diff --git a/libs/horizon/src/event/ControlledValueUpdater.ts b/libs/horizon/src/event/ControlledValueUpdater.ts new file mode 100644 index 00000000..dde7f99b --- /dev/null +++ b/libs/horizon/src/event/ControlledValueUpdater.ts @@ -0,0 +1,39 @@ +import {getVNodeProps} from '../dom/DOMInternalKeys'; +import {resetValue} from '../dom/valueHandler'; +import {getDomTag} from '../dom/utils/Common'; + +let updateList = null; + +// 受控组件值重新赋值 +function updateValue(target: Element) { + const props = getVNodeProps(target); + if (props) { + const type = getDomTag(target); + resetValue(target, type, props); + } +} + +// 存储队列中缓存组件 +export function addValueUpdateList(target: EventTarget): void { + if (updateList) { + updateList.push(target); + } else { + updateList = [target]; + } +} + +// 判断是否需要重新赋值 +export function shouldUpdateValue(): boolean { + return updateList !== null && updateList.length > 0; +} + +// 从缓存队列中对受控组件进行赋值 +export function updateControlledValue() { + if (!updateList) { + return; + } + updateList.forEach(item => { + updateValue(item); + }); + updateList = null; +} diff --git a/libs/horizon/src/event/EventBinding.ts b/libs/horizon/src/event/EventBinding.ts new file mode 100644 index 00000000..068280ea --- /dev/null +++ b/libs/horizon/src/event/EventBinding.ts @@ -0,0 +1,120 @@ +/** + * 事件绑定实现 + */ +import {allDelegatedNativeEvents} from './EventCollection'; +import {isDocument} from '../dom/utils/Common'; +import { + getEventListeners, + getEventToListenerMap, +} from '../dom/DOMInternalKeys'; +import {createCustomEventListener} from './WrapperListener'; +import {CustomBaseEvent} from './customEvents/CustomBaseEvent'; + +const listeningMarker = + '_horizonListening' + + Math.random() + .toString(36) + .slice(4); + +// 获取节点上已经委托事件名称 +function getListenerSetKey(nativeEvtName: string, isCapture: boolean): string { + const sufix = isCapture ? 'capture' : 'bubble'; + return `${nativeEvtName}__${sufix}`; +} + +function listenToNativeEvent( + nativeEvtName: string, + delegatedElement: Element, + isCapture: boolean, +): void { + let target: Element | Document = delegatedElement; + // document层次可能触发selectionchange事件,为了捕获这类事件,selectionchange事件绑定在document节点上 + if (nativeEvtName === 'selectionchange' && !isDocument(delegatedElement)) { + target = delegatedElement.ownerDocument; + } + + const listenerSet = getEventListeners(target); + const listenerSetKey = getListenerSetKey(nativeEvtName, isCapture); + + if (!listenerSet.has(listenerSetKey)) { + const listener = createCustomEventListener( + target, + nativeEvtName, + isCapture, + ); + target.addEventListener(nativeEvtName, listener, !!isCapture); + listenerSet.add(listenerSetKey); + } +} + +// 监听所有委托事件 +export function listenDelegatedEvents(dom: Element) { + if (dom[listeningMarker]) { + // 不需要重复注册事件 + return; + } + dom[listeningMarker] = true; + allDelegatedNativeEvents.forEach((eventName: string) => { + // 委托冒泡事件 + listenToNativeEvent(eventName, dom, false); + // 委托捕获事件 + listenToNativeEvent(eventName, dom, true); + }); +} + +// 通过horizon事件名获取到native事件名 +function getNativeEvtName(horizonEventName, capture) { + let nativeName; + if (capture) { + nativeName = horizonEventName.slice(2, -7); + } else { + nativeName = horizonEventName.slice(2); + } + if (!nativeName) { + return ''; + } + return nativeName.toLowerCase(); +} + +// 是否捕获事件 +function getIsCapture(horizonEventName) { + if (horizonEventName === 'onLostPointerCapture' || horizonEventName === 'onGotPointerCapture') { + return false; + } + return horizonEventName.slice(-7) === 'Capture'; +} + +// 封装监听函数 +function getWrapperListener(horizonEventName, nativeEvtName, targetElement, listener) { + return (event) => { + const customEvent = new CustomBaseEvent(horizonEventName, nativeEvtName, event, null, targetElement); + listener(customEvent); + }; +} + +// 非委托事件单独监听到各自dom节点 +export function listenNonDelegatedEvent( + horizonEventName: string, + domElement: Element, + listener, +): void { + const isCapture = getIsCapture(horizonEventName); + const nativeEvtName = getNativeEvtName(horizonEventName, isCapture); + + // 先判断是否存在老的监听事件,若存在则移除 + const eventToListenerMap = getEventToListenerMap(domElement); + if (eventToListenerMap.get(horizonEventName)) { + domElement.removeEventListener(nativeEvtName, eventToListenerMap.get(horizonEventName)); + } + + if (typeof listener !== 'function') { + eventToListenerMap.delete(nativeEvtName); + return; + } + + // 为了和委托事件对外行为一致,将事件对象封装成CustomBaseEvent + const wrapperListener = getWrapperListener(horizonEventName, nativeEvtName, domElement, listener); + // 添加新的监听 + eventToListenerMap.set(horizonEventName, wrapperListener); + domElement.addEventListener(nativeEvtName, wrapperListener, isCapture); +} diff --git a/libs/horizon/src/event/EventCollection.ts b/libs/horizon/src/event/EventCollection.ts new file mode 100644 index 00000000..8163df63 --- /dev/null +++ b/libs/horizon/src/event/EventCollection.ts @@ -0,0 +1,14 @@ +import {horizonEventToNativeMap} from './const'; + +// 需要委托的horizon事件和原生事件对应关系 +export const allDelegatedHorizonEvents = new Map(); +// 所有委托的原生事件集合 +export const allDelegatedNativeEvents = new Set(); + +horizonEventToNativeMap.forEach((dependencies, horizonEvent) => { + allDelegatedHorizonEvents.set(horizonEvent, dependencies); + allDelegatedHorizonEvents.set(horizonEvent + 'Capture', dependencies); + dependencies.forEach(d => { + allDelegatedNativeEvents.add(d); + }); +}); diff --git a/libs/horizon/src/event/const.ts b/libs/horizon/src/event/const.ts new file mode 100644 index 00000000..2274bc03 --- /dev/null +++ b/libs/horizon/src/event/const.ts @@ -0,0 +1,83 @@ +import { + STYLE_AMT_END, + STYLE_AMT_ITERATION, + STYLE_AMT_START, + STYLE_TRANS_END +} from './StyleEventNames'; + +// Horizon事件和原生事件对应关系 +export const horizonEventToNativeMap = new Map([ + ['onKeyPress', ['keypress']], + ['onTextInput', ['textInput']], + ['onClick', ['click']], + ['onDoubleClick', ['dblclick']], + ['onFocus', ['focusin']], + ['onBlur', ['focusout']], + ['onInput', ['input']], + ['onMouseOut', ['mouseout']], + ['onMouseOver', ['mouseover']], + ['onPointerOut', ['pointerout']], + ['onPointerOver', ['pointerover']], + ['onContextMenu', ['contextmenu']], + ['onDragEnd', ['dragend']], + ['onKeyDown', ['keydown']], + ['onKeyUp', ['keyup']], + ['onMouseDown', ['mousedown']], + ['onMouseMove', ['mousemove']], + ['onMouseUp', ['mouseup']], + ['onSelectChange', ['selectionchange']], + ['onTouchEnd', ['touchend']], + ['onTouchMove', ['touchmove']], + ['onTouchStart', ['touchstart']], + + ['onCompositionEnd', ['compositionend']], + ['onCompositionStart', ['compositionstart']], + ['onCompositionUpdate', ['compositionupdate']], + ['onBeforeInput', ['compositionend', 'keypress', 'textInput']], + ['onChange', ['change', 'click', 'focusout', 'input',]], + ['onSelect', ['focusout', 'contextmenu', 'dragend', 'focusin', 'keydown', 'keyup', 'mousedown', 'mouseup', 'selectionchange']], + + ['onAnimationEnd', [STYLE_AMT_END]], + ['onAnimationIteration', [STYLE_AMT_ITERATION]], + ['onAnimationStart', [STYLE_AMT_START]], + ['onTransitionEnd', [STYLE_TRANS_END]] +]); + +export const CommonEventToHorizonMap = { + click: 'click', + dblclick: 'doubleClick', + contextmenu: 'contextMenu', + dragend: 'dragEnd', + focusin: 'focus', + focusout: 'blur', + input: 'input', + keydown: 'keyDown', + keypress: 'keyPress', + keyup: 'keyUp', + mousedown: 'mouseDown', + mouseup: 'mouseUp', + touchend: 'touchEnd', + touchstart: 'touchStart', + mousemove: 'mouseMove', + mouseout: 'mouseOut', + mouseover: 'mouseOver', + pointermove: 'pointerMove', + pointerout: 'pointerOut', + pointerover: 'pointerOver', + selectionchange: 'selectChange', + textInput: 'textInput', + touchmove: 'touchMove', + [STYLE_AMT_END]: 'animationEnd', + [STYLE_AMT_ITERATION]: 'animationIteration', + [STYLE_AMT_START]: 'animationStart', + [STYLE_TRANS_END]: 'transitionEnd', +}; + +export const CHAR_CODE_ENTER = 13; +export const CHAR_CODE_SPACE = 32; + + +export const EVENT_TYPE_BUBBLE = 'Bubble'; +export const EVENT_TYPE_CAPTURE = 'Capture'; +export const EVENT_TYPE_ALL = 'All'; + diff --git a/libs/horizon/src/event/customEvents/CustomBaseEvent.ts b/libs/horizon/src/event/customEvents/CustomBaseEvent.ts new file mode 100644 index 00000000..0ebce67a --- /dev/null +++ b/libs/horizon/src/event/customEvents/CustomBaseEvent.ts @@ -0,0 +1,119 @@ +/** + * 自定义的基本事件类型 + */ + +import {VNode} from '../../renderer/Types'; + +export class CustomBaseEvent { + + data: string; + defaultPrevented: boolean; + customEventName: string; + targetVNode: VNode; + type: string; + nativeEvent: any; + target: EventTarget; + timeStamp: number; + isDefaultPrevented: () => boolean; + isPropagationStopped: () => boolean; + currentTarget: EventTarget; + + constructor( + customEvtName: string | null, + nativeEvtName: string, + nativeEvt: { [propName: string]: any }, + vNode: VNode, + target: null | EventTarget + ) { + // 复制原生属性到自定义事件 + extendAttribute(this, nativeEvt); + + // custom事件自定义属性 + this.customEventName = customEvtName; + this.targetVNode = vNode; + this.type = nativeEvtName; + this.nativeEvent = nativeEvt; + this.target = target; + this.timeStamp = nativeEvt.timeStamp || Date.now(); + + const defaultPrevented = nativeEvt.defaultPrevented != null ? nativeEvt.defaultPrevented : nativeEvt.returnValue === false; + this.defaultPrevented = defaultPrevented; + + this.preventDefault = this.preventDefault.bind(this); + this.stopPropagation = this.stopPropagation.bind(this); + this.isDefaultPrevented = () => defaultPrevented; + this.isPropagationStopped = () => false; + } + + // 兼容性方法 + persist() { + + } + + // 阻止默认行为 + preventDefault() { + this.defaultPrevented = true; + if (!this.nativeEvent) { + return; + } + + if (typeof this.nativeEvent.preventDefault === 'function') { + this.nativeEvent.preventDefault(); + } + this.nativeEvent.returnValue = false; + this.isDefaultPrevented = () => true; + } + + // 停止冒泡 + stopPropagation() { + if (!this.nativeEvent) { + return; + } + + if (typeof this.nativeEvent.stopPropagation === 'function') { + this.nativeEvent.stopPropagation(); + } + this.nativeEvent.cancelBubble = true; + this.isPropagationStopped = () => true; + } +} + +// 从原生事件中复制属性到自定义事件中 +function extendAttribute(target, source) { + const attributes = [ + // AnimationEvent + 'animationName', 'elapsedTime', 'pseudoElement', + // CompositionEvent、InputEvent + 'data', + // DragEvent + 'dataTransfer', + // FocusEvent + 'relatedTarget', + // KeyboardEvent + 'key', 'keyCode', 'charCode', 'code', 'location', 'ctrlKey', 'shiftKey', 'altKey', 'metaKey', 'repeat', 'locale', 'getModifierState', 'clipboardData', + // MouseEvent + 'button', 'buttons', 'clientX', 'clientY', 'movementX', 'movementY', 'pageX', 'pageY', 'screenX', 'screenY', 'currentTarget', + // PointerEvent + 'pointerId', 'width', 'height', 'pressure', 'tangentialPressure', 'tiltX', 'tiltY', 'twist', 'pointerType', 'isPrimary', + // TouchEvent + 'touches', 'targetTouches', 'changedTouches', + // TransitionEvent + 'propertyName', + // UIEvent + 'view', 'detail', + // WheelEvent + 'deltaX', 'deltaY', 'deltaZ', 'deltaMode', + ]; + + attributes.forEach(attr => { + if (typeof source[attr] !== 'undefined') { + if (typeof source[attr] === 'function') { + target[attr] = function() { + return source[attr].apply(source, arguments); + }; + } else { + target[attr] = source[attr]; + } + } + }) +} diff --git a/libs/horizon/src/event/customEvents/CustomKeyboardEvent.ts b/libs/horizon/src/event/customEvents/CustomKeyboardEvent.ts new file mode 100644 index 00000000..33c6d13c --- /dev/null +++ b/libs/horizon/src/event/customEvents/CustomKeyboardEvent.ts @@ -0,0 +1,78 @@ +/** + * 自定义键盘事件 + */ + +import type {VNode} from '../../renderer/Types'; +import {uniqueCharCode} from '../utils'; +import {CustomBaseEvent} from './CustomBaseEvent'; +import {CHAR_CODE_ENTER} from '../const'; + +const uniqueKeyMap = new Map([ + ['Esc', 'Escape'], + ['Spacebar', ' '], + ['Left', 'ArrowLeft'], + ['Up', 'ArrowUp'], + ['Right', 'ArrowRight'], + ['Down', 'ArrowDown'], + ['Del', 'Delete'], +]); + +const charCodeToKeyMap = new Map([ + [8, 'Backspace'], + [9, 'Tab'], + [13, 'Enter'], + [16, 'Shift'], + [17, 'Control'], + [18, 'Alt'], + [19, 'Pause'], + [27, 'Escape'], + [32, ' '], + [33, 'PageUp'], + [34, 'PageDown'], + [35, 'End'], + [36, 'Home'], + [37, 'ArrowLeft'], + [38, 'ArrowUp'], + [39, 'ArrowRight'], + [40, 'ArrowDown'], + [46, 'Delete'] +]); + +function getKey(event) { + if (event.key) { + return uniqueKeyMap.get(event.key) || event.key; + } + + if (event.type === 'keypress') { + const charCode = uniqueCharCode(event); + return charCode === CHAR_CODE_ENTER ? 'Enter' : String.fromCharCode(charCode); + } + + if (event.type === 'keydown' || event.type === 'keyup') { + return charCodeToKeyMap.get(event.keyCode); + } + + return ''; +} + +export class CustomKeyboardEvent extends CustomBaseEvent { + + key: string; + charCode: number; + keyCode: number; + which: number; + + constructor( + customEvtName: string | null, + nativeEvtName: string, + nativeEvt: { [propName: string]: any }, + vNode: VNode, + target: null | EventTarget + ) { + super(customEvtName, nativeEvtName, nativeEvt, vNode, target); + this.key = getKey(nativeEvt); + this.charCode = nativeEvtName === 'keypress' ? uniqueCharCode(nativeEvt) : 0; + this.keyCode = (nativeEvtName === 'keydown' || nativeEvtName === 'keyup') ? nativeEvt.keyCode : 0; + this.which = this.charCode || this.keyCode; + } +} diff --git a/libs/horizon/src/event/customEvents/CustomMouseEvent.ts b/libs/horizon/src/event/customEvents/CustomMouseEvent.ts new file mode 100644 index 00000000..8ed6f746 --- /dev/null +++ b/libs/horizon/src/event/customEvents/CustomMouseEvent.ts @@ -0,0 +1,26 @@ +import type {VNode} from '../../renderer/Types'; +import {CustomBaseEvent} from './CustomBaseEvent'; + +export class CustomMouseEvent extends CustomBaseEvent { + relatedTarget: EventTarget; + + constructor( + customEvtName: string | null, + nativeEvtName: string, + nativeEvt: { [propName: string]: any }, + vNode: VNode, + target: null | EventTarget + ) { + super(customEvtName, nativeEvtName, nativeEvt, vNode, target); + + let relatedTarget = nativeEvt.relatedTarget; + if (relatedTarget === undefined) { + if (nativeEvt.fromElement === nativeEvt.srcElement) { + relatedTarget = nativeEvt.toElement; + } else { + relatedTarget = nativeEvt.fromElement; + } + } + this.relatedTarget = relatedTarget; + } +} diff --git a/libs/horizon/src/event/customEvents/EventFactory.ts b/libs/horizon/src/event/customEvents/EventFactory.ts new file mode 100644 index 00000000..74c36a2f --- /dev/null +++ b/libs/horizon/src/event/customEvents/EventFactory.ts @@ -0,0 +1,46 @@ +import {CustomKeyboardEvent} from './CustomKeyboardEvent'; +import {CustomMouseEvent} from './CustomMouseEvent'; +import {CustomBaseEvent} from './CustomBaseEvent'; + +const CommonEventToCustom = { + keypress: CustomKeyboardEvent, + keydown: CustomKeyboardEvent, + keyup: CustomKeyboardEvent, + click: CustomMouseEvent, + dblclick: CustomMouseEvent, + mousedown: CustomMouseEvent, + mousemove: CustomMouseEvent, + mouseup: CustomMouseEvent, + mouseout: CustomMouseEvent, + mouseover: CustomMouseEvent, + contextmenu: CustomMouseEvent, + pointercancel: CustomMouseEvent, + pointerdown: CustomMouseEvent, + pointermove: CustomMouseEvent, + pointerout: CustomMouseEvent, + pointerover: CustomMouseEvent, + pointerup: CustomMouseEvent, +} + +// 创建普通自定义事件对象实例,和原生事件对应 +export function createCommonCustomEvent(customEventName, nativeEvtName, nativeEvent, vNode, currentTarget) { + const EventConstructor = CommonEventToCustom[nativeEvtName] || CustomBaseEvent; + return new EventConstructor( + customEventName, + nativeEvtName, + nativeEvent, + vNode, + currentTarget, + ); +} + +// 创建模拟事件实例对象,需要handler特殊处理 +export function createHandlerCustomEvent(customEventName, nativeEvtName, nativeEvent, vNode, currentTarget) { + return new CustomMouseEvent( + customEventName, + nativeEvtName, + nativeEvent, + vNode, + currentTarget, + ); +} diff --git a/libs/horizon/src/event/simulatedEvtHandler/BeforeInputEventHandler.ts b/libs/horizon/src/event/simulatedEvtHandler/BeforeInputEventHandler.ts new file mode 100644 index 00000000..833db40a --- /dev/null +++ b/libs/horizon/src/event/simulatedEvtHandler/BeforeInputEventHandler.ts @@ -0,0 +1,48 @@ +import type {VNode} from '../../renderer/Types'; +import type {AnyNativeEvent, ProcessingListenerList} from '../Types'; +import {getListenersFromTree} from '../ListenerGetter'; +import {createHandlerCustomEvent} from '../customEvents/EventFactory'; +import {CHAR_CODE_SPACE, EVENT_TYPE_ALL} from '../const'; +import {CustomBaseEvent} from '../customEvents/CustomBaseEvent'; +const SPACE_CHAR = String.fromCharCode(CHAR_CODE_SPACE); + +function getInputCharsByNative( + eventName: string, + nativeEvent: any, +): string | void { + if (eventName === 'compositionend') { + return (nativeEvent.detail && nativeEvent.detail.data) || null; + } + if (eventName === 'keypress') { + return nativeEvent.which === CHAR_CODE_SPACE ? SPACE_CHAR : null; + } + if (eventName === 'textInput') { + return nativeEvent.data === SPACE_CHAR ? null : nativeEvent.data; + } + return null; +} + +// 自定义beforeInput的hook事件处理 +export function getListeners( + nativeEvtName: string, + nativeEvent: AnyNativeEvent, + vNode: null | VNode, + target: null | EventTarget, +): ProcessingListenerList { + const chars = getInputCharsByNative(nativeEvtName, nativeEvent); + // 无字符将要输入,无需处理 + if (!chars) { + return []; + } + + const event: CustomBaseEvent = createHandlerCustomEvent( + 'onBeforeInput', + 'beforeinput', + nativeEvent, + null, + target, + ); + event.data = chars; + + return getListenersFromTree(vNode, 'onBeforeInput', event, EVENT_TYPE_ALL); +} diff --git a/libs/horizon/src/event/simulatedEvtHandler/ChangeEventHandler.ts b/libs/horizon/src/event/simulatedEvtHandler/ChangeEventHandler.ts new file mode 100644 index 00000000..781ea607 --- /dev/null +++ b/libs/horizon/src/event/simulatedEvtHandler/ChangeEventHandler.ts @@ -0,0 +1,62 @@ +import {createHandlerCustomEvent} from '../customEvents/EventFactory'; +import {getDom} from '../../dom/DOMInternalKeys'; +import {isInputValueChanged} from '../../dom/valueHandler/ValueChangeHandler'; +import {addValueUpdateList} from '../ControlledValueUpdater'; +import {isTextInputElement} from '../utils'; +import {EVENT_TYPE_ALL} from '../const'; +import {AnyNativeEvent, ProcessingListenerList} from '../Types'; +import { + getListenersFromTree, +} from '../ListenerGetter'; +import {VNode} from '../../renderer/Types'; +import {getDomTag} from '../../dom/utils/Common'; + +// 返回是否需要触发change事件标记 +function shouldTriggerChangeEvent(targetDom, evtName) { + const { type } = targetDom; + const domTag = getDomTag(targetDom); + + if (domTag === 'select' || (domTag === 'input' && type === 'file')) { + return evtName === 'change'; + } else if (isTextInputElement(targetDom)) { + if (evtName === 'input' || evtName === 'change') { + return isInputValueChanged(targetDom); + } + } else if (domTag === 'input' && (type === 'checkbox' || type === 'radio')) { + if (evtName === 'click') { + return isInputValueChanged(targetDom); + } + } + return false; +} + +/** + * + * 支持input/textarea/select的onChange事件 + */ +export function getListeners( + nativeEvtName: string, + nativeEvt: AnyNativeEvent, + vNode: null | VNode, + target: null | EventTarget, +): ProcessingListenerList { + if (!vNode) { + return []; + } + const targetDom = getDom(vNode); + + // 判断是否需要触发change事件 + if (shouldTriggerChangeEvent(targetDom, nativeEvtName)) { + addValueUpdateList(target); + const event = createHandlerCustomEvent( + 'onChange', + 'change', + nativeEvt, + null, + target, + ); + return getListenersFromTree(vNode, 'onChange', event, EVENT_TYPE_ALL); + } + + return []; +} diff --git a/libs/horizon/src/event/simulatedEvtHandler/CompositionEventHandler.ts b/libs/horizon/src/event/simulatedEvtHandler/CompositionEventHandler.ts new file mode 100644 index 00000000..a3914893 --- /dev/null +++ b/libs/horizon/src/event/simulatedEvtHandler/CompositionEventHandler.ts @@ -0,0 +1,30 @@ +import type {VNode} from '../../renderer/Types'; +import type {AnyNativeEvent, ProcessingListenerList} from '../Types'; +import {getListenersFromTree} from '../ListenerGetter'; +import {createHandlerCustomEvent} from '../customEvents/EventFactory'; +import {EVENT_TYPE_ALL} from '../const'; + +const compositionEventObj = { + compositionstart: 'onCompositionStart', + compositionend: 'onCompositionEnd', + compositionupdate: 'onCompositionUpdate', +}; + +// compoisition事件主要处理中文输入法输入时的触发事件 +export function getListeners( + evtName: string, + nativeEvt: AnyNativeEvent, + vNode: null | VNode, + target: null | EventTarget, +): ProcessingListenerList { + const evtType = compositionEventObj[evtName]; + + const event = createHandlerCustomEvent( + evtType, + evtName, + nativeEvt, + null, + target, + ); + return getListenersFromTree(vNode, evtType, event, EVENT_TYPE_ALL); +} diff --git a/libs/horizon/src/event/simulatedEvtHandler/SelectionEventHandler.ts b/libs/horizon/src/event/simulatedEvtHandler/SelectionEventHandler.ts new file mode 100644 index 00000000..81bc1fcc --- /dev/null +++ b/libs/horizon/src/event/simulatedEvtHandler/SelectionEventHandler.ts @@ -0,0 +1,111 @@ +import {createHandlerCustomEvent} from '../customEvents/EventFactory'; +import {shallowCompare} from '../../renderer/utils/compare'; +import {getFocusedDom} from '../../dom/utils/Common'; +import {getDom} from '../../dom/DOMInternalKeys'; +import {isDocument} from '../../dom/utils/Common'; +import {isTextInputElement} from '../utils'; +import type {AnyNativeEvent, ProcessingListenerList} from '../Types'; +import {getListenersFromTree} from '../ListenerGetter'; +import type {VNode} from '../../renderer/Types'; +import {EVENT_TYPE_ALL} from '../const'; + +const horizonEventName = 'onSelect' + +let currentElement = null; +let currentVNode = null; +let lastSelection = null; + +function initTargetCache(dom, vNode) { + if (isTextInputElement(dom) || dom.contentEditable === 'true') { + currentElement = dom; + currentVNode = vNode; + lastSelection = null; + } +} + +function clearTargetCache() { + currentElement = null; + currentVNode = null; + lastSelection = null; +} + +// 标记是否在鼠标事件过程中 +let isInMouseEvent = false; + +// 获取节点所在的document对象 +function getDocument(eventTarget) { + if (eventTarget.window === eventTarget) { + return eventTarget.document; + } + if (isDocument(eventTarget)) { + return eventTarget; + } + return eventTarget.ownerDocument; +} + +function getSelectEvent(nativeEvent, target) { + const doc = getDocument(target); + if (isInMouseEvent || currentElement == null || currentElement !== getFocusedDom(doc)) { + return []; + } + + const currentSelection = window.getSelection(); + if (!shallowCompare(lastSelection, currentSelection)) { + lastSelection = currentSelection; + + const event = createHandlerCustomEvent( + horizonEventName, + 'select', + nativeEvent, + null, + target, + ); + event.target = currentElement; + + return getListenersFromTree( + currentVNode, + horizonEventName, + event, + EVENT_TYPE_ALL + ); + } + return []; +} + + +/** + * 该插件创建一个onSelect事件 + * 支持元素: input、textarea、contentEditable元素 + * 触发场景:用户输入、折叠选择、文本选择 + */ +export function getListeners( + name: string, + nativeEvt: AnyNativeEvent, + vNode: null | VNode, + target: null | EventTarget, +): ProcessingListenerList { + const targetNode = vNode ? getDom(vNode) : window; + let eventUnitList = []; + switch (name) { + case 'focusin': + initTargetCache(targetNode, vNode); + return eventUnitList; + case 'focusout': + clearTargetCache(); + return eventUnitList; + case 'mousedown': + isInMouseEvent = true; + return eventUnitList; + case 'contextmenu': + case 'mouseup': + case 'dragend': + isInMouseEvent = false; + eventUnitList = getSelectEvent(nativeEvt, target); + break; + case 'selectionchange': + case 'keydown': + case 'keyup': + eventUnitList = getSelectEvent(nativeEvt, target); + } + return eventUnitList; +}