Skip to content

Commit

Permalink
fix: head-support race condition #2599
Browse files Browse the repository at this point in the history
  • Loading branch information
stukennedy committed Jul 9, 2024
1 parent d9eb6d7 commit eda8e28
Showing 1 changed file with 167 additions and 141 deletions.
308 changes: 167 additions & 141 deletions dist/ext/head-support.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,144 +3,170 @@
//
// An extension to htmx 1.0 to add head tag merging.
//==========================================================
(function(){

if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://extensions.htmx.org")
}

var api = null;

function log() {
//console.log(arguments);
}

function mergeHead(newContent, defaultMergeStrategy) {

if (newContent && newContent.indexOf('<head') > -1) {
const htmlDoc = document.createElement("html");
// remove svgs to avoid conflicts
var contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
// extract head tag
var headTag = contentWithSvgsRemoved.match(/(<head(\s[^>]*>|>)([\s\S]*?)<\/head>)/im);

// if the head tag exists...
if (headTag) {

var added = []
var removed = []
var preserved = []
var nodesToAppend = []

htmlDoc.innerHTML = headTag;
var newHeadTag = htmlDoc.querySelector("head");
var currentHead = document.head;

if (newHeadTag == null) {
return;
} else {
// put all new head elements into a Map, by their outerHTML
var srcToNewHeadNodes = new Map();
for (const newHeadChild of newHeadTag.children) {
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
}
}



// determine merge strategy
var mergeStrategy = api.getAttributeValue(newHeadTag, "hx-head") || defaultMergeStrategy;

// get the current head
for (const currentHeadElt of currentHead.children) {

// If the current head element is in the map
var inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
var isReAppended = currentHeadElt.getAttribute("hx-head") === "re-eval";
var isPreserved = api.getAttributeValue(currentHeadElt, "hx-preserve") === "true";
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 (mergeStrategy === "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 (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: currentHeadElt}) !== false) {
removed.push(currentHeadElt);
}
}
}
}

// Push the tremaining new head elements in the Map into the
// nodes to append to the head tag
nodesToAppend.push(...srcToNewHeadNodes.values());
log("to append: ", nodesToAppend);

for (const newNode of nodesToAppend) {
log("adding: ", newNode);
var newElt = document.createRange().createContextualFragment(newNode.outerHTML);
log(newElt);
if (api.triggerEvent(document.body, "htmx:addingHeadElement", {headElement: newElt}) !== false) {
currentHead.appendChild(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 (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: removedElement}) !== false) {
currentHead.removeChild(removedElement);
}
}

api.triggerEvent(document.body, "htmx:afterHeadMerge", {added: added, kept: preserved, removed: removed});
}
}
}

htmx.defineExtension("head-support", {
init: function(apiRef) {
// store a reference to the internal API.
api = apiRef;

htmx.on('htmx:afterSwap', function(evt){
var serverResponse = evt.detail.xhr.response;
if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) {
mergeHead(serverResponse, evt.detail.boosted ? "merge" : "append");
}
})

htmx.on('htmx:historyRestore', function(evt){
if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) {
if (evt.detail.cacheMiss) {
mergeHead(evt.detail.serverResponse, "merge");
} else {
mergeHead(evt.detail.item.head, "merge");
}
}
})

htmx.on('htmx:historyItemCreated', function(evt){
var historyItem = evt.detail.item;
historyItem.head = document.head.outerHTML;
})
}
});

})()
(function () {
if (htmx.version && !htmx.version.startsWith('1.')) {
console.warn(
'WARNING: You are using an htmx 1 extension with htmx ' +
htmx.version +
'. It is recommended that you move to the version of this extension found on https://extensions.htmx.org'
);
}

var api = null;

function log() {
//console.log(arguments);
}

function mergeHead(newContent, defaultMergeStrategy) {
if (newContent && newContent.indexOf('<head') > -1) {
const htmlDoc = document.createElement('html');
// remove svgs to avoid conflicts
var contentWithSvgsRemoved = newContent.replace(
/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim,
''
);
// extract head tag
var headTag = contentWithSvgsRemoved.match(
/(<head(\s[^>]*>|>)([\s\S]*?)<\/head>)/im
);

// if the head tag exists...
if (headTag) {
var added = [];
var removed = [];
var preserved = [];
var nodesToAppend = [];

htmlDoc.innerHTML = headTag;
var newHeadTag = htmlDoc.querySelector('head');
var currentHead = document.head;

if (newHeadTag == null) {
return;
} else {
// put all new head elements into a Map, by their outerHTML
var srcToNewHeadNodes = new Map();
for (const newHeadChild of newHeadTag.children) {
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
}
}

// determine merge strategy
var mergeStrategy =
api.getAttributeValue(newHeadTag, 'hx-head') || defaultMergeStrategy;

// get the current head
for (const currentHeadElt of currentHead.children) {
// If the current head element is in the map
var inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
var isReAppended =
currentHeadElt.getAttribute('hx-head') === 're-eval';
var isPreserved =
api.getAttributeValue(currentHeadElt, 'hx-preserve') === 'true';
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 (mergeStrategy === '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 (
api.triggerEvent(document.body, 'htmx:removingHeadElement', {
headElement: currentHeadElt,
}) !== false
) {
removed.push(currentHeadElt);
}
}
}
}

// Push the tremaining new head elements in the Map into the
// nodes to append to the head tag
nodesToAppend.push(...srcToNewHeadNodes.values());
log('to append: ', nodesToAppend);

for (const newNode of nodesToAppend) {
log('adding: ', newNode);
var newElt = document
.createRange()
.createContextualFragment(newNode.outerHTML);
log(newElt);
if (
api.triggerEvent(document.body, 'htmx:addingHeadElement', {
headElement: newElt,
}) !== false
) {
currentHead.appendChild(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 (
api.triggerEvent(document.body, 'htmx:removingHeadElement', {
headElement: removedElement,
}) !== false
) {
currentHead.removeChild(removedElement);
}
}

api.triggerEvent(document.body, 'htmx:afterHeadMerge', {
added: added,
kept: preserved,
removed: removed,
});
}
}
}

htmx.defineExtension('head-support', {
init: function (apiRef) {
// store a reference to the internal API.
api = apiRef;

htmx.on('htmx:afterSettle', function (evt) {
var serverResponse = evt.detail.xhr.response;
if (
api.triggerEvent(document.body, 'htmx:beforeHeadMerge', evt.detail)
) {
mergeHead(serverResponse, evt.detail.boosted ? 'merge' : 'append');
}
});

htmx.on('htmx:historyRestore', function (evt) {
if (
api.triggerEvent(document.body, 'htmx:beforeHeadMerge', evt.detail)
) {
if (evt.detail.cacheMiss) {
mergeHead(evt.detail.serverResponse, 'merge');
} else {
mergeHead(evt.detail.item.head, 'merge');
}
}
});

htmx.on('htmx:historyItemCreated', function (evt) {
var historyItem = evt.detail.item;
historyItem.head = document.head.outerHTML;
});
},
});
})();

0 comments on commit eda8e28

Please sign in to comment.