@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.
- package/README.md +177 -0
- package/js/COMPONENTS.md +1102 -0
- package/js/index.js +41 -0
- package/js/ln-accordion/README.md +137 -0
- package/js/ln-accordion/ln-accordion.js +1 -0
- package/js/ln-accordion/src/ln-accordion.js +41 -0
- package/js/ln-ajax/README.md +91 -0
- package/js/ln-ajax/ln-ajax.js +1 -0
- package/js/ln-ajax/src/ln-ajax.js +277 -0
- package/js/ln-api-connector/README.md +150 -0
- package/js/ln-api-connector/ln-api-connector.js +1 -0
- package/js/ln-api-connector/src/ln-api-connector.js +265 -0
- package/js/ln-autoresize/README.md +80 -0
- package/js/ln-autoresize/ln-autoresize.js +1 -0
- package/js/ln-autoresize/src/ln-autoresize.js +47 -0
- package/js/ln-autosave/README.md +92 -0
- package/js/ln-autosave/ln-autosave.js +1 -0
- package/js/ln-autosave/src/ln-autosave.js +147 -0
- package/js/ln-circular-progress/README.md +161 -0
- package/js/ln-circular-progress/ln-circular-progress.js +1 -0
- package/js/ln-circular-progress/src/ln-circular-progress.js +133 -0
- package/js/ln-confirm/README.md +86 -0
- package/js/ln-confirm/_ln-confirm.scss +13 -0
- package/js/ln-confirm/ln-confirm.js +1 -0
- package/js/ln-confirm/src/ln-confirm.js +131 -0
- package/js/ln-core/crypto.js +83 -0
- package/js/ln-core/helpers.js +411 -0
- package/js/ln-core/index.js +5 -0
- package/js/ln-core/persist.js +71 -0
- package/js/ln-core/positioning.js +207 -0
- package/js/ln-core/reactive.js +74 -0
- package/js/ln-couchdb-connector/README.md +156 -0
- package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -0
- package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +348 -0
- package/js/ln-data-coordinator/README.md +165 -0
- package/js/ln-data-coordinator/ln-data-coordinator.js +1 -0
- package/js/ln-data-coordinator/src/ln-data-coordinator.js +249 -0
- package/js/ln-data-store/README.md +94 -0
- package/js/ln-data-store/ln-data-store.js +1 -0
- package/js/ln-data-store/src/ln-data-store.js +699 -0
- package/js/ln-data-table/README.md +110 -0
- package/js/ln-data-table/ln-data-table.js +1 -0
- package/js/ln-data-table/ln-data-table.scss +10 -0
- package/js/ln-data-table/src/ln-data-table.js +1103 -0
- package/js/ln-date/README.md +151 -0
- package/js/ln-date/ln-date.js +1 -0
- package/js/ln-date/src/ln-date.js +442 -0
- package/js/ln-dropdown/README.md +117 -0
- package/js/ln-dropdown/ln-dropdown.js +1 -0
- package/js/ln-dropdown/ln-dropdown.scss +15 -0
- package/js/ln-dropdown/src/ln-dropdown.js +174 -0
- package/js/ln-external-links/README.md +341 -0
- package/js/ln-external-links/ln-external-links.js +1 -0
- package/js/ln-external-links/src/ln-external-links.js +116 -0
- package/js/ln-filter/README.md +99 -0
- package/js/ln-filter/ln-filter.js +1 -0
- package/js/ln-filter/ln-filter.scss +7 -0
- package/js/ln-filter/src/ln-filter.js +404 -0
- package/js/ln-form/README.md +101 -0
- package/js/ln-form/ln-form.js +1 -0
- package/js/ln-form/src/ln-form.js +199 -0
- package/js/ln-http/README.md +89 -0
- package/js/ln-http/ln-http.js +1 -0
- package/js/ln-http/src/ln-http.js +219 -0
- package/js/ln-icons/README.md +88 -0
- package/js/ln-icons/ln-icons.js +1 -0
- package/js/ln-icons/src/ln-icons.js +169 -0
- package/js/ln-link/README.md +303 -0
- package/js/ln-link/ln-link.js +1 -0
- package/js/ln-link/src/ln-link.js +196 -0
- package/js/ln-modal/README.md +154 -0
- package/js/ln-modal/ln-modal.js +1 -0
- package/js/ln-modal/ln-modal.scss +11 -0
- package/js/ln-modal/src/ln-modal.js +201 -0
- package/js/ln-nav/README.md +70 -0
- package/js/ln-nav/ln-nav.js +1 -0
- package/js/ln-nav/src/ln-nav.js +177 -0
- package/js/ln-number/README.md +122 -0
- package/js/ln-number/ln-number.js +1 -0
- package/js/ln-number/src/ln-number.js +302 -0
- package/js/ln-popover/README.md +127 -0
- package/js/ln-popover/ln-popover.js +1 -0
- package/js/ln-popover/src/ln-popover.js +288 -0
- package/js/ln-progress/README.md +442 -0
- package/js/ln-progress/ln-progress.js +1 -0
- package/js/ln-progress/src/ln-progress.js +150 -0
- package/js/ln-search/README.md +83 -0
- package/js/ln-search/ln-search.js +1 -0
- package/js/ln-search/ln-search.scss +7 -0
- package/js/ln-search/src/ln-search.js +114 -0
- package/js/ln-sortable/README.md +95 -0
- package/js/ln-sortable/ln-sortable.js +1 -0
- package/js/ln-sortable/src/ln-sortable.js +203 -0
- package/js/ln-table/README.md +101 -0
- package/js/ln-table/ln-table-sort.js +1 -0
- package/js/ln-table/ln-table.js +1 -0
- package/js/ln-table/ln-table.scss +11 -0
- package/js/ln-table/src/ln-table-sort.js +168 -0
- package/js/ln-table/src/ln-table.js +473 -0
- package/js/ln-tabs/README.md +137 -0
- package/js/ln-tabs/ln-tabs.js +1 -0
- package/js/ln-tabs/src/ln-tabs.js +171 -0
- package/js/ln-time/README.md +81 -0
- package/js/ln-time/ln-time.js +1 -0
- package/js/ln-time/src/ln-time.js +192 -0
- package/js/ln-toast/README.md +122 -0
- package/js/ln-toast/ln-toast.js +15 -0
- package/js/ln-toast/src/ln-toast.js +210 -0
- package/js/ln-toast/template.html +14 -0
- package/js/ln-toggle/README.md +137 -0
- package/js/ln-toggle/ln-toggle.js +1 -0
- package/js/ln-toggle/src/ln-toggle.js +139 -0
- package/js/ln-tooltip/README.md +58 -0
- package/js/ln-tooltip/ln-tooltip.js +1 -0
- package/js/ln-tooltip/ln-tooltip.scss +9 -0
- package/js/ln-tooltip/src/ln-tooltip.js +169 -0
- package/js/ln-translations/README.md +96 -0
- package/js/ln-translations/ln-translations.js +1 -0
- package/js/ln-translations/src/ln-translations.js +275 -0
- package/js/ln-upload/README.md +180 -0
- package/js/ln-upload/ln-upload.js +1 -0
- package/js/ln-upload/ln-upload.scss +20 -0
- package/js/ln-upload/src/ln-upload.js +407 -0
- package/js/ln-validate/README.md +108 -0
- package/js/ln-validate/ln-validate.js +1 -0
- package/js/ln-validate/src/ln-validate.js +160 -0
- package/package.json +55 -0
- package/scss/base/_global.scss +83 -0
- package/scss/base/_reset.scss +17 -0
- package/scss/base/_typography.scss +125 -0
- package/scss/components/_accordion.scss +34 -0
- package/scss/components/_ajax.scss +15 -0
- package/scss/components/_alert.scss +5 -0
- package/scss/components/_app-shell.scss +15 -0
- package/scss/components/_avatar.scss +6 -0
- package/scss/components/_breadcrumbs.scss +33 -0
- package/scss/components/_button.scss +20 -0
- package/scss/components/_card.scss +10 -0
- package/scss/components/_chip.scss +5 -0
- package/scss/components/_circular-progress.scss +29 -0
- package/scss/components/_confirm.scss +5 -0
- package/scss/components/_data-table.scss +83 -0
- package/scss/components/_dropdown.scss +25 -0
- package/scss/components/_empty-state.scss +22 -0
- package/scss/components/_form.scss +100 -0
- package/scss/components/_layout.scss +8 -0
- package/scss/components/_link.scss +11 -0
- package/scss/components/_ln-table.scss +60 -0
- package/scss/components/_loader.scss +6 -0
- package/scss/components/_modal.scss +20 -0
- package/scss/components/_nav.scss +9 -0
- package/scss/components/_page-header.scss +10 -0
- package/scss/components/_popover.scss +10 -0
- package/scss/components/_progress.scss +17 -0
- package/scss/components/_prose.scss +5 -0
- package/scss/components/_scrollbar.scss +32 -0
- package/scss/components/_sections.scss +12 -0
- package/scss/components/_sidebar.scss +5 -0
- package/scss/components/_stat-card.scss +5 -0
- package/scss/components/_status-badge.scss +4 -0
- package/scss/components/_stepper.scss +5 -0
- package/scss/components/_table.scss +19 -0
- package/scss/components/_tabs.scss +21 -0
- package/scss/components/_timeline.scss +14 -0
- package/scss/components/_toast.scss +41 -0
- package/scss/components/_toggle.scss +81 -0
- package/scss/components/_tooltip.scss +18 -0
- package/scss/components/_translations.scss +111 -0
- package/scss/components/_upload.scss +51 -0
- package/scss/config/_breakpoints.scss +72 -0
- package/scss/config/_density.scss +117 -0
- package/scss/config/_icons.scss +37 -0
- package/scss/config/_mixins.scss +13 -0
- package/scss/config/_theme.scss +216 -0
- package/scss/config/_tokens.scss +419 -0
- package/scss/config/mixins/_accordion.scss +52 -0
- package/scss/config/mixins/_ajax.scss +39 -0
- package/scss/config/mixins/_alert.scss +82 -0
- package/scss/config/mixins/_app-shell.scss +312 -0
- package/scss/config/mixins/_avatar.scss +109 -0
- package/scss/config/mixins/_borders.scss +36 -0
- package/scss/config/mixins/_breadcrumbs.scss +72 -0
- package/scss/config/mixins/_breakpoints.scss +62 -0
- package/scss/config/mixins/_btn.scss +179 -0
- package/scss/config/mixins/_card.scss +338 -0
- package/scss/config/mixins/_chip.scss +66 -0
- package/scss/config/mixins/_circular-progress.scss +71 -0
- package/scss/config/mixins/_collapsible.scss +24 -0
- package/scss/config/mixins/_colors.scss +46 -0
- package/scss/config/mixins/_confirm.scss +31 -0
- package/scss/config/mixins/_data-table.scss +346 -0
- package/scss/config/mixins/_display.scss +32 -0
- package/scss/config/mixins/_dropdown.scss +143 -0
- package/scss/config/mixins/_empty-state.scss +30 -0
- package/scss/config/mixins/_focus.scss +55 -0
- package/scss/config/mixins/_footer.scss +42 -0
- package/scss/config/mixins/_form.scss +601 -0
- package/scss/config/mixins/_index.scss +58 -0
- package/scss/config/mixins/_interaction.scss +15 -0
- package/scss/config/mixins/_kbd.scss +22 -0
- package/scss/config/mixins/_layout.scss +117 -0
- package/scss/config/mixins/_link.scss +55 -0
- package/scss/config/mixins/_ln-table.scss +420 -0
- package/scss/config/mixins/_loader.scss +26 -0
- package/scss/config/mixins/_modal.scss +66 -0
- package/scss/config/mixins/_motion.scss +19 -0
- package/scss/config/mixins/_nav.scss +273 -0
- package/scss/config/mixins/_page-header.scss +69 -0
- package/scss/config/mixins/_popover.scss +25 -0
- package/scss/config/mixins/_position.scss +32 -0
- package/scss/config/mixins/_progress.scss +56 -0
- package/scss/config/mixins/_prose.scss +127 -0
- package/scss/config/mixins/_shadows.scss +8 -0
- package/scss/config/mixins/_sidebar.scss +95 -0
- package/scss/config/mixins/_sizing.scss +6 -0
- package/scss/config/mixins/_spacing.scss +19 -0
- package/scss/config/mixins/_stat-card.scss +68 -0
- package/scss/config/mixins/_status-badge.scss +83 -0
- package/scss/config/mixins/_stepper.scss +78 -0
- package/scss/config/mixins/_table.scss +215 -0
- package/scss/config/mixins/_tabs.scss +64 -0
- package/scss/config/mixins/_timeline.scss +69 -0
- package/scss/config/mixins/_toast.scss +148 -0
- package/scss/config/mixins/_tooltip.scss +111 -0
- package/scss/config/mixins/_transitions.scss +10 -0
- package/scss/config/mixins/_translations.scss +124 -0
- package/scss/config/mixins/_typography.scss +57 -0
- package/scss/config/mixins/_upload.scss +168 -0
- package/scss/ln-ashlar.scss +62 -0
- package/scss/tabler-icons.txt +5039 -0
- package/scss/utilities/_animations.scss +83 -0
- 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
|
+
})();
|