Match-id-92e487ca0204fd9e264978def867b1500a78059f

This commit is contained in:
* 2021-12-22 20:06:23 +08:00 committed by *
parent 325d9571ec
commit a300fa7246
8 changed files with 657 additions and 0 deletions

View File

@ -0,0 +1,125 @@
/* eslint-disable no-sparse-arrays */
// 属性值的数据类型
export enum PROPERTY_TYPE {
BOOLEAN, // 普通布尔类型
STRING, // 普通的字符串类型
SPECIAL, // 需要特殊处理的属性类型
BOOLEAN_STR, // 字符串类型的 true false
};
export type PropDetails = {
propName: string,
type: PROPERTY_TYPE,
attrName: string,
attrNS: string | null,
};
export function getPropDetails(name: string): PropDetails | null {
return propsDetailData[name] || null;
}
// 属性相关数据
// 依次为 propertyName、type、attributeName、attributeNamespace不填则使用默认值
// type 默认 STRING
// attributeName 默认与 propertyName 相同
// attributeNamespace 默认 null
const propertiesData = [
// 一些特殊属性
['children', PROPERTY_TYPE.SPECIAL],
['dangerouslySetInnerHTML', PROPERTY_TYPE.SPECIAL],
['defaultValue', PROPERTY_TYPE.SPECIAL],
['defaultChecked', PROPERTY_TYPE.SPECIAL],
['innerHTML', PROPERTY_TYPE.SPECIAL],
['style', PROPERTY_TYPE.SPECIAL],
// propertyName 和 attributeName 不一样
['acceptCharset', , 'accept-charset'],
['className', , 'class'],
['htmlFor', , 'for'],
['httpEquiv', , 'http-equiv'],
// 字符串类型的 true false
['contentEditable', PROPERTY_TYPE.BOOLEAN_STR, 'contenteditable'],
['spellCheck', PROPERTY_TYPE.BOOLEAN_STR, 'spellcheck'],
['draggable', PROPERTY_TYPE.BOOLEAN_STR],
['value', PROPERTY_TYPE.BOOLEAN_STR],
// SVG 相关,字符串类型的 true false
['autoReverse', PROPERTY_TYPE.BOOLEAN_STR],
['externalResourcesRequired', PROPERTY_TYPE.BOOLEAN_STR],
['focusable', PROPERTY_TYPE.BOOLEAN_STR],
['preserveAlpha', PROPERTY_TYPE.BOOLEAN_STR],
// 布尔类型
['allowFullScreen', PROPERTY_TYPE.BOOLEAN, 'allowfullscreen'],
['async', PROPERTY_TYPE.BOOLEAN],
['autoFocus', PROPERTY_TYPE.BOOLEAN, 'autofocus'],
['autoPlay', PROPERTY_TYPE.BOOLEAN, 'autoplay'],
['controls', PROPERTY_TYPE.BOOLEAN],
['default', PROPERTY_TYPE.BOOLEAN],
['defer', PROPERTY_TYPE.BOOLEAN],
['disabled', PROPERTY_TYPE.BOOLEAN],
['disablePictureInPicture', PROPERTY_TYPE.BOOLEAN, 'disablepictureinpicture'],
['disableRemotePlayback', PROPERTY_TYPE.BOOLEAN, 'disableremoteplayback'],
['formNoValidate', PROPERTY_TYPE.BOOLEAN, 'formnovalidate'],
['hidden', PROPERTY_TYPE.BOOLEAN],
['loop', PROPERTY_TYPE.BOOLEAN],
['noModule', PROPERTY_TYPE.BOOLEAN, 'nomodule'],
['noValidate', PROPERTY_TYPE.BOOLEAN, 'novalidate'],
['open', PROPERTY_TYPE.BOOLEAN],
['playsInline', PROPERTY_TYPE.BOOLEAN, 'playsinline'],
['readOnly', PROPERTY_TYPE.BOOLEAN, 'readonly'],
['required', PROPERTY_TYPE.BOOLEAN],
['reversed', PROPERTY_TYPE.BOOLEAN],
['scoped', PROPERTY_TYPE.BOOLEAN],
['seamless', PROPERTY_TYPE.BOOLEAN],
['itemScope', PROPERTY_TYPE.BOOLEAN, 'itemscope'],
// 框架需要当做 property 来处理的,而不是 attribute 来处理的属性
['checked', PROPERTY_TYPE.BOOLEAN],
['multiple', PROPERTY_TYPE.BOOLEAN],
['muted', PROPERTY_TYPE.BOOLEAN],
['selected', PROPERTY_TYPE.BOOLEAN],
// SVG 属性
// xlink namespace 的 SVG 属性
['xlinkActuate', , 'xlink:actuate', 'http://www.w3.org/1999/xlink'],
['xlinkArcrole', , 'xlink:arcrole', 'http://www.w3.org/1999/xlink'],
['xlinkRole', , 'xlink:role', 'http://www.w3.org/1999/xlink'],
['xlinkShow', , 'xlink:show', 'http://www.w3.org/1999/xlink'],
['xlinkTitle', , 'xlink:title', 'http://www.w3.org/1999/xlink'],
['xlinkType', , 'xlink:type', 'http://www.w3.org/1999/xlink'],
// xml namespace 的 SVG 属性
['xmlBase', , 'xml:base', 'http://www.w3.org/XML/1998/namespace'],
['xmlLang', , 'xml:lang', 'http://www.w3.org/XML/1998/namespace'],
['xmlSpace', , 'xml:space', 'http://www.w3.org/XML/1998/namespace'],
// HTML and SVG 中都有的属性,大小写敏感
['tabIndex', , 'tabindex'],
['crossOrigin', , 'crossorigin'],
// 接受 URL 的属性
['xlinkHref', , 'xlink:href', 'http://www.w3.org/1999/xlink'],
['formAction', , 'formaction'],
];
const propsDetailData = {};
propertiesData.forEach(record => {
const propName = record[0];
let [type, attrName, attrNS] = record.slice(1);
if (type === undefined) {
type = PROPERTY_TYPE.STRING;
}
if (!attrName) {
attrName = propName;
}
if (!attrNS) {
attrNS = null;
}
propsDetailData[propName] = {
propName,
type,
attrName,
attrNS,
};
});

