UNPKG

6.2 kBJavaScriptView Raw
1//==========================================================
2// head-support.js
3//
4// An extension to htmx 1.0 to add head tag merging.
5//==========================================================
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})()
\No newline at end of file