├── .editorconfig ├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── SECURITY.md ├── dist ├── index.js ├── index.js.map ├── jails.js └── jails.js.map ├── html.mjs ├── package.json ├── readme.md ├── src ├── component.ts ├── element.ts ├── index.ts ├── template-system.ts └── utils │ ├── index.ts │ └── pubsub.ts ├── tsconfig.json ├── types.d.ts ├── vite-env.d.ts └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | [*] 3 | indent_style = tab 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | 9 | [*.md] 10 | trim_trailing_whitespace = false 11 | indent_style = space 12 | tab_width = 2 13 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | patreon: jails 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | files to ignore 2 | *~ 3 | *.lock 4 | *.DS_Store 5 | *.swp 6 | *.out 7 | node_modules/ 8 | *.cache 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Eduardo Ottaviani Aragão 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | var __defProp = Object.defineProperty; 2 | var __getOwnPropSymbols = Object.getOwnPropertySymbols; 3 | var __hasOwnProp = Object.prototype.hasOwnProperty; 4 | var __propIsEnum = Object.prototype.propertyIsEnumerable; 5 | var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; 6 | var __spreadValues = (a, b) => { 7 | for (var prop in b || (b = {})) 8 | if (__hasOwnProp.call(b, prop)) 9 | __defNormalProp(a, prop, b[prop]); 10 | if (__getOwnPropSymbols) 11 | for (var prop of __getOwnPropSymbols(b)) { 12 | if (__propIsEnum.call(b, prop)) 13 | __defNormalProp(a, prop, b[prop]); 14 | } 15 | return a; 16 | }; 17 | let textarea; 18 | const g = { 19 | scope: {} 20 | }; 21 | const decodeHTML = (text) => { 22 | textarea = textarea || document.createElement("textarea"); 23 | textarea.innerHTML = text; 24 | return textarea.value; 25 | }; 26 | const uuid = () => { 27 | return Math.random().toString(36).substring(2, 9); 28 | }; 29 | const dup = (o) => { 30 | return JSON.parse(JSON.stringify(o)); 31 | }; 32 | const safe = (execute, val) => { 33 | try { 34 | return execute(); 35 | } catch (err) { 36 | return val || ""; 37 | } 38 | }; 39 | var Idiomorph = function() { 40 | const noOp = () => { 41 | }; 42 | const defaults = { 43 | morphStyle: "outerHTML", 44 | callbacks: { 45 | beforeNodeAdded: noOp, 46 | afterNodeAdded: noOp, 47 | beforeNodeMorphed: noOp, 48 | afterNodeMorphed: noOp, 49 | beforeNodeRemoved: noOp, 50 | afterNodeRemoved: noOp, 51 | beforeAttributeUpdated: noOp 52 | }, 53 | head: { 54 | style: "merge", 55 | shouldPreserve: (elt) => elt.getAttribute("im-preserve") === "true", 56 | shouldReAppend: (elt) => elt.getAttribute("im-re-append") === "true", 57 | shouldRemove: noOp, 58 | afterHeadMorphed: noOp 59 | }, 60 | restoreFocus: true 61 | }; 62 | function morph(oldNode, newContent, config2 = {}) { 63 | oldNode = normalizeElement(oldNode); 64 | const newNode = normalizeParent(newContent); 65 | const ctx = createMorphContext(oldNode, newNode, config2); 66 | const morphedNodes = saveAndRestoreFocus(ctx, () => { 67 | return withHeadBlocking( 68 | ctx, 69 | oldNode, 70 | newNode, 71 | /** @param {MorphContext} ctx */ 72 | (ctx2) => { 73 | if (ctx2.morphStyle === "innerHTML") { 74 | morphChildren(ctx2, oldNode, newNode); 75 | return Array.from(oldNode.childNodes); 76 | } else { 77 | return morphOuterHTML(ctx2, oldNode, newNode); 78 | } 79 | } 80 | ); 81 | }); 82 | ctx.pantry.remove(); 83 | return morphedNodes; 84 | } 85 | function morphOuterHTML(ctx, oldNode, newNode) { 86 | const oldParent = normalizeParent(oldNode); 87 | let childNodes = Array.from(oldParent.childNodes); 88 | const index = childNodes.indexOf(oldNode); 89 | const rightMargin = childNodes.length - (index + 1); 90 | morphChildren( 91 | ctx, 92 | oldParent, 93 | newNode, 94 | // these two optional params are the secret sauce 95 | oldNode, 96 | // start point for iteration 97 | oldNode.nextSibling 98 | // end point for iteration 99 | ); 100 | childNodes = Array.from(oldParent.childNodes); 101 | return childNodes.slice(index, childNodes.length - rightMargin); 102 | } 103 | function saveAndRestoreFocus(ctx, fn) { 104 | var _a; 105 | if (!ctx.config.restoreFocus) return fn(); 106 | let activeElement = ( 107 | /** @type {HTMLInputElement|HTMLTextAreaElement|null} */ 108 | document.activeElement 109 | ); 110 | if (!(activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement)) { 111 | return fn(); 112 | } 113 | const { id: activeElementId, selectionStart, selectionEnd } = activeElement; 114 | const results = fn(); 115 | if (activeElementId && activeElementId !== ((_a = document.activeElement) == null ? void 0 : _a.id)) { 116 | activeElement = ctx.target.querySelector(`#${activeElementId}`); 117 | activeElement == null ? void 0 : activeElement.focus(); 118 | } 119 | if (activeElement && !activeElement.selectionEnd && selectionEnd) { 120 | activeElement.setSelectionRange(selectionStart, selectionEnd); 121 | } 122 | return results; 123 | } 124 | const morphChildren = /* @__PURE__ */ function() { 125 | function morphChildren2(ctx, oldParent, newParent, insertionPoint = null, endPoint = null) { 126 | if (oldParent instanceof HTMLTemplateElement && newParent instanceof HTMLTemplateElement) { 127 | oldParent = oldParent.content; 128 | newParent = newParent.content; 129 | } 130 | insertionPoint || (insertionPoint = oldParent.firstChild); 131 | for (const newChild of newParent.childNodes) { 132 | if (insertionPoint && insertionPoint != endPoint) { 133 | const bestMatch = findBestMatch( 134 | ctx, 135 | newChild, 136 | insertionPoint, 137 | endPoint 138 | ); 139 | if (bestMatch) { 140 | if (bestMatch !== insertionPoint) { 141 | removeNodesBetween(ctx, insertionPoint, bestMatch); 142 | } 143 | morphNode(bestMatch, newChild, ctx); 144 | insertionPoint = bestMatch.nextSibling; 145 | continue; 146 | } 147 | } 148 | if (newChild instanceof Element && ctx.persistentIds.has(newChild.id)) { 149 | const movedChild = moveBeforeById( 150 | oldParent, 151 | newChild.id, 152 | insertionPoint, 153 | ctx 154 | ); 155 | morphNode(movedChild, newChild, ctx); 156 | insertionPoint = movedChild.nextSibling; 157 | continue; 158 | } 159 | const insertedNode = createNode( 160 | oldParent, 161 | newChild, 162 | insertionPoint, 163 | ctx 164 | ); 165 | if (insertedNode) { 166 | insertionPoint = insertedNode.nextSibling; 167 | } 168 | } 169 | while (insertionPoint && insertionPoint != endPoint) { 170 | const tempNode = insertionPoint; 171 | insertionPoint = insertionPoint.nextSibling; 172 | removeNode(ctx, tempNode); 173 | } 174 | } 175 | function createNode(oldParent, newChild, insertionPoint, ctx) { 176 | if (ctx.callbacks.beforeNodeAdded(newChild) === false) return null; 177 | if (ctx.idMap.has(newChild)) { 178 | const newEmptyChild = document.createElement( 179 | /** @type {Element} */ 180 | newChild.tagName 181 | ); 182 | oldParent.insertBefore(newEmptyChild, insertionPoint); 183 | morphNode(newEmptyChild, newChild, ctx); 184 | ctx.callbacks.afterNodeAdded(newEmptyChild); 185 | return newEmptyChild; 186 | } else { 187 | const newClonedChild = document.importNode(newChild, true); 188 | oldParent.insertBefore(newClonedChild, insertionPoint); 189 | ctx.callbacks.afterNodeAdded(newClonedChild); 190 | return newClonedChild; 191 | } 192 | } 193 | const findBestMatch = /* @__PURE__ */ function() { 194 | function findBestMatch2(ctx, node, startPoint, endPoint) { 195 | let softMatch = null; 196 | let nextSibling = node.nextSibling; 197 | let siblingSoftMatchCount = 0; 198 | let cursor = startPoint; 199 | while (cursor && cursor != endPoint) { 200 | if (isSoftMatch(cursor, node)) { 201 | if (isIdSetMatch(ctx, cursor, node)) { 202 | return cursor; 203 | } 204 | if (softMatch === null) { 205 | if (!ctx.idMap.has(cursor)) { 206 | softMatch = cursor; 207 | } 208 | } 209 | } 210 | if (softMatch === null && nextSibling && isSoftMatch(cursor, nextSibling)) { 211 | siblingSoftMatchCount++; 212 | nextSibling = nextSibling.nextSibling; 213 | if (siblingSoftMatchCount >= 2) { 214 | softMatch = void 0; 215 | } 216 | } 217 | if (cursor.contains(document.activeElement)) break; 218 | cursor = cursor.nextSibling; 219 | } 220 | return softMatch || null; 221 | } 222 | function isIdSetMatch(ctx, oldNode, newNode) { 223 | let oldSet = ctx.idMap.get(oldNode); 224 | let newSet = ctx.idMap.get(newNode); 225 | if (!newSet || !oldSet) return false; 226 | for (const id of oldSet) { 227 | if (newSet.has(id)) { 228 | return true; 229 | } 230 | } 231 | return false; 232 | } 233 | function isSoftMatch(oldNode, newNode) { 234 | const oldElt = ( 235 | /** @type {Element} */ 236 | oldNode 237 | ); 238 | const newElt = ( 239 | /** @type {Element} */ 240 | newNode 241 | ); 242 | return oldElt.nodeType === newElt.nodeType && oldElt.tagName === newElt.tagName && // If oldElt has an `id` with possible state and it doesn't match newElt.id then avoid morphing. 243 | // We'll still match an anonymous node with an IDed newElt, though, because if it got this far, 244 | // its not persistent, and new nodes can't have any hidden state. 245 | (!oldElt.id || oldElt.id === newElt.id); 246 | } 247 | return findBestMatch2; 248 | }(); 249 | function removeNode(ctx, node) { 250 | var _a; 251 | if (ctx.idMap.has(node)) { 252 | moveBefore(ctx.pantry, node, null); 253 | } else { 254 | if (ctx.callbacks.beforeNodeRemoved(node) === false) return; 255 | (_a = node.parentNode) == null ? void 0 : _a.removeChild(node); 256 | ctx.callbacks.afterNodeRemoved(node); 257 | } 258 | } 259 | function removeNodesBetween(ctx, startInclusive, endExclusive) { 260 | let cursor = startInclusive; 261 | while (cursor && cursor !== endExclusive) { 262 | let tempNode = ( 263 | /** @type {Node} */ 264 | cursor 265 | ); 266 | cursor = cursor.nextSibling; 267 | removeNode(ctx, tempNode); 268 | } 269 | return cursor; 270 | } 271 | function moveBeforeById(parentNode, id, after, ctx) { 272 | const target = ( 273 | /** @type {Element} - will always be found */ 274 | ctx.target.querySelector(`#${id}`) || ctx.pantry.querySelector(`#${id}`) 275 | ); 276 | removeElementFromAncestorsIdMaps(target, ctx); 277 | moveBefore(parentNode, target, after); 278 | return target; 279 | } 280 | function removeElementFromAncestorsIdMaps(element, ctx) { 281 | const id = element.id; 282 | while (element = element.parentNode) { 283 | let idSet = ctx.idMap.get(element); 284 | if (idSet) { 285 | idSet.delete(id); 286 | if (!idSet.size) { 287 | ctx.idMap.delete(element); 288 | } 289 | } 290 | } 291 | } 292 | function moveBefore(parentNode, element, after) { 293 | if (parentNode.moveBefore) { 294 | try { 295 | parentNode.moveBefore(element, after); 296 | } catch (e) { 297 | parentNode.insertBefore(element, after); 298 | } 299 | } else { 300 | parentNode.insertBefore(element, after); 301 | } 302 | } 303 | return morphChildren2; 304 | }(); 305 | const morphNode = /* @__PURE__ */ function() { 306 | function morphNode2(oldNode, newContent, ctx) { 307 | if (ctx.ignoreActive && oldNode === document.activeElement) { 308 | return null; 309 | } 310 | if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) { 311 | return oldNode; 312 | } 313 | if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) ; 314 | else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") { 315 | handleHeadElement( 316 | oldNode, 317 | /** @type {HTMLHeadElement} */ 318 | newContent, 319 | ctx 320 | ); 321 | } else { 322 | morphAttributes(oldNode, newContent, ctx); 323 | if (!ignoreValueOfActiveElement(oldNode, ctx)) { 324 | morphChildren(ctx, oldNode, newContent); 325 | } 326 | } 327 | ctx.callbacks.afterNodeMorphed(oldNode, newContent); 328 | return oldNode; 329 | } 330 | function morphAttributes(oldNode, newNode, ctx) { 331 | let type = newNode.nodeType; 332 | if (type === 1) { 333 | const oldElt = ( 334 | /** @type {Element} */ 335 | oldNode 336 | ); 337 | const newElt = ( 338 | /** @type {Element} */ 339 | newNode 340 | ); 341 | const oldAttributes = oldElt.attributes; 342 | const newAttributes = newElt.attributes; 343 | for (const newAttribute of newAttributes) { 344 | if (ignoreAttribute(newAttribute.name, oldElt, "update", ctx)) { 345 | continue; 346 | } 347 | if (oldElt.getAttribute(newAttribute.name) !== newAttribute.value) { 348 | oldElt.setAttribute(newAttribute.name, newAttribute.value); 349 | } 350 | } 351 | for (let i = oldAttributes.length - 1; 0 <= i; i--) { 352 | const oldAttribute = oldAttributes[i]; 353 | if (!oldAttribute) continue; 354 | if (!newElt.hasAttribute(oldAttribute.name)) { 355 | if (ignoreAttribute(oldAttribute.name, oldElt, "remove", ctx)) { 356 | continue; 357 | } 358 | oldElt.removeAttribute(oldAttribute.name); 359 | } 360 | } 361 | if (!ignoreValueOfActiveElement(oldElt, ctx)) { 362 | syncInputValue(oldElt, newElt, ctx); 363 | } 364 | } 365 | if (type === 8 || type === 3) { 366 | if (oldNode.nodeValue !== newNode.nodeValue) { 367 | oldNode.nodeValue = newNode.nodeValue; 368 | } 369 | } 370 | } 371 | function syncInputValue(oldElement, newElement, ctx) { 372 | if (oldElement instanceof HTMLInputElement && newElement instanceof HTMLInputElement && newElement.type !== "file") { 373 | let newValue = newElement.value; 374 | let oldValue = oldElement.value; 375 | syncBooleanAttribute(oldElement, newElement, "checked", ctx); 376 | syncBooleanAttribute(oldElement, newElement, "disabled", ctx); 377 | if (!newElement.hasAttribute("value")) { 378 | if (!ignoreAttribute("value", oldElement, "remove", ctx)) { 379 | oldElement.value = ""; 380 | oldElement.removeAttribute("value"); 381 | } 382 | } else if (oldValue !== newValue) { 383 | if (!ignoreAttribute("value", oldElement, "update", ctx)) { 384 | oldElement.setAttribute("value", newValue); 385 | oldElement.value = newValue; 386 | } 387 | } 388 | } else if (oldElement instanceof HTMLOptionElement && newElement instanceof HTMLOptionElement) { 389 | syncBooleanAttribute(oldElement, newElement, "selected", ctx); 390 | } else if (oldElement instanceof HTMLTextAreaElement && newElement instanceof HTMLTextAreaElement) { 391 | let newValue = newElement.value; 392 | let oldValue = oldElement.value; 393 | if (ignoreAttribute("value", oldElement, "update", ctx)) { 394 | return; 395 | } 396 | if (newValue !== oldValue) { 397 | oldElement.value = newValue; 398 | } 399 | if (oldElement.firstChild && oldElement.firstChild.nodeValue !== newValue) { 400 | oldElement.firstChild.nodeValue = newValue; 401 | } 402 | } 403 | } 404 | function syncBooleanAttribute(oldElement, newElement, attributeName, ctx) { 405 | const newLiveValue = newElement[attributeName], oldLiveValue = oldElement[attributeName]; 406 | if (newLiveValue !== oldLiveValue) { 407 | const ignoreUpdate = ignoreAttribute( 408 | attributeName, 409 | oldElement, 410 | "update", 411 | ctx 412 | ); 413 | if (!ignoreUpdate) { 414 | oldElement[attributeName] = newElement[attributeName]; 415 | } 416 | if (newLiveValue) { 417 | if (!ignoreUpdate) { 418 | oldElement.setAttribute(attributeName, ""); 419 | } 420 | } else { 421 | if (!ignoreAttribute(attributeName, oldElement, "remove", ctx)) { 422 | oldElement.removeAttribute(attributeName); 423 | } 424 | } 425 | } 426 | } 427 | function ignoreAttribute(attr, element, updateType, ctx) { 428 | if (attr === "value" && ctx.ignoreActiveValue && element === document.activeElement) { 429 | return true; 430 | } 431 | return ctx.callbacks.beforeAttributeUpdated(attr, element, updateType) === false; 432 | } 433 | function ignoreValueOfActiveElement(possibleActiveElement, ctx) { 434 | return !!ctx.ignoreActiveValue && possibleActiveElement === document.activeElement && possibleActiveElement !== document.body; 435 | } 436 | return morphNode2; 437 | }(); 438 | function withHeadBlocking(ctx, oldNode, newNode, callback) { 439 | if (ctx.head.block) { 440 | const oldHead = oldNode.querySelector("head"); 441 | const newHead = newNode.querySelector("head"); 442 | if (oldHead && newHead) { 443 | const promises = handleHeadElement(oldHead, newHead, ctx); 444 | return Promise.all(promises).then(() => { 445 | const newCtx = Object.assign(ctx, { 446 | head: { 447 | block: false, 448 | ignore: true 449 | } 450 | }); 451 | return callback(newCtx); 452 | }); 453 | } 454 | } 455 | return callback(ctx); 456 | } 457 | function handleHeadElement(oldHead, newHead, ctx) { 458 | let added = []; 459 | let removed = []; 460 | let preserved = []; 461 | let nodesToAppend = []; 462 | let srcToNewHeadNodes = /* @__PURE__ */ new Map(); 463 | for (const newHeadChild of newHead.children) { 464 | srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild); 465 | } 466 | for (const currentHeadElt of oldHead.children) { 467 | let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML); 468 | let isReAppended = ctx.head.shouldReAppend(currentHeadElt); 469 | let isPreserved = ctx.head.shouldPreserve(currentHeadElt); 470 | if (inNewContent || isPreserved) { 471 | if (isReAppended) { 472 | removed.push(currentHeadElt); 473 | } else { 474 | srcToNewHeadNodes.delete(currentHeadElt.outerHTML); 475 | preserved.push(currentHeadElt); 476 | } 477 | } else { 478 | if (ctx.head.style === "append") { 479 | if (isReAppended) { 480 | removed.push(currentHeadElt); 481 | nodesToAppend.push(currentHeadElt); 482 | } 483 | } else { 484 | if (ctx.head.shouldRemove(currentHeadElt) !== false) { 485 | removed.push(currentHeadElt); 486 | } 487 | } 488 | } 489 | } 490 | nodesToAppend.push(...srcToNewHeadNodes.values()); 491 | let promises = []; 492 | for (const newNode of nodesToAppend) { 493 | let newElt = ( 494 | /** @type {ChildNode} */ 495 | document.createRange().createContextualFragment(newNode.outerHTML).firstChild 496 | ); 497 | if (ctx.callbacks.beforeNodeAdded(newElt) !== false) { 498 | if ("href" in newElt && newElt.href || "src" in newElt && newElt.src) { 499 | let resolve; 500 | let promise = new Promise(function(_resolve) { 501 | resolve = _resolve; 502 | }); 503 | newElt.addEventListener("load", function() { 504 | resolve(); 505 | }); 506 | promises.push(promise); 507 | } 508 | oldHead.appendChild(newElt); 509 | ctx.callbacks.afterNodeAdded(newElt); 510 | added.push(newElt); 511 | } 512 | } 513 | for (const removedElement of removed) { 514 | if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) { 515 | oldHead.removeChild(removedElement); 516 | ctx.callbacks.afterNodeRemoved(removedElement); 517 | } 518 | } 519 | ctx.head.afterHeadMorphed(oldHead, { 520 | added, 521 | kept: preserved, 522 | removed 523 | }); 524 | return promises; 525 | } 526 | const createMorphContext = /* @__PURE__ */ function() { 527 | function createMorphContext2(oldNode, newContent, config2) { 528 | const { persistentIds, idMap } = createIdMaps(oldNode, newContent); 529 | const mergedConfig = mergeDefaults(config2); 530 | const morphStyle = mergedConfig.morphStyle || "outerHTML"; 531 | if (!["innerHTML", "outerHTML"].includes(morphStyle)) { 532 | throw `Do not understand how to morph style ${morphStyle}`; 533 | } 534 | return { 535 | target: oldNode, 536 | newContent, 537 | config: mergedConfig, 538 | morphStyle, 539 | ignoreActive: mergedConfig.ignoreActive, 540 | ignoreActiveValue: mergedConfig.ignoreActiveValue, 541 | restoreFocus: mergedConfig.restoreFocus, 542 | idMap, 543 | persistentIds, 544 | pantry: createPantry(), 545 | callbacks: mergedConfig.callbacks, 546 | head: mergedConfig.head 547 | }; 548 | } 549 | function mergeDefaults(config2) { 550 | let finalConfig = Object.assign({}, defaults); 551 | Object.assign(finalConfig, config2); 552 | finalConfig.callbacks = Object.assign( 553 | {}, 554 | defaults.callbacks, 555 | config2.callbacks 556 | ); 557 | finalConfig.head = Object.assign({}, defaults.head, config2.head); 558 | return finalConfig; 559 | } 560 | function createPantry() { 561 | const pantry = document.createElement("div"); 562 | pantry.hidden = true; 563 | document.body.insertAdjacentElement("afterend", pantry); 564 | return pantry; 565 | } 566 | function findIdElements(root) { 567 | let elements = Array.from(root.querySelectorAll("[id]")); 568 | if (root.id) { 569 | elements.push(root); 570 | } 571 | return elements; 572 | } 573 | function populateIdMapWithTree(idMap, persistentIds, root, elements) { 574 | for (const elt of elements) { 575 | if (persistentIds.has(elt.id)) { 576 | let current = elt; 577 | while (current) { 578 | let idSet = idMap.get(current); 579 | if (idSet == null) { 580 | idSet = /* @__PURE__ */ new Set(); 581 | idMap.set(current, idSet); 582 | } 583 | idSet.add(elt.id); 584 | if (current === root) break; 585 | current = current.parentElement; 586 | } 587 | } 588 | } 589 | } 590 | function createIdMaps(oldContent, newContent) { 591 | const oldIdElements = findIdElements(oldContent); 592 | const newIdElements = findIdElements(newContent); 593 | const persistentIds = createPersistentIds(oldIdElements, newIdElements); 594 | let idMap = /* @__PURE__ */ new Map(); 595 | populateIdMapWithTree(idMap, persistentIds, oldContent, oldIdElements); 596 | const newRoot = newContent.__idiomorphRoot || newContent; 597 | populateIdMapWithTree(idMap, persistentIds, newRoot, newIdElements); 598 | return { persistentIds, idMap }; 599 | } 600 | function createPersistentIds(oldIdElements, newIdElements) { 601 | let duplicateIds = /* @__PURE__ */ new Set(); 602 | let oldIdTagNameMap = /* @__PURE__ */ new Map(); 603 | for (const { id, tagName } of oldIdElements) { 604 | if (oldIdTagNameMap.has(id)) { 605 | duplicateIds.add(id); 606 | } else { 607 | oldIdTagNameMap.set(id, tagName); 608 | } 609 | } 610 | let persistentIds = /* @__PURE__ */ new Set(); 611 | for (const { id, tagName } of newIdElements) { 612 | if (persistentIds.has(id)) { 613 | duplicateIds.add(id); 614 | } else if (oldIdTagNameMap.get(id) === tagName) { 615 | persistentIds.add(id); 616 | } 617 | } 618 | for (const id of duplicateIds) { 619 | persistentIds.delete(id); 620 | } 621 | return persistentIds; 622 | } 623 | return createMorphContext2; 624 | }(); 625 | const { normalizeElement, normalizeParent } = /* @__PURE__ */ function() { 626 | const generatedByIdiomorph = /* @__PURE__ */ new WeakSet(); 627 | function normalizeElement2(content) { 628 | if (content instanceof Document) { 629 | return content.documentElement; 630 | } else { 631 | return content; 632 | } 633 | } 634 | function normalizeParent2(newContent) { 635 | if (newContent == null) { 636 | return document.createElement("div"); 637 | } else if (typeof newContent === "string") { 638 | return normalizeParent2(parseContent(newContent)); 639 | } else if (generatedByIdiomorph.has( 640 | /** @type {Element} */ 641 | newContent 642 | )) { 643 | return ( 644 | /** @type {Element} */ 645 | newContent 646 | ); 647 | } else if (newContent instanceof Node) { 648 | if (newContent.parentNode) { 649 | return createDuckTypedParent(newContent); 650 | } else { 651 | const dummyParent = document.createElement("div"); 652 | dummyParent.append(newContent); 653 | return dummyParent; 654 | } 655 | } else { 656 | const dummyParent = document.createElement("div"); 657 | for (const elt of [...newContent]) { 658 | dummyParent.append(elt); 659 | } 660 | return dummyParent; 661 | } 662 | } 663 | function createDuckTypedParent(newContent) { 664 | return ( 665 | /** @type {Element} */ 666 | /** @type {unknown} */ 667 | { 668 | childNodes: [newContent], 669 | /** @ts-ignore - cover your eyes for a minute, tsc */ 670 | querySelectorAll: (s) => { 671 | const elements = newContent.querySelectorAll(s); 672 | return newContent.matches(s) ? [newContent, ...elements] : elements; 673 | }, 674 | /** @ts-ignore */ 675 | insertBefore: (n, r) => newContent.parentNode.insertBefore(n, r), 676 | /** @ts-ignore */ 677 | moveBefore: (n, r) => newContent.parentNode.moveBefore(n, r), 678 | // for later use with populateIdMapWithTree to halt upwards iteration 679 | get __idiomorphRoot() { 680 | return newContent; 681 | } 682 | } 683 | ); 684 | } 685 | function parseContent(newContent) { 686 | let parser = new DOMParser(); 687 | let contentWithSvgsRemoved = newContent.replace( 688 | /]*>|>)([\s\S]*?)<\/svg>/gim, 689 | "" 690 | ); 691 | if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) { 692 | let content = parser.parseFromString(newContent, "text/html"); 693 | if (contentWithSvgsRemoved.match(/<\/html>/)) { 694 | generatedByIdiomorph.add(content); 695 | return content; 696 | } else { 697 | let htmlElement = content.firstChild; 698 | if (htmlElement) { 699 | generatedByIdiomorph.add(htmlElement); 700 | } 701 | return htmlElement; 702 | } 703 | } else { 704 | let responseDoc = parser.parseFromString( 705 | "", 706 | "text/html" 707 | ); 708 | let content = ( 709 | /** @type {HTMLTemplateElement} */ 710 | responseDoc.body.querySelector("template").content 711 | ); 712 | generatedByIdiomorph.add(content); 713 | return content; 714 | } 715 | } 716 | return { normalizeElement: normalizeElement2, normalizeParent: normalizeParent2 }; 717 | }(); 718 | return { 719 | morph, 720 | defaults 721 | }; 722 | }(); 723 | const topics = {}; 724 | const _async = {}; 725 | const publish = (name, params) => { 726 | _async[name] = Object.assign({}, _async[name], params); 727 | if (topics[name]) 728 | topics[name].forEach((topic) => topic(params)); 729 | }; 730 | const subscribe = (name, method) => { 731 | topics[name] = topics[name] || []; 732 | topics[name].push(method); 733 | if (name in _async) { 734 | method(_async[name]); 735 | } 736 | return () => { 737 | topics[name] = topics[name].filter((fn) => fn != method); 738 | }; 739 | }; 740 | const Component = ({ name, module, dependencies, node, templates: templates2, signal, register: register2 }) => { 741 | var _a; 742 | let tick; 743 | let preserve = []; 744 | const _model = module.model || {}; 745 | const initialState = new Function(`return ${node.getAttribute("html-model") || "{}"}`)(); 746 | const tplid = node.getAttribute("tplid"); 747 | const scopeid = node.getAttribute("html-scopeid"); 748 | const tpl = templates2[tplid]; 749 | const scope = g.scope[scopeid]; 750 | const model = dup(((_a = module == null ? void 0 : module.model) == null ? void 0 : _a.apply) ? _model({ elm: node, initialState }) : _model); 751 | const state = Object.assign({}, scope, model, initialState); 752 | const view = module.view ? module.view : (data) => data; 753 | const base = { 754 | name, 755 | model, 756 | elm: node, 757 | template: tpl.template, 758 | dependencies, 759 | publish, 760 | subscribe, 761 | main(fn) { 762 | node.addEventListener(":mount", fn); 763 | }, 764 | /** 765 | * @State 766 | */ 767 | state: { 768 | protected(list) { 769 | if (list) { 770 | preserve = list; 771 | } else { 772 | return preserve; 773 | } 774 | }, 775 | save(data) { 776 | if (data.constructor === Function) { 777 | data(state); 778 | } else { 779 | Object.assign(state, data); 780 | } 781 | }, 782 | set(data) { 783 | if (!document.body.contains(node)) { 784 | return; 785 | } 786 | if (data.constructor === Function) { 787 | data(state); 788 | } else { 789 | Object.assign(state, data); 790 | } 791 | const newstate = Object.assign({}, state, scope); 792 | return new Promise((resolve) => { 793 | render(newstate, () => resolve(newstate)); 794 | }); 795 | }, 796 | get() { 797 | return Object.assign({}, state); 798 | } 799 | }, 800 | dataset(target, name2) { 801 | const el = name2 ? target : node; 802 | const key = name2 ? name2 : target; 803 | const value = el.dataset[key]; 804 | if (value === "true") return true; 805 | if (value === "false") return false; 806 | if (!isNaN(value) && value.trim() !== "") return Number(value); 807 | try { 808 | return new Function("return (" + value + ")")(); 809 | } catch (e) { 810 | } 811 | try { 812 | return JSON.parse(value); 813 | } catch (e) { 814 | } 815 | return value; 816 | }, 817 | attr(target) { 818 | let callbacks = []; 819 | const elm = target || node; 820 | const observer = new MutationObserver((mutationsList) => { 821 | for (const mutation of mutationsList) { 822 | if (mutation.type === "attributes") { 823 | const attributeName = mutation.attributeName; 824 | callbacks.forEach((item) => { 825 | if (item.name == attributeName) { 826 | item.callback( 827 | attributeName, 828 | elm.getAttribute(attributeName) 829 | ); 830 | } 831 | }); 832 | } 833 | } 834 | }); 835 | observer.observe(node, { attributes: true }); 836 | node.addEventListener(":unmount", () => { 837 | callbacks = null; 838 | observer.disconnect(); 839 | }); 840 | return { 841 | change(name2, callback) { 842 | callbacks.push({ name: name2, callback }); 843 | }, 844 | disconnect(callback) { 845 | callbacks = callbacks.filter((item) => item.callback !== callback); 846 | } 847 | }; 848 | }, 849 | /** 850 | * @Events 851 | */ 852 | on(ev, selectorOrCallback, callback) { 853 | if (callback) { 854 | callback.handler = (e) => { 855 | const detail = e.detail || {}; 856 | let parent = e.target; 857 | while (parent) { 858 | if (parent.matches(selectorOrCallback)) { 859 | e.delegateTarget = parent; 860 | callback.apply(node, [e].concat(detail.args)); 861 | } 862 | if (parent === node) break; 863 | parent = parent.parentNode; 864 | } 865 | }; 866 | node.addEventListener(ev, callback.handler, { 867 | signal, 868 | capture: ev == "focus" || ev == "blur" || ev == "mouseenter" || ev == "mouseleave" 869 | }); 870 | } else { 871 | selectorOrCallback.handler = (e) => { 872 | e.delegateTarget = node; 873 | selectorOrCallback.apply(node, [e].concat(e.detail.args)); 874 | }; 875 | node.addEventListener(ev, selectorOrCallback.handler, { signal }); 876 | } 877 | }, 878 | off(ev, callback) { 879 | if (callback.handler) { 880 | node.removeEventListener(ev, callback.handler); 881 | } 882 | }, 883 | trigger(ev, selectorOrCallback, data) { 884 | if (selectorOrCallback.constructor === String) { 885 | Array.from(node.querySelectorAll(selectorOrCallback)).forEach((children) => { 886 | children.dispatchEvent(new CustomEvent(ev, { bubbles: true, detail: { args: data } })); 887 | }); 888 | } else { 889 | node.dispatchEvent(new CustomEvent(ev, { bubbles: true, detail: { args: data } })); 890 | } 891 | }, 892 | emit(ev, data) { 893 | node.dispatchEvent(new CustomEvent(ev, { bubbles: true, detail: { args: data } })); 894 | }, 895 | unmount(fn) { 896 | node.addEventListener(":unmount", fn); 897 | }, 898 | innerHTML(target, html_) { 899 | const element = html_ ? target : node; 900 | const clone = element.cloneNode(); 901 | const html = html_ ? html_ : target; 902 | clone.innerHTML = html; 903 | Idiomorph.morph(element, clone); 904 | } 905 | }; 906 | const render = (data, callback = () => { 907 | }) => { 908 | clearTimeout(tick); 909 | tick = setTimeout(() => { 910 | const html = tpl.render.call(__spreadValues(__spreadValues({}, data), view(data)), node, safe, g); 911 | Idiomorph.morph(node, html, IdiomorphOptions(node, register2)); 912 | Promise.resolve().then(() => { 913 | node.querySelectorAll("[tplid]").forEach((element) => { 914 | const child = register2.get(element); 915 | if (!child) return; 916 | child.state.protected().forEach((key) => delete data[key]); 917 | child.state.set(data); 918 | }); 919 | Promise.resolve().then(() => { 920 | g.scope = {}; 921 | callback(); 922 | }); 923 | }); 924 | }); 925 | }; 926 | render(state); 927 | register2.set(node, base); 928 | return module.default(base); 929 | }; 930 | const IdiomorphOptions = (parent, register2) => ({ 931 | callbacks: { 932 | beforeNodeMorphed(node) { 933 | if (node.nodeType === 1) { 934 | if ("html-static" in node.attributes) { 935 | return false; 936 | } 937 | if (register2.get(node) && node !== parent) { 938 | return false; 939 | } 940 | } 941 | } 942 | } 943 | }); 944 | const register$1 = /* @__PURE__ */ new WeakMap(); 945 | const Element$1 = ({ component, templates: templates2, start: start2 }) => { 946 | const { name, module, dependencies } = component; 947 | return class extends HTMLElement { 948 | constructor() { 949 | super(); 950 | } 951 | connectedCallback() { 952 | this.abortController = new AbortController(); 953 | if (!this.getAttribute("tplid")) { 954 | start2(this.parentNode); 955 | } 956 | const rtrn = Component({ 957 | node: this, 958 | name, 959 | module, 960 | dependencies, 961 | templates: templates2, 962 | signal: this.abortController.signal, 963 | register: register$1 964 | }); 965 | if (rtrn && rtrn.constructor === Promise) { 966 | rtrn.then(() => { 967 | this.dispatchEvent(new CustomEvent(":mount")); 968 | }); 969 | } else { 970 | this.dispatchEvent(new CustomEvent(":mount")); 971 | } 972 | } 973 | disconnectedCallback() { 974 | this.dispatchEvent(new CustomEvent(":unmount")); 975 | this.abortController.abort(); 976 | } 977 | }; 978 | }; 979 | const templates = {}; 980 | const config = { 981 | tags: ["{{", "}}"] 982 | }; 983 | const templateConfig$1 = (newconfig) => { 984 | Object.assign(config, newconfig); 985 | }; 986 | const template = (target, { components }) => { 987 | tagElements(target, [...Object.keys(components), "[html-if]", "template"], components); 988 | const clone = target.cloneNode(true); 989 | transformTemplate(clone); 990 | removeTemplateTagsRecursively(clone); 991 | setTemplates(clone, components); 992 | return templates; 993 | }; 994 | const compile = (html) => { 995 | const parsedHtml = JSON.stringify(html); 996 | return new Function("$element", "safe", "$g", ` 997 | var $data = this; 998 | with( $data ){ 999 | var output=${parsedHtml.replace(/%%_=(.+?)_%%/g, function(_, variable) { 1000 | return '"+safe(function(){return ' + decodeHTML(variable) + ';})+"'; 1001 | }).replace(/%%_(.+?)_%%/g, function(_, variable) { 1002 | return '";' + decodeHTML(variable) + '\noutput+="'; 1003 | })};return output; 1004 | } 1005 | `); 1006 | }; 1007 | const tagElements = (target, keys, components) => { 1008 | target.querySelectorAll(keys.toString()).forEach((node) => { 1009 | const name = node.localName; 1010 | if (name === "template") { 1011 | return tagElements(node.content, keys, components); 1012 | } 1013 | if (node.getAttribute("html-if") && !node.id) { 1014 | node.id = uuid(); 1015 | } 1016 | if (name in components) { 1017 | node.setAttribute("tplid", uuid()); 1018 | } 1019 | }); 1020 | }; 1021 | const transformAttributes = (html) => { 1022 | const regexTags = new RegExp(`\\${config.tags[0]}(.+?)\\${config.tags[1]}`, "g"); 1023 | return html.replace(/jails___scope-id/g, "%%_=$scopeid_%%").replace(regexTags, "%%_=$1_%%").replace(/html-(allowfullscreen|async|autofocus|autoplay|checked|controls|default|defer|disabled|formnovalidate|inert|ismap|itemscope|loop|multiple|muted|nomodule|novalidate|open|playsinline|readonly|required|reversed|selected)=\"(.*?)\"/g, `%%_if(safe(function(){ return $2 })){_%%$1%%_}_%%`).replace(/html-([^\s]*?)=\"(.*?)\"/g, (all, key, value) => { 1024 | if (key === "key" || key === "model" || key === "scopeid") { 1025 | return all; 1026 | } 1027 | if (value) { 1028 | value = value.replace(/^{|}$/g, ""); 1029 | return `${key}="%%_=safe(function(){ return ${value} })_%%"`; 1030 | } else { 1031 | return all; 1032 | } 1033 | }); 1034 | }; 1035 | const transformTemplate = (clone) => { 1036 | clone.querySelectorAll("template, [html-for], [html-if], [html-inner], [html-class]").forEach((element) => { 1037 | const htmlFor = element.getAttribute("html-for"); 1038 | const htmlIf = element.getAttribute("html-if"); 1039 | const htmlInner = element.getAttribute("html-inner"); 1040 | const htmlClass = element.getAttribute("html-class"); 1041 | if (htmlFor) { 1042 | element.removeAttribute("html-for"); 1043 | const split = htmlFor.match(/(.*)\sin\s(.*)/) || ""; 1044 | const varname = split[1]; 1045 | const object = split[2]; 1046 | const objectname = object.split(/\./).shift(); 1047 | const open = document.createTextNode(`%%_ ;(function(){ var $index = 0; for(var $key in safe(function(){ return ${object} }) ){ var $scopeid = Math.random().toString(36).substring(2, 9); var ${varname} = ${object}[$key]; $g.scope[$scopeid] = Object.assign({}, { ${objectname}: ${objectname} }, { ${varname} :${varname}, $index: $index, $key: $key }); _%%`); 1048 | const close = document.createTextNode(`%%_ $index++; } })() _%%`); 1049 | wrap(open, element, close); 1050 | } 1051 | if (htmlIf) { 1052 | element.removeAttribute("html-if"); 1053 | const open = document.createTextNode(`%%_ if ( safe(function(){ return ${htmlIf} }) ){ _%%`); 1054 | const close = document.createTextNode(`%%_ } _%%`); 1055 | wrap(open, element, close); 1056 | } 1057 | if (htmlInner) { 1058 | element.removeAttribute("html-inner"); 1059 | element.innerHTML = `%%_=${htmlInner}_%%`; 1060 | } 1061 | if (htmlClass) { 1062 | element.removeAttribute("html-class"); 1063 | element.className = (element.className + ` %%_=${htmlClass}_%%`).trim(); 1064 | } 1065 | if (element.localName === "template") { 1066 | transformTemplate(element.content); 1067 | } 1068 | }); 1069 | }; 1070 | const setTemplates = (clone, components) => { 1071 | Array.from(clone.querySelectorAll("[tplid]")).reverse().forEach((node) => { 1072 | const tplid = node.getAttribute("tplid"); 1073 | const name = node.localName; 1074 | node.setAttribute("html-scopeid", "jails___scope-id"); 1075 | if (name in components && components[name].module.template) { 1076 | const children = node.innerHTML; 1077 | const html2 = components[name].module.template({ elm: node, children }); 1078 | node.innerHTML = html2; 1079 | transformTemplate(node); 1080 | removeTemplateTagsRecursively(node); 1081 | } 1082 | const html = transformAttributes(node.outerHTML); 1083 | templates[tplid] = { 1084 | template: html, 1085 | render: compile(html) 1086 | }; 1087 | }); 1088 | }; 1089 | const removeTemplateTagsRecursively = (node) => { 1090 | const templates2 = node.querySelectorAll("template"); 1091 | templates2.forEach((template2) => { 1092 | if (template2.getAttribute("html-if") || template2.getAttribute("html-inner")) { 1093 | return; 1094 | } 1095 | removeTemplateTagsRecursively(template2.content); 1096 | const parent = template2.parentNode; 1097 | if (parent) { 1098 | const content = template2.content; 1099 | while (content.firstChild) { 1100 | parent.insertBefore(content.firstChild, template2); 1101 | } 1102 | parent.removeChild(template2); 1103 | } 1104 | }); 1105 | }; 1106 | const wrap = (open, node, close) => { 1107 | var _a, _b; 1108 | (_a = node.parentNode) == null ? void 0 : _a.insertBefore(open, node); 1109 | (_b = node.parentNode) == null ? void 0 : _b.insertBefore(close, node.nextSibling); 1110 | }; 1111 | const templateConfig = (options) => { 1112 | templateConfig$1(options); 1113 | }; 1114 | globalThis.__jails__ = globalThis.__jails__ || { components: {} }; 1115 | const register = (name, module, dependencies) => { 1116 | const { components } = globalThis.__jails__; 1117 | components[name] = { name, module, dependencies }; 1118 | }; 1119 | const start = (target = document.body) => { 1120 | const { components } = globalThis.__jails__; 1121 | const templates2 = template(target, { components }); 1122 | Object.values(components).forEach(({ name, module, dependencies }) => { 1123 | if (!customElements.get(name)) { 1124 | customElements.define(name, Element$1({ component: { name, module, dependencies }, templates: templates2, start })); 1125 | } 1126 | }); 1127 | }; 1128 | export { 1129 | publish, 1130 | register, 1131 | start, 1132 | subscribe, 1133 | templateConfig 1134 | }; 1135 | //# sourceMappingURL=index.js.map 1136 | -------------------------------------------------------------------------------- /dist/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sources":["../src/utils/index.ts","../node_modules/idiomorph/dist/idiomorph.esm.js","../src/utils/pubsub.ts","../src/component.ts","../src/element.ts","../src/template-system.ts","../src/index.ts"],"sourcesContent":["let textarea\n\nexport const g = {\n\tscope: {}\n}\n\nexport const decodeHTML = (text) => {\n\ttextarea = textarea || document.createElement('textarea')\n\ttextarea.innerHTML = text\n\treturn textarea.value\n}\n\nexport const rAF = (fn) => {\n\tif (requestAnimationFrame)\n\t\treturn requestAnimationFrame(fn)\n\telse\n\t\treturn setTimeout(fn, 1000 / 60)\n}\n\nexport const uuid = () => {\n\treturn Math.random().toString(36).substring(2, 9)\n}\n\nexport const dup = (o) => {\n\treturn JSON.parse(JSON.stringify(o))\n}\n\nexport const safe = (execute, val) => {\n\ttry{\n\t\treturn execute()\n\t}catch(err){\n\t\treturn val || ''\n\t}\n}\n","/**\n * @typedef {object} ConfigHead\n *\n * @property {'merge' | 'append' | 'morph' | 'none'} [style]\n * @property {boolean} [block]\n * @property {boolean} [ignore]\n * @property {function(Element): boolean} [shouldPreserve]\n * @property {function(Element): boolean} [shouldReAppend]\n * @property {function(Element): boolean} [shouldRemove]\n * @property {function(Element, {added: Node[], kept: Element[], removed: Element[]}): void} [afterHeadMorphed]\n */\n\n/**\n * @typedef {object} ConfigCallbacks\n *\n * @property {function(Node): boolean} [beforeNodeAdded]\n * @property {function(Node): void} [afterNodeAdded]\n * @property {function(Element, Node): boolean} [beforeNodeMorphed]\n * @property {function(Element, Node): void} [afterNodeMorphed]\n * @property {function(Element): boolean} [beforeNodeRemoved]\n * @property {function(Element): void} [afterNodeRemoved]\n * @property {function(string, Element, \"update\" | \"remove\"): boolean} [beforeAttributeUpdated]\n */\n\n/**\n * @typedef {object} Config\n *\n * @property {'outerHTML' | 'innerHTML'} [morphStyle]\n * @property {boolean} [ignoreActive]\n * @property {boolean} [ignoreActiveValue]\n * @property {boolean} [restoreFocus]\n * @property {ConfigCallbacks} [callbacks]\n * @property {ConfigHead} [head]\n */\n\n/**\n * @typedef {function} NoOp\n *\n * @returns {void}\n */\n\n/**\n * @typedef {object} ConfigHeadInternal\n *\n * @property {'merge' | 'append' | 'morph' | 'none'} style\n * @property {boolean} [block]\n * @property {boolean} [ignore]\n * @property {(function(Element): boolean) | NoOp} shouldPreserve\n * @property {(function(Element): boolean) | NoOp} shouldReAppend\n * @property {(function(Element): boolean) | NoOp} shouldRemove\n * @property {(function(Element, {added: Node[], kept: Element[], removed: Element[]}): void) | NoOp} afterHeadMorphed\n */\n\n/**\n * @typedef {object} ConfigCallbacksInternal\n *\n * @property {(function(Node): boolean) | NoOp} beforeNodeAdded\n * @property {(function(Node): void) | NoOp} afterNodeAdded\n * @property {(function(Node, Node): boolean) | NoOp} beforeNodeMorphed\n * @property {(function(Node, Node): void) | NoOp} afterNodeMorphed\n * @property {(function(Node): boolean) | NoOp} beforeNodeRemoved\n * @property {(function(Node): void) | NoOp} afterNodeRemoved\n * @property {(function(string, Element, \"update\" | \"remove\"): boolean) | NoOp} beforeAttributeUpdated\n */\n\n/**\n * @typedef {object} ConfigInternal\n *\n * @property {'outerHTML' | 'innerHTML'} morphStyle\n * @property {boolean} [ignoreActive]\n * @property {boolean} [ignoreActiveValue]\n * @property {boolean} [restoreFocus]\n * @property {ConfigCallbacksInternal} callbacks\n * @property {ConfigHeadInternal} head\n */\n\n/**\n * @typedef {Object} IdSets\n * @property {Set} persistentIds\n * @property {Map>} idMap\n */\n\n/**\n * @typedef {Function} Morph\n *\n * @param {Element | Document} oldNode\n * @param {Element | Node | HTMLCollection | Node[] | string | null} newContent\n * @param {Config} [config]\n * @returns {undefined | Node[]}\n */\n\n// base IIFE to define idiomorph\n/**\n *\n * @type {{defaults: ConfigInternal, morph: Morph}}\n */\nvar Idiomorph = (function () {\n \"use strict\";\n\n /**\n * @typedef {object} MorphContext\n *\n * @property {Element} target\n * @property {Element} newContent\n * @property {ConfigInternal} config\n * @property {ConfigInternal['morphStyle']} morphStyle\n * @property {ConfigInternal['ignoreActive']} ignoreActive\n * @property {ConfigInternal['ignoreActiveValue']} ignoreActiveValue\n * @property {ConfigInternal['restoreFocus']} restoreFocus\n * @property {Map>} idMap\n * @property {Set} persistentIds\n * @property {ConfigInternal['callbacks']} callbacks\n * @property {ConfigInternal['head']} head\n * @property {HTMLDivElement} pantry\n */\n\n //=============================================================================\n // AND NOW IT BEGINS...\n //=============================================================================\n\n const noOp = () => {};\n /**\n * Default configuration values, updatable by users now\n * @type {ConfigInternal}\n */\n const defaults = {\n morphStyle: \"outerHTML\",\n callbacks: {\n beforeNodeAdded: noOp,\n afterNodeAdded: noOp,\n beforeNodeMorphed: noOp,\n afterNodeMorphed: noOp,\n beforeNodeRemoved: noOp,\n afterNodeRemoved: noOp,\n beforeAttributeUpdated: noOp,\n },\n head: {\n style: \"merge\",\n shouldPreserve: (elt) => elt.getAttribute(\"im-preserve\") === \"true\",\n shouldReAppend: (elt) => elt.getAttribute(\"im-re-append\") === \"true\",\n shouldRemove: noOp,\n afterHeadMorphed: noOp,\n },\n restoreFocus: true,\n };\n\n /**\n * Core idiomorph function for morphing one DOM tree to another\n *\n * @param {Element | Document} oldNode\n * @param {Element | Node | HTMLCollection | Node[] | string | null} newContent\n * @param {Config} [config]\n * @returns {Promise | Node[]}\n */\n function morph(oldNode, newContent, config = {}) {\n oldNode = normalizeElement(oldNode);\n const newNode = normalizeParent(newContent);\n const ctx = createMorphContext(oldNode, newNode, config);\n\n const morphedNodes = saveAndRestoreFocus(ctx, () => {\n return withHeadBlocking(\n ctx,\n oldNode,\n newNode,\n /** @param {MorphContext} ctx */ (ctx) => {\n if (ctx.morphStyle === \"innerHTML\") {\n morphChildren(ctx, oldNode, newNode);\n return Array.from(oldNode.childNodes);\n } else {\n return morphOuterHTML(ctx, oldNode, newNode);\n }\n },\n );\n });\n\n ctx.pantry.remove();\n return morphedNodes;\n }\n\n /**\n * Morph just the outerHTML of the oldNode to the newContent\n * We have to be careful because the oldNode could have siblings which need to be untouched\n * @param {MorphContext} ctx\n * @param {Element} oldNode\n * @param {Element} newNode\n * @returns {Node[]}\n */\n function morphOuterHTML(ctx, oldNode, newNode) {\n const oldParent = normalizeParent(oldNode);\n\n // basis for calulating which nodes were morphed\n // since there may be unmorphed sibling nodes\n let childNodes = Array.from(oldParent.childNodes);\n const index = childNodes.indexOf(oldNode);\n // how many elements are to the right of the oldNode\n const rightMargin = childNodes.length - (index + 1);\n\n morphChildren(\n ctx,\n oldParent,\n newNode,\n // these two optional params are the secret sauce\n oldNode, // start point for iteration\n oldNode.nextSibling, // end point for iteration\n );\n\n // return just the morphed nodes\n childNodes = Array.from(oldParent.childNodes);\n return childNodes.slice(index, childNodes.length - rightMargin);\n }\n\n /**\n * @param {MorphContext} ctx\n * @param {Function} fn\n * @returns {Promise | Node[]}\n */\n function saveAndRestoreFocus(ctx, fn) {\n if (!ctx.config.restoreFocus) return fn();\n let activeElement =\n /** @type {HTMLInputElement|HTMLTextAreaElement|null} */ (\n document.activeElement\n );\n\n // don't bother if the active element is not an input or textarea\n if (\n !(\n activeElement instanceof HTMLInputElement ||\n activeElement instanceof HTMLTextAreaElement\n )\n ) {\n return fn();\n }\n\n const { id: activeElementId, selectionStart, selectionEnd } = activeElement;\n\n const results = fn();\n\n if (activeElementId && activeElementId !== document.activeElement?.id) {\n activeElement = ctx.target.querySelector(`#${activeElementId}`);\n activeElement?.focus();\n }\n if (activeElement && !activeElement.selectionEnd && selectionEnd) {\n activeElement.setSelectionRange(selectionStart, selectionEnd);\n }\n\n return results;\n }\n\n const morphChildren = (function () {\n /**\n * This is the core algorithm for matching up children. The idea is to use id sets to try to match up\n * nodes as faithfully as possible. We greedily match, which allows us to keep the algorithm fast, but\n * by using id sets, we are able to better match up with content deeper in the DOM.\n *\n * Basic algorithm:\n * - for each node in the new content:\n * - search self and siblings for an id set match, falling back to a soft match\n * - if match found\n * - remove any nodes up to the match:\n * - pantry persistent nodes\n * - delete the rest\n * - morph the match\n * - elsif no match found, and node is persistent\n * - find its match by querying the old root (future) and pantry (past)\n * - move it and its children here\n * - morph it\n * - else\n * - create a new node from scratch as a last result\n *\n * @param {MorphContext} ctx the merge context\n * @param {Element} oldParent the old content that we are merging the new content into\n * @param {Element} newParent the parent element of the new content\n * @param {Node|null} [insertionPoint] the point in the DOM we start morphing at (defaults to first child)\n * @param {Node|null} [endPoint] the point in the DOM we stop morphing at (defaults to after last child)\n */\n function morphChildren(\n ctx,\n oldParent,\n newParent,\n insertionPoint = null,\n endPoint = null,\n ) {\n // normalize\n if (\n oldParent instanceof HTMLTemplateElement &&\n newParent instanceof HTMLTemplateElement\n ) {\n // @ts-ignore we can pretend the DocumentFragment is an Element\n oldParent = oldParent.content;\n // @ts-ignore ditto\n newParent = newParent.content;\n }\n insertionPoint ||= oldParent.firstChild;\n\n // run through all the new content\n for (const newChild of newParent.childNodes) {\n // once we reach the end of the old parent content skip to the end and insert the rest\n if (insertionPoint && insertionPoint != endPoint) {\n const bestMatch = findBestMatch(\n ctx,\n newChild,\n insertionPoint,\n endPoint,\n );\n if (bestMatch) {\n // if the node to morph is not at the insertion point then remove/move up to it\n if (bestMatch !== insertionPoint) {\n removeNodesBetween(ctx, insertionPoint, bestMatch);\n }\n morphNode(bestMatch, newChild, ctx);\n insertionPoint = bestMatch.nextSibling;\n continue;\n }\n }\n\n // if the matching node is elsewhere in the original content\n if (newChild instanceof Element && ctx.persistentIds.has(newChild.id)) {\n // move it and all its children here and morph\n const movedChild = moveBeforeById(\n oldParent,\n newChild.id,\n insertionPoint,\n ctx,\n );\n morphNode(movedChild, newChild, ctx);\n insertionPoint = movedChild.nextSibling;\n continue;\n }\n\n // last resort: insert the new node from scratch\n const insertedNode = createNode(\n oldParent,\n newChild,\n insertionPoint,\n ctx,\n );\n // could be null if beforeNodeAdded prevented insertion\n if (insertedNode) {\n insertionPoint = insertedNode.nextSibling;\n }\n }\n\n // remove any remaining old nodes that didn't match up with new content\n while (insertionPoint && insertionPoint != endPoint) {\n const tempNode = insertionPoint;\n insertionPoint = insertionPoint.nextSibling;\n removeNode(ctx, tempNode);\n }\n }\n\n /**\n * This performs the action of inserting a new node while handling situations where the node contains\n * elements with persistent ids and possible state info we can still preserve by moving in and then morphing\n *\n * @param {Element} oldParent\n * @param {Node} newChild\n * @param {Node|null} insertionPoint\n * @param {MorphContext} ctx\n * @returns {Node|null}\n */\n function createNode(oldParent, newChild, insertionPoint, ctx) {\n if (ctx.callbacks.beforeNodeAdded(newChild) === false) return null;\n if (ctx.idMap.has(newChild)) {\n // node has children with ids with possible state so create a dummy elt of same type and apply full morph algorithm\n const newEmptyChild = document.createElement(\n /** @type {Element} */ (newChild).tagName,\n );\n oldParent.insertBefore(newEmptyChild, insertionPoint);\n morphNode(newEmptyChild, newChild, ctx);\n ctx.callbacks.afterNodeAdded(newEmptyChild);\n return newEmptyChild;\n } else {\n // optimisation: no id state to preserve so we can just insert a clone of the newChild and its descendants\n const newClonedChild = document.importNode(newChild, true); // importNode to not mutate newParent\n oldParent.insertBefore(newClonedChild, insertionPoint);\n ctx.callbacks.afterNodeAdded(newClonedChild);\n return newClonedChild;\n }\n }\n\n //=============================================================================\n // Matching Functions\n //=============================================================================\n const findBestMatch = (function () {\n /**\n * Scans forward from the startPoint to the endPoint looking for a match\n * for the node. It looks for an id set match first, then a soft match.\n * We abort softmatching if we find two future soft matches, to reduce churn.\n * @param {Node} node\n * @param {MorphContext} ctx\n * @param {Node | null} startPoint\n * @param {Node | null} endPoint\n * @returns {Node | null}\n */\n function findBestMatch(ctx, node, startPoint, endPoint) {\n let softMatch = null;\n let nextSibling = node.nextSibling;\n let siblingSoftMatchCount = 0;\n\n let cursor = startPoint;\n while (cursor && cursor != endPoint) {\n // soft matching is a prerequisite for id set matching\n if (isSoftMatch(cursor, node)) {\n if (isIdSetMatch(ctx, cursor, node)) {\n return cursor; // found an id set match, we're done!\n }\n\n // we haven't yet saved a soft match fallback\n if (softMatch === null) {\n // the current soft match will hard match something else in the future, leave it\n if (!ctx.idMap.has(cursor)) {\n // save this as the fallback if we get through the loop without finding a hard match\n softMatch = cursor;\n }\n }\n }\n if (\n softMatch === null &&\n nextSibling &&\n isSoftMatch(cursor, nextSibling)\n ) {\n // The next new node has a soft match with this node, so\n // increment the count of future soft matches\n siblingSoftMatchCount++;\n nextSibling = nextSibling.nextSibling;\n\n // If there are two future soft matches, block soft matching for this node to allow\n // future siblings to soft match. This is to reduce churn in the DOM when an element\n // is prepended.\n if (siblingSoftMatchCount >= 2) {\n softMatch = undefined;\n }\n }\n\n // if the current node contains active element, stop looking for better future matches,\n // because if one is found, this node will be moved to the pantry, reparenting it and thus losing focus\n if (cursor.contains(document.activeElement)) break;\n\n cursor = cursor.nextSibling;\n }\n\n return softMatch || null;\n }\n\n /**\n *\n * @param {MorphContext} ctx\n * @param {Node} oldNode\n * @param {Node} newNode\n * @returns {boolean}\n */\n function isIdSetMatch(ctx, oldNode, newNode) {\n let oldSet = ctx.idMap.get(oldNode);\n let newSet = ctx.idMap.get(newNode);\n\n if (!newSet || !oldSet) return false;\n\n for (const id of oldSet) {\n // a potential match is an id in the new and old nodes that\n // has not already been merged into the DOM\n // But the newNode content we call this on has not been\n // merged yet and we don't allow duplicate IDs so it is simple\n if (newSet.has(id)) {\n return true;\n }\n }\n return false;\n }\n\n /**\n *\n * @param {Node} oldNode\n * @param {Node} newNode\n * @returns {boolean}\n */\n function isSoftMatch(oldNode, newNode) {\n // ok to cast: if one is not element, `id` and `tagName` will be undefined and we'll just compare that.\n const oldElt = /** @type {Element} */ (oldNode);\n const newElt = /** @type {Element} */ (newNode);\n\n return (\n oldElt.nodeType === newElt.nodeType &&\n oldElt.tagName === newElt.tagName &&\n // If oldElt has an `id` with possible state and it doesn't match newElt.id then avoid morphing.\n // We'll still match an anonymous node with an IDed newElt, though, because if it got this far,\n // its not persistent, and new nodes can't have any hidden state.\n (!oldElt.id || oldElt.id === newElt.id)\n );\n }\n\n return findBestMatch;\n })();\n\n //=============================================================================\n // DOM Manipulation Functions\n //=============================================================================\n\n /**\n * Gets rid of an unwanted DOM node; strategy depends on nature of its reuse:\n * - Persistent nodes will be moved to the pantry for later reuse\n * - Other nodes will have their hooks called, and then are removed\n * @param {MorphContext} ctx\n * @param {Node} node\n */\n function removeNode(ctx, node) {\n // are we going to id set match this later?\n if (ctx.idMap.has(node)) {\n // skip callbacks and move to pantry\n moveBefore(ctx.pantry, node, null);\n } else {\n // remove for realsies\n if (ctx.callbacks.beforeNodeRemoved(node) === false) return;\n node.parentNode?.removeChild(node);\n ctx.callbacks.afterNodeRemoved(node);\n }\n }\n\n /**\n * Remove nodes between the start and end nodes\n * @param {MorphContext} ctx\n * @param {Node} startInclusive\n * @param {Node} endExclusive\n * @returns {Node|null}\n */\n function removeNodesBetween(ctx, startInclusive, endExclusive) {\n /** @type {Node | null} */\n let cursor = startInclusive;\n // remove nodes until the endExclusive node\n while (cursor && cursor !== endExclusive) {\n let tempNode = /** @type {Node} */ (cursor);\n cursor = cursor.nextSibling;\n removeNode(ctx, tempNode);\n }\n return cursor;\n }\n\n /**\n * Search for an element by id within the document and pantry, and move it using moveBefore.\n *\n * @param {Element} parentNode - The parent node to which the element will be moved.\n * @param {string} id - The ID of the element to be moved.\n * @param {Node | null} after - The reference node to insert the element before.\n * If `null`, the element is appended as the last child.\n * @param {MorphContext} ctx\n * @returns {Element} The found element\n */\n function moveBeforeById(parentNode, id, after, ctx) {\n const target =\n /** @type {Element} - will always be found */\n (\n ctx.target.querySelector(`#${id}`) ||\n ctx.pantry.querySelector(`#${id}`)\n );\n removeElementFromAncestorsIdMaps(target, ctx);\n moveBefore(parentNode, target, after);\n return target;\n }\n\n /**\n * Removes an element from its ancestors' id maps. This is needed when an element is moved from the\n * \"future\" via `moveBeforeId`. Otherwise, its erstwhile ancestors could be mistakenly moved to the\n * pantry rather than being deleted, preventing their removal hooks from being called.\n *\n * @param {Element} element - element to remove from its ancestors' id maps\n * @param {MorphContext} ctx\n */\n function removeElementFromAncestorsIdMaps(element, ctx) {\n const id = element.id;\n /** @ts-ignore - safe to loop in this way **/\n while ((element = element.parentNode)) {\n let idSet = ctx.idMap.get(element);\n if (idSet) {\n idSet.delete(id);\n if (!idSet.size) {\n ctx.idMap.delete(element);\n }\n }\n }\n }\n\n /**\n * Moves an element before another element within the same parent.\n * Uses the proposed `moveBefore` API if available (and working), otherwise falls back to `insertBefore`.\n * This is essentialy a forward-compat wrapper.\n *\n * @param {Element} parentNode - The parent node containing the after element.\n * @param {Node} element - The element to be moved.\n * @param {Node | null} after - The reference node to insert `element` before.\n * If `null`, `element` is appended as the last child.\n */\n function moveBefore(parentNode, element, after) {\n // @ts-ignore - use proposed moveBefore feature\n if (parentNode.moveBefore) {\n try {\n // @ts-ignore - use proposed moveBefore feature\n parentNode.moveBefore(element, after);\n } catch (e) {\n // fall back to insertBefore as some browsers may fail on moveBefore when trying to move Dom disconnected nodes to pantry\n parentNode.insertBefore(element, after);\n }\n } else {\n parentNode.insertBefore(element, after);\n }\n }\n\n return morphChildren;\n })();\n\n //=============================================================================\n // Single Node Morphing Code\n //=============================================================================\n const morphNode = (function () {\n /**\n * @param {Node} oldNode root node to merge content into\n * @param {Node} newContent new content to merge\n * @param {MorphContext} ctx the merge context\n * @returns {Node | null} the element that ended up in the DOM\n */\n function morphNode(oldNode, newContent, ctx) {\n if (ctx.ignoreActive && oldNode === document.activeElement) {\n // don't morph focused element\n return null;\n }\n\n if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) {\n return oldNode;\n }\n\n if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) {\n // ignore the head element\n } else if (\n oldNode instanceof HTMLHeadElement &&\n ctx.head.style !== \"morph\"\n ) {\n // ok to cast: if newContent wasn't also a , it would've got caught in the `!isSoftMatch` branch above\n handleHeadElement(\n oldNode,\n /** @type {HTMLHeadElement} */ (newContent),\n ctx,\n );\n } else {\n morphAttributes(oldNode, newContent, ctx);\n if (!ignoreValueOfActiveElement(oldNode, ctx)) {\n // @ts-ignore newContent can be a node here because .firstChild will be null\n morphChildren(ctx, oldNode, newContent);\n }\n }\n ctx.callbacks.afterNodeMorphed(oldNode, newContent);\n return oldNode;\n }\n\n /**\n * syncs the oldNode to the newNode, copying over all attributes and\n * inner element state from the newNode to the oldNode\n *\n * @param {Node} oldNode the node to copy attributes & state to\n * @param {Node} newNode the node to copy attributes & state from\n * @param {MorphContext} ctx the merge context\n */\n function morphAttributes(oldNode, newNode, ctx) {\n let type = newNode.nodeType;\n\n // if is an element type, sync the attributes from the\n // new node into the new node\n if (type === 1 /* element type */) {\n const oldElt = /** @type {Element} */ (oldNode);\n const newElt = /** @type {Element} */ (newNode);\n\n const oldAttributes = oldElt.attributes;\n const newAttributes = newElt.attributes;\n for (const newAttribute of newAttributes) {\n if (ignoreAttribute(newAttribute.name, oldElt, \"update\", ctx)) {\n continue;\n }\n if (oldElt.getAttribute(newAttribute.name) !== newAttribute.value) {\n oldElt.setAttribute(newAttribute.name, newAttribute.value);\n }\n }\n // iterate backwards to avoid skipping over items when a delete occurs\n for (let i = oldAttributes.length - 1; 0 <= i; i--) {\n const oldAttribute = oldAttributes[i];\n\n // toAttributes is a live NamedNodeMap, so iteration+mutation is unsafe\n // e.g. custom element attribute callbacks can remove other attributes\n if (!oldAttribute) continue;\n\n if (!newElt.hasAttribute(oldAttribute.name)) {\n if (ignoreAttribute(oldAttribute.name, oldElt, \"remove\", ctx)) {\n continue;\n }\n oldElt.removeAttribute(oldAttribute.name);\n }\n }\n\n if (!ignoreValueOfActiveElement(oldElt, ctx)) {\n syncInputValue(oldElt, newElt, ctx);\n }\n }\n\n // sync text nodes\n if (type === 8 /* comment */ || type === 3 /* text */) {\n if (oldNode.nodeValue !== newNode.nodeValue) {\n oldNode.nodeValue = newNode.nodeValue;\n }\n }\n }\n\n /**\n * NB: many bothans died to bring us information:\n *\n * https://github.com/patrick-steele-idem/morphdom/blob/master/src/specialElHandlers.js\n * https://github.com/choojs/nanomorph/blob/master/lib/morph.jsL113\n *\n * @param {Element} oldElement the element to sync the input value to\n * @param {Element} newElement the element to sync the input value from\n * @param {MorphContext} ctx the merge context\n */\n function syncInputValue(oldElement, newElement, ctx) {\n if (\n oldElement instanceof HTMLInputElement &&\n newElement instanceof HTMLInputElement &&\n newElement.type !== \"file\"\n ) {\n let newValue = newElement.value;\n let oldValue = oldElement.value;\n\n // sync boolean attributes\n syncBooleanAttribute(oldElement, newElement, \"checked\", ctx);\n syncBooleanAttribute(oldElement, newElement, \"disabled\", ctx);\n\n if (!newElement.hasAttribute(\"value\")) {\n if (!ignoreAttribute(\"value\", oldElement, \"remove\", ctx)) {\n oldElement.value = \"\";\n oldElement.removeAttribute(\"value\");\n }\n } else if (oldValue !== newValue) {\n if (!ignoreAttribute(\"value\", oldElement, \"update\", ctx)) {\n oldElement.setAttribute(\"value\", newValue);\n oldElement.value = newValue;\n }\n }\n // TODO: QUESTION(1cg): this used to only check `newElement` unlike the other branches -- why?\n // did I break something?\n } else if (\n oldElement instanceof HTMLOptionElement &&\n newElement instanceof HTMLOptionElement\n ) {\n syncBooleanAttribute(oldElement, newElement, \"selected\", ctx);\n } else if (\n oldElement instanceof HTMLTextAreaElement &&\n newElement instanceof HTMLTextAreaElement\n ) {\n let newValue = newElement.value;\n let oldValue = oldElement.value;\n if (ignoreAttribute(\"value\", oldElement, \"update\", ctx)) {\n return;\n }\n if (newValue !== oldValue) {\n oldElement.value = newValue;\n }\n if (\n oldElement.firstChild &&\n oldElement.firstChild.nodeValue !== newValue\n ) {\n oldElement.firstChild.nodeValue = newValue;\n }\n }\n }\n\n /**\n * @param {Element} oldElement element to write the value to\n * @param {Element} newElement element to read the value from\n * @param {string} attributeName the attribute name\n * @param {MorphContext} ctx the merge context\n */\n function syncBooleanAttribute(oldElement, newElement, attributeName, ctx) {\n // @ts-ignore this function is only used on boolean attrs that are reflected as dom properties\n const newLiveValue = newElement[attributeName],\n // @ts-ignore ditto\n oldLiveValue = oldElement[attributeName];\n if (newLiveValue !== oldLiveValue) {\n const ignoreUpdate = ignoreAttribute(\n attributeName,\n oldElement,\n \"update\",\n ctx,\n );\n if (!ignoreUpdate) {\n // update attribute's associated DOM property\n // @ts-ignore this function is only used on boolean attrs that are reflected as dom properties\n oldElement[attributeName] = newElement[attributeName];\n }\n if (newLiveValue) {\n if (!ignoreUpdate) {\n // https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML\n // this is the correct way to set a boolean attribute to \"true\"\n oldElement.setAttribute(attributeName, \"\");\n }\n } else {\n if (!ignoreAttribute(attributeName, oldElement, \"remove\", ctx)) {\n oldElement.removeAttribute(attributeName);\n }\n }\n }\n }\n\n /**\n * @param {string} attr the attribute to be mutated\n * @param {Element} element the element that is going to be updated\n * @param {\"update\" | \"remove\"} updateType\n * @param {MorphContext} ctx the merge context\n * @returns {boolean} true if the attribute should be ignored, false otherwise\n */\n function ignoreAttribute(attr, element, updateType, ctx) {\n if (\n attr === \"value\" &&\n ctx.ignoreActiveValue &&\n element === document.activeElement\n ) {\n return true;\n }\n return (\n ctx.callbacks.beforeAttributeUpdated(attr, element, updateType) ===\n false\n );\n }\n\n /**\n * @param {Node} possibleActiveElement\n * @param {MorphContext} ctx\n * @returns {boolean}\n */\n function ignoreValueOfActiveElement(possibleActiveElement, ctx) {\n return (\n !!ctx.ignoreActiveValue &&\n possibleActiveElement === document.activeElement &&\n possibleActiveElement !== document.body\n );\n }\n\n return morphNode;\n })();\n\n //=============================================================================\n // Head Management Functions\n //=============================================================================\n /**\n * @param {MorphContext} ctx\n * @param {Element} oldNode\n * @param {Element} newNode\n * @param {function} callback\n * @returns {Node[] | Promise}\n */\n function withHeadBlocking(ctx, oldNode, newNode, callback) {\n if (ctx.head.block) {\n const oldHead = oldNode.querySelector(\"head\");\n const newHead = newNode.querySelector(\"head\");\n if (oldHead && newHead) {\n const promises = handleHeadElement(oldHead, newHead, ctx);\n // when head promises resolve, proceed ignoring the head tag\n return Promise.all(promises).then(() => {\n const newCtx = Object.assign(ctx, {\n head: {\n block: false,\n ignore: true,\n },\n });\n return callback(newCtx);\n });\n }\n }\n // just proceed if we not head blocking\n return callback(ctx);\n }\n\n /**\n * The HEAD tag can be handled specially, either w/ a 'merge' or 'append' style\n *\n * @param {Element} oldHead\n * @param {Element} newHead\n * @param {MorphContext} ctx\n * @returns {Promise[]}\n */\n function handleHeadElement(oldHead, newHead, ctx) {\n let added = [];\n let removed = [];\n let preserved = [];\n let nodesToAppend = [];\n\n // put all new head elements into a Map, by their outerHTML\n let srcToNewHeadNodes = new Map();\n for (const newHeadChild of newHead.children) {\n srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);\n }\n\n // for each elt in the current head\n for (const currentHeadElt of oldHead.children) {\n // If the current head element is in the map\n let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);\n let isReAppended = ctx.head.shouldReAppend(currentHeadElt);\n let isPreserved = ctx.head.shouldPreserve(currentHeadElt);\n if (inNewContent || isPreserved) {\n if (isReAppended) {\n // remove the current version and let the new version replace it and re-execute\n removed.push(currentHeadElt);\n } else {\n // this element already exists and should not be re-appended, so remove it from\n // the new content map, preserving it in the DOM\n srcToNewHeadNodes.delete(currentHeadElt.outerHTML);\n preserved.push(currentHeadElt);\n }\n } else {\n if (ctx.head.style === \"append\") {\n // we are appending and this existing element is not new content\n // so if and only if it is marked for re-append do we do anything\n if (isReAppended) {\n removed.push(currentHeadElt);\n nodesToAppend.push(currentHeadElt);\n }\n } else {\n // if this is a merge, we remove this content since it is not in the new head\n if (ctx.head.shouldRemove(currentHeadElt) !== false) {\n removed.push(currentHeadElt);\n }\n }\n }\n }\n\n // Push the remaining new head elements in the Map into the\n // nodes to append to the head tag\n nodesToAppend.push(...srcToNewHeadNodes.values());\n\n let promises = [];\n for (const newNode of nodesToAppend) {\n // TODO: This could theoretically be null, based on type\n let newElt = /** @type {ChildNode} */ (\n document.createRange().createContextualFragment(newNode.outerHTML)\n .firstChild\n );\n if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {\n if (\n (\"href\" in newElt && newElt.href) ||\n (\"src\" in newElt && newElt.src)\n ) {\n /** @type {(result?: any) => void} */ let resolve;\n let promise = new Promise(function (_resolve) {\n resolve = _resolve;\n });\n newElt.addEventListener(\"load\", function () {\n resolve();\n });\n promises.push(promise);\n }\n oldHead.appendChild(newElt);\n ctx.callbacks.afterNodeAdded(newElt);\n added.push(newElt);\n }\n }\n\n // remove all removed elements, after we have appended the new elements to avoid\n // additional network requests for things like style sheets\n for (const removedElement of removed) {\n if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {\n oldHead.removeChild(removedElement);\n ctx.callbacks.afterNodeRemoved(removedElement);\n }\n }\n\n ctx.head.afterHeadMorphed(oldHead, {\n added: added,\n kept: preserved,\n removed: removed,\n });\n return promises;\n }\n\n //=============================================================================\n // Create Morph Context Functions\n //=============================================================================\n const createMorphContext = (function () {\n /**\n *\n * @param {Element} oldNode\n * @param {Element} newContent\n * @param {Config} config\n * @returns {MorphContext}\n */\n function createMorphContext(oldNode, newContent, config) {\n const { persistentIds, idMap } = createIdMaps(oldNode, newContent);\n\n const mergedConfig = mergeDefaults(config);\n const morphStyle = mergedConfig.morphStyle || \"outerHTML\";\n if (![\"innerHTML\", \"outerHTML\"].includes(morphStyle)) {\n throw `Do not understand how to morph style ${morphStyle}`;\n }\n\n return {\n target: oldNode,\n newContent: newContent,\n config: mergedConfig,\n morphStyle: morphStyle,\n ignoreActive: mergedConfig.ignoreActive,\n ignoreActiveValue: mergedConfig.ignoreActiveValue,\n restoreFocus: mergedConfig.restoreFocus,\n idMap: idMap,\n persistentIds: persistentIds,\n pantry: createPantry(),\n callbacks: mergedConfig.callbacks,\n head: mergedConfig.head,\n };\n }\n\n /**\n * Deep merges the config object and the Idiomorph.defaults object to\n * produce a final configuration object\n * @param {Config} config\n * @returns {ConfigInternal}\n */\n function mergeDefaults(config) {\n let finalConfig = Object.assign({}, defaults);\n\n // copy top level stuff into final config\n Object.assign(finalConfig, config);\n\n // copy callbacks into final config (do this to deep merge the callbacks)\n finalConfig.callbacks = Object.assign(\n {},\n defaults.callbacks,\n config.callbacks,\n );\n\n // copy head config into final config (do this to deep merge the head)\n finalConfig.head = Object.assign({}, defaults.head, config.head);\n\n return finalConfig;\n }\n\n /**\n * @returns {HTMLDivElement}\n */\n function createPantry() {\n const pantry = document.createElement(\"div\");\n pantry.hidden = true;\n document.body.insertAdjacentElement(\"afterend\", pantry);\n return pantry;\n }\n\n /**\n * Returns all elements with an ID contained within the root element and its descendants\n *\n * @param {Element} root\n * @returns {Element[]}\n */\n function findIdElements(root) {\n let elements = Array.from(root.querySelectorAll(\"[id]\"));\n if (root.id) {\n elements.push(root);\n }\n return elements;\n }\n\n /**\n * A bottom-up algorithm that populates a map of Element -> IdSet.\n * The idSet for a given element is the set of all IDs contained within its subtree.\n * As an optimzation, we filter these IDs through the given list of persistent IDs,\n * because we don't need to bother considering IDed elements that won't be in the new content.\n *\n * @param {Map>} idMap\n * @param {Set} persistentIds\n * @param {Element} root\n * @param {Element[]} elements\n */\n function populateIdMapWithTree(idMap, persistentIds, root, elements) {\n for (const elt of elements) {\n if (persistentIds.has(elt.id)) {\n /** @type {Element|null} */\n let current = elt;\n // walk up the parent hierarchy of that element, adding the id\n // of element to the parent's id set\n while (current) {\n let idSet = idMap.get(current);\n // if the id set doesn't exist, create it and insert it in the map\n if (idSet == null) {\n idSet = new Set();\n idMap.set(current, idSet);\n }\n idSet.add(elt.id);\n\n if (current === root) break;\n current = current.parentElement;\n }\n }\n }\n }\n\n /**\n * This function computes a map of nodes to all ids contained within that node (inclusive of the\n * node). This map can be used to ask if two nodes have intersecting sets of ids, which allows\n * for a looser definition of \"matching\" than tradition id matching, and allows child nodes\n * to contribute to a parent nodes matching.\n *\n * @param {Element} oldContent the old content that will be morphed\n * @param {Element} newContent the new content to morph to\n * @returns {IdSets}\n */\n function createIdMaps(oldContent, newContent) {\n const oldIdElements = findIdElements(oldContent);\n const newIdElements = findIdElements(newContent);\n\n const persistentIds = createPersistentIds(oldIdElements, newIdElements);\n\n /** @type {Map>} */\n let idMap = new Map();\n populateIdMapWithTree(idMap, persistentIds, oldContent, oldIdElements);\n\n /** @ts-ignore - if newContent is a duck-typed parent, pass its single child node as the root to halt upwards iteration */\n const newRoot = newContent.__idiomorphRoot || newContent;\n populateIdMapWithTree(idMap, persistentIds, newRoot, newIdElements);\n\n return { persistentIds, idMap };\n }\n\n /**\n * This function computes the set of ids that persist between the two contents excluding duplicates\n *\n * @param {Element[]} oldIdElements\n * @param {Element[]} newIdElements\n * @returns {Set}\n */\n function createPersistentIds(oldIdElements, newIdElements) {\n let duplicateIds = new Set();\n\n /** @type {Map} */\n let oldIdTagNameMap = new Map();\n for (const { id, tagName } of oldIdElements) {\n if (oldIdTagNameMap.has(id)) {\n duplicateIds.add(id);\n } else {\n oldIdTagNameMap.set(id, tagName);\n }\n }\n\n let persistentIds = new Set();\n for (const { id, tagName } of newIdElements) {\n if (persistentIds.has(id)) {\n duplicateIds.add(id);\n } else if (oldIdTagNameMap.get(id) === tagName) {\n persistentIds.add(id);\n }\n // skip if tag types mismatch because its not possible to morph one tag into another\n }\n\n for (const id of duplicateIds) {\n persistentIds.delete(id);\n }\n return persistentIds;\n }\n\n return createMorphContext;\n })();\n\n //=============================================================================\n // HTML Normalization Functions\n //=============================================================================\n const { normalizeElement, normalizeParent } = (function () {\n /** @type {WeakSet} */\n const generatedByIdiomorph = new WeakSet();\n\n /**\n *\n * @param {Element | Document} content\n * @returns {Element}\n */\n function normalizeElement(content) {\n if (content instanceof Document) {\n return content.documentElement;\n } else {\n return content;\n }\n }\n\n /**\n *\n * @param {null | string | Node | HTMLCollection | Node[] | Document & {generatedByIdiomorph:boolean}} newContent\n * @returns {Element}\n */\n function normalizeParent(newContent) {\n if (newContent == null) {\n return document.createElement(\"div\"); // dummy parent element\n } else if (typeof newContent === \"string\") {\n return normalizeParent(parseContent(newContent));\n } else if (\n generatedByIdiomorph.has(/** @type {Element} */ (newContent))\n ) {\n // the template tag created by idiomorph parsing can serve as a dummy parent\n return /** @type {Element} */ (newContent);\n } else if (newContent instanceof Node) {\n if (newContent.parentNode) {\n // we can't use the parent directly because newContent may have siblings\n // that we don't want in the morph, and reparenting might be expensive (TODO is it?),\n // so we create a duck-typed parent node instead.\n return createDuckTypedParent(newContent);\n } else {\n // a single node is added as a child to a dummy parent\n const dummyParent = document.createElement(\"div\");\n dummyParent.append(newContent);\n return dummyParent;\n }\n } else {\n // all nodes in the array or HTMLElement collection are consolidated under\n // a single dummy parent element\n const dummyParent = document.createElement(\"div\");\n for (const elt of [...newContent]) {\n dummyParent.append(elt);\n }\n return dummyParent;\n }\n }\n\n /**\n * Creates a fake duck-typed parent element to wrap a single node, without actually reparenting it.\n * \"If it walks like a duck, and quacks like a duck, then it must be a duck!\" -- James Whitcomb Riley (1849–1916)\n *\n * @param {Node} newContent\n * @returns {Element}\n */\n function createDuckTypedParent(newContent) {\n return /** @type {Element} */ (\n /** @type {unknown} */ ({\n childNodes: [newContent],\n /** @ts-ignore - cover your eyes for a minute, tsc */\n querySelectorAll: (s) => {\n /** @ts-ignore */\n const elements = newContent.querySelectorAll(s);\n /** @ts-ignore */\n return newContent.matches(s) ? [newContent, ...elements] : elements;\n },\n /** @ts-ignore */\n insertBefore: (n, r) => newContent.parentNode.insertBefore(n, r),\n /** @ts-ignore */\n moveBefore: (n, r) => newContent.parentNode.moveBefore(n, r),\n // for later use with populateIdMapWithTree to halt upwards iteration\n get __idiomorphRoot() {\n return newContent;\n },\n })\n );\n }\n\n /**\n *\n * @param {string} newContent\n * @returns {Node | null | DocumentFragment}\n */\n function parseContent(newContent) {\n let parser = new DOMParser();\n\n // remove svgs to avoid false-positive matches on head, etc.\n let contentWithSvgsRemoved = newContent.replace(\n /]*>|>)([\\s\\S]*?)<\\/svg>/gim,\n \"\",\n );\n\n // if the newContent contains a html, head or body tag, we can simply parse it w/o wrapping\n if (\n contentWithSvgsRemoved.match(/<\\/html>/) ||\n contentWithSvgsRemoved.match(/<\\/head>/) ||\n contentWithSvgsRemoved.match(/<\\/body>/)\n ) {\n let content = parser.parseFromString(newContent, \"text/html\");\n // if it is a full HTML document, return the document itself as the parent container\n if (contentWithSvgsRemoved.match(/<\\/html>/)) {\n generatedByIdiomorph.add(content);\n return content;\n } else {\n // otherwise return the html element as the parent container\n let htmlElement = content.firstChild;\n if (htmlElement) {\n generatedByIdiomorph.add(htmlElement);\n }\n return htmlElement;\n }\n } else {\n // if it is partial HTML, wrap it in a template tag to provide a parent element and also to help\n // deal with touchy tags like tr, tbody, etc.\n let responseDoc = parser.parseFromString(\n \"\",\n \"text/html\",\n );\n let content = /** @type {HTMLTemplateElement} */ (\n responseDoc.body.querySelector(\"template\")\n ).content;\n generatedByIdiomorph.add(content);\n return content;\n }\n }\n\n return { normalizeElement, normalizeParent };\n })();\n\n //=============================================================================\n // This is what ends up becoming the Idiomorph global object\n //=============================================================================\n return {\n morph,\n defaults,\n };\n})();\n\nexport {Idiomorph};\n","\nconst topics: any = {}\nconst _async: any = {}\n\nexport const publish = (name, params) => {\n\t_async[name] = Object.assign({}, _async[name], params)\n\tif (topics[name])\n\t\ttopics[name].forEach(topic => topic(params))\n}\n\nexport const subscribe = (name, method) => {\n\ttopics[name] = topics[name] || []\n\ttopics[name].push(method)\n\tif (name in _async) {\n\t\tmethod(_async[name])\n\t}\n\treturn () => {\n\t\ttopics[name] = topics[name].filter( fn => fn != method )\n\t}\n}\n","import { safe, g, dup } from './utils'\nimport { Idiomorph } from 'idiomorph/dist/idiomorph.esm'\nimport { publish, subscribe } from './utils/pubsub'\n\nexport const Component = ({ name, module, dependencies, node, templates, signal, register }) => {\n\n\tlet tick\n\tlet preserve\t\t= []\n\n\tconst _model \t\t= module.model || {}\n\tconst initialState \t= (new Function( `return ${node.getAttribute('html-model') || '{}'}`))()\n\tconst tplid \t\t= node.getAttribute('tplid')\n\tconst scopeid \t\t= node.getAttribute('html-scopeid')\n\tconst tpl \t\t\t= templates[ tplid ]\n\tconst scope \t\t= g.scope[ scopeid ]\n\tconst model \t\t= dup(module?.model?.apply ? _model({ elm:node, initialState }) : _model)\n\tconst state \t\t= Object.assign({}, scope, model, initialState)\n\tconst view \t\t\t= module.view? module.view : (data) => data\n\n\tconst base = {\n\t\tname,\n\t\tmodel,\n\t\telm: node,\n\t\ttemplate: tpl.template,\n\t\tdependencies,\n\t\tpublish,\n\t\tsubscribe,\n\n\t\tmain(fn) {\n\t\t\tnode.addEventListener(':mount', fn)\n\t\t},\n\n\t\t/**\n\t\t * @State\n\t\t */\n\t\tstate : {\n\n\t\t\tprotected( list ) {\n\t\t\t\tif( list ) {\n\t\t\t\t\tpreserve = list\n\t\t\t\t} else {\n\t\t\t\t\treturn preserve\n\t\t\t\t}\n\t\t\t},\n\n\t\t\tsave(data) {\n\t\t\t\tif( data.constructor === Function ) {\n\t\t\t\t\tdata( state )\n\t\t\t\t} else {\n\t\t\t\t\tObject.assign(state, data)\n\t\t\t\t}\n\t\t\t},\n\n\t\t\tset( data ) {\n\n\t\t\t\tif (!document.body.contains(node)) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif( data.constructor === Function ) {\n\t\t\t\t\tdata(state)\n\t\t\t\t} else {\n\t\t\t\t\tObject.assign(state, data)\n\t\t\t\t}\n\n\t\t\t\tconst newstate = Object.assign({}, state, scope)\n\n\t\t\t\treturn new Promise((resolve) => {\n\t\t\t\t\trender(newstate, () => resolve(newstate))\n\t\t\t\t})\n\t\t\t},\n\n\t\t\tget() {\n\t\t\t\treturn Object.assign({}, state)\n\t\t\t}\n\t\t},\n\n\t\tdataset( target, name ) {\n\n\t\t\tconst el = name? target : node\n\t\t\tconst key = name? name : target\n\t\t\tconst value = el.dataset[key]\n\n\t\t\tif (value === 'true') return true\n\t\t\tif (value === 'false') return false\n\t\t\tif (!isNaN(value) && value.trim() !== '') return Number(value)\n\n\t\t\ttry {\n\t\t\t\treturn new Function('return (' + value + ')')()\n\t\t\t} catch {}\n\n\t\t\ttry {\n\t\t\t\treturn JSON.parse(value)\n\t\t\t} catch {}\n\n\t\t\treturn value\n\t\t},\n\n\t\tattr( target ) {\n\n\t\t\tlet callbacks = []\n\t\t\tconst elm = target || node\n\n\t\t\tconst observer = new MutationObserver((mutationsList) => {\n\t\t\t\tfor (const mutation of mutationsList) {\n\t\t\t\t\tif (mutation.type === 'attributes') {\n\t\t\t\t\t\tconst attributeName = mutation.attributeName\n\t\t\t\t\t\tcallbacks.forEach((item) => {\n\t\t\t\t\t\t\tif (item.name == attributeName) {\n\t\t\t\t\t\t\t\titem.callback(\n\t\t\t\t\t\t\t\t\tattributeName,\n\t\t\t\t\t\t\t\t\telm.getAttribute(attributeName)\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tobserver.observe(node, { attributes: true })\n\n\t\t\tnode.addEventListener(':unmount', () => {\n\t\t\t\tcallbacks = null\n\t\t\t\tobserver.disconnect()\n\t\t\t})\n\n\t\t\treturn {\n\n\t\t\t\tchange( name, callback ) {\n\t\t\t\t\tcallbacks.push({ name, callback })\n\t\t\t\t},\n\n\t\t\t\tdisconnect( callback ) {\n\t\t\t\t\tcallbacks = callbacks.filter((item) => item.callback !== callback)\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * @Events\n\t\t */\n\t\ton( ev, selectorOrCallback, callback ) {\n\n\t\t\tif( callback ) {\n\t\t\t\tcallback.handler = (e) => {\n\t\t\t\t\tconst detail = e.detail || {}\n\t\t\t\t\tlet parent = e.target\n\t\t\t\t\twhile (parent) {\n\t\t\t\t\t\tif (parent.matches(selectorOrCallback)) {\n\t\t\t\t\t\t\te.delegateTarget = parent\n\t\t\t\t\t\t\tcallback.apply(node, [e].concat(detail.args))\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (parent === node) break\n\t\t\t\t\t\tparent = parent.parentNode\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tnode.addEventListener(ev, callback.handler, {\n\t\t\t\t\tsignal,\n\t\t\t\t\tcapture: (ev == 'focus' || ev == 'blur' || ev == 'mouseenter' || ev == 'mouseleave')\n\t\t\t\t})\n\n\t\t\t} else {\n\t\t\t\tselectorOrCallback.handler = (e) => {\n\t\t\t\t\te.delegateTarget = node\n\t\t\t\t\tselectorOrCallback.apply(node, [e].concat(e.detail.args))\n\t\t\t\t}\n\t\t\t\tnode.addEventListener(ev, selectorOrCallback.handler, { signal })\n\t\t\t}\n\n\t\t},\n\n\t\toff( ev, callback ) {\n\t\t\tif( callback.handler ) {\n\t\t\t\tnode.removeEventListener(ev, callback.handler)\n\t\t\t}\n\t\t},\n\n\t\ttrigger(ev, selectorOrCallback, data) {\n\t\t\tif( selectorOrCallback.constructor === String ) {\n\t\t\t\tArray\n\t\t\t\t\t.from(node.querySelectorAll(selectorOrCallback))\n\t\t\t\t\t.forEach( children => {\n\t\t\t\t\t\tchildren.dispatchEvent(new CustomEvent(ev, { bubbles: true, detail: { args: data } }) )\n\t\t\t\t\t})\n\t\t\t} else {\n\t\t\t\tnode.dispatchEvent(new CustomEvent(ev, { bubbles: true, detail:{ args: data } }))\n\t\t\t}\n\t\t},\n\n\t\temit(ev, data) {\n\t\t\tnode.dispatchEvent(new CustomEvent(ev, { bubbles: true, detail: { args: data } }))\n\t\t},\n\n\t\tunmount( fn ) {\n\t\t\tnode.addEventListener(':unmount', fn)\n\t\t},\n\n\t\tinnerHTML ( target, html_ ) {\n\t\t\tconst element = html_? target : node\n\t\t\tconst clone = element.cloneNode()\n\t\t\tconst html = html_? html_ : target\n\t\t\tclone.innerHTML = html\n\t\t\tIdiomorph.morph(element, clone)\n\t\t}\n\t}\n\n\tconst render = ( data, callback = (() => {}) ) => {\n\t\tclearTimeout( tick )\n\t\ttick = setTimeout(() => {\n\t\t\tconst html = tpl.render.call({...data, ...view(data)}, node, safe, g )\n\t\t\tIdiomorph.morph( node, html, IdiomorphOptions(node, register) )\n\t\t\tPromise.resolve().then(() => {\n\t\t\t\tnode.querySelectorAll('[tplid]')\n\t\t\t\t\t.forEach((element) => {\n\t\t\t\t\t\tconst child = register.get(element)\n\t\t\t\t\t\tif(!child) return\n\t\t\t\t\t\tchild.state.protected().forEach( key => delete data[key] )\n\t\t\t\t\t\tchild.state.set(data)\n\t\t\t\t\t})\n\t\t\t\tPromise.resolve().then(() => {\n\t\t\t\t\tg.scope = {}\n\t\t\t\t\tcallback()\n\t\t\t\t})\n\t\t\t})\n\t\t})\n\t}\n\n\trender( state )\n\tregister.set( node, base )\n\treturn module.default( base )\n}\n\nconst IdiomorphOptions = ( parent, register ) => ({\n\tcallbacks: {\n\t\tbeforeNodeMorphed( node ) {\n\t\t\tif( node.nodeType === 1 ) {\n\t\t\t\tif( 'html-static' in node.attributes ) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\tif( register.get(node) && node !== parent ) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n})\n","import { Component } from './component'\n\nconst register = new WeakMap()\n\nexport const Element = ({ component, templates, start }) => {\n\n\tconst { name, module, dependencies } = component\n\n\treturn class extends HTMLElement {\n\n\t\tconstructor() {\n\t\t\tsuper()\n\t\t}\n\n\t\tconnectedCallback() {\n\n\t\t\tthis.abortController = new AbortController()\n\n\t\t\tif( !this.getAttribute('tplid') ) {\n\t\t\t\tstart( this.parentNode )\n\t\t\t}\n\n\t\t\tconst rtrn = Component({\n\t\t\t\tnode:this,\n\t\t\t\tname,\n\t\t\t\tmodule,\n\t\t\t\tdependencies,\n\t\t\t\ttemplates,\n\t\t\t\tsignal: this.abortController.signal,\n\t\t\t\tregister\n\t\t\t})\n\n\t\t\tif ( rtrn && rtrn.constructor === Promise ) {\n\t\t\t\trtrn.then(() => {\n\t\t\t\t\tthis.dispatchEvent( new CustomEvent(':mount') )\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\tthis.dispatchEvent( new CustomEvent(':mount') )\n\t\t\t}\n\t\t}\n\n\t\tdisconnectedCallback() {\n\t\t\tthis.dispatchEvent( new CustomEvent(':unmount') )\n\t\t\tthis.abortController.abort()\n\t\t}\n\t}\n}\n","import { uuid, decodeHTML } from './utils'\n\nconst templates = {}\n\nconst config = {\n\ttags: ['{{', '}}']\n}\n\nexport const templateConfig = (newconfig) => {\n\tObject.assign( config, newconfig )\n}\n\nexport const template = ( target, { components }) => {\n\n\ttagElements( target, [...Object.keys( components ), '[html-if]', 'template'], components )\n\tconst clone = target.cloneNode( true )\n\n\ttransformTemplate( clone )\n\tremoveTemplateTagsRecursively( clone )\n\tsetTemplates( clone, components )\n\n\treturn templates\n}\n\nexport const compile = ( html ) => {\n\n\tconst parsedHtml = JSON.stringify( html )\n\n\treturn new Function('$element', 'safe', '$g',`\n\t\tvar $data = this;\n\t\twith( $data ){\n\t\t\tvar output=${parsedHtml\n\t\t\t\t.replace(/%%_=(.+?)_%%/g, function(_, variable){\n\t\t\t\t\treturn '\"+safe(function(){return '+ decodeHTML(variable) +';})+\"'\n\t\t\t\t})\n\t\t\t\t.replace(/%%_(.+?)_%%/g, function(_, variable){\n\t\t\t\t\treturn '\";' + decodeHTML(variable) +'\\noutput+=\"'\n\t\t\t\t})};return output;\n\t\t}\n\t`)\n}\n\nconst tagElements = ( target, keys, components ) => {\n\ttarget\n\t\t.querySelectorAll( keys.toString() )\n\t\t.forEach((node) => {\n\t\t\tconst name = node.localName\n\t\t\tif( name === 'template' ) {\n\t\t\t\treturn tagElements( node.content, keys, components )\n\t\t\t}\n\t\t\tif( node.getAttribute('html-if') && !node.id ) {\n\t\t\t\tnode.id = uuid()\n\t\t\t}\n\t\t\tif( name in components ) {\n\t\t\t\tnode.setAttribute('tplid', uuid())\n\t\t\t}\n\t\t})\n}\n\nconst transformAttributes = ( html ) => {\n\n\tconst regexTags = new RegExp(`\\\\${config.tags[0]}(.+?)\\\\${config.tags[1]}`, 'g')\n\n\treturn html\n\t\t.replace(/jails___scope-id/g, '%%_=$scopeid_%%')\n\t\t.replace(regexTags, '%%_=$1_%%')\n\t\t// Booleans\n\t\t// https://meiert.com/en/blog/boolean-attributes-of-html/\n\t\t.replace(/html-(allowfullscreen|async|autofocus|autoplay|checked|controls|default|defer|disabled|formnovalidate|inert|ismap|itemscope|loop|multiple|muted|nomodule|novalidate|open|playsinline|readonly|required|reversed|selected)=\\\"(.*?)\\\"/g, `%%_if(safe(function(){ return $2 })){_%%$1%%_}_%%`)\n\t\t// The rest\n\t\t.replace(/html-([^\\s]*?)=\\\"(.*?)\\\"/g, (all, key, value) => {\n\t\t\tif (key === 'key' || key === 'model' || key === 'scopeid' ) {\n\t\t\t\treturn all\n\t\t\t}\n\t\t\tif (value) {\n\t\t\t\tvalue = value.replace(/^{|}$/g, '')\n\t\t\t\treturn `${key}=\"%%_=safe(function(){ return ${value} })_%%\"`\n\t\t\t} else {\n\t\t\t\treturn all\n\t\t\t}\n\t\t})\n}\n\nconst transformTemplate = ( clone ) => {\n\n\tclone.querySelectorAll('template, [html-for], [html-if], [html-inner], [html-class]')\n\t\t.forEach(( element ) => {\n\n\t\t\tconst htmlFor \t= element.getAttribute('html-for')\n\t\t\tconst htmlIf \t= element.getAttribute('html-if')\n\t\t\tconst htmlInner = element.getAttribute('html-inner')\n\t\t\tconst htmlClass = element.getAttribute('html-class')\n\n\t\t\tif ( htmlFor ) {\n\n\t\t\t\telement.removeAttribute('html-for')\n\n\t\t\t\tconst split \t = htmlFor.match(/(.*)\\sin\\s(.*)/) || ''\n\t\t\t\tconst varname \t = split[1]\n\t\t\t\tconst object \t = split[2]\n\t\t\t\tconst objectname = object.split(/\\./).shift()\n\t\t\t\tconst open \t\t = document.createTextNode(`%%_ ;(function(){ var $index = 0; for(var $key in safe(function(){ return ${object} }) ){ var $scopeid = Math.random().toString(36).substring(2, 9); var ${varname} = ${object}[$key]; $g.scope[$scopeid] = Object.assign({}, { ${objectname}: ${objectname} }, { ${varname} :${varname}, $index: $index, $key: $key }); _%%`)\n\t\t\t\tconst close \t = document.createTextNode(`%%_ $index++; } })() _%%`)\n\n\t\t\t\twrap(open, element, close)\n\t\t\t}\n\n\t\t\tif (htmlIf) {\n\t\t\t\telement.removeAttribute('html-if')\n\t\t\t\tconst open = document.createTextNode(`%%_ if ( safe(function(){ return ${htmlIf} }) ){ _%%`)\n\t\t\t\tconst close = document.createTextNode(`%%_ } _%%`)\n\t\t\t\twrap(open, element, close)\n\t\t\t}\n\n\t\t\tif (htmlInner) {\n\t\t\t\telement.removeAttribute('html-inner')\n\t\t\t\telement.innerHTML = `%%_=${htmlInner}_%%`\n\t\t\t}\n\n\t\t\tif (htmlClass) {\n\t\t\t\telement.removeAttribute('html-class')\n\t\t\t\telement.className = (element.className + ` %%_=${htmlClass}_%%`).trim()\n\t\t\t}\n\n\t\t\tif( element.localName === 'template' ) {\n\t\t\t\ttransformTemplate(element.content)\n\t\t\t}\n\t\t})\n}\n\nconst setTemplates = ( clone, components ) => {\n\n\tArray.from(clone.querySelectorAll('[tplid]'))\n\t\t.reverse()\n\t\t.forEach((node) => {\n\n\t\t\tconst tplid = node.getAttribute('tplid')\n\t\t\tconst name = node.localName\n\t\t\tnode.setAttribute('html-scopeid', 'jails___scope-id')\n\n\t\t\tif( name in components && components[name].module.template ) {\n\t\t\t\tconst children = node.innerHTML\n\t\t\t\tconst html = components[name].module.template({ elm:node, children })\n\t\t\t\tnode.innerHTML = html\n\t\t\t\ttransformTemplate(node)\n\t\t\t\tremoveTemplateTagsRecursively(node)\n\t\t\t}\n\n\t\t\tconst html = transformAttributes(node.outerHTML)\n\n\t\t\ttemplates[ tplid ] = {\n\t\t\t\ttemplate: html,\n\t\t\t\trender\t: compile(html)\n\t\t\t}\n\t\t})\n}\n\nconst removeTemplateTagsRecursively = (node) => {\n\n\t// Get all