@unofficialbox/box-open-elements 0.17.0 → 0.18.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/dist/components/files/drop-zone.d.ts +26 -5
- package/dist/components/files/drop-zone.js +181 -22
- package/dist/foundations/tokens/box-defaults.js +36 -0
- package/dist/patterns/content-uploader/content-uploader.d.ts +33 -0
- package/dist/patterns/content-uploader/content-uploader.js +175 -16
- package/package.json +1 -1
|
@@ -5,8 +5,13 @@ export declare class DropZone extends BaseElement {
|
|
|
5
5
|
private dragging;
|
|
6
6
|
private zoneEl;
|
|
7
7
|
private inputEl;
|
|
8
|
+
private folderInputEl;
|
|
8
9
|
private labelEl;
|
|
9
10
|
private messageEl;
|
|
11
|
+
private browseEl;
|
|
12
|
+
private folderBrowseEl;
|
|
13
|
+
private separatorEl;
|
|
14
|
+
private illustrationEl;
|
|
10
15
|
get label(): string;
|
|
11
16
|
set label(value: string);
|
|
12
17
|
get message(): string;
|
|
@@ -14,15 +19,31 @@ export declare class DropZone extends BaseElement {
|
|
|
14
19
|
get description(): string;
|
|
15
20
|
set description(value: string);
|
|
16
21
|
/**
|
|
17
|
-
*
|
|
22
|
+
* Offer folder selection from the browse controls as well as files.
|
|
18
23
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
24
|
+
* A single `<input>` is either a file picker or a folder picker, never both —
|
|
25
|
+
* so this adds a *second* control backed by its own `webkitdirectory` input
|
|
26
|
+
* rather than converting the first, which is how box-ui-elements does it and
|
|
27
|
+
* means a person is never made to choose between the two.
|
|
28
|
+
*
|
|
29
|
+
* Drag-and-drop reads folders regardless; a dropped directory is always
|
|
30
|
+
* traversed whatever this is set to.
|
|
23
31
|
*/
|
|
24
32
|
get directories(): boolean;
|
|
25
33
|
set directories(value: boolean);
|
|
34
|
+
/** Text of the file-browse control. */
|
|
35
|
+
get browseLabel(): string;
|
|
36
|
+
set browseLabel(value: string);
|
|
37
|
+
/** Text of the folder-browse control, shown only when `directories` is set. */
|
|
38
|
+
get folderLabel(): string;
|
|
39
|
+
set folderLabel(value: string);
|
|
40
|
+
/**
|
|
41
|
+
* `"compact"` (default) keeps the small inline target; `"hero"` is the tall
|
|
42
|
+
* centred empty state, with room for an illustration in the `illustration`
|
|
43
|
+
* slot.
|
|
44
|
+
*/
|
|
45
|
+
get variant(): string;
|
|
46
|
+
set variant(value: string);
|
|
26
47
|
/**
|
|
27
48
|
* What the browse dialog offers, in the `<input accept>` syntax —
|
|
28
49
|
* `".pdf,.docx"` or `"image/*"`.
|
|
@@ -11,12 +11,19 @@ const dropZoneStyles = `
|
|
|
11
11
|
font: inherit;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/* Without this, the host's own display:block beats the UA rule for [hidden]
|
|
15
|
+
and the element stays visible when a host hides it. */
|
|
16
|
+
:host([hidden]) {
|
|
17
|
+
display: none;
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
[part="zone"] {
|
|
15
21
|
position: relative;
|
|
16
22
|
display: grid;
|
|
17
23
|
gap: 0.55rem;
|
|
18
24
|
justify-items: start;
|
|
19
25
|
padding: 0.75rem;
|
|
26
|
+
text-align: start;
|
|
20
27
|
border: 1.5px dashed color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 16%, var(--boe-token-stroke-stroke, #e8e8e8) 84%);
|
|
21
28
|
border-radius: ${boeRadius.large};
|
|
22
29
|
background: var(--boe-token-surface-surface-secondary, #fbfbfb);
|
|
@@ -49,7 +56,9 @@ const dropZoneStyles = `
|
|
|
49
56
|
0 14px 28px rgba(15, 23, 42, 0.05);
|
|
50
57
|
}
|
|
51
58
|
|
|
52
|
-
|
|
59
|
+
/* The inputs are never the focusable control — the browse buttons are — so
|
|
60
|
+
they are taken out of the tab order rather than merely hidden. */
|
|
61
|
+
[part~="file-input"] {
|
|
53
62
|
position: absolute;
|
|
54
63
|
inline-size: 1px;
|
|
55
64
|
block-size: 1px;
|
|
@@ -57,8 +66,6 @@ const dropZoneStyles = `
|
|
|
57
66
|
pointer-events: none;
|
|
58
67
|
}
|
|
59
68
|
|
|
60
|
-
${boeFocusVisibleStyles('[part="input"]')}
|
|
61
|
-
|
|
62
69
|
[part="label"] {
|
|
63
70
|
font-size: 1rem;
|
|
64
71
|
font-weight: 700;
|
|
@@ -70,17 +77,99 @@ const dropZoneStyles = `
|
|
|
70
77
|
line-height: 1.5;
|
|
71
78
|
max-width: 34ch;
|
|
72
79
|
}
|
|
80
|
+
|
|
81
|
+
[part="actions"] {
|
|
82
|
+
display: flex;
|
|
83
|
+
flex-wrap: wrap;
|
|
84
|
+
align-items: baseline;
|
|
85
|
+
gap: 0.25rem 0.5rem;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
[part~="browse"] {
|
|
89
|
+
appearance: none;
|
|
90
|
+
margin: 0;
|
|
91
|
+
padding: 0;
|
|
92
|
+
border: 0;
|
|
93
|
+
background: none;
|
|
94
|
+
font: inherit;
|
|
95
|
+
font-weight: 600;
|
|
96
|
+
color: var(--boe-token-surface-surface-brand, #0061d5);
|
|
97
|
+
text-decoration: none;
|
|
98
|
+
cursor: pointer;
|
|
99
|
+
border-radius: ${boeRadius.control};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
[part~="browse"]:hover {
|
|
103
|
+
text-decoration: underline;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
${boeFocusVisibleStyles('[part~="browse"]')}
|
|
107
|
+
|
|
108
|
+
[part="separator"] {
|
|
109
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* Hero: the full-height empty state, centred, with room for an illustration.
|
|
113
|
+
Matches the shape box-ui-elements gives its uploader. */
|
|
114
|
+
:host([variant="hero"]) [part="zone"] {
|
|
115
|
+
justify-items: center;
|
|
116
|
+
align-content: center;
|
|
117
|
+
gap: 0.35rem;
|
|
118
|
+
min-block-size: 18rem;
|
|
119
|
+
padding: 2rem 1.5rem;
|
|
120
|
+
text-align: center;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
:host([variant="hero"]) [part="label"] {
|
|
124
|
+
font-size: 1.0625rem;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
:host([variant="hero"]) [part~="description"] {
|
|
128
|
+
max-width: 42ch;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
[part="illustration"] {
|
|
132
|
+
display: none;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
:host([variant="hero"]) [part="illustration"] {
|
|
136
|
+
display: block;
|
|
137
|
+
/* The art carries its own padding inside the viewBox, so the box is set a
|
|
138
|
+
little larger than the 130px box-ui-elements uses for the drawn area. */
|
|
139
|
+
inline-size: min(10.5rem, 70%);
|
|
140
|
+
margin-block-end: 0.15rem;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
::slotted([slot="illustration"]) {
|
|
144
|
+
display: block;
|
|
145
|
+
inline-size: 100%;
|
|
146
|
+
block-size: auto;
|
|
147
|
+
}
|
|
73
148
|
`;
|
|
74
149
|
export class DropZone extends BaseElement {
|
|
75
150
|
static tagName = DEFAULT_TAG_NAME;
|
|
76
151
|
static get observedAttributes() {
|
|
77
|
-
return [
|
|
152
|
+
return [
|
|
153
|
+
"accept",
|
|
154
|
+
"browse-label",
|
|
155
|
+
"description",
|
|
156
|
+
"directories",
|
|
157
|
+
"folder-label",
|
|
158
|
+
"label",
|
|
159
|
+
"message",
|
|
160
|
+
"variant",
|
|
161
|
+
];
|
|
78
162
|
}
|
|
79
163
|
dragging = false;
|
|
80
164
|
zoneEl;
|
|
81
165
|
inputEl;
|
|
166
|
+
folderInputEl;
|
|
82
167
|
labelEl;
|
|
83
168
|
messageEl;
|
|
169
|
+
browseEl;
|
|
170
|
+
folderBrowseEl;
|
|
171
|
+
separatorEl;
|
|
172
|
+
illustrationEl;
|
|
84
173
|
get label() {
|
|
85
174
|
return this.getAttribute("label") ?? "Upload files";
|
|
86
175
|
}
|
|
@@ -100,12 +189,15 @@ export class DropZone extends BaseElement {
|
|
|
100
189
|
this.setAttribute("description", value);
|
|
101
190
|
}
|
|
102
191
|
/**
|
|
103
|
-
*
|
|
192
|
+
* Offer folder selection from the browse controls as well as files.
|
|
193
|
+
*
|
|
194
|
+
* A single `<input>` is either a file picker or a folder picker, never both —
|
|
195
|
+
* so this adds a *second* control backed by its own `webkitdirectory` input
|
|
196
|
+
* rather than converting the first, which is how box-ui-elements does it and
|
|
197
|
+
* means a person is never made to choose between the two.
|
|
104
198
|
*
|
|
105
|
-
* Drag-and-drop reads folders regardless
|
|
106
|
-
* traversed
|
|
107
|
-
* makes it either/or: an input is a file picker or a folder picker, never
|
|
108
|
-
* both.
|
|
199
|
+
* Drag-and-drop reads folders regardless; a dropped directory is always
|
|
200
|
+
* traversed whatever this is set to.
|
|
109
201
|
*/
|
|
110
202
|
get directories() {
|
|
111
203
|
return this.hasAttribute("directories");
|
|
@@ -113,6 +205,31 @@ export class DropZone extends BaseElement {
|
|
|
113
205
|
set directories(value) {
|
|
114
206
|
this.toggleAttribute("directories", value);
|
|
115
207
|
}
|
|
208
|
+
/** Text of the file-browse control. */
|
|
209
|
+
get browseLabel() {
|
|
210
|
+
return this.getAttribute("browse-label") ?? "Browse your device";
|
|
211
|
+
}
|
|
212
|
+
set browseLabel(value) {
|
|
213
|
+
this.setAttribute("browse-label", value);
|
|
214
|
+
}
|
|
215
|
+
/** Text of the folder-browse control, shown only when `directories` is set. */
|
|
216
|
+
get folderLabel() {
|
|
217
|
+
return this.getAttribute("folder-label") ?? "Select folders";
|
|
218
|
+
}
|
|
219
|
+
set folderLabel(value) {
|
|
220
|
+
this.setAttribute("folder-label", value);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* `"compact"` (default) keeps the small inline target; `"hero"` is the tall
|
|
224
|
+
* centred empty state, with room for an illustration in the `illustration`
|
|
225
|
+
* slot.
|
|
226
|
+
*/
|
|
227
|
+
get variant() {
|
|
228
|
+
return this.getAttribute("variant") === "hero" ? "hero" : "compact";
|
|
229
|
+
}
|
|
230
|
+
set variant(value) {
|
|
231
|
+
this.setAttribute("variant", value);
|
|
232
|
+
}
|
|
116
233
|
/**
|
|
117
234
|
* What the browse dialog offers, in the `<input accept>` syntax —
|
|
118
235
|
* `".pdf,.docx"` or `"image/*"`.
|
|
@@ -135,16 +252,31 @@ export class DropZone extends BaseElement {
|
|
|
135
252
|
if (!this.shadowRoot) {
|
|
136
253
|
return;
|
|
137
254
|
}
|
|
255
|
+
// A div rather than a label wrapping the input: the browse controls are
|
|
256
|
+
// real buttons, and a button inside a label is both ambiguous to assistive
|
|
257
|
+
// technology and prone to firing the picker twice.
|
|
138
258
|
this.shadowRoot.innerHTML = `
|
|
139
259
|
<style>${dropZoneStyles}</style>
|
|
140
|
-
<
|
|
141
|
-
<input type="file" part="input" multiple />
|
|
260
|
+
<div part="zone">
|
|
261
|
+
<input type="file" part="input file-input" multiple tabindex="-1" aria-hidden="true" />
|
|
262
|
+
<input type="file" part="folder-input file-input" multiple tabindex="-1" aria-hidden="true" webkitdirectory />
|
|
263
|
+
<span part="illustration"><slot name="illustration"></slot></span>
|
|
142
264
|
<strong part="label"></strong>
|
|
143
265
|
<span part="description message"></span>
|
|
144
|
-
|
|
266
|
+
<span part="actions">
|
|
267
|
+
<button type="button" part="browse browse-files"></button>
|
|
268
|
+
<span part="separator" hidden>or</span>
|
|
269
|
+
<button type="button" part="browse browse-folders" hidden></button>
|
|
270
|
+
</span>
|
|
271
|
+
</div>
|
|
145
272
|
`;
|
|
146
273
|
this.zoneEl = this.shadowRoot.querySelector('[part="zone"]');
|
|
147
|
-
this.inputEl = this.shadowRoot.querySelector('[part
|
|
274
|
+
this.inputEl = this.shadowRoot.querySelector('[part~="input"]');
|
|
275
|
+
this.folderInputEl = this.shadowRoot.querySelector('[part~="folder-input"]');
|
|
276
|
+
this.browseEl = this.shadowRoot.querySelector('[part~="browse-files"]');
|
|
277
|
+
this.folderBrowseEl = this.shadowRoot.querySelector('[part~="browse-folders"]');
|
|
278
|
+
this.separatorEl = this.shadowRoot.querySelector('[part="separator"]');
|
|
279
|
+
this.illustrationEl = this.shadowRoot.querySelector('[part="illustration"]');
|
|
148
280
|
this.labelEl = this.shadowRoot.querySelector('[part="label"]');
|
|
149
281
|
this.messageEl = this.shadowRoot.querySelector('[part~="description"]');
|
|
150
282
|
}
|
|
@@ -177,11 +309,30 @@ export class DropZone extends BaseElement {
|
|
|
177
309
|
this.emitSelection(entries, skipped);
|
|
178
310
|
});
|
|
179
311
|
});
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
312
|
+
// Clicking the body of the zone still opens the file picker, as it did when
|
|
313
|
+
// the zone was a label. Clicks that land on a button are left alone, or the
|
|
314
|
+
// picker would be asked to open twice.
|
|
315
|
+
this.zoneEl.addEventListener("click", event => {
|
|
316
|
+
if (event.target.closest('[part~="browse"]')) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
this.inputEl.click();
|
|
320
|
+
});
|
|
321
|
+
this.browseEl.addEventListener("click", () => {
|
|
322
|
+
this.inputEl.click();
|
|
323
|
+
});
|
|
324
|
+
this.folderBrowseEl.addEventListener("click", () => {
|
|
325
|
+
this.folderInputEl.click();
|
|
184
326
|
});
|
|
327
|
+
for (const input of [this.inputEl, this.folderInputEl]) {
|
|
328
|
+
input.addEventListener("change", () => {
|
|
329
|
+
// A folder picked through `webkitdirectory` reports its tree through
|
|
330
|
+
// each file's webkitRelativePath rather than through entries.
|
|
331
|
+
this.emitSelection(entriesFromFileList(Array.from(input.files ?? [])));
|
|
332
|
+
// Cleared so choosing the same folder twice in a row still fires.
|
|
333
|
+
input.value = "";
|
|
334
|
+
});
|
|
335
|
+
}
|
|
185
336
|
}
|
|
186
337
|
/**
|
|
187
338
|
* Report a selection.
|
|
@@ -213,13 +364,21 @@ export class DropZone extends BaseElement {
|
|
|
213
364
|
this.zoneEl.dataset.dragging = String(this.dragging);
|
|
214
365
|
this.labelEl.textContent = this.label;
|
|
215
366
|
this.messageEl.textContent = this.message;
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
367
|
+
// Hidden rather than left empty, so it does not open a gap in the stack.
|
|
368
|
+
this.messageEl.hidden = this.message.trim() === "";
|
|
369
|
+
this.browseEl.textContent = this.browseLabel;
|
|
370
|
+
this.folderBrowseEl.textContent = this.folderLabel;
|
|
371
|
+
// The folder control is a second input rather than a mode on the first, so
|
|
372
|
+
// files and folders are both reachable instead of mutually exclusive.
|
|
219
373
|
const directories = this.directories;
|
|
220
|
-
this.
|
|
221
|
-
this.
|
|
374
|
+
this.folderBrowseEl.hidden = !directories;
|
|
375
|
+
this.separatorEl.hidden = !directories;
|
|
376
|
+
// webkitdirectory is a property as well as an attribute, and only the
|
|
377
|
+
// property is honoured reliably once the input exists.
|
|
378
|
+
this.folderInputEl.webkitdirectory = true;
|
|
379
|
+
// A folder picker offers folders, so an extension filter would only mislead.
|
|
222
380
|
this.inputEl.accept = this.accept;
|
|
381
|
+
this.illustrationEl.hidden = this.variant !== "hero";
|
|
223
382
|
}
|
|
224
383
|
}
|
|
225
384
|
DropZone.register();
|
|
@@ -46,6 +46,42 @@ export const boxDefaultDesignSystem = {
|
|
|
46
46
|
},
|
|
47
47
|
illustrations: {
|
|
48
48
|
"empty-state-folder": illustrationSvg("0 0 140 140", '<g fill="var(--boe-token-surface-illustration-surface-box-neutral, #0061d5)" fill-rule="evenodd" clip-rule="evenodd" opacity="0.1"><path d="M47.11 36H35.383c-1.261 0-2.294.97-2.378 2.198L33 38.36v48.604c0 2.162 1.716 3.926 3.872 4.031l.546.005h.257l.19-.036c1.743-.375 3.062-1.86 3.152-3.663l.005-.197V73.972a.98.98 0 0 1 .017-.18.87.87 0 0 1-.032-.155L41 73.52V56.717c0-4.209 3.7-7.59 8.268-7.713l.26-.004L97 48.999v-2.26c0-3.08-2.463-5.592-5.546-5.707l-.221-.004H54.118a4.402 4.402 0 0 1-3.51-1.738l-.14-.197-1.375-2.042a2.386 2.386 0 0 0-1.8-1.044L47.11 36Z" /><path d="M51 96a7 7 0 1 1 0 14 7 7 0 0 1 0-14Zm47-66c5.523 0 10 4.477 10 10s-4.477 10-10 10-10-4.477-10-10 4.477-10 10-10Z" /></g><path fill="var(--boe-token-surface-illustration-surface-box-neutral, #0061d5)" fill-rule="evenodd" d="M95 90c0-7.18-5.82-13-13-13s-13 5.82-13 13 5.82 13 13 13 13-5.82 13-13Zm-12.268-6.723a1.03 1.03 0 0 0-1.269-.107l-.105.079-.061.057-.057.061-4.99 5.898-.074.098a1.06 1.06 0 0 0 .187 1.384l.098.076c.17.115.371.177.578.177H79l.001 4.921c0 .596.497 1.08 1.11 1.08h3.779c.614 0 1.11-.484 1.11-1.081V91h1.96l.115-.006c.52-.057.925-.503.925-1.045 0-.249-.087-.489-.246-.679l-4.932-5.898-.09-.095Z" clip-rule="evenodd" /><path fill="var(--boe-token-surface-illustration-surface-box-neutral, #0061d5)" d="M44.156 39c.466 0 .844.448.844 1s-.378 1-.844 1h-6.312c-.466 0-.844-.448-.844-1s.378-1 .844-1h6.312Zm41.952 21c.493 0 .892.448.892 1s-.4 1-.892 1H61.892c-.493 0-.892-.448-.892-1s.4-1 .892-1h24.216Z" /><path fill="var(--boe-token-surface-illustration-surface-box-neutral, #0061d5)" fill-rule="evenodd" d="M99 91a6 6 0 0 0 5.996-5.775L105 85l.005-28.283c0-2.983-2.745-5.672-6.005-5.708l-49.473-.019c-3.55 0-6.403 2.476-6.528 5.518l-.004.209c0 10.13.005 20.255.005 30.387a5.817 5.817 0 0 1-1.497 3.897L65 91a1 1 0 0 1 0 2H37c-3.238 0-5.878-2.521-5.996-5.675L31 87.104V38.347c0-2.33 1.85-4.232 4.174-4.342l.212-.005h11.727a4.4 4.4 0 0 1 3.509 1.739l.14.197 1.376 2.043c.407.607 1.073.99 1.799 1.045l.183.007h37.11c4.208 0 7.636 3.316 7.766 7.456l.004.244v2.284c4.358.24 7.863 3.449 7.996 7.464l.004.238V85a8 8 0 0 1-7.75 7.996L99 93a1 1 0 0 1 0-2ZM35.383 36H47.11l.183.007a2.386 2.386 0 0 1 1.8 1.044l1.375 2.042.14.197a4.402 4.402 0 0 0 3.51 1.738h37.115l.221.004c3.083.115 5.546 2.627 5.546 5.707v2.26L49.528 49l-.26.004C44.7 49.127 41 52.508 41 56.717c0 10.129.022 20.258.022 30.387l-.005.197c-.09 1.803-1.409 3.288-3.152 3.663l-.19.036h-.257l-.546-.005C34.716 90.89 33 89.126 33 86.964V38.36l.005-.162A2.374 2.374 0 0 1 35.383 36Z" clip-rule="evenodd" />'),
|
|
49
|
+
// Fanned file cards behind a cloud carrying an upload arrow — the empty
|
|
50
|
+
// state for box-content-uploader, matching what box-ui-elements shows.
|
|
51
|
+
// Cards are stroked rather than filled so they read on a dark surface too.
|
|
52
|
+
"upload-cloud": illustrationSvg("0 0 140 140", [
|
|
53
|
+
'<ellipse cx="70" cy="116" rx="27" ry="3.5" fill="var(--boe-token-surface-illustration-surface-box-neutral, #0061d5)" opacity="0.1" />',
|
|
54
|
+
'<g fill="var(--boe-token-surface-surface, #ffffff)" stroke="var(--boe-token-surface-illustration-surface-box-neutral, #0061d5)" stroke-width="2" stroke-linejoin="round">',
|
|
55
|
+
// Five cards fanned across an arc. Each is drawn at the origin and
|
|
56
|
+
// placed by transform, so the shape is written once.
|
|
57
|
+
[
|
|
58
|
+
{ x: 44, y: 72, rotate: -26, glyph: "grid" },
|
|
59
|
+
{ x: 57, y: 65, rotate: -13, glyph: "lines" },
|
|
60
|
+
{ x: 70, y: 62, rotate: 0, glyph: "lines" },
|
|
61
|
+
{ x: 83, y: 65, rotate: 13, glyph: "image" },
|
|
62
|
+
{ x: 96, y: 72, rotate: 26, glyph: "grid" },
|
|
63
|
+
]
|
|
64
|
+
.map(card => {
|
|
65
|
+
const glyphs = {
|
|
66
|
+
lines: '<rect x="5" y="13" width="16" height="2" rx="1" /><rect x="5" y="18" width="16" height="2" rx="1" /><rect x="5" y="23" width="10" height="2" rx="1" />',
|
|
67
|
+
image: '<circle cx="9" cy="15" r="2" /><path d="M5 25l5-6 4 5 3-3 4 5v1H5z" />',
|
|
68
|
+
grid: '<rect x="5" y="13" width="7" height="6" rx="1" /><rect x="14" y="13" width="7" height="6" rx="1" /><rect x="5" y="21" width="7" height="6" rx="1" /><rect x="14" y="21" width="7" height="6" rx="1" />',
|
|
69
|
+
};
|
|
70
|
+
return [
|
|
71
|
+
`<g transform="translate(${String(card.x)} ${String(card.y)}) rotate(${String(card.rotate)}) translate(-13 -17)">`,
|
|
72
|
+
'<path d="M0 4a4 4 0 0 1 4-4h14l8 8v22a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4Z" />',
|
|
73
|
+
'<path d="M18 0v6a2 2 0 0 0 2 2h6" fill="none" />',
|
|
74
|
+
`<g fill="var(--boe-token-surface-illustration-surface-box-neutral, #0061d5)" stroke="none" opacity="0.45">${glyphs[card.glyph] ?? ""}</g>`,
|
|
75
|
+
"</g>",
|
|
76
|
+
].join("");
|
|
77
|
+
})
|
|
78
|
+
.join(""),
|
|
79
|
+
"</g>",
|
|
80
|
+
// The cloud sits in front of the cards, so only their tops show.
|
|
81
|
+
'<path d="M52 108A14 14 0 0 1 52 80A20 20 0 0 1 88 80A14 14 0 0 1 88 108Z" fill="var(--boe-token-surface-surface, #ffffff)" stroke="var(--boe-token-surface-illustration-surface-box-neutral, #0061d5)" stroke-width="2" stroke-linejoin="round" />',
|
|
82
|
+
'<circle cx="70" cy="89" r="11.5" fill="var(--boe-token-surface-illustration-surface-box-neutral, #0061d5)" fill-opacity="0.12" stroke="var(--boe-token-surface-illustration-surface-box-neutral, #0061d5)" stroke-width="1.75" stroke-dasharray="2 3.5" stroke-linecap="round" />',
|
|
83
|
+
'<path d="M70 83l5 5h-3v5h-4v-5h-3z" fill="var(--boe-token-surface-illustration-surface-box-neutral, #0061d5)" />',
|
|
84
|
+
].join("")),
|
|
49
85
|
"files-information": illustrationSvg("0 0 140 140", '<path fill="var(--boe-token-surface-illustration-surface-box-neutral, #0061d5)" fill-opacity="0.1" d="M97.5 99a2.5 2.5 0 0 1 2.5 2.5v7a2.5 2.5 0 1 1-5 0v-7a2.5 2.5 0 0 1 2.5-2.5ZM97.5 91a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5ZM74 78a2 2 0 1 1 0 4H57a2 2 0 1 1 0-4h17ZM90.01 67a1.99 1.99 0 0 1 0 3.978H56.99a1.99 1.99 0 0 1 0-3.978h33.02ZM90 56a2 2 0 1 1 0 4H57a2 2 0 1 1 0-4h33ZM80.058 27c.61 0 1.202.206 1.671.595C83.773 29.286 87.784 32.783 92 37c4.641 4.641 8.41 8.877 9.859 10.81a.73.73 0 0 1-1.105.944L98.89 46.89a3.037 3.037 0 0 0-2.148-.89h-9.264A4.479 4.479 0 0 1 83 41.522v-8.743a4.479 4.479 0 0 0-1.22-3.073L79.226 27h.83Z" /><path fill="var(--boe-token-surface-illustration-surface-box-neutral, #0061d5)" fill-opacity="0.25" d="M45 91.001a8 8 0 0 0 8 8h28.068c-.044.494-.068.994-.068 1.499a16.42 16.42 0 0 0 2.966 9.439A7.896 7.896 0 0 1 83 110H43c-5.523 0-10-4.477-10-10V47a7 7 0 0 1 7-7h5v51.001Z" /><path fill="var(--boe-token-surface-illustration-surface-box-neutral, #0061d5)" fill-rule="evenodd" d="M97.5 98.25a3.25 3.25 0 0 1 3.25 3.25v7a3.25 3.25 0 1 1-6.5 0v-7a3.25 3.25 0 0 1 3.25-3.25Zm0 1.5a1.75 1.75 0 0 0-1.75 1.75v7a1.75 1.75 0 0 0 3.5 0v-7a1.75 1.75 0 0 0-1.75-1.75ZM97.5 90.25a3.25 3.25 0 1 1 0 6.5 3.25 3.25 0 0 1 0-6.5Zm0 1.5a1.75 1.75 0 1 0 0 3.5 1.75 1.75 0 0 0 0-3.5Z" clip-rule="evenodd" /><path fill="var(--boe-token-surface-illustration-surface-box-neutral, #0061d5)" fill-rule="evenodd" d="M79.1 26.25a7.75 7.75 0 0 1 5.481 2.27l15.899 15.899a7.749 7.749 0 0 1 2.27 5.48v34.165c6.96 2.222 12 8.74 12 16.436 0 9.527-7.723 17.25-17.25 17.25-5.743 0-10.83-2.807-13.965-7.123-.5.08-1.013.123-1.535.123H42c-5.385 0-9.75-4.365-9.75-9.75V47A7.75 7.75 0 0 1 40 39.25h4.25V34A7.75 7.75 0 0 1 52 26.25h27.1Zm18.4 58.5c-8.698 0-15.75 7.052-15.75 15.75s7.052 15.75 15.75 15.75 15.75-7.052 15.75-15.75-7.052-15.75-15.75-15.75Zm-57.5-44A6.25 6.25 0 0 0 33.75 47v54a8.25 8.25 0 0 0 8.25 8.25h40c.207 0 .413-.011.616-.026a17.172 17.172 0 0 1-2.348-9.473H52a7.75 7.75 0 0 1-7.75-7.75V40.75H40Zm12-13A6.25 6.25 0 0 0 45.75 34v58.001a6.25 6.25 0 0 0 6.25 6.25h28.395c1.103-8.464 8.34-15.001 17.105-15.001 1.288 0 2.543.141 3.75.41V51.112c0-.514-.204-1.007-.567-1.37l-1.902-1.901a3.73 3.73 0 0 0-2.637-1.092h-8.666a5.229 5.229 0 0 1-5.228-5.228v-8.743c0-.951-.364-1.866-1.017-2.558l-1.756-1.864a1.938 1.938 0 0 0-1.41-.607H52Zm30.325 1.441a5.23 5.23 0 0 1 1.425 3.588v8.742c0 2.06 1.67 3.729 3.728 3.729h8.666a5.23 5.23 0 0 1 3.698 1.531l1.056 1.057a6.241 6.241 0 0 0-1.48-2.358L83.52 29.581a6.252 6.252 0 0 0-2.15-1.403l.956 1.013Z" clip-rule="evenodd" />'),
|
|
50
86
|
},
|
|
51
87
|
};
|
|
@@ -14,6 +14,13 @@ export declare class ContentUploader extends BaseElement {
|
|
|
14
14
|
private summaryEl;
|
|
15
15
|
private queueSignature;
|
|
16
16
|
private updateScheduled;
|
|
17
|
+
private closeEl;
|
|
18
|
+
private cancelEl;
|
|
19
|
+
private uploadEl;
|
|
20
|
+
private clearCompletedEl;
|
|
21
|
+
private illustrationEl;
|
|
22
|
+
private illustrationSignature;
|
|
23
|
+
private readonly handleDesignSystemChange;
|
|
17
24
|
get folderId(): string | null;
|
|
18
25
|
set folderId(value: string | null);
|
|
19
26
|
get token(): string | null;
|
|
@@ -50,8 +57,24 @@ export declare class ContentUploader extends BaseElement {
|
|
|
50
57
|
*/
|
|
51
58
|
get directories(): boolean;
|
|
52
59
|
set directories(value: boolean);
|
|
60
|
+
/**
|
|
61
|
+
* Show the Close control. On by default, matching box-ui-elements; a host
|
|
62
|
+
* that owns its own dismissal (a drawer with its own close button) sets
|
|
63
|
+
* `closable="false"` rather than styling ours away.
|
|
64
|
+
*/
|
|
65
|
+
get closable(): boolean;
|
|
66
|
+
set closable(value: boolean);
|
|
67
|
+
/**
|
|
68
|
+
* Headline of the empty state. Defaults to the box-ui-elements wording, and
|
|
69
|
+
* names folders too when they can be uploaded, so the invitation matches what
|
|
70
|
+
* the zone will actually accept.
|
|
71
|
+
*/
|
|
53
72
|
get dropLabel(): string;
|
|
54
73
|
set dropLabel(value: string);
|
|
74
|
+
/**
|
|
75
|
+
* Optional supporting line under the headline. Empty by default: the browse
|
|
76
|
+
* controls say what to do, and a sentence repeating them is noise.
|
|
77
|
+
*/
|
|
55
78
|
get dropMessage(): string;
|
|
56
79
|
set dropMessage(value: string);
|
|
57
80
|
get transport(): UploadTransport | null;
|
|
@@ -98,7 +121,17 @@ export declare class ContentUploader extends BaseElement {
|
|
|
98
121
|
private scheduleUpdate;
|
|
99
122
|
private teardownController;
|
|
100
123
|
protected renderTemplate(): void;
|
|
124
|
+
/**
|
|
125
|
+
* Paints the empty-state art from the active design system, so a host that
|
|
126
|
+
* registers its own system gets its own illustration without touching this.
|
|
127
|
+
*
|
|
128
|
+
* Resolved during update rather than at template time: a host may register or
|
|
129
|
+
* switch design systems after the element upgrades, and the art has to follow.
|
|
130
|
+
*/
|
|
131
|
+
private renderIllustration;
|
|
101
132
|
protected setupListeners(): void;
|
|
133
|
+
/** Cancel every item still queued or in flight. Settled items are untouched. */
|
|
134
|
+
cancelAll(): void;
|
|
102
135
|
private rebuildQueue;
|
|
103
136
|
private patchQueueProgress;
|
|
104
137
|
protected update(): void;
|
|
@@ -4,6 +4,7 @@ import { formatItemSize } from "../content-explorer/adapters/item-summary.js";
|
|
|
4
4
|
import { DropZone } from "../../components/files/drop-zone.js";
|
|
5
5
|
import { ProgressBar } from "../../components/feedback/progress-bar.js";
|
|
6
6
|
import { BaseElement } from "../../core/index.js";
|
|
7
|
+
import { DESIGN_SYSTEM_CHANGE_EVENT, resolveDesignIllustration, } from "../../foundations/tokens/index.js";
|
|
7
8
|
import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
|
|
8
9
|
import { boePanel, boeRadius } from "../../foundations/geometry/index.js";
|
|
9
10
|
const DEFAULT_TAG_NAME = "box-content-uploader";
|
|
@@ -171,10 +172,19 @@ const elementStyles = `
|
|
|
171
172
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 18%, transparent);
|
|
172
173
|
}
|
|
173
174
|
|
|
175
|
+
/* The footer is a bar rather than a caption: Close sits apart on the
|
|
176
|
+
left, the queue-level actions group on the right, as they do in
|
|
177
|
+
box-ui-elements. */
|
|
178
|
+
/* Wraps rather than squeezes: in a narrow column the summary would
|
|
179
|
+
otherwise concertina onto three lines and the button labels break
|
|
180
|
+
mid-phrase. Wrapped, the action group drops to its own row intact. */
|
|
174
181
|
[part="footer"] {
|
|
175
182
|
display: flex;
|
|
183
|
+
flex-wrap: wrap;
|
|
176
184
|
align-items: center;
|
|
177
|
-
gap: ${boePanel.gap};
|
|
185
|
+
gap: 0.5rem ${boePanel.gap};
|
|
186
|
+
padding-block-start: ${boePanel.gap};
|
|
187
|
+
border-block-start: 1px solid color-mix(in srgb, var(--boe-token-stroke-stroke, #e8e8e8) 82%, transparent);
|
|
178
188
|
}
|
|
179
189
|
|
|
180
190
|
[part="footer"][hidden] {
|
|
@@ -182,39 +192,81 @@ const elementStyles = `
|
|
|
182
192
|
}
|
|
183
193
|
|
|
184
194
|
[part="summary"] {
|
|
185
|
-
|
|
195
|
+
/* Takes the slack but never forces a wrap of its own: it is a status
|
|
196
|
+
line, not a reason for the buttons to be squeezed. */
|
|
197
|
+
flex: 1 1 auto;
|
|
198
|
+
min-inline-size: 0;
|
|
186
199
|
font-size: 0.85rem;
|
|
187
200
|
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
188
201
|
}
|
|
189
202
|
|
|
190
|
-
[part="
|
|
203
|
+
[part="footer-actions"] {
|
|
204
|
+
display: flex;
|
|
205
|
+
flex-wrap: wrap;
|
|
206
|
+
align-items: center;
|
|
207
|
+
justify-content: flex-end;
|
|
208
|
+
/* Keeps the group right-aligned once the footer wraps it onto its
|
|
209
|
+
own row. */
|
|
210
|
+
margin-inline-start: auto;
|
|
211
|
+
gap: 0.5rem;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
[part~="action"] {
|
|
191
215
|
appearance: none;
|
|
192
216
|
font: inherit;
|
|
193
217
|
font-size: 0.875rem;
|
|
194
218
|
font-weight: 600;
|
|
195
|
-
padding: 0.
|
|
219
|
+
padding: 0.5rem 0.9rem;
|
|
196
220
|
border-radius: ${boeRadius.control};
|
|
197
221
|
border: 1px solid var(--boe-token-stroke-stroke, #e8e8e8);
|
|
198
222
|
background: var(--boe-token-surface-surface, #ffffff);
|
|
199
223
|
color: var(--boe-token-text-text, #222222);
|
|
200
224
|
cursor: pointer;
|
|
225
|
+
/* A label that breaks mid-phrase reads as two controls. */
|
|
226
|
+
white-space: nowrap;
|
|
227
|
+
transition:
|
|
228
|
+
background ${boeMotionDuration.interactive} ${boeMotionEasing.standard},
|
|
229
|
+
border-color ${boeMotionDuration.interactive} ${boeMotionEasing.standard};
|
|
201
230
|
}
|
|
202
231
|
|
|
203
|
-
[part
|
|
232
|
+
[part~="action"]:hover:not(:disabled) {
|
|
204
233
|
background: var(--boe-token-surface-surface-hover, #f4f4f4);
|
|
205
234
|
border-color: var(--boe-token-stroke-stroke-hover, #bcbcbc);
|
|
206
235
|
}
|
|
207
236
|
|
|
208
|
-
[part
|
|
237
|
+
[part~="action"]:focus-visible {
|
|
209
238
|
outline: none;
|
|
210
239
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 18%, transparent);
|
|
211
240
|
}
|
|
241
|
+
|
|
242
|
+
/* Disabled, not removed: the control keeps its place so the footer does
|
|
243
|
+
not reflow as the queue changes, and stays discoverable. */
|
|
244
|
+
[part~="action"]:disabled {
|
|
245
|
+
cursor: not-allowed;
|
|
246
|
+
opacity: 0.5;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
[part~="action-primary"] {
|
|
250
|
+
border-color: transparent;
|
|
251
|
+
background: var(--boe-token-surface-surface-brand, #0061d5);
|
|
252
|
+
color: var(--boe-token-text-text-on-color, #ffffff);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
[part~="action-primary"]:hover:not(:disabled) {
|
|
256
|
+
background: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 86%, black 14%);
|
|
257
|
+
border-color: transparent;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
[part="close"][hidden] {
|
|
261
|
+
display: none;
|
|
262
|
+
}
|
|
212
263
|
`;
|
|
213
264
|
export class ContentUploader extends BaseElement {
|
|
214
265
|
static tagName = DEFAULT_TAG_NAME;
|
|
215
266
|
static get observedAttributes() {
|
|
216
267
|
return [
|
|
217
268
|
"auto-start",
|
|
269
|
+
"closable",
|
|
218
270
|
"concurrency",
|
|
219
271
|
"drop-label",
|
|
220
272
|
"drop-message",
|
|
@@ -237,6 +289,17 @@ export class ContentUploader extends BaseElement {
|
|
|
237
289
|
summaryEl;
|
|
238
290
|
queueSignature = "";
|
|
239
291
|
updateScheduled = false;
|
|
292
|
+
closeEl;
|
|
293
|
+
cancelEl;
|
|
294
|
+
uploadEl;
|
|
295
|
+
clearCompletedEl;
|
|
296
|
+
illustrationEl;
|
|
297
|
+
illustrationSignature = "";
|
|
298
|
+
handleDesignSystemChange = () => {
|
|
299
|
+
if (this.isRendered) {
|
|
300
|
+
this.renderIllustration();
|
|
301
|
+
}
|
|
302
|
+
};
|
|
240
303
|
get folderId() {
|
|
241
304
|
return this.getAttribute("folder-id");
|
|
242
305
|
}
|
|
@@ -326,14 +389,39 @@ export class ContentUploader extends BaseElement {
|
|
|
326
389
|
set directories(value) {
|
|
327
390
|
this.toggleAttribute("directories", value);
|
|
328
391
|
}
|
|
392
|
+
/**
|
|
393
|
+
* Show the Close control. On by default, matching box-ui-elements; a host
|
|
394
|
+
* that owns its own dismissal (a drawer with its own close button) sets
|
|
395
|
+
* `closable="false"` rather than styling ours away.
|
|
396
|
+
*/
|
|
397
|
+
get closable() {
|
|
398
|
+
return this.getAttribute("closable") !== "false";
|
|
399
|
+
}
|
|
400
|
+
set closable(value) {
|
|
401
|
+
if (value) {
|
|
402
|
+
this.removeAttribute("closable");
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
this.setAttribute("closable", "false");
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Headline of the empty state. Defaults to the box-ui-elements wording, and
|
|
409
|
+
* names folders too when they can be uploaded, so the invitation matches what
|
|
410
|
+
* the zone will actually accept.
|
|
411
|
+
*/
|
|
329
412
|
get dropLabel() {
|
|
330
|
-
return this.getAttribute("drop-label") ??
|
|
413
|
+
return (this.getAttribute("drop-label") ??
|
|
414
|
+
(this.directories ? "Drag and drop files and folders" : "Drag and drop files"));
|
|
331
415
|
}
|
|
332
416
|
set dropLabel(value) {
|
|
333
417
|
this.setAttribute("drop-label", value);
|
|
334
418
|
}
|
|
419
|
+
/**
|
|
420
|
+
* Optional supporting line under the headline. Empty by default: the browse
|
|
421
|
+
* controls say what to do, and a sentence repeating them is noise.
|
|
422
|
+
*/
|
|
335
423
|
get dropMessage() {
|
|
336
|
-
return this.getAttribute("drop-message") ?? "
|
|
424
|
+
return this.getAttribute("drop-message") ?? "";
|
|
337
425
|
}
|
|
338
426
|
set dropMessage(value) {
|
|
339
427
|
this.setAttribute("drop-message", value);
|
|
@@ -357,6 +445,7 @@ export class ContentUploader extends BaseElement {
|
|
|
357
445
|
* else feeds the session config, so changing it re-creates the queue.
|
|
358
446
|
*/
|
|
359
447
|
static PRESENTATION_ATTRIBUTES = new Set([
|
|
448
|
+
"closable",
|
|
360
449
|
"directories",
|
|
361
450
|
"drop-label",
|
|
362
451
|
"drop-message",
|
|
@@ -368,10 +457,12 @@ export class ContentUploader extends BaseElement {
|
|
|
368
457
|
super.attributeChangedCallback(name, oldValue, newValue);
|
|
369
458
|
}
|
|
370
459
|
connectedCallback() {
|
|
460
|
+
globalThis.addEventListener?.(DESIGN_SYSTEM_CHANGE_EVENT, this.handleDesignSystemChange);
|
|
371
461
|
super.connectedCallback();
|
|
372
462
|
this.scheduleStart();
|
|
373
463
|
}
|
|
374
464
|
disconnectedCallback() {
|
|
465
|
+
globalThis.removeEventListener?.(DESIGN_SYSTEM_CHANGE_EVENT, this.handleDesignSystemChange);
|
|
375
466
|
this.teardownController();
|
|
376
467
|
}
|
|
377
468
|
addFiles(files) {
|
|
@@ -511,11 +602,18 @@ export class ContentUploader extends BaseElement {
|
|
|
511
602
|
this.shadowRoot.innerHTML = `
|
|
512
603
|
<style>${elementStyles}</style>
|
|
513
604
|
<div part="panel">
|
|
514
|
-
<box-drop-zone part="drop-zone"
|
|
605
|
+
<box-drop-zone part="drop-zone" variant="hero">
|
|
606
|
+
<span slot="illustration" part="drop-illustration"></span>
|
|
607
|
+
</box-drop-zone>
|
|
515
608
|
<ul part="queue" aria-label="Upload queue"></ul>
|
|
516
|
-
<footer part="footer"
|
|
609
|
+
<footer part="footer">
|
|
610
|
+
<button type="button" part="action close">Close</button>
|
|
517
611
|
<span part="summary" role="status" aria-live="polite"></span>
|
|
518
|
-
<
|
|
612
|
+
<span part="footer-actions">
|
|
613
|
+
<button type="button" part="action clear-completed">Clear completed</button>
|
|
614
|
+
<button type="button" part="action cancel">Cancel</button>
|
|
615
|
+
<button type="button" part="action action-primary upload">Upload</button>
|
|
616
|
+
</span>
|
|
519
617
|
</footer>
|
|
520
618
|
</div>
|
|
521
619
|
`;
|
|
@@ -523,6 +621,25 @@ export class ContentUploader extends BaseElement {
|
|
|
523
621
|
this.queueEl = this.shadowRoot.querySelector('[part="queue"]');
|
|
524
622
|
this.footerEl = this.shadowRoot.querySelector('[part="footer"]');
|
|
525
623
|
this.summaryEl = this.shadowRoot.querySelector('[part="summary"]');
|
|
624
|
+
this.closeEl = this.shadowRoot.querySelector('[part~="close"]');
|
|
625
|
+
this.cancelEl = this.shadowRoot.querySelector('[part~="cancel"]');
|
|
626
|
+
this.uploadEl = this.shadowRoot.querySelector('[part~="upload"]');
|
|
627
|
+
this.clearCompletedEl = this.shadowRoot.querySelector('[part~="clear-completed"]');
|
|
628
|
+
this.illustrationEl = this.shadowRoot.querySelector('[part="drop-illustration"]');
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Paints the empty-state art from the active design system, so a host that
|
|
632
|
+
* registers its own system gets its own illustration without touching this.
|
|
633
|
+
*
|
|
634
|
+
* Resolved during update rather than at template time: a host may register or
|
|
635
|
+
* switch design systems after the element upgrades, and the art has to follow.
|
|
636
|
+
*/
|
|
637
|
+
renderIllustration() {
|
|
638
|
+
const markup = resolveDesignIllustration("upload-cloud") ?? "";
|
|
639
|
+
if (markup !== this.illustrationSignature) {
|
|
640
|
+
this.illustrationSignature = markup;
|
|
641
|
+
this.illustrationEl.innerHTML = markup;
|
|
642
|
+
}
|
|
526
643
|
}
|
|
527
644
|
setupListeners() {
|
|
528
645
|
this.dropZoneEl.addEventListener("files-selected", event => {
|
|
@@ -554,9 +671,33 @@ export class ContentUploader extends BaseElement {
|
|
|
554
671
|
this.removeItem(itemId);
|
|
555
672
|
}
|
|
556
673
|
});
|
|
557
|
-
this.
|
|
674
|
+
this.clearCompletedEl.addEventListener("click", () => {
|
|
558
675
|
this.clearCompleted();
|
|
559
676
|
});
|
|
677
|
+
this.uploadEl.addEventListener("click", () => {
|
|
678
|
+
this.start();
|
|
679
|
+
});
|
|
680
|
+
this.cancelEl.addEventListener("click", () => {
|
|
681
|
+
this.cancelAll();
|
|
682
|
+
});
|
|
683
|
+
// The uploader does not own the surface it sits in — a dialog, a drawer, a
|
|
684
|
+
// page — so Close reports the intent and the host decides what closing
|
|
685
|
+
// means. Cancellable, so a host can refuse.
|
|
686
|
+
this.closeEl.addEventListener("click", () => {
|
|
687
|
+
this.dispatchEvent(new CustomEvent("close", { bubbles: true, composed: true, cancelable: true }));
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
/** Cancel every item still queued or in flight. Settled items are untouched. */
|
|
691
|
+
cancelAll() {
|
|
692
|
+
const controller = this.controller;
|
|
693
|
+
if (!controller) {
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
for (const item of controller.getState().items) {
|
|
697
|
+
if (item.status === "queued" || item.status === "uploading") {
|
|
698
|
+
controller.cancelItem(item.id);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
560
701
|
}
|
|
561
702
|
rebuildQueue(items) {
|
|
562
703
|
this.queueEl.innerHTML = items
|
|
@@ -605,6 +746,7 @@ export class ContentUploader extends BaseElement {
|
|
|
605
746
|
if (!this.queueEl) {
|
|
606
747
|
return;
|
|
607
748
|
}
|
|
749
|
+
this.renderIllustration();
|
|
608
750
|
this.dropZoneEl.label = this.dropLabel;
|
|
609
751
|
this.dropZoneEl.message = this.dropMessage;
|
|
610
752
|
this.dropZoneEl.directories = this.directories;
|
|
@@ -625,12 +767,29 @@ export class ContentUploader extends BaseElement {
|
|
|
625
767
|
}
|
|
626
768
|
this.patchQueueProgress(items);
|
|
627
769
|
const summary = summarizeUploadQueue(items);
|
|
628
|
-
this.footerEl.hidden = summary.total === 0;
|
|
629
770
|
const active = summary.queued + summary.uploading;
|
|
771
|
+
// The empty state and the queue are alternatives, not neighbours: the
|
|
772
|
+
// illustration is the invitation, and once files are in the queue the list
|
|
773
|
+
// is what a person needs to see. Dropping still works over the list.
|
|
774
|
+
this.dropZoneEl.hidden = summary.total > 0;
|
|
775
|
+
// The footer stays put with its controls disabled rather than appearing
|
|
776
|
+
// when the first file lands — a bar that materialises under the pointer is
|
|
777
|
+
// how people click the wrong thing.
|
|
778
|
+
this.uploadEl.disabled = summary.queued === 0;
|
|
779
|
+
this.cancelEl.disabled = active === 0;
|
|
780
|
+
this.clearCompletedEl.disabled = summary.succeeded + summary.cancelled === 0;
|
|
781
|
+
// Closing mid-upload would abandon transfers in flight, so Close waits.
|
|
782
|
+
// box-ui-elements disables it for any non-empty queue; that would trap a
|
|
783
|
+
// person on a finished queue with no way out, so it is only held while
|
|
784
|
+
// something is actually running.
|
|
785
|
+
this.closeEl.disabled = active > 0;
|
|
786
|
+
this.closeEl.hidden = !this.closable;
|
|
630
787
|
this.summaryEl.textContent =
|
|
631
|
-
|
|
632
|
-
?
|
|
633
|
-
:
|
|
788
|
+
summary.total === 0
|
|
789
|
+
? ""
|
|
790
|
+
: active > 0
|
|
791
|
+
? `Uploading ${String(summary.uploading)} · ${String(summary.queued)} queued`
|
|
792
|
+
: `${String(summary.succeeded)} of ${String(summary.total)} uploaded${summary.failed ? ` · ${String(summary.failed)} failed` : ""}`;
|
|
634
793
|
}
|
|
635
794
|
}
|
|
636
795
|
DropZone.register();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unofficialbox/box-open-elements",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Box Open Elements: an open, framework-agnostic design system and web component library for Box-style experiences",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://unofficialbox.github.io/box-open-elements",
|