oui-kit
Version:
🎯 *UI toolkit with a French touch* 🇫🇷
129 lines (106 loc) • 3.72 kB
text/typescript
import type { LoggerInterface } from 'zeed'
import type { OuiMenuItem } from './_types'
import { isRecord, Logger } from 'zeed'
import { mountComponentAsApp } from '../basic/app-helper'
import OuiMenu from './oui-menu.vue'
const log: LoggerInterface = Logger('use-menu')
type OuiMenuCreator<T = any> = (value: T, ...args: any) => OuiMenuItem[]
type OuiMenuItemSource<T = any> = (OuiMenuItem | false | undefined | null)[] | OuiMenuCreator<T>
function generateGetBoundingClientRect(x = 0, y = 0) {
return () => ({
width: 0,
height: 0,
top: y,
right: x,
bottom: y,
left: x,
} as DOMRect)
}
function isSeparator(item: any) {
return item === null || (isRecord(item) && Object.keys(item).length === 0)
}
/**
* Context menu emulation.
*
* If triggered by BUTTON it will show as a drop down, else close to the mouse position.
*/
export function useMenu<T = any>(itemsSource: OuiMenuItemSource<T>) {
let app: any
// todo this would require to use it top level always
// onBeforeUnmount(() => app?.done())
let lastX: number, lastY: number, lastReference: any
return (...args: any) => {
log('useMenu trigger', args)
// Find and handle event
const event = args.find((a: any) => a instanceof Event) as MouseEvent
const { clientX: x, clientY: y, target } = event
// Find reference element. Use containing BUTTON if available
let reference: HTMLElement | undefined | null
let cursorElement: HTMLElement | null = target as any
while (cursorElement) {
if (cursorElement.tagName?.toUpperCase() === 'BUTTON') {
reference = cursorElement
break
}
cursorElement = cursorElement.parentElement
}
// No element? Then narrow down the coordinates on screen.
if (reference == null && target != null) {
reference = target as any
reference!.getBoundingClientRect = generateGetBoundingClientRect(x + 4, y + 4)
}
// cursor?.classList.add("menuVisible") // todo highlight selected item
// Nothing more to be done with this event
event.stopPropagation()
event.preventDefault()
// Second click closes, this is like macOS does it as well
if (app != null && (reference?.isSameNode(lastReference) || (x === lastX && y === lastY))) {
log('close on second click')
lastX = -1
lastY = -1
lastReference = undefined
app?.done()
return
}
// Items with empty ones filted out
const items = (
typeof itemsSource === 'function'
? (itemsSource as any)(...args)
: itemsSource
) .filter((item: any) => item != null && item !== false)
// Cleanup separators at ends and multiple
for (let i = items.length - 1; i >= 0; i--) {
if (isSeparator(items[i])) {
if (i === 0 || i === items.length - 1 || isSeparator(items[i - 1]) || isSeparator(items[i + 1]))
items.splice(i, 1)
}
}
log('items', items)
// No item? Don't show.
if (items.length <= 0)
return
// We have a hook? Then show the menu eventually
if (reference) {
lastX = x
lastY = y
lastReference = reference
app?.done()
app = mountComponentAsApp(OuiMenu, {
items,
reference,
args,
})
app.awaitDone.then(() => (app = undefined))
}
else {
log.warn('useMenu target missing')
}
}
}
/** Menu function where an argument can be passed, like: `const menu = useMenuWithValue(value => [...])` then in HTML `v-menu="menu(item)"` */
export function useMenuWithValue<T = any>(itemsSource: OuiMenuCreator<T>) {
const menu = useMenu(itemsSource)
return (value: T) => {
return (...args: any) => menu(value, ...args)
}
}