Skip to content

Commit 8be3305

Browse files
committed
fix: head-support race condition #2599
1 parent 7dd6cd7 commit 8be3305

File tree

1 file changed

+159
-136
lines changed

1 file changed

+159
-136
lines changed

dist/ext/head-support.js

+159-136
Original file line numberDiff line numberDiff line change
@@ -3,139 +3,162 @@
33
//
44
// An extension to htmx 1.0 to add head tag merging.
55
//==========================================================
6-
(function(){
7-
8-
var api = null;
9-
10-
function log() {
11-
//console.log(arguments);
12-
}
13-
14-
function mergeHead(newContent, defaultMergeStrategy) {
15-
16-
if (newContent && newContent.indexOf('<head') > -1) {
17-
const htmlDoc = document.createElement("html");
18-
// remove svgs to avoid conflicts
19-
var contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
20-
// extract head tag
21-
var headTag = contentWithSvgsRemoved.match(/(<head(\s[^>]*>|>)([\s\S]*?)<\/head>)/im);
22-
23-
// if the head tag exists...
24-
if (headTag) {
25-
26-
var added = []
27-
var removed = []
28-
var preserved = []
29-
var nodesToAppend = []
30-
31-
htmlDoc.innerHTML = headTag;
32-
var newHeadTag = htmlDoc.querySelector("head");
33-
var currentHead = document.head;
34-
35-
if (newHeadTag == null) {
36-
return;
37-
} else {
38-
// put all new head elements into a Map, by their outerHTML
39-
var srcToNewHeadNodes = new Map();
40-
for (const newHeadChild of newHeadTag.children) {
41-
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
42-
}
43-
}
44-
45-
46-
47-
// determine merge strategy
48-
var mergeStrategy = api.getAttributeValue(newHeadTag, "hx-head") || defaultMergeStrategy;
49-
50-
// get the current head
51-
for (const currentHeadElt of currentHead.children) {
52-
53-
// If the current head element is in the map
54-
var inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
55-
var isReAppended = currentHeadElt.getAttribute("hx-head") === "re-eval";
56-
var isPreserved = api.getAttributeValue(currentHeadElt, "hx-preserve") === "true";
57-
if (inNewContent || isPreserved) {
58-
if (isReAppended) {
59-
// remove the current version and let the new version replace it and re-execute
60-
removed.push(currentHeadElt);
61-
} else {
62-
// this element already exists and should not be re-appended, so remove it from
63-
// the new content map, preserving it in the DOM
64-
srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
65-
preserved.push(currentHeadElt);
66-
}
67-
} else {
68-
if (mergeStrategy === "append") {
69-
// we are appending and this existing element is not new content
70-
// so if and only if it is marked for re-append do we do anything
71-
if (isReAppended) {
72-
removed.push(currentHeadElt);
73-
nodesToAppend.push(currentHeadElt);
74-
}
75-
} else {
76-
// if this is a merge, we remove this content since it is not in the new head
77-
if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: currentHeadElt}) !== false) {
78-
removed.push(currentHeadElt);
79-
}
80-
}
81-
}
82-
}
83-
84-
// Push the tremaining new head elements in the Map into the
85-
// nodes to append to the head tag
86-
nodesToAppend.push(...srcToNewHeadNodes.values());
87-
log("to append: ", nodesToAppend);
88-
89-
for (const newNode of nodesToAppend) {
90-
log("adding: ", newNode);
91-
var newElt = document.createRange().createContextualFragment(newNode.outerHTML);
92-
log(newElt);
93-
if (api.triggerEvent(document.body, "htmx:addingHeadElement", {headElement: newElt}) !== false) {
94-
currentHead.appendChild(newElt);
95-
added.push(newElt);
96-
}
97-
}
98-
99-
// remove all removed elements, after we have appended the new elements to avoid
100-
// additional network requests for things like style sheets
101-
for (const removedElement of removed) {
102-
if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: removedElement}) !== false) {
103-
currentHead.removeChild(removedElement);
104-
}
105-
}
106-
107-
api.triggerEvent(document.body, "htmx:afterHeadMerge", {added: added, kept: preserved, removed: removed});
108-
}
109-
}
110-
}
111-
112-
htmx.defineExtension("head-support", {
113-
init: function(apiRef) {
114-
// store a reference to the internal API.
115-
api = apiRef;
116-
117-
htmx.on('htmx:afterSwap', function(evt){
118-
var serverResponse = evt.detail.xhr.response;
119-
if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) {
120-
mergeHead(serverResponse, evt.detail.boosted ? "merge" : "append");
121-
}
122-
})
123-
124-
htmx.on('htmx:historyRestore', function(evt){
125-
if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) {
126-
if (evt.detail.cacheMiss) {
127-
mergeHead(evt.detail.serverResponse, "merge");
128-
} else {
129-
mergeHead(evt.detail.item.head, "merge");
130-
}
131-
}
132-
})
133-
134-
htmx.on('htmx:historyItemCreated', function(evt){
135-
var historyItem = evt.detail.item;
136-
historyItem.head = document.head.outerHTML;
137-
})
138-
}
139-
});
140-
141-
})()
6+
(function () {
7+
var api = null;
8+
9+
function log() {
10+
//console.log(arguments);
11+
}
12+
13+
function mergeHead(newContent, defaultMergeStrategy) {
14+
if (newContent && newContent.indexOf('<head') > -1) {
15+
const htmlDoc = document.createElement('html');
16+
// remove svgs to avoid conflicts
17+
var contentWithSvgsRemoved = newContent.replace(
18+
/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim,
19+
''
20+
);
21+
// extract head tag
22+
var headTag = contentWithSvgsRemoved.match(
23+
/(<head(\s[^>]*>|>)([\s\S]*?)<\/head>)/im
24+
);
25+
26+
// if the head tag exists...
27+
if (headTag) {
28+
var added = [];
29+
var removed = [];
30+
var preserved = [];
31+
var nodesToAppend = [];
32+
33+
htmlDoc.innerHTML = headTag;
34+
var newHeadTag = htmlDoc.querySelector('head');
35+
var currentHead = document.head;
36+
37+
if (newHeadTag == null) {
38+
return;
39+
} else {
40+
// put all new head elements into a Map, by their outerHTML
41+
var srcToNewHeadNodes = new Map();
42+
for (const newHeadChild of newHeadTag.children) {
43+
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
44+
}
45+
}
46+
47+
// determine merge strategy
48+
var mergeStrategy =
49+
api.getAttributeValue(newHeadTag, 'hx-head') || defaultMergeStrategy;
50+
51+
// get the current head
52+
for (const currentHeadElt of currentHead.children) {
53+
// If the current head element is in the map
54+
var inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
55+
var isReAppended =
56+
currentHeadElt.getAttribute('hx-head') === 're-eval';
57+
var isPreserved =
58+
api.getAttributeValue(currentHeadElt, 'hx-preserve') === 'true';
59+
if (inNewContent || isPreserved) {
60+
if (isReAppended) {
61+
// remove the current version and let the new version replace it and re-execute
62+
removed.push(currentHeadElt);
63+
} else {
64+
// this element already exists and should not be re-appended, so remove it from
65+
// the new content map, preserving it in the DOM
66+
srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
67+
preserved.push(currentHeadElt);
68+
}
69+
} else {
70+
if (mergeStrategy === 'append') {
71+
// we are appending and this existing element is not new content
72+
// so if and only if it is marked for re-append do we do anything
73+
if (isReAppended) {
74+
removed.push(currentHeadElt);
75+
nodesToAppend.push(currentHeadElt);
76+
}
77+
} else {
78+
// if this is a merge, we remove this content since it is not in the new head
79+
if (
80+
api.triggerEvent(document.body, 'htmx:removingHeadElement', {
81+
headElement: currentHeadElt,
82+
}) !== false
83+
) {
84+
removed.push(currentHeadElt);
85+
}
86+
}
87+
}
88+
}
89+
90+
// Push the tremaining new head elements in the Map into the
91+
// nodes to append to the head tag
92+
nodesToAppend.push(...srcToNewHeadNodes.values());
93+
log('to append: ', nodesToAppend);
94+
95+
for (const newNode of nodesToAppend) {
96+
log('adding: ', newNode);
97+
var newElt = document
98+
.createRange()
99+
.createContextualFragment(newNode.outerHTML);
100+
log(newElt);
101+
if (
102+
api.triggerEvent(document.body, 'htmx:addingHeadElement', {
103+
headElement: newElt,
104+
}) !== false
105+
) {
106+
currentHead.appendChild(newElt);
107+
added.push(newElt);
108+
}
109+
}
110+
111+
// remove all removed elements, after we have appended the new elements to avoid
112+
// additional network requests for things like style sheets
113+
for (const removedElement of removed) {
114+
if (
115+
api.triggerEvent(document.body, 'htmx:removingHeadElement', {
116+
headElement: removedElement,
117+
}) !== false
118+
) {
119+
currentHead.removeChild(removedElement);
120+
}
121+
}
122+
123+
api.triggerEvent(document.body, 'htmx:afterHeadMerge', {
124+
added: added,
125+
kept: preserved,
126+
removed: removed,
127+
});
128+
}
129+
}
130+
}
131+
132+
htmx.defineExtension('head-support', {
133+
init: function (apiRef) {
134+
// store a reference to the internal API.
135+
api = apiRef;
136+
137+
htmx.on('htmx:afterSettle', function (evt) {
138+
var serverResponse = evt.detail.xhr.response;
139+
if (
140+
api.triggerEvent(document.body, 'htmx:beforeHeadMerge', evt.detail)
141+
) {
142+
mergeHead(serverResponse, evt.detail.boosted ? 'merge' : 'append');
143+
}
144+
});
145+
146+
htmx.on('htmx:historyRestore', function (evt) {
147+
if (
148+
api.triggerEvent(document.body, 'htmx:beforeHeadMerge', evt.detail)
149+
) {
150+
if (evt.detail.cacheMiss) {
151+
mergeHead(evt.detail.serverResponse, 'merge');
152+
} else {
153+
mergeHead(evt.detail.item.head, 'merge');
154+
}
155+
}
156+
});
157+
158+
htmx.on('htmx:historyItemCreated', function (evt) {
159+
var historyItem = evt.detail.item;
160+
historyItem.head = document.head.outerHTML;
161+
});
162+
},
163+
});
164+
})();

0 commit comments

Comments
 (0)