mirror of
https://github.com/bigskysoftware/htmx.git
synced 2025-10-02 15:25:26 +00:00
update head-support.js
This commit is contained in:
parent
fe4c002db9
commit
6ec00b7bed
@ -11,7 +11,7 @@
|
||||
//console.log(arguments);
|
||||
}
|
||||
|
||||
function mergeHead(newContent) {
|
||||
function mergeHead(newContent, defaultMergeStrategy) {
|
||||
|
||||
if (newContent && newContent.indexOf('<head') > -1) {
|
||||
const htmlDoc = document.createElement("html");
|
||||
@ -25,65 +25,69 @@
|
||||
|
||||
var added = []
|
||||
var removed = []
|
||||
var kept = []
|
||||
|
||||
var preserved = []
|
||||
var nodesToAppend = []
|
||||
|
||||
htmlDoc.innerHTML = headTag;
|
||||
var newHeadTag = htmlDoc.querySelector("head");
|
||||
|
||||
//
|
||||
var appendOnly = false;
|
||||
if (api.getAttributeValue(newHeadTag, "hx-swap-oob") === "beforeend") {
|
||||
appendOnly = true;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// put all new head elements into a Map, by their outerHTML
|
||||
var srcToNewHeadNodes = new Map();
|
||||
var newHeadChildren = newHeadTag.children;
|
||||
for (let i = 0; i < newHeadChildren.length; i++) {
|
||||
const newHeadChild = newHeadChildren[i];
|
||||
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
|
||||
}
|
||||
|
||||
// iterate the existing head elements
|
||||
var currentHead = document.querySelector("head");
|
||||
var currentHeadChildren = currentHead.children;
|
||||
for (let i = 0; i < currentHeadChildren.length; i++) {
|
||||
const currentHeadChild = currentHeadChildren[i];
|
||||
|
||||
// determine merge strategy
|
||||
var mergeStrategy = api.getAttributeValue(newHeadTag, "hx-head") || defaultMergeStrategy;
|
||||
|
||||
// get the current head
|
||||
var currentHead = document.head;
|
||||
for (const currentHeadElt of currentHead.children) {
|
||||
|
||||
// If the current head element is in the map
|
||||
if (srcToNewHeadNodes.has(currentHeadChild.outerHTML)) {
|
||||
|
||||
// Remove it from the map, we aren't going to insert it and
|
||||
// the current element can stay
|
||||
log(currentHeadChild, " found in current head content, removing from Map");
|
||||
srcToNewHeadNodes.delete(currentHeadChild.outerHTML);
|
||||
kept.push(currentHeadChild);
|
||||
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 the current head element is NOT in the map, remove it
|
||||
if (appendOnly === false &&
|
||||
api.getAttributeValue(currentHeadChild, "hx-preserve") !== "true" &&
|
||||
api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: currentHeadChild}) !== false) {
|
||||
log(currentHeadChild, " not found in new content, removing from head tag");
|
||||
removed.push(currentHeadChild);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove all removed elements
|
||||
for (let i = 0; i < removed.length; i++) {
|
||||
const removedElement = removed[i];
|
||||
currentHead.removeChild(removedElement);
|
||||
}
|
||||
// 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);
|
||||
|
||||
// The remaining elements in the Map are in the new content but
|
||||
// not in the old content, so add them (using a contextual fragment
|
||||
// so that script tags will evaluate)
|
||||
var remainder = srcToNewHeadNodes.keys();
|
||||
log("remainder: ", remainder);
|
||||
for (const remainderNodeSource of remainder) {
|
||||
log("adding: ", remainderNodeSource);
|
||||
var newElt = document.createRange().createContextualFragment(remainderNodeSource);
|
||||
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);
|
||||
@ -91,7 +95,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
api.triggerEvent(document.body, "htmx:afterHeadMerge", {added: added, kept: kept, removed: removed});
|
||||
// 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});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -104,16 +116,16 @@
|
||||
htmx.on('htmx:afterSwap', function(evt){
|
||||
var serverResponse = evt.detail.xhr.response;
|
||||
if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) {
|
||||
mergeHead(serverResponse);
|
||||
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);
|
||||
mergeHead(evt.detail.serverResponse, "merge");
|
||||
} else {
|
||||
mergeHead(evt.detail.item.head);
|
||||
mergeHead(evt.detail.item.head, "merge");
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -2745,7 +2745,9 @@ return (function () {
|
||||
|
||||
var requestAttrValues = getValuesForElement(elt, 'hx-request');
|
||||
|
||||
var eltIsBoosted = getInternalData(elt).boosted;
|
||||
var requestConfig = {
|
||||
boosted: eltIsBoosted,
|
||||
parameters: filteredParameters,
|
||||
unfilteredParameters: allParameters,
|
||||
headers:headers,
|
||||
@ -2818,7 +2820,7 @@ return (function () {
|
||||
}
|
||||
|
||||
var responseInfo = {
|
||||
xhr: xhr, target: target, requestConfig: requestConfig, etc: etc,
|
||||
xhr: xhr, target: target, requestConfig: requestConfig, etc: etc, boosted: eltIsBoosted,
|
||||
pathInfo: {
|
||||
requestPath: path,
|
||||
finalRequestPath: finalPathForGet || path,
|
||||
|
@ -11,16 +11,26 @@
|
||||
|
||||
<h1>head-support Extension Testing</h1>
|
||||
|
||||
<h2>CSS</h2>
|
||||
<h2>CSS + Boosting</h2>
|
||||
|
||||
<h3>Basic Merge 1 (inline CSS)</h3>
|
||||
<button hx-get="./basic-css-1.html">Basic CSS Merge (Should Add Red Border)</button>
|
||||
<a href="./basic-css-1.html"
|
||||
hx-boost="true"
|
||||
hx-target="this"
|
||||
hx-push-url="false">
|
||||
Basic CSS Merge (Should Add Red Border)
|
||||
</a>
|
||||
<p id="basic-css-1">
|
||||
Basic Merge 1
|
||||
</p>
|
||||
|
||||
<h3>Basic Merge 2 (inline CSS)</h3>
|
||||
<button hx-get="./basic-css-2.html">Basic CSS Merge (Should Add Blue Border)</button>
|
||||
<a href="./basic-css-2.html"
|
||||
hx-boost="true"
|
||||
hx-target="this"
|
||||
hx-push-url="false">
|
||||
Basic CSS Merge (Should Add Blue Border)
|
||||
</a>
|
||||
<p id="basic-css-2">
|
||||
Basic Merge 2
|
||||
</p>
|
||||
@ -31,7 +41,7 @@
|
||||
Basic Merge 3
|
||||
</p>
|
||||
|
||||
<h3>History</h3>
|
||||
<h2>History</h2>
|
||||
<button hx-get="./basic-history-1.html" hx-push-url="true">Basic CSS Merge (Should Add Red Border)</button>
|
||||
<p id="basic-history-1">
|
||||
Basic History 1
|
||||
@ -39,17 +49,47 @@
|
||||
|
||||
<h2>Title</h2>
|
||||
|
||||
<h3>Basic Title Update</h3>
|
||||
<button hx-get="./basic-title.html">Basic Title Merge (Should Set Title to "A New Title")</button>
|
||||
<h2>Title + Boosting</h2>
|
||||
<a href="./basic-title.html"
|
||||
hx-boost="true"
|
||||
hx-target="this"
|
||||
hx-push-url="false">
|
||||
Basic Title Merge (Should Set Title to "A New Title")
|
||||
</a>
|
||||
|
||||
<h2>JavaScript</h2>
|
||||
<h2>Javascript + Boosting</h2>
|
||||
<h3>Inline Script</h3>
|
||||
<button hx-get="./basic-script.html">Basic Inline Script (Should Alert)</button>
|
||||
<a href="./basic-script.html"
|
||||
hx-boost="true"
|
||||
hx-target="this"
|
||||
hx-push-url="false">
|
||||
Basic Inline Script (Should Alert)
|
||||
</a>
|
||||
|
||||
<h3>Script File</h3>
|
||||
<button hx-get="./basic-script-2.html">Basic Sourced Script (Should Alert)</button>
|
||||
<a href="reeval-basic-script-2.html"
|
||||
hx-boost="true"
|
||||
hx-target="this"
|
||||
hx-push-url="false">Basic Sourced Script (Should Alert)</a>
|
||||
|
||||
<h2>Merging Options</h2>
|
||||
<h2>Re-Evaluate + Boosting</h2>
|
||||
|
||||
<h3>Inline Script</h3>
|
||||
<a href="./reeval-script.html"
|
||||
hx-boost="true"
|
||||
hx-target="this"
|
||||
hx-push-url="false">
|
||||
Basic Inline Script (Should Alert)
|
||||
</a>
|
||||
|
||||
<h3>Script File</h3>
|
||||
<a href="reeval-script-2.html"
|
||||
hx-boost="true"
|
||||
hx-target="this"
|
||||
hx-push-url="false">Basic Sourced Script (Should Alert)</a>
|
||||
|
||||
|
||||
<h2>Appending</h2>
|
||||
|
||||
<h3><code>hx-preserve</code> keeps element in head</h3>
|
||||
<button hx-get="./preserve-1.html">Adds Preserved Style via hx-preserve</button>
|
||||
@ -57,8 +97,8 @@
|
||||
Basic Preserve 1
|
||||
</p>
|
||||
|
||||
<h3><code>hx-swap-oob</code> adds elements to existing head</h3>
|
||||
<button hx-get="./preserve-2.html">Preserves Current Style via hx-swap</button>
|
||||
<h3>Normal GET appends</h3>
|
||||
<button hx-get="./preserve-2.html">Preserves Current Style via hx-get</button>
|
||||
<p id="basic-perserve-2">
|
||||
Basic Preserve 2
|
||||
</p>
|
||||
@ -66,7 +106,7 @@
|
||||
<h3>Script can keep element in head</h3>
|
||||
<script>
|
||||
htmx.on("htmx:removingHeadElement", function(evt){
|
||||
if (evt.detail.headElement.getAttribute("sample-preserve")) {
|
||||
if (evt.detail.headElement && evt.detail.headElement.getAttribute("sample-preserve")) {
|
||||
evt.preventDefault();
|
||||
}
|
||||
})
|
||||
|
4
test/manual/head-support/reeval-script-2.html
Normal file
4
test/manual/head-support/reeval-script-2.html
Normal file
@ -0,0 +1,4 @@
|
||||
<head>
|
||||
<script hx-head="re-eval" src="./basic-script.js"></script>
|
||||
</head>
|
||||
Basic Sourced Script (Should Alert)
|
6
test/manual/head-support/reeval-script.html
Normal file
6
test/manual/head-support/reeval-script.html
Normal file
@ -0,0 +1,6 @@
|
||||
<head>
|
||||
<script hx-head="re-eval">
|
||||
alert("basic script")
|
||||
</script>
|
||||
</head>
|
||||
Basic Inline Script (Should Alert)
|
@ -29,14 +29,32 @@ install the extension using the `hx-ext` attribute:
|
||||
```
|
||||
|
||||
With this installed, all responses that htmx receives that contain a `head` tag in them (even if they are not complete
|
||||
HTML documents with a root `<html>` element) will be processed and _merged_ into the current head tag.
|
||||
HTML documents with a root `<html>` element) will be processed.
|
||||
|
||||
The merge algorithm is as follows:
|
||||
How the head tag is handled depends on the type of htmx request.
|
||||
|
||||
If the htmx request is from a boosted element, then the following merge algorithm is used:
|
||||
|
||||
* Elements that exist in the current head as exact textual matches will be left in place
|
||||
* Elements that do not exist in the current head will be added at the end of the head tag
|
||||
* Elements that exist in the current head, but not in the new head will be removed from the head
|
||||
|
||||
If the htmx request is from a non-boosted element, then all content will be _appended_ to the existing head element.
|
||||
|
||||
If you wish to override this behavior in either case, you can place the `hx-head` attribute on the new `<head>` tag,
|
||||
with either of the following two values:
|
||||
|
||||
* `merge` - follow the merging algorithm outlined above
|
||||
* `append` - append the elements to the existing head
|
||||
|
||||
#### Controlling Merge Behavior
|
||||
|
||||
Beyond this, you may also control merging behavior of individual elements with the following attributes:
|
||||
|
||||
* If you place `hx-head="re-eval"` on a head element, it will be re-added (removed and appended) to the head tag on every
|
||||
request, even if it already exists. This can be useful to execute a script on every htmx request, for example.
|
||||
* If you place `hx-preserve="true"` on an element, it will never be removed from the head
|
||||
|
||||
#### Example
|
||||
|
||||
As an example, consider the following head tag in an existing document:
|
||||
@ -81,25 +99,6 @@ The final head element will look like this:
|
||||
</head>
|
||||
```
|
||||
|
||||
### Controlling Merge Behavior
|
||||
|
||||
Sometimes you may want to preserve an element in the head tag. You can do so using events, discussed below, but this
|
||||
extension also gives you two declarative mechanisms for doing so:
|
||||
|
||||
* If an element in the `head` tag has an `hx-preserve="true"` attribute & value on it, it will not be removed from the head tag:
|
||||
```html
|
||||
<!-- This element will not be removed even if it is not in new head content received from the server-->
|
||||
<link rel="stylesheet" href="/css/site1.css" hx-preserve="true">
|
||||
```
|
||||
* If a new `head` element _in the content of a response_ has the `hx-swap-oob="beforeend"` attribute & value, the content of the new
|
||||
head element will be added to the existing head tag, but no content will be removed from the existing head tag.
|
||||
```html
|
||||
<!-- This content will be appended to the head tag, leaving current content in place -->
|
||||
<head hx-swap-oob="beforeend">
|
||||
<link rel="stylesheet" href="/css/site1.css">
|
||||
</head>
|
||||
```
|
||||
|
||||
### Events
|
||||
|
||||
This extension triggers the following events:
|
||||
|
Loading…
x
Reference in New Issue
Block a user