@livenetworks/ashlar 1.3.2

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 (232) hide show
  1. package/README.md +177 -0
  2. package/js/COMPONENTS.md +1102 -0
  3. package/js/index.js +41 -0
  4. package/js/ln-accordion/README.md +137 -0
  5. package/js/ln-accordion/ln-accordion.js +1 -0
  6. package/js/ln-accordion/src/ln-accordion.js +41 -0
  7. package/js/ln-ajax/README.md +91 -0
  8. package/js/ln-ajax/ln-ajax.js +1 -0
  9. package/js/ln-ajax/src/ln-ajax.js +277 -0
  10. package/js/ln-api-connector/README.md +150 -0
  11. package/js/ln-api-connector/ln-api-connector.js +1 -0
  12. package/js/ln-api-connector/src/ln-api-connector.js +265 -0
  13. package/js/ln-autoresize/README.md +80 -0
  14. package/js/ln-autoresize/ln-autoresize.js +1 -0
  15. package/js/ln-autoresize/src/ln-autoresize.js +47 -0
  16. package/js/ln-autosave/README.md +92 -0
  17. package/js/ln-autosave/ln-autosave.js +1 -0
  18. package/js/ln-autosave/src/ln-autosave.js +147 -0
  19. package/js/ln-circular-progress/README.md +161 -0
  20. package/js/ln-circular-progress/ln-circular-progress.js +1 -0
  21. package/js/ln-circular-progress/src/ln-circular-progress.js +133 -0
  22. package/js/ln-confirm/README.md +86 -0
  23. package/js/ln-confirm/_ln-confirm.scss +13 -0
  24. package/js/ln-confirm/ln-confirm.js +1 -0
  25. package/js/ln-confirm/src/ln-confirm.js +131 -0
  26. package/js/ln-core/crypto.js +83 -0
  27. package/js/ln-core/helpers.js +411 -0
  28. package/js/ln-core/index.js +5 -0
  29. package/js/ln-core/persist.js +71 -0
  30. package/js/ln-core/positioning.js +207 -0
  31. package/js/ln-core/reactive.js +74 -0
  32. package/js/ln-couchdb-connector/README.md +156 -0
  33. package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -0
  34. package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +348 -0
  35. package/js/ln-data-coordinator/README.md +165 -0
  36. package/js/ln-data-coordinator/ln-data-coordinator.js +1 -0
  37. package/js/ln-data-coordinator/src/ln-data-coordinator.js +249 -0
  38. package/js/ln-data-store/README.md +94 -0
  39. package/js/ln-data-store/ln-data-store.js +1 -0
  40. package/js/ln-data-store/src/ln-data-store.js +699 -0
  41. package/js/ln-data-table/README.md +110 -0
  42. package/js/ln-data-table/ln-data-table.js +1 -0
  43. package/js/ln-data-table/ln-data-table.scss +10 -0
  44. package/js/ln-data-table/src/ln-data-table.js +1103 -0
  45. package/js/ln-date/README.md +151 -0
  46. package/js/ln-date/ln-date.js +1 -0
  47. package/js/ln-date/src/ln-date.js +442 -0
  48. package/js/ln-dropdown/README.md +117 -0
  49. package/js/ln-dropdown/ln-dropdown.js +1 -0
  50. package/js/ln-dropdown/ln-dropdown.scss +15 -0
  51. package/js/ln-dropdown/src/ln-dropdown.js +174 -0
  52. package/js/ln-external-links/README.md +341 -0
  53. package/js/ln-external-links/ln-external-links.js +1 -0
  54. package/js/ln-external-links/src/ln-external-links.js +116 -0
  55. package/js/ln-filter/README.md +99 -0
  56. package/js/ln-filter/ln-filter.js +1 -0
  57. package/js/ln-filter/ln-filter.scss +7 -0
  58. package/js/ln-filter/src/ln-filter.js +404 -0
  59. package/js/ln-form/README.md +101 -0
  60. package/js/ln-form/ln-form.js +1 -0
  61. package/js/ln-form/src/ln-form.js +199 -0
  62. package/js/ln-http/README.md +89 -0
  63. package/js/ln-http/ln-http.js +1 -0
  64. package/js/ln-http/src/ln-http.js +219 -0
  65. package/js/ln-icons/README.md +88 -0
  66. package/js/ln-icons/ln-icons.js +1 -0
  67. package/js/ln-icons/src/ln-icons.js +169 -0
  68. package/js/ln-link/README.md +303 -0
  69. package/js/ln-link/ln-link.js +1 -0
  70. package/js/ln-link/src/ln-link.js +196 -0
  71. package/js/ln-modal/README.md +154 -0
  72. package/js/ln-modal/ln-modal.js +1 -0
  73. package/js/ln-modal/ln-modal.scss +11 -0
  74. package/js/ln-modal/src/ln-modal.js +201 -0
  75. package/js/ln-nav/README.md +70 -0
  76. package/js/ln-nav/ln-nav.js +1 -0
  77. package/js/ln-nav/src/ln-nav.js +177 -0
  78. package/js/ln-number/README.md +122 -0
  79. package/js/ln-number/ln-number.js +1 -0
  80. package/js/ln-number/src/ln-number.js +302 -0
  81. package/js/ln-popover/README.md +127 -0
  82. package/js/ln-popover/ln-popover.js +1 -0
  83. package/js/ln-popover/src/ln-popover.js +288 -0
  84. package/js/ln-progress/README.md +442 -0
  85. package/js/ln-progress/ln-progress.js +1 -0
  86. package/js/ln-progress/src/ln-progress.js +150 -0
  87. package/js/ln-search/README.md +83 -0
  88. package/js/ln-search/ln-search.js +1 -0
  89. package/js/ln-search/ln-search.scss +7 -0
  90. package/js/ln-search/src/ln-search.js +114 -0
  91. package/js/ln-sortable/README.md +95 -0
  92. package/js/ln-sortable/ln-sortable.js +1 -0
  93. package/js/ln-sortable/src/ln-sortable.js +203 -0
  94. package/js/ln-table/README.md +101 -0
  95. package/js/ln-table/ln-table-sort.js +1 -0
  96. package/js/ln-table/ln-table.js +1 -0
  97. package/js/ln-table/ln-table.scss +11 -0
  98. package/js/ln-table/src/ln-table-sort.js +168 -0
  99. package/js/ln-table/src/ln-table.js +473 -0
  100. package/js/ln-tabs/README.md +137 -0
  101. package/js/ln-tabs/ln-tabs.js +1 -0
  102. package/js/ln-tabs/src/ln-tabs.js +171 -0
  103. package/js/ln-time/README.md +81 -0
  104. package/js/ln-time/ln-time.js +1 -0
  105. package/js/ln-time/src/ln-time.js +192 -0
  106. package/js/ln-toast/README.md +122 -0
  107. package/js/ln-toast/ln-toast.js +15 -0
  108. package/js/ln-toast/src/ln-toast.js +210 -0
  109. package/js/ln-toast/template.html +14 -0
  110. package/js/ln-toggle/README.md +137 -0
  111. package/js/ln-toggle/ln-toggle.js +1 -0
  112. package/js/ln-toggle/src/ln-toggle.js +139 -0
  113. package/js/ln-tooltip/README.md +58 -0
  114. package/js/ln-tooltip/ln-tooltip.js +1 -0
  115. package/js/ln-tooltip/ln-tooltip.scss +9 -0
  116. package/js/ln-tooltip/src/ln-tooltip.js +169 -0
  117. package/js/ln-translations/README.md +96 -0
  118. package/js/ln-translations/ln-translations.js +1 -0
  119. package/js/ln-translations/src/ln-translations.js +275 -0
  120. package/js/ln-upload/README.md +180 -0
  121. package/js/ln-upload/ln-upload.js +1 -0
  122. package/js/ln-upload/ln-upload.scss +20 -0
  123. package/js/ln-upload/src/ln-upload.js +407 -0
  124. package/js/ln-validate/README.md +108 -0
  125. package/js/ln-validate/ln-validate.js +1 -0
  126. package/js/ln-validate/src/ln-validate.js +160 -0
  127. package/package.json +55 -0
  128. package/scss/base/_global.scss +83 -0
  129. package/scss/base/_reset.scss +17 -0
  130. package/scss/base/_typography.scss +125 -0
  131. package/scss/components/_accordion.scss +34 -0
  132. package/scss/components/_ajax.scss +15 -0
  133. package/scss/components/_alert.scss +5 -0
  134. package/scss/components/_app-shell.scss +15 -0
  135. package/scss/components/_avatar.scss +6 -0
  136. package/scss/components/_breadcrumbs.scss +33 -0
  137. package/scss/components/_button.scss +20 -0
  138. package/scss/components/_card.scss +10 -0
  139. package/scss/components/_chip.scss +5 -0
  140. package/scss/components/_circular-progress.scss +29 -0
  141. package/scss/components/_confirm.scss +5 -0
  142. package/scss/components/_data-table.scss +83 -0
  143. package/scss/components/_dropdown.scss +25 -0
  144. package/scss/components/_empty-state.scss +22 -0
  145. package/scss/components/_form.scss +100 -0
  146. package/scss/components/_layout.scss +8 -0
  147. package/scss/components/_link.scss +11 -0
  148. package/scss/components/_ln-table.scss +60 -0
  149. package/scss/components/_loader.scss +6 -0
  150. package/scss/components/_modal.scss +20 -0
  151. package/scss/components/_nav.scss +9 -0
  152. package/scss/components/_page-header.scss +10 -0
  153. package/scss/components/_popover.scss +10 -0
  154. package/scss/components/_progress.scss +17 -0
  155. package/scss/components/_prose.scss +5 -0
  156. package/scss/components/_scrollbar.scss +32 -0
  157. package/scss/components/_sections.scss +12 -0
  158. package/scss/components/_sidebar.scss +5 -0
  159. package/scss/components/_stat-card.scss +5 -0
  160. package/scss/components/_status-badge.scss +4 -0
  161. package/scss/components/_stepper.scss +5 -0
  162. package/scss/components/_table.scss +19 -0
  163. package/scss/components/_tabs.scss +21 -0
  164. package/scss/components/_timeline.scss +14 -0
  165. package/scss/components/_toast.scss +41 -0
  166. package/scss/components/_toggle.scss +81 -0
  167. package/scss/components/_tooltip.scss +18 -0
  168. package/scss/components/_translations.scss +111 -0
  169. package/scss/components/_upload.scss +51 -0
  170. package/scss/config/_breakpoints.scss +72 -0
  171. package/scss/config/_density.scss +117 -0
  172. package/scss/config/_icons.scss +37 -0
  173. package/scss/config/_mixins.scss +13 -0
  174. package/scss/config/_theme.scss +216 -0
  175. package/scss/config/_tokens.scss +419 -0
  176. package/scss/config/mixins/_accordion.scss +52 -0
  177. package/scss/config/mixins/_ajax.scss +39 -0
  178. package/scss/config/mixins/_alert.scss +82 -0
  179. package/scss/config/mixins/_app-shell.scss +312 -0
  180. package/scss/config/mixins/_avatar.scss +109 -0
  181. package/scss/config/mixins/_borders.scss +36 -0
  182. package/scss/config/mixins/_breadcrumbs.scss +72 -0
  183. package/scss/config/mixins/_breakpoints.scss +62 -0
  184. package/scss/config/mixins/_btn.scss +179 -0
  185. package/scss/config/mixins/_card.scss +338 -0
  186. package/scss/config/mixins/_chip.scss +66 -0
  187. package/scss/config/mixins/_circular-progress.scss +71 -0
  188. package/scss/config/mixins/_collapsible.scss +24 -0
  189. package/scss/config/mixins/_colors.scss +46 -0
  190. package/scss/config/mixins/_confirm.scss +31 -0
  191. package/scss/config/mixins/_data-table.scss +346 -0
  192. package/scss/config/mixins/_display.scss +32 -0
  193. package/scss/config/mixins/_dropdown.scss +143 -0
  194. package/scss/config/mixins/_empty-state.scss +30 -0
  195. package/scss/config/mixins/_focus.scss +55 -0
  196. package/scss/config/mixins/_footer.scss +42 -0
  197. package/scss/config/mixins/_form.scss +601 -0
  198. package/scss/config/mixins/_index.scss +58 -0
  199. package/scss/config/mixins/_interaction.scss +15 -0
  200. package/scss/config/mixins/_kbd.scss +22 -0
  201. package/scss/config/mixins/_layout.scss +117 -0
  202. package/scss/config/mixins/_link.scss +55 -0
  203. package/scss/config/mixins/_ln-table.scss +420 -0
  204. package/scss/config/mixins/_loader.scss +26 -0
  205. package/scss/config/mixins/_modal.scss +66 -0
  206. package/scss/config/mixins/_motion.scss +19 -0
  207. package/scss/config/mixins/_nav.scss +273 -0
  208. package/scss/config/mixins/_page-header.scss +69 -0
  209. package/scss/config/mixins/_popover.scss +25 -0
  210. package/scss/config/mixins/_position.scss +32 -0
  211. package/scss/config/mixins/_progress.scss +56 -0
  212. package/scss/config/mixins/_prose.scss +127 -0
  213. package/scss/config/mixins/_shadows.scss +8 -0
  214. package/scss/config/mixins/_sidebar.scss +95 -0
  215. package/scss/config/mixins/_sizing.scss +6 -0
  216. package/scss/config/mixins/_spacing.scss +19 -0
  217. package/scss/config/mixins/_stat-card.scss +68 -0
  218. package/scss/config/mixins/_status-badge.scss +83 -0
  219. package/scss/config/mixins/_stepper.scss +78 -0
  220. package/scss/config/mixins/_table.scss +215 -0
  221. package/scss/config/mixins/_tabs.scss +64 -0
  222. package/scss/config/mixins/_timeline.scss +69 -0
  223. package/scss/config/mixins/_toast.scss +148 -0
  224. package/scss/config/mixins/_tooltip.scss +111 -0
  225. package/scss/config/mixins/_transitions.scss +10 -0
  226. package/scss/config/mixins/_translations.scss +124 -0
  227. package/scss/config/mixins/_typography.scss +57 -0
  228. package/scss/config/mixins/_upload.scss +168 -0
  229. package/scss/ln-ashlar.scss +62 -0
  230. package/scss/tabler-icons.txt +5039 -0
  231. package/scss/utilities/_animations.scss +83 -0
  232. package/scss/utilities/_utilities.scss +49 -0
