diff --git a/examples/example-vdom/index.html b/examples/example-vdom/index.html new file mode 100644 index 000000000..c81e1e0dd --- /dev/null +++ b/examples/example-vdom/index.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/examples/example-vdom/package.json b/examples/example-vdom/package.json new file mode 100644 index 000000000..6ac853b4e --- /dev/null +++ b/examples/example-vdom/package.json @@ -0,0 +1,18 @@ +{ + "name": "@phosphor/example-vdom", + "version": "0.0.1", + "private": true, + "scripts": { + "build": "tsc && webpack", + "clean": "rimraf build" + }, + "dependencies": { + "@phosphor/vdom": "^0.0.1", + "@phosphor/widgets": "^1.9.3" + }, + "devDependencies": { + "rimraf": "^2.5.2", + "typescript": "~3.6.0", + "webpack": "^2.2.1" + } +} diff --git a/examples/example-vdom/src/index.tsx b/examples/example-vdom/src/index.tsx new file mode 100644 index 000000000..8be141038 --- /dev/null +++ b/examples/example-vdom/src/index.tsx @@ -0,0 +1,91 @@ +/*------------------------------------------------------------------------------ +| Copyright (c) 2014-2019, PhosphorJS Contributors +| +| Distributed under the terms of the BSD 3-Clause License. +| +| The full license is in the file LICENSE, distributed with this software. +|-----------------------------------------------------------------------------*/ +import { + VDOM +} from '@phosphor/vdom'; + +import { + Widget +} from '@phosphor/widgets'; + + +type TickData = { + readonly title: string; + readonly count: number; +} + + +const TickRow = (props: TickData) => { + return ( + + {props.title} + {props.count} + + ); +}; + + +class TimeWidget extends Widget { + + constructor() { + super(); + this.addClass('TimeWidget'); + } + + protected onBeforeAttach(): void { + setInterval(() => this._tick(), 30); + } + + protected onUpdateRequest(): void { + VDOM.render(this.render(), this.node); + } + + protected render() { + let time = this._time; + let now = this._now; + return ( +
+

This page is updated every 30ms

+

+ UTC Time: + {time.toUTCString()} +

+

+ Local Time: + {time.toString()} +

+

+ Milliseconds Since Epoch: + {now.toString()} +

+ + + + +
+
+ ); + } + + private _tick(): void { + this._time = new Date(); + this._now = Date.now(); + this.update(); + } + + private _time = new Date(); + private _now = Date.now(); +} + + +function main(): void { + Widget.attach(new TimeWidget(), document.body); +} + + +window.onload = main; diff --git a/examples/example-vdom/tsconfig.json b/examples/example-vdom/tsconfig.json new file mode 100644 index 000000000..a5b0b0374 --- /dev/null +++ b/examples/example-vdom/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "declaration": false, + "noImplicitAny": true, + "noEmitOnError": true, + "noUnusedLocals": true, + "strictNullChecks": true, + "module": "commonjs", + "moduleResolution": "node", + "target": "es5", + "outDir": "./build", + "jsx": "react", + "jsxFactory": "VDOM.createElement", + "lib": ["es2015", "dom"], + "types": [] + }, + "include": ["src/*"] + } diff --git a/examples/example-vdom/webpack.config.js b/examples/example-vdom/webpack.config.js new file mode 100644 index 000000000..2a76db0f3 --- /dev/null +++ b/examples/example-vdom/webpack.config.js @@ -0,0 +1,16 @@ +var path = require('path'); + +module.exports = { + entry: './build/index.js', + output: { + path: __dirname + '/build/', + filename: 'bundle.example.js', + publicPath: './build/' + }, + module: { + rules: [ + { test: /\.css$/, use: ['style-loader', 'css-loader'] }, + { test: /\.png$/, use: 'file-loader' } + ] + } +}; diff --git a/packages/vdom/package.json b/packages/vdom/package.json new file mode 100644 index 000000000..6777e211c --- /dev/null +++ b/packages/vdom/package.json @@ -0,0 +1,37 @@ +{ + "name": "@phosphor/vdom", + "version": "0.0.1", + "description": "PhosphorJS - VDOM", + "homepage": "https://github.com/phosphorjs/phosphor", + "bugs": { + "url": "https://github.com/phosphorjs/phosphor/issues" + }, + "license": "BSD-3-Clause", + "author": "S. Chris Colbert ", + "contributors": [ + "S. Chris Colbert " + ], + "files": [ + "lib/*.d.ts", + "lib/*.js" + ], + "main": "lib/index.js", + "types": "lib/index.d.ts", + "directories": { + "lib": "lib/" + }, + "repository": { + "type": "git", + "url": "https://github.com/phosphorjs/phosphor.git" + }, + "scripts": { + "build": "tsc --build", + "clean": "rimraf lib", + "watch": "tsc --build --watch" + }, + "dependencies": {}, + "devDependencies": { + "rimraf": "^2.5.2", + "typescript": "~3.6.0" + } +} diff --git a/packages/vdom/src/index.ts b/packages/vdom/src/index.ts new file mode 100644 index 000000000..f75fb4b55 --- /dev/null +++ b/packages/vdom/src/index.ts @@ -0,0 +1,10 @@ +/*------------------------------------------------------------------------------ +| Copyright (c) 2014-2019, PhosphorJS Contributors +| +| Distributed under the terms of the BSD 3-Clause License. +| +| The full license is in the file LICENSE, distributed with this software. +|-----------------------------------------------------------------------------*/ +export * from './pjsx'; +export * from './vdom'; +export * from './vnode'; diff --git a/packages/vdom/src/pjsx.ts b/packages/vdom/src/pjsx.ts new file mode 100644 index 000000000..9b936e2aa --- /dev/null +++ b/packages/vdom/src/pjsx.ts @@ -0,0 +1,708 @@ +/*------------------------------------------------------------------------------ +| Copyright (c) 2014-2019, PhosphorJS Contributors +| +| Distributed under the terms of the BSD 3-Clause License. +| +| The full license is in the file LICENSE, distributed with this software. +|-----------------------------------------------------------------------------*/ +import { + VNode +} from './vnode'; + + +/** + * The namespace for the Phosphor JSX type defintions. + */ +export +namespace PJSX { + + export type Element = VNode; + export type Child = VNode.Child | number | boolean | null; + export type Children = Child[] | Child; + export type Key = VNode.Key; + export type Ref = VNode.Ref; + + export + interface ElementChildrenAttribute { + children: {} + } + + export + interface SpecialAttributes { + children?: Children; + key?: Key; + ref?: Ref; + } + + export type EventHandlerFunction = (event: E) => void; + export type EventHandlerObject = { handleEvent(event: E): void; }; + export type EventHandler = EventHandlerFunction | EventHandlerObject; + + export + interface DOMAttributes extends SpecialAttributes { + // Clipboard Events + oncopy?: EventHandler; + oncut?: EventHandler; + onpaste?: EventHandler; + + // Composition Events + oncompositionend?: EventHandler; + oncompositionstart?: EventHandler; + oncompositionupdate?: EventHandler; + + // Focus Events + onfocus?: EventHandler; + onblur?: EventHandler; + + // Form Events + onchange?: EventHandler; + oninput?: EventHandler; + onsearch?: EventHandler; + onsubmit?: EventHandler; + oninvalid?: EventHandler; + + // Image Events + onload?: EventHandler; + onerror?: EventHandler; + + // Keyboard Events + onkeydown?: EventHandler; + onkeypress?: EventHandler; + onkeyup?: EventHandler; + + // Media Events + onabort?: EventHandler; + oncanplay?: EventHandler; + oncanplaythrough?: EventHandler; + ondurationchange?: EventHandler; + onemptied?: EventHandler; + onencrypted?: EventHandler; + onended?: EventHandler; + onloadeddata?: EventHandler; + onloadedmetadata?: EventHandler; + onloadstart?: EventHandler; + onpause?: EventHandler; + onplay?: EventHandler; + onplaying?: EventHandler; + onprogress?: EventHandler; + onratechange?: EventHandler; + onseeked?: EventHandler; + onseeking?: EventHandler; + onstalled?: EventHandler; + onsuspend?: EventHandler; + ontimeupdate?: EventHandler; + onvolumechange?: EventHandler; + onwaiting?: EventHandler; + + // MouseEvents + onclick?: EventHandler; + oncontextmenu?: EventHandler; + ondblclick?: EventHandler; + ondrag?: EventHandler; + ondragend?: EventHandler; + ondragenter?: EventHandler; + ondragexit?: EventHandler; + ondragleave?: EventHandler; + ondragover?: EventHandler; + ondragstart?: EventHandler; + ondrop?: EventHandler; + onmousedown?: EventHandler; + onmouseenter?: EventHandler; + onmouseleave?: EventHandler; + onmousemove?: EventHandler; + onmouseout?: EventHandler; + onmouseover?: EventHandler; + onmouseup?: EventHandler; + + // Selection Events + onselect?: EventHandler; + + // Touch Events + ontouchcancel?: EventHandler; + ontouchend?: EventHandler; + ontouchmove?: EventHandler; + ontouchstart?: EventHandler; + + // Pointer Events + onpointerover?: EventHandler; + onpointerenter?: EventHandler; + onpointerdown?: EventHandler; + onpointermove?: EventHandler; + onpointerup?: EventHandler; + onpointercancel?: EventHandler; + onpointerout?: EventHandler; + onpointerleave?: EventHandler; + ongotpointercapture?: EventHandler; + onlostpointercapture?: EventHandler; + + // UI Events + onscroll?: EventHandler; + + // Wheel Events + onwheel?: EventHandler; + + // Animation Events + onanimationstart?: EventHandler; + onanimationend?: EventHandler; + onanimationiteration?: EventHandler; + + // Transition Events + ontransitionend?: EventHandler; + } + + export + type StyleAttributes = { [key: string]: string | number }; + + export + interface HTMLAttributes extends DOMAttributes { + // Standard HTML Attributes + accept?: string; + acceptcharset?: string; + accesskey?: string; + action?: string; + allowfullscreen?: boolean; + allowtransparency?: boolean; + alt?: string; + async?: boolean; + autocomplete?: string; + autocorrect?: string; + autofocus?: boolean; + autoplay?: boolean; + capture?: boolean; + cellpadding?: number | string; + cellspacing?: number | string; + charset?: string; + challenge?: string; + checked?: boolean; + class?: string; + cols?: number; + colspan?: number; + content?: string; + contenteditable?: boolean; + contextmenu?: string; + controls?: boolean; + controlslist?: string; + coords?: string; + crossorigin?: string; + data?: string; + datetime?: string; + default?: boolean; + defer?: boolean; + dir?: string; + disabled?: boolean; + disableremoteplayback?: boolean; + download?: any; + draggable?: boolean; + enctype?: string; + form?: string; + formaction?: string; + formenctype?: string; + formmethod?: string; + formnovalidate?: boolean; + formtarget?: string; + frameborder?: number | string; + headers?: string; + height?: number | string; + hidden?: boolean; + high?: number; + href?: string; + hreflang?: string; + for?: string; + httpequiv?: string; + icon?: string; + id?: string; + inputmode?: string; + integrity?: string; + is?: string; + keyparams?: string; + keytype?: string; + kind?: string; + label?: string; + lang?: string; + list?: string; + loop?: boolean; + low?: number; + manifest?: string; + marginheight?: number; + marginwidth?: number; + max?: number | string; + maxlength?: number; + media?: string; + mediagroup?: string; + method?: string; + min?: number | string; + minlength?: number; + multiple?: boolean; + muted?: boolean; + name?: string; + novalidate?: boolean; + open?: boolean; + optimum?: number; + pattern?: string; + placeholder?: string; + playsinline?: boolean; + poster?: string; + preload?: string; + radiogroup?: string; + readonly?: boolean; + rel?: string; + required?: boolean; + role?: string; + rows?: number; + rowspan?: number; + sandbox?: string; + scope?: string; + scoped?: boolean; + scrolling?: string; + seamless?: boolean; + selected?: boolean; + shape?: string; + size?: number; + sizes?: string; + slot?: string; + span?: number; + spellcheck?: boolean; + src?: string; + srcdoc?: string; + srclang?: string; + srcset?: string; + start?: number; + step?: number | string; + style?: StyleAttributes; + summary?: string; + tabindex?: number; + target?: string; + title?: string; + type?: string; + usemap?: string; + value?: string | string[] | number; + volume?: string | number; + width?: number | string; + wmode?: string; + wrap?: string; + + // RDFa Attributes + about?: string; + datatype?: string; + inlist?: any; + prefix?: string; + property?: string; + resource?: string; + typeof?: string; + vocab?: string; + + // Microdata Attributes + itemprop?: string; + itemscope?: boolean; + itemtype?: string; + itemid?: string; + itemref?: string; + } + + export + interface SVGAttributes extends HTMLAttributes { + accentheight?: number | string; + accumulate?: "none" | "sum"; + additive?: "replace" | "sum"; + alignmentbaseline?: "auto" | "baseline" | "before-edge" | "text-before-edge" | "middle" | "central" | "after-edge" | "text-after-edge" | "ideographic" | "alphabetic" | "hanging" | "mathematical" | "inherit"; + allowreorder?: "no" | "yes"; + alphabetic?: number | string; + amplitude?: number | string; + arabicform?: "initial" | "medial" | "terminal" | "isolated"; + ascent?: number | string; + attributename?: string; + attributetype?: string; + autoreverse?: number | string; + azimuth?: number | string; + basefrequency?: number | string; + baselineshift?: number | string; + baseprofile?: number | string; + bbox?: number | string; + begin?: number | string; + bias?: number | string; + by?: number | string; + calcmode?: number | string; + capheight?: number | string; + clip?: number | string; + clippath?: string; + clippathunits?: number | string; + cliprule?: number | string; + colorinterpolation?: number | string; + colorinterpolationfilters?: "auto" | "sRGB" | "linearRGB" | "inherit"; + colorprofile?: number | string; + colorrendering?: number | string; + contentscripttype?: number | string; + contentstyletype?: number | string; + cursor?: number | string; + cx?: number | string; + cy?: number | string; + d?: string; + decelerate?: number | string; + descent?: number | string; + diffuseconstant?: number | string; + direction?: number | string; + display?: number | string; + divisor?: number | string; + dominantbaseline?: number | string; + dur?: number | string; + dx?: number | string; + dy?: number | string; + edgemode?: number | string; + elevation?: number | string; + enablebackground?: number | string; + end?: number | string; + exponent?: number | string; + externalresourcesrequired?: number | string; + fill?: string; + fillopacity?: number | string; + fillrule?: "nonzero" | "evenodd" | "inherit"; + filter?: string; + filterres?: number | string; + filterunits?: number | string; + floodcolor?: number | string; + floodopacity?: number | string; + focusable?: number | string; + fontfamily?: string; + fontsize?: number | string; + fontsizeadjust?: number | string; + fontstretch?: number | string; + fontstyle?: number | string; + fontvariant?: number | string; + fontweight?: number | string; + format?: number | string; + from?: number | string; + fx?: number | string; + fy?: number | string; + g1?: number | string; + g2?: number | string; + glyphname?: number | string; + glyphorientationhorizontal?: number | string; + glyphorientationvertical?: number | string; + glyphref?: number | string; + gradienttransform?: string; + gradientunits?: string; + hanging?: number | string; + horizadvx?: number | string; + horizoriginx?: number | string; + ideographic?: number | string; + imagerendering?: number | string; + in2?: number | string; + in?: string; + intercept?: number | string; + k1?: number | string; + k2?: number | string; + k3?: number | string; + k4?: number | string; + k?: number | string; + kernelmatrix?: number | string; + kernelunitlength?: number | string; + kerning?: number | string; + keypoints?: number | string; + keysplines?: number | string; + keytimes?: number | string; + lengthadjust?: number | string; + letterspacing?: number | string; + lightingcolor?: number | string; + limitingconeangle?: number | string; + local?: number | string; + markerend?: string; + markerheight?: number | string; + markermid?: string; + markerstart?: string; + markerunits?: number | string; + markerwidth?: number | string; + mask?: string; + maskcontentunits?: number | string; + maskunits?: number | string; + mathematical?: number | string; + mode?: number | string; + numoctaves?: number | string; + offset?: number | string; + opacity?: number | string; + operator?: number | string; + order?: number | string; + orient?: number | string; + orientation?: number | string; + origin?: number | string; + overflow?: number | string; + overlineposition?: number | string; + overlinethickness?: number | string; + paintorder?: number | string; + panose1?: number | string; + pathlength?: number | string; + patterncontentunits?: string; + patterntransform?: number | string; + patternunits?: string; + pointerevents?: number | string; + points?: string; + pointsatx?: number | string; + pointsaty?: number | string; + pointsatz?: number | string; + preservealpha?: number | string; + preserveaspectratio?: string; + primitiveunits?: number | string; + r?: number | string; + radius?: number | string; + refx?: number | string; + refy?: number | string; + renderingintent?: number | string; + repeatcount?: number | string; + repeatdur?: number | string; + requiredextensions?: number | string; + requiredfeatures?: number | string; + restart?: number | string; + result?: string; + rotate?: number | string; + rx?: number | string; + ry?: number | string; + scale?: number | string; + seed?: number | string; + shaperendering?: number | string; + slope?: number | string; + spacing?: number | string; + specularconstant?: number | string; + specularexponent?: number | string; + speed?: number | string; + spreadmethod?: string; + startoffset?: number | string; + stddeviation?: number | string; + stemh?: number | string; + stemv?: number | string; + stitchtiles?: number | string; + stopcolor?: string; + stopopacity?: number | string; + strikethroughposition?: number | string; + strikethroughthickness?: number | string; + string?: number | string; + stroke?: string; + strokedasharray?: string | number; + strokedashoffset?: string | number; + strokelinecap?: "butt" | "round" | "square" | "inherit"; + strokelinejoin?: "miter" | "round" | "bevel" | "inherit"; + strokemiterlimit?: string; + strokeopacity?: number | string; + strokewidth?: number | string; + surfacescale?: number | string; + systemlanguage?: number | string; + tablevalues?: number | string; + targetx?: number | string; + targety?: number | string; + textanchor?: string; + textdecoration?: number | string; + textlength?: number | string; + textrendering?: number | string; + to?: number | string; + transform?: string; + u1?: number | string; + u2?: number | string; + underlineposition?: number | string; + underlinethickness?: number | string; + unicode?: number | string; + unicodebidi?: number | string; + unicoderange?: number | string; + unitsperem?: number | string; + valphabetic?: number | string; + values?: string; + vectoreffect?: number | string; + version?: string; + vertadvy?: number | string; + vertoriginx?: number | string; + vertoriginy?: number | string; + vhanging?: number | string; + videographic?: number | string; + viewbox?: string; + viewtarget?: number | string; + visibility?: number | string; + vmathematical?: number | string; + widths?: number | string; + wordspacing?: number | string; + writingmode?: number | string; + x1?: number | string; + x2?: number | string; + x?: number | string; + xchannelselector?: string; + xheight?: number | string; + xlinkactuate?: string; + xlinkarcrole?: string; + xlinkhref?: string; + xlinkrole?: string; + xlinkshow?: string; + xlinktitle?: string; + xlinktype?: string; + xmlbase?: string; + xmllang?: string; + xmlns?: string; + xmlnsxlink?: string; + xmlspace?: string; + y1?: number | string; + y2?: number | string; + y?: number | string; + ychannelselector?: string; + z?: number | string; + zoomandpan?: string; + } + + export + interface IntrinsicElements { + // HTML + a: HTMLAttributes; + abbr: HTMLAttributes; + address: HTMLAttributes; + area: HTMLAttributes; + article: HTMLAttributes; + aside: HTMLAttributes; + audio: HTMLAttributes; + b: HTMLAttributes; + base: HTMLAttributes; + bdi: HTMLAttributes; + bdo: HTMLAttributes; + big: HTMLAttributes; + blockquote: HTMLAttributes; + body: HTMLAttributes; + br: HTMLAttributes; + button: HTMLAttributes; + canvas: HTMLAttributes; + caption: HTMLAttributes; + cite: HTMLAttributes; + code: HTMLAttributes; + col: HTMLAttributes; + colgroup: HTMLAttributes; + data: HTMLAttributes; + datalist: HTMLAttributes; + dd: HTMLAttributes; + del: HTMLAttributes; + details: HTMLAttributes; + dfn: HTMLAttributes; + dialog: HTMLAttributes; + div: HTMLAttributes; + dl: HTMLAttributes; + dt: HTMLAttributes; + em: HTMLAttributes; + embed: HTMLAttributes; + fieldset: HTMLAttributes; + figcaption: HTMLAttributes; + figure: HTMLAttributes; + footer: HTMLAttributes; + form: HTMLAttributes; + h1: HTMLAttributes; + h2: HTMLAttributes; + h3: HTMLAttributes; + h4: HTMLAttributes; + h5: HTMLAttributes; + h6: HTMLAttributes; + head: HTMLAttributes; + header: HTMLAttributes; + hgroup: HTMLAttributes; + hr: HTMLAttributes; + html: HTMLAttributes; + i: HTMLAttributes; + iframe: HTMLAttributes; + img: HTMLAttributes; + input: HTMLAttributes; + ins: HTMLAttributes; + kbd: HTMLAttributes; + keygen: HTMLAttributes; + label: HTMLAttributes; + legend: HTMLAttributes; + li: HTMLAttributes; + link: HTMLAttributes; + main: HTMLAttributes; + map: HTMLAttributes; + mark: HTMLAttributes; + menu: HTMLAttributes; + menuitem: HTMLAttributes; + meta: HTMLAttributes; + meter: HTMLAttributes; + nav: HTMLAttributes; + noscript: HTMLAttributes; + object: HTMLAttributes; + ol: HTMLAttributes; + optgroup: HTMLAttributes; + option: HTMLAttributes; + output: HTMLAttributes; + p: HTMLAttributes; + param: HTMLAttributes; + picture: HTMLAttributes; + pre: HTMLAttributes; + progress: HTMLAttributes; + q: HTMLAttributes; + rp: HTMLAttributes; + rt: HTMLAttributes; + ruby: HTMLAttributes; + s: HTMLAttributes; + samp: HTMLAttributes; + script: HTMLAttributes; + section: HTMLAttributes; + select: HTMLAttributes; + slot: HTMLAttributes; + small: HTMLAttributes; + source: HTMLAttributes; + span: HTMLAttributes; + strong: HTMLAttributes; + style: HTMLAttributes; + sub: HTMLAttributes; + summary: HTMLAttributes; + sup: HTMLAttributes; + table: HTMLAttributes; + tbody: HTMLAttributes; + td: HTMLAttributes; + textarea: HTMLAttributes; + tfoot: HTMLAttributes; + th: HTMLAttributes; + thead: HTMLAttributes; + time: HTMLAttributes; + title: HTMLAttributes; + tr: HTMLAttributes; + track: HTMLAttributes; + u: HTMLAttributes; + ul: HTMLAttributes; + "var": HTMLAttributes; + video: HTMLAttributes; + wbr: HTMLAttributes; + + //SVG + svg: SVGAttributes; + animate: SVGAttributes; + circle: SVGAttributes; + clipPath: SVGAttributes; + defs: SVGAttributes; + desc: SVGAttributes; + ellipse: SVGAttributes; + feBlend: SVGAttributes; + feColorMatrix: SVGAttributes; + feComponentTransfer: SVGAttributes; + feComposite: SVGAttributes; + feConvolveMatrix: SVGAttributes; + feDiffuseLighting: SVGAttributes; + feDisplacementMap: SVGAttributes; + feFlood: SVGAttributes; + feGaussianBlur: SVGAttributes; + feImage: SVGAttributes; + feMerge: SVGAttributes; + feMergeNode: SVGAttributes; + feMorphology: SVGAttributes; + feOffset: SVGAttributes; + feSpecularLighting: SVGAttributes; + feTile: SVGAttributes; + feTurbulence: SVGAttributes; + filter: SVGAttributes; + foreignObject: SVGAttributes; + g: SVGAttributes; + image: SVGAttributes; + line: SVGAttributes; + linearGradient: SVGAttributes; + marker: SVGAttributes; + mask: SVGAttributes; + path: SVGAttributes; + pattern: SVGAttributes; + polygon: SVGAttributes; + polyline: SVGAttributes; + radialGradient: SVGAttributes; + rect: SVGAttributes; + stop: SVGAttributes; + symbol: SVGAttributes; + text: SVGAttributes; + tspan: SVGAttributes; + use: SVGAttributes; + } +} diff --git a/packages/vdom/src/vdom.ts b/packages/vdom/src/vdom.ts new file mode 100644 index 000000000..57afa5aef --- /dev/null +++ b/packages/vdom/src/vdom.ts @@ -0,0 +1,483 @@ +/*------------------------------------------------------------------------------ +| Copyright (c) 2014-2019, PhosphorJS Contributors +| +| Distributed under the terms of the BSD 3-Clause License. +| +| The full license is in the file LICENSE, distributed with this software. +|-----------------------------------------------------------------------------*/ +import { + ArrayExt +} from '@phosphor/algorithm'; + +import { + PJSX +} from './pjsx'; + +import { + VNode +} from './vnode'; + + +/** + * The namespace for the virtual DOM functionality. + */ +export +namespace VDOM { + /** + * Export `PJSX` as the namespace `JSX`. + */ + export + import JSX = PJSX; + + /** + * A type alias for VDOM props. + */ + export + type Props = PJSX.SpecialAttributes & Record; + + /** + * A type alias for a pure function component. + */ + export + type FC = (props: Props) => PJSX.Element; + + /** + * Create a virtual DOM node for the given content. + * + * @param type - The element tag or function component to create. + * + * @param props - The props for the component. + * + * @param children - The children for the component. + * + * @returns A new virtual node for the given parameters. + */ + export + function createElement(type: string | FC, props: Props | null, ...children: PJSX.Children[]): PJSX.Element { + return Private.createElement(type, props, children); + } + + /** + * The namespace for the `createElement` function statics. + */ + export + namespace createElement { + /** + * Export `PJSX` as the namespace `JSX`. + */ + export + import JSX = PJSX; + } + + /** + * Render virtual DOM content into a host element. + * + * @param content - The virtual content to render. + * + * @param host - The host element into which the content will be rendered. + */ + export + function render(content: PJSX.Children, host: Element): void { + Private.render(content, host); + } +} + + +/** + * The namespace for the module implementation details. + */ +namespace Private { + /** + * Create a virtual DOM node for the given parameters. + */ + export + function createElement(type: string | VDOM.FC, props: VDOM.Props | null, children: PJSX.Children[]): PJSX.Element { + let element: PJSX.Element; + if (typeof type === 'string') { + element = { tag: type, props: createProps(props, children) }; + } else { + element = type(createProps(props, children)); + } + return element; + } + + /** + * Render virtual DOM content into a host node. + */ + export + function render(content: PJSX.Children, host: Element): void { + // Fetch the old content. + let oldContent: VNode.Children = hostMap.get(host) || emptyArray; + + // Flatten the new content. + let newContent: VNode.Children = flattenChildren([content]); + + // Save the new content. + hostMap.set(host, newContent); + + // Update the host with the difference between old and new. + updateContent(host, oldContent, newContent); + } + + /** + * A weakmap of host element to rendered content. + */ + const hostMap = new WeakMap>(); + + /** + * A frozen empty VNode array. + */ + const emptyArray: ReadonlyArray = Object.freeze([]); + + /** + * A frozen empty VNode props object. + */ + const emptyProps: VNode.Props = Object.freeze({ children: emptyArray }); + + /** + * A frozen empty style attributes object. + */ + const emptyStyle: PJSX.StyleAttributes = Object.freeze({}); + + /** + * A type guard for DOM elements. + */ + function isElement(node: Node): node is Element { + return node.nodeType === Node.ELEMENT_NODE; + } + + /** + * Create the props for a virtual node. + */ + function createProps(props: VDOM.Props | null, children: PJSX.Children[]): VNode.Props { + // Flatten the children. + let kids = flattenChildren(children); + + // Clone and return the props. + return props ? { ...props, children: kids } : { children: kids }; + } + + /** + * Process virtual DOM children in a flat VNode array. + */ + function flattenChildren(children: PJSX.Children[]): VNode.Children { + // Return the frozen array singleton if there are no children. + if (children.length === 0) { + return emptyArray; + } + + // Set up the flat result array. + let result: VNode.Child[] = []; + + // Process each child. + children.forEach(process); + + // Return the result. + return result; + + // Process an element from the children array. + function process(child: PJSX.Children): void { + // Skip null children. + if (child === null) { + return; + } + + // Handle string children. + if (typeof child === 'string') { + result.push(child); + return; + } + + // Handle other primitive children. + if (typeof child === 'number' || typeof child === 'boolean') { + result.push(String(child)); + return; + } + + // Handle VNode children. + if (!Array.isArray(child)) { + result.push(child); + return; + } + + // Handle array children. + child.forEach(process); + } + } + + /** + * Create a new DOM node for the given virtual node. + */ + function createDOM(node: string): Text; + function createDOM(node: VNode): Element; + function createDOM(node: VNode | string): Element | Text; + function createDOM(node: VNode | string): Element | Text { + // Handle string content. + if (typeof node === 'string') { + return document.createTextNode(node); + } + + // Create the HTML element with the specified tag. + let element = document.createElement(node.tag); + + // Update the props of the element. + updateProps(element, emptyProps, node.props); + + // Update the content of the element. + updateContent(element, emptyArray, node.props.children); + + // Return the populated element. + return element; + } + + /** + * Find the index of a keyed vnode in a content array. + */ + function findKeyIndex(key: string | number, content: VNode.Children, start: number): number { + for (let i = start; i < content.length; ++i) { + let child = content[i]; + if (typeof child !== 'string' && key === child.props.key) { + return i; + } + } + return -1; + } + + /** + * Update a host element with the delta of the virtual content. + * + * This is the core "diff" algorithm. There is no explicit "patch" + * phase. The host is patched at each step as the diff progresses. + */ + function updateContent(host: Element, oldContent: VNode.Children, newContent: VNode.Children): void { + // Bail early if the content is identical. + if (oldContent === newContent) { + return; + } + + // Get a shallow copy of the content that can be mutated in-place. + let tmpContent = [...oldContent]; + + + // Set up the current node variable. + let currNode = host.firstChild; + + // Update the host with the new content. The diff always proceeds + // forward and never modifies a previously visited index. The tmp + // copy array is modified in-place to reflect the changes made to + // the host children. This causes the stale nodes to be pushed to + // the end of the host node and removed at the end of the loop. + for (let i = 0; i < newContent.length; ++i) { + // Look up the new child. + let newChild = newContent[i]; + + // If the old content is exhausted, create a new node. + if (i >= tmpContent.length) { + host.appendChild(createDOM(newChild)); + continue; + } + + // Sanity check the DOM state. + if (currNode === null) { + throw new Error('invalid VDOM state'); + } + + // Lookup the old child. + let oldChild = tmpContent[i]; + + // If both elements are identical, there is nothing to do. + if (oldChild === newChild) { + currNode = currNode.nextSibling; + continue; + } + + // Handle the simplest case of in-place text update first. + if (typeof oldChild === 'string' && typeof newChild === 'string') { + currNode.textContent = newChild; + currNode = currNode.nextSibling; + continue; + } + + // If the old or new node is a text node, the other node is now + // known to be an element node, so create and insert a new node. + if (typeof oldChild === 'string' || typeof newChild === 'string') { + host.insertBefore(createDOM(newChild), currNode); + ArrayExt.insert(tmpContent, i, newChild); + continue; + } + + // If the new elem is keyed, move an old keyed elem to the proper + // location before proceeding with the diff. The search can start + // at the current index, since the unmatched old keyed elems are + // pushed forward in the content array. + if (newChild.props.key !== undefined) { + let j = findKeyIndex(newChild.props.key, tmpContent, i); + if (j !== -1 && i !== j) { + let node = host.childNodes[j]; + host.insertBefore(node, currNode); + ArrayExt.move(tmpContent, j, i); + oldChild = tmpContent[i] as VNode; + currNode = node; + } + } + + // If both nodes are identical, there is nothing to do. + if (oldChild === newChild) { + currNode = currNode.nextSibling; + continue; + } + + // If the keys are different, create a new node. + if (oldChild.props.key !== newChild.props.key) { + host.insertBefore(createDOM(newChild), currNode); + ArrayExt.insert(tmpContent, i, newChild); + continue; + } + + // If the tags are different, create a new node. + if (oldChild.tag !== newChild.tag) { + host.insertBefore(createDOM(newChild), currNode); + ArrayExt.insert(tmpContent, i, newChild); + continue; + } + + // Sanity check the DOM state. + if (!isElement(currNode)) { + throw new Error('invalid virtual DOM state'); + } + + // Update the props of the current element. + updateProps(currNode, oldChild.props, newChild.props); + + // Update the content of the current element. + updateContent(currNode, oldChild.props.children, newChild.props.children); + + // Step to the next sibling element. + currNode = currNode.nextSibling; + } + + // Dispose of the old nodes pushed to the end of the host. + for (let n = tmpContent.length - newContent.length; n > 0; --n) { + host.removeChild(host.lastChild!); + } + } + + /** + * Update an element with the difference of props. + */ + function updateProps(element: Element, oldProps: VDOM.Props, newProps: VDOM.Props): void { + // Do nothing if the props are the same object. + if (oldProps === newProps) { + return; + } + + // Process the old props. + for (let name in oldProps) { + if (!(name in newProps)) { + setProp(element, name, oldProps[name], null); + } + } + + // Process the new props. + for (let name in newProps) { + if (name in oldProps) { + setProp(element, name, oldProps[name], newProps[name]); + } else { + setProp(element, name, null, newProps[name]); + } + } + } + + /** + * Apply a property difference to an element. + */ + function setProp(element: Element, name: string, oldValue: any | null, newValue: any | null): void { + // Skip the special `key` and `children` props. + if (name === 'key' || name === 'children') { + return; + } + + // Handle the special `ref` prop. + if (name === 'ref') { + if (oldValue) { + oldValue.current = null; + } + if (newValue) { + newValue.current = element; + } + return; + } + + // Bail early if the value does not change. + if (oldValue === newValue) { + return; + } + + // Handle the style props. + if (name === 'style') { + if (oldValue === null) { + oldValue = emptyStyle; + } + if (newValue === null) { + newValue = emptyStyle; + } + updateStyle((element as HTMLElement).style, oldValue, newValue); + return; + } + + // Handle inline event listeners. + if (name[0] === 'o' && name[1] === 'n') { + (element as any)[name] = newValue; + return; + } + + // Set or remove the attribute as appropriate. + if (newValue === false || newValue === null) { + element.removeAttribute(name); + } else if (newValue === true) { + element.setAttribute(name, ''); + } else { + element.setAttribute(name, newValue); + } + + // Special-case `input.value`. + if (name === 'value' && element.tagName === 'INPUT') { + (element as HTMLInputElement).value = newValue; + } + } + + /** + * Update a style declaration with the difference of style attributes. + */ + function updateStyle(style: CSSStyleDeclaration, oldAttrs: PJSX.StyleAttributes, newAttrs: PJSX.StyleAttributes): void { + // Bail early if the attr objects don't change. + if (oldAttrs === newAttrs) { + return; + } + + // Process the old attrs. + for (let name in oldAttrs) { + if (!(name in newAttrs)) { + setStyleAttr(style, name, oldAttrs[name], ''); + } + } + + // Process the new attrs. + for (let name in newAttrs) { + if (name in oldAttrs) { + setStyleAttr(style, name, oldAttrs[name], newAttrs[name]); + } else { + setStyleAttr(style, name, '', newAttrs[name]); + } + } + } + + /** + * Apply an attribute difference to a style declaration. + */ + function setStyleAttr(style: CSSStyleDeclaration, name: string, oldValue: string | number, newValue: string | number): void { + if (oldValue !== newValue) { + (style as any)[name] = String(newValue); + } + } +} diff --git a/packages/vdom/src/vnode.ts b/packages/vdom/src/vnode.ts new file mode 100644 index 000000000..6ee0eda80 --- /dev/null +++ b/packages/vdom/src/vnode.ts @@ -0,0 +1,88 @@ +/*------------------------------------------------------------------------------ +| Copyright (c) 2014-2019, PhosphorJS Contributors +| +| Distributed under the terms of the BSD 3-Clause License. +| +| The full license is in the file LICENSE, distributed with this software. +|-----------------------------------------------------------------------------*/ + + +/** + * A type alias for a virtual node. + */ +export +type VNode = { + /** + * The element tag name. + */ + readonly tag: string; + + /** + * The element props. + */ + readonly props: VNode.Props; +}; + + +/** + * The namespace for the `VNode` type statics. + */ +export +namespace VNode { + /** + * A type alias for a node child. + */ + export + type Child = VNode | string; + + /** + * A type alias for VNode children. + */ + export + type Children = ReadonlyArray; + + /** + * A type alias for a node key. + */ + export + type Key = string | number; + + /** + * A type alias for a node ref. + */ + export + type Ref = { current?: HTMLElement | null }; + + /** + * A type alias for intrinsic node props. + */ + export + type IntrinsicProps = { + /** + * The children of the node. + */ + readonly children: Children; + + /** + * The key for the node. + */ + readonly key?: Key; + + /** + * The ref for the node. + */ + readonly ref?: Ref; + }; + + /** + * A type alias for the attribute node props. + */ + export + type AttributeProps = Readonly>; + + /** + * A type alias for the node props. + */ + export + type Props = IntrinsicProps & AttributeProps; +} diff --git a/packages/vdom/tsconfig.json b/packages/vdom/tsconfig.json new file mode 100644 index 000000000..5f8158f37 --- /dev/null +++ b/packages/vdom/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "declaration": true, + "noImplicitAny": true, + "noEmitOnError": true, + "noUnusedLocals": true, + "strictNullChecks": true, + "module": "commonjs", + "moduleResolution": "node", + "target": "ES5", + "outDir": "lib", + "lib": ["es2015", "dom"], + "types": [], + "rootDir": "src" + }, + "include": ["src/*"] +}