@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.
Files changed (217) hide show
  1. package/README.md +189 -177
  2. package/js/COMPONENTS.md +55 -49
  3. package/js/index.js +22 -5
  4. package/js/ln-accordion/README.md +186 -137
  5. package/js/ln-accordion/ln-accordion.js +1 -1
  6. package/js/ln-ajax/README.md +73 -4
  7. package/js/ln-ajax/ln-ajax.js +1 -1
  8. package/js/ln-ajax/src/ln-ajax.js +13 -13
  9. package/js/ln-api-connector/README.md +261 -150
  10. package/js/ln-api-connector/ln-api-connector.js +1 -1
  11. package/js/ln-api-connector/src/ln-api-connector.js +368 -265
  12. package/js/ln-api-queue/README.md +219 -0
  13. package/js/ln-api-queue/ln-api-queue.js +1 -0
  14. package/js/ln-api-queue/src/ln-api-queue.js +320 -0
  15. package/js/ln-api-queue/src/queue-storage.js +455 -0
  16. package/js/ln-autoresize/README.md +47 -0
  17. package/js/ln-autoresize/ln-autoresize.js +1 -1
  18. package/js/ln-autosave/README.md +33 -1
  19. package/js/ln-autosave/ln-autosave.js +1 -1
  20. package/js/ln-autosave/src/ln-autosave.js +4 -4
  21. package/js/ln-circular-progress/README.md +27 -13
  22. package/js/ln-circular-progress/ln-circular-progress.js +1 -1
  23. package/js/ln-circular-progress/src/ln-circular-progress.js +18 -5
  24. package/js/ln-confirm/README.md +162 -86
  25. package/js/ln-confirm/_ln-confirm.scss +11 -0
  26. package/js/ln-confirm/ln-confirm.js +1 -1
  27. package/js/ln-confirm/src/ln-confirm.js +77 -41
  28. package/js/ln-core/README.md +896 -0
  29. package/js/ln-core/hash.js +92 -0
  30. package/js/ln-core/helpers.js +337 -8
  31. package/js/ln-core/index.js +4 -2
  32. package/js/ln-core/persist.js +29 -3
  33. package/js/ln-core/positioning.js +1 -35
  34. package/js/ln-core/window-cache.js +213 -0
  35. package/js/ln-couchdb-connector/README.md +177 -156
  36. package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -1
  37. package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +398 -348
  38. package/js/ln-data-coordinator/README.md +536 -165
  39. package/js/ln-data-coordinator/ln-data-coordinator.js +1 -1
  40. package/js/ln-data-coordinator/src/data-read-policy.js +18 -0
  41. package/js/ln-data-coordinator/src/ln-data-coordinator.js +1042 -249
  42. package/js/ln-data-coordinator/src/mutation-receipts.js +46 -0
  43. package/js/ln-data-store/README.md +241 -94
  44. package/js/ln-data-store/ln-data-store.js +1 -1
  45. package/js/ln-data-store/src/ln-data-store.js +694 -699
  46. package/js/ln-date/README.md +39 -1
  47. package/js/ln-date/ln-date-dev.scss +14 -0
  48. package/js/ln-date/ln-date.js +1 -1
  49. package/js/ln-date/src/ln-date.js +236 -178
  50. package/js/ln-debug/README.md +58 -0
  51. package/js/ln-debug/ln-debug.js +1 -0
  52. package/js/ln-debug/src/ln-debug.js +19 -0
  53. package/js/ln-dropdown/README.md +31 -8
  54. package/js/ln-dropdown/ln-dropdown.js +1 -1
  55. package/js/ln-dropdown/src/ln-dropdown.js +30 -13
  56. package/js/ln-editor/README.md +274 -0
  57. package/js/ln-editor/ln-editor.js +1 -0
  58. package/js/ln-editor/ln-editor.scss +43 -0
  59. package/js/ln-editor/src/ln-editor.js +619 -0
  60. package/js/ln-external-links/README.md +44 -5
  61. package/js/ln-external-links/ln-external-links.js +1 -1
  62. package/js/ln-fill/README.md +199 -0
  63. package/js/ln-fill/ln-fill.js +1 -0
  64. package/js/ln-fill/src/ln-fill.js +87 -0
  65. package/js/ln-filter/README.md +92 -2
  66. package/js/ln-filter/ln-filter-dev.scss +8 -0
  67. package/js/ln-filter/ln-filter.js +1 -1
  68. package/js/ln-filter/src/ln-filter.js +22 -7
  69. package/js/ln-form/README.md +212 -49
  70. package/js/ln-form/ln-form.js +1 -1
  71. package/js/ln-form/src/ln-form.js +99 -199
  72. package/js/ln-http/README.md +125 -89
  73. package/js/ln-http/ln-http.js +1 -1
  74. package/js/ln-http/src/ln-http.js +32 -13
  75. package/js/ln-icon/README.md +130 -0
  76. package/js/ln-icon/ln-icon.js +1 -0
  77. package/js/{ln-icons/src/ln-icons.js → ln-icon/src/ln-icon.js} +20 -15
  78. package/js/ln-link/README.md +47 -9
  79. package/js/ln-link/ln-link.js +1 -1
  80. package/js/ln-link/src/ln-link.js +22 -6
  81. package/js/ln-list/README.md +129 -0
  82. package/js/ln-list/ln-list.js +1 -0
  83. package/js/ln-list/ln-list.scss +28 -0
  84. package/js/ln-list/src/ln-list.js +1216 -0
  85. package/js/ln-modal/README.md +185 -154
  86. package/js/ln-modal/ln-modal-dev.scss +13 -0
  87. package/js/ln-modal/ln-modal.js +1 -1
  88. package/js/ln-modal/ln-modal.scss +28 -4
  89. package/js/ln-modal/src/ln-modal.js +137 -201
  90. package/js/ln-modal-coordinator/README.md +99 -0
  91. package/js/ln-modal-coordinator/ln-modal-coordinator-dev.scss +8 -0
  92. package/js/ln-modal-coordinator/ln-modal-coordinator.js +1 -0
  93. package/js/ln-modal-coordinator/src/ln-modal-coordinator.js +345 -0
  94. package/js/ln-nav/README.md +46 -3
  95. package/js/ln-nav/ln-nav.js +1 -1
  96. package/js/ln-nav/src/ln-nav.js +92 -127
  97. package/js/ln-number/README.md +28 -1
  98. package/js/ln-number/ln-number-dev.scss +13 -0
  99. package/js/ln-number/ln-number.js +1 -1
  100. package/js/ln-number/src/ln-number.js +443 -350
  101. package/js/ln-options/README.md +66 -0
  102. package/js/ln-options/ln-options.js +1 -0
  103. package/js/ln-options/src/ln-options.js +74 -0
  104. package/js/ln-popover/README.md +40 -5
  105. package/js/ln-popover/ln-popover.js +1 -1
  106. package/js/ln-popover/src/ln-popover.js +38 -11
  107. package/js/ln-progress/README.md +413 -442
  108. package/js/ln-progress/ln-progress.js +1 -1
  109. package/js/ln-progress/src/ln-progress.js +98 -150
  110. package/js/ln-router/README.md +432 -0
  111. package/js/ln-router/ln-router.js +1 -0
  112. package/js/ln-router/src/ln-router.js +539 -0
  113. package/js/ln-search/README.md +158 -83
  114. package/js/ln-search/ln-search-dev.scss +8 -0
  115. package/js/ln-search/ln-search.js +1 -1
  116. package/js/ln-search/src/ln-search.js +12 -9
  117. package/js/ln-slug/README.md +110 -0
  118. package/js/ln-slug/ln-slug.js +1 -0
  119. package/js/ln-slug/src/ln-slug.js +74 -0
  120. package/js/ln-sort/README.md +170 -0
  121. package/js/ln-sort/ln-sort-dev.scss +8 -0
  122. package/js/ln-sort/ln-sort.js +1 -0
  123. package/js/ln-sort/src/ln-sort.js +178 -0
  124. package/js/ln-sortable/README.md +133 -95
  125. package/js/ln-sortable/ln-sortable.js +1 -1
  126. package/js/ln-stat/README.md +55 -0
  127. package/js/ln-stat/ln-stat.js +1 -0
  128. package/js/ln-stat/src/ln-stat.js +55 -0
  129. package/js/ln-table/README.md +285 -101
  130. package/js/ln-table/ln-table-dev.scss +17 -0
  131. package/js/ln-table/ln-table.js +1 -1
  132. package/js/ln-table/ln-table.scss +1 -1
  133. package/js/ln-table/src/ln-table.js +1493 -473
  134. package/js/ln-table-coordinator/README.md +73 -0
  135. package/js/ln-table-coordinator/ln-table-coordinator.js +1 -0
  136. package/js/ln-table-coordinator/src/ln-table-coordinator.js +160 -0
  137. package/js/ln-tabs/README.md +221 -137
  138. package/js/ln-tabs/ln-tabs-dev.scss +18 -0
  139. package/js/ln-tabs/ln-tabs.js +1 -1
  140. package/js/ln-tabs/src/ln-tabs.js +41 -24
  141. package/js/ln-time/README.md +14 -0
  142. package/js/ln-time/ln-time.js +1 -1
  143. package/js/ln-time/src/ln-time.js +223 -192
  144. package/js/ln-toast/README.md +178 -122
  145. package/js/ln-toast/ln-toast.js +1 -15
  146. package/js/ln-toast/src/ln-toast.js +232 -210
  147. package/js/ln-toast/template.html +16 -12
  148. package/js/ln-toggle/README.md +61 -2
  149. package/js/ln-toggle/ln-toggle-dev.scss +13 -0
  150. package/js/ln-toggle/ln-toggle.js +1 -1
  151. package/js/ln-toggle/src/ln-toggle.js +56 -43
  152. package/js/ln-tooltip/README.md +95 -58
  153. package/js/ln-tooltip/ln-tooltip-dev.scss +9 -0
  154. package/js/ln-tooltip/ln-tooltip.js +1 -1
  155. package/js/ln-tooltip/src/ln-tooltip.js +45 -6
  156. package/js/ln-translations/README.md +118 -96
  157. package/js/ln-translations/ln-translations.js +1 -1
  158. package/js/ln-translations/src/ln-translations.js +10 -5
  159. package/js/ln-upload/README.md +85 -10
  160. package/js/ln-upload/ln-upload.js +1 -1
  161. package/js/ln-upload/src/ln-upload.js +26 -10
  162. package/js/ln-validate/README.md +72 -0
  163. package/js/ln-validate/ln-validate-dev.scss +15 -0
  164. package/js/ln-validate/ln-validate.js +1 -1
  165. package/js/ln-validate/src/ln-validate.js +64 -0
  166. package/package.json +61 -56
  167. package/scss/base/_global.scss +2 -2
  168. package/scss/base/_typography.scss +1 -2
  169. package/scss/components/_date.scss +1 -0
  170. package/scss/components/_dropdown.scss +0 -5
  171. package/scss/components/_editor.scss +5 -0
  172. package/scss/components/_filter.scss +26 -25
  173. package/scss/components/_form.scss +7 -0
  174. package/scss/components/_ln-table.scss +13 -3
  175. package/scss/components/_sort.scss +10 -0
  176. package/scss/components/_table.scss +36 -23
  177. package/scss/components/_toast.scss +68 -41
  178. package/scss/components/_toggle.scss +16 -0
  179. package/scss/config/_density.scss +205 -117
  180. package/scss/config/_icons.scss +10 -9
  181. package/scss/config/_tokens.scss +69 -47
  182. package/scss/config/mixins/_accordion.scss +2 -7
  183. package/scss/config/mixins/_alert.scss +4 -4
  184. package/scss/config/mixins/_breadcrumbs.scss +2 -2
  185. package/scss/config/mixins/_btn.scss +1 -1
  186. package/scss/config/mixins/_card.scss +89 -14
  187. package/scss/config/mixins/_collapsible.scss +3 -0
  188. package/scss/config/mixins/_colors.scss +1 -1
  189. package/scss/config/mixins/_diagnostics.scss +11 -0
  190. package/scss/config/mixins/_display.scss +1 -0
  191. package/scss/config/mixins/_dropdown.scss +3 -5
  192. package/scss/config/mixins/_editor.scss +124 -0
  193. package/scss/config/mixins/_empty-state.scss +7 -0
  194. package/scss/config/mixins/_filter.scss +3 -5
  195. package/scss/config/mixins/_footer.scss +1 -2
  196. package/scss/config/mixins/_form.scss +207 -21
  197. package/scss/config/mixins/_index.scss +2 -0
  198. package/scss/config/mixins/_ln-table.scss +41 -2
  199. package/scss/config/mixins/_modal.scss +149 -66
  200. package/scss/config/mixins/_nav.scss +3 -4
  201. package/scss/config/mixins/_popover.scss +39 -0
  202. package/scss/config/mixins/_sidebar.scss +1 -1
  203. package/scss/config/mixins/_sort.scss +42 -0
  204. package/scss/config/mixins/_table.scss +85 -60
  205. package/scss/config/mixins/_toast.scss +179 -152
  206. package/scss/config/mixins/_tooltip.scss +17 -7
  207. package/scss/config/mixins/_typography.scss +4 -2
  208. package/scss/ln-ashlar-dev.scss +27 -9
  209. package/scss/ln-ashlar.scss +7 -1
  210. package/js/ln-data-table/README.md +0 -110
  211. package/js/ln-data-table/ln-data-table.js +0 -1
  212. package/js/ln-data-table/ln-data-table.scss +0 -10
  213. package/js/ln-data-table/src/ln-data-table.js +0 -1087
  214. package/js/ln-icons/README.md +0 -88
  215. package/js/ln-icons/ln-icons.js +0 -1
  216. package/js/ln-table/ln-table-sort.js +0 -1
  217. package/js/ln-table/src/ln-table-sort.js +0 -156
