pict-section-flow 0.0.18 → 0.0.19

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.
@@ -21,72 +21,152 @@ const _DefaultConfiguration =
21
21
  [
22
22
  {
23
23
  Hash: 'Flow-Toolbar-Template',
24
+ // Inline onclick handlers route to the toolbar via the FlowView
25
+ // (the flow toolbar is reachable as ._ToolbarView from the
26
+ // registered FlowView). Each button is responsible for its own
27
+ // action — no event delegation.
24
28
  Template: /*html*/`
25
29
  <div class="pict-flow-toolbar" id="Flow-Toolbar-Bar-{~D:Record.FlowViewIdentifier~}">
26
30
  <div class="pict-flow-toolbar-group">
27
- <button class="pict-flow-toolbar-btn" data-flow-action="add-node" id="Flow-Toolbar-AddNode-{~D:Record.FlowViewIdentifier~}" title="Add Node">
31
+ <button class="pict-flow-toolbar-btn" data-flow-action="add-node" id="Flow-Toolbar-AddNode-{~D:Record.FlowViewIdentifier~}" title="Add Node"
32
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('add-node')">
28
33
  <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-plus-{~D:Record.FlowViewIdentifier~}"></span>
29
34
  <span class="pict-flow-toolbar-btn-text">Node</span>
30
35
  </button>
31
- <button class="pict-flow-toolbar-btn" data-flow-action="cards-popup" id="Flow-Toolbar-Cards-{~D:Record.FlowViewIdentifier~}" title="Card Palette">
36
+ <button class="pict-flow-toolbar-btn" data-flow-action="cards-popup" id="Flow-Toolbar-Cards-{~D:Record.FlowViewIdentifier~}" title="Card Palette"
37
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('cards-popup')">
32
38
  <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-cards-{~D:Record.FlowViewIdentifier~}"></span>
33
39
  <span class="pict-flow-toolbar-btn-text">Cards</span>
34
40
  <span class="pict-flow-toolbar-btn-chevron" id="Flow-Toolbar-CardsChevron-{~D:Record.FlowViewIdentifier~}"></span>
35
41
  </button>
36
- <button class="pict-flow-toolbar-btn" data-flow-action="delete-selected" title="Delete Node">
42
+ <button class="pict-flow-toolbar-btn" data-flow-action="delete-selected" title="Delete Node"
43
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('delete-selected')">
37
44
  <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-trash-{~D:Record.FlowViewIdentifier~}"></span>
38
45
  </button>
39
46
  </div>
40
47
  <div class="pict-flow-toolbar-group">
41
- <button class="pict-flow-toolbar-btn" data-flow-action="layout-popup" id="Flow-Toolbar-Layout-{~D:Record.FlowViewIdentifier~}" title="Manage saved layouts">
48
+ <button class="pict-flow-toolbar-btn" data-flow-action="layout-popup" id="Flow-Toolbar-Layout-{~D:Record.FlowViewIdentifier~}" title="Manage saved layouts"
49
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('layout-popup')">
42
50
  <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-layout-{~D:Record.FlowViewIdentifier~}"></span>
43
51
  <span class="pict-flow-toolbar-btn-text">Layouts</span>
44
52
  <span class="pict-flow-toolbar-btn-chevron" id="Flow-Toolbar-LayoutChevron-{~D:Record.FlowViewIdentifier~}"></span>
45
53
  </button>
46
54
  <div class="pict-flow-toolbar-btn-split" id="Flow-Toolbar-Auto-{~D:Record.FlowViewIdentifier~}">
47
- <button class="pict-flow-toolbar-btn pict-flow-toolbar-btn-split-main" data-flow-action="apply-current-layout" title="Apply current layout algorithm">
55
+ <button class="pict-flow-toolbar-btn pict-flow-toolbar-btn-split-main" data-flow-action="apply-current-layout" title="Apply current layout algorithm"
56
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('apply-current-layout')">
48
57
  <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-auto-{~D:Record.FlowViewIdentifier~}"></span>
49
58
  <span class="pict-flow-toolbar-btn-text">Auto</span>
50
59
  </button>
51
- <button class="pict-flow-toolbar-btn pict-flow-toolbar-btn-split-chevron" data-flow-action="layout-algorithm-popup" title="Choose layout algorithm">
60
+ <button class="pict-flow-toolbar-btn pict-flow-toolbar-btn-split-chevron" data-flow-action="layout-algorithm-popup" title="Choose layout algorithm"
61
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('layout-algorithm-popup')">
52
62
  <span class="pict-flow-toolbar-btn-chevron" id="Flow-Toolbar-AutoChevron-{~D:Record.FlowViewIdentifier~}"></span>
53
63
  </button>
54
64
  </div>
55
65
  </div>
56
66
  <div class="pict-flow-toolbar-group">
57
- <button class="pict-flow-toolbar-btn" data-flow-action="zoom-in" title="Zoom In">
67
+ <button class="pict-flow-toolbar-btn" data-flow-action="zoom-in" title="Zoom In"
68
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('zoom-in')">
58
69
  <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-zoom-in-{~D:Record.FlowViewIdentifier~}"></span>
59
70
  </button>
60
- <button class="pict-flow-toolbar-btn" data-flow-action="zoom-out" title="Zoom Out">
71
+ <button class="pict-flow-toolbar-btn" data-flow-action="zoom-out" title="Zoom Out"
72
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('zoom-out')">
61
73
  <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-zoom-out-{~D:Record.FlowViewIdentifier~}"></span>
62
74
  </button>
63
- <button class="pict-flow-toolbar-btn" data-flow-action="zoom-fit" title="Fit to View">
75
+ <button class="pict-flow-toolbar-btn" data-flow-action="zoom-fit" title="Fit to View"
76
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('zoom-fit')">
64
77
  <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-zoom-fit-{~D:Record.FlowViewIdentifier~}"></span>
65
78
  </button>
66
79
  </div>
67
80
  <div class="pict-flow-toolbar-group pict-flow-toolbar-right">
68
- <button class="pict-flow-toolbar-btn" data-flow-action="settings-popup" id="Flow-Toolbar-Settings-{~D:Record.FlowViewIdentifier~}" title="Theme Settings">
81
+ <button class="pict-flow-toolbar-btn" data-flow-action="settings-popup" id="Flow-Toolbar-Settings-{~D:Record.FlowViewIdentifier~}" title="Theme Settings"
82
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('settings-popup')">
69
83
  <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-settings-{~D:Record.FlowViewIdentifier~}"></span>
70
84
  </button>
71
- <button class="pict-flow-toolbar-btn" data-flow-action="fullscreen" id="Flow-Toolbar-Fullscreen-{~D:Record.FlowViewIdentifier~}" title="Toggle Fullscreen">
85
+ <button class="pict-flow-toolbar-btn" data-flow-action="fullscreen" id="Flow-Toolbar-Fullscreen-{~D:Record.FlowViewIdentifier~}" title="Toggle Fullscreen"
86
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('fullscreen')">
72
87
  <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Fullscreen-Icon-{~D:Record.FlowViewIdentifier~}"></span>
73
88
  </button>
74
- <button class="pict-flow-toolbar-btn" data-flow-action="toggle-floating" title="Float">
89
+ <button class="pict-flow-toolbar-btn" data-flow-action="toggle-floating" title="Float"
90
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('toggle-floating')">
75
91
  <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-grip-{~D:Record.FlowViewIdentifier~}"></span>
76
92
  </button>
77
- <button class="pict-flow-toolbar-btn" data-flow-action="collapse-toolbar" title="Collapse Toolbar">
93
+ <button class="pict-flow-toolbar-btn" data-flow-action="collapse-toolbar" title="Collapse Toolbar"
94
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('collapse-toolbar')">
78
95
  <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-collapse-{~D:Record.FlowViewIdentifier~}"></span>
79
96
  </button>
80
97
  </div>
81
98
  </div>
82
99
  <div class="pict-flow-toolbar-collapsed" id="Flow-Toolbar-Collapsed-{~D:Record.FlowViewIdentifier~}">
83
- <button class="pict-flow-toolbar-expand-btn" data-flow-action="expand-toolbar" title="Expand Toolbar" id="Flow-Toolbar-ExpandBtn-{~D:Record.FlowViewIdentifier~}">
100
+ <button class="pict-flow-toolbar-expand-btn" data-flow-action="expand-toolbar" title="Expand Toolbar" id="Flow-Toolbar-ExpandBtn-{~D:Record.FlowViewIdentifier~}"
101
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('expand-toolbar')">
84
102
  <span id="Flow-Toolbar-Icon-expand-{~D:Record.FlowViewIdentifier~}"></span>
85
103
  </button>
86
104
  </div>
87
105
  <div class="pict-flow-toolbar-popup-anchor" id="Flow-Toolbar-PopupAnchor-{~D:Record.FlowViewIdentifier~}">
88
106
  </div>
89
107
  `
108
+ },
109
+ {
110
+ Hash: 'Flow-AddNode-List',
111
+ // Iteration source is `Record.Rows` — the outer call sets the
112
+ // FlowViewIdentifier on each row so inline handlers resolve their
113
+ // owning view independently. Nested {~D:~} inside {~TS:~}
114
+ // addresses isn't supported by the template engine, so we keep
115
+ // addresses static.
116
+ Template: '{~TS:Flow-AddNode-Row:Record.Rows~}'
117
+ },
118
+ {
119
+ Hash: 'Flow-AddNode-Row',
120
+ Template: '<div class="pict-flow-popup-list-item" data-node-type="{~D:Record.NodeType~}"'
121
+ + ' onclick="_Pict.views[\'{~D:Record.FlowViewIdentifier~}\']._ToolbarView._addNodeFromPopup(this.getAttribute(\'data-node-type\'))">'
122
+ + '<span class="pict-flow-popup-list-item-icon">{~D:Record.IconHTML~}</span>'
123
+ + '<span class="pict-flow-popup-list-item-label">{~D:Record.Label~}</span>'
124
+ + '{~NE:Record.Code^<span class="pict-flow-popup-list-item-code">{~D:Record.Code~}</span>~}'
125
+ + '</div>'
126
+ },
127
+ {
128
+ Hash: 'Flow-Cards-List',
129
+ Template: '{~TS:Flow-Cards-Category:Record.Categories~}'
130
+ },
131
+ {
132
+ Hash: 'Flow-Cards-Category',
133
+ Template: '<div class="pict-flow-palette-category" style="padding:0.35em 0.5em;">'
134
+ + '<div class="pict-flow-palette-category-label">{~D:Record.Name~}</div>'
135
+ + '<div class="pict-flow-palette-cards">{~TS:Flow-Cards-Card:Record.Cards~}</div>'
136
+ + '</div>'
137
+ },
138
+ {
139
+ Hash: 'Flow-Cards-Card',
140
+ Template: '<div class="pict-flow-palette-card{~D:Record.DisabledClass~}" data-card-type="{~D:Record.CardType~}" title="{~D:Record.Tooltip~}"'
141
+ + ' onclick="_Pict.views[\'{~D:Record.FlowViewIdentifier~}\']._ToolbarView._addCardFromPopup(this.getAttribute(\'data-card-type\'))">'
142
+ + '{~NE:Record.IconHTML^<span class="pict-flow-palette-card-icon">{~D:Record.IconHTML~}</span>~}'
143
+ + '{~NE:Record.IconEmoji^<span class="pict-flow-palette-card-icon">{~D:Record.IconEmoji~}</span>~}'
144
+ + '{~NE:Record.SwatchColor^<span class="pict-flow-palette-card-swatch" style="background-color: {~D:Record.SwatchColor~};"></span>~}'
145
+ + '<span class="pict-flow-palette-card-title">{~D:Record.Label~}</span>'
146
+ + '{~NE:Record.Code^<span class="pict-flow-palette-card-code">{~D:Record.Code~}</span>~}'
147
+ + '</div>'
148
+ },
149
+ {
150
+ Hash: 'Flow-Layout-List',
151
+ Template: '{~TS:Flow-Layout-Row:Record.Rows~}'
152
+ },
153
+ {
154
+ Hash: 'Flow-Layout-Row',
155
+ Template: '<div class="pict-flow-popup-layout-row" data-layout-hash="{~D:Record.LayoutHash~}"'
156
+ + ' onclick="_Pict.views[\'{~D:Record.FlowViewIdentifier~}\']._ToolbarView._restoreLayoutFromPopup(\'{~D:Record.LayoutHash~}\', event)">'
157
+ + '<span class="pict-flow-popup-layout-name">{~D:Record.Name~}</span>'
158
+ + '<button class="pict-flow-popup-layout-delete" title="Delete layout"'
159
+ + ' onclick="_Pict.views[\'{~D:Record.FlowViewIdentifier~}\']._ToolbarView._deleteLayoutFromPopup(\'{~D:Record.LayoutHash~}\', event)">'
160
+ + '{~D:Record.DeleteIconHTML~}</button>'
161
+ + '</div>'
162
+ },
163
+ {
164
+ Hash: 'Flow-Layout-OptionList',
165
+ Template: '{~TS:Flow-Layout-Option:Record.Options~}'
166
+ },
167
+ {
168
+ Hash: 'Flow-Layout-Option',
169
+ Template: '<option value="{~D:Record.Value~}"{~D:Record.SelectedAttr~}>{~D:Record.Label~}</option>'
90
170
  }
91
171
  ],
