@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
|
@@ -1,96 +1,118 @@
|
|
|
1
|
-
# ln-translations
|
|
2
|
-
|
|
3
|
-
A zero-dependency, event-driven **Form Translation Coordinator** that manages inline multi-lingual inputs by dynamically cloning translatable fields on-demand.
|
|
4
|
-
|
|
5
|
-
Instead of writing custom layout handlers to manage multi-language fields, this component intercepts locale additions and removals, duplicates translatable fields with appropriate naming conventions, and handles pre-rendered server payloads automatically.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## 🧭 Philosophy & Architecture
|
|
10
|
-
|
|
11
|
-
1. **Inline Field Cloning:** Translatable containers carrying `data-ln-translatable="field"` are treated as templates. When a language is activated, the coordinator clones the inner input/textarea, binds the target language value, and appends it beneath the original.
|
|
12
|
-
2. **Deterministic Name Generation:** To support clean form submission, cloned inputs automatically update their name attribute following standard nested arrays:
|
|
13
|
-
- **Default Name**: `scope` becomes `trans[en][scope]` for English.
|
|
14
|
-
- **Nested Name**: `items[5][title]` with prefix `items[5]` becomes `items[5][trans][en][title]`.
|
|
15
|
-
3. **Menu & Badge Coordination:** It coordinates locale-selector dropdowns and active language indicator badges using native HTML `<template>` nodes, keeping visual UI elements synchronized in real-time.
|
|
16
|
-
4. **Server-Rendered Auto-Detection:** If the server pre-renders localized fields with `data-ln-translatable-lang="{lang}"` on page load, the coordinator auto-detects and integrates them instantly.
|
|
17
|
-
|
|
18
|
-
---
|
|
19
|
-
|
|
20
|
-
## 📦 Minimal Blueprint
|
|
21
|
-
|
|
22
|
-
### Translatable Form Structure
|
|
23
|
-
```html
|
|
24
|
-
<form data-ln-translations data-ln-translations-default="en">
|
|
25
|
-
<header class="ln-translations__header">
|
|
26
|
-
<h3>Form Content</h3>
|
|
27
|
-
<!-- Active Language Badges -->
|
|
28
|
-
<ul data-ln-translations-active></ul>
|
|
29
|
-
<!-- Locale Add Menu (Dropdown coordinated) -->
|
|
30
|
-
<div data-ln-dropdown>
|
|
31
|
-
<button type="button" data-ln-translations-add data-ln-toggle-for="trans-menu">
|
|
32
|
-
<svg class="ln-icon"><use href="#ln-world"></use></svg>
|
|
33
|
-
</button>
|
|
34
|
-
<ul id="trans-menu" data-ln-toggle data-ln-dropdown-menu></ul>
|
|
35
|
-
</div>
|
|
36
|
-
</header>
|
|
37
|
-
|
|
38
|
-
<main>
|
|
39
|
-
<div data-ln-translatable="description">
|
|
40
|
-
<label>Description <textarea name="description">Acme scope...</textarea></label>
|
|
41
|
-
</div>
|
|
42
|
-
</main>
|
|
43
|
-
</form>
|
|
44
|
-
|
|
45
|
-
<!-- REQUIRED GLOBAL TEMPLATES (Declared once before body closing) -->
|
|
46
|
-
<template data-ln-template="ln-translations-badge">
|
|
47
|
-
<li>
|
|
48
|
-
<p data-ln-translations-lang>
|
|
49
|
-
<span></span>
|
|
50
|
-
<button type="button" aria-label="Remove"><svg class="ln-icon ln-icon--sm" aria-hidden="true"><use href="#ln-x"></use></svg></button>
|
|
51
|
-
</p>
|
|
52
|
-
</li>
|
|
53
|
-
</template>
|
|
54
|
-
<template data-ln-template="ln-translations-menu-item">
|
|
55
|
-
<li><button type="button" data-ln-translations-lang></button></li>
|
|
56
|
-
</template>
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
---
|
|
60
|
-
|
|
61
|
-
## 🛠️ Declarative API Contract
|
|
62
|
-
|
|
63
|
-
### HTML Attributes
|
|
64
|
-
|
|
65
|
-
| Attribute | Elements | Description |
|
|
66
|
-
| :--- | :--- | :--- |
|
|
67
|
-
| `data-ln-translations` | `<form>` | Component root. Initializes the translations coordinator. |
|
|
68
|
-
| `data-ln-translations-default` | `<form>` | Default language code (e.g. `"en"`). Sets a flag on the original inputs. |
|
|
69
|
-
| `data-ln-translations-locales` | `<form>` | Opt-in. Custom locales JSON list (e.g. `'{"en":"English", "de":"German"}'`). |
|
|
70
|
-
| `data-ln-translations-add` | `<button>` | Trigger button inside `data-ln-dropdown` to open language menu. |
|
|
71
|
-
| `data-ln-translations-active` | `<ul>` | Mount container where active language badges will be rendered. |
|
|
72
|
-
| `data-ln-
|
|
73
|
-
| `data-ln-translations-
|
|
74
|
-
| `data-ln-translatable
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
- **`ln-translations:
|
|
84
|
-
- Detail: `{ target, lang }`
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
- **
|
|
1
|
+
# ln-translations
|
|
2
|
+
|
|
3
|
+
A zero-dependency, event-driven **Form Translation Coordinator** that manages inline multi-lingual inputs by dynamically cloning translatable fields on-demand.
|
|
4
|
+
|
|
5
|
+
Instead of writing custom layout handlers to manage multi-language fields, this component intercepts locale additions and removals, duplicates translatable fields with appropriate naming conventions, and handles pre-rendered server payloads automatically.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 🧭 Philosophy & Architecture
|
|
10
|
+
|
|
11
|
+
1. **Inline Field Cloning:** Translatable containers carrying `data-ln-translatable="field"` are treated as templates. When a language is activated, the coordinator clones the inner input/textarea/select, binds the target language value, and appends it beneath the original.
|
|
12
|
+
2. **Deterministic Name Generation:** To support clean form submission, cloned inputs automatically update their name attribute following standard nested arrays:
|
|
13
|
+
- **Default Name**: `scope` becomes `trans[en][scope]` for English.
|
|
14
|
+
- **Nested Name**: `items[5][title]` with prefix `items[5]` becomes `items[5][trans][en][title]`.
|
|
15
|
+
3. **Menu & Badge Coordination:** It coordinates locale-selector dropdowns and active language indicator badges using native HTML `<template>` nodes, keeping visual UI elements synchronized in real-time.
|
|
16
|
+
4. **Server-Rendered Auto-Detection:** If the server pre-renders localized fields with `data-ln-translatable-lang="{lang}"` on page load, the coordinator auto-detects and integrates them instantly.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 📦 Minimal Blueprint
|
|
21
|
+
|
|
22
|
+
### Translatable Form Structure
|
|
23
|
+
```html
|
|
24
|
+
<form data-ln-translations data-ln-translations-default="en">
|
|
25
|
+
<header class="ln-translations__header">
|
|
26
|
+
<h3>Form Content</h3>
|
|
27
|
+
<!-- Active Language Badges -->
|
|
28
|
+
<ul data-ln-translations-active></ul>
|
|
29
|
+
<!-- Locale Add Menu (Dropdown coordinated) -->
|
|
30
|
+
<div data-ln-dropdown>
|
|
31
|
+
<button type="button" data-ln-translations-add data-ln-toggle-for="trans-menu">
|
|
32
|
+
<svg class="ln-icon"><use href="#ln-icon-world"></use></svg>
|
|
33
|
+
</button>
|
|
34
|
+
<ul id="trans-menu" data-ln-toggle data-ln-dropdown-menu></ul>
|
|
35
|
+
</div>
|
|
36
|
+
</header>
|
|
37
|
+
|
|
38
|
+
<main>
|
|
39
|
+
<div data-ln-translatable="description">
|
|
40
|
+
<label>Description <textarea name="description">Acme scope...</textarea></label>
|
|
41
|
+
</div>
|
|
42
|
+
</main>
|
|
43
|
+
</form>
|
|
44
|
+
|
|
45
|
+
<!-- REQUIRED GLOBAL TEMPLATES (Declared once before body closing) -->
|
|
46
|
+
<template data-ln-template="ln-translations-badge">
|
|
47
|
+
<li>
|
|
48
|
+
<p data-ln-translations-lang>
|
|
49
|
+
<span></span>
|
|
50
|
+
<button type="button" aria-label="Remove"><svg class="ln-icon ln-icon--sm" aria-hidden="true"><use href="#ln-icon-x"></use></svg></button>
|
|
51
|
+
</p>
|
|
52
|
+
</li>
|
|
53
|
+
</template>
|
|
54
|
+
<template data-ln-template="ln-translations-menu-item">
|
|
55
|
+
<li><button type="button" data-ln-translations-lang></button></li>
|
|
56
|
+
</template>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 🛠️ Declarative API Contract
|
|
62
|
+
|
|
63
|
+
### HTML Attributes
|
|
64
|
+
|
|
65
|
+
| Attribute | Elements | Description |
|
|
66
|
+
| :--- | :--- | :--- |
|
|
67
|
+
| `data-ln-translations` | `<form>` | Component root. Initializes the translations coordinator. |
|
|
68
|
+
| `data-ln-translations-default` | `<form>` | Default language code (e.g. `"en"`). Sets a flag on the original inputs. |
|
|
69
|
+
| `data-ln-translations-locales` | `<form>` | Opt-in. Custom locales JSON list (e.g. `'{"en":"English", "de":"German"}'`). |
|
|
70
|
+
| `data-ln-translations-add` | `<button>` | Trigger button inside `data-ln-dropdown` to open language menu. |
|
|
71
|
+
| `data-ln-translations-active` | `<ul>` | Mount container where active language badges will be rendered. |
|
|
72
|
+
| `data-ln-translations-placeholder` | `<form>` | Opt-in. Placeholder template for cloned inputs, e.g. `"{lang} translation"`. `{lang}` is replaced with the language display name. Default: `"{lang} translation"`. |
|
|
73
|
+
| `data-ln-translations-remove-label` | `<form>` | Opt-in. `aria-label` template for the badge remove button, e.g. `"Remove {lang}"`. `{lang}` is replaced with the language display name. Default: `"Remove {lang}"`. |
|
|
74
|
+
| `data-ln-translatable="field"` | Form field wrapper | Marks a translatable group. Value is the entity's field name. |
|
|
75
|
+
| `data-ln-translations-prefix` | Form field wrapper | Opt-in. Naming prefix (e.g. `"items[1]"`) for nested form layouts. |
|
|
76
|
+
| `data-ln-translatable-lang` | `<input>`, `<textarea>`, `<select>` | Language code identifying a cloned or pre-rendered translation input. |
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## ⚡ DOM Events
|
|
81
|
+
|
|
82
|
+
### Telemetry (Dispatched by component)
|
|
83
|
+
- **`ln-translations:before-add`** / **`ln-translations:before-remove`** (Cancelable)
|
|
84
|
+
- Detail: `{ target, lang, langName }`
|
|
85
|
+
- **`ln-translations:added`** / **`ln-translations:removed`**
|
|
86
|
+
- Detail: `{ target, lang }`
|
|
87
|
+
|
|
88
|
+
### Commands (Dispatched to component)
|
|
89
|
+
- **`ln-translations:request-add`** / **`ln-translations:request-remove`**
|
|
90
|
+
- Detail: `{ lang: string }` (dispatched on the `<form>` to programmatically toggle languages).
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## ⚠️ Common Pitfalls
|
|
95
|
+
|
|
96
|
+
- **Forgetting Global Templates:** The coordinator will fail to render badges or locale dropdowns if `ln-translations-badge` and `ln-translations-menu-item` templates are missing from the page.
|
|
97
|
+
- **Incorrect Translatable Wrappers:** `data-ln-translatable` must sit on the parent container (e.g. `<div data-ln-translatable="title">`) wrapping the default `<input>`/`<label>`, not on the input itself.
|
|
98
|
+
- **Mismatched Prefixes:** When nesting entities (e.g., repeating list items), ensure the translatable wrapper declares the correct scope prefix: `data-ln-translations-prefix="items[index]"` to generate valid submission structures.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 🔧 Internals
|
|
103
|
+
|
|
104
|
+
Source: `js/ln-translations/ln-translations.js`. Instance state per `[data-ln-translations]` form (`form.lnTranslations`): `activeLanguages` (Set), `defaultLang`, `locales` map, `badgesEl`/`menuEl` refs, and the `placeholderLabel`/`removeLabel` templates (`{lang}` token).
|
|
105
|
+
|
|
106
|
+
### Add-language flow
|
|
107
|
+
|
|
108
|
+
`addLanguage(lang, values)`: cancelable `:before-add` → add to `activeLanguages` → for every `[data-ln-translatable]` wrapper, clone the original input (deep clone for `<select>` to preserve its `<option>`s, shallow otherwise), rename to `prefix[trans][lang][field]` or `trans[lang][field]`, strip `id`, apply the placeholder template, tag `data-ln-translatable-lang`, insert after the last existing translation input for that field → rebuild the dropdown (only unactivated languages) and badges → `:added`.
|
|
109
|
+
|
|
110
|
+
Original (default-language) inputs are tagged `data-ln-translatable-lang="{defaultLang}"` at init specifically so `removeLanguage` never matches and removes them.
|
|
111
|
+
|
|
112
|
+
### Remove-language flow
|
|
113
|
+
|
|
114
|
+
`removeLanguage(lang)`: cancelable `:before-remove` → `querySelectorAll('[data-ln-translatable-lang="{lang}"]')` removed from DOM → delete from `activeLanguages` → rebuild dropdown/badges → `:removed`.
|
|
115
|
+
|
|
116
|
+
### MutationObserver
|
|
117
|
+
|
|
118
|
+
A single shared observer (via `registerComponent`) watches `childList` for new `[data-ln-translations]` forms and `attributes` for the attribute landing on an existing element — no per-instance observer.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(){"use strict";const b={};function A(e,a){b[e]||(b[e]=document.querySelector('[data-ln-template="'+e+'"]'));const d=b[e];return d?d.content.cloneNode(!0):(console.warn("["+a+'] Template "'+e+'" not found'),null)}function w(e,a,d){e.dispatchEvent(new CustomEvent(a,{bubbles:!0,detail:d||{}}))}function q(e,a,d){const r=new CustomEvent(a,{bubbles:!0,cancelable:!0,detail:d||{}});return e.dispatchEvent(r),r}function E(e,a){if(!document.body){document.addEventListener("DOMContentLoaded",function(){E(e,a)}),console.warn("["+a+'] Script loaded before <body> — add "defer" to your <script> tag');return}e()}function m(e,a,d,r){if(e.nodeType!==1)return;const s=a.indexOf("[")!==-1||a.indexOf(".")!==-1||a.indexOf("#")!==-1?a:"["+a+"]",o=Array.from(e.querySelectorAll(s));e.matches&&e.matches(s)&&o.push(e);for(const n of o)n[d]||(n[d]=new r(n))}function _(e,a,d,r,t={}){const s=t.extraAttributes||[],o=t.onAttributeChange||null,n=t.onInit||null;function c(u){const l=u||document.body;m(l,e,a,d),n&&n(l)}return E(function(){const u=new MutationObserver(function(p){for(let h=0;h<p.length;h++){const i=p[h];if(i.type==="childList"){for(let g=0;g<i.addedNodes.length;g++){const f=i.addedNodes[g];f.nodeType===1&&(m(f,e,a,d),n&&n(f))}for(let g=0;g<i.removedNodes.length;g++){const f=i.removedNodes[g];if(f.nodeType===1){const C=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]",y=Array.from(f.querySelectorAll(C));f.matches&&f.matches(C)&&y.push(f);for(let v=0;v<y.length;v++){const x=y[v];if(!document.contains(x)){const L=x[a];L&&typeof L.destroy=="function"&&L.destroy()}}}}}else i.type==="attributes"&&(o&&i.target[a]?o(i.target,i.attributeName):(m(i.target,e,a,d),n&&n(i.target)))}});let l=[];if(e.indexOf("[")!==-1){const p=/\[([\w-]+)/g;let h;for(;(h=p.exec(e))!==null;)l.push(h[1])}else l.push(e);u.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:l.concat(s)})},r),window[a]=c,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){c(document.body)}):c(document.body),c}const S={};function D(e,a){S[e]=a}function O(e){return S[e]||{ingress:a=>a,egress:a=>a}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=D,window.lnCore.getDataMapper=O),(function(){const e="data-ln-translations",a="lnTranslations";if(window[a]!==void 0)return;const d={en:"English",sq:"Shqip",sr:"Srpski"};function r(t){this.dom=t,this.activeLanguages=new Set,this.defaultLang=t.getAttribute(e+"-default")||"",this.badgesEl=t.querySelector("["+e+"-active]"),this.menuEl=t.querySelector("[data-ln-dropdown] > [data-ln-toggle]");const s=t.getAttribute(e+"-locales");if(this.locales=d,s)try{this.locales=JSON.parse(s)}catch{console.warn("[ln-translations] Invalid JSON in data-ln-translations-locales")}this._applyDefaultLang(),this._updateDropdown();const o=this;return this._onRequestAdd=function(n){n.detail&&n.detail.lang&&o.addLanguage(n.detail.lang)},this._onRequestRemove=function(n){n.detail&&n.detail.lang&&o.removeLanguage(n.detail.lang)},t.addEventListener("ln-translations:request-add",this._onRequestAdd),t.addEventListener("ln-translations:request-remove",this._onRequestRemove),this._detectExisting(),this}r.prototype._applyDefaultLang=function(){if(!this.defaultLang)return;const t=this.dom.querySelectorAll("[data-ln-translatable]");for(const s of t){const o=s.querySelectorAll("input:not([data-ln-translatable-lang]), textarea:not([data-ln-translatable-lang]), select:not([data-ln-translatable-lang])");for(const n of o)n.setAttribute("data-ln-translatable-lang",this.defaultLang)}},r.prototype._detectExisting=function(){const t=this.dom.querySelectorAll("[data-ln-translatable-lang]");for(const s of t){const o=s.getAttribute("data-ln-translatable-lang");o&&o!==this.defaultLang&&this.activeLanguages.add(o)}this.activeLanguages.size>0&&(this._updateBadges(),this._updateDropdown())},r.prototype._updateDropdown=function(){if(!this.menuEl)return;this.menuEl.textContent="";const t=this;let s=0;for(const n in this.locales){if(!this.locales.hasOwnProperty(n)||this.activeLanguages.has(n))continue;s++;const c=A("ln-translations-menu-item","ln-translations");if(!c)return;const u=c.querySelector("[data-ln-translations-lang]");u.setAttribute("data-ln-translations-lang",n),u.textContent=this.locales[n],u.addEventListener("click",function(l){l.ctrlKey||l.metaKey||l.button===1||(l.preventDefault(),l.stopPropagation(),t.menuEl.getAttribute("data-ln-toggle")==="open"&&t.menuEl.setAttribute("data-ln-toggle","close"),t.addLanguage(n))}),this.menuEl.appendChild(c)}const o=this.dom.querySelector("["+e+"-add]");o&&(o.style.display=s===0?"none":"")},r.prototype._updateBadges=function(){if(!this.badgesEl)return;this.badgesEl.textContent="";const t=this;this.activeLanguages.forEach(function(s){const o=A("ln-translations-badge","ln-translations");if(!o)return;const n=o.querySelector("[data-ln-translations-lang]");n.setAttribute("data-ln-translations-lang",s);const c=n.querySelector("span");c.textContent=t.locales[s]||s.toUpperCase();const u=n.querySelector("button");u.setAttribute("aria-label","Remove "+(t.locales[s]||s.toUpperCase())),u.addEventListener("click",function(l){l.ctrlKey||l.metaKey||l.button===1||(l.preventDefault(),l.stopPropagation(),t.removeLanguage(s))}),t.badgesEl.appendChild(o)})},r.prototype.addLanguage=function(t,s){if(this.activeLanguages.has(t))return;const o=this.locales[t]||t;if(q(this.dom,"ln-translations:before-add",{target:this.dom,lang:t,langName:o}).defaultPrevented)return;this.activeLanguages.add(t),s=s||{};const c=this.dom.querySelectorAll("[data-ln-translatable]");for(const u of c){const l=u.getAttribute("data-ln-translatable"),p=u.getAttribute("data-ln-translations-prefix")||"",h=u.querySelector(this.defaultLang?'[data-ln-translatable-lang="'+this.defaultLang+'"]':"input:not([data-ln-translatable-lang]), textarea:not([data-ln-translatable-lang]), select:not([data-ln-translatable-lang])");if(!h)continue;const i=h.cloneNode(!1);p?i.name=p+"[trans]["+t+"]["+l+"]":i.name="trans["+t+"]["+l+"]",i.value=s[l]!==void 0?s[l]:"",i.removeAttribute("id"),i.placeholder=o+" translation",i.setAttribute("data-ln-translatable-lang",t);const g=u.querySelectorAll('[data-ln-translatable-lang]:not([data-ln-translatable-lang="'+this.defaultLang+'"])'),f=g.length>0?g[g.length-1]:h;f.parentNode.insertBefore(i,f.nextSibling)}this._updateDropdown(),this._updateBadges(),w(this.dom,"ln-translations:added",{target:this.dom,lang:t,langName:o})},r.prototype.removeLanguage=function(t){if(!this.activeLanguages.has(t)||q(this.dom,"ln-translations:before-remove",{target:this.dom,lang:t}).defaultPrevented)return;const o=this.dom.querySelectorAll('[data-ln-translatable-lang="'+t+'"]');for(const n of o)n.parentNode.removeChild(n);this.activeLanguages.delete(t),this._updateDropdown(),this._updateBadges(),w(this.dom,"ln-translations:removed",{target:this.dom,lang:t})},r.prototype.getActiveLanguages=function(){return new Set(this.activeLanguages)},r.prototype.hasLanguage=function(t){return this.activeLanguages.has(t)},r.prototype.destroy=function(){if(!this.dom[a])return;const t=this.defaultLang,s=this.dom.querySelectorAll("[data-ln-translatable-lang]");for(const o of s)o.getAttribute("data-ln-translatable-lang")!==t&&o.parentNode.removeChild(o);this.dom.removeEventListener("ln-translations:request-add",this._onRequestAdd),this.dom.removeEventListener("ln-translations:request-remove",this._onRequestRemove),delete this.dom[a]},_(e,a,r,"ln-translations")})()})();
|
|
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 m(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 A(t,e,i){t.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:i||{}}))}function S(t,e,i){const c=new CustomEvent(e,{bubbles:!0,cancelable:!0,detail:i||{}});return t.dispatchEvent(c),c}function E(t,e){if(!t||!e)return t;const i=t.querySelectorAll("[data-ln-field]");for(let n=0;n<i.length;n++){const l=i[n],s=l.getAttribute("data-ln-field");e[s]!=null&&(l.textContent=e[s])}const c=t.querySelectorAll("[data-ln-attr]");for(let n=0;n<c.length;n++){const l=c[n],s=l.getAttribute("data-ln-attr").split(",");for(let r=0;r<s.length;r++){const d=s[r].trim().split(":");if(d.length!==2)continue;const u=d[0].trim(),f=d[1].trim();e[f]!=null&&l.setAttribute(u,e[f])}}const a=t.querySelectorAll("[data-ln-show]");for(let n=0;n<a.length;n++){const l=a[n],s=l.getAttribute("data-ln-show");s in e&&l.classList.toggle("hidden",!e[s])}const o=t.querySelectorAll("[data-ln-class]");for(let n=0;n<o.length;n++){const l=o[n],s=l.getAttribute("data-ln-class").split(",");for(let r=0;r<s.length;r++){const d=s[r].trim().split(":");if(d.length!==2)continue;const u=d[0].trim(),f=d[1].trim();f in e&&l.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)E(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 x(t,e){if(!t||!e)return t;const i=document.createTreeWalker(t,NodeFilter.SHOW_TEXT);for(;i.nextNode();){const o=i.currentNode;o.textContent.indexOf("{{")!==-1&&(o.textContent=o.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(n,l){return e[l]!==void 0?e[l]:""}))}const c=function(o,n){return e[n]!==void 0?e[n]:""},a=Array.from(t.querySelectorAll("*"));t.nodeType===1&&a.push(t);for(let o=0;o<a.length;o++){const n=a[o],l=n.attributes;for(let s=0;s<l.length;s++){const r=l[s];r.value.indexOf("{{")!==-1&&n.setAttribute(r.name,r.value.replace(/\{\{\s*(\w+)\s*\}\}/g,c))}}return t}function B(t,e,i,c,a,o){const n={};for(let s=0;s<t.children.length;s++){const r=t.children[s],d=r.getAttribute("data-ln-key");d&&(n[d]=r)}const l=document.createDocumentFragment();for(let s=0;s<e.length;s++){const r=e[s],d=String(c(r));let u=n[d];if(u)a(u,r,s);else{const f=m(i,o);if(!f||(x(f,r),u=f.firstElementChild,!u))continue;u.setAttribute("data-ln-key",d),a(u,r,s)}l.appendChild(u)}t.textContent="",t.appendChild(l)}function q(t,e){if(!document.body){document.addEventListener("DOMContentLoaded",function(){q(t,e)}),console.warn("["+e+'] Script loaded before <body> — add "defer" to your <script> tag');return}t()}function y(t,e,i,c){if(t.nodeType!==1)return;const o=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]",n=Array.from(t.querySelectorAll(o));t.matches&&t.matches(o)&&n.push(t);for(const l of n)l[i]||(l[i]=new c(l))}function M(t,e,i,c,a={}){const o=a.extraAttributes||[],n=a.onAttributeChange||null,l=a.onInit||null;function s(r){const d=r||document.body;y(d,t,e,i),l&&l(d)}return q(function(){const r=new MutationObserver(function(u){for(let f=0;f<u.length;f++){const h=u[f];if(h.type==="childList"){for(let p=0;p<h.addedNodes.length;p++){const g=h.addedNodes[p];g.nodeType===1&&(y(g,t,e,i),l&&l(g))}for(let p=0;p<h.removedNodes.length;p++){const g=h.removedNodes[p];if(g.nodeType===1){const N=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",L=Array.from(g.querySelectorAll(N));g.matches&&g.matches(N)&&L.push(g);for(let v=0;v<L.length;v++){const T=L[v];if(!document.contains(T)){const C=T[e];C&&typeof C.destroy=="function"&&C.destroy()}}}}}else h.type==="attributes"&&(n&&h.target[e]?n(h.target,h.attributeName):(y(h.target,t,e,i),l&&l(h.target)))}});let d=[];if(t.indexOf("[")!==-1){const u=/\[([\w-]+)/g;let f;for(;(f=u.exec(t))!==null;)d.push(f[1])}else d.push(t);r.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:d.concat(o)})},c),window[e]=s,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){s(document.body)}):s(document.body),s}const _={};function j(t,e){_[t]=e}function K(t){return _[t]||{ingress:e=>e,egress:e=>e}}const D={};function O(t,e){if(!t||typeof e!="object")return;const i=t.toLowerCase().split("-")[0];D[i]=e}function F(t){if(!t)return null;const e=t.toLowerCase().split("-")[0];return D[e]||null}O("mk",{monthsLong:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"],monthsShort:["јан","фев","мар","апр","мај","јун","јул","авг","септ","окт","ноем","дек"],daysLong:["недела","понеделник","вторник","среда","четврток","петок","сабота"],daysShort:["нед","пон","вт","ср","чет","пет","саб"]}),typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=j,window.lnCore.getDataMapper=K,window.lnCore.registerLocaleFallback=O,window.lnCore.getLocaleFallback=F,window.lnCore.fillTemplate=x,window.lnCore.fill=E,window.lnCore.lnFill=R,window.lnCore.renderList=B);function k(t){return(t||"").replace(/^#/,"")}function w(t){const e=t===void 0?location.hash:t,i={},c=k(e);if(!c)return i;const a=c.split("&");for(let o=0;o<a.length;o++){const n=a[o];if(!n)continue;const l=n.indexOf(":"),s=l>-1?n.slice(0,l):n,r=l>-1?n.slice(l+1):"";if(s)try{i[s]=decodeURIComponent(r)}catch{i[s]=r}}return i}function W(t){if(!t)return null;const e=w();return t in e?e[t]:null}function I(t,e){if(!t)return;const i=w();e==null?delete i[t]:i[t]=String(e);const a=Object.keys(i).map(function(o){const n=i[o];return n===""?o:o+":"+encodeURIComponent(n)}).join("&");k(location.hash)!==a&&(location.hash=a)}function P(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=w,window.lnCore.hashGet=W,window.lnCore.hashSet=I,window.lnCore.hashLinkClick=P),(function(){const t="data-ln-translations",e="lnTranslations";if(window[e]!==void 0)return;const i={en:"English",sq:"Shqip",sr:"Srpski"};function c(a){this.dom=a,this.activeLanguages=new Set,this.defaultLang=a.getAttribute(t+"-default")||"",this.placeholderLabel=a.getAttribute(t+"-placeholder")||"{lang} translation",this.removeLabel=a.getAttribute(t+"-remove-label")||"Remove {lang}",this.badgesEl=a.querySelector("["+t+"-active]"),this.menuEl=a.querySelector("[data-ln-dropdown] > [data-ln-toggle]");const o=a.getAttribute(t+"-locales");if(this.locales=i,o)try{this.locales=JSON.parse(o)}catch{console.warn("[ln-translations] Invalid JSON in data-ln-translations-locales")}this._applyDefaultLang(),this._updateDropdown();const n=this;return this._onRequestAdd=function(l){l.detail&&l.detail.lang&&n.addLanguage(l.detail.lang)},this._onRequestRemove=function(l){l.detail&&l.detail.lang&&n.removeLanguage(l.detail.lang)},a.addEventListener("ln-translations:request-add",this._onRequestAdd),a.addEventListener("ln-translations:request-remove",this._onRequestRemove),this._detectExisting(),this}c.prototype._applyDefaultLang=function(){if(!this.defaultLang)return;const a=this.dom.querySelectorAll("[data-ln-translatable]");for(const o of a){const n=o.querySelectorAll("input:not([data-ln-translatable-lang]), textarea:not([data-ln-translatable-lang]), select:not([data-ln-translatable-lang])");for(const l of n)l.setAttribute("data-ln-translatable-lang",this.defaultLang)}},c.prototype._detectExisting=function(){const a=this.dom.querySelectorAll("[data-ln-translatable-lang]");for(const o of a){const n=o.getAttribute("data-ln-translatable-lang");n&&n!==this.defaultLang&&this.activeLanguages.add(n)}this.activeLanguages.size>0&&(this._updateBadges(),this._updateDropdown())},c.prototype._updateDropdown=function(){if(!this.menuEl)return;this.menuEl.textContent="";const a=this;let o=0;for(const l in this.locales){if(!this.locales.hasOwnProperty(l)||this.activeLanguages.has(l))continue;o++;const s=m("ln-translations-menu-item","ln-translations");if(!s)return;const r=s.querySelector("[data-ln-translations-lang]");r.setAttribute("data-ln-translations-lang",l),r.textContent=this.locales[l],r.addEventListener("click",function(d){d.ctrlKey||d.metaKey||d.button===1||(d.preventDefault(),d.stopPropagation(),a.menuEl.getAttribute("data-ln-toggle")==="open"&&a.menuEl.setAttribute("data-ln-toggle","close"),a.addLanguage(l))}),this.menuEl.appendChild(s)}const n=this.dom.querySelector("["+t+"-add]");n&&(n.hidden=o===0)},c.prototype._updateBadges=function(){if(!this.badgesEl)return;this.badgesEl.textContent="";const a=this;this.activeLanguages.forEach(function(o){const n=m("ln-translations-badge","ln-translations");if(!n)return;const l=n.querySelector("[data-ln-translations-lang]");l.setAttribute("data-ln-translations-lang",o);const s=l.querySelector("span");s.textContent=a.locales[o]||o.toUpperCase();const r=l.querySelector("button"),d=a.locales[o]||o.toUpperCase();r.setAttribute("aria-label",a.removeLabel.replace("{lang}",d)),r.addEventListener("click",function(u){u.ctrlKey||u.metaKey||u.button===1||(u.preventDefault(),u.stopPropagation(),a.removeLanguage(o))}),a.badgesEl.appendChild(n)})},c.prototype.addLanguage=function(a,o){if(this.activeLanguages.has(a))return;const n=this.locales[a]||a;if(S(this.dom,"ln-translations:before-add",{target:this.dom,lang:a,langName:n}).defaultPrevented)return;this.activeLanguages.add(a),o=o||{};const s=this.dom.querySelectorAll("[data-ln-translatable]");for(const r of s){const d=r.getAttribute("data-ln-translatable"),u=r.getAttribute("data-ln-translations-prefix")||"",f=r.querySelector(this.defaultLang?'[data-ln-translatable-lang="'+this.defaultLang+'"]':"input:not([data-ln-translatable-lang]), textarea:not([data-ln-translatable-lang]), select:not([data-ln-translatable-lang])");if(!f)continue;const h=f.cloneNode(f.tagName==="SELECT");u?h.name=u+"[trans]["+a+"]["+d+"]":h.name="trans["+a+"]["+d+"]",h.value=o[d]!==void 0?o[d]:"",h.removeAttribute("id"),"placeholder"in h&&(h.placeholder=this.placeholderLabel.replace("{lang}",n)),h.setAttribute("data-ln-translatable-lang",a);const p=r.querySelectorAll('[data-ln-translatable-lang]:not([data-ln-translatable-lang="'+this.defaultLang+'"])'),g=p.length>0?p[p.length-1]:f;g.parentNode.insertBefore(h,g.nextSibling)}this._updateDropdown(),this._updateBadges(),A(this.dom,"ln-translations:added",{target:this.dom,lang:a,langName:n})},c.prototype.removeLanguage=function(a){if(!this.activeLanguages.has(a)||S(this.dom,"ln-translations:before-remove",{target:this.dom,lang:a}).defaultPrevented)return;const n=this.dom.querySelectorAll('[data-ln-translatable-lang="'+a+'"]');for(const l of n)l.parentNode.removeChild(l);this.activeLanguages.delete(a),this._updateDropdown(),this._updateBadges(),A(this.dom,"ln-translations:removed",{target:this.dom,lang:a})},c.prototype.getActiveLanguages=function(){return new Set(this.activeLanguages)},c.prototype.hasLanguage=function(a){return this.activeLanguages.has(a)},c.prototype.destroy=function(){if(!this.dom[e])return;const a=this.defaultLang,o=this.dom.querySelectorAll("[data-ln-translatable-lang]");for(const n of o)n.getAttribute("data-ln-translatable-lang")!==a&&n.parentNode.removeChild(n);this.dom.removeEventListener("ln-translations:request-add",this._onRequestAdd),this.dom.removeEventListener("ln-translations:request-remove",this._onRequestRemove),delete this.dom[e]},M(t,e,c,"ln-translations")})()})();
|
|
@@ -20,6 +20,8 @@ import { cloneTemplate, dispatch, dispatchCancelable, registerComponent } from '
|
|
|
20
20
|
this.dom = dom;
|
|
21
21
|
this.activeLanguages = new Set();
|
|
22
22
|
this.defaultLang = dom.getAttribute(DOM_SELECTOR + '-default') || '';
|
|
23
|
+
this.placeholderLabel = dom.getAttribute(DOM_SELECTOR + '-placeholder') || '{lang} translation';
|
|
24
|
+
this.removeLabel = dom.getAttribute(DOM_SELECTOR + '-remove-label') || 'Remove {lang}';
|
|
23
25
|
this.badgesEl = dom.querySelector('[' + DOM_SELECTOR + '-active]');
|
|
24
26
|
this.menuEl = dom.querySelector('[data-ln-dropdown] > [data-ln-toggle]');
|
|
25
27
|
|
|
@@ -121,7 +123,7 @@ import { cloneTemplate, dispatch, dispatchCancelable, registerComponent } from '
|
|
|
121
123
|
// Hide trigger if no languages available
|
|
122
124
|
const triggerBtn = this.dom.querySelector('[' + DOM_SELECTOR + '-add]');
|
|
123
125
|
if (triggerBtn) {
|
|
124
|
-
triggerBtn.
|
|
126
|
+
triggerBtn.hidden = availableCount === 0;
|
|
125
127
|
}
|
|
126
128
|
};
|
|
127
129
|
|
|
@@ -143,7 +145,8 @@ import { cloneTemplate, dispatch, dispatchCancelable, registerComponent } from '
|
|
|
143
145
|
label.textContent = self.locales[lang] || lang.toUpperCase();
|
|
144
146
|
|
|
145
147
|
const closeBtn = p.querySelector('button');
|
|
146
|
-
|
|
148
|
+
const removeLangName = self.locales[lang] || lang.toUpperCase();
|
|
149
|
+
closeBtn.setAttribute('aria-label', self.removeLabel.replace('{lang}', removeLangName));
|
|
147
150
|
|
|
148
151
|
closeBtn.addEventListener('click', function (e) {
|
|
149
152
|
if (e.ctrlKey || e.metaKey || e.button === 1) return;
|
|
@@ -184,7 +187,7 @@ import { cloneTemplate, dispatch, dispatchCancelable, registerComponent } from '
|
|
|
184
187
|
);
|
|
185
188
|
if (!original) continue;
|
|
186
189
|
|
|
187
|
-
const clone = original.cloneNode(
|
|
190
|
+
const clone = original.cloneNode(original.tagName === 'SELECT');
|
|
188
191
|
|
|
189
192
|
// Set name
|
|
190
193
|
if (prefix) {
|
|
@@ -199,8 +202,10 @@ import { cloneTemplate, dispatch, dispatchCancelable, registerComponent } from '
|
|
|
199
202
|
// Remove id to avoid duplicates
|
|
200
203
|
clone.removeAttribute('id');
|
|
201
204
|
|
|
202
|
-
// Set placeholder
|
|
203
|
-
|
|
205
|
+
// Set placeholder (selects have no placeholder property)
|
|
206
|
+
if ('placeholder' in clone) {
|
|
207
|
+
clone.placeholder = this.placeholderLabel.replace('{lang}', langName);
|
|
208
|
+
}
|
|
204
209
|
|
|
205
210
|
// Mark with language attribute
|
|
206
211
|
clone.setAttribute('data-ln-translatable-lang', lang);
|
package/js/ln-upload/README.md
CHANGED
|
@@ -2,13 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
File upload component — drag-and-drop zone with XHR progress, client-side validation, and auto-rendered hidden inputs for form submit.
|
|
4
4
|
|
|
5
|
+
## Rationale & Mindset
|
|
6
|
+
|
|
7
|
+
Historically, the component used custom attributes like `data-ln-upload-context` to pass extra parameters (such as the context category `lecture`, `email_logo`, etc.) to the upload endpoint. This created a **tight coupling** between the generic frontend library component and specific project-level backend parameter names.
|
|
8
|
+
|
|
9
|
+
To make the component truly generic and declarative, it follows a **native-HTML form design mindset**:
|
|
10
|
+
* **Inputs as Parameters**: Any parameter or metadata that needs to be sent alongside the uploaded file is defined using standard HTML form inputs (e.g. `<input type="hidden">`, `<select>`, `<textarea>`) nested directly inside the `.ln-upload` container.
|
|
11
|
+
* **Dynamic Gathering**: When a file is dropped or selected, the component dynamically serializes all nested inputs and appends them to the `FormData` request.
|
|
12
|
+
* **Separation of Concerns**: The frontend library does not need to know about specific keys (like `context` or `entity_id`). It simply forwards whatever form inputs you declare.
|
|
13
|
+
|
|
14
|
+
### Benefits
|
|
15
|
+
1. **Zero Configuration for Backend Keys**: No need to rewrite the JS component if the backend changes a parameter name from `context` to `type` or requires extra fields.
|
|
16
|
+
2. **Dynamic Values**: If an input's value is modified by other scripts on the page prior to upload (e.g., dynamically setting an `entity_id` based on selection), the upload request will automatically receive the latest value at the moment of file dispatch.
|
|
17
|
+
3. **Decoupled Delete Routes**: The component derives the `DELETE` endpoint dynamically by replacing `/upload` in the upload URL with the file ID, or you can specify a custom delete pattern using `data-ln-upload-delete="/my-api/{id}"`.
|
|
18
|
+
|
|
5
19
|
## Attributes
|
|
6
20
|
|
|
7
21
|
| Attribute | On | Description |
|
|
8
22
|
|-----------|-----|-------------|
|
|
9
23
|
| `data-ln-upload="/files/upload"` | container | Upload URL (default: `/files/upload`) |
|
|
10
24
|
| `data-ln-upload-accept=".pdf,.doc,.docx"` | container | Allowed extensions (comma-separated) |
|
|
11
|
-
| `data-ln-upload-
|
|
25
|
+
| `data-ln-upload-delete="/files/{id}"` | container | Optional custom delete URL pattern containing `{id}` (default: derived dynamically by replacing `/upload` with the file ID) |
|
|
26
|
+
| `data-ln-upload-context="documents"` | container | **[DEPRECATED]** Legacy context string sent as `context` fallback if no `<input name="context">` is nested. |
|
|
12
27
|
| `data-ln-upload-dict="key"` | hidden element | I18n dictionary for messages (see below) |
|
|
13
28
|
|
|
14
29
|
## Dictionary (i18n)
|
|
@@ -57,7 +72,7 @@ Your template MUST include these elements for the component to function:
|
|
|
57
72
|
| `<li>` root | `data-ln-class="ln-upload__item--uploading:uploading, ln-upload__item--error:error, ln-upload__item--deleting:deleting"` | State classes toggled via `fill()` |
|
|
58
73
|
| File name target | `data-ln-field="name"` | File name text |
|
|
59
74
|
| Size/status target | `data-ln-field="sizeText"` | `"0%"` → `"45%"` → `"12.3 KB"` → `"Error"` |
|
|
60
|
-
| File icon `<use>` | `data-ln-attr="href:iconHref"` | Auto-swapped to `#ln-file` / `#
|
|
75
|
+
| File icon `<use>` | `data-ln-attr="href:iconHref"` | Auto-swapped to `#ln-icon-file` / `#ln-icon-custom-file-pdf` / `#ln-icon-custom-file-doc` / `#ln-icon-custom-file-epub` based on extension |
|
|
61
76
|
| Remove button | `data-ln-upload-action="remove"` and `data-ln-attr="aria-label:removeLabel, title:removeLabel"` | Click target (attribute-based, not class-based) |
|
|
62
77
|
| Progress bar | `class="ln-upload__progress-bar"` | Width is animated imperatively via inline style |
|
|
63
78
|
|
|
@@ -67,13 +82,13 @@ Your template MUST include these elements for the component to function:
|
|
|
67
82
|
<div data-ln-upload="/files/upload">
|
|
68
83
|
<template data-ln-template="ln-upload-item">
|
|
69
84
|
<li class="ln-upload__item" data-ln-class="ln-upload__item--uploading:uploading, ln-upload__item--error:error, ln-upload__item--deleting:deleting">
|
|
70
|
-
<svg class="ln-icon ln-icon--lg" aria-hidden="true"><use data-ln-attr="href:iconHref" href="#ln-file"></use></svg>
|
|
85
|
+
<svg class="ln-icon ln-icon--lg" aria-hidden="true"><use data-ln-attr="href:iconHref" href="#ln-icon-file"></use></svg>
|
|
71
86
|
<article>
|
|
72
87
|
<span class="ln-upload__name" data-ln-field="name"></span>
|
|
73
88
|
<span class="ln-upload__size" data-ln-field="sizeText"></span>
|
|
74
89
|
</article>
|
|
75
90
|
<button type="button" class="ln-upload__remove" data-ln-upload-action="remove" data-ln-attr="aria-label:removeLabel, title:removeLabel">
|
|
76
|
-
<svg class="ln-icon" aria-hidden="true"><use href="#ln-x"></use></svg>
|
|
91
|
+
<svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-x"></use></svg>
|
|
77
92
|
</button>
|
|
78
93
|
<div class="ln-upload__progress"><div class="ln-upload__progress-bar"></div></div>
|
|
79
94
|
</li>
|
|
@@ -129,8 +144,16 @@ Expected status: `200`
|
|
|
129
144
|
|
|
130
145
|
## HTML Structure
|
|
131
146
|
|
|
147
|
+
### Recommended (New Declarative Approach)
|
|
148
|
+
|
|
149
|
+
Place nested hidden inputs (or any form fields) representing your upload metadata inside the container:
|
|
150
|
+
|
|
132
151
|
```html
|
|
133
|
-
<div data-ln-upload="/files/upload" data-ln-upload-accept=".pdf,.doc,.docx"
|
|
152
|
+
<div class="ln-upload" data-ln-upload="/files/upload" data-ln-upload-accept=".pdf,.doc,.docx">
|
|
153
|
+
<!-- Extra parameters sent dynamically to the POST endpoint -->
|
|
154
|
+
<input type="hidden" name="context" value="documents">
|
|
155
|
+
<input type="hidden" name="entity_id" value="123">
|
|
156
|
+
|
|
134
157
|
<div class="ln-upload__zone">
|
|
135
158
|
<p>Drag files here or click to browse</p>
|
|
136
159
|
</div>
|
|
@@ -140,15 +163,26 @@ Expected status: `200`
|
|
|
140
163
|
</div>
|
|
141
164
|
```
|
|
142
165
|
|
|
166
|
+
### Legacy (Attribute-Based Approach)
|
|
167
|
+
|
|
168
|
+
```html
|
|
169
|
+
<div class="ln-upload" data-ln-upload="/files/upload" data-ln-upload-accept=".pdf,.doc,.docx" data-ln-upload-context="documents">
|
|
170
|
+
<div class="ln-upload__zone">
|
|
171
|
+
<p>Drag files here or click to browse</p>
|
|
172
|
+
</div>
|
|
173
|
+
<ul class="ln-upload__list"></ul>
|
|
174
|
+
</div>
|
|
175
|
+
```
|
|
176
|
+
|
|
143
177
|
## File Icons
|
|
144
178
|
|
|
145
179
|
The component automatically adds an SVG icon per file type using the icon loader:
|
|
146
|
-
- `#
|
|
147
|
-
- `#
|
|
148
|
-
- `#
|
|
149
|
-
- `#ln-file` — all other types (Tabler CDN)
|
|
180
|
+
- `#ln-icon-custom-file-pdf` — PDF (custom CDN)
|
|
181
|
+
- `#ln-icon-custom-file-doc` — DOC/DOCX (custom CDN)
|
|
182
|
+
- `#ln-icon-custom-file-epub` — EPUB (custom CDN)
|
|
183
|
+
- `#ln-icon-file` — all other types (Tabler CDN)
|
|
150
184
|
|
|
151
|
-
Custom icons require `window.
|
|
185
|
+
Custom icons require `window.LN_ICON_CUSTOM_CDN` to be set. See `js/ln-icon/README.md`.
|
|
152
186
|
|
|
153
187
|
## Hidden Inputs
|
|
154
188
|
|
|
@@ -178,3 +212,44 @@ For reference, active development, or customization, the component's codebase is
|
|
|
178
212
|
|
|
179
213
|
* **Active Development Source (ESM)**: The primary development file where all component features, drag-and-drop mechanics, validation, and XHR progress logic are implemented is [js/ln-upload/src/ln-upload.js](file:///c:/laragon/www/ln-ashlar/js/ln-upload/src/ln-upload.js).
|
|
180
214
|
* **Compiled Standalone (IIFE)**: The built zero-dependency distribution version compiled for browser execution is [js/ln-upload/ln-upload.js](file:///c:/laragon/www/ln-ashlar/js/ln-upload/ln-upload.js).
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## 🔧 Internals
|
|
219
|
+
|
|
220
|
+
Source: `js/ln-upload/ln-upload.js`. Each container has a closure-scoped `uploadedFiles` Map (`localId → { serverId, name, size }`) — not reactive; DOM is mutated directly as XHR events fire. `fileIdCounter` generates unique `localId` values (`'file-1'`, `'file-2'`, ...).
|
|
221
|
+
|
|
222
|
+
### Init
|
|
223
|
+
|
|
224
|
+
`_initUpload(container)`, guarded by `container.lnUploadAPI` against double-init. `_ensureDefaultItemTemplate()` runs once per init and injects the default `<template>` into `<body>` if no `[data-ln-template="ln-upload-item"]` exists anywhere in the document (see template lookup order above). A hidden `<input type="file" multiple>` is created if none exists inside the container.
|
|
225
|
+
|
|
226
|
+
### `addFile(file)` flow
|
|
227
|
+
|
|
228
|
+
1. Validate extension against `data-ln-upload-accept`. Invalid → dispatch `ln-upload:invalid`, enqueue error toast, return.
|
|
229
|
+
2. `cloneTemplateScoped` resolves the template (scoped → global → auto-injected default).
|
|
230
|
+
3. `fill(li, {...})` populates name, `sizeText: '0%'`, icon `href`, `removeLabel`, and the `uploading`/`error`/`deleting` state classes in one pass — the `ln-icon` observer independently picks up the `<use href>` swap and fetches the sprite.
|
|
231
|
+
4. `data-file-id` set (structural, not a `fill` slot); remove button disabled until upload completes.
|
|
232
|
+
5. `<li>` appended to `.ln-upload__list`, XHR POST opened with `FormData` (nested container inputs serialized in).
|
|
233
|
+
6. Progress: `progressBar.style.width` set imperatively + `fill(li, { sizeText: percent + '%' })`.
|
|
234
|
+
7. 2xx: `fill` to final state, remove button re-enabled, entry stored in `uploadedFiles`, hidden inputs rebuilt, `ln-upload:uploaded` dispatched.
|
|
235
|
+
8. Non-2xx / XHR error: progress forced to 100%, `fill(li, { sizeText: dict.error, error: true })`, `ln-upload:error` dispatched, error toast enqueued.
|
|
236
|
+
|
|
237
|
+
### Remove / delete flow
|
|
238
|
+
|
|
239
|
+
Remove-button clicks are delegated on `.ln-upload__list`, resolved via `closest('[data-ln-upload-action="remove"]')` (works through nested `svg`/`use`). `removeFile(localId)`: if the file has no `serverId` yet (still uploading), it's removed from DOM + Map directly. If a `serverId` exists, `fill(item, { deleting: true })` then `DELETE /files/{serverId}` — success removes from DOM + Map and dispatches `ln-upload:removed`; failure (non-200 or network catch) reverts `deleting: false` and enqueues an error toast.
|
|
240
|
+
|
|
241
|
+
### Hidden inputs
|
|
242
|
+
|
|
243
|
+
`updateHiddenInput()` fully rebuilds `<input type="hidden" name="file_ids[]">` from the current `uploadedFiles` Map after every upload and delete — form submit always reflects the current (not deleted) file set.
|
|
244
|
+
|
|
245
|
+
### Auto-toast
|
|
246
|
+
|
|
247
|
+
Errors dispatch `ln-toast:enqueue` on `window`. This is a soft integration — `ln-upload` does not import `ln-toast`; the toast only renders if that component is present.
|
|
248
|
+
|
|
249
|
+
### `destroy()`
|
|
250
|
+
|
|
251
|
+
Part of `container.lnUploadAPI`. Removes all listeners (zone, input, list), clears `uploadedFiles`, empties the `<ul>`, removes the init guard, deletes `lnUploadAPI`.
|
|
252
|
+
|
|
253
|
+
### MutationObserver
|
|
254
|
+
|
|
255
|
+
Global observer on `document.body` detects `[data-ln-upload]` elements added to the DOM and calls `_initUpload()` on them.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(){"use strict";const z={};function V(n,o){z[n]||(z[n]=document.querySelector('[data-ln-template="'+n+'"]'));const c=z[n];return c?c.content.cloneNode(!0):(console.warn("["+o+'] Template "'+n+'" not found'),null)}function E(n,o,c){n.dispatchEvent(new CustomEvent(o,{bubbles:!0,detail:c||{}}))}function A(n,o){if(!n||!o)return n;const c=n.querySelectorAll("[data-ln-field]");for(let s=0;s<c.length;s++){const f=c[s],p=f.getAttribute("data-ln-field");o[p]!=null&&(f.textContent=o[p])}const m=n.querySelectorAll("[data-ln-attr]");for(let s=0;s<m.length;s++){const f=m[s],p=f.getAttribute("data-ln-attr").split(",");for(let b=0;b<p.length;b++){const L=p[b].trim().split(":");if(L.length!==2)continue;const w=L[0].trim(),y=L[1].trim();o[y]!=null&&f.setAttribute(w,o[y])}}const _=n.querySelectorAll("[data-ln-show]");for(let s=0;s<_.length;s++){const f=_[s],p=f.getAttribute("data-ln-show");p in o&&f.classList.toggle("hidden",!o[p])}const M=n.querySelectorAll("[data-ln-class]");for(let s=0;s<M.length;s++){const f=M[s],p=f.getAttribute("data-ln-class").split(",");for(let b=0;b<p.length;b++){const L=p[b].trim().split(":");if(L.length!==2)continue;const w=L[0].trim(),y=L[1].trim();y in o&&f.classList.toggle(w,!!o[y])}}return n}function R(n,o){if(!document.body){document.addEventListener("DOMContentLoaded",function(){R(n,o)}),console.warn("["+o+'] Script loaded before <body> — add "defer" to your <script> tag');return}n()}function W(n,o,c){if(n){const m=n.querySelector('[data-ln-template="'+o+'"]');if(m)return m.content.cloneNode(!0)}return V(o,c)}function Z(n,o){const c={},m=n.querySelectorAll("["+o+"]");for(let _=0;_<m.length;_++)c[m[_].getAttribute(o)]=m[_].textContent,m[_].remove();return c}const N={};function Q(n,o){N[n]=o}function Y(n){return N[n]||{ingress:o=>o,egress:o=>o}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=Q,window.lnCore.getDataMapper=Y),(function(){const n="data-ln-upload",o="lnUpload",c="data-ln-upload-dict",m="data-ln-upload-accept",_="data-ln-upload-context",M='<template data-ln-template="ln-upload-item"><li class="ln-upload__item" data-ln-class="ln-upload__item--uploading:uploading, ln-upload__item--error:error, ln-upload__item--deleting:deleting"><svg class="ln-icon" aria-hidden="true"><use data-ln-attr="href:iconHref" href="#ln-file"></use></svg><span class="ln-upload__name" data-ln-field="name"></span><span class="ln-upload__size" data-ln-field="sizeText"></span><button type="button" class="ln-upload__remove" data-ln-upload-action="remove" data-ln-attr="aria-label:removeLabel, title:removeLabel"><svg class="ln-icon" aria-hidden="true"><use href="#ln-x"></use></svg></button><div class="ln-upload__progress"><div class="ln-upload__progress-bar"></div></div></li></template>';function s(){if(document.querySelector('[data-ln-template="ln-upload-item"]')||!document.body)return;const t=document.createElement("div");t.innerHTML=M;const r=t.firstElementChild;r&&document.body.appendChild(r)}if(window[o]!==void 0)return;function f(t){if(t===0)return"0 B";const r=1024,l=["B","KB","MB","GB"],d=Math.floor(Math.log(t)/Math.log(r));return parseFloat((t/Math.pow(r,d)).toFixed(1))+" "+l[d]}function p(t){return t.split(".").pop().toLowerCase()}function b(t){return t==="docx"&&(t="doc"),["pdf","doc","epub"].includes(t)?"lnc-file-"+t:"ln-file"}function L(t,r){if(!r)return!0;const l="."+p(t.name);return r.split(",").map(function(C){return C.trim().toLowerCase()}).includes(l.toLowerCase())}function w(t){if(t.hasAttribute("data-ln-upload-initialized"))return;t.setAttribute("data-ln-upload-initialized","true"),s();const r=Z(t,c),l=t.querySelector(".ln-upload__zone"),d=t.querySelector(".ln-upload__list"),C=t.getAttribute(m)||"";if(!l||!d){console.warn("[ln-upload] Missing .ln-upload__zone or .ln-upload__list in container:",t);return}let v=t.querySelector('input[type="file"]');v||(v=document.createElement("input"),v.type="file",v.multiple=!0,v.classList.add("hidden"),C&&(v.accept=C.split(",").map(function(e){return e=e.trim(),e.startsWith(".")?e:"."+e}).join(",")),t.appendChild(v));const ee=t.getAttribute(n)||"/files/upload",te=t.getAttribute(_)||"",h=new Map;let ne=0;function F(){const e=document.querySelector('meta[name="csrf-token"]');return e?e.getAttribute("content"):""}function oe(e){if(!L(e,C)){const i=r["invalid-type"];E(t,"ln-upload:invalid",{file:e,message:i}),E(window,"ln-toast:enqueue",{type:"error",title:r["invalid-title"]||"Invalid File",message:i||r["invalid-type"]||"This file type is not allowed"});return}const a="file-"+ ++ne,u=p(e.name),q=b(u),G=W(t,"ln-upload-item","ln-upload");if(!G)return;const T=G.firstElementChild;if(!T)return;T.setAttribute("data-file-id",a),A(T,{name:e.name,sizeText:"0%",iconHref:"#"+q,removeLabel:r.remove||"Remove",uploading:!0,error:!1,deleting:!1});const O=T.querySelector(".ln-upload__progress-bar"),D=T.querySelector('[data-ln-upload-action="remove"]');D&&(D.disabled=!0),d.appendChild(T);const k=new FormData;k.append("file",e),k.append("context",te);const g=new XMLHttpRequest;g.upload.addEventListener("progress",function(i){if(i.lengthComputable){const I=Math.round(i.loaded/i.total*100);O.style.width=I+"%",A(T,{sizeText:I+"%"})}}),g.addEventListener("load",function(){if(g.status>=200&&g.status<300){let i;try{i=JSON.parse(g.responseText)}catch{x("Invalid response");return}A(T,{sizeText:f(i.size||e.size),uploading:!1}),D&&(D.disabled=!1),h.set(a,{serverId:i.id,name:i.name,size:i.size}),S(),E(t,"ln-upload:uploaded",{localId:a,serverId:i.id,name:i.name})}else{let i=r["upload-failed"]||"Upload failed";try{i=JSON.parse(g.responseText).message||i}catch{}x(i)}}),g.addEventListener("error",function(){x(r["network-error"]||"Network error")});function x(i){O&&(O.style.width="100%"),A(T,{sizeText:r.error||"Error",uploading:!1,error:!0}),D&&(D.disabled=!1),E(t,"ln-upload:error",{file:e,message:i}),E(window,"ln-toast:enqueue",{type:"error",title:r["error-title"]||"Upload Error",message:i||r["upload-failed"]||"Failed to upload file"})}g.open("POST",ee),g.setRequestHeader("X-CSRF-TOKEN",F()),g.setRequestHeader("Accept","application/json"),g.send(k)}function S(){for(const e of t.querySelectorAll('input[name="file_ids[]"]'))e.remove();for(const[,e]of h){const a=document.createElement("input");a.type="hidden",a.name="file_ids[]",a.value=e.serverId,t.appendChild(a)}}function re(e){const a=h.get(e),u=d.querySelector('[data-file-id="'+e+'"]');if(!a||!a.serverId){u&&u.remove(),h.delete(e),S();return}u&&A(u,{deleting:!0}),fetch("/files/"+a.serverId,{method:"DELETE",headers:{"X-CSRF-TOKEN":F(),Accept:"application/json"}}).then(function(q){q.status===200?(u&&u.remove(),h.delete(e),S(),E(t,"ln-upload:removed",{localId:e,serverId:a.serverId})):(u&&A(u,{deleting:!1}),E(window,"ln-toast:enqueue",{type:"error",title:r["delete-title"]||"Error",message:r["delete-error"]||"Failed to delete file"}))}).catch(function(q){console.warn("[ln-upload] Delete error:",q),u&&A(u,{deleting:!1}),E(window,"ln-toast:enqueue",{type:"error",title:r["network-error"]||"Network error",message:r["connection-error"]||"Could not connect to server"})})}function H(e){for(const a of e)oe(a);v.value=""}const B=function(){v.click()},P=function(){H(this.files)},U=function(e){e.preventDefault(),e.stopPropagation(),l.classList.add("ln-upload__zone--dragover")},j=function(e){e.preventDefault(),e.stopPropagation(),l.classList.add("ln-upload__zone--dragover")},X=function(e){e.preventDefault(),e.stopPropagation(),l.classList.remove("ln-upload__zone--dragover")},K=function(e){e.preventDefault(),e.stopPropagation(),l.classList.remove("ln-upload__zone--dragover"),H(e.dataTransfer.files)},J=function(e){const a=e.target.closest('[data-ln-upload-action="remove"]');if(!a||!d.contains(a)||a.disabled)return;const u=a.closest(".ln-upload__item");u&&re(u.getAttribute("data-file-id"))};l.addEventListener("click",B),v.addEventListener("change",P),l.addEventListener("dragenter",U),l.addEventListener("dragover",j),l.addEventListener("dragleave",X),l.addEventListener("drop",K),d.addEventListener("click",J),t.lnUploadAPI={getFileIds:function(){return Array.from(h.values()).map(function(e){return e.serverId})},getFiles:function(){return Array.from(h.values())},clear:function(){for(const[,e]of h)e.serverId&&fetch("/files/"+e.serverId,{method:"DELETE",headers:{"X-CSRF-TOKEN":F(),Accept:"application/json"}});h.clear(),d.innerHTML="",S(),E(t,"ln-upload:cleared",{})},destroy:function(){l.removeEventListener("click",B),v.removeEventListener("change",P),l.removeEventListener("dragenter",U),l.removeEventListener("dragover",j),l.removeEventListener("dragleave",X),l.removeEventListener("drop",K),d.removeEventListener("click",J),h.clear(),d.innerHTML="",S(),t.removeAttribute("data-ln-upload-initialized"),delete t.lnUploadAPI}}}function y(){for(const t of document.querySelectorAll("["+n+"]"))w(t)}function $(){R(function(){new MutationObserver(function(r){for(const l of r)if(l.type==="childList"){for(const d of l.addedNodes)if(d.nodeType===1){d.hasAttribute(n)&&w(d);for(const C of d.querySelectorAll("["+n+"]"))w(C)}}else l.type==="attributes"&&l.target.hasAttribute(n)&&w(l.target)}).observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:[n]})},"ln-upload")}window[o]={init:w,initAll:y},$(),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",y):y()})()})();
|
|
1
|
+
(function(){"use strict";if(typeof window<"u"){const e=console.warn;console.warn=function(...t){typeof t[0]=="string"&&(t[0].startsWith("[ln-")||t[0].startsWith("[lnCore"))&&!(document.documentElement.hasAttribute("data-ln-debug")||document.body&&document.body.hasAttribute("data-ln-debug"))||e.apply(console,t)}}const M={};function B(e,t){M[e]||(M[e]=document.querySelector('[data-ln-template="'+e+'"]'));const l=M[e];return l?l.content.cloneNode(!0):(console.warn("["+(t||"ln-core")+'] Template "'+e+'" not found'),null)}function E(e,t,l){e.dispatchEvent(new CustomEvent(t,{bubbles:!0,detail:l||{}}))}function A(e,t){if(!e||!t)return e;const l=e.querySelectorAll("[data-ln-field]");for(let r=0;r<l.length;r++){const s=l[r],i=s.getAttribute("data-ln-field");t[i]!=null&&(s.textContent=t[i])}const d=e.querySelectorAll("[data-ln-attr]");for(let r=0;r<d.length;r++){const s=d[r],i=s.getAttribute("data-ln-attr").split(",");for(let f=0;f<i.length;f++){const w=i[f].trim().split(":");if(w.length!==2)continue;const h=w[0].trim(),y=w[1].trim();t[y]!=null&&s.setAttribute(h,t[y])}}const p=e.querySelectorAll("[data-ln-show]");for(let r=0;r<p.length;r++){const s=p[r],i=s.getAttribute("data-ln-show");i in t&&s.classList.toggle("hidden",!t[i])}const m=e.querySelectorAll("[data-ln-class]");for(let r=0;r<m.length;r++){const s=m[r],i=s.getAttribute("data-ln-class").split(",");for(let f=0;f<i.length;f++){const w=i[f].trim().split(":");if(w.length!==2)continue;const h=w[0].trim(),y=w[1].trim();y in t&&s.classList.toggle(h,!!t[y])}}return e}function le(e,t){e.matches&&e.matches("[data-ln-form], [data-ln-fillable]")&&e.dispatchEvent(new CustomEvent("ln-fill",{detail:t??null,bubbles:!0}));const l=e.querySelectorAll("[data-ln-form], [data-ln-fillable]");for(let d=0;d<l.length;d++)l[d].dispatchEvent(new CustomEvent("ln-fill",{detail:t??null,bubbles:!0}));return e}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore._fillBound||(window.lnCore._fillBound=!0,document.addEventListener("ln-fill",function(e){if(!(!e.target.matches||!e.target.matches("[data-ln-fillable]")))if(e.detail)A(e.target,e.detail);else{const t=e.target.querySelectorAll("[data-ln-field]");for(let l=0;l<t.length;l++)t[l].textContent=""}})));function P(e,t){if(!e||!t)return e;const l=document.createTreeWalker(e,NodeFilter.SHOW_TEXT);for(;l.nextNode();){const m=l.currentNode;m.textContent.indexOf("{{")!==-1&&(m.textContent=m.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(r,s){return t[s]!==void 0?t[s]:""}))}const d=function(m,r){return t[r]!==void 0?t[r]:""},p=Array.from(e.querySelectorAll("*"));e.nodeType===1&&p.push(e);for(let m=0;m<p.length;m++){const r=p[m],s=r.attributes;for(let i=0;i<s.length;i++){const f=s[i];f.value.indexOf("{{")!==-1&&r.setAttribute(f.name,f.value.replace(/\{\{\s*(\w+)\s*\}\}/g,d))}}return e}function re(e,t,l,d,p,m){const r={};for(let i=0;i<e.children.length;i++){const f=e.children[i],w=f.getAttribute("data-ln-key");w&&(r[w]=f)}const s=document.createDocumentFragment();for(let i=0;i<t.length;i++){const f=t[i],w=String(d(f));let h=r[w];if(h)p(h,f,i);else{const y=B(l,m);if(!y||(P(y,f),h=y.firstElementChild,!h))continue;h.setAttribute("data-ln-key",w),p(h,f,i)}s.appendChild(h)}e.textContent="",e.appendChild(s)}function H(e,t){if(!document.body){document.addEventListener("DOMContentLoaded",function(){H(e,t)}),console.warn("["+t+'] Script loaded before <body> — add "defer" to your <script> tag');return}e()}function ie(e,t,l){if(e){const d=e.querySelector('[data-ln-template="'+t+'"]');if(d)return d.content.cloneNode(!0)}return B(t,l)}function ae(e,t){const l={},d=e.querySelectorAll("["+t+"]");for(let p=0;p<d.length;p++)l[d[p].getAttribute(t)]=d[p].textContent,d[p].remove();return l}const j={};function se(e,t){j[e]=t}function de(e){return j[e]||{ingress:t=>t,egress:t=>t}}const K={};function W(e,t){if(!e||typeof t!="object")return;const l=e.toLowerCase().split("-")[0];K[l]=t}function ce(e){if(!e)return null;const t=e.toLowerCase().split("-")[0];return K[t]||null}W("mk",{monthsLong:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"],monthsShort:["јан","фев","мар","апр","мај","јун","јул","авг","септ","окт","ноем","дек"],daysLong:["недела","понеделник","вторник","среда","четврток","петок","сабота"],daysShort:["нед","пон","вт","ср","чет","пет","саб"]}),typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=se,window.lnCore.getDataMapper=de,window.lnCore.registerLocaleFallback=W,window.lnCore.getLocaleFallback=ce,window.lnCore.fillTemplate=P,window.lnCore.fill=A,window.lnCore.lnFill=le,window.lnCore.renderList=re);function X(e){return(e||"").replace(/^#/,"")}function O(e){const t=e===void 0?location.hash:e,l={},d=X(t);if(!d)return l;const p=d.split("&");for(let m=0;m<p.length;m++){const r=p[m];if(!r)continue;const s=r.indexOf(":"),i=s>-1?r.slice(0,s):r,f=s>-1?r.slice(s+1):"";if(i)try{l[i]=decodeURIComponent(f)}catch{l[i]=f}}return l}function ue(e){if(!e)return null;const t=O();return e in t?t[e]:null}function fe(e,t){if(!e)return;const l=O();t==null?delete l[e]:l[e]=String(t);const p=Object.keys(l).map(function(m){const r=l[m];return r===""?m:m+":"+encodeURIComponent(r)}).join("&");X(location.hash)!==p&&(location.hash=p)}function pe(e){return e.button===1||e.ctrlKey||e.metaKey||e.shiftKey?!1:(e.preventDefault(),!0)}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.hashParse=O,window.lnCore.hashGet=ue,window.lnCore.hashSet=fe,window.lnCore.hashLinkClick=pe),(function(){const e="data-ln-upload",t="lnUpload",l="data-ln-upload-dict",d="data-ln-upload-accept",p="data-ln-upload-context",m='<template data-ln-template="ln-upload-item"><li class="ln-upload__item" data-ln-class="ln-upload__item--uploading:uploading, ln-upload__item--error:error, ln-upload__item--deleting:deleting"><svg class="ln-icon" aria-hidden="true"><use data-ln-attr="href:iconHref" href="#ln-icon-file"></use></svg><span class="ln-upload__name" data-ln-field="name"></span><span class="ln-upload__size" data-ln-field="sizeText"></span><button type="button" class="ln-upload__remove" data-ln-upload-action="remove" data-ln-attr="aria-label:removeLabel, title:removeLabel"><svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-x"></use></svg></button><div class="ln-upload__progress"><div class="ln-upload__progress-bar"></div></div></li></template>';function r(){if(document.querySelector('[data-ln-template="ln-upload-item"]')||!document.body)return;const o=document.createElement("div");o.innerHTML=m;const c=o.firstElementChild;c&&document.body.appendChild(c)}if(window[t]!==void 0)return;function s(o){if(o===0)return"0 B";const c=1024,u=["B","KB","MB","GB"],v=Math.floor(Math.log(o)/Math.log(c));return parseFloat((o/Math.pow(c,v)).toFixed(1))+" "+u[v]}function i(o){return o.split(".").pop().toLowerCase()}function f(o){return o==="docx"&&(o="doc"),["pdf","doc","epub"].includes(o)?"ln-icon-custom-file-"+o:"ln-icon-file"}function w(o,c){if(!c)return!0;const u="."+i(o.name);return c.split(",").map(function(S){return S.trim().toLowerCase()}).includes(u.toLowerCase())}function h(o){if(o.lnUploadAPI)return;r();const c=ae(o,l),u=o.querySelector(".ln-upload__zone"),v=o.querySelector(".ln-upload__list"),S=o.getAttribute(d)||"";if(!u||!v){console.warn("[ln-upload] Missing .ln-upload__zone or .ln-upload__list in container:",o);return}let _=o.querySelector('input[type="file"]');_||(_=document.createElement("input"),_.type="file",_.multiple=!0,_.classList.add("hidden"),S&&(_.accept=S.split(",").map(function(n){return n=n.trim(),n.startsWith(".")?n:"."+n}).join(",")),o.appendChild(_));const x=o.getAttribute(e)||"/files/upload",G=o.getAttribute(p)||"",J=o.getAttribute("data-ln-upload-delete")||(x.includes("/upload")?x.replace(/\/upload\/?$/,"/{id}"):x+"/{id}"),L=new Map;let ge=0;function z(){const n=document.querySelector('meta[name="csrf-token"]');return n?n.getAttribute("content"):""}function he(n){if(!w(n,S)){const a=c["invalid-type"];E(o,"ln-upload:invalid",{file:n,message:a}),E(window,"ln-toast:enqueue",{type:"error",title:c["invalid-title"]||"Invalid File",message:a||c["invalid-type"]||"This file type is not allowed"});return}const g="file-"+ ++ge,b=i(n.name),N=f(b),D=ie(o,"ln-upload-item","ln-upload");if(!D)return;const T=D.firstElementChild;if(!T)return;T.setAttribute("data-file-id",g),A(T,{name:n.name,sizeText:"0%",iconHref:"#"+N,removeLabel:c.remove||"Remove",uploading:!0,error:!1,deleting:!1});const U=T.querySelector(".ln-upload__progress-bar"),k=T.querySelector('[data-ln-upload-action="remove"]');k&&(k.disabled=!0),v.appendChild(T);const F=new FormData;F.append("file",n);const oe=new Set;o.querySelectorAll("input, select, textarea").forEach(function(a){if(a.name&&a.name!=="file_ids[]"&&a.type!=="file"){if((a.type==="checkbox"||a.type==="radio")&&!a.checked)return;F.append(a.name,a.value),oe.add(a.name)}}),!oe.has("context")&&G&&F.append("context",G);const C=new XMLHttpRequest;C.upload.addEventListener("progress",function(a){if(a.lengthComputable){const I=Math.round(a.loaded/a.total*100);U.style.width=I+"%",A(T,{sizeText:I+"%"})}}),C.addEventListener("load",function(){if(C.status>=200&&C.status<300){let a;try{a=JSON.parse(C.responseText)}catch{R("Invalid response");return}A(T,{sizeText:s(a.size||n.size),uploading:!1}),k&&(k.disabled=!1),L.set(g,{serverId:a.id,name:a.name,size:a.size}),q(),E(o,"ln-upload:uploaded",{localId:g,serverId:a.id,name:a.name})}else{let a=c["upload-failed"]||"Upload failed";try{a=JSON.parse(C.responseText).message||a}catch{}R(a)}}),C.addEventListener("error",function(){R(c["network-error"]||"Network error")});function R(a){U&&(U.style.width="100%"),A(T,{sizeText:c.error||"Error",uploading:!1,error:!0}),k&&(k.disabled=!1),E(o,"ln-upload:error",{file:n,message:a}),E(window,"ln-toast:enqueue",{type:"error",title:c["error-title"]||"Upload Error",message:a||c["upload-failed"]||"Failed to upload file"})}C.open("POST",x),C.setRequestHeader("X-CSRF-TOKEN",z()),C.setRequestHeader("Accept","application/json"),C.send(F)}function q(){for(const n of o.querySelectorAll('input[name="file_ids[]"]'))n.remove();for(const[,n]of L){const g=document.createElement("input");g.type="hidden",g.name="file_ids[]",g.value=n.serverId,o.appendChild(g)}}function ve(n){const g=L.get(n),b=v.querySelector('[data-file-id="'+n+'"]');if(!g||!g.serverId){b&&b.remove(),L.delete(n),q();return}b&&A(b,{deleting:!0});const N=J.replace("{id}",g.serverId);fetch(N,{method:"DELETE",headers:{"X-CSRF-TOKEN":z(),Accept:"application/json"}}).then(function(D){D.status===200?(b&&b.remove(),L.delete(n),q(),E(o,"ln-upload:removed",{localId:n,serverId:g.serverId})):(b&&A(b,{deleting:!1}),E(window,"ln-toast:enqueue",{type:"error",title:c["delete-title"]||"Error",message:c["delete-error"]||"Failed to delete file"}))}).catch(function(D){console.warn("[ln-upload] Delete error:",D),b&&A(b,{deleting:!1}),E(window,"ln-toast:enqueue",{type:"error",title:c["network-error"]||"Network error",message:c["connection-error"]||"Could not connect to server"})})}function V(n){for(const g of n)he(g);_.value=""}const Z=function(){_.click()},$=function(){V(this.files)},Q=function(n){n.preventDefault(),n.stopPropagation(),u.classList.add("ln-upload__zone--dragover")},Y=function(n){n.preventDefault(),n.stopPropagation(),u.classList.add("ln-upload__zone--dragover")},ee=function(n){n.preventDefault(),n.stopPropagation(),u.classList.remove("ln-upload__zone--dragover")},te=function(n){n.preventDefault(),n.stopPropagation(),u.classList.remove("ln-upload__zone--dragover"),V(n.dataTransfer.files)},ne=function(n){const g=n.target.closest('[data-ln-upload-action="remove"]');if(!g||!v.contains(g)||g.disabled)return;const b=g.closest(".ln-upload__item");b&&ve(b.getAttribute("data-file-id"))};u.addEventListener("click",Z),_.addEventListener("change",$),u.addEventListener("dragenter",Q),u.addEventListener("dragover",Y),u.addEventListener("dragleave",ee),u.addEventListener("drop",te),v.addEventListener("click",ne),o.lnUploadAPI={getFileIds:function(){return Array.from(L.values()).map(function(n){return n.serverId})},getFiles:function(){return Array.from(L.values())},clear:function(){for(const[,n]of L)if(n.serverId){const g=J.replace("{id}",n.serverId);fetch(g,{method:"DELETE",headers:{"X-CSRF-TOKEN":z(),Accept:"application/json"}})}L.clear(),v.innerHTML="",q(),E(o,"ln-upload:cleared",{})},destroy:function(){u.removeEventListener("click",Z),_.removeEventListener("change",$),u.removeEventListener("dragenter",Q),u.removeEventListener("dragover",Y),u.removeEventListener("dragleave",ee),u.removeEventListener("drop",te),v.removeEventListener("click",ne),L.clear(),v.innerHTML="",q(),delete o.lnUploadAPI}}}function y(){for(const o of document.querySelectorAll("["+e+"]"))h(o)}function me(){H(function(){new MutationObserver(function(c){for(const u of c)if(u.type==="childList"){for(const v of u.addedNodes)if(v.nodeType===1){v.hasAttribute(e)&&h(v);for(const S of v.querySelectorAll("["+e+"]"))h(S)}}else u.type==="attributes"&&u.target.hasAttribute(e)&&h(u.target)}).observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:[e]})},"ln-upload")}window[t]={init:h,initAll:y},me(),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",y):y()})()})();
|