@@ -1 +1 @@
1
- (function(){"use strict";function d(o,n,a){o.dispatchEvent(new CustomEvent(n,{bubbles:!0,detail:a||{}}))}function s(o,n){if(!document.body){document.addEventListener("DOMContentLoaded",function(){s(o,n)}),console.warn("["+n+'] Script loaded before <body> — add "defer" to your <script> tag');return}o()}const u={};function l(o,n){u[o]=n}function f(o){return u[o]||{ingress:n=>n,egress:n=>n}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=l,window.lnCore.getDataMapper=f),(function(){const o="lnExternalLinks";if(window[o]!==void 0)return;function n(e){return e.hostname&&e.hostname!==window.location.hostname}function a(e){if(e.getAttribute("data-ln-external-link")==="processed"||!n(e))return;e.target="_blank";const t=(e.rel||"").split(/\s+/).filter(Boolean);t.includes("noopener")||t.push("noopener"),t.includes("noreferrer")||t.push("noreferrer"),e.rel=t.join(" ");const i=document.createElement("span");i.className="sr-only",i.textContent="(opens in new tab)",e.appendChild(i),e.setAttribute("data-ln-external-link","processed"),d(e,"ln-external-links:processed",{link:e,href:e.href})}function c(e){e=e||document.body;for(const t of e.querySelectorAll("a, area"))a(t)}function p(){s(function(){document.body.addEventListener("click",function(e){const t=e.target.closest("a, area");t&&t.getAttribute("data-ln-external-link")==="processed"&&d(t,"ln-external-links:clicked",{link:t,href:t.href,text:t.textContent||t.title||""})})},"ln-external-links")}function b(){s(function(){new MutationObserver(function(t){for(const i of t){if(i.type==="childList"){for(const r of i.addedNodes)if(r.nodeType===1&&(r.matches&&(r.matches("a")||r.matches("area"))&&a(r),r.querySelectorAll))for(const m of r.querySelectorAll("a, area"))a(m)}if(i.type==="attributes"&&i.attributeName==="href"){const r=i.target;r.matches&&(r.matches("a")||r.matches("area"))&&a(r)}}}).observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["href"]})},"ln-external-links")}function h(){p(),b(),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){c()}):c()}window[o]={process:c},h()})()})();
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
+ })();
@@ -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 Mutual Exclusion:** The `data-ln-filter-reset` ("All") checkbox is kept in sync automatically: checking any value checkbox unchecks the reset sentinel; checking the sentinel unchecks all value inputs; unchecking all value inputs re-checks the 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
- - **Filtering Coordinated Tables:** `ln-filter` is designed for static lists or plain native tables. Do not target virtualised components like `ln-table` or `ln-data-table`, which manage their own column filters internally.
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.
@@ -0,0 +1,8 @@
1
+ @use '../../scss/config/mixins' as *;
2
+
3
+ [data-ln-debug] {
4
+ // 1. Filter Trigger Check: data-ln-filter must target a component ID and cannot be empty
5
+ [data-ln-filter=""] {
6
+ @include dev-inline-error("data-ln-filter target is empty");
7
+ }
8
+ }
@@ -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 = null;
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