Skip to content

Commit eda8e28

Browse files
committed
fix: head-support race condition #2599
1 parent d9eb6d7 commit eda8e28

File tree

1 file changed

+167
-141
lines changed

1 file changed

+167
-141
lines changed

dist/ext/head-support.js

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

0 commit comments

Comments
 (0)