@livenetworks/ashlar 1.4.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 (219) 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 +189 -99
  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 -55
  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 -0
  173. package/scss/components/_form.scss +7 -0
  174. package/scss/components/_ln-table.scss +14 -5
  175. package/scss/components/_sort.scss +10 -0
  176. package/scss/components/_table.scss +68 -1
  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 +48 -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 +47 -0
  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 +4 -1
  198. package/scss/config/mixins/_ln-table.scss +51 -93
  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 +232 -38
  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 +39 -0
  209. package/scss/ln-ashlar.scss +8 -2
  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 -1103
  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 -168
  218. package/scss/components/_data-table.scss +0 -83
  219. package/scss/config/mixins/_data-table.scss +0 -346
@@ -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
+ })();
@@ -1,99 +1,189 @@
1
- # ln-filter
2
-
3
- A zero-dependency, event-driven **Generic List & Table Filter Primitive** that manages item visibility states through declarative checkbox controls.
4
-
5
- It filters target elements either by comparing child dataset attributes (for custom cards/lists) or scanning table column cell contents (for plain HTML tables). It operates independently of and in harmony with `ln-search`, combining multiple filter criteria seamlessly.
6
-
7
- ---
8
-
9
- ## 🧭 Philosophy & Architecture
10
-
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.
13
- 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
- 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.
15
-
16
- ---
17
-
18
- ## 📦 Minimal Blueprint
19
-
20
- ### Generic List Attribute Filter (Zero-JS)
21
- Bind the filter to a container `id`. Target items declare attributes matching the filter key.
22
- ```html
23
- <nav data-ln-filter="employees-list">
24
- <!-- Reset Sentinel -->
25
- <label><input type="checkbox" data-ln-filter-key="category" data-ln-filter-reset checked> All</label>
26
- <!-- Values -->
27
- <label><input type="checkbox" data-ln-filter-key="category" data-ln-filter-value="design"> Design</label>
28
- <label><input type="checkbox" data-ln-filter-key="category" data-ln-filter-value="dev"> Development</label>
29
- </nav>
30
-
31
- <ul id="employees-list">
32
- <li data-category="design">Ana Petrova — UI Designer</li>
33
- <li data-category="dev">Marko Nikolov — Developer</li>
34
- </ul>
35
- ```
36
-
37
- ### Table Column Filter with Auto-Population & Persistence
38
- Auto-populates filter checkboxes from Column Index `2` (Department) and saves state to storage.
39
- ```html
40
- <nav id="dept-filter" data-ln-filter="users-table" data-ln-filter-col="2" data-ln-persist>
41
- <label><input type="checkbox" data-ln-filter-key="dept" data-ln-filter-reset checked> All Departments</label>
42
- <!-- Checklist generated dynamically here -->
43
- <template>
44
- <label><input type="checkbox"> {{ text }}</label>
45
- </template>
46
- </nav>
47
-
48
- <table id="users-table">
49
- <thead>
50
- <tr><th>ID</th><th>Name</th><th>Department</th></tr>
51
- </thead>
52
- <tbody>
53
- <tr><td>1</td><td>Ana Petrova</td><td>Engineering</td></tr>
54
- <tr><td>2</td><td>Marko Nikolov</td><td>Design</td></tr>
55
- </tbody>
56
- </table>
57
- ```
58
-
59
- ---
60
-
61
- ## 🛠️ Declarative API Contract
62
-
63
- ### HTML Attributes
64
-
65
- | Attribute | Elements | Description |
66
- | :--- | :--- | :--- |
67
- | `data-ln-filter` | Container root | Component root. Value is the `id` of the target container whose items are filtered. |
68
- | `data-ln-filter-key` | `<input type="checkbox">` | The field name representing the target dataset attribute (e.g. `category` matches `data-category`). |
69
- | `data-ln-filter-value` | `<input type="checkbox">` | The value to match. Active checkboxes show matching items; others are hidden. |
70
- | `data-ln-filter-reset` | `<input type="checkbox">` | Marks the reset ("All") sentinel. |
71
- | `data-ln-filter-col` | Container root | Opt-in. 0-based column index to filter plain `<table>` rows by column cell text. |
72
- | `data-ln-persist` | Container root | Opt-in. Persists active checkbox selections in `localStorage` under `lnf:{id}`. |
73
- | `data-ln-filter-hide` | Children of target | *State*. Automatically toggled on non-matching elements (`display: none !important`). |
74
-
75
- ---
76
-
77
- ## ⚡ DOM Events
78
-
79
- Events are dispatched on **both** the filter navigation container and the target element (dual dispatch).
80
-
81
- ### `ln-filter:changed`
82
- Fired when any filter selection is modified.
83
- - **Payload (`detail`)**: `{ key: string, values: string[] }` (where `values` lists active filter options).
84
-
85
- ### `ln-filter:reset`
86
- Fired when the reset sentinel is activated.
87
- - **Payload (`detail`)**: `{}`
88
-
89
- ---
90
-
91
- ## ⚠️ Common Pitfalls
92
-
93
- - **Driving State Programmatically Without Events:** Changing `input.checked = true` using JavaScript does not trigger browser `change` listeners. You must explicitly dispatch the event:
94
- ```javascript
95
- input.checked = true;
96
- input.dispatchEvent(new Event('change', { bubbles: true }));
97
- ```
98
- - **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.
1
+ # ln-filter
2
+
3
+ A zero-dependency, event-driven **Generic List & Table Filter Primitive** that manages item visibility states through declarative checkbox controls.
4
+
5
+ It filters target elements either by comparing child dataset attributes (for custom cards/lists) or scanning table column cell contents (for plain HTML tables). It operates independently of and in harmony with `ln-search`, combining multiple filter criteria seamlessly.
6
+
7
+ ---
8
+
9
+ ## 🧭 Philosophy & Architecture
10
+
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 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.
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.
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.
21
+
22
+ ---
23
+
24
+ ## 📦 Minimal Blueprint
25
+
26
+ ### Generic List Attribute Filter (Zero-JS)
27
+ Bind the filter to a container `id`. Target items declare attributes matching the filter key.
28
+ ```html
29
+ <nav data-ln-filter="employees-list">
30
+ <!-- Reset Sentinel -->
31
+ <label><input type="checkbox" data-ln-filter-key="category" data-ln-filter-reset checked> All</label>
32
+ <!-- Values -->
33
+ <label><input type="checkbox" data-ln-filter-key="category" data-ln-filter-value="design"> Design</label>
34
+ <label><input type="checkbox" data-ln-filter-key="category" data-ln-filter-value="dev"> Development</label>
35
+ </nav>
36
+
37
+ <ul id="employees-list">
38
+ <li data-category="design">Ana Petrova — UI Designer</li>
39
+ <li data-category="dev">Marko Nikolov — Developer</li>
40
+ </ul>
41
+ ```
42
+
43
+ ### Table Column Filter with Auto-Population & Persistence
44
+ Auto-populates filter checkboxes from Column Index `2` (Department) and saves state to storage.
45
+ ```html
46
+ <nav id="dept-filter" data-ln-filter="users-table" data-ln-filter-col="2" data-ln-persist>
47
+ <label><input type="checkbox" data-ln-filter-key="dept" data-ln-filter-reset checked> All Departments</label>
48
+ <!-- Checklist generated dynamically here -->
49
+ <template>
50
+ <label><input type="checkbox"> {{ text }}</label>
51
+ </template>
52
+ </nav>
53
+
54
+ <table id="users-table">
55
+ <thead>
56
+ <tr><th>ID</th><th>Name</th><th>Department</th></tr>
57
+ </thead>
58
+ <tbody>
59
+ <tr><td>1</td><td>Ana Petrova</td><td>Engineering</td></tr>
60
+ <tr><td>2</td><td>Marko Nikolov</td><td>Design</td></tr>
61
+ </tbody>
62
+ </table>
63
+ ```
64
+
65
+ ---
66
+
67
+ ## 🛠️ Declarative API Contract
68
+
69
+ ### HTML Attributes
70
+
71
+ | Attribute | Elements | Description |
72
+ | :--- | :--- | :--- |
73
+ | `data-ln-filter` | Container root | Component root. Value is the `id` of the target container whose items are filtered. |
74
+ | `data-ln-filter-key` | `<input type="checkbox">` | The field name representing the target dataset attribute (e.g. `category` matches `data-category`). |
75
+ | `data-ln-filter-value` | `<input type="checkbox">` | The value to match. Active checkboxes show matching items; others are hidden. |
76
+ | `data-ln-filter-reset` | `<input type="checkbox">` | Marks the reset ("All") sentinel. |
77
+ | `data-ln-filter-col` | Container root | Opt-in. 0-based column index to filter plain `<table>` rows by column cell text. |
78
+ | `data-ln-persist` | Container root | Opt-in. Persists active checkbox selections in `localStorage` under `lnf:{id}`. |
79
+ | `data-ln-filter-hide` | Children of target | *State*. Automatically toggled on non-matching elements (`display: none !important`). |
80
+
81
+ ---
82
+
83
+ ## ⚡ DOM Events
84
+
85
+ Events are dispatched on **both** the filter navigation container and the target element (dual dispatch).
86
+
87
+ ### `ln-filter:changed`
88
+ Fired when any filter selection is modified.
89
+ - **Payload (`detail`)**: `{ key: string, values: string[] }` (where `values` lists active filter options).
90
+
91
+ ### `ln-filter:reset`
92
+ Fired when the reset sentinel is activated.
93
+ - **Payload (`detail`)**: `{}`
94
+
95
+ ---
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
+
151
+ ## ⚠️ Common Pitfalls
152
+
153
+ - **Driving State Programmatically Without Events:** Changing `input.checked = true` using JavaScript does not trigger browser `change` listeners. You must explicitly dispatch the event:
154
+ ```javascript
155
+ input.checked = true;
156
+ input.dispatchEvent(new Event('change', { bubbles: true }));
157
+ ```
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.
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
+ }