update head-support.js

This commit is contained in:
Carson Gross 2022-10-28 10:17:27 -06:00
parent fe4c002db9
commit 6ec00b7bed
6 changed files with 148 additions and 85 deletions

View File

@ -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");
}
}
})

View File

@ -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,

View File

@ -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();
}
})

View File

@ -0,0 +1,4 @@
<head>
<script hx-head="re-eval" src="./basic-script.js"></script>
</head>
Basic Sourced Script (Should Alert)

View File

@ -0,0 +1,6 @@
<head>
<script hx-head="re-eval">
alert("basic script")
</script>
</head>
Basic Inline Script (Should Alert)

View File

@ -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: