UNPKG

rabbit-simple-ui

Version:

A simple UI component library based on JavaScript

317 lines (272 loc) β€’ 11.2 kB
/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { $el, bind, getBooleanTypeAttr, getStrTypeAttr, removeAttrs, setCss } from '../../dom-utils'; import { clickoutside, CssTransition, warn, _Popper } from '../../mixins'; import { type, validComps } from '../../utils'; interface Config { config( el: string ): { visible: boolean; events: ({ onClick, onVisibleChange }: DropdownEvents) => void; }; } interface DropdownEvents { onClick?: (key: string) => void; onVisibleChange?: (visible: boolean) => void; onClickOutside?: (event: Event) => void; } const DEFAULTDELAY = 80; const STATEKEY = 'visibleState'; const ITEMKEY = 'itemKey'; const DROPENTERCLS = 'transition-drop-enter'; const DROPLEAVECLS = 'transition-drop-leave'; let VISIBLETIMER: any = null, EVENTTIMER: any = null; class Dropdown implements Config { readonly VERSION: string; readonly COMPONENTS: NodeListOf<Element>; constructor() { this.VERSION = 'v2.0'; this.COMPONENTS = $el('r-dropdown', { all: true }); this._create(this.COMPONENTS); } public config( el: string ): { visible: boolean; events({ onClick, onVisibleChange, onClickOutside }: DropdownEvents): void; } { const target = $el(el) as HTMLElement; validComps(target, 'dropdown'); const { _attrs, _setVisible, _getChildDisabled } = Dropdown.prototype; const { trigger, placement } = _attrs(target); const DropdownRefElm = target.firstElementChild!; const DropdownMenu = target.querySelector('r-dropdown-menu')! as HTMLElement; const DropdownItem = DropdownMenu.querySelectorAll('r-dropdown-item'); return { get visible() { return DropdownMenu.dataset[STATEKEY] === 'visible'; }, set visible(newVal: boolean) { if (newVal && !type.isBol(newVal)) return; _setVisible(target, DropdownMenu, newVal, placement); }, events({ onClick, onVisibleChange, onClickOutside }) { // onVisibleChange const visibleChange = () => { setTimeout(() => { const visible = DropdownMenu.dataset[STATEKEY] === 'visible'; onVisibleChange && type.isFn(onVisibleChange, visible); }, DEFAULTDELAY); }; // onClick const itemClickEv = (elem: Element) => { if (_getChildDisabled(elem)) return false; // @ts-ignore const key = elem.dataset[ITEMKEY]; visibleChange(); onClick && type.isFn(onClick, key); }; if (trigger === 'hover') { bind(target, 'mouseenter', () => { if (EVENTTIMER) clearTimeout(EVENTTIMER); EVENTTIMER = setTimeout(visibleChange, DEFAULTDELAY); }); bind(target, 'mouseleave', () => { if (EVENTTIMER) clearTimeout(EVENTTIMER); if (DropdownMenu.dataset[STATEKEY] === 'visible') setTimeout(visibleChange, DEFAULTDELAY); }); } if (trigger === 'click' || trigger === 'contextMenu') { onClickOutside && clickoutside(target, onClickOutside, DropdownMenu, STATEKEY, 'visible'); } if (trigger === 'click') { bind(DropdownRefElm, 'click', visibleChange); } if (trigger === 'contextMenu') { bind(DropdownRefElm, 'contextmenu', visibleChange); } DropdownItem.forEach((child) => bind(child, 'click', () => itemClickEv(child))); } }; } private _create(COMPONENTS: NodeListOf<Element>): void { COMPONENTS.forEach((node) => { if (!this._correctCompositionNodes(node)) return; const { trigger, placement, visible, stopPropagation } = this._attrs(node); const DropdownMenu = node.querySelector('r-dropdown-menu')! as HTMLElement; const DropdownItem = DropdownMenu.querySelector('r-dropdown-item')! as HTMLElement; const { key } = this._attrs(DropdownItem); this._setVisible(node, DropdownMenu, visible, placement); this._setChildKey(DropdownItem, key); this._setStopPropagation(stopPropagation, node, DropdownMenu); this._handleTrigger(trigger, placement, node, DropdownMenu); this._handleItemClick(trigger, node, DropdownMenu, placement); removeAttrs(node, ['key', 'trigger', 'placement', 'visible', 'stop-propagation']); }); } private _correctCompositionNodes(node: Element): boolean { if (node.firstElementChild?.tagName === 'R-DROPDOWN-MENU') { warn( 'πŸ‘‡ The first child element must be the reference element used to trigger the menu display hidden, not r-dropdown-menu' ); console.error(node); return false; } if (node.lastElementChild!.tagName !== 'R-DROPDOWN-MENU') { warn('πŸ‘‡ The last child element tag must be made up of r-dropdown-menu'); console.error(node); return false; } if (node.childElementCount > 2) { warn('πŸ‘‡ The number of child element nodes in this r-dropdown tag cannot exceed two'); console.error(node); return false; } return true; } private _setStopPropagation(stop: boolean, node: Element, child: HTMLElement): void { if (!stop) return; bind(node, 'click', (e: MouseEvent) => e.stopPropagation()); bind(child, 'click', (e: MouseEvent) => e.stopPropagation()); } private _handleTrigger( type: string, placement: string, node: Element, child: HTMLElement ): void { if (type === 'custom') return; const referenceElem = node.firstElementChild!; // θ§¦ε‘θœε•ζ˜Ύη€Ίιšθ—ηš„εΌ•η”¨ε…ƒη΄ ε¦‚ζžœζ˜―η¦η”¨ηŠΆζ€εˆ™δΈεšζ“δ½œ if (/disabled/.test(referenceElem.className)) return; if (this._getChildDisabled(referenceElem)) return; const showMenu = () => { if (VISIBLETIMER) clearTimeout(VISIBLETIMER); if (child.dataset[STATEKEY] === 'visible') return; VISIBLETIMER = setTimeout( () => this._setVisible(node, child, true, placement), DEFAULTDELAY ); }; const hidenMenu = () => { if (VISIBLETIMER) clearTimeout(VISIBLETIMER); if (child.dataset[STATEKEY] === 'visible') { setTimeout(() => this._setVisible(node, child, false, placement), DEFAULTDELAY); } }; const clickIsShow = (e: MouseEvent) => { e.stopPropagation(); if (child.dataset[STATEKEY] === 'hidden') { showMenu(); } else { hidenMenu(); } }; if (type === 'hover') { bind(node, 'mouseenter', showMenu); bind(node, 'mouseleave', hidenMenu); } // η‚Ήε‡»θœε•ζ δ»₯ε€–ηš„εœ°ζ–Ήιšθ— if (type === 'click' || type === 'contextMenu') { clickoutside(node, hidenMenu); } if (type === 'click') { bind(referenceElem, 'click', (e: MouseEvent) => clickIsShow(e)); } if (type === 'contextMenu') { bind(referenceElem, 'contextmenu', (e: MouseEvent) => { e.preventDefault(); clickIsShow(e); }); } } private _handleItemClick( type: string, node: Element, child: HTMLElement, placement: string ): void { if (type === 'custom') return; const DropdownItems = child.querySelectorAll('r-dropdown-item'); DropdownItems.forEach((item) => bind(item, 'click', () => { if (this._getChildDisabled(item)) return; this._setVisible(node, child, false, placement); }) ); } private _setChildKey(child: HTMLElement, key: string): void { if (key) { child.dataset[ITEMKEY] = key; child.removeAttribute('key'); } } private _setVisible( node: Element, child: HTMLElement, visible: boolean, placement: string ): void { const { _setPlacement, _setTransitionDrop } = Dropdown.prototype; if (visible) { child.dataset[STATEKEY] = 'visible'; _setPlacement(node, child, placement); _setTransitionDrop('in', child); } else { child.dataset[STATEKEY] = 'hidden'; setTimeout(() => { child.dataset[STATEKEY] === 'hidden' && _setTransitionDrop('out', child); }, 0); } } private _setPlacement(node: Element, child: HTMLElement, placement: string): void { const popperPlacement = child.dataset['popperPlacement'] || placement; if (/^top|right-end|left-end/.test(popperPlacement)) { setCss(child, 'transformOrigin', 'center bottom'); } if (/^bottom|right-start|left-start/.test(popperPlacement)) { setCss(child, 'transformOrigin', 'center top'); } _Popper._newCreatePopper(node, child, placement, 0); } private _setTransitionDrop(type: 'in' | 'out', child: HTMLElement): void { const transitionCls = type === 'in' ? { enterCls: DROPENTERCLS } : { leaveCls: DROPLEAVECLS }; CssTransition(child, { inOrOut: type, ...transitionCls, rmCls: true, timeout: 290 }); } private _getChildDisabled(elem: Element): boolean { if ( elem.getAttribute('disabled') === 'disabled' || elem.getAttribute('disabled') === 'true' || elem.getAttribute('disabled') === '' ) { return true; } return false; } private _attrs(node: Element) { return { key: getStrTypeAttr(node, 'key', ''), trigger: getStrTypeAttr(node, 'trigger', 'hover'), placement: getStrTypeAttr(node, 'placement', 'bottom'), visible: getBooleanTypeAttr(node, 'visible'), stopPropagation: getBooleanTypeAttr(node, 'stop-propagation') }; } } export default Dropdown;