# backtick_javascript: true # helpers: hash_get, hash_put if RUBY_ENGINE == 'opal' class VNode # just a empty place holder to make is_a?(VNode) work # internally using the js implementation end end class Fragment def initialize(props, _context) @props = props end if RUBY_ENGINE == 'opal' def render `Opal.Preact.render_buffer.pop()` children = @props[:children] `Opal.Preact.render_buffer.push(children.$$is_array ? children : [children])` nil end else def render ::Preact.render_buffer.pop ::Preact.render_buffer.push(@props[:children]) end end end module Preact EMPTY_ARR = [] UNSAFE_NAME = /[\s\n\\\/='"\0<>]/ if RUBY_ENGINE == 'opal' %x{ let vnodeId = 0; function vnode_eql(me, other) { for(let prop in me) { if (prop === 'props' && !(me[prop]["$=="](other[prop]))) { return false; } else if (me[prop] != other[prop]) { return false; } } return true; } class VNode { constructor(type, props, key, ref, original) { this.type = type; this.props = props; this.key = key; this.ref = ref; this._children = null; this._parent = null; this._depth = 0; this._dom = null; this._nextDom = undefined; this._component = null; this._hydrating = null; this._original = (original == null) ? ++vnodeId : original; this.constructor = undefined; } "$=="(other) { return vnode_eql(this, other); } "$eql?"(other) { return vnode_eql(this, other); } "$is_a?"(t) { return t === Opal.VNode; } } function _catchError(error, vnode, oldVNode) { let component, ctor, handled; for (; (vnode = vnode._parent); ) { if ((component = vnode._component) && !component._processingException) { try { if (component["$respond_to?"]("get_derived_state_from_error")) { component.$set_state(component.$get_derived_state_from_error(error)); handled = component._dirty; } if (component["$respond_to?"]("component_did_catch")) { component.$component_did_catch(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; } const EMPTY_OBJ = new VNode(); const EMPTY_ARR = []; const slice = EMPTY_ARR.slice; function hash_fetch(hash, key) { let val = $hash_get(hash, key); return (val === undefined || val === null) ? nil : val; } function assign(obj, props) { for (let i in props) obj[i] = props[i]; return obj; } function applyRef(ref, value, vnode) { try { let converted_value; if (value == null || typeof(value) === 'undefined' ) { converted_value = nil; } else if (typeof value.$$class !== 'undefined') { converted_value = value; } else if (value instanceof Element || value instanceof Node) { converted_value = #{Browser::Element.new(`value`)}; } if (typeof ref === "function") ref.$call(converted_value); else ref.set("current", converted_value); } catch (e) { _catchError(e, vnode); } } 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; } 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 || oldVNode === nil || newDom != oldDom || newDom.parentNode == null || newDom.parentNode === nil) { outer: if (oldDom == null || oldDom === nil || oldDom.parentNode !== parentDom) { parentDom.appendChild(newDom); nextDom = null; } else { // `j{reuse}{reuse}) 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') { childVNode = newParentVNode._children[i] = new VNode( null, childVNode, null, null, childVNode ); } else if (childVNode.$$is_string) { let str = childVNode.valueOf(); childVNode = newParentVNode._children[i] = new VNode( null, str, null, null, str ); } else if (childVNode.$$is_array) { childVNode = newParentVNode._children[i] = new VNode( Opal.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 =
//
{reuse}{reuse}
childVNode = newParentVNode._children[i] = new VNode( childVNode.type, childVNode.props, childVNode.key, childVNode.ref ? childVNode.ref : null, childVNode._original ); } else { childVNode = newParentVNode._children[i] = childVNode; } if (childVNode === nil || 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 === nil || (oldVNode && oldVNode !== nil && 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 && oldVNode !== nil && childVNode.key == oldVNode.key && childVNode.type === oldVNode.type ) { oldChildren[j] = undefined; break; } oldVNode = null; } } oldVNode = (oldVNode && oldVNode !== nil) ? 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) && j !== nil && oldVNode.ref != j) { if (!refs) refs = []; if (oldVNode.ref && oldVNode.ref !== nil) 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 === oldVNode._children ) { childVNode._nextDom = oldDom = reorderChildren( childVNode, oldDom, parentDom ); } else { oldDom = placeChild( parentDom, childVNode, oldVNode, oldChildren, newDom, oldDom ); } 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); } self.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 eventProxy(e) { this._listeners[e.type + false].$call(#{::Browser::Event.new(`e`)}); } function eventProxyCapture(e) { this._listeners[e.type + true].$call(#{::Browser::Event.new(`e`)}); } const IS_NON_DIMENSIONAL = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i; function setStyle(style, key, value) { if (key[0] === '-') { style.setProperty(key, value); } else if (value == null || value === nil) { style[key] = ''; } else if (typeof value != 'number' || IS_NON_DIMENSIONAL.test(key)) { style[key] = value; } else { style[key] = value + 'px'; } } self.setProperty = function(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 (value && value !== nil && value.$$is_hash) { value = Object.fromEntries(value); } if (oldValue && oldValue !== nil && oldValue.$$is_hash) { oldValue = Object.fromEntries(oldValue); } if (oldValue && oldValue !== nil) { for (name in oldValue) { if (!(value && name in value)) { setStyle(dom.style, name, ''); } } } if (value && value !== nil) { for (name in value) { if (!oldValue || oldValue === nil || 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' && name[2] === '_') { useCapture = name !== (name = name.replace(/_capture$/, '')); // Infer correct casing for DOM built-in events: let namesl = name.slice(3); let domname = 'on' + namesl; if (domname in dom) name = namesl; else name = namesl; let evhandler = value; if (!dom._listeners) dom._listeners = {}; dom._listeners[name + useCapture] = evhandler; if (value && value !== nil) { if (!oldValue || oldValue === nil) { 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 !== 'rowSpan' && name !== 'colSpan' && name in dom ) { try { dom[name] = (value == null || value === nil) ? '' : value; // labelled break is 1b smaller here than a return statement (sorry) break o; } catch (e) {} } // aria- and data- attributes have no boolean representation. // A `false` value is different from the attribute not being // present, 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 too many bytes. On top of // that other frameworks generally stringify `false`. if (typeof value === 'function') { // never serialize functions as attribute values } else if ( value != null && value !== nil && (value !== false || (name[4] === '-')) ) { dom.setAttribute(name, value); } else { dom.removeAttribute(name); } } } function diff_props(dom, new_props, old_props, is_svg, hydrate) { #{`old_props`.each do |prop, value| `if (prop !== "children" && prop !== "key" && !(prop.$$is_string && new_props.has(prop))) { self.setProperty(dom, prop, null, value, is_svg); }` nil end `new_props`.each do |prop, value| if (`!hydrate || (prop[0] === 'o' && prop[1] === 'n' && prop[2] === '_')` || value.is_a?(Proc)) && `prop !== "children" && prop !== "key" && prop !== "value" && prop !== "checked"` && `hash_fetch(old_props, prop)` != value `self.setProperty(dom, prop, value, hash_fetch(old_props, prop), is_svg)` end end } } 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 && 'setAttribute' in child === !!nodeType && (nodeType ? child.localName === nodeType : child.nodeType === 3) ) { dom = child; excessDomChildren[i] = null; break; } } } if (dom == null) { if (nodeType === null) { // createTextNode returns Text, we expect PreactElement return document.createTextNode(newProps); } if (isSvg) { dom = document.createElementNS( 'http://www.w3.org/2000/svg', // We know `newVNode.type` is a string nodeType ); } else { let np = Object.fromEntries(newProps); dom = document.createElement( // We know `newVNode.type` is a string nodeType, np.is && np ); } // 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 || oldProps === nil || (!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 || new Map(); let oldHtml = hash_fetch(oldProps, "dangerouslySetInnerHTML"); let newHtml = hash_fetch(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 = new Map(); for (i = 0; i < dom.attributes.length; i++) { oldProps.set(dom.attributes[i].name, dom.attributes[i].value); } } if (newHtml !== nil || oldHtml !== nil) { // Avoid re-applying the same '__html' if it has not changed between re-render if ( newHtml === nil || ((oldHtml === nil || hash_fetch(newHtml, "__html") != hash_fetch(oldHtml, "__html")) && hash_fetch(newHtml, "__html") !== dom.innerHTML) ) { dom.innerHTML = (newHtml !== nil && hash_fetch(newHtml, "__html")) || ''; } } } diff_props(dom, newProps, oldProps, isSvg, isHydrating); // If the new vnode didn't have dangerouslySetInnerHTML, diff its children if (newHtml !== nil) { newVNode._children = []; } else { i = hash_fetch(newVNode.props, "children"); diffChildren( dom, i.$$is_array ? 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 ( (i = hash_fetch(newProps, "value")) !== nil && // #2756 For the -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) || // This is only for IE 11 to fix