amateras 0.13.2 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +39 -38
  2. package/build/core.js +1 -1
  3. package/build/css-keyframes.js +1 -0
  4. package/build/css-property.js +1 -0
  5. package/build/css-variable.js +1 -0
  6. package/build/for.js +1 -1
  7. package/build/i18n.js +1 -1
  8. package/build/if.js +1 -1
  9. package/build/import-map.js +1 -1
  10. package/build/match.js +1 -1
  11. package/build/meta.js +1 -1
  12. package/build/prefetch.js +1 -1
  13. package/build/router.js +1 -1
  14. package/build/signal.js +1 -1
  15. package/build/store.js +1 -1
  16. package/build/ui.js +1 -1
  17. package/build/utils.js +1 -1
  18. package/build/widget.js +1 -1
  19. package/package.json +3 -2
  20. package/packages/core/src/index.ts +86 -31
  21. package/packages/core/src/lib/hmr.ts +4 -4
  22. package/packages/core/src/structure/ElementProto.ts +33 -11
  23. package/packages/core/src/structure/GlobalState.ts +13 -4
  24. package/packages/core/src/structure/NodeProto.ts +2 -4
  25. package/packages/core/src/structure/Proto.ts +37 -23
  26. package/packages/core/src/structure/TextProto.ts +1 -2
  27. package/packages/css/README.md +18 -15
  28. package/packages/css/src/ext/property.ts +2 -3
  29. package/packages/css/src/index.ts +1 -1
  30. package/packages/css/src/structure/$CSSProperty.ts +4 -0
  31. package/packages/css/src/structure/$CSSVariable.ts +1 -1
  32. package/packages/css/src/types.ts +5 -0
  33. package/packages/for/src/global.ts +12 -3
  34. package/packages/for/src/structure/For.ts +5 -3
  35. package/packages/i18n/README.md +16 -24
  36. package/packages/i18n/src/index.ts +26 -5
  37. package/packages/i18n/src/structure/I18n.ts +2 -4
  38. package/packages/i18n/src/structure/I18nSession.ts +4 -2
  39. package/packages/i18n/src/structure/I18nTranslation.ts +15 -26
  40. package/packages/idb/src/structure/$IDBStore.ts +2 -2
  41. package/packages/if/src/global.ts +15 -4
  42. package/packages/if/src/index.ts +18 -8
  43. package/packages/if/src/structure/Condition.ts +16 -13
  44. package/packages/if/src/structure/ConditionStatement.ts +9 -9
  45. package/packages/match/src/global.ts +9 -3
  46. package/packages/match/src/structure/Match.ts +1 -1
  47. package/packages/meta/src/index.ts +4 -5
  48. package/packages/prefetch/src/index.ts +30 -9
  49. package/packages/router/src/global.ts +17 -4
  50. package/packages/router/src/index.ts +25 -18
  51. package/packages/router/src/structure/Route.ts +2 -1
  52. package/packages/router/src/structure/RouteNode.ts +8 -6
  53. package/packages/router/src/structure/RouteSlot.ts +15 -2
  54. package/packages/router/src/structure/Router.ts +28 -19
  55. package/packages/router/src/structure/RouterConstructor.ts +5 -5
  56. package/packages/router/src/types.ts +2 -2
  57. package/packages/signal/README.md +28 -48
  58. package/packages/signal/src/index.ts +61 -38
  59. package/packages/signal/src/structure/Signal.ts +40 -7
  60. package/packages/store/src/structure/Store.ts +1 -1
  61. package/packages/ui/package.json +2 -1
  62. package/packages/ui/src/icon/check.svg.ts +1 -0
  63. package/packages/ui/src/icon/x.svg.ts +1 -0
  64. package/packages/ui/src/index.ts +9 -2
  65. package/packages/ui/src/lib/combobox_style.ts +20 -0
  66. package/packages/ui/src/lib/hover.ts +2 -0
  67. package/packages/ui/src/structure/Badge.ts +10 -1
  68. package/packages/ui/src/structure/Button.ts +54 -27
  69. package/packages/ui/src/structure/Card.ts +3 -4
  70. package/packages/ui/src/structure/Combobox/Combobox.ts +312 -0
  71. package/packages/ui/src/structure/Combobox/ComboboxChips.ts +178 -0
  72. package/packages/ui/src/structure/Combobox/ComboboxList.ts +209 -0
  73. package/packages/ui/src/structure/ContextMenu.ts +89 -0
  74. package/packages/ui/src/structure/Field.ts +109 -0
  75. package/packages/ui/src/structure/Input.ts +29 -0
  76. package/packages/ui/src/structure/Select/Select.ts +18 -8
  77. package/packages/ui/src/structure/Select/SelectContent.ts +6 -1
  78. package/packages/ui/src/structure/Select/SelectItem.ts +2 -1
  79. package/packages/ui/src/structure/Slideshow.ts +2 -2
  80. package/packages/ui/src/structure/Switch.ts +45 -0
  81. package/packages/ui/src/structure/Tabs.ts +3 -3
  82. package/packages/ui/src/structure/Toggle.ts +155 -0
  83. package/packages/ui/src/structure/Waterfall.ts +1 -1
  84. package/packages/ui/src/structure/WaterfallItem.ts +1 -1
  85. package/packages/utils/src/lib/utils.ts +30 -8
  86. package/packages/utils/src/structure/UID.ts +1 -1
  87. package/packages/widget/src/index.ts +29 -9
  88. package/packages/widget/src/structure/Widget.ts +7 -3
  89. package/packages/ui/src/structure/TextBlock.ts +0 -11
