head-support.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. //==========================================================
  2. // head-support.js
  3. //
  4. // An extension to htmx 1.0 to add head tag merging.
  5. //==========================================================
  6. (function(){
  7. var api = null;
  8. function log() {
  9. //console.log(arguments);
  10. }
  11. function mergeHead(newContent, defaultMergeStrategy) {
  12. if (newContent && newContent.indexOf('<head') > -1) {
  13. const htmlDoc = document.createElement("html");
  14. // remove svgs to avoid conflicts
  15. var contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
  16. // extract head tag
  17. var headTag = contentWithSvgsRemoved.match(/(<head(\s[^>]*>|>)([\s\S]*?)<\/head>)/im);
  18. // if the head tag exists...
  19. if (headTag) {
  20. var added = []
  21. var removed = []
  22. var preserved = []
  23. var nodesToAppend = []
  24. htmlDoc.innerHTML = headTag;
  25. var newHeadTag = htmlDoc.querySelector("head");
  26. var currentHead = document.head;
  27. if (newHeadTag == null) {
  28. return;
  29. } else {
  30. // put all new head elements into a Map, by their outerHTML
  31. var srcToNewHeadNodes = new Map();
  32. for (const newHeadChild of newHeadTag.children) {
  33. srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
  34. }
  35. }
  36. // determine merge strategy
  37. var mergeStrategy = api.getAttributeValue(newHeadTag, "hx-head") || defaultMergeStrategy;
  38. // get the current head
  39. for (const currentHeadElt of currentHead.children) {
  40. // If the current head element is in the map
  41. var inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
  42. var isReAppended = currentHeadElt.getAttribute("hx-head") === "re-eval";
  43. var isPreserved = api.getAttributeValue(currentHeadElt, "hx-preserve") === "true";
  44. if (inNewContent || isPreserved) {
  45. if (isReAppended) {
  46. // remove the current version and let the new version replace it and re-execute
  47. removed.push(currentHeadElt);
  48. } else {
  49. // this element already exists and should not be re-appended, so remove it from
  50. // the new content map, preserving it in the DOM
  51. srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
  52. preserved.push(currentHeadElt);
  53. }
  54. } else {
  55. if (mergeStrategy === "append") {
  56. // we are appending and this existing element is not new content
  57. // so if and only if it is marked for re-append do we do anything
  58. if (isReAppended) {
  59. removed.push(currentHeadElt);
  60. nodesToAppend.push(currentHeadElt);
  61. }
  62. } else {
  63. // if this is a merge, we remove this content since it is not in the new head
  64. if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: currentHeadElt}) !== false) {
  65. removed.push(currentHeadElt);
  66. }
  67. }
  68. }
  69. }
  70. // Push the tremaining new head elements in the Map into the
  71. // nodes to append to the head tag
  72. nodesToAppend.push(...srcToNewHeadNodes.values());
  73. log("to append: ", nodesToAppend);
  74. for (const newNode of nodesToAppend) {
  75. log("adding: ", newNode);
  76. var newElt = document.createRange().createContextualFragment(newNode.outerHTML);
  77. log(newElt);
  78. if (api.triggerEvent(document.body, "htmx:addingHeadElement", {headElement: newElt}) !== false) {
  79. currentHead.appendChild(newElt);
  80. added.push(newElt);
  81. }
  82. }
  83. // remove all removed elements, after we have appended the new elements to avoid
  84. // additional network requests for things like style sheets
  85. for (const removedElement of removed) {
  86. if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: removedElement}) !== false) {
  87. currentHead.removeChild(removedElement);
  88. }
  89. }
  90. api.triggerEvent(document.body, "htmx:afterHeadMerge", {added: added, kept: preserved, removed: removed});
  91. }
  92. }
  93. }
  94. htmx.defineExtension("head-support", {
  95. init: function(apiRef) {
  96. // store a reference to the internal API.
  97. api = apiRef;
  98. htmx.on('htmx:afterSwap', function(evt){
  99. var serverResponse = evt.detail.xhr.response;
  100. if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) {
  101. mergeHead(serverResponse, evt.detail.boosted ? "merge" : "append");
  102. }
  103. })
  104. htmx.on('htmx:historyRestore', function(evt){
  105. if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) {
  106. if (evt.detail.cacheMiss) {
  107. mergeHead(evt.detail.serverResponse, "merge");
  108. } else {
  109. mergeHead(evt.detail.item.head, "merge");
  110. }
  111. }
  112. })
  113. htmx.on('htmx:historyItemCreated', function(evt){
  114. var historyItem = evt.detail.item;
  115. historyItem.head = document.head.outerHTML;
  116. })
  117. }
  118. });
  119. })()