Compare commits
1 commit
master
...
dom-swappi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c528f9ddd |
18 changed files with 1490 additions and 21 deletions
562
framework/assets/dist/htmgo.js
vendored
562
framework/assets/dist/htmgo.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -6,6 +6,7 @@ import "./htmxextensions/response-targets";
|
|||
import "./htmxextensions/mutation-error";
|
||||
import "./htmxextensions/livereload"
|
||||
import "./htmxextensions/htmgo";
|
||||
import "./htmxextensions/diffdomswap"
|
||||
|
||||
function watchUrl(callback: (oldUrl: string, newUrl: string) => void) {
|
||||
let lastUrl = window.location.href;
|
||||
|
|
|
|||
23
framework/assets/js/htmxextensions/diffdomswap.ts
Normal file
23
framework/assets/js/htmxextensions/diffdomswap.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import htmx from "htmx.org";
|
||||
import Idiomorph from './../idiomorph.js'
|
||||
|
||||
// @ts-ignore
|
||||
htmx.defineExtension('diffdom-swap', {
|
||||
isInlineSwap: function(swapStyle) {
|
||||
return swapStyle === 'diffdom'
|
||||
},
|
||||
// @ts-ignore
|
||||
handleSwap: function(swapStyle: string, target: any, fragment: any) {
|
||||
if (swapStyle === 'diffdom') {
|
||||
if (fragment.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
||||
console.log('diffdom swap', target, fragment)
|
||||
console.log(Idiomorph.morph(target, fragment.firstElementChild))
|
||||
return [target]
|
||||
} else {
|
||||
console.log('diffdom swap', target, fragment)
|
||||
console.log(Idiomorph.morph(target, fragment.outerHTML))
|
||||
return [target]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
850
framework/assets/js/idiomorph.js
Normal file
850
framework/assets/js/idiomorph.js
Normal file
|
|
@ -0,0 +1,850 @@
|
|||
// base IIFE to define idiomorph
|
||||
const Idiomorph = (function () {
|
||||
'use strict';
|
||||
|
||||
//=============================================================================
|
||||
// AND NOW IT BEGINS...
|
||||
//=============================================================================
|
||||
let EMPTY_SET = new Set();
|
||||
|
||||
// default configuration values, updatable by users now
|
||||
let defaults = {
|
||||
morphStyle: "outerHTML",
|
||||
callbacks : {
|
||||
beforeNodeAdded: noOp,
|
||||
afterNodeAdded: noOp,
|
||||
beforeNodeMorphed: noOp,
|
||||
afterNodeMorphed: noOp,
|
||||
beforeNodeRemoved: noOp,
|
||||
afterNodeRemoved: noOp,
|
||||
beforeAttributeUpdated: noOp,
|
||||
|
||||
},
|
||||
head: {
|
||||
style: 'merge',
|
||||
shouldPreserve: function (elt) {
|
||||
return elt.getAttribute("im-preserve") === "true";
|
||||
},
|
||||
shouldReAppend: function (elt) {
|
||||
return elt.getAttribute("im-re-append") === "true";
|
||||
},
|
||||
shouldRemove: noOp,
|
||||
afterHeadMorphed: noOp,
|
||||
}
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// Core Morphing Algorithm - morph, morphNormalizedContent, morphOldNodeTo, morphChildren
|
||||
//=============================================================================
|
||||
function morph(oldNode, newContent, config = {}) {
|
||||
|
||||
if (oldNode instanceof Document) {
|
||||
oldNode = oldNode.documentElement;
|
||||
}
|
||||
|
||||
if (typeof newContent === 'string') {
|
||||
newContent = parseContent(newContent);
|
||||
}
|
||||
|
||||
let normalizedContent = normalizeContent(newContent);
|
||||
|
||||
let ctx = createMorphContext(oldNode, normalizedContent, config);
|
||||
|
||||
return morphNormalizedContent(oldNode, normalizedContent, ctx);
|
||||
}
|
||||
|
||||
function morphNormalizedContent(oldNode, normalizedNewContent, ctx) {
|
||||
if (ctx.head.block) {
|
||||
let oldHead = oldNode.querySelector('head');
|
||||
let newHead = normalizedNewContent.querySelector('head');
|
||||
if (oldHead && newHead) {
|
||||
let promises = handleHeadElement(newHead, oldHead, ctx);
|
||||
// when head promises resolve, call morph again, ignoring the head tag
|
||||
Promise.all(promises).then(function () {
|
||||
morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, {
|
||||
head: {
|
||||
block: false,
|
||||
ignore: true
|
||||
}
|
||||
}));
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.morphStyle === "innerHTML") {
|
||||
|
||||
// innerHTML, so we are only updating the children
|
||||
morphChildren(normalizedNewContent, oldNode, ctx);
|
||||
return oldNode.children;
|
||||
|
||||
} else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) {
|
||||
// otherwise find the best element match in the new content, morph that, and merge its siblings
|
||||
// into either side of the best match
|
||||
let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx);
|
||||
|
||||
// stash the siblings that will need to be inserted on either side of the best match
|
||||
let previousSibling = bestMatch?.previousSibling;
|
||||
let nextSibling = bestMatch?.nextSibling;
|
||||
|
||||
// morph it
|
||||
let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx);
|
||||
|
||||
if (bestMatch) {
|
||||
// if there was a best match, merge the siblings in too and return the
|
||||
// whole bunch
|
||||
return insertSiblings(previousSibling, morphedNode, nextSibling);
|
||||
} else {
|
||||
// otherwise nothing was added to the DOM
|
||||
return []
|
||||
}
|
||||
} else {
|
||||
throw "Do not understand how to morph style " + ctx.morphStyle;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param possibleActiveElement
|
||||
* @param ctx
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
|
||||
return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param oldNode root node to merge content into
|
||||
* @param newContent new content to merge
|
||||
* @param ctx the merge context
|
||||
* @returns {Element} the element that ended up in the DOM
|
||||
*/
|
||||
function morphOldNodeTo(oldNode, newContent, ctx) {
|
||||
if (ctx.ignoreActive && oldNode === document.activeElement) {
|
||||
// don't morph focused element
|
||||
} else if (newContent == null) {
|
||||
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
|
||||
|
||||
oldNode.remove();
|
||||
ctx.callbacks.afterNodeRemoved(oldNode);
|
||||
return null;
|
||||
} else if (!isSoftMatch(oldNode, newContent)) {
|
||||
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
|
||||
if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode;
|
||||
|
||||
oldNode.parentElement.replaceChild(newContent, oldNode);
|
||||
ctx.callbacks.afterNodeAdded(newContent);
|
||||
ctx.callbacks.afterNodeRemoved(oldNode);
|
||||
return newContent;
|
||||
} else {
|
||||
if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return oldNode;
|
||||
|
||||
if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) {
|
||||
// ignore the head element
|
||||
} else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") {
|
||||
handleHeadElement(newContent, oldNode, ctx);
|
||||
} else {
|
||||
syncNodeFrom(newContent, oldNode, ctx);
|
||||
if (!ignoreValueOfActiveElement(oldNode, ctx)) {
|
||||
morphChildren(newContent, oldNode, ctx);
|
||||
}
|
||||
}
|
||||
ctx.callbacks.afterNodeMorphed(oldNode, newContent);
|
||||
return oldNode;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the core algorithm for matching up children. The idea is to use id sets to try to match up
|
||||
* nodes as faithfully as possible. We greedily match, which allows us to keep the algorithm fast, but
|
||||
* by using id sets, we are able to better match up with content deeper in the DOM.
|
||||
*
|
||||
* Basic algorithm is, for each node in the new content:
|
||||
*
|
||||
* - if we have reached the end of the old parent, append the new content
|
||||
* - if the new content has an id set match with the current insertion point, morph
|
||||
* - search for an id set match
|
||||
* - if id set match found, morph
|
||||
* - otherwise search for a "soft" match
|
||||
* - if a soft match is found, morph
|
||||
* - otherwise, prepend the new node before the current insertion point
|
||||
*
|
||||
* The two search algorithms terminate if competing node matches appear to outweigh what can be achieved
|
||||
* with the current node. See findIdSetMatch() and findSoftMatch() for details.
|
||||
*
|
||||
* @param {Element} newParent the parent element of the new content
|
||||
* @param {Element } oldParent the old content that we are merging the new content into
|
||||
* @param ctx the merge context
|
||||
*/
|
||||
function morphChildren(newParent, oldParent, ctx) {
|
||||
|
||||
let nextNewChild = newParent.firstChild;
|
||||
let insertionPoint = oldParent.firstChild;
|
||||
let newChild;
|
||||
|
||||
// run through all the new content
|
||||
while (nextNewChild) {
|
||||
|
||||
newChild = nextNewChild;
|
||||
nextNewChild = newChild.nextSibling;
|
||||
|
||||
// if we are at the end of the exiting parent's children, just append
|
||||
if (insertionPoint == null) {
|
||||
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
||||
|
||||
oldParent.appendChild(newChild);
|
||||
ctx.callbacks.afterNodeAdded(newChild);
|
||||
removeIdsFromConsideration(ctx, newChild);
|
||||
continue;
|
||||
}
|
||||
|
||||
// if the current node has an id set match then morph
|
||||
if (isIdSetMatch(newChild, insertionPoint, ctx)) {
|
||||
morphOldNodeTo(insertionPoint, newChild, ctx);
|
||||
insertionPoint = insertionPoint.nextSibling;
|
||||
removeIdsFromConsideration(ctx, newChild);
|
||||
continue;
|
||||
}
|
||||
|
||||
// otherwise search forward in the existing old children for an id set match
|
||||
let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx);
|
||||
|
||||
// if we found a potential match, remove the nodes until that point and morph
|
||||
if (idSetMatch) {
|
||||
insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx);
|
||||
morphOldNodeTo(idSetMatch, newChild, ctx);
|
||||
removeIdsFromConsideration(ctx, newChild);
|
||||
continue;
|
||||
}
|
||||
|
||||
// no id set match found, so scan forward for a soft match for the current node
|
||||
let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx);
|
||||
|
||||
// if we found a soft match for the current node, morph
|
||||
if (softMatch) {
|
||||
insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx);
|
||||
morphOldNodeTo(softMatch, newChild, ctx);
|
||||
removeIdsFromConsideration(ctx, newChild);
|
||||
continue;
|
||||
}
|
||||
|
||||
// abandon all hope of morphing, just insert the new child before the insertion point
|
||||
// and move on
|
||||
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
||||
|
||||
oldParent.insertBefore(newChild, insertionPoint);
|
||||
ctx.callbacks.afterNodeAdded(newChild);
|
||||
removeIdsFromConsideration(ctx, newChild);
|
||||
}
|
||||
|
||||
// remove any remaining old nodes that didn't match up with new content
|
||||
while (insertionPoint !== null) {
|
||||
|
||||
let tempNode = insertionPoint;
|
||||
insertionPoint = insertionPoint.nextSibling;
|
||||
removeNode(tempNode, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Attribute Syncing Code
|
||||
//=============================================================================
|
||||
|
||||
/**
|
||||
* @param attr {String} the attribute to be mutated
|
||||
* @param to {Element} the element that is going to be updated
|
||||
* @param updateType {("update"|"remove")}
|
||||
* @param ctx the merge context
|
||||
* @returns {boolean} true if the attribute should be ignored, false otherwise
|
||||
*/
|
||||
function ignoreAttribute(attr, to, updateType, ctx) {
|
||||
if(attr === 'value' && ctx.ignoreActiveValue && to === document.activeElement){
|
||||
return true;
|
||||
}
|
||||
return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* syncs a given node with another node, copying over all attributes and
|
||||
* inner element state from the 'from' node to the 'to' node
|
||||
*
|
||||
* @param {Element} from the element to copy attributes & state from
|
||||
* @param {Element} to the element to copy attributes & state to
|
||||
* @param ctx the merge context
|
||||
*/
|
||||
function syncNodeFrom(from, to, ctx) {
|
||||
let type = from.nodeType
|
||||
|
||||
// if is an element type, sync the attributes from the
|
||||
// new node into the new node
|
||||
if (type === 1 /* element type */) {
|
||||
const fromAttributes = from.attributes;
|
||||
const toAttributes = to.attributes;
|
||||
for (const fromAttribute of fromAttributes) {
|
||||
if (ignoreAttribute(fromAttribute.name, to, 'update', ctx)) {
|
||||
continue;
|
||||
}
|
||||
if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) {
|
||||
to.setAttribute(fromAttribute.name, fromAttribute.value);
|
||||
}
|
||||
}
|
||||
// iterate backwards to avoid skipping over items when a delete occurs
|
||||
for (let i = toAttributes.length - 1; 0 <= i; i--) {
|
||||
const toAttribute = toAttributes[i];
|
||||
if (ignoreAttribute(toAttribute.name, to, 'remove', ctx)) {
|
||||
continue;
|
||||
}
|
||||
if (!from.hasAttribute(toAttribute.name)) {
|
||||
to.removeAttribute(toAttribute.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sync text nodes
|
||||
if (type === 8 /* comment */ || type === 3 /* text */) {
|
||||
if (to.nodeValue !== from.nodeValue) {
|
||||
to.nodeValue = from.nodeValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ignoreValueOfActiveElement(to, ctx)) {
|
||||
// sync input values
|
||||
syncInputValue(from, to, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param from {Element} element to sync the value from
|
||||
* @param to {Element} element to sync the value to
|
||||
* @param attributeName {String} the attribute name
|
||||
* @param ctx the merge context
|
||||
*/
|
||||
function syncBooleanAttribute(from, to, attributeName, ctx) {
|
||||
if (from[attributeName] !== to[attributeName]) {
|
||||
let ignoreUpdate = ignoreAttribute(attributeName, to, 'update', ctx);
|
||||
if (!ignoreUpdate) {
|
||||
to[attributeName] = from[attributeName];
|
||||
}
|
||||
if (from[attributeName]) {
|
||||
if (!ignoreUpdate) {
|
||||
to.setAttribute(attributeName, from[attributeName]);
|
||||
}
|
||||
} else {
|
||||
if (!ignoreAttribute(attributeName, to, 'remove', ctx)) {
|
||||
to.removeAttribute(attributeName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NB: many bothans died to bring us information:
|
||||
*
|
||||
* https://github.com/patrick-steele-idem/morphdom/blob/master/src/specialElHandlers.js
|
||||
* https://github.com/choojs/nanomorph/blob/master/lib/morph.jsL113
|
||||
*
|
||||
* @param from {Element} the element to sync the input value from
|
||||
* @param to {Element} the element to sync the input value to
|
||||
* @param ctx the merge context
|
||||
*/
|
||||
function syncInputValue(from, to, ctx) {
|
||||
if (from instanceof HTMLInputElement &&
|
||||
to instanceof HTMLInputElement &&
|
||||
from.type !== 'file') {
|
||||
|
||||
let fromValue = from.value;
|
||||
let toValue = to.value;
|
||||
|
||||
// sync boolean attributes
|
||||
syncBooleanAttribute(from, to, 'checked', ctx);
|
||||
syncBooleanAttribute(from, to, 'disabled', ctx);
|
||||
|
||||
if (!from.hasAttribute('value')) {
|
||||
if (!ignoreAttribute('value', to, 'remove', ctx)) {
|
||||
to.value = '';
|
||||
to.removeAttribute('value');
|
||||
}
|
||||
} else if (fromValue !== toValue) {
|
||||
if (!ignoreAttribute('value', to, 'update', ctx)) {
|
||||
to.setAttribute('value', fromValue);
|
||||
to.value = fromValue;
|
||||
}
|
||||
}
|
||||
} else if (from instanceof HTMLOptionElement) {
|
||||
syncBooleanAttribute(from, to, 'selected', ctx)
|
||||
} else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) {
|
||||
let fromValue = from.value;
|
||||
let toValue = to.value;
|
||||
if (ignoreAttribute('value', to, 'update', ctx)) {
|
||||
return;
|
||||
}
|
||||
if (fromValue !== toValue) {
|
||||
to.value = fromValue;
|
||||
}
|
||||
if (to.firstChild && to.firstChild.nodeValue !== fromValue) {
|
||||
to.firstChild.nodeValue = fromValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// the HEAD tag can be handled specially, either w/ a 'merge' or 'append' style
|
||||
//=============================================================================
|
||||
function handleHeadElement(newHeadTag, currentHead, ctx) {
|
||||
|
||||
let added = []
|
||||
let removed = []
|
||||
let preserved = []
|
||||
let nodesToAppend = []
|
||||
|
||||
let headMergeStyle = ctx.head.style;
|
||||
|
||||
// put all new head elements into a Map, by their outerHTML
|
||||
let srcToNewHeadNodes = new Map();
|
||||
for (const newHeadChild of newHeadTag.children) {
|
||||
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
|
||||
}
|
||||
|
||||
// for each elt in the current head
|
||||
for (const currentHeadElt of currentHead.children) {
|
||||
|
||||
// If the current head element is in the map
|
||||
let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
|
||||
let isReAppended = ctx.head.shouldReAppend(currentHeadElt);
|
||||
let isPreserved = ctx.head.shouldPreserve(currentHeadElt);
|
||||
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 (headMergeStyle === "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 (ctx.head.shouldRemove(currentHeadElt) !== false) {
|
||||
removed.push(currentHeadElt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Push the remaining new head elements in the Map into the
|
||||
// nodes to append to the head tag
|
||||
nodesToAppend.push(...srcToNewHeadNodes.values());
|
||||
log("to append: ", nodesToAppend);
|
||||
|
||||
let promises = [];
|
||||
for (const newNode of nodesToAppend) {
|
||||
log("adding: ", newNode);
|
||||
let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild;
|
||||
log(newElt);
|
||||
if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
|
||||
if (newElt.href || newElt.src) {
|
||||
let resolve = null;
|
||||
let promise = new Promise(function (_resolve) {
|
||||
resolve = _resolve;
|
||||
});
|
||||
newElt.addEventListener('load', function () {
|
||||
resolve();
|
||||
});
|
||||
promises.push(promise);
|
||||
}
|
||||
currentHead.appendChild(newElt);
|
||||
ctx.callbacks.afterNodeAdded(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 (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
|
||||
currentHead.removeChild(removedElement);
|
||||
ctx.callbacks.afterNodeRemoved(removedElement);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.head.afterHeadMorphed(currentHead, {added: added, kept: preserved, removed: removed});
|
||||
return promises;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Misc
|
||||
//=============================================================================
|
||||
|
||||
function log() {
|
||||
//console.log(arguments);
|
||||
}
|
||||
|
||||
function noOp() {
|
||||
}
|
||||
|
||||
/*
|
||||
Deep merges the config object and the Idiomoroph.defaults object to
|
||||
produce a final configuration object
|
||||
*/
|
||||
function mergeDefaults(config) {
|
||||
let finalConfig = {};
|
||||
// copy top level stuff into final config
|
||||
Object.assign(finalConfig, defaults);
|
||||
Object.assign(finalConfig, config);
|
||||
|
||||
// copy callbacks into final config (do this to deep merge the callbacks)
|
||||
finalConfig.callbacks = {};
|
||||
Object.assign(finalConfig.callbacks, defaults.callbacks);
|
||||
Object.assign(finalConfig.callbacks, config.callbacks);
|
||||
|
||||
// copy head config into final config (do this to deep merge the head)
|
||||
finalConfig.head = {};
|
||||
Object.assign(finalConfig.head, defaults.head);
|
||||
Object.assign(finalConfig.head, config.head);
|
||||
return finalConfig;
|
||||
}
|
||||
|
||||
function createMorphContext(oldNode, newContent, config) {
|
||||
config = mergeDefaults(config);
|
||||
return {
|
||||
target: oldNode,
|
||||
newContent: newContent,
|
||||
config: config,
|
||||
morphStyle: config.morphStyle,
|
||||
ignoreActive: config.ignoreActive,
|
||||
ignoreActiveValue: config.ignoreActiveValue,
|
||||
idMap: createIdMap(oldNode, newContent),
|
||||
deadIds: new Set(),
|
||||
callbacks: config.callbacks,
|
||||
head: config.head
|
||||
}
|
||||
}
|
||||
|
||||
function isIdSetMatch(node1, node2, ctx) {
|
||||
if (node1 == null || node2 == null) {
|
||||
return false;
|
||||
}
|
||||
if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) {
|
||||
if (node1.id !== "" && node1.id === node2.id) {
|
||||
return true;
|
||||
} else {
|
||||
return getIdIntersectionCount(ctx, node1, node2) > 0;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isSoftMatch(node1, node2) {
|
||||
if (node1 == null || node2 == null) {
|
||||
return false;
|
||||
}
|
||||
return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName
|
||||
}
|
||||
|
||||
function removeNodesBetween(startInclusive, endExclusive, ctx) {
|
||||
while (startInclusive !== endExclusive) {
|
||||
let tempNode = startInclusive;
|
||||
startInclusive = startInclusive.nextSibling;
|
||||
removeNode(tempNode, ctx);
|
||||
}
|
||||
removeIdsFromConsideration(ctx, endExclusive);
|
||||
return endExclusive.nextSibling;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Scans forward from the insertionPoint in the old parent looking for a potential id match
|
||||
// for the newChild. We stop if we find a potential id match for the new child OR
|
||||
// if the number of potential id matches we are discarding is greater than the
|
||||
// potential id matches for the new child
|
||||
//=============================================================================
|
||||
function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
|
||||
|
||||
// max id matches we are willing to discard in our search
|
||||
let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent);
|
||||
|
||||
let potentialMatch = null;
|
||||
|
||||
// only search forward if there is a possibility of an id match
|
||||
if (newChildPotentialIdCount > 0) {
|
||||
let potentialMatch = insertionPoint;
|
||||
// if there is a possibility of an id match, scan forward
|
||||
// keep track of the potential id match count we are discarding (the
|
||||
// newChildPotentialIdCount must be greater than this to make it likely
|
||||
// worth it)
|
||||
let otherMatchCount = 0;
|
||||
while (potentialMatch != null) {
|
||||
|
||||
// If we have an id match, return the current potential match
|
||||
if (isIdSetMatch(newChild, potentialMatch, ctx)) {
|
||||
return potentialMatch;
|
||||
}
|
||||
|
||||
// computer the other potential matches of this new content
|
||||
otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent);
|
||||
if (otherMatchCount > newChildPotentialIdCount) {
|
||||
// if we have more potential id matches in _other_ content, we
|
||||
// do not have a good candidate for an id match, so return null
|
||||
return null;
|
||||
}
|
||||
|
||||
// advanced to the next old content child
|
||||
potentialMatch = potentialMatch.nextSibling;
|
||||
}
|
||||
}
|
||||
return potentialMatch;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Scans forward from the insertionPoint in the old parent looking for a potential soft match
|
||||
// for the newChild. We stop if we find a potential soft match for the new child OR
|
||||
// if we find a potential id match in the old parents children OR if we find two
|
||||
// potential soft matches for the next two pieces of new content
|
||||
//=============================================================================
|
||||
function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
|
||||
|
||||
let potentialSoftMatch = insertionPoint;
|
||||
let nextSibling = newChild.nextSibling;
|
||||
let siblingSoftMatchCount = 0;
|
||||
|
||||
while (potentialSoftMatch != null) {
|
||||
|
||||
if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) {
|
||||
// the current potential soft match has a potential id set match with the remaining new
|
||||
// content so bail out of looking
|
||||
return null;
|
||||
}
|
||||
|
||||
// if we have a soft match with the current node, return it
|
||||
if (isSoftMatch(newChild, potentialSoftMatch)) {
|
||||
return potentialSoftMatch;
|
||||
}
|
||||
|
||||
if (isSoftMatch(nextSibling, potentialSoftMatch)) {
|
||||
// the next new node has a soft match with this node, so
|
||||
// increment the count of future soft matches
|
||||
siblingSoftMatchCount++;
|
||||
nextSibling = nextSibling.nextSibling;
|
||||
|
||||
// If there are two future soft matches, bail to allow the siblings to soft match
|
||||
// so that we don't consume future soft matches for the sake of the current node
|
||||
if (siblingSoftMatchCount >= 2) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// advanced to the next old content child
|
||||
potentialSoftMatch = potentialSoftMatch.nextSibling;
|
||||
}
|
||||
|
||||
return potentialSoftMatch;
|
||||
}
|
||||
|
||||
function parseContent(newContent) {
|
||||
let parser = new DOMParser();
|
||||
|
||||
// remove svgs to avoid false-positive matches on head, etc.
|
||||
let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
|
||||
|
||||
// if the newContent contains a html, head or body tag, we can simply parse it w/o wrapping
|
||||
if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) {
|
||||
let content = parser.parseFromString(newContent, "text/html");
|
||||
// if it is a full HTML document, return the document itself as the parent container
|
||||
if (contentWithSvgsRemoved.match(/<\/html>/)) {
|
||||
content.generatedByIdiomorph = true;
|
||||
return content;
|
||||
} else {
|
||||
// otherwise return the html element as the parent container
|
||||
let htmlElement = content.firstChild;
|
||||
if (htmlElement) {
|
||||
htmlElement.generatedByIdiomorph = true;
|
||||
return htmlElement;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if it is partial HTML, wrap it in a template tag to provide a parent element and also to help
|
||||
// deal with touchy tags like tr, tbody, etc.
|
||||
let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html");
|
||||
let content = responseDoc.body.querySelector('template').content;
|
||||
content.generatedByIdiomorph = true;
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeContent(newContent) {
|
||||
if (newContent == null) {
|
||||
// noinspection UnnecessaryLocalVariableJS
|
||||
const dummyParent = document.createElement('div');
|
||||
return dummyParent;
|
||||
} else if (newContent.generatedByIdiomorph) {
|
||||
// the template tag created by idiomorph parsing can serve as a dummy parent
|
||||
return newContent;
|
||||
} else if (newContent instanceof Node) {
|
||||
// a single node is added as a child to a dummy parent
|
||||
const dummyParent = document.createElement('div');
|
||||
dummyParent.append(newContent);
|
||||
return dummyParent;
|
||||
} else {
|
||||
// all nodes in the array or HTMLElement collection are consolidated under
|
||||
// a single dummy parent element
|
||||
const dummyParent = document.createElement('div');
|
||||
for (const elt of [...newContent]) {
|
||||
dummyParent.append(elt);
|
||||
}
|
||||
return dummyParent;
|
||||
}
|
||||
}
|
||||
|
||||
function insertSiblings(previousSibling, morphedNode, nextSibling) {
|
||||
let stack = []
|
||||
let added = []
|
||||
while (previousSibling != null) {
|
||||
stack.push(previousSibling);
|
||||
previousSibling = previousSibling.previousSibling;
|
||||
}
|
||||
while (stack.length > 0) {
|
||||
let node = stack.pop();
|
||||
added.push(node); // push added preceding siblings on in order and insert
|
||||
morphedNode.parentElement.insertBefore(node, morphedNode);
|
||||
}
|
||||
added.push(morphedNode);
|
||||
while (nextSibling != null) {
|
||||
stack.push(nextSibling);
|
||||
added.push(nextSibling); // here we are going in order, so push on as we scan, rather than add
|
||||
nextSibling = nextSibling.nextSibling;
|
||||
}
|
||||
while (stack.length > 0) {
|
||||
morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling);
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
||||
function findBestNodeMatch(newContent, oldNode, ctx) {
|
||||
let currentElement;
|
||||
currentElement = newContent.firstChild;
|
||||
let bestElement = currentElement;
|
||||
let score = 0;
|
||||
while (currentElement) {
|
||||
let newScore = scoreElement(currentElement, oldNode, ctx);
|
||||
if (newScore > score) {
|
||||
bestElement = currentElement;
|
||||
score = newScore;
|
||||
}
|
||||
currentElement = currentElement.nextSibling;
|
||||
}
|
||||
return bestElement;
|
||||
}
|
||||
|
||||
function scoreElement(node1, node2, ctx) {
|
||||
if (isSoftMatch(node1, node2)) {
|
||||
return .5 + getIdIntersectionCount(ctx, node1, node2);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function removeNode(tempNode, ctx) {
|
||||
removeIdsFromConsideration(ctx, tempNode)
|
||||
if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return;
|
||||
|
||||
tempNode.remove();
|
||||
ctx.callbacks.afterNodeRemoved(tempNode);
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// ID Set Functions
|
||||
//=============================================================================
|
||||
|
||||
function isIdInConsideration(ctx, id) {
|
||||
return !ctx.deadIds.has(id);
|
||||
}
|
||||
|
||||
function idIsWithinNode(ctx, id, targetNode) {
|
||||
let idSet = ctx.idMap.get(targetNode) || EMPTY_SET;
|
||||
return idSet.has(id);
|
||||
}
|
||||
|
||||
function removeIdsFromConsideration(ctx, node) {
|
||||
let idSet = ctx.idMap.get(node) || EMPTY_SET;
|
||||
for (const id of idSet) {
|
||||
ctx.deadIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
function getIdIntersectionCount(ctx, node1, node2) {
|
||||
let sourceSet = ctx.idMap.get(node1) || EMPTY_SET;
|
||||
let matchCount = 0;
|
||||
for (const id of sourceSet) {
|
||||
// a potential match is an id in the source and potentialIdsSet, but
|
||||
// that has not already been merged into the DOM
|
||||
if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) {
|
||||
++matchCount;
|
||||
}
|
||||
}
|
||||
return matchCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* A bottom up algorithm that finds all elements with ids inside of the node
|
||||
* argument and populates id sets for those nodes and all their parents, generating
|
||||
* a set of ids contained within all nodes for the entire hierarchy in the DOM
|
||||
*
|
||||
* @param node {Element}
|
||||
* @param {Map<Node, Set<String>>} idMap
|
||||
*/
|
||||
function populateIdMapForNode(node, idMap) {
|
||||
let nodeParent = node.parentElement;
|
||||
// find all elements with an id property
|
||||
let idElements = node.querySelectorAll('[id]');
|
||||
for (const elt of idElements) {
|
||||
let current = elt;
|
||||
// walk up the parent hierarchy of that element, adding the id
|
||||
// of element to the parent's id set
|
||||
while (current !== nodeParent && current != null) {
|
||||
let idSet = idMap.get(current);
|
||||
// if the id set doesn't exist, create it and insert it in the map
|
||||
if (idSet == null) {
|
||||
idSet = new Set();
|
||||
idMap.set(current, idSet);
|
||||
}
|
||||
idSet.add(elt.id);
|
||||
current = current.parentElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function computes a map of nodes to all ids contained within that node (inclusive of the
|
||||
* node). This map can be used to ask if two nodes have intersecting sets of ids, which allows
|
||||
* for a looser definition of "matching" than tradition id matching, and allows child nodes
|
||||
* to contribute to a parent nodes matching.
|
||||
*
|
||||
* @param {Element} oldContent the old content that will be morphed
|
||||
* @param {Element} newContent the new content to morph to
|
||||
* @returns {Map<Node, Set<String>>} a map of nodes to id sets for the
|
||||
*/
|
||||
function createIdMap(oldContent, newContent) {
|
||||
let idMap = new Map();
|
||||
populateIdMapForNode(oldContent, idMap);
|
||||
populateIdMapForNode(newContent, idMap);
|
||||
return idMap;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// This is what ends up becoming the Idiomorph global object
|
||||
//=============================================================================
|
||||
return {
|
||||
morph,
|
||||
defaults
|
||||
}
|
||||
})();
|
||||
|
||||
export default Idiomorph
|
||||
34
framework/assets/js/package-lock.json
generated
34
framework/assets/js/package-lock.json
generated
|
|
@ -9,6 +9,9 @@
|
|||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@alpinejs/morph": "^3.14.1",
|
||||
"alpinejs": "^3.14.1",
|
||||
"diff-dom": "^5.1.4",
|
||||
"htmx.org": "~2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -33,6 +36,11 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@alpinejs/morph": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@alpinejs/morph/-/morph-3.14.1.tgz",
|
||||
"integrity": "sha512-6wJuI7QETygFhw0ZLvIZ5mAM082rbK++t4U624I4NagNjJHGiKKuPJBIw+oAYjHz7KT/tvp/Ng4PheEH+GY4eA=="
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz",
|
||||
|
|
@ -1043,6 +1051,27 @@
|
|||
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz",
|
||||
"integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==",
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz",
|
||||
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA=="
|
||||
},
|
||||
"node_modules/alpinejs": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.1.tgz",
|
||||
"integrity": "sha512-ICar8UsnRZAYvv/fCNfNeKMXNoXGUfwHrjx7LqXd08zIP95G2d9bAOuaL97re+1mgt/HojqHsfdOLo/A5LuWgQ==",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "~3.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||
|
|
@ -1344,6 +1373,11 @@
|
|||
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/diff-dom": {
|
||||
"version": "5.1.4",
|
||||
"resolved": "https://registry.npmjs.org/diff-dom/-/diff-dom-5.1.4.tgz",
|
||||
"integrity": "sha512-TSEaVdVGictY1KHg7VpVw2nuM02YKC9C8/qBkGiCnkiAybVbu1zQTMj2/dnVLRO7Z62UsqzHGpXweiOj5/jaZg=="
|
||||
},
|
||||
"node_modules/dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@
|
|||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@alpinejs/morph": "^3.14.1",
|
||||
"alpinejs": "^3.14.1",
|
||||
"diff-dom": "^5.1.4",
|
||||
"htmx.org": "~2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
"moduleResolution": "node",
|
||||
"noEmit": true,
|
||||
"downlevelIteration": true,
|
||||
"allowJs": true,
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package h
|
|||
import "strings"
|
||||
|
||||
func BaseExtensions() string {
|
||||
extensions := []string{"path-deps", "response-targets", "mutation-error", "htmgo"}
|
||||
extensions := []string{"path-deps", "response-targets", "mutation-error", "htmgo", "diffdom-swap"}
|
||||
if IsDevelopment() {
|
||||
extensions = append(extensions, "livereload")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ func renderScripts(context *RenderContext) {
|
|||
}
|
||||
|
||||
func (a *AttributeR) Render(context *RenderContext) {
|
||||
context.builder.WriteString(fmt.Sprintf(`%s="%s"`, a.Name, a.Value))
|
||||
context.builder.WriteString(fmt.Sprintf(`%s='%s'`, a.Name, a.Value))
|
||||
}
|
||||
|
||||
func (t *TextContent) Render(context *RenderContext) {
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ func OobSwapWithSelector(ctx *RequestContext, selector string, content *Element,
|
|||
}
|
||||
|
||||
func outOfBandSwap(selector string, option ...SwapOption) Ren {
|
||||
swapType := hx.SwapTypeTrue
|
||||
swapType := "diffdom"
|
||||
|
||||
if len(option) > 0 {
|
||||
o := option[0]
|
||||
|
|
|
|||
|
|
@ -51,6 +51,10 @@ func Body(children ...Ren) *Element {
|
|||
return Tag("body", children...)
|
||||
}
|
||||
|
||||
func HtmxConfig(m map[string]string) Ren {
|
||||
return Meta("htmx-config", JsonSerialize(m))
|
||||
}
|
||||
|
||||
func Meta(name string, content string) Ren {
|
||||
return &Element{
|
||||
tag: "meta",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ func RootPage(children ...h.Ren) *h.Element {
|
|||
`),
|
||||
),
|
||||
h.Body(
|
||||
h.Attribute("hx-swap", "diffdom"),
|
||||
h.Class("bg-neutral-50 min-h-screen overflow-x-hidden"),
|
||||
partials.NavBar(false),
|
||||
h.Fragment(children...),
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ func NavBar(expanded bool) *h.Element {
|
|||
func MobileNav(expanded bool) *h.Element {
|
||||
return h.Nav(
|
||||
h.Id("mobile-nav"),
|
||||
h.Attribute("hx-swap", "diffdom"),
|
||||
h.Class("block sm:hidden bg-neutral-100 border border-b-slate-300 p-4 md:p-3"),
|
||||
h.Div(
|
||||
h.Class("max-w-[95%] md:max-w-prose mx-auto"),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ go 1.23.0
|
|||
require (
|
||||
entgo.io/ent v0.14.1
|
||||
github.com/labstack/echo/v4 v4.12.0
|
||||
github.com/maddalax/htmgo/framework v0.0.0-20240918150417-f0f979e3a293
|
||||
github.com/maddalax/htmgo/framework v0.0.0-20240922165341-9e44fb5d5226
|
||||
github.com/mattn/go-sqlite3 v1.14.16
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -37,11 +37,14 @@ github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0
|
|||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/maddalax/htmgo/framework v0.0.0-20240918150417-f0f979e3a293 h1:/VisQ3836jcJkZQDiZUXk9CdOUCu3AY9476/924OSTQ=
|
||||
github.com/maddalax/htmgo/framework v0.0.0-20240918150417-f0f979e3a293/go.mod h1:hH6EgyyjquAj9BWFRPcTnAB+dOVfLuO125/L8C3iilA=
|
||||
github.com/maddalax/htmgo/framework v0.0.0-20240922165341-9e44fb5d5226 h1:V+RmSiPcpCXpv/S/WfSLERqwxOpKfWmjyOrw7H8SUbs=
|
||||
github.com/maddalax/htmgo/framework v0.0.0-20240922165341-9e44fb5d5226/go.mod h1:TA7KCaKhurpXceQrzClJOHqRsUSd5NL64ZngSg+I3oc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
|
|
|
|||
|
|
@ -2,20 +2,11 @@ package base
|
|||
|
||||
import (
|
||||
"github.com/maddalax/htmgo/framework/h"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Extensions() string {
|
||||
extensions := []string{"path-deps", "response-targets", "mutation-error"}
|
||||
if h.IsDevelopment() {
|
||||
extensions = append(extensions, "livereload")
|
||||
}
|
||||
return strings.Join(extensions, ", ")
|
||||
}
|
||||
|
||||
func RootPage(children ...h.Ren) h.Ren {
|
||||
return h.Html(
|
||||
h.HxExtension(Extensions()),
|
||||
h.HxExtension(h.BaseExtensions()),
|
||||
h.Head(
|
||||
h.Link("/public/main.css", "stylesheet"),
|
||||
h.Script("/public/htmgo.js"),
|
||||
|
|
|
|||
|
|
@ -3,13 +3,12 @@ package pages
|
|||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/maddalax/htmgo/framework/h"
|
||||
"todolist/pages/base"
|
||||
"todolist/partials"
|
||||
)
|
||||
|
||||
func IndexPage(c echo.Context) *h.Page {
|
||||
return h.NewPage(h.Html(
|
||||
h.HxExtension(base.Extensions()),
|
||||
h.HxExtension(h.BaseExtensions()),
|
||||
h.Class("bg-red-200 flex flex-col items-center h-full w-full"),
|
||||
h.Head(
|
||||
h.Link("/public/main.css", "stylesheet"),
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ func List(list []*ent.Task, tab Tab) *h.Element {
|
|||
|
||||
func Task(task *ent.Task, editing bool) *h.Element {
|
||||
return h.Div(
|
||||
h.Attribute("key", task.ID.String()),
|
||||
h.Id(fmt.Sprintf("task-%s", task.ID.String())),
|
||||
h.ClassX("h-[80px] max-h-[80px] max-w-2xl flex items-center p-4 gap-4 cursor-pointer", h.ClassMap{
|
||||
"border border-b-slate-100": !editing,
|
||||
|
|
@ -260,10 +261,7 @@ func ToggleCompleted(ctx *h.RequestContext) *h.Partial {
|
|||
list, _ := service.List()
|
||||
|
||||
return h.SwapManyPartial(ctx,
|
||||
List(list, getActiveTab(ctx)),
|
||||
Footer(list, getActiveTab(ctx)),
|
||||
CompleteAllIcon(list),
|
||||
)
|
||||
List(list, getActiveTab(ctx)))
|
||||
}
|
||||
|
||||
func CompleteAll(ctx *h.RequestContext) *h.Partial {
|
||||
|
|
|
|||
Loading…
Reference in a new issue