92
172
 
@@ -141,33 +221,8 @@ class PictViewFlowToolbar extends libPictView
141
221
  {
142
222
  let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
143
223
 
144
- // Bind toolbar button events via event delegation
145
- let tmpToolbarBar = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-Bar-${tmpFlowViewIdentifier}`);
146
- if (tmpToolbarBar.length > 0)
147
- {
148
- tmpToolbarBar[0].addEventListener('click', (pEvent) =>
149
- {
150
- let tmpTarget = pEvent.target;
151
- if (!tmpTarget) return;
152
-
153
- // Walk up to find the button with the action
154
- let tmpButton = tmpTarget.closest('[data-flow-action]');
155
- if (!tmpButton) return;
156
-
157
- let tmpAction = tmpButton.getAttribute('data-flow-action');
158
- this._handleToolbarAction(tmpAction);
159
- });
160
- }
161
-
162
- // Bind expand button click (it's outside the main toolbar bar)
163
- let tmpExpandBtn = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-ExpandBtn-${tmpFlowViewIdentifier}`);
164
- if (tmpExpandBtn.length > 0)
165
- {
166
- tmpExpandBtn[0].addEventListener('click', () =>
167
- {
168
- this._setToolbarMode('docked');
169
- });
170
- }
224
+ // Click handlers live on each button as inline `onclick` attributes
225
+ // in Flow-Toolbar-Template — they call _handleToolbarAction directly.
171
226
 
172
227
  // Populate SVG icons for toolbar buttons
173
228
  this._populateToolbarIcons();
@@ -425,212 +480,180 @@ class PictViewFlowToolbar extends libPictView
425
480
  // ── Add Node Popup ────────────────────────────────────────────────────
426
481
 
427
482
  /**
428
- * Build the searchable Add Node popup content.
483
+ * Build the searchable Add Node popup content via templates with inline
484
+ * handlers. Search input fires `_filterNodeList` which re-renders the
485
+ * list section in place.
486
+ *
429
487
  * @param {HTMLElement} pContainer
430
488
  */
431
489
  _buildAddNodePopup(pContainer)
432
490
  {
433
- // Search wrapper
434
- let tmpSearchWrapper = document.createElement('div');
435
- tmpSearchWrapper.className = 'pict-flow-popup-search-wrapper';
436
-
437
- let tmpSearchIcon = document.createElement('span');
438
- tmpSearchIcon.className = 'pict-flow-popup-search-icon';
491
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
439
492
  let tmpIconProvider = this._FlowView ? this._FlowView._IconProvider : null;
440
- if (tmpIconProvider)
441
- {
442
- tmpSearchIcon.innerHTML = tmpIconProvider.getIconSVGMarkup('search', 12);
443
- }
444
- tmpSearchWrapper.appendChild(tmpSearchIcon);
445
-
446
- let tmpSearchInput = document.createElement('input');
447
- tmpSearchInput.className = 'pict-flow-popup-search';
448
- tmpSearchInput.setAttribute('type', 'text');
449
- tmpSearchInput.setAttribute('placeholder', 'Search node types...');
450
- tmpSearchWrapper.appendChild(tmpSearchInput);
451
- pContainer.appendChild(tmpSearchWrapper);
452
-
453
- // Node list
454
- let tmpListDiv = document.createElement('div');
455
- tmpListDiv.className = 'pict-flow-popup-node-list';
456
- pContainer.appendChild(tmpListDiv);
457
-
458
- // Initial population
459
- this._populateNodeList(tmpListDiv, '');
460
-
461
- // Filter on input
462
- tmpSearchInput.addEventListener('input', () =>
463
- {
464
- this._populateNodeList(tmpListDiv, tmpSearchInput.value);
465
- });
493
+ let tmpSearchIconHTML = tmpIconProvider ? tmpIconProvider.getIconSVGMarkup('search', 12) : '';
494
+
495
+ let tmpListID = `Flow-Toolbar-AddNodeList-${tmpFlowViewIdentifier}`;
496
+ let tmpSearchID = `Flow-Toolbar-AddNodeSearch-${tmpFlowViewIdentifier}`;
497
+
498
+ // Stage AppData and render the initial list HTML inline — the popup
499
+ // element isn't in the DOM yet, so we can't look it up via
500
+ // getElementById here. _filterNodeList handles updates after open.
501
+ let tmpInitialListHTML = this._renderNodeListHTML('');
502
+
503
+ this.pict.ContentAssignment.assignContent(pContainer,
504
+ '<div class="pict-flow-popup-search-wrapper">'
505
+ + '<span class="pict-flow-popup-search-icon">' + tmpSearchIconHTML + '</span>'
506
+ + '<input id="' + tmpSearchID + '" class="pict-flow-popup-search" type="text" placeholder="Search node types..." '
507
+ + 'oninput="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._filterNodeList(\'' + tmpListID + '\', this.value)" />'
508
+ + '</div>'
509
+ + '<div id="' + tmpListID + '" class="pict-flow-popup-node-list">' + tmpInitialListHTML + '</div>');
466
510
  }
467
511
 
468
512
  /**
469
- * Populate the node list in the Add Node popup, filtered by search text.
470
- * @param {HTMLElement} pListDiv
513
+ * Build the HTML string for the Add Node list filtered by the given
514
+ * text. Stages AppData and renders the {~TS:~} template into a string;
515
+ * returns "no matches" markup when nothing matches.
516
+ *
471
517
  * @param {string} pFilter
518
+ * @returns {string}
472
519
  */
473
- _populateNodeList(pListDiv, pFilter)
520
+ _renderNodeListHTML(pFilter)
474
521
  {
475
- if (!this._FlowView || !this._FlowView._NodeTypeProvider) return;
476
-
477
- // Clear
478
- while (pListDiv.firstChild)
479
- {
480
- pListDiv.removeChild(pListDiv.firstChild);
481
- }
522
+ if (!this._FlowView || !this._FlowView._NodeTypeProvider) return '';
482
523
 
524
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
483
525
  let tmpTypes = this._FlowView._NodeTypeProvider.getNodeTypes();
484
526
  let tmpTypeKeys = Object.keys(tmpTypes);
485
527
  let tmpFilter = (pFilter || '').toLowerCase().trim();
486
528
  let tmpIconProvider = this._FlowView._IconProvider;
487
- let tmpMatchCount = 0;
488
529
 
530
+ let tmpRows = [];
489
531
  for (let i = 0; i < tmpTypeKeys.length; i++)
490
532
  {
491
533
  let tmpTypeConfig = tmpTypes[tmpTypeKeys[i]];
492
534
  let tmpMeta = tmpTypeConfig.CardMetadata || {};
493
-
494
- // Skip disabled cards
495
535
  if (tmpMeta.Enabled === false) continue;
496
536
 
497
- // Filter match: label, code, or category
498
537
  if (tmpFilter)
499
538
  {
500
539
  let tmpLabel = (tmpTypeConfig.Label || '').toLowerCase();
501
540
  let tmpCode = (tmpMeta.Code || '').toLowerCase();
502
541
  let tmpCategory = (tmpMeta.Category || '').toLowerCase();
503
- if (tmpLabel.indexOf(tmpFilter) < 0 &&
504
- tmpCode.indexOf(tmpFilter) < 0 &&
505
- tmpCategory.indexOf(tmpFilter) < 0)
506
- {
507
- continue;
508
- }
542
+ if (tmpLabel.indexOf(tmpFilter) < 0 && tmpCode.indexOf(tmpFilter) < 0 && tmpCategory.indexOf(tmpFilter) < 0) continue;
509
543
  }
510
544
 
511
- tmpMatchCount++;
512
-
513
- let tmpRow = document.createElement('div');
514
- tmpRow.className = 'pict-flow-popup-list-item';
515
- tmpRow.setAttribute('data-node-type', tmpTypeKeys[i]);
516
-
517
- // Icon
518
- let tmpIconSpan = document.createElement('span');
519
- tmpIconSpan.className = 'pict-flow-popup-list-item-icon';
545
+ let tmpIconHTML = '';
520
546
  if (tmpIconProvider)
521
547
  {
522
548
  let tmpResolvedKey = tmpIconProvider.resolveIconKey(tmpMeta);
523
- tmpIconSpan.innerHTML = tmpIconProvider.getIconSVGMarkup(tmpResolvedKey, 16);
549
+ tmpIconHTML = tmpIconProvider.getIconSVGMarkup(tmpResolvedKey, 16);
524
550
  }
525
- tmpRow.appendChild(tmpIconSpan);
526
-
527
- // Label
528
- let tmpLabelSpan = document.createElement('span');
529
- tmpLabelSpan.className = 'pict-flow-popup-list-item-label';
530
- tmpLabelSpan.textContent = tmpTypeConfig.Label;
531
- tmpRow.appendChild(tmpLabelSpan);
532
-
533
- // Code badge
534
- if (tmpMeta.Code)
535
- {
536
- let tmpCodeSpan = document.createElement('span');
537
- tmpCodeSpan.className = 'pict-flow-popup-list-item-code';
538
- tmpCodeSpan.textContent = tmpMeta.Code;
539
- tmpRow.appendChild(tmpCodeSpan);
540
- }
541
-
542
- // Click handler
543
- tmpRow.addEventListener('click', () =>
551
+ tmpRows.push(
544
552
  {
545
- this._addNodeAtCenter(tmpTypeKeys[i]);
546
- this._closePopup();
553
+ NodeType: tmpTypeKeys[i],
554
+ Label: tmpTypeConfig.Label || '',
555
+ IconHTML: tmpIconHTML,
556
+ Code: tmpMeta.Code || '',
557
+ FlowViewIdentifier: tmpFlowViewIdentifier
547
558
  });
548
-
549
- pListDiv.appendChild(tmpRow);
550
559
  }
551
560
 
552
- if (tmpMatchCount === 0)
561
+ if (tmpRows.length === 0)
553
562
  {
554
- let tmpEmpty = document.createElement('div');
555
- tmpEmpty.className = 'pict-flow-popup-list-empty';
556
- tmpEmpty.textContent = 'No matching node types';
557
- pListDiv.appendChild(tmpEmpty);
563
+ return '<div class="pict-flow-popup-list-empty">No matching node types</div>';
558
564
  }
565
+ return this.pict.parseTemplateByHash('Flow-AddNode-List', { Rows: tmpRows });
566
+ }
567
+
568
+ /**
569
+ * Update the Add Node list contents in response to a search input
570
+ * change. Called from the inline `oninput` handler. The list element
571
+ * is already in the DOM by the time this fires.
572
+ *
573
+ * @param {string} pListID
574
+ * @param {string} pFilter
575
+ */
576
+ _filterNodeList(pListID, pFilter)
577
+ {
578
+ let tmpListEl = document.getElementById(pListID);
579
+ if (!tmpListEl) return;
580
+ this.pict.ContentAssignment.assignContent(tmpListEl, this._renderNodeListHTML(pFilter));
581
+ }
582
+
583
+ /**
584
+ * Inline handler for an Add Node row click. Adds the node at viewport
585
+ * center and closes the popup.
586
+ *
587
+ * @param {string} pNodeType
588
+ */
589
+ _addNodeFromPopup(pNodeType)
590
+ {
591
+ this._addNodeAtCenter(pNodeType);
592
+ this._closePopup();
559
593
  }
560
594
 
561
595
  // ── Cards Popup ───────────────────────────────────────────────────────
562
596
 
563
597
  /**
564
- * Build the Cards popup content with search and categorized palette.
598
+ * Build the Cards popup using templates with inline handlers. Search
599
+ * input fires `_filterCardsList` which re-renders the categorized list.
600
+ *
565
601
  * @param {HTMLElement} pContainer
566
602
  */
567
603
  _buildCardsPopup(pContainer)
568
604
  {
569
- // Search wrapper
570
- let tmpSearchWrapper = document.createElement('div');
571
- tmpSearchWrapper.className = 'pict-flow-popup-search-wrapper';
572
-
573
- let tmpSearchIcon = document.createElement('span');
574
- tmpSearchIcon.className = 'pict-flow-popup-search-icon';
605
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
575
606
  let tmpIconProvider = this._FlowView ? this._FlowView._IconProvider : null;
576
- if (tmpIconProvider)
577
- {
578
- tmpSearchIcon.innerHTML = tmpIconProvider.getIconSVGMarkup('search', 12);
579
- }
580
- tmpSearchWrapper.appendChild(tmpSearchIcon);
607
+ let tmpSearchIconHTML = tmpIconProvider ? tmpIconProvider.getIconSVGMarkup('search', 12) : '';
581
608
 
582
- let tmpSearchInput = document.createElement('input');
583
- tmpSearchInput.className = 'pict-flow-popup-search';
584
- tmpSearchInput.setAttribute('type', 'text');
585
- tmpSearchInput.setAttribute('placeholder', 'Search cards...');
586
- tmpSearchWrapper.appendChild(tmpSearchInput);
587
- pContainer.appendChild(tmpSearchWrapper);
609
+ let tmpListID = `Flow-Toolbar-CardsList-${tmpFlowViewIdentifier}`;
610
+ let tmpSearchID = `Flow-Toolbar-CardsSearch-${tmpFlowViewIdentifier}`;
588
611
 
589
- // Palette list container
590
- let tmpListDiv = document.createElement('div');
591
- tmpListDiv.className = 'pict-flow-popup-node-list';
592
- pContainer.appendChild(tmpListDiv);
612
+ // Initial render inline — popup not yet in DOM.
613
+ let tmpInitialListHTML = this._renderCardsListHTML('');
593
614
 
594
- // Initial population
595
- this._renderPalette(tmpListDiv, '');
615
+ this.pict.ContentAssignment.assignContent(pContainer,
616
+ '<div class="pict-flow-popup-search-wrapper">'
617
+ + '<span class="pict-flow-popup-search-icon">' + tmpSearchIconHTML + '</span>'
618
+ + '<input id="' + tmpSearchID + '" class="pict-flow-popup-search" type="text" placeholder="Search cards..." '
619
+ + 'oninput="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._filterCardsList(\'' + tmpListID + '\', this.value)" />'
620
+ + '</div>'
621
+ + '<div id="' + tmpListID + '" class="pict-flow-popup-node-list">' + tmpInitialListHTML + '</div>');
596
622
 
597
- // Filter on input
598
- tmpSearchInput.addEventListener('input', () =>
623
+ // Focus search input — defer past the popup append.
624
+ setTimeout(() =>
599
625
  {
600
- this._renderPalette(tmpListDiv, tmpSearchInput.value);
601
- });
602
-
603
- // Focus search input
604
- setTimeout(() => { tmpSearchInput.focus(); }, 50);
626
+ let tmpSearch = document.getElementById(tmpSearchID);
627
+ if (tmpSearch) tmpSearch.focus();
628
+ }, 50);
605
629
  }
