@myrmidon/gve-core 1.0.0 → 3.0.2

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 (27) hide show
  1. package/README.md +32 -2
  2. package/fesm2022/myrmidon-gve-core.mjs +1277 -927
  3. package/fesm2022/myrmidon-gve-core.mjs.map +1 -1
  4. package/lib/components/animation-timeline/animation-timeline.component.d.ts +7 -10
  5. package/lib/components/animation-timeline-set/animation-timeline-set.component.d.ts +9 -12
  6. package/lib/components/animation-tween/animation-tween.component.d.ts +6 -8
  7. package/lib/components/base-text-char/base-text-char.component.d.ts +3 -6
  8. package/lib/components/base-text-editor/base-text-editor.component.d.ts +6 -8
  9. package/lib/components/base-text-view/base-text-view.component.d.ts +15 -12
  10. package/lib/components/batch-operation-editor/batch-operation-editor.component.d.ts +5 -7
  11. package/lib/components/chain-operation-editor/chain-operation-editor.component.d.ts +14 -15
  12. package/lib/components/chain-result-view/chain-result-view.component.d.ts +8 -11
  13. package/lib/components/chain-view/chain-view.component.d.ts +42 -0
  14. package/lib/components/feature-editor/feature-editor.component.d.ts +8 -12
  15. package/lib/components/feature-set-editor/feature-set-editor.component.d.ts +11 -13
  16. package/lib/components/feature-set-view/feature-set-view.component.d.ts +5 -7
  17. package/lib/components/ln-heights-editor/ln-heights-editor.component.d.ts +5 -8
  18. package/lib/components/operation-source-editor/operation-source-editor.component.d.ts +8 -11
  19. package/lib/components/snapshot-editor/snapshot-editor.component.d.ts +46 -35
  20. package/lib/components/snapshot-text-editor/snapshot-text-editor.component.d.ts +37 -0
  21. package/lib/components/steps-map/steps-map.component.d.ts +9 -9
  22. package/lib/services/gve-api.service.d.ts +48 -4
  23. package/lib/services/gve-graphviz.service.d.ts +20 -0
  24. package/lib/services/settings.service.d.ts +40 -5
  25. package/package.json +5 -5
  26. package/public-api.d.ts +3 -1
  27. package/lib/components/simple-tree/simple-tree.component.d.ts +0 -35
@@ -1,7 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
- import { EventEmitter, Component, Input, Output, ViewChild, Injectable, Optional, Inject, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
2
+ import { model, input, output, effect, Component, ViewChild, Injectable, Optional, Inject, computed, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
3
3
  import * as i1 from '@angular/forms';
4
- import { Validators, ReactiveFormsModule, FormControl, FormsModule } from '@angular/forms';
4
+ import { Validators, ReactiveFormsModule, FormControl, FormGroup, FormsModule } from '@angular/forms';
5
5
  import * as i2 from '@angular/common';
6
6
  import { CommonModule } from '@angular/common';
7
7
  import * as i3 from '@angular/material/button';
@@ -18,33 +18,37 @@ import * as i6 from '@angular/material/input';
18
18
  import { MatInputModule } from '@angular/material/input';
19
19
  import * as i8 from '@angular/material/select';
20
20
  import { MatSelectModule } from '@angular/material/select';
21
- import * as i14 from '@angular/material/tabs';
21
+ import * as i16 from '@angular/material/tabs';
22
22
  import { MatTabsModule } from '@angular/material/tabs';
23
23
  import * as i10 from '@angular/material/tooltip';
24
24
  import { MatTooltipModule } from '@angular/material/tooltip';
25
- import * as i2$1 from '@myrmidon/ng-tools';
26
- import { NgToolsValidators, NgToolsModule, deepCopy } from '@myrmidon/ng-tools';
27
- import { debounceTime, distinctUntilChanged, catchError } from 'rxjs';
25
+ import * as i2$2 from '@myrmidon/ngx-tools';
26
+ import { NgxToolsValidators, ColorToContrastPipe, FlatLookupPipe, SafeHtmlPipe, deepCopy } from '@myrmidon/ngx-tools';
27
+ import { debounceTime, distinctUntilChanged, catchError, BehaviorSubject } from 'rxjs';
28
28
  import * as i9 from '@angular/material/core';
29
29
  import { MatRippleModule } from '@angular/material/core';
30
- import { SnapshotViewService, FeatureSetPolicy, OperationType, DEFAULT_SVG_BASE_TEXT_OPTIONS } from '@myrmidon/gve-snapshot-view';
31
- import * as i2$2 from '@myrmidon/ng-mat-tools';
32
- import { NgMatToolsModule } from '@myrmidon/ng-mat-tools';
30
+ import { FeatureSetPolicy, SnapshotViewService, OperationType, DEFAULT_SVG_BASE_TEXT_OPTIONS } from '@myrmidon/gve-snapshot-view';
31
+ import * as i2$1 from '@myrmidon/ngx-mat-tools';
33
32
  import * as i3$1 from '@angular/material/dialog';
34
33
  import { MAT_DIALOG_DATA } from '@angular/material/dialog';
35
34
  import * as i1$1 from '@angular/common/http';
36
35
  import { filter, debounceTime as debounceTime$1 } from 'rxjs/operators';
37
36
  import * as i2$3 from '@angular/cdk/clipboard';
38
37
  import { ClipboardModule } from '@angular/cdk/clipboard';
39
- import * as i16 from '@cisstech/nge/monaco';
38
+ import * as i6$1 from '@angular/material/badge';
39
+ import { MatBadgeModule } from '@angular/material/badge';
40
+ import * as i17 from '@cisstech/nge/monaco';
40
41
  import { NgeMonacoModule } from '@cisstech/nge/monaco';
41
42
  import { customAlphabet } from 'nanoid';
43
+ import { VizComponent } from '@myrmidon/ngx-viz';
44
+ import * as i4$2 from '@angular/material/snack-bar';
45
+ import { MatSnackBarModule } from '@angular/material/snack-bar';
42
46
  import * as i8$1 from '@angular/material/button-toggle';
43
47
  import { MatButtonToggleModule } from '@angular/material/button-toggle';
44
- import * as i13 from '@angular/material/progress-bar';
48
+ import * as i14 from '@angular/material/progress-bar';
45
49
  import { MatProgressBarModule } from '@angular/material/progress-bar';
46
- import * as i5$2 from '@angular/material/snack-bar';
47
- import { MatSnackBarModule } from '@angular/material/snack-bar';
50
+ import * as i15 from '@angular/material/slider';
51
+ import { MatSliderModule } from '@angular/material/slider';
48
52
 
49
53
  /**
50
54
  * 🔑 `gve-animation-tween`
@@ -58,28 +62,24 @@ import { MatSnackBarModule } from '@angular/material/snack-bar';
58
62
  * - 🔥 `tweenCancel` (`void`): emitted when the user cancels the edit.
59
63
  */
60
64
  class AnimationTweenComponent {
61
- /**
62
- * The tween to edit.
63
- */
64
- get tween() {
65
- return this._tween;
66
- }
67
- set tween(value) {
68
- if (this._tween === value) {
69
- return;
70
- }
71
- this._tween = value || undefined;
72
- this.updateForm(this._tween);
73
- }
74
65
  constructor(formBuilder) {
66
+ /**
67
+ * The tween to edit.
68
+ */
69
+ this.tween = model();
70
+ /**
71
+ * The IDs of the elements that can be selected by the tween.
72
+ * This list is used to allow the user to select an element from a dropdown.
73
+ */
74
+ this.elementIds = input();
75
75
  /**
76
76
  * Emitted when the tween is changed.
77
77
  */
78
- this.tweenChange = new EventEmitter();
78
+ this.tweenChange = output();
79
79
  /**
80
80
  * Emitted when the user cancels the edit.
81
81
  */
82
- this.tweenCancel = new EventEmitter();
82
+ this.tweenCancel = output();
83
83
  this.types = ['to', 'from', 'fromTo', 'set'];
84
84
  this.label = formBuilder.control('', {
85
85
  nonNullable: true,
@@ -107,6 +107,9 @@ class AnimationTweenComponent {
107
107
  position: this.position,
108
108
  });
109
109
  this.elementId = formBuilder.control(null);
110
+ effect(() => {
111
+ this.updateForm(this.tween());
112
+ });
110
113
  }
111
114
  ngOnInit() {
112
115
  // set selector when elementId changes
@@ -178,13 +181,13 @@ class AnimationTweenComponent {
178
181
  if (!this.form.valid) {
179
182
  return;
180
183
  }
181
- this._tween = this.getTween();
182
- this.tweenChange.emit(this._tween);
184
+ this.tween.set(this.getTween());
185
+ this.tweenChange.emit(this.tween());
183
186
  }
184
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: AnimationTweenComponent, deps: [{ token: i1.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); }
185
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.0", type: AnimationTweenComponent, isStandalone: true, selector: "gve-animation-tween", inputs: { tween: "tween", elementIds: "elementIds" }, outputs: { tweenChange: "tweenChange", tweenCancel: "tweenCancel" }, ngImport: i0, template: "<form [formGroup]=\"form\" (submit)=\"save()\">\r\n <div class=\"form-row\">\r\n <!-- label -->\r\n <mat-form-field>\r\n <mat-label>label</mat-label>\r\n <input matInput [formControl]=\"label\" />\r\n <mat-error\r\n *ngIf=\"$any(label).errors?.required && (label.dirty || label.touched)\"\r\n >label required</mat-error\r\n >\r\n <mat-error\r\n *ngIf=\"$any(label).errors?.maxLength && (label.dirty || label.touched)\"\r\n >label too long</mat-error\r\n >\r\n </mat-form-field>\r\n\r\n <!-- type -->\r\n <mat-form-field>\r\n <mat-label>type</mat-label>\r\n <mat-select [formControl]=\"type\">\r\n <mat-option *ngFor=\"let t of types\" [value]=\"t\">{{ t }}</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n\r\n <!-- position -->\r\n <mat-form-field>\r\n <mat-label>position</mat-label>\r\n <input matInput [formControl]=\"position\" />\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matSuffix\r\n [attr.aria-label]=\"'Information about position'\"\r\n (click)=\"\r\n openUrl(\r\n 'https://gsap.com/docs/v3/GSAP/Timeline/#positioning-animations-in-a-timeline'\r\n )\r\n \"\r\n >\r\n <mat-icon>info</mat-icon>\r\n </button>\r\n <mat-hint>time label &lt; &gt; += -=</mat-hint>\r\n <mat-error\r\n *ngIf=\"\r\n $any(position).errors?.maxLength &&\r\n (position.dirty || position.touched)\r\n \"\r\n >position too long</mat-error\r\n >\r\n </mat-form-field>\r\n </div>\r\n\r\n <div class=\"form-row\">\r\n <!-- selector -->\r\n <mat-form-field>\r\n <mat-label>selector</mat-label>\r\n <input matInput [formControl]=\"selector\" />\r\n <mat-hint>CSS selector</mat-hint>\r\n <mat-error\r\n *ngIf=\"\r\n $any(selector).errors?.required &&\r\n (selector.dirty || selector.touched)\r\n \"\r\n >selector required</mat-error\r\n >\r\n <mat-error\r\n *ngIf=\"\r\n $any(selector).errors?.maxLength &&\r\n (selector.dirty || selector.touched)\r\n \"\r\n >selector too long</mat-error\r\n >\r\n </mat-form-field>\r\n\r\n <!-- element id -->\r\n @if (elementIds?.length) {\r\n <mat-form-field>\r\n <mat-label>element ID</mat-label>\r\n <mat-select [formControl]=\"elementId\">\r\n <mat-option *ngFor=\"let id of elementIds\" [value]=\"id\">{{\r\n id\r\n }}</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n }\r\n </div>\r\n\r\n <!-- note -->\r\n <div>\r\n <mat-form-field class=\"long-text\">\r\n <mat-label>note</mat-label>\r\n <textarea rows=\"2\" matInput [formControl]=\"note\"></textarea>\r\n <mat-error\r\n *ngIf=\"$any(note).errors?.maxLength && (note.dirty || note.touched)\"\r\n >note too long</mat-error\r\n >\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- vars -->\r\n <div>\r\n <mat-form-field class=\"long-text\">\r\n <mat-label>vars</mat-label>\r\n <textarea matInput [formControl]=\"vars\"></textarea>\r\n <mat-hint>JSON object</mat-hint>\r\n <mat-error\r\n *ngIf=\"$any(vars).errors?.required && (vars.dirty || vars.touched)\"\r\n >vars required</mat-error\r\n >\r\n <mat-error *ngIf=\"$any(vars).errors?.json && (vars.dirty || vars.touched)\"\r\n >invalid JSON</mat-error\r\n >\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- vars2 only when type is fromTo -->\r\n @if (type.value === 'fromTo') {\r\n <div>\r\n <mat-form-field class=\"long-text\">\r\n <mat-label>2nd vars</mat-label>\r\n <textarea matInput [formControl]=\"vars2\"></textarea>\r\n <mat-hint>JSON object</mat-hint>\r\n <mat-error\r\n *ngIf=\"$any(vars2).errors?.required && (vars2.dirty || vars2.touched)\"\r\n >vars required</mat-error\r\n >\r\n <mat-error\r\n *ngIf=\"$any(vars2).errors?.json && (vars2.dirty || vars2.touched)\"\r\n >invalid JSON</mat-error\r\n >\r\n </mat-form-field>\r\n </div>\r\n }\r\n\r\n <div class=\"form-row\">\r\n <button\r\n type=\"button\"\r\n class=\"mat-warn\"\r\n mat-icon-button\r\n matTooltip=\"Close tween\"\r\n (click)=\"close()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n <button\r\n type=\"submit\"\r\n class=\"mat-primary\"\r\n mat-icon-button\r\n matTooltip=\"Save tween\"\r\n [disabled]=\"form.invalid || form.pristine\"\r\n >\r\n <mat-icon class=\"mat-primary\">check_circle</mat-icon>\r\n </button>\r\n </div>\r\n</form>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}.long-text{width:100%;max-width:800px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "directive", type: i5.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i5.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i5.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i8.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i9.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i10.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] }); }
187
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: AnimationTweenComponent, deps: [{ token: i1.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); }
188
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.6", type: AnimationTweenComponent, isStandalone: true, selector: "gve-animation-tween", inputs: { tween: { classPropertyName: "tween", publicName: "tween", isSignal: true, isRequired: false, transformFunction: null }, elementIds: { classPropertyName: "elementIds", publicName: "elementIds", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { tween: "tweenChange", tweenChange: "tweenChange", tweenCancel: "tweenCancel" }, ngImport: i0, template: "<form [formGroup]=\"form\" (submit)=\"save()\">\r\n <div class=\"form-row\">\r\n <!-- label -->\r\n <mat-form-field>\r\n <mat-label>label</mat-label>\r\n <input matInput [formControl]=\"label\" />\r\n <mat-error\r\n *ngIf=\"$any(label).errors?.required && (label.dirty || label.touched)\"\r\n >label required</mat-error\r\n >\r\n <mat-error\r\n *ngIf=\"$any(label).errors?.maxLength && (label.dirty || label.touched)\"\r\n >label too long</mat-error\r\n >\r\n </mat-form-field>\r\n\r\n <!-- type -->\r\n <mat-form-field>\r\n <mat-label>type</mat-label>\r\n <mat-select [formControl]=\"type\">\r\n <mat-option *ngFor=\"let t of types\" [value]=\"t\">{{ t }}</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n\r\n <!-- position -->\r\n <mat-form-field>\r\n <mat-label>position</mat-label>\r\n <input matInput [formControl]=\"position\" />\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matSuffix\r\n [attr.aria-label]=\"'Information about position'\"\r\n (click)=\"\r\n openUrl(\r\n 'https://gsap.com/docs/v3/GSAP/Timeline/#positioning-animations-in-a-timeline'\r\n )\r\n \"\r\n >\r\n <mat-icon>info</mat-icon>\r\n </button>\r\n <mat-hint>time label &lt; &gt; += -=</mat-hint>\r\n <mat-error\r\n *ngIf=\"\r\n $any(position).errors?.maxLength &&\r\n (position.dirty || position.touched)\r\n \"\r\n >position too long</mat-error\r\n >\r\n </mat-form-field>\r\n </div>\r\n\r\n <div class=\"form-row\">\r\n <!-- selector -->\r\n <mat-form-field>\r\n <mat-label>selector</mat-label>\r\n <input matInput [formControl]=\"selector\" />\r\n <mat-hint>CSS selector</mat-hint>\r\n <mat-error\r\n *ngIf=\"\r\n $any(selector).errors?.required &&\r\n (selector.dirty || selector.touched)\r\n \"\r\n >selector required</mat-error\r\n >\r\n <mat-error\r\n *ngIf=\"\r\n $any(selector).errors?.maxLength &&\r\n (selector.dirty || selector.touched)\r\n \"\r\n >selector too long</mat-error\r\n >\r\n </mat-form-field>\r\n\r\n <!-- element id -->\r\n @if ((elementIds())?.length) {\r\n <mat-form-field>\r\n <mat-label>element ID</mat-label>\r\n <mat-select [formControl]=\"elementId\">\r\n <mat-option *ngFor=\"let id of elementIds()\" [value]=\"id\">{{\r\n id\r\n }}</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n }\r\n </div>\r\n\r\n <!-- note -->\r\n <div>\r\n <mat-form-field class=\"long-text\">\r\n <mat-label>note</mat-label>\r\n <textarea rows=\"2\" matInput [formControl]=\"note\"></textarea>\r\n <mat-error\r\n *ngIf=\"$any(note).errors?.maxLength && (note.dirty || note.touched)\"\r\n >note too long</mat-error\r\n >\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- vars -->\r\n <div>\r\n <mat-form-field class=\"long-text\">\r\n <mat-label>vars</mat-label>\r\n <textarea matInput [formControl]=\"vars\"></textarea>\r\n <mat-hint>JSON object</mat-hint>\r\n <mat-error\r\n *ngIf=\"$any(vars).errors?.required && (vars.dirty || vars.touched)\"\r\n >vars required</mat-error\r\n >\r\n <mat-error *ngIf=\"$any(vars).errors?.json && (vars.dirty || vars.touched)\"\r\n >invalid JSON</mat-error\r\n >\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- vars2 only when type is fromTo -->\r\n @if (type.value === 'fromTo') {\r\n <div>\r\n <mat-form-field class=\"long-text\">\r\n <mat-label>2nd vars</mat-label>\r\n <textarea matInput [formControl]=\"vars2\"></textarea>\r\n <mat-hint>JSON object</mat-hint>\r\n <mat-error\r\n *ngIf=\"$any(vars2).errors?.required && (vars2.dirty || vars2.touched)\"\r\n >vars required</mat-error\r\n >\r\n <mat-error\r\n *ngIf=\"$any(vars2).errors?.json && (vars2.dirty || vars2.touched)\"\r\n >invalid JSON</mat-error\r\n >\r\n </mat-form-field>\r\n </div>\r\n }\r\n\r\n <div class=\"form-row\">\r\n <button\r\n type=\"button\"\r\n class=\"mat-warn\"\r\n mat-icon-button\r\n matTooltip=\"Close tween\"\r\n (click)=\"close()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n <button\r\n type=\"submit\"\r\n class=\"mat-primary\"\r\n mat-icon-button\r\n matTooltip=\"Save tween\"\r\n [disabled]=\"form.invalid || form.pristine\"\r\n >\r\n <mat-icon class=\"mat-primary\">check_circle</mat-icon>\r\n </button>\r\n </div>\r\n</form>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}.long-text{width:100%;max-width:800px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "directive", type: i5.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i5.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i5.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i8.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i9.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i10.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] }); }
186
189
  }
187
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: AnimationTweenComponent, decorators: [{
190
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: AnimationTweenComponent, decorators: [{
188
191
  type: Component,
189
192
  args: [{ selector: 'gve-animation-tween', imports: [
190
193
  CommonModule,
@@ -197,16 +200,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImpor
197
200
  MatSelectModule,
198
201
  MatTabsModule,
199
202
  MatTooltipModule,
200
- ], template: "<form [formGroup]=\"form\" (submit)=\"save()\">\r\n <div class=\"form-row\">\r\n <!-- label -->\r\n <mat-form-field>\r\n <mat-label>label</mat-label>\r\n <input matInput [formControl]=\"label\" />\r\n <mat-error\r\n *ngIf=\"$any(label).errors?.required && (label.dirty || label.touched)\"\r\n >label required</mat-error\r\n >\r\n <mat-error\r\n *ngIf=\"$any(label).errors?.maxLength && (label.dirty || label.touched)\"\r\n >label too long</mat-error\r\n >\r\n </mat-form-field>\r\n\r\n <!-- type -->\r\n <mat-form-field>\r\n <mat-label>type</mat-label>\r\n <mat-select [formControl]=\"type\">\r\n <mat-option *ngFor=\"let t of types\" [value]=\"t\">{{ t }}</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n\r\n <!-- position -->\r\n <mat-form-field>\r\n <mat-label>position</mat-label>\r\n <input matInput [formControl]=\"position\" />\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matSuffix\r\n [attr.aria-label]=\"'Information about position'\"\r\n (click)=\"\r\n openUrl(\r\n 'https://gsap.com/docs/v3/GSAP/Timeline/#positioning-animations-in-a-timeline'\r\n )\r\n \"\r\n >\r\n <mat-icon>info</mat-icon>\r\n </button>\r\n <mat-hint>time label &lt; &gt; += -=</mat-hint>\r\n <mat-error\r\n *ngIf=\"\r\n $any(position).errors?.maxLength &&\r\n (position.dirty || position.touched)\r\n \"\r\n >position too long</mat-error\r\n >\r\n </mat-form-field>\r\n </div>\r\n\r\n <div class=\"form-row\">\r\n <!-- selector -->\r\n <mat-form-field>\r\n <mat-label>selector</mat-label>\r\n <input matInput [formControl]=\"selector\" />\r\n <mat-hint>CSS selector</mat-hint>\r\n <mat-error\r\n *ngIf=\"\r\n $any(selector).errors?.required &&\r\n (selector.dirty || selector.touched)\r\n \"\r\n >selector required</mat-error\r\n >\r\n <mat-error\r\n *ngIf=\"\r\n $any(selector).errors?.maxLength &&\r\n (selector.dirty || selector.touched)\r\n \"\r\n >selector too long</mat-error\r\n >\r\n </mat-form-field>\r\n\r\n <!-- element id -->\r\n @if (elementIds?.length) {\r\n <mat-form-field>\r\n <mat-label>element ID</mat-label>\r\n <mat-select [formControl]=\"elementId\">\r\n <mat-option *ngFor=\"let id of elementIds\" [value]=\"id\">{{\r\n id\r\n }}</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n }\r\n </div>\r\n\r\n <!-- note -->\r\n <div>\r\n <mat-form-field class=\"long-text\">\r\n <mat-label>note</mat-label>\r\n <textarea rows=\"2\" matInput [formControl]=\"note\"></textarea>\r\n <mat-error\r\n *ngIf=\"$any(note).errors?.maxLength && (note.dirty || note.touched)\"\r\n >note too long</mat-error\r\n >\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- vars -->\r\n <div>\r\n <mat-form-field class=\"long-text\">\r\n <mat-label>vars</mat-label>\r\n <textarea matInput [formControl]=\"vars\"></textarea>\r\n <mat-hint>JSON object</mat-hint>\r\n <mat-error\r\n *ngIf=\"$any(vars).errors?.required && (vars.dirty || vars.touched)\"\r\n >vars required</mat-error\r\n >\r\n <mat-error *ngIf=\"$any(vars).errors?.json && (vars.dirty || vars.touched)\"\r\n >invalid JSON</mat-error\r\n >\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- vars2 only when type is fromTo -->\r\n @if (type.value === 'fromTo') {\r\n <div>\r\n <mat-form-field class=\"long-text\">\r\n <mat-label>2nd vars</mat-label>\r\n <textarea matInput [formControl]=\"vars2\"></textarea>\r\n <mat-hint>JSON object</mat-hint>\r\n <mat-error\r\n *ngIf=\"$any(vars2).errors?.required && (vars2.dirty || vars2.touched)\"\r\n >vars required</mat-error\r\n >\r\n <mat-error\r\n *ngIf=\"$any(vars2).errors?.json && (vars2.dirty || vars2.touched)\"\r\n >invalid JSON</mat-error\r\n >\r\n </mat-form-field>\r\n </div>\r\n }\r\n\r\n <div class=\"form-row\">\r\n <button\r\n type=\"button\"\r\n class=\"mat-warn\"\r\n mat-icon-button\r\n matTooltip=\"Close tween\"\r\n (click)=\"close()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n <button\r\n type=\"submit\"\r\n class=\"mat-primary\"\r\n mat-icon-button\r\n matTooltip=\"Save tween\"\r\n [disabled]=\"form.invalid || form.pristine\"\r\n >\r\n <mat-icon class=\"mat-primary\">check_circle</mat-icon>\r\n </button>\r\n </div>\r\n</form>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}.long-text{width:100%;max-width:800px}\n"] }]
201
- }], ctorParameters: () => [{ type: i1.FormBuilder }], propDecorators: { tween: [{
202
- type: Input
203
- }], elementIds: [{
204
- type: Input
205
- }], tweenChange: [{
206
- type: Output
207
- }], tweenCancel: [{
208
- type: Output
209
- }] } });
203
+ ], template: "<form [formGroup]=\"form\" (submit)=\"save()\">\r\n <div class=\"form-row\">\r\n <!-- label -->\r\n <mat-form-field>\r\n <mat-label>label</mat-label>\r\n <input matInput [formControl]=\"label\" />\r\n <mat-error\r\n *ngIf=\"$any(label).errors?.required && (label.dirty || label.touched)\"\r\n >label required</mat-error\r\n >\r\n <mat-error\r\n *ngIf=\"$any(label).errors?.maxLength && (label.dirty || label.touched)\"\r\n >label too long</mat-error\r\n >\r\n </mat-form-field>\r\n\r\n <!-- type -->\r\n <mat-form-field>\r\n <mat-label>type</mat-label>\r\n <mat-select [formControl]=\"type\">\r\n <mat-option *ngFor=\"let t of types\" [value]=\"t\">{{ t }}</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n\r\n <!-- position -->\r\n <mat-form-field>\r\n <mat-label>position</mat-label>\r\n <input matInput [formControl]=\"position\" />\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matSuffix\r\n [attr.aria-label]=\"'Information about position'\"\r\n (click)=\"\r\n openUrl(\r\n 'https://gsap.com/docs/v3/GSAP/Timeline/#positioning-animations-in-a-timeline'\r\n )\r\n \"\r\n >\r\n <mat-icon>info</mat-icon>\r\n </button>\r\n <mat-hint>time label &lt; &gt; += -=</mat-hint>\r\n <mat-error\r\n *ngIf=\"\r\n $any(position).errors?.maxLength &&\r\n (position.dirty || position.touched)\r\n \"\r\n >position too long</mat-error\r\n >\r\n </mat-form-field>\r\n </div>\r\n\r\n <div class=\"form-row\">\r\n <!-- selector -->\r\n <mat-form-field>\r\n <mat-label>selector</mat-label>\r\n <input matInput [formControl]=\"selector\" />\r\n <mat-hint>CSS selector</mat-hint>\r\n <mat-error\r\n *ngIf=\"\r\n $any(selector).errors?.required &&\r\n (selector.dirty || selector.touched)\r\n \"\r\n >selector required</mat-error\r\n >\r\n <mat-error\r\n *ngIf=\"\r\n $any(selector).errors?.maxLength &&\r\n (selector.dirty || selector.touched)\r\n \"\r\n >selector too long</mat-error\r\n >\r\n </mat-form-field>\r\n\r\n <!-- element id -->\r\n @if ((elementIds())?.length) {\r\n <mat-form-field>\r\n <mat-label>element ID</mat-label>\r\n <mat-select [formControl]=\"elementId\">\r\n <mat-option *ngFor=\"let id of elementIds()\" [value]=\"id\">{{\r\n id\r\n }}</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n }\r\n </div>\r\n\r\n <!-- note -->\r\n <div>\r\n <mat-form-field class=\"long-text\">\r\n <mat-label>note</mat-label>\r\n <textarea rows=\"2\" matInput [formControl]=\"note\"></textarea>\r\n <mat-error\r\n *ngIf=\"$any(note).errors?.maxLength && (note.dirty || note.touched)\"\r\n >note too long</mat-error\r\n >\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- vars -->\r\n <div>\r\n <mat-form-field class=\"long-text\">\r\n <mat-label>vars</mat-label>\r\n <textarea matInput [formControl]=\"vars\"></textarea>\r\n <mat-hint>JSON object</mat-hint>\r\n <mat-error\r\n *ngIf=\"$any(vars).errors?.required && (vars.dirty || vars.touched)\"\r\n >vars required</mat-error\r\n >\r\n <mat-error *ngIf=\"$any(vars).errors?.json && (vars.dirty || vars.touched)\"\r\n >invalid JSON</mat-error\r\n >\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- vars2 only when type is fromTo -->\r\n @if (type.value === 'fromTo') {\r\n <div>\r\n <mat-form-field class=\"long-text\">\r\n <mat-label>2nd vars</mat-label>\r\n <textarea matInput [formControl]=\"vars2\"></textarea>\r\n <mat-hint>JSON object</mat-hint>\r\n <mat-error\r\n *ngIf=\"$any(vars2).errors?.required && (vars2.dirty || vars2.touched)\"\r\n >vars required</mat-error\r\n >\r\n <mat-error\r\n *ngIf=\"$any(vars2).errors?.json && (vars2.dirty || vars2.touched)\"\r\n >invalid JSON</mat-error\r\n >\r\n </mat-form-field>\r\n </div>\r\n }\r\n\r\n <div class=\"form-row\">\r\n <button\r\n type=\"button\"\r\n class=\"mat-warn\"\r\n mat-icon-button\r\n matTooltip=\"Close tween\"\r\n (click)=\"close()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n <button\r\n type=\"submit\"\r\n class=\"mat-primary\"\r\n mat-icon-button\r\n matTooltip=\"Save tween\"\r\n [disabled]=\"form.invalid || form.pristine\"\r\n >\r\n <mat-icon class=\"mat-primary\">check_circle</mat-icon>\r\n </button>\r\n </div>\r\n</form>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}.long-text{width:100%;max-width:800px}\n"] }]
204
+ }], ctorParameters: () => [{ type: i1.FormBuilder }] });
210
205
 
211
206
  /**
212
207
  * 🔑 `gve-animation-timeline`
@@ -223,29 +218,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImpor
223
218
  * - 🔥 `timelineCancel` (`void`): emitted when the timeline editing is canceled.
224
219
  */
225
220
  class AnimationTimelineComponent {
226
- /**
227
- * The animation timeline to edit.
228
- */
229
- get timeline() {
230
- return this._timeline;
231
- }
232
- set timeline(value) {
233
- this._timeline = value || undefined;
234
- this.updateForm(this._timeline);
235
- }
236
221
  constructor(formBuilder) {
222
+ /**
223
+ * The animation timeline to edit.
224
+ */
225
+ this.timeline = model();
226
+ /**
227
+ * The IDs of the elements that can be selected by the tween.
228
+ * This list is used to allow the user to select an element from a dropdown.
229
+ */
230
+ this.elementIds = input();
237
231
  /**
238
232
  * The tags that can be used by the timeline.
239
233
  */
240
- this.tags = [];
234
+ this.tags = input([]);
241
235
  /**
242
236
  * Emitted when the timeline is changed.
243
237
  */
244
- this.timelineChange = new EventEmitter();
238
+ this.timelineChange = output();
245
239
  /**
246
240
  * Emitted when the timeline editing is canceled.
247
241
  */
248
- this.timelineCancel = new EventEmitter();
242
+ this.timelineCancel = output();
249
243
  this.editedTweenIndex = -1;
250
244
  this.tag = formBuilder.control('', {
251
245
  nonNullable: true,
@@ -253,7 +247,7 @@ class AnimationTimelineComponent {
253
247
  });
254
248
  this.tweens = formBuilder.control([], {
255
249
  nonNullable: true,
256
- validators: NgToolsValidators.strictMinLengthValidator(1),
250
+ validators: NgxToolsValidators.strictMinLengthValidator(1),
257
251
  });
258
252
  this.vars = formBuilder.control(null, this.jsonValidator);
259
253
  this.form = formBuilder.group({
@@ -261,6 +255,10 @@ class AnimationTimelineComponent {
261
255
  tweens: this.tweens,
262
256
  vars: this.vars,
263
257
  });
258
+ // when the timeline changes, update the form
259
+ effect(() => {
260
+ this.updateForm(this.timeline());
261
+ });
264
262
  }
265
263
  jsonValidator(control) {
266
264
  if (!control.value) {
@@ -354,14 +352,14 @@ class AnimationTimelineComponent {
354
352
  if (this.form.invalid) {
355
353
  return;
356
354
  }
357
- this._timeline = this.getTimeline();
358
- this.timelineChange.emit(this._timeline);
355
+ this.timeline.set(this.getTimeline());
356
+ this.timelineChange.emit(this.timeline());
359
357
  this.form.markAsPristine();
360
358
  }
361
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: AnimationTimelineComponent, deps: [{ token: i1.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); }
362
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.0", type: AnimationTimelineComponent, isStandalone: true, selector: "gve-animation-timeline", inputs: { timeline: "timeline", elementIds: "elementIds", tags: "tags" }, outputs: { timelineChange: "timelineChange", timelineCancel: "timelineCancel" }, ngImport: i0, template: "<form [formGroup]=\"form\" (submit)=\"save()\">\r\n <div class=\"form-row\">\r\n <!-- tag (bound) -->\r\n @if (tags.length) {\r\n <mat-form-field>\r\n <mat-label>tag</mat-label>\r\n <mat-select [formControl]=\"tag\">\r\n @for (t of tags; track t) {\r\n <mat-option [value]=\"t\">{{ t }}</mat-option>\r\n }\r\n </mat-select>\r\n <mat-error\r\n *ngIf=\"$any(tag).errors?.required && (tag.dirty || tag.touched)\"\r\n >tag required</mat-error\r\n >\r\n </mat-form-field>\r\n } @else {\r\n <!-- tag (free) -->\r\n <mat-form-field>\r\n <mat-label>tag</mat-label>\r\n <input matInput [formControl]=\"tag\" />\r\n <mat-error\r\n *ngIf=\"$any(tag).errors?.required && (tag.dirty || tag.touched)\"\r\n >tag required</mat-error\r\n >\r\n <mat-error\r\n *ngIf=\"$any(tag).errors?.maxLength && (tag.dirty || tag.touched)\"\r\n >tag too long</mat-error\r\n >\r\n </mat-form-field>\r\n }\r\n\r\n <button\r\n mat-flat-button\r\n type=\"button\"\r\n color=\"primary\"\r\n class=\"mat-primary\"\r\n (click)=\"addTween()\"\r\n >\r\n <mat-icon>add_circle</mat-icon> tween\r\n </button>\r\n </div>\r\n\r\n <!-- vars -->\r\n <div>\r\n <mat-form-field class=\"long-text\">\r\n <mat-label>vars</mat-label>\r\n <textarea matInput [formControl]=\"vars\"></textarea>\r\n <mat-hint>JSON object</mat-hint>\r\n <mat-error *ngIf=\"$any(vars).errors?.json && (vars.dirty || vars.touched)\"\r\n >invalid JSON</mat-error\r\n >\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- tweens -->\r\n <table>\r\n <thead>\r\n <tr>\r\n <th></th>\r\n <th>label</th>\r\n <th>type</th>\r\n <th>selector</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (t of tweens.value; track t; let index = $index) {\r\n <tr>\r\n <td class=\"fit-width\">\r\n <!-- edit -->\r\n <button\r\n mat-icon-button\r\n type=\"button\"\r\n color=\"primary\"\r\n (click)=\"editTween(index)\"\r\n matTooltip=\"Edit tween\"\r\n >\r\n <mat-icon class=\"mat-primary\">edit</mat-icon>\r\n </button>\r\n <!-- delete -->\r\n <button\r\n mat-icon-button\r\n type=\"button\"\r\n color=\"warn\"\r\n (click)=\"deleteTween(index)\"\r\n matTooltip=\"Delete tween\"\r\n >\r\n <mat-icon class=\"mat-warn\">remove_circle</mat-icon>\r\n </button>\r\n <!-- up -->\r\n <button\r\n mat-icon-button\r\n type=\"button\"\r\n (click)=\"moveTweenUp(index)\"\r\n matTooltip=\"Move tween up\"\r\n [disabled]=\"index === 0\"\r\n >\r\n <mat-icon>arrow_circle_up</mat-icon>\r\n </button>\r\n <!-- down -->\r\n <button\r\n mat-icon-button\r\n type=\"button\"\r\n (click)=\"moveTweenDown(index)\"\r\n matTooltip=\"Move tween down\"\r\n [disabled]=\"index === tweens.value.length - 1\"\r\n >\r\n <mat-icon>arrow_circle_down</mat-icon>\r\n </button>\r\n </td>\r\n <td>\r\n {{ t.label }}\r\n </td>\r\n <td>\r\n {{ t.type }}\r\n </td>\r\n <td>\r\n {{ t.selector }}\r\n </td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n\r\n <!-- tween editor -->\r\n <mat-expansion-panel [expanded]=\"editedTween\" [disabled]=\"!editedTween\">\r\n @if (editedTween) {\r\n <mat-expansion-panel-header>\r\n <mat-panel-title>tween {{ editedTween.label }}</mat-panel-title>\r\n </mat-expansion-panel-header>\r\n }\r\n <fieldset>\r\n <gve-animation-tween\r\n [elementIds]=\"elementIds\"\r\n [tween]=\"editedTween\"\r\n (tweenChange)=\"saveTween($event)\"\r\n (tweenCancel)=\"closeTween()\"\r\n />\r\n </fieldset>\r\n </mat-expansion-panel>\r\n\r\n <!-- buttons -->\r\n <div class=\"button-row\">\r\n <button\r\n type=\"button\"\r\n class=\"mat-warn\"\r\n mat-icon-button\r\n matTooltip=\"Close timeline\"\r\n (click)=\"close()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n <button\r\n type=\"submit\"\r\n class=\"mat-primary\"\r\n mat-flat-button\r\n matTooltip=\"Save timeline\"\r\n [disabled]=\"form.invalid\"\r\n >\r\n <mat-icon class=\"mat-primary\">check_circle</mat-icon>\r\n timeline\r\n </button>\r\n </div>\r\n</form>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}.button-row{display:flex;align-items:center;flex-wrap:wrap}.button-row *{flex:0 0 auto}.long-text{width:100%;max-width:800px}table{width:100%;border-collapse:collapse}th{color:#909090;font-weight:400;text-align:left;background-color:#e1e0e0}th,td{padding:4px;border-bottom:1px solid silver}tbody tr:nth-child(2n){background-color:#e8e8e8}td.fit-width{width:1px;white-space:nowrap}fieldset{border:1px solid silver;border-radius:6px;padding:8px;margin:8px 0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "component", type: i4.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i4.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i4.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "directive", type: i5.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i5.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i8.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i9.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i10.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: NgToolsModule }, { kind: "component", type: AnimationTweenComponent, selector: "gve-animation-tween", inputs: ["tween", "elementIds"], outputs: ["tweenChange", "tweenCancel"] }] }); }
359
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: AnimationTimelineComponent, deps: [{ token: i1.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); }
360
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.6", type: AnimationTimelineComponent, isStandalone: true, selector: "gve-animation-timeline", inputs: { timeline: { classPropertyName: "timeline", publicName: "timeline", isSignal: true, isRequired: false, transformFunction: null }, elementIds: { classPropertyName: "elementIds", publicName: "elementIds", isSignal: true, isRequired: false, transformFunction: null }, tags: { classPropertyName: "tags", publicName: "tags", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { timeline: "timelineChange", timelineChange: "timelineChange", timelineCancel: "timelineCancel" }, ngImport: i0, template: "<form [formGroup]=\"form\" (submit)=\"save()\">\r\n <div class=\"form-row\">\r\n <!-- tag (bound) -->\r\n @if (tags().length) {\r\n <mat-form-field>\r\n <mat-label>tag</mat-label>\r\n <mat-select [formControl]=\"tag\">\r\n @for (t of tags(); track t) {\r\n <mat-option [value]=\"t\">{{ t }}</mat-option>\r\n }\r\n </mat-select>\r\n <mat-error\r\n *ngIf=\"$any(tag).errors?.required && (tag.dirty || tag.touched)\"\r\n >tag required</mat-error\r\n >\r\n </mat-form-field>\r\n } @else {\r\n <!-- tag (free) -->\r\n <mat-form-field>\r\n <mat-label>tag</mat-label>\r\n <input matInput [formControl]=\"tag\" />\r\n <mat-error\r\n *ngIf=\"$any(tag).errors?.required && (tag.dirty || tag.touched)\"\r\n >tag required</mat-error\r\n >\r\n <mat-error\r\n *ngIf=\"$any(tag).errors?.maxLength && (tag.dirty || tag.touched)\"\r\n >tag too long</mat-error\r\n >\r\n </mat-form-field>\r\n }\r\n\r\n <button\r\n mat-flat-button\r\n type=\"button\"\r\n color=\"primary\"\r\n class=\"mat-primary\"\r\n (click)=\"addTween()\"\r\n >\r\n <mat-icon>add_circle</mat-icon> tween\r\n </button>\r\n </div>\r\n\r\n <!-- vars -->\r\n <div>\r\n <mat-form-field class=\"long-text\">\r\n <mat-label>vars</mat-label>\r\n <textarea matInput [formControl]=\"vars\"></textarea>\r\n <mat-hint>JSON object</mat-hint>\r\n <mat-error *ngIf=\"$any(vars).errors?.json && (vars.dirty || vars.touched)\"\r\n >invalid JSON</mat-error\r\n >\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- tweens -->\r\n <table>\r\n <thead>\r\n <tr>\r\n <th></th>\r\n <th>label</th>\r\n <th>type</th>\r\n <th>selector</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (t of tweens.value; track t; let index = $index) {\r\n <tr>\r\n <td class=\"fit-width\">\r\n <!-- edit -->\r\n <button\r\n mat-icon-button\r\n type=\"button\"\r\n color=\"primary\"\r\n (click)=\"editTween(index)\"\r\n matTooltip=\"Edit tween\"\r\n >\r\n <mat-icon class=\"mat-primary\">edit</mat-icon>\r\n </button>\r\n <!-- delete -->\r\n <button\r\n mat-icon-button\r\n type=\"button\"\r\n color=\"warn\"\r\n (click)=\"deleteTween(index)\"\r\n matTooltip=\"Delete tween\"\r\n >\r\n <mat-icon class=\"mat-warn\">remove_circle</mat-icon>\r\n </button>\r\n <!-- up -->\r\n <button\r\n mat-icon-button\r\n type=\"button\"\r\n (click)=\"moveTweenUp(index)\"\r\n matTooltip=\"Move tween up\"\r\n [disabled]=\"index === 0\"\r\n >\r\n <mat-icon>arrow_circle_up</mat-icon>\r\n </button>\r\n <!-- down -->\r\n <button\r\n mat-icon-button\r\n type=\"button\"\r\n (click)=\"moveTweenDown(index)\"\r\n matTooltip=\"Move tween down\"\r\n [disabled]=\"index === tweens.value.length - 1\"\r\n >\r\n <mat-icon>arrow_circle_down</mat-icon>\r\n </button>\r\n </td>\r\n <td>\r\n {{ t.label }}\r\n </td>\r\n <td>\r\n {{ t.type }}\r\n </td>\r\n <td>\r\n {{ t.selector }}\r\n </td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n\r\n <!-- tween editor -->\r\n <mat-expansion-panel [expanded]=\"editedTween\" [disabled]=\"!editedTween\">\r\n @if (editedTween) {\r\n <mat-expansion-panel-header>\r\n <mat-panel-title>tween {{ editedTween.label }}</mat-panel-title>\r\n </mat-expansion-panel-header>\r\n }\r\n <fieldset>\r\n <gve-animation-tween\r\n [elementIds]=\"elementIds()\"\r\n [tween]=\"editedTween\"\r\n (tweenChange)=\"saveTween($event)\"\r\n (tweenCancel)=\"closeTween()\"\r\n />\r\n </fieldset>\r\n </mat-expansion-panel>\r\n\r\n <!-- buttons -->\r\n <div class=\"button-row\">\r\n <button\r\n type=\"button\"\r\n class=\"mat-warn\"\r\n mat-icon-button\r\n matTooltip=\"Close timeline\"\r\n (click)=\"close()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n <button\r\n type=\"submit\"\r\n class=\"mat-primary\"\r\n mat-flat-button\r\n matTooltip=\"Save timeline\"\r\n [disabled]=\"form.invalid\"\r\n >\r\n <mat-icon class=\"mat-primary\">check_circle</mat-icon>\r\n timeline\r\n </button>\r\n </div>\r\n</form>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}.button-row{display:flex;align-items:center;flex-wrap:wrap}.button-row *{flex:0 0 auto}.long-text{width:100%;max-width:800px}table{width:100%;border-collapse:collapse}th{color:#909090;font-weight:400;text-align:left;background-color:#e1e0e0}th,td{padding:4px;border-bottom:1px solid silver}tbody tr:nth-child(2n){background-color:#e8e8e8}td.fit-width{width:1px;white-space:nowrap}fieldset{border:1px solid silver;border-radius:6px;padding:8px;margin:8px 0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "component", type: i4.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i4.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i4.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "directive", type: i5.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i5.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i8.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i9.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i10.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: AnimationTweenComponent, selector: "gve-animation-tween", inputs: ["tween", "elementIds"], outputs: ["tweenChange", "tweenCancel"] }] }); }
363
361
  }
364
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: AnimationTimelineComponent, decorators: [{
362
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: AnimationTimelineComponent, decorators: [{
365
363
  type: Component,
366
364
  args: [{ selector: 'gve-animation-timeline', imports: [
367
365
  CommonModule,
@@ -375,20 +373,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImpor
375
373
  MatSelectModule,
376
374
  MatTabsModule,
377
375
  MatTooltipModule,
378
- NgToolsModule,
379
376
  AnimationTweenComponent,
380
- ], template: "<form [formGroup]=\"form\" (submit)=\"save()\">\r\n <div class=\"form-row\">\r\n <!-- tag (bound) -->\r\n @if (tags.length) {\r\n <mat-form-field>\r\n <mat-label>tag</mat-label>\r\n <mat-select [formControl]=\"tag\">\r\n @for (t of tags; track t) {\r\n <mat-option [value]=\"t\">{{ t }}</mat-option>\r\n }\r\n </mat-select>\r\n <mat-error\r\n *ngIf=\"$any(tag).errors?.required && (tag.dirty || tag.touched)\"\r\n >tag required</mat-error\r\n >\r\n </mat-form-field>\r\n } @else {\r\n <!-- tag (free) -->\r\n <mat-form-field>\r\n <mat-label>tag</mat-label>\r\n <input matInput [formControl]=\"tag\" />\r\n <mat-error\r\n *ngIf=\"$any(tag).errors?.required && (tag.dirty || tag.touched)\"\r\n >tag required</mat-error\r\n >\r\n <mat-error\r\n *ngIf=\"$any(tag).errors?.maxLength && (tag.dirty || tag.touched)\"\r\n >tag too long</mat-error\r\n >\r\n </mat-form-field>\r\n }\r\n\r\n <button\r\n mat-flat-button\r\n type=\"button\"\r\n color=\"primary\"\r\n class=\"mat-primary\"\r\n (click)=\"addTween()\"\r\n >\r\n <mat-icon>add_circle</mat-icon> tween\r\n </button>\r\n </div>\r\n\r\n <!-- vars -->\r\n <div>\r\n <mat-form-field class=\"long-text\">\r\n <mat-label>vars</mat-label>\r\n <textarea matInput [formControl]=\"vars\"></textarea>\r\n <mat-hint>JSON object</mat-hint>\r\n <mat-error *ngIf=\"$any(vars).errors?.json && (vars.dirty || vars.touched)\"\r\n >invalid JSON</mat-error\r\n >\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- tweens -->\r\n <table>\r\n <thead>\r\n <tr>\r\n <th></th>\r\n <th>label</th>\r\n <th>type</th>\r\n <th>selector</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (t of tweens.value; track t; let index = $index) {\r\n <tr>\r\n <td class=\"fit-width\">\r\n <!-- edit -->\r\n <button\r\n mat-icon-button\r\n type=\"button\"\r\n color=\"primary\"\r\n (click)=\"editTween(index)\"\r\n matTooltip=\"Edit tween\"\r\n >\r\n <mat-icon class=\"mat-primary\">edit</mat-icon>\r\n </button>\r\n <!-- delete -->\r\n <button\r\n mat-icon-button\r\n type=\"button\"\r\n color=\"warn\"\r\n (click)=\"deleteTween(index)\"\r\n matTooltip=\"Delete tween\"\r\n >\r\n <mat-icon class=\"mat-warn\">remove_circle</mat-icon>\r\n </button>\r\n <!-- up -->\r\n <button\r\n mat-icon-button\r\n type=\"button\"\r\n (click)=\"moveTweenUp(index)\"\r\n matTooltip=\"Move tween up\"\r\n [disabled]=\"index === 0\"\r\n >\r\n <mat-icon>arrow_circle_up</mat-icon>\r\n </button>\r\n <!-- down -->\r\n <button\r\n mat-icon-button\r\n type=\"button\"\r\n (click)=\"moveTweenDown(index)\"\r\n matTooltip=\"Move tween down\"\r\n [disabled]=\"index === tweens.value.length - 1\"\r\n >\r\n <mat-icon>arrow_circle_down</mat-icon>\r\n </button>\r\n </td>\r\n <td>\r\n {{ t.label }}\r\n </td>\r\n <td>\r\n {{ t.type }}\r\n </td>\r\n <td>\r\n {{ t.selector }}\r\n </td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n\r\n <!-- tween editor -->\r\n <mat-expansion-panel [expanded]=\"editedTween\" [disabled]=\"!editedTween\">\r\n @if (editedTween) {\r\n <mat-expansion-panel-header>\r\n <mat-panel-title>tween {{ editedTween.label }}</mat-panel-title>\r\n </mat-expansion-panel-header>\r\n }\r\n <fieldset>\r\n <gve-animation-tween\r\n [elementIds]=\"elementIds\"\r\n [tween]=\"editedTween\"\r\n (tweenChange)=\"saveTween($event)\"\r\n (tweenCancel)=\"closeTween()\"\r\n />\r\n </fieldset>\r\n </mat-expansion-panel>\r\n\r\n <!-- buttons -->\r\n <div class=\"button-row\">\r\n <button\r\n type=\"button\"\r\n class=\"mat-warn\"\r\n mat-icon-button\r\n matTooltip=\"Close timeline\"\r\n (click)=\"close()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n <button\r\n type=\"submit\"\r\n class=\"mat-primary\"\r\n mat-flat-button\r\n matTooltip=\"Save timeline\"\r\n [disabled]=\"form.invalid\"\r\n >\r\n <mat-icon class=\"mat-primary\">check_circle</mat-icon>\r\n timeline\r\n </button>\r\n </div>\r\n</form>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}.button-row{display:flex;align-items:center;flex-wrap:wrap}.button-row *{flex:0 0 auto}.long-text{width:100%;max-width:800px}table{width:100%;border-collapse:collapse}th{color:#909090;font-weight:400;text-align:left;background-color:#e1e0e0}th,td{padding:4px;border-bottom:1px solid silver}tbody tr:nth-child(2n){background-color:#e8e8e8}td.fit-width{width:1px;white-space:nowrap}fieldset{border:1px solid silver;border-radius:6px;padding:8px;margin:8px 0}\n"] }]
381
- }], ctorParameters: () => [{ type: i1.FormBuilder }], propDecorators: { timeline: [{
382
- type: Input
383
- }], elementIds: [{
384
- type: Input
385
- }], tags: [{
386
- type: Input
387
- }], timelineChange: [{
388
- type: Output
389
- }], timelineCancel: [{
390
- type: Output
391
- }] } });
377
+ ], template: "<form [formGroup]=\"form\" (submit)=\"save()\">\r\n <div class=\"form-row\">\r\n <!-- tag (bound) -->\r\n @if (tags().length) {\r\n <mat-form-field>\r\n <mat-label>tag</mat-label>\r\n <mat-select [formControl]=\"tag\">\r\n @for (t of tags(); track t) {\r\n <mat-option [value]=\"t\">{{ t }}</mat-option>\r\n }\r\n </mat-select>\r\n <mat-error\r\n *ngIf=\"$any(tag).errors?.required && (tag.dirty || tag.touched)\"\r\n >tag required</mat-error\r\n >\r\n </mat-form-field>\r\n } @else {\r\n <!-- tag (free) -->\r\n <mat-form-field>\r\n <mat-label>tag</mat-label>\r\n <input matInput [formControl]=\"tag\" />\r\n <mat-error\r\n *ngIf=\"$any(tag).errors?.required && (tag.dirty || tag.touched)\"\r\n >tag required</mat-error\r\n >\r\n <mat-error\r\n *ngIf=\"$any(tag).errors?.maxLength && (tag.dirty || tag.touched)\"\r\n >tag too long</mat-error\r\n >\r\n </mat-form-field>\r\n }\r\n\r\n <button\r\n mat-flat-button\r\n type=\"button\"\r\n color=\"primary\"\r\n class=\"mat-primary\"\r\n (click)=\"addTween()\"\r\n >\r\n <mat-icon>add_circle</mat-icon> tween\r\n </button>\r\n </div>\r\n\r\n <!-- vars -->\r\n <div>\r\n <mat-form-field class=\"long-text\">\r\n <mat-label>vars</mat-label>\r\n <textarea matInput [formControl]=\"vars\"></textarea>\r\n <mat-hint>JSON object</mat-hint>\r\n <mat-error *ngIf=\"$any(vars).errors?.json && (vars.dirty || vars.touched)\"\r\n >invalid JSON</mat-error\r\n >\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- tweens -->\r\n <table>\r\n <thead>\r\n <tr>\r\n <th></th>\r\n <th>label</th>\r\n <th>type</th>\r\n <th>selector</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (t of tweens.value; track t; let index = $index) {\r\n <tr>\r\n <td class=\"fit-width\">\r\n <!-- edit -->\r\n <button\r\n mat-icon-button\r\n type=\"button\"\r\n color=\"primary\"\r\n (click)=\"editTween(index)\"\r\n matTooltip=\"Edit tween\"\r\n >\r\n <mat-icon class=\"mat-primary\">edit</mat-icon>\r\n </button>\r\n <!-- delete -->\r\n <button\r\n mat-icon-button\r\n type=\"button\"\r\n color=\"warn\"\r\n (click)=\"deleteTween(index)\"\r\n matTooltip=\"Delete tween\"\r\n >\r\n <mat-icon class=\"mat-warn\">remove_circle</mat-icon>\r\n </button>\r\n <!-- up -->\r\n <button\r\n mat-icon-button\r\n type=\"button\"\r\n (click)=\"moveTweenUp(index)\"\r\n matTooltip=\"Move tween up\"\r\n [disabled]=\"index === 0\"\r\n >\r\n <mat-icon>arrow_circle_up</mat-icon>\r\n </button>\r\n <!-- down -->\r\n <button\r\n mat-icon-button\r\n type=\"button\"\r\n (click)=\"moveTweenDown(index)\"\r\n matTooltip=\"Move tween down\"\r\n [disabled]=\"index === tweens.value.length - 1\"\r\n >\r\n <mat-icon>arrow_circle_down</mat-icon>\r\n </button>\r\n </td>\r\n <td>\r\n {{ t.label }}\r\n </td>\r\n <td>\r\n {{ t.type }}\r\n </td>\r\n <td>\r\n {{ t.selector }}\r\n </td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n\r\n <!-- tween editor -->\r\n <mat-expansion-panel [expanded]=\"editedTween\" [disabled]=\"!editedTween\">\r\n @if (editedTween) {\r\n <mat-expansion-panel-header>\r\n <mat-panel-title>tween {{ editedTween.label }}</mat-panel-title>\r\n </mat-expansion-panel-header>\r\n }\r\n <fieldset>\r\n <gve-animation-tween\r\n [elementIds]=\"elementIds()\"\r\n [tween]=\"editedTween\"\r\n (tweenChange)=\"saveTween($event)\"\r\n (tweenCancel)=\"closeTween()\"\r\n />\r\n </fieldset>\r\n </mat-expansion-panel>\r\n\r\n <!-- buttons -->\r\n <div class=\"button-row\">\r\n <button\r\n type=\"button\"\r\n class=\"mat-warn\"\r\n mat-icon-button\r\n matTooltip=\"Close timeline\"\r\n (click)=\"close()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n <button\r\n type=\"submit\"\r\n class=\"mat-primary\"\r\n mat-flat-button\r\n matTooltip=\"Save timeline\"\r\n [disabled]=\"form.invalid\"\r\n >\r\n <mat-icon class=\"mat-primary\">check_circle</mat-icon>\r\n timeline\r\n </button>\r\n </div>\r\n</form>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}.button-row{display:flex;align-items:center;flex-wrap:wrap}.button-row *{flex:0 0 auto}.long-text{width:100%;max-width:800px}table{width:100%;border-collapse:collapse}th{color:#909090;font-weight:400;text-align:left;background-color:#e1e0e0}th,td{padding:4px;border-bottom:1px solid silver}tbody tr:nth-child(2n){background-color:#e8e8e8}td.fit-width{width:1px;white-space:nowrap}fieldset{border:1px solid silver;border-radius:6px;padding:8px;margin:8px 0}\n"] }]
378
+ }], ctorParameters: () => [{ type: i1.FormBuilder }] });
392
379
 
393
380
  /**
394
381
  * 🔑 `gve-base-text-char`
@@ -401,34 +388,25 @@ class BaseTextCharComponent {
401
388
  this.defaultColor = '#D8D8D8';
402
389
  this.defaultBorderColor = '#D8D8D8';
403
390
  this.defaultEmSize = 1.5;
391
+ /**
392
+ * The character to display.
393
+ */
394
+ this.char = input();
404
395
  /**
405
396
  * Emitted when the character is clicked.
406
397
  */
407
- this.charPick = new EventEmitter();
408
- }
409
- /**
410
- * The character to display.
411
- */
412
- get char() {
413
- return this._char;
414
- }
415
- set char(value) {
416
- this._char = value || undefined;
398
+ this.charPick = output();
417
399
  }
418
400
  onCharClick(event) {
419
- this.charPick.emit({ char: this._char, event });
401
+ this.charPick.emit({ char: this.char(), event });
420
402
  }
421
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: BaseTextCharComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
422
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.0", type: BaseTextCharComponent, isStandalone: true, selector: "gve-base-text-char", inputs: { char: "char" }, outputs: { charPick: "charPick" }, ngImport: i0, template: "@if (char) {\r\n<div matRipple id=\"container\" (click)=\"onCharClick($event)\">\r\n <div\r\n id=\"c-label\"\r\n [style.fontSize]=\"char.emSize + 'em'\"\r\n [style.borderColor]=\"char.borderColor\"\r\n >\r\n {{ char.label }}\r\n </div>\r\n <div\r\n id=\"c-id\"\r\n [style.fontSize]=\"char.emSize / 2 + 'em'\"\r\n [style.color]=\"char.color | colorToContrast\"\r\n [style.borderColor]=\"char.borderColor\"\r\n [style.backgroundColor]=\"char.color\"\r\n >\r\n {{ char.id }}\r\n </div>\r\n</div>\r\n}\r\n", styles: ["div#container{cursor:pointer;flex-direction:column;align-items:center}div#c-label{border:1px solid silver;border-radius:6px;padding:6px;height:1.5em;align-items:center;justify-content:center;text-align:center}div#c-id{margin-top:4px;margin-bottom:16px;border:1px solid silver;border-radius:6px;padding:6px;align-items:center;justify-content:center;text-align:center}\n"], dependencies: [{ kind: "ngmodule", type: MatRippleModule }, { kind: "directive", type: i9.MatRipple, selector: "[mat-ripple], [matRipple]", inputs: ["matRippleColor", "matRippleUnbounded", "matRippleCentered", "matRippleRadius", "matRippleAnimation", "matRippleDisabled", "matRippleTrigger"], exportAs: ["matRipple"] }, { kind: "ngmodule", type: NgToolsModule }, { kind: "pipe", type: i2$1.ColorToContrastPipe, name: "colorToContrast" }] }); }
403
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: BaseTextCharComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
404
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.6", type: BaseTextCharComponent, isStandalone: true, selector: "gve-base-text-char", inputs: { char: { classPropertyName: "char", publicName: "char", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { charPick: "charPick" }, ngImport: i0, template: "@if (char()) {\r\n<div matRipple id=\"container\" (click)=\"onCharClick($event)\">\r\n <div\r\n id=\"c-label\"\r\n [style.fontSize]=\"char()!.emSize + 'em'\"\r\n [style.borderColor]=\"char()!.borderColor\"\r\n >\r\n {{ char()!.label }}\r\n </div>\r\n <div\r\n id=\"c-id\"\r\n [style.fontSize]=\"char()!.emSize / 2 + 'em'\"\r\n [style.color]=\"char()!.color | colorToContrast\"\r\n [style.borderColor]=\"char()!.borderColor\"\r\n [style.backgroundColor]=\"char()!.color\"\r\n >\r\n {{ char()!.id }}\r\n </div>\r\n</div>\r\n}\r\n", styles: ["div#container{cursor:pointer;flex-direction:column;align-items:center}div#c-label{border:1px solid silver;border-radius:6px;padding:6px;height:1.5em;align-items:center;justify-content:center;text-align:center}div#c-id{margin-top:4px;margin-bottom:16px;border:1px solid silver;border-radius:6px;padding:6px;align-items:center;justify-content:center;text-align:center}\n"], dependencies: [{ kind: "ngmodule", type: MatRippleModule }, { kind: "directive", type: i9.MatRipple, selector: "[mat-ripple], [matRipple]", inputs: ["matRippleColor", "matRippleUnbounded", "matRippleCentered", "matRippleRadius", "matRippleAnimation", "matRippleDisabled", "matRippleTrigger"], exportAs: ["matRipple"] }, { kind: "pipe", type: ColorToContrastPipe, name: "colorToContrast" }] }); }
423
405
  }
424
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: BaseTextCharComponent, decorators: [{
406
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: BaseTextCharComponent, decorators: [{
425
407
  type: Component,
426
- args: [{ selector: 'gve-base-text-char', imports: [MatRippleModule, NgToolsModule], template: "@if (char) {\r\n<div matRipple id=\"container\" (click)=\"onCharClick($event)\">\r\n <div\r\n id=\"c-label\"\r\n [style.fontSize]=\"char.emSize + 'em'\"\r\n [style.borderColor]=\"char.borderColor\"\r\n >\r\n {{ char.label }}\r\n </div>\r\n <div\r\n id=\"c-id\"\r\n [style.fontSize]=\"char.emSize / 2 + 'em'\"\r\n [style.color]=\"char.color | colorToContrast\"\r\n [style.borderColor]=\"char.borderColor\"\r\n [style.backgroundColor]=\"char.color\"\r\n >\r\n {{ char.id }}\r\n </div>\r\n</div>\r\n}\r\n", styles: ["div#container{cursor:pointer;flex-direction:column;align-items:center}div#c-label{border:1px solid silver;border-radius:6px;padding:6px;height:1.5em;align-items:center;justify-content:center;text-align:center}div#c-id{margin-top:4px;margin-bottom:16px;border:1px solid silver;border-radius:6px;padding:6px;align-items:center;justify-content:center;text-align:center}\n"] }]
427
- }], propDecorators: { char: [{
428
- type: Input
429
- }], charPick: [{
430
- type: Output
431
- }] } });
408
+ args: [{ selector: 'gve-base-text-char', imports: [MatRippleModule, ColorToContrastPipe], template: "@if (char()) {\r\n<div matRipple id=\"container\" (click)=\"onCharClick($event)\">\r\n <div\r\n id=\"c-label\"\r\n [style.fontSize]=\"char()!.emSize + 'em'\"\r\n [style.borderColor]=\"char()!.borderColor\"\r\n >\r\n {{ char()!.label }}\r\n </div>\r\n <div\r\n id=\"c-id\"\r\n [style.fontSize]=\"char()!.emSize / 2 + 'em'\"\r\n [style.color]=\"char()!.color | colorToContrast\"\r\n [style.borderColor]=\"char()!.borderColor\"\r\n [style.backgroundColor]=\"char()!.color\"\r\n >\r\n {{ char()!.id }}\r\n </div>\r\n</div>\r\n}\r\n", styles: ["div#container{cursor:pointer;flex-direction:column;align-items:center}div#c-label{border:1px solid silver;border-radius:6px;padding:6px;height:1.5em;align-items:center;justify-content:center;text-align:center}div#c-id{margin-top:4px;margin-bottom:16px;border:1px solid silver;border-radius:6px;padding:6px;align-items:center;justify-content:center;text-align:center}\n"] }]
409
+ }] });
432
410
 
433
411
  /**
434
412
  * 🔑 `gve-base-text-view`
@@ -449,79 +427,66 @@ class BaseTextViewComponent {
449
427
  /**
450
428
  * The default color for the text.
451
429
  */
452
- this.defaultColor = '#DBDBDB';
430
+ this.defaultColor = input('#DBDBDB');
453
431
  /**
454
432
  * The default border color for the text.
455
433
  */
456
- this.defaultBorderColor = '#DBDBDB';
434
+ this.defaultBorderColor = input('#DBDBDB');
457
435
  /**
458
436
  * The color for the selected text.
459
437
  */
460
- this.selectionColor = '#3E92CC';
438
+ this.selectionColor = input('#3E92CC');
461
439
  /**
462
440
  * True if line numbers should be displayed next to each line.
463
441
  */
464
- this.hasLineNumber = false;
442
+ this.hasLineNumber = input(false);
443
+ /**
444
+ * The text to display.
445
+ */
446
+ this.text = input([]);
447
+ /**
448
+ * An optional callback to get the color of a character node.
449
+ * Return null to use the default color.
450
+ */
451
+ this.borderColorCallback = input();
465
452
  /**
466
453
  * Emitted when a character is picked.
467
454
  */
468
- this.charPick = new EventEmitter();
455
+ this.charPick = output();
469
456
  /**
470
457
  * Emitted when a range is picked. This is preceded by a character pick event.
471
458
  * The range is defined by the starting character and the number of characters.
472
459
  * The range is inclusive.
473
460
  */
474
- this.rangePick = new EventEmitter();
461
+ this.rangePick = output();
475
462
  this.lines = [];
463
+ // build lines when text or border color callback change
464
+ effect(() => {
465
+ this.buildLines(this.text(), this.borderColorCallback());
466
+ });
476
467
  }
477
- /**
478
- * The text to display.
479
- */
480
- get text() {
481
- return this._text;
482
- }
483
- set text(value) {
484
- this._text = value || undefined;
485
- this.buildLines();
486
- }
487
- buildLines() {
488
- if (!this._text) {
468
+ buildLines(text, borderColorCallback) {
469
+ if (!text) {
489
470
  this.lines = [];
490
471
  return;
491
472
  }
492
473
  const newLines = [];
493
474
  newLines.push([]);
494
- if (Array.isArray(this._text)) {
495
- const nodes = this._text;
496
- for (let i = 0; i < nodes.length; i++) {
497
- const char = {
498
- id: nodes[i].id,
499
- value: nodes[i].data,
500
- label: nodes[i].label,
501
- color: this.defaultColor,
502
- borderColor: this.defaultBorderColor,
503
- emSize: 1.5,
504
- };
505
- newLines[newLines.length - 1].push(char);
506
- if (char.value === '\n') {
507
- newLines.push([]);
508
- }
509
- }
510
- }
511
- else {
512
- for (let i = 0; i < this._text.length; i++) {
513
- const char = {
514
- id: i + 1,
515
- value: this._text[i],
516
- label: SnapshotViewService.translateSpecialChar(this._text[i]),
517
- color: this.defaultColor,
518
- borderColor: this.defaultBorderColor,
519
- emSize: 1.5,
520
- };
521
- newLines[newLines.length - 1].push(char);
522
- if (char.value === '\n') {
523
- newLines.push([]);
524
- }
475
+ const nodes = text;
476
+ for (let i = 0; i < nodes.length; i++) {
477
+ const char = {
478
+ id: nodes[i].id,
479
+ value: nodes[i].data,
480
+ label: nodes[i].label,
481
+ color: this.defaultColor(),
482
+ borderColor: borderColorCallback
483
+ ? borderColorCallback(nodes[i]) || this.defaultBorderColor()
484
+ : this.defaultBorderColor(),
485
+ emSize: 1.5,
486
+ };
487
+ newLines[newLines.length - 1].push(char);
488
+ if (char.value === '\n') {
489
+ newLines.push([]);
525
490
  }
526
491
  }
527
492
  this.lines = newLines;
@@ -529,8 +494,8 @@ class BaseTextViewComponent {
529
494
  resetColors() {
530
495
  for (const line of this.lines) {
531
496
  for (const c of line) {
532
- c.color = this.defaultColor;
533
- c.borderColor = this.defaultBorderColor;
497
+ c.color = this.defaultColor();
498
+ c.borderColor = this.defaultBorderColor();
534
499
  }
535
500
  }
536
501
  }
@@ -544,8 +509,8 @@ class BaseTextViewComponent {
544
509
  for (const line of this.lines) {
545
510
  for (const c of line) {
546
511
  if (c.id >= minId && c.id <= maxId) {
547
- c.color = this.selectionColor;
548
- c.borderColor = this.selectionColor;
512
+ c.color = this.selectionColor();
513
+ c.borderColor = this.selectionColor();
549
514
  }
550
515
  }
551
516
  }
@@ -555,34 +520,20 @@ class BaseTextViewComponent {
555
520
  }
556
521
  else {
557
522
  // select current char
558
- event.char.color = this.selectionColor;
559
- event.char.borderColor = this.selectionColor;
523
+ event.char.color = this.selectionColor();
524
+ event.char.borderColor = this.selectionColor();
560
525
  this.charPick.emit(event);
561
526
  this.rangePick.emit({ at: event.char.id, run: 1 });
562
527
  }
563
528
  this._lastSelectedChar = event.char;
564
529
  }
565
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: BaseTextViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
566
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.0", type: BaseTextViewComponent, isStandalone: true, selector: "gve-base-text-view", inputs: { defaultColor: "defaultColor", defaultBorderColor: "defaultBorderColor", selectionColor: "selectionColor", hasLineNumber: "hasLineNumber", text: "text" }, outputs: { charPick: "charPick", rangePick: "rangePick" }, ngImport: i0, template: "<div id=\"text\">\r\n @for (line of lines; track $index) {\r\n <div class=\"line\">\r\n @if (hasLineNumber) {\r\n <div class=\"nr\">\r\n {{ lines.indexOf(line) + 1 }}\r\n </div>\r\n } @for (c of line; track c.id) {\r\n <gve-base-text-char [char]=\"c\" (charPick)=\"onCharPick($event)\" />\r\n }\r\n </div>\r\n }\r\n</div>\r\n", styles: [".line{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.line *{flex:0 0 auto}.nr{font-size:.8em;font-weight:700;color:silver;margin:0 4px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: BaseTextCharComponent, selector: "gve-base-text-char", inputs: ["char"], outputs: ["charPick"] }] }); }
530
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: BaseTextViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
531
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.6", type: BaseTextViewComponent, isStandalone: true, selector: "gve-base-text-view", inputs: { defaultColor: { classPropertyName: "defaultColor", publicName: "defaultColor", isSignal: true, isRequired: false, transformFunction: null }, defaultBorderColor: { classPropertyName: "defaultBorderColor", publicName: "defaultBorderColor", isSignal: true, isRequired: false, transformFunction: null }, selectionColor: { classPropertyName: "selectionColor", publicName: "selectionColor", isSignal: true, isRequired: false, transformFunction: null }, hasLineNumber: { classPropertyName: "hasLineNumber", publicName: "hasLineNumber", isSignal: true, isRequired: false, transformFunction: null }, text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: false, transformFunction: null }, borderColorCallback: { classPropertyName: "borderColorCallback", publicName: "borderColorCallback", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { charPick: "charPick", rangePick: "rangePick" }, ngImport: i0, template: "<div id=\"text\">\r\n @for (line of lines; track $index) {\r\n <div class=\"line\">\r\n @if (hasLineNumber()) {\r\n <div class=\"nr\">\r\n {{ lines.indexOf(line) + 1 }}\r\n </div>\r\n } @for (c of line; track c.id) {\r\n <gve-base-text-char [char]=\"c\" (charPick)=\"onCharPick($event)\" />\r\n }\r\n </div>\r\n }\r\n</div>\r\n", styles: [".line{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.line *{flex:0 0 auto}.nr{font-size:.8em;font-weight:700;color:silver;margin:0 4px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: BaseTextCharComponent, selector: "gve-base-text-char", inputs: ["char"], outputs: ["charPick"] }] }); }
567
532
  }
568
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: BaseTextViewComponent, decorators: [{
533
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: BaseTextViewComponent, decorators: [{
569
534
  type: Component,
570
- args: [{ selector: 'gve-base-text-view', imports: [CommonModule, BaseTextCharComponent], template: "<div id=\"text\">\r\n @for (line of lines; track $index) {\r\n <div class=\"line\">\r\n @if (hasLineNumber) {\r\n <div class=\"nr\">\r\n {{ lines.indexOf(line) + 1 }}\r\n </div>\r\n } @for (c of line; track c.id) {\r\n <gve-base-text-char [char]=\"c\" (charPick)=\"onCharPick($event)\" />\r\n }\r\n </div>\r\n }\r\n</div>\r\n", styles: [".line{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.line *{flex:0 0 auto}.nr{font-size:.8em;font-weight:700;color:silver;margin:0 4px}\n"] }]
571
- }], propDecorators: { defaultColor: [{
572
- type: Input
573
- }], defaultBorderColor: [{
574
- type: Input
575
- }], selectionColor: [{
576
- type: Input
577
- }], hasLineNumber: [{
578
- type: Input
579
- }], text: [{
580
- type: Input
581
- }], charPick: [{
582
- type: Output
583
- }], rangePick: [{
584
- type: Output
585
- }] } });
535
+ args: [{ selector: 'gve-base-text-view', imports: [CommonModule, BaseTextCharComponent], template: "<div id=\"text\">\r\n @for (line of lines; track $index) {\r\n <div class=\"line\">\r\n @if (hasLineNumber()) {\r\n <div class=\"nr\">\r\n {{ lines.indexOf(line) + 1 }}\r\n </div>\r\n } @for (c of line; track c.id) {\r\n <gve-base-text-char [char]=\"c\" (charPick)=\"onCharPick($event)\" />\r\n }\r\n </div>\r\n }\r\n</div>\r\n", styles: [".line{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.line *{flex:0 0 auto}.nr{font-size:.8em;font-weight:700;color:silver;margin:0 4px}\n"] }]
536
+ }], ctorParameters: () => [] });
586
537
 
587
538
  /**
588
539
  * 🔑 `gve-feature-editor`
@@ -604,48 +555,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImpor
604
555
  * - 🔥 `featureCancel`: emitted when the user cancels the feature editing.
605
556
  */
606
557
  class FeatureEditorComponent {
607
- /**
608
- * The feature values map. When specified and the user selects a feature
609
- * name present in the map keys, the corresponding values will be used
610
- * to populate the value selection.
611
- */
612
- get featValues() {
613
- return this._map;
614
- }
615
- set featValues(value) {
616
- this._map = value || undefined;
617
- this.nameIds = this.getLabeledIdsFor(this.name.value);
618
- }
619
- /**
620
- * The feature to edit.
621
- */
622
- get feature() {
623
- return this._feature;
624
- }
625
- set feature(value) {
626
- if (this._feature === value) {
627
- return;
628
- }
629
- this._feature = value || undefined;
630
- this.updateForm(this._feature);
631
- setTimeout(() => {
632
- this.nameControl?.nativeElement?.focus();
633
- }, 500);
634
- }
635
558
  constructor(formBuilder) {
559
+ /**
560
+ * The list of feature names to display in the name selection.
561
+ * This is used when you have a closed list of features.
562
+ */
563
+ this.featNames = input();
564
+ /**
565
+ * The feature values map. When specified and the user selects a feature
566
+ * name present in the map keys, the corresponding values will be used
567
+ * to populate the value selection.
568
+ */
569
+ this.featValues = input();
570
+ /**
571
+ * The feature to edit.
572
+ */
573
+ this.feature = model();
636
574
  /**
637
575
  * True if the feature is a variant operation feature, which has
638
576
  * additional properties like negation, global, and short-lived.
639
577
  */
640
- this.isVar = false;
578
+ this.isVar = input(false);
641
579
  /**
642
580
  * Event emitted when the user cancels the feature editing.
643
581
  */
644
- this.featureCancel = new EventEmitter();
582
+ this.featureCancel = output();
645
583
  /**
646
584
  * Event emitted when the user saves the edited feature.
647
585
  */
648
- this.featureChange = new EventEmitter();
586
+ this.featureChange = output();
649
587
  this.name = formBuilder.control('', {
650
588
  validators: [Validators.required, Validators.maxLength(50)],
651
589
  nonNullable: true,
@@ -668,26 +606,35 @@ class FeatureEditorComponent {
668
606
  isGlobal: this.isGlobal,
669
607
  isShortLived: this.isShortLived,
670
608
  });
609
+ // when feature changes, update the form
610
+ effect(() => {
611
+ this.updateForm(this.feature());
612
+ setTimeout(() => {
613
+ this.nameControl?.nativeElement?.focus();
614
+ this.nameIds = this.getLabeledIdsFor(this.name.value, this.featValues());
615
+ }, 500);
616
+ });
671
617
  }
672
618
  ngOnInit() {
673
619
  // whenever the name changes, update the value selection
674
620
  this._sub = this.name.valueChanges
675
621
  .pipe(debounceTime(300), distinctUntilChanged())
676
622
  .subscribe((name) => {
677
- if (!this._frozen && this.featNames?.length) {
623
+ if (!this._frozen && this.featNames()?.length) {
678
624
  this.value.reset();
679
- this.nameIds = this.getLabeledIdsFor(name);
625
+ this.nameIds = this.getLabeledIdsFor(name, this.featValues());
680
626
  }
681
627
  });
682
628
  }
683
629
  ngOnDestroy() {
684
630
  this._sub?.unsubscribe();
685
631
  }
686
- getLabeledIdsFor(id) {
632
+ getLabeledIdsFor(id, featValues) {
687
633
  if (!id) {
688
634
  return undefined;
689
635
  }
690
- return this.featValues ? this.featValues[id] : undefined;
636
+ console.log('getLabeledIdsFor', id, featValues);
637
+ return featValues ? featValues[id] : undefined;
691
638
  }
692
639
  updateForm(feature) {
693
640
  if (!feature) {
@@ -711,7 +658,7 @@ class FeatureEditorComponent {
711
658
  if (!this.form.valid) {
712
659
  return;
713
660
  }
714
- this.feature = this.isVar
661
+ this.feature.set(this.isVar()
715
662
  ? {
716
663
  name: this.name.value.trim(),
717
664
  value: this.value.value.trim(),
@@ -724,13 +671,13 @@ class FeatureEditorComponent {
724
671
  name: this.name.value.trim(),
725
672
  value: this.value.value.trim(),
726
673
  setPolicy: this.setPolicy.value,
727
- };
728
- this.featureChange.emit(this.feature);
674
+ });
675
+ this.featureChange.emit(this.feature());
729
676
  }
730
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: FeatureEditorComponent, deps: [{ token: i1.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); }
731
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.0", type: FeatureEditorComponent, isStandalone: true, selector: "gve-feature-editor", inputs: { featNames: "featNames", featValues: "featValues", feature: "feature", isVar: "isVar" }, outputs: { featureCancel: "featureCancel", featureChange: "featureChange" }, viewQueries: [{ propertyName: "nameControl", first: true, predicate: ["nameCtl"], descendants: true }], ngImport: i0, template: "<form [formGroup]=\"form\" (submit)=\"save()\">\r\n <div class=\"form-row\">\r\n @if (featNames?.length) {\r\n <!-- name (bound) -->\r\n <mat-form-field>\r\n <mat-label>name</mat-label>\r\n <mat-select #nameCtl [formControl]=\"name\">\r\n <mat-option *ngFor=\"let i of featNames\" [value]=\"i.id\">{{\r\n i.label\r\n }}</mat-option>\r\n </mat-select>\r\n @if ($any(name).errors?.required && (name.dirty || name.touched)) {\r\n <mat-error>name required</mat-error>\r\n }\r\n </mat-form-field>\r\n } @else {\r\n <!-- name (free) -->\r\n <mat-form-field>\r\n <mat-label>name</mat-label>\r\n <input #nameCtl matInput [formControl]=\"name\" />\r\n <mat-error\r\n *ngIf=\"$any(name).errors?.required && (name.dirty || name.touched)\"\r\n >name required</mat-error\r\n >\r\n @if ($any(name).errors?.maxLength && (name.dirty || name.touched)) {\r\n <mat-error>name too long</mat-error>\r\n }\r\n </mat-form-field>\r\n }\r\n\r\n <!-- value (bound) -->\r\n @if (nameIds?.length) {\r\n <mat-form-field>\r\n <mat-label>value</mat-label>\r\n <mat-select [formControl]=\"value\">\r\n <mat-option *ngFor=\"let e of nameIds\" [value]=\"e.id\">{{\r\n e.label\r\n }}</mat-option>\r\n </mat-select>\r\n @if ($any(value).errors?.required && (value.dirty || value.touched)) {\r\n <mat-error>value required</mat-error>\r\n }\r\n </mat-form-field>\r\n } @else {\r\n <!-- value (free) -->\r\n <mat-form-field>\r\n <mat-label>value</mat-label>\r\n <input matInput [formControl]=\"value\" />\r\n @if ($any(value).errors?.required && (value.dirty || value.touched)) {\r\n <mat-error>value required</mat-error>\r\n } @if ($any(value).errors?.maxLength && (value.dirty || value.touched)) {\r\n <mat-error>value too long</mat-error>\r\n }\r\n </mat-form-field>\r\n }\r\n\r\n <!-- setPolicy -->\r\n <mat-form-field>\r\n <mat-label>set policy</mat-label>\r\n <mat-select [formControl]=\"setPolicy\">\r\n <mat-option [value]=\"0\">multiple</mat-option>\r\n <mat-option [value]=\"1\">single</mat-option>\r\n <mat-option [value]=\"2\">single first</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n </div>\r\n\r\n @if (isVar) {\r\n <div class=\"form-row\">\r\n <!-- isNegated -->\r\n <mat-checkbox [formControl]=\"isNegated\">negated</mat-checkbox>\r\n\r\n <!-- isShortLived -->\r\n <mat-checkbox [formControl]=\"isShortLived\">short-lived</mat-checkbox>\r\n\r\n <!-- isGlobal -->\r\n <mat-checkbox [formControl]=\"isGlobal\">global</mat-checkbox>\r\n </div>\r\n }\r\n\r\n <!-- buttons -->\r\n <div class=\"form-row\">\r\n <button\r\n type=\"button\"\r\n color=\"warn\"\r\n mat-icon-button\r\n matTooltip=\"Discard changes\"\r\n (click)=\"cancel()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n <button\r\n type=\"submit\"\r\n color=\"primary\"\r\n mat-icon-button\r\n matTooltip=\"Accept changes\"\r\n [disabled]=\"form.invalid || form.pristine\"\r\n >\r\n <mat-icon class=\"mat-primary\">check_circle</mat-icon>\r\n </button>\r\n </div>\r\n</form>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i4$1.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "directive", type: i5.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i8.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i9.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i10.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] }); }
677
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: FeatureEditorComponent, deps: [{ token: i1.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); }
678
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.6", type: FeatureEditorComponent, isStandalone: true, selector: "gve-feature-editor", inputs: { featNames: { classPropertyName: "featNames", publicName: "featNames", isSignal: true, isRequired: false, transformFunction: null }, featValues: { classPropertyName: "featValues", publicName: "featValues", isSignal: true, isRequired: false, transformFunction: null }, feature: { classPropertyName: "feature", publicName: "feature", isSignal: true, isRequired: false, transformFunction: null }, isVar: { classPropertyName: "isVar", publicName: "isVar", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { feature: "featureChange", featureCancel: "featureCancel", featureChange: "featureChange" }, viewQueries: [{ propertyName: "nameControl", first: true, predicate: ["nameCtl"], descendants: true }], ngImport: i0, template: "<form [formGroup]=\"form\" (submit)=\"save()\">\r\n <div class=\"form-row\">\r\n @if (featNames()?.length) {\r\n <!-- name (bound) -->\r\n <mat-form-field>\r\n <mat-label>name</mat-label>\r\n <mat-select #nameCtl [formControl]=\"name\">\r\n <mat-option *ngFor=\"let i of featNames()\" [value]=\"i.id\">{{\r\n i.label\r\n }}</mat-option>\r\n </mat-select>\r\n @if ($any(name).errors?.required && (name.dirty || name.touched)) {\r\n <mat-error>name required</mat-error>\r\n }\r\n </mat-form-field>\r\n } @else {\r\n <!-- name (free) -->\r\n <mat-form-field>\r\n <mat-label>name</mat-label>\r\n <input #nameCtl matInput [formControl]=\"name\" />\r\n <mat-error\r\n *ngIf=\"$any(name).errors?.required && (name.dirty || name.touched)\"\r\n >name required</mat-error\r\n >\r\n @if ($any(name).errors?.maxLength && (name.dirty || name.touched)) {\r\n <mat-error>name too long</mat-error>\r\n }\r\n </mat-form-field>\r\n }\r\n\r\n <!-- value (bound) -->\r\n @if (nameIds?.length) {\r\n <mat-form-field>\r\n <mat-label>value</mat-label>\r\n <mat-select [formControl]=\"value\">\r\n <mat-option *ngFor=\"let e of nameIds\" [value]=\"e.id\">{{\r\n e.label\r\n }}</mat-option>\r\n </mat-select>\r\n @if ($any(value).errors?.required && (value.dirty || value.touched)) {\r\n <mat-error>value required</mat-error>\r\n }\r\n </mat-form-field>\r\n } @else {\r\n <!-- value (free) -->\r\n <mat-form-field>\r\n <mat-label>value</mat-label>\r\n <input matInput [formControl]=\"value\" />\r\n @if ($any(value).errors?.required && (value.dirty || value.touched)) {\r\n <mat-error>value required</mat-error>\r\n } @if ($any(value).errors?.maxLength && (value.dirty || value.touched)) {\r\n <mat-error>value too long</mat-error>\r\n }\r\n </mat-form-field>\r\n }\r\n\r\n <!-- setPolicy -->\r\n <mat-form-field>\r\n <mat-label>set policy</mat-label>\r\n <mat-select [formControl]=\"setPolicy\">\r\n <mat-option [value]=\"0\">multiple</mat-option>\r\n <mat-option [value]=\"1\">single</mat-option>\r\n <mat-option [value]=\"2\">single first</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n </div>\r\n\r\n @if (isVar()) {\r\n <div class=\"form-row\">\r\n <!-- isNegated -->\r\n <mat-checkbox [formControl]=\"isNegated\">negated</mat-checkbox>\r\n\r\n <!-- isShortLived -->\r\n <mat-checkbox [formControl]=\"isShortLived\">short-lived</mat-checkbox>\r\n\r\n <!-- isGlobal -->\r\n <mat-checkbox [formControl]=\"isGlobal\">global</mat-checkbox>\r\n </div>\r\n }\r\n\r\n <!-- buttons -->\r\n <div class=\"form-row\">\r\n <button\r\n type=\"button\"\r\n color=\"warn\"\r\n mat-icon-button\r\n matTooltip=\"Discard changes\"\r\n (click)=\"cancel()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n <button\r\n type=\"submit\"\r\n color=\"primary\"\r\n mat-icon-button\r\n matTooltip=\"Accept changes\"\r\n [disabled]=\"form.invalid || form.pristine\"\r\n >\r\n <mat-icon class=\"mat-primary\">check_circle</mat-icon>\r\n </button>\r\n </div>\r\n</form>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i4$1.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "directive", type: i5.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i8.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i9.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i10.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] }); }
732
679
  }
733
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: FeatureEditorComponent, decorators: [{
680
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: FeatureEditorComponent, decorators: [{
734
681
  type: Component,
735
682
  args: [{ selector: 'gve-feature-editor', imports: [
736
683
  CommonModule,
@@ -742,22 +689,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImpor
742
689
  MatInputModule,
743
690
  MatSelectModule,
744
691
  MatTooltipModule,
745
- ], template: "<form [formGroup]=\"form\" (submit)=\"save()\">\r\n <div class=\"form-row\">\r\n @if (featNames?.length) {\r\n <!-- name (bound) -->\r\n <mat-form-field>\r\n <mat-label>name</mat-label>\r\n <mat-select #nameCtl [formControl]=\"name\">\r\n <mat-option *ngFor=\"let i of featNames\" [value]=\"i.id\">{{\r\n i.label\r\n }}</mat-option>\r\n </mat-select>\r\n @if ($any(name).errors?.required && (name.dirty || name.touched)) {\r\n <mat-error>name required</mat-error>\r\n }\r\n </mat-form-field>\r\n } @else {\r\n <!-- name (free) -->\r\n <mat-form-field>\r\n <mat-label>name</mat-label>\r\n <input #nameCtl matInput [formControl]=\"name\" />\r\n <mat-error\r\n *ngIf=\"$any(name).errors?.required && (name.dirty || name.touched)\"\r\n >name required</mat-error\r\n >\r\n @if ($any(name).errors?.maxLength && (name.dirty || name.touched)) {\r\n <mat-error>name too long</mat-error>\r\n }\r\n </mat-form-field>\r\n }\r\n\r\n <!-- value (bound) -->\r\n @if (nameIds?.length) {\r\n <mat-form-field>\r\n <mat-label>value</mat-label>\r\n <mat-select [formControl]=\"value\">\r\n <mat-option *ngFor=\"let e of nameIds\" [value]=\"e.id\">{{\r\n e.label\r\n }}</mat-option>\r\n </mat-select>\r\n @if ($any(value).errors?.required && (value.dirty || value.touched)) {\r\n <mat-error>value required</mat-error>\r\n }\r\n </mat-form-field>\r\n } @else {\r\n <!-- value (free) -->\r\n <mat-form-field>\r\n <mat-label>value</mat-label>\r\n <input matInput [formControl]=\"value\" />\r\n @if ($any(value).errors?.required && (value.dirty || value.touched)) {\r\n <mat-error>value required</mat-error>\r\n } @if ($any(value).errors?.maxLength && (value.dirty || value.touched)) {\r\n <mat-error>value too long</mat-error>\r\n }\r\n </mat-form-field>\r\n }\r\n\r\n <!-- setPolicy -->\r\n <mat-form-field>\r\n <mat-label>set policy</mat-label>\r\n <mat-select [formControl]=\"setPolicy\">\r\n <mat-option [value]=\"0\">multiple</mat-option>\r\n <mat-option [value]=\"1\">single</mat-option>\r\n <mat-option [value]=\"2\">single first</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n </div>\r\n\r\n @if (isVar) {\r\n <div class=\"form-row\">\r\n <!-- isNegated -->\r\n <mat-checkbox [formControl]=\"isNegated\">negated</mat-checkbox>\r\n\r\n <!-- isShortLived -->\r\n <mat-checkbox [formControl]=\"isShortLived\">short-lived</mat-checkbox>\r\n\r\n <!-- isGlobal -->\r\n <mat-checkbox [formControl]=\"isGlobal\">global</mat-checkbox>\r\n </div>\r\n }\r\n\r\n <!-- buttons -->\r\n <div class=\"form-row\">\r\n <button\r\n type=\"button\"\r\n color=\"warn\"\r\n mat-icon-button\r\n matTooltip=\"Discard changes\"\r\n (click)=\"cancel()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n <button\r\n type=\"submit\"\r\n color=\"primary\"\r\n mat-icon-button\r\n matTooltip=\"Accept changes\"\r\n [disabled]=\"form.invalid || form.pristine\"\r\n >\r\n <mat-icon class=\"mat-primary\">check_circle</mat-icon>\r\n </button>\r\n </div>\r\n</form>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}\n"] }]
692
+ ], template: "<form [formGroup]=\"form\" (submit)=\"save()\">\r\n <div class=\"form-row\">\r\n @if (featNames()?.length) {\r\n <!-- name (bound) -->\r\n <mat-form-field>\r\n <mat-label>name</mat-label>\r\n <mat-select #nameCtl [formControl]=\"name\">\r\n <mat-option *ngFor=\"let i of featNames()\" [value]=\"i.id\">{{\r\n i.label\r\n }}</mat-option>\r\n </mat-select>\r\n @if ($any(name).errors?.required && (name.dirty || name.touched)) {\r\n <mat-error>name required</mat-error>\r\n }\r\n </mat-form-field>\r\n } @else {\r\n <!-- name (free) -->\r\n <mat-form-field>\r\n <mat-label>name</mat-label>\r\n <input #nameCtl matInput [formControl]=\"name\" />\r\n <mat-error\r\n *ngIf=\"$any(name).errors?.required && (name.dirty || name.touched)\"\r\n >name required</mat-error\r\n >\r\n @if ($any(name).errors?.maxLength && (name.dirty || name.touched)) {\r\n <mat-error>name too long</mat-error>\r\n }\r\n </mat-form-field>\r\n }\r\n\r\n <!-- value (bound) -->\r\n @if (nameIds?.length) {\r\n <mat-form-field>\r\n <mat-label>value</mat-label>\r\n <mat-select [formControl]=\"value\">\r\n <mat-option *ngFor=\"let e of nameIds\" [value]=\"e.id\">{{\r\n e.label\r\n }}</mat-option>\r\n </mat-select>\r\n @if ($any(value).errors?.required && (value.dirty || value.touched)) {\r\n <mat-error>value required</mat-error>\r\n }\r\n </mat-form-field>\r\n } @else {\r\n <!-- value (free) -->\r\n <mat-form-field>\r\n <mat-label>value</mat-label>\r\n <input matInput [formControl]=\"value\" />\r\n @if ($any(value).errors?.required && (value.dirty || value.touched)) {\r\n <mat-error>value required</mat-error>\r\n } @if ($any(value).errors?.maxLength && (value.dirty || value.touched)) {\r\n <mat-error>value too long</mat-error>\r\n }\r\n </mat-form-field>\r\n }\r\n\r\n <!-- setPolicy -->\r\n <mat-form-field>\r\n <mat-label>set policy</mat-label>\r\n <mat-select [formControl]=\"setPolicy\">\r\n <mat-option [value]=\"0\">multiple</mat-option>\r\n <mat-option [value]=\"1\">single</mat-option>\r\n <mat-option [value]=\"2\">single first</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n </div>\r\n\r\n @if (isVar()) {\r\n <div class=\"form-row\">\r\n <!-- isNegated -->\r\n <mat-checkbox [formControl]=\"isNegated\">negated</mat-checkbox>\r\n\r\n <!-- isShortLived -->\r\n <mat-checkbox [formControl]=\"isShortLived\">short-lived</mat-checkbox>\r\n\r\n <!-- isGlobal -->\r\n <mat-checkbox [formControl]=\"isGlobal\">global</mat-checkbox>\r\n </div>\r\n }\r\n\r\n <!-- buttons -->\r\n <div class=\"form-row\">\r\n <button\r\n type=\"button\"\r\n color=\"warn\"\r\n mat-icon-button\r\n matTooltip=\"Discard changes\"\r\n (click)=\"cancel()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n <button\r\n type=\"submit\"\r\n color=\"primary\"\r\n mat-icon-button\r\n matTooltip=\"Accept changes\"\r\n [disabled]=\"form.invalid || form.pristine\"\r\n >\r\n <mat-icon class=\"mat-primary\">check_circle</mat-icon>\r\n </button>\r\n </div>\r\n</form>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}\n"] }]
746
693
  }], ctorParameters: () => [{ type: i1.FormBuilder }], propDecorators: { nameControl: [{
747
694
  type: ViewChild,
748
695
  args: ['nameCtl']
749
- }], featNames: [{
750
- type: Input
751
- }], featValues: [{
752
- type: Input
753
- }], feature: [{
754
- type: Input
755
- }], isVar: [{
756
- type: Input
757
- }], featureCancel: [{
758
- type: Output
759
- }], featureChange: [{
760
- type: Output
761
696
  }] } });
762
697
 
763
698
  /**
@@ -776,58 +711,65 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImpor
776
711
  * - ▶️ `filterThreshold` (`number`): the threshold at which the features filter
777
712
  * should become visible. If set to 0, the filter is always visible; if set to -1,
778
713
  * it is always invisible; otherwise, it gets visible when the number of features
779
- * is greater than the threshold. Default is 5.
714
+ * is equal to or greater than the threshold. Default is 5.
780
715
  * - ▶️ `features` (`Feature[]`): the features to edit.
781
716
  * - ▶️ `isVar`: true if the feature is a variant operation feature, which
782
717
  * has additional properties like negation, global, and short-lived.
783
718
  * - 🔥 `featuresChange` (`Feature[]`): emitted when features have changed.
784
719
  */
785
720
  class FeatureSetEditorComponent {
786
- /**
787
- * The features to edit.
788
- */
789
- get features() {
790
- return this._features;
791
- }
792
- set features(value) {
793
- if (this._features === value) {
794
- return;
795
- }
796
- this._features = value;
797
- this.applyFeatureFilter(this.filter.value);
798
- }
799
721
  constructor(formBuilder) {
800
722
  this.POLICIES = ['M', 'S', 'SF'];
801
723
  /**
802
724
  * True if the features are variable features.
803
725
  */
804
- this.isVar = false;
726
+ this.isVar = input(false);
727
+ /**
728
+ * The list of feature names to display in the name selection.
729
+ * This is used when you have a closed list of features.
730
+ */
731
+ this.featNames = input();
732
+ /**
733
+ * The feature values map. When specified and the user selects a feature
734
+ * name present in the map keys, the corresponding values will be used
735
+ * to populate the value selection.
736
+ */
737
+ this.featValues = input();
805
738
  /**
806
739
  * The threshold at which the features filter should become visible.
807
740
  * If set to 0, the filter is always visible; if set to -1, it is always
808
741
  * invisible; otherwise, it gets visible when the number of features
809
- * is greater than the threshold. Default is 5.
742
+ * is greater than or equal to the threshold. Default is 5.
810
743
  */
811
- this.filterThreshold = 5;
744
+ this.filterThreshold = input(5);
745
+ /**
746
+ * The features to edit.
747
+ */
748
+ this.features = model([]);
812
749
  /**
813
750
  * Emitted when the features change.
814
751
  */
815
- this.featuresChange = new EventEmitter();
816
- this._features = [];
752
+ this.featuresChange = output();
817
753
  this.filteredFeatures = [];
818
754
  this.filter = formBuilder.control(null);
819
755
  this._editedFeatureIndex = -1;
820
- }
821
- applyFeatureFilter(name) {
822
- this.filteredFeatures = this.features.filter((feature) => {
823
- return !name || feature.name.toLowerCase().includes(name.toLowerCase());
756
+ // when features change, apply the filter
757
+ effect(() => {
758
+ this.applyFeatureFilter(this.features(), this.filter.value);
824
759
  });
825
760
  }
761
+ applyFeatureFilter(features, name) {
762
+ this.filteredFeatures =
763
+ features.filter((feature) => {
764
+ return !name || feature.name.toLowerCase().includes(name.toLowerCase());
765
+ }) || [];
766
+ }
826
767
  ngOnInit() {
768
+ // when the filter changes, apply the filter
827
769
  this._sub = this.filter.valueChanges
828
770
  .pipe(distinctUntilChanged(), debounceTime(300))
829
771
  .subscribe((value) => {
830
- this.applyFeatureFilter(value);
772
+ this.applyFeatureFilter(this.features(), value);
831
773
  });
832
774
  }
833
775
  ngOnDestroy() {
@@ -843,20 +785,23 @@ class FeatureSetEditorComponent {
843
785
  }
844
786
  editFeature(feature) {
845
787
  this.editedFeature = feature;
846
- this._editedFeatureIndex = this.features.indexOf(feature);
788
+ this._editedFeatureIndex = this.features().indexOf(feature);
847
789
  }
848
790
  deleteFeature(feature) {
849
- const index = this.features.indexOf(feature);
791
+ const index = this.features().indexOf(feature);
850
792
  if (index < 0) {
851
793
  return;
852
794
  }
853
- const features = [...this.features];
795
+ const features = [...this.features()];
854
796
  features.splice(index, 1);
855
- this.features = features;
856
- this.featuresChange.emit(this.features);
797
+ this.features.set(features);
798
+ this.featuresChange.emit(this.features());
857
799
  }
858
800
  onFeatureChange(feature) {
859
- const features = [...this.features];
801
+ if (!feature) {
802
+ return;
803
+ }
804
+ const features = [...this.features()];
860
805
  for (let i = features.length - 1; i >= 0; i--) {
861
806
  if (features[i].name === feature.name) {
862
807
  if (feature.setPolicy === FeatureSetPolicy.single ||
@@ -871,19 +816,18 @@ class FeatureSetEditorComponent {
871
816
  else {
872
817
  features.splice(this._editedFeatureIndex, 1, feature);
873
818
  }
874
- this.features = features;
875
- this.editedFeature = undefined;
876
- this._editedFeatureIndex = -1;
877
- this.featuresChange.emit(this.features);
819
+ this.onFeatureCancel();
820
+ this.features.set(features);
821
+ this.featuresChange.emit(this.features());
878
822
  }
879
823
  onFeatureCancel() {
880
824
  this.editedFeature = undefined;
881
825
  this._editedFeatureIndex = -1;
882
826
  }
883
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: FeatureSetEditorComponent, deps: [{ token: i1.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); }
884
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.0", type: FeatureSetEditorComponent, isStandalone: true, selector: "gve-feature-set-editor", inputs: { isVar: "isVar", featNames: "featNames", featValues: "featValues", filterThreshold: "filterThreshold", features: "features" }, outputs: { featuresChange: "featuresChange" }, ngImport: i0, template: "<div>\r\n <div class=\"form-row\">\r\n <!-- filter -->\r\n @if (filterThreshold === 0 || features.length > filterThreshold) {\r\n <mat-form-field>\r\n <input matInput placeholder=\"filter\" [formControl]=\"filter\" />\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matSuffix\r\n (click)=\"filter.reset()\"\r\n [disabled]=\"!filter.value\"\r\n [attr.aria-label]=\"'Reset filter'\"\r\n >\r\n <mat-icon color=\"warn\" class=\"mat-warn\">cancel</mat-icon>\r\n </button>\r\n </mat-form-field>\r\n }\r\n\r\n <!-- add feature button -->\r\n <button\r\n type=\"button\"\r\n color=\"primary\"\r\n class=\"mat-primary\"\r\n mat-flat-button\r\n matTooltip=\"Add a new feature\"\r\n (click)=\"addFeature()\"\r\n [class.in-row-button]=\"\r\n filterThreshold === 0 || features.length > filterThreshold\r\n \"\r\n >\r\n <mat-icon>add</mat-icon>\r\n feature\r\n </button>\r\n </div>\r\n\r\n <!-- list -->\r\n @if (features.length) {\r\n <table>\r\n <thead>\r\n <tr>\r\n <th></th>\r\n <th>feature</th>\r\n <th>value</th>\r\n <th>policy</th>\r\n @if (isVar) {\r\n <th>negated</th>\r\n <th>global</th>\r\n <th>short-lived</th>\r\n }\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (feature of filteredFeatures; track feature) {\r\n <tr>\r\n <td class=\"fit-width\">\r\n <!-- edit -->\r\n <button\r\n type=\"button\"\r\n color=\"primary\"\r\n mat-icon-button\r\n matTooltip=\"Edit this feature\"\r\n (click)=\"editFeature(feature)\"\r\n >\r\n <mat-icon class=\"mat-primary\">edit</mat-icon>\r\n </button>\r\n <!-- delete -->\r\n <button\r\n type=\"button\"\r\n color=\"warn\"\r\n mat-icon-button\r\n matTooltip=\"Delete this feature\"\r\n (click)=\"deleteFeature(feature)\"\r\n >\r\n <mat-icon class=\"mat-warn\">delete</mat-icon>\r\n </button>\r\n </td>\r\n <td>\r\n @if (featNames?.length) {\r\n <span>{{\r\n feature.name | flatLookup : featNames : \"id\" : \"label\"\r\n }}</span>\r\n } @else {\r\n <span>{{ feature.name }}</span>\r\n }\r\n </td>\r\n <td>\r\n @if (featValues) {\r\n <span>{{\r\n feature.value | flatLookup : featValues[feature.name]\r\n }}</span>\r\n } @else {\r\n <span>{{ feature.value }}</span>\r\n }\r\n </td>\r\n <td>\r\n <span>{{ POLICIES[feature.setPolicy] }}</span>\r\n </td>\r\n @if (isVar) {\r\n <td>\r\n <span>{{ $any(feature).isNegated ? \"yes\" : \"no\" }}</span>\r\n </td>\r\n <td>\r\n <span>{{ $any(feature).isGlobal ? \"yes\" : \"no\" }}</span>\r\n </td>\r\n <td>\r\n <span>{{ $any(feature).isShortLived ? \"yes\" : \"no\" }}</span>\r\n </td>\r\n }\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n }\r\n\r\n <!-- editor -->\r\n <mat-expansion-panel [disabled]=\"!editedFeature\" [expanded]=\"editedFeature\">\r\n <mat-expansion-panel-header>\r\n <mat-panel-title>\r\n <span>{{ editedFeature?.name || \"feature\" }}</span>\r\n </mat-panel-title>\r\n </mat-expansion-panel-header>\r\n <gve-feature-editor\r\n [isVar]=\"isVar\"\r\n [featNames]=\"featNames\"\r\n [featValues]=\"featValues\"\r\n [feature]=\"editedFeature\"\r\n (featureChange)=\"onFeatureChange($event)\"\r\n (featureCancel)=\"onFeatureCancel()\"\r\n />\r\n </mat-expansion-panel>\r\n</div>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}.in-row-button{margin-top:-16px}table{width:100%;border-collapse:collapse;margin-top:8px}th{color:#909090;font-weight:400;text-align:left;background-color:#e1e0e0}th,td{padding:4px;border-bottom:1px solid silver}tbody tr:nth-child(2n){background-color:#e8e8e8}td.fit-width{width:1px;white-space:nowrap}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "component", type: i4.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i4.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i4.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i10.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: NgToolsModule }, { kind: "pipe", type: i2$1.FlatLookupPipe, name: "flatLookup" }, { kind: "component", type: FeatureEditorComponent, selector: "gve-feature-editor", inputs: ["featNames", "featValues", "feature", "isVar"], outputs: ["featureCancel", "featureChange"] }] }); }
827
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: FeatureSetEditorComponent, deps: [{ token: i1.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); }
828
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.6", type: FeatureSetEditorComponent, isStandalone: true, selector: "gve-feature-set-editor", inputs: { isVar: { classPropertyName: "isVar", publicName: "isVar", isSignal: true, isRequired: false, transformFunction: null }, featNames: { classPropertyName: "featNames", publicName: "featNames", isSignal: true, isRequired: false, transformFunction: null }, featValues: { classPropertyName: "featValues", publicName: "featValues", isSignal: true, isRequired: false, transformFunction: null }, filterThreshold: { classPropertyName: "filterThreshold", publicName: "filterThreshold", isSignal: true, isRequired: false, transformFunction: null }, features: { classPropertyName: "features", publicName: "features", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { features: "featuresChange", featuresChange: "featuresChange" }, ngImport: i0, template: "<div>\r\n <div class=\"form-row\">\r\n <!-- filter -->\r\n @if (filterThreshold() === 0 || features().length >= filterThreshold()) {\r\n <mat-form-field>\r\n <input matInput placeholder=\"filter\" [formControl]=\"filter\" />\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matSuffix\r\n (click)=\"filter.reset()\"\r\n [disabled]=\"!filter.value\"\r\n [attr.aria-label]=\"'Reset filter'\"\r\n >\r\n <mat-icon color=\"warn\" class=\"mat-warn\">cancel</mat-icon>\r\n </button>\r\n </mat-form-field>\r\n }\r\n\r\n <!-- add feature button -->\r\n <button\r\n type=\"button\"\r\n color=\"primary\"\r\n class=\"mat-primary\"\r\n mat-flat-button\r\n matTooltip=\"Add a new feature\"\r\n (click)=\"addFeature()\"\r\n [class.in-row-button]=\"\r\n filterThreshold() === 0 || features()!.length >= filterThreshold()\r\n \"\r\n >\r\n <mat-icon>add</mat-icon>\r\n feature\r\n </button>\r\n </div>\r\n\r\n <!-- list -->\r\n @if (features().length) {\r\n <table>\r\n <thead>\r\n <tr>\r\n <th></th>\r\n <th>feature</th>\r\n <th>value</th>\r\n <th>policy</th>\r\n @if (isVar()) {\r\n <th>negated</th>\r\n <th>global</th>\r\n <th>short-lived</th>\r\n }\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (feature of filteredFeatures; track feature) {\r\n <tr>\r\n <td class=\"fit-width\">\r\n <!-- edit -->\r\n <button\r\n type=\"button\"\r\n color=\"primary\"\r\n mat-icon-button\r\n matTooltip=\"Edit this feature\"\r\n (click)=\"editFeature(feature)\"\r\n >\r\n <mat-icon class=\"mat-primary\">edit</mat-icon>\r\n </button>\r\n <!-- delete -->\r\n <button\r\n type=\"button\"\r\n color=\"warn\"\r\n mat-icon-button\r\n matTooltip=\"Delete this feature\"\r\n (click)=\"deleteFeature(feature)\"\r\n >\r\n <mat-icon class=\"mat-warn\">delete</mat-icon>\r\n </button>\r\n </td>\r\n <td>\r\n @if (featNames()?.length) {\r\n <span>{{\r\n feature.name | flatLookup : featNames : \"id\" : \"label\"\r\n }}</span>\r\n } @else {\r\n <span>{{ feature.name }}</span>\r\n }\r\n </td>\r\n <td>\r\n @if (featValues()) {\r\n <span>{{\r\n feature.value | flatLookup : featValues()![feature.name]\r\n }}</span>\r\n } @else {\r\n <span>{{ feature.value }}</span>\r\n }\r\n </td>\r\n <td>\r\n <span>{{ POLICIES[feature.setPolicy] }}</span>\r\n </td>\r\n @if (isVar()) {\r\n <td>\r\n <span>{{ $any(feature).isNegated ? \"yes\" : \"no\" }}</span>\r\n </td>\r\n <td>\r\n <span>{{ $any(feature).isGlobal ? \"yes\" : \"no\" }}</span>\r\n </td>\r\n <td>\r\n <span>{{ $any(feature).isShortLived ? \"yes\" : \"no\" }}</span>\r\n </td>\r\n }\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n }\r\n\r\n <!-- editor -->\r\n <mat-expansion-panel [disabled]=\"!editedFeature\" [expanded]=\"editedFeature\">\r\n <mat-expansion-panel-header>\r\n <mat-panel-title>\r\n <span>{{ editedFeature?.name || \"feature\" }}</span>\r\n </mat-panel-title>\r\n </mat-expansion-panel-header>\r\n <gve-feature-editor\r\n [isVar]=\"isVar()\"\r\n [featNames]=\"featNames()\"\r\n [featValues]=\"featValues()\"\r\n [feature]=\"editedFeature\"\r\n (featureChange)=\"onFeatureChange($event)\"\r\n (featureCancel)=\"onFeatureCancel()\"\r\n />\r\n </mat-expansion-panel>\r\n</div>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}.in-row-button{margin-top:-16px}table{width:100%;border-collapse:collapse;margin-top:8px}th{color:#909090;font-weight:400;text-align:left;background-color:#e1e0e0}th,td{padding:4px;border-bottom:1px solid silver}tbody tr:nth-child(2n){background-color:#e8e8e8}td.fit-width{width:1px;white-space:nowrap}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "component", type: i4.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i4.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i4.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i10.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "pipe", type: FlatLookupPipe, name: "flatLookup" }, { kind: "component", type: FeatureEditorComponent, selector: "gve-feature-editor", inputs: ["featNames", "featValues", "feature", "isVar"], outputs: ["featureChange", "featureCancel"] }] }); }
885
829
  }
886
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: FeatureSetEditorComponent, decorators: [{
830
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: FeatureSetEditorComponent, decorators: [{
887
831
  type: Component,
888
832
  args: [{ selector: 'gve-feature-set-editor', imports: [
889
833
  CommonModule,
@@ -895,22 +839,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImpor
895
839
  MatInputModule,
896
840
  MatSelectModule,
897
841
  MatTooltipModule,
898
- NgToolsModule,
842
+ FlatLookupPipe,
899
843
  FeatureEditorComponent,
900
- ], template: "<div>\r\n <div class=\"form-row\">\r\n <!-- filter -->\r\n @if (filterThreshold === 0 || features.length > filterThreshold) {\r\n <mat-form-field>\r\n <input matInput placeholder=\"filter\" [formControl]=\"filter\" />\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matSuffix\r\n (click)=\"filter.reset()\"\r\n [disabled]=\"!filter.value\"\r\n [attr.aria-label]=\"'Reset filter'\"\r\n >\r\n <mat-icon color=\"warn\" class=\"mat-warn\">cancel</mat-icon>\r\n </button>\r\n </mat-form-field>\r\n }\r\n\r\n <!-- add feature button -->\r\n <button\r\n type=\"button\"\r\n color=\"primary\"\r\n class=\"mat-primary\"\r\n mat-flat-button\r\n matTooltip=\"Add a new feature\"\r\n (click)=\"addFeature()\"\r\n [class.in-row-button]=\"\r\n filterThreshold === 0 || features.length > filterThreshold\r\n \"\r\n >\r\n <mat-icon>add</mat-icon>\r\n feature\r\n </button>\r\n </div>\r\n\r\n <!-- list -->\r\n @if (features.length) {\r\n <table>\r\n <thead>\r\n <tr>\r\n <th></th>\r\n <th>feature</th>\r\n <th>value</th>\r\n <th>policy</th>\r\n @if (isVar) {\r\n <th>negated</th>\r\n <th>global</th>\r\n <th>short-lived</th>\r\n }\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (feature of filteredFeatures; track feature) {\r\n <tr>\r\n <td class=\"fit-width\">\r\n <!-- edit -->\r\n <button\r\n type=\"button\"\r\n color=\"primary\"\r\n mat-icon-button\r\n matTooltip=\"Edit this feature\"\r\n (click)=\"editFeature(feature)\"\r\n >\r\n <mat-icon class=\"mat-primary\">edit</mat-icon>\r\n </button>\r\n <!-- delete -->\r\n <button\r\n type=\"button\"\r\n color=\"warn\"\r\n mat-icon-button\r\n matTooltip=\"Delete this feature\"\r\n (click)=\"deleteFeature(feature)\"\r\n >\r\n <mat-icon class=\"mat-warn\">delete</mat-icon>\r\n </button>\r\n </td>\r\n <td>\r\n @if (featNames?.length) {\r\n <span>{{\r\n feature.name | flatLookup : featNames : \"id\" : \"label\"\r\n }}</span>\r\n } @else {\r\n <span>{{ feature.name }}</span>\r\n }\r\n </td>\r\n <td>\r\n @if (featValues) {\r\n <span>{{\r\n feature.value | flatLookup : featValues[feature.name]\r\n }}</span>\r\n } @else {\r\n <span>{{ feature.value }}</span>\r\n }\r\n </td>\r\n <td>\r\n <span>{{ POLICIES[feature.setPolicy] }}</span>\r\n </td>\r\n @if (isVar) {\r\n <td>\r\n <span>{{ $any(feature).isNegated ? \"yes\" : \"no\" }}</span>\r\n </td>\r\n <td>\r\n <span>{{ $any(feature).isGlobal ? \"yes\" : \"no\" }}</span>\r\n </td>\r\n <td>\r\n <span>{{ $any(feature).isShortLived ? \"yes\" : \"no\" }}</span>\r\n </td>\r\n }\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n }\r\n\r\n <!-- editor -->\r\n <mat-expansion-panel [disabled]=\"!editedFeature\" [expanded]=\"editedFeature\">\r\n <mat-expansion-panel-header>\r\n <mat-panel-title>\r\n <span>{{ editedFeature?.name || \"feature\" }}</span>\r\n </mat-panel-title>\r\n </mat-expansion-panel-header>\r\n <gve-feature-editor\r\n [isVar]=\"isVar\"\r\n [featNames]=\"featNames\"\r\n [featValues]=\"featValues\"\r\n [feature]=\"editedFeature\"\r\n (featureChange)=\"onFeatureChange($event)\"\r\n (featureCancel)=\"onFeatureCancel()\"\r\n />\r\n </mat-expansion-panel>\r\n</div>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}.in-row-button{margin-top:-16px}table{width:100%;border-collapse:collapse;margin-top:8px}th{color:#909090;font-weight:400;text-align:left;background-color:#e1e0e0}th,td{padding:4px;border-bottom:1px solid silver}tbody tr:nth-child(2n){background-color:#e8e8e8}td.fit-width{width:1px;white-space:nowrap}\n"] }]
901
- }], ctorParameters: () => [{ type: i1.FormBuilder }], propDecorators: { isVar: [{
902
- type: Input
903
- }], featNames: [{
904
- type: Input
905
- }], featValues: [{
906
- type: Input
907
- }], filterThreshold: [{
908
- type: Input
909
- }], features: [{
910
- type: Input
911
- }], featuresChange: [{
912
- type: Output
913
- }] } });
844
+ ], template: "<div>\r\n <div class=\"form-row\">\r\n <!-- filter -->\r\n @if (filterThreshold() === 0 || features().length >= filterThreshold()) {\r\n <mat-form-field>\r\n <input matInput placeholder=\"filter\" [formControl]=\"filter\" />\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matSuffix\r\n (click)=\"filter.reset()\"\r\n [disabled]=\"!filter.value\"\r\n [attr.aria-label]=\"'Reset filter'\"\r\n >\r\n <mat-icon color=\"warn\" class=\"mat-warn\">cancel</mat-icon>\r\n </button>\r\n </mat-form-field>\r\n }\r\n\r\n <!-- add feature button -->\r\n <button\r\n type=\"button\"\r\n color=\"primary\"\r\n class=\"mat-primary\"\r\n mat-flat-button\r\n matTooltip=\"Add a new feature\"\r\n (click)=\"addFeature()\"\r\n [class.in-row-button]=\"\r\n filterThreshold() === 0 || features()!.length >= filterThreshold()\r\n \"\r\n >\r\n <mat-icon>add</mat-icon>\r\n feature\r\n </button>\r\n </div>\r\n\r\n <!-- list -->\r\n @if (features().length) {\r\n <table>\r\n <thead>\r\n <tr>\r\n <th></th>\r\n <th>feature</th>\r\n <th>value</th>\r\n <th>policy</th>\r\n @if (isVar()) {\r\n <th>negated</th>\r\n <th>global</th>\r\n <th>short-lived</th>\r\n }\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (feature of filteredFeatures; track feature) {\r\n <tr>\r\n <td class=\"fit-width\">\r\n <!-- edit -->\r\n <button\r\n type=\"button\"\r\n color=\"primary\"\r\n mat-icon-button\r\n matTooltip=\"Edit this feature\"\r\n (click)=\"editFeature(feature)\"\r\n >\r\n <mat-icon class=\"mat-primary\">edit</mat-icon>\r\n </button>\r\n <!-- delete -->\r\n <button\r\n type=\"button\"\r\n color=\"warn\"\r\n mat-icon-button\r\n matTooltip=\"Delete this feature\"\r\n (click)=\"deleteFeature(feature)\"\r\n >\r\n <mat-icon class=\"mat-warn\">delete</mat-icon>\r\n </button>\r\n </td>\r\n <td>\r\n @if (featNames()?.length) {\r\n <span>{{\r\n feature.name | flatLookup : featNames : \"id\" : \"label\"\r\n }}</span>\r\n } @else {\r\n <span>{{ feature.name }}</span>\r\n }\r\n </td>\r\n <td>\r\n @if (featValues()) {\r\n <span>{{\r\n feature.value | flatLookup : featValues()![feature.name]\r\n }}</span>\r\n } @else {\r\n <span>{{ feature.value }}</span>\r\n }\r\n </td>\r\n <td>\r\n <span>{{ POLICIES[feature.setPolicy] }}</span>\r\n </td>\r\n @if (isVar()) {\r\n <td>\r\n <span>{{ $any(feature).isNegated ? \"yes\" : \"no\" }}</span>\r\n </td>\r\n <td>\r\n <span>{{ $any(feature).isGlobal ? \"yes\" : \"no\" }}</span>\r\n </td>\r\n <td>\r\n <span>{{ $any(feature).isShortLived ? \"yes\" : \"no\" }}</span>\r\n </td>\r\n }\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n }\r\n\r\n <!-- editor -->\r\n <mat-expansion-panel [disabled]=\"!editedFeature\" [expanded]=\"editedFeature\">\r\n <mat-expansion-panel-header>\r\n <mat-panel-title>\r\n <span>{{ editedFeature?.name || \"feature\" }}</span>\r\n </mat-panel-title>\r\n </mat-expansion-panel-header>\r\n <gve-feature-editor\r\n [isVar]=\"isVar()\"\r\n [featNames]=\"featNames()\"\r\n [featValues]=\"featValues()\"\r\n [feature]=\"editedFeature\"\r\n (featureChange)=\"onFeatureChange($event)\"\r\n (featureCancel)=\"onFeatureCancel()\"\r\n />\r\n </mat-expansion-panel>\r\n</div>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}.in-row-button{margin-top:-16px}table{width:100%;border-collapse:collapse;margin-top:8px}th{color:#909090;font-weight:400;text-align:left;background-color:#e1e0e0}th,td{padding:4px;border-bottom:1px solid silver}tbody tr:nth-child(2n){background-color:#e8e8e8}td.fit-width{width:1px;white-space:nowrap}\n"] }]
845
+ }], ctorParameters: () => [{ type: i1.FormBuilder }] });
914
846
 
915
847
  /**
916
848
  * 🔑 `gve-base-text-editor`
@@ -922,34 +854,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImpor
922
854
  * Used by the `gve-snapshot-editor` component.
923
855
  */
924
856
  class BaseTextEditorComponent {
925
- /**
926
- * The text to edit. In input this can be a string, an array of `CharNode`
927
- * objects. In output this will be an array of `CharNode` objects.
928
- */
929
- get text() {
930
- return this._text;
931
- }
932
- set text(value) {
933
- this.resetSelectedChar();
934
- if (this._text === value) {
935
- return;
936
- }
937
- if (!value) {
938
- this._text = [];
939
- }
940
- else {
941
- this._text = Array.isArray(value)
942
- ? value
943
- : SnapshotViewService.stringToBaseChars(value);
944
- }
945
- }
946
857
  constructor(formBuilder, _dialogService) {
947
858
  this._dialogService = _dialogService;
948
- this._text = [];
859
+ /**
860
+ * The text to edit. In input this can be a string, an array of `CharNode`
861
+ * objects, or undefined. In output this will be an array of `CharNode`
862
+ * objects.
863
+ */
864
+ this.text = input.required({
865
+ transform: (value) => {
866
+ if (value === undefined) {
867
+ return undefined;
868
+ }
869
+ if (Array.isArray(value)) {
870
+ return value;
871
+ }
872
+ if (typeof value === 'string') {
873
+ return SnapshotViewService.stringToBaseChars(value);
874
+ }
875
+ return undefined;
876
+ },
877
+ });
949
878
  /**
950
879
  * Emitted for the edited text as an array of `CharNode`'s whenever it changes.
951
880
  */
952
- this.textChange = new EventEmitter();
881
+ this.textChange = output();
953
882
  this.userText = formBuilder.control('', {
954
883
  nonNullable: true,
955
884
  validators: [Validators.required, Validators.maxLength(10000)],
@@ -957,18 +886,31 @@ class BaseTextEditorComponent {
957
886
  this.form = formBuilder.group({
958
887
  userText: this.userText,
959
888
  });
889
+ effect(() => {
890
+ this.resetSelectedChar();
891
+ if (!this.text()) {
892
+ this.userText.reset();
893
+ }
894
+ else {
895
+ this.userText.setValue(this.text()
896
+ .map((c) => c.label)
897
+ .join(''));
898
+ this.userText.markAsPristine();
899
+ this.userText.updateValueAndValidity();
900
+ }
901
+ });
960
902
  }
961
903
  resetSelectedChar() {
962
904
  this.selectedChar = undefined;
963
905
  this.selectedCharLabel = undefined;
964
906
  }
965
907
  onSelectedChar(event) {
966
- this.selectedChar = this._text.find((c) => c.id === event.char.id);
908
+ this.selectedChar = this.text().find((c) => c.id === event.char.id);
967
909
  this.selectedCharLabel = event.char.label;
968
910
  }
969
911
  onFeaturesChange(features) {
970
912
  this.selectedChar.features = features;
971
- this.textChange.emit(this._text);
913
+ this.textChange.emit(this.text());
972
914
  }
973
915
  onRangePick(range) {
974
916
  this.textRange = range;
@@ -981,15 +923,14 @@ class BaseTextEditorComponent {
981
923
  .confirm('Confirm', 'Reset text?')
982
924
  .subscribe((yes) => {
983
925
  if (yes) {
984
- this.text = this.userText.value;
985
- this.textChange.emit(this._text);
926
+ this.textChange.emit(SnapshotViewService.stringToBaseChars(this.userText.value));
986
927
  }
987
928
  });
988
929
  }
989
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: BaseTextEditorComponent, deps: [{ token: i1.FormBuilder }, { token: i2$2.DialogService }], target: i0.ɵɵFactoryTarget.Component }); }
990
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.0", type: BaseTextEditorComponent, isStandalone: true, selector: "gve-base-text-editor", inputs: { text: "text" }, outputs: { textChange: "textChange" }, ngImport: i0, template: "<form [formGroup]=\"form\" (submit)=\"setTextFromUser()\">\r\n <!-- text -->\r\n <div>\r\n <mat-form-field class=\"full-width\">\r\n <mat-label>text</mat-label>\r\n <textarea matInput [formControl]=\"userText\" rows=\"5\"></textarea>\r\n <mat-error\r\n *ngIf=\"\r\n $any(userText).errors?.required &&\r\n (userText.dirty || userText.touched)\r\n \"\r\n >text required</mat-error\r\n >\r\n <mat-error\r\n *ngIf=\"\r\n $any(userText).errors?.maxLength &&\r\n (userText.dirty || userText.touched)\r\n \"\r\n >text too long</mat-error\r\n >\r\n </mat-form-field>\r\n </div>\r\n <!-- buttons -->\r\n <div class=\"form-row\">\r\n <!-- set -->\r\n <button\r\n type=\"button\"\r\n mat-flat-button\r\n class=\"mat-warn\"\r\n matTooltip=\"Reset characters to newly entered text\"\r\n [disabled]=\"!userText.value\"\r\n (click)=\"setTextFromUser()\"\r\n >\r\n set\r\n </button>\r\n <!-- patch: TODO -->\r\n <!-- <button\r\n type=\"button\"\r\n mat-flat-button\r\n class=\"mat-primary\"\r\n matTooltip=\"Patch characters with newly entered text\"\r\n [disabled]=\"!userText.value\"\r\n (click)=\"patchTextFromUser()\"\r\n >\r\n patch\r\n </button> -->\r\n </div>\r\n\r\n <!-- base text -->\r\n <div id=\"text-view\">\r\n <gve-base-text-view\r\n [text]=\"text\"\r\n (charPick)=\"onSelectedChar($event)\"\r\n (rangePick)=\"textRange = $event\"\r\n />\r\n\r\n <!-- text range -->\r\n @if (textRange) {\r\n <div id=\"text-range\">{{ textRange.at }} \u00D7 {{ textRange.run }}</div>\r\n }\r\n </div>\r\n\r\n <!-- char features -->\r\n @if (selectedChar) {\r\n <fieldset>\r\n <legend>features</legend>\r\n <gve-feature-set-editor\r\n [features]=\"selectedChar.features || []\"\r\n (featuresChange)=\"onFeaturesChange($event)\"\r\n />\r\n </fieldset>\r\n }\r\n</form>\r\n", styles: [".full-width{width:100%}#text-view{margin:8px 0}fieldset{border:1px solid #ccc;padding:10px;margin:10px 0;border-radius:5px}legend{color:silver}.form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "directive", type: i5.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i10.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: BaseTextViewComponent, selector: "gve-base-text-view", inputs: ["defaultColor", "defaultBorderColor", "selectionColor", "hasLineNumber", "text"], outputs: ["charPick", "rangePick"] }, { kind: "component", type: FeatureSetEditorComponent, selector: "gve-feature-set-editor", inputs: ["isVar", "featNames", "featValues", "filterThreshold", "features"], outputs: ["featuresChange"] }] }); }
930
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: BaseTextEditorComponent, deps: [{ token: i1.FormBuilder }, { token: i2$1.DialogService }], target: i0.ɵɵFactoryTarget.Component }); }
931
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.6", type: BaseTextEditorComponent, isStandalone: true, selector: "gve-base-text-editor", inputs: { text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { textChange: "textChange" }, ngImport: i0, template: "<form [formGroup]=\"form\" (submit)=\"setTextFromUser()\">\r\n <!-- text -->\r\n <div>\r\n <mat-form-field class=\"full-width\">\r\n <mat-label>text</mat-label>\r\n <textarea matInput [formControl]=\"userText\" rows=\"5\"></textarea>\r\n <mat-error\r\n *ngIf=\"\r\n $any(userText).errors?.required &&\r\n (userText.dirty || userText.touched)\r\n \"\r\n >text required</mat-error\r\n >\r\n <mat-error\r\n *ngIf=\"\r\n $any(userText).errors?.maxLength &&\r\n (userText.dirty || userText.touched)\r\n \"\r\n >text too long</mat-error\r\n >\r\n </mat-form-field>\r\n </div>\r\n <!-- buttons -->\r\n <div class=\"form-row\">\r\n <!-- set -->\r\n <button\r\n type=\"button\"\r\n mat-flat-button\r\n class=\"mat-warn\"\r\n matTooltip=\"Reset characters to newly entered text\"\r\n [disabled]=\"!userText.value\"\r\n (click)=\"setTextFromUser()\"\r\n >\r\n set\r\n </button>\r\n <!-- patch: TODO -->\r\n <!-- <button\r\n type=\"button\"\r\n mat-flat-button\r\n class=\"mat-primary\"\r\n matTooltip=\"Patch characters with newly entered text\"\r\n [disabled]=\"!userText.value\"\r\n (click)=\"patchTextFromUser()\"\r\n >\r\n patch\r\n </button> -->\r\n </div>\r\n\r\n <!-- base text -->\r\n <div id=\"text-view\">\r\n <gve-base-text-view\r\n [text]=\"text() || []\"\r\n (charPick)=\"onSelectedChar($event)\"\r\n (rangePick)=\"textRange = $event\"\r\n />\r\n\r\n <!-- text range -->\r\n @if (textRange) {\r\n <div id=\"text-range\">{{ textRange.at }} \u00D7 {{ textRange.run }}</div>\r\n }\r\n </div>\r\n\r\n <!-- char features -->\r\n @if (selectedChar) {\r\n <fieldset>\r\n <legend>features</legend>\r\n <gve-feature-set-editor\r\n [features]=\"selectedChar.features || []\"\r\n (featuresChange)=\"onFeaturesChange($event)\"\r\n />\r\n </fieldset>\r\n }\r\n</form>\r\n", styles: [".full-width{width:100%}#text-view{margin:8px 0}fieldset{border:1px solid #ccc;padding:10px;margin:10px 0;border-radius:5px}legend{color:silver}.form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "directive", type: i5.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i10.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: BaseTextViewComponent, selector: "gve-base-text-view", inputs: ["defaultColor", "defaultBorderColor", "selectionColor", "hasLineNumber", "text", "borderColorCallback"], outputs: ["charPick", "rangePick"] }, { kind: "component", type: FeatureSetEditorComponent, selector: "gve-feature-set-editor", inputs: ["isVar", "featNames", "featValues", "filterThreshold", "features"], outputs: ["featuresChange"] }] }); }
991
932
  }
992
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: BaseTextEditorComponent, decorators: [{
933
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: BaseTextEditorComponent, decorators: [{
993
934
  type: Component,
994
935
  args: [{ selector: 'gve-base-text-editor', imports: [
995
936
  CommonModule,
@@ -1001,12 +942,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImpor
1001
942
  MatTooltipModule,
1002
943
  BaseTextViewComponent,
1003
944
  FeatureSetEditorComponent,
1004
- ], template: "<form [formGroup]=\"form\" (submit)=\"setTextFromUser()\">\r\n <!-- text -->\r\n <div>\r\n <mat-form-field class=\"full-width\">\r\n <mat-label>text</mat-label>\r\n <textarea matInput [formControl]=\"userText\" rows=\"5\"></textarea>\r\n <mat-error\r\n *ngIf=\"\r\n $any(userText).errors?.required &&\r\n (userText.dirty || userText.touched)\r\n \"\r\n >text required</mat-error\r\n >\r\n <mat-error\r\n *ngIf=\"\r\n $any(userText).errors?.maxLength &&\r\n (userText.dirty || userText.touched)\r\n \"\r\n >text too long</mat-error\r\n >\r\n </mat-form-field>\r\n </div>\r\n <!-- buttons -->\r\n <div class=\"form-row\">\r\n <!-- set -->\r\n <button\r\n type=\"button\"\r\n mat-flat-button\r\n class=\"mat-warn\"\r\n matTooltip=\"Reset characters to newly entered text\"\r\n [disabled]=\"!userText.value\"\r\n (click)=\"setTextFromUser()\"\r\n >\r\n set\r\n </button>\r\n <!-- patch: TODO -->\r\n <!-- <button\r\n type=\"button\"\r\n mat-flat-button\r\n class=\"mat-primary\"\r\n matTooltip=\"Patch characters with newly entered text\"\r\n [disabled]=\"!userText.value\"\r\n (click)=\"patchTextFromUser()\"\r\n >\r\n patch\r\n </button> -->\r\n </div>\r\n\r\n <!-- base text -->\r\n <div id=\"text-view\">\r\n <gve-base-text-view\r\n [text]=\"text\"\r\n (charPick)=\"onSelectedChar($event)\"\r\n (rangePick)=\"textRange = $event\"\r\n />\r\n\r\n <!-- text range -->\r\n @if (textRange) {\r\n <div id=\"text-range\">{{ textRange.at }} \u00D7 {{ textRange.run }}</div>\r\n }\r\n </div>\r\n\r\n <!-- char features -->\r\n @if (selectedChar) {\r\n <fieldset>\r\n <legend>features</legend>\r\n <gve-feature-set-editor\r\n [features]=\"selectedChar.features || []\"\r\n (featuresChange)=\"onFeaturesChange($event)\"\r\n />\r\n </fieldset>\r\n }\r\n</form>\r\n", styles: [".full-width{width:100%}#text-view{margin:8px 0}fieldset{border:1px solid #ccc;padding:10px;margin:10px 0;border-radius:5px}legend{color:silver}.form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}\n"] }]
1005
- }], ctorParameters: () => [{ type: i1.FormBuilder }, { type: i2$2.DialogService }], propDecorators: { text: [{
1006
- type: Input
1007
- }], textChange: [{
1008
- type: Output
1009
- }] } });
945
+ ], template: "<form [formGroup]=\"form\" (submit)=\"setTextFromUser()\">\r\n <!-- text -->\r\n <div>\r\n <mat-form-field class=\"full-width\">\r\n <mat-label>text</mat-label>\r\n <textarea matInput [formControl]=\"userText\" rows=\"5\"></textarea>\r\n <mat-error\r\n *ngIf=\"\r\n $any(userText).errors?.required &&\r\n (userText.dirty || userText.touched)\r\n \"\r\n >text required</mat-error\r\n >\r\n <mat-error\r\n *ngIf=\"\r\n $any(userText).errors?.maxLength &&\r\n (userText.dirty || userText.touched)\r\n \"\r\n >text too long</mat-error\r\n >\r\n </mat-form-field>\r\n </div>\r\n <!-- buttons -->\r\n <div class=\"form-row\">\r\n <!-- set -->\r\n <button\r\n type=\"button\"\r\n mat-flat-button\r\n class=\"mat-warn\"\r\n matTooltip=\"Reset characters to newly entered text\"\r\n [disabled]=\"!userText.value\"\r\n (click)=\"setTextFromUser()\"\r\n >\r\n set\r\n </button>\r\n <!-- patch: TODO -->\r\n <!-- <button\r\n type=\"button\"\r\n mat-flat-button\r\n class=\"mat-primary\"\r\n matTooltip=\"Patch characters with newly entered text\"\r\n [disabled]=\"!userText.value\"\r\n (click)=\"patchTextFromUser()\"\r\n >\r\n patch\r\n </button> -->\r\n </div>\r\n\r\n <!-- base text -->\r\n <div id=\"text-view\">\r\n <gve-base-text-view\r\n [text]=\"text() || []\"\r\n (charPick)=\"onSelectedChar($event)\"\r\n (rangePick)=\"textRange = $event\"\r\n />\r\n\r\n <!-- text range -->\r\n @if (textRange) {\r\n <div id=\"text-range\">{{ textRange.at }} \u00D7 {{ textRange.run }}</div>\r\n }\r\n </div>\r\n\r\n <!-- char features -->\r\n @if (selectedChar) {\r\n <fieldset>\r\n <legend>features</legend>\r\n <gve-feature-set-editor\r\n [features]=\"selectedChar.features || []\"\r\n (featuresChange)=\"onFeaturesChange($event)\"\r\n />\r\n </fieldset>\r\n }\r\n</form>\r\n", styles: [".full-width{width:100%}#text-view{margin:8px 0}fieldset{border:1px solid #ccc;padding:10px;margin:10px 0;border-radius:5px}legend{color:silver}.form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}\n"] }]
946
+ }], ctorParameters: () => [{ type: i1.FormBuilder }, { type: i2$1.DialogService }] });
1010
947
 
1011
948
  /**
1012
949
  * Service to interact with the GVE API.
@@ -1058,15 +995,32 @@ class GveApiService {
1058
995
  })
1059
996
  .pipe(catchError(this._error.handleError));
1060
997
  }
1061
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: GveApiService, deps: [{ token: i1$1.HttpClient }, { token: i2$1.ErrorService }, { token: i2$1.EnvService }], target: i0.ɵɵFactoryTarget.Injectable }); }
1062
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: GveApiService, providedIn: 'root' }); }
998
+ /**
999
+ * Get the chain corresponding to the operations run on the
1000
+ * specified text. This is used for diagnostic or demonstration
1001
+ * purposes.
1002
+ *
1003
+ * @param text The base text.
1004
+ * @param operations The operations to run.
1005
+ * @returns Result wrapper.
1006
+ */
1007
+ getChain(text, operations) {
1008
+ return this._http
1009
+ .post(`${this._env.get('apiUrl')}text/operations/run/chain`, {
1010
+ text,
1011
+ operations,
1012
+ })
1013
+ .pipe(catchError(this._error.handleError));
1014
+ }
1015
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: GveApiService, deps: [{ token: i1$1.HttpClient }, { token: i2$2.ErrorService }, { token: i2$2.EnvService }], target: i0.ɵɵFactoryTarget.Injectable }); }
1016
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: GveApiService, providedIn: 'root' }); }
1063
1017
  }
1064
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: GveApiService, decorators: [{
1018
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: GveApiService, decorators: [{
1065
1019
  type: Injectable,
1066
1020
  args: [{
1067
1021
  providedIn: 'root',
1068
1022
  }]
1069
- }], ctorParameters: () => [{ type: i1$1.HttpClient }, { type: i2$1.ErrorService }, { type: i2$1.EnvService }] });
1023
+ }], ctorParameters: () => [{ type: i1$1.HttpClient }, { type: i2$2.ErrorService }, { type: i2$2.EnvService }] });
1070
1024
 
1071
1025
  /**
1072
1026
  * 🔑 `gve-batch-operation-editor`
@@ -1078,30 +1032,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImpor
1078
1032
  * - 🔥 `operationsChange` (`CharChainOperation[]`): event emitted when operations change.
1079
1033
  */
1080
1034
  class BatchOperationEditorComponent {
1081
- /**
1082
- * The preset text to parse if any. Usually you start with a blank
1083
- * text, but sometimes you might want to pre-set it.
1084
- */
1085
- get preset() {
1086
- return this._preset;
1087
- }
1088
- set preset(value) {
1089
- if (this._preset === value) {
1090
- return;
1091
- }
1092
- this._preset = value || undefined;
1093
- this.parseOperations();
1094
- }
1095
1035
  constructor(formBuilder, _api,
1096
1036
  // this component can be used as a dialog
1097
1037
  dialogRef, data) {
1098
1038
  this._api = _api;
1099
1039
  this.dialogRef = dialogRef;
1100
1040
  this.data = data;
1041
+ /**
1042
+ * The preset text to parse if any. Usually you start with a blank
1043
+ * text, but sometimes you might want to pre-set it.
1044
+ */
1045
+ this.preset = input();
1101
1046
  /**
1102
1047
  * Emitted when operations change.
1103
1048
  */
1104
- this.operationsChange = new EventEmitter();
1049
+ this.operationsChange = output();
1105
1050
  this.text = formBuilder.control(null, [
1106
1051
  Validators.required,
1107
1052
  Validators.maxLength(2000),
@@ -1109,24 +1054,30 @@ class BatchOperationEditorComponent {
1109
1054
  this.form = formBuilder.group({
1110
1055
  inputOps: this.text,
1111
1056
  });
1057
+ effect(() => {
1058
+ this.parseOperations(this.preset() || undefined);
1059
+ });
1112
1060
  }
1113
1061
  ngOnInit() {
1114
1062
  if (this.data?.payload?.preset) {
1115
- this.preset = this.data.payload.preset;
1116
- this.parseOperations();
1063
+ this.parseOperations(this.data?.payload?.preset);
1117
1064
  }
1118
1065
  }
1119
- parseOperations() {
1120
- if (this.busy || !this.text.value) {
1066
+ parseOperations(text) {
1067
+ if (this.busy || !text) {
1121
1068
  return;
1122
1069
  }
1123
1070
  this.busy = true;
1124
1071
  this.parseError = undefined;
1125
- this._api.parseOperations(this.text.value).subscribe({
1072
+ this._api.parseOperations(text).subscribe({
1126
1073
  next: (wrapper) => {
1127
- this.operationsChange.emit(wrapper.result);
1128
- if (!this.preset) {
1129
- this.dialogRef?.close(wrapper.result);
1074
+ if (wrapper.error) {
1075
+ this.parseError = wrapper.error;
1076
+ return;
1077
+ }
1078
+ this.operationsChange.emit(wrapper.result || []);
1079
+ if (this.dialogRef) {
1080
+ this.dialogRef.close(wrapper.result);
1130
1081
  }
1131
1082
  },
1132
1083
  error: (error) => {
@@ -1140,10 +1091,10 @@ class BatchOperationEditorComponent {
1140
1091
  close() {
1141
1092
  this.dialogRef?.close();
1142
1093
  }
1143
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: BatchOperationEditorComponent, deps: [{ token: i1.FormBuilder }, { token: GveApiService }, { token: i3$1.MatDialogRef, optional: true }, { token: MAT_DIALOG_DATA, optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
1144
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.0", type: BatchOperationEditorComponent, isStandalone: true, selector: "gve-batch-operation-editor", inputs: { preset: "preset" }, outputs: { operationsChange: "operationsChange" }, ngImport: i0, template: "<div [style.padding]=\"dialogRef ? '8px' : '0'\">\r\n @if (dialogRef) {\r\n <div id=\"heading\">\r\n <h2>Add Operations</h2>\r\n </div>\r\n }\r\n <fieldset>\r\n <div id=\"batch-input\">\r\n <div>\r\n <mat-form-field class=\"full-width\">\r\n <mat-label>operations</mat-label>\r\n <textarea\r\n class=\"code\"\r\n matInput\r\n [formControl]=\"text\"\r\n rows=\"8\"\r\n spellcheck=\"false\"\r\n ></textarea>\r\n </mat-form-field>\r\n @if (parseError) {\r\n <div class=\"error\">{{ parseError }}</div>\r\n }\r\n </div>\r\n </div>\r\n <div id=\"batch-help\">\r\n <ul>\r\n <li>\r\n <strong>replace</strong>:\r\n <code>ATxRUN<strong>=</strong>\"VALUE\"</code>\r\n </li>\r\n <li>\r\n <strong>delete</strong>:\r\n <code>ATxRUN<strong>-</strong></code>\r\n </li>\r\n <li>\r\n <strong>add-before</strong>:\r\n <code>ATxRUN<strong>+[</strong>\"VALUE\"</code>\r\n </li>\r\n <li>\r\n <strong>add-after</strong>:\r\n <code>ATxRUN<strong>+]</strong>\"VALUE\"</code>\r\n </li>\r\n <li>\r\n <strong>move-before</strong>:\r\n <code>ATxRUN<strong>&gt;[</strong>TO</code>\r\n </li>\r\n <li>\r\n <strong>move-after</strong>:\r\n <code>ATxRUN<strong>&gt;]</strong>TO</code>\r\n </li>\r\n <li>\r\n <strong>swap</strong>:\r\n <code>ATxRUN<strong>&lt;&gt;</strong>TOxRUN</code>\r\n </li>\r\n <li>\r\n <strong>annotate</strong>:\r\n <code>ATxRUN<strong>:</strong></code>\r\n </li>\r\n </ul>\r\n <p>For all the operations:</p>\r\n <ul>\r\n <li>\r\n prefix <code>(ITAG:OTAG)</code> to define input and/or output tags,\r\n separated by colon.\r\n </li>\r\n <li>\r\n prepend <code>&#x40;</code> to AT to use character indexes (0-N)\r\n rather than IDs.\r\n </li>\r\n <li>RUN (where applicable) defaults to 1.</li>\r\n <li>\r\n append features like <code>[NAME OPERATOR VALUE]</code>, separated by\r\n space, where:\r\n </li>\r\n <li>\r\n <ol>\r\n <li>\r\n NAME is an arbitrary string. Prefixes:\r\n <code>*</code>=global features, <code>!</code>=remove feature (no\r\n value). Suffixes: <code>^</code>=short-lived (like\r\n <code>*version^=alpha</code>).\r\n </li>\r\n <li>\r\n OPERATOR is <code>=</code> (multiple), <code>:=</code> (single),\r\n <code>==</code> (first-single).\r\n </li>\r\n <li>\r\n VALUE is a string. If it includes spaces, wrap it in\r\n <code>\"\"</code>.\r\n </li>\r\n </ol>\r\n </li>\r\n </ul>\r\n </div>\r\n <div class=\"form-row-center\">\r\n @if (dialogRef) {\r\n <button\r\n type=\"button\"\r\n class=\"mat-warn\"\r\n mat-flat-button\r\n matTooltip=\"Close dialog\"\r\n (click)=\"close()\"\r\n >\r\n close\r\n </button>\r\n }\r\n <button\r\n type=\"button\"\r\n class=\"mat-primary\"\r\n mat-flat-button\r\n matTooltip=\"Parse text into operations\"\r\n [disabled]=\"!text.value || busy\"\r\n (click)=\"parseOperations()\"\r\n >\r\n batch add\r\n </button>\r\n </div>\r\n </fieldset>\r\n</div>\r\n", styles: [".full-width{width:100%}.error{color:red}.form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row-center{display:flex;gap:8px;align-items:center;justify-content:center;flex-wrap:wrap}.form-row,.form-row-center *{flex:0 0 auto}div#heading{margin:8px;text-align:center}div#batch-help strong{color:#ff8c00}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatIconModule }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }] }); }
1094
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: BatchOperationEditorComponent, deps: [{ token: i1.FormBuilder }, { token: GveApiService }, { token: i3$1.MatDialogRef, optional: true }, { token: MAT_DIALOG_DATA, optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
1095
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.6", type: BatchOperationEditorComponent, isStandalone: true, selector: "gve-batch-operation-editor", inputs: { preset: { classPropertyName: "preset", publicName: "preset", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { operationsChange: "operationsChange" }, ngImport: i0, template: "<div [style.padding]=\"dialogRef ? '8px' : '0'\">\r\n @if (dialogRef) {\r\n <div id=\"heading\">\r\n <h2>Add Operations</h2>\r\n </div>\r\n }\r\n <fieldset>\r\n <div id=\"batch-input\">\r\n <div>\r\n <mat-form-field class=\"full-width\">\r\n <mat-label>operations</mat-label>\r\n <textarea\r\n class=\"code\"\r\n matInput\r\n [formControl]=\"text\"\r\n rows=\"8\"\r\n spellcheck=\"false\"\r\n ></textarea>\r\n </mat-form-field>\r\n @if (parseError) {\r\n <div class=\"error\">{{ parseError }}</div>\r\n }\r\n </div>\r\n </div>\r\n <div id=\"batch-help\">\r\n <ul>\r\n <li>\r\n <strong>replace</strong>:\r\n <code>ATxRUN<strong>=</strong>\"VALUE\"</code>\r\n </li>\r\n <li>\r\n <strong>delete</strong>:\r\n <code>ATxRUN<strong>-</strong></code>\r\n </li>\r\n <li>\r\n <strong>add-before</strong>:\r\n <code>ATxRUN<strong>+[</strong>\"VALUE\"</code>\r\n </li>\r\n <li>\r\n <strong>add-after</strong>:\r\n <code>ATxRUN<strong>+]</strong>\"VALUE\"</code>\r\n </li>\r\n <li>\r\n <strong>move-before</strong>:\r\n <code>ATxRUN<strong>&gt;[</strong>TO</code>\r\n </li>\r\n <li>\r\n <strong>move-after</strong>:\r\n <code>ATxRUN<strong>&gt;]</strong>TO</code>\r\n </li>\r\n <li>\r\n <strong>swap</strong>:\r\n <code>ATxRUN<strong>&lt;&gt;</strong>TOxRUN</code>\r\n </li>\r\n <li>\r\n <strong>annotate</strong>:\r\n <code>ATxRUN<strong>:</strong></code>\r\n </li>\r\n </ul>\r\n <p>For all the operations:</p>\r\n <ul>\r\n <li>\r\n prefix <code>(ITAG:OTAG)</code> to define input and/or output tags,\r\n separated by colon.\r\n </li>\r\n <li>\r\n prepend <code>&#x40;</code> to AT to use character indexes (0-N)\r\n rather than IDs.\r\n </li>\r\n <li>RUN (where applicable) defaults to 1.</li>\r\n <li>\r\n append features like <code>[NAME OPERATOR VALUE]</code>, separated by\r\n space, where:\r\n </li>\r\n <li>\r\n <ol>\r\n <li>\r\n NAME is an arbitrary string. Prefixes:\r\n <code>*</code>=global features, <code>!</code>=remove feature (no\r\n value). Suffixes: <code>^</code>=short-lived (like\r\n <code>*version^=alpha</code>).\r\n </li>\r\n <li>\r\n OPERATOR is <code>=</code> (multiple), <code>:=</code> (single),\r\n <code>==</code> (first-single).\r\n </li>\r\n <li>\r\n VALUE is a string. If it includes spaces, wrap it in\r\n <code>\"\"</code>.\r\n </li>\r\n </ol>\r\n </li>\r\n </ul>\r\n </div>\r\n <div class=\"form-row-center\">\r\n @if (dialogRef) {\r\n <button\r\n type=\"button\"\r\n class=\"mat-warn\"\r\n mat-flat-button\r\n matTooltip=\"Close dialog\"\r\n (click)=\"close()\"\r\n >\r\n close\r\n </button>\r\n }\r\n <button\r\n type=\"button\"\r\n class=\"mat-primary\"\r\n mat-flat-button\r\n matTooltip=\"Parse text into operations\"\r\n [disabled]=\"!text.value || busy\"\r\n (click)=\"parseOperations(text.value!)\"\r\n >\r\n batch add\r\n </button>\r\n </div>\r\n </fieldset>\r\n</div>\r\n", styles: [".full-width{width:100%}.error{color:red}.form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row-center{display:flex;gap:8px;align-items:center;justify-content:center;flex-wrap:wrap}.form-row,.form-row-center *{flex:0 0 auto}div#heading{margin:8px;text-align:center}div#batch-help strong{color:#ff8c00}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatIconModule }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }] }); }
1145
1096
  }
1146
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: BatchOperationEditorComponent, decorators: [{
1097
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: BatchOperationEditorComponent, decorators: [{
1147
1098
  type: Component,
1148
1099
  args: [{ selector: 'gve-batch-operation-editor', imports: [
1149
1100
  ReactiveFormsModule,
@@ -1151,7 +1102,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImpor
1151
1102
  MatFormFieldModule,
1152
1103
  MatIconModule,
1153
1104
  MatInputModule,
1154
- ], template: "<div [style.padding]=\"dialogRef ? '8px' : '0'\">\r\n @if (dialogRef) {\r\n <div id=\"heading\">\r\n <h2>Add Operations</h2>\r\n </div>\r\n }\r\n <fieldset>\r\n <div id=\"batch-input\">\r\n <div>\r\n <mat-form-field class=\"full-width\">\r\n <mat-label>operations</mat-label>\r\n <textarea\r\n class=\"code\"\r\n matInput\r\n [formControl]=\"text\"\r\n rows=\"8\"\r\n spellcheck=\"false\"\r\n ></textarea>\r\n </mat-form-field>\r\n @if (parseError) {\r\n <div class=\"error\">{{ parseError }}</div>\r\n }\r\n </div>\r\n </div>\r\n <div id=\"batch-help\">\r\n <ul>\r\n <li>\r\n <strong>replace</strong>:\r\n <code>ATxRUN<strong>=</strong>\"VALUE\"</code>\r\n </li>\r\n <li>\r\n <strong>delete</strong>:\r\n <code>ATxRUN<strong>-</strong></code>\r\n </li>\r\n <li>\r\n <strong>add-before</strong>:\r\n <code>ATxRUN<strong>+[</strong>\"VALUE\"</code>\r\n </li>\r\n <li>\r\n <strong>add-after</strong>:\r\n <code>ATxRUN<strong>+]</strong>\"VALUE\"</code>\r\n </li>\r\n <li>\r\n <strong>move-before</strong>:\r\n <code>ATxRUN<strong>&gt;[</strong>TO</code>\r\n </li>\r\n <li>\r\n <strong>move-after</strong>:\r\n <code>ATxRUN<strong>&gt;]</strong>TO</code>\r\n </li>\r\n <li>\r\n <strong>swap</strong>:\r\n <code>ATxRUN<strong>&lt;&gt;</strong>TOxRUN</code>\r\n </li>\r\n <li>\r\n <strong>annotate</strong>:\r\n <code>ATxRUN<strong>:</strong></code>\r\n </li>\r\n </ul>\r\n <p>For all the operations:</p>\r\n <ul>\r\n <li>\r\n prefix <code>(ITAG:OTAG)</code> to define input and/or output tags,\r\n separated by colon.\r\n </li>\r\n <li>\r\n prepend <code>&#x40;</code> to AT to use character indexes (0-N)\r\n rather than IDs.\r\n </li>\r\n <li>RUN (where applicable) defaults to 1.</li>\r\n <li>\r\n append features like <code>[NAME OPERATOR VALUE]</code>, separated by\r\n space, where:\r\n </li>\r\n <li>\r\n <ol>\r\n <li>\r\n NAME is an arbitrary string. Prefixes:\r\n <code>*</code>=global features, <code>!</code>=remove feature (no\r\n value). Suffixes: <code>^</code>=short-lived (like\r\n <code>*version^=alpha</code>).\r\n </li>\r\n <li>\r\n OPERATOR is <code>=</code> (multiple), <code>:=</code> (single),\r\n <code>==</code> (first-single).\r\n </li>\r\n <li>\r\n VALUE is a string. If it includes spaces, wrap it in\r\n <code>\"\"</code>.\r\n </li>\r\n </ol>\r\n </li>\r\n </ul>\r\n </div>\r\n <div class=\"form-row-center\">\r\n @if (dialogRef) {\r\n <button\r\n type=\"button\"\r\n class=\"mat-warn\"\r\n mat-flat-button\r\n matTooltip=\"Close dialog\"\r\n (click)=\"close()\"\r\n >\r\n close\r\n </button>\r\n }\r\n <button\r\n type=\"button\"\r\n class=\"mat-primary\"\r\n mat-flat-button\r\n matTooltip=\"Parse text into operations\"\r\n [disabled]=\"!text.value || busy\"\r\n (click)=\"parseOperations()\"\r\n >\r\n batch add\r\n </button>\r\n </div>\r\n </fieldset>\r\n</div>\r\n", styles: [".full-width{width:100%}.error{color:red}.form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row-center{display:flex;gap:8px;align-items:center;justify-content:center;flex-wrap:wrap}.form-row,.form-row-center *{flex:0 0 auto}div#heading{margin:8px;text-align:center}div#batch-help strong{color:#ff8c00}\n"] }]
1105
+ ], template: "<div [style.padding]=\"dialogRef ? '8px' : '0'\">\r\n @if (dialogRef) {\r\n <div id=\"heading\">\r\n <h2>Add Operations</h2>\r\n </div>\r\n }\r\n <fieldset>\r\n <div id=\"batch-input\">\r\n <div>\r\n <mat-form-field class=\"full-width\">\r\n <mat-label>operations</mat-label>\r\n <textarea\r\n class=\"code\"\r\n matInput\r\n [formControl]=\"text\"\r\n rows=\"8\"\r\n spellcheck=\"false\"\r\n ></textarea>\r\n </mat-form-field>\r\n @if (parseError) {\r\n <div class=\"error\">{{ parseError }}</div>\r\n }\r\n </div>\r\n </div>\r\n <div id=\"batch-help\">\r\n <ul>\r\n <li>\r\n <strong>replace</strong>:\r\n <code>ATxRUN<strong>=</strong>\"VALUE\"</code>\r\n </li>\r\n <li>\r\n <strong>delete</strong>:\r\n <code>ATxRUN<strong>-</strong></code>\r\n </li>\r\n <li>\r\n <strong>add-before</strong>:\r\n <code>ATxRUN<strong>+[</strong>\"VALUE\"</code>\r\n </li>\r\n <li>\r\n <strong>add-after</strong>:\r\n <code>ATxRUN<strong>+]</strong>\"VALUE\"</code>\r\n </li>\r\n <li>\r\n <strong>move-before</strong>:\r\n <code>ATxRUN<strong>&gt;[</strong>TO</code>\r\n </li>\r\n <li>\r\n <strong>move-after</strong>:\r\n <code>ATxRUN<strong>&gt;]</strong>TO</code>\r\n </li>\r\n <li>\r\n <strong>swap</strong>:\r\n <code>ATxRUN<strong>&lt;&gt;</strong>TOxRUN</code>\r\n </li>\r\n <li>\r\n <strong>annotate</strong>:\r\n <code>ATxRUN<strong>:</strong></code>\r\n </li>\r\n </ul>\r\n <p>For all the operations:</p>\r\n <ul>\r\n <li>\r\n prefix <code>(ITAG:OTAG)</code> to define input and/or output tags,\r\n separated by colon.\r\n </li>\r\n <li>\r\n prepend <code>&#x40;</code> to AT to use character indexes (0-N)\r\n rather than IDs.\r\n </li>\r\n <li>RUN (where applicable) defaults to 1.</li>\r\n <li>\r\n append features like <code>[NAME OPERATOR VALUE]</code>, separated by\r\n space, where:\r\n </li>\r\n <li>\r\n <ol>\r\n <li>\r\n NAME is an arbitrary string. Prefixes:\r\n <code>*</code>=global features, <code>!</code>=remove feature (no\r\n value). Suffixes: <code>^</code>=short-lived (like\r\n <code>*version^=alpha</code>).\r\n </li>\r\n <li>\r\n OPERATOR is <code>=</code> (multiple), <code>:=</code> (single),\r\n <code>==</code> (first-single).\r\n </li>\r\n <li>\r\n VALUE is a string. If it includes spaces, wrap it in\r\n <code>\"\"</code>.\r\n </li>\r\n </ol>\r\n </li>\r\n </ul>\r\n </div>\r\n <div class=\"form-row-center\">\r\n @if (dialogRef) {\r\n <button\r\n type=\"button\"\r\n class=\"mat-warn\"\r\n mat-flat-button\r\n matTooltip=\"Close dialog\"\r\n (click)=\"close()\"\r\n >\r\n close\r\n </button>\r\n }\r\n <button\r\n type=\"button\"\r\n class=\"mat-primary\"\r\n mat-flat-button\r\n matTooltip=\"Parse text into operations\"\r\n [disabled]=\"!text.value || busy\"\r\n (click)=\"parseOperations(text.value!)\"\r\n >\r\n batch add\r\n </button>\r\n </div>\r\n </fieldset>\r\n</div>\r\n", styles: [".full-width{width:100%}.error{color:red}.form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row-center{display:flex;gap:8px;align-items:center;justify-content:center;flex-wrap:wrap}.form-row,.form-row-center *{flex:0 0 auto}div#heading{margin:8px;text-align:center}div#batch-help strong{color:#ff8c00}\n"] }]
1155
1106
  }], ctorParameters: () => [{ type: i1.FormBuilder }, { type: GveApiService }, { type: i3$1.MatDialogRef, decorators: [{
1156
1107
  type: Optional
1157
1108
  }] }, { type: undefined, decorators: [{
@@ -1159,11 +1110,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImpor
1159
1110
  }, {
1160
1111
  type: Inject,
1161
1112
  args: [MAT_DIALOG_DATA]
1162
- }] }], propDecorators: { preset: [{
1163
- type: Input
1164
- }], operationsChange: [{
1165
- type: Output
1166
- }] } });
1113
+ }] }] });
1167
1114
 
1168
1115
  /**
1169
1116
  * 🔑 `gve-operation-source-editor`
@@ -1182,28 +1129,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImpor
1182
1129
  * edit is canceled.
1183
1130
  */
1184
1131
  class OperationSourceEditorComponent {
1185
- /**
1186
- * The source to edit.
1187
- */
1188
- get source() {
1189
- return this._source;
1190
- }
1191
- set source(value) {
1192
- if (this._source === value) {
1193
- return;
1194
- }
1195
- this._source = value || undefined;
1196
- this.updateForm(this._source);
1197
- }
1198
1132
  constructor(formBuilder) {
1133
+ /**
1134
+ * The source to edit.
1135
+ */
1136
+ this.source = model();
1137
+ /**
1138
+ * The list of source IDs when it's a closed set.
1139
+ */
1140
+ this.ids = input();
1141
+ /**
1142
+ * The list of source types when it's a closed set.
1143
+ */
1144
+ this.types = input();
1199
1145
  /**
1200
1146
  * The event emitted when the source changes.
1201
1147
  */
1202
- this.sourceChange = new EventEmitter();
1148
+ this.sourceChange = output();
1203
1149
  /**
1204
1150
  * The event emitted when the edit is canceled.
1205
1151
  */
1206
- this.sourceCancel = new EventEmitter();
1152
+ this.sourceCancel = output();
1207
1153
  this.id = new FormControl('', {
1208
1154
  validators: [Validators.required, Validators.maxLength(50)],
1209
1155
  nonNullable: true,
@@ -1224,6 +1170,10 @@ class OperationSourceEditorComponent {
1224
1170
  rank: this.rank,
1225
1171
  note: this.note,
1226
1172
  });
1173
+ // when source changes, update form
1174
+ effect(() => {
1175
+ this.updateForm(this.source());
1176
+ });
1227
1177
  }
1228
1178
  updateForm(source) {
1229
1179
  if (!source) {
@@ -1243,18 +1193,18 @@ class OperationSourceEditorComponent {
1243
1193
  if (this.form.invalid) {
1244
1194
  return;
1245
1195
  }
1246
- this._source = {
1196
+ this.source.set({
1247
1197
  id: this.id.value,
1248
1198
  type: this.type.value,
1249
1199
  rank: this.rank.value,
1250
1200
  note: this.note.value || undefined,
1251
- };
1252
- this.sourceChange.emit(this._source);
1201
+ });
1202
+ this.sourceChange.emit(this.source());
1253
1203
  }
1254
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: OperationSourceEditorComponent, deps: [{ token: i1.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); }
1255
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.0", type: OperationSourceEditorComponent, isStandalone: true, selector: "gve-operation-source-editor", inputs: { source: "source", ids: "ids", types: "types" }, outputs: { sourceChange: "sourceChange", sourceCancel: "sourceCancel" }, ngImport: i0, template: "<form [formGroup]=\"form\" (submit)=\"save()\">\r\n <div class=\"form-row\">\r\n <!-- id -->\r\n <!-- id (bound) -->\r\n @if (ids?.length) {\r\n <mat-form-field>\r\n <mat-label>id</mat-label>\r\n <mat-select [formControl]=\"id\">\r\n @for (i of ids; track i.id) {\r\n <mat-option [value]=\"i.id\">{{ i.label }}</mat-option>\r\n }\r\n </mat-select>\r\n @if ($any(id.errors)?.required && (id.dirty || id.touched)) {\r\n <mat-error>ID required</mat-error>\r\n }\r\n </mat-form-field>\r\n } @else {\r\n <!-- id (free) -->\r\n <mat-form-field>\r\n <mat-label>id</mat-label>\r\n <input matInput [formControl]=\"id\" />\r\n @if ($any(id.errors)?.required && (id.dirty || id.touched)) {\r\n <mat-error>ID required</mat-error>\r\n } @if ($any(id.errors)?.maxLength && (id.dirty || id.touched)) {\r\n <mat-error>id too long</mat-error>\r\n }\r\n </mat-form-field>\r\n }\r\n\r\n <!-- type (bound) -->\r\n @if (types?.length) {\r\n <mat-form-field>\r\n <mat-label>type</mat-label>\r\n <mat-select [formControl]=\"type\">\r\n @for (i of types; track i.id) {\r\n <mat-option [value]=\"i.id\">{{ i.label }}</mat-option>\r\n }\r\n </mat-select>\r\n @if ($any(type.errors)?.required && (type.dirty || type.touched)) {\r\n <mat-error>type required</mat-error>\r\n }\r\n </mat-form-field>\r\n } @else {\r\n <!-- type (free) -->\r\n <mat-form-field>\r\n <mat-label>type</mat-label>\r\n <input matInput [formControl]=\"type\" />\r\n @if ($any(type.errors)?.required && (type.dirty || type.touched)) {\r\n <mat-error>type required</mat-error>\r\n } @if ($any(type.errors)?.maxLength && (type.dirty || type.touched)) {\r\n <mat-error>type too long</mat-error>\r\n }\r\n </mat-form-field>\r\n }\r\n\r\n <!-- rank -->\r\n <mat-form-field class=\"nr\">\r\n <mat-label>rank</mat-label>\r\n <input matInput [formControl]=\"rank\" type=\"number\" min=\"0\" />\r\n </mat-form-field>\r\n </div>\r\n <!-- note -->\r\n <div>\r\n <mat-form-field class=\"long-text\">\r\n <mat-label>note</mat-label>\r\n <textarea matInput [formControl]=\"note\" class=\"long-text\"></textarea>\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- buttons -->\r\n <div class=\"form-row\">\r\n <button\r\n type=\"button\"\r\n color=\"warn\"\r\n mat-icon-button\r\n matTooltip=\"Discard changes\"\r\n (click)=\"cancel()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n <button\r\n type=\"submit\"\r\n color=\"primary\"\r\n mat-icon-button\r\n matTooltip=\"Accept changes\"\r\n [disabled]=\"form.invalid || form.pristine\"\r\n >\r\n <mat-icon class=\"mat-primary\">check_circle</mat-icon>\r\n </button>\r\n </div>\r\n</form>\r\n", styles: [".long-text{width:100%;max-width:800px}.form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}.nr{width:5em}table{width:100%;border-collapse:collapse}th{color:#909090;font-weight:400;text-align:left;background-color:#e1e0e0}th,td{padding:4px;border-bottom:1px solid silver}tbody tr:nth-child(2n){background-color:#e8e8e8}td.fit-width{width:1px;white-space:nowrap}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "directive", type: i5.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i8.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i9.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i10.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] }); }
1204
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: OperationSourceEditorComponent, deps: [{ token: i1.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); }
1205
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.6", type: OperationSourceEditorComponent, isStandalone: true, selector: "gve-operation-source-editor", inputs: { source: { classPropertyName: "source", publicName: "source", isSignal: true, isRequired: false, transformFunction: null }, ids: { classPropertyName: "ids", publicName: "ids", isSignal: true, isRequired: false, transformFunction: null }, types: { classPropertyName: "types", publicName: "types", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { source: "sourceChange", sourceChange: "sourceChange", sourceCancel: "sourceCancel" }, ngImport: i0, template: "<form [formGroup]=\"form\" (submit)=\"save()\">\r\n <div class=\"form-row\">\r\n <!-- id -->\r\n <!-- id (bound) -->\r\n @if (ids()?.length) {\r\n <mat-form-field>\r\n <mat-label>id</mat-label>\r\n <mat-select [formControl]=\"id\">\r\n @for (i of ids(); track i.id) {\r\n <mat-option [value]=\"i.id\">{{ i.label }}</mat-option>\r\n }\r\n </mat-select>\r\n @if ($any(id.errors)?.required && (id.dirty || id.touched)) {\r\n <mat-error>ID required</mat-error>\r\n }\r\n </mat-form-field>\r\n } @else {\r\n <!-- id (free) -->\r\n <mat-form-field>\r\n <mat-label>id</mat-label>\r\n <input matInput [formControl]=\"id\" />\r\n @if ($any(id.errors)?.required && (id.dirty || id.touched)) {\r\n <mat-error>ID required</mat-error>\r\n } @if ($any(id.errors)?.maxLength && (id.dirty || id.touched)) {\r\n <mat-error>id too long</mat-error>\r\n }\r\n </mat-form-field>\r\n }\r\n\r\n <!-- type (bound) -->\r\n @if (types()?.length) {\r\n <mat-form-field>\r\n <mat-label>type</mat-label>\r\n <mat-select [formControl]=\"type\">\r\n @for (i of types(); track i.id) {\r\n <mat-option [value]=\"i.id\">{{ i.label }}</mat-option>\r\n }\r\n </mat-select>\r\n @if ($any(type.errors)?.required && (type.dirty || type.touched)) {\r\n <mat-error>type required</mat-error>\r\n }\r\n </mat-form-field>\r\n } @else {\r\n <!-- type (free) -->\r\n <mat-form-field>\r\n <mat-label>type</mat-label>\r\n <input matInput [formControl]=\"type\" />\r\n @if ($any(type.errors)?.required && (type.dirty || type.touched)) {\r\n <mat-error>type required</mat-error>\r\n } @if ($any(type.errors)?.maxLength && (type.dirty || type.touched)) {\r\n <mat-error>type too long</mat-error>\r\n }\r\n </mat-form-field>\r\n }\r\n\r\n <!-- rank -->\r\n <mat-form-field class=\"nr\">\r\n <mat-label>rank</mat-label>\r\n <input matInput [formControl]=\"rank\" type=\"number\" min=\"0\" />\r\n </mat-form-field>\r\n </div>\r\n <!-- note -->\r\n <div>\r\n <mat-form-field class=\"long-text\">\r\n <mat-label>note</mat-label>\r\n <textarea matInput [formControl]=\"note\" class=\"long-text\"></textarea>\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- buttons -->\r\n <div class=\"form-row\">\r\n <button\r\n type=\"button\"\r\n color=\"warn\"\r\n mat-icon-button\r\n matTooltip=\"Discard changes\"\r\n (click)=\"cancel()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n <button\r\n type=\"submit\"\r\n color=\"primary\"\r\n mat-icon-button\r\n matTooltip=\"Accept changes\"\r\n [disabled]=\"form.invalid || form.pristine\"\r\n >\r\n <mat-icon class=\"mat-primary\">check_circle</mat-icon>\r\n </button>\r\n </div>\r\n</form>\r\n", styles: [".long-text{width:100%;max-width:800px}.form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}.nr{width:5em}table{width:100%;border-collapse:collapse}th{color:#909090;font-weight:400;text-align:left;background-color:#e1e0e0}th,td{padding:4px;border-bottom:1px solid silver}tbody tr:nth-child(2n){background-color:#e8e8e8}td.fit-width{width:1px;white-space:nowrap}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "directive", type: i5.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i8.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i9.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i10.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] }); }
1256
1206
  }
1257
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: OperationSourceEditorComponent, decorators: [{
1207
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: OperationSourceEditorComponent, decorators: [{
1258
1208
  type: Component,
1259
1209
  args: [{ selector: 'gve-operation-source-editor', imports: [
1260
1210
  CommonModule,
@@ -1265,18 +1215,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImpor
1265
1215
  MatInputModule,
1266
1216
  MatSelectModule,
1267
1217
  MatTooltipModule,
1268
- ], template: "<form [formGroup]=\"form\" (submit)=\"save()\">\r\n <div class=\"form-row\">\r\n <!-- id -->\r\n <!-- id (bound) -->\r\n @if (ids?.length) {\r\n <mat-form-field>\r\n <mat-label>id</mat-label>\r\n <mat-select [formControl]=\"id\">\r\n @for (i of ids; track i.id) {\r\n <mat-option [value]=\"i.id\">{{ i.label }}</mat-option>\r\n }\r\n </mat-select>\r\n @if ($any(id.errors)?.required && (id.dirty || id.touched)) {\r\n <mat-error>ID required</mat-error>\r\n }\r\n </mat-form-field>\r\n } @else {\r\n <!-- id (free) -->\r\n <mat-form-field>\r\n <mat-label>id</mat-label>\r\n <input matInput [formControl]=\"id\" />\r\n @if ($any(id.errors)?.required && (id.dirty || id.touched)) {\r\n <mat-error>ID required</mat-error>\r\n } @if ($any(id.errors)?.maxLength && (id.dirty || id.touched)) {\r\n <mat-error>id too long</mat-error>\r\n }\r\n </mat-form-field>\r\n }\r\n\r\n <!-- type (bound) -->\r\n @if (types?.length) {\r\n <mat-form-field>\r\n <mat-label>type</mat-label>\r\n <mat-select [formControl]=\"type\">\r\n @for (i of types; track i.id) {\r\n <mat-option [value]=\"i.id\">{{ i.label }}</mat-option>\r\n }\r\n </mat-select>\r\n @if ($any(type.errors)?.required && (type.dirty || type.touched)) {\r\n <mat-error>type required</mat-error>\r\n }\r\n </mat-form-field>\r\n } @else {\r\n <!-- type (free) -->\r\n <mat-form-field>\r\n <mat-label>type</mat-label>\r\n <input matInput [formControl]=\"type\" />\r\n @if ($any(type.errors)?.required && (type.dirty || type.touched)) {\r\n <mat-error>type required</mat-error>\r\n } @if ($any(type.errors)?.maxLength && (type.dirty || type.touched)) {\r\n <mat-error>type too long</mat-error>\r\n }\r\n </mat-form-field>\r\n }\r\n\r\n <!-- rank -->\r\n <mat-form-field class=\"nr\">\r\n <mat-label>rank</mat-label>\r\n <input matInput [formControl]=\"rank\" type=\"number\" min=\"0\" />\r\n </mat-form-field>\r\n </div>\r\n <!-- note -->\r\n <div>\r\n <mat-form-field class=\"long-text\">\r\n <mat-label>note</mat-label>\r\n <textarea matInput [formControl]=\"note\" class=\"long-text\"></textarea>\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- buttons -->\r\n <div class=\"form-row\">\r\n <button\r\n type=\"button\"\r\n color=\"warn\"\r\n mat-icon-button\r\n matTooltip=\"Discard changes\"\r\n (click)=\"cancel()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n <button\r\n type=\"submit\"\r\n color=\"primary\"\r\n mat-icon-button\r\n matTooltip=\"Accept changes\"\r\n [disabled]=\"form.invalid || form.pristine\"\r\n >\r\n <mat-icon class=\"mat-primary\">check_circle</mat-icon>\r\n </button>\r\n </div>\r\n</form>\r\n", styles: [".long-text{width:100%;max-width:800px}.form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}.nr{width:5em}table{width:100%;border-collapse:collapse}th{color:#909090;font-weight:400;text-align:left;background-color:#e1e0e0}th,td{padding:4px;border-bottom:1px solid silver}tbody tr:nth-child(2n){background-color:#e8e8e8}td.fit-width{width:1px;white-space:nowrap}\n"] }]
1269
- }], ctorParameters: () => [{ type: i1.FormBuilder }], propDecorators: { source: [{
1270
- type: Input
1271
- }], ids: [{
1272
- type: Input
1273
- }], types: [{
1274
- type: Input
1275
- }], sourceChange: [{
1276
- type: Output
1277
- }], sourceCancel: [{
1278
- type: Output
1279
- }] } });
1218
+ ], template: "<form [formGroup]=\"form\" (submit)=\"save()\">\r\n <div class=\"form-row\">\r\n <!-- id -->\r\n <!-- id (bound) -->\r\n @if (ids()?.length) {\r\n <mat-form-field>\r\n <mat-label>id</mat-label>\r\n <mat-select [formControl]=\"id\">\r\n @for (i of ids(); track i.id) {\r\n <mat-option [value]=\"i.id\">{{ i.label }}</mat-option>\r\n }\r\n </mat-select>\r\n @if ($any(id.errors)?.required && (id.dirty || id.touched)) {\r\n <mat-error>ID required</mat-error>\r\n }\r\n </mat-form-field>\r\n } @else {\r\n <!-- id (free) -->\r\n <mat-form-field>\r\n <mat-label>id</mat-label>\r\n <input matInput [formControl]=\"id\" />\r\n @if ($any(id.errors)?.required && (id.dirty || id.touched)) {\r\n <mat-error>ID required</mat-error>\r\n } @if ($any(id.errors)?.maxLength && (id.dirty || id.touched)) {\r\n <mat-error>id too long</mat-error>\r\n }\r\n </mat-form-field>\r\n }\r\n\r\n <!-- type (bound) -->\r\n @if (types()?.length) {\r\n <mat-form-field>\r\n <mat-label>type</mat-label>\r\n <mat-select [formControl]=\"type\">\r\n @for (i of types(); track i.id) {\r\n <mat-option [value]=\"i.id\">{{ i.label }}</mat-option>\r\n }\r\n </mat-select>\r\n @if ($any(type.errors)?.required && (type.dirty || type.touched)) {\r\n <mat-error>type required</mat-error>\r\n }\r\n </mat-form-field>\r\n } @else {\r\n <!-- type (free) -->\r\n <mat-form-field>\r\n <mat-label>type</mat-label>\r\n <input matInput [formControl]=\"type\" />\r\n @if ($any(type.errors)?.required && (type.dirty || type.touched)) {\r\n <mat-error>type required</mat-error>\r\n } @if ($any(type.errors)?.maxLength && (type.dirty || type.touched)) {\r\n <mat-error>type too long</mat-error>\r\n }\r\n </mat-form-field>\r\n }\r\n\r\n <!-- rank -->\r\n <mat-form-field class=\"nr\">\r\n <mat-label>rank</mat-label>\r\n <input matInput [formControl]=\"rank\" type=\"number\" min=\"0\" />\r\n </mat-form-field>\r\n </div>\r\n <!-- note -->\r\n <div>\r\n <mat-form-field class=\"long-text\">\r\n <mat-label>note</mat-label>\r\n <textarea matInput [formControl]=\"note\" class=\"long-text\"></textarea>\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- buttons -->\r\n <div class=\"form-row\">\r\n <button\r\n type=\"button\"\r\n color=\"warn\"\r\n mat-icon-button\r\n matTooltip=\"Discard changes\"\r\n (click)=\"cancel()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n <button\r\n type=\"submit\"\r\n color=\"primary\"\r\n mat-icon-button\r\n matTooltip=\"Accept changes\"\r\n [disabled]=\"form.invalid || form.pristine\"\r\n >\r\n <mat-icon class=\"mat-primary\">check_circle</mat-icon>\r\n </button>\r\n </div>\r\n</form>\r\n", styles: [".long-text{width:100%;max-width:800px}.form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}.nr{width:5em}table{width:100%;border-collapse:collapse}th{color:#909090;font-weight:400;text-align:left;background-color:#e1e0e0}th,td{padding:4px;border-bottom:1px solid silver}tbody tr:nth-child(2n){background-color:#e8e8e8}td.fit-width{width:1px;white-space:nowrap}\n"] }]
1219
+ }], ctorParameters: () => [{ type: i1.FormBuilder }] });
1280
1220
 
1281
1221
  /**
1282
1222
  * Validators for SVG.
@@ -1313,12 +1253,22 @@ class SvgValidators {
1313
1253
  }
1314
1254
 
1315
1255
  /**
1316
- * Service to store and retrieve dynamic settings. On demand, settings can
1317
- * be persisted in the local storage.
1256
+ * Service to store and retrieve dynamic settings.
1318
1257
  */
1319
1258
  class SettingsService {
1320
1259
  constructor() {
1321
1260
  this._cache = new Map();
1261
+ this._subject = new BehaviorSubject(null);
1262
+ /**
1263
+ * True to persist the settings in the local storage by default.
1264
+ */
1265
+ this.persistDefault = false;
1266
+ }
1267
+ /**
1268
+ * Observable emitting the key of the setting that has changed.
1269
+ */
1270
+ get settingChange$() {
1271
+ return this._subject.asObservable();
1322
1272
  }
1323
1273
  /**
1324
1274
  * Scopes the key by prefixing it with the keyPrefix if any.
@@ -1329,6 +1279,9 @@ class SettingsService {
1329
1279
  getScopedKey(key) {
1330
1280
  return this.keyPrefix ? this.keyPrefix + key : key;
1331
1281
  }
1282
+ isPersist(persist) {
1283
+ return persist === undefined ? this.persistDefault : persist;
1284
+ }
1332
1285
  /**
1333
1286
  * Sets a setting's value.
1334
1287
  *
@@ -1336,11 +1289,33 @@ class SettingsService {
1336
1289
  * @param value The value.
1337
1290
  * @param persist True to persist this setting in local storage.
1338
1291
  */
1339
- set(key, value, persist = false) {
1292
+ set(key, value, persist) {
1340
1293
  key = this.getScopedKey(key);
1341
1294
  this._cache.set(key, value);
1342
- if (persist && value) {
1343
- localStorage.setItem(key, JSON.stringify(value));
1295
+ if (this.isPersist(persist) && value !== undefined) {
1296
+ try {
1297
+ localStorage.setItem(key, JSON.stringify(value));
1298
+ }
1299
+ catch (e) {
1300
+ console.error(`Error saving key "${key}" to localStorage`, e);
1301
+ }
1302
+ }
1303
+ this._subject.next(key);
1304
+ }
1305
+ /**
1306
+ * Set multiple settings at once from the specified settings object.
1307
+ *
1308
+ * @param settings The settings to set.
1309
+ * @param persist True to persist these settings in local storage.
1310
+ */
1311
+ setFrom(settings, persist) {
1312
+ if (!settings) {
1313
+ return;
1314
+ }
1315
+ for (const key in settings) {
1316
+ if (settings.hasOwnProperty(key)) {
1317
+ this.set(key, settings[key], persist);
1318
+ }
1344
1319
  }
1345
1320
  }
1346
1321
  /**
@@ -1351,18 +1326,25 @@ class SettingsService {
1351
1326
  * is not found in the store.
1352
1327
  * @returns The value.
1353
1328
  */
1354
- get(key, defaultValue) {
1329
+ get(key, defaultValue, persist) {
1355
1330
  key = this.getScopedKey(key);
1356
1331
  // immediately return value from cache
1357
1332
  if (this._cache.has(key)) {
1358
1333
  return this._cache.get(key);
1359
1334
  }
1360
1335
  // try loading value from local storage
1361
- const value = localStorage.getItem(key);
1362
- if (value) {
1363
- const v = JSON.parse(value);
1364
- this._cache.set(key, v);
1365
- return value;
1336
+ if (this.isPersist(persist)) {
1337
+ const value = localStorage.getItem(key);
1338
+ if (value) {
1339
+ try {
1340
+ const parsedValue = JSON.parse(value);
1341
+ this._cache.set(key, parsedValue);
1342
+ return parsedValue;
1343
+ }
1344
+ catch (e) {
1345
+ console.error(`Error parsing value for key "${key}" from localStorage`, e);
1346
+ }
1347
+ }
1366
1348
  }
1367
1349
  // not found, return default value
1368
1350
  return defaultValue;
@@ -1371,26 +1353,91 @@ class SettingsService {
1371
1353
  * Check if a key exists in the store.
1372
1354
  *
1373
1355
  * @param key The key.
1356
+ * @param persist True to check if the key exists in local storage.
1374
1357
  * @returns True if the key exists in the store.
1375
1358
  */
1376
- contains(key) {
1359
+ contains(key, persist) {
1377
1360
  key = this.getScopedKey(key);
1378
- return this._cache.has(key) || localStorage.getItem(key) !== null;
1361
+ if (this._cache.has(key)) {
1362
+ return true;
1363
+ }
1364
+ return this.isPersist(persist) && localStorage.getItem(key) !== null;
1365
+ }
1366
+ /**
1367
+ * Get all the keys of the settings in the store.
1368
+ *
1369
+ * @param persist True to include keys from local storage.
1370
+ * @returns The keys of the settings in the store.
1371
+ */
1372
+ getKeys(persist) {
1373
+ const keys = new Set();
1374
+ // collect keys from local storage
1375
+ if (this.isPersist(persist)) {
1376
+ for (let i = 0; i < localStorage.length; i++) {
1377
+ const key = localStorage.key(i);
1378
+ if (key && (!this.keyPrefix || key.startsWith(this.keyPrefix))) {
1379
+ keys.add(key);
1380
+ }
1381
+ }
1382
+ }
1383
+ // collect keys from cache
1384
+ this._cache.forEach((_, key) => keys.add(key));
1385
+ // return keys as array
1386
+ return Array.from(keys);
1379
1387
  }
1380
1388
  /**
1381
1389
  * Remove a setting if present.
1382
1390
  *
1383
1391
  * @param key The key.
1392
+ * @param persist True to remove the setting from local storage.
1384
1393
  */
1385
- remove(key) {
1394
+ remove(key, persist) {
1386
1395
  key = this.getScopedKey(key);
1387
1396
  this._cache.delete(key);
1388
- localStorage.removeItem(key);
1397
+ if (this.isPersist(persist)) {
1398
+ localStorage.removeItem(key);
1399
+ }
1400
+ this._subject.next(key);
1389
1401
  }
1390
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: SettingsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1391
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: SettingsService, providedIn: 'root' }); }
1402
+ /**
1403
+ * Clear all settings. Warning: this will remove all settings from the
1404
+ * local storage if no key prefix is set.
1405
+ *
1406
+ * @param persist True to remove all settings from local storage.
1407
+ */
1408
+ clear(persist) {
1409
+ // remove from local storage all the settings
1410
+ const removedKeys = new Set();
1411
+ if (this.isPersist(persist)) {
1412
+ if (this.keyPrefix) {
1413
+ // collect keys to remove from local storage
1414
+ for (let i = 0; i < localStorage.length; i++) {
1415
+ const key = localStorage.key(i);
1416
+ if (key && key.startsWith(this.keyPrefix)) {
1417
+ removedKeys.add(key);
1418
+ }
1419
+ }
1420
+ // remove collected keys from local storage
1421
+ removedKeys.forEach((key) => localStorage.removeItem(key));
1422
+ }
1423
+ else {
1424
+ localStorage.clear();
1425
+ }
1426
+ }
1427
+ // remove from cache all the items emitting change events
1428
+ this._cache.forEach((_, key) => {
1429
+ this._cache.delete(key);
1430
+ this._subject.next(key);
1431
+ removedKeys.delete(key);
1432
+ });
1433
+ // emit changes for the remaining removed keys
1434
+ // for when the cache was not in sync with the local storage
1435
+ removedKeys.forEach((key) => this._subject.next(key));
1436
+ }
1437
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: SettingsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1438
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: SettingsService, providedIn: 'root' }); }
1392
1439
  }
1393
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: SettingsService, decorators: [{
1440
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: SettingsService, decorators: [{
1394
1441
  type: Injectable,
1395
1442
  args: [{
1396
1443
  providedIn: 'root',
@@ -1448,36 +1495,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImpor
1448
1495
  * as a whole.
1449
1496
  */
1450
1497
  class ChainOperationEditorComponent {
1451
- /**
1452
- * The operation to edit.
1453
- */
1454
- get operation() {
1455
- return this._operation || undefined;
1456
- }
1457
- set operation(value) {
1458
- if (this._operation === value) {
1459
- return;
1460
- }
1461
- this._operation = value;
1462
- this.updateForm(value);
1463
- }
1464
- /**
1465
- * The snapshot the operation refers to.
1466
- */
1467
- get snapshot() {
1468
- return this._snapshot;
1469
- }
1470
- set snapshot(value) {
1471
- if (this._snapshot === value) {
1472
- return;
1473
- }
1474
- const dirty = this.hasTextChanges(value || undefined);
1475
- this._snapshot = value || undefined;
1476
- if (dirty) {
1477
- this.requestPreview();
1478
- this._editorModel?.setValue(this.svg.value || '');
1479
- }
1480
- }
1481
1498
  constructor(formBuilder, _clipboard, _settings, _dialogService) {
1482
1499
  this._clipboard = _clipboard;
1483
1500
  this._settings = _settings;
@@ -1486,18 +1503,30 @@ class ChainOperationEditorComponent {
1486
1503
  this._disposables = [];
1487
1504
  this._nanoid = customAlphabet('1234567890abcdef', 10);
1488
1505
  this._editedSourceIndex = -1;
1506
+ /**
1507
+ * The operation to edit.
1508
+ */
1509
+ this.operation = model();
1510
+ /**
1511
+ * The snapshot the operation refers to.
1512
+ */
1513
+ this.snapshot = input();
1514
+ /**
1515
+ * Whether to hide the preview request button.
1516
+ */
1517
+ this.hidePreview = input();
1489
1518
  /**
1490
1519
  * Emitted when the operation is changed.
1491
1520
  */
1492
- this.operationChange = new EventEmitter();
1521
+ this.operationChange = output();
1493
1522
  /**
1494
1523
  * Emitted when the operation preview is requested.
1495
1524
  */
1496
- this.operationPreview = new EventEmitter();
1525
+ this.operationPreview = output();
1497
1526
  /**
1498
1527
  * Emitted when operation editing is cancelled.
1499
1528
  */
1500
- this.operationCancel = new EventEmitter();
1529
+ this.operationCancel = output();
1501
1530
  this.tabIndex = 0;
1502
1531
  this.hasTo = false;
1503
1532
  this.hasToRun = false;
@@ -1535,6 +1564,9 @@ class ChainOperationEditorComponent {
1535
1564
  nonNullable: true,
1536
1565
  });
1537
1566
  this.elementFeatures = formBuilder.control({}, { nonNullable: true });
1567
+ this.newTextHidden = formBuilder.control(false, {
1568
+ nonNullable: true,
1569
+ });
1538
1570
  this.dpFeatures = formBuilder.control([], { nonNullable: true });
1539
1571
  this.form = formBuilder.group({
1540
1572
  rank: this.rank,
@@ -1553,10 +1585,24 @@ class ChainOperationEditorComponent {
1553
1585
  value: this.value,
1554
1586
  svg: this.svg,
1555
1587
  elementFeatures: this.elementFeatures,
1588
+ newTextHidden: this.newTextHidden,
1556
1589
  dpFeatures: this.dpFeatures,
1557
1590
  });
1558
1591
  // SVG elements
1559
1592
  this.elements = [];
1593
+ // when operation changes, update form
1594
+ effect(() => {
1595
+ this.updateForm(this.operation());
1596
+ });
1597
+ // when snapshot changes, update SVG
1598
+ effect(() => {
1599
+ const snapshot = this.snapshot();
1600
+ const dirty = this.hasTextChanges(snapshot || undefined);
1601
+ if (dirty) {
1602
+ this.requestPreview();
1603
+ this._editorModel?.setValue(this.svg.value || '');
1604
+ }
1605
+ });
1560
1606
  }
1561
1607
  ngOnInit() {
1562
1608
  // whenever svg or base change, update preview
@@ -1575,25 +1621,25 @@ class ChainOperationEditorComponent {
1575
1621
  }
1576
1622
  }
1577
1623
  hasTextChanges(snapshot) {
1578
- if ((!snapshot && this._snapshot) || (snapshot && !this._snapshot)) {
1624
+ if ((!snapshot && this.snapshot()) || (snapshot && !this.snapshot())) {
1579
1625
  return true;
1580
1626
  }
1581
- if (snapshot?.size?.width !== this._snapshot?.size?.width ||
1582
- snapshot?.size?.height !== this._snapshot?.size?.height) {
1627
+ if (snapshot?.size?.width !== this.snapshot()?.size?.width ||
1628
+ snapshot?.size?.height !== this.snapshot()?.size?.height) {
1583
1629
  return true;
1584
1630
  }
1585
- if (snapshot?.style !== this._snapshot?.style ||
1586
- snapshot?.text !== this._snapshot?.text ||
1587
- snapshot?.textStyle !== this._snapshot?.textStyle) {
1631
+ if (snapshot?.style !== this.snapshot()?.style ||
1632
+ snapshot?.text !== this.snapshot()?.text ||
1633
+ snapshot?.textStyle !== this.snapshot()?.textStyle) {
1588
1634
  return true;
1589
1635
  }
1590
1636
  // compare textOptions returning true if any different
1591
1637
  const options = snapshot?.textOptions;
1592
1638
  if (options?.lineHeightOffset !==
1593
- this._snapshot?.textOptions?.lineHeightOffset ||
1639
+ this.snapshot()?.textOptions?.lineHeightOffset ||
1594
1640
  options?.charSpacingOffset !==
1595
- this._snapshot?.textOptions?.charSpacingOffset ||
1596
- options?.spcWidthOffset !== this._snapshot?.textOptions?.spcWidthOffset) {
1641
+ this.snapshot()?.textOptions?.charSpacingOffset ||
1642
+ options?.spcWidthOffset !== this.snapshot()?.textOptions?.spcWidthOffset) {
1597
1643
  return true;
1598
1644
  }
1599
1645
  return false;
@@ -1611,7 +1657,9 @@ class ChainOperationEditorComponent {
1611
1657
  this._editorModel || monaco.editor.createModel(this.svg.value, 'xml');
1612
1658
  editor.setModel(this._editorModel);
1613
1659
  this._editor = editor;
1614
- this._disposables.push(this._editorModel.onDidChangeContent((e) => {
1660
+ this._disposables.push(
1661
+ // when the editor content changes, update the SVG control value
1662
+ this._editorModel.onDidChangeContent((e) => {
1615
1663
  console.log('change content');
1616
1664
  const code = this._editorModel.getValue();
1617
1665
  if (code !== this.svg.value) {
@@ -1691,7 +1739,7 @@ class ChainOperationEditorComponent {
1691
1739
  if (file) {
1692
1740
  const reader = new FileReader();
1693
1741
  reader.onload = (e) => {
1694
- this.svg.setValue(e.target?.result);
1742
+ this._editorModel?.setValue(e.target?.result);
1695
1743
  };
1696
1744
  reader.readAsText(file);
1697
1745
  }
@@ -1700,7 +1748,7 @@ class ChainOperationEditorComponent {
1700
1748
  }
1701
1749
  setSvgFromClipboard() {
1702
1750
  navigator.clipboard.readText().then((text) => {
1703
- this.svg.setValue(text);
1751
+ this._editorModel?.setValue(text);
1704
1752
  });
1705
1753
  }
1706
1754
  parseSvg(svg) {
@@ -1728,6 +1776,18 @@ class ChainOperationEditorComponent {
1728
1776
  return [];
1729
1777
  }
1730
1778
  }
1779
+ removeDecimals() {
1780
+ const svg = this.svg.value;
1781
+ const newSvg = svg.replace(/(\d+)\.\d+/g, '$1');
1782
+ this._editorModel?.setValue(newSvg);
1783
+ }
1784
+ wrapInGroup() {
1785
+ // wrap SVG code in <g>...</g> if it does not already start with <g>
1786
+ const svg = this.svg.value;
1787
+ if (!svg.startsWith('<g')) {
1788
+ this._editorModel?.setValue(`<g>${svg}</g>`);
1789
+ }
1790
+ }
1731
1791
  // #endregion
1732
1792
  // #region element features
1733
1793
  onTabIndexChange(index) {
@@ -1769,6 +1829,7 @@ class ChainOperationEditorComponent {
1769
1829
  ...this.elementFeatures.value,
1770
1830
  [this.editedElementId]: features,
1771
1831
  });
1832
+ this.editedElementId = undefined;
1772
1833
  }
1773
1834
  }
1774
1835
  // #endregion
@@ -1819,6 +1880,7 @@ class ChainOperationEditorComponent {
1819
1880
  this.groupId.setValue(operation.groupId || null);
1820
1881
  this.features.setValue(operation.features || []);
1821
1882
  this.sources.setValue(operation.sources || []);
1883
+ this.newTextHidden.setValue(operation.diplomatics?.isNewTextHidden || false);
1822
1884
  this.dpFeatures.setValue(operation.diplomatics?.features || []);
1823
1885
  this.type.setValue(operation.type);
1824
1886
  this.at.setValue(operation.at);
@@ -1848,6 +1910,7 @@ class ChainOperationEditorComponent {
1848
1910
  sources: this.sources.value?.length ? this.sources.value : undefined,
1849
1911
  diplomatics: {
1850
1912
  g: this.svg.value,
1913
+ isNewTextHidden: this.newTextHidden.value,
1851
1914
  features: this.dpFeatures.value,
1852
1915
  elementFeatures: this.elementFeatures.value,
1853
1916
  },
@@ -1871,25 +1934,26 @@ class ChainOperationEditorComponent {
1871
1934
  if (!this.form.valid) {
1872
1935
  return;
1873
1936
  }
1874
- this._operation = this.getOperation();
1875
- this.operationChange.emit(this._operation);
1937
+ this.operation.set(this.getOperation());
1938
+ this.operationChange.emit(this.operation());
1876
1939
  }
1877
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: ChainOperationEditorComponent, deps: [{ token: i1.FormBuilder }, { token: i2$3.Clipboard }, { token: SettingsService }, { token: i2$2.DialogService }], target: i0.ɵɵFactoryTarget.Component }); }
1878
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.0", type: ChainOperationEditorComponent, isStandalone: true, selector: "gve-chain-operation-editor", inputs: { operation: "operation", snapshot: "snapshot", hidePreview: "hidePreview" }, outputs: { operationChange: "operationChange", operationPreview: "operationPreview", operationCancel: "operationCancel" }, ngImport: i0, template: "<form [formGroup]=\"form\" (submit)=\"save()\">\r\n <!-- tabs -->\r\n <mat-tab-group\r\n [selectedIndex]=\"tabIndex\"\r\n (selectedIndexChange)=\"onTabIndexChange($event)\"\r\n >\r\n <!-- GENERAL -->\r\n <mat-tab label=\"general\">\r\n <div class=\"form-row\">\r\n <!-- id -->\r\n <div id=\"id\" class=\"muted\">{{ id }}</div>\r\n\r\n <!-- type -->\r\n <mat-form-field>\r\n <mat-label>type</mat-label>\r\n <mat-select [formControl]=\"type\">\r\n <mat-option [value]=\"0\"><mat-icon>layers</mat-icon> replace</mat-option>\r\n <mat-option [value]=\"1\"><mat-icon>close</mat-icon>delete</mat-option>\r\n <mat-option [value]=\"2\"><mat-icon>last_page</mat-icon>add-before</mat-option>\r\n <mat-option [value]=\"3\"><mat-icon>first_page</mat-icon>add-after</mat-option>\r\n <mat-option [value]=\"4\"><mat-icon>login</mat-icon>move-before</mat-option>\r\n <mat-option [value]=\"5\"><mat-icon>logout</mat-icon>move-after</mat-option>\r\n <mat-option [value]=\"6\"><mat-icon>compare_arrows</mat-icon>swap</mat-option>\r\n <mat-option [value]=\"7\"><mat-icon>edit_note</mat-icon>annotate</mat-option>\r\n </mat-select>\r\n <mat-error\r\n *ngIf=\"$any(type).errors?.required && (type.dirty || type.touched)\"\r\n >type required</mat-error\r\n >\r\n </mat-form-field>\r\n </div>\r\n <div class=\"form-row\">\r\n <!-- at -->\r\n <mat-form-field class=\"nr\">\r\n <mat-label>at</mat-label>\r\n <input matInput [formControl]=\"at\" type=\"number\" min=\"0\" />\r\n <mat-error\r\n *ngIf=\"$any(at).errors?.required && (at.dirty || at.touched)\"\r\n >at required</mat-error\r\n >\r\n </mat-form-field>\r\n\r\n <!-- atAsIndex -->\r\n <mat-checkbox [formControl]=\"atAsIndex\">idx</mat-checkbox>\r\n\r\n <!-- run -->\r\n <mat-form-field class=\"nr\">\r\n <mat-label>run</mat-label>\r\n <input matInput [formControl]=\"run\" type=\"number\" min=\"0\" />\r\n <mat-error\r\n *ngIf=\"$any(run).errors?.required && (run.dirty || run.touched)\"\r\n >run required</mat-error\r\n >\r\n </mat-form-field>\r\n\r\n <!-- to -->\r\n @if (hasTo) {\r\n <mat-form-field class=\"nr\">\r\n <mat-label>to</mat-label>\r\n <input matInput [formControl]=\"to\" type=\"number\" min=\"0\" />\r\n <mat-error\r\n *ngIf=\"$any(to).errors?.required && (to.dirty || to.touched)\"\r\n >to required</mat-error\r\n >\r\n </mat-form-field>\r\n\r\n <!-- toAsIndex -->\r\n <mat-checkbox [formControl]=\"toAsIndex\">idx</mat-checkbox>\r\n\r\n <!-- toRun -->\r\n @if (hasToRun) {\r\n <mat-form-field class=\"nr\">\r\n <mat-label>to run</mat-label>\r\n <input matInput [formControl]=\"toRun\" type=\"number\" min=\"0\" />\r\n <mat-error\r\n *ngIf=\"\r\n $any(toRun).errors?.required && (toRun.dirty || toRun.touched)\r\n \"\r\n >to run required</mat-error\r\n >\r\n </mat-form-field>\r\n } }\r\n\r\n <!-- value -->\r\n @if (hasValue) {\r\n <mat-form-field>\r\n <mat-label>value</mat-label>\r\n <input matInput [formControl]=\"value\" />\r\n </mat-form-field>\r\n }\r\n </div>\r\n <div class=\"form-row\">\r\n <!-- rank -->\r\n <mat-form-field class=\"nr\">\r\n <mat-label>rank</mat-label>\r\n <input matInput [formControl]=\"rank\" type=\"number\" min=\"0\" />\r\n <mat-error\r\n *ngIf=\"$any(rank).errors?.required && (rank.dirty || rank.touched)\"\r\n >rank required</mat-error\r\n >\r\n </mat-form-field>\r\n\r\n <!-- groupId -->\r\n <mat-form-field>\r\n <mat-label>group ID</mat-label>\r\n <input matInput [formControl]=\"groupId\" />\r\n <mat-error\r\n *ngIf=\"\r\n $any(groupId).errors?.required &&\r\n (groupId.dirty || groupId.touched)\r\n \"\r\n >group ID required</mat-error\r\n >\r\n </mat-form-field>\r\n\r\n <!-- inputTag -->\r\n <mat-form-field>\r\n <mat-label>input tag</mat-label>\r\n <input matInput [formControl]=\"inputTag\" />\r\n <mat-hint>blank=latest</mat-hint>\r\n </mat-form-field>\r\n\r\n <!-- outputTag -->\r\n <mat-form-field>\r\n <mat-label>output tag</mat-label>\r\n <input matInput [formControl]=\"outputTag\" />\r\n <mat-hint>blank=auto (vN)</mat-hint>\r\n </mat-form-field>\r\n </div>\r\n <div>\r\n <!-- features -->\r\n <fieldset>\r\n <legend>operation features</legend>\r\n <gve-feature-set-editor\r\n [isVar]=\"true\"\r\n [features]=\"features.value\"\r\n (featuresChange)=\"onFeaturesChange($event)\"\r\n ></gve-feature-set-editor>\r\n </fieldset>\r\n </div>\r\n </mat-tab>\r\n\r\n <!-- SOURCES -->\r\n <mat-tab label=\"sources\">\r\n <div>\r\n <button\r\n type=\"button\"\r\n mat-flat-button\r\n color=\"primary\"\r\n class=\"mat-primary\"\r\n (click)=\"addSource()\"\r\n >\r\n <mat-icon>add_circle</mat-icon>\r\n source\r\n </button>\r\n </div>\r\n <table>\r\n <thead>\r\n <tr>\r\n <th></th>\r\n <th>type</th>\r\n <th>id</th>\r\n <th>rank</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (s of sources.value; track s) {\r\n <tr>\r\n <td class=\"fit-width\">\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n color=\"primary\"\r\n (click)=\"editSource($index)\"\r\n >\r\n <mat-icon class=\"mat-primary\">edit</mat-icon>\r\n </button>\r\n <button type=\"button\" mat-icon-button color=\"warn\">\r\n <mat-icon class=\"mat-warn\">delete</mat-icon>\r\n </button>\r\n </td>\r\n <td>{{ s.type }}</td>\r\n <td>{{ s.id }}</td>\r\n <td>{{ s.rank }}</td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n <!-- source editor -->\r\n <mat-expansion-panel [disabled]=\"!editedSource\" [expanded]=\"editedSource\">\r\n <mat-expansion-panel-header>\r\n <mat-panel-title>source</mat-panel-title>\r\n </mat-expansion-panel-header>\r\n <gve-operation-source-editor\r\n [source]=\"editedSource\"\r\n (sourceChange)=\"onSourceChange($event!)\"\r\n (sourceCancel)=\"closeSource()\"\r\n />\r\n </mat-expansion-panel>\r\n </mat-tab>\r\n\r\n <!-- DIPLOMATIC -->\r\n <mat-tab label=\"diplomatic\">\r\n <div class=\"toolbar-row\">\r\n <button\r\n id=\"btn-save\"\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Save to file\"\r\n [disabled]=\"!svg.value\"\r\n (click)=\"saveSvg()\"\r\n >\r\n <mat-icon>save</mat-icon>\r\n </button>\r\n <button\r\n id=\"btn-load\"\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Load from file\"\r\n (click)=\"loadSvg()\"\r\n >\r\n <mat-icon>folder</mat-icon>\r\n </button>\r\n <button\r\n id=\"btn-copy\"\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Copy\"\r\n [cdkCopyToClipboard]=\"svg.value\"\r\n >\r\n <mat-icon>content_copy</mat-icon>\r\n </button>\r\n <button\r\n id=\"btn-paste\"\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Set SVG from clipboard\"\r\n (click)=\"setSvgFromClipboard()\"\r\n >\r\n <mat-icon>content_paste_go</mat-icon>\r\n </button>\r\n <button\r\n id=\"btn-editor\"\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Open SVG external editor\"\r\n (click)=\"openSvgEditor()\"\r\n >\r\n <mat-icon>launch</mat-icon>\r\n </button>\r\n </div>\r\n\r\n <div id=\"code\">\r\n <nge-monaco-editor\r\n style=\"--editor-height: 400px\"\r\n (ready)=\"onCreateEditor($event)\"\r\n />\r\n @if (svg.invalid) {\r\n <mat-error>invalid SVG</mat-error>\r\n }\r\n </div>\r\n </mat-tab>\r\n\r\n <!-- ELEMENTS -->\r\n <mat-tab label=\"elements\">\r\n <table>\r\n <thead>\r\n <tr>\r\n <th></th>\r\n <th>id</th>\r\n <th>visual</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (e of elements; track e.id) {\r\n <tr>\r\n <td class=\"fit-width\">\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n color=\"primary\"\r\n matTooltip=\"Edit element features\"\r\n (click)=\"editElementFeatures(e)\"\r\n >\r\n <mat-icon class=\"mat-primary\">edit</mat-icon>\r\n </button>\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n color=\"warn\"\r\n matTooltip=\"Delete all element features\"\r\n (click)=\"deleteElementFeatures(e)\"\r\n [disabled]=\"!$any(elementFeatures.value[e.id])?.length\"\r\n >\r\n <mat-icon class=\"mat-warn\">delete</mat-icon>\r\n </button>\r\n <span>{{$any(elementFeatures.value[e.id])?.length || 0}}</span>\r\n </td>\r\n <td class=\"feat-count\">{{ e.id }}</td>\r\n <td class=\"svg-cell\">\r\n <svg [innerHTML]=\"e.outerHTML | safeHtml : 'html'\"></svg>\r\n </td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n\r\n <mat-expansion-panel\r\n [disabled]=\"!editedElementId\"\r\n [expanded]=\"editedElementId\"\r\n >\r\n <mat-expansion-panel-header>element</mat-expansion-panel-header>\r\n <gve-feature-set-editor\r\n [features]=\"elementFeatures.value[editedElementId!] || []\"\r\n (featuresChange)=\"onElementFeaturesChange($event)\"\r\n />\r\n </mat-expansion-panel>\r\n </mat-tab>\r\n\r\n <!-- DP FEATS -->\r\n <mat-tab label=\"d-features\">\r\n <gve-feature-set-editor\r\n [features]=\"dpFeatures.value\"\r\n (featuresChange)=\"onDpFeaturesChange($event)\"\r\n ></gve-feature-set-editor>\r\n </mat-tab>\r\n </mat-tab-group>\r\n\r\n <!-- buttons -->\r\n <div id=\"submit-row\">\r\n <button\r\n type=\"button\"\r\n color=\"warn\"\r\n mat-icon-button\r\n matTooltip=\"Discard operation\"\r\n (click)=\"cancel()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n @if (!hidePreview) {\r\n <button\r\n type=\"button\"\r\n color=\"primary\"\r\n mat-icon-button\r\n matTooltip=\"Preview operation\"\r\n (click)=\"requestPreview()\"\r\n >\r\n <mat-icon class=\"mat-primary\">preview</mat-icon>\r\n </button>\r\n }\r\n <button\r\n type=\"submit\"\r\n color=\"primary\"\r\n class=\"mat-primary\"\r\n mat-flat-button\r\n matTooltip=\"Save operation\"\r\n [disabled]=\"form.invalid || form.pristine\"\r\n >\r\n <mat-icon>check_circle</mat-icon>\r\n save\r\n </button>\r\n </div>\r\n</form>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}#submit-row{display:flex;gap:8px;align-items:center;justify-content:center;flex-wrap:wrap;margin-top:8px;border-top:1px solid silver}.toolbar-row{display:flex;align-items:center;flex-wrap:wrap}.nr{width:5em}.long-text{width:100%;max-width:800px}#id{border-radius:6px;padding:4px;background-color:#beb9b9;color:#fff;margin-top:-16px}fieldset{border:1px solid silver;border-radius:6px;padding:8px;margin:8px 0}legend{color:silver}table{width:100%;border-collapse:collapse;margin:8px 0}th{color:#909090;font-weight:400;text-align:left;background-color:#e1e0e0}th,td{padding:4px;border-bottom:1px solid silver}td.feat-count{vertical-align:super;font-size:smaller}tbody tr:nth-child(2n){background-color:#e8e8e8}td.fit-width{width:1px;white-space:nowrap}tr{border-bottom:1px solid silver}th,td{text-align:center}#btn-save{color:#072d3e}#btn-load{color:#dbd112}#btn-copy{color:#22549f}#btn-preview{color:#095409}#code{height:500px;border:1px solid silver}#monaco{height:100%}.svg-cell{padding:0 8px}.svg-cell svg{border:1px solid silver;width:100%;height:auto}#preview{box-sizing:border-box;width:100%;border:1px solid silver;border-radius:4px;padding:8px}.tree-invisible{display:none}.tree ul,.tree li{margin-top:0;margin-bottom:0;list-style-type:none}.selected-node{background-color:#e5e5e5}.child-title{font-weight:700;margin:0;background-color:#ccc;color:#fff;padding:8px}#tree-container{display:grid;grid-template-rows:auto;grid-template-columns:auto 1fr;grid-template-areas:\"nav ed\";gap:0 16px;align-items:stretch}#tree-nav{grid-area:nav;border:1px solid silver;border-radius:6px;margin:8px 0;padding-right:8px}#tree-ed{grid-area:ed;border:1px solid silver;border-radius:6px;margin:8px 0}@media only screen and (max-width: 959px){div#container{grid-template-columns:1fr;grid-template-areas:\"nav\" \"ed\";gap:16px 0;align-items:start}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type:
1940
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: ChainOperationEditorComponent, deps: [{ token: i1.FormBuilder }, { token: i2$3.Clipboard }, { token: SettingsService }, { token: i2$1.DialogService }], target: i0.ɵɵFactoryTarget.Component }); }
1941
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.6", type: ChainOperationEditorComponent, isStandalone: true, selector: "gve-chain-operation-editor", inputs: { operation: { classPropertyName: "operation", publicName: "operation", isSignal: true, isRequired: false, transformFunction: null }, snapshot: { classPropertyName: "snapshot", publicName: "snapshot", isSignal: true, isRequired: false, transformFunction: null }, hidePreview: { classPropertyName: "hidePreview", publicName: "hidePreview", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { operation: "operationChange", operationChange: "operationChange", operationPreview: "operationPreview", operationCancel: "operationCancel" }, ngImport: i0, template: "<form [formGroup]=\"form\" (submit)=\"save()\">\r\n <!-- tabs -->\r\n <mat-tab-group\r\n [selectedIndex]=\"tabIndex\"\r\n (selectedIndexChange)=\"onTabIndexChange($event)\"\r\n >\r\n <!-- GENERAL -->\r\n <mat-tab label=\"general\">\r\n <div class=\"form-row\">\r\n <!-- id -->\r\n <div id=\"id\" class=\"muted\">{{ id }}</div>\r\n\r\n <!-- type -->\r\n <mat-form-field>\r\n <mat-label>type</mat-label>\r\n <mat-select [formControl]=\"type\">\r\n <mat-option [value]=\"0\"\r\n ><mat-icon>layers</mat-icon> replace</mat-option\r\n >\r\n <mat-option [value]=\"1\"\r\n ><mat-icon>close</mat-icon>delete</mat-option\r\n >\r\n <mat-option [value]=\"2\"\r\n ><mat-icon>last_page</mat-icon>add-before</mat-option\r\n >\r\n <mat-option [value]=\"3\"\r\n ><mat-icon>first_page</mat-icon>add-after</mat-option\r\n >\r\n <mat-option [value]=\"4\"\r\n ><mat-icon>login</mat-icon>move-before</mat-option\r\n >\r\n <mat-option [value]=\"5\"\r\n ><mat-icon>logout</mat-icon>move-after</mat-option\r\n >\r\n <mat-option [value]=\"6\"\r\n ><mat-icon>compare_arrows</mat-icon>swap</mat-option\r\n >\r\n <mat-option [value]=\"7\"\r\n ><mat-icon>edit_note</mat-icon>annotate</mat-option\r\n >\r\n </mat-select>\r\n <mat-error\r\n *ngIf=\"$any(type).errors?.required && (type.dirty || type.touched)\"\r\n >type required</mat-error\r\n >\r\n </mat-form-field>\r\n </div>\r\n <div class=\"form-row\">\r\n <!-- at -->\r\n <mat-form-field class=\"nr\">\r\n <mat-label>at</mat-label>\r\n <input matInput [formControl]=\"at\" type=\"number\" min=\"0\" />\r\n <mat-error\r\n *ngIf=\"$any(at).errors?.required && (at.dirty || at.touched)\"\r\n >at required</mat-error\r\n >\r\n </mat-form-field>\r\n\r\n <!-- atAsIndex -->\r\n <mat-checkbox [formControl]=\"atAsIndex\">idx</mat-checkbox>\r\n\r\n <!-- run -->\r\n <mat-form-field class=\"nr\">\r\n <mat-label>run</mat-label>\r\n <input matInput [formControl]=\"run\" type=\"number\" min=\"0\" />\r\n <mat-error\r\n *ngIf=\"$any(run).errors?.required && (run.dirty || run.touched)\"\r\n >run required</mat-error\r\n >\r\n </mat-form-field>\r\n\r\n <!-- to -->\r\n @if (hasTo) {\r\n <mat-form-field class=\"nr\">\r\n <mat-label>to</mat-label>\r\n <input matInput [formControl]=\"to\" type=\"number\" min=\"0\" />\r\n <mat-error\r\n *ngIf=\"$any(to).errors?.required && (to.dirty || to.touched)\"\r\n >to required</mat-error\r\n >\r\n </mat-form-field>\r\n\r\n <!-- toAsIndex -->\r\n <mat-checkbox [formControl]=\"toAsIndex\">idx</mat-checkbox>\r\n\r\n <!-- toRun -->\r\n @if (hasToRun) {\r\n <mat-form-field class=\"nr\">\r\n <mat-label>to run</mat-label>\r\n <input matInput [formControl]=\"toRun\" type=\"number\" min=\"0\" />\r\n <mat-error\r\n *ngIf=\"\r\n $any(toRun).errors?.required && (toRun.dirty || toRun.touched)\r\n \"\r\n >to run required</mat-error\r\n >\r\n </mat-form-field>\r\n } }\r\n\r\n <!-- value -->\r\n @if (hasValue) {\r\n <mat-form-field>\r\n <mat-label>value</mat-label>\r\n <input matInput [formControl]=\"value\" />\r\n </mat-form-field>\r\n }\r\n </div>\r\n <div class=\"form-row\">\r\n <!-- rank -->\r\n <mat-form-field class=\"nr\">\r\n <mat-label>rank</mat-label>\r\n <input matInput [formControl]=\"rank\" type=\"number\" min=\"0\" />\r\n <mat-error\r\n *ngIf=\"$any(rank).errors?.required && (rank.dirty || rank.touched)\"\r\n >rank required</mat-error\r\n >\r\n </mat-form-field>\r\n\r\n <!-- groupId -->\r\n <mat-form-field>\r\n <mat-label>group ID</mat-label>\r\n <input matInput [formControl]=\"groupId\" />\r\n <mat-error\r\n *ngIf=\"\r\n $any(groupId).errors?.required &&\r\n (groupId.dirty || groupId.touched)\r\n \"\r\n >group ID required</mat-error\r\n >\r\n </mat-form-field>\r\n\r\n <!-- inputTag -->\r\n <mat-form-field>\r\n <mat-label>input tag</mat-label>\r\n <input matInput [formControl]=\"inputTag\" />\r\n <mat-hint>blank=latest</mat-hint>\r\n </mat-form-field>\r\n\r\n <!-- outputTag -->\r\n <mat-form-field>\r\n <mat-label>output tag</mat-label>\r\n <input matInput [formControl]=\"outputTag\" />\r\n <mat-hint>blank=auto (vN)</mat-hint>\r\n </mat-form-field>\r\n </div>\r\n <div>\r\n <!-- features -->\r\n <fieldset>\r\n <legend>operation features</legend>\r\n <gve-feature-set-editor\r\n [isVar]=\"true\"\r\n [features]=\"features.value\"\r\n (featuresChange)=\"onFeaturesChange($event)\"\r\n ></gve-feature-set-editor>\r\n </fieldset>\r\n </div>\r\n </mat-tab>\r\n\r\n <!-- SOURCES -->\r\n <mat-tab label=\"sources\">\r\n <div>\r\n <button\r\n type=\"button\"\r\n mat-flat-button\r\n color=\"primary\"\r\n class=\"mat-primary\"\r\n (click)=\"addSource()\"\r\n >\r\n <mat-icon>add_circle</mat-icon>\r\n source\r\n </button>\r\n </div>\r\n <table>\r\n <thead>\r\n <tr>\r\n <th></th>\r\n <th>type</th>\r\n <th>id</th>\r\n <th>rank</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (s of sources.value; track s) {\r\n <tr>\r\n <td class=\"fit-width\">\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n color=\"primary\"\r\n (click)=\"editSource($index)\"\r\n >\r\n <mat-icon class=\"mat-primary\">edit</mat-icon>\r\n </button>\r\n <button type=\"button\" mat-icon-button color=\"warn\">\r\n <mat-icon class=\"mat-warn\">delete</mat-icon>\r\n </button>\r\n </td>\r\n <td>{{ s.type }}</td>\r\n <td>{{ s.id }}</td>\r\n <td>{{ s.rank }}</td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n <!-- source editor -->\r\n <mat-expansion-panel [disabled]=\"!editedSource\" [expanded]=\"editedSource\">\r\n <mat-expansion-panel-header>\r\n <mat-panel-title>source</mat-panel-title>\r\n </mat-expansion-panel-header>\r\n <gve-operation-source-editor\r\n [source]=\"editedSource\"\r\n (sourceChange)=\"onSourceChange($event!)\"\r\n (sourceCancel)=\"closeSource()\"\r\n />\r\n </mat-expansion-panel>\r\n </mat-tab>\r\n\r\n <!-- DIPLOMATIC -->\r\n <mat-tab label=\"diplomatic\">\r\n <div class=\"toolbar-row\">\r\n <button\r\n id=\"btn-save\"\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Save to file\"\r\n [disabled]=\"!svg.value\"\r\n (click)=\"saveSvg()\"\r\n >\r\n <mat-icon>save</mat-icon>\r\n </button>\r\n <button\r\n id=\"btn-load\"\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Load from file\"\r\n (click)=\"loadSvg()\"\r\n >\r\n <mat-icon>folder</mat-icon>\r\n </button>\r\n <button\r\n id=\"btn-copy\"\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Copy\"\r\n [cdkCopyToClipboard]=\"svg.value\"\r\n >\r\n <mat-icon>content_copy</mat-icon>\r\n </button>\r\n <button\r\n id=\"btn-paste\"\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Set SVG from clipboard\"\r\n (click)=\"setSvgFromClipboard()\"\r\n >\r\n <mat-icon>content_paste_go</mat-icon>\r\n </button>\r\n <button\r\n id=\"btn-editor\"\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Open SVG external editor\"\r\n (click)=\"openSvgEditor()\"\r\n >\r\n <mat-icon>launch</mat-icon>\r\n </button>\r\n <button\r\n id=\"btn-decimals\"\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Remove decimals\"\r\n [disabled]=\"!svg.value\"\r\n (click)=\"removeDecimals()\"\r\n >\r\n <mat-icon>pin</mat-icon>\r\n </button>\r\n <button\r\n id=\"btn-group\"\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Wrap code in group\"\r\n (click)=\"wrapInGroup()\"\r\n >\r\n <mat-icon>code</mat-icon>\r\n </button>\r\n </div>\r\n\r\n <div id=\"code\">\r\n <nge-monaco-editor\r\n style=\"--editor-height: 400px\"\r\n (ready)=\"onCreateEditor($event)\"\r\n />\r\n @if (svg.invalid) {\r\n <mat-error>invalid SVG</mat-error>\r\n }\r\n </div>\r\n </mat-tab>\r\n\r\n <!-- ELEMENTS -->\r\n <mat-tab label=\"elements\">\r\n <table>\r\n <thead>\r\n <tr>\r\n <th></th>\r\n <th>id</th>\r\n <th>visual</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (e of elements; track e.id) {\r\n <tr>\r\n <td class=\"fit-width\">\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n color=\"primary\"\r\n matTooltip=\"Edit element features\"\r\n (click)=\"editElementFeatures(e)\"\r\n >\r\n <mat-icon class=\"mat-primary\">edit</mat-icon>\r\n </button>\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n color=\"warn\"\r\n matTooltip=\"Delete all element features\"\r\n (click)=\"deleteElementFeatures(e)\"\r\n [disabled]=\"!$any(elementFeatures.value)[e.id]?.length\"\r\n >\r\n <mat-icon class=\"mat-warn\">delete</mat-icon>\r\n </button>\r\n </td>\r\n <td>\r\n <span\r\n [matBadge]=\"$any(elementFeatures.value)[e.id]?.length || 0\"\r\n [matBadgeHidden]=\"!$any(elementFeatures.value)[e.id]?.length\"\r\n matBadgeOverlap=\"false\"\r\n >{{ e.id }}</span\r\n >\r\n </td>\r\n <td class=\"svg-cell\">\r\n <svg [innerHTML]=\"e.outerHTML | safeHtml : 'html'\"></svg>\r\n </td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n\r\n <mat-expansion-panel\r\n [disabled]=\"!editedElementId\"\r\n [expanded]=\"editedElementId\"\r\n >\r\n <mat-expansion-panel-header>{{\r\n editedElementId\r\n }}</mat-expansion-panel-header>\r\n <gve-feature-set-editor\r\n [features]=\"elementFeatures.value[editedElementId!] || []\"\r\n (featuresChange)=\"onElementFeaturesChange($event)\"\r\n />\r\n </mat-expansion-panel>\r\n </mat-tab>\r\n\r\n <!-- DP FEATS -->\r\n <mat-tab label=\"d-features\">\r\n <div>\r\n <mat-checkbox [formControl]=\"newTextHidden\">hide new text</mat-checkbox>\r\n </div>\r\n <gve-feature-set-editor\r\n [features]=\"dpFeatures.value\"\r\n (featuresChange)=\"onDpFeaturesChange($event)\"\r\n ></gve-feature-set-editor>\r\n </mat-tab>\r\n </mat-tab-group>\r\n\r\n <!-- buttons -->\r\n <div id=\"submit-row\">\r\n <button\r\n type=\"button\"\r\n color=\"warn\"\r\n mat-icon-button\r\n matTooltip=\"Discard operation\"\r\n (click)=\"cancel()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n @if (!hidePreview()) {\r\n <button\r\n type=\"button\"\r\n color=\"primary\"\r\n mat-icon-button\r\n matTooltip=\"Preview operation\"\r\n (click)=\"requestPreview()\"\r\n >\r\n <mat-icon class=\"mat-primary\">preview</mat-icon>\r\n </button>\r\n }\r\n <button\r\n type=\"submit\"\r\n color=\"primary\"\r\n class=\"mat-primary\"\r\n mat-flat-button\r\n matTooltip=\"Save operation\"\r\n [disabled]=\"form.invalid || form.pristine\"\r\n >\r\n <mat-icon>check_circle</mat-icon>\r\n save\r\n </button>\r\n </div>\r\n</form>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}#submit-row{display:flex;gap:8px;align-items:center;justify-content:center;flex-wrap:wrap;margin-top:8px;border-top:1px solid silver}.toolbar-row{display:flex;align-items:center;flex-wrap:wrap}.nr{width:5em}.long-text{width:100%;max-width:800px}#id{border-radius:6px;padding:4px;background-color:#beb9b9;color:#fff;margin-top:-16px}fieldset{border:1px solid silver;border-radius:6px;padding:8px;margin:8px 0}legend{color:silver}table{width:100%;border-collapse:collapse;margin:8px 0}th{color:#909090;font-weight:400;text-align:left;background-color:#e1e0e0}th,td{padding:4px;border-bottom:1px solid silver}tbody tr:nth-child(2n){background-color:#e8e8e8}td.fit-width{width:1px;white-space:nowrap}tr{border-bottom:1px solid silver}th,td{text-align:center}#btn-save{color:#072d3e}#btn-load{color:#dbd112}#btn-copy{color:#22549f}#btn-preview{color:#095409}#code{height:500px;border:1px solid silver}#monaco{height:100%}.svg-cell{padding:0 8px}.svg-cell svg{border:1px solid silver;width:100%;height:auto}#preview{box-sizing:border-box;width:100%;border:1px solid silver;border-radius:4px;padding:8px}.tree-invisible{display:none}.tree ul,.tree li{margin-top:0;margin-bottom:0;list-style-type:none}.selected-node{background-color:#e5e5e5}.child-title{font-weight:700;margin:0;background-color:#ccc;color:#fff;padding:8px}#tree-container{display:grid;grid-template-rows:auto;grid-template-columns:auto 1fr;grid-template-areas:\"nav ed\";gap:0 16px;align-items:stretch}#tree-nav{grid-area:nav;border:1px solid silver;border-radius:6px;margin:8px 0;padding-right:8px}#tree-ed{grid-area:ed;border:1px solid silver;border-radius:6px;margin:8px 0}@media only screen and (max-width: 959px){div#container{grid-template-columns:1fr;grid-template-areas:\"nav\" \"ed\";gap:16px 0;align-items:start}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type:
1879
1942
  // material
1880
- ClipboardModule }, { kind: "directive", type: i2$3.CdkCopyToClipboard, selector: "[cdkCopyToClipboard]", inputs: ["cdkCopyToClipboard", "cdkCopyToClipboardAttempts"], outputs: ["cdkCopyToClipboardCopied"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i4$1.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "component", type: i4.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i4.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i4.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "directive", type: i5.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i5.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i8.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i9.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i14.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i14.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i10.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type:
1943
+ ClipboardModule }, { kind: "directive", type: i2$3.CdkCopyToClipboard, selector: "[cdkCopyToClipboard]", inputs: ["cdkCopyToClipboard", "cdkCopyToClipboardAttempts"], outputs: ["cdkCopyToClipboardCopied"] }, { kind: "ngmodule", type: MatBadgeModule }, { kind: "directive", type: i6$1.MatBadge, selector: "[matBadge]", inputs: ["matBadgeColor", "matBadgeOverlap", "matBadgeDisabled", "matBadgePosition", "matBadge", "matBadgeDescription", "matBadgeSize", "matBadgeHidden"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i4$1.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "component", type: i4.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i4.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i4.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "directive", type: i5.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i5.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i8.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i9.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i16.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i16.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i10.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type:
1881
1944
  // monaco
1882
- NgeMonacoModule }, { kind: "component", type: i16.NgeMonacoEditorComponent, selector: "nge-monaco-editor", inputs: ["autoLayout", "options"], outputs: ["ready"] }, { kind: "ngmodule", type: NgToolsModule }, { kind: "pipe", type: i2$1.SafeHtmlPipe, name: "safeHtml" }, { kind: "ngmodule", type: NgMatToolsModule }, { kind: "component", type:
1945
+ NgeMonacoModule }, { kind: "component", type: i17.NgeMonacoEditorComponent, selector: "nge-monaco-editor", inputs: ["autoLayout", "options"], outputs: ["ready"] }, { kind: "pipe", type:
1883
1946
  // myrmex
1884
- FeatureSetEditorComponent, selector: "gve-feature-set-editor", inputs: ["isVar", "featNames", "featValues", "filterThreshold", "features"], outputs: ["featuresChange"] }, { kind: "component", type: OperationSourceEditorComponent, selector: "gve-operation-source-editor", inputs: ["source", "ids", "types"], outputs: ["sourceChange", "sourceCancel"] }] }); }
1947
+ SafeHtmlPipe, name: "safeHtml" }, { kind: "component", type: FeatureSetEditorComponent, selector: "gve-feature-set-editor", inputs: ["isVar", "featNames", "featValues", "filterThreshold", "features"], outputs: ["featuresChange"] }, { kind: "component", type: OperationSourceEditorComponent, selector: "gve-operation-source-editor", inputs: ["source", "ids", "types"], outputs: ["sourceChange", "sourceCancel"] }] }); }
1885
1948
  }
1886
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: ChainOperationEditorComponent, decorators: [{
1949
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: ChainOperationEditorComponent, decorators: [{
1887
1950
  type: Component,
1888
1951
  args: [{ selector: 'gve-chain-operation-editor', imports: [
1889
1952
  CommonModule,
1890
1953
  ReactiveFormsModule,
1891
1954
  // material
1892
1955
  ClipboardModule,
1956
+ MatBadgeModule,
1893
1957
  MatButtonModule,
1894
1958
  MatCheckboxModule,
1895
1959
  MatExpansionModule,
@@ -1901,25 +1965,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImpor
1901
1965
  MatTooltipModule,
1902
1966
  // monaco
1903
1967
  NgeMonacoModule,
1904
- NgToolsModule,
1905
- NgMatToolsModule,
1906
1968
  // myrmex
1969
+ SafeHtmlPipe,
1907
1970
  FeatureSetEditorComponent,
1908
1971
  OperationSourceEditorComponent,
1909
- ], template: "<form [formGroup]=\"form\" (submit)=\"save()\">\r\n <!-- tabs -->\r\n <mat-tab-group\r\n [selectedIndex]=\"tabIndex\"\r\n (selectedIndexChange)=\"onTabIndexChange($event)\"\r\n >\r\n <!-- GENERAL -->\r\n <mat-tab label=\"general\">\r\n <div class=\"form-row\">\r\n <!-- id -->\r\n <div id=\"id\" class=\"muted\">{{ id }}</div>\r\n\r\n <!-- type -->\r\n <mat-form-field>\r\n <mat-label>type</mat-label>\r\n <mat-select [formControl]=\"type\">\r\n <mat-option [value]=\"0\"><mat-icon>layers</mat-icon> replace</mat-option>\r\n <mat-option [value]=\"1\"><mat-icon>close</mat-icon>delete</mat-option>\r\n <mat-option [value]=\"2\"><mat-icon>last_page</mat-icon>add-before</mat-option>\r\n <mat-option [value]=\"3\"><mat-icon>first_page</mat-icon>add-after</mat-option>\r\n <mat-option [value]=\"4\"><mat-icon>login</mat-icon>move-before</mat-option>\r\n <mat-option [value]=\"5\"><mat-icon>logout</mat-icon>move-after</mat-option>\r\n <mat-option [value]=\"6\"><mat-icon>compare_arrows</mat-icon>swap</mat-option>\r\n <mat-option [value]=\"7\"><mat-icon>edit_note</mat-icon>annotate</mat-option>\r\n </mat-select>\r\n <mat-error\r\n *ngIf=\"$any(type).errors?.required && (type.dirty || type.touched)\"\r\n >type required</mat-error\r\n >\r\n </mat-form-field>\r\n </div>\r\n <div class=\"form-row\">\r\n <!-- at -->\r\n <mat-form-field class=\"nr\">\r\n <mat-label>at</mat-label>\r\n <input matInput [formControl]=\"at\" type=\"number\" min=\"0\" />\r\n <mat-error\r\n *ngIf=\"$any(at).errors?.required && (at.dirty || at.touched)\"\r\n >at required</mat-error\r\n >\r\n </mat-form-field>\r\n\r\n <!-- atAsIndex -->\r\n <mat-checkbox [formControl]=\"atAsIndex\">idx</mat-checkbox>\r\n\r\n <!-- run -->\r\n <mat-form-field class=\"nr\">\r\n <mat-label>run</mat-label>\r\n <input matInput [formControl]=\"run\" type=\"number\" min=\"0\" />\r\n <mat-error\r\n *ngIf=\"$any(run).errors?.required && (run.dirty || run.touched)\"\r\n >run required</mat-error\r\n >\r\n </mat-form-field>\r\n\r\n <!-- to -->\r\n @if (hasTo) {\r\n <mat-form-field class=\"nr\">\r\n <mat-label>to</mat-label>\r\n <input matInput [formControl]=\"to\" type=\"number\" min=\"0\" />\r\n <mat-error\r\n *ngIf=\"$any(to).errors?.required && (to.dirty || to.touched)\"\r\n >to required</mat-error\r\n >\r\n </mat-form-field>\r\n\r\n <!-- toAsIndex -->\r\n <mat-checkbox [formControl]=\"toAsIndex\">idx</mat-checkbox>\r\n\r\n <!-- toRun -->\r\n @if (hasToRun) {\r\n <mat-form-field class=\"nr\">\r\n <mat-label>to run</mat-label>\r\n <input matInput [formControl]=\"toRun\" type=\"number\" min=\"0\" />\r\n <mat-error\r\n *ngIf=\"\r\n $any(toRun).errors?.required && (toRun.dirty || toRun.touched)\r\n \"\r\n >to run required</mat-error\r\n >\r\n </mat-form-field>\r\n } }\r\n\r\n <!-- value -->\r\n @if (hasValue) {\r\n <mat-form-field>\r\n <mat-label>value</mat-label>\r\n <input matInput [formControl]=\"value\" />\r\n </mat-form-field>\r\n }\r\n </div>\r\n <div class=\"form-row\">\r\n <!-- rank -->\r\n <mat-form-field class=\"nr\">\r\n <mat-label>rank</mat-label>\r\n <input matInput [formControl]=\"rank\" type=\"number\" min=\"0\" />\r\n <mat-error\r\n *ngIf=\"$any(rank).errors?.required && (rank.dirty || rank.touched)\"\r\n >rank required</mat-error\r\n >\r\n </mat-form-field>\r\n\r\n <!-- groupId -->\r\n <mat-form-field>\r\n <mat-label>group ID</mat-label>\r\n <input matInput [formControl]=\"groupId\" />\r\n <mat-error\r\n *ngIf=\"\r\n $any(groupId).errors?.required &&\r\n (groupId.dirty || groupId.touched)\r\n \"\r\n >group ID required</mat-error\r\n >\r\n </mat-form-field>\r\n\r\n <!-- inputTag -->\r\n <mat-form-field>\r\n <mat-label>input tag</mat-label>\r\n <input matInput [formControl]=\"inputTag\" />\r\n <mat-hint>blank=latest</mat-hint>\r\n </mat-form-field>\r\n\r\n <!-- outputTag -->\r\n <mat-form-field>\r\n <mat-label>output tag</mat-label>\r\n <input matInput [formControl]=\"outputTag\" />\r\n <mat-hint>blank=auto (vN)</mat-hint>\r\n </mat-form-field>\r\n </div>\r\n <div>\r\n <!-- features -->\r\n <fieldset>\r\n <legend>operation features</legend>\r\n <gve-feature-set-editor\r\n [isVar]=\"true\"\r\n [features]=\"features.value\"\r\n (featuresChange)=\"onFeaturesChange($event)\"\r\n ></gve-feature-set-editor>\r\n </fieldset>\r\n </div>\r\n </mat-tab>\r\n\r\n <!-- SOURCES -->\r\n <mat-tab label=\"sources\">\r\n <div>\r\n <button\r\n type=\"button\"\r\n mat-flat-button\r\n color=\"primary\"\r\n class=\"mat-primary\"\r\n (click)=\"addSource()\"\r\n >\r\n <mat-icon>add_circle</mat-icon>\r\n source\r\n </button>\r\n </div>\r\n <table>\r\n <thead>\r\n <tr>\r\n <th></th>\r\n <th>type</th>\r\n <th>id</th>\r\n <th>rank</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (s of sources.value; track s) {\r\n <tr>\r\n <td class=\"fit-width\">\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n color=\"primary\"\r\n (click)=\"editSource($index)\"\r\n >\r\n <mat-icon class=\"mat-primary\">edit</mat-icon>\r\n </button>\r\n <button type=\"button\" mat-icon-button color=\"warn\">\r\n <mat-icon class=\"mat-warn\">delete</mat-icon>\r\n </button>\r\n </td>\r\n <td>{{ s.type }}</td>\r\n <td>{{ s.id }}</td>\r\n <td>{{ s.rank }}</td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n <!-- source editor -->\r\n <mat-expansion-panel [disabled]=\"!editedSource\" [expanded]=\"editedSource\">\r\n <mat-expansion-panel-header>\r\n <mat-panel-title>source</mat-panel-title>\r\n </mat-expansion-panel-header>\r\n <gve-operation-source-editor\r\n [source]=\"editedSource\"\r\n (sourceChange)=\"onSourceChange($event!)\"\r\n (sourceCancel)=\"closeSource()\"\r\n />\r\n </mat-expansion-panel>\r\n </mat-tab>\r\n\r\n <!-- DIPLOMATIC -->\r\n <mat-tab label=\"diplomatic\">\r\n <div class=\"toolbar-row\">\r\n <button\r\n id=\"btn-save\"\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Save to file\"\r\n [disabled]=\"!svg.value\"\r\n (click)=\"saveSvg()\"\r\n >\r\n <mat-icon>save</mat-icon>\r\n </button>\r\n <button\r\n id=\"btn-load\"\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Load from file\"\r\n (click)=\"loadSvg()\"\r\n >\r\n <mat-icon>folder</mat-icon>\r\n </button>\r\n <button\r\n id=\"btn-copy\"\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Copy\"\r\n [cdkCopyToClipboard]=\"svg.value\"\r\n >\r\n <mat-icon>content_copy</mat-icon>\r\n </button>\r\n <button\r\n id=\"btn-paste\"\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Set SVG from clipboard\"\r\n (click)=\"setSvgFromClipboard()\"\r\n >\r\n <mat-icon>content_paste_go</mat-icon>\r\n </button>\r\n <button\r\n id=\"btn-editor\"\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Open SVG external editor\"\r\n (click)=\"openSvgEditor()\"\r\n >\r\n <mat-icon>launch</mat-icon>\r\n </button>\r\n </div>\r\n\r\n <div id=\"code\">\r\n <nge-monaco-editor\r\n style=\"--editor-height: 400px\"\r\n (ready)=\"onCreateEditor($event)\"\r\n />\r\n @if (svg.invalid) {\r\n <mat-error>invalid SVG</mat-error>\r\n }\r\n </div>\r\n </mat-tab>\r\n\r\n <!-- ELEMENTS -->\r\n <mat-tab label=\"elements\">\r\n <table>\r\n <thead>\r\n <tr>\r\n <th></th>\r\n <th>id</th>\r\n <th>visual</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (e of elements; track e.id) {\r\n <tr>\r\n <td class=\"fit-width\">\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n color=\"primary\"\r\n matTooltip=\"Edit element features\"\r\n (click)=\"editElementFeatures(e)\"\r\n >\r\n <mat-icon class=\"mat-primary\">edit</mat-icon>\r\n </button>\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n color=\"warn\"\r\n matTooltip=\"Delete all element features\"\r\n (click)=\"deleteElementFeatures(e)\"\r\n [disabled]=\"!$any(elementFeatures.value[e.id])?.length\"\r\n >\r\n <mat-icon class=\"mat-warn\">delete</mat-icon>\r\n </button>\r\n <span>{{$any(elementFeatures.value[e.id])?.length || 0}}</span>\r\n </td>\r\n <td class=\"feat-count\">{{ e.id }}</td>\r\n <td class=\"svg-cell\">\r\n <svg [innerHTML]=\"e.outerHTML | safeHtml : 'html'\"></svg>\r\n </td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n\r\n <mat-expansion-panel\r\n [disabled]=\"!editedElementId\"\r\n [expanded]=\"editedElementId\"\r\n >\r\n <mat-expansion-panel-header>element</mat-expansion-panel-header>\r\n <gve-feature-set-editor\r\n [features]=\"elementFeatures.value[editedElementId!] || []\"\r\n (featuresChange)=\"onElementFeaturesChange($event)\"\r\n />\r\n </mat-expansion-panel>\r\n </mat-tab>\r\n\r\n <!-- DP FEATS -->\r\n <mat-tab label=\"d-features\">\r\n <gve-feature-set-editor\r\n [features]=\"dpFeatures.value\"\r\n (featuresChange)=\"onDpFeaturesChange($event)\"\r\n ></gve-feature-set-editor>\r\n </mat-tab>\r\n </mat-tab-group>\r\n\r\n <!-- buttons -->\r\n <div id=\"submit-row\">\r\n <button\r\n type=\"button\"\r\n color=\"warn\"\r\n mat-icon-button\r\n matTooltip=\"Discard operation\"\r\n (click)=\"cancel()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n @if (!hidePreview) {\r\n <button\r\n type=\"button\"\r\n color=\"primary\"\r\n mat-icon-button\r\n matTooltip=\"Preview operation\"\r\n (click)=\"requestPreview()\"\r\n >\r\n <mat-icon class=\"mat-primary\">preview</mat-icon>\r\n </button>\r\n }\r\n <button\r\n type=\"submit\"\r\n color=\"primary\"\r\n class=\"mat-primary\"\r\n mat-flat-button\r\n matTooltip=\"Save operation\"\r\n [disabled]=\"form.invalid || form.pristine\"\r\n >\r\n <mat-icon>check_circle</mat-icon>\r\n save\r\n </button>\r\n </div>\r\n</form>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}#submit-row{display:flex;gap:8px;align-items:center;justify-content:center;flex-wrap:wrap;margin-top:8px;border-top:1px solid silver}.toolbar-row{display:flex;align-items:center;flex-wrap:wrap}.nr{width:5em}.long-text{width:100%;max-width:800px}#id{border-radius:6px;padding:4px;background-color:#beb9b9;color:#fff;margin-top:-16px}fieldset{border:1px solid silver;border-radius:6px;padding:8px;margin:8px 0}legend{color:silver}table{width:100%;border-collapse:collapse;margin:8px 0}th{color:#909090;font-weight:400;text-align:left;background-color:#e1e0e0}th,td{padding:4px;border-bottom:1px solid silver}td.feat-count{vertical-align:super;font-size:smaller}tbody tr:nth-child(2n){background-color:#e8e8e8}td.fit-width{width:1px;white-space:nowrap}tr{border-bottom:1px solid silver}th,td{text-align:center}#btn-save{color:#072d3e}#btn-load{color:#dbd112}#btn-copy{color:#22549f}#btn-preview{color:#095409}#code{height:500px;border:1px solid silver}#monaco{height:100%}.svg-cell{padding:0 8px}.svg-cell svg{border:1px solid silver;width:100%;height:auto}#preview{box-sizing:border-box;width:100%;border:1px solid silver;border-radius:4px;padding:8px}.tree-invisible{display:none}.tree ul,.tree li{margin-top:0;margin-bottom:0;list-style-type:none}.selected-node{background-color:#e5e5e5}.child-title{font-weight:700;margin:0;background-color:#ccc;color:#fff;padding:8px}#tree-container{display:grid;grid-template-rows:auto;grid-template-columns:auto 1fr;grid-template-areas:\"nav ed\";gap:0 16px;align-items:stretch}#tree-nav{grid-area:nav;border:1px solid silver;border-radius:6px;margin:8px 0;padding-right:8px}#tree-ed{grid-area:ed;border:1px solid silver;border-radius:6px;margin:8px 0}@media only screen and (max-width: 959px){div#container{grid-template-columns:1fr;grid-template-areas:\"nav\" \"ed\";gap:16px 0;align-items:start}}\n"] }]
1910
- }], ctorParameters: () => [{ type: i1.FormBuilder }, { type: i2$3.Clipboard }, { type: SettingsService }, { type: i2$2.DialogService }], propDecorators: { operation: [{
1911
- type: Input
1912
- }], snapshot: [{
1913
- type: Input
1914
- }], hidePreview: [{
1915
- type: Input
1916
- }], operationChange: [{
1917
- type: Output
1918
- }], operationPreview: [{
1919
- type: Output
1920
- }], operationCancel: [{
1921
- type: Output
1922
- }] } });
1972
+ ], template: "<form [formGroup]=\"form\" (submit)=\"save()\">\r\n <!-- tabs -->\r\n <mat-tab-group\r\n [selectedIndex]=\"tabIndex\"\r\n (selectedIndexChange)=\"onTabIndexChange($event)\"\r\n >\r\n <!-- GENERAL -->\r\n <mat-tab label=\"general\">\r\n <div class=\"form-row\">\r\n <!-- id -->\r\n <div id=\"id\" class=\"muted\">{{ id }}</div>\r\n\r\n <!-- type -->\r\n <mat-form-field>\r\n <mat-label>type</mat-label>\r\n <mat-select [formControl]=\"type\">\r\n <mat-option [value]=\"0\"\r\n ><mat-icon>layers</mat-icon> replace</mat-option\r\n >\r\n <mat-option [value]=\"1\"\r\n ><mat-icon>close</mat-icon>delete</mat-option\r\n >\r\n <mat-option [value]=\"2\"\r\n ><mat-icon>last_page</mat-icon>add-before</mat-option\r\n >\r\n <mat-option [value]=\"3\"\r\n ><mat-icon>first_page</mat-icon>add-after</mat-option\r\n >\r\n <mat-option [value]=\"4\"\r\n ><mat-icon>login</mat-icon>move-before</mat-option\r\n >\r\n <mat-option [value]=\"5\"\r\n ><mat-icon>logout</mat-icon>move-after</mat-option\r\n >\r\n <mat-option [value]=\"6\"\r\n ><mat-icon>compare_arrows</mat-icon>swap</mat-option\r\n >\r\n <mat-option [value]=\"7\"\r\n ><mat-icon>edit_note</mat-icon>annotate</mat-option\r\n >\r\n </mat-select>\r\n <mat-error\r\n *ngIf=\"$any(type).errors?.required && (type.dirty || type.touched)\"\r\n >type required</mat-error\r\n >\r\n </mat-form-field>\r\n </div>\r\n <div class=\"form-row\">\r\n <!-- at -->\r\n <mat-form-field class=\"nr\">\r\n <mat-label>at</mat-label>\r\n <input matInput [formControl]=\"at\" type=\"number\" min=\"0\" />\r\n <mat-error\r\n *ngIf=\"$any(at).errors?.required && (at.dirty || at.touched)\"\r\n >at required</mat-error\r\n >\r\n </mat-form-field>\r\n\r\n <!-- atAsIndex -->\r\n <mat-checkbox [formControl]=\"atAsIndex\">idx</mat-checkbox>\r\n\r\n <!-- run -->\r\n <mat-form-field class=\"nr\">\r\n <mat-label>run</mat-label>\r\n <input matInput [formControl]=\"run\" type=\"number\" min=\"0\" />\r\n <mat-error\r\n *ngIf=\"$any(run).errors?.required && (run.dirty || run.touched)\"\r\n >run required</mat-error\r\n >\r\n </mat-form-field>\r\n\r\n <!-- to -->\r\n @if (hasTo) {\r\n <mat-form-field class=\"nr\">\r\n <mat-label>to</mat-label>\r\n <input matInput [formControl]=\"to\" type=\"number\" min=\"0\" />\r\n <mat-error\r\n *ngIf=\"$any(to).errors?.required && (to.dirty || to.touched)\"\r\n >to required</mat-error\r\n >\r\n </mat-form-field>\r\n\r\n <!-- toAsIndex -->\r\n <mat-checkbox [formControl]=\"toAsIndex\">idx</mat-checkbox>\r\n\r\n <!-- toRun -->\r\n @if (hasToRun) {\r\n <mat-form-field class=\"nr\">\r\n <mat-label>to run</mat-label>\r\n <input matInput [formControl]=\"toRun\" type=\"number\" min=\"0\" />\r\n <mat-error\r\n *ngIf=\"\r\n $any(toRun).errors?.required && (toRun.dirty || toRun.touched)\r\n \"\r\n >to run required</mat-error\r\n >\r\n </mat-form-field>\r\n } }\r\n\r\n <!-- value -->\r\n @if (hasValue) {\r\n <mat-form-field>\r\n <mat-label>value</mat-label>\r\n <input matInput [formControl]=\"value\" />\r\n </mat-form-field>\r\n }\r\n </div>\r\n <div class=\"form-row\">\r\n <!-- rank -->\r\n <mat-form-field class=\"nr\">\r\n <mat-label>rank</mat-label>\r\n <input matInput [formControl]=\"rank\" type=\"number\" min=\"0\" />\r\n <mat-error\r\n *ngIf=\"$any(rank).errors?.required && (rank.dirty || rank.touched)\"\r\n >rank required</mat-error\r\n >\r\n </mat-form-field>\r\n\r\n <!-- groupId -->\r\n <mat-form-field>\r\n <mat-label>group ID</mat-label>\r\n <input matInput [formControl]=\"groupId\" />\r\n <mat-error\r\n *ngIf=\"\r\n $any(groupId).errors?.required &&\r\n (groupId.dirty || groupId.touched)\r\n \"\r\n >group ID required</mat-error\r\n >\r\n </mat-form-field>\r\n\r\n <!-- inputTag -->\r\n <mat-form-field>\r\n <mat-label>input tag</mat-label>\r\n <input matInput [formControl]=\"inputTag\" />\r\n <mat-hint>blank=latest</mat-hint>\r\n </mat-form-field>\r\n\r\n <!-- outputTag -->\r\n <mat-form-field>\r\n <mat-label>output tag</mat-label>\r\n <input matInput [formControl]=\"outputTag\" />\r\n <mat-hint>blank=auto (vN)</mat-hint>\r\n </mat-form-field>\r\n </div>\r\n <div>\r\n <!-- features -->\r\n <fieldset>\r\n <legend>operation features</legend>\r\n <gve-feature-set-editor\r\n [isVar]=\"true\"\r\n [features]=\"features.value\"\r\n (featuresChange)=\"onFeaturesChange($event)\"\r\n ></gve-feature-set-editor>\r\n </fieldset>\r\n </div>\r\n </mat-tab>\r\n\r\n <!-- SOURCES -->\r\n <mat-tab label=\"sources\">\r\n <div>\r\n <button\r\n type=\"button\"\r\n mat-flat-button\r\n color=\"primary\"\r\n class=\"mat-primary\"\r\n (click)=\"addSource()\"\r\n >\r\n <mat-icon>add_circle</mat-icon>\r\n source\r\n </button>\r\n </div>\r\n <table>\r\n <thead>\r\n <tr>\r\n <th></th>\r\n <th>type</th>\r\n <th>id</th>\r\n <th>rank</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (s of sources.value; track s) {\r\n <tr>\r\n <td class=\"fit-width\">\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n color=\"primary\"\r\n (click)=\"editSource($index)\"\r\n >\r\n <mat-icon class=\"mat-primary\">edit</mat-icon>\r\n </button>\r\n <button type=\"button\" mat-icon-button color=\"warn\">\r\n <mat-icon class=\"mat-warn\">delete</mat-icon>\r\n </button>\r\n </td>\r\n <td>{{ s.type }}</td>\r\n <td>{{ s.id }}</td>\r\n <td>{{ s.rank }}</td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n <!-- source editor -->\r\n <mat-expansion-panel [disabled]=\"!editedSource\" [expanded]=\"editedSource\">\r\n <mat-expansion-panel-header>\r\n <mat-panel-title>source</mat-panel-title>\r\n </mat-expansion-panel-header>\r\n <gve-operation-source-editor\r\n [source]=\"editedSource\"\r\n (sourceChange)=\"onSourceChange($event!)\"\r\n (sourceCancel)=\"closeSource()\"\r\n />\r\n </mat-expansion-panel>\r\n </mat-tab>\r\n\r\n <!-- DIPLOMATIC -->\r\n <mat-tab label=\"diplomatic\">\r\n <div class=\"toolbar-row\">\r\n <button\r\n id=\"btn-save\"\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Save to file\"\r\n [disabled]=\"!svg.value\"\r\n (click)=\"saveSvg()\"\r\n >\r\n <mat-icon>save</mat-icon>\r\n </button>\r\n <button\r\n id=\"btn-load\"\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Load from file\"\r\n (click)=\"loadSvg()\"\r\n >\r\n <mat-icon>folder</mat-icon>\r\n </button>\r\n <button\r\n id=\"btn-copy\"\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Copy\"\r\n [cdkCopyToClipboard]=\"svg.value\"\r\n >\r\n <mat-icon>content_copy</mat-icon>\r\n </button>\r\n <button\r\n id=\"btn-paste\"\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Set SVG from clipboard\"\r\n (click)=\"setSvgFromClipboard()\"\r\n >\r\n <mat-icon>content_paste_go</mat-icon>\r\n </button>\r\n <button\r\n id=\"btn-editor\"\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Open SVG external editor\"\r\n (click)=\"openSvgEditor()\"\r\n >\r\n <mat-icon>launch</mat-icon>\r\n </button>\r\n <button\r\n id=\"btn-decimals\"\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Remove decimals\"\r\n [disabled]=\"!svg.value\"\r\n (click)=\"removeDecimals()\"\r\n >\r\n <mat-icon>pin</mat-icon>\r\n </button>\r\n <button\r\n id=\"btn-group\"\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Wrap code in group\"\r\n (click)=\"wrapInGroup()\"\r\n >\r\n <mat-icon>code</mat-icon>\r\n </button>\r\n </div>\r\n\r\n <div id=\"code\">\r\n <nge-monaco-editor\r\n style=\"--editor-height: 400px\"\r\n (ready)=\"onCreateEditor($event)\"\r\n />\r\n @if (svg.invalid) {\r\n <mat-error>invalid SVG</mat-error>\r\n }\r\n </div>\r\n </mat-tab>\r\n\r\n <!-- ELEMENTS -->\r\n <mat-tab label=\"elements\">\r\n <table>\r\n <thead>\r\n <tr>\r\n <th></th>\r\n <th>id</th>\r\n <th>visual</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (e of elements; track e.id) {\r\n <tr>\r\n <td class=\"fit-width\">\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n color=\"primary\"\r\n matTooltip=\"Edit element features\"\r\n (click)=\"editElementFeatures(e)\"\r\n >\r\n <mat-icon class=\"mat-primary\">edit</mat-icon>\r\n </button>\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n color=\"warn\"\r\n matTooltip=\"Delete all element features\"\r\n (click)=\"deleteElementFeatures(e)\"\r\n [disabled]=\"!$any(elementFeatures.value)[e.id]?.length\"\r\n >\r\n <mat-icon class=\"mat-warn\">delete</mat-icon>\r\n </button>\r\n </td>\r\n <td>\r\n <span\r\n [matBadge]=\"$any(elementFeatures.value)[e.id]?.length || 0\"\r\n [matBadgeHidden]=\"!$any(elementFeatures.value)[e.id]?.length\"\r\n matBadgeOverlap=\"false\"\r\n >{{ e.id }}</span\r\n >\r\n </td>\r\n <td class=\"svg-cell\">\r\n <svg [innerHTML]=\"e.outerHTML | safeHtml : 'html'\"></svg>\r\n </td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n\r\n <mat-expansion-panel\r\n [disabled]=\"!editedElementId\"\r\n [expanded]=\"editedElementId\"\r\n >\r\n <mat-expansion-panel-header>{{\r\n editedElementId\r\n }}</mat-expansion-panel-header>\r\n <gve-feature-set-editor\r\n [features]=\"elementFeatures.value[editedElementId!] || []\"\r\n (featuresChange)=\"onElementFeaturesChange($event)\"\r\n />\r\n </mat-expansion-panel>\r\n </mat-tab>\r\n\r\n <!-- DP FEATS -->\r\n <mat-tab label=\"d-features\">\r\n <div>\r\n <mat-checkbox [formControl]=\"newTextHidden\">hide new text</mat-checkbox>\r\n </div>\r\n <gve-feature-set-editor\r\n [features]=\"dpFeatures.value\"\r\n (featuresChange)=\"onDpFeaturesChange($event)\"\r\n ></gve-feature-set-editor>\r\n </mat-tab>\r\n </mat-tab-group>\r\n\r\n <!-- buttons -->\r\n <div id=\"submit-row\">\r\n <button\r\n type=\"button\"\r\n color=\"warn\"\r\n mat-icon-button\r\n matTooltip=\"Discard operation\"\r\n (click)=\"cancel()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n @if (!hidePreview()) {\r\n <button\r\n type=\"button\"\r\n color=\"primary\"\r\n mat-icon-button\r\n matTooltip=\"Preview operation\"\r\n (click)=\"requestPreview()\"\r\n >\r\n <mat-icon class=\"mat-primary\">preview</mat-icon>\r\n </button>\r\n }\r\n <button\r\n type=\"submit\"\r\n color=\"primary\"\r\n class=\"mat-primary\"\r\n mat-flat-button\r\n matTooltip=\"Save operation\"\r\n [disabled]=\"form.invalid || form.pristine\"\r\n >\r\n <mat-icon>check_circle</mat-icon>\r\n save\r\n </button>\r\n </div>\r\n</form>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}#submit-row{display:flex;gap:8px;align-items:center;justify-content:center;flex-wrap:wrap;margin-top:8px;border-top:1px solid silver}.toolbar-row{display:flex;align-items:center;flex-wrap:wrap}.nr{width:5em}.long-text{width:100%;max-width:800px}#id{border-radius:6px;padding:4px;background-color:#beb9b9;color:#fff;margin-top:-16px}fieldset{border:1px solid silver;border-radius:6px;padding:8px;margin:8px 0}legend{color:silver}table{width:100%;border-collapse:collapse;margin:8px 0}th{color:#909090;font-weight:400;text-align:left;background-color:#e1e0e0}th,td{padding:4px;border-bottom:1px solid silver}tbody tr:nth-child(2n){background-color:#e8e8e8}td.fit-width{width:1px;white-space:nowrap}tr{border-bottom:1px solid silver}th,td{text-align:center}#btn-save{color:#072d3e}#btn-load{color:#dbd112}#btn-copy{color:#22549f}#btn-preview{color:#095409}#code{height:500px;border:1px solid silver}#monaco{height:100%}.svg-cell{padding:0 8px}.svg-cell svg{border:1px solid silver;width:100%;height:auto}#preview{box-sizing:border-box;width:100%;border:1px solid silver;border-radius:4px;padding:8px}.tree-invisible{display:none}.tree ul,.tree li{margin-top:0;margin-bottom:0;list-style-type:none}.selected-node{background-color:#e5e5e5}.child-title{font-weight:700;margin:0;background-color:#ccc;color:#fff;padding:8px}#tree-container{display:grid;grid-template-rows:auto;grid-template-columns:auto 1fr;grid-template-areas:\"nav ed\";gap:0 16px;align-items:stretch}#tree-nav{grid-area:nav;border:1px solid silver;border-radius:6px;margin:8px 0;padding-right:8px}#tree-ed{grid-area:ed;border:1px solid silver;border-radius:6px;margin:8px 0}@media only screen and (max-width: 959px){div#container{grid-template-columns:1fr;grid-template-areas:\"nav\" \"ed\";gap:16px 0;align-items:start}}\n"] }]
1973
+ }], ctorParameters: () => [{ type: i1.FormBuilder }, { type: i2$3.Clipboard }, { type: SettingsService }, { type: i2$1.DialogService }] });
1923
1974
 
1924
1975
  /**
1925
1976
  * 🔑 `gve-feature-set-view`
@@ -1939,34 +1990,40 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImpor
1939
1990
  * is greater than the threshold. Default is 5.
1940
1991
  */
1941
1992
  class FeatureSetViewComponent {
1942
- /**
1943
- * The features.
1944
- */
1945
- get features() {
1946
- return this._features;
1947
- }
1948
- set features(value) {
1949
- if (this._features === value) {
1950
- return;
1951
- }
1952
- this._features = value || undefined;
1953
- this.filter.reset();
1954
- this.applyFeatureFilter();
1955
- }
1956
1993
  constructor(formBuilder) {
1957
- this._features = [];
1994
+ /**
1995
+ * The features.
1996
+ */
1997
+ this.features = input([]);
1998
+ /**
1999
+ * The list of feature names to display in the name selection.
2000
+ * This is used when you have a closed list of features.
2001
+ */
2002
+ this.featNames = input();
2003
+ /**
2004
+ * The feature values map. When specified and the user selects a feature
2005
+ * name present in the map keys, the corresponding values will be used
2006
+ * to populate the value selection.
2007
+ */
2008
+ this.featValues = input();
1958
2009
  /**
1959
2010
  * The threshold at which the features filter should become visible.
1960
2011
  * If set to 0, the filter is always visible; if set to -1, it is always
1961
2012
  * invisible; otherwise, it gets visible when the number of features
1962
2013
  * is greater than the threshold. Default is 5.
1963
2014
  */
1964
- this.filterThreshold = 5;
2015
+ this.filterThreshold = input(5);
1965
2016
  this.filteredFeatures = [];
1966
2017
  this.filter = formBuilder.control(null);
2018
+ // when features change, reset the filter and apply it
2019
+ effect(() => {
2020
+ const features = this.features();
2021
+ this.filter.reset();
2022
+ this.applyFeatureFilter();
2023
+ });
1967
2024
  }
1968
2025
  applyFeatureFilter(name) {
1969
- this.filteredFeatures = this._features.filter((feature) => {
2026
+ this.filteredFeatures = this.features().filter((feature) => {
1970
2027
  return !name || feature.name.toLowerCase().includes(name.toLowerCase());
1971
2028
  });
1972
2029
  }
@@ -1981,10 +2038,10 @@ class FeatureSetViewComponent {
1981
2038
  ngOnDestroy() {
1982
2039
  this._sub?.unsubscribe();
1983
2040
  }
1984
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: FeatureSetViewComponent, deps: [{ token: i1.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); }
1985
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.0", type: FeatureSetViewComponent, isStandalone: true, selector: "gve-feature-set-view", inputs: { features: "features", featNames: "featNames", featValues: "featValues", filterThreshold: "filterThreshold" }, ngImport: i0, template: "<div>\r\n <!-- filter -->\r\n <div>\r\n @if (filterThreshold === 0 || features.length > filterThreshold) {\r\n <div class=\"form-row\">\r\n <mat-form-field id=\"filter\">\r\n <mat-label>filter</mat-label>\r\n <input matInput [formControl]=\"filter\" />\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matSuffix\r\n (click)=\"filter.reset()\"\r\n [disabled]=\"!filter.value\"\r\n [attr.aria-label]=\"'Reset filter'\"\r\n >\r\n <mat-icon color=\"warn\" class=\"mat-warn\">cancel</mat-icon>\r\n </button>\r\n </mat-form-field>\r\n\r\n <span>{{ filteredFeatures.length }}</span>\r\n </div>\r\n }\r\n\r\n <!-- list -->\r\n @if (filteredFeatures.length) {\r\n <table>\r\n <tbody>\r\n @for (feature of filteredFeatures; track $index) {\r\n <tr>\r\n <th>\r\n @if (featNames?.length) {\r\n <span>{{\r\n feature.name | flatLookup : featNames : \"id\" : \"label\"\r\n }}</span>\r\n } @else {\r\n <span>{{ feature.name }}</span>\r\n }\r\n </th>\r\n <td>\r\n @if (featValues) {\r\n <span>{{\r\n feature.value | flatLookup : featValues[feature.name]\r\n }}</span>\r\n } @else {\r\n <span>{{ feature.value }}</span>\r\n }\r\n </td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n }\r\n </div>\r\n</div>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}.in-row-button{margin-top:-16px}table{width:100%;border-collapse:collapse}th{text-align:left;background-color:#c8d9eb;color:#333;font-weight:400}th,td{border:1px solid silver;padding:4px}tr:nth-child(2n){background-color:#dfdfdf}#filter{width:7em}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "directive", type: i5.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: NgToolsModule }, { kind: "pipe", type: i2$1.FlatLookupPipe, name: "flatLookup" }] }); }
2041
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: FeatureSetViewComponent, deps: [{ token: i1.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); }
2042
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.6", type: FeatureSetViewComponent, isStandalone: true, selector: "gve-feature-set-view", inputs: { features: { classPropertyName: "features", publicName: "features", isSignal: true, isRequired: false, transformFunction: null }, featNames: { classPropertyName: "featNames", publicName: "featNames", isSignal: true, isRequired: false, transformFunction: null }, featValues: { classPropertyName: "featValues", publicName: "featValues", isSignal: true, isRequired: false, transformFunction: null }, filterThreshold: { classPropertyName: "filterThreshold", publicName: "filterThreshold", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div>\r\n <!-- filter -->\r\n <div>\r\n @if (filterThreshold() === 0 || features()!.length > filterThreshold()) {\r\n <div class=\"form-row\">\r\n <mat-form-field id=\"filter\">\r\n <mat-label>filter</mat-label>\r\n <input matInput [formControl]=\"filter\" />\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matSuffix\r\n (click)=\"filter.reset()\"\r\n [disabled]=\"!filter.value\"\r\n [attr.aria-label]=\"'Reset filter'\"\r\n >\r\n <mat-icon color=\"warn\" class=\"mat-warn\">cancel</mat-icon>\r\n </button>\r\n </mat-form-field>\r\n\r\n <span>{{ filteredFeatures.length }}</span>\r\n </div>\r\n }\r\n\r\n <!-- list -->\r\n @if (filteredFeatures.length) {\r\n <table>\r\n <tbody>\r\n @for (feature of filteredFeatures; track $index) {\r\n <tr>\r\n <th>\r\n @if (featNames.length) {\r\n <span>{{\r\n feature.name | flatLookup : featNames : \"id\" : \"label\"\r\n }}</span>\r\n } @else {\r\n <span>{{ feature.name }}</span>\r\n }\r\n </th>\r\n <td>\r\n @if (featValues()) {\r\n <span>{{\r\n feature.value | flatLookup : featValues()![feature.name]\r\n }}</span>\r\n } @else {\r\n <span>{{ feature.value }}</span>\r\n }\r\n </td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n }\r\n </div>\r\n</div>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}.in-row-button{margin-top:-16px}table{width:100%;border-collapse:collapse}th{text-align:left;background-color:#c8d9eb;color:#333;font-weight:400}th,td{border:1px solid silver;padding:4px}tr:nth-child(2n){background-color:#dfdfdf}#filter{width:7em}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "directive", type: i5.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "pipe", type: FlatLookupPipe, name: "flatLookup" }] }); }
1986
2043
  }
1987
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: FeatureSetViewComponent, decorators: [{
2044
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: FeatureSetViewComponent, decorators: [{
1988
2045
  type: Component,
1989
2046
  args: [{ selector: 'gve-feature-set-view', imports: [
1990
2047
  CommonModule,
@@ -1993,17 +2050,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImpor
1993
2050
  MatFormFieldModule,
1994
2051
  MatIconModule,
1995
2052
  MatInputModule,
1996
- NgToolsModule,
1997
- ], template: "<div>\r\n <!-- filter -->\r\n <div>\r\n @if (filterThreshold === 0 || features.length > filterThreshold) {\r\n <div class=\"form-row\">\r\n <mat-form-field id=\"filter\">\r\n <mat-label>filter</mat-label>\r\n <input matInput [formControl]=\"filter\" />\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matSuffix\r\n (click)=\"filter.reset()\"\r\n [disabled]=\"!filter.value\"\r\n [attr.aria-label]=\"'Reset filter'\"\r\n >\r\n <mat-icon color=\"warn\" class=\"mat-warn\">cancel</mat-icon>\r\n </button>\r\n </mat-form-field>\r\n\r\n <span>{{ filteredFeatures.length }}</span>\r\n </div>\r\n }\r\n\r\n <!-- list -->\r\n @if (filteredFeatures.length) {\r\n <table>\r\n <tbody>\r\n @for (feature of filteredFeatures; track $index) {\r\n <tr>\r\n <th>\r\n @if (featNames?.length) {\r\n <span>{{\r\n feature.name | flatLookup : featNames : \"id\" : \"label\"\r\n }}</span>\r\n } @else {\r\n <span>{{ feature.name }}</span>\r\n }\r\n </th>\r\n <td>\r\n @if (featValues) {\r\n <span>{{\r\n feature.value | flatLookup : featValues[feature.name]\r\n }}</span>\r\n } @else {\r\n <span>{{ feature.value }}</span>\r\n }\r\n </td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n }\r\n </div>\r\n</div>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}.in-row-button{margin-top:-16px}table{width:100%;border-collapse:collapse}th{text-align:left;background-color:#c8d9eb;color:#333;font-weight:400}th,td{border:1px solid silver;padding:4px}tr:nth-child(2n){background-color:#dfdfdf}#filter{width:7em}\n"] }]
1998
- }], ctorParameters: () => [{ type: i1.FormBuilder }], propDecorators: { features: [{
1999
- type: Input
2000
- }], featNames: [{
2001
- type: Input
2002
- }], featValues: [{
2003
- type: Input
2004
- }], filterThreshold: [{
2005
- type: Input
2006
- }] } });
2053
+ FlatLookupPipe,
2054
+ ], template: "<div>\r\n <!-- filter -->\r\n <div>\r\n @if (filterThreshold() === 0 || features()!.length > filterThreshold()) {\r\n <div class=\"form-row\">\r\n <mat-form-field id=\"filter\">\r\n <mat-label>filter</mat-label>\r\n <input matInput [formControl]=\"filter\" />\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matSuffix\r\n (click)=\"filter.reset()\"\r\n [disabled]=\"!filter.value\"\r\n [attr.aria-label]=\"'Reset filter'\"\r\n >\r\n <mat-icon color=\"warn\" class=\"mat-warn\">cancel</mat-icon>\r\n </button>\r\n </mat-form-field>\r\n\r\n <span>{{ filteredFeatures.length }}</span>\r\n </div>\r\n }\r\n\r\n <!-- list -->\r\n @if (filteredFeatures.length) {\r\n <table>\r\n <tbody>\r\n @for (feature of filteredFeatures; track $index) {\r\n <tr>\r\n <th>\r\n @if (featNames.length) {\r\n <span>{{\r\n feature.name | flatLookup : featNames : \"id\" : \"label\"\r\n }}</span>\r\n } @else {\r\n <span>{{ feature.name }}</span>\r\n }\r\n </th>\r\n <td>\r\n @if (featValues()) {\r\n <span>{{\r\n feature.value | flatLookup : featValues()![feature.name]\r\n }}</span>\r\n } @else {\r\n <span>{{ feature.value }}</span>\r\n }\r\n </td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n }\r\n </div>\r\n</div>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}.in-row-button{margin-top:-16px}table{width:100%;border-collapse:collapse}th{text-align:left;background-color:#c8d9eb;color:#333;font-weight:400}th,td{border:1px solid silver;padding:4px}tr:nth-child(2n){background-color:#dfdfdf}#filter{width:7em}\n"] }]
2055
+ }], ctorParameters: () => [{ type: i1.FormBuilder }] });
2007
2056
 
2008
2057
  /**
2009
2058
  * 🔑 `gve-steps-map`
@@ -2026,59 +2075,62 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImpor
2026
2075
  */
2027
2076
  class StepsMapComponent {
2028
2077
  constructor() {
2029
- this.lines = [];
2078
+ // the lines of each version text, keyed under the output tag
2079
+ this.lines = {};
2080
+ /**
2081
+ * The steps to display.
2082
+ */
2083
+ this.steps = input([]);
2084
+ /**
2085
+ * The step that is currently selected.
2086
+ */
2087
+ this.selectedStep = model();
2030
2088
  /**
2031
2089
  * The font size of the steps text.
2032
2090
  */
2033
- this.textFontSize = '0.8em';
2091
+ this.textFontSize = input('0.8em');
2034
2092
  /**
2035
2093
  * Emitted when the selected step has changed by user, or when
2036
2094
  * the steps are set for the first time.
2037
2095
  */
2038
- this.selectedStepChange = new EventEmitter();
2096
+ this.selectedStepChange = output();
2097
+ // when the steps change, update lines and selected step
2098
+ effect(() => {
2099
+ const steps = this.steps();
2100
+ const first = steps === undefined;
2101
+ if (first && steps.length) {
2102
+ this.selectedStep.set(steps[steps.length - 1]);
2103
+ this.updateLines(steps);
2104
+ this.selectedStepChange.emit(this.selectedStep());
2105
+ }
2106
+ else {
2107
+ this.selectedStep.set(undefined);
2108
+ this.selectedStepChange.emit(undefined);
2109
+ this.updateLines(steps);
2110
+ }
2111
+ });
2039
2112
  }
2040
- /**
2041
- * The steps to display.
2042
- */
2043
- get steps() {
2044
- return this._steps;
2045
- }
2046
- set steps(value) {
2047
- if (this._steps === value) {
2113
+ updateLines(steps) {
2114
+ if (!steps) {
2115
+ this.lines = {};
2048
2116
  return;
2049
2117
  }
2050
- const first = this._steps === undefined;
2051
- this.selectedStep = undefined;
2052
- this._steps = value || undefined;
2053
- if (first && this._steps?.length) {
2054
- this.selectedStep = this._steps[this._steps.length - 1];
2055
- this.selectedStepChange.emit(this.selectedStep);
2118
+ for (let i = 0; i < steps.length; i++) {
2119
+ const step = this.steps()[i];
2120
+ this.lines[step.outputTag] = step.result?.split('\n') || [];
2056
2121
  }
2057
- this.updateLines();
2058
- }
2059
- updateLines() {
2060
- this.lines = this.selectedStep?.result?.split('\n') || [];
2061
2122
  }
2062
2123
  onStepClick(step) {
2063
- this.selectedStep = step;
2064
- this.updateLines();
2065
- this.selectedStepChange.emit(step);
2124
+ this.selectedStep.set(step);
2125
+ this.selectedStepChange.emit(this.selectedStep());
2066
2126
  }
2067
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: StepsMapComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2068
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.0", type: StepsMapComponent, isStandalone: true, selector: "gve-steps-map", inputs: { steps: "steps", selectedStep: "selectedStep", textFontSize: "textFontSize" }, outputs: { selectedStepChange: "selectedStepChange" }, ngImport: i0, template: "<div>\r\n @if (steps?.length) { @for (step of steps; track step.outputTag) {\r\n <div\r\n matRipple\r\n class=\"step form-row\"\r\n [ngClass]=\"{ selected: step === selectedStep }\"\r\n (click)=\"onStepClick(step)\"\r\n >\r\n <div class=\"tag\">\r\n <span class=\"muted-tag\">{{ step.inputTag }} &#x25b6; </span>\r\n {{ step.outputTag }}\r\n </div>\r\n <div class=\"text\" [style.fontSize]=\"textFontSize\">\r\n @for (line of lines; track $index) {\r\n <div class=\"line\">{{ line }}</div>\r\n }\r\n <div></div>\r\n </div>\r\n </div>\r\n } }\r\n</div>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:start;flex-wrap:wrap}.form-row *{flex:0 0 auto}.selected{background-color:#ffc}.step{border:1px solid rgb(68,114,253);border-radius:6px;margin:8px 0}.tag{background-color:#4472fd;color:#fff;border:1px solid rgb(68,114,253);border-radius:6px;padding:4px;cursor:pointer}.muted-tag{color:silver}.text{font-size:.5em}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "ngmodule", type: MatRippleModule }, { kind: "directive", type: i9.MatRipple, selector: "[mat-ripple], [matRipple]", inputs: ["matRippleColor", "matRippleUnbounded", "matRippleCentered", "matRippleRadius", "matRippleAnimation", "matRippleDisabled", "matRippleTrigger"], exportAs: ["matRipple"] }, { kind: "ngmodule", type: MatTooltipModule }] }); }
2127
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: StepsMapComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2128
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.6", type: StepsMapComponent, isStandalone: true, selector: "gve-steps-map", inputs: { steps: { classPropertyName: "steps", publicName: "steps", isSignal: true, isRequired: false, transformFunction: null }, selectedStep: { classPropertyName: "selectedStep", publicName: "selectedStep", isSignal: true, isRequired: false, transformFunction: null }, textFontSize: { classPropertyName: "textFontSize", publicName: "textFontSize", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedStep: "selectedStepChange", selectedStepChange: "selectedStepChange" }, ngImport: i0, template: "<div>\r\n @if (steps().length) { @for (step of steps(); track step.outputTag) {\r\n <div\r\n matRipple\r\n class=\"step form-row\"\r\n [ngClass]=\"{ selected: step === selectedStep() }\"\r\n (click)=\"onStepClick(step)\"\r\n >\r\n <!-- tag -->\r\n <div class=\"tag\">\r\n <span class=\"muted-tag\">{{ step.inputTag }} &#x25b6; </span>\r\n {{ step.outputTag }}\r\n </div>\r\n <!-- text lines -->\r\n <div class=\"text\" [style.fontSize]=\"textFontSize()\">\r\n @for (line of lines[step.outputTag]; track $index) {\r\n <div class=\"line\">{{ line }}</div>\r\n }\r\n <div></div>\r\n </div>\r\n </div>\r\n } }\r\n</div>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:start;flex-wrap:wrap}.form-row *{flex:0 0 auto}.selected{background-color:#ffc}.step{border:1px solid rgb(68,114,253);border-radius:6px;margin:8px 0}.tag{background-color:#4472fd;color:#fff;border:1px solid rgb(68,114,253);border-radius:6px;padding:4px;cursor:pointer}.muted-tag{color:silver}.text{font-size:.5em}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "ngmodule", type: MatRippleModule }, { kind: "directive", type: i9.MatRipple, selector: "[mat-ripple], [matRipple]", inputs: ["matRippleColor", "matRippleUnbounded", "matRippleCentered", "matRippleRadius", "matRippleAnimation", "matRippleDisabled", "matRippleTrigger"], exportAs: ["matRipple"] }, { kind: "ngmodule", type: MatTooltipModule }] }); }
2069
2129
  }
2070
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: StepsMapComponent, decorators: [{
2130
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: StepsMapComponent, decorators: [{
2071
2131
  type: Component,
2072
- args: [{ selector: 'gve-steps-map', imports: [CommonModule, MatButtonModule, MatRippleModule, MatTooltipModule], template: "<div>\r\n @if (steps?.length) { @for (step of steps; track step.outputTag) {\r\n <div\r\n matRipple\r\n class=\"step form-row\"\r\n [ngClass]=\"{ selected: step === selectedStep }\"\r\n (click)=\"onStepClick(step)\"\r\n >\r\n <div class=\"tag\">\r\n <span class=\"muted-tag\">{{ step.inputTag }} &#x25b6; </span>\r\n {{ step.outputTag }}\r\n </div>\r\n <div class=\"text\" [style.fontSize]=\"textFontSize\">\r\n @for (line of lines; track $index) {\r\n <div class=\"line\">{{ line }}</div>\r\n }\r\n <div></div>\r\n </div>\r\n </div>\r\n } }\r\n</div>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:start;flex-wrap:wrap}.form-row *{flex:0 0 auto}.selected{background-color:#ffc}.step{border:1px solid rgb(68,114,253);border-radius:6px;margin:8px 0}.tag{background-color:#4472fd;color:#fff;border:1px solid rgb(68,114,253);border-radius:6px;padding:4px;cursor:pointer}.muted-tag{color:silver}.text{font-size:.5em}\n"] }]
2073
- }], propDecorators: { steps: [{
2074
- type: Input
2075
- }], selectedStep: [{
2076
- type: Input
2077
- }], textFontSize: [{
2078
- type: Input
2079
- }], selectedStepChange: [{
2080
- type: Output
2081
- }] } });
2132
+ args: [{ selector: 'gve-steps-map', imports: [CommonModule, MatButtonModule, MatRippleModule, MatTooltipModule], template: "<div>\r\n @if (steps().length) { @for (step of steps(); track step.outputTag) {\r\n <div\r\n matRipple\r\n class=\"step form-row\"\r\n [ngClass]=\"{ selected: step === selectedStep() }\"\r\n (click)=\"onStepClick(step)\"\r\n >\r\n <!-- tag -->\r\n <div class=\"tag\">\r\n <span class=\"muted-tag\">{{ step.inputTag }} &#x25b6; </span>\r\n {{ step.outputTag }}\r\n </div>\r\n <!-- text lines -->\r\n <div class=\"text\" [style.fontSize]=\"textFontSize()\">\r\n @for (line of lines[step.outputTag]; track $index) {\r\n <div class=\"line\">{{ line }}</div>\r\n }\r\n <div></div>\r\n </div>\r\n </div>\r\n } }\r\n</div>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:start;flex-wrap:wrap}.form-row *{flex:0 0 auto}.selected{background-color:#ffc}.step{border:1px solid rgb(68,114,253);border-radius:6px;margin:8px 0}.tag{background-color:#4472fd;color:#fff;border:1px solid rgb(68,114,253);border-radius:6px;padding:4px;cursor:pointer}.muted-tag{color:silver}.text{font-size:.5em}\n"] }]
2133
+ }], ctorParameters: () => [] });
2082
2134
 
2083
2135
  /**
2084
2136
  * 🔑 `gve-chain-result-view`
@@ -2094,48 +2146,37 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImpor
2094
2146
  * result's step is picked by the user.
2095
2147
  */
2096
2148
  class ChainResultViewComponent {
2097
- /**
2098
- * The result to display.
2099
- */
2100
- get result() {
2101
- return this._result;
2102
- }
2103
- set result(value) {
2104
- if (this._result === value) {
2105
- return;
2106
- }
2107
- this._result = value || undefined;
2108
- this.updateForm(this._result);
2109
- // select the initial step if set
2110
- this.selectInitialStep();
2111
- }
2112
- /**
2113
- * The index of the initial step to display after the result is set.
2114
- * If the index is negative, it is counted from the end of the steps.
2115
- */
2116
- get initialStepIndex() {
2117
- return this._initialStepIndex;
2118
- }
2119
- set initialStepIndex(value) {
2120
- if (value === undefined || value === null) {
2121
- this._initialStepIndex = undefined;
2122
- this.step = undefined;
2123
- }
2124
- else {
2125
- this._initialStepIndex = value;
2126
- this.selectInitialStep();
2127
- }
2128
- }
2129
2149
  constructor(formBuilder) {
2150
+ /**
2151
+ * The result to display.
2152
+ */
2153
+ this.result = input();
2154
+ /**
2155
+ * The index of the initial step to display after the result is set.
2156
+ * If the index is negative, it is counted from the end of the steps.
2157
+ */
2158
+ this.initialStepIndex = input();
2130
2159
  /**
2131
2160
  * Emitted when a result's step is picked by the user.
2132
2161
  */
2133
- this.stepPick = new EventEmitter();
2162
+ this.stepPick = output();
2134
2163
  this.versionTags = [];
2135
2164
  this.tags = [];
2136
2165
  this.selectionFeatures = [];
2166
+ // bind callback methods
2167
+ this.getCharBorderColor = this.getCharBorderColor.bind(this);
2137
2168
  this.versionTag = formBuilder.control(null);
2138
2169
  this.tag = formBuilder.control(null);
2170
+ // when result changes, update the form
2171
+ effect(() => {
2172
+ this.updateForm(this.result());
2173
+ // select the initial step if set
2174
+ this.selectInitialStep(this.initialStepIndex());
2175
+ });
2176
+ // when initialStepIndex changes, update the selected step
2177
+ effect(() => {
2178
+ this.selectInitialStep(this.initialStepIndex());
2179
+ });
2139
2180
  }
2140
2181
  ngOnInit() {
2141
2182
  this._subs = [
@@ -2149,32 +2190,38 @@ class ChainResultViewComponent {
2149
2190
  this.tag.valueChanges
2150
2191
  .pipe(distinctUntilChanged(), debounceTime(200))
2151
2192
  .subscribe((value) => {
2152
- this.step = this._result?.steps.find((step) => step.outputTag === value);
2193
+ this.step = this.result()?.steps.find((step) => step.outputTag === value);
2153
2194
  if (this.step) {
2154
2195
  this.stepPick.emit(this.step);
2155
2196
  }
2156
2197
  }),
2157
2198
  ];
2158
2199
  }
2159
- selectInitialStep() {
2160
- if (this._initialStepIndex !== undefined && this._result?.steps) {
2200
+ getCharBorderColor(node) {
2201
+ return this.step?.refNodeIds.includes(node.id) ? 'orange' : null;
2202
+ }
2203
+ selectInitialStep(initialStepIndex) {
2204
+ if (initialStepIndex !== undefined && this.result()?.steps) {
2161
2205
  this.step =
2162
- this._result.steps[this._initialStepIndex < 0
2163
- ? this._result.steps.length + this._initialStepIndex
2164
- : this._initialStepIndex];
2206
+ this.result().steps[initialStepIndex < 0
2207
+ ? this.result().steps.length + initialStepIndex
2208
+ : initialStepIndex];
2209
+ }
2210
+ else {
2211
+ this.step = undefined;
2165
2212
  }
2166
2213
  }
2167
2214
  ngOnDestroy() {
2168
2215
  this._subs?.forEach((sub) => sub.unsubscribe());
2169
2216
  }
2170
2217
  updateVersionTags() {
2171
- if (!this._result) {
2218
+ if (!this.result()) {
2172
2219
  this.versionTags = [];
2173
2220
  return;
2174
2221
  }
2175
2222
  // extract version tags from result steps features named 'version'
2176
2223
  const tags = new Set();
2177
- for (const step of this._result.steps) {
2224
+ for (const step of this.result().steps) {
2178
2225
  for (const feature of step.featureSet.features) {
2179
2226
  if (feature.name === 'version') {
2180
2227
  tags.add(feature.value);
@@ -2184,10 +2231,10 @@ class ChainResultViewComponent {
2184
2231
  this.versionTags = Array.from(tags).sort();
2185
2232
  }
2186
2233
  getTagFromVersion(version) {
2187
- if (!this._result || !version) {
2234
+ if (!this.result() || !version) {
2188
2235
  return undefined;
2189
2236
  }
2190
- for (const step of this._result.steps) {
2237
+ for (const step of this.result().steps) {
2191
2238
  for (const feature of step.featureSet.features) {
2192
2239
  if (feature.name === 'version' && feature.value === version) {
2193
2240
  return step.outputTag;
@@ -2219,8 +2266,8 @@ class ChainResultViewComponent {
2219
2266
  if (!this.step) {
2220
2267
  return null;
2221
2268
  }
2222
- for (let key of Object.keys(this._result.taggedNodes)) {
2223
- const node = this._result.taggedNodes[key].find((n) => n.id === id);
2269
+ for (let key of Object.keys(this.result().taggedNodes)) {
2270
+ const node = this.result().taggedNodes[key].find((n) => n.id === id);
2224
2271
  if (node) {
2225
2272
  return node;
2226
2273
  }
@@ -2241,7 +2288,7 @@ class ChainResultViewComponent {
2241
2288
  this.selection = range.at + '×' + range.run;
2242
2289
  const end = range.at + range.run;
2243
2290
  const features = [];
2244
- const tagNodes = this._result.taggedNodes[this.tag.value];
2291
+ const tagNodes = this.result().taggedNodes[this.tag.value];
2245
2292
  for (let node of tagNodes) {
2246
2293
  // if in range, intersect features
2247
2294
  if (node.id >= range.at && node.id < end) {
@@ -2260,14 +2307,16 @@ class ChainResultViewComponent {
2260
2307
  this.selectionFeatures = features;
2261
2308
  }
2262
2309
  onStepChange(step) {
2263
- // setting the tag will trigger the step change
2264
- this.tag.setValue(step.outputTag);
2265
- this.selectionFeatures = [];
2310
+ if (step) {
2311
+ // setting the tag will trigger the step change
2312
+ this.tag.setValue(step.outputTag);
2313
+ this.selectionFeatures = [];
2314
+ }
2266
2315
  }
2267
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: ChainResultViewComponent, deps: [{ token: i1.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); }
2268
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.0", type: ChainResultViewComponent, isStandalone: true, selector: "gve-chain-result-view", inputs: { result: "result", initialStepIndex: "initialStepIndex" }, outputs: { stepPick: "stepPick" }, ngImport: i0, template: "@if (result) {\r\n<div id=\"container\">\r\n <div id=\"bar\" class=\"form-row\">\r\n <!-- version -->\r\n <mat-form-field *ngIf=\"versionTags?.length\">\r\n <mat-label>version</mat-label>\r\n <mat-select [formControl]=\"versionTag\">\r\n <mat-option [value]=\"null\">-</mat-option>\r\n <mat-option *ngFor=\"let t of versionTags\" [value]=\"t\">{{\r\n t\r\n }}</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n <!-- tag -->\r\n <mat-form-field *ngIf=\"tags?.length\">\r\n <mat-label>tag</mat-label>\r\n <mat-select [formControl]=\"tag\">\r\n <mat-option *ngFor=\"let t of tags\" [value]=\"t\">{{ t }}</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- step -->\r\n @if (step) {\r\n <div id=\"step\">\r\n <!-- text -->\r\n <div id=\"text\">\r\n <gve-base-text-view\r\n [text]=\"result.taggedNodes[step.outputTag]\"\r\n (charPick)=\"onStepCharPick($event)\"\r\n (rangePick)=\"onStepRangePick($event)\"\r\n />\r\n </div>\r\n <!-- features -->\r\n <div id=\"feats\" class=\"form-row-top\">\r\n <div id=\"g-feats\">\r\n <div class=\"feat-header\">{{ step.outputTag }}</div>\r\n <gve-feature-set-view [features]=\"step.featureSet.features\" />\r\n </div>\r\n <div id=\"n-feats\">\r\n @if (selectionFeatures.length) {\r\n <div class=\"feat-header\">{{ selection }}</div>\r\n <gve-feature-set-view [features]=\"selectionFeatures\" />\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- steps map -->\r\n <div id=\"map\">\r\n <gve-steps-map\r\n [steps]=\"result.steps\"\r\n [selectedStep]=\"step\"\r\n (selectedStepChange)=\"onStepChange($event)\"\r\n />\r\n </div>\r\n</div>\r\n}\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}.form-row-top{display:flex;gap:8px;align-items:start;flex-wrap:wrap}div#container{display:grid;grid-template-rows:auto 1fr;grid-template-columns:3fr 1fr;grid-template-areas:\"bar map\" \"step map\";gap:8px}div#bar{grid-area:bar}div#map{grid-area:map}div#step{grid-area:step}.feat-header{background-color:#3e92cc;color:#fff;text-align:center;border:1px solid #3e92cc;border-top-left-radius:4px;border-top-right-radius:4px}@media only screen and (max-width: 959px){div#container{grid-template-columns:1fr;grid-template-areas:\"bar\" \"step\"}#map{display:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatIconModule }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i8.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i9.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: FeatureSetViewComponent, selector: "gve-feature-set-view", inputs: ["features", "featNames", "featValues", "filterThreshold"] }, { kind: "component", type: StepsMapComponent, selector: "gve-steps-map", inputs: ["steps", "selectedStep", "textFontSize"], outputs: ["selectedStepChange"] }, { kind: "component", type: BaseTextViewComponent, selector: "gve-base-text-view", inputs: ["defaultColor", "defaultBorderColor", "selectionColor", "hasLineNumber", "text"], outputs: ["charPick", "rangePick"] }] }); }
2316
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: ChainResultViewComponent, deps: [{ token: i1.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); }
2317
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.6", type: ChainResultViewComponent, isStandalone: true, selector: "gve-chain-result-view", inputs: { result: { classPropertyName: "result", publicName: "result", isSignal: true, isRequired: false, transformFunction: null }, initialStepIndex: { classPropertyName: "initialStepIndex", publicName: "initialStepIndex", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { stepPick: "stepPick" }, ngImport: i0, template: "@if (result()) {\r\n<div id=\"container\">\r\n <div id=\"bar\" class=\"form-row\">\r\n <!-- version -->\r\n <mat-form-field *ngIf=\"versionTags?.length\">\r\n <mat-label>version</mat-label>\r\n <mat-select [formControl]=\"versionTag\">\r\n <mat-option [value]=\"null\">-</mat-option>\r\n <mat-option *ngFor=\"let t of versionTags\" [value]=\"t\">{{\r\n t\r\n }}</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n <!-- tag -->\r\n <mat-form-field *ngIf=\"tags?.length\">\r\n <mat-label>tag</mat-label>\r\n <mat-select [formControl]=\"tag\">\r\n <mat-option *ngFor=\"let t of tags\" [value]=\"t\">{{ t }}</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- step -->\r\n @if (step) {\r\n <div id=\"step\">\r\n <!-- text -->\r\n <div id=\"text\">\r\n <gve-base-text-view\r\n [text]=\"result()!.taggedNodes[step.outputTag]\"\r\n [borderColorCallback]=\"getCharBorderColor\"\r\n (charPick)=\"onStepCharPick($event)\"\r\n (rangePick)=\"onStepRangePick($event)\"\r\n />\r\n </div>\r\n <!-- features -->\r\n <div id=\"feats\" class=\"form-row-top\">\r\n <div id=\"g-feats\">\r\n <div class=\"feat-header\">{{ step.outputTag }}</div>\r\n <gve-feature-set-view [features]=\"step.featureSet.features\" />\r\n </div>\r\n <div id=\"n-feats\">\r\n @if (selectionFeatures.length) {\r\n <div class=\"feat-header\">{{ selection }}</div>\r\n <gve-feature-set-view [features]=\"selectionFeatures\" />\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- steps map -->\r\n <div id=\"map\">\r\n <gve-steps-map\r\n [steps]=\"result()!.steps\"\r\n [selectedStep]=\"step\"\r\n (selectedStepChange)=\"onStepChange($event)\"\r\n />\r\n </div>\r\n</div>\r\n}\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}.form-row-top{display:flex;gap:8px;align-items:start;flex-wrap:wrap}div#container{display:grid;grid-template-rows:auto 1fr;grid-template-columns:3fr 1fr;grid-template-areas:\"bar map\" \"step map\";gap:8px}div#bar{grid-area:bar}div#map{grid-area:map}div#step{grid-area:step}.feat-header{background-color:#3e92cc;color:#fff;text-align:center;border:1px solid #3e92cc;border-top-left-radius:4px;border-top-right-radius:4px}@media only screen and (max-width: 959px){div#container{grid-template-columns:1fr;grid-template-areas:\"bar\" \"step\"}#map{display:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatIconModule }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i8.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i9.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: FeatureSetViewComponent, selector: "gve-feature-set-view", inputs: ["features", "featNames", "featValues", "filterThreshold"] }, { kind: "component", type: StepsMapComponent, selector: "gve-steps-map", inputs: ["steps", "selectedStep", "textFontSize"], outputs: ["selectedStepChange"] }, { kind: "component", type: BaseTextViewComponent, selector: "gve-base-text-view", inputs: ["defaultColor", "defaultBorderColor", "selectionColor", "hasLineNumber", "text", "borderColorCallback"], outputs: ["charPick", "rangePick"] }] }); }
2269
2318
  }
2270
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: ChainResultViewComponent, decorators: [{
2319
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: ChainResultViewComponent, decorators: [{
2271
2320
  type: Component,
2272
2321
  args: [{ selector: 'gve-chain-result-view', imports: [
2273
2322
  CommonModule,
@@ -2279,14 +2328,230 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImpor
2279
2328
  FeatureSetViewComponent,
2280
2329
  StepsMapComponent,
2281
2330
  BaseTextViewComponent,
2282
- ], template: "@if (result) {\r\n<div id=\"container\">\r\n <div id=\"bar\" class=\"form-row\">\r\n <!-- version -->\r\n <mat-form-field *ngIf=\"versionTags?.length\">\r\n <mat-label>version</mat-label>\r\n <mat-select [formControl]=\"versionTag\">\r\n <mat-option [value]=\"null\">-</mat-option>\r\n <mat-option *ngFor=\"let t of versionTags\" [value]=\"t\">{{\r\n t\r\n }}</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n <!-- tag -->\r\n <mat-form-field *ngIf=\"tags?.length\">\r\n <mat-label>tag</mat-label>\r\n <mat-select [formControl]=\"tag\">\r\n <mat-option *ngFor=\"let t of tags\" [value]=\"t\">{{ t }}</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- step -->\r\n @if (step) {\r\n <div id=\"step\">\r\n <!-- text -->\r\n <div id=\"text\">\r\n <gve-base-text-view\r\n [text]=\"result.taggedNodes[step.outputTag]\"\r\n (charPick)=\"onStepCharPick($event)\"\r\n (rangePick)=\"onStepRangePick($event)\"\r\n />\r\n </div>\r\n <!-- features -->\r\n <div id=\"feats\" class=\"form-row-top\">\r\n <div id=\"g-feats\">\r\n <div class=\"feat-header\">{{ step.outputTag }}</div>\r\n <gve-feature-set-view [features]=\"step.featureSet.features\" />\r\n </div>\r\n <div id=\"n-feats\">\r\n @if (selectionFeatures.length) {\r\n <div class=\"feat-header\">{{ selection }}</div>\r\n <gve-feature-set-view [features]=\"selectionFeatures\" />\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- steps map -->\r\n <div id=\"map\">\r\n <gve-steps-map\r\n [steps]=\"result.steps\"\r\n [selectedStep]=\"step\"\r\n (selectedStepChange)=\"onStepChange($event)\"\r\n />\r\n </div>\r\n</div>\r\n}\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}.form-row-top{display:flex;gap:8px;align-items:start;flex-wrap:wrap}div#container{display:grid;grid-template-rows:auto 1fr;grid-template-columns:3fr 1fr;grid-template-areas:\"bar map\" \"step map\";gap:8px}div#bar{grid-area:bar}div#map{grid-area:map}div#step{grid-area:step}.feat-header{background-color:#3e92cc;color:#fff;text-align:center;border:1px solid #3e92cc;border-top-left-radius:4px;border-top-right-radius:4px}@media only screen and (max-width: 959px){div#container{grid-template-columns:1fr;grid-template-areas:\"bar\" \"step\"}#map{display:none}}\n"] }]
2283
- }], ctorParameters: () => [{ type: i1.FormBuilder }], propDecorators: { result: [{
2284
- type: Input
2285
- }], initialStepIndex: [{
2286
- type: Input
2287
- }], stepPick: [{
2288
- type: Output
2289
- }] } });
2331
+ ], template: "@if (result()) {\r\n<div id=\"container\">\r\n <div id=\"bar\" class=\"form-row\">\r\n <!-- version -->\r\n <mat-form-field *ngIf=\"versionTags?.length\">\r\n <mat-label>version</mat-label>\r\n <mat-select [formControl]=\"versionTag\">\r\n <mat-option [value]=\"null\">-</mat-option>\r\n <mat-option *ngFor=\"let t of versionTags\" [value]=\"t\">{{\r\n t\r\n }}</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n <!-- tag -->\r\n <mat-form-field *ngIf=\"tags?.length\">\r\n <mat-label>tag</mat-label>\r\n <mat-select [formControl]=\"tag\">\r\n <mat-option *ngFor=\"let t of tags\" [value]=\"t\">{{ t }}</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- step -->\r\n @if (step) {\r\n <div id=\"step\">\r\n <!-- text -->\r\n <div id=\"text\">\r\n <gve-base-text-view\r\n [text]=\"result()!.taggedNodes[step.outputTag]\"\r\n [borderColorCallback]=\"getCharBorderColor\"\r\n (charPick)=\"onStepCharPick($event)\"\r\n (rangePick)=\"onStepRangePick($event)\"\r\n />\r\n </div>\r\n <!-- features -->\r\n <div id=\"feats\" class=\"form-row-top\">\r\n <div id=\"g-feats\">\r\n <div class=\"feat-header\">{{ step.outputTag }}</div>\r\n <gve-feature-set-view [features]=\"step.featureSet.features\" />\r\n </div>\r\n <div id=\"n-feats\">\r\n @if (selectionFeatures.length) {\r\n <div class=\"feat-header\">{{ selection }}</div>\r\n <gve-feature-set-view [features]=\"selectionFeatures\" />\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- steps map -->\r\n <div id=\"map\">\r\n <gve-steps-map\r\n [steps]=\"result()!.steps\"\r\n [selectedStep]=\"step\"\r\n (selectedStepChange)=\"onStepChange($event)\"\r\n />\r\n </div>\r\n</div>\r\n}\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}.form-row-top{display:flex;gap:8px;align-items:start;flex-wrap:wrap}div#container{display:grid;grid-template-rows:auto 1fr;grid-template-columns:3fr 1fr;grid-template-areas:\"bar map\" \"step map\";gap:8px}div#bar{grid-area:bar}div#map{grid-area:map}div#step{grid-area:step}.feat-header{background-color:#3e92cc;color:#fff;text-align:center;border:1px solid #3e92cc;border-top-left-radius:4px;border-top-right-radius:4px}@media only screen and (max-width: 959px){div#container{grid-template-columns:1fr;grid-template-areas:\"bar\" \"step\"}#map{display:none}}\n"] }]
2332
+ }], ctorParameters: () => [{ type: i1.FormBuilder }] });
2333
+
2334
+ class GveGraphvizService {
2335
+ hashString(str) {
2336
+ let hash = 0;
2337
+ for (let i = 0; i < str.length; i++) {
2338
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
2339
+ hash = hash & hash; // convert to 32bit integer
2340
+ }
2341
+ return Math.abs(hash);
2342
+ }
2343
+ hslToRgb(hue, saturation, lightness) {
2344
+ const chroma = ((1 - Math.abs((2 * lightness) / 100 - 1)) * saturation) / 100;
2345
+ const x = chroma * (1 - Math.abs(((hue / 60) % 2) - 1));
2346
+ const m = lightness / 100 - chroma / 2;
2347
+ let r = 0, g = 0, b = 0;
2348
+ if (hue >= 0 && hue < 60) {
2349
+ r = chroma;
2350
+ g = x;
2351
+ }
2352
+ else if (hue >= 60 && hue < 120) {
2353
+ r = x;
2354
+ g = chroma;
2355
+ }
2356
+ else if (hue >= 120 && hue < 180) {
2357
+ g = chroma;
2358
+ b = x;
2359
+ }
2360
+ else if (hue >= 180 && hue < 240) {
2361
+ g = x;
2362
+ b = chroma;
2363
+ }
2364
+ else if (hue >= 240 && hue < 300) {
2365
+ r = x;
2366
+ b = chroma;
2367
+ }
2368
+ else if (hue >= 300 && hue < 360) {
2369
+ r = chroma;
2370
+ b = x;
2371
+ }
2372
+ r = Math.round((r + m) * 255);
2373
+ g = Math.round((g + m) * 255);
2374
+ b = Math.round((b + m) * 255);
2375
+ return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
2376
+ }
2377
+ getColorForTag(tag) {
2378
+ const hash = this.hashString(tag);
2379
+ const hue = (hash * 137) % 360; // Use a prime number to spread out the hues
2380
+ const saturation = 60 + (hash % 40); // Saturation between 60% and 100%
2381
+ const lightness = 50 + (hash % 30); // Lightness between 50% and 80%
2382
+ return this.hslToRgb(hue, saturation, lightness);
2383
+ }
2384
+ getExcludedNodeIds(chain, tags) {
2385
+ // get only the desired links
2386
+ const links = chain.links.filter((link) => tags ? tags.includes(link.tag) : true);
2387
+ // return all the nodes not referenced by the links
2388
+ const excluded = new Set();
2389
+ chain.nodes.forEach((node) => {
2390
+ excluded.add(node.id);
2391
+ });
2392
+ links.forEach((link) => {
2393
+ excluded.delete(link.sourceId);
2394
+ excluded.delete(link.targetId);
2395
+ });
2396
+ return excluded;
2397
+ }
2398
+ /**
2399
+ * Represent the received chain as a Graphviz digraph.
2400
+ *
2401
+ * @param chain The source chain if any.
2402
+ * @param tags The tags to show. When set, only the links with these tags are shown.
2403
+ * @param rankdir The rank direction.
2404
+ * @returns Graphviz representation of the chain.
2405
+ */
2406
+ generateGraph(chain, tags, rankdir = 'LR') {
2407
+ if (!chain) {
2408
+ return 'digraph G {}';
2409
+ }
2410
+ const sb = [];
2411
+ const excludedNodeIds = this.getExcludedNodeIds(chain, tags);
2412
+ sb.push('digraph G {');
2413
+ sb.push(' bgcolor=transparent;');
2414
+ sb.push(' node [style=filled];');
2415
+ sb.push(` rankdir=${rankdir};`);
2416
+ chain.nodes.forEach((node) => {
2417
+ sb.push(` ${node.id} [label="${node.label}"` +
2418
+ (node.sourceTag
2419
+ ? ` fillcolor="${this.getColorForTag(node.sourceTag)}"`
2420
+ : '') +
2421
+ (node.sourceTag && excludedNodeIds.has(node.id)
2422
+ ? ` xlabel="${node.sourceTag}"`
2423
+ : '') +
2424
+ '];');
2425
+ });
2426
+ chain.links.forEach((link) => {
2427
+ if (!link.sourceId ||
2428
+ !link.targetId ||
2429
+ (tags && !tags.includes(link.tag))) {
2430
+ return;
2431
+ }
2432
+ const color = this.getColorForTag(link.tag);
2433
+ sb.push(` ${link.sourceId} -> ${link.targetId} [label="${link.tag}", color="${color}"];`);
2434
+ });
2435
+ sb.push('}');
2436
+ return sb.join('\n');
2437
+ }
2438
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: GveGraphvizService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2439
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: GveGraphvizService, providedIn: 'root' }); }
2440
+ }
2441
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: GveGraphvizService, decorators: [{
2442
+ type: Injectable,
2443
+ args: [{
2444
+ providedIn: 'root',
2445
+ }]
2446
+ }] });
2447
+
2448
+ class ChainViewComponent {
2449
+ constructor(_graphviz, _settings, _clipboard, _snackbar) {
2450
+ this._graphviz = _graphviz;
2451
+ this._settings = _settings;
2452
+ this._clipboard = _clipboard;
2453
+ this._snackbar = _snackbar;
2454
+ /**
2455
+ * The chain to display.
2456
+ */
2457
+ this.chain = input();
2458
+ /**
2459
+ * The direction of the graph.
2460
+ */
2461
+ this.direction = input('LR');
2462
+ /**
2463
+ * All the distinct tags in the chain.
2464
+ */
2465
+ this.tags = computed(() => {
2466
+ const chain = this.chain();
2467
+ if (!chain) {
2468
+ return [];
2469
+ }
2470
+ const set = new Set();
2471
+ for (const link of chain.links) {
2472
+ set.add(link.tag);
2473
+ }
2474
+ return Array.from(set).sort();
2475
+ });
2476
+ /**
2477
+ * The tags to show, or empty to show all of them.
2478
+ */
2479
+ this.selectedTags = model([]);
2480
+ /**
2481
+ * The Graphviz representation of the chain.
2482
+ */
2483
+ this.graph = computed(() => {
2484
+ let tags = this.selectedTags();
2485
+ if (tags && tags.length === 0) {
2486
+ tags = undefined;
2487
+ }
2488
+ return this._graphviz.generateGraph(this.chain(), tags, this.direction());
2489
+ });
2490
+ this.userTags = new FormControl([], {
2491
+ nonNullable: true,
2492
+ });
2493
+ // when the user changes the tags, update the selected tags
2494
+ this._sub = this.userTags.valueChanges.subscribe((value) => {
2495
+ this.selectedTags.set([...value]);
2496
+ });
2497
+ effect(() => {
2498
+ // update the user tags when tags change
2499
+ const tags = this.tags();
2500
+ let userTags = [];
2501
+ if (tags.length > 1) {
2502
+ // pick first and last tag from tags
2503
+ userTags = [tags[0], tags[tags.length - 1]];
2504
+ }
2505
+ else {
2506
+ // if there is only one tag, pick it
2507
+ userTags = [...tags];
2508
+ }
2509
+ this.userTags.setValue(userTags);
2510
+ });
2511
+ }
2512
+ ngOnDestroy() {
2513
+ this._sub?.unsubscribe();
2514
+ }
2515
+ copyGraph() {
2516
+ const graph = this.graph();
2517
+ if (!graph) {
2518
+ return;
2519
+ }
2520
+ this._clipboard.copy(graph);
2521
+ this._snackbar.open('Graph copied to clipboard', 'OK', {
2522
+ duration: 2000,
2523
+ });
2524
+ }
2525
+ openExternalEditor() {
2526
+ let url = this._settings.get('graphviz-editor', 'https://dreampuf.github.io/GraphvizOnline?engine=dot#CODE');
2527
+ if (!url) {
2528
+ return;
2529
+ }
2530
+ const graph = this.graph();
2531
+ if (!graph) {
2532
+ return;
2533
+ }
2534
+ if (url.indexOf('CODE') > -1) {
2535
+ url = url.replace('CODE', encodeURIComponent(graph));
2536
+ }
2537
+ window.open(url, '_blank');
2538
+ }
2539
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: ChainViewComponent, deps: [{ token: GveGraphvizService }, { token: SettingsService }, { token: i2$3.Clipboard }, { token: i4$2.MatSnackBar }], target: i0.ɵɵFactoryTarget.Component }); }
2540
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.6", type: ChainViewComponent, isStandalone: true, selector: "gve-chain-view", inputs: { chain: { classPropertyName: "chain", publicName: "chain", isSignal: true, isRequired: false, transformFunction: null }, direction: { classPropertyName: "direction", publicName: "direction", isSignal: true, isRequired: false, transformFunction: null }, selectedTags: { classPropertyName: "selectedTags", publicName: "selectedTags", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedTags: "selectedTagsChange" }, ngImport: i0, template: "<div id=\"container\">\r\n <div>\r\n <ngx-viz [code]=\"graph()\" />\r\n </div>\r\n <div class=\"button-row\">\r\n <!-- select -->\r\n <mat-form-field>\r\n <mat-label>tags</mat-label>\r\n <mat-select [formControl]=\"userTags\" multiple>\r\n @for (tag of tags(); track tag) {\r\n <mat-option [value]=\"tag\">{{ tag }}</mat-option>\r\n }\r\n </mat-select>\r\n </mat-form-field>\r\n <!-- copy -->\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Copy graphviz code\"\r\n [disabled]=\"!graph()\"\r\n (click)=\"copyGraph()\"\r\n >\r\n <mat-icon>content_copy</mat-icon>\r\n </button>\r\n <!--editor -->\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Open in editor\"\r\n [disabled]=\"!graph()\"\r\n (click)=\"openExternalEditor()\"\r\n >\r\n <mat-icon>launch</mat-icon>\r\n </button>\r\n </div>\r\n</div>\r\n", styles: ["div#container{border:1px solid silver;border-radius:6px;padding:4px}.button-row{display:flex;align-items:center;flex-wrap:wrap}.button-row *{flex:0 0 auto}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i8.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i9.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i10.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: VizComponent, selector: "ngx-viz", inputs: ["code"] }] }); }
2541
+ }
2542
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: ChainViewComponent, decorators: [{
2543
+ type: Component,
2544
+ args: [{ selector: 'gve-chain-view', imports: [
2545
+ CommonModule,
2546
+ ReactiveFormsModule,
2547
+ MatButtonModule,
2548
+ MatFormFieldModule,
2549
+ MatIconModule,
2550
+ MatSelectModule,
2551
+ MatTooltipModule,
2552
+ VizComponent,
2553
+ ], schemas: [CUSTOM_ELEMENTS_SCHEMA], template: "<div id=\"container\">\r\n <div>\r\n <ngx-viz [code]=\"graph()\" />\r\n </div>\r\n <div class=\"button-row\">\r\n <!-- select -->\r\n <mat-form-field>\r\n <mat-label>tags</mat-label>\r\n <mat-select [formControl]=\"userTags\" multiple>\r\n @for (tag of tags(); track tag) {\r\n <mat-option [value]=\"tag\">{{ tag }}</mat-option>\r\n }\r\n </mat-select>\r\n </mat-form-field>\r\n <!-- copy -->\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Copy graphviz code\"\r\n [disabled]=\"!graph()\"\r\n (click)=\"copyGraph()\"\r\n >\r\n <mat-icon>content_copy</mat-icon>\r\n </button>\r\n <!--editor -->\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Open in editor\"\r\n [disabled]=\"!graph()\"\r\n (click)=\"openExternalEditor()\"\r\n >\r\n <mat-icon>launch</mat-icon>\r\n </button>\r\n </div>\r\n</div>\r\n", styles: ["div#container{border:1px solid silver;border-radius:6px;padding:4px}.button-row{display:flex;align-items:center;flex-wrap:wrap}.button-row *{flex:0 0 auto}\n"] }]
2554
+ }], ctorParameters: () => [{ type: GveGraphvizService }, { type: SettingsService }, { type: i2$3.Clipboard }, { type: i4$2.MatSnackBar }] });
2290
2555
 
2291
2556
  /**
2292
2557
  * 🔑 `gve-ln-heights-editor`
@@ -2300,39 +2565,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImpor
2300
2565
  * emitted when the heights change.
2301
2566
  */
2302
2567
  class LnHeightsEditorComponent {
2303
- /**
2304
- * The total number of lines in the text.
2305
- */
2306
- get lineCount() {
2307
- return this._lineCount;
2308
- }
2309
- set lineCount(value) {
2310
- if (this._lineCount === value)
2311
- return;
2312
- this._lineCount = value;
2313
- this.lineNumbers = Array.from({ length: value }, (_, i) => i + 1);
2314
- }
2315
- /**
2316
- * The heights map of the lines. Each key is a line number and the value is
2317
- * the height of the line.
2318
- */
2319
- get heights() {
2320
- return this._heights;
2321
- }
2322
- set heights(value) {
2323
- if (this._heights === value)
2324
- return;
2325
- this._heights = value || undefined;
2326
- }
2327
2568
  constructor(formBuilder) {
2328
- this._lineCount = 0;
2569
+ /**
2570
+ * The total number of lines in the text.
2571
+ */
2572
+ this.lineCount = input(0);
2573
+ /**
2574
+ * The heights map of the lines. Each key is a line number and the value is
2575
+ * the height of the line.
2576
+ */
2577
+ this.heights = input();
2329
2578
  /**
2330
2579
  * The event emitted when the heights change.
2331
2580
  */
2332
- this.heightsChange = new EventEmitter();
2581
+ this.heightsChange = output();
2333
2582
  this.lineNumbers = [];
2334
2583
  this.lineNumber = formBuilder.control(0, { nonNullable: true });
2335
2584
  this.height = formBuilder.control(0, { nonNullable: true });
2585
+ // when lineCount changes, update lineNumbers
2586
+ effect(() => {
2587
+ this.lineNumbers = Array.from({ length: this.lineCount() }, (_, i) => i + 1);
2588
+ });
2336
2589
  }
2337
2590
  pruneHeights() {
2338
2591
  // remove all the heigths with value=0
@@ -2371,10 +2624,10 @@ class LnHeightsEditorComponent {
2371
2624
  this._heights = undefined;
2372
2625
  this.heightsChange.emit(undefined);
2373
2626
  }
2374
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: LnHeightsEditorComponent, deps: [{ token: i1.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); }
2375
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.0", type: LnHeightsEditorComponent, isStandalone: true, selector: "gve-ln-heights-editor", inputs: { lineCount: "lineCount", heights: "heights" }, outputs: { heightsChange: "heightsChange" }, ngImport: i0, template: "@if (lineNumbers.length) {\r\n<div class=\"form-row\">\r\n <!-- line number -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>line</mat-label>\r\n <mat-select [formControl]=\"lineNumber\">\r\n <mat-option *ngFor=\"let n of lineNumbers\" [value]=\"n\">\r\n {{ n }}\r\n </mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n\r\n <!-- height -->\r\n @if (lineNumber.value) {\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>height</mat-label>\r\n <input matInput type=\"number\" [formControl]=\"height\" min=\"0\" />\r\n </mat-form-field>\r\n }\r\n\r\n <!-- reset button -->\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n color=\"warn\"\r\n (click)=\"reset()\"\r\n matTooltip=\"Remove all the heights\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n</div>\r\n}\r\n", styles: [".input-nr{width:5em}.form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i8.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i9.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i10.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] }); }
2627
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: LnHeightsEditorComponent, deps: [{ token: i1.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); }
2628
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.6", type: LnHeightsEditorComponent, isStandalone: true, selector: "gve-ln-heights-editor", inputs: { lineCount: { classPropertyName: "lineCount", publicName: "lineCount", isSignal: true, isRequired: false, transformFunction: null }, heights: { classPropertyName: "heights", publicName: "heights", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { heightsChange: "heightsChange" }, ngImport: i0, template: "@if (lineNumbers.length) {\r\n<div class=\"form-row\">\r\n <!-- line number -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>line</mat-label>\r\n <mat-select [formControl]=\"lineNumber\">\r\n <mat-option *ngFor=\"let n of lineNumbers\" [value]=\"n\">\r\n {{ n }}\r\n </mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n\r\n <!-- height -->\r\n @if (lineNumber.value) {\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>height</mat-label>\r\n <input matInput type=\"number\" [formControl]=\"height\" min=\"0\" />\r\n </mat-form-field>\r\n }\r\n\r\n <!-- reset button -->\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n color=\"warn\"\r\n (click)=\"reset()\"\r\n matTooltip=\"Remove all the heights\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n</div>\r\n}\r\n", styles: [".input-nr{width:5em}.form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i8.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i9.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i10.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] }); }
2376
2629
  }
2377
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: LnHeightsEditorComponent, decorators: [{
2630
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: LnHeightsEditorComponent, decorators: [{
2378
2631
  type: Component,
2379
2632
  args: [{ selector: 'gve-ln-heights-editor', imports: [
2380
2633
  CommonModule,
@@ -2386,76 +2639,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImpor
2386
2639
  MatSelectModule,
2387
2640
  MatTooltipModule,
2388
2641
  ], template: "@if (lineNumbers.length) {\r\n<div class=\"form-row\">\r\n <!-- line number -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>line</mat-label>\r\n <mat-select [formControl]=\"lineNumber\">\r\n <mat-option *ngFor=\"let n of lineNumbers\" [value]=\"n\">\r\n {{ n }}\r\n </mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n\r\n <!-- height -->\r\n @if (lineNumber.value) {\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>height</mat-label>\r\n <input matInput type=\"number\" [formControl]=\"height\" min=\"0\" />\r\n </mat-form-field>\r\n }\r\n\r\n <!-- reset button -->\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n color=\"warn\"\r\n (click)=\"reset()\"\r\n matTooltip=\"Remove all the heights\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n</div>\r\n}\r\n", styles: [".input-nr{width:5em}.form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}\n"] }]
2389
- }], ctorParameters: () => [{ type: i1.FormBuilder }], propDecorators: { lineCount: [{
2390
- type: Input
2391
- }], heights: [{
2392
- type: Input
2393
- }], heightsChange: [{
2394
- type: Output
2395
- }] } });
2396
-
2397
- /**
2398
- * 🔑 `gve-simple-tree`
2399
- *
2400
- * A simple tree component.
2401
- *
2402
- * - ▶️ `node` (`TreeNode<any>`): the node to display.
2403
- * - ▶️ `selectedNode` (`TreeNode<any> | null`): the selected node.
2404
- * - 🔥 `selectedNodeChange` (`EventEmitter<TreeNode<any> | null>`): the event
2405
- * emitted when the selected node changes.
2406
- */
2407
- class SimpleTreeComponent {
2408
- constructor() {
2409
- /**
2410
- * The event emitted when the selected node changes.
2411
- */
2412
- this.selectedNodeChange = new EventEmitter();
2413
- this.level = 0;
2414
- }
2415
- /**
2416
- * The node to display.
2417
- */
2418
- get node() {
2419
- return this._node;
2420
- }
2421
- set node(value) {
2422
- if (this._node === value) {
2423
- return;
2424
- }
2425
- this._node = value;
2426
- this.level = this.calculateLevel(value);
2427
- }
2428
- ngOnInit() {
2429
- this.level = this.calculateLevel(this.node);
2430
- }
2431
- calculateLevel(node) {
2432
- let level = 0;
2433
- if (node) {
2434
- let n = node;
2435
- while (n.parent) {
2436
- level++;
2437
- n = n.parent;
2438
- }
2439
- }
2440
- return level;
2441
- }
2442
- selectNode(node) {
2443
- this.selectedNode = node;
2444
- this.selectedNodeChange.emit(node);
2445
- }
2446
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: SimpleTreeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2447
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.0", type: SimpleTreeComponent, isStandalone: true, selector: "gve-simple-tree", inputs: { node: "node", selectedNode: "selectedNode" }, outputs: { selectedNodeChange: "selectedNodeChange" }, ngImport: i0, template: "@if (node) {\r\n<div [style.margin-left]=\"level * 8 + 'px'\">\r\n <ng-template\r\n #tree\r\n let-node\r\n let-parentLevel=\"level\"\r\n let-selectedNode=\"selectedNode\"\r\n >\r\n <div\r\n [style.margin-left]=\"parentLevel * 8 + 'px'\"\r\n [class.selected]=\"node === selectedNode\"\r\n >\r\n <div class=\"toolbar-row\">\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n (click)=\"node.isExpanded = !node.isExpanded\"\r\n >\r\n <mat-icon>{{\r\n node.isExpanded ? \"expand_more\" : \"chevron_right\"\r\n }}</mat-icon>\r\n </button>\r\n <span class=\"label\" (click)=\"selectNode(node)\">\r\n {{ node.label }}\r\n </span>\r\n </div>\r\n <div *ngIf=\"node.children && node.isExpanded\">\r\n <ng-container *ngFor=\"let child of node.children\">\r\n <ng-container\r\n *ngTemplateOutlet=\"\r\n tree;\r\n context: {\r\n $implicit: child,\r\n level: parentLevel + 1,\r\n selectedNode: selectedNode\r\n }\r\n \"\r\n ></ng-container>\r\n </ng-container>\r\n </div>\r\n </div>\r\n </ng-template>\r\n\r\n <!-- $implicit is the source of let-node -->\r\n <ng-container\r\n *ngTemplateOutlet=\"\r\n tree;\r\n context: { $implicit: node, level: level, selectedNode: selectedNode }\r\n \"\r\n ></ng-container>\r\n</div>\r\n}\r\n", styles: [".label{cursor:pointer}.toolbar-row{display:flex;align-items:center;flex-wrap:wrap}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }] }); }
2448
- }
2449
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: SimpleTreeComponent, decorators: [{
2450
- type: Component,
2451
- args: [{ selector: 'gve-simple-tree', imports: [CommonModule, MatButtonModule, MatIconModule, MatTooltipModule], template: "@if (node) {\r\n<div [style.margin-left]=\"level * 8 + 'px'\">\r\n <ng-template\r\n #tree\r\n let-node\r\n let-parentLevel=\"level\"\r\n let-selectedNode=\"selectedNode\"\r\n >\r\n <div\r\n [style.margin-left]=\"parentLevel * 8 + 'px'\"\r\n [class.selected]=\"node === selectedNode\"\r\n >\r\n <div class=\"toolbar-row\">\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n (click)=\"node.isExpanded = !node.isExpanded\"\r\n >\r\n <mat-icon>{{\r\n node.isExpanded ? \"expand_more\" : \"chevron_right\"\r\n }}</mat-icon>\r\n </button>\r\n <span class=\"label\" (click)=\"selectNode(node)\">\r\n {{ node.label }}\r\n </span>\r\n </div>\r\n <div *ngIf=\"node.children && node.isExpanded\">\r\n <ng-container *ngFor=\"let child of node.children\">\r\n <ng-container\r\n *ngTemplateOutlet=\"\r\n tree;\r\n context: {\r\n $implicit: child,\r\n level: parentLevel + 1,\r\n selectedNode: selectedNode\r\n }\r\n \"\r\n ></ng-container>\r\n </ng-container>\r\n </div>\r\n </div>\r\n </ng-template>\r\n\r\n <!-- $implicit is the source of let-node -->\r\n <ng-container\r\n *ngTemplateOutlet=\"\r\n tree;\r\n context: { $implicit: node, level: level, selectedNode: selectedNode }\r\n \"\r\n ></ng-container>\r\n</div>\r\n}\r\n", styles: [".label{cursor:pointer}.toolbar-row{display:flex;align-items:center;flex-wrap:wrap}\n"] }]
2452
- }], propDecorators: { node: [{
2453
- type: Input
2454
- }], selectedNode: [{
2455
- type: Input
2456
- }], selectedNodeChange: [{
2457
- type: Output
2458
- }] } });
2642
+ }], ctorParameters: () => [{ type: i1.FormBuilder }] });
2459
2643
 
2460
2644
  /**
2461
2645
  * 🔑 `gve-animation-timeline-set`
@@ -2470,43 +2654,41 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImpor
2470
2654
  * - 🔥 `timelinesCancel` (`void`): emitted when the timeline editing is canceled.
2471
2655
  */
2472
2656
  class AnimationTimelineSetComponent {
2473
- /**
2474
- * The animation timelines to edit.
2475
- */
2476
- get timelines() {
2477
- return this._timelines;
2478
- }
2479
- set timelines(value) {
2480
- if (this._timelines === value) {
2481
- return;
2482
- }
2483
- this._timelines = value || [];
2484
- this.updateForm(this._timelines);
2485
- }
2486
2657
  constructor(formBuilder, _dialogService) {
2487
2658
  this._dialogService = _dialogService;
2488
- this._timelines = [];
2659
+ /**
2660
+ * The animation timelines to edit.
2661
+ */
2662
+ this.timelines = model([]);
2663
+ /**
2664
+ * The IDs of the elements that can be selected by the tween.
2665
+ * This list is used to allow the user to select an element from a dropdown.
2666
+ */
2667
+ this.elementIds = input();
2489
2668
  /**
2490
2669
  * The tags that can be used by the timeline.
2491
2670
  */
2492
- this.tags = [];
2671
+ this.tags = input([]);
2493
2672
  /**
2494
2673
  * Emitted when the timelines are changed.
2495
2674
  */
2496
- this.timelinesChange = new EventEmitter();
2675
+ this.timelinesChange = output();
2497
2676
  /**
2498
2677
  * Emitted when the timeline editing is canceled.
2499
2678
  */
2500
- this.timelinesCancel = new EventEmitter();
2679
+ this.timelinesCancel = output();
2501
2680
  this.set = formBuilder.control([], {
2502
2681
  nonNullable: true,
2503
2682
  });
2504
2683
  this.form = formBuilder.group({
2505
2684
  sets: this.set,
2506
2685
  });
2686
+ effect(() => {
2687
+ this.updateForm(this.timelines());
2688
+ });
2507
2689
  }
2508
2690
  updateForm(animations) {
2509
- if (!animations.length) {
2691
+ if (!animations?.length) {
2510
2692
  this.form.reset();
2511
2693
  }
2512
2694
  else {
@@ -2524,7 +2706,7 @@ class AnimationTimelineSetComponent {
2524
2706
  };
2525
2707
  }
2526
2708
  editTimeline(index) {
2527
- this.editedTimeline = this._timelines[index];
2709
+ this.editedTimeline = this.timelines()[index];
2528
2710
  }
2529
2711
  onTimelineChange(timeline) {
2530
2712
  const timelines = [...this.set.value];
@@ -2541,8 +2723,8 @@ class AnimationTimelineSetComponent {
2541
2723
  this.set.setValue(timelines);
2542
2724
  this.set.markAsDirty();
2543
2725
  this.set.updateValueAndValidity();
2544
- this._timelines = this.set.value;
2545
- this.timelinesChange.emit(this._timelines);
2726
+ this.timelines.set(this.set.value);
2727
+ this.timelinesChange.emit(this.timelines());
2546
2728
  }
2547
2729
  deleteTimeline(index) {
2548
2730
  this._dialogService
@@ -2554,15 +2736,15 @@ class AnimationTimelineSetComponent {
2554
2736
  this.set.setValue(timelines);
2555
2737
  this.set.markAsDirty();
2556
2738
  this.set.updateValueAndValidity();
2557
- this._timelines = this.set.value;
2558
- this.timelinesChange.emit(this._timelines);
2739
+ this.timelines.set(this.set.value);
2740
+ this.timelinesChange.emit(this.timelines());
2559
2741
  }
2560
2742
  });
2561
2743
  }
2562
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: AnimationTimelineSetComponent, deps: [{ token: i1.FormBuilder }, { token: i2$2.DialogService }], target: i0.ɵɵFactoryTarget.Component }); }
2563
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.0", type: AnimationTimelineSetComponent, isStandalone: true, selector: "gve-animation-timeline-set", inputs: { timelines: "timelines", elementIds: "elementIds", tags: "tags" }, outputs: { timelinesChange: "timelinesChange", timelinesCancel: "timelinesCancel" }, ngImport: i0, template: "<form [formGroup]=\"form\">\r\n <!-- add -->\r\n <div>\r\n <button\r\n type=\"button\"\r\n class=\"mat-primary\"\r\n mat-flat-button\r\n (click)=\"newTimeline()\"\r\n >\r\n <mat-icon>add_circle</mat-icon>\r\n timeline\r\n </button>\r\n </div>\r\n\r\n <!-- table -->\r\n <table>\r\n <thead>\r\n <tr>\r\n <th></th>\r\n <th>tag</th>\r\n <th>tweens</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (t of set.value; track t.tag; let index = $index) {\r\n <tr>\r\n <td class=\"fit-width\">\r\n <!-- edit -->\r\n <button type=\"button\" mat-icon-button (click)=\"editTimeline(index)\">\r\n <mat-icon class=\"mat-primary\">edit</mat-icon>\r\n </button>\r\n <!-- delete -->\r\n <button type=\"button\" mat-icon-button (click)=\"deleteTimeline(index)\">\r\n <mat-icon class=\"mat-warn\">remove_circle</mat-icon>\r\n </button>\r\n </td>\r\n <td>{{ t.tag }}</td>\r\n <td>{{ t.tweens.length }}</td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n\r\n <!-- editor -->\r\n <mat-expansion-panel [disabled]=\"!editedTimeline\" [expanded]=\"editedTimeline\">\r\n <mat-expansion-panel-header>\r\n timeline {{ editedTimeline?.tag }}\r\n </mat-expansion-panel-header>\r\n <gve-animation-timeline\r\n [elementIds]=\"elementIds\"\r\n [tags]=\"tags\"\r\n [timeline]=\"editedTimeline\"\r\n (timelineChange)=\"onTimelineChange($event)\"\r\n (timelineCancel)=\"closeTimeline()\"\r\n />\r\n </mat-expansion-panel>\r\n</form>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}table{width:100%;border-collapse:collapse;margin:8px 0}table{width:100%;border-collapse:collapse}th{color:#909090;font-weight:400;text-align:left;background-color:#e1e0e0}th,td{padding:4px;border-bottom:1px solid silver}tbody tr:nth-child(2n){background-color:#e8e8e8}td.fit-width{width:1px;white-space:nowrap}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "component", type: i4.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i4.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "ngmodule", type: MatSelectModule }, { kind: "ngmodule", type: MatTabsModule }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "ngmodule", type: NgToolsModule }, { kind: "component", type: AnimationTimelineComponent, selector: "gve-animation-timeline", inputs: ["timeline", "elementIds", "tags"], outputs: ["timelineChange", "timelineCancel"] }] }); }
2744
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: AnimationTimelineSetComponent, deps: [{ token: i1.FormBuilder }, { token: i2$1.DialogService }], target: i0.ɵɵFactoryTarget.Component }); }
2745
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.6", type: AnimationTimelineSetComponent, isStandalone: true, selector: "gve-animation-timeline-set", inputs: { timelines: { classPropertyName: "timelines", publicName: "timelines", isSignal: true, isRequired: false, transformFunction: null }, elementIds: { classPropertyName: "elementIds", publicName: "elementIds", isSignal: true, isRequired: false, transformFunction: null }, tags: { classPropertyName: "tags", publicName: "tags", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { timelines: "timelinesChange", timelinesChange: "timelinesChange", timelinesCancel: "timelinesCancel" }, ngImport: i0, template: "<form [formGroup]=\"form\">\r\n <!-- add -->\r\n <div>\r\n <button\r\n type=\"button\"\r\n class=\"mat-primary\"\r\n mat-flat-button\r\n (click)=\"newTimeline()\"\r\n >\r\n <mat-icon>add_circle</mat-icon>\r\n timeline\r\n </button>\r\n </div>\r\n\r\n <!-- table -->\r\n <table>\r\n <thead>\r\n <tr>\r\n <th></th>\r\n <th>tag</th>\r\n <th>tweens</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (t of set.value; track t.tag; let index = $index) {\r\n <tr>\r\n <td class=\"fit-width\">\r\n <!-- edit -->\r\n <button type=\"button\" mat-icon-button (click)=\"editTimeline(index)\">\r\n <mat-icon class=\"mat-primary\">edit</mat-icon>\r\n </button>\r\n <!-- delete -->\r\n <button type=\"button\" mat-icon-button (click)=\"deleteTimeline(index)\">\r\n <mat-icon class=\"mat-warn\">remove_circle</mat-icon>\r\n </button>\r\n </td>\r\n <td>{{ t.tag }}</td>\r\n <td>{{ t.tweens.length }}</td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n\r\n <!-- editor -->\r\n <mat-expansion-panel [disabled]=\"!editedTimeline\" [expanded]=\"editedTimeline\">\r\n <mat-expansion-panel-header>\r\n timeline {{ editedTimeline?.tag }}\r\n </mat-expansion-panel-header>\r\n <gve-animation-timeline\r\n [elementIds]=\"elementIds()\"\r\n [tags]=\"tags()\"\r\n [timeline]=\"editedTimeline\"\r\n (timelineChange)=\"onTimelineChange($event)\"\r\n (timelineCancel)=\"closeTimeline()\"\r\n />\r\n </mat-expansion-panel>\r\n</form>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}table{width:100%;border-collapse:collapse;margin:8px 0}table{width:100%;border-collapse:collapse}th{color:#909090;font-weight:400;text-align:left;background-color:#e1e0e0}th,td{padding:4px;border-bottom:1px solid silver}tbody tr:nth-child(2n){background-color:#e8e8e8}td.fit-width{width:1px;white-space:nowrap}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "component", type: i4.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i4.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "ngmodule", type: MatSelectModule }, { kind: "ngmodule", type: MatTabsModule }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "component", type: AnimationTimelineComponent, selector: "gve-animation-timeline", inputs: ["timeline", "elementIds", "tags"], outputs: ["timelineChange", "timelineCancel"] }] }); }
2564
2746
  }
2565
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: AnimationTimelineSetComponent, decorators: [{
2747
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: AnimationTimelineSetComponent, decorators: [{
2566
2748
  type: Component,
2567
2749
  args: [{ selector: 'gve-animation-timeline-set', imports: [
2568
2750
  CommonModule,
@@ -2576,20 +2758,87 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImpor
2576
2758
  MatSelectModule,
2577
2759
  MatTabsModule,
2578
2760
  MatTooltipModule,
2579
- NgToolsModule,
2580
2761
  AnimationTimelineComponent,
2581
- ], template: "<form [formGroup]=\"form\">\r\n <!-- add -->\r\n <div>\r\n <button\r\n type=\"button\"\r\n class=\"mat-primary\"\r\n mat-flat-button\r\n (click)=\"newTimeline()\"\r\n >\r\n <mat-icon>add_circle</mat-icon>\r\n timeline\r\n </button>\r\n </div>\r\n\r\n <!-- table -->\r\n <table>\r\n <thead>\r\n <tr>\r\n <th></th>\r\n <th>tag</th>\r\n <th>tweens</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (t of set.value; track t.tag; let index = $index) {\r\n <tr>\r\n <td class=\"fit-width\">\r\n <!-- edit -->\r\n <button type=\"button\" mat-icon-button (click)=\"editTimeline(index)\">\r\n <mat-icon class=\"mat-primary\">edit</mat-icon>\r\n </button>\r\n <!-- delete -->\r\n <button type=\"button\" mat-icon-button (click)=\"deleteTimeline(index)\">\r\n <mat-icon class=\"mat-warn\">remove_circle</mat-icon>\r\n </button>\r\n </td>\r\n <td>{{ t.tag }}</td>\r\n <td>{{ t.tweens.length }}</td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n\r\n <!-- editor -->\r\n <mat-expansion-panel [disabled]=\"!editedTimeline\" [expanded]=\"editedTimeline\">\r\n <mat-expansion-panel-header>\r\n timeline {{ editedTimeline?.tag }}\r\n </mat-expansion-panel-header>\r\n <gve-animation-timeline\r\n [elementIds]=\"elementIds\"\r\n [tags]=\"tags\"\r\n [timeline]=\"editedTimeline\"\r\n (timelineChange)=\"onTimelineChange($event)\"\r\n (timelineCancel)=\"closeTimeline()\"\r\n />\r\n </mat-expansion-panel>\r\n</form>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}table{width:100%;border-collapse:collapse;margin:8px 0}table{width:100%;border-collapse:collapse}th{color:#909090;font-weight:400;text-align:left;background-color:#e1e0e0}th,td{padding:4px;border-bottom:1px solid silver}tbody tr:nth-child(2n){background-color:#e8e8e8}td.fit-width{width:1px;white-space:nowrap}\n"] }]
2582
- }], ctorParameters: () => [{ type: i1.FormBuilder }, { type: i2$2.DialogService }], propDecorators: { timelines: [{
2583
- type: Input
2584
- }], elementIds: [{
2585
- type: Input
2586
- }], tags: [{
2587
- type: Input
2588
- }], timelinesChange: [{
2589
- type: Output
2590
- }], timelinesCancel: [{
2591
- type: Output
2592
- }] } });
2762
+ ], template: "<form [formGroup]=\"form\">\r\n <!-- add -->\r\n <div>\r\n <button\r\n type=\"button\"\r\n class=\"mat-primary\"\r\n mat-flat-button\r\n (click)=\"newTimeline()\"\r\n >\r\n <mat-icon>add_circle</mat-icon>\r\n timeline\r\n </button>\r\n </div>\r\n\r\n <!-- table -->\r\n <table>\r\n <thead>\r\n <tr>\r\n <th></th>\r\n <th>tag</th>\r\n <th>tweens</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (t of set.value; track t.tag; let index = $index) {\r\n <tr>\r\n <td class=\"fit-width\">\r\n <!-- edit -->\r\n <button type=\"button\" mat-icon-button (click)=\"editTimeline(index)\">\r\n <mat-icon class=\"mat-primary\">edit</mat-icon>\r\n </button>\r\n <!-- delete -->\r\n <button type=\"button\" mat-icon-button (click)=\"deleteTimeline(index)\">\r\n <mat-icon class=\"mat-warn\">remove_circle</mat-icon>\r\n </button>\r\n </td>\r\n <td>{{ t.tag }}</td>\r\n <td>{{ t.tweens.length }}</td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n\r\n <!-- editor -->\r\n <mat-expansion-panel [disabled]=\"!editedTimeline\" [expanded]=\"editedTimeline\">\r\n <mat-expansion-panel-header>\r\n timeline {{ editedTimeline?.tag }}\r\n </mat-expansion-panel-header>\r\n <gve-animation-timeline\r\n [elementIds]=\"elementIds()\"\r\n [tags]=\"tags()\"\r\n [timeline]=\"editedTimeline\"\r\n (timelineChange)=\"onTimelineChange($event)\"\r\n (timelineCancel)=\"closeTimeline()\"\r\n />\r\n </mat-expansion-panel>\r\n</form>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}table{width:100%;border-collapse:collapse;margin:8px 0}table{width:100%;border-collapse:collapse}th{color:#909090;font-weight:400;text-align:left;background-color:#e1e0e0}th,td{padding:4px;border-bottom:1px solid silver}tbody tr:nth-child(2n){background-color:#e8e8e8}td.fit-width{width:1px;white-space:nowrap}\n"] }]
2763
+ }], ctorParameters: () => [{ type: i1.FormBuilder }, { type: i2$1.DialogService }] });
2764
+
2765
+ /**
2766
+ * 🔑 `gve-snapshot-text-editor`
2767
+ *
2768
+ * A component to edit a snapshot text. This component can be used both in a dialog
2769
+ * and as a standalone component.
2770
+ *
2771
+ * - ▶️ `text` (`CharNode[]`): the text to edit.
2772
+ * - 🔥 `textChange` (`CharNode[]`): event emitted when text changes.
2773
+ */
2774
+ class SnapshotTextEditorComponent {
2775
+ constructor(formBuilder,
2776
+ // this component can be used as a dialog
2777
+ dialogRef, data) {
2778
+ this.dialogRef = dialogRef;
2779
+ this.data = data;
2780
+ this.text = model([]);
2781
+ /**
2782
+ * Emitted when text changes.
2783
+ */
2784
+ this.textChange = output();
2785
+ this.userText = formBuilder.control('', {
2786
+ nonNullable: true,
2787
+ validators: [Validators.required, Validators.maxLength(5000)],
2788
+ });
2789
+ this.form = new FormGroup({
2790
+ userText: this.userText,
2791
+ });
2792
+ // when text changes, update the user text
2793
+ effect(() => {
2794
+ this.updateUserText(this.text());
2795
+ });
2796
+ }
2797
+ updateUserText(nodes) {
2798
+ this.userText.setValue(nodes.map((node) => node.label).join(''));
2799
+ this.userText.markAsPristine();
2800
+ this.userText.updateValueAndValidity();
2801
+ }
2802
+ ngOnInit() {
2803
+ if (this.data?.payload?.text) {
2804
+ this.text.set(this.data.payload.text);
2805
+ }
2806
+ }
2807
+ close() {
2808
+ this.dialogRef?.close();
2809
+ }
2810
+ save() {
2811
+ if (this.form.invalid) {
2812
+ return;
2813
+ }
2814
+ this.userText.setValue(this.userText.value.trim());
2815
+ if (!this.userText.value) {
2816
+ return;
2817
+ }
2818
+ this.text.set(SnapshotViewService.stringToBaseChars(this.userText.value));
2819
+ this.textChange.emit(this.text());
2820
+ this.dialogRef?.close(this.text());
2821
+ }
2822
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: SnapshotTextEditorComponent, deps: [{ token: i1.FormBuilder }, { token: i3$1.MatDialogRef, optional: true }, { token: MAT_DIALOG_DATA, optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
2823
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.6", type: SnapshotTextEditorComponent, isStandalone: true, selector: "gve-snapshot-text-editor", inputs: { text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { text: "textChange", textChange: "textChange" }, ngImport: i0, template: "<div [style.padding]=\"dialogRef ? '8px' : '0'\">\r\n @if (dialogRef) {\r\n <div id=\"heading\">\r\n <h2>Set Base Text</h2>\r\n </div>\r\n }\r\n <form [formGroup]=\"form\" (submit)=\"save()\">\r\n <fieldset>\r\n <div id=\"batch-input\">\r\n <div>\r\n <mat-form-field class=\"full-width\">\r\n <mat-label>base text</mat-label>\r\n <textarea matInput [formControl]=\"userText\" rows=\"8\"></textarea>\r\n @if ($any(userText).errors?.required && (userText.dirty ||\r\n userText.touched)) {\r\n <mat-error>text required</mat-error>\r\n } @if ($any(userText).errors?.maxLength && (userText.dirty ||\r\n userText.touched)) {\r\n <mat-error>text too long</mat-error>\r\n }\r\n </mat-form-field>\r\n </div>\r\n </div>\r\n <div class=\"form-row-center\">\r\n @if (dialogRef) {\r\n <button\r\n type=\"button\"\r\n class=\"mat-warn\"\r\n mat-flat-button\r\n matTooltip=\"Close dialog\"\r\n (click)=\"close()\"\r\n >\r\n close\r\n </button>\r\n }\r\n <button\r\n type=\"button\"\r\n class=\"mat-primary\"\r\n mat-flat-button\r\n matTooltip=\"Set base text\"\r\n [disabled]=\"form.invalid\"\r\n (click)=\"save()\"\r\n >\r\n set\r\n </button>\r\n </div>\r\n </fieldset>\r\n </form>\r\n</div>\r\n", styles: [".full-width{width:100%}.form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row-center{display:flex;gap:8px;align-items:center;justify-content:center;flex-wrap:wrap}.form-row,.form-row-center *{flex:0 0 auto}div#heading{margin:8px;text-align:center}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "directive", type: i5.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }] }); }
2824
+ }
2825
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: SnapshotTextEditorComponent, decorators: [{
2826
+ type: Component,
2827
+ args: [{ selector: 'gve-snapshot-text-editor', imports: [
2828
+ ReactiveFormsModule,
2829
+ MatButtonModule,
2830
+ MatFormFieldModule,
2831
+ MatIconModule,
2832
+ MatInputModule,
2833
+ ], template: "<div [style.padding]=\"dialogRef ? '8px' : '0'\">\r\n @if (dialogRef) {\r\n <div id=\"heading\">\r\n <h2>Set Base Text</h2>\r\n </div>\r\n }\r\n <form [formGroup]=\"form\" (submit)=\"save()\">\r\n <fieldset>\r\n <div id=\"batch-input\">\r\n <div>\r\n <mat-form-field class=\"full-width\">\r\n <mat-label>base text</mat-label>\r\n <textarea matInput [formControl]=\"userText\" rows=\"8\"></textarea>\r\n @if ($any(userText).errors?.required && (userText.dirty ||\r\n userText.touched)) {\r\n <mat-error>text required</mat-error>\r\n } @if ($any(userText).errors?.maxLength && (userText.dirty ||\r\n userText.touched)) {\r\n <mat-error>text too long</mat-error>\r\n }\r\n </mat-form-field>\r\n </div>\r\n </div>\r\n <div class=\"form-row-center\">\r\n @if (dialogRef) {\r\n <button\r\n type=\"button\"\r\n class=\"mat-warn\"\r\n mat-flat-button\r\n matTooltip=\"Close dialog\"\r\n (click)=\"close()\"\r\n >\r\n close\r\n </button>\r\n }\r\n <button\r\n type=\"button\"\r\n class=\"mat-primary\"\r\n mat-flat-button\r\n matTooltip=\"Set base text\"\r\n [disabled]=\"form.invalid\"\r\n (click)=\"save()\"\r\n >\r\n set\r\n </button>\r\n </div>\r\n </fieldset>\r\n </form>\r\n</div>\r\n", styles: [".full-width{width:100%}.form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row-center{display:flex;gap:8px;align-items:center;justify-content:center;flex-wrap:wrap}.form-row,.form-row-center *{flex:0 0 auto}div#heading{margin:8px;text-align:center}\n"] }]
2834
+ }], ctorParameters: () => [{ type: i1.FormBuilder }, { type: i3$1.MatDialogRef, decorators: [{
2835
+ type: Optional
2836
+ }] }, { type: undefined, decorators: [{
2837
+ type: Optional
2838
+ }, {
2839
+ type: Inject,
2840
+ args: [MAT_DIALOG_DATA]
2841
+ }] }] });
2593
2842
 
2594
2843
  // default snapshot view title
2595
2844
  const VIEW_TITLE = 'preview';
@@ -2608,36 +2857,36 @@ const SVG_TRANSP_SUFFIX = '_t';
2608
2857
  * - 🔥 `snapshotCancel` (`void`): emitted when the user cancels the snapshot editing.
2609
2858
  */
2610
2859
  class SnapshotEditorComponent {
2611
- /**
2612
- * The snapshot to edit.
2613
- */
2614
- get snapshot() {
2615
- return this._snapshot;
2616
- }
2617
- set snapshot(value) {
2618
- if (this._snapshot === value) {
2619
- return;
2620
- }
2621
- this._snapshot = value || undefined;
2622
- this.updateForm(this._snapshot);
2623
- setTimeout(() => {
2624
- this.runToLast();
2625
- }, 0);
2626
- }
2627
2860
  constructor(formBuilder, _api, _dialog, _dialogService, _snackbar) {
2628
2861
  this._api = _api;
2629
2862
  this._dialog = _dialog;
2630
2863
  this._dialogService = _dialogService;
2631
2864
  this._snackbar = _snackbar;
2632
2865
  this._nanoid = customAlphabet('1234567890abcdef', 10);
2866
+ /**
2867
+ * The snapshot to edit.
2868
+ */
2869
+ this.snapshot = model();
2870
+ /**
2871
+ * The batch operations text to set for the editor.
2872
+ */
2873
+ this.batchOps = input();
2874
+ /**
2875
+ * True to disable saving.
2876
+ */
2877
+ this.noSave = input();
2878
+ /**
2879
+ * True to enable debug mode for view rendition.
2880
+ */
2881
+ this.debug = input();
2633
2882
  /**
2634
2883
  * Emitted when the user saves the edited snapshot.
2635
2884
  */
2636
- this.snapshotChange = new EventEmitter();
2885
+ this.snapshotChange = output();
2637
2886
  /**
2638
2887
  * Emitted when the user cancels the snapshot editing.
2639
2888
  */
2640
- this.snapshotCancel = new EventEmitter();
2889
+ this.snapshotCancel = output();
2641
2890
  // list of operations output tags
2642
2891
  this.opTags = [];
2643
2892
  // list of operation diplomatic.g element IDs
@@ -2659,6 +2908,7 @@ class SnapshotEditorComponent {
2659
2908
  this.viewTitle = VIEW_TITLE;
2660
2909
  this.rulers = true;
2661
2910
  this.initialStepIndex = -1;
2911
+ this.showChain = new FormControl(false, { nonNullable: true });
2662
2912
  // general
2663
2913
  this.width = new FormControl(800, { nonNullable: true });
2664
2914
  this.height = new FormControl(600, { nonNullable: true });
@@ -2718,6 +2968,13 @@ class SnapshotEditorComponent {
2718
2968
  // timelines
2719
2969
  timelines: this.timelines,
2720
2970
  });
2971
+ // when snapshot changes, update the form
2972
+ effect(() => {
2973
+ this.updateForm(this.snapshot());
2974
+ setTimeout(() => {
2975
+ this.runToLast();
2976
+ }, 0);
2977
+ });
2721
2978
  }
2722
2979
  /**
2723
2980
  * Set the view data for the snapshot view.
@@ -2738,7 +2995,7 @@ class SnapshotEditorComponent {
2738
2995
  this.viewData = {
2739
2996
  snapshot: snapshot,
2740
2997
  options: {
2741
- debug: this.debug,
2998
+ debug: this.debug(),
2742
2999
  delayedRender: true,
2743
3000
  showRulers: true,
2744
3001
  showGrid: true,
@@ -2765,7 +3022,12 @@ class SnapshotEditorComponent {
2765
3022
  this.imageWidth.setValue(snapshot.image?.canvas?.width || 0);
2766
3023
  this.imageHeight.setValue(snapshot.image?.canvas?.height || 0);
2767
3024
  this.defs.setValue(snapshot.defs || null);
2768
- this.baseText.setValue(snapshot.text);
3025
+ if (Array.isArray(snapshot.text)) {
3026
+ this.baseText.setValue(snapshot.text);
3027
+ }
3028
+ else {
3029
+ this.baseText.setValue(SnapshotViewService.stringToBaseChars(snapshot.text));
3030
+ }
2769
3031
  this.offsetX.setValue(snapshot.textOptions?.offset?.x || 0);
2770
3032
  this.offsetY.setValue(snapshot.textOptions?.offset?.y || 0);
2771
3033
  this.lineHeightOffset.setValue(snapshot.textOptions?.lineHeightOffset);
@@ -2789,6 +3051,30 @@ class SnapshotEditorComponent {
2789
3051
  this.form.markAsPristine();
2790
3052
  }
2791
3053
  // #region base text
3054
+ inputBaseText() {
3055
+ if (this.busy) {
3056
+ return;
3057
+ }
3058
+ const dialogRef = this._dialog.open(SnapshotTextEditorComponent, {
3059
+ data: { payload: { text: this.baseText.value } },
3060
+ width: '80%',
3061
+ });
3062
+ dialogRef.afterClosed().subscribe((text) => {
3063
+ if (text?.length) {
3064
+ // update the form control and reset the current text range
3065
+ this.baseText.setValue(text);
3066
+ this.baseText.updateValueAndValidity();
3067
+ this.baseText.markAsDirty();
3068
+ this.textRange = undefined;
3069
+ // update the line count
3070
+ this.updateLineCount(text);
3071
+ // remove all operations and update the view data
3072
+ this.removeAllOperations();
3073
+ // remove all timelines
3074
+ this.timelines.reset();
3075
+ }
3076
+ });
3077
+ }
2792
3078
  /**
2793
3079
  * Update the line count based on the received text.
2794
3080
  * @param text The text to use for counting lines.
@@ -2808,24 +3094,6 @@ class SnapshotEditorComponent {
2808
3094
  onRangePick(range) {
2809
3095
  this.textRange = range;
2810
3096
  }
2811
- /**
2812
- * Handle the event fired by the base text editor to change the base text.
2813
- * @param text The text to set.
2814
- */
2815
- onTextChange(text) {
2816
- console.log('text change', text);
2817
- // update the form control and reset the current text range
2818
- this.baseText.setValue(text);
2819
- this.baseText.updateValueAndValidity();
2820
- this.baseText.markAsDirty();
2821
- this.textRange = undefined;
2822
- // update the line count
2823
- this.updateLineCount(text);
2824
- // remove all operations and update the view data
2825
- this.removeAllOperations();
2826
- // remove all timelines
2827
- this.timelines.reset();
2828
- }
2829
3097
  // #endregion
2830
3098
  // #region operations
2831
3099
  /**
@@ -2890,7 +3158,10 @@ class SnapshotEditorComponent {
2890
3158
  * was a new operation.
2891
3159
  * @param op The changed operation.
2892
3160
  */
2893
- async onOperationChange(op) {
3161
+ onOperationChange(op) {
3162
+ if (!op) {
3163
+ return;
3164
+ }
2894
3165
  console.log('operation change');
2895
3166
  const operations = [...this.operations.value];
2896
3167
  // replace or add the operation
@@ -2908,8 +3179,8 @@ class SnapshotEditorComponent {
2908
3179
  this.operations.updateValueAndValidity();
2909
3180
  // update the operation lists
2910
3181
  this.updateOpLists();
2911
- // update the view data
2912
- this.result = await this.runTo(i);
3182
+ // run to the edited operation
3183
+ this.runTo(i);
2913
3184
  // close the edited operation
2914
3185
  this.closeEditedOperation();
2915
3186
  }
@@ -2972,6 +3243,9 @@ class SnapshotEditorComponent {
2972
3243
  }
2973
3244
  });
2974
3245
  }
3246
+ /**
3247
+ * Remove all the operations, close the edited operation and update the view data.
3248
+ */
2975
3249
  removeAllOperations() {
2976
3250
  this.resultOperationId = undefined;
2977
3251
  this.closeEditedOperation();
@@ -3020,6 +3294,53 @@ class SnapshotEditorComponent {
3020
3294
  }
3021
3295
  return this.parseSvgIds(g)?.filter((id) => id.endsWith(SVG_TRANSP_SUFFIX));
3022
3296
  }
3297
+ /**
3298
+ * Supply operation nodes to the snapshot text nodes. An execution result is
3299
+ * a compact object, so that to build a snapshot accumulating all the nodes
3300
+ * and their features up to the last executed operation, we need to start
3301
+ * with the base text nodes, and then examine each execution step in the
3302
+ * result. Each step contains only those nodes used by the version of the
3303
+ * text corresponding to its stage. Among them, some are new nodes, and some
3304
+ * are base text nodes. Also, the step contains the full set of features
3305
+ * current at its time, both for the text as a whole and for each node.
3306
+ * This method accumulates the nodes and their features up to the specified
3307
+ * step, updating the features of the nodes already present in the snapshot
3308
+ * text nodes, and adding the new nodes.
3309
+ *
3310
+ * @param snapshot The snapshot to supply the nodes to.
3311
+ * @param result The operations execution result.
3312
+ * @param tag The output tag of the operation to supply the nodes from.
3313
+ */
3314
+ supplyOpNodes(snapshot, result, tag) {
3315
+ // get the original nodes from the snapshot base text
3316
+ const nodes = snapshot.text;
3317
+ // find the max ID in the original nodes, so that any ID greater than it
3318
+ // belongs to new nodes
3319
+ const maxOriginalId = nodes.reduce((max, n) => Math.max(max, n.id), 0);
3320
+ const stepIndex = result.steps.findIndex((s) => s.outputTag === tag);
3321
+ // for each step up to the specified one, add the nodes introduced by the step
3322
+ // and update the features from each step node that is already present
3323
+ for (let i = 0; i <= stepIndex; i++) {
3324
+ // get the nodes of the step
3325
+ const stepNodes = result.taggedNodes[result.steps[i].outputTag];
3326
+ for (const stepNode of stepNodes) {
3327
+ // if the node was added, add it
3328
+ if (stepNode.id > maxOriginalId &&
3329
+ !nodes.find((n) => n.id === stepNode.id)) {
3330
+ nodes.push(stepNode);
3331
+ }
3332
+ else {
3333
+ // if the node was already in the original nodes, update its features
3334
+ const existing = nodes.findIndex((n) => n.id === stepNode.id);
3335
+ if (existing > -1) {
3336
+ nodes[existing].features = stepNode.features;
3337
+ }
3338
+ }
3339
+ }
3340
+ }
3341
+ // update the snapshot text nodes
3342
+ snapshot.text = nodes;
3343
+ }
3023
3344
  /**
3024
3345
  * Run the operations up to the specified index (included).
3025
3346
  * This is called when:
@@ -3035,63 +3356,81 @@ class SnapshotEditorComponent {
3035
3356
  * @param lastOperation The operation to use in place of the existing
3036
3357
  * operation in the snapshot at index. This is used when previewing
3037
3358
  * the edited operation from within the operation editor.
3359
+ * @returns A promise that resolves to the execution result.
3038
3360
  */
3039
3361
  runTo(index, lastOperation) {
3040
- return new Promise((resolve, reject) => {
3041
- if (this.busy) {
3042
- return reject('Busy');
3043
- }
3044
- console.log('run to: ' + index);
3045
- // get the snapshot (=text and ops) to run operations on
3046
- const snapshot = this.getSnapshot();
3047
- if (!snapshot.text) {
3048
- return reject('No snapshot text');
3049
- }
3050
- // remove from the snapshot the operations past the specified index,
3051
- // also replacing the last operation when this was received as a parameter
3052
- snapshot.operations = [...this.operations.value];
3053
- if (lastOperation) {
3054
- snapshot.operations = snapshot.operations.slice(0, index);
3055
- snapshot.operations.push(lastOperation);
3056
- }
3057
- else {
3058
- snapshot.operations = snapshot.operations.slice(0, index + 1);
3059
- }
3060
- // run the operations
3061
- this.busy = true;
3062
- this.initialStepIndex = index;
3063
- this._api
3064
- .runOperations(snapshot.text, snapshot.operations)
3065
- .subscribe({
3066
- next: (wrapper) => {
3067
- // handle operation (non-fatal) error or result
3068
- if (wrapper.error) {
3069
- this._snackbar.open(wrapper.error, 'OK');
3070
- reject(wrapper.error);
3071
- return;
3072
- }
3073
- // extract the IDs from the last operation's diplomatics and filter
3074
- // them so that only the ones ending with _t are kept. By convention,
3075
- // all the IDs ending with this suffix are to be made invisible at
3076
- // their first rendition (opacity=0). An animation will then make
3077
- // them visible.
3078
- const lastOp = snapshot.operations[snapshot.operations.length - 1];
3079
- this._transparentIds = this.getTransparentIds(lastOp.diplomatics?.g);
3080
- // update the view data
3081
- this.setViewData(snapshot, lastOp.outputTag);
3082
- // return the result
3083
- resolve(wrapper.result);
3084
- },
3085
- error: (error) => {
3086
- console.error(error);
3087
- this._transparentIds = undefined;
3088
- this._snackbar.open('Error running operations', 'OK');
3089
- reject(error);
3090
- },
3091
- complete: () => {
3092
- this.busy = false;
3093
- },
3094
- });
3362
+ if (this.busy) {
3363
+ return;
3364
+ }
3365
+ console.log('run to: ' + index);
3366
+ // get the snapshot to run operations on
3367
+ const snapshot = this.getSnapshot();
3368
+ if (!snapshot.text) {
3369
+ return;
3370
+ }
3371
+ // remove from the snapshot the operations past the specified index,
3372
+ // also replacing the last operation when this was received as a parameter
3373
+ if (lastOperation) {
3374
+ snapshot.operations = snapshot.operations.slice(0, index);
3375
+ snapshot.operations.push(lastOperation);
3376
+ }
3377
+ else {
3378
+ lastOperation = snapshot.operations[index];
3379
+ snapshot.operations = snapshot.operations.slice(0, index + 1);
3380
+ }
3381
+ // update result operation ID
3382
+ this.resultOperationId = snapshot.operations[index].id;
3383
+ // extract the IDs from the last operation's diplomatics and filter
3384
+ // them so that only the ones ending with _t are kept. By convention,
3385
+ // all the IDs ending with this suffix are to be made invisible at
3386
+ // their first rendition (opacity=0). An animation will then make
3387
+ // them visible.
3388
+ this._transparentIds = this.getTransparentIds(snapshot.operations[index].diplomatics?.g);
3389
+ // run the operations
3390
+ this.busy = true;
3391
+ this.initialStepIndex = index;
3392
+ this._api
3393
+ .runOperations(snapshot.text, snapshot.operations)
3394
+ .subscribe({
3395
+ next: (wrapper) => {
3396
+ // handle operation (non-fatal) error or result
3397
+ if (wrapper.error) {
3398
+ this._snackbar.open(wrapper.error, 'OK');
3399
+ return;
3400
+ }
3401
+ // set the result
3402
+ console.log('result:', wrapper.result);
3403
+ this.result = wrapper.result;
3404
+ // supply the last operation output tag from its result step if not set
3405
+ if (!lastOperation.outputTag) {
3406
+ const step = wrapper.result.steps.find((s) => s.operation.id === lastOperation.id);
3407
+ lastOperation.outputTag = step.outputTag;
3408
+ }
3409
+ // update the snapshot text nodes with those introduced by operations
3410
+ this.supplyOpNodes(snapshot, wrapper.result, lastOperation.outputTag);
3411
+ // update the view data
3412
+ this.setViewData(snapshot, lastOperation.outputTag);
3413
+ // play animation if any
3414
+ const tl = this.timelines.value.find((t) => t.tag === lastOperation.outputTag);
3415
+ if (tl) {
3416
+ // play the timeline
3417
+ setTimeout(() => {
3418
+ this.playTimeline(tl);
3419
+ }, 0);
3420
+ }
3421
+ // get the chain model if requested
3422
+ if (this.showChain.value) {
3423
+ this.getChainAt(index);
3424
+ }
3425
+ },
3426
+ error: (error) => {
3427
+ console.error(error);
3428
+ this._transparentIds = undefined;
3429
+ this._snackbar.open('Error running operations', 'OK');
3430
+ },
3431
+ complete: () => {
3432
+ this.busy = false;
3433
+ },
3095
3434
  });
3096
3435
  }
3097
3436
  /**
@@ -3099,15 +3438,55 @@ class SnapshotEditorComponent {
3099
3438
  * execution result. The execution result is always the result from
3100
3439
  * all the operations, as users must be able to browse across its steps.
3101
3440
  */
3102
- async runToLast() {
3441
+ runToLast() {
3103
3442
  if (this.operations.value.length) {
3104
- this.result = await this.runTo(this.operations.value.length - 1);
3443
+ this.runTo(this.operations.value.length - 1);
3105
3444
  }
3106
3445
  else {
3107
3446
  this.setViewData();
3108
3447
  this.result = undefined;
3109
3448
  }
3110
3449
  }
3450
+ getChainAt(index, lastOperation) {
3451
+ // get the snapshot to run operations on
3452
+ const snapshot = this.getSnapshot();
3453
+ if (!snapshot.text) {
3454
+ return;
3455
+ }
3456
+ // remove from the snapshot the operations past the specified index,
3457
+ // also replacing the last operation when this was received as a parameter
3458
+ if (lastOperation) {
3459
+ snapshot.operations = snapshot.operations.slice(0, index);
3460
+ snapshot.operations.push(lastOperation);
3461
+ }
3462
+ else {
3463
+ lastOperation = snapshot.operations[index];
3464
+ snapshot.operations = snapshot.operations.slice(0, index + 1);
3465
+ }
3466
+ this.busy = true;
3467
+ this._api
3468
+ .getChain(snapshot.text, snapshot.operations)
3469
+ .subscribe({
3470
+ next: (wrapper) => {
3471
+ // handle operation (non-fatal) error or result
3472
+ if (wrapper.error) {
3473
+ this._snackbar.open(wrapper.error, 'OK');
3474
+ this.chain = undefined;
3475
+ }
3476
+ else {
3477
+ this.chain = wrapper.result;
3478
+ }
3479
+ },
3480
+ error: (error) => {
3481
+ console.error(error);
3482
+ this._transparentIds = undefined;
3483
+ this._snackbar.open('Error running operations', 'OK');
3484
+ },
3485
+ complete: () => {
3486
+ this.busy = false;
3487
+ },
3488
+ });
3489
+ }
3111
3490
  /**
3112
3491
  * Update the snapshot view by running the operations up to the
3113
3492
  * currently edited operation if any.
@@ -3126,64 +3505,6 @@ class SnapshotEditorComponent {
3126
3505
  }, 0);
3127
3506
  }
3128
3507
  // #endregion
3129
- /**
3130
- * Build the snapshot at the specified execution step. This implies adding
3131
- * the nodes introduced by the operations up to the specified step, and
3132
- * updating the features of the nodes introduced by the specified step.
3133
- * Also, the operations after the specified step are removed from the
3134
- * snapshot.
3135
- *
3136
- * @param step The step to build the snapshot at.
3137
- * @returns The snapshot at the specified step.
3138
- */
3139
- buildSnapshotAtStep(step) {
3140
- // no result, nothing to do
3141
- if (!this.result) {
3142
- return null;
3143
- }
3144
- // get the currently edited snapshot
3145
- const snapshot = this.getSnapshot();
3146
- // find the max ID in snapshot.text (=original) nodes,
3147
- // so that any ID greater than it belongs to new nodes
3148
- const maxOriginalId = snapshot.text.reduce((max, n) => Math.max(max, n.id), 0);
3149
- // get a copy of the nodes of the snapshot base text
3150
- const snapNodes = deepCopy(snapshot.text);
3151
- // get the nodes of the picked step
3152
- const stepNodes = this.result.taggedNodes[step.outputTag];
3153
- // update the snapshot text nodes copies with the picked step nodes,
3154
- // by updating their features if existing, or adding new nodes if not.
3155
- for (const sn of stepNodes) {
3156
- // find the node in the original snapshot text
3157
- const i = snapNodes.findIndex((n) => n.id === sn.id);
3158
- // if found, update its features, else add it
3159
- if (i > -1) {
3160
- snapNodes[i].features = sn.features;
3161
- }
3162
- else {
3163
- // new node: we assume that it has a manually set position (x,y features)
3164
- snapNodes.push(sn);
3165
- }
3166
- }
3167
- // append nodes introduced *before* the picked step, if any
3168
- // (those introduced by the picked step have already been added)
3169
- const stepIndex = this.result.steps.indexOf(step);
3170
- for (let i = 0; i < stepIndex; i++) {
3171
- const stepNodes = this.result.taggedNodes[this.result.steps[i].outputTag];
3172
- for (const n of stepNodes) {
3173
- if (n.id > maxOriginalId && !snapNodes.find((x) => x.id === n.id)) {
3174
- snapNodes.push(n);
3175
- }
3176
- }
3177
- }
3178
- // update the snapshot text nodes
3179
- snapshot.text = snapNodes;
3180
- // remove from the snapshot the operations after the picked step
3181
- const i = snapshot.operations.findIndex((o) => o.id === step.operation.id);
3182
- if (i > -1) {
3183
- snapshot.operations = snapshot.operations.slice(0, i + 1);
3184
- }
3185
- return snapshot;
3186
- }
3187
3508
  playTimeline(tl) {
3188
3509
  const shadowRoot = this.snapshotView?.nativeElement?.shadowRoot;
3189
3510
  if (tl && shadowRoot) {
@@ -3204,28 +3525,32 @@ class SnapshotEditorComponent {
3204
3525
  *
3205
3526
  * @param step The step to pick.
3206
3527
  */
3207
- async onStepPick(step) {
3528
+ onStepPick(step) {
3208
3529
  if (!this.result || this._stepPickFrozen) {
3209
3530
  return;
3210
3531
  }
3211
- // get the currently edited snapshot
3212
- const snapshot = this.buildSnapshotAtStep(step);
3532
+ // get base text snapshot
3533
+ const snapshot = this.getSnapshot();
3534
+ // get the step index
3535
+ const index = snapshot.operations.findIndex((o) => o.id === step.operation.id);
3536
+ // remove snapshot operations after the picked step to avoid
3537
+ // their visuals creep into the snapshot view
3538
+ snapshot.operations = snapshot.operations.slice(0, index + 1);
3213
3539
  // update result operation ID
3214
- this.resultOperationId = step.operation.id;
3215
- // update transparent IDs
3216
- this._transparentIds = this.getTransparentIds(step.operation.diplomatics?.g);
3217
- // update view data
3540
+ this.resultOperationId = snapshot.operations[index].id;
3541
+ // extract the IDs from the last operation's diplomatics and filter
3542
+ // them so that only the ones ending with _t are kept. By convention,
3543
+ // all the IDs ending with this suffix are to be made invisible at
3544
+ // their first rendition (opacity=0). An animation will then make
3545
+ // them visible.
3546
+ this._transparentIds = this.getTransparentIds(snapshot.operations[index].diplomatics?.g);
3547
+ // update the snapshot text nodes with those introduced by operations
3548
+ this.supplyOpNodes(snapshot, this.result, step.outputTag);
3549
+ // update the view data
3218
3550
  this.setViewData(snapshot, step.outputTag);
3219
3551
  // play animation if any
3220
3552
  const tl = this.timelines.value.find((t) => t.tag === step.outputTag);
3221
3553
  if (tl) {
3222
- this._stepPickFrozen = true;
3223
- // find the index of the picked step in the snapshot operations
3224
- const i = snapshot.operations.findIndex((o) => o.id === step.operation.id);
3225
- // run the operations up to the picked step as we need to hide
3226
- // those visuals to be revealed by the animation
3227
- await this.runTo(i);
3228
- this._stepPickFrozen = false;
3229
3554
  // play the timeline
3230
3555
  setTimeout(() => {
3231
3556
  this.playTimeline(tl);
@@ -3278,6 +3603,9 @@ class SnapshotEditorComponent {
3278
3603
  * @param timelines The timelines.
3279
3604
  */
3280
3605
  onTimelinesChange(timelines) {
3606
+ if (!timelines) {
3607
+ return;
3608
+ }
3281
3609
  this.timelines.setValue(timelines);
3282
3610
  this.timelines.markAsDirty();
3283
3611
  this.timelines.updateValueAndValidity();
@@ -3297,6 +3625,36 @@ class SnapshotEditorComponent {
3297
3625
  onSnapshotRender(event) {
3298
3626
  this._renderer = event.detail.renderer;
3299
3627
  }
3628
+ onImageLoad(imageElement) {
3629
+ const size = {
3630
+ width: imageElement.naturalWidth,
3631
+ height: imageElement.naturalHeight,
3632
+ };
3633
+ if (!this.imageWidth.value) {
3634
+ this.imageWidth.setValue(size.width);
3635
+ this.imageWidth.updateValueAndValidity();
3636
+ this.imageWidth.markAsDirty();
3637
+ }
3638
+ if (!this.imageHeight.value) {
3639
+ this.imageHeight.setValue(size.height);
3640
+ this.imageHeight.updateValueAndValidity();
3641
+ this.imageHeight.markAsDirty();
3642
+ }
3643
+ }
3644
+ onImageOpacityChange(value) {
3645
+ this.imageOpacity.setValue(value);
3646
+ this.imageOpacity.updateValueAndValidity();
3647
+ this.imageOpacity.markAsDirty();
3648
+ if (this._renderer) {
3649
+ this._renderer.setImageOpacity(value);
3650
+ }
3651
+ }
3652
+ resetImgMetadata() {
3653
+ this.imageX.reset();
3654
+ this.imageY.reset();
3655
+ this.imageWidth.reset();
3656
+ this.imageHeight.reset();
3657
+ }
3300
3658
  /**
3301
3659
  * Toggle rulers in the snapshot view.
3302
3660
  */
@@ -3319,7 +3677,9 @@ class SnapshotEditorComponent {
3319
3677
  },
3320
3678
  style: this.style.value || undefined,
3321
3679
  defs: this.defs.value || undefined,
3322
- text: this.baseText.value,
3680
+ // snapshot nodes might be changed, so we copy them
3681
+ // to avoid changing the original snapshot
3682
+ text: deepCopy(this.baseText.value),
3323
3683
  textStyle: this.textStyle.value || undefined,
3324
3684
  textOptions: {
3325
3685
  lineHeightOffset: this.lineHeightOffset.value,
@@ -3369,16 +3729,16 @@ class SnapshotEditorComponent {
3369
3729
  * in the snapshot change event.
3370
3730
  */
3371
3731
  save() {
3372
- if (this.form.invalid || this.noSave) {
3732
+ if (this.form.invalid || this.noSave()) {
3373
3733
  return;
3374
3734
  }
3375
- this._snapshot = this.getSnapshot();
3376
- this.snapshotChange.emit(this._snapshot);
3735
+ this.snapshot.set(this.getSnapshot());
3736
+ this.snapshotChange.emit(this.snapshot());
3377
3737
  }
3378
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: SnapshotEditorComponent, deps: [{ token: i1.FormBuilder }, { token: GveApiService }, { token: i3$1.MatDialog }, { token: i2$2.DialogService }, { token: i5$2.MatSnackBar }], target: i0.ɵɵFactoryTarget.Component }); }
3379
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.0", type: SnapshotEditorComponent, isStandalone: true, selector: "gve-snapshot-editor", inputs: { snapshot: "snapshot", batchOps: "batchOps", noSave: "noSave", debug: "debug" }, outputs: { snapshotChange: "snapshotChange", snapshotCancel: "snapshotCancel" }, viewQueries: [{ propertyName: "snapshotView", first: true, predicate: ["snapshotView"], descendants: true }], ngImport: i0, template: "<form [formGroup]=\"form\" (submit)=\"save()\">\r\n <mat-tab-group>\r\n <!-- text -->\r\n <mat-tab>\r\n <ng-template mat-tab-label>\r\n <mat-icon>article</mat-icon> <span class=\"label\">text</span>\r\n </ng-template>\r\n <mat-expansion-panel [expanded]=\"!baseText.value\">\r\n <mat-expansion-panel-header>\r\n <mat-panel-title> base text </mat-panel-title>\r\n </mat-expansion-panel-header>\r\n <gve-base-text-editor\r\n [text]=\"baseText.value\"\r\n (textChange)=\"onTextChange($event)\"\r\n />\r\n <fieldset>\r\n <div class=\"form-row\">\r\n <!-- offsetX -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>X offset</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"offsetX\"\r\n placeholder=\"X offset\"\r\n />\r\n </mat-form-field>\r\n\r\n <!-- offsetY -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>Y offset</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"offsetY\"\r\n placeholder=\"Y offset\"\r\n />\r\n </mat-form-field>\r\n\r\n <!-- lineHeightOffset -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>ln h-offset</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"lineHeightOffset\"\r\n placeholder=\"ln h-offset\"\r\n />\r\n </mat-form-field>\r\n\r\n <!-- charSpacingOffset -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>char spacing</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"charSpacingOffset\"\r\n placeholder=\"char spacing\"\r\n />\r\n </mat-form-field>\r\n\r\n <!-- spcWidthOffset -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>spc w-offset</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"spcWidthOffset\"\r\n placeholder=\"spc w-offset\"\r\n />\r\n </mat-form-field>\r\n <!-- minLineHeights -->\r\n <div class=\"boxed\">\r\n <gve-ln-heights-editor\r\n [lineCount]=\"lineCount\"\r\n [heights]=\"lnHeights.value\"\r\n (heightsChange)=\"onHeightsChange($event)\"\r\n />\r\n </div>\r\n </div>\r\n <!-- textStyle -->\r\n <div>\r\n <mat-form-field class=\"long-text\" appearance=\"outline\">\r\n <mat-label>text style</mat-label>\r\n <textarea\r\n matInput\r\n [formControl]=\"textStyle\"\r\n placeholder=\"text style\"\r\n ></textarea>\r\n </mat-form-field>\r\n </div>\r\n </fieldset>\r\n </mat-expansion-panel>\r\n </mat-tab>\r\n\r\n <!-- operations -->\r\n <mat-tab>\r\n <ng-template mat-tab-label>\r\n <mat-icon>edit</mat-icon> <span class=\"label\">operations</span>\r\n </ng-template>\r\n\r\n <div id=\"snapshot-container\">\r\n <div id=\"general\">\r\n <div class=\"form-row\">\r\n <!-- width -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>width</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n min=\"0\"\r\n [formControl]=\"width\"\r\n placeholder=\"width\"\r\n />\r\n </mat-form-field>\r\n\r\n <!-- height -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>height</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n min=\"0\"\r\n [formControl]=\"height\"\r\n placeholder=\"height\"\r\n />\r\n </mat-form-field>\r\n\r\n <div class=\"form-row right\">\r\n <!-- remove ops -->\r\n <button\r\n type=\"button\"\r\n class=\"mat-warn\"\r\n mat-flat-button\r\n matTooltip=\"Remove all the operations\"\r\n [disabled]=\"!operations.value.length || busy\"\r\n (click)=\"clearOperations()\"\r\n >\r\n <mat-icon>delete_forever</mat-icon> clear\r\n </button>\r\n\r\n <!-- batch add ops -->\r\n <button\r\n type=\"button\"\r\n mat-flat-button\r\n matTooltip=\"Add a batch of operations\"\r\n class=\"mat-primary\"\r\n [disabled]=\"busy\"\r\n (click)=\"parseOperations()\"\r\n >\r\n <mat-icon>post_add</mat-icon> batch\r\n </button>\r\n\r\n <!-- add op -->\r\n <button\r\n type=\"button\"\r\n mat-flat-button\r\n class=\"mat-primary\"\r\n [disabled]=\"!baseText.value\"\r\n (click)=\"editNewOperation()\"\r\n >\r\n <mat-icon>add_circle</mat-icon> operation\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- ops -->\r\n <div id=\"ops\">\r\n <!-- operations list -->\r\n @if (operations.value.length) {\r\n <table id=\"list\">\r\n <thead>\r\n <tr>\r\n <th></th>\r\n <th>ID</th>\r\n <th>type</th>\r\n <th>at</th>\r\n <th>run</th>\r\n <th>value</th>\r\n <th>itag</th>\r\n <th>otag</th>\r\n <th>gid</th>\r\n <th>feats</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (operation of operations.value; track operation.id; let\r\n index=$index) {\r\n <tr\r\n [ngClass]=\"{ selected: operation.id === resultOperationId }\"\r\n [ngClass]=\"{ edited: operation.id === editedOp?.id }\"\r\n >\r\n <td class=\"fit-width\">\r\n <!-- edit -->\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n class=\"mat-primary\"\r\n (click)=\"editOperation(index)\"\r\n matTooltip=\"Edit operation\"\r\n >\r\n <mat-icon>edit</mat-icon>\r\n </button>\r\n <!-- delete -->\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n class=\"mat-warn\"\r\n (click)=\"deleteOperation(index)\"\r\n matTooltip=\"Delete operation\"\r\n >\r\n <mat-icon class=\"mat-warn\">delete</mat-icon>\r\n </button>\r\n <!-- run to -->\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n class=\"mat-accent\"\r\n (click)=\"runTo(index)\"\r\n matTooltip=\"Run to this operation\"\r\n >\r\n <mat-icon class=\"mat-accent\">subscriptions</mat-icon>\r\n </button>\r\n </td>\r\n <td>{{ operation.id }}</td>\r\n <td>{{ operation.type | flatLookup : opTypeMap }}</td>\r\n <td>{{ operation.at }}</td>\r\n <td>{{ operation.run }}</td>\r\n <td>{{ operation.value }}</td>\r\n <td>{{ operation.inputTag }}</td>\r\n <td>{{ operation.outputTag }}</td>\r\n <td>{{ operation.groupId }}</td>\r\n <td>{{ operation.features?.length }}</td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n }\r\n\r\n <!-- operation editor -->\r\n <mat-expansion-panel [expanded]=\"editedOp\" [disabled]=\"!editedOp\">\r\n <mat-expansion-panel-header>\r\n <mat-panel-title>operation {{ editedOp?.id }}</mat-panel-title>\r\n </mat-expansion-panel-header>\r\n <fieldset>\r\n <gve-chain-operation-editor\r\n [hidePreview]=\"editedOpIndex === -1\"\r\n [operation]=\"editedOp\"\r\n (operationCancel)=\"onOperationCancel()\"\r\n (operationChange)=\"onOperationChange($event)\"\r\n (operationPreview)=\"onOperationPreview($event)\"\r\n />\r\n </fieldset>\r\n </mat-expansion-panel>\r\n\r\n <!-- opStyle -->\r\n <div id=\"opStyle\">\r\n <mat-form-field class=\"long-text\" appearance=\"outline\">\r\n <mat-label>operations style</mat-label>\r\n <textarea\r\n matInput\r\n [formControl]=\"opStyle\"\r\n placeholder=\"operations style\"\r\n ></textarea>\r\n </mat-form-field>\r\n </div>\r\n </div>\r\n </div>\r\n </mat-tab>\r\n\r\n <!-- image -->\r\n <mat-tab>\r\n <ng-template mat-tab-label>\r\n <mat-icon>image</mat-icon> <span class=\"label\">image</span>\r\n </ng-template>\r\n\r\n <div id=\"image\">\r\n <div id=\"image-ctl\">\r\n <!-- url -->\r\n <mat-form-field class=\"long-text\">\r\n <mat-label>URL</mat-label>\r\n <input matInput [formControl]=\"imageUrl\" />\r\n </mat-form-field>\r\n <div class=\"form-row\">\r\n <!-- x -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>X</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"imageX\"\r\n placeholder=\"X\"\r\n />\r\n </mat-form-field>\r\n <!-- y -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>Y</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"imageY\"\r\n placeholder=\"Y\"\r\n />\r\n </mat-form-field>\r\n </div>\r\n <div class=\"form-row\">\r\n <!-- width -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>width</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n min=\"0\"\r\n [formControl]=\"imageWidth\"\r\n />\r\n </mat-form-field>\r\n <!-- height -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>height</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n min=\"0\"\r\n [formControl]=\"imageHeight\"\r\n />\r\n </mat-form-field>\r\n </div>\r\n <!-- defs -->\r\n <div>\r\n <mat-form-field class=\"long-text\">\r\n <mat-label>defs</mat-label>\r\n <textarea matInput [formControl]=\"defs\" rows=\"3\"></textarea>\r\n </mat-form-field>\r\n </div>\r\n </div>\r\n <div id=\"image-view\">\r\n @if (imageUrl.value) {\r\n <img alt=\"background\" [src]=\"imageUrl.value\" width=\"600\" />\r\n }\r\n </div>\r\n </div>\r\n </mat-tab>\r\n\r\n <!-- timelines -->\r\n <mat-tab>\r\n <ng-template mat-tab-label>\r\n <mat-icon>animation</mat-icon> <span class=\"label\">animation</span>\r\n </ng-template>\r\n\r\n <gve-animation-timeline-set\r\n [tags]=\"opTags\"\r\n [elementIds]=\"opElementIds\"\r\n [timelines]=\"timelines.value\"\r\n (timelinesChange)=\"onTimelinesChange($event)\"\r\n />\r\n </mat-tab>\r\n </mat-tab-group>\r\n\r\n <!-- progress -->\r\n <div>\r\n <mat-progress-bar mode=\"indeterminate\" *ngIf=\"busy\" />\r\n </div>\r\n\r\n <!-- result -->\r\n @if (result) {\r\n <fieldset id=\"result\">\r\n <legend>result</legend>\r\n <gve-chain-result-view\r\n [result]=\"result\"\r\n [initialStepIndex]=\"initialStepIndex\"\r\n (stepPick)=\"onStepPick($event)\"\r\n />\r\n </fieldset>\r\n }\r\n\r\n <!-- snapshot view -->\r\n <fieldset id=\"preview\">\r\n <legend class=\"button-row\">\r\n <span>{{ viewTitle }}</span>\r\n </legend>\r\n <!-- snapshot view -->\r\n <gve-snapshot-view\r\n #snapshotView\r\n [debug]=\"debug\"\r\n [data]=\"viewData\"\r\n (snapshotRender)=\"onSnapshotRender($any($event))\"\r\n (visualEvent)=\"onVisualEvent($any($event))\"\r\n />\r\n <div class=\"button-row\">\r\n <!-- toggle ruler -->\r\n <mat-button-toggle\r\n type=\"button\"\r\n mat-icon-button\r\n color=\"primary\"\r\n matTooltip=\"Toggle rulers\"\r\n [checked]=\"rulers\"\r\n (change)=\"toggleRulers()\"\r\n >\r\n <mat-icon class=\"mat-primary\">straighten</mat-icon>\r\n </mat-button-toggle>\r\n <!-- run to last -->\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n class=\"primary\"\r\n matTooltip=\"Run all operations\"\r\n [disabled]=\"!operations.value.length\"\r\n (click)=\"runToLast()\"\r\n >\r\n <mat-icon>play_circle</mat-icon>\r\n </button>\r\n </div>\r\n @if (visualInfo) {\r\n <div id=\"visual-info\">{{ visualInfo }}</div>\r\n }\r\n </fieldset>\r\n\r\n <!--buttons -->\r\n <div class=\"form-row-center\">\r\n <button\r\n type=\"button\"\r\n class=\"mat-warn\"\r\n mat-flat-button\r\n matTooltip=\"Discard changes\"\r\n (click)=\"close()\"\r\n >\r\n <mat-icon>clear</mat-icon>\r\n close\r\n </button>\r\n @if (!noSave) {\r\n <button\r\n type=\"submit\"\r\n class=\"mat-primary\"\r\n mat-flat-button\r\n matTooltip=\"Save changes\"\r\n [disabled]=\"form.invalid\"\r\n >\r\n <mat-icon>check_circle</mat-icon>\r\n save\r\n </button>\r\n }\r\n </div>\r\n</form>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row-center{display:flex;gap:8px;align-items:center;justify-content:center;flex-wrap:wrap}.form-row,.form-row-center *{flex:0 0 auto}.form-row .right{margin-left:auto}.button-row{display:flex;align-items:center;flex-wrap:wrap}.button-row *{flex:0 0 auto}.long-text{width:100%;max-width:800px}#text-range{margin:8px;border:1px solid silver;border-radius:6px;padding:6px}mat-expansion-panel{margin:8px 0}div#visual-info{font-size:95%;color:#909090;margin:8px}#list{margin:8px 0}#opStyle{margin-top:8px}table{width:100%;border-collapse:collapse}th{color:#909090;font-weight:400;text-align:left;background-color:#e1e0e0}th,td{padding:4px;border-bottom:1px solid silver}tbody tr:nth-child(2n){background-color:#e8e8e8}td.fit-width{width:1px;white-space:nowrap}tr.selected{background-color:#c8d9eb}tr.edited{background-color:#f6f6e4}fieldset{border:1px solid silver;border-radius:6px;padding:8px;margin:8px 0}legend{color:#909090}.error{color:red}.input-nr{width:6em}.full-width{width:100%}.code{font-family:Courier New,Courier,monospace}.boxed{border:1px solid silver;border-radius:6px;padding:8px;margin:8px 0}span.label{margin-left:8px}gve-animation-timeline-set{margin:8px 0}div#image{display:grid;gap:8px;grid-template-rows:auto;grid-template-columns:1fr auto;grid-template-areas:\"image-ctl image-view\"}div#image-ctl{grid-area:image-ctl}div#image-view{grid-area:image-view}@media only screen and (max-width: 959px){div#image{grid-template-columns:1fr;grid-template-areas:\"image-ctl\" \"image-view\"}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatButtonToggleModule }, { kind: "component", type: i8$1.MatButtonToggle, selector: "mat-button-toggle", inputs: ["aria-label", "aria-labelledby", "id", "name", "value", "tabIndex", "disableRipple", "appearance", "checked", "disabled", "disabledInteractive"], outputs: ["change"], exportAs: ["matButtonToggle"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "component", type: i4.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i4.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i4.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatProgressBarModule }, { kind: "component", type: i13.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "ngmodule", type: MatSnackBarModule }, { kind: "ngmodule", type: MatTabsModule }, { kind: "directive", type: i14.MatTabLabel, selector: "[mat-tab-label], [matTabLabel]" }, { kind: "component", type: i14.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i14.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i10.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: NgToolsModule }, { kind: "pipe", type: i2$1.FlatLookupPipe, name: "flatLookup" }, { kind: "component", type: BaseTextEditorComponent, selector: "gve-base-text-editor", inputs: ["text"], outputs: ["textChange"] }, { kind: "component", type: ChainOperationEditorComponent, selector: "gve-chain-operation-editor", inputs: ["operation", "snapshot", "hidePreview"], outputs: ["operationChange", "operationPreview", "operationCancel"] }, { kind: "component", type: ChainResultViewComponent, selector: "gve-chain-result-view", inputs: ["result", "initialStepIndex"], outputs: ["stepPick"] }, { kind: "component", type: LnHeightsEditorComponent, selector: "gve-ln-heights-editor", inputs: ["lineCount", "heights"], outputs: ["heightsChange"] }, { kind: "component", type: AnimationTimelineSetComponent, selector: "gve-animation-timeline-set", inputs: ["timelines", "elementIds", "tags"], outputs: ["timelinesChange", "timelinesCancel"] }] }); }
3738
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: SnapshotEditorComponent, deps: [{ token: i1.FormBuilder }, { token: GveApiService }, { token: i3$1.MatDialog }, { token: i2$1.DialogService }, { token: i4$2.MatSnackBar }], target: i0.ɵɵFactoryTarget.Component }); }
3739
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.6", type: SnapshotEditorComponent, isStandalone: true, selector: "gve-snapshot-editor", inputs: { snapshot: { classPropertyName: "snapshot", publicName: "snapshot", isSignal: true, isRequired: false, transformFunction: null }, batchOps: { classPropertyName: "batchOps", publicName: "batchOps", isSignal: true, isRequired: false, transformFunction: null }, noSave: { classPropertyName: "noSave", publicName: "noSave", isSignal: true, isRequired: false, transformFunction: null }, debug: { classPropertyName: "debug", publicName: "debug", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { snapshot: "snapshotChange", snapshotChange: "snapshotChange", snapshotCancel: "snapshotCancel" }, viewQueries: [{ propertyName: "snapshotView", first: true, predicate: ["snapshotView"], descendants: true }], ngImport: i0, template: "<form [formGroup]=\"form\" (submit)=\"save()\">\r\n <mat-tab-group>\r\n <!-- text -->\r\n <mat-tab>\r\n <ng-template mat-tab-label>\r\n <mat-icon>article</mat-icon> <span class=\"label\">text</span>\r\n </ng-template>\r\n <mat-expansion-panel>\r\n <mat-expansion-panel-header>\r\n <mat-panel-title> base text </mat-panel-title>\r\n </mat-expansion-panel-header>\r\n <!-- base text -->\r\n <div class=\"form-row\">\r\n <button\r\n type=\"button\"\r\n mat-flat-button\r\n color=\"mat-warn\"\r\n matTooltip=\"Enter base text\"\r\n (click)=\"inputBaseText()\"\r\n [disabled]=\"busy\"\r\n >\r\n <mat-icon>edit</mat-icon> base text\r\n </button>\r\n\r\n @if (textRange) {\r\n <span id=\"text-range\">{{ textRange.at }}\u00D7{{ textRange.run }}</span>\r\n }\r\n </div>\r\n <!-- base text metadata -->\r\n <fieldset>\r\n <div class=\"form-row\">\r\n <!-- offsetX -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>X offset</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"offsetX\"\r\n placeholder=\"X offset\"\r\n />\r\n </mat-form-field>\r\n\r\n <!-- offsetY -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>Y offset</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"offsetY\"\r\n placeholder=\"Y offset\"\r\n />\r\n </mat-form-field>\r\n\r\n <!-- lineHeightOffset -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>ln h-offset</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"lineHeightOffset\"\r\n placeholder=\"ln h-offset\"\r\n />\r\n </mat-form-field>\r\n\r\n <!-- charSpacingOffset -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>char spacing</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"charSpacingOffset\"\r\n placeholder=\"char spacing\"\r\n />\r\n </mat-form-field>\r\n\r\n <!-- spcWidthOffset -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>spc w-offset</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"spcWidthOffset\"\r\n placeholder=\"spc w-offset\"\r\n />\r\n </mat-form-field>\r\n <!-- minLineHeights -->\r\n <div class=\"boxed\">\r\n <gve-ln-heights-editor\r\n [lineCount]=\"lineCount\"\r\n [heights]=\"lnHeights.value || undefined\"\r\n (heightsChange)=\"onHeightsChange($event)\"\r\n />\r\n </div>\r\n </div>\r\n <!-- textStyle -->\r\n <div>\r\n <mat-form-field class=\"long-text\" appearance=\"outline\">\r\n <mat-label>text style</mat-label>\r\n <textarea\r\n matInput\r\n [formControl]=\"textStyle\"\r\n placeholder=\"text style\"\r\n ></textarea>\r\n </mat-form-field>\r\n </div>\r\n </fieldset>\r\n <!-- text characters -->\r\n <gve-base-text-view\r\n [text]=\"baseText.value\"\r\n [hasLineNumber]=\"true\"\r\n (rangePick)=\"onRangePick($event)\"\r\n />\r\n </mat-expansion-panel>\r\n </mat-tab>\r\n\r\n <!-- operations -->\r\n <mat-tab>\r\n <ng-template mat-tab-label>\r\n <mat-icon>edit</mat-icon> <span class=\"label\">operations</span>\r\n </ng-template>\r\n\r\n <div id=\"snapshot-container\">\r\n <div id=\"general\">\r\n <div class=\"form-row\">\r\n <!-- width -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>width</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n min=\"0\"\r\n [formControl]=\"width\"\r\n placeholder=\"width\"\r\n />\r\n </mat-form-field>\r\n\r\n <!-- height -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>height</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n min=\"0\"\r\n [formControl]=\"height\"\r\n placeholder=\"height\"\r\n />\r\n </mat-form-field>\r\n\r\n <!-- chain -->\r\n <mat-checkbox [formControl]=\"showChain\">show chain</mat-checkbox>\r\n\r\n <div class=\"form-row right\">\r\n <!-- remove ops -->\r\n <button\r\n type=\"button\"\r\n class=\"mat-warn\"\r\n mat-flat-button\r\n matTooltip=\"Remove all the operations\"\r\n [disabled]=\"!operations.value.length || busy\"\r\n (click)=\"clearOperations()\"\r\n >\r\n <mat-icon>delete_forever</mat-icon> clear\r\n </button>\r\n\r\n <!-- batch add ops -->\r\n <button\r\n type=\"button\"\r\n mat-flat-button\r\n matTooltip=\"Add a batch of operations\"\r\n class=\"mat-primary\"\r\n [disabled]=\"busy\"\r\n (click)=\"parseOperations()\"\r\n >\r\n <mat-icon>post_add</mat-icon> batch\r\n </button>\r\n\r\n <!-- add op -->\r\n <button\r\n type=\"button\"\r\n mat-flat-button\r\n class=\"mat-primary\"\r\n [disabled]=\"!baseText.value\"\r\n (click)=\"editNewOperation()\"\r\n >\r\n <mat-icon>add_circle</mat-icon> operation\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- ops -->\r\n <div id=\"ops\">\r\n <!-- operations list -->\r\n @if (operations.value.length) {\r\n <table id=\"list\">\r\n <thead>\r\n <tr>\r\n <th>nr.</th>\r\n <th></th>\r\n <th>ID</th>\r\n <th>type</th>\r\n <th>at</th>\r\n <th>run</th>\r\n <th>value</th>\r\n <th>itag</th>\r\n <th>otag</th>\r\n <th>gid</th>\r\n <th>feats</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (operation of operations.value; track operation.id; let\r\n index=$index) {\r\n <tr\r\n [ngClass]=\"{ selected: operation.id === resultOperationId }\"\r\n [ngClass]=\"{ edited: operation.id === editedOp?.id }\"\r\n >\r\n <td class=\"fit-width\">{{ index + 1 }}.</td>\r\n <td class=\"fit-width\">\r\n <!-- edit -->\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n class=\"mat-primary\"\r\n (click)=\"editOperation(index)\"\r\n matTooltip=\"Edit operation\"\r\n >\r\n <mat-icon>edit</mat-icon>\r\n </button>\r\n <!-- delete -->\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n class=\"mat-warn\"\r\n (click)=\"deleteOperation(index)\"\r\n matTooltip=\"Delete operation\"\r\n >\r\n <mat-icon class=\"mat-warn\">delete</mat-icon>\r\n </button>\r\n <!-- run to -->\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n class=\"mat-accent\"\r\n (click)=\"runTo(index)\"\r\n matTooltip=\"Run to this operation\"\r\n >\r\n <mat-icon class=\"mat-accent\">subscriptions</mat-icon>\r\n </button>\r\n </td>\r\n <td>{{ operation.id }}</td>\r\n <td>{{ operation.type | flatLookup : opTypeMap }}</td>\r\n <td>{{ operation.at }}</td>\r\n <td>{{ operation.run }}</td>\r\n <td>{{ operation.value }}</td>\r\n <td>{{ operation.inputTag }}</td>\r\n <td>{{ operation.outputTag }}</td>\r\n <td>{{ operation.groupId }}</td>\r\n <td>{{ operation.features?.length }}</td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n }\r\n\r\n <!-- operation editor -->\r\n <mat-expansion-panel [expanded]=\"editedOp\" [disabled]=\"!editedOp\">\r\n <mat-expansion-panel-header>\r\n <mat-panel-title>operation {{ editedOp?.id }}</mat-panel-title>\r\n </mat-expansion-panel-header>\r\n <fieldset>\r\n <gve-chain-operation-editor\r\n [hidePreview]=\"editedOpIndex === -1\"\r\n [operation]=\"editedOp\"\r\n (operationCancel)=\"onOperationCancel()\"\r\n (operationChange)=\"onOperationChange($event)\"\r\n (operationPreview)=\"onOperationPreview($event)\"\r\n />\r\n </fieldset>\r\n </mat-expansion-panel>\r\n\r\n <!-- opStyle -->\r\n <div id=\"opStyle\">\r\n <mat-form-field class=\"long-text\" appearance=\"outline\">\r\n <mat-label>operations style</mat-label>\r\n <textarea\r\n matInput\r\n [formControl]=\"opStyle\"\r\n placeholder=\"operations style\"\r\n ></textarea>\r\n </mat-form-field>\r\n </div>\r\n </div>\r\n </div>\r\n </mat-tab>\r\n\r\n <!-- image -->\r\n <mat-tab>\r\n <ng-template mat-tab-label>\r\n <mat-icon>image</mat-icon> <span class=\"label\">image</span>\r\n </ng-template>\r\n\r\n <div id=\"image\">\r\n <div id=\"image-ctl\">\r\n <!-- url -->\r\n <mat-form-field class=\"long-text\">\r\n <mat-label>URL</mat-label>\r\n <input matInput [formControl]=\"imageUrl\" />\r\n </mat-form-field>\r\n <div class=\"form-row\">\r\n <!-- x -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>X</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"imageX\"\r\n placeholder=\"X\"\r\n />\r\n </mat-form-field>\r\n <!-- y -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>Y</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"imageY\"\r\n placeholder=\"Y\"\r\n />\r\n </mat-form-field>\r\n <!-- width -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>width</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n min=\"0\"\r\n [formControl]=\"imageWidth\"\r\n />\r\n </mat-form-field>\r\n <!-- height -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>height</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n min=\"0\"\r\n [formControl]=\"imageHeight\"\r\n />\r\n </mat-form-field>\r\n <!-- reset -->\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Reset image metadata\"\r\n (click)=\"resetImgMetadata()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n </div>\r\n <div>\r\n <!-- image opacity -->\r\n <mat-slider\r\n min=\"0\"\r\n max=\"1\"\r\n step=\"0.01\"\r\n thumbLabel\r\n color=\"primary\"\r\n class=\"mat-primary\"\r\n matTooltip=\"Image opacity\"\r\n >\r\n <input\r\n matSliderThumb\r\n [value]=\"imageOpacity.value\"\r\n (valueChange)=\"onImageOpacityChange($event)\"\r\n />\r\n </mat-slider>\r\n <span id=\"opacity-value\">{{\r\n imageOpacity.value | number : \"1.2-2\"\r\n }}</span>\r\n </div>\r\n <!-- defs -->\r\n <div>\r\n <mat-form-field class=\"long-text\">\r\n <mat-label>defs</mat-label>\r\n <textarea matInput [formControl]=\"defs\" rows=\"3\"></textarea>\r\n </mat-form-field>\r\n </div>\r\n </div>\r\n <div id=\"image-view\">\r\n @if (imageUrl.value) {\r\n <img\r\n #imgElement\r\n alt=\"background\"\r\n [src]=\"imageUrl.value\"\r\n width=\"600\"\r\n (load)=\"onImageLoad(imgElement)\"\r\n />\r\n }\r\n </div>\r\n </div>\r\n </mat-tab>\r\n\r\n <!-- timelines -->\r\n <mat-tab>\r\n <ng-template mat-tab-label>\r\n <mat-icon>animation</mat-icon> <span class=\"label\">animation</span>\r\n </ng-template>\r\n\r\n <gve-animation-timeline-set\r\n [tags]=\"opTags\"\r\n [elementIds]=\"opElementIds\"\r\n [timelines]=\"timelines.value\"\r\n (timelinesChange)=\"onTimelinesChange($event)\"\r\n />\r\n </mat-tab>\r\n </mat-tab-group>\r\n\r\n <!-- progress -->\r\n <div>\r\n <mat-progress-bar mode=\"indeterminate\" *ngIf=\"busy\" />\r\n </div>\r\n\r\n <!-- result -->\r\n @if (result) {\r\n <fieldset id=\"result\">\r\n <legend>result</legend>\r\n <gve-chain-result-view\r\n [result]=\"result\"\r\n [initialStepIndex]=\"initialStepIndex\"\r\n (stepPick)=\"onStepPick($event)\"\r\n />\r\n </fieldset>\r\n }\r\n\r\n <!-- snapshot view -->\r\n <fieldset id=\"preview\">\r\n <legend class=\"button-row\">\r\n <span>{{ viewTitle }}</span>\r\n </legend>\r\n <!-- snapshot view -->\r\n <gve-snapshot-view\r\n #snapshotView\r\n [debug]=\"debug()\"\r\n [data]=\"viewData\"\r\n (snapshotRender)=\"onSnapshotRender($any($event))\"\r\n (visualEvent)=\"onVisualEvent($any($event))\"\r\n />\r\n <div class=\"button-row\">\r\n <!-- toggle ruler -->\r\n <mat-button-toggle\r\n type=\"button\"\r\n mat-icon-button\r\n color=\"primary\"\r\n matTooltip=\"Toggle rulers\"\r\n [checked]=\"rulers\"\r\n (change)=\"toggleRulers()\"\r\n >\r\n <mat-icon class=\"mat-primary\">straighten</mat-icon>\r\n </mat-button-toggle>\r\n <!-- run to last -->\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n class=\"primary\"\r\n matTooltip=\"Refresh preview\"\r\n (click)=\"runToLast()\"\r\n >\r\n <mat-icon>refresh</mat-icon>\r\n </button>\r\n </div>\r\n @if (visualInfo) {\r\n <div id=\"visual-info\">{{ visualInfo }}</div>\r\n }\r\n </fieldset>\r\n\r\n <!-- chain view -->\r\n @if (chain) {\r\n <div id=\"chain-view\">\r\n <gve-chain-view [chain]=\"chain\" />\r\n </div>\r\n }\r\n\r\n <!--buttons -->\r\n <div class=\"form-row-center\">\r\n <button\r\n type=\"button\"\r\n class=\"mat-warn\"\r\n mat-flat-button\r\n matTooltip=\"Discard changes\"\r\n (click)=\"close()\"\r\n >\r\n <mat-icon>clear</mat-icon>\r\n close\r\n </button>\r\n @if (!noSave()) {\r\n <button\r\n type=\"submit\"\r\n class=\"mat-primary\"\r\n mat-flat-button\r\n matTooltip=\"Save changes\"\r\n [disabled]=\"form.invalid\"\r\n >\r\n <mat-icon>check_circle</mat-icon>\r\n save\r\n </button>\r\n }\r\n </div>\r\n</form>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row-center{display:flex;gap:8px;align-items:center;justify-content:center;flex-wrap:wrap}.form-row,.form-row-center *{flex:0 0 auto}.form-row .right{margin-left:auto}.button-row{display:flex;align-items:center;flex-wrap:wrap}.button-row *{flex:0 0 auto}.long-text{width:100%;max-width:800px}#text-range{margin:8px;border:1px solid silver;border-radius:6px;padding:6px}mat-expansion-panel{margin:8px 0}div#visual-info{font-size:95%;color:#909090;margin:8px}#list{margin:8px 0}#opStyle{margin-top:8px}table{width:100%;border-collapse:collapse}th{color:#909090;font-weight:400;text-align:left;background-color:#e1e0e0}th,td{padding:4px;border-bottom:1px solid silver}tbody tr:nth-child(2n){background-color:#e8e8e8}td.fit-width{width:1px;white-space:nowrap}tr.selected{background-color:#c8d9eb}tr.edited{background-color:#f6f6e4}fieldset{border:1px solid silver;border-radius:6px;padding:8px;margin:8px 0}legend{color:#909090}.error{color:red}.input-nr{width:6em}.full-width{width:100%}.code{font-family:Courier New,Courier,monospace}.boxed{border:1px solid silver;border-radius:6px;padding:8px;margin:8px 0}span.label{margin-left:8px}gve-animation-timeline-set{margin:8px 0}div#image{display:grid;gap:8px;grid-template-rows:auto;grid-template-columns:1fr auto;grid-template-areas:\"image-ctl image-view\"}div#image-ctl{grid-area:image-ctl}div#image-view{grid-area:image-view}div#chain-view{margin-bottom:8px}@media only screen and (max-width: 959px){div#image{grid-template-columns:1fr;grid-template-areas:\"image-ctl\" \"image-view\"}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i2.DecimalPipe, name: "number" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatButtonToggleModule }, { kind: "component", type: i8$1.MatButtonToggle, selector: "mat-button-toggle", inputs: ["aria-label", "aria-labelledby", "id", "name", "value", "tabIndex", "disableRipple", "appearance", "checked", "disabled", "disabledInteractive"], outputs: ["change"], exportAs: ["matButtonToggle"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i4$1.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "component", type: i4.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i4.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i4.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatProgressBarModule }, { kind: "component", type: i14.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "ngmodule", type: MatSliderModule }, { kind: "component", type: i15.MatSlider, selector: "mat-slider", inputs: ["disabled", "discrete", "showTickMarks", "min", "color", "disableRipple", "max", "step", "displayWith"], exportAs: ["matSlider"] }, { kind: "directive", type: i15.MatSliderThumb, selector: "input[matSliderThumb]", inputs: ["value"], outputs: ["valueChange", "dragStart", "dragEnd"], exportAs: ["matSliderThumb"] }, { kind: "ngmodule", type: MatSnackBarModule }, { kind: "ngmodule", type: MatTabsModule }, { kind: "directive", type: i16.MatTabLabel, selector: "[mat-tab-label], [matTabLabel]" }, { kind: "component", type: i16.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i16.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i10.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: ChainOperationEditorComponent, selector: "gve-chain-operation-editor", inputs: ["operation", "snapshot", "hidePreview"], outputs: ["operationChange", "operationPreview", "operationCancel"] }, { kind: "component", type: ChainResultViewComponent, selector: "gve-chain-result-view", inputs: ["result", "initialStepIndex"], outputs: ["stepPick"] }, { kind: "component", type: BaseTextViewComponent, selector: "gve-base-text-view", inputs: ["defaultColor", "defaultBorderColor", "selectionColor", "hasLineNumber", "text", "borderColorCallback"], outputs: ["charPick", "rangePick"] }, { kind: "component", type: LnHeightsEditorComponent, selector: "gve-ln-heights-editor", inputs: ["lineCount", "heights"], outputs: ["heightsChange"] }, { kind: "component", type: AnimationTimelineSetComponent, selector: "gve-animation-timeline-set", inputs: ["timelines", "elementIds", "tags"], outputs: ["timelinesChange", "timelinesCancel"] }, { kind: "pipe", type: FlatLookupPipe, name: "flatLookup" }, { kind: "component", type: ChainViewComponent, selector: "gve-chain-view", inputs: ["chain", "direction", "selectedTags"], outputs: ["selectedTagsChange"] }] }); }
3380
3740
  }
3381
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: SnapshotEditorComponent, decorators: [{
3741
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.6", ngImport: i0, type: SnapshotEditorComponent, decorators: [{
3382
3742
  type: Component,
3383
3743
  args: [{ selector: 'gve-snapshot-editor', imports: [
3384
3744
  CommonModule,
@@ -3392,31 +3752,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImpor
3392
3752
  MatIconModule,
3393
3753
  MatInputModule,
3394
3754
  MatProgressBarModule,
3755
+ MatSliderModule,
3395
3756
  MatSnackBarModule,
3396
3757
  MatTabsModule,
3397
3758
  MatTooltipModule,
3398
- NgToolsModule,
3399
- BaseTextEditorComponent,
3400
3759
  ChainOperationEditorComponent,
3401
3760
  ChainResultViewComponent,
3761
+ BaseTextViewComponent,
3402
3762
  LnHeightsEditorComponent,
3403
3763
  AnimationTimelineSetComponent,
3404
- ], schemas: [CUSTOM_ELEMENTS_SCHEMA], template: "<form [formGroup]=\"form\" (submit)=\"save()\">\r\n <mat-tab-group>\r\n <!-- text -->\r\n <mat-tab>\r\n <ng-template mat-tab-label>\r\n <mat-icon>article</mat-icon> <span class=\"label\">text</span>\r\n </ng-template>\r\n <mat-expansion-panel [expanded]=\"!baseText.value\">\r\n <mat-expansion-panel-header>\r\n <mat-panel-title> base text </mat-panel-title>\r\n </mat-expansion-panel-header>\r\n <gve-base-text-editor\r\n [text]=\"baseText.value\"\r\n (textChange)=\"onTextChange($event)\"\r\n />\r\n <fieldset>\r\n <div class=\"form-row\">\r\n <!-- offsetX -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>X offset</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"offsetX\"\r\n placeholder=\"X offset\"\r\n />\r\n </mat-form-field>\r\n\r\n <!-- offsetY -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>Y offset</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"offsetY\"\r\n placeholder=\"Y offset\"\r\n />\r\n </mat-form-field>\r\n\r\n <!-- lineHeightOffset -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>ln h-offset</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"lineHeightOffset\"\r\n placeholder=\"ln h-offset\"\r\n />\r\n </mat-form-field>\r\n\r\n <!-- charSpacingOffset -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>char spacing</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"charSpacingOffset\"\r\n placeholder=\"char spacing\"\r\n />\r\n </mat-form-field>\r\n\r\n <!-- spcWidthOffset -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>spc w-offset</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"spcWidthOffset\"\r\n placeholder=\"spc w-offset\"\r\n />\r\n </mat-form-field>\r\n <!-- minLineHeights -->\r\n <div class=\"boxed\">\r\n <gve-ln-heights-editor\r\n [lineCount]=\"lineCount\"\r\n [heights]=\"lnHeights.value\"\r\n (heightsChange)=\"onHeightsChange($event)\"\r\n />\r\n </div>\r\n </div>\r\n <!-- textStyle -->\r\n <div>\r\n <mat-form-field class=\"long-text\" appearance=\"outline\">\r\n <mat-label>text style</mat-label>\r\n <textarea\r\n matInput\r\n [formControl]=\"textStyle\"\r\n placeholder=\"text style\"\r\n ></textarea>\r\n </mat-form-field>\r\n </div>\r\n </fieldset>\r\n </mat-expansion-panel>\r\n </mat-tab>\r\n\r\n <!-- operations -->\r\n <mat-tab>\r\n <ng-template mat-tab-label>\r\n <mat-icon>edit</mat-icon> <span class=\"label\">operations</span>\r\n </ng-template>\r\n\r\n <div id=\"snapshot-container\">\r\n <div id=\"general\">\r\n <div class=\"form-row\">\r\n <!-- width -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>width</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n min=\"0\"\r\n [formControl]=\"width\"\r\n placeholder=\"width\"\r\n />\r\n </mat-form-field>\r\n\r\n <!-- height -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>height</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n min=\"0\"\r\n [formControl]=\"height\"\r\n placeholder=\"height\"\r\n />\r\n </mat-form-field>\r\n\r\n <div class=\"form-row right\">\r\n <!-- remove ops -->\r\n <button\r\n type=\"button\"\r\n class=\"mat-warn\"\r\n mat-flat-button\r\n matTooltip=\"Remove all the operations\"\r\n [disabled]=\"!operations.value.length || busy\"\r\n (click)=\"clearOperations()\"\r\n >\r\n <mat-icon>delete_forever</mat-icon> clear\r\n </button>\r\n\r\n <!-- batch add ops -->\r\n <button\r\n type=\"button\"\r\n mat-flat-button\r\n matTooltip=\"Add a batch of operations\"\r\n class=\"mat-primary\"\r\n [disabled]=\"busy\"\r\n (click)=\"parseOperations()\"\r\n >\r\n <mat-icon>post_add</mat-icon> batch\r\n </button>\r\n\r\n <!-- add op -->\r\n <button\r\n type=\"button\"\r\n mat-flat-button\r\n class=\"mat-primary\"\r\n [disabled]=\"!baseText.value\"\r\n (click)=\"editNewOperation()\"\r\n >\r\n <mat-icon>add_circle</mat-icon> operation\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- ops -->\r\n <div id=\"ops\">\r\n <!-- operations list -->\r\n @if (operations.value.length) {\r\n <table id=\"list\">\r\n <thead>\r\n <tr>\r\n <th></th>\r\n <th>ID</th>\r\n <th>type</th>\r\n <th>at</th>\r\n <th>run</th>\r\n <th>value</th>\r\n <th>itag</th>\r\n <th>otag</th>\r\n <th>gid</th>\r\n <th>feats</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (operation of operations.value; track operation.id; let\r\n index=$index) {\r\n <tr\r\n [ngClass]=\"{ selected: operation.id === resultOperationId }\"\r\n [ngClass]=\"{ edited: operation.id === editedOp?.id }\"\r\n >\r\n <td class=\"fit-width\">\r\n <!-- edit -->\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n class=\"mat-primary\"\r\n (click)=\"editOperation(index)\"\r\n matTooltip=\"Edit operation\"\r\n >\r\n <mat-icon>edit</mat-icon>\r\n </button>\r\n <!-- delete -->\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n class=\"mat-warn\"\r\n (click)=\"deleteOperation(index)\"\r\n matTooltip=\"Delete operation\"\r\n >\r\n <mat-icon class=\"mat-warn\">delete</mat-icon>\r\n </button>\r\n <!-- run to -->\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n class=\"mat-accent\"\r\n (click)=\"runTo(index)\"\r\n matTooltip=\"Run to this operation\"\r\n >\r\n <mat-icon class=\"mat-accent\">subscriptions</mat-icon>\r\n </button>\r\n </td>\r\n <td>{{ operation.id }}</td>\r\n <td>{{ operation.type | flatLookup : opTypeMap }}</td>\r\n <td>{{ operation.at }}</td>\r\n <td>{{ operation.run }}</td>\r\n <td>{{ operation.value }}</td>\r\n <td>{{ operation.inputTag }}</td>\r\n <td>{{ operation.outputTag }}</td>\r\n <td>{{ operation.groupId }}</td>\r\n <td>{{ operation.features?.length }}</td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n }\r\n\r\n <!-- operation editor -->\r\n <mat-expansion-panel [expanded]=\"editedOp\" [disabled]=\"!editedOp\">\r\n <mat-expansion-panel-header>\r\n <mat-panel-title>operation {{ editedOp?.id }}</mat-panel-title>\r\n </mat-expansion-panel-header>\r\n <fieldset>\r\n <gve-chain-operation-editor\r\n [hidePreview]=\"editedOpIndex === -1\"\r\n [operation]=\"editedOp\"\r\n (operationCancel)=\"onOperationCancel()\"\r\n (operationChange)=\"onOperationChange($event)\"\r\n (operationPreview)=\"onOperationPreview($event)\"\r\n />\r\n </fieldset>\r\n </mat-expansion-panel>\r\n\r\n <!-- opStyle -->\r\n <div id=\"opStyle\">\r\n <mat-form-field class=\"long-text\" appearance=\"outline\">\r\n <mat-label>operations style</mat-label>\r\n <textarea\r\n matInput\r\n [formControl]=\"opStyle\"\r\n placeholder=\"operations style\"\r\n ></textarea>\r\n </mat-form-field>\r\n </div>\r\n </div>\r\n </div>\r\n </mat-tab>\r\n\r\n <!-- image -->\r\n <mat-tab>\r\n <ng-template mat-tab-label>\r\n <mat-icon>image</mat-icon> <span class=\"label\">image</span>\r\n </ng-template>\r\n\r\n <div id=\"image\">\r\n <div id=\"image-ctl\">\r\n <!-- url -->\r\n <mat-form-field class=\"long-text\">\r\n <mat-label>URL</mat-label>\r\n <input matInput [formControl]=\"imageUrl\" />\r\n </mat-form-field>\r\n <div class=\"form-row\">\r\n <!-- x -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>X</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"imageX\"\r\n placeholder=\"X\"\r\n />\r\n </mat-form-field>\r\n <!-- y -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>Y</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"imageY\"\r\n placeholder=\"Y\"\r\n />\r\n </mat-form-field>\r\n </div>\r\n <div class=\"form-row\">\r\n <!-- width -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>width</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n min=\"0\"\r\n [formControl]=\"imageWidth\"\r\n />\r\n </mat-form-field>\r\n <!-- height -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>height</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n min=\"0\"\r\n [formControl]=\"imageHeight\"\r\n />\r\n </mat-form-field>\r\n </div>\r\n <!-- defs -->\r\n <div>\r\n <mat-form-field class=\"long-text\">\r\n <mat-label>defs</mat-label>\r\n <textarea matInput [formControl]=\"defs\" rows=\"3\"></textarea>\r\n </mat-form-field>\r\n </div>\r\n </div>\r\n <div id=\"image-view\">\r\n @if (imageUrl.value) {\r\n <img alt=\"background\" [src]=\"imageUrl.value\" width=\"600\" />\r\n }\r\n </div>\r\n </div>\r\n </mat-tab>\r\n\r\n <!-- timelines -->\r\n <mat-tab>\r\n <ng-template mat-tab-label>\r\n <mat-icon>animation</mat-icon> <span class=\"label\">animation</span>\r\n </ng-template>\r\n\r\n <gve-animation-timeline-set\r\n [tags]=\"opTags\"\r\n [elementIds]=\"opElementIds\"\r\n [timelines]=\"timelines.value\"\r\n (timelinesChange)=\"onTimelinesChange($event)\"\r\n />\r\n </mat-tab>\r\n </mat-tab-group>\r\n\r\n <!-- progress -->\r\n <div>\r\n <mat-progress-bar mode=\"indeterminate\" *ngIf=\"busy\" />\r\n </div>\r\n\r\n <!-- result -->\r\n @if (result) {\r\n <fieldset id=\"result\">\r\n <legend>result</legend>\r\n <gve-chain-result-view\r\n [result]=\"result\"\r\n [initialStepIndex]=\"initialStepIndex\"\r\n (stepPick)=\"onStepPick($event)\"\r\n />\r\n </fieldset>\r\n }\r\n\r\n <!-- snapshot view -->\r\n <fieldset id=\"preview\">\r\n <legend class=\"button-row\">\r\n <span>{{ viewTitle }}</span>\r\n </legend>\r\n <!-- snapshot view -->\r\n <gve-snapshot-view\r\n #snapshotView\r\n [debug]=\"debug\"\r\n [data]=\"viewData\"\r\n (snapshotRender)=\"onSnapshotRender($any($event))\"\r\n (visualEvent)=\"onVisualEvent($any($event))\"\r\n />\r\n <div class=\"button-row\">\r\n <!-- toggle ruler -->\r\n <mat-button-toggle\r\n type=\"button\"\r\n mat-icon-button\r\n color=\"primary\"\r\n matTooltip=\"Toggle rulers\"\r\n [checked]=\"rulers\"\r\n (change)=\"toggleRulers()\"\r\n >\r\n <mat-icon class=\"mat-primary\">straighten</mat-icon>\r\n </mat-button-toggle>\r\n <!-- run to last -->\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n class=\"primary\"\r\n matTooltip=\"Run all operations\"\r\n [disabled]=\"!operations.value.length\"\r\n (click)=\"runToLast()\"\r\n >\r\n <mat-icon>play_circle</mat-icon>\r\n </button>\r\n </div>\r\n @if (visualInfo) {\r\n <div id=\"visual-info\">{{ visualInfo }}</div>\r\n }\r\n </fieldset>\r\n\r\n <!--buttons -->\r\n <div class=\"form-row-center\">\r\n <button\r\n type=\"button\"\r\n class=\"mat-warn\"\r\n mat-flat-button\r\n matTooltip=\"Discard changes\"\r\n (click)=\"close()\"\r\n >\r\n <mat-icon>clear</mat-icon>\r\n close\r\n </button>\r\n @if (!noSave) {\r\n <button\r\n type=\"submit\"\r\n class=\"mat-primary\"\r\n mat-flat-button\r\n matTooltip=\"Save changes\"\r\n [disabled]=\"form.invalid\"\r\n >\r\n <mat-icon>check_circle</mat-icon>\r\n save\r\n </button>\r\n }\r\n </div>\r\n</form>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row-center{display:flex;gap:8px;align-items:center;justify-content:center;flex-wrap:wrap}.form-row,.form-row-center *{flex:0 0 auto}.form-row .right{margin-left:auto}.button-row{display:flex;align-items:center;flex-wrap:wrap}.button-row *{flex:0 0 auto}.long-text{width:100%;max-width:800px}#text-range{margin:8px;border:1px solid silver;border-radius:6px;padding:6px}mat-expansion-panel{margin:8px 0}div#visual-info{font-size:95%;color:#909090;margin:8px}#list{margin:8px 0}#opStyle{margin-top:8px}table{width:100%;border-collapse:collapse}th{color:#909090;font-weight:400;text-align:left;background-color:#e1e0e0}th,td{padding:4px;border-bottom:1px solid silver}tbody tr:nth-child(2n){background-color:#e8e8e8}td.fit-width{width:1px;white-space:nowrap}tr.selected{background-color:#c8d9eb}tr.edited{background-color:#f6f6e4}fieldset{border:1px solid silver;border-radius:6px;padding:8px;margin:8px 0}legend{color:#909090}.error{color:red}.input-nr{width:6em}.full-width{width:100%}.code{font-family:Courier New,Courier,monospace}.boxed{border:1px solid silver;border-radius:6px;padding:8px;margin:8px 0}span.label{margin-left:8px}gve-animation-timeline-set{margin:8px 0}div#image{display:grid;gap:8px;grid-template-rows:auto;grid-template-columns:1fr auto;grid-template-areas:\"image-ctl image-view\"}div#image-ctl{grid-area:image-ctl}div#image-view{grid-area:image-view}@media only screen and (max-width: 959px){div#image{grid-template-columns:1fr;grid-template-areas:\"image-ctl\" \"image-view\"}}\n"] }]
3405
- }], ctorParameters: () => [{ type: i1.FormBuilder }, { type: GveApiService }, { type: i3$1.MatDialog }, { type: i2$2.DialogService }, { type: i5$2.MatSnackBar }], propDecorators: { snapshotView: [{
3764
+ FlatLookupPipe,
3765
+ ChainViewComponent,
3766
+ ], schemas: [CUSTOM_ELEMENTS_SCHEMA], template: "<form [formGroup]=\"form\" (submit)=\"save()\">\r\n <mat-tab-group>\r\n <!-- text -->\r\n <mat-tab>\r\n <ng-template mat-tab-label>\r\n <mat-icon>article</mat-icon> <span class=\"label\">text</span>\r\n </ng-template>\r\n <mat-expansion-panel>\r\n <mat-expansion-panel-header>\r\n <mat-panel-title> base text </mat-panel-title>\r\n </mat-expansion-panel-header>\r\n <!-- base text -->\r\n <div class=\"form-row\">\r\n <button\r\n type=\"button\"\r\n mat-flat-button\r\n color=\"mat-warn\"\r\n matTooltip=\"Enter base text\"\r\n (click)=\"inputBaseText()\"\r\n [disabled]=\"busy\"\r\n >\r\n <mat-icon>edit</mat-icon> base text\r\n </button>\r\n\r\n @if (textRange) {\r\n <span id=\"text-range\">{{ textRange.at }}\u00D7{{ textRange.run }}</span>\r\n }\r\n </div>\r\n <!-- base text metadata -->\r\n <fieldset>\r\n <div class=\"form-row\">\r\n <!-- offsetX -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>X offset</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"offsetX\"\r\n placeholder=\"X offset\"\r\n />\r\n </mat-form-field>\r\n\r\n <!-- offsetY -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>Y offset</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"offsetY\"\r\n placeholder=\"Y offset\"\r\n />\r\n </mat-form-field>\r\n\r\n <!-- lineHeightOffset -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>ln h-offset</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"lineHeightOffset\"\r\n placeholder=\"ln h-offset\"\r\n />\r\n </mat-form-field>\r\n\r\n <!-- charSpacingOffset -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>char spacing</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"charSpacingOffset\"\r\n placeholder=\"char spacing\"\r\n />\r\n </mat-form-field>\r\n\r\n <!-- spcWidthOffset -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>spc w-offset</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"spcWidthOffset\"\r\n placeholder=\"spc w-offset\"\r\n />\r\n </mat-form-field>\r\n <!-- minLineHeights -->\r\n <div class=\"boxed\">\r\n <gve-ln-heights-editor\r\n [lineCount]=\"lineCount\"\r\n [heights]=\"lnHeights.value || undefined\"\r\n (heightsChange)=\"onHeightsChange($event)\"\r\n />\r\n </div>\r\n </div>\r\n <!-- textStyle -->\r\n <div>\r\n <mat-form-field class=\"long-text\" appearance=\"outline\">\r\n <mat-label>text style</mat-label>\r\n <textarea\r\n matInput\r\n [formControl]=\"textStyle\"\r\n placeholder=\"text style\"\r\n ></textarea>\r\n </mat-form-field>\r\n </div>\r\n </fieldset>\r\n <!-- text characters -->\r\n <gve-base-text-view\r\n [text]=\"baseText.value\"\r\n [hasLineNumber]=\"true\"\r\n (rangePick)=\"onRangePick($event)\"\r\n />\r\n </mat-expansion-panel>\r\n </mat-tab>\r\n\r\n <!-- operations -->\r\n <mat-tab>\r\n <ng-template mat-tab-label>\r\n <mat-icon>edit</mat-icon> <span class=\"label\">operations</span>\r\n </ng-template>\r\n\r\n <div id=\"snapshot-container\">\r\n <div id=\"general\">\r\n <div class=\"form-row\">\r\n <!-- width -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>width</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n min=\"0\"\r\n [formControl]=\"width\"\r\n placeholder=\"width\"\r\n />\r\n </mat-form-field>\r\n\r\n <!-- height -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>height</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n min=\"0\"\r\n [formControl]=\"height\"\r\n placeholder=\"height\"\r\n />\r\n </mat-form-field>\r\n\r\n <!-- chain -->\r\n <mat-checkbox [formControl]=\"showChain\">show chain</mat-checkbox>\r\n\r\n <div class=\"form-row right\">\r\n <!-- remove ops -->\r\n <button\r\n type=\"button\"\r\n class=\"mat-warn\"\r\n mat-flat-button\r\n matTooltip=\"Remove all the operations\"\r\n [disabled]=\"!operations.value.length || busy\"\r\n (click)=\"clearOperations()\"\r\n >\r\n <mat-icon>delete_forever</mat-icon> clear\r\n </button>\r\n\r\n <!-- batch add ops -->\r\n <button\r\n type=\"button\"\r\n mat-flat-button\r\n matTooltip=\"Add a batch of operations\"\r\n class=\"mat-primary\"\r\n [disabled]=\"busy\"\r\n (click)=\"parseOperations()\"\r\n >\r\n <mat-icon>post_add</mat-icon> batch\r\n </button>\r\n\r\n <!-- add op -->\r\n <button\r\n type=\"button\"\r\n mat-flat-button\r\n class=\"mat-primary\"\r\n [disabled]=\"!baseText.value\"\r\n (click)=\"editNewOperation()\"\r\n >\r\n <mat-icon>add_circle</mat-icon> operation\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- ops -->\r\n <div id=\"ops\">\r\n <!-- operations list -->\r\n @if (operations.value.length) {\r\n <table id=\"list\">\r\n <thead>\r\n <tr>\r\n <th>nr.</th>\r\n <th></th>\r\n <th>ID</th>\r\n <th>type</th>\r\n <th>at</th>\r\n <th>run</th>\r\n <th>value</th>\r\n <th>itag</th>\r\n <th>otag</th>\r\n <th>gid</th>\r\n <th>feats</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (operation of operations.value; track operation.id; let\r\n index=$index) {\r\n <tr\r\n [ngClass]=\"{ selected: operation.id === resultOperationId }\"\r\n [ngClass]=\"{ edited: operation.id === editedOp?.id }\"\r\n >\r\n <td class=\"fit-width\">{{ index + 1 }}.</td>\r\n <td class=\"fit-width\">\r\n <!-- edit -->\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n class=\"mat-primary\"\r\n (click)=\"editOperation(index)\"\r\n matTooltip=\"Edit operation\"\r\n >\r\n <mat-icon>edit</mat-icon>\r\n </button>\r\n <!-- delete -->\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n class=\"mat-warn\"\r\n (click)=\"deleteOperation(index)\"\r\n matTooltip=\"Delete operation\"\r\n >\r\n <mat-icon class=\"mat-warn\">delete</mat-icon>\r\n </button>\r\n <!-- run to -->\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n class=\"mat-accent\"\r\n (click)=\"runTo(index)\"\r\n matTooltip=\"Run to this operation\"\r\n >\r\n <mat-icon class=\"mat-accent\">subscriptions</mat-icon>\r\n </button>\r\n </td>\r\n <td>{{ operation.id }}</td>\r\n <td>{{ operation.type | flatLookup : opTypeMap }}</td>\r\n <td>{{ operation.at }}</td>\r\n <td>{{ operation.run }}</td>\r\n <td>{{ operation.value }}</td>\r\n <td>{{ operation.inputTag }}</td>\r\n <td>{{ operation.outputTag }}</td>\r\n <td>{{ operation.groupId }}</td>\r\n <td>{{ operation.features?.length }}</td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n }\r\n\r\n <!-- operation editor -->\r\n <mat-expansion-panel [expanded]=\"editedOp\" [disabled]=\"!editedOp\">\r\n <mat-expansion-panel-header>\r\n <mat-panel-title>operation {{ editedOp?.id }}</mat-panel-title>\r\n </mat-expansion-panel-header>\r\n <fieldset>\r\n <gve-chain-operation-editor\r\n [hidePreview]=\"editedOpIndex === -1\"\r\n [operation]=\"editedOp\"\r\n (operationCancel)=\"onOperationCancel()\"\r\n (operationChange)=\"onOperationChange($event)\"\r\n (operationPreview)=\"onOperationPreview($event)\"\r\n />\r\n </fieldset>\r\n </mat-expansion-panel>\r\n\r\n <!-- opStyle -->\r\n <div id=\"opStyle\">\r\n <mat-form-field class=\"long-text\" appearance=\"outline\">\r\n <mat-label>operations style</mat-label>\r\n <textarea\r\n matInput\r\n [formControl]=\"opStyle\"\r\n placeholder=\"operations style\"\r\n ></textarea>\r\n </mat-form-field>\r\n </div>\r\n </div>\r\n </div>\r\n </mat-tab>\r\n\r\n <!-- image -->\r\n <mat-tab>\r\n <ng-template mat-tab-label>\r\n <mat-icon>image</mat-icon> <span class=\"label\">image</span>\r\n </ng-template>\r\n\r\n <div id=\"image\">\r\n <div id=\"image-ctl\">\r\n <!-- url -->\r\n <mat-form-field class=\"long-text\">\r\n <mat-label>URL</mat-label>\r\n <input matInput [formControl]=\"imageUrl\" />\r\n </mat-form-field>\r\n <div class=\"form-row\">\r\n <!-- x -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>X</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"imageX\"\r\n placeholder=\"X\"\r\n />\r\n </mat-form-field>\r\n <!-- y -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>Y</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [formControl]=\"imageY\"\r\n placeholder=\"Y\"\r\n />\r\n </mat-form-field>\r\n <!-- width -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>width</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n min=\"0\"\r\n [formControl]=\"imageWidth\"\r\n />\r\n </mat-form-field>\r\n <!-- height -->\r\n <mat-form-field class=\"input-nr\">\r\n <mat-label>height</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n min=\"0\"\r\n [formControl]=\"imageHeight\"\r\n />\r\n </mat-form-field>\r\n <!-- reset -->\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Reset image metadata\"\r\n (click)=\"resetImgMetadata()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n </div>\r\n <div>\r\n <!-- image opacity -->\r\n <mat-slider\r\n min=\"0\"\r\n max=\"1\"\r\n step=\"0.01\"\r\n thumbLabel\r\n color=\"primary\"\r\n class=\"mat-primary\"\r\n matTooltip=\"Image opacity\"\r\n >\r\n <input\r\n matSliderThumb\r\n [value]=\"imageOpacity.value\"\r\n (valueChange)=\"onImageOpacityChange($event)\"\r\n />\r\n </mat-slider>\r\n <span id=\"opacity-value\">{{\r\n imageOpacity.value | number : \"1.2-2\"\r\n }}</span>\r\n </div>\r\n <!-- defs -->\r\n <div>\r\n <mat-form-field class=\"long-text\">\r\n <mat-label>defs</mat-label>\r\n <textarea matInput [formControl]=\"defs\" rows=\"3\"></textarea>\r\n </mat-form-field>\r\n </div>\r\n </div>\r\n <div id=\"image-view\">\r\n @if (imageUrl.value) {\r\n <img\r\n #imgElement\r\n alt=\"background\"\r\n [src]=\"imageUrl.value\"\r\n width=\"600\"\r\n (load)=\"onImageLoad(imgElement)\"\r\n />\r\n }\r\n </div>\r\n </div>\r\n </mat-tab>\r\n\r\n <!-- timelines -->\r\n <mat-tab>\r\n <ng-template mat-tab-label>\r\n <mat-icon>animation</mat-icon> <span class=\"label\">animation</span>\r\n </ng-template>\r\n\r\n <gve-animation-timeline-set\r\n [tags]=\"opTags\"\r\n [elementIds]=\"opElementIds\"\r\n [timelines]=\"timelines.value\"\r\n (timelinesChange)=\"onTimelinesChange($event)\"\r\n />\r\n </mat-tab>\r\n </mat-tab-group>\r\n\r\n <!-- progress -->\r\n <div>\r\n <mat-progress-bar mode=\"indeterminate\" *ngIf=\"busy\" />\r\n </div>\r\n\r\n <!-- result -->\r\n @if (result) {\r\n <fieldset id=\"result\">\r\n <legend>result</legend>\r\n <gve-chain-result-view\r\n [result]=\"result\"\r\n [initialStepIndex]=\"initialStepIndex\"\r\n (stepPick)=\"onStepPick($event)\"\r\n />\r\n </fieldset>\r\n }\r\n\r\n <!-- snapshot view -->\r\n <fieldset id=\"preview\">\r\n <legend class=\"button-row\">\r\n <span>{{ viewTitle }}</span>\r\n </legend>\r\n <!-- snapshot view -->\r\n <gve-snapshot-view\r\n #snapshotView\r\n [debug]=\"debug()\"\r\n [data]=\"viewData\"\r\n (snapshotRender)=\"onSnapshotRender($any($event))\"\r\n (visualEvent)=\"onVisualEvent($any($event))\"\r\n />\r\n <div class=\"button-row\">\r\n <!-- toggle ruler -->\r\n <mat-button-toggle\r\n type=\"button\"\r\n mat-icon-button\r\n color=\"primary\"\r\n matTooltip=\"Toggle rulers\"\r\n [checked]=\"rulers\"\r\n (change)=\"toggleRulers()\"\r\n >\r\n <mat-icon class=\"mat-primary\">straighten</mat-icon>\r\n </mat-button-toggle>\r\n <!-- run to last -->\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n class=\"primary\"\r\n matTooltip=\"Refresh preview\"\r\n (click)=\"runToLast()\"\r\n >\r\n <mat-icon>refresh</mat-icon>\r\n </button>\r\n </div>\r\n @if (visualInfo) {\r\n <div id=\"visual-info\">{{ visualInfo }}</div>\r\n }\r\n </fieldset>\r\n\r\n <!-- chain view -->\r\n @if (chain) {\r\n <div id=\"chain-view\">\r\n <gve-chain-view [chain]=\"chain\" />\r\n </div>\r\n }\r\n\r\n <!--buttons -->\r\n <div class=\"form-row-center\">\r\n <button\r\n type=\"button\"\r\n class=\"mat-warn\"\r\n mat-flat-button\r\n matTooltip=\"Discard changes\"\r\n (click)=\"close()\"\r\n >\r\n <mat-icon>clear</mat-icon>\r\n close\r\n </button>\r\n @if (!noSave()) {\r\n <button\r\n type=\"submit\"\r\n class=\"mat-primary\"\r\n mat-flat-button\r\n matTooltip=\"Save changes\"\r\n [disabled]=\"form.invalid\"\r\n >\r\n <mat-icon>check_circle</mat-icon>\r\n save\r\n </button>\r\n }\r\n </div>\r\n</form>\r\n", styles: [".form-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.form-row-center{display:flex;gap:8px;align-items:center;justify-content:center;flex-wrap:wrap}.form-row,.form-row-center *{flex:0 0 auto}.form-row .right{margin-left:auto}.button-row{display:flex;align-items:center;flex-wrap:wrap}.button-row *{flex:0 0 auto}.long-text{width:100%;max-width:800px}#text-range{margin:8px;border:1px solid silver;border-radius:6px;padding:6px}mat-expansion-panel{margin:8px 0}div#visual-info{font-size:95%;color:#909090;margin:8px}#list{margin:8px 0}#opStyle{margin-top:8px}table{width:100%;border-collapse:collapse}th{color:#909090;font-weight:400;text-align:left;background-color:#e1e0e0}th,td{padding:4px;border-bottom:1px solid silver}tbody tr:nth-child(2n){background-color:#e8e8e8}td.fit-width{width:1px;white-space:nowrap}tr.selected{background-color:#c8d9eb}tr.edited{background-color:#f6f6e4}fieldset{border:1px solid silver;border-radius:6px;padding:8px;margin:8px 0}legend{color:#909090}.error{color:red}.input-nr{width:6em}.full-width{width:100%}.code{font-family:Courier New,Courier,monospace}.boxed{border:1px solid silver;border-radius:6px;padding:8px;margin:8px 0}span.label{margin-left:8px}gve-animation-timeline-set{margin:8px 0}div#image{display:grid;gap:8px;grid-template-rows:auto;grid-template-columns:1fr auto;grid-template-areas:\"image-ctl image-view\"}div#image-ctl{grid-area:image-ctl}div#image-view{grid-area:image-view}div#chain-view{margin-bottom:8px}@media only screen and (max-width: 959px){div#image{grid-template-columns:1fr;grid-template-areas:\"image-ctl\" \"image-view\"}}\n"] }]
3767
+ }], ctorParameters: () => [{ type: i1.FormBuilder }, { type: GveApiService }, { type: i3$1.MatDialog }, { type: i2$1.DialogService }, { type: i4$2.MatSnackBar }], propDecorators: { snapshotView: [{
3406
3768
  type: ViewChild,
3407
3769
  args: ['snapshotView', { static: false }]
3408
- }], snapshot: [{
3409
- type: Input
3410
- }], batchOps: [{
3411
- type: Input
3412
- }], noSave: [{
3413
- type: Input
3414
- }], debug: [{
3415
- type: Input
3416
- }], snapshotChange: [{
3417
- type: Output
3418
- }], snapshotCancel: [{
3419
- type: Output
3420
3770
  }] } });
3421
3771
 
3422
3772
  /*
@@ -3427,5 +3777,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImpor
3427
3777
  * Generated bundle index. Do not edit.
3428
3778
  */
3429
3779
 
3430
- export { AnimationTimelineComponent, AnimationTweenComponent, BaseTextCharComponent, BaseTextEditorComponent, BaseTextViewComponent, BatchOperationEditorComponent, ChainOperationEditorComponent, ChainResultViewComponent, FeatureEditorComponent, FeatureSetEditorComponent, FeatureSetViewComponent, GveApiService, LnHeightsEditorComponent, OperationSourceEditorComponent, SettingsService, SimpleTreeComponent, SnapshotEditorComponent, StepsMapComponent };
3780
+ export { AnimationTimelineComponent, AnimationTweenComponent, BaseTextCharComponent, BaseTextEditorComponent, BaseTextViewComponent, BatchOperationEditorComponent, ChainOperationEditorComponent, ChainResultViewComponent, ChainViewComponent, FeatureEditorComponent, FeatureSetEditorComponent, FeatureSetViewComponent, GveApiService, GveGraphvizService, LnHeightsEditorComponent, OperationSourceEditorComponent, SettingsService, SnapshotEditorComponent, SnapshotTextEditorComponent, StepsMapComponent };
3431
3781
  //# sourceMappingURL=myrmidon-gve-core.mjs.map