@node-projects/web-component-designer-visualization-addons 0.1.152 → 0.3.1
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/dist/components/BindingsEditor.js +11 -5
- package/dist/components/BindingsEditorHelp.d.ts +1 -1
- package/dist/components/BindingsEditorHelp.js +4 -0
- package/dist/components/ClassBindingsPropertiesService.d.ts +23 -0
- package/dist/components/ClassBindingsPropertiesService.js +73 -0
- package/dist/components/ClassBindingsPropertyGridExtensionProvider.d.ts +5 -0
- package/dist/components/ClassBindingsPropertyGridExtensionProvider.js +15 -0
- package/dist/components/EventAssignment.d.ts +1 -0
- package/dist/components/EventAssignment.js +5 -2
- package/dist/components/PropertyGridClassBindings.d.ts +17 -0
- package/dist/components/PropertyGridClassBindings.js +231 -0
- package/dist/helpers/BindingsHelper.d.ts +25 -13
- package/dist/helpers/BindingsHelper.js +605 -605
- package/dist/index-min.js +124 -55
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/services/VisualizationBindingsRefactorService.js +20 -23
- package/dist/services/VisualizationBindingsService.js +3 -0
- package/package.json +1 -1
- package/tests/BindingsHelper.test.ts +554 -0
- package/tests/PropertyGridClassBindings.test.ts +167 -0
|
@@ -9,7 +9,7 @@ export class BindingsEditor extends BaseCustomWebComponentConstructorAppend {
|
|
|
9
9
|
<div id="full-width-col">
|
|
10
10
|
<div id="signals-area">
|
|
11
11
|
<div class="row">
|
|
12
|
-
<span class="section-label" title="Add signals by name and path. The variable name is used in the formula (__0, __1,... or a custom name).
Access signal
|
|
12
|
+
<span class="section-label" title="Add signals by name and path. The variable name is used in the formula (__0, __1,... or a custom name).
Access signal names in properties via ?propertyName and #propertyName, property values via ??propertyName and ##propertyName, and signal objects stored in properties via ?$propertyName and #$propertyName.
Access screen attributes via ?@attributeName and target attributes via #@attributeName.
Resolve a named signal object via $objectId.
Bind to special values via §name (if supported by your framework).
You could also use signals inside of a Signal Name via {name}">objects</span>
|
|
13
13
|
<button id="helpBtn" class="info-btn" title="Help"><svg viewBox="0 0 512 512" width="16" height="16"><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"/></svg></button>
|
|
14
14
|
</div>
|
|
15
15
|
<div class="bordered-section">
|
|
@@ -58,7 +58,7 @@ export class BindingsEditor extends BaseCustomWebComponentConstructorAppend {
|
|
|
58
58
|
<input id="namedConverterInput" value="{{?this.convertersString::change}}">
|
|
59
59
|
<button id="namedConverterBrowse" class="signal-select-btn" @click="[[this._selectNamedConverter()]]">...</button>
|
|
60
60
|
</div>
|
|
61
|
-
<div class="bordered-section">
|
|
61
|
+
<div id="converterSection" class="bordered-section">
|
|
62
62
|
<div class="signal-row signal-header">
|
|
63
63
|
<span class="converter-header-col">condition</span>
|
|
64
64
|
<span class="converter-header-col">value</span>
|
|
@@ -84,10 +84,15 @@ export class BindingsEditor extends BaseCustomWebComponentConstructorAppend {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
#converterListContainer {
|
|
87
|
-
max-height: 72px;
|
|
88
87
|
overflow-y: auto;
|
|
89
88
|
}
|
|
90
89
|
|
|
90
|
+
#converterSection {
|
|
91
|
+
display: grid;
|
|
92
|
+
grid-template-rows: min-content minmax(22px, 1fr) min-content;
|
|
93
|
+
min-height: 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
91
96
|
.input-headline {
|
|
92
97
|
height: 30px;
|
|
93
98
|
}
|
|
@@ -105,7 +110,7 @@ export class BindingsEditor extends BaseCustomWebComponentConstructorAppend {
|
|
|
105
110
|
#root {
|
|
106
111
|
padding: 2px 10px;
|
|
107
112
|
display: grid;
|
|
108
|
-
grid-template-rows: min-content min-content;
|
|
113
|
+
grid-template-rows: min-content min-content minmax(0, 1fr);
|
|
109
114
|
overflow: auto;
|
|
110
115
|
height: calc(100% - 4px)
|
|
111
116
|
}
|
|
@@ -283,6 +288,7 @@ export class BindingsEditor extends BaseCustomWebComponentConstructorAppend {
|
|
|
283
288
|
|
|
284
289
|
.add-signal-btn {
|
|
285
290
|
align-self: flex-start;
|
|
291
|
+
justify-self: start;
|
|
286
292
|
margin-top: 4px;
|
|
287
293
|
height: 22px;
|
|
288
294
|
cursor: pointer;
|
|
@@ -454,7 +460,7 @@ export class BindingsEditor extends BaseCustomWebComponentConstructorAppend {
|
|
|
454
460
|
signalInput.className = 'signal-path-input';
|
|
455
461
|
signalInput.value = sig.signal;
|
|
456
462
|
signalInput.placeholder = 'signal path';
|
|
457
|
-
signalInput.title = 'signal path (prefix: ? =
|
|
463
|
+
signalInput.title = 'signal path (prefix: ? = signal name in screen property, ?? = screen property value, ?$ = signal object in screen property, ?@ = screen attribute, # = signal name in target property, ## = target property value, #$ = signal object in target property, #@ = target attribute)';
|
|
458
464
|
signalInput.oninput = () => {
|
|
459
465
|
this._signals[i].signal = signalInput.value;
|
|
460
466
|
this._syncSignals();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const bindingsEditorHelpHtml = "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n<style>\n:root { --accent: #2c6e9b; }\n\n*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\nhtml, body {\n width: 100%;\n height: 100%;\n font-family: Segoe UI, Arial, sans-serif;\n font-size: 13px;\n color: #333;\n background: #f0f1f3;\n}\n\n.content {\n height: 100%;\n overflow-y: auto;\n padding: 10px 14px 16px;\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.intro-banner {\n background: #ffffff;\n border-left: 4px solid var(--accent);\n border-radius: 4px;\n padding: 8px 12px;\n font-size: 12px;\n line-height: 1.5;\n}\n\n.intro-banner code {\n font-family: Consolas, monospace;\n background: #eef2f6;\n padding: 1px 4px;\n border-radius: 3px;\n color: #2c4a68;\n}\n\n.content-section {\n background: #ffffff;\n border-radius: 6px;\n padding: 8px 10px 10px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.section-title {\n display: flex;\n align-items: baseline;\n gap: 8px;\n border-bottom: 2px solid var(--accent);\n font-size: 13px;\n font-weight: bold;\n padding-bottom: 4px;\n color: #24405c;\n}\n\n.section-num {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 18px;\n height: 18px;\n border-radius: 50%;\n background: var(--accent);\n color: white;\n font-size: 10px;\n font-weight: bold;\n flex-shrink: 0;\n}\n\n.section-hint {\n font-size: 11px;\n font-weight: normal;\n color: #808080;\n}\n\n.snippet {\n font-family: Consolas, \"Courier New\", monospace;\n font-size: 11px;\n background: #f6f8fa;\n color: #24292e;\n border: 1px solid #e1e4e8;\n border-radius: 4px;\n padding: 6px 10px;\n white-space: pre;\n overflow-x: auto;\n line-height: 1.6;\n}\n\n.ref-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 12px;\n}\n\n.ref-table th {\n text-align: left;\n font-weight: 600;\n color: #24405c;\n border-bottom: 1px solid #d0d7de;\n padding: 4px 6px;\n}\n\n.ref-table td {\n padding: 3px 6px;\n border-bottom: 1px solid #f0f0f0;\n vertical-align: top;\n}\n\n.ref-table tr:last-child td { border-bottom: none; }\n\n.ref-table td:first-child {\n font-family: Consolas, monospace;\n color: #005cc5;\n white-space: nowrap;\n width: 110px;\n}\n\n.note {\n font-size: 11px;\n color: #666;\n font-style: italic;\n}\n\ncode {\n font-family: Consolas, monospace;\n background: #eef2f6;\n padding: 1px 4px;\n border-radius: 3px;\n color: #2c4a68;\n font-size: 11px;\n}\n</style>\n</head>\n<body>\n<div class=\"content\">\n\n <div class=\"intro-banner\">\n A <strong>Binding</strong> connects one or more signals to a property of an element.<br>\n An optional <strong>formula</strong> transforms the signal values before they are applied.\n A <strong>converter</strong> maps the final value to a display value.\n </div>\n\n <div class=\"content-section\">\n <div class=\"section-title\"><span class=\"section-num\">1</span> Signals (Objects) <span class=\"section-hint\">what data to read</span></div>\n <div class=\"note\" style=\"margin-bottom:2px;\">Each row adds one signal. The <em>name</em> column sets the variable name used in the formula.</div>\n <pre class=\"snippet\"><span style=\"color:#6a737d;font-style:italic\">// name column empty \u2192 variable is __0, __1, __2 \u2026 (positional)</span>\n<span style=\"color:#6a737d;font-style:italic\">// name column filled \u2192 variable uses that name in the formula</span>\n\n<span style=\"color:#005cc5\">name</span>: mySpeed <span style=\"color:#005cc5\">signal</span>: .Drive.Speed \u2192 accessible as mySpeed\n<span style=\"color:#005cc5\">name</span>: (empty) <span style=\"color:#005cc5\">signal</span>: .Drive.Active \u2192 accessible as __1 (second row)</pre>\n <div class=\"note\">Multiple signals let you combine values in a formula, e.g. <code>__0 > 0 && __1</code>.</div>\n </div>\n\n <div class=\"content-section\">\n <div class=\"section-title\"><span class=\"section-num\">2</span> Signal path prefixes & special paths <span class=\"section-hint\">special access modes</span></div>\n <table class=\"ref-table\">\n <tr><th>Prefix / Pattern</th><th>What it accesses</th></tr>\n <tr><td>(none)</td><td>The signal's current value directly</td></tr>\n <tr><td>__name</td><td>Internal screen variable \u2014 local to the current screen, not connected to any external data source. Initial value is <code>null</code>. Useful for screen-local state like tab selection or panel visibility.</td></tr>\n <tr><td>?name</td><td>A signal in a screen property</td></tr>\n <tr><td>??name</td><td>The current value of a screen property</td></tr>\n <tr><td>#name</td><td>A signal in a property of the bound target element</td></tr>\n <tr><td>##name</td><td>The current value of a property of the bound target element</td></tr>\n <tr><td>$objectId</td><td>The entire signal object</td></tr>\n <tr><td>\u00A7name</td><td>A special framework-provided value (if supported)</td></tr>\n <tr><td>{name}</td><td>Dynamic signal path: embed another signal's value inside the path, e.g. <code>.Plant.{.ActiveLine}.Speed</code></td></tr>\n </table>\n </div>\n\n <div class=\"content-section\">\n <div class=\"section-title\"><span class=\"section-num\">3</span> Formula <span class=\"section-hint\">transform signal values with JavaScript</span></div>\n <div class=\"note\" style=\"margin-bottom:2px;\">Leave the formula empty to pass the first signal's value through unchanged.</div>\n <pre class=\"snippet\"><span style=\"color:#6a737d;font-style:italic\">// Access signal values by position or by name:</span>\n__0 * 100 <span style=\"color:#6a737d;font-style:italic\">// first signal \u00D7 100</span>\n__0 > 0 && __1 <span style=\"color:#6a737d;font-style:italic\">// combine two signals</span>\nspeed > limit <span style=\"color:#6a737d;font-style:italic\">// named variables</span>\n\n<span style=\"color:#6a737d;font-style:italic\">// Special variables available in the formula:</span>\n__ctx <span style=\"color:#6a737d;font-style:italic\">// the full binding context object</span>\n__res <span style=\"color:#6a737d;font-style:italic\">// the result of the previous evaluation</span></pre>\n <div class=\"note\">The formula must be a single JavaScript expression that returns a value (no <code>return</code> statement needed).</div>\n </div>\n\n <div class=\"content-section\">\n <div class=\"section-title\"><span class=\"section-num\">4</span> Write Back & Two-Way <span class=\"section-hint\">send values back to signals</span></div>\n <pre class=\"snippet\"><span style=\"color:#6a737d;font-style:italic\">// Write back signal</span>\n<span style=\"color:#6a737d;font-style:italic\">// \u2192 writes the formula result back to a signal whenever</span>\n<span style=\"color:#6a737d;font-style:italic\">// the element property changes (useful for derived values)</span>\n\n<span style=\"color:#6a737d;font-style:italic\">// Two-Way binding</span>\n<span style=\"color:#6a737d;font-style:italic\">// \u2192 also sets the signal when the element fires a DOM event</span>\n<span style=\"color:#6a737d;font-style:italic\">// (e.g. \"change\" for inputs). Enable \"events\" to configure</span>\n<span style=\"color:#6a737d;font-style:italic\">// which events trigger the write-back.</span>\n<span style=\"color:#6a737d;font-style:italic\">// \u2192 \"formula write back\" applies a reverse transformation</span>\n<span style=\"color:#6a737d;font-style:italic\">// before writing (useful when the formula changes the scale)</span></pre>\n <div class=\"note\">Two-Way is only available for <em>property</em> and <em>attribute</em> targets. Separate multiple event names with a semicolon.</div>\n </div>\n\n <div class=\"content-section\">\n <div class=\"section-title\"><span class=\"section-num\">5</span> Converter <span class=\"section-hint\">map values to display values</span></div>\n <div class=\"note\" style=\"margin-bottom:4px;\">Use the table to map specific conditions to output values. The <em>condition</em> column supports several matching modes:</div>\n <table class=\"ref-table\">\n <tr><th>Condition key</th><th>Matches when \u2026</th></tr>\n <tr><td>true / false</td><td>Signal value equals the boolean</td></tr>\n <tr><td>42</td><td>Signal value equals the number exactly</td></tr>\n <tr><td>running</td><td>Signal value equals the string \"running\"</td></tr>\n <tr><td>>10</td><td>Signal value is greater than 10</td></tr>\n <tr><td>>=5</td><td>Signal value is \u2265 5</td></tr>\n <tr><td><3</td><td>Signal value is < 3</td></tr>\n <tr><td>10-20</td><td>Signal value is in the range 10 \u2026 20 (inclusive)</td></tr>\n <tr><td>default</td><td>Fallback when no other key matches</td></tr>\n </table>\n <div class=\"note\" style=\"margin-top:4px;\">The value column supports template literals \u2014 use <code>${value}</code> to embed the original signal value in the output string.</div>\n <div class=\"note\" style=\"margin-top:4px;\"><strong>Named converter:</strong> enter a converter name in the <em>converter</em> field above the table to use a pre-registered converter instead of the inline table.</div>\n </div>\n\n</div>\n</body>\n</html>";
|
|
1
|
+
export declare const bindingsEditorHelpHtml = "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n<style>\n:root { --accent: #2c6e9b; }\n\n*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\nhtml, body {\n width: 100%;\n height: 100%;\n font-family: Segoe UI, Arial, sans-serif;\n font-size: 13px;\n color: #333;\n background: #f0f1f3;\n}\n\n.content {\n height: 100%;\n overflow-y: auto;\n padding: 10px 14px 16px;\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.intro-banner {\n background: #ffffff;\n border-left: 4px solid var(--accent);\n border-radius: 4px;\n padding: 8px 12px;\n font-size: 12px;\n line-height: 1.5;\n}\n\n.intro-banner code {\n font-family: Consolas, monospace;\n background: #eef2f6;\n padding: 1px 4px;\n border-radius: 3px;\n color: #2c4a68;\n}\n\n.content-section {\n background: #ffffff;\n border-radius: 6px;\n padding: 8px 10px 10px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.section-title {\n display: flex;\n align-items: baseline;\n gap: 8px;\n border-bottom: 2px solid var(--accent);\n font-size: 13px;\n font-weight: bold;\n padding-bottom: 4px;\n color: #24405c;\n}\n\n.section-num {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 18px;\n height: 18px;\n border-radius: 50%;\n background: var(--accent);\n color: white;\n font-size: 10px;\n font-weight: bold;\n flex-shrink: 0;\n}\n\n.section-hint {\n font-size: 11px;\n font-weight: normal;\n color: #808080;\n}\n\n.snippet {\n font-family: Consolas, \"Courier New\", monospace;\n font-size: 11px;\n background: #f6f8fa;\n color: #24292e;\n border: 1px solid #e1e4e8;\n border-radius: 4px;\n padding: 6px 10px;\n white-space: pre;\n overflow-x: auto;\n line-height: 1.6;\n}\n\n.ref-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 12px;\n}\n\n.ref-table th {\n text-align: left;\n font-weight: 600;\n color: #24405c;\n border-bottom: 1px solid #d0d7de;\n padding: 4px 6px;\n}\n\n.ref-table td {\n padding: 3px 6px;\n border-bottom: 1px solid #f0f0f0;\n vertical-align: top;\n}\n\n.ref-table tr:last-child td { border-bottom: none; }\n\n.ref-table td:first-child {\n font-family: Consolas, monospace;\n color: #005cc5;\n white-space: nowrap;\n width: 110px;\n}\n\n.note {\n font-size: 11px;\n color: #666;\n font-style: italic;\n}\n\ncode {\n font-family: Consolas, monospace;\n background: #eef2f6;\n padding: 1px 4px;\n border-radius: 3px;\n color: #2c4a68;\n font-size: 11px;\n}\n</style>\n</head>\n<body>\n<div class=\"content\">\n\n <div class=\"intro-banner\">\n A <strong>Binding</strong> connects one or more signals to a property of an element.<br>\n An optional <strong>formula</strong> transforms the signal values before they are applied.\n A <strong>converter</strong> maps the final value to a display value.\n </div>\n\n <div class=\"content-section\">\n <div class=\"section-title\"><span class=\"section-num\">1</span> Signals (Objects) <span class=\"section-hint\">what data to read</span></div>\n <div class=\"note\" style=\"margin-bottom:2px;\">Each row adds one signal. The <em>name</em> column sets the variable name used in the formula.</div>\n <pre class=\"snippet\"><span style=\"color:#6a737d;font-style:italic\">// name column empty \u2192 variable is __0, __1, __2 \u2026 (positional)</span>\n<span style=\"color:#6a737d;font-style:italic\">// name column filled \u2192 variable uses that name in the formula</span>\n\n<span style=\"color:#005cc5\">name</span>: mySpeed <span style=\"color:#005cc5\">signal</span>: .Drive.Speed \u2192 accessible as mySpeed\n<span style=\"color:#005cc5\">name</span>: (empty) <span style=\"color:#005cc5\">signal</span>: .Drive.Active \u2192 accessible as __1 (second row)</pre>\n <div class=\"note\">Multiple signals let you combine values in a formula, e.g. <code>__0 > 0 && __1</code>.</div>\n </div>\n\n <div class=\"content-section\">\n <div class=\"section-title\"><span class=\"section-num\">2</span> Signal path prefixes & special paths <span class=\"section-hint\">special access modes</span></div>\n <table class=\"ref-table\">\n <tr><th>Prefix / Pattern</th><th>What it accesses</th></tr>\n <tr><td>(none)</td><td>The signal's current value directly</td></tr>\n <tr><td>__name</td><td>Internal screen variable \u2014 local to the current screen, not connected to any external data source. Initial value is <code>null</code>. Useful for screen-local state like tab selection or panel visibility.</td></tr>\n <tr><td>?name</td><td>A signal in a screen property</td></tr>\n <tr><td>??name</td><td>The current value of a screen property</td></tr>\n <tr><td>?$name</td><td>The signal object stored in a screen property</td></tr>\n <tr><td>?@name</td><td>The current value of a screen attribute</td></tr>\n <tr><td>#name</td><td>A signal in a property of the bound target element</td></tr>\n <tr><td>##name</td><td>The current value of a property of the bound target element</td></tr>\n <tr><td>#$name</td><td>The signal object stored in a property of the bound target element</td></tr>\n <tr><td>#@name</td><td>The current value of an attribute of the bound target element</td></tr>\n <tr><td>$objectId</td><td>The entire signal object</td></tr>\n <tr><td>\u00A7name</td><td>A special framework-provided value (if supported)</td></tr>\n <tr><td>{name}</td><td>Dynamic signal path: embed another signal's value inside the path, e.g. <code>.Plant.{.ActiveLine}.Speed</code></td></tr>\n </table>\n </div>\n\n <div class=\"content-section\">\n <div class=\"section-title\"><span class=\"section-num\">3</span> Formula <span class=\"section-hint\">transform signal values with JavaScript</span></div>\n <div class=\"note\" style=\"margin-bottom:2px;\">Leave the formula empty to pass the first signal's value through unchanged.</div>\n <pre class=\"snippet\"><span style=\"color:#6a737d;font-style:italic\">// Access signal values by position or by name:</span>\n__0 * 100 <span style=\"color:#6a737d;font-style:italic\">// first signal \u00D7 100</span>\n__0 > 0 && __1 <span style=\"color:#6a737d;font-style:italic\">// combine two signals</span>\nspeed > limit <span style=\"color:#6a737d;font-style:italic\">// named variables</span>\n\n<span style=\"color:#6a737d;font-style:italic\">// Special variables available in the formula:</span>\n__ctx <span style=\"color:#6a737d;font-style:italic\">// the full binding context object</span>\n__res <span style=\"color:#6a737d;font-style:italic\">// the result of the previous evaluation</span></pre>\n <div class=\"note\">The formula must be a single JavaScript expression that returns a value (no <code>return</code> statement needed).</div>\n </div>\n\n <div class=\"content-section\">\n <div class=\"section-title\"><span class=\"section-num\">4</span> Write Back & Two-Way <span class=\"section-hint\">send values back to signals</span></div>\n <pre class=\"snippet\"><span style=\"color:#6a737d;font-style:italic\">// Write back signal</span>\n<span style=\"color:#6a737d;font-style:italic\">// \u2192 writes the formula result back to a signal whenever</span>\n<span style=\"color:#6a737d;font-style:italic\">// the element property changes (useful for derived values)</span>\n\n<span style=\"color:#6a737d;font-style:italic\">// Two-Way binding</span>\n<span style=\"color:#6a737d;font-style:italic\">// \u2192 also sets the signal when the element fires a DOM event</span>\n<span style=\"color:#6a737d;font-style:italic\">// (e.g. \"change\" for inputs). Enable \"events\" to configure</span>\n<span style=\"color:#6a737d;font-style:italic\">// which events trigger the write-back.</span>\n<span style=\"color:#6a737d;font-style:italic\">// \u2192 \"formula write back\" applies a reverse transformation</span>\n<span style=\"color:#6a737d;font-style:italic\">// before writing (useful when the formula changes the scale)</span></pre>\n <div class=\"note\">Two-Way is only available for <em>property</em> and <em>attribute</em> targets. Separate multiple event names with a semicolon.</div>\n </div>\n\n <div class=\"content-section\">\n <div class=\"section-title\"><span class=\"section-num\">5</span> Converter <span class=\"section-hint\">map values to display values</span></div>\n <div class=\"note\" style=\"margin-bottom:4px;\">Use the table to map specific conditions to output values. The <em>condition</em> column supports several matching modes:</div>\n <table class=\"ref-table\">\n <tr><th>Condition key</th><th>Matches when \u2026</th></tr>\n <tr><td>true / false</td><td>Signal value equals the boolean</td></tr>\n <tr><td>42</td><td>Signal value equals the number exactly</td></tr>\n <tr><td>running</td><td>Signal value equals the string \"running\"</td></tr>\n <tr><td>>10</td><td>Signal value is greater than 10</td></tr>\n <tr><td>>=5</td><td>Signal value is \u2265 5</td></tr>\n <tr><td><3</td><td>Signal value is < 3</td></tr>\n <tr><td>10-20</td><td>Signal value is in the range 10 \u2026 20 (inclusive)</td></tr>\n <tr><td>default</td><td>Fallback when no other key matches</td></tr>\n </table>\n <div class=\"note\" style=\"margin-top:4px;\">The value column supports template literals \u2014 use <code>${value}</code> to embed the original signal value in the output string.</div>\n <div class=\"note\" style=\"margin-top:4px;\"><strong>Named converter:</strong> enter a converter name in the <em>converter</em> field above the table to use a pre-registered converter instead of the inline table.</div>\n </div>\n\n</div>\n</body>\n</html>";
|
|
@@ -168,8 +168,12 @@ code {
|
|
|
168
168
|
<tr><td>__name</td><td>Internal screen variable — local to the current screen, not connected to any external data source. Initial value is <code>null</code>. Useful for screen-local state like tab selection or panel visibility.</td></tr>
|
|
169
169
|
<tr><td>?name</td><td>A signal in a screen property</td></tr>
|
|
170
170
|
<tr><td>??name</td><td>The current value of a screen property</td></tr>
|
|
171
|
+
<tr><td>?$name</td><td>The signal object stored in a screen property</td></tr>
|
|
172
|
+
<tr><td>?@name</td><td>The current value of a screen attribute</td></tr>
|
|
171
173
|
<tr><td>#name</td><td>A signal in a property of the bound target element</td></tr>
|
|
172
174
|
<tr><td>##name</td><td>The current value of a property of the bound target element</td></tr>
|
|
175
|
+
<tr><td>#$name</td><td>The signal object stored in a property of the bound target element</td></tr>
|
|
176
|
+
<tr><td>#@name</td><td>The current value of an attribute of the bound target element</td></tr>
|
|
173
177
|
<tr><td>$objectId</td><td>The entire signal object</td></tr>
|
|
174
178
|
<tr><td>§name</td><td>A special framework-provided value (if supported)</td></tr>
|
|
175
179
|
<tr><td>{name}</td><td>Dynamic signal path: embed another signal's value inside the path, e.g. <code>.Plant.{.ActiveLine}.Speed</code></td></tr>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { BindingTarget } from '@node-projects/web-component-designer/dist/elements/item/BindingTarget.js';
|
|
2
|
+
import { IBinding } from '@node-projects/web-component-designer/dist/elements/item/IBinding.js';
|
|
3
|
+
import { IDesignItem } from '@node-projects/web-component-designer/dist/elements/item/IDesignItem.js';
|
|
4
|
+
import { RefreshMode } from '@node-projects/web-component-designer/dist/elements/services/propertiesService/IPropertiesService.js';
|
|
5
|
+
import { IProperty } from '@node-projects/web-component-designer/dist/elements/services/propertiesService/IProperty.js';
|
|
6
|
+
import { ValueType } from '@node-projects/web-component-designer/dist/elements/services/propertiesService/ValueType.js';
|
|
7
|
+
import { AbstractPropertiesService } from '@node-projects/web-component-designer/dist/elements/services/propertiesService/services/AbstractPropertiesService.js';
|
|
8
|
+
/** Adapter for host binding editors: a class token is a boolean binding target. */
|
|
9
|
+
export declare class ClassBindingsPropertiesService extends AbstractPropertiesService {
|
|
10
|
+
getRefreshMode(): RefreshMode;
|
|
11
|
+
isHandledElement(designItem: IDesignItem): boolean;
|
|
12
|
+
getBindings(designItem: IDesignItem): IBinding[];
|
|
13
|
+
getProperties(designItem: IDesignItem): Promise<IProperty[]>;
|
|
14
|
+
getProperty(designItem: IDesignItem, name: string): Promise<IProperty>;
|
|
15
|
+
createProperty(name: string): IProperty;
|
|
16
|
+
getPropertyTarget(): BindingTarget;
|
|
17
|
+
getBinding(items: IDesignItem[], property: IProperty): IBinding;
|
|
18
|
+
isSet(items: IDesignItem[], property: IProperty): ValueType.none | ValueType.bound;
|
|
19
|
+
getValue(items: IDesignItem[], property: IProperty): string;
|
|
20
|
+
validateName(designItem: IDesignItem, name: string, previousName?: string): string;
|
|
21
|
+
rename(designItem: IDesignItem, binding: IBinding, name: string): void;
|
|
22
|
+
remove(designItem: IDesignItem, binding: IBinding): void;
|
|
23
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { BindingTarget } from '@node-projects/web-component-designer/dist/elements/item/BindingTarget.js';
|
|
2
|
+
import { RefreshMode } from '@node-projects/web-component-designer/dist/elements/services/propertiesService/IPropertiesService.js';
|
|
3
|
+
import { PropertyType } from '@node-projects/web-component-designer/dist/elements/services/propertiesService/PropertyType.js';
|
|
4
|
+
import { ValueType } from '@node-projects/web-component-designer/dist/elements/services/propertiesService/ValueType.js';
|
|
5
|
+
import { AbstractPropertiesService } from '@node-projects/web-component-designer/dist/elements/services/propertiesService/services/AbstractPropertiesService.js';
|
|
6
|
+
/** Adapter for host binding editors: a class token is a boolean binding target. */
|
|
7
|
+
export class ClassBindingsPropertiesService extends AbstractPropertiesService {
|
|
8
|
+
getRefreshMode() { return RefreshMode.fullOnValueChange; }
|
|
9
|
+
isHandledElement(designItem) { return !designItem.isRootItem; }
|
|
10
|
+
getBindings(designItem) {
|
|
11
|
+
return designItem.serviceContainer.getServices('bindingService')
|
|
12
|
+
.flatMap(service => (service.getBindings(designItem) ?? [])
|
|
13
|
+
.filter(binding => binding.target === BindingTarget.class)
|
|
14
|
+
.map(binding => ({ ...binding, service })));
|
|
15
|
+
}
|
|
16
|
+
async getProperties(designItem) {
|
|
17
|
+
return this.getBindings(designItem).map(binding => this.createProperty(binding.targetName));
|
|
18
|
+
}
|
|
19
|
+
async getProperty(designItem, name) {
|
|
20
|
+
return this.createProperty(name);
|
|
21
|
+
}
|
|
22
|
+
createProperty(name) {
|
|
23
|
+
return { name, type: 'boolean', propertyType: PropertyType.property, service: this };
|
|
24
|
+
}
|
|
25
|
+
getPropertyTarget() { return BindingTarget.class; }
|
|
26
|
+
getBinding(items, property) {
|
|
27
|
+
return this.getBindings(items[0]).find(binding => binding.targetName === property.name);
|
|
28
|
+
}
|
|
29
|
+
isSet(items, property) {
|
|
30
|
+
return this.getBinding(items, property) ? ValueType.bound : ValueType.none;
|
|
31
|
+
}
|
|
32
|
+
getValue(items, property) {
|
|
33
|
+
return this.getBinding(items, property)?.expression;
|
|
34
|
+
}
|
|
35
|
+
validateName(designItem, name, previousName) {
|
|
36
|
+
if (!name || /[\s"'<>/=]/.test(name) || name !== name.toLowerCase())
|
|
37
|
+
return 'Use a lowercase class name without spaces or HTML attribute delimiters, e.g. is-active.';
|
|
38
|
+
if (name !== previousName && this.getBindings(designItem).some(binding => binding.targetName === name))
|
|
39
|
+
return 'This class already has a binding.';
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
rename(designItem, binding, name) {
|
|
43
|
+
const error = this.validateName(designItem, name, binding.targetName);
|
|
44
|
+
if (error)
|
|
45
|
+
throw new Error(error);
|
|
46
|
+
if (name === binding.targetName)
|
|
47
|
+
return;
|
|
48
|
+
const group = designItem.openGroup(`rename class binding: ${binding.targetName} to ${name}`);
|
|
49
|
+
try {
|
|
50
|
+
if (!binding.service.setBinding(designItem, { ...binding, targetName: name }))
|
|
51
|
+
throw new Error('The binding service could not rename this binding.');
|
|
52
|
+
if (!binding.service.clearBinding(designItem, binding.targetName, BindingTarget.class))
|
|
53
|
+
throw new Error('The binding service could not remove the previous binding.');
|
|
54
|
+
group.commit();
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
group.abort();
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
remove(designItem, binding) {
|
|
62
|
+
const group = designItem.openGroup(`remove class binding: ${binding.targetName}`);
|
|
63
|
+
try {
|
|
64
|
+
if (!binding.service.clearBinding(designItem, binding.targetName, BindingTarget.class))
|
|
65
|
+
throw new Error('The binding service could not remove this binding.');
|
|
66
|
+
group.commit();
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
group.abort();
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { IProperty, IPropertyGridExtensionProvider, IPropertyGridExtension } from '@node-projects/web-component-designer';
|
|
2
|
+
/** Register in serviceContainer.propertyGridExtensions to enable visualization class bindings. */
|
|
3
|
+
export declare class ClassBindingsPropertyGridExtensionProvider implements IPropertyGridExtensionProvider {
|
|
4
|
+
createExtension(property: IProperty): IPropertyGridExtension | undefined;
|
|
5
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { PropertyType } from '@node-projects/web-component-designer/dist/elements/services/propertiesService/PropertyType.js';
|
|
2
|
+
import { PropertyGridClassBindings } from './PropertyGridClassBindings.js';
|
|
3
|
+
/** Register in serviceContainer.propertyGridExtensions to enable visualization class bindings. */
|
|
4
|
+
export class ClassBindingsPropertyGridExtensionProvider {
|
|
5
|
+
createExtension(property) {
|
|
6
|
+
if ((property.name !== 'class' && property.name !== 'className') || property.propertyType === PropertyType.cssValue)
|
|
7
|
+
return undefined;
|
|
8
|
+
const element = new PropertyGridClassBindings();
|
|
9
|
+
return {
|
|
10
|
+
element,
|
|
11
|
+
refresh: items => element.refresh(items),
|
|
12
|
+
getContextMenuItems: items => [{ title: 'Add class binding…', disabled: items?.length !== 1 || !items[0].serviceContainer.config.openBindingsEditor, action: () => element.addBinding() }]
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -34,6 +34,7 @@ export declare class EventAssignment extends BaseCustomWebComponentConstructorAp
|
|
|
34
34
|
})[];
|
|
35
35
|
initialize(visualizationHandler: VisualizationHandler, visualizationShell: VisualizationShell, scriptCommandsTypeInfo: any, propertiesTypeInfo: any, blocklyToolbox: any, commandDescriptions?: Record<string, ScriptCommandHelp>): void;
|
|
36
36
|
set instanceServiceContainer(value: InstanceServiceContainer);
|
|
37
|
+
dispose(): void;
|
|
37
38
|
protected _createControlsForScript(eventItem: eventWithDesignItem): any;
|
|
38
39
|
protected _getScriptName(eventItem: IEvent): any;
|
|
39
40
|
protected _changeScriptName(e: KeyboardEvent, eventItem: IEvent): Promise<void>;
|
|
@@ -88,10 +88,13 @@ export class EventAssignment extends BaseCustomWebComponentConstructorAppend {
|
|
|
88
88
|
set instanceServiceContainer(value) {
|
|
89
89
|
this._instanceServiceContainer = value;
|
|
90
90
|
this._selectionChangedHandler?.dispose();
|
|
91
|
-
this._selectionChangedHandler = this._instanceServiceContainer
|
|
91
|
+
this._selectionChangedHandler = this._instanceServiceContainer?.selectionService.onSelectionChanged.on(e => {
|
|
92
92
|
this.selectedItems = e.selectedElements;
|
|
93
93
|
});
|
|
94
|
-
this.selectedItems = this._instanceServiceContainer
|
|
94
|
+
this.selectedItems = this._instanceServiceContainer?.selectionService.selectedElements ?? [];
|
|
95
|
+
}
|
|
96
|
+
dispose() {
|
|
97
|
+
this.instanceServiceContainer = null;
|
|
95
98
|
}
|
|
96
99
|
_createControlsForScript(eventItem) {
|
|
97
100
|
const st = this._getScriptType(eventItem);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { BaseCustomWebComponentLazyAppend } from '@node-projects/base-custom-webcomponent';
|
|
2
|
+
import { IDesignItem } from '@node-projects/web-component-designer/dist/elements/item/IDesignItem.js';
|
|
3
|
+
export declare class PropertyGridClassBindings extends BaseCustomWebComponentLazyAppend {
|
|
4
|
+
private _items;
|
|
5
|
+
private _service;
|
|
6
|
+
private _rows;
|
|
7
|
+
private _add;
|
|
8
|
+
private _toggle;
|
|
9
|
+
private _signature;
|
|
10
|
+
private _editing;
|
|
11
|
+
static readonly style: CSSStyleSheet;
|
|
12
|
+
constructor();
|
|
13
|
+
refresh(items: IDesignItem[]): void;
|
|
14
|
+
addBinding(): void;
|
|
15
|
+
private editBinding;
|
|
16
|
+
private showNameDialog;
|
|
17
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { BaseCustomWebComponentLazyAppend, css } from '@node-projects/base-custom-webcomponent';
|
|
2
|
+
import { BindingTarget } from '@node-projects/web-component-designer/dist/elements/item/BindingTarget.js';
|
|
3
|
+
import { ContextMenu } from '@node-projects/web-component-designer/dist/elements/helper/contextMenu/ContextMenu.js';
|
|
4
|
+
import { ClassBindingsPropertiesService } from './ClassBindingsPropertiesService.js';
|
|
5
|
+
export class PropertyGridClassBindings extends BaseCustomWebComponentLazyAppend {
|
|
6
|
+
_items = [];
|
|
7
|
+
_service = new ClassBindingsPropertiesService();
|
|
8
|
+
_rows;
|
|
9
|
+
_add;
|
|
10
|
+
_toggle;
|
|
11
|
+
_signature;
|
|
12
|
+
_editing = false;
|
|
13
|
+
static style = css `
|
|
14
|
+
:host { display: block; grid-column: 1 / -1; min-width: 0; color: var(--wcd-color-text, white); }
|
|
15
|
+
:host([hidden]) { display: none !important; }
|
|
16
|
+
.header, .row { display: flex; align-items: center; gap: 8px; min-height: 28px; }
|
|
17
|
+
.row { padding-left: 16px; }
|
|
18
|
+
button { font: inherit; color: inherit; background: transparent; border: none; cursor: pointer; padding: 4px; }
|
|
19
|
+
button:focus-visible { outline: 2px solid var(--wcd-color-accent, #e91e63); outline-offset: -2px; }
|
|
20
|
+
button:disabled { opacity: .5; cursor: default; }
|
|
21
|
+
.toggle, .name { text-align: left; }
|
|
22
|
+
.toggle { flex: 1; }
|
|
23
|
+
.name { flex: 0 1 45%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
24
|
+
.summary { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--wcd-color-text-muted, #bdbdbd); }
|
|
25
|
+
.message { padding: 4px 16px; font-size: 12px; color: var(--wcd-color-text-muted, #bdbdbd); }
|
|
26
|
+
dialog { box-sizing: border-box; width: min(420px, calc(100vw - 32px)); color: inherit; background: var(--wcd-property-grid-background, var(--wcd-color-surface-raised, #2f3545)); border: 1px solid var(--wcd-color-border, #596c7a); padding: 16px; }
|
|
27
|
+
dialog::backdrop { background: #0006; }
|
|
28
|
+
form, label { display: grid; gap: 8px; }
|
|
29
|
+
form { gap: 16px; }
|
|
30
|
+
h3 { font: inherit; font-weight: 600; margin: 0; }
|
|
31
|
+
input { box-sizing: border-box; width: 100%; padding: 6px; font: inherit; color: inherit; background: transparent; border: 1px solid var(--wcd-color-border, #596c7a); }
|
|
32
|
+
.help { font-size: 12px; color: var(--wcd-color-text-muted, #bdbdbd); }
|
|
33
|
+
.error { color: var(--wcd-color-error, #ff8a80); font-size: 12px; }
|
|
34
|
+
.actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
35
|
+
.actions button { border: 1px solid var(--wcd-color-border, #596c7a); padding: 6px 12px; }
|
|
36
|
+
[hidden] { display: none !important; }
|
|
37
|
+
:host([appearance="modern"]) { color: var(--_wcd-pg-text); font-size: 13px; }
|
|
38
|
+
:host([appearance="modern"]) .header {
|
|
39
|
+
padding: 5px 12px;
|
|
40
|
+
min-height: 38px;
|
|
41
|
+
background: var(--_wcd-pg-raised);
|
|
42
|
+
border-bottom: 1px solid var(--_wcd-pg-border);
|
|
43
|
+
}
|
|
44
|
+
:host([appearance="modern"]) .toggle { font-weight: 600; }
|
|
45
|
+
:host([appearance="modern"]) .row {
|
|
46
|
+
display: grid;
|
|
47
|
+
grid-template-columns: minmax(0, .85fr) minmax(0, 1fr) 28px;
|
|
48
|
+
padding: 5px 12px 5px 24px;
|
|
49
|
+
min-height: 38px;
|
|
50
|
+
border-bottom: 1px solid var(--_wcd-pg-border);
|
|
51
|
+
}
|
|
52
|
+
:host([appearance="modern"]) .row:hover,
|
|
53
|
+
:host([appearance="modern"]) .row:focus-within { background: var(--_wcd-pg-hover); }
|
|
54
|
+
:host([appearance="modern"]) .summary { color: var(--_wcd-pg-muted); }
|
|
55
|
+
:host([appearance="modern"]) button { border-radius: 4px; }
|
|
56
|
+
:host([appearance="modern"]) button:hover { background: var(--_wcd-pg-hover); }
|
|
57
|
+
:host([appearance="modern"]) button:focus-visible { outline-color: var(--wcd-color-focus, var(--_wcd-pg-accent)); }
|
|
58
|
+
:host([appearance="modern"]) dialog {
|
|
59
|
+
padding: 24px;
|
|
60
|
+
border-radius: 10px;
|
|
61
|
+
color: var(--_wcd-pg-text);
|
|
62
|
+
background: var(--_wcd-pg-surface);
|
|
63
|
+
border-color: var(--_wcd-pg-border);
|
|
64
|
+
box-shadow: 0 16px 64px #0003;
|
|
65
|
+
}
|
|
66
|
+
:host([appearance="modern"]) h3 { font-size: 16px; }
|
|
67
|
+
:host([appearance="modern"]) input { border-radius: 5px; border-color: var(--_wcd-pg-border); padding: 8px; }
|
|
68
|
+
:host([appearance="modern"]) input:focus-visible { outline: 2px solid var(--wcd-color-focus, var(--_wcd-pg-accent)); outline-offset: 1px; }
|
|
69
|
+
:host([appearance="modern"]) .help { color: var(--_wcd-pg-muted); }
|
|
70
|
+
:host([appearance="modern"]) .actions button { border-color: var(--_wcd-pg-border); }
|
|
71
|
+
:host([appearance="modern"]) .actions button[type="submit"] {
|
|
72
|
+
background: var(--_wcd-pg-accent);
|
|
73
|
+
color: var(--wcd-color-accent-text, light-dark(white, #162b48));
|
|
74
|
+
border-color: transparent;
|
|
75
|
+
}
|
|
76
|
+
`;
|
|
77
|
+
constructor() {
|
|
78
|
+
super();
|
|
79
|
+
const header = document.createElement('div');
|
|
80
|
+
header.className = 'header';
|
|
81
|
+
this._toggle = document.createElement('button');
|
|
82
|
+
this._toggle.type = 'button';
|
|
83
|
+
this._toggle.className = 'toggle';
|
|
84
|
+
this._toggle.textContent = '▾ Class bindings';
|
|
85
|
+
this._toggle.setAttribute('aria-expanded', 'true');
|
|
86
|
+
this._toggle.onclick = () => {
|
|
87
|
+
this._rows.hidden = !this._rows.hidden;
|
|
88
|
+
this._toggle.textContent = (this._rows.hidden ? '▸' : '▾') + ' Class bindings';
|
|
89
|
+
this._toggle.setAttribute('aria-expanded', String(!this._rows.hidden));
|
|
90
|
+
};
|
|
91
|
+
this._add = document.createElement('button');
|
|
92
|
+
this._add.type = 'button';
|
|
93
|
+
this._add.textContent = '+';
|
|
94
|
+
this._add.title = 'Add class binding…';
|
|
95
|
+
this._add.setAttribute('aria-label', 'Add class binding');
|
|
96
|
+
this._add.onclick = () => this.addBinding();
|
|
97
|
+
header.append(this._toggle, this._add);
|
|
98
|
+
this._rows = document.createElement('div');
|
|
99
|
+
this.shadowRoot.append(header, this._rows);
|
|
100
|
+
}
|
|
101
|
+
refresh(items) {
|
|
102
|
+
const selectionChanged = this._items[0] !== items?.[0] || this._items.length !== items?.length;
|
|
103
|
+
if (selectionChanged)
|
|
104
|
+
this.shadowRoot.querySelector('dialog')?.close();
|
|
105
|
+
this._items = items ?? [];
|
|
106
|
+
const item = this._items.length === 1 ? this._items[0] : null;
|
|
107
|
+
const bindings = item ? this._service.getBindings(item) : [];
|
|
108
|
+
this._add.disabled = !item || !item.serviceContainer.config.openBindingsEditor;
|
|
109
|
+
const signature = JSON.stringify(bindings.map(({ service, ...binding }) => binding));
|
|
110
|
+
if (!selectionChanged && signature === this._signature)
|
|
111
|
+
return;
|
|
112
|
+
this._signature = signature;
|
|
113
|
+
this._rows.replaceChildren();
|
|
114
|
+
if (!item) {
|
|
115
|
+
const message = document.createElement('div');
|
|
116
|
+
message.className = 'message';
|
|
117
|
+
message.textContent = 'Select one element to edit class bindings.';
|
|
118
|
+
this._rows.appendChild(message);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
for (const binding of bindings) {
|
|
122
|
+
const row = document.createElement('div');
|
|
123
|
+
row.className = 'row';
|
|
124
|
+
const name = document.createElement('button');
|
|
125
|
+
name.type = 'button';
|
|
126
|
+
name.className = 'name';
|
|
127
|
+
name.textContent = binding.targetName;
|
|
128
|
+
name.title = `Edit class binding: ${binding.targetName}`;
|
|
129
|
+
name.onclick = () => this.editBinding(item, binding);
|
|
130
|
+
const summary = document.createElement('span');
|
|
131
|
+
summary.className = 'summary';
|
|
132
|
+
summary.textContent = '← ' + (binding.expression || binding.bindableObjectNames?.join(';') || binding.rawValue || 'binding');
|
|
133
|
+
summary.title = summary.textContent;
|
|
134
|
+
summary.ondblclick = () => this.editBinding(item, binding);
|
|
135
|
+
const menu = document.createElement('button');
|
|
136
|
+
menu.type = 'button';
|
|
137
|
+
menu.textContent = '⋯';
|
|
138
|
+
menu.setAttribute('aria-label', `Actions for class binding ${binding.targetName}`);
|
|
139
|
+
const openMenu = (event) => {
|
|
140
|
+
event.preventDefault();
|
|
141
|
+
ContextMenu.show([
|
|
142
|
+
{ title: 'Edit binding…', action: () => this.editBinding(item, binding) },
|
|
143
|
+
{ title: 'Rename class…', action: () => this.showNameDialog(item, binding) },
|
|
144
|
+
{ title: 'Remove binding', action: () => { this._service.remove(item, binding); this.refresh(this._items); } }
|
|
145
|
+
], event);
|
|
146
|
+
};
|
|
147
|
+
menu.onclick = openMenu;
|
|
148
|
+
row.oncontextmenu = openMenu;
|
|
149
|
+
row.append(name, summary, menu);
|
|
150
|
+
this._rows.appendChild(row);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
addBinding() {
|
|
154
|
+
if (!this._add.disabled)
|
|
155
|
+
this.showNameDialog(this._items[0]);
|
|
156
|
+
}
|
|
157
|
+
async editBinding(item, binding, name = binding?.targetName) {
|
|
158
|
+
if (this._editing)
|
|
159
|
+
return;
|
|
160
|
+
const editor = item.serviceContainer.config.openBindingsEditor;
|
|
161
|
+
if (editor) {
|
|
162
|
+
this._editing = true;
|
|
163
|
+
try {
|
|
164
|
+
await editor(this._service.createProperty(name), [item], binding, BindingTarget.class);
|
|
165
|
+
this.refresh(this._items);
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
this._editing = false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
showNameDialog(item, binding, name = binding?.targetName ?? '') {
|
|
173
|
+
if (this.shadowRoot.querySelector('dialog'))
|
|
174
|
+
return;
|
|
175
|
+
const dialog = document.createElement('dialog');
|
|
176
|
+
const form = document.createElement('form');
|
|
177
|
+
const title = document.createElement('h3');
|
|
178
|
+
title.textContent = binding ? 'Rename class' : 'Add class binding';
|
|
179
|
+
const nameLabel = document.createElement('label');
|
|
180
|
+
nameLabel.textContent = 'Class name';
|
|
181
|
+
const input = document.createElement('input');
|
|
182
|
+
input.value = name;
|
|
183
|
+
input.required = true;
|
|
184
|
+
const help = document.createElement('span');
|
|
185
|
+
help.className = 'help';
|
|
186
|
+
help.textContent = 'Use lowercase names, e.g. is-active.';
|
|
187
|
+
nameLabel.append(input, help);
|
|
188
|
+
const error = document.createElement('div');
|
|
189
|
+
error.className = 'error';
|
|
190
|
+
error.setAttribute('role', 'alert');
|
|
191
|
+
error.hidden = true;
|
|
192
|
+
const actions = document.createElement('div');
|
|
193
|
+
actions.className = 'actions';
|
|
194
|
+
const cancel = document.createElement('button');
|
|
195
|
+
cancel.type = 'button';
|
|
196
|
+
cancel.textContent = 'Cancel';
|
|
197
|
+
cancel.onclick = () => dialog.close();
|
|
198
|
+
const save = document.createElement('button');
|
|
199
|
+
save.type = 'submit';
|
|
200
|
+
save.textContent = binding ? 'Save' : 'Next…';
|
|
201
|
+
actions.append(cancel, save);
|
|
202
|
+
form.append(title, nameLabel);
|
|
203
|
+
form.append(error, actions);
|
|
204
|
+
dialog.appendChild(form);
|
|
205
|
+
this.shadowRoot.appendChild(dialog);
|
|
206
|
+
dialog.onclose = () => dialog.remove();
|
|
207
|
+
form.onsubmit = async (event) => {
|
|
208
|
+
event.preventDefault();
|
|
209
|
+
const newName = input.value.trim();
|
|
210
|
+
const validation = this._service.validateName(item, newName, binding?.targetName);
|
|
211
|
+
try {
|
|
212
|
+
if (validation)
|
|
213
|
+
throw new Error(validation);
|
|
214
|
+
if (binding) {
|
|
215
|
+
this._service.rename(item, binding, newName);
|
|
216
|
+
}
|
|
217
|
+
dialog.close();
|
|
218
|
+
if (!binding)
|
|
219
|
+
await this.editBinding(item, undefined, newName);
|
|
220
|
+
this.refresh(this._items);
|
|
221
|
+
}
|
|
222
|
+
catch (exception) {
|
|
223
|
+
error.textContent = exception.message;
|
|
224
|
+
error.hidden = false;
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
dialog.showModal();
|
|
228
|
+
input.focus();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
customElements.define('node-projects-property-grid-class-bindings', PropertyGridClassBindings);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { VisualizationBinding } from "../interfaces/VisualizationBinding.js";
|
|
2
|
-
import { VisualizationHandler } from "../interfaces/VisualizationHandler.js";
|
|
2
|
+
import { State, VisualizationHandler } from "../interfaces/VisualizationHandler.js";
|
|
3
3
|
import { BindingTarget } from "@node-projects/web-component-designer/dist/elements/item/BindingTarget.js";
|
|
4
4
|
export type SpecialValueHandler = {
|
|
5
5
|
valueProvider: (propertyName: string, context: {
|
|
@@ -22,6 +22,8 @@ export declare const bindingPrefixInsideCss = "bind(";
|
|
|
22
22
|
export declare const bindingPrefixInsideCssVarName = "--tmpBinding_";
|
|
23
23
|
export declare const bindingsInCssRegex: RegExp;
|
|
24
24
|
export type namedBinding = [name: string, binding: VisualizationBinding];
|
|
25
|
+
type Cleanup = () => void;
|
|
26
|
+
type StateChangeCallback = (id: string, value: State) => void;
|
|
25
27
|
export declare function isLit(element: Element): boolean;
|
|
26
28
|
export declare function parseBindingString(id: string): {
|
|
27
29
|
parts: string[];
|
|
@@ -32,30 +34,40 @@ export declare class BindingsHelper {
|
|
|
32
34
|
#private;
|
|
33
35
|
_visualizationHandler: VisualizationHandler;
|
|
34
36
|
namedConverterCallback: (converter: string, value: any, element: Element, binding: namedBinding) => any;
|
|
37
|
+
private _stateSubscriptions;
|
|
38
|
+
private _scheduledUpdates;
|
|
39
|
+
private _updateFlushScheduled;
|
|
40
|
+
private _compiledConverters;
|
|
35
41
|
constructor(visualizationHandler: VisualizationHandler);
|
|
42
|
+
private scheduleUpdate;
|
|
36
43
|
getChangedEventName(element: Element, propertyName: string): string;
|
|
44
|
+
private getParsedTargetName;
|
|
37
45
|
parseBinding(element: Element, name: string, value: string, bindingTarget: BindingTarget, prefix: string): namedBinding;
|
|
38
46
|
serializeBinding(element: Element, targetName: string, binding: VisualizationBinding): [name: string, value: string];
|
|
47
|
+
private getShortBindingAttributeName;
|
|
48
|
+
private getJsonBindingAttributeName;
|
|
39
49
|
getBindingAttributeName(element: Element, propertyName: string, propertyTarget: BindingTarget): string;
|
|
40
50
|
getBindings(element: Element): Generator<namedBinding, void, unknown>;
|
|
41
51
|
applyAllBindings(rootElement: ParentNode, relativeSignalPath: string, root: HTMLElement, specialValueHandler?: SpecialValueHandler, skipChildrenFor?: (element: Element) => boolean): (() => void)[];
|
|
42
52
|
parseCssBindings(sheet: string, element: Element, relativeSignalPath: string, root: HTMLElement): Promise<[stylesheet: CSSStyleSheet, unsub: (() => void)[]]>;
|
|
43
53
|
parseCssBinding(value: string, element: Element, relativeSignalPath: string, root: HTMLElement, specialValueHandler?: SpecialValueHandler): [name: string, unsub: (() => void)[]];
|
|
54
|
+
/** @internal Shared fan-out avoids duplicate backend subscriptions and initial reads. */
|
|
55
|
+
subscribeToState(id: string, callback: StateChangeCallback): Cleanup;
|
|
44
56
|
/**
|
|
45
|
-
* ? =
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
* @param root
|
|
53
|
-
* @returns
|
|
54
|
-
*/
|
|
55
|
-
applyBinding(element: Element, binding: namedBinding, relativeSignalPath: string, root: HTMLElement, specialValueHandler?: SpecialValueHandler): () => void;
|
|
57
|
+
* ? = signal name from a root property; ?? = a root property value
|
|
58
|
+
* # = signal name from a target property; ## = a target property value
|
|
59
|
+
* ?$ = signal object from a root property; #$ = signal object from a target property
|
|
60
|
+
* ?@ = a root attribute value; #@ = a target attribute value
|
|
61
|
+
* $ = a signal configuration; § = a special value
|
|
62
|
+
*/
|
|
63
|
+
applyBinding(element: Element, binding: namedBinding, relativeSignalPath: string, root: HTMLElement, specialValueHandler?: SpecialValueHandler): Cleanup;
|
|
56
64
|
private addTwoWayBinding;
|
|
57
65
|
private static parseValueWithType;
|
|
58
|
-
|
|
66
|
+
private compileConverter;
|
|
67
|
+
private getCompiledConverter;
|
|
68
|
+
private applyCompiledConverter;
|
|
69
|
+
private createTargetWriter;
|
|
59
70
|
static camelToDotCase(text: string): string;
|
|
60
71
|
static dotToCamelCase(text: string): string;
|
|
61
72
|
}
|
|
73
|
+
export {};
|