underpost 3.2.5 → 3.2.9

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 (144) hide show
  1. package/.github/workflows/release.cd.yml +1 -2
  2. package/CHANGELOG.md +351 -1
  3. package/CLI-HELP.md +40 -13
  4. package/Dockerfile +0 -4
  5. package/README.md +4 -4
  6. package/bin/build.js +14 -5
  7. package/bin/deploy.js +570 -1
  8. package/bin/file.js +6 -0
  9. package/conf.js +11 -2
  10. package/jsconfig.json +1 -1
  11. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
  12. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +2 -2
  13. package/manifests/deployment/dd-default-development/deployment.yaml +2 -6
  14. package/manifests/deployment/dd-test-development/deployment.yaml +136 -66
  15. package/manifests/deployment/dd-test-development/proxy.yaml +41 -5
  16. package/package.json +24 -15
  17. package/scripts/k3s-node-setup.sh +2 -2
  18. package/scripts/nat-iptables.sh +103 -18
  19. package/src/api/core/core.controller.js +10 -10
  20. package/src/api/core/core.service.js +10 -10
  21. package/src/api/default/default.controller.js +10 -10
  22. package/src/api/default/default.service.js +10 -10
  23. package/src/api/document/document.controller.js +12 -12
  24. package/src/api/document/document.model.js +10 -16
  25. package/src/api/file/file.controller.js +8 -8
  26. package/src/api/file/file.model.js +10 -10
  27. package/src/api/file/file.service.js +36 -36
  28. package/src/api/test/test.controller.js +8 -8
  29. package/src/api/test/test.service.js +8 -8
  30. package/src/api/user/guest.service.js +99 -0
  31. package/src/api/user/user.controller.js +6 -6
  32. package/src/api/user/user.model.js +8 -13
  33. package/src/api/user/user.service.js +3 -20
  34. package/src/cli/cluster.js +61 -14
  35. package/src/cli/db.js +47 -2
  36. package/src/cli/deploy.js +67 -35
  37. package/src/cli/fs.js +79 -8
  38. package/src/cli/image.js +43 -1
  39. package/src/cli/index.js +26 -1
  40. package/src/cli/release.js +57 -1
  41. package/src/cli/repository.js +69 -31
  42. package/src/cli/run.js +415 -36
  43. package/src/cli/ssh.js +1 -1
  44. package/src/cli/static.js +43 -115
  45. package/src/client/Default.index.js +21 -33
  46. package/src/client/components/core/404.js +4 -4
  47. package/src/client/components/core/500.js +4 -4
  48. package/src/client/components/core/Account.js +73 -60
  49. package/src/client/components/core/AgGrid.js +23 -33
  50. package/src/client/components/core/Alert.js +12 -13
  51. package/src/client/components/core/AppStore.js +1 -1
  52. package/src/client/components/core/Auth.js +35 -37
  53. package/src/client/components/core/Badge.js +7 -13
  54. package/src/client/components/core/BtnIcon.js +15 -17
  55. package/src/client/components/core/CalendarCore.js +42 -63
  56. package/src/client/components/core/Chat.js +13 -15
  57. package/src/client/components/core/ClientEvents.js +87 -0
  58. package/src/client/components/core/ColorPaletteElement.js +309 -0
  59. package/src/client/components/core/Content.js +17 -14
  60. package/src/client/components/core/Css.js +15 -71
  61. package/src/client/components/core/CssCore.js +12 -16
  62. package/src/client/components/core/D3Chart.js +4 -4
  63. package/src/client/components/core/Docs.js +64 -91
  64. package/src/client/components/core/DropDown.js +69 -91
  65. package/src/client/components/core/EventBus.js +92 -0
  66. package/src/client/components/core/EventsUI.js +14 -17
  67. package/src/client/components/core/FileExplorer.js +96 -228
  68. package/src/client/components/core/FullScreen.js +47 -75
  69. package/src/client/components/core/Input.js +24 -69
  70. package/src/client/components/core/Keyboard.js +25 -18
  71. package/src/client/components/core/KeyboardAvoidance.js +145 -0
  72. package/src/client/components/core/LoadingAnimation.js +25 -31
  73. package/src/client/components/core/LogIn.js +41 -41
  74. package/src/client/components/core/LogOut.js +23 -14
  75. package/src/client/components/core/Modal.js +462 -178
  76. package/src/client/components/core/NotificationManager.js +14 -18
  77. package/src/client/components/core/Panel.js +54 -50
  78. package/src/client/components/core/PanelForm.js +25 -125
  79. package/src/client/components/core/Polyhedron.js +110 -214
  80. package/src/client/components/core/PublicProfile.js +39 -32
  81. package/src/client/components/core/Recover.js +48 -44
  82. package/src/client/components/core/Responsive.js +88 -32
  83. package/src/client/components/core/RichText.js +9 -18
  84. package/src/client/components/core/Router.js +24 -3
  85. package/src/client/components/core/SearchBox.js +37 -37
  86. package/src/client/components/core/SignUp.js +39 -30
  87. package/src/client/components/core/SocketIo.js +31 -2
  88. package/src/client/components/core/SocketIoHandler.js +6 -6
  89. package/src/client/components/core/ToggleSwitch.js +8 -20
  90. package/src/client/components/core/ToolTip.js +5 -17
  91. package/src/client/components/core/Translate.js +56 -59
  92. package/src/client/components/core/Validator.js +26 -16
  93. package/src/client/components/core/Wallet.js +15 -26
  94. package/src/client/components/core/Worker.js +163 -27
  95. package/src/client/components/core/windowGetDimensions.js +7 -7
  96. package/src/client/components/default/{MenuDefault.js → AppShellDefault.js} +87 -87
  97. package/src/client/components/default/CssDefault.js +12 -12
  98. package/src/client/components/default/LogInDefault.js +6 -4
  99. package/src/client/components/default/LogOutDefault.js +6 -4
  100. package/src/client/components/default/RouterDefault.js +47 -0
  101. package/src/client/components/default/SettingsDefault.js +4 -4
  102. package/src/client/components/default/SignUpDefault.js +6 -4
  103. package/src/client/components/default/TranslateDefault.js +3 -3
  104. package/src/client/services/core/core.service.js +17 -49
  105. package/src/client/services/default/default.management.js +159 -267
  106. package/src/client/services/default/default.service.js +10 -16
  107. package/src/client/services/document/document.service.js +14 -19
  108. package/src/client/services/file/file.service.js +8 -13
  109. package/src/client/services/test/test.service.js +8 -13
  110. package/src/client/services/user/guest.service.js +86 -0
  111. package/src/client/services/user/user.management.js +5 -5
  112. package/src/client/services/user/user.service.js +14 -20
  113. package/src/client/ssr/body/404.js +3 -3
  114. package/src/client/ssr/body/500.js +3 -3
  115. package/src/client/ssr/body/CacheControl.js +5 -2
  116. package/src/client/ssr/body/DefaultSplashScreen.js +19 -12
  117. package/src/client/ssr/mailer/DefaultRecoverEmail.js +19 -20
  118. package/src/client/ssr/mailer/DefaultVerifyEmail.js +15 -16
  119. package/src/client/ssr/offline/Maintenance.js +12 -11
  120. package/src/client/ssr/offline/NoNetworkConnection.js +3 -3
  121. package/src/client/ssr/pages/Test.js +2 -2
  122. package/src/client/sw/core.sw.js +212 -0
  123. package/src/index.js +1 -1
  124. package/src/runtime/express/Dockerfile +4 -4
  125. package/src/runtime/lampp/Dockerfile +8 -7
  126. package/src/runtime/wp/Dockerfile +11 -17
  127. package/src/server/client-build-docs.js +45 -46
  128. package/src/server/client-build.js +334 -60
  129. package/src/server/client-formatted.js +47 -16
  130. package/src/server/conf.js +5 -4
  131. package/src/server/data-query.js +32 -20
  132. package/src/server/dns.js +22 -0
  133. package/src/server/ipfs-client.js +232 -91
  134. package/src/server/process.js +13 -27
  135. package/src/server/start.js +17 -3
  136. package/src/server/valkey.js +141 -235
  137. package/tsconfig.docs.json +15 -0
  138. package/typedoc.json +29 -0
  139. package/jsdoc.json +0 -52
  140. package/src/client/components/core/ColorPalette.js +0 -5267
  141. package/src/client/components/core/JoyStick.js +0 -80
  142. package/src/client/components/default/RoutesDefault.js +0 -49
  143. package/src/client/sw/default.sw.js +0 -127
  144. package/src/client/sw/template.sw.js +0 -84
@@ -39,21 +39,120 @@ import { SearchBox } from './SearchBox.js';
39
39
 