606
630
 
607
631
  /**
608
- * Render the card palette with categories and card chips into a container.
609
- * @param {HTMLElement} pContainer - The target container element
610
- * @param {string} [pFilter] - Optional search filter text
632
+ * Build the cards-list HTML string for the given filter. Stages
633
+ * AppData and renders via the {~TS:~} templates; returns "no matches"
634
+ * markup when nothing matches.
635
+ *
636
+ * @param {string} pFilter
637
+ * @returns {string}
611
638
  */
612
- _renderPalette(pContainer, pFilter)
639
+ _renderCardsListHTML(pFilter)
613
640
  {
614
- if (!this._FlowView || !this._FlowView._NodeTypeProvider) return;
615
-
616
- // Clear existing content
617
- while (pContainer.firstChild)
618
- {
619
- pContainer.removeChild(pContainer.firstChild);
620
- }
641
+ if (!this._FlowView || !this._FlowView._NodeTypeProvider) return '';
621
642
 
643
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
622
644
  let tmpCategories = this._FlowView._NodeTypeProvider.getCardsByCategory();
623
645
  let tmpCategoryKeys = Object.keys(tmpCategories);
624
646
  let tmpFilter = (pFilter || '').toLowerCase().trim();
647
+ let tmpIconProvider = this._FlowView._IconProvider;
625
648
  let tmpTotalMatchCount = 0;
626
649
 
650
+ let tmpCategoryRecords = [];
627
651
  for (let i = 0; i < tmpCategoryKeys.length; i++)
628
652
  {
629
653
  let tmpCategoryName = tmpCategoryKeys[i];
630
654
  let tmpCards = tmpCategories[tmpCategoryName];
631
- let tmpMatchingCards = [];
655
+ let tmpMatching = [];
632
656
 
633
- // Filter cards within this category
634
657
  for (let j = 0; j < tmpCards.length; j++)
635
658
  {
636
659
  let tmpCardConfig = tmpCards[j];
@@ -641,318 +664,225 @@ class PictViewFlowToolbar extends libPictView
641
664
  let tmpLabel = (tmpCardConfig.Label || '').toLowerCase();
642
665
  let tmpCode = (tmpMeta.Code || '').toLowerCase();
643
666
  let tmpCategory = tmpCategoryName.toLowerCase();
644
- if (tmpLabel.indexOf(tmpFilter) < 0 &&
645
- tmpCode.indexOf(tmpFilter) < 0 &&
646
- tmpCategory.indexOf(tmpFilter) < 0)
647
- {
648
- continue;
649
- }
667
+ if (tmpLabel.indexOf(tmpFilter) < 0 && tmpCode.indexOf(tmpFilter) < 0 && tmpCategory.indexOf(tmpFilter) < 0) continue;
650
668
  }
651
669
 
652
- tmpMatchingCards.push(tmpCardConfig);
653
- }
654
-
655
- if (tmpMatchingCards.length === 0) continue;
656
-
657
- tmpTotalMatchCount += tmpMatchingCards.length;
658
-
659
- let tmpCategoryDiv = document.createElement('div');
660
- tmpCategoryDiv.className = 'pict-flow-palette-category';
661
- tmpCategoryDiv.style.padding = '0.35em 0.5em';
662
-
663
- let tmpCategoryLabel = document.createElement('div');
664
- tmpCategoryLabel.className = 'pict-flow-palette-category-label';
665
- tmpCategoryLabel.textContent = tmpCategoryName;
666
- tmpCategoryDiv.appendChild(tmpCategoryLabel);
667
-
668
- let tmpCardsDiv = document.createElement('div');
669
- tmpCardsDiv.className = 'pict-flow-palette-cards';
670
-
671
- for (let j = 0; j < tmpMatchingCards.length; j++)
672
- {
673
- let tmpCardConfig = tmpMatchingCards[j];
674
- let tmpMeta = tmpCardConfig.CardMetadata || {};
675
-
676
- let tmpCardEl = document.createElement('div');
677
- tmpCardEl.className = 'pict-flow-palette-card';
678
- if (tmpMeta.Enabled === false)
670
+ let tmpIconHTML = '';
671
+ let tmpIsEmoji = false;
672
+ if (tmpMeta.Icon && tmpIconProvider && !tmpIconProvider.isEmojiIcon(tmpMeta.Icon))
679
673
  {
680
- tmpCardEl.classList.add('disabled');
674
+ tmpIconHTML = tmpIconProvider.getIconSVGMarkup(tmpIconProvider.resolveIconKey(tmpMeta), 14);
681
675
  }
682
- tmpCardEl.setAttribute('data-card-type', tmpCardConfig.Hash);
683
-
684
- if (tmpMeta.Tooltip)
676
+ else if (tmpMeta.Icon)
685
677
  {
686
- tmpCardEl.setAttribute('title', tmpMeta.Tooltip);
678
+ tmpIsEmoji = true;
687
679
  }
688
- else if (tmpMeta.Description)
680
+ else if (tmpIconProvider)
689
681
  {
690
- tmpCardEl.setAttribute('title', tmpMeta.Description);
682
+ tmpIconHTML = tmpIconProvider.getIconSVGMarkup('default', 14);
691
683
  }
692
684
 
693
- // Icon or color swatch
694
- if (tmpMeta.Icon)
685
+ tmpMatching.push(
695
686
  {
696
- let tmpIconSpan = document.createElement('span');
697
- tmpIconSpan.className = 'pict-flow-palette-card-icon';
698
- let tmpIconProvider = this._FlowView._IconProvider;
699
- if (tmpIconProvider && !tmpIconProvider.isEmojiIcon(tmpMeta.Icon))
700
- {
701
- let tmpResolvedKey = tmpIconProvider.resolveIconKey(tmpMeta);
702
- tmpIconSpan.innerHTML = tmpIconProvider.getIconSVGMarkup(tmpResolvedKey, 14);
703
- }
704
- else
705
- {
706
- tmpIconSpan.textContent = tmpMeta.Icon;
707
- }
708
- tmpCardEl.appendChild(tmpIconSpan);
709
- }
710
- else if (this._FlowView._IconProvider)
711
- {
712
- let tmpIconSpan = document.createElement('span');
713
- tmpIconSpan.className = 'pict-flow-palette-card-icon';
714
- tmpIconSpan.innerHTML = this._FlowView._IconProvider.getIconSVGMarkup('default', 14);
715
- tmpCardEl.appendChild(tmpIconSpan);
716
- }
717
- else if (tmpCardConfig.TitleBarColor)
718
- {
719
- let tmpSwatch = document.createElement('span');
720
- tmpSwatch.className = 'pict-flow-palette-card-swatch';
721
- tmpSwatch.style.backgroundColor = tmpCardConfig.TitleBarColor;
722
- tmpCardEl.appendChild(tmpSwatch);
723
- }
724
-
725
- // Title
726
- let tmpTitleSpan = document.createElement('span');
727
- tmpTitleSpan.className = 'pict-flow-palette-card-title';
728
- tmpTitleSpan.textContent = tmpCardConfig.Label;
729
- tmpCardEl.appendChild(tmpTitleSpan);
730
-
731
- // Code badge
732
- if (tmpMeta.Code)
733
- {
734
- let tmpCodeSpan = document.createElement('span');
735
- tmpCodeSpan.className = 'pict-flow-palette-card-code';
736
- tmpCodeSpan.textContent = tmpMeta.Code;
737
- tmpCardEl.appendChild(tmpCodeSpan);
738
- }
739
-
740
- // Click handler
741
- tmpCardEl.addEventListener('click', () =>
742
- {
743
- this._addCardFromPalette(tmpCardConfig.Hash);
744
- this._closePopup();
687
+ CardType: tmpCardConfig.Hash,
688
+ Label: tmpCardConfig.Label || '',
689
+ Code: tmpMeta.Code || '',
690
+ IconHTML: tmpIconHTML,
691
+ IconEmoji: tmpIsEmoji ? tmpMeta.Icon : '',
692
+ DisabledClass: (tmpMeta.Enabled === false) ? ' disabled' : '',
693
+ Tooltip: tmpMeta.Tooltip || tmpMeta.Description || '',
694
+ SwatchColor: (!tmpIconHTML && !tmpIsEmoji && tmpCardConfig.TitleBarColor) ? tmpCardConfig.TitleBarColor : '',
695
+ FlowViewIdentifier: tmpFlowViewIdentifier
745
696
  });
746
-
747
- tmpCardsDiv.appendChild(tmpCardEl);
748
697
  }
749
698
 
750
- tmpCategoryDiv.appendChild(tmpCardsDiv);
751
- pContainer.appendChild(tmpCategoryDiv);
699
+ if (tmpMatching.length === 0) continue;
700
+ tmpTotalMatchCount += tmpMatching.length;
701
+ tmpCategoryRecords.push(
702
+ {
703
+ Name: tmpCategoryName,
704
+ Cards: tmpMatching,
705
+ FlowViewIdentifier: tmpFlowViewIdentifier
706
+ });
752
707
  }
753
708
 
754
709
  if (tmpTotalMatchCount === 0)
755
710
  {
756
- let tmpEmpty = document.createElement('div');
757
- tmpEmpty.className = 'pict-flow-popup-list-empty';
758
- tmpEmpty.textContent = tmpFilter ? 'No matching cards' : 'No card types available';
759
- pContainer.appendChild(tmpEmpty);
711
+ return '<div class="pict-flow-popup-list-empty">' + (tmpFilter ? 'No matching cards' : 'No card types available') + '</div>';
760
712
  }
713
+ return this.pict.parseTemplateByHash('Flow-Cards-List', { Categories: tmpCategoryRecords });
714
+ }
715
+
716
+ /**
717
+ * Inline `oninput` handler — refresh card list with the new filter.
718
+ */
719
+ _filterCardsList(pListID, pFilter)
720
+ {
721
+ let tmpListEl = document.getElementById(pListID);
722
+ if (!tmpListEl) return;
723
+ this.pict.ContentAssignment.assignContent(tmpListEl, this._renderCardsListHTML(pFilter));
724
+ }
725
+
726
+ /**
727
+ * Inline handler for a card click. Adds the node and closes the popup.
728
+ *
729
+ * @param {string} pCardType
730
+ */
731
+ _addCardFromPopup(pCardType)
732
+ {
733
+ this._addCardFromPalette(pCardType);
734
+ this._closePopup();
761
735
  }
