@zolomedia/bifrost-client 1.7.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/L1_Foundation/L1_Foundation.js +13 -0
- package/L1_Foundation/bootstrap/bootstrap.js +11 -0
- package/L1_Foundation/bootstrap/bootstrap_hooks.js +123 -0
- package/L1_Foundation/bootstrap/bootstrap_index.js +15 -0
- package/L1_Foundation/bootstrap/bootstrap_logger.js +135 -0
- package/L1_Foundation/bootstrap/cdn_loader.js +217 -0
- package/L1_Foundation/bootstrap/module_registry.js +102 -0
- package/L1_Foundation/bootstrap/prism_loader.js +164 -0
- package/L1_Foundation/config/client_config.js +110 -0
- package/L1_Foundation/config/config.js +7 -0
- package/L1_Foundation/connection/connection.js +8 -0
- package/L1_Foundation/connection/websocket_connection.js +122 -0
- package/L1_Foundation/constants/bifrost_constants.js +284 -0
- package/L1_Foundation/constants/constants.js +7 -0
- package/L1_Foundation/logger/logger.js +10 -0
- package/L2_Handling/L2_Handling.js +15 -0
- package/L2_Handling/cache/cache.js +22 -0
- package/L2_Handling/cache/cache_constants.js +69 -0
- package/L2_Handling/cache/orchestration/cache_manager.js +299 -0
- package/L2_Handling/cache/orchestration/cache_orchestrator.js +260 -0
- package/L2_Handling/cache/orchestration/orchestration.js +12 -0
- package/L2_Handling/cache/storage/session_manager.js +289 -0
- package/L2_Handling/cache/storage/storage.js +10 -0
- package/L2_Handling/cache/storage/storage_manager.js +590 -0
- package/L2_Handling/display/composite/composite.js +13 -0
- package/L2_Handling/display/composite/dashboard_renderer.js +221 -0
- package/L2_Handling/display/composite/swiper_renderer.js +564 -0
- package/L2_Handling/display/composite/terminal_renderer.js +922 -0
- package/L2_Handling/display/composite/wizard_conditional_renderer.js +274 -0
- package/L2_Handling/display/display.js +30 -0
- package/L2_Handling/display/feedback/feedback.js +11 -0
- package/L2_Handling/display/feedback/progressbar_renderer.js +418 -0
- package/L2_Handling/display/feedback/spinner_renderer.js +246 -0
- package/L2_Handling/display/inputs/button_renderer.js +634 -0
- package/L2_Handling/display/inputs/form_renderer.js +583 -0
- package/L2_Handling/display/inputs/input_renderer.js +658 -0
- package/L2_Handling/display/inputs/inputs.js +12 -0
- package/L2_Handling/display/navigation/menu_renderer.js +206 -0
- package/L2_Handling/display/navigation/navigation.js +11 -0
- package/L2_Handling/display/navigation/navigation_renderer.js +703 -0
- package/L2_Handling/display/orchestration/orchestration.js +11 -0
- package/L2_Handling/display/orchestration/renderer.js +430 -0
- package/L2_Handling/display/orchestration/zdisplay_orchestrator.js +1759 -0
- package/L2_Handling/display/outputs/alert_renderer.js +161 -0
- package/L2_Handling/display/outputs/audio_renderer.js +94 -0
- package/L2_Handling/display/outputs/card_renderer.js +229 -0
- package/L2_Handling/display/outputs/code_renderer.js +66 -0
- package/L2_Handling/display/outputs/dl_renderer.js +131 -0
- package/L2_Handling/display/outputs/header_renderer.js +162 -0
- package/L2_Handling/display/outputs/icon_renderer.js +107 -0
- package/L2_Handling/display/outputs/image_renderer.js +145 -0
- package/L2_Handling/display/outputs/list_renderer.js +190 -0
- package/L2_Handling/display/outputs/outputs.js +19 -0
- package/L2_Handling/display/outputs/table_renderer.js +765 -0
- package/L2_Handling/display/outputs/text_renderer.js +818 -0
- package/L2_Handling/display/outputs/typography_renderer.js +293 -0
- package/L2_Handling/display/outputs/video_renderer.js +116 -0
- package/L2_Handling/display/primitives/document_structure_primitives.js +319 -0
- package/L2_Handling/display/primitives/form_primitives.js +526 -0
- package/L2_Handling/display/primitives/generic_containers.js +109 -0
- package/L2_Handling/display/primitives/interactive_primitives.js +305 -0
- package/L2_Handling/display/primitives/link_primitives.js +552 -0
- package/L2_Handling/display/primitives/lists_primitives.js +262 -0
- package/L2_Handling/display/primitives/media_primitives.js +383 -0
- package/L2_Handling/display/primitives/primitives.js +19 -0
- package/L2_Handling/display/primitives/semantic_element_primitive.js +226 -0
- package/L2_Handling/display/primitives/table_primitives.js +528 -0
- package/L2_Handling/display/primitives/typography_primitives.js +175 -0
- package/L2_Handling/display/specialized/input_request_renderer.js +467 -0
- package/L2_Handling/display/specialized/specialized.js +10 -0
- package/L2_Handling/hooks/hooks.js +9 -0
- package/L2_Handling/hooks/menu_integration.js +57 -0
- package/L2_Handling/hooks/widget_hook_manager.js +292 -0
- package/L2_Handling/message/message.js +8 -0
- package/L2_Handling/message/message_handler.js +701 -0
- package/L2_Handling/navigation/navigation.js +8 -0
- package/L2_Handling/navigation/navigation_manager.js +403 -0
- package/L2_Handling/zhooks/features/cache_live.js +287 -0
- package/L2_Handling/zhooks/features/crumbs_live.js +292 -0
- package/L2_Handling/zhooks/zhooks_manager.js +65 -0
- package/L2_Handling/zvaf/zvaf.js +8 -0
- package/L2_Handling/zvaf/zvaf_manager.js +334 -0
- package/L3_Abstraction/L3_Abstraction.js +12 -0
- package/L3_Abstraction/orchestrator/container_unwrapper.js +101 -0
- package/L3_Abstraction/orchestrator/group_renderer.js +698 -0
- package/L3_Abstraction/orchestrator/input_event_handler.js +797 -0
- package/L3_Abstraction/orchestrator/metadata_processor.js +249 -0
- package/L3_Abstraction/orchestrator/navbar_builder.js +201 -0
- package/L3_Abstraction/orchestrator/orchestrator.js +13 -0
- package/L3_Abstraction/orchestrator/wizard_gate_handler.js +360 -0
- package/L3_Abstraction/renderer/renderer.js +1 -0
- package/L3_Abstraction/session/session.js +1 -0
- package/L4_Orchestration/L4_Orchestration.js +11 -0
- package/L4_Orchestration/client/client.js +1 -0
- package/L4_Orchestration/facade/facade.js +9 -0
- package/L4_Orchestration/facade/manager_registry.js +118 -0
- package/L4_Orchestration/facade/renderer_registry.js +274 -0
- package/L4_Orchestration/lifecycle/asset_loader.js +255 -0
- package/L4_Orchestration/lifecycle/initializer.js +135 -0
- package/L4_Orchestration/lifecycle/lifecycle.js +8 -0
- package/L4_Orchestration/rendering/facade.js +94 -0
- package/L4_Orchestration/rendering/rendering.js +7 -0
- package/LICENSE +21 -0
- package/README.md +82 -0
- package/bifrost_client.js +204 -0
- package/bifrost_core.js +1686 -0
- package/docs/ARCHITECTURE.md +111 -0
- package/docs/PROTOCOL.md +106 -0
- package/docs/RENDERERS.md +101 -0
- package/docs/SECURITY.md +92 -0
- package/package.json +24 -0
- package/syntax/prism-zconfig.js +41 -0
- package/syntax/prism-zenv.js +69 -0
- package/syntax/prism-zolo-theme.css +288 -0
- package/syntax/prism-zolo.js +380 -0
- package/syntax/prism-zschema.js +38 -0
- package/syntax/prism-zspark.js +25 -0
- package/syntax/prism-zui.js +68 -0
- package/zSys/accessibility/accessibility.js +10 -0
- package/zSys/accessibility/emoji_accessibility.js +173 -0
- package/zSys/dom/block_utils.js +122 -0
- package/zSys/dom/container_utils.js +370 -0
- package/zSys/dom/dom.js +13 -0
- package/zSys/dom/dom_utils.js +328 -0
- package/zSys/dom/encoding_utils.js +117 -0
- package/zSys/dom/style_utils.js +71 -0
- package/zSys/errors/error_display.js +299 -0
- package/zSys/errors/errors.js +10 -0
- package/zSys/theme/color_utils.js +274 -0
- package/zSys/theme/dark_mode_utils.js +272 -0
- package/zSys/theme/size_utils.js +256 -0
- package/zSys/theme/spacing_utils.js +405 -0
- package/zSys/theme/theme.js +14 -0
- package/zSys/theme/zbase.css +1735 -0
- package/zSys/theme/zbase_inject.js +161 -0
- package/zSys/theme/ztheme_utils.js +305 -0
- package/zSys/validation/error_boundary.js +201 -0
- package/zSys/validation/validation.js +11 -0
- package/zSys/validation/validation_utils.js +238 -0
- package/zSys/zSys.js +14 -0
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L3_Abstraction/orchestrator/group_renderer.js
|
|
3
|
+
*
|
|
4
|
+
* Grouped Rendering for _zGroup Metadata
|
|
5
|
+
*
|
|
6
|
+
* Handles grouped rendering contexts where multiple items are rendered
|
|
7
|
+
* into a single container with group-specific styling:
|
|
8
|
+
* - input-group: Radio buttons with conditional inputs, checkboxes
|
|
9
|
+
* - list-group: Lists with interactive items
|
|
10
|
+
*
|
|
11
|
+
* Includes complex logic for:
|
|
12
|
+
* - Radio zSelect splitting (each option gets its own zInputGroup)
|
|
13
|
+
* - Conditional input matching (if conditions paired with radio options)
|
|
14
|
+
* - Position-based and condition-based pairing
|
|
15
|
+
* - Group-specific styling application
|
|
16
|
+
*
|
|
17
|
+
* Extracted from zdisplay_orchestrator.js (Phase 4.4b)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// Layer 0: Primitives
|
|
21
|
+
import { createSemanticElement } from '../../L2_Handling/display/primitives/semantic_element_primitive.js';
|
|
22
|
+
import { convertStyleToString } from '../../zSys/dom/style_utils.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* GroupRenderer - Handles _zGroup rendering contexts
|
|
26
|
+
*/
|
|
27
|
+
export class GroupRenderer {
|
|
28
|
+
constructor(client, logger, orchestrator, metadataProcessor) {
|
|
29
|
+
this.client = client;
|
|
30
|
+
this.logger = logger;
|
|
31
|
+
this.orchestrator = orchestrator;
|
|
32
|
+
this.metadataProcessor = metadataProcessor;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if data should be rendered as a group
|
|
37
|
+
* @param {Object} metadata - Metadata object
|
|
38
|
+
* @returns {boolean} True if should render as group
|
|
39
|
+
*/
|
|
40
|
+
shouldRenderAsGroup(metadata) {
|
|
41
|
+
return !!(metadata._zGroup || this.metadataProcessor.isInputGroupContext(metadata));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Render items as a grouped container
|
|
46
|
+
* @param {Object} data - Data object with items to group
|
|
47
|
+
* @param {Object} metadata - Metadata with _zGroup
|
|
48
|
+
* @param {HTMLElement} parentElement - Parent to append group to
|
|
49
|
+
* @param {string} currentPath - Current path for recursion
|
|
50
|
+
*/
|
|
51
|
+
async renderGroupedItems(data, metadata, parentElement, currentPath = '') {
|
|
52
|
+
// If _zClass contains zInputGroup but no _zGroup, treat as input-group
|
|
53
|
+
if (isInputGroupContext && !metadata._zGroup) {
|
|
54
|
+
metadata._zGroup = 'input-group';
|
|
55
|
+
}
|
|
56
|
+
this.logger.log(`[ZDisplayOrchestrator] _zGroup detected: "${metadata._zGroup}" - rendering as grouped container`);
|
|
57
|
+
this.logger.log(` _zGroup detected: "${metadata._zGroup}"`);
|
|
58
|
+
|
|
59
|
+
// Create group container with zTheme classes based on _zGroup type
|
|
60
|
+
const groupContainer = document.createElement('div');
|
|
61
|
+
groupContainer.setAttribute('data-zgroup', metadata._zGroup);
|
|
62
|
+
|
|
63
|
+
// Apply zTheme container class based on group type
|
|
64
|
+
if (metadata._zGroup === 'list-group') {
|
|
65
|
+
groupContainer.classList.add('zList-group');
|
|
66
|
+
this.logger.debug('Applied zTheme class: zList-group');
|
|
67
|
+
} else if (metadata._zGroup === 'input-group') {
|
|
68
|
+
groupContainer.classList.add('zInputGroup');
|
|
69
|
+
this.logger.debug('Applied zTheme class: zInputGroup');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Apply additional _zClass styling if provided (from YAML)
|
|
73
|
+
if (metadata._zClass) {
|
|
74
|
+
const classes = metadata._zClass.split(' ').filter(c => c.trim());
|
|
75
|
+
if (classes.length > 0) {
|
|
76
|
+
groupContainer.classList.add(...classes);
|
|
77
|
+
this.logger.log(` Applied additional _zClass: ${metadata._zClass}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Add prefix label for input-group if _zGroupLabel is provided
|
|
82
|
+
if (metadata._zGroup === 'input-group' && metadata._zGroupLabel) {
|
|
83
|
+
const labelSpan = document.createElement('span');
|
|
84
|
+
labelSpan.classList.add('zInputGroup-text');
|
|
85
|
+
labelSpan.textContent = metadata._zGroupLabel;
|
|
86
|
+
groupContainer.appendChild(labelSpan);
|
|
87
|
+
this.logger.log(` Added input group label: ${metadata._zGroupLabel}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// DEBUG: Log what we're about to iterate
|
|
91
|
+
if (metadata._zGroup === 'input-group') {
|
|
92
|
+
this.logger.debug('[INPUT-GROUP] Data keys:', Object.keys(data));
|
|
93
|
+
this.logger.debug('[INPUT-GROUP] Full data:', JSON.stringify(data, null, 2));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Track matched conditional inputs (for radio zSelect splitting)
|
|
97
|
+
const matchedInputKeys = new Set();
|
|
98
|
+
|
|
99
|
+
// Iterate through all non-metadata children and render into group
|
|
100
|
+
for (const [key, value] of Object.entries(data)) {
|
|
101
|
+
// Skip ONLY metadata keys (not organizational containers like _Visual_Progression)
|
|
102
|
+
// Delegated to MetadataProcessor (Phase 4.4a)
|
|
103
|
+
if (this.metadataProcessor.isMetadataKey(key)) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Skip if this input was already matched and rendered by radio zSelect splitting
|
|
108
|
+
if (matchedInputKeys.has(key)) {
|
|
109
|
+
this.logger.log(` Skipping already-matched conditional input: ${key}`);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this.logger.log(` Rendering grouped item: ${key}`);
|
|
114
|
+
|
|
115
|
+
// DEBUG for input-group
|
|
116
|
+
if (metadata._zGroup === 'input-group') {
|
|
117
|
+
this.logger.log(`[INPUT-GROUP] Processing child: ${key}`);
|
|
118
|
+
this.logger.log(`[INPUT-GROUP] Value type: ${Array.isArray(value) ? 'Array' : typeof value}`);
|
|
119
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
120
|
+
this.logger.log(`[INPUT-GROUP] Value keys: ${Object.keys(value).join(', ')}`);
|
|
121
|
+
if (value.event) {
|
|
122
|
+
this.logger.log(`[INPUT-GROUP] Event: ${value.event}, type: ${value.type}`);
|
|
123
|
+
}
|
|
124
|
+
if (value.zSelect) {
|
|
125
|
+
this.logger.log(`[INPUT-GROUP] zSelect found, type: ${value.zSelect.type}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Handle list/array values (zDisplay events)
|
|
131
|
+
if (Array.isArray(value)) {
|
|
132
|
+
for (const item of value) {
|
|
133
|
+
if (item && item.zDisplay) {
|
|
134
|
+
// SEPARATION OF CONCERNS: Render element without group context
|
|
135
|
+
const element = await this.renderZDisplayEvent(item.zDisplay, groupContainer);
|
|
136
|
+
if (element) {
|
|
137
|
+
// Apply group-specific styling AFTER rendering
|
|
138
|
+
this._applyGroupStyling(element, metadata._zGroup, item.zDisplay);
|
|
139
|
+
groupContainer.appendChild(element);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} else if (value && value.zDisplay && value.zDisplay.event === 'selection' && value.zDisplay.type === 'radio' && metadata._zGroup === 'input-group') {
|
|
144
|
+
// Backend sent expanded zSelect as value.zDisplay: run same radio+input split logic
|
|
145
|
+
const sel = value.zDisplay;
|
|
146
|
+
const options = sel.options || [];
|
|
147
|
+
const zCross = sel.zCross !== undefined ? sel.zCross : false;
|
|
148
|
+
const groupName = sel.zId || sel._zId || sel._id || `radio_${Math.random().toString(36).substr(2, 9)}`;
|
|
149
|
+
const conditionalInputs = [];
|
|
150
|
+
for (const [childKey, childValue] of Object.entries(data)) {
|
|
151
|
+
if (childValue && childValue.zInput && childValue.zInput.if) {
|
|
152
|
+
conditionalInputs.push({ key: childKey, value: childValue, condition: (childValue.zInput.if || '').replace(/#.*$/gm, '').trim(), payload: childValue.zInput });
|
|
153
|
+
} else if (childValue && childValue.zDisplay && childValue.zDisplay.event === 'read_string' && childValue.zDisplay.if) {
|
|
154
|
+
conditionalInputs.push({ key: childKey, value: childValue, condition: (childValue.zDisplay.if || '').replace(/#.*$/gm, '').trim(), payload: childValue.zDisplay });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const parseSuffixN = (s) => {
|
|
158
|
+
if (s == null) return null;
|
|
159
|
+
if (typeof s === 'number' && s > 0) return s;
|
|
160
|
+
const m = String(s).trim().match(/^\+(\d+)$/);
|
|
161
|
+
return m ? parseInt(m[1], 10) : null;
|
|
162
|
+
};
|
|
163
|
+
const suffixN = parseSuffixN(sel.suffix);
|
|
164
|
+
|
|
165
|
+
// Helper function to parse option string
|
|
166
|
+
const parseOptionString = (optionString) => {
|
|
167
|
+
let cleanLabel = optionString;
|
|
168
|
+
let isDisabled = false;
|
|
169
|
+
|
|
170
|
+
const disabledMatch = optionString.match(/^(.*?)\s*\[disabled\]\s*$/i);
|
|
171
|
+
if (disabledMatch) {
|
|
172
|
+
cleanLabel = disabledMatch[1].trim();
|
|
173
|
+
isDisabled = true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const defaultMatch = cleanLabel.match(/^(.*?)\s*\[default\]\s*$/i);
|
|
177
|
+
if (defaultMatch) {
|
|
178
|
+
cleanLabel = defaultMatch[1].trim();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { cleanLabel, isDisabled };
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
for (let i = 0; i < options.length; i++) {
|
|
185
|
+
let optionValue, optionLabel, optionDisabled;
|
|
186
|
+
|
|
187
|
+
if (typeof options[i] === 'string') {
|
|
188
|
+
const parsed = parseOptionString(options[i]);
|
|
189
|
+
optionValue = parsed.cleanLabel;
|
|
190
|
+
optionLabel = parsed.cleanLabel;
|
|
191
|
+
optionDisabled = parsed.isDisabled;
|
|
192
|
+
} else {
|
|
193
|
+
optionValue = options[i].value || options[i].label || '';
|
|
194
|
+
optionLabel = options[i].label || options[i].value || '';
|
|
195
|
+
optionDisabled = options[i].disabled || false;
|
|
196
|
+
}
|
|
197
|
+
let matchingInput;
|
|
198
|
+
if (suffixN != null && i < suffixN && i < conditionalInputs.length) {
|
|
199
|
+
matchingInput = conditionalInputs[i];
|
|
200
|
+
} else {
|
|
201
|
+
matchingInput = conditionalInputs.find(input => {
|
|
202
|
+
const condition = (input.condition || '').trim();
|
|
203
|
+
const valueMatch = condition.match(/==\s*['"]?([^'"\s]+)['"]?\s*(?:#|$)/);
|
|
204
|
+
if (valueMatch && valueMatch[1].trim() === optionValue) return true;
|
|
205
|
+
return [`== '${optionValue}'`, `== "${optionValue}"`, `=='${optionValue}'`, `=="${optionValue}"`, `== ${optionValue}`, `==${optionValue}`].some(p => condition.includes(p));
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
const inputGroupDiv = document.createElement('div');
|
|
209
|
+
inputGroupDiv.classList.add('zInputGroup');
|
|
210
|
+
if (sel._zClass) {
|
|
211
|
+
const classes = sel._zClass.split(' ').filter(c => c.trim() && c !== 'zCheck-input');
|
|
212
|
+
if (classes.length) inputGroupDiv.classList.add(...classes);
|
|
213
|
+
}
|
|
214
|
+
const textWrapper = document.createElement('div');
|
|
215
|
+
textWrapper.classList.add('zInputGroup-text');
|
|
216
|
+
const { createInput } = await import('../primitives/form_primitives.js');
|
|
217
|
+
const radioInput = createInput('radio', {
|
|
218
|
+
id: `${groupName}_${i}`,
|
|
219
|
+
name: groupName,
|
|
220
|
+
value: optionValue,
|
|
221
|
+
class: 'zCheck-input',
|
|
222
|
+
'aria-label': optionLabel,
|
|
223
|
+
disabled: optionDisabled
|
|
224
|
+
});
|
|
225
|
+
if (sel.default === optionValue) radioInput.checked = true;
|
|
226
|
+
radioInput.setAttribute('data-zcross', zCross.toString());
|
|
227
|
+
textWrapper.appendChild(radioInput);
|
|
228
|
+
inputGroupDiv.appendChild(textWrapper);
|
|
229
|
+
if (matchingInput) {
|
|
230
|
+
matchedInputKeys.add(matchingInput.key);
|
|
231
|
+
const payload = matchingInput.payload || matchingInput.value.zInput || matchingInput.value.zDisplay || {};
|
|
232
|
+
const inputEventData = { event: 'read_string', ...payload };
|
|
233
|
+
if (matchingInput.value.zCross !== undefined) inputEventData.zCross = matchingInput.value.zCross;
|
|
234
|
+
delete inputEventData.prompt;
|
|
235
|
+
const inputElement = await this.renderZDisplayEvent(inputEventData, inputGroupDiv);
|
|
236
|
+
if (inputElement) {
|
|
237
|
+
if (inputElement.tagName === 'INPUT' || inputElement.tagName === 'TEXTAREA') {
|
|
238
|
+
inputGroupDiv.appendChild(inputElement);
|
|
239
|
+
} else {
|
|
240
|
+
const actualInput = inputElement.querySelector('input, textarea');
|
|
241
|
+
inputGroupDiv.appendChild(actualInput || inputElement);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
groupContainer.appendChild(inputGroupDiv);
|
|
246
|
+
}
|
|
247
|
+
continue;
|
|
248
|
+
} else if (value && value.zDisplay) {
|
|
249
|
+
// Handle direct zDisplay event
|
|
250
|
+
// SEPARATION OF CONCERNS: Render element without group context
|
|
251
|
+
const element = await this.renderZDisplayEvent(value.zDisplay, groupContainer);
|
|
252
|
+
if (element) {
|
|
253
|
+
// Apply group-specific styling AFTER rendering
|
|
254
|
+
this._applyGroupStyling(element, metadata._zGroup, value.zDisplay);
|
|
255
|
+
groupContainer.appendChild(element);
|
|
256
|
+
}
|
|
257
|
+
} else if (value && typeof value === 'object') {
|
|
258
|
+
// Check if this is already a zDisplay event object
|
|
259
|
+
if (value.event) {
|
|
260
|
+
// This is a zDisplay event that was already expanded by the backend
|
|
261
|
+
this.logger.log(` Found pre-expanded zDisplay event '${value.event}' in grouped item: ${key}`);
|
|
262
|
+
|
|
263
|
+
// Special handling for radio selection events in input-group: split into separate zInputGroup containers
|
|
264
|
+
this.logger.log(` Checking for radio selection: key=${key}, event=${value.event}, type=${value.type}, _zGroup=${metadata._zGroup}`);
|
|
265
|
+
if (value.event === 'selection' && metadata._zGroup === 'input-group' && value.type === 'radio') {
|
|
266
|
+
this.logger.log(` Radio selection detected! Processing ${value.options?.length || 0} options`);
|
|
267
|
+
// Radio buttons in input-group: create separate zInputGroup for each option
|
|
268
|
+
const options = value.options || [];
|
|
269
|
+
const zCross = value.zCross !== undefined ? value.zCross : false;
|
|
270
|
+
const groupName = value.zId || value._zId || value._id || `radio_${Math.random().toString(36).substr(2, 9)}`;
|
|
271
|
+
|
|
272
|
+
// Find all conditional zInput elements (shorthand or expanded zDisplay) in declaration order
|
|
273
|
+
const conditionalInputs = [];
|
|
274
|
+
for (const [childKey, childValue] of Object.entries(data)) {
|
|
275
|
+
if (childValue && childValue.zInput && childValue.zInput.if) {
|
|
276
|
+
conditionalInputs.push({ key: childKey, value: childValue, condition: (childValue.zInput.if || '').replace(/#.*$/gm, '').trim(), payload: childValue.zInput });
|
|
277
|
+
this.logger.log(` Found conditional input: ${childKey} with condition: "${childValue.zInput.if}"`);
|
|
278
|
+
} else if (childValue && childValue.zDisplay && childValue.zDisplay.event === 'read_string' && childValue.zDisplay.if) {
|
|
279
|
+
conditionalInputs.push({ key: childKey, value: childValue, condition: (childValue.zDisplay.if || '').replace(/#.*$/gm, '').trim(), payload: childValue.zDisplay });
|
|
280
|
+
this.logger.log(` Found conditional input (zDisplay): ${childKey} with condition: "${childValue.zDisplay.if}"`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
this.logger.log(` Found ${conditionalInputs.length} conditional input(s) total`);
|
|
284
|
+
// Parse suffix +N (e.g. "+3" => pair with next N inputs by position)
|
|
285
|
+
const parseSuffixN = (s) => {
|
|
286
|
+
if (s == null) return null;
|
|
287
|
+
if (typeof s === 'number' && s > 0) return s;
|
|
288
|
+
const m = String(s).trim().match(/^\+(\d+)$/);
|
|
289
|
+
return m ? parseInt(m[1], 10) : null;
|
|
290
|
+
};
|
|
291
|
+
const suffixN = parseSuffixN(value.suffix);
|
|
292
|
+
if (suffixN != null) this.logger.log(` suffix: ${value.suffix} → suffixN=${suffixN} (position-based pairing)`);
|
|
293
|
+
|
|
294
|
+
// Create a separate zInputGroup for each radio option
|
|
295
|
+
for (let i = 0; i < options.length; i++) {
|
|
296
|
+
const optionValue = typeof options[i] === 'string' ? options[i] : (options[i].value || options[i].label || '');
|
|
297
|
+
const optionLabel = typeof options[i] === 'string' ? options[i] : (options[i].label || options[i].value || '');
|
|
298
|
+
|
|
299
|
+
// Pair by position when suffix +N is set; else match by condition
|
|
300
|
+
let matchingInput;
|
|
301
|
+
if (suffixN != null && i < suffixN && i < conditionalInputs.length) {
|
|
302
|
+
matchingInput = conditionalInputs[i];
|
|
303
|
+
this.logger.log(` Position-based pair: option[${i}] "${optionValue}" → ${matchingInput.key}`);
|
|
304
|
+
} else {
|
|
305
|
+
matchingInput = conditionalInputs.find(input => {
|
|
306
|
+
const condition = (input.condition || '').trim();
|
|
307
|
+
const valueMatch = condition.match(/==\s*['"]?([^'"\s]+)['"]?\s*(?:#|$)/);
|
|
308
|
+
if (valueMatch && valueMatch[1].trim() === optionValue) return true;
|
|
309
|
+
const patterns = [
|
|
310
|
+
`== '${optionValue}'`,
|
|
311
|
+
`== "${optionValue}"`,
|
|
312
|
+
`=='${optionValue}'`,
|
|
313
|
+
`=="${optionValue}"`,
|
|
314
|
+
`== ${optionValue}`,
|
|
315
|
+
`==${optionValue}`
|
|
316
|
+
];
|
|
317
|
+
return patterns.some(pattern => condition.includes(pattern));
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
if (!suffixN) this.logger.log(` Looking for input matching option "${optionValue}": ${matchingInput ? `Found: ${matchingInput.key}` : 'Not found'}`);
|
|
321
|
+
|
|
322
|
+
// Create zInputGroup container for this radio + input pair
|
|
323
|
+
const inputGroupDiv = document.createElement('div');
|
|
324
|
+
inputGroupDiv.classList.add('zInputGroup');
|
|
325
|
+
if (value._zClass) {
|
|
326
|
+
const classes = value._zClass.split(' ').filter(c => c.trim() && c !== 'zCheck-input');
|
|
327
|
+
if (classes.length > 0) {
|
|
328
|
+
inputGroupDiv.classList.add(...classes);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Create zInputGroup-text wrapper for radio
|
|
333
|
+
const textWrapper = document.createElement('div');
|
|
334
|
+
textWrapper.classList.add('zInputGroup-text');
|
|
335
|
+
|
|
336
|
+
// Create radio input
|
|
337
|
+
const { createInput } = await import('../primitives/form_primitives.js');
|
|
338
|
+
const radioInput = createInput('radio', {
|
|
339
|
+
id: `${groupName}_${i}`,
|
|
340
|
+
name: groupName,
|
|
341
|
+
value: optionValue,
|
|
342
|
+
class: 'zCheck-input',
|
|
343
|
+
'aria-label': optionLabel
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Set checked state if default matches
|
|
347
|
+
if (value.default === optionValue) {
|
|
348
|
+
radioInput.checked = true;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Store zCross flag on radio element (for conditional rendering)
|
|
352
|
+
radioInput.setAttribute('data-zcross', zCross.toString());
|
|
353
|
+
|
|
354
|
+
textWrapper.appendChild(radioInput);
|
|
355
|
+
inputGroupDiv.appendChild(textWrapper);
|
|
356
|
+
|
|
357
|
+
// Add matching conditional input if found
|
|
358
|
+
if (matchingInput) {
|
|
359
|
+
// Mark this input as matched so we don't process it again
|
|
360
|
+
matchedInputKeys.add(matchingInput.key);
|
|
361
|
+
// Support both shorthand (zInput) and expanded (zDisplay) payloads
|
|
362
|
+
const payload = matchingInput.payload || matchingInput.value.zInput || matchingInput.value.zDisplay || {};
|
|
363
|
+
const inputEventData = { event: 'read_string', ...payload };
|
|
364
|
+
if (matchingInput.value.zCross !== undefined) {
|
|
365
|
+
inputEventData.zCross = matchingInput.value.zCross;
|
|
366
|
+
}
|
|
367
|
+
{
|
|
368
|
+
// Remove prompt to prevent wrapper div creation
|
|
369
|
+
delete inputEventData.prompt;
|
|
370
|
+
// Render input directly into the zInputGroup (no wrapper needed)
|
|
371
|
+
// Pass inputGroupDiv as parent so it detects input-group context
|
|
372
|
+
const inputElement = await this.renderZDisplayEvent(inputEventData, inputGroupDiv);
|
|
373
|
+
if (inputElement) {
|
|
374
|
+
// If renderZDisplayEvent returns a wrapper, extract the actual input
|
|
375
|
+
// Otherwise use the element directly
|
|
376
|
+
if (inputElement.tagName === 'INPUT' || inputElement.tagName === 'TEXTAREA') {
|
|
377
|
+
inputGroupDiv.appendChild(inputElement);
|
|
378
|
+
} else {
|
|
379
|
+
// Wrapper returned - find the input inside and move it
|
|
380
|
+
const actualInput = inputElement.querySelector('input, textarea');
|
|
381
|
+
if (actualInput) {
|
|
382
|
+
inputGroupDiv.appendChild(actualInput);
|
|
383
|
+
} else {
|
|
384
|
+
inputGroupDiv.appendChild(inputElement);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
groupContainer.appendChild(inputGroupDiv);
|
|
392
|
+
this.logger.log(` Created zInputGroup for radio option: ${optionValue}`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Skip normal rendering for this zSelect
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Special handling for checkboxes in input-group: wrap in zInputGroup-text
|
|
400
|
+
if (value.event === 'read_bool' && metadata._zGroup === 'input-group') {
|
|
401
|
+
const wrapperDiv = document.createElement('div');
|
|
402
|
+
wrapperDiv.classList.add('zInputGroup-text');
|
|
403
|
+
wrapperDiv.setAttribute('data-zkey', key);
|
|
404
|
+
|
|
405
|
+
// Render checkbox inside wrapper, passing wrapper as parent so it detects input-group context
|
|
406
|
+
const checkboxElement = await this.renderZDisplayEvent(value, wrapperDiv);
|
|
407
|
+
if (checkboxElement) {
|
|
408
|
+
wrapperDiv.appendChild(checkboxElement);
|
|
409
|
+
}
|
|
410
|
+
groupContainer.appendChild(wrapperDiv);
|
|
411
|
+
this.logger.log(` Wrapped read_bool checkbox in zInputGroup-text for input-group`);
|
|
412
|
+
} else {
|
|
413
|
+
// Normal rendering for other events
|
|
414
|
+
const element = await this.renderZDisplayEvent(value, groupContainer);
|
|
415
|
+
if (element) {
|
|
416
|
+
groupContainer.appendChild(element);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
} else {
|
|
420
|
+
// Check for shorthand keys (zInput, zButton, etc.) that need expansion
|
|
421
|
+
const shorthandKeys = ['zInput', 'zButton', 'zCheckbox', 'zSelect', 'zText', 'zMD', 'zH1', 'zH2', 'zH3', 'zH4', 'zH5', 'zH6', 'zURL', 'zImage'];
|
|
422
|
+
const foundShorthand = shorthandKeys.find(sk => value[sk]);
|
|
423
|
+
|
|
424
|
+
if (foundShorthand) {
|
|
425
|
+
// This is a shorthand that needs to be rendered as a zDisplay event
|
|
426
|
+
this.logger.debug(`Found shorthand '%s' in grouped item: %s`, foundShorthand, key);
|
|
427
|
+
|
|
428
|
+
// Special handling for radio zSelect in input-group: split into separate zInputGroup containers
|
|
429
|
+
this.logger.debug(`Checking radio zSelect shorthand: %s`, foundShorthand);
|
|
430
|
+
if (foundShorthand === 'zSelect' && metadata._zGroup === 'input-group' && value.zSelect && value.zSelect.type === 'radio') {
|
|
431
|
+
this.logger.debug(`Radio zSelect shorthand detected (%s options)`, value.zSelect.options?.length || 0);
|
|
432
|
+
// Radio buttons in input-group: create separate zInputGroup for each option
|
|
433
|
+
const options = value.zSelect.options || [];
|
|
434
|
+
const zCross = value.zCross !== undefined ? value.zCross : false;
|
|
435
|
+
const groupName = value.zSelect.zId || value.zSelect._zId || value.zSelect._id || `radio_${Math.random().toString(36).substr(2, 9)}`;
|
|
436
|
+
|
|
437
|
+
// Find all conditional zInput elements (shorthand or expanded zDisplay) in declaration order
|
|
438
|
+
const conditionalInputs = [];
|
|
439
|
+
for (const [childKey, childValue] of Object.entries(data)) {
|
|
440
|
+
if (childValue && childValue.zInput && childValue.zInput.if) {
|
|
441
|
+
conditionalInputs.push({ key: childKey, value: childValue, condition: (childValue.zInput.if || '').replace(/#.*$/gm, '').trim(), payload: childValue.zInput });
|
|
442
|
+
} else if (childValue && childValue.zDisplay && childValue.zDisplay.event === 'read_string' && childValue.zDisplay.if) {
|
|
443
|
+
conditionalInputs.push({ key: childKey, value: childValue, condition: (childValue.zDisplay.if || '').replace(/#.*$/gm, '').trim(), payload: childValue.zDisplay });
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// Parse suffix +N (e.g. "+3" => pair with next N inputs by position)
|
|
447
|
+
const parseSuffixN = (s) => {
|
|
448
|
+
if (s == null) return null;
|
|
449
|
+
if (typeof s === 'number' && s > 0) return s;
|
|
450
|
+
const m = String(s).trim().match(/^\+(\d+)$/);
|
|
451
|
+
return m ? parseInt(m[1], 10) : null;
|
|
452
|
+
};
|
|
453
|
+
const suffixN = parseSuffixN(value.zSelect.suffix);
|
|
454
|
+
this.logger.log(` suffix: ${value.zSelect.suffix} → suffixN=${suffixN}, conditionalInputs=${conditionalInputs.length}`);
|
|
455
|
+
|
|
456
|
+
// Create a separate zInputGroup for each radio option
|
|
457
|
+
for (let i = 0; i < options.length; i++) {
|
|
458
|
+
const optionValue = typeof options[i] === 'string' ? options[i] : (options[i].value || options[i].label || '');
|
|
459
|
+
const optionLabel = typeof options[i] === 'string' ? options[i] : (options[i].label || options[i].value || '');
|
|
460
|
+
|
|
461
|
+
// Pair by position when suffix +N is set; else match by condition
|
|
462
|
+
let matchingInput;
|
|
463
|
+
if (suffixN != null && i < suffixN && i < conditionalInputs.length) {
|
|
464
|
+
matchingInput = conditionalInputs[i];
|
|
465
|
+
this.logger.log(` Position-based pair: option[${i}] "${optionValue}" → ${matchingInput.key}`);
|
|
466
|
+
} else {
|
|
467
|
+
matchingInput = conditionalInputs.find(input => {
|
|
468
|
+
const condition = (input.condition || '').trim();
|
|
469
|
+
const valueMatch = condition.match(/==\s*['"]?([^'"\s]+)['"]?\s*(?:#|$)/);
|
|
470
|
+
if (valueMatch && valueMatch[1].trim() === optionValue) return true;
|
|
471
|
+
const patterns = [
|
|
472
|
+
`== '${optionValue}'`,
|
|
473
|
+
`== "${optionValue}"`,
|
|
474
|
+
`=='${optionValue}'`,
|
|
475
|
+
`=="${optionValue}"`,
|
|
476
|
+
`== ${optionValue}`,
|
|
477
|
+
`==${optionValue}`
|
|
478
|
+
];
|
|
479
|
+
return patterns.some(pattern => condition.includes(pattern));
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Create zInputGroup container for this radio + input pair
|
|
484
|
+
const inputGroupDiv = document.createElement('div');
|
|
485
|
+
inputGroupDiv.classList.add('zInputGroup');
|
|
486
|
+
if (value.zSelect._zClass) {
|
|
487
|
+
const classes = value.zSelect._zClass.split(' ').filter(c => c.trim() && c !== 'zCheck-input');
|
|
488
|
+
if (classes.length > 0) {
|
|
489
|
+
inputGroupDiv.classList.add(...classes);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Create zInputGroup-text wrapper for radio
|
|
494
|
+
const textWrapper = document.createElement('div');
|
|
495
|
+
textWrapper.classList.add('zInputGroup-text');
|
|
496
|
+
|
|
497
|
+
// Create radio input
|
|
498
|
+
const { createInput } = await import('../primitives/form_primitives.js');
|
|
499
|
+
const radioInput = createInput('radio', {
|
|
500
|
+
id: `${groupName}_${i}`,
|
|
501
|
+
name: groupName,
|
|
502
|
+
value: optionValue,
|
|
503
|
+
class: 'zCheck-input',
|
|
504
|
+
'aria-label': optionLabel
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// Set checked state if default matches
|
|
508
|
+
if (value.zSelect.default === optionValue) {
|
|
509
|
+
radioInput.checked = true;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Store zCross flag on radio element (for conditional rendering)
|
|
513
|
+
radioInput.setAttribute('data-zcross', zCross.toString());
|
|
514
|
+
|
|
515
|
+
textWrapper.appendChild(radioInput);
|
|
516
|
+
inputGroupDiv.appendChild(textWrapper);
|
|
517
|
+
|
|
518
|
+
// Add matching conditional input if found
|
|
519
|
+
if (matchingInput) {
|
|
520
|
+
// Mark this input as matched so we don't process it again
|
|
521
|
+
matchedInputKeys.add(matchingInput.key);
|
|
522
|
+
// Support both shorthand (zInput) and expanded (zDisplay) payloads from backend
|
|
523
|
+
const payload = matchingInput.payload || matchingInput.value.zInput || matchingInput.value.zDisplay || {};
|
|
524
|
+
const inputEventData = { event: 'read_string', ...payload };
|
|
525
|
+
if (matchingInput.value.zCross !== undefined) {
|
|
526
|
+
inputEventData.zCross = matchingInput.value.zCross;
|
|
527
|
+
}
|
|
528
|
+
// Remove prompt to prevent wrapper div creation
|
|
529
|
+
delete inputEventData.prompt;
|
|
530
|
+
const inputElement = await this.renderZDisplayEvent(inputEventData, inputGroupDiv);
|
|
531
|
+
if (inputElement) {
|
|
532
|
+
// Extract actual input/textarea if renderZDisplayEvent returned a wrapper
|
|
533
|
+
if (inputElement.tagName === 'INPUT' || inputElement.tagName === 'TEXTAREA') {
|
|
534
|
+
inputGroupDiv.appendChild(inputElement);
|
|
535
|
+
} else {
|
|
536
|
+
const actualInput = inputElement.querySelector('input, textarea');
|
|
537
|
+
if (actualInput) {
|
|
538
|
+
inputGroupDiv.appendChild(actualInput);
|
|
539
|
+
} else {
|
|
540
|
+
inputGroupDiv.appendChild(inputElement);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
groupContainer.appendChild(inputGroupDiv);
|
|
547
|
+
this.logger.log(` Created zInputGroup for radio option: ${optionValue}`);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Skip normal rendering for zSelect
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Special handling for checkboxes in input-group: wrap in zInputGroup-text
|
|
555
|
+
if (foundShorthand === 'zCheckbox' && metadata._zGroup === 'input-group') {
|
|
556
|
+
// Check if already wrapped in a container with zInputGroup-text
|
|
557
|
+
const hasInputGroupTextWrapper = value._zClass && value._zClass.includes('zInputGroup-text');
|
|
558
|
+
|
|
559
|
+
if (!hasInputGroupTextWrapper) {
|
|
560
|
+
// Wrap checkbox in zInputGroup-text container
|
|
561
|
+
const wrapperDiv = document.createElement('div');
|
|
562
|
+
wrapperDiv.classList.add('zInputGroup-text');
|
|
563
|
+
wrapperDiv.setAttribute('data-zkey', key);
|
|
564
|
+
|
|
565
|
+
// Expand zCheckbox to read_bool event and render directly into wrapper
|
|
566
|
+
// Pass zCross from parent value if present
|
|
567
|
+
const eventData = { event: 'read_bool', ...value.zCheckbox };
|
|
568
|
+
if (value.zCross !== undefined) {
|
|
569
|
+
eventData.zCross = value.zCross;
|
|
570
|
+
}
|
|
571
|
+
const checkboxElement = await this.renderZDisplayEvent(eventData, wrapperDiv);
|
|
572
|
+
if (checkboxElement) {
|
|
573
|
+
wrapperDiv.appendChild(checkboxElement);
|
|
574
|
+
}
|
|
575
|
+
groupContainer.appendChild(wrapperDiv);
|
|
576
|
+
this.logger.log(` Wrapped checkbox in zInputGroup-text for input-group`);
|
|
577
|
+
} else {
|
|
578
|
+
// Already wrapped, render normally
|
|
579
|
+
const element = await this.renderChunk({ [key]: value });
|
|
580
|
+
if (element && element.firstChild) {
|
|
581
|
+
const actualElement = element.firstChild;
|
|
582
|
+
groupContainer.appendChild(actualElement);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
} else {
|
|
586
|
+
// Recursively render the entire structure for other shorthands
|
|
587
|
+
const element = await this.renderChunk({ [key]: value });
|
|
588
|
+
if (element && element.firstChild) {
|
|
589
|
+
// Extract the actual rendered element (skip wrapper if present)
|
|
590
|
+
const actualElement = element.firstChild;
|
|
591
|
+
groupContainer.appendChild(actualElement);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
} else {
|
|
595
|
+
// Handle nested objects (recurse)
|
|
596
|
+
// DEBUG: Log organizational containers
|
|
597
|
+
if (key.startsWith('_')) {
|
|
598
|
+
this.logger.log(` [GROUP] Processing organizational container: ${key}`);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Use centralized semantic element primitive (SSOT for _zHTML)
|
|
602
|
+
const elementType = value._zHTML || 'div';
|
|
603
|
+
const itemDiv = createSemanticElement(elementType, {}, this.logger);
|
|
604
|
+
itemDiv.setAttribute('data-zkey', key);
|
|
605
|
+
|
|
606
|
+
// Apply metadata to the organizational container
|
|
607
|
+
if (value._zClass) {
|
|
608
|
+
itemDiv.className = value._zClass;
|
|
609
|
+
}
|
|
610
|
+
if (value._zStyle) {
|
|
611
|
+
const cssString = convertStyleToString(value._zStyle, this.logger);
|
|
612
|
+
if (cssString) {
|
|
613
|
+
itemDiv.setAttribute('style', cssString);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
await this.renderItems(value, itemDiv, keyPath);
|
|
618
|
+
|
|
619
|
+
if (itemDiv.children.length > 0) {
|
|
620
|
+
// OPTIMIZATION: Unwrap single-child containers with no styling
|
|
621
|
+
// This handles zText with semantic:div where the div itself has styling
|
|
622
|
+
if (itemDiv.children.length === 1 && !itemDiv.className) {
|
|
623
|
+
const child = itemDiv.children[0];
|
|
624
|
+
// Transfer data-zkey and id to the child
|
|
625
|
+
child.setAttribute('data-zkey', key);
|
|
626
|
+
if (!child.id) {
|
|
627
|
+
child.setAttribute('id', key);
|
|
628
|
+
}
|
|
629
|
+
groupContainer.appendChild(child);
|
|
630
|
+
} else {
|
|
631
|
+
groupContainer.appendChild(itemDiv);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (key.startsWith('_')) {
|
|
635
|
+
this.logger.log(`[GROUP] Rendered organizational container ${key} with ${itemDiv.children.length} children`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Append group to parent
|
|
644
|
+
if (groupContainer.children.length > 0) {
|
|
645
|
+
parentElement.appendChild(groupContainer);
|
|
646
|
+
this.logger.log(`[ZDisplayOrchestrator] Grouped container rendered with ${groupContainer.children.length} items`);
|
|
647
|
+
this.logger.log(`Grouped container rendered with ${groupContainer.children.length} items`);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Exit early - we've handled all children in the group
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Apply group-specific styling to an element (Terminal-first pattern)
|
|
656
|
+
* @param {HTMLElement} element - The rendered element
|
|
657
|
+
* @param {string} groupType - The type of group
|
|
658
|
+
* @param {Object} eventData - The original event data
|
|
659
|
+
* @private
|
|
660
|
+
*/
|
|
661
|
+
_applyGroupStyling(element, groupType, eventData) {
|
|
662
|
+
if (!element || !groupType) {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
this.logger.log(`[_applyGroupStyling] Applying group styling: ${groupType}, color: ${eventData.color || 'none'}`);
|
|
667
|
+
|
|
668
|
+
// Apply group-specific zTheme classes based on group type and event type
|
|
669
|
+
switch (groupType) {
|
|
670
|
+
case 'list-group':
|
|
671
|
+
// For links, buttons, or any interactive element in a list-group
|
|
672
|
+
if (eventData.event === 'zURL' || eventData.event === 'button') {
|
|
673
|
+
element.classList.add('zList-group-item', 'zList-group-item-action');
|
|
674
|
+
|
|
675
|
+
// Terminal-first: Auto-infer color variant from YAML color parameter
|
|
676
|
+
if (eventData.color) {
|
|
677
|
+
const colorClass = `zList-group-item-${eventData.color.toLowerCase()}`;
|
|
678
|
+
element.classList.add(colorClass);
|
|
679
|
+
this.logger.log(`[_applyGroupStyling] Applied list-group color: ${colorClass}`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
break;
|
|
683
|
+
|
|
684
|
+
case 'button-group':
|
|
685
|
+
// For future: Button groups (horizontal button toolbar)
|
|
686
|
+
// element.classList.add('zBtn-group-item');
|
|
687
|
+
break;
|
|
688
|
+
|
|
689
|
+
case 'card-group':
|
|
690
|
+
// For future: Card groups (masonry/grid layout)
|
|
691
|
+
// element.classList.add('zCard-group-item');
|
|
692
|
+
break;
|
|
693
|
+
|
|
694
|
+
default:
|
|
695
|
+
this.logger.warn(`[_applyGroupStyling] Unknown group type: ${groupType}`);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|