View File

@ -0,0 +1,117 @@
import {
getPropDetails, PROPERTY_TYPE, PropDetails,
} from './PropertiesData';
const INVALID_EVENT_NAME_REGEX = /^on[^A-Z]/;
// 是内置元素
export function isNativeElement(tagName: string, props: Object) {
return !tagName.includes('-') && props.is === undefined;
}
function isInvalidBoolean(
attributeName: string,
value: any,
propDetails: PropDetails,
): boolean {
if (propDetails.type === PROPERTY_TYPE.SPECIAL) {
return false;
}
// 布尔值校验
if (typeof value === 'boolean') {
const isBooleanType = propDetails.type === PROPERTY_TYPE.BOOLEAN_STR || propDetails.type === PROPERTY_TYPE.BOOLEAN;
if (isBooleanType || (attributeName.startsWith('data-') && attributeName.startsWith('aria-'))) {
return false;
}
// 否则有问题
return true;
}
return false;
}
function isValidProp(tagName, name, value) {
// 校验事件名称
if (isEventProp(name)) {
// 事件名称不满足小驼峰
if (INVALID_EVENT_NAME_REGEX.test(name)) {
console.error('Invalid event property `%s`, events use the camelCase name.', name);
}
return true;
}
const propDetails = getPropDetails(name);
// 当已知属性为错误类型时发出警告
if (propDetails !== null && isInvalidBoolean(name, value, propDetails)) {
return false;
}
return true;
}
export function isInvalidValue(
name: string,
value: any,
propDetails: PropDetails | null,
isNativeTag: boolean,
): boolean {
if (value == null) {
return true;
}
if (!isNativeTag) {
return false;
}
if (propDetails !== null && isInvalidBoolean(name, value, propDetails)) {
return true;
}
if (propDetails !== null && propDetails.type === PROPERTY_TYPE.BOOLEAN) {
return !value;
}
return false;
}
// 是事件属性
export function isEventProp(propName) {
return propName.substr(0, 2) === 'on';
}
// dev模式下校验属性是否合法
export function validateProps(type, props) {
if (!props) {
return;
}
// 非内置的变迁
if (!isNativeElement(type, props)) {
return;
}
// style属性必须是对象
if (props.style != null && typeof props.style !== 'object') {
throw new Error('style should be a object.');
}
if (__DEV__) {
// 校验属性
const invalidProps = Object.keys(props).filter(key => !isValidProp(type, key, props[key]));
const propString = invalidProps.map(prop => '`' + prop + '`').join(', ');
if (invalidProps.length >= 1) {
console.error(
'Invalid value for prop %s on <%s> tag.',
propString,
type,
);
}
}
}