762
736
 
763
737
  // ── Layout Popup ──────────────────────────────────────────────────────
764
738
 
765
739
  /**
766
- * Build the Layout popup content.
740
+ * Build the Layout popup using templates with inline handlers.
741
+ *
742
+ * Layout records (name + hash) are pushed into AppData and iterated via
743
+ * a {~TS:~} template. The save section is a static fragment with inline
744
+ * handlers that call the toolbar's helper methods.
745
+ *
767
746
  * @param {HTMLElement} pContainer
768
747
  */
769
748
  _buildLayoutPopup(pContainer)
770
749
  {
750
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
771
751
  let tmpIconProvider = this._FlowView ? this._FlowView._IconProvider : null;
772
-
773
- // Save Layout section at top
774
- let tmpSaveSection = document.createElement('div');
775
- tmpSaveSection.className = 'pict-flow-popup-layout-save-section';
776
-
777
- // Save input row (hidden initially)
778
- let tmpSaveInputRow = document.createElement('div');
779
- tmpSaveInputRow.className = 'pict-flow-popup-layout-save-input-row';
780
- tmpSaveInputRow.style.display = 'none';
781
-
782
- let tmpSaveInput = document.createElement('input');
783
- tmpSaveInput.className = 'pict-flow-popup-layout-save-input';
784
- tmpSaveInput.setAttribute('type', 'text');
785
- tmpSaveInput.setAttribute('placeholder', 'Layout name...');
786
- tmpSaveInputRow.appendChild(tmpSaveInput);
787
-
788
- let tmpSaveConfirmBtn = document.createElement('button');
789
- tmpSaveConfirmBtn.className = 'pict-flow-popup-layout-save-confirm';
790
- tmpSaveConfirmBtn.title = 'Save';
791
- if (tmpIconProvider)
792
- {
793
- tmpSaveConfirmBtn.innerHTML = tmpIconProvider.getIconSVGMarkup('save', 14);
794
- }
795
- else
752
+ let tmpSaveIconHTML = tmpIconProvider ? tmpIconProvider.getIconSVGMarkup('save', 14) : '✓';
753
+ let tmpDeleteIconHTML = tmpIconProvider ? tmpIconProvider.getIconSVGMarkup('trash', 12) : '×';
754
+
755
+ let tmpSaveRowID = `Flow-Toolbar-LayoutSaveRow-${tmpFlowViewIdentifier}`;
756
+ let tmpSaveInputRowID = `Flow-Toolbar-LayoutSaveInputRow-${tmpFlowViewIdentifier}`;
757
+ let tmpSaveInputID = `Flow-Toolbar-LayoutSaveInput-${tmpFlowViewIdentifier}`;
758
+ let tmpListID = `Flow-Toolbar-LayoutList-${tmpFlowViewIdentifier}`;
759
+
760
+ // Build layout records for the {~TS:~} iteration
761
+ let tmpLayouts = (this._FlowView && this._FlowView._LayoutProvider)
762
+ ? this._FlowView._LayoutProvider.getLayouts() : [];
763
+ let tmpLayoutRecords = [];
764
+ for (let i = 0; i < tmpLayouts.length; i++)
796
765
  {
797
- tmpSaveConfirmBtn.textContent = '✓';
766
+ tmpLayoutRecords.push(
767
+ {
768
+ LayoutHash: tmpLayouts[i].Hash,
769
+ Name: tmpLayouts[i].Name || '',
770
+ DeleteIconHTML: tmpDeleteIconHTML,
771
+ FlowViewIdentifier: tmpFlowViewIdentifier
772
+ });
798
773
  }
799
- tmpSaveInputRow.appendChild(tmpSaveConfirmBtn);
800
774
 
801
- // "Save Current Layout" clickable row
802
- let tmpSaveRow = document.createElement('div');
803
- tmpSaveRow.className = 'pict-flow-popup-layout-save';
775
+ let tmpListHTML = (tmpLayoutRecords.length === 0)
776
+ ? '<div class="pict-flow-popup-list-empty">No saved layouts</div>'
777
+ : this.pict.parseTemplateByHash('Flow-Layout-List', { Rows: tmpLayoutRecords });
778
+
779
+ this.pict.ContentAssignment.assignContent(pContainer,
780
+ '<div class="pict-flow-popup-layout-save-section">'
781
+ + '<div id="' + tmpSaveRowID + '" class="pict-flow-popup-layout-save"'
782
+ + ' onclick="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._showLayoutSaveInput(\'' + tmpSaveRowID + '\', \'' + tmpSaveInputRowID + '\', \'' + tmpSaveInputID + '\')">'
783
+ + '<span class="pict-flow-popup-layout-save-icon">' + tmpSaveIconHTML + '</span>'
784
+ + '<span>Save Current Layout</span>'
785
+ + '</div>'
786
+ + '<div id="' + tmpSaveInputRowID + '" class="pict-flow-popup-layout-save-input-row" style="display:none;">'
787
+ + '<input id="' + tmpSaveInputID + '" class="pict-flow-popup-layout-save-input" type="text" placeholder="Layout name..."'
788
+ + ' onclick="event.stopPropagation()"'
789
+ + ' onkeydown="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._handleLayoutSaveKey(event, \'' + tmpSaveInputID + '\', \'' + tmpSaveRowID + '\', \'' + tmpSaveInputRowID + '\')" />'
790
+ + '<button class="pict-flow-popup-layout-save-confirm" title="Save"'
791
+ + ' onclick="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._confirmLayoutSave(\'' + tmpSaveInputID + '\')">'
792
+ + tmpSaveIconHTML + '</button>'
793
+ + '</div>'
794
+ + '</div>'
795
+ + '<div class="pict-flow-popup-divider"></div>'
796
+ + '<div id="' + tmpListID + '">' + tmpListHTML + '</div>');
797
+ }
804
798
 
