Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add more composables #138

Merged
merged 2 commits into from
Mar 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions docs/pages/examples.vue
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,16 @@
<UButton icon="i-heroicons-bell" variant="red" label="Trigger an error" @click="onNotificationClick" />
</div>

<div>
<div class="font-medium text-sm mb-1 u-text-gray-700">
Copy text to clipboard:
</div>
<div class="flex gap-2">
<UInput v-model="textToCopy" name="textToCopy" />
<UButton icon="i-heroicons-document-duplicate-solid" variant="primary" label="Copy text" @click="onCopyTextClick" />
</div>
</div>

<div>
<div class="font-medium text-sm mb-1 u-text-gray-700">
Context menu:
Expand Down Expand Up @@ -230,6 +240,7 @@

<script setup>
const isModalOpen = ref(false)
const textToCopy = ref('Copied text')

const people = ref([
{ id: 1, name: 'Durward Reynolds', disabled: false },
Expand All @@ -251,7 +262,8 @@ const form = reactive({
persons: ref([people.value[0]])
})

const { $toast } = useNuxtApp()
const toast = useToast()
const clipboard = useCopyToClipboard()

const x = ref(0)
const y = ref(0)
Expand Down Expand Up @@ -346,6 +358,10 @@ const customDropdownItems = [
]

const onNotificationClick = () => {
$toast.error({ title: 'Error', description: 'This is an error message' })
toast.error({ title: 'Error', description: 'This is an error message' })
}

const onCopyTextClick = () => {
clipboard.copy(textToCopy.value, { title: 'Text copied successfully!' })
}
</script>
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@tailwindcss/typography": "^0.5.9",
"@vueuse/core": "^9.13.0",
"@vueuse/integrations": "^9.13.0",
"@vueuse/math": "^9.13.0",
"defu": "^6.1.2",
"fuse.js": "^6.6.2",
"lodash-es": "^4.17.21",
Expand Down
5 changes: 1 addition & 4 deletions src/module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineNuxtModule, installModule, addComponentsDir, addImportsDir, addTemplate, addPlugin, createResolver } from '@nuxt/kit'
import { defineNuxtModule, installModule, addComponentsDir, addImportsDir, addTemplate, createResolver } from '@nuxt/kit'
import { defu } from 'defu'
import colors from 'tailwindcss/colors.js'
import type { Config } from 'tailwindcss'
Expand Down Expand Up @@ -155,9 +155,6 @@ export default defineNuxtModule<ModuleOptions>({
cssPath: resolve(runtimeDir, 'tailwind.css')
})

addPlugin(resolve(runtimeDir, 'plugins', 'toast.client'))
addPlugin(resolve(runtimeDir, 'plugins', 'clipboard.client'))

addComponentsDir({
path: resolve(runtimeDir, 'components', 'elements'),
prefix,
Expand Down
7 changes: 4 additions & 3 deletions src/runtime/components/overlays/Notifications.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
v-bind="notification"
:class="notification.click && 'cursor-pointer'"
@click="notification.click && notification.click(notification)"
@close="$toast.removeNotification(notification.id)"
@close="toast.removeNotification(notification.id)"
/>
</div>
</div>
Expand All @@ -18,10 +18,11 @@

<script setup lang="ts">
import type { ToastNotification } from '../../types'
import { useToast } from '../../composables/useToast'
import Notification from './Notification.vue'
import { useNuxtApp, useState } from '#imports'
import { useState } from '#imports'

const { $toast } = useNuxtApp()
const toast = useToast()
const notifications = useState<ToastNotification[]>('notifications', () => [])
</script>

Expand Down
109 changes: 109 additions & 0 deletions src/runtime/composables/defineShortcuts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import type { Ref, ComputedRef } from 'vue'
import { logicAnd, logicNot } from '@vueuse/math'
import { onMounted, onBeforeUnmount } from 'vue'
import { useShortcuts } from './useShortcuts'

export interface ShortcutConfig {
handler: Function
usingInput?: string | boolean
whenever?: Ref<Boolean>[]
}

export interface ShortcutsConfig {
[key: string]: ShortcutConfig | Function
}

interface Shortcut {
handler: Function
condition: ComputedRef<Boolean>
// KeyboardEvent attributes
key: string
ctrlKey: boolean
metaKey: boolean
shiftKey: boolean
altKey: boolean
// code?: string
// keyCode?: number
}

export const defineShortcuts = (config: ShortcutsConfig) => {
const { macOS, usingInput } = useShortcuts()

let shortcuts: Shortcut[] = []

const onKeyDown = (e: KeyboardEvent) => {
const alphabeticalKey = /^[a-z]{1}$/.test(e.key)

for (const shortcut of shortcuts) {
if (e.key.toLowerCase() !== shortcut.key) { continue }
if (e.metaKey !== shortcut.metaKey) { continue }
if (e.ctrlKey !== shortcut.ctrlKey) { continue }
// shift modifier is only checked in combination with alphabetical keys
// (shift with non-alphabetical keys would change the key)
if (alphabeticalKey && e.shiftKey !== shortcut.shiftKey) { continue }
// alt modifier changes the combined key anyways
// if (e.altKey !== shortcut.altKey) { continue }

if (shortcut.condition.value) {
e.preventDefault()
shortcut.handler()
}
return
}
}

onMounted(() => {
// Map config to full detailled shortcuts
shortcuts = Object.entries(config).map(([key, shortcutConfig]) => {
if (!shortcutConfig) {
return null
}

// Parse key and modifiers
const keySplit = key.toLowerCase().split('_').map(k => k)
let shortcut: Partial<Shortcut> = {
key: keySplit.filter(k => !['meta', 'ctrl', 'shift', 'alt'].includes(k)).join('_'),
metaKey: keySplit.includes('meta'),
ctrlKey: keySplit.includes('ctrl'),
shiftKey: keySplit.includes('shift'),
altKey: keySplit.includes('alt')
}

// Convert Meta to Ctrl for non-MacOS
if (!macOS.value && shortcut.metaKey && !shortcut.ctrlKey) {
shortcut.metaKey = false
shortcut.ctrlKey = true
}

// Retrieve handler function
if (typeof shortcutConfig === 'function') {
shortcut.handler = shortcutConfig
} else if (typeof shortcutConfig === 'object') {
shortcut = { ...shortcut, handler: shortcutConfig.handler }
}

if (!shortcut.handler) {
// eslint-disable-next-line no-console
console.trace('[Shortcut] Invalid value')
return null
}

// Create shortcut computed
const conditions = []
if (!(shortcutConfig as ShortcutConfig).usingInput) {
conditions.push(logicNot(usingInput))
} else if (typeof (shortcutConfig as ShortcutConfig).usingInput === 'string') {
conditions.push(computed(() => usingInput.value === (shortcutConfig as ShortcutConfig).usingInput))
}
shortcut.condition = logicAnd(...conditions, ...((shortcutConfig as ShortcutConfig).whenever || []))

return shortcut as Shortcut
}).filter(Boolean) as Shortcut[]

document.addEventListener('keydown', onKeyDown)
})

onBeforeUnmount(() => {
document.removeEventListener('keydown', onKeyDown)
})
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useClipboard } from '@vueuse/core'
import { defineNuxtPlugin } from '#app'
import { useToast } from './useToast'

export default defineNuxtPlugin((nuxtApp) => {
export function useCopyToClipboard () {
const { copy: copyToClipboard, isSupported } = useClipboard()
const toast = useToast()

function copy (text: string, success: { title?: string, description?: string } = {}, failure: { title?: string, description?: string } = {}) {
if (!isSupported) {
Expand All @@ -14,20 +15,16 @@ export default defineNuxtPlugin((nuxtApp) => {
return
}

nuxtApp.$toast.success(success)
toast.success(success)
}, function (e) {
nuxtApp.$toast.error({
toast.error({
...failure,
description: failure.description || e.message
})
})
}

return {
provide: {
clipboard: {
copy
}
}
copy
}
})
}
32 changes: 32 additions & 0 deletions src/runtime/composables/useShortcuts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { createSharedComposable, useActiveElement } from '@vueuse/core'
import { ref, computed, onMounted } from 'vue'

export const _useShortcuts = () => {
const macOS = computed(() => process.client && navigator && navigator.userAgent && navigator.userAgent.match(/Macintosh;/))

const metaSymbol = ref(' ')

const activeElement = useActiveElement()
const usingInput = computed(() => {
const usingInput = !!(activeElement.value?.tagName === 'INPUT' || activeElement.value?.tagName === 'TEXTAREA' || activeElement.value?.contentEditable === 'true')

if (usingInput) {
return ((activeElement.value as any)?.name as string) || true
}

return false
})

onMounted(() => {
metaSymbol.value = macOS.value ? '⌘' : 'Ctrl'
})

return {
macOS,
metaSymbol,
activeElement,
usingInput
}
}

export const useShortcuts = createSharedComposable(_useShortcuts)
41 changes: 41 additions & 0 deletions src/runtime/composables/useToast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { ToastNotification } from '../types'
import { useState } from '#imports'

export function useToast () {
const notifications = useState<ToastNotification[]>('notifications', () => [])

function addNotification (notification: Partial<ToastNotification>) {
const body = {
id: new Date().getTime().toString(),
...notification
}

const index = notifications.value.findIndex((n: ToastNotification) => n.id === body.id)
if (index === -1) {
notifications.value.push(body as ToastNotification)
}

return body
}

function removeNotification (id: string) {
notifications.value = notifications.value.filter((n: ToastNotification) => n.id !== id)
}

const success = (notification: Partial<ToastNotification> = {}) => addNotification({ type: 'success', ...notification })

const info = (notification: Partial<ToastNotification> = {}) => addNotification({ type: 'info', ...notification })

const warning = (notification: Partial<ToastNotification> = {}) => addNotification({ type: 'warning', ...notification })

const error = (notification: Partial<ToastNotification>) => addNotification({ type: 'error', title: 'An error occurred!', ...notification })

return {
addNotification,
removeNotification,
success,
info,
warning,
error
}
}
45 changes: 0 additions & 45 deletions src/runtime/plugins/toast.client.ts

This file was deleted.

7 changes: 0 additions & 7 deletions src/runtime/types/toast.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,3 @@ export interface ToastNotification {
click?: Function
callback?: Function
}

export interface ToastPlugin {
addNotification: (notification: Partial<Notification>) => Notification
removeNotification: (id: string) => void
success: (options: { title?: string, description?: string }) => void
error: (options: { title?: string, description?: string }) => void
}
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1827,6 +1827,14 @@
"@vueuse/shared" "9.13.0"
vue-demi "*"

"@vueuse/math@^9.13.0":
version "9.13.0"
resolved "https://registry.yarnpkg.com/@vueuse/math/-/math-9.13.0.tgz#3b4890dd80035b923195a725e2af73470a16bddf"
integrity sha512-FE2n8J1AfBb4dNvNyE6wS+l87XDcC/y3/037AmrwonsGD5QwJJl6rGr57idszs3PXTuEYcEkDysHLxstSxbQEg==
dependencies:
"@vueuse/shared" "9.13.0"
vue-demi "*"

"@vueuse/metadata@9.13.0":
version "9.13.0"
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-9.13.0.tgz#bc25a6cdad1b1a93c36ce30191124da6520539ff"
Expand Down