View File

@ -0,0 +1,96 @@
import {updateCommonProp} from '../DOMPropertiesHandler/UpdateCommonProp';
import {getVNodeProps} from '../DOMInternalKeys';
import {IProperty} from '../utils/Interface';
import {getRootElement} from '../utils/Common';
import {isInputValueChanged} from './ValueChangeHandler';
function getInitValue(dom: HTMLInputElement, properties: IProperty) {
const {value, defaultValue, checked, defaultChecked} = properties;
const defaultValueStr = defaultValue != null ? defaultValue : '';
const initValue = value != null ? value : defaultValueStr;
const initChecked = checked != null ? checked : defaultChecked;
return {initValue, initChecked};
}
export function getInputPropsWithoutValue(dom: HTMLInputElement, properties: IProperty) {
// checked属于必填属性无法置空
let {checked} = properties;
if (checked == null) {
checked = getInitValue(dom, properties).initChecked;
}
return {
...properties,
value: undefined,
defaultValue: undefined,
defaultChecked: undefined,
checked,
};
}
export function updateInputValue(dom: HTMLInputElement, properties: IProperty) {
const {value, checked} = properties;
if (checked != null) {
updateCommonProp(dom, 'checked', checked);
} else if (value != null) { // 处理 dom.value 逻辑
if (dom.value !== String(value)) {
dom.value = String(value);
}
}
}
// 设置input的初始值
export function setInitInputValue(dom: HTMLInputElement, properties: IProperty) {
const {value, defaultValue} = properties;
const {initValue, initChecked} = getInitValue(dom, properties);
if (value != null || defaultValue != null) {
// value 的使用优先级 value 属性 > defaultValue 属性 > 空字符串
const initValueStr = String(initValue);
dom.value = initValueStr;
dom.defaultValue = initValueStr;
}
// checked 的使用优先级 checked 属性 > defaultChecked 属性 > false
dom.defaultChecked = Boolean(initChecked);
}
export function resetInputValue(dom: HTMLInputElement, properties: IProperty) {
const {name, type} = properties;
// 如果是 radio先更新相同 name 的 radio
if (type === 'radio' && name != null) {
// radio 的根节点
const radioRoot = getRootElement(dom);
const radioList = radioRoot.querySelectorAll(`input[type="radio"]`);
for (let i = 0; i < radioList.length; i++) {
const radio = radioList[i];
// @ts-ignore
if (radio.name !== name) {
continue;
}
if (radio === dom) {
continue;
}
// @ts-ignore
if (radio.form !== dom.form) {
continue;
}
// @ts-ignore
const nonHorizonRadioProps = getVNodeProps(radio);
isInputValueChanged(radio);
// @ts-ignore
updateInputValue(radio, nonHorizonRadioProps);
}
} else {
updateInputValue(dom, properties);
}
}

View File

