@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.
- package/package.json +1 -1
- package/pg/autocompleteItem/__examples__/basic/basic.html +3 -0
- package/pg/autocompleteItem/autocompleteItem.css +55 -0
- package/pg/autocompleteItem/autocompleteItem.html +1 -0
- package/pg/autocompleteItem/autocompleteItem.ts +65 -0
- package/pg/button/button.css +5 -0
- package/pg/buttonMenu/buttonMenu.ts +1 -1
- package/pg/inputCombobox/__examples__/basic/basic.ts +33 -0
- package/pg/inputCombobox/inputCombobox.ts +158 -0
- package/pg/inputCombobox/inputCombobox.ts.tmp.44252.751480cb81cc +158 -0
- package/pg/inputCombobox/inputCombobox.ts.tmp.44252.dcea847c40a8 +156 -0
- package/pg/inputPill/inputPill.html +6 -0
- package/pg/inputPill/inputPill.html.tmp.26852.84ac63f4c46f +6 -0
- package/pg/inputPill/inputPill.ts +41 -0
- package/pg/inputPills/__examples__/basic/basic.html +9 -0
- package/pg/inputPills/__examples__/basic/basic.html.tmp.26852.4771e4cad5b7 +9 -0
- package/pg/inputPills/__examples__/basic/basic.ts.tmp.26852.d8fe41df18bd +0 -0
- package/pg/inputPixelEditor/README.md +104 -69
- package/pg/inputPixelEditor/README.md.tmp.42596.658de5f0a6f5 +0 -0
- package/pg/inputPixelEditor/__examples__/basic/basic.ts +6 -5
- package/pg/inputPixelEditor/inputPixelEditor.ts +96 -24
- package/pg/inputPixelEditor/inputPixelEditor.ts.tmp.38516.96529dd82670 +1703 -0
- package/pg/inputPixelEditor/inputPixelEditor.ts.tmp.42596.48ecc3ad9c72 +1724 -0
- package/pg/inputPixelEditor/utils/bitmapToMask.ts +113 -79
- package/pg/inputPixelEditor/utils/getLassoPixels.ts +51 -0
- package/pg/inputSelect/inputSelect.ts +5 -1
- package/pg/inputText/README.md +6 -6
- package/pg/inputText/__examples__/basic/basic.ts +1 -1
- package/pg/inputText/inputText.css +2 -2
- package/pg/inputText copy/README.md +38 -0
- package/pg/inputText copy/inputText.css +41 -0
- package/pg/inputText copy/inputText.html +1 -0
- package/pg/inputText copy/inputText.spec.ts +59 -0
- package/pg/inputText copy/inputText.ts +67 -0
- package/pg/inputTextarea/README.md +39 -0
- package/pg/inputTextarea/__examples__/basic/basic.html +9 -0
- package/pg/inputTextarea/__examples__/basic/basic.ts +30 -0
- package/pg/inputTextarea/inputTextarea.ts +121 -0
- package/pg/inputTextarea copy/README.md +39 -0
- package/pg/inputTextarea copy/inputTextarea.css +41 -0
- package/pg/inputTextarea copy/inputTextarea.html +1 -0
- package/pg/inputTextarea copy/inputTextarea.spec.ts +59 -0
- package/pg/inputTextarea copy/inputTextarea.ts +79 -0
- package/pg/inputTextareaAutocomplete/README.md +70 -0
- package/pg/inputTextareaAutocomplete/__examples__/basic/basic.ts +67 -0
- package/pg/inputTextareaAutocomplete/inputTextareaAutocomplete.ts +267 -0
- package/pg/node/README.md +35 -0
- package/pg/node/__examples__/basic/basic.ts +34 -0
- package/pg/node/index.ts +3 -0
- package/pg/node/node.css +53 -0
- package/pg/node/node.html +5 -0
- package/pg/node/node.ts +270 -0
- package/pg/nodeEditorLink/nodeEditorLink.css +69 -0
- package/pg/nodeEditorLink/nodeEditorLink.css.tmp.1408.09e961fd0dcc +69 -0
- package/pg/nodeEditorLink/nodeEditorLink.html +9 -0
- package/pg/nodeEditorNumber/nodeEditorNumber.css +59 -0
- package/pg/nodeEditorNumber/nodeEditorNumber.ts +57 -0
- package/pg/nodeEditorRange/nodeEditorRange.css +23 -0
- package/pg/nodeEditorRange/nodeEditorRange.ts +69 -0
- package/pg/nodeEditorText/__examples__/basic/basic.html +9 -0
- package/pg/nodeEditorText/__examples__/basic/basic.ts +30 -0
- package/pg/nodeEditorText/nodeEditorText.css +17 -0
- package/pg/nodeEditorText/nodeEditorText.html +4 -0
- package/pg/nodeEditorText/nodeEditorText.ts +57 -0
- package/pg/nodeEditorText copy/README.md +35 -0
- package/pg/nodeEditorText copy/nodeEditorText.css +49 -0
- package/pg/nodeEditorText copy/nodeEditorText.html +4 -0
- package/pg/nodeEditorText copy/nodeEditorText.ts +50 -0
- package/pg/nodeEditorTextArray/nodeEditorTextArray.css +68 -0
- package/pg/nodeEditorTextArray/nodeEditorTextArray.html +9 -0
- package/pg/nodeEditorTextArray/nodeEditorTextArray.ts +162 -0
- package/pg/nodeEntry/nodeEntry.css +61 -0
- package/pg/nodeEntry/nodeEntry.ts +164 -0
- package/pg/nodeEntry/nodeEntry.ts.tmp.28724.dda67762bca5 +164 -0
- package/pg/nodeInputText/nodeInputText.css +75 -0
- package/pg/nodeInputText/nodeInputText.html +4 -0
- package/pg/nodeInputText/nodeInputText.ts +105 -0
- package/pg/nodeOutput/nodeOutput.css +32 -0
- package/pg/nodeResize/README.md +3 -0
- package/pg/nodeResize/THINKING.md +81 -0
- package/pg/nodeResize/THINKING.md.tmp.24812.99a70ba81062 +81 -0
- package/pg/nodeResize/basic/basic.ts +17 -0
- package/pg/nodeResize/dragUtil.ts +105 -0
- package/pg/nodeResize/dragUtil.ts.tmp.34372.787464c73309 +75 -0
- package/pg/nodeResize/nodeResize.css +187 -0
- package/pg/nodeResize/nodeResize.html +9 -0
- package/pg/nodeResize/nodeResize.ts +156 -0
- package/pg/nodes/README.md +177 -0
- package/pg/nodes/__examples__/basic/basic.html +33 -0
- package/pg/nodes/__examples__/basic/basic.ts +716 -0
- package/pg/nodes/__examples__/basic/exampleNodes.ts +129 -0
- package/pg/nodes/__examples__/basic/exampleScripts.ts +129 -0
- package/pg/nodes/__examples__/basic/keystore.ts +38 -0
- package/pg/nodes/executeInSandbox.ts +32 -0
- package/pg/nodes/index.ts +3 -0
- package/pg/nodes/nodes.css +69 -0
- package/pg/nodes/nodes.html +6 -0
- package/pg/nodes/nodes.ts +1240 -0
- package/pg/nodes copy/nodes.css +41 -0
- package/pg/nodes copy/nodes.html +1 -0
- package/pg/overlayContextMenu/__examples__/filter/filter.ts +87 -0
- package/pg/overlayContextMenu/overlayContextMenu.css +14 -0
- package/pg/overlayContextMenu/overlayContextMenu.html +4 -3
- package/pg/overlayContextMenu/overlayContextMenu.ts +6 -2
- package/pg/overlayContextMenu/overlayContextMenu.ts.tmp.37088.530ff1c0a321 +0 -0
- package/pg/overlayMenu/overlayMenu.css +3 -1
- package/pg/overlayMenu/overlayMenu.ts +11 -2
- package/pg/overlayMenu/overlayMenu.ts.tmp.44252.9bb22fd59ede +80 -0
- package/pg/overlaySelectMenu/overlaySelectMenu.css +2 -0
- package/pg/overlaySubMenu/overlaySubMenu.css +8 -6
- package/pg/poco/README.md +22 -0
- package/pg/poco/__examples__/basic/basic.html +11 -0
- package/pg/poco/__examples__/basic/basic.ts +95 -0
- package/pg/poco/__examples__/basic/examples.ts +258 -0
- package/pg/poco/__examples__/basic/myFont.ts +1300 -0
- package/pg/poco/index.ts +3 -0
- package/pg/poco/mockPoco.ts +640 -0
- package/pg/poco/poco.css +7 -0
- package/pg/poco/poco.html +1 -0
- package/pg/poco/poco.ts +75 -0
- package/pg/popover/README.md +47 -0
- package/pg/popover/__examples__/basic/basic.html +3 -0
- package/pg/popover/popover.html +3 -0
- package/pg/popover/popover.ts +44 -0
- package/pg/popoverAutocomplete/__examples__/basic/basic.html +4 -0
- package/pg/popoverAutocomplete/__examples__/basic/basic.ts +46 -0
- package/pg/popoverAutocomplete/popoverAutocomplete.css +25 -0
- package/pg/popoverAutocomplete/popoverAutocomplete.html +1 -0
- package/pg/popoverAutocomplete/popoverAutocomplete.ts +134 -0
- package/pg/popoverAutocompleteItem/popoverAutocompleteItem.css +69 -0
- package/pg/popoverAutocompleteItem/popoverAutocompleteItem.ts +90 -0
- package/pg/table/__examples__/basic/basic.ts +1 -0
- package/pg/tableCellFileLocal/tableCellFileLocal.html +3 -0
- package/pg/tableCellFileLocal/tableCellFileLocal.html.tmp.20484.68ee2e30710e +3 -0
- package/pg/tableCellFileLocal/tableCellFileLocal.ts +44 -0
- package/pg/tableCellSelect/tableCellSelect.html +3 -0
- package/pg/tableCellSelect/tableCellSelect.html.tmp.41808.99c1a6f14340 +3 -0
- package/pg/tableCellSelect/tableCellSelect.ts +51 -0
- package/pg/tree/__examples__/basic/basic.ts +12 -5
- package/pg/tree/tree.html +1 -1
- package/pg/tree/tree.ts +385 -227
- package/pg/tree/tree.ts.tmp.38376.07d376a04f41 +343 -0
- package/pg/tree/tree.ts.tmp.38376.2a04599ade9f +361 -0
- package/pg/tree/tree.ts.tmp.38376.6cb2c663d476 +0 -0
- package/pg/tree/tree.ts.tmp.38376.ac5af1aff2b0 +393 -0
- package/pg/tree/tree.ts.tmp.38376.ac9acfc4f95f +324 -0
- package/pg/treeItem/treeItem.ts +536 -558
- package/pg/treeItem/treeItem.ts.tmp.38376.145d3121d648 +530 -0
- 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
|
+
}
|
package/pg/node/index.ts
ADDED
package/pg/node/node.css
ADDED
|
@@ -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
|
+
}
|
package/pg/node/node.ts
ADDED
|
@@ -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
|
+
}
|