@pictogrammers/components 0.5.14 → 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 (153) 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/inputNumber/inputNumber.ts +31 -13
  13. package/pg/inputPill/inputPill.html +6 -0
  14. package/pg/inputPill/inputPill.html.tmp.26852.84ac63f4c46f +6 -0
  15. package/pg/inputPill/inputPill.ts +41 -0
  16. package/pg/inputPills/__examples__/basic/basic.html +9 -0
  17. package/pg/inputPills/__examples__/basic/basic.html.tmp.26852.4771e4cad5b7 +9 -0
  18. package/pg/inputPills/__examples__/basic/basic.ts.tmp.26852.d8fe41df18bd +0 -0
  19. package/pg/inputPixelEditor/README.md +104 -69
  20. package/pg/inputPixelEditor/README.md.tmp.42596.658de5f0a6f5 +0 -0
  21. package/pg/inputPixelEditor/__examples__/basic/basic.ts +6 -5
  22. package/pg/inputPixelEditor/inputPixelEditor.ts +95 -23
  23. package/pg/inputPixelEditor/inputPixelEditor.ts.tmp.38516.96529dd82670 +1703 -0
  24. package/pg/inputPixelEditor/inputPixelEditor.ts.tmp.42596.48ecc3ad9c72 +1724 -0
  25. package/pg/inputPixelEditor/utils/bitmapToMask.ts +113 -79
  26. package/pg/inputPixelEditor/utils/getLassoPixels.ts +51 -0
  27. package/pg/inputRange/inputRange.css +7 -0
  28. package/pg/inputSelect/__examples__/basic/basic.ts +2 -2
  29. package/pg/inputSelect/inputSelect.css +1 -1
  30. package/pg/inputSelect/inputSelect.ts +62 -5
  31. package/pg/inputText/README.md +6 -6
  32. package/pg/inputText/__examples__/basic/basic.ts +1 -1
  33. package/pg/inputText/inputText.css +2 -2
  34. package/pg/inputText copy/README.md +38 -0
  35. package/pg/inputText copy/inputText.css +41 -0
  36. package/pg/inputText copy/inputText.html +1 -0
  37. package/pg/inputText copy/inputText.spec.ts +59 -0
  38. package/pg/inputText copy/inputText.ts +67 -0
  39. package/pg/inputTextarea/README.md +39 -0
  40. package/pg/inputTextarea/__examples__/basic/basic.html +9 -0
  41. package/pg/inputTextarea/__examples__/basic/basic.ts +30 -0
  42. package/pg/inputTextarea/inputTextarea.ts +121 -0
  43. package/pg/inputTextarea copy/README.md +39 -0
  44. package/pg/inputTextarea copy/inputTextarea.css +41 -0
  45. package/pg/inputTextarea copy/inputTextarea.html +1 -0
  46. package/pg/inputTextarea copy/inputTextarea.spec.ts +59 -0
  47. package/pg/inputTextarea copy/inputTextarea.ts +79 -0
  48. package/pg/inputTextareaAutocomplete/README.md +70 -0
  49. package/pg/inputTextareaAutocomplete/__examples__/basic/basic.ts +67 -0
  50. package/pg/inputTextareaAutocomplete/inputTextareaAutocomplete.ts +267 -0
  51. package/pg/node/README.md +35 -0
  52. package/pg/node/__examples__/basic/basic.ts +34 -0
  53. package/pg/node/index.ts +3 -0
  54. package/pg/node/node.css +53 -0
  55. package/pg/node/node.html +5 -0
  56. package/pg/node/node.ts +270 -0
  57. package/pg/nodeEditorLink/nodeEditorLink.css +69 -0
  58. package/pg/nodeEditorLink/nodeEditorLink.css.tmp.1408.09e961fd0dcc +69 -0
  59. package/pg/nodeEditorLink/nodeEditorLink.html +9 -0
  60. package/pg/nodeEditorNumber/nodeEditorNumber.css +59 -0
  61. package/pg/nodeEditorNumber/nodeEditorNumber.ts +57 -0
  62. package/pg/nodeEditorRange/nodeEditorRange.css +23 -0
  63. package/pg/nodeEditorRange/nodeEditorRange.ts +69 -0
  64. package/pg/nodeEditorText/__examples__/basic/basic.html +9 -0
  65. package/pg/nodeEditorText/__examples__/basic/basic.ts +30 -0
  66. package/pg/nodeEditorText/nodeEditorText.css +17 -0
  67. package/pg/nodeEditorText/nodeEditorText.html +4 -0
  68. package/pg/nodeEditorText/nodeEditorText.ts +57 -0
  69. package/pg/nodeEditorText copy/README.md +35 -0
  70. package/pg/nodeEditorText copy/nodeEditorText.css +49 -0
  71. package/pg/nodeEditorText copy/nodeEditorText.html +4 -0
  72. package/pg/nodeEditorText copy/nodeEditorText.ts +50 -0
  73. package/pg/nodeEditorTextArray/nodeEditorTextArray.css +68 -0
  74. package/pg/nodeEditorTextArray/nodeEditorTextArray.html +9 -0
  75. package/pg/nodeEditorTextArray/nodeEditorTextArray.ts +162 -0
  76. package/pg/nodeEntry/nodeEntry.css +61 -0
  77. package/pg/nodeEntry/nodeEntry.ts +164 -0
  78. package/pg/nodeEntry/nodeEntry.ts.tmp.28724.dda67762bca5 +164 -0
  79. package/pg/nodeInputText/nodeInputText.css +75 -0
  80. package/pg/nodeInputText/nodeInputText.html +4 -0
  81. package/pg/nodeInputText/nodeInputText.ts +105 -0
  82. package/pg/nodeOutput/nodeOutput.css +32 -0
  83. package/pg/nodeResize/README.md +3 -0
  84. package/pg/nodeResize/THINKING.md +81 -0
  85. package/pg/nodeResize/THINKING.md.tmp.24812.99a70ba81062 +81 -0
  86. package/pg/nodeResize/basic/basic.ts +17 -0
  87. package/pg/nodeResize/dragUtil.ts +105 -0
  88. package/pg/nodeResize/dragUtil.ts.tmp.34372.787464c73309 +75 -0
  89. package/pg/nodeResize/nodeResize.css +187 -0
  90. package/pg/nodeResize/nodeResize.html +9 -0
  91. package/pg/nodeResize/nodeResize.ts +156 -0
  92. package/pg/nodes/README.md +177 -0
  93. package/pg/nodes/__examples__/basic/basic.html +33 -0
  94. package/pg/nodes/__examples__/basic/basic.ts +716 -0
  95. package/pg/nodes/__examples__/basic/exampleNodes.ts +129 -0
  96. package/pg/nodes/__examples__/basic/exampleScripts.ts +129 -0
  97. package/pg/nodes/__examples__/basic/keystore.ts +38 -0
  98. package/pg/nodes/executeInSandbox.ts +32 -0
  99. package/pg/nodes/index.ts +3 -0
  100. package/pg/nodes/nodes.css +69 -0
  101. package/pg/nodes/nodes.html +6 -0
  102. package/pg/nodes/nodes.ts +1240 -0
  103. package/pg/nodes copy/nodes.css +41 -0
  104. package/pg/nodes copy/nodes.html +1 -0
  105. package/pg/overlayContextMenu/__examples__/filter/filter.ts +87 -0
  106. package/pg/overlayContextMenu/overlayContextMenu.css +14 -0
  107. package/pg/overlayContextMenu/overlayContextMenu.html +4 -3
  108. package/pg/overlayContextMenu/overlayContextMenu.ts +6 -2
  109. package/pg/overlayContextMenu/overlayContextMenu.ts.tmp.37088.530ff1c0a321 +0 -0
  110. package/pg/overlayMenu/overlayMenu.css +3 -1
  111. package/pg/overlayMenu/overlayMenu.ts +11 -2
  112. package/pg/overlayMenu/overlayMenu.ts.tmp.44252.9bb22fd59ede +80 -0
  113. package/pg/overlaySelectMenu/overlaySelectMenu.css +2 -0
  114. package/pg/overlaySubMenu/overlaySubMenu.css +8 -6
  115. package/pg/poco/README.md +22 -0
  116. package/pg/poco/__examples__/basic/basic.html +11 -0
  117. package/pg/poco/__examples__/basic/basic.ts +95 -0
  118. package/pg/poco/__examples__/basic/examples.ts +258 -0
  119. package/pg/poco/__examples__/basic/myFont.ts +1300 -0
  120. package/pg/poco/index.ts +3 -0
  121. package/pg/poco/mockPoco.ts +640 -0
  122. package/pg/poco/poco.css +7 -0
  123. package/pg/poco/poco.html +1 -0
  124. package/pg/poco/poco.ts +75 -0
  125. package/pg/popover/README.md +47 -0
  126. package/pg/popover/__examples__/basic/basic.html +3 -0
  127. package/pg/popover/popover.html +3 -0
  128. package/pg/popover/popover.ts +44 -0
  129. package/pg/popoverAutocomplete/__examples__/basic/basic.html +4 -0
  130. package/pg/popoverAutocomplete/__examples__/basic/basic.ts +46 -0
  131. package/pg/popoverAutocomplete/popoverAutocomplete.css +25 -0
  132. package/pg/popoverAutocomplete/popoverAutocomplete.html +1 -0
  133. package/pg/popoverAutocomplete/popoverAutocomplete.ts +134 -0
  134. package/pg/popoverAutocompleteItem/popoverAutocompleteItem.css +69 -0
  135. package/pg/popoverAutocompleteItem/popoverAutocompleteItem.ts +90 -0
  136. package/pg/table/__examples__/basic/basic.ts +1 -0
  137. package/pg/tableCellFileLocal/tableCellFileLocal.html +3 -0
  138. package/pg/tableCellFileLocal/tableCellFileLocal.html.tmp.20484.68ee2e30710e +3 -0
  139. package/pg/tableCellFileLocal/tableCellFileLocal.ts +44 -0
  140. package/pg/tableCellSelect/tableCellSelect.html +3 -0
  141. package/pg/tableCellSelect/tableCellSelect.html.tmp.41808.99c1a6f14340 +3 -0
  142. package/pg/tableCellSelect/tableCellSelect.ts +51 -0
  143. package/pg/tree/__examples__/basic/basic.ts +12 -5
  144. package/pg/tree/tree.html +1 -1
  145. package/pg/tree/tree.ts +385 -227
  146. package/pg/tree/tree.ts.tmp.38376.07d376a04f41 +343 -0
  147. package/pg/tree/tree.ts.tmp.38376.2a04599ade9f +361 -0
  148. package/pg/tree/tree.ts.tmp.38376.6cb2c663d476 +0 -0
  149. package/pg/tree/tree.ts.tmp.38376.ac5af1aff2b0 +393 -0
  150. package/pg/tree/tree.ts.tmp.38376.ac9acfc4f95f +324 -0
  151. package/pg/treeItem/treeItem.ts +536 -558
  152. package/pg/treeItem/treeItem.ts.tmp.38376.145d3121d648 +530 -0
  153. package/pg/treeItem/treeItem.ts.tmp.38376.b5224d48e5c0 +0 -0