805
- let tmpSaveIcon = document.createElement('span');
806
- tmpSaveIcon.className = 'pict-flow-popup-layout-save-icon';
807
- if (tmpIconProvider)
808
- {
809
- tmpSaveIcon.innerHTML = tmpIconProvider.getIconSVGMarkup('save', 14);
799
+ /**
800
+ * Inline handler — reveal the layout-name input and focus it.
801
+ */
802
+ _showLayoutSaveInput(pSaveRowID, pInputRowID, pInputID)
803
+ {
804
+ let tmpRow = document.getElementById(pSaveRowID);
805
+ let tmpInputRow = document.getElementById(pInputRowID);
806
+ let tmpInput = document.getElementById(pInputID);
807
+ if (tmpRow) tmpRow.style.display = 'none';
808
+ if (tmpInputRow) tmpInputRow.style.display = '';
809
+ if (tmpInput)
810
+ {
811
+ tmpInput.value = '';
812
+ setTimeout(() => { tmpInput.focus(); }, 50);
810
813
  }
811
- tmpSaveRow.appendChild(tmpSaveIcon);
812
-
813
- let tmpSaveText = document.createElement('span');
814
- tmpSaveText.textContent = 'Save Current Layout';
815
- tmpSaveRow.appendChild(tmpSaveText);
816
-
817
- // Click "Save Current Layout" to reveal the input row
818
- tmpSaveRow.addEventListener('click', () =>
819
- {
820
- tmpSaveRow.style.display = 'none';
821
- tmpSaveInputRow.style.display = '';
822
- tmpSaveInput.value = '';
823
- setTimeout(() => { tmpSaveInput.focus(); }, 50);
824
- });
825
-
826
- // Confirm save via button click
827
- let tmpDoSave = () =>
828
- {
829
- let tmpName = tmpSaveInput.value.trim();
830
- if (tmpName === '') return;
831
- this._FlowView._LayoutProvider.saveLayout(tmpName);
832
- // Refresh the popup content
833
- while (pContainer.firstChild)
834
- {
835
- pContainer.removeChild(pContainer.firstChild);
836
- }
837
- this._buildLayoutPopup(pContainer);
838
- };
839
-
840
- tmpSaveConfirmBtn.addEventListener('click', tmpDoSave);
841
-
842
- // Confirm save via Enter key
843
- tmpSaveInput.addEventListener('keydown', (pEvent) =>
844
- {
845
- if (pEvent.key === 'Enter')
846
- {
847
- pEvent.preventDefault();
848
- tmpDoSave();
849
- }
850
- else if (pEvent.key === 'Escape')
851
- {
852
- // Cancel — hide input, show the save row again
853
- tmpSaveInputRow.style.display = 'none';
854
- tmpSaveRow.style.display = '';
855
- }
856
- });
857
-
858
- // Prevent clicks inside the input from closing the popup
859
- tmpSaveInput.addEventListener('click', (pEvent) =>
860
- {
861
- pEvent.stopPropagation();
862
- });
863
-
864
- tmpSaveSection.appendChild(tmpSaveRow);
865
- tmpSaveSection.appendChild(tmpSaveInputRow);
866
- pContainer.appendChild(tmpSaveSection);
867
-
868
- // Divider
869
- let tmpDivider = document.createElement('div');
870
- tmpDivider.className = 'pict-flow-popup-divider';
871
- pContainer.appendChild(tmpDivider);
814
+ }
872
815
 
873
- // Layout rows
874
- if (!this._FlowView || !this._FlowView._LayoutProvider)
816
+ /**
817
+ * Inline handler — Enter saves, Escape cancels.
818
+ */
819
+ _handleLayoutSaveKey(pEvent, pInputID, pSaveRowID, pInputRowID)
820
+ {
821
+ if (pEvent.key === 'Enter')
875
822
  {
876
- let tmpEmpty = document.createElement('div');
877
- tmpEmpty.className = 'pict-flow-popup-list-empty';
878
- tmpEmpty.textContent = 'No saved layouts';
879
- pContainer.appendChild(tmpEmpty);
880
- return;
823
+ pEvent.preventDefault();
824
+ this._confirmLayoutSave(pInputID);
881
825
  }
882
-
883
- let tmpLayouts = this._FlowView._LayoutProvider.getLayouts();
884
-
885
- if (tmpLayouts.length === 0)
826
+ else if (pEvent.key === 'Escape')
886
827
  {
887
- let tmpEmpty = document.createElement('div');
888
- tmpEmpty.className = 'pict-flow-popup-list-empty';
889
- tmpEmpty.textContent = 'No saved layouts';
890
- pContainer.appendChild(tmpEmpty);
891
- return;
828
+ let tmpRow = document.getElementById(pSaveRowID);
829
+ let tmpInputRow = document.getElementById(pInputRowID);
830
+ if (tmpInputRow) tmpInputRow.style.display = 'none';
831
+ if (tmpRow) tmpRow.style.display = '';
892
832
  }
833
+ }
893
834
 
894
- for (let i = 0; i < tmpLayouts.length; i++)
895
- {
896
- let tmpLayout = tmpLayouts[i];
897
-
898
- let tmpRow = document.createElement('div');
899
- tmpRow.className = 'pict-flow-popup-layout-row';
900
-
901
- let tmpNameSpan = document.createElement('span');
902
- tmpNameSpan.className = 'pict-flow-popup-layout-name';
903
- tmpNameSpan.textContent = tmpLayout.Name;
904
- tmpRow.appendChild(tmpNameSpan);
835
+ /**
836
+ * Inline handler — save the layout under the entered name and rebuild
837
+ * the popup so the new entry appears.
838
+ */
839
+ _confirmLayoutSave(pInputID)
840
+ {
841
+ let tmpInput = document.getElementById(pInputID);
842
+ if (!tmpInput) return;
843
+ let tmpName = (tmpInput.value || '').trim();
844
+ if (tmpName === '') return;
845
+ this._FlowView._LayoutProvider.saveLayout(tmpName);
905
846
 
906
- // Delete button (visible on hover via CSS)
907
- let tmpDeleteBtn = document.createElement('button');
908
- tmpDeleteBtn.className = 'pict-flow-popup-layout-delete';
909
- tmpDeleteBtn.title = 'Delete layout';
910
- if (tmpIconProvider)
911
- {
912
- tmpDeleteBtn.innerHTML = tmpIconProvider.getIconSVGMarkup('trash', 12);
913
- }
914
- else
915
- {
916
- tmpDeleteBtn.textContent = '×';
917
- }
918
- tmpRow.appendChild(tmpDeleteBtn);
847
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
848
+ let tmpPopupEl = document.getElementById(`Flow-Toolbar-Popup-${tmpFlowViewIdentifier}`);
849
+ if (tmpPopupEl) this._buildLayoutPopup(tmpPopupEl);
850
+ }
919
851
 
920
- // Click row → restore layout
921
- tmpRow.addEventListener('click', (pEvent) =>
922
- {
923
- // Don't restore if they clicked the delete button
924
- if (pEvent.target.closest('.pict-flow-popup-layout-delete'))
925
- {
926
- return;
927
- }
928
- this._FlowView._LayoutProvider.restoreLayout(tmpLayout.Hash);
929
- this._closePopup();
930
- });
852
+ /**
853
+ * Inline handler — restore layout when the row (not its delete button)
854
+ * is clicked.
855
+ */
856
+ _restoreLayoutFromPopup(pLayoutHash, pEvent)
857
+ {
858
+ if (pEvent && pEvent.target && pEvent.target.closest('.pict-flow-popup-layout-delete')) return;
859
+ this._FlowView._LayoutProvider.restoreLayout(pLayoutHash);
860
+ this._closePopup();
861
+ }
931
862
 
932
- // Click delete → delete layout and refresh popup
933
- tmpDeleteBtn.addEventListener('click', (pEvent) =>
934
- {
935
- pEvent.stopPropagation();
936
- this._FlowView._LayoutProvider.deleteLayout(tmpLayout.Hash);
937
- // Refresh the popup content
938
- while (pContainer.firstChild)
939
- {
940
- pContainer.removeChild(pContainer.firstChild);
941
- }
942
- this._buildLayoutPopup(pContainer);
943
- });
863
+ /**
864
+ * Inline handler — delete a saved layout and refresh the popup.
865
+ */
866
+ _deleteLayoutFromPopup(pLayoutHash, pEvent)
867
+ {
868
+ if (pEvent && typeof pEvent.stopPropagation === 'function') pEvent.stopPropagation();
869
+ this._FlowView._LayoutProvider.deleteLayout(pLayoutHash);
944
870
 
945
- pContainer.appendChild(tmpRow);
946
- }
871
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
872
+ let tmpPopupEl = document.getElementById(`Flow-Toolbar-Popup-${tmpFlowViewIdentifier}`);
873
+ if (tmpPopupEl) this._buildLayoutPopup(tmpPopupEl);
947
874
  }
948
875
 
949
876
  // ── Layout Algorithm Popup ────────────────────────────────────────────
950
877
 
951
878
  /**
952
- * Build the Layout Algorithm popup content. Distinct from the
953
- * `_buildLayoutPopup` above which manages **saved layout snapshots**.
954
- * This popup lets the user pick a layout algorithm, tune its
955
- * parameters, and toggle auto-apply.
879
+ * Build the Layout Algorithm popup using templates with inline handlers.
880
+ * The popup has three logical sections (algorithm chooser, edge-theme
881
+ * chooser, auto-apply toggle + apply button). Each control's behavior
882
+ * lives on the toolbar as a helper method called from inline handlers.
883
+ *
884
+ * Iterations (algorithm options, edge themes) are template-driven via
885
+ * AppData arrays.
956
886
  *
957
887
  * @param {HTMLElement} pContainer
958
888
  */
@@ -960,251 +890,202 @@ class PictViewFlowToolbar extends libPictView
960
890
  {
961
891
  if (!this._FlowView || !this._FlowView._LayoutService) return;
962
892
 
893
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
963
894
  let tmpLayoutService = this._FlowView._LayoutService;
964
895
  let tmpCurrentSettings = this._FlowView.getLayoutAlgorithm();
965
896
  let tmpAlgoDescriptor = tmpLayoutService.getAlgorithm(tmpCurrentSettings.Algorithm);
897
+ let tmpEdgeSettings = this._FlowView.getEdgeTheme();
898
+ let tmpResolvedEdge = tmpEdgeSettings.Theme;
899
+ let tmpExplicitEdgeOverride = tmpEdgeSettings.Override;
900
+ let tmpDefaultThemeName = (tmpAlgoDescriptor && tmpAlgoDescriptor.DefaultEdgeTheme) || 'Bezier';
901
+ let tmpIconProvider = this._FlowView ? this._FlowView._IconProvider : null;
902
+ let tmpSettingsIconHTML = tmpIconProvider ? tmpIconProvider.getIconSVGMarkup('settings', 13) : '⚙';
966
903
 
967
- // ── Algorithm row: label + dropdown + collapse toggle ────
968
- let tmpAlgoSection = document.createElement('div');
969
- tmpAlgoSection.className = 'pict-flow-popup-settings-section pict-flow-popup-layout-algorithm-row';
970
-
971
- let tmpAlgoLabel = document.createElement('label');
972
- tmpAlgoLabel.className = 'pict-flow-popup-settings-label';
973
- tmpAlgoLabel.textContent = 'Algorithm';
974
- tmpAlgoSection.appendChild(tmpAlgoLabel);
975
-
976
- let tmpAlgoControls = document.createElement('div');
977
- tmpAlgoControls.className = 'pict-flow-popup-layout-algorithm-controls';
978
-
979
- let tmpAlgoSelect = document.createElement('select');
980
- tmpAlgoSelect.className = 'pict-flow-popup-settings-select pict-flow-popup-layout-algorithm-select';
981
- tmpAlgoSelect.setAttribute('data-layout-control', 'algorithm');
982
-
983
- let tmpAlgorithms = tmpLayoutService.listAlgorithms();
984
- for (let i = 0; i < tmpAlgorithms.length; i++)
904
+ // Build option records for the dropdowns
905
+ let tmpAlgoOptions = tmpLayoutService.listAlgorithms().map((pAlgo) => (
985
906
  {
986
- let tmpOption = document.createElement('option');
987
- tmpOption.value = tmpAlgorithms[i].Name;
988
- tmpOption.textContent = tmpAlgorithms[i].Label || tmpAlgorithms[i].Name;
989
- if (tmpAlgorithms[i].Name === tmpCurrentSettings.Algorithm)
990
- {
991
- tmpOption.selected = true;
992
- }
993
- tmpAlgoSelect.appendChild(tmpOption);
994
- }
907
+ Value: pAlgo.Name,
908
+ Label: pAlgo.Label || pAlgo.Name,
909
+ SelectedAttr: (pAlgo.Name === tmpCurrentSettings.Algorithm) ? ' selected="selected"' : ''
910
+ }));
995
911
 
996
- tmpAlgoSelect.addEventListener('change', () =>
912
+ let tmpEdgeOptionRecords = [];
913
+ tmpEdgeOptionRecords.push(
997
914
  {
998
- let tmpName = tmpAlgoSelect.value;
999
- let tmpAlgo = tmpLayoutService.getAlgorithm(tmpName);
1000
- let tmpDefaults = (tmpAlgo && tmpAlgo.DefaultParameters) ? JSON.parse(JSON.stringify(tmpAlgo.DefaultParameters)) : {};
1001
- this._FlowView.setLayoutAlgorithm(tmpName, tmpDefaults);
1002
- // Refresh the popup so the parameter form reflects the new algorithm
1003
- while (pContainer.firstChild)
1004
- {
1005
- pContainer.removeChild(pContainer.firstChild);
1006
- }
1007
- this._buildLayoutAlgorithmPopup(pContainer);
915
+ Value: '__inherit__',
916
+ Label: `Inherit from layout (${tmpDefaultThemeName})`,
917
+ SelectedAttr: !tmpExplicitEdgeOverride ? ' selected="selected"' : ''
1008
918
  });
1009
- tmpAlgoSelect.addEventListener('click', (pEvent) => { pEvent.stopPropagation(); });
919
+ let tmpEdgeThemes = tmpLayoutService.listEdgeThemes();
920
+ for (let i = 0; i < tmpEdgeThemes.length; i++)
921
+ {
922
+ tmpEdgeOptionRecords.push(
923
+ {
924
+ Value: tmpEdgeThemes[i].Name,
925
+ Label: tmpEdgeThemes[i].Label || tmpEdgeThemes[i].Name,
926
+ SelectedAttr: (tmpEdgeThemes[i].Name === tmpExplicitEdgeOverride) ? ' selected="selected"' : ''
927
+ });
928
+ }
1010
929
 
1011
- tmpAlgoControls.appendChild(tmpAlgoSelect);
930
+ let tmpAlgoOptionsHTML = this.pict.parseTemplateByHash('Flow-Layout-OptionList', { Options: tmpAlgoOptions });
931
+ let tmpEdgeOptionsHTML = this.pict.parseTemplateByHash('Flow-Layout-OptionList', { Options: tmpEdgeOptionRecords });
1012
932
 
1013
- // Collapse toggle for the parameter form. Only meaningful when
1014
- // the algorithm actually has parameters (Custom doesn't).
1015
933
  let tmpHasParameters = !!(tmpAlgoDescriptor && (
1016
934
  (tmpAlgoDescriptor.ParameterManifest && tmpAlgoDescriptor.ParameterManifest.Descriptors) ||
1017
935
  (tmpAlgoDescriptor.ParameterSchema && Object.keys(tmpAlgoDescriptor.ParameterSchema).length > 0)
1018
936
  ));
1019
- let tmpFormToggle = null;
1020
- if (tmpHasParameters)
1021
- {
1022
- tmpFormToggle = document.createElement('button');
1023
- tmpFormToggle.type = 'button';
1024
- tmpFormToggle.className = 'pict-flow-popup-collapse-toggle';
1025
- tmpFormToggle.title = this._LayoutFormExpanded ? 'Hide parameters' : 'Show parameters';
1026
- tmpFormToggle.setAttribute('aria-expanded', this._LayoutFormExpanded ? 'true' : 'false');
1027
- let tmpIconProvider = this._FlowView ? this._FlowView._IconProvider : null;
1028
- tmpFormToggle.innerHTML = tmpIconProvider
1029
- ? tmpIconProvider.getIconSVGMarkup('settings', 13)
1030
- : '⚙';
1031
- tmpAlgoControls.appendChild(tmpFormToggle);
1032
- }
1033
937
 
1034
- tmpAlgoSection.appendChild(tmpAlgoControls);
1035
- pContainer.appendChild(tmpAlgoSection);
938
+ let tmpAlgoDescription = (tmpAlgoDescriptor && tmpAlgoDescriptor.Description)
939
+ ? '<div class="pict-flow-popup-control-description">' + tmpAlgoDescriptor.Description + '</div>'
940
+ : '';
1036
941
 
1037
- // ── Description (one-liner under the dropdown row) ───
1038
- if (tmpAlgoDescriptor && tmpAlgoDescriptor.Description)
942
+ let tmpFormToggleHTML = '';
943
+ if (tmpHasParameters)
1039
944
  {
1040
- let tmpDescDiv = document.createElement('div');
1041
- tmpDescDiv.className = 'pict-flow-popup-control-description';
1042
- tmpDescDiv.textContent = tmpAlgoDescriptor.Description;
1043
- pContainer.appendChild(tmpDescDiv);
945
+ tmpFormToggleHTML = '<button type="button" class="pict-flow-popup-collapse-toggle"'
946
+ + ' aria-expanded="' + (this._LayoutFormExpanded ? 'true' : 'false') + '"'
947
+ + ' title="' + (this._LayoutFormExpanded ? 'Hide parameters' : 'Show parameters') + '"'
948
+ + ' onclick="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._toggleLayoutFormCollapsed(this)">'
949
+ + tmpSettingsIconHTML
950
+ + '</button>';
1044
951
  }
1045
952
 
1046
- // ── Parameter form (collapsible, sits right under the
1047
- // algorithm row so the editor reads as part of the same
1048
- // "this is the algorithm" section) ───────────────────
1049
- this._buildLayoutParameterFormSection(pContainer, tmpAlgoDescriptor, tmpCurrentSettings);
953
+ let tmpAutoApplyID = `Flow-Toolbar-AutoApply-${tmpFlowViewIdentifier}`;
954
+ let tmpApplyDisabled = (tmpCurrentSettings.Algorithm === 'Custom') ? ' disabled' : '';
955
+ let tmpApplyTitle = (tmpCurrentSettings.Algorithm === 'Custom') ? ' title="Custom does not auto-position nodes"' : '';
1050
956
 
1051
- // Wire toggle now that the form host element exists
1052
- if (tmpFormToggle)
957
+ let tmpEdgeDescriptionHTML = '';
958
+ if (tmpResolvedEdge)
1053
959
  {
1054
- tmpFormToggle.addEventListener('click', (pEvent) =>
960
+ let tmpResolvedDescriptor = tmpLayoutService.getEdgeTheme(tmpResolvedEdge);
961
+ if (tmpResolvedDescriptor && tmpResolvedDescriptor.Description)
1055
962
  {
1056
- pEvent.stopPropagation();
1057
- this._LayoutFormExpanded = !this._LayoutFormExpanded;
1058
- tmpFormToggle.setAttribute('aria-expanded', this._LayoutFormExpanded ? 'true' : 'false');
1059
- tmpFormToggle.title = this._LayoutFormExpanded ? 'Hide parameters' : 'Show parameters';
1060
- let tmpHost = this._LayoutFormHostID ? document.getElementById(this._LayoutFormHostID) : null;
1061
- if (tmpHost)
1062
- {
1063
- tmpHost.setAttribute('data-collapsed', this._LayoutFormExpanded ? 'false' : 'true');
1064
- }
1065
- });
963
+ tmpEdgeDescriptionHTML = '<div class="pict-flow-popup-control-description">' + tmpResolvedDescriptor.Description + '</div>';
964
+ }
1066
965
  }
1067
966
 
1068
- // ── Edge theme dropdown ──────────────────────────────
1069
- let tmpEdgeDivider = document.createElement('div');
1070
- tmpEdgeDivider.className = 'pict-flow-popup-divider';
1071
- pContainer.appendChild(tmpEdgeDivider);
1072
-
1073
- let tmpEdgeSection = document.createElement('div');
1074
- tmpEdgeSection.className = 'pict-flow-popup-settings-section';
1075
-
1076
- let tmpEdgeLabel = document.createElement('label');
1077
- tmpEdgeLabel.className = 'pict-flow-popup-settings-label';
1078
- tmpEdgeLabel.textContent = 'Edge theme';
1079
- tmpEdgeSection.appendChild(tmpEdgeLabel);
1080
-
1081
- let tmpEdgeSelect = document.createElement('select');
1082
- tmpEdgeSelect.className = 'pict-flow-popup-settings-select';
1083
- tmpEdgeSelect.setAttribute('data-layout-control', 'edge-theme');
967
+ // Place a host div for the parameter form; mount the form into it
968
+ // after the popup is in the DOM (the metacontroller resolves
969
+ // destinations via document.querySelector).
970
+ let tmpParamFormHostID = `Flow-Toolbar-LayoutParamFormHost-${tmpFlowViewIdentifier}`;
971
+
972
+ this.pict.ContentAssignment.assignContent(pContainer,
973
+ '<div class="pict-flow-popup-settings-section pict-flow-popup-layout-algorithm-row">'
974
+ + '<label class="pict-flow-popup-settings-label">Algorithm</label>'
975
+ + '<div class="pict-flow-popup-layout-algorithm-controls">'
976
+ + '<select class="pict-flow-popup-settings-select pict-flow-popup-layout-algorithm-select" data-layout-control="algorithm"'
977
+ + ' onclick="event.stopPropagation()"'
978
+ + ' onchange="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._handleLayoutAlgoChange(this.value)">'
979
+ + tmpAlgoOptionsHTML
980
+ + '</select>'
981
+ + tmpFormToggleHTML
982
+ + '</div>'
983
+ + '</div>'
984
+ + tmpAlgoDescription
985
+ + '<div id="' + tmpParamFormHostID + '"></div>'
986
+ + '<div class="pict-flow-popup-divider"></div>'
987
+ + '<div class="pict-flow-popup-settings-section">'
988
+ + '<label class="pict-flow-popup-settings-label">Edge theme</label>'
989
+ + '<select class="pict-flow-popup-settings-select" data-layout-control="edge-theme"'
990
+ + ' onclick="event.stopPropagation()"'
991
+ + ' onchange="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._handleEdgeThemeChange(this.value)">'
992
+ + tmpEdgeOptionsHTML
993
+ + '</select>'
994
+ + '</div>'
995
+ + tmpEdgeDescriptionHTML
996
+ + '<div class="pict-flow-popup-divider"></div>'
997
+ + '<div class="pict-flow-popup-settings-section" style="display:flex;align-items:center;gap:8px;">'
998
+ + '<input type="checkbox" id="' + tmpAutoApplyID + '"' + (tmpCurrentSettings.AutoApply ? ' checked="checked"' : '') + ''
999
+ + ' onclick="event.stopPropagation()"'
1000
+ + ' onchange="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._handleAutoApplyChange(this.checked)" />'
1001
+ + '<label class="pict-flow-popup-settings-label" for="' + tmpAutoApplyID + '" style="cursor:pointer;">Auto-apply on changes</label>'
1002
+ + '</div>'
1003
+ + '<div class="pict-flow-popup-divider"></div>'
1004
+ + '<div class="pict-flow-popup-settings-section" style="padding:4px 8px;">'
1005
+ + '<button class="pict-flow-popup-layout-save-confirm" style="width:100%;padding:6px;"' + tmpApplyDisabled + tmpApplyTitle
1006
+ + ' onclick="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._handleApplyLayoutNow()">Apply Now</button>'
1007
+ + '</div>');
1008
+
1009
+ // Mount the parameter form (handles its own DOM building /
1010
+ // metacontroller). Use pContainer.querySelector — the popup may
1011
+ // not be in the document yet, so document.getElementById would miss.
1012
+ let tmpFormHostEl = pContainer.querySelector('#' + tmpParamFormHostID);
1013
+ if (tmpFormHostEl)
1014
+ {
1015
+ this._buildLayoutParameterFormSection(tmpFormHostEl, tmpAlgoDescriptor, tmpCurrentSettings);
1016
+ }
1017
+ }
1084
1018
 
