@livenetworks/ashlar 1.5.1 → 1.6.0
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 +162 -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 +77 -41
- 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 d(o,n,
|
|
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 p={};function x(t,e){p[t]||(p[t]=document.querySelector('[data-ln-template="'+t+'"]'));const r=p[t];return r?r.content.cloneNode(!0):(console.warn("["+(e||"ln-core")+'] Template "'+t+'" not found'),null)}function m(t,e,r){t.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:r||{}}))}function g(t,e){if(!t||!e)return t;const r=t.querySelectorAll("[data-ln-field]");for(let l=0;l<r.length;l++){const o=r[l],n=o.getAttribute("data-ln-field");e[n]!=null&&(o.textContent=e[n])}const a=t.querySelectorAll("[data-ln-attr]");for(let l=0;l<a.length;l++){const o=a[l],n=o.getAttribute("data-ln-attr").split(",");for(let i=0;i<n.length;i++){const s=n[i].trim().split(":");if(s.length!==2)continue;const f=s[0].trim(),d=s[1].trim();e[d]!=null&&o.setAttribute(f,e[d])}}const u=t.querySelectorAll("[data-ln-show]");for(let l=0;l<u.length;l++){const o=u[l],n=o.getAttribute("data-ln-show");n in e&&o.classList.toggle("hidden",!e[n])}const c=t.querySelectorAll("[data-ln-class]");for(let l=0;l<c.length;l++){const o=c[l],n=o.getAttribute("data-ln-class").split(",");for(let i=0;i<n.length;i++){const s=n[i].trim().split(":");if(s.length!==2)continue;const f=s[0].trim(),d=s[1].trim();d in e&&o.classList.toggle(f,!!e[d])}}return t}function A(t,e){t.matches&&t.matches("[data-ln-form], [data-ln-fillable]")&&t.dispatchEvent(new CustomEvent("ln-fill",{detail:e??null,bubbles:!0}));const r=t.querySelectorAll("[data-ln-form], [data-ln-fillable]");for(let a=0;a<r.length;a++)r[a].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)g(t.target,t.detail);else{const e=t.target.querySelectorAll("[data-ln-field]");for(let r=0;r<e.length;r++)e[r].textContent=""}})));function b(t,e){if(!t||!e)return t;const r=document.createTreeWalker(t,NodeFilter.SHOW_TEXT);for(;r.nextNode();){const c=r.currentNode;c.textContent.indexOf("{{")!==-1&&(c.textContent=c.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(l,o){return e[o]!==void 0?e[o]:""}))}const a=function(c,l){return e[l]!==void 0?e[l]:""},u=Array.from(t.querySelectorAll("*"));t.nodeType===1&&u.push(t);for(let c=0;c<u.length;c++){const l=u[c],o=l.attributes;for(let n=0;n<o.length;n++){const i=o[n];i.value.indexOf("{{")!==-1&&l.setAttribute(i.name,i.value.replace(/\{\{\s*(\w+)\s*\}\}/g,a))}}return t}function S(t,e,r,a,u,c){const l={};for(let n=0;n<t.children.length;n++){const i=t.children[n],s=i.getAttribute("data-ln-key");s&&(l[s]=i)}const o=document.createDocumentFragment();for(let n=0;n<e.length;n++){const i=e[n],s=String(a(i));let f=l[s];if(f)u(f,i,n);else{const d=x(r,c);if(!d||(b(d,i),f=d.firstElementChild,!f))continue;f.setAttribute("data-ln-key",s),u(f,i,n)}o.appendChild(f)}t.textContent="",t.appendChild(o)}function h(t,e){if(!document.body){document.addEventListener("DOMContentLoaded",function(){h(t,e)}),console.warn("["+e+'] Script loaded before <body> — add "defer" to your <script> tag');return}t()}const y={};function v(t,e){y[t]=e}function _(t){return y[t]||{ingress:e=>e,egress:e=>e}}const C={};function L(t,e){if(!t||typeof e!="object")return;const r=t.toLowerCase().split("-")[0];C[r]=e}function E(t){if(!t)return null;const e=t.toLowerCase().split("-")[0];return C[e]||null}L("mk",{monthsLong:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"],monthsShort:["јан","фев","мар","апр","мај","јун","јул","авг","септ","окт","ноем","дек"],daysLong:["недела","понеделник","вторник","среда","четврток","петок","сабота"],daysShort:["нед","пон","вт","ср","чет","пет","саб"]}),typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=v,window.lnCore.getDataMapper=_,window.lnCore.registerLocaleFallback=L,window.lnCore.getLocaleFallback=E,window.lnCore.fillTemplate=b,window.lnCore.fill=g,window.lnCore.lnFill=A,window.lnCore.renderList=S);function k(t){return(t||"").replace(/^#/,"")}function w(t){const e=t===void 0?location.hash:t,r={},a=k(e);if(!a)return r;const u=a.split("&");for(let c=0;c<u.length;c++){const l=u[c];if(!l)continue;const o=l.indexOf(":"),n=o>-1?l.slice(0,o):l,i=o>-1?l.slice(o+1):"";if(n)try{r[n]=decodeURIComponent(i)}catch{r[n]=i}}return r}function T(t){if(!t)return null;const e=w();return t in e?e[t]:null}function q(t,e){if(!t)return;const r=w();e==null?delete r[t]:r[t]=String(e);const u=Object.keys(r).map(function(c){const l=r[c];return l===""?c:c+":"+encodeURIComponent(l)}).join("&");k(location.hash)!==u&&(location.hash=u)}function D(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=w,window.lnCore.hashGet=T,window.lnCore.hashSet=q,window.lnCore.hashLinkClick=D),(function(){const t="lnExternalLinks";if(window[t]!==void 0)return;function e(o){return o.hostname&&o.hostname!==window.location.hostname}function r(o){if(o.getAttribute("data-ln-external-link")==="processed"||!e(o))return;o.target="_blank";const n=(o.rel||"").split(/\s+/).filter(Boolean);n.includes("noopener")||n.push("noopener"),n.includes("noreferrer")||n.push("noreferrer"),o.rel=n.join(" ");const i=document.createElement("span");i.className="sr-only",i.textContent="(opens in new tab)",o.appendChild(i),o.setAttribute("data-ln-external-link","processed"),m(o,"ln-external-links:processed",{link:o,href:o.href})}function a(o){o=o||document.body;for(const n of o.querySelectorAll("a, area"))r(n)}function u(){h(function(){document.body.addEventListener("click",function(o){const n=o.target.closest("a, area");n&&n.getAttribute("data-ln-external-link")==="processed"&&m(n,"ln-external-links:clicked",{link:n,href:n.href,text:n.textContent||n.title||""})})},"ln-external-links")}function c(){h(function(){new MutationObserver(function(n){for(const i of n){if(i.type==="childList"){for(const s of i.addedNodes)if(s.nodeType===1&&(s.matches&&(s.matches("a")||s.matches("area"))&&r(s),s.querySelectorAll))for(const f of s.querySelectorAll("a, area"))r(f)}if(i.type==="attributes"&&i.attributeName==="href"){const s=i.target;s.matches&&(s.matches("a")||s.matches("area"))&&r(s)}}}).observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["href"]})},"ln-external-links")}function l(){u(),c(),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){a()}):a()}window[t]={process:a},l()})()})();
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# ln-fill
|
|
2
|
+
|
|
3
|
+
> Declarative click-triggered form/display fill. A document-level delegated
|
|
4
|
+
> click listener — not a per-element component. Composes with `data-ln-modal-for`
|
|
5
|
+
> on the same button.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Philosophy
|
|
10
|
+
|
|
11
|
+
`ln-fill` is the declarative trigger layer for filling forms and display
|
|
12
|
+
regions from a click. It converts flat `data-ln-fill-*` attributes on any
|
|
13
|
+
clickable element into a fan-out `lnFill()` call — no JS coordinator required
|
|
14
|
+
for the common case.
|
|
15
|
+
|
|
16
|
+
The underlying primitive is `window.lnCore.lnFill(container, record)`, which
|
|
17
|
+
dispatches `ln-fill` at every `[data-ln-form]` and `[data-ln-fillable]`
|
|
18
|
+
descendant. `ln-fill` is the module that wraps a click listener around that
|
|
19
|
+
primitive so you can drive it entirely from HTML.
|
|
20
|
+
|
|
21
|
+
### Why a dispatcher function, not a document broadcast
|
|
22
|
+
|
|
23
|
+
`ln-fill` events are addressed by **dispatch target**, not by payload
|
|
24
|
+
filtering. The DOM has no multicast — an event must be born at one node — so
|
|
25
|
+
there are two possible addressing models:
|
|
26
|
+
|
|
27
|
+
1. **Broadcast**: dispatch once at `document` with the target id in `detail`,
|
|
28
|
+
and every consumer subscribes globally and filters "is this for me?".
|
|
29
|
+
2. **Targeted dispatch** (ours): dispatch the event directly at the addressed
|
|
30
|
+
element — the DOM tree is the routing infrastructure, `e.target` IS the
|
|
31
|
+
address.
|
|
32
|
+
|
|
33
|
+
Targeted dispatch wins on three counts. Scaling cost is inverted: with
|
|
34
|
+
broadcast every fillable on the page pays a global filter on every fill
|
|
35
|
+
anywhere; with targeted dispatch only the addressed element's listener runs,
|
|
36
|
+
and its "digest only if addressed to me" guard is an identity check
|
|
37
|
+
(`e.target === self.dom` in ln-form), not a string comparison. Containment
|
|
38
|
+
addressing: `lnFill(modalEl, record)` fills everything *inside* the modal —
|
|
39
|
+
form plus display fillables — without the producer knowing a single id, which
|
|
40
|
+
a flat id-addressed broadcast cannot express. Single payload contract: the
|
|
41
|
+
`record ?? null` normalization (null = reset) and `bubbles: true` are defined
|
|
42
|
+
in one place, shared by every producer (the declarative click trigger,
|
|
43
|
+
`ln-modal-coordinator`, store-sync coordinators).
|
|
44
|
+
|
|
45
|
+
So `lnFill()` is not an imperative bypass of the event model — it IS the
|
|
46
|
+
event dispatcher. The targeted fan-out loop and the payload contract have to
|
|
47
|
+
live somewhere; the helper is that single canonical place.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 2. Minimal Blueprint
|
|
52
|
+
|
|
53
|
+
```html
|
|
54
|
+
<!-- Trigger: data-ln-fill-form points to the form id.
|
|
55
|
+
data-ln-fill-<key> attributes become the record. -->
|
|
56
|
+
<button
|
|
57
|
+
data-ln-fill-form="event-form"
|
|
58
|
+
data-ln-fill-event-id="42"
|
|
59
|
+
data-ln-fill-title="Annual Conference"
|
|
60
|
+
>Edit</button>
|
|
61
|
+
|
|
62
|
+
<!-- Target form -->
|
|
63
|
+
<form id="event-form" data-ln-form>
|
|
64
|
+
<input name="eventId" type="hidden">
|
|
65
|
+
<input name="title">
|
|
66
|
+
<button type="submit">Save</button>
|
|
67
|
+
</form>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
When the button is clicked:
|
|
71
|
+
1. `ln-fill` reads `dataset.lnFillForm` → locates `#event-form`.
|
|
72
|
+
2. Builds record `{ eventId: "42", title: "Annual Conference" }` from all
|
|
73
|
+
`data-ln-fill-*` keys (excluding the reserved `form` and `store` suffixes).
|
|
74
|
+
3. Calls `window.lnCore.lnFill(form, record)`.
|
|
75
|
+
4. `ln-form` receives `ln-fill` → calls `this.fill(record)`.
|
|
76
|
+
|
|
77
|
+
No payload attributes → `lnFill(form, null)` → form resets (calls `this.reset()`).
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 3. Attribute Reference
|
|
82
|
+
|
|
83
|
+
| Attribute | Placement | Description |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `data-ln-fill-form="<id>"` | Any clickable element | Required. ID of the target `<form>`. |
|
|
86
|
+
| `data-ln-fill-<key>="<value>"` | Same element | Becomes `record[camelKey]`. Kebab-case is camelCased by the browser dataset API: `data-ln-fill-event-id` → key `eventId`. |
|
|
87
|
+
|
|
88
|
+
**Reserved suffixes** (ignored, never put in the record):
|
|
89
|
+
- `form` (`data-ln-fill-form`) — the target form id
|
|
90
|
+
- `store` (`data-ln-fill-store`) — reserved for a future store-source seam
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 4. Composing with `data-ln-modal-for`
|
|
95
|
+
|
|
96
|
+
`ln-fill` intentionally does NOT call `e.preventDefault()`. A trigger may
|
|
97
|
+
carry both `data-ln-fill-form` and `data-ln-modal-for` — both document
|
|
98
|
+
listeners fire on the same click independently.
|
|
99
|
+
|
|
100
|
+
```html
|
|
101
|
+
<!-- One button fills the form AND opens the modal -->
|
|
102
|
+
<button
|
|
103
|
+
data-ln-modal-for="event-modal"
|
|
104
|
+
data-ln-fill-form="event-form"
|
|
105
|
+
data-ln-fill-event-id="42"
|
|
106
|
+
data-ln-fill-title="Annual Conference"
|
|
107
|
+
>Edit</button>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Order of side-effects per click (both listeners are on `document`):
|
|
111
|
+
1. `ln-modal` click listener → opens modal, sets `data-ln-modal-mode`.
|
|
112
|
+
2. `ln-fill` click listener → fills form via `lnFill(form, record)`.
|
|
113
|
+
|
|
114
|
+
### Double-Fill Prevention for Hash-Bound triggers
|
|
115
|
+
If a clickable trigger is an anchor link that points to a hash segment (i.e., its `href` attribute contains `#`, such as `<a href="#event-modal:42" ...>`), `ln-fill`'s click listener will **ignore** the click. The fill process is instead delegated entirely to the `ln-modal-coordinator`, which will map the resulting hash change and `ln-modal:open` event to an `ln-fill:request` event. This prevents redundant, parallel form-filling operations.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 5. Composing with ln-table row templates
|
|
120
|
+
|
|
121
|
+
Because `fillTemplate()` now interpolates `{{ key }}` in **element attribute
|
|
122
|
+
values** (not only text nodes), per-row trigger data stamps cleanly into
|
|
123
|
+
`data-ln-fill-*` attributes at clone time:
|
|
124
|
+
|
|
125
|
+
```html
|
|
126
|
+
<template data-ln-template="events-row">
|
|
127
|
+
<tr data-ln-table-row>
|
|
128
|
+
<td>{{ title }}</td>
|
|
129
|
+
<td>
|
|
130
|
+
<ul>
|
|
131
|
+
<li>
|
|
132
|
+
<button
|
|
133
|
+
data-ln-modal-for="event-modal"
|
|
134
|
+
data-ln-fill-form="event-form"
|
|
135
|
+
data-ln-fill-event-id="{{ id }}"
|
|
136
|
+
data-ln-fill-title="{{ title }}"
|
|
137
|
+
aria-label="Edit"
|
|
138
|
+
>
|
|
139
|
+
<svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-edit"></use></svg>
|
|
140
|
+
</button>
|
|
141
|
+
</li>
|
|
142
|
+
</ul>
|
|
143
|
+
</td>
|
|
144
|
+
</tr>
|
|
145
|
+
</template>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`{{ id }}` and `{{ title }}` are replaced by `fillTemplate()` when `ln-table`
|
|
149
|
+
clones the row. The resulting static attributes are then read by `ln-fill` on
|
|
150
|
+
click — no coordinator needed. This is the canonical pattern for table rows
|
|
151
|
+
that open a modal with pre-filled data.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## 6. When NOT to use `ln-fill` (use the coordinator instead)
|
|
156
|
+
|
|
157
|
+
`ln-fill` is click-triggered. Use `window.lnCore.lnFill(container, record)`
|
|
158
|
+
directly in a coordinator when the fill is programmatic and not driven by a
|
|
159
|
+
user click — for example, when a conflict-resolution handler writes a remote
|
|
160
|
+
record into the form:
|
|
161
|
+
|
|
162
|
+
```js
|
|
163
|
+
// Coordinator — store-driven fill after sync conflict
|
|
164
|
+
storeEl.addEventListener('ln-data-store:sync-conflict', function (e) {
|
|
165
|
+
window.lnCore.lnFill(modalEl, e.detail.serverRecord);
|
|
166
|
+
modalEl.dataset.lnModalMode = 'edit';
|
|
167
|
+
modalEl.setAttribute('data-ln-modal', 'open');
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Decision rule:
|
|
172
|
+
|
|
173
|
+
| Trigger | Use |
|
|
174
|
+
|---|---|
|
|
175
|
+
| User clicks a button or table row action | `data-ln-fill-form` + `data-ln-fill-*` (declarative) |
|
|
176
|
+
| Programmatic / store-event-driven | `window.lnCore.lnFill(container, record)` in coordinator |
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## 7. Integration & Source Files
|
|
181
|
+
|
|
182
|
+
- **Unified Bundle** — loaded automatically with the main bundle:
|
|
183
|
+
```html
|
|
184
|
+
<script src="dist/ln-ashlar.iife.js" defer></script>
|
|
185
|
+
```
|
|
186
|
+
- **Standalone IIFE** — for lightweight pages:
|
|
187
|
+
```html
|
|
188
|
+
<script src="js/ln-fill/ln-fill.js" defer></script>
|
|
189
|
+
```
|
|
190
|
+
- **Active Source (ESM)**: `js/ln-fill/src/ln-fill.js`
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Related
|
|
195
|
+
|
|
196
|
+
- **[`ln-form`](../ln-form/README.md)** — the fill target; `data-ln-fill-as` decoupled key.
|
|
197
|
+
- **[`ln-modal`](../ln-modal/README.md)** — `data-ln-modal-*` display namespace fill.
|
|
198
|
+
- **[`docs/architecture/data-flow.md §5.7`](../../docs/architecture/data-flow.md)** — `lnFill` primitive reference.
|
|
199
|
+
- **[`js/ln-core/README.md`](../ln-core/README.md)** — `lnFill`, `fillTemplate`, `populateForm` helpers.
|
|
@@ -0,0 +1 @@
|
|
|
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 S(t,e){w[t]||(w[t]=document.querySelector('[data-ln-template="'+t+'"]'));const i=w[t];return i?i.content.cloneNode(!0):(console.warn("["+(e||"ln-core")+'] Template "'+t+'" not found'),null)}function p(t,e){if(!t||!e)return t;const i=t.querySelectorAll("[data-ln-field]");for(let n=0;n<i.length;n++){const o=i[n],l=o.getAttribute("data-ln-field");e[l]!=null&&(o.textContent=e[l])}const a=t.querySelectorAll("[data-ln-attr]");for(let n=0;n<a.length;n++){const o=a[n],l=o.getAttribute("data-ln-attr").split(",");for(let s=0;s<l.length;s++){const u=l[s].trim().split(":");if(u.length!==2)continue;const f=u[0].trim(),d=u[1].trim();e[d]!=null&&o.setAttribute(f,e[d])}}const c=t.querySelectorAll("[data-ln-show]");for(let n=0;n<c.length;n++){const o=c[n],l=o.getAttribute("data-ln-show");l in e&&o.classList.toggle("hidden",!e[l])}const r=t.querySelectorAll("[data-ln-class]");for(let n=0;n<r.length;n++){const o=r[n],l=o.getAttribute("data-ln-class").split(",");for(let s=0;s<l.length;s++){const u=l[s].trim().split(":");if(u.length!==2)continue;const f=u[0].trim(),d=u[1].trim();d in e&&o.classList.toggle(f,!!e[d])}}return t}function A(t,e){t.matches&&t.matches("[data-ln-form], [data-ln-fillable]")&&t.dispatchEvent(new CustomEvent("ln-fill",{detail:e??null,bubbles:!0}));const i=t.querySelectorAll("[data-ln-form], [data-ln-fillable]");for(let a=0;a<i.length;a++)i[a].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)p(t.target,t.detail);else{const e=t.target.querySelectorAll("[data-ln-field]");for(let i=0;i<e.length;i++)e[i].textContent=""}})));function g(t,e){if(!t||!e)return t;const i=document.createTreeWalker(t,NodeFilter.SHOW_TEXT);for(;i.nextNode();){const r=i.currentNode;r.textContent.indexOf("{{")!==-1&&(r.textContent=r.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(n,o){return e[o]!==void 0?e[o]:""}))}const a=function(r,n){return e[n]!==void 0?e[n]:""},c=Array.from(t.querySelectorAll("*"));t.nodeType===1&&c.push(t);for(let r=0;r<c.length;r++){const n=c[r],o=n.attributes;for(let l=0;l<o.length;l++){const s=o[l];s.value.indexOf("{{")!==-1&&n.setAttribute(s.name,s.value.replace(/\{\{\s*(\w+)\s*\}\}/g,a))}}return t}function L(t,e,i,a,c,r){const n={};for(let l=0;l<t.children.length;l++){const s=t.children[l],u=s.getAttribute("data-ln-key");u&&(n[u]=s)}const o=document.createDocumentFragment();for(let l=0;l<e.length;l++){const s=e[l],u=String(a(s));let f=n[u];if(f)c(f,s,l);else{const d=S(i,r);if(!d||(g(d,s),f=d.firstElementChild,!f))continue;f.setAttribute("data-ln-key",u),c(f,s,l)}o.appendChild(f)}t.textContent="",t.appendChild(o)}const m={};function k(t,e){m[t]=e}function F(t){return m[t]||{ingress:e=>e,egress:e=>e}}const C={};function b(t,e){if(!t||typeof e!="object")return;const i=t.toLowerCase().split("-")[0];C[i]=e}function E(t){if(!t)return null;const e=t.toLowerCase().split("-")[0];return C[e]||null}b("mk",{monthsLong:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"],monthsShort:["јан","фев","мар","апр","мај","јун","јул","авг","септ","окт","ноем","дек"],daysLong:["недела","понеделник","вторник","среда","четврток","петок","сабота"],daysShort:["нед","пон","вт","ср","чет","пет","саб"]}),typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=k,window.lnCore.getDataMapper=F,window.lnCore.registerLocaleFallback=b,window.lnCore.getLocaleFallback=E,window.lnCore.fillTemplate=g,window.lnCore.fill=p,window.lnCore.lnFill=A,window.lnCore.renderList=L);function y(t){return(t||"").replace(/^#/,"")}function h(t){const e=t===void 0?location.hash:t,i={},a=y(e);if(!a)return i;const c=a.split("&");for(let r=0;r<c.length;r++){const n=c[r];if(!n)continue;const o=n.indexOf(":"),l=o>-1?n.slice(0,o):n,s=o>-1?n.slice(o+1):"";if(l)try{i[l]=decodeURIComponent(s)}catch{i[l]=s}}return i}function x(t){if(!t)return null;const e=h();return t in e?e[t]:null}function _(t,e){if(!t)return;const i=h();e==null?delete i[t]:i[t]=String(e);const c=Object.keys(i).map(function(r){const n=i[r];return n===""?r:r+":"+encodeURIComponent(n)}).join("&");y(location.hash)!==c&&(location.hash=c)}function T(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=h,window.lnCore.hashGet=x,window.lnCore.hashSet=_,window.lnCore.hashLinkClick=T),(function(){const t="lnFill";if(window[t]!==void 0)return;const e={lnFillForm:!0,lnFillStore:!0};function i(c){const r={},n=c.dataset;for(const o in n){if(!o.startsWith("lnFill")||e[o])continue;const l=o.slice(6);l&&(r[l.charAt(0).toLowerCase()+l.slice(1)]=n[o])}return r}function a(c,r){const n=window.CSS&&CSS.escape?CSS.escape(r):r,o=document.querySelectorAll('[data-ln-fill-id="'+n+'"]');if(o.length===0)return null;for(let l=0;l<o.length;l++){const s=o[l].getAttribute("data-ln-fill-form");if(s){const u=document.getElementById(s);if(u&&c.contains(u))return o[l]}}return o[0]}document.addEventListener("click",function(c){if(c.ctrlKey||c.metaKey||c.button===1)return;const r=c.target.closest("[data-ln-fill-form]");if(!r)return;const n=r.getAttribute("href");if(n&&n.indexOf("#")!==-1)return;const o=r.getAttribute("data-ln-fill-form"),l=document.getElementById(o);if(!l)return;const s=i(r),u=Object.keys(s).length>0;window.lnCore.lnFill(l,u?s:null)}),document.addEventListener("ln-fill:request",function(c){const r=c.detail;if(!r)return;const n=c.target,o=r.id;if(o==null){window.lnCore.lnFill(n,null);return}const l=a(n,o);if(!l)return;const s=i(l);window.lnCore.lnFill(n,s)}),window[t]=!0})()})();
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { } from '../../ln-core';
|
|
2
|
+
|
|
3
|
+
(function () {
|
|
4
|
+
const DOM_ATTRIBUTE = 'lnFill';
|
|
5
|
+
|
|
6
|
+
if (window[DOM_ATTRIBUTE] !== undefined) return;
|
|
7
|
+
|
|
8
|
+
// data-ln-fill-store is RESERVED for a future store-source seam.
|
|
9
|
+
// When implemented, the click handler will look up a record from a named
|
|
10
|
+
// store (e.g. window.lnCore stores) instead of reading flat data-* attributes.
|
|
11
|
+
const RESERVED = { lnFillForm: true, lnFillStore: true };
|
|
12
|
+
|
|
13
|
+
function _recordFrom(source) {
|
|
14
|
+
const record = {};
|
|
15
|
+
const ds = source.dataset;
|
|
16
|
+
for (const key in ds) {
|
|
17
|
+
if (!key.startsWith('lnFill')) continue;
|
|
18
|
+
if (RESERVED[key]) continue;
|
|
19
|
+
const suffix = key.slice(6); // strip 'lnFill'
|
|
20
|
+
if (!suffix) continue;
|
|
21
|
+
record[suffix.charAt(0).toLowerCase() + suffix.slice(1)] = ds[key];
|
|
22
|
+
}
|
|
23
|
+
return record;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function _findSource(container, param) {
|
|
27
|
+
const escaped = (window.CSS && CSS.escape) ? CSS.escape(param) : param;
|
|
28
|
+
const candidates = document.querySelectorAll('[data-ln-fill-id="' + escaped + '"]');
|
|
29
|
+
if (candidates.length === 0) return null;
|
|
30
|
+
// Prefer a source whose data-ln-fill-form resolves to a form INSIDE container
|
|
31
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
32
|
+
const formId = candidates[i].getAttribute('data-ln-fill-form');
|
|
33
|
+
if (formId) {
|
|
34
|
+
const form = document.getElementById(formId);
|
|
35
|
+
if (form && container.contains(form)) return candidates[i];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return candidates[0];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
document.addEventListener('click', function (e) {
|
|
42
|
+
if (e.ctrlKey || e.metaKey || e.button === 1) return;
|
|
43
|
+
|
|
44
|
+
const trigger = e.target.closest('[data-ln-fill-form]');
|
|
45
|
+
if (!trigger) return;
|
|
46
|
+
|
|
47
|
+
// Prevent double fill: if this trigger is a hash-bound anchor (e.g. href="#user-modal:142"),
|
|
48
|
+
// we skip the click-based fill. The resulting hash change will trigger ln-modal:open,
|
|
49
|
+
// and the ln-modal-coordinator will request the fill via ln-fill:request.
|
|
50
|
+
const href = trigger.getAttribute('href');
|
|
51
|
+
if (href && href.indexOf('#') !== -1) return;
|
|
52
|
+
|
|
53
|
+
// No e.preventDefault() — the same click may also be handled by
|
|
54
|
+
// [data-ln-modal-for] listener in ln-modal. Leave it alone.
|
|
55
|
+
|
|
56
|
+
const formId = trigger.getAttribute('data-ln-fill-form');
|
|
57
|
+
const form = document.getElementById(formId);
|
|
58
|
+
if (!form) return;
|
|
59
|
+
|
|
60
|
+
const record = _recordFrom(trigger);
|
|
61
|
+
const hasKeys = Object.keys(record).length > 0;
|
|
62
|
+
window.lnCore.lnFill(form, hasKeys ? record : null);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
document.addEventListener('ln-fill:request', function (e) {
|
|
66
|
+
const detail = e.detail;
|
|
67
|
+
if (!detail) return;
|
|
68
|
+
|
|
69
|
+
const target = e.target;
|
|
70
|
+
const param = detail.id;
|
|
71
|
+
|
|
72
|
+
if (param == null) {
|
|
73
|
+
window.lnCore.lnFill(target, null);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const source = _findSource(target, param);
|
|
78
|
+
if (!source) return;
|
|
79
|
+
|
|
80
|
+
const record = _recordFrom(source);
|
|
81
|
+
window.lnCore.lnFill(target, record);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Sentinel: boolean (not a constructor). Guards against double-binding
|
|
85
|
+
// across inlined standalone bundles.
|
|
86
|
+
window[DOM_ATTRIBUTE] = true;
|
|
87
|
+
})();
|
package/js/ln-filter/README.md
CHANGED
|
@@ -9,9 +9,15 @@ It filters target elements either by comparing child dataset attributes (for cus
|
|
|
9
9
|
## 🧭 Philosophy & Architecture
|
|
10
10
|
|
|
11
11
|
1. **Declarative State Model:** The component has no custom imperative state-change methods. Filter state is driven entirely by native checkboxes. External scripts update selections by writing `input.checked = true` and dispatching a standard bubbled `change` event.
|
|
12
|
-
2. **Sentinel
|
|
12
|
+
2. **Sentinel Rules:** The `data-ln-filter-reset` ("All") checkbox is kept in sync
|
|
13
|
+
automatically through three rules:
|
|
14
|
+
- **Check sentinel** → unchecks all value inputs (resets to All).
|
|
15
|
+
- **Uncheck last value** → re-checks the sentinel (never allows empty selection).
|
|
16
|
+
- **Check all values** → collapses to sentinel: unchecks all values, re-checks
|
|
17
|
+
sentinel. Guard: only applies when a reset sentinel exists in the list.
|
|
13
18
|
3. **Table Column & Auto-Population Mode:** By defining `data-ln-filter-col="N"`, the component filters plain HTML `<table>` rows by column cell text. When a `<template>` tag is nested inside, the component automatically populates value checkboxes from the column's unique values on page load.
|
|
14
19
|
4. **Local State Persistence:** Adding the `data-ln-persist` attribute saves active filter selections to `localStorage` under `lnf:{id}`, ensuring filter states survive page reloads and browser transitions.
|
|
20
|
+
5. **Decoupled `ln-table` Integration (Guard & Event Pattern):** When targeting an `ln-table` component (identified by the `data-ln-table` attribute on the target container), `ln-filter` acts purely as an event emitter. It detects the attribute and bypasses local DOM-based column option generation (`_populateFromColumn`) and DOM-based row hiding (`_filterTableRows`). This avoids interfering with `ln-table`'s internal rendering lifecycle, delegating all rendering and filtering to `ln-table` via the `ln-filter:changed` event.
|
|
15
21
|
|
|
16
22
|
---
|
|
17
23
|
|
|
@@ -88,6 +94,60 @@ Fired when the reset sentinel is activated.
|
|
|
88
94
|
|
|
89
95
|
---
|
|
90
96
|
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Table Column Filter with Popover
|
|
100
|
+
|
|
101
|
+
The canonical composition for `ln-table` per-column filters. `ln-filter` handles mutual exclusion and dispatches to the table by id — DOM position is irrelevant because dispatch uses `getElementById`.
|
|
102
|
+
|
|
103
|
+
```html
|
|
104
|
+
<!-- th: data-ln-table-filter-col maps the filter key to this column -->
|
|
105
|
+
<th data-ln-table-filter-col="department">
|
|
106
|
+
Department
|
|
107
|
+
<button class="table-filter" type="button"
|
|
108
|
+
data-ln-table-col-filter
|
|
109
|
+
data-ln-popover-for="filter-my-table-dept"
|
|
110
|
+
aria-label="Filter department">
|
|
111
|
+
<svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-filter"></use></svg>
|
|
112
|
+
</button>
|
|
113
|
+
</th>
|
|
114
|
+
|
|
115
|
+
<!-- Popover: sibling to [data-ln-table], NOT inside it -->
|
|
116
|
+
<!-- ln-popover promotes this to the top layer via showPopover() on open — no overflow clipping -->
|
|
117
|
+
<div data-ln-popover id="filter-my-table-dept">
|
|
118
|
+
<!-- search targets the OPTIONS UL id, not the table id -->
|
|
119
|
+
<input type="search" placeholder="Search..."
|
|
120
|
+
data-ln-search="filter-my-table-dept-list"
|
|
121
|
+
data-ln-search-items="label">
|
|
122
|
+
<ul id="filter-my-table-dept-list" data-ln-filter="my-table">
|
|
123
|
+
<li><label><input type="checkbox" data-ln-filter-key="department" data-ln-filter-reset checked> All</label></li>
|
|
124
|
+
<li><label><input type="checkbox" data-ln-filter-key="department" data-ln-filter-value="Engineering"> Engineering</label></li>
|
|
125
|
+
<li><label><input type="checkbox" data-ln-filter-key="department" data-ln-filter-value="Legal"> Legal</label></li>
|
|
126
|
+
</ul>
|
|
127
|
+
</div>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Attribute roles in this composition
|
|
131
|
+
|
|
132
|
+
| Attribute | Where | Purpose |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| `data-ln-filter-key="department"` | `<input type="checkbox">` | Column field name — must match `data-ln-table-filter-col` on `<th>` |
|
|
135
|
+
| `data-ln-filter-value="Engineering"` | `<input type="checkbox">` | Raw machine value — must match `data-ln-value` in the corresponding `<td>` |
|
|
136
|
+
| `data-ln-filter-reset` | `<input type="checkbox">` | Marks the "All" sentinel; checking it unchecks all value inputs |
|
|
137
|
+
| `data-ln-filter="my-table"` | `<ul>` | Targets the TABLE wrapper id — not the popover id |
|
|
138
|
+
|
|
139
|
+
### How dispatch reaches ln-table
|
|
140
|
+
|
|
141
|
+
`ln-filter` fires `ln-filter:changed` on both the filter container and on `getElementById(tableId)` (see `_dispatchOnBoth` in [ln-filter.js](file:///c:/laragon/www/ln-ashlar/js/ln-filter/src/ln-filter.js#L314-L320)). `ln-table` receives the event directly on its root element regardless of where in the DOM the filter markup lives.
|
|
142
|
+
|
|
143
|
+
Because `ln-filter` detects the `data-ln-table` attribute on the target container, it activates its internal safety guards to avoid conflicting with the table's state management:
|
|
144
|
+
- **`_populateFromColumn` (Option Generation):** Skips dynamically parsing HTML cells to generate checkbox options, as `ln-table` governs its own data columns.
|
|
145
|
+
- **`_filterTableRows` (Row Hiding):** Skips setting `data-ln-filter-hide="true"` on rows, avoiding collisions with `ln-table`'s rendering lifecycle (like pagination and virtual scrolling).
|
|
146
|
+
|
|
147
|
+
Instead, `ln-table` itself listens for the `ln-filter:changed` event on its root element (e.g., `dom.addEventListener('ln-filter:changed', this._onColumnFilter)`) and handles the actual filtration and display logic internally.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
91
151
|
## ⚠️ Common Pitfalls
|
|
92
152
|
|
|
93
153
|
- **Driving State Programmatically Without Events:** Changing `input.checked = true` using JavaScript does not trigger browser `change` listeners. You must explicitly dispatch the event:
|
|
@@ -96,4 +156,34 @@ Fired when the reset sentinel is activated.
|
|
|
96
156
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
97
157
|
```
|
|
98
158
|
- **Missing `id` on Persisted Filters:** The `data-ln-persist` storage key relies on the filter element's ID (e.g. `<nav id="my-filter" data-ln-persist>`). If the ID is missing, the component will fail to initialize persistence.
|
|
99
|
-
- **
|
|
159
|
+
- **Using `data-ln-filter-col` for ln-table column filters:** The `data-ln-filter-col` attribute (0-based column index for plain table row filtering) is for standalone `ln-filter` targeting a plain `<table>` — not for the `ln-table` component. When composing with `ln-table`, use `data-ln-filter="<tableId>"` on the `<ul>` and `data-ln-table-filter-col="<fieldName>"` on the `<th>`. `ln-table` receives `ln-filter:changed` and maps keys to columns via `data-ln-table-filter-col`.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## 🔧 Internals
|
|
164
|
+
|
|
165
|
+
Source: `js/ln-filter/ln-filter.js`. No JS-side state object — the active filter is derived on demand from checked inputs; `_lastSnapshot` is a diff cache only, used to decide whether to fire `ln-filter:changed`, never a source of truth.
|
|
166
|
+
|
|
167
|
+
### Render pipeline
|
|
168
|
+
|
|
169
|
+
change → mutual-exclusion rule (see Sentinel Rules above) → `queueRender()` (batched via `queueMicrotask`, dedupes same-tick calls) → `_render()` (derive + apply visibility) → `_afterRender()` (derive again, diff vs `_lastSnapshot`, dispatch, persist). Splitting render from afterRender keeps rendering free of side effects.
|
|
170
|
+
|
|
171
|
+
### Table-column filtering
|
|
172
|
+
|
|
173
|
+
When `data-ln-filter-col` is set (and the target is a plain `<table>`, not `[data-ln-table]`), a module-level `WeakMap<table, {key: {col, values}}>` tracks every active column filter per table — multiple `ln-filter` navs can filter the same table concurrently, each owning one key. Per row: AND across filter keys, OR within a key's values. WeakMap avoids leaks when tables are removed from the DOM.
|
|
174
|
+
|
|
175
|
+
### Auto-populate from column
|
|
176
|
+
|
|
177
|
+
Runs once at construction end (before `this.inputs` is collected) when a `<template>` is nested and `data-ln-filter-col` is set: scans the table's column cells, de-duplicates, sorts, clones the template per unique value via `fillTemplate(clone, { text })`, appends. Ordering matters — inputs are collected after population so the new checkboxes are wired.
|
|
178
|
+
|
|
179
|
+
### Dual dispatch
|
|
180
|
+
|
|
181
|
+
Every event fires on both the filter nav (`this.dom`) and `document.getElementById(targetId)` via `_dispatchOnBoth`. This is how a filter nested inside a `<th>` reaches `ln-table`'s root listener regardless of DOM position.
|
|
182
|
+
|
|
183
|
+
### Persistence
|
|
184
|
+
|
|
185
|
+
`data-ln-persist` stores `{ key, values }` (or `null` on reset) under `ln:filter:{pagePath}:{filterId}` — restored in the constructor *before* the DOM-init scan, so a persisted selection overrides any server-rendered `checked` attributes; the DOM-init render is skipped entirely when persistence restores state.
|
|
186
|
+
|
|
187
|
+
### Destroy
|
|
188
|
+
|
|
189
|
+
Removes this filter's key entry from the table-filter WeakMap (deleting the table's map entirely when it empties), removes all `change` listeners and their double-bind guard flags.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(){"use strict";function R(s,n,u){s.dispatchEvent(new CustomEvent(n,{bubbles:!0,detail:u||{}}))}function D(s,n){if(!s||!n)return s;const u=document.createTreeWalker(s,NodeFilter.SHOW_TEXT);for(;u.nextNode();){const c=u.currentNode;c.textContent.indexOf("{{")!==-1&&(c.textContent=c.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(m,h){return n[h]!==void 0?n[h]:""}))}return s}function B(s,n){if(!document.body){document.addEventListener("DOMContentLoaded",function(){B(s,n)}),console.warn("["+n+'] Script loaded before <body> — add "defer" to your <script> tag');return}s()}function S(s,n,u,c){if(s.nodeType!==1)return;const h=n.indexOf("[")!==-1||n.indexOf(".")!==-1||n.indexOf("#")!==-1?n:"["+n+"]",T=Array.from(s.querySelectorAll(h));s.matches&&s.matches(h)&&T.push(s);for(const y of T)y[u]||(y[u]=new c(y))}function F(s,n,u,c,m={}){const h=m.extraAttributes||[],T=m.onAttributeChange||null,y=m.onInit||null;function A(b){const _=b||document.body;S(_,s,n,u),y&&y(_)}return B(function(){const b=new MutationObserver(function(I){for(let w=0;w<I.length;w++){const f=I[w];if(f.type==="childList"){for(let t=0;t<f.addedNodes.length;t++){const e=f.addedNodes[t];e.nodeType===1&&(S(e,s,n,u),y&&y(e))}for(let t=0;t<f.removedNodes.length;t++){const e=f.removedNodes[t];if(e.nodeType===1){const r=s.indexOf("[")!==-1||s.indexOf(".")!==-1||s.indexOf("#")!==-1?s:"["+s+"]",o=Array.from(e.querySelectorAll(r));e.matches&&e.matches(r)&&o.push(e);for(let l=0;l<o.length;l++){const d=o[l];if(!document.contains(d)){const a=d[n];a&&typeof a.destroy=="function"&&a.destroy()}}}}}else f.type==="attributes"&&(T&&f.target[n]?T(f.target,f.attributeName):(S(f.target,s,n,u),y&&y(f.target)))}});let _=[];if(s.indexOf("[")!==-1){const I=/\[([\w-]+)/g;let w;for(;(w=I.exec(s))!==null;)_.push(w[1])}else _.push(s);b.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:_.concat(h)})},c),window[n]=A,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){A(document.body)}):A(document.body),A}const L={};function K(s,n){L[s]=n}function j(s){return L[s]||{ingress:n=>n,egress:n=>n}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=K,window.lnCore.getDataMapper=j);function H(s,n){let u=!1;return function(){u||(u=!0,queueMicrotask(function(){u=!1,s(),n&&n()}))}}const W="ln:";function J(){return location.pathname.replace(/\/+$/,"").toLowerCase()||"/"}function q(s,n){const u=n.getAttribute("data-ln-persist"),c=u!==null&&u!==""?u:n.id;return c?W+s+":"+J()+":"+c:(console.warn('[ln-persist] Element requires id or data-ln-persist="key"',n),null)}function U(s,n){const u=q(s,n);if(!u)return null;try{const c=localStorage.getItem(u);return c!==null?JSON.parse(c):null}catch{return null}}function N(s,n,u){const c=q(s,n);if(c)try{localStorage.setItem(c,JSON.stringify(u))}catch{}}(function(){const s="data-ln-filter",n="lnFilter",u="data-ln-filter-initialized",c="data-ln-filter-key",m="data-ln-filter-value",h="data-ln-filter-hide",T="data-ln-filter-reset",y="data-ln-filter-col",A=new WeakMap;if(window[n]!==void 0)return;function b(t){return t.hasAttribute(T)||t.getAttribute(m)===""}function _(t){let e=null;const i=[];for(let r=0;r<t.inputs.length;r++){const o=t.inputs[r];if(o.checked&&!b(o)){e===null&&(e=o.getAttribute(c));const l=o.getAttribute(m);l&&i.push(l)}}return{key:e,values:i}}function I(t,e){if(t.length!==e.length)return!0;for(let i=0;i<t.length;i++)if(t[i]!==e[i])return!0;return!1}function w(t){const e=t.dom,i=t.colIndex,r=e.querySelector("template");if(!r||i===null)return;const o=document.getElementById(t.targetId);if(!o)return;const l=o.tagName==="TABLE"?o:o.querySelector("table");if(!l||o.hasAttribute("data-ln-table"))return;const d={},a=[],x=l.tBodies;for(let g=0;g<x.length;g++){const p=x[g].rows;for(let v=0;v<p.length;v++){const O=p[v].cells[i],E=O?O.textContent.trim():"";E&&!d[E]&&(d[E]=!0,a.push(E))}}a.sort(function(g,p){return g.localeCompare(p)});const k=e.querySelector("["+c+"]"),C=k?k.getAttribute(c):e.getAttribute("data-ln-filter-key")||"col"+i;for(let g=0;g<a.length;g++){const p=r.content.cloneNode(!0),v=p.querySelector("input");v&&(v.setAttribute(c,C),v.setAttribute(m,a[g]),D(p,{text:a[g]}),e.appendChild(p))}}function f(t){if(t.hasAttribute(u))return this;this.dom=t,this.targetId=t.getAttribute(s);const e=t.getAttribute(y);this.colIndex=e!==null?parseInt(e,10):null,w(this),this.inputs=Array.from(t.querySelectorAll("["+c+"]")),this._filterKey=this.inputs.length>0?this.inputs[0].getAttribute(c):null,this._lastSnapshot=null;const i=this,r=H(function(){i._render()},function(){i._afterRender()});this._queueRender=r,this._attachHandlers();let o=!1;if(t.hasAttribute("data-ln-persist")){const l=U("filter",t);if(l&&l.key&&Array.isArray(l.values)&&l.values.length>0){for(let d=0;d<this.inputs.length;d++){const a=this.inputs[d];b(a)?a.checked=!1:a.getAttribute(c)===l.key&&l.values.indexOf(a.getAttribute(m))!==-1?a.checked=!0:a.checked=!1}r(),o=!0}}if(!o){for(let l=0;l<this.inputs.length;l++)if(this.inputs[l].checked&&!b(this.inputs[l])){r();break}}return t.setAttribute(u,""),this}f.prototype._attachHandlers=function(){const t=this;this.inputs.forEach(function(e){e[n+"Bound"]||(e[n+"Bound"]=!0,e._lnFilterChange=function(){if(b(e)){for(let i=0;i<t.inputs.length;i++)b(t.inputs[i])||(t.inputs[i].checked=!1);e.checked=!0,t._queueRender();return}if(e.checked)for(let i=0;i<t.inputs.length;i++)b(t.inputs[i])&&(t.inputs[i].checked=!1);else{let i=!1;for(let r=0;r<t.inputs.length;r++)if(!b(t.inputs[r])&&t.inputs[r].checked){i=!0;break}if(!i)for(let r=0;r<t.inputs.length;r++)b(t.inputs[r])&&(t.inputs[r].checked=!0)}t._queueRender()},e.addEventListener("change",e._lnFilterChange))})},f.prototype._render=function(){const t=this,e=_(this),i=e.key===null||e.values.length===0,r=[];for(let o=0;o<e.values.length;o++)r.push(e.values[o].toLowerCase());if(t.colIndex!==null)t._filterTableRows(e);else{const o=document.getElementById(t.targetId);if(!o)return;const l=o.children;for(let d=0;d<l.length;d++){const a=l[d];if(i){a.removeAttribute(h);continue}const x=a.getAttribute("data-"+e.key);a.removeAttribute(h),x!==null&&r.indexOf(x.toLowerCase())===-1&&a.setAttribute(h,"true")}}},f.prototype._afterRender=function(){const t=_(this),e=this._lastSnapshot;if(!e||e.key!==t.key||I(e.values,t.values)){this._dispatchOnBoth("ln-filter:changed",{key:t.key,values:t.values.slice()});const r=e&&e.values.length>0,o=t.values.length===0;r&&o&&this._dispatchOnBoth("ln-filter:reset",{}),this._lastSnapshot={key:t.key,values:t.values.slice()}}this.dom.hasAttribute("data-ln-persist")&&(t.key&&t.values.length>0?N("filter",this.dom,{key:t.key,values:t.values.slice()}):N("filter",this.dom,null))},f.prototype._dispatchOnBoth=function(t,e){R(this.dom,t,e);const i=document.getElementById(this.targetId);i&&i!==this.dom&&R(i,t,e)},f.prototype._filterTableRows=function(t){const e=document.getElementById(this.targetId);if(!e)return;const i=e.tagName==="TABLE"?e:e.querySelector("table");if(!i||e.hasAttribute("data-ln-table"))return;const r=t.key||this._filterKey,o=t.values;A.has(i)||A.set(i,{});const l=A.get(i);if(r&&o.length>0){const k=[];for(let C=0;C<o.length;C++)k.push(o[C].toLowerCase());l[r]={col:this.colIndex,values:k}}else r&&delete l[r];const d=Object.keys(l),a=d.length>0,x=i.tBodies;for(let k=0;k<x.length;k++){const C=x[k].rows;for(let g=0;g<C.length;g++){const p=C[g];if(!a){p.removeAttribute(h);continue}let v=!0;for(let O=0;O<d.length;O++){const E=l[d[O]],M=p.cells[E.col],V=M?M.textContent.trim().toLowerCase():"";if(E.values.indexOf(V)===-1){v=!1;break}}v?p.removeAttribute(h):p.setAttribute(h,"true")}}},f.prototype.destroy=function(){if(this.dom[n]){if(this.colIndex!==null){const t=document.getElementById(this.targetId);if(t){const e=t.tagName==="TABLE"?t:t.querySelector("table");if(e&&A.has(e)){const i=A.get(e),r=this._filterKey;r&&i[r]&&delete i[r],Object.keys(i).length===0&&A.delete(e)}}}this.inputs.forEach(function(t){t._lnFilterChange&&(t.removeEventListener("change",t._lnFilterChange),delete t._lnFilterChange),delete t[n+"Bound"]}),this.dom.removeAttribute(u),delete this.dom[n]}},F(s,n,f,"ln-filter")})()})();
|
|
1
|
+
(function(){"use strict";if(typeof window<"u"){const t=console.warn;console.warn=function(...n){typeof n[0]=="string"&&(n[0].startsWith("[ln-")||n[0].startsWith("[lnCore"))&&!(document.documentElement.hasAttribute("data-ln-debug")||document.body&&document.body.hasAttribute("data-ln-debug"))||t.apply(console,n)}}const I={};function J(t,n){I[t]||(I[t]=document.querySelector('[data-ln-template="'+t+'"]'));const l=I[t];return l?l.content.cloneNode(!0):(console.warn("["+(n||"ln-core")+'] Template "'+t+'" not found'),null)}function B(t,n,l){t.dispatchEvent(new CustomEvent(n,{bubbles:!0,detail:l||{}}))}function F(t,n){if(!t||!n)return t;const l=t.querySelectorAll("[data-ln-field]");for(let u=0;u<l.length;u++){const c=l[u],s=c.getAttribute("data-ln-field");n[s]!=null&&(c.textContent=n[s])}const f=t.querySelectorAll("[data-ln-attr]");for(let u=0;u<f.length;u++){const c=f[u],s=c.getAttribute("data-ln-attr").split(",");for(let d=0;d<s.length;d++){const m=s[d].trim().split(":");if(m.length!==2)continue;const w=m[0].trim(),y=m[1].trim();n[y]!=null&&c.setAttribute(w,n[y])}}const p=t.querySelectorAll("[data-ln-show]");for(let u=0;u<p.length;u++){const c=p[u],s=c.getAttribute("data-ln-show");s in n&&c.classList.toggle("hidden",!n[s])}const g=t.querySelectorAll("[data-ln-class]");for(let u=0;u<g.length;u++){const c=g[u],s=c.getAttribute("data-ln-class").split(",");for(let d=0;d<s.length;d++){const m=s[d].trim().split(":");if(m.length!==2)continue;const w=m[0].trim(),y=m[1].trim();y in n&&c.classList.toggle(w,!!n[y])}}return t}function P(t,n){t.matches&&t.matches("[data-ln-form], [data-ln-fillable]")&&t.dispatchEvent(new CustomEvent("ln-fill",{detail:n??null,bubbles:!0}));const l=t.querySelectorAll("[data-ln-form], [data-ln-fillable]");for(let f=0;f<l.length;f++)l[f].dispatchEvent(new CustomEvent("ln-fill",{detail:n??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)F(t.target,t.detail);else{const n=t.target.querySelectorAll("[data-ln-field]");for(let l=0;l<n.length;l++)n[l].textContent=""}})));function O(t,n){if(!t||!n)return t;const l=document.createTreeWalker(t,NodeFilter.SHOW_TEXT);for(;l.nextNode();){const g=l.currentNode;g.textContent.indexOf("{{")!==-1&&(g.textContent=g.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(u,c){return n[c]!==void 0?n[c]:""}))}const f=function(g,u){return n[u]!==void 0?n[u]:""},p=Array.from(t.querySelectorAll("*"));t.nodeType===1&&p.push(t);for(let g=0;g<p.length;g++){const u=p[g],c=u.attributes;for(let s=0;s<c.length;s++){const d=c[s];d.value.indexOf("{{")!==-1&&u.setAttribute(d.name,d.value.replace(/\{\{\s*(\w+)\s*\}\}/g,f))}}return t}function V(t,n,l,f,p,g){const u={};for(let s=0;s<t.children.length;s++){const d=t.children[s],m=d.getAttribute("data-ln-key");m&&(u[m]=d)}const c=document.createDocumentFragment();for(let s=0;s<n.length;s++){const d=n[s],m=String(f(d));let w=u[m];if(w)p(w,d,s);else{const y=J(l,g);if(!y||(O(y,d),w=y.firstElementChild,!w))continue;w.setAttribute("data-ln-key",m),p(w,d,s)}c.appendChild(w)}t.textContent="",t.appendChild(c)}function D(t,n){if(!document.body){document.addEventListener("DOMContentLoaded",function(){D(t,n)}),console.warn("["+n+'] Script loaded before <body> — add "defer" to your <script> tag');return}t()}function q(t,n,l,f){if(t.nodeType!==1)return;const g=n.indexOf("[")!==-1||n.indexOf(".")!==-1||n.indexOf("#")!==-1?n:"["+n+"]",u=Array.from(t.querySelectorAll(g));t.matches&&t.matches(g)&&u.push(t);for(const c of u)c[l]||(c[l]=new f(c))}function X(t,n,l,f,p={}){const g=p.extraAttributes||[],u=p.onAttributeChange||null,c=p.onInit||null;function s(d){const m=d||document.body;q(m,t,n,l),c&&c(m)}return D(function(){const d=new MutationObserver(function(w){for(let y=0;y<w.length;y++){const e=w[y];if(e.type==="childList"){for(let i=0;i<e.addedNodes.length;i++){const r=e.addedNodes[i];r.nodeType===1&&(q(r,t,n,l),c&&c(r))}for(let i=0;i<e.removedNodes.length;i++){const r=e.removedNodes[i];if(r.nodeType===1){const a=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",h=Array.from(r.querySelectorAll(a));r.matches&&r.matches(a)&&h.push(r);for(let A=0;A<h.length;A++){const b=h[A];if(!document.contains(b)){const v=b[n];v&&typeof v.destroy=="function"&&v.destroy()}}}}}else e.type==="attributes"&&(u&&e.target[n]?u(e.target,e.attributeName):(q(e.target,t,n,l),c&&c(e.target)))}});let m=[];if(t.indexOf("[")!==-1){const w=/\[([\w-]+)/g;let y;for(;(y=w.exec(t))!==null;)m.push(y[1])}else m.push(t);d.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:m.concat(g)})},f),window[n]=s,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){s(document.body)}):s(document.body),s}const K={};function Y(t,n){K[t]=n}function $(t){return K[t]||{ingress:n=>n,egress:n=>n}}const N={};function M(t,n){if(!t||typeof n!="object")return;const l=t.toLowerCase().split("-")[0];N[l]=n}function z(t){if(!t)return null;const n=t.toLowerCase().split("-")[0];return N[n]||null}M("mk",{monthsLong:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"],monthsShort:["јан","фев","мар","апр","мај","јун","јул","авг","септ","окт","ноем","дек"],daysLong:["недела","понеделник","вторник","среда","четврток","петок","сабота"],daysShort:["нед","пон","вт","ср","чет","пет","саб"]}),typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=Y,window.lnCore.getDataMapper=$,window.lnCore.registerLocaleFallback=M,window.lnCore.getLocaleFallback=z,window.lnCore.fillTemplate=O,window.lnCore.fill=F,window.lnCore.lnFill=P,window.lnCore.renderList=V);function Q(t,n){let l=!1;return function(){l||(l=!0,queueMicrotask(function(){l=!1,t(),n&&n()}))}}const Z="ln:";let E=null;function j(){if(E!==null)return E;try{if(typeof localStorage>"u")return E=!1,!1;const t="__ln_test__";localStorage.setItem(t,t),localStorage.removeItem(t),E=!0}catch{E=!1}return E}function tt(){return location.pathname.replace(/\/+$/,"").toLowerCase()||"/"}function W(t,n){const l=n.getAttribute("data-ln-persist"),f=l!==null&&l!==""?l:n.id;return f?Z+t+":"+tt()+":"+f:(console.warn('[ln-persist] Element requires id or data-ln-persist="key"',n),null)}function et(t,n){if(!j())return null;const l=W(t,n);if(!l)return null;try{const f=localStorage.getItem(l);return f!==null?JSON.parse(f):null}catch{return null}}function H(t,n,l){if(!j())return;const f=W(t,n);if(f)try{l==null?localStorage.removeItem(f):localStorage.setItem(f,JSON.stringify(l))}catch{}}function U(t){return(t||"").replace(/^#/,"")}function R(t){const n=t===void 0?location.hash:t,l={},f=U(n);if(!f)return l;const p=f.split("&");for(let g=0;g<p.length;g++){const u=p[g];if(!u)continue;const c=u.indexOf(":"),s=c>-1?u.slice(0,c):u,d=c>-1?u.slice(c+1):"";if(s)try{l[s]=decodeURIComponent(d)}catch{l[s]=d}}return l}function nt(t){if(!t)return null;const n=R();return t in n?n[t]:null}function lt(t,n){if(!t)return;const l=R();n==null?delete l[t]:l[t]=String(n);const p=Object.keys(l).map(function(g){const u=l[g];return u===""?g:g+":"+encodeURIComponent(u)}).join("&");U(location.hash)!==p&&(location.hash=p)}function it(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=R,window.lnCore.hashGet=nt,window.lnCore.hashSet=lt,window.lnCore.hashLinkClick=it),(function(){const t="data-ln-filter",n="lnFilter",l="data-ln-filter-key",f="data-ln-filter-value",p="data-ln-filter-hide",g="data-ln-filter-reset",u="data-ln-filter-col",c=new WeakMap;if(window[n]!==void 0)return;function s(e){return e.hasAttribute(g)||e.getAttribute(f)===""}function d(e){let i=e._filterKey;const r=[];for(let o=0;o<e.inputs.length;o++){const a=e.inputs[o];if(a.checked&&!s(a)){const h=a.getAttribute(f);h&&r.push(h)}}return{key:i,values:r}}function m(e,i){if(e.length!==i.length)return!0;for(let r=0;r<e.length;r++)if(e[r]!==i[r])return!0;return!1}function w(e){const i=e.dom,r=e.colIndex,o=i.querySelector("template");if(!o||r===null)return;const a=document.getElementById(e.targetId);if(!a)return;const h=a.tagName==="TABLE"?a:a.querySelector("table");if(!h||a.hasAttribute("data-ln-table"))return;const A={},b=[],v=h.tBodies;for(let k=0;k<v.length;k++){const C=v[k].rows;for(let _=0;_<C.length;_++){const T=C[_].cells[r],L=T?T.textContent.trim():"";L&&!A[L]&&(A[L]=!0,b.push(L))}}b.sort(function(k,C){return k.localeCompare(C)});const x=i.querySelector("["+l+"]"),S=x?x.getAttribute(l):i.getAttribute("data-ln-filter-key")||"col"+r;for(let k=0;k<b.length;k++){const C=o.content.cloneNode(!0),_=C.querySelector("input");_&&(_.setAttribute(l,S),_.setAttribute(f,b[k]),O(C,{text:b[k]}),i.appendChild(C))}}function y(e){this.dom=e,this.targetId=e.getAttribute(t);const i=e.getAttribute(u);this.colIndex=i!==null?parseInt(i,10):null,w(this),this.inputs=Array.from(e.querySelectorAll("["+l+"]")),this._filterKey=this.inputs.length>0?this.inputs[0].getAttribute(l):null,this._lastSnapshot=null;const r=this,o=Q(function(){r._render()},function(){r._afterRender()});this._queueRender=o,this._attachHandlers();let a=!1;if(e.hasAttribute("data-ln-persist")){const h=et("filter",e);if(h&&h.key&&Array.isArray(h.values)&&h.values.length>0){for(let A=0;A<this.inputs.length;A++){const b=this.inputs[A];s(b)?b.checked=!1:b.getAttribute(l)===h.key&&h.values.indexOf(b.getAttribute(f))!==-1?b.checked=!0:b.checked=!1}o(),a=!0}}if(!a){for(let h=0;h<this.inputs.length;h++)if(this.inputs[h].checked&&!s(this.inputs[h])){o();break}}return this}y.prototype._attachHandlers=function(){const e=this;this.inputs.forEach(function(i){i[n+"Bound"]||(i[n+"Bound"]=!0,i._lnFilterChange=function(){if(s(i)){for(let r=0;r<e.inputs.length;r++)s(e.inputs[r])||(e.inputs[r].checked=!1);i.checked=!0,e._queueRender();return}if(i.checked){for(let o=0;o<e.inputs.length;o++)s(e.inputs[o])&&(e.inputs[o].checked=!1);let r=!1;for(let o=0;o<e.inputs.length;o++)if(s(e.inputs[o])){r=!0;break}if(r){let o=!0;for(let a=0;a<e.inputs.length;a++)if(!s(e.inputs[a])&&!e.inputs[a].checked){o=!1;break}if(o)for(let a=0;a<e.inputs.length;a++)s(e.inputs[a])?e.inputs[a].checked=!0:e.inputs[a].checked=!1}}else{let r=!1;for(let o=0;o<e.inputs.length;o++)if(!s(e.inputs[o])&&e.inputs[o].checked){r=!0;break}if(!r)for(let o=0;o<e.inputs.length;o++)s(e.inputs[o])&&(e.inputs[o].checked=!0)}e._queueRender()},i.addEventListener("change",i._lnFilterChange))})},y.prototype._render=function(){const e=this,i=d(this),r=i.key===null||i.values.length===0,o=[];for(let a=0;a<i.values.length;a++)o.push(i.values[a].toLowerCase());if(e.colIndex!==null)e._filterTableRows(i);else{const a=document.getElementById(e.targetId);if(!a)return;const h=a.children;for(let A=0;A<h.length;A++){const b=h[A];if(r){b.removeAttribute(p);continue}const v=b.getAttribute("data-"+i.key);b.removeAttribute(p),v!==null&&o.indexOf(v.toLowerCase())===-1&&b.setAttribute(p,"true")}}},y.prototype._afterRender=function(){const e=d(this),i=this._lastSnapshot;if(!i||i.key!==e.key||m(i.values,e.values)){this._dispatchOnBoth("ln-filter:changed",{key:e.key,values:e.values.slice()});const o=i&&i.values.length>0,a=e.values.length===0;o&&a&&this._dispatchOnBoth("ln-filter:reset",{}),this._lastSnapshot={key:e.key,values:e.values.slice()}}this.dom.hasAttribute("data-ln-persist")&&(e.key&&e.values.length>0?H("filter",this.dom,{key:e.key,values:e.values.slice()}):H("filter",this.dom,null))},y.prototype._dispatchOnBoth=function(e,i){B(this.dom,e,i);const r=document.getElementById(this.targetId);r&&r!==this.dom&&B(r,e,i)},y.prototype._filterTableRows=function(e){const i=document.getElementById(this.targetId);if(!i)return;const r=i.tagName==="TABLE"?i:i.querySelector("table");if(!r||i.hasAttribute("data-ln-table"))return;const o=e.key||this._filterKey,a=e.values;c.has(r)||c.set(r,{});const h=c.get(r);if(o&&a.length>0){const x=[];for(let S=0;S<a.length;S++)x.push(a[S].toLowerCase());h[o]={col:this.colIndex,values:x}}else o&&delete h[o];const A=Object.keys(h),b=A.length>0,v=r.tBodies;for(let x=0;x<v.length;x++){const S=v[x].rows;for(let k=0;k<S.length;k++){const C=S[k];if(!b){C.removeAttribute(p);continue}let _=!0;for(let T=0;T<A.length;T++){const L=h[A[T]],G=C.cells[L.col],rt=G?G.textContent.trim().toLowerCase():"";if(L.values.indexOf(rt)===-1){_=!1;break}}_?C.removeAttribute(p):C.setAttribute(p,"true")}}},y.prototype.destroy=function(){if(this.dom[n]){if(this.colIndex!==null){const e=document.getElementById(this.targetId);if(e){const i=e.tagName==="TABLE"?e:e.querySelector("table");if(i&&c.has(i)){const r=c.get(i),o=this._filterKey;o&&r[o]&&delete r[o],Object.keys(r).length===0&&c.delete(i)}}}this.inputs.forEach(function(e){e._lnFilterChange&&(e.removeEventListener("change",e._lnFilterChange),delete e._lnFilterChange),delete e[n+"Bound"]}),delete this.dom[n]}},X(t,n,y,"ln-filter")})()})();
|
|
@@ -5,7 +5,6 @@ import { persistGet, persistSet } from '../../ln-core';
|
|
|
5
5
|
(function () {
|
|
6
6
|
const DOM_SELECTOR = 'data-ln-filter';
|
|
7
7
|
const DOM_ATTRIBUTE = 'lnFilter';
|
|
8
|
-
const INIT_ATTR = 'data-ln-filter-initialized';
|
|
9
8
|
const KEY_ATTR = 'data-ln-filter-key';
|
|
10
9
|
const VALUE_ATTR = 'data-ln-filter-value';
|
|
11
10
|
const HIDE_ATTR = 'data-ln-filter-hide';
|
|
@@ -22,12 +21,11 @@ import { persistGet, persistSet } from '../../ln-core';
|
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
function _deriveActive(self) {
|
|
25
|
-
let key =
|
|
24
|
+
let key = self._filterKey;
|
|
26
25
|
const values = [];
|
|
27
26
|
for (let i = 0; i < self.inputs.length; i++) {
|
|
28
27
|
const input = self.inputs[i];
|
|
29
28
|
if (input.checked && !_isReset(input)) {
|
|
30
|
-
if (key === null) key = input.getAttribute(KEY_ATTR);
|
|
31
29
|
const v = input.getAttribute(VALUE_ATTR);
|
|
32
30
|
if (v) values.push(v);
|
|
33
31
|
}
|
|
@@ -95,8 +93,6 @@ import { persistGet, persistSet } from '../../ln-core';
|
|
|
95
93
|
// ─── Component ─────────────────────────────────────────────
|
|
96
94
|
|
|
97
95
|
function _component(dom) {
|
|
98
|
-
if (dom.hasAttribute(INIT_ATTR)) return this;
|
|
99
|
-
|
|
100
96
|
this.dom = dom;
|
|
101
97
|
this.targetId = dom.getAttribute(DOM_SELECTOR);
|
|
102
98
|
|
|
@@ -160,7 +156,6 @@ import { persistGet, persistSet } from '../../ln-core';
|
|
|
160
156
|
}
|
|
161
157
|
}
|
|
162
158
|
|
|
163
|
-
dom.setAttribute(INIT_ATTR, '');
|
|
164
159
|
return this;
|
|
165
160
|
}
|
|
166
161
|
|
|
@@ -191,6 +186,27 @@ import { persistGet, persistSet } from '../../ln-core';
|
|
|
191
186
|
for (let i = 0; i < self.inputs.length; i++) {
|
|
192
187
|
if (_isReset(self.inputs[i])) self.inputs[i].checked = false;
|
|
193
188
|
}
|
|
189
|
+
// If all non-reset inputs are now checked → collapse to sentinel
|
|
190
|
+
// (only when a reset sentinel exists; lists without one skip this)
|
|
191
|
+
let hasReset = false;
|
|
192
|
+
for (let ri = 0; ri < self.inputs.length; ri++) {
|
|
193
|
+
if (_isReset(self.inputs[ri])) { hasReset = true; break; }
|
|
194
|
+
}
|
|
195
|
+
if (hasReset) {
|
|
196
|
+
let allChecked = true;
|
|
197
|
+
for (let ci = 0; ci < self.inputs.length; ci++) {
|
|
198
|
+
if (!_isReset(self.inputs[ci]) && !self.inputs[ci].checked) {
|
|
199
|
+
allChecked = false;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (allChecked) {
|
|
204
|
+
for (let mi = 0; mi < self.inputs.length; mi++) {
|
|
205
|
+
if (_isReset(self.inputs[mi])) self.inputs[mi].checked = true;
|
|
206
|
+
else self.inputs[mi].checked = false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
194
210
|
} else {
|
|
195
211
|
// If no non-reset values remain checked, fall back to reset
|
|
196
212
|
let anyChecked = false;
|
|
@@ -394,7 +410,6 @@ import { persistGet, persistSet } from '../../ln-core';
|
|
|
394
410
|
}
|
|
395
411
|
delete input[DOM_ATTRIBUTE + 'Bound'];
|
|
396
412
|
});
|
|
397
|
-
this.dom.removeAttribute(INIT_ATTR);
|
|
398
413
|
delete this.dom[DOM_ATTRIBUTE];
|
|
399
414
|
};
|
|
400
415
|
|