@pictogrammers/components 0.5.15 → 0.5.18

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 +96 -24
  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
@@ -0,0 +1,267 @@
1
+ import { Component, Prop, Part } from '@pictogrammers/element';
2
+
3
+ import PgInputTextarea from '../inputTextarea/inputTextarea';
4
+ import PgOverlayMenu from '../overlayMenu/overlayMenu';
5
+
6
+ import template from './inputTextareaAutocomplete.html';
7
+ import style from './inputTextareaAutocomplete.css';
8
+
9
+ function getCursorRowCol(
10
+ textArea: HTMLTextAreaElement
11
+ ): { row: number; col: number } {
12
+ const cursorIndex = textArea.selectionStart;
13
+ const textUpToCursor = textArea.value.substring(0, cursorIndex);
14
+ const lines = textUpToCursor.split('\n');
15
+ const row = lines.length - 1;
16
+ const col = lines[lines.length - 1].length;
17
+ return { row, col };
18
+ }
19
+
20
+ function getCaretCoordinates(
21
+ element: HTMLTextAreaElement,
22
+ position: number
23
+ ): { top: number; left: number; height: number } {
24
+ const mirror = document.createElement('div');
25
+ const computed = window.getComputedStyle(element);
26
+
27
+ [
28
+ 'direction', 'boxSizing', 'width', 'overflowX', 'overflowY',
29
+ 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth',
30
+ 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft',
31
+ 'fontStyle', 'fontVariant', 'fontWeight', 'fontSize', 'lineHeight', 'fontFamily',
32
+ 'textAlign', 'textTransform', 'textIndent', 'letterSpacing', 'wordSpacing',
33
+ ].forEach(prop => {
34
+ (mirror.style as any)[prop] = (computed as any)[prop];
35
+ });
36
+
37
+ mirror.style.position = 'absolute';
38
+ mirror.style.visibility = 'hidden';
39
+ mirror.style.top = '0';
40
+ mirror.style.left = '0';
41
+ mirror.style.whiteSpace = 'pre-wrap';
42
+ mirror.style.overflowWrap = 'break-word';
43
+
44
+ mirror.textContent = element.value.substring(0, position);
45
+
46
+ const span = document.createElement('span');
47
+ span.textContent = '\u200b';
48
+ mirror.appendChild(span);
49
+
50
+ document.body.appendChild(mirror);
51
+
52
+ const rect = element.getBoundingClientRect();
53
+ const lineHeight = parseInt(computed.lineHeight) || parseInt(computed.fontSize) * 1.2;
54
+ const coords = {
55
+ top: rect.top + span.offsetTop - element.scrollTop + parseInt(computed.borderTopWidth),
56
+ left: rect.left + span.offsetLeft - element.scrollLeft + parseInt(computed.borderLeftWidth),
57
+ height: lineHeight,
58
+ };
59
+
60
+ document.body.removeChild(mirror);
61
+ return coords;
62
+ }
63
+
64
+ @Component({
65
+ selector: 'pg-input-textarea-autocomplete',
66
+ style,
67
+ template,
68
+ })
69
+ export default class PgInputTextareaAutocomplete extends PgInputTextarea {
70
+
71
+ @Prop() tokens: RegExp[] = [];
72
+
73
+ connectedCallback() {
74
+ super.connectedCallback();
75
+
76
+ this.$input.addEventListener('input', this.handleCursorMove.bind(this));
77
+ this.$input.addEventListener('click', this.handleCursorMove.bind(this));
78
+ this.$input.addEventListener('keyup', this.handleCursorMove.bind(this));
79
+ }
80
+
81
+ focus() {
82
+ this.$input.focus();
83
+ }
84
+
85
+ skipValue = false;
86
+
87
+ render(changes) {
88
+ if (changes.value && !this.skipValue) {
89
+ this.$input.value = this.value;
90
+ }
91
+ if (changes.placeholder) {
92
+ this.$input.placeholder = this.placeholder;
93
+ }
94
+ if (changes.readOnly) {
95
+ this.$input.readOnly = this.readOnly;
96
+ }
97
+ this.skipValue = false;
98
+ }
99
+
100
+ handleInput(e) {
101
+ e.stopPropagation();
102
+ this.skipValue = true;
103
+ this.value = e.target.value;
104
+ this.dispatchEvent(
105
+ new CustomEvent('input', {
106
+ detail: {
107
+ value: e.target.value,
108
+ name: e.name
109
+ }
110
+ })
111
+ );
112
+ }
113
+
114
+ handleChange(e) {
115
+ this.dispatchEvent(
116
+ new CustomEvent('change', {
117
+ detail: {
118
+ value: e.target.value,
119
+ name: e.name
120
+ }
121
+ })
122
+ );
123
+ }
124
+
125
+ #menuOpen = false;
126
+ #activeOverlay: PgOverlayMenu | null = null;
127
+ #ghostElement: HTMLDivElement | null = null;
128
+ #pendingMatchInfo: { row: number; matchStart: number; matchEnd: number; triggerChar: string } | null = null;
129
+
130
+ #closeMenu() {
131
+ if (this.#menuOpen && this.#activeOverlay) {
132
+ this.#activeOverlay.close(undefined);
133
+ this.#activeOverlay = null;
134
+ this.#menuOpen = false;
135
+ }
136
+ if (this.#ghostElement) {
137
+ this.#ghostElement.remove();
138
+ this.#ghostElement = null;
139
+ }
140
+ }
141
+
142
+ async #openMenu(
143
+ textArea: HTMLTextAreaElement,
144
+ matchStart: number,
145
+ row: number,
146
+ options: { label: string; value: string }[]
147
+ ) {
148
+ if (this.#ghostElement) {
149
+ this.#ghostElement.remove();
150
+ }
151
+
152
+ const fullText = textArea.value;
153
+ let lineStartPos = 0;
154
+ const textLines = fullText.split('\n');
155
+ for (let i = 0; i < row; i++) {
156
+ lineStartPos += textLines[i].length + 1;
157
+ }
158
+ const matchAbsStart = lineStartPos + matchStart;
159
+
160
+ const coords = getCaretCoordinates(textArea, matchAbsStart);
161
+
162
+ const ghost = document.createElement('div');
163
+ ghost.style.cssText = `position:fixed;top:${coords.top}px;left:${coords.left}px;width:1px;height:${coords.height}px;pointer-events:none;`;
164
+ document.body.appendChild(ghost);
165
+ this.#ghostElement = ghost;
166
+ this.#menuOpen = true;
167
+
168
+ const prevMenus = new Set(Array.from(document.querySelectorAll('pg-overlay-menu')));
169
+ const promise = PgOverlayMenu.open({
170
+ source: ghost,
171
+ items: options,
172
+ });
173
+ const newEl = Array.from(document.querySelectorAll('pg-overlay-menu')).find(el => !prevMenus.has(el as PgOverlayMenu));
174
+ this.#activeOverlay = (newEl ?? null) as PgOverlayMenu | null;
175
+
176
+ const result = await promise;
177
+
178
+ ghost.remove();
179
+ if (this.#ghostElement === ghost) this.#ghostElement = null;
180
+ this.#menuOpen = false;
181
+ this.#activeOverlay = null;
182
+
183
+ if (result !== undefined) {
184
+ const { item } = result;
185
+ const matchInfo = this.#pendingMatchInfo;
186
+ if (!matchInfo) return;
187
+
188
+ const text = textArea.value;
189
+ const lines = text.split('\n');
190
+ let linePos = 0;
191
+ for (let i = 0; i < matchInfo.row; i++) {
192
+ linePos += lines[i].length + 1;
193
+ }
194
+ const absStart = linePos + matchInfo.matchStart;
195
+ const absEnd = linePos + matchInfo.matchEnd;
196
+
197
+ const newText = text.substring(0, absStart) + matchInfo.triggerChar + item.value + text.substring(absEnd);
198
+ textArea.value = newText;
199
+ const newCursorPos = absStart + matchInfo.triggerChar.length + String(item.value).length;
200
+ textArea.setSelectionRange(newCursorPos, newCursorPos);
201
+
202
+ this.skipValue = true;
203
+ this.value = newText;
204
+ this.dispatchEvent(new CustomEvent('change', {
205
+ detail: { value: newText, name: this.name }
206
+ }));
207
+ }
208
+
209
+ textArea.focus();
210
+ }
211
+
212
+ handleCursorMove(event: Event) {
213
+ const textArea = event.target as HTMLTextAreaElement;
214
+ const { row, col: column } = getCursorRowCol(textArea);
215
+
216
+ let matchIndex = 0;
217
+ let matchStart = -1;
218
+ let matchEnd = -1;
219
+ let matchText = '';
220
+
221
+ if (this.tokens.length > 0) {
222
+ const lines = textArea.value.split('\n');
223
+ const currentLine = lines[row] || '';
224
+
225
+ for (let i = 0; i < this.tokens.length; i++) {
226
+ const regex = new RegExp(this.tokens[i].source, 'g');
227
+ let match: RegExpExecArray | null;
228
+ while ((match = regex.exec(currentLine)) !== null) {
229
+ const start = match.index;
230
+ const end = match.index + match[0].length;
231
+ if (start <= column && column <= end) {
232
+ matchIndex = i + 1;
233
+ matchStart = start;
234
+ matchEnd = end;
235
+ matchText = match[0];
236
+ break;
237
+ }
238
+ }
239
+ if (matchIndex > 0) break;
240
+ }
241
+ }
242
+
243
+ let pendingOptions: { label: string; value: string }[] | null = null;
244
+ const setOptions = (options: { label: string; value: string }[]) => {
245
+ pendingOptions = options;
246
+ };
247
+
248
+ this.dispatchEvent(new CustomEvent('caret', {
249
+ detail: { column, row, matchIndex, matchText, setOptions }
250
+ }));
251
+
252
+ if (!pendingOptions || (pendingOptions as any[]).length === 0) {
253
+ this.#closeMenu();
254
+ return;
255
+ }
256
+
257
+ const options = pendingOptions as { label: string; value: string }[];
258
+ this.#pendingMatchInfo = { row, matchStart, matchEnd, triggerChar: matchText[0] ?? '' };
259
+
260
+ if (this.#menuOpen && this.#activeOverlay) {
261
+ const menuItems = (this.#activeOverlay as any).$menu.items;
262
+ menuItems.splice(0, menuItems.length, ...options);
263
+ } else if (!this.#menuOpen) {
264
+ this.#openMenu(textArea, matchStart, row, options);
265
+ }
266
+ }
267
+ }
@@ -0,0 +1,35 @@
1
+ # `<pg-nodes>`
2
+
3
+ The `pg-nodes` component allows creating and linking `PgNode` extended data types.
4
+
5
+ > If a `type` is omitted a `fields` property is required. See below for details.
6
+
7
+ ```typescript
8
+ import '@pictogrammers/components/pgNodes';
9
+ import { PgNodes } from '@pictogrammers/components/pgNodes';
10
+ ```
11
+
12
+ ```html
13
+ <pg-nodes></pg-nodes>
14
+ ```
15
+
16
+ ## Attributes
17
+
18
+ | Attributes | Tested | Description |
19
+ | ----------- | -------- | ----------- |
20
+ | `fields` | | Node list |
21
+
22
+ > Note: All nodes are sized off a `1rem` grid.
23
+
24
+ ## Events
25
+
26
+ | Events | Tested | Description |
27
+ | ---------- | -------- | ----------- |
28
+ | change | | `{ detail: { index, item[, field] }` |
29
+ | input | | `{ detail: { index, item[, field] }` |
30
+
31
+ ## CSS Variables
32
+
33
+ | CSS Variables | Default | Description |
34
+ | ------------------- | --------- | ----------- |
35
+ | n/a | | |
@@ -0,0 +1,34 @@
1
+ import { Component, Part } from '@pictogrammers/element';
2
+ import PgNode from '../../node';
3
+
4
+ import template from './basic.html';
5
+
6
+ @Component({
7
+ selector: 'x-pg-node-basic',
8
+ template,
9
+ })
10
+ export default class XPgNodeBasic extends HTMLElement {
11
+
12
+ @Part() $node: PgNode;
13
+
14
+ connectedCallback() {
15
+ this.$node.fields = [{
16
+ label: 'Name',
17
+ value: 'foo',
18
+ name: 'name',
19
+ type: 'text',
20
+ }];
21
+ this.$node.addEventListener('change', this.#handleChange.bind(this));
22
+ this.$node.addEventListener('input', this.#handleInput.bind(this));
23
+ }
24
+
25
+ #handleChange(e: CustomEvent) {
26
+ const { item } = e.detail;
27
+ //this.$value1.textContent = value;
28
+ }
29
+
30
+ #handleInput(e: CustomEvent) {
31
+ const { item } = e.detail;
32
+ //this.$value2.textContent = value;
33
+ }
34
+ }
@@ -0,0 +1,3 @@
1
+ import PgNode from './node';
2
+
3
+ export default PgNode;
@@ -0,0 +1,53 @@
1
+ :host {
2
+ display: flex;
3
+ flex-direction: column;
4
+ position: absolute;
5
+ width: var(--pg-node-width, 12rem);
6
+ min-height: var(--pg-node-height, 3rem);
7
+ }
8
+
9
+ [part=header] {
10
+ font-size: 0.75rem;
11
+ line-height: 1.375rem;
12
+ padding-inline: 0.5rem;
13
+ padding-block: 0 0.125rem;
14
+ background-color: #181818;
15
+ color: rgba(255, 255, 255, 0.9);
16
+ }
17
+
18
+ [part=node] {
19
+ display: flex;
20
+ flex-direction: column;
21
+ flex: auto;
22
+ box-sizing: border-box;
23
+ border: 2px solid rgba(255, 255, 255, 0.5);
24
+ border-radius: 0.25rem;
25
+ background: #1D1D1D;
26
+ color: #FFF;
27
+ padding-block-end: 0.25rem;
28
+ }
29
+
30
+ [part=node]:hover {
31
+ border-color: rgba(255, 255, 255, 0.75);
32
+ }
33
+
34
+ [part=node].selected {
35
+ outline: 2px solid #248BE5 !important;
36
+ outline-offset: 1px !important;
37
+ }
38
+
39
+ [part=nodes] {
40
+ margin-bottom: -0.125rem;
41
+ margin-top: 0.125rem;
42
+ }
43
+
44
+ [part=node].debug {
45
+ border-color: #4ade80;
46
+ box-shadow: 0 0 0.5rem rgba(74, 222, 128, 0.75);
47
+ outline: 1px solid #1a1d2e;
48
+ outline-offset: 0;
49
+ }
50
+
51
+ [part=node].debug [part=header] {
52
+ color: #4ade80;
53
+ }
@@ -0,0 +1,5 @@
1
+ <div part="node">
2
+ <div part="header">Node</div>
3
+ <div part="items"></div>
4
+ <div part="nodes"></div>
5
+ </div>
@@ -0,0 +1,270 @@
1
+ import { Component, Prop, Part, forEach } from '@pictogrammers/element';
2
+
3
+ import template from './node.html';
4
+ import style from './node.css';
5
+ import PgNodeResize from '../nodeResize/nodeResize';
6
+ import PgNodeOutput from '../nodeOutput/nodeOutput';
7
+
8
+ const ANCHOR_NAME = '--node-resize-anchor';
9
+
10
+ @Component({
11
+ selector: 'pg-node',
12
+ style,
13
+ template,
14
+ })
15
+ export default class PgNode extends HTMLElement {
16
+ @Prop() x: number = 0;
17
+ @Prop() y: number = 0;
18
+ @Prop() width: number = 12;
19
+ @Prop() height: number = 3;
20
+ // The node type's declared width; nodes cannot be resized below it.
21
+ @Prop() minWidth: number = 6;
22
+ @Prop() itemId: number = 0;
23
+ @Prop() label: string = '';
24
+ @Prop() fields: any = [];
25
+ @Prop() outputs: any = [];
26
+ @Prop() debug: boolean = false;
27
+ @Prop() editors: any = [];
28
+
29
+ @Part() $node: HTMLDivElement;
30
+ @Part() $items: HTMLDivElement;
31
+ @Part() $outputs: HTMLDivElement;
32
+ @Part() $header: HTMLDivElement;
33
+
34
+ connectedCallback() {
35
+ // The height synced from the item (a persisted resize) is recomputed
36
+ // below as the intrinsic minimum; keep the larger of the two.
37
+ const requestedHeight = this.height;
38
+ this.height = 2;
39
+ forEach({
40
+ container: this.$items,
41
+ items: this.fields,
42
+ type: (item) => {
43
+ return this.editors.find(x => x.type === item.type);
44
+ },
45
+ create: ($item: any, item) => {
46
+ this.height += $item.height;
47
+ this.#fieldHeights.set(item.itemKey, $item.height);
48
+ $item.addEventListener('input', (e: any) => {
49
+ this.#dispatchArg('input', item.itemKey, e.detail.value);
50
+ });
51
+ $item.addEventListener('change', (e: any) => {
52
+ // Editors can grow or shrink on change (e.g. TextArray rows);
53
+ // reflow first so change listeners see the final geometry.
54
+ this.#syncFieldHeight(item.itemKey, $item);
55
+ this.#dispatchArg('change', item.itemKey, e.detail.value);
56
+ });
57
+ },
58
+ });
59
+
60
+ forEach({
61
+ container: this.$outputs,
62
+ items: this.outputs,
63
+ type: (item) => {
64
+ return item.key === 'then' ? null : PgNodeOutput;
65
+ },
66
+ create: ($item: PgNodeOutput, _item) => {
67
+ this.height += $item.height;
68
+ },
69
+ connect: ($item: any, item) => {
70
+ // Measured mid-setup, before render() applies the host styles, so
71
+ // the adjust constant differs from the post-layout one below.
72
+ const top = this.$node.getBoundingClientRect().top;
73
+ this.#registerOutputPin(item.key, item.label, $item.getBoundingClientRect().top - top + 31);
74
+ },
75
+ });
76
+ this.#intrinsicHeight = this.height;
77
+ if (requestedHeight > this.height) {
78
+ this.height = requestedHeight;
79
+ }
80
+ this.$node.addEventListener('pointerover', this.#handlePointerOver.bind(this));
81
+ }
82
+
83
+ // Editor heights are cached by field key so a change event only triggers a
84
+ // reflow (node height + output pin offsets) when a height actually changed.
85
+ #fieldHeights = new Map<string, number>();
86
+ #intrinsicHeight = 2;
87
+ #syncFieldHeight(key: string, $item: any) {
88
+ const previous = this.#fieldHeights.get(key) ?? 0;
89
+ if ($item.height === previous) return;
90
+ this.#fieldHeights.set(key, $item.height);
91
+ // Auto-sized nodes track their content; manually resized nodes only grow
92
+ // when the content no longer fits.
93
+ const wasAuto = this.height === this.#intrinsicHeight;
94
+ this.#intrinsicHeight += $item.height - previous;
95
+ this.height = wasAuto ? this.#intrinsicHeight : Math.max(this.height, this.#intrinsicHeight);
96
+ this.#registerOutputPins();
97
+ }
98
+
99
+ #dispatchArg(type: 'input' | 'change', key: string, value: any) {
100
+ this.dispatchEvent(new CustomEvent(type, {
101
+ detail: {
102
+ type: 'arg',
103
+ id: this.itemId,
104
+ key,
105
+ value,
106
+ }
107
+ }));
108
+ }
109
+
110
+ #registerOutputPin(key: string, label: string, offset: number) {
111
+ this.dispatchEvent(new CustomEvent('registernodeoutput', {
112
+ detail: {
113
+ node: this.itemId,
114
+ key,
115
+ label,
116
+ offset,
117
+ }
118
+ }));
119
+ }
120
+
121
+ // Re-measures every rendered output row. 'then' outputs render no row
122
+ // (their pin stays on the header line), so the DOM index advances only
123
+ // for outputs that produced an element.
124
+ #registerOutputPins() {
125
+ const top = this.$node.getBoundingClientRect().top;
126
+ let domIndex = 0;
127
+ this.outputs.forEach((output: any) => {
128
+ if (output.key === 'then') return;
129
+ const $output = this.$outputs.children[domIndex++];
130
+ if (!$output) return;
131
+ this.#registerOutputPin(output.key, output.label, $output.getBoundingClientRect().top - top + 9);
132
+ });
133
+ }
134
+
135
+ render(changes: any) {
136
+ if (changes.outputs) {
137
+ this.outputs.forEach(({ key, label }: any) => {
138
+ this.dispatchEvent(new CustomEvent('registernode', {
139
+ detail: {
140
+ node: this.itemId,
141
+ key,
142
+ label,
143
+ }
144
+ }));
145
+ });
146
+ }
147
+ if (changes.label) {
148
+ this.$header.textContent = this.label;
149
+ }
150
+ if (changes.x) {
151
+ this.style.setProperty('left', `${this.x}rem`);
152
+ }
153
+ if (changes.y) {
154
+ this.style.setProperty('top', `${this.y}rem`);
155
+ }
156
+ if (changes.width) {
157
+ this.style.setProperty('--pg-node-width', `${this.width}rem`);
158
+ }
159
+ if (changes.height) {
160
+ this.style.setProperty('--pg-node-height', `${this.height}rem`);
161
+ }
162
+ if (changes.debug) {
163
+ this.$node.classList.toggle('debug', this.debug);
164
+ }
165
+ if (this.#resizeElement) {
166
+ if (changes.x) this.#resizeElement.x = this.x;
167
+ if (changes.y) this.#resizeElement.y = this.y;
168
+ if (changes.width) this.#resizeElement.width = this.width;
169
+ if (changes.height) this.#resizeElement.height = this.height;
170
+ }
171
+ }
172
+
173
+ #handleSelect(e: any) {
174
+ this.dispatchEvent(new CustomEvent('select', {
175
+ detail: {
176
+ nodeId: this.itemId,
177
+ addToSelection: !!(e?.detail?.shiftKey || e?.detail?.ctrlKey),
178
+ }
179
+ }));
180
+ }
181
+
182
+ #resizeElement: PgNodeResize | null = null;
183
+ #handlePointerOver(_e: any) {
184
+ if (this.#resizeElement) {
185
+ this.#resizeElement.style.visibility = 'visible';
186
+ return;
187
+ }
188
+ // @ts-ignore
189
+ this.$node.style.anchorName = ANCHOR_NAME;
190
+ const ele = document.createElement('pg-node-resize') as PgNodeResize;
191
+ ele.addEventListener('pointerout', this.#handlePointerOut.bind(this));
192
+ ele.style.setProperty('position-anchor', ANCHOR_NAME);
193
+ ele.x = this.x;
194
+ ele.y = this.y;
195
+ ele.width = this.width;
196
+ ele.height = this.height;
197
+ ele.minHeight = this.getMinHeight();
198
+ ele.minWidth = this.getMinWidth();
199
+ ele.addEventListener('change', (e: any) => {
200
+ const { x, y, width, height } = e.detail;
201
+ this.x = x;
202
+ this.y = y;
203
+ this.width = width;
204
+ this.height = height;
205
+ ele.x = x;
206
+ ele.y = y;
207
+ ele.width = width;
208
+ ele.height = height;
209
+ this.dispatchEvent(new CustomEvent('change', { detail: { type: 'transform', x, y, width, height } }));
210
+ });
211
+ ele.addEventListener('dragstart', () => {
212
+ this.dispatchEvent(new CustomEvent('nodedragstart', { detail: { nodeId: this.itemId } }));
213
+ });
214
+ ele.addEventListener('dragmove', (e: any) => {
215
+ this.dispatchEvent(new CustomEvent('nodedragmove', { detail: e.detail }));
216
+ });
217
+ ele.addEventListener('dragend', (e: any) => {
218
+ const { dx, dy, complete } = e.detail;
219
+ this.dispatchEvent(new CustomEvent('nodedragend', { detail: { nodeId: this.itemId } }));
220
+ // Only commit when the node actually landed on a new grid position.
221
+ if (!complete || (dx === 0 && dy === 0)) return;
222
+ this.x += dx;
223
+ this.y += dy;
224
+ this.dispatchEvent(new CustomEvent('change', {
225
+ detail: { type: 'transform', x: this.x, y: this.y, width: this.width, height: this.height, final: true },
226
+ }));
227
+ });
228
+ ele.addEventListener('select', this.#handleSelect.bind(this));
229
+ this.shadowRoot?.appendChild(ele);
230
+ this.#resizeElement = ele;
231
+ this.$node.classList.toggle('resize', true);
232
+ }
233
+
234
+ #handlePointerOut(_e: any) {
235
+ this.$node.classList.toggle('resize', false);
236
+ if (this.#resizeElement) {
237
+ this.#resizeElement.style.visibility = 'hidden';
238
+ }
239
+ }
240
+
241
+ select() {
242
+ this.$node.classList.toggle('selected', true);
243
+ }
244
+
245
+ deselect() {
246
+ this.$node.classList.toggle('selected', false);
247
+ }
248
+
249
+ getMinHeight() {
250
+ return Array.from(this.$items.children).reduce((val, $item: any) => {
251
+ return val + $item.height;
252
+ }, 2);
253
+ }
254
+
255
+ getMinWidth() {
256
+ return this.minWidth;
257
+ }
258
+
259
+ focus() {
260
+ if (this.$items.children.length) {
261
+ (this.$items.children[0] as any).focus();
262
+ }
263
+ }
264
+
265
+ pulse() {
266
+ // todo: add css class to create a blue animated pulse.
267
+ // it should also focus the node into view as it may be scrolled out of view.
268
+ // it should only pulse 3 times.
269
+ }
270
+ }