···11+//==========================================================
22+// head-support.js
33+//
44+// An extension to htmx 1.0 to add head tag merging.
55+//==========================================================
66+(function(){
77+88+ var api = null;
99+1010+ function log() {
1111+ //console.log(arguments);
1212+ }
1313+1414+ function mergeHead(newContent, defaultMergeStrategy) {
1515+1616+ if (newContent && newContent.indexOf('<head') > -1) {
1717+ const htmlDoc = document.createElement("html");
1818+ // remove svgs to avoid conflicts
1919+ var contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
2020+ // extract head tag
2121+ var headTag = contentWithSvgsRemoved.match(/(<head(\s[^>]*>|>)([\s\S]*?)<\/head>)/im);
2222+2323+ // if the head tag exists...
2424+ if (headTag) {
2525+2626+ var added = []
2727+ var removed = []
2828+ var preserved = []
2929+ var nodesToAppend = []
3030+3131+ htmlDoc.innerHTML = headTag;
3232+ var newHeadTag = htmlDoc.querySelector("head");
3333+ var currentHead = document.head;
3434+3535+ if (newHeadTag == null) {
3636+ return;
3737+ } else {
3838+ // put all new head elements into a Map, by their outerHTML
3939+ var srcToNewHeadNodes = new Map();
4040+ for (const newHeadChild of newHeadTag.children) {
4141+ srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
4242+ }
4343+ }
4444+4545+4646+4747+ // determine merge strategy
4848+ var mergeStrategy = api.getAttributeValue(newHeadTag, "hx-head") || defaultMergeStrategy;
4949+5050+ // get the current head
5151+ for (const currentHeadElt of currentHead.children) {
5252+5353+ // If the current head element is in the map
5454+ var inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
5555+ var isReAppended = currentHeadElt.getAttribute("hx-head") === "re-eval";
5656+ var isPreserved = api.getAttributeValue(currentHeadElt, "hx-preserve") === "true";
5757+ if (inNewContent || isPreserved) {
5858+ if (isReAppended) {
5959+ // remove the current version and let the new version replace it and re-execute
6060+ removed.push(currentHeadElt);
6161+ } else {
6262+ // this element already exists and should not be re-appended, so remove it from
6363+ // the new content map, preserving it in the DOM
6464+ srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
6565+ preserved.push(currentHeadElt);
6666+ }
6767+ } else {
6868+ if (mergeStrategy === "append") {
6969+ // we are appending and this existing element is not new content
7070+ // so if and only if it is marked for re-append do we do anything
7171+ if (isReAppended) {
7272+ removed.push(currentHeadElt);
7373+ nodesToAppend.push(currentHeadElt);
7474+ }
7575+ } else {
7676+ // if this is a merge, we remove this content since it is not in the new head
7777+ if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: currentHeadElt}) !== false) {
7878+ removed.push(currentHeadElt);
7979+ }
8080+ }
8181+ }
8282+ }
8383+8484+ // Push the tremaining new head elements in the Map into the
8585+ // nodes to append to the head tag
8686+ nodesToAppend.push(...srcToNewHeadNodes.values());
8787+ log("to append: ", nodesToAppend);
8888+8989+ for (const newNode of nodesToAppend) {
9090+ log("adding: ", newNode);
9191+ var newElt = document.createRange().createContextualFragment(newNode.outerHTML);
9292+ log(newElt);
9393+ if (api.triggerEvent(document.body, "htmx:addingHeadElement", {headElement: newElt}) !== false) {
9494+ currentHead.appendChild(newElt);
9595+ added.push(newElt);
9696+ }
9797+ }
9898+9999+ // remove all removed elements, after we have appended the new elements to avoid
100100+ // additional network requests for things like style sheets
101101+ for (const removedElement of removed) {
102102+ if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: removedElement}) !== false) {
103103+ currentHead.removeChild(removedElement);
104104+ }
105105+ }
106106+107107+ api.triggerEvent(document.body, "htmx:afterHeadMerge", {added: added, kept: preserved, removed: removed});
108108+ }
109109+ }
110110+ }
111111+112112+ htmx.defineExtension("head-support", {
113113+ init: function(apiRef) {
114114+ // store a reference to the internal API.
115115+ api = apiRef;
116116+117117+ htmx.on('htmx:afterSwap', function(evt){
118118+ var serverResponse = evt.detail.xhr.response;
119119+ if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) {
120120+ mergeHead(serverResponse, evt.detail.boosted ? "merge" : "append");
121121+ }
122122+ })
123123+124124+ htmx.on('htmx:historyRestore', function(evt){
125125+ if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) {
126126+ if (evt.detail.cacheMiss) {
127127+ mergeHead(evt.detail.serverResponse, "merge");
128128+ } else {
129129+ mergeHead(evt.detail.item.head, "merge");
130130+ }
131131+ }
132132+ })
133133+134134+ htmx.on('htmx:historyItemCreated', function(evt){
135135+ var historyItem = evt.detail.item;
136136+ historyItem.head = document.head.outerHTML;
137137+ })
138138+ }
139139+ });
140140+141141+})()