@hawiyah/vue 0.0.0-stage → 0.2.1

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.
Files changed (81) hide show
  1. package/LICENSE +73 -0
  2. package/NOTICE +19 -0
  3. package/PORTAL.md +70 -0
  4. package/README.md +388 -2
  5. package/dist/calendar.d.ts +183 -0
  6. package/dist/calendar.d.ts.map +1 -0
  7. package/dist/calendar.js +448 -0
  8. package/dist/calendar.js.map +1 -0
  9. package/dist/content-navigation.d.ts +61 -0
  10. package/dist/content-navigation.d.ts.map +1 -0
  11. package/dist/content-navigation.js +31 -0
  12. package/dist/content-navigation.js.map +1 -0
  13. package/dist/content.d.ts +392 -0
  14. package/dist/content.d.ts.map +1 -0
  15. package/dist/content.js +310 -0
  16. package/dist/content.js.map +1 -0
  17. package/dist/controls.d.ts +657 -0
  18. package/dist/controls.d.ts.map +1 -0
  19. package/dist/controls.js +679 -0
  20. package/dist/controls.js.map +1 -0
  21. package/dist/display.d.ts +387 -0
  22. package/dist/display.d.ts.map +1 -0
  23. package/dist/display.js +293 -0
  24. package/dist/display.js.map +1 -0
  25. package/dist/expansion-interactive.d.ts +476 -0
  26. package/dist/expansion-interactive.d.ts.map +1 -0
  27. package/dist/expansion-interactive.js +613 -0
  28. package/dist/expansion-interactive.js.map +1 -0
  29. package/dist/file-upload-list.d.ts +54 -0
  30. package/dist/file-upload-list.d.ts.map +1 -0
  31. package/dist/file-upload-list.js +21 -0
  32. package/dist/file-upload-list.js.map +1 -0
  33. package/dist/forms.d.ts +388 -0
  34. package/dist/forms.d.ts.map +1 -0
  35. package/dist/forms.js +156 -0
  36. package/dist/forms.js.map +1 -0
  37. package/dist/index.d.ts +26 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +16 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/interactive.d.ts +240 -0
  42. package/dist/interactive.d.ts.map +1 -0
  43. package/dist/interactive.js +341 -0
  44. package/dist/interactive.js.map +1 -0
  45. package/dist/layout.d.ts +755 -0
  46. package/dist/layout.d.ts.map +1 -0
  47. package/dist/layout.js +101 -0
  48. package/dist/layout.js.map +1 -0
  49. package/dist/mobile-top-bar.d.ts +47 -0
  50. package/dist/mobile-top-bar.d.ts.map +1 -0
  51. package/dist/mobile-top-bar.js +18 -0
  52. package/dist/mobile-top-bar.js.map +1 -0
  53. package/dist/native-attributes.d.ts +38 -0
  54. package/dist/native-attributes.d.ts.map +1 -0
  55. package/dist/native-attributes.js +27 -0
  56. package/dist/native-attributes.js.map +1 -0
  57. package/dist/navigation-drawer.d.ts +87 -0
  58. package/dist/navigation-drawer.d.ts.map +1 -0
  59. package/dist/navigation-drawer.js +56 -0
  60. package/dist/navigation-drawer.js.map +1 -0
  61. package/dist/patterns.d.ts +455 -0
  62. package/dist/patterns.d.ts.map +1 -0
  63. package/dist/patterns.js +355 -0
  64. package/dist/patterns.js.map +1 -0
  65. package/dist/portal.d.ts +174 -0
  66. package/dist/portal.d.ts.map +1 -0
  67. package/dist/portal.js +36 -0
  68. package/dist/portal.js.map +1 -0
  69. package/dist/search-field.d.ts +87 -0
  70. package/dist/search-field.d.ts.map +1 -0
  71. package/dist/search-field.js +123 -0
  72. package/dist/search-field.js.map +1 -0
  73. package/dist/shared.d.ts +46 -0
  74. package/dist/shared.d.ts.map +1 -0
  75. package/dist/shared.js +40 -0
  76. package/dist/shared.js.map +1 -0
  77. package/dist/site-verification.d.ts +63 -0
  78. package/dist/site-verification.d.ts.map +1 -0
  79. package/dist/site-verification.js +34 -0
  80. package/dist/site-verification.js.map +1 -0
  81. package/package.json +53 -3
