@pictogrammers/components 0.5.15 → 0.5.17

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 (149) hide show
  1. package/package.json +1 -1
  2. package/pg/autocompleteItem/__examples__/basic/basic.html +3 -0
  3. package/pg/autocompleteItem/autocompleteItem.css +55 -0
  4. package/pg/autocompleteItem/autocompleteItem.html +1 -0
  5. package/pg/autocompleteItem/autocompleteItem.ts +65 -0
  6. package/pg/button/button.css +5 -0
  7. package/pg/buttonMenu/buttonMenu.ts +1 -1
  8. package/pg/inputCombobox/__examples__/basic/basic.ts +33 -0
  9. package/pg/inputCombobox/inputCombobox.ts +158 -0
  10. package/pg/inputCombobox/inputCombobox.ts.tmp.44252.751480cb81cc +158 -0
  11. package/pg/inputCombobox/inputCombobox.ts.tmp.44252.dcea847c40a8 +156 -0
  12. package/pg/inputPill/inputPill.html +6 -0
  13. package/pg/inputPill/inputPill.html.tmp.26852.84ac63f4c46f +6 -0
  14. package/pg/inputPill/inputPill.ts +41 -0
  15. package/pg/inputPills/__examples__/basic/basic.html +9 -0
  16. package/pg/inputPills/__examples__/basic/basic.html.tmp.26852.4771e4cad5b7 +9 -0
  17. package/pg/inputPills/__examples__/basic/basic.ts.tmp.26852.d8fe41df18bd +0 -0
  18. package/pg/inputPixelEditor/README.md +104 -69
  19. package/pg/inputPixelEditor/README.md.tmp.42596.658de5f0a6f5 +0 -0
  20. package/pg/inputPixelEditor/__examples__/basic/basic.ts +6 -5
  21. package/pg/inputPixelEditor/inputPixelEditor.ts +95 -23
  22. package/pg/inputPixelEditor/inputPixelEditor.ts.tmp.38516.96529dd82670 +1703 -0
  23. package/pg/inputPixelEditor/inputPixelEditor.ts.tmp.42596.48ecc3ad9c72 +1724 -0
  24. package/pg/inputPixelEditor/utils/bitmapToMask.ts +113 -79
  25. package/pg/inputPixelEditor/utils/getLassoPixels.ts +51 -0
  26. package/pg/inputSelect/inputSelect.ts +5 -1
  27. package/pg/inputText/README.md +6 -6
  28. package/pg/inputText/__examples__/basic/basic.ts +1 -1
  29. package/pg/inputText/inputText.css +2 -2
  30. package/pg/inputText copy/README.md +38 -0
  31. package/pg/inputText copy/inputText.css +41 -0
  32. package/pg/inputText copy/inputText.html +1 -0
  33. package/pg/inputText copy/inputText.spec.ts +59 -0
  34. package/pg/inputText copy/inputText.ts +67 -0
  35. package/pg/inputTextarea/README.md +39 -0
  36. package/pg/inputTextarea/__examples__/basic/basic.html +9 -0
  37. package/pg/inputTextarea/__examples__/basic/basic.ts +30 -0
  38. package/pg/inputTextarea/inputTextarea.ts +121 -0
  39. package/pg/inputTextarea copy/README.md +39 -0
  40. package/pg/inputTextarea copy/inputTextarea.css +41 -0
  41. package/pg/inputTextarea copy/inputTextarea.html +1 -0
  42. package/pg/inputTextarea copy/inputTextarea.spec.ts +59 -0
  43. package/pg/inputTextarea copy/inputTextarea.ts +79 -0
  44. package/pg/inputTextareaAutocomplete/README.md +70 -0
  45. package/pg/inputTextareaAutocomplete/__examples__/basic/basic.ts +67 -0
  46. package/pg/inputTextareaAutocomplete/inputTextareaAutocomplete.ts +267 -0
  47. package/pg/node/README.md +35 -0
  48. package/pg/node/__examples__/basic/basic.ts +34 -0
  49. package/pg/node/index.ts +3 -0
  50. package/pg/node/node.css +53 -0
  51. package/pg/node/node.html +5 -0
  52. package/pg/node/node.ts +270 -0
  53. package/pg/nodeEditorLink/nodeEditorLink.css +69 -0
  54. package/pg/nodeEditorLink/nodeEditorLink.css.tmp.1408.09e961fd0dcc +69 -0
  55. package/pg/nodeEditorLink/nodeEditorLink.html +9 -0
  56. package/pg/nodeEditorNumber/nodeEditorNumber.css +59 -0
  57. package/pg/nodeEditorNumber/nodeEditorNumber.ts +57 -0
  58. package/pg/nodeEditorRange/nodeEditorRange.css +23 -0
  59. package/pg/nodeEditorRange/nodeEditorRange.ts +69 -0
  60. package/pg/nodeEditorText/__examples__/basic/basic.html +9 -0
  61. package/pg/nodeEditorText/__examples__/basic/basic.ts +30 -0
  62. package/pg/nodeEditorText/nodeEditorText.css +17 -0
  63. package/pg/nodeEditorText/nodeEditorText.html +4 -0
  64. package/pg/nodeEditorText/nodeEditorText.ts +57 -0
  65. package/pg/nodeEditorText copy/README.md +35 -0
  66. package/pg/nodeEditorText copy/nodeEditorText.css +49 -0
  67. package/pg/nodeEditorText copy/nodeEditorText.html +4 -0
  68. package/pg/nodeEditorText copy/nodeEditorText.ts +50 -0
  69. package/pg/nodeEditorTextArray/nodeEditorTextArray.css +68 -0
  70. package/pg/nodeEditorTextArray/nodeEditorTextArray.html +9 -0
  71. package/pg/nodeEditorTextArray/nodeEditorTextArray.ts +162 -0
  72. package/pg/nodeEntry/nodeEntry.css +61 -0
  73. package/pg/nodeEntry/nodeEntry.ts +164 -0
  74. package/pg/nodeEntry/nodeEntry.ts.tmp.28724.dda67762bca5 +164 -0
  75. package/pg/nodeInputText/nodeInputText.css +75 -0
  76. package/pg/nodeInputText/nodeInputText.html +4 -0
  77. package/pg/nodeInputText/nodeInputText.ts +105 -0
  78. package/pg/nodeOutput/nodeOutput.css +32 -0
  79. package/pg/nodeResize/README.md +3 -0
  80. package/pg/nodeResize/THINKING.md +81 -0
  81. package/pg/nodeResize/THINKING.md.tmp.24812.99a70ba81062 +81 -0
  82. package/pg/nodeResize/basic/basic.ts +17 -0
  83. package/pg/nodeResize/dragUtil.ts +105 -0
  84. package/pg/nodeResize/dragUtil.ts.tmp.34372.787464c73309 +75 -0
  85. package/pg/nodeResize/nodeResize.css +187 -0
  86. package/pg/nodeResize/nodeResize.html +9 -0
  87. package/pg/nodeResize/nodeResize.ts +156 -0
  88. package/pg/nodes/README.md +177 -0
  89. package/pg/nodes/__examples__/basic/basic.html +33 -0
  90. package/pg/nodes/__examples__/basic/basic.ts +716 -0
  91. package/pg/nodes/__examples__/basic/exampleNodes.ts +129 -0
  92. package/pg/nodes/__examples__/basic/exampleScripts.ts +129 -0
  93. package/pg/nodes/__examples__/basic/keystore.ts +38 -0
  94. package/pg/nodes/executeInSandbox.ts +32 -0
  95. package/pg/nodes/index.ts +3 -0
  96. package/pg/nodes/nodes.css +69 -0
  97. package/pg/nodes/nodes.html +6 -0
  98. package/pg/nodes/nodes.ts +1240 -0
  99. package/pg/nodes copy/nodes.css +41 -0
  100. package/pg/nodes copy/nodes.html +1 -0
  101. package/pg/overlayContextMenu/__examples__/filter/filter.ts +87 -0
  102. package/pg/overlayContextMenu/overlayContextMenu.css +14 -0
  103. package/pg/overlayContextMenu/overlayContextMenu.html +4 -3
  104. package/pg/overlayContextMenu/overlayContextMenu.ts +6 -2
  105. package/pg/overlayContextMenu/overlayContextMenu.ts.tmp.37088.530ff1c0a321 +0 -0
  106. package/pg/overlayMenu/overlayMenu.css +3 -1
  107. package/pg/overlayMenu/overlayMenu.ts +11 -2
  108. package/pg/overlayMenu/overlayMenu.ts.tmp.44252.9bb22fd59ede +80 -0
  109. package/pg/overlaySelectMenu/overlaySelectMenu.css +2 -0
  110. package/pg/overlaySubMenu/overlaySubMenu.css +8 -6
  111. package/pg/poco/README.md +22 -0
  112. package/pg/poco/__examples__/basic/basic.html +11 -0
  113. package/pg/poco/__examples__/basic/basic.ts +95 -0
  114. package/pg/poco/__examples__/basic/examples.ts +258 -0
  115. package/pg/poco/__examples__/basic/myFont.ts +1300 -0
  116. package/pg/poco/index.ts +3 -0
  117. package/pg/poco/mockPoco.ts +640 -0
  118. package/pg/poco/poco.css +7 -0
  119. package/pg/poco/poco.html +1 -0
  120. package/pg/poco/poco.ts +75 -0
  121. package/pg/popover/README.md +47 -0
  122. package/pg/popover/__examples__/basic/basic.html +3 -0
  123. package/pg/popover/popover.html +3 -0
  124. package/pg/popover/popover.ts +44 -0
  125. package/pg/popoverAutocomplete/__examples__/basic/basic.html +4 -0
  126. package/pg/popoverAutocomplete/__examples__/basic/basic.ts +46 -0
  127. package/pg/popoverAutocomplete/popoverAutocomplete.css +25 -0
  128. package/pg/popoverAutocomplete/popoverAutocomplete.html +1 -0
  129. package/pg/popoverAutocomplete/popoverAutocomplete.ts +134 -0
  130. package/pg/popoverAutocompleteItem/popoverAutocompleteItem.css +69 -0
  131. package/pg/popoverAutocompleteItem/popoverAutocompleteItem.ts +90 -0
  132. package/pg/table/__examples__/basic/basic.ts +1 -0
  133. package/pg/tableCellFileLocal/tableCellFileLocal.html +3 -0
  134. package/pg/tableCellFileLocal/tableCellFileLocal.html.tmp.20484.68ee2e30710e +3 -0
  135. package/pg/tableCellFileLocal/tableCellFileLocal.ts +44 -0
  136. package/pg/tableCellSelect/tableCellSelect.html +3 -0
  137. package/pg/tableCellSelect/tableCellSelect.html.tmp.41808.99c1a6f14340 +3 -0
  138. package/pg/tableCellSelect/tableCellSelect.ts +51 -0
  139. package/pg/tree/__examples__/basic/basic.ts +12 -5
  140. package/pg/tree/tree.html +1 -1
  141. package/pg/tree/tree.ts +385 -227
  142. package/pg/tree/tree.ts.tmp.38376.07d376a04f41 +343 -0
  143. package/pg/tree/tree.ts.tmp.38376.2a04599ade9f +361 -0
  144. package/pg/tree/tree.ts.tmp.38376.6cb2c663d476 +0 -0
  145. package/pg/tree/tree.ts.tmp.38376.ac5af1aff2b0 +393 -0
  146. package/pg/tree/tree.ts.tmp.38376.ac9acfc4f95f +324 -0
  147. package/pg/treeItem/treeItem.ts +536 -558
  148. package/pg/treeItem/treeItem.ts.tmp.38376.145d3121d648 +530 -0
  149. package/pg/treeItem/treeItem.ts.tmp.38376.b5224d48e5c0 +0 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pictogrammers/components",
