From 5a387e821b97555b4bc7a000f2934ac982c51887 Mon Sep 17 00:00:00 2001 From: AntonLV Date: Fri, 15 Mar 2024 15:37:44 +0300 Subject: [PATCH] Ticket #4641 - Use HTMX to load page content without genaral page structure. --- inc/classes/BxDolMenu.php | 1 + install/sql/system.sql | 3 +- .../template/system/scripts/BxTemplMenu.php | 9 +- plugins_public/htmx/head-support.js | 141 ++++++++++++++++++ plugins_public/{ => htmx}/htmx.min.js | 0 template/page_58.html | 4 + 6 files changed, 155 insertions(+), 3 deletions(-) create mode 100644 plugins_public/htmx/head-support.js rename plugins_public/{ => htmx}/htmx.min.js (100%) diff --git a/inc/classes/BxDolMenu.php b/inc/classes/BxDolMenu.php index d326cd5806..8a008eb62b 100644 --- a/inc/classes/BxDolMenu.php +++ b/inc/classes/BxDolMenu.php @@ -84,6 +84,7 @@ class BxDolMenu extends BxDolFactory implements iBxDolFactoryObject, iBxDolRepla protected $_bIsApi; protected $_bHx; + protected $_bHxHead; protected $_aHx; protected $_bDynamicMode; diff --git a/install/sql/system.sql b/install/sql/system.sql index 88e94b34a1..2d463d164b 100644 --- a/install/sql/system.sql +++ b/install/sql/system.sql @@ -6256,7 +6256,8 @@ INSERT INTO `sys_preloader`(`module`, `type`, `content`, `active`, `order`) VALU ('system', 'js_system', 'headroom.min.js', 1, 10), ('system', 'js_system', 'at.js/js/jquery.atwho.min.js', 1, 11), ('system', 'js_system', 'prism/prism.js', 1, 12), -('system', 'js_system', 'htmx.min.js', 1, 13), +('system', 'js_system', 'htmx/htmx.min.js', 1, 13), +('system', 'js_system', 'htmx/head-support.js', 1, 14), ('system', 'js_system', 'functions.js', 1, 20), ('system', 'js_system', 'jquery.webForms.js', 1, 21), ('system', 'js_system', 'jquery.dolPopup.js', 1, 22), diff --git a/modules/boonex/artificer/data/template/system/scripts/BxTemplMenu.php b/modules/boonex/artificer/data/template/system/scripts/BxTemplMenu.php index 72baca6351..b65145bf9c 100644 --- a/modules/boonex/artificer/data/template/system/scripts/BxTemplMenu.php +++ b/modules/boonex/artificer/data/template/system/scripts/BxTemplMenu.php @@ -22,16 +22,21 @@ public function __construct ($aObject, $oTemplate = false) $sClass = 'bx-menu-tab-active'; $this->_bHx = true; + $this->_bHxHead = true; $this->_aHx = [ 'get' => '', 'trigger' => 'click', 'target' => '#bx-content-wrapper', 'swap' => 'outerHTML settle:200ms', - 'replace-url' => 'true', + 'push-url' => 'true', 'on:htmx-after-on-load' => 'jQuery(this).parent().addClass(\'' . $sClass . '\').siblings().removeClass(\'' . $sClass . '\');' ]; - $this->_oTemplate->addInjection('injection_body', 'text', 'hx-on::after-request="jQuery(this).bxProcessHtml()"'); + $sInjection = 'hx-on::after-request="jQuery(this).bxProcessHtml()"'; + if($this->_bHxHead) + $sInjection .= ' hx-ext="head-support"'; + + $this->_oTemplate->addInjection('injection_body', 'text', $sInjection); } } diff --git a/plugins_public/htmx/head-support.js b/plugins_public/htmx/head-support.js new file mode 100644 index 0000000000..67cfc6924b --- /dev/null +++ b/plugins_public/htmx/head-support.js @@ -0,0 +1,141 @@ +//========================================================== +// head-support.js +// +// An extension to htmx 1.0 to add head tag merging. +//========================================================== +(function(){ + + var api = null; + + function log() { + //console.log(arguments); + } + + function mergeHead(newContent, defaultMergeStrategy) { + + if (newContent && newContent.indexOf(' -1) { + const htmlDoc = document.createElement("html"); + // remove svgs to avoid conflicts + var contentWithSvgsRemoved = newContent.replace(/]*>|>)([\s\S]*?)<\/svg>/gim, ''); + // extract head tag + var headTag = contentWithSvgsRemoved.match(/(]*>|>)([\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; + }) + } + }); + +})() \ No newline at end of file diff --git a/plugins_public/htmx.min.js b/plugins_public/htmx/htmx.min.js similarity index 100% rename from plugins_public/htmx.min.js rename to plugins_public/htmx/htmx.min.js diff --git a/template/page_58.html b/template/page_58.html index 49fbd0df69..0488006a31 100644 --- a/template/page_58.html +++ b/template/page_58.html @@ -1,3 +1,7 @@ + + __page_header__ + __meta_info__ +
__page_main_code__