@livenetworks/ashlar 1.5.1 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +189 -177
- package/js/COMPONENTS.md +55 -49
- package/js/index.js +22 -5
- package/js/ln-accordion/README.md +186 -137
- package/js/ln-accordion/ln-accordion.js +1 -1
- package/js/ln-ajax/README.md +73 -4
- package/js/ln-ajax/ln-ajax.js +1 -1
- package/js/ln-ajax/src/ln-ajax.js +13 -13
- package/js/ln-api-connector/README.md +261 -150
- package/js/ln-api-connector/ln-api-connector.js +1 -1
- package/js/ln-api-connector/src/ln-api-connector.js +368 -265
- package/js/ln-api-queue/README.md +219 -0
- package/js/ln-api-queue/ln-api-queue.js +1 -0
- package/js/ln-api-queue/src/ln-api-queue.js +320 -0
- package/js/ln-api-queue/src/queue-storage.js +455 -0
- package/js/ln-autoresize/README.md +47 -0
- package/js/ln-autoresize/ln-autoresize.js +1 -1
- package/js/ln-autosave/README.md +33 -1
- package/js/ln-autosave/ln-autosave.js +1 -1
- package/js/ln-autosave/src/ln-autosave.js +4 -4
- package/js/ln-circular-progress/README.md +27 -13
- package/js/ln-circular-progress/ln-circular-progress.js +1 -1
- package/js/ln-circular-progress/src/ln-circular-progress.js +18 -5
- package/js/ln-confirm/README.md +165 -86
- package/js/ln-confirm/_ln-confirm.scss +11 -0
- package/js/ln-confirm/ln-confirm.js +1 -1
- package/js/ln-confirm/src/ln-confirm.js +82 -42
- package/js/ln-core/README.md +896 -0
- package/js/ln-core/hash.js +92 -0
- package/js/ln-core/helpers.js +337 -8
- package/js/ln-core/index.js +4 -2
- package/js/ln-core/persist.js +29 -3
- package/js/ln-core/positioning.js +1 -35
- package/js/ln-core/window-cache.js +213 -0
- package/js/ln-couchdb-connector/README.md +177 -156
- package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -1
- package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +398 -348
- package/js/ln-data-coordinator/README.md +536 -165
- package/js/ln-data-coordinator/ln-data-coordinator.js +1 -1
- package/js/ln-data-coordinator/src/data-read-policy.js +18 -0
- package/js/ln-data-coordinator/src/ln-data-coordinator.js +1042 -249
- package/js/ln-data-coordinator/src/mutation-receipts.js +46 -0
- package/js/ln-data-store/README.md +241 -94
- package/js/ln-data-store/ln-data-store.js +1 -1
- package/js/ln-data-store/src/ln-data-store.js +694 -699
- package/js/ln-date/README.md +39 -1
- package/js/ln-date/ln-date-dev.scss +14 -0
- package/js/ln-date/ln-date.js +1 -1
- package/js/ln-date/src/ln-date.js +236 -178
- package/js/ln-debug/README.md +58 -0
- package/js/ln-debug/ln-debug.js +1 -0
- package/js/ln-debug/src/ln-debug.js +19 -0
- package/js/ln-dropdown/README.md +31 -8
- package/js/ln-dropdown/ln-dropdown.js +1 -1
- package/js/ln-dropdown/src/ln-dropdown.js +30 -13
- package/js/ln-editor/README.md +274 -0
- package/js/ln-editor/ln-editor.js +1 -0
- package/js/ln-editor/ln-editor.scss +43 -0
- package/js/ln-editor/src/ln-editor.js +619 -0
- package/js/ln-external-links/README.md +44 -5
- package/js/ln-external-links/ln-external-links.js +1 -1
- package/js/ln-fill/README.md +199 -0
- package/js/ln-fill/ln-fill.js +1 -0
- package/js/ln-fill/src/ln-fill.js +87 -0
- package/js/ln-filter/README.md +92 -2
- package/js/ln-filter/ln-filter-dev.scss +8 -0
- package/js/ln-filter/ln-filter.js +1 -1
- package/js/ln-filter/src/ln-filter.js +22 -7
- package/js/ln-form/README.md +212 -49
- package/js/ln-form/ln-form.js +1 -1
- package/js/ln-form/src/ln-form.js +99 -199
- package/js/ln-http/README.md +125 -89
- package/js/ln-http/ln-http.js +1 -1
- package/js/ln-http/src/ln-http.js +32 -13
- package/js/ln-icon/README.md +130 -0
- package/js/ln-icon/ln-icon.js +1 -0
- package/js/{ln-icons/src/ln-icons.js → ln-icon/src/ln-icon.js} +20 -15
- package/js/ln-link/README.md +47 -9
- package/js/ln-link/ln-link.js +1 -1
- package/js/ln-link/src/ln-link.js +22 -6
- package/js/ln-list/README.md +129 -0
- package/js/ln-list/ln-list.js +1 -0
- package/js/ln-list/ln-list.scss +28 -0
- package/js/ln-list/src/ln-list.js +1216 -0
- package/js/ln-modal/README.md +185 -154
- package/js/ln-modal/ln-modal-dev.scss +13 -0
- package/js/ln-modal/ln-modal.js +1 -1
- package/js/ln-modal/ln-modal.scss +28 -4
- package/js/ln-modal/src/ln-modal.js +137 -201
- package/js/ln-modal-coordinator/README.md +99 -0
- package/js/ln-modal-coordinator/ln-modal-coordinator-dev.scss +8 -0
- package/js/ln-modal-coordinator/ln-modal-coordinator.js +1 -0
- package/js/ln-modal-coordinator/src/ln-modal-coordinator.js +345 -0
- package/js/ln-nav/README.md +46 -3
- package/js/ln-nav/ln-nav.js +1 -1
- package/js/ln-nav/src/ln-nav.js +92 -127
- package/js/ln-number/README.md +28 -1
- package/js/ln-number/ln-number-dev.scss +13 -0
- package/js/ln-number/ln-number.js +1 -1
- package/js/ln-number/src/ln-number.js +443 -350
- package/js/ln-options/README.md +66 -0
- package/js/ln-options/ln-options.js +1 -0
- package/js/ln-options/src/ln-options.js +74 -0
- package/js/ln-popover/README.md +40 -5
- package/js/ln-popover/ln-popover.js +1 -1
- package/js/ln-popover/src/ln-popover.js +38 -11
- package/js/ln-progress/README.md +413 -442
- package/js/ln-progress/ln-progress.js +1 -1
- package/js/ln-progress/src/ln-progress.js +98 -150
- package/js/ln-router/README.md +432 -0
- package/js/ln-router/ln-router.js +1 -0
- package/js/ln-router/src/ln-router.js +539 -0
- package/js/ln-search/README.md +158 -83
- package/js/ln-search/ln-search-dev.scss +8 -0
- package/js/ln-search/ln-search.js +1 -1
- package/js/ln-search/src/ln-search.js +12 -9
- package/js/ln-slug/README.md +110 -0
- package/js/ln-slug/ln-slug.js +1 -0
- package/js/ln-slug/src/ln-slug.js +74 -0
- package/js/ln-sort/README.md +170 -0
- package/js/ln-sort/ln-sort-dev.scss +8 -0
- package/js/ln-sort/ln-sort.js +1 -0
- package/js/ln-sort/src/ln-sort.js +178 -0
- package/js/ln-sortable/README.md +133 -95
- package/js/ln-sortable/ln-sortable.js +1 -1
- package/js/ln-stat/README.md +55 -0
- package/js/ln-stat/ln-stat.js +1 -0
- package/js/ln-stat/src/ln-stat.js +55 -0
- package/js/ln-table/README.md +285 -101
- package/js/ln-table/ln-table-dev.scss +17 -0
- package/js/ln-table/ln-table.js +1 -1
- package/js/ln-table/ln-table.scss +1 -1
- package/js/ln-table/src/ln-table.js +1493 -473
- package/js/ln-table-coordinator/README.md +73 -0
- package/js/ln-table-coordinator/ln-table-coordinator.js +1 -0
- package/js/ln-table-coordinator/src/ln-table-coordinator.js +160 -0
- package/js/ln-tabs/README.md +221 -137
- package/js/ln-tabs/ln-tabs-dev.scss +18 -0
- package/js/ln-tabs/ln-tabs.js +1 -1
- package/js/ln-tabs/src/ln-tabs.js +41 -24
- package/js/ln-time/README.md +14 -0
- package/js/ln-time/ln-time.js +1 -1
- package/js/ln-time/src/ln-time.js +223 -192
- package/js/ln-toast/README.md +178 -122
- package/js/ln-toast/ln-toast.js +1 -15
- package/js/ln-toast/src/ln-toast.js +232 -210
- package/js/ln-toast/template.html +16 -12
- package/js/ln-toggle/README.md +61 -2
- package/js/ln-toggle/ln-toggle-dev.scss +13 -0
- package/js/ln-toggle/ln-toggle.js +1 -1
- package/js/ln-toggle/src/ln-toggle.js +56 -43
- package/js/ln-tooltip/README.md +95 -58
- package/js/ln-tooltip/ln-tooltip-dev.scss +9 -0
- package/js/ln-tooltip/ln-tooltip.js +1 -1
- package/js/ln-tooltip/src/ln-tooltip.js +45 -6
- package/js/ln-translations/README.md +118 -96
- package/js/ln-translations/ln-translations.js +1 -1
- package/js/ln-translations/src/ln-translations.js +10 -5
- package/js/ln-upload/README.md +85 -10
- package/js/ln-upload/ln-upload.js +1 -1
- package/js/ln-upload/src/ln-upload.js +26 -10
- package/js/ln-validate/README.md +72 -0
- package/js/ln-validate/ln-validate-dev.scss +15 -0
- package/js/ln-validate/ln-validate.js +1 -1
- package/js/ln-validate/src/ln-validate.js +64 -0
- package/package.json +61 -56
- package/scss/base/_global.scss +2 -2
- package/scss/base/_typography.scss +1 -2
- package/scss/components/_date.scss +1 -0
- package/scss/components/_dropdown.scss +0 -5
- package/scss/components/_editor.scss +5 -0
- package/scss/components/_filter.scss +26 -25
- package/scss/components/_form.scss +7 -0
- package/scss/components/_ln-table.scss +13 -3
- package/scss/components/_sort.scss +10 -0
- package/scss/components/_table.scss +36 -23
- package/scss/components/_toast.scss +68 -41
- package/scss/components/_toggle.scss +16 -0
- package/scss/config/_density.scss +205 -117
- package/scss/config/_icons.scss +10 -9
- package/scss/config/_tokens.scss +69 -47
- package/scss/config/mixins/_accordion.scss +2 -7
- package/scss/config/mixins/_alert.scss +4 -4
- package/scss/config/mixins/_breadcrumbs.scss +2 -2
- package/scss/config/mixins/_btn.scss +1 -1
- package/scss/config/mixins/_card.scss +89 -14
- package/scss/config/mixins/_collapsible.scss +3 -0
- package/scss/config/mixins/_colors.scss +1 -1
- package/scss/config/mixins/_diagnostics.scss +11 -0
- package/scss/config/mixins/_display.scss +1 -0
- package/scss/config/mixins/_dropdown.scss +3 -5
- package/scss/config/mixins/_editor.scss +124 -0
- package/scss/config/mixins/_empty-state.scss +7 -0
- package/scss/config/mixins/_filter.scss +3 -5
- package/scss/config/mixins/_footer.scss +1 -2
- package/scss/config/mixins/_form.scss +207 -21
- package/scss/config/mixins/_index.scss +2 -0
- package/scss/config/mixins/_ln-table.scss +41 -2
- package/scss/config/mixins/_modal.scss +149 -66
- package/scss/config/mixins/_nav.scss +3 -4
- package/scss/config/mixins/_popover.scss +39 -0
- package/scss/config/mixins/_sidebar.scss +1 -1
- package/scss/config/mixins/_sort.scss +42 -0
- package/scss/config/mixins/_table.scss +85 -60
- package/scss/config/mixins/_toast.scss +179 -152
- package/scss/config/mixins/_tooltip.scss +17 -7
- package/scss/config/mixins/_typography.scss +4 -2
- package/scss/ln-ashlar-dev.scss +27 -9
- package/scss/ln-ashlar.scss +7 -1
- package/js/ln-data-table/README.md +0 -110
- package/js/ln-data-table/ln-data-table.js +0 -1
- package/js/ln-data-table/ln-data-table.scss +0 -10
- package/js/ln-data-table/src/ln-data-table.js +0 -1087
- package/js/ln-icons/README.md +0 -88
- package/js/ln-icons/ln-icons.js +0 -1
- package/js/ln-table/ln-table-sort.js +0 -1
- package/js/ln-table/src/ln-table-sort.js +0 -156
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(){"use strict";function
|
|
1
|
+
(function(){"use strict";if(typeof window<"u"){const t=console.warn;console.warn=function(...e){typeof e[0]=="string"&&(e[0].startsWith("[ln-")||e[0].startsWith("[lnCore"))&&!(document.documentElement.hasAttribute("data-ln-debug")||document.body&&document.body.hasAttribute("data-ln-debug"))||t.apply(console,e)}}const w={};function D(t,e){w[t]||(w[t]=document.querySelector('[data-ln-template="'+t+'"]'));const o=w[t];return o?o.content.cloneNode(!0):(console.warn("["+(e||"ln-core")+'] Template "'+t+'" not found'),null)}function x(t,e,o){t.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:o||{}}))}function L(t,e){if(!t||!e)return t;const o=t.querySelectorAll("[data-ln-field]");for(let n=0;n<o.length;n++){const i=o[n],l=i.getAttribute("data-ln-field");e[l]!=null&&(i.textContent=e[l])}const s=t.querySelectorAll("[data-ln-attr]");for(let n=0;n<s.length;n++){const i=s[n],l=i.getAttribute("data-ln-attr").split(",");for(let c=0;c<l.length;c++){const a=l[c].trim().split(":");if(a.length!==2)continue;const u=a[0].trim(),f=a[1].trim();e[f]!=null&&i.setAttribute(u,e[f])}}const d=t.querySelectorAll("[data-ln-show]");for(let n=0;n<d.length;n++){const i=d[n],l=i.getAttribute("data-ln-show");l in e&&i.classList.toggle("hidden",!e[l])}const r=t.querySelectorAll("[data-ln-class]");for(let n=0;n<r.length;n++){const i=r[n],l=i.getAttribute("data-ln-class").split(",");for(let c=0;c<l.length;c++){const a=l[c].trim().split(":");if(a.length!==2)continue;const u=a[0].trim(),f=a[1].trim();f in e&&i.classList.toggle(u,!!e[f])}}return t}function M(t,e){t.matches&&t.matches("[data-ln-form], [data-ln-fillable]")&&t.dispatchEvent(new CustomEvent("ln-fill",{detail:e??null,bubbles:!0}));const o=t.querySelectorAll("[data-ln-form], [data-ln-fillable]");for(let s=0;s<o.length;s++)o[s].dispatchEvent(new CustomEvent("ln-fill",{detail:e??null,bubbles:!0}));return t}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore._fillBound||(window.lnCore._fillBound=!0,document.addEventListener("ln-fill",function(t){if(!(!t.target.matches||!t.target.matches("[data-ln-fillable]")))if(t.detail)L(t.target,t.detail);else{const e=t.target.querySelectorAll("[data-ln-field]");for(let o=0;o<e.length;o++)e[o].textContent=""}})));function v(t,e){if(!t||!e)return t;const o=document.createTreeWalker(t,NodeFilter.SHOW_TEXT);for(;o.nextNode();){const r=o.currentNode;r.textContent.indexOf("{{")!==-1&&(r.textContent=r.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(n,i){return e[i]!==void 0?e[i]:""}))}const s=function(r,n){return e[n]!==void 0?e[n]:""},d=Array.from(t.querySelectorAll("*"));t.nodeType===1&&d.push(t);for(let r=0;r<d.length;r++){const n=d[r],i=n.attributes;for(let l=0;l<i.length;l++){const c=i[l];c.value.indexOf("{{")!==-1&&n.setAttribute(c.name,c.value.replace(/\{\{\s*(\w+)\s*\}\}/g,s))}}return t}function N(t,e,o,s,d,r){const n={};for(let l=0;l<t.children.length;l++){const c=t.children[l],a=c.getAttribute("data-ln-key");a&&(n[a]=c)}const i=document.createDocumentFragment();for(let l=0;l<e.length;l++){const c=e[l],a=String(s(c));let u=n[a];if(u)d(u,c,l);else{const f=D(o,r);if(!f||(v(f,c),u=f.firstElementChild,!u))continue;u.setAttribute("data-ln-key",a),d(u,c,l)}i.appendChild(u)}t.textContent="",t.appendChild(i)}function O(t,e){if(!document.body){document.addEventListener("DOMContentLoaded",function(){O(t,e)}),console.warn("["+e+'] Script loaded before <body> — add "defer" to your <script> tag');return}t()}function m(t,e,o,s){if(t.nodeType!==1)return;const r=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]",n=Array.from(t.querySelectorAll(r));t.matches&&t.matches(r)&&n.push(t);for(const i of n)i[o]||(i[o]=new s(i))}function j(t,e,o,s,d={}){const r=d.extraAttributes||[],n=d.onAttributeChange||null,i=d.onInit||null;function l(c){const a=c||document.body;m(a,t,e,o),i&&i(a)}return O(function(){const c=new MutationObserver(function(u){for(let f=0;f<u.length;f++){const h=u[f];if(h.type==="childList"){for(let g=0;g<h.addedNodes.length;g++){const p=h.addedNodes[g];p.nodeType===1&&(m(p,t,e,o),i&&i(p))}for(let g=0;g<h.removedNodes.length;g++){const p=h.removedNodes[g];if(p.nodeType===1){const _=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",b=Array.from(p.querySelectorAll(_));p.matches&&p.matches(_)&&b.push(p);for(let C=0;C<b.length;C++){const q=b[C];if(!document.contains(q)){const A=q[e];A&&typeof A.destroy=="function"&&A.destroy()}}}}}else h.type==="attributes"&&(n&&h.target[e]?n(h.target,h.attributeName):(m(h.target,t,e,o),i&&i(h.target)))}});let a=[];if(t.indexOf("[")!==-1){const u=/\[([\w-]+)/g;let f;for(;(f=u.exec(t))!==null;)a.push(f[1])}else a.push(t);c.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:a.concat(r)})},s),window[e]=l,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){l(document.body)}):l(document.body),l}const S={};function W(t,e){S[t]=e}function F(t){return S[t]||{ingress:e=>e,egress:e=>e}}const E={};function T(t,e){if(!t||typeof e!="object")return;const o=t.toLowerCase().split("-")[0];E[o]=e}function B(t){if(!t)return null;const e=t.toLowerCase().split("-")[0];return E[e]||null}T("mk",{monthsLong:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"],monthsShort:["јан","фев","мар","апр","мај","јун","јул","авг","септ","окт","ноем","дек"],daysLong:["недела","понеделник","вторник","среда","четврток","петок","сабота"],daysShort:["нед","пон","вт","ср","чет","пет","саб"]}),typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=W,window.lnCore.getDataMapper=F,window.lnCore.registerLocaleFallback=T,window.lnCore.getLocaleFallback=B,window.lnCore.fillTemplate=v,window.lnCore.fill=L,window.lnCore.lnFill=M,window.lnCore.renderList=N);function k(t){return(t||"").replace(/^#/,"")}function y(t){const e=t===void 0?location.hash:t,o={},s=k(e);if(!s)return o;const d=s.split("&");for(let r=0;r<d.length;r++){const n=d[r];if(!n)continue;const i=n.indexOf(":"),l=i>-1?n.slice(0,i):n,c=i>-1?n.slice(i+1):"";if(l)try{o[l]=decodeURIComponent(c)}catch{o[l]=c}}return o}function I(t){if(!t)return null;const e=y();return t in e?e[t]:null}function K(t,e){if(!t)return;const o=y();e==null?delete o[t]:o[t]=String(e);const d=Object.keys(o).map(function(r){const n=o[r];return n===""?r:r+":"+encodeURIComponent(n)}).join("&");k(location.hash)!==d&&(location.hash=d)}function R(t){return t.button===1||t.ctrlKey||t.metaKey||t.shiftKey?!1:(t.preventDefault(),!0)}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.hashParse=y,window.lnCore.hashGet=I,window.lnCore.hashSet=K,window.lnCore.hashLinkClick=R),(function(){const t="data-ln-accordion",e="lnAccordion";if(window[e]!==void 0)return;function o(s){return this.dom=s,this._onToggleOpen=function(d){if(d.detail.target.closest("[data-ln-accordion]")!==s)return;const r=s.querySelectorAll("[data-ln-toggle]");for(const n of r)n!==d.detail.target&&n.closest("[data-ln-accordion]")===s&&n.getAttribute("data-ln-toggle")==="open"&&n.setAttribute("data-ln-toggle","close");x(s,"ln-accordion:change",{target:d.detail.target})},s.addEventListener("ln-toggle:open",this._onToggleOpen),this}o.prototype.destroy=function(){this.dom[e]&&(this.dom.removeEventListener("ln-toggle:open",this._onToggleOpen),x(this.dom,"ln-accordion:destroyed",{target:this.dom}),delete this.dom[e])},j(t,e,o,"ln-accordion")})()})();
|
package/js/ln-ajax/README.md
CHANGED
|
@@ -40,10 +40,15 @@ Wrap interactive elements or entire layouts with the `data-ln-ajax` selector.
|
|
|
40
40
|
|
|
41
41
|
### HTML Attributes
|
|
42
42
|
|
|
43
|
-
| Attribute | Elements | Description |
|
|
44
|
-
| :--- | :--- | :--- |
|
|
45
|
-
| `data-ln-ajax` | Container, `<a>`, `<form>` | Activates AJAX capture on the element and its descendants. |
|
|
46
|
-
| `data-ln-ajax="false"` | `<a>`, `<form>` | Excludes the specific link or form from AJAX interception. |
|
|
43
|
+
| Attribute | Elements | Values | Description |
|
|
44
|
+
| :--- | :--- | :--- | :--- |
|
|
45
|
+
| `data-ln-ajax` | Container, `<a>`, `<form>` | *none* (empty attribute) | Activates AJAX capture on the element and its descendants. Acts as a boolean flag. |
|
|
46
|
+
| `data-ln-ajax="false"` | `<a>`, `<form>` | `"false"` | **The only supported value.** Excludes the specific link or form from AJAX interception. |
|
|
47
|
+
|
|
48
|
+
> [!IMPORTANT]
|
|
49
|
+
> **Attribute Value Restriction:** The `data-ln-ajax` attribute operates as a presence-based trigger (like a boolean flag).
|
|
50
|
+
> * **The only valid and supported value is `"false"`**, which is used to exclude/opt-out specific elements inside an AJAX-enabled container.
|
|
51
|
+
> * **Do not use custom values** (e.g., `data-ln-ajax="loginform"` or `data-ln-ajax="my-widget"`). The component does **not** accept, process, or assign behavior based on other string values. Using custom values will simply activate normal AJAX routing (acting the same as an empty attribute) but is a major anti-pattern that confuses both developers and AI agents.
|
|
47
52
|
|
|
48
53
|
### Server Response Protocol
|
|
49
54
|
|
|
@@ -89,3 +94,67 @@ All events are dispatched on the initiating element (`<a>` or `<form>`) and bubb
|
|
|
89
94
|
- **Missing DOM IDs on Swap Targets:** If the server returns a key in `content` that does not match a mounting ID in the active document (e.g. `id="main-content"`), that segment swap fails silently.
|
|
90
95
|
- **Forgetting CSRF Meta:** `ln-ajax` automatically reads `<meta name="csrf-token" content="...">` to inject the `X-CSRF-TOKEN` header on non-GET calls. If this meta tag is missing, POST/PUT requests may fail authentication.
|
|
91
96
|
- **Breaking External Links:** Links with different hostnames are ignored automatically, but absolute paths on the same host are captured. Ensure assets/downloads use `data-ln-ajax="false"`.
|
|
97
|
+
- **Forms carrying `data-ln-form-scope` are skipped entirely** (one-time `console.warn`) — the `ln-data-coordinator` write pipeline takes precedence over ajax progressive enhancement.
|
|
98
|
+
- **Respects `e.defaultPrevented`.** If a prior `submit` listener on the same form (e.g. a validation gate) already called `preventDefault()`, `ln-ajax` does nothing — no fetch, no loader class.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 🔧 Internals
|
|
103
|
+
|
|
104
|
+
Source: `js/ln-ajax/ln-ajax.js`.
|
|
105
|
+
|
|
106
|
+
### Request lifecycle
|
|
107
|
+
|
|
108
|
+
In order, per intercepted click/submit:
|
|
109
|
+
|
|
110
|
+
1. **Intercept** — click on `<a>` or submit on `<form>`. Modifier-key clicks (Ctrl/Cmd/middle) and `#`-href links pass through untouched.
|
|
111
|
+
2. **External-hostname skip** — if the link's hostname differs from `location.hostname`, ln-ajax aborts and the browser follows the link normally (no opt-out needed).
|
|
112
|
+
3. **`ln-ajax:before-start`** — cancelable; `preventDefault()` stops here (no spinner, no fetch).
|
|
113
|
+
4. **Spinner mount** — `.ln-ajax--loading` on the trigger, `<span class="ln-ajax-spinner">` appended, form buttons disabled.
|
|
114
|
+
5. **`ln-ajax:start`** — non-cancelable; fetch is about to begin.
|
|
115
|
+
6. **`fetch()`** — built from `href`/`action`, `method`, `FormData`. Headers always include `X-Requested-With`, `Accept: application/json`, and `X-CSRF-TOKEN` from the page meta. A `<input name="_method">` (auto-added by `ln-form`'s RESTful mode) rides in `FormData` transparently — the HTTP verb stays POST while `_method` travels in the body.
|
|
116
|
+
7. **Response** — see the branches below.
|
|
117
|
+
8. **`ln-ajax:success` / `ln-ajax:error`** — dispatched on the trigger, bubbling.
|
|
118
|
+
9. **Cleanup** — loading class + spinner removed, buttons re-enabled.
|
|
119
|
+
10. **`ln-ajax:complete`** — always fires last, success or error.
|
|
120
|
+
|
|
121
|
+
Response branches:
|
|
122
|
+
- **HTTP error** (`!response.ok`): parse JSON, dispatch `ln-ajax:error` `{ method, url, status, data }`, auto-toast if `data.message`.
|
|
123
|
+
- **Fetch rejection** (network / JSON parse): dispatch `ln-ajax:error` `{ method, url, error }`.
|
|
124
|
+
- **Success**: update `document.title`, swap `target.innerHTML` for each `data.content[id]`, auto-toast if `data.message`, `history.pushState` for `<a>` and GET `<form>`.
|
|
125
|
+
|
|
126
|
+
### MutationObserver flow
|
|
127
|
+
|
|
128
|
+
A single observer on `document.body` handles three cases:
|
|
129
|
+
- **New `data-ln-ajax` root injected** → `new lnAjax(node)` attaches listeners (covers AJAX-swapped frames and `innerHTML` writes).
|
|
130
|
+
- **Elements injected inside a live root** → `findElements` re-runs on the subtree and attaches to any new `<a>`/`<form>` — no full re-init of the root.
|
|
131
|
+
- **`data-ln-ajax` set on an existing element** → the attributes branch runs `new lnAjax(node)`, so programmatic opt-in works.
|
|
132
|
+
|
|
133
|
+
### DOM mutations performed
|
|
134
|
+
|
|
135
|
+
| Phase | Mutation |
|
|
136
|
+
|-------|----------|
|
|
137
|
+
| Request start | `.ln-ajax--loading` on trigger |
|
|
138
|
+
| Request start | `<span class="ln-ajax-spinner">` appended to trigger |
|
|
139
|
+
| Request start | `disabled` on all `<button>` descendants of a `<form>` trigger |
|
|
140
|
+
| Success | `target.innerHTML` replaced per `id` in `data.content` |
|
|
141
|
+
| Success | `document.title` updated from `data.title` |
|
|
142
|
+
| Success (`<a>` / GET `<form>`) | `history.pushState` |
|
|
143
|
+
| Completion | loading class + spinner removed, form buttons re-enabled |
|
|
144
|
+
|
|
145
|
+
### `findElements` local divergence
|
|
146
|
+
|
|
147
|
+
ln-ajax uses a local `findElements` rather than the ln-core helper of the same name: it needs a `{ links, forms }` partition (to bind `click` on links and `submit` on forms in one pass), where ln-core returns a flat list. The two must not be merged without updating all call sites.
|
|
148
|
+
|
|
149
|
+
### Trust boundary
|
|
150
|
+
|
|
151
|
+
ln-ajax operates under a trusted, same-origin model: it expects every HTML fragment in the JSON response to be safe. It performs **no** client-side sanitization or regex script/attribute filtering — that would be fragile and would corrupt valid markup (e.g. stripping `data-ln-confirm` values that contain event-like substrings). Sanitizing user-submitted HTML is the server's job, done before the fragment is rendered. See [Security §5 — AJAX Fragment Trust Boundary](../../docs/architecture/security.md#5-ajax-fragment-trust-boundary).
|
|
152
|
+
|
|
153
|
+
### Error detail shape
|
|
154
|
+
|
|
155
|
+
`ln-ajax:error` carries one of two shapes depending on failure mode:
|
|
156
|
+
|
|
157
|
+
- **HTTP-status error** (response received, `!ok`): `{ method, url, status, data }` — `data` is the parsed body (may contain `message`).
|
|
158
|
+
- **Fetch rejection** (network / DNS / JSON parse): `{ method, url, error }` — the caught `Error`, no `status`/`data`.
|
|
159
|
+
|
|
160
|
+
A single `ln-ajax:error` listener must guard with `'status' in e.detail` before reading `status`, and `'error' in e.detail` before reading `error`. (`ln-api-connector`, the data-layer transport tier, emits a single unified error shape — `{ action, error, status, data }` — and is the pattern to prefer for new data-flow code.)
|
package/js/ln-ajax/ln-ajax.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(){"use strict";function
|
|
1
|
+
(function(){"use strict";if(typeof window<"u"){const t=console.warn;console.warn=function(...e){typeof e[0]=="string"&&(e[0].startsWith("[ln-")||e[0].startsWith("[lnCore"))&&!(document.documentElement.hasAttribute("data-ln-debug")||document.body&&document.body.hasAttribute("data-ln-debug"))||t.apply(console,e)}}const S={};function N(t,e){S[t]||(S[t]=document.querySelector('[data-ln-template="'+t+'"]'));const o=S[t];return o?o.content.cloneNode(!0):(console.warn("["+(e||"ln-core")+'] Template "'+t+'" not found'),null)}function A(t,e,o){t.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:o||{}}))}function K(t,e,o){const u=new CustomEvent(e,{bubbles:!0,cancelable:!0,detail:o||{}});return t.dispatchEvent(u),u}function T(t,e){if(!t||!e)return t;const o=t.querySelectorAll("[data-ln-field]");for(let i=0;i<o.length;i++){const a=o[i],s=a.getAttribute("data-ln-field");e[s]!=null&&(a.textContent=e[s])}const u=t.querySelectorAll("[data-ln-attr]");for(let i=0;i<u.length;i++){const a=u[i],s=a.getAttribute("data-ln-attr").split(",");for(let d=0;d<s.length;d++){const h=s[d].trim().split(":");if(h.length!==2)continue;const r=h[0].trim(),n=h[1].trim();e[n]!=null&&a.setAttribute(r,e[n])}}const p=t.querySelectorAll("[data-ln-show]");for(let i=0;i<p.length;i++){const a=p[i],s=a.getAttribute("data-ln-show");s in e&&a.classList.toggle("hidden",!e[s])}const f=t.querySelectorAll("[data-ln-class]");for(let i=0;i<f.length;i++){const a=f[i],s=a.getAttribute("data-ln-class").split(",");for(let d=0;d<s.length;d++){const h=s[d].trim().split(":");if(h.length!==2)continue;const r=h[0].trim(),n=h[1].trim();n in e&&a.classList.toggle(r,!!e[n])}}return t}function U(t,e){t.matches&&t.matches("[data-ln-form], [data-ln-fillable]")&&t.dispatchEvent(new CustomEvent("ln-fill",{detail:e??null,bubbles:!0}));const o=t.querySelectorAll("[data-ln-form], [data-ln-fillable]");for(let u=0;u<o.length;u++)o[u].dispatchEvent(new CustomEvent("ln-fill",{detail:e??null,bubbles:!0}));return t}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore._fillBound||(window.lnCore._fillBound=!0,document.addEventListener("ln-fill",function(t){if(!(!t.target.matches||!t.target.matches("[data-ln-fillable]")))if(t.detail)T(t.target,t.detail);else{const e=t.target.querySelectorAll("[data-ln-field]");for(let o=0;o<e.length;o++)e[o].textContent=""}})));function v(t,e){if(!t||!e)return t;const o=document.createTreeWalker(t,NodeFilter.SHOW_TEXT);for(;o.nextNode();){const f=o.currentNode;f.textContent.indexOf("{{")!==-1&&(f.textContent=f.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(i,a){return e[a]!==void 0?e[a]:""}))}const u=function(f,i){return e[i]!==void 0?e[i]:""},p=Array.from(t.querySelectorAll("*"));t.nodeType===1&&p.push(t);for(let f=0;f<p.length;f++){const i=p[f],a=i.attributes;for(let s=0;s<a.length;s++){const d=a[s];d.value.indexOf("{{")!==-1&&i.setAttribute(d.name,d.value.replace(/\{\{\s*(\w+)\s*\}\}/g,u))}}return t}function B(t,e,o,u,p,f){const i={};for(let s=0;s<t.children.length;s++){const d=t.children[s],h=d.getAttribute("data-ln-key");h&&(i[h]=d)}const a=document.createDocumentFragment();for(let s=0;s<e.length;s++){const d=e[s],h=String(u(d));let r=i[h];if(r)p(r,d,s);else{const n=N(o,f);if(!n||(v(n,d),r=n.firstElementChild,!r))continue;r.setAttribute("data-ln-key",h),p(r,d,s)}a.appendChild(r)}t.textContent="",t.appendChild(a)}function _(t,e){if(!document.body){document.addEventListener("DOMContentLoaded",function(){_(t,e)}),console.warn("["+e+'] Script loaded before <body> — add "defer" to your <script> tag');return}t()}function G(t,e){if(t.ctrlKey||t.metaKey||t.shiftKey||t.altKey||t.button!==0||!e)return!1;const o=e.getAttribute("href");return!(!o||e.getAttribute("target")==="_blank"||e.hasAttribute("download")||o.startsWith("mailto:")||o.startsWith("tel:")||o==="#"||o.startsWith("#")||e.hostname&&e.hostname!==window.location.hostname)}const q={};function I(t,e){q[t]=e}function P(t){return q[t]||{ingress:e=>e,egress:e=>e}}const D={};function O(t,e){if(!t||typeof e!="object")return;const o=t.toLowerCase().split("-")[0];D[o]=e}function X(t){if(!t)return null;const e=t.toLowerCase().split("-")[0];return D[e]||null}O("mk",{monthsLong:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"],monthsShort:["јан","фев","мар","апр","мај","јун","јул","авг","септ","окт","ноем","дек"],daysLong:["недела","понеделник","вторник","среда","четврток","петок","сабота"],daysShort:["нед","пон","вт","ср","чет","пет","саб"]}),typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=I,window.lnCore.getDataMapper=P,window.lnCore.registerLocaleFallback=O,window.lnCore.getLocaleFallback=X,window.lnCore.fillTemplate=v,window.lnCore.fill=T,window.lnCore.lnFill=U,window.lnCore.renderList=B);function F(t){return(t||"").replace(/^#/,"")}function E(t){const e=t===void 0?location.hash:t,o={},u=F(e);if(!u)return o;const p=u.split("&");for(let f=0;f<p.length;f++){const i=p[f];if(!i)continue;const a=i.indexOf(":"),s=a>-1?i.slice(0,a):i,d=a>-1?i.slice(a+1):"";if(s)try{o[s]=decodeURIComponent(d)}catch{o[s]=d}}return o}function H(t){if(!t)return null;const e=E();return t in e?e[t]:null}function z(t,e){if(!t)return;const o=E();e==null?delete o[t]:o[t]=String(e);const p=Object.keys(o).map(function(f){const i=o[f];return i===""?f:f+":"+encodeURIComponent(i)}).join("&");F(location.hash)!==p&&(location.hash=p)}function R(t){return t.button===1||t.ctrlKey||t.metaKey||t.shiftKey?!1:(t.preventDefault(),!0)}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.hashParse=E,window.lnCore.hashGet=H,window.lnCore.hashSet=z,window.lnCore.hashLinkClick=R),(function(){const t="data-ln-ajax",e="lnAjax",o="data-ln-form-scope";if(window[e]!==void 0)return;function u(r){if(!r.hasAttribute(t)||r[e])return;r[e]=!0;const n=s(r);p(n.links),f(n.forms)}function p(r){for(const n of r){if(n[e+"Trigger"]||n.hostname&&n.hostname!==window.location.hostname)continue;const c=n.getAttribute("href");if(c&&c.includes("#"))continue;const l=function(b){if(!G(b,n))return;b.preventDefault();const y=n.getAttribute("href");y&&a("GET",y,null,n)};n.addEventListener("click",l),n[e+"Trigger"]=l}}function f(r){for(const n of r){if(n[e+"Trigger"])continue;if(n.hasAttribute(o)){n[e+"ScopeWarned"]||(n[e+"ScopeWarned"]=!0,console.warn("[ln-ajax] Form has data-ln-form-scope — the ln-data-coordinator write pipeline takes precedence; skipping ajax interception for this form."));continue}const c=function(l){if(l.defaultPrevented)return;l.preventDefault();const b=n.method.toUpperCase(),y=n.action,x=new FormData(n);for(const k of n.querySelectorAll('button, input[type="submit"]'))k.disabled=!0;a(b,y,x,n,function(){for(const k of n.querySelectorAll('button, input[type="submit"]'))k.disabled=!1})};n.addEventListener("submit",c),n[e+"Trigger"]=c}}function i(r){if(!r[e])return;const n=s(r);for(const c of n.links)c[e+"Trigger"]&&(c.removeEventListener("click",c[e+"Trigger"]),delete c[e+"Trigger"]);for(const c of n.forms)c[e+"Trigger"]&&(c.removeEventListener("submit",c[e+"Trigger"]),delete c[e+"Trigger"]);delete r[e]}function a(r,n,c,l,b){if(K(l,"ln-ajax:before-start",{method:r,url:n}).defaultPrevented)return;A(l,"ln-ajax:start",{method:r,url:n}),l.classList.add("ln-ajax--loading");const x=document.createElement("span");x.className="ln-ajax-spinner",l.appendChild(x);function k(){l.classList.remove("ln-ajax--loading");const g=l.querySelector(".ln-ajax-spinner");g&&g.remove(),b&&b()}let C=n;const M=document.querySelector('meta[name="csrf-token"]'),L=M?M.getAttribute("content"):null;c instanceof FormData&&L&&c.append("_token",L);const j={method:r,headers:{"X-Requested-With":"XMLHttpRequest",Accept:"application/json"}};if(L&&(j.headers["X-CSRF-TOKEN"]=L),r==="GET"&&c){const g=new URLSearchParams(c);C=n+(n.includes("?")?"&":"?")+g.toString()}else r!=="GET"&&c&&(j.body=c);fetch(C,j).then(function(g){const m=g.ok;return g.json().then(function(w){return{ok:m,status:g.status,data:w}})}).then(function(g){const m=g.data;if(g.ok){if(m.title&&(document.title=m.title),m.content)for(const w in m.content){const W=document.getElementById(w);W&&(W.innerHTML=m.content[w])}if(l.tagName==="A"){const w=l.getAttribute("href");w&&window.history.pushState({ajax:!0},"",w)}else l.tagName==="FORM"&&l.method.toUpperCase()==="GET"&&window.history.pushState({ajax:!0},"",C);A(l,"ln-ajax:success",{method:r,url:C,data:m})}else A(l,"ln-ajax:error",{method:r,url:C,status:g.status,data:m});if(m.message){const w=m.message;window.dispatchEvent(new CustomEvent("ln-toast:enqueue",{detail:{type:w.type||(g.ok?"success":"error"),title:w.title||"",message:w.body||""}}))}A(l,"ln-ajax:complete",{method:r,url:C}),k()}).catch(function(g){A(l,"ln-ajax:error",{method:r,url:C,error:g}),A(l,"ln-ajax:complete",{method:r,url:C}),k()})}function s(r){const n={links:[],forms:[]};return r.tagName==="A"&&r.getAttribute(t)!=="false"?n.links.push(r):r.tagName==="FORM"&&r.getAttribute(t)!=="false"?n.forms.push(r):(n.links=Array.from(r.querySelectorAll('a:not([data-ln-ajax="false"])')),n.forms=Array.from(r.querySelectorAll('form:not([data-ln-ajax="false"])'))),n}function d(){_(function(){new MutationObserver(function(n){for(const c of n)if(c.type==="childList"){for(const l of c.addedNodes)if(l.nodeType===1&&(u(l),!l.hasAttribute(t))){for(const y of l.querySelectorAll("["+t+"]"))u(y);const b=l.closest&&l.closest("["+t+"]");if(b&&b.getAttribute(t)!=="false"){const y=s(l);p(y.links),f(y.forms)}}}else c.type==="attributes"&&u(c.target)}).observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:[t]})},"ln-ajax")}function h(){for(const r of document.querySelectorAll("["+t+"]"))u(r)}window[e]=u,window[e].destroy=i,d(),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",h):h()})()})();
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { guardBody, dispatch, dispatchCancelable } from '../../ln-core';
|
|
1
|
+
import { guardBody, dispatch, dispatchCancelable, shouldInterceptLink } from '../../ln-core';
|
|
2
2
|
|
|
3
3
|
(function () {
|
|
4
4
|
const DOM_SELECTOR = 'data-ln-ajax';
|
|
5
5
|
const DOM_ATTRIBUTE = 'lnAjax';
|
|
6
|
+
const SCOPE_ATTR = 'data-ln-form-scope';
|
|
6
7
|
|
|
7
8
|
if (window[DOM_ATTRIBUTE] !== undefined) return;
|
|
8
9
|
|
|
@@ -27,7 +28,7 @@ import { guardBody, dispatch, dispatchCancelable } from '../../ln-core';
|
|
|
27
28
|
if (href && href.includes('#')) continue;
|
|
28
29
|
|
|
29
30
|
const handler = function (e) {
|
|
30
|
-
if (e
|
|
31
|
+
if (!shouldInterceptLink(e, link)) return;
|
|
31
32
|
|
|
32
33
|
e.preventDefault();
|
|
33
34
|
const url = link.getAttribute('href');
|
|
@@ -45,7 +46,16 @@ import { guardBody, dispatch, dispatchCancelable } from '../../ln-core';
|
|
|
45
46
|
for (const form of forms) {
|
|
46
47
|
if (form[DOM_ATTRIBUTE + 'Trigger']) continue;
|
|
47
48
|
|
|
49
|
+
if (form.hasAttribute(SCOPE_ATTR)) {
|
|
50
|
+
if (!form[DOM_ATTRIBUTE + 'ScopeWarned']) {
|
|
51
|
+
form[DOM_ATTRIBUTE + 'ScopeWarned'] = true;
|
|
52
|
+
console.warn('[ln-ajax] Form has data-ln-form-scope — the ln-data-coordinator write pipeline takes precedence; skipping ajax interception for this form.');
|
|
53
|
+
}
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
48
57
|
const handler = function (e) {
|
|
58
|
+
if (e.defaultPrevented) return; // a prior handler (e.g. a validation gate) already blocked this submit
|
|
49
59
|
e.preventDefault();
|
|
50
60
|
const method = form.method.toUpperCase();
|
|
51
61
|
const action = form.action;
|
|
@@ -152,17 +162,7 @@ import { guardBody, dispatch, dispatchCancelable } from '../../ln-core';
|
|
|
152
162
|
for (const targetId in data.content) {
|
|
153
163
|
const targetElement = document.getElementById(targetId);
|
|
154
164
|
if (targetElement) {
|
|
155
|
-
|
|
156
|
-
// Defense-in-depth: sanitize if DOMPurify is available globally, otherwise use native mitigation
|
|
157
|
-
if (window.DOMPurify && typeof window.DOMPurify.sanitize === 'function') {
|
|
158
|
-
htmlContent = window.DOMPurify.sanitize(htmlContent);
|
|
159
|
-
} else {
|
|
160
|
-
// Safe fallback: strip inline script tags and inline events (on*) to prevent basic XSS
|
|
161
|
-
htmlContent = htmlContent
|
|
162
|
-
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
|
163
|
-
.replace(/on\w+\s*=\s*(['"][^'"]*['"]|[^\s>]+)/gi, '');
|
|
164
|
-
}
|
|
165
|
-
targetElement.innerHTML = htmlContent;
|
|
165
|
+
targetElement.innerHTML = data.content[targetId];
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
}
|