3
3
  "type": "module",
4
- "version": "0.5.15",
4
+ "version": "0.5.17",
5
5
  "license": "MIT",
6
6
  "author": "Austin Andrews",
7
7
  "scripts": {
@@ -0,0 +1,3 @@
1
+ <div class="example">
2
+ <p>Used internally by <code>PgPopoverAutocomplete</code>.</p>
3
+ </div>
@@ -0,0 +1,55 @@
1
+ :host {
2
+ display: contents;
3
+ }
4
+
5
+ [part=label] {
6
+ outline: none;
7
+ display: flex;
8
+ align-items: center;
9
+ font-family: var(--pg-font-family, system-ui);
10
+ text-align: var(--pg-autocomplete-item-text-align, left);
11
+ background-color: var(--pg-autocomplete-item-background, transparent);
12
+ padding: var(--pg-autocomplete-item-padding, 0.25rem 0.5rem);
13
+ border-color: transparent;
14
+ border-width: 0;
15
+ border-style: solid;
16
+ border-radius: var(--pg-autocomplete-item-border-radius, 0.25rem);
17
+ color: var(--pg-autocomplete-item-color, #453C4F);
18
+ width: 100%;
19
+ cursor: pointer;
20
+ }
21
+
22
+ [part=label]:not(:disabled):hover {
23
+ background-color: var(--pg-autocomplete-item-selected-background, #453C4F);
24
+ color: #FFFFFF;
25
+ }
26
+
27
+ [part=label]:not(:disabled):active {
28
+ background-color: var(--pg-autocomplete-item-active-background, #5f516e);
29
+ color: #FFFFFF;
30
+ }
31
+
32
+ [part=label]:disabled {
33
+ color: var(--pg-autocomplete-item-disabled-color, rgba(69, 60, 79, 0.75));
34
+ cursor: default;
35
+ }
36
+
37
+ [part=label]:focus-visible {
38
+ position: relative;
39
+ }
40
+
41
+ [part=label]:focus-visible::after {
42
+ pointer-events: none;
43
+ content: '';
44
+ position: absolute;
45
+ top: -1px;
46
+ right: -1px;
47
+ bottom: -1px;
48
+ left: -1px;
49
+ border-radius: var(--pg-autocomplete-item-border-radius, 0.25rem);
50
+ box-shadow: 0 0 0 3px var(--pg-focus-color, rgba(79, 143, 249, 0.5));
51
+ }
52
+
53
+ [part=label]:focus-visible:not(:hover)::after {
54
+ background-color: var(--pg-focus-background-color, rgba(79, 143, 249, 0.1));
55
+ }
@@ -0,0 +1 @@
1
+ <button part="label"></button>
@@ -0,0 +1,65 @@
1
+ import { Component, Prop, Part } from '@pictogrammers/element';
2
+
3
+ import template from './autocompleteItem.html';
4
+ import style from './autocompleteItem.css';
5
+
6
+ @Component({
7
+ selector: 'pg-autocomplete-item',
8
+ style,
9
+ template
10
+ })
11
+ export default class PgAutocompleteItem extends HTMLElement {
12
+ static delegatesFocus = true;
13
+
14
+ @Prop() index: number = 0;
15
+ @Prop() label: string = '';
16
+ @Prop() value: any = null;
17
+ @Prop() disabled: boolean = false;
18
+
19
+ @Part() $label: HTMLButtonElement;
20
+
21
+ render(changes) {
22
+ if (changes.label) {
23
+ this.$label.textContent = this.label;
24
+ }
25
+ if (changes.disabled) {
26
+ this.$label.disabled = this.disabled;
27
+ }
28
+ }
29
+
30
+ connectedCallback() {
31
+ this.$label.addEventListener('click', () => {
32
+ this.dispatchEvent(new CustomEvent('select', {
33
+ detail: { index: this.index, label: this.label, value: this.value }
34
+ }));
35
+ });
36
+ this.$label.addEventListener('keydown', (e: KeyboardEvent) => {
37
+ switch (e.key) {
38
+ case 'ArrowDown':
39
+ this.dispatchEvent(new CustomEvent('down', {
40
+ detail: { index: this.index }
41
+ }));
42
+ e.preventDefault();
43
+ break;
44
+ case 'ArrowUp':
45
+ this.dispatchEvent(new CustomEvent('up', {
46
+ detail: { index: this.index }
47
+ }));
48
+ e.preventDefault();
49
+ break;
50
+ case 'Escape':
51
+ this.dispatchEvent(new CustomEvent('close'));
52
+ e.preventDefault();
53
+ break;
54
+ }
55
+ });
56
+ }
57
+
58
+ focus() {
59
+ this.$label.focus();
60
+ }
61
+
62
+ getHeight(): number {
63
+ return this.$label.getBoundingClientRect().height;
64
+ }
65
+ }
@@ -3,6 +3,11 @@
3
3
  width: fit-content;
4
4
  }
5
5
 
6
+ :host:has(button.block) {
7
+ display: flex;
8
+ width: unset;
9
+ }
10
+
6
11
  [part=button] {
7
12
  display: flex;
8
13
  align-items: center;
@@ -48,7 +48,7 @@ export default class PgButtonMenu extends HTMLElement {
48
48
  source: this,
49
49
  default: this.default ?? this.items[0],
50
50
  value: this.items.find(x => x.value === this.value) ?? null,
51
- items: this.items
51
+ items: this.items,
52
52
  });
53
53
  this.$icon.path = IconExpand;
54
54
  if (result !== undefined) {
@@ -0,0 +1,33 @@
1
+ import { Component, Part } from '@pictogrammers/element';
2
+ import PgInputCombobox from '../../inputCombobox';
3
+
4
+ import template from './basic.html';
5
+
6
+ @Component({
7
+ selector: 'x-pg-input-combobox-basic',
8
+ template
9
+ })
10
+ export default class XPgInputComboboxBasic extends HTMLElement {
11
+
12
+ @Part() $input: PgInputCombobox;
13
+ @Part() $value: HTMLSpanElement;
14
+
15
+ connectedCallback() {
16
+ this.$input.options = [
17
+ { label: 'Dipper Pines', value: 'Dipper Pines' },
18
+ { label: 'Mabel Pines', value: 'Mabel Pines' },
19
+ { label: 'Stanley "Grunkle Stan" Pines', value: 'Stanley Pines' },
20
+ { label: 'Soos Ramirez', value: 'Soos Ramirez' },
21
+ { label: 'Wendy Corduroy', value: 'Wendy Corduroy' },
22
+ { label: 'Bill Cipher', value: 'Bill Cipher' },
23
+ { label: 'Stanford Pines', value: 'Stanford Pines' },
24
+ ];
25
+ this.$input.addEventListener('change', this.#handleChange.bind(this));
26
+ }
27
+
28
+ #handleChange(e: any) {
29
+ const { value } = e.detail;
30
+ this.$input.value = value;
31
+ this.$value.textContent = value;
32
+ }
33
+ }
@@ -0,0 +1,158 @@
1
+ import { Component, Prop, Part } from '@pictogrammers/element';
2
+ import PgOverlayMenu from '../overlayMenu/overlayMenu';
3
+ import PgMenuItem from '../menuItem/menuItem';
4
+
5
+ import template from './inputCombobox.html';
6
+ import style from './inputCombobox.css';
7
+
8
+ interface InputComboboxItem {
9
+ label: string;
10
+ value: string;
11
+ }
12
+
13
+ @Component({
14
+ selector: 'pg-input-combobox',
15
+ style,
16
+ template
17
+ })
18
+ export default class PgInputCombobox extends HTMLElement {
19
+ @Prop() options: InputComboboxItem[] = [];
20
+ @Prop() value: string = '';
21
+ @Prop() name: string = '';
22
+ @Prop() placeholder: string = '';
23
+
24
+ @Part() $container: HTMLDivElement;
25
+ @Part() $input: HTMLInputElement;
26
+
27
+ connectedCallback() {
28
+ this.$container.addEventListener('mousedown', this.#handleMousedown.bind(this));
29
+ this.$input.addEventListener('pointerdown', this.#handlePointerdown.bind(this));
30
+ this.$input.addEventListener('focus', this.#handleFocus.bind(this));
31
+ this.$input.addEventListener('click', this.#handleClick.bind(this));
32
+ this.$input.addEventListener('input', this.#handleInput.bind(this));
33
+ this.$input.addEventListener('keydown', this.#handleKeydown.bind(this));
34
+ }
35
+
36
+ render(changes) {
37
+ if (changes.value) {
38
+ const item = this.options.find(x => x.value === this.value);
39
+ this.$input.value = item ? item.label : '';
40
+ }
41
+ if (changes.placeholder) {
42
+ this.$input.placeholder = this.placeholder;
43
+ }
44
+ }
45
+
46
+ #menuOpen = false;
47
+ #activeOverlay: PgOverlayMenu | null = null;
48
+ #pointerOnInput = false;
49
+
50
+ #toMenuItems(items: InputComboboxItem[]) {
51
+ return items.map(item => ({
52
+ label: item.label,
53
+ value: item.value,
54
+ type: PgMenuItem,
55
+ }));
56
+ }
57
+
58
+ #getFilteredItems(): InputComboboxItem[] {
59
+ const query = this.$input.value.toLowerCase().trim();
60
+ if (!query) return [...this.options];
61
+ return this.options.filter(x => x.label.toLowerCase().includes(query));
62
+ }
63
+
64
+ async #openMenu() {
65
+ if (this.#menuOpen || this.options.length === 0) return;
66
+ this.#menuOpen = true;
67
+
68
+ const filtered = this.#getFilteredItems();
69
+ const menuItems = this.#toMenuItems(filtered);
70
+
71
+ const prevMenus = new Set(Array.from(document.querySelectorAll('pg-overlay-menu')));
72
+ const promise = PgOverlayMenu.open({
73
+ source: this.$container,
74
+ items: menuItems,
75
+ preventFocus: true,
76
+ });
77
+ const newEl = Array.from(document.querySelectorAll('pg-overlay-menu')).find(
78
+ el => !prevMenus.has(el as PgOverlayMenu)
79
+ ) as PgOverlayMenu | null;
80
+ this.#activeOverlay = newEl ?? null;
81
+
82
+ const result = await promise;
83
+ this.#menuOpen = false;
84
+ this.#activeOverlay = null;
85
+
86
+ if (result !== undefined) {
87
+ const { item } = result;
88
+ if (item) {
89
+ this.$input.value = item.label;
90
+ this.dispatchEvent(new CustomEvent('change', {
91
+ detail: { value: item.value, label: item.label }
92
+ }));
93
+ }
94
+ }
95
+ }
96
+
97
+ #handleMousedown(e: MouseEvent) {
98
+ if (e.target !== this.$input) {
99
+ e.preventDefault();
100
+ this.$input.focus();
101
+ }
102
+ }
103
+
104
+ #handleFocus() {
105
+ // Defer past the click event: focus fires on mousedown, but the click that
106
+ // triggered it would otherwise be seen as an outside-click by popover="auto"
107
+ setTimeout(() => {
108
+ if (!this.#menuOpen) {
109
+ this.#openMenu();
110
+ }
111
+ }, 0);
112
+ }
113
+
114
+ #handleClick() {
115
+ // Handles the case where the input is already focused (no focus event fires)
116
+ if (!this.#menuOpen) {
117
+ this.#openMenu();
118
+ }
119
+ }
120
+
121
+ #handleInput() {
122
+ if (this.#menuOpen && this.#activeOverlay) {
123
+ const filtered = this.#getFilteredItems();
124
+ if (filtered.length === 0) {
125
+ this.#activeOverlay.close(undefined);
126
+ return;
127
+ }
128
+ const menuItems = (this.#activeOverlay as any).$menu?.items;
129
+ if (menuItems) {
130
+ menuItems.splice(0, menuItems.length, ...this.#toMenuItems(filtered));
131
+ }
132
+ } else {
133
+ this.#openMenu();
134
+ }
135
+ }
136
+
137
+ #handleKeydown(e: KeyboardEvent) {
138
+ switch (e.key) {
139
+ case 'ArrowDown':
140
+ e.preventDefault();
141
+ if (this.#activeOverlay) {
142
+ (this.#activeOverlay as any).$menu?.focus(0);
143
+ } else {
144
+ this.#openMenu();
145
+ }
146
+ break;
147
+ case 'Escape':
148
+ if (this.#activeOverlay) {
149
+ this.#activeOverlay.close(undefined);
150
+ }
151
+ break;
152
+ }
153
+ }
154
+
155
+ focus() {
156
+ this.$input.focus();
157
+ }
158
+ }
@@ -0,0 +1,158 @@
1
+ import { Component, Prop, Part } from '@pictogrammers/element';
2
+ import PgOverlayMenu from '../overlayMenu/overlayMenu';
3
+ import PgMenuItem from '../menuItem/menuItem';
4
+
5
+ import template from './inputCombobox.html';
6
+ import style from './inputCombobox.css';
7
+
8
+ interface InputComboboxItem {
9
+ label: string;
10
+ value: string;
11
+ }
12
+
13
+ @Component({
14
+ selector: 'pg-input-combobox',
15
+ style,
16
+ template
17
+ })
18
+ export default class PgInputCombobox extends HTMLElement {
19
+ @Prop() options: InputComboboxItem[] = [];
20
+ @Prop() value: string = '';
21
+ @Prop() name: string = '';
22
+ @Prop() placeholder: string = '';
23
+
24
+ @Part() $container: HTMLDivElement;
25
+ @Part() $input: HTMLInputElement;
26
+
27
+ connectedCallback() {
28
+ this.$container.addEventListener('mousedown', this.#handleMousedown.bind(this));
29
+ this.$input.addEventListener('pointerdown', this.#handlePointerdown.bind(this));
30
+ this.$input.addEventListener('focus', this.#handleFocus.bind(this));
31
+ this.$input.addEventListener('click', this.#handleClick.bind(this));
32
+ this.$input.addEventListener('input', this.#handleInput.bind(this));
33
+ this.$input.addEventListener('keydown', this.#handleKeydown.bind(this));
34
+ }
35
+
36
+ render(changes) {
37
+ if (changes.value) {
38
+ const item = this.options.find(x => x.value === this.value);
39
+ this.$input.value = item ? item.label : '';
40
+ }
41
+ if (changes.placeholder) {
42
+ this.$input.placeholder = this.placeholder;
43
+ }
44
+ }
45
+
46
+ #menuOpen = false;
47
+ #activeOverlay: PgOverlayMenu | null = null;
48
+ #pointerOnInput = false;
49
+
50
+ #toMenuItems(items: InputComboboxItem[]) {
51
+ return items.map(item => ({
52
+ label: item.label,
53
+ value: item.value,
54
+ type: PgMenuItem,
55
+ }));
56
+ }
57
+
58
+ #getFilteredItems(): InputComboboxItem[] {
59
+ const query = this.$input.value.toLowerCase().trim();
60
+ if (!query) return [...this.options];
61
+ return this.options.filter(x => x.label.toLowerCase().includes(query));
62
+ }
63
+
64
+ async #openMenu() {
65
+ if (this.#menuOpen || this.options.length === 0) return;
66
+ this.#menuOpen = true;
67
+
68
+ const filtered = this.#getFilteredItems();
69
+ const menuItems = this.#toMenuItems(filtered);
70
+
71
+ const prevMenus = new Set(Array.from(document.querySelectorAll('pg-overlay-menu')));
72
+ const promise = PgOverlayMenu.open({
73
+ source: this.$container,
74
+ items: menuItems,
75
+ preventFocus: true,
76
+ });
77
+ const newEl = Array.from(document.querySelectorAll('pg-overlay-menu')).find(
78
+ el => !prevMenus.has(el as PgOverlayMenu)
79
+ ) as PgOverlayMenu | null;
80
+ this.#activeOverlay = newEl ?? null;
81
+
82
+ const result = await promise;
83
+ this.#menuOpen = false;
84
+ this.#activeOverlay = null;
85
+
86
+ if (result !== undefined) {
87
+ const { item } = result;
88
+ if (item) {
89
+ this.$input.value = item.label;
90
+ this.dispatchEvent(new CustomEvent('change', {
91
+ detail: { value: item.value, label: item.label }
92
+ }));
93
+ }
94
+ }
95
+ }
96
+
97
+ #handleMousedown(e: MouseEvent) {
98
+ if (e.target !== this.$input) {
99
+ e.preventDefault();
100
+ this.$input.focus();
101
+ }
102
+ }
103
+
104
+ #handleFocus() {
105
+ // Defer past the click event: focus fires on mousedown, but the click that
106
+ // triggered it would otherwise be seen as an outside-click by popover="auto"
107
+ setTimeout(() => {
108
+ if (!this.#menuOpen) {
109
+ this.#openMenu();
110
+ }
111
+ }, 0);
112
+ }
113
+
114
+ #handleClick() {
115
+ // Handles the case where the input is already focused (no focus event fires)
116
+ if (!this.#menuOpen) {
117
+ this.#openMenu();
118
+ }
119
+ }
120
+
121
+ #handleInput() {
122
+ if (this.#menuOpen && this.#activeOverlay) {
123
+ const filtered = this.#getFilteredItems();
124
+ if (filtered.length === 0) {
125
+ this.#activeOverlay.close(undefined);
126
+ return;
127
+ }
128
+ const menuItems = (this.#activeOverlay as any).$menu?.items;
129
+ if (menuItems) {
130
+ menuItems.splice(0, menuItems.length, ...this.#toMenuItems(filtered));
131
+ }
132
+ } else {
133
+ this.#openMenu();
134
+ }
135
+ }
136
+
137
+ #handleKeydown(e: KeyboardEvent) {
138
+ switch (e.key) {
139
+ case 'ArrowDown':
140
+ e.preventDefault();
141
+ if (this.#activeOverlay) {
142
+ (this.#activeOverlay as any).$menu?.focus(0);
143
+ } else {
144
+ this.#openMenu();
145
+ }
146
+ break;
147
+ case 'Escape':
148
+ if (this.#activeOverlay) {
149
+ this.#activeOverlay.close(undefined);
150
+ }
151
+ break;
152
+ }
153
+ }
154
+
155
+ focus() {
156
+ this.$input.focus();
157
+ }
158
+ }
@@ -0,0 +1,156 @@
1
+ import { Component, Prop, Part } from '@pictogrammers/element';
2
+ import PgOverlayMenu from '../overlayMenu/overlayMenu';
3
+ import PgMenuItem from '../menuItem/menuItem';
4
+
5
+ import template from './inputCombobox.html';
6
+ import style from './inputCombobox.css';
7
+
8
+ interface InputComboboxItem {
9
+ label: string;
10
+ value: string;
11
+ }
12
+
13
+ @Component({
14
+ selector: 'pg-input-combobox',
15
+ style,
16
+ template
17
+ })
18
+ export default class PgInputCombobox extends HTMLElement {
19
+ @Prop() options: InputComboboxItem[] = [];
20
+ @Prop() value: string = '';
21
+ @Prop() name: string = '';
22
+ @Prop() placeholder: string = '';
23
+
24
+ @Part() $container: HTMLDivElement;
25
+ @Part() $input: HTMLInputElement;
26
+
27
+ connectedCallback() {
28
+ this.$container.addEventListener('mousedown', this.#handleMousedown.bind(this));
29
+ this.$input.addEventListener('focus', this.#handleFocus.bind(this));
30
+ this.$input.addEventListener('click', this.#handleClick.bind(this));
31
+ this.$input.addEventListener('input', this.#handleInput.bind(this));
32
+ this.$input.addEventListener('keydown', this.#handleKeydown.bind(this));
33
+ }
34
+
35
+ render(changes) {
36
+ if (changes.value) {
37
+ const item = this.options.find(x => x.value === this.value);
38
+ this.$input.value = item ? item.label : '';
39
+ }
40
+ if (changes.placeholder) {
41
+ this.$input.placeholder = this.placeholder;
42
+ }
43
+ }
44
+
45
+ #menuOpen = false;
46
+ #activeOverlay: PgOverlayMenu | null = null;
47
+
48
+ #toMenuItems(items: InputComboboxItem[]) {
49
+ return items.map(item => ({
50
+ label: item.label,
51
+ value: item.value,
52
+ type: PgMenuItem,
53
+ }));
54
+ }
55
+
56
+ #getFilteredItems(): InputComboboxItem[] {
57
+ const query = this.$input.value.toLowerCase().trim();
58
+ if (!query) return [...this.options];
59
+ return this.options.filter(x => x.label.toLowerCase().includes(query));
60
+ }
61
+
62
+ async #openMenu() {
63
+ if (this.#menuOpen || this.options.length === 0) return;
64
+ this.#menuOpen = true;
65
+
66
+ const filtered = this.#getFilteredItems();
67
+ const menuItems = this.#toMenuItems(filtered);
68
+
69
+ const prevMenus = new Set(Array.from(document.querySelectorAll('pg-overlay-menu')));
70
+ const promise = PgOverlayMenu.open({
71
+ source: this.$container,
72
+ items: menuItems,
73
+ preventFocus: true,
74
+ });
75
+ const newEl = Array.from(document.querySelectorAll('pg-overlay-menu')).find(
76
+ el => !prevMenus.has(el as PgOverlayMenu)
77
+ ) as PgOverlayMenu | null;
78
+ this.#activeOverlay = newEl ?? null;
79
+
80
+ const result = await promise;
81
+ this.#menuOpen = false;
82
+ this.#activeOverlay = null;
83
+
84
+ if (result !== undefined) {
85
+ const { item } = result;
86
+ if (item) {
87
+ this.$input.value = item.label;
88
+ this.dispatchEvent(new CustomEvent('change', {
89
+ detail: { value: item.value, label: item.label }
90
+ }));
91
+ }
92
+ }
93
+ }
94
+
95
+ #handleMousedown(e: MouseEvent) {
96
+ if (e.target !== this.$input) {
97
+ e.preventDefault();
98
+ this.$input.focus();
99
+ }
100
+ }
101
+
102
+ #handleFocus() {
103
+ // Defer past the click event: focus fires on mousedown, but the click that
104
+ // triggered it would otherwise be seen as an outside-click by popover="auto"
105
+ setTimeout(() => {
106
+ if (!this.#menuOpen) {
107
+ this.#openMenu();
108
+ }
109
+ }, 0);
110
+ }
111
+
112
+ #handleClick() {
113
+ // Handles the case where the input is already focused (no focus event fires)
114
+ if (!this.#menuOpen) {
115
+ this.#openMenu();
116
+ }
117
+ }
118
+
119
+ #handleInput() {
120
+ if (this.#menuOpen && this.#activeOverlay) {
121
+ const filtered = this.#getFilteredItems();
122
+ if (filtered.length === 0) {
123
+ this.#activeOverlay.close(undefined);
124
+ return;
125
+ }
126
+ const menuItems = (this.#activeOverlay as any).$menu?.items;
127
+ if (menuItems) {
128
+ menuItems.splice(0, menuItems.length, ...this.#toMenuItems(filtered));
129
+ }
130
+ } else {
131
+ this.#openMenu();
132
+ }
133
+ }
134
+
135
+ #handleKeydown(e: KeyboardEvent) {
136
+ switch (e.key) {
137
+ case 'ArrowDown':
138
+ e.preventDefault();
139
+ if (this.#activeOverlay) {
140
+ (this.#activeOverlay as any).$menu?.focus(0);
141
+ } else {
142
+ this.#openMenu();
143
+ }
144
+ break;
145
+ case 'Escape':
146
+ if (this.#activeOverlay) {
147
+ this.#activeOverlay.close(undefined);
148
+ }
149
+ break;
150
+ }
151
+ }
152
+
153
+ focus() {
154
+ this.$input.focus();
155
+ }
156
+ }
@@ -0,0 +1,6 @@
1
+ <span part="label"></span>
2
+ <button part="remove" type="button" aria-label="Remove">
3
+ <svg part="removeIcon" viewBox="0 0 24 24">
4
+ <path fill="currentColor" d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" />
5
+ </svg>
6
+ </button>
@@ -0,0 +1,6 @@
1
+ <span part="label"></span>
2
+ <button part="remove" type="button" aria-label="Remove">
3
+ <svg part="removeIcon" viewBox="0 0 24 24">
4
+ <path fill="currentColor" d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" />
5
+ </svg>
6
+ </button>