1085
- let tmpEdgeSettings = this._FlowView.getEdgeTheme();
1086
- let tmpResolvedEdge = tmpEdgeSettings.Theme;
1087
- let tmpExplicitEdgeOverride = tmpEdgeSettings.Override;
1019
+ /**
1020
+ * Inline handler — switch layout algorithm, refresh popup.
1021
+ */
1022
+ _handleLayoutAlgoChange(pName)
1023
+ {
1024
+ if (!this._FlowView || !this._FlowView._LayoutService) return;
1025
+ let tmpAlgo = this._FlowView._LayoutService.getAlgorithm(pName);
1026
+ let tmpDefaults = (tmpAlgo && tmpAlgo.DefaultParameters)
1027
+ ? JSON.parse(JSON.stringify(tmpAlgo.DefaultParameters)) : {};
1028
+ this._FlowView.setLayoutAlgorithm(pName, tmpDefaults);
1088
1029
 
1089
- // Inherit option — falls back to the active layout's DefaultEdgeTheme
1090
- let tmpInheritOpt = document.createElement('option');
1091
- tmpInheritOpt.value = '__inherit__';
1092
- let tmpDefaultThemeName = (tmpAlgoDescriptor && tmpAlgoDescriptor.DefaultEdgeTheme) || 'Bezier';
1093
- tmpInheritOpt.textContent = `Inherit from layout (${tmpDefaultThemeName})`;
1094
- if (!tmpExplicitEdgeOverride) tmpInheritOpt.selected = true;
1095
- tmpEdgeSelect.appendChild(tmpInheritOpt);
1030
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
1031
+ let tmpPopupEl = document.getElementById(`Flow-Toolbar-Popup-${tmpFlowViewIdentifier}`);
1032
+ if (tmpPopupEl) this._buildLayoutAlgorithmPopup(tmpPopupEl);
1033
+ }
1096
1034
 
1097
- let tmpEdgeThemes = tmpLayoutService.listEdgeThemes();
1098
- for (let i = 0; i < tmpEdgeThemes.length; i++)
1035
+ /**
1036
+ * Inline handler switch edge theme, refresh popup so descriptions
1037
+ * track the new theme.
1038
+ */
1039
+ _handleEdgeThemeChange(pValue)
1040
+ {
1041
+ if (!this._FlowView) return;
1042
+ if (pValue === '__inherit__')
1099
1043
  {
1100
- let tmpOpt = document.createElement('option');
1101
- tmpOpt.value = tmpEdgeThemes[i].Name;
1102
- tmpOpt.textContent = tmpEdgeThemes[i].Label || tmpEdgeThemes[i].Name;
1103
- if (tmpEdgeThemes[i].Name === tmpExplicitEdgeOverride)
1104
- {
1105
- tmpOpt.selected = true;
1106
- }
1107
- tmpEdgeSelect.appendChild(tmpOpt);
1044
+ this._FlowView.setEdgeTheme(null);
1108
1045
  }
1109
-
1110
- tmpEdgeSelect.addEventListener('change', () =>
1111
- {
1112
- let tmpVal = tmpEdgeSelect.value;
1113
- if (tmpVal === '__inherit__')
1114
- {
1115
- this._FlowView.setEdgeTheme(null);
1116
- }
1117
- else
1118
- {
1119
- this._FlowView.setEdgeTheme(tmpVal);
1120
- }
1121
- // Rebuild the popup so the description and any theme-specific
1122
- // controls reflect the new theme.
1123
- while (pContainer.firstChild)
1124
- {
1125
- pContainer.removeChild(pContainer.firstChild);
1126
- }
1127
- this._buildLayoutAlgorithmPopup(pContainer);
1128
- });
1129
- tmpEdgeSelect.addEventListener('click', (pEvent) => { pEvent.stopPropagation(); });
1130
-
1131
- tmpEdgeSection.appendChild(tmpEdgeSelect);
1132
- pContainer.appendChild(tmpEdgeSection);
1133
-
1134
- // Edge theme description (uses the *resolved* theme so users see
1135
- // what's actually rendering, regardless of inherit/explicit choice)
1136
- if (tmpResolvedEdge)
1046
+ else
1137
1047
  {
1138
- let tmpResolvedDescriptor = tmpLayoutService.getEdgeTheme(tmpResolvedEdge);
1139
- if (tmpResolvedDescriptor && tmpResolvedDescriptor.Description)
1140
- {
1141
- let tmpEdgeDesc = document.createElement('div');
1142
- tmpEdgeDesc.className = 'pict-flow-popup-control-description';
1143
- tmpEdgeDesc.textContent = tmpResolvedDescriptor.Description;
1144
- pContainer.appendChild(tmpEdgeDesc);
1145
- }
1048
+ this._FlowView.setEdgeTheme(pValue);
1146
1049
  }
1147
1050
 
1148
- // ── Auto-apply toggle ────────────────────────────────
1149
- let tmpAutoApplyDivider = document.createElement('div');
1150
- tmpAutoApplyDivider.className = 'pict-flow-popup-divider';
1151
- pContainer.appendChild(tmpAutoApplyDivider);
1051
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
1052
+ let tmpPopupEl = document.getElementById(`Flow-Toolbar-Popup-${tmpFlowViewIdentifier}`);
1053
+ if (tmpPopupEl) this._buildLayoutAlgorithmPopup(tmpPopupEl);
1054
+ }
1152
1055
 
1153
- let tmpAutoApplySection = document.createElement('div');
1154
- tmpAutoApplySection.className = 'pict-flow-popup-settings-section';
1155
- tmpAutoApplySection.style.display = 'flex';
1156
- tmpAutoApplySection.style.alignItems = 'center';
1157
- tmpAutoApplySection.style.gap = '8px';
1056
+ /**
1057
+ * Inline handler — toggle auto-apply.
1058
+ */
1059
+ _handleAutoApplyChange(pChecked)
1060
+ {
1061
+ if (!this._FlowView) return;
1062
+ this._FlowView.setLayoutAutoApply(pChecked);
1063
+ }
1064
+
1065
+ /**
1066
+ * Inline handler — fire the layout (when not Custom).
1067
+ */
1068
+ _handleApplyLayoutNow()
1069
+ {
1070
+ if (!this._FlowView) return;
1071
+ let tmpSettings = this._FlowView.getLayoutAlgorithm();
1072
+ if (tmpSettings.Algorithm === 'Custom') return;
1073
+ this._FlowView.applyCurrentLayout();
1074
+ }
1158
1075
 
