diff options
author | Sebastian <sebasjm@gmail.com> | 2021-08-23 16:46:06 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2021-08-23 16:48:30 -0300 |
commit | 38acabfa6089ab8ac469c12b5f55022fb96935e5 (patch) | |
tree | 453dbf70000cc5e338b06201af1eaca8343f8f73 /preact/src | |
parent | f26125e039143b92dc0d84e7775f508ab0cdcaa8 (diff) | |
download | node-vendor-master.tar.gz node-vendor-master.tar.bz2 node-vendor-master.zip |
Diffstat (limited to 'preact/src')
-rw-r--r-- | preact/src/cjs.js | 3 | ||||
-rw-r--r-- | preact/src/clone-element.js | 34 | ||||
-rw-r--r-- | preact/src/component.js | 225 | ||||
-rw-r--r-- | preact/src/constants.js | 3 | ||||
-rw-r--r-- | preact/src/create-context.js | 68 | ||||
-rw-r--r-- | preact/src/create-element.js | 97 | ||||
-rw-r--r-- | preact/src/diff/catch-error.js | 38 | ||||
-rw-r--r-- | preact/src/diff/children.js | 347 | ||||
-rw-r--r-- | preact/src/diff/index.js | 514 | ||||
-rw-r--r-- | preact/src/diff/props.js | 158 | ||||
-rw-r--r-- | preact/src/index.d.ts | 310 | ||||
-rw-r--r-- | preact/src/index.js | 13 | ||||
-rw-r--r-- | preact/src/internal.d.ts | 146 | ||||
-rw-r--r-- | preact/src/jsx.d.ts | 974 | ||||
-rw-r--r-- | preact/src/options.js | 16 | ||||
-rw-r--r-- | preact/src/render.js | 75 | ||||
-rw-r--r-- | preact/src/util.js | 27 |
17 files changed, 3048 insertions, 0 deletions
diff --git a/preact/src/cjs.js b/preact/src/cjs.js new file mode 100644 index 0000000..b4721b1 --- /dev/null +++ b/preact/src/cjs.js @@ -0,0 +1,3 @@ +import * as preact from './index.js'; +if (typeof module < 'u') module.exports = preact; +else self.preact = preact; diff --git a/preact/src/clone-element.js b/preact/src/clone-element.js new file mode 100644 index 0000000..4cff226 --- /dev/null +++ b/preact/src/clone-element.js @@ -0,0 +1,34 @@ +import { assign, slice } from './util'; +import { createVNode } from './create-element'; + +/** + * Clones the given VNode, optionally adding attributes/props and replacing its children. + * @param {import('./internal').VNode} vnode The virtual DOM element to clone + * @param {object} props Attributes/props to add when cloning + * @param {Array<import('./internal').ComponentChildren>} rest Any additional arguments will be used as replacement children. + * @returns {import('./internal').VNode} + */ +export function cloneElement(vnode, props, children) { + let normalizedProps = assign({}, vnode.props), + key, + ref, + i; + for (i in props) { + if (i == 'key') key = props[i]; + else if (i == 'ref') ref = props[i]; + else normalizedProps[i] = props[i]; + } + + if (arguments.length > 2) { + normalizedProps.children = + arguments.length > 3 ? slice.call(arguments, 2) : children; + } + + return createVNode( + vnode.type, + normalizedProps, + key || vnode.key, + ref || vnode.ref, + null + ); +} diff --git a/preact/src/component.js b/preact/src/component.js new file mode 100644 index 0000000..c2043af --- /dev/null +++ b/preact/src/component.js @@ -0,0 +1,225 @@ +import { assign } from './util'; +import { diff, commitRoot } from './diff/index'; +import options from './options'; +import { Fragment } from './create-element'; + +/** + * Base Component class. Provides `setState()` and `forceUpdate()`, which + * trigger rendering + * @param {object} props The initial component props + * @param {object} context The initial context from parent components' + * getChildContext + */ +export function Component(props, context) { + this.props = props; + this.context = context; +} + +/** + * Update component state and schedule a re-render. + * @this {import('./internal').Component} + * @param {object | ((s: object, p: object) => object)} update A hash of state + * properties to update with new values or a function that given the current + * state and props returns a new partial state + * @param {() => void} [callback] A function to be called once component state is + * updated + */ +Component.prototype.setState = function(update, callback) { + // only clone state when copying to nextState the first time. + let s; + if (this._nextState != null && this._nextState !== this.state) { + s = this._nextState; + } else { + s = this._nextState = assign({}, this.state); + } + + if (typeof update == 'function') { + // Some libraries like `immer` mark the current state as readonly, + // preventing us from mutating it, so we need to clone it. See #2716 + update = update(assign({}, s), this.props); + } + + if (update) { + assign(s, update); + } + + // Skip update if updater function returned null + if (update == null) return; + + if (this._vnode) { + if (callback) this._renderCallbacks.push(callback); + enqueueRender(this); + } +}; + +/** + * Immediately perform a synchronous re-render of the component + * @this {import('./internal').Component} + * @param {() => void} [callback] A function to be called after component is + * re-rendered + */ +Component.prototype.forceUpdate = function(callback) { + if (this._vnode) { + // Set render mode so that we can differentiate where the render request + // is coming from. We need this because forceUpdate should never call + // shouldComponentUpdate + this._force = true; + if (callback) this._renderCallbacks.push(callback); + enqueueRender(this); + } +}; + +/** + * Accepts `props` and `state`, and returns a new Virtual DOM tree to build. + * Virtual DOM is generally constructed via [JSX](http://jasonformat.com/wtf-is-jsx). + * @param {object} props Props (eg: JSX attributes) received from parent + * element/component + * @param {object} state The component's current state + * @param {object} context Context object, as returned by the nearest + * ancestor's `getChildContext()` + * @returns {import('./index').ComponentChildren | void} + */ +Component.prototype.render = Fragment; + +/** + * @param {import('./internal').VNode} vnode + * @param {number | null} [childIndex] + */ +export function getDomSibling(vnode, childIndex) { + if (childIndex == null) { + // Use childIndex==null as a signal to resume the search from the vnode's sibling + return vnode._parent + ? getDomSibling(vnode._parent, vnode._parent._children.indexOf(vnode) + 1) + : null; + } + + let sibling; + for (; childIndex < vnode._children.length; childIndex++) { + sibling = vnode._children[childIndex]; + + if (sibling != null && sibling._dom != null) { + // Since updateParentDomPointers keeps _dom pointer correct, + // we can rely on _dom to tell us if this subtree contains a + // rendered DOM node, and what the first rendered DOM node is + return sibling._dom; + } + } + + // If we get here, we have not found a DOM node in this vnode's children. + // We must resume from this vnode's sibling (in it's parent _children array) + // Only climb up and search the parent if we aren't searching through a DOM + // VNode (meaning we reached the DOM parent of the original vnode that began + // the search) + return typeof vnode.type == 'function' ? getDomSibling(vnode) : null; +} + +/** + * Trigger in-place re-rendering of a component. + * @param {import('./internal').Component} component The component to rerender + */ +function renderComponent(component) { + let vnode = component._vnode, + oldDom = vnode._dom, + parentDom = component._parentDom; + + if (parentDom) { + let commitQueue = []; + const oldVNode = assign({}, vnode); + oldVNode._original = vnode._original + 1; + + diff( + parentDom, + vnode, + oldVNode, + component._globalContext, + parentDom.ownerSVGElement !== undefined, + vnode._hydrating != null ? [oldDom] : null, + commitQueue, + oldDom == null ? getDomSibling(vnode) : oldDom, + vnode._hydrating + ); + commitRoot(commitQueue, vnode); + + if (vnode._dom != oldDom) { + updateParentDomPointers(vnode); + } + } +} + +/** + * @param {import('./internal').VNode} vnode + */ +function updateParentDomPointers(vnode) { + if ((vnode = vnode._parent) != null && vnode._component != null) { + vnode._dom = vnode._component.base = null; + for (let i = 0; i < vnode._children.length; i++) { + let child = vnode._children[i]; + if (child != null && child._dom != null) { + vnode._dom = vnode._component.base = child._dom; + break; + } + } + + return updateParentDomPointers(vnode); + } +} + +/** + * The render queue + * @type {Array<import('./internal').Component>} + */ +let rerenderQueue = []; + +/** + * Asynchronously schedule a callback + * @type {(cb: () => void) => void} + */ +/* istanbul ignore next */ +// Note the following line isn't tree-shaken by rollup cuz of rollup/rollup#2566 +const defer = + typeof Promise == 'function' + ? Promise.prototype.then.bind(Promise.resolve()) + : setTimeout; + +/* + * The value of `Component.debounce` must asynchronously invoke the passed in callback. It is + * important that contributors to Preact can consistently reason about what calls to `setState`, etc. + * do, and when their effects will be applied. See the links below for some further reading on designing + * asynchronous APIs. + * * [Designing APIs for Asynchrony](https://blog.izs.me/2013/08/designing-apis-for-asynchrony) + * * [Callbacks synchronous and asynchronous](https://blog.ometer.com/2011/07/24/callbacks-synchronous-and-asynchronous/) + */ + +let prevDebounce; + +/** + * Enqueue a rerender of a component + * @param {import('./internal').Component} c The component to rerender + */ +export function enqueueRender(c) { + if ( + (!c._dirty && + (c._dirty = true) && + rerenderQueue.push(c) && + !process._rerenderCount++) || + prevDebounce !== options.debounceRendering + ) { + prevDebounce = options.debounceRendering; + (prevDebounce || defer)(process); + } +} + +/** Flush the render queue by rerendering all queued components */ +function process() { + let queue; + while ((process._rerenderCount = rerenderQueue.length)) { + queue = rerenderQueue.sort((a, b) => a._vnode._depth - b._vnode._depth); + rerenderQueue = []; + // Don't update `renderCount` yet. Keep its value non-zero to prevent unnecessary + // process() calls from getting scheduled while `queue` is still being consumed. + queue.some(c => { + if (c._dirty) renderComponent(c); + }); + } +} +process._rerenderCount = 0; diff --git a/preact/src/constants.js b/preact/src/constants.js new file mode 100644 index 0000000..dddc9a3 --- /dev/null +++ b/preact/src/constants.js @@ -0,0 +1,3 @@ +export const EMPTY_OBJ = {}; +export const EMPTY_ARR = []; +export const IS_NON_DIMENSIONAL = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i; diff --git a/preact/src/create-context.js b/preact/src/create-context.js new file mode 100644 index 0000000..750517d --- /dev/null +++ b/preact/src/create-context.js @@ -0,0 +1,68 @@ +import { enqueueRender } from './component'; + +export let i = 0; + +export function createContext(defaultValue, contextId) { + contextId = '__cC' + i++; + + const context = { + _id: contextId, + _defaultValue: defaultValue, + /** @type {import('./internal').FunctionComponent} */ + Consumer(props, contextValue) { + // return props.children( + // context[contextId] ? context[contextId].props.value : defaultValue + // ); + return props.children(contextValue); + }, + /** @type {import('./internal').FunctionComponent} */ + Provider(props) { + if (!this.getChildContext) { + let subs = []; + let ctx = {}; + ctx[contextId] = this; + + this.getChildContext = () => ctx; + + this.shouldComponentUpdate = function(_props) { + if (this.props.value !== _props.value) { + // I think the forced value propagation here was only needed when `options.debounceRendering` was being bypassed: + // https://github.com/preactjs/preact/commit/4d339fb803bea09e9f198abf38ca1bf8ea4b7771#diff-54682ce380935a717e41b8bfc54737f6R358 + // In those cases though, even with the value corrected, we're double-rendering all nodes. + // It might be better to just tell folks not to use force-sync mode. + // Currently, using `useContext()` in a class component will overwrite its `this.context` value. + // subs.some(c => { + // c.context = _props.value; + // enqueueRender(c); + // }); + + // subs.some(c => { + // c.context[contextId] = _props.value; + // enqueueRender(c); + // }); + subs.some(enqueueRender); + } + }; + + this.sub = c => { + subs.push(c); + let old = c.componentWillUnmount; + c.componentWillUnmount = () => { + subs.splice(subs.indexOf(c), 1); + if (old) old.call(c); + }; + }; + } + + return props.children; + } + }; + + // Devtools needs access to the context object when it + // encounters a Provider. This is necessary to support + // setting `displayName` on the context object instead + // of on the component itself. See: + // https://reactjs.org/docs/context.html#contextdisplayname + + return (context.Provider._contextRef = context.Consumer.contextType = context); +} diff --git a/preact/src/create-element.js b/preact/src/create-element.js new file mode 100644 index 0000000..90ee753 --- /dev/null +++ b/preact/src/create-element.js @@ -0,0 +1,97 @@ +import { slice } from './util'; +import options from './options'; + +let vnodeId = 0; + +/** + * Create an virtual node (used for JSX) + * @param {import('./internal').VNode["type"]} type The node name or Component + * constructor for this virtual node + * @param {object | null | undefined} [props] The properties of the virtual node + * @param {Array<import('.').ComponentChildren>} [children] The children of the virtual node + * @returns {import('./internal').VNode} + */ +export function createElement(type, props, children) { + let normalizedProps = {}, + key, + ref, + i; + for (i in props) { + if (i == 'key') key = props[i]; + else if (i == 'ref') ref = props[i]; + else normalizedProps[i] = props[i]; + } + + if (arguments.length > 2) { + normalizedProps.children = + arguments.length > 3 ? slice.call(arguments, 2) : children; + } + + // If a Component VNode, check for and apply defaultProps + // Note: type may be undefined in development, must never error here. + if (typeof type == 'function' && type.defaultProps != null) { + for (i in type.defaultProps) { + if (normalizedProps[i] === undefined) { + normalizedProps[i] = type.defaultProps[i]; + } + } + } + + return createVNode(type, normalizedProps, key, ref, null); +} + +/** + * Create a VNode (used internally by Preact) + * @param {import('./internal').VNode["type"]} type The node name or Component + * Constructor for this virtual node + * @param {object | string | number | null} props The properties of this virtual node. + * If this virtual node represents a text node, this is the text of the node (string or number). + * @param {string | number | null} key The key for this virtual node, used when + * diffing it against its children + * @param {import('./internal').VNode["ref"]} ref The ref property that will + * receive a reference to its created child + * @returns {import('./internal').VNode} + */ +export function createVNode(type, props, key, ref, original) { + // V8 seems to be better at detecting type shapes if the object is allocated from the same call site + // Do not inline into createElement and coerceToVNode! + const vnode = { + type, + props, + key, + ref, + _children: null, + _parent: null, + _depth: 0, + _dom: null, + // _nextDom must be initialized to undefined b/c it will eventually + // be set to dom.nextSibling which can return `null` and it is important + // to be able to distinguish between an uninitialized _nextDom and + // a _nextDom that has been set to `null` + _nextDom: undefined, + _component: null, + _hydrating: null, + constructor: undefined, + _original: original == null ? ++vnodeId : original + }; + + if (options.vnode != null) options.vnode(vnode); + + return vnode; +} + +export function createRef() { + return { current: null }; +} + +export function Fragment(props) { + return props.children; +} + +/** + * Check if a the argument is a valid Preact VNode. + * @param {*} vnode + * @returns {vnode is import('./internal').VNode} + */ +export const isValidElement = vnode => + vnode != null && vnode.constructor === undefined; diff --git a/preact/src/diff/catch-error.js b/preact/src/diff/catch-error.js new file mode 100644 index 0000000..893a076 --- /dev/null +++ b/preact/src/diff/catch-error.js @@ -0,0 +1,38 @@ +/** + * Find the closest error boundary to a thrown error and call it + * @param {object} error The thrown value + * @param {import('../internal').VNode} vnode The vnode that threw + * the error that was caught (except for unmounting when this parameter + * is the highest parent that was being unmounted) + */ +export function _catchError(error, vnode) { + /** @type {import('../internal').Component} */ + let component, ctor, handled; + + for (; (vnode = vnode._parent); ) { + if ((component = vnode._component) && !component._processingException) { + try { + ctor = component.constructor; + + if (ctor && ctor.getDerivedStateFromError != null) { + component.setState(ctor.getDerivedStateFromError(error)); + handled = component._dirty; + } + + if (component.componentDidCatch != null) { + component.componentDidCatch(error); + handled = component._dirty; + } + + // This is an error boundary. Mark it as having bailed out, and whether it was mid-hydration. + if (handled) { + return (component._pendingError = component); + } + } catch (e) { + error = e; + } + } + } + + throw error; +} diff --git a/preact/src/diff/children.js b/preact/src/diff/children.js new file mode 100644 index 0000000..204e3e4 --- /dev/null +++ b/preact/src/diff/children.js @@ -0,0 +1,347 @@ +import { diff, unmount, applyRef } from './index'; +import { createVNode, Fragment } from '../create-element'; +import { EMPTY_OBJ, EMPTY_ARR } from '../constants'; +import { getDomSibling } from '../component'; + +/** + * Diff the children of a virtual node + * @param {import('../internal').PreactElement} parentDom The DOM element whose + * children are being diffed + * @param {import('../internal').ComponentChildren[]} renderResult + * @param {import('../internal').VNode} newParentVNode The new virtual + * node whose children should be diff'ed against oldParentVNode + * @param {import('../internal').VNode} oldParentVNode The old virtual + * node whose children should be diff'ed against newParentVNode + * @param {object} globalContext The current context object - modified by getChildContext + * @param {boolean} isSvg Whether or not this DOM node is an SVG node + * @param {Array<import('../internal').PreactElement>} excessDomChildren + * @param {Array<import('../internal').Component>} commitQueue List of components + * which have callbacks to invoke in commitRoot + * @param {import('../internal').PreactElement} oldDom The current attached DOM + * element any new dom elements should be placed around. Likely `null` on first + * render (except when hydrating). Can be a sibling DOM element when diffing + * Fragments that have siblings. In most cases, it starts out as `oldChildren[0]._dom`. + * @param {boolean} isHydrating Whether or not we are in hydration + */ +export function diffChildren( + parentDom, + renderResult, + newParentVNode, + oldParentVNode, + globalContext, + isSvg, + excessDomChildren, + commitQueue, + oldDom, + isHydrating +) { + let i, j, oldVNode, childVNode, newDom, firstChildDom, refs; + + // This is a compression of oldParentVNode!=null && oldParentVNode != EMPTY_OBJ && oldParentVNode._children || EMPTY_ARR + // as EMPTY_OBJ._children should be `undefined`. + let oldChildren = (oldParentVNode && oldParentVNode._children) || EMPTY_ARR; + + let oldChildrenLength = oldChildren.length; + + newParentVNode._children = []; + for (i = 0; i < renderResult.length; i++) { + childVNode = renderResult[i]; + + if (childVNode == null || typeof childVNode == 'boolean') { + childVNode = newParentVNode._children[i] = null; + } + // If this newVNode is being reused (e.g. <div>{reuse}{reuse}</div>) in the same diff, + // or we are rendering a component (e.g. setState) copy the oldVNodes so it can have + // it's own DOM & etc. pointers + else if ( + typeof childVNode == 'string' || + typeof childVNode == 'number' || + // eslint-disable-next-line valid-typeof + typeof childVNode == 'bigint' + ) { + childVNode = newParentVNode._children[i] = createVNode( + null, + childVNode, + null, + null, + childVNode + ); + } else if (Array.isArray(childVNode)) { + childVNode = newParentVNode._children[i] = createVNode( + Fragment, + { children: childVNode }, + null, + null, + null + ); + } else if (childVNode._depth > 0) { + // VNode is already in use, clone it. This can happen in the following + // scenario: + // const reuse = <div /> + // <div>{reuse}<span />{reuse}</div> + childVNode = newParentVNode._children[i] = createVNode( + childVNode.type, + childVNode.props, + childVNode.key, + null, + childVNode._original + ); + } else { + childVNode = newParentVNode._children[i] = childVNode; + } + + // Terser removes the `continue` here and wraps the loop body + // in a `if (childVNode) { ... } condition + if (childVNode == null) { + continue; + } + + childVNode._parent = newParentVNode; + childVNode._depth = newParentVNode._depth + 1; + + // Check if we find a corresponding element in oldChildren. + // If found, delete the array item by setting to `undefined`. + // We use `undefined`, as `null` is reserved for empty placeholders + // (holes). + oldVNode = oldChildren[i]; + + if ( + oldVNode === null || + (oldVNode && + childVNode.key == oldVNode.key && + childVNode.type === oldVNode.type) + ) { + oldChildren[i] = undefined; + } else { + // Either oldVNode === undefined or oldChildrenLength > 0, + // so after this loop oldVNode == null or oldVNode is a valid value. + for (j = 0; j < oldChildrenLength; j++) { + oldVNode = oldChildren[j]; + // If childVNode is unkeyed, we only match similarly unkeyed nodes, otherwise we match by key. + // We always match by type (in either case). + if ( + oldVNode && + childVNode.key == oldVNode.key && + childVNode.type === oldVNode.type + ) { + oldChildren[j] = undefined; + break; + } + oldVNode = null; + } + } + + oldVNode = oldVNode || EMPTY_OBJ; + + // Morph the old element into the new one, but don't append it to the dom yet + diff( + parentDom, + childVNode, + oldVNode, + globalContext, + isSvg, + excessDomChildren, + commitQueue, + oldDom, + isHydrating + ); + + newDom = childVNode._dom; + + if ((j = childVNode.ref) && oldVNode.ref != j) { + if (!refs) refs = []; + if (oldVNode.ref) refs.push(oldVNode.ref, null, childVNode); + refs.push(j, childVNode._component || newDom, childVNode); + } + + if (newDom != null) { + if (firstChildDom == null) { + firstChildDom = newDom; + } + + if ( + typeof childVNode.type == 'function' && + childVNode._children != null && // Can be null if childVNode suspended + childVNode._children === oldVNode._children + ) { + childVNode._nextDom = oldDom = reorderChildren( + childVNode, + oldDom, + parentDom + ); + } else { + oldDom = placeChild( + parentDom, + childVNode, + oldVNode, + oldChildren, + newDom, + oldDom + ); + } + + // Browsers will infer an option's `value` from `textContent` when + // no value is present. This essentially bypasses our code to set it + // later in `diff()`. It works fine in all browsers except for IE11 + // where it breaks setting `select.value`. There it will be always set + // to an empty string. Re-applying an options value will fix that, so + // there are probably some internal data structures that aren't + // updated properly. + // + // To fix it we make sure to reset the inferred value, so that our own + // value check in `diff()` won't be skipped. + if (!isHydrating && newParentVNode.type === 'option') { + // @ts-ignore We have validated that the type of parentDOM is 'option' + // in the above check + parentDom.value = ''; + } else if (typeof newParentVNode.type == 'function') { + // Because the newParentVNode is Fragment-like, we need to set it's + // _nextDom property to the nextSibling of its last child DOM node. + // + // `oldDom` contains the correct value here because if the last child + // is a Fragment-like, then oldDom has already been set to that child's _nextDom. + // If the last child is a DOM VNode, then oldDom will be set to that DOM + // node's nextSibling. + newParentVNode._nextDom = oldDom; + } + } else if ( + oldDom && + oldVNode._dom == oldDom && + oldDom.parentNode != parentDom + ) { + // The above condition is to handle null placeholders. See test in placeholder.test.js: + // `efficiently replace null placeholders in parent rerenders` + oldDom = getDomSibling(oldVNode); + } + } + + newParentVNode._dom = firstChildDom; + + // Remove remaining oldChildren if there are any. + for (i = oldChildrenLength; i--; ) { + if (oldChildren[i] != null) { + if ( + typeof newParentVNode.type == 'function' && + oldChildren[i]._dom != null && + oldChildren[i]._dom == newParentVNode._nextDom + ) { + // If the newParentVNode.__nextDom points to a dom node that is about to + // be unmounted, then get the next sibling of that vnode and set + // _nextDom to it + newParentVNode._nextDom = getDomSibling(oldParentVNode, i + 1); + } + + unmount(oldChildren[i], oldChildren[i]); + } + } + + // Set refs only after unmount + if (refs) { + for (i = 0; i < refs.length; i++) { + applyRef(refs[i], refs[++i], refs[++i]); + } + } +} + +function reorderChildren(childVNode, oldDom, parentDom) { + for (let tmp = 0; tmp < childVNode._children.length; tmp++) { + let vnode = childVNode._children[tmp]; + if (vnode) { + // We typically enter this code path on sCU bailout, where we copy + // oldVNode._children to newVNode._children. If that is the case, we need + // to update the old children's _parent pointer to point to the newVNode + // (childVNode here). + vnode._parent = childVNode; + + if (typeof vnode.type == 'function') { + oldDom = reorderChildren(vnode, oldDom, parentDom); + } else { + oldDom = placeChild( + parentDom, + vnode, + vnode, + childVNode._children, + vnode._dom, + oldDom + ); + } + } + } + + return oldDom; +} + +/** + * Flatten and loop through the children of a virtual node + * @param {import('../index').ComponentChildren} children The unflattened + * children of a virtual node + * @returns {import('../internal').VNode[]} + */ +export function toChildArray(children, out) { + out = out || []; + if (children == null || typeof children == 'boolean') { + } else if (Array.isArray(children)) { + children.some(child => { + toChildArray(child, out); + }); + } else { + out.push(children); + } + return out; +} + +function placeChild( + parentDom, + childVNode, + oldVNode, + oldChildren, + newDom, + oldDom +) { + let nextDom; + if (childVNode._nextDom !== undefined) { + // Only Fragments or components that return Fragment like VNodes will + // have a non-undefined _nextDom. Continue the diff from the sibling + // of last DOM child of this child VNode + nextDom = childVNode._nextDom; + + // Eagerly cleanup _nextDom. We don't need to persist the value because + // it is only used by `diffChildren` to determine where to resume the diff after + // diffing Components and Fragments. Once we store it the nextDOM local var, we + // can clean up the property + childVNode._nextDom = undefined; + } else if ( + oldVNode == null || + newDom != oldDom || + newDom.parentNode == null + ) { + outer: if (oldDom == null || oldDom.parentNode !== parentDom) { + parentDom.appendChild(newDom); + nextDom = null; + } else { + // `j<oldChildrenLength; j+=2` is an alternative to `j++<oldChildrenLength/2` + for ( + let sibDom = oldDom, j = 0; + (sibDom = sibDom.nextSibling) && j < oldChildren.length; + j += 2 + ) { + if (sibDom == newDom) { + break outer; + } + } + parentDom.insertBefore(newDom, oldDom); + nextDom = oldDom; + } + } + + // If we have pre-calculated the nextDOM node, use it. Else calculate it now + // Strictly check for `undefined` here cuz `null` is a valid value of `nextDom`. + // See more detail in create-element.js:createVNode + if (nextDom !== undefined) { + oldDom = nextDom; + } else { + oldDom = newDom.nextSibling; + } + + return oldDom; +} diff --git a/preact/src/diff/index.js b/preact/src/diff/index.js new file mode 100644 index 0000000..de63843 --- /dev/null +++ b/preact/src/diff/index.js @@ -0,0 +1,514 @@ +import { EMPTY_OBJ } from '../constants'; +import { Component, getDomSibling } from '../component'; +import { Fragment } from '../create-element'; +import { diffChildren } from './children'; +import { diffProps, setProperty } from './props'; +import { assign, removeNode, slice } from '../util'; +import options from '../options'; + +/** + * Diff two virtual nodes and apply proper changes to the DOM + * @param {import('../internal').PreactElement} parentDom The parent of the DOM element + * @param {import('../internal').VNode} newVNode The new virtual node + * @param {import('../internal').VNode} oldVNode The old virtual node + * @param {object} globalContext The current context object. Modified by getChildContext + * @param {boolean} isSvg Whether or not this element is an SVG node + * @param {Array<import('../internal').PreactElement>} excessDomChildren + * @param {Array<import('../internal').Component>} commitQueue List of components + * which have callbacks to invoke in commitRoot + * @param {import('../internal').PreactElement} oldDom The current attached DOM + * element any new dom elements should be placed around. Likely `null` on first + * render (except when hydrating). Can be a sibling DOM element when diffing + * Fragments that have siblings. In most cases, it starts out as `oldChildren[0]._dom`. + * @param {boolean} [isHydrating] Whether or not we are in hydration + */ +export function diff( + parentDom, + newVNode, + oldVNode, + globalContext, + isSvg, + excessDomChildren, + commitQueue, + oldDom, + isHydrating +) { + let tmp, + newType = newVNode.type; + + // When passing through createElement it assigns the object + // constructor as undefined. This to prevent JSON-injection. + if (newVNode.constructor !== undefined) return null; + + // If the previous diff bailed out, resume creating/hydrating. + if (oldVNode._hydrating != null) { + isHydrating = oldVNode._hydrating; + oldDom = newVNode._dom = oldVNode._dom; + // if we resume, we want the tree to be "unlocked" + newVNode._hydrating = null; + excessDomChildren = [oldDom]; + } + + if ((tmp = options._diff)) tmp(newVNode); + + try { + outer: if (typeof newType == 'function') { + let c, isNew, oldProps, oldState, snapshot, clearProcessingException; + let newProps = newVNode.props; + + // Necessary for createContext api. Setting this property will pass + // the context value as `this.context` just for this component. + tmp = newType.contextType; + let provider = tmp && globalContext[tmp._id]; + let componentContext = tmp + ? provider + ? provider.props.value + : tmp._defaultValue + : globalContext; + + // Get component and set it to `c` + if (oldVNode._component) { + c = newVNode._component = oldVNode._component; + clearProcessingException = c._processingException = c._pendingError; + } else { + // Instantiate the new component + if ('prototype' in newType && newType.prototype.render) { + // @ts-ignore The check above verifies that newType is suppose to be constructed + newVNode._component = c = new newType(newProps, componentContext); // eslint-disable-line new-cap + } else { + // @ts-ignore Trust me, Component implements the interface we want + newVNode._component = c = new Component(newProps, componentContext); + c.constructor = newType; + c.render = doRender; + } + if (provider) provider.sub(c); + + c.props = newProps; + if (!c.state) c.state = {}; + c.context = componentContext; + c._globalContext = globalContext; + isNew = c._dirty = true; + c._renderCallbacks = []; + } + + // Invoke getDerivedStateFromProps + if (c._nextState == null) { + c._nextState = c.state; + } + if (newType.getDerivedStateFromProps != null) { + if (c._nextState == c.state) { + c._nextState = assign({}, c._nextState); + } + + assign( + c._nextState, + newType.getDerivedStateFromProps(newProps, c._nextState) + ); + } + + oldProps = c.props; + oldState = c.state; + + // Invoke pre-render lifecycle methods + if (isNew) { + if ( + newType.getDerivedStateFromProps == null && + c.componentWillMount != null + ) { + c.componentWillMount(); + } + + if (c.componentDidMount != null) { + c._renderCallbacks.push(c.componentDidMount); + } + } else { + if ( + newType.getDerivedStateFromProps == null && + newProps !== oldProps && + c.componentWillReceiveProps != null + ) { + c.componentWillReceiveProps(newProps, componentContext); + } + + if ( + (!c._force && + c.shouldComponentUpdate != null && + c.shouldComponentUpdate( + newProps, + c._nextState, + componentContext + ) === false) || + newVNode._original === oldVNode._original + ) { + c.props = newProps; + c.state = c._nextState; + // More info about this here: https://gist.github.com/JoviDeCroock/bec5f2ce93544d2e6070ef8e0036e4e8 + if (newVNode._original !== oldVNode._original) c._dirty = false; + c._vnode = newVNode; + newVNode._dom = oldVNode._dom; + newVNode._children = oldVNode._children; + newVNode._children.forEach(vnode => { + if (vnode) vnode._parent = newVNode; + }); + if (c._renderCallbacks.length) { + commitQueue.push(c); + } + + break outer; + } + + if (c.componentWillUpdate != null) { + c.componentWillUpdate(newProps, c._nextState, componentContext); + } + + if (c.componentDidUpdate != null) { + c._renderCallbacks.push(() => { + c.componentDidUpdate(oldProps, oldState, snapshot); + }); + } + } + + c.context = componentContext; + c.props = newProps; + c.state = c._nextState; + + if ((tmp = options._render)) tmp(newVNode); + + c._dirty = false; + c._vnode = newVNode; + c._parentDom = parentDom; + + tmp = c.render(c.props, c.state, c.context); + + // Handle setState called in render, see #2553 + c.state = c._nextState; + + if (c.getChildContext != null) { + globalContext = assign(assign({}, globalContext), c.getChildContext()); + } + + if (!isNew && c.getSnapshotBeforeUpdate != null) { + snapshot = c.getSnapshotBeforeUpdate(oldProps, oldState); + } + + let isTopLevelFragment = + tmp != null && tmp.type === Fragment && tmp.key == null; + let renderResult = isTopLevelFragment ? tmp.props.children : tmp; + + diffChildren( + parentDom, + Array.isArray(renderResult) ? renderResult : [renderResult], + newVNode, + oldVNode, + globalContext, + isSvg, + excessDomChildren, + commitQueue, + oldDom, + isHydrating + ); + + c.base = newVNode._dom; + + // We successfully rendered this VNode, unset any stored hydration/bailout state: + newVNode._hydrating = null; + + if (c._renderCallbacks.length) { + commitQueue.push(c); + } + + if (clearProcessingException) { + c._pendingError = c._processingException = null; + } + + c._force = false; + } else if ( + excessDomChildren == null && + newVNode._original === oldVNode._original + ) { + newVNode._children = oldVNode._children; + newVNode._dom = oldVNode._dom; + } else { + newVNode._dom = diffElementNodes( + oldVNode._dom, + newVNode, + oldVNode, + globalContext, + isSvg, + excessDomChildren, + commitQueue, + isHydrating + ); + } + + if ((tmp = options.diffed)) tmp(newVNode); + } catch (e) { + newVNode._original = null; + // if hydrating or creating initial tree, bailout preserves DOM: + if (isHydrating || excessDomChildren != null) { + newVNode._dom = oldDom; + newVNode._hydrating = !!isHydrating; + excessDomChildren[excessDomChildren.indexOf(oldDom)] = null; + // ^ could possibly be simplified to: + // excessDomChildren.length = 0; + } + options._catchError(e, newVNode, oldVNode); + } +} + +/** + * @param {Array<import('../internal').Component>} commitQueue List of components + * which have callbacks to invoke in commitRoot + * @param {import('../internal').VNode} root + */ +export function commitRoot(commitQueue, root) { + if (options._commit) options._commit(root, commitQueue); + + commitQueue.some(c => { + try { + // @ts-ignore Reuse the commitQueue variable here so the type changes + commitQueue = c._renderCallbacks; + c._renderCallbacks = []; + commitQueue.some(cb => { + // @ts-ignore See above ts-ignore on commitQueue + cb.call(c); + }); + } catch (e) { + options._catchError(e, c._vnode); + } + }); +} + +/** + * Diff two virtual nodes representing DOM element + * @param {import('../internal').PreactElement} dom The DOM element representing + * the virtual nodes being diffed + * @param {import('../internal').VNode} newVNode The new virtual node + * @param {import('../internal').VNode} oldVNode The old virtual node + * @param {object} globalContext The current context object + * @param {boolean} isSvg Whether or not this DOM node is an SVG node + * @param {*} excessDomChildren + * @param {Array<import('../internal').Component>} commitQueue List of components + * which have callbacks to invoke in commitRoot + * @param {boolean} isHydrating Whether or not we are in hydration + * @returns {import('../internal').PreactElement} + */ +function diffElementNodes( + dom, + newVNode, + oldVNode, + globalContext, + isSvg, + excessDomChildren, + commitQueue, + isHydrating +) { + let oldProps = oldVNode.props; + let newProps = newVNode.props; + let nodeType = newVNode.type; + let i = 0; + + // Tracks entering and exiting SVG namespace when descending through the tree. + if (nodeType === 'svg') isSvg = true; + + if (excessDomChildren != null) { + for (; i < excessDomChildren.length; i++) { + const child = excessDomChildren[i]; + + // if newVNode matches an element in excessDomChildren or the `dom` + // argument matches an element in excessDomChildren, remove it from + // excessDomChildren so it isn't later removed in diffChildren + if ( + child && + (child === dom || + (nodeType ? child.localName == nodeType : child.nodeType == 3)) + ) { + dom = child; + excessDomChildren[i] = null; + break; + } + } + } + + if (dom == null) { + if (nodeType === null) { + // @ts-ignore createTextNode returns Text, we expect PreactElement + return document.createTextNode(newProps); + } + + if (isSvg) { + dom = document.createElementNS( + 'http://www.w3.org/2000/svg', + // @ts-ignore We know `newVNode.type` is a string + nodeType + ); + } else { + dom = document.createElement( + // @ts-ignore We know `newVNode.type` is a string + nodeType, + newProps.is && newProps + ); + } + + // we created a new parent, so none of the previously attached children can be reused: + excessDomChildren = null; + // we are creating a new node, so we can assume this is a new subtree (in case we are hydrating), this deopts the hydrate + isHydrating = false; + } + + if (nodeType === null) { + // During hydration, we still have to split merged text from SSR'd HTML. + if (oldProps !== newProps && (!isHydrating || dom.data !== newProps)) { + dom.data = newProps; + } + } else { + // If excessDomChildren was not null, repopulate it with the current element's children: + excessDomChildren = excessDomChildren && slice.call(dom.childNodes); + + oldProps = oldVNode.props || EMPTY_OBJ; + + let oldHtml = oldProps.dangerouslySetInnerHTML; + let newHtml = newProps.dangerouslySetInnerHTML; + + // During hydration, props are not diffed at all (including dangerouslySetInnerHTML) + // @TODO we should warn in debug mode when props don't match here. + if (!isHydrating) { + // But, if we are in a situation where we are using existing DOM (e.g. replaceNode) + // we should read the existing DOM attributes to diff them + if (excessDomChildren != null) { + oldProps = {}; + for (i = 0; i < dom.attributes.length; i++) { + oldProps[dom.attributes[i].name] = dom.attributes[i].value; + } + } + + if (newHtml || oldHtml) { + // Avoid re-applying the same '__html' if it did not changed between re-render + if ( + !newHtml || + ((!oldHtml || newHtml.__html != oldHtml.__html) && + newHtml.__html !== dom.innerHTML) + ) { + dom.innerHTML = (newHtml && newHtml.__html) || ''; + } + } + } + + diffProps(dom, newProps, oldProps, isSvg, isHydrating); + + // If the new vnode didn't have dangerouslySetInnerHTML, diff its children + if (newHtml) { + newVNode._children = []; + } else { + i = newVNode.props.children; + diffChildren( + dom, + Array.isArray(i) ? i : [i], + newVNode, + oldVNode, + globalContext, + isSvg && nodeType !== 'foreignObject', + excessDomChildren, + commitQueue, + excessDomChildren + ? excessDomChildren[0] + : oldVNode._children && getDomSibling(oldVNode, 0), + isHydrating + ); + + // Remove children that are not part of any vnode. + if (excessDomChildren != null) { + for (i = excessDomChildren.length; i--; ) { + if (excessDomChildren[i] != null) removeNode(excessDomChildren[i]); + } + } + } + + // (as above, don't diff props during hydration) + if (!isHydrating) { + if ( + 'value' in newProps && + (i = newProps.value) !== undefined && + // #2756 For the <progress>-element the initial value is 0, + // despite the attribute not being present. When the attribute + // is missing the progress bar is treated as indeterminate. + // To fix that we'll always update it when it is 0 for progress elements + (i !== dom.value || (nodeType === 'progress' && !i)) + ) { + setProperty(dom, 'value', i, oldProps.value, false); + } + if ( + 'checked' in newProps && + (i = newProps.checked) !== undefined && + i !== dom.checked + ) { + setProperty(dom, 'checked', i, oldProps.checked, false); + } + } + } + + return dom; +} + +/** + * Invoke or update a ref, depending on whether it is a function or object ref. + * @param {object|function} ref + * @param {any} value + * @param {import('../internal').VNode} vnode + */ +export function applyRef(ref, value, vnode) { + try { + if (typeof ref == 'function') ref(value); + else ref.current = value; + } catch (e) { + options._catchError(e, vnode); + } +} + +/** + * Unmount a virtual node from the tree and apply DOM changes + * @param {import('../internal').VNode} vnode The virtual node to unmount + * @param {import('../internal').VNode} parentVNode The parent of the VNode that + * initiated the unmount + * @param {boolean} [skipRemove] Flag that indicates that a parent node of the + * current element is already detached from the DOM. + */ +export function unmount(vnode, parentVNode, skipRemove) { + let r; + if (options.unmount) options.unmount(vnode); + + if ((r = vnode.ref)) { + if (!r.current || r.current === vnode._dom) applyRef(r, null, parentVNode); + } + + if ((r = vnode._component) != null) { + if (r.componentWillUnmount) { + try { + r.componentWillUnmount(); + } catch (e) { + options._catchError(e, parentVNode); + } + } + + r.base = r._parentDom = null; + } + + if ((r = vnode._children)) { + for (let i = 0; i < r.length; i++) { + if (r[i]) { + unmount(r[i], parentVNode, typeof vnode.type != 'function'); + } + } + } + + if (!skipRemove && vnode._dom != null) removeNode(vnode._dom); + + // Must be set to `undefined` to properly clean up `_nextDom` + // for which `null` is a valid value. See comment in `create-element.js` + vnode._dom = vnode._nextDom = undefined; +} + +/** The `.render()` method for a PFC backing instance. */ +function doRender(props, state, context) { + return this.constructor(props, context); +} diff --git a/preact/src/diff/props.js b/preact/src/diff/props.js new file mode 100644 index 0000000..472d997 --- /dev/null +++ b/preact/src/diff/props.js @@ -0,0 +1,158 @@ +import { IS_NON_DIMENSIONAL } from '../constants'; +import options from '../options'; + +/** + * Diff the old and new properties of a VNode and apply changes to the DOM node + * @param {import('../internal').PreactElement} dom The DOM node to apply + * changes to + * @param {object} newProps The new props + * @param {object} oldProps The old props + * @param {boolean} isSvg Whether or not this node is an SVG node + * @param {boolean} hydrate Whether or not we are in hydration mode + */ +export function diffProps(dom, newProps, oldProps, isSvg, hydrate) { + let i; + + for (i in oldProps) { + if (i !== 'children' && i !== 'key' && !(i in newProps)) { + setProperty(dom, i, null, oldProps[i], isSvg); + } + } + + for (i in newProps) { + if ( + (!hydrate || typeof newProps[i] == 'function') && + i !== 'children' && + i !== 'key' && + i !== 'value' && + i !== 'checked' && + oldProps[i] !== newProps[i] + ) { + setProperty(dom, i, newProps[i], oldProps[i], isSvg); + } + } +} + +function setStyle(style, key, value) { + if (key[0] === '-') { + style.setProperty(key, value); + } else if (value == null) { + style[key] = ''; + } else if (typeof value != 'number' || IS_NON_DIMENSIONAL.test(key)) { + style[key] = value; + } else { + style[key] = value + 'px'; + } +} + +/** + * Set a property value on a DOM node + * @param {import('../internal').PreactElement} dom The DOM node to modify + * @param {string} name The name of the property to set + * @param {*} value The value to set the property to + * @param {*} oldValue The old value the property had + * @param {boolean} isSvg Whether or not this DOM node is an SVG node or not + */ +export function setProperty(dom, name, value, oldValue, isSvg) { + let useCapture; + + o: if (name === 'style') { + if (typeof value == 'string') { + dom.style.cssText = value; + } else { + if (typeof oldValue == 'string') { + dom.style.cssText = oldValue = ''; + } + + if (oldValue) { + for (name in oldValue) { + if (!(value && name in value)) { + setStyle(dom.style, name, ''); + } + } + } + + if (value) { + for (name in value) { + if (!oldValue || value[name] !== oldValue[name]) { + setStyle(dom.style, name, value[name]); + } + } + } + } + } + // Benchmark for comparison: https://esbench.com/bench/574c954bdb965b9a00965ac6 + else if (name[0] === 'o' && name[1] === 'n') { + useCapture = name !== (name = name.replace(/Capture$/, '')); + + // Infer correct casing for DOM built-in events: + if (name.toLowerCase() in dom) name = name.toLowerCase().slice(2); + else name = name.slice(2); + + if (!dom._listeners) dom._listeners = {}; + dom._listeners[name + useCapture] = value; + + if (value) { + if (!oldValue) { + const handler = useCapture ? eventProxyCapture : eventProxy; + dom.addEventListener(name, handler, useCapture); + } + } else { + const handler = useCapture ? eventProxyCapture : eventProxy; + dom.removeEventListener(name, handler, useCapture); + } + } else if (name !== 'dangerouslySetInnerHTML') { + if (isSvg) { + // Normalize incorrect prop usage for SVG: + // - xlink:href / xlinkHref --> href (xlink:href was removed from SVG and isn't needed) + // - className --> class + name = name.replace(/xlink[H:h]/, 'h').replace(/sName$/, 's'); + } else if ( + name !== 'href' && + name !== 'list' && + name !== 'form' && + // Default value in browsers is `-1` and an empty string is + // cast to `0` instead + name !== 'tabIndex' && + name !== 'download' && + name in dom + ) { + try { + dom[name] = value == null ? '' : value; + // labelled break is 1b smaller here than a return statement (sorry) + break o; + } catch (e) {} + } + + // ARIA-attributes have a different notion of boolean values. + // The value `false` is different from the attribute not + // existing on the DOM, so we can't remove it. For non-boolean + // ARIA-attributes we could treat false as a removal, but the + // amount of exceptions would cost us too many bytes. On top of + // that other VDOM frameworks also always stringify `false`. + + if (typeof value === 'function') { + // never serialize functions as attribute values + } else if ( + value != null && + (value !== false || (name[0] === 'a' && name[1] === 'r')) + ) { + dom.setAttribute(name, value); + } else { + dom.removeAttribute(name); + } + } +} + +/** + * Proxy an event to hooked event handlers + * @param {Event} e The event object from the browser + * @private + */ +function eventProxy(e) { + this._listeners[e.type + false](options.event ? options.event(e) : e); +} + +function eventProxyCapture(e) { + this._listeners[e.type + true](options.event ? options.event(e) : e); +} diff --git a/preact/src/index.d.ts b/preact/src/index.d.ts new file mode 100644 index 0000000..e55f978 --- /dev/null +++ b/preact/src/index.d.ts @@ -0,0 +1,310 @@ +export as namespace preact; + +import { JSXInternal } from './jsx'; + +export import JSX = JSXInternal; + +// +// Preact Virtual DOM +// ----------------------------------- + +export interface VNode<P = {}> { + type: ComponentType<P> | string; + props: P & { children: ComponentChildren }; + key: Key; + /** + * ref is not guaranteed by React.ReactElement, for compatibility reasons + * with popular react libs we define it as optional too + */ + ref?: Ref<any> | null; + /** + * The time this `vnode` started rendering. Will only be set when + * the devtools are attached. + * Default value: `0` + */ + startTime?: number; + /** + * The time that the rendering of this `vnode` was completed. Will only be + * set when the devtools are attached. + * Default value: `-1` + */ + endTime?: number; +} + +// +// Preact Component interface +// ----------------------------------- + +export type Key = string | number | any; + +export type RefObject<T> = { current: T | null }; +export type RefCallback<T> = (instance: T | null) => void; +export type Ref<T> = RefObject<T> | RefCallback<T>; + +export type ComponentChild = + | VNode<any> + | object + | string + | number + | bigint + | boolean + | null + | undefined; +export type ComponentChildren = ComponentChild[] | ComponentChild; + +export interface Attributes { + key?: Key; + jsx?: boolean; +} + +export interface ClassAttributes<T> extends Attributes { + ref?: Ref<T>; +} + +export interface PreactDOMAttributes { + children?: ComponentChildren; + dangerouslySetInnerHTML?: { + __html: string; + }; +} + +export type RenderableProps<P, RefType = any> = P & + Readonly<Attributes & { children?: ComponentChildren; ref?: Ref<RefType> }>; + +export type ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P>; +export type ComponentFactory<P = {}> = ComponentType<P>; + +export type ComponentProps< + C extends ComponentType<any> | keyof JSXInternal.IntrinsicElements +> = C extends ComponentType<infer P> + ? P + : C extends keyof JSXInternal.IntrinsicElements + ? JSXInternal.IntrinsicElements[C] + : never; + +export interface FunctionComponent<P = {}> { + (props: RenderableProps<P>, context?: any): VNode<any> | null; + displayName?: string; + defaultProps?: Partial<P>; +} +export interface FunctionalComponent<P = {}> extends FunctionComponent<P> {} + +export interface ComponentClass<P = {}, S = {}> { + new (props: P, context?: any): Component<P, S>; + displayName?: string; + defaultProps?: Partial<P>; + contextType?: Context<any>; + getDerivedStateFromProps?( + props: Readonly<P>, + state: Readonly<S> + ): Partial<S> | null; + getDerivedStateFromError?(error: any): Partial<S> | null; +} +export interface ComponentConstructor<P = {}, S = {}> + extends ComponentClass<P, S> {} + +// Type alias for a component instance considered generally, whether stateless or stateful. +export type AnyComponent<P = {}, S = {}> = + | FunctionComponent<P> + | Component<P, S>; + +export interface Component<P = {}, S = {}> { + componentWillMount?(): void; + componentDidMount?(): void; + componentWillUnmount?(): void; + getChildContext?(): object; + componentWillReceiveProps?(nextProps: Readonly<P>, nextContext: any): void; + shouldComponentUpdate?( + nextProps: Readonly<P>, + nextState: Readonly<S>, + nextContext: any + ): boolean; + componentWillUpdate?( + nextProps: Readonly<P>, + nextState: Readonly<S>, + nextContext: any + ): void; + getSnapshotBeforeUpdate?(oldProps: Readonly<P>, oldState: Readonly<S>): any; + componentDidUpdate?( + previousProps: Readonly<P>, + previousState: Readonly<S>, + snapshot: any + ): void; + componentDidCatch?(error: any, errorInfo: any): void; +} + +export abstract class Component<P, S> { + constructor(props?: P, context?: any); + + static displayName?: string; + static defaultProps?: any; + static contextType?: Context<any>; + + // Static members cannot reference class type parameters. This is not + // supported in TypeScript. Reusing the same type arguments from `Component` + // will lead to an impossible state where one cannot satisfy the type + // constraint under no circumstances, see #1356.In general type arguments + // seem to be a bit buggy and not supported well at the time of this + // writing with TS 3.3.3333. + static getDerivedStateFromProps?( + props: Readonly<object>, + state: Readonly<object> + ): object | null; + static getDerivedStateFromError?(error: any): object | null; + + state: Readonly<S>; + props: RenderableProps<P>; + context: any; + base?: Element | Text; + + // From https://github.com/DefinitelyTyped/DefinitelyTyped/blob/e836acc75a78cf0655b5dfdbe81d69fdd4d8a252/types/react/index.d.ts#L402 + // // We MUST keep setState() as a unified signature because it allows proper checking of the method return type. + // // See: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment-351013257 + setState<K extends keyof S>( + state: + | (( + prevState: Readonly<S>, + props: Readonly<P> + ) => Pick<S, K> | Partial<S> | null) + | (Pick<S, K> | Partial<S> | null), + callback?: () => void + ): void; + + forceUpdate(callback?: () => void): void; + + abstract render( + props?: RenderableProps<P>, + state?: Readonly<S>, + context?: any + ): ComponentChild; +} + +// +// Preact createElement +// ----------------------------------- + +export function createElement( + type: string, + props: + | (JSXInternal.HTMLAttributes & + JSXInternal.SVGAttributes & + Record<string, any>) + | null, + ...children: ComponentChildren[] +): VNode<any>; +export function createElement<P>( + type: ComponentType<P>, + props: (Attributes & P) | null, + ...children: ComponentChildren[] +): VNode<any>; +export namespace createElement { + export import JSX = JSXInternal; +} + +export function h( + type: string, + props: + | (JSXInternal.HTMLAttributes & + JSXInternal.SVGAttributes & + Record<string, any>) + | null, + ...children: ComponentChildren[] +): VNode<any>; +export function h<P>( + type: ComponentType<P>, + props: (Attributes & P) | null, + ...children: ComponentChildren[] +): VNode<any>; +export namespace h { + export import JSX = JSXInternal; +} + +// +// Preact render +// ----------------------------------- + +export function render( + vnode: ComponentChild, + parent: Element | Document | ShadowRoot | DocumentFragment, + replaceNode?: Element | Text +): void; +export function hydrate( + vnode: ComponentChild, + parent: Element | Document | ShadowRoot | DocumentFragment +): void; +export function cloneElement( + vnode: VNode<any>, + props?: any, + ...children: ComponentChildren[] +): VNode<any>; +export function cloneElement<P>( + vnode: VNode<P>, + props?: any, + ...children: ComponentChildren[] +): VNode<P>; + +// +// Preact Built-in Components +// ----------------------------------- + +// TODO: Revisit what the public type of this is... +export const Fragment: ComponentClass<{}, {}>; + +// +// Preact options +// ----------------------------------- + +/** + * Global options for preact + */ +export interface Options { + /** Attach a hook that is invoked whenever a VNode is created. */ + vnode?(vnode: VNode): void; + /** Attach a hook that is invoked immediately before a vnode is unmounted. */ + unmount?(vnode: VNode): void; + /** Attach a hook that is invoked after a vnode has rendered. */ + diffed?(vnode: VNode): void; + event?(e: Event): any; + requestAnimationFrame?: typeof requestAnimationFrame; + debounceRendering?(cb: () => void): void; + useDebugValue?(value: string | number): void; + _addHookName?(name: string | number): void; + __suspenseDidResolve?(vnode: VNode, cb: () => void): void; + // __canSuspenseResolve?(vnode: VNode, cb: () => void): void; +} + +export const options: Options; + +// +// Preact helpers +// ----------------------------------- +export function createRef<T = any>(): RefObject<T>; +export function toChildArray( + children: ComponentChildren +): Array<VNode | string | number>; +export function isValidElement(vnode: any): vnode is VNode; + +// +// Context +// ----------------------------------- +export interface Consumer<T> + extends FunctionComponent<{ + children: (value: T) => ComponentChildren; + }> {} +export interface PreactConsumer<T> extends Consumer<T> {} + +export interface Provider<T> + extends FunctionComponent<{ + value: T; + children: ComponentChildren; + }> {} +export interface PreactProvider<T> extends Provider<T> {} + +export interface Context<T> { + Consumer: Consumer<T>; + Provider: Provider<T>; + displayName?: string; +} +export interface PreactContext<T> extends Context<T> {} + +export function createContext<T>(defaultValue: T): Context<T>; diff --git a/preact/src/index.js b/preact/src/index.js new file mode 100644 index 0000000..ae76ccb --- /dev/null +++ b/preact/src/index.js @@ -0,0 +1,13 @@ +export { render, hydrate } from './render'; +export { + createElement, + createElement as h, + Fragment, + createRef, + isValidElement +} from './create-element'; +export { Component } from './component'; +export { cloneElement } from './clone-element'; +export { createContext } from './create-context'; +export { toChildArray } from './diff/children'; +export { default as options } from './options'; diff --git a/preact/src/internal.d.ts b/preact/src/internal.d.ts new file mode 100644 index 0000000..4592143 --- /dev/null +++ b/preact/src/internal.d.ts @@ -0,0 +1,146 @@ +import * as preact from './index'; + +export enum HookType { + useState = 1, + useReducer = 2, + useEffect = 3, + useLayoutEffect = 4, + useRef = 5, + useImperativeHandle = 6, + useMemo = 7, + useCallback = 8, + useContext = 9, + useErrorBoundary = 10, + // Not a real hook, but the devtools treat is as such + useDebugvalue = 11 +} + +export interface DevSource { + fileName: string; + lineNumber: number; +} + +export interface Options extends preact.Options { + /** Attach a hook that is invoked before render, mainly to check the arguments. */ + _root?( + vnode: ComponentChild, + parent: Element | Document | ShadowRoot | DocumentFragment + ): void; + /** Attach a hook that is invoked before a vnode is diffed. */ + _diff?(vnode: VNode): void; + /** Attach a hook that is invoked after a tree was mounted or was updated. */ + _commit?(vnode: VNode, commitQueue: Component[]): void; + /** Attach a hook that is invoked before a vnode has rendered. */ + _render?(vnode: VNode): void; + /** Attach a hook that is invoked before a hook's state is queried. */ + _hook?(component: Component, index: number, type: HookType): void; + /** Bypass effect execution. Currenty only used in devtools for hooks inspection */ + _skipEffects?: boolean; + /** Attach a hook that is invoked after an error is caught in a component but before calling lifecycle hooks */ + _catchError(error: any, vnode: VNode, oldVNode?: VNode | undefined): void; +} + +export type ComponentChild = + | VNode<any> + | string + | number + | boolean + | null + | undefined; +export type ComponentChildren = ComponentChild[] | ComponentChild; + +export interface FunctionComponent<P = {}> extends preact.FunctionComponent<P> { + // Internally, createContext uses `contextType` on a Function component to + // implement the Consumer component + contextType?: PreactContext; + + // Internally, createContext stores a ref to the context object on the Provider + // Function component to help devtools + _contextRef?: PreactContext; + + // Define these properties as undefined on FunctionComponent to get rid of + // some errors in `diff()` + getDerivedStateFromProps?: undefined; + getDerivedStateFromError?: undefined; +} + +export interface ComponentClass<P = {}> extends preact.ComponentClass<P> { + _contextRef?: any; + + // Override public contextType with internal PreactContext type + contextType?: PreactContext; +} + +// Redefine ComponentType using our new internal FunctionComponent interface above +export type ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P>; + +export interface PreactElement extends HTMLElement { + _children?: VNode<any> | null; + /** Event listeners to support event delegation */ + _listeners?: Record<string, (e: Event) => void>; + + // Preact uses this attribute to detect SVG nodes + ownerSVGElement?: SVGElement | null; + + // style: HTMLElement["style"]; // From HTMLElement + + data?: string | number; // From Text node +} + +// We use the `current` property to differentiate between the two kinds of Refs so +// internally we'll define `current` on both to make TypeScript happy +type RefObject<T> = { current: T | null }; +type RefCallback<T> = { (instance: T | null): void; current: undefined }; +type Ref<T> = RefObject<T> | RefCallback<T>; + +export interface VNode<P = {}> extends preact.VNode<P> { + // Redefine type here using our internal ComponentType type + type: string | ComponentType<P>; + props: P & { children: ComponentChildren }; + ref?: Ref<any> | null; + _children: Array<VNode<any>> | null; + _parent: VNode | null; + _depth: number | null; + /** + * The [first (for Fragments)] DOM child of a VNode + */ + _dom: PreactElement | null; + /** + * The last dom child of a Fragment, or components that return a Fragment + */ + _nextDom: PreactElement | null; + _component: Component | null; + _hydrating: boolean | null; + constructor: undefined; + _original: number; +} + +export interface Component<P = {}, S = {}> extends preact.Component<P, S> { + // When component is functional component, this is reset to functional component + constructor: ComponentType<P>; + state: S; // Override Component["state"] to not be readonly for internal use, specifically Hooks + base?: PreactElement; + + _dirty: boolean; + _force?: boolean; + _renderCallbacks: Array<() => void>; // Only class components + _globalContext?: any; + _vnode?: VNode<P> | null; + _nextState?: S | null; // Only class components + /** Only used in the devtools to later dirty check if state has changed */ + _prevState?: S | null; + /** + * Pointer to the parent dom node. This is only needed for top-level Fragment + * components or array returns. + */ + _parentDom?: PreactElement | null; + // Always read, set only when handling error + _processingException?: Component<any, any> | null; + // Always read, set only when handling error. This is used to indicate at diffTime to set _processingException + _pendingError?: Component<any, any> | null; +} + +export interface PreactContext extends preact.Context<any> { + _id: string; + _defaultValue: any; +} diff --git a/preact/src/jsx.d.ts b/preact/src/jsx.d.ts new file mode 100644 index 0000000..7533435 --- /dev/null +++ b/preact/src/jsx.d.ts @@ -0,0 +1,974 @@ +// Users who only use Preact for SSR might not specify "dom" in their lib in tsconfig.json +/// <reference lib="dom" /> + +import { + ClassAttributes, + Component, + PreactDOMAttributes, + VNode +} from './index'; + +type Defaultize<Props, Defaults> = + // Distribute over unions + Props extends any // Make any properties included in Default optional + ? Partial<Pick<Props, Extract<keyof Props, keyof Defaults>>> & // Include the remaining properties from Props + Pick<Props, Exclude<keyof Props, keyof Defaults>> + : never; + +export namespace JSXInternal { + export type LibraryManagedAttributes<Component, Props> = Component extends { + defaultProps: infer Defaults; + } + ? Defaultize<Props, Defaults> + : Props; + + export interface IntrinsicAttributes { + key?: any; + } + + export interface Element extends VNode<any> {} + + export interface ElementClass extends Component<any, any> {} + + export interface ElementAttributesProperty { + props: any; + } + + export interface ElementChildrenAttribute { + children: any; + } + + export type DOMCSSProperties = { + [key in keyof Omit< + CSSStyleDeclaration, + | 'item' + | 'setProperty' + | 'removeProperty' + | 'getPropertyValue' + | 'getPropertyPriority' + >]?: string | number | null | undefined; + }; + export type AllCSSProperties = { + [key: string]: string | number | null | undefined; + }; + export interface CSSProperties extends AllCSSProperties, DOMCSSProperties { + cssText?: string | null; + } + + export interface SVGAttributes<Target extends EventTarget = SVGElement> + extends HTMLAttributes<Target> { + 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 | number; + 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 PathAttributes { + d: string; + } + + export type TargetedEvent< + Target extends EventTarget = EventTarget, + TypedEvent extends Event = Event + > = Omit<TypedEvent, 'currentTarget'> & { + readonly currentTarget: Target; + }; + + export type TargetedAnimationEvent< + Target extends EventTarget + > = TargetedEvent<Target, AnimationEvent>; + export type TargetedClipboardEvent< + Target extends EventTarget + > = TargetedEvent<Target, ClipboardEvent>; + export type TargetedCompositionEvent< + Target extends EventTarget + > = TargetedEvent<Target, CompositionEvent>; + export type TargetedDragEvent<Target extends EventTarget> = TargetedEvent< + Target, + DragEvent + >; + export type TargetedFocusEvent<Target extends EventTarget> = TargetedEvent< + Target, + FocusEvent + >; + export type TargetedKeyboardEvent<Target extends EventTarget> = TargetedEvent< + Target, + KeyboardEvent + >; + export type TargetedMouseEvent<Target extends EventTarget> = TargetedEvent< + Target, + MouseEvent + >; + export type TargetedPointerEvent<Target extends EventTarget> = TargetedEvent< + Target, + PointerEvent + >; + export type TargetedTouchEvent<Target extends EventTarget> = TargetedEvent< + Target, + TouchEvent + >; + export type TargetedTransitionEvent< + Target extends EventTarget + > = TargetedEvent<Target, TransitionEvent>; + export type TargetedUIEvent<Target extends EventTarget> = TargetedEvent< + Target, + UIEvent + >; + export type TargetedWheelEvent<Target extends EventTarget> = TargetedEvent< + Target, + WheelEvent + >; + + export interface EventHandler<E extends TargetedEvent> { + /** + * The `this` keyword always points to the DOM element the event handler + * was invoked on. See: https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Event_handlers#Event_handlers_parameters_this_binding_and_the_return_value + */ + (this: never, event: E): void; + } + + export type AnimationEventHandler<Target extends EventTarget> = EventHandler< + TargetedAnimationEvent<Target> + >; + export type ClipboardEventHandler<Target extends EventTarget> = EventHandler< + TargetedClipboardEvent<Target> + >; + export type CompositionEventHandler< + Target extends EventTarget + > = EventHandler<TargetedCompositionEvent<Target>>; + export type DragEventHandler<Target extends EventTarget> = EventHandler< + TargetedDragEvent<Target> + >; + export type FocusEventHandler<Target extends EventTarget> = EventHandler< + TargetedFocusEvent<Target> + >; + export type GenericEventHandler<Target extends EventTarget> = EventHandler< + TargetedEvent<Target> + >; + export type KeyboardEventHandler<Target extends EventTarget> = EventHandler< + TargetedKeyboardEvent<Target> + >; + export type MouseEventHandler<Target extends EventTarget> = EventHandler< + TargetedMouseEvent<Target> + >; + export type PointerEventHandler<Target extends EventTarget> = EventHandler< + TargetedPointerEvent<Target> + >; + export type TouchEventHandler<Target extends EventTarget> = EventHandler< + TargetedTouchEvent<Target> + >; + export type TransitionEventHandler<Target extends EventTarget> = EventHandler< + TargetedTransitionEvent<Target> + >; + export type UIEventHandler<Target extends EventTarget> = EventHandler< + TargetedUIEvent<Target> + >; + export type WheelEventHandler<Target extends EventTarget> = EventHandler< + TargetedWheelEvent<Target> + >; + + export interface DOMAttributes<Target extends EventTarget> + extends PreactDOMAttributes { + // Image Events + onLoad?: GenericEventHandler<Target>; + onLoadCapture?: GenericEventHandler<Target>; + onError?: GenericEventHandler<Target>; + onErrorCapture?: GenericEventHandler<Target>; + + // Clipboard Events + onCopy?: ClipboardEventHandler<Target>; + onCopyCapture?: ClipboardEventHandler<Target>; + onCut?: ClipboardEventHandler<Target>; + onCutCapture?: ClipboardEventHandler<Target>; + onPaste?: ClipboardEventHandler<Target>; + onPasteCapture?: ClipboardEventHandler<Target>; + + // Composition Events + onCompositionEnd?: CompositionEventHandler<Target>; + onCompositionEndCapture?: CompositionEventHandler<Target>; + onCompositionStart?: CompositionEventHandler<Target>; + onCompositionStartCapture?: CompositionEventHandler<Target>; + onCompositionUpdate?: CompositionEventHandler<Target>; + onCompositionUpdateCapture?: CompositionEventHandler<Target>; + + // Details Events + onToggle?: GenericEventHandler<Target>; + + // Focus Events + onFocus?: FocusEventHandler<Target>; + onFocusCapture?: FocusEventHandler<Target>; + onBlur?: FocusEventHandler<Target>; + onBlurCapture?: FocusEventHandler<Target>; + + // Form Events + onChange?: GenericEventHandler<Target>; + onChangeCapture?: GenericEventHandler<Target>; + onInput?: GenericEventHandler<Target>; + onInputCapture?: GenericEventHandler<Target>; + onSearch?: GenericEventHandler<Target>; + onSearchCapture?: GenericEventHandler<Target>; + onSubmit?: GenericEventHandler<Target>; + onSubmitCapture?: GenericEventHandler<Target>; + onInvalid?: GenericEventHandler<Target>; + onInvalidCapture?: GenericEventHandler<Target>; + onReset?: GenericEventHandler<Target>; + onResetCapture?: GenericEventHandler<Target>; + onFormData?: GenericEventHandler<Target>; + onFormDataCapture?: GenericEventHandler<Target>; + + // Keyboard Events + onKeyDown?: KeyboardEventHandler<Target>; + onKeyDownCapture?: KeyboardEventHandler<Target>; + onKeyPress?: KeyboardEventHandler<Target>; + onKeyPressCapture?: KeyboardEventHandler<Target>; + onKeyUp?: KeyboardEventHandler<Target>; + onKeyUpCapture?: KeyboardEventHandler<Target>; + + // Media Events + onAbort?: GenericEventHandler<Target>; + onAbortCapture?: GenericEventHandler<Target>; + onCanPlay?: GenericEventHandler<Target>; + onCanPlayCapture?: GenericEventHandler<Target>; + onCanPlayThrough?: GenericEventHandler<Target>; + onCanPlayThroughCapture?: GenericEventHandler<Target>; + onDurationChange?: GenericEventHandler<Target>; + onDurationChangeCapture?: GenericEventHandler<Target>; + onEmptied?: GenericEventHandler<Target>; + onEmptiedCapture?: GenericEventHandler<Target>; + onEncrypted?: GenericEventHandler<Target>; + onEncryptedCapture?: GenericEventHandler<Target>; + onEnded?: GenericEventHandler<Target>; + onEndedCapture?: GenericEventHandler<Target>; + onLoadedData?: GenericEventHandler<Target>; + onLoadedDataCapture?: GenericEventHandler<Target>; + onLoadedMetadata?: GenericEventHandler<Target>; + onLoadedMetadataCapture?: GenericEventHandler<Target>; + onLoadStart?: GenericEventHandler<Target>; + onLoadStartCapture?: GenericEventHandler<Target>; + onPause?: GenericEventHandler<Target>; + onPauseCapture?: GenericEventHandler<Target>; + onPlay?: GenericEventHandler<Target>; + onPlayCapture?: GenericEventHandler<Target>; + onPlaying?: GenericEventHandler<Target>; + onPlayingCapture?: GenericEventHandler<Target>; + onProgress?: GenericEventHandler<Target>; + onProgressCapture?: GenericEventHandler<Target>; + onRateChange?: GenericEventHandler<Target>; + onRateChangeCapture?: GenericEventHandler<Target>; + onSeeked?: GenericEventHandler<Target>; + onSeekedCapture?: GenericEventHandler<Target>; + onSeeking?: GenericEventHandler<Target>; + onSeekingCapture?: GenericEventHandler<Target>; + onStalled?: GenericEventHandler<Target>; + onStalledCapture?: GenericEventHandler<Target>; + onSuspend?: GenericEventHandler<Target>; + onSuspendCapture?: GenericEventHandler<Target>; + onTimeUpdate?: GenericEventHandler<Target>; + onTimeUpdateCapture?: GenericEventHandler<Target>; + onVolumeChange?: GenericEventHandler<Target>; + onVolumeChangeCapture?: GenericEventHandler<Target>; + onWaiting?: GenericEventHandler<Target>; + onWaitingCapture?: GenericEventHandler<Target>; + + // MouseEvents + onClick?: MouseEventHandler<Target>; + onClickCapture?: MouseEventHandler<Target>; + onContextMenu?: MouseEventHandler<Target>; + onContextMenuCapture?: MouseEventHandler<Target>; + onDblClick?: MouseEventHandler<Target>; + onDblClickCapture?: MouseEventHandler<Target>; + onDrag?: DragEventHandler<Target>; + onDragCapture?: DragEventHandler<Target>; + onDragEnd?: DragEventHandler<Target>; + onDragEndCapture?: DragEventHandler<Target>; + onDragEnter?: DragEventHandler<Target>; + onDragEnterCapture?: DragEventHandler<Target>; + onDragExit?: DragEventHandler<Target>; + onDragExitCapture?: DragEventHandler<Target>; + onDragLeave?: DragEventHandler<Target>; + onDragLeaveCapture?: DragEventHandler<Target>; + onDragOver?: DragEventHandler<Target>; + onDragOverCapture?: DragEventHandler<Target>; + onDragStart?: DragEventHandler<Target>; + onDragStartCapture?: DragEventHandler<Target>; + onDrop?: DragEventHandler<Target>; + onDropCapture?: DragEventHandler<Target>; + onMouseDown?: MouseEventHandler<Target>; + onMouseDownCapture?: MouseEventHandler<Target>; + onMouseEnter?: MouseEventHandler<Target>; + onMouseEnterCapture?: MouseEventHandler<Target>; + onMouseLeave?: MouseEventHandler<Target>; + onMouseLeaveCapture?: MouseEventHandler<Target>; + onMouseMove?: MouseEventHandler<Target>; + onMouseMoveCapture?: MouseEventHandler<Target>; + onMouseOut?: MouseEventHandler<Target>; + onMouseOutCapture?: MouseEventHandler<Target>; + onMouseOver?: MouseEventHandler<Target>; + onMouseOverCapture?: MouseEventHandler<Target>; + onMouseUp?: MouseEventHandler<Target>; + onMouseUpCapture?: MouseEventHandler<Target>; + + // Selection Events + onSelect?: GenericEventHandler<Target>; + onSelectCapture?: GenericEventHandler<Target>; + + // Touch Events + onTouchCancel?: TouchEventHandler<Target>; + onTouchCancelCapture?: TouchEventHandler<Target>; + onTouchEnd?: TouchEventHandler<Target>; + onTouchEndCapture?: TouchEventHandler<Target>; + onTouchMove?: TouchEventHandler<Target>; + onTouchMoveCapture?: TouchEventHandler<Target>; + onTouchStart?: TouchEventHandler<Target>; + onTouchStartCapture?: TouchEventHandler<Target>; + + // Pointer Events + onPointerOver?: PointerEventHandler<Target>; + onPointerOverCapture?: PointerEventHandler<Target>; + onPointerEnter?: PointerEventHandler<Target>; + onPointerEnterCapture?: PointerEventHandler<Target>; + onPointerDown?: PointerEventHandler<Target>; + onPointerDownCapture?: PointerEventHandler<Target>; + onPointerMove?: PointerEventHandler<Target>; + onPointerMoveCapture?: PointerEventHandler<Target>; + onPointerUp?: PointerEventHandler<Target>; + onPointerUpCapture?: PointerEventHandler<Target>; + onPointerCancel?: PointerEventHandler<Target>; + onPointerCancelCapture?: PointerEventHandler<Target>; + onPointerOut?: PointerEventHandler<Target>; + onPointerOutCapture?: PointerEventHandler<Target>; + onPointerLeave?: PointerEventHandler<Target>; + onPointerLeaveCapture?: PointerEventHandler<Target>; + onGotPointerCapture?: PointerEventHandler<Target>; + onGotPointerCaptureCapture?: PointerEventHandler<Target>; + onLostPointerCapture?: PointerEventHandler<Target>; + onLostPointerCaptureCapture?: PointerEventHandler<Target>; + + // UI Events + onScroll?: UIEventHandler<Target>; + onScrollCapture?: UIEventHandler<Target>; + + // Wheel Events + onWheel?: WheelEventHandler<Target>; + onWheelCapture?: WheelEventHandler<Target>; + + // Animation Events + onAnimationStart?: AnimationEventHandler<Target>; + onAnimationStartCapture?: AnimationEventHandler<Target>; + onAnimationEnd?: AnimationEventHandler<Target>; + onAnimationEndCapture?: AnimationEventHandler<Target>; + onAnimationIteration?: AnimationEventHandler<Target>; + onAnimationIterationCapture?: AnimationEventHandler<Target>; + + // Transition Events + onTransitionEnd?: TransitionEventHandler<Target>; + onTransitionEndCapture?: TransitionEventHandler<Target>; + } + + export interface HTMLAttributes<RefType extends EventTarget = EventTarget> + extends ClassAttributes<RefType>, + DOMAttributes<RefType> { + // Standard HTML Attributes + accept?: string; + acceptCharset?: string; + accessKey?: string; + action?: string; + allowFullScreen?: boolean; + allowTransparency?: boolean; + alt?: string; + as?: string; + async?: boolean; + autocomplete?: string; + autoComplete?: string; + autocorrect?: string; + autoCorrect?: string; + autofocus?: boolean; + autoFocus?: boolean; + autoPlay?: boolean; + capture?: boolean | string; + cellPadding?: number | string; + cellSpacing?: number | string; + charSet?: string; + challenge?: string; + checked?: boolean; + class?: string; + className?: 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?: 'auto' | 'rtl' | 'ltr'; + disabled?: boolean; + disableRemotePlayback?: boolean; + download?: any; + decoding?: 'sync' | 'async' | 'auto'; + 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; + htmlFor?: 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; + loading?: 'eager' | 'lazy'; + 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; + nonce?: string; + noValidate?: boolean; + open?: boolean; + optimum?: number; + pattern?: string; + placeholder?: string; + playsInline?: boolean; + poster?: string; + preload?: string; + radioGroup?: string; + readonly?: boolean; + 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; + spellCheck?: boolean; + src?: string; + srcset?: string; + srcDoc?: string; + srcLang?: string; + srcSet?: string; + start?: number; + step?: number | string; + style?: string | CSSProperties; + 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; + + // Non-standard Attributes + autocapitalize?: + | 'off' + | 'none' + | 'on' + | 'sentences' + | 'words' + | 'characters'; + autoCapitalize?: + | 'off' + | 'none' + | 'on' + | 'sentences' + | 'words' + | 'characters'; + + // 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 HTMLMarqueeElement extends HTMLElement { + behavior?: 'scroll' | 'slide' | 'alternate'; + bgColor?: string; + direction?: 'left' | 'right' | 'up' | 'down'; + height?: number | string; + hspace?: number | string; + loop?: number | string; + scrollAmount?: number | string; + scrollDelay?: number | string; + trueSpeed?: boolean; + vspace?: number | string; + width?: number | string; + } + + export interface IntrinsicElements { + // HTML + a: HTMLAttributes<HTMLAnchorElement>; + abbr: HTMLAttributes<HTMLElement>; + address: HTMLAttributes<HTMLElement>; + area: HTMLAttributes<HTMLAreaElement>; + article: HTMLAttributes<HTMLElement>; + aside: HTMLAttributes<HTMLElement>; + audio: HTMLAttributes<HTMLAudioElement>; + b: HTMLAttributes<HTMLElement>; + base: HTMLAttributes<HTMLBaseElement>; + bdi: HTMLAttributes<HTMLElement>; + bdo: HTMLAttributes<HTMLElement>; + big: HTMLAttributes<HTMLElement>; + blockquote: HTMLAttributes<HTMLQuoteElement>; + body: HTMLAttributes<HTMLBodyElement>; + br: HTMLAttributes<HTMLBRElement>; + button: HTMLAttributes<HTMLButtonElement>; + canvas: HTMLAttributes<HTMLCanvasElement>; + caption: HTMLAttributes<HTMLTableCaptionElement>; + cite: HTMLAttributes<HTMLElement>; + code: HTMLAttributes<HTMLElement>; + col: HTMLAttributes<HTMLTableColElement>; + colgroup: HTMLAttributes<HTMLTableColElement>; + data: HTMLAttributes<HTMLDataElement>; + datalist: HTMLAttributes<HTMLDataListElement>; + dd: HTMLAttributes<HTMLElement>; + del: HTMLAttributes<HTMLModElement>; + details: HTMLAttributes<HTMLDetailsElement>; + dfn: HTMLAttributes<HTMLElement>; + dialog: HTMLAttributes<HTMLDialogElement>; + div: HTMLAttributes<HTMLDivElement>; + dl: HTMLAttributes<HTMLDListElement>; + dt: HTMLAttributes<HTMLElement>; + em: HTMLAttributes<HTMLElement>; + embed: HTMLAttributes<HTMLEmbedElement>; + fieldset: HTMLAttributes<HTMLFieldSetElement>; + figcaption: HTMLAttributes<HTMLElement>; + figure: HTMLAttributes<HTMLElement>; + footer: HTMLAttributes<HTMLElement>; + form: HTMLAttributes<HTMLFormElement>; + h1: HTMLAttributes<HTMLHeadingElement>; + h2: HTMLAttributes<HTMLHeadingElement>; + h3: HTMLAttributes<HTMLHeadingElement>; + h4: HTMLAttributes<HTMLHeadingElement>; + h5: HTMLAttributes<HTMLHeadingElement>; + h6: HTMLAttributes<HTMLHeadingElement>; + head: HTMLAttributes<HTMLHeadElement>; + header: HTMLAttributes<HTMLElement>; + hgroup: HTMLAttributes<HTMLElement>; + hr: HTMLAttributes<HTMLHRElement>; + html: HTMLAttributes<HTMLHtmlElement>; + i: HTMLAttributes<HTMLElement>; + iframe: HTMLAttributes<HTMLIFrameElement>; + img: HTMLAttributes<HTMLImageElement>; + input: HTMLAttributes<HTMLInputElement>; + ins: HTMLAttributes<HTMLModElement>; + kbd: HTMLAttributes<HTMLElement>; + keygen: HTMLAttributes<HTMLUnknownElement>; + label: HTMLAttributes<HTMLLabelElement>; + legend: HTMLAttributes<HTMLLegendElement>; + li: HTMLAttributes<HTMLLIElement>; + link: HTMLAttributes<HTMLLinkElement>; + main: HTMLAttributes<HTMLElement>; + map: HTMLAttributes<HTMLMapElement>; + mark: HTMLAttributes<HTMLElement>; + marquee: HTMLAttributes<HTMLMarqueeElement>; + menu: HTMLAttributes<HTMLMenuElement>; + menuitem: HTMLAttributes<HTMLUnknownElement>; + meta: HTMLAttributes<HTMLMetaElement>; + meter: HTMLAttributes<HTMLMeterElement>; + nav: HTMLAttributes<HTMLElement>; + noscript: HTMLAttributes<HTMLElement>; + object: HTMLAttributes<HTMLObjectElement>; + ol: HTMLAttributes<HTMLOListElement>; + optgroup: HTMLAttributes<HTMLOptGroupElement>; + option: HTMLAttributes<HTMLOptionElement>; + output: HTMLAttributes<HTMLOutputElement>; + p: HTMLAttributes<HTMLParagraphElement>; + param: HTMLAttributes<HTMLParamElement>; + picture: HTMLAttributes<HTMLPictureElement>; + pre: HTMLAttributes<HTMLPreElement>; + progress: HTMLAttributes<HTMLProgressElement>; + q: HTMLAttributes<HTMLQuoteElement>; + rp: HTMLAttributes<HTMLElement>; + rt: HTMLAttributes<HTMLElement>; + ruby: HTMLAttributes<HTMLElement>; + s: HTMLAttributes<HTMLElement>; + samp: HTMLAttributes<HTMLElement>; + script: HTMLAttributes<HTMLScriptElement>; + section: HTMLAttributes<HTMLElement>; + select: HTMLAttributes<HTMLSelectElement>; + slot: HTMLAttributes<HTMLSlotElement>; + small: HTMLAttributes<HTMLElement>; + source: HTMLAttributes<HTMLSourceElement>; + span: HTMLAttributes<HTMLSpanElement>; + strong: HTMLAttributes<HTMLElement>; + style: HTMLAttributes<HTMLStyleElement>; + sub: HTMLAttributes<HTMLElement>; + summary: HTMLAttributes<HTMLElement>; + sup: HTMLAttributes<HTMLElement>; + table: HTMLAttributes<HTMLTableElement>; + tbody: HTMLAttributes<HTMLTableSectionElement>; + td: HTMLAttributes<HTMLTableCellElement>; + textarea: HTMLAttributes<HTMLTextAreaElement>; + tfoot: HTMLAttributes<HTMLTableSectionElement>; + th: HTMLAttributes<HTMLTableCellElement>; + thead: HTMLAttributes<HTMLTableSectionElement>; + time: HTMLAttributes<HTMLTimeElement>; + title: HTMLAttributes<HTMLTitleElement>; + tr: HTMLAttributes<HTMLTableRowElement>; + track: HTMLAttributes<HTMLTrackElement>; + u: HTMLAttributes<HTMLElement>; + ul: HTMLAttributes<HTMLUListElement>; + var: HTMLAttributes<HTMLElement>; + video: HTMLAttributes<HTMLVideoElement>; + wbr: HTMLAttributes<HTMLElement>; + + //SVG + svg: SVGAttributes<SVGSVGElement>; + animate: SVGAttributes<SVGAnimateElement>; + circle: SVGAttributes<SVGCircleElement>; + animateTransform: SVGAttributes<SVGAnimateElement>; + clipPath: SVGAttributes<SVGClipPathElement>; + defs: SVGAttributes<SVGDefsElement>; + desc: SVGAttributes<SVGDescElement>; + ellipse: SVGAttributes<SVGEllipseElement>; + feBlend: SVGAttributes<SVGFEBlendElement>; + feColorMatrix: SVGAttributes<SVGFEColorMatrixElement>; + feComponentTransfer: SVGAttributes<SVGFEComponentTransferElement>; + feComposite: SVGAttributes<SVGFECompositeElement>; + feConvolveMatrix: SVGAttributes<SVGFEConvolveMatrixElement>; + feDiffuseLighting: SVGAttributes<SVGFEDiffuseLightingElement>; + feDisplacementMap: SVGAttributes<SVGFEDisplacementMapElement>; + feDropShadow: SVGAttributes<SVGFEDropShadowElement>; + feFlood: SVGAttributes<SVGFEFloodElement>; + feFuncA: SVGAttributes<SVGFEFuncAElement>; + feFuncB: SVGAttributes<SVGFEFuncBElement>; + feFuncG: SVGAttributes<SVGFEFuncGElement>; + feFuncR: SVGAttributes<SVGFEFuncRElement>; + feGaussianBlur: SVGAttributes<SVGFEGaussianBlurElement>; + feImage: SVGAttributes<SVGFEImageElement>; + feMerge: SVGAttributes<SVGFEMergeElement>; + feMergeNode: SVGAttributes<SVGFEMergeNodeElement>; + feMorphology: SVGAttributes<SVGFEMorphologyElement>; + feOffset: SVGAttributes<SVGFEOffsetElement>; + feSpecularLighting: SVGAttributes<SVGFESpecularLightingElement>; + feTile: SVGAttributes<SVGFETileElement>; + feTurbulence: SVGAttributes<SVGFETurbulenceElement>; + filter: SVGAttributes<SVGFilterElement>; + foreignObject: SVGAttributes<SVGForeignObjectElement>; + g: SVGAttributes<SVGGElement>; + image: SVGAttributes<SVGImageElement>; + line: SVGAttributes<SVGLineElement>; + linearGradient: SVGAttributes<SVGLinearGradientElement>; + marker: SVGAttributes<SVGMarkerElement>; + mask: SVGAttributes<SVGMaskElement>; + path: SVGAttributes<SVGPathElement>; + pattern: SVGAttributes<SVGPatternElement>; + polygon: SVGAttributes<SVGPolygonElement>; + polyline: SVGAttributes<SVGPolylineElement>; + radialGradient: SVGAttributes<SVGRadialGradientElement>; + rect: SVGAttributes<SVGRectElement>; + stop: SVGAttributes<SVGStopElement>; + symbol: SVGAttributes<SVGSymbolElement>; + text: SVGAttributes<SVGTextElement>; + tspan: SVGAttributes<SVGTSpanElement>; + use: SVGAttributes<SVGUseElement>; + } +} diff --git a/preact/src/options.js b/preact/src/options.js new file mode 100644 index 0000000..174f322 --- /dev/null +++ b/preact/src/options.js @@ -0,0 +1,16 @@ +import { _catchError } from './diff/catch-error'; + +/** + * The `option` object can potentially contain callback functions + * that are called during various stages of our renderer. This is the + * foundation on which all our addons like `preact/debug`, `preact/compat`, + * and `preact/hooks` are based on. See the `Options` type in `internal.d.ts` + * for a full list of available option hooks (most editors/IDEs allow you to + * ctrl+click or cmd+click on mac the type definition below). + * @type {import('./internal').Options} + */ +const options = { + _catchError +}; + +export default options; diff --git a/preact/src/render.js b/preact/src/render.js new file mode 100644 index 0000000..c8ef828 --- /dev/null +++ b/preact/src/render.js @@ -0,0 +1,75 @@ +import { EMPTY_OBJ } from './constants'; +import { commitRoot, diff } from './diff/index'; +import { createElement, Fragment } from './create-element'; +import options from './options'; +import { slice } from './util'; + +/** + * Render a Preact virtual node into a DOM element + * @param {import('./internal').ComponentChild} vnode The virtual node to render + * @param {import('./internal').PreactElement} parentDom The DOM element to + * render into + * @param {import('./internal').PreactElement | object} [replaceNode] Optional: Attempt to re-use an + * existing DOM tree rooted at `replaceNode` + */ +export function render(vnode, parentDom, replaceNode) { + if (options._root) options._root(vnode, parentDom); + + // We abuse the `replaceNode` parameter in `hydrate()` to signal if we are in + // hydration mode or not by passing the `hydrate` function instead of a DOM + // element.. + let isHydrating = typeof replaceNode === 'function'; + + // To be able to support calling `render()` multiple times on the same + // DOM node, we need to obtain a reference to the previous tree. We do + // this by assigning a new `_children` property to DOM nodes which points + // to the last rendered tree. By default this property is not present, which + // means that we are mounting a new tree for the first time. + let oldVNode = isHydrating + ? null + : (replaceNode && replaceNode._children) || parentDom._children; + + vnode = ( + (!isHydrating && replaceNode) || + parentDom + )._children = createElement(Fragment, null, [vnode]); + + // List of effects that need to be called after diffing. + let commitQueue = []; + diff( + parentDom, + // Determine the new vnode tree and store it on the DOM element on + // our custom `_children` property. + vnode, + oldVNode || EMPTY_OBJ, + EMPTY_OBJ, + parentDom.ownerSVGElement !== undefined, + !isHydrating && replaceNode + ? [replaceNode] + : oldVNode + ? null + : parentDom.firstChild + ? slice.call(parentDom.childNodes) + : null, + commitQueue, + !isHydrating && replaceNode + ? replaceNode + : oldVNode + ? oldVNode._dom + : parentDom.firstChild, + isHydrating + ); + + // Flush all queued effects + commitRoot(commitQueue, vnode); +} + +/** + * Update an existing DOM element with data from a Preact virtual node + * @param {import('./internal').ComponentChild} vnode The virtual node to render + * @param {import('./internal').PreactElement} parentDom The DOM element to + * update + */ +export function hydrate(vnode, parentDom) { + render(vnode, parentDom, hydrate); +} diff --git a/preact/src/util.js b/preact/src/util.js new file mode 100644 index 0000000..0e9b3b1 --- /dev/null +++ b/preact/src/util.js @@ -0,0 +1,27 @@ +import { EMPTY_ARR } from './constants'; + +/** + * Assign properties from `props` to `obj` + * @template O, P The obj and props types + * @param {O} obj The object to copy properties to + * @param {P} props The object to copy properties from + * @returns {O & P} + */ +export function assign(obj, props) { + // @ts-ignore We change the type of `obj` to be `O & P` + for (let i in props) obj[i] = props[i]; + return /** @type {O & P} */ (obj); +} + +/** + * Remove a child node from its parent if attached. This is a workaround for + * IE11 which doesn't support `Element.prototype.remove()`. Using this function + * is smaller than including a dedicated polyfill. + * @param {Node} node The node to remove + */ +export function removeNode(node) { + let parentNode = node.parentNode; + if (parentNode) parentNode.removeChild(node); +} + +export const slice = EMPTY_ARR.slice; |