@unofficialbox/box-open-elements 0.16.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 +55 -0
- package/dist/components/files/drop-zone.js +242 -21
- package/dist/foundations/files/directory-entries.d.ts +69 -0
- package/dist/foundations/files/directory-entries.js +149 -0
- package/dist/foundations/files/index.d.ts +1 -0
- package/dist/foundations/files/index.js +3 -0
- package/dist/foundations/tokens/box-defaults.js +36 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/patterns/content-uploader/content-uploader.d.ts +76 -0
- package/dist/patterns/content-uploader/content-uploader.js +260 -22
- package/dist/patterns/content-uploader/controller.d.ts +37 -0
- package/dist/patterns/content-uploader/controller.js +111 -2
- package/dist/patterns/content-uploader/types.d.ts +51 -1
- package/package.json +1 -1
|
@@ -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
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export type { ApplyDesignProfileOptions, DesignProfileChangeDetail, DesignProfil
|
|
|
10
10
|
export { boxIconography, boxIconographyAliases } from "./foundations/icons/index.js";
|
|
11
11
|
export * from "./foundations/layout/index.js";
|
|
12
12
|
export * from "./foundations/format/index.js";
|
|
13
|
+
export * from "./foundations/files/index.js";
|
|
13
14
|
export { anchorFloating, parsePlacement, resolvePosition, trackAnchor, } from "./foundations/overlay/index.js";
|
|
14
15
|
export type { OverlayAlign, OverlayPlacement, OverlaySide, PositionOptions, PositionResult, } from "./foundations/overlay/index.js";
|
|
15
16
|
export * from "./components/actions/button.js";
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,9 @@ export { boxIconography, boxIconographyAliases } from "./foundations/icons/index
|
|
|
9
9
|
export * from "./foundations/layout/index.js";
|
|
10
10
|
// Foundations: locale-aware typed-value formatting (shared by box-formatted-*)
|
|
11
11
|
export * from "./foundations/format/index.js";
|
|
12
|
+
// Foundations: reading dropped files and folders (shared by box-drop-zone and
|
|
13
|
+
// the content-uploader pattern)
|
|
14
|
+
export * from "./foundations/files/index.js";
|
|
12
15
|
// Foundations: viewport-aware overlay positioning (for building custom overlays)
|
|
13
16
|
export { anchorFloating, parsePlacement, resolvePosition, trackAnchor, } from "./foundations/overlay/index.js";
|
|
14
17
|
// Components: actions
|
|
@@ -13,6 +13,14 @@ export declare class ContentUploader extends BaseElement {
|
|
|
13
13
|
private footerEl;
|
|
14
14
|
private summaryEl;
|
|
15
15
|
private queueSignature;
|
|
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;
|
|
16
24
|
get folderId(): string | null;
|
|
17
25
|
set folderId(value: string | null);
|
|
18
26
|
get token(): string | null;
|
|
@@ -30,8 +38,43 @@ export declare class ContentUploader extends BaseElement {
|
|
|
30
38
|
/** Maximum accepted file size in bytes. */
|
|
31
39
|
get maxFileSize(): number | undefined;
|
|
32
40
|
set maxFileSize(value: number | undefined);
|
|
41
|
+
/**
|
|
42
|
+
* Most files the queue will hold, default 100 — the same ceiling
|
|
43
|
+
* box-ui-elements uses. Further files are rejected with
|
|
44
|
+
* `file-limit-reached` rather than enqueued.
|
|
45
|
+
*
|
|
46
|
+
* A default matters here in a way it does not for the other constraints: a
|
|
47
|
+
* dropped folder can carry thousands of files, and an unbounded queue has no
|
|
48
|
+
* back pressure at all.
|
|
49
|
+
*/
|
|
50
|
+
get fileLimit(): number;
|
|
51
|
+
set fileLimit(value: number);
|
|
52
|
+
/**
|
|
53
|
+
* Make the browse dialog pick a folder rather than files.
|
|
54
|
+
*
|
|
55
|
+
* Dropping a folder works either way; the platform only forces the choice on
|
|
56
|
+
* the click-to-browse path.
|
|
57
|
+
*/
|
|
58
|
+
get directories(): boolean;
|
|
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
|
+
*/
|
|
33
72
|
get dropLabel(): string;
|
|
34
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
|
+
*/
|
|
35
78
|
get dropMessage(): string;
|
|
36
79
|
set dropMessage(value: string);
|
|
37
80
|
get transport(): UploadTransport | null;
|
|
@@ -39,10 +82,24 @@ export declare class ContentUploader extends BaseElement {
|
|
|
39
82
|
get state(): Readonly<UploaderState> | null;
|
|
40
83
|
/** The live queue controller. Null until configured. */
|
|
41
84
|
get uploaderController(): ContentUploaderController | null;
|
|
85
|
+
/**
|
|
86
|
+
* Attributes that only affect how the drop zone presents itself. Everything
|
|
87
|
+
* else feeds the session config, so changing it re-creates the queue.
|
|
88
|
+
*/
|
|
89
|
+
private static readonly PRESENTATION_ATTRIBUTES;
|
|
42
90
|
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
43
91
|
connectedCallback(): void;
|
|
44
92
|
disconnectedCallback(): void;
|
|
45
93
|
addFiles(files: UploadFileLike[]): UploadQueueItem[];
|
|
94
|
+
/**
|
|
95
|
+
* Queue files that carry a directory path relative to the destination, the
|
|
96
|
+
* way a dropped folder does. Requires `createFolder` on the transport;
|
|
97
|
+
* without it each pathed file is rejected with `folder-unsupported`.
|
|
98
|
+
*/
|
|
99
|
+
addEntries(entries: Array<{
|
|
100
|
+
file: UploadFileLike;
|
|
101
|
+
path?: string;
|
|
102
|
+
}>): UploadQueueItem[];
|
|
46
103
|
start(): void;
|
|
47
104
|
cancelItem(itemId: string): void;
|
|
48
105
|
retryItem(itemId: string): void;
|
|
@@ -53,9 +110,28 @@ export declare class ContentUploader extends BaseElement {
|
|
|
53
110
|
private startController;
|
|
54
111
|
private readConfig;
|
|
55
112
|
private subscribeToController;
|
|
113
|
+
/**
|
|
114
|
+
* Coalesce the re-render for a burst of controller events.
|
|
115
|
+
*
|
|
116
|
+
* A folder drop adds up to `fileLimit` items in one go, each emitting its own
|
|
117
|
+
* `itemAdded`; rendering per event rebuilt the whole queue once per file,
|
|
118
|
+
* which is quadratic in the size of the drop. One render per microtask turn
|
|
119
|
+
* gives the same result for a fraction of the work.
|
|
120
|
+
*/
|
|
121
|
+
private scheduleUpdate;
|
|
56
122
|
private teardownController;
|
|
57
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;
|
|
58
132
|
protected setupListeners(): void;
|
|
133
|
+
/** Cancel every item still queued or in flight. Settled items are untouched. */
|
|
134
|
+
cancelAll(): void;
|
|
59
135
|
private rebuildQueue;
|
|
60
136
|
private patchQueueProgress;
|
|
61
137
|
protected update(): void;
|
|
@@ -4,9 +4,12 @@ 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";
|
|
11
|
+
/** Matches the box-ui-elements ceiling, so a port behaves the same way. */
|
|
12
|
+
const DEFAULT_FILE_LIMIT = 100;
|
|
10
13
|
const escapeHtml = (value) => value
|
|
11
14
|
.replaceAll("&", "&")
|
|
12
15
|
.replaceAll("<", "<")
|
|
@@ -169,10 +172,19 @@ const elementStyles = `
|
|
|
169
172
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 18%, transparent);
|
|
170
173
|
}
|
|
171
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. */
|
|
172
181
|
[part="footer"] {
|
|
173
182
|
display: flex;
|
|
183
|
+
flex-wrap: wrap;
|
|
174
184
|
align-items: center;
|
|
175
|
-
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);
|
|
176
188
|
}
|
|
177
189
|
|
|
178
190
|
[part="footer"][hidden] {
|
|
@@ -180,43 +192,87 @@ const elementStyles = `
|
|
|
180
192
|
}
|
|
181
193
|
|
|
182
194
|
[part="summary"] {
|
|
183
|
-
|
|
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;
|
|
184
199
|
font-size: 0.85rem;
|
|
185
200
|
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
186
201
|
}
|
|
187
202
|
|
|
188
|
-
[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"] {
|
|
189
215
|
appearance: none;
|
|
190
216
|
font: inherit;
|
|
191
217
|
font-size: 0.875rem;
|
|
192
218
|
font-weight: 600;
|
|
193
|
-
padding: 0.
|
|
219
|
+
padding: 0.5rem 0.9rem;
|
|
194
220
|
border-radius: ${boeRadius.control};
|
|
195
221
|
border: 1px solid var(--boe-token-stroke-stroke, #e8e8e8);
|
|
196
222
|
background: var(--boe-token-surface-surface, #ffffff);
|
|
197
223
|
color: var(--boe-token-text-text, #222222);
|
|
198
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};
|
|
199
230
|
}
|
|
200
231
|
|
|
201
|
-
[part
|
|
232
|
+
[part~="action"]:hover:not(:disabled) {
|
|
202
233
|
background: var(--boe-token-surface-surface-hover, #f4f4f4);
|
|
203
234
|
border-color: var(--boe-token-stroke-stroke-hover, #bcbcbc);
|
|
204
235
|
}
|
|
205
236
|
|
|
206
|
-
[part
|
|
237
|
+
[part~="action"]:focus-visible {
|
|
207
238
|
outline: none;
|
|
208
239
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 18%, transparent);
|
|
209
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
|
+
}
|
|
210
263
|
`;
|
|
211
264
|
export class ContentUploader extends BaseElement {
|
|
212
265
|
static tagName = DEFAULT_TAG_NAME;
|
|
213
266
|
static get observedAttributes() {
|
|
214
267
|
return [
|
|
215
268
|
"auto-start",
|
|
269
|
+
"closable",
|
|
216
270
|
"concurrency",
|
|
217
271
|
"drop-label",
|
|
218
272
|
"drop-message",
|
|
273
|
+
"directories",
|
|
219
274
|
"extensions",
|
|
275
|
+
"file-limit",
|
|
220
276
|
"folder-id",
|
|
221
277
|
"language",
|
|
222
278
|
"max-file-size",
|
|
@@ -232,6 +288,18 @@ export class ContentUploader extends BaseElement {
|
|
|
232
288
|
footerEl;
|
|
233
289
|
summaryEl;
|
|
234
290
|
queueSignature = "";
|
|
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
|
+
};
|
|
235
303
|
get folderId() {
|
|
236
304
|
return this.getAttribute("folder-id");
|
|
237
305
|
}
|
|
@@ -293,14 +361,67 @@ export class ContentUploader extends BaseElement {
|
|
|
293
361
|
}
|
|
294
362
|
this.removeAttribute("max-file-size");
|
|
295
363
|
}
|
|
364
|
+
/**
|
|
365
|
+
* Most files the queue will hold, default 100 — the same ceiling
|
|
366
|
+
* box-ui-elements uses. Further files are rejected with
|
|
367
|
+
* `file-limit-reached` rather than enqueued.
|
|
368
|
+
*
|
|
369
|
+
* A default matters here in a way it does not for the other constraints: a
|
|
370
|
+
* dropped folder can carry thousands of files, and an unbounded queue has no
|
|
371
|
+
* back pressure at all.
|
|
372
|
+
*/
|
|
373
|
+
get fileLimit() {
|
|
374
|
+
const raw = Number(this.getAttribute("file-limit"));
|
|
375
|
+
return Number.isFinite(raw) && raw >= 1 ? Math.floor(raw) : DEFAULT_FILE_LIMIT;
|
|
376
|
+
}
|
|
377
|
+
set fileLimit(value) {
|
|
378
|
+
this.setAttribute("file-limit", String(value));
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Make the browse dialog pick a folder rather than files.
|
|
382
|
+
*
|
|
383
|
+
* Dropping a folder works either way; the platform only forces the choice on
|
|
384
|
+
* the click-to-browse path.
|
|
385
|
+
*/
|
|
386
|
+
get directories() {
|
|
387
|
+
return this.hasAttribute("directories");
|
|
388
|
+
}
|
|
389
|
+
set directories(value) {
|
|
390
|
+
this.toggleAttribute("directories", value);
|
|
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
|
+
*/
|
|
296
412
|
get dropLabel() {
|
|
297
|
-
return this.getAttribute("drop-label") ??
|
|
413
|
+
return (this.getAttribute("drop-label") ??
|
|
414
|
+
(this.directories ? "Drag and drop files and folders" : "Drag and drop files"));
|
|
298
415
|
}
|
|
299
416
|
set dropLabel(value) {
|
|
300
417
|
this.setAttribute("drop-label", value);
|
|
301
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
|
+
*/
|
|
302
423
|
get dropMessage() {
|
|
303
|
-
return this.getAttribute("drop-message") ?? "
|
|
424
|
+
return this.getAttribute("drop-message") ?? "";
|
|
304
425
|
}
|
|
305
426
|
set dropMessage(value) {
|
|
306
427
|
this.setAttribute("drop-message", value);
|
|
@@ -319,23 +440,42 @@ export class ContentUploader extends BaseElement {
|
|
|
319
440
|
get uploaderController() {
|
|
320
441
|
return this.controller;
|
|
321
442
|
}
|
|
443
|
+
/**
|
|
444
|
+
* Attributes that only affect how the drop zone presents itself. Everything
|
|
445
|
+
* else feeds the session config, so changing it re-creates the queue.
|
|
446
|
+
*/
|
|
447
|
+
static PRESENTATION_ATTRIBUTES = new Set([
|
|
448
|
+
"closable",
|
|
449
|
+
"directories",
|
|
450
|
+
"drop-label",
|
|
451
|
+
"drop-message",
|
|
452
|
+
]);
|
|
322
453
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
323
|
-
|
|
324
|
-
if (name !== "drop-label" && name !== "drop-message") {
|
|
454
|
+
if (!ContentUploader.PRESENTATION_ATTRIBUTES.has(name)) {
|
|
325
455
|
this.scheduleStart();
|
|
326
456
|
}
|
|
327
457
|
super.attributeChangedCallback(name, oldValue, newValue);
|
|
328
458
|
}
|
|
329
459
|
connectedCallback() {
|
|
460
|
+
globalThis.addEventListener?.(DESIGN_SYSTEM_CHANGE_EVENT, this.handleDesignSystemChange);
|
|
330
461
|
super.connectedCallback();
|
|
331
462
|
this.scheduleStart();
|
|
332
463
|
}
|
|
333
464
|
disconnectedCallback() {
|
|
465
|
+
globalThis.removeEventListener?.(DESIGN_SYSTEM_CHANGE_EVENT, this.handleDesignSystemChange);
|
|
334
466
|
this.teardownController();
|
|
335
467
|
}
|
|
336
468
|
addFiles(files) {
|
|
337
469
|
return this.controller?.addFiles(files) ?? [];
|
|
338
470
|
}
|
|
471
|
+
/**
|
|
472
|
+
* Queue files that carry a directory path relative to the destination, the
|
|
473
|
+
* way a dropped folder does. Requires `createFolder` on the transport;
|
|
474
|
+
* without it each pathed file is rejected with `folder-unsupported`.
|
|
475
|
+
*/
|
|
476
|
+
addEntries(entries) {
|
|
477
|
+
return this.controller?.addEntries(entries) ?? [];
|
|
478
|
+
}
|
|
339
479
|
start() {
|
|
340
480
|
this.controller?.start();
|
|
341
481
|
}
|
|
@@ -396,6 +536,7 @@ export class ContentUploader extends BaseElement {
|
|
|
396
536
|
autoStart: this.autoStart,
|
|
397
537
|
concurrency: this.concurrency,
|
|
398
538
|
extensions: this.extensions.length ? this.extensions : undefined,
|
|
539
|
+
fileLimit: this.fileLimit,
|
|
399
540
|
folderId: this.folderId,
|
|
400
541
|
language: this.language ?? undefined,
|
|
401
542
|
maxFileSizeBytes: this.maxFileSize,
|
|
@@ -422,10 +563,28 @@ export class ContentUploader extends BaseElement {
|
|
|
422
563
|
composed: true,
|
|
423
564
|
detail: payload,
|
|
424
565
|
}));
|
|
566
|
+
this.scheduleUpdate();
|
|
567
|
+
}));
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Coalesce the re-render for a burst of controller events.
|
|
571
|
+
*
|
|
572
|
+
* A folder drop adds up to `fileLimit` items in one go, each emitting its own
|
|
573
|
+
* `itemAdded`; rendering per event rebuilt the whole queue once per file,
|
|
574
|
+
* which is quadratic in the size of the drop. One render per microtask turn
|
|
575
|
+
* gives the same result for a fraction of the work.
|
|
576
|
+
*/
|
|
577
|
+
scheduleUpdate() {
|
|
578
|
+
if (!this.isRendered || this.updateScheduled) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
this.updateScheduled = true;
|
|
582
|
+
queueMicrotask(() => {
|
|
583
|
+
this.updateScheduled = false;
|
|
425
584
|
if (this.isRendered) {
|
|
426
585
|
this.update();
|
|
427
586
|
}
|
|
428
|
-
})
|
|
587
|
+
});
|
|
429
588
|
}
|
|
430
589
|
teardownController() {
|
|
431
590
|
for (const unsubscribe of this.unsubscribeFns) {
|
|
@@ -443,11 +602,18 @@ export class ContentUploader extends BaseElement {
|
|
|
443
602
|
this.shadowRoot.innerHTML = `
|
|
444
603
|
<style>${elementStyles}</style>
|
|
445
604
|
<div part="panel">
|
|
446
|
-
<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>
|
|
447
608
|
<ul part="queue" aria-label="Upload queue"></ul>
|
|
448
|
-
<footer part="footer"
|
|
609
|
+
<footer part="footer">
|
|
610
|
+
<button type="button" part="action close">Close</button>
|
|
449
611
|
<span part="summary" role="status" aria-live="polite"></span>
|
|
450
|
-
<
|
|
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>
|
|
451
617
|
</footer>
|
|
452
618
|
</div>
|
|
453
619
|
`;
|
|
@@ -455,12 +621,34 @@ export class ContentUploader extends BaseElement {
|
|
|
455
621
|
this.queueEl = this.shadowRoot.querySelector('[part="queue"]');
|
|
456
622
|
this.footerEl = this.shadowRoot.querySelector('[part="footer"]');
|
|
457
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
|
+
}
|
|
458
643
|
}
|
|
459
644
|
setupListeners() {
|
|
460
645
|
this.dropZoneEl.addEventListener("files-selected", event => {
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
646
|
+
const detail = event.detail;
|
|
647
|
+
// Prefer entries: they carry the folder each file came from. `files` is
|
|
648
|
+
// the flat fallback for a host dispatching this event by hand.
|
|
649
|
+
const entries = detail?.entries ?? (detail?.files ?? []).map(file => ({ file, path: "" }));
|
|
650
|
+
if (entries.length) {
|
|
651
|
+
this.controller?.addEntries(entries);
|
|
464
652
|
}
|
|
465
653
|
});
|
|
466
654
|
this.queueEl.addEventListener("click", event => {
|
|
@@ -483,9 +671,33 @@ export class ContentUploader extends BaseElement {
|
|
|
483
671
|
this.removeItem(itemId);
|
|
484
672
|
}
|
|
485
673
|
});
|
|
486
|
-
this.
|
|
674
|
+
this.clearCompletedEl.addEventListener("click", () => {
|
|
487
675
|
this.clearCompleted();
|
|
488
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
|
+
}
|
|
489
701
|
}
|
|
490
702
|
rebuildQueue(items) {
|
|
491
703
|
this.queueEl.innerHTML = items
|
|
@@ -534,8 +746,17 @@ export class ContentUploader extends BaseElement {
|
|
|
534
746
|
if (!this.queueEl) {
|
|
535
747
|
return;
|
|
536
748
|
}
|
|
749
|
+
this.renderIllustration();
|
|
537
750
|
this.dropZoneEl.label = this.dropLabel;
|
|
538
751
|
this.dropZoneEl.message = this.dropMessage;
|
|
752
|
+
this.dropZoneEl.directories = this.directories;
|
|
753
|
+
// Greys out the files the queue would reject anyway, so the picker stops
|
|
754
|
+
// offering a choice that ends in a rejection. The queue's own check still
|
|
755
|
+
// decides — `accept` is advisory, and does nothing for a drop.
|
|
756
|
+
// A leading dot is optional in `extensions`, as it is for the queue check.
|
|
757
|
+
this.dropZoneEl.accept = this.extensions
|
|
758
|
+
.map(extension => `.${extension.replace(/^\./, "")}`)
|
|
759
|
+
.join(",");
|
|
539
760
|
const items = this.controller?.getState().items ?? [];
|
|
540
761
|
// Structural changes rebuild the rows; progress ticks patch the bars in
|
|
541
762
|
// place so in-row focus (Cancel while uploading) survives.
|
|
@@ -546,12 +767,29 @@ export class ContentUploader extends BaseElement {
|
|
|
546
767
|
}
|
|
547
768
|
this.patchQueueProgress(items);
|
|
548
769
|
const summary = summarizeUploadQueue(items);
|
|
549
|
-
this.footerEl.hidden = summary.total === 0;
|
|
550
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;
|
|
551
787
|
this.summaryEl.textContent =
|
|
552
|
-
|
|
553
|
-
?
|
|
554
|
-
:
|
|
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` : ""}`;
|
|
555
793
|
}
|
|
556
794
|
}
|
|
557
795
|
DropZone.register();
|