1159
- let tmpAutoApplyCheckbox = document.createElement('input');
1160
- tmpAutoApplyCheckbox.type = 'checkbox';
1161
- tmpAutoApplyCheckbox.id = `Flow-Toolbar-AutoApply-${this.options.FlowViewIdentifier}`;
1162
- tmpAutoApplyCheckbox.checked = !!tmpCurrentSettings.AutoApply;
1163
- tmpAutoApplyCheckbox.addEventListener('change', () =>
1076
+ /**
1077
+ * Inline handler — toggle the parameter-form collapse state.
1078
+ */
1079
+ _toggleLayoutFormCollapsed(pToggleButton)
1080
+ {
1081
+ this._LayoutFormExpanded = !this._LayoutFormExpanded;
1082
+ pToggleButton.setAttribute('aria-expanded', this._LayoutFormExpanded ? 'true' : 'false');
1083
+ pToggleButton.title = this._LayoutFormExpanded ? 'Hide parameters' : 'Show parameters';
1084
+ let tmpHost = this._LayoutFormHostID ? document.getElementById(this._LayoutFormHostID) : null;
1085
+ if (tmpHost)
1164
1086
  {
1165
- this._FlowView.setLayoutAutoApply(tmpAutoApplyCheckbox.checked);
1166
- });
1167
- tmpAutoApplyCheckbox.addEventListener('click', (pEvent) => { pEvent.stopPropagation(); });
1168
-
1169
- let tmpAutoApplyLabel = document.createElement('label');
1170
- tmpAutoApplyLabel.className = 'pict-flow-popup-settings-label';
1171
- tmpAutoApplyLabel.setAttribute('for', tmpAutoApplyCheckbox.id);
1172
- tmpAutoApplyLabel.textContent = 'Auto-apply on changes';
1173
- tmpAutoApplyLabel.style.cursor = 'pointer';
1174
-
1175
- tmpAutoApplySection.appendChild(tmpAutoApplyCheckbox);
1176
- tmpAutoApplySection.appendChild(tmpAutoApplyLabel);
1177
- pContainer.appendChild(tmpAutoApplySection);
1178
-
1179
- // (Parameter form is rendered above, right under the algorithm row,
1180
- // so it reads as part of the same "this is the algorithm" section.)
1181
-
1182
- // ── Apply Now button ─────────────────────────────────
1183
- let tmpApplyDivider = document.createElement('div');
1184
- tmpApplyDivider.className = 'pict-flow-popup-divider';
1185
- pContainer.appendChild(tmpApplyDivider);
1186
-
1187
- let tmpApplyRow = document.createElement('div');
1188
- tmpApplyRow.className = 'pict-flow-popup-settings-section';
1189
- tmpApplyRow.style.padding = '4px 8px';
1190
-
1191
- let tmpApplyBtn = document.createElement('button');
1192
- tmpApplyBtn.className = 'pict-flow-popup-layout-save-confirm';
1193
- tmpApplyBtn.textContent = 'Apply Now';
1194
- tmpApplyBtn.style.width = '100%';
1195
- tmpApplyBtn.style.padding = '6px';
1196
- if (tmpCurrentSettings.Algorithm === 'Custom')
1197
- {
1198
- tmpApplyBtn.disabled = true;
1199
- tmpApplyBtn.title = 'Custom does not auto-position nodes';
1087
+ tmpHost.setAttribute('data-collapsed', this._LayoutFormExpanded ? 'false' : 'true');
1200
1088
  }
1201
- tmpApplyBtn.addEventListener('click', () =>
1202
- {
1203
- if (tmpCurrentSettings.Algorithm === 'Custom') return;
1204
- this._FlowView.applyCurrentLayout();
1205
- });
1206
- tmpApplyRow.appendChild(tmpApplyBtn);
1207
- pContainer.appendChild(tmpApplyRow);
1208
1089
  }
1209
1090
 
1210
1091
  /**
@@ -1346,7 +1227,9 @@ class PictViewFlowToolbar extends libPictView
1346
1227
  tmpDivider.className = 'pict-flow-popup-divider';
1347
1228
  pContainer.appendChild(tmpDivider);
1348
1229
 
1349
- // Form host div — the metacontroller renders into here
1230
+ // Form host div — the metacontroller renders into here. Inline
1231
+ // onclick stops popup-close propagation; onchange/oninput drive the
1232
+ // _FlowData.LayoutParameters writeback (see _pushFormBack).
1350
1233
  let tmpHostID = `Flow-Toolbar-LayoutForm-${tmpFlowViewIdentifier}`;
1351
1234
  this._LayoutFormHostID = tmpHostID;
1352
1235
 
@@ -1354,8 +1237,11 @@ class PictViewFlowToolbar extends libPictView
1354
1237
  tmpHostDiv.id = tmpHostID;
1355
1238
  tmpHostDiv.className = 'pict-flow-popup-layout-form-host';
1356
1239
  tmpHostDiv.setAttribute('data-collapsed', this._LayoutFormExpanded ? 'false' : 'true');
1357
- // Clicks inside the form must NOT close the popup
1358
- tmpHostDiv.addEventListener('click', (pEvent) => { pEvent.stopPropagation(); });
1240
+ tmpHostDiv.setAttribute('onclick', 'event.stopPropagation()');
1241
+ tmpHostDiv.setAttribute('onchange',
1242
+ "_Pict.views['" + tmpFlowViewIdentifier + "']._ToolbarView._pushLayoutFormBack('" + pCurrentSettings.Algorithm + "')");
1243
+ tmpHostDiv.setAttribute('oninput',
1244
+ "_Pict.views['" + tmpFlowViewIdentifier + "']._ToolbarView._pushLayoutFormBack('" + pCurrentSettings.Algorithm + "')");
1359
1245
  pContainer.appendChild(tmpHostDiv);
1360
1246
 
1361
1247
  // Bind the layout parameters as the data source for the form.
@@ -1467,28 +1353,32 @@ class PictViewFlowToolbar extends libPictView
1467
1353
  return;
1468
1354
  }
1469
1355
 
1470
- // Push form changes back into _FlowData.LayoutParameters and re-apply
1471
- // the layout (when non-Custom). One listener at the host level catches
1472
- // both `change` (for selects/numbers/checkboxes) and `input` (for the
1473
- // live-update case). De-bounce via micro-task so multi-key edits
1474
- // resolve to a single re-layout pass. Read from the algorithm-scoped
1475
- // AppData branch (matches the UUID-prefixed descriptor addresses).
1476
- let tmpScheduled = false;
1477
- let tmpPushBack = () =>
1478
- {
1479
- if (tmpScheduled) return;
1480
- tmpScheduled = true;
1481
- Promise.resolve().then(() =>
1482
- {
1483
- tmpScheduled = false;
1484
- let tmpScopedRoot = this.pict.AppData[tmpScope] || {};
1485
- let tmpEditorParams = (tmpScopedRoot.PictFlowLayoutEditor || {}).Parameters || {};
1486
- let tmpMerged = Object.assign({}, this._FlowView.getLayoutAlgorithm().Parameters || {}, tmpEditorParams);
1487
- this._FlowView.setLayoutAlgorithm(pCurrentSettings.Algorithm, tmpMerged);
1488
- });
1489
- };
1490
- tmpHostDiv.addEventListener('change', tmpPushBack);
1491
- tmpHostDiv.addEventListener('input', tmpPushBack);
1356
+ // Form-change writeback runs through the host div's inline
1357
+ // onchange/oninput attributes (set above), which call
1358
+ // `_pushLayoutFormBack(algorithm)`.
1359
+ }
1360
+
1361
+ /**
1362
+ * Inline handler for layout-parameter form changes. Pushes Informary's
1363
+ * write into AppData back into `_FlowData.LayoutParameters` and
1364
+ * re-applies the layout (when non-Custom). De-bounces via microtask so
1365
+ * multi-key edits collapse to a single layout pass.
1366
+ *
1367
+ * @param {string} pAlgorithm
1368
+ */
1369
+ _pushLayoutFormBack(pAlgorithm)
1370
+ {
1371
+ if (this._LayoutFormPushScheduled) return;
1372
+ this._LayoutFormPushScheduled = true;
1373
+ Promise.resolve().then(() =>
1374
+ {
1375
+ this._LayoutFormPushScheduled = false;
1376
+ if (!this._FlowView) return;
1377
+ let tmpScopedRoot = this.pict.AppData[pAlgorithm] || {};
1378
+ let tmpEditorParams = (tmpScopedRoot.PictFlowLayoutEditor || {}).Parameters || {};
1379
+ let tmpMerged = Object.assign({}, this._FlowView.getLayoutAlgorithm().Parameters || {}, tmpEditorParams);
1380
+ this._FlowView.setLayoutAlgorithm(pAlgorithm, tmpMerged);
1381
+ });
1492
1382
  }
1493
1383
 
1494
1384
  /**
@@ -1503,6 +1393,9 @@ class PictViewFlowToolbar extends libPictView
1503
1393
  */
1504
1394
  _buildLayoutParamInput(pKey, pSchema, pCurrentValue)
1505
1395
  {
1396
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
1397
+ let tmpViewPath = "_Pict.views['" + tmpFlowViewIdentifier + "']._ToolbarView";
1398
+ let tmpKeyEsc = pKey.replace(/'/g, "\\'");
1506
1399
  let tmpInput;
1507
1400
 
1508
1401
  if (pSchema.Type === 'boolean')
@@ -1510,10 +1403,8 @@ class PictViewFlowToolbar extends libPictView
1510
1403
  tmpInput = document.createElement('input');
1511
1404
  tmpInput.type = 'checkbox';
1512
1405
  tmpInput.checked = !!pCurrentValue;
1513
- tmpInput.addEventListener('change', () =>
1514
- {
1515
- this._updateLayoutParameter(pKey, tmpInput.checked);
1516
- });
1406
+ tmpInput.setAttribute('onchange',
1407
+ tmpViewPath + "._updateLayoutParameter('" + tmpKeyEsc + "', this.checked)");
1517
1408
  }
1518
1409
  else if (pSchema.Type === 'enum' && Array.isArray(pSchema.Options))
1519
1410
  {
@@ -1527,42 +1418,23 @@ class PictViewFlowToolbar extends libPictView
1527
1418
  if (pSchema.Options[i] === pCurrentValue) tmpOpt.selected = true;
1528
1419
  tmpInput.appendChild(tmpOpt);
1529
1420
  }
1530
- tmpInput.addEventListener('change', () =>
1531
- {
1532
- this._updateLayoutParameter(pKey, tmpInput.value);
1533
- });
1421
+ tmpInput.setAttribute('onchange',
1422
+ tmpViewPath + "._updateLayoutParameter('" + tmpKeyEsc + "', this.value)");
1534
1423
  }
1535
1424
  else if (pSchema.Type === 'number' || pSchema.Type === 'Number' || pSchema.Type === 'PreciseNumber')
1536
1425
  {
1537
1426
  tmpInput = document.createElement('input');
1538
1427
  tmpInput.type = 'number';
1539
1428
  tmpInput.className = 'pict-flow-popup-settings-input';
1540
- // PreciseNumber: arbitrary precision (string-stored, big.js / ExpressionParser-friendly).
1541
- // Number: integer or simple float.
1542
1429
  let tmpIsPrecise = (pSchema.Type === 'PreciseNumber');
1543
1430
  if (typeof pSchema.Min === 'number') tmpInput.min = String(pSchema.Min);
1544
1431
  if (typeof pSchema.Max === 'number') tmpInput.max = String(pSchema.Max);
1545
1432
  tmpInput.step = tmpIsPrecise ? 'any' : '1';
1546
- let tmpDisplayValue = (pCurrentValue == null) ? '' : String(pCurrentValue);
1547
- tmpInput.value = tmpDisplayValue;
1433
+ tmpInput.value = (pCurrentValue == null) ? '' : String(pCurrentValue);
1548
1434
  tmpInput.style.width = '90px';
1549
- tmpInput.addEventListener('change', () =>
1550
- {
1551
- let tmpRaw = tmpInput.value;
1552
- if (tmpRaw === '') return;
1553
- if (tmpIsPrecise)
1554
- {
1555
- // Preserve the user-entered string so big.js / ExpressionParser
1556
- // can use it without float round-trip drift.
1557
- this._updateLayoutParameter(pKey, tmpRaw);
1558
- }
1559
- else
1560
- {
1561
- let tmpNum = parseFloat(tmpRaw);
1562
- if (isNaN(tmpNum)) return;
1563
- this._updateLayoutParameter(pKey, tmpNum);
1564
- }
1565
- });
1435
+ let tmpPreciseFlag = tmpIsPrecise ? 'true' : 'false';
1436
+ tmpInput.setAttribute('onchange',
1437
+ tmpViewPath + "._updateLayoutNumberParameter('" + tmpKeyEsc + "', this.value, " + tmpPreciseFlag + ")");
1566
1438
  }
