chore: add more composables (#138)

This commit is contained in:
Sylvain Marroufin
2023-03-09 11:42:22 +01:00
committed by GitHub
parent fef93f3198
commit fd4b608150
11 changed files with 221 additions and 71 deletions

View File

@@ -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)
})
}

View File

@@ -0,0 +1,30 @@
import { useClipboard } from '@vueuse/core'
import { useToast } from './useToast'
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) {
return
}
copyToClipboard(text).then(() => {
if (!success.title && !success.description) {
return
}
toast.success(success)
}, function (e) {
toast.error({
...failure,
description: failure.description || e.message
})
})
}
return {
copy
}
}

View File

@@ -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)

View File

@@ -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
}
}