Comparing version 0.2.0 to 0.3.0
@@ -1,299 +0,334 @@ | ||
//============================================================================= | ||
// UMD insanity... i hate javascript so much | ||
// | ||
// IGNORE EVERYTHING FROM HERE UNTIL THE COMMENT SAYING 'AND NOW IT BEGINS..." | ||
//============================================================================= | ||
(function (root, factory) { | ||
//@ts-ignore | ||
if (typeof define === 'function' && define.amd) { | ||
// AMD. Register as an anonymous module. | ||
//@ts-ignore | ||
define([], factory); | ||
} else if (typeof module === 'object' && module.exports) { | ||
// Node. Does not work with strict CommonJS, but | ||
// only CommonJS-like environments that support module.exports, | ||
// like Node. | ||
module.exports = factory(); | ||
} else { | ||
// Browser globals | ||
root.Idiomorph = root.Idiomorph || factory(); | ||
} | ||
}(typeof self !== 'undefined' ? self : this, | ||
function () { | ||
return (function () { | ||
'use strict'; | ||
// base IIFE to define idiomorph | ||
var Idiomorph = (function () { | ||
'use strict'; | ||
//============================================================================= | ||
// AND NOW IT BEGINS... | ||
//============================================================================= | ||
let EMPTY_SET = new Set(); | ||
//============================================================================= | ||
// AND NOW IT BEGINS... | ||
//============================================================================= | ||
let EMPTY_SET = new Set(); | ||
//============================================================================= | ||
// Core Morphing Algorithm - morph, morphNormalizedContent, morphOldNodeTo, morphChildren | ||
//============================================================================= | ||
function morph(oldNode, newContent, config = {}) { | ||
// default configuration values, updatable by users now | ||
let defaults = { | ||
morphStyle: "outerHTML", | ||
callbacks : { | ||
beforeNodeAdded: noOp, | ||
afterNodeAdded: noOp, | ||
beforeNodeMorphed: noOp, | ||
afterNodeMorphed: noOp, | ||
beforeNodeRemoved: noOp, | ||
afterNodeRemoved: noOp, | ||
beforeAttributeUpdated: noOp, | ||
if (oldNode instanceof Document) { | ||
oldNode = oldNode.documentElement; | ||
} | ||
}, | ||
head: { | ||
style: 'merge', | ||
shouldPreserve: function (elt) { | ||
return elt.getAttribute("im-preserve") === "true"; | ||
}, | ||
shouldReAppend: function (elt) { | ||
return elt.getAttribute("im-re-append") === "true"; | ||
}, | ||
shouldRemove: noOp, | ||
afterHeadMorphed: noOp, | ||
} | ||
}; | ||
if (typeof newContent === 'string') { | ||
newContent = parseContent(newContent); | ||
} | ||
//============================================================================= | ||
// Core Morphing Algorithm - morph, morphNormalizedContent, morphOldNodeTo, morphChildren | ||
//============================================================================= | ||
function morph(oldNode, newContent, config = {}) { | ||
let normalizedContent = normalizeContent(newContent); | ||
if (oldNode instanceof Document) { | ||
oldNode = oldNode.documentElement; | ||
} | ||
let ctx = createMorphContext(oldNode, normalizedContent, config); | ||
return morphNormalizedContent(oldNode, normalizedContent, ctx); | ||
if (typeof newContent === 'string') { | ||
newContent = parseContent(newContent); | ||
} | ||
function morphNormalizedContent(oldNode, normalizedNewContent, ctx) { | ||
if (ctx.head.block) { | ||
let oldHead = oldNode.querySelector('head'); | ||
let newHead = normalizedNewContent.querySelector('head'); | ||
if (oldHead && newHead) { | ||
let promises = handleHeadElement(newHead, oldHead, ctx); | ||
// when head promises resolve, call morph again, ignoring the head tag | ||
Promise.all(promises).then(function () { | ||
morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, { | ||
head: { | ||
block: false, | ||
ignore: true | ||
} | ||
})); | ||
}); | ||
return; | ||
} | ||
let normalizedContent = normalizeContent(newContent); | ||
let ctx = createMorphContext(oldNode, normalizedContent, config); | ||
return morphNormalizedContent(oldNode, normalizedContent, ctx); | ||
} | ||
function morphNormalizedContent(oldNode, normalizedNewContent, ctx) { | ||
if (ctx.head.block) { | ||
let oldHead = oldNode.querySelector('head'); | ||
let newHead = normalizedNewContent.querySelector('head'); | ||
if (oldHead && newHead) { | ||
let promises = handleHeadElement(newHead, oldHead, ctx); | ||
// when head promises resolve, call morph again, ignoring the head tag | ||
Promise.all(promises).then(function () { | ||
morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, { | ||
head: { | ||
block: false, | ||
ignore: true | ||
} | ||
})); | ||
}); | ||
return; | ||
} | ||
} | ||
if (ctx.morphStyle === "innerHTML") { | ||
if (ctx.morphStyle === "innerHTML") { | ||
// innerHTML, so we are only updating the children | ||
morphChildren(normalizedNewContent, oldNode, ctx); | ||
return oldNode.children; | ||
// innerHTML, so we are only updating the children | ||
morphChildren(normalizedNewContent, oldNode, ctx); | ||
return oldNode.children; | ||
} else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) { | ||
// otherwise find the best element match in the new content, morph that, and merge its siblings | ||
// into either side of the best match | ||
let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx); | ||
} else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) { | ||
// otherwise find the best element match in the new content, morph that, and merge its siblings | ||
// into either side of the best match | ||
let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx); | ||
// stash the siblings that will need to be inserted on either side of the best match | ||
let previousSibling = bestMatch?.previousSibling; | ||
let nextSibling = bestMatch?.nextSibling; | ||
// stash the siblings that will need to be inserted on either side of the best match | ||
let previousSibling = bestMatch?.previousSibling; | ||
let nextSibling = bestMatch?.nextSibling; | ||
// morph it | ||
let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx); | ||
// morph it | ||
let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx); | ||
if (bestMatch) { | ||
// if there was a best match, merge the siblings in too and return the | ||
// whole bunch | ||
return insertSiblings(previousSibling, morphedNode, nextSibling); | ||
} else { | ||
// otherwise nothing was added to the DOM | ||
return [] | ||
} | ||
if (bestMatch) { | ||
// if there was a best match, merge the siblings in too and return the | ||
// whole bunch | ||
return insertSiblings(previousSibling, morphedNode, nextSibling); | ||
} else { | ||
throw "Do not understand how to morph style " + ctx.morphStyle; | ||
// otherwise nothing was added to the DOM | ||
return [] | ||
} | ||
} else { | ||
throw "Do not understand how to morph style " + ctx.morphStyle; | ||
} | ||
} | ||
/** | ||
* @param possibleActiveElement | ||
* @param ctx | ||
* @returns {boolean} | ||
*/ | ||
function ignoreValueOfActiveElement(possibleActiveElement, ctx) { | ||
return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement; | ||
} | ||
/** | ||
* @param possibleActiveElement | ||
* @param ctx | ||
* @returns {boolean} | ||
*/ | ||
function ignoreValueOfActiveElement(possibleActiveElement, ctx) { | ||
return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement; | ||
} | ||
/** | ||
* @param oldNode root node to merge content into | ||
* @param newContent new content to merge | ||
* @param ctx the merge context | ||
* @returns {Element} the element that ended up in the DOM | ||
*/ | ||
function morphOldNodeTo(oldNode, newContent, ctx) { | ||
if (ctx.ignoreActive && oldNode === document.activeElement) { | ||
// don't morph focused element | ||
} else if (newContent == null) { | ||
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return; | ||
/** | ||
* @param oldNode root node to merge content into | ||
* @param newContent new content to merge | ||
* @param ctx the merge context | ||
* @returns {Element} the element that ended up in the DOM | ||
*/ | ||
function morphOldNodeTo(oldNode, newContent, ctx) { | ||
if (ctx.ignoreActive && oldNode === document.activeElement) { | ||
// don't morph focused element | ||
} else if (newContent == null) { | ||
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode; | ||
oldNode.remove(); | ||
ctx.callbacks.afterNodeRemoved(oldNode); | ||
return null; | ||
} else if (!isSoftMatch(oldNode, newContent)) { | ||
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return; | ||
if (ctx.callbacks.beforeNodeAdded(newContent) === false) return; | ||
oldNode.remove(); | ||
ctx.callbacks.afterNodeRemoved(oldNode); | ||
return null; | ||
} else if (!isSoftMatch(oldNode, newContent)) { | ||
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode; | ||
if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode; | ||
oldNode.parentElement.replaceChild(newContent, oldNode); | ||
ctx.callbacks.afterNodeAdded(newContent); | ||
ctx.callbacks.afterNodeRemoved(oldNode); | ||
return newContent; | ||
oldNode.parentElement.replaceChild(newContent, oldNode); | ||
ctx.callbacks.afterNodeAdded(newContent); | ||
ctx.callbacks.afterNodeRemoved(oldNode); | ||
return newContent; | ||
} else { | ||
if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return oldNode; | ||
if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) { | ||
// ignore the head element | ||
} else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") { | ||
handleHeadElement(newContent, oldNode, ctx); | ||
} else { | ||
if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return; | ||
if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) { | ||
// ignore the head element | ||
} else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") { | ||
handleHeadElement(newContent, oldNode, ctx); | ||
} else { | ||
syncNodeFrom(newContent, oldNode, ctx); | ||
if (!ignoreValueOfActiveElement(oldNode, ctx)) { | ||
morphChildren(newContent, oldNode, ctx); | ||
} | ||
syncNodeFrom(newContent, oldNode, ctx); | ||
if (!ignoreValueOfActiveElement(oldNode, ctx)) { | ||
morphChildren(newContent, oldNode, ctx); | ||
} | ||
ctx.callbacks.afterNodeMorphed(oldNode, newContent); | ||
return oldNode; | ||
} | ||
ctx.callbacks.afterNodeMorphed(oldNode, newContent); | ||
return oldNode; | ||
} | ||
} | ||
/** | ||
* This is the core algorithm for matching up children. The idea is to use id sets to try to match up | ||
* nodes as faithfully as possible. We greedily match, which allows us to keep the algorithm fast, but | ||
* by using id sets, we are able to better match up with content deeper in the DOM. | ||
* | ||
* Basic algorithm is, for each node in the new content: | ||
* | ||
* - if we have reached the end of the old parent, append the new content | ||
* - if the new content has an id set match with the current insertion point, morph | ||
* - search for an id set match | ||
* - if id set match found, morph | ||
* - otherwise search for a "soft" match | ||
* - if a soft match is found, morph | ||
* - otherwise, prepend the new node before the current insertion point | ||
* | ||
* The two search algorithms terminate if competing node matches appear to outweigh what can be achieved | ||
* with the current node. See findIdSetMatch() and findSoftMatch() for details. | ||
* | ||
* @param {Element} newParent the parent element of the new content | ||
* @param {Element } oldParent the old content that we are merging the new content into | ||
* @param ctx the merge context | ||
*/ | ||
function morphChildren(newParent, oldParent, ctx) { | ||
/** | ||
* This is the core algorithm for matching up children. The idea is to use id sets to try to match up | ||
* nodes as faithfully as possible. We greedily match, which allows us to keep the algorithm fast, but | ||
* by using id sets, we are able to better match up with content deeper in the DOM. | ||
* | ||
* Basic algorithm is, for each node in the new content: | ||
* | ||
* - if we have reached the end of the old parent, append the new content | ||
* - if the new content has an id set match with the current insertion point, morph | ||
* - search for an id set match | ||
* - if id set match found, morph | ||
* - otherwise search for a "soft" match | ||
* - if a soft match is found, morph | ||
* - otherwise, prepend the new node before the current insertion point | ||
* | ||
* The two search algorithms terminate if competing node matches appear to outweigh what can be achieved | ||
* with the current node. See findIdSetMatch() and findSoftMatch() for details. | ||
* | ||
* @param {Element} newParent the parent element of the new content | ||
* @param {Element } oldParent the old content that we are merging the new content into | ||
* @param ctx the merge context | ||
*/ | ||
function morphChildren(newParent, oldParent, ctx) { | ||
let nextNewChild = newParent.firstChild; | ||
let insertionPoint = oldParent.firstChild; | ||
let newChild; | ||
let nextNewChild = newParent.firstChild; | ||
let insertionPoint = oldParent.firstChild; | ||
let newChild; | ||
// run through all the new content | ||
while (nextNewChild) { | ||
// run through all the new content | ||
while (nextNewChild) { | ||
newChild = nextNewChild; | ||
nextNewChild = newChild.nextSibling; | ||
newChild = nextNewChild; | ||
nextNewChild = newChild.nextSibling; | ||
// if we are at the end of the exiting parent's children, just append | ||
if (insertionPoint == null) { | ||
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return; | ||
// if we are at the end of the exiting parent's children, just append | ||
if (insertionPoint == null) { | ||
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return; | ||
oldParent.appendChild(newChild); | ||
ctx.callbacks.afterNodeAdded(newChild); | ||
removeIdsFromConsideration(ctx, newChild); | ||
continue; | ||
} | ||
oldParent.appendChild(newChild); | ||
ctx.callbacks.afterNodeAdded(newChild); | ||
removeIdsFromConsideration(ctx, newChild); | ||
continue; | ||
} | ||
// if the current node has an id set match then morph | ||
if (isIdSetMatch(newChild, insertionPoint, ctx)) { | ||
morphOldNodeTo(insertionPoint, newChild, ctx); | ||
insertionPoint = insertionPoint.nextSibling; | ||
removeIdsFromConsideration(ctx, newChild); | ||
continue; | ||
} | ||
// if the current node has an id set match then morph | ||
if (isIdSetMatch(newChild, insertionPoint, ctx)) { | ||
morphOldNodeTo(insertionPoint, newChild, ctx); | ||
insertionPoint = insertionPoint.nextSibling; | ||
removeIdsFromConsideration(ctx, newChild); | ||
continue; | ||
} | ||
// otherwise search forward in the existing old children for an id set match | ||
let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx); | ||
// otherwise search forward in the existing old children for an id set match | ||
let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx); | ||
// if we found a potential match, remove the nodes until that point and morph | ||
if (idSetMatch) { | ||
insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx); | ||
morphOldNodeTo(idSetMatch, newChild, ctx); | ||
removeIdsFromConsideration(ctx, newChild); | ||
continue; | ||
} | ||
// if we found a potential match, remove the nodes until that point and morph | ||
if (idSetMatch) { | ||
insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx); | ||
morphOldNodeTo(idSetMatch, newChild, ctx); | ||
removeIdsFromConsideration(ctx, newChild); | ||
continue; | ||
} | ||
// no id set match found, so scan forward for a soft match for the current node | ||
let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx); | ||
// no id set match found, so scan forward for a soft match for the current node | ||
let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx); | ||
// if we found a soft match for the current node, morph | ||
if (softMatch) { | ||
insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx); | ||
morphOldNodeTo(softMatch, newChild, ctx); | ||
removeIdsFromConsideration(ctx, newChild); | ||
continue; | ||
} | ||
// abandon all hope of morphing, just insert the new child before the insertion point | ||
// and move on | ||
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return; | ||
oldParent.insertBefore(newChild, insertionPoint); | ||
ctx.callbacks.afterNodeAdded(newChild); | ||
// if we found a soft match for the current node, morph | ||
if (softMatch) { | ||
insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx); | ||
morphOldNodeTo(softMatch, newChild, ctx); | ||
removeIdsFromConsideration(ctx, newChild); | ||
continue; | ||
} | ||
// remove any remaining old nodes that didn't match up with new content | ||
while (insertionPoint !== null) { | ||
// abandon all hope of morphing, just insert the new child before the insertion point | ||
// and move on | ||
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return; | ||
let tempNode = insertionPoint; | ||
insertionPoint = insertionPoint.nextSibling; | ||
removeNode(tempNode, ctx); | ||
} | ||
oldParent.insertBefore(newChild, insertionPoint); | ||
ctx.callbacks.afterNodeAdded(newChild); | ||
removeIdsFromConsideration(ctx, newChild); | ||
} | ||
//============================================================================= | ||
// Attribute Syncing Code | ||
//============================================================================= | ||
// remove any remaining old nodes that didn't match up with new content | ||
while (insertionPoint !== null) { | ||
/** | ||
* syncs a given node with another node, copying over all attributes and | ||
* inner element state from the 'from' node to the 'to' node | ||
* | ||
* @param {Element} from the element to copy attributes & state from | ||
* @param {Element} to the element to copy attributes & state to | ||
*/ | ||
function syncNodeFrom(from, to, ctx) { | ||
let type = from.nodeType | ||
let tempNode = insertionPoint; | ||
insertionPoint = insertionPoint.nextSibling; | ||
removeNode(tempNode, ctx); | ||
} | ||
} | ||
// if is an element type, sync the attributes from the | ||
// new node into the new node | ||
if (type === 1 /* element type */) { | ||
const fromAttributes = from.attributes; | ||
const toAttributes = to.attributes; | ||
for (const fromAttribute of fromAttributes) { | ||
if (fromAttribute.name === 'value' && ignoreValueOfActiveElement(to, ctx)) { | ||
continue; | ||
} | ||
if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) { | ||
to.setAttribute(fromAttribute.name, fromAttribute.value); | ||
} | ||
//============================================================================= | ||
// Attribute Syncing Code | ||
//============================================================================= | ||
/** | ||
* @param attr {String} the attribute to be mutated | ||
* @param to {Element} the element that is going to be updated | ||
* @param updateType {("update"|"remove")} | ||
* @param ctx the merge context | ||
* @returns {boolean} true if the attribute should be ignored, false otherwise | ||
*/ | ||
function ignoreAttribute(attr, to, updateType, ctx) { | ||
if(attr === 'value' && ctx.ignoreActiveValue && to === document.activeElement){ | ||
return true; | ||
} | ||
return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false; | ||
} | ||
/** | ||
* syncs a given node with another node, copying over all attributes and | ||
* inner element state from the 'from' node to the 'to' node | ||
* | ||
* @param {Element} from the element to copy attributes & state from | ||
* @param {Element} to the element to copy attributes & state to | ||
* @param ctx the merge context | ||
*/ | ||
function syncNodeFrom(from, to, ctx) { | ||
let type = from.nodeType | ||
// if is an element type, sync the attributes from the | ||
// new node into the new node | ||
if (type === 1 /* element type */) { | ||
const fromAttributes = from.attributes; | ||
const toAttributes = to.attributes; | ||
for (const fromAttribute of fromAttributes) { | ||
if (ignoreAttribute(fromAttribute.name, to, 'update', ctx)) { | ||
continue; | ||
} | ||
// iterate backwards to avoid skipping over items when a delete occurs | ||
for (let i = toAttributes.length - 1; 0 <= i; i--) { | ||
const toAttribute = toAttributes[i]; | ||
if (!from.hasAttribute(toAttribute.name)) { | ||
to.removeAttribute(toAttribute.name); | ||
} | ||
if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) { | ||
to.setAttribute(fromAttribute.name, fromAttribute.value); | ||
} | ||
} | ||
// sync text nodes | ||
if (type === 8 /* comment */ || type === 3 /* text */) { | ||
if (to.nodeValue !== from.nodeValue) { | ||
to.nodeValue = from.nodeValue; | ||
// iterate backwards to avoid skipping over items when a delete occurs | ||
for (let i = toAttributes.length - 1; 0 <= i; i--) { | ||
const toAttribute = toAttributes[i]; | ||
if (ignoreAttribute(toAttribute.name, to, 'remove', ctx)) { | ||
continue; | ||
} | ||
if (!from.hasAttribute(toAttribute.name)) { | ||
to.removeAttribute(toAttribute.name); | ||
} | ||
} | ||
} | ||
if (!ignoreValueOfActiveElement(to, ctx)) { | ||
// sync input values | ||
syncInputValue(from, to); | ||
// sync text nodes | ||
if (type === 8 /* comment */ || type === 3 /* text */) { | ||
if (to.nodeValue !== from.nodeValue) { | ||
to.nodeValue = from.nodeValue; | ||
} | ||
} | ||
function syncBooleanAttribute(from, to, attributeName) { | ||
if (from[attributeName] !== to[attributeName]) { | ||
if (!ignoreValueOfActiveElement(to, ctx)) { | ||
// sync input values | ||
syncInputValue(from, to, ctx); | ||
} | ||
} | ||
/** | ||
* @param from {Element} element to sync the value from | ||
* @param to {Element} element to sync the value to | ||
* @param attributeName {String} the attribute name | ||
* @param ctx the merge context | ||
*/ | ||
function syncBooleanAttribute(from, to, attributeName, ctx) { | ||
if (from[attributeName] !== to[attributeName]) { | ||
let ignoreUpdate = ignoreAttribute(attributeName, to, 'update', ctx); | ||
if (!ignoreUpdate) { | ||
to[attributeName] = from[attributeName]; | ||
if (from[attributeName]) { | ||
} | ||
if (from[attributeName]) { | ||
if (!ignoreUpdate) { | ||
to.setAttribute(attributeName, from[attributeName]); | ||
} else { | ||
} | ||
} else { | ||
if (!ignoreAttribute(attributeName, to, 'remove', ctx)) { | ||
to.removeAttribute(attributeName); | ||
@@ -303,505 +338,536 @@ } | ||
} | ||
} | ||
// NB: many bothans died to bring us information: | ||
// | ||
// https://github.com/patrick-steele-idem/morphdom/blob/master/src/specialElHandlers.js | ||
// https://github.com/choojs/nanomorph/blob/master/lib/morph.jsL113 | ||
function syncInputValue(from, to) { | ||
if (from instanceof HTMLInputElement && | ||
to instanceof HTMLInputElement && | ||
from.type !== 'file') { | ||
/** | ||
* NB: many bothans died to bring us information: | ||
* | ||
* https://github.com/patrick-steele-idem/morphdom/blob/master/src/specialElHandlers.js | ||
* https://github.com/choojs/nanomorph/blob/master/lib/morph.jsL113 | ||
* | ||
* @param from {Element} the element to sync the input value from | ||
* @param to {Element} the element to sync the input value to | ||
* @param ctx the merge context | ||
*/ | ||
function syncInputValue(from, to, ctx) { | ||
if (from instanceof HTMLInputElement && | ||
to instanceof HTMLInputElement && | ||
from.type !== 'file') { | ||
let fromValue = from.value; | ||
let toValue = to.value; | ||
let fromValue = from.value; | ||
let toValue = to.value; | ||
// sync boolean attributes | ||
syncBooleanAttribute(from, to, 'checked'); | ||
syncBooleanAttribute(from, to, 'disabled'); | ||
// sync boolean attributes | ||
syncBooleanAttribute(from, to, 'checked', ctx); | ||
syncBooleanAttribute(from, to, 'disabled', ctx); | ||
if (!from.hasAttribute('value')) { | ||
if (!from.hasAttribute('value')) { | ||
if (!ignoreAttribute('value', to, 'remove', ctx)) { | ||
to.value = ''; | ||
to.removeAttribute('value'); | ||
} else if (fromValue !== toValue) { | ||
} | ||
} else if (fromValue !== toValue) { | ||
if (!ignoreAttribute('value', to, 'update', ctx)) { | ||
to.setAttribute('value', fromValue); | ||
to.value = fromValue; | ||
} | ||
} else if (from instanceof HTMLOptionElement) { | ||
syncBooleanAttribute(from, to, 'selected') | ||
} else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) { | ||
let fromValue = from.value; | ||
let toValue = to.value; | ||
if (fromValue !== toValue) { | ||
to.value = fromValue; | ||
} | ||
if (to.firstChild && to.firstChild.nodeValue !== fromValue) { | ||
to.firstChild.nodeValue = fromValue | ||
} | ||
} | ||
} else if (from instanceof HTMLOptionElement) { | ||
syncBooleanAttribute(from, to, 'selected', ctx) | ||
} else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) { | ||
let fromValue = from.value; | ||
let toValue = to.value; | ||
if (ignoreAttribute('value', to, 'update', ctx)) { | ||
return; | ||
} | ||
if (fromValue !== toValue) { | ||
to.value = fromValue; | ||
} | ||
if (to.firstChild && to.firstChild.nodeValue !== fromValue) { | ||
to.firstChild.nodeValue = fromValue | ||
} | ||
} | ||
} | ||
//============================================================================= | ||
// the HEAD tag can be handled specially, either w/ a 'merge' or 'append' style | ||
//============================================================================= | ||
function handleHeadElement(newHeadTag, currentHead, ctx) { | ||
//============================================================================= | ||
// the HEAD tag can be handled specially, either w/ a 'merge' or 'append' style | ||
//============================================================================= | ||
function handleHeadElement(newHeadTag, currentHead, ctx) { | ||
let added = [] | ||
let removed = [] | ||
let preserved = [] | ||
let nodesToAppend = [] | ||
let added = [] | ||
let removed = [] | ||
let preserved = [] | ||
let nodesToAppend = [] | ||
let headMergeStyle = ctx.head.style; | ||
let headMergeStyle = ctx.head.style; | ||
// put all new head elements into a Map, by their outerHTML | ||
let srcToNewHeadNodes = new Map(); | ||
for (const newHeadChild of newHeadTag.children) { | ||
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild); | ||
} | ||
// put all new head elements into a Map, by their outerHTML | ||
let srcToNewHeadNodes = new Map(); | ||
for (const newHeadChild of newHeadTag.children) { | ||
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild); | ||
} | ||
// for each elt in the current head | ||
for (const currentHeadElt of currentHead.children) { | ||
// for each elt in the current head | ||
for (const currentHeadElt of currentHead.children) { | ||
// If the current head element is in the map | ||
let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML); | ||
let isReAppended = ctx.head.shouldReAppend(currentHeadElt); | ||
let isPreserved = ctx.head.shouldPreserve(currentHeadElt); | ||
if (inNewContent || isPreserved) { | ||
// If the current head element is in the map | ||
let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML); | ||
let isReAppended = ctx.head.shouldReAppend(currentHeadElt); | ||
let isPreserved = ctx.head.shouldPreserve(currentHeadElt); | ||
if (inNewContent || isPreserved) { | ||
if (isReAppended) { | ||
// remove the current version and let the new version replace it and re-execute | ||
removed.push(currentHeadElt); | ||
} else { | ||
// this element already exists and should not be re-appended, so remove it from | ||
// the new content map, preserving it in the DOM | ||
srcToNewHeadNodes.delete(currentHeadElt.outerHTML); | ||
preserved.push(currentHeadElt); | ||
} | ||
} else { | ||
if (headMergeStyle === "append") { | ||
// we are appending and this existing element is not new content | ||
// so if and only if it is marked for re-append do we do anything | ||
if (isReAppended) { | ||
// remove the current version and let the new version replace it and re-execute | ||
removed.push(currentHeadElt); | ||
} else { | ||
// this element already exists and should not be re-appended, so remove it from | ||
// the new content map, preserving it in the DOM | ||
srcToNewHeadNodes.delete(currentHeadElt.outerHTML); | ||
preserved.push(currentHeadElt); | ||
nodesToAppend.push(currentHeadElt); | ||
} | ||
} else { | ||
if (headMergeStyle === "append") { | ||
// we are appending and this existing element is not new content | ||
// so if and only if it is marked for re-append do we do anything | ||
if (isReAppended) { | ||
removed.push(currentHeadElt); | ||
nodesToAppend.push(currentHeadElt); | ||
} | ||
} else { | ||
// if this is a merge, we remove this content since it is not in the new head | ||
if (ctx.head.shouldRemove(currentHeadElt) !== false) { | ||
removed.push(currentHeadElt); | ||
} | ||
// if this is a merge, we remove this content since it is not in the new head | ||
if (ctx.head.shouldRemove(currentHeadElt) !== false) { | ||
removed.push(currentHeadElt); | ||
} | ||
} | ||
} | ||
} | ||
// Push the remaining new head elements in the Map into the | ||
// nodes to append to the head tag | ||
nodesToAppend.push(...srcToNewHeadNodes.values()); | ||
log("to append: ", nodesToAppend); | ||
// Push the remaining new head elements in the Map into the | ||
// nodes to append to the head tag | ||
nodesToAppend.push(...srcToNewHeadNodes.values()); | ||
log("to append: ", nodesToAppend); | ||
let promises = []; | ||
for (const newNode of nodesToAppend) { | ||
log("adding: ", newNode); | ||
let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild; | ||
log(newElt); | ||
if (ctx.callbacks.beforeNodeAdded(newElt) !== false) { | ||
if (newElt.href || newElt.src) { | ||
let resolve = null; | ||
let promise = new Promise(function (_resolve) { | ||
resolve = _resolve; | ||
}); | ||
newElt.addEventListener('load',function() { | ||
resolve(); | ||
}); | ||
promises.push(promise); | ||
} | ||
currentHead.appendChild(newElt); | ||
ctx.callbacks.afterNodeAdded(newElt); | ||
added.push(newElt); | ||
let promises = []; | ||
for (const newNode of nodesToAppend) { | ||
log("adding: ", newNode); | ||
let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild; | ||
log(newElt); | ||
if (ctx.callbacks.beforeNodeAdded(newElt) !== false) { | ||
if (newElt.href || newElt.src) { | ||
let resolve = null; | ||
let promise = new Promise(function (_resolve) { | ||
resolve = _resolve; | ||
}); | ||
newElt.addEventListener('load', function () { | ||
resolve(); | ||
}); | ||
promises.push(promise); | ||
} | ||
currentHead.appendChild(newElt); | ||
ctx.callbacks.afterNodeAdded(newElt); | ||
added.push(newElt); | ||
} | ||
} | ||
// remove all removed elements, after we have appended the new elements to avoid | ||
// additional network requests for things like style sheets | ||
for (const removedElement of removed) { | ||
if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) { | ||
currentHead.removeChild(removedElement); | ||
ctx.callbacks.afterNodeRemoved(removedElement); | ||
} | ||
// remove all removed elements, after we have appended the new elements to avoid | ||
// additional network requests for things like style sheets | ||
for (const removedElement of removed) { | ||
if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) { | ||
currentHead.removeChild(removedElement); | ||
ctx.callbacks.afterNodeRemoved(removedElement); | ||
} | ||
ctx.head.afterHeadMorphed(currentHead, {added: added, kept: preserved, removed: removed}); | ||
return promises; | ||
} | ||
//============================================================================= | ||
// Misc | ||
//============================================================================= | ||
ctx.head.afterHeadMorphed(currentHead, {added: added, kept: preserved, removed: removed}); | ||
return promises; | ||
} | ||
function log() { | ||
//console.log(arguments); | ||
} | ||
//============================================================================= | ||
// Misc | ||
//============================================================================= | ||
function noOp() {} | ||
function log() { | ||
//console.log(arguments); | ||
} | ||
function createMorphContext(oldNode, newContent, config) { | ||
return { | ||
target:oldNode, | ||
newContent: newContent, | ||
config: config, | ||
morphStyle : config.morphStyle, | ||
ignoreActive : config.ignoreActive, | ||
ignoreActiveValue : config.ignoreActiveValue, | ||
idMap: createIdMap(oldNode, newContent), | ||
deadIds: new Set(), | ||
callbacks: Object.assign({ | ||
beforeNodeAdded: noOp, | ||
afterNodeAdded : noOp, | ||
beforeNodeMorphed: noOp, | ||
afterNodeMorphed : noOp, | ||
beforeNodeRemoved: noOp, | ||
afterNodeRemoved : noOp, | ||
function noOp() { | ||
} | ||
}, config.callbacks), | ||
head: Object.assign({ | ||
style: 'merge', | ||
shouldPreserve : function(elt) { | ||
return elt.getAttribute("im-preserve") === "true"; | ||
}, | ||
shouldReAppend : function(elt) { | ||
return elt.getAttribute("im-re-append") === "true"; | ||
}, | ||
shouldRemove : noOp, | ||
afterHeadMorphed : noOp, | ||
}, config.head), | ||
} | ||
/* | ||
Deep merges the config object and the Idiomoroph.defaults object to | ||
produce a final configuration object | ||
*/ | ||
function mergeDefaults(config) { | ||
let finalConfig = {}; | ||
// copy top level stuff into final config | ||
Object.assign(finalConfig, defaults); | ||
Object.assign(finalConfig, config); | ||
// copy callbacks into final config (do this to deep merge the callbacks) | ||
finalConfig.callbacks = {}; | ||
Object.assign(finalConfig.callbacks, defaults.callbacks); | ||
Object.assign(finalConfig.callbacks, config.callbacks); | ||
// copy head config into final config (do this to deep merge the head) | ||
finalConfig.head = {}; | ||
Object.assign(finalConfig.head, defaults.head); | ||
Object.assign(finalConfig.head, config.head); | ||
return finalConfig; | ||
} | ||
function createMorphContext(oldNode, newContent, config) { | ||
config = mergeDefaults(config); | ||
return { | ||
target: oldNode, | ||
newContent: newContent, | ||
config: config, | ||
morphStyle: config.morphStyle, | ||
ignoreActive: config.ignoreActive, | ||
ignoreActiveValue: config.ignoreActiveValue, | ||
idMap: createIdMap(oldNode, newContent), | ||
deadIds: new Set(), | ||
callbacks: config.callbacks, | ||
head: config.head | ||
} | ||
} | ||
function isIdSetMatch(node1, node2, ctx) { | ||
if (node1 == null || node2 == null) { | ||
return false; | ||
} | ||
if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) { | ||
if (node1.id !== "" && node1.id === node2.id) { | ||
return true; | ||
} else { | ||
return getIdIntersectionCount(ctx, node1, node2) > 0; | ||
} | ||
} | ||
function isIdSetMatch(node1, node2, ctx) { | ||
if (node1 == null || node2 == null) { | ||
return false; | ||
} | ||
function isSoftMatch(node1, node2) { | ||
if (node1 == null || node2 == null) { | ||
return false; | ||
if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) { | ||
if (node1.id !== "" && node1.id === node2.id) { | ||
return true; | ||
} else { | ||
return getIdIntersectionCount(ctx, node1, node2) > 0; | ||
} | ||
return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName | ||
} | ||
return false; | ||
} | ||
function removeNodesBetween(startInclusive, endExclusive, ctx) { | ||
while (startInclusive !== endExclusive) { | ||
let tempNode = startInclusive; | ||
startInclusive = startInclusive.nextSibling; | ||
removeNode(tempNode, ctx); | ||
} | ||
removeIdsFromConsideration(ctx, endExclusive); | ||
return endExclusive.nextSibling; | ||
function isSoftMatch(node1, node2) { | ||
if (node1 == null || node2 == null) { | ||
return false; | ||
} | ||
return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName | ||
} | ||
//============================================================================= | ||
// Scans forward from the insertionPoint in the old parent looking for a potential id match | ||
// for the newChild. We stop if we find a potential id match for the new child OR | ||
// if the number of potential id matches we are discarding is greater than the | ||
// potential id matches for the new child | ||
//============================================================================= | ||
function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) { | ||
function removeNodesBetween(startInclusive, endExclusive, ctx) { | ||
while (startInclusive !== endExclusive) { | ||
let tempNode = startInclusive; | ||
startInclusive = startInclusive.nextSibling; | ||
removeNode(tempNode, ctx); | ||
} | ||
removeIdsFromConsideration(ctx, endExclusive); | ||
return endExclusive.nextSibling; | ||
} | ||
// max id matches we are willing to discard in our search | ||
let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent); | ||
//============================================================================= | ||
// Scans forward from the insertionPoint in the old parent looking for a potential id match | ||
// for the newChild. We stop if we find a potential id match for the new child OR | ||
// if the number of potential id matches we are discarding is greater than the | ||
// potential id matches for the new child | ||
//============================================================================= | ||
function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) { | ||
let potentialMatch = null; | ||
// max id matches we are willing to discard in our search | ||
let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent); | ||
// only search forward if there is a possibility of an id match | ||
if (newChildPotentialIdCount > 0) { | ||
let potentialMatch = insertionPoint; | ||
// if there is a possibility of an id match, scan forward | ||
// keep track of the potential id match count we are discarding (the | ||
// newChildPotentialIdCount must be greater than this to make it likely | ||
// worth it) | ||
let otherMatchCount = 0; | ||
while (potentialMatch != null) { | ||
let potentialMatch = null; | ||
// If we have an id match, return the current potential match | ||
if (isIdSetMatch(newChild, potentialMatch, ctx)) { | ||
return potentialMatch; | ||
} | ||
// only search forward if there is a possibility of an id match | ||
if (newChildPotentialIdCount > 0) { | ||
let potentialMatch = insertionPoint; | ||
// if there is a possibility of an id match, scan forward | ||
// keep track of the potential id match count we are discarding (the | ||
// newChildPotentialIdCount must be greater than this to make it likely | ||
// worth it) | ||
let otherMatchCount = 0; | ||
while (potentialMatch != null) { | ||
// computer the other potential matches of this new content | ||
otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent); | ||
if (otherMatchCount > newChildPotentialIdCount) { | ||
// if we have more potential id matches in _other_ content, we | ||
// do not have a good candidate for an id match, so return null | ||
return null; | ||
} | ||
// If we have an id match, return the current potential match | ||
if (isIdSetMatch(newChild, potentialMatch, ctx)) { | ||
return potentialMatch; | ||
} | ||
// advanced to the next old content child | ||
potentialMatch = potentialMatch.nextSibling; | ||
// computer the other potential matches of this new content | ||
otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent); | ||
if (otherMatchCount > newChildPotentialIdCount) { | ||
// if we have more potential id matches in _other_ content, we | ||
// do not have a good candidate for an id match, so return null | ||
return null; | ||
} | ||
// advanced to the next old content child | ||
potentialMatch = potentialMatch.nextSibling; | ||
} | ||
return potentialMatch; | ||
} | ||
return potentialMatch; | ||
} | ||
//============================================================================= | ||
// Scans forward from the insertionPoint in the old parent looking for a potential soft match | ||
// for the newChild. We stop if we find a potential soft match for the new child OR | ||
// if we find a potential id match in the old parents children OR if we find two | ||
// potential soft matches for the next two pieces of new content | ||
//============================================================================= | ||
function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) { | ||
//============================================================================= | ||
// Scans forward from the insertionPoint in the old parent looking for a potential soft match | ||
// for the newChild. We stop if we find a potential soft match for the new child OR | ||
// if we find a potential id match in the old parents children OR if we find two | ||
// potential soft matches for the next two pieces of new content | ||
//============================================================================= | ||
function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) { | ||
let potentialSoftMatch = insertionPoint; | ||
let nextSibling = newChild.nextSibling; | ||
let siblingSoftMatchCount = 0; | ||
let potentialSoftMatch = insertionPoint; | ||
let nextSibling = newChild.nextSibling; | ||
let siblingSoftMatchCount = 0; | ||
while (potentialSoftMatch != null) { | ||
while (potentialSoftMatch != null) { | ||
if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) { | ||
// the current potential soft match has a potential id set match with the remaining new | ||
// content so bail out of looking | ||
return null; | ||
} | ||
if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) { | ||
// the current potential soft match has a potential id set match with the remaining new | ||
// content so bail out of looking | ||
return null; | ||
} | ||
// if we have a soft match with the current node, return it | ||
if (isSoftMatch(newChild, potentialSoftMatch)) { | ||
return potentialSoftMatch; | ||
} | ||
// if we have a soft match with the current node, return it | ||
if (isSoftMatch(newChild, potentialSoftMatch)) { | ||
return potentialSoftMatch; | ||
} | ||
if (isSoftMatch(nextSibling, potentialSoftMatch)) { | ||
// the next new node has a soft match with this node, so | ||
// increment the count of future soft matches | ||
siblingSoftMatchCount++; | ||
nextSibling = nextSibling.nextSibling; | ||
if (isSoftMatch(nextSibling, potentialSoftMatch)) { | ||
// the next new node has a soft match with this node, so | ||
// increment the count of future soft matches | ||
siblingSoftMatchCount++; | ||
nextSibling = nextSibling.nextSibling; | ||
// If there are two future soft matches, bail to allow the siblings to soft match | ||
// so that we don't consume future soft matches for the sake of the current node | ||
if (siblingSoftMatchCount >= 2) { | ||
return null; | ||
} | ||
// If there are two future soft matches, bail to allow the siblings to soft match | ||
// so that we don't consume future soft matches for the sake of the current node | ||
if (siblingSoftMatchCount >= 2) { | ||
return null; | ||
} | ||
// advanced to the next old content child | ||
potentialSoftMatch = potentialSoftMatch.nextSibling; | ||
} | ||
return potentialSoftMatch; | ||
// advanced to the next old content child | ||
potentialSoftMatch = potentialSoftMatch.nextSibling; | ||
} | ||
function parseContent(newContent) { | ||
let parser = new DOMParser(); | ||
return potentialSoftMatch; | ||
} | ||
// remove svgs to avoid false-positive matches on head, etc. | ||
let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, ''); | ||
function parseContent(newContent) { | ||
let parser = new DOMParser(); | ||
// if the newContent contains a html, head or body tag, we can simply parse it w/o wrapping | ||
if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) { | ||
let content = parser.parseFromString(newContent, "text/html"); | ||
// if it is a full HTML document, return the document itself as the parent container | ||
if (contentWithSvgsRemoved.match(/<\/html>/)) { | ||
content.generatedByIdiomorph = true; | ||
return content; | ||
// remove svgs to avoid false-positive matches on head, etc. | ||
let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, ''); | ||
// if the newContent contains a html, head or body tag, we can simply parse it w/o wrapping | ||
if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) { | ||
let content = parser.parseFromString(newContent, "text/html"); | ||
// if it is a full HTML document, return the document itself as the parent container | ||
if (contentWithSvgsRemoved.match(/<\/html>/)) { | ||
content.generatedByIdiomorph = true; | ||
return content; | ||
} else { | ||
// otherwise return the html element as the parent container | ||
let htmlElement = content.firstChild; | ||
if (htmlElement) { | ||
htmlElement.generatedByIdiomorph = true; | ||
return htmlElement; | ||
} else { | ||
// otherwise return the html element as the parent container | ||
let htmlElement = content.firstChild; | ||
if (htmlElement) { | ||
htmlElement.generatedByIdiomorph = true; | ||
return htmlElement; | ||
} else { | ||
return null; | ||
} | ||
return null; | ||
} | ||
} else { | ||
// if it is partial HTML, wrap it in a template tag to provide a parent element and also to help | ||
// deal with touchy tags like tr, tbody, etc. | ||
let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html"); | ||
let content = responseDoc.body.querySelector('template').content; | ||
content.generatedByIdiomorph = true; | ||
return content | ||
} | ||
} else { | ||
// if it is partial HTML, wrap it in a template tag to provide a parent element and also to help | ||
// deal with touchy tags like tr, tbody, etc. | ||
let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html"); | ||
let content = responseDoc.body.querySelector('template').content; | ||
content.generatedByIdiomorph = true; | ||
return content | ||
} | ||
} | ||
function normalizeContent(newContent) { | ||
if (newContent == null) { | ||
// noinspection UnnecessaryLocalVariableJS | ||
const dummyParent = document.createElement('div'); | ||
return dummyParent; | ||
} else if (newContent.generatedByIdiomorph) { | ||
// the template tag created by idiomorph parsing can serve as a dummy parent | ||
return newContent; | ||
} else if (newContent instanceof Node) { | ||
// a single node is added as a child to a dummy parent | ||
const dummyParent = document.createElement('div'); | ||
dummyParent.append(newContent); | ||
return dummyParent; | ||
} else { | ||
// all nodes in the array or HTMLElement collection are consolidated under | ||
// a single dummy parent element | ||
const dummyParent = document.createElement('div'); | ||
for (const elt of [...newContent]) { | ||
dummyParent.append(elt); | ||
} | ||
return dummyParent; | ||
function normalizeContent(newContent) { | ||
if (newContent == null) { | ||
// noinspection UnnecessaryLocalVariableJS | ||
const dummyParent = document.createElement('div'); | ||
return dummyParent; | ||
} else if (newContent.generatedByIdiomorph) { | ||
// the template tag created by idiomorph parsing can serve as a dummy parent | ||
return newContent; | ||
} else if (newContent instanceof Node) { | ||
// a single node is added as a child to a dummy parent | ||
const dummyParent = document.createElement('div'); | ||
dummyParent.append(newContent); | ||
return dummyParent; | ||
} else { | ||
// all nodes in the array or HTMLElement collection are consolidated under | ||
// a single dummy parent element | ||
const dummyParent = document.createElement('div'); | ||
for (const elt of [...newContent]) { | ||
dummyParent.append(elt); | ||
} | ||
return dummyParent; | ||
} | ||
} | ||
function insertSiblings(previousSibling, morphedNode, nextSibling) { | ||
let stack = [] | ||
let added = [] | ||
while (previousSibling != null) { | ||
stack.push(previousSibling); | ||
previousSibling = previousSibling.previousSibling; | ||
} | ||
while (stack.length > 0) { | ||
let node = stack.pop(); | ||
added.push(node); // push added preceding siblings on in order and insert | ||
morphedNode.parentElement.insertBefore(node, morphedNode); | ||
} | ||
added.push(morphedNode); | ||
while (nextSibling != null) { | ||
stack.push(nextSibling); | ||
added.push(nextSibling); // here we are going in order, so push on as we scan, rather than add | ||
nextSibling = nextSibling.nextSibling; | ||
} | ||
while (stack.length > 0) { | ||
morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling); | ||
} | ||
return added; | ||
function insertSiblings(previousSibling, morphedNode, nextSibling) { | ||
let stack = [] | ||
let added = [] | ||
while (previousSibling != null) { | ||
stack.push(previousSibling); | ||
previousSibling = previousSibling.previousSibling; | ||
} | ||
while (stack.length > 0) { | ||
let node = stack.pop(); | ||
added.push(node); // push added preceding siblings on in order and insert | ||
morphedNode.parentElement.insertBefore(node, morphedNode); | ||
} | ||
added.push(morphedNode); | ||
while (nextSibling != null) { | ||
stack.push(nextSibling); | ||
added.push(nextSibling); // here we are going in order, so push on as we scan, rather than add | ||
nextSibling = nextSibling.nextSibling; | ||
} | ||
while (stack.length > 0) { | ||
morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling); | ||
} | ||
return added; | ||
} | ||
function findBestNodeMatch(newContent, oldNode, ctx) { | ||
let currentElement; | ||
currentElement = newContent.firstChild; | ||
let bestElement = currentElement; | ||
let score = 0; | ||
while (currentElement) { | ||
let newScore = scoreElement(currentElement, oldNode, ctx); | ||
if (newScore > score) { | ||
bestElement = currentElement; | ||
score = newScore; | ||
} | ||
currentElement = currentElement.nextSibling; | ||
function findBestNodeMatch(newContent, oldNode, ctx) { | ||
let currentElement; | ||
currentElement = newContent.firstChild; | ||
let bestElement = currentElement; | ||
let score = 0; | ||
while (currentElement) { | ||
let newScore = scoreElement(currentElement, oldNode, ctx); | ||
if (newScore > score) { | ||
bestElement = currentElement; | ||
score = newScore; | ||
} | ||
return bestElement; | ||
currentElement = currentElement.nextSibling; | ||
} | ||
return bestElement; | ||
} | ||
function scoreElement(node1, node2, ctx) { | ||
if (isSoftMatch(node1, node2)) { | ||
return .5 + getIdIntersectionCount(ctx, node1, node2); | ||
} | ||
return 0; | ||
function scoreElement(node1, node2, ctx) { | ||
if (isSoftMatch(node1, node2)) { | ||
return .5 + getIdIntersectionCount(ctx, node1, node2); | ||
} | ||
return 0; | ||
} | ||
function removeNode(tempNode, ctx) { | ||
removeIdsFromConsideration(ctx, tempNode) | ||
if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return; | ||
function removeNode(tempNode, ctx) { | ||
removeIdsFromConsideration(ctx, tempNode) | ||
if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return; | ||
tempNode.remove(); | ||
ctx.callbacks.afterNodeRemoved(tempNode); | ||
} | ||
tempNode.remove(); | ||
ctx.callbacks.afterNodeRemoved(tempNode); | ||
} | ||
//============================================================================= | ||
// ID Set Functions | ||
//============================================================================= | ||
//============================================================================= | ||
// ID Set Functions | ||
//============================================================================= | ||
function isIdInConsideration(ctx, id) { | ||
return !ctx.deadIds.has(id); | ||
} | ||
function isIdInConsideration(ctx, id) { | ||
return !ctx.deadIds.has(id); | ||
} | ||
function idIsWithinNode(ctx, id, targetNode) { | ||
let idSet = ctx.idMap.get(targetNode) || EMPTY_SET; | ||
return idSet.has(id); | ||
} | ||
function idIsWithinNode(ctx, id, targetNode) { | ||
let idSet = ctx.idMap.get(targetNode) || EMPTY_SET; | ||
return idSet.has(id); | ||
} | ||
function removeIdsFromConsideration(ctx, node) { | ||
let idSet = ctx.idMap.get(node) || EMPTY_SET; | ||
for (const id of idSet) { | ||
ctx.deadIds.add(id); | ||
} | ||
function removeIdsFromConsideration(ctx, node) { | ||
let idSet = ctx.idMap.get(node) || EMPTY_SET; | ||
for (const id of idSet) { | ||
ctx.deadIds.add(id); | ||
} | ||
} | ||
function getIdIntersectionCount(ctx, node1, node2) { | ||
let sourceSet = ctx.idMap.get(node1) || EMPTY_SET; | ||
let matchCount = 0; | ||
for (const id of sourceSet) { | ||
// a potential match is an id in the source and potentialIdsSet, but | ||
// that has not already been merged into the DOM | ||
if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) { | ||
++matchCount; | ||
} | ||
function getIdIntersectionCount(ctx, node1, node2) { | ||
let sourceSet = ctx.idMap.get(node1) || EMPTY_SET; | ||
let matchCount = 0; | ||
for (const id of sourceSet) { | ||
// a potential match is an id in the source and potentialIdsSet, but | ||
// that has not already been merged into the DOM | ||
if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) { | ||
++matchCount; | ||
} | ||
return matchCount; | ||
} | ||
return matchCount; | ||
} | ||
/** | ||
* A bottom up algorithm that finds all elements with ids inside of the node | ||
* argument and populates id sets for those nodes and all their parents, generating | ||
* a set of ids contained within all nodes for the entire hierarchy in the DOM | ||
* | ||
* @param node {Element} | ||
* @param {Map<Node, Set<String>>} idMap | ||
*/ | ||
function populateIdMapForNode(node, idMap) { | ||
let nodeParent = node.parentElement; | ||
// find all elements with an id property | ||
let idElements = node.querySelectorAll('[id]'); | ||
for (const elt of idElements) { | ||
let current = elt; | ||
// walk up the parent hierarchy of that element, adding the id | ||
// of element to the parent's id set | ||
while (current !== nodeParent && current != null) { | ||
let idSet = idMap.get(current); | ||
// if the id set doesn't exist, create it and insert it in the map | ||
if (idSet == null) { | ||
idSet = new Set(); | ||
idMap.set(current, idSet); | ||
} | ||
idSet.add(elt.id); | ||
current = current.parentElement; | ||
/** | ||
* A bottom up algorithm that finds all elements with ids inside of the node | ||
* argument and populates id sets for those nodes and all their parents, generating | ||
* a set of ids contained within all nodes for the entire hierarchy in the DOM | ||
* | ||
* @param node {Element} | ||
* @param {Map<Node, Set<String>>} idMap | ||
*/ | ||
function populateIdMapForNode(node, idMap) { | ||
let nodeParent = node.parentElement; | ||
// find all elements with an id property | ||
let idElements = node.querySelectorAll('[id]'); | ||
for (const elt of idElements) { | ||
let current = elt; | ||
// walk up the parent hierarchy of that element, adding the id | ||
// of element to the parent's id set | ||
while (current !== nodeParent && current != null) { | ||
let idSet = idMap.get(current); | ||
// if the id set doesn't exist, create it and insert it in the map | ||
if (idSet == null) { | ||
idSet = new Set(); | ||
idMap.set(current, idSet); | ||
} | ||
idSet.add(elt.id); | ||
current = current.parentElement; | ||
} | ||
} | ||
} | ||
/** | ||
* This function computes a map of nodes to all ids contained within that node (inclusive of the | ||
* node). This map can be used to ask if two nodes have intersecting sets of ids, which allows | ||
* for a looser definition of "matching" than tradition id matching, and allows child nodes | ||
* to contribute to a parent nodes matching. | ||
* | ||
* @param {Element} oldContent the old content that will be morphed | ||
* @param {Element} newContent the new content to morph to | ||
* @returns {Map<Node, Set<String>>} a map of nodes to id sets for the | ||
*/ | ||
function createIdMap(oldContent, newContent) { | ||
let idMap = new Map(); | ||
populateIdMapForNode(oldContent, idMap); | ||
populateIdMapForNode(newContent, idMap); | ||
return idMap; | ||
} | ||
/** | ||
* This function computes a map of nodes to all ids contained within that node (inclusive of the | ||
* node). This map can be used to ask if two nodes have intersecting sets of ids, which allows | ||
* for a looser definition of "matching" than tradition id matching, and allows child nodes | ||
* to contribute to a parent nodes matching. | ||
* | ||
* @param {Element} oldContent the old content that will be morphed | ||
* @param {Element} newContent the new content to morph to | ||
* @returns {Map<Node, Set<String>>} a map of nodes to id sets for the | ||
*/ | ||
function createIdMap(oldContent, newContent) { | ||
let idMap = new Map(); | ||
populateIdMapForNode(oldContent, idMap); | ||
populateIdMapForNode(newContent, idMap); | ||
return idMap; | ||
} | ||
//============================================================================= | ||
// This is what ends up becoming the Idiomorph global object | ||
//============================================================================= | ||
return { | ||
morph | ||
} | ||
})() | ||
})); | ||
htmx.defineExtension('morph', { | ||
isInlineSwap: function(swapStyle) { | ||
return swapStyle === 'morph'; | ||
}, | ||
handleSwap: function (swapStyle, target, fragment) { | ||
//============================================================================= | ||
// This is what ends up becoming the Idiomorph global object | ||
//============================================================================= | ||
return { | ||
morph, | ||
defaults | ||
} | ||
})(); | ||
(function(){ | ||
function createMorphConfig(swapStyle) { | ||
if (swapStyle === 'morph' || swapStyle === 'morph:outerHTML') { | ||
return Idiomorph.morph(target, fragment.children); | ||
return {morphStyle: 'outerHTML'} | ||
} else if (swapStyle === 'morph:innerHTML') { | ||
return Idiomorph.morph(target, fragment.children, {morphStyle:'innerHTML'}); | ||
return {morphStyle: 'innerHTML'} | ||
} else if (swapStyle.startsWith("morph:")) { | ||
return Function("return (" + swapStyle.slice(6) + ")")(); | ||
} | ||
} | ||
}); | ||
htmx.defineExtension('morph', { | ||
isInlineSwap: function(swapStyle) { | ||
let config = createMorphConfig(swapStyle); | ||
return config.swapStyle === "outerHTML" || config.swapStyle == null; | ||
}, | ||
handleSwap: function (swapStyle, target, fragment) { | ||
let config = createMorphConfig(swapStyle); | ||
if (config) { | ||
return Idiomorph.morph(target, fragment.children, config); | ||
} | ||
} | ||
}); | ||
})() |
@@ -1,1 +0,1 @@ | ||
(function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else if(typeof module==="object"&&module.exports){module.exports=t()}else{e.Idiomorph=e.Idiomorph||t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";let o=new Set;function e(e,t,n={}){if(e instanceof Document){e=e.documentElement}if(typeof t==="string"){t=y(t)}let l=A(t);let r=h(e,l,n);return u(e,l,r)}function u(r,i,o){if(o.head.block){let t=r.querySelector("head");let n=i.querySelector("head");if(t&&n){let e=s(n,t,o);Promise.all(e).then(function(){u(r,i,Object.assign(o,{head:{block:false,ignore:true}}))});return}}if(o.morphStyle==="innerHTML"){l(i,r,o);return r.children}else if(o.morphStyle==="outerHTML"||o.morphStyle==null){let e=N(i,r,o);let t=e?.previousSibling;let n=e?.nextSibling;let l=f(r,e,o);if(e){return M(t,l,n)}else{return[]}}else{throw"Do not understand how to morph style "+o.morphStyle}}function d(e,t){return t.ignoreActiveValue&&e===document.activeElement}function f(e,t,n){if(n.ignoreActive&&e===document.activeElement){}else if(t==null){if(n.callbacks.beforeNodeRemoved(e)===false)return;e.remove();n.callbacks.afterNodeRemoved(e);return null}else if(!b(e,t)){if(n.callbacks.beforeNodeRemoved(e)===false)return;if(n.callbacks.beforeNodeAdded(t)===false)return;e.parentElement.replaceChild(t,e);n.callbacks.afterNodeAdded(t);n.callbacks.afterNodeRemoved(e);return t}else{if(n.callbacks.beforeNodeMorphed(e,t)===false)return;if(e instanceof HTMLHeadElement&&n.head.ignore){}else if(e instanceof HTMLHeadElement&&n.head.style!=="morph"){s(t,e,n)}else{r(t,e,n);if(!d(e,n)){l(t,e,n)}}n.callbacks.afterNodeMorphed(e,t);return e}}function l(n,l,r){let i=n.firstChild;let o=l.firstChild;let u;while(i){u=i;i=u.nextSibling;if(o==null){if(r.callbacks.beforeNodeAdded(u)===false)return;l.appendChild(u);r.callbacks.afterNodeAdded(u);E(r,u);continue}if(m(u,o,r)){f(o,u,r);o=o.nextSibling;E(r,u);continue}let e=v(n,l,u,o,r);if(e){o=g(o,e,r);f(e,u,r);E(r,u);continue}let t=S(n,l,u,o,r);if(t){o=g(o,t,r);f(t,u,r);E(r,u);continue}if(r.callbacks.beforeNodeAdded(u)===false)return;l.insertBefore(u,o);r.callbacks.afterNodeAdded(u);E(r,u)}while(o!==null){let e=o;o=o.nextSibling;w(e,r)}}function r(t,n,e){let l=t.nodeType;if(l===1){const r=t.attributes;const i=n.attributes;for(const o of r){if(o.name==="value"&&d(n,e)){continue}if(n.getAttribute(o.name)!==o.value){n.setAttribute(o.name,o.value)}}for(let e=i.length-1;0<=e;e--){const u=i[e];if(!t.hasAttribute(u.name)){n.removeAttribute(u.name)}}}if(l===8||l===3){if(n.nodeValue!==t.nodeValue){n.nodeValue=t.nodeValue}}if(!d(n,e)){a(t,n)}}function i(e,t,n){if(e[n]!==t[n]){t[n]=e[n];if(e[n]){t.setAttribute(n,e[n])}else{t.removeAttribute(n)}}}function a(n,l){if(n instanceof HTMLInputElement&&l instanceof HTMLInputElement&&n.type!=="file"){let e=n.value;let t=l.value;i(n,l,"checked");i(n,l,"disabled");if(!n.hasAttribute("value")){l.value="";l.removeAttribute("value")}else if(e!==t){l.setAttribute("value",e);l.value=e}}else if(n instanceof HTMLOptionElement){i(n,l,"selected")}else if(n instanceof HTMLTextAreaElement&&l instanceof HTMLTextAreaElement){let e=n.value;let t=l.value;if(e!==t){l.value=e}if(l.firstChild&&l.firstChild.nodeValue!==e){l.firstChild.nodeValue=e}}}function s(e,t,l){let r=[];let i=[];let o=[];let u=[];let d=l.head.style;let f=new Map;for(const n of e.children){f.set(n.outerHTML,n)}for(const s of t.children){let e=f.has(s.outerHTML);let t=l.head.shouldReAppend(s);let n=l.head.shouldPreserve(s);if(e||n){if(t){i.push(s)}else{f.delete(s.outerHTML);o.push(s)}}else{if(d==="append"){if(t){i.push(s);u.push(s)}}else{if(l.head.shouldRemove(s)!==false){i.push(s)}}}}u.push(...f.values());p("to append: ",u);let a=[];for(const c of u){p("adding: ",c);let n=document.createRange().createContextualFragment(c.outerHTML).firstChild;p(n);if(l.callbacks.beforeNodeAdded(n)!==false){if(n.href||n.src){let t=null;let e=new Promise(function(e){t=e});n.addEventListener("load",function(){t()});a.push(e)}t.appendChild(n);l.callbacks.afterNodeAdded(n);r.push(n)}}for(const h of i){if(l.callbacks.beforeNodeRemoved(h)!==false){t.removeChild(h);l.callbacks.afterNodeRemoved(h)}}l.head.afterHeadMorphed(t,{added:r,kept:o,removed:i});return a}function p(){}function c(){}function h(e,t,n){return{target:e,newContent:t,config:n,morphStyle:n.morphStyle,ignoreActive:n.ignoreActive,ignoreActiveValue:n.ignoreActiveValue,idMap:R(e,t),deadIds:new Set,callbacks:Object.assign({beforeNodeAdded:c,afterNodeAdded:c,beforeNodeMorphed:c,afterNodeMorphed:c,beforeNodeRemoved:c,afterNodeRemoved:c},n.callbacks),head:Object.assign({style:"merge",shouldPreserve:function(e){return e.getAttribute("im-preserve")==="true"},shouldReAppend:function(e){return e.getAttribute("im-re-append")==="true"},shouldRemove:c,afterHeadMorphed:c},n.head)}}function m(e,t,n){if(e==null||t==null){return false}if(e.nodeType===t.nodeType&&e.tagName===t.tagName){if(e.id!==""&&e.id===t.id){return true}else{return H(n,e,t)>0}}return false}function b(e,t){if(e==null||t==null){return false}return e.nodeType===t.nodeType&&e.tagName===t.tagName}function g(t,e,n){while(t!==e){let e=t;t=t.nextSibling;w(e,n)}E(n,e);return e.nextSibling}function v(n,e,l,r,i){let o=H(i,l,e);let t=null;if(o>0){let e=r;let t=0;while(e!=null){if(m(l,e,i)){return e}t+=H(i,e,n);if(t>o){return null}e=e.nextSibling}}return t}function S(e,t,n,l,r){let i=l;let o=n.nextSibling;let u=0;while(i!=null){if(H(r,i,e)>0){return null}if(b(n,i)){return i}if(b(o,i)){u++;o=o.nextSibling;if(u>=2){return null}}i=i.nextSibling}return i}function y(n){let l=new DOMParser;let e=n.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim,"");if(e.match(/<\/html>/)||e.match(/<\/head>/)||e.match(/<\/body>/)){let t=l.parseFromString(n,"text/html");if(e.match(/<\/html>/)){t.generatedByIdiomorph=true;return t}else{let e=t.firstChild;if(e){e.generatedByIdiomorph=true;return e}else{return null}}}else{let e=l.parseFromString("<body><template>"+n+"</template></body>","text/html");let t=e.body.querySelector("template").content;t.generatedByIdiomorph=true;return t}}function A(e){if(e==null){const t=document.createElement("div");return t}else if(e.generatedByIdiomorph){return e}else if(e instanceof Node){const t=document.createElement("div");t.append(e);return t}else{const t=document.createElement("div");for(const n of[...e]){t.append(n)}return t}}function M(e,t,n){let l=[];let r=[];while(e!=null){l.push(e);e=e.previousSibling}while(l.length>0){let e=l.pop();r.push(e);t.parentElement.insertBefore(e,t)}r.push(t);while(n!=null){l.push(n);r.push(n);n=n.nextSibling}while(l.length>0){t.parentElement.insertBefore(l.pop(),t.nextSibling)}return r}function N(e,t,n){let l;l=e.firstChild;let r=l;let i=0;while(l){let e=k(l,t,n);if(e>i){r=l;i=e}l=l.nextSibling}return r}function k(e,t,n){if(b(e,t)){return.5+H(n,e,t)}return 0}function w(e,t){E(t,e);if(t.callbacks.beforeNodeRemoved(e)===false)return;e.remove();t.callbacks.afterNodeRemoved(e)}function T(e,t){return!e.deadIds.has(t)}function x(e,t,n){let l=e.idMap.get(n)||o;return l.has(t)}function E(e,t){let n=e.idMap.get(t)||o;for(const l of n){e.deadIds.add(l)}}function H(e,t,n){let l=e.idMap.get(t)||o;let r=0;for(const i of l){if(T(e,i)&&x(e,i,n)){++r}}return r}function L(e,n){let l=e.parentElement;let t=e.querySelectorAll("[id]");for(const r of t){let t=r;while(t!==l&&t!=null){let e=n.get(t);if(e==null){e=new Set;n.set(t,e)}e.add(r.id);t=t.parentElement}}}function R(e,t){let n=new Map;L(e,n);L(t,n);return n}return{morph:e}}()});htmx.defineExtension("morph",{isInlineSwap:function(e){return e==="morph"},handleSwap:function(e,t,n){if(e==="morph"||e==="morph:outerHTML"){return Idiomorph.morph(t,n.children)}else if(e==="morph:innerHTML"){return Idiomorph.morph(t,n.children,{morphStyle:"innerHTML"})}}}); | ||
var Idiomorph=function(){"use strict";let o=new Set;let n={morphStyle:"outerHTML",callbacks:{beforeNodeAdded:t,afterNodeAdded:t,beforeNodeMorphed:t,afterNodeMorphed:t,beforeNodeRemoved:t,afterNodeRemoved:t,beforeAttributeUpdated:t},head:{style:"merge",shouldPreserve:function(e){return e.getAttribute("im-preserve")==="true"},shouldReAppend:function(e){return e.getAttribute("im-re-append")==="true"},shouldRemove:t,afterHeadMorphed:t}};function e(e,t,n={}){if(e instanceof Document){e=e.documentElement}if(typeof t==="string"){t=y(t)}let l=M(t);let r=m(e,l,n);return a(e,l,r)}function a(r,i,o){if(o.head.block){let t=r.querySelector("head");let n=i.querySelector("head");if(t&&n){let e=c(n,t,o);Promise.all(e).then(function(){a(r,i,Object.assign(o,{head:{block:false,ignore:true}}))});return}}if(o.morphStyle==="innerHTML"){l(i,r,o);return r.children}else if(o.morphStyle==="outerHTML"||o.morphStyle==null){let e=N(i,r,o);let t=e?.previousSibling;let n=e?.nextSibling;let l=d(r,e,o);if(e){return k(t,l,n)}else{return[]}}else{throw"Do not understand how to morph style "+o.morphStyle}}function u(e,t){return t.ignoreActiveValue&&e===document.activeElement}function d(e,t,n){if(n.ignoreActive&&e===document.activeElement){}else if(t==null){if(n.callbacks.beforeNodeRemoved(e)===false)return e;e.remove();n.callbacks.afterNodeRemoved(e);return null}else if(!g(e,t)){if(n.callbacks.beforeNodeRemoved(e)===false)return e;if(n.callbacks.beforeNodeAdded(t)===false)return e;e.parentElement.replaceChild(t,e);n.callbacks.afterNodeAdded(t);n.callbacks.afterNodeRemoved(e);return t}else{if(n.callbacks.beforeNodeMorphed(e,t)===false)return e;if(e instanceof HTMLHeadElement&&n.head.ignore){}else if(e instanceof HTMLHeadElement&&n.head.style!=="morph"){c(t,e,n)}else{r(t,e,n);if(!u(e,n)){l(t,e,n)}}n.callbacks.afterNodeMorphed(e,t);return e}}function l(n,l,r){let i=n.firstChild;let o=l.firstChild;let a;while(i){a=i;i=a.nextSibling;if(o==null){if(r.callbacks.beforeNodeAdded(a)===false)return;l.appendChild(a);r.callbacks.afterNodeAdded(a);x(r,a);continue}if(b(a,o,r)){d(o,a,r);o=o.nextSibling;x(r,a);continue}let e=S(n,l,a,o,r);if(e){o=v(o,e,r);d(e,a,r);x(r,a);continue}let t=A(n,l,a,o,r);if(t){o=v(o,t,r);d(t,a,r);x(r,a);continue}if(r.callbacks.beforeNodeAdded(a)===false)return;l.insertBefore(a,o);r.callbacks.afterNodeAdded(a);x(r,a)}while(o!==null){let e=o;o=o.nextSibling;w(e,r)}}function f(e,t,n,l){if(e==="value"&&l.ignoreActiveValue&&t===document.activeElement){return true}return l.callbacks.beforeAttributeUpdated(e,t,n)===false}function r(t,n,l){let e=t.nodeType;if(e===1){const r=t.attributes;const i=n.attributes;for(const o of r){if(f(o.name,n,"update",l)){continue}if(n.getAttribute(o.name)!==o.value){n.setAttribute(o.name,o.value)}}for(let e=i.length-1;0<=e;e--){const a=i[e];if(f(a.name,n,"remove",l)){continue}if(!t.hasAttribute(a.name)){n.removeAttribute(a.name)}}}if(e===8||e===3){if(n.nodeValue!==t.nodeValue){n.nodeValue=t.nodeValue}}if(!u(n,l)){s(t,n,l)}}function i(t,n,l,r){if(t[l]!==n[l]){let e=f(l,n,"update",r);if(!e){n[l]=t[l]}if(t[l]){if(!e){n.setAttribute(l,t[l])}}else{if(!f(l,n,"remove",r)){n.removeAttribute(l)}}}}function s(n,l,r){if(n instanceof HTMLInputElement&&l instanceof HTMLInputElement&&n.type!=="file"){let e=n.value;let t=l.value;i(n,l,"checked",r);i(n,l,"disabled",r);if(!n.hasAttribute("value")){if(!f("value",l,"remove",r)){l.value="";l.removeAttribute("value")}}else if(e!==t){if(!f("value",l,"update",r)){l.setAttribute("value",e);l.value=e}}}else if(n instanceof HTMLOptionElement){i(n,l,"selected",r)}else if(n instanceof HTMLTextAreaElement&&l instanceof HTMLTextAreaElement){let e=n.value;let t=l.value;if(f("value",l,"update",r)){return}if(e!==t){l.value=e}if(l.firstChild&&l.firstChild.nodeValue!==e){l.firstChild.nodeValue=e}}}function c(e,t,l){let r=[];let i=[];let o=[];let a=[];let u=l.head.style;let d=new Map;for(const n of e.children){d.set(n.outerHTML,n)}for(const s of t.children){let e=d.has(s.outerHTML);let t=l.head.shouldReAppend(s);let n=l.head.shouldPreserve(s);if(e||n){if(t){i.push(s)}else{d.delete(s.outerHTML);o.push(s)}}else{if(u==="append"){if(t){i.push(s);a.push(s)}}else{if(l.head.shouldRemove(s)!==false){i.push(s)}}}}a.push(...d.values());p("to append: ",a);let f=[];for(const c of a){p("adding: ",c);let n=document.createRange().createContextualFragment(c.outerHTML).firstChild;p(n);if(l.callbacks.beforeNodeAdded(n)!==false){if(n.href||n.src){let t=null;let e=new Promise(function(e){t=e});n.addEventListener("load",function(){t()});f.push(e)}t.appendChild(n);l.callbacks.afterNodeAdded(n);r.push(n)}}for(const h of i){if(l.callbacks.beforeNodeRemoved(h)!==false){t.removeChild(h);l.callbacks.afterNodeRemoved(h)}}l.head.afterHeadMorphed(t,{added:r,kept:o,removed:i});return f}function p(){}function t(){}function h(e){let t={};Object.assign(t,n);Object.assign(t,e);t.callbacks={};Object.assign(t.callbacks,n.callbacks);Object.assign(t.callbacks,e.callbacks);t.head={};Object.assign(t.head,n.head);Object.assign(t.head,e.head);return t}function m(e,t,n){n=h(n);return{target:e,newContent:t,config:n,morphStyle:n.morphStyle,ignoreActive:n.ignoreActive,ignoreActiveValue:n.ignoreActiveValue,idMap:C(e,t),deadIds:new Set,callbacks:n.callbacks,head:n.head}}function b(e,t,n){if(e==null||t==null){return false}if(e.nodeType===t.nodeType&&e.tagName===t.tagName){if(e.id!==""&&e.id===t.id){return true}else{return L(n,e,t)>0}}return false}function g(e,t){if(e==null||t==null){return false}return e.nodeType===t.nodeType&&e.tagName===t.tagName}function v(t,e,n){while(t!==e){let e=t;t=t.nextSibling;w(e,n)}x(n,e);return e.nextSibling}function S(n,e,l,r,i){let o=L(i,l,e);let t=null;if(o>0){let e=r;let t=0;while(e!=null){if(b(l,e,i)){return e}t+=L(i,e,n);if(t>o){return null}e=e.nextSibling}}return t}function A(e,t,n,l,r){let i=l;let o=n.nextSibling;let a=0;while(i!=null){if(L(r,i,e)>0){return null}if(g(n,i)){return i}if(g(o,i)){a++;o=o.nextSibling;if(a>=2){return null}}i=i.nextSibling}return i}function y(n){let l=new DOMParser;let e=n.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim,"");if(e.match(/<\/html>/)||e.match(/<\/head>/)||e.match(/<\/body>/)){let t=l.parseFromString(n,"text/html");if(e.match(/<\/html>/)){t.generatedByIdiomorph=true;return t}else{let e=t.firstChild;if(e){e.generatedByIdiomorph=true;return e}else{return null}}}else{let e=l.parseFromString("<body><template>"+n+"</template></body>","text/html");let t=e.body.querySelector("template").content;t.generatedByIdiomorph=true;return t}}function M(e){if(e==null){const t=document.createElement("div");return t}else if(e.generatedByIdiomorph){return e}else if(e instanceof Node){const t=document.createElement("div");t.append(e);return t}else{const t=document.createElement("div");for(const n of[...e]){t.append(n)}return t}}function k(e,t,n){let l=[];let r=[];while(e!=null){l.push(e);e=e.previousSibling}while(l.length>0){let e=l.pop();r.push(e);t.parentElement.insertBefore(e,t)}r.push(t);while(n!=null){l.push(n);r.push(n);n=n.nextSibling}while(l.length>0){t.parentElement.insertBefore(l.pop(),t.nextSibling)}return r}function N(e,t,n){let l;l=e.firstChild;let r=l;let i=0;while(l){let e=T(l,t,n);if(e>i){r=l;i=e}l=l.nextSibling}return r}function T(e,t,n){if(g(e,t)){return.5+L(n,e,t)}return 0}function w(e,t){x(t,e);if(t.callbacks.beforeNodeRemoved(e)===false)return;e.remove();t.callbacks.afterNodeRemoved(e)}function H(e,t){return!e.deadIds.has(t)}function E(e,t,n){let l=e.idMap.get(n)||o;return l.has(t)}function x(e,t){let n=e.idMap.get(t)||o;for(const l of n){e.deadIds.add(l)}}function L(e,t,n){let l=e.idMap.get(t)||o;let r=0;for(const i of l){if(H(e,i)&&E(e,i,n)){++r}}return r}function R(e,n){let l=e.parentElement;let t=e.querySelectorAll("[id]");for(const r of t){let t=r;while(t!==l&&t!=null){let e=n.get(t);if(e==null){e=new Set;n.set(t,e)}e.add(r.id);t=t.parentElement}}}function C(e,t){let n=new Map;R(e,n);R(t,n);return n}return{morph:e,defaults:n}}();(function(){function r(e){if(e==="morph"||e==="morph:outerHTML"){return{morphStyle:"outerHTML"}}else if(e==="morph:innerHTML"){return{morphStyle:"innerHTML"}}else if(e.startsWith("morph:")){return Function("return ("+e.slice(6)+")")()}}htmx.defineExtension("morph",{isInlineSwap:function(e){let t=r(e);return t.swapStyle==="outerHTML"||t.swapStyle==null},handleSwap:function(e,t,n){let l=r(e);if(l){return Idiomorph.morph(t,n.children,l)}}})})(); |
@@ -1,12 +0,24 @@ | ||
htmx.defineExtension('morph', { | ||
isInlineSwap: function(swapStyle) { | ||
return swapStyle === 'morph'; | ||
}, | ||
handleSwap: function (swapStyle, target, fragment) { | ||
(function(){ | ||
function createMorphConfig(swapStyle) { | ||
if (swapStyle === 'morph' || swapStyle === 'morph:outerHTML') { | ||
return Idiomorph.morph(target, fragment.children); | ||
return {morphStyle: 'outerHTML'} | ||
} else if (swapStyle === 'morph:innerHTML') { | ||
return Idiomorph.morph(target, fragment.children, {morphStyle:'innerHTML'}); | ||
return {morphStyle: 'innerHTML'} | ||
} else if (swapStyle.startsWith("morph:")) { | ||
return Function("return (" + swapStyle.slice(6) + ")")(); | ||
} | ||
} | ||
}); | ||
htmx.defineExtension('morph', { | ||
isInlineSwap: function(swapStyle) { | ||
let config = createMorphConfig(swapStyle); | ||
return config.swapStyle === "outerHTML" || config.swapStyle == null; | ||
}, | ||
handleSwap: function (swapStyle, target, fragment) { | ||
let config = createMorphConfig(swapStyle); | ||
if (config) { | ||
return Idiomorph.morph(target, fragment.children, config); | ||
} | ||
} | ||
}); | ||
})() |
@@ -1,299 +0,334 @@ | ||
//============================================================================= | ||
// UMD insanity... i hate javascript so much | ||
// | ||
// IGNORE EVERYTHING FROM HERE UNTIL THE COMMENT SAYING 'AND NOW IT BEGINS..." | ||
//============================================================================= | ||
(function (root, factory) { | ||
//@ts-ignore | ||
if (typeof define === 'function' && define.amd) { | ||
// AMD. Register as an anonymous module. | ||
//@ts-ignore | ||
define([], factory); | ||
} else if (typeof module === 'object' && module.exports) { | ||
// Node. Does not work with strict CommonJS, but | ||
// only CommonJS-like environments that support module.exports, | ||
// like Node. | ||
module.exports = factory(); | ||
} else { | ||
// Browser globals | ||
root.Idiomorph = root.Idiomorph || factory(); | ||
} | ||
}(typeof self !== 'undefined' ? self : this, | ||
function () { | ||
return (function () { | ||
'use strict'; | ||
// base IIFE to define idiomorph | ||
var Idiomorph = (function () { | ||
'use strict'; | ||
//============================================================================= | ||
// AND NOW IT BEGINS... | ||
//============================================================================= | ||
let EMPTY_SET = new Set(); | ||
//============================================================================= | ||
// AND NOW IT BEGINS... | ||
//============================================================================= | ||
let EMPTY_SET = new Set(); | ||
//============================================================================= | ||
// Core Morphing Algorithm - morph, morphNormalizedContent, morphOldNodeTo, morphChildren | ||
//============================================================================= | ||
function morph(oldNode, newContent, config = {}) { | ||
// default configuration values, updatable by users now | ||
let defaults = { | ||
morphStyle: "outerHTML", | ||
callbacks : { | ||
beforeNodeAdded: noOp, | ||
afterNodeAdded: noOp, | ||
beforeNodeMorphed: noOp, | ||
afterNodeMorphed: noOp, | ||
beforeNodeRemoved: noOp, | ||
afterNodeRemoved: noOp, | ||
beforeAttributeUpdated: noOp, | ||
if (oldNode instanceof Document) { | ||
oldNode = oldNode.documentElement; | ||
} | ||
}, | ||
head: { | ||
style: 'merge', | ||
shouldPreserve: function (elt) { | ||
return elt.getAttribute("im-preserve") === "true"; | ||
}, | ||
shouldReAppend: function (elt) { | ||
return elt.getAttribute("im-re-append") === "true"; | ||
}, | ||
shouldRemove: noOp, | ||
afterHeadMorphed: noOp, | ||
} | ||
}; | ||
if (typeof newContent === 'string') { | ||
newContent = parseContent(newContent); | ||
} | ||
//============================================================================= | ||
// Core Morphing Algorithm - morph, morphNormalizedContent, morphOldNodeTo, morphChildren | ||
//============================================================================= | ||
function morph(oldNode, newContent, config = {}) { | ||
let normalizedContent = normalizeContent(newContent); | ||
if (oldNode instanceof Document) { | ||
oldNode = oldNode.documentElement; | ||
} | ||
let ctx = createMorphContext(oldNode, normalizedContent, config); | ||
return morphNormalizedContent(oldNode, normalizedContent, ctx); | ||
if (typeof newContent === 'string') { | ||
newContent = parseContent(newContent); | ||
} | ||
function morphNormalizedContent(oldNode, normalizedNewContent, ctx) { | ||
if (ctx.head.block) { | ||
let oldHead = oldNode.querySelector('head'); | ||
let newHead = normalizedNewContent.querySelector('head'); | ||
if (oldHead && newHead) { | ||
let promises = handleHeadElement(newHead, oldHead, ctx); | ||
// when head promises resolve, call morph again, ignoring the head tag | ||
Promise.all(promises).then(function () { | ||
morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, { | ||
head: { | ||
block: false, | ||
ignore: true | ||
} | ||
})); | ||
}); | ||
return; | ||
} | ||
let normalizedContent = normalizeContent(newContent); | ||
let ctx = createMorphContext(oldNode, normalizedContent, config); | ||
return morphNormalizedContent(oldNode, normalizedContent, ctx); | ||
} | ||
function morphNormalizedContent(oldNode, normalizedNewContent, ctx) { | ||
if (ctx.head.block) { | ||
let oldHead = oldNode.querySelector('head'); | ||
let newHead = normalizedNewContent.querySelector('head'); | ||
if (oldHead && newHead) { | ||
let promises = handleHeadElement(newHead, oldHead, ctx); | ||
// when head promises resolve, call morph again, ignoring the head tag | ||
Promise.all(promises).then(function () { | ||
morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, { | ||
head: { | ||
block: false, | ||
ignore: true | ||
} | ||
})); | ||
}); | ||
return; | ||
} | ||
} | ||
if (ctx.morphStyle === "innerHTML") { | ||
if (ctx.morphStyle === "innerHTML") { | ||
// innerHTML, so we are only updating the children | ||
morphChildren(normalizedNewContent, oldNode, ctx); | ||
return oldNode.children; | ||
// innerHTML, so we are only updating the children | ||
morphChildren(normalizedNewContent, oldNode, ctx); | ||
return oldNode.children; | ||
} else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) { | ||
// otherwise find the best element match in the new content, morph that, and merge its siblings | ||
// into either side of the best match | ||
let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx); | ||
} else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) { | ||
// otherwise find the best element match in the new content, morph that, and merge its siblings | ||
// into either side of the best match | ||
let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx); | ||
// stash the siblings that will need to be inserted on either side of the best match | ||
let previousSibling = bestMatch?.previousSibling; | ||
let nextSibling = bestMatch?.nextSibling; | ||
// stash the siblings that will need to be inserted on either side of the best match | ||
let previousSibling = bestMatch?.previousSibling; | ||
let nextSibling = bestMatch?.nextSibling; | ||
// morph it | ||
let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx); | ||
// morph it | ||
let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx); | ||
if (bestMatch) { | ||
// if there was a best match, merge the siblings in too and return the | ||
// whole bunch | ||
return insertSiblings(previousSibling, morphedNode, nextSibling); | ||
} else { | ||
// otherwise nothing was added to the DOM | ||
return [] | ||
} | ||
if (bestMatch) { | ||
// if there was a best match, merge the siblings in too and return the | ||
// whole bunch | ||
return insertSiblings(previousSibling, morphedNode, nextSibling); | ||
} else { | ||
throw "Do not understand how to morph style " + ctx.morphStyle; | ||
// otherwise nothing was added to the DOM | ||
return [] | ||
} | ||
} else { | ||
throw "Do not understand how to morph style " + ctx.morphStyle; | ||
} | ||
} | ||
/** | ||
* @param possibleActiveElement | ||
* @param ctx | ||
* @returns {boolean} | ||
*/ | ||
function ignoreValueOfActiveElement(possibleActiveElement, ctx) { | ||
return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement; | ||
} | ||
/** | ||
* @param possibleActiveElement | ||
* @param ctx | ||
* @returns {boolean} | ||
*/ | ||
function ignoreValueOfActiveElement(possibleActiveElement, ctx) { | ||
return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement; | ||
} | ||
/** | ||
* @param oldNode root node to merge content into | ||
* @param newContent new content to merge | ||
* @param ctx the merge context | ||
* @returns {Element} the element that ended up in the DOM | ||
*/ | ||
function morphOldNodeTo(oldNode, newContent, ctx) { | ||
if (ctx.ignoreActive && oldNode === document.activeElement) { | ||
// don't morph focused element | ||
} else if (newContent == null) { | ||
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return; | ||
/** | ||
* @param oldNode root node to merge content into | ||
* @param newContent new content to merge | ||
* @param ctx the merge context | ||
* @returns {Element} the element that ended up in the DOM | ||
*/ | ||
function morphOldNodeTo(oldNode, newContent, ctx) { | ||
if (ctx.ignoreActive && oldNode === document.activeElement) { | ||
// don't morph focused element | ||
} else if (newContent == null) { | ||
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode; | ||
oldNode.remove(); | ||
ctx.callbacks.afterNodeRemoved(oldNode); | ||
return null; | ||
} else if (!isSoftMatch(oldNode, newContent)) { | ||
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return; | ||
if (ctx.callbacks.beforeNodeAdded(newContent) === false) return; | ||
oldNode.remove(); | ||
ctx.callbacks.afterNodeRemoved(oldNode); | ||
return null; | ||
} else if (!isSoftMatch(oldNode, newContent)) { | ||
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode; | ||
if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode; | ||
oldNode.parentElement.replaceChild(newContent, oldNode); | ||
ctx.callbacks.afterNodeAdded(newContent); | ||
ctx.callbacks.afterNodeRemoved(oldNode); | ||
return newContent; | ||
oldNode.parentElement.replaceChild(newContent, oldNode); | ||
ctx.callbacks.afterNodeAdded(newContent); | ||
ctx.callbacks.afterNodeRemoved(oldNode); | ||
return newContent; | ||
} else { | ||
if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return oldNode; | ||
if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) { | ||
// ignore the head element | ||
} else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") { | ||
handleHeadElement(newContent, oldNode, ctx); | ||
} else { | ||
if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return; | ||
if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) { | ||
// ignore the head element | ||
} else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") { | ||
handleHeadElement(newContent, oldNode, ctx); | ||
} else { | ||
syncNodeFrom(newContent, oldNode, ctx); | ||
if (!ignoreValueOfActiveElement(oldNode, ctx)) { | ||
morphChildren(newContent, oldNode, ctx); | ||
} | ||
syncNodeFrom(newContent, oldNode, ctx); | ||
if (!ignoreValueOfActiveElement(oldNode, ctx)) { | ||
morphChildren(newContent, oldNode, ctx); | ||
} | ||
ctx.callbacks.afterNodeMorphed(oldNode, newContent); | ||
return oldNode; | ||
} | ||
ctx.callbacks.afterNodeMorphed(oldNode, newContent); | ||
return oldNode; | ||
} | ||
} | ||
/** | ||
* This is the core algorithm for matching up children. The idea is to use id sets to try to match up | ||
* nodes as faithfully as possible. We greedily match, which allows us to keep the algorithm fast, but | ||
* by using id sets, we are able to better match up with content deeper in the DOM. | ||
* | ||
* Basic algorithm is, for each node in the new content: | ||
* | ||
* - if we have reached the end of the old parent, append the new content | ||
* - if the new content has an id set match with the current insertion point, morph | ||
* - search for an id set match | ||
* - if id set match found, morph | ||
* - otherwise search for a "soft" match | ||
* - if a soft match is found, morph | ||
* - otherwise, prepend the new node before the current insertion point | ||
* | ||
* The two search algorithms terminate if competing node matches appear to outweigh what can be achieved | ||
* with the current node. See findIdSetMatch() and findSoftMatch() for details. | ||
* | ||
* @param {Element} newParent the parent element of the new content | ||
* @param {Element } oldParent the old content that we are merging the new content into | ||
* @param ctx the merge context | ||
*/ | ||
function morphChildren(newParent, oldParent, ctx) { | ||
/** | ||
* This is the core algorithm for matching up children. The idea is to use id sets to try to match up | ||
* nodes as faithfully as possible. We greedily match, which allows us to keep the algorithm fast, but | ||
* by using id sets, we are able to better match up with content deeper in the DOM. | ||
* | ||
* Basic algorithm is, for each node in the new content: | ||
* | ||
* - if we have reached the end of the old parent, append the new content | ||
* - if the new content has an id set match with the current insertion point, morph | ||
* - search for an id set match | ||
* - if id set match found, morph | ||
* - otherwise search for a "soft" match | ||
* - if a soft match is found, morph | ||
* - otherwise, prepend the new node before the current insertion point | ||
* | ||
* The two search algorithms terminate if competing node matches appear to outweigh what can be achieved | ||
* with the current node. See findIdSetMatch() and findSoftMatch() for details. | ||
* | ||
* @param {Element} newParent the parent element of the new content | ||
* @param {Element } oldParent the old content that we are merging the new content into | ||
* @param ctx the merge context | ||
*/ | ||
function morphChildren(newParent, oldParent, ctx) { | ||
let nextNewChild = newParent.firstChild; | ||
let insertionPoint = oldParent.firstChild; | ||
let newChild; | ||
let nextNewChild = newParent.firstChild; | ||
let insertionPoint = oldParent.firstChild; | ||
let newChild; | ||
// run through all the new content | ||
while (nextNewChild) { | ||
// run through all the new content | ||
while (nextNewChild) { | ||
newChild = nextNewChild; | ||
nextNewChild = newChild.nextSibling; | ||
newChild = nextNewChild; | ||
nextNewChild = newChild.nextSibling; | ||
// if we are at the end of the exiting parent's children, just append | ||
if (insertionPoint == null) { | ||
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return; | ||
// if we are at the end of the exiting parent's children, just append | ||
if (insertionPoint == null) { | ||
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return; | ||
oldParent.appendChild(newChild); | ||
ctx.callbacks.afterNodeAdded(newChild); | ||
removeIdsFromConsideration(ctx, newChild); | ||
continue; | ||
} | ||
oldParent.appendChild(newChild); | ||
ctx.callbacks.afterNodeAdded(newChild); | ||
removeIdsFromConsideration(ctx, newChild); | ||
continue; | ||
} | ||
// if the current node has an id set match then morph | ||
if (isIdSetMatch(newChild, insertionPoint, ctx)) { | ||
morphOldNodeTo(insertionPoint, newChild, ctx); | ||
insertionPoint = insertionPoint.nextSibling; | ||
removeIdsFromConsideration(ctx, newChild); | ||
continue; | ||
} | ||
// if the current node has an id set match then morph | ||
if (isIdSetMatch(newChild, insertionPoint, ctx)) { | ||
morphOldNodeTo(insertionPoint, newChild, ctx); | ||
insertionPoint = insertionPoint.nextSibling; | ||
removeIdsFromConsideration(ctx, newChild); | ||
continue; | ||
} | ||
// otherwise search forward in the existing old children for an id set match | ||
let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx); | ||
// otherwise search forward in the existing old children for an id set match | ||
let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx); | ||
// if we found a potential match, remove the nodes until that point and morph | ||
if (idSetMatch) { | ||
insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx); | ||
morphOldNodeTo(idSetMatch, newChild, ctx); | ||
removeIdsFromConsideration(ctx, newChild); | ||
continue; | ||
} | ||
// if we found a potential match, remove the nodes until that point and morph | ||
if (idSetMatch) { | ||
insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx); | ||
morphOldNodeTo(idSetMatch, newChild, ctx); | ||
removeIdsFromConsideration(ctx, newChild); | ||
continue; | ||
} | ||
// no id set match found, so scan forward for a soft match for the current node | ||
let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx); | ||
// no id set match found, so scan forward for a soft match for the current node | ||
let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx); | ||
// if we found a soft match for the current node, morph | ||
if (softMatch) { | ||
insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx); | ||
morphOldNodeTo(softMatch, newChild, ctx); | ||
removeIdsFromConsideration(ctx, newChild); | ||
continue; | ||
} | ||
// abandon all hope of morphing, just insert the new child before the insertion point | ||
// and move on | ||
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return; | ||
oldParent.insertBefore(newChild, insertionPoint); | ||
ctx.callbacks.afterNodeAdded(newChild); | ||
// if we found a soft match for the current node, morph | ||
if (softMatch) { | ||
insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx); | ||
morphOldNodeTo(softMatch, newChild, ctx); | ||
removeIdsFromConsideration(ctx, newChild); | ||
continue; | ||
} | ||
// remove any remaining old nodes that didn't match up with new content | ||
while (insertionPoint !== null) { | ||
// abandon all hope of morphing, just insert the new child before the insertion point | ||
// and move on | ||
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return; | ||
let tempNode = insertionPoint; | ||
insertionPoint = insertionPoint.nextSibling; | ||
removeNode(tempNode, ctx); | ||
} | ||
oldParent.insertBefore(newChild, insertionPoint); | ||
ctx.callbacks.afterNodeAdded(newChild); | ||
removeIdsFromConsideration(ctx, newChild); | ||
} | ||
//============================================================================= | ||
// Attribute Syncing Code | ||
//============================================================================= | ||
// remove any remaining old nodes that didn't match up with new content | ||
while (insertionPoint !== null) { | ||
/** | ||
* syncs a given node with another node, copying over all attributes and | ||
* inner element state from the 'from' node to the 'to' node | ||
* | ||
* @param {Element} from the element to copy attributes & state from | ||
* @param {Element} to the element to copy attributes & state to | ||
*/ | ||
function syncNodeFrom(from, to, ctx) { | ||
let type = from.nodeType | ||
let tempNode = insertionPoint; | ||
insertionPoint = insertionPoint.nextSibling; | ||
removeNode(tempNode, ctx); | ||
} | ||
} | ||
// if is an element type, sync the attributes from the | ||
// new node into the new node | ||
if (type === 1 /* element type */) { | ||
const fromAttributes = from.attributes; | ||
const toAttributes = to.attributes; | ||
for (const fromAttribute of fromAttributes) { | ||
if (fromAttribute.name === 'value' && ignoreValueOfActiveElement(to, ctx)) { | ||
continue; | ||
} | ||
if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) { | ||
to.setAttribute(fromAttribute.name, fromAttribute.value); | ||
} | ||
//============================================================================= | ||
// Attribute Syncing Code | ||
//============================================================================= | ||
/** | ||
* @param attr {String} the attribute to be mutated | ||
* @param to {Element} the element that is going to be updated | ||
* @param updateType {("update"|"remove")} | ||
* @param ctx the merge context | ||
* @returns {boolean} true if the attribute should be ignored, false otherwise | ||
*/ | ||
function ignoreAttribute(attr, to, updateType, ctx) { | ||
if(attr === 'value' && ctx.ignoreActiveValue && to === document.activeElement){ | ||
return true; | ||
} | ||
return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false; | ||
} | ||
/** | ||
* syncs a given node with another node, copying over all attributes and | ||
* inner element state from the 'from' node to the 'to' node | ||
* | ||
* @param {Element} from the element to copy attributes & state from | ||
* @param {Element} to the element to copy attributes & state to | ||
* @param ctx the merge context | ||
*/ | ||
function syncNodeFrom(from, to, ctx) { | ||
let type = from.nodeType | ||
// if is an element type, sync the attributes from the | ||
// new node into the new node | ||
if (type === 1 /* element type */) { | ||
const fromAttributes = from.attributes; | ||
const toAttributes = to.attributes; | ||
for (const fromAttribute of fromAttributes) { | ||
if (ignoreAttribute(fromAttribute.name, to, 'update', ctx)) { | ||
continue; | ||
} | ||
// iterate backwards to avoid skipping over items when a delete occurs | ||
for (let i = toAttributes.length - 1; 0 <= i; i--) { | ||
const toAttribute = toAttributes[i]; | ||
if (!from.hasAttribute(toAttribute.name)) { | ||
to.removeAttribute(toAttribute.name); | ||
} | ||
if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) { | ||
to.setAttribute(fromAttribute.name, fromAttribute.value); | ||
} | ||
} | ||
// sync text nodes | ||
if (type === 8 /* comment */ || type === 3 /* text */) { | ||
if (to.nodeValue !== from.nodeValue) { | ||
to.nodeValue = from.nodeValue; | ||
// iterate backwards to avoid skipping over items when a delete occurs | ||
for (let i = toAttributes.length - 1; 0 <= i; i--) { | ||
const toAttribute = toAttributes[i]; | ||
if (ignoreAttribute(toAttribute.name, to, 'remove', ctx)) { | ||
continue; | ||
} | ||
if (!from.hasAttribute(toAttribute.name)) { | ||
to.removeAttribute(toAttribute.name); | ||
} | ||
} | ||
} | ||
if (!ignoreValueOfActiveElement(to, ctx)) { | ||
// sync input values | ||
syncInputValue(from, to); | ||
// sync text nodes | ||
if (type === 8 /* comment */ || type === 3 /* text */) { | ||
if (to.nodeValue !== from.nodeValue) { | ||
to.nodeValue = from.nodeValue; | ||
} | ||
} | ||
function syncBooleanAttribute(from, to, attributeName) { | ||
if (from[attributeName] !== to[attributeName]) { | ||
if (!ignoreValueOfActiveElement(to, ctx)) { | ||
// sync input values | ||
syncInputValue(from, to, ctx); | ||
} | ||
} | ||
/** | ||
* @param from {Element} element to sync the value from | ||
* @param to {Element} element to sync the value to | ||
* @param attributeName {String} the attribute name | ||
* @param ctx the merge context | ||
*/ | ||
function syncBooleanAttribute(from, to, attributeName, ctx) { | ||
if (from[attributeName] !== to[attributeName]) { | ||
let ignoreUpdate = ignoreAttribute(attributeName, to, 'update', ctx); | ||
if (!ignoreUpdate) { | ||
to[attributeName] = from[attributeName]; | ||
if (from[attributeName]) { | ||
} | ||
if (from[attributeName]) { | ||
if (!ignoreUpdate) { | ||
to.setAttribute(attributeName, from[attributeName]); | ||
} else { | ||
} | ||
} else { | ||
if (!ignoreAttribute(attributeName, to, 'remove', ctx)) { | ||
to.removeAttribute(attributeName); | ||
@@ -303,493 +338,512 @@ } | ||
} | ||
} | ||
// NB: many bothans died to bring us information: | ||
// | ||
// https://github.com/patrick-steele-idem/morphdom/blob/master/src/specialElHandlers.js | ||
// https://github.com/choojs/nanomorph/blob/master/lib/morph.jsL113 | ||
function syncInputValue(from, to) { | ||
if (from instanceof HTMLInputElement && | ||
to instanceof HTMLInputElement && | ||
from.type !== 'file') { | ||
/** | ||
* NB: many bothans died to bring us information: | ||
* | ||
* https://github.com/patrick-steele-idem/morphdom/blob/master/src/specialElHandlers.js | ||
* https://github.com/choojs/nanomorph/blob/master/lib/morph.jsL113 | ||
* | ||
* @param from {Element} the element to sync the input value from | ||
* @param to {Element} the element to sync the input value to | ||
* @param ctx the merge context | ||
*/ | ||
function syncInputValue(from, to, ctx) { | ||
if (from instanceof HTMLInputElement && | ||
to instanceof HTMLInputElement && | ||
from.type !== 'file') { | ||
let fromValue = from.value; | ||
let toValue = to.value; | ||
let fromValue = from.value; | ||
let toValue = to.value; | ||
// sync boolean attributes | ||
syncBooleanAttribute(from, to, 'checked'); | ||
syncBooleanAttribute(from, to, 'disabled'); | ||
// sync boolean attributes | ||
syncBooleanAttribute(from, to, 'checked', ctx); | ||
syncBooleanAttribute(from, to, 'disabled', ctx); | ||
if (!from.hasAttribute('value')) { | ||
if (!from.hasAttribute('value')) { | ||
if (!ignoreAttribute('value', to, 'remove', ctx)) { | ||
to.value = ''; | ||
to.removeAttribute('value'); | ||
} else if (fromValue !== toValue) { | ||
} | ||
} else if (fromValue !== toValue) { | ||
if (!ignoreAttribute('value', to, 'update', ctx)) { | ||
to.setAttribute('value', fromValue); | ||
to.value = fromValue; | ||
} | ||
} else if (from instanceof HTMLOptionElement) { | ||
syncBooleanAttribute(from, to, 'selected') | ||
} else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) { | ||
let fromValue = from.value; | ||
let toValue = to.value; | ||
if (fromValue !== toValue) { | ||
to.value = fromValue; | ||
} | ||
if (to.firstChild && to.firstChild.nodeValue !== fromValue) { | ||
to.firstChild.nodeValue = fromValue | ||
} | ||
} | ||
} else if (from instanceof HTMLOptionElement) { | ||
syncBooleanAttribute(from, to, 'selected', ctx) | ||
} else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) { | ||
let fromValue = from.value; | ||
let toValue = to.value; | ||
if (ignoreAttribute('value', to, 'update', ctx)) { | ||
return; | ||
} | ||
if (fromValue !== toValue) { | ||
to.value = fromValue; | ||
} | ||
if (to.firstChild && to.firstChild.nodeValue !== fromValue) { | ||
to.firstChild.nodeValue = fromValue | ||
} | ||
} | ||
} | ||
//============================================================================= | ||
// the HEAD tag can be handled specially, either w/ a 'merge' or 'append' style | ||
//============================================================================= | ||
function handleHeadElement(newHeadTag, currentHead, ctx) { | ||
//============================================================================= | ||
// the HEAD tag can be handled specially, either w/ a 'merge' or 'append' style | ||
//============================================================================= | ||
function handleHeadElement(newHeadTag, currentHead, ctx) { | ||
let added = [] | ||
let removed = [] | ||
let preserved = [] | ||
let nodesToAppend = [] | ||
let added = [] | ||
let removed = [] | ||
let preserved = [] | ||
let nodesToAppend = [] | ||
let headMergeStyle = ctx.head.style; | ||
let headMergeStyle = ctx.head.style; | ||
// put all new head elements into a Map, by their outerHTML | ||
let srcToNewHeadNodes = new Map(); | ||
for (const newHeadChild of newHeadTag.children) { | ||
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild); | ||
} | ||
// put all new head elements into a Map, by their outerHTML | ||
let srcToNewHeadNodes = new Map(); | ||
for (const newHeadChild of newHeadTag.children) { | ||
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild); | ||
} | ||
// for each elt in the current head | ||
for (const currentHeadElt of currentHead.children) { | ||
// for each elt in the current head | ||
for (const currentHeadElt of currentHead.children) { | ||
// If the current head element is in the map | ||
let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML); | ||
let isReAppended = ctx.head.shouldReAppend(currentHeadElt); | ||
let isPreserved = ctx.head.shouldPreserve(currentHeadElt); | ||
if (inNewContent || isPreserved) { | ||
// If the current head element is in the map | ||
let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML); | ||
let isReAppended = ctx.head.shouldReAppend(currentHeadElt); | ||
let isPreserved = ctx.head.shouldPreserve(currentHeadElt); | ||
if (inNewContent || isPreserved) { | ||
if (isReAppended) { | ||
// remove the current version and let the new version replace it and re-execute | ||
removed.push(currentHeadElt); | ||
} else { | ||
// this element already exists and should not be re-appended, so remove it from | ||
// the new content map, preserving it in the DOM | ||
srcToNewHeadNodes.delete(currentHeadElt.outerHTML); | ||
preserved.push(currentHeadElt); | ||
} | ||
} else { | ||
if (headMergeStyle === "append") { | ||
// we are appending and this existing element is not new content | ||
// so if and only if it is marked for re-append do we do anything | ||
if (isReAppended) { | ||
// remove the current version and let the new version replace it and re-execute | ||
removed.push(currentHeadElt); | ||
} else { | ||
// this element already exists and should not be re-appended, so remove it from | ||
// the new content map, preserving it in the DOM | ||
srcToNewHeadNodes.delete(currentHeadElt.outerHTML); | ||
preserved.push(currentHeadElt); | ||
nodesToAppend.push(currentHeadElt); | ||
} | ||
} else { | ||
if (headMergeStyle === "append") { | ||
// we are appending and this existing element is not new content | ||
// so if and only if it is marked for re-append do we do anything | ||
if (isReAppended) { | ||
removed.push(currentHeadElt); | ||
nodesToAppend.push(currentHeadElt); | ||
} | ||
} else { | ||
// if this is a merge, we remove this content since it is not in the new head | ||
if (ctx.head.shouldRemove(currentHeadElt) !== false) { | ||
removed.push(currentHeadElt); | ||
} | ||
// if this is a merge, we remove this content since it is not in the new head | ||
if (ctx.head.shouldRemove(currentHeadElt) !== false) { | ||
removed.push(currentHeadElt); | ||
} | ||
} | ||
} | ||
} | ||
// Push the remaining new head elements in the Map into the | ||
// nodes to append to the head tag | ||
nodesToAppend.push(...srcToNewHeadNodes.values()); | ||
log("to append: ", nodesToAppend); | ||
// Push the remaining new head elements in the Map into the | ||
// nodes to append to the head tag | ||
nodesToAppend.push(...srcToNewHeadNodes.values()); | ||
log("to append: ", nodesToAppend); | ||
let promises = []; | ||
for (const newNode of nodesToAppend) { | ||
log("adding: ", newNode); | ||
let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild; | ||
log(newElt); | ||
if (ctx.callbacks.beforeNodeAdded(newElt) !== false) { | ||
if (newElt.href || newElt.src) { | ||
let resolve = null; | ||
let promise = new Promise(function (_resolve) { | ||
resolve = _resolve; | ||
}); | ||
newElt.addEventListener('load',function() { | ||
resolve(); | ||
}); | ||
promises.push(promise); | ||
} | ||
currentHead.appendChild(newElt); | ||
ctx.callbacks.afterNodeAdded(newElt); | ||
added.push(newElt); | ||
let promises = []; | ||
for (const newNode of nodesToAppend) { | ||
log("adding: ", newNode); | ||
let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild; | ||
log(newElt); | ||
if (ctx.callbacks.beforeNodeAdded(newElt) !== false) { | ||
if (newElt.href || newElt.src) { | ||
let resolve = null; | ||
let promise = new Promise(function (_resolve) { | ||
resolve = _resolve; | ||
}); | ||
newElt.addEventListener('load', function () { | ||
resolve(); | ||
}); | ||
promises.push(promise); | ||
} | ||
currentHead.appendChild(newElt); | ||
ctx.callbacks.afterNodeAdded(newElt); | ||
added.push(newElt); | ||
} | ||
} | ||
// remove all removed elements, after we have appended the new elements to avoid | ||
// additional network requests for things like style sheets | ||
for (const removedElement of removed) { | ||
if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) { | ||
currentHead.removeChild(removedElement); | ||
ctx.callbacks.afterNodeRemoved(removedElement); | ||
} | ||
// remove all removed elements, after we have appended the new elements to avoid | ||
// additional network requests for things like style sheets | ||
for (const removedElement of removed) { | ||
if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) { | ||
currentHead.removeChild(removedElement); | ||
ctx.callbacks.afterNodeRemoved(removedElement); | ||
} | ||
ctx.head.afterHeadMorphed(currentHead, {added: added, kept: preserved, removed: removed}); | ||
return promises; | ||
} | ||
//============================================================================= | ||
// Misc | ||
//============================================================================= | ||
ctx.head.afterHeadMorphed(currentHead, {added: added, kept: preserved, removed: removed}); | ||
return promises; | ||
} | ||
function log() { | ||
//console.log(arguments); | ||
} | ||
//============================================================================= | ||
// Misc | ||
//============================================================================= | ||
function noOp() {} | ||
function log() { | ||
//console.log(arguments); | ||
} | ||
function createMorphContext(oldNode, newContent, config) { | ||
return { | ||
target:oldNode, | ||
newContent: newContent, | ||
config: config, | ||
morphStyle : config.morphStyle, | ||
ignoreActive : config.ignoreActive, | ||
ignoreActiveValue : config.ignoreActiveValue, | ||
idMap: createIdMap(oldNode, newContent), | ||
deadIds: new Set(), | ||
callbacks: Object.assign({ | ||
beforeNodeAdded: noOp, | ||
afterNodeAdded : noOp, | ||
beforeNodeMorphed: noOp, | ||
afterNodeMorphed : noOp, | ||
beforeNodeRemoved: noOp, | ||
afterNodeRemoved : noOp, | ||
function noOp() { | ||
} | ||
}, config.callbacks), | ||
head: Object.assign({ | ||
style: 'merge', | ||
shouldPreserve : function(elt) { | ||
return elt.getAttribute("im-preserve") === "true"; | ||
}, | ||
shouldReAppend : function(elt) { | ||
return elt.getAttribute("im-re-append") === "true"; | ||
}, | ||
shouldRemove : noOp, | ||
afterHeadMorphed : noOp, | ||
}, config.head), | ||
} | ||
/* | ||
Deep merges the config object and the Idiomoroph.defaults object to | ||
produce a final configuration object | ||
*/ | ||
function mergeDefaults(config) { | ||
let finalConfig = {}; | ||
// copy top level stuff into final config | ||
Object.assign(finalConfig, defaults); | ||
Object.assign(finalConfig, config); | ||
// copy callbacks into final config (do this to deep merge the callbacks) | ||
finalConfig.callbacks = {}; | ||
Object.assign(finalConfig.callbacks, defaults.callbacks); | ||
Object.assign(finalConfig.callbacks, config.callbacks); | ||
// copy head config into final config (do this to deep merge the head) | ||
finalConfig.head = {}; | ||
Object.assign(finalConfig.head, defaults.head); | ||
Object.assign(finalConfig.head, config.head); | ||
return finalConfig; | ||
} | ||
function createMorphContext(oldNode, newContent, config) { | ||
config = mergeDefaults(config); | ||
return { | ||
target: oldNode, | ||
newContent: newContent, | ||
config: config, | ||
morphStyle: config.morphStyle, | ||
ignoreActive: config.ignoreActive, | ||
ignoreActiveValue: config.ignoreActiveValue, | ||
idMap: createIdMap(oldNode, newContent), | ||
deadIds: new Set(), | ||
callbacks: config.callbacks, | ||
head: config.head | ||
} | ||
} | ||
function isIdSetMatch(node1, node2, ctx) { | ||
if (node1 == null || node2 == null) { | ||
return false; | ||
} | ||
if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) { | ||
if (node1.id !== "" && node1.id === node2.id) { | ||
return true; | ||
} else { | ||
return getIdIntersectionCount(ctx, node1, node2) > 0; | ||
} | ||
} | ||
function isIdSetMatch(node1, node2, ctx) { | ||
if (node1 == null || node2 == null) { | ||
return false; | ||
} | ||
function isSoftMatch(node1, node2) { | ||
if (node1 == null || node2 == null) { | ||
return false; | ||
if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) { | ||
if (node1.id !== "" && node1.id === node2.id) { | ||
return true; | ||
} else { | ||
return getIdIntersectionCount(ctx, node1, node2) > 0; | ||
} | ||
return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName | ||
} | ||
return false; | ||
} | ||
function removeNodesBetween(startInclusive, endExclusive, ctx) { | ||
while (startInclusive !== endExclusive) { | ||
let tempNode = startInclusive; | ||
startInclusive = startInclusive.nextSibling; | ||
removeNode(tempNode, ctx); | ||
} | ||
removeIdsFromConsideration(ctx, endExclusive); | ||
return endExclusive.nextSibling; | ||
function isSoftMatch(node1, node2) { | ||
if (node1 == null || node2 == null) { | ||
return false; | ||
} | ||
return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName | ||
} | ||
//============================================================================= | ||
// Scans forward from the insertionPoint in the old parent looking for a potential id match | ||
// for the newChild. We stop if we find a potential id match for the new child OR | ||
// if the number of potential id matches we are discarding is greater than the | ||
// potential id matches for the new child | ||
//============================================================================= | ||
function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) { | ||
function removeNodesBetween(startInclusive, endExclusive, ctx) { | ||
while (startInclusive !== endExclusive) { | ||
let tempNode = startInclusive; | ||
startInclusive = startInclusive.nextSibling; | ||
removeNode(tempNode, ctx); | ||
} | ||
removeIdsFromConsideration(ctx, endExclusive); | ||
return endExclusive.nextSibling; | ||
} | ||
// max id matches we are willing to discard in our search | ||
let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent); | ||
//============================================================================= | ||
// Scans forward from the insertionPoint in the old parent looking for a potential id match | ||
// for the newChild. We stop if we find a potential id match for the new child OR | ||
// if the number of potential id matches we are discarding is greater than the | ||
// potential id matches for the new child | ||
//============================================================================= | ||
function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) { | ||
let potentialMatch = null; | ||
// max id matches we are willing to discard in our search | ||
let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent); | ||
// only search forward if there is a possibility of an id match | ||
if (newChildPotentialIdCount > 0) { | ||
let potentialMatch = insertionPoint; | ||
// if there is a possibility of an id match, scan forward | ||
// keep track of the potential id match count we are discarding (the | ||
// newChildPotentialIdCount must be greater than this to make it likely | ||
// worth it) | ||
let otherMatchCount = 0; | ||
while (potentialMatch != null) { | ||
let potentialMatch = null; | ||
// If we have an id match, return the current potential match | ||
if (isIdSetMatch(newChild, potentialMatch, ctx)) { | ||
return potentialMatch; | ||
} | ||
// only search forward if there is a possibility of an id match | ||
if (newChildPotentialIdCount > 0) { | ||
let potentialMatch = insertionPoint; | ||
// if there is a possibility of an id match, scan forward | ||
// keep track of the potential id match count we are discarding (the | ||
// newChildPotentialIdCount must be greater than this to make it likely | ||
// worth it) | ||
let otherMatchCount = 0; | ||
while (potentialMatch != null) { | ||
// computer the other potential matches of this new content | ||
otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent); | ||
if (otherMatchCount > newChildPotentialIdCount) { | ||
// if we have more potential id matches in _other_ content, we | ||
// do not have a good candidate for an id match, so return null | ||
return null; | ||
} | ||
// If we have an id match, return the current potential match | ||
if (isIdSetMatch(newChild, potentialMatch, ctx)) { | ||
return potentialMatch; | ||
} | ||
// advanced to the next old content child | ||
potentialMatch = potentialMatch.nextSibling; | ||
// computer the other potential matches of this new content | ||
otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent); | ||
if (otherMatchCount > newChildPotentialIdCount) { | ||
// if we have more potential id matches in _other_ content, we | ||
// do not have a good candidate for an id match, so return null | ||
return null; | ||
} | ||
// advanced to the next old content child | ||
potentialMatch = potentialMatch.nextSibling; | ||
} | ||
return potentialMatch; | ||
} | ||
return potentialMatch; | ||
} | ||
//============================================================================= | ||
// Scans forward from the insertionPoint in the old parent looking for a potential soft match | ||
// for the newChild. We stop if we find a potential soft match for the new child OR | ||
// if we find a potential id match in the old parents children OR if we find two | ||
// potential soft matches for the next two pieces of new content | ||
//============================================================================= | ||
function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) { | ||
//============================================================================= | ||
// Scans forward from the insertionPoint in the old parent looking for a potential soft match | ||
// for the newChild. We stop if we find a potential soft match for the new child OR | ||
// if we find a potential id match in the old parents children OR if we find two | ||
// potential soft matches for the next two pieces of new content | ||
//============================================================================= | ||
function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) { | ||
let potentialSoftMatch = insertionPoint; | ||
let nextSibling = newChild.nextSibling; | ||
let siblingSoftMatchCount = 0; | ||
let potentialSoftMatch = insertionPoint; | ||
let nextSibling = newChild.nextSibling; | ||
let siblingSoftMatchCount = 0; | ||
while (potentialSoftMatch != null) { | ||
while (potentialSoftMatch != null) { | ||
if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) { | ||
// the current potential soft match has a potential id set match with the remaining new | ||
// content so bail out of looking | ||
return null; | ||
} | ||
if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) { | ||
// the current potential soft match has a potential id set match with the remaining new | ||
// content so bail out of looking | ||
return null; | ||
} | ||
// if we have a soft match with the current node, return it | ||
if (isSoftMatch(newChild, potentialSoftMatch)) { | ||
return potentialSoftMatch; | ||
} | ||
// if we have a soft match with the current node, return it | ||
if (isSoftMatch(newChild, potentialSoftMatch)) { | ||
return potentialSoftMatch; | ||
} | ||
if (isSoftMatch(nextSibling, potentialSoftMatch)) { | ||
// the next new node has a soft match with this node, so | ||
// increment the count of future soft matches | ||
siblingSoftMatchCount++; | ||
nextSibling = nextSibling.nextSibling; | ||
if (isSoftMatch(nextSibling, potentialSoftMatch)) { | ||
// the next new node has a soft match with this node, so | ||
// increment the count of future soft matches | ||
siblingSoftMatchCount++; | ||
nextSibling = nextSibling.nextSibling; | ||
// If there are two future soft matches, bail to allow the siblings to soft match | ||
// so that we don't consume future soft matches for the sake of the current node | ||
if (siblingSoftMatchCount >= 2) { | ||
return null; | ||
} | ||
// If there are two future soft matches, bail to allow the siblings to soft match | ||
// so that we don't consume future soft matches for the sake of the current node | ||
if (siblingSoftMatchCount >= 2) { | ||
return null; | ||
} | ||
// advanced to the next old content child | ||
potentialSoftMatch = potentialSoftMatch.nextSibling; | ||
} | ||
return potentialSoftMatch; | ||
// advanced to the next old content child | ||
potentialSoftMatch = potentialSoftMatch.nextSibling; | ||
} | ||
function parseContent(newContent) { | ||
let parser = new DOMParser(); | ||
return potentialSoftMatch; | ||
} | ||
// remove svgs to avoid false-positive matches on head, etc. | ||
let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, ''); | ||
function parseContent(newContent) { | ||
let parser = new DOMParser(); | ||
// if the newContent contains a html, head or body tag, we can simply parse it w/o wrapping | ||
if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) { | ||
let content = parser.parseFromString(newContent, "text/html"); | ||
// if it is a full HTML document, return the document itself as the parent container | ||
if (contentWithSvgsRemoved.match(/<\/html>/)) { | ||
content.generatedByIdiomorph = true; | ||
return content; | ||
// remove svgs to avoid false-positive matches on head, etc. | ||
let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, ''); | ||
// if the newContent contains a html, head or body tag, we can simply parse it w/o wrapping | ||
if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) { | ||
let content = parser.parseFromString(newContent, "text/html"); | ||
// if it is a full HTML document, return the document itself as the parent container | ||
if (contentWithSvgsRemoved.match(/<\/html>/)) { | ||
content.generatedByIdiomorph = true; | ||
return content; | ||
} else { | ||
// otherwise return the html element as the parent container | ||
let htmlElement = content.firstChild; | ||
if (htmlElement) { | ||
htmlElement.generatedByIdiomorph = true; | ||
return htmlElement; | ||
} else { | ||
// otherwise return the html element as the parent container | ||
let htmlElement = content.firstChild; | ||
if (htmlElement) { | ||
htmlElement.generatedByIdiomorph = true; | ||
return htmlElement; | ||
} else { | ||
return null; | ||
} | ||
return null; | ||
} | ||
} else { | ||
// if it is partial HTML, wrap it in a template tag to provide a parent element and also to help | ||
// deal with touchy tags like tr, tbody, etc. | ||
let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html"); | ||
let content = responseDoc.body.querySelector('template').content; | ||
content.generatedByIdiomorph = true; | ||
return content | ||
} | ||
} else { | ||
// if it is partial HTML, wrap it in a template tag to provide a parent element and also to help | ||
// deal with touchy tags like tr, tbody, etc. | ||
let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html"); | ||
let content = responseDoc.body.querySelector('template').content; | ||
content.generatedByIdiomorph = true; | ||
return content | ||
} | ||
} | ||
function normalizeContent(newContent) { | ||
if (newContent == null) { | ||
// noinspection UnnecessaryLocalVariableJS | ||
const dummyParent = document.createElement('div'); | ||
return dummyParent; | ||
} else if (newContent.generatedByIdiomorph) { | ||
// the template tag created by idiomorph parsing can serve as a dummy parent | ||
return newContent; | ||
} else if (newContent instanceof Node) { | ||
// a single node is added as a child to a dummy parent | ||
const dummyParent = document.createElement('div'); | ||
dummyParent.append(newContent); | ||
return dummyParent; | ||
} else { | ||
// all nodes in the array or HTMLElement collection are consolidated under | ||
// a single dummy parent element | ||
const dummyParent = document.createElement('div'); | ||
for (const elt of [...newContent]) { | ||
dummyParent.append(elt); | ||
} | ||
return dummyParent; | ||
function normalizeContent(newContent) { | ||
if (newContent == null) { | ||
// noinspection UnnecessaryLocalVariableJS | ||
const dummyParent = document.createElement('div'); | ||
return dummyParent; | ||
} else if (newContent.generatedByIdiomorph) { | ||
// the template tag created by idiomorph parsing can serve as a dummy parent | ||
return newContent; | ||
} else if (newContent instanceof Node) { | ||
// a single node is added as a child to a dummy parent | ||
const dummyParent = document.createElement('div'); | ||
dummyParent.append(newContent); | ||
return dummyParent; | ||
} else { | ||
// all nodes in the array or HTMLElement collection are consolidated under | ||
// a single dummy parent element | ||
const dummyParent = document.createElement('div'); | ||
for (const elt of [...newContent]) { | ||
dummyParent.append(elt); | ||
} | ||
return dummyParent; | ||
} | ||
} | ||
function insertSiblings(previousSibling, morphedNode, nextSibling) { | ||
let stack = [] | ||
let added = [] | ||
while (previousSibling != null) { | ||
stack.push(previousSibling); | ||
previousSibling = previousSibling.previousSibling; | ||
} | ||
while (stack.length > 0) { | ||
let node = stack.pop(); | ||
added.push(node); // push added preceding siblings on in order and insert | ||
morphedNode.parentElement.insertBefore(node, morphedNode); | ||
} | ||
added.push(morphedNode); | ||
while (nextSibling != null) { | ||
stack.push(nextSibling); | ||
added.push(nextSibling); // here we are going in order, so push on as we scan, rather than add | ||
nextSibling = nextSibling.nextSibling; | ||
} | ||
while (stack.length > 0) { | ||
morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling); | ||
} | ||
return added; | ||
function insertSiblings(previousSibling, morphedNode, nextSibling) { | ||
let stack = [] | ||
let added = [] | ||
while (previousSibling != null) { | ||
stack.push(previousSibling); | ||
previousSibling = previousSibling.previousSibling; | ||
} | ||
while (stack.length > 0) { | ||
let node = stack.pop(); | ||
added.push(node); // push added preceding siblings on in order and insert | ||
morphedNode.parentElement.insertBefore(node, morphedNode); | ||
} | ||
added.push(morphedNode); | ||
while (nextSibling != null) { | ||
stack.push(nextSibling); | ||
added.push(nextSibling); // here we are going in order, so push on as we scan, rather than add | ||
nextSibling = nextSibling.nextSibling; | ||
} | ||
while (stack.length > 0) { | ||
morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling); | ||
} | ||
return added; | ||
} | ||
function findBestNodeMatch(newContent, oldNode, ctx) { | ||
let currentElement; | ||
currentElement = newContent.firstChild; | ||
let bestElement = currentElement; | ||
let score = 0; | ||
while (currentElement) { | ||
let newScore = scoreElement(currentElement, oldNode, ctx); | ||
if (newScore > score) { | ||
bestElement = currentElement; | ||
score = newScore; | ||
} | ||
currentElement = currentElement.nextSibling; | ||
function findBestNodeMatch(newContent, oldNode, ctx) { | ||
let currentElement; | ||
currentElement = newContent.firstChild; | ||
let bestElement = currentElement; | ||
let score = 0; | ||
while (currentElement) { | ||
let newScore = scoreElement(currentElement, oldNode, ctx); | ||
if (newScore > score) { | ||
bestElement = currentElement; | ||
score = newScore; | ||
} | ||
return bestElement; | ||
currentElement = currentElement.nextSibling; | ||
} | ||
return bestElement; | ||
} | ||
function scoreElement(node1, node2, ctx) { | ||
if (isSoftMatch(node1, node2)) { | ||
return .5 + getIdIntersectionCount(ctx, node1, node2); | ||
} | ||
return 0; | ||
function scoreElement(node1, node2, ctx) { | ||
if (isSoftMatch(node1, node2)) { | ||
return .5 + getIdIntersectionCount(ctx, node1, node2); | ||
} | ||
return 0; | ||
} | ||
function removeNode(tempNode, ctx) { | ||
removeIdsFromConsideration(ctx, tempNode) | ||
if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return; | ||
function removeNode(tempNode, ctx) { | ||
removeIdsFromConsideration(ctx, tempNode) | ||
if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return; | ||
tempNode.remove(); | ||
ctx.callbacks.afterNodeRemoved(tempNode); | ||
} | ||
tempNode.remove(); | ||
ctx.callbacks.afterNodeRemoved(tempNode); | ||
} | ||
//============================================================================= | ||
// ID Set Functions | ||
//============================================================================= | ||
//============================================================================= | ||
// ID Set Functions | ||
//============================================================================= | ||
function isIdInConsideration(ctx, id) { | ||
return !ctx.deadIds.has(id); | ||
} | ||
function isIdInConsideration(ctx, id) { | ||
return !ctx.deadIds.has(id); | ||
} | ||
function idIsWithinNode(ctx, id, targetNode) { | ||
let idSet = ctx.idMap.get(targetNode) || EMPTY_SET; | ||
return idSet.has(id); | ||
} | ||
function idIsWithinNode(ctx, id, targetNode) { | ||
let idSet = ctx.idMap.get(targetNode) || EMPTY_SET; | ||
return idSet.has(id); | ||
} | ||
function removeIdsFromConsideration(ctx, node) { | ||
let idSet = ctx.idMap.get(node) || EMPTY_SET; | ||
for (const id of idSet) { | ||
ctx.deadIds.add(id); | ||
} | ||
function removeIdsFromConsideration(ctx, node) { | ||
let idSet = ctx.idMap.get(node) || EMPTY_SET; | ||
for (const id of idSet) { | ||
ctx.deadIds.add(id); | ||
} | ||
} | ||
function getIdIntersectionCount(ctx, node1, node2) { | ||
let sourceSet = ctx.idMap.get(node1) || EMPTY_SET; | ||
let matchCount = 0; | ||
for (const id of sourceSet) { | ||
// a potential match is an id in the source and potentialIdsSet, but | ||
// that has not already been merged into the DOM | ||
if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) { | ||
++matchCount; | ||
} | ||
function getIdIntersectionCount(ctx, node1, node2) { | ||
let sourceSet = ctx.idMap.get(node1) || EMPTY_SET; | ||
let matchCount = 0; | ||
for (const id of sourceSet) { | ||
// a potential match is an id in the source and potentialIdsSet, but | ||
// that has not already been merged into the DOM | ||
if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) { | ||
++matchCount; | ||
} | ||
return matchCount; | ||
} | ||
return matchCount; | ||
} | ||
/** | ||
* A bottom up algorithm that finds all elements with ids inside of the node | ||
* argument and populates id sets for those nodes and all their parents, generating | ||
* a set of ids contained within all nodes for the entire hierarchy in the DOM | ||
* | ||
* @param node {Element} | ||
* @param {Map<Node, Set<String>>} idMap | ||
*/ | ||
function populateIdMapForNode(node, idMap) { | ||
let nodeParent = node.parentElement; | ||
// find all elements with an id property | ||
let idElements = node.querySelectorAll('[id]'); | ||
for (const elt of idElements) { | ||
let current = elt; | ||
// walk up the parent hierarchy of that element, adding the id | ||
// of element to the parent's id set | ||
while (current !== nodeParent && current != null) { | ||
let idSet = idMap.get(current); | ||
// if the id set doesn't exist, create it and insert it in the map | ||
if (idSet == null) { | ||
idSet = new Set(); | ||
idMap.set(current, idSet); | ||
} | ||
idSet.add(elt.id); | ||
current = current.parentElement; | ||
/** | ||
* A bottom up algorithm that finds all elements with ids inside of the node | ||
* argument and populates id sets for those nodes and all their parents, generating | ||
* a set of ids contained within all nodes for the entire hierarchy in the DOM | ||
* | ||
* @param node {Element} | ||
* @param {Map<Node, Set<String>>} idMap | ||
*/ | ||
function populateIdMapForNode(node, idMap) { | ||
let nodeParent = node.parentElement; | ||
// find all elements with an id property | ||
let idElements = node.querySelectorAll('[id]'); | ||
for (const elt of idElements) { | ||
let current = elt; | ||
// walk up the parent hierarchy of that element, adding the id | ||
// of element to the parent's id set | ||
while (current !== nodeParent && current != null) { | ||
let idSet = idMap.get(current); | ||
// if the id set doesn't exist, create it and insert it in the map | ||
if (idSet == null) { | ||
idSet = new Set(); | ||
idMap.set(current, idSet); | ||
} | ||
idSet.add(elt.id); | ||
current = current.parentElement; | ||
} | ||
} | ||
} | ||
/** | ||
* This function computes a map of nodes to all ids contained within that node (inclusive of the | ||
* node). This map can be used to ask if two nodes have intersecting sets of ids, which allows | ||
* for a looser definition of "matching" than tradition id matching, and allows child nodes | ||
* to contribute to a parent nodes matching. | ||
* | ||
* @param {Element} oldContent the old content that will be morphed | ||
* @param {Element} newContent the new content to morph to | ||
* @returns {Map<Node, Set<String>>} a map of nodes to id sets for the | ||
*/ | ||
function createIdMap(oldContent, newContent) { | ||
let idMap = new Map(); | ||
populateIdMapForNode(oldContent, idMap); | ||
populateIdMapForNode(newContent, idMap); | ||
return idMap; | ||
} | ||
/** | ||
* This function computes a map of nodes to all ids contained within that node (inclusive of the | ||
* node). This map can be used to ask if two nodes have intersecting sets of ids, which allows | ||
* for a looser definition of "matching" than tradition id matching, and allows child nodes | ||
* to contribute to a parent nodes matching. | ||
* | ||
* @param {Element} oldContent the old content that will be morphed | ||
* @param {Element} newContent the new content to morph to | ||
* @returns {Map<Node, Set<String>>} a map of nodes to id sets for the | ||
*/ | ||
function createIdMap(oldContent, newContent) { | ||
let idMap = new Map(); | ||
populateIdMapForNode(oldContent, idMap); | ||
populateIdMapForNode(newContent, idMap); | ||
return idMap; | ||
} | ||
//============================================================================= | ||
// This is what ends up becoming the Idiomorph global object | ||
//============================================================================= | ||
return { | ||
morph | ||
} | ||
})() | ||
})); | ||
//============================================================================= | ||
// This is what ends up becoming the Idiomorph global object | ||
//============================================================================= | ||
return { | ||
morph, | ||
defaults | ||
} | ||
})(); |
@@ -1,1 +0,1 @@ | ||
(function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else if(typeof module==="object"&&module.exports){module.exports=t()}else{e.Idiomorph=e.Idiomorph||t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";let o=new Set;function e(e,t,n={}){if(e instanceof Document){e=e.documentElement}if(typeof t==="string"){t=A(t)}let l=S(t);let r=h(e,l,n);return u(e,l,r)}function u(r,i,o){if(o.head.block){let t=r.querySelector("head");let n=i.querySelector("head");if(t&&n){let e=s(n,t,o);Promise.all(e).then(function(){u(r,i,Object.assign(o,{head:{block:false,ignore:true}}))});return}}if(o.morphStyle==="innerHTML"){l(i,r,o);return r.children}else if(o.morphStyle==="outerHTML"||o.morphStyle==null){let e=M(i,r,o);let t=e?.previousSibling;let n=e?.nextSibling;let l=d(r,e,o);if(e){return N(t,l,n)}else{return[]}}else{throw"Do not understand how to morph style "+o.morphStyle}}function a(e,t){return t.ignoreActiveValue&&e===document.activeElement}function d(e,t,n){if(n.ignoreActive&&e===document.activeElement){}else if(t==null){if(n.callbacks.beforeNodeRemoved(e)===false)return;e.remove();n.callbacks.afterNodeRemoved(e);return null}else if(!b(e,t)){if(n.callbacks.beforeNodeRemoved(e)===false)return;if(n.callbacks.beforeNodeAdded(t)===false)return;e.parentElement.replaceChild(t,e);n.callbacks.afterNodeAdded(t);n.callbacks.afterNodeRemoved(e);return t}else{if(n.callbacks.beforeNodeMorphed(e,t)===false)return;if(e instanceof HTMLHeadElement&&n.head.ignore){}else if(e instanceof HTMLHeadElement&&n.head.style!=="morph"){s(t,e,n)}else{r(t,e,n);if(!a(e,n)){l(t,e,n)}}n.callbacks.afterNodeMorphed(e,t);return e}}function l(n,l,r){let i=n.firstChild;let o=l.firstChild;let u;while(i){u=i;i=u.nextSibling;if(o==null){if(r.callbacks.beforeNodeAdded(u)===false)return;l.appendChild(u);r.callbacks.afterNodeAdded(u);E(r,u);continue}if(p(u,o,r)){d(o,u,r);o=o.nextSibling;E(r,u);continue}let e=v(n,l,u,o,r);if(e){o=g(o,e,r);d(e,u,r);E(r,u);continue}let t=y(n,l,u,o,r);if(t){o=g(o,t,r);d(t,u,r);E(r,u);continue}if(r.callbacks.beforeNodeAdded(u)===false)return;l.insertBefore(u,o);r.callbacks.afterNodeAdded(u);E(r,u)}while(o!==null){let e=o;o=o.nextSibling;w(e,r)}}function r(t,n,e){let l=t.nodeType;if(l===1){const r=t.attributes;const i=n.attributes;for(const o of r){if(o.name==="value"&&a(n,e)){continue}if(n.getAttribute(o.name)!==o.value){n.setAttribute(o.name,o.value)}}for(let e=i.length-1;0<=e;e--){const u=i[e];if(!t.hasAttribute(u.name)){n.removeAttribute(u.name)}}}if(l===8||l===3){if(n.nodeValue!==t.nodeValue){n.nodeValue=t.nodeValue}}if(!a(n,e)){f(t,n)}}function i(e,t,n){if(e[n]!==t[n]){t[n]=e[n];if(e[n]){t.setAttribute(n,e[n])}else{t.removeAttribute(n)}}}function f(n,l){if(n instanceof HTMLInputElement&&l instanceof HTMLInputElement&&n.type!=="file"){let e=n.value;let t=l.value;i(n,l,"checked");i(n,l,"disabled");if(!n.hasAttribute("value")){l.value="";l.removeAttribute("value")}else if(e!==t){l.setAttribute("value",e);l.value=e}}else if(n instanceof HTMLOptionElement){i(n,l,"selected")}else if(n instanceof HTMLTextAreaElement&&l instanceof HTMLTextAreaElement){let e=n.value;let t=l.value;if(e!==t){l.value=e}if(l.firstChild&&l.firstChild.nodeValue!==e){l.firstChild.nodeValue=e}}}function s(e,t,l){let r=[];let i=[];let o=[];let u=[];let a=l.head.style;let d=new Map;for(const n of e.children){d.set(n.outerHTML,n)}for(const s of t.children){let e=d.has(s.outerHTML);let t=l.head.shouldReAppend(s);let n=l.head.shouldPreserve(s);if(e||n){if(t){i.push(s)}else{d.delete(s.outerHTML);o.push(s)}}else{if(a==="append"){if(t){i.push(s);u.push(s)}}else{if(l.head.shouldRemove(s)!==false){i.push(s)}}}}u.push(...d.values());m("to append: ",u);let f=[];for(const c of u){m("adding: ",c);let n=document.createRange().createContextualFragment(c.outerHTML).firstChild;m(n);if(l.callbacks.beforeNodeAdded(n)!==false){if(n.href||n.src){let t=null;let e=new Promise(function(e){t=e});n.addEventListener("load",function(){t()});f.push(e)}t.appendChild(n);l.callbacks.afterNodeAdded(n);r.push(n)}}for(const h of i){if(l.callbacks.beforeNodeRemoved(h)!==false){t.removeChild(h);l.callbacks.afterNodeRemoved(h)}}l.head.afterHeadMorphed(t,{added:r,kept:o,removed:i});return f}function m(){}function c(){}function h(e,t,n){return{target:e,newContent:t,config:n,morphStyle:n.morphStyle,ignoreActive:n.ignoreActive,ignoreActiveValue:n.ignoreActiveValue,idMap:C(e,t),deadIds:new Set,callbacks:Object.assign({beforeNodeAdded:c,afterNodeAdded:c,beforeNodeMorphed:c,afterNodeMorphed:c,beforeNodeRemoved:c,afterNodeRemoved:c},n.callbacks),head:Object.assign({style:"merge",shouldPreserve:function(e){return e.getAttribute("im-preserve")==="true"},shouldReAppend:function(e){return e.getAttribute("im-re-append")==="true"},shouldRemove:c,afterHeadMorphed:c},n.head)}}function p(e,t,n){if(e==null||t==null){return false}if(e.nodeType===t.nodeType&&e.tagName===t.tagName){if(e.id!==""&&e.id===t.id){return true}else{return H(n,e,t)>0}}return false}function b(e,t){if(e==null||t==null){return false}return e.nodeType===t.nodeType&&e.tagName===t.tagName}function g(t,e,n){while(t!==e){let e=t;t=t.nextSibling;w(e,n)}E(n,e);return e.nextSibling}function v(n,e,l,r,i){let o=H(i,l,e);let t=null;if(o>0){let e=r;let t=0;while(e!=null){if(p(l,e,i)){return e}t+=H(i,e,n);if(t>o){return null}e=e.nextSibling}}return t}function y(e,t,n,l,r){let i=l;let o=n.nextSibling;let u=0;while(i!=null){if(H(r,i,e)>0){return null}if(b(n,i)){return i}if(b(o,i)){u++;o=o.nextSibling;if(u>=2){return null}}i=i.nextSibling}return i}function A(n){let l=new DOMParser;let e=n.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim,"");if(e.match(/<\/html>/)||e.match(/<\/head>/)||e.match(/<\/body>/)){let t=l.parseFromString(n,"text/html");if(e.match(/<\/html>/)){t.generatedByIdiomorph=true;return t}else{let e=t.firstChild;if(e){e.generatedByIdiomorph=true;return e}else{return null}}}else{let e=l.parseFromString("<body><template>"+n+"</template></body>","text/html");let t=e.body.querySelector("template").content;t.generatedByIdiomorph=true;return t}}function S(e){if(e==null){const t=document.createElement("div");return t}else if(e.generatedByIdiomorph){return e}else if(e instanceof Node){const t=document.createElement("div");t.append(e);return t}else{const t=document.createElement("div");for(const n of[...e]){t.append(n)}return t}}function N(e,t,n){let l=[];let r=[];while(e!=null){l.push(e);e=e.previousSibling}while(l.length>0){let e=l.pop();r.push(e);t.parentElement.insertBefore(e,t)}r.push(t);while(n!=null){l.push(n);r.push(n);n=n.nextSibling}while(l.length>0){t.parentElement.insertBefore(l.pop(),t.nextSibling)}return r}function M(e,t,n){let l;l=e.firstChild;let r=l;let i=0;while(l){let e=k(l,t,n);if(e>i){r=l;i=e}l=l.nextSibling}return r}function k(e,t,n){if(b(e,t)){return.5+H(n,e,t)}return 0}function w(e,t){E(t,e);if(t.callbacks.beforeNodeRemoved(e)===false)return;e.remove();t.callbacks.afterNodeRemoved(e)}function x(e,t){return!e.deadIds.has(t)}function T(e,t,n){let l=e.idMap.get(n)||o;return l.has(t)}function E(e,t){let n=e.idMap.get(t)||o;for(const l of n){e.deadIds.add(l)}}function H(e,t,n){let l=e.idMap.get(t)||o;let r=0;for(const i of l){if(x(e,i)&&T(e,i,n)){++r}}return r}function R(e,n){let l=e.parentElement;let t=e.querySelectorAll("[id]");for(const r of t){let t=r;while(t!==l&&t!=null){let e=n.get(t);if(e==null){e=new Set;n.set(t,e)}e.add(r.id);t=t.parentElement}}}function C(e,t){let n=new Map;R(e,n);R(t,n);return n}return{morph:e}}()}); | ||
var Idiomorph=function(){"use strict";let o=new Set;let n={morphStyle:"outerHTML",callbacks:{beforeNodeAdded:t,afterNodeAdded:t,beforeNodeMorphed:t,afterNodeMorphed:t,beforeNodeRemoved:t,afterNodeRemoved:t,beforeAttributeUpdated:t},head:{style:"merge",shouldPreserve:function(e){return e.getAttribute("im-preserve")==="true"},shouldReAppend:function(e){return e.getAttribute("im-re-append")==="true"},shouldRemove:t,afterHeadMorphed:t}};function e(e,t,n={}){if(e instanceof Document){e=e.documentElement}if(typeof t==="string"){t=k(t)}let l=y(t);let r=p(e,l,n);return a(e,l,r)}function a(r,i,o){if(o.head.block){let t=r.querySelector("head");let n=i.querySelector("head");if(t&&n){let e=c(n,t,o);Promise.all(e).then(function(){a(r,i,Object.assign(o,{head:{block:false,ignore:true}}))});return}}if(o.morphStyle==="innerHTML"){l(i,r,o);return r.children}else if(o.morphStyle==="outerHTML"||o.morphStyle==null){let e=M(i,r,o);let t=e?.previousSibling;let n=e?.nextSibling;let l=d(r,e,o);if(e){return N(t,l,n)}else{return[]}}else{throw"Do not understand how to morph style "+o.morphStyle}}function u(e,t){return t.ignoreActiveValue&&e===document.activeElement}function d(e,t,n){if(n.ignoreActive&&e===document.activeElement){}else if(t==null){if(n.callbacks.beforeNodeRemoved(e)===false)return e;e.remove();n.callbacks.afterNodeRemoved(e);return null}else if(!g(e,t)){if(n.callbacks.beforeNodeRemoved(e)===false)return e;if(n.callbacks.beforeNodeAdded(t)===false)return e;e.parentElement.replaceChild(t,e);n.callbacks.afterNodeAdded(t);n.callbacks.afterNodeRemoved(e);return t}else{if(n.callbacks.beforeNodeMorphed(e,t)===false)return e;if(e instanceof HTMLHeadElement&&n.head.ignore){}else if(e instanceof HTMLHeadElement&&n.head.style!=="morph"){c(t,e,n)}else{r(t,e,n);if(!u(e,n)){l(t,e,n)}}n.callbacks.afterNodeMorphed(e,t);return e}}function l(n,l,r){let i=n.firstChild;let o=l.firstChild;let a;while(i){a=i;i=a.nextSibling;if(o==null){if(r.callbacks.beforeNodeAdded(a)===false)return;l.appendChild(a);r.callbacks.afterNodeAdded(a);H(r,a);continue}if(b(a,o,r)){d(o,a,r);o=o.nextSibling;H(r,a);continue}let e=A(n,l,a,o,r);if(e){o=v(o,e,r);d(e,a,r);H(r,a);continue}let t=S(n,l,a,o,r);if(t){o=v(o,t,r);d(t,a,r);H(r,a);continue}if(r.callbacks.beforeNodeAdded(a)===false)return;l.insertBefore(a,o);r.callbacks.afterNodeAdded(a);H(r,a)}while(o!==null){let e=o;o=o.nextSibling;T(e,r)}}function f(e,t,n,l){if(e==="value"&&l.ignoreActiveValue&&t===document.activeElement){return true}return l.callbacks.beforeAttributeUpdated(e,t,n)===false}function r(t,n,l){let e=t.nodeType;if(e===1){const r=t.attributes;const i=n.attributes;for(const o of r){if(f(o.name,n,"update",l)){continue}if(n.getAttribute(o.name)!==o.value){n.setAttribute(o.name,o.value)}}for(let e=i.length-1;0<=e;e--){const a=i[e];if(f(a.name,n,"remove",l)){continue}if(!t.hasAttribute(a.name)){n.removeAttribute(a.name)}}}if(e===8||e===3){if(n.nodeValue!==t.nodeValue){n.nodeValue=t.nodeValue}}if(!u(n,l)){s(t,n,l)}}function i(t,n,l,r){if(t[l]!==n[l]){let e=f(l,n,"update",r);if(!e){n[l]=t[l]}if(t[l]){if(!e){n.setAttribute(l,t[l])}}else{if(!f(l,n,"remove",r)){n.removeAttribute(l)}}}}function s(n,l,r){if(n instanceof HTMLInputElement&&l instanceof HTMLInputElement&&n.type!=="file"){let e=n.value;let t=l.value;i(n,l,"checked",r);i(n,l,"disabled",r);if(!n.hasAttribute("value")){if(!f("value",l,"remove",r)){l.value="";l.removeAttribute("value")}}else if(e!==t){if(!f("value",l,"update",r)){l.setAttribute("value",e);l.value=e}}}else if(n instanceof HTMLOptionElement){i(n,l,"selected",r)}else if(n instanceof HTMLTextAreaElement&&l instanceof HTMLTextAreaElement){let e=n.value;let t=l.value;if(f("value",l,"update",r)){return}if(e!==t){l.value=e}if(l.firstChild&&l.firstChild.nodeValue!==e){l.firstChild.nodeValue=e}}}function c(e,t,l){let r=[];let i=[];let o=[];let a=[];let u=l.head.style;let d=new Map;for(const n of e.children){d.set(n.outerHTML,n)}for(const s of t.children){let e=d.has(s.outerHTML);let t=l.head.shouldReAppend(s);let n=l.head.shouldPreserve(s);if(e||n){if(t){i.push(s)}else{d.delete(s.outerHTML);o.push(s)}}else{if(u==="append"){if(t){i.push(s);a.push(s)}}else{if(l.head.shouldRemove(s)!==false){i.push(s)}}}}a.push(...d.values());m("to append: ",a);let f=[];for(const c of a){m("adding: ",c);let n=document.createRange().createContextualFragment(c.outerHTML).firstChild;m(n);if(l.callbacks.beforeNodeAdded(n)!==false){if(n.href||n.src){let t=null;let e=new Promise(function(e){t=e});n.addEventListener("load",function(){t()});f.push(e)}t.appendChild(n);l.callbacks.afterNodeAdded(n);r.push(n)}}for(const h of i){if(l.callbacks.beforeNodeRemoved(h)!==false){t.removeChild(h);l.callbacks.afterNodeRemoved(h)}}l.head.afterHeadMorphed(t,{added:r,kept:o,removed:i});return f}function m(){}function t(){}function h(e){let t={};Object.assign(t,n);Object.assign(t,e);t.callbacks={};Object.assign(t.callbacks,n.callbacks);Object.assign(t.callbacks,e.callbacks);t.head={};Object.assign(t.head,n.head);Object.assign(t.head,e.head);return t}function p(e,t,n){n=h(n);return{target:e,newContent:t,config:n,morphStyle:n.morphStyle,ignoreActive:n.ignoreActive,ignoreActiveValue:n.ignoreActiveValue,idMap:C(e,t),deadIds:new Set,callbacks:n.callbacks,head:n.head}}function b(e,t,n){if(e==null||t==null){return false}if(e.nodeType===t.nodeType&&e.tagName===t.tagName){if(e.id!==""&&e.id===t.id){return true}else{return L(n,e,t)>0}}return false}function g(e,t){if(e==null||t==null){return false}return e.nodeType===t.nodeType&&e.tagName===t.tagName}function v(t,e,n){while(t!==e){let e=t;t=t.nextSibling;T(e,n)}H(n,e);return e.nextSibling}function A(n,e,l,r,i){let o=L(i,l,e);let t=null;if(o>0){let e=r;let t=0;while(e!=null){if(b(l,e,i)){return e}t+=L(i,e,n);if(t>o){return null}e=e.nextSibling}}return t}function S(e,t,n,l,r){let i=l;let o=n.nextSibling;let a=0;while(i!=null){if(L(r,i,e)>0){return null}if(g(n,i)){return i}if(g(o,i)){a++;o=o.nextSibling;if(a>=2){return null}}i=i.nextSibling}return i}function k(n){let l=new DOMParser;let e=n.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim,"");if(e.match(/<\/html>/)||e.match(/<\/head>/)||e.match(/<\/body>/)){let t=l.parseFromString(n,"text/html");if(e.match(/<\/html>/)){t.generatedByIdiomorph=true;return t}else{let e=t.firstChild;if(e){e.generatedByIdiomorph=true;return e}else{return null}}}else{let e=l.parseFromString("<body><template>"+n+"</template></body>","text/html");let t=e.body.querySelector("template").content;t.generatedByIdiomorph=true;return t}}function y(e){if(e==null){const t=document.createElement("div");return t}else if(e.generatedByIdiomorph){return e}else if(e instanceof Node){const t=document.createElement("div");t.append(e);return t}else{const t=document.createElement("div");for(const n of[...e]){t.append(n)}return t}}function N(e,t,n){let l=[];let r=[];while(e!=null){l.push(e);e=e.previousSibling}while(l.length>0){let e=l.pop();r.push(e);t.parentElement.insertBefore(e,t)}r.push(t);while(n!=null){l.push(n);r.push(n);n=n.nextSibling}while(l.length>0){t.parentElement.insertBefore(l.pop(),t.nextSibling)}return r}function M(e,t,n){let l;l=e.firstChild;let r=l;let i=0;while(l){let e=w(l,t,n);if(e>i){r=l;i=e}l=l.nextSibling}return r}function w(e,t,n){if(g(e,t)){return.5+L(n,e,t)}return 0}function T(e,t){H(t,e);if(t.callbacks.beforeNodeRemoved(e)===false)return;e.remove();t.callbacks.afterNodeRemoved(e)}function E(e,t){return!e.deadIds.has(t)}function x(e,t,n){let l=e.idMap.get(n)||o;return l.has(t)}function H(e,t){let n=e.idMap.get(t)||o;for(const l of n){e.deadIds.add(l)}}function L(e,t,n){let l=e.idMap.get(t)||o;let r=0;for(const i of l){if(E(e,i)&&x(e,i,n)){++r}}return r}function R(e,n){let l=e.parentElement;let t=e.querySelectorAll("[id]");for(const r of t){let t=r;while(t!==l&&t!=null){let e=n.get(t);if(e==null){e=new Set;n.set(t,e)}e.add(r.id);t=t.parentElement}}}function C(e,t){let n=new Map;R(e,n);R(t,n);return n}return{morph:e,defaults:n}}(); |
@@ -7,3 +7,3 @@ { | ||
], | ||
"version": "0.2.0", | ||
"version": "0.3.0", | ||
"homepage": "https://github.com/bigskysoftware/idiomorph", | ||
@@ -23,3 +23,7 @@ "bugs": { | ||
"test": "mocha-chrome test/index.html", | ||
"dist": "cp -r src/* dist/ && cat src/idiomorph.js src/idiomorph-htmx.js > dist/idiomorph-ext.js && npm run-script uglify && gzip -9 -k -f dist/idiomorph.min.js > dist/idiomorph.min.js.gz && exit", | ||
"amd" : "(echo \"define(() => {\n\" && cat src/idiomorph.js && echo \"\nreturn Idiomorph});\") > dist/idiomorph.amd.js", | ||
"cjs" : "(cat src/idiomorph.js && echo \"\nmodule.exports = Idiomorph;\") > dist/idiomorph.cjs.js", | ||
"esm" : "(cat src/idiomorph.js && echo \"\nexport {Idiomorph};\") > dist/idiomorph.esm.js", | ||
"gen-modules" : "npm run-script amd && npm run-script cjs && npm run-script esm", | ||
"dist": "cp -r src/* dist/ && cat src/idiomorph.js src/idiomorph-htmx.js > dist/idiomorph-ext.js && npm run-script gen-modules && npm run-script uglify && gzip -9 -k -f dist/idiomorph.min.js > dist/idiomorph.min.js.gz && exit", | ||
"uglify": "uglifyjs -m eval -o dist/idiomorph.min.js dist/idiomorph.js && uglifyjs -m eval -o dist/idiomorph-ext.min.js dist/idiomorph-ext.js" | ||
@@ -26,0 +30,0 @@ }, |
@@ -82,10 +82,12 @@ <h1 style="font-family: Verdana,sans-serif;">♻️ Idiomorph</h1> | ||
| callback | description | | ||
|-------------------|----------------------------------------------| | ||
| beforeNodeAdded | Called before a new node is added to the DOM | | ||
| afterNodeAdded | Called after a new node is added to the DOM | | ||
| beforeNodeMorphed | Called before a node is morphed in the DOM | | ||
| afterNodeMorphed | Called after a node is morphed in the DOM | | ||
| beforeNodeRemoved | Called before a node is removed from the DOM | | ||
| afterNodeRemoved | Called after a node is removed from the DOM | | ||
| callback | description | return value meaning | | ||
| callback | description | return value meaning | | ||
|--------------------------------------------------------------|------------------------------------------------------------------------------------------|----------------------------------------------------| | ||
| beforeNodeAdded(node) | Called before a new node is added to the DOM | return false to not add the node | | ||
| afterNodeAdded(node) | Called after a new node is added to the DOM | none | | ||
| beforeNodeMorphed(oldNode, newNode) | Called before a node is morphed in the DOM | return false to skip morphing the node | | ||
| afterNodeMorphed(oldNode, newNode) | Called after a node is morphed in the DOM | none | | ||
| beforeNodeRemoved(node) | Called before a node is removed from the DOM | return false to not remove the node | | ||
| afterNodeRemoved(node) | Called after a node is removed from the DOM | none | | ||
| beforeAttributeUpdated(attributeName, node, mutationType) | Called before an attribute on an element. `mutationType` is either "updated" or "removed" | return false to not update or remove the attribute | | ||
@@ -92,0 +94,0 @@ ### The `head` tag |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
222941
11
3785
299
1