From f7f9956ddc40ec4d60f33f47cb8c4fe721474b57 Mon Sep 17 00:00:00 2001 From: * <8> Date: Wed, 22 Dec 2021 20:00:43 +0800 Subject: [PATCH] Match-id-967e808cdb47df39560696171de0c8f743a70f84 --- libs/horizon/src/dom/DOMExternal.ts | 108 ++++++ libs/horizon/src/dom/DOMInternalKeys.ts | 109 ++++++ libs/horizon/src/dom/DOMOperator.ts | 333 ++++++++++++++++++ .../DOMPropertiesHandler.ts | 191 ++++++++++ .../dom/DOMPropertiesHandler/StyleHandler.ts | 36 ++ .../DOMPropertiesHandler/UpdateCommonProp.ts | 67 ++++ libs/horizon/src/dom/SelectionRangeHandler.ts | 163 +++++++++ 7 files changed, 1007 insertions(+) create mode 100644 libs/horizon/src/dom/DOMExternal.ts create mode 100644 libs/horizon/src/dom/DOMInternalKeys.ts create mode 100644 libs/horizon/src/dom/DOMOperator.ts create mode 100644 libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts create mode 100644 libs/horizon/src/dom/DOMPropertiesHandler/StyleHandler.ts create mode 100644 libs/horizon/src/dom/DOMPropertiesHandler/UpdateCommonProp.ts create mode 100644 libs/horizon/src/dom/SelectionRangeHandler.ts diff --git a/libs/horizon/src/dom/DOMExternal.ts b/libs/horizon/src/dom/DOMExternal.ts new file mode 100644 index 00000000..37d9dd79 --- /dev/null +++ b/libs/horizon/src/dom/DOMExternal.ts @@ -0,0 +1,108 @@ +import { + asyncUpdates, createVNode, getFirstCustomDom, + syncUpdates, startUpdate, +} from '../renderer/Renderer'; +import {createPortal} from '../renderer/components/CreatePortal'; +import { + clearContainer, saveContainer, +} from './DOMInternalKeys'; +import type {Container} from './DOMOperator'; +import {isElement} from './utils/Common'; +import {listenDelegatedEvents} from '../event/EventBinding'; +import {findDOMByClassInst} from '../renderer/vnode/VNodeUtils'; +import {TreeRoot} from '../renderer/vnode/VNodeTags'; + +function executeRender( + children: any, + container: Container, + callback?: Function, +) { + let treeRoot = container._treeRoot; + + if (!treeRoot) { + treeRoot = createRoot(children, container, callback); + } else { // container被render过 + if (typeof callback === 'function') { + const cb = callback; + callback = function () { + const instance = getFirstCustomDom(treeRoot); + cb.call(instance); + }; + } + // 执行更新操作 + startUpdate(children, treeRoot, callback); + } + + return getFirstCustomDom(treeRoot); +} + +function createRoot(children: any, container: Container, callback?: Function) { + // 清空容器 + let child = container.lastChild; + while (child) { + container.removeChild(child); + child = container.lastChild; + } + + // 调度器创建根节点,并给容器dom赋vNode结构体 + const treeRoot = createVNode(TreeRoot, container); + saveContainer(container, treeRoot._domRoot); + container._treeRoot = treeRoot; + + // 根节点挂接全量事件 + listenDelegatedEvents(container); + + // 执行回调 + if (typeof callback === 'function') { + const cb = callback; + callback = function () { + const instance = getFirstCustomDom(treeRoot); + cb.call(instance); + }; + } + + // 建VNode树,启动页面绘制 + syncUpdates(() => { + startUpdate(children, treeRoot, callback); + }); + + return treeRoot; +} + +function findDOMNode(domOrEle: Element): null | Element | Text { + if (domOrEle == null) { + return null; + } + + // 普通节点 + if (isElement(domOrEle)) { + return domOrEle; + } + + // class的实例 + return findDOMByClassInst(domOrEle); +} + +// 卸载入口 +function destroy(container: Container) { + if (container._treeRoot) { + syncUpdates(() => { + executeRender(null, container, () => { + container._treeRoot = null; + clearContainer(container); + }); + }); + + return true; + } + + return false; +} + +export { + createPortal, + asyncUpdates as unstable_batchedUpdates, + findDOMNode, + executeRender as render, + destroy as unmountComponentAtNode, +}; diff --git a/libs/horizon/src/dom/DOMInternalKeys.ts b/libs/horizon/src/dom/DOMInternalKeys.ts new file mode 100644 index 00000000..445db94c --- /dev/null +++ b/libs/horizon/src/dom/DOMInternalKeys.ts @@ -0,0 +1,109 @@ +/** + * 文件整体功能:给dom节点赋 VNode 的结构体和事件初始化标记 + */ + +import type {VNode} from '../renderer/Types'; +import type { + Container, + Props, +} from './DOMOperator'; + +import { + DomComponent, + DomText, + DomRoot, +} from '../renderer/vnode/VNodeTags'; + +const suffixKey = new Date().getTime().toString(); +const prefix = '_horizon'; + +const internalKeys = { + VNode: `${prefix}VNode@${suffixKey}`, + props: `${prefix}Props@${suffixKey}`, + container: `${prefix}Container@${suffixKey}`, + events: `${prefix}Events@${suffixKey}`, + nonDelegatedEvents: `${prefix}NonDelegatedEvents@${suffixKey}`, +}; + +// 通过 VNode 实例获取 DOM 节点 +export function getDom(vNode: VNode): Element | Text | void { + const {tag} = vNode; + if (tag === DomComponent || tag === DomText) { + return vNode.realNode; + } +} + +// 将 VNode 属性相关信息挂到 DOM 对象的特定属性上 +export function saveVNode( + vNode: VNode, + dom: Element | Text | Container, +): void { + dom[internalKeys.VNode] = vNode; +} + +// 用 DOM 节点,来找其对应的 VNode 实例 +export function getVNode(dom: Node): VNode | null { + const vNode = dom[internalKeys.VNode] || dom[internalKeys.container]; + if (vNode) { + const {tag} = vNode; + if (tag === DomComponent || tag === DomText || tag === DomRoot) { + return vNode; + } + } + return null; +} + +// 用 DOM 对象,来寻找其对应或者说是最近的 VNode 实例 +export function getNearestVNode(dom: Node): null | VNode { + let vNode = dom[internalKeys.VNode]; + if (vNode) { // 如果是已经被框架标记过的 DOM 节点,那么直接返回其 VNode 实例 + return vNode; + } + // 下面处理的是为被框架标记过的 DOM 节点,向上找其父节点是否被框架标记过 + let parent = dom.parentNode; + let nearVNode = null; + while (parent) { + vNode = parent[internalKeys.VNode]; + if (vNode) { + nearVNode = vNode; + break; + } + parent = parent.parentNode; + } + return nearVNode; +} + +// 获取 vNode 上的属性相关信息 +export function getVNodeProps(dom: Element | Text): Props { + return dom[internalKeys.props] || null; +} + +// 将 DOM 属性相关信息挂到 DOM 对象的特定属性上 +export function updateVNodeProps(dom: Element | Text, props: Props): void { + dom[internalKeys.props] = props; +} + +export function getEventListeners(dom: EventTarget): Set { + let elementListeners = dom[internalKeys.events]; + if (!elementListeners) { + elementListeners = new Set(); + dom[internalKeys.events] = elementListeners; + } + return elementListeners; +} + +export function getEventToListenerMap(target: EventTarget): Map { + let eventsMap = target[internalKeys.nonDelegatedEvents]; + if (!eventsMap) { + eventsMap = target[internalKeys.nonDelegatedEvents] = new Map(); + } + return eventsMap; +} + +export function saveContainer(dom: Container, domRoot: VNode): void { + dom[internalKeys.container] = domRoot; +} + +export function clearContainer(dom: Container): void { + dom[internalKeys.container] = null; +} diff --git a/libs/horizon/src/dom/DOMOperator.ts b/libs/horizon/src/dom/DOMOperator.ts new file mode 100644 index 00000000..c33628f3 --- /dev/null +++ b/libs/horizon/src/dom/DOMOperator.ts @@ -0,0 +1,333 @@ +/** + * Copyright (c) Huawei Technologies Co., Ltd. 2021-2021. All rights reserved. + */ + +import { + saveVNode, + updateVNodeProps, +} from './DOMInternalKeys'; +import { + createDom, +} from './utils/DomCreator'; +import {getSelectionInfo, resetSelectionRange, selectionData} from './SelectionRangeHandler'; +import {isElement, isComment, isDocument, isDocumentFragment} from './utils/Common'; +import {NSS} from './utils/DomCreator'; +import {adjustStyleValue} from './DOMPropertiesHandler/StyleHandler'; + +import {listenDelegatedEvents} from '../event/EventBinding'; +import type {VNode} from '../renderer/Types'; +import { + setInitValue, + getPropsWithoutValue, + updateValue, +} from './valueHandler/ValueHandler'; +import { + compareProps, + setDomProps, updateDomProps +} from './DOMPropertiesHandler/DOMPropertiesHandler'; +import {isNativeElement, validateProps} from './validators/ValidateProps'; +import {watchValueChange} from './valueHandler/ValueChangeHandler'; +import {DomComponent, DomText} from '../renderer/vnode/VNodeTags'; +import {updateCommonProp} from './DOMPropertiesHandler/UpdateCommonProp'; + +export type Props = { + autoFocus?: boolean, + children?: any, + dangerouslySetInnerHTML?: any, + disabled?: boolean, + hidden?: boolean, + style?: { display?: string }, +}; + +export type Container = (Element & { _treeRoot?: VNode }) | (Document & { _treeRoot?: VNode }); + +let selectionInfo: null | selectionData = null; + +const types = ['button', 'input', 'select', 'textarea']; + +// button、input、select、textarea、如果有 autoFocus 属性需要focus +function shouldAutoFocus(type: string, props: Props): boolean { + return types.includes(type) ? Boolean(props.autoFocus) : false; +} + +function getChildNS(parent: string | null, type: string,): string { + if (parent === NSS.svg && type === 'foreignObject') { + return NSS.html; + } + if (parent == null || parent === NSS.html) { + // 没有父命名空间 + return Object.keys(NSS).includes(type) ? NSS[type] : NSS.html; + } + // 默认返回parentNamespace. + return parent; +} + +function getRootNS(dom, root, nextRoot) { + let namespace; + let tag; + let container, ownNamespace; + + if (isDocument(dom)) { + tag = '#document'; + namespace = root ? root.namespaceURI : getChildNS(null, ''); + } else if (isDocumentFragment(dom)) { + tag = '#fragment'; + namespace = root ? root.namespaceURI : getChildNS(null, ''); + } else if (isComment(dom)) { + container = nextRoot.parentNode; + ownNamespace = container.namespaceURI || null; + tag = container.tagName; + namespace = getChildNS(ownNamespace, tag); + } else { + container = nextRoot; + ownNamespace = container.namespaceURI || null; + tag = container.tagName; + namespace = getChildNS(ownNamespace, tag); + } + + return namespace; +} + +// 获取容器 +export function getNSCtx( + nextRoot: Container, + ctxNamespace: string, + type: string): string { + let namespace; + if (nextRoot) { + // 获取并解析根节点容器 + const root = nextRoot.documentElement; + namespace = getRootNS(nextRoot, root, nextRoot); + } else { + // 获取子节点容器 + namespace = getChildNS(ctxNamespace, type); + } + return namespace; +} + +export function prepareForSubmit(): void { + selectionInfo = getSelectionInfo(); +} + +export function resetAfterSubmit(): void { + resetSelectionRange(selectionInfo); + selectionInfo = null; +} + +/** + * 在内存中创建 DOM 对象 + * @param tagName 元素的类型 + * @param props 属性 + * @param parentNamespace 当前上下文 + * @param vNode 当前元素对应的 VNode + * @returns DOM 对象 + */ +export function newDom( + tagName: string, + props: Props, + parentNamespace: string, + vNode: VNode, +): Element { + const dom: Element = createDom(tagName, props, parentNamespace); + // 将 vNode 节点挂到 DOM 对象上 + saveVNode(vNode, dom); + // 将属性挂到 DOM 对象上 + updateVNodeProps(dom, props); + + return dom; +} + +// 设置节点默认事件、属性 +export function initDomProps(dom: Element, tagName: string, rawProps: Props): boolean { + validateProps(tagName, rawProps); + + // 获取不包括value,defaultValue的属性 + const props: Object = getPropsWithoutValue(tagName, dom, rawProps); + + // 初始化DOM属性(不包括value,defaultValue) + setDomProps(tagName, dom, props); + + if (tagName === 'input' || tagName === 'textarea') { + // 增加监听value和checked的set、get方法 + watchValueChange(dom); + } + + // 设置dom.value值,触发受控组件的set方法 + setInitValue(tagName, dom, rawProps); + + return shouldAutoFocus(tagName, rawProps); +} + +// 准备更新之前进行一系列校验 DOM,寻找属性差异等准备工作 +export function getPropChangeList( + dom: Element, + type: string, + lastRawProps: Props, + nextRawProps: Props, +): null | Array { + // 校验两个对象的不同 + validateProps(type, nextRawProps); + + // 重新定义的属性不需要参与对比,被代理的组件需要把这些属性覆盖到props中 + const oldProps: Object = getPropsWithoutValue(type, dom, lastRawProps); + const newProps: Object = getPropsWithoutValue(type, dom, nextRawProps); + + const changeList = compareProps(oldProps, newProps); + return changeList; +} + +export function isTextChild(type: string, props: Props): boolean { + if (type === 'textarea') { + return true; + } else if (type === 'option') { + return true; + } else if (type === 'noscript') { + return true; + } else if (typeof props.children === 'string') { + return true; + } else if (typeof props.children === 'number') { + return true; + } else { + return ( + props.dangerouslySetInnerHTML && + typeof props.dangerouslySetInnerHTML === 'object' && + props.dangerouslySetInnerHTML.__html != null + ); + } +} + +export function newTextDom( + text: string, + processing: VNode, +): Text { + const textNode: Text = document.createTextNode(text); + saveVNode(processing, textNode); + return textNode; +} + +export function submitMount( + dom: Element, + type: string, + newProps: Props, +): void { + if (shouldAutoFocus(type, newProps)) { + // button、input、select、textarea、如果有 autoFocus 属性需要focus + dom.focus(); + } +} + +// 提交vNode的类型为Component或者Text的更新 +export function submitDomUpdate(tag: number, vNode: VNode) { + const newProps = vNode.props; + const element: Element = vNode.realNode; + + if (tag === DomComponent) { + // DomComponent类型 + if (element != null) { + const type = vNode.type; + const changeList = vNode.changeList; + vNode.changeList = null; + if (changeList !== null) { + saveVNode(vNode, element); + updateVNodeProps(element, newProps); + // 应用diff更新Properties. + // 当一个选中的radio改变名称,浏览器使另一个radio的复选框为false. + if (type === 'input' && newProps.type === 'radio' && newProps.name != null && newProps.checked != null) { + updateCommonProp(element, 'checked', newProps.checked); + } + const isNativeTag = isNativeElement(type, newProps); + updateDomProps(element, changeList, isNativeTag); + updateValue(type, element, newProps); + } + } + } else if (tag === DomText) { + // text类型 + element.textContent = newProps; + } +} + +export function clearText(dom: Element): void { + dom.innerHTML = ''; +} + +// 添加child元素 +export function appendChildElement(isContainer: boolean, + parent: Element | Container, + child: Element | Text): void { + if (isContainer && isComment(parent)) { + parent.parentNode.insertBefore(child, parent); + } else { + parent.appendChild(child); + } +} + +// 插入dom元素 +export function insertDomBefore( + isContainer: boolean, + parent: Element | Container, + child: Element | Text, + beforeChild: Element | Text, +) { + if (isContainer && isComment(parent)) { + parent.parentNode.insertBefore(child, beforeChild); + } else { + parent.insertBefore(child, beforeChild); + } +} + +export function removeChildDom( + isContainer: boolean, + parent: Element | Container, + child: Element | Text +) { + if (isContainer && isComment(parent)) { + parent.parentNode.removeChild(child); + } else { + parent.removeChild(child); + } +} + +// 隐藏元素 +export function hideDom(tag: number, element: Element | Text) { + if (tag === DomComponent) { + // DomComponent类型 + const {style} = element; + if (style.setProperty && typeof style.setProperty === 'function') { + style.setProperty('display', 'none', 'important'); + } else { + style.display = 'none'; + } + } else if (tag === DomText) { + // text类型 + element.textContent = ''; + } +} + +// 不隐藏元素 +export function unHideDom(tag: number, element: Element | Text, props: Props) { + if (tag === DomComponent) { + // DomComponent类型 + const style = props.style; + let display = null; + if (style !== undefined && style !== null && style.hasOwnProperty('display')) { + display = style.display; + } + element.style.display = adjustStyleValue('display', display); + } else if (tag === DomText) { + // text类型 + element.textContent = props; + } +} + +export function clearContainer(container: Container): void { + if (isElement(container)) { + container.textContent = ''; + } + if (isDocument(container) && container.body != null) { + container.body.textContent = ''; + } +} + +export function prePortal(portal: Element): void { + listenDelegatedEvents(portal); +} diff --git a/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts b/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts new file mode 100644 index 00000000..b0c1352e --- /dev/null +++ b/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts @@ -0,0 +1,191 @@ +import { + allDelegatedHorizonEvents, +} from '../../event/EventCollection'; +import {updateCommonProp} from './UpdateCommonProp'; +import {setStyles} from './StyleHandler'; +import { + listenNonDelegatedEvent +} from '../../event/EventBinding'; +import {isEventProp, isNativeElement} from '../validators/ValidateProps'; + +// 初始化DOM属性 +export function setDomProps( + tagName: string, + dom: Element, + props: Object, +): void { + const isNativeTag = isNativeElement(tagName, props); + const keysOfProps = Object.keys(props); + + for (let i = 0; i < keysOfProps.length; i++) { + const propName = keysOfProps[i]; + const propVal = props[propName]; + + updateOneProp(dom, propName, propVal, isNativeTag, true); + } +} + +// 更新 DOM 属性 +export function updateDomProps( + dom: Element, + changeList: Array, + isNativeTag: boolean, +): void { + for (let i = 0; i < changeList.length; i ++) { + const {propName, propVal} = changeList[i]; + + updateOneProp(dom, propName, propVal, isNativeTag); + } +} + +function updateOneProp(dom, propName, propVal, isNativeTag, isInit = false) { + if (propName === 'style') { + setStyles(dom, propVal); + } else if (propName === 'dangerouslySetInnerHTML') { + dom.innerHTML = propVal.__html; + } else if (propName === 'children') { // 只处理纯文本子节点,其他children在VNode树中处理 + if (typeof propVal === 'string' || typeof propVal === 'number') { + dom.textContent = String(propVal); + } + } else if (isEventProp(propName)) { + // 事件监听属性处理 + if (!allDelegatedHorizonEvents.has(propName)) { + listenNonDelegatedEvent(propName, dom, propVal); + } + } else { + if (!isInit || (isInit && propVal != null)) { + updateCommonProp(dom, propName, propVal, isNativeTag); + } + } +} + +// 找出两个 DOM 属性的差别,生成需要更新的属性集合 +export function compareProps( + oldProps: Object, + newProps: Object, +): null | Array { + let updatesForStyle = {}; + const toBeDeletedProps = []; + const toBeUpdatedProps = []; + const keysOfOldProps = Object.keys(oldProps); + const keysOfNewProps = Object.keys(newProps); + + // 找到旧属性中需要删除的属性 + for (let i = 0; i < keysOfOldProps.length; i++) { + const propName = keysOfOldProps[i]; + // 新属性中包含该属性或者该属性为空值的属性不需要处理 + if (keysOfNewProps.includes(propName) || oldProps[propName] == null) { + continue; + } + + if (propName === 'style') { + const oldStyle = oldProps[propName]; + const styleProps = Object.keys(oldStyle); + for (let j = 0; j < styleProps.length; j++) { + const styleProp = styleProps[j]; + updatesForStyle[styleProp] = ''; + } + } else if ( + propName === 'autoFocus' || + propName === 'children' || + propName === 'dangerouslySetInnerHTML' + ) { + continue; + } else if (isEventProp(propName)) { + if (!allDelegatedHorizonEvents.has(propName)) { + toBeDeletedProps.push({ + propName, + propVal: null, + }); + } + } else { + // 其它属性都要加入到删除队列里面,等待删除 + toBeDeletedProps.push({ + propName, + propVal: null, + }); + } + } + + // 遍历新属性,获取新增和变更属性 + for (let i = 0; i < keysOfNewProps.length; i++) { + const propName = keysOfNewProps[i]; + const newPropValue = newProps[propName]; + const oldPropValue = oldProps != null ? oldProps[propName] : undefined; + + if (newPropValue === oldPropValue || (newPropValue == null && oldPropValue == null)) { + // 新旧属性值未发生变化,或者新旧属性皆为空值,不需要进行处理 + continue; + } + + if (propName === 'style') { + if (oldPropValue) { // 之前 style 属性有设置非空值 + // 原来有这个 style,但现在没这个 style 了 + const oldStyleProps = Object.keys(oldPropValue); + for (let j = 0; j < oldStyleProps.length; j++) { + const styleProp = oldStyleProps[j]; + if (!newPropValue || !newPropValue.hasOwnProperty(styleProp)) { + updatesForStyle[styleProp] = ''; + } + } + + // 现在有这个 style,但是和原来不相等 + const newStyleProps = newPropValue ? Object.keys(newPropValue) : []; + for (let j = 0; j < newStyleProps.length; j++) { + const styleProp = newStyleProps[j]; + if (oldPropValue[styleProp] !== newPropValue[styleProp]) { + updatesForStyle[styleProp] = newPropValue[styleProp]; + } + } + } else { // 之前未设置 style 属性或者设置了空值 + if (Object.keys(updatesForStyle).length === 0) { + toBeUpdatedProps.push({ + propName, + propVal: null, + }); + } + updatesForStyle = newPropValue; + } + } else if (propName === 'dangerouslySetInnerHTML') { + const newHTML = newPropValue ? newPropValue.__html : undefined; + const oldHTML = oldPropValue ? oldPropValue.__html : undefined; + if (newHTML != null) { + if (oldHTML !== newHTML) { + toBeUpdatedProps.push({ + propName, + propVal: newPropValue, + }); + } + } + } else if (propName === 'children') { + if (typeof newPropValue === 'string' || typeof newPropValue === 'number') { + toBeUpdatedProps.push({ + propName, + propVal: String(newPropValue), + }); + } + } else if (isEventProp(propName)) { + if (!allDelegatedHorizonEvents.has(propName)) { + toBeUpdatedProps.push({ + propName, + propVal: newPropValue, + }); + } + } else { + toBeUpdatedProps.push({ + propName, + propVal: newPropValue, + }); + } + } + + // 处理style + if (Object.keys(updatesForStyle).length > 0) { + toBeUpdatedProps.push({ + propName: 'style', + propVal: updatesForStyle, + }); + } + + return [...toBeDeletedProps, ...toBeUpdatedProps]; +} diff --git a/libs/horizon/src/dom/DOMPropertiesHandler/StyleHandler.ts b/libs/horizon/src/dom/DOMPropertiesHandler/StyleHandler.ts new file mode 100644 index 00000000..48e75c46 --- /dev/null +++ b/libs/horizon/src/dom/DOMPropertiesHandler/StyleHandler.ts @@ -0,0 +1,36 @@ +/** + * 设置 DOM 节点的 style 属性 + */ +export function setStyles(dom, styles) { + if (!styles) { + return; + } + + const style = dom.style; + const styleKeys = Object.keys(styles); + + for (let i = 0; i < styleKeys.length; i++) { + const styleKey = styleKeys[i]; + const styleVal = styles[styleKey]; + + const validStyleValue = adjustStyleValue(styleKey, styleVal); + + style[styleKey] = validStyleValue; + } +} + +/** + * 1. 对空值或布尔值进行适配,转为空字符串 + * 2. 去掉多余空字符 + */ +export function adjustStyleValue(name, value) { + let validValue; + + if (value === '' || value == null || typeof value === 'boolean') { + validValue = ''; + } else { + validValue = String(value).trim(); + } + + return validValue; +} diff --git a/libs/horizon/src/dom/DOMPropertiesHandler/UpdateCommonProp.ts b/libs/horizon/src/dom/DOMPropertiesHandler/UpdateCommonProp.ts new file mode 100644 index 00000000..6c2a940a --- /dev/null +++ b/libs/horizon/src/dom/DOMPropertiesHandler/UpdateCommonProp.ts @@ -0,0 +1,67 @@ +import { + getPropDetails, + PROPERTY_TYPE, +} from '../validators/PropertiesData'; +import {isInvalidValue} from '../validators/ValidateProps'; +import {getNamespaceCtx} from '../../renderer/ContextSaver'; +import {NSS} from '../utils/DomCreator'; + +/** + * 给 dom 设置属性 + * attrName 指代码中属性设置的属性名称(如 class) + * 多数情况 attrName 仅用作初始 DOM 节点对象使用,而 property 更多用于页面交互 + */ +export function updateCommonProp(dom: Element, attrName: string, value: any, isNativeTag: boolean = true) { + const propDetails = getPropDetails(attrName); + + if (isInvalidValue(attrName, value, propDetails, isNativeTag)) { + value = null; + } + + if (!isNativeTag || propDetails === null) { + // 特殊处理svg的属性,把驼峰式的属性名称转成'-' + if (dom.tagName.toLowerCase() === 'svg' || getNamespaceCtx() === NSS.svg) { + attrName = convertToLowerCase(attrName); + } + + if (value === null) { + dom.removeAttribute(attrName); + } else { + dom.setAttribute(attrName, String(value)); + } + } else if (['checked', 'multiple', 'muted', 'selected'].includes(propDetails.attrName)) { + if (value === null) { // 必填属性设置默认值 + dom[propDetails.attrName] = false; + } else { + dom[propDetails.attrName] = value; + } + } else { // 处理其他普通属性 + if (value === null) { + dom.removeAttribute(propDetails.attrName); + } else { + const {type, attrNS} = propDetails; // 数据类型、固有属性命名空间 + const attributeName = propDetails.attrName; // 固有属性名 + let attributeValue; + if (type === PROPERTY_TYPE.BOOLEAN) { // 即可以用作标志又可以是属性值的属性 + attributeValue = ''; + } else { + attributeValue = String(value); + } + + if (attrNS) { + dom.setAttributeNS(attrNS, attributeName, attributeValue); + } else { + dom.setAttribute(attributeName, attributeValue); + } + } + } +} + +// 驼峰 变 “-” +function convertToLowerCase(str) { + const replacer = (match, char) => { + return `-${char.toLowerCase()}`; + } + + return str.replace(/([A-Z])/g, replacer); +}; diff --git a/libs/horizon/src/dom/SelectionRangeHandler.ts b/libs/horizon/src/dom/SelectionRangeHandler.ts new file mode 100644 index 00000000..d4f98942 --- /dev/null +++ b/libs/horizon/src/dom/SelectionRangeHandler.ts @@ -0,0 +1,163 @@ +/** + * 处理文本框、输入框中框选范围内的数据 + */ + +import {getIFrameFocusedDom, isText} from './utils/Common'; + +import {isElement} from './utils/Common'; + +/** + * 设置聚焦的 textarea 或 input 节点的选择范围 + * @param dom 需要设置选择范围的 input 或 textarea + * @param range 选择范围对象 + */ +function setSelectionRange(dom: HTMLInputElement | HTMLTextAreaElement, range) { + const { start, end } = range; + let realEnd = end; + + if (realEnd == null) { + realEnd = start; + } + + if (typeof dom.setSelectionRange === 'function') { + dom.setSelectionRange(start, realEnd); + } +} + +/** + * 获取文本框、输入框中选中的文本的范围 + * @param dom 需要设置选择范围的 input 或 textarea + * @return {start: selectionStart, end: selectionEnd} + */ +function getSelectionRange(dom: Element | HTMLInputElement | HTMLTextAreaElement | void) { + const selectionRange = { start: 0, end: 0 }; + + if (!dom) { + return selectionRange; + } + if ('selectionStart' in dom) { + // 现代浏览器的 input 或 textarea 有 selectionStart 属性. + selectionRange.start = dom.selectionStart; + selectionRange.end = dom.selectionEnd; + } + + return selectionRange; +} + +// 判断第一个节点和另一个节点是否是包含关系 +function isNodeContainsByTargetNode(targetNode, node) { + if (!targetNode || !node) { + return false; + } + if (targetNode === node) { + return true; + } + if (isText(targetNode)) { + return false; + } + if (isText(node)) { + return isNodeContainsByTargetNode(targetNode, node.parentNode); + } + if (typeof targetNode.contains === 'function') { + return targetNode.contains(node); // 该的节点是否为目标节点的后代节点 + } + if (typeof targetNode.compareDocumentPosition === 'function') { // compareDocumentPosition 数值,表示两个节点彼此做比较的位置 + const CONTAINS_CODE = 16; + // 返回 16 代表 第二节点在第一节点内部 + return targetNode.compareDocumentPosition(node) === CONTAINS_CODE; + } + return false; +} + +function isInDocument(dom) { + if (dom && dom.ownerDocument) { + return isNodeContainsByTargetNode(dom.ownerDocument.documentElement, dom); + } +} + +// 判断一个标签是否有设置选择范围的能力 +export function hasSelectionProperties(dom) { + let elementType; + + if (dom && dom.nodeName) { + elementType = dom.nodeName.toLowerCase(); + const validInputType = ['text', 'search', 'tel', 'url', 'password']; + + if (elementType === 'input') { + return validInputType.includes(dom.type); + } else if (elementType === 'textarea') { + return dom.contentEditable === 'true'; + } else { + return false; + } + } else { + return false; + } +} + +// 返回当前 focus 的元素以及其选中的范围 +export function getSelectionInfo() { + const focusedDom = getIFrameFocusedDom(); + return { + focusedDom, + selectionRange: hasSelectionProperties(focusedDom) ? getSelectionRange(focusedDom) : null, + }; +} + +export interface selectionData { + focusedDom: HTMLInputElement | HTMLTextAreaElement | void; + selectionRange: { + start: number; + end: number; + } +}; + +// 防止选择范围内的信息因为节点删除或其他原因导致的信息丢失 +export function resetSelectionRange(preSelectionRangeData: selectionData) { + // 当前 focus 的元素 + const currentFocusedDom = getIFrameFocusedDom(); + + // 先前 focus 的元素 + const preFocusedDom = preSelectionRangeData.focusedDom; + + if (!preFocusedDom) { + return; + } + + // 先前的选择范围信息 + const preSelectionRange = preSelectionRangeData.selectionRange; + + if (currentFocusedDom !== preFocusedDom && isInDocument(preFocusedDom)) { + if (preSelectionRange !== null) { + setSelectionRange(preFocusedDom, preSelectionRange); + } + + // 滚动条位置可能会因为一个节点的选中变化位置,需要做处理 + const ancestors = []; + let ancestor = preFocusedDom.parentNode; + // 查找先前的 focus 节点的先祖 + while (ancestor) { + if (isElement(ancestor)) { // 是元素节点,就把先祖信息放到先祖数组中 + // @ts-ignore + const {scrollLeft, scrollTop} = ancestor; + ancestors.push({ + dom: ancestor, + scrollLeft, + scrollTop, + }); + } + ancestor = ancestor.parentNode; + } + + // 执行先前 focus 节点的 focus 方法 + if (typeof preFocusedDom.focus === 'function') { + preFocusedDom.focus(); + } + + ancestors.forEach(ancestorInfo => { + const ancestorDom = ancestorInfo.dom; + ancestorDom.scrollLeft = ancestorInfo.scrollLeft; + ancestorDom.scrollTop = ancestorInfo.scrollTop; + }); + } +}