UNPKG

@lumino/virtualdom

Version:
1,690 lines (1,597 loc) 48 kB
// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. /*----------------------------------------------------------------------------- | Copyright (c) 2014-2017, PhosphorJS Contributors | | Distributed under the terms of the BSD 3-Clause License. | | The full license is in the file LICENSE, distributed with this software. |----------------------------------------------------------------------------*/ /** * @packageDocumentation * @module virtualdom */ import { ArrayExt } from '@lumino/algorithm'; /** * The names of the supported HTML5 DOM element attributes. * * This list is not all-encompassing, rather it attempts to define the * attribute names which are relevant for use in a virtual DOM context. * If a standardized or widely supported name is missing, please open * an issue to have it added. * * The attribute names were collected from the following sources: * - https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes * - https://www.w3.org/TR/html5/index.html#attributes-1 * - https://html.spec.whatwg.org/multipage/indices.html#attributes-3 */ export type ElementAttrNames = | 'abbr' | 'accept' | 'accept-charset' | 'accesskey' | 'action' | 'allowfullscreen' | 'alt' | 'autocomplete' | 'autofocus' | 'autoplay' | 'autosave' | 'checked' | 'cite' | 'cols' | 'colspan' | 'contenteditable' | 'controls' | 'coords' | 'crossorigin' | 'data' | 'datetime' | 'default' | 'dir' | 'dirname' | 'disabled' | 'download' | 'draggable' | 'dropzone' | 'enctype' | 'form' | 'formaction' | 'formenctype' | 'formmethod' | 'formnovalidate' | 'formtarget' | 'headers' | 'height' | 'hidden' | 'high' | 'href' | 'hreflang' | 'id' | 'inputmode' | 'integrity' | 'ismap' | 'kind' | 'label' | 'lang' | 'list' | 'loop' | 'low' | 'max' | 'maxlength' | 'media' | 'mediagroup' | 'method' | 'min' | 'minlength' | 'multiple' | 'muted' | 'name' | 'novalidate' | 'optimum' | 'pattern' | 'placeholder' | 'poster' | 'preload' | 'readonly' | 'rel' | 'required' | 'reversed' | 'rows' | 'rowspan' | 'sandbox' | 'scope' | 'selected' | 'shape' | 'size' | 'sizes' | 'span' | 'spellcheck' | 'src' | 'srcdoc' | 'srclang' | 'srcset' | 'start' | 'step' | 'tabindex' | 'target' | 'title' | 'type' | 'typemustmatch' | 'usemap' | 'value' | 'width' | 'wrap'; /** * The names of ARIA attributes for HTML elements. * * The attribute names are collected from * https://www.w3.org/TR/html5/infrastructure.html#element-attrdef-aria-role */ export type ARIAAttrNames = | 'aria-activedescendant' | 'aria-atomic' | 'aria-autocomplete' | 'aria-busy' | 'aria-checked' | 'aria-colcount' | 'aria-colindex' | 'aria-colspan' | 'aria-controls' | 'aria-current' | 'aria-describedby' | 'aria-details' | 'aria-dialog' | 'aria-disabled' | 'aria-dropeffect' | 'aria-errormessage' | 'aria-expanded' | 'aria-flowto' | 'aria-grabbed' | 'aria-haspopup' | 'aria-hidden' | 'aria-invalid' | 'aria-keyshortcuts' | 'aria-label' | 'aria-labelledby' | 'aria-level' | 'aria-live' | 'aria-multiline' | 'aria-multiselectable' | 'aria-orientation' | 'aria-owns' | 'aria-placeholder' | 'aria-posinset' | 'aria-pressed' | 'aria-readonly' | 'aria-relevant' | 'aria-required' | 'aria-roledescription' | 'aria-rowcount' | 'aria-rowindex' | 'aria-rowspan' | 'aria-selected' | 'aria-setsize' | 'aria-sort' | 'aria-valuemax' | 'aria-valuemin' | 'aria-valuenow' | 'aria-valuetext' | 'role'; /** * The names of the supported HTML5 CSS property names. * * If a standardized or widely supported name is missing, please open * an issue to have it added. * * The property names were collected from the following sources: * - TypeScript's `lib.dom.d.ts` file */ export type CSSPropertyNames = | 'alignContent' | 'alignItems' | 'alignSelf' | 'alignmentBaseline' | 'animation' | 'animationDelay' | 'animationDirection' | 'animationDuration' | 'animationFillMode' | 'animationIterationCount' | 'animationName' | 'animationPlayState' | 'animationTimingFunction' | 'backfaceVisibility' | 'background' | 'backgroundAttachment' | 'backgroundClip' | 'backgroundColor' | 'backgroundImage' | 'backgroundOrigin' | 'backgroundPosition' | 'backgroundPositionX' | 'backgroundPositionY' | 'backgroundRepeat' | 'backgroundSize' | 'baselineShift' | 'border' | 'borderBottom' | 'borderBottomColor' | 'borderBottomLeftRadius' | 'borderBottomRightRadius' | 'borderBottomStyle' | 'borderBottomWidth' | 'borderCollapse' | 'borderColor' | 'borderImage' | 'borderImageOutset' | 'borderImageRepeat' | 'borderImageSlice' | 'borderImageSource' | 'borderImageWidth' | 'borderLeft' | 'borderLeftColor' | 'borderLeftStyle' | 'borderLeftWidth' | 'borderRadius' | 'borderRight' | 'borderRightColor' | 'borderRightStyle' | 'borderRightWidth' | 'borderSpacing' | 'borderStyle' | 'borderTop' | 'borderTopColor' | 'borderTopLeftRadius' | 'borderTopRightRadius' | 'borderTopStyle' | 'borderTopWidth' | 'borderWidth' | 'bottom' | 'boxShadow' | 'boxSizing' | 'breakAfter' | 'breakBefore' | 'breakInside' | 'captionSide' | 'clear' | 'clip' | 'clipPath' | 'clipRule' | 'color' | 'colorInterpolationFilters' | 'columnCount' | 'columnFill' | 'columnGap' | 'columnRule' | 'columnRuleColor' | 'columnRuleStyle' | 'columnRuleWidth' | 'columnSpan' | 'columnWidth' | 'columns' | 'content' | 'counterIncrement' | 'counterReset' | 'cssFloat' | 'cssText' | 'cursor' | 'direction' | 'display' | 'dominantBaseline' | 'emptyCells' | 'enableBackground' | 'fill' | 'fillOpacity' | 'fillRule' | 'filter' | 'flex' | 'flexBasis' | 'flexDirection' | 'flexFlow' | 'flexGrow' | 'flexShrink' | 'flexWrap' | 'floodColor' | 'floodOpacity' | 'font' | 'fontFamily' | 'fontFeatureSettings' | 'fontSize' | 'fontSizeAdjust' | 'fontStretch' | 'fontStyle' | 'fontVariant' | 'fontWeight' | 'glyphOrientationHorizontal' | 'glyphOrientationVertical' | 'height' | 'imeMode' | 'justifyContent' | 'kerning' | 'left' | 'letterSpacing' | 'lightingColor' | 'lineHeight' | 'listStyle' | 'listStyleImage' | 'listStylePosition' | 'listStyleType' | 'margin' | 'marginBottom' | 'marginLeft' | 'marginRight' | 'marginTop' | 'marker' | 'markerEnd' | 'markerMid' | 'markerStart' | 'mask' | 'maxHeight' | 'maxWidth' | 'minHeight' | 'minWidth' | 'msContentZoomChaining' | 'msContentZoomLimit' | 'msContentZoomLimitMax' | 'msContentZoomLimitMin' | 'msContentZoomSnap' | 'msContentZoomSnapPoints' | 'msContentZoomSnapType' | 'msContentZooming' | 'msFlowFrom' | 'msFlowInto' | 'msFontFeatureSettings' | 'msGridColumn' | 'msGridColumnAlign' | 'msGridColumnSpan' | 'msGridColumns' | 'msGridRow' | 'msGridRowAlign' | 'msGridRowSpan' | 'msGridRows' | 'msHighContrastAdjust' | 'msHyphenateLimitChars' | 'msHyphenateLimitLines' | 'msHyphenateLimitZone' | 'msHyphens' | 'msImeAlign' | 'msOverflowStyle' | 'msScrollChaining' | 'msScrollLimit' | 'msScrollLimitXMax' | 'msScrollLimitXMin' | 'msScrollLimitYMax' | 'msScrollLimitYMin' | 'msScrollRails' | 'msScrollSnapPointsX' | 'msScrollSnapPointsY' | 'msScrollSnapType' | 'msScrollSnapX' | 'msScrollSnapY' | 'msScrollTranslation' | 'msTextCombineHorizontal' | 'msTextSizeAdjust' | 'msTouchAction' | 'msTouchSelect' | 'msUserSelect' | 'msWrapFlow' | 'msWrapMargin' | 'msWrapThrough' | 'opacity' | 'order' | 'orphans' | 'outline' | 'outlineColor' | 'outlineStyle' | 'outlineWidth' | 'overflow' | 'overflowX' | 'overflowY' | 'padding' | 'paddingBottom' | 'paddingLeft' | 'paddingRight' | 'paddingTop' | 'pageBreakAfter' | 'pageBreakBefore' | 'pageBreakInside' | 'perspective' | 'perspectiveOrigin' | 'pointerEvents' | 'position' | 'quotes' | 'resize' | 'right' | 'rubyAlign' | 'rubyOverhang' | 'rubyPosition' | 'stopColor' | 'stopOpacity' | 'stroke' | 'strokeDasharray' | 'strokeDashoffset' | 'strokeLinecap' | 'strokeLinejoin' | 'strokeMiterlimit' | 'strokeOpacity' | 'strokeWidth' | 'tableLayout' | 'textAlign' | 'textAlignLast' | 'textAnchor' | 'textDecoration' | 'textIndent' | 'textJustify' | 'textKashida' | 'textKashidaSpace' | 'textOverflow' | 'textShadow' | 'textTransform' | 'textUnderlinePosition' | 'top' | 'touchAction' | 'transform' | 'transformOrigin' | 'transformStyle' | 'transition' | 'transitionDelay' | 'transitionDuration' | 'transitionProperty' | 'transitionTimingFunction' | 'unicodeBidi' | 'verticalAlign' | 'visibility' | 'webkitAlignContent' | 'webkitAlignItems' | 'webkitAlignSelf' | 'webkitAnimation' | 'webkitAnimationDelay' | 'webkitAnimationDirection' | 'webkitAnimationDuration' | 'webkitAnimationFillMode' | 'webkitAnimationIterationCount' | 'webkitAnimationName' | 'webkitAnimationPlayState' | 'webkitAnimationTimingFunction' | 'webkitAppearance' | 'webkitBackfaceVisibility' | 'webkitBackgroundClip' | 'webkitBackgroundOrigin' | 'webkitBackgroundSize' | 'webkitBorderBottomLeftRadius' | 'webkitBorderBottomRightRadius' | 'webkitBorderImage' | 'webkitBorderRadius' | 'webkitBorderTopLeftRadius' | 'webkitBorderTopRightRadius' | 'webkitBoxAlign' | 'webkitBoxDirection' | 'webkitBoxFlex' | 'webkitBoxOrdinalGroup' | 'webkitBoxOrient' | 'webkitBoxPack' | 'webkitBoxSizing' | 'webkitColumnBreakAfter' | 'webkitColumnBreakBefore' | 'webkitColumnBreakInside' | 'webkitColumnCount' | 'webkitColumnGap' | 'webkitColumnRule' | 'webkitColumnRuleColor' | 'webkitColumnRuleStyle' | 'webkitColumnRuleWidth' | 'webkitColumnSpan' | 'webkitColumnWidth' | 'webkitColumns' | 'webkitFilter' | 'webkitFlex' | 'webkitFlexBasis' | 'webkitFlexDirection' | 'webkitFlexFlow' | 'webkitFlexGrow' | 'webkitFlexShrink' | 'webkitFlexWrap' | 'webkitJustifyContent' | 'webkitOrder' | 'webkitPerspective' | 'webkitPerspectiveOrigin' | 'webkitTapHighlightColor' | 'webkitTextFillColor' | 'webkitTextSizeAdjust' | 'webkitTransform' | 'webkitTransformOrigin' | 'webkitTransformStyle' | 'webkitTransition' | 'webkitTransitionDelay' | 'webkitTransitionDuration' | 'webkitTransitionProperty' | 'webkitTransitionTimingFunction' | 'webkitUserModify' | 'webkitUserSelect' | 'webkitWritingMode' | 'whiteSpace' | 'widows' | 'width' | 'wordBreak' | 'wordSpacing' | 'wordWrap' | 'writingMode' | 'zIndex' | 'zoom'; /** * A mapping of inline event name to event object type. * * This mapping is used to create the event listener properties for * the virtual DOM element attributes object. If a standardized or * widely supported name is missing, please open an issue to have it * added. * * The event names were collected from the following sources: * - TypeScript's `lib.dom.d.ts` file * - https://www.w3.org/TR/html5/index.html#attributes-1 * - https://html.spec.whatwg.org/multipage/webappapis.html#idl-definitions */ export type ElementEventMap = { onabort: UIEvent; onauxclick: MouseEvent; onblur: FocusEvent; oncanplay: Event; oncanplaythrough: Event; onchange: Event; onclick: MouseEvent; oncontextmenu: PointerEvent; oncopy: ClipboardEvent; oncuechange: Event; oncut: ClipboardEvent; ondblclick: MouseEvent; ondrag: DragEvent; ondragend: DragEvent; ondragenter: DragEvent; ondragexit: DragEvent; ondragleave: DragEvent; ondragover: DragEvent; ondragstart: DragEvent; ondrop: DragEvent; ondurationchange: Event; onemptied: Event; onended: ErrorEvent; onerror: ErrorEvent; onfocus: FocusEvent; oninput: Event; oninvalid: Event; onkeydown: KeyboardEvent; onkeypress: KeyboardEvent; onkeyup: KeyboardEvent; onload: Event; onloadeddata: Event; onloadedmetadata: Event; onloadend: Event; onloadstart: Event; onmousedown: MouseEvent; onmouseenter: MouseEvent; onmouseleave: MouseEvent; onmousemove: MouseEvent; onmouseout: MouseEvent; onmouseover: MouseEvent; onmouseup: MouseEvent; onmousewheel: WheelEvent; onpaste: ClipboardEvent; onpause: Event; onplay: Event; onplaying: Event; onpointercancel: PointerEvent; onpointerdown: PointerEvent; onpointerenter: PointerEvent; onpointerleave: PointerEvent; onpointermove: PointerEvent; onpointerout: PointerEvent; onpointerover: PointerEvent; onpointerup: PointerEvent; onprogress: ProgressEvent; onratechange: Event; onreset: Event; onscroll: UIEvent; onseeked: Event; onseeking: Event; onselect: UIEvent; onselectstart: Event; onstalled: Event; onsubmit: Event; onsuspend: Event; ontimeupdate: Event; onvolumechange: Event; onwaiting: Event; }; /** * An object which represents a dataset for a virtual DOM element. * * The names of the dataset properties will be automatically prefixed * with `data-` before being added to the node, e.g. `{ thing: '12' }` * will be rendered as `data-thing='12'` in the DOM element. * * Dataset property names should not contain spaces. */ export type ElementDataset = { readonly [name: string]: string; }; /** * The inline style for for a virtual DOM element. * * Style attributes use the JS camel-cased property names instead of * the CSS hyphenated names for performance and security. */ export type ElementInlineStyle = { readonly [T in CSSPropertyNames]?: string; }; /** * The ARIA attributes for a virtual element node. * * These are the attributes which are applied to a real DOM element via * `element.setAttribute()`. The supported attribute names are defined * by the `ARIAAttrNames` type. */ export type ElementARIAAttrs = { readonly [T in ARIAAttrNames]?: string; }; /** * The base attributes for a virtual element node. * * These are the attributes which are applied to a real DOM element via * `element.setAttribute()`. The supported attribute names are defined * by the `ElementAttrNames` type. * * Node attributes are specified using the lower-case HTML name instead * of the camel-case JS name due to browser inconsistencies in handling * the JS versions. */ export type ElementBaseAttrs = { readonly [T in ElementAttrNames]?: string; }; /** * The inline event listener attributes for a virtual element node. * * The supported listeners are defined by the `ElementEventMap` type. */ export type ElementEventAttrs = { readonly [T in keyof ElementEventMap]?: ( this: HTMLElement, event: ElementEventMap[T] ) => any; }; /** * The special-cased attributes for a virtual element node. */ export type ElementSpecialAttrs = { /** * The key id for the virtual element node. * * If a node is given a key id, the generated DOM node will not be * recreated during a rendering update if it only moves among its * siblings in the render tree. * * In general, reordering child nodes will cause the nodes to be * completely re-rendered. Keys allow this to be optimized away. * * If a key is provided, it must be unique among sibling nodes. */ readonly key?: string; /** * The JS-safe name for the HTML `class` attribute. */ readonly className?: string; /** * The JS-safe name for the HTML `for` attribute. */ readonly htmlFor?: string; /** * The dataset for the rendered DOM element. */ readonly dataset?: ElementDataset; /** * The inline style for the rendered DOM element. */ readonly style?: ElementInlineStyle; }; /** * The full set of attributes supported by a virtual element node. * * This is the combination of the base element attributes, the the ARIA attributes, * the inline element event listeners, and the special element attributes. */ export type ElementAttrs = ElementBaseAttrs & ElementARIAAttrs & ElementEventAttrs & ElementSpecialAttrs; /** * A virtual node which represents plain text content. * * #### Notes * User code will not typically create a `VirtualText` node directly. * Instead, the `h()` function will be used to create an element tree. */ export class VirtualText { /** * The text content for the node. */ readonly content: string; /** * The type of the node. * * This value can be used as a type guard for discriminating the * `VirtualNode` union type. */ readonly type = 'text' as const; /** * Construct a new virtual text node. * * @param content - The text content for the node. */ constructor(content: string) { this.content = content; } } /** * A virtual node which represents an HTML element. * * #### Notes * User code will not typically create a `VirtualElement` node directly. * Instead, the `h()` function will be used to create an element tree. */ export class VirtualElement { /** * The tag name for the element. */ readonly tag: string; /** * The attributes for the element. */ readonly attrs: ElementAttrs; /** * The children for the element. */ readonly children: ReadonlyArray<VirtualNode>; /** * An optional custom renderer for the element's children. If set, on render * this element's DOM node and it's attrs will be created/updated as normal. * At that point the DOM node is handed off to the renderer. */ readonly renderer: VirtualElement.IRenderer | undefined; /** * The type of the node. * * This value can be used as a type guard for discriminating the * `VirtualNode` union type. */ readonly type = 'element' as const; /** * Construct a new virtual element node. * * @param tag - The element tag name. * * @param attrs - The element attributes. * * @param children - The element children. * * @param renderer - An optional custom renderer for the element. */ constructor( tag: string, attrs: ElementAttrs, children: ReadonlyArray<VirtualNode>, renderer?: VirtualElement.IRenderer ) { this.tag = tag; this.attrs = attrs; this.children = children; this.renderer = renderer; } } export namespace VirtualElement { /** * A type describing a custom element renderer */ export type IRenderer = { /** * Customize how a DOM node is rendered. If .renderer is set on a given * instance of VirtualElement, this function will be called every time * that VirtualElement is rendered. * * @param host - The actual DOM node created for a VirtualElement during * rendering. * * On render, host is created and its attrs are set/updated via * the standard routines in updateContent. host is then handed off to this * function. * * The render function is free to modify host. The only restriction is * is that render should not modify any attributes set by external * routines (ie updateContent), as this may cause thrashing when the * virtual element is next rendered. * * @param options - Will be populated with the .attrs and .children fields * set on the VirtualElement being rendered. */ render: ( host: HTMLElement, options?: { attrs?: ElementAttrs; children?: ReadonlyArray<VirtualNode> } ) => void; /** * Optional cleanup function for custom renderers. If the .renderer field * of a VirtualELement is set, and if .renderer.unrender is defined, when * the element is changed or removed its corresponding DOM element will be * passed to this function immediately before it is removed from the DOM. * * unrender is not required for for simple renderers, such as those * implemented using `document.createElement()`. However, for certain * rendering techniques explicit cleanup is required in order to avoid * resource leaks. * * For example, if render calls `ReactDOM.render(..., host)`, then * there has to also be a corresponding implementation of unrender that * calls `ReactDOM.unmountComponentAtNode(host)` in order to prevent * a memory leak. * * @param host - the DOM element to be removed. * * @param options - Will be populated with the .attrs and .children fields * set on the VirtualElement being unrendered. */ unrender?: ( host: HTMLElement, options?: { attrs?: ElementAttrs; children?: ReadonlyArray<VirtualNode> } ) => void; }; } /** * DEPRECATED - use VirtualElement with a defined renderer param instead. * This class is provided as a backwards compatibility shim * * A "pass thru" virtual node whose children are managed by a render and an * unrender callback. The intent of this flavor of virtual node is to make * it easy to blend other kinds of virtualdom (eg React) into Phosphor's * virtualdom. * * #### Notes * User code will not typically create a `VirtualElementPass` node directly. * Instead, the `hpass()` function will be used to create an element tree. */ export class VirtualElementPass extends VirtualElement { /** * DEPRECATED - use VirtualElement with a defined renderer param instead * * Construct a new virtual element pass thru node. * * @param tag - the tag of the parent element of this node. Once the parent * element is rendered, it will be passed as an argument to * renderer.render * * @param attrs - attributes that will assigned to the * parent element * * @param renderer - an object with render and unrender * functions, each of which should take a single argument of type * HTMLElement and return nothing. If null, the parent element * will be rendered barren without any children. */ constructor( tag: string, attrs: ElementAttrs, renderer: VirtualElementPass.IRenderer | null ) { super(tag, attrs, [], renderer || undefined); } } export namespace VirtualElementPass { /** * DEPRECATED - use VirtualElement.IRenderer instead * * A type describing a custom element renderer */ export type IRenderer = VirtualElement.IRenderer; } /** * A type alias for a general virtual node. */ export type VirtualNode = VirtualElement | VirtualText; /** * Create a new virtual element node. * * @param tag - The tag name for the element. * * @param attrs - The attributes for the element, if any. * * @param renderer - An optional custom renderer for the element. * * @param children - The children for the element, if any. * * @returns A new virtual element node for the given parameters. * * #### Notes * The children may be string literals, other virtual nodes, `null`, or * an array of those things. Strings are converted into text nodes, and * arrays are inlined as if the array contents were given as positional * arguments. This makes it simple to build up an array of children by * any desired means. `null` child values are simply ignored. * * A bound function for each HTML tag name is available as a static * function attached to the `h()` function. E.g. `h('div', ...)` is * equivalent to `h.div(...)`. */ export function h(tag: string, ...children: h.Child[]): VirtualElement; export function h( tag: string, attrs: ElementAttrs, ...children: h.Child[] ): VirtualElement; export function h( tag: string, renderer: VirtualElement.IRenderer, ...children: h.Child[] ): VirtualElement; export function h( tag: string, attrs: ElementAttrs, renderer: VirtualElement.IRenderer, ...children: h.Child[] ): VirtualElement; export function h(tag: string): VirtualElement { let attrs: ElementAttrs = {}; let renderer: VirtualElement.IRenderer | undefined; let children: VirtualNode[] = []; for (let i = 1, n = arguments.length; i < n; ++i) { // eslint-disable-next-line prefer-rest-params let arg = arguments[i]; if (typeof arg === 'string') { children.push(new VirtualText(arg)); } else if (arg instanceof VirtualText) { children.push(arg); } else if (arg instanceof VirtualElement) { children.push(arg); } else if (arg instanceof Array) { extend(children, arg); } else if ((i === 1 || i === 2) && arg && typeof arg === 'object') { if ('render' in arg) { renderer = arg; } else { attrs = arg; } } } return new VirtualElement(tag, attrs, children, renderer); function extend(array: VirtualNode[], values: h.Child[]): void { for (let child of values) { if (typeof child === 'string') { array.push(new VirtualText(child)); } else if (child instanceof VirtualText) { array.push(child); } else if (child instanceof VirtualElement) { array.push(child); } } } } /** * The namespace for the `h` function statics. */ export namespace h { /** * A type alias for the supported child argument types. */ export type Child = | (string | VirtualNode | null) | Array<string | VirtualNode | null>; /** * A bound factory function for a specific `h()` tag. */ export interface IFactory { (...children: Child[]): VirtualElement; (attrs: ElementAttrs, ...children: Child[]): VirtualElement; ( renderer: VirtualElement.IRenderer, ...children: h.Child[] ): VirtualElement; ( attrs: ElementAttrs, renderer: VirtualElement.IRenderer, ...children: h.Child[] ): VirtualElement; } export const a: IFactory = h.bind(undefined, 'a'); export const abbr: IFactory = h.bind(undefined, 'abbr'); export const address: IFactory = h.bind(undefined, 'address'); export const area: IFactory = h.bind(undefined, 'area'); export const article: IFactory = h.bind(undefined, 'article'); export const aside: IFactory = h.bind(undefined, 'aside'); export const audio: IFactory = h.bind(undefined, 'audio'); export const b: IFactory = h.bind(undefined, 'b'); export const bdi: IFactory = h.bind(undefined, 'bdi'); export const bdo: IFactory = h.bind(undefined, 'bdo'); export const blockquote: IFactory = h.bind(undefined, 'blockquote'); export const br: IFactory = h.bind(undefined, 'br'); export const button: IFactory = h.bind(undefined, 'button'); export const canvas: IFactory = h.bind(undefined, 'canvas'); export const caption: IFactory = h.bind(undefined, 'caption'); export const cite: IFactory = h.bind(undefined, 'cite'); export const code: IFactory = h.bind(undefined, 'code'); export const col: IFactory = h.bind(undefined, 'col'); export const colgroup: IFactory = h.bind(undefined, 'colgroup'); export const data: IFactory = h.bind(undefined, 'data'); export const datalist: IFactory = h.bind(undefined, 'datalist'); export const dd: IFactory = h.bind(undefined, 'dd'); export const del: IFactory = h.bind(undefined, 'del'); export const dfn: IFactory = h.bind(undefined, 'dfn'); export const div: IFactory = h.bind(undefined, 'div'); export const dl: IFactory = h.bind(undefined, 'dl'); export const dt: IFactory = h.bind(undefined, 'dt'); export const em: IFactory = h.bind(undefined, 'em'); export const embed: IFactory = h.bind(undefined, 'embed'); export const fieldset: IFactory = h.bind(undefined, 'fieldset'); export const figcaption: IFactory = h.bind(undefined, 'figcaption'); export const figure: IFactory = h.bind(undefined, 'figure'); export const footer: IFactory = h.bind(undefined, 'footer'); export const form: IFactory = h.bind(undefined, 'form'); export const h1: IFactory = h.bind(undefined, 'h1'); export const h2: IFactory = h.bind(undefined, 'h2'); export const h3: IFactory = h.bind(undefined, 'h3'); export const h4: IFactory = h.bind(undefined, 'h4'); export const h5: IFactory = h.bind(undefined, 'h5'); export const h6: IFactory = h.bind(undefined, 'h6'); export const header: IFactory = h.bind(undefined, 'header'); export const hr: IFactory = h.bind(undefined, 'hr'); export const i: IFactory = h.bind(undefined, 'i'); export const iframe: IFactory = h.bind(undefined, 'iframe'); export const img: IFactory = h.bind(undefined, 'img'); export const input: IFactory = h.bind(undefined, 'input'); export const ins: IFactory = h.bind(undefined, 'ins'); export const kbd: IFactory = h.bind(undefined, 'kbd'); export const label: IFactory = h.bind(undefined, 'label'); export const legend: IFactory = h.bind(undefined, 'legend'); export const li: IFactory = h.bind(undefined, 'li'); export const main: IFactory = h.bind(undefined, 'main'); export const map: IFactory = h.bind(undefined, 'map'); export const mark: IFactory = h.bind(undefined, 'mark'); export const meter: IFactory = h.bind(undefined, 'meter'); export const nav: IFactory = h.bind(undefined, 'nav'); export const noscript: IFactory = h.bind(undefined, 'noscript'); export const object: IFactory = h.bind(undefined, 'object'); export const ol: IFactory = h.bind(undefined, 'ol'); export const optgroup: IFactory = h.bind(undefined, 'optgroup'); export const option: IFactory = h.bind(undefined, 'option'); export const output: IFactory = h.bind(undefined, 'output'); export const p: IFactory = h.bind(undefined, 'p'); export const param: IFactory = h.bind(undefined, 'param'); export const pre: IFactory = h.bind(undefined, 'pre'); export const progress: IFactory = h.bind(undefined, 'progress'); export const q: IFactory = h.bind(undefined, 'q'); export const rp: IFactory = h.bind(undefined, 'rp'); export const rt: IFactory = h.bind(undefined, 'rt'); export const ruby: IFactory = h.bind(undefined, 'ruby'); export const s: IFactory = h.bind(undefined, 's'); export const samp: IFactory = h.bind(undefined, 'samp'); export const section: IFactory = h.bind(undefined, 'section'); export const select: IFactory = h.bind(undefined, 'select'); export const small: IFactory = h.bind(undefined, 'small'); export const source: IFactory = h.bind(undefined, 'source'); export const span: IFactory = h.bind(undefined, 'span'); export const strong: IFactory = h.bind(undefined, 'strong'); export const sub: IFactory = h.bind(undefined, 'sub'); export const summary: IFactory = h.bind(undefined, 'summary'); export const sup: IFactory = h.bind(undefined, 'sup'); export const table: IFactory = h.bind(undefined, 'table'); export const tbody: IFactory = h.bind(undefined, 'tbody'); export const td: IFactory = h.bind(undefined, 'td'); export const textarea: IFactory = h.bind(undefined, 'textarea'); export const tfoot: IFactory = h.bind(undefined, 'tfoot'); export const th: IFactory = h.bind(undefined, 'th'); export const thead: IFactory = h.bind(undefined, 'thead'); export const time: IFactory = h.bind(undefined, 'time'); export const title: IFactory = h.bind(undefined, 'title'); export const tr: IFactory = h.bind(undefined, 'tr'); export const track: IFactory = h.bind(undefined, 'track'); export const u: IFactory = h.bind(undefined, 'u'); export const ul: IFactory = h.bind(undefined, 'ul'); export const var_: IFactory = h.bind(undefined, 'var'); export const video: IFactory = h.bind(undefined, 'video'); export const wbr: IFactory = h.bind(undefined, 'wbr'); } /** * DEPRECATED - pass the renderer arg to the h function instead * * Create a new "pass thru" virtual element node. * * @param tag - The tag name for the parent element. * * @param attrs - The attributes for the parent element, if any. * * @param renderer - an object with render and unrender functions, if any. * * @returns A new "pass thru" virtual element node for the given parameters. * */ export function hpass( tag: string, renderer?: VirtualElementPass.IRenderer ): VirtualElementPass; export function hpass( tag: string, attrs: ElementAttrs, renderer?: VirtualElementPass.IRenderer ): VirtualElementPass; export function hpass(tag: string): VirtualElementPass { let attrs: ElementAttrs = {}; let renderer: VirtualElementPass.IRenderer | null = null; if (arguments.length === 2) { // eslint-disable-next-line prefer-rest-params const arg = arguments[1]; if ('render' in arg) { renderer = arg; } else { attrs = arg; } } else if (arguments.length === 3) { // eslint-disable-next-line prefer-rest-params attrs = arguments[1]; // eslint-disable-next-line prefer-rest-params renderer = arguments[2]; } else if (arguments.length > 3) { throw new Error('hpass() should be called with 1, 2, or 3 arguments'); } return new VirtualElementPass(tag, attrs, renderer); } /** * The namespace for the virtual DOM rendering functions. */ export namespace VirtualDOM { /** * Create a real DOM element from a virtual element node. * * @param node - The virtual element node to realize. * * @returns A new DOM element for the given virtual element node. * * #### Notes * This creates a brand new *real* DOM element with a structure which * matches the given virtual DOM node. * * If virtual diffing is desired, use the `render` function instead. */ export function realize(node: VirtualText): Text; export function realize(node: VirtualElement): HTMLElement; export function realize(node: VirtualNode): HTMLElement | Text { return Private.createDOMNode(node); } /** * Render virtual DOM content into a host element. * * @param content - The virtual DOM content to render. * * @param host - The host element for the rendered content. * * #### Notes * This renders the delta from the previous rendering. It assumes that * the content of the host element is not manipulated by external code. * * Providing `null` content will clear the rendering. * * Externally modifying the provided content or the host element will * result in undefined rendering behavior. */ export function render( content: VirtualNode | ReadonlyArray<VirtualNode> | null, host: HTMLElement ): void { let oldContent = Private.hostMap.get(host) || []; let newContent = Private.asContentArray(content); Private.hostMap.set(host, newContent); Private.updateContent(host, oldContent, newContent); } } /** * The namespace for the module implementation details. */ namespace Private { /** * A weak mapping of host element to virtual DOM content. */ export const hostMap = new WeakMap<HTMLElement, ReadonlyArray<VirtualNode>>(); /** * Cast a content value to a content array. */ export function asContentArray( value: VirtualNode | ReadonlyArray<VirtualNode> | null ): ReadonlyArray<VirtualNode> { if (!value) { return []; } if (value instanceof Array) { return value as ReadonlyArray<VirtualNode>; } return [value as VirtualNode]; } /** * Create a new DOM element for a virtual node. */ export function createDOMNode(node: VirtualText): Text; export function createDOMNode(node: VirtualElement): HTMLElement; export function createDOMNode(node: VirtualNode): HTMLElement | Text; export function createDOMNode( node: VirtualNode, host: HTMLElement | null ): HTMLElement | Text; export function createDOMNode( node: VirtualNode, host: HTMLElement | null, before: Node | null ): HTMLElement | Text; export function createDOMNode(node: VirtualNode): HTMLElement | Text { // eslint-disable-next-line prefer-rest-params let host = arguments[1] || null; // eslint-disable-next-line prefer-rest-params const before = arguments[2] || null; if (host) { host.insertBefore(createDOMNode(node), before); } else { // Create a text node for a virtual text node. if (node.type === 'text') { return document.createTextNode(node.content); } // Create the HTML element with the specified tag. host = document.createElement(node.tag); // Add the attributes for the new element. addAttrs(host, node.attrs); if (node.renderer) { node.renderer.render(host, { attrs: node.attrs, children: node.children }); return host; } // Recursively populate the element with child content. for (let i = 0, n = node.children.length; i < n; ++i) { createDOMNode(node.children[i], host); } } return host; } /** * 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. */ export function updateContent( host: HTMLElement, oldContent: ReadonlyArray<VirtualNode>, newContent: ReadonlyArray<VirtualNode> ): void { // Bail early if the content is identical. if (oldContent === newContent) { return; } // Collect the old keyed elems into a mapping. let oldKeyed = collectKeys(host, oldContent); // Create a copy of the old content which can be modified in-place. let oldCopy = oldContent.slice(); // Update the host with the new content. The diff always proceeds // forward and never modifies a previously visited index. The old // 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. let currElem = host.firstChild; let newCount = newContent.length; for (let i = 0; i < newCount; ++i) { // If the old content is exhausted, create a new node. if (i >= oldCopy.length) { createDOMNode(newContent[i], host); continue; } // Lookup the old and new virtual nodes. let oldVNode = oldCopy[i]; let newVNode = newContent[i]; // If both elements are identical, there is nothing to do. if (oldVNode === newVNode) { currElem = currElem!.nextSibling; continue; } // Handle the simplest case of in-place text update first. if (oldVNode.type === 'text' && newVNode.type === 'text') { // Avoid spurious updates for performance. if (currElem!.textContent !== newVNode.content) { currElem!.textContent = newVNode.content; } currElem = currElem!.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 (oldVNode.type === 'text' || newVNode.type === 'text') { ArrayExt.insert(oldCopy, i, newVNode); createDOMNode(newVNode, host, currElem); continue; } // If the old XOR new node has a custom renderer, // create and insert a new node. if (!oldVNode.renderer != !newVNode.renderer) { ArrayExt.insert(oldCopy, i, newVNode); createDOMNode(newVNode, host, currElem); continue; } // At this point, both nodes are known to be element nodes. // 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 old copy array. let newKey = newVNode.attrs.key; if (newKey && newKey in oldKeyed) { let pair = oldKeyed[newKey]; if (pair.vNode !== oldVNode) { ArrayExt.move(oldCopy, oldCopy.indexOf(pair.vNode, i + 1), i); host.insertBefore(pair.element, currElem); oldVNode = pair.vNode; currElem = pair.element; } } // If both elements are identical, there is nothing to do. if (oldVNode === newVNode) { currElem = currElem!.nextSibling; continue; } // If the old elem is keyed and does not match the new elem key, // create a new node. This is necessary since the old keyed elem // may be matched at a later point in the diff. let oldKey = oldVNode.attrs.key; if (oldKey && oldKey !== newKey) { ArrayExt.insert(oldCopy, i, newVNode); createDOMNode(newVNode, host, currElem); continue; } // If the tags are different, create a new node. if (oldVNode.tag !== newVNode.tag) { ArrayExt.insert(oldCopy, i, newVNode); createDOMNode(newVNode, host, currElem); continue; } // At this point, the element can be updated in-place. // Update the element attributes. updateAttrs(currElem as HTMLElement, oldVNode.attrs, newVNode.attrs); // Update the element content. if (newVNode.renderer) { newVNode.renderer.render(currElem as HTMLElement, { attrs: newVNode.attrs, children: newVNode.children }); } else { updateContent( currElem as HTMLElement, oldVNode.children, newVNode.children ); } // Step to the next sibling element. currElem = currElem!.nextSibling; } // Cleanup stale DOM removeContent(host, oldCopy, newCount, true); } /** * Handle cleanup of stale vdom and its associated DOM. The host node is * traversed recursively (in depth-first order), and any explicit cleanup * required by a child node is carried out when it is visited (eg if a node * has a custom renderer, the renderer.unrender function will be called). * Once the subtree beneath each child of host has been completely visited, * that child will be removed via a call to host.removeChild. */ function removeContent( host: HTMLElement, oldContent: ReadonlyArray<VirtualNode>, newCount: number, _sentinel: boolean ) { // Dispose of the old nodes pushed to the end of the host. for (let i = oldContent.length - 1; i >= newCount; --i) { const oldNode = oldContent[i]; const child = ( _sentinel ? host.lastChild : host.childNodes[i] ) as HTMLElement; // recursively clean up host children if (oldNode.type === 'text') { // pass } else if (oldNode.renderer && oldNode.renderer.unrender) { oldNode.renderer.unrender(child!, { attrs: oldNode.attrs, children: oldNode.children }); } else { removeContent(child!, oldNode.children, 0, false); } if (_sentinel) { host.removeChild(child!); } } } /** * A set of special-cased attribute names. */ const specialAttrs = { key: true, className: true, htmlFor: true, dataset: true, style: true }; /** * Add element attributes to a newly created HTML element. */ function addAttrs(element: HTMLElement, attrs: ElementAttrs): void { // Add the inline event listeners and node attributes. for (let name in attrs) { if (name in specialAttrs) { continue; } if (name.substr(0, 2) === 'on') { (element as any)[name] = (attrs as any)[name]; } else { element.setAttribute(name, (attrs as any)[name]); } } // Add the element `class` attribute. if (attrs.className !== undefined) { element.setAttribute('class', attrs.className); } // Add the element `for` attribute. if (attrs.htmlFor !== undefined) { element.setAttribute('for', attrs.htmlFor); } // Add the dataset values. if (attrs.dataset) { addDataset(element, attrs.dataset); } // Add the inline styles. if (attrs.style) { addStyle(element, attrs.style); } } /** * Update the element attributes of an HTML element. */ function updateAttrs( element: HTMLElement, oldAttrs: ElementAttrs, newAttrs: ElementAttrs ): void { // Do nothing if the attrs are the same object. if (oldAttrs === newAttrs) { return; } // Setup the strongly typed loop variable. let name: keyof ElementAttrs; // Remove attributes and listeners which no longer exist. for (name in oldAttrs) { if (name in specialAttrs || name in newAttrs) { continue; } if (name.substr(0, 2) === 'on') { (element as any)[name] = null; } else { element.removeAttribute(name); } } // Add and update new and existing attributes and listeners. for (name in newAttrs) { if (name in specialAttrs || oldAttrs[name] === newAttrs[name]) { continue; } if (name.substr(0, 2) === 'on') { (element as any)[name] = (newAttrs as any)[name]; } else { element.setAttribute(name, (newAttrs as any)[name]); } } // Update the element `class` attribute. if (oldAttrs.className !== newAttrs.className) { if (newAttrs.className !== undefined) { element.setAttribute('class', newAttrs.className); } else { element.removeAttribute('class'); } } // Add the element `for` attribute. if (oldAttrs.htmlFor !== newAttrs.htmlFor) { if (newAttrs.htmlFor !== undefined) { element.setAttribute('for', newAttrs.htmlFor); } else { element.removeAttribute('for'); } } // Update the dataset values. if (oldAttrs.dataset !== newAttrs.dataset) { updateDataset(element, oldAttrs.dataset || {}, newAttrs.dataset || {}); } // Update the inline styles. if (oldAttrs.style !== newAttrs.style) { updateStyle(element, oldAttrs.style || {}, newAttrs.style || {}); } } /** * Add dataset values to a newly created HTML element. */ function addDataset(element: HTMLElement, dataset: ElementDataset): void { for (let name in dataset) { element.setAttribute(`data-${name}`, dataset[name]); } } /** * Update the dataset values of an HTML element. */ function updateDataset( element: HTMLElement, oldDataset: ElementDataset, newDataset: ElementDataset ): void { for (let name in oldDataset) { if (!(name in newDataset)) { element.removeAttribute(`data-${name}`); } } for (let name in newDataset) { if (oldDataset[name] !== newDataset[name]) { element.setAttribute(`data-${name}`, newDataset[name]); } } } /** * Add inline style values to a newly created HTML element. */ function addStyle(element: HTMLElement, style: ElementInlineStyle): void { let elemStyle = element.style; let name: keyof ElementInlineStyle; for (name in style) { (elemStyle as any)[name] = style[name]; } } /** * Update the inline style values of an HTML element. */ function updateStyle( element: HTMLElement, oldStyle: ElementInlineStyle, newStyle: ElementInlineStyle ): void { let elemStyle = element.style; let name: keyof ElementInlineStyle; for (name in oldStyle) { if (!(name in newStyle)) { (elemStyle as any)[name] = ''; } } for (name in newStyle) { if (oldStyle[name] !== newStyle[name]) { (elemStyle as any)[name] = newStyle[name]; } } } /** * A mapping of string key to pair of element and rendered node. */ type KeyMap = { [key: string]: { vNode: VirtualElement; element: HTMLElement }; }; /** * Collect a mapping of keyed elements for the host content. */ function collectKeys( host: HTMLElement, content: ReadonlyArray<VirtualNode> ): KeyMap { let node = host.firstChild; let keyMap: KeyMap = Object.create(null); for (let vNode of content) { if (vNode.type === 'element' && vNode.attrs.key) { keyMap[vNode.attrs.key] = { vNode, element: node as HTMLElement }; } node = node!.nextSibling; } return keyMap; } }