@kubex/zinc 1.1.67 → 1.1.69
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/custom-elements.json +1288 -20
- package/dist/vscode.html-custom-data.json +167 -11
- package/dist/web-types.json +360 -17
- package/dist/zn.d.ts +368 -20
- package/dist/zn.min.js +764 -645
- package/docs/pages/components/collapsible.md +6 -2
- package/docs/pages/components/preview-frame-demo.njk +33 -4
- package/docs/pages/components/preview-frame.md +20 -0
- package/docs/pages/components/theme-editor.md +269 -0
- package/docs/superpowers/plans/2026-08-03-theme-editor.md +1536 -0
- package/docs/superpowers/specs/2026-08-03-theme-editor-design.md +327 -0
- package/package.json +1 -1
- package/src/components/collapsible/collapsible.component.ts +21 -23
- package/src/components/collapsible/collapsible.scss +10 -0
- package/src/components/data-table/data-table.component.ts +49 -44
- package/src/components/data-table/data-table.scss +4 -0
- package/src/components/data-table/data-table.test.ts +42 -0
- package/src/components/defined-label/defined-label.component.ts +118 -95
- package/src/components/defined-label/defined-label.scss +40 -28
- package/src/components/dropdown/dropdown.component.ts +9 -0
- package/src/components/editor/editor.component.ts +20 -21
- package/src/components/file/file.component.ts +70 -0
- package/src/components/page-builder/page-builder.scss +1 -1
- package/src/components/preview-frame/preview-frame.component.ts +126 -16
- package/src/components/preview-frame/preview-frame.scss +27 -0
- package/src/components/preview-frame/preview-frame.test.ts +253 -0
- package/src/components/theme-editor/index.ts +12 -0
- package/src/components/theme-editor/theme-editor.component.ts +761 -0
- package/src/components/theme-editor/theme-editor.scss +309 -0
- package/src/components/theme-editor/theme-editor.test.ts +1584 -0
- package/src/events/events.ts +2 -0
- package/src/events/zn-theme-change.ts +13 -0
- package/src/events/zn-theme-submit.ts +9 -0
- package/src/types/web-test-runner-commands.d.ts +6 -0
- package/src/zinc.ts +1 -0
- package/tsconfig.json +7 -1
- package/web-test-runner.config.js +3 -1
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
import {type CSSResultGroup, html, type PropertyValues, type TemplateResult, unsafeCSS} from 'lit';
|
|
1
|
+
import {type CSSResultGroup, html, nothing, type PropertyValues, type TemplateResult, unsafeCSS} from 'lit';
|
|
2
2
|
import {FormControlController} from '../../internal/form';
|
|
3
3
|
import {ifDefined} from 'lit/directives/if-defined.js';
|
|
4
4
|
import {property, query} from 'lit/decorators.js';
|
|
5
5
|
import {watch} from '../../internal/watch';
|
|
6
6
|
import ZincElement from '../../internal/zinc-element';
|
|
7
|
+
import ZnButton from '../button';
|
|
8
|
+
import ZnDropdown from '../dropdown';
|
|
9
|
+
import ZnInput from '../input';
|
|
10
|
+
import ZnOption from '../option';
|
|
11
|
+
import ZnSelect from '../select';
|
|
7
12
|
import type {ZincFormControl} from '../../internal/zinc-element';
|
|
8
|
-
import type ZnDropdown from '../dropdown';
|
|
9
|
-
import type ZnInput from '../input';
|
|
10
13
|
|
|
11
14
|
import styles from './defined-label.scss';
|
|
12
15
|
|
|
16
|
+
interface PredefinedLabel {
|
|
17
|
+
name: string;
|
|
18
|
+
options?: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
13
21
|
/**
|
|
14
22
|
* @summary This component provides a labeled input with support for predefined and custom labels,
|
|
15
23
|
* allowing users to select or enter label-value pairs within a dropdown interface.
|
|
@@ -21,31 +29,51 @@ import styles from './defined-label.scss';
|
|
|
21
29
|
* @dependency zn-dropdown
|
|
22
30
|
* @dependency zn-input
|
|
23
31
|
* @dependency zn-option
|
|
24
|
-
* @dependency zn-panel
|
|
25
32
|
* @dependency zn-select
|
|
26
|
-
* @dependency zn-sp
|
|
27
33
|
*
|
|
28
34
|
* @csspart input - The component's main input.
|
|
29
35
|
* @csspart input-value - The label's value inputs.
|
|
30
36
|
*/
|
|
31
37
|
export default class ZnDefinedLabel extends ZincElement implements ZincFormControl {
|
|
32
38
|
static styles: CSSResultGroup = unsafeCSS(styles);
|
|
39
|
+
static dependencies = {
|
|
40
|
+
'zn-button': ZnButton,
|
|
41
|
+
'zn-dropdown': ZnDropdown,
|
|
42
|
+
'zn-input': ZnInput,
|
|
43
|
+
'zn-option': ZnOption,
|
|
44
|
+
'zn-select': ZnSelect
|
|
45
|
+
};
|
|
33
46
|
|
|
34
47
|
private readonly formControlController = new FormControlController(this, {
|
|
35
48
|
value: (control: this) => control.value + (control.inputValue ? `:${control.inputValue}` : ''),
|
|
36
49
|
});
|
|
37
50
|
|
|
38
|
-
@query('.
|
|
51
|
+
@query('.defined-label__input') input: ZnInput;
|
|
39
52
|
@query('.defined-label__dropdown') dropdown: ZnDropdown;
|
|
40
53
|
|
|
54
|
+
/** The selected label key. Also acts as the filter while typing. */
|
|
41
55
|
@property() value: string = '';
|
|
56
|
+
|
|
57
|
+
/** The value entered for the selected label. Submitted as `label:value` when present. */
|
|
42
58
|
@property() inputValue: string = '';
|
|
59
|
+
|
|
60
|
+
/** The size of the main input. */
|
|
43
61
|
@property({attribute: 'input-size', reflect: true}) inputSize: 'x-small' | 'small' | 'medium' | 'large' = 'medium';
|
|
62
|
+
|
|
63
|
+
/** The name of the control. Used for form submission. */
|
|
44
64
|
@property() name: string = 'label';
|
|
65
|
+
|
|
66
|
+
/** The title of the main input. */
|
|
45
67
|
@property() title: string;
|
|
68
|
+
|
|
69
|
+
/** Disables the control. */
|
|
46
70
|
@property({type: Boolean}) disabled: boolean = false;
|
|
71
|
+
|
|
72
|
+
/** Allows labels that aren't in the predefined list. */
|
|
47
73
|
@property({attribute: 'allow-custom', type: Boolean}) allowCustom: boolean = false;
|
|
48
|
-
|
|
74
|
+
|
|
75
|
+
/** The predefined labels. Entries are either a string or `{name, options}`. */
|
|
76
|
+
@property({type: Array, attribute: 'predefined-labels'}) predefinedLabels: (PredefinedLabel | string)[] = [];
|
|
49
77
|
|
|
50
78
|
get validationMessage() {
|
|
51
79
|
return this.input.validationMessage;
|
|
@@ -83,6 +111,14 @@ export default class ZnDefinedLabel extends ZincElement implements ZincFormContr
|
|
|
83
111
|
this.formControlController.updateValidity();
|
|
84
112
|
}
|
|
85
113
|
|
|
114
|
+
private getFilteredLabels(): PredefinedLabel[] {
|
|
115
|
+
const filter = this.value.toLowerCase();
|
|
116
|
+
return this.predefinedLabels
|
|
117
|
+
.map(label => typeof label === 'string' ? {name: label} : label)
|
|
118
|
+
.filter((label): label is PredefinedLabel => Boolean(label?.name))
|
|
119
|
+
.filter(label => !filter || label.name.toLowerCase().includes(filter));
|
|
120
|
+
}
|
|
121
|
+
|
|
86
122
|
private handleChange() {
|
|
87
123
|
if (!this.dropdown.open) {
|
|
88
124
|
this.dropdown.show().then(r => r);
|
|
@@ -117,14 +153,18 @@ export default class ZnDefinedLabel extends ZincElement implements ZincFormContr
|
|
|
117
153
|
if (target.hasAttribute('data-label')) this.value = target.getAttribute('data-label') ?? "";
|
|
118
154
|
}
|
|
119
155
|
|
|
120
|
-
private
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
156
|
+
private handleFormSubmit(e: Event, label?: string) {
|
|
157
|
+
// Sync from the submitted row so a partially typed filter never wins over
|
|
158
|
+
// the row's actual label key, and stale values from other rows are discarded
|
|
159
|
+
const row = (e.currentTarget as HTMLElement).closest('.defined-label__row');
|
|
160
|
+
const control = row?.querySelector<ZnInput | ZnSelect>('.defined-label__value');
|
|
161
|
+
if (label) {
|
|
162
|
+
this.value = label;
|
|
163
|
+
}
|
|
164
|
+
if (control) {
|
|
165
|
+
this.inputValue = String(control.value ?? '').toLowerCase();
|
|
166
|
+
}
|
|
126
167
|
|
|
127
|
-
private handleFormSubmit() {
|
|
128
168
|
const form = this.formControlController.getForm();
|
|
129
169
|
|
|
130
170
|
if (form && form.reportValidity()) {
|
|
@@ -135,106 +175,89 @@ export default class ZnDefinedLabel extends ZincElement implements ZincFormContr
|
|
|
135
175
|
}
|
|
136
176
|
}
|
|
137
177
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
let selector: TemplateResult<1>;
|
|
155
|
-
if (options && options.length > 0) {
|
|
156
|
-
selector = html`
|
|
157
|
-
<zn-select
|
|
158
|
-
part="input-value"
|
|
159
|
-
id="input-value input-value-${label}"
|
|
160
|
-
class="input__control-value input__control-value--${label}"
|
|
161
|
-
data-label="${label}"
|
|
162
|
-
@zn-change="${this.handleInputValueChange}"
|
|
163
|
-
@zn-input="${this.handleInputValueInput}"
|
|
164
|
-
size="small">
|
|
165
|
-
<zn-option value="">Select ${label}</zn-option>
|
|
166
|
-
${options.map((option: string) => html`
|
|
167
|
-
<zn-option value="${option}">${option}</zn-option>
|
|
168
|
-
`)}
|
|
169
|
-
</zn-select>`;
|
|
170
|
-
} else {
|
|
171
|
-
selector = html`
|
|
172
|
-
<zn-input
|
|
173
|
-
part="input-value"
|
|
174
|
-
id="input-value input-value-${label}"
|
|
175
|
-
class="input__control-value input__control-value--${label}"
|
|
176
|
-
type="text"
|
|
177
|
-
data-label="${label}"
|
|
178
|
-
@zn-change="${this.handleInputValueChange}"
|
|
179
|
-
@zn-input="${this.handleInputValueInput}"
|
|
180
|
-
size="small"></zn-input>`;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
predefinedLabels = html`
|
|
184
|
-
<div class="defined-label__input">
|
|
185
|
-
<small>${label}</small>
|
|
186
|
-
<div class="defined-label__input-wrap">
|
|
187
|
-
${selector}
|
|
188
|
-
<zn-button type="submit" icon="add" @click="${this.handleFormSubmit}"></zn-button>
|
|
189
|
-
</div>
|
|
190
|
-
</div>`;
|
|
191
|
-
});
|
|
178
|
+
private renderValueControl(label: PredefinedLabel): TemplateResult {
|
|
179
|
+
if (label.options && label.options.length > 0) {
|
|
180
|
+
return html`
|
|
181
|
+
<zn-select
|
|
182
|
+
part="input-value"
|
|
183
|
+
class="defined-label__value"
|
|
184
|
+
data-label="${label.name}"
|
|
185
|
+
size="small"
|
|
186
|
+
@zn-change="${this.handleInputValueChange}"
|
|
187
|
+
@zn-input="${this.handleInputValueChange}">
|
|
188
|
+
<zn-option value="">Select ${label.name}</zn-option>
|
|
189
|
+
${label.options.map(option => html`
|
|
190
|
+
<zn-option value="${option}">${option}</zn-option>`)}
|
|
191
|
+
</zn-select>`;
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
return html`
|
|
195
|
-
<zn-
|
|
195
|
+
<zn-input
|
|
196
|
+
part="input-value"
|
|
197
|
+
class="defined-label__value"
|
|
198
|
+
type="text"
|
|
199
|
+
placeholder="Label Value"
|
|
200
|
+
data-label="${label.name}"
|
|
201
|
+
size="small"
|
|
202
|
+
@zn-change="${this.handleInputValueChange}"
|
|
203
|
+
@zn-input="${this.handleInputValueChange}"></zn-input>`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private renderRow(name: string, control: TemplateResult, label?: string): TemplateResult {
|
|
207
|
+
return html`
|
|
208
|
+
<div class="defined-label__row">
|
|
209
|
+
<small class="defined-label__row-label">${name}</small>
|
|
210
|
+
<div class="defined-label__row-controls">
|
|
211
|
+
${control}
|
|
212
|
+
<zn-button
|
|
213
|
+
type="submit"
|
|
214
|
+
icon="add"
|
|
215
|
+
@click="${(e: Event) => this.handleFormSubmit(e, label)}"></zn-button>
|
|
216
|
+
</div>
|
|
217
|
+
</div>`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
render() {
|
|
221
|
+
const labels = this.getFilteredLabels();
|
|
222
|
+
|
|
223
|
+
return html`
|
|
224
|
+
<zn-dropdown class="defined-label__dropdown" sync="width">
|
|
196
225
|
<zn-input
|
|
197
226
|
part="input"
|
|
198
227
|
id="input"
|
|
199
|
-
class="
|
|
228
|
+
class="defined-label__input"
|
|
200
229
|
type="text"
|
|
201
|
-
title="${this.title}"
|
|
230
|
+
title="${ifDefined(this.title)}"
|
|
202
231
|
value="${this.value}"
|
|
203
232
|
name="${ifDefined(this.name)}"
|
|
204
233
|
placeholder="Add a Label"
|
|
205
234
|
maxlength="60"
|
|
206
235
|
autocomplete="off"
|
|
207
236
|
size="${this.inputSize}"
|
|
237
|
+
?disabled="${this.disabled}"
|
|
208
238
|
@zn-change="${this.handleChange}"
|
|
209
239
|
@zn-input="${this.handleInput}"
|
|
210
240
|
@click="${this.handleClick}"
|
|
211
241
|
slot="trigger"
|
|
212
242
|
></zn-input>
|
|
213
243
|
|
|
214
|
-
<
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
@zn-input="${this.handleInputValueInput}"></zn-input>
|
|
232
|
-
<zn-button type="submit" icon="add" @click="${this.handleFormSubmit}"></zn-button>
|
|
233
|
-
</div>
|
|
234
|
-
</div>` : ''}
|
|
235
|
-
|
|
236
|
-
</zn-sp>
|
|
237
|
-
</zn-panel>
|
|
244
|
+
<div class="defined-label__panel">
|
|
245
|
+
${labels.length > 0
|
|
246
|
+
? labels.map(label => this.renderRow(label.name, this.renderValueControl(label), label.name))
|
|
247
|
+
: html`
|
|
248
|
+
<div class="defined-label__empty">Cannot find any predefined labels</div>`}
|
|
249
|
+
|
|
250
|
+
${this.allowCustom && this.value !== '' ? this.renderRow(this.value, html`
|
|
251
|
+
<zn-input
|
|
252
|
+
part="input-value"
|
|
253
|
+
class="defined-label__value"
|
|
254
|
+
placeholder="Label Value"
|
|
255
|
+
type="text"
|
|
256
|
+
size="small"
|
|
257
|
+
maxlength="60"
|
|
258
|
+
@zn-change="${this.handleInputValueChange}"
|
|
259
|
+
@zn-input="${this.handleInputValueChange}"></zn-input>`) : nothing}
|
|
260
|
+
</div>
|
|
238
261
|
</zn-dropdown>
|
|
239
262
|
`;
|
|
240
263
|
}
|
|
@@ -4,40 +4,52 @@
|
|
|
4
4
|
display: contents;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
.defined-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
7
|
+
.defined-label__dropdown {
|
|
8
|
+
width: 100%;
|
|
9
|
+
}
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
.defined-label__panel {
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-direction: column;
|
|
14
|
+
min-width: 260px;
|
|
15
|
+
max-height: 275px;
|
|
16
|
+
overflow-y: auto;
|
|
17
|
+
overscroll-behavior: none;
|
|
18
|
+
background-color: rgb(var(--zn-panel));
|
|
19
|
+
border: 1px solid rgb(var(--zn-border-color));
|
|
20
|
+
border-radius: var(--zn-border-radius);
|
|
21
|
+
box-shadow: 0 4px 8px -4px rgba(33, 33, 33, 0.1);
|
|
22
|
+
}
|
|
15
23
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
width: 100%;
|
|
24
|
+
.defined-label__row {
|
|
25
|
+
display: flex;
|
|
26
|
+
flex-direction: column;
|
|
27
|
+
gap: var(--zn-spacing-x-small);
|
|
28
|
+
padding: var(--zn-spacing-small);
|
|
22
29
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
30
|
+
& + & {
|
|
31
|
+
border-top: 1px solid rgb(var(--zn-border-color));
|
|
26
32
|
}
|
|
33
|
+
}
|
|
27
34
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
.defined-label__row-label {
|
|
36
|
+
font-size: var(--zn-font-size-small);
|
|
37
|
+
color: rgb(var(--zn-text-muted));
|
|
38
|
+
}
|
|
32
39
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
40
|
+
.defined-label__row-controls {
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
gap: var(--zn-spacing-x-small);
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
45
|
+
.defined-label__value {
|
|
46
|
+
flex-grow: 1;
|
|
42
47
|
}
|
|
43
48
|
}
|
|
49
|
+
|
|
50
|
+
.defined-label__empty {
|
|
51
|
+
padding: var(--zn-spacing-small);
|
|
52
|
+
font-size: var(--zn-font-size-small);
|
|
53
|
+
color: rgb(var(--zn-text-muted));
|
|
54
|
+
text-align: center;
|
|
55
|
+
}
|
|
@@ -335,6 +335,15 @@ export default class ZnDropdown extends ZincElement {
|
|
|
335
335
|
this.emit('zn-hide');
|
|
336
336
|
topLayerManager.unregisterDropdown(this);
|
|
337
337
|
this.removeOpenListeners();
|
|
338
|
+
|
|
339
|
+
// Move focus out of the panel before it becomes aria-hidden/inert,
|
|
340
|
+
// otherwise the browser blocks aria-hidden on the focused item and warns
|
|
341
|
+
const active = (this.getRootNode() as Document | ShadowRoot).activeElement as HTMLElement | null;
|
|
342
|
+
if (active && this.panel.assignedElements({flatten: true}).some(el => el.contains(active))) {
|
|
343
|
+
this.focusOnTrigger();
|
|
344
|
+
active.blur();
|
|
345
|
+
}
|
|
346
|
+
|
|
338
347
|
this.panel.blur();
|
|
339
348
|
this.panel.hidden = true;
|
|
340
349
|
this.popup.active = false;
|
|
@@ -247,28 +247,27 @@ export default class ZnEditor extends ZincElement implements ZincFormControl {
|
|
|
247
247
|
onFileUploaded: () => {
|
|
248
248
|
window.onbeforeunload = () => null;
|
|
249
249
|
},
|
|
250
|
-
upload: (file: File) => {
|
|
250
|
+
upload: async (file: File) => {
|
|
251
251
|
window.onbeforeunload = () => 'You have unsaved changes. Are you sure you want to leave?';
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
});
|
|
252
|
+
|
|
253
|
+
const fd = new FormData();
|
|
254
|
+
fd.append('filename', file.name);
|
|
255
|
+
fd.append('size', file.size.toString());
|
|
256
|
+
fd.append('mimeType', file.type);
|
|
257
|
+
|
|
258
|
+
const res = await fetch(this.uploadAttachmentUrl, {method: 'POST', body: fd});
|
|
259
|
+
if (!res.ok) throw new Error(`Upload request failed: ${res.status}`);
|
|
260
|
+
const response = await res.json() as {
|
|
261
|
+
uploadPath: string;
|
|
262
|
+
uploadUrl: string;
|
|
263
|
+
originalFilename: string;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// resolve relative upload targets against the attachment url so both keep
|
|
267
|
+
// the same base path when the host page rewrites upload-attachment-url (e.g. the
|
|
268
|
+
// kubex console proxying apps under an app base); absolute urls pass through
|
|
269
|
+
const url = new URL(response.uploadUrl, new URL(this.uploadAttachmentUrl, window.location.href)).toString();
|
|
270
|
+
return {path: response.uploadPath, url, filename: response.originalFilename};
|
|
272
271
|
},
|
|
273
272
|
},
|
|
274
273
|
}),
|
|
@@ -78,7 +78,13 @@ export default class ZnFile extends ZincElement implements ZincFormControl {
|
|
|
78
78
|
assumeInteractionOn: ['zn-change'],
|
|
79
79
|
// An explicitly cleared control submits an empty value so the server can
|
|
80
80
|
// remove the stored upload; an untouched empty control submits nothing.
|
|
81
|
+
// In upload-url mode the control submits the uploaded file's URL string
|
|
82
|
+
// instead of the file bytes.
|
|
81
83
|
value: (el: ZnFile) => {
|
|
84
|
+
if (el.uploadUrl) {
|
|
85
|
+
if (el.src) return el.src;
|
|
86
|
+
return el.clearedByUser ? '' : undefined;
|
|
87
|
+
}
|
|
82
88
|
if (el.files?.length) return el.files;
|
|
83
89
|
return el.clearedByUser ? '' : undefined;
|
|
84
90
|
}
|
|
@@ -148,12 +154,21 @@ export default class ZnFile extends ZincElement implements ZincFormControl {
|
|
|
148
154
|
*/
|
|
149
155
|
@property({type: String})
|
|
150
156
|
set value(v: string) {
|
|
157
|
+
// In upload-url mode the value is the uploaded file's URL, held in `src`
|
|
158
|
+
// (a native file input only accepts an empty string as value).
|
|
159
|
+
if (this.uploadUrl) {
|
|
160
|
+
this.src = v ?? '';
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
151
163
|
if (this.input) {
|
|
152
164
|
this.input.value = v;
|
|
153
165
|
}
|
|
154
166
|
}
|
|
155
167
|
|
|
156
168
|
get value() {
|
|
169
|
+
if (this.uploadUrl) {
|
|
170
|
+
return this.src ?? '';
|
|
171
|
+
}
|
|
157
172
|
return this.input?.value;
|
|
158
173
|
}
|
|
159
174
|
|
|
@@ -254,6 +269,16 @@ export default class ZnFile extends ZincElement implements ZincFormControl {
|
|
|
254
269
|
*/
|
|
255
270
|
@property({type: Boolean, attribute: 'show-link'}) showLink = false;
|
|
256
271
|
|
|
272
|
+
/**
|
|
273
|
+
* When set, a chosen file is immediately POSTed (multipart, field `file`) to this URL.
|
|
274
|
+
* The endpoint must respond with JSON `{"url": "..."}`; that URL becomes the control's
|
|
275
|
+
* value (and `src` preview) in place of the file bytes, so the control works inside
|
|
276
|
+
* JSON-serialising hosts such as `zn-page-builder`. Emits `zn-error` when the upload fails.
|
|
277
|
+
*/
|
|
278
|
+
@property({attribute: 'upload-url'}) uploadUrl = '';
|
|
279
|
+
|
|
280
|
+
@state() private uploading = false;
|
|
281
|
+
|
|
257
282
|
/** Gets the validity state object */
|
|
258
283
|
get validity() {
|
|
259
284
|
return this.input?.validity;
|
|
@@ -387,6 +412,14 @@ export default class ZnFile extends ZincElement implements ZincFormControl {
|
|
|
387
412
|
e.stopPropagation();
|
|
388
413
|
|
|
389
414
|
this.updatePreview();
|
|
415
|
+
|
|
416
|
+
const file = this.files?.[0];
|
|
417
|
+
if (this.uploadUrl && file) {
|
|
418
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
419
|
+
this.uploadFile(file);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
390
423
|
this.emit('zn-input');
|
|
391
424
|
this.emit('zn-change');
|
|
392
425
|
|
|
@@ -395,6 +428,42 @@ export default class ZnFile extends ZincElement implements ZincFormControl {
|
|
|
395
428
|
}
|
|
396
429
|
}
|
|
397
430
|
|
|
431
|
+
/**
|
|
432
|
+
* Uploads the chosen file to `uploadUrl` and adopts the returned URL as the
|
|
433
|
+
* control's value. The local file preview stays visible while the upload runs.
|
|
434
|
+
*/
|
|
435
|
+
private async uploadFile(file: File) {
|
|
436
|
+
this.uploading = true;
|
|
437
|
+
try {
|
|
438
|
+
const body = new FormData();
|
|
439
|
+
body.append('file', file, file.name);
|
|
440
|
+
const response = await fetch(this.uploadUrl, {method: 'POST', body, credentials: 'same-origin'});
|
|
441
|
+
if (!response.ok) {
|
|
442
|
+
throw new Error(`upload failed with status ${response.status}`);
|
|
443
|
+
}
|
|
444
|
+
const data = await response.json() as { url?: string };
|
|
445
|
+
if (!data.url) {
|
|
446
|
+
throw new Error('upload response missing url');
|
|
447
|
+
}
|
|
448
|
+
this.src = data.url;
|
|
449
|
+
this.input.value = '';
|
|
450
|
+
this.files = null;
|
|
451
|
+
this.clearedByUser = false;
|
|
452
|
+
this.emit('zn-input');
|
|
453
|
+
this.emit('zn-change');
|
|
454
|
+
if (this.triggerSubmit) {
|
|
455
|
+
this.formControlController.submit();
|
|
456
|
+
}
|
|
457
|
+
} catch (error) {
|
|
458
|
+
console.warn('<zn-file> upload failed', error);
|
|
459
|
+
this.input.value = '';
|
|
460
|
+
this.files = null;
|
|
461
|
+
this.emit('zn-error');
|
|
462
|
+
} finally {
|
|
463
|
+
this.uploading = false;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
398
467
|
private handleDragOver = (e: DragEvent) => {
|
|
399
468
|
e.preventDefault();
|
|
400
469
|
e.stopPropagation();
|
|
@@ -698,6 +767,7 @@ export default class ZnFile extends ZincElement implements ZincFormControl {
|
|
|
698
767
|
'form-control--large': this.size === 'large',
|
|
699
768
|
'form-control--medium': this.size === 'medium',
|
|
700
769
|
'form-control--small': this.size === 'small',
|
|
770
|
+
'form-control--uploading': this.uploading,
|
|
701
771
|
'form-control--user-dragging': this.userIsDragging,
|
|
702
772
|
})}"
|
|
703
773
|
@dragenter="${this.handleDragOver}"
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Canonical canvas card/container width, inherited by the section-card
|
|
6
6
|
// component's shadow styles (whose 560px var() fallback only applies when
|
|
7
7
|
// the card is used outside a builder).
|
|
8
|
-
--pb-card-width:
|
|
8
|
+
--pb-card-width: 1024px;
|
|
9
9
|
|
|
10
10
|
display: block;
|
|
11
11
|
width: 100%;
|