1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
|
//==========================================================
// head-support.js
//
// 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://htmx.org/extensions")
}
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;
})
}
});
})()
|