|
3 | 3 | //
|
4 | 4 | // An extension to htmx 1.0 to add head tag merging.
|
5 | 5 | //==========================================================
|
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