@livenetworks/ashlar 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +177 -0
- package/js/COMPONENTS.md +1102 -0
- package/js/index.js +41 -0
- package/js/ln-accordion/README.md +137 -0
- package/js/ln-accordion/ln-accordion.js +1 -0
- package/js/ln-accordion/src/ln-accordion.js +41 -0
- package/js/ln-ajax/README.md +91 -0
- package/js/ln-ajax/ln-ajax.js +1 -0
- package/js/ln-ajax/src/ln-ajax.js +277 -0
- package/js/ln-api-connector/README.md +150 -0
- package/js/ln-api-connector/ln-api-connector.js +1 -0
- package/js/ln-api-connector/src/ln-api-connector.js +265 -0
- package/js/ln-autoresize/README.md +80 -0
- package/js/ln-autoresize/ln-autoresize.js +1 -0
- package/js/ln-autoresize/src/ln-autoresize.js +47 -0
- package/js/ln-autosave/README.md +92 -0
- package/js/ln-autosave/ln-autosave.js +1 -0
- package/js/ln-autosave/src/ln-autosave.js +147 -0
- package/js/ln-circular-progress/README.md +161 -0
- package/js/ln-circular-progress/ln-circular-progress.js +1 -0
- package/js/ln-circular-progress/src/ln-circular-progress.js +133 -0
- package/js/ln-confirm/README.md +86 -0
- package/js/ln-confirm/_ln-confirm.scss +13 -0
- package/js/ln-confirm/ln-confirm.js +1 -0
- package/js/ln-confirm/src/ln-confirm.js +131 -0
- package/js/ln-core/crypto.js +83 -0
- package/js/ln-core/helpers.js +411 -0
- package/js/ln-core/index.js +5 -0
- package/js/ln-core/persist.js +71 -0
- package/js/ln-core/positioning.js +207 -0
- package/js/ln-core/reactive.js +74 -0
- package/js/ln-couchdb-connector/README.md +156 -0
- package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -0
- package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +348 -0
- package/js/ln-data-coordinator/README.md +165 -0
- package/js/ln-data-coordinator/ln-data-coordinator.js +1 -0
- package/js/ln-data-coordinator/src/ln-data-coordinator.js +249 -0
- package/js/ln-data-store/README.md +94 -0
- package/js/ln-data-store/ln-data-store.js +1 -0
- package/js/ln-data-store/src/ln-data-store.js +699 -0
- package/js/ln-data-table/README.md +110 -0
- package/js/ln-data-table/ln-data-table.js +1 -0
- package/js/ln-data-table/ln-data-table.scss +10 -0
- package/js/ln-data-table/src/ln-data-table.js +1103 -0
- package/js/ln-date/README.md +151 -0
- package/js/ln-date/ln-date.js +1 -0
- package/js/ln-date/src/ln-date.js +442 -0
- package/js/ln-dropdown/README.md +117 -0
- package/js/ln-dropdown/ln-dropdown.js +1 -0
- package/js/ln-dropdown/ln-dropdown.scss +15 -0
- package/js/ln-dropdown/src/ln-dropdown.js +174 -0
- package/js/ln-external-links/README.md +341 -0
- package/js/ln-external-links/ln-external-links.js +1 -0
- package/js/ln-external-links/src/ln-external-links.js +116 -0
- package/js/ln-filter/README.md +99 -0
- package/js/ln-filter/ln-filter.js +1 -0
- package/js/ln-filter/ln-filter.scss +7 -0
- package/js/ln-filter/src/ln-filter.js +404 -0
- package/js/ln-form/README.md +101 -0
- package/js/ln-form/ln-form.js +1 -0
- package/js/ln-form/src/ln-form.js +199 -0
- package/js/ln-http/README.md +89 -0
- package/js/ln-http/ln-http.js +1 -0
- package/js/ln-http/src/ln-http.js +219 -0
- package/js/ln-icons/README.md +88 -0
- package/js/ln-icons/ln-icons.js +1 -0
- package/js/ln-icons/src/ln-icons.js +169 -0
- package/js/ln-link/README.md +303 -0
- package/js/ln-link/ln-link.js +1 -0
- package/js/ln-link/src/ln-link.js +196 -0
- package/js/ln-modal/README.md +154 -0
- package/js/ln-modal/ln-modal.js +1 -0
- package/js/ln-modal/ln-modal.scss +11 -0
- package/js/ln-modal/src/ln-modal.js +201 -0
- package/js/ln-nav/README.md +70 -0
- package/js/ln-nav/ln-nav.js +1 -0
- package/js/ln-nav/src/ln-nav.js +177 -0
- package/js/ln-number/README.md +122 -0
- package/js/ln-number/ln-number.js +1 -0
- package/js/ln-number/src/ln-number.js +302 -0
- package/js/ln-popover/README.md +127 -0
- package/js/ln-popover/ln-popover.js +1 -0
- package/js/ln-popover/src/ln-popover.js +288 -0
- package/js/ln-progress/README.md +442 -0
- package/js/ln-progress/ln-progress.js +1 -0
- package/js/ln-progress/src/ln-progress.js +150 -0
- package/js/ln-search/README.md +83 -0
- package/js/ln-search/ln-search.js +1 -0
- package/js/ln-search/ln-search.scss +7 -0
- package/js/ln-search/src/ln-search.js +114 -0
- package/js/ln-sortable/README.md +95 -0
- package/js/ln-sortable/ln-sortable.js +1 -0
- package/js/ln-sortable/src/ln-sortable.js +203 -0
- package/js/ln-table/README.md +101 -0
- package/js/ln-table/ln-table-sort.js +1 -0
- package/js/ln-table/ln-table.js +1 -0
- package/js/ln-table/ln-table.scss +11 -0
- package/js/ln-table/src/ln-table-sort.js +168 -0
- package/js/ln-table/src/ln-table.js +473 -0
- package/js/ln-tabs/README.md +137 -0
- package/js/ln-tabs/ln-tabs.js +1 -0
- package/js/ln-tabs/src/ln-tabs.js +171 -0
- package/js/ln-time/README.md +81 -0
- package/js/ln-time/ln-time.js +1 -0
- package/js/ln-time/src/ln-time.js +192 -0
- package/js/ln-toast/README.md +122 -0
- package/js/ln-toast/ln-toast.js +15 -0
- package/js/ln-toast/src/ln-toast.js +210 -0
- package/js/ln-toast/template.html +14 -0
- package/js/ln-toggle/README.md +137 -0
- package/js/ln-toggle/ln-toggle.js +1 -0
- package/js/ln-toggle/src/ln-toggle.js +139 -0
- package/js/ln-tooltip/README.md +58 -0
- package/js/ln-tooltip/ln-tooltip.js +1 -0
- package/js/ln-tooltip/ln-tooltip.scss +9 -0
- package/js/ln-tooltip/src/ln-tooltip.js +169 -0
- package/js/ln-translations/README.md +96 -0
- package/js/ln-translations/ln-translations.js +1 -0
- package/js/ln-translations/src/ln-translations.js +275 -0
- package/js/ln-upload/README.md +180 -0
- package/js/ln-upload/ln-upload.js +1 -0
- package/js/ln-upload/ln-upload.scss +20 -0
- package/js/ln-upload/src/ln-upload.js +407 -0
- package/js/ln-validate/README.md +108 -0
- package/js/ln-validate/ln-validate.js +1 -0
- package/js/ln-validate/src/ln-validate.js +160 -0
- package/package.json +55 -0
- package/scss/base/_global.scss +83 -0
- package/scss/base/_reset.scss +17 -0
- package/scss/base/_typography.scss +125 -0
- package/scss/components/_accordion.scss +34 -0
- package/scss/components/_ajax.scss +15 -0
- package/scss/components/_alert.scss +5 -0
- package/scss/components/_app-shell.scss +15 -0
- package/scss/components/_avatar.scss +6 -0
- package/scss/components/_breadcrumbs.scss +33 -0
- package/scss/components/_button.scss +20 -0
- package/scss/components/_card.scss +10 -0
- package/scss/components/_chip.scss +5 -0
- package/scss/components/_circular-progress.scss +29 -0
- package/scss/components/_confirm.scss +5 -0
- package/scss/components/_data-table.scss +83 -0
- package/scss/components/_dropdown.scss +25 -0
- package/scss/components/_empty-state.scss +22 -0
- package/scss/components/_form.scss +100 -0
- package/scss/components/_layout.scss +8 -0
- package/scss/components/_link.scss +11 -0
- package/scss/components/_ln-table.scss +60 -0
- package/scss/components/_loader.scss +6 -0
- package/scss/components/_modal.scss +20 -0
- package/scss/components/_nav.scss +9 -0
- package/scss/components/_page-header.scss +10 -0
- package/scss/components/_popover.scss +10 -0
- package/scss/components/_progress.scss +17 -0
- package/scss/components/_prose.scss +5 -0
- package/scss/components/_scrollbar.scss +32 -0
- package/scss/components/_sections.scss +12 -0
- package/scss/components/_sidebar.scss +5 -0
- package/scss/components/_stat-card.scss +5 -0
- package/scss/components/_status-badge.scss +4 -0
- package/scss/components/_stepper.scss +5 -0
- package/scss/components/_table.scss +19 -0
- package/scss/components/_tabs.scss +21 -0
- package/scss/components/_timeline.scss +14 -0
- package/scss/components/_toast.scss +41 -0
- package/scss/components/_toggle.scss +81 -0
- package/scss/components/_tooltip.scss +18 -0
- package/scss/components/_translations.scss +111 -0
- package/scss/components/_upload.scss +51 -0
- package/scss/config/_breakpoints.scss +72 -0
- package/scss/config/_density.scss +117 -0
- package/scss/config/_icons.scss +37 -0
- package/scss/config/_mixins.scss +13 -0
- package/scss/config/_theme.scss +216 -0
- package/scss/config/_tokens.scss +419 -0
- package/scss/config/mixins/_accordion.scss +52 -0
- package/scss/config/mixins/_ajax.scss +39 -0
- package/scss/config/mixins/_alert.scss +82 -0
- package/scss/config/mixins/_app-shell.scss +312 -0
- package/scss/config/mixins/_avatar.scss +109 -0
- package/scss/config/mixins/_borders.scss +36 -0
- package/scss/config/mixins/_breadcrumbs.scss +72 -0
- package/scss/config/mixins/_breakpoints.scss +62 -0
- package/scss/config/mixins/_btn.scss +179 -0
- package/scss/config/mixins/_card.scss +338 -0
- package/scss/config/mixins/_chip.scss +66 -0
- package/scss/config/mixins/_circular-progress.scss +71 -0
- package/scss/config/mixins/_collapsible.scss +24 -0
- package/scss/config/mixins/_colors.scss +46 -0
- package/scss/config/mixins/_confirm.scss +31 -0
- package/scss/config/mixins/_data-table.scss +346 -0
- package/scss/config/mixins/_display.scss +32 -0
- package/scss/config/mixins/_dropdown.scss +143 -0
- package/scss/config/mixins/_empty-state.scss +30 -0
- package/scss/config/mixins/_focus.scss +55 -0
- package/scss/config/mixins/_footer.scss +42 -0
- package/scss/config/mixins/_form.scss +601 -0
- package/scss/config/mixins/_index.scss +58 -0
- package/scss/config/mixins/_interaction.scss +15 -0
- package/scss/config/mixins/_kbd.scss +22 -0
- package/scss/config/mixins/_layout.scss +117 -0
- package/scss/config/mixins/_link.scss +55 -0
- package/scss/config/mixins/_ln-table.scss +420 -0
- package/scss/config/mixins/_loader.scss +26 -0
- package/scss/config/mixins/_modal.scss +66 -0
- package/scss/config/mixins/_motion.scss +19 -0
- package/scss/config/mixins/_nav.scss +273 -0
- package/scss/config/mixins/_page-header.scss +69 -0
- package/scss/config/mixins/_popover.scss +25 -0
- package/scss/config/mixins/_position.scss +32 -0
- package/scss/config/mixins/_progress.scss +56 -0
- package/scss/config/mixins/_prose.scss +127 -0
- package/scss/config/mixins/_shadows.scss +8 -0
- package/scss/config/mixins/_sidebar.scss +95 -0
- package/scss/config/mixins/_sizing.scss +6 -0
- package/scss/config/mixins/_spacing.scss +19 -0
- package/scss/config/mixins/_stat-card.scss +68 -0
- package/scss/config/mixins/_status-badge.scss +83 -0
- package/scss/config/mixins/_stepper.scss +78 -0
- package/scss/config/mixins/_table.scss +215 -0
- package/scss/config/mixins/_tabs.scss +64 -0
- package/scss/config/mixins/_timeline.scss +69 -0
- package/scss/config/mixins/_toast.scss +148 -0
- package/scss/config/mixins/_tooltip.scss +111 -0
- package/scss/config/mixins/_transitions.scss +10 -0
- package/scss/config/mixins/_translations.scss +124 -0
- package/scss/config/mixins/_typography.scss +57 -0
- package/scss/config/mixins/_upload.scss +168 -0
- package/scss/ln-ashlar.scss +62 -0
- package/scss/tabler-icons.txt +5039 -0
- package/scss/utilities/_animations.scss +83 -0
- package/scss/utilities/_utilities.scss +49 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# ln-upload
|
|
2
|
+
|
|
3
|
+
File upload component — drag-and-drop zone with XHR progress, client-side validation, and auto-rendered hidden inputs for form submit.
|
|
4
|
+
|
|
5
|
+
## Attributes
|
|
6
|
+
|
|
7
|
+
| Attribute | On | Description |
|
|
8
|
+
|-----------|-----|-------------|
|
|
9
|
+
| `data-ln-upload="/files/upload"` | container | Upload URL (default: `/files/upload`) |
|
|
10
|
+
| `data-ln-upload-accept=".pdf,.doc,.docx"` | container | Allowed extensions (comma-separated) |
|
|
11
|
+
| `data-ln-upload-context="documents"` | container | Context string sent with upload (FormData `context` field) |
|
|
12
|
+
| `data-ln-upload-dict="key"` | hidden element | I18n dictionary for messages (see below) |
|
|
13
|
+
|
|
14
|
+
## Dictionary (i18n)
|
|
15
|
+
|
|
16
|
+
All keys are optional. If a key is missing, the component falls back to the English value shown below. Dict entries are read once at init via `buildDict()` and then removed from the DOM.
|
|
17
|
+
|
|
18
|
+
| Key | Used for | Fallback |
|
|
19
|
+
|-----|----------|----------|
|
|
20
|
+
| `remove` | Remove button aria-label and tooltip | `Remove` |
|
|
21
|
+
| `error` | Size slot text when upload fails | `Error` |
|
|
22
|
+
| `invalid-type` | Toast body — wrong extension | `This file type is not allowed` |
|
|
23
|
+
| `upload-failed` | Toast body — upload error | `Upload failed` |
|
|
24
|
+
| `delete-error` | Toast body — delete error | `Failed to delete file` |
|
|
25
|
+
| `network-error` | XHR network error + toast title for delete | `Network error` |
|
|
26
|
+
| `invalid-title` | Toast title — invalid file | `Invalid File` |
|
|
27
|
+
| `error-title` | Toast title — upload error | `Upload Error` |
|
|
28
|
+
| `delete-title` | Toast title — delete error | `Error` |
|
|
29
|
+
| `connection-error` | Toast body — delete fetch network failure | `Could not connect to server` |
|
|
30
|
+
|
|
31
|
+
Example (full override):
|
|
32
|
+
|
|
33
|
+
```html
|
|
34
|
+
<ul hidden>
|
|
35
|
+
<li data-ln-upload-dict="remove">Ukloni</li>
|
|
36
|
+
<li data-ln-upload-dict="error">Greška</li>
|
|
37
|
+
<li data-ln-upload-dict="invalid-type">Tip fajla nije dozvoljen</li>
|
|
38
|
+
<li data-ln-upload-dict="invalid-title">Neispravan fajl</li>
|
|
39
|
+
<!-- ... other keys as needed -->
|
|
40
|
+
</ul>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Customization — item template
|
|
44
|
+
|
|
45
|
+
The component clones a `<template data-ln-template="ln-upload-item">` for every file row. Lookup order:
|
|
46
|
+
|
|
47
|
+
1. **Scoped** — a `<template>` inside the `[data-ln-upload]` container (per-instance override)
|
|
48
|
+
2. **Global** — a `<template>` anywhere at document root
|
|
49
|
+
3. **Auto-injected default** — the component inserts a default template into `<body>` on first init if none is present, so zero-config usage keeps working
|
|
50
|
+
|
|
51
|
+
### Required slots
|
|
52
|
+
|
|
53
|
+
Your template MUST include these elements for the component to function:
|
|
54
|
+
|
|
55
|
+
| Element | Attribute | Purpose |
|
|
56
|
+
|---------|-----------|---------|
|
|
57
|
+
| `<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
|
+
| File name target | `data-ln-field="name"` | File name text |
|
|
59
|
+
| 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` / `#lnc-file-pdf` / `#lnc-file-doc` / `#lnc-file-epub` based on extension |
|
|
61
|
+
| Remove button | `data-ln-upload-action="remove"` and `data-ln-attr="aria-label:removeLabel, title:removeLabel"` | Click target (attribute-based, not class-based) |
|
|
62
|
+
| Progress bar | `class="ln-upload__progress-bar"` | Width is animated imperatively via inline style |
|
|
63
|
+
|
|
64
|
+
### Example — override with a two-line article layout
|
|
65
|
+
|
|
66
|
+
```html
|
|
67
|
+
<div data-ln-upload="/files/upload">
|
|
68
|
+
<template data-ln-template="ln-upload-item">
|
|
69
|
+
<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>
|
|
71
|
+
<article>
|
|
72
|
+
<span class="ln-upload__name" data-ln-field="name"></span>
|
|
73
|
+
<span class="ln-upload__size" data-ln-field="sizeText"></span>
|
|
74
|
+
</article>
|
|
75
|
+
<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>
|
|
77
|
+
</button>
|
|
78
|
+
<div class="ln-upload__progress"><div class="ln-upload__progress-bar"></div></div>
|
|
79
|
+
</li>
|
|
80
|
+
</template>
|
|
81
|
+
<div class="ln-upload__zone"><p>Drop files</p></div>
|
|
82
|
+
<ul class="ln-upload__list"></ul>
|
|
83
|
+
</div>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## API
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
// Instance API (on container element)
|
|
90
|
+
const uploader = document.getElementById('my-upload');
|
|
91
|
+
|
|
92
|
+
uploader.lnUploadAPI.getFileIds(); // [1, 2, 3] — server IDs
|
|
93
|
+
uploader.lnUploadAPI.getFiles(); // [{serverId, name, size}, ...]
|
|
94
|
+
uploader.lnUploadAPI.clear(); // Deletes everything (from server too)
|
|
95
|
+
uploader.lnUploadAPI.destroy(); // Cleanup — remove listeners and clear state
|
|
96
|
+
|
|
97
|
+
// Global API — only for non-standard cases (Shadow DOM, iframe)
|
|
98
|
+
// For AJAX/dynamic DOM or setAttribute: MutationObserver auto-initializes
|
|
99
|
+
window.lnUpload.init(containerElement); // Manual initialization
|
|
100
|
+
window.lnUpload.initAll(); // Initialize all
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Events
|
|
104
|
+
|
|
105
|
+
| Event | Bubbles | Detail |
|
|
106
|
+
|-------|---------|--------|
|
|
107
|
+
| `ln-upload:uploaded` | yes | `{ localId, serverId, name }` |
|
|
108
|
+
| `ln-upload:error` | yes | `{ file, message }` |
|
|
109
|
+
| `ln-upload:invalid` | yes | `{ file, message }` |
|
|
110
|
+
| `ln-upload:removed` | yes | `{ localId, serverId }` |
|
|
111
|
+
| `ln-upload:cleared` | yes | `{}` |
|
|
112
|
+
|
|
113
|
+
## Server API
|
|
114
|
+
|
|
115
|
+
### Upload (POST)
|
|
116
|
+
|
|
117
|
+
Request: `multipart/form-data` with `file` and `context` fields.
|
|
118
|
+
Headers: `X-CSRF-TOKEN`, `Accept: application/json`
|
|
119
|
+
|
|
120
|
+
Expected response:
|
|
121
|
+
```json
|
|
122
|
+
{ "id": 123, "name": "document.pdf", "size": 45678 }
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Delete (DELETE `/files/{id}`)
|
|
126
|
+
|
|
127
|
+
Headers: `X-CSRF-TOKEN`, `Accept: application/json`
|
|
128
|
+
Expected status: `200`
|
|
129
|
+
|
|
130
|
+
## HTML Structure
|
|
131
|
+
|
|
132
|
+
```html
|
|
133
|
+
<div data-ln-upload="/files/upload" data-ln-upload-accept=".pdf,.doc,.docx" data-ln-upload-context="documents">
|
|
134
|
+
<div class="ln-upload__zone">
|
|
135
|
+
<p>Drag files here or click to browse</p>
|
|
136
|
+
</div>
|
|
137
|
+
<ul class="ln-upload__list"></ul>
|
|
138
|
+
|
|
139
|
+
<!-- Dictionary (optional) — see "Dictionary (i18n)" section above -->
|
|
140
|
+
</div>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## File Icons
|
|
144
|
+
|
|
145
|
+
The component automatically adds an SVG icon per file type using the icon loader:
|
|
146
|
+
- `#lnc-file-pdf` — PDF (custom CDN)
|
|
147
|
+
- `#lnc-file-doc` — DOC/DOCX (custom CDN)
|
|
148
|
+
- `#lnc-file-epub` — EPUB (custom CDN)
|
|
149
|
+
- `#ln-file` — all other types (Tabler CDN)
|
|
150
|
+
|
|
151
|
+
Custom icons require `window.LN_ICONS_CUSTOM_CDN` to be set. See `js/ln-icons/README.md`.
|
|
152
|
+
|
|
153
|
+
## Hidden Inputs
|
|
154
|
+
|
|
155
|
+
After each successful upload, the component automatically creates `<input type="hidden" name="file_ids[]" value="serverId">` for each file. On form submit, the server receives the IDs directly.
|
|
156
|
+
|
|
157
|
+
## Integration & Source Files
|
|
158
|
+
|
|
159
|
+
### Loading the Component
|
|
160
|
+
|
|
161
|
+
The `ln-upload` component can be integrated into your project using one of the following methods:
|
|
162
|
+
|
|
163
|
+
#### 1. In-Bundle (Standard Integration)
|
|
164
|
+
Include the main unified `ln-ashlar` bundle to load all component observers together, which is standard for full application environments:
|
|
165
|
+
```html
|
|
166
|
+
<script src="dist/ln-ashlar.iife.js" defer></script>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
#### 2. Standalone (Zero-Dependency IIFE)
|
|
170
|
+
If you only need file upload capability without the rest of the bundle, load the standalone, self-registering IIFE version of the component directly:
|
|
171
|
+
```html
|
|
172
|
+
<script src="js/ln-upload/ln-upload.js" defer></script>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Source Files
|
|
176
|
+
|
|
177
|
+
For reference, active development, or customization, the component's codebase is structured into two main files:
|
|
178
|
+
|
|
179
|
+
* **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
|
+
* **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).
|
|
@@ -0,0 +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()})()})();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// ── JS state: upload display toggles ──
|
|
2
|
+
// Dictionary items hidden by JS
|
|
3
|
+
.ln-upload [data-ln-upload-dict] {
|
|
4
|
+
display: none;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// Progress bar visibility controlled by JS uploading state
|
|
8
|
+
.ln-upload__item--uploading .ln-upload__progress {
|
|
9
|
+
display: block;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.ln-upload__item:not(.ln-upload__item--uploading) .ln-upload__progress {
|
|
13
|
+
display: none;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Deleting state — JS disables interaction
|
|
17
|
+
.ln-upload__item--deleting {
|
|
18
|
+
opacity: 0.7;
|
|
19
|
+
pointer-events: none;
|
|
20
|
+
}
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { guardBody, dispatch, buildDict, cloneTemplateScoped, fill } from '../../ln-core';
|
|
2
|
+
|
|
3
|
+
(function () {
|
|
4
|
+
const DOM_SELECTOR = 'data-ln-upload';
|
|
5
|
+
const DOM_ATTRIBUTE = 'lnUpload';
|
|
6
|
+
const DICT_SELECTOR = 'data-ln-upload-dict';
|
|
7
|
+
const ACCEPT_ATTR = 'data-ln-upload-accept';
|
|
8
|
+
const CONTEXT_ATTR = 'data-ln-upload-context';
|
|
9
|
+
|
|
10
|
+
const DEFAULT_ITEM_TEMPLATE_HTML =
|
|
11
|
+
'<template data-ln-template="ln-upload-item">' +
|
|
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>' +
|
|
14
|
+
'<span class="ln-upload__name" data-ln-field="name"></span>' +
|
|
15
|
+
'<span class="ln-upload__size" data-ln-field="sizeText"></span>' +
|
|
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>' +
|
|
18
|
+
'</button>' +
|
|
19
|
+
'<div class="ln-upload__progress"><div class="ln-upload__progress-bar"></div></div>' +
|
|
20
|
+
'</li>' +
|
|
21
|
+
'</template>';
|
|
22
|
+
|
|
23
|
+
function _ensureDefaultItemTemplate() {
|
|
24
|
+
if (document.querySelector('[data-ln-template="ln-upload-item"]')) return;
|
|
25
|
+
if (!document.body) return;
|
|
26
|
+
const holder = document.createElement('div');
|
|
27
|
+
// Trust boundary: DEFAULT_ITEM_TEMPLATE_HTML is a hardcoded template string defined above
|
|
28
|
+
holder.innerHTML = DEFAULT_ITEM_TEMPLATE_HTML;
|
|
29
|
+
const tmpl = holder.firstElementChild;
|
|
30
|
+
if (tmpl) document.body.appendChild(tmpl);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (window[DOM_ATTRIBUTE] !== undefined) return;
|
|
34
|
+
|
|
35
|
+
function _formatSize(bytes) {
|
|
36
|
+
if (bytes === 0) return '0 B';
|
|
37
|
+
const k = 1024;
|
|
38
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
39
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
40
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function _getExtension(filename) {
|
|
44
|
+
return filename.split('.').pop().toLowerCase();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function _getIconId(extension) {
|
|
48
|
+
if (extension === 'docx') extension = 'doc';
|
|
49
|
+
const supported = ['pdf', 'doc', 'epub'];
|
|
50
|
+
return supported.includes(extension) ? 'lnc-file-' + extension : 'ln-file';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function _isValidFile(file, acceptString) {
|
|
54
|
+
if (!acceptString) return true;
|
|
55
|
+
const ext = '.' + _getExtension(file.name);
|
|
56
|
+
const allowed = acceptString.split(',').map(function (s) { return s.trim().toLowerCase(); });
|
|
57
|
+
return allowed.includes(ext.toLowerCase());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function _initUpload(container) {
|
|
61
|
+
if (container.hasAttribute('data-ln-upload-initialized')) return;
|
|
62
|
+
container.setAttribute('data-ln-upload-initialized', 'true');
|
|
63
|
+
|
|
64
|
+
_ensureDefaultItemTemplate();
|
|
65
|
+
const dict = buildDict(container, DICT_SELECTOR);
|
|
66
|
+
const zone = container.querySelector('.ln-upload__zone');
|
|
67
|
+
const list = container.querySelector('.ln-upload__list');
|
|
68
|
+
const acceptString = container.getAttribute(ACCEPT_ATTR) || '';
|
|
69
|
+
|
|
70
|
+
if (!zone || !list) {
|
|
71
|
+
console.warn('[ln-upload] Missing .ln-upload__zone or .ln-upload__list in container:', container);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let input = container.querySelector('input[type="file"]');
|
|
76
|
+
if (!input) {
|
|
77
|
+
input = document.createElement('input');
|
|
78
|
+
input.type = 'file';
|
|
79
|
+
input.multiple = true;
|
|
80
|
+
input.classList.add('hidden');
|
|
81
|
+
if (acceptString) {
|
|
82
|
+
input.accept = acceptString.split(',').map(function(ext) {
|
|
83
|
+
ext = ext.trim();
|
|
84
|
+
return ext.startsWith('.') ? ext : '.' + ext;
|
|
85
|
+
}).join(',');
|
|
86
|
+
}
|
|
87
|
+
container.appendChild(input);
|
|
88
|
+
}
|
|
89
|
+
const uploadUrl = container.getAttribute(DOM_SELECTOR) || '/files/upload';
|
|
90
|
+
const uploadContext = container.getAttribute(CONTEXT_ATTR) || '';
|
|
91
|
+
|
|
92
|
+
const uploadedFiles = new Map();
|
|
93
|
+
let fileIdCounter = 0;
|
|
94
|
+
|
|
95
|
+
function getCsrfToken() {
|
|
96
|
+
const meta = document.querySelector('meta[name="csrf-token"]');
|
|
97
|
+
return meta ? meta.getAttribute('content') : '';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function addFile(file) {
|
|
101
|
+
if (!_isValidFile(file, acceptString)) {
|
|
102
|
+
const message = dict['invalid-type'];
|
|
103
|
+
dispatch(container, 'ln-upload:invalid', {
|
|
104
|
+
file: file,
|
|
105
|
+
message: message
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
dispatch(window, 'ln-toast:enqueue', {
|
|
109
|
+
type: 'error',
|
|
110
|
+
title: dict['invalid-title'] || 'Invalid File',
|
|
111
|
+
message: message || dict['invalid-type'] || 'This file type is not allowed'
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const localId = 'file-' + (++fileIdCounter);
|
|
117
|
+
const ext = _getExtension(file.name);
|
|
118
|
+
const iconId = _getIconId(ext);
|
|
119
|
+
|
|
120
|
+
const fragment = cloneTemplateScoped(container, 'ln-upload-item', 'ln-upload');
|
|
121
|
+
if (!fragment) return;
|
|
122
|
+
const li = fragment.firstElementChild;
|
|
123
|
+
if (!li) return;
|
|
124
|
+
|
|
125
|
+
li.setAttribute('data-file-id', localId);
|
|
126
|
+
|
|
127
|
+
fill(li, {
|
|
128
|
+
name: file.name,
|
|
129
|
+
sizeText: '0%',
|
|
130
|
+
iconHref: '#' + iconId,
|
|
131
|
+
removeLabel: dict['remove'] || 'Remove',
|
|
132
|
+
uploading: true,
|
|
133
|
+
error: false,
|
|
134
|
+
deleting: false
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const progressBar = li.querySelector('.ln-upload__progress-bar');
|
|
138
|
+
const removeBtn = li.querySelector('[data-ln-upload-action="remove"]');
|
|
139
|
+
if (removeBtn) removeBtn.disabled = true;
|
|
140
|
+
|
|
141
|
+
list.appendChild(li);
|
|
142
|
+
|
|
143
|
+
const formData = new FormData();
|
|
144
|
+
formData.append('file', file);
|
|
145
|
+
formData.append('context', uploadContext);
|
|
146
|
+
|
|
147
|
+
const xhr = new XMLHttpRequest();
|
|
148
|
+
|
|
149
|
+
xhr.upload.addEventListener('progress', function (e) {
|
|
150
|
+
if (e.lengthComputable) {
|
|
151
|
+
const percent = Math.round((e.loaded / e.total) * 100);
|
|
152
|
+
progressBar.style.width = percent + '%';
|
|
153
|
+
fill(li, { sizeText: percent + '%' });
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
xhr.addEventListener('load', function () {
|
|
158
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
159
|
+
let data;
|
|
160
|
+
try {
|
|
161
|
+
data = JSON.parse(xhr.responseText);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
handleError('Invalid response');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
fill(li, { sizeText: _formatSize(data.size || file.size), uploading: false });
|
|
168
|
+
if (removeBtn) removeBtn.disabled = false;
|
|
169
|
+
|
|
170
|
+
uploadedFiles.set(localId, {
|
|
171
|
+
serverId: data.id,
|
|
172
|
+
name: data.name,
|
|
173
|
+
size: data.size
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
updateHiddenInput();
|
|
177
|
+
|
|
178
|
+
dispatch(container, 'ln-upload:uploaded', {
|
|
179
|
+
localId: localId,
|
|
180
|
+
serverId: data.id,
|
|
181
|
+
name: data.name
|
|
182
|
+
});
|
|
183
|
+
} else {
|
|
184
|
+
let message = dict['upload-failed'] || 'Upload failed';
|
|
185
|
+
try {
|
|
186
|
+
const errorData = JSON.parse(xhr.responseText);
|
|
187
|
+
message = errorData.message || message;
|
|
188
|
+
} catch (e) { }
|
|
189
|
+
handleError(message);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
xhr.addEventListener('error', function () {
|
|
194
|
+
handleError(dict['network-error'] || 'Network error');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
function handleError(message) {
|
|
198
|
+
if (progressBar) progressBar.style.width = '100%';
|
|
199
|
+
fill(li, { sizeText: dict['error'] || 'Error', uploading: false, error: true });
|
|
200
|
+
if (removeBtn) removeBtn.disabled = false;
|
|
201
|
+
|
|
202
|
+
dispatch(container, 'ln-upload:error', {
|
|
203
|
+
file: file,
|
|
204
|
+
message: message
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
dispatch(window, 'ln-toast:enqueue', {
|
|
208
|
+
type: 'error',
|
|
209
|
+
title: dict['error-title'] || 'Upload Error',
|
|
210
|
+
message: message || dict['upload-failed'] || 'Failed to upload file'
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
xhr.open('POST', uploadUrl);
|
|
215
|
+
xhr.setRequestHeader('X-CSRF-TOKEN', getCsrfToken());
|
|
216
|
+
xhr.setRequestHeader('Accept', 'application/json');
|
|
217
|
+
xhr.send(formData);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function updateHiddenInput() {
|
|
221
|
+
for (const el of container.querySelectorAll('input[name="file_ids[]"]')) {
|
|
222
|
+
el.remove();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
for (const [, fileData] of uploadedFiles) {
|
|
226
|
+
const hiddenInput = document.createElement('input');
|
|
227
|
+
hiddenInput.type = 'hidden';
|
|
228
|
+
hiddenInput.name = 'file_ids[]';
|
|
229
|
+
hiddenInput.value = fileData.serverId;
|
|
230
|
+
container.appendChild(hiddenInput);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function removeFile(localId) {
|
|
235
|
+
const fileData = uploadedFiles.get(localId);
|
|
236
|
+
const item = list.querySelector('[data-file-id="' + localId + '"]');
|
|
237
|
+
|
|
238
|
+
if (!fileData || !fileData.serverId) {
|
|
239
|
+
if (item) item.remove();
|
|
240
|
+
uploadedFiles.delete(localId);
|
|
241
|
+
updateHiddenInput();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (item) fill(item, { deleting: true });
|
|
246
|
+
|
|
247
|
+
fetch('/files/' + fileData.serverId, {
|
|
248
|
+
method: 'DELETE',
|
|
249
|
+
headers: {
|
|
250
|
+
'X-CSRF-TOKEN': getCsrfToken(),
|
|
251
|
+
'Accept': 'application/json'
|
|
252
|
+
}
|
|
253
|
+
})
|
|
254
|
+
.then(function (response) {
|
|
255
|
+
if (response.status === 200) {
|
|
256
|
+
if (item) item.remove();
|
|
257
|
+
uploadedFiles.delete(localId);
|
|
258
|
+
updateHiddenInput();
|
|
259
|
+
|
|
260
|
+
dispatch(container, 'ln-upload:removed', {
|
|
261
|
+
localId: localId,
|
|
262
|
+
serverId: fileData.serverId
|
|
263
|
+
});
|
|
264
|
+
} else {
|
|
265
|
+
if (item) fill(item, { deleting: false });
|
|
266
|
+
|
|
267
|
+
dispatch(window, 'ln-toast:enqueue', {
|
|
268
|
+
type: 'error',
|
|
269
|
+
title: dict['delete-title'] || 'Error',
|
|
270
|
+
message: dict['delete-error'] || 'Failed to delete file'
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
.catch(function (error) {
|
|
275
|
+
console.warn('[ln-upload] Delete error:', error);
|
|
276
|
+
if (item) fill(item, { deleting: false });
|
|
277
|
+
|
|
278
|
+
dispatch(window, 'ln-toast:enqueue', {
|
|
279
|
+
type: 'error',
|
|
280
|
+
title: dict['network-error'] || 'Network error',
|
|
281
|
+
message: dict['connection-error'] || 'Could not connect to server'
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function handleFiles(fileList) {
|
|
287
|
+
for (const file of fileList) {
|
|
288
|
+
addFile(file);
|
|
289
|
+
}
|
|
290
|
+
input.value = '';
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const _onZoneClick = function () { input.click(); };
|
|
294
|
+
const _onInputChange = function () { handleFiles(this.files); };
|
|
295
|
+
const _onDragEnter = function (e) { e.preventDefault(); e.stopPropagation(); zone.classList.add('ln-upload__zone--dragover'); };
|
|
296
|
+
const _onDragOver = function (e) { e.preventDefault(); e.stopPropagation(); zone.classList.add('ln-upload__zone--dragover'); };
|
|
297
|
+
const _onDragLeave = function (e) { e.preventDefault(); e.stopPropagation(); zone.classList.remove('ln-upload__zone--dragover'); };
|
|
298
|
+
const _onDrop = function (e) { e.preventDefault(); e.stopPropagation(); zone.classList.remove('ln-upload__zone--dragover'); handleFiles(e.dataTransfer.files); };
|
|
299
|
+
const _onListClick = function (e) {
|
|
300
|
+
const btn = e.target.closest('[data-ln-upload-action="remove"]');
|
|
301
|
+
if (!btn || !list.contains(btn)) return;
|
|
302
|
+
if (btn.disabled) return;
|
|
303
|
+
const item = btn.closest('.ln-upload__item');
|
|
304
|
+
if (item) removeFile(item.getAttribute('data-file-id'));
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
zone.addEventListener('click', _onZoneClick);
|
|
308
|
+
input.addEventListener('change', _onInputChange);
|
|
309
|
+
zone.addEventListener('dragenter', _onDragEnter);
|
|
310
|
+
zone.addEventListener('dragover', _onDragOver);
|
|
311
|
+
zone.addEventListener('dragleave', _onDragLeave);
|
|
312
|
+
zone.addEventListener('drop', _onDrop);
|
|
313
|
+
list.addEventListener('click', _onListClick);
|
|
314
|
+
|
|
315
|
+
container.lnUploadAPI = {
|
|
316
|
+
getFileIds: function () {
|
|
317
|
+
return Array.from(uploadedFiles.values()).map(function (f) { return f.serverId; });
|
|
318
|
+
},
|
|
319
|
+
getFiles: function () {
|
|
320
|
+
return Array.from(uploadedFiles.values());
|
|
321
|
+
},
|
|
322
|
+
clear: function () {
|
|
323
|
+
for (const [, fileData] of uploadedFiles) {
|
|
324
|
+
if (fileData.serverId) {
|
|
325
|
+
fetch('/files/' + fileData.serverId, {
|
|
326
|
+
method: 'DELETE',
|
|
327
|
+
headers: {
|
|
328
|
+
'X-CSRF-TOKEN': getCsrfToken(),
|
|
329
|
+
'Accept': 'application/json'
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
uploadedFiles.clear();
|
|
335
|
+
list.innerHTML = '';
|
|
336
|
+
updateHiddenInput();
|
|
337
|
+
dispatch(container, 'ln-upload:cleared', {});
|
|
338
|
+
},
|
|
339
|
+
destroy: function () {
|
|
340
|
+
zone.removeEventListener('click', _onZoneClick);
|
|
341
|
+
input.removeEventListener('change', _onInputChange);
|
|
342
|
+
zone.removeEventListener('dragenter', _onDragEnter);
|
|
343
|
+
zone.removeEventListener('dragover', _onDragOver);
|
|
344
|
+
zone.removeEventListener('dragleave', _onDragLeave);
|
|
345
|
+
zone.removeEventListener('drop', _onDrop);
|
|
346
|
+
list.removeEventListener('click', _onListClick);
|
|
347
|
+
uploadedFiles.clear();
|
|
348
|
+
list.innerHTML = '';
|
|
349
|
+
updateHiddenInput();
|
|
350
|
+
container.removeAttribute('data-ln-upload-initialized');
|
|
351
|
+
delete container.lnUploadAPI;
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function _initializeAll() {
|
|
357
|
+
for (const el of document.querySelectorAll('[' + DOM_SELECTOR + ']')) {
|
|
358
|
+
_initUpload(el);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function _domObserver() {
|
|
363
|
+
guardBody(function () {
|
|
364
|
+
const observer = new MutationObserver(function (mutations) {
|
|
365
|
+
for (const mutation of mutations) {
|
|
366
|
+
if (mutation.type === 'childList') {
|
|
367
|
+
for (const node of mutation.addedNodes) {
|
|
368
|
+
if (node.nodeType === 1) {
|
|
369
|
+
if (node.hasAttribute(DOM_SELECTOR)) {
|
|
370
|
+
_initUpload(node);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
for (const child of node.querySelectorAll('[' + DOM_SELECTOR + ']')) {
|
|
374
|
+
_initUpload(child);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
} else if (mutation.type === 'attributes') {
|
|
379
|
+
if (mutation.target.hasAttribute(DOM_SELECTOR)) {
|
|
380
|
+
_initUpload(mutation.target);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
observer.observe(document.body, {
|
|
387
|
+
childList: true,
|
|
388
|
+
subtree: true,
|
|
389
|
+
attributes: true,
|
|
390
|
+
attributeFilter: [DOM_SELECTOR]
|
|
391
|
+
});
|
|
392
|
+
}, 'ln-upload');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
window[DOM_ATTRIBUTE] = {
|
|
396
|
+
init: _initUpload,
|
|
397
|
+
initAll: _initializeAll
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
_domObserver();
|
|
401
|
+
|
|
402
|
+
if (document.readyState === 'loading') {
|
|
403
|
+
document.addEventListener('DOMContentLoaded', _initializeAll);
|
|
404
|
+
} else {
|
|
405
|
+
_initializeAll();
|
|
406
|
+
}
|
|
407
|
+
})();
|