@pure-ds/storybook 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.storybook/addons/description/preview.js +15 -0
- package/.storybook/addons/description/register.js +60 -0
- package/.storybook/addons/html-preview/Panel.jsx +327 -0
- package/.storybook/addons/html-preview/constants.js +6 -0
- package/.storybook/addons/html-preview/preview.js +178 -0
- package/.storybook/addons/html-preview/register.js +16 -0
- package/.storybook/addons/pds-configurator/SearchTool.js +44 -0
- package/.storybook/addons/pds-configurator/Tool.js +30 -0
- package/.storybook/addons/pds-configurator/constants.js +9 -0
- package/.storybook/addons/pds-configurator/preview.js +159 -0
- package/.storybook/addons/pds-configurator/register.js +24 -0
- package/.storybook/docs.css +35 -0
- package/.storybook/htmlPreview.css +103 -0
- package/.storybook/htmlPreview.js +271 -0
- package/.storybook/main.js +160 -0
- package/.storybook/preview-body.html +48 -0
- package/.storybook/preview-head.html +11 -0
- package/.storybook/preview.js +1563 -0
- package/README.md +266 -0
- package/bin/index.js +40 -0
- package/dist/pds-reference.json +2101 -0
- package/package.json +45 -0
- package/pds.config.js +6 -0
- package/public/assets/css/app.css +1216 -0
- package/public/assets/data/auto-design-advanced.json +704 -0
- package/public/assets/data/auto-design-simple.json +123 -0
- package/public/assets/img/icon-512x512.png +0 -0
- package/public/assets/img/logo-trans.png +0 -0
- package/public/assets/img/logo.png +0 -0
- package/public/assets/js/app.js +15088 -0
- package/public/assets/js/app.js.map +7 -0
- package/public/assets/js/lit.js +1176 -0
- package/public/assets/js/lit.js.map +7 -0
- package/public/assets/js/pds.js +9801 -0
- package/public/assets/js/pds.js.map +7 -0
- package/public/assets/pds/components/pds-calendar.js +837 -0
- package/public/assets/pds/components/pds-drawer.js +857 -0
- package/public/assets/pds/components/pds-icon.js +338 -0
- package/public/assets/pds/components/pds-jsonform.js +1775 -0
- package/public/assets/pds/components/pds-richtext.js +1035 -0
- package/public/assets/pds/components/pds-scrollrow.js +331 -0
- package/public/assets/pds/components/pds-splitpanel.js +401 -0
- package/public/assets/pds/components/pds-tabstrip.js +251 -0
- package/public/assets/pds/components/pds-toaster.js +446 -0
- package/public/assets/pds/components/pds-upload.js +657 -0
- package/public/assets/pds/custom-elements.json +2003 -0
- package/public/assets/pds/icons/pds-icons.svg +498 -0
- package/public/assets/pds/pds-css-complete.json +1861 -0
- package/public/assets/pds/pds-runtime-config.json +11 -0
- package/public/assets/pds/pds.css-data.json +2152 -0
- package/public/assets/pds/styles/pds-components.css +1944 -0
- package/public/assets/pds/styles/pds-components.css.js +3895 -0
- package/public/assets/pds/styles/pds-primitives.css +352 -0
- package/public/assets/pds/styles/pds-primitives.css.js +711 -0
- package/public/assets/pds/styles/pds-styles.css +3761 -0
- package/public/assets/pds/styles/pds-styles.css.js +7529 -0
- package/public/assets/pds/styles/pds-tokens.css +699 -0
- package/public/assets/pds/styles/pds-tokens.css.js +1405 -0
- package/public/assets/pds/styles/pds-utilities.css +763 -0
- package/public/assets/pds/styles/pds-utilities.css.js +1533 -0
- package/public/assets/pds/vscode-custom-data.json +824 -0
- package/scripts/build-pds-reference.mjs +807 -0
- package/scripts/generate-stories.js +542 -0
- package/scripts/package-build.js +86 -0
- package/src/js/app.js +17 -0
- package/src/js/common/ask.js +208 -0
- package/src/js/common/common.js +20 -0
- package/src/js/common/font-loader.js +200 -0
- package/src/js/common/msg.js +90 -0
- package/src/js/lit.js +40 -0
- package/src/js/pds-core/pds-config.js +1162 -0
- package/src/js/pds-core/pds-enhancer-metadata.js +75 -0
- package/src/js/pds-core/pds-enhancers.js +357 -0
- package/src/js/pds-core/pds-enums.js +86 -0
- package/src/js/pds-core/pds-generator.js +5317 -0
- package/src/js/pds-core/pds-ontology.js +256 -0
- package/src/js/pds-core/pds-paths.js +109 -0
- package/src/js/pds-core/pds-query.js +571 -0
- package/src/js/pds-core/pds-registry.js +129 -0
- package/src/js/pds-core/pds.d.ts +129 -0
- package/src/js/pds.d.ts +408 -0
- package/src/js/pds.js +1579 -0
- package/src/pds-core/pds-api.js +105 -0
- package/stories/GettingStarted.md +96 -0
- package/stories/GettingStarted.stories.js +144 -0
- package/stories/WhatIsPDS.md +194 -0
- package/stories/WhatIsPDS.stories.js +144 -0
- package/stories/components/PdsCalendar.stories.js +263 -0
- package/stories/components/PdsDrawer.stories.js +623 -0
- package/stories/components/PdsIcon.stories.js +78 -0
- package/stories/components/PdsJsonform.stories.js +1444 -0
- package/stories/components/PdsRichtext.stories.js +367 -0
- package/stories/components/PdsScrollrow.stories.js +140 -0
- package/stories/components/PdsSplitpanel.stories.js +502 -0
- package/stories/components/PdsTabstrip.stories.js +442 -0
- package/stories/components/PdsToaster.stories.js +186 -0
- package/stories/components/PdsUpload.stories.js +66 -0
- package/stories/enhancements/Dropdowns.stories.js +185 -0
- package/stories/enhancements/InteractiveStates.stories.js +625 -0
- package/stories/enhancements/MeshGradients.stories.js +320 -0
- package/stories/enhancements/OpenGroups.stories.js +227 -0
- package/stories/enhancements/RangeSliders.stories.js +232 -0
- package/stories/enhancements/RequiredFields.stories.js +189 -0
- package/stories/enhancements/Toggles.stories.js +167 -0
- package/stories/foundations/Colors.stories.js +283 -0
- package/stories/foundations/Icons.stories.js +305 -0
- package/stories/foundations/SmartSurfaces.stories.js +367 -0
- package/stories/foundations/Spacing.stories.js +175 -0
- package/stories/foundations/Typography.stories.js +960 -0
- package/stories/foundations/ZIndex.stories.js +325 -0
- package/stories/patterns/BorderEffects.stories.js +72 -0
- package/stories/patterns/Layout.stories.js +99 -0
- package/stories/patterns/Utilities.stories.js +107 -0
- package/stories/primitives/Accordion.stories.js +359 -0
- package/stories/primitives/Alerts.stories.js +64 -0
- package/stories/primitives/Badges.stories.js +183 -0
- package/stories/primitives/Buttons.stories.js +229 -0
- package/stories/primitives/Cards.stories.js +353 -0
- package/stories/primitives/FormGroups.stories.js +569 -0
- package/stories/primitives/Forms.stories.js +131 -0
- package/stories/primitives/Media.stories.js +203 -0
- package/stories/primitives/Tables.stories.js +232 -0
- package/stories/reference/ReferenceCatalog.stories.js +28 -0
- package/stories/reference/reference-catalog.js +413 -0
- package/stories/reference/reference-docs.js +302 -0
- package/stories/reference/reference-helpers.js +310 -0
- package/stories/utilities/GridSystem.stories.js +208 -0
- package/stories/utils/PdsAsk.stories.js +420 -0
- package/stories/utils/toast-utils.js +148 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drag-and-drop file uploader that participates in native forms.
|
|
3
|
+
*
|
|
4
|
+
* @element pds-upload
|
|
5
|
+
* @formAssociated
|
|
6
|
+
*
|
|
7
|
+
* @attr {string} accept - Comma separated list of accepted MIME types and file extensions
|
|
8
|
+
* @attr {boolean} multiple - Allows selecting more than one file
|
|
9
|
+
* @attr {boolean} disabled - Disables interaction with the drop zone and button
|
|
10
|
+
* @attr {number} max-files - Optional cap on the number of files the user may select
|
|
11
|
+
*/
|
|
12
|
+
class UploadArea extends HTMLElement {
|
|
13
|
+
static get observedAttributes() {
|
|
14
|
+
return ["accept", "multiple", "disabled", "max-files"];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Private fields
|
|
18
|
+
#root;
|
|
19
|
+
#files;
|
|
20
|
+
#objectUrls;
|
|
21
|
+
#container;
|
|
22
|
+
#btnSelect;
|
|
23
|
+
#tiles;
|
|
24
|
+
#input;
|
|
25
|
+
#internals;
|
|
26
|
+
|
|
27
|
+
constructor() {
|
|
28
|
+
super();
|
|
29
|
+
this.#root = this.attachShadow({ mode: "open" });
|
|
30
|
+
|
|
31
|
+
this.#files = []; // Array<File>
|
|
32
|
+
this.#objectUrls = new Map(); // File -> objectUrl
|
|
33
|
+
|
|
34
|
+
// Attach element internals for form participation
|
|
35
|
+
this.#internals = this.attachInternals();
|
|
36
|
+
|
|
37
|
+
// Render structure first
|
|
38
|
+
this.#renderStructure();
|
|
39
|
+
|
|
40
|
+
// Adopt primitives stylesheet asynchronously
|
|
41
|
+
this.#adoptStyles();
|
|
42
|
+
|
|
43
|
+
this.#container = this.#root.querySelector(".container");
|
|
44
|
+
this.#btnSelect = this.#root.querySelector(".btn-select");
|
|
45
|
+
this.#tiles = this.#root.querySelector(".tiles");
|
|
46
|
+
this.#input = this.#root.querySelector('input[type="file"]');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
#renderStructure() {
|
|
50
|
+
this.#root.innerHTML = `
|
|
51
|
+
<div class="container" role="button" tabindex="0" aria-disabled="false">
|
|
52
|
+
<div class="instructions">
|
|
53
|
+
<div class="headline">Drop files here or</div>
|
|
54
|
+
<div class="sub">Click to browse, or drag & drop files to upload.</div>
|
|
55
|
+
</div>
|
|
56
|
+
<button type="button" class="btn-select">Select</button>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div class="tiles" aria-live="polite" aria-relevant="additions removals"></div>
|
|
60
|
+
|
|
61
|
+
<input type="file" multiple />
|
|
62
|
+
`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async #adoptStyles() {
|
|
66
|
+
// Component-specific styles (button styles come from primitives)
|
|
67
|
+
const componentStyles = PDS.createStylesheet(/*css*/`
|
|
68
|
+
@layer upload {
|
|
69
|
+
:host {
|
|
70
|
+
display: block;
|
|
71
|
+
font-family: var(--font-family-body, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.container {
|
|
75
|
+
position: relative;
|
|
76
|
+
border: 2px dashed var(--color-border, #d1d5db);
|
|
77
|
+
border-radius: var(--radius-md, 8px);
|
|
78
|
+
padding: var(--spacing-3, 12px);
|
|
79
|
+
background: var(--color-input-bg);
|
|
80
|
+
display: flex;
|
|
81
|
+
gap: var(--spacing-3, 12px);
|
|
82
|
+
align-items: center;
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
transition: border-color .15s ease, background .15s ease, box-shadow .12s ease;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.container[aria-disabled="true"] {
|
|
88
|
+
opacity: 0.6;
|
|
89
|
+
cursor: not-allowed;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.container.dragover {
|
|
93
|
+
border-color: var(--color-primary-500);
|
|
94
|
+
box-shadow: 0 2px 8px var(--color-primary-500)20;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.instructions {
|
|
98
|
+
flex: 1;
|
|
99
|
+
min-width: 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.headline {
|
|
103
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
104
|
+
color: var(--color-text-primary);
|
|
105
|
+
margin: 0 0 var(--spacing-1, 6px) 0;
|
|
106
|
+
font-size: var(--font-size-sm, 14px);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.sub {
|
|
110
|
+
font-size: var(--font-size-xs, 13px);
|
|
111
|
+
color: var(--color-text-secondary);
|
|
112
|
+
margin: 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.tiles {
|
|
116
|
+
display: flex;
|
|
117
|
+
gap: var(--spacing-2, 8px);
|
|
118
|
+
flex-wrap: wrap;
|
|
119
|
+
margin-top: var(--spacing-3, 12px);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.tile {
|
|
123
|
+
display: inline-flex;
|
|
124
|
+
align-items: center;
|
|
125
|
+
gap: var(--spacing-2, 8px);
|
|
126
|
+
background: var(--color-surface-subtle);
|
|
127
|
+
border: 1px solid var(--color-border);
|
|
128
|
+
padding: var(--spacing-1, 6px);
|
|
129
|
+
border-radius: var(--radius-md, 6px);
|
|
130
|
+
min-width: 120px;
|
|
131
|
+
max-width: 220px;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.thumb {
|
|
135
|
+
width: 40px;
|
|
136
|
+
height: 40px;
|
|
137
|
+
flex: 0 0 40px;
|
|
138
|
+
border-radius: var(--radius-sm, 4px);
|
|
139
|
+
background: var(--color-surface-base, #fff);
|
|
140
|
+
border: 1px solid var(--color-border);
|
|
141
|
+
display: flex;
|
|
142
|
+
align-items: center;
|
|
143
|
+
justify-content: center;
|
|
144
|
+
overflow: hidden;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.thumb img {
|
|
148
|
+
width: 100%;
|
|
149
|
+
height: 100%;
|
|
150
|
+
object-fit: cover;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.meta {
|
|
154
|
+
flex: 1;
|
|
155
|
+
min-width: 0;
|
|
156
|
+
font-size: var(--font-size-xs, 12px);
|
|
157
|
+
color: var(--color-text-primary);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.meta .name {
|
|
161
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
162
|
+
white-space: nowrap;
|
|
163
|
+
overflow: hidden;
|
|
164
|
+
text-overflow: ellipsis;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.meta .size {
|
|
168
|
+
color: var(--color-text-secondary);
|
|
169
|
+
font-size: var(--font-size-xs, 11px);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.remove {
|
|
173
|
+
background: transparent;
|
|
174
|
+
border: none;
|
|
175
|
+
color: var(--color-danger-600);
|
|
176
|
+
cursor: pointer;
|
|
177
|
+
padding: var(--spacing-1, 4px);
|
|
178
|
+
font-size: var(--font-size-sm, 14px);
|
|
179
|
+
line-height: 1;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.remove:hover {
|
|
183
|
+
color: var(--color-danger-700);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
input[type="file"] {
|
|
187
|
+
display: none;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* Focus styles */
|
|
191
|
+
.container:focus {
|
|
192
|
+
outline: none;
|
|
193
|
+
box-shadow: 0 0 0 3px var(--color-primary-500)20;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
`);
|
|
197
|
+
|
|
198
|
+
// Adopt primitives (for button) + component styles
|
|
199
|
+
await PDS.adoptLayers(this.#root, ["primitives", "components", "utilities"], [componentStyles]);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Form-associated lifecycle callbacks
|
|
203
|
+
/**
|
|
204
|
+
* Invoked when the element becomes associated with a `<form>`.
|
|
205
|
+
*/
|
|
206
|
+
formAssociatedCallback() {
|
|
207
|
+
// Hook reserved for future enhancements (e.g., default validity state)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Sync disabled state across internal controls when the host form toggles.
|
|
212
|
+
* @param {boolean} disabled
|
|
213
|
+
*/
|
|
214
|
+
formDisabledCallback(disabled) {
|
|
215
|
+
this.#container.setAttribute("aria-disabled", disabled);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Clear selected files when the host form resets.
|
|
220
|
+
*/
|
|
221
|
+
formResetCallback() {
|
|
222
|
+
this.clear();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Restore previously submitted files during BFCache or session restores.
|
|
227
|
+
* @param {File[]} state
|
|
228
|
+
*/
|
|
229
|
+
formStateRestoreCallback(state) {
|
|
230
|
+
if (state) {
|
|
231
|
+
this.#files = state;
|
|
232
|
+
this.#updateTiles();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Custom methods for form participation
|
|
237
|
+
/**
|
|
238
|
+
* Comma separated list of file names for form serialisation.
|
|
239
|
+
* @returns {string}
|
|
240
|
+
*/
|
|
241
|
+
get value() {
|
|
242
|
+
return this.#files.map((file) => file.name).join(", ");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Value is derived from the selected files and cannot be set manually.
|
|
247
|
+
* @param {string} val
|
|
248
|
+
*/
|
|
249
|
+
set value(val) {
|
|
250
|
+
// No-op by design: value is derived from files
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Run constraint validation leveraging ElementInternals.
|
|
255
|
+
* @returns {boolean}
|
|
256
|
+
*/
|
|
257
|
+
checkValidity() {
|
|
258
|
+
return this.#internals.checkValidity();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Report validity issues using the browser UI.
|
|
263
|
+
* @returns {boolean}
|
|
264
|
+
*/
|
|
265
|
+
reportValidity() {
|
|
266
|
+
return this.#internals.reportValidity();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Clear all selected files and update the UI accordingly.
|
|
271
|
+
*/
|
|
272
|
+
clear() {
|
|
273
|
+
this.#files = [];
|
|
274
|
+
this.#updateTiles();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
#updateTiles() {
|
|
278
|
+
// Update the UI to reflect the current files
|
|
279
|
+
this.#tiles.innerHTML = "";
|
|
280
|
+
this.#files.forEach((file) => {
|
|
281
|
+
const tile = document.createElement("div");
|
|
282
|
+
tile.className = "tile";
|
|
283
|
+
tile.textContent = file.name;
|
|
284
|
+
this.#tiles.appendChild(tile);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Lifecycle hook used to upgrade properties and wire event listeners.
|
|
290
|
+
*/
|
|
291
|
+
connectedCallback() {
|
|
292
|
+
this.#upgradeProperty("accept");
|
|
293
|
+
this.#upgradeProperty("multiple");
|
|
294
|
+
this.#upgradeProperty("disabled");
|
|
295
|
+
this.#upgradeProperty("max-files");
|
|
296
|
+
|
|
297
|
+
this.#applyAttributesToInput();
|
|
298
|
+
|
|
299
|
+
this.#container.addEventListener("click", this.#onClick);
|
|
300
|
+
this.#container.addEventListener("keydown", this.#onKey);
|
|
301
|
+
this.#btnSelect.addEventListener("click", this.#onSelectClick);
|
|
302
|
+
this.#input.addEventListener("change", this.#onFileInput);
|
|
303
|
+
|
|
304
|
+
// Drag & drop
|
|
305
|
+
this.#container.addEventListener("dragover", this.#onDragOver);
|
|
306
|
+
this.#container.addEventListener("dragenter", this.#onDragEnter);
|
|
307
|
+
this.#container.addEventListener("dragleave", this.#onDragLeave);
|
|
308
|
+
this.#container.addEventListener("drop", this.#onDrop);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Clean up listeners and revoke object URLs when disconnected.
|
|
313
|
+
*/
|
|
314
|
+
disconnectedCallback() {
|
|
315
|
+
this.#container.removeEventListener("click", this.#onClick);
|
|
316
|
+
this.#container.removeEventListener("keydown", this.#onKey);
|
|
317
|
+
this.#btnSelect.removeEventListener("click", this.#onSelectClick);
|
|
318
|
+
this.#input.removeEventListener("change", this.#onFileInput);
|
|
319
|
+
|
|
320
|
+
this.#container.removeEventListener("dragover", this.#onDragOver);
|
|
321
|
+
this.#container.removeEventListener("dragenter", this.#onDragEnter);
|
|
322
|
+
this.#container.removeEventListener("dragleave", this.#onDragLeave);
|
|
323
|
+
this.#container.removeEventListener("drop", this.#onDrop);
|
|
324
|
+
|
|
325
|
+
this.#revokeAllObjectUrls();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Mirror attribute mutations onto the hidden file input.
|
|
330
|
+
* @param {string} name
|
|
331
|
+
* @param {string|null} oldVal
|
|
332
|
+
* @param {string|null} newVal
|
|
333
|
+
*/
|
|
334
|
+
attributeChangedCallback(name, oldVal, newVal) {
|
|
335
|
+
if (oldVal === newVal) return;
|
|
336
|
+
switch (name) {
|
|
337
|
+
case "accept":
|
|
338
|
+
case "multiple":
|
|
339
|
+
case "disabled":
|
|
340
|
+
case "max-files":
|
|
341
|
+
this.#applyAttributesToInput();
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Public API
|
|
347
|
+
/**
|
|
348
|
+
* Retrieve a shallow copy of the current file selection.
|
|
349
|
+
* @returns {File[]}
|
|
350
|
+
*/
|
|
351
|
+
getFiles() {
|
|
352
|
+
return Array.from(this.#files);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Remove all files and emit a change notification.
|
|
357
|
+
*/
|
|
358
|
+
clear() {
|
|
359
|
+
this.#files = [];
|
|
360
|
+
this.#updateInputFiles();
|
|
361
|
+
this.#renderTiles();
|
|
362
|
+
this.#emitFilesChanged();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Internals
|
|
366
|
+
#upgradeProperty(prop) {
|
|
367
|
+
if (this.hasOwnProperty(prop)) {
|
|
368
|
+
let value = this[prop];
|
|
369
|
+
delete this[prop];
|
|
370
|
+
this[prop] = value;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
#applyAttributesToInput() {
|
|
375
|
+
// accept
|
|
376
|
+
if (this.hasAttribute("accept")) {
|
|
377
|
+
this.#input.setAttribute("accept", this.getAttribute("accept"));
|
|
378
|
+
} else {
|
|
379
|
+
this.#input.removeAttribute("accept");
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// multiple
|
|
383
|
+
if (this.hasAttribute("multiple")) {
|
|
384
|
+
this.#input.setAttribute("multiple", "");
|
|
385
|
+
this.#container.setAttribute("aria-multiselectable", "true");
|
|
386
|
+
} else {
|
|
387
|
+
this.#input.removeAttribute("multiple");
|
|
388
|
+
this.#container.removeAttribute("aria-multiselectable");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// disabled
|
|
392
|
+
const disabled = this.hasAttribute("disabled");
|
|
393
|
+
this.#input.disabled = disabled;
|
|
394
|
+
this.#container.setAttribute("aria-disabled", disabled ? "true" : "false");
|
|
395
|
+
this.#btnSelect.disabled = disabled;
|
|
396
|
+
if (disabled) {
|
|
397
|
+
this.#container.tabIndex = -1;
|
|
398
|
+
} else {
|
|
399
|
+
this.#container.tabIndex = 0;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
#onClick = (e) => {
|
|
404
|
+
if (this.hasAttribute("disabled")) return;
|
|
405
|
+
// Ensure clicks on the whole container open file picker
|
|
406
|
+
if (e.target === this.#btnSelect) return; // handled separately
|
|
407
|
+
this.#input.click();
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
#onSelectClick = (e) => {
|
|
411
|
+
e.stopPropagation();
|
|
412
|
+
if (this.hasAttribute("disabled")) return;
|
|
413
|
+
this.#input.click();
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
#onKey = (e) => {
|
|
417
|
+
if (this.hasAttribute("disabled")) return;
|
|
418
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
419
|
+
e.preventDefault();
|
|
420
|
+
this.#input.click();
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
#onFileInput = (e) => {
|
|
425
|
+
if (!this.#input.files) return;
|
|
426
|
+
this.#addFiles(this.#input.files);
|
|
427
|
+
// reset input to allow selecting same file again
|
|
428
|
+
this.#input.value = "";
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
#onDragOver = (e) => {
|
|
432
|
+
e.preventDefault();
|
|
433
|
+
e.dataTransfer.dropEffect = "copy";
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
#onDragEnter = (e) => {
|
|
437
|
+
e.preventDefault();
|
|
438
|
+
if (this.hasAttribute("disabled")) return;
|
|
439
|
+
this.#container.classList.add("dragover");
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
#onDragLeave = (e) => {
|
|
443
|
+
// Only remove when leaving the container entirely
|
|
444
|
+
const rect = this.#container.getBoundingClientRect();
|
|
445
|
+
if (
|
|
446
|
+
e.clientX < rect.left ||
|
|
447
|
+
e.clientX > rect.right ||
|
|
448
|
+
e.clientY < rect.top ||
|
|
449
|
+
e.clientY > rect.bottom
|
|
450
|
+
) {
|
|
451
|
+
this.#container.classList.remove("dragover");
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
#onDrop = (e) => {
|
|
456
|
+
e.preventDefault();
|
|
457
|
+
this.#container.classList.remove("dragover");
|
|
458
|
+
if (this.hasAttribute("disabled")) return;
|
|
459
|
+
const dtFiles =
|
|
460
|
+
e.dataTransfer && e.dataTransfer.files ? e.dataTransfer.files : null;
|
|
461
|
+
if (dtFiles && dtFiles.length) {
|
|
462
|
+
this.#addFiles(dtFiles);
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
#addFiles(fileListLike) {
|
|
467
|
+
const incoming = Array.from(fileListLike);
|
|
468
|
+
|
|
469
|
+
// If accept is present, filter by accept patterns
|
|
470
|
+
const accept = this.getAttribute("accept");
|
|
471
|
+
const acceptList = accept
|
|
472
|
+
? accept
|
|
473
|
+
.split(",")
|
|
474
|
+
.map((s) => s.trim())
|
|
475
|
+
.filter(Boolean)
|
|
476
|
+
: null;
|
|
477
|
+
const acceptedFiles = acceptList
|
|
478
|
+
? incoming.filter((f) => this.#fileMatchesAccept(f, acceptList))
|
|
479
|
+
: incoming;
|
|
480
|
+
|
|
481
|
+
// max-files handling
|
|
482
|
+
const maxAttr = this.getAttribute("max-files");
|
|
483
|
+
let max = maxAttr ? parseInt(maxAttr, 10) : null;
|
|
484
|
+
if (Number.isNaN(max)) max = null;
|
|
485
|
+
|
|
486
|
+
if (this.hasAttribute("multiple")) {
|
|
487
|
+
// Append while respecting max
|
|
488
|
+
const available =
|
|
489
|
+
max != null ? Math.max(0, max - this.#files.length) : Infinity;
|
|
490
|
+
const toTake = acceptedFiles.slice(0, available);
|
|
491
|
+
this.#files = this.#files.concat(toTake);
|
|
492
|
+
} else {
|
|
493
|
+
// Only first accepted file
|
|
494
|
+
if (acceptedFiles.length > 0) {
|
|
495
|
+
this.#files = [acceptedFiles[0]];
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
this.#updateInputFiles();
|
|
500
|
+
this.#renderTiles();
|
|
501
|
+
this.#emitFilesChanged();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
#fileMatchesAccept(file, acceptList) {
|
|
505
|
+
// Accept patterns can be MIME types, wildcards like image/*, or extensions like .png
|
|
506
|
+
const name = (file.name || "").toLowerCase();
|
|
507
|
+
const type = (file.type || "").toLowerCase();
|
|
508
|
+
|
|
509
|
+
for (const pattern of acceptList) {
|
|
510
|
+
if (pattern.startsWith(".")) {
|
|
511
|
+
if (name.endsWith(pattern.toLowerCase())) return true;
|
|
512
|
+
} else if (pattern.endsWith("/*")) {
|
|
513
|
+
const base = pattern.slice(0, -2).toLowerCase();
|
|
514
|
+
if (type.startsWith(base + "/")) return true;
|
|
515
|
+
} else {
|
|
516
|
+
if (type === pattern.toLowerCase()) return true;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
#updateInputFiles() {
|
|
523
|
+
// Create a DataTransfer and assign files to hidden input for form compatibility
|
|
524
|
+
try {
|
|
525
|
+
const dt = new DataTransfer();
|
|
526
|
+
for (const f of this.#files) dt.items.add(f);
|
|
527
|
+
this.#input.files = dt.files;
|
|
528
|
+
} catch (err) {
|
|
529
|
+
// Some older browsers may not support DataTransfer() constructor.
|
|
530
|
+
// In that case, we can't sync input.files but component still works.
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
#renderTiles() {
|
|
535
|
+
// Clear tiles
|
|
536
|
+
this.#tiles.innerHTML = "";
|
|
537
|
+
this.#revokeUnusedObjectUrls();
|
|
538
|
+
|
|
539
|
+
this.#files.forEach((file, idx) => {
|
|
540
|
+
const tile = document.createElement("div");
|
|
541
|
+
tile.className = "tile";
|
|
542
|
+
tile.dataset.index = String(idx);
|
|
543
|
+
|
|
544
|
+
const thumb = document.createElement("div");
|
|
545
|
+
thumb.className = "thumb";
|
|
546
|
+
// If image, create preview
|
|
547
|
+
if (file.type && file.type.startsWith("image/")) {
|
|
548
|
+
const url = URL.createObjectURL(file);
|
|
549
|
+
this.#objectUrls.set(file, url);
|
|
550
|
+
const img = document.createElement("img");
|
|
551
|
+
img.src = url;
|
|
552
|
+
img.alt = file.name;
|
|
553
|
+
thumb.appendChild(img);
|
|
554
|
+
} else {
|
|
555
|
+
// non-image icon (simple file icon)
|
|
556
|
+
thumb.innerHTML = this.#fileIconSvg();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const meta = document.createElement("div");
|
|
560
|
+
meta.className = "meta";
|
|
561
|
+
const name = document.createElement("div");
|
|
562
|
+
name.className = "name";
|
|
563
|
+
name.textContent = file.name;
|
|
564
|
+
const size = document.createElement("div");
|
|
565
|
+
size.className = "size";
|
|
566
|
+
size.textContent = this.#formatBytes(file.size);
|
|
567
|
+
meta.appendChild(name);
|
|
568
|
+
meta.appendChild(size);
|
|
569
|
+
|
|
570
|
+
const remove = document.createElement("button");
|
|
571
|
+
remove.className = "remove";
|
|
572
|
+
remove.type = "button";
|
|
573
|
+
remove.title = `Remove ${file.name}`;
|
|
574
|
+
remove.setAttribute("aria-label", `Remove ${file.name}`);
|
|
575
|
+
remove.textContent = "✕";
|
|
576
|
+
remove.addEventListener("click", (e) => {
|
|
577
|
+
e.stopPropagation();
|
|
578
|
+
this.#removeFileAtIndex(idx);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
tile.appendChild(thumb);
|
|
582
|
+
tile.appendChild(meta);
|
|
583
|
+
tile.appendChild(remove);
|
|
584
|
+
|
|
585
|
+
this.#tiles.appendChild(tile);
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
#removeFileAtIndex(i) {
|
|
590
|
+
const file = this.#files[i];
|
|
591
|
+
if (!file) return;
|
|
592
|
+
this.#files.splice(i, 1);
|
|
593
|
+
this.#updateInputFiles();
|
|
594
|
+
this.#renderTiles();
|
|
595
|
+
this.#emitFilesChanged();
|
|
596
|
+
// revoke object url for removed file
|
|
597
|
+
if (this.#objectUrls.has(file)) {
|
|
598
|
+
URL.revokeObjectURL(this.#objectUrls.get(file));
|
|
599
|
+
this.#objectUrls.delete(file);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
#emitFilesChanged() {
|
|
604
|
+
const filesArray = this.getFiles();
|
|
605
|
+
this.dispatchEvent(
|
|
606
|
+
new CustomEvent("files-changed", {
|
|
607
|
+
detail: { files: filesArray },
|
|
608
|
+
bubbles: true,
|
|
609
|
+
composed: true,
|
|
610
|
+
})
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
#formatBytes(bytes) {
|
|
615
|
+
if (bytes === 0) return "0 B";
|
|
616
|
+
const k = 1024;
|
|
617
|
+
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
|
618
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
619
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
#fileIconSvg() {
|
|
623
|
+
// Minimal inline SVG for generic file icon
|
|
624
|
+
return `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">
|
|
625
|
+
<rect x="3" y="3" width="18" height="18" rx="2" fill="#fff" stroke="#e5e7eb"/>
|
|
626
|
+
<path d="M7 7h6v2H7zM7 11h10v2H7z" fill="#9ca3af"/>
|
|
627
|
+
</svg>`;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
#revokeAllObjectUrls() {
|
|
631
|
+
for (const url of this.#objectUrls.values()) {
|
|
632
|
+
try {
|
|
633
|
+
URL.revokeObjectURL(url);
|
|
634
|
+
} catch (_) {}
|
|
635
|
+
}
|
|
636
|
+
this.#objectUrls.clear();
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
#revokeUnusedObjectUrls() {
|
|
640
|
+
// Remove any object URLs for files no longer present
|
|
641
|
+
const currentFiles = new Set(this.#files);
|
|
642
|
+
for (const [file, url] of Array.from(this.#objectUrls.entries())) {
|
|
643
|
+
if (!currentFiles.has(file)) {
|
|
644
|
+
try {
|
|
645
|
+
URL.revokeObjectURL(url);
|
|
646
|
+
} catch (_) {}
|
|
647
|
+
this.#objectUrls.delete(file);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (!customElements.get("pds-upload")) {
|
|
654
|
+
customElements.define("pds-upload", UploadArea);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
export default UploadArea;
|