@livenetworks/ashlar 1.4.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +162 -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 +77 -41
- 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 +189 -99
- 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 -55
- 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 -0
- package/scss/components/_form.scss +7 -0
- package/scss/components/_ln-table.scss +14 -5
- package/scss/components/_sort.scss +10 -0
- package/scss/components/_table.scss +68 -1
- 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 +48 -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 +47 -0
- package/scss/config/mixins/_footer.scss +1 -2
- package/scss/config/mixins/_form.scss +207 -21
- package/scss/config/mixins/_index.scss +4 -1
- package/scss/config/mixins/_ln-table.scss +51 -93
- 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 +232 -38
- 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 +39 -0
- package/scss/ln-ashlar.scss +8 -2
- 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 -1103
- 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 -168
- package/scss/components/_data-table.scss +0 -83
- package/scss/config/mixins/_data-table.scss +0 -346
|
@@ -10,11 +10,11 @@ import { guardBody, dispatch, buildDict, cloneTemplateScoped, fill } from '../..
|
|
|
10
10
|
const DEFAULT_ITEM_TEMPLATE_HTML =
|
|
11
11
|
'<template data-ln-template="ln-upload-item">' +
|
|
12
12
|
'<li class="ln-upload__item" data-ln-class="ln-upload__item--uploading:uploading, ln-upload__item--error:error, ln-upload__item--deleting:deleting">' +
|
|
13
|
-
'<svg class="ln-icon" aria-hidden="true"><use data-ln-attr="href:iconHref" href="#ln-file"></use></svg>' +
|
|
13
|
+
'<svg class="ln-icon" aria-hidden="true"><use data-ln-attr="href:iconHref" href="#ln-icon-file"></use></svg>' +
|
|
14
14
|
'<span class="ln-upload__name" data-ln-field="name"></span>' +
|
|
15
15
|
'<span class="ln-upload__size" data-ln-field="sizeText"></span>' +
|
|
16
16
|
'<button type="button" class="ln-upload__remove" data-ln-upload-action="remove" data-ln-attr="aria-label:removeLabel, title:removeLabel">' +
|
|
17
|
-
'<svg class="ln-icon" aria-hidden="true"><use href="#ln-x"></use></svg>' +
|
|
17
|
+
'<svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-x"></use></svg>' +
|
|
18
18
|
'</button>' +
|
|
19
19
|
'<div class="ln-upload__progress"><div class="ln-upload__progress-bar"></div></div>' +
|
|
20
20
|
'</li>' +
|
|
@@ -47,7 +47,7 @@ import { guardBody, dispatch, buildDict, cloneTemplateScoped, fill } from '../..
|
|
|
47
47
|
function _getIconId(extension) {
|
|
48
48
|
if (extension === 'docx') extension = 'doc';
|
|
49
49
|
const supported = ['pdf', 'doc', 'epub'];
|
|
50
|
-
return supported.includes(extension) ? '
|
|
50
|
+
return supported.includes(extension) ? 'ln-icon-custom-file-' + extension : 'ln-icon-file';
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
function _isValidFile(file, acceptString) {
|
|
@@ -58,8 +58,7 @@ import { guardBody, dispatch, buildDict, cloneTemplateScoped, fill } from '../..
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function _initUpload(container) {
|
|
61
|
-
if (container.
|
|
62
|
-
container.setAttribute('data-ln-upload-initialized', 'true');
|
|
61
|
+
if (container.lnUploadAPI) return;
|
|
63
62
|
|
|
64
63
|
_ensureDefaultItemTemplate();
|
|
65
64
|
const dict = buildDict(container, DICT_SELECTOR);
|
|
@@ -87,7 +86,9 @@ import { guardBody, dispatch, buildDict, cloneTemplateScoped, fill } from '../..
|
|
|
87
86
|
container.appendChild(input);
|
|
88
87
|
}
|
|
89
88
|
const uploadUrl = container.getAttribute(DOM_SELECTOR) || '/files/upload';
|
|
90
|
-
const
|
|
89
|
+
const uploadContextFallback = container.getAttribute(CONTEXT_ATTR) || '';
|
|
90
|
+
const deleteUrlPattern = container.getAttribute('data-ln-upload-delete') ||
|
|
91
|
+
(uploadUrl.includes('/upload') ? uploadUrl.replace(/\/upload\/?$/, '/{id}') : uploadUrl + '/{id}');
|
|
91
92
|
|
|
92
93
|
const uploadedFiles = new Map();
|
|
93
94
|
let fileIdCounter = 0;
|
|
@@ -142,7 +143,21 @@ import { guardBody, dispatch, buildDict, cloneTemplateScoped, fill } from '../..
|
|
|
142
143
|
|
|
143
144
|
const formData = new FormData();
|
|
144
145
|
formData.append('file', file);
|
|
145
|
-
|
|
146
|
+
|
|
147
|
+
const gatheredNames = new Set();
|
|
148
|
+
container.querySelectorAll('input, select, textarea').forEach(function (el) {
|
|
149
|
+
if (el.name && el.name !== 'file_ids[]' && el.type !== 'file') {
|
|
150
|
+
if ((el.type === 'checkbox' || el.type === 'radio') && !el.checked) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
formData.append(el.name, el.value);
|
|
154
|
+
gatheredNames.add(el.name);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (!gatheredNames.has('context') && uploadContextFallback) {
|
|
159
|
+
formData.append('context', uploadContextFallback);
|
|
160
|
+
}
|
|
146
161
|
|
|
147
162
|
const xhr = new XMLHttpRequest();
|
|
148
163
|
|
|
@@ -244,7 +259,8 @@ import { guardBody, dispatch, buildDict, cloneTemplateScoped, fill } from '../..
|
|
|
244
259
|
|
|
245
260
|
if (item) fill(item, { deleting: true });
|
|
246
261
|
|
|
247
|
-
|
|
262
|
+
const deleteUrl = deleteUrlPattern.replace('{id}', fileData.serverId);
|
|
263
|
+
fetch(deleteUrl, {
|
|
248
264
|
method: 'DELETE',
|
|
249
265
|
headers: {
|
|
250
266
|
'X-CSRF-TOKEN': getCsrfToken(),
|
|
@@ -322,7 +338,8 @@ import { guardBody, dispatch, buildDict, cloneTemplateScoped, fill } from '../..
|
|
|
322
338
|
clear: function () {
|
|
323
339
|
for (const [, fileData] of uploadedFiles) {
|
|
324
340
|
if (fileData.serverId) {
|
|
325
|
-
|
|
341
|
+
const deleteUrl = deleteUrlPattern.replace('{id}', fileData.serverId);
|
|
342
|
+
fetch(deleteUrl, {
|
|
326
343
|
method: 'DELETE',
|
|
327
344
|
headers: {
|
|
328
345
|
'X-CSRF-TOKEN': getCsrfToken(),
|
|
@@ -347,7 +364,6 @@ import { guardBody, dispatch, buildDict, cloneTemplateScoped, fill } from '../..
|
|
|
347
364
|
uploadedFiles.clear();
|
|
348
365
|
list.innerHTML = '';
|
|
349
366
|
updateHiddenInput();
|
|
350
|
-
container.removeAttribute('data-ln-upload-initialized');
|
|
351
367
|
delete container.lnUploadAPI;
|
|
352
368
|
}
|
|
353
369
|
};
|
package/js/ln-validate/README.md
CHANGED
|
@@ -11,6 +11,22 @@ It maintains no custom rules in JavaScript; instead, it relies fully on native H
|
|
|
11
11
|
1. **Platform First:** Browsers compile and execute rules in native C++ instantly. `ln-validate` simply acts as a visual layer: translating native `ValidityState` properties into visible HTML elements on demand.
|
|
12
12
|
2. **The Touch Gate:** To prevent annoying page-load errors, validation is completely visual-silent until a field receives its first user interaction (`input` or `change`), setting its internal state `_touched = true`.
|
|
13
13
|
3. **The Custom Escape Hatch:** Validation rules the browser cannot express (such as checking if an email is taken via a server lookup, or confirming passwords match) are routed through asynchronous `set-custom` / `clear-custom` events.
|
|
14
|
+
4. **Owns Browser Validation:** The moment a form contains at least one
|
|
15
|
+
`data-ln-validate` field, that field's constructor injects
|
|
16
|
+
`novalidate` on the host `<form>` (via `field.form`, so `form="id"`
|
|
17
|
+
attribute association works too). Authors never write `novalidate`
|
|
18
|
+
by hand. A form with zero `data-ln-validate` fields is untouched and
|
|
19
|
+
keeps native browser validation as the default. The injection is
|
|
20
|
+
idempotent and one-way — it is never removed on field `destroy()`,
|
|
21
|
+
since other validated fields on the same form may still own the
|
|
22
|
+
gate.
|
|
23
|
+
5. **Owns the Submit Gate:** The same field that injects `novalidate`
|
|
24
|
+
also attaches (once per form) a `submit` listener: on submit, it
|
|
25
|
+
dispatches `ln-validate:request-validate` to collect invalid fields;
|
|
26
|
+
if any exist, it calls `preventDefault()`, sorts them by document
|
|
27
|
+
position, and focuses the first one. Runs on every HTTP method — GET
|
|
28
|
+
included, exactly like the native validation it replaces. `ln-form`
|
|
29
|
+
has no role in this at all.
|
|
14
30
|
|
|
15
31
|
---
|
|
16
32
|
|
|
@@ -98,6 +114,7 @@ input.lnValidate.destroy();
|
|
|
98
114
|
| :--- | :--- | :--- |
|
|
99
115
|
| `ln-validate:set-custom` | `{ error: String }` | Injects a custom error key, highlights the field as invalid. |
|
|
100
116
|
| `ln-validate:clear-custom` | `{ error: String }` / `{}` | Clears a specific custom error, or all custom errors at once. |
|
|
117
|
+
| `ln-validate:request-validate` | `{ invalidFields: Array }` | Dispatched by `ln-validate`'s own submit gate (see Philosophy §5). Forces validation (sets `_touched = true`, calls `validate()`) and pushes the field element to the `invalidFields` array if invalid. |
|
|
101
118
|
|
|
102
119
|
---
|
|
103
120
|
|
|
@@ -106,3 +123,58 @@ input.lnValidate.destroy();
|
|
|
106
123
|
- **Skipping the `.form-element` wrapper:** `ln-validate` locates sibling error elements relative to the closest `.form-element` ancestor. If you omit the wrapper class, no error lists will be toggled.
|
|
107
124
|
- **Relying on Native browser CSS (`:invalid`):** The browser applies `:invalid` to empty required fields on page load. **Always** use our custom classes `.ln-validate-valid` and `.ln-validate-invalid` for visual borders and focus states.
|
|
108
125
|
- **Injecting custom errors without validating:** Dispatching `ln-validate:set-custom` updates visual classes, but it **does not** bubble an `:invalid` event. If you need form-level coordinators (`ln-form`) to react immediately, call `input.lnValidate.validate()` right after injecting.
|
|
126
|
+
- **Expecting `novalidate` in your own markup:** Don't write it by hand
|
|
127
|
+
— `ln-validate` injects it on the host form as soon as one field
|
|
128
|
+
initializes. Writing it yourself is harmless (the injection checks
|
|
129
|
+
`hasAttribute` first) but unnecessary.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## 🔧 Internals
|
|
134
|
+
|
|
135
|
+
Source: `js/ln-validate/ln-validate.js` (~160 lines). Imports only `dispatch`/`registerComponent` from `ln-core` — no sibling-component imports.
|
|
136
|
+
|
|
137
|
+
### State
|
|
138
|
+
|
|
139
|
+
Three fields per instance (`dom.lnValidate`): `dom`, `_touched` (bool), `_customErrors` (Set). No cached error-`<li>` array, no cached `.form-element` ancestor reference, no debounce — every `validate()` call re-queries `parent.querySelectorAll` and recomputes from `dom.checkValidity()`. Cheap because the query is scoped to one field's wrapper and validity checks are microsecond-fast.
|
|
140
|
+
|
|
141
|
+
### Listener discriminator
|
|
142
|
+
|
|
143
|
+
`input`+`change` fire on `<input>`/`<textarea>`; `change`-only fires on `<select>`, checkbox, radio (`input` never fires on these). This exact fork is mirrored in `ln-form.fill()`, `ln-form.reset()`, and `ln-autosave`'s restore path — not enforced by a shared helper, just a documented invariant all three must keep in sync.
|
|
144
|
+
|
|
145
|
+
### `validate()` flow
|
|
146
|
+
|
|
147
|
+
1. `nativeValid = dom.checkValidity()`; `isValid = nativeValid && _customErrors.size === 0`.
|
|
148
|
+
2. Walks the field's `.form-element`'s `[data-ln-validate-error]` `<li>`s; keys present in `ERROR_MAP` (`required`/`typeMismatch`/`tooShort`/`tooLong`/`patternMismatch`/`rangeUnderflow`/`rangeOverflow`) toggle `hidden` off `validity[prop]`; keys NOT in the map are skipped — that's the mechanism that lets native and custom errors share one `<ul>` without conflict.
|
|
149
|
+
3. Toggles `.ln-validate-valid`/`.ln-validate-invalid`, dispatches `:valid`/`:invalid` `{ target, field: dom.name }`.
|
|
150
|
+
4. If the field has no `.form-element` ancestor, error rendering is skipped but the class toggle and dispatch still run.
|
|
151
|
+
|
|
152
|
+
`validate()` never reads `_touched` — it always renders. `_touched` instead gates whether the *listener handlers* call it, so the submit gate can force-validate an untouched field on demand.
|
|
153
|
+
|
|
154
|
+
### Custom-error asymmetry
|
|
155
|
+
|
|
156
|
+
`set-custom` (`_onSetCustom`) adds to `_customErrors`, unhides the matching `<li>`, flips classes to invalid — but does **not** call `validate()` or dispatch `:invalid`. The caller already knows the field is invalid; a coordinator that needs `ln-form`'s submit gate to react must call `instance.validate()` itself afterwards.
|
|
157
|
+
|
|
158
|
+
`clear-custom` (`_onClearCustom`) is the counterweight: clear-one or clear-all, then `if (_touched) validate()` — removing one custom error can leave others active, leave native errors active, or leave the field fully valid, and only `validate()` can recompute which.
|
|
159
|
+
|
|
160
|
+
### The submit gate
|
|
161
|
+
|
|
162
|
+
The first `data-ln-validate` field on a form injects `novalidate` (idempotent, one-way, never removed on `destroy()`) and attaches one `submit` listener to the form, guarded by a `form._lnValidateGateBound` marker so N fields produce exactly one listener. On submit: dispatches `ln-validate:request-validate`; every field's own handler force-validates and pushes itself into `invalidFields` if invalid; if any exist, `preventDefault()`, sort by document position, focus the first. Runs on every HTTP method, exactly like the native validation it replaces. `ln-form` has no involvement — it never listens for `submit`. `ln-data-coordinator`'s own method gate reads `e.defaultPrevented` first, so it never claims a submit this gate already blocked.
|
|
163
|
+
|
|
164
|
+
### `isValid` getter
|
|
165
|
+
|
|
166
|
+
Pure read (`dom.checkValidity() && _customErrors.size === 0`) — no side effects, no rendering, no `_touched` mutation. For "is this field currently valid?" checks that shouldn't trigger the render pipeline.
|
|
167
|
+
|
|
168
|
+
### `reset()` / `destroy()`
|
|
169
|
+
|
|
170
|
+
`reset()`: `_touched = false`, clears `_customErrors`, removes valid/invalid classes, hides every `[data-ln-validate-error]` (native and custom alike). Does NOT dispatch `:valid`/`:invalid` (the field is "untouched," not "newly valid") and does NOT touch `dom.value` — the form element owns that; `ln-form.reset()` calls the native `dom.reset()` first, then dispatches synthetic `input`/`change` per field, then calls `instance.reset()`.
|
|
171
|
+
|
|
172
|
+
`destroy()` removes the four listeners (`input`, `change`, `set-custom`, `clear-custom`), removes the CSS classes (but leaves error `<li>` visibility as-is — a different intent than `reset()`), dispatches `:destroyed`, deletes the instance. Idempotent via a `DOM_ATTRIBUTE` guard.
|
|
173
|
+
|
|
174
|
+
### MutationObserver
|
|
175
|
+
|
|
176
|
+
`registerComponent` wires the shared observer: `childList` upgrades newly-inserted `[data-ln-validate]` fields, `attributes` (filtered to `data-ln-validate`) upgrades the attribute added to an existing element. Removal is not observed — call `instance.destroy()` before detaching. Changes to `data-ln-validate-error`/`-errors` are not observed either (typically static server-rendered markup) — a manual `instance.validate()` call re-renders if that markup changes at runtime.
|
|
177
|
+
|
|
178
|
+
### Cross-component contract
|
|
179
|
+
|
|
180
|
+
`ln-form` listens for bubbling `:valid`/`:invalid` to re-evaluate submit-button state, and dispatches synthetic `input`/`change` on `fill()`/`reset()` using the identical listener discriminator; `ln-autosave` does the same on restore. None of these components import each other — the wiring is pure platform event flow, so keeping the discriminator in sync across all three files is a manual invariant, not a compile-time one.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
@use '../../scss/config/mixins' as *;
|
|
2
|
+
|
|
3
|
+
[data-ln-debug] {
|
|
4
|
+
// 1. Validation Input Missing ID: input, select, textarea elements utilizing data-ln-validate must have an ID
|
|
5
|
+
input[data-ln-validate]:not([id]),
|
|
6
|
+
select[data-ln-validate]:not([id]),
|
|
7
|
+
textarea[data-ln-validate]:not([id]) {
|
|
8
|
+
@include dev-inline-error("data-ln-validate element is missing required 'id' attribute");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// 2. Validation Nesting check: data-ln-validate must be wrapped inside .form-element or data-ln-form
|
|
12
|
+
[data-ln-validate]:not(.form-element [data-ln-validate]):not([data-ln-form] [data-ln-validate]) {
|
|
13
|
+
@include dev-dom-error("data-ln-validate must be a child of .form-element or data-ln-form wrapper");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(){"use strict";function C(t,e,f){t.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:f||{}}))}function w(t,e){if(!document.body){document.addEventListener("DOMContentLoaded",function(){w(t,e)}),console.warn("["+e+'] Script loaded before <body> — add "defer" to your <script> tag');return}t()}function L(t,e,f,h){if(t.nodeType!==1)return;const v=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]",p=Array.from(t.querySelectorAll(v));t.matches&&t.matches(v)&&p.push(t);for(const a of p)a[f]||(a[f]=new h(a))}function O(t,e,f,h,m={}){const v=m.extraAttributes||[],p=m.onAttributeChange||null,a=m.onInit||null;function n(i){const c=i||document.body;L(c,t,e,f),a&&a(c)}return w(function(){const i=new MutationObserver(function(l){for(let u=0;u<l.length;u++){const s=l[u];if(s.type==="childList"){for(let r=0;r<s.addedNodes.length;r++){const o=s.addedNodes[r];o.nodeType===1&&(L(o,t,e,f),a&&a(o))}for(let r=0;r<s.removedNodes.length;r++){const o=s.removedNodes[r];if(o.nodeType===1){const y=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",g=Array.from(o.querySelectorAll(y));o.matches&&o.matches(y)&&g.push(o);for(let _=0;_<g.length;_++){const b=g[_];if(!document.contains(b)){const E=b[e];E&&typeof E.destroy=="function"&&E.destroy()}}}}}else s.type==="attributes"&&(p&&s.target[e]?p(s.target,s.attributeName):(L(s.target,t,e,f),a&&a(s.target)))}});let c=[];if(t.indexOf("[")!==-1){const l=/\[([\w-]+)/g;let u;for(;(u=l.exec(t))!==null;)c.push(u[1])}else c.push(t);i.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:c.concat(v)})},h),window[e]=n,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){n(document.body)}):n(document.body),n}const S={};function A(t,e){S[t]=e}function M(t){return S[t]||{ingress:e=>e,egress:e=>e}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=A,window.lnCore.getDataMapper=M),(function(){const t="data-ln-validate",e="lnValidate",f="data-ln-validate-errors",h="data-ln-validate-error",m="ln-validate-valid",v="ln-validate-invalid",p={required:"valueMissing",typeMismatch:"typeMismatch",tooShort:"tooShort",tooLong:"tooLong",patternMismatch:"patternMismatch",rangeUnderflow:"rangeUnderflow",rangeOverflow:"rangeOverflow"};if(window[e]!==void 0)return;function a(n){this.dom=n,this._touched=!1,this._customErrors=new Set;const i=this,c=n.tagName,l=n.type,u=c==="SELECT"||l==="checkbox"||l==="radio";return this._onInput=function(){i._touched=!0,i.validate()},this._onChange=function(){i._touched=!0,i.validate()},this._onSetCustom=function(s){const r=s.detail&&s.detail.error;if(!r)return;i._customErrors.add(r),i._touched=!0;const o=n.closest(".form-element");if(o){const d=o.querySelector("["+h+'="'+r+'"]');d&&d.classList.remove("hidden")}n.classList.remove(m),n.classList.add(v)},this._onClearCustom=function(s){const r=s.detail&&s.detail.error,o=n.closest(".form-element");if(r){if(i._customErrors.delete(r),o){const d=o.querySelector("["+h+'="'+r+'"]');d&&d.classList.add("hidden")}}else i._customErrors.forEach(function(d){if(o){const y=o.querySelector("["+h+'="'+d+'"]');y&&y.classList.add("hidden")}}),i._customErrors.clear();i._touched&&i.validate()},u||n.addEventListener("input",this._onInput),n.addEventListener("change",this._onChange),n.addEventListener("ln-validate:set-custom",this._onSetCustom),n.addEventListener("ln-validate:clear-custom",this._onClearCustom),this}a.prototype.validate=function(){const n=this.dom,i=n.validity,l=n.checkValidity()&&this._customErrors.size===0,u=n.closest(".form-element");if(u){const r=u.querySelector("["+f+"]");if(r){const o=r.querySelectorAll("["+h+"]");for(let d=0;d<o.length;d++){const y=o[d].getAttribute(h),g=p[y];g&&(i[g]?o[d].classList.remove("hidden"):o[d].classList.add("hidden"))}}}return n.classList.toggle(m,l),n.classList.toggle(v,!l),C(n,l?"ln-validate:valid":"ln-validate:invalid",{target:n,field:n.name}),l},a.prototype.reset=function(){this._touched=!1,this._customErrors.clear(),this.dom.classList.remove(m,v);const n=this.dom.closest(".form-element");if(n){const i=n.querySelectorAll("["+h+"]");for(let c=0;c<i.length;c++)i[c].classList.add("hidden")}},Object.defineProperty(a.prototype,"isValid",{get:function(){return this.dom.checkValidity()&&this._customErrors.size===0}}),a.prototype.destroy=function(){this.dom[e]&&(this.dom.removeEventListener("input",this._onInput),this.dom.removeEventListener("change",this._onChange),this.dom.removeEventListener("ln-validate:set-custom",this._onSetCustom),this.dom.removeEventListener("ln-validate:clear-custom",this._onClearCustom),this.dom.classList.remove(m,v),C(this.dom,"ln-validate:destroyed",{target:this.dom}),delete this.dom[e])},O(t,e,a,"ln-validate")})()})();
|
|
1
|
+
(function(){"use strict";if(typeof window<"u"){const t=console.warn;console.warn=function(...e){typeof e[0]=="string"&&(e[0].startsWith("[ln-")||e[0].startsWith("[lnCore"))&&!(document.documentElement.hasAttribute("data-ln-debug")||document.body&&document.body.hasAttribute("data-ln-debug"))||t.apply(console,e)}}const w={};function D(t,e){w[t]||(w[t]=document.querySelector('[data-ln-template="'+t+'"]'));const r=w[t];return r?r.content.cloneNode(!0):(console.warn("["+(e||"ln-core")+'] Template "'+t+'" not found'),null)}function C(t,e,r){t.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:r||{}}))}function S(t,e){if(!t||!e)return t;const r=t.querySelectorAll("[data-ln-field]");for(let o=0;o<r.length;o++){const s=r[o],n=s.getAttribute("data-ln-field");e[n]!=null&&(s.textContent=e[n])}const c=t.querySelectorAll("[data-ln-attr]");for(let o=0;o<c.length;o++){const s=c[o],n=s.getAttribute("data-ln-attr").split(",");for(let i=0;i<n.length;i++){const a=n[i].trim().split(":");if(a.length!==2)continue;const f=a[0].trim(),p=a[1].trim();e[p]!=null&&s.setAttribute(f,e[p])}}const u=t.querySelectorAll("[data-ln-show]");for(let o=0;o<u.length;o++){const s=u[o],n=s.getAttribute("data-ln-show");n in e&&s.classList.toggle("hidden",!e[n])}const l=t.querySelectorAll("[data-ln-class]");for(let o=0;o<l.length;o++){const s=l[o],n=s.getAttribute("data-ln-class").split(",");for(let i=0;i<n.length;i++){const a=n[i].trim().split(":");if(a.length!==2)continue;const f=a[0].trim(),p=a[1].trim();p in e&&s.classList.toggle(f,!!e[p])}}return t}function M(t,e){t.matches&&t.matches("[data-ln-form], [data-ln-fillable]")&&t.dispatchEvent(new CustomEvent("ln-fill",{detail:e??null,bubbles:!0}));const r=t.querySelectorAll("[data-ln-form], [data-ln-fillable]");for(let c=0;c<r.length;c++)r[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)S(t.target,t.detail);else{const e=t.target.querySelectorAll("[data-ln-field]");for(let r=0;r<e.length;r++)e[r].textContent=""}})));function A(t,e){if(!t||!e)return t;const r=document.createTreeWalker(t,NodeFilter.SHOW_TEXT);for(;r.nextNode();){const l=r.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]:""},u=Array.from(t.querySelectorAll("*"));t.nodeType===1&&u.push(t);for(let l=0;l<u.length;l++){const o=u[l],s=o.attributes;for(let n=0;n<s.length;n++){const i=s[n];i.value.indexOf("{{")!==-1&&o.setAttribute(i.name,i.value.replace(/\{\{\s*(\w+)\s*\}\}/g,c))}}return t}function N(t,e,r,c,u,l){const o={};for(let n=0;n<t.children.length;n++){const i=t.children[n],a=i.getAttribute("data-ln-key");a&&(o[a]=i)}const s=document.createDocumentFragment();for(let n=0;n<e.length;n++){const i=e[n],a=String(c(i));let f=o[a];if(f)u(f,i,n);else{const p=D(r,l);if(!p||(A(p,i),f=p.firstElementChild,!f))continue;f.setAttribute("data-ln-key",a),u(f,i,n)}s.appendChild(f)}t.textContent="",t.appendChild(s)}function O(t,e){if(!document.body){document.addEventListener("DOMContentLoaded",function(){O(t,e)}),console.warn("["+e+'] Script loaded before <body> — add "defer" to your <script> tag');return}t()}function b(t,e,r,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[r]||(s[r]=new c(s))}function V(t,e,r,c,u={}){const l=u.extraAttributes||[],o=u.onAttributeChange||null,s=u.onInit||null;function n(i){const a=i||document.body;b(a,t,e,r),s&&s(a)}return O(function(){const i=new MutationObserver(function(f){for(let p=0;p<f.length;p++){const h=f[p];if(h.type==="childList"){for(let m=0;m<h.addedNodes.length;m++){const d=h.addedNodes[m];d.nodeType===1&&(b(d,t,e,r),s&&s(d))}for(let m=0;m<h.removedNodes.length;m++){const d=h.removedNodes[m];if(d.nodeType===1){const g=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",y=Array.from(d.querySelectorAll(g));d.matches&&d.matches(g)&&y.push(d);for(let _=0;_<y.length;_++){const T=y[_];if(!document.contains(T)){const E=T[e];E&&typeof E.destroy=="function"&&E.destroy()}}}}}else h.type==="attributes"&&(o&&h.target[e]?o(h.target,h.attributeName):(b(h.target,t,e,r),s&&s(h.target)))}});let a=[];if(t.indexOf("[")!==-1){const f=/\[([\w-]+)/g;let p;for(;(p=f.exec(t))!==null;)a.push(p[1])}else a.push(t);i.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:a.concat(l)})},c),window[e]=n,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){n(document.body)}):n(document.body),n}const x={};function F(t,e){x[t]=e}function I(t){return x[t]||{ingress:e=>e,egress:e=>e}}const q={};function R(t,e){if(!t||typeof e!="object")return;const r=t.toLowerCase().split("-")[0];q[r]=e}function j(t){if(!t)return null;const e=t.toLowerCase().split("-")[0];return q[e]||null}R("mk",{monthsLong:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"],monthsShort:["јан","фев","мар","апр","мај","јун","јул","авг","септ","окт","ноем","дек"],daysLong:["недела","понеделник","вторник","среда","четврток","петок","сабота"],daysShort:["нед","пон","вт","ср","чет","пет","саб"]}),typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=F,window.lnCore.getDataMapper=I,window.lnCore.registerLocaleFallback=R,window.lnCore.getLocaleFallback=j,window.lnCore.fillTemplate=A,window.lnCore.fill=S,window.lnCore.lnFill=M,window.lnCore.renderList=N);function k(t){return(t||"").replace(/^#/,"")}function L(t){const e=t===void 0?location.hash:t,r={},c=k(e);if(!c)return r;const u=c.split("&");for(let l=0;l<u.length;l++){const o=u[l];if(!o)continue;const s=o.indexOf(":"),n=s>-1?o.slice(0,s):o,i=s>-1?o.slice(s+1):"";if(n)try{r[n]=decodeURIComponent(i)}catch{r[n]=i}}return r}function B(t){if(!t)return null;const e=L();return t in e?e[t]:null}function P(t,e){if(!t)return;const r=L();e==null?delete r[t]:r[t]=String(e);const u=Object.keys(r).map(function(l){const o=r[l];return o===""?l:l+":"+encodeURIComponent(o)}).join("&");k(location.hash)!==u&&(location.hash=u)}function W(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=L,window.lnCore.hashGet=B,window.lnCore.hashSet=P,window.lnCore.hashLinkClick=W),(function(){const t="data-ln-validate",e="lnValidate",r="data-ln-validate-errors",c="data-ln-validate-error",u="ln-validate-valid",l="ln-validate-invalid",o={required:"valueMissing",typeMismatch:"typeMismatch",tooShort:"tooShort",tooLong:"tooLong",patternMismatch:"patternMismatch",rangeUnderflow:"rangeUnderflow",rangeOverflow:"rangeOverflow"};if(window[e]!==void 0)return;function s(n){this.dom=n,this._touched=!1,this._customErrors=new Set;const i=this,a=n.tagName,f=n.type,p=a==="SELECT"||f==="checkbox"||f==="radio";this._onInput=function(){i._touched=!0,i.validate()},this._onChange=function(){i._touched=!0,i.validate()},this._onSetCustom=function(m){const d=m.detail&&m.detail.error;if(!d)return;i._customErrors.add(d),i._touched=!0;const v=n.closest(".form-element");if(v){const g=v.querySelector("["+c+'="'+d+'"]');g&&g.classList.remove("hidden")}n.classList.remove(u),n.classList.add(l),n.setAttribute("aria-invalid","true")},this._onClearCustom=function(m){const d=m.detail&&m.detail.error,v=n.closest(".form-element");if(d){if(i._customErrors.delete(d),v){const g=v.querySelector("["+c+'="'+d+'"]');g&&g.classList.add("hidden")}}else i._customErrors.forEach(function(g){if(v){const y=v.querySelector("["+c+'="'+g+'"]');y&&y.classList.add("hidden")}}),i._customErrors.clear();i._touched&&i.validate()},p||n.addEventListener("input",this._onInput),n.addEventListener("change",this._onChange),n.addEventListener("ln-validate:set-custom",this._onSetCustom),n.addEventListener("ln-validate:clear-custom",this._onClearCustom);const h=n.form;return h&&(h.hasAttribute("novalidate")||h.setAttribute("novalidate",""),this._onFormReset=function(){i.reset()},this._onValidateRequest=function(m){i._touched=!0,!i.validate()&&m.detail&&m.detail.invalidFields&&m.detail.invalidFields.push(i.dom)},h.addEventListener("reset",this._onFormReset),h.addEventListener("ln-validate:request-validate",this._onValidateRequest),h._lnValidateGateBound||(h._lnValidateGateBound=!0,h.addEventListener("submit",function(m){const d={invalidFields:[]};C(h,"ln-validate:request-validate",d),d.invalidFields.length>0&&(m.preventDefault(),d.invalidFields.sort((v,g)=>v.compareDocumentPosition(g)&Node.DOCUMENT_POSITION_PRECEDING?-1:1),d.invalidFields[0].focus())}))),this}s.prototype.validate=function(){const n=this.dom,i=n.validity,f=n.checkValidity()&&this._customErrors.size===0,p=n.closest(".form-element");if(p){const m=p.querySelector("["+r+"]");if(m){const d=m.querySelectorAll("["+c+"]");for(let v=0;v<d.length;v++){const g=d[v].getAttribute(c),y=o[g];y&&(i[y]?d[v].classList.remove("hidden"):d[v].classList.add("hidden"))}}}return n.classList.toggle(u,f),n.classList.toggle(l,!f),n.setAttribute("aria-invalid",f?"false":"true"),C(n,f?"ln-validate:valid":"ln-validate:invalid",{target:n,field:n.name}),f},s.prototype.reset=function(){this._touched=!1,this._customErrors.clear(),this.dom.classList.remove(u,l),this.dom.removeAttribute("aria-invalid");const n=this.dom.closest(".form-element");if(n){const i=n.querySelectorAll("["+c+"]");for(let a=0;a<i.length;a++)i[a].classList.add("hidden")}},Object.defineProperty(s.prototype,"isValid",{get:function(){return this.dom.checkValidity()&&this._customErrors.size===0}}),s.prototype.destroy=function(){if(!this.dom[e])return;this.dom.removeEventListener("input",this._onInput),this.dom.removeEventListener("change",this._onChange),this.dom.removeEventListener("ln-validate:set-custom",this._onSetCustom),this.dom.removeEventListener("ln-validate:clear-custom",this._onClearCustom);const n=this.dom.form;n&&(this._onFormReset&&n.removeEventListener("reset",this._onFormReset),this._onValidateRequest&&n.removeEventListener("ln-validate:request-validate",this._onValidateRequest)),this.dom.classList.remove(u,l),this.dom.removeAttribute("aria-invalid"),C(this.dom,"ln-validate:destroyed",{target:this.dom}),delete this.dom[e]},V(t,e,s,"ln-validate")})()})();
|
|
@@ -54,6 +54,7 @@ import { dispatch, registerComponent } from '../../ln-core';
|
|
|
54
54
|
}
|
|
55
55
|
dom.classList.remove(CSS_VALID);
|
|
56
56
|
dom.classList.add(CSS_INVALID);
|
|
57
|
+
dom.setAttribute('aria-invalid', 'true');
|
|
57
58
|
};
|
|
58
59
|
|
|
59
60
|
this._onClearCustom = function (e) {
|
|
@@ -86,6 +87,57 @@ import { dispatch, registerComponent } from '../../ln-core';
|
|
|
86
87
|
dom.addEventListener('ln-validate:set-custom', this._onSetCustom);
|
|
87
88
|
dom.addEventListener('ln-validate:clear-custom', this._onClearCustom);
|
|
88
89
|
|
|
90
|
+
const form = dom.form;
|
|
91
|
+
if (form) {
|
|
92
|
+
// One validated field is enough to own the form's gate —
|
|
93
|
+
// browser bubbles must yield to ln-validate's own error display.
|
|
94
|
+
if (!form.hasAttribute('novalidate')) {
|
|
95
|
+
form.setAttribute('novalidate', '');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this._onFormReset = function () {
|
|
99
|
+
self.reset();
|
|
100
|
+
};
|
|
101
|
+
this._onValidateRequest = function (e) {
|
|
102
|
+
self._touched = true;
|
|
103
|
+
const isValid = self.validate();
|
|
104
|
+
if (!isValid && e.detail && e.detail.invalidFields) {
|
|
105
|
+
e.detail.invalidFields.push(self.dom);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
form.addEventListener('reset', this._onFormReset);
|
|
109
|
+
form.addEventListener('ln-validate:request-validate', this._onValidateRequest);
|
|
110
|
+
|
|
111
|
+
// Submit gate — attached once per form, by whichever validated
|
|
112
|
+
// field initializes first (marker lives on the form itself,
|
|
113
|
+
// same one-shot-guard shape as ln-fill's _fillBound). Runs on
|
|
114
|
+
// every method, GET included — this is a straight replacement
|
|
115
|
+
// for the native browser validation `novalidate` just silenced
|
|
116
|
+
// above, and native validation never had a method condition
|
|
117
|
+
// either. ln-data-coordinator's own POST/PUT/PATCH method gate
|
|
118
|
+
// for the write pipeline is a separate, later decision — it
|
|
119
|
+
// reads e.defaultPrevented first, so it never claims a submit
|
|
120
|
+
// this gate already blocked.
|
|
121
|
+
if (!form._lnValidateGateBound) {
|
|
122
|
+
form._lnValidateGateBound = true;
|
|
123
|
+
form.addEventListener('submit', function (e) {
|
|
124
|
+
const validationDetail = { invalidFields: [] };
|
|
125
|
+
dispatch(form, 'ln-validate:request-validate', validationDetail);
|
|
126
|
+
|
|
127
|
+
if (validationDetail.invalidFields.length > 0) {
|
|
128
|
+
e.preventDefault();
|
|
129
|
+
validationDetail.invalidFields.sort((a, b) => {
|
|
130
|
+
return a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_PRECEDING ? -1 : 1;
|
|
131
|
+
});
|
|
132
|
+
validationDetail.invalidFields[0].focus();
|
|
133
|
+
}
|
|
134
|
+
// Valid — nothing left to do. Native submit continues;
|
|
135
|
+
// ln-data-coordinator (document, bubble phase) or plain
|
|
136
|
+
// HTML/ln-ajax decides what happens to it next.
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
89
141
|
return this;
|
|
90
142
|
}
|
|
91
143
|
|
|
@@ -118,6 +170,9 @@ import { dispatch, registerComponent } from '../../ln-core';
|
|
|
118
170
|
dom.classList.toggle(CSS_VALID, isValid);
|
|
119
171
|
dom.classList.toggle(CSS_INVALID, !isValid);
|
|
120
172
|
|
|
173
|
+
// Manage ARIA invalid attribute
|
|
174
|
+
dom.setAttribute('aria-invalid', isValid ? 'false' : 'true');
|
|
175
|
+
|
|
121
176
|
// Emit event
|
|
122
177
|
const eventName = isValid ? 'ln-validate:valid' : 'ln-validate:invalid';
|
|
123
178
|
dispatch(dom, eventName, { target: dom, field: dom.name });
|
|
@@ -129,6 +184,7 @@ import { dispatch, registerComponent } from '../../ln-core';
|
|
|
129
184
|
this._touched = false;
|
|
130
185
|
this._customErrors.clear();
|
|
131
186
|
this.dom.classList.remove(CSS_VALID, CSS_INVALID);
|
|
187
|
+
this.dom.removeAttribute('aria-invalid');
|
|
132
188
|
|
|
133
189
|
const parent = this.dom.closest('.form-element');
|
|
134
190
|
if (parent) {
|
|
@@ -149,7 +205,15 @@ import { dispatch, registerComponent } from '../../ln-core';
|
|
|
149
205
|
this.dom.removeEventListener('change', this._onChange);
|
|
150
206
|
this.dom.removeEventListener('ln-validate:set-custom', this._onSetCustom);
|
|
151
207
|
this.dom.removeEventListener('ln-validate:clear-custom', this._onClearCustom);
|
|
208
|
+
|
|
209
|
+
const form = this.dom.form;
|
|
210
|
+
if (form) {
|
|
211
|
+
if (this._onFormReset) form.removeEventListener('reset', this._onFormReset);
|
|
212
|
+
if (this._onValidateRequest) form.removeEventListener('ln-validate:request-validate', this._onValidateRequest);
|
|
213
|
+
}
|
|
214
|
+
|
|
152
215
|
this.dom.classList.remove(CSS_VALID, CSS_INVALID);
|
|
216
|
+
this.dom.removeAttribute('aria-invalid');
|
|
153
217
|
dispatch(this.dom, 'ln-validate:destroyed', { target: this.dom });
|
|
154
218
|
delete this.dom[DOM_ATTRIBUTE];
|
|
155
219
|
};
|
package/package.json
CHANGED
|
@@ -1,55 +1,61 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@livenetworks/ashlar",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "LiveNetworks unified frontend library - CSS framework and vanilla JS components",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"author": "Live Networks",
|
|
7
|
-
"type": "module",
|
|
8
|
-
"publishConfig": {
|
|
9
|
-
"access": "public"
|
|
10
|
-
},
|
|
11
|
-
"repository": {
|
|
12
|
-
"type": "git",
|
|
13
|
-
"url": "git+https://github.com/livenetworks/ln-ashlar.git"
|
|
14
|
-
},
|
|
15
|
-
"homepage": "https://github.com/livenetworks/ln-ashlar#readme",
|
|
16
|
-
"bugs": "https://github.com/livenetworks/ln-ashlar/issues",
|
|
17
|
-
"keywords": [
|
|
18
|
-
"css",
|
|
19
|
-
"scss",
|
|
20
|
-
"design-system",
|
|
21
|
-
"frontend",
|
|
22
|
-
"components",
|
|
23
|
-
"vanilla-js"
|
|
24
|
-
],
|
|
25
|
-
"files": [
|
|
26
|
-
"scss/",
|
|
27
|
-
"js/"
|
|
28
|
-
],
|
|
29
|
-
"exports": {
|
|
30
|
-
"./scss/*": "./scss/*",
|
|
31
|
-
"./js/*": "./js/*"
|
|
32
|
-
},
|
|
33
|
-
"scripts": {
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"build
|
|
38
|
-
"build:
|
|
39
|
-
"build:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@livenetworks/ashlar",
|
|
3
|
+
"version": "1.6.0",
|
|
4
|
+
"description": "LiveNetworks unified frontend library - CSS framework and vanilla JS components",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Live Networks",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/livenetworks/ln-ashlar.git"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/livenetworks/ln-ashlar#readme",
|
|
16
|
+
"bugs": "https://github.com/livenetworks/ln-ashlar/issues",
|
|
17
|
+
"keywords": [
|
|
18
|
+
"css",
|
|
19
|
+
"scss",
|
|
20
|
+
"design-system",
|
|
21
|
+
"frontend",
|
|
22
|
+
"components",
|
|
23
|
+
"vanilla-js"
|
|
24
|
+
],
|
|
25
|
+
"files": [
|
|
26
|
+
"scss/",
|
|
27
|
+
"js/"
|
|
28
|
+
],
|
|
29
|
+
"exports": {
|
|
30
|
+
"./scss/*": "./scss/*",
|
|
31
|
+
"./js/*": "./js/*"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test": "node --test \"tests/*.test.js\"",
|
|
35
|
+
"test:p0": "node --test tests/p0-data-integrity.test.js",
|
|
36
|
+
"dev": "node scripts/build.mjs --watch",
|
|
37
|
+
"build": "node scripts/build.mjs && npm run build:dev-css && npm run build:demos",
|
|
38
|
+
"build:demos": "npm run build:demo-index && npm run build:demo-admin && npm run build:demo-docuflow && npm run build:demo-spa",
|
|
39
|
+
"build:dev-css": "sass scss/ln-ashlar-dev.scss demo/dist/ln-ashlar-dev.css --no-source-map --style=compressed",
|
|
40
|
+
"build:demo-index": "sass demo/index.scss demo/index.css --no-source-map --style=compressed",
|
|
41
|
+
"build:demo-admin": "sass demo/admin/src/admin.scss demo/admin/dist/admin.css --no-source-map --style=compressed && cp demo/admin/src/demo.js demo/admin/dist/demo.js && node demo/admin/src/build-pages.mjs",
|
|
42
|
+
"build:demo-docuflow": "sass demo/docuflow/docuflow.scss demo/docuflow/dist/docuflow.css --no-source-map --style=compressed",
|
|
43
|
+
"build:demo-spa": "node demo/spa/build.mjs"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@tabler/icons": "^3.41.1",
|
|
47
|
+
"fake-indexeddb": "^6.2.4",
|
|
48
|
+
"flag-icons": "^7.5.0",
|
|
49
|
+
"puppeteer-core": "^25.1.0",
|
|
50
|
+
"sass-embedded": "^1.83.0",
|
|
51
|
+
"vite": "^6.0.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"tom-select": "^2.0.0"
|
|
55
|
+
},
|
|
56
|
+
"peerDependenciesMeta": {
|
|
57
|
+
"tom-select": {
|
|
58
|
+
"optional": true
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
package/scss/base/_global.scss
CHANGED
|
@@ -14,8 +14,8 @@ body {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
body {
|
|
17
|
-
font-size: var(--
|
|
18
|
-
line-height: var(--
|
|
17
|
+
font-size: var(--font-size);
|
|
18
|
+
line-height: var(--line-height);
|
|
19
19
|
color: var(--color-fg);
|
|
20
20
|
-webkit-font-smoothing: antialiased;
|
|
21
21
|
-moz-osx-font-smoothing: grayscale;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
@use '../config/mixins' as *;
|
|
2
|
+
|
|
3
|
+
// Filter dropdown content chrome (search input + options list; NO clear button —
|
|
4
|
+
// the data-ln-filter-reset "All" sentinel is the reset affordance).
|
|
5
|
+
// Floating shell is supplied separately:
|
|
6
|
+
// • popover-driven (ln-filter static): @mixin popover on the [data-ln-popover] wrapper
|
|
7
|
+
// ln-table dynamic clone: floating-panel + box below
|
|
8
|
+
.column-filter-dropdown {
|
|
9
|
+
@include filter-dropdown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// ln-table appends its cloned dropdown directly into the <th> and
|
|
13
|
+
// manages its own open/close — it does NOT route through ln-popover, so it
|
|
14
|
+
// needs the floating shell applied here (the popover path gets it from
|
|
15
|
+
// @mixin popover instead).
|
|
16
|
+
[data-ln-table] .column-filter-dropdown {
|
|
17
|
+
@include floating-panel;
|
|
18
|
+
position: absolute;
|
|
19
|
+
z-index: var(--z-dropdown);
|
|
20
|
+
top: 100%;
|
|
21
|
+
right: 0;
|
|
22
|
+
left: auto;
|
|
23
|
+
margin-top: var(--size-xs);
|
|
24
|
+
min-width: 14rem;
|
|
25
|
+
padding: var(--size-sm);
|
|
26
|
+
}
|
|
@@ -68,6 +68,10 @@ label:not(:has(> input)) {
|
|
|
68
68
|
@include pills-switch;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
.pills-segmented {
|
|
72
|
+
@include pills-segmented;
|
|
73
|
+
}
|
|
74
|
+
|
|
71
75
|
.pill {
|
|
72
76
|
@include pill;
|
|
73
77
|
}
|
|
@@ -80,6 +84,9 @@ label:not(:has(> input)) {
|
|
|
80
84
|
@include pill-switch;
|
|
81
85
|
}
|
|
82
86
|
|
|
87
|
+
.search {
|
|
88
|
+
@include search;
|
|
89
|
+
}
|
|
83
90
|
|
|
84
91
|
.form-actions {
|
|
85
92
|
@include form-actions;
|
|
@@ -2,20 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
[data-ln-table] {
|
|
4
4
|
@include ln-table;
|
|
5
|
-
@include ln-table-column-filter;
|
|
6
|
-
@include ln-table-sort;
|
|
7
5
|
}
|
|
8
6
|
|
|
9
7
|
// In-thead toolbar — first thead row's colspan th.
|
|
10
8
|
[data-ln-table] thead > tr:first-child:has(> th[colspan]) > th {
|
|
11
9
|
@include ln-table-thead-toolbar;
|
|
12
10
|
}
|
|
13
|
-
|
|
11
|
+
[data-ln-table] > header,
|
|
14
12
|
.ln-table__toolbar {
|
|
15
13
|
@include ln-table-toolbar;
|
|
16
14
|
}
|
|
17
15
|
|
|
18
|
-
.ln-table__toolbar label:has(> input[type="search"])
|
|
16
|
+
.ln-table__toolbar label:has(> input[type="search"]) {
|
|
19
17
|
@include ln-table-toolbar-search;
|
|
20
18
|
}
|
|
21
19
|
|
|
@@ -26,7 +24,7 @@
|
|
|
26
24
|
.table-container {
|
|
27
25
|
@include ln-table-card-mode;
|
|
28
26
|
}
|
|
29
|
-
|
|
27
|
+
[data-ln-table] > footer,
|
|
30
28
|
.ln-table__footer {
|
|
31
29
|
@include ln-table-footer;
|
|
32
30
|
}
|
|
@@ -58,3 +56,14 @@
|
|
|
58
56
|
.ln-table__spacer {
|
|
59
57
|
@include ln-table-spacer-row;
|
|
60
58
|
}
|
|
59
|
+
|
|
60
|
+
.ln-table__placeholder {
|
|
61
|
+
@include ln-table-placeholder-row;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.ln-table--loading tbody {
|
|
65
|
+
opacity: 0.5;
|
|
66
|
+
pointer-events: none;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|