@@ -0,0 +1,679 @@
1
+ import { computed, defineComponent, h, mergeProps, ref, watchEffect, } from "vue";
2
+ import { clampRange, filterOptions, isDisabled, normalizeDigits, normalizeRatingMax, normalizeRatingValue } from "@hawiyah/core";
3
+ import { Button, TextField, decorate } from "./forms.js";
4
+ import { decorationProps, useField, useFormReset, useValue } from "./shared.js";
5
+ const textProps = {
6
+ ...decorationProps,
7
+ modelValue: String,
8
+ defaultValue: { type: String, default: "" },
9
+ disabled: Boolean,
10
+ readonly: Boolean,
11
+ };
12
+ const numberProps = {
13
+ ...decorationProps,
14
+ modelValue: Number,
15
+ defaultValue: { type: Number, default: 0 },
16
+ min: { type: Number, default: 0 },
17
+ max: { type: Number, default: 100 },
18
+ step: { type: Number, default: 1 },
19
+ disabled: Boolean,
20
+ readonly: Boolean,
21
+ };
22
+ const arrayProps = {
23
+ ...decorationProps,
24
+ modelValue: Array,
25
+ defaultValue: {
26
+ type: Array,
27
+ default: () => [],
28
+ },
29
+ disabled: Boolean,
30
+ readonly: Boolean,
31
+ };
32
+ export const InputGroup = defineComponent({
33
+ name: "HwInputGroup",
34
+ inheritAttrs: false,
35
+ setup(_, { attrs, slots }) {
36
+ return () => h("div", mergeProps(attrs, { class: "hw-input-group" }), [
37
+ slots.prefix && h("span", { class: "hw-input-addon" }, slots.prefix()),
38
+ slots.default?.(),
39
+ slots.suffix && h("span", { class: "hw-input-addon" }, slots.suffix()),
40
+ ]);
41
+ },
42
+ });
43
+ function actionField(kind) {
44
+ return defineComponent({
45
+ name: kind === "password" ? "HwPasswordField" : "HwSearchField",
46
+ inheritAttrs: false,
47
+ props: {
48
+ ...textProps,
49
+ showLabel: { type: String, default: "Show password" },
50
+ hideLabel: { type: String, default: "Hide password" },
51
+ toggleVariant: { type: String, default: 'icon' },
52
+ clearLabel: { type: String, default: "Clear search" },
53
+ },
54
+ emits: {
55
+ "update:modelValue": (_value) => true,
56
+ search: (_value) => true,
57
+ },
58
+ setup(props, { attrs, emit, expose }) {
59
+ const field = useField(props, attrs);
60
+ const element = ref();
61
+ const visible = ref(false);
62
+ let composing = false;
63
+ const model = useValue(() => props.modelValue, props.defaultValue, (value) => emit("update:modelValue", value));
64
+ useFormReset(element, () => { model.set(props.defaultValue); if (kind === 'password')
65
+ visible.value = false; });
66
+ expose({ element, focus: () => element.value?.focus() });
67
+ return () => decorate(props, field.id.value, h("div", {
68
+ class: [
69
+ kind === 'password' ? "hw-password-control" : "hw-input-group hw-search-control",
70
+ `hw-${kind === "password" ? "password-field" : "search-field"}`,
71
+ ],
72
+ }, [
73
+ h("input", mergeProps(attrs, field.controlAttrs.value, {
74
+ ref: element,
75
+ class: "hw-input",
76
+ type: kind === "password"
77
+ ? visible.value
78
+ ? "text"
79
+ : "password"
80
+ : "search",
81
+ value: model.value.value,
82
+ disabled: props.disabled,
83
+ readonly: props.readonly,
84
+ onInput: (event) => {
85
+ if (!props.disabled && !props.readonly)
86
+ model.set(event.target.value);
87
+ },
88
+ onCompositionstart: () => { composing = true; },
89
+ onCompositionend: () => { composing = false; },
90
+ onKeydown: (event) => {
91
+ if (kind === "search" &&
92
+ event.key === "Enter" &&
93
+ !event.defaultPrevented && !composing && !event.isComposing && !event.ctrlKey && !event.metaKey && !event.altKey && !!element.value && !isDisabled(element.value))
94
+ emit("search", model.value.value);
95
+ },
96
+ })),
97
+ kind === "password"
98
+ ? h(Button, {
99
+ variant: "ghost",
100
+ class: "hw-password-toggle",
101
+ "data-toggle-variant": props.toggleVariant,
102
+ disabled: props.disabled,
103
+ "aria-label": visible.value
104
+ ? props.hideLabel
105
+ : props.showLabel,
106
+ "aria-pressed": visible.value,
107
+ "aria-controls": field.id.value,
108
+ onClick: () => {
109
+ if (element.value && !isDisabled(element.value))
110
+ visible.value = !visible.value;
111
+ },
112
+ }, () => props.toggleVariant === 'text' ? (visible.value ? props.hideLabel : props.showLabel) : h('span', { class: 'hw-password-icon', 'aria-hidden': 'true' }))
113
+ : h(Button, {
114
+ variant: "ghost",
115
+ class: "hw-search-clear",
116
+ hidden: !model.value.value || props.readonly,
117
+ disabled: props.disabled,
118
+ "aria-controls": field.id.value,
119
+ "aria-label": props.clearLabel,
120
+ onClick: () => {
121
+ if (element.value && !props.readonly && !isDisabled(element.value)) {
122
+ model.set("");
123
+ element.value.focus();
124
+ }
125
+ },
126
+ }, () => h("span", { class: "hw-search-clear-icon", "aria-hidden": "true" })),
127
+ ]));
128
+ },
129
+ });
130
+ }
131
+ export const PasswordField = actionField("password");
132
+ export { SearchField } from './search-field.js';
133
+ export const NumberField = defineComponent({
134
+ name: "HwNumberField",
135
+ inheritAttrs: false,
136
+ props: {
137
+ ...decorationProps,
138
+ modelValue: [Number, String],
139
+ defaultValue: {
140
+ type: [Number, String],
141
+ default: "",
142
+ },
143
+ disabled: Boolean,
144
+ readonly: Boolean,
145
+ },
146
+ emits: { "update:modelValue": (_value) => true },
147
+ setup(props, { attrs, emit, expose }) {
148
+ const field = useField(props, attrs);
149
+ const element = ref();
150
+ const model = useValue(() => props.modelValue, props.defaultValue, (v) => emit("update:modelValue", v));
151
+ useFormReset(element, () => model.set(props.defaultValue));
152
+ expose({ element, focus: () => element.value?.focus() });
153
+ return () => decorate(props, field.id.value, h("input", mergeProps(attrs, field.controlAttrs.value, {
154
+ ref: element,
155
+ class: "hw-input hw-number-field",
156
+ type: "number",
157
+ disabled: props.disabled,
158
+ readonly: props.readonly,
159
+ value: model.value.value,
160
+ onInput: (event) => {
161
+ if (props.disabled || props.readonly)
162
+ return;
163
+ const value = event.target.valueAsNumber;
164
+ model.set(Number.isNaN(value) ? "" : value);
165
+ },
166
+ })));
167
+ },
168
+ });
169
+ export const Slider = defineComponent({
170
+ name: "HwSlider",
171
+ inheritAttrs: false,
172
+ props: numberProps,
173
+ emits: { "update:modelValue": (_value) => true },
174
+ setup(props, { attrs, emit, expose }) {
175
+ const field = useField(props, attrs);
176
+ const element = ref();
177
+ const model = useValue(() => props.modelValue, props.defaultValue, (v) => emit("update:modelValue", v));
178
+ useFormReset(element, () => model.set(props.defaultValue));
179
+ expose({ element, focus: () => element.value?.focus() });
180
+ return () => decorate(props, field.id.value, h("input", mergeProps(attrs, field.controlAttrs.value, {
181
+ ref: element,
182
+ class: "hw-slider",
183
+ type: "range",
184
+ min: props.min,
185
+ max: props.max,
186
+ step: props.step,
187
+ value: model.value.value,
188
+ disabled: props.disabled,
189
+ "aria-readonly": props.readonly || undefined,
190
+ onPointerdown: (event) => {
191
+ if (props.readonly)
192
+ event.preventDefault();
193
+ },
194
+ onKeydown: (event) => {
195
+ if (props.readonly && ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'PageUp', 'PageDown'].includes(event.key))
196
+ event.preventDefault();
197
+ },
198
+ onInput: (event) => {
199
+ const input = event.target;
200
+ if (props.readonly || isDisabled(input)) {
201
+ input.value = String(model.value.value);
202
+ return;
203
+ }
204
+ model.set(input.valueAsNumber);
205
+ },
206
+ })));
207
+ },
208
+ });
209
+ export const RangeSlider = defineComponent({
210
+ name: "HwRangeSlider",
211
+ inheritAttrs: false,
212
+ props: {
213
+ ...numberProps,
214
+ modelValue: Array,
215
+ defaultValue: {
216
+ type: Array,
217
+ default: () => [0, 100],
218
+ },
219
+ lowerLabel: { type: String, default: "Minimum" },
220
+ upperLabel: { type: String, default: "Maximum" },
221
+ lowerName: String,
222
+ upperName: String,
223
+ },
224
+ emits: { "update:modelValue": (_value) => true },
225
+ setup(props, { attrs, emit }) {
226
+ const field = useField(props, attrs);
227
+ const element = ref();
228
+ const model = useValue(() => props.modelValue, props.defaultValue, (v) => emit("update:modelValue", v));
229
+ useFormReset(element, () => model.set(props.defaultValue));
230
+ return () => {
231
+ const values = clampRange([...model.value.value], props.min, props.max, "lower");
232
+ return h("fieldset", mergeProps(attrs, {
233
+ class: "hw-field hw-range-slider",
234
+ disabled: props.disabled,
235
+ "aria-describedby": field.controlAttrs.value["aria-describedby"],
236
+ "aria-invalid": field.controlAttrs.value["aria-invalid"],
237
+ }), [
238
+ props.label && h("legend", { class: "hw-label" }, props.label),
239
+ ...["lower", "upper"].map((side, index) => h("label", { class: "hw-label" }, [
240
+ side === "lower" ? props.lowerLabel : props.upperLabel,
241
+ h("input", {
242
+ ref: index === 0 ? element : undefined,
243
+ type: "range",
244
+ class: "hw-slider",
245
+ name: side === "lower" ? props.lowerName : props.upperName,
246
+ form: attrs.form,
247
+ min: index === 0 ? props.min : values[0],
248
+ max: index === 0 ? values[1] : props.max,
249
+ step: props.step,
250
+ value: values[index],
251
+ disabled: props.disabled,
252
+ "aria-readonly": props.readonly || undefined,
253
+ onPointerdown: (event) => {
254
+ if (props.readonly)
255
+ event.preventDefault();
256
+ },
257
+ onKeydown: (event) => {
258
+ if (props.readonly && ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'PageUp', 'PageDown'].includes(event.key))
259
+ event.preventDefault();
260
+ },
261
+ onInput: (event) => {
262
+ const input = event.target;
263
+ if (props.readonly || isDisabled(input)) {
264
+ input.value = String(values[index]);
265
+ return;
266
+ }
267
+ const next = [values[0], values[1]];
268
+ next[index] = input.valueAsNumber;
269
+ model.set(clampRange(next, props.min, props.max, side));
270
+ },
271
+ }),
272
+ ])),
273
+ props.hint &&
274
+ h("span", { id: `${field.id.value}-hint`, class: "hw-hint" }, props.hint),
275
+ props.error &&
276
+ h("span", { id: `${field.id.value}-error`, class: "hw-error" }, props.error),
277
+ ]);
278
+ };
279
+ },
280
+ });
281
+ export const Rating = defineComponent({
282
+ name: "HwRating",
283
+ inheritAttrs: false,
284
+ props: {
285
+ ...numberProps,
286
+ max: { type: Number, default: 5 },
287
+ optionLabel: {
288
+ type: Function,
289
+ default: (value, max) => `${value} من ${max}`,
290
+ },
291
+ },
292
+ emits: { "update:modelValue": (_value) => true },
293
+ setup(props, { attrs, emit }) {
294
+ const field = useField(props, attrs);
295
+ const element = ref();
296
+ const model = useValue(() => props.modelValue, props.defaultValue, (v) => emit("update:modelValue", v));
297
+ useFormReset(element, () => model.set(props.defaultValue));
298
+ return () => {
299
+ const count = normalizeRatingMax(props.max);
300
+ const current = Math.floor(normalizeRatingValue(model.value.value, count));
301
+ return h("fieldset", mergeProps(attrs, {
302
+ class: "hw-field hw-rating",
303
+ disabled: props.disabled,
304
+ "data-hw-readonly": props.readonly || undefined,
305
+ "aria-describedby": field.controlAttrs.value["aria-describedby"],
306
+ "aria-invalid": props.error ? true : undefined,
307
+ }), [
308
+ props.label && h("legend", { class: "hw-label" }, props.label),
309
+ ...Array.from({ length: count }, (_, index) => h("label", {
310
+ "data-selected": index < current ? "true" : undefined,
311
+ }, [
312
+ h("input", {
313
+ ref: index === 0 ? element : undefined,
314
+ type: "radio",
315
+ name: attrs.name ?? `${field.id.value}-rating`,
316
+ form: attrs.form,
317
+ value: index + 1,
318
+ checked: current === index + 1,
319
+ required: props.required,
320
+ disabled: props.disabled,
321
+ "aria-label": props.optionLabel(index + 1, count),
322
+ "aria-disabled": props.readonly || undefined,
323
+ onClick: (event) => {
324
+ if (props.readonly) {
325
+ event.preventDefault();
326
+ // Radios activate before click; restore the whole native group when cancellation rolls back only the target.
327
+ element.value?.closest('fieldset')?.querySelectorAll('input[type="radio"]').forEach(input => {
328
+ input.checked = Number(input.value) === current;
329
+ });
330
+ }
331
+ },
332
+ onKeydown: (event) => {
333
+ if (props.readonly && ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', ' '].includes(event.key))
334
+ event.preventDefault();
335
+ },
336
+ onChange: (event) => {
337
+ if (!props.readonly && !isDisabled(event.currentTarget))
338
+ model.set(index + 1);
339
+ },
340
+ }),
341
+ h("span", { "aria-hidden": "true" }, "★"),
342
+ ])),
343
+ props.hint &&
344
+ h("span", { id: `${field.id.value}-hint`, class: "hw-hint" }, props.hint),
345
+ props.error &&
346
+ h("span", { id: `${field.id.value}-error`, class: "hw-error" }, props.error),
347
+ ]);
348
+ };
349
+ },
350
+ });
351
+ /** Read-only aggregate score; Rating remains an integer radio input. */
352
+ export const RatingDisplay = defineComponent({
353
+ name: 'HwRatingDisplay', inheritAttrs: false,
354
+ props: {
355
+ label: { type: String, required: true }, value: { type: Number, default: 0 },
356
+ max: { type: Number, default: 5 }, showValue: { type: Boolean, default: true },
357
+ valueLabel: { type: Function, default: (value, max) => `${value} من ${max}` },
358
+ },
359
+ setup(props, { attrs }) {
360
+ return () => {
361
+ const total = normalizeRatingMax(props.max), score = normalizeRatingValue(props.value, total);
362
+ return h('div', mergeProps(attrs, { class: 'hw-rating-display', role: 'img', 'aria-label': attrs['aria-label'] ?? `${props.label}: ${props.valueLabel(score, total)}` }), [
363
+ h('span', { class: 'hw-rating-stars', 'aria-hidden': 'true' }, Array.from({ length: total }, (_, index) => h('span', { class: 'hw-rating-star', key: index }, ['★', h('span', { class: 'hw-rating-fill', style: { '--hw-rating-fill': `${Math.min(1, Math.max(0, score - index)) * 100}%` } })]))),
364
+ props.showValue && h('span', { class: 'hw-rating-score', 'aria-hidden': 'true' }, `${score}/${total}`),
365
+ ]);
366
+ };
367
+ },
368
+ });
369
+ export const PinInput = defineComponent({
370
+ name: "HwPinInput",
371
+ inheritAttrs: false,
372
+ props: { ...textProps, length: { type: Number, default: 6 } },
373
+ emits: {
374
+ "update:modelValue": (_value) => true,
375
+ complete: (_value) => true,
376
+ },
377
+ setup(props, { attrs, emit, expose }) {
378
+ const field = useField(props, attrs);
379
+ const element = ref();
380
+ const normalizedLength = () => Math.max(1, Math.floor(props.length) || 6);
381
+ const normalize = (value) => normalizeDigits(value).replace(/\D/g, '').slice(0, normalizedLength());
382
+ const model = useValue(() => props.modelValue, normalize(props.defaultValue), (v) => emit("update:modelValue", v));
383
+ useFormReset(element, () => model.set(normalize(props.defaultValue)));
384
+ expose({ element, focus: () => element.value?.focus() });
385
+ return () => {
386
+ const length = normalizedLength();
387
+ return decorate(props, field.id.value, h("input", mergeProps(attrs, field.controlAttrs.value, {
388
+ ref: element,
389
+ class: "hw-input hw-pin-input",
390
+ type: "text",
391
+ autocomplete: "one-time-code",
392
+ inputmode: "numeric",
393
+ pattern: `[0-9]{${length}}`,
394
+ maxlength: length,
395
+ dir: "ltr",
396
+ value: normalize(model.value.value),
397
+ disabled: props.disabled,
398
+ readonly: props.readonly,
399
+ onInput: (event) => {
400
+ if (props.disabled || props.readonly)
401
+ return;
402
+ const input = event.target;
403
+ const value = normalize(input.value);
404
+ input.value = value;
405
+ model.set(value);
406
+ if (value.length === length)
407
+ emit("complete", value);
408
+ },
409
+ })));
410
+ };
411
+ },
412
+ });
413
+ export const TagInput = defineComponent({
414
+ name: "HwTagInput",
415
+ inheritAttrs: false,
416
+ props: {
417
+ ...arrayProps,
418
+ requiredMessage: { type: String, default: "Add at least one item." },
419
+ removeLabel: {
420
+ type: Function,
421
+ default: (value) => `Remove ${value}`,
422
+ },
423
+ },
424
+ emits: { "update:modelValue": (_value) => true },
425
+ setup(props, { attrs, emit, expose }) {
426
+ const field = useField(props, attrs);
427
+ const element = ref();
428
+ const query = ref("");
429
+ let composing = false;
430
+ const blocked = () => props.disabled || props.readonly || !!element.value && isDisabled(element.value);
431
+ const model = useValue(() => props.modelValue, props.defaultValue, (v) => emit("update:modelValue", v));
432
+ useFormReset(element, () => {
433
+ query.value = "";
434
+ model.set(props.defaultValue);
435
+ });
436
+ watchEffect(() => element.value?.setCustomValidity(field.controlAttrs.value.required && !model.value.value.length
437
+ ? props.requiredMessage
438
+ : ""));
439
+ expose({ element, focus: () => element.value?.focus() });
440
+ const remove = (value) => {
441
+ if (!blocked()) {
442
+ model.set(model.value.value.filter((item) => item !== value));
443
+ element.value?.focus();
444
+ }
445
+ };
446
+ return () => {
447
+ const { name, form, ...rest } = attrs;
448
+ return decorate(props, field.id.value, h("div", { class: "hw-tag-input" }, [
449
+ h("ul", {}, model.value.value.map((value) => h("li", { class: "hw-chip", key: value }, [
450
+ value,
451
+ h(Button, {
452
+ variant: "ghost",
453
+ size: "sm",
454
+ disabled: props.disabled || props.readonly,
455
+ "aria-label": props.removeLabel(value),
456
+ onClick: () => remove(value),
457
+ }, () => h('span', { class: 'hw-icon', 'data-icon': 'cancel-01', 'data-size': 'md', 'aria-hidden': 'true' })),
458
+ ]))),
459
+ h("input", mergeProps(rest, field.controlAttrs.value, {
460
+ ref: element,
461
+ form,
462
+ class: "hw-input",
463
+ value: query.value,
464
+ required: undefined,
465
+ "aria-required": field.controlAttrs.value.required || undefined,
466
+ disabled: props.disabled,
467
+ readonly: props.readonly,
468
+ onInput: (event) => {
469
+ query.value = event.target.value;
470
+ },
471
+ onCompositionstart: () => { composing = true; },
472
+ onCompositionend: () => { composing = false; },
473
+ onKeydown: (event) => {
474
+ if (event.defaultPrevented || blocked() || composing || event.isComposing || event.ctrlKey || event.altKey || event.metaKey)
475
+ return;
476
+ if ((event.key === "Enter" || event.key === ",") && query.value.trim()) {
477
+ event.preventDefault();
478
+ const value = query.value.trim();
479
+ if (value && !model.value.value.includes(value))
480
+ model.set([...model.value.value, value]);
481
+ query.value = "";
482
+ }
483
+ else if (event.key === "Backspace" && !query.value) {
484
+ const last = model.value.value.at(-1);
485
+ if (last) {
486
+ event.preventDefault();
487
+ remove(last);
488
+ }
489
+ }
490
+ },
491
+ })),
492
+ ...model.value.value.map((value) => h("input", {
493
+ type: "hidden",
494
+ name,
495
+ form,
496
+ value,
497
+ disabled: props.disabled,
498
+ })),
499
+ ]));
500
+ };
501
+ },
502
+ });
503
+ export const MultiSelect = defineComponent({
504
+ name: "HwMultiSelect",
505
+ inheritAttrs: false,
506
+ props: {
507
+ ...arrayProps,
508
+ options: { type: Array, required: true },
509
+ searchLabel: { type: String, default: "Search options" },
510
+ emptyMessage: { type: String, default: "No options found" },
511
+ requiredMessage: { type: String, default: "Choose at least one option." },
512
+ },
513
+ emits: { "update:modelValue": (_value) => true },
514
+ setup(props, { attrs, emit }) {
515
+ const field = useField(props, attrs);
516
+ const element = ref();
517
+ const query = ref("");
518
+ const model = useValue(() => props.modelValue, props.defaultValue, (v) => emit("update:modelValue", v));
519
+ // Keep caller state intact; only known, enabled choices count or submit.
520
+ const selected = computed(() => model.value.value.filter(value => props.options.some(option => option.value === value && !option.disabled)));
521
+ useFormReset(element, () => {
522
+ query.value = "";
523
+ model.set(props.defaultValue);
524
+ });
525
+ watchEffect(() => element.value?.setCustomValidity(field.controlAttrs.value.required && !props.disabled && !selected.value.length
526
+ ? props.requiredMessage
527
+ : ""));
528
+ return () => {
529
+ const options = filterOptions(props.options, query.value);
530
+ return h("fieldset", {
531
+ class: "hw-field hw-multi-select",
532
+ disabled: props.disabled,
533
+ "aria-describedby": field.controlAttrs.value["aria-describedby"],
534
+ }, [
535
+ props.label && h("legend", { class: "hw-label" }, props.label),
536
+ h("input", mergeProps(attrs, {
537
+ ref: element,
538
+ id: field.id.value,
539
+ name: undefined,
540
+ type: "search",
541
+ class: "hw-input",
542
+ "aria-label": props.searchLabel,
543
+ "aria-required": field.controlAttrs.value.required || undefined,
544
+ "aria-describedby": field.controlAttrs.value["aria-describedby"],
545
+ "aria-invalid": field.controlAttrs.value["aria-invalid"],
546
+ disabled: props.disabled,
547
+ value: query.value,
548
+ onInput: (event) => {
549
+ query.value = event.target.value;
550
+ },
551
+ })),
552
+ h("div", { class: "hw-multi-select-options" }, options.map((option) => h("label", { class: "hw-choice", key: option.value }, [
553
+ h("input", {
554
+ type: "checkbox",
555
+ class: "hw-checkbox",
556
+ checked: selected.value.includes(option.value),
557
+ disabled: props.disabled || props.readonly || option.disabled,
558
+ onChange: (event) => {
559
+ if (props.disabled || props.readonly || option.disabled)
560
+ return;
561
+ model.set(event.target.checked
562
+ ? [...selected.value, option.value]
563
+ : selected.value.filter((value) => value !== option.value));
564
+ },
565
+ }),
566
+ option.label,
567
+ ]))),
568
+ !options.length && h("p", { role: "status" }, props.emptyMessage),
569
+ ...selected.value.map((value) => h("input", {
570
+ type: "hidden",
571
+ name: attrs.name,
572
+ form: attrs.form,
573
+ value,
574
+ disabled: props.disabled,
575
+ })),
576
+ props.hint &&
577
+ h("span", { id: `${field.id.value}-hint`, class: "hw-hint" }, props.hint),
578
+ props.error &&
579
+ h("span", { id: `${field.id.value}-error`, class: "hw-error" }, props.error),
580
+ ]);
581
+ };
582
+ },
583
+ });
584
+ export const FileDropzone = defineComponent({
585
+ name: "HwFileDropzone",
586
+ inheritAttrs: false,
587
+ props: {
588
+ ...decorationProps,
589
+ disabled: Boolean,
590
+ readonly: Boolean,
591
+ accept: String,
592
+ multiple: Boolean,
593
+ maxSize: Number,
594
+ dropLabel: { type: String, default: "Choose files or drop them here" },
595
+ },
596
+ emits: {
597
+ "update:files": (_files) => true,
598
+ reject: (_files) => true,
599
+ },
600
+ setup(props, { attrs, emit, slots, expose }) {
601
+ const field = useField(props, attrs);
602
+ const element = ref();
603
+ const dragging = ref(false);
604
+ const names = ref([]);
605
+ useFormReset(element, () => {
606
+ names.value = [];
607
+ emit("update:files", []);
608
+ });
609
+ expose({ element, focus: () => element.value?.focus() });
610
+ const accepted = (file) => {
611
+ if (props.maxSize && file.size > props.maxSize)
612
+ return false;
613
+ return (!props.accept ||
614
+ props.accept.split(",").some((rule) => {
615
+ rule = rule.trim().toLowerCase();
616
+ return rule.startsWith(".")
617
+ ? file.name.toLowerCase().endsWith(rule)
618
+ : rule.endsWith("/*")
619
+ ? file.type.startsWith(rule.slice(0, -1))
620
+ : file.type === rule;
621
+ }));
622
+ };
623
+ const unavailable = () => props.disabled || props.readonly || !element.value || isDisabled(element.value);
624
+ const select = (list, drop = false) => {
625
+ if (unavailable() || !list)
626
+ return;
627
+ const files = Array.from(list);
628
+ if (files.some((file) => !accepted(file)) ||
629
+ (!props.multiple && files.length > 1)) {
630
+ if (element.value)
631
+ element.value.value = "";
632
+ names.value = [];
633
+ emit("update:files", []);
634
+ emit("reject", files);
635
+ return;
636
+ }
637
+ if (drop && element.value)
638
+ element.value.files = list;
639
+ names.value = files.map((file) => file.name);
640
+ emit("update:files", files);
641
+ };
642
+ return () => decorate(props, field.id.value, h("div", {
643
+ class: "hw-file-dropzone",
644
+ "data-dragging": dragging.value ? "true" : undefined,
645
+ onDragover: (event) => {
646
+ event.preventDefault();
647
+ if (!unavailable())
648
+ dragging.value = true;
649
+ },
650
+ onDragleave: () => {
651
+ dragging.value = false;
652
+ },
653
+ onDrop: (event) => {
654
+ event.preventDefault();
655
+ dragging.value = false;
656
+ select(event.dataTransfer?.files ?? null, true);
657
+ },
658
+ }, [
659
+ h("p", {}, props.dropLabel),
660
+ h("input", mergeProps(attrs, field.controlAttrs.value, {
661
+ ref: element,
662
+ class: "hw-input",
663
+ type: "file",
664
+ accept: props.accept,
665
+ multiple: props.multiple,
666
+ disabled: props.disabled,
667
+ "aria-readonly": props.readonly || undefined,
668
+ onClick: (event) => {
669
+ if (props.readonly)
670
+ event.preventDefault();
671
+ },
672
+ onChange: (event) => select(event.target.files),
673
+ })),
674
+ h("p", { role: "status" }, names.value.join(", ")),
675
+ slots.default?.(),
676
+ ]));
677
+ },
678
+ });
679
+ //# sourceMappingURL=controls.js.map