@operato/input 10.29.29 → 10.29.36
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/CHANGELOG.md +17 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +776 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/webpack/bootstrap.html +118 -0
- package/coverage/lcov-report/webpack/index.html +116 -0
- package/coverage/lcov-report/webpack/runtime/define property getters.html +118 -0
- package/coverage/lcov-report/webpack/runtime/global.html +118 -0
- package/coverage/lcov-report/webpack/runtime/hasOwnProperty shorthand.html +118 -0
- package/coverage/lcov-report/webpack/runtime/index.html +161 -0
- package/coverage/lcov-report/webpack/runtime/make namespace object.html +118 -0
- package/coverage/lcov.info +46329 -0
- package/dist/src/ox-input-file.d.ts +42 -0
- package/dist/src/ox-input-file.js +271 -38
- package/dist/src/ox-input-file.js.map +1 -1
- package/dist/test/ox-input-file.test.d.ts +1 -0
- package/dist/test/ox-input-file.test.js +103 -0
- package/dist/test/ox-input-file.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/test/ox-input-file.test.ts +136 -0
- package/translations/en.json +9 -0
- package/translations/es.json +9 -0
- package/translations/ja.json +9 -0
- package/translations/ko.json +9 -0
- package/translations/ms.json +9 -0
- package/translations/zh.json +9 -0
|
@@ -4,6 +4,21 @@
|
|
|
4
4
|
import '@material/web/icon/icon.js';
|
|
5
5
|
import { PropertyValues } from 'lit';
|
|
6
6
|
import { OxFormField } from './ox-form-field.js';
|
|
7
|
+
/**
|
|
8
|
+
* Picks files — the one picker for every screen.
|
|
9
|
+
*
|
|
10
|
+
* Two looks:
|
|
11
|
+
* (default) a drop box, for a screen where the file is the subject (an import, an attachment gallery)
|
|
12
|
+
* quiet text buttons only — 「첨부」, then the picked name with 「바꾸기」「빼기」 — for a screen that is
|
|
13
|
+
* read first (a 360 section, a form row). The drop area is `dropTarget` (the section) and
|
|
14
|
+
* shows only while a file is dragged over it.
|
|
15
|
+
*
|
|
16
|
+
* Both refuse on the spot what the server would refuse after the upload: a type outside `accept`
|
|
17
|
+
* (a drop skips the native filter), a file over the server's size limit, more files than it takes.
|
|
18
|
+
* The limits are the server's own numbers (`fileUploadLimits` in @operato/utils), never written here.
|
|
19
|
+
*
|
|
20
|
+
* The owner uploads; while it does, it sets `progress` (0‒1) and the picker shows it.
|
|
21
|
+
*/
|
|
7
22
|
export declare class OxInputFile extends OxFormField {
|
|
8
23
|
static styles: import("lit").CSSResult[];
|
|
9
24
|
multiple?: boolean;
|
|
@@ -13,12 +28,39 @@ export declare class OxInputFile extends OxFormField {
|
|
|
13
28
|
description?: string;
|
|
14
29
|
hideFileList: boolean;
|
|
15
30
|
attachFileList: boolean;
|
|
31
|
+
/** Text buttons instead of the drop box — for a screen that is read first. */
|
|
32
|
+
quiet: boolean;
|
|
33
|
+
/** Where a file may be dropped. The picker itself when not given; a 360 passes its section. */
|
|
34
|
+
dropTarget?: HTMLElement;
|
|
35
|
+
/** Upload progress 0‒1, set by the owner while it uploads; the buttons wait meanwhile. */
|
|
36
|
+
progress?: number;
|
|
16
37
|
fileInput: HTMLInputElement;
|
|
38
|
+
private refusals;
|
|
39
|
+
private refusalLimits;
|
|
40
|
+
private dropArea?;
|
|
41
|
+
private undoDrop?;
|
|
42
|
+
private previews;
|
|
43
|
+
private onFileDrop;
|
|
44
|
+
private get uploading();
|
|
17
45
|
render(): import("lit-html").TemplateResult<1>;
|
|
46
|
+
private renderInput;
|
|
47
|
+
private renderBox;
|
|
48
|
+
private renderQuiet;
|
|
49
|
+
private renderKind;
|
|
50
|
+
private renderProgress;
|
|
51
|
+
private renderRefusals;
|
|
18
52
|
updated(changes: PropertyValues<this>): void;
|
|
53
|
+
connectedCallback(): void;
|
|
19
54
|
firstUpdated(): void;
|
|
55
|
+
disconnectedCallback(): void;
|
|
56
|
+
private setDropArea;
|
|
57
|
+
private syncPreviews;
|
|
20
58
|
get files(): any;
|
|
59
|
+
/** Opens the file dialog — for an owner whose own button starts the pick. */
|
|
60
|
+
pick(): void;
|
|
21
61
|
reset(): void;
|
|
62
|
+
private removeFile;
|
|
63
|
+
private take;
|
|
22
64
|
_onChangeValue(e: Event): void;
|
|
23
65
|
_notifyChange(): void;
|
|
24
66
|
}
|
|
@@ -3,25 +3,54 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { __decorate } from "tslib";
|
|
5
5
|
import '@material/web/icon/icon.js';
|
|
6
|
-
import { css, html } from 'lit';
|
|
7
|
-
import { customElement, property, query } from 'lit/decorators.js';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
6
|
+
import { css, html, nothing } from 'lit';
|
|
7
|
+
import { customElement, property, query, state } from 'lit/decorators.js';
|
|
8
|
+
import { i18next } from '@operato/i18n';
|
|
9
|
+
import { ButtonStyles, ScrollbarStyles } from '@operato/styles';
|
|
10
|
+
import { FileDropHelper, fileUploadLimits, sortPickedFiles } from '@operato/utils';
|
|
10
11
|
import { OxFormField } from './ox-form-field.js';
|
|
11
12
|
import { InputStyles } from './input-styles.js';
|
|
13
|
+
const megabytes = (bytes) => bytes >= 1048576 ? `${Math.round((bytes / 1048576) * 10) / 10}MB` : `${Math.max(1, Math.round(bytes / 1024))}KB`;
|
|
14
|
+
/**
|
|
15
|
+
* Picks files — the one picker for every screen.
|
|
16
|
+
*
|
|
17
|
+
* Two looks:
|
|
18
|
+
* (default) a drop box, for a screen where the file is the subject (an import, an attachment gallery)
|
|
19
|
+
* quiet text buttons only — 「첨부」, then the picked name with 「바꾸기」「빼기」 — for a screen that is
|
|
20
|
+
* read first (a 360 section, a form row). The drop area is `dropTarget` (the section) and
|
|
21
|
+
* shows only while a file is dragged over it.
|
|
22
|
+
*
|
|
23
|
+
* Both refuse on the spot what the server would refuse after the upload: a type outside `accept`
|
|
24
|
+
* (a drop skips the native filter), a file over the server's size limit, more files than it takes.
|
|
25
|
+
* The limits are the server's own numbers (`fileUploadLimits` in @operato/utils), never written here.
|
|
26
|
+
*
|
|
27
|
+
* The owner uploads; while it does, it sets `progress` (0‒1) and the picker shows it.
|
|
28
|
+
*/
|
|
12
29
|
let OxInputFile = class OxInputFile extends OxFormField {
|
|
13
30
|
constructor() {
|
|
14
31
|
super(...arguments);
|
|
15
32
|
this.icon = 'upload';
|
|
16
|
-
this.description = 'drop files here!';
|
|
17
33
|
this.hideFileList = false;
|
|
18
34
|
this.attachFileList = false;
|
|
35
|
+
/** Text buttons instead of the drop box — for a screen that is read first. */
|
|
36
|
+
this.quiet = false;
|
|
37
|
+
this.refusals = [];
|
|
38
|
+
this.refusalLimits = {};
|
|
39
|
+
this.previews = new Map();
|
|
40
|
+
this.onFileDrop = (e) => {
|
|
41
|
+
if (this.disabled || this.readonly || this.uploading) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const files = e.detail;
|
|
45
|
+
this.take(this.multiple ? files : files.slice(0, 1));
|
|
46
|
+
};
|
|
19
47
|
}
|
|
20
48
|
static { this.styles = [
|
|
21
49
|
InputStyles,
|
|
22
50
|
ScrollbarStyles,
|
|
51
|
+
ButtonStyles,
|
|
23
52
|
css `
|
|
24
|
-
:host {
|
|
53
|
+
:host(:not([quiet])) {
|
|
25
54
|
display: flex;
|
|
26
55
|
flex-direction: column;
|
|
27
56
|
border-radius: var(--md-sys-shape-corner-small);
|
|
@@ -35,17 +64,17 @@ let OxInputFile = class OxInputFile extends OxFormField {
|
|
|
35
64
|
background-color: var(--md-sys-color-surface);
|
|
36
65
|
font-size: var(--md-sys-typescale-label-large-size, 0.875rem);
|
|
37
66
|
}
|
|
38
|
-
:host > span {
|
|
67
|
+
:host(:not([quiet])) > span {
|
|
39
68
|
color: var(--md-sys-color-primary);
|
|
40
69
|
font-weight: var(--md-sys-typescale-label-medium-weight, var(--md-ref-typeface-weight-medium, 500));
|
|
41
70
|
}
|
|
42
71
|
|
|
43
|
-
:host > md-icon {
|
|
72
|
+
:host(:not([quiet])) > md-icon {
|
|
44
73
|
color: var(--md-sys-color-primary);
|
|
45
74
|
--md-icon-size: var(--file-uploader-icon-size, 36px);
|
|
46
75
|
}
|
|
47
76
|
|
|
48
|
-
:host(.candrop) {
|
|
77
|
+
:host(.candrop:not([quiet])) {
|
|
49
78
|
background-color: var(--md-sys-color-primary-fixed);
|
|
50
79
|
border: 1px dashed var(--md-sys-color-primary-fixed-dim);
|
|
51
80
|
}
|
|
@@ -95,27 +124,89 @@ let OxInputFile = class OxInputFile extends OxFormField {
|
|
|
95
124
|
li md-icon:active {
|
|
96
125
|
color: var(--md-sys-color-error);
|
|
97
126
|
}
|
|
127
|
+
|
|
128
|
+
/* ── quiet ── no box: the words are the control. */
|
|
129
|
+
:host([quiet]) {
|
|
130
|
+
display: inline-flex;
|
|
131
|
+
flex-wrap: wrap;
|
|
132
|
+
align-items: center;
|
|
133
|
+
gap: var(--spacing-small, 4px);
|
|
134
|
+
font-size: var(--ui-t-body, 13px);
|
|
135
|
+
color: var(--md-sys-color-on-surface);
|
|
136
|
+
border-radius: var(--md-sys-shape-corner-small, 4px);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
:host([quiet].candrop) {
|
|
140
|
+
outline: 1px dashed var(--md-sys-color-primary);
|
|
141
|
+
outline-offset: 2px;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.picked {
|
|
145
|
+
display: inline-flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
gap: var(--spacing-small, 4px);
|
|
148
|
+
min-width: 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.picked .name {
|
|
152
|
+
overflow: hidden;
|
|
153
|
+
text-overflow: ellipsis;
|
|
154
|
+
white-space: nowrap;
|
|
155
|
+
max-width: var(--ox-input-file-name-width, 24em);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.picked .size,
|
|
159
|
+
.progress {
|
|
160
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
161
|
+
font-variant-numeric: tabular-nums;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.picked md-icon {
|
|
165
|
+
--md-icon-size: 16px;
|
|
166
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.picked img {
|
|
170
|
+
width: 20px;
|
|
171
|
+
height: 20px;
|
|
172
|
+
object-fit: cover;
|
|
173
|
+
border-radius: 2px;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.refusal {
|
|
177
|
+
flex-basis: 100%;
|
|
178
|
+
color: var(--ui-crit, var(--md-sys-color-error));
|
|
179
|
+
}
|
|
98
180
|
`
|
|
99
181
|
]; }
|
|
182
|
+
get uploading() {
|
|
183
|
+
return typeof this.progress === 'number';
|
|
184
|
+
}
|
|
100
185
|
render() {
|
|
186
|
+
return this.quiet ? this.renderQuiet() : this.renderBox();
|
|
187
|
+
}
|
|
188
|
+
renderInput() {
|
|
189
|
+
return html `<input
|
|
190
|
+
id="input-file"
|
|
191
|
+
type="file"
|
|
192
|
+
accept=${this.accept || ''}
|
|
193
|
+
?multiple=${this.multiple}
|
|
194
|
+
hidden
|
|
195
|
+
capture="environment"
|
|
196
|
+
@change=${(e) => this._onChangeValue(e)}
|
|
197
|
+
?disabled=${this.disabled || this.readonly}
|
|
198
|
+
/>`;
|
|
199
|
+
}
|
|
200
|
+
renderBox() {
|
|
101
201
|
var files = this.value || [];
|
|
102
202
|
return html `
|
|
103
203
|
<md-icon>${this.icon || 'upload'}</md-icon>
|
|
104
204
|
|
|
105
|
-
<span>${this.description || 'drop files here!'}</span>
|
|
205
|
+
<span>${this.description || i18next.t('text.drop files here', { defaultValue: 'drop files here!' })}</span>
|
|
106
206
|
|
|
107
|
-
|
|
108
|
-
id="input-file"
|
|
109
|
-
type="file"
|
|
110
|
-
accept=${this.accept}
|
|
111
|
-
?multiple=${this.multiple}
|
|
112
|
-
hidden
|
|
113
|
-
capture="environment"
|
|
114
|
-
@change=${(e) => this._onChangeValue(e)}
|
|
115
|
-
?disabled=${this.disabled}
|
|
116
|
-
/>
|
|
207
|
+
${this.renderInput()}
|
|
117
208
|
|
|
118
|
-
<label for="input-file">${this.label || 'select files'}</label>
|
|
209
|
+
<label for="input-file">${this.label || i18next.t('label.select files', { defaultValue: 'select files' })}</label>
|
|
119
210
|
|
|
120
211
|
${!this.hideFileList && files?.length
|
|
121
212
|
? html ` <ul>
|
|
@@ -127,9 +218,7 @@ let OxInputFile = class OxInputFile extends OxFormField {
|
|
|
127
218
|
if (this.disabled) {
|
|
128
219
|
return;
|
|
129
220
|
}
|
|
130
|
-
|
|
131
|
-
this.value = [...files];
|
|
132
|
-
this._notifyChange();
|
|
221
|
+
this.removeFile(file);
|
|
133
222
|
}}
|
|
134
223
|
>delete_outline</md-icon
|
|
135
224
|
>
|
|
@@ -137,41 +226,170 @@ let OxInputFile = class OxInputFile extends OxFormField {
|
|
|
137
226
|
`)}
|
|
138
227
|
</ul>`
|
|
139
228
|
: html ``}
|
|
229
|
+
${this.renderRefusals()} ${this.renderProgress()}
|
|
140
230
|
`;
|
|
141
231
|
}
|
|
232
|
+
renderQuiet() {
|
|
233
|
+
const files = this.value || [];
|
|
234
|
+
const busy = this.disabled || this.readonly || this.uploading;
|
|
235
|
+
const shown = this.hideFileList ? [] : files;
|
|
236
|
+
return html `
|
|
237
|
+
${this.renderInput()}
|
|
238
|
+
${shown.map(file => html `<span class="picked">
|
|
239
|
+
${this.renderKind(file)}
|
|
240
|
+
<span class="name" title=${file.name}>${file.name}</span>
|
|
241
|
+
<span class="size">${megabytes(file.size)}</span>
|
|
242
|
+
${!this.multiple
|
|
243
|
+
? html `<button type="button" quiet small ?disabled=${busy} @click=${() => this.pick()}>
|
|
244
|
+
${i18next.t('label.replace file', { defaultValue: 'replace' })}
|
|
245
|
+
</button>`
|
|
246
|
+
: nothing}
|
|
247
|
+
<button type="button" quiet small danger ?disabled=${busy} @click=${() => this.removeFile(file)}>
|
|
248
|
+
${i18next.t('label.remove file', { defaultValue: 'remove' })}
|
|
249
|
+
</button>
|
|
250
|
+
</span>`)}
|
|
251
|
+
${this.multiple || !shown.length
|
|
252
|
+
? html `<button type="button" quiet small ?disabled=${busy} @click=${() => this.pick()}>
|
|
253
|
+
${this.label || i18next.t('label.attach file', { defaultValue: 'attach' })}
|
|
254
|
+
</button>`
|
|
255
|
+
: nothing}
|
|
256
|
+
${this.renderProgress()} ${this.renderRefusals()}
|
|
257
|
+
`;
|
|
258
|
+
}
|
|
259
|
+
/* A picture shows itself; anything else is told by its kind. */
|
|
260
|
+
renderKind(file) {
|
|
261
|
+
const preview = this.previews.get(file);
|
|
262
|
+
if (preview)
|
|
263
|
+
return html `<img src=${preview} alt="" />`;
|
|
264
|
+
const icon = file.type === 'application/pdf' ? 'picture_as_pdf' : 'draft';
|
|
265
|
+
return html `<md-icon>${icon}</md-icon>`;
|
|
266
|
+
}
|
|
267
|
+
renderProgress() {
|
|
268
|
+
if (!this.uploading)
|
|
269
|
+
return nothing;
|
|
270
|
+
const percent = Math.round(Math.min(1, Math.max(0, this.progress)) * 100);
|
|
271
|
+
return html `<span class="progress" role="status">
|
|
272
|
+
${i18next.t('text.uploading file', { percent, defaultValue: `uploading ${percent}%` })}
|
|
273
|
+
</span>`;
|
|
274
|
+
}
|
|
275
|
+
renderRefusals() {
|
|
276
|
+
if (!this.refusals.length)
|
|
277
|
+
return nothing;
|
|
278
|
+
const { maxFileSize, maxFiles } = this.refusalLimits;
|
|
279
|
+
return html `<div class="refusal" role="alert">
|
|
280
|
+
${this.refusals.map(({ file, reason }) => reason === 'type'
|
|
281
|
+
? i18next.t('text.file refused for type', { name: file.name, defaultValue: `${file.name}: not a type taken here` })
|
|
282
|
+
: reason === 'size'
|
|
283
|
+
? i18next.t('text.file refused for size', {
|
|
284
|
+
name: file.name,
|
|
285
|
+
limit: megabytes(maxFileSize),
|
|
286
|
+
defaultValue: `${file.name}: over ${megabytes(maxFileSize)}`
|
|
287
|
+
})
|
|
288
|
+
: i18next.t('text.file refused for count', {
|
|
289
|
+
name: file.name,
|
|
290
|
+
limit: maxFiles,
|
|
291
|
+
defaultValue: `${file.name}: ${maxFiles} files at most`
|
|
292
|
+
})).join(' · ')}
|
|
293
|
+
</div>`;
|
|
294
|
+
}
|
|
142
295
|
updated(changes) {
|
|
143
296
|
if (changes.has('value') && !(this.value instanceof Array)) {
|
|
144
297
|
this.value = !this.value ? [] : [this.value];
|
|
145
298
|
}
|
|
299
|
+
if (changes.has('value')) {
|
|
300
|
+
this.syncPreviews();
|
|
301
|
+
}
|
|
302
|
+
if (changes.has('dropTarget')) {
|
|
303
|
+
this.setDropArea();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
connectedCallback() {
|
|
307
|
+
super.connectedCallback();
|
|
308
|
+
if (this.hasUpdated)
|
|
309
|
+
this.setDropArea();
|
|
146
310
|
}
|
|
147
311
|
firstUpdated() {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
312
|
+
this.setDropArea();
|
|
313
|
+
}
|
|
314
|
+
disconnectedCallback() {
|
|
315
|
+
super.disconnectedCallback();
|
|
316
|
+
this.undoDrop?.();
|
|
317
|
+
this.dropArea?.removeEventListener('file-drop', this.onFileDrop);
|
|
318
|
+
this.undoDrop = this.dropArea = undefined;
|
|
319
|
+
this.previews.forEach(url => URL.revokeObjectURL(url));
|
|
320
|
+
this.previews.clear();
|
|
321
|
+
}
|
|
322
|
+
setDropArea() {
|
|
323
|
+
const area = this.dropTarget || this;
|
|
324
|
+
if (area === this.dropArea)
|
|
325
|
+
return;
|
|
326
|
+
this.undoDrop?.();
|
|
327
|
+
this.dropArea?.removeEventListener('file-drop', this.onFileDrop);
|
|
328
|
+
this.dropArea = area;
|
|
329
|
+
this.undoDrop = FileDropHelper.set(area);
|
|
330
|
+
area.addEventListener('file-drop', this.onFileDrop);
|
|
331
|
+
}
|
|
332
|
+
syncPreviews() {
|
|
333
|
+
const files = this.value || [];
|
|
334
|
+
this.previews.forEach((url, file) => {
|
|
335
|
+
if (!files.includes(file)) {
|
|
336
|
+
URL.revokeObjectURL(url);
|
|
337
|
+
this.previews.delete(file);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
if (!this.quiet)
|
|
341
|
+
return;
|
|
342
|
+
files.forEach(file => {
|
|
343
|
+
if (!this.previews.has(file) && file.type?.startsWith('image/')) {
|
|
344
|
+
this.previews.set(file, URL.createObjectURL(file));
|
|
152
345
|
}
|
|
153
|
-
const detail = e.detail;
|
|
154
|
-
this.value = this.multiple ? detail : detail[0] ? [detail[0]] : [];
|
|
155
|
-
this._notifyChange();
|
|
156
346
|
});
|
|
157
347
|
}
|
|
158
348
|
get files() {
|
|
159
349
|
return this.value;
|
|
160
350
|
}
|
|
351
|
+
/** Opens the file dialog — for an owner whose own button starts the pick. */
|
|
352
|
+
pick() {
|
|
353
|
+
if (this.disabled || this.readonly || this.uploading)
|
|
354
|
+
return;
|
|
355
|
+
this.fileInput?.click();
|
|
356
|
+
}
|
|
161
357
|
reset() {
|
|
162
|
-
this.fileInput
|
|
358
|
+
if (this.fileInput)
|
|
359
|
+
this.fileInput.value = '';
|
|
163
360
|
this.value = [];
|
|
361
|
+
this.refusals = [];
|
|
164
362
|
this._notifyChange();
|
|
165
363
|
}
|
|
166
|
-
|
|
167
|
-
const fileInput = e.currentTarget;
|
|
364
|
+
removeFile(file) {
|
|
168
365
|
const files = this.value || [];
|
|
169
|
-
this.value =
|
|
170
|
-
|
|
171
|
-
: [...Array.from(fileInput.files)];
|
|
172
|
-
fileInput.files = null;
|
|
366
|
+
this.value = files.filter(one => one !== file);
|
|
367
|
+
this.refusals = [];
|
|
173
368
|
this._notifyChange();
|
|
174
369
|
}
|
|
370
|
+
async take(picked) {
|
|
371
|
+
const files = this.value || [];
|
|
372
|
+
const keep = this.attachFileList;
|
|
373
|
+
const limits = await fileUploadLimits();
|
|
374
|
+
const { taken, refused } = sortPickedFiles(picked, {
|
|
375
|
+
accept: this.accept,
|
|
376
|
+
limits,
|
|
377
|
+
already: keep ? files.length : 0
|
|
378
|
+
});
|
|
379
|
+
this.refusals = refused;
|
|
380
|
+
this.refusalLimits = limits;
|
|
381
|
+
if (!taken.length)
|
|
382
|
+
return;
|
|
383
|
+
this.value = keep ? [...files, ...taken] : taken;
|
|
384
|
+
this._notifyChange();
|
|
385
|
+
}
|
|
386
|
+
_onChangeValue(e) {
|
|
387
|
+
const fileInput = e.currentTarget;
|
|
388
|
+
const picked = Array.from(fileInput.files);
|
|
389
|
+
/* The same file picked again must still raise change. */
|
|
390
|
+
fileInput.value = '';
|
|
391
|
+
this.take(picked);
|
|
392
|
+
}
|
|
175
393
|
_notifyChange() {
|
|
176
394
|
this.dispatchEvent(new CustomEvent('change', {
|
|
177
395
|
bubbles: true,
|
|
@@ -201,9 +419,24 @@ __decorate([
|
|
|
201
419
|
__decorate([
|
|
202
420
|
property({ type: Boolean, reflect: true, attribute: 'attach-filelist' })
|
|
203
421
|
], OxInputFile.prototype, "attachFileList", void 0);
|
|
422
|
+
__decorate([
|
|
423
|
+
property({ type: Boolean, reflect: true })
|
|
424
|
+
], OxInputFile.prototype, "quiet", void 0);
|
|
425
|
+
__decorate([
|
|
426
|
+
property({ attribute: false })
|
|
427
|
+
], OxInputFile.prototype, "dropTarget", void 0);
|
|
428
|
+
__decorate([
|
|
429
|
+
property({ type: Number })
|
|
430
|
+
], OxInputFile.prototype, "progress", void 0);
|
|
204
431
|
__decorate([
|
|
205
432
|
query('#input-file')
|
|
206
433
|
], OxInputFile.prototype, "fileInput", void 0);
|
|
434
|
+
__decorate([
|
|
435
|
+
state()
|
|
436
|
+
], OxInputFile.prototype, "refusals", void 0);
|
|
437
|
+
__decorate([
|
|
438
|
+
state()
|
|
439
|
+
], OxInputFile.prototype, "refusalLimits", void 0);
|
|
207
440
|
OxInputFile = __decorate([
|
|
208
441
|
customElement('ox-input-file')
|
|
209
442
|
], OxInputFile);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ox-input-file.js","sourceRoot":"","sources":["../../src/ox-input-file.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @license Copyright © HatioLab Inc. All rights reserved.\n */\n\nimport '@material/web/icon/icon.js'\n\nimport { css, html, PropertyValues } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\n\nimport { ScrollbarStyles } from '@operato/styles'\nimport { FileDropHelper } from '@operato/utils'\n\nimport { OxFormField } from './ox-form-field.js'\nimport { InputStyles } from './input-styles.js'\n\n@customElement('ox-input-file')\nexport class OxInputFile extends OxFormField {\n static styles = [\n InputStyles,\n ScrollbarStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n border-radius: var(--md-sys-shape-corner-small);\n align-items: center;\n justify-content: center;\n padding: calc(var(--ox-input-gap) * 2);\n min-height: 100px;\n text-transform: capitalize;\n\n border: 1px dashed var(--md-sys-color-outline-variant);\n background-color: var(--md-sys-color-surface);\n font-size: var(--md-sys-typescale-label-large-size, 0.875rem);\n }\n :host > span {\n color: var(--md-sys-color-primary);\n font-weight: var(--md-sys-typescale-label-medium-weight, var(--md-ref-typeface-weight-medium, 500));\n }\n\n :host > md-icon {\n color: var(--md-sys-color-primary);\n --md-icon-size: var(--file-uploader-icon-size, 36px);\n }\n\n :host(.candrop) {\n background-color: var(--md-sys-color-primary-fixed);\n border: 1px dashed var(--md-sys-color-primary-fixed-dim);\n }\n\n #input-file {\n display: none;\n }\n\n label {\n position: relative;\n width: auto;\n border: none;\n text-transform: capitalize;\n\n padding: var(--file-uploader-label-padding);\n border-radius: var(--file-uploader-label-border-radius);\n font-size: var(--md-sys-typescale-label-large-size, 0.875rem);\n color: var(--md-sys-color-on-surface-variant);\n }\n\n ul {\n max-width: 500px;\n width: 100%;\n list-style: none;\n margin: 5px 0 0 0;\n padding: 0;\n max-height: 46px;\n overflow: auto;\n background-color: var(--md-sys-color-on-primary);\n }\n li {\n text-align: left;\n\n padding: 3px 5px 2px 5px;\n border-bottom: var(--file-uploader-li-border-bottom);\n font-size: var(--md-sys-typescale-label-large-size, 0.875rem);\n color: var(--md-sys-color-on-primary-container);\n display: flex;\n align-items: center;\n }\n li md-icon {\n cursor: pointer;\n font-size: var(--icon-size-small);\n margin-left: auto;\n }\n li md-icon:hover,\n li md-icon:active {\n color: var(--md-sys-color-error);\n }\n `\n ]\n\n @property({ type: Boolean }) multiple?: boolean\n @property({ type: String }) accept?: string\n @property({ type: String }) icon?: string = 'upload'\n @property({ type: String }) label?: string\n @property({ type: String }) description?: string = 'drop files here!'\n @property({ type: Boolean, reflect: true, attribute: 'hide-filelist' })\n hideFileList: boolean = false\n @property({ type: Boolean, reflect: true, attribute: 'attach-filelist' })\n attachFileList: boolean = false\n\n @query('#input-file') fileInput!: HTMLInputElement\n\n render() {\n var files: File[] = this.value || []\n\n return html`\n <md-icon>${this.icon || 'upload'}</md-icon>\n\n <span>${this.description || 'drop files here!'}</span>\n\n <input\n id=\"input-file\"\n type=\"file\"\n accept=${this.accept}\n ?multiple=${this.multiple}\n hidden\n capture=\"environment\"\n @change=${(e: Event) => this._onChangeValue(e)}\n ?disabled=${this.disabled}\n />\n\n <label for=\"input-file\">${this.label || 'select files'}</label>\n\n ${!this.hideFileList && files?.length\n ? html` <ul>\n ${files.map(\n file => html`\n <li>\n - ${file.name}\n <md-icon\n @click=${(e: Event) => {\n if (this.disabled) {\n return\n }\n files.splice(files.indexOf(file), 1)\n this.value = [...files]\n this._notifyChange()\n }}\n >delete_outline</md-icon\n >\n </li>\n `\n )}\n </ul>`\n : html``}\n `\n }\n\n updated(changes: PropertyValues<this>) {\n if (changes.has('value') && !(this.value instanceof Array)) {\n this.value = !this.value ? [] : [this.value]\n }\n }\n\n firstUpdated() {\n FileDropHelper.set(this)\n\n this.addEventListener('file-drop', (e: Event) => {\n if (this.disabled) {\n return\n }\n\n const detail = (e as CustomEvent).detail\n this.value = this.multiple ? detail : detail[0] ? [detail[0]] : []\n\n this._notifyChange()\n })\n }\n\n get files() {\n return this.value\n }\n\n reset() {\n this.fileInput.value = ''\n this.value = []\n\n this._notifyChange()\n }\n\n _onChangeValue(e: Event) {\n const fileInput = e.currentTarget as HTMLInputElement\n const files = this.value || []\n\n this.value = this.attachFileList\n ? [...files, ...Array.from(fileInput.files as FileList)]\n : [...Array.from(fileInput.files as FileList)]\n\n fileInput.files = null\n\n this._notifyChange()\n }\n\n _notifyChange() {\n this.dispatchEvent(\n new CustomEvent('change', {\n bubbles: true,\n composed: true,\n detail: this.value\n })\n )\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ox-input-file.js","sourceRoot":"","sources":["../../src/ox-input-file.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @license Copyright © HatioLab Inc. All rights reserved.\n */\n\nimport '@material/web/icon/icon.js'\n\nimport { css, html, nothing, PropertyValues } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\n\nimport { i18next } from '@operato/i18n'\nimport { ButtonStyles, ScrollbarStyles } from '@operato/styles'\nimport { FileDropHelper, FileRefusal, fileUploadLimits, sortPickedFiles } from '@operato/utils'\n\nimport { OxFormField } from './ox-form-field.js'\nimport { InputStyles } from './input-styles.js'\n\nconst megabytes = (bytes: number) =>\n bytes >= 1048576 ? `${Math.round((bytes / 1048576) * 10) / 10}MB` : `${Math.max(1, Math.round(bytes / 1024))}KB`\n\n/**\n * Picks files — the one picker for every screen.\n *\n * Two looks:\n * (default) a drop box, for a screen where the file is the subject (an import, an attachment gallery)\n * quiet text buttons only — 「첨부」, then the picked name with 「바꾸기」「빼기」 — for a screen that is\n * read first (a 360 section, a form row). The drop area is `dropTarget` (the section) and\n * shows only while a file is dragged over it.\n *\n * Both refuse on the spot what the server would refuse after the upload: a type outside `accept`\n * (a drop skips the native filter), a file over the server's size limit, more files than it takes.\n * The limits are the server's own numbers (`fileUploadLimits` in @operato/utils), never written here.\n *\n * The owner uploads; while it does, it sets `progress` (0‒1) and the picker shows it.\n */\n@customElement('ox-input-file')\nexport class OxInputFile extends OxFormField {\n static styles = [\n InputStyles,\n ScrollbarStyles,\n ButtonStyles,\n css`\n :host(:not([quiet])) {\n display: flex;\n flex-direction: column;\n border-radius: var(--md-sys-shape-corner-small);\n align-items: center;\n justify-content: center;\n padding: calc(var(--ox-input-gap) * 2);\n min-height: 100px;\n text-transform: capitalize;\n\n border: 1px dashed var(--md-sys-color-outline-variant);\n background-color: var(--md-sys-color-surface);\n font-size: var(--md-sys-typescale-label-large-size, 0.875rem);\n }\n :host(:not([quiet])) > span {\n color: var(--md-sys-color-primary);\n font-weight: var(--md-sys-typescale-label-medium-weight, var(--md-ref-typeface-weight-medium, 500));\n }\n\n :host(:not([quiet])) > md-icon {\n color: var(--md-sys-color-primary);\n --md-icon-size: var(--file-uploader-icon-size, 36px);\n }\n\n :host(.candrop:not([quiet])) {\n background-color: var(--md-sys-color-primary-fixed);\n border: 1px dashed var(--md-sys-color-primary-fixed-dim);\n }\n\n #input-file {\n display: none;\n }\n\n label {\n position: relative;\n width: auto;\n border: none;\n text-transform: capitalize;\n\n padding: var(--file-uploader-label-padding);\n border-radius: var(--file-uploader-label-border-radius);\n font-size: var(--md-sys-typescale-label-large-size, 0.875rem);\n color: var(--md-sys-color-on-surface-variant);\n }\n\n ul {\n max-width: 500px;\n width: 100%;\n list-style: none;\n margin: 5px 0 0 0;\n padding: 0;\n max-height: 46px;\n overflow: auto;\n background-color: var(--md-sys-color-on-primary);\n }\n li {\n text-align: left;\n\n padding: 3px 5px 2px 5px;\n border-bottom: var(--file-uploader-li-border-bottom);\n font-size: var(--md-sys-typescale-label-large-size, 0.875rem);\n color: var(--md-sys-color-on-primary-container);\n display: flex;\n align-items: center;\n }\n li md-icon {\n cursor: pointer;\n font-size: var(--icon-size-small);\n margin-left: auto;\n }\n li md-icon:hover,\n li md-icon:active {\n color: var(--md-sys-color-error);\n }\n\n /* ── quiet ── no box: the words are the control. */\n :host([quiet]) {\n display: inline-flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--spacing-small, 4px);\n font-size: var(--ui-t-body, 13px);\n color: var(--md-sys-color-on-surface);\n border-radius: var(--md-sys-shape-corner-small, 4px);\n }\n\n :host([quiet].candrop) {\n outline: 1px dashed var(--md-sys-color-primary);\n outline-offset: 2px;\n }\n\n .picked {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-small, 4px);\n min-width: 0;\n }\n\n .picked .name {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n max-width: var(--ox-input-file-name-width, 24em);\n }\n\n .picked .size,\n .progress {\n color: var(--md-sys-color-on-surface-variant);\n font-variant-numeric: tabular-nums;\n }\n\n .picked md-icon {\n --md-icon-size: 16px;\n color: var(--md-sys-color-on-surface-variant);\n }\n\n .picked img {\n width: 20px;\n height: 20px;\n object-fit: cover;\n border-radius: 2px;\n }\n\n .refusal {\n flex-basis: 100%;\n color: var(--ui-crit, var(--md-sys-color-error));\n }\n `\n ]\n\n @property({ type: Boolean }) multiple?: boolean\n @property({ type: String }) accept?: string\n @property({ type: String }) icon?: string = 'upload'\n @property({ type: String }) label?: string\n @property({ type: String }) description?: string\n @property({ type: Boolean, reflect: true, attribute: 'hide-filelist' })\n hideFileList: boolean = false\n @property({ type: Boolean, reflect: true, attribute: 'attach-filelist' })\n attachFileList: boolean = false\n\n /** Text buttons instead of the drop box — for a screen that is read first. */\n @property({ type: Boolean, reflect: true }) quiet: boolean = false\n\n /** Where a file may be dropped. The picker itself when not given; a 360 passes its section. */\n @property({ attribute: false }) dropTarget?: HTMLElement\n\n /** Upload progress 0‒1, set by the owner while it uploads; the buttons wait meanwhile. */\n @property({ type: Number }) progress?: number\n\n @query('#input-file') fileInput!: HTMLInputElement\n\n @state() private refusals: FileRefusal[] = []\n @state() private refusalLimits: { maxFileSize?: number; maxFiles?: number } = {}\n\n private dropArea?: HTMLElement\n private undoDrop?: () => void\n private previews = new Map<File, string>()\n\n private onFileDrop = (e: Event) => {\n if (this.disabled || this.readonly || this.uploading) {\n return\n }\n\n const files = (e as CustomEvent).detail as File[]\n this.take(this.multiple ? files : files.slice(0, 1))\n }\n\n private get uploading() {\n return typeof this.progress === 'number'\n }\n\n render() {\n return this.quiet ? this.renderQuiet() : this.renderBox()\n }\n\n private renderInput() {\n return html`<input\n id=\"input-file\"\n type=\"file\"\n accept=${this.accept || ''}\n ?multiple=${this.multiple}\n hidden\n capture=\"environment\"\n @change=${(e: Event) => this._onChangeValue(e)}\n ?disabled=${this.disabled || this.readonly}\n />`\n }\n\n private renderBox() {\n var files: File[] = this.value || []\n\n return html`\n <md-icon>${this.icon || 'upload'}</md-icon>\n\n <span>${this.description || i18next.t('text.drop files here', { defaultValue: 'drop files here!' })}</span>\n\n ${this.renderInput()}\n\n <label for=\"input-file\">${this.label || i18next.t('label.select files', { defaultValue: 'select files' })}</label>\n\n ${!this.hideFileList && files?.length\n ? html` <ul>\n ${files.map(\n file => html`\n <li>\n - ${file.name}\n <md-icon\n @click=${(e: Event) => {\n if (this.disabled) {\n return\n }\n this.removeFile(file)\n }}\n >delete_outline</md-icon\n >\n </li>\n `\n )}\n </ul>`\n : html``}\n ${this.renderRefusals()} ${this.renderProgress()}\n `\n }\n\n private renderQuiet() {\n const files: File[] = this.value || []\n const busy = this.disabled || this.readonly || this.uploading\n const shown = this.hideFileList ? [] : files\n\n return html`\n ${this.renderInput()}\n ${shown.map(\n file => html`<span class=\"picked\">\n ${this.renderKind(file)}\n <span class=\"name\" title=${file.name}>${file.name}</span>\n <span class=\"size\">${megabytes(file.size)}</span>\n ${!this.multiple\n ? html`<button type=\"button\" quiet small ?disabled=${busy} @click=${() => this.pick()}>\n ${i18next.t('label.replace file', { defaultValue: 'replace' })}\n </button>`\n : nothing}\n <button type=\"button\" quiet small danger ?disabled=${busy} @click=${() => this.removeFile(file)}>\n ${i18next.t('label.remove file', { defaultValue: 'remove' })}\n </button>\n </span>`\n )}\n ${this.multiple || !shown.length\n ? html`<button type=\"button\" quiet small ?disabled=${busy} @click=${() => this.pick()}>\n ${this.label || i18next.t('label.attach file', { defaultValue: 'attach' })}\n </button>`\n : nothing}\n ${this.renderProgress()} ${this.renderRefusals()}\n `\n }\n\n /* A picture shows itself; anything else is told by its kind. */\n private renderKind(file: File) {\n const preview = this.previews.get(file)\n if (preview) return html`<img src=${preview} alt=\"\" />`\n\n const icon = file.type === 'application/pdf' ? 'picture_as_pdf' : 'draft'\n return html`<md-icon>${icon}</md-icon>`\n }\n\n private renderProgress() {\n if (!this.uploading) return nothing\n\n const percent = Math.round(Math.min(1, Math.max(0, this.progress!)) * 100)\n return html`<span class=\"progress\" role=\"status\">\n ${i18next.t('text.uploading file', { percent, defaultValue: `uploading ${percent}%` })}\n </span>`\n }\n\n private renderRefusals() {\n if (!this.refusals.length) return nothing\n\n const { maxFileSize, maxFiles } = this.refusalLimits\n return html`<div class=\"refusal\" role=\"alert\">\n ${this.refusals.map(({ file, reason }) =>\n reason === 'type'\n ? i18next.t('text.file refused for type', { name: file.name, defaultValue: `${file.name}: not a type taken here` })\n : reason === 'size'\n ? i18next.t('text.file refused for size', {\n name: file.name,\n limit: megabytes(maxFileSize!),\n defaultValue: `${file.name}: over ${megabytes(maxFileSize!)}`\n })\n : i18next.t('text.file refused for count', {\n name: file.name,\n limit: maxFiles,\n defaultValue: `${file.name}: ${maxFiles} files at most`\n })\n ).join(' · ')}\n </div>`\n }\n\n updated(changes: PropertyValues<this>) {\n if (changes.has('value') && !(this.value instanceof Array)) {\n this.value = !this.value ? [] : [this.value]\n }\n\n if (changes.has('value')) {\n this.syncPreviews()\n }\n\n if (changes.has('dropTarget')) {\n this.setDropArea()\n }\n }\n\n connectedCallback() {\n super.connectedCallback()\n if (this.hasUpdated) this.setDropArea()\n }\n\n firstUpdated() {\n this.setDropArea()\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n\n this.undoDrop?.()\n this.dropArea?.removeEventListener('file-drop', this.onFileDrop)\n this.undoDrop = this.dropArea = undefined\n\n this.previews.forEach(url => URL.revokeObjectURL(url))\n this.previews.clear()\n }\n\n private setDropArea() {\n const area: HTMLElement = this.dropTarget || (this as HTMLElement)\n if (area === this.dropArea) return\n\n this.undoDrop?.()\n this.dropArea?.removeEventListener('file-drop', this.onFileDrop)\n\n this.dropArea = area\n this.undoDrop = FileDropHelper.set(area)\n area.addEventListener('file-drop', this.onFileDrop)\n }\n\n private syncPreviews() {\n const files: File[] = this.value || []\n\n this.previews.forEach((url, file) => {\n if (!files.includes(file)) {\n URL.revokeObjectURL(url)\n this.previews.delete(file)\n }\n })\n\n if (!this.quiet) return\n\n files.forEach(file => {\n if (!this.previews.has(file) && file.type?.startsWith('image/')) {\n this.previews.set(file, URL.createObjectURL(file))\n }\n })\n }\n\n get files() {\n return this.value\n }\n\n /** Opens the file dialog — for an owner whose own button starts the pick. */\n pick() {\n if (this.disabled || this.readonly || this.uploading) return\n this.fileInput?.click()\n }\n\n reset() {\n if (this.fileInput) this.fileInput.value = ''\n this.value = []\n this.refusals = []\n\n this._notifyChange()\n }\n\n private removeFile(file: File) {\n const files: File[] = this.value || []\n\n this.value = files.filter(one => one !== file)\n this.refusals = []\n this._notifyChange()\n }\n\n private async take(picked: File[]) {\n const files: File[] = this.value || []\n const keep = this.attachFileList\n const limits = await fileUploadLimits()\n\n const { taken, refused } = sortPickedFiles(picked, {\n accept: this.accept,\n limits,\n already: keep ? files.length : 0\n })\n\n this.refusals = refused\n this.refusalLimits = limits\n\n if (!taken.length) return\n\n this.value = keep ? [...files, ...taken] : taken\n this._notifyChange()\n }\n\n _onChangeValue(e: Event) {\n const fileInput = e.currentTarget as HTMLInputElement\n const picked = Array.from(fileInput.files as FileList)\n\n /* The same file picked again must still raise change. */\n fileInput.value = ''\n\n this.take(picked)\n }\n\n _notifyChange() {\n this.dispatchEvent(\n new CustomEvent('change', {\n bubbles: true,\n composed: true,\n detail: this.value\n })\n )\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '../src/ox-input-file.js';
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { aTimeout, expect, fixture } from '@open-wc/testing';
|
|
2
|
+
import { html } from 'lit';
|
|
3
|
+
import { registerFileUploadLimits } from '@operato/utils';
|
|
4
|
+
import '../src/ox-input-file.js';
|
|
5
|
+
/**
|
|
6
|
+
* The one file picker.
|
|
7
|
+
*
|
|
8
|
+
* ## Why this is tested
|
|
9
|
+
*
|
|
10
|
+
* Every screen that takes a file goes through this element, and a 360 uses its quiet look. What
|
|
11
|
+
* must hold:
|
|
12
|
+
*
|
|
13
|
+
* ```
|
|
14
|
+
* quiet no box: an attach button; once picked, the name with replace and remove
|
|
15
|
+
* refusal a type outside accept or a file over the server's limit never becomes the value,
|
|
16
|
+
* and says so in place — a drop skips the native accept filter
|
|
17
|
+
* drop target a drop lands on the element the owner gives (a 360 section), and only there
|
|
18
|
+
* progress while the owner uploads, the buttons wait and the percent shows
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
describe('ox-input-file', () => {
|
|
22
|
+
const file = (name, type, size = 10) => new File([new Uint8Array(size)], name, { type });
|
|
23
|
+
const drop = (area, ...files) => {
|
|
24
|
+
const dataTransfer = new DataTransfer();
|
|
25
|
+
files.forEach(one => dataTransfer.items.add(one));
|
|
26
|
+
area.dispatchEvent(new DragEvent('drop', { dataTransfer, bubbles: true, cancelable: true }));
|
|
27
|
+
};
|
|
28
|
+
const settle = async (el) => {
|
|
29
|
+
await aTimeout(0);
|
|
30
|
+
await el.updateComplete;
|
|
31
|
+
};
|
|
32
|
+
const buttons = (el) => [...el.renderRoot.querySelectorAll('button')];
|
|
33
|
+
/* The theme token the box border is drawn with; without it the border falls away and every look reads 'none'. */
|
|
34
|
+
before(() => document.documentElement.style.setProperty('--md-sys-color-outline-variant', 'gray'));
|
|
35
|
+
after(() => document.documentElement.style.removeProperty('--md-sys-color-outline-variant'));
|
|
36
|
+
afterEach(() => registerFileUploadLimits(async () => ({})));
|
|
37
|
+
it('draws no box when quiet, and shows the picked name with replace and remove', async () => {
|
|
38
|
+
const el = await fixture(html `<ox-input-file quiet accept="image/*,application/pdf"></ox-input-file>`);
|
|
39
|
+
expect(getComputedStyle(el).borderTopStyle, 'quiet still draws a box').to.equal('none');
|
|
40
|
+
expect(buttons(el).length, 'one attach button before a pick').to.equal(1);
|
|
41
|
+
let changes = 0;
|
|
42
|
+
el.addEventListener('change', () => changes++);
|
|
43
|
+
drop(el, file('paper.pdf', 'application/pdf'));
|
|
44
|
+
await settle(el);
|
|
45
|
+
expect(el.value.map((one) => one.name)).to.deep.equal(['paper.pdf']);
|
|
46
|
+
expect(el.renderRoot.querySelector('.picked .name').textContent).to.equal('paper.pdf');
|
|
47
|
+
expect(buttons(el).length, 'replace and remove').to.equal(2);
|
|
48
|
+
const remove = buttons(el).find(one => one.hasAttribute('danger'));
|
|
49
|
+
remove.click();
|
|
50
|
+
await el.updateComplete;
|
|
51
|
+
expect(el.value).to.deep.equal([]);
|
|
52
|
+
expect(changes).to.equal(2);
|
|
53
|
+
});
|
|
54
|
+
it('refuses a type outside accept and a file over the server limit, in place', async () => {
|
|
55
|
+
registerFileUploadLimits(async () => ({ maxFileSize: 20 }));
|
|
56
|
+
const el = await fixture(html `<ox-input-file quiet multiple accept="image/*"></ox-input-file>`);
|
|
57
|
+
drop(el, file('note.txt', 'text/plain'), file('big.png', 'image/png', 50), file('ok.png', 'image/png', 5));
|
|
58
|
+
await settle(el);
|
|
59
|
+
expect(el.value.map((one) => one.name)).to.deep.equal(['ok.png']);
|
|
60
|
+
const refusal = el.renderRoot.querySelector('[role="alert"]');
|
|
61
|
+
expect(refusal, 'no refusal shown').to.exist;
|
|
62
|
+
expect(refusal.textContent).to.contain('note.txt');
|
|
63
|
+
expect(refusal.textContent).to.contain('big.png');
|
|
64
|
+
});
|
|
65
|
+
it('takes a drop on the given target and lets go of the old one', async () => {
|
|
66
|
+
const section = document.createElement('section');
|
|
67
|
+
const other = document.createElement('section');
|
|
68
|
+
document.body.append(section, other);
|
|
69
|
+
const el = await fixture(html `<ox-input-file quiet .dropTarget=${section}></ox-input-file>`);
|
|
70
|
+
drop(section, file('a.pdf', 'application/pdf'));
|
|
71
|
+
await settle(el);
|
|
72
|
+
expect(el.value.map((one) => one.name)).to.deep.equal(['a.pdf']);
|
|
73
|
+
el.dropTarget = other;
|
|
74
|
+
await el.updateComplete;
|
|
75
|
+
drop(section, file('b.pdf', 'application/pdf'));
|
|
76
|
+
await settle(el);
|
|
77
|
+
expect(el.value.map((one) => one.name), 'the old target still takes drops').to.deep.equal(['a.pdf']);
|
|
78
|
+
section.dispatchEvent(new DragEvent('dragenter', { bubbles: true }));
|
|
79
|
+
expect(section.classList.contains('candrop'), 'the old target still lights up').to.equal(false);
|
|
80
|
+
drop(other, file('c.pdf', 'application/pdf'));
|
|
81
|
+
await settle(el);
|
|
82
|
+
expect(el.value.map((one) => one.name)).to.deep.equal(['c.pdf']);
|
|
83
|
+
section.remove();
|
|
84
|
+
other.remove();
|
|
85
|
+
});
|
|
86
|
+
it('waits while the owner uploads and shows the percent', async () => {
|
|
87
|
+
const el = await fixture(html `<ox-input-file quiet .progress=${0.4}></ox-input-file>`);
|
|
88
|
+
expect(buttons(el).every(one => one.disabled), 'a button works mid-upload').to.equal(true);
|
|
89
|
+
expect(el.renderRoot.querySelector('[role="status"]').textContent).to.contain('40');
|
|
90
|
+
drop(el, file('a.pdf', 'application/pdf'));
|
|
91
|
+
await settle(el);
|
|
92
|
+
expect(el.value ?? [], 'a drop landed mid-upload').to.deep.equal([]);
|
|
93
|
+
});
|
|
94
|
+
it('keeps the drop box when not quiet', async () => {
|
|
95
|
+
const el = await fixture(html `<ox-input-file></ox-input-file>`);
|
|
96
|
+
expect(getComputedStyle(el).borderTopStyle).to.equal('dashed');
|
|
97
|
+
expect(el.renderRoot.querySelector('label[for="input-file"]')).to.exist;
|
|
98
|
+
drop(el, file('a.bin', ''));
|
|
99
|
+
await settle(el);
|
|
100
|
+
expect(el.renderRoot.querySelectorAll('li').length).to.equal(1);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
//# sourceMappingURL=ox-input-file.test.js.map
|