1567
1439
  else
1568
1440
  {
@@ -1571,14 +1443,11 @@ class PictViewFlowToolbar extends libPictView
1571
1443
  tmpInput.className = 'pict-flow-popup-settings-input';
1572
1444
  tmpInput.value = (pCurrentValue == null) ? '' : String(pCurrentValue);
1573
1445
  tmpInput.style.width = '90px';
1574
- tmpInput.addEventListener('change', () =>
1575
- {
1576
- this._updateLayoutParameter(pKey, tmpInput.value);
1577
- });
1446
+ tmpInput.setAttribute('onchange',
1447
+ tmpViewPath + "._updateLayoutParameter('" + tmpKeyEsc + "', this.value)");
1578
1448
  }
1579
1449
 
1580
- tmpInput.addEventListener('click', (pEvent) => { pEvent.stopPropagation(); });
1581
-
1450
+ tmpInput.setAttribute('onclick', 'event.stopPropagation()');
1582
1451
  return tmpInput;
1583
1452
  }
1584
1453
 
@@ -1597,108 +1466,114 @@ class PictViewFlowToolbar extends libPictView
1597
1466
  this._FlowView.setLayoutAlgorithm(tmpSettings.Algorithm, tmpParams);
1598
1467
  }
1599
1468
 
1469
+ /**
1470
+ * Inline-handler helper for numeric parameter inputs. Preserves the
1471
+ * raw string when PreciseNumber so big.js / ExpressionParser can use
1472
+ * it without float round-trip drift.
1473
+ *
1474
+ * @param {string} pKey
1475
+ * @param {string} pRawValue
1476
+ * @param {boolean} pIsPrecise
1477
+ */
1478
+ _updateLayoutNumberParameter(pKey, pRawValue, pIsPrecise)
1479
+ {
1480
+ if (pRawValue === '') return;
1481
+ if (pIsPrecise)
1482
+ {
1483
+ this._updateLayoutParameter(pKey, pRawValue);
1484
+ return;
1485
+ }
1486
+ let tmpNum = parseFloat(pRawValue);
1487
+ if (isNaN(tmpNum)) return;
1488
+ this._updateLayoutParameter(pKey, tmpNum);
1489
+ }
1490
+
1600
1491
  // ── Settings Popup ───────────────────────────────────────────────────
1601
1492
 
1602
1493
  /**
1603
- * Build the Settings popup content (theme dropdown + noise slider).
1494
+ * Build the Settings popup using a template with inline handlers.
1495
+ *
1496
+ * Theme options are iterated via `{~TS:~}` from AppData. Sliders and
1497
+ * selects fire inline handlers that update the FlowView's theme state.
1498
+ *
1604
1499
  * @param {HTMLElement} pContainer
1605
1500
  */
1606
1501
  _buildSettingsPopup(pContainer)
1607
1502
  {
1608
1503
  if (!this._FlowView || !this._FlowView._ThemeProvider) return;
1609
1504
 
1505
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
1610
1506
  let tmpThemeProvider = this._FlowView._ThemeProvider;
1611
-
1612
- // Theme selector section
1613
- let tmpThemeSection = document.createElement('div');
1614
- tmpThemeSection.className = 'pict-flow-popup-settings-section';
1615
-
1616
- let tmpThemeLabel = document.createElement('label');
1617
- tmpThemeLabel.className = 'pict-flow-popup-settings-label';
1618
- tmpThemeLabel.textContent = 'Theme';
1619
- tmpThemeSection.appendChild(tmpThemeLabel);
1620
-
1621
- let tmpThemeSelect = document.createElement('select');
1622
- tmpThemeSelect.className = 'pict-flow-popup-settings-select';
1623
-
1624
1507
  let tmpThemeKeys = tmpThemeProvider.getThemeKeys();
1625
1508
  let tmpActiveKey = tmpThemeProvider.getActiveThemeKey();
1626
1509
 
1510
+ let tmpThemeOptions = [];
1627
1511
  for (let i = 0; i < tmpThemeKeys.length; i++)
1628
1512
  {
1629
- let tmpOption = document.createElement('option');
1630
- tmpOption.value = tmpThemeKeys[i];
1631
-
1632
1513
  let tmpTheme = tmpThemeProvider._Themes[tmpThemeKeys[i]];
1633
- tmpOption.textContent = tmpTheme.Label || tmpThemeKeys[i];
1634
-
1635
- if (tmpThemeKeys[i] === tmpActiveKey)
1514
+ tmpThemeOptions.push(
1636
1515
  {
1637
- tmpOption.selected = true;
1638
- }
1639
- tmpThemeSelect.appendChild(tmpOption);
1516
+ Value: tmpThemeKeys[i],
1517
+ Label: tmpTheme.Label || tmpThemeKeys[i],
1518
+ SelectedAttr: (tmpThemeKeys[i] === tmpActiveKey) ? ' selected="selected"' : ''
1519
+ });
1640
1520
  }
1641
1521
 
1642
- tmpThemeSelect.addEventListener('change', () =>
1643
- {
1644
- this._FlowView.setTheme(tmpThemeSelect.value);
1645
- // Refresh the noise slider visibility
1646
- this._refreshNoiseSlider(pContainer);
1647
- });
1648
-
1649
- // Prevent popup close on select interaction
1650
- tmpThemeSelect.addEventListener('click', (pEvent) => { pEvent.stopPropagation(); });
1651
-
1652
- tmpThemeSection.appendChild(tmpThemeSelect);
1653
- pContainer.appendChild(tmpThemeSection);
1654
-
1655
- // Divider
1656
- let tmpDivider = document.createElement('div');
1657
- tmpDivider.className = 'pict-flow-popup-divider';
1658
- pContainer.appendChild(tmpDivider);
1659
-
1660
- // Noise level section
1661
- let tmpNoiseSection = document.createElement('div');
1662
- tmpNoiseSection.className = 'pict-flow-popup-settings-section pict-flow-popup-settings-noise';
1663
- tmpNoiseSection.setAttribute('data-settings-type', 'noise');
1664
-
1665
- let tmpNoiseLabel = document.createElement('label');
1666
- tmpNoiseLabel.className = 'pict-flow-popup-settings-label';
1667
- tmpNoiseLabel.textContent = 'Noise';
1668
- tmpNoiseSection.appendChild(tmpNoiseLabel);
1669
-
1670
- let tmpNoiseRow = document.createElement('div');
1671
- tmpNoiseRow.className = 'pict-flow-popup-settings-slider-row';
1672
-
1673
- let tmpNoiseSlider = document.createElement('input');
1674
- tmpNoiseSlider.type = 'range';
1675
- tmpNoiseSlider.className = 'pict-flow-popup-settings-slider';
1676
- tmpNoiseSlider.min = '0';
1677
- tmpNoiseSlider.max = '100';
1678
- tmpNoiseSlider.value = String(Math.round(tmpThemeProvider.getNoiseLevel() * 100));
1679
-
1680
- let tmpNoiseValue = document.createElement('span');
1681
- tmpNoiseValue.className = 'pict-flow-popup-settings-slider-value';
1682
- tmpNoiseValue.textContent = tmpNoiseSlider.value + '%';
1683
-
1684
- tmpNoiseSlider.addEventListener('input', () =>
1685
- {
1686
- let tmpLevel = parseInt(tmpNoiseSlider.value, 10) / 100;
1687
- tmpNoiseValue.textContent = tmpNoiseSlider.value + '%';
1688
- this._FlowView.setNoiseLevel(tmpLevel);
1689
- });
1522
+ let tmpThemeOptionsHTML = this.pict.parseTemplateByHash('Flow-Layout-OptionList', { Options: tmpThemeOptions });
1523
+
1524
+ let tmpNoiseLevel = Math.round(tmpThemeProvider.getNoiseLevel() * 100);
1525
+ let tmpActiveTheme = tmpThemeProvider.getActiveTheme();
1526
+ let tmpNoiseEnabled = !!(tmpActiveTheme && tmpActiveTheme.NoiseConfig && tmpActiveTheme.NoiseConfig.Enabled);
1527
+ let tmpNoiseDisplay = tmpNoiseEnabled ? '' : 'display:none;';
1528
+
1529
+ this.pict.ContentAssignment.assignContent(pContainer,
1530
+ '<div class="pict-flow-popup-settings-section">'
1531
+ + '<label class="pict-flow-popup-settings-label">Theme</label>'
1532
+ + '<select class="pict-flow-popup-settings-select"'
1533
+ + ' onclick="event.stopPropagation()"'
1534
+ + ' onchange="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._handleThemeSelectChange(this.value)">'
1535
+ + tmpThemeOptionsHTML
1536
+ + '</select>'
1537
+ + '</div>'
1538
+ + '<div class="pict-flow-popup-divider"></div>'
1539
+ + '<div class="pict-flow-popup-settings-section pict-flow-popup-settings-noise" data-settings-type="noise" style="' + tmpNoiseDisplay + '">'
1540
+ + '<label class="pict-flow-popup-settings-label">Noise</label>'
1541
+ + '<div class="pict-flow-popup-settings-slider-row">'
1542
+ + '<input type="range" class="pict-flow-popup-settings-slider" min="0" max="100" value="' + tmpNoiseLevel + '"'
1543
+ + ' onclick="event.stopPropagation()"'
1544
+ + ' onpointerdown="event.stopPropagation()"'
1545
+ + ' oninput="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._handleNoiseSliderInput(this)" />'
1546
+ + '<span class="pict-flow-popup-settings-slider-value">' + tmpNoiseLevel + '%</span>'
1547
+ + '</div>'
1548
+ + '</div>');
1549
+ }
1690
1550
 
1691
- // Prevent popup close on slider interaction
1692
- tmpNoiseSlider.addEventListener('click', (pEvent) => { pEvent.stopPropagation(); });
1693
- tmpNoiseSlider.addEventListener('pointerdown', (pEvent) => { pEvent.stopPropagation(); });
1551
+ /**
1552
+ * Inline handler apply selected flow theme and refresh the noise
1553
+ * slider visibility (some themes don't expose noise).
1554
+ */
1555
+ _handleThemeSelectChange(pThemeKey)
1556
+ {
1557
+ if (!this._FlowView) return;
1558
+ this._FlowView.setTheme(pThemeKey);
1694
1559
 
1695
- tmpNoiseRow.appendChild(tmpNoiseSlider);
1696
- tmpNoiseRow.appendChild(tmpNoiseValue);
1697
- tmpNoiseSection.appendChild(tmpNoiseRow);
1698
- pContainer.appendChild(tmpNoiseSection);
1560
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
1561
+ let tmpPopupEl = document.getElementById(`Flow-Toolbar-Popup-${tmpFlowViewIdentifier}`);
1562
+ if (tmpPopupEl) this._refreshNoiseSlider(tmpPopupEl);
1563
+ }
1699
1564
 
1700
- // Show/hide noise slider based on active theme
1701
- this._refreshNoiseSlider(pContainer);
1565
+ /**
1566
+ * Inline handler — push slider position into the noise level and
1567
+ * update the percentage label adjacent to it.
1568
+ */
1569
+ _handleNoiseSliderInput(pSlider)
1570
+ {
1571
+ let tmpRaw = parseInt(pSlider.value, 10);
1572
+ if (isNaN(tmpRaw)) return;
1573
+ let tmpLevel = tmpRaw / 100;
1574
+ let tmpValueLabel = pSlider.parentNode ? pSlider.parentNode.querySelector('.pict-flow-popup-settings-slider-value') : null;
1575
+ if (tmpValueLabel) tmpValueLabel.textContent = tmpRaw + '%';
1576
+ if (this._FlowView) this._FlowView.setNoiseLevel(tmpLevel);
1702
1577
  }
1703
1578
 
1704
1579
  /**