From d76fcbb8ffa4ba59e9e92c91127f8acb751005c3 Mon Sep 17 00:00:00 2001 From: * <8> Date: Sat, 25 Dec 2021 11:53:59 +0800 Subject: [PATCH] Match-id-7ac125241fa134ee7baaca41cfcb10bee8c65f65 --- .../src/event/ControlledValueUpdater.ts | 39 ++++ libs/horizon/src/event/EventBinding.ts | 120 +++++++++++ libs/horizon/src/event/EventCollection.ts | 14 ++ libs/horizon/src/event/EventError.ts | 24 +++ libs/horizon/src/event/HorizonEventMain.ts | 201 ++++++++++++++++++ libs/horizon/src/event/ListenerGetter.ts | 108 ++++++++++ libs/horizon/src/event/StyleEventNames.ts | 55 +++++ libs/horizon/src/event/Types.ts | 16 ++ libs/horizon/src/event/WrapperListener.ts | 41 ++++ libs/horizon/src/event/const.ts | 83 ++++++++ .../src/event/customEvents/CustomBaseEvent.ts | 119 +++++++++++ .../event/customEvents/CustomKeyboardEvent.ts | 78 +++++++ .../event/customEvents/CustomMouseEvent.ts | 26 +++ .../src/event/customEvents/EventFactory.ts | 46 ++++ .../BeforeInputEventHandler.ts | 48 +++++ .../simulatedEvtHandler/ChangeEventHandler.ts | 62 ++++++ .../CompositionEventHandler.ts | 30 +++ .../SelectionEventHandler.ts | 111 ++++++++++ libs/horizon/src/event/utils.ts | 56 +++++ 19 files changed, 1277 insertions(+) create mode 100644 libs/horizon/src/event/ControlledValueUpdater.ts create mode 100644 libs/horizon/src/event/EventBinding.ts create mode 100644 libs/horizon/src/event/EventCollection.ts create mode 100644 libs/horizon/src/event/EventError.ts create mode 100644 libs/horizon/src/event/HorizonEventMain.ts create mode 100644 libs/horizon/src/event/ListenerGetter.ts create mode 100644 libs/horizon/src/event/StyleEventNames.ts create mode 100644 libs/horizon/src/event/Types.ts create mode 100644 libs/horizon/src/event/WrapperListener.ts create mode 100644 libs/horizon/src/event/const.ts create mode 100644 libs/horizon/src/event/customEvents/CustomBaseEvent.ts create mode 100644 libs/horizon/src/event/customEvents/CustomKeyboardEvent.ts create mode 100644 libs/horizon/src/event/customEvents/CustomMouseEvent.ts create mode 100644 libs/horizon/src/event/customEvents/EventFactory.ts create mode 100644 libs/horizon/src/event/simulatedEvtHandler/BeforeInputEventHandler.ts create mode 100644 libs/horizon/src/event/simulatedEvtHandler/ChangeEventHandler.ts create mode 100644 libs/horizon/src/event/simulatedEvtHandler/CompositionEventHandler.ts create mode 100644 libs/horizon/src/event/simulatedEvtHandler/SelectionEventHandler.ts create mode 100644 libs/horizon/src/event/utils.ts 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/EventError.ts b/libs/horizon/src/event/EventError.ts new file mode 100644 index 00000000..e5df0770 --- /dev/null +++ b/libs/horizon/src/event/EventError.ts @@ -0,0 +1,24 @@ +// 处理事件的错误 +let hasError = false; +let caughtError = null; + +// 执行事件监听器,并且捕捉第一个错误,事件执行完成后抛出第一个错误 +export function runListenerAndCatchFirstError(listener, event) { + try { + listener(event); + } catch (error) { + if (!hasError) { + hasError = true; + caughtError = error; + } + } +} + +export function throwCaughtEventError() { + if (hasError) { + const err = caughtError; + caughtError = null; + hasError = false; + throw err; + } +} diff --git a/libs/horizon/src/event/HorizonEventMain.ts b/libs/horizon/src/event/HorizonEventMain.ts new file mode 100644 index 00000000..1ee6ba8b --- /dev/null +++ b/libs/horizon/src/event/HorizonEventMain.ts @@ -0,0 +1,201 @@ +import type {AnyNativeEvent, ProcessingListenerList} from './types'; +import type {VNode} from '../renderer/Types'; + +import { + CommonEventToHorizonMap, + horizonEventToNativeMap, + EVENT_TYPE_BUBBLE, + EVENT_TYPE_CAPTURE, +} from './const'; +import { + throwCaughtEventError, + runListenerAndCatchFirstError, +} from './EventError'; +import {getListeners as getBeforeInputListeners} from './simulatedEvtHandler/BeforeInputEventHandler'; +import {getListeners as getCompositionListeners} from './simulatedEvtHandler/CompositionEventHandler'; +import {getListeners as getChangeListeners} from './simulatedEvtHandler/ChangeEventHandler'; +import {getListeners as getSelectionListeners} from './simulatedEvtHandler/SelectionEventHandler'; +import { + getCustomEventNameWithOn, + uniqueCharCode, + getEventTarget +} from './utils'; +import {createCommonCustomEvent} from './customEvents/EventFactory'; +import {getListenersFromTree} from './ListenerGetter'; +import {shouldUpdateValue, updateControlledValue} from './ControlledValueUpdater'; +import {asyncUpdates, runDiscreteUpdates} from '../renderer/Renderer'; +import {getExactNode} from '../renderer/vnode/VNodeUtils'; + +// 获取事件触发的普通事件监听方法队列 +function getCommonListeners( + nativeEvtName: string, + vNode: null | VNode, + nativeEvent: AnyNativeEvent, + target: null | EventTarget, + isCapture: boolean, +): ProcessingListenerList { + const customEventName = getCustomEventNameWithOn(CommonEventToHorizonMap[nativeEvtName]); + if (!customEventName) { + return []; + } + + // 火狐浏览器兼容。火狐浏览器下功能键将触发keypress事件 火狐下keypress的charcode有值,keycode为0 + if (nativeEvtName === 'keypress' && uniqueCharCode(nativeEvent) === 0) { + return []; + } + + // 鼠标点击右键 + if (nativeEvent instanceof MouseEvent && nativeEvtName === 'click' && nativeEvent.button === 2) { + return []; + } + + if (nativeEvtName === 'focusin') { + nativeEvtName = 'focus'; + } + if (nativeEvtName === 'focusout') { + nativeEvtName = 'blur'; + } + + const customEvent = createCommonCustomEvent(customEventName, nativeEvtName, nativeEvent, null, target); + return getListenersFromTree( + vNode, + customEventName, + customEvent, + isCapture ? EVENT_TYPE_CAPTURE: EVENT_TYPE_BUBBLE, + ); +} + +// 按顺序执行事件队列 +export function processListeners( + processingEventsList: ProcessingListenerList +): void { + processingEventsList.forEach(eventUnitList => { + let lastVNode; + eventUnitList.forEach(eventUnit => { + const {vNode, currentTarget, listener, event} = eventUnit; + if (vNode !== lastVNode && event.isPropagationStopped()) { + return; + } + event.currentTarget = currentTarget; + runListenerAndCatchFirstError(listener, event); + event.currentTarget = null; + lastVNode = vNode; + }); + }); + // 执行所有事件后,重新throw遇到的第一个错误 + throwCaughtEventError(); +} + +function getProcessListenersFacade( + nativeEvtName: string, + vNode: VNode, + nativeEvent: AnyNativeEvent, + target, + isCapture: boolean +): ProcessingListenerList { + // 触发普通委托事件 + let processingListenerList: ProcessingListenerList = getCommonListeners( + nativeEvtName, + vNode, + nativeEvent, + target, + isCapture, + ); + + // 触发特殊handler委托事件 + if (!isCapture) { + if (horizonEventToNativeMap.get('onChange').includes(nativeEvtName)) { + processingListenerList = processingListenerList.concat(getChangeListeners( + nativeEvtName, + nativeEvent, + vNode, + target, + )); + } + + if (horizonEventToNativeMap.get('onSelect').includes(nativeEvtName)) { + processingListenerList = processingListenerList.concat(getSelectionListeners( + nativeEvtName, + nativeEvent, + vNode, + target, + )); + } + + if (nativeEvtName === 'compositionend' || nativeEvtName === 'compositionstart' || nativeEvtName === 'compositionupdate') { + processingListenerList = processingListenerList.concat(getCompositionListeners( + nativeEvtName, + nativeEvent, + vNode, + target, + )); + } + + if (horizonEventToNativeMap.get('onBeforeInput').includes(nativeEvtName)) { + processingListenerList = processingListenerList.concat(getBeforeInputListeners( + nativeEvtName, + nativeEvent, + vNode, + target, + )); + } + } + return processingListenerList; +} + +// 触发可以被执行的horizon事件监听 +function triggerHorizonEvents( + nativeEvtName: string, + isCapture: boolean, + nativeEvent: AnyNativeEvent, + vNode: null | VNode, +): void { + const nativeEventTarget = getEventTarget(nativeEvent); + const processingListenerList = getProcessListenersFacade(nativeEvtName, vNode, nativeEvent, nativeEventTarget, isCapture); + + // 处理触发的事件队列 + processListeners(processingListenerList); +} + + +// 其他事件正在执行中标记 +let isInEventsExecution = false; + +export function handleEventMain( + nativeEvtName: string, + isCapture: boolean, + nativeEvent: AnyNativeEvent, + vNode: null | VNode, + target: EventTarget, +): void { + let rootVNode = vNode; + if (vNode !== null) { + rootVNode = getExactNode(vNode, target); + if (!rootVNode) { + return; + } + } + + // 有事件正在执行,同步执行事件 + if (isInEventsExecution) { + return triggerHorizonEvents(nativeEvtName, isCapture, nativeEvent, rootVNode); + } + + // 没有事件在执行,经过调度再执行事件 + isInEventsExecution = true; + try { + return asyncUpdates(() => + triggerHorizonEvents( + nativeEvtName, + isCapture, + nativeEvent, + rootVNode, + )); + } finally { + isInEventsExecution = false; + if (shouldUpdateValue()) { + runDiscreteUpdates(); + updateControlledValue(); + } + } +} diff --git a/libs/horizon/src/event/ListenerGetter.ts b/libs/horizon/src/event/ListenerGetter.ts new file mode 100644 index 00000000..3c233c48 --- /dev/null +++ b/libs/horizon/src/event/ListenerGetter.ts @@ -0,0 +1,108 @@ +import {VNode} from '../renderer/Types'; +import {DomComponent} from '../renderer/vnode/VNodeTags'; +import {throwIfTrue} from '../renderer/utils/throwIfTrue'; +import type {Props} from '../dom/DOMOperator'; +import {EVENT_TYPE_ALL, EVENT_TYPE_CAPTURE, EVENT_TYPE_BUBBLE} from './const'; +import {ProcessingListenerList, ListenerUnitList} from './Types'; +import {CustomBaseEvent} from './customEvents/CustomBaseEvent'; + +// 返回是否应该阻止事件响应标记,disabled组件不响应鼠标事件 +function shouldPrevent( + name: string, + type: string, + props: Props, +): boolean { + const canPreventMouseEvents = [ + 'onClick', + 'onClickCapture', + 'onDoubleClick', + 'onDoubleClickCapture', + 'onMouseDown', + 'onMouseDownCapture', + 'onMouseMove', + 'onMouseMoveCapture', + 'onMouseUp', + 'onMouseUpCapture', + 'onMouseEnter', + ]; + const interActiveElements = ['button', 'input', 'select', 'textarea']; + if (canPreventMouseEvents.includes(name)) { + return !!(props.disabled && interActiveElements.includes(type)); + } + return false; +} + +// 从vnode属性中获取事件listener +function getListener( + vNode: VNode, + eventName: string, +): Function | null { + const realNode = vNode.realNode; + if (realNode === null) { + return null; + } + const props = vNode.props; + if (props === null) { + return null; + } + const listener = props[eventName]; + if (shouldPrevent(eventName, vNode.type, props)) { + return null; + } + throwIfTrue( + listener && typeof listener !== 'function', + '`%s` listener should be a function.', + eventName + ); + return listener; +} + +// 获取监听事件 +export function getListenersFromTree( + targetVNode: VNode | null, + name: string | null, + horizonEvent: CustomBaseEvent, + eventType: string, +): ProcessingListenerList { + if (!name) { + return []; + } + const captureName = name + EVENT_TYPE_CAPTURE; + const listeners: ListenerUnitList = []; + + let vNode = targetVNode; + + // 从目标节点到根节点遍历获取listener + while (vNode !== null) { + const {realNode, tag} = vNode; + if (tag === DomComponent && realNode !== null) { + if (eventType === EVENT_TYPE_ALL || eventType === EVENT_TYPE_CAPTURE) { + const captureListener = getListener(vNode, captureName); + if (captureListener) { + listeners.unshift({ + vNode, + listener: captureListener, + currentTarget: realNode, + event: horizonEvent, + }); + } + } + if (eventType === EVENT_TYPE_ALL || eventType === EVENT_TYPE_BUBBLE) { + const bubbleListener = getListener(vNode, name); + if (bubbleListener) { + listeners.push({ + vNode, + listener: bubbleListener, + currentTarget: realNode, + event: horizonEvent, + }); + } + } + } + vNode = vNode.parent; + } + return listeners.length > 0 ? [listeners]: []; +} + + + diff --git a/libs/horizon/src/event/StyleEventNames.ts b/libs/horizon/src/event/StyleEventNames.ts new file mode 100644 index 00000000..d2bf85b1 --- /dev/null +++ b/libs/horizon/src/event/StyleEventNames.ts @@ -0,0 +1,55 @@ +/** + * style中的动画事件 + */ + +// style事件浏览器兼容前缀 +const vendorPrefixes = { + animationend: { + MozAnimation: 'mozAnimationEnd', + WebkitAnimation: 'webkitAnimationEnd', + animation: 'animationend', + }, + animationiteration: { + MozAnimation: 'mozAnimationIteration', + WebkitAnimation: 'webkitAnimationIteration', + animation: 'animationiteration', + }, + animationstart: { + MozAnimation: 'mozAnimationStart', + WebkitAnimation: 'webkitAnimationStart', + animation: 'animationstart', + }, + transitionend: { + MozTransition: 'mozTransitionEnd', + WebkitTransition: 'webkitTransitionEnd', + transition: 'transitionend', + }, +}; + +// 获取属性中对应事件名 +function getEventNameByStyle(eventName) { + const prefixMap = vendorPrefixes[eventName]; + if (!prefixMap) { + return eventName; + } + const style = document.createElement('div').style + for (const styleProp in prefixMap) { + if (styleProp in style) { + return prefixMap[styleProp]; + } + } + return eventName; +} + +export const STYLE_AMT_END: string = getEventNameByStyle( + 'animationend', +); +export const STYLE_AMT_ITERATION: string = getEventNameByStyle( + 'animationiteration', +); +export const STYLE_AMT_START: string = getEventNameByStyle( + 'animationstart', +); +export const STYLE_TRANS_END: string = getEventNameByStyle( + 'transitionend', +); diff --git a/libs/horizon/src/event/Types.ts b/libs/horizon/src/event/Types.ts new file mode 100644 index 00000000..ac9bab0f --- /dev/null +++ b/libs/horizon/src/event/Types.ts @@ -0,0 +1,16 @@ + +import type {VNode} from '../renderer/Types'; +import {CustomBaseEvent} from './customEvents/CustomBaseEvent'; + +export type AnyNativeEvent = KeyboardEvent | MouseEvent | TouchEvent | UIEvent | Event; + +export type ListenerUnit = { + vNode: null | VNode, + listener: Function, + currentTarget: EventTarget, + event: CustomBaseEvent, +}; + +export type ListenerUnitList = Array; + +export type ProcessingListenerList = Array; diff --git a/libs/horizon/src/event/WrapperListener.ts b/libs/horizon/src/event/WrapperListener.ts new file mode 100644 index 00000000..9c73299c --- /dev/null +++ b/libs/horizon/src/event/WrapperListener.ts @@ -0,0 +1,41 @@ +import {isMounted} from '../renderer/vnode/VNodeUtils'; +import {SuspenseComponent} from '../renderer/vnode/VNodeTags'; +import {getNearestVNode} from '../dom/DOMInternalKeys'; +import {handleEventMain} from './HorizonEventMain'; +import {runDiscreteUpdates} from '../renderer/Renderer'; +import {getEventTarget} from './utils'; + +// 生成委托事件的监听方法 +export function createCustomEventListener( + target: EventTarget, + nativeEvtName: string, + isCapture: boolean, +): EventListener { + return triggerDelegatedEvent.bind(null, nativeEvtName, isCapture, target); +} + +// 触发委托事件 +function triggerDelegatedEvent( + nativeEvtName: string, + isCapture: boolean, + targetDom: EventTarget, + nativeEvent, +) { + // 执行之前的调度事件 + runDiscreteUpdates(); + + const nativeEventTarget = getEventTarget(nativeEvent); + let targetVNode = getNearestVNode(nativeEventTarget); + + if (targetVNode !== null) { + if (isMounted(targetVNode)) { + if (targetVNode.tag === SuspenseComponent) { + targetVNode = null; + } + } else { + // vnode已销毁 + targetVNode = null; + } + } + handleEventMain(nativeEvtName, isCapture, nativeEvent, targetVNode, targetDom); +} 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; +} diff --git a/libs/horizon/src/event/utils.ts b/libs/horizon/src/event/utils.ts new file mode 100644 index 00000000..a23e52da --- /dev/null +++ b/libs/horizon/src/event/utils.ts @@ -0,0 +1,56 @@ +import {isText} from '../dom/utils/Common'; +import { CHAR_CODE_ENTER, CHAR_CODE_SPACE } from './const'; + +export function uniqueCharCode(nativeEvent): number { + let charCode = nativeEvent.charCode; + + // 火狐浏览器没有设置enter键的charCode,用keyCode + if (charCode === 0 && nativeEvent.keyCode === CHAR_CODE_ENTER) { + charCode = CHAR_CODE_ENTER; + } + + // 当ctrl按下时10表示enter键按下 + if (charCode === 10) { + charCode = CHAR_CODE_ENTER; + } + + // 忽略非打印的Enter键 + if (charCode >= CHAR_CODE_SPACE || charCode === CHAR_CODE_ENTER) { + return charCode; + } + + return 0; +} + +// 获取事件的target对象 +export function getEventTarget(nativeEvent) { + const target = nativeEvent.target || nativeEvent.srcElement || window; + if (isText(target)) { + return target.parentNode; + } + return target; +} + +// 支持的输入框类型 +const supportedInputTypes = ['color', 'date', 'datetime', 'datetime-local', 'email', 'month', + 'number', 'password', 'range', 'search', 'tel', 'text', 'time', 'url', 'week']; + +export function isTextInputElement(dom?: HTMLElement): boolean { + if (dom instanceof HTMLInputElement) { + return supportedInputTypes.includes(dom.type); + } + + const nodeName = dom && dom.nodeName && dom.nodeName.toLowerCase(); + return nodeName === 'textarea'; +} + + +// 例:dragEnd -> onDragEnd +export function getCustomEventNameWithOn(name) { + if (!name) { + return ''; + } + const capitalizedEvent = name[0].toUpperCase() + name.slice(1); + const horizonEventName = 'on' + capitalizedEvent; + return horizonEventName; +}