underpost
Version:
Underpost Platform — end-to-end CI/CD and application-delivery toolchain CLI. Covers bare metal, Kubernetes, K3s, kubeadm, LXD, container/image orchestration, secrets, databases, cron jobs, monitoring, SSH, runners, PWA + Workbox delivery, and release orc
2,957 lines • 126 kB
JavaScript
import { getId, newInstance, s4 } from './CommonJs.js';
import { Draggable } from '@neodrag/vanilla';
import { append, s, prepend, htmls, sa, getAllChildNodes, isActiveElement } from './VanillaJs.js';
import { BtnIcon } from './BtnIcon.js';
import { Responsive } from './Responsive.js';
import { loggerFactory } from './Logger.js';
import {
Css,
ThemeEvents,
Themes,
ThemesScope,
darkTheme,
renderStyleTag,
renderStatus,
renderCssAttr,
} from './Css.js';
import {
setDocTitle,
closeModalRouteChangeEvent,
handleModalViewRoute,
getProxyPath,
setPath,
coreUI,
sanitizeRoute,
getQueryParams,
setRouterReady,
} from './Router.js';
import { NotificationManager } from './NotificationManager.js';
import { EventsUI } from './EventsUI.js';
import { Translate } from './Translate.js';
import { Input, isTextInputFocused } from './Input.js';
import { DropDown } from './DropDown.js';
import { Keyboard } from './Keyboard.js';
import { Badge } from './Badge.js';
import { Worker } from './Worker.js';
import { Scroll } from './Scroll.js';
import { windowGetH, windowGetW } from './windowGetDimensions.js';
import { SearchBox } from './SearchBox.js';
import { createModalEvents } from './ClientEvents.js';
const logger = loggerFactory(import.meta, { trace: true });
/**
* @typedef {object} ModalBarButton
* @property {string} [label] - Button label HTML
* @property {boolean} [disabled] - Whether the button is hidden/disabled
* @property {Function} [onClick] - Optional click handler override
*/
/**
* @typedef {object} ModalBarConfig
* @property {{ minimize: ModalBarButton, restore: ModalBarButton, maximize: ModalBarButton, close: ModalBarButton, menu: ModalBarButton }} buttons
*/
/**
* @typedef {object} ModalRenderOptions
* @property {string} [id=''] - Modal element id/class name. Auto-generated if omitted.
* @property {ModalBarConfig} [barConfig={}] - Bar button configuration.
* @property {string} [title=''] - Modal title HTML.
* @property {string|Function} [html=''] - Modal body HTML or async factory.
* @property {string} [handleType='bar'] - Drag handle type ('bar' or default full).
* @property {string} [mode=''] - Layout mode: 'view', 'slide-menu', 'slide-menu-right', 'slide-menu-left', 'dropNotification'.
* @property {object} [RouterInstance={}] - Router instance for route-aware modals.
* @property {string[]} [disableTools=[]] - Tool ids to hide ('app-icon', 'text-box', 'profile', 'center', 'lang', 'theme', 'navigator').
* @property {boolean} [observer=false] - Attach a ResizeObserver to the modal element.
* @property {boolean} [disableBoxShadow=false] - Remove box shadow from the modal.
* @property {boolean} [dragDisabled=false] - Disable dragging.
* @property {boolean} [maximize=false] - Immediately maximize the modal after render.
* @property {boolean} [disableCenter=false] - Skip auto-centering.
* @property {object} [style={}] - Inline style overrides applied via renderStyleTag.
* @property {string} [class=''] - Extra CSS class(es) on the modal wrapper.
* @property {string} [titleClass=''] - Class for the title element.
* @property {string} [btnBarModalClass=''] - Class override for the button bar container.
* @property {string} [btnContainerClass=''] - Class for each bar button.
* @property {string} [btnIconContainerClass=''] - Class for each bar button inner icon div.
* @property {string} [barClass=''] - Class for the top/bottom bar flex row.
* @property {string} [barMode=''] - Bar layout variant ('top-bottom-bar').
* @property {string} [renderType=''] - Insertion strategy ('prepend' or default append).
* @property {string} [selector='body'] - Parent selector for insertion.
* @property {string} [slideMenu=''] - Id of the slide-menu modal this view should attach to.
* @property {string} [route=''] - URL path segment for view-mode route tracking.
* @property {string} [status=''] - Status icon descriptor rendered in the bar.
* @property {boolean} [zIndexSync=false] - Enable z-index management for stacked view modals.
* @property {boolean} [query=false] - Snapshot the current query string into modal data.
* @property {string[]} [homeModals=[]] - Modal ids that belong to the home screen (not closed on home nav).
* @property {Function} [titleRender] - Function returning title HTML (takes priority over title string).
* @property {Function} [htmlMainBody] - Factory for main-body modal html (slide-menu mode).
* @property {Function} [slideMenuTopBarBannerFix] - Async factory for top-bar banner content.
* @property {number} [minSearchQueryLength=1] - Minimum search query length.
* @property {Function} [onCollapseMenu] - Callback when slide menu collapses.
* @property {Function} [onExtendMenu] - Callback when slide menu extends.
*/
/**
* @typedef {object} ModalDataEntry
* @property {ModalRenderOptions} options - Original render options.
* @property {import('./EventBus.js').EventBus} events - Per-modal event bus backing the listener channels below.
*
* The `onX` channels are EventBus-backed proxies that preserve the historical
* `{ [key]: listener }` map API: assign to register, read+call to invoke a single
* listener, `delete` to remove, and `Object.keys` to enumerate registered keys.
* @property {Object.<string, Function>} onCloseListener - Close event listeners keyed by id.
* @property {Object.<string, Function>} onMenuListener - Menu button event listeners.
* @property {Object.<string, Function>} onCollapseMenuListener - Collapse menu listeners.
* @property {Object.<string, Function>} onExtendMenuListener - Extend menu listeners.
* @property {Object.<string, Function>} onDragEndListener - Drag-end listeners.
* @property {Object.<string, Function>} onObserverListener - ResizeObserver listeners.
* @property {Object.<string, Function>} onClickListener - Click listeners.
* @property {Object.<string, Function>} onExpandUiListener - UI expand/collapse listeners.
* @property {Object.<string, Function>} onBarUiOpen - Bar UI open listeners.
* @property {Object.<string, Function>} onBarUiClose - Bar UI close listeners.
* @property {Object.<string, Function>} onReloadModalListener - Reload listeners.
* @property {Object.<string, Function>} onHome - Home navigation listeners.
* @property {string[]} homeModals - Home modal ids.
* @property {string} [query] - Snapshotted query string.
* @property {Function} getTop - Returns computed top offset.
* @property {Function} getHeight - Returns computed modal height.
* @property {Function} getMenuLeftStyle - Returns slide menu left CSS value.
* @property {Function} center - Centers the modal in the viewport.
* @property {object} [slideMenu] - Active slide-menu link data.
* @property {ResizeObserver} [observer] - Attached ResizeObserver.
* @property {Function} [observerCallBack] - ResizeObserver callback.
* @property {Function} [setDragInstance] - Updates drag options and re-creates the Draggable.
* @property {object} [dragInstance] - Active Draggable instance.
* @property {object} [dragOptions] - Current drag configuration.
*/
class Modal {
/** @type {Object.<string, ModalDataEntry>} */
static Data = {};
/**
* Bottom offset of the slide-menu area for the given options.
* @param {ModalRenderOptions} options
* @param {number} [heightDefaultBottomBar=0]
* @returns {number}
*/
static getModalTop(options, heightDefaultBottomBar = 0) {
return windowGetH() - (options.heightBottomBar ? options.heightBottomBar : heightDefaultBottomBar);
}
/**
* Available modal height, accounting for the top/bottom bars when the UI is expanded.
* @param {ModalRenderOptions} options
* @param {number} [heightDefaultTopBar=50]
* @param {number} [heightDefaultBottomBar=0]
* @returns {number}
*/
static getModalHeight(options, heightDefaultTopBar = 50, heightDefaultBottomBar = 0) {
const barsVisible = s(`.main-body-btn-ui-close`) && !s(`.main-body-btn-ui-close`).classList.contains('hide');
return (
windowGetH() -
(barsVisible
? (options.heightTopBar ? options.heightTopBar : heightDefaultTopBar) +
(options.heightBottomBar ? options.heightBottomBar : heightDefaultBottomBar)
: 0)
);
}
/**
* Left CSS value for a slide menu in its open/closed state.
* @param {ModalRenderOptions} options
* @param {{ originSlideMenuWidth: number, collapseSlideMenuWidth: number }} widths
* @param {{ open: boolean }} [ops={ open: false }]
* @returns {string}
*/
static getModalMenuLeftStyle(options, { originSlideMenuWidth, collapseSlideMenuWidth }, ops = { open: false }) {
return `${
options.mode === 'slide-menu-right'
? `${
windowGetW() +
(ops?.open
? -1 * originSlideMenuWidth +
(options.mode === 'slide-menu-right' && s(`.btn-icon-menu-mode-right`).classList.contains('hide')
? originSlideMenuWidth - collapseSlideMenuWidth
: 0)
: originSlideMenuWidth)
}px`
: `-${ops?.open ? '0px' : originSlideMenuWidth}px`
}`;
}
/**
* Viewport-centered top/left CSS values for a modal of the given size.
* @param {{ width: number, height: number }} param0
* @returns {{ top: string, left: string }}
*/
static getModalCenter({ width, height }) {
return {
top: `${windowGetH() / 2 - height / 2}px`,
left: `${windowGetW() / 2 - width / 2}px`,
};
}
/**
* Create or reload a modal. When the modal already exists in the DOM the
* existing instance is reloaded via its onReloadModalListener callbacks.
* @param {ModalRenderOptions} options
* @returns {Promise<ModalDataEntry & { id: string }>}
*/
static async instance(
options = {
id: '',
barConfig: {},
title: '',
html: '',
handleType: 'bar',
mode: '',
RouterInstance: {},
disableTools: [],
observer: false,
disableBoxShadow: false,
dragDisabled: false,
maximize: false,
disableCenter: false,
style: {},
class: '',
titleClass: '',
barMode: '',
route: '',
slideMenu: '',
zIndexSync: false,
query: false,
homeModals: [],
},
) {
const originHeightBottomBar = 50;
const originHeightTopBar = 50;
options.heightBottomBar = 0;
options.heightTopBar = 100;
if (options && options.barMode && options.barMode === 'top-bottom-bar') {
options.heightTopBar = 50;
options.heightBottomBar = 50;
}
let width = 300;
let height = 400;
let top = options.style?.top ? options.style.top : 0;
let left = options.style?.left ? options.style.left : 0;
let transition = `opacity 0.3s, box-shadow 0.3s, bottom 0.3s`;
const originSlideMenuWidth = 320;
const collapseSlideMenuWidth = 50;
let slideMenuWidth = originSlideMenuWidth;
const minWidth = width;
const heightDefaultTopBar = 50;
const heightDefaultBottomBar = 0;
const idModal = options.id ? options.id : getId(this.Data, 'modal-');
const { bus: eventBus, channels: eventChannels } = createModalEvents();
this.Data[idModal] = {
options,
events: eventBus,
...eventChannels,
homeModals: options.homeModals ? options.homeModals : [],
query: options.query ? `${window.location.search}` : undefined,
getTop: () => Modal.getModalTop(options, heightDefaultBottomBar),
getHeight: () => Modal.getModalHeight(options, heightDefaultTopBar, heightDefaultBottomBar),
getMenuLeftStyle: (ops = { open: false }) =>
Modal.getModalMenuLeftStyle(options, { originSlideMenuWidth, collapseSlideMenuWidth }, ops),
center: () => {
const { top: centeredTop, left: centeredLeft } = Modal.getModalCenter({ width, height });
top = centeredTop;
left = centeredLeft;
},
...this.Data[idModal],
};
if (options && 'mode' in options) {
Modal.Data[idModal][options.mode] = {};
switch (options.mode) {
case 'view':
// if (options && options.slideMenu) s(`.btn-close-${options.slideMenu}`).click();
options.zIndexSync = true;
options.style = { width: '100%', ...options.style, 'min-width': `${minWidth}px` };
if (Modal.mobileModal()) {
options.barConfig.buttons.restore.disabled = true;
options.barConfig.buttons.minimize.disabled = true;
options.dragDisabled = true;
options.style.resize = 'none';
setTimeout(() => {
s(`.btn-close-modal-menu`).click();
});
}
Responsive.onChanged(
() => {
if (!this.Data[idModal]) return Responsive.offChanged(`view-${idModal}`);
if (this.Data[idModal].slideMenu) s(`.${idModal}`).style.height = `${this.Data[idModal].getHeight()}px`;
},
{ key: `view-${idModal}` },
);
Responsive.triggerChanged(`view-${idModal}`);
// Handle view mode modal route
if (options.route) {
handleModalViewRoute({
route: options.route,
RouterInstance: options.RouterInstance,
});
}
break;
case 'slide-menu':
case 'slide-menu-right':
case 'slide-menu-left':
(async () => {
if (!options.slideMenuTopBarBannerFix) {
options.slideMenuTopBarBannerFix = async () => {
let style = html``;
if (options.barMode === 'top-bottom-bar') {
style = html`<style>
.default-slide-menu-top-bar-fix-logo-container {
width: 50px;
height: 50px;
}
.default-slide-menu-top-bar-fix-logo {
width: 40px;
height: 40px;
padding: 5px;
}
.default-slide-menu-top-bar-fix-title-container-text {
font-size: 26px;
top: 8px;
color: ${darkTheme ? '#ffffff' : '#000000'};
}
</style>`;
} else {
style = html`<style>
.default-slide-menu-top-bar-fix-logo-container {
width: 100px;
height: 100px;
}
.default-slide-menu-top-bar-fix-logo {
width: 50px;
height: 50px;
padding: 24px;
}
.default-slide-menu-top-bar-fix-title-container-text {
font-size: 30px;
top: 30px;
color: ${darkTheme ? '#ffffff' : '#000000'};
}
</style>`;
}
setTimeout(() => {
if (s(`.top-bar-app-icon`) && s(`.top-bar-app-icon`).src) {
s(`.default-slide-menu-top-bar-fix-logo`).src = s(`.top-bar-app-icon`).src;
if (s(`.top-bar-app-icon`).classList.contains('negative-color'))
s(`.default-slide-menu-top-bar-fix-logo`).classList.add('negative-color');
htmls(
`.default-slide-menu-top-bar-fix-title-container`,
html`
<div class="inl default-slide-menu-top-bar-fix-title-container-text">${options.title}</div>
`,
);
} else
htmls(
`.default-slide-menu-top-bar-fix-logo-container`,
html`<div class="abs center">${s(`.action-btn-app-icon-render`).innerHTML}</div>`,
);
});
return html`${style}
<div class="in fll default-slide-menu-top-bar-fix-logo-container">
<img class="default-slide-menu-top-bar-fix-logo in fll" />
</div>
<div class="in fll default-slide-menu-top-bar-fix-title-container"></div>`;
};
}
const { barConfig } = options;
options.style = {
height: `${windowGetH() - options.heightTopBar - options.heightBottomBar}px`,
width: `${slideMenuWidth}px`,
// 'overflow-x': 'hidden',
// overflow: 'visible', // required for tooltip
'z-index': 6,
resize: 'none',
top: `${options.heightTopBar ? options.heightTopBar : heightDefaultTopBar}px`,
};
const contentIconClass = 'abs center';
top = 'auto';
left = Modal.Data[idModal].getMenuLeftStyle();
transition = '.3s';
options.dragDisabled = true;
options.titleClass = 'hide';
options.disableCenter = true;
// barConfig.buttons.maximize.disabled = true;
// barConfig.buttons.minimize.disabled = true;
// barConfig.buttons.restore.disabled = true;
// barConfig.buttons.menu.disabled = true;
// barConfig.buttons.close.disabled = true;
options.btnBarModalClass = 'hide';
Responsive.onChanged(
() => {
for (const _idModal of Object.keys(this.Data)) {
if (this.Data[_idModal].slideMenu && this.Data[_idModal].slideMenu.id === idModal)
this.Data[_idModal].slideMenu.callBack();
}
s(`.${idModal}`).style.height = `${Modal.Data[idModal].getHeight()}px`;
s(`.${idModal}`).style.left = Modal.Data[idModal].getMenuLeftStyle({
open: s(`.btn-bar-center-icon-menu`).classList.contains('hide') ? true : false,
});
if (s(`.main-body-top`)) {
if (Modal.mobileModal()) {
if (
s(`.btn-menu-${idModal}`).classList.contains('hide') &&
collapseSlideMenuWidth !== slideMenuWidth
)
s(`.main-body-top`).classList.remove('hide');
if (s(`.btn-close-${idModal}`).classList.contains('hide'))
s(`.main-body-top`).classList.add('hide');
} else if (!s(`.main-body-top`).classList.contains('hide')) s(`.main-body-top`).classList.add('hide');
}
},
{ key: `slide-menu-${idModal}` },
);
barConfig.buttons.menu.onClick = () => {
Modal.Data[idModal][options.mode].width = slideMenuWidth;
s(`.btn-menu-${idModal}`).classList.add('hide');
s(`.btn-close-${idModal}`).classList.remove('hide');
// s(`.${idModal}`).style.width = `${this.Data[idModal][options.mode].width}px`;
s(`.html-${idModal}`).style.display = 'block';
// s(`.title-modal-${idModal}`).style.display = 'block';
s(`.main-body-btn-ui-menu-menu`).classList.add('hide');
s(`.main-body-btn-ui-menu-close`).classList.remove('hide');
if (s(`.btn-bar-center-icon-menu`)) {
sa(`.btn-bar-center-icon-close`).forEach((el) => el.classList.remove('hide'));
sa(`.btn-bar-center-icon-menu`).forEach((el) => el.classList.add('hide'));
}
s(`.main-body-btn-container`).style[
true || (options.mode && options.mode.match('right')) ? 'right' : 'left'
] = options.mode && options.mode.match('right') ? `${slideMenuWidth}px` : '0px';
if (options.mode === 'slide-menu-right') {
s(`.${idModal}`).style.left = `${windowGetW() - originSlideMenuWidth}px`;
} else {
s(`.${idModal}`).style.left = `0px`;
}
Responsive.triggerChanged(`slide-menu-${idModal}`);
};
barConfig.buttons.close.onClick = () => {
Modal.Data[idModal][options.mode].width = 0;
s(`.btn-close-${idModal}`).classList.add('hide');
s(`.btn-menu-${idModal}`).classList.remove('hide');
// s(`.${idModal}`).style.width = `${this.Data[idModal][options.mode].width}px`;
// s(`.html-${idModal}`).style.display = 'none';
// s(`.title-modal-${idModal}`).style.display = 'none';
s(`.main-body-btn-ui-menu-close`).classList.add('hide');
s(`.main-body-btn-ui-menu-menu`).classList.remove('hide');
if (s(`.btn-bar-center-icon-menu`)) {
sa(`.btn-bar-center-icon-menu`).forEach((el) => el.classList.remove('hide'));
sa(`.btn-bar-center-icon-close`).forEach((el) => el.classList.add('hide'));
}
s(`.main-body-btn-container`).style[
true || (options.mode && options.mode.match('right')) ? 'right' : 'left'
] = `${0}px`;
if (options.mode === 'slide-menu-right') {
s(`.${idModal}`).style.left = `${windowGetW() + originSlideMenuWidth}px`;
} else {
s(`.${idModal}`).style.left = `-${originSlideMenuWidth}px`;
}
Responsive.triggerChanged(`slide-menu-${idModal}`);
};
transition += `, width 0.3s`;
setTimeout(() => {
setTimeout(btnCloseEvent);
append(
'body',
html`
<div
class="abs main-body-btn-container"
style="top: ${options.heightTopBar + 50}px; z-index: 9; ${true ||
(options.mode && options.mode.match('right'))
? 'right'
: 'left'}: 0px; width: 50px; height: 150px; transition: .3s"
>
<div
class="abs main-body-btn main-body-btn-ui"
style="top: 0px; ${true || (options.mode && options.mode.match('right')) ? 'right' : 'left'}: 0px"
>
<div class="abs center">
<i class="fas fa-caret-down main-body-btn-ui-open hide"></i>
<i class="fas fa-caret-up main-body-btn-ui-close"></i>
</div>
</div>
<div
class="abs main-body-btn main-body-btn-menu"
style="top: 50px; ${true || (options.mode && options.mode.match('right'))
? 'right'
: 'left'}: 0px"
>
<div class="abs center">
<i class="fa-solid fa-xmark hide main-body-btn-ui-menu-close"></i>
<i class="fa-solid fa-bars main-body-btn-ui-menu-menu"></i>
</div>
</div>
<div
class="abs main-body-btn main-body-btn-bar-custom ${options?.slideMenuTopBarBannerFix
? ''
: 'hide'}"
style="top: 100px; ${true || (options.mode && options.mode.match('right'))
? 'right'
: 'left'}: 0px"
>
<div class="abs center">
<i class="fa-solid fa-magnifying-glass main-body-btn-ui-bar-custom-open"></i>
<i class="fa-solid fa-home hide main-body-btn-ui-bar-custom-close"></i>
</div>
</div>
</div>
`,
);
s(`.main-body-btn-menu`).onclick = () => {
Modal.actionBtnCenter();
};
s(`.main-body-btn-bar-custom`).onclick = () => {
if (s(`.main-body-btn-ui-close`).classList.contains('hide')) {
s(`.main-body-btn-ui`).click();
}
if (s(`.main-body-btn-ui-bar-custom-open`).classList.contains('hide')) {
s(`.main-body-btn-ui-bar-custom-open`).classList.remove('hide');
s(`.main-body-btn-ui-bar-custom-close`).classList.add('hide');
s(`.slide-menu-top-bar-fix`).style.top = '0px';
} else {
s(`.main-body-btn-ui-bar-custom-open`).classList.add('hide');
s(`.main-body-btn-ui-bar-custom-close`).classList.remove('hide');
s(`.slide-menu-top-bar-fix`).style.top = '-100px';
s(`.top-bar-search-box-container`).click();
}
if (Modal.mobileModal()) {
btnCloseEvent();
}
};
let _heightTopBar, _heightBottomBar, _topMenu;
s(`.main-body-btn-ui`).onclick = () => {
if (s(`.main-body-btn-ui-open`).classList.contains('hide')) {
s(`.main-body-btn-ui-open`).classList.remove('hide');
s(`.main-body-btn-ui-close`).classList.add('hide');
_heightTopBar = newInstance(options.heightTopBar);
_heightBottomBar = newInstance(options.heightBottomBar);
_topMenu = newInstance(s(`.modal-menu`).style.top);
options.heightTopBar = 0;
options.heightBottomBar = 0;
s(`.slide-menu-top-bar`).classList.add('hide');
s(`.bottom-bar`).classList.add('hide');
s(`.modal-menu`).style.top = '0px';
s(`.main-body-btn-container`).style.top = '50px';
s(`.main-body`).style.top = '0px';
s(`.main-body`).style.height = `${windowGetH()}px`;
for (const event of Object.keys(Modal.Data[idModal].onBarUiClose))
Modal.Data[idModal].onBarUiClose[event]();
} else {
s(`.main-body-btn-ui-close`).classList.remove('hide');
s(`.main-body-btn-ui-open`).classList.add('hide');
options.heightTopBar = _heightTopBar;
options.heightBottomBar = _heightBottomBar;
s(`.modal-menu`).style.top = _topMenu;
s(`.main-body-btn-container`).style.top = `${options.heightTopBar + 50}px`;
s(`.slide-menu-top-bar`).classList.remove('hide');
s(`.bottom-bar`).classList.remove('hide');
s(`.main-body`).style.top = `${options.heightTopBar}px`;
s(`.main-body`).style.height = `${windowGetH() - options.heightTopBar}px`;
for (const event of Object.keys(Modal.Data[idModal].onBarUiOpen))
Modal.Data[idModal].onBarUiOpen[event]();
}
Responsive.triggerChanged(`slide-menu-modal-menu`);
Object.keys(this.Data).map((_idModal) => {
if (this.Data[_idModal].slideMenu) {
if (s(`.btn-maximize-${_idModal}`)) s(`.btn-maximize-${_idModal}`).click();
}
});
Responsive.triggerChanged(`view-${'main-body'}`);
if (Responsive.hasChangedListener(`view-${'bottom-bar'}`))
Responsive.triggerChanged(`view-${'bottom-bar'}`);
if (Responsive.hasChangedListener(`view-${'main-body-top'}`))
Responsive.triggerChanged(`view-${'main-body-top'}`);
for (const keyEvent of Object.keys(this.Data[idModal].onExpandUiListener)) {
this.Data[idModal].onExpandUiListener[keyEvent](
!s(`.main-body-btn-ui-open`).classList.contains('hide'),
);
}
};
Modal.setTopBannerLink();
});
const inputSearchBoxId = `top-bar-search-box`;
append(
'body',
html` <div class="fix modal slide-menu-top-bar">
<div
class="fl top-bar ${options.barClass ? options.barClass : ''}"
style="height: ${originHeightTopBar}px;"
>
${await BtnIcon.instance({
style: `height: 100%`,
class: 'in fll main-btn-menu action-bar-box action-btn-close hide',
label: html` <div class="${contentIconClass} action-btn-close-render">
<i class="fa-solid fa-xmark"></i>
</div>`,
})}
${await BtnIcon.instance({
style: `height: 100%`,
class: `in fll main-btn-menu action-bar-box action-btn-app-icon ${
options?.disableTools?.includes('app-icon') ? 'hide' : ''
}`,
label: html` <div class="${contentIconClass} action-btn-app-icon-render"></div>`,
})}
<form
class="in fll top-bar-search-box-container hover ${options?.disableTools?.includes('text-box')
? 'hide'
: ''}"
>
${await Input.instance({
id: inputSearchBoxId,
autocomplete: 'off',
placeholder: Modal.mobileModal()
? Translate.instance('search', '.top-bar-search-box')
: undefined, // html`<i class="fa-solid fa-magnifying-glass"></i> ${Translate.instance('search')}`,
placeholderIcon: html`<div
class="in fll"
style="width: ${originHeightTopBar}px; height: ${originHeightTopBar}px;"
>
<div class="abs center"><i class="fa-solid fa-magnifying-glass"></i></div>
${!Modal.mobileModal()
? html` <div
class="inl wfm key-shortcut-container-info"
style="${renderCssAttr({ style: { top: '10px', left: '60px' } })}"
>
${await Badge.instance({
id: 'shortcut-key-info-search',
text: 'Shift',
classList: 'inl',
style: { 'z-index': 1 },
})}
${await Badge.instance({
id: 'shortcut-key-info-search',
text: '+',
classList: 'inl',
style: { 'z-index': 1, background: 'none', color: '#5f5f5f' },
})}
${await Badge.instance({
id: 'shortcut-key-info-search',
text: 'k',
classList: 'inl',
style: { 'z-index': 1 },
})}
</div>`
: ''}
</div>`,
inputClass: 'in fll',
// containerClass: '',
})}
</form>
<div
class="abs top-box-profile-container ${options?.disableTools?.includes('profile') ? 'hide' : ''}"
>
${await BtnIcon.instance({
style: `height: 100%`,
class: 'in fll session-in-log-in main-btn-menu action-bar-box action-btn-profile-log-in',
label: html` <div class="${contentIconClass} action-btn-profile-log-in-render"></div>`,
})}
${await BtnIcon.instance({
style: `height: 100%`,
class: 'in fll session-in-log-out main-btn-menu action-bar-box action-btn-profile-log-out',
label: html` <div class="${contentIconClass} action-btn-profile-log-out-render">
<i class="fas fa-user-plus"></i>
</div>`,
})}
</div>
</div>
${options?.slideMenuTopBarBannerFix
? html`<div
class="abs modal slide-menu-top-bar-fix"
style="height: ${options.heightTopBar}px; top: 0px"
>
<a class="a-link-top-banner fl">
<div class="inl">${await options.slideMenuTopBarBannerFix()}</div></a
>
</div>`
: ''}
${idModal === 'modal-menu' && options.mode !== 'slide-menu-right'
? html`<div
class="abs main-btn-menu-top-container"
style="bottom: 0px; left: 0px; z-index: 10; height: ${originHeightTopBar}px; width: ${originHeightTopBar}px"
>
${await BtnIcon.instance({
style: `height: 100%`,
class: `in fll main-btn-menu-top action-bar-box action-btn-center-top`,
label: html`<div class="abs center">
<i class="far fa-square btn-bar-center-icon-square hide"></i>
<span class="btn-bar-center-icon-close hide">${barConfig.buttons.close.label}</span>
<span class="btn-bar-center-icon-menu">${barConfig.buttons.menu.label}</span>
</div>`,
})}
</div>
<style>
.a-link-top-banner {
padding-left: 35px;
}
.main-body-btn-bar-custom {
top: 50px !important;
}
.main-body-btn-menu {
display: none;
}
</style>`
: ''}
</div>`,
);
EventsUI.onClick(`.action-btn-profile-log-in`, () => {
if (Modal.mobileModal() && s(`.btn-close-search-box-history`)) {
s(`.btn-close-search-box-history`).click();
}
s(`.main-btn-account`).click();
});
EventsUI.onClick(`.action-btn-profile-log-out`, () => {
if (Modal.mobileModal() && s(`.btn-close-search-box-history`)) {
s(`.btn-close-search-box-history`).click();
}
s(`.main-btn-sign-up`).click();
});
if (idModal === 'modal-menu' && options.mode !== 'slide-menu-right') {
EventsUI.onClick(`.action-btn-center-top`, (e) => {
e.preventDefault();
Modal.actionBtnCenter();
});
}
s(`.input-info-${inputSearchBoxId}`).style.textAlign = 'left';
htmls(`.input-info-${inputSearchBoxId}`, '');
const inputInfoNode = s(`.input-info-${inputSearchBoxId}`).cloneNode(true);
s(`.input-info-${inputSearchBoxId}`).remove();
{
// Inject SearchBox base styles
SearchBox.injectStyles();
const id = 'search-box-history';
const searchBoxHistoryId = id;
const formDataInfoNode = [
{
model: 'search-box',
id: inputSearchBoxId,
rules: [] /*{ type: 'isEmpty' }, { type: 'isEmail' }*/,
},
];
// Reusable hover/focus controller for search history panel
let unbindDocSearch = null;
const hoverFocusCtl = EventsUI.HoverFocusController({
inputSelector: `.top-bar-search-box-container`,
panelSelector: `.${id}`,
activeElementId: inputSearchBoxId,
onDismiss: () => dismissSearchBox(),
});
let currentKeyBoardSearchBoxIndex = 0;
let results = [];
const checkHistoryBoxTitleStatus = () => {
if (s(`.search-box-result-title`) && s(`.search-box-result-title`).classList) {
if (!s(`.${inputSearchBoxId}`).value.trim()) {
s(`.search-box-result-title`).classList.add('hide');
s(`.search-box-recent-title`).classList.remove('hide');
} else {
s(`.search-box-recent-title`).classList.add('hide');
s(`.search-box-result-title`).classList.remove('hide');
}
}
};
const checkShortcutContainerInfoEnabled = () => {
if (Modal.mobileModal() || !s(`.key-shortcut-container-info`)) return;
if (!s(`.${inputSearchBoxId}`).value) {
s(`.key-shortcut-container-info`).classList.remove('hide');
} else s(`.key-shortcut-container-info`).classList.add('hide');
};
const renderSearchResult = async (results, isRecentHistory = false) => {
// Check if the search history modal still exists before rendering
if (!s(`.html-${searchBoxHistoryId}`)) return;
htmls(`.html-${searchBoxHistoryId}`, '');
// Show/hide clear-all button based on whether showing recent history
// Use setTimeout to ensure the button is in the DOM after modal renders
const updateClearAllBtn = () => {
const clearAllBtn = s(`.btn-search-history-clear-all`);
if (clearAllBtn) {
if (isRecentHistory && results.length > 0) {
clearAllBtn.style.display = 'flex';
} else {
clearAllBtn.style.display = 'none';
}
}
};
// Try immediately and also with a delay to handle timing
updateClearAllBtn();
setTimeout(updateClearAllBtn, 50);
if (results.length === 0) {
append(
`.html-${searchBoxHistoryId}`,
await BtnIcon.instance({
label: html`<i class="fas fa-exclamation-circle"></i> ${Translate.instance('no-result-found')}`,
class: `wfa`,
style: renderCssAttr({
style: {
padding: '3px',
margin: '2px',
'text-align': 'center',
border: 'none',
cursor: 'default',
background: 'none !important',
},
}),
}),
);
return;
}
// Use SearchBox component for rendering results
const searchContext = {
RouterInstance: Worker.RouterInstance,
options: options,
isRecentHistory: isRecentHistory, // Flag for delete button visibility
onResultClick: () => {
// Dismiss search box on result click
if (s(`.${searchBoxHistoryId}`)) {
Modal.removeModal(searchBoxHistoryId);
}
},
};
SearchBox.renderResults(results, `html-${searchBoxHistoryId}`, searchContext);
};
const getResultSearchBox = async (validatorData) => {
if (!s(`.html-${searchBoxHistoryId}`)) return;
const { model, id } = validatorData;
switch (model) {
case 'search-box':
{
currentKeyBoardSearchBoxIndex = 0;
results = [];
const query = s(`.${id}`) ? s(`.${id}`).value : '';
const trimmedQuery = query.trim();
// Use SearchBox component for extensible search
const searchContext = {
RouterInstance: Worker.RouterInstance,
options: options,
minQueryLength: options?.minSearchQueryLength || 1, // Allow single character search by default
onResultClick: () => {
// Dismiss search box on result click
if (s(`.${searchBoxHistoryId}`)) {
Modal.removeModal(searchBoxHistoryId);
}
},
};
// Check minimum query length (default: 1 character)
const minLength = searchContext.minQueryLength;
if (trimmedQuery.length >= minLength) {
results = await SearchBox.search(trimmedQuery, searchContext);
renderSearchResult(results, false); // Search results - no delete buttons
} else if (trimmedQuery.length === 0) {
// Show recent results from persistent history when query is empty
const recentResults = SearchBox.RecentResults.getAll();
renderSearchResult(recentResults, true); // Recent history - show delete buttons
} else {
// Query is too short - show nothing or a hint
renderSearchResult([], false);
}
}
break;
default:
break;
}
};
const searchBoxCallBack = async (validatorData) => {
const isSearchBoxActiveElement = isActiveElement(inputSearchBoxId);
checkHistoryBoxTitleStatus();
checkShortcutContainerInfoEnabled();
if (!isSearchBoxActiveElement && !hoverFocusCtl.shouldStay()) {
Modal.removeModal(searchBoxHistoryId);
return;
}
setTimeout(() => {
getResultSearchBox(validatorData);
if (
s(`.slide-menu-top-bar-fix`) &&
!s(`.main-body-btn-ui-bar-custom-open`).classList.contains('hide')
) {
s(`.main-body-btn-bar-custom`).click();
}
});
};
const updateSearchBoxValue = (selector) => {
if (!selector) {
// Get the currently active search result item
const activeItem = s(`.html-${searchBoxHistoryId} .search-result-item.active-search-result`);
if (activeItem) {
const titleEl = activeItem.querySelector('.search-result-title');
if (titleEl && titleEl.textContent) {
s(`.${inputSearchBoxId}`).value = titleEl.textContent.trim();
}
}
} else if (s(selector)) {
const titleEl = s(selector).querySelector('.search-result-title');
if (titleEl && titleEl.textContent) {
s(`.${inputSearchBoxId}`).value = titleEl.textContent.trim();
}
}
checkHistoryBoxTitleStatus();
checkShortcutContainerInfoEnabled();
};
const setSearchValue = (selector) => {
// Get all search result items
const allItems = sa(`.html-${searchBoxHistoryId} .search-result-item`);
if (!allItems || allItems.length === 0) return;
const activeItem = allItems[currentKeyBoardSearchBoxIndex];
if (!activeItem) return;
const resultId = activeItem.getAttribute('data-result-id');
const resultType = activeItem.getAttribute('data-result-type');
if (resultType === 'route' && results[currentKeyBoardSearchBoxIndex]) {
const result = results[currentKeyBoardSearchBoxIndex];
// Track in persistent history
SearchBox.RecentResults.add(result);
updateSearchBoxValue();
if (s(`.main-btn-${resultId}`)) {
s(`.main-btn-${resultId}`).click();
}
} else {
// Track custom provider result in persistent history
if (results[currentKeyBoardSearchBoxIndex]) {
SearchBox.RecentResults.add(results[currentKeyBoardSearchBoxIndex]);
}
// Trigger click on custom result
activeItem.click();
}
Modal.removeModal(searchBoxHistoryId);
};
let boxHistoryDelayRender = 0;
const searchBoxHistoryOpen = async () => {
if (boxHistoryDelayRender) return;
if (Modal.mobileModal()) {
btnCloseEvent();
}
boxHistoryDelayRender = 1000;
setTimeout(() => (boxHistoryDelayRender = 0));
if (!s(`.${searchBoxHistoryId}`)) {
const { barConfig } = await Themes[Css.currentTheme]();
barConfig.buttons.maximize.disabled = true;
barConfig.buttons.minimize.disabled = true;
barConfig.buttons.restore.disabled = true;
barConfig.buttons.menu.disabled = true;
barConfig.buttons.close.disabled = false;
await Modal.instance({
id: searchBoxHistoryId,
barConfig,
title: html`<div
style="display: flex; align-items: center; justify-content: space-between; width: 100%;"
>
<div style="display: flex; align-items: center; gap: 8px;">
<div class="search-box-recent-title">
${renderViewTitle({
icon: html`<i class="fas fa-history mini-title"></i>`,
text: Translate.instance('recent'),
})}
</div>
<div class="search-box-result-title hide">
${renderViewTitle({
icon: html`<i class="far fa-list-alt mini-title"></i>`,
text: Translate.instance('results'),
})}
</div>
</div>
</div>`,
html: () => html``,
titleClass: 'mini-title',
style: {
resize: 'none',
'max-width': '450px',
height:
this.mobileModal() && windowGetW() < 445
? `${windowGetH() - originHeightTopBar}px !important`
: '300px !important',
'z-index': 7,
},
class: 'search-history-modal',
dragDisabled: true,
maximize: true,
barMode: options.barMode,
});
// Bind hover/focus and click-outside to dismiss
hoverFocusCtl.bind();
unbindDocSearch = EventsUI.bindDismissOnDocumentClick({
shouldStay: hoverFocusCtl.shouldStay,
onDismiss: () => dismissSearchBox(),
anchors: [`.top-bar-search-box-container`, `.${id}`],
});
// Ensure cleanup when modal closes
Modal.Data[id].onCloseListener[`unbind-doc-${id}`] = () => unbindDocSearch && unbindDocSearch();
Modal.MoveTitleToBar(id);
// Add styles for inline button layout in the search history modal bar
const styleId = 'search-history-modal-bar-styles';
if (!s(`#${styleId}`)) {
const styleTag = document.createElement('style');
styleTag.id = styleId;
styleTag.textContent = `
.search-history-modal .btn-bar-modal-container .bar-default-modal {
display: flex;
flex-direction: row-reverse;
align-items: center;
}
.search-history-modal .btn-bar-modal-container .btn-modal-default {
display: inline-flex;
align-items: center;
justify-content: center;
float: none;
}
.search-history-modal .html-${searchBoxHistoryId} {
overflow-x: hidden;
box-sizing: border-box;
}
.search-history-modal .html-${searchBoxHistoryId} .search-result-item,
.search-history-modal .html-${searchBoxHistoryId} .search-result-history-item {
box-sizing: border-box;
max-width: 100%;
}
`;
document.head.appendChild(styleTag);
}
// Add clear all button to the bar area, before the close button
const clearAllBtnHtml = await BtnIcon.instance({
class: `btn-search-history-clear-all btn-modal-default btn-modal-default-${searchBoxHistoryId}`,
label: html`<i class="fas fa-trash-alt"></i>`,
attrs: `title="Clear all recent items"`,
style: 'padding: 4px 8px; font-size: 12px; display: none;',
});
// Insert before close button in the bar (with flex row-reverse, inserting after close button places our button to its left visually)
const closeBtn = s(`.btn-close-${searchBoxHistoryId}`);
if (closeBtn) {
closeBtn.insertAdjacentHTML('afterend', clearAllBtnHtml);
}
// Add click handler for clear all history button
const clearAllBtn = s(`.btn-search-history-clear-all`);
if (clearAllBtn) {
clearAllBtn.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
// Clear all history from persistent storage
SearchBox.RecentResults.clear();
// Re-render to show empty history (with isRecentHistory true to hide button)
renderSearchResult([], true);
};
}
prepend(`.btn-bar-modal-container-${id}`, html`<div class="hide">${inputInfoNode.outerHTML}</div>`);
}
};
s('.top-bar-search-box').oninput = () => {
searchBoxHistoryOpen();
searchBoxCallBack(formDataInfoNode[0]);
};
s('.top-bar-search-box').onfocus = () => {
searchBoxHistoryOpen();
searchBoxCallBack(formDataInfoNode[0]);
};
const dismissSearchBox = () => {
if (unbindDocSearch) {
try {
unbindDocSearch();
} catch (e) {}
}
Modal.removeModal(searchBoxHistoryId);
};
s('.top-bar-search-box').onblur = () => {
hoverFocusCtl.checkDismiss();
};
EventsUI.onClick(`.top-bar-search-box-container`, () => {
if (SearchBox.Data.skipOpen) {
SearchBox.Data.skipOpen = false;
return;
}
searchBoxHistoryOpen();
searchBoxCallBack(formDataInfoNode[0]);
const inputEl = s(`.${inputSearchBoxId}`);
if (inputEl && inputEl.focus) inputEl.focus();
});
const timePressDelay = 100;
Keyboard.instanceMultiPressKey({
id: 'input-search-shortcut-ArrowUp',
keys: ['ArrowUp'],
eventCallBack: () => {
if (s(`.${id}`)) {
const allItems = sa(`.html-${searchBoxHistoryId} .search-result-item`);
if (!allItems || allItems.length === 0) return;
// Remove active class from current
if (allItems[currentKeyBoardSearchBoxIndex]) {
allItems[currentKeyBoardSearchBoxIndex].classList.remove('active-search-result');
}
// Navigate up
if (currentKeyBoardSearchBoxIndex > 0) {
currentKeyBoardSearchBoxIndex--;
} else {
currentKeyBoardSearchBoxIndex = allItems.length - 1;
}
// Add active class to new and ensure visibility
if (allItems[currentKeyBoardSearchBoxIndex]) {
allItems[currentKeyBoardSearchBoxIndex].classList.add('active-search-result');
// Use optimized scroll method to ensure item is always visible
const container = s(`.html-${searchBoxHistoryId}`);
if (container) {
SearchBox.scrollIntoViewIfNeeded(allItems[currentKeyBoardSearchBoxIndex], container);
}
}
updateSearchBoxValue();
}
},
timePressDelay,
});
Keyboard.instanceMultiPressKey({
id: 'input-search-shortcut-ArrowDown',
keys: ['ArrowDown'],
eventCallBack: () => {
if (s(`.${id}`)) {
const allItems = sa(`.html-${searchBoxHistoryId} .search-result-item`);
if (!allItems || allItems.length === 0) return;
// Remove active class from current
if (allItems[currentKeyBoardSearchBoxIndex]) {
allItems[currentKeyBoardSearchBoxIndex].classList.remove('active-search-result');
}
// Navigate down
if (currentKeyBoardSearchBoxIndex < allItems.length - 1) {
currentKeyBoardSearchBoxIndex++;
} else {
currentKeyBoardSearchBoxIndex = 0;
}
// Add active class to new and ensure visibility
if (allItems[currentKeyBoardSearchBoxIndex]) {
allItems[currentKeyBoardSearchBoxIndex].classList.add('active-search-result');
// Use optimized scroll method to ensure item is always visible
const container = s(`.html-${searchBoxHistoryId}`);
if (container) {
SearchBox.scrollIntoViewIfNeeded(allItems[currentKeyBoardSearchBoxIndex], container);
}
}
updateSearchBoxValue();
}
},
timePressDelay,
});
s(`.top-bar-search-box-container`).onsubmit = (e) => {
e.preventDefault();
setSearchValue();
};
}
setTimeout(async () => {
// clone and change position
// s(`.btn-close-${idModal}`);
// s(`.btn-menu-${idModal}`);
if (originHeightBottomBar === 0) {
const btnCloseNode = s(`.btn-close-${idModal}`).cloneNode(true);
s(`.btn-close-${idModal}`).remove();
s(`.top-bar`).appendChild(btnCloseNode);
s(`.btn-close-${idModal}`).onclick = btnCloseEvent;
const btnMenuNode = s(`.btn-menu-${idModal}`).cloneNode(true);
s(`.btn-menu-${idModal}`).remove();
s(`.top-bar`).appendChild(btnMenuNode);
s(`.btn-menu-${idModal}`).onclick = btnMenuEvent;
}
// const titleNode = s(`.title-modal-${idModal}`).cloneNode(true);
// s(`.title-modal-${idModal}`).remove();
// s(`.top-bar`).appendChild(titleNode);
s(`.slide-menu-top-bar`).style.zIndex = 7;
// s('body').removeChild(`.${idModal}`);
// while (s(`.top-modal`).firstChild) s(`.top-modal`).removeChild(s(`.top-modal`).firstChild);
{
const { barConfig } = await Themes[Css.currentTheme]();
barConfig.buttons.maximize.disabled = true;
barConfig.buttons.minimize.disabled = true;
barConfig.buttons.restore.disabled = true;
barConfig.buttons.menu.disabled = true;
barConfig.buttons.close.disabled = true;
const id = 'main-body';
await Modal.instance({
id,
barConfig,
html: options.htmlMainBody ? options.htmlMainBody : () => html``,
titleClass: 'hide',
style: {
// overflow: 'hidden',
background: 'none',
resize: 'none',
'min-width': `${minWidth}px`,
width: '100%',
// border: '3px solid red',
},
dragDisabled: true,
maximize: true,
slideMenu: 'modal-menu',
barMode: options.barMode,
observer: true,
disableBoxShadow: true,
});
const maxWidthInputSearchBox = 450;
const paddingInputSearchBox = 5;
const paddingRightSearchBox = 50;
Responsive.onChanged(
() => {
if (!this.Data[id] || !s(`.${id}`)) return Responsive.offChanged(`view-${id}`);
const widthInputSearchBox =
windowGetW() > maxWidthInputSearchBox ? maxWidthInputSearchBox : windowGetW();
s(`.top-bar-search-box-container`).style.width = `${
widthInputSearchBox - originHeightTopBar - paddingRightSearchBox - 1
}px`;
s(`.top-bar-search-box`).style.width = `${
widthInputSearchBox -
originHeightTopBar * 2 -
paddingRightSearchBox -
paddingInputSearchBox * 2 /*padding input*/ -
10 /* right-margin */
}px`;
s(`.top-bar-search-box`).style.top = `${
(originHeightTopBar - s(`.top-bar-search-box`).clientHeight) / 2
}px`;
if (this.Data[id].slideMenu) s(`.${id}`).style.height = `${Modal.Data[id].getHeight()}px`;
},
{ key: `view-${id}` },
);
Responsive.triggerChanged(`view-${id}`);
Keyboard.instanceMultiPressKey({
id: 'input-search-shortcut-k',
keys: [
['Shift', 'k'],
['Alt', 'k'],
],
eventCallBack: () => {
if (isTextInputFocused()) return;
if (s(`.top-bar-search-box`)) {
if (s(`.main-body-btn-ui-close`).classList.contains('hide')) {
s(`.main-body-btn-ui-open`).click();
}
s(`.top-bar-search-box`).blur();
s(`.top-bar-search-box`).focus();
s(`.top-bar-search-box`).select();
}
},
});
}
{
const { barConfig } = await Themes[Css.currentTheme]();
barConfig.buttons.maximize.disabled = true;
barConfig.buttons.minimize.disabled = true;
barConfig.buttons.restore.disabled = true;
barConfig.buttons.menu.disabled = true;
barConfig.buttons.close.disabled = true;
const id = 'bottom-bar';
if (!this.Data[idModal].homeModals) this.Data[idModal].homeModals = [];
if (!this.Data[idModal].homeModals.includes(id)) {
this.Data[idModal].homeModals.push(id);
}
const html = async () => html`
<style>
.top-bar-search-box-container {
height: 100%;
overflow: hidden;
}
.bottom-bar {
overflow: hidden;
}
.action-bar-box {
margin: 0px;
border: none;
width: 50px;
min-height: 50px;
}
</style>
<div
class="fl ${options.barClass ? options.barClass : ''}"
style="height: ${originHeightBottomBar}px;"
>
${await BtnIcon.instance({
style: `height: 100%`,
class: `in fl${
options.mode === 'slide-menu-right' ? 'r' : 'l'
} main-btn-menu action-bar-box action-btn-center ${
options?.disableTools?.includes('center') ? 'hide' : ''
}`,
label: html`
<div class="${contentIconClass}">
<i class="far fa-square btn-bar-center-icon-square hide"></i>
<span class="btn-bar-center-icon-close hide">${barConfig.buttons.close.label}</span>
<span class="btn-bar-center-icon-menu">${barConfig.buttons.menu.label}</span>
</div>
`,
})}
${await BtnIcon.instance({
style: `height: 100%`,
class: `in flr main-btn-menu action-bar-box action-btn-lang ${
options?.disableTools?.includes('lang') ? 'hide' : ''
}`,
label: html` <div class="${contentIconClass} action-btn-lang-render"></div>`,
})}
${await BtnIcon.instance({
style: `height: 100%`,
class: `in flr main-btn-menu action-bar-box action-btn-theme ${
options?.disableTools?.includes('theme') ? 'hide' : ''
}`,
label: html` <div class="${contentIconClass} action-btn-theme-render"></div>`,
})}
${await BtnIcon.instance({
style: `height: 100%`,
class: `in flr main-btn-menu action-bar-box action-btn-home ${
options?.disableTools?.includes('navigator') ? 'hide' : ''
}`,
label: html` <div class="${contentIconClass}"><i class="fas fa-home"></i></div>`,
})}
${await BtnIcon.instance({
style: `height: 100%`,
class: `in flr main-btn-menu action-bar-box action-btn-right ${
options?.disableTools?.includes('navigator') ? 'hide' : ''
}`,
label: html` <div class="${contentIconClass}"><i class="fas fa-chevron-right"></i></div>`,
})}
${await BtnIcon.instance({
style: `height: 100%`,
class: `in flr main-btn-menu action-bar-box action-btn-left ${
options?.disableTools?.includes('navigator') ? 'hide' : ''
}`,
label: html`<div class="${contentIconClass}"><i class="fas fa-chevron-left"></i></div>`,
})}
</div>
`;
if (options.heightBottomBar === 0 && options.heightTopBar > 0) {
append(`.slide-menu-top-bar`, html` <div class="in ${id}">${await html()}</div>`);
} else {
await Modal.instance({
id,
barConfig,
html,
titleClass: 'hide',
style: {
resize: 'none',
height: `${options.heightBottomBar}px`,
'min-width': `${minWidth}px`,
'z-index': 7,
// bottom: '0px !important',
width: `${windowGetW()}px`,
top: `${Modal.Data['modal-menu'].getTop()}px`,
},
dragDisabled: true,
disableCenter: true,
// maximize: true,
barMode: options.barMode,
});
Responsive.onChanged(
() => {
if (!this.Data[id] || !s(`.${id}`)) return Responsive.offChanged(`view-${id}`);
// <div class="in fll right-offset-menu-bottom-bar" style="height: 100%"></div>
// s(`.right-offset-menu-bottom-bar`).style.width = `${windowGetW() - slideMenuWidth}px`;
s(`.${id}`).style.top = `${Modal.Data['modal-menu'].getTop()}px`;
s(`.${id}`).style.width = `${windowGetW()}px`;
},
{ key: `view-${id}` },
);
// Responsive.Event[`view-${id}`]();
}
EventsUI.onClick(`.action-btn-left`, (e) => {
e.preventDefault();
window.history.back();
});
EventsUI.onClick(`.action-btn-center`, (e) => {
e.preventDefault();
Modal.actionBtnCenter();
});
EventsUI.onClick(`.action-btn-right`, (e) => {
e.preventDefault();
window.history.forward();
});
EventsUI.onClick(`.action-btn-home`, async () => {
await Modal.onHomeRouterEvent();
subMenuHandler(Object.keys(options.RouterInstance.Routes()));
Object.keys(this.Data[idModal].onHome).map((keyListener) => this.Data[idModal].onHome[keyListener]());
});
EventsUI.onClick(`.action-btn-app-icon`, () => s(`.action-btn-home`).click());
Keyboard.instanceMultiPressKey({
id: 'input-shortcut-global-escape',
keys: ['Escape'],
eventCallBack: () => {
if (s(`.btn-close-${this.currentTopModalId}`)) s(`.btn-close-${this.currentTopModalId}`).click();
},
});
}
{
ThemeEvents['action-btn-theme'] = async () => {
htmls(
`.action-btn-theme-render`,
html` ${darkTheme ? html` <i class="fas fa-moon"></i>` : html`<i class="far fa-sun"></i>`}`,
);
if (s(`.slide-menu-top-bar-fix`)) {
htmls(
`.slide-menu-top-bar-fix`,
html`<a class="a-link-top-banner fl">${await options.slideMenuTopBarBannerFix()}</a>`,
);
Modal.setTopBannerLink();
}
};
ThemeEvents['action-btn-theme']();
EventsUI.onClick(`.action-btn-theme`, async () => {
const themePair = ThemesScope.find((t) => t.theme === Css.currentTheme).themePair;
const theme = themePair ? themePair : ThemesScope.find((t) => t.dark === !darkTheme).theme;
if (s(`.dropdown-option-${theme}`))
DropDown.Tokens['settings-theme'].onClickEvents[`dropdown-option-${theme}`]();
else await Themes[theme]();
});
if (!(ThemesScope.find((t) => t.dark) && ThemesScope.find((t) => !t.dark))) {
s(`.action-btn-theme`).classList.add('hide');
}
}
{
htmls(`.action-btn-lang-render`, html` ${s('html').lang}`);
// old method
// EventsUI.onClick(`.action-btn-lang`, () => {
// let lang = 'en';
// if (s('html').lang === 'en') lang = 'es';
// if (s(`.dropdown-option-${lang}`))
// DropDown.Tokens['settings-lang'].onClickEvents[`dropdown-option-${lang}`]();
// else Translate.renderLang(lang);
// });
// Open lightweight empty modal on language button, with shared dismiss logic
EventsUI.onClick(
`.action-btn-lang`,
async () => {
const id = 'action-btn-lang-modal';
if (s(`.${id}`)) {
return s(`.btn-close-${id}`).click();
}
const { barConfig } = await Themes[Css.currentTheme]();
barConfig.buttons.maximize.disabled = true;
barConfig.buttons.minimize.disabled = true;
barConfig.buttons.restore.disabled = true;
barConfig.buttons.menu.disabled = true;
barConfig.buttons.close.disabled = false;
await Modal.instance({
id,
barConfig,
title: html`${renderViewTitle({
icon: html`<i class="fas fa-language mini-title"></i>`,
text: Translate.instance('select lang'),
})}`,
html: async () => html`${await Translate.RenderSetting('action-drop-modal' + id)}`,
titleClass: 'mini-title',
style: {
resize: 'none',
width: '100% !important',
height: '350px !important',
'max-width': '350px !important',
'z-index': 7,
},
dragDisabled: true,
maximize: true,
barMode: options.barMode,
});
// Move title inside the bar container to align with control buttons
Modal.MoveTitleToBar(id);
// Position the language selection modal relative to the language button
Modal.positionRelativeToAnchor({
modalSelector: `.${id}`,
anchorSelector: '.action-btn-lang',
align: 'right',
offset: { x: 0, y: 6 },
autoVertical: true,
});
// Hover/focus controller uses the button as input anchor
const hoverFocusCtl = EventsUI.HoverFocusController({
inputSelector: `.action-btn-lang`,
panelSelector: `.${id}`,
onDismiss: () => Modal.removeModal(id),
});
hoverFocusCtl.bind();
const unbindDoc = EventsUI.bindDismissOnDocumentClick({
shouldStay: hoverFocusCtl.shouldStay,
onDismiss: () => Modal.removeModal(id),
anchors: [`.action-btn-lang`, `.${id}`],
});
Modal.Data[id].onCloseListener[`unbind-doc-${id}`] = () => unbindDoc();
},
{ context: 'modal', noGate: true, noLoading: true },
);
}
{
const { barConfig } = await Themes[Css.currentTheme]();
barConfig.buttons.maximize.disabled = true;
barConfig.buttons.minimize.disabled = true;
barConfig.buttons.restore.disabled = true;
barConfig.buttons.menu.disabled = true;
barConfig.buttons.close.disabled = true;
const id = 'main-body-top';
await Modal.instance({
id,
barConfig,
html: () => html``,
titleClass: 'hide',
class: 'hide',
style: {
// overflow: 'hidden',
resize: 'none',
'min-width': `${minWidth}px`,
'z-index': 5,
background: `rgba(61, 61, 61, 0.5)`,
// border: '3px solid red',
},
dragDisabled: true,
maximize: true,
barMode: options.barMode,
});
Responsive.onChanged(
() => {
if (!this.Data[id] || !s(`.${id}`)) return Responsive.offChanged(`view-${id}`);
s(`.${id}`).style.height =
s(`.main-body-btn-ui-close`).classList.contains('hide') &&
s(`.btn-restore-${id}`).style.display !== 'none'
? `${windowGetH()}px`
: `${Modal.Data[id].getHeight()}px`;
if (
s(`.main-body-btn-ui-close`).classList.contains('hide') &&
s(`.btn-restore-${id}`).style.display !== 'none'
) {
s(`.${id}`).style.top = '0px';
} else {
s(`.${id}`).style.top = `${options.heightTopBar ? options.heightTopBar : heightDefaultTopBar}px`;
}
},
{ key: `view-${id}` },
);
Responsive.triggerChanged(`view-${id}`);
s(`.main-body-top`).onclick = () => s(`.btn-close-modal-menu`).click();
}
await NotificationManager.RenderBoard(options);
const { removeEvent } = Scroll.setEvent('.main-body', async (payload) => {
// console.warn('scroll', payload);
if (payload.scrollTop > 100) {
if (!s(`.main-body-btn-ui-close`).classList.contains('hide')) s(`.main-body-btn-ui-close`).click();
removeEvent();
}
});
setTimeout(window.onresize);
setRouterReady();
});
})();
break;
case 'dropNotification':
(() => {
setTimeout(() => {
s(`.${idModal}`).style.top = 'auto';
s(`.${idModal}`).style.left = 'auto';
s(`.${idModal}`).style.height = 'auto';
s(`.${idModal}`).style.position = 'absolute';
let countDrop = 0;
Object.keys(this.Data)
.reverse()
.map((_idModal) => {
if (this.Data[_idModal][options.mode]) {
s(`.${_idModal}`).style.bottom = `${countDrop * s(`.${_idModal}`).clientHeight * 1.05}px`;
countDrop++;
}
});
});
})();
break;
default:
break;
}
}
if (options.zIndexSync) this.zIndexSync({ idModal });
if (s(`.${idModal}`)) {
for (const event of Object.keys(Modal.Data[idModal].onReloadModalListener))
await Modal.Data[idModal].onReloadModalListener[event]();
s(`.btn-maximize-${idModal}`).click();
return;
}
if (idModal !== 'main-body' && options.mode !== 'view' && !options.disableCenter) Modal.Data[idModal].center();
const render = html` <style class="style-${idModal}">
.${idModal} {
width: ${width}px;
height: ${height}px;
top: ${top};
left: ${left};
overflow: auto; /* resizable required */
resize: both; /* resizable required */
transition: ${transition};
opacity: 0;
z-index: 1;
}
.bar-default-modal-${idModal} {
top: 0px;
left: 0px;
z-index: 3;
}
.modal-html-${idModal} {
}
.btn-modal-default-${idModal} {
}
.modal-handle-${idModal} {
width: 90%;
height: 90%;
top: 5%;
left: 5%;
}
.sub-menu-title-container-${idModal},
.nav-path-container-${idModal} {
top: 0px;
left: 0px;
width: 200px;
height: 50px;
overflow: hidden;
font-size: 20px;
cursor: default;
}
.nav-path-display-${idModal} {
font-size: 11px;
width: 100%;
top: 35px;
left: 37px;
}
.nav-title-display-${idModal} {
font-size: 19px;
width: 100%;
top: 13px;
left: 13px;
}
</style>
${renderStyleTag(`style-${idModal}`, `.${idModal}`, options)}
<div
class="fix ${options && options.class ? options.class : ''} modal ${options.disableBoxShadow
? ''
: 'box-shadow'} ${idModal === 'main-body' ? `${idModal} modal-home` : idModal}"
>
<div class="abs modal-handle-${idModal}"></div>
<div class="in modal-html-${idModal}">
<div class="stq bar-default-modal bar-default-modal-${idModal}">
<div
class="fl btn-bar-modal-container btn-bar-modal-container-${idModal} ${options?.btnBarModalClass
? options.btnBarModalClass
: ''}"
>
<div class="btn-bar-modal-container-render-${idModal}"></div>
<div class="in flr bar-default-modal" style="z-index: 1">
${await BtnIcon.instance({
class: `btn-minimize-${idModal} btn-modal-default btn-modal-default-${idModal} ${
options?.btnContainerClass ? options.btnContainerClass : ''
} ${options?.barConfig?.buttons?.minimize?.disabled ? 'hide' : ''}`,
label: html`<div class="${options?.btnIconContainerClass ? options.btnIconContainerClass : ''}">
${options?.barConfig?.buttons?.minimize?.label ? options.barConfig.buttons.minimize.label : html`_`}
</div>`,
})}
${await BtnIcon.instance({
class: `btn-restore-${idModal} btn-modal-default btn-modal-default-${idModal} ${
options?.btnContainerClass ? options.btnContainerClass : ''
} ${options?.barConfig?.buttons?.restore?.disabled ? 'hide' : ''}`,
label: html`<div class="${options?.btnIconContainerClass ? options.btnIconContainerClass : ''}">
${options?.barConfig?.buttons?.restore?.label ? options.barConfig.buttons.restore.label : html`□`}
</div>`,
style: 'display: none',
})}
${await BtnIcon.instance({
class: `btn-maximize-${idModal} btn-modal-default btn-modal-default-${idModal} ${
options?.btnContainerClass ? options.btnContainerClass : ''
} ${options?.barConfig?.buttons?.maximize?.disabled ? 'hide' : ''}`,
label: html`<div class="${options?.btnIconContainerClass ? options.btnIconContainerClass : ''}">
${options?.barConfig?.buttons?.maximize?.label ? options.barConfig.buttons.maximize.label : html`▢`}
</div>`,
})}
${await BtnIcon.instance({
class: `btn-close-${idModal} btn-modal-default btn-modal-default-${idModal} ${
options?.btnContainerClass ? options.btnContainerClass : ''
} ${options?.barConfig?.buttons?.close?.disabled ? 'hide' : ''}`,
label: html`<div class="${options?.btnIconContainerClass ? options.btnIconContainerClass : ''}">
${options?.barConfig?.buttons?.close?.label ? options.barConfig.buttons.close.label : html`X`}
</div>`,
})}
${await BtnIcon.instance({
class: `btn-menu-${idModal} btn-modal-default btn-modal-default-${idModal} ${
options?.btnContainerClass ? options.btnContainerClass : ''
} ${options?.barConfig?.buttons?.menu?.disabled ? 'hide' : ''}`,
label: html`<div class="${options?.btnIconContainerClass ? options.btnIconContainerClass : ''}">
${options?.barConfig?.buttons?.menu?.label ? options.barConfig.buttons.menu.label : html`≡`}
</div>`,
})}
</div>
</div>
${options && options.status
? html` <div class="abs modal-icon-container">${renderStatus(options.status)}</div> `
: ''}
<div
class="inl title-modal-${idModal} ${options && options.titleClass
? options.titleClass
: 'title-main-modal'}"
>
${options && options.titleRender ? options.titleRender() : options.title ? options.title : ''}
</div>
</div>
<div class="in html-${idModal}">
${options.mode && options.mode.match('slide-menu')
? html`<div
class="stq modal"
style="${renderCssAttr({ style: { height: '50px', 'z-index': 1, top: '0px' } })}"
>
${await BtnIcon.instance({
style: renderCssAttr({ style: { height: '100%', color: '#5f5f5f' } }),
class: `in flr main-btn-menu action-bar-box btn-icon-menu-mode`,
label: html` <div class="abs center">
<i
class="fas fa-caret-right btn-icon-menu-mode-right ${options.mode && options.mode.match('right')
? ''
: 'hide'}"
></i
><i
class="fas fa-caret-left btn-icon-menu-mode-left ${options.mode && options.mode.match('right')
? 'hide'
: ''}"
></i>
</div>`,
})}
${await BtnIcon.instance({
style: renderCssAttr({ style: { height: '100%', color: '#5f5f5f' } }),
class: `in flr main-btn-menu action-bar-box btn-icon-menu-back hide`,
label: html`<div class="abs center"><i class="fas fa-undo-alt"></i></div>`,
})}
<div class="abs sub-menu-title-container-${idModal} ac">
<div class="abs nav-title-display-${idModal}">
<!-- <i class="fas fa-home"></i> ${Translate.instance('home')} -->
</div>
</div>
<div class="abs nav-path-container-${idModal} ahc bold">
<div class="abs nav-path-display-${idModal}"><!-- ${location.pathname} --></div>
</div>
</div>`
: ''}
${options && options.html ? (typeof options.html === 'function' ? await options.html() : options.html) : ''}
</div>
</div>
</div>`;
const selector = options && options.selector ? options.selector : 'body';
if (options) {
switch (options.renderType) {
case 'prepend':
prepend(selector, render);
break;
default:
append(selector, render);
break;
}
} else append(selector, render);
let handle = [s(`.bar-default-modal-${idModal}`), s(`.modal-handle-${idModal}`), s(`.modal-html-${idModal}`)];
if (options && 'handleType' in options) {
switch (options.handleType) {
case 'bar':
handle = [s(`.bar-default-modal-${idModal}`)];
break;
default:
break;
}
}
switch (options.mode) {
case 'slide-menu':
case 'slide-menu-right':
case 'slide-menu-left':
const backMenuButtonEvent = async () => {
// htmls(`.nav-title-display-${'modal-menu'}`, html`<i class="fas fa-home"></i> ${Translate.instance('home')}`);
htmls(`.nav-title-display-${'modal-menu'}`, html``);
htmls(`.nav-path-display-${idModal}`, '');
s(`.btn-icon-menu-back`).classList.add('hide');
// sa(`.main-btn-menu`).forEach((el) => {
// el.classList.remove('hide');
// });
};
s(`.main-btn-home`).onclick = async () => {
// await this.onHomeRouterEvent();
s(`.action-btn-home`).click();
};
EventsUI.onClick(`.btn-icon-menu-back`, backMenuButtonEvent);
EventsUI.onClick(`.btn-icon-menu-mode`, () => {
if (s(`.btn-icon-menu-mode-right`).classList.contains('hide')) {
s(`.btn-icon-menu-mode-right`).classList.remove('hide');
s(`.btn-icon-menu-mode-left`).classList.add('hide');
} else {
s(`.btn-icon-menu-mode-left`).classList.remove('hide');
s(`.btn-icon-menu-mode-right`).classList.add('hide');
}
if (slideMenuWidth === originSlideMenuWidth) {
slideMenuWidth = collapseSlideMenuWidth;
s(`.${idModal}`).style.width = `${slideMenuWidth}px`;
sa(`.menu-label-text`).forEach((el) => el.classList.add('hide'));
s(`.main-body-btn-container`).style[
true || (options.mode && options.mode.match('right')) ? 'right' : 'left'
] = options.mode && options.mode.match('right') ? `${slideMenuWidth}px` : '0px';
sa(`.handle-btn-container`).forEach((el) => el.classList.add('hide'));
if (options.onCollapseMenu) options.onCollapseMenu();
s(`.sub-menu-title-container-${'modal-menu'}`).classList.add('hide');
s(`.nav-path-container-${'modal-menu'}`).classList.add('hide');
// Shrink any already-open submenu containers to icon-only width
Object.keys(Modal.subMenuBtnClass).forEach((subMenuId) => {
const container = s(`.menu-btn-container-children-${subMenuId}`);
if (container && container.style.height && container.style.height !== '0px')
container.style.width = `${collapseSlideMenuWidth}px`;
});
Object.keys(this.Data[idModal].onCollapseMenuListener).map((keyListener) =>
this.Data[idModal].onCollapseMenuListener[keyListener](),
);
} else {
slideMenuWidth = originSlideMenuWidth;
s(`.${idModal}`).style.width = `${slideMenuWidth}px`;
sa(`.menu-label-text`).forEach((el) => el.classList.remove('hide'));
s(`.main-body-btn-container`).style[
true || (options.mode && options.mode.match('right')) ? 'right' : 'left'
] = options.mode && options.mode.match('right') ? `${slideMenuWidth}px` : '0px';
sa(`.handle-btn-container`).forEach((el) => el.classList.remove('hide'));
Modal.menuTextLabelAnimation(idModal);
if (options.onExtendMenu) options.onExtendMenu();
s(`.sub-menu-title-container-${'modal-menu'}`).classList.remove('hide');
s(`.nav-path-container-${'modal-menu'}`).classList.remove('hide');
// Expand any already-open submenu containers back to full width
Object.keys(Modal.subMenuBtnClass).forEach((subMenuId) => {
const container = s(`.menu-btn-container-children-${subMenuId}`);
if (container && container.style.height && container.style.height !== '0px')
container.style.width = `${originSlideMenuWidth}px`;
});
Object.keys(this.Data[idModal].onExtendMenuListener).map((keyListener) =>
this.Data[idModal].onExtendMenuListener[keyListener](),
);
}
Modal.Data[idModal][options.mode].width = slideMenuWidth;
Responsive.triggerChanged(`slide-menu-${idModal}`);
});
break;
default:
break;
}
// Track drag position for consistency
let dragPosition = { x: 0, y: 0 };
// Initialize drag options with proper bounds and smooth transitions
let dragOptions = {
handle: handle,
bounds: {
top: 0,
left: 0,
right: 0,
bottom: 0,
},
preventDefault: true,
position: { x: 0, y: 0 },
onDragStart: () => {
const modal = s(`.${idModal}`);
if (!modal) return false; // Prevent drag if modal not found
// Store current position
const computedStyle = window.getComputedStyle(modal);
const matrix = new DOMMatrixReadOnly(computedStyle.transform);
dragPosition = {
x: matrix.m41 || 0,
y: matrix.m42 || 0,
};
modal.style.transition = 'none';
modal.style.willChange = 'transform';
return true; // Allow drag to start
},
onDrag: (data) => {
// Update position based on drag delta
dragPosition = { x: data.x, y: data.y };
},
onDragEnd: () => {
const modal = s(`.${idModal}`);
if (!modal) return;
modal.style.willChange = '';
modal.style.transition = transition;
// Update drag instance with current position
if (dragInstance) {
dragInstance.updateOptions({
position: dragPosition,
});
}
// Notify listeners
Object.keys(this.Data[idModal].onDragEndListener || {}).forEach((keyListener) => {
this.Data[idModal].onDragEndListener[keyListener]?.();
});
},
};
let dragInstance = null;
// Initialize or update drag instance
const setDragInstance = () => {
if (options?.dragDisabled) {
if (dragInstance) {
dragInstance.destroy();
dragInstance = null;
}
return null;
}
const modal = s(`.${idModal}`);
if (!modal) {
console.warn(`Modal element .${idModal} not found for drag initialization`);
return null;
}
// Ensure the modal has position: absolute for proper dragging
if (window.getComputedStyle(modal).position !== 'absolute') {
modal.style.position = 'absolute';
}
// Clean up existing instance
if (dragInstance) {
dragInstance.destroy();
}
try {
// Create new instance with updated options
dragInstance = new Draggable(modal, dragOptions);
return dragInstance;
} catch (error) {
console.error('Failed to initialize draggable:', error);
return null;
}
};
// Expose method to update drag options
this.Data[idModal].setDragInstance = (updateDragOptions) => {
if (updateDragOptions) {
dragOptions = { ...dragOptions, ...updateDragOptions };
}
dragInstance = setDragInstance();
this.Data[idModal].dragInstance = dragInstance;
this.Data[idModal].dragOptions = dragOptions;
};
// Initialize modal with proper transitions
const modal = s(`.${idModal}`);
if (modal) {
// Initial state
modal.style.transition = 'opacity 0.15s ease, transform 0.3s ease';
modal.style.opacity = '0';
// Trigger fade-in after a small delay to allow initial render
requestAnimationFrame(() => {
if (!modal) return;
modal.style.opacity = '1';
// Set final transition after initial animation completes
setTimeout(() => {
if (modal) {
modal.style.transition = transition;
// Initialize drag after transitions are set
if (!options.dragDisabled) {
setDragInstance();
if (!options.mode) {
dragInstance.updateOptions({
position: { x: 0, y: 0 },
disabled: false, // Ensure drag is enabled after restore
});
}
}
}
}, 150);
});
}
const btnCloseEvent = () => {
Object.keys(this.Data[idModal].onCloseListener).map((keyListener) =>
this.Data[idModal].onCloseListener[keyListener](),
);
if (options && 'barConfig' in options && options.barConfig.buttons.close.onClick)
return options.barConfig.buttons.close.onClick();
s(`.${idModal}`).style.opacity = '0';
if (this.Data[idModal].observer) {
this.Data[idModal].observer.disconnect();
// this.Data[idModal].observer.unobserve();
}
setTimeout(() => {
if (!s(`.${idModal}`)) return;
this.removeModal(idModal);
// Handle modal route change
closeModalRouteChangeEvent({ closedId: idModal });
// history.back();
}, 300);
};
s(`.btn-close-${idModal}`).onclick = btnCloseEvent;
// Minimize button handler
s(`.btn-minimize-${idModal}`).onclick = () => {
const modal = s(`.${idModal}`);
if (!modal) return;
if (options.slideMenu) {
delete this.Data[idModal].slideMenu;
}
// Keep drag enabled when minimized
if (dragInstance) {
dragInstance.updateOptions({ disabled: false });
}
// Set up transition
modal.style.transition = 'height 0.3s ease, transform 0.3s ease';
// Update button states
s(`.btn-minimize-${idModal}`).style.display = 'none';
s(`.btn-maximize-${idModal}`).style.display = null;
s(`.btn-restore-${idModal}`).style.display = null;
// Collapse to header height
const header = s(`.bar-default-modal-${idModal}`);
if (header) {
modal.style.height = `${header.clientHeight}px`;
modal.style.overflow = 'hidden';
}
// Restore transition after animation
setTimeout(() => {
if (modal) {
modal.style.transition = transition;
}
}, 300);
};
// Restore button handler
s(`.btn-restore-${idModal}`).onclick = () => {
const modal = s(`.${idModal}`);
if (!modal) return;
if (options.slideMenu) {
delete this.Data[idModal].slideMenu;
}
// Re-enable dragging
if (dragInstance) {
dragInstance.updateOptions({ disabled: false });
}
// Set up transition
modal.style.transition = 'all 0.3s ease';
// Update button states
s(`.btn-restore-${idModal}`).style.display = 'none';
s(`.btn-minimize-${idModal}`).style.display = null;
s(`.btn-maximize-${idModal}`).style.display = null;
// Restore original dimensions and position
this.Data[idModal].center();
modal.style.transform = '';
modal.style.height = `${height}px`;
modal.style.left = left;
modal.style.top = top;
modal.style.width = `${width}px`;
modal.style.overflow = '';
// Re-enable drag after restore
if (dragInstance) {
dragInstance.updateOptions({
position: { x: 0, y: 0 },
disabled: false, // Ensure drag is enabled after restore
});
}
setTimeout(() => (s(`.${idModal}`) ? (s(`.${idModal}`).style.transition = transition) : null), 300);
};
s(`.btn-maximize-${idModal}`).onclick = () => {
const modal = s(`.${idModal}`);
if (!modal) return;
// Disable drag when maximizing
if (dragInstance) {
dragInstance.updateOptions({ disabled: true });
}
modal.style.transition = '0.3s';
setTimeout(() => (modal ? (modal.style.transition = transition) : null), 300);
s(`.btn-maximize-${idModal}`).style.display = 'none';
s(`.btn-restore-${idModal}`).style.display = null;
s(`.btn-minimize-${idModal}`).style.display = null;
modal.style.transform = null;
if (options.slideMenu) {
const idSlide = this.Data[options.slideMenu]['slide-menu']
? 'slide-menu'
: this.Data[options.slideMenu]['slide-menu-right']
? 'slide-menu-right'
: 'slide-menu-left';
const callBack = () => {
s(`.${idModal}`).style.transition = '0.3s';
s(`.${idModal}`).style.width = `${windowGetW() - this.Data[options.slideMenu][idSlide].width}px`;
s(`.${idModal}`).style.left =
idSlide === 'slide-menu-right' ? `0px` : `${this.Data[options.slideMenu][idSlide].width}px`;
setTimeout(() => (s(`.${idModal}`) ? (s(`.${idModal}`).style.transition = transition) : null), 300);
};
callBack();
this.Data[idModal].slideMenu = {
callBack,
id: options.slideMenu,
};
Responsive.onChanged(
() => {
setTimeout(() => {
if (!s(`.${idModal}`) || !s(`.main-body-btn-ui-close`)) return;
if (s(`.btn-restore-${idModal}`) && s(`.btn-restore-${idModal}`).style.display !== 'none') {
s(`.${idModal}`).style.height = s(`.main-body-btn-ui-close`).classList.contains('hide')
? `${windowGetH()}px`
: `${Modal.Data[idModal].getHeight()}px`;
}
s(`.${idModal}`).style.top = s(`.main-body-btn-ui-close`).classList.contains('hide')
? `0px`
: `${options.heightTopBar ? options.heightTopBar : heightDefaultTopBar}px`;
});
},
{ key: 'h-ui-hide-' + idModal },
);
Responsive.triggerChanged('h-ui-hide-' + idModal);
} else {
Responsive.offChanged('h-ui-hide-' + idModal);
s(`.${idModal}`).style.width = '100%';
s(`.${idModal}`).style.height = '100%';
s(`.${idModal}`).style.top = `${options.heightTopBar ? options.heightTopBar : heightDefaultTopBar}px`;
s(`.${idModal}`).style.left = `0px`;
}
dragInstance = setDragInstance();
};
const btnMenuEvent = () => {
Modal.menuTextLabelAnimation(idModal);
Object.keys(this.Data[idModal].onMenuListener).map((keyListener) =>
this.Data[idModal].onMenuListener[keyListener](),
);
if (options && 'barConfig' in options && options.barConfig.buttons.menu.onClick)
options.barConfig.buttons.menu.onClick();
};
s(`.btn-menu-${idModal}`).onclick = btnMenuEvent;
dragInstance = setDragInstance();
if (options && options.maximize) s(`.btn-maximize-${idModal}`).click();
if (options.observer) {
this.Data[idModal].observerCallBack = () => {
// logger.info('ResizeObserver', `.${idModal}`, s(`.${idModal}`).offsetWidth, s(`.${idModal}`).offsetHeight);
if (this.Data[idModal] && this.Data[idModal].onObserverListener)
Object.keys(this.Data[idModal].onObserverListener).map((eventKey) =>
this.Data[idModal].onObserverListener[eventKey]({
width: s(`.${idModal}`).offsetWidth,
height: s(`.${idModal}`).offsetHeight,
}),
);
else console.warn('observer not found', idModal);
};
this.Data[idModal].observer = new ResizeObserver(this.Data[idModal].observerCallBack);
this.Data[idModal].observer.observe(s(`.${idModal}`));
setTimeout(this.Data[idModal].observerCallBack);
}
// cancel: [cancel1, cancel2]
s(`.${idModal}`).onclick = () => {
this.Data[idModal]?.onClickListener
? Object.keys(this.Data[idModal].onClickListener).map((keyListener) =>
this.Data[idModal].onClickListener[keyListener](),
)
: null;
};
return {
id: idModal,
...this.Data[idModal],
};
}
/** @type {Object.<string, object>} */
static subMenuBtnClass = {};
/** Navigate to the home route and close all non-home modals. */
static onHomeRouterEvent = async () => {
// 1. Get list of modals to close.
const modalsToClose = Object.keys(Modal.Data).filter((idModal) => {
const modal = Modal.Data[idModal];
if (!modal) return false;
// Don't close the core UI elements
if (coreUI.find((id) => idModal.startsWith(id))) {
return false;
}
// Don't close modals that are part of the "home" screen itself
const homeModals = Modal.Data['modal-menu']?.homeModals || [];
if (homeModals.includes(idModal)) {
return false;
}
return true;
});
// 2. Navigate to home first, creating a new history entry.
setPath(`${getProxyPath()}${location.search ?? ''}${location.hash ?? ''}`);
setDocTitle();
// 3. Close the modals without them affecting the URL.
for (const id of modalsToClose) {
Modal.removeModal(id);
}
// 4. Finally, handle UI cleanup for the slide-menu.
if (s(`.nav-title-display-modal-menu`)) htmls(`.nav-title-display-modal-menu`, '');
if (s(`.nav-path-display-modal-menu`)) htmls(`.nav-path-display-modal-menu`, '');
if (s(`.btn-icon-menu-back`)) s(`.btn-icon-menu-back`).classList.add('hide');
// sa(`.main-btn-menu`).forEach((el) => {
// el.classList.remove('hide');
// });
// And close the slide menu if it's open
if (s(`.btn-close-modal-menu`) && !s(`.btn-close-modal-menu`).classList.contains('hide')) {
s(`.btn-close-modal-menu`).click();
}
};
/** @type {string} */
static currentTopModalId = '';
/**
* Synchronise z-index for view modals, promoting the given modal to top.
* @param {{ idModal: string }} param0
*/
static zIndexSync({ idModal }) {
setTimeout(() => {
if (!this.Data[idModal]) return;
const cleanTopModal = () => {
Object.keys(this.Data).map((_idModal) => {
if (this.Data[_idModal].options.zIndexSync && s(`.${_idModal}`)) s(`.${_idModal}`).style.zIndex = '3';
});
};
const setTopModal = () => {
if (s(`.${idModal}`)) {
this.setTopModalCallback(idModal);
} else setTimeout(setTopModal, 100);
};
cleanTopModal();
setTopModal();
this.Data[idModal].onClickListener[`${idModal}-z-index`] = () => {
if (s(`.${idModal}`) && s(`.${idModal}`).style.zIndex === '3') {
if (this.Data[idModal].options.route)
setPath(
`${getProxyPath()}${this.Data[idModal].options.route}${location.search ?? ''}${location.hash ?? ''}`,
);
cleanTopModal();
setTopModal();
}
};
});
}
/**
* @param {string} idModal
*/
static setTopModalCallback(idModal) {
s(`.${idModal}`).style.zIndex = '4';
this.currentTopModalId = `${idModal}`;
}
/** @returns {boolean} True when the viewport is considered mobile-sized. */
static mobileModal = () => windowGetW() < 600 || windowGetH() < 600;
/**
* @param {{ idModal: string, html: string }} param0
*/
static writeHTML = ({ idModal, html }) => htmls(`.html-${idModal}`, html);
/**
* Update an existing modal's content and/or title.
* @param {{ idModal: string, html?: string, title?: string }} param0
* @returns {Promise<boolean>}
*/
static async updateModal({ idModal, html, title }) {
if (!this.Data[idModal] || !s(`.${idModal}`)) {
console.warn(`Modal ${idModal} not found for update`);
return false;
}
// Update modal content
if (html) {
this.writeHTML({ idModal, html });
}
// Update modal title if provided
if (title) {
const titleElement = s(`.${idModal} .modal-title`);
if (titleElement) {
titleElement.innerHTML = title;
}
}
// Trigger reload listeners
if (this.Data[idModal].onReloadModalListener) {
for (const event of Object.keys(this.Data[idModal].onReloadModalListener)) {
await this.Data[idModal].onReloadModalListener[event]();
}
}
return true;
}
/** @returns {string|undefined} Id of the first open view-mode modal, or undefined. */
static viewModalOpen() {
return Object.keys(this.Data).find((idModal) => s(`.${idModal}`) && this.Data[idModal].options.mode === 'view');
}
/**
* Remove a modal element and its style tags, and delete its data entry.
* @param {string} idModal
*/
static removeModal(idModal) {
if (!s(`.${idModal}`)) return;
s(`.${idModal}`).remove();
sa(`.style-${idModal}`).forEach((element) => {
element.remove();
});
delete this.Data[idModal];
}
/**
* Render a confirmation dialog and return a promise resolving to the user's choice.
* @param {{ id: string, html: Function, icon?: string, disableBtnCancel?: boolean }} options
* @returns {Promise<{ status: 'confirm'|'cancelled' }>}
*/
static async RenderConfirm(options) {
const { id } = options;
append(
'body',
html`
<div
class="fix background-confirm-modal-${id}"
style="${renderCssAttr({
style: {
'z-index': '10',
background: 'rgba(0,0,0,0.5)',
width: '100%',
height: '100%',
top: '0px',
left: '0px',
transition: '0.3s',
opacity: '1',
},
})}"
></div>
`,
);
const removeBackgroundConfirmModal = () => {
s(`.background-confirm-modal-${id}`).style.opacity = '0';
setTimeout(() => {
s(`.background-confirm-modal-${id}`).remove();
});
};
return new Promise(async (resolve, reject) => {
const { barConfig } = await Themes[Css.currentTheme]();
barConfig.buttons.maximize.disabled = true;
barConfig.buttons.minimize.disabled = true;
barConfig.buttons.restore.disabled = true;
barConfig.buttons.menu.disabled = true;
barConfig.buttons.close.disabled = false;
const htmlRender = html`
<br />
<div class="in section-mp" style="font-size: 40px; text-align: center">
${options.icon ? options.icon : html` <i class="fas fa-question-circle"></i>`}
</div>
${await options.html()}
<div class="in section-mp">
${await BtnIcon.instance({
class: `in section-mp form-button btn-confirm-${id}`,
label: Translate.instance('confirm'),
type: 'submit',
style: `margin: auto`,
})}
</div>
<div class="in section-mp ${options.disableBtnCancel ? 'hide' : ''}">
${await BtnIcon.instance({
class: `in section-mp form-button btn-cancel-${id}`,
label: Translate.instance('cancel'),
type: 'submit',
style: `margin: auto`,
})}
</div>
`;
await Modal.instance({
id,
barConfig,
titleClass: 'hide',
style: {
width: '300px',
height: '400px',
overflow: 'hidden',
'z-index': '11',
resize: 'none',
},
dragDisabled: true,
...options,
html: htmlRender,
});
const end = () => {
removeBackgroundConfirmModal();
Modal.removeModal(id);
};
barConfig.buttons.close.onClick = () => {
end();
resolve({ status: 'cancelled' });
};
s(`.background-confirm-modal-${id}`).onclick = () => {
end();
resolve({ status: 'cancelled' });
};
s(`.btn-cancel-${id}`).onclick = () => {
end();
resolve({ status: 'cancelled' });
};
s(`.btn-confirm-${id}`).onclick = () => {
end();
resolve({ status: 'confirm' });
};
});
}
/** @type {string} */
static labelSelectorTopOffsetStartAnimation = `-40px`;
/** @type {string} */
static labelSelectorTopOffsetEndAnimation = `-3px`;
/**
* Animate slide-menu labels into view.
* @param {string} idModal - The slide-menu modal id.
* @param {string} [subMenuId] - If provided, animate only this sub-menu.
*/
static menuTextLabelAnimation = (idModal, subMenuId) => {
if (
!s(
`.btn-icon-menu-mode-${Modal.Data[idModal].options.mode === 'slide-menu-right' ? 'left' : 'right'}`,
).classList.contains('hide')
) {
return;
}
const btnSelector = `.main-btn-menu`;
const labelSelector = `.menu-label-text`;
const _data = subMenuId
? {
[subMenuId]: Modal.subMenuBtnClass[subMenuId],
}
: { ...Modal.subMenuBtnClass, _: { btnSelector, labelSelector } };
for (const keyDataBtn of Object.keys(_data)) {
const { labelSelector, top } = _data[keyDataBtn];
if (top)
setTimeout(() => {
top();
});
sa(labelSelector).forEach((el) => {
if (!el.classList.contains('hide')) el.classList.add('hide');
el.style.transition = null;
});
setTimeout(() => {
sa(labelSelector).forEach((el) => {
el.classList.remove('hide');
el.style.transition = null;
});
sa(labelSelector).forEach((el) => {
el.style.top = Modal.labelSelectorTopOffsetStartAnimation;
});
}, 300);
setTimeout(() => {
sa(labelSelector).forEach((el) => {
el.style.top = Modal.labelSelectorTopOffsetEndAnimation;
});
}, 400);
}
};
/**
* Builds submenu item HTML using the canonical BtnIcon pattern.
* Centralises the item-rendering contract so individual submenu owners (e.g. Docs)
* only need to supply data — not layout concerns.
* @param {string} subMenuId - Submenu identifier (e.g. 'docs')
* @param {Array<{type:string, icon:string, text:string, url:function|string}>} items
* @param {Object} [options]
* @param {function} [options.subMenuIcon] - Optional icon override per item type
* @returns {Promise<string>} Rendered HTML string
*/
static buildSubMenuItemsHtml = async (subMenuId, items = [], options = {}) => {
let result = '';
const _menuMode = Modal.Data['modal-menu']?.options?.mode;
const _tooltipSide = options.tooltipSide || (_menuMode === 'slide-menu-right' ? 'right' : 'left');
for (const item of items) {
const tabHref = typeof item.url === 'function' ? item.url() : item.url || '';
const icon =
options.subMenuIcon && typeof options.subMenuIcon === 'function' ? options.subMenuIcon(item.type) : item.icon;
result += html`${await BtnIcon.instance({
class: `in wfa main-btn-menu submenu-btn btn-${subMenuId} btn-${subMenuId}-${item.type}`,
label: html`<span class="inl menu-btn-icon">${icon}</span
><span class="menu-label-text menu-label-text-${subMenuId}"> ${item.text} </span>`,
tabHref,
tooltipHtml: await Badge.instance(buildBadgeToolTipMenuOption(item.text, _tooltipSide)),
useMenuBtn: true,
})} `;
}
return result;
};
/**
* Injects pre-built submenu item HTML into the submenu container and syncs
* the collapse state so labels are hidden when the menu is icon-only.
* @param {string} subMenuId
* @param {string} itemsHtml
*/
static subMenuPopulate = (subMenuId, itemsHtml) => {
htmls(`.menu-btn-container-children-${subMenuId}`, itemsHtml);
const _menuMode = Modal.Data['modal-menu']?.options?.mode;
const _collapseIndicatorClass =
_menuMode === 'slide-menu-right' ? '.btn-icon-menu-mode-left' : '.btn-icon-menu-mode-right';
if (s(_collapseIndicatorClass) && !s(_collapseIndicatorClass).classList.contains('hide')) {
sa(`.menu-label-text-${subMenuId}`).forEach((el) => el.classList.add('hide'));
}
};
// Move modal title element into the bar's render container so it aligns with control buttons
/**
* Position a modal relative to an anchor element.
* @param {object} options - Positioning options
* @param {string} options.modalSelector - CSS selector for the modal element
* @param {string} options.anchorSelector - CSS selector for the anchor element
* @param {object} [options.offset={x: 0, y: 6}] - Offset from anchor
* @param {string} [options.align='right'] - Horizontal alignment ('left' or 'right')
* @param {boolean} [options.autoVertical=true] - Whether to automatically determine vertical position
* @param {boolean} [options.placeAbove] - Force position above/below anchor (overrides autoVertical)
*/
static positionRelativeToAnchor({
modalSelector,
anchorSelector,
offset = { x: 0, y: 6 },
align = 'right',
autoVertical = true,
placeAbove,
}) {
try {
const modal = s(modalSelector);
const anchor = s(anchorSelector);
if (!modal || !anchor || !anchor.getBoundingClientRect) return;
// First, position the modal near its final position but off-screen
const arect = anchor.getBoundingClientRect();
const vh = windowGetH();
const vw = windowGetW();
const safeMargin = 6;
// Determine vertical position
let finalPlaceAbove = placeAbove;
if (autoVertical && placeAbove === undefined) {
const inBottomBar = anchor.closest && anchor.closest('.bottom-bar');
const inTopBar = anchor.closest && anchor.closest('.slide-menu-top-bar');
if (inBottomBar) finalPlaceAbove = true;
else if (inTopBar) finalPlaceAbove = false;
else finalPlaceAbove = arect.top > vh / 2; // heuristic fallback
}
// Set initial position (slightly offset from final position)
const initialOffset = finalPlaceAbove ? 20 : -20;
modal.style.position = 'fixed';
modal.style.opacity = '0';
modal.style.transition = 'opacity 150ms ease-out, transform 150ms ease-out';
// Position near the anchor but slightly offset
modal.style.top = `${finalPlaceAbove ? arect.top - 40 : arect.bottom + 20}px`;
modal.style.left = `${align === 'right' ? arect.right - 200 : arect.left}px`;
modal.style.transform = 'translateY(0)';
// Force reflow to ensure initial styles are applied
modal.offsetHeight;
// Now calculate final position
const mrect = modal.getBoundingClientRect();
// Calculate final top position
const top = finalPlaceAbove ? arect.top - mrect.height - offset.y : arect.bottom + offset.y;
// Calculate final left position based on alignment
let left;
if (align === 'right') {
left = arect.right - mrect.width - offset.x; // align right edges
} else {
left = arect.left + offset.x; // align left edges
}
// Ensure modal stays within viewport bounds
left = Math.max(safeMargin, Math.min(left, vw - mrect.width - safeMargin));
const finalTop = Math.max(safeMargin, Math.min(top, vh - mrect.height - safeMargin));
// Apply final position with smooth transition
requestAnimationFrame(() => {
modal.style.top = `${Math.round(finalTop)}px`;
modal.style.left = `${Math.round(left)}px`;
modal.style.opacity = '1';
});
} catch (e) {
console.error('Error positioning modal:', e);
}
}
/**
* Move the title element inside the bar button container for inline alignment.
* @param {string} idModal
*/
static MoveTitleToBar(idModal) {
try {
const titleEl = s(`.title-modal-${idModal}`);
const container = s(`.btn-bar-modal-container-render-${idModal}`);
if (!titleEl || !container) return;
const titleNode = titleEl.cloneNode(true);
titleEl.remove();
container.classList.add('in');
container.classList.add('fll');
container.appendChild(titleNode);
} catch (e) {
// non-fatal: keep default placement if structure not present
}
}
/** Update the top-banner anchor href and bind the home-click handler. */
static setTopBannerLink() {
if (s(`.a-link-top-banner`)) {
s(`.a-link-top-banner`).setAttribute('href', `${location.origin}${getProxyPath()}`);
EventsUI.onClick(`.a-link-top-banner`, (e) => {
e.preventDefault();
s(`.action-btn-home`).click();
});
}
}
/** @type {number} */
static headerTitleHeight = 40;
/** Toggle the slide-menu open/close via the center action button. */
static actionBtnCenter() {
if (!s(`.btn-close-modal-menu`).classList.contains('hide')) {
return s(`.btn-close-modal-menu`).click();
}
if (!s(`.btn-menu-modal-menu`).classList.contains('hide')) {
return s(`.btn-menu-modal-menu`).click();
}
}
/** Hide the top-bar, bottom-bar and slide-menu (full-screen mode). */
static cleanUI() {
s(`.top-bar`).classList.add('hide');
s(`.bottom-bar`).classList.add('hide');
s(`.modal-menu`).classList.add('hide');
}
/** Restore the top-bar, bottom-bar and slide-menu after cleanUI. */
static restoreUI() {
s(`.top-bar`).classList.remove('hide');
s(`.bottom-bar`).classList.remove('hide');
s(`.modal-menu`).classList.remove('hide');
}
/**
* Re-applies canonical top/height layout for maximized slide-menu-backed view modals.
* This avoids iframe navigation leaving view containers offset at the top.
*/
static syncViewLayout() {
const modalMenuOptions = this.Data['modal-menu']?.options || {};
const uiCollapsed = !!s(`.main-body-btn-ui-close`) && s(`.main-body-btn-ui-close`).classList.contains('hide');
const topOffset = uiCollapsed ? 0 : modalMenuOptions.heightTopBar ? modalMenuOptions.heightTopBar : 50;
const bottomOffset = uiCollapsed ? 0 : modalMenuOptions.heightBottomBar ? modalMenuOptions.heightBottomBar : 0;
const height = `${windowGetH() - topOffset - bottomOffset}px`;
Object.keys(this.Data).forEach((idModal) => {
const modalData = this.Data[idModal];
const modalEl = s(`.${idModal}`);
if (!modalData || !modalEl || !modalData.slideMenu) return;
if (s(`.btn-restore-${idModal}`) && s(`.btn-restore-${idModal}`).style.display === 'none') return;
modalEl.style.position = 'fixed';
modalEl.style.translate = '0px 0px';
modalEl.style.transform = '';
modalEl.style.top = `${topOffset}px`;
modalEl.style.height = height;
});
for (const id of coreUI) {
const key = `view-${id}`;
if (Responsive.hasChangedListener(key)) Responsive.triggerChanged(key);
}
}
/** Add missing `alt` attributes to all images for SEO/accessibility. */
static RenderSeoSanitizer = async () => {
sa('img').forEach((img) => {
if (!img.getAttribute('alt')) img.setAttribute('alt', 'image ' + Worker.title + ' ' + s4());
});
};
}
const renderMenuLabel = ({ img, src, text, icon }) => {
if (!img && !src) return html`<span class="inl menu-btn-icon">${icon}</span> ${text}`;
const imgSrc = src ? src : `${getProxyPath()}assets/ui-icons/${img}`;
return html`<img class="abs center img-btn-square-menu" src="${imgSrc}" />
<div class="abs center main-btn-menu-text">${text}</div>`;
};
const renderViewTitle = (
options = {
icon: '',
img: '',
text: '',
assetFolder: '',
'ui-icon': '',
imgClass: '',
textClass: '',
dim,
top,
topText: '',
},
) => {
if (options.dim === undefined) options.dim = 30;
const { img, text, icon, dim, top } = options;
if (!img && !options['ui-icon']) return html`<span class="view-title-icon">${icon}</span> ${text}`;
const imgClass = options.imgClass || 'abs img-btn-square-view-title';
const textClass = options.textClass || 'in text-btn-square-view-title';
return html`<img
class="${imgClass}"
style="${renderCssAttr({
style: {
width: `${dim}px`,
height: `${dim}px`,
top: top !== undefined ? `${top}px !important` : undefined,
},
})}"
src="${options['ui-icon']
? `${getProxyPath()}assets/${options.assetFolder ? options.assetFolder : 'ui-icons'}/${options['ui-icon']}`
: img}"
/>
<div
class="${textClass}"
style="${renderCssAttr({
style: {
// 'padding-left': `${20 + dim}px`,
...(options.topText !== undefined ? { top: options.topText + 'px !important' } : {}),
},
})}"
>
${text}
</div>`;
};
const buildBadgeToolTipMenuOption = (id, sideKey = 'left') => {
const option = {
id: `tooltip-content-main-btn-${id}`,
text: `${Translate.instance(`${id}`)}`,
classList: 'tooltip-menu',
style: { top: `-40px` },
};
switch (sideKey) {
case 'left':
option.style.left = '60px';
break;
case 'right':
option.style.right = '60px';
break;
default:
break;
}
return option;
};
const isSubMenuOpen = (subMenuId) => {
return s(`.down-arrow-submenu-${subMenuId}`) && s(`.down-arrow-submenu-${subMenuId}`).style.rotate === '180deg';
};
const subMenuRender = async (subMenuId) => {
const _hBtn = 51;
const menuBtn = s(`.main-btn-${subMenuId}`);
const menuContainer = s(`.menu-btn-container-children-${subMenuId}`);
const arrow = s(`.down-arrow-submenu-${subMenuId}`);
if (!menuBtn || !menuContainer || !arrow) return;
const top = () => {
let value = menuBtn.offsetTop + Modal.Data['modal-menu'].options.heightTopBar;
const isHidden = s('.main-body-btn-ui-open').classList.contains('hide');
const isTopBottom = Modal.Data['modal-menu'].options.barMode === 'top-bottom-bar';
if (!isTopBottom && isHidden) value -= 51;
else if (!isHidden) value += 51;
menuContainer.style.top = `${value}px`;
};
Modal.subMenuBtnClass[subMenuId] = {
...Modal.subMenuBtnClass[subMenuId],
btnSelector: `.btn-${subMenuId}`,
labelSelector: `.menu-label-text-${subMenuId}`,
top,
};
menuBtn.style.transition = '.3s';
arrow.style.transition = '.3s';
if (isSubMenuOpen(subMenuId)) {
// Close animation
menuContainer.style.overflow = 'hidden';
menuContainer.style.height = '0px';
arrow.style.rotate = '180deg';
setTimeout(() => {
menuBtn.style.marginBottom = '0px';
arrow.style.rotate = '0deg';
});
} else {
sa(`.menu-label-text-${subMenuId}`).forEach((el) => {
if (!el.classList.contains('hide')) el.classList.add('hide');
});
setTimeout(() => {
Modal.menuTextLabelAnimation('modal-menu', subMenuId);
});
// Open animation — match the current menu width (collapsed = 50px, extended = 320px)
const _menuMode = Modal.Data['modal-menu']?.options?.mode;
const _collapseIndicatorClass =
_menuMode === 'slide-menu-right' ? '.btn-icon-menu-mode-left' : '.btn-icon-menu-mode-right';
const _isMenuCollapsed = s(_collapseIndicatorClass) && !s(_collapseIndicatorClass).classList.contains('hide');
const _menuContainerWidth = _isMenuCollapsed ? 50 : 320;
setTimeout(top, 360);
menuContainer.style.width = `${_menuContainerWidth}px`;
menuContainer.style.overflow = null;
menuContainer.style.height = '0px';
menuContainer.style.height = `${_hBtn * 6}px`;
arrow.style.rotate = '0deg';
setTimeout(() => {
menuBtn.style.marginBottom = `${_hBtn * sa(`.menu-label-text-${subMenuId}`).length + 4}px`;
arrow.style.rotate = '180deg';
});
}
setTimeout(() => {
menuBtn.style.transition = null;
}, 500);
};
const subMenuHandler = (routes, route) => {
route = sanitizeRoute(route);
for (let _route of routes) {
_route = sanitizeRoute(_route);
if (_route !== route) {
if (isSubMenuOpen(_route)) subMenuRender(_route);
}
}
setTimeout(() => {
let cid = getQueryParams().cid;
if (cid && cid.includes(',')) {
cid = cid.split(',')[0];
}
if (s(`.main-sub-btn-active`)) s(`.main-sub-btn-active`).classList.remove('main-sub-btn-active');
if (cid && s(`.btn-${route}-${cid}`)) {
s(`.btn-${route}-${cid}`).classList.add('main-sub-btn-active');
}
});
};
export {
Modal,
renderMenuLabel,
renderViewTitle,
buildBadgeToolTipMenuOption,
subMenuRender,
isSubMenuOpen,
subMenuHandler,
};