@@ -0,0 +1,343 @@
1
+ import { Component, Prop, Part, forEach, getProxyValue } from '@pictogrammers/element';
2
+
3
+ import PgTreeItem from '../treeItem/treeItem';
4
+
5
+ import template from './tree.html';
6
+ import style from './tree.css';
7
+
8
+ export type SelectedTreeItem = {
9
+ indexes: number[];
10
+ remove: () => void;
11
+ getData: () => any;
12
+ getParentData: () => any;
13
+ move: (item: SelectedTreeItem, position: string) => void;
14
+ }
15
+
16
+ @Component({
17
+ selector: 'pg-tree',
18
+ style,
19
+ template
20
+ })
21
+ export default class PgTree extends HTMLElement {
22
+ @Prop() items: any[] = [];
23
+
24
+ @Part() $items: HTMLDivElement;
25
+
26
+ #selectedIndexes = new Map();
27
+ #lastSelectedIndexes: number[] | null = null;
28
+
29
+ connectedCallback() {
30
+ forEach({
31
+ container: this.$items,
32
+ items: this.items,
33
+ type: () => PgTreeItem
34
+ });
35
+ this.$items.addEventListener('action', (e: any) => {
36
+ e.stopPropagation();
37
+ this.dispatchEvent(new CustomEvent('action', {
38
+ detail: {
39
+ actionIndex: e.detail.actionIndex,
40
+ item: this.#wrap(e.detail.indexes),
41
+ }
42
+ }));
43
+ });
44
+ this.$items.addEventListener('move', (e: any) => {
45
+ e.stopPropagation();
46
+ const selectedIdxsList = [...this.#selectedIndexes.values()] as number[][];
47
+ this.#batchMove(selectedIdxsList, e.detail.indexes, e.detail.position);
48
+ this.dispatchEvent(new CustomEvent('move', {
49
+ detail: { position: e.detail.position }
50
+ }));
51
+ });
52
+ this.$items.addEventListener('rename', (e: any) => {
53
+ e.stopPropagation();
54
+ this.dispatchEvent(new CustomEvent('rename', {
55
+ detail: {
56
+ item: this.#wrap(e.detail.indexes),
57
+ label: e.detail.label,
58
+ }
59
+ }));
60
+ });
61
+ this.$items.addEventListener('toggle', (e: any) => {
62
+ const item = this.#getItem(e.detail.indexes);
63
+ item.expanded = !item.expanded;
64
+ });
65
+ this.$items.addEventListener('expand', (e: any) => {
66
+ this.#getItem(e.detail.indexes).expanded = true;
67
+ });
68
+ this.$items.addEventListener('menu', (e: any) => {
69
+ e.stopPropagation();
70
+ this.dispatchEvent(new CustomEvent('menu', {
71
+ detail: {
72
+ item: this.#wrap(e.detail.indexes),
73
+ x: e.detail.x,
74
+ y: e.detail.y,
75
+ }
76
+ }));
77
+ });
78
+ this.$items.addEventListener('select', (e: any) => {
79
+ e.stopPropagation();
80
+ const { indexes, type, ctrlKey, shiftKey } = e.detail;
81
+ const item = this.#getItem(indexes);
82
+ const unproxyItem = getProxyValue(item);
83
+
84
+ if (shiftKey && this.#lastSelectedIndexes) {
85
+ const visible = this.#getVisibleIndexes();
86
+ const lastPos = visible.findIndex(v => v.join(',') === this.#lastSelectedIndexes!.join(','));
87
+ const currPos = visible.findIndex(v => v.join(',') === indexes.join(','));
88
+ this.#selectedIndexes.forEach((x: any) => this.#getItem(x).selected = false);
89
+ this.#selectedIndexes.clear();
90
+ const start = Math.min(lastPos, currPos);
91
+ const end = Math.max(lastPos, currPos);
92
+ for (let i = start; i <= end; i++) {
93
+ const rangeItem = this.#getItem(visible[i]);
94
+ rangeItem.selected = true;
95
+ this.#selectedIndexes.set(getProxyValue(rangeItem), visible[i]);
96
+ }
97
+ } else {
98
+ if (!ctrlKey && this.#selectedIndexes.size && !this.#selectedIndexes.has(unproxyItem)) {
99
+ this.#selectedIndexes.forEach((x: any) => this.#getItem(x).selected = false);
100
+ this.#selectedIndexes.clear();
101
+ }
102
+ item.selected = type === 'rename' ? true : !item.selected;
103
+ if (item.selected) {
104
+ this.#selectedIndexes.set(unproxyItem, indexes);
105
+ this.#lastSelectedIndexes = indexes;
106
+ } else {
107
+ this.#selectedIndexes.delete(unproxyItem);
108
+ }
109
+ }
110
+
111
+ this.dispatchEvent(new CustomEvent('select', {
112
+ detail: {
113
+ items: [...this.#selectedIndexes.values()].map((idxs: any) => this.#wrap(idxs)),
114
+ }
115
+ }));
116
+ });
117
+ this.$items.addEventListener('up', (e: any) => {
118
+ e.stopPropagation();
119
+ const visible = this.#getVisibleIndexes();
120
+ const idx = visible.findIndex(v => v.join(',') === e.detail.indexes.join(','));
121
+ if (idx > 0) {
122
+ this.#focusItem(visible[idx - 1]);
123
+ }
124
+ });
125
+ this.$items.addEventListener('down', (e: any) => {
126
+ e.stopPropagation();
127
+ const visible = this.#getVisibleIndexes();
128
+ const idx = visible.findIndex(v => v.join(',') === e.detail.indexes.join(','));
129
+ if (idx < visible.length - 1) {
130
+ this.#focusItem(visible[idx + 1]);
131
+ }
132
+ });
133
+ this.$items.addEventListener('keydown', (e: any) => {
134
+ if (e.key === 'Delete') {
135
+ const toRemove = [...this.#selectedIndexes.values()] as number[][];
136
+ // Remove higher sibling indexes first to avoid index shifts
137
+ toRemove.sort((a, b) => {
138
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
139
+ const ai = i < a.length ? a[i] : -1;
140
+ const bi = i < b.length ? b[i] : -1;
141
+ if (ai !== bi) return bi - ai;
142
+ }
143
+ return 0;
144
+ });
145
+ toRemove.forEach((indexes: number[]) => this.#removeItem(indexes));
146
+ this.#selectedIndexes.clear();
147
+ this.#lastSelectedIndexes = null;
148
+ }
149
+ });
150
+ this.$items.addEventListener('itemdragstart', (e: any) => {
151
+ const { indexes, callback, ctrlKey } = e.detail;
152
+ const item = this.#getItem(indexes);
153
+ const unproxyItem = getProxyValue(item);
154
+ if (!ctrlKey && this.#selectedIndexes.size && !this.#selectedIndexes.has(unproxyItem)) {
155
+ this.#selectedIndexes.forEach((x: any) => this.#getItem(x).selected = false);
156
+ this.#selectedIndexes.clear();
157
+ }
158
+ item.selected = true;
159
+ this.#selectedIndexes.set(unproxyItem, indexes);
160
+ let count = 0;
161
+ this.#selectedIndexes.forEach((idxs: any) => {
162
+ count = this.#getDragCount(this.#getItem(idxs), count);
163
+ });
164
+ callback(count);
165
+ this.$items.classList.toggle('dragging', true);
166
+ // Sync selection to consumers so #selectedItems reflects the dragged item
167
+ this.dispatchEvent(new CustomEvent('select', {
168
+ detail: {
169
+ items: [...this.#selectedIndexes.values()].map((idxs: any) => this.#wrap(idxs)),
170
+ }
171
+ }));
172
+ });
173
+ this.$items.addEventListener('itemdragend', () => {
174
+ this.$items.classList.toggle('dragging', false);
175
+ this.#clearDropHighlights(this.$items);
176
+ });
177
+ this.$items.addEventListener('itemdropenter', this.#handleDropEnter.bind(this));
178
+ }
179
+
180
+ #getDragCount(item: any, count: number): number {
181
+ count += 1;
182
+ if (item.items?.length) {
183
+ item.items.forEach((itm: any) => {
184
+ count = this.#getDragCount(itm, count);
185
+ });
186
+ }
187
+ return count;
188
+ }
189
+
190
+ #handleDropEnter(e: any) {
191
+ const { indexes, callback } = e.detail;
192
+ const excludes = this.#calculateDragExcludes();
193
+ const joined = indexes.join(',') as string;
194
+ // Use comma-terminated prefix to prevent "0,1" from matching "0,10"
195
+ const isInvalid = excludes.some(ex => joined === ex || joined.startsWith(ex + ','));
196
+ callback(!isInvalid);
197
+ }
198
+
199
+ #removeItem(indexes: number[]) {
200
+ const deleteIndex = indexes[indexes.length - 1];
201
+ const parentIndexes = indexes.slice(0, indexes.length - 1);
202
+ const parent = parentIndexes.reduce((acc: any, i) => acc.items[i], this);
203
+ parent.items.splice(deleteIndex, 1);
204
+ }
205
+
206
+ #getItem(indexes: number[]) {
207
+ return indexes.reduce((acc: any, i) => acc.items[i], this);
208
+ }
209
+
210
+ #getItemElement(indexes: number[]): PgTreeItem | null {
211
+ let container: HTMLElement = this.$items;
212
+ let element: PgTreeItem | null = null;
213
+ for (const i of indexes) {
214
+ element = container.children[i] as PgTreeItem;
215
+ if (!element) return null;
216
+ container = (element as any).$items;
217
+ if (!container) return null;
218
+ }
219
+ return element;
220
+ }
221
+
222
+ #focusItem(indexes: number[]) {
223
+ const element = this.#getItemElement(indexes);
224
+ if (element) {
225
+ (element as any).$labelButton.focus();
226
+ }
227
+ }
228
+
229
+ #getVisibleIndexes(): number[][] {
230
+ const result: number[][] = [];
231
+ const traverse = (items: any[], prefix: number[]) => {
232
+ items.forEach((item: any, i: number) => {
233
+ const indexes = [...prefix, i];
234
+ result.push(indexes);
235
+ if (item.expanded && item.items?.length) {
236
+ traverse(item.items, indexes);
237
+ }
238
+ });
239
+ };
240
+ traverse(this.items, []);
241
+ return result;
242
+ }
243
+
244
+ #clearDropHighlights(container: HTMLElement) {
245
+ Array.from(container.children).forEach((child: any) => {
246
+ if (child.$dropabove) {
247
+ child.$dropabove.classList.remove('drop');
248
+ child.$dropon.classList.remove('drop');
249
+ child.$dropbelow.classList.remove('drop');
250
+ }
251
+ if (child.$items) {
252
+ this.#clearDropHighlights(child.$items);
253
+ }
254
+ });
255
+ }
256
+
257
+ #batchMove(selectedIdxsList: number[][], targetIdxs: number[], position: string) {
258
+ if (selectedIdxsList.length === 0) return;
259
+
260
+ // Ascending order preserves the original relative order of dragged items on insertion
261
+ const ascending = [...selectedIdxsList].sort((a, b) => {
262
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
263
+ const av = i < a.length ? a[i] : -1;
264
+ const bv = i < b.length ? b[i] : -1;
265
+ if (av !== bv) return av - bv;
266
+ }
267
+ return 0;
268
+ });
269
+
270
+ // Capture data references before any structural changes
271
+ const caches = ascending.map(idxs => this.#getItem(idxs));
272
+
273
+ if (position === 'on') {
274
+ // Capture target by reference so it's unaffected by the removals below
275
+ const targetData = this.#getItem(targetIdxs);
276
+ // Remove descending so higher sibling indexes don't shift lower ones mid-loop
277
+ [...ascending].reverse().forEach(idxs => this.#removeItem(idxs));
278
+ if (!Array.isArray(targetData.items)) targetData.items = [];
279
+ caches.forEach(cache => targetData.items.push(cache));
280
+ targetData.expanded = true;
281
+ } else {
282
+ // Count how many selected items are in the same parent as the target and
283
+ // precede it — each one shifts the target index down by one after removal.
284
+ const targetParent = targetIdxs.slice(0, -1).join(',');
285
+ let shift = 0;
286
+ for (const sidxs of ascending) {
287
+ if (sidxs.slice(0, -1).join(',') === targetParent &&
288
+ sidxs[sidxs.length - 1] < targetIdxs[targetIdxs.length - 1]) {
289
+ shift++;
290
+ }
291
+ }
292
+ const adjustedIdx = targetIdxs[targetIdxs.length - 1] - shift;
293
+ const adjustedParent = targetIdxs.slice(0, -1);
294
+ // Remove descending to keep earlier indexes stable
295
+ [...ascending].reverse().forEach(idxs => this.#removeItem(idxs));
296
+ const insertAt = position === 'after' ? adjustedIdx + 1 : adjustedIdx;
297
+ if (adjustedParent.length === 0) {
298
+ this.items.splice(insertAt, 0, ...caches);
299
+ } else {
300
+ this.#getItem(adjustedParent).items.splice(insertAt, 0, ...caches);
301
+ }
302
+ }
303
+
304
+ caches.forEach(cache => { cache.selected = false; });
305
+ this.#selectedIndexes.clear();
306
+ this.#lastSelectedIndexes = null;
307
+ this.$items.classList.toggle('dragging', false);
308
+ }
309
+
310
+ #wrap(indexes: number[]): SelectedTreeItem {
311
+ return {
312
+ indexes,
313
+ remove: () => {
314
+ this.#removeItem(indexes);
315
+ this.#selectedIndexes.clear();
316
+ this.#lastSelectedIndexes = null;
317
+ },
318
+ getData: () => this.#getItem(indexes),
319
+ getParentData: () => {
320
+ const parent = indexes.slice(0, indexes.length - 1);
321
+ return parent.length === 0 ? this : this.#getItem(parent);
322
+ },
323
+ move: (item: SelectedTreeItem, position: string) => {
324
+ this.#batchMove([indexes], item.indexes, position);
325
+ }
326
+ };
327
+ }
328
+
329
+ unselect(indexes: number[]) {
330
+ const item = this.#getItem(indexes);
331
+ const unproxyItem = getProxyValue(item);
332
+ item.selected = false;
333
+ this.#selectedIndexes.delete(unproxyItem);
334
+ }
335
+
336
+ #calculateDragExcludes(): string[] {
337
+ const exclude: string[] = [];
338
+ this.#selectedIndexes.forEach((indexes: number[]) => {
339
+ exclude.push(indexes.join(','));
340
+ });
341
+ return exclude;
342
+ }
343
+ }
@@ -0,0 +1,361 @@
1
+ import { Component, Prop, Part, forEach, getProxyValue } from '@pictogrammers/element';
2
+
3
+ import PgTreeItem from '../treeItem/treeItem';
4
+
5
+ import template from './tree.html';
6
+ import style from './tree.css';
7
+
8
+ export type SelectedTreeItem = {
9
+ indexes: number[];
10
+ remove: () => void;
11
+ getData: () => any;
12
+ getParentData: () => any;
13
+ move: (item: SelectedTreeItem, position: string) => void;
14
+ }
15
+
16
+ @Component({
17
+ selector: 'pg-tree',
18
+ style,
19
+ template
20
+ })
21
+ export default class PgTree extends HTMLElement {
22
+ @Prop() items: any[] = [];
23
+
24
+ @Part() $items: HTMLDivElement;
25
+
26
+ #selectedIndexes = new Map();
27
+ #lastSelectedIndexes: number[] | null = null;
28
+ #draggingElements: Set<HTMLElement> = new Set();
29
+
30
+ connectedCallback() {
31
+ forEach({
32
+ container: this.$items,
33
+ items: this.items,
34
+ type: () => PgTreeItem
35
+ });
36
+ this.$items.addEventListener('action', (e: any) => {
37
+ e.stopPropagation();
38
+ this.dispatchEvent(new CustomEvent('action', {
39
+ detail: {
40
+ actionIndex: e.detail.actionIndex,
41
+ item: this.#wrap(e.detail.indexes),
42
+ }
43
+ }));
44
+ });
45
+ this.$items.addEventListener('move', (e: any) => {
46
+ e.stopPropagation();
47
+ const selectedIdxsList = [...this.#selectedIndexes.values()] as number[][];
48
+ this.#batchMove(selectedIdxsList, e.detail.indexes, e.detail.position);
49
+ this.dispatchEvent(new CustomEvent('move', {
50
+ detail: { position: e.detail.position }
51
+ }));
52
+ this.dispatchEvent(new CustomEvent('select', {
53
+ detail: {
54
+ items: [...this.#selectedIndexes.values()].map((idxs: any) => this.#wrap(idxs)),
55
+ }
56
+ }));
57
+ });
58
+ this.$items.addEventListener('rename', (e: any) => {
59
+ e.stopPropagation();
60
+ this.dispatchEvent(new CustomEvent('rename', {
61
+ detail: {
62
+ item: this.#wrap(e.detail.indexes),
63
+ label: e.detail.label,
64
+ }
65
+ }));
66
+ });
67
+ this.$items.addEventListener('toggle', (e: any) => {
68
+ const item = this.#getItem(e.detail.indexes);
69
+ item.expanded = !item.expanded;
70
+ });
71
+ this.$items.addEventListener('expand', (e: any) => {
72
+ this.#getItem(e.detail.indexes).expanded = true;
73
+ });
74
+ this.$items.addEventListener('menu', (e: any) => {
75
+ e.stopPropagation();
76
+ this.dispatchEvent(new CustomEvent('menu', {
77
+ detail: {
78
+ item: this.#wrap(e.detail.indexes),
79
+ x: e.detail.x,
80
+ y: e.detail.y,
81
+ }
82
+ }));
83
+ });
84
+ this.$items.addEventListener('select', (e: any) => {
85
+ e.stopPropagation();
86
+ const { indexes, type, ctrlKey, shiftKey } = e.detail;
87
+ const item = this.#getItem(indexes);
88
+ const unproxyItem = getProxyValue(item);
89
+
90
+ if (shiftKey && this.#lastSelectedIndexes) {
91
+ const visible = this.#getVisibleIndexes();
92
+ const lastPos = visible.findIndex(v => v.join(',') === this.#lastSelectedIndexes!.join(','));
93
+ const currPos = visible.findIndex(v => v.join(',') === indexes.join(','));
94
+ this.#selectedIndexes.forEach((x: any) => this.#getItem(x).selected = false);
95
+ this.#selectedIndexes.clear();
96
+ const start = Math.min(lastPos, currPos);
97
+ const end = Math.max(lastPos, currPos);
98
+ for (let i = start; i <= end; i++) {
99
+ const rangeItem = this.#getItem(visible[i]);
100
+ rangeItem.selected = true;
101
+ this.#selectedIndexes.set(getProxyValue(rangeItem), visible[i]);
102
+ }
103
+ } else {
104
+ if (!ctrlKey && this.#selectedIndexes.size && !this.#selectedIndexes.has(unproxyItem)) {
105
+ this.#selectedIndexes.forEach((x: any) => this.#getItem(x).selected = false);
106
+ this.#selectedIndexes.clear();
107
+ }
108
+ item.selected = type === 'rename' ? true : !item.selected;
109
+ if (item.selected) {
110
+ this.#selectedIndexes.set(unproxyItem, indexes);
111
+ this.#lastSelectedIndexes = indexes;
112
+ } else {
113
+ this.#selectedIndexes.delete(unproxyItem);
114
+ }
115
+ }
116
+
117
+ this.dispatchEvent(new CustomEvent('select', {
118
+ detail: {
119
+ items: [...this.#selectedIndexes.values()].map((idxs: any) => this.#wrap(idxs)),
120
+ }
121
+ }));
122
+ });
123
+ this.$items.addEventListener('up', (e: any) => {
124
+ e.stopPropagation();
125
+ const visible = this.#getVisibleIndexes();
126
+ const idx = visible.findIndex(v => v.join(',') === e.detail.indexes.join(','));
127
+ if (idx > 0) {
128
+ this.#focusItem(visible[idx - 1]);
129
+ }
130
+ });
131
+ this.$items.addEventListener('down', (e: any) => {
132
+ e.stopPropagation();
133
+ const visible = this.#getVisibleIndexes();
134
+ const idx = visible.findIndex(v => v.join(',') === e.detail.indexes.join(','));
135
+ if (idx < visible.length - 1) {
136
+ this.#focusItem(visible[idx + 1]);
137
+ }
138
+ });
139
+ this.$items.addEventListener('keydown', (e: any) => {
140
+ if (e.key === 'Delete') {
141
+ const toRemove = [...this.#selectedIndexes.values()] as number[][];
142
+ // Remove higher sibling indexes first to avoid index shifts
143
+ toRemove.sort((a, b) => {
144
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
145
+ const ai = i < a.length ? a[i] : -1;
146
+ const bi = i < b.length ? b[i] : -1;
147
+ if (ai !== bi) return bi - ai;
148
+ }
149
+ return 0;
150
+ });
151
+ toRemove.forEach((indexes: number[]) => this.#removeItem(indexes));
152
+ this.#selectedIndexes.clear();
153
+ this.#lastSelectedIndexes = null;
154
+ }
155
+ });
156
+ this.$items.addEventListener('itemdragstart', (e: any) => {
157
+ const { indexes, callback, ctrlKey } = e.detail;
158
+ const item = this.#getItem(indexes);
159
+ const unproxyItem = getProxyValue(item);
160
+ if (!ctrlKey && this.#selectedIndexes.size && !this.#selectedIndexes.has(unproxyItem)) {
161
+ this.#selectedIndexes.forEach((x: any) => this.#getItem(x).selected = false);
162
+ this.#selectedIndexes.clear();
163
+ }
164
+ item.selected = true;
165
+ this.#selectedIndexes.set(unproxyItem, indexes);
166
+ let count = 0;
167
+ this.#selectedIndexes.forEach((idxs: any) => {
168
+ count = this.#getDragCount(this.#getItem(idxs), count);
169
+ });
170
+ callback(count);
171
+ this.$items.classList.toggle('dragging', true);
172
+ // Sync selection to consumers so #selectedItems reflects the dragged item
173
+ this.dispatchEvent(new CustomEvent('select', {
174
+ detail: {
175
+ items: [...this.#selectedIndexes.values()].map((idxs: any) => this.#wrap(idxs)),
176
+ }
177
+ }));
178
+ });
179
+ this.$items.addEventListener('itemdragend', () => {
180
+ this.$items.classList.toggle('dragging', false);
181
+ this.#clearDropHighlights(this.$items);
182
+ });
183
+ this.$items.addEventListener('itemdropenter', this.#handleDropEnter.bind(this));
184
+ }
185
+
186
+ #getDragCount(item: any, count: number): number {
187
+ count += 1;
188
+ if (item.items?.length) {
189
+ item.items.forEach((itm: any) => {
190
+ count = this.#getDragCount(itm, count);
191
+ });
192
+ }
193
+ return count;
194
+ }
195
+
196
+ #handleDropEnter(e: any) {
197
+ const { indexes, callback } = e.detail;
198
+ const excludes = this.#calculateDragExcludes();
199
+ const joined = indexes.join(',') as string;
200
+ // Use comma-terminated prefix to prevent "0,1" from matching "0,10"
201
+ const isInvalid = excludes.some(ex => joined === ex || joined.startsWith(ex + ','));
202
+ callback(!isInvalid);
203
+ }
204
+
205
+ #removeItem(indexes: number[]) {
206
+ const deleteIndex = indexes[indexes.length - 1];
207
+ const parentIndexes = indexes.slice(0, indexes.length - 1);
208
+ const parent = parentIndexes.reduce((acc: any, i) => acc.items[i], this);
209
+ parent.items.splice(deleteIndex, 1);
210
+ }
211
+
212
+ #getItem(indexes: number[]) {
213
+ return indexes.reduce((acc: any, i) => acc.items[i], this);
214
+ }
215
+
216
+ #getItemElement(indexes: number[]): PgTreeItem | null {
217
+ let container: HTMLElement = this.$items;
218
+ let element: PgTreeItem | null = null;
219
+ for (const i of indexes) {
220
+ element = container.children[i] as PgTreeItem;
221
+ if (!element) return null;
222
+ container = (element as any).$items;
223
+ if (!container) return null;
224
+ }
225
+ return element;
226
+ }
227
+
228
+ #focusItem(indexes: number[]) {
229
+ const element = this.#getItemElement(indexes);
230
+ if (element) {
231
+ (element as any).$labelButton.focus();
232
+ }
233
+ }
234
+
235
+ #getVisibleIndexes(): number[][] {
236
+ const result: number[][] = [];
237
+ const traverse = (items: any[], prefix: number[]) => {
238
+ items.forEach((item: any, i: number) => {
239
+ const indexes = [...prefix, i];
240
+ result.push(indexes);
241
+ if (item.expanded && item.items?.length) {
242
+ traverse(item.items, indexes);
243
+ }
244
+ });
245
+ };
246
+ traverse(this.items, []);
247
+ return result;
248
+ }
249
+
250
+ #clearDropHighlights(container: HTMLElement) {
251
+ Array.from(container.children).forEach((child: any) => {
252
+ if (child.$dropabove) {
253
+ child.$dropabove.classList.remove('drop');
254
+ child.$dropon.classList.remove('drop');
255
+ child.$dropbelow.classList.remove('drop');
256
+ }
257
+ if (child.$items) {
258
+ this.#clearDropHighlights(child.$items);
259
+ }
260
+ });
261
+ }
262
+
263
+ #batchMove(selectedIdxsList: number[][], targetIdxs: number[], position: string) {
264
+ if (selectedIdxsList.length === 0) return;
265
+
266
+ // Ascending order preserves the original relative order of dragged items on insertion
267
+ const ascending = [...selectedIdxsList].sort((a, b) => {
268
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
269
+ const av = i < a.length ? a[i] : -1;
270
+ const bv = i < b.length ? b[i] : -1;
271
+ if (av !== bv) return av - bv;
272
+ }
273
+ return 0;
274
+ });
275
+
276
+ // Capture data references before any structural changes
277
+ const caches = ascending.map(idxs => this.#getItem(idxs));
278
+
279
+ // Count how many selected siblings precede the target — each one shifts its index down after removal
280
+ const targetParentStr = targetIdxs.slice(0, -1).join(',');
281
+ let targetShift = 0;
282
+ for (const sidxs of ascending) {
283
+ if (sidxs.slice(0, -1).join(',') === targetParentStr &&
284
+ sidxs[sidxs.length - 1] < targetIdxs[targetIdxs.length - 1]) {
285
+ targetShift++;
286
+ }
287
+ }
288
+ const adjustedTargetIdxs = [
289
+ ...targetIdxs.slice(0, -1),
290
+ targetIdxs[targetIdxs.length - 1] - targetShift
291
+ ];
292
+
293
+ // Remove descending so higher sibling indexes don't shift lower ones mid-loop
294
+ [...ascending].reverse().forEach(idxs => this.#removeItem(idxs));
295
+
296
+ // Rebuild selection map with post-move positions so items stay selected
297
+ this.#selectedIndexes.clear();
298
+ this.#lastSelectedIndexes = null;
299
+
300
+ if (position === 'on') {
301
+ const targetData = this.#getItem(adjustedTargetIdxs);
302
+ const prevLen = Array.isArray(targetData.items) ? targetData.items.length : 0;
303
+ if (!Array.isArray(targetData.items)) targetData.items = [];
304
+ caches.forEach((cache, i) => {
305
+ targetData.items.push(cache);
306
+ const newIdxs = [...adjustedTargetIdxs, prevLen + i];
307
+ this.#selectedIndexes.set(getProxyValue(this.#getItem(newIdxs)), newIdxs);
308
+ });
309
+ targetData.expanded = true;
310
+ } else {
311
+ const adjustedParent = adjustedTargetIdxs.slice(0, -1);
312
+ const adjustedIdx = adjustedTargetIdxs[adjustedTargetIdxs.length - 1];
313
+ const insertAt = position === 'after' ? adjustedIdx + 1 : adjustedIdx;
314
+ if (adjustedParent.length === 0) {
315
+ this.items.splice(insertAt, 0, ...caches);
316
+ } else {
317
+ this.#getItem(adjustedParent).items.splice(insertAt, 0, ...caches);
318
+ }
319
+ caches.forEach((_, i) => {
320
+ const newIdxs = [...adjustedParent, insertAt + i];
321
+ this.#selectedIndexes.set(getProxyValue(this.#getItem(newIdxs)), newIdxs);
322
+ });
323
+ }
324
+
325
+ this.$items.classList.toggle('dragging', false);
326
+ }
327
+
328
+ #wrap(indexes: number[]): SelectedTreeItem {
329
+ return {
330
+ indexes,
331
+ remove: () => {
332
+ this.#removeItem(indexes);
333
+ this.#selectedIndexes.clear();
334
+ this.#lastSelectedIndexes = null;
335
+ },
336
+ getData: () => this.#getItem(indexes),
337
+ getParentData: () => {
338
+ const parent = indexes.slice(0, indexes.length - 1);
339
+ return parent.length === 0 ? this : this.#getItem(parent);
340
+ },
341
+ move: (item: SelectedTreeItem, position: string) => {
342
+ this.#batchMove([indexes], item.indexes, position);
343
+ }
344
+ };
345
+ }
346
+
347
+ unselect(indexes: number[]) {
348
+ const item = this.#getItem(indexes);
349
+ const unproxyItem = getProxyValue(item);
350
+ item.selected = false;
351
+ this.#selectedIndexes.delete(unproxyItem);
352
+ }
353
+
354
+ #calculateDragExcludes(): string[] {
355
+ const exclude: string[] = [];
356
+ this.#selectedIndexes.forEach((indexes: number[]) => {
357
+ exclude.push(indexes.join(','));
358
+ });
359
+ return exclude;
360
+ }
361
+ }
File without changes