var Idiomorph = (function () {
'use strict';
const noOp = () => {};
const defaults = {
morphStyle: 'outerHTML',
callbacks: {
beforeNodeAdded: noOp,
afterNodeAdded: noOp,
beforeNodeMorphed: noOp,
afterNodeMorphed: noOp,
beforeNodeRemoved: noOp,
afterNodeRemoved: noOp,
beforeAttributeUpdated: noOp,
},
head: {
style: 'merge',
shouldPreserve: (elt) => elt.getAttribute('im-preserve') === 'true',
shouldReAppend: (elt) =>
elt.getAttribute('im-re-append') === 'true',
shouldRemove: noOp,
afterHeadMorphed: noOp,
},
restoreFocus: true,
};
function morph(oldNode, newContent, config = {}) {
oldNode = normalizeElement(oldNode);
const newNode = normalizeParent(newContent);
const ctx = createMorphContext(oldNode, newNode, config);
const morphedNodes = saveAndRestoreFocus(ctx, () => {
return withHeadBlocking(
ctx,
oldNode,
newNode,
(ctx) => {
if (ctx.morphStyle === 'innerHTML') {
morphChildren(ctx, oldNode, newNode);
return Array.from(oldNode.childNodes);
} else {
return morphOuterHTML(ctx, oldNode, newNode);
}
},
);
});
ctx.pantry.remove();
return morphedNodes;
}
function morphOuterHTML(ctx, oldNode, newNode) {
const oldParent = normalizeParent(oldNode);
let childNodes = Array.from(oldParent.childNodes);
const index = childNodes.indexOf(oldNode);
const rightMargin = childNodes.length - (index + 1);
morphChildren(
ctx,
oldParent,
newNode,
oldNode, oldNode.nextSibling, );
childNodes = Array.from(oldParent.childNodes);
return childNodes.slice(index, childNodes.length - rightMargin);
}
function saveAndRestoreFocus(ctx, fn) {
if (!ctx.config.restoreFocus) return fn();
let activeElement =
(
document.activeElement
);
if (
!(
activeElement instanceof HTMLInputElement ||
activeElement instanceof HTMLTextAreaElement
)
) {
return fn();
}
const {
id: activeElementId,
selectionStart,
selectionEnd,
} = activeElement;
const results = fn();
if (activeElementId && activeElementId !== document.activeElement?.id) {
activeElement = ctx.target.querySelector(`#${activeElementId}`);
activeElement?.focus();
}
if (activeElement && !activeElement.selectionEnd && selectionEnd) {
activeElement.setSelectionRange(selectionStart, selectionEnd);
}
return results;
}
const morphChildren = (function () {
function morphChildren(
ctx,
oldParent,
newParent,
insertionPoint = null,
endPoint = null,
) {
if (
oldParent instanceof HTMLTemplateElement &&
newParent instanceof HTMLTemplateElement
) {
oldParent = oldParent.content;
newParent = newParent.content;
}
insertionPoint ||= oldParent.firstChild;
for (const newChild of newParent.childNodes) {
if (insertionPoint && insertionPoint != endPoint) {
const bestMatch = findBestMatch(
ctx,
newChild,
insertionPoint,
endPoint,
);
if (bestMatch) {
if (bestMatch !== insertionPoint) {
removeNodesBetween(ctx, insertionPoint, bestMatch);
}
morphNode(bestMatch, newChild, ctx);
insertionPoint = bestMatch.nextSibling;
continue;
}
}
if (
newChild instanceof Element &&
ctx.persistentIds.has(newChild.id)
) {
const movedChild = moveBeforeById(
oldParent,
newChild.id,
insertionPoint,
ctx,
);
morphNode(movedChild, newChild, ctx);
insertionPoint = movedChild.nextSibling;
continue;
}
const insertedNode = createNode(
oldParent,
newChild,
insertionPoint,
ctx,
);
if (insertedNode) {
insertionPoint = insertedNode.nextSibling;
}
}
while (insertionPoint && insertionPoint != endPoint) {
const tempNode = insertionPoint;
insertionPoint = insertionPoint.nextSibling;
removeNode(ctx, tempNode);
}
}
function createNode(oldParent, newChild, insertionPoint, ctx) {
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return null;
if (ctx.idMap.has(newChild)) {
const newEmptyChild = document.createElement(
(newChild).tagName,
);
oldParent.insertBefore(newEmptyChild, insertionPoint);
morphNode(newEmptyChild, newChild, ctx);
ctx.callbacks.afterNodeAdded(newEmptyChild);
return newEmptyChild;
} else {
const newClonedChild = document.importNode(newChild, true); oldParent.insertBefore(newClonedChild, insertionPoint);
ctx.callbacks.afterNodeAdded(newClonedChild);
return newClonedChild;
}
}
const findBestMatch = (function () {
function findBestMatch(ctx, node, startPoint, endPoint) {
let softMatch = null;
let nextSibling = node.nextSibling;
let siblingSoftMatchCount = 0;
let cursor = startPoint;
while (cursor && cursor != endPoint) {
if (isSoftMatch(cursor, node)) {
if (isIdSetMatch(ctx, cursor, node)) {
return cursor; }
if (softMatch === null) {
if (!ctx.idMap.has(cursor)) {
softMatch = cursor;
}
}
}
if (
softMatch === null &&
nextSibling &&
isSoftMatch(cursor, nextSibling)
) {
siblingSoftMatchCount++;
nextSibling = nextSibling.nextSibling;
if (siblingSoftMatchCount >= 2) {
softMatch = undefined;
}
}
if (cursor.contains(document.activeElement)) break;
cursor = cursor.nextSibling;
}
return softMatch || null;
}
function isIdSetMatch(ctx, oldNode, newNode) {
let oldSet = ctx.idMap.get(oldNode);
let newSet = ctx.idMap.get(newNode);
if (!newSet || !oldSet) return false;
for (const id of oldSet) {
if (newSet.has(id)) {
return true;
}
}
return false;
}
function isSoftMatch(oldNode, newNode) {
const oldElt = (oldNode);
const newElt = (newNode);
return (
oldElt.nodeType === newElt.nodeType &&
oldElt.tagName === newElt.tagName &&
(!oldElt.id || oldElt.id === newElt.id)
);
}
return findBestMatch;
})();
function removeNode(ctx, node) {
if (ctx.idMap.has(node)) {
moveBefore(ctx.pantry, node, null);
} else {
if (ctx.callbacks.beforeNodeRemoved(node) === false) return;
node.parentNode?.removeChild(node);
ctx.callbacks.afterNodeRemoved(node);
}
}
function removeNodesBetween(ctx, startInclusive, endExclusive) {
let cursor = startInclusive;
while (cursor && cursor !== endExclusive) {
let tempNode = (cursor);
cursor = cursor.nextSibling;
removeNode(ctx, tempNode);
}
return cursor;
}
function moveBeforeById(parentNode, id, after, ctx) {
const target =
(
ctx.target.querySelector(`#${id}`) ||
ctx.pantry.querySelector(`#${id}`)
);
removeElementFromAncestorsIdMaps(target, ctx);
moveBefore(parentNode, target, after);
return target;
}
function removeElementFromAncestorsIdMaps(element, ctx) {
const id = element.id;
while ((element = element.parentNode)) {
let idSet = ctx.idMap.get(element);
if (idSet) {
idSet.delete(id);
if (!idSet.size) {
ctx.idMap.delete(element);
}
}
}
}
function moveBefore(parentNode, element, after) {
if (parentNode.moveBefore) {
try {
parentNode.moveBefore(element, after);
} catch {
parentNode.insertBefore(element, after);
}
} else {
parentNode.insertBefore(element, after);
}
}
return morphChildren;
})();
const morphNode = (function () {
function morphNode(oldNode, newContent, ctx) {
if (ctx.ignoreActive && oldNode === document.activeElement) {
return null;
}
if (
ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false
) {
return oldNode;
}
if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) {
} else if (
oldNode instanceof HTMLHeadElement &&
ctx.head.style !== 'morph'
) {
handleHeadElement(
oldNode,
(newContent),
ctx,
);
} else {
morphAttributes(oldNode, newContent, ctx);
if (!ignoreValueOfActiveElement(oldNode, ctx)) {
morphChildren(ctx, oldNode, newContent);
}
}
ctx.callbacks.afterNodeMorphed(oldNode, newContent);
return oldNode;
}
function morphAttributes(oldNode, newNode, ctx) {
let type = newNode.nodeType;
if (type === 1 ) {
const oldElt = (oldNode);
const newElt = (newNode);
const oldAttributes = oldElt.attributes;
const newAttributes = newElt.attributes;
for (const newAttribute of newAttributes) {
if (
ignoreAttribute(
newAttribute.name,
oldElt,
'update',
ctx,
)
) {
continue;
}
if (
oldElt.getAttribute(newAttribute.name) !==
newAttribute.value
) {
oldElt.setAttribute(
newAttribute.name,
newAttribute.value,
);
}
}
for (let i = oldAttributes.length - 1; 0 <= i; i--) {
const oldAttribute = oldAttributes[i];
if (!oldAttribute) continue;
if (!newElt.hasAttribute(oldAttribute.name)) {
if (
ignoreAttribute(
oldAttribute.name,
oldElt,
'remove',
ctx,
)
) {
continue;
}
oldElt.removeAttribute(oldAttribute.name);
}
}
if (!ignoreValueOfActiveElement(oldElt, ctx)) {
syncInputValue(oldElt, newElt, ctx);
}
}
if (type === 8 || type === 3 ) {
if (oldNode.nodeValue !== newNode.nodeValue) {
oldNode.nodeValue = newNode.nodeValue;
}
}
}
function syncInputValue(oldElement, newElement, ctx) {
if (
oldElement instanceof HTMLInputElement &&
newElement instanceof HTMLInputElement &&
newElement.type !== 'file'
) {
let newValue = newElement.value;
let oldValue = oldElement.value;
syncBooleanAttribute(oldElement, newElement, 'checked', ctx);
syncBooleanAttribute(oldElement, newElement, 'disabled', ctx);
if (!newElement.hasAttribute('value')) {
if (!ignoreAttribute('value', oldElement, 'remove', ctx)) {
oldElement.value = '';
oldElement.removeAttribute('value');
}
} else if (oldValue !== newValue) {
if (!ignoreAttribute('value', oldElement, 'update', ctx)) {
oldElement.setAttribute('value', newValue);
oldElement.value = newValue;
}
}
} else if (
oldElement instanceof HTMLOptionElement &&
newElement instanceof HTMLOptionElement
) {
syncBooleanAttribute(oldElement, newElement, 'selected', ctx);
} else if (
oldElement instanceof HTMLTextAreaElement &&
newElement instanceof HTMLTextAreaElement
) {
let newValue = newElement.value;
let oldValue = oldElement.value;
if (ignoreAttribute('value', oldElement, 'update', ctx)) {
return;
}
if (newValue !== oldValue) {
oldElement.value = newValue;
}
if (
oldElement.firstChild &&
oldElement.firstChild.nodeValue !== newValue
) {
oldElement.firstChild.nodeValue = newValue;
}
}
}
function syncBooleanAttribute(
oldElement,
newElement,
attributeName,
ctx,
) {
const newLiveValue = newElement[attributeName],
oldLiveValue = oldElement[attributeName];
if (newLiveValue !== oldLiveValue) {
const ignoreUpdate = ignoreAttribute(
attributeName,
oldElement,
'update',
ctx,
);
if (!ignoreUpdate) {
oldElement[attributeName] = newElement[attributeName];
}
if (newLiveValue) {
if (!ignoreUpdate) {
oldElement.setAttribute(attributeName, '');
}
} else {
if (
!ignoreAttribute(
attributeName,
oldElement,
'remove',
ctx,
)
) {
oldElement.removeAttribute(attributeName);
}
}
}
}
function ignoreAttribute(attr, element, updateType, ctx) {
if (
attr === 'value' &&
ctx.ignoreActiveValue &&
element === document.activeElement
) {
return true;
}
return (
ctx.callbacks.beforeAttributeUpdated(
attr,
element,
updateType,
) === false
);
}
function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
return (
!!ctx.ignoreActiveValue &&
possibleActiveElement === document.activeElement &&
possibleActiveElement !== document.body
);
}
return morphNode;
})();
function withHeadBlocking(ctx, oldNode, newNode, callback) {
if (ctx.head.block) {
const oldHead = oldNode.querySelector('head');
const newHead = newNode.querySelector('head');
if (oldHead && newHead) {
const promises = handleHeadElement(oldHead, newHead, ctx);
return Promise.all(promises).then(() => {
const newCtx = Object.assign(ctx, {
head: {
block: false,
ignore: true,
},
});
return callback(newCtx);
});
}
}
return callback(ctx);
}
function handleHeadElement(oldHead, newHead, ctx) {
let added = [];
let removed = [];
let preserved = [];
let nodesToAppend = [];
let srcToNewHeadNodes = new Map();
for (const newHeadChild of newHead.children) {
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
}
for (const currentHeadElt of oldHead.children) {
let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
let isReAppended = ctx.head.shouldReAppend(currentHeadElt);
let isPreserved = ctx.head.shouldPreserve(currentHeadElt);
if (inNewContent || isPreserved) {
if (isReAppended) {
removed.push(currentHeadElt);
} else {
srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
preserved.push(currentHeadElt);
}
} else {
if (ctx.head.style === 'append') {
if (isReAppended) {
removed.push(currentHeadElt);
nodesToAppend.push(currentHeadElt);
}
} else {
if (ctx.head.shouldRemove(currentHeadElt) !== false) {
removed.push(currentHeadElt);
}
}
}
}
nodesToAppend.push(...srcToNewHeadNodes.values());
let promises = [];
for (const newNode of nodesToAppend) {
let newElt = (
document
.createRange()
.createContextualFragment(newNode.outerHTML).firstChild
);
if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
if (
('href' in newElt && newElt.href) ||
('src' in newElt && newElt.src)
) {
let resolve;
let promise = new Promise(function (_resolve) {
resolve = _resolve;
});
newElt.addEventListener('load', function () {
resolve();
});
promises.push(promise);
}
oldHead.appendChild(newElt);
ctx.callbacks.afterNodeAdded(newElt);
added.push(newElt);
}
}
for (const removedElement of removed) {
if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
oldHead.removeChild(removedElement);
ctx.callbacks.afterNodeRemoved(removedElement);
}
}
ctx.head.afterHeadMorphed(oldHead, {
added: added,
kept: preserved,
removed: removed,
});
return promises;
}
const createMorphContext = (function () {
function createMorphContext(oldNode, newContent, config) {
const { persistentIds, idMap } = createIdMaps(oldNode, newContent);
const mergedConfig = mergeDefaults(config);
const morphStyle = mergedConfig.morphStyle || 'outerHTML';
if (!['innerHTML', 'outerHTML'].includes(morphStyle)) {
throw `Do not understand how to morph style ${morphStyle}`;
}
return {
target: oldNode,
newContent: newContent,
config: mergedConfig,
morphStyle: morphStyle,
ignoreActive: mergedConfig.ignoreActive,
ignoreActiveValue: mergedConfig.ignoreActiveValue,
restoreFocus: mergedConfig.restoreFocus,
idMap: idMap,
persistentIds: persistentIds,
pantry: createPantry(),
callbacks: mergedConfig.callbacks,
head: mergedConfig.head,
};
}
function mergeDefaults(config) {
let finalConfig = Object.assign({}, defaults);
Object.assign(finalConfig, config);
finalConfig.callbacks = Object.assign(
{},
defaults.callbacks,
config.callbacks,
);
finalConfig.head = Object.assign({}, defaults.head, config.head);
return finalConfig;
}
function createPantry() {
const pantry = document.createElement('div');
pantry.hidden = true;
document.body.insertAdjacentElement('afterend', pantry);
return pantry;
}
function findIdElements(root) {
let elements = Array.from(root.querySelectorAll('[id]'));
if (root.id) {
elements.push(root);
}
return elements;
}
function populateIdMapWithTree(idMap, persistentIds, root, elements) {
for (const elt of elements) {
if (persistentIds.has(elt.id)) {
let current = elt;
while (current) {
let idSet = idMap.get(current);
if (idSet == null) {
idSet = new Set();
idMap.set(current, idSet);
}
idSet.add(elt.id);
if (current === root) break;
current = current.parentElement;
}
}
}
}
function createIdMaps(oldContent, newContent) {
const oldIdElements = findIdElements(oldContent);
const newIdElements = findIdElements(newContent);
const persistentIds = createPersistentIds(
oldIdElements,
newIdElements,
);
let idMap = new Map();
populateIdMapWithTree(
idMap,
persistentIds,
oldContent,
oldIdElements,
);
const newRoot = newContent.__idiomorphRoot || newContent;
populateIdMapWithTree(idMap, persistentIds, newRoot, newIdElements);
return { persistentIds, idMap };
}
function createPersistentIds(oldIdElements, newIdElements) {
let duplicateIds = new Set();
let oldIdTagNameMap = new Map();
for (const { id, tagName } of oldIdElements) {
if (oldIdTagNameMap.has(id)) {
duplicateIds.add(id);
} else {
oldIdTagNameMap.set(id, tagName);
}
}
let persistentIds = new Set();
for (const { id, tagName } of newIdElements) {
if (persistentIds.has(id)) {
duplicateIds.add(id);
} else if (oldIdTagNameMap.get(id) === tagName) {
persistentIds.add(id);
}
}
for (const id of duplicateIds) {
persistentIds.delete(id);
}
return persistentIds;
}
return createMorphContext;
})();
const { normalizeElement, normalizeParent } = (function () {
const generatedByIdiomorph = new WeakSet();
function normalizeElement(content) {
if (content instanceof Document) {
return content.documentElement;
} else {
return content;
}
}
function normalizeParent(newContent) {
if (newContent == null) {
return document.createElement('div'); } else if (typeof newContent === 'string') {
return normalizeParent(parseContent(newContent));
} else if (
generatedByIdiomorph.has( (newContent))
) {
return (newContent);
} else if (newContent instanceof Node) {
if (newContent.parentNode) {
return createDuckTypedParent(newContent);
} else {
const dummyParent = document.createElement('div');
dummyParent.append(newContent);
return dummyParent;
}
} else {
const dummyParent = document.createElement('div');
for (const elt of [...newContent]) {
dummyParent.append(elt);
}
return dummyParent;
}
}
function createDuckTypedParent(newContent) {
return (
({
childNodes: [newContent],
querySelectorAll: (s) => {
const elements = newContent.querySelectorAll(s);
return newContent.matches(s)
? [newContent, ...elements]
: elements;
},
insertBefore: (n, r) =>
newContent.parentNode.insertBefore(n, r),
moveBefore: (n, r) =>
newContent.parentNode.moveBefore(n, r),
get __idiomorphRoot() {
return newContent;
},
})
);
}
function parseContent(newContent) {
let parser = new DOMParser();
let contentWithSvgsRemoved = newContent.replace(
/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim,
'',
);
if (
contentWithSvgsRemoved.match(/<\/html>/) ||
contentWithSvgsRemoved.match(/<\/head>/) ||
contentWithSvgsRemoved.match(/<\/body>/)
) {
let content = parser.parseFromString(newContent, 'text/html');
if (contentWithSvgsRemoved.match(/<\/html>/)) {
generatedByIdiomorph.add(content);
return content;
} else {
let htmlElement = content.firstChild;
if (htmlElement) {
generatedByIdiomorph.add(htmlElement);
}
return htmlElement;
}
} else {
let responseDoc = parser.parseFromString(
'<body><template>' + newContent + '</template></body>',
'text/html',
);
let content = (
responseDoc.body.querySelector('template')
).content;
generatedByIdiomorph.add(content);
return content;
}
}
return { normalizeElement, normalizeParent };
})();
return {
morph,
defaults,
};
})();
export { Idiomorph };