@@ -0,0 +1,312 @@
1
+ import { type FloatDisconnect, float } from "#lib/float";
2
+ import { toCSS } from "#lib/toCSS";
3
+ import { ElementProto, onclient, Proto } from "@amateras/core";
4
+ import { _null, _Array_from, _instanceof, isNull, isUndefined, forEach } from "@amateras/utils";
5
+ import type { ComboboxChips } from "./ComboboxChips";
6
+ import { type ComboboxList, ComboboxItem } from "./ComboboxList";
7
+ import { item_css } from "#lib/combobox_style";
8
+
9
+ export interface ComboboxProps {
10
+ values?: OrSignal<any[]>;
11
+ }
12
+
13
+ export class Combobox extends ElementProto {
14
+ static tagname = 'combobox'
15
+ $trigger: ComboboxTrigger | null = _null;
16
+ $content: ComboboxContent | null = _null;
17
+ $list: ComboboxList | null = _null;
18
+ $chips: ComboboxChips | null = _null;
19
+ $input: ComboboxInput | null = _null;
20
+ itemMap = new Map<any, ComboboxItem>();
21
+ #values = new Set<string>();
22
+ private disconnect: FloatDisconnect | null = _null;
23
+ constructor(props: $.Props<ComboboxProps>, layout?: $.Layout<Combobox>) {
24
+ super('combobox', props, layout);
25
+ }
26
+
27
+ static {
28
+ $.style(this, toCSS(this.tagname, {
29
+ display: 'inline-block',
30
+ width: '10rem',
31
+ userSelect: 'none'
32
+ }))
33
+ }
34
+
35
+ override build(cascading?: boolean): this {
36
+ super.build(cascading);
37
+ forEach(this.#values, value => this.select(value));
38
+ return this;
39
+ }
40
+
41
+ override toDOM(children = true): HTMLElement[] {
42
+ super.toDOM(false);
43
+ if (children && this.$trigger) {
44
+ this.node?.append(...this.$trigger.toDOM());
45
+ this.$content?.toDOM();
46
+ }
47
+ return [this.node!]
48
+ }
49
+
50
+ override props({ values, ...props }: $.Props<ComboboxProps>): void {
51
+ super.props(props);
52
+ this.values(values);
53
+ }
54
+
55
+ open() {
56
+ this.attr('opened', '');
57
+ if (onclient() && this.$content) {
58
+ this.disconnect = float(this.$trigger?.node!, this.$content.node!);
59
+ document.body.append(...this.$content.toDOM());
60
+ if (this.$input) this.$list?.filter(this.$input.node?.value ?? '')
61
+ }
62
+ }
63
+
64
+ close() {
65
+ this.attr('opened', _null);
66
+ if (!onclient()) return;
67
+ this.$content?.removeNode();
68
+ this.disconnect?.();
69
+ this.disconnect = _null;
70
+ }
71
+
72
+ select(value: any, bool = true) {
73
+ const $item = this.itemMap.get(value);
74
+ if (!$item) return;
75
+ $item.selected(bool);
76
+ if (bool) {
77
+ this.#values.add(value);
78
+ this.$chips?.appendChip(value);
79
+ this.dispatch('combobox_select', [value])
80
+ } else {
81
+ this.#values.delete(value);
82
+ this.$chips?.removeChip(value);
83
+ this.dispatch('combobox_unselect', [value])
84
+ }
85
+
86
+ this.dispatch('combobox_input', [value])
87
+ }
88
+
89
+ get selected() {
90
+ return _Array_from(this.itemMap.values()).filter($item => $item.selected())
91
+ }
92
+
93
+ values(): any[]
94
+ values(values?: OrSignal<any[]>): void;
95
+ values(values?: OrSignal<any[]>) {
96
+ if (!arguments.length) return _Array_from(this.#values);
97
+ if (isUndefined(values)) return;
98
+ $.resolve(values, values => {
99
+ this.#values = new Set(values);
100
+ })
101
+ }
102
+ }
103
+
104
+ export class ComboboxTrigger extends ElementProto {
105
+ static tagname = 'combobox-trigger'
106
+ $combobox: Combobox | null = _null;
107
+ constructor(props: $.Props, layout?: $.Layout<ComboboxTrigger>) {
108
+ super('combobox-trigger', props, layout);
109
+ }
110
+
111
+ static {
112
+ $.style(this, toCSS(this.tagname, {
113
+ display: 'flex',
114
+ flexWrap: 'wrap',
115
+ columnGap: 'calc(var(--spacing) * 1.25)',
116
+ placeItems: 'center',
117
+ boxSizing: 'border-box',
118
+ border: '1px solid var(--input)',
119
+ background: 'color-mix(in oklch, var(--input) 30%, transparent)',
120
+ borderRadius: 'var(--radius)',
121
+ fontSize: '0.875rem',
122
+ fontWeight: 'var(--font-weight-medium)',
123
+ lineHeight: '1',
124
+ padding: `0 calc(var(--spacing) * 1.25)`,
125
+
126
+ '&:hover': {
127
+ background: 'color-mix(in oklch, var(--input) 50%, transparent)'
128
+ },
129
+
130
+ '&:focus-within': {
131
+ outline: '0.1rem solid var(--border)'
132
+ }
133
+ }))
134
+ }
135
+
136
+ override build(cascading?: boolean): this {
137
+ super.build(cascading);
138
+ this.$combobox = this.findAbove<Combobox>(proto => _instanceof(proto, Combobox));
139
+ if (this.$combobox) this.$combobox.$trigger = this;
140
+ return this;
141
+ }
142
+ }
143
+
144
+ export class ComboboxInput extends ElementProto<HTMLInputElement> {
145
+ $combobox: Combobox | null = _null;
146
+ constructor(props: $.Props<{}, HTMLInputElement>, layout?: $.Layout<ComboboxInput>) {
147
+ super('input', {ui: 'combobox-input', ...props}, layout);
148
+ this.on('focus', e => isNull(this.$combobox?.attr('opened')) && this.$combobox.open())
149
+ this.on('blur', e => {
150
+ this.$combobox?.close();
151
+ this.$combobox?.$chips?.$focusedChip?.blur();
152
+ })
153
+ this.on('input', e => {
154
+ this.$combobox?.$list?.filter(e.currentTarget.value)
155
+ this.$combobox?.$list?.focusFirstItem();
156
+ })
157
+ this.on('keydown', e => {
158
+ switch (e.key) {
159
+ case 'ArrowDown': {
160
+ e.preventDefault();
161
+ this.$combobox?.$list?.switch('down');
162
+ return;
163
+ }
164
+ case 'ArrowUp': {
165
+ e.preventDefault();
166
+ this.$combobox?.$list?.switch('up')
167
+ return;
168
+ }
169
+ case 'Backspace': {
170
+ if (e.currentTarget.value.length === 0) this.$combobox?.$chips?.children.at(-1)?.delete();
171
+ return;
172
+ }
173
+ case 'ArrowRight': {
174
+ if (e.currentTarget.value.length === 0) this.$combobox?.$chips?.switch('right');
175
+ return;
176
+ }
177
+ case 'ArrowLeft': {
178
+ if (e.currentTarget.value.length === 0) this.$combobox?.$chips?.switch('left');
179
+ return;
180
+ }
181
+ }
182
+ })
183
+
184
+ this.on('keyup', e => {
185
+ switch (e.key) {
186
+ case 'Escape': {
187
+ e.preventDefault();
188
+ this.$combobox?.close();
189
+ this.$combobox?.$trigger?.node?.focus()
190
+ return;
191
+ }
192
+ case 'Delete': {
193
+ const $targetChip = this.$combobox?.$chips?.$focusedChip;
194
+ this.$combobox?.$chips?.switch('right');
195
+ $targetChip?.delete();
196
+ return;
197
+ }
198
+ case 'Tab':
199
+ case 'Enter': {
200
+ e.preventDefault();
201
+ const $focusedItem = this.$combobox?.$list?.$focusedItem;
202
+ if (!$focusedItem) return;
203
+ if (_instanceof($focusedItem, ComboboxItem)) {
204
+ this.$combobox?.select($focusedItem.value());
205
+ } else {
206
+ this.dispatch('combobox_create', [e.currentTarget.value], {bubbles: true})
207
+ }
208
+ e.currentTarget.value = '';
209
+ this.$combobox?.$list?.filter('');
210
+ return;
211
+ }
212
+ }
213
+
214
+ if (e.currentTarget.value.length) this.$combobox?.$chips?.$focusedChip?.blur();
215
+ })
216
+ }
217
+
218
+ static {
219
+ $.style(this, toCSS('input[ui="combobox-input"]', {
220
+ border: 'unset',
221
+ background: 'unset',
222
+ color: 'oklch(from var(--fg) l c h / .9)',
223
+ fontSize: '0.875rem',
224
+ fontWeight: 'var(--font-weight-medium)',
225
+ fontFamily: 'inherit',
226
+ lineHeight: '1',
227
+ height: 'calc(var(--spacing) * 8)',
228
+ flex: '1',
229
+ minWidth: '2rem',
230
+ padding: `0 calc(var(--spacing) * 1.25)`,
231
+
232
+ '&:focus': {
233
+ outline: 'unset'
234
+ }
235
+ }))
236
+ }
237
+
238
+ override build(cascading?: boolean): this {
239
+ super.build(cascading);
240
+ this.$combobox = this.findAbove<Combobox>(proto => _instanceof(proto, Combobox));
241
+ if (this.$combobox) this.$combobox.$input = this;
242
+ return this;
243
+ }
244
+ }
245
+
246
+ export class ComboboxContent extends ElementProto {
247
+ static tagname = 'combobox-content';
248
+ $combobox: Combobox | null = _null;
249
+ $empty: ComboboxEmpty | null = _null;
250
+ constructor(props: $.Props, layout?: $.Layout<ComboboxContent>) {
251
+ super('combobox-content', props, layout);
252
+ }
253
+
254
+ static {
255
+ $.style(this, toCSS(this.tagname, {
256
+ position: 'absolute',
257
+ top: '0',
258
+ left: '0',
259
+ maxHeight: '50dvh',
260
+ overflowY: 'auto',
261
+ display: 'block',
262
+ boxSizing: 'border-box',
263
+ border: '1px solid var(--input)',
264
+ background: 'oklch(from var(--bg) l c h)',
265
+ padding: 'calc(var(--spacing) * 2) calc(var(--spacing) * 1.25)',
266
+ borderRadius: 'var(--radius)',
267
+ userSelect: 'none',
268
+
269
+ scrollbarWidth: 'thin',
270
+ scrollbarColor: 'var(--input) transparent',
271
+ }))
272
+ }
273
+
274
+ override build(cascading?: boolean): this {
275
+ super.build(cascading);
276
+ this.$combobox = this.findAbove<Combobox>(proto => _instanceof(proto, Combobox));
277
+ if (this.$combobox) this.$combobox.$content = this;
278
+ return this;
279
+ }
280
+ }
281
+
282
+ export class ComboboxEmpty extends ElementProto {
283
+ static tagname = 'combobox-empty'
284
+ $content: ComboboxContent | null = _null;
285
+ constructor(props: $.Props, layout?: $.Layout<ComboboxEmpty>) {
286
+ super('combobox-empty', props, layout);
287
+ }
288
+
289
+ static {
290
+ $.style(this, toCSS(this.tagname, {
291
+ ...item_css
292
+ }))
293
+ }
294
+
295
+ override build(cascading?: boolean): this {
296
+ super.build(cascading);
297
+ this.$content = this.findAbove<ComboboxContent>(proto => _instanceof(proto, ComboboxContent));
298
+ if (this.$content) this.$content.$empty = this;
299
+ return this;
300
+ }
301
+ }
302
+
303
+ declare global {
304
+ export namespace $ {
305
+ export interface ProtoEventMap<P extends Proto> {
306
+ combobox_create: [string];
307
+ combobox_select: [string];
308
+ combobox_unselect: [string];
309
+ combobox_input: [string];
310
+ }
311
+ }
312
+ }
@@ -0,0 +1,178 @@
1
+ import { toCSS } from "#lib/toCSS";
2
+ import { Icon } from "#structure/Icon";
3
+ import { ProxyProto, ElementProto } from "@amateras/core";
4
+ import { _null, _instanceof, isUndefined } from "@amateras/utils";
5
+ import { x_svg } from "../../icon/x.svg";
6
+ import { Combobox } from "./Combobox";
7
+ import type { ComboboxItemProps } from "./ComboboxList";
8
+
9
+
10
+ export class ComboboxChips extends ProxyProto {
11
+ static tagname = 'combobox-chips';
12
+ $combobox: Combobox | null = _null;
13
+ chipMap = new Map<any, ComboboxChip>();
14
+ $focusedChip: ComboboxChip | null = _null;
15
+ declare __child__: ComboboxChip;
16
+ constructor(layout?: $.Layout<ComboboxChips>) {
17
+ super(layout);
18
+ }
19
+
20
+ override build(cascading?: boolean): this {
21
+ super.build(cascading);
22
+ this.$combobox = this.findAbove<Combobox>(proto => _instanceof(proto, Combobox));
23
+ if (this.$combobox) this.$combobox.$chips = this;
24
+ return this;
25
+ }
26
+
27
+ appendChip(value: any) {
28
+ const $item = this.$combobox?.itemMap.get(value);
29
+ if (!$item) throw 'ComboboxChips.addChip: $item not found';
30
+ const $chip = this.chipMap.get(value) ?? $(ComboboxChip, {value, label: $item.label()});
31
+ this.chipMap.set(value, $chip);
32
+ if (this.protos.has($chip)) return;
33
+ this.append($chip);
34
+ if (!$chip.builded) $chip.build();
35
+ this.node?.replaceWith(...this.toDOM());
36
+ }
37
+
38
+ removeChip(value: any) {
39
+ const $chip = this.chipMap.get(value);
40
+ if (!$chip) throw 'ComboboxChips.removeChip: $chip not found';
41
+ $chip.remove();
42
+ $chip.removeNode();
43
+ }
44
+
45
+ switch(dir: 'left' | 'right') {
46
+ const $focusedChip = this.$focusedChip;
47
+ const chips = this.children;
48
+ const currentPosition = $focusedChip ? chips.indexOf($focusedChip) : dir === 'left' ? 0 : -1;
49
+ let targetIndex = dir === 'left' ? currentPosition - 1 : currentPosition + 1;
50
+ if (targetIndex < 0 || targetIndex >= chips.length) targetIndex = dir === 'left' ? -1 : 0;
51
+ this.focus(targetIndex);
52
+ }
53
+
54
+ focus(index: number) {
55
+ let $target = this.children.at(index);
56
+ this.$focusedChip?.blur();
57
+ $target?.focus();
58
+ }
59
+ }
60
+
61
+ export interface ComboboxChipProps {
62
+ value: OrSignal<any>;
63
+ label: OrSignal<string>;
64
+ }
65
+
66
+ export class ComboboxChip extends ElementProto {
67
+ static tagname = 'combobox-chip';
68
+ $chips: ComboboxChips | null = _null;
69
+ #value: any;
70
+ #label: string = '';
71
+ constructor(props: $.Props<ComboboxChipProps>, layout?: $.Layout<ComboboxChip>) {
72
+ super('combobox-chip', props, () => {
73
+ if (layout) layout(this);
74
+ else {
75
+ $([ this.#label ])
76
+ $(ComboboxChipRemoveButton)
77
+ }
78
+ });
79
+ }
80
+
81
+ static {
82
+ $.style(this, toCSS(this.tagname, {
83
+ display: 'inline-flex',
84
+ placeItems: 'center',
85
+ fontSize: 'var(--text-xs)',
86
+ padding: '0 calc(var(--spacing) * 1.5)',
87
+ background: 'var(--secondary-bg)',
88
+ borderRadius: 'calc(var(--radius) * .6)',
89
+ height: 'calc(var(--spacing) * 5.25)',
90
+ marginBlock: 'calc(var(--spacing))',
91
+
92
+ '&[focus]': {
93
+ outline: `2px solid var(--input)`
94
+ },
95
+
96
+ 'button[ui="combobox-chip-remove"]': {
97
+ background: 'unset',
98
+ border: 'unset',
99
+ color: 'oklch(from var(--fg) l c h / .9)',
100
+ paddingInlineStart: 'calc(var(--spacing) * 1.5)',
101
+
102
+ 'icon': {
103
+ height: 'calc(var(--spacing) * 3.25)',
104
+ width: 'calc(var(--spacing) * 3.25)'
105
+ }
106
+ }
107
+ }))
108
+ }
109
+
110
+ override build(cascading?: boolean): this {
111
+ super.build(cascading);
112
+ this.$chips = this.findAbove<ComboboxChips>(proto => _instanceof(proto, ComboboxChips));
113
+ return this;
114
+ }
115
+
116
+ override props({ value, label, ...props }: $.Props<ComboboxItemProps>): void {
117
+ super.props(props);
118
+ this.value(value);
119
+ this.label(label);
120
+ }
121
+
122
+ value(): any;
123
+ value(val?: OrSignal<any>): void;
124
+ value(val?: OrSignal<any>) {
125
+ if (!arguments.length) return this.#value;
126
+ if (isUndefined(val)) return;
127
+ $.resolve(val, val => {
128
+ this.$chips?.chipMap.delete(val);
129
+ this.$chips?.chipMap.set(val, this);
130
+ this.#value = val;
131
+ })
132
+ }
133
+
134
+ label(): string;
135
+ label(val?: OrSignal<string>): void;
136
+ label(val?: OrSignal<string>) {
137
+ if (!arguments.length) return this.#label;
138
+ if (isUndefined(val)) return;
139
+ $.resolve(val, val => {
140
+ this.#label = val;
141
+ })
142
+ }
143
+
144
+ delete() {
145
+ this.$chips?.$combobox?.select(this.#value, false);
146
+ }
147
+
148
+ focus() {
149
+ this.attr('focus', '')
150
+ if (this.$chips) this.$chips.$focusedChip = this;
151
+ }
152
+
153
+ blur() {
154
+ this.attr('focus', _null)
155
+ if (this.$chips) this.$chips.$focusedChip = _null;
156
+ }
157
+ }
158
+
159
+ export class ComboboxChipRemoveButton extends ElementProto<HTMLButtonElement> {
160
+ static tagname = 'button';
161
+ $chip: ComboboxChip | null = _null;
162
+ constructor(props: $.Props<{}, HTMLButtonElement>, layout?: $.Layout<ComboboxChipRemoveButton>) {
163
+ super('button', {ui: 'combobox-chip-remove', ...props}, () => {
164
+ if (layout) layout(this);
165
+ else $(Icon, {svg: x_svg})
166
+ })
167
+
168
+ this.on('click', () => {
169
+ this.$chip?.$chips?.$combobox?.select(this.$chip.value(), false)
170
+ })
171
+ }
172
+
173
+ override build(cascading?: boolean): this {
174
+ super.build(cascading);
175
+ this.$chip = this.findAbove<ComboboxChip>(proto => _instanceof(proto, ComboboxChip));
176
+ return this;
177
+ }
178
+ }
@@ -0,0 +1,209 @@
1
+ import { toCSS } from "#lib/toCSS";
2
+ import { Icon } from "#structure/Icon";
3
+ import { ElementProto } from "@amateras/core";
4
+ import { _null, _instanceof, forEach, isUndefined } from "@amateras/utils";
5
+ import { check_svg } from "../../icon/check.svg";
6
+ import { Combobox } from "./Combobox";
7
+ import { item_css } from "#lib/combobox_style";
8
+
9
+
10
+ export class ComboboxList extends ElementProto {
11
+ $combobox: Combobox | null = _null;
12
+ $focusedItem: ComboboxItem | ComboboxCreateItem | null = _null;
13
+ $createItem: ComboboxCreateItem | null = _null;
14
+ declare __child__: ComboboxItem | ComboboxCreateItem;
15
+ constructor(props: $.Props, layout?: $.Layout<ComboboxList>) {
16
+ super('combobox-list', props, layout);
17
+ }
18
+
19
+ override build(cascading?: boolean): this {
20
+ super.build(cascading);
21
+ this.$combobox = this.findAbove<Combobox>(proto => _instanceof(proto, Combobox));
22
+ if (this.$combobox) this.$combobox.$list = this;
23
+ return this;
24
+ }
25
+
26
+ filter(input: string) {
27
+ if (!this.$combobox) return;
28
+ const items: ComboboxItem[] = [];
29
+ if (this.$createItem) this.$createItem.visible = !!input && !this.$combobox.itemMap.has(input);
30
+ forEach(this.$combobox.itemMap, ([_, $item]) => {
31
+ $item.visible = false;
32
+ if ($item.text.toLowerCase().includes(input.toLowerCase())) {
33
+ $item.visible = true;
34
+ items.push($item);
35
+ }
36
+ })
37
+ if (this.$combobox?.$content?.$empty) this.$combobox.$content.$empty.visible = !this.$createItem?.visible && !items.length;
38
+ this.$combobox.$content?.toDOM();
39
+ }
40
+
41
+ switch(dir: 'up' | 'down') {
42
+ const $focusedItem = this.$focusedItem;
43
+ const items = this.children;
44
+ const currentPosition = $focusedItem ? items.indexOf($focusedItem) : dir === 'up' ? 0 : -1;
45
+ let targetIndex = dir === 'up' ? currentPosition - 1 : currentPosition + 1;
46
+ if (targetIndex < 0 || targetIndex >= items.length) targetIndex = dir === 'up' ? -1 : 0;
47
+ this.focus(targetIndex);
48
+ }
49
+
50
+ focus(index: number) {
51
+ let $target = this.children.at(index);
52
+ this.$focusedItem?.blur();
53
+ $target?.focus();
54
+ }
55
+
56
+ focusFirstItem() {
57
+ if (this.$createItem?.visible && this.children[1]) this.focus(1);
58
+ else this.focus(0)
59
+ }
60
+
61
+ override mutate(): void {
62
+ // super.mutate();
63
+ // const items = this.findBelowAll<ComboboxItem>(proto => _instanceof(proto, ComboboxItem));
64
+ // console.debug(items)
65
+ // this.$combobox?.itemMap.clear();
66
+ // forEach(items, $item => {
67
+ // this.$combobox?.itemMap.set($item.value(), $item);
68
+ // })
69
+ }
70
+ }
71
+
72
+ export interface ComboboxItemProps {
73
+ value: OrSignal<any>;
74
+ label: OrSignal<string>;
75
+ selected?: OrSignal<boolean>;
76
+ }
77
+
78
+ export class ComboboxItem extends ElementProto {
79
+ static tagname = 'combobox-item'
80
+ $combobox: Combobox | null = _null;
81
+ $list: ComboboxList | null = _null;
82
+ #value: any = _null;
83
+ #label: string = '';
84
+ #selected = false;
85
+ constructor(props: $.Props<ComboboxItemProps>, layout?: $.Layout<ComboboxItem>) {
86
+ super('combobox-item', props, () => {
87
+ if (layout) layout(this);
88
+ else $([ this.#label ])
89
+ $(Icon, {ui: 'combobox-item-check', svg: check_svg})
90
+ });
91
+
92
+ this.on('mousedown', e => e.preventDefault())
93
+
94
+ this.on('click', () => {
95
+ this.$combobox?.select(this.#value, !this.selected())
96
+ })
97
+ }
98
+
99
+ static {
100
+ $.style(this, toCSS(this.tagname, {
101
+ ...item_css,
102
+
103
+ 'icon[ui="combobox-item-check"]': {
104
+ visibility: 'hidden',
105
+ marginInlineStart: 'auto'
106
+ },
107
+
108
+ '&[selected] icon[ui="combobox-item-check"]': {
109
+ visibility: 'visible'
110
+ }
111
+ }))
112
+ }
113
+
114
+ override build(cascading?: boolean): this {
115
+ super.build(cascading);
116
+ this.$combobox = this.findAbove<Combobox>(proto => _instanceof(proto, Combobox));
117
+ this.$list = this.findAbove<ComboboxList>(proto => _instanceof(proto, ComboboxList));
118
+ this.$combobox?.itemMap.set(this.#value, this);
119
+ return this;
120
+ }
121
+
122
+ override props({ value, label, selected, ...props }: $.Props<ComboboxItemProps>): void {
123
+ super.props(props);
124
+ this.value(value);
125
+ this.label(label);
126
+ this.selected(selected);
127
+ }
128
+
129
+ value(): any;
130
+ value(val?: OrSignal<any>): void;
131
+ value(val?: OrSignal<any>) {
132
+ if (!arguments.length) return this.#value;
133
+ if (isUndefined(val)) return;
134
+ $.resolve(val, val => {
135
+ this.$combobox?.itemMap.delete(this.#value);
136
+ this.$combobox?.itemMap.set(val, this);
137
+ this.#value = val;
138
+ })
139
+ }
140
+
141
+ label(): string;
142
+ label(val?: OrSignal<string>): void;
143
+ label(val?: OrSignal<string>) {
144
+ if (!arguments.length) return this.#label;
145
+ if (isUndefined(val)) return;
146
+ $.resolve(val, val => {
147
+ this.#label = val;
148
+ })
149
+ }
150
+
151
+ selected(): boolean;
152
+ selected(val?: OrSignal<boolean>): void;
153
+ selected(val?: OrSignal<boolean>) {
154
+ if (!arguments.length) return this.#selected;
155
+ if (isUndefined(val)) return;
156
+ $.resolve(val, val => {
157
+ this.#selected = val;
158
+ this.attr('selected', val ? '' : null)
159
+ })
160
+ }
161
+
162
+ focus() {
163
+ this.attr('focus', '');
164
+ if (this.$list) this.$list.$focusedItem = this;
165
+ }
166
+
167
+ blur() {
168
+ this.attr('focus', _null)
169
+ if (this.$list) this.$list.$focusedItem = null;
170
+ }
171
+ }
172
+
173
+ export class ComboboxCreateItem extends ElementProto {
174
+ static tagname = 'combobox-create-item'
175
+ $combobox: Combobox | null = _null;
176
+ $list: ComboboxList | null = _null;
177
+ constructor(props: $.Props, layout?: $.Layout<ComboboxCreateItem>) {
178
+ super('combobox-create-item', props, layout);
179
+
180
+ this.on('mousedown', e => e.preventDefault())
181
+
182
+ this.on('click', () => {
183
+ })
184
+ }
185
+
186
+ static {
187
+ $.style(this, toCSS(this.tagname, {
188
+ ...item_css
189
+ }))
190
+ }
191
+
192
+ override build(cascading?: boolean): this {
193
+ super.build(cascading);
194
+ this.$combobox = this.findAbove<Combobox>(proto => _instanceof(proto, Combobox));
195
+ this.$list = this.findAbove<ComboboxList>(proto => _instanceof(proto, ComboboxList));
196
+ if (this.$list) this.$list.$createItem = this;
197
+ return this;
198
+ }
199
+
200
+ focus() {
201
+ this.attr('focus', '');
202
+ if (this.$list) this.$list.$focusedItem = this;
203
+ }
204
+
205
+ blur() {
206
+ this.attr('focus', _null)
207
+ if (this.$list) this.$list.$focusedItem = null;
208
+ }
209
+ }