From 690cdd1bb3fb08f9439a65c10f70bf34365a2b7d Mon Sep 17 00:00:00 2001 From: fxy060608 Date: Tue, 8 Jun 2021 21:20:01 +0800 Subject: [PATCH] feat: add vdom --- .../uni-components/src/helpers/useField.ts | 2 +- packages/uni-h5/dist/uni-h5.cjs.js | 28 +- packages/uni-h5/dist/uni-h5.es.js | 28 +- .../uni-shared/__tests__/vdom/style.spec.ts | 20 + packages/uni-shared/dist/uni-shared.cjs.js | 352 ++++++++++++++++++ packages/uni-shared/dist/uni-shared.d.ts | 145 ++++++++ packages/uni-shared/dist/uni-shared.es.js | 344 ++++++++++++++++- packages/uni-shared/src/index.ts | 1 + packages/uni-shared/src/vdom/Comment.ts | 8 + packages/uni-shared/src/vdom/DOMException.ts | 6 + packages/uni-shared/src/vdom/Element.ts | 20 + packages/uni-shared/src/vdom/Event.ts | 99 +++++ packages/uni-shared/src/vdom/Node.ts | 255 +++++++++++++ packages/uni-shared/src/vdom/Style.ts | 84 +++++ packages/uni-shared/src/vdom/Text.ts | 16 + packages/uni-shared/src/vdom/index.ts | 14 + 16 files changed, 1413 insertions(+), 9 deletions(-) create mode 100644 packages/uni-shared/__tests__/vdom/style.spec.ts create mode 100644 packages/uni-shared/src/vdom/Comment.ts create mode 100644 packages/uni-shared/src/vdom/DOMException.ts create mode 100644 packages/uni-shared/src/vdom/Element.ts create mode 100644 packages/uni-shared/src/vdom/Event.ts create mode 100644 packages/uni-shared/src/vdom/Node.ts create mode 100644 packages/uni-shared/src/vdom/Style.ts create mode 100644 packages/uni-shared/src/vdom/Text.ts create mode 100644 packages/uni-shared/src/vdom/index.ts diff --git a/packages/uni-components/src/helpers/useField.ts b/packages/uni-components/src/helpers/useField.ts index cb196a14d..d2dc0c063 100644 --- a/packages/uni-components/src/helpers/useField.ts +++ b/packages/uni-components/src/helpers/useField.ts @@ -360,7 +360,7 @@ function useEvent( if (!state.composing) { triggerInput( event, - Object.assign( + extend( { value: field.value, cursor: field.selectionEnd, diff --git a/packages/uni-h5/dist/uni-h5.cjs.js b/packages/uni-h5/dist/uni-h5.cjs.js index 97d67cc5f..025e199f6 100644 --- a/packages/uni-h5/dist/uni-h5.cjs.js +++ b/packages/uni-h5/dist/uni-h5.cjs.js @@ -3085,15 +3085,16 @@ function useEvent(fieldRef, state, trigger, triggerInput, beforeInput) { }; const onInput = function(event, force) { event.stopPropagation(); - if (typeof beforeInput === "function" && beforeInput(event, state) === false) { + let beforeInputDetail = {}; + if (typeof beforeInput === "function" && (beforeInputDetail = beforeInput(event, state)) === false) { return; } state.value = field.value; if (!state.composing) { - triggerInput(event, { + triggerInput(event, shared.extend({ value: field.value, cursor: field.selectionEnd - }, force); + }, (() => beforeInputDetail instanceof Object ? beforeInputDetail : void 0)()), force); } }; const onBlur = function(event) { @@ -3149,6 +3150,10 @@ const props$n = /* @__PURE__ */ shared.extend({}, props$o, { placeholderClass: { type: String, default: "input-placeholder" + }, + verifyNumber: { + type: Boolean, + default: false } }); var Input = /* @__PURE__ */ defineBuiltInComponent({ @@ -3180,6 +3185,7 @@ var Input = /* @__PURE__ */ defineBuiltInComponent({ return props2.password ? "password" : type2; }); const valid = vue.ref(true); + let cachedValue = ""; const rootRef = vue.ref(null); const { fieldRef, @@ -3191,6 +3197,17 @@ var Input = /* @__PURE__ */ defineBuiltInComponent({ const input = event.target; if (NUMBER_TYPES.includes(props2.type)) { valid.value = input.validity && input.validity.valid; + if (!props2.verifyNumber) { + cachedValue = state2.value; + } else { + if (input.validity && !valid.value) { + input.value = cachedValue; + state2.value = input.value; + return false; + } else { + cachedValue = state2.value; + } + } } if (type.value === "number") { const maxlength = state2.maxlength; @@ -3200,6 +3217,11 @@ var Input = /* @__PURE__ */ defineBuiltInComponent({ return false; } } + if (!props2.verifyNumber) { + return { + valid: valid.value + }; + } }); const NUMBER_TYPES = ["number", "digit"]; const step = vue.computed(() => NUMBER_TYPES.includes(props2.type) ? "0.000000000000000001" : ""); diff --git a/packages/uni-h5/dist/uni-h5.es.js b/packages/uni-h5/dist/uni-h5.es.js index aa6e094b8..fc93fc959 100644 --- a/packages/uni-h5/dist/uni-h5.es.js +++ b/packages/uni-h5/dist/uni-h5.es.js @@ -7989,15 +7989,16 @@ function useEvent(fieldRef, state2, trigger, triggerInput, beforeInput) { }; const onInput = function(event, force) { event.stopPropagation(); - if (typeof beforeInput === "function" && beforeInput(event, state2) === false) { + let beforeInputDetail = {}; + if (typeof beforeInput === "function" && (beforeInputDetail = beforeInput(event, state2)) === false) { return; } state2.value = field.value; if (!state2.composing) { - triggerInput(event, { + triggerInput(event, extend({ value: field.value, cursor: field.selectionEnd - }, force); + }, (() => beforeInputDetail instanceof Object ? beforeInputDetail : void 0)()), force); } }; const onBlur = function(event) { @@ -8053,6 +8054,10 @@ const props$u = /* @__PURE__ */ extend({}, props$v, { placeholderClass: { type: String, default: "input-placeholder" + }, + verifyNumber: { + type: Boolean, + default: false } }); var Input = /* @__PURE__ */ defineBuiltInComponent({ @@ -8084,6 +8089,7 @@ var Input = /* @__PURE__ */ defineBuiltInComponent({ return props2.password ? "password" : type2; }); const valid = ref(true); + let cachedValue = ""; const rootRef = ref(null); const { fieldRef, @@ -8095,6 +8101,17 @@ var Input = /* @__PURE__ */ defineBuiltInComponent({ const input = event.target; if (NUMBER_TYPES.includes(props2.type)) { valid.value = input.validity && input.validity.valid; + if (!props2.verifyNumber) { + cachedValue = state3.value; + } else { + if (input.validity && !valid.value) { + input.value = cachedValue; + state3.value = input.value; + return false; + } else { + cachedValue = state3.value; + } + } } if (type.value === "number") { const maxlength = state3.maxlength; @@ -8104,6 +8121,11 @@ var Input = /* @__PURE__ */ defineBuiltInComponent({ return false; } } + if (!props2.verifyNumber) { + return { + valid: valid.value + }; + } }); const NUMBER_TYPES = ["number", "digit"]; const step = computed(() => NUMBER_TYPES.includes(props2.type) ? "0.000000000000000001" : ""); diff --git a/packages/uni-shared/__tests__/vdom/style.spec.ts b/packages/uni-shared/__tests__/vdom/style.spec.ts new file mode 100644 index 000000000..1bfe64460 --- /dev/null +++ b/packages/uni-shared/__tests__/vdom/style.spec.ts @@ -0,0 +1,20 @@ +import { proxyStyle, UniCSSStyleDeclaration } from '../../src/vdom/Style' + +describe('vdom', () => { + test('style', () => { + const uniCSSStyle = proxyStyle(new UniCSSStyleDeclaration()) + expect(uniCSSStyle.toJSON()).toBe(null) + uniCSSStyle.cssText = 'color:red' + expect(uniCSSStyle.toJSON()).toBe(uniCSSStyle.cssText) + uniCSSStyle.backgroundColor = 'black' + expect(uniCSSStyle.toJSON()).toEqual([ + uniCSSStyle.cssText, + { backgroundColor: uniCSSStyle.backgroundColor }, + ]) + const uniCSSStyle1 = proxyStyle(new UniCSSStyleDeclaration()) + uniCSSStyle1.setProperty('--window-top', '0px') + expect(uniCSSStyle1.toJSON()).toEqual({ + '--window-top': uniCSSStyle1['--window-top'], + }) + }) +}) diff --git a/packages/uni-shared/dist/uni-shared.cjs.js b/packages/uni-shared/dist/uni-shared.cjs.js index 6cf754481..6e8f10595 100644 --- a/packages/uni-shared/dist/uni-shared.cjs.js +++ b/packages/uni-shared/dist/uni-shared.cjs.js @@ -215,6 +215,346 @@ function isNativeTag(tag) { const COMPONENT_SELECTOR_PREFIX = 'uni-'; const COMPONENT_PREFIX = 'v-' + COMPONENT_SELECTOR_PREFIX; +class DOMException extends Error { + constructor(message) { + super(message); + this.name = 'DOMException'; + } +} + +function normalizeEventType(type) { + return `on${shared.capitalize(shared.camelize(type))}`; +} +class UniEvent { + constructor(type, opts) { + this.defaultPrevented = false; + this.timeStamp = Date.now(); + this._stop = false; + this._end = false; + this.type = type.toLowerCase(); + this.bubbles = !!opts.bubbles; + this.cancelable = !!opts.cancelable; + } + preventDefault() { + this.defaultPrevented = true; + } + stopImmediatePropagation() { + this._end = this._stop = true; + } + stopPropagation() { + this._stop = true; + } +} +class UniEventTarget { + constructor() { + this._listeners = {}; + } + dispatchEvent(evt) { + const listeners = this._listeners[evt.type]; + if (!listeners) { + return false; + } + const len = listeners.length; + for (let i = 0; i < len; i++) { + listeners[i].call(this, evt); + if (evt._end) { + break; + } + } + return evt.cancelable && evt.defaultPrevented; + } + addEventListener(type, listener, options) { + const isOnce = options && options.once; + if (isOnce) { + const wrapper = function (evt) { + listener.apply(this, [evt]); + this.removeEventListener(type, wrapper, options); + }; + return this.addEventListener(type, wrapper, shared.extend(options, { once: false })); + } + (this._listeners[type] || (this._listeners[type] = [])).push(listener); + } + removeEventListener(type, callback, options) { + const listeners = this._listeners[type.toLowerCase()]; + if (!listeners) { + return; + } + const index = listeners.indexOf(callback); + if (index > -1) { + listeners.splice(index, 1); + } + } +} + +class UniCSSStyleDeclaration { + constructor() { + this._cssText = null; + this._value = null; + } + setProperty(property, value) { + if (value === null || value === '') { + this.removeProperty(property); + } + else { + if (!this._value) { + this._value = {}; + } + this._value[property] = value; + } + } + getPropertyValue(property) { + if (!this._value) { + return ''; + } + return this._value[property] || ''; + } + removeProperty(property) { + if (!this._value) { + return ''; + } + const value = this._value[property]; + delete this._value[property]; + return value; + } + get cssText() { + return this._cssText || ''; + } + set cssText(cssText) { + this._cssText = cssText; + } + toJSON() { + const { _cssText, _value } = this; + const hasCssText = _cssText !== null; + const hasValue = _value !== null; + if (hasCssText && hasValue) { + return [_cssText, _value]; + } + return hasCssText ? _cssText : _value; + } +} +const STYLE_PROPS = [ + '_value', + '_cssText', + 'cssText', + 'getPropertyValue', + 'setProperty', + 'removeProperty', + 'toJSON', +]; +function proxyStyle(uniCssStyle) { + return new Proxy(uniCssStyle, { + get(target, key, receiver) { + if (STYLE_PROPS.indexOf(key) === -1) { + return target.getPropertyValue(key); + } + return Reflect.get(target, key, receiver); + }, + set(target, key, value, receiver) { + if (STYLE_PROPS.indexOf(key) === -1) { + target.setProperty(key, value); + return true; + } + return Reflect.set(target, key, value, receiver); + }, + }); +} + +const NODE_TYPE_PAGE = 0; +const NODE_TYPE_ELEMENT = 1; +const NODE_TYPE_TEXT = 3; +const NODE_TYPE_COMMENT = 8; +function sibling(node, type) { + const { parentNode } = node; + if (!parentNode) { + return null; + } + const { childNodes } = parentNode; + return childNodes[childNodes.indexOf(node) + (type === 'n' ? 1 : -1)] || null; +} +function removeNode(node) { + const { parentNode } = node; + if (parentNode) { + parentNode.removeChild(node); + } +} +function checkNodeId(node) { + if (!node.nodeId) { + node.nodeId = node.pageNode.genId(); + } +} +class UniNode extends UniEventTarget { + constructor(nodeType, nodeName) { + super(); + this.pageNode = null; + this.parentNode = null; + this._text = null; + this.nodeType = nodeType; + this.nodeName = nodeName; + this.childNodes = []; + } + get firstChild() { + return this.childNodes[0] || null; + } + get lastChild() { + const { childNodes } = this; + const length = childNodes.length; + return length ? childNodes[length - 1] : null; + } + get nextSibling() { + return sibling(this, 'n'); + } + get textContent() { + return this._text || ''; + } + set textContent(text) { + this._text = text; + } + get parentElement() { + const { parentNode } = this; + if (parentNode && parentNode.nodeType === NODE_TYPE_ELEMENT) { + return parentNode; + } + return null; + } + get previousSibling() { + return sibling(this, 'p'); + } + appendChild(newChild) { + return this.insertBefore(newChild, null); + } + cloneNode(deep) { + const cloned = shared.extend(Object.create(Object.getPrototypeOf(this)), this); + const { attributes } = cloned; + if (attributes) { + cloned.attributes = shared.extend({}, attributes); + } + if (deep) { + cloned.childNodes = cloned.childNodes.map((childNode) => childNode.cloneNode(true)); + } + return cloned; + } + insertBefore(newChild, refChild) { + removeNode(newChild); + newChild.pageNode = this.pageNode; + newChild.parentNode = this; + checkNodeId(newChild); + const { childNodes } = this; + if (refChild) { + const index = childNodes.indexOf(refChild); + if (index === -1) { + throw new DOMException(`Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.`); + } + childNodes.splice(childNodes.indexOf(refChild), 0, newChild); + } + else { + childNodes.push(newChild); + } + return newChild; + } + removeChild(oldChild) { + const { childNodes } = this; + const index = childNodes.indexOf(oldChild); + if (index === -1) { + throw new DOMException(`Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.`); + } + oldChild.parentNode = null; + childNodes.splice(index, 1); + return oldChild; + } +} +class UniBaseNode extends UniNode { + constructor(nodeType, nodeName) { + super(nodeType, nodeName); + this.attributes = Object.create(null); + this._html = null; + this.style = proxyStyle(new UniCSSStyleDeclaration()); + } + get className() { + return (this.attributes['class'] || ''); + } + set className(val) { + this.setAttribute('class', val); + } + get innerHTML() { + return ''; + } + set innerHTML(html) { + this._html = html; + } + addEventListener(type, listener, options) { + super.addEventListener(type, listener, options); + const normalized = normalizeEventType(type); + if (!this.attributes[normalized]) { + this.setAttribute(normalized, 1); + } + } + removeEventListener(type, callback, options) { + super.removeEventListener(type, callback, options); + const normalized = normalizeEventType(type); + if (this.attributes[normalized]) { + this.removeAttribute(normalized); + } + } + getAttribute(qualifiedName) { + return this.attributes[qualifiedName]; + } + removeAttribute(qualifiedName) { + delete this.attributes[qualifiedName]; + } + setAttribute(qualifiedName, value) { + this.attributes[qualifiedName] = value; + } + toJSON() { + const res = { + i: this.nodeId, + n: this.nodeName, + a: this.attributes, + s: this.style.toJSON(), + }; + if (this._text !== null) { + res.t = this._text; + } + return res; + } +} + +class UniCommentNode extends UniNode { + constructor(text) { + super(NODE_TYPE_COMMENT, '#comment'); + this._text = text; + } +} + +class UniElement extends UniBaseNode { + constructor(nodeName) { + super(NODE_TYPE_ELEMENT, nodeName.toUpperCase()); + this.tagName = this.nodeName; + } +} +class UniInputElement extends UniElement { + get value() { + return this.getAttribute('value'); + } + set value(val) { + this.setAttribute('value', val); + } +} +class UniTextAreaElement extends UniInputElement { +} + +class UniTextNode extends UniBaseNode { + constructor(text) { + super(NODE_TYPE_TEXT, '#text'); + this._text = text; + } + get nodeValue() { + return this._text || ''; + } + set nodeValue(text) { + this._text = text; + } +} + function getLen(str = '') { return ('' + str).replace(/[^\x00-\xff]/g, '**').length; } @@ -398,6 +738,10 @@ exports.COMPONENT_NAME_PREFIX = COMPONENT_NAME_PREFIX; exports.COMPONENT_PREFIX = COMPONENT_PREFIX; exports.COMPONENT_SELECTOR_PREFIX = COMPONENT_SELECTOR_PREFIX; exports.NAVBAR_HEIGHT = NAVBAR_HEIGHT; +exports.NODE_TYPE_COMMENT = NODE_TYPE_COMMENT; +exports.NODE_TYPE_ELEMENT = NODE_TYPE_ELEMENT; +exports.NODE_TYPE_PAGE = NODE_TYPE_PAGE; +exports.NODE_TYPE_TEXT = NODE_TYPE_TEXT; exports.ON_REACH_BOTTOM_DISTANCE = ON_REACH_BOTTOM_DISTANCE; exports.PLUS_RE = PLUS_RE; exports.PRIMARY_COLOR = PRIMARY_COLOR; @@ -409,6 +753,14 @@ exports.UNI_SSR_DATA = UNI_SSR_DATA; exports.UNI_SSR_GLOBAL_DATA = UNI_SSR_GLOBAL_DATA; exports.UNI_SSR_STORE = UNI_SSR_STORE; exports.UNI_SSR_TITLE = UNI_SSR_TITLE; +exports.UniBaseNode = UniBaseNode; +exports.UniCommentNode = UniCommentNode; +exports.UniElement = UniElement; +exports.UniEvent = UniEvent; +exports.UniInputElement = UniInputElement; +exports.UniNode = UniNode; +exports.UniTextAreaElement = UniTextAreaElement; +exports.UniTextNode = UniTextNode; exports.addFont = addFont; exports.callOptions = callOptions; exports.createRpx2Unit = createRpx2Unit; diff --git a/packages/uni-shared/dist/uni-shared.d.ts b/packages/uni-shared/dist/uni-shared.d.ts index b959c7e4a..d91bbb9cb 100644 --- a/packages/uni-shared/dist/uni-shared.d.ts +++ b/packages/uni-shared/dist/uni-shared.d.ts @@ -66,8 +66,22 @@ export declare function isCustomElement(tag: string): boolean; export declare function isNativeTag(tag: string): boolean; +export declare interface IUniPageNode { + pageId: number; + genId: () => number; + push: (...args: any[]) => void; +} + export declare const NAVBAR_HEIGHT = 44; +export declare const NODE_TYPE_COMMENT = 8; + +export declare const NODE_TYPE_ELEMENT = 1; + +export declare const NODE_TYPE_PAGE = 0; + +export declare const NODE_TYPE_TEXT = 3; + export declare function normalizeDataset(el: Element): any; export declare function normalizeTarget(el: HTMLElement): { @@ -133,6 +147,137 @@ export declare const UNI_SSR_STORE = "store"; export declare const UNI_SSR_TITLE = "title"; +export declare class UniBaseNode extends UniNode { + attributes: Record; + style: UniCSSStyleDeclaration; + protected _html: string | null; + constructor(nodeType: UniNodeType, nodeName: string); + get className(): string; + set className(val: string); + get innerHTML(): string; + set innerHTML(html: string); + addEventListener(type: string, listener: UniEventListener, options?: AddEventListenerOptions): void; + removeEventListener(type: string, callback: UniEventListener, options?: EventListenerOptions): void; + getAttribute(qualifiedName: string): unknown; + removeAttribute(qualifiedName: string): void; + setAttribute(qualifiedName: string, value: unknown): void; + toJSON(): UniNodeJSON; +} + +export declare class UniCommentNode extends UniNode { + constructor(text: string); +} + +declare class UniCSSStyleDeclaration { + [name: string]: string | unknown; + private _cssText; + private _value; + setProperty(property: string, value: string | null): void; + getPropertyValue(property: string): string | string[]; + removeProperty(property: string): string; + get cssText(): string; + set cssText(cssText: string); + toJSON(): UniCSSStyleDeclarationJSON; +} + +declare type UniCSSStyleDeclarationJSON = string | null | Record | [string, Record]; + +export declare class UniElement extends UniBaseNode { + tagName: string; + constructor(nodeName: string); +} + +export declare class UniEvent { + type: string; + bubbles: boolean; + cancelable: boolean; + defaultPrevented: boolean; + timeStamp: number; + _stop: boolean; + _end: boolean; + constructor(type: string, opts: UniEventOptions); + preventDefault(): void; + stopImmediatePropagation(): void; + stopPropagation(): void; +} + +export declare interface UniEventListener { + (evt: UniEvent): void; +} + +declare interface UniEventOptions { + bubbles: boolean; + cancelable: boolean; +} + +declare class UniEventTarget { + private _listeners; + dispatchEvent(evt: UniEvent): boolean; + addEventListener(type: string, listener: UniEventListener, options?: AddEventListenerOptions): void; + removeEventListener(type: string, callback: UniEventListener, options?: EventListenerOptions): void; +} + +export declare class UniInputElement extends UniElement { + get value(): string | number; + set value(val: string | number); +} + +export declare class UniNode extends UniEventTarget { + nodeId?: number; + nodeType: UniNodeType; + nodeName: string; + childNodes: UniNode[]; + pageNode: IUniPageNode | null; + parentNode: UniNode | null; + protected _text: string | null; + constructor(nodeType: UniNodeType, nodeName: string); + get firstChild(): UniNode | null; + get lastChild(): UniNode | null; + get nextSibling(): UniNode | null; + get textContent(): string; + set textContent(text: string); + get parentElement(): UniElement | null; + get previousSibling(): UniNode | null; + appendChild(newChild: T): T; + cloneNode(deep?: boolean): UniNode; + insertBefore(newChild: T, refChild: UniNode | null): T; + removeChild(oldChild: T): T; +} + +export declare interface UniNodeJSON { + /** + * nodeId + */ + i: number; + /** + * nodeName + */ + n: string; + /** + * attributes + */ + a: Record; + /** + * style + */ + s: UniCSSStyleDeclarationJSON; + /** + * text + */ + t?: string; +} + +declare type UniNodeType = typeof NODE_TYPE_PAGE | typeof NODE_TYPE_ELEMENT | typeof NODE_TYPE_TEXT | typeof NODE_TYPE_COMMENT; + +export declare class UniTextAreaElement extends UniInputElement { +} + +export declare class UniTextNode extends UniBaseNode { + constructor(text: string); + get nodeValue(): string; + set nodeValue(text: string); +} + export declare function updateElementStyle(element: HTMLElement, styles: Partial): void; export { } diff --git a/packages/uni-shared/dist/uni-shared.es.js b/packages/uni-shared/dist/uni-shared.es.js index c6875e54f..b70cba7f4 100644 --- a/packages/uni-shared/dist/uni-shared.es.js +++ b/packages/uni-shared/dist/uni-shared.es.js @@ -1,4 +1,4 @@ -import { camelize, extend, isString, isHTMLTag, isSVGTag, isPlainObject, isArray } from '@vue/shared'; +import { camelize, extend, isString, isHTMLTag, isSVGTag, capitalize, isPlainObject, isArray } from '@vue/shared'; function formatKey(key) { return camelize(key.substring(5)); @@ -211,6 +211,346 @@ function isNativeTag(tag) { const COMPONENT_SELECTOR_PREFIX = 'uni-'; const COMPONENT_PREFIX = 'v-' + COMPONENT_SELECTOR_PREFIX; +class DOMException extends Error { + constructor(message) { + super(message); + this.name = 'DOMException'; + } +} + +function normalizeEventType(type) { + return `on${capitalize(camelize(type))}`; +} +class UniEvent { + constructor(type, opts) { + this.defaultPrevented = false; + this.timeStamp = Date.now(); + this._stop = false; + this._end = false; + this.type = type.toLowerCase(); + this.bubbles = !!opts.bubbles; + this.cancelable = !!opts.cancelable; + } + preventDefault() { + this.defaultPrevented = true; + } + stopImmediatePropagation() { + this._end = this._stop = true; + } + stopPropagation() { + this._stop = true; + } +} +class UniEventTarget { + constructor() { + this._listeners = {}; + } + dispatchEvent(evt) { + const listeners = this._listeners[evt.type]; + if (!listeners) { + return false; + } + const len = listeners.length; + for (let i = 0; i < len; i++) { + listeners[i].call(this, evt); + if (evt._end) { + break; + } + } + return evt.cancelable && evt.defaultPrevented; + } + addEventListener(type, listener, options) { + const isOnce = options && options.once; + if (isOnce) { + const wrapper = function (evt) { + listener.apply(this, [evt]); + this.removeEventListener(type, wrapper, options); + }; + return this.addEventListener(type, wrapper, extend(options, { once: false })); + } + (this._listeners[type] || (this._listeners[type] = [])).push(listener); + } + removeEventListener(type, callback, options) { + const listeners = this._listeners[type.toLowerCase()]; + if (!listeners) { + return; + } + const index = listeners.indexOf(callback); + if (index > -1) { + listeners.splice(index, 1); + } + } +} + +class UniCSSStyleDeclaration { + constructor() { + this._cssText = null; + this._value = null; + } + setProperty(property, value) { + if (value === null || value === '') { + this.removeProperty(property); + } + else { + if (!this._value) { + this._value = {}; + } + this._value[property] = value; + } + } + getPropertyValue(property) { + if (!this._value) { + return ''; + } + return this._value[property] || ''; + } + removeProperty(property) { + if (!this._value) { + return ''; + } + const value = this._value[property]; + delete this._value[property]; + return value; + } + get cssText() { + return this._cssText || ''; + } + set cssText(cssText) { + this._cssText = cssText; + } + toJSON() { + const { _cssText, _value } = this; + const hasCssText = _cssText !== null; + const hasValue = _value !== null; + if (hasCssText && hasValue) { + return [_cssText, _value]; + } + return hasCssText ? _cssText : _value; + } +} +const STYLE_PROPS = [ + '_value', + '_cssText', + 'cssText', + 'getPropertyValue', + 'setProperty', + 'removeProperty', + 'toJSON', +]; +function proxyStyle(uniCssStyle) { + return new Proxy(uniCssStyle, { + get(target, key, receiver) { + if (STYLE_PROPS.indexOf(key) === -1) { + return target.getPropertyValue(key); + } + return Reflect.get(target, key, receiver); + }, + set(target, key, value, receiver) { + if (STYLE_PROPS.indexOf(key) === -1) { + target.setProperty(key, value); + return true; + } + return Reflect.set(target, key, value, receiver); + }, + }); +} + +const NODE_TYPE_PAGE = 0; +const NODE_TYPE_ELEMENT = 1; +const NODE_TYPE_TEXT = 3; +const NODE_TYPE_COMMENT = 8; +function sibling(node, type) { + const { parentNode } = node; + if (!parentNode) { + return null; + } + const { childNodes } = parentNode; + return childNodes[childNodes.indexOf(node) + (type === 'n' ? 1 : -1)] || null; +} +function removeNode(node) { + const { parentNode } = node; + if (parentNode) { + parentNode.removeChild(node); + } +} +function checkNodeId(node) { + if (!node.nodeId) { + node.nodeId = node.pageNode.genId(); + } +} +class UniNode extends UniEventTarget { + constructor(nodeType, nodeName) { + super(); + this.pageNode = null; + this.parentNode = null; + this._text = null; + this.nodeType = nodeType; + this.nodeName = nodeName; + this.childNodes = []; + } + get firstChild() { + return this.childNodes[0] || null; + } + get lastChild() { + const { childNodes } = this; + const length = childNodes.length; + return length ? childNodes[length - 1] : null; + } + get nextSibling() { + return sibling(this, 'n'); + } + get textContent() { + return this._text || ''; + } + set textContent(text) { + this._text = text; + } + get parentElement() { + const { parentNode } = this; + if (parentNode && parentNode.nodeType === NODE_TYPE_ELEMENT) { + return parentNode; + } + return null; + } + get previousSibling() { + return sibling(this, 'p'); + } + appendChild(newChild) { + return this.insertBefore(newChild, null); + } + cloneNode(deep) { + const cloned = extend(Object.create(Object.getPrototypeOf(this)), this); + const { attributes } = cloned; + if (attributes) { + cloned.attributes = extend({}, attributes); + } + if (deep) { + cloned.childNodes = cloned.childNodes.map((childNode) => childNode.cloneNode(true)); + } + return cloned; + } + insertBefore(newChild, refChild) { + removeNode(newChild); + newChild.pageNode = this.pageNode; + newChild.parentNode = this; + checkNodeId(newChild); + const { childNodes } = this; + if (refChild) { + const index = childNodes.indexOf(refChild); + if (index === -1) { + throw new DOMException(`Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.`); + } + childNodes.splice(childNodes.indexOf(refChild), 0, newChild); + } + else { + childNodes.push(newChild); + } + return newChild; + } + removeChild(oldChild) { + const { childNodes } = this; + const index = childNodes.indexOf(oldChild); + if (index === -1) { + throw new DOMException(`Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.`); + } + oldChild.parentNode = null; + childNodes.splice(index, 1); + return oldChild; + } +} +class UniBaseNode extends UniNode { + constructor(nodeType, nodeName) { + super(nodeType, nodeName); + this.attributes = Object.create(null); + this._html = null; + this.style = proxyStyle(new UniCSSStyleDeclaration()); + } + get className() { + return (this.attributes['class'] || ''); + } + set className(val) { + this.setAttribute('class', val); + } + get innerHTML() { + return ''; + } + set innerHTML(html) { + this._html = html; + } + addEventListener(type, listener, options) { + super.addEventListener(type, listener, options); + const normalized = normalizeEventType(type); + if (!this.attributes[normalized]) { + this.setAttribute(normalized, 1); + } + } + removeEventListener(type, callback, options) { + super.removeEventListener(type, callback, options); + const normalized = normalizeEventType(type); + if (this.attributes[normalized]) { + this.removeAttribute(normalized); + } + } + getAttribute(qualifiedName) { + return this.attributes[qualifiedName]; + } + removeAttribute(qualifiedName) { + delete this.attributes[qualifiedName]; + } + setAttribute(qualifiedName, value) { + this.attributes[qualifiedName] = value; + } + toJSON() { + const res = { + i: this.nodeId, + n: this.nodeName, + a: this.attributes, + s: this.style.toJSON(), + }; + if (this._text !== null) { + res.t = this._text; + } + return res; + } +} + +class UniCommentNode extends UniNode { + constructor(text) { + super(NODE_TYPE_COMMENT, '#comment'); + this._text = text; + } +} + +class UniElement extends UniBaseNode { + constructor(nodeName) { + super(NODE_TYPE_ELEMENT, nodeName.toUpperCase()); + this.tagName = this.nodeName; + } +} +class UniInputElement extends UniElement { + get value() { + return this.getAttribute('value'); + } + set value(val) { + this.setAttribute('value', val); + } +} +class UniTextAreaElement extends UniInputElement { +} + +class UniTextNode extends UniBaseNode { + constructor(text) { + super(NODE_TYPE_TEXT, '#text'); + this._text = text; + } + get nodeValue() { + return this._text || ''; + } + set nodeValue(text) { + this._text = text; + } +} + function getLen(str = '') { return ('' + str).replace(/[^\x00-\xff]/g, '**').length; } @@ -389,4 +729,4 @@ function getEnvLocale() { return (lang && lang.replace(/[.:].*/, '')) || 'en'; } -export { BUILT_IN_TAGS, COMPONENT_NAME_PREFIX, COMPONENT_PREFIX, COMPONENT_SELECTOR_PREFIX, NAVBAR_HEIGHT, ON_REACH_BOTTOM_DISTANCE, PLUS_RE, PRIMARY_COLOR, RESPONSIVE_MIN_WIDTH, TABBAR_HEIGHT, TAGS, UNI_SSR, UNI_SSR_DATA, UNI_SSR_GLOBAL_DATA, UNI_SSR_STORE, UNI_SSR_TITLE, addFont, callOptions, createRpx2Unit, debounce, decode, decodedQuery, defaultRpx2Unit, formatDateTime, getCustomDataset, getEnvLocale, getLen, initCustomDataset, invokeArrayFns, isBuiltInComponent, isCustomElement, isNativeTag, normalizeDataset, normalizeTarget, once, parseQuery, passive, plusReady, removeLeadingSlash, sanitise, scrollTo, stringifyQuery, updateElementStyle }; +export { BUILT_IN_TAGS, COMPONENT_NAME_PREFIX, COMPONENT_PREFIX, COMPONENT_SELECTOR_PREFIX, NAVBAR_HEIGHT, NODE_TYPE_COMMENT, NODE_TYPE_ELEMENT, NODE_TYPE_PAGE, NODE_TYPE_TEXT, ON_REACH_BOTTOM_DISTANCE, PLUS_RE, PRIMARY_COLOR, RESPONSIVE_MIN_WIDTH, TABBAR_HEIGHT, TAGS, UNI_SSR, UNI_SSR_DATA, UNI_SSR_GLOBAL_DATA, UNI_SSR_STORE, UNI_SSR_TITLE, UniBaseNode, UniCommentNode, UniElement, UniEvent, UniInputElement, UniNode, UniTextAreaElement, UniTextNode, addFont, callOptions, createRpx2Unit, debounce, decode, decodedQuery, defaultRpx2Unit, formatDateTime, getCustomDataset, getEnvLocale, getLen, initCustomDataset, invokeArrayFns, isBuiltInComponent, isCustomElement, isNativeTag, normalizeDataset, normalizeTarget, once, parseQuery, passive, plusReady, removeLeadingSlash, sanitise, scrollTo, stringifyQuery, updateElementStyle }; diff --git a/packages/uni-shared/src/index.ts b/packages/uni-shared/src/index.ts index b996ddc61..ed9363656 100644 --- a/packages/uni-shared/src/index.ts +++ b/packages/uni-shared/src/index.ts @@ -1,6 +1,7 @@ export * from './dom' export * from './plus' export * from './tags' +export * from './vdom' export * from './utils' export * from './query' export * from './debounce' diff --git a/packages/uni-shared/src/vdom/Comment.ts b/packages/uni-shared/src/vdom/Comment.ts new file mode 100644 index 000000000..5539cebb6 --- /dev/null +++ b/packages/uni-shared/src/vdom/Comment.ts @@ -0,0 +1,8 @@ +import { NODE_TYPE_COMMENT, UniNode } from './Node' + +export class UniCommentNode extends UniNode { + constructor(text: string) { + super(NODE_TYPE_COMMENT, '#comment') + this._text = text + } +} diff --git a/packages/uni-shared/src/vdom/DOMException.ts b/packages/uni-shared/src/vdom/DOMException.ts new file mode 100644 index 000000000..2ceb5b6ea --- /dev/null +++ b/packages/uni-shared/src/vdom/DOMException.ts @@ -0,0 +1,6 @@ +export class DOMException extends Error { + constructor(message?: string) { + super(message) + this.name = 'DOMException' + } +} diff --git a/packages/uni-shared/src/vdom/Element.ts b/packages/uni-shared/src/vdom/Element.ts new file mode 100644 index 000000000..3bcce2cee --- /dev/null +++ b/packages/uni-shared/src/vdom/Element.ts @@ -0,0 +1,20 @@ +import { NODE_TYPE_ELEMENT, UniBaseNode } from './Node' + +export class UniElement extends UniBaseNode { + tagName: string + constructor(nodeName: string) { + super(NODE_TYPE_ELEMENT, nodeName.toUpperCase()) + this.tagName = this.nodeName + } +} + +export class UniInputElement extends UniElement { + get value() { + return this.getAttribute('value') as string | number + } + set value(val: string | number) { + this.setAttribute('value', val) + } +} + +export class UniTextAreaElement extends UniInputElement {} diff --git a/packages/uni-shared/src/vdom/Event.ts b/packages/uni-shared/src/vdom/Event.ts new file mode 100644 index 000000000..ac8afbdf1 --- /dev/null +++ b/packages/uni-shared/src/vdom/Event.ts @@ -0,0 +1,99 @@ +import { extend, capitalize, camelize } from '@vue/shared' +import { UniElement } from './Element' + +export function normalizeEventType(type: string) { + return `on${capitalize(camelize(type))}` +} + +export interface UniEventListener { + (evt: UniEvent): void +} + +interface UniEventOptions { + bubbles: boolean + cancelable: boolean +} + +export class UniEvent { + type: string + bubbles: boolean + cancelable: boolean + defaultPrevented: boolean = false + + timeStamp = Date.now() + + _stop: boolean = false + _end: boolean = false + + constructor(type: string, opts: UniEventOptions) { + this.type = type.toLowerCase() + this.bubbles = !!opts.bubbles + this.cancelable = !!opts.cancelable + } + + preventDefault(): void { + this.defaultPrevented = true + } + + stopImmediatePropagation(): void { + this._end = this._stop = true + } + + stopPropagation(): void { + this._stop = true + } +} + +export class UniEventTarget { + private _listeners: Record = {} + + dispatchEvent(evt: UniEvent): boolean { + const listeners = this._listeners[evt.type] + if (!listeners) { + return false + } + const len = listeners.length + for (let i = 0; i < len; i++) { + listeners[i].call(this, evt) + if (evt._end) { + break + } + } + return evt.cancelable && evt.defaultPrevented + } + + addEventListener( + type: string, + listener: UniEventListener, + options?: AddEventListenerOptions + ): void { + const isOnce = options && options.once + if (isOnce) { + const wrapper = function (this: UniElement, evt: UniEvent) { + listener.apply(this, [evt]) + this.removeEventListener(type, wrapper, options) + } + return this.addEventListener( + type, + wrapper, + extend(options, { once: false }) + ) + } + ;(this._listeners[type] || (this._listeners[type] = [])).push(listener) + } + + removeEventListener( + type: string, + callback: UniEventListener, + options?: EventListenerOptions + ): void { + const listeners = this._listeners[type.toLowerCase()] + if (!listeners) { + return + } + const index = listeners.indexOf(callback) + if (index > -1) { + listeners.splice(index, 1) + } + } +} diff --git a/packages/uni-shared/src/vdom/Node.ts b/packages/uni-shared/src/vdom/Node.ts new file mode 100644 index 000000000..a5ddff7b8 --- /dev/null +++ b/packages/uni-shared/src/vdom/Node.ts @@ -0,0 +1,255 @@ +import { extend } from '@vue/shared' + +import { UniElement } from './Element' +import { DOMException } from './DOMException' +import { normalizeEventType, UniEventListener, UniEventTarget } from './Event' +import { + proxyStyle, + UniCSSStyleDeclaration, + UniCSSStyleDeclarationJSON, +} from './Style' + +export const NODE_TYPE_PAGE = 0 +export const NODE_TYPE_ELEMENT = 1 +export const NODE_TYPE_TEXT = 3 +export const NODE_TYPE_COMMENT = 8 + +type UniNodeType = + | typeof NODE_TYPE_PAGE + | typeof NODE_TYPE_ELEMENT + | typeof NODE_TYPE_TEXT + | typeof NODE_TYPE_COMMENT + +function sibling(node: UniNode, type: 'n' | 'p') { + const { parentNode } = node + if (!parentNode) { + return null + } + const { childNodes } = parentNode + return childNodes[childNodes.indexOf(node) + (type === 'n' ? 1 : -1)] || null +} + +function removeNode(node: UniNode) { + const { parentNode } = node + if (parentNode) { + parentNode.removeChild(node) + } +} + +function checkNodeId(node: UniNode) { + if (!node.nodeId) { + node.nodeId = node.pageNode!.genId() + } +} + +export interface IUniPageNode { + pageId: number + genId: () => number + push: (...args: any[]) => void +} +export class UniNode extends UniEventTarget { + nodeId?: number + nodeType: UniNodeType + nodeName: string + childNodes: UniNode[] + + pageNode: IUniPageNode | null = null + parentNode: UniNode | null = null + + protected _text: string | null = null + + constructor(nodeType: UniNodeType, nodeName: string) { + super() + this.nodeType = nodeType + this.nodeName = nodeName + this.childNodes = [] + } + + get firstChild(): UniNode | null { + return this.childNodes[0] || null + } + + get lastChild(): UniNode | null { + const { childNodes } = this + const length = childNodes.length + return length ? childNodes[length - 1] : null + } + + get nextSibling(): UniNode | null { + return sibling(this, 'n') + } + + get textContent() { + return this._text || '' + } + + set textContent(text: string) { + this._text = text + } + + get parentElement(): UniElement | null { + const { parentNode } = this + if (parentNode && parentNode.nodeType === NODE_TYPE_ELEMENT) { + return parentNode as unknown as UniElement + } + return null + } + + get previousSibling(): UniNode | null { + return sibling(this, 'p') + } + + appendChild(newChild: T): T { + return this.insertBefore(newChild, null) + } + + cloneNode(deep?: boolean): UniNode { + const cloned = extend( + Object.create(Object.getPrototypeOf(this)), + this + ) as UniNode + const { attributes } = cloned as unknown as UniElement + if (attributes) { + ;(cloned as unknown as UniElement).attributes = extend({}, attributes) + } + if (deep) { + cloned.childNodes = cloned.childNodes.map((childNode) => + childNode.cloneNode(true) + ) + } + return cloned + } + + insertBefore(newChild: T, refChild: UniNode | null): T { + removeNode(newChild) + newChild.pageNode = this.pageNode + newChild.parentNode = this + checkNodeId(newChild) + const { childNodes } = this + if (refChild) { + const index = childNodes.indexOf(refChild) + if (index === -1) { + throw new DOMException( + `Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.` + ) + } + childNodes.splice(childNodes.indexOf(refChild), 0, newChild) + } else { + childNodes.push(newChild) + } + return newChild + } + + removeChild(oldChild: T): T { + const { childNodes } = this + const index = childNodes.indexOf(oldChild) + if (index === -1) { + throw new DOMException( + `Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.` + ) + } + oldChild.parentNode = null + childNodes.splice(index, 1) + return oldChild + } +} + +export interface UniNodeJSON { + /** + * nodeId + */ + i: number + /** + * nodeName + */ + n: string + /** + * attributes + */ + a: Record + /** + * style + */ + s: UniCSSStyleDeclarationJSON + /** + * text + */ + t?: string +} + +export class UniBaseNode extends UniNode { + attributes: Record = Object.create(null) + style: UniCSSStyleDeclaration + + protected _html: string | null = null + + constructor(nodeType: UniNodeType, nodeName: string) { + super(nodeType, nodeName) + this.style = proxyStyle(new UniCSSStyleDeclaration()) + } + + get className() { + return (this.attributes['class'] || '') as string + } + + set className(val: string) { + this.setAttribute('class', val) + } + + get innerHTML() { + return '' + } + + set innerHTML(html: string) { + this._html = html + } + + addEventListener( + type: string, + listener: UniEventListener, + options?: AddEventListenerOptions + ) { + super.addEventListener(type, listener, options) + const normalized = normalizeEventType(type) + if (!this.attributes[normalized]) { + this.setAttribute(normalized, 1) + } + } + + removeEventListener( + type: string, + callback: UniEventListener, + options?: EventListenerOptions + ) { + super.removeEventListener(type, callback, options) + const normalized = normalizeEventType(type) + if (this.attributes[normalized]) { + this.removeAttribute(normalized) + } + } + + getAttribute(qualifiedName: string) { + return this.attributes[qualifiedName] + } + + removeAttribute(qualifiedName: string): void { + delete this.attributes[qualifiedName] + } + + setAttribute(qualifiedName: string, value: unknown): void { + this.attributes[qualifiedName] = value + } + + toJSON() { + const res: UniNodeJSON = { + i: this.nodeId!, + n: this.nodeName, + a: this.attributes, + s: this.style.toJSON(), + } + if (this._text !== null) { + res.t = this._text + } + return res + } +} diff --git a/packages/uni-shared/src/vdom/Style.ts b/packages/uni-shared/src/vdom/Style.ts new file mode 100644 index 000000000..4562aac4d --- /dev/null +++ b/packages/uni-shared/src/vdom/Style.ts @@ -0,0 +1,84 @@ +export type UniCSSStyleDeclarationJSON = + | string + | null + | Record + | [string, Record] + +export class UniCSSStyleDeclaration { + [name: string]: string | unknown + private _cssText: string | null = null + private _value: Record | null = null + + setProperty(property: string, value: string | null): void { + if (value === null || value === '') { + this.removeProperty(property) + } else { + if (!this._value) { + this._value = {} + } + this._value[property] = value + } + } + + getPropertyValue(property: string) { + if (!this._value) { + return '' + } + return this._value[property] || '' + } + + removeProperty(property: string): string { + if (!this._value) { + return '' + } + const value = this._value[property] + delete this._value[property] + return value as string + } + + get cssText() { + return this._cssText || '' + } + + set cssText(cssText: string) { + this._cssText = cssText + } + + toJSON(): UniCSSStyleDeclarationJSON { + const { _cssText, _value } = this + const hasCssText = _cssText !== null + const hasValue = _value !== null + if (hasCssText && hasValue) { + return [_cssText!, _value!] + } + return hasCssText ? _cssText : _value + } +} + +const STYLE_PROPS = [ + '_value', + '_cssText', + 'cssText', + 'getPropertyValue', + 'setProperty', + 'removeProperty', + 'toJSON', +] + +export function proxyStyle(uniCssStyle: UniCSSStyleDeclaration) { + return new Proxy(uniCssStyle, { + get(target, key, receiver) { + if (STYLE_PROPS.indexOf(key as string) === -1) { + return target.getPropertyValue(key as string) + } + return Reflect.get(target, key, receiver) + }, + set(target, key, value, receiver) { + if (STYLE_PROPS.indexOf(key as string) === -1) { + target.setProperty(key as string, value) + return true + } + return Reflect.set(target, key, value, receiver) + }, + }) +} diff --git a/packages/uni-shared/src/vdom/Text.ts b/packages/uni-shared/src/vdom/Text.ts new file mode 100644 index 000000000..149910ab2 --- /dev/null +++ b/packages/uni-shared/src/vdom/Text.ts @@ -0,0 +1,16 @@ +import { NODE_TYPE_TEXT, UniBaseNode } from './Node' + +export class UniTextNode extends UniBaseNode { + constructor(text: string) { + super(NODE_TYPE_TEXT, '#text') + this._text = text + } + + get nodeValue() { + return this._text || '' + } + + set nodeValue(text: string) { + this._text = text + } +} diff --git a/packages/uni-shared/src/vdom/index.ts b/packages/uni-shared/src/vdom/index.ts new file mode 100644 index 000000000..98464447e --- /dev/null +++ b/packages/uni-shared/src/vdom/index.ts @@ -0,0 +1,14 @@ +export { UniCommentNode } from './Comment' +export { UniElement, UniInputElement, UniTextAreaElement } from './Element' +export { UniEvent, UniEventListener } from './Event' +export { + NODE_TYPE_PAGE, + NODE_TYPE_ELEMENT, + NODE_TYPE_TEXT, + NODE_TYPE_COMMENT, + UniNode, + UniBaseNode, + UniNodeJSON, + IUniPageNode, +} from './Node' +export { UniTextNode } from './Text' -- GitLab