@livenetworks/ashlar 1.5.1 → 1.6.1
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 +189 -177
- package/js/COMPONENTS.md +55 -49
- package/js/index.js +22 -5
- package/js/ln-accordion/README.md +186 -137
- package/js/ln-accordion/ln-accordion.js +1 -1
- package/js/ln-ajax/README.md +73 -4
- package/js/ln-ajax/ln-ajax.js +1 -1
- package/js/ln-ajax/src/ln-ajax.js +13 -13
- package/js/ln-api-connector/README.md +261 -150
- package/js/ln-api-connector/ln-api-connector.js +1 -1
- package/js/ln-api-connector/src/ln-api-connector.js +368 -265
- package/js/ln-api-queue/README.md +219 -0
- package/js/ln-api-queue/ln-api-queue.js +1 -0
- package/js/ln-api-queue/src/ln-api-queue.js +320 -0
- package/js/ln-api-queue/src/queue-storage.js +455 -0
- package/js/ln-autoresize/README.md +47 -0
- package/js/ln-autoresize/ln-autoresize.js +1 -1
- package/js/ln-autosave/README.md +33 -1
- package/js/ln-autosave/ln-autosave.js +1 -1
- package/js/ln-autosave/src/ln-autosave.js +4 -4
- package/js/ln-circular-progress/README.md +27 -13
- package/js/ln-circular-progress/ln-circular-progress.js +1 -1
- package/js/ln-circular-progress/src/ln-circular-progress.js +18 -5
- package/js/ln-confirm/README.md +165 -86
- package/js/ln-confirm/_ln-confirm.scss +11 -0
- package/js/ln-confirm/ln-confirm.js +1 -1
- package/js/ln-confirm/src/ln-confirm.js +82 -42
- package/js/ln-core/README.md +896 -0
- package/js/ln-core/hash.js +92 -0
- package/js/ln-core/helpers.js +337 -8
- package/js/ln-core/index.js +4 -2
- package/js/ln-core/persist.js +29 -3
- package/js/ln-core/positioning.js +1 -35
- package/js/ln-core/window-cache.js +213 -0
- package/js/ln-couchdb-connector/README.md +177 -156
- package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -1
- package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +398 -348
- package/js/ln-data-coordinator/README.md +536 -165
- package/js/ln-data-coordinator/ln-data-coordinator.js +1 -1
- package/js/ln-data-coordinator/src/data-read-policy.js +18 -0
- package/js/ln-data-coordinator/src/ln-data-coordinator.js +1042 -249
- package/js/ln-data-coordinator/src/mutation-receipts.js +46 -0
- package/js/ln-data-store/README.md +241 -94
- package/js/ln-data-store/ln-data-store.js +1 -1
- package/js/ln-data-store/src/ln-data-store.js +694 -699
- package/js/ln-date/README.md +39 -1
- package/js/ln-date/ln-date-dev.scss +14 -0
- package/js/ln-date/ln-date.js +1 -1
- package/js/ln-date/src/ln-date.js +236 -178
- package/js/ln-debug/README.md +58 -0
- package/js/ln-debug/ln-debug.js +1 -0
- package/js/ln-debug/src/ln-debug.js +19 -0
- package/js/ln-dropdown/README.md +31 -8
- package/js/ln-dropdown/ln-dropdown.js +1 -1
- package/js/ln-dropdown/src/ln-dropdown.js +30 -13
- package/js/ln-editor/README.md +274 -0
- package/js/ln-editor/ln-editor.js +1 -0
- package/js/ln-editor/ln-editor.scss +43 -0
- package/js/ln-editor/src/ln-editor.js +619 -0
- package/js/ln-external-links/README.md +44 -5
- package/js/ln-external-links/ln-external-links.js +1 -1
- package/js/ln-fill/README.md +199 -0
- package/js/ln-fill/ln-fill.js +1 -0
- package/js/ln-fill/src/ln-fill.js +87 -0
- package/js/ln-filter/README.md +92 -2
- package/js/ln-filter/ln-filter-dev.scss +8 -0
- package/js/ln-filter/ln-filter.js +1 -1
- package/js/ln-filter/src/ln-filter.js +22 -7
- package/js/ln-form/README.md +212 -49
- package/js/ln-form/ln-form.js +1 -1
- package/js/ln-form/src/ln-form.js +99 -199
- package/js/ln-http/README.md +125 -89
- package/js/ln-http/ln-http.js +1 -1
- package/js/ln-http/src/ln-http.js +32 -13
- package/js/ln-icon/README.md +130 -0
- package/js/ln-icon/ln-icon.js +1 -0
- package/js/{ln-icons/src/ln-icons.js → ln-icon/src/ln-icon.js} +20 -15
- package/js/ln-link/README.md +47 -9
- package/js/ln-link/ln-link.js +1 -1
- package/js/ln-link/src/ln-link.js +22 -6
- package/js/ln-list/README.md +129 -0
- package/js/ln-list/ln-list.js +1 -0
- package/js/ln-list/ln-list.scss +28 -0
- package/js/ln-list/src/ln-list.js +1216 -0
- package/js/ln-modal/README.md +185 -154
- package/js/ln-modal/ln-modal-dev.scss +13 -0
- package/js/ln-modal/ln-modal.js +1 -1
- package/js/ln-modal/ln-modal.scss +28 -4
- package/js/ln-modal/src/ln-modal.js +137 -201
- package/js/ln-modal-coordinator/README.md +99 -0
- package/js/ln-modal-coordinator/ln-modal-coordinator-dev.scss +8 -0
- package/js/ln-modal-coordinator/ln-modal-coordinator.js +1 -0
- package/js/ln-modal-coordinator/src/ln-modal-coordinator.js +345 -0
- package/js/ln-nav/README.md +46 -3
- package/js/ln-nav/ln-nav.js +1 -1
- package/js/ln-nav/src/ln-nav.js +92 -127
- package/js/ln-number/README.md +28 -1
- package/js/ln-number/ln-number-dev.scss +13 -0
- package/js/ln-number/ln-number.js +1 -1
- package/js/ln-number/src/ln-number.js +443 -350
- package/js/ln-options/README.md +66 -0
- package/js/ln-options/ln-options.js +1 -0
- package/js/ln-options/src/ln-options.js +74 -0
- package/js/ln-popover/README.md +40 -5
- package/js/ln-popover/ln-popover.js +1 -1
- package/js/ln-popover/src/ln-popover.js +38 -11
- package/js/ln-progress/README.md +413 -442
- package/js/ln-progress/ln-progress.js +1 -1
- package/js/ln-progress/src/ln-progress.js +98 -150
- package/js/ln-router/README.md +432 -0
- package/js/ln-router/ln-router.js +1 -0
- package/js/ln-router/src/ln-router.js +539 -0
- package/js/ln-search/README.md +158 -83
- package/js/ln-search/ln-search-dev.scss +8 -0
- package/js/ln-search/ln-search.js +1 -1
- package/js/ln-search/src/ln-search.js +12 -9
- package/js/ln-slug/README.md +110 -0
- package/js/ln-slug/ln-slug.js +1 -0
- package/js/ln-slug/src/ln-slug.js +74 -0
- package/js/ln-sort/README.md +170 -0
- package/js/ln-sort/ln-sort-dev.scss +8 -0
- package/js/ln-sort/ln-sort.js +1 -0
- package/js/ln-sort/src/ln-sort.js +178 -0
- package/js/ln-sortable/README.md +133 -95
- package/js/ln-sortable/ln-sortable.js +1 -1
- package/js/ln-stat/README.md +55 -0
- package/js/ln-stat/ln-stat.js +1 -0
- package/js/ln-stat/src/ln-stat.js +55 -0
- package/js/ln-table/README.md +285 -101
- package/js/ln-table/ln-table-dev.scss +17 -0
- package/js/ln-table/ln-table.js +1 -1
- package/js/ln-table/ln-table.scss +1 -1
- package/js/ln-table/src/ln-table.js +1493 -473
- package/js/ln-table-coordinator/README.md +73 -0
- package/js/ln-table-coordinator/ln-table-coordinator.js +1 -0
- package/js/ln-table-coordinator/src/ln-table-coordinator.js +160 -0
- package/js/ln-tabs/README.md +221 -137
- package/js/ln-tabs/ln-tabs-dev.scss +18 -0
- package/js/ln-tabs/ln-tabs.js +1 -1
- package/js/ln-tabs/src/ln-tabs.js +41 -24
- package/js/ln-time/README.md +14 -0
- package/js/ln-time/ln-time.js +1 -1
- package/js/ln-time/src/ln-time.js +223 -192
- package/js/ln-toast/README.md +178 -122
- package/js/ln-toast/ln-toast.js +1 -15
- package/js/ln-toast/src/ln-toast.js +232 -210
- package/js/ln-toast/template.html +16 -12
- package/js/ln-toggle/README.md +61 -2
- package/js/ln-toggle/ln-toggle-dev.scss +13 -0
- package/js/ln-toggle/ln-toggle.js +1 -1
- package/js/ln-toggle/src/ln-toggle.js +56 -43
- package/js/ln-tooltip/README.md +95 -58
- package/js/ln-tooltip/ln-tooltip-dev.scss +9 -0
- package/js/ln-tooltip/ln-tooltip.js +1 -1
- package/js/ln-tooltip/src/ln-tooltip.js +45 -6
- package/js/ln-translations/README.md +118 -96
- package/js/ln-translations/ln-translations.js +1 -1
- package/js/ln-translations/src/ln-translations.js +10 -5
- package/js/ln-upload/README.md +85 -10
- package/js/ln-upload/ln-upload.js +1 -1
- package/js/ln-upload/src/ln-upload.js +26 -10
- package/js/ln-validate/README.md +72 -0
- package/js/ln-validate/ln-validate-dev.scss +15 -0
- package/js/ln-validate/ln-validate.js +1 -1
- package/js/ln-validate/src/ln-validate.js +64 -0
- package/package.json +61 -56
- package/scss/base/_global.scss +2 -2
- package/scss/base/_typography.scss +1 -2
- package/scss/components/_date.scss +1 -0
- package/scss/components/_dropdown.scss +0 -5
- package/scss/components/_editor.scss +5 -0
- package/scss/components/_filter.scss +26 -25
- package/scss/components/_form.scss +7 -0
- package/scss/components/_ln-table.scss +13 -3
- package/scss/components/_sort.scss +10 -0
- package/scss/components/_table.scss +36 -23
- package/scss/components/_toast.scss +68 -41
- package/scss/components/_toggle.scss +16 -0
- package/scss/config/_density.scss +205 -117
- package/scss/config/_icons.scss +10 -9
- package/scss/config/_tokens.scss +69 -47
- package/scss/config/mixins/_accordion.scss +2 -7
- package/scss/config/mixins/_alert.scss +4 -4
- package/scss/config/mixins/_breadcrumbs.scss +2 -2
- package/scss/config/mixins/_btn.scss +1 -1
- package/scss/config/mixins/_card.scss +89 -14
- package/scss/config/mixins/_collapsible.scss +3 -0
- package/scss/config/mixins/_colors.scss +1 -1
- package/scss/config/mixins/_diagnostics.scss +11 -0
- package/scss/config/mixins/_display.scss +1 -0
- package/scss/config/mixins/_dropdown.scss +3 -5
- package/scss/config/mixins/_editor.scss +124 -0
- package/scss/config/mixins/_empty-state.scss +7 -0
- package/scss/config/mixins/_filter.scss +3 -5
- package/scss/config/mixins/_footer.scss +1 -2
- package/scss/config/mixins/_form.scss +207 -21
- package/scss/config/mixins/_index.scss +2 -0
- package/scss/config/mixins/_ln-table.scss +41 -2
- package/scss/config/mixins/_modal.scss +149 -66
- package/scss/config/mixins/_nav.scss +3 -4
- package/scss/config/mixins/_popover.scss +39 -0
- package/scss/config/mixins/_sidebar.scss +1 -1
- package/scss/config/mixins/_sort.scss +42 -0
- package/scss/config/mixins/_table.scss +85 -60
- package/scss/config/mixins/_toast.scss +179 -152
- package/scss/config/mixins/_tooltip.scss +17 -7
- package/scss/config/mixins/_typography.scss +4 -2
- package/scss/ln-ashlar-dev.scss +27 -9
- package/scss/ln-ashlar.scss +7 -1
- package/js/ln-data-table/README.md +0 -110
- package/js/ln-data-table/ln-data-table.js +0 -1
- package/js/ln-data-table/ln-data-table.scss +0 -10
- package/js/ln-data-table/src/ln-data-table.js +0 -1087
- package/js/ln-icons/README.md +0 -88
- package/js/ln-icons/ln-icons.js +0 -1
- package/js/ln-table/ln-table-sort.js +0 -1
- package/js/ln-table/src/ln-table-sort.js +0 -156
package/js/ln-modal/README.md
CHANGED
|
@@ -1,154 +1,185 @@
|
|
|
1
|
-
# ln-modal
|
|
2
|
-
|
|
3
|
-
> Focus-gated viewport-blocking dialog overlays, managed reactively via the DOM.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## 1. Philosophy & The Modal Mindset
|
|
8
|
-
|
|
9
|
-
In `ln-ashlar`, the core design principle is **
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
<!--
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
</
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
</
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
- **
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
modal.
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
modal.
|
|
72
|
-
|
|
73
|
-
//
|
|
74
|
-
modal.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
1
|
+
# ln-modal
|
|
2
|
+
|
|
3
|
+
> Focus-gated viewport-blocking dialog overlays, managed reactively via the DOM.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Philosophy & The Modal Mindset
|
|
8
|
+
|
|
9
|
+
In `ln-ashlar`, the core design principle is **Three-Layer Architecture** and **Component Isolation**.
|
|
10
|
+
|
|
11
|
+
`ln-modal` is a **Layer 1 Simple Component**. It manages ONLY the overlay panel state and dialog accessibility. It is completely isolated and unaware of sibling components, form logic, or data-fetching pipelines.
|
|
12
|
+
|
|
13
|
+
1. **State & Accessibility (JavaScript + native `<dialog>`)**: The `ln-modal` script manages the binary `open` / `close` attribute contract (`data-ln-modal="open|close"`) and suppresses `<body>` scrolling (toggling the `.ln-modal-open` class on `<body>`). Focus placement, native focus trapping, ESC dismissal, and focus restoration are delegated to the native `<dialog>` element via `showModal()`/`close()`.
|
|
14
|
+
2. **The Content Root (HTML)**: The modal content root is structured in standard semantic HTML (such as `<dialog class="ln-modal" data-ln-modal>`).
|
|
15
|
+
3. **Visual Presentation & Sizing (CSS)**: Overlay backdrops, sticky headers/footers, and scrollable body areas are styled using Vanilla CSS. Sizing variants (`modal-sm|md|lg|xl`) are applied via SCSS mixins on `> form`, keeping markup clean.
|
|
16
|
+
4. **Layer Separation (Coordinators)**: `ln-modal` does NOT handle form submissions, auto-closing on AJAX success, validation error inspection, or DOM data population. Those responsibilities belong strictly to **Layer 2 Coordinators** (e.g. `ln-modal-coordinator` or `ln-data-coordinator`).
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 2. Minimal Blueprint
|
|
21
|
+
|
|
22
|
+
Triggers and modals are paired by ID. The overlay has `class="ln-modal"` and `data-ln-modal`.
|
|
23
|
+
|
|
24
|
+
```html
|
|
25
|
+
<!-- Trigger button -->
|
|
26
|
+
<button data-ln-modal-for="user-modal">Add User</button>
|
|
27
|
+
|
|
28
|
+
<!-- Modal overlay -->
|
|
29
|
+
<dialog class="ln-modal" data-ln-modal id="user-modal">
|
|
30
|
+
<form>
|
|
31
|
+
<!-- Header -->
|
|
32
|
+
<header>
|
|
33
|
+
<h3>Add User</h3>
|
|
34
|
+
<button type="button" data-ln-modal-close aria-label="Close">
|
|
35
|
+
<svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-x"></use></svg>
|
|
36
|
+
</button>
|
|
37
|
+
</header>
|
|
38
|
+
|
|
39
|
+
<!-- Scrollable content -->
|
|
40
|
+
<main>
|
|
41
|
+
<label>Name <input type="text" name="name" autofocus></label>
|
|
42
|
+
</main>
|
|
43
|
+
|
|
44
|
+
<!-- Sticky footer -->
|
|
45
|
+
<footer>
|
|
46
|
+
<button type="button" data-ln-modal-close>Cancel</button>
|
|
47
|
+
<button type="submit">Save</button>
|
|
48
|
+
</footer>
|
|
49
|
+
</form>
|
|
50
|
+
</dialog>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Key Anatomy Rules
|
|
54
|
+
- **The Overlay (`data-ln-modal`)**: Driven by the value `"open"` (open) and `"close"` (closed).
|
|
55
|
+
- **The Trigger (`data-ln-modal-for="id"`)**: Placed on buttons/links to request toggling modal display.
|
|
56
|
+
- **The Dismiss button (`data-ln-modal-close`)**: Placed on cancel or close buttons to request close.
|
|
57
|
+
- **Focus Override (`autofocus`)**: Place on any form field to override default focus placement on open.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 3. Declarative API & State Contract
|
|
62
|
+
|
|
63
|
+
There are no imperative JavaScript mutation methods on the component instance. **The HTML attribute is the sole source of truth.**
|
|
64
|
+
|
|
65
|
+
Coordinators and external scripts request state changes by dispatching request events or writing the attribute on the modal element:
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
const modal = document.getElementById('user-modal');
|
|
69
|
+
|
|
70
|
+
// Request open via event
|
|
71
|
+
modal.dispatchEvent(new CustomEvent('ln-modal:request-open', { bubbles: true }));
|
|
72
|
+
|
|
73
|
+
// Or set attribute directly
|
|
74
|
+
modal.setAttribute('data-ln-modal', 'open');
|
|
75
|
+
|
|
76
|
+
// Request close via event
|
|
77
|
+
modal.dispatchEvent(new CustomEvent('ln-modal:request-close', { bubbles: true }));
|
|
78
|
+
|
|
79
|
+
// Read-only state query
|
|
80
|
+
const isOpen = modal.lnModal.isOpen; // Returns true/false
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Attributes
|
|
84
|
+
- `data-ln-modal`: Placed on the overlay element. Value `"open"` = open; `"close"` = closed.
|
|
85
|
+
- `data-ln-modal-for="id"`: Placed on trigger elements referencing the modal ID.
|
|
86
|
+
- `data-ln-modal-close`: Placed on any close trigger inside the modal.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 4. Transition & Command Events
|
|
91
|
+
|
|
92
|
+
All events bubble (`bubbles: true`). The dispatch target is the overlay element. Every event's `detail` carries `{ modalId, target }`.
|
|
93
|
+
|
|
94
|
+
| Event | Direction | Cancelable | Dispatched / Handled When |
|
|
95
|
+
|---|:---:|:---:|---|
|
|
96
|
+
| **`ln-modal:request-open`** | Listens | No | Command event sent by coordinators/triggers to open the modal. |
|
|
97
|
+
| **`ln-modal:request-close`** | Listens | No | Command event sent by coordinators/triggers to close the modal. |
|
|
98
|
+
| **`ln-modal:before-open`** | Emits | **Yes** | Dispatched before opening. Calling `event.preventDefault()` cancels the transition. |
|
|
99
|
+
| **`ln-modal:open`** | Emits | No | Dispatched after modal is active, body scroll locked, and initial focus set. |
|
|
100
|
+
| **`ln-modal:before-close`** | Emits | **Yes** | Dispatched before closing. Calling `event.preventDefault()` cancels the close transition. |
|
|
101
|
+
| **`ln-modal:close`** | Emits | No | Dispatched after modal is closed, scroll locks released, and focus restored. |
|
|
102
|
+
| **`ln-modal:destroyed`** | Emits | No | Dispatched when the component instance is destroyed. |
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
// Example: Block close transition if form has unsaved changes
|
|
106
|
+
const modal = document.getElementById('user-modal');
|
|
107
|
+
modal.addEventListener('ln-modal:before-close', (e) => {
|
|
108
|
+
if (formIsDirty()) {
|
|
109
|
+
e.preventDefault(); // Cancels transition and reverts attribute to "open"
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## 5. Visual Sizing & SCSS Mixins
|
|
117
|
+
|
|
118
|
+
Do not use visual layout utility classes in your markup. Apply structural sizing variants inside your SCSS to the modal form element:
|
|
119
|
+
|
|
120
|
+
| Mixin | `max-width` | Ideal For |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `@include modal-sm;` | `28rem` | Simple confirms, single-field inputs |
|
|
123
|
+
| `@include modal-md;` | `32rem` | Standard 2-4 field forms (Default) |
|
|
124
|
+
| `@include modal-lg;` | `42rem` | Wide multi-column forms, data lists |
|
|
125
|
+
| `@include modal-xl;` | `48rem` | Detail previews, large forms |
|
|
126
|
+
|
|
127
|
+
```scss
|
|
128
|
+
// Apply in project SCSS
|
|
129
|
+
.ln-modal {
|
|
130
|
+
@include modal-overlay;
|
|
131
|
+
|
|
132
|
+
> form {
|
|
133
|
+
@include modal-panel;
|
|
134
|
+
@include modal-md; // default
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
&#user-modal > form {
|
|
138
|
+
@include modal-lg; // Override specific modal width
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## 6. Hash Addressing & Deep-Linking
|
|
146
|
+
|
|
147
|
+
Any modal with an `id` is hash-addressable (e.g. `#user-modal` or `#user-modal:42`).
|
|
148
|
+
When an `id` modal opens, `ln-modal` emits `ln-modal:open` containing `{ hashNs, param }` in `e.detail`.
|
|
149
|
+
|
|
150
|
+
Coordinators (such as `ln-modal-coordinator`) intercept hash changes and coordinate data fetching / form populating via `ln-fill`.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 7. Related
|
|
155
|
+
- **[`ln-confirm`](../ln-confirm/README.md)** — Two-click inline confirm actions (lightweight alternative for single-element actions).
|
|
156
|
+
- **[`ln-form`](../ln-form/README.md)** — Form serialization and submission pipeline.
|
|
157
|
+
- **[`ln-validate`](../ln-validate/README.md)** — Declarative field-level constraints and visual errors.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## 🔧 Internals
|
|
162
|
+
|
|
163
|
+
Source: `js/ln-modal/src/ln-modal.js` (~137 lines, native `<dialog>`-backed). Trigger delegation (`data-ln-modal-for`), hash-addressing, and form-fill on open are NOT implemented here — they live in `js/ln-modal-coordinator/src/ln-modal-coordinator.js` (a Layer 2 coordinator per §1.4). This section covers `ln-modal.js` only.
|
|
164
|
+
|
|
165
|
+
### Single source of truth
|
|
166
|
+
|
|
167
|
+
`data-ln-modal` is the only state. `_syncAttribute(el)` (wired via `registerComponent`'s `onAttributeChange`) is the sole place open/close side effects happen — there is no imperative `open()`/`close()` method on the instance. Cancellation is attribute-revert: if `ln-modal:before-open`/`before-close` is prevented, the handler writes the attribute back to its prior value, which the same no-op guard (`shouldBeOpen === instance.isOpen`) swallows on the resulting mutation.
|
|
168
|
+
|
|
169
|
+
### Open / close
|
|
170
|
+
|
|
171
|
+
Open: cancelable `:before-open` → `instance.isOpen = true` → `body.ln-modal-open` class → `el.showModal()` (native `<dialog>` — top-layer stacking and backdrop are the platform's) → focus priority `[autofocus]` → first non-disabled input/select/textarea → first link/button, each filtered through `isVisible` → `:open`.
|
|
172
|
+
|
|
173
|
+
Close: cancelable `:before-close` → `instance.isOpen = false` → `:close` → `el.close()` (native — restores focus to the pre-open `document.activeElement` automatically) → if no other `[data-ln-modal="open"]` remains, remove `body.ln-modal-open`.
|
|
174
|
+
|
|
175
|
+
### ESC handling
|
|
176
|
+
|
|
177
|
+
Not a custom keydown listener — the native `<dialog>` fires its own `cancel` event on ESC. `_onCancel` calls `preventDefault()` (so the dialog doesn't close ahead of the attribute-driven flow) and writes `data-ln-modal="close"`, routing ESC through the same single attribute path as every other close trigger.
|
|
178
|
+
|
|
179
|
+
### Trigger/close wiring
|
|
180
|
+
|
|
181
|
+
The instance listens for `ln-modal:request-open`/`request-close` on the modal element itself (writes the attribute) and a `click` listener on the modal for `[data-ln-modal-close]` descendants. `data-ln-modal-for` trigger buttons are resolved by the coordinator's document-level delegated click listener, which looks up the modal by id and dispatches the request event — this file never sees the trigger element.
|
|
182
|
+
|
|
183
|
+
### Destroy
|
|
184
|
+
|
|
185
|
+
Removes the four listeners (`request-open`, `request-close`, `cancel`, `click`), releases the body scroll-lock class if this was the last open modal, dispatches `:destroyed`, deletes the instance.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
@use '../../scss/config/mixins' as *;
|
|
2
|
+
|
|
3
|
+
[data-ln-debug] {
|
|
4
|
+
// 1. Modal Trigger Check: [data-ln-modal-for] cannot have an empty target ID
|
|
5
|
+
[data-ln-modal-for=""] {
|
|
6
|
+
@include dev-inline-error("data-ln-modal-for is missing a target modal id");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// 2. Non-dialog modal: data-ln-modal must be on a <dialog> element (native showModal/close)
|
|
10
|
+
[data-ln-modal]:not(dialog) {
|
|
11
|
+
@include dev-inline-error("data-ln-modal must be on a <dialog> element");
|
|
12
|
+
}
|
|
13
|
+
}
|
package/js/ln-modal/ln-modal.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(){"use strict";function
|
|
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 b={};function I(t,e){b[t]||(b[t]=document.querySelector('[data-ln-template="'+t+'"]'));const i=b[t];return i?i.content.cloneNode(!0):(console.warn("["+(e||"ln-core")+'] Template "'+t+'" not found'),null)}function y(t,e,i){t.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:i||{}}))}function O(t,e,i){const c=new CustomEvent(e,{bubbles:!0,cancelable:!0,detail:i||{}});return t.dispatchEvent(c),c}function x(t,e){if(!t||!e)return t;const i=t.querySelectorAll("[data-ln-field]");for(let o=0;o<i.length;o++){const s=i[o],r=s.getAttribute("data-ln-field");e[r]!=null&&(s.textContent=e[r])}const c=t.querySelectorAll("[data-ln-attr]");for(let o=0;o<c.length;o++){const s=c[o],r=s.getAttribute("data-ln-attr").split(",");for(let d=0;d<r.length;d++){const a=r[d].trim().split(":");if(a.length!==2)continue;const u=a[0].trim(),f=a[1].trim();e[f]!=null&&s.setAttribute(u,e[f])}}const n=t.querySelectorAll("[data-ln-show]");for(let o=0;o<n.length;o++){const s=n[o],r=s.getAttribute("data-ln-show");r in e&&s.classList.toggle("hidden",!e[r])}const l=t.querySelectorAll("[data-ln-class]");for(let o=0;o<l.length;o++){const s=l[o],r=s.getAttribute("data-ln-class").split(",");for(let d=0;d<r.length;d++){const a=r[d].trim().split(":");if(a.length!==2)continue;const u=a[0].trim(),f=a[1].trim();f in e&&s.classList.toggle(u,!!e[f])}}return t}function R(t,e){t.matches&&t.matches("[data-ln-form], [data-ln-fillable]")&&t.dispatchEvent(new CustomEvent("ln-fill",{detail:e??null,bubbles:!0}));const i=t.querySelectorAll("[data-ln-form], [data-ln-fillable]");for(let c=0;c<i.length;c++)i[c].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)x(t.target,t.detail);else{const e=t.target.querySelectorAll("[data-ln-field]");for(let i=0;i<e.length;i++)e[i].textContent=""}})));function q(t,e){if(!t||!e)return t;const i=document.createTreeWalker(t,NodeFilter.SHOW_TEXT);for(;i.nextNode();){const l=i.currentNode;l.textContent.indexOf("{{")!==-1&&(l.textContent=l.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(o,s){return e[s]!==void 0?e[s]:""}))}const c=function(l,o){return e[o]!==void 0?e[o]:""},n=Array.from(t.querySelectorAll("*"));t.nodeType===1&&n.push(t);for(let l=0;l<n.length;l++){const o=n[l],s=o.attributes;for(let r=0;r<s.length;r++){const d=s[r];d.value.indexOf("{{")!==-1&&o.setAttribute(d.name,d.value.replace(/\{\{\s*(\w+)\s*\}\}/g,c))}}return t}function N(t,e,i,c,n,l){const o={};for(let r=0;r<t.children.length;r++){const d=t.children[r],a=d.getAttribute("data-ln-key");a&&(o[a]=d)}const s=document.createDocumentFragment();for(let r=0;r<e.length;r++){const d=e[r],a=String(c(d));let u=o[a];if(u)n(u,d,r);else{const f=I(i,l);if(!f||(q(f,d),u=f.firstElementChild,!u))continue;u.setAttribute("data-ln-key",a),n(u,d,r)}s.appendChild(u)}t.textContent="",t.appendChild(s)}function E(t,e){if(!document.body){document.addEventListener("DOMContentLoaded",function(){E(t,e)}),console.warn("["+e+'] Script loaded before <body> — add "defer" to your <script> tag');return}t()}function w(t,e,i,c){if(t.nodeType!==1)return;const l=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]",o=Array.from(t.querySelectorAll(l));t.matches&&t.matches(l)&&o.push(t);for(const s of o)s[i]||(s[i]=new c(s))}function g(t){return!!(t.offsetWidth||t.offsetHeight||t.getClientRects().length)}function j(t,e,i,c,n={}){const l=n.extraAttributes||[],o=n.onAttributeChange||null,s=n.onInit||null;function r(d){const a=d||document.body;w(a,t,e,i),s&&s(a)}return E(function(){const d=new MutationObserver(function(u){for(let f=0;f<u.length;f++){const h=u[f];if(h.type==="childList"){for(let m=0;m<h.addedNodes.length;m++){const p=h.addedNodes[m];p.nodeType===1&&(w(p,t,e,i),s&&s(p))}for(let m=0;m<h.removedNodes.length;m++){const p=h.removedNodes[m];if(p.nodeType===1){const T=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",A=Array.from(p.querySelectorAll(T));p.matches&&p.matches(T)&&A.push(p);for(let v=0;v<A.length;v++){const D=A[v];if(!document.contains(D)){const L=D[e];L&&typeof L.destroy=="function"&&L.destroy()}}}}}else h.type==="attributes"&&(o&&h.target[e]?o(h.target,h.attributeName):(w(h.target,t,e,i),s&&s(h.target)))}});let a=[];if(t.indexOf("[")!==-1){const u=/\[([\w-]+)/g;let f;for(;(f=u.exec(t))!==null;)a.push(f[1])}else a.push(t);d.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:a.concat(l)})},c),window[e]=r,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){r(document.body)}):r(document.body),r}const _={};function F(t,e){_[t]=e}function W(t){return _[t]||{ingress:e=>e,egress:e=>e}}const S={};function k(t,e){if(!t||typeof e!="object")return;const i=t.toLowerCase().split("-")[0];S[i]=e}function B(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=F,window.lnCore.getDataMapper=W,window.lnCore.registerLocaleFallback=k,window.lnCore.getLocaleFallback=B,window.lnCore.fillTemplate=q,window.lnCore.fill=x,window.lnCore.lnFill=R,window.lnCore.renderList=N);function M(t){return(t||"").replace(/^#/,"")}function C(t){const e=t===void 0?location.hash:t,i={},c=M(e);if(!c)return i;const n=c.split("&");for(let l=0;l<n.length;l++){const o=n[l];if(!o)continue;const s=o.indexOf(":"),r=s>-1?o.slice(0,s):o,d=s>-1?o.slice(s+1):"";if(r)try{i[r]=decodeURIComponent(d)}catch{i[r]=d}}return i}function K(t){if(!t)return null;const e=C();return t in e?e[t]:null}function P(t,e){if(!t)return;const i=C();e==null?delete i[t]:i[t]=String(e);const n=Object.keys(i).map(function(l){const o=i[l];return o===""?l:l+":"+encodeURIComponent(o)}).join("&");M(location.hash)!==n&&(location.hash=n)}function U(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=C,window.lnCore.hashGet=K,window.lnCore.hashSet=P,window.lnCore.hashLinkClick=U),(function(){const t="data-ln-modal",e="lnModal";if(window[e]!==void 0)return;function i(n){this.dom=n,this.isOpen=n.getAttribute(t)==="open";const l=this;return this._onRequestOpen=function(){l.dom.setAttribute(t,"open")},this._onRequestClose=function(){l.dom.setAttribute(t,"close")},this._onCancel=function(o){o.preventDefault(),l.dom.setAttribute(t,"close")},this._onClickClose=function(o){const s=o.target.closest("[data-ln-modal-close]");s&&l.dom.contains(s)&&(o.preventDefault(),l.dom.setAttribute(t,"close"))},this.dom.addEventListener("ln-modal:request-open",this._onRequestOpen),this.dom.addEventListener("ln-modal:request-close",this._onRequestClose),this.dom.addEventListener("cancel",this._onCancel),this.dom.addEventListener("click",this._onClickClose),this.isOpen&&(typeof this.dom.showModal=="function"&&this.dom.showModal(),document.body.classList.add("ln-modal-open")),this}i.prototype.destroy=function(){if(this.dom[e]){if(this.dom.removeEventListener("ln-modal:request-open",this._onRequestOpen),this.dom.removeEventListener("ln-modal:request-close",this._onRequestClose),this.dom.removeEventListener("cancel",this._onCancel),this.dom.removeEventListener("click",this._onClickClose),this.isOpen){const n=this.dom;Array.prototype.some.call(document.querySelectorAll("["+t+'="open"]'),function(o){return o!==n})||document.body.classList.remove("ln-modal-open")}y(this.dom,"ln-modal:destroyed",{modalId:this.dom.id,target:this.dom}),delete this.dom[e]}};function c(n){const l=n[e];if(!l)return;const s=n.getAttribute(t)==="open";if(s!==l.isOpen)if(s){if(O(n,"ln-modal:before-open",{modalId:n.id,target:n}).defaultPrevented){n.setAttribute(t,"close");return}l.isOpen=!0,document.body.classList.add("ln-modal-open"),typeof n.showModal=="function"&&n.showModal();const d=n.querySelector("[autofocus]");if(d&&g(d))d.focus();else{const a=n.querySelectorAll('input:not([disabled]):not([type="hidden"]), textarea:not([disabled]), select:not([disabled])'),u=Array.prototype.find.call(a,g);if(u)u.focus();else{const f=n.querySelectorAll("a[href], button:not([disabled])"),h=Array.prototype.find.call(f,g);h&&h.focus()}}y(n,"ln-modal:open",{modalId:n.id,target:n})}else{if(O(n,"ln-modal:before-close",{modalId:n.id,target:n}).defaultPrevented){n.setAttribute(t,"open");return}l.isOpen=!1,y(n,"ln-modal:close",{modalId:n.id,target:n}),typeof n.close=="function"&&n.close(),document.querySelector("["+t+'="open"]')||document.body.classList.remove("ln-modal-open")}}j(t,e,i,"ln-modal",{onAttributeChange:c})})()})();
|
|
@@ -1,11 +1,35 @@
|
|
|
1
1
|
// ── JS state: modal open/close ──
|
|
2
|
-
// data-ln-modal attribute drives visibility. JS
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
// data-ln-modal attribute drives visibility. JS calls native showModal()/close();
|
|
3
|
+
// this attribute is the CSS surface — reset native <dialog> UA styles and drive
|
|
4
|
+
// the flex-centering layout on top.
|
|
5
|
+
[data-ln-modal] {
|
|
6
|
+
background: transparent;
|
|
7
|
+
border: none;
|
|
8
|
+
padding: 0;
|
|
9
|
+
margin: 0;
|
|
10
|
+
width: 100%;
|
|
11
|
+
height: 100%;
|
|
12
|
+
max-width: none;
|
|
13
|
+
max-height: none;
|
|
14
|
+
color: inherit;
|
|
15
|
+
overflow: visible;
|
|
16
|
+
|
|
17
|
+
&[data-ln-modal="open"] {
|
|
18
|
+
display: flex;
|
|
19
|
+
}
|
|
6
20
|
}
|
|
7
21
|
|
|
8
22
|
// Prevent body scroll when modal is open
|
|
9
23
|
body.ln-modal-open {
|
|
10
24
|
overflow: hidden;
|
|
11
25
|
}
|
|
26
|
+
|
|
27
|
+
// Mode-conditional title variants: shown only when ancestor modal matches the mode
|
|
28
|
+
[data-ln-modal-when] {
|
|
29
|
+
display: none;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
[data-ln-modal-mode="new"] [data-ln-modal-when="new"],
|
|
33
|
+
[data-ln-modal-mode="edit"] [data-ln-modal-when="edit"] {
|
|
34
|
+
display: inline;
|
|
35
|
+
}
|