@componyx/ui 0.0.0-stage → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/AppSkin/AppSkin.css +1268 -0
  2. package/AppSkin/AppSkin.js +2748 -0
  3. package/AppSkin/Themes/Default.css +241 -0
  4. package/Base/Base.js +3407 -0
  5. package/Base/ComponentIcons.css +66 -0
  6. package/Base/Font/ComponentIcons.woff +0 -0
  7. package/Base/Font/UI.woff +0 -0
  8. package/Base/Library.js +8432 -0
  9. package/Base/NavigationManager.js +462 -0
  10. package/Base/ResponsiveTemplates.js +371 -0
  11. package/Base/Sanitizer.js +288 -0
  12. package/Base/Themes/Default.css +255 -0
  13. package/Base/UI.css +618 -0
  14. package/Base/UIResourcePath.js +6 -0
  15. package/Bindary/Bindary.js +2011 -0
  16. package/Bindary/Modules/Connection.js +216 -0
  17. package/Bindary/Modules/Core.js +854 -0
  18. package/Bindary/Modules/DataUpdater.js +351 -0
  19. package/Bindary/Modules/ExpressionEngine.js +347 -0
  20. package/Bindary/Modules/ProxyManager.js +482 -0
  21. package/Bindary/Modules/Renderer.js +605 -0
  22. package/Bindary/Modules/Router.js +291 -0
  23. package/Bindary/Modules/Scroller.js +89 -0
  24. package/Bindary/Modules/Template.js +1273 -0
  25. package/BookmarkSpy/BookmarkSpy.js +430 -0
  26. package/Box/Box.css +36 -0
  27. package/Box/Box.js +1685 -0
  28. package/Button/Button.css +470 -0
  29. package/Button/Button.js +1398 -0
  30. package/Button/Themes/Default.css +200 -0
  31. package/Carousel/Carousel.css +273 -0
  32. package/Carousel/Carousel.js +607 -0
  33. package/ColorButton/ColorButton.css +39 -0
  34. package/ColorButton/ColorButton.js +336 -0
  35. package/ColorPicker/ColorPicker.css +359 -0
  36. package/ColorPicker/ColorPicker.js +972 -0
  37. package/ComboBox/ComboBox.css +357 -0
  38. package/ComboBox/ComboBox.js +2342 -0
  39. package/ComboBox/Themes/Default.css +23 -0
  40. package/DataPager/DataPager.css +72 -0
  41. package/DataPager/DataPager.js +674 -0
  42. package/DatePicker/DatePicker.css +227 -0
  43. package/DatePicker/DatePicker.js +1531 -0
  44. package/DatePicker/Themes/Default.css +26 -0
  45. package/Dialog/Dialog.css +106 -0
  46. package/Dialog/Dialog.js +572 -0
  47. package/Dialog/Themes/Default.css +10 -0
  48. package/Editor/Data/Emoji.json +8285 -0
  49. package/Editor/Data/Special.json +1042 -0
  50. package/Editor/Editor.css +1562 -0
  51. package/Editor/Editor.js +4058 -0
  52. package/Editor/Font/Editor.woff +0 -0
  53. package/Editor/Modules/ComponentFactory.js +565 -0
  54. package/Editor/Modules/ContentManager.js +748 -0
  55. package/Editor/Modules/DialogManager.js +1234 -0
  56. package/Editor/Modules/EventManager.js +458 -0
  57. package/Editor/Modules/Format.js +950 -0
  58. package/Editor/Modules/History.js +191 -0
  59. package/Editor/Modules/LayoutState.js +379 -0
  60. package/Editor/Modules/ListManager.js +296 -0
  61. package/Editor/Modules/MenuManager.js +388 -0
  62. package/Editor/Modules/NodeManager.js +681 -0
  63. package/Editor/Modules/ParagraphButtons.js +122 -0
  64. package/Editor/Modules/Resizer.js +176 -0
  65. package/Editor/Modules/SelectionRange.js +564 -0
  66. package/Editor/Modules/SourceViewBuilder.js +212 -0
  67. package/Editor/Modules/SymbolPicker.js +208 -0
  68. package/Editor/Modules/Table.js +2327 -0
  69. package/Editor/Modules/TableDialog.js +757 -0
  70. package/Editor/Modules/Toolbar.js +375 -0
  71. package/Editor/Modules/TooltipMenu.js +125 -0
  72. package/Editor/Modules/Utility.js +131 -0
  73. package/Editor/Themes/Default.css +204 -0
  74. package/FileUpload/FileUpload.css +156 -0
  75. package/FileUpload/FileUpload.js +898 -0
  76. package/FileUpload/Themes/Default.css +17 -0
  77. package/Form/Form.css +1934 -0
  78. package/Form/Form.js +4943 -0
  79. package/Form/Modules/ActionManager.js +368 -0
  80. package/Form/Modules/BuildPanelManager.js +319 -0
  81. package/Form/Modules/ComponentFactory.js +468 -0
  82. package/Form/Modules/ComponentPanel.js +265 -0
  83. package/Form/Modules/ConfigPanelManager.js +685 -0
  84. package/Form/Modules/DataObserver.js +717 -0
  85. package/Form/Modules/Databinder.js +602 -0
  86. package/Form/Modules/Draggable.js +756 -0
  87. package/Form/Modules/OptionsPanel.js +342 -0
  88. package/Form/Modules/Renderer.js +1889 -0
  89. package/Form/Modules/RuleEngine.js +585 -0
  90. package/Form/Modules/RulesPanel.js +1158 -0
  91. package/Form/Modules/Types.js +1709 -0
  92. package/Form/Modules/ValidationManager.js +194 -0
  93. package/Form/Themes/Default.css +216 -0
  94. package/FormField/FormField.css +468 -0
  95. package/FormField/FormField.js +576 -0
  96. package/FormField/Themes/Default.css +99 -0
  97. package/Grid/Grid.css +544 -0
  98. package/Grid/Grid.js +3761 -0
  99. package/Grid/Themes/Default.css +43 -0
  100. package/LICENSE +126 -0
  101. package/MaskedTextBox/MaskedTextBox.css +28 -0
  102. package/MaskedTextBox/MaskedTextBox.js +642 -0
  103. package/MaskedTextBox/Themes/Default.css +6 -0
  104. package/Menu/Menu.css +328 -0
  105. package/Menu/Menu.js +2386 -0
  106. package/Menu/Themes/Default.css +24 -0
  107. package/NumericBox/NumericBox.css +120 -0
  108. package/NumericBox/NumericBox.js +984 -0
  109. package/PanelBar/PanelBar.css +123 -0
  110. package/PanelBar/PanelBar.js +505 -0
  111. package/PanelBar/Themes/Default.css +51 -0
  112. package/PieChart/PieChart.css +95 -0
  113. package/PieChart/PieChart.js +820 -0
  114. package/PieChart/Themes/Default.css +9 -0
  115. package/README.md +178 -3
  116. package/Slider/Slider.css +348 -0
  117. package/Slider/Slider.js +1306 -0
  118. package/Slider/Themes/Default.css +44 -0
  119. package/TabStrip/TabStrip.css +52 -0
  120. package/TabStrip/TabStrip.js +362 -0
  121. package/TabStrip/Themes/Default.css +35 -0
  122. package/TimePicker/Themes/Default.css +30 -0
  123. package/TimePicker/TimePicker.css +253 -0
  124. package/TimePicker/TimePicker.js +1485 -0
  125. package/TooltipManager/Themes/Default.css +21 -0
  126. package/TooltipManager/TooltipManager.css +227 -0
  127. package/TooltipManager/TooltipManager.js +791 -0
  128. package/Validator/Validator.css +54 -0
  129. package/Validator/Validator.js +2508 -0
  130. package/componyx.html-data.json +619 -0
  131. package/index.js +27 -0
  132. package/package.json +43 -4
  133. package/types/AppSkin.d.ts +842 -0
  134. package/types/Base.d.ts +1034 -0
  135. package/types/Bindary.d.ts +1196 -0
  136. package/types/BookmarkSpy.d.ts +112 -0
  137. package/types/Box.d.ts +374 -0
  138. package/types/Button.d.ts +455 -0
  139. package/types/Carousel.d.ts +136 -0
  140. package/types/ColorButton.d.ts +120 -0
  141. package/types/ColorPicker.d.ts +225 -0
  142. package/types/ComboBox.d.ts +507 -0
  143. package/types/DataPager.d.ts +152 -0
  144. package/types/DatePicker.d.ts +354 -0
  145. package/types/Dialog.d.ts +160 -0
  146. package/types/Editor.d.ts +1289 -0
  147. package/types/FileUpload.d.ts +206 -0
  148. package/types/Form.d.ts +3078 -0
  149. package/types/FormField.d.ts +204 -0
  150. package/types/Grid.d.ts +911 -0
  151. package/types/Library.d.ts +1899 -0
  152. package/types/MaskedTextBox.d.ts +91 -0
  153. package/types/Menu.d.ts +577 -0
  154. package/types/NumericBox.d.ts +310 -0
  155. package/types/PanelBar.d.ts +223 -0
  156. package/types/PieChart.d.ts +224 -0
  157. package/types/ResponsiveTemplates.d.ts +108 -0
  158. package/types/Slider.d.ts +227 -0
  159. package/types/TabStrip.d.ts +185 -0
  160. package/types/TimePicker.d.ts +265 -0
  161. package/types/TooltipManager.d.ts +178 -0
  162. package/types/UI.d.ts +29 -0
  163. package/types/Validator.d.ts +653 -0
  164. package/types/index.d.ts +59 -0
  165. package/ui.css +11783 -0
  166. package/ui.js +79477 -0
  167. package/ui.min.css +138 -0
  168. package/ui.min.js +264 -0
  169. package/ui.min.js.map +1 -0
