@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 p(n,o,f){n.dispatchEvent(new CustomEvent(o,{bubbles:!0,detail:f||{}}))}function _(n,o,f){const a=new CustomEvent(o,{bubbles:!0,cancelable:!0,detail:f||{}});return n.dispatchEvent(a),a}function w(n,o){if(!document.body){document.addEventListener("DOMContentLoaded",function(){w(n,o)}),console.warn("["+o+'] Script loaded before <body> — add "defer" to your <script> tag');return}n()}function g(n,o,f,a){if(n.nodeType!==1)return;const i=o.indexOf("[")!==-1||o.indexOf(".")!==-1||o.indexOf("#")!==-1?o:"["+o+"]",t=Array.from(n.querySelectorAll(i));n.matches&&n.matches(i)&&t.push(n);for(const e of t)e[f]||(e[f]=new a(e))}function C(n,o,f,a,m={}){const i=m.extraAttributes||[],t=m.onAttributeChange||null,e=m.onInit||null;function l(r){const c=r||document.body;g(c,n,o,f),e&&e(c)}return w(function(){const r=new MutationObserver(function(d){for(let u=0;u<d.length;u++){const s=d[u];if(s.type==="childList"){for(let h=0;h<s.addedNodes.length;h++){const b=s.addedNodes[h];b.nodeType===1&&(g(b,n,o,f),e&&e(b))}for(let h=0;h<s.removedNodes.length;h++){const b=s.removedNodes[h];if(b.nodeType===1){const x=n.indexOf("[")!==-1||n.indexOf(".")!==-1||n.indexOf("#")!==-1?n:"["+n+"]",v=Array.from(b.querySelectorAll(x));b.matches&&b.matches(x)&&v.push(b);for(let y=0;y<v.length;y++){const A=v[y];if(!document.contains(A)){const E=A[o];E&&typeof E.destroy=="function"&&E.destroy()}}}}}else s.type==="attributes"&&(t&&s.target[o]?t(s.target,s.attributeName):(g(s.target,n,o,f),e&&e(s.target)))}});let c=[];if(n.indexOf("[")!==-1){const d=/\[([\w-]+)/g;let u;for(;(u=d.exec(n))!==null;)c.push(u[1])}else c.push(n);r.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:c.concat(i)})},a),window[o]=l,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){l(document.body)}):l(document.body),l}const L={};function O(n,o){L[n]=o}function D(n){return L[n]||{ingress:o=>o,egress:o=>o}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=O,window.lnCore.getDataMapper=D),(function(){const n="data-ln-sortable",o="lnSortable",f="data-ln-sortable-handle";if(window[o]!==void 0)return;function a(i){this.dom=i,this.isEnabled=i.getAttribute(n)!=="disabled",this._dragging=null,i.setAttribute("aria-roledescription","sortable list");const t=this;return this._onPointerDown=function(e){t.isEnabled&&t._handlePointerDown(e)},i.addEventListener("pointerdown",this._onPointerDown),this}a.prototype.enable=function(){this.isEnabled||this.dom.setAttribute(n,"")},a.prototype.disable=function(){this.isEnabled&&this.dom.setAttribute(n,"disabled")},a.prototype.destroy=function(){this.dom[o]&&(this.dom.removeEventListener("pointerdown",this._onPointerDown),p(this.dom,"ln-sortable:destroyed",{target:this.dom}),delete this.dom[o])},a.prototype._handlePointerDown=function(i){let t=i.target.closest("["+f+"]"),e;if(t){for(e=t;e&&e.parentElement!==this.dom;)e=e.parentElement;if(!e||e.parentElement!==this.dom)return}else{if(this.dom.querySelector("["+f+"]"))return;for(e=i.target;e&&e.parentElement!==this.dom;)e=e.parentElement;if(!e||e.parentElement!==this.dom)return;t=e}const r=Array.from(this.dom.children).indexOf(e);if(_(this.dom,"ln-sortable:before-drag",{item:e,index:r}).defaultPrevented)return;i.preventDefault(),t.setPointerCapture(i.pointerId),this._dragging=e,e.classList.add("ln-sortable--dragging"),e.setAttribute("aria-grabbed","true"),this.dom.classList.add("ln-sortable--active"),p(this.dom,"ln-sortable:drag-start",{item:e,index:r});const d=this,u=function(h){d._handlePointerMove(h)},s=function(h){d._handlePointerEnd(h),t.removeEventListener("pointermove",u),t.removeEventListener("pointerup",s),t.removeEventListener("pointercancel",s)};t.addEventListener("pointermove",u),t.addEventListener("pointerup",s),t.addEventListener("pointercancel",s)},a.prototype._handlePointerMove=function(i){if(!this._dragging)return;const t=Array.from(this.dom.children),e=this._dragging;for(const l of t)l.classList.remove("ln-sortable--drop-before","ln-sortable--drop-after");for(const l of t){if(l===e)continue;const r=l.getBoundingClientRect(),c=r.top+r.height/2;if(i.clientY>=r.top&&i.clientY<c){l.classList.add("ln-sortable--drop-before");break}else if(i.clientY>=c&&i.clientY<=r.bottom){l.classList.add("ln-sortable--drop-after");break}}},a.prototype._handlePointerEnd=function(i){if(!this._dragging)return;const t=this._dragging,e=Array.from(this.dom.children),l=e.indexOf(t);let r=null,c=null;for(const d of e){if(d.classList.contains("ln-sortable--drop-before")){r=d,c="before";break}if(d.classList.contains("ln-sortable--drop-after")){r=d,c="after";break}}for(const d of e)d.classList.remove("ln-sortable--drop-before","ln-sortable--drop-after");if(t.classList.remove("ln-sortable--dragging"),t.removeAttribute("aria-grabbed"),this.dom.classList.remove("ln-sortable--active"),r&&r!==t){c==="before"?this.dom.insertBefore(t,r):this.dom.insertBefore(t,r.nextElementSibling);const u=Array.from(this.dom.children).indexOf(t);p(this.dom,"ln-sortable:reordered",{item:t,oldIndex:l,newIndex:u})}this._dragging=null};function m(i){const t=i[o];if(!t)return;const e=i.getAttribute(n)!=="disabled";e!==t.isEnabled&&(t.isEnabled=e,p(i,e?"ln-sortable:enabled":"ln-sortable:disabled",{target:i}))}C(n,o,a,"ln-sortable",{onAttributeChange:m})})()})();
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 g={};function q(t,e){g[t]||(g[t]=document.querySelector('[data-ln-template="'+t+'"]'));const r=g[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 M(t,e,r){const d=new CustomEvent(e,{bubbles:!0,cancelable:!0,detail:r||{}});return t.dispatchEvent(d),d}function L(t,e){if(!t||!e)return t;const r=t.querySelectorAll("[data-ln-field]");for(let n=0;n<r.length;n++){const o=r[n],l=o.getAttribute("data-ln-field");e[l]!=null&&(o.textContent=e[l])}const d=t.querySelectorAll("[data-ln-attr]");for(let n=0;n<d.length;n++){const o=d[n],l=o.getAttribute("data-ln-attr").split(",");for(let s=0;s<l.length;s++){const a=l[s].trim().split(":");if(a.length!==2)continue;const c=a[0].trim(),u=a[1].trim();e[u]!=null&&o.setAttribute(c,e[u])}}const f=t.querySelectorAll("[data-ln-show]");for(let n=0;n<f.length;n++){const o=f[n],l=o.getAttribute("data-ln-show");l in e&&o.classList.toggle("hidden",!e[l])}const i=t.querySelectorAll("[data-ln-class]");for(let n=0;n<i.length;n++){const o=i[n],l=o.getAttribute("data-ln-class").split(",");for(let s=0;s<l.length;s++){const a=l[s].trim().split(":");if(a.length!==2)continue;const c=a[0].trim(),u=a[1].trim();u in e&&o.classList.toggle(c,!!e[u])}}return t}function P(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 d=0;d<r.length;d++)r[d].dispatchEvent(new CustomEvent("ln-fill",{detail:e??null,bubbles:!0}));return t}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore._fillBound||(window.lnCore._fillBound=!0,document.addEventListener("ln-fill",function(t){if(!(!t.target.matches||!t.target.matches("[data-ln-fillable]")))if(t.detail)L(t.target,t.detail);else{const e=t.target.querySelectorAll("[data-ln-field]");for(let r=0;r<e.length;r++)e[r].textContent=""}})));function A(t,e){if(!t||!e)return t;const r=document.createTreeWalker(t,NodeFilter.SHOW_TEXT);for(;r.nextNode();){const i=r.currentNode;i.textContent.indexOf("{{")!==-1&&(i.textContent=i.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(n,o){return e[o]!==void 0?e[o]:""}))}const d=function(i,n){return e[n]!==void 0?e[n]:""},f=Array.from(t.querySelectorAll("*"));t.nodeType===1&&f.push(t);for(let i=0;i<f.length;i++){const n=f[i],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,d))}}return t}function I(t,e,r,d,f,i){const n={};for(let l=0;l<t.children.length;l++){const s=t.children[l],a=s.getAttribute("data-ln-key");a&&(n[a]=s)}const o=document.createDocumentFragment();for(let l=0;l<e.length;l++){const s=e[l],a=String(d(s));let c=n[a];if(c)f(c,s,l);else{const u=q(r,i);if(!u||(A(u,s),c=u.firstElementChild,!c))continue;c.setAttribute("data-ln-key",a),f(c,s,l)}o.appendChild(c)}t.textContent="",t.appendChild(o)}function x(t,e){if(!document.body){document.addEventListener("DOMContentLoaded",function(){x(t,e)}),console.warn("["+e+'] Script loaded before <body> — add "defer" to your <script> tag');return}t()}function w(t,e,r,d){if(t.nodeType!==1)return;const i=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]",n=Array.from(t.querySelectorAll(i));t.matches&&t.matches(i)&&n.push(t);for(const o of n)o[r]||(o[r]=new d(o))}function N(t,e,r,d,f={}){const i=f.extraAttributes||[],n=f.onAttributeChange||null,o=f.onInit||null;function l(s){const a=s||document.body;w(a,t,e,r),o&&o(a)}return x(function(){const s=new MutationObserver(function(c){for(let u=0;u<c.length;u++){const h=c[u];if(h.type==="childList"){for(let p=0;p<h.addedNodes.length;p++){const b=h.addedNodes[p];b.nodeType===1&&(w(b,t,e,r),o&&o(b))}for(let p=0;p<h.removedNodes.length;p++){const b=h.removedNodes[p];if(b.nodeType===1){const D=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",C=Array.from(b.querySelectorAll(D));b.matches&&b.matches(D)&&C.push(b);for(let v=0;v<C.length;v++){const T=C[v];if(!document.contains(T)){const E=T[e];E&&typeof E.destroy=="function"&&E.destroy()}}}}}else h.type==="attributes"&&(n&&h.target[e]?n(h.target,h.attributeName):(w(h.target,t,e,r),o&&o(h.target)))}});let a=[];if(t.indexOf("[")!==-1){const c=/\[([\w-]+)/g;let u;for(;(u=c.exec(t))!==null;)a.push(u[1])}else a.push(t);s.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:a.concat(i)})},d),window[e]=l,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){l(document.body)}):l(document.body),l}const _={};function B(t,e){_[t]=e}function j(t){return _[t]||{ingress:e=>e,egress:e=>e}}const S={};function k(t,e){if(!t||typeof e!="object")return;const r=t.toLowerCase().split("-")[0];S[r]=e}function W(t){if(!t)return null;const e=t.toLowerCase().split("-")[0];return S[e]||null}k("mk",{monthsLong:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"],monthsShort:["јан","фев","мар","апр","мај","јун","јул","авг","септ","окт","ноем","дек"],daysLong:["недела","понеделник","вторник","среда","четврток","петок","сабота"],daysShort:["нед","пон","вт","ср","чет","пет","саб"]}),typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=B,window.lnCore.getDataMapper=j,window.lnCore.registerLocaleFallback=k,window.lnCore.getLocaleFallback=W,window.lnCore.fillTemplate=A,window.lnCore.fill=L,window.lnCore.lnFill=P,window.lnCore.renderList=I);function O(t){return(t||"").replace(/^#/,"")}function y(t){const e=t===void 0?location.hash:t,r={},d=O(e);if(!d)return r;const f=d.split("&");for(let i=0;i<f.length;i++){const n=f[i];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{r[l]=decodeURIComponent(s)}catch{r[l]=s}}return r}function F(t){if(!t)return null;const e=y();return t in e?e[t]:null}function R(t,e){if(!t)return;const r=y();e==null?delete r[t]:r[t]=String(e);const f=Object.keys(r).map(function(i){const n=r[i];return n===""?i:i+":"+encodeURIComponent(n)}).join("&");O(location.hash)!==f&&(location.hash=f)}function K(t){return t.button===1||t.ctrlKey||t.metaKey||t.shiftKey?!1:(t.preventDefault(),!0)}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.hashParse=y,window.lnCore.hashGet=F,window.lnCore.hashSet=R,window.lnCore.hashLinkClick=K),(function(){const t="data-ln-sortable",e="lnSortable",r="data-ln-sortable-handle";if(window[e]!==void 0)return;function d(i){this.dom=i,this.isEnabled=i.getAttribute(t)!=="disabled",this._dragging=null,i.setAttribute("aria-roledescription","sortable list");const n=this;return this._onPointerDown=function(o){n.isEnabled&&n._handlePointerDown(o)},i.addEventListener("pointerdown",this._onPointerDown),this}d.prototype.enable=function(){this.isEnabled||this.dom.setAttribute(t,"")},d.prototype.disable=function(){this.isEnabled&&this.dom.setAttribute(t,"disabled")},d.prototype.destroy=function(){this.dom[e]&&(this.dom.removeEventListener("pointerdown",this._onPointerDown),m(this.dom,"ln-sortable:destroyed",{target:this.dom}),delete this.dom[e])},d.prototype._handlePointerDown=function(i){let n=i.target.closest("["+r+"]"),o;if(n){for(o=n;o&&o.parentElement!==this.dom;)o=o.parentElement;if(!o||o.parentElement!==this.dom)return}else{if(this.dom.querySelector("["+r+"]"))return;for(o=i.target;o&&o.parentElement!==this.dom;)o=o.parentElement;if(!o||o.parentElement!==this.dom)return;n=o}const s=Array.from(this.dom.children).indexOf(o);if(M(this.dom,"ln-sortable:before-drag",{item:o,index:s}).defaultPrevented)return;i.preventDefault(),n.setPointerCapture(i.pointerId),this._dragging=o,o.classList.add("ln-sortable--dragging"),o.setAttribute("aria-grabbed","true"),this.dom.classList.add("ln-sortable--active"),m(this.dom,"ln-sortable:drag-start",{item:o,index:s});const c=this,u=function(p){c._handlePointerMove(p)},h=function(p){c._handlePointerEnd(p),n.removeEventListener("pointermove",u),n.removeEventListener("pointerup",h),n.removeEventListener("pointercancel",h)};n.addEventListener("pointermove",u),n.addEventListener("pointerup",h),n.addEventListener("pointercancel",h)},d.prototype._handlePointerMove=function(i){if(!this._dragging)return;const n=Array.from(this.dom.children),o=this._dragging;for(const l of n)l.classList.remove("ln-sortable--drop-before","ln-sortable--drop-after");for(const l of n){if(l===o)continue;const s=l.getBoundingClientRect(),a=s.top+s.height/2;if(i.clientY>=s.top&&i.clientY<a){l.classList.add("ln-sortable--drop-before");break}else if(i.clientY>=a&&i.clientY<=s.bottom){l.classList.add("ln-sortable--drop-after");break}}},d.prototype._handlePointerEnd=function(i){if(!this._dragging)return;const n=this._dragging,o=Array.from(this.dom.children),l=o.indexOf(n);let s=null,a=null;for(const c of o){if(c.classList.contains("ln-sortable--drop-before")){s=c,a="before";break}if(c.classList.contains("ln-sortable--drop-after")){s=c,a="after";break}}for(const c of o)c.classList.remove("ln-sortable--drop-before","ln-sortable--drop-after");if(n.classList.remove("ln-sortable--dragging"),n.removeAttribute("aria-grabbed"),this.dom.classList.remove("ln-sortable--active"),s&&s!==n){a==="before"?this.dom.insertBefore(n,s):this.dom.insertBefore(n,s.nextElementSibling);const u=Array.from(this.dom.children).indexOf(n);m(this.dom,"ln-sortable:reordered",{item:n,oldIndex:l,newIndex:u})}this._dragging=null};function f(i){const n=i[e];if(!n)return;const o=i.getAttribute(t)!=="disabled";o!==n.isEnabled&&(n.isEnabled=o,m(i,o?"ln-sortable:enabled":"ln-sortable:disabled",{target:i}))}N(t,e,d,"ln-sortable",{onAttributeChange:f})})()})();
@@ -0,0 +1,55 @@
1
+ # ln-stat
2
+
3
+ Displays a live count from an `ln-data-store` via the `ln-data-coordinator` binder. Updates automatically on every store mutation. Opt-in: add `data-ln-stat="<storeName>"` to any inline element.
4
+
5
+ ---
6
+
7
+ ## Attributes
8
+
9
+ | Attribute | Element | Description |
10
+ |---|---|---|
11
+ | `data-ln-stat="<storeName>"` | Any inline element | Store name to bind. |
12
+ | `data-ln-stat-filter="<field>:<value>"` | Same element | Optional single-field filter. Format: `field:value`. The value is String-matched against the store's `_filter` (treated as an array `[value]`). Multi-field filters are out of scope for v1. |
13
+
14
+ ---
15
+
16
+ ## Events
17
+
18
+ ### Dispatched
19
+ - `ln-stat:request-count` `{ stat: storeName, filters }` — fired on init (bubbles to coordinator).
20
+
21
+ ### Received
22
+ - `ln-stat:set-count` `{ count }` — coordinator delivers this; component sets `textContent` and removes `is-loading` class.
23
+
24
+ ---
25
+
26
+ ## Example
27
+
28
+ ```html
29
+ <div data-ln-data-coordinator>
30
+ <div data-ln-data-store="tenants"></div>
31
+ <div data-ln-api-connector data-ln-api-endpoint="/api/tenants"></div>
32
+
33
+ <!-- Total count -->
34
+ <strong data-ln-stat="tenants"></strong> tenants
35
+
36
+ <!-- Filtered count -->
37
+ <strong data-ln-stat="tenants" data-ln-stat-filter="active:true" class="is-loading"></strong> active
38
+ </div>
39
+ ```
40
+
41
+ The `is-loading` class is removed on first delivery — add it to show a loading placeholder.
42
+
43
+ ---
44
+
45
+ ## Notes
46
+
47
+ - ln-stat never imports or reads `ln-data-store` directly — it is a pure event client.
48
+ - The coordinator re-serves `ln-stat:set-count` on every store mutation.
49
+ - `data-ln-stat-filter` supports one `field:value` pair. The value is passed as `[value]` (array) to `store.count(filters)`.
50
+
51
+ ---
52
+
53
+ ## 🔧 Internals
54
+
55
+ Source: `js/ln-stat/ln-stat.js`. The coordinator's `_serveStat` method handles `ln-stat:request-count`: reads `data-ln-stat` from the dispatching element, guards with `_ownsStore(name)`, calls `store.count(filters)` (`filters` from `e.detail.filters`), and dispatches `ln-stat:set-count { count }` back on the element. On every store mutation, the coordinator's `_refreshAll()` re-parses `data-ln-stat-filter` from each stat element and re-calls `store.count()` — the component itself holds no count state between deliveries.
@@ -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 q(t,e){w[t]||(w[t]=document.querySelector('[data-ln-template="'+t+'"]'));const o=w[t];return o?o.content.cloneNode(!0):(console.warn("["+(e||"ln-core")+'] Template "'+t+'" not found'),null)}function D(t,e,o){t.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:o||{}}))}function A(t,e){if(!t||!e)return t;const o=t.querySelectorAll("[data-ln-field]");for(let n=0;n<o.length;n++){const i=o[n],l=i.getAttribute("data-ln-field");e[l]!=null&&(i.textContent=e[l])}const u=t.querySelectorAll("[data-ln-attr]");for(let n=0;n<u.length;n++){const i=u[n],l=i.getAttribute("data-ln-attr").split(",");for(let c=0;c<l.length;c++){const d=l[c].trim().split(":");if(d.length!==2)continue;const a=d[0].trim(),f=d[1].trim();e[f]!=null&&i.setAttribute(a,e[f])}}const r=t.querySelectorAll("[data-ln-show]");for(let n=0;n<r.length;n++){const i=r[n],l=i.getAttribute("data-ln-show");l in e&&i.classList.toggle("hidden",!e[l])}const s=t.querySelectorAll("[data-ln-class]");for(let n=0;n<s.length;n++){const i=s[n],l=i.getAttribute("data-ln-class").split(",");for(let c=0;c<l.length;c++){const d=l[c].trim().split(":");if(d.length!==2)continue;const a=d[0].trim(),f=d[1].trim();f in e&&i.classList.toggle(a,!!e[f])}}return t}function N(t,e){t.matches&&t.matches("[data-ln-form], [data-ln-fillable]")&&t.dispatchEvent(new CustomEvent("ln-fill",{detail:e??null,bubbles:!0}));const o=t.querySelectorAll("[data-ln-form], [data-ln-fillable]");for(let u=0;u<o.length;u++)o[u].dispatchEvent(new CustomEvent("ln-fill",{detail:e??null,bubbles:!0}));return t}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore._fillBound||(window.lnCore._fillBound=!0,document.addEventListener("ln-fill",function(t){if(!(!t.target.matches||!t.target.matches("[data-ln-fillable]")))if(t.detail)A(t.target,t.detail);else{const e=t.target.querySelectorAll("[data-ln-field]");for(let o=0;o<e.length;o++)e[o].textContent=""}})));function L(t,e){if(!t||!e)return t;const o=document.createTreeWalker(t,NodeFilter.SHOW_TEXT);for(;o.nextNode();){const s=o.currentNode;s.textContent.indexOf("{{")!==-1&&(s.textContent=s.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(n,i){return e[i]!==void 0?e[i]:""}))}const u=function(s,n){return e[n]!==void 0?e[n]:""},r=Array.from(t.querySelectorAll("*"));t.nodeType===1&&r.push(t);for(let s=0;s<r.length;s++){const n=r[s],i=n.attributes;for(let l=0;l<i.length;l++){const c=i[l];c.value.indexOf("{{")!==-1&&n.setAttribute(c.name,c.value.replace(/\{\{\s*(\w+)\s*\}\}/g,u))}}return t}function M(t,e,o,u,r,s){const n={};for(let l=0;l<t.children.length;l++){const c=t.children[l],d=c.getAttribute("data-ln-key");d&&(n[d]=c)}const i=document.createDocumentFragment();for(let l=0;l<e.length;l++){const c=e[l],d=String(u(c));let a=n[d];if(a)r(a,c,l);else{const f=q(o,s);if(!f||(L(f,c),a=f.firstElementChild,!a))continue;a.setAttribute("data-ln-key",d),r(a,c,l)}i.appendChild(a)}t.textContent="",t.appendChild(i)}function S(t,e){if(!document.body){document.addEventListener("DOMContentLoaded",function(){S(t,e)}),console.warn("["+e+'] Script loaded before <body> — add "defer" to your <script> tag');return}t()}function g(t,e,o,u){if(t.nodeType!==1)return;const s=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]",n=Array.from(t.querySelectorAll(s));t.matches&&t.matches(s)&&n.push(t);for(const i of n)i[o]||(i[o]=new u(i))}function j(t,e,o,u,r={}){const s=r.extraAttributes||[],n=r.onAttributeChange||null,i=r.onInit||null;function l(c){const d=c||document.body;g(d,t,e,o),i&&i(d)}return S(function(){const c=new MutationObserver(function(a){for(let f=0;f<a.length;f++){const h=a[f];if(h.type==="childList"){for(let m=0;m<h.addedNodes.length;m++){const p=h.addedNodes[m];p.nodeType===1&&(g(p,t,e,o),i&&i(p))}for(let m=0;m<h.removedNodes.length;m++){const p=h.removedNodes[m];if(p.nodeType===1){const k=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",b=Array.from(p.querySelectorAll(k));p.matches&&p.matches(k)&&b.push(p);for(let C=0;C<b.length;C++){const T=b[C];if(!document.contains(T)){const x=T[e];x&&typeof x.destroy=="function"&&x.destroy()}}}}}else h.type==="attributes"&&(n&&h.target[e]?n(h.target,h.attributeName):(g(h.target,t,e,o),i&&i(h.target)))}});let d=[];if(t.indexOf("[")!==-1){const a=/\[([\w-]+)/g;let f;for(;(f=a.exec(t))!==null;)d.push(f[1])}else d.push(t);c.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:d.concat(s)})},u),window[e]=l,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){l(document.body)}):l(document.body),l}const v={};function F(t,e){v[t]=e}function W(t){return v[t]||{ingress:e=>e,egress:e=>e}}const _={};function O(t,e){if(!t||typeof e!="object")return;const o=t.toLowerCase().split("-")[0];_[o]=e}function I(t){if(!t)return null;const e=t.toLowerCase().split("-")[0];return _[e]||null}O("mk",{monthsLong:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"],monthsShort:["јан","фев","мар","апр","мај","јун","јул","авг","септ","окт","ноем","дек"],daysLong:["недела","понеделник","вторник","среда","четврток","петок","сабота"],daysShort:["нед","пон","вт","ср","чет","пет","саб"]}),typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=F,window.lnCore.getDataMapper=W,window.lnCore.registerLocaleFallback=O,window.lnCore.getLocaleFallback=I,window.lnCore.fillTemplate=L,window.lnCore.fill=A,window.lnCore.lnFill=N,window.lnCore.renderList=M);function E(t){return(t||"").replace(/^#/,"")}function y(t){const e=t===void 0?location.hash:t,o={},u=E(e);if(!u)return o;const r=u.split("&");for(let s=0;s<r.length;s++){const n=r[s];if(!n)continue;const i=n.indexOf(":"),l=i>-1?n.slice(0,i):n,c=i>-1?n.slice(i+1):"";if(l)try{o[l]=decodeURIComponent(c)}catch{o[l]=c}}return o}function B(t){if(!t)return null;const e=y();return t in e?e[t]:null}function K(t,e){if(!t)return;const o=y();e==null?delete o[t]:o[t]=String(e);const r=Object.keys(o).map(function(s){const n=o[s];return n===""?s:s+":"+encodeURIComponent(n)}).join("&");E(location.hash)!==r&&(location.hash=r)}function R(t){return t.button===1||t.ctrlKey||t.metaKey||t.shiftKey?!1:(t.preventDefault(),!0)}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.hashParse=y,window.lnCore.hashGet=B,window.lnCore.hashSet=K,window.lnCore.hashLinkClick=R),(function(){const t="data-ln-stat",e="lnStat";if(window[e]!==void 0)return;function o(r){if(!r)return null;const s=r.indexOf(":");if(s===-1)return null;const n=r.slice(0,s),i=r.slice(s+1),l={};return l[n]=[i],l}function u(r){return this.dom=r,this._storeName=r.getAttribute(t),this._filters=o(r.getAttribute("data-ln-stat-filter")),this._onSetCount=function(s){r.textContent=String(s.detail.count),r.classList.remove("is-loading")},r.addEventListener("ln-stat:set-count",this._onSetCount),D(r,"ln-stat:request-count",{stat:this._storeName,filters:this._filters}),this}u.prototype.destroy=function(){this.dom[e]&&(this.dom.removeEventListener("ln-stat:set-count",this._onSetCount),delete this.dom[e])},j(t,e,u,"ln-stat")})()})();
@@ -0,0 +1,55 @@
1
+ import { registerComponent, dispatch } from '../../ln-core';
2
+
3
+ (function () {
4
+ const DOM_SELECTOR = 'data-ln-stat';
5
+ const DOM_ATTRIBUTE = 'lnStat';
6
+
7
+ if (window[DOM_ATTRIBUTE] !== undefined) return;
8
+
9
+ // ─── Filter Parser ─────────────────────────────────────────
10
+
11
+ function _parseFilter(raw) {
12
+ if (!raw) return null;
13
+ const colonIdx = raw.indexOf(':');
14
+ if (colonIdx === -1) return null;
15
+ const field = raw.slice(0, colonIdx);
16
+ const val = raw.slice(colonIdx + 1);
17
+ const filters = {};
18
+ filters[field] = [val];
19
+ return filters;
20
+ }
21
+
22
+ // ─── Component ─────────────────────────────────────────────
23
+
24
+ function _component(dom) {
25
+ this.dom = dom;
26
+ this._storeName = dom.getAttribute(DOM_SELECTOR);
27
+ this._filters = _parseFilter(dom.getAttribute('data-ln-stat-filter'));
28
+
29
+ const self = this;
30
+
31
+ this._onSetCount = function (e) {
32
+ dom.textContent = String(e.detail.count);
33
+ dom.classList.remove('is-loading');
34
+ };
35
+ dom.addEventListener('ln-stat:set-count', this._onSetCount);
36
+
37
+ // Request initial count from coordinator
38
+ dispatch(dom, 'ln-stat:request-count', {
39
+ stat: this._storeName,
40
+ filters: this._filters
41
+ });
42
+
43
+ return this;
44
+ }
45
+
46
+ _component.prototype.destroy = function () {
47
+ if (!this.dom[DOM_ATTRIBUTE]) return;
48
+ this.dom.removeEventListener('ln-stat:set-count', this._onSetCount);
49
+ delete this.dom[DOM_ATTRIBUTE];
50
+ };
51
+
52
+ // ─── Init ──────────────────────────────────────────────────
53
+
54
+ registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-stat');
55
+ })();
@@ -1,101 +1,285 @@
1
- # ln-table
2
-
3
- A zero-dependency, high-performance **Server-Rendered Table Primitive** that enhances standard, pre-populated HTML tables in-place. It parses existing DOM `<tbody>` elements once on load and layers ultra-fast, client-side in-memory sorting, column filtering, text searches, and scroll virtualization on top of pre-rendered markup.
4
-
5
- ---
6
-
7
- ## 🧭 Philosophy & Architecture
8
-
9
- 1. **`ln-table` vs `ln-data-table`:**
10
- - Use `ln-table` when rows are already printed in `<tbody>` by your backend server on page load. It parses existing elements and manipulates visibility in-place.
11
- - Use `ln-data-table` when your data source is an active JS array (such as an `ln-store` IndexedDB cache or REST fetch API) and needs to clone structural templates.
12
- 2. **Platform-First Search Indexing:** Search and column filters scan the visible display texts of cells, whereas sorting reads numeric or date values defined in `data-ln-value` overrides.
13
- 3. **Actions Column Exclusivity:** The last cell (`<td>`) of each row is automatically ignored during search indexes. This prevents buttons (e.g. Edit / Delete) from polluting search results.
14
-
15
- ---
16
-
17
- ## 📦 Minimal Blueprint
18
-
19
- ```html
20
- <div id="employees-table" data-ln-table>
21
- <!-- Empty State Template -->
22
- <template data-ln-table-empty>
23
- <article class="ln-table__empty-state">
24
- <h3>No matches found</h3>
25
- <button type="button" data-ln-table-clear>Clear filters</button>
26
- </article>
27
- </template>
28
-
29
- <table>
30
- <thead>
31
- <tr>
32
- <th data-ln-sort="string">Name</th>
33
- <th data-ln-sort="number">Salary</th>
34
- </tr>
35
- </thead>
36
- <tbody>
37
- <tr>
38
- <td>Dalibor Sojic</td>
39
- <td data-ln-value="120000">$120,000</td>
40
- </tr>
41
- </tbody>
42
- </table>
43
- </div>
44
- ```
45
-
46
- ---
47
-
48
- ## 🛠️ Declarative API Contract
49
-
50
- ### HTML Attributes
51
-
52
- | Attribute | Elements | Description |
53
- | :--- | :--- | :--- |
54
- | `data-ln-table` | `<div>` wrapper | Root marker. Target must carry a unique `id`. |
55
- | `data-ln-sort="type"` | `<th>` | Marks column sortable. Values: `string`, `number`, `date`. |
56
- | `data-ln-value="raw"` | `<td>` | Raw value used exclusively by sort comparators. |
57
- | `data-ln-filter-col="key"` | `<th>` | Maps the column to an active `ln-filter` key. |
58
- | `data-ln-table-clear` | `<button>` | Click delegate. Instantly clears search query and active filters. |
59
-
60
- ### JS API
61
-
62
- Access the parsed database model directly via the `lnTable` property on the wrapper element:
63
-
64
- ```javascript
65
- const table = document.getElementById('employees-table');
66
-
67
- // 1. Inspect parsed arrays
68
- const cachedRows = table.lnTable._data; // All parsed DOM rows
69
- const visibleRows = table.lnTable._filteredData; // Current matches
70
-
71
- // 2. Tear down listeners and unlock column widths
72
- table.lnTable.destroy();
73
- ```
74
-
75
- ---
76
-
77
- ## ⚡ DOM Events
78
-
79
- ### Emitted
80
-
81
- | Event | Payload | Description |
82
- | :--- | :--- | :--- |
83
- | `ln-table:ready` | `{ total }` | Dispatched after the initial DOM parse is complete. |
84
- | `ln-table:filter` | `{ term, matched, total }` | Dispatched after any search or filter state updates. |
85
- | `ln-table:sorted` | `{ column, direction }` | Dispatched after sorting is recalculated in-place. |
86
-
87
- ### Received
88
-
89
- `ln-table` coordinates with sibling UI primitives through standard DOM event channels:
90
-
91
- - **Listens to `ln-search:change`:** Updates text queries and filters the DOM.
92
- - **Listens to `ln-table:sort`:** Triggers structural sorting in-place.
93
- - **Listens to `ln-filter:changed`:** Triggers column filters and highlights headers.
94
-
95
- ---
96
-
97
- ## ⚠️ Common Pitfalls
98
-
99
- - **Bypassing the ID Requirement:** Sibling search controls (`ln-search`) target wrappers by calling `getElementById(targetId)`. The table wrapper **must** carry a unique `id`.
100
- - **Dynamic Row Replacements:** Modifying cell values after initial paint using `innerHTML` will be lost. The script caches rows as static strings on page load. Use `ln-data-table` instead for dynamic writes.
101
- - **Scroll Container Wrapping:** Do not wrap tables in scroll trapping boxes (`overflow-y: auto`). Scroll virtualization and sticky headers automatically bind to the outer window scroll bounds.
1
+ # ln-table
2
+
3
+ A zero-dependency, high-performance table presenter component that supports both Server-Rendered (SSR) Mode and Data-Driven Mode in `ln-ashlar`.
4
+
5
+ ---
6
+
7
+ ## 🧭 Philosophical Modes
8
+
9
+ 1. **Server-Rendered (SSR) Mode**:
10
+ Hydrates standard, backend-printed HTML tables in-place. Reads the existing `<tbody>` rows once on load and layers sorting, filtering, text searches, and scroll virtualization on top of pre-rendered markup.
11
+ 2. **Data-Driven Mode**:
12
+ Operates as a dynamic presenter engine. Clones templates, interpolates double-curly braces (`{{ field }}`), manages checkbox selection lists, updates footers, and coordinates dataset requests via AJAX coordinators.
13
+
14
+ ---
15
+
16
+ ## 🏛️ Layer 1 vs Layer 2 Architecture
17
+
18
+ `ln-table` strictly operates as a **Layer 1 Component** (Data & DOM presenter):
19
+ - Manages internal data arrays, virtual scroll rendering, sliding window cache, row template cloning, cell formatting, and selection state.
20
+ - Responds to command/request events (`ln-table:set-search`, `ln-table:set-filter`, `ln-table:set-data`, `ln-table:set-loading`, `ln-table:request-clear-filters`).
21
+ - Emits lifecycle notifications (`ln-table:ready`, `ln-table:rendered`, `ln-table:filter`, `ln-table:sorted`, `ln-table:select`).
22
+
23
+ External UI controls (`ln-search`, `ln-filter`, filter header buttons, clear buttons, keyboard focus shortcuts) are orchestrated by **Layer 2 Coordinator** [`ln-table-coordinator`](../ln-table-coordinator/README.md).
24
+
25
+ ---
26
+
27
+ ## 📦 Minimal Blueprint
28
+
29
+ ### 1. SSR / Markup Mode
30
+ ```html
31
+ <div id="employees-table" data-ln-table>
32
+ <template data-ln-table-empty>
33
+ <article class="ln-table__empty-state">
34
+ <h3>No matches found</h3>
35
+ <button type="button" data-ln-table-clear>Clear filters</button>
36
+ </article>
37
+ </template>
38
+
39
+ <table>
40
+ <thead>
41
+ <tr>
42
+ <th>Name
43
+ <ul data-ln-sort="employees-table"><li><button type="button" data-ln-sort-dir="asc" aria-label="Sort ascending"><svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-arrows-sort"></use></svg></button></li><li><button type="button" data-ln-sort-dir="desc" aria-label="Sort descending"><svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-arrow-up"></use></svg></button></li><li><button type="button" data-ln-sort-dir="none" aria-label="Remove sort"><svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-arrow-down"></use></svg></button></li></ul>
44
+ <!-- Icon mapping is state-based, not direction-based — see js/ln-sort/README.md "Icon convention". -->
45
+ </th>
46
+ <th>Salary</th>
47
+ </tr>
48
+ </thead>
49
+ <tbody>
50
+ <tr>
51
+ <td>Dalibor Sojic</td>
52
+ <td data-ln-value="120000">$120,000</td>
53
+ </tr>
54
+ </tbody>
55
+ </table>
56
+ </div>
57
+ ```
58
+
59
+ ### 2. Data-Driven Mode
60
+ ```html
61
+ <section data-ln-table="products" data-ln-table-source="products" id="products-table">
62
+ <table>
63
+ <thead>
64
+ <tr>
65
+ <th data-ln-table-col="name">Product Name</th>
66
+ <th data-ln-table-col="category">Category</th>
67
+ </tr>
68
+ </thead>
69
+ <tbody data-ln-table-body></tbody>
70
+ </table>
71
+
72
+ <!-- Row Template -->
73
+ <template data-ln-template="products-row">
74
+ <tr data-ln-table-row>
75
+ <td>{{ name }}</td>
76
+ <td>{{ category }}</td>
77
+ </tr>
78
+ </template>
79
+ </section>
80
+ ```
81
+
82
+ > Row templates support `{{ field }}` (text) and
83
+ > `data-ln-table-cell-attr="field:attr"` (attributes) only — both stamped once
84
+ > at clone time. `data-ln-field` is **not** processed in rows (the row
85
+ > pipeline never calls `fill()`); it would sit inert in the DOM.
86
+
87
+ ---
88
+
89
+ ## 🛠️ Attributes Reference
90
+
91
+ | Attribute | Elements | Description |
92
+ | :--- | :--- | :--- |
93
+ | `data-ln-table` | Root wrapper | Component identifier. Target must carry a unique `id`. |
94
+ | `data-ln-table-source` | Root wrapper | Opt-in indicator for Data-Driven Mode. |
95
+ | `data-ln-table-selectable` | Root wrapper | Enables checkbox-based row selections. |
96
+ | `data-ln-table-window="N"` | Root wrapper | Opt-in server-side sliding-window virtualization. `N` sets the resident-row cap (default 1000). Requires Data-Driven Mode. Observable: add/remove toggles windowed mode ON/OFF live; changing `N` while windowed reconfigures the live cache. |
97
+ | ~~`data-ln-table-search`~~ | — | **Removed.** Drive the search input with `data-ln-search="<tableId>"` — `ln-table` consumes `ln-search:change` directly in SSR mode only; data-driven mode relies on `ln-table-coordinator` to translate it into `ln-table:set-search`. |
98
+ | `data-ln-table-col="field"` | `<th>` | Maps column header to data object field keys. |
99
+ | `data-ln-value` | `<td>` | Raw machine value behind a formatted cell — sorting/filtering operate on this, not the displayed text. Read via `ln-core.readValue`. |
100
+ | `data-ln-sort` | `<ul>` inside `<th>` | Sort control — see [`ln-sort`](../ln-sort/README.md). Omit `data-ln-sort-field` on SSR columns (index fallback); set it on data-driven columns (must match `data-ln-table-col`). |
101
+ | `data-ln-table-col-filter` | `<button>` | JS id hook — identifies the filter button in a `<th>`. Pair with `data-ln-popover-for` to open the filter popover. |
102
+ | `data-ln-table-col-select` | `<th>` | Header checkbox column selector. |
103
+ | `data-ln-table-row` | `<tr>` | Target row container in row templates. |
104
+ | `data-ln-table-row-select` | `<input>` | Selection checkbox in row templates. |
105
+ | `data-ln-table-row-action="name"`| `<button>` | Action button trigger in row templates. |
106
+
107
+ ---
108
+
109
+ ## ⚡ DOM Events
110
+
111
+ ### Emitted Events
112
+
113
+ - **`ln-table:ready`** `{ total }`
114
+ Fired after the initial DOM rows are parsed.
115
+ - **`ln-table:request-data`** `{ table, sort, filters, search }`
116
+ Requests a fresh dataset when sort, filter, or search is changed. Windowed mode (`data-ln-table-window`) adds `{ offset, limit, queryGen }`.
117
+ - **`ln-table:rendered`** `{ table, total, visible }`
118
+ Fired after a dynamic template render finishes drawing.
119
+ - **`ln-table:row-click`** `{ table, id, record }`
120
+ Fired when clicking on row contents.
121
+ - **`ln-table:row-action`** `{ table, id, action, record }`
122
+ Fired when clicking row buttons.
123
+ Note: the row-action click handler calls `e.stopPropagation()`, so a `data-ln-table-row-action` button cannot be combined with document-delegated triggers like `data-ln-modal-for` or `data-ln-fill-*` on the same button — the click never reaches those listeners. Pick one mechanism per button.
124
+
125
+ ### Received Events
126
+
127
+ - **`ln-table:set-data`** `{ data, total, filtered, filterOptions }`
128
+ Applies the payload array and triggers rendering. Windowed mode expects `{ offset, queryGen }` echoed back — routed straight into the internal window cache.
129
+ - **`ln-table:set-loading`** `{ loading }`
130
+ Toggles the visual loading dimmed state overlay.
131
+ - **`ln-table:page-failed`** `{ offset }`
132
+ Windowed mode: the coordinator reports the page fetch at `offset` failed. Releases it from the cache's in-flight set — no auto-retry; the next `ensure()` (scroll, filter, resize) requests it again.
133
+ - **`ln-table:request-revalidate`**
134
+ Windowed mode: the coordinator asks the cache to re-fetch the currently visible page after a local mutation. Stale rows stay visible until the response lands; does not jump back to page 0.
135
+
136
+ ---
137
+
138
+ ## Column Filters
139
+
140
+ Column filters use static authored markup — a `[data-ln-popover]` block containing `[data-ln-filter]` checkboxes. `ln-filter` dispatches `ln-filter:changed`; `ln-table` consumes the coordinator's translated `ln-table:set-filter` instead.
141
+
142
+ ### What ln-table does
143
+
144
+ `ln-table` never listens for `ln-filter:changed` itself — no such binding exists in either mode. `ln-filter` dual-dispatches `ln-filter:changed` on both its own `<ul data-ln-filter>` root and directly on the table element; `ln-table-coordinator` receives it there and translates it into the commands `ln-table` actually listens for:
145
+
146
+ 1. `ln-table-coordinator` maps `e.detail.key` to a column via `data-ln-table-filter-col` on `<th>` and toggles `.ln-filter-active` on the funnel `<button>` (the dot indicator).
147
+ 2. `ln-table-coordinator` dispatches `ln-table:set-filter` at the table.
148
+ 3. `ln-table` stores the active filter values in `_columnFilters`.
149
+ 4. **SSR mode**: runs `_applyFilterAndSort()` + `_render()` in-memory.
150
+ 5. **Data-driven mode**: calls `_requestData()`.
151
+ 6. Dispatches `ln-table:filter`.
152
+
153
+ An SSR table using column filters still requires the `[data-ln-table-coordinator]` wrapper — SSR self-binding only covers sort and search (see "Sort integration" below), not filters or clear-all.
154
+
155
+ ### What ln-table does NOT do
156
+
157
+ - Does not generate filter dropdown markup.
158
+ - Does not track which options exist in the dataset.
159
+ - Does not handle mutual exclusion of checkboxes (`ln-filter` owns that).
160
+
161
+ ### Markup contract
162
+
163
+ ```html
164
+ <!-- th attribute maps filter key → column -->
165
+ <th data-ln-table-filter-col="department">
166
+ Department
167
+ <button class="table-filter" type="button"
168
+ data-ln-table-col-filter
169
+ data-ln-popover-for="filter-dept"
170
+ aria-label="Filter department">
171
+ <svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-filter"></use></svg>
172
+ </button>
173
+ </th>
174
+
175
+ <!-- Popover: sibling to [data-ln-table], not inside it -->
176
+ <div data-ln-popover id="filter-dept">
177
+ <input type="search" data-ln-search="filter-dept-list" data-ln-search-items="label" placeholder="Search...">
178
+ <ul id="filter-dept-list" data-ln-filter="my-table">
179
+ <li><label><input type="checkbox" data-ln-filter-key="department" data-ln-filter-reset checked> All</label></li>
180
+ <li><label><input type="checkbox" data-ln-filter-key="department" data-ln-filter-value="Engineering"> Engineering</label></li>
181
+ </ul>
182
+ </div>
183
+ ```
184
+
185
+ Options come from the domain (backend enum or lookup), never derived from the dataset. Include options that may have zero matches — they represent valid domain states.
186
+
187
+ ### Clear-all
188
+
189
+ **SSR mode:** `data-ln-table-clear` on a button inside the table wrapper. Clears search term and all `[data-ln-filter]` containers targeting this table.
190
+
191
+ **Data-driven mode:** `data-ln-table-clear-all` on a button. Resets `currentFilters`, clears visual indicators on all filter buttons, and calls `_requestData()`.
192
+
193
+ ---
194
+
195
+ ## Windowed Mode
196
+
197
+ Opt-in server-side sliding-window virtualization for datasets too large to cache client-side. Add `data-ln-table-window="N"` to a Data-Driven Mode table; the component owns a bounded `ln-core.createWindowCache` instance instead of the full dataset.
198
+
199
+ ### What it does
200
+
201
+ 1. Caches at most `N` rows (default 1000), LRU-evicting the least-recently-touched rows once the cap is exceeded.
202
+ 2. Extends `ln-table:request-data` with `{ offset, limit, queryGen }` on every scroll pass that needs new rows.
203
+ 3. Reads `{ offset, queryGen }` off `ln-table:set-data` and routes it into the window cache, dropping the response if `queryGen` no longer matches the current query.
204
+ 4. Renders blank placeholder rows (no shimmer) for logical rows not yet resident.
205
+ 5. Hides the select-all checkbox (D4) — per-row selection still works and survives eviction.
206
+
207
+ ### Live observability
208
+
209
+ `data-ln-table-window`, `data-ln-table-window-page`, `data-ln-table-window-threshold`, and `data-ln-table-count` are all observed and apply live, without re-init. Adding `data-ln-table-window` to an initialized, non-windowed, data-driven table enables windowing live — the cache seeds from resident rows, honoring `data-ln-table-count` if present. Removing it disables windowing live: the cache is destroyed and the table issues a fresh full `ln-table:request-data` (no `offset`/`limit`) to repopulate the complete dataset before rendering non-windowed again.
210
+
211
+ ### What it does NOT do
212
+
213
+ - Does not cache the full dataset client-side — that's the non-windowed Data-Driven path.
214
+ - Does not paginate — scrolling stays continuous; `offset`/`limit` address server-side pages, not user-facing "Page 2" navigation.
215
+ - Does not retry a dropped stale response — the next `ensure()` pass re-requests it under the current `queryGen`.
216
+
217
+ ### Markup contract
218
+
219
+ ```html
220
+ <section data-ln-table="documents" data-ln-table-source="documents" data-ln-table-window="1000" id="documents-table">
221
+ ...
222
+ </section>
223
+ ```
224
+
225
+ ---
226
+
227
+ ## 🔧 Internals
228
+
229
+ Source: `js/ln-table/ln-table.js`. Sort is a separate, co-loaded component — see `js/ln-sort/ln-sort.js`. Imports from `ln-core`: `cloneTemplateScoped`, `dispatch`, `fill`, `fillTemplate`, `registerComponent`, `createWindowCache`, `readValue`, `detectValueType`, `compareValues`.
230
+
231
+ ### Mode detection & lifecycle
232
+
233
+ - The constructor branches once, at construction: `this.isDataDriven = dom.hasAttribute('data-ln-table-source')`.
234
+ - **SSR**: reads `<tbody>` rows once at bootstrap, caches them as static HTML strings in `_data`, and re-sorts/filters that in-memory cache on `ln-search:change` / `ln-sort:change`. Column-filter values arrive as `ln-table:set-filter` from `ln-table-coordinator`, not as a direct `ln-filter:changed` listener.
235
+ - **Data-driven**: `isLoaded = false` until the first `ln-table:set-data`. If `<tbody>` already has rows (hybrid), they're parsed synchronously first — instant local sort/filter/search response before the authoritative dataset arrives. When the background sync lands, `_vStart`/`_vEnd` reset to `-1` and virtual scroll re-initializes against the full dataset.
236
+ - **Loading dimming**: `ln-table:set-loading {loading:true}` adds `.ln-table--loading` to the wrapper; `ln-table:set-data` clears it automatically.
237
+
238
+ ### DOM mutations
239
+
240
+ | Phase | Mutation |
241
+ |---|---|
242
+ | `set-loading` | `.ln-table--loading` on the wrapper |
243
+ | `set-data` / sort / filter / search | `<tbody>` rows re-rendered (cloned templates in data-driven mode, cached HTML in SSR); footer counters (`data-ln-table-total` / `-filtered` / `-selected`) updated |
244
+ | sort click | `data-ln-sort-state` on the `[data-ln-sort]` element, owned entirely by `ln-sort` — `ln-table` never touches it |
245
+ | filter change | `.ln-filter-active` toggled on the column's filter button |
246
+
247
+ ### Sort integration (`ln-sort`)
248
+
249
+ Sort is fully owned by the standalone [`ln-sort`](../ln-sort/README.md) component — `ln-table`
250
+ only listens for its `ln-sort:change` event on itself, in both modes:
251
+
252
+ - **SSR** — `e.preventDefault()`, re-sorts the in-memory `_data` cache using `row.values[colIndex]`
253
+ (raw `readValue` per cell, type inferred once via `ln-core.detectValueType`), re-renders, keeps
254
+ emitting `ln-table:sorted`.
255
+ - **Data-driven** — `e.preventDefault()`, sets `currentSort = { field, direction }` (or `null` on
256
+ `direction: 'none'`), calls `_requestData()`.
257
+
258
+ `ln-table` never writes `data-ln-sort-state` and never persists sort state — both are `ln-sort`'s
259
+ own responsibility (`data-ln-persist` goes on the `[data-ln-sort]` element itself, not the table
260
+ wrapper — see `js/ln-sort/README.md`).
261
+
262
+ ### Search integration (`ln-search`)
263
+
264
+ SSR mode also self-binds `ln-search:change` directly on itself, mirroring `ln-sort:change` above — an SSR table with a `[data-ln-search="<its own id>"]` input works standalone, no coordinator required. Data-driven mode relies on `ln-table-coordinator` translating `ln-search:change` into `ln-table:set-search` — do not wrap an SSR table in `[data-ln-table-coordinator]`, it will double-process search.
265
+
266
+ ### MutationObserver flow (`ln-table.js`)
267
+
268
+ A single observer on `document.body`: new `data-ln-table` root injected → `new lnTable(node)`; elements injected inside a live root → local rescan (no full re-init); `data-ln-table-source`/`-window` attribute set on an existing element → re-evaluated live (see Windowed Mode below).
269
+
270
+ ### Windowed cache mechanics
271
+
272
+ Backed by `ln-core.createWindowCache` — the table owns the cache instance, not the full dataset:
273
+
274
+ - LRU eviction by touch-recency (not viewport distance) once resident rows exceed `windowSize`.
275
+ - `_renderWindowed()` calls `cache.ensure(startRow, endRow)` on every scroll pass; unresident logical rows render as blank placeholder `<tr>`s (no shimmer) until their page arrives.
276
+ - `requestPage` dispatches `ln-table:request-data` with `{offset, limit, queryGen}`; `cache.ingest()` consumes the matching `ln-table:set-data {offset, queryGen}` and splices the page in. A response whose `queryGen` doesn't match the cache's current generation is dropped — the guard against a stale sort/filter/search response landing after a newer query superseded it.
277
+ - Live reconfiguration routes through `configure()`/`setGrandTotal()`: enabling `data-ln-table-window` on an initialized non-windowed table seeds the cache from resident rows (honoring `data-ln-table-count` if present, else the resident count); removing it destroys the cache and re-issues a full `ln-table:request-data` (no `offset`/`limit`) to repopulate non-windowed.
278
+
279
+ ### Filter options — `{value, label}` shape
280
+
281
+ `filterOptions` in `ln-table:set-data` accepts per-field arrays mixing plain strings (`'Draft'` — used as both raw value and label) and `{value, label}` objects (`{value:'true', label:'Active'}` — label renders in the dropdown, raw `value` is echoed verbatim in `ln-table:request-data` filters). This lets a column filter on a raw boolean/enum field (`data-ln-table-col="active"`) while displaying a human-readable computed field in the row template (`{{ status_display }}`) — no app-side label↔value translation needed.
282
+
283
+ ### Sticky header/footer
284
+
285
+ `@include ln-table-toolbar` / `@include ln-table-footer` mixins (in `scss/components/_ln-table.scss`) pin the header/footer flush at the scroll container's bounds, with fallback rules targeting direct-child `[data-ln-table] > header` / `> footer`.
@@ -0,0 +1,17 @@
1
+ @use '../../scss/config/mixins' as *;
2
+
3
+ [data-ln-debug] {
4
+ // Table Integration: missing ID warning
5
+ // Warns if a table wrapper [data-ln-table] is missing an ID, which breaks search/filter integration
6
+ [data-ln-table]:not([id]) {
7
+ @include dev-dom-error("[data-ln-table] is missing a required 'id' attribute for filter mapping.");
8
+ }
9
+
10
+ // Legacy sort markup warning
11
+ // These hooks are read only by the retired ln-table-sort.js — ln-table.js ignores them
12
+ th[data-ln-table-sort],
13
+ th:has([data-ln-table-col-sort]),
14
+ th:has(.table-sort) {
15
+ @include dev-inline-error("LEGACY SORT MARKUP — use [data-ln-sort]");
16
+ }
17
+ }