package/js/index.js ADDED
@@ -0,0 +1,41 @@
1
+ // SCSS Framework — master entry (tokens, base, components, layouts, utilities + JS component styles)
2
+ import '../scss/ln-ashlar.scss';
3
+
4
+ // JS Components
5
+ import './ln-http/src/ln-http.js';
6
+ import './ln-ajax/src/ln-ajax.js';
7
+ import './ln-modal/src/ln-modal.js';
8
+ import './ln-number/src/ln-number.js';
9
+ import './ln-date/src/ln-date.js';
10
+ import './ln-nav/src/ln-nav.js';
11
+ import './ln-tabs/src/ln-tabs.js';
12
+ import './ln-toggle/src/ln-toggle.js';
13
+ import './ln-accordion/src/ln-accordion.js';
14
+ import './ln-dropdown/src/ln-dropdown.js';
15
+ import './ln-popover/src/ln-popover.js';
16
+ import './ln-tooltip/src/ln-tooltip.js';
17
+ import './ln-toast/src/ln-toast.js';
18
+ import './ln-upload/src/ln-upload.js';
19
+ import './ln-external-links/src/ln-external-links.js';
20
+ import './ln-link/src/ln-link.js';
21
+ import './ln-progress/src/ln-progress.js';
22
+ import './ln-filter/src/ln-filter.js';
23
+ import './ln-search/src/ln-search.js';
24
+ import './ln-table/src/ln-table-sort.js';
25
+ import './ln-table/src/ln-table.js';
26
+ import './ln-circular-progress/src/ln-circular-progress.js';
27
+ import './ln-sortable/src/ln-sortable.js';
28
+ import './ln-confirm/src/ln-confirm.js';
29
+ import './ln-translations/src/ln-translations.js';
30
+ import './ln-autosave/src/ln-autosave.js';
31
+ import './ln-autoresize/src/ln-autoresize.js';
32
+ import './ln-validate/src/ln-validate.js';
33
+ import './ln-form/src/ln-form.js';
34
+ import './ln-time/src/ln-time.js';
35
+ import './ln-data-store/src/ln-data-store.js';
36
+ import './ln-api-connector/src/ln-api-connector.js';
37
+ import './ln-couchdb-connector/src/ln-couchdb-connector.js';
38
+ import './ln-data-coordinator/src/ln-data-coordinator.js';
39
+ import './ln-data-table/src/ln-data-table.js';
40
+ import './ln-icons/src/ln-icons.js';
41
+
@@ -0,0 +1,137 @@
1
+ # ln-accordion
2
+
3
+ > A lightweight, stateless **Coordinator** that enforces a single-open rule across a list of independent `ln-toggle` panels.
4
+
5
+ ---
6
+
7
+ ## 1. Philosophy & The Coordinator Mindset
8
+
9
+ In traditional frontend architectures, an accordion is a heavy, monolithic component that owns click events, height transitions, ARIA states, active-panel state, and storage persistence.
10
+
11
+ In `ln-ashlar`, `ln-accordion` is fundamentally different: **it is a pure coordinator**. It contains only 38 lines of JavaScript, carries zero internal state, and coordinates other highly-specialized primitives.
12
+
13
+ Every accordion is a perfect synchronization of three orthogonal concerns:
14
+
15
+ 1. **State Primitive (`ln-toggle`)**: Each panel is an independent `ln-toggle` instance. It owns the binary `open`/`close` state, coordinates trigger buttons, synchronizes `aria-expanded`/`aria-controls`, and handles per-panel localStorage persistence (`data-ln-persist`). It is completely oblivious to the other panels or the fact that it is inside an accordion.
16
+ 2. **Animation Engine (CSS `.collapsible`)**: The height transition is handled entirely in Vanilla CSS via the `.collapsible` mixin (transitioning `grid-template-rows` from `0fr` to `1fr`). No JS framerate stuttering or inline height hacks.
17
+ 3. **The Coordinator (`ln-accordion`)**: Lives on the wrapper element. It enforces a single rule: *"When one panel opens, all other open panels in this group must close."*
18
+
19
+ ---
20
+
21
+ ## 2. Minimal Blueprint
22
+
23
+ This is the standard HTML structure. The pairing of triggers and panels is by ID, keeping layout and proximity decoupled.
24
+
25
+ ```html
26
+ <ul data-ln-accordion>
27
+ <li>
28
+ <!-- The Trigger -->
29
+ <header data-ln-toggle-for="panel1">
30
+ Section 1
31
+ <svg class="ln-icon ln-chevron" aria-hidden="true"><use href="#ln-arrow-down"></use></svg>
32
+ </header>
33
+ <!-- The Collapsible Panel -->
34
+ <section id="panel1" data-ln-toggle="open" class="collapsible">
35
+ <article class="collapsible-body">
36
+ <p>Content 1 (Starts open).</p>
37
+ </article>
38
+ </section>
39
+ </li>
40
+ <li>
41
+ <header data-ln-toggle-for="panel2">
42
+ Section 2
43
+ <svg class="ln-icon ln-chevron" aria-hidden="true"><use href="#ln-arrow-down"></use></svg>
44
+ </header>
45
+ <section id="panel2" data-ln-toggle class="collapsible">
46
+ <article class="collapsible-body">
47
+ <p>Content 2 (Starts closed).</p>
48
+ </article>
49
+ </section>
50
+ </li>
51
+ </ul>
52
+ ```
53
+
54
+ ### Key Anatomy Rules
55
+ - **The Wrapper (`data-ln-accordion`)**: Markers for the coordinator. Listens for bubbled events and coordinates siblings.
56
+ - **The Trigger (`data-ln-toggle-for`)**: Click target that toggles the target panel ID. The chevron rotates automatically driven by `aria-expanded`.
57
+ - **The Panel (`data-ln-toggle`)**: Creates the `ln-toggle` state instance. Value `open` or empty.
58
+ - **The Body (`.collapsible-body`)**: Wraps actual content. Padding and margins must live here, not on the parent `.collapsible` (which needs zero padding to collapse to exactly `0px` height).
59
+
60
+ ---
61
+
62
+ ## 3. The Decoupled State & API Contract
63
+
64
+ The coordinator has **zero public state** in JavaScript. The DOM is the source of truth, and **the HTML attribute is the sole contract**.
65
+
66
+ ### Attributes
67
+ - `data-ln-accordion` on the wrapper creates the coordinator instance. It takes no values.
68
+
69
+ ### Events
70
+ - **`ln-accordion:change`**: Dispatched on the wrapper after a panel opens and siblings close.
71
+ - `event.detail.target`: The HTML element of the panel that just opened.
72
+ ```js
73
+ document.addEventListener('ln-accordion:change', (e) => {
74
+ console.log('Active panel ID:', e.detail.target.id);
75
+ });
76
+ ```
77
+
78
+ ### Programmatic Control
79
+ There are no `open()` or `close()` methods on the coordinator instance. To programmatically change panels, write directly to the target panel's attribute:
80
+
81
+ ```js
82
+ // The coordinator catches the bubbled event and closes all siblings automatically.
83
+ document.getElementById('panel2').setAttribute('data-ln-toggle', 'open');
84
+ ```
85
+
86
+ ---
87
+
88
+ ## 4. Integration Patterns
89
+
90
+ ### A. All-Closed by Default
91
+ Simply omit the `="open"` value from all panels in the markup.
92
+ ```html
93
+ <section id="panel1" data-ln-toggle class="collapsible">...</section>
94
+ ```
95
+
96
+ ### B. Persistent Accordion State (Across Page Reloads)
97
+ Add `data-ln-persist` to the panels. Each panel saves its state in `localStorage` individually. The coordinator stays completely oblivious. On page load, whichever panel restores as `open` bubbles an event, and the coordinator handles the rest.
98
+ ```html
99
+ <section id="panel1" data-ln-toggle data-ln-persist class="collapsible">...</section>
100
+ ```
101
+
102
+ ### C. Zero-Configuration Multi-Open
103
+ If your requirements change and you want a "multi-open accordion" (where panels toggle independently without closing others), **you do not need any JavaScript options or class re-configuration**. Simply *remove the `data-ln-accordion` attribute* from the wrapper. The individual panels continue to work perfectly.
104
+
105
+ ### D. Nested Accordions
106
+ Supported natively out of the box. Scoping is determined by DOM ancestry (using `element.closest('[data-ln-accordion]')`). Opening an inner accordion panel bubbles upwards, but the outer coordinator ignores it, allowing infinite nesting depth without any configuration.
107
+
108
+ ---
109
+
110
+ ## 5. Common Implementation Pitfalls
111
+
112
+ ### 1. Padding on `.collapsible` directly
113
+ The `.collapsible` container must have zero padding so it can transition to exactly `0px` height. Placing padding directly on `.collapsible` will cause a thin strip of content to remain visible even when closed. **Padding must live on the `.collapsible-body` child**.
114
+
115
+ ### 2. Double-Binding Attributes
116
+ Never place `data-ln-toggle` and `data-ln-toggle-for` on the same element. One element is either a trigger or a panel, never both.
117
+
118
+ ---
119
+
120
+ ## 6. Integration & Source Files
121
+
122
+ - **Unified Bundle**: Loaded automatically with the main bundle:
123
+ ```html
124
+ <script src="dist/ln-ashlar.iife.js" defer></script>
125
+ ```
126
+ - **Standalone IIFE**: For lightweight pages, load the standalone, self-registering IIFE version:
127
+ ```html
128
+ <script src="js/ln-accordion/ln-accordion.js" defer></script>
129
+ ```
130
+ - **Active Source (ESM)**: Development source is located at [js/ln-accordion/src/ln-accordion.js](file:///c:/laragon/www/ln-ashlar/js/ln-accordion/src/ln-accordion.js).
131
+
132
+ ---
133
+
134
+ ## Related
135
+ - **[`ln-toggle`](../ln-toggle/README.md)** — Binary state primitive.
136
+ - **Architecture deep-dive** — [`docs/js/accordion.md`](../../docs/js/accordion.md).
137
+ - **Cross-component principles** — [`docs/architecture/data-flow.md`](../../docs/architecture/data-flow.md).
@@ -0,0 +1 @@
1
+ (function(){"use strict";function y(e,t,n){e.dispatchEvent(new CustomEvent(t,{bubbles:!0,detail:n||{}}))}function w(e,t){if(!document.body){document.addEventListener("DOMContentLoaded",function(){w(e,t)}),console.warn("["+t+'] Script loaded before <body> — add "defer" to your <script> tag');return}e()}function g(e,t,n,o){if(e.nodeType!==1)return;const s=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",i=Array.from(e.querySelectorAll(s));e.matches&&e.matches(s)&&i.push(e);for(const r of i)r[n]||(r[n]=new o(r))}function O(e,t,n,o,a={}){const s=a.extraAttributes||[],i=a.onAttributeChange||null,r=a.onInit||null;function l(p){const c=p||document.body;g(c,e,t,n),r&&r(c)}return w(function(){const p=new MutationObserver(function(f){for(let u=0;u<f.length;u++){const d=f[u];if(d.type==="childList")for(let h=0;h<d.addedNodes.length;h++){const b=d.addedNodes[h];b.nodeType===1&&(g(b,e,t,n),r&&r(b))}else d.type==="attributes"&&(i&&d.target[t]?i(d.target,d.attributeName):(g(d.target,e,t,n),r&&r(d.target)))}});let c=[];if(e.indexOf("[")!==-1){const f=/\[([\w-]+)/g;let u;for(;(u=f.exec(e))!==null;)c.push(u[1])}else c.push(e);p.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:c.concat(s)})},o),window[t]=l,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){l(document.body)}):l(document.body),l}const m={};function A(e,t){m[e]=t}function v(e){return m[e]||{ingress:t=>t,egress:t=>t}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=A,window.lnCore.getDataMapper=v),(function(){const e="data-ln-accordion",t="lnAccordion";if(window[t]!==void 0)return;function n(o){return this.dom=o,this._onToggleOpen=function(a){if(a.detail.target.closest("[data-ln-accordion]")!==o)return;const s=o.querySelectorAll("[data-ln-toggle]");for(const i of s)i!==a.detail.target&&i.closest("[data-ln-accordion]")===o&&i.getAttribute("data-ln-toggle")==="open"&&i.setAttribute("data-ln-toggle","close");y(o,"ln-accordion:change",{target:a.detail.target})},o.addEventListener("ln-toggle:open",this._onToggleOpen),this}n.prototype.destroy=function(){this.dom[t]&&(this.dom.removeEventListener("ln-toggle:open",this._onToggleOpen),y(this.dom,"ln-accordion:destroyed",{target:this.dom}),delete this.dom[t])},O(e,t,n,"ln-accordion")})()})();
@@ -0,0 +1,41 @@
1
+ import { dispatch, registerComponent } from '../../ln-core';
2
+
3
+ (function () {
4
+ const DOM_SELECTOR = 'data-ln-accordion';
5
+ const DOM_ATTRIBUTE = 'lnAccordion';
6
+
7
+ if (window[DOM_ATTRIBUTE] !== undefined) return;
8
+
9
+ // ─── Component ─────────────────────────────────────────────
10
+
11
+ function _component(dom) {
12
+ this.dom = dom;
13
+
14
+ this._onToggleOpen = function (e) {
15
+ if (e.detail.target.closest('[data-ln-accordion]') !== dom) return;
16
+ const toggles = dom.querySelectorAll('[data-ln-toggle]');
17
+ for (const el of toggles) {
18
+ if (el === e.detail.target) continue;
19
+ if (el.closest('[data-ln-accordion]') !== dom) continue;
20
+ if (el.getAttribute('data-ln-toggle') === 'open') {
21
+ el.setAttribute('data-ln-toggle', 'close');
22
+ }
23
+ }
24
+ dispatch(dom, 'ln-accordion:change', { target: e.detail.target });
25
+ };
26
+ dom.addEventListener('ln-toggle:open', this._onToggleOpen);
27
+
28
+ return this;
29
+ }
30
+
31
+ _component.prototype.destroy = function () {
32
+ if (!this.dom[DOM_ATTRIBUTE]) return;
33
+ this.dom.removeEventListener('ln-toggle:open', this._onToggleOpen);
34
+ dispatch(this.dom, 'ln-accordion:destroyed', { target: this.dom });
35
+ delete this.dom[DOM_ATTRIBUTE];
36
+ };
37
+
38
+ // ─── Init ──────────────────────────────────────────────────
39
+
40
+ registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-accordion');
41
+ })();
@@ -0,0 +1,91 @@
1
+ # ln-ajax
2
+
3
+ A zero-dependency, event-driven **HTML Fragment Swapping Primitive** that intercepts clicks on `<a>` elements and submits on `<form>` tags to enable instant, SPA-like navigation without full page reloads.
4
+
5
+ It communicates via a structured server JSON protocol, exchanging targeted DOM updates, updating browser history states, and re-attaching lifecycle managers to newly injected nodes.
6
+
7
+ ---
8
+
9
+ ## 🧭 Philosophy & Architecture
10
+
11
+ 1. **HTML-First Swapping:** The server remains the single source of truth for both data and markup. Instead of client routers rendering JSON arrays, the server compiles standard HTML fragments and returns them inside a structured JSON payload.
12
+ 2. **Selective DOM Merges:** The response maps selector IDs (e.g. `main-content`) directly to their new HTML chunks, replacing only the specified regions in-place.
13
+ 3. **Transparent Enhancements:** Intercepts only native interactions (links to the same origin, form submissions). Safely falls back to native browser redirects on errors or external hosts.
14
+
15
+ ---
16
+
17
+ ## 📦 Minimal Blueprint
18
+
19
+ Wrap interactive elements or entire layouts with the `data-ln-ajax` selector.
20
+
21
+ ```html
22
+ <div data-ln-ajax>
23
+ <!-- Clicking this fetches /dashboard and swaps only the returned targets -->
24
+ <a href="/dashboard">Dashboard</a>
25
+
26
+ <!-- Submitting this posts data and swaps target parts on success -->
27
+ <form method="POST" action="/users/create">
28
+ <input name="username" type="text" required>
29
+ <button type="submit">Create User</button>
30
+ </form>
31
+
32
+ <!-- Exclude specific elements from AJAX handling -->
33
+ <a href="/logout" data-ln-ajax="false">Logout</a>
34
+ </div>
35
+ ```
36
+
37
+ ---
38
+
39
+ ## 🛠️ Declarative API Contract
40
+
41
+ ### HTML Attributes
42
+
43
+ | Attribute | Elements | Description |
44
+ | :--- | :--- | :--- |
45
+ | `data-ln-ajax` | Container, `<a>`, `<form>` | Activates AJAX capture on the element and its descendants. |
46
+ | `data-ln-ajax="false"` | `<a>`, `<form>` | Excludes the specific link or form from AJAX interception. |
47
+
48
+ ### Server Response Protocol
49
+
50
+ The server must return JSON with the `application/json` Content-Type:
51
+
52
+ ```json
53
+ {
54
+ "title": "New Dashboard Page",
55
+ "content": {
56
+ "main-content": "<h1>Dashboard</h1><p>Welcome back!</p>",
57
+ "sidebar-nav": "<ul><li>Active Nav Item</li></ul>"
58
+ },
59
+ "message": {
60
+ "type": "success",
61
+ "title": "User Created",
62
+ "body": "The user was registered successfully."
63
+ }
64
+ }
65
+ ```
66
+
67
+ * **`title`**: Updates `document.title` on page swap.
68
+ * **`content`**: Key-value pairs matching container `id` selectors to their new `innerHTML` content.
69
+ * **`message`**: Optional. If present, automatically dispatches `ln-toast:enqueue` on the `window` to trigger native notifications.
70
+
71
+ ---
72
+
73
+ ## ⚡ DOM Events
74
+
75
+ All events are dispatched on the initiating element (`<a>` or `<form>`) and bubble.
76
+
77
+ | Event | Cancelable | Description | Payload (`detail`) |
78
+ | :--- | :--- | :--- | :--- |
79
+ | `ln-ajax:before-start` | **Yes** | Fires before any network activity. Call `e.preventDefault()` to cancel. | `{ method, url }` |
80
+ | `ln-ajax:start` | No | Fires as the loader class is added and fetch begins. | `{ method, url }` |
81
+ | `ln-ajax:success` | No | Fires after successful DOM swaps. | `{ method, url, data }` |
82
+ | `ln-ajax:error` | No | Fires on HTTP status failure or network rejects. | `{ method, url, status, data }` or `{ method, url, error }` |
83
+ | `ln-ajax:complete` | No | Fires at the very end of the lifecycle (success or error). | `{ method, url }` |
84
+
85
+ ---
86
+
87
+ ## ⚠️ Common Pitfalls
88
+
89
+ - **Missing DOM IDs on Swap Targets:** If the server returns a key in `content` that does not match a mounting ID in the active document (e.g. `id="main-content"`), that segment swap fails silently.
90
+ - **Forgetting CSRF Meta:** `ln-ajax` automatically reads `<meta name="csrf-token" content="...">` to inject the `X-CSRF-TOKEN` header on non-GET calls. If this meta tag is missing, POST/PUT requests may fail authentication.
91
+ - **Breaking External Links:** Links with different hostnames are ignored automatically, but absolute paths on the same host are captured. Ensure assets/downloads use `data-ln-ajax="false"`.
@@ -0,0 +1 @@
1
+ (function(){"use strict";function p(o,n,l){o.dispatchEvent(new CustomEvent(n,{bubbles:!0,detail:l||{}}))}function M(o,n,l){const b=new CustomEvent(n,{bubbles:!0,cancelable:!0,detail:l||{}});return o.dispatchEvent(b),b}function v(o,n){if(!document.body){document.addEventListener("DOMContentLoaded",function(){v(o,n)}),console.warn("["+n+'] Script loaded before <body> — add "defer" to your <script> tag');return}o()}const j={};function C(o,n){j[o]=n}function D(o){return j[o]||{ingress:n=>n,egress:n=>n}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=C,window.lnCore.getDataMapper=D),(function(){const o="data-ln-ajax",n="lnAjax";if(window[n]!==void 0)return;function l(e){if(!e.hasAttribute(o)||e[n])return;e[n]=!0;const t=y(e);b(t.links),E(t.forms)}function b(e){for(const t of e){if(t[n+"Trigger"]||t.hostname&&t.hostname!==window.location.hostname)continue;const i=t.getAttribute("href");if(i&&i.includes("#"))continue;const r=function(a){if(a.ctrlKey||a.metaKey||a.button===1)return;a.preventDefault();const f=t.getAttribute("href");f&&x("GET",f,null,t)};t.addEventListener("click",r),t[n+"Trigger"]=r}}function E(e){for(const t of e){if(t[n+"Trigger"])continue;const i=function(r){r.preventDefault();const a=t.method.toUpperCase(),f=t.action,h=new FormData(t);for(const g of t.querySelectorAll('button, input[type="submit"]'))g.disabled=!0;x(a,f,h,t,function(){for(const g of t.querySelectorAll('button, input[type="submit"]'))g.disabled=!1})};t.addEventListener("submit",i),t[n+"Trigger"]=i}}function S(e){if(!e[n])return;const t=y(e);for(const i of t.links)i[n+"Trigger"]&&(i.removeEventListener("click",i[n+"Trigger"]),delete i[n+"Trigger"]);for(const i of t.forms)i[n+"Trigger"]&&(i.removeEventListener("submit",i[n+"Trigger"]),delete i[n+"Trigger"]);delete e[n]}function x(e,t,i,r,a){if(M(r,"ln-ajax:before-start",{method:e,url:t}).defaultPrevented)return;p(r,"ln-ajax:start",{method:e,url:t}),r.classList.add("ln-ajax--loading");const h=document.createElement("span");h.className="ln-ajax-spinner",r.appendChild(h);function g(){r.classList.remove("ln-ajax--loading");const s=r.querySelector(".ln-ajax-spinner");s&&s.remove(),a&&a()}let d=t;const k=document.querySelector('meta[name="csrf-token"]'),w=k?k.getAttribute("content"):null;i instanceof FormData&&w&&i.append("_token",w);const A={method:e,headers:{"X-Requested-With":"XMLHttpRequest",Accept:"application/json"}};if(w&&(A.headers["X-CSRF-TOKEN"]=w),e==="GET"&&i){const s=new URLSearchParams(i);d=t+(t.includes("?")?"&":"?")+s.toString()}else e!=="GET"&&i&&(A.body=i);fetch(d,A).then(function(s){const c=s.ok;return s.json().then(function(u){return{ok:c,status:s.status,data:u}})}).then(function(s){const c=s.data;if(s.ok){if(c.title&&(document.title=c.title),c.content)for(const u in c.content){const L=document.getElementById(u);if(L){let m=c.content[u];window.DOMPurify&&typeof window.DOMPurify.sanitize=="function"?m=window.DOMPurify.sanitize(m):m=m.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,"").replace(/on\w+\s*=\s*(['"][^'"]*['"]|[^\s>]+)/gi,""),L.innerHTML=m}}if(r.tagName==="A"){const u=r.getAttribute("href");u&&window.history.pushState({ajax:!0},"",u)}else r.tagName==="FORM"&&r.method.toUpperCase()==="GET"&&window.history.pushState({ajax:!0},"",d);p(r,"ln-ajax:success",{method:e,url:d,data:c})}else p(r,"ln-ajax:error",{method:e,url:d,status:s.status,data:c});if(c.message){const u=c.message;window.dispatchEvent(new CustomEvent("ln-toast:enqueue",{detail:{type:u.type||(s.ok?"success":"error"),title:u.title||"",message:u.body||""}}))}p(r,"ln-ajax:complete",{method:e,url:d}),g()}).catch(function(s){p(r,"ln-ajax:error",{method:e,url:d,error:s}),p(r,"ln-ajax:complete",{method:e,url:d}),g()})}function y(e){const t={links:[],forms:[]};return e.tagName==="A"&&e.getAttribute(o)!=="false"?t.links.push(e):e.tagName==="FORM"&&e.getAttribute(o)!=="false"?t.forms.push(e):(t.links=Array.from(e.querySelectorAll('a:not([data-ln-ajax="false"])')),t.forms=Array.from(e.querySelectorAll('form:not([data-ln-ajax="false"])'))),t}function O(){v(function(){new MutationObserver(function(t){for(const i of t)if(i.type==="childList"){for(const r of i.addedNodes)if(r.nodeType===1&&(l(r),!r.hasAttribute(o))){for(const f of r.querySelectorAll("["+o+"]"))l(f);const a=r.closest&&r.closest("["+o+"]");if(a&&a.getAttribute(o)!=="false"){const f=y(r);b(f.links),E(f.forms)}}}else i.type==="attributes"&&l(i.target)}).observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:[o]})},"ln-ajax")}function T(){for(const e of document.querySelectorAll("["+o+"]"))l(e)}window[n]=l,window[n].destroy=S,O(),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",T):T()})()})();
@@ -0,0 +1,277 @@
1
+ import { guardBody, dispatch, dispatchCancelable } from '../../ln-core';
2
+
3
+ (function () {
4
+ const DOM_SELECTOR = 'data-ln-ajax';
5
+ const DOM_ATTRIBUTE = 'lnAjax';
6
+
7
+ if (window[DOM_ATTRIBUTE] !== undefined) return;
8
+
9
+ function _constructor(domRoot) {
10
+ if (!domRoot.hasAttribute(DOM_SELECTOR)) return;
11
+ if (domRoot[DOM_ATTRIBUTE]) return;
12
+ domRoot[DOM_ATTRIBUTE] = true;
13
+
14
+ const items = findElements(domRoot);
15
+ _attachLinksAjax(items.links);
16
+ _attachFormsAjax(items.forms);
17
+ }
18
+
19
+ function _attachLinksAjax(links) {
20
+ for (const link of links) {
21
+ if (link[DOM_ATTRIBUTE + 'Trigger']) continue;
22
+
23
+ // Skip external links — CORS blocks them and they should open normally
24
+ if (link.hostname && link.hostname !== window.location.hostname) continue;
25
+
26
+ const href = link.getAttribute('href');
27
+ if (href && href.includes('#')) continue;
28
+
29
+ const handler = function (e) {
30
+ if (e.ctrlKey || e.metaKey || e.button === 1) return;
31
+
32
+ e.preventDefault();
33
+ const url = link.getAttribute('href');
34
+ if (url) {
35
+ _makeAjaxRequest('GET', url, null, link);
36
+ }
37
+ };
38
+
39
+ link.addEventListener('click', handler);
40
+ link[DOM_ATTRIBUTE + 'Trigger'] = handler;
41
+ }
42
+ }
43
+
44
+ function _attachFormsAjax(forms) {
45
+ for (const form of forms) {
46
+ if (form[DOM_ATTRIBUTE + 'Trigger']) continue;
47
+
48
+ const handler = function (e) {
49
+ e.preventDefault();
50
+ const method = form.method.toUpperCase();
51
+ const action = form.action;
52
+ const formData = new FormData(form);
53
+
54
+ for (const btn of form.querySelectorAll('button, input[type="submit"]')) {
55
+ btn.disabled = true;
56
+ }
57
+
58
+ _makeAjaxRequest(method, action, formData, form, function () {
59
+ for (const btn of form.querySelectorAll('button, input[type="submit"]')) {
60
+ btn.disabled = false;
61
+ }
62
+ });
63
+ };
64
+
65
+ form.addEventListener('submit', handler);
66
+ form[DOM_ATTRIBUTE + 'Trigger'] = handler;
67
+ }
68
+ }
69
+
70
+ function _destroy(domRoot) {
71
+ if (!domRoot[DOM_ATTRIBUTE]) return;
72
+
73
+ const items = findElements(domRoot);
74
+ for (const link of items.links) {
75
+ if (link[DOM_ATTRIBUTE + 'Trigger']) {
76
+ link.removeEventListener('click', link[DOM_ATTRIBUTE + 'Trigger']);
77
+ delete link[DOM_ATTRIBUTE + 'Trigger'];
78
+ }
79
+ }
80
+ for (const form of items.forms) {
81
+ if (form[DOM_ATTRIBUTE + 'Trigger']) {
82
+ form.removeEventListener('submit', form[DOM_ATTRIBUTE + 'Trigger']);
83
+ delete form[DOM_ATTRIBUTE + 'Trigger'];
84
+ }
85
+ }
86
+
87
+ delete domRoot[DOM_ATTRIBUTE];
88
+ }
89
+
90
+ function _makeAjaxRequest(method, url, data, element, callback) {
91
+ const before = dispatchCancelable(element, 'ln-ajax:before-start', { method: method, url: url });
92
+ if (before.defaultPrevented) return;
93
+
94
+ dispatch(element, 'ln-ajax:start', { method: method, url: url });
95
+
96
+ element.classList.add('ln-ajax--loading');
97
+ const spinner = document.createElement('span');
98
+ spinner.className = 'ln-ajax-spinner';
99
+ element.appendChild(spinner);
100
+
101
+ function _cleanup() {
102
+ element.classList.remove('ln-ajax--loading');
103
+ const s = element.querySelector('.ln-ajax-spinner');
104
+ if (s) s.remove();
105
+ if (callback) callback();
106
+ }
107
+
108
+ let finalUrl = url;
109
+
110
+ const csrfToken = document.querySelector('meta[name="csrf-token"]');
111
+ const token = csrfToken ? csrfToken.getAttribute('content') : null;
112
+
113
+ if (data instanceof FormData && token) {
114
+ data.append('_token', token);
115
+ }
116
+
117
+ const options = {
118
+ method: method,
119
+ headers: {
120
+ 'X-Requested-With': 'XMLHttpRequest',
121
+ 'Accept': 'application/json'
122
+ }
123
+ };
124
+
125
+ if (token) {
126
+ options.headers['X-CSRF-TOKEN'] = token;
127
+ }
128
+
129
+ if (method === 'GET' && data) {
130
+ const params = new URLSearchParams(data);
131
+ finalUrl = url + (url.includes('?') ? '&' : '?') + params.toString();
132
+ } else if (method !== 'GET' && data) {
133
+ options.body = data;
134
+ }
135
+
136
+ fetch(finalUrl, options)
137
+ .then(function (response) {
138
+ const ok = response.ok;
139
+ return response.json().then(function (data) {
140
+ return { ok: ok, status: response.status, data: data };
141
+ });
142
+ })
143
+ .then(function (result) {
144
+ const data = result.data;
145
+
146
+ if (result.ok) {
147
+ if (data.title) {
148
+ document.title = data.title;
149
+ }
150
+
151
+ if (data.content) {
152
+ for (const targetId in data.content) {
153
+ const targetElement = document.getElementById(targetId);
154
+ if (targetElement) {
155
+ let htmlContent = data.content[targetId];
156
+ // Defense-in-depth: sanitize if DOMPurify is available globally, otherwise use native mitigation
157
+ if (window.DOMPurify && typeof window.DOMPurify.sanitize === 'function') {
158
+ htmlContent = window.DOMPurify.sanitize(htmlContent);
159
+ } else {
160
+ // Safe fallback: strip inline script tags and inline events (on*) to prevent basic XSS
161
+ htmlContent = htmlContent
162
+ .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
163
+ .replace(/on\w+\s*=\s*(['"][^'"]*['"]|[^\s>]+)/gi, '');
164
+ }
165
+ targetElement.innerHTML = htmlContent;
166
+ }
167
+ }
168
+ }
169
+
170
+ if (element.tagName === 'A') {
171
+ const historyUrl = element.getAttribute('href');
172
+ if (historyUrl) {
173
+ window.history.pushState({ ajax: true }, '', historyUrl);
174
+ }
175
+ } else if (element.tagName === 'FORM' && element.method.toUpperCase() === 'GET') {
176
+ window.history.pushState({ ajax: true }, '', finalUrl);
177
+ }
178
+
179
+ dispatch(element, 'ln-ajax:success', { method: method, url: finalUrl, data: data });
180
+ } else {
181
+ dispatch(element, 'ln-ajax:error', { method: method, url: finalUrl, status: result.status, data: data });
182
+ }
183
+
184
+ // Auto-dispatch response message as a toast event.
185
+ // Any listener (ln-toast by default) can pick it up.
186
+ if (data.message) {
187
+ const msg = data.message;
188
+ window.dispatchEvent(new CustomEvent('ln-toast:enqueue', {
189
+ detail: {
190
+ type: msg.type || (result.ok ? 'success' : 'error'),
191
+ title: msg.title || '',
192
+ message: msg.body || ''
193
+ }
194
+ }));
195
+ }
196
+
197
+ dispatch(element, 'ln-ajax:complete', { method: method, url: finalUrl });
198
+ _cleanup();
199
+ })
200
+ .catch(function (error) {
201
+ dispatch(element, 'ln-ajax:error', { method: method, url: finalUrl, error: error });
202
+ dispatch(element, 'ln-ajax:complete', { method: method, url: finalUrl });
203
+ _cleanup();
204
+ });
205
+ }
206
+
207
+ // Local findElements — intentional divergence from ln-core helper: returns { links, forms } partition, not per-element constructors.
208
+ function findElements(domRoot) {
209
+ const items = { links: [], forms: [] };
210
+
211
+ if (domRoot.tagName === 'A' && domRoot.getAttribute(DOM_SELECTOR) !== 'false') {
212
+ items.links.push(domRoot);
213
+ } else if (domRoot.tagName === 'FORM' && domRoot.getAttribute(DOM_SELECTOR) !== 'false') {
214
+ items.forms.push(domRoot);
215
+ } else {
216
+ items.links = Array.from(domRoot.querySelectorAll('a:not([data-ln-ajax="false"])'));
217
+ items.forms = Array.from(domRoot.querySelectorAll('form:not([data-ln-ajax="false"])'));
218
+ }
219
+
220
+ return items;
221
+ }
222
+
223
+ function _domObserver() {
224
+ guardBody(function () {
225
+ const observer = new MutationObserver(function (mutations) {
226
+ for (const mutation of mutations) {
227
+ if (mutation.type === 'childList') {
228
+ for (const node of mutation.addedNodes) {
229
+ if (node.nodeType === 1) {
230
+ _constructor(node);
231
+
232
+ if (!node.hasAttribute(DOM_SELECTOR)) {
233
+ for (const el of node.querySelectorAll('[' + DOM_SELECTOR + ']')) {
234
+ _constructor(el);
235
+ }
236
+
237
+ const ajaxRoot = node.closest && node.closest('[' + DOM_SELECTOR + ']');
238
+ if (ajaxRoot && ajaxRoot.getAttribute(DOM_SELECTOR) !== 'false') {
239
+ const items = findElements(node);
240
+ _attachLinksAjax(items.links);
241
+ _attachFormsAjax(items.forms);
242
+ }
243
+ }
244
+ }
245
+ }
246
+ } else if (mutation.type === 'attributes') {
247
+ _constructor(mutation.target);
248
+ }
249
+ }
250
+ });
251
+
252
+ observer.observe(document.body, {
253
+ childList: true,
254
+ subtree: true,
255
+ attributes: true,
256
+ attributeFilter: [DOM_SELECTOR]
257
+ });
258
+ }, 'ln-ajax');
259
+ }
260
+
261
+ function _initializeAll() {
262
+ for (const element of document.querySelectorAll('[' + DOM_SELECTOR + ']')) {
263
+ _constructor(element);
264
+ }
265
+ }
266
+
267
+ window[DOM_ATTRIBUTE] = _constructor;
268
+ window[DOM_ATTRIBUTE].destroy = _destroy;
269
+
270
+ _domObserver();
271
+
272
+ if (document.readyState === 'loading') {
273
+ document.addEventListener('DOMContentLoaded', _initializeAll);
274
+ } else {
275
+ _initializeAll();
276
+ }
277
+ })();