@@ -0,0 +1,1889 @@
1
+ /*! Componyx.UI | (c) E.H. Daanen / Componyx | BUSL-1.1 | componyx.com/license */
2
+ componyx.UI.form_modules = componyx.UI.form_modules || {};
3
+ componyx.UI.form_modules.Renderer = class Renderer
4
+ {
5
+ #header;
6
+ #footer;
7
+ #formPane;
8
+ #contentEditor;
9
+ #valueHandlers = {};
10
+ #tabStripCaret;
11
+ constructor(form)
12
+ {
13
+ this.form = form;
14
+ this.cf = form.componentFactory;
15
+ this.InputTypeOption = form.constructor.InputTypeOption;
16
+ this.classOption = form.classOption;
17
+ this.getCssClass = form.getCssClass;
18
+ this.sectionTabStrip = null;
19
+ this.prevButton = null;
20
+ this.nextButton = null;
21
+ this.submitButton = null;
22
+ }
23
+
24
+ get id()
25
+ {
26
+ return this.form.id;
27
+ }
28
+
29
+ get labels()
30
+ {
31
+ return this.form.labels;
32
+ }
33
+
34
+ get fieldSets()
35
+ {
36
+ return this.form.fieldSets;
37
+ }
38
+
39
+ get formPane()
40
+ {
41
+ return (!this.#formPane) ? this.#formPane = this.form.querySelector('.' + this.getCssClass(this.classOption.FORM_PANE)) : this.#formPane;
42
+ }
43
+
44
+ get header()
45
+ {
46
+ return this.#header;
47
+ }
48
+
49
+ get footer()
50
+ {
51
+ if (!this.#footer)
52
+ this.#footer = $lib.element({ container: this.formPane.parentElement, tag: 'footer' });
53
+
54
+ return this.#footer;
55
+ }
56
+
57
+ get saveStatusElement()
58
+ {
59
+ return this.#header.querySelector(`.${this.getCssClass(this.classOption.SAVE_STATUS)}`);
60
+ }
61
+
62
+ destroy()
63
+ {
64
+ this.#contentEditor = null;
65
+ this.clearValueHandlers();
66
+
67
+ this.sectionTabStrip?.destroy();
68
+ this.prevButton?.destroy();
69
+ this.nextButton?.destroy();
70
+ this.submitButton?.destroy();
71
+ }
72
+
73
+ clearValueHandlers()
74
+ {
75
+ if (!this.#valueHandlers) return;
76
+
77
+ Object.keys(this.#valueHandlers).forEach(id =>
78
+ {
79
+ $bindary.unregisterValueHandler(this.#valueHandlers[id]);
80
+ });
81
+
82
+ this.#valueHandlers = {};
83
+ }
84
+
85
+ /**
86
+ * Creates the header.
87
+ */
88
+ createHeader()
89
+ {
90
+ let headerId = 'Header',
91
+ cssClassTitle = this.getCssClass(this.classOption.TITLE),
92
+ cssClassStatus = this.getCssClass(this.classOption.SAVE_STATUS),
93
+ cssClassSpinner = this.getCssClass(this.classOption.SPINNER),
94
+ cssClassSectionSwitch = this.getCssClass(this.classOption.SECTION_SWITCH),
95
+ cssClassCommands = this.getCssClass(this.classOption.COMMANDS),
96
+ values = {
97
+ title: $lib.format('<h1 class="{0}"></h1>', cssClassTitle),
98
+ saveStatus: $lib.format('<div class="{0}"><div class="{1}"></div></div>', cssClassStatus, cssClassSpinner),
99
+ sectionSwitch: (this.form.isBuildMode()) ? $lib.format('<div class="{0}"></div>', cssClassSectionSwitch) : '',
100
+ commands: (this.form.isBuildMode() || this.form.isPreviewMode()) ? $lib.format('<span class="{0}"></span>', cssClassCommands) : '',
101
+ };
102
+
103
+ this.#header = $lib.element(this.form, '', 'header');
104
+
105
+ if (this.form.hasTemplate(headerId))
106
+ this.form.applyTemplate(this.#header, headerId, values);
107
+
108
+ const titleEl = this.#header.querySelector('.' + cssClassTitle),
109
+ sectionSwitchEl = this.#header.querySelector('.' + cssClassSectionSwitch),
110
+ commandsEl = this.#header.querySelector('.' + cssClassCommands),
111
+ statusEl = this.#header.querySelector('.' + cssClassStatus),
112
+ hasSections = this.form.hasSections();
113
+
114
+ if (titleEl)
115
+ titleEl.innerHTML = this.labels.headerTitle;
116
+
117
+ if (!this.form.isBuildMode() && !this.form.isPreviewMode())
118
+ {
119
+ if (statusEl)
120
+ statusEl.style.display = 'none';
121
+
122
+ if ($lib.isEmpty(this.labels.headerTitle))
123
+ this.#header.style.display = 'none';
124
+
125
+ return;
126
+ }
127
+
128
+ if (sectionSwitchEl)
129
+ {
130
+ const formFieldId = `${this.id}_section_switch`,
131
+ input = this.cf.createCheckbox(`${formFieldId}_input`, '', '', hasSections, { onchange: () => { this.form.toggleSectionMode(); } });
132
+
133
+ this.cf.createFormField(sectionSwitchEl, formFieldId, this.form.sectionSwitchFormFieldId, this.labels.sectionSwitch, input, { switch: true });
134
+ }
135
+
136
+ if (commandsEl)
137
+ {
138
+ const headerCommands = this.form.commands.filter(cmd => cmd.location === this.form.constructor.CommandLocationOption.HEADER);
139
+
140
+ $lib.each(headerCommands, (cmd, index) =>
141
+ {
142
+ cmd.id = cmd.id || `${this.id}_command_header_${index}`;
143
+ this.cf.createButton(commandsEl, cmd.id, cmd.buttonId,
144
+ {
145
+ ...cmd,
146
+ shortcutScope: this.form,
147
+ primary: true,
148
+ hasIcon: true,
149
+ disabled: (cmd.isEnabled) ? !cmd.isEnabled() : false,
150
+ showing: (cmd.isVisible) ? cmd.isVisible() : true
151
+ });
152
+ });
153
+ }
154
+
155
+ if (statusEl)
156
+ {
157
+ statusEl.setAttribute('data-busy', this.labels.saveInProgress);
158
+ statusEl.setAttribute('data-success', this.labels.saveSuccess);
159
+ statusEl.setAttribute('data-failed', this.labels.saveFailed);
160
+ statusEl.style.visibility = 'hidden';
161
+ }
162
+
163
+ if (this.form.isBuildMode() && hasSections)
164
+ this.renderSectionTabStrip();
165
+ }
166
+
167
+ renderSectionTabStrip(focusTabInput = false)
168
+ {
169
+ const sectionsEl = this.#header.parentNode.querySelector(`:scope > .${this.getCssClass(this.classOption.SECTIONS)}`)
170
+ || $lib.element({
171
+ tag: 'nav',
172
+ container: this.#header.parentNode,
173
+ before: this.#header.nextElementSibling,
174
+ attrs: { class: this.getCssClass(this.classOption.SECTIONS) }
175
+ });
176
+
177
+ if (this.sectionTabStrip)
178
+ this.sectionTabStrip.destroy();
179
+
180
+ sectionsEl.innerHTML = '';
181
+
182
+ if (!this.form.hasSections())
183
+ {
184
+ sectionsEl.remove();
185
+ return;
186
+ }
187
+
188
+ const addSectionId = `${this.form.id}_section_add`,
189
+ showRemoveBtn = this.form.fieldSets.length > 1,
190
+ tabItems = this.form.fieldSets.map((section) =>
191
+ {
192
+ const labelInput = this.#createLabelInput(null, section.label, `${section.id}_input`),
193
+ removeBtn = (showRemoveBtn) ? $lib.element({ tag: 'i', attrs: { class: this.getCssClass(this.classOption.SECTION_REMOVE) } }) : null,
194
+ tabContent = (showRemoveBtn) ? [labelInput, removeBtn] : labelInput,
195
+ tabItem = {
196
+ id: section.id,
197
+ content: tabContent,
198
+ selected: section.selected
199
+ };
200
+
201
+ this.form.dataBinder.bindProperty(section, labelInput);
202
+
203
+ if (showRemoveBtn)
204
+ {
205
+ $lib.on(removeBtn, 'click', (e) =>
206
+ {
207
+ e.stopPropagation();
208
+ this.form.removeSection(section);
209
+ });
210
+ }
211
+
212
+ return tabItem;
213
+ }),
214
+ setFocus = () => setTimeout(() =>
215
+ {
216
+ const selectedItem = tabItems.find(item => item.selected),
217
+ el = selectedItem && $lib(`#${selectedItem.id}_input`);
218
+
219
+ if (el)
220
+ {
221
+ el.focus();
222
+
223
+ if (this.#tabStripCaret && this.#tabStripCaret.id === selectedItem.id)
224
+ el.setSelectionRange(this.#tabStripCaret.start, this.#tabStripCaret.end);
225
+ }
226
+
227
+ this.#tabStripCaret = null;
228
+ }, 0);
229
+
230
+ tabItems.push({
231
+ id: addSectionId,
232
+ text: `${this.labels.addItemPrefix}${this.labels.section}`,
233
+ hasIcon: true,
234
+ cssClass: this.getCssClass(this.classOption.SECTION_ADD),
235
+ cssClassIcon: 'ico-plus',
236
+ selected: false
237
+ });
238
+
239
+ this.sectionTabStrip = this.cf.createComponent(
240
+ sectionsEl,
241
+ componyx.UI.TabStrip,
242
+ `${this.form.id}_section_tabstrip`,
243
+ this.form.sectionTabStripId,
244
+ {
245
+ itemList: tabItems,
246
+ alignment: componyx.UI.TabStrip.AlignmentOption.TOP,
247
+ events: {
248
+ onPostRender: (!focusTabInput) ? null : () =>
249
+ {
250
+ setFocus();
251
+ },
252
+ onItemSelect: (tabStrip, args) =>
253
+ {
254
+ if (args.item.id === addSectionId)
255
+ this.form.addSection();
256
+ else
257
+ {
258
+ const activeEl = document.activeElement,
259
+ input = $lib(`#${args.item.id}_input`);
260
+
261
+ if (activeEl == input)
262
+ {
263
+ this.#tabStripCaret = {
264
+ id: args.item.id,
265
+ start: activeEl.selectionStart,
266
+ end: activeEl.selectionEnd
267
+ };
268
+ }
269
+
270
+ this.form.selectSection(this.form.getSection(args.item.id), true);
271
+ }
272
+ }
273
+ }
274
+ }
275
+ );
276
+ }
277
+
278
+ createStepIndicator(container)
279
+ {
280
+ const stepIndicatorEl = $lib.element({ container, attrs: { class: this.getCssClass(this.classOption.STEP_INDICATOR) } });
281
+
282
+ this.form.fieldSets.forEach((s) =>
283
+ {
284
+ const step = $lib.element({ container: stepIndicatorEl, attrs: { id: `${this.form.id}_${s.id}_step`, class: this.getCssClass(this.classOption.STEP) } });
285
+ $lib.element({ container: step, tag: 'span', content: s.label });
286
+ });
287
+ }
288
+
289
+ ensurePrevButton()
290
+ {
291
+ if (!this.prevButton)
292
+ {
293
+ this.prevButton = this.cf.createButton(this.footer, `${this.form.id}_section_prev`, this.form.previousSectionButtonId,
294
+ {
295
+ transparent: false,
296
+ hasIcon: true,
297
+ cssClassIcon: 'ico-arrow-l',
298
+ cssClass: this.getCssClass(this.classOption.SECTION_PREVIOUS),
299
+ command: this.form.previousSection.bind(this.form),
300
+ text: this.labels.previous
301
+ }
302
+ );
303
+ }
304
+ return this.prevButton;
305
+ }
306
+
307
+ ensureNextButton()
308
+ {
309
+ if (!this.nextButton)
310
+ {
311
+ this.nextButton = this.cf.createButton(this.footer, `${this.form.id}_section_next`, this.form.nextSectionButtonId,
312
+ {
313
+ transparent: false,
314
+ hasIcon: true,
315
+ cssClassIcon: 'ico-arrow-r',
316
+ iconAlign: componyx.UI.Button.AlignOption.RIGHT,
317
+ cssClass: this.getCssClass(this.classOption.SECTION_NEXT),
318
+ command: this.form.nextSection.bind(this.form),
319
+ text: this.labels.next,
320
+ disabled: true
321
+ }
322
+ );
323
+ }
324
+ return this.nextButton;
325
+ }
326
+
327
+ ensureSubmitButton()
328
+ {
329
+ if (!this.submitButton)
330
+ {
331
+ this.submitButton = this.cf.createButton(this.footer, `${this.form.id}_submit`, this.submitButtonId,
332
+ {
333
+ transparent: false,
334
+ primary: true,
335
+ cssClass: this.getCssClass(this.classOption.SUBMIT),
336
+ command: this.form.submit.bind(this.form),
337
+ text: this.labels.submit,
338
+ disabled: true
339
+ }
340
+ );
341
+ }
342
+ return this.submitButton;
343
+ }
344
+
345
+ /**
346
+ * Destroys the section
347
+ * @param {componyx.UI.Form.Section} section The section to destroy.
348
+ */
349
+ destroySection(section)
350
+ {
351
+ if (section.fieldSets)
352
+ {
353
+ section.fieldSets.forEach((fieldSet) =>
354
+ {
355
+ this.destroyFieldSet(fieldSet);
356
+ });
357
+ }
358
+
359
+ if (section.element)
360
+ section.element.remove();
361
+
362
+ section.element = null;
363
+ this.renderSectionTabStrip();
364
+
365
+ this.form.buildPanelManager.scheduleNavigatorUpdate();
366
+ }
367
+
368
+ /**
369
+ * Renders the section.
370
+ * @param {componyx.UI.Form.Section} section The section to render.
371
+ */
372
+ renderSection(section)
373
+ {
374
+ const index = this.fieldSets.findIndex(fs => fs.id === section.id),
375
+ nextConnectedEl = this.getNextConnectedElement(this.fieldSets, index);
376
+
377
+ this.createSection(section);
378
+ this.formPane.insertBefore(section.element, nextConnectedEl);
379
+ section.fieldSets.forEach(fs => this.renderFieldSet(fs));
380
+
381
+ this.form.buildPanelManager.scheduleNavigatorUpdate();
382
+ }
383
+
384
+ /**
385
+ * Updates simple settings that don't required a re-render.
386
+ * @param {componyx.UI.Form.Section} section The section to update.
387
+ */
388
+ updateSection(section)
389
+ {
390
+ const input = $lib(`#${section.id}_input`);
391
+ input.value = section.label;
392
+
393
+ this.form.buildPanelManager.scheduleNavigatorItemUpdate(section);
394
+ }
395
+
396
+ /**
397
+ * Destroys the field-set.
398
+ * @param {componyx.UI.Form.FieldSet} fieldSet The field-set to destroy.
399
+ */
400
+ destroyFieldSet(fieldSet)
401
+ {
402
+ if (!fieldSet.element)
403
+ return;
404
+
405
+ if (fieldSet.fieldSets)
406
+ {
407
+ fieldSet.fieldSets.forEach((fieldSet) =>
408
+ {
409
+ this.destroyFieldSet(fieldSet);
410
+ });
411
+ }
412
+
413
+ if (!$lib.isEmpty(fieldSet.fields))
414
+ {
415
+ fieldSet.fields.forEach((field) =>
416
+ {
417
+ this.destroyField(field);
418
+ });
419
+ }
420
+
421
+ if (fieldSet.element)
422
+ fieldSet.element.remove();
423
+
424
+ fieldSet.element = null;
425
+ this.form.removeLineBreak(fieldSet, false);
426
+
427
+ this.form.rebindFieldSetLabels();
428
+ this.form.rebindFieldElements();
429
+
430
+ if (!this.form.isBuildMode() && fieldSet.parent && fieldSet.parent.type === 'FieldSet' && fieldSet.repeatable)
431
+ this.#ensureRepeatContainer(fieldSet);
432
+
433
+ if (this.form.autoDataBindFields && !this.form.isBuildMode())
434
+ this.form.dataBinder.scheduleBindFieldsToModel(fieldSet.parent);
435
+
436
+ this.form.buildPanelManager.scheduleNavigatorUpdate();
437
+ }
438
+
439
+ /**
440
+ * Renders the field-set.
441
+ * @param {componyx.UI.Form.FieldSet} fieldSet The field-set to render.
442
+ */
443
+ renderFieldSet(fieldSet)
444
+ {
445
+ this.form.ensureItemId(fieldSet);
446
+
447
+ const parentFieldSet = fieldSet.parent,
448
+ isSelected = (this.form.selectedFieldSetId === fieldSet.id || fieldSet.selected),
449
+ element = parentFieldSet?.element || this.formPane,
450
+ siblings = parentFieldSet?.fieldSets || this.fieldSets,
451
+ index = siblings.findIndex(fs => fs.id === fieldSet.id),
452
+ nextConnectedEl = this.getNextConnectedElement(siblings, index);
453
+
454
+ if (isSelected)
455
+ this.form.deselectFieldSet();
456
+
457
+ this.createFieldSet(fieldSet);
458
+ element.insertBefore(fieldSet.element, nextConnectedEl);
459
+
460
+ if (fieldSet.lineBreakElement)
461
+ fieldSet.element.parentElement.insertBefore(fieldSet.lineBreakElement, fieldSet.element);
462
+
463
+ if (fieldSet.fieldSets)
464
+ {
465
+ fieldSet.fieldSets.forEach(subFieldSet => this.renderFieldSet(subFieldSet));
466
+ }
467
+
468
+ if (fieldSet.fields)
469
+ {
470
+ fieldSet.fields.forEach(field => this.renderField(field));
471
+ }
472
+
473
+ this.form.rebindFieldSetLabels();
474
+ this.form.rebindFieldElements();
475
+
476
+ if (isSelected)
477
+ this.form.selectFieldSet(fieldSet);
478
+
479
+ this.form.buildPanelManager.scheduleNavigatorUpdate();
480
+ }
481
+
482
+ /**
483
+ * Updates simple field set settings that don't required a re-render.
484
+ * @param {componyx.UI.Form.FieldSet} fieldSet The field-set to update.
485
+ * @param {Boolean} fireEvent A value indicating to fire the update event.
486
+ */
487
+ updateFieldSet(fieldSet, fireEvent = true)
488
+ {
489
+ this.#updateLabel(fieldSet);
490
+ this.#updateFieldSetLayout(fieldSet);
491
+ this.#updateDisabledState(fieldSet);
492
+ this.#updateVisibleState(fieldSet);
493
+ this.#updateLineBreak(fieldSet);
494
+ this.#updateFieldSetTooltip(fieldSet);
495
+ this.#updateRepeatButton(fieldSet);
496
+
497
+ if (!this.form.isBuildMode() && fieldSet.parent && fieldSet.parent.type === 'FieldSet' && fieldSet.repeatable)
498
+ this.#ensureRepeatContainer(fieldSet);
499
+
500
+ if (fireEvent)
501
+ this.form.events.onUpdateFieldSet.fire(this.form, { fieldSet: fieldSet });
502
+
503
+ this.form.buildPanelManager.scheduleNavigatorItemUpdate(fieldSet);
504
+
505
+ if (this.form.renderState == $base.static.RenderState.RENDERED && (this.form.isEditMode() || this.form.isPreviewMode()))
506
+ this.form.validationManager.validator.validate(this.form, true);
507
+ }
508
+
509
+ /**
510
+ * Renders the field.
511
+ * @param {componyx.UI.Form.Field} field The field to render.
512
+ */
513
+ renderField(field)
514
+ {
515
+ this.form.ensureItemId(field);
516
+ this.destroyField(field);
517
+
518
+ const context = this.form.getFieldContext(field),
519
+ isSelected = (this.form.selectedFieldId === field.id || field.selected);
520
+
521
+ if (isSelected)
522
+ {
523
+ this.form.deselectFieldSet();
524
+ this.form.deselectField();
525
+ }
526
+
527
+ if (!context)
528
+ {
529
+ if (isSelected)
530
+ this.selectFirst();
531
+
532
+ return;
533
+ }
534
+
535
+ let fieldPane;
536
+
537
+ if (context.fieldSet.element?.isConnected)
538
+ {
539
+ fieldPane = $lib(this.getCssClass(this.classOption.FIELD_PANE), context.fieldSet.element, '', true);
540
+ this.createField(field, fieldPane);
541
+
542
+ if (!context.isLastField)
543
+ {
544
+ const siblings = context.fieldSet.fields,
545
+ index = siblings.findIndex(f => f.id === field.id),
546
+ nextConnectedEl = this.getNextConnectedElement(siblings, index);
547
+
548
+ fieldPane.insertBefore(field.element, nextConnectedEl || null);
549
+ }
550
+ else
551
+ {
552
+ fieldPane.appendChild(field.element);
553
+ }
554
+
555
+ if (field.lineBreakElement)
556
+ fieldPane.insertBefore(field.lineBreakElement, field.element);
557
+ }
558
+ else // new field set
559
+ {
560
+ fieldPane = this.createFieldSet(context.fieldSet);
561
+ this.createField(field, fieldPane);
562
+ }
563
+
564
+ if (this.form.autoDataBindFields && !this.form.isBuildMode())
565
+ this.form.dataBinder.scheduleBindFieldsToModel([field.parent]);
566
+
567
+ this.form.rebindFieldElements(field.parent);
568
+
569
+ if (isSelected)
570
+ this.form.selectField(field);
571
+
572
+ this.form.buildPanelManager.scheduleNavigatorUpdate();
573
+ }
574
+
575
+ /**
576
+ * Updates simple field settings that don't required a re-render.
577
+ * @param {componyx.UI.Form.FieldSet} field The field to update.
578
+ * @param {Boolean} fireEvent A value indicating to fire the update event.
579
+ */
580
+ updateField(field, fireEvent = true)
581
+ {
582
+ const formField = $UI.store[field.element.id],
583
+ customConfig = this.form.constructor.getCustomType(field.customType),
584
+ labelDisplay = componyx.UI.FormField.LabelDisplayOption;
585
+
586
+ if (!this.form.isViewMode())
587
+ {
588
+ formField.setRequired(field.required || false);
589
+ this.form.validationManager.updateRequiredRule(field);
590
+ }
591
+
592
+ this.#ensureLineBreaks(field.parent);
593
+ this.#updateLabel(field);
594
+ this.#updateLineBreak(field);
595
+ this.#updateDisabledState(field);
596
+ this.#updateVisibleState(field);
597
+ this.#updateWidth(field);
598
+
599
+ if (!this.form.isViewMode())
600
+ this.#updateFieldTooltip(field);
601
+
602
+ if (this.form.isBuildMode())
603
+ {
604
+ formField.setLabelDisplay((field.labelDisplay == labelDisplay.FLOATING || field.labelDisplay == labelDisplay.INSIDE) ? labelDisplay.ABOVE : field.labelDisplay || 0);
605
+ this.#updateFieldIndicators(field);
606
+
607
+ if (field.type === 'ContentField')
608
+ {
609
+ this.#updateContentField(field);
610
+ }
611
+ else if (field.type === 'SpacerField')
612
+ {
613
+ this.#updateSpacerFieldSettings(field);
614
+ }
615
+ }
616
+ else
617
+ {
618
+ formField.setLabelDisplay(field.labelDisplay || 0);
619
+ formField.linkLabelToInput();
620
+ }
621
+
622
+ if (field.type === 'Field' && !this.form.isViewMode())
623
+ {
624
+ if (customConfig)
625
+ {
626
+ if (customConfig.updater)
627
+ customConfig.updater(this.form, field);
628
+ }
629
+ else if (field.inputType === this.InputTypeOption.TEXTBOX || field.inputType === this.InputTypeOption.TEXTAREA)
630
+ {
631
+ const value = this.#getFieldValue(field),
632
+ input = field.editElement.querySelector('input, textarea');
633
+
634
+ this.#setInputEditAttributes({ input, name: field.name, value: value, placeholder: field.placeholder, disabled: field.disabled, readOnly: field.readOnly });
635
+ }
636
+ else if (field.inputType >= this.InputTypeOption.MASKEDTEXTBOX) // component
637
+ {
638
+ if (this.form.isBuildMode())
639
+ this.#updateComponentPlaceholder(field);
640
+
641
+ if (!this.form.isBuildMode() || field.inputType != this.InputTypeOption.COMBOBOX) // do not update component value for combo-box when in build-mode (we show only option count)
642
+ {
643
+ if (this.form.renderState !== $base.static.RenderState.RENDERED)
644
+ this.form.classList.remove('hidden'); // temporarily make form visible for value calculation based on width (e.g. slider handle position)
645
+
646
+ this.#updateComponentValue(field);
647
+
648
+ if (this.form.renderState !== $base.static.RenderState.RENDERED)
649
+ this.form.classList.add('hidden');
650
+ }
651
+ }
652
+ }
653
+
654
+ if (this.form.renderState == $base.static.RenderState.RENDERED && (this.form.isEditMode() || this.form.isPreviewMode()))
655
+ this.form.validationManager.validator.validate(this.form, true);
656
+
657
+ if (fireEvent)
658
+ this.form.events.onUpdateField.fire(this.form, { field: field });
659
+
660
+ this.form.buildPanelManager.scheduleNavigatorItemUpdate(field);
661
+ }
662
+
663
+ /**
664
+ * Updates the field with new options for build mode view.
665
+ * @param {componyx.UI.Form.FieldSet} field The field to update.
666
+ */
667
+ updateFieldOptions(field)
668
+ {
669
+ if (!field.editElement || !this.form.isBuildMode())
670
+ return;
671
+
672
+ if (field.inputType == this.InputTypeOption.COMBOBOX)
673
+ {
674
+ this.#updateComponentPlaceholder(field);
675
+ }
676
+ else
677
+ {
678
+ field.editElement.innerHTML = '';
679
+ this.#createFieldOptions(field, field.editElement);
680
+ this.#addFeedbackElement(field);
681
+ }
682
+ }
683
+
684
+ /**
685
+ * Destroys the field.
686
+ * @param {componyx.UI.Form.Field} field The field to destroy.
687
+ */
688
+ destroyField(field)
689
+ {
690
+ const fieldId = `${this.id}_${field.id}`;
691
+
692
+ if (!field.element || !$UI.store[fieldId])
693
+ return;
694
+
695
+ if (field.type === 'ContentField' && this.#contentEditor)
696
+ {
697
+ const contentId = this.form.getId(field, 'content'),
698
+ index = this.#contentEditor.editableElements.findIndex(item => item.id === contentId);
699
+
700
+ if (index !== -1)
701
+ {
702
+ this.#contentEditor.editableElements.splice(index, 1);
703
+ if (this.#valueHandlers[contentId])
704
+ {
705
+ $bindary.unregisterValueHandler(this.#valueHandlers[contentId]);
706
+ delete this.#valueHandlers[contentId];
707
+ }
708
+ }
709
+
710
+ }
711
+
712
+ $lib.each(field.options, function (option, index)
713
+ {
714
+ if ($UI.store[`${fieldId}_${index}`])
715
+ $UI.store[`${fieldId}_${index}`].destroy();
716
+
717
+ option.element = null;
718
+ });
719
+
720
+ if (field.inputType > this.InputTypeOption.SWITCH)
721
+ $UI.store[fieldId + '_input'].destroy();
722
+
723
+ $UI.store[fieldId].destroy();
724
+ field.element = field.editElement = field.viewElement = null;
725
+ this.form.removeLineBreak(field, false);
726
+ this.form.rebindFieldElements(field.parent);
727
+
728
+ this.form.buildPanelManager.scheduleNavigatorItemUpdate(field, true);
729
+ }
730
+
731
+ /**
732
+ * Returns the next sibling element in the array that is connected to the DOM. Useful for determining where to insert a new element in a reliable position.
733
+ * @param {Array} siblings - Array of sibling fieldSets or fields.
734
+ * @param {number} startIndex - The index to start searching from.
735
+ * @returns {HTMLElement|null} The next connected element or null if none found.
736
+ */
737
+ getNextConnectedElement(siblings, startIndex)
738
+ {
739
+ for (let index = startIndex + 1; index < siblings.length; index++)
740
+ {
741
+ const candidate = siblings[index];
742
+ const el = candidate.lineBreakElement || candidate.element;
743
+ if (el?.isConnected)
744
+ return el;
745
+ }
746
+ return null;
747
+ }
748
+
749
+ createSection(section)
750
+ {
751
+ if ($lib.isEmpty(section.id))
752
+ this.form.ensureItemId(section);
753
+
754
+ let element = section.element,
755
+ id = section.id,
756
+ headerTemplateId = `SectionHeader_${id}`,
757
+ footerTemplateId = `SectionFooter_${id}`;
758
+
759
+ if (element)
760
+ {
761
+ element.innerHTML = '';
762
+ }
763
+ else
764
+ {
765
+ element = $lib.element(this.formPane, '', 'section');
766
+ element.id = `${this.id}_${section.id}`;
767
+ }
768
+
769
+ section.element = element;
770
+ this.#updateClassAndStyle(section);
771
+
772
+ if (!this.form.hasTemplate(headerTemplateId) && section.headerTemplate)
773
+ this.form.addTemplate(headerTemplateId, section.headerTemplate);
774
+
775
+ if (!this.form.hasTemplate(footerTemplateId) && section.footerTemplate)
776
+ this.form.addTemplate(footerTemplateId, section.footerTemplate);
777
+
778
+ if (this.form.hasTemplate(headerTemplateId))
779
+ this.form.applyTemplate($lib.element(element, '', 'header'), headerTemplateId, section);
780
+
781
+ if (this.form.hasTemplate(footerTemplateId))
782
+ this.form.applyTemplate($lib.element(element, '', 'footer'), footerTemplateId, section);
783
+
784
+ return section.element;
785
+ }
786
+
787
+
788
+ createFieldSet(fieldSet)
789
+ {
790
+ this.form.ensureItemId(fieldSet);
791
+
792
+ let id = fieldSet.id,
793
+ element = fieldSet.element,
794
+ fieldPane,
795
+ headerTemplateId = `Header_${id}`,
796
+ footerTemplateId = `Footer_${id}`,
797
+ parentEl = fieldSet.parent?.element || this.formPane,
798
+ footerEl = parentEl.querySelector('footer'),
799
+ siblings = fieldSet.parent?.fieldSets || this.fieldSets,
800
+ index = siblings.findIndex(fs => fs.id === fieldSet.id),
801
+ hasTooltip = !$lib.isEmpty(fieldSet.tooltip);
802
+
803
+ if (!element || !element.isConnected) // if element does not exist or is no longer connected, we recreate it
804
+ {
805
+ element = $lib.element({ tag: 'fieldset', props: { id: `${this.id}_${id}` } });
806
+ const nextConnectedEl = this.getNextConnectedElement(siblings, index);
807
+
808
+ parentEl.insertBefore(element, nextConnectedEl || footerEl);
809
+ fieldSet.element = element;
810
+ fieldPane = $lib.element({ attrs: { "class": this.getCssClass(this.classOption.FIELD_PANE) } });
811
+ }
812
+ else
813
+ {
814
+ fieldPane = $lib(this.getCssClass(this.classOption.FIELD_PANE), element, '', true);
815
+ element.innerHTML = '';
816
+ element.className = '';
817
+ }
818
+
819
+ fieldPane.setAttribute('data-placeholder', this.labels.fieldPaneEmptyHint);
820
+
821
+ const legend = $lib.element(element, '', 'legend', (!this.form.isBuildMode()) ? [fieldSet.label] : '');
822
+
823
+ if (this.form.isBuildMode())
824
+ {
825
+ this.#createLabelInput(legend, fieldSet.label);
826
+ $lib.on(legend, 'click', () => { legend.querySelector('input').focus(); });
827
+ }
828
+
829
+ if (this.form.isBuildMode() || hasTooltip)
830
+ $lib.element({ container: legend, tag: 'i', attrs: { "class": this.getCssClass(this.classOption.ICON) + ' ' + this.getCssClass(this.classOption.TOOLTIP_ICON) } });
831
+
832
+ if (!this.form.isBuildMode() && !hasTooltip && $lib.isEmpty(fieldSet.label))
833
+ legend.style.display = 'none';
834
+
835
+ if (!this.form.hasTemplate(headerTemplateId) && fieldSet.headerTemplate)
836
+ this.form.addTemplate(headerTemplateId, fieldSet.headerTemplate);
837
+
838
+ if (!this.form.hasTemplate(footerTemplateId) && fieldSet.footerTemplate)
839
+ this.form.addTemplate(footerTemplateId, fieldSet.footerTemplate);
840
+
841
+ if (this.form.hasTemplate(headerTemplateId))
842
+ this.form.applyTemplate($lib.element(element, '', 'header'), headerTemplateId, fieldSet);
843
+
844
+ element.appendChild(fieldPane); // field-pane comes before footer
845
+
846
+ if (this.form.hasTemplate(footerTemplateId))
847
+ {
848
+ $lib.element(element, '', 'p', '', { "class": this.getCssClass(this.classOption.FILLER) });
849
+ this.form.applyTemplate($lib.element(element, '', 'footer'), footerTemplateId, fieldSet);
850
+ }
851
+
852
+ if (this.form.isBuildMode())
853
+ {
854
+ this.#createBuildView(fieldSet);
855
+ }
856
+
857
+ this.updateFieldSet(fieldSet, false);
858
+ this.form.events.onRenderFieldSet.fire(this, { fieldSet: fieldSet });
859
+
860
+ return fieldPane;
861
+ }
862
+
863
+ createField(field, container)
864
+ {
865
+ this.form.ensureItemId(field);
866
+
867
+ let showLabel = !this.form.isBuildMode() && field.hideLabel !== true && field.type !== 'ContentField' && field.type !== 'SpacerField',
868
+ labelEl = (showLabel) ? $lib.element({ tag: 'span', content: field.label }) : '',
869
+ formField = this.cf.createFormField(container, this.id + '_' + field.id, field.formFieldId, (showLabel) ? [labelEl] : '', this.#createFieldTemplate(field),
870
+ {
871
+ inline: false,
872
+ borderless: false,
873
+ cssClass: field.cssClass,
874
+ style: field.style,
875
+ required: (!this.form.isViewMode()) ? field.required : false,
876
+ labelDisplay: (!$lib.isEmpty(field.labelDisplay)) ? field.labelDisplay : (!$lib.isEmpty(this.form.labelDisplay)) ? this.form.labelDisplay : undefined
877
+ }, this.#formFieldPostRender.bind(this, field));
878
+
879
+ field.element = formField.element;
880
+ }
881
+
882
+ updateLabel(item)
883
+ {
884
+ this.#updateLabel(item);
885
+ }
886
+
887
+ #createLabelInput(element, label, id)
888
+ {
889
+ return $lib.element({
890
+ container: element, tag: 'input',
891
+ attrs: { "data-ui-form-setting": '' },
892
+ props: { id: id, type: 'text', name: 'label', value: label, placeholder: this.labels.labelPlaceholder }
893
+ });
894
+ }
895
+
896
+ #updateFieldSetLayout(fieldSet)
897
+ {
898
+ const el = fieldSet.element,
899
+ cl = el.classList,
900
+ layout = ($lib.isEmpty(fieldSet.layout)) ? 0 : parseFloat(fieldSet.layout),
901
+ fieldSetLayoutOption = this.form.constructor.FieldSetLayoutOption;
902
+
903
+ cl.remove(this.getCssClass(this.classOption.LAYOUTLESS), this.getCssClass(this.classOption.BANNERED));
904
+
905
+ if (layout === fieldSetLayoutOption.NONE)
906
+ el.classList.add(this.getCssClass(this.classOption.LAYOUTLESS));
907
+ else if (layout === fieldSetLayoutOption.BANNERED)
908
+ el.classList.add(this.getCssClass(this.classOption.BANNERED));
909
+ }
910
+
911
+ #updateClassAndStyle(item)
912
+ {
913
+ const el = item.element;
914
+
915
+ if (item.cssClass)
916
+ el.className = item.cssClass;
917
+ else
918
+ el.removeAttribute('class');
919
+
920
+ if (item.style)
921
+ el.style = item.style;
922
+ else
923
+ el.removeAttribute('style');
924
+ }
925
+
926
+ #createFieldTemplate(field)
927
+ {
928
+ let content = $lib.element();
929
+
930
+ if (this.form.isViewMode())
931
+ {
932
+ let viewEl = $lib.element(content);
933
+
934
+ viewEl.className = this.getCssClass(this.classOption.VIEW_FIELD);
935
+ field.viewElement = viewEl;
936
+ }
937
+ else
938
+ {
939
+ let editEl = $lib.element(content);
940
+
941
+ editEl.className = this.getCssClass(this.classOption.EDIT_FIELD);
942
+ field.editElement = editEl;
943
+ }
944
+
945
+ if (this.form.isBuildMode() && field.type !== 'ContentField')
946
+ content.inert = true;
947
+
948
+ return content;
949
+ }
950
+
951
+ #createViewTemplate(field, element, callback)
952
+ {
953
+ let templateId = field.viewTemplateId || 'View_' + field.id,
954
+ fireCallback = true;
955
+
956
+ if (!this.form.hasTemplate(templateId) && field.viewTemplate)
957
+ this.form.addTemplate(templateId, field.viewTemplate);
958
+
959
+ if (this.form.hasTemplate(templateId))
960
+ {
961
+ this.form.applyTemplate(element, templateId, field);
962
+ }
963
+ else if (field.type === 'ContentField')
964
+ {
965
+ this.#renderContentField(field);
966
+ }
967
+ else if (field.type === 'SpacerField')
968
+ {
969
+ this.#renderSpacerField(field);
970
+ }
971
+ else if (field.customType)
972
+ {
973
+ const customConfig = this.form.constructor.getCustomType(field.customType);
974
+
975
+ if (customConfig && customConfig.renderer)
976
+ {
977
+ customConfig.renderer(this.form, field, element, true, callback);
978
+ fireCallback = false;
979
+ }
980
+ else
981
+ {
982
+ let value = (customConfig.getter) ? customConfig.getter(field) : field.value;
983
+ let view = $lib.element(element, '', 'span');
984
+ view.textContent = value;
985
+ view.classList.add(this.getCssClass(this.classOption.READONLY));
986
+ }
987
+ }
988
+ else
989
+ {
990
+ let tag = (field.viewAsLabel) ? 'span' : 'input',
991
+ view;
992
+
993
+ if (field.inputType === this.InputTypeOption.TEXTAREA || field.inputType === this.InputTypeOption.EDITOR)
994
+ tag = 'textarea';
995
+
996
+ if (field.viewElementId)
997
+ {
998
+ view = element.appendChild(this.form.getSourceElement(field.viewElementId));
999
+ tag = view.nodeName.toLowerCase();
1000
+ }
1001
+ else
1002
+ {
1003
+ view = $lib.element(element, '', tag);
1004
+
1005
+ if (!field.viewAsLabel && tag == 'input')
1006
+ view.type = "text";
1007
+ }
1008
+
1009
+ view.textContent = field.value;
1010
+ view.classList.add(this.getCssClass(this.classOption.READONLY));
1011
+
1012
+ if (tag == 'input' || tag == 'textarea')
1013
+ {
1014
+ view.readOnly = true;
1015
+ view.name = field.name || '';
1016
+ view.placeholder = field.placeholder || '';
1017
+ }
1018
+
1019
+ if (!$lib.isEmpty(field.width))
1020
+ view.width = $lib.unit(field.width);
1021
+ }
1022
+
1023
+ this.form.events.onRenderViewTemplate.fire(this.form, { field: field });
1024
+
1025
+ if (fireCallback)
1026
+ callback();
1027
+ }
1028
+
1029
+ #createEditTemplate(field, element, callback)
1030
+ {
1031
+ let templateId = field.editTemplateId || 'Edit_' + field.id,
1032
+ fireCallback = true;
1033
+
1034
+ if (!this.form.hasTemplate(templateId) && field.editTemplate)
1035
+ this.form.addTemplate(templateId, field.editTemplate);
1036
+
1037
+ if (this.form.hasTemplate(templateId))
1038
+ this.form.applyTemplate(element, templateId, field);
1039
+ else if (field.type === 'ContentField')
1040
+ {
1041
+ if (this.form.isBuildMode())
1042
+ {
1043
+ this.#renderContentBuildField(field, callback);
1044
+ fireCallback = false;
1045
+ }
1046
+ else
1047
+ this.#renderContentField(field);
1048
+ }
1049
+ else if (field.type === 'SpacerField')
1050
+ {
1051
+ this.#renderSpacerField(field);
1052
+ }
1053
+ else
1054
+ {
1055
+ const customConfig = this.form.constructor.getCustomType(field.customType);
1056
+
1057
+ if (customConfig)
1058
+ {
1059
+ if (customConfig.renderer)
1060
+ field.renderedEditComponent = customConfig.renderer(this.form, field, element, false, callback);
1061
+
1062
+ fireCallback = false;
1063
+ }
1064
+ else if (field.inputType <= this.InputTypeOption.TEXTAREA) // textbox/textarea
1065
+ field.renderedEditComponent = this.#createTextBox(field, element);
1066
+ else if (field.inputType <= this.InputTypeOption.SWITCH) // radio/checkbox
1067
+ {
1068
+ fireCallback = false;
1069
+ this.#createFieldOptions(field, element, callback);
1070
+ }
1071
+ else
1072
+ {
1073
+ let config = this.cf.getComponentConfig(field, !this.form.isBuildMode());
1074
+ config.events.onPostRender = callback;
1075
+ fireCallback = false;
1076
+ field.renderedEditComponent = this.cf.createComponent(element, this.cf.getComponentType(field.inputType), this.form.getId(field, 'input'), field.inputId, config);
1077
+ }
1078
+
1079
+ this.#addFeedbackElement(field);
1080
+ }
1081
+
1082
+ this.form.events.onRenderEditTemplate.fire(this.form, { field: field });
1083
+
1084
+ if (fireCallback)
1085
+ callback();
1086
+ }
1087
+
1088
+ #addFeedbackElement(field)
1089
+ {
1090
+ if (!this.form.isBuildMode())
1091
+ {
1092
+ const feedbackEl = $lib.element({ container: field.editElement, attrs: { class: this.getCssClass(this.classOption.FEEDBACK) } });
1093
+
1094
+ $lib.element({ container: feedbackEl, tag: 'span', attrs: { class: this.getCssClass(this.classOption.RULE_FEEDBACK) } });
1095
+ $lib.element({ container: feedbackEl, attrs: { class: this.getCssClass(this.classOption.VALIDATION_FEEDBACK) } });
1096
+ }
1097
+ }
1098
+
1099
+ #renderContentBuildField(field, callback)
1100
+ {
1101
+ const contentFieldEl = this.#createContentFieldElement(field);
1102
+ this.form.ownerDocument || document;
1103
+ const wrappedCallback = (editor) =>
1104
+ {
1105
+ const matcher = (item) => contentFieldEl.id === item.element.id,
1106
+ getValue = (element) =>
1107
+ {
1108
+ return this.#contentEditor.getValue(element);
1109
+ };
1110
+
1111
+ if (this.#valueHandlers[contentFieldEl.id])
1112
+ $bindary.unregisterValueHandler(this.#valueHandlers[contentFieldEl.id]);
1113
+
1114
+ this.#valueHandlers[contentFieldEl.id] = $bindary.registerValueHandler(matcher, getValue);
1115
+ this.form.dataBinder.updateView(true);
1116
+
1117
+ if (callback)
1118
+ callback();
1119
+ };
1120
+
1121
+ contentFieldEl.id = this.form.getId(field, 'content');
1122
+ contentFieldEl.dataset.name = 'value';
1123
+ this.form.dataBinder.bindProperty(field, contentFieldEl); // bind to field value
1124
+
1125
+ if (!this.#contentEditor)
1126
+ {
1127
+ const config = {
1128
+ ...field.componentSettings,
1129
+ editableElements: [{ element: contentFieldEl }],
1130
+ events: { onPostRender: wrappedCallback }
1131
+ };
1132
+
1133
+ this.#contentEditor = this.cf.createComponent(this.form, componyx.UI.Editor, `${this.form.id}_content_field_editor`, this.form.contentFieldEditorId, config);
1134
+ }
1135
+ else
1136
+ {
1137
+ this.#contentEditor.addEditableElement({ element: contentFieldEl });
1138
+ wrappedCallback();
1139
+ }
1140
+ }
1141
+
1142
+ #renderContentField(field)
1143
+ {
1144
+ const contentFieldEl = this.#createContentFieldElement(field);
1145
+ contentFieldEl.innerHTML = (field.trustHTML) ? field.value : this.sanitizer ? this.form.sanitizer(field.value) : this.form.getSanitizer().sanitize(field.value);
1146
+ }
1147
+
1148
+ #createContentFieldElement(field)
1149
+ {
1150
+ const rootElement = (this.form.isViewMode()) ? field.viewElement : field.editElement,
1151
+ contentFieldEl = $lib.element({ container: rootElement, attrs: { class: this.getCssClass(this.classOption.CONTENT) } });
1152
+
1153
+ this.#updateContentFieldSettings(field);
1154
+
1155
+ if (!this.form.isViewMode())
1156
+ contentFieldEl.contentEditable = true;
1157
+
1158
+ return contentFieldEl;
1159
+ }
1160
+
1161
+ #updateContentFieldSettings(field)
1162
+ {
1163
+ const rootElement = (this.form.isViewMode()) ? field.viewElement : field.editElement,
1164
+ contentFieldEl = rootElement.querySelector(`:scope > .${this.getCssClass(this.classOption.CONTENT)}`),
1165
+ style = contentFieldEl.style;
1166
+
1167
+ const styleMap = [
1168
+ { prop: 'margin', unit: true },
1169
+ { prop: 'padding', unit: true },
1170
+ { prop: 'borderWidth', unit: true },
1171
+ { prop: 'borderColor', rgba: true },
1172
+ { prop: 'borderRadius', unit: true },
1173
+ { prop: 'backgroundColor', rgba: true }
1174
+ ];
1175
+
1176
+ for (const { prop, unit, rgba } of styleMap)
1177
+ {
1178
+ if (!$lib.isEmpty(field[prop]) && field[prop] != '0')
1179
+ style[prop] = unit ? $lib.unit(field[prop]) : rgba ? `rgba(${field[prop]})` : field[prop];
1180
+ else
1181
+ style.removeProperty(prop);
1182
+ }
1183
+ }
1184
+
1185
+ #renderSpacerField(field)
1186
+ {
1187
+ const rootElement = (this.form.isViewMode()) ? field.viewElement : field.editElement,
1188
+ spacerEl = $lib.element({ container: rootElement, attrs: { class: this.getCssClass(this.classOption.SPACER) } });
1189
+
1190
+ this.#updateSpacerFieldSettings(field);
1191
+
1192
+ return spacerEl;
1193
+ }
1194
+
1195
+ #updateSpacerFieldSettings(field)
1196
+ {
1197
+ const rootElement = (this.form.isViewMode()) ? field.viewElement : field.editElement,
1198
+ spacerEl = rootElement.querySelector(`:scope > .${this.getCssClass(this.classOption.SPACER)}`),
1199
+ divider = this.getCssClass(this.classOption.DIVIDER);
1200
+
1201
+ if (field.height)
1202
+ spacerEl.style.height = $lib.unit(field.height);
1203
+ else
1204
+ spacerEl.style.removeProperty('height');
1205
+
1206
+ if (field.showDivider)
1207
+ spacerEl.classList.add(divider);
1208
+ else
1209
+ spacerEl.classList.remove(divider);
1210
+ }
1211
+
1212
+ #createTextBox(field, element)
1213
+ {
1214
+ if (field.inputId)
1215
+ return element.appendChild(this.form.getSourceElement(field.inputId));
1216
+ else
1217
+ return this.cf.createInput({ type: (field.inputType == this.InputTypeOption.TEXTAREA) ? 'textarea' : 'text', container: element });
1218
+ }
1219
+
1220
+ #createFieldOptions(field, container, callback = () => { })
1221
+ {
1222
+ const useRowBasedOptions = (field.inlineOptions && (!$lib.isEmpty(field.optionWidth) && field.optionWidth != 'auto')),
1223
+ createEmpty = () => { $lib.element({ container: container, content: this.labels.addItemPrefix + this.labels.fieldOption, attrs: { class: this.getCssClass(this.classOption.EMPTY) } }); },
1224
+ createOption = (option) =>
1225
+ {
1226
+ const isRadio = field.inputType === this.InputTypeOption.RADIO,
1227
+ input = (option.inputId) ? this.form.getSourceElement(option.inputId) :
1228
+ (field.inputId) ? this.form.getSourceElement(field.inputId) : $lib.element('', '', 'input', ''),
1229
+ config = (useRowBasedOptions) ? { style: `flex-basis:${field.optionWidth}` } : { inline: field.inlineOptions || false };
1230
+
1231
+ if (field.inputType === this.InputTypeOption.SWITCH)
1232
+ config.switch = true;
1233
+
1234
+ this.form.ensureItemId(option);
1235
+ this.#setInputEditAttributes({ input, name: field.name, value: option.value, disabled: option.disabled });
1236
+ input.checked = option.selected;
1237
+ input.type = isRadio ? 'radio' : 'checkbox';
1238
+ this.cf.createFormField(optionsContainer, this.form.getId(field, `${option.id}_field`), option.formFieldId, option.label, input, config);
1239
+
1240
+ option.element = input;
1241
+ this.form.events.onRenderEditFieldOption.fire(this.form, { field, option });
1242
+ };
1243
+
1244
+ container.innerHTML = '';
1245
+ const optionsContainer = $lib.element({ container: container, attrs: { class: this.getCssClass(this.classOption.FIELD_OPTIONS) } });
1246
+
1247
+ if (field.inlineOptions)
1248
+ optionsContainer.classList.add(this.getCssClass(this.classOption.FIELD_OPTIONS_INLINE));
1249
+
1250
+ if (useRowBasedOptions)
1251
+ optionsContainer.classList.add(this.getCssClass(this.classOption.FIELD_OPTIONS_ROW));
1252
+
1253
+ if (field.dataSourceId)
1254
+ {
1255
+ this.#loadFieldOptions(field).then(result =>
1256
+ {
1257
+ if (result.itemList.length > 0)
1258
+ {
1259
+ result.itemList.forEach(item =>
1260
+ {
1261
+ createOption({
1262
+ id: item.id || this.form.guid(),
1263
+ name: field.name,
1264
+ value: item.value,
1265
+ label: item.text || item.value,
1266
+ selected: item.selected,
1267
+ disabled: item.disabled
1268
+ });
1269
+ });
1270
+ }
1271
+ else
1272
+ {
1273
+ createEmpty();
1274
+ }
1275
+
1276
+ callback();
1277
+ });
1278
+ }
1279
+ else if (!$lib.isEmpty(field.options))
1280
+ {
1281
+ field.options.forEach(createOption);
1282
+ callback();
1283
+ }
1284
+ else
1285
+ {
1286
+ createEmpty();
1287
+ callback();
1288
+ }
1289
+ }
1290
+
1291
+ async #loadFieldOptions(field)
1292
+ {
1293
+ const empty = { itemList: [] },
1294
+ ds = this.form.dataSources.find(d => d.id === field.dataSourceId);
1295
+
1296
+ if (!ds) return empty;
1297
+
1298
+ const result = this.form.fetchFromEndpoint(ds, { form: this.form, field });
1299
+ return result || empty;
1300
+ }
1301
+
1302
+ #setInputEditAttributes({ input, name, value, placeholder, disabled, readOnly })
1303
+ {
1304
+ input.name = name || '';
1305
+ input.value = value || '';
1306
+ input.placeholder = placeholder || '';
1307
+ input.disabled = (!$lib.isEmpty(disabled)) ? disabled : false;
1308
+ input.readOnly = (!$lib.isEmpty(disabled)) ? readOnly : false;
1309
+ }
1310
+
1311
+ async #formFieldPostRender(field, formField)
1312
+ {
1313
+ if (field)
1314
+ {
1315
+ let el = formField.element;
1316
+
1317
+ el.setAttribute(this.form.fieldAttribute, field.id);
1318
+ field.element = el;
1319
+
1320
+ if (this.form.isBuildMode())
1321
+ {
1322
+ let label = el.querySelector(':scope > label');
1323
+
1324
+ if (field.type === 'FieldSet' || field.type === 'Field')
1325
+ {
1326
+ const input = this.#createLabelInput(label, field.label);
1327
+
1328
+ if (field.type === 'Field')
1329
+ {
1330
+ input.onblur = (e) =>
1331
+ {
1332
+ if (this.form.autoNameCandidates.has(field.id)) // make sure that we set the field-name to match the label
1333
+ {
1334
+ const nameSettingInput = $lib(`#${this.form.id}_FieldPanel_name_input`);
1335
+
1336
+ if (nameSettingInput)
1337
+ {
1338
+ const val = e.target.value.trim(),
1339
+ name = this.form.getValidFieldNameFromLabel(val),
1340
+ event = new FocusEvent('blur', { bubbles: true, cancelable: true });
1341
+
1342
+ nameSettingInput.value = `${name}`;
1343
+ nameSettingInput.dispatchEvent(event);
1344
+ }
1345
+ }
1346
+ };
1347
+ }
1348
+ }
1349
+ else
1350
+ {
1351
+ label.classList.add(this.getCssClass(this.classOption.DISABLED));
1352
+ label.textContent = field.label;
1353
+ }
1354
+
1355
+ this.#createBuildView(field);
1356
+ }
1357
+
1358
+ const callback = () =>
1359
+ {
1360
+ this.form.events.onRenderField.fire(this.form, { field: field });
1361
+
1362
+ this.form.dataBinder.initModelValue(field);
1363
+ this.updateField(field, false);
1364
+ };
1365
+
1366
+ if (this.form.isViewMode())
1367
+ {
1368
+ this.#createViewTemplate(field, field.viewElement, callback);
1369
+ }
1370
+ else
1371
+ {
1372
+ this.#createEditTemplate(field, field.editElement, callback);
1373
+ }
1374
+ }
1375
+ }
1376
+
1377
+ #createBuildView(item)
1378
+ {
1379
+ if (item.type === 'Field' && item.cssClassIcon)
1380
+ $lib.element({ container: item.element, tag: 'i', attrs: { "class": `${this.getCssClass(this.classOption.ICON)} ${item.cssClassIcon}`.trim() } });
1381
+
1382
+ this.#createBuildMenu(item);
1383
+ this.cf.createDragHandle(item.element);
1384
+ this.form.draggable.createDraggableField(item);
1385
+ this.#createSelectable(item);
1386
+ }
1387
+
1388
+ #createBuildMenu(item)
1389
+ {
1390
+ const menuEl = $lib.element(item.element, '', '', '', { "class": this.getCssClass(this.classOption.BUILD_MENU) }),
1391
+ addAction = (item, clone) =>
1392
+ {
1393
+ const action = (item.type === 'FieldSet')
1394
+ ? this.form.addFieldSet.bind(this.form, item, clone)
1395
+ : this.form.addField.bind(this.form, item, clone);
1396
+
1397
+ return () =>
1398
+ {
1399
+ action();
1400
+ setTimeout(() =>
1401
+ {
1402
+ const doc = this.form.ownerDocument || document;
1403
+ doc.activeElement?.blur();
1404
+ }, 0);
1405
+ };
1406
+ };
1407
+
1408
+ this.cf.createButton(menuEl, this.form.getId(item, 'remove'), this.form.removeFieldButtonId,
1409
+ {
1410
+ hasIcon: true,
1411
+ cssClassIcon: 'ico-bin',
1412
+ cssClass: this.getCssClass(this.classOption.REMOVE),
1413
+ command: (item.type === 'FieldSet') ? this.form.removeFieldSet.bind(this.form, item, true) : this.form.removeField.bind(this.form, item, true)
1414
+ });
1415
+ this.cf.createButton(menuEl, this.form.getId(item, 'clone'), this.form.cloneFieldButtonId,
1416
+ {
1417
+ hasIcon: true,
1418
+ cssClassIcon: 'ico-copy',
1419
+ cssClass: this.getCssClass(this.classOption.CLONE),
1420
+ command: addAction(item, true)
1421
+ });
1422
+ this.cf.createButton(menuEl, this.form.getId(item, 'add'), this.form.addFieldButtonId,
1423
+ {
1424
+ hasIcon: true,
1425
+ cssClassIcon: 'ico-plus',
1426
+ cssClass: this.getCssClass(this.classOption.ADD),
1427
+ command: addAction(item, false)
1428
+ });
1429
+ }
1430
+
1431
+ #createSelectable(item)
1432
+ {
1433
+ if (!$lib.has(item.element, 'click', this.#selectItem))
1434
+ $lib.on(item.element, 'click', this.#selectItem, [item], this);
1435
+ }
1436
+
1437
+ #selectItem(item, evt)
1438
+ {
1439
+ const clickedEl = evt.target;
1440
+
1441
+ if (clickedEl.closest(`.${this.getCssClass(this.classOption.BUILD_MENU)}`)) // do not select the field(set) when any of the menu-buttons were clicked
1442
+ return;
1443
+
1444
+ if (item.type === 'FieldSet')
1445
+ this.form.selectFieldSet(item);
1446
+ else
1447
+ this.form.selectField(item);
1448
+
1449
+ evt.stopPropagation();
1450
+ }
1451
+
1452
+ #updateDisabledState(item)
1453
+ {
1454
+ const el = item.element,
1455
+ cssClass = this.getCssClass(this.classOption.DISABLED);
1456
+
1457
+ if (item.disabled)
1458
+ {
1459
+ el.classList.add(cssClass);
1460
+
1461
+ if (!this.form.isBuildMode())
1462
+ el.inert = true;
1463
+ }
1464
+ else
1465
+ {
1466
+ if (this.form.isViewMode())
1467
+ return;
1468
+
1469
+ el.classList.remove(cssClass);
1470
+ el.inert = false;
1471
+
1472
+ const isOptionField = [this.InputTypeOption.CHECKBOX, this.InputTypeOption.SWITCH, this.InputTypeOption.RADIO].includes(item.inputType);
1473
+
1474
+ if (item.type === 'Field')
1475
+ {
1476
+ if (item.inputType === this.InputTypeOption.COMBOBOX)
1477
+ {
1478
+ (item.options || []).forEach((opt) =>
1479
+ {
1480
+ const comboBox = $UI.store[this.form.getId(item, 'input')];
1481
+
1482
+ if (opt.disabled)
1483
+ comboBox.disableItem(opt.id);
1484
+ else
1485
+ comboBox.enableItem(opt.id);
1486
+ });
1487
+ }
1488
+ else if (isOptionField)
1489
+ {
1490
+ (item.options || []).forEach((opt) =>
1491
+ {
1492
+ opt.element.disabled = opt.disabled;
1493
+ });
1494
+ }
1495
+ }
1496
+ }
1497
+ }
1498
+
1499
+ #updateVisibleState(item)
1500
+ {
1501
+ const el = item.element,
1502
+ cssClass = this.getCssClass(this.classOption.INVISIBLE);
1503
+
1504
+ if (this.form.isBuildMode())
1505
+ {
1506
+ if (!item.visible)
1507
+ el.classList.add(cssClass);
1508
+ else
1509
+ el.classList.remove(cssClass);
1510
+ }
1511
+ else
1512
+ {
1513
+ el.style.display = (item.visible !== false) ? '' : 'none';
1514
+ }
1515
+ }
1516
+
1517
+ #ensureLineBreaks(fieldSet)
1518
+ {
1519
+ let rowWidth = 0;
1520
+
1521
+ (fieldSet.fields || []).forEach((field, index) =>
1522
+ {
1523
+ const width = field.width?.toString().endsWith('%') ? parseFloat(field.width) : 0;
1524
+
1525
+ if (!field.lineBreak && (index === 0 || width >= 100 || rowWidth >= 100 || rowWidth + width > 100)) // force line break
1526
+ this.form.addLineBreak(field);
1527
+
1528
+ rowWidth = field.lineBreak ? width : rowWidth + width;
1529
+ });
1530
+ }
1531
+
1532
+ #updateLabel(item)
1533
+ {
1534
+ if (item.type !== 'FieldSet' && item.type !== 'Field')
1535
+ return;
1536
+
1537
+ if (item.type === 'Field')
1538
+ {
1539
+ if (!this.form.isBuildMode())
1540
+ {
1541
+ $UI.store[item.element.id].updateLabel(item.label); // considers possible tooltip
1542
+ }
1543
+ else
1544
+ {
1545
+ const labelInput = item.element.querySelector(':scope > label > input');
1546
+
1547
+ if (labelInput)
1548
+ labelInput.value = item.label;
1549
+ }
1550
+
1551
+ return;
1552
+ }
1553
+ else // fieldset
1554
+ {
1555
+ const query = ':scope > legend',
1556
+ labelEl = item.element.querySelector(this.form.isBuildMode() ? `${query} > input` : query);
1557
+
1558
+ if (labelEl)
1559
+ {
1560
+ if (labelEl.nodeName === 'INPUT') // build mode
1561
+ labelEl.value = item.label;
1562
+ else if (item.tooltip)
1563
+ labelEl.firstChild.textContent = item.label;
1564
+ else
1565
+ labelEl.textContent = item.label;
1566
+ }
1567
+ }
1568
+ }
1569
+
1570
+ #updateLineBreak(item)
1571
+ {
1572
+ const hasLineBreak = item.lineBreak;
1573
+
1574
+ if (!hasLineBreak && item.lineBreakElement?.isConnected)
1575
+ this.form.removeLineBreak(item);
1576
+
1577
+ if (hasLineBreak && !item.lineBreakElement?.isConnected)
1578
+ this.form.addLineBreak(item);
1579
+ }
1580
+
1581
+ #updateFieldSetTooltip(fieldSet)
1582
+ {
1583
+ const tooltipEl = fieldSet.element.querySelector(`.${this.getCssClass(this.classOption.TOOLTIP_ICON)}`),
1584
+ tooltipManager = this.form.tooltipManager,
1585
+ hasTooltip = tooltipManager.hasTemplate(fieldSet.id);
1586
+
1587
+ if (!tooltipEl)
1588
+ return;
1589
+
1590
+ tooltipEl.style.display = !$lib.isEmpty(fieldSet.tooltip) ? '' : 'none';
1591
+
1592
+ if ($lib.isEmpty(fieldSet.tooltip) && hasTooltip)
1593
+ {
1594
+ tooltipManager.removeTooltip(fieldSet.id);
1595
+ }
1596
+ else if (!$lib.isEmpty(fieldSet.tooltip) && !hasTooltip)
1597
+ {
1598
+ tooltipManager.addTooltip(fieldSet.id, fieldSet.tooltip);
1599
+ tooltipManager.addTrigger(tooltipEl, fieldSet.id);
1600
+ }
1601
+ }
1602
+
1603
+ #updateFieldIndicators(field)
1604
+ {
1605
+ let cssClass = this.getCssClass(this.classOption.FIELD_INDICATORS),
1606
+ container = field.element.querySelector(`.${cssClass}`),
1607
+ excludeKeys = ['type', 'compareOperator', 'parent'];
1608
+
1609
+ if (!container)
1610
+ container = $lib.element({ container: field.element, attrs: { "class": cssClass } });
1611
+
1612
+ let iconRules = container.querySelector(`.${this.getCssClass(this.classOption.RULES_ICON)}`),
1613
+ iconValidator = container.querySelector(`.${this.getCssClass(this.classOption.VALIDATOR_ICON)}`);
1614
+
1615
+ if (field.ruleCases?.length)
1616
+ {
1617
+ if (!iconRules)
1618
+ $lib.element({ container, tag: 'i', attrs: { "class": `${this.getCssClass(this.classOption.ICON)} ${this.getCssClass(this.classOption.RULES_ICON)}` } });
1619
+ }
1620
+ else
1621
+ iconRules?.remove();
1622
+
1623
+ if (field.required || Object.entries(field.validationSettings ?? {})
1624
+ .some(([key, value]) => !excludeKeys.includes(key) && !$lib.isEmpty(value)))
1625
+ {
1626
+ if (!iconValidator)
1627
+ $lib.element({ container, tag: 'i', attrs: { "class": `${this.getCssClass(this.classOption.ICON)} ${this.getCssClass(this.classOption.VALIDATOR_ICON)}` } });
1628
+ }
1629
+ else
1630
+ iconValidator?.remove();
1631
+
1632
+ if (!container.childNodes.length)
1633
+ container.remove();
1634
+ }
1635
+
1636
+ #updateFieldTooltip(field)
1637
+ {
1638
+ const formField = $UI.store[field.element.id],
1639
+ tooltipManager = this.form.tooltipManager,
1640
+ hasTooltip = tooltipManager.hasTemplate(field.id);
1641
+
1642
+ formField.tooltipManagerId = tooltipManager.id;
1643
+ formField.tooltipId = $lib.isEmpty(field.tooltip) ? null : field.id;
1644
+
1645
+ if ($lib.isEmpty(field.tooltip) && hasTooltip)
1646
+ tooltipManager.removeTooltip(field.id);
1647
+ else if (!$lib.isEmpty(field.tooltip) && !hasTooltip)
1648
+ tooltipManager.addTooltip(field.id, field.tooltip);
1649
+
1650
+ formField.updateTooltip();
1651
+ }
1652
+
1653
+ #updateWidth(field)
1654
+ {
1655
+ const el = field.element;
1656
+
1657
+ if ($lib.isEmpty(field.width))
1658
+ {
1659
+ el.style.removeProperty('width');
1660
+ return;
1661
+ }
1662
+
1663
+ let isPercentage = field.width.toString().trim().endsWith('%'),
1664
+ width = field.width;
1665
+
1666
+ if (isPercentage)
1667
+ {
1668
+ const numeric = parseInt(width, 10);
1669
+
1670
+ if (100 % numeric)
1671
+ width = (100 / Math.round(100 / numeric)).toFixed(6) + '%'; // always convert percentage values to correct decimals to avoid rounding issues, e.g. 33% -> 33.333333%
1672
+
1673
+ if (this.form.isBuildMode())
1674
+ {
1675
+ const inlineFieldCount = this.getInlineFieldCount(field);
1676
+ el.style.width = `calc(${width} - var(--field-margin-right, 0px) - (var(--droppable-margin, 0px) / ${inlineFieldCount}))`; // remove the margin from the width and also reserve the droppable margin
1677
+ }
1678
+ else
1679
+ el.style.width = `calc(${width} - var(--field-margin-right, 0px))`; // remove the margin from the width
1680
+ }
1681
+ else
1682
+ el.style.width = $lib.unit(width);
1683
+ }
1684
+
1685
+ getInlineFieldCount(field)
1686
+ {
1687
+ const fields = field.parent.fields,
1688
+ index = fields.findIndex(f => f.id === field.id);
1689
+
1690
+ if (index === -1) return 1;
1691
+
1692
+ let start = index;
1693
+ while (start > 0 && !fields[start].lineBreak)
1694
+ start--;
1695
+
1696
+ let end = index + 1;
1697
+ while (end < fields.length && !fields[end].lineBreak)
1698
+ end++;
1699
+
1700
+ return fields.slice(start, end).length || 1;
1701
+ }
1702
+
1703
+ #getFieldValue(field)
1704
+ {
1705
+ if (this.form.isBuildMode())
1706
+ return field.value;
1707
+
1708
+ return this.form.dataBinder.getModelValue(field);
1709
+ }
1710
+
1711
+ #updateComponentPlaceholder(field)
1712
+ {
1713
+ const component = $UI.store[this.form.getId(field, 'input')];
1714
+
1715
+ if (field.inputType === this.InputTypeOption.COMBOBOX)
1716
+ {
1717
+ const getPlaceholderValue = (optionCount) =>
1718
+ {
1719
+ if (optionCount > 0)
1720
+ {
1721
+ const base = field.placeholder ? `${field.placeholder} ` : '';
1722
+ return `${base}(${optionCount} ${this.labels.fieldOptions})`;
1723
+ }
1724
+ return field.placeholder;
1725
+ };
1726
+
1727
+ if (field.dataSourceId)
1728
+ {
1729
+ this.#loadFieldOptions(field).then(result =>
1730
+ {
1731
+ component.setPlaceholder(getPlaceholderValue(result.totalItemCount || result.itemList.length));
1732
+ });
1733
+ }
1734
+ else
1735
+ {
1736
+ component.setPlaceholder(getPlaceholderValue(field.options?.length || 0));
1737
+ }
1738
+ }
1739
+ else if (field.inputType === this.InputTypeOption.MASKEDTEXTBOX)
1740
+ component.mask = field.componentSettings?.mask;
1741
+ else if (component.setPlaceholder)
1742
+ component.setPlaceholder(field.placeholder);
1743
+ }
1744
+
1745
+ #updateContentField(field)
1746
+ {
1747
+ if (!this.form.isBuildMode())
1748
+ return;
1749
+
1750
+ this.#updateContentFieldValue(field);
1751
+ this.#updateContentFieldSettings(field);
1752
+ }
1753
+
1754
+ #updateContentFieldValue(field)
1755
+ {
1756
+ const component = this.#contentEditor;
1757
+
1758
+ if (!component || component.renderState != $base.static.RenderState.RENDERED)
1759
+ return;
1760
+
1761
+ const contentId = this.form.getId(field, 'content'),
1762
+ index = component.editableElements.findIndex(item => item.id === contentId);
1763
+
1764
+ if (index !== -1)
1765
+ {
1766
+ const currentValue = component.getValue();
1767
+ if (currentValue !== field.value)
1768
+ component.setValue(field.value, component.editableElements[index]);
1769
+ }
1770
+ }
1771
+
1772
+ #updateComponentValue(field)
1773
+ {
1774
+ const component = $UI.store[this.form.getId(field, 'input')],
1775
+ settings = field.componentSettings || {};
1776
+
1777
+ if (component.renderState != $base.static.RenderState.RENDERED)
1778
+ return;
1779
+
1780
+ let currentValue = component.getValue(),
1781
+ modelValue = this.#getFieldValue(field);
1782
+
1783
+ // we suppress the next change event if the component value is set here (inner input fires onchange by default)
1784
+ if (field.inputType === this.InputTypeOption.COMBOBOX)
1785
+ {
1786
+ const currentArray = currentValue ? currentValue.split(',').map(s => s.trim()) : [],
1787
+ modelValueArray = Array.isArray(modelValue) ? modelValue : [],
1788
+ isEqual = currentArray.length === modelValueArray.length &&
1789
+ currentArray.every((val, index) => val === modelValueArray[index]);
1790
+
1791
+ if (!isEqual)
1792
+ {
1793
+ component.suppressNextChangeEvent(() => { component.setValue(modelValueArray.join(',')); });
1794
+ }
1795
+ }
1796
+ else if (field.inputType === this.InputTypeOption.DATEPICKER)
1797
+ {
1798
+ let formattedValue = modelValue; // value is always in iso-date format (YYYY-MM-DD), this format is supported by DatePicker
1799
+
1800
+ if (!modelValue && settings.today)
1801
+ formattedValue = $lib.formatDate(new Date(), component.dateFormat);
1802
+
1803
+ if (formattedValue !== currentValue)
1804
+ {
1805
+ component.suppressNextChangeEvent(() => { component.setValue(formattedValue); });
1806
+ }
1807
+ }
1808
+ else if (field.inputType >= this.InputTypeOption.MASKEDTEXTBOX && (component.setValue && currentValue != modelValue))
1809
+ {
1810
+ component.suppressNextChangeEvent(() => { component.setValue((modelValue == undefined) ? null : modelValue); });
1811
+ }
1812
+ }
1813
+
1814
+ #updateRepeatButton(fieldSet)
1815
+ {
1816
+ const id = this.form.getId(fieldSet, 'add_repeat'),
1817
+ isRepeatable = fieldSet.parent && fieldSet.parent.type === 'FieldSet' && fieldSet.repeatable && !fieldSet.repeatGroupId,
1818
+ addButton = $UI.store[id],
1819
+ baseLabel = fieldSet.repeatLabel || fieldSet.label,
1820
+ label = baseLabel ? `${this.labels.addItemPrefix} ${baseLabel}` : this.labels.fieldSetRepeatButton;
1821
+
1822
+ if (addButton && !addButton.element?.isConnected)
1823
+ addButton.destroy();
1824
+
1825
+ if (isRepeatable)
1826
+ {
1827
+ fieldSet.element.classList.add(this.getCssClass(this.classOption.REPEATABLE));
1828
+
1829
+ if (!addButton || !addButton.renderState)
1830
+ {
1831
+ const container = fieldSet.element;
1832
+
1833
+ this.cf.createButton(container, id, this.form.addRepeatButtonId,
1834
+ {
1835
+ hasIcon: true,
1836
+ transparent: false,
1837
+ cssClassIcon: 'ico-plus',
1838
+ text: label,
1839
+ cssClass: this.getCssClass(this.classOption.REPEAT_ADD),
1840
+ command: this.form.addRepeatedFieldSet.bind(this.form, fieldSet)
1841
+ });
1842
+ }
1843
+ else
1844
+ {
1845
+ addButton.text = label;
1846
+ addButton.updateContent();
1847
+ }
1848
+ }
1849
+ else if (!isRepeatable && addButton)
1850
+ {
1851
+ addButton.destroy();
1852
+ }
1853
+ }
1854
+
1855
+ #ensureRepeatContainer(fs)
1856
+ {
1857
+ this.#ensureContainerOnModel(this.form.values, fs);
1858
+
1859
+ if (this.form.bindToCustomModel && this.form.customModelGetter)
1860
+ {
1861
+ const customModel = this.form.customModelGetter();
1862
+ if (customModel)
1863
+ this.#ensureContainerOnModel(customModel, fs);
1864
+ }
1865
+ }
1866
+
1867
+ #ensureContainerOnModel(model, fs)
1868
+ {
1869
+ const fieldSets = fs.parent.fieldSets,
1870
+ originalFieldSet = fs.repeatGroupId ? fieldSets.find(f => f.id === fs.repeatGroupId) : fs,
1871
+ groupName = originalFieldSet.id,
1872
+ groupFieldSets = fieldSets.filter(fs => fs.id === originalFieldSet.id || fs.repeatGroupId === originalFieldSet.id),
1873
+ repeatGroupIndex = groupFieldSets.findIndex((f) => f.id === fs.id),
1874
+ index = repeatGroupIndex >= 0 ? repeatGroupIndex : 0;
1875
+
1876
+ if (!Array.isArray(model[groupName]))
1877
+ model[groupName] = [];
1878
+
1879
+ if (!model[groupName][index])
1880
+ model[groupName][index] = {};
1881
+
1882
+ if (model[groupName].length > groupFieldSets.length) // If repeated fieldset is removed, we correct the length
1883
+ model[groupName].length = groupFieldSets.length;
1884
+ }
1885
+ };
1886
+
1887
+ var Renderer = componyx.UI.form_modules.Renderer;
1888
+
1889
+ export { Renderer as default };