40
40
  const logger = loggerFactory(import.meta, { trace: true });
41
41
 
42
- const Modal = {
43
- Data: {},
42
+ /**
43
+ * @typedef {object} ModalBarButton
44
+ * @property {string} [label] - Button label HTML
45
+ * @property {boolean} [disabled] - Whether the button is hidden/disabled
46
+ * @property {Function} [onClick] - Optional click handler override
47
+ */
48
+
49
+ /**
50
+ * @typedef {object} ModalBarConfig
51
+ * @property {{ minimize: ModalBarButton, restore: ModalBarButton, maximize: ModalBarButton, close: ModalBarButton, menu: ModalBarButton }} buttons
52
+ */
53
+
54
+ /**
55
+ * @typedef {object} ModalRenderOptions
56
+ * @property {string} [id=''] - Modal element id/class name. Auto-generated if omitted.
57
+ * @property {ModalBarConfig} [barConfig={}] - Bar button configuration.
58
+ * @property {string} [title=''] - Modal title HTML.
59
+ * @property {string|Function} [html=''] - Modal body HTML or async factory.
60
+ * @property {string} [handleType='bar'] - Drag handle type ('bar' or default full).
61
+ * @property {string} [mode=''] - Layout mode: 'view', 'slide-menu', 'slide-menu-right', 'slide-menu-left', 'dropNotification'.
62
+ * @property {object} [RouterInstance={}] - Router instance for route-aware modals.
63
+ * @property {string[]} [disableTools=[]] - Tool ids to hide ('app-icon', 'text-box', 'profile', 'center', 'lang', 'theme', 'navigator').
64
+ * @property {boolean} [observer=false] - Attach a ResizeObserver to the modal element.
65
+ * @property {boolean} [disableBoxShadow=false] - Remove box shadow from the modal.
66
+ * @property {boolean} [dragDisabled=false] - Disable dragging.
67
+ * @property {boolean} [maximize=false] - Immediately maximize the modal after render.
68
+ * @property {boolean} [disableCenter=false] - Skip auto-centering.
69
+ * @property {object} [style={}] - Inline style overrides applied via renderStyleTag.
70
+ * @property {string} [class=''] - Extra CSS class(es) on the modal wrapper.
71
+ * @property {string} [titleClass=''] - Class for the title element.
72
+ * @property {string} [btnBarModalClass=''] - Class override for the button bar container.
73
+ * @property {string} [btnContainerClass=''] - Class for each bar button.
74
+ * @property {string} [btnIconContainerClass=''] - Class for each bar button inner icon div.
75
+ * @property {string} [barClass=''] - Class for the top/bottom bar flex row.
76
+ * @property {string} [barMode=''] - Bar layout variant ('top-bottom-bar').
77
+ * @property {string} [renderType=''] - Insertion strategy ('prepend' or default append).
78
+ * @property {string} [selector='body'] - Parent selector for insertion.
79
+ * @property {string} [slideMenu=''] - Id of the slide-menu modal this view should attach to.
80
+ * @property {string} [route=''] - URL path segment for view-mode route tracking.
81
+ * @property {string} [status=''] - Status icon descriptor rendered in the bar.
82
+ * @property {boolean} [zIndexSync=false] - Enable z-index management for stacked view modals.
83
+ * @property {boolean} [query=false] - Snapshot the current query string into modal data.
84
+ * @property {string[]} [homeModals=[]] - Modal ids that belong to the home screen (not closed on home nav).
85
+ * @property {Function} [titleRender] - Function returning title HTML (takes priority over title string).
86
+ * @property {Function} [htmlMainBody] - Factory for main-body modal html (slide-menu mode).
87
+ * @property {Function} [slideMenuTopBarBannerFix] - Async factory for top-bar banner content.
88
+ * @property {number} [minSearchQueryLength=1] - Minimum search query length.
89
+ * @property {Function} [onCollapseMenu] - Callback when slide menu collapses.
90
+ * @property {Function} [onExtendMenu] - Callback when slide menu extends.
91
+ */
92
+
93
+ /**
94
+ * @typedef {object} ModalDataEntry
95
+ * @property {ModalRenderOptions} options - Original render options.
96
+ * @property {Object.<string, Function>} onCloseListener - Close event listeners keyed by id.
97
+ * @property {Object.<string, Function>} onMenuListener - Menu button event listeners.
98
+ * @property {Object.<string, Function>} onCollapseMenuListener - Collapse menu listeners.
99
+ * @property {Object.<string, Function>} onExtendMenuListener - Extend menu listeners.
100
+ * @property {Object.<string, Function>} onDragEndListener - Drag-end listeners.
101
+ * @property {Object.<string, Function>} onObserverListener - ResizeObserver listeners.
102
+ * @property {Object.<string, Function>} onClickListener - Click listeners.
103
+ * @property {Object.<string, Function>} onExpandUiListener - UI expand/collapse listeners.
104
+ * @property {Object.<string, Function>} onBarUiOpen - Bar UI open listeners.
105
+ * @property {Object.<string, Function>} onBarUiClose - Bar UI close listeners.
106
+ * @property {Object.<string, Function>} onReloadModalListener - Reload listeners.
107
+ * @property {Object.<string, Function>} onHome - Home navigation listeners.
108
+ * @property {string[]} homeModals - Home modal ids.
109
+ * @property {string} [query] - Snapshotted query string.
110
+ * @property {Function} getTop - Returns computed top offset.
111
+ * @property {Function} getHeight - Returns computed modal height.
112
+ * @property {Function} getMenuLeftStyle - Returns slide menu left CSS value.
113
+ * @property {Function} center - Centers the modal in the viewport.
114
+ * @property {object} [slideMenu] - Active slide-menu link data.
115
+ * @property {ResizeObserver} [observer] - Attached ResizeObserver.
116
+ * @property {Function} [observerCallBack] - ResizeObserver callback.
117
+ * @property {Function} [setDragInstance] - Updates drag options and re-creates the Draggable.
118
+ * @property {object} [dragInstance] - Active Draggable instance.
119
+ * @property {object} [dragOptions] - Current drag configuration.
120
+ */
121
+
122
+ class Modal {
123
+ /** @type {Object.<string, ModalDataEntry>} */
124
+ static Data = {};
44
125
 
45
- Render: async function (
126
+ /**
127
+ * Create or reload a modal. When the modal already exists in the DOM the
128
+ * existing instance is reloaded via its onReloadModalListener callbacks.
129
+ * @param {ModalRenderOptions} options
130
+ * @returns {Promise<ModalDataEntry & { id: string }>}
131
+ */
132
+ static async instance(
46
133
  options = {
47
134
  id: '',
48
135
  barConfig: {},
49
136
  title: '',
50
137
  html: '',
51
138
  handleType: 'bar',
52
- mode: '' /* slide-menu */,
139
+ mode: '',
53
140
  RouterInstance: {},
54
141
  disableTools: [],
55
142
  observer: false,
56
143
  disableBoxShadow: false,
144
+ dragDisabled: false,
145
+ maximize: false,
146
+ disableCenter: false,
147
+ style: {},
148
+ class: '',
149
+ titleClass: '',
150
+ barMode: '',
151
+ route: '',
152
+ slideMenu: '',
153
+ zIndexSync: false,
154
+ query: false,
155
+ homeModals: [],
57
156
  },
58
157
  ) {
59
158
  const originHeightBottomBar = 50;
@@ -145,11 +244,14 @@ const Modal = {
145
244
  });
146
245
  }
147
246
 
148
- Responsive.Event[`view-${idModal}`] = () => {
149
- if (!this.Data[idModal]) return delete Responsive.Event[`view-${idModal}`];
150
- if (this.Data[idModal].slideMenu) s(`.${idModal}`).style.height = `${this.Data[idModal].getHeight()}px`;
151
- };
152
- Responsive.Event[`view-${idModal}`]();
247
+ Responsive.onChanged(
248
+ () => {
249
+ if (!this.Data[idModal]) return Responsive.offChanged(`view-${idModal}`);
250
+ if (this.Data[idModal].slideMenu) s(`.${idModal}`).style.height = `${this.Data[idModal].getHeight()}px`;
251
+ },
252
+ { key: `view-${idModal}` },
253
+ );
254
+ Responsive.triggerChanged(`view-${idModal}`);
153
255
 
154
256
  // Handle view mode modal route
155
257
  if (options.route) {
@@ -251,23 +353,30 @@ const Modal = {
251
353
  // barConfig.buttons.menu.disabled = true;
252
354
  // barConfig.buttons.close.disabled = true;
253
355
  options.btnBarModalClass = 'hide';
254
- Responsive.Event[`slide-menu-${idModal}`] = () => {
255
- for (const _idModal of Object.keys(this.Data)) {
256
- if (this.Data[_idModal].slideMenu && this.Data[_idModal].slideMenu.id === idModal)
257
- this.Data[_idModal].slideMenu.callBack();
258
- }
259
- s(`.${idModal}`).style.height = `${Modal.Data[idModal].getHeight()}px`;
260
- s(`.${idModal}`).style.left = Modal.Data[idModal].getMenuLeftStyle({
261
- open: s(`.btn-bar-center-icon-menu`).classList.contains('hide') ? true : false,
262
- });
263
- if (s(`.main-body-top`)) {
264
- if (Modal.mobileModal()) {
265
- if (s(`.btn-menu-${idModal}`).classList.contains('hide') && collapseSlideMenuWidth !== slideMenuWidth)
266
- s(`.main-body-top`).classList.remove('hide');
267
- if (s(`.btn-close-${idModal}`).classList.contains('hide')) s(`.main-body-top`).classList.add('hide');
268
- } else if (!s(`.main-body-top`).classList.contains('hide')) s(`.main-body-top`).classList.add('hide');
269
- }
270
- };
356
+ Responsive.onChanged(
357
+ () => {
358
+ for (const _idModal of Object.keys(this.Data)) {
359
+ if (this.Data[_idModal].slideMenu && this.Data[_idModal].slideMenu.id === idModal)
360
+ this.Data[_idModal].slideMenu.callBack();
361
+ }
362
+ s(`.${idModal}`).style.height = `${Modal.Data[idModal].getHeight()}px`;
363
+ s(`.${idModal}`).style.left = Modal.Data[idModal].getMenuLeftStyle({
364
+ open: s(`.btn-bar-center-icon-menu`).classList.contains('hide') ? true : false,
365
+ });
366
+ if (s(`.main-body-top`)) {
367
+ if (Modal.mobileModal()) {
368
+ if (
369
+ s(`.btn-menu-${idModal}`).classList.contains('hide') &&
370
+ collapseSlideMenuWidth !== slideMenuWidth
371
+ )
372
+ s(`.main-body-top`).classList.remove('hide');
373
+ if (s(`.btn-close-${idModal}`).classList.contains('hide'))
374
+ s(`.main-body-top`).classList.add('hide');
375
+ } else if (!s(`.main-body-top`).classList.contains('hide')) s(`.main-body-top`).classList.add('hide');
376
+ }
377
+ },
378
+ { key: `slide-menu-${idModal}` },
379
+ );
271
380
  barConfig.buttons.menu.onClick = () => {
272
381
  Modal.Data[idModal][options.mode].width = slideMenuWidth;
273
382
  s(`.btn-menu-${idModal}`).classList.add('hide');
@@ -290,7 +399,7 @@ const Modal = {
290
399
  } else {
291
400
  s(`.${idModal}`).style.left = `0px`;
292
401
  }
293
- Responsive.Event[`slide-menu-${idModal}`]();
402
+ Responsive.triggerChanged(`slide-menu-${idModal}`);
294
403
  };
295
404
  barConfig.buttons.close.onClick = () => {
296
405
  Modal.Data[idModal][options.mode].width = 0;
@@ -313,7 +422,7 @@ const Modal = {
313
422
  } else {
314
423
  s(`.${idModal}`).style.left = `-${originSlideMenuWidth}px`;
315
424
  }
316
- Responsive.Event[`slide-menu-${idModal}`]();
425
+ Responsive.triggerChanged(`slide-menu-${idModal}`);
317
426
  };
318
427
  transition += `, width 0.3s`;
319
428
 
@@ -421,15 +530,17 @@ const Modal = {
421
530
  for (const event of Object.keys(Modal.Data[idModal].onBarUiOpen))
422
531
  Modal.Data[idModal].onBarUiOpen[event]();
423
532
  }
424
- Responsive.Event[`slide-menu-modal-menu`]();
533
+ Responsive.triggerChanged(`slide-menu-modal-menu`);
425
534
  Object.keys(this.Data).map((_idModal) => {
426
535
  if (this.Data[_idModal].slideMenu) {
427
536
  if (s(`.btn-maximize-${_idModal}`)) s(`.btn-maximize-${_idModal}`).click();
428
537
  }
429
538
  });
430
- Responsive.Event[`view-${'main-body'}`]();
431
- if (Responsive.Event[`view-${'bottom-bar'}`]) Responsive.Event[`view-${'bottom-bar'}`]();
432
- if (Responsive.Event[`view-${'main-body-top'}`]) Responsive.Event[`view-${'main-body-top'}`]();
539
+ Responsive.triggerChanged(`view-${'main-body'}`);
540
+ if (Responsive.hasChangedListener(`view-${'bottom-bar'}`))
541
+ Responsive.triggerChanged(`view-${'bottom-bar'}`);
542
+ if (Responsive.hasChangedListener(`view-${'main-body-top'}`))
543
+ Responsive.triggerChanged(`view-${'main-body-top'}`);
433
544
  for (const keyEvent of Object.keys(this.Data[idModal].onExpandUiListener)) {
434
545
  this.Data[idModal].onExpandUiListener[keyEvent](
435
546
  !s(`.main-body-btn-ui-open`).classList.contains('hide'),
@@ -447,14 +558,14 @@ const Modal = {
447
558
  class="fl top-bar ${options.barClass ? options.barClass : ''}"
448
559
  style="height: ${originHeightTopBar}px;"
449
560
  >
450
- ${await BtnIcon.Render({
561
+ ${await BtnIcon.instance({
451
562
  style: `height: 100%`,
452
563
  class: 'in fll main-btn-menu action-bar-box action-btn-close hide',
453
564
  label: html` <div class="${contentIconClass} action-btn-close-render">
454
565
  <i class="fa-solid fa-xmark"></i>
455
566
  </div>`,
456
567
  })}
457
- ${await BtnIcon.Render({
568
+ ${await BtnIcon.instance({
458
569
  style: `height: 100%`,
459
570
  class: `in fll main-btn-menu action-bar-box action-btn-app-icon ${
460
571
  options?.disableTools?.includes('app-icon') ? 'hide' : ''
@@ -466,10 +577,12 @@ const Modal = {
466
577
  ? 'hide'
467
578
  : ''}"
468
579
  >
469
- ${await Input.Render({
580
+ ${await Input.instance({
470
581
  id: inputSearchBoxId,
471
582
  autocomplete: 'off',
472
- placeholder: Modal.mobileModal() ? Translate.Render('search', '.top-bar-search-box') : undefined, // html`<i class="fa-solid fa-magnifying-glass"></i> ${Translate.Render('search')}`,
583
+ placeholder: Modal.mobileModal()
584
+ ? Translate.instance('search', '.top-bar-search-box')
585
+ : undefined, // html`<i class="fa-solid fa-magnifying-glass"></i> ${Translate.instance('search')}`,
473
586
  placeholderIcon: html`<div
474
587
  class="in fll"
475
588
  style="width: ${originHeightTopBar}px; height: ${originHeightTopBar}px;"
@@ -480,19 +593,19 @@ const Modal = {
480
593
  class="inl wfm key-shortcut-container-info"
481
594
  style="${renderCssAttr({ style: { top: '10px', left: '60px' } })}"
482
595
  >
483
- ${await Badge.Render({
596
+ ${await Badge.instance({
484
597
  id: 'shortcut-key-info-search',
485
598
  text: 'Shift',
486
599
  classList: 'inl',
487
600
  style: { 'z-index': 1 },
488
601
  })}
489
- ${await Badge.Render({
602
+ ${await Badge.instance({
490
603
  id: 'shortcut-key-info-search',
491
604
  text: '+',
492
605
  classList: 'inl',
493
606
  style: { 'z-index': 1, background: 'none', color: '#5f5f5f' },
494
607
  })}
495
- ${await Badge.Render({
608
+ ${await Badge.instance({
496
609
  id: 'shortcut-key-info-search',
497
610
  text: 'k',
498
611
  classList: 'inl',
@@ -508,12 +621,12 @@ const Modal = {
508
621
  <div
509
622
  class="abs top-box-profile-container ${options?.disableTools?.includes('profile') ? 'hide' : ''}"
510
623
  >
511
- ${await BtnIcon.Render({
624
+ ${await BtnIcon.instance({
512
625
  style: `height: 100%`,
513
626
  class: 'in fll session-in-log-in main-btn-menu action-bar-box action-btn-profile-log-in',
514
627
  label: html` <div class="${contentIconClass} action-btn-profile-log-in-render"></div>`,
515
628
  })}
516
- ${await BtnIcon.Render({
629
+ ${await BtnIcon.instance({
517
630
  style: `height: 100%`,
518
631
  class: 'in fll session-in-log-out main-btn-menu action-bar-box action-btn-profile-log-out',
519
632
  label: html` <div class="${contentIconClass} action-btn-profile-log-out-render">
@@ -618,8 +731,8 @@ const Modal = {
618
731
  if (results.length === 0) {
619
732
  append(
620
733
  `.html-${searchBoxHistoryId}`,
621
- await BtnIcon.Render({
622
- label: html`<i class="fas fa-exclamation-circle"></i> ${Translate.Render('no-result-found')}`,
734
+ await BtnIcon.instance({
735
+ label: html`<i class="fas fa-exclamation-circle"></i> ${Translate.instance('no-result-found')}`,
623
736
  class: `wfa`,
624
737
  style: renderCssAttr({
625
738
  style: {
@@ -784,7 +897,7 @@ const Modal = {
784
897
  barConfig.buttons.menu.disabled = true;
785
898
  barConfig.buttons.close.disabled = false;
786
899
 
787
- await Modal.Render({
900
+ await Modal.instance({
788
901
  id: searchBoxHistoryId,
789
902
  barConfig,
790
903
  title: html`<div
@@ -794,13 +907,13 @@ const Modal = {
794
907
  <div class="search-box-recent-title">
795
908
  ${renderViewTitle({
796
909
  icon: html`<i class="fas fa-history mini-title"></i>`,
797
- text: Translate.Render('recent'),
910
+ text: Translate.instance('recent'),
798
911
  })}
799
912
  </div>
800
913
  <div class="search-box-result-title hide">
801
914
  ${renderViewTitle({
802
915
  icon: html`<i class="far fa-list-alt mini-title"></i>`,
803
- text: Translate.Render('results'),
916
+ text: Translate.instance('results'),
804
917
  })}
805
918
  </div>
806
919
  </div>
@@ -865,7 +978,7 @@ const Modal = {
865
978
  }
866
979
 
867
980
  // Add clear all button to the bar area, before the close button
868
- const clearAllBtnHtml = await BtnIcon.Render({
981
+ const clearAllBtnHtml = await BtnIcon.instance({
869
982
  class: `btn-search-history-clear-all btn-modal-default btn-modal-default-${searchBoxHistoryId}`,
870
983
  label: html`<i class="fas fa-trash-alt"></i>`,
871
984
  attrs: `title="Clear all recent items"`,
@@ -1032,7 +1145,7 @@ const Modal = {
1032
1145
  barConfig.buttons.menu.disabled = true;
1033
1146
  barConfig.buttons.close.disabled = true;
1034
1147
  const id = 'main-body';
1035
- await Modal.Render({
1148
+ await Modal.instance({
1036
1149
  id,
1037
1150
  barConfig,
1038
1151
  html: options.htmlMainBody ? options.htmlMainBody : () => html``,
@@ -1055,26 +1168,29 @@ const Modal = {
1055
1168
  const maxWidthInputSearchBox = 450;
1056
1169
  const paddingInputSearchBox = 5;
1057
1170
  const paddingRightSearchBox = 50;
1058
- Responsive.Event[`view-${id}`] = () => {
1059
- if (!this.Data[id] || !s(`.${id}`)) return delete Responsive.Event[`view-${id}`];
1060
- const widthInputSearchBox =
1061
- windowGetW() > maxWidthInputSearchBox ? maxWidthInputSearchBox : windowGetW();
1062
- s(`.top-bar-search-box-container`).style.width = `${
1063
- widthInputSearchBox - originHeightTopBar - paddingRightSearchBox - 1
1064
- }px`;
1065
- s(`.top-bar-search-box`).style.width = `${
1066
- widthInputSearchBox -
1067
- originHeightTopBar * 2 -
1068
- paddingRightSearchBox -
1069
- paddingInputSearchBox * 2 /*padding input*/ -
1070
- 10 /* right-margin */
1071
- }px`;
1072
- s(`.top-bar-search-box`).style.top = `${
1073
- (originHeightTopBar - s(`.top-bar-search-box`).clientHeight) / 2
1074
- }px`;
1075
- if (this.Data[id].slideMenu) s(`.${id}`).style.height = `${Modal.Data[id].getHeight()}px`;
1076
- };
1077
- Responsive.Event[`view-${id}`]();
1171
+ Responsive.onChanged(
1172
+ () => {
1173
+ if (!this.Data[id] || !s(`.${id}`)) return Responsive.offChanged(`view-${id}`);
1174
+ const widthInputSearchBox =
1175
+ windowGetW() > maxWidthInputSearchBox ? maxWidthInputSearchBox : windowGetW();
1176
+ s(`.top-bar-search-box-container`).style.width = `${
1177
+ widthInputSearchBox - originHeightTopBar - paddingRightSearchBox - 1
1178
+ }px`;
1179
+ s(`.top-bar-search-box`).style.width = `${
1180
+ widthInputSearchBox -
1181
+ originHeightTopBar * 2 -
1182
+ paddingRightSearchBox -
1183
+ paddingInputSearchBox * 2 /*padding input*/ -
1184
+ 10 /* right-margin */
1185
+ }px`;
1186
+ s(`.top-bar-search-box`).style.top = `${
1187
+ (originHeightTopBar - s(`.top-bar-search-box`).clientHeight) / 2
1188
+ }px`;
1189
+ if (this.Data[id].slideMenu) s(`.${id}`).style.height = `${Modal.Data[id].getHeight()}px`;
1190
+ },
1191
+ { key: `view-${id}` },
1192
+ );
1193
+ Responsive.triggerChanged(`view-${id}`);
1078
1194
  Keyboard.instanceMultiPressKey({
1079
1195
  id: 'input-search-shortcut-k',
1080
1196
  keys: [
@@ -1127,7 +1243,7 @@ const Modal = {
1127
1243
  class="fl ${options.barClass ? options.barClass : ''}"
1128
1244
  style="height: ${originHeightBottomBar}px;"
1129
1245
  >
1130
- ${await BtnIcon.Render({
1246
+ ${await BtnIcon.instance({
1131
1247
  style: `height: 100%`,
1132
1248
  class: `in fl${
1133
1249
  options.mode === 'slide-menu-right' ? 'r' : 'l'
@@ -1142,35 +1258,35 @@ const Modal = {
1142
1258
  </div>
1143
1259
  `,
1144
1260
  })}
1145
- ${await BtnIcon.Render({
1261
+ ${await BtnIcon.instance({
1146
1262
  style: `height: 100%`,
1147
1263
  class: `in flr main-btn-menu action-bar-box action-btn-lang ${
1148
1264
  options?.disableTools?.includes('lang') ? 'hide' : ''
1149
1265
  }`,
1150
1266
  label: html` <div class="${contentIconClass} action-btn-lang-render"></div>`,
1151
1267
  })}
1152
- ${await BtnIcon.Render({
1268
+ ${await BtnIcon.instance({
1153
1269
  style: `height: 100%`,
1154
1270
  class: `in flr main-btn-menu action-bar-box action-btn-theme ${
1155
1271
  options?.disableTools?.includes('theme') ? 'hide' : ''
1156
1272
  }`,
1157
1273
  label: html` <div class="${contentIconClass} action-btn-theme-render"></div>`,
1158
1274
  })}
1159
- ${await BtnIcon.Render({
1275
+ ${await BtnIcon.instance({
1160
1276
  style: `height: 100%`,
1161
1277
  class: `in flr main-btn-menu action-bar-box action-btn-home ${
1162
1278
  options?.disableTools?.includes('navigator') ? 'hide' : ''
1163
1279
  }`,
1164
1280
  label: html` <div class="${contentIconClass}"><i class="fas fa-home"></i></div>`,
1165
1281
  })}
1166
- ${await BtnIcon.Render({
1282
+ ${await BtnIcon.instance({
1167
1283
  style: `height: 100%`,
1168
1284
  class: `in flr main-btn-menu action-bar-box action-btn-right ${
1169
1285
  options?.disableTools?.includes('navigator') ? 'hide' : ''
1170
1286
  }`,
1171
1287
  label: html` <div class="${contentIconClass}"><i class="fas fa-chevron-right"></i></div>`,
1172
1288
  })}
1173
- ${await BtnIcon.Render({
1289
+ ${await BtnIcon.instance({
1174
1290
  style: `height: 100%`,
1175
1291
  class: `in flr main-btn-menu action-bar-box action-btn-left ${
1176
1292
  options?.disableTools?.includes('navigator') ? 'hide' : ''
@@ -1182,7 +1298,7 @@ const Modal = {
1182
1298
  if (options.heightBottomBar === 0 && options.heightTopBar > 0) {
1183
1299
  append(`.slide-menu-top-bar`, html` <div class="in ${id}">${await html()}</div>`);
1184
1300
  } else {
1185
- await Modal.Render({
1301
+ await Modal.instance({
1186
1302
  id,
1187
1303
  barConfig,
1188
1304
  html,
@@ -1201,13 +1317,16 @@ const Modal = {
1201
1317
  // maximize: true,
1202
1318
  barMode: options.barMode,
1203
1319
  });
1204
- Responsive.Event[`view-${id}`] = () => {
1205
- if (!this.Data[id] || !s(`.${id}`)) return delete Responsive.Event[`view-${id}`];
1206
- // <div class="in fll right-offset-menu-bottom-bar" style="height: 100%"></div>
1207
- // s(`.right-offset-menu-bottom-bar`).style.width = `${windowGetW() - slideMenuWidth}px`;
1208
- s(`.${id}`).style.top = `${Modal.Data['modal-menu'].getTop()}px`;
1209
- s(`.${id}`).style.width = `${windowGetW()}px`;
1210
- };
1320
+ Responsive.onChanged(
1321
+ () => {
1322
+ if (!this.Data[id] || !s(`.${id}`)) return Responsive.offChanged(`view-${id}`);
1323
+ // <div class="in fll right-offset-menu-bottom-bar" style="height: 100%"></div>
1324
+ // s(`.right-offset-menu-bottom-bar`).style.width = `${windowGetW() - slideMenuWidth}px`;
1325
+ s(`.${id}`).style.top = `${Modal.Data['modal-menu'].getTop()}px`;
1326
+ s(`.${id}`).style.width = `${windowGetW()}px`;
1327
+ },
1328
+ { key: `view-${id}` },
1329
+ );
1211
1330
  // Responsive.Event[`view-${id}`]();
1212
1331
  }
1213
1332
  EventsUI.onClick(`.action-btn-left`, (e) => {
@@ -1290,12 +1409,12 @@ const Modal = {
1290
1409
  barConfig.buttons.restore.disabled = true;
1291
1410
  barConfig.buttons.menu.disabled = true;
1292
1411
  barConfig.buttons.close.disabled = false;
1293
- await Modal.Render({
1412
+ await Modal.instance({
1294
1413
  id,
1295
1414
  barConfig,
1296
1415
  title: html`${renderViewTitle({
1297
1416
  icon: html`<i class="fas fa-language mini-title"></i>`,
1298
- text: Translate.Render('select lang'),
1417
+ text: Translate.instance('select lang'),
1299
1418
  })}`,
1300
1419
  html: async () => html`${await Translate.RenderSetting('action-drop-modal' + id)}`,
1301
1420
  titleClass: 'mini-title',
@@ -1349,7 +1468,7 @@ const Modal = {
1349
1468
  barConfig.buttons.menu.disabled = true;
1350
1469
  barConfig.buttons.close.disabled = true;
1351
1470
  const id = 'main-body-top';
1352
- await Modal.Render({
1471
+ await Modal.instance({
1353
1472
  id,
1354
1473
  barConfig,
1355
1474
  html: () => html``,
@@ -1368,24 +1487,27 @@ const Modal = {
1368
1487
  barMode: options.barMode,
1369
1488
  });
1370
1489
 
1371
- Responsive.Event[`view-${id}`] = () => {
1372
- if (!this.Data[id] || !s(`.${id}`)) return delete Responsive.Event[`view-${id}`];
1373
- s(`.${id}`).style.height =
1374
- s(`.main-body-btn-ui-close`).classList.contains('hide') &&
1375
- s(`.btn-restore-${id}`).style.display !== 'none'
1376
- ? `${windowGetH()}px`
1377
- : `${Modal.Data[id].getHeight()}px`;
1378
-
1379
- if (
1380
- s(`.main-body-btn-ui-close`).classList.contains('hide') &&
1381
- s(`.btn-restore-${id}`).style.display !== 'none'
1382
- ) {
1383
- s(`.${id}`).style.top = '0px';
1384
- } else {
1385
- s(`.${id}`).style.top = `${options.heightTopBar ? options.heightTopBar : heightDefaultTopBar}px`;
1386
- }
1387
- };
1388
- Responsive.Event[`view-${id}`]();
1490
+ Responsive.onChanged(
1491
+ () => {
1492
+ if (!this.Data[id] || !s(`.${id}`)) return Responsive.offChanged(`view-${id}`);
1493
+ s(`.${id}`).style.height =
1494
+ s(`.main-body-btn-ui-close`).classList.contains('hide') &&
1495
+ s(`.btn-restore-${id}`).style.display !== 'none'
1496
+ ? `${windowGetH()}px`
1497
+ : `${Modal.Data[id].getHeight()}px`;
1498
+
1499
+ if (
1500
+ s(`.main-body-btn-ui-close`).classList.contains('hide') &&
1501
+ s(`.btn-restore-${id}`).style.display !== 'none'
1502
+ ) {
1503
+ s(`.${id}`).style.top = '0px';
1504
+ } else {
1505
+ s(`.${id}`).style.top = `${options.heightTopBar ? options.heightTopBar : heightDefaultTopBar}px`;
1506
+ }
1507
+ },
1508
+ { key: `view-${id}` },
1509
+ );
1510
+ Responsive.triggerChanged(`view-${id}`);
1389
1511
 
1390
1512
  s(`.main-body-top`).onclick = () => s(`.btn-close-modal-menu`).click();
1391
1513
  }
@@ -1511,7 +1633,7 @@ const Modal = {
1511
1633
  >
1512
1634
  <div class="btn-bar-modal-container-render-${idModal}"></div>
1513
1635
  <div class="in flr bar-default-modal" style="z-index: 1">
1514
- ${await BtnIcon.Render({
1636
+ ${await BtnIcon.instance({
1515
1637
  class: `btn-minimize-${idModal} btn-modal-default btn-modal-default-${idModal} ${
1516
1638
  options?.btnContainerClass ? options.btnContainerClass : ''
1517
1639
  } ${options?.barConfig?.buttons?.minimize?.disabled ? 'hide' : ''}`,
@@ -1519,7 +1641,7 @@ const Modal = {
1519
1641
  ${options?.barConfig?.buttons?.minimize?.label ? options.barConfig.buttons.minimize.label : html`_`}
1520
1642
  </div>`,
1521
1643
  })}
1522
- ${await BtnIcon.Render({
1644
+ ${await BtnIcon.instance({
1523
1645
  class: `btn-restore-${idModal} btn-modal-default btn-modal-default-${idModal} ${
1524
1646
  options?.btnContainerClass ? options.btnContainerClass : ''
1525
1647
  } ${options?.barConfig?.buttons?.restore?.disabled ? 'hide' : ''}`,
@@ -1528,7 +1650,7 @@ const Modal = {
1528
1650
  </div>`,
1529
1651
  style: 'display: none',
1530
1652
  })}
1531
- ${await BtnIcon.Render({
1653
+ ${await BtnIcon.instance({
1532
1654
  class: `btn-maximize-${idModal} btn-modal-default btn-modal-default-${idModal} ${
1533
1655
  options?.btnContainerClass ? options.btnContainerClass : ''
1534
1656
  } ${options?.barConfig?.buttons?.maximize?.disabled ? 'hide' : ''}`,
@@ -1536,7 +1658,7 @@ const Modal = {
1536
1658
  ${options?.barConfig?.buttons?.maximize?.label ? options.barConfig.buttons.maximize.label : html`▢`}
1537
1659
  </div>`,
1538
1660
  })}
1539
- ${await BtnIcon.Render({
1661
+ ${await BtnIcon.instance({
1540
1662
  class: `btn-close-${idModal} btn-modal-default btn-modal-default-${idModal} ${
1541
1663
  options?.btnContainerClass ? options.btnContainerClass : ''
1542
1664
  } ${options?.barConfig?.buttons?.close?.disabled ? 'hide' : ''}`,
@@ -1544,7 +1666,7 @@ const Modal = {
1544
1666
  ${options?.barConfig?.buttons?.close?.label ? options.barConfig.buttons.close.label : html`X`}
1545
1667
  </div>`,
1546
1668
  })}
1547
- ${await BtnIcon.Render({
1669
+ ${await BtnIcon.instance({
1548
1670
  class: `btn-menu-${idModal} btn-modal-default btn-modal-default-${idModal} ${
1549
1671
  options?.btnContainerClass ? options.btnContainerClass : ''
1550
1672
  } ${options?.barConfig?.buttons?.menu?.disabled ? 'hide' : ''}`,
@@ -1572,7 +1694,7 @@ const Modal = {
1572
1694
  class="stq modal"
1573
1695
  style="${renderCssAttr({ style: { height: '50px', 'z-index': 1, top: '0px' } })}"
1574
1696
  >
1575
- ${await BtnIcon.Render({
1697
+ ${await BtnIcon.instance({
1576
1698
  style: renderCssAttr({ style: { height: '100%', color: '#5f5f5f' } }),
1577
1699
  class: `in flr main-btn-menu action-bar-box btn-icon-menu-mode`,
1578
1700
  label: html` <div class="abs center">
@@ -1588,14 +1710,14 @@ const Modal = {
1588
1710
  ></i>
1589
1711
  </div>`,
1590
1712
  })}
1591
- ${await BtnIcon.Render({
1713
+ ${await BtnIcon.instance({
1592
1714
  style: renderCssAttr({ style: { height: '100%', color: '#5f5f5f' } }),
1593
1715
  class: `in flr main-btn-menu action-bar-box btn-icon-menu-back hide`,
1594
1716
  label: html`<div class="abs center"><i class="fas fa-undo-alt"></i></div>`,
1595
1717
  })}
1596
1718
  <div class="abs sub-menu-title-container-${idModal} ac">
1597
1719
  <div class="abs nav-title-display-${idModal}">
1598
- <!-- <i class="fas fa-home"></i> ${Translate.Render('home')} -->
1720
+ <!-- <i class="fas fa-home"></i> ${Translate.instance('home')} -->
1599
1721
  </div>
1600
1722
  </div>
1601
1723
  <div class="abs nav-path-container-${idModal} ahc bold">
@@ -1635,7 +1757,7 @@ const Modal = {
1635
1757
  case 'slide-menu-right':
1636
1758
  case 'slide-menu-left':
1637
1759
  const backMenuButtonEvent = async () => {
1638
- // htmls(`.nav-title-display-${'modal-menu'}`, html`<i class="fas fa-home"></i> ${Translate.Render('home')}`);
1760
+ // htmls(`.nav-title-display-${'modal-menu'}`, html`<i class="fas fa-home"></i> ${Translate.instance('home')}`);
1639
1761
  htmls(`.nav-title-display-${'modal-menu'}`, html``);
1640
1762
  htmls(`.nav-path-display-${idModal}`, '');
1641
1763
  s(`.btn-icon-menu-back`).classList.add('hide');
@@ -1667,6 +1789,12 @@ const Modal = {
1667
1789
  if (options.onCollapseMenu) options.onCollapseMenu();
1668
1790
  s(`.sub-menu-title-container-${'modal-menu'}`).classList.add('hide');
1669
1791
  s(`.nav-path-container-${'modal-menu'}`).classList.add('hide');
1792
+ // Shrink any already-open submenu containers to icon-only width
1793
+ Object.keys(Modal.subMenuBtnClass).forEach((subMenuId) => {
1794
+ const container = s(`.menu-btn-container-children-${subMenuId}`);
1795
+ if (container && container.style.height && container.style.height !== '0px')
1796
+ container.style.width = `${collapseSlideMenuWidth}px`;
1797
+ });
1670
1798
  Object.keys(this.Data[idModal].onCollapseMenuListener).map((keyListener) =>
1671
1799
  this.Data[idModal].onCollapseMenuListener[keyListener](),
1672
1800
  );
@@ -1684,12 +1812,18 @@ const Modal = {
1684
1812
  if (options.onExtendMenu) options.onExtendMenu();
1685
1813
  s(`.sub-menu-title-container-${'modal-menu'}`).classList.remove('hide');
1686
1814
  s(`.nav-path-container-${'modal-menu'}`).classList.remove('hide');
1815
+ // Expand any already-open submenu containers back to full width
1816
+ Object.keys(Modal.subMenuBtnClass).forEach((subMenuId) => {
1817
+ const container = s(`.menu-btn-container-children-${subMenuId}`);
1818
+ if (container && container.style.height && container.style.height !== '0px')
1819
+ container.style.width = `${originSlideMenuWidth}px`;
1820
+ });
1687
1821
  Object.keys(this.Data[idModal].onExtendMenuListener).map((keyListener) =>
1688
1822
  this.Data[idModal].onExtendMenuListener[keyListener](),
1689
1823
  );
1690
1824
  }
1691
1825
  Modal.Data[idModal][options.mode].width = slideMenuWidth;
1692
- Responsive.Event[`slide-menu-${idModal}`]();
1826
+ Responsive.triggerChanged(`slide-menu-${idModal}`);
1693
1827
  });
1694
1828
 
1695
1829
  break;
@@ -1964,22 +2098,25 @@ const Modal = {
1964
2098
  callBack,
1965
2099
  id: options.slideMenu,
1966
2100
  };
1967
- Responsive.Event['h-ui-hide-' + idModal] = () => {
1968
- setTimeout(() => {
1969
- if (!s(`.${idModal}`) || !s(`.main-body-btn-ui-close`)) return;
1970
- if (s(`.btn-restore-${idModal}`) && s(`.btn-restore-${idModal}`).style.display !== 'none') {
1971
- s(`.${idModal}`).style.height = s(`.main-body-btn-ui-close`).classList.contains('hide')
1972
- ? `${windowGetH()}px`
1973
- : `${Modal.Data[idModal].getHeight()}px`;
1974
- }
1975
- s(`.${idModal}`).style.top = s(`.main-body-btn-ui-close`).classList.contains('hide')
1976
- ? `0px`
1977
- : `${options.heightTopBar ? options.heightTopBar : heightDefaultTopBar}px`;
1978
- });
1979
- };
1980
- Responsive.Event['h-ui-hide-' + idModal]();
2101
+ Responsive.onChanged(
2102
+ () => {
2103
+ setTimeout(() => {
2104
+ if (!s(`.${idModal}`) || !s(`.main-body-btn-ui-close`)) return;
2105
+ if (s(`.btn-restore-${idModal}`) && s(`.btn-restore-${idModal}`).style.display !== 'none') {
2106
+ s(`.${idModal}`).style.height = s(`.main-body-btn-ui-close`).classList.contains('hide')
2107
+ ? `${windowGetH()}px`
2108
+ : `${Modal.Data[idModal].getHeight()}px`;
2109
+ }
2110
+ s(`.${idModal}`).style.top = s(`.main-body-btn-ui-close`).classList.contains('hide')
2111
+ ? `0px`
2112
+ : `${options.heightTopBar ? options.heightTopBar : heightDefaultTopBar}px`;
2113
+ });
2114
+ },
2115
+ { key: 'h-ui-hide-' + idModal },
2116
+ );
2117
+ Responsive.triggerChanged('h-ui-hide-' + idModal);
1981
2118
  } else {
1982
- delete Responsive.Event['h-ui-hide-' + idModal];
2119
+ Responsive.offChanged('h-ui-hide-' + idModal);
1983
2120
  s(`.${idModal}`).style.width = '100%';
1984
2121
  s(`.${idModal}`).style.height = '100%';
1985
2122
  s(`.${idModal}`).style.top = `${options.heightTopBar ? options.heightTopBar : heightDefaultTopBar}px`;
@@ -2029,10 +2166,13 @@ const Modal = {
2029
2166
  id: idModal,
2030
2167
  ...this.Data[idModal],
2031
2168
  };
2032
- },
2033
- subMenuBtnClass: {},
2169
+ }
2034
2170
 
2035
- onHomeRouterEvent: async () => {
2171
+ /** @type {Object.<string, object>} */
2172
+ static subMenuBtnClass = {};
2173
+
2174
+ /** Navigate to the home route and close all non-home modals. */
2175
+ static onHomeRouterEvent = async () => {
2036
2176
  // 1. Get list of modals to close.
2037
2177
  const modalsToClose = Object.keys(Modal.Data).filter((idModal) => {
2038
2178
  const modal = Modal.Data[idModal];
@@ -2072,9 +2212,16 @@ const Modal = {
2072
2212
  if (s(`.btn-close-modal-menu`) && !s(`.btn-close-modal-menu`).classList.contains('hide')) {
2073
2213
  s(`.btn-close-modal-menu`).click();
2074
2214
  }
2075
- },
2076
- currentTopModalId: '',
2077
- zIndexSync: function ({ idModal }) {
2215
+ };
2216
+
2217
+ /** @type {string} */
2218
+ static currentTopModalId = '';
2219
+
2220
+ /**
2221
+ * Synchronise z-index for view modals, promoting the given modal to top.
2222
+ * @param {{ idModal: string }} param0
2223
+ */
2224
+ static zIndexSync({ idModal }) {
2078
2225
  setTimeout(() => {
2079
2226
  if (!this.Data[idModal]) return;
2080
2227
  const cleanTopModal = () => {
@@ -2100,14 +2247,30 @@ const Modal = {
2100
2247
  }
2101
2248
  };
2102
2249
  });
2103
- },
2104
- setTopModalCallback: function (idModal) {
2250
+ }
2251
+
2252
+ /**
2253
+ * @param {string} idModal
2254
+ */
2255
+ static setTopModalCallback(idModal) {
2105
2256
  s(`.${idModal}`).style.zIndex = '4';
2106
2257
  this.currentTopModalId = `${idModal}`;
2107
- },
2108
- mobileModal: () => windowGetW() < 600 || windowGetH() < 600,
2109
- writeHTML: ({ idModal, html }) => htmls(`.html-${idModal}`, html),
2110
- updateModal: async function ({ idModal, html, title }) {
2258
+ }
2259
+
2260
+ /** @returns {boolean} True when the viewport is considered mobile-sized. */
2261
+ static mobileModal = () => windowGetW() < 600 || windowGetH() < 600;
2262
+
2263
+ /**
2264
+ * @param {{ idModal: string, html: string }} param0
2265
+ */
2266
+ static writeHTML = ({ idModal, html }) => htmls(`.html-${idModal}`, html);
2267
+
2268
+ /**
2269
+ * Update an existing modal's content and/or title.
2270
+ * @param {{ idModal: string, html?: string, title?: string }} param0
2271
+ * @returns {Promise<boolean>}
2272
+ */
2273
+ static async updateModal({ idModal, html, title }) {
2111
2274
  if (!this.Data[idModal] || !s(`.${idModal}`)) {
2112
2275
  console.warn(`Modal ${idModal} not found for update`);
2113
2276
  return false;
@@ -2134,19 +2297,32 @@ const Modal = {
2134
2297
  }
2135
2298
 
2136
2299
  return true;
2137
- },
2138
- viewModalOpen: function () {
2300
+ }
2301
+
2302
+ /** @returns {string|undefined} Id of the first open view-mode modal, or undefined. */
2303
+ static viewModalOpen() {
2139
2304
  return Object.keys(this.Data).find((idModal) => s(`.${idModal}`) && this.Data[idModal].options.mode === 'view');
2140
- },
2141
- removeModal: function (idModal) {
2305
+ }
2306
+
2307
+ /**
2308
+ * Remove a modal element and its style tags, and delete its data entry.
2309
+ * @param {string} idModal
2310
+ */
2311
+ static removeModal(idModal) {
2142
2312
  if (!s(`.${idModal}`)) return;
2143
2313
  s(`.${idModal}`).remove();
2144
2314
  sa(`.style-${idModal}`).forEach((element) => {
2145
2315
  element.remove();
2146
2316
  });
2147
2317
  delete this.Data[idModal];
2148
- },
2149
- RenderConfirm: async function (options) {
2318
+ }
2319
+
2320
+ /**
2321
+ * Render a confirmation dialog and return a promise resolving to the user's choice.
2322
+ * @param {{ id: string, html: Function, icon?: string, disableBtnCancel?: boolean }} options
2323
+ * @returns {Promise<{ status: 'confirm'|'cancelled' }>}
2324
+ */
2325
+ static async RenderConfirm(options) {
2150
2326
  const { id } = options;
2151
2327
  append(
2152
2328
  'body',
@@ -2189,23 +2365,23 @@ const Modal = {
2189
2365
  </div>
2190
2366
  ${await options.html()}
2191
2367
  <div class="in section-mp">
2192
- ${await BtnIcon.Render({
2368
+ ${await BtnIcon.instance({
2193
2369
  class: `in section-mp form-button btn-confirm-${id}`,
2194
- label: Translate.Render('confirm'),
2370
+ label: Translate.instance('confirm'),
2195
2371
  type: 'submit',
2196
2372
  style: `margin: auto`,
2197
2373
  })}
2198
2374
  </div>
2199
2375
  <div class="in section-mp ${options.disableBtnCancel ? 'hide' : ''}">
2200
- ${await BtnIcon.Render({
2376
+ ${await BtnIcon.instance({
2201
2377
  class: `in section-mp form-button btn-cancel-${id}`,
2202
- label: Translate.Render('cancel'),
2378
+ label: Translate.instance('cancel'),
2203
2379
  type: 'submit',
2204
2380
  style: `margin: auto`,
2205
2381
  })}
2206
2382
  </div>
2207
2383
  `;
2208
- await Modal.Render({
2384
+ await Modal.instance({
2209
2385
  id,
2210
2386
  barConfig,
2211
2387
  titleClass: 'hide',
@@ -2242,10 +2418,20 @@ const Modal = {
2242
2418
  resolve({ status: 'confirm' });
2243
2419
  };
2244
2420
  });
2245
- },
2246
- labelSelectorTopOffsetStartAnimation: `-40px`,
2247
- labelSelectorTopOffsetEndAnimation: `-3px`,
2248
- menuTextLabelAnimation: (idModal, subMenuId) => {
2421
+ }
2422
+
2423
+ /** @type {string} */
2424
+ static labelSelectorTopOffsetStartAnimation = `-40px`;
2425
+
2426
+ /** @type {string} */
2427
+ static labelSelectorTopOffsetEndAnimation = `-3px`;
2428
+
2429
+ /**
2430
+ * Animate slide-menu labels into view.
2431
+ * @param {string} idModal - The slide-menu modal id.
2432
+ * @param {string} [subMenuId] - If provided, animate only this sub-menu.
2433
+ */
2434
+ static menuTextLabelAnimation = (idModal, subMenuId) => {
2249
2435
  if (
2250
2436
  !s(
2251
2437
  `.btn-icon-menu-mode-${Modal.Data[idModal].options.mode === 'slide-menu-right' ? 'left' : 'right'}`,
@@ -2288,19 +2474,66 @@ const Modal = {
2288
2474
  });
2289
2475
  }, 400);
2290
2476
  }
2291
- },
2477
+ };
2478
+
2479
+ /**
2480
+ * Builds submenu item HTML using the canonical BtnIcon pattern.
2481
+ * Centralises the item-rendering contract so individual submenu owners (e.g. Docs)
2482
+ * only need to supply data — not layout concerns.
2483
+ * @param {string} subMenuId - Submenu identifier (e.g. 'docs')
2484
+ * @param {Array<{type:string, icon:string, text:string, url:function|string}>} items
2485
+ * @param {Object} [options]
2486
+ * @param {function} [options.subMenuIcon] - Optional icon override per item type
2487
+ * @returns {Promise<string>} Rendered HTML string
2488
+ */
2489
+ static buildSubMenuItemsHtml = async (subMenuId, items = [], options = {}) => {
2490
+ let result = '';
2491
+ const _menuMode = Modal.Data['modal-menu']?.options?.mode;
2492
+ const _tooltipSide = options.tooltipSide || (_menuMode === 'slide-menu-right' ? 'right' : 'left');
2493
+ for (const item of items) {
2494
+ const tabHref = typeof item.url === 'function' ? item.url() : item.url || '';
2495
+ const icon =
2496
+ options.subMenuIcon && typeof options.subMenuIcon === 'function' ? options.subMenuIcon(item.type) : item.icon;
2497
+ result += html`${await BtnIcon.instance({
2498
+ class: `in wfa main-btn-menu submenu-btn btn-${subMenuId} btn-${subMenuId}-${item.type}`,
2499
+ label: html`<span class="inl menu-btn-icon">${icon}</span
2500
+ ><span class="menu-label-text menu-label-text-${subMenuId}"> ${item.text} </span>`,
2501
+ tabHref,
2502
+ tooltipHtml: await Badge.instance(buildBadgeToolTipMenuOption(item.text, _tooltipSide)),
2503
+ useMenuBtn: true,
2504
+ })} `;
2505
+ }
2506
+ return result;
2507
+ };
2508
+
2509
+ /**
2510
+ * Injects pre-built submenu item HTML into the submenu container and syncs
2511
+ * the collapse state so labels are hidden when the menu is icon-only.
2512
+ * @param {string} subMenuId
2513
+ * @param {string} itemsHtml
2514
+ */
2515
+ static subMenuPopulate = (subMenuId, itemsHtml) => {
2516
+ htmls(`.menu-btn-container-children-${subMenuId}`, itemsHtml);
2517
+ const _menuMode = Modal.Data['modal-menu']?.options?.mode;
2518
+ const _collapseIndicatorClass =
2519
+ _menuMode === 'slide-menu-right' ? '.btn-icon-menu-mode-left' : '.btn-icon-menu-mode-right';
2520
+ if (s(_collapseIndicatorClass) && !s(_collapseIndicatorClass).classList.contains('hide')) {
2521
+ sa(`.menu-label-text-${subMenuId}`).forEach((el) => el.classList.add('hide'));
2522
+ }
2523
+ };
2524
+
2292
2525
  // Move modal title element into the bar's render container so it aligns with control buttons
2293
2526
  /**
2294
- * Position a modal relative to an anchor element
2295
- * @param {Object} options - Positioning options
2527
+ * Position a modal relative to an anchor element.
2528
+ * @param {object} options - Positioning options
2296
2529
  * @param {string} options.modalSelector - CSS selector for the modal element
2297
2530
  * @param {string} options.anchorSelector - CSS selector for the anchor element
2298
- * @param {Object} [options.offset={x: 0, y: 6}] - Offset from anchor
2531
+ * @param {object} [options.offset={x: 0, y: 6}] - Offset from anchor
2299
2532
  * @param {string} [options.align='right'] - Horizontal alignment ('left' or 'right')
2300
2533
  * @param {boolean} [options.autoVertical=true] - Whether to automatically determine vertical position
2301
2534
  * @param {boolean} [options.placeAbove] - Force position above/below anchor (overrides autoVertical)
2302
2535
  */
2303
- positionRelativeToAnchor({
2536
+ static positionRelativeToAnchor({
2304
2537
  modalSelector,
2305
2538
  anchorSelector,
2306
2539
  offset = { x: 0, y: 6 },
@@ -2372,9 +2605,13 @@ const Modal = {
2372
2605
  } catch (e) {
2373
2606
  console.error('Error positioning modal:', e);
2374
2607
  }
2375
- },
2608
+ }
2376
2609
 
2377
- MoveTitleToBar(idModal) {
2610
+ /**
2611
+ * Move the title element inside the bar button container for inline alignment.
2612
+ * @param {string} idModal
2613
+ */
2614
+ static MoveTitleToBar(idModal) {
2378
2615
  try {
2379
2616
  const titleEl = s(`.title-modal-${idModal}`);
2380
2617
  const container = s(`.btn-bar-modal-container-render-${idModal}`);
@@ -2387,8 +2624,10 @@ const Modal = {
2387
2624
  } catch (e) {
2388
2625
  // non-fatal: keep default placement if structure not present
2389
2626
  }
2390
- },
2391
- setTopBannerLink: function () {
2627
+ }
2628
+
2629
+ /** Update the top-banner anchor href and bind the home-click handler. */
2630
+ static setTopBannerLink() {
2392
2631
  if (s(`.a-link-top-banner`)) {
2393
2632
  s(`.a-link-top-banner`).setAttribute('href', `${location.origin}${getProxyPath()}`);
2394
2633
  EventsUI.onClick(`.a-link-top-banner`, (e) => {
@@ -2396,32 +2635,72 @@ const Modal = {
2396
2635
  s(`.action-btn-home`).click();
2397
2636
  });
2398
2637
  }
2399
- },
2400
- headerTitleHeight: 40,
2401
- actionBtnCenter: function () {
2638
+ }
2639
+
2640
+ /** @type {number} */
2641
+ static headerTitleHeight = 40;
2642
+
2643
+ /** Toggle the slide-menu open/close via the center action button. */
2644
+ static actionBtnCenter() {
2402
2645
  if (!s(`.btn-close-modal-menu`).classList.contains('hide')) {
2403
2646
  return s(`.btn-close-modal-menu`).click();
2404
2647
  }
2405
2648
  if (!s(`.btn-menu-modal-menu`).classList.contains('hide')) {
2406
2649
  return s(`.btn-menu-modal-menu`).click();
2407
2650
  }
2408
- },
2409
- cleanUI: function () {
2651
+ }
2652
+
2653
+ /** Hide the top-bar, bottom-bar and slide-menu (full-screen mode). */
2654
+ static cleanUI() {
2410
2655
  s(`.top-bar`).classList.add('hide');
2411
2656
  s(`.bottom-bar`).classList.add('hide');
2412
2657
  s(`.modal-menu`).classList.add('hide');
2413
- },
2414
- restoreUI: function () {
2658
+ }
2659
+
2660
+ /** Restore the top-bar, bottom-bar and slide-menu after cleanUI. */
2661
+ static restoreUI() {
2415
2662
  s(`.top-bar`).classList.remove('hide');
2416
2663
  s(`.bottom-bar`).classList.remove('hide');
2417
2664
  s(`.modal-menu`).classList.remove('hide');
2418
- },
2419
- RenderSeoSanitizer: async () => {
2665
+ }
2666
+
2667
+ /**
2668
+ * Re-applies canonical top/height layout for maximized slide-menu-backed view modals.
2669
+ * This avoids iframe navigation leaving view containers offset at the top.
2670
+ */
2671
+ static syncViewLayout() {
2672
+ const modalMenuOptions = this.Data['modal-menu']?.options || {};
2673
+ const uiCollapsed = !!s(`.main-body-btn-ui-close`) && s(`.main-body-btn-ui-close`).classList.contains('hide');
2674
+ const topOffset = uiCollapsed ? 0 : modalMenuOptions.heightTopBar ? modalMenuOptions.heightTopBar : 50;
2675
+ const bottomOffset = uiCollapsed ? 0 : modalMenuOptions.heightBottomBar ? modalMenuOptions.heightBottomBar : 0;
2676
+ const height = `${windowGetH() - topOffset - bottomOffset}px`;
2677
+
2678
+ Object.keys(this.Data).forEach((idModal) => {
2679
+ const modalData = this.Data[idModal];
2680
+ const modalEl = s(`.${idModal}`);
2681
+ if (!modalData || !modalEl || !modalData.slideMenu) return;
2682
+ if (s(`.btn-restore-${idModal}`) && s(`.btn-restore-${idModal}`).style.display === 'none') return;
2683
+
2684
+ modalEl.style.position = 'fixed';
2685
+ modalEl.style.translate = '0px 0px';
2686
+ modalEl.style.transform = '';
2687
+ modalEl.style.top = `${topOffset}px`;
2688
+ modalEl.style.height = height;
2689
+ });
2690
+
2691
+ for (const id of coreUI) {
2692
+ const key = `view-${id}`;
2693
+ if (Responsive.hasChangedListener(key)) Responsive.triggerChanged(key);
2694
+ }
2695
+ }
2696
+
2697
+ /** Add missing `alt` attributes to all images for SEO/accessibility. */
2698
+ static RenderSeoSanitizer = async () => {
2420
2699
  sa('img').forEach((img) => {
2421
2700
  if (!img.getAttribute('alt')) img.setAttribute('alt', 'image ' + Worker.title + ' ' + s4());
2422
2701
  });
2423
- },
2424
- };
2702
+ };
2703
+ }
2425
2704
 
2426
2705
  const renderMenuLabel = ({ img, src, text, icon }) => {
2427
2706
  if (!img && !src) return html`<span class="inl menu-btn-icon">${icon}</span> ${text}`;
@@ -2478,7 +2757,7 @@ const renderViewTitle = (
2478
2757
  const buildBadgeToolTipMenuOption = (id, sideKey = 'left') => {
2479
2758
  const option = {
2480
2759
  id: `tooltip-content-main-btn-${id}`,
2481
- text: `${Translate.Render(`${id}`)}`,
2760
+ text: `${Translate.instance(`${id}`)}`,
2482
2761
  classList: 'tooltip-menu',
2483
2762
  style: { top: `-40px` },
2484
2763
  };
@@ -2548,9 +2827,14 @@ const subMenuRender = async (subMenuId) => {
2548
2827
  setTimeout(() => {
2549
2828
  Modal.menuTextLabelAnimation('modal-menu', subMenuId);
2550
2829
  });
2551
- // Open animation
2830
+ // Open animation — match the current menu width (collapsed = 50px, extended = 320px)
2831
+ const _menuMode = Modal.Data['modal-menu']?.options?.mode;
2832
+ const _collapseIndicatorClass =
2833
+ _menuMode === 'slide-menu-right' ? '.btn-icon-menu-mode-left' : '.btn-icon-menu-mode-right';
2834
+ const _isMenuCollapsed = s(_collapseIndicatorClass) && !s(_collapseIndicatorClass).classList.contains('hide');
2835
+ const _menuContainerWidth = _isMenuCollapsed ? 50 : 320;
2552
2836
  setTimeout(top, 360);
2553
- menuContainer.style.width = '320px';
2837
+ menuContainer.style.width = `${_menuContainerWidth}px`;
2554
2838
  menuContainer.style.overflow = null;
2555
2839
  menuContainer.style.height = '0px';
2556
2840
  menuContainer.style.height = `${_hBtn * 6}px`;