@ -0,0 +1,21 @@
import * as Horizon from '../../external/Horizon';
import {IProperty} from '../utils/Interface';
// 把 const a = 'a'; <option>gir{a}ffe</option> 转成 giraffe
function concatChildren(children) {
let content = '';
Horizon.Children.forEach(children, function(child) {
content += child;
});
return content;
}
export function getOptionPropsWithoutValue(dom: Element, properties: IProperty) {
const content = concatChildren(properties.children);
return {
...properties,
children: content || undefined, // 覆盖children
};
}

View File

@ -0,0 +1,70 @@
import {HorizonSelect, IProperty} from '../utils/Interface';
// 更新 <option>
function updateValue(options, newValues: any, isMultiple: boolean) {
if (isMultiple) {
updateMultipleValue(options, newValues);
} else {
updateSingleValue(options, newValues);
}
}
function updateMultipleValue(options, newValues) {
const newValueSet = new Set();
newValues.forEach((val) => {
newValueSet.add(String(val));
});
// options 非数组
for (let i = 0; i < options.length; i++) {
const option = options[i];
const newValue = newValueSet.has(option.value);
if (option.selected !== newValue) {
option.selected = newValue;
}
}
}
// 单选时传入的选项参数必须是可以转为字符串的类型
function updateSingleValue(options, newValue) {
for (let i = 0; i < options.length; i++) {
const option = options[i];
if (option.value === String(newValue)) {
option.selected = true;
break;
}
}
}
export function getSelectPropsWithoutValue(dom: HorizonSelect, properties: Object) {
return {
...properties,
value: undefined,
}
}
export function updateSelectValue(dom: HorizonSelect, properties: IProperty, isInit: boolean = false) {
const {value, defaultValue, multiple} = properties;
const oldMultiple = dom._multiple !== undefined ? dom._multiple : dom.multiple;
const newMultiple = Boolean(multiple);
dom._multiple = newMultiple;
// 设置了 value 属性
if (value != null) {
updateValue(dom.options, value, newMultiple);
} else if (oldMultiple !== newMultiple) { // 修改了 multiple 属性
// 切换 multiple 之后,如果设置了 defaultValue 需要重新应用
if (defaultValue != null) {
updateValue(dom.options, defaultValue, newMultiple);
} else {
// 恢复到未选定状态
updateValue(dom.options, newMultiple ? [] : '', newMultiple);
}
} else if (isInit && defaultValue != null) { // 设置了 defaultValue 属性
updateValue(dom.options, defaultValue, newMultiple);
}
}

View File

@ -0,0 +1,55 @@
import {IProperty} from '../utils/Interface';
// 值的优先级 value > children > defaultValue
function getInitValue(properties: IProperty) {
const {value} = properties;
if (value == null) {
const {defaultValue, children} = properties;
let initValue = defaultValue;
// children content存在时会覆盖defaultValue
if (children != null) {
// 子节点不是纯文本,则取第一个子节点
if (children instanceof Array) {
initValue = children[0];
} else {
initValue = children;
}
}
// defaultValue 属性未配置,置为空字符串
initValue = initValue != null ? initValue : '';
return initValue;
} else {
return value;
}
}
export function getTextareaPropsWithoutValue(dom: HTMLTextAreaElement, properties: Object) {
return {
...properties,
value: undefined,
};
}
export function updateTextareaValue(dom: HTMLTextAreaElement, properties: IProperty, isInit: boolean = false) {
if (isInit) {
const initValue = getInitValue(properties);
if (initValue !== '') {
dom.value = initValue;
}
} else {
// 获取当前节点的 value 值
let value = properties.value;
if (value != null) {
value = String(value);
// 当且仅当值实际发生变化时才去设置节点的value值
if (dom.value !== value) {
dom.value = value;
}
}
}
}

View File

