@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,797 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L3_Abstraction/orchestrator/input_event_handler.js
|
|
3
|
+
*
|
|
4
|
+
* Input Event Handler - Input Request Rendering
|
|
5
|
+
*
|
|
6
|
+
* Handles complex input request events:
|
|
7
|
+
* - read_string / read_password: Text/password inputs with validation
|
|
8
|
+
* - read_bool: Boolean checkboxes with conditional rendering
|
|
9
|
+
* - selection: Dropdown/radio/checkbox selection controls
|
|
10
|
+
*
|
|
11
|
+
* Extracted from zdisplay_orchestrator.js (Phase 9, Task 2.7)
|
|
12
|
+
*
|
|
13
|
+
* @module orchestrator/input_event_handler
|
|
14
|
+
* @layer L3 (Abstraction)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { TYPOGRAPHY } from '../../L1_Foundation/constants/bifrost_constants.js';
|
|
18
|
+
import { convertStyleToString } from '../../zSys/dom/style_utils.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Append a red required marker (*) to a label/legend when the field is required.
|
|
22
|
+
* SSOT — styled by .zRequired in zbase.css. aria-hidden since `required` already
|
|
23
|
+
* exposes the constraint to assistive tech via the input element.
|
|
24
|
+
*/
|
|
25
|
+
function appendRequiredMark(labelEl, required) {
|
|
26
|
+
if (!required || !labelEl) return;
|
|
27
|
+
const star = document.createElement('span');
|
|
28
|
+
star.className = 'zRequired';
|
|
29
|
+
star.textContent = ' *';
|
|
30
|
+
star.setAttribute('aria-hidden', 'true');
|
|
31
|
+
labelEl.appendChild(star);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class InputEventHandler {
|
|
35
|
+
constructor(client, logger) {
|
|
36
|
+
this.client = client;
|
|
37
|
+
this.logger = logger;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Handle read_string and read_password events
|
|
42
|
+
* @param {string} event - Event type ('read_string' or 'read_password')
|
|
43
|
+
* @param {Object} eventData - Event data
|
|
44
|
+
* @param {HTMLElement} parentElement - Parent element for context detection
|
|
45
|
+
* @returns {HTMLElement} - Input element or wrapper
|
|
46
|
+
*/
|
|
47
|
+
async handleTextInput(event, eventData, parentElement) {
|
|
48
|
+
const { createLabel, createInput, createTextarea } = await import('../../L2_Handling/display/primitives/form_primitives.js');
|
|
49
|
+
|
|
50
|
+
const inputType = event === 'read_password' ? 'password' : (eventData.type || 'text');
|
|
51
|
+
const prompt = eventData.prompt || '';
|
|
52
|
+
const placeholder = eventData.placeholder || '';
|
|
53
|
+
const required = eventData.required || false;
|
|
54
|
+
const defaultValue = eventData.default || '';
|
|
55
|
+
const disabled = eventData.disabled || false;
|
|
56
|
+
const readonly = eventData.readonly || false;
|
|
57
|
+
const multiple = eventData.multiple || false;
|
|
58
|
+
const title = eventData.title || '';
|
|
59
|
+
const datalist = eventData.datalist || null;
|
|
60
|
+
|
|
61
|
+
// Conditional rendering support (if parameter from zWizard)
|
|
62
|
+
const condition = eventData.if || null;
|
|
63
|
+
if (condition) {
|
|
64
|
+
this.logger.log(`[InputEventHandler] Found 'if' condition in read_string event: "${condition}"`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Support zId (universal), _zId (Bifrost-only), and _id (legacy)
|
|
68
|
+
const inputId = eventData.zId || eventData._zId || eventData._id || `input_${Math.random().toString(36).substr(2, 9)}`;
|
|
69
|
+
|
|
70
|
+
// Generate datalist ID if datalist exists
|
|
71
|
+
const datalistId = datalist ? `${inputId}_datalist` : null;
|
|
72
|
+
|
|
73
|
+
// Support aria-describedby for accessibility (link to help text)
|
|
74
|
+
const ariaDescribedBy = eventData.aria_described_by || eventData.ariaDescribedBy || eventData['aria-describedby'];
|
|
75
|
+
|
|
76
|
+
// Detect if we're inside a zInputGroup context (parent or ancestor has zInputGroup class)
|
|
77
|
+
let isInsideInputGroup = false;
|
|
78
|
+
let checkParent = parentElement;
|
|
79
|
+
while (checkParent && checkParent !== document.body) {
|
|
80
|
+
if (checkParent.classList && checkParent.classList.contains('zInputGroup')) {
|
|
81
|
+
isInsideInputGroup = true;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
checkParent = checkParent.parentElement;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Create wrapper div only if prompt exists AND not inside input group
|
|
88
|
+
// Otherwise return input directly to avoid double-nesting in grid layouts or input groups
|
|
89
|
+
let wrapper = null;
|
|
90
|
+
let wrapperClasses = null; // Track _zClass for wrapper (not input element)
|
|
91
|
+
|
|
92
|
+
// Create label if prompt exists (connected to input via for/id)
|
|
93
|
+
if (prompt && !isInsideInputGroup) {
|
|
94
|
+
wrapper = document.createElement('div');
|
|
95
|
+
|
|
96
|
+
// Apply _zClass to wrapper (not input element) to avoid double-nesting
|
|
97
|
+
// When wrapper exists, _zClass applies to the wrapper container
|
|
98
|
+
if (eventData._zClass) {
|
|
99
|
+
const classes = eventData._zClass.split(' ').filter(c => c.trim());
|
|
100
|
+
wrapper.classList.add(...classes);
|
|
101
|
+
wrapperClasses = eventData._zClass;
|
|
102
|
+
this.logger.log(`[InputEventHandler] Applied _zClass to wrapper: ${eventData._zClass}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// SSOT — every prompt label is a .zLabel (styled by zbase.css), regardless
|
|
106
|
+
// of _zClass. No magic-string branch → consistent label across all fields.
|
|
107
|
+
const label = createLabel(inputId, { class: 'zLabel' });
|
|
108
|
+
label.textContent = prompt;
|
|
109
|
+
appendRequiredMark(label, required);
|
|
110
|
+
wrapper.appendChild(label);
|
|
111
|
+
// Add line break after label (semantic HTML pattern)
|
|
112
|
+
wrapper.appendChild(document.createElement('br'));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Check if we'll have prefix/suffix (which creates zInputGroup)
|
|
116
|
+
// Helper to format prefix/suffix values (defined early for class determination)
|
|
117
|
+
const formatAffix = (value) => {
|
|
118
|
+
if (!value && value !== 0) return '';
|
|
119
|
+
if (typeof value === 'string') return value;
|
|
120
|
+
if (typeof value === 'boolean') return String(value);
|
|
121
|
+
if (typeof value === 'number') {
|
|
122
|
+
if (value >= 0 && value < 1) {
|
|
123
|
+
return value.toFixed(2).replace(/^0/, '') || '0';
|
|
124
|
+
}
|
|
125
|
+
return String(value);
|
|
126
|
+
}
|
|
127
|
+
return String(value);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const prefix = formatAffix(eventData.prefix);
|
|
131
|
+
const suffix = formatAffix(eventData.suffix);
|
|
132
|
+
const hasInputGroup = !!(prefix || suffix);
|
|
133
|
+
|
|
134
|
+
// Build input classes:
|
|
135
|
+
// - If inside zInputGroup, use 'zInput' (required by CSS: .zInputGroup > .zInput)
|
|
136
|
+
// - If wrapper exists, use default 'zForm-control' (wrapper has _zClass)
|
|
137
|
+
// - Otherwise use _zClass or default 'zForm-control'
|
|
138
|
+
let inputClasses;
|
|
139
|
+
if (isInsideInputGroup) {
|
|
140
|
+
// If inside zInputGroup, use 'zInput' class (required by CSS: .zInputGroup > .zInput)
|
|
141
|
+
inputClasses = 'zInput';
|
|
142
|
+
} else if (hasInputGroup) {
|
|
143
|
+
// Input groups require 'zInput' class for proper flex styling
|
|
144
|
+
inputClasses = 'zInput';
|
|
145
|
+
} else if (wrapperClasses) {
|
|
146
|
+
// Wrapper has _zClass, input gets default styling
|
|
147
|
+
inputClasses = 'zForm-control';
|
|
148
|
+
} else {
|
|
149
|
+
// No wrapper, use _zClass if provided, otherwise default
|
|
150
|
+
inputClasses = eventData._zClass || 'zForm-control';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Handle textarea vs input
|
|
154
|
+
let inputElement;
|
|
155
|
+
if (inputType === 'textarea') {
|
|
156
|
+
// Multi-line textarea
|
|
157
|
+
const rows = eventData.rows || 3;
|
|
158
|
+
const textareaAttrs = {
|
|
159
|
+
id: inputId,
|
|
160
|
+
placeholder: placeholder,
|
|
161
|
+
required: required,
|
|
162
|
+
rows: rows,
|
|
163
|
+
class: inputClasses
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
if (ariaDescribedBy) {
|
|
167
|
+
textareaAttrs['aria-describedby'] = ariaDescribedBy;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (disabled) {
|
|
171
|
+
textareaAttrs.disabled = true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (readonly) {
|
|
175
|
+
textareaAttrs.readonly = true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (title) {
|
|
179
|
+
textareaAttrs.title = title;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
inputElement = createTextarea(textareaAttrs);
|
|
183
|
+
inputElement.textContent = defaultValue; // Use textContent for textarea, not value
|
|
184
|
+
} else {
|
|
185
|
+
// Single-line input
|
|
186
|
+
const inputAttrs = {
|
|
187
|
+
id: inputId,
|
|
188
|
+
placeholder: placeholder,
|
|
189
|
+
required: required,
|
|
190
|
+
value: defaultValue,
|
|
191
|
+
class: inputClasses
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Add list attribute if datalist exists
|
|
195
|
+
if (datalistId) {
|
|
196
|
+
inputAttrs.list = datalistId;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (ariaDescribedBy) {
|
|
200
|
+
inputAttrs['aria-describedby'] = ariaDescribedBy;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (disabled) {
|
|
204
|
+
inputAttrs.disabled = true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (readonly) {
|
|
208
|
+
inputAttrs.readonly = true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (multiple) {
|
|
212
|
+
inputAttrs.multiple = true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (title) {
|
|
216
|
+
inputAttrs.title = title;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Field name (needed for multipart field + stable selectors) and file accept filter.
|
|
220
|
+
if (eventData.name) {
|
|
221
|
+
inputAttrs.name = eventData.name;
|
|
222
|
+
}
|
|
223
|
+
if (inputType === 'file' && eventData.accept) {
|
|
224
|
+
inputAttrs.accept = eventData.accept;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
inputElement = createInput(inputType, inputAttrs);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Handle input groups (prefix/suffix pattern) - Terminal-first design
|
|
231
|
+
// Note: prefix and suffix were already determined above for class selection
|
|
232
|
+
if (hasInputGroup) {
|
|
233
|
+
// Create .zInputGroup wrapper for prefix/suffix pattern
|
|
234
|
+
const inputGroup = document.createElement('div');
|
|
235
|
+
inputGroup.classList.add('zInputGroup');
|
|
236
|
+
|
|
237
|
+
// Add prefix text before input
|
|
238
|
+
// Position-specific class (zInputGroup-prefix) lets users restyle the prefix
|
|
239
|
+
// alone (color/font) without touching the input or the suffix.
|
|
240
|
+
if (prefix) {
|
|
241
|
+
const prefixSpan = document.createElement('span');
|
|
242
|
+
prefixSpan.classList.add('zInputGroup-text', 'zInputGroup-prefix');
|
|
243
|
+
prefixSpan.textContent = prefix;
|
|
244
|
+
inputGroup.appendChild(prefixSpan);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Add input element
|
|
248
|
+
inputGroup.appendChild(inputElement);
|
|
249
|
+
|
|
250
|
+
// Add suffix text after input
|
|
251
|
+
// Position-specific class (zInputGroup-suffix) lets users restyle the suffix
|
|
252
|
+
// alone (color/font) without touching the input or the prefix.
|
|
253
|
+
if (suffix) {
|
|
254
|
+
const suffixSpan = document.createElement('span');
|
|
255
|
+
suffixSpan.classList.add('zInputGroup-text', 'zInputGroup-suffix');
|
|
256
|
+
suffixSpan.textContent = suffix;
|
|
257
|
+
inputGroup.appendChild(suffixSpan);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Replace inputElement with the input group
|
|
261
|
+
inputElement = inputGroup;
|
|
262
|
+
|
|
263
|
+
this.logger.log(`[InputEventHandler] Created input group with prefix='${prefix}', suffix='${suffix}'`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// If wrapper exists (has prompt), append input to wrapper and return wrapper
|
|
267
|
+
// Otherwise return input/textarea directly to avoid double-nesting in grid layouts
|
|
268
|
+
let element;
|
|
269
|
+
if (wrapper) {
|
|
270
|
+
wrapper.appendChild(inputElement);
|
|
271
|
+
|
|
272
|
+
// Add datalist element if datalist exists
|
|
273
|
+
if (datalist && Array.isArray(datalist)) {
|
|
274
|
+
const datalistElement = document.createElement('datalist');
|
|
275
|
+
datalistElement.id = datalistId;
|
|
276
|
+
|
|
277
|
+
datalist.forEach(optionValue => {
|
|
278
|
+
const option = document.createElement('option');
|
|
279
|
+
option.value = optionValue;
|
|
280
|
+
datalistElement.appendChild(option);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
wrapper.appendChild(datalistElement);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Apply _zStyle to wrapper if present (when wrapper exists, styles go on wrapper)
|
|
287
|
+
if (eventData._zStyle) {
|
|
288
|
+
const cssString = convertStyleToString(eventData._zStyle, this.logger);
|
|
289
|
+
if (cssString) {
|
|
290
|
+
wrapper.setAttribute('style', cssString);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Handle conditional rendering (if parameter from zWizard)
|
|
295
|
+
if (condition) {
|
|
296
|
+
wrapper.setAttribute('data-zif', condition);
|
|
297
|
+
wrapper.style.display = 'none'; // Initially hidden
|
|
298
|
+
this.logger.log(`[InputEventHandler] Input with condition: ${condition} (initially hidden)`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
element = wrapper;
|
|
302
|
+
} else {
|
|
303
|
+
// When returning input/textarea directly, apply _zStyle if present
|
|
304
|
+
// This allows inline styles for grid layout adjustments (e.g., padding-top)
|
|
305
|
+
if (eventData._zStyle) {
|
|
306
|
+
const cssString = convertStyleToString(eventData._zStyle, this.logger);
|
|
307
|
+
if (cssString) {
|
|
308
|
+
inputElement.setAttribute('style', cssString);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// If datalist exists but no wrapper, create minimal wrapper for datalist
|
|
313
|
+
if (datalist && Array.isArray(datalist)) {
|
|
314
|
+
const container = document.createElement('div');
|
|
315
|
+
container.appendChild(inputElement);
|
|
316
|
+
|
|
317
|
+
const datalistElement = document.createElement('datalist');
|
|
318
|
+
datalistElement.id = datalistId;
|
|
319
|
+
|
|
320
|
+
datalist.forEach(optionValue => {
|
|
321
|
+
const option = document.createElement('option');
|
|
322
|
+
option.value = optionValue;
|
|
323
|
+
datalistElement.appendChild(option);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
container.appendChild(datalistElement);
|
|
327
|
+
|
|
328
|
+
// Handle conditional rendering
|
|
329
|
+
if (condition) {
|
|
330
|
+
container.setAttribute('data-zif', condition);
|
|
331
|
+
container.style.display = 'none'; // Initially hidden
|
|
332
|
+
this.logger.log(`[InputEventHandler] Input with condition: ${condition} (initially hidden)`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
element = container;
|
|
336
|
+
} else {
|
|
337
|
+
// Handle conditional rendering for bare input
|
|
338
|
+
if (condition) {
|
|
339
|
+
inputElement.setAttribute('data-zif', condition);
|
|
340
|
+
inputElement.style.display = 'none'; // Initially hidden
|
|
341
|
+
this.logger.log(`[InputEventHandler] Input with condition: ${condition} (initially hidden)`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
element = inputElement;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Declarative zAPI upload: a file input whose block carried onChange.zAPI is
|
|
349
|
+
// stamped server-side with zapi_url/zapi_method. Bind change → multipart POST
|
|
350
|
+
// → swap the nearest avatar img. (No zFunc ever reaches the client.)
|
|
351
|
+
if (inputType === 'file' && eventData.zapi_url) {
|
|
352
|
+
const fileInput = (element.tagName === 'INPUT' && element.type === 'file')
|
|
353
|
+
? element
|
|
354
|
+
: element.querySelector('input[type=file]');
|
|
355
|
+
if (fileInput) {
|
|
356
|
+
this._bindZapiUpload(fileInput, eventData);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
this.logger.log(`[InputEventHandler] Rendered ${event} ${inputType} (id=${inputId}, aria-describedby=${ariaDescribedBy || 'none'}, condition=${condition || 'none'})`);
|
|
361
|
+
return element;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Bind a declarative zAPI file upload to a file input.
|
|
366
|
+
* On change: POST the selected file as multipart to the server-stamped endpoint,
|
|
367
|
+
* then on a {ok, data.url} envelope swap the nearest avatar image.
|
|
368
|
+
* @param {HTMLInputElement} fileInput
|
|
369
|
+
* @param {Object} eventData - carries zapi_url, zapi_method, zapi_field/name
|
|
370
|
+
*/
|
|
371
|
+
_bindZapiUpload(fileInput, eventData) {
|
|
372
|
+
const url = eventData.zapi_url;
|
|
373
|
+
const method = (eventData.zapi_method || 'POST').toUpperCase();
|
|
374
|
+
const field = eventData.zapi_field || eventData.name || 'file';
|
|
375
|
+
|
|
376
|
+
fileInput.addEventListener('change', async () => {
|
|
377
|
+
const file = fileInput.files && fileInput.files[0];
|
|
378
|
+
if (!file) return;
|
|
379
|
+
|
|
380
|
+
const form = new FormData();
|
|
381
|
+
form.append(field, file, file.name);
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
this.logger.log(`[InputEventHandler] zAPI upload → ${method} ${url} (${file.name}, ${file.size}B)`);
|
|
385
|
+
const resp = await fetch(url, { method, body: form, credentials: 'same-origin' });
|
|
386
|
+
let json = null;
|
|
387
|
+
try { json = await resp.json(); } catch (_) { /* non-JSON */ }
|
|
388
|
+
|
|
389
|
+
if (resp.ok && json && json.ok && json.data && json.data.url) {
|
|
390
|
+
this._swapAvatar(fileInput, json.data.url);
|
|
391
|
+
this.logger.log(`[InputEventHandler] zAPI upload ok → ${json.data.url}`);
|
|
392
|
+
} else {
|
|
393
|
+
const err = (json && json.error) || `HTTP ${resp.status}`;
|
|
394
|
+
this.logger.error(`[InputEventHandler] zAPI upload failed: ${err}`);
|
|
395
|
+
}
|
|
396
|
+
} catch (e) {
|
|
397
|
+
this.logger.error(`[InputEventHandler] zAPI upload error: ${e}`);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Swap the nearest avatar image to the freshly uploaded URL.
|
|
404
|
+
* Prefers an `img.acct-avatar` in the input's ancestry, then falls back globally.
|
|
405
|
+
* @param {HTMLInputElement} fileInput
|
|
406
|
+
* @param {string} url - already cache-busted server-side (?v=...)
|
|
407
|
+
*/
|
|
408
|
+
_swapAvatar(fileInput, url) {
|
|
409
|
+
let img = null;
|
|
410
|
+
let anc = fileInput.parentElement;
|
|
411
|
+
while (anc && anc !== document.body) {
|
|
412
|
+
const candidate = anc.querySelector('img.acct-avatar');
|
|
413
|
+
if (candidate) { img = candidate; break; }
|
|
414
|
+
anc = anc.parentElement;
|
|
415
|
+
}
|
|
416
|
+
if (!img) img = document.querySelector('img.acct-avatar');
|
|
417
|
+
if (img) img.src = url;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Handle read_bool event (checkbox)
|
|
422
|
+
* @param {string} event - Event type ('read_bool')
|
|
423
|
+
* @param {Object} eventData - Event data
|
|
424
|
+
* @param {HTMLElement} parentElement - Parent element for context detection
|
|
425
|
+
* @returns {HTMLElement} - Checkbox element or wrapper
|
|
426
|
+
*/
|
|
427
|
+
async handleBoolInput(event, eventData, parentElement) {
|
|
428
|
+
const { createDiv } = await import('../../L2_Handling/display/primitives/generic_containers.js');
|
|
429
|
+
const { createLabel, createInput } = await import('../../L2_Handling/display/primitives/form_primitives.js');
|
|
430
|
+
|
|
431
|
+
const prompt = eventData.prompt || eventData.label || '';
|
|
432
|
+
const checked = eventData.checked || false;
|
|
433
|
+
const required = eventData.required || false;
|
|
434
|
+
const disabled = eventData.disabled || false;
|
|
435
|
+
|
|
436
|
+
// Build checkbox classes from _zClass (defaults to zForm-check-input)
|
|
437
|
+
const checkboxClasses = eventData._zClass || 'zForm-check-input';
|
|
438
|
+
|
|
439
|
+
// Support zId (universal), _zId (Bifrost-only), and _id (legacy)
|
|
440
|
+
const checkboxId = eventData.zId || eventData._zId || eventData._id || `checkbox_${Math.random().toString(36).substr(2, 9)}`;
|
|
441
|
+
|
|
442
|
+
// Detect if we're inside a zInputGroup context (parent has zInputGroup-text class)
|
|
443
|
+
const isInsideInputGroup = parentElement && parentElement.classList && parentElement.classList.contains('zInputGroup-text');
|
|
444
|
+
|
|
445
|
+
// Create checkbox input (type='checkbox')
|
|
446
|
+
const checkbox = createInput('checkbox', {
|
|
447
|
+
checked: checked,
|
|
448
|
+
required: required,
|
|
449
|
+
disabled: disabled,
|
|
450
|
+
class: checkboxClasses,
|
|
451
|
+
id: checkboxId
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// Store zCross flag on checkbox element (defaults to false if not set)
|
|
455
|
+
// zCross: true = terminal-first behavior (conditional rendering)
|
|
456
|
+
// zCross: false = HTML-like behavior (always visible)
|
|
457
|
+
const zCross = eventData.zCross !== undefined ? eventData.zCross : false;
|
|
458
|
+
checkbox.setAttribute('data-zcross', zCross.toString());
|
|
459
|
+
|
|
460
|
+
let element;
|
|
461
|
+
// If inside input group, render checkbox directly without wrapper
|
|
462
|
+
if (isInsideInputGroup) {
|
|
463
|
+
element = checkbox;
|
|
464
|
+
this.logger.log(`[InputEventHandler] Rendered ${event} checkbox (input-group mode, no wrapper): (id=${checkboxId}, checked=${checked})`);
|
|
465
|
+
} else {
|
|
466
|
+
// Normal mode: Create form check container (Bootstrap-style checkbox)
|
|
467
|
+
const formCheck = createDiv({ class: disabled ? 'zForm-check zForm-check-disabled zmb-2' : 'zForm-check zmb-2' });
|
|
468
|
+
|
|
469
|
+
// Create label for checkbox (wraps around or uses 'for' attribute)
|
|
470
|
+
if (prompt) {
|
|
471
|
+
const label = createLabel(checkboxId, { class: 'zForm-check-label' });
|
|
472
|
+
label.textContent = prompt;
|
|
473
|
+
|
|
474
|
+
// Add checkbox first, then label (Bootstrap convention)
|
|
475
|
+
formCheck.appendChild(checkbox);
|
|
476
|
+
formCheck.appendChild(label);
|
|
477
|
+
} else {
|
|
478
|
+
// No label, just the checkbox
|
|
479
|
+
formCheck.appendChild(checkbox);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
element = formCheck;
|
|
483
|
+
this.logger.log(`[InputEventHandler] Rendered ${event} checkbox: ${prompt} (id=${checkboxId}, checked=${checked})`);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return element;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Handle selection event (dropdown, radio, checkbox)
|
|
491
|
+
* @param {string} event - Event type ('selection')
|
|
492
|
+
* @param {Object} eventData - Event data
|
|
493
|
+
* @param {HTMLElement} parentElement - Parent element for context detection
|
|
494
|
+
* @returns {HTMLElement} - Selection element or wrapper
|
|
495
|
+
*/
|
|
496
|
+
/**
|
|
497
|
+
* Parse option string for inline modifiers like [disabled] or [default]
|
|
498
|
+
* @private
|
|
499
|
+
*/
|
|
500
|
+
_parseOptionString(optionString) {
|
|
501
|
+
let cleanLabel = optionString;
|
|
502
|
+
let isDisabled = false;
|
|
503
|
+
let isDefault = false;
|
|
504
|
+
|
|
505
|
+
// Check for [disabled] suffix
|
|
506
|
+
const disabledMatch = optionString.match(/^(.*?)\s*\[disabled\]\s*$/i);
|
|
507
|
+
if (disabledMatch) {
|
|
508
|
+
cleanLabel = disabledMatch[1].trim();
|
|
509
|
+
isDisabled = true;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Check for [default] suffix
|
|
513
|
+
const defaultMatch = cleanLabel.match(/^(.*?)\s*\[default\]\s*$/i);
|
|
514
|
+
if (defaultMatch) {
|
|
515
|
+
cleanLabel = defaultMatch[1].trim();
|
|
516
|
+
isDefault = true;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return { cleanLabel, isDisabled, isDefault };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async handleSelection(event, eventData, parentElement) {
|
|
523
|
+
const { createLabel, createInput } = await import('../../L2_Handling/display/primitives/form_primitives.js');
|
|
524
|
+
|
|
525
|
+
const prompt = eventData.prompt || '';
|
|
526
|
+
const options = eventData.options || [];
|
|
527
|
+
const multi = eventData.multi || false;
|
|
528
|
+
let defaultValue = eventData.default || null;
|
|
529
|
+
const disabled = eventData.disabled || false;
|
|
530
|
+
const required = eventData.required || false;
|
|
531
|
+
// `type` (interactive WS path) or `widget_type` (declarative — the zSelect
|
|
532
|
+
// expander renames type→widget_type). Accept both; default to dropdown.
|
|
533
|
+
const type = eventData.type || eventData.widget_type || 'dropdown';
|
|
534
|
+
|
|
535
|
+
// Auto-detect default from [default] suffix if no explicit default provided
|
|
536
|
+
if (defaultValue === null && options.length > 0) {
|
|
537
|
+
for (const opt of options) {
|
|
538
|
+
if (typeof opt === 'string') {
|
|
539
|
+
const parsed = this._parseOptionString(opt);
|
|
540
|
+
if (parsed.isDefault) {
|
|
541
|
+
defaultValue = parsed.cleanLabel;
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Build classes from _zClass
|
|
549
|
+
const elementClasses = eventData._zClass || '';
|
|
550
|
+
|
|
551
|
+
// Support zId (universal), _zId (Bifrost-only), and _id (legacy)
|
|
552
|
+
const baseId = eventData.zId || eventData._zId || eventData._id || `select_${Math.random().toString(36).substr(2, 9)}`;
|
|
553
|
+
|
|
554
|
+
// Support aria-label for accessibility
|
|
555
|
+
const ariaLabel = eventData['_aria-label'] || eventData.ariaLabel || eventData['aria-label'];
|
|
556
|
+
|
|
557
|
+
// Detect if we're inside a zInputGroup context (for compact rendering)
|
|
558
|
+
const isInsideInputGroup = parentElement && parentElement.classList && parentElement.classList.contains('zInputGroup-text');
|
|
559
|
+
|
|
560
|
+
let element;
|
|
561
|
+
// Render based on type
|
|
562
|
+
if (type === 'radio' || (type === 'checkbox' && multi)) {
|
|
563
|
+
// Radio button group or checkbox group
|
|
564
|
+
const inputType = type === 'radio' ? 'radio' : 'checkbox';
|
|
565
|
+
const groupName = baseId; // Use baseId as group name for radio buttons
|
|
566
|
+
|
|
567
|
+
// CHUNK MODE: When inside zInputGroup-text, render raw radios without labels/wrappers
|
|
568
|
+
if (isInsideInputGroup) {
|
|
569
|
+
// Just render the first radio input directly (for single-option-per-group pattern)
|
|
570
|
+
// Or all radios stacked if multiple options
|
|
571
|
+
const firstOptionValue = options[0];
|
|
572
|
+
const optionVal = typeof firstOptionValue === 'string' ? firstOptionValue : (firstOptionValue.value || firstOptionValue.label || '');
|
|
573
|
+
|
|
574
|
+
const input = createInput(inputType, {
|
|
575
|
+
id: `${baseId}_0`,
|
|
576
|
+
name: groupName,
|
|
577
|
+
value: optionVal,
|
|
578
|
+
disabled: disabled,
|
|
579
|
+
required: required,
|
|
580
|
+
class: elementClasses || 'zCheck-input'
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
// Set checked state
|
|
584
|
+
if (defaultValue !== null && (optionVal === defaultValue || (Array.isArray(defaultValue) && defaultValue.includes(optionVal)))) {
|
|
585
|
+
input.checked = true;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
element = input;
|
|
589
|
+
this.logger.log(`[InputEventHandler] Rendered ${event} ${inputType} (input-group mode, no wrapper): (id=${baseId}, value=${optionVal})`);
|
|
590
|
+
} else {
|
|
591
|
+
// NORMAL MODE: Standard radio/checkbox group with labels
|
|
592
|
+
// Create container
|
|
593
|
+
const container = document.createElement('div');
|
|
594
|
+
container.className = elementClasses ? `${elementClasses} zForm-check-group` : 'zForm-check-group';
|
|
595
|
+
if (eventData._zStyle) {
|
|
596
|
+
const cssString = convertStyleToString(eventData._zStyle, this.logger);
|
|
597
|
+
if (cssString) {
|
|
598
|
+
container.setAttribute('style', cssString);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Create prompt label if exists (fieldset legend style)
|
|
603
|
+
if (prompt) {
|
|
604
|
+
const promptLabel = document.createElement('div');
|
|
605
|
+
promptLabel.textContent = prompt;
|
|
606
|
+
promptLabel.className = 'zLabel';
|
|
607
|
+
appendRequiredMark(promptLabel, required);
|
|
608
|
+
container.appendChild(promptLabel);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Create radio/checkbox inputs for each option
|
|
612
|
+
options.forEach((optionValue, index) => {
|
|
613
|
+
const optionId = `${baseId}_${index}`;
|
|
614
|
+
|
|
615
|
+
// Parse option string for modifiers or extract from object
|
|
616
|
+
let optionLabel, optionVal, optionDisabled, optionIsDefault;
|
|
617
|
+
if (typeof optionValue === 'string') {
|
|
618
|
+
const parsed = this._parseOptionString(optionValue);
|
|
619
|
+
optionLabel = parsed.cleanLabel;
|
|
620
|
+
optionVal = parsed.cleanLabel;
|
|
621
|
+
optionDisabled = parsed.isDisabled;
|
|
622
|
+
optionIsDefault = parsed.isDefault;
|
|
623
|
+
} else {
|
|
624
|
+
optionLabel = optionValue.label || optionValue.value || '';
|
|
625
|
+
optionVal = optionValue.value || optionValue.label || '';
|
|
626
|
+
optionDisabled = optionValue.disabled || false;
|
|
627
|
+
optionIsDefault = false;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Create wrapper div for input + label (canonical .zForm-check row)
|
|
631
|
+
const rowDisabled = disabled || optionDisabled;
|
|
632
|
+
const optionWrapper = document.createElement('div');
|
|
633
|
+
optionWrapper.className = rowDisabled ? 'zForm-check zForm-check-disabled zmb-2' : 'zForm-check zmb-2';
|
|
634
|
+
|
|
635
|
+
// Create input with per-option disabled state
|
|
636
|
+
const input = createInput(inputType, {
|
|
637
|
+
id: optionId,
|
|
638
|
+
name: groupName,
|
|
639
|
+
value: optionVal,
|
|
640
|
+
disabled: rowDisabled, // Component-level OR per-option disabled
|
|
641
|
+
required: required && index === 0, // Only first input has required
|
|
642
|
+
class: 'zForm-check-input'
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// Set checked state based on default value
|
|
646
|
+
if (defaultValue !== null) {
|
|
647
|
+
if (multi && Array.isArray(defaultValue)) {
|
|
648
|
+
// Multi-select (checkbox): check if option is in default array
|
|
649
|
+
if (defaultValue.includes(optionVal) || defaultValue.includes(optionLabel)) {
|
|
650
|
+
input.checked = true;
|
|
651
|
+
}
|
|
652
|
+
} else {
|
|
653
|
+
// Single-select (radio): check if option matches default
|
|
654
|
+
if (optionVal === defaultValue || optionLabel === defaultValue) {
|
|
655
|
+
input.checked = true;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Create label
|
|
661
|
+
const label = createLabel(optionId, { class: 'zForm-check-label' });
|
|
662
|
+
label.textContent = optionLabel;
|
|
663
|
+
|
|
664
|
+
// Assemble option
|
|
665
|
+
optionWrapper.appendChild(input);
|
|
666
|
+
optionWrapper.appendChild(label);
|
|
667
|
+
container.appendChild(optionWrapper);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
element = container;
|
|
671
|
+
this.logger.log(`[InputEventHandler] Rendered ${event} ${inputType} group (id=${baseId}, options=${options.length})`);
|
|
672
|
+
}
|
|
673
|
+
} else {
|
|
674
|
+
// Dropdown select (default behavior)
|
|
675
|
+
let wrapper = null;
|
|
676
|
+
|
|
677
|
+
// Create label if prompt exists
|
|
678
|
+
if (prompt) {
|
|
679
|
+
wrapper = document.createElement('div');
|
|
680
|
+
// Use zLabel class for styled selects
|
|
681
|
+
const labelClass = elementClasses.includes('zSelect') ? 'zLabel' : '';
|
|
682
|
+
const labelAttrs = labelClass ? { class: labelClass } : {};
|
|
683
|
+
const label = createLabel(baseId, labelAttrs);
|
|
684
|
+
label.textContent = prompt;
|
|
685
|
+
appendRequiredMark(label, required);
|
|
686
|
+
wrapper.appendChild(label);
|
|
687
|
+
// Add line break after label (semantic HTML pattern)
|
|
688
|
+
wrapper.appendChild(document.createElement('br'));
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Create select element
|
|
692
|
+
const selectElement = document.createElement('select');
|
|
693
|
+
selectElement.id = baseId;
|
|
694
|
+
|
|
695
|
+
// Always carry the canonical .zSelect base so every dropdown / multi-select
|
|
696
|
+
// gets themed — even when _zClass overrides it (merge + de-dupe).
|
|
697
|
+
const selectClassList = elementClasses ? elementClasses.split(/\s+/).filter(Boolean) : [];
|
|
698
|
+
if (!selectClassList.includes('zSelect')) selectClassList.unshift('zSelect');
|
|
699
|
+
selectElement.className = selectClassList.join(' ');
|
|
700
|
+
|
|
701
|
+
if (disabled) {
|
|
702
|
+
selectElement.disabled = true;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (required) {
|
|
706
|
+
selectElement.required = true;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (multi) {
|
|
710
|
+
selectElement.multiple = true;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Support size attribute (number of visible options)
|
|
714
|
+
const size = eventData.size || null;
|
|
715
|
+
if (size !== null) {
|
|
716
|
+
selectElement.size = size;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (ariaLabel) {
|
|
720
|
+
selectElement.setAttribute('aria-label', ariaLabel);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Add autocomplete="off" to prevent browser from remembering selections
|
|
724
|
+
selectElement.setAttribute('autocomplete', 'off');
|
|
725
|
+
|
|
726
|
+
// Apply inline styles if no wrapper (to avoid nesting issues)
|
|
727
|
+
if (!wrapper && eventData._zStyle) {
|
|
728
|
+
const cssString = convertStyleToString(eventData._zStyle, this.logger);
|
|
729
|
+
if (cssString) {
|
|
730
|
+
selectElement.setAttribute('style', cssString);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Create option elements
|
|
735
|
+
options.forEach((optionValue, index) => {
|
|
736
|
+
const optionElement = document.createElement('option');
|
|
737
|
+
|
|
738
|
+
// Parse option string for modifiers or extract from object
|
|
739
|
+
let optionLabel, optionVal, optionDisabled;
|
|
740
|
+
if (typeof optionValue === 'string') {
|
|
741
|
+
const parsed = this._parseOptionString(optionValue);
|
|
742
|
+
optionLabel = parsed.cleanLabel;
|
|
743
|
+
optionVal = parsed.cleanLabel;
|
|
744
|
+
optionDisabled = parsed.isDisabled;
|
|
745
|
+
} else {
|
|
746
|
+
optionLabel = optionValue.label || optionValue.value || '';
|
|
747
|
+
optionVal = optionValue.value || optionValue.label || '';
|
|
748
|
+
optionDisabled = optionValue.disabled || false;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
optionElement.textContent = optionLabel;
|
|
752
|
+
optionElement.value = optionVal;
|
|
753
|
+
|
|
754
|
+
// Set disabled state (per-option)
|
|
755
|
+
if (optionDisabled) {
|
|
756
|
+
optionElement.disabled = true;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Set selected state based on default value
|
|
760
|
+
if (defaultValue !== null) {
|
|
761
|
+
if (multi && Array.isArray(defaultValue)) {
|
|
762
|
+
// Multi-select: check if option is in default array
|
|
763
|
+
if (defaultValue.includes(optionVal)) {
|
|
764
|
+
optionElement.selected = true;
|
|
765
|
+
}
|
|
766
|
+
} else {
|
|
767
|
+
// Single-select: check if option matches default
|
|
768
|
+
if (optionVal === defaultValue || optionLabel === defaultValue) {
|
|
769
|
+
optionElement.selected = true;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
selectElement.appendChild(optionElement);
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
// Assemble final element
|
|
778
|
+
if (wrapper) {
|
|
779
|
+
wrapper.appendChild(selectElement);
|
|
780
|
+
// Apply wrapper styles if specified
|
|
781
|
+
if (eventData._zStyle) {
|
|
782
|
+
const cssString = convertStyleToString(eventData._zStyle, this.logger);
|
|
783
|
+
if (cssString) {
|
|
784
|
+
wrapper.setAttribute('style', cssString);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
element = wrapper;
|
|
788
|
+
} else {
|
|
789
|
+
element = selectElement;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
this.logger.log(`[InputEventHandler] Rendered ${event} select (id=${baseId}, options=${options.length}, multi=${multi})`);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
return element;
|
|
796
|
+
}
|
|
797
|
+
}
|