Match-id-7ac125241fa134ee7baaca41cfcb10bee8c65f65

This commit is contained in:
* 2021-12-25 11:53:59 +08:00 committed by *
parent 19a39513e6
commit d76fcbb8ff
19 changed files with 1277 additions and 0 deletions

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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);
});
});

View File

@ -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;
}
}

View File

@ -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();
}
}
}

View File

@ -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]: [];
}

View File

@ -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',
);

View File

@ -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<ListenerUnit>;
export type ProcessingListenerList = Array<ListenerUnitList>;

View File

@ -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);
}

View File

@ -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';

View File

@ -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];
}
}
})
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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,
);
}

View File

@ -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);
}

View File

@ -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 [];
}

View File

@ -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);
}

View File

@ -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事件
* inputtextareacontentEditable元素
*
*/
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;
}

View File

@ -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;
}