256 lines
8 KiB
JavaScript
256 lines
8 KiB
JavaScript
(function() {
|
|
htmx.defineExtension('preload', {
|
|
onEvent: function(name, event) {
|
|
if (name === 'htmx:afterProcessNode') {
|
|
const parent = event.target || event.detail.elt;
|
|
const preloadNodes = [
|
|
...parent.hasAttribute("preload") ? [parent] : [],
|
|
...parent.querySelectorAll("[preload]")]
|
|
preloadNodes.forEach(function(node) {
|
|
init(node)
|
|
node.querySelectorAll('[href],[hx-get],[data-hx-get]').forEach(init)
|
|
})
|
|
return
|
|
}
|
|
|
|
if (name === 'htmx:beforeRequest') {
|
|
const requestHeaders = event.detail.requestConfig.headers
|
|
if (!("HX-Preloaded" in requestHeaders
|
|
&& requestHeaders["HX-Preloaded"] === "true")) {
|
|
return
|
|
}
|
|
|
|
event.preventDefault()
|
|
const xhr = event.detail.xhr
|
|
xhr.onload = function() {
|
|
processResponse(event.detail.elt, xhr.responseText)
|
|
}
|
|
xhr.onerror = null
|
|
xhr.onabort = null
|
|
xhr.ontimeout = null
|
|
xhr.send()
|
|
}
|
|
}
|
|
})
|
|
|
|
function init(node) {
|
|
if (node.preloadState !== undefined) {
|
|
return
|
|
}
|
|
|
|
if (!isValidNodeForPreloading(node)) {
|
|
return
|
|
}
|
|
|
|
if (node instanceof HTMLFormElement) {
|
|
const form = node
|
|
if (!((form.hasAttribute('method') && form.method === 'get')
|
|
|| form.hasAttribute('hx-get') || form.hasAttribute('hx-data-get'))) {
|
|
return
|
|
}
|
|
for (let i = 0; i < form.elements.length; i++) {
|
|
const element = form.elements.item(i);
|
|
init(element);
|
|
if ("labels" in element) {
|
|
element.labels.forEach(init);
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
let preloadAttr = getClosestAttribute(node, 'preload');
|
|
node.preloadAlways = preloadAttr && preloadAttr.includes('always');
|
|
if (node.preloadAlways) {
|
|
preloadAttr = preloadAttr.replace('always', '').trim();
|
|
}
|
|
let triggerEventName = preloadAttr || 'mousedown';
|
|
|
|
const needsTimeout = triggerEventName === 'mouseover'
|
|
node.addEventListener(triggerEventName, getEventHandler(node, needsTimeout), {passive: true})
|
|
|
|
if (triggerEventName === 'mousedown' || triggerEventName === 'mouseover') {
|
|
node.addEventListener('touchstart', getEventHandler(node), {passive: true})
|
|
}
|
|
|
|
if (triggerEventName === 'mouseover') {
|
|
node.addEventListener('mouseout', function(evt) {
|
|
if ((evt.target === node) && (node.preloadState === 'TIMEOUT')) {
|
|
node.preloadState = 'READY'
|
|
}
|
|
}, {passive: true})
|
|
}
|
|
|
|
node.preloadState = 'READY'
|
|
htmx.trigger(node, 'preload:init')
|
|
}
|
|
|
|
function getEventHandler(node, needsTimeout = false) {
|
|
return function() {
|
|
if (node.preloadState !== 'READY') {
|
|
return
|
|
}
|
|
|
|
if (needsTimeout) {
|
|
node.preloadState = 'TIMEOUT'
|
|
const timeoutMs = 100
|
|
window.setTimeout(function() {
|
|
if (node.preloadState === 'TIMEOUT') {
|
|
node.preloadState = 'READY'
|
|
load(node)
|
|
}
|
|
}, timeoutMs)
|
|
return
|
|
}
|
|
|
|
load(node)
|
|
}
|
|
}
|
|
|
|
function load(node) {
|
|
if (node.preloadState !== 'READY') {
|
|
return
|
|
}
|
|
node.preloadState = 'LOADING'
|
|
|
|
const hxGet = node.getAttribute('hx-get') || node.getAttribute('data-hx-get')
|
|
if (hxGet) {
|
|
sendHxGetRequest(hxGet, node);
|
|
return
|
|
}
|
|
|
|
const hxBoost = getClosestAttribute(node, "hx-boost") === "true"
|
|
if (node.hasAttribute('href')) {
|
|
const url = node.getAttribute('href');
|
|
if (hxBoost) {
|
|
sendHxGetRequest(url, node);
|
|
} else {
|
|
sendXmlGetRequest(url, node);
|
|
}
|
|
return
|
|
}
|
|
|
|
if (isPreloadableFormElement(node)) {
|
|
const url = node.form.getAttribute('action')
|
|
|| node.form.getAttribute('hx-get')
|
|
|| node.form.getAttribute('data-hx-get');
|
|
const formData = htmx.values(node.form);
|
|
const isStandardForm = !(node.form.getAttribute('hx-get')
|
|
|| node.form.getAttribute('data-hx-get')
|
|
|| hxBoost);
|
|
const sendGetRequest = isStandardForm ? sendXmlGetRequest : sendHxGetRequest
|
|
|
|
if (node.type === 'submit') {
|
|
sendGetRequest(url, node.form, formData)
|
|
return
|
|
}
|
|
|
|
const inputName = node.name || node.control.name;
|
|
if (node.tagName === 'SELECT') {
|
|
Array.from(node.options).forEach(option => {
|
|
if (option.selected) return;
|
|
formData.set(inputName, option.value);
|
|
const formDataOrdered = forceFormDataInOrder(node.form, formData);
|
|
sendGetRequest(url, node.form, formDataOrdered)
|
|
});
|
|
return
|
|
}
|
|
|
|
const inputType = node.getAttribute("type") || node.control.getAttribute("type");
|
|
const nodeValue = node.value || node.control?.value;
|
|
if (inputType === 'radio') {
|
|
formData.set(inputName, nodeValue);
|
|
} else if (inputType === 'checkbox'){
|
|
const inputValues = formData.getAll(inputName);
|
|
if (inputValues.includes(nodeValue)) {
|
|
formData[inputName] = inputValues.filter(value => value !== nodeValue);
|
|
} else {
|
|
formData.append(inputName, nodeValue);
|
|
}
|
|
}
|
|
const formDataOrdered = forceFormDataInOrder(node.form, formData);
|
|
sendGetRequest(url, node.form, formDataOrdered)
|
|
return
|
|
}
|
|
}
|
|
|
|
function forceFormDataInOrder(form, formData) {
|
|
const formElements = form.elements;
|
|
const orderedFormData = new FormData();
|
|
for(let i = 0; i < formElements.length; i++) {
|
|
const element = formElements.item(i);
|
|
if (formData.has(element.name) && element.tagName === 'SELECT') {
|
|
orderedFormData.append(
|
|
element.name, formData.get(element.name));
|
|
continue;
|
|
}
|
|
if (formData.has(element.name) && formData.getAll(element.name)
|
|
.includes(element.value)) {
|
|
orderedFormData.append(element.name, element.value);
|
|
}
|
|
}
|
|
return orderedFormData;
|
|
}
|
|
|
|
function sendHxGetRequest(url, sourceNode, formData = undefined) {
|
|
htmx.ajax('GET', url, {
|
|
source: sourceNode,
|
|
values: formData,
|
|
headers: {"HX-Preloaded": "true"}
|
|
});
|
|
}
|
|
|
|
function sendXmlGetRequest(url, sourceNode, formData = undefined) {
|
|
const xhr = new XMLHttpRequest()
|
|
if (formData) {
|
|
url += '?' + new URLSearchParams(formData.entries()).toString()
|
|
}
|
|
xhr.open('GET', url);
|
|
xhr.setRequestHeader("HX-Preloaded", "true")
|
|
xhr.onload = function() { processResponse(sourceNode, xhr.responseText) }
|
|
xhr.send()
|
|
}
|
|
|
|
function processResponse(node, responseText) {
|
|
node.preloadState = node.preloadAlways ? 'READY' : 'DONE'
|
|
|
|
if (getClosestAttribute(node, 'preload-images') === 'true') {
|
|
document.createElement('div').innerHTML = responseText
|
|
}
|
|
}
|
|
|
|
function getClosestAttribute(node, attribute) {
|
|
if (node == undefined) { return undefined }
|
|
return node.getAttribute(attribute)
|
|
|| node.getAttribute('data-' + attribute)
|
|
|| getClosestAttribute(node.parentElement, attribute)
|
|
}
|
|
|
|
function isValidNodeForPreloading(node) {
|
|
const getReqAttrs = ['href', 'hx-get', 'data-hx-get'];
|
|
const includesGetRequest = node => getReqAttrs.some(a => node.hasAttribute(a))
|
|
|| node.method === 'get';
|
|
const isPreloadableGetFormElement = node.form instanceof HTMLFormElement
|
|
&& includesGetRequest(node.form)
|
|
&& isPreloadableFormElement(node)
|
|
if (!includesGetRequest(node) && !isPreloadableGetFormElement) {
|
|
return false
|
|
}
|
|
|
|
if (node instanceof HTMLInputElement && node.closest('label')) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
function isPreloadableFormElement(node) {
|
|
if (node instanceof HTMLInputElement || node instanceof HTMLButtonElement) {
|
|
const type = node.getAttribute('type');
|
|
return ['checkbox', 'radio', 'submit'].includes(type);
|
|
}
|
|
if (node instanceof HTMLLabelElement) {
|
|
return node.control && isPreloadableFormElement(node.control);
|
|
}
|
|
return node instanceof HTMLSelectElement;
|
|
}
|
|
})()
|