mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-02-03 13:47:55 +01:00
chore: add more composables (#138)
This commit is contained in:
committed by
GitHub
parent
fef93f3198
commit
fd4b608150
@@ -128,6 +128,16 @@
|
|||||||
<UButton icon="i-heroicons-bell" variant="red" label="Trigger an error" @click="onNotificationClick" />
|
<UButton icon="i-heroicons-bell" variant="red" label="Trigger an error" @click="onNotificationClick" />
|
||||||
</div>
|
</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>
|
||||||
<div class="font-medium text-sm mb-1 u-text-gray-700">
|
<div class="font-medium text-sm mb-1 u-text-gray-700">
|
||||||
Context menu:
|
Context menu:
|
||||||
@@ -230,6 +240,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
const isModalOpen = ref(false)
|
const isModalOpen = ref(false)
|
||||||
|
const textToCopy = ref('Copied text')
|
||||||
|
|
||||||
const people = ref([
|
const people = ref([
|
||||||
{ id: 1, name: 'Durward Reynolds', disabled: false },
|
{ id: 1, name: 'Durward Reynolds', disabled: false },
|
||||||
@@ -251,7 +262,8 @@ const form = reactive({
|
|||||||
persons: ref([people.value[0]])
|
persons: ref([people.value[0]])
|
||||||
})
|
})
|
||||||
|
|
||||||
const { $toast } = useNuxtApp()
|
const toast = useToast()
|
||||||
|
const clipboard = useCopyToClipboard()
|
||||||
|
|
||||||
const x = ref(0)
|
const x = ref(0)
|
||||||
const y = ref(0)
|
const y = ref(0)
|
||||||
@@ -346,6 +358,10 @@ const customDropdownItems = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const onNotificationClick = () => {
|
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>
|
</script>
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
"@vueuse/core": "^9.13.0",
|
"@vueuse/core": "^9.13.0",
|
||||||
"@vueuse/integrations": "^9.13.0",
|
"@vueuse/integrations": "^9.13.0",
|
||||||
|
"@vueuse/math": "^9.13.0",
|
||||||
"defu": "^6.1.2",
|
"defu": "^6.1.2",
|
||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^6.6.2",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
|
|||||||
@@ -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 { defu } from 'defu'
|
||||||
import colors from 'tailwindcss/colors.js'
|
import colors from 'tailwindcss/colors.js'
|
||||||
import type { Config } from 'tailwindcss'
|
import type { Config } from 'tailwindcss'
|
||||||
@@ -155,9 +155,6 @@ export default defineNuxtModule<ModuleOptions>({
|
|||||||
cssPath: resolve(runtimeDir, 'tailwind.css')
|
cssPath: resolve(runtimeDir, 'tailwind.css')
|
||||||
})
|
})
|
||||||
|
|
||||||
addPlugin(resolve(runtimeDir, 'plugins', 'toast.client'))
|
|
||||||
addPlugin(resolve(runtimeDir, 'plugins', 'clipboard.client'))
|
|
||||||
|
|
||||||
addComponentsDir({
|
addComponentsDir({
|
||||||
path: resolve(runtimeDir, 'components', 'elements'),
|
path: resolve(runtimeDir, 'components', 'elements'),
|
||||||
prefix,
|
prefix,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
v-bind="notification"
|
v-bind="notification"
|
||||||
:class="notification.click && 'cursor-pointer'"
|
:class="notification.click && 'cursor-pointer'"
|
||||||
@click="notification.click && notification.click(notification)"
|
@click="notification.click && notification.click(notification)"
|
||||||
@close="$toast.removeNotification(notification.id)"
|
@close="toast.removeNotification(notification.id)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -18,10 +18,11 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ToastNotification } from '../../types'
|
import type { ToastNotification } from '../../types'
|
||||||
|
import { useToast } from '../../composables/useToast'
|
||||||
import Notification from './Notification.vue'
|
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', () => [])
|
const notifications = useState<ToastNotification[]>('notifications', () => [])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
109
src/runtime/composables/defineShortcuts.ts
Normal file
109
src/runtime/composables/defineShortcuts.ts
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useClipboard } from '@vueuse/core'
|
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 { copy: copyToClipboard, isSupported } = useClipboard()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
function copy (text: string, success: { title?: string, description?: string } = {}, failure: { title?: string, description?: string } = {}) {
|
function copy (text: string, success: { title?: string, description?: string } = {}, failure: { title?: string, description?: string } = {}) {
|
||||||
if (!isSupported) {
|
if (!isSupported) {
|
||||||
@@ -14,9 +15,9 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nuxtApp.$toast.success(success)
|
toast.success(success)
|
||||||
}, function (e) {
|
}, function (e) {
|
||||||
nuxtApp.$toast.error({
|
toast.error({
|
||||||
...failure,
|
...failure,
|
||||||
description: failure.description || e.message
|
description: failure.description || e.message
|
||||||
})
|
})
|
||||||
@@ -24,10 +25,6 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
provide: {
|
copy
|
||||||
clipboard: {
|
|
||||||
copy
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
32
src/runtime/composables/useShortcuts.ts
Normal file
32
src/runtime/composables/useShortcuts.ts
Normal 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)
|
||||||
41
src/runtime/composables/useToast.ts
Normal file
41
src/runtime/composables/useToast.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { defineNuxtPlugin, useState } from '#app'
|
|
||||||
import type { ToastNotification } from '../types'
|
|
||||||
|
|
||||||
export default defineNuxtPlugin(() => {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
provide: {
|
|
||||||
toast: {
|
|
||||||
addNotification,
|
|
||||||
removeNotification,
|
|
||||||
success (notification: Partial<ToastNotification> = {}) {
|
|
||||||
return addNotification({ type: 'success', ...notification })
|
|
||||||
},
|
|
||||||
info (notification: Partial<ToastNotification> = {}) {
|
|
||||||
return addNotification({ type: 'info', ...notification })
|
|
||||||
},
|
|
||||||
warning (notification: Partial<ToastNotification> = {}) {
|
|
||||||
return addNotification({ type: 'warning', ...notification })
|
|
||||||
},
|
|
||||||
error (notification: Partial<ToastNotification>) {
|
|
||||||
return addNotification({ type: 'error', title: 'An error occurred!', ...notification })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
7
src/runtime/types/toast.d.ts
vendored
7
src/runtime/types/toast.d.ts
vendored
@@ -14,10 +14,3 @@ export interface ToastNotification {
|
|||||||
click?: Function
|
click?: Function
|
||||||
callback?: 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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1827,6 +1827,14 @@
|
|||||||
"@vueuse/shared" "9.13.0"
|
"@vueuse/shared" "9.13.0"
|
||||||
vue-demi "*"
|
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":
|
"@vueuse/metadata@9.13.0":
|
||||||
version "9.13.0"
|
version "9.13.0"
|
||||||
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-9.13.0.tgz#bc25a6cdad1b1a93c36ce30191124da6520539ff"
|
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-9.13.0.tgz#bc25a6cdad1b1a93c36ce30191124da6520539ff"
|
||||||
|
|||||||
Reference in New Issue
Block a user