@ -0,0 +1,77 @@
/**
* Horizon的输入框和文本框的change事件在原生的change事件上做了一层处理
* change事件
*/
const HANDLER_KEY = '_valueChangeHandler';
// 判断是否是 check 类型
function isCheckType(dom: HTMLInputElement): boolean {
const {type, nodeName} = dom;
if (nodeName && nodeName.toLowerCase() === 'input') {
return type === 'checkbox' || type === 'radio';
}
return false;
}
/**
* value值发生变化时value的gettersetter
* currentVal input
* change事件
*/
export function watchValueChange(dom) {
if (!dom[HANDLER_KEY]) {
// check: 复选框、单选框; value: 输入框、文本框等
const keyForValue = isCheckType(dom) ? 'checked' : 'value';
// 获取 value 属性的描述信息,其 value 在其 constructor 的 原型上
const descriptor = Object.getOwnPropertyDescriptor(dom.constructor.prototype, keyForValue);
if (dom.hasOwnProperty(keyForValue)) {
return;
}
// currentVal存储最新值并重写value的setter、getter
let currentVal = String(dom[keyForValue]);
const setFunc = descriptor.set;
Object.defineProperty(dom, keyForValue, {
...descriptor,
set: function(value) {
currentVal = String(value);
setFunc.apply(this, [value]);
},
});
dom[HANDLER_KEY] = {
getValue() {
return currentVal;
},
setValue(value) {
currentVal = String(value);
},
};
}
}
export function isInputValueChanged(dom) {
const handler = dom[HANDLER_KEY];
if (!handler) {
return true;
}
let newValue;
if (isCheckType(dom)) {
newValue = dom.checked ? 'true' : 'false';
} else {
newValue = dom.value;
}
const oldValue = handler.getValue();
if (newValue !== oldValue) {
handler.setValue(newValue);
return true;
}
return false;
}

View File

@ -0,0 +1,96 @@
/**
* <input> <textarea> <select> <option> value
*
*/
import {HorizonDom, HorizonSelect, IProperty} from '../utils/Interface';
import {
getInputPropsWithoutValue,
setInitInputValue,
updateInputValue,
resetInputValue,
} from './InputValueHandler';
import {
getOptionPropsWithoutValue,
} from './OptionValueHandler';
import {
getSelectPropsWithoutValue,
updateSelectValue,
} from './SelectValueHandler';
import {
getTextareaPropsWithoutValue,
updateTextareaValue,
} from './TextareaValueHandler';
// 获取元素除了被代理的值以外的属性
function getPropsWithoutValue(type: string, dom: HorizonDom, properties: IProperty) {
switch (type) {
case 'input':
return getInputPropsWithoutValue(<HTMLInputElement>dom, properties);
case 'option':
return getOptionPropsWithoutValue(dom, properties);
case 'select':
return getSelectPropsWithoutValue(<HorizonSelect>dom, properties);
case 'textarea':
return getTextareaPropsWithoutValue(<HTMLTextAreaElement>dom, properties);
default:
return properties;
}
}
// 其它属性挂载完成后处理被代理值相关的属性
function setInitValue(type: string, dom: HorizonDom, properties: IProperty) {
switch (type) {
case 'input':
setInitInputValue(<HTMLInputElement>dom, properties);
break;
case 'select':
updateSelectValue(<HorizonSelect>dom, properties, true);
break;
case 'textarea':
updateTextareaValue(<HTMLTextAreaElement>dom, properties, true);
break;
default:
break;
}
}
// 更新需要适配的属性
function updateValue(type: string, dom: HorizonDom, properties: IProperty) {
switch (type) {
case 'input':
updateInputValue(<HTMLInputElement>dom, properties);
break;
case 'select':
updateSelectValue(<HorizonSelect>dom, properties);
break;
case 'textarea':
updateTextareaValue(<HTMLTextAreaElement>dom, properties);
break;
default:
break;
}
}
function resetValue(dom: HorizonDom, type: string, properties: IProperty) {
switch (type) {
case 'input':
resetInputValue(<HTMLInputElement>dom, properties);
break;
case 'select':
updateSelectValue(<HorizonSelect>dom, properties);
break;
case 'textarea':
updateTextareaValue(<HTMLTextAreaElement>dom, properties);
break;
default:
break;
}
}
export {
getPropsWithoutValue,
setInitValue,
updateValue,
resetValue,
};