Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

idiomorph

Package Overview
Dependencies
Maintainers
1
Versions
13
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

idiomorph - npm Package Compare versions

Comparing version 0.2.0 to 0.3.0

dist/idiomorph.amd.js

1352

dist/idiomorph-ext.js

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc