diff --git a/inc/classes/BxDolMenu.php b/inc/classes/BxDolMenu.php index 8a008eb62b..6ce814dfba 100644 --- a/inc/classes/BxDolMenu.php +++ b/inc/classes/BxDolMenu.php @@ -85,6 +85,7 @@ class BxDolMenu extends BxDolFactory implements iBxDolFactoryObject, iBxDolRepla protected $_bHx; protected $_bHxHead; + protected $_mHxPreload; protected $_aHx; protected $_bDynamicMode; diff --git a/inc/utils.inc.php b/inc/utils.inc.php index 6b1e4b471d..3a313b0877 100644 --- a/inc/utils.inc.php +++ b/inc/utils.inc.php @@ -2240,17 +2240,20 @@ function bx_get_htmx_target () return isset($_SERVER['HTTP_HX_TARGET']) ? $_SERVER['HTTP_HX_TARGET'] : false; } -function bx_get_htmx_attrs ($a) +function bx_get_htmx_attrs ($aAttrs, $mPreload = false) { - if(!$a) + if(!$aAttrs) return ''; + if($mPreload === true) + $mPreload = 'mousedown'; + $aHxAttrs = []; - array_walk($a, function($mixedValue, $sIndex) use (&$aHxAttrs) { + array_walk($aAttrs, function($mixedValue, $sIndex) use (&$aHxAttrs) { $aHxAttrs['hx-' . $sIndex] = $mixedValue; }); - return bx_convert_array2attrs($aHxAttrs); + return bx_convert_array2attrs($aHxAttrs) . ($mPreload ? ' preload="' . $mPreload . '"' : ''); } function bx_idn_to_utf8($sUrl, $bReturnDomain = false) diff --git a/install/sql/system.sql b/install/sql/system.sql index 2d463d164b..dee11de5fb 100644 --- a/install/sql/system.sql +++ b/install/sql/system.sql @@ -6258,6 +6258,7 @@ INSERT INTO `sys_preloader`(`module`, `type`, `content`, `active`, `order`) VALU ('system', 'js_system', 'prism/prism.js', 1, 12), ('system', 'js_system', 'htmx/htmx.min.js', 1, 13), ('system', 'js_system', 'htmx/head-support.js', 1, 14), +('system', 'js_system', 'htmx/preload.js', 1, 15), ('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 0867ad03c3..523771e4fa 100644 --- a/modules/boonex/artificer/data/template/system/scripts/BxTemplMenu.php +++ b/modules/boonex/artificer/data/template/system/scripts/BxTemplMenu.php @@ -23,6 +23,7 @@ public function __construct ($aObject, $oTemplate = false) $this->_bHx = true; $this->_bHxHead = true; + $this->_mHxPreload = true; $this->_aHx = [ 'get' => '', 'trigger' => 'click', @@ -33,9 +34,15 @@ public function __construct ($aObject, $oTemplate = false) 'on::after-on-load' => 'oBxArtificerUtils.submenuClickAl(this)' ]; - $sInjection = 'hx-on::after-request="jQuery(this).bxProcessHtml()"'; + $sExtensions = ''; if($this->_bHxHead) - $sInjection .= ' hx-ext="head-support"'; + $sExtensions .= ' head-support'; + if($this->_mHxPreload) + $sExtensions .= ' preload'; + + $sInjection = 'hx-on::after-request="jQuery(this).bxProcessHtml()"'; + if(($sExtensions = trim($sExtensions)) != '') + $sInjection .= ' hx-ext="' . $sExtensions . '"'; $this->_oTemplate->addInjection('injection_body', 'text', $sInjection); } diff --git a/plugins_public/htmx/preload.js b/plugins_public/htmx/preload.js new file mode 100644 index 0000000000..a749370339 --- /dev/null +++ b/plugins_public/htmx/preload.js @@ -0,0 +1,147 @@ +// This adds the "preload" extension to htmx. By default, this will +// preload the targets of any tags with `href` or `hx-get` attributes +// if they also have a `preload` attribute as well. See documentation +// for more details +htmx.defineExtension("preload", { + + onEvent: function(name, event) { + + // Only take actions on "htmx:afterProcessNode" + if (name !== "htmx:afterProcessNode") { + return; + } + + // SOME HELPER FUNCTIONS WE'LL NEED ALONG THE WAY + + // attr gets the closest non-empty value from the attribute. + var attr = function(node, property) { + if (node == undefined) {return undefined;} + return node.getAttribute(property) || node.getAttribute("data-" + property) || attr(node.parentElement, property) + } + + // load handles the actual HTTP fetch, and uses htmx.ajax in cases where we're + // preloading an htmx resource (this sends the same HTTP headers as a regular htmx request) + var load = function(node) { + + // Called after a successful AJAX request, to mark the + // content as loaded (and prevent additional AJAX calls.) + var done = function(html) { + if (!node.preloadAlways) { + node.preloadState = "DONE" + } + + if (attr(node, "preload-images") == "true") { + document.createElement("div").innerHTML = html // create and populate a node to load linked resources, too. + } + } + + return function() { + + // If this value has already been loaded, then do not try again. + if (node.preloadState !== "READY") { + return; + } + + // Special handling for HX-GET - use built-in htmx.ajax function + // so that headers match other htmx requests, then set + // node.preloadState = TRUE so that requests are not duplicated + // in the future + var hxGet = node.getAttribute("hx-get") || node.getAttribute("data-hx-get") + if (hxGet) { + htmx.ajax("GET", hxGet, { + source: node, + handler:function(elt, info) { + done(info.xhr.responseText); + } + }); + return; + } + + // Otherwise, perform a standard xhr request, then set + // node.preloadState = TRUE so that requests are not duplicated + // in the future. + if (node.getAttribute("href")) { + var r = new XMLHttpRequest(); + r.open("GET", node.getAttribute("href")); + r.onload = function() {done(r.responseText);}; + r.send(); + return; + } + } + } + + // This function processes a specific node and sets up event handlers. + // We'll search for nodes and use it below. + var init = function(node) { + + // If this node DOES NOT include a "GET" transaction, then there's nothing to do here. + if (node.getAttribute("href") + node.getAttribute("hx-get") + node.getAttribute("data-hx-get") == "") { + return; + } + + // Guarantee that we only initialize each node once. + if (node.preloadState !== undefined) { + return; + } + + // Get event name from config. + var on = attr(node, "preload") || "mousedown" + const always = on.indexOf("always") !== -1 + if (always) { + on = on.replace('always', '').trim() + } + + // FALL THROUGH to here means we need to add an EventListener + + // Apply the listener to the node + node.addEventListener(on, function(evt) { + if (node.preloadState === "PAUSE") { // Only add one event listener + node.preloadState = "READY"; // Required for the `load` function to trigger + + // Special handling for "mouseover" events. Wait 100ms before triggering load. + if (on === "mouseover") { + window.setTimeout(load(node), 100); + } else { + load(node)() // all other events trigger immediately. + } + } + }) + + // Special handling for certain built-in event handlers + switch (on) { + + case "mouseover": + // Mirror `touchstart` events (fires immediately) + node.addEventListener("touchstart", load(node)); + + // WHhen the mouse leaves, immediately disable the preload + node.addEventListener("mouseout", function(evt) { + if ((evt.target === node) && (node.preloadState === "READY")) { + node.preloadState = "PAUSE"; + } + }) + break; + + case "mousedown": + // Mirror `touchstart` events (fires immediately) + node.addEventListener("touchstart", load(node)); + break; + } + + // Mark the node as ready to run. + node.preloadState = "PAUSE"; + node.preloadAlways = always; + htmx.trigger(node, "preload:init") // This event can be used to load content immediately. + } + + // Search for all child nodes that have a "preload" attribute + event.target.querySelectorAll("[preload]").forEach(function(node) { + + // Initialize the node with the "preload" attribute + init(node) + + // Initialize all child elements that are anchors or have `hx-get` (use with care) + node.querySelectorAll("a,[hx-get],[data-hx-get]").forEach(init) + }) + } +}) diff --git a/template/scripts/BxBaseMenu.php b/template/scripts/BxBaseMenu.php index 6a173ccd59..e6034cf4d8 100644 --- a/template/scripts/BxBaseMenu.php +++ b/template/scripts/BxBaseMenu.php @@ -238,7 +238,7 @@ protected function _getMenuItem ($a) if($this->_bHx && !empty($a['link']) && strpos($a['link'], 'javascript:') === false) { $this->_aHx['get'] = $a['link']; - $a['attrs'] .= bx_get_htmx_attrs($this->_aHx); + $a['attrs'] .= bx_get_htmx_attrs($this->_aHx, $this->_mHxPreload); if(!bx_is_htmx_request() && !$this->_isSelected($a)) $a['attrs_wrp'] .= bx_get_htmx_attrs([