From 795a0192a4f58dd7a47958d652fd7c58da9a4b22 Mon Sep 17 00:00:00 2001 From: * <8> Date: Sat, 25 Dec 2021 14:12:17 +0800 Subject: [PATCH] Match-id-54dd6e2cf6fd54463b67f130a3e6f8f1ccd83caf --- 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/utils.ts | 56 ++++++ 7 files changed, 501 insertions(+) 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/utils.ts 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/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; +}