self.uhtml = (function (exports) { 'use strict'; var umap = (function (_) { return { // About: get: _.get.bind(_) // It looks like WebKit/Safari didn't optimize bind at all, // so that using bind slows it down by 60%. // Firefox and Chrome are just fine in both cases, // so let's use the approach that works fast everywhere š get: function get(key) { return _.get(key); }, set: function set(key, value) { return _.set(key, value), value; } }; }); var attr = /([^\s\\>"'=]+)\s*=\s*(['"]?)$/; var empty = /^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i; var node = /<[a-z][^>]+$/i; var notNode = />[^<>]*$/; var selfClosing = /<([a-z]+[a-z0-9:._-]*)([^>]*?)(\/>)/ig; var trimEnd = /\s+$/; var isNode = function isNode(template, i) { return 0 < i-- && (node.test(template[i]) || !notNode.test(template[i]) && isNode(template, i)); }; var regular = function regular(original, name, extra) { return empty.test(name) ? original : "<".concat(name).concat(extra.replace(trimEnd, ''), ">").concat(name, ">"); }; var instrument = (function (template, prefix, svg) { var text = []; var length = template.length; var _loop = function _loop(i) { var chunk = template[i - 1]; text.push(attr.test(chunk) && isNode(template, i) ? chunk.replace(attr, function (_, $1, $2) { return "".concat(prefix).concat(i - 1, "=").concat($2 || '"').concat($1).concat($2 ? '' : '"'); }) : "".concat(chunk, "")); }; for (var i = 1; i < length; i++) { _loop(i); } text.push(template[length - 1]); var output = text.join('').trim(); return svg ? output : output.replace(selfClosing, regular); }); var isArray = Array.isArray; var _ref = [], indexOf = _ref.indexOf, slice = _ref.slice; var ELEMENT_NODE = 1; var nodeType = 111; var remove = function remove(_ref) { var firstChild = _ref.firstChild, lastChild = _ref.lastChild; var range = document.createRange(); range.setStartAfter(firstChild); range.setEndAfter(lastChild); range.deleteContents(); return firstChild; }; var diffable = function diffable(node, operation) { return node.nodeType === nodeType ? 1 / operation < 0 ? operation ? remove(node) : node.lastChild : operation ? node.valueOf() : node.firstChild : node; }; var persistent = function persistent(fragment) { var childNodes = fragment.childNodes; var length = childNodes.length; if (length < 2) return length ? childNodes[0] : fragment; var nodes = slice.call(childNodes, 0); var firstChild = nodes[0]; var lastChild = nodes[length - 1]; return { ELEMENT_NODE: ELEMENT_NODE, nodeType: nodeType, firstChild: firstChild, lastChild: lastChild, valueOf: function valueOf() { if (childNodes.length !== length) { var i = 0; while (i < length) { fragment.appendChild(nodes[i++]); } } return fragment; } }; }; /** * ISC License * * Copyright (c) 2020, Andrea Giammarchi, @WebReflection * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. */ /** * @param {Node} parentNode The container where children live * @param {Node[]} a The list of current/live children * @param {Node[]} b The list of future children * @param {(entry: Node, action: number) => Node} get * The callback invoked per each entry related DOM operation. * @param {Node} [before] The optional node used as anchor to insert before. * @returns {Node[]} The same list of future children. */ var udomdiff = (function (parentNode, a, b, get, before) { var bLength = b.length; var aEnd = a.length; var bEnd = bLength; var aStart = 0; var bStart = 0; var map = null; while (aStart < aEnd || bStart < bEnd) { // append head, tail, or nodes in between: fast path if (aEnd === aStart) { // we could be in a situation where the rest of nodes that // need to be added are not at the end, and in such case // the node to `insertBefore`, if the index is more than 0 // must be retrieved, otherwise it's gonna be the first item. var node = bEnd < bLength ? bStart ? get(b[bStart - 1], -0).nextSibling : get(b[bEnd - bStart], 0) : before; while (bStart < bEnd) { parentNode.insertBefore(get(b[bStart++], 1), node); } } // remove head or tail: fast path else if (bEnd === bStart) { while (aStart < aEnd) { // remove the node only if it's unknown or not live if (!map || !map.has(a[aStart])) parentNode.removeChild(get(a[aStart], -1)); aStart++; } } // same node: fast path else if (a[aStart] === b[bStart]) { aStart++; bStart++; } // same tail: fast path else if (a[aEnd - 1] === b[bEnd - 1]) { aEnd--; bEnd--; } // The once here single last swap "fast path" has been removed in v1.1.0 // https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85 // reverse swap: also fast path else if (a[aStart] === b[bEnd - 1] && b[bStart] === a[aEnd - 1]) { // this is a "shrink" operation that could happen in these cases: // [1, 2, 3, 4, 5] // [1, 4, 3, 2, 5] // or asymmetric too // [1, 2, 3, 4, 5] // [1, 2, 3, 5, 6, 4] var _node = get(a[--aEnd], -1).nextSibling; parentNode.insertBefore(get(b[bStart++], 1), get(a[aStart++], -1).nextSibling); parentNode.insertBefore(get(b[--bEnd], 1), _node); // mark the future index as identical (yeah, it's dirty, but cheap š) // The main reason to do this, is that when a[aEnd] will be reached, // the loop will likely be on the fast path, as identical to b[bEnd]. // In the best case scenario, the next loop will skip the tail, // but in the worst one, this node will be considered as already // processed, bailing out pretty quickly from the map index check a[aEnd] = b[bEnd]; } // map based fallback, "slow" path else { // the map requires an O(bEnd - bStart) operation once // to store all future nodes indexes for later purposes. // In the worst case scenario, this is a full O(N) cost, // and such scenario happens at least when all nodes are different, // but also if both first and last items of the lists are different if (!map) { map = new Map(); var i = bStart; while (i < bEnd) { map.set(b[i], i++); } } // if it's a future node, hence it needs some handling if (map.has(a[aStart])) { // grab the index of such node, 'cause it might have been processed var index = map.get(a[aStart]); // if it's not already processed, look on demand for the next LCS if (bStart < index && index < bEnd) { var _i = aStart; // counts the amount of nodes that are the same in the future var sequence = 1; while (++_i < aEnd && _i < bEnd && map.get(a[_i]) === index + sequence) { sequence++; } // effort decision here: if the sequence is longer than replaces // needed to reach such sequence, which would brings again this loop // to the fast path, prepend the difference before a sequence, // and move only the future list index forward, so that aStart // and bStart will be aligned again, hence on the fast path. // An example considering aStart and bStart are both 0: // a: [1, 2, 3, 4] // b: [7, 1, 2, 3, 6] // this would place 7 before 1 and, from that time on, 1, 2, and 3 // will be processed at zero cost if (sequence > index - bStart) { var _node2 = get(a[aStart], 0); while (bStart < index) { parentNode.insertBefore(get(b[bStart++], 1), _node2); } } // if the effort wasn't good enough, fallback to a replace, // moving both source and target indexes forward, hoping that some // similar node will be found later on, to go back to the fast path else { parentNode.replaceChild(get(b[bStart++], 1), get(a[aStart++], -1)); } } // otherwise move the source forward, 'cause there's nothing to do else aStart++; } // this node has no meaning in the future list, so it's more than safe // to remove it, and check the next live node out instead, meaning // that only the live list index should be forwarded else parentNode.removeChild(get(a[aStart++], -1)); } } return b; }); var aria = function aria(node) { return function (values) { for (var key in values) { var name = key === 'role' ? key : "aria-".concat(key); var value = values[key]; if (value == null) node.removeAttribute(name);else node.setAttribute(name, value); } }; }; var attribute = function attribute(node, name) { var oldValue, orphan = true; var attributeNode = document.createAttributeNS(null, name); return function (newValue) { if (oldValue !== newValue) { oldValue = newValue; if (oldValue == null) { if (!orphan) { node.removeAttributeNode(attributeNode); orphan = true; } } else { attributeNode.value = newValue; if (orphan) { node.setAttributeNodeNS(attributeNode); orphan = false; } } } }; }; var _boolean = function _boolean(node, key, oldValue) { return function (newValue) { if (oldValue !== !!newValue) { // when IE won't be around anymore ... // node.toggleAttribute(key, oldValue = !!newValue); if (oldValue = !!newValue) node.setAttribute(key, '');else node.removeAttribute(key); } }; }; var data = function data(_ref) { var dataset = _ref.dataset; return function (values) { for (var key in values) { var value = values[key]; if (value == null) delete dataset[key];else dataset[key] = value; } }; }; var event = function event(node, name) { var oldValue, type = name.slice(2); if (!(name in node) && name.toLowerCase() in node) type = type.toLowerCase(); return function (newValue) { var info = isArray(newValue) ? newValue : [newValue, false]; if (oldValue !== info[0]) { if (oldValue) node.removeEventListener(type, oldValue, info[1]); if (oldValue = info[0]) node.addEventListener(type, oldValue, info[1]); } }; }; var ref = function ref(node) { var oldValue; return function (value) { if (oldValue !== value) { oldValue = value; if (typeof value === 'function') value(node);else value.current = node; } }; }; var setter = function setter(node, key) { return key === 'dataset' ? data(node) : function (value) { node[key] = value; }; }; var text = function text(node) { var oldValue; return function (newValue) { if (oldValue != newValue) { oldValue = newValue; node.textContent = newValue == null ? '' : newValue; } }; }; var reducePath = function reducePath(_ref, i) { var childNodes = _ref.childNodes; return childNodes[i]; }; // this helper avoid code bloat around handleAnything() callback var diff = function diff(comment, oldNodes, newNodes) { return udomdiff(comment.parentNode, // TODO: there is a possible edge case where a node has been // removed manually, or it was a keyed one, attached // to a shared reference between renders. // In this case udomdiff might fail at removing such node // as its parent won't be the expected one. // The best way to avoid this issue is to filter oldNodes // in search of those not live, or not in the current parent // anymore, but this would require both a change to uwire, // exposing a parentNode from the firstChild, as example, // but also a filter per each diff that should exclude nodes // that are not in there, penalizing performance quite a lot. // As this has been also a potential issue with domdiff, // and both lighterhtml and hyperHTML might fail with this // very specific edge case, I might as well document this possible // "diffing shenanigan" and call it a day. oldNodes, newNodes, diffable, comment); }; // if an interpolation represents a comment, the whole // diffing will be related to such comment. // This helper is in charge of understanding how the new // content for such interpolation/hole should be updated var handleAnything = function handleAnything(comment) { var oldValue, text, nodes = []; var anyContent = function anyContent(newValue) { switch (typeof(newValue)) { // primitives are handled as text content case 'string': case 'number': case 'boolean': if (oldValue !== newValue) { oldValue = newValue; if (!text) text = document.createTextNode(''); text.data = newValue; nodes = diff(comment, nodes, [text]); } break; // null, and undefined are used to cleanup previous content case 'object': case 'undefined': if (newValue == null) { if (oldValue != newValue) { oldValue = newValue; nodes = diff(comment, nodes, []); } break; } // arrays and nodes have a special treatment if (isArray(newValue)) { oldValue = newValue; // arrays can be used to cleanup, if empty if (newValue.length === 0) nodes = diff(comment, nodes, []); // or diffed, if these contains nodes or "wires" else if (typeof(newValue[0]) === 'object') nodes = diff(comment, nodes, newValue); // in all other cases the content is stringified as is else anyContent(String(newValue)); break; } // if the new value is a DOM node, or a wire, and it's // different from the one already live, then it's diffed. // if the node is a fragment, it's appended once via its childNodes // There is no `else` here, meaning if the content // is not expected one, nothing happens, as easy as that. if (oldValue !== newValue && 'ELEMENT_NODE' in newValue) { oldValue = newValue; nodes = diff(comment, nodes, newValue.nodeType === 11 ? slice.call(newValue.childNodes) : [newValue]); } break; case 'function': anyContent(newValue(comment)); break; } }; return anyContent; }; // attributes can be: // * ref=${...} for hooks and other purposes // * aria=${...} for aria attributes // * ?boolean=${...} for boolean attributes // * .dataset=${...} for dataset related attributes // * .setter=${...} for Custom Elements setters or nodes with setters // such as buttons, details, options, select, etc // * @event=${...} to explicitly handle event listeners // * onevent=${...} to automatically handle event listeners // * generic=${...} to handle an attribute just like an attribute var handleAttribute = function handleAttribute(node, name /*, svg*/ ) { switch (name[0]) { case '?': return _boolean(node, name.slice(1), false); case '.': return setter(node, name.slice(1)); case '@': return event(node, 'on' + name.slice(1)); case 'o': if (name[1] === 'n') return event(node, name); } switch (name) { case 'ref': return ref(node); case 'aria': return aria(node); } return attribute(node, name /*, svg*/ ); }; // each mapped update carries the update type and its path // the type is either node, attribute, or text, while // the path is how to retrieve the related node to update. // In the attribute case, the attribute name is also carried along. function handlers(options) { var type = options.type, path = options.path; var node = path.reduceRight(reducePath, this); return type === 'node' ? handleAnything(node) : type === 'attr' ? handleAttribute(node, options.name /*, options.svg*/ ) : text(node); } /*! (c) Andrea Giammarchi - ISC */ var createContent = function (document) { var FRAGMENT = 'fragment'; var TEMPLATE = 'template'; var HAS_CONTENT = ('content' in create(TEMPLATE)); var createHTML = HAS_CONTENT ? function (html) { var template = create(TEMPLATE); template.innerHTML = html; return template.content; } : function (html) { var content = create(FRAGMENT); var template = create(TEMPLATE); var childNodes = null; if (/^[^\S]*?<(col(?:group)?|t(?:head|body|foot|r|d|h))/i.test(html)) { var selector = RegExp.$1; template.innerHTML = '
${data}
` var mapTemplate = function mapTemplate(type, template) { var text = instrument(template, prefix, type === 'svg'); var content = createFragment(text, type); // once instrumented and reproduced as fragment, it's crawled // to find out where each update is in the fragment tree var tw = createIterator(content); var nodes = []; var length = template.length - 1; var i = 0; // updates are searched via unique names, linearly increased across the tree // var search = "".concat(prefix).concat(i); while (i < length) { var node = tw.nextNode(); // if not all updates are bound but there's nothing else to crawl // it means that there is something wrong with the template. if (!node) throw "bad template: ".concat(text); // if the current node is a comment, and it contains isµX // it means the update should take care of any content if (node.nodeType === 8) { // The only comments to be considered are those // which content is exactly the same as the searched one. if (node.data === search) { nodes.push({ type: 'node', path: createPath(node) }); search = "".concat(prefix).concat(++i); } } else { // if the node is not a comment, loop through all its attributes // named isµX and relate attribute updates to this node and the // attribute name, retrieved through node.getAttribute("isµX") // the isµX attribute will be removed as irrelevant for the layout // let svg = -1; while (node.hasAttribute(search)) { nodes.push({ type: 'attr', path: createPath(node), name: node.getAttribute(search) //svg: svg < 0 ? (svg = ('ownerSVGElement' in node ? 1 : 0)) : svg }); node.removeAttribute(search); search = "".concat(prefix).concat(++i); } // if the node was a style, textarea, or others, check its content // and if it is then update tex-only this node if (textOnly.test(node.tagName) && node.textContent.trim() === "")) { node.textContent = ''; nodes.push({ type: 'text', path: createPath(node) }); search = "".concat(prefix).concat(++i); } } } // once all nodes to update, or their attributes, are known, the content // will be cloned in the future to represent the template, and all updates // related to such content retrieved right away without needing to re-crawl // the exact same template, and its content, more than once. return { content: content, nodes: nodes }; }; // if a template is unknown, perform the previous mapping, otherwise grab // its details such as the fragment with all nodes, and updates info. var mapUpdates = function mapUpdates(type, template) { var _ref = cache$1.get(template) || cache$1.set(template, mapTemplate(type, template)), content = _ref.content, nodes = _ref.nodes; // clone deeply the fragment var fragment = document.importNode(content, true); // and relate an update handler per each node that needs one var updates = nodes.map(handlers, fragment); // return the fragment and all updates to use within its nodes return { content: fragment, updates: updates }; }; // as html and svg can be nested calls, but no parent node is known // until rendered somewhere, the unroll operation is needed to // discover what to do with each interpolation, which will result // into an update operation. var unroll = function unroll(info, _ref2) { var type = _ref2.type, template = _ref2.template, values = _ref2.values; var length = values.length; // interpolations can contain holes and arrays, so these need // to be recursively discovered unrollValues(info, values, length); var entry = info.entry; // if the cache entry is either null or different from the template // and the type this unroll should resolve, create a new entry // assigning a new content fragment and the list of updates. if (!entry || entry.template !== template || entry.type !== type) info.entry = entry = createEntry(type, template); var _entry = entry, content = _entry.content, updates = _entry.updates, wire = _entry.wire; // even if the fragment and its nodes is not live yet, // it is already possible to update via interpolations values. for (var i = 0; i < length; i++) { updates[i](values[i]); } // if the entry was new, or representing a different template or type, // create a new persistent entity to use during diffing. // This is simply a DOM node, when the template has a single container, // as in ``, or a "wire" in `` and similar cases. return wire || (entry.wire = persistent(content)); }; // the stack retains, per each interpolation value, the cache // related to each interpolation value, or null, if the render // was conditional and the value is not special (Array or Hole) var unrollValues = function unrollValues(_ref3, values, length) { var stack = _ref3.stack; for (var i = 0; i < length; i++) { var hole = values[i]; // each Hole gets unrolled and re-assigned as value // so that domdiff will deal with a node/wire, not with a hole if (hole instanceof Hole) values[i] = unroll(stack[i] || (stack[i] = createCache()), hole); // arrays are recursively resolved so that each entry will contain // also a DOM node or a wire, hence it can be diffed if/when needed else if (isArray(hole)) unrollValues(stack[i] || (stack[i] = createCache()), hole, hole.length); // if the value is nothing special, the stack doesn't need to retain data // this is useful also to cleanup previously retained data, if the value // was a Hole, or an Array, but not anymore, i.e.: // const update = content => html`