UNPKG

@underpostnet/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

1,192 lines (1,124 loc) 53.2 kB
import { getId, isValidDate, newInstance } from './CommonJs.js'; import { LoadingAnimation } from '../core/LoadingAnimation.js'; import { Validator } from '../core/Validator.js'; import { Input } from '../core/Input.js'; import { darkTheme, ThemeEvents, subThemeManager, lightenHex, darkenHex } from './Css.js'; import { append, copyData, getDataFromInputFile, htmls, s, sa } from './VanillaJs.js'; import { BtnIcon } from './BtnIcon.js'; import { Translate } from './Translate.js'; import { DropDown } from './DropDown.js'; import { dynamicCol } from './Css.js'; import { EventsUI } from './EventsUI.js'; import { ToggleSwitch } from './ToggleSwitch.js'; import { RichText } from './RichText.js'; import { loggerFactory } from './Logger.js'; import { Badge } from './Badge.js'; import { Content, attachMarkdownLinkHandlers } from './Content.js'; import { DocumentService } from '../../services/document/document.service.js'; import { NotificationManager } from './NotificationManager.js'; import { getApiBaseUrl } from '../../services/core/core.service.js'; import { getProxyPath, setQueryPath, navigateToProfile } from './Router.js'; import { PublicProfile } from './PublicProfile.js'; const logger = loggerFactory(import.meta); class Panel { static Tokens = {}; static async instance( options = { idPanel: '', parentIdModal: '', scrollClassContainer: '', htmlFormHeader: async () => '', formData: [], data: [], originData: () => [], filesData: () => [], onClick: () => {}, share: { copyLink: false, copySourceMd: false, }, showCreatorProfile: false, }, ) { const idPanel = options?.idPanel ? options.idPanel : getId(this.Tokens, `${idPanel}-`); if (options.formData) options.formData = options.formData.map((formObj) => { formObj.id = `${idPanel}-${formObj.id}`; return formObj; }); const { scrollClassContainer, formData, data } = options; const titleObj = formData.find((f) => f.panel && f.panel.type === 'title'); const titleKey = titleObj ? titleObj.model : ''; const subTitleObj = formData.find((f) => f.panel && f.panel.type === 'subtitle'); const subTitleKey = subTitleObj ? subTitleObj.model : ''; const fileNameInputExtDefaultContent = html` <div class="abs center"> <i style="font-size: 25px" class="fa-solid fa-cloud"></i> </div>`; const openPanelForm = () => { s(`.${idPanel}-form-body`).classList.remove('hide'); s(`.btn-${idPanel}-add`).classList.add('hide'); // s(`.${scrollClassContainer}`).style.overflow = 'hidden'; if (options.customButtons) { let customBtnIndex = -1; for (const dataBtn of options.customButtons) { customBtnIndex++; const customBtnIndexFn = customBtnIndex; const btnSelector = `btn-${idPanel}-custom${customBtnIndexFn}`; s(`.${btnSelector}`).classList.add('hide'); } } setTimeout(() => { s(`.${idPanel}-form-body`).style.opacity = 1; }); }; const renderPanel = async (payload) => { const obj = newInstance(payload); if ('_id' in obj) obj.id = obj._id; const { id } = obj; setTimeout(async () => { if (!s(`.${idPanel}`)) return; LoadingAnimation.spinner.play(`.${idPanel}-img-spinner-${id}`, 'dual-ring'); if (options && options.callBackPanelRender) await options.callBackPanelRender({ data: obj, fileRender: async (options = { file: '', style: {}, class: '' }) => { await Content.RenderFile({ container: `.${idPanel}-cell-col-a-${id}`, ...options }); s(`.${idPanel}-img-spinner-${id}`).classList.add('hide'); }, htmlRender: async ({ render }) => { htmls(`.${idPanel}-cell-col-a-${id}`, render); }, }); if (options.share && options.share.copyLink) { EventsUI.onClick( `.${idPanel}-btn-copy-share-${id}`, async (e) => { try { const shareUrl = `${window.location.origin}${window.location.pathname}?cid=${obj._id}`; await copyData(shareUrl); await NotificationManager.Push({ status: 'success', html: html`<div>${Translate.instance('link-copied')}</div>`, }); // Track the copy share link event await DocumentService.patch({ id: obj._id, action: 'copy-share-link' }); // Update the count in the UI - read current value from span first const countSpan = s(`.${idPanel}-share-count-${id}`); if (countSpan) { const currentCount = parseInt(countSpan.textContent) || 0; const newCount = currentCount + 1; htmls(`.${idPanel}-share-count-${id}`, newCount); } else { // Create count badge if it didn't exist before (was 0) const btn = s(`.${idPanel}-btn-copy-share-${id}`); if (btn) { const countBadge = document.createElement('span'); countBadge.className = `${idPanel}-share-count-${id}`; countBadge.style.cssText = 'position: absolute; top: -4px; right: -4px; background: #666; color: white; border-radius: 10px; padding: 1px 5px; font-size: 10px; font-weight: bold; min-width: 16px; text-align: center;'; countBadge.textContent = '1'; btn.appendChild(countBadge); } } } catch (error) { logger.error('Error copying share link:', error); await NotificationManager.Push({ status: 'error', html: html`<div>${Translate.instance('error-copying-link')}</div>`, }); } }, { context: 'modal' }, ); // Add tooltip hover effect setTimeout(() => { const btn = s(`.${idPanel}-btn-copy-share-${id}`); const tooltip = s(`.${idPanel}-share-tooltip-${id}`); if (btn && tooltip) { btn.addEventListener('mouseenter', () => { tooltip.style.opacity = '1'; }); btn.addEventListener('mouseleave', () => { tooltip.style.opacity = '0'; }); } }); } if (options.share && options.share.copySourceMd) { EventsUI.onClick( `.${idPanel}-btn-copy-source-md-${id}`, async (e) => { try { const filesData = options.filesData(); const foundFiles = filesData.find((d) => String(d._id || d.id) === String(obj._id || obj.id)); if (foundFiles && foundFiles.mdFileId && foundFiles.mdFileId.mdPlain) { await copyData(foundFiles.mdFileId.mdPlain); await NotificationManager.Push({ status: 'success', html: html`<div>${Translate.instance('markdown-source-copied')}</div>`, }); } else { await NotificationManager.Push({ status: 'warning', html: html`<div>No markdown source available</div>`, }); } } catch (error) { logger.error('Error copying markdown source:', error); await NotificationManager.Push({ status: 'error', html: html`<div>${Translate.instance('error-copying-markdown')}</div>`, }); } }, { context: 'modal' }, ); // Add tooltip hover effect setTimeout(() => { const btn = s(`.${idPanel}-btn-copy-source-md-${id}`); const tooltip = s(`.${idPanel}-source-md-tooltip-${id}`); if (btn && tooltip) { btn.addEventListener('mouseenter', () => { tooltip.style.opacity = '1'; }); btn.addEventListener('mouseleave', () => { tooltip.style.opacity = '0'; }); } }); } EventsUI.onClick( `.${idPanel}-btn-delete-${id}`, async (e) => { logger.warn('delete', obj); const { status } = await options.on.remove({ e, data: obj }); if (status === 'error') return; if (s(`.${idPanel}-${id}`)) s(`.${idPanel}-${id}`).remove(); }, { context: 'modal' }, ); EventsUI.onClick(`.${idPanel}-btn-edit-${id}`, async () => { logger.warn('edit', obj); const searchId = String(obj._id || obj.id); if (obj._id) Panel.Tokens[idPanel].editId = obj._id; else if (obj.id) Panel.Tokens[idPanel].editId = obj.id; s(`.btn-${idPanel}-label-edit`).classList.remove('hide'); s(`.btn-${idPanel}-label-add`).classList.add('hide'); openPanelForm(); // s(`.${scrollClassContainer}`).scrollTop = 0; const originData = options.originData(); const filesData = options.filesData(); // Convert IDs to strings for comparison to handle ObjectId vs string issues const foundOrigin = originData.find((d) => String(d._id || d.id) === searchId); const foundFiles = filesData.find((d) => String(d._id || d.id) === searchId); if (!foundOrigin) { logger.error('Could not find origin data for ID:', searchId); logger.error( 'Available originData IDs:', originData.map((d) => String(d._id || d.id)), ); } if (!foundFiles) { logger.error('Could not find files data for ID:', searchId); logger.error( 'Available filesData IDs:', filesData.map((d) => String(d._id || d.id)), ); } // Clear previous form values then populate with the current item's data Input.cleanValues(formData); await Input.setValues(formData, obj, foundOrigin, foundFiles); if (options.on.initEdit) await options.on.initEdit({ data: obj }); }); s(`.a-${payload._id}`).onclick = async (e) => { e.preventDefault(); if (options.onClick) await options.onClick({ payload }); }; s(`.container-${idPanel}-${id}`).onclick = async (e) => { e.preventDefault(); // if (options.onClick) await options.onClick({ payload }); }; // Add theme change handler for creator profile header if (options.showCreatorProfile && obj.userInfo) { const updateCreatorProfileTheme = () => { const profileHeader = s(`.creator-profile-header-${id}`); if (profileHeader) { profileHeader.style.borderBottom = `1px solid ${darkTheme ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.08)'}`; profileHeader.style.background = `${darkTheme ? 'rgba(255,255,255,0.02)' : 'rgba(0,0,0,0.02)'}`; // Update avatar border if it's an image const avatarImg = profileHeader.querySelector('.creator-avatar'); if (avatarImg && avatarImg.tagName === 'IMG') { avatarImg.style.border = `2px solid ${darkTheme ? 'rgba(102, 126, 234, 0.5)' : 'rgba(102, 126, 234, 0.3)'}`; } // Update username color const username = profileHeader.querySelector('.creator-username'); if (username) { username.style.color = `${darkTheme ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.85)'}`; } // Update "Creator" label color const creatorLabel = username?.nextElementSibling; if (creatorLabel) { creatorLabel.style.color = `${darkTheme ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.45)'}`; } } }; // Register theme change handler const profileThemeHandlerId = `${id}-creator-profile-theme`; ThemeEvents[profileThemeHandlerId] = updateCreatorProfileTheme; // Add click handlers for public profile links setTimeout(() => { const links = sa(`.creator-profile-link-${id}`); links.forEach((link) => { link.onclick = async (e) => { e.preventDefault(); const username = link.getAttribute('data-id'); // Check if public profile modal is already open const currentModal = s('.modal-public-profile'); if (currentModal) { // Modal is already open, update the profile content dynamically // Navigate to clean URL without intermediate ?cid= in history navigateToProfile(username, { replace: false }); await PublicProfile.Update({ idModal: 'modal-public-profile', user: { username }, }); } else { // Modal is not open, navigate to clean URL and open modal navigateToProfile(username, { replace: false }); if (s('.main-btn-public-profile')) s('.main-btn-public-profile').click(); } }; }); }); } }); if (s(`.${idPanel}-${id}`)) s(`.${idPanel}-${id}`).remove(); // Check if document is public (from obj.isPublic field) const isPublic = obj.isPublic === true; // Visibility icon: globe for public, padlock for private const visibilityIcon = isPublic ? '<i class="fas fa-globe" title="Public document"></i>' : '<i class="fas fa-lock" title="Private document"></i>'; return html` <div class="in box-shadow ${idPanel} ${idPanel}-${id}" style="position: relative;"> <div class="fl ${idPanel}-tools session-fl-log-in ${obj.tools ? '' : 'hide'}"> ${await BtnIcon.instance({ class: `in flr main-btn-menu action-bar-box ${idPanel}-btn-tool ${idPanel}-btn-delete-${id}`, label: html`<div class="abs center"><i class="fas fa-trash"></i></div>`, useVisibilityHover: true, tooltipHtml: await Badge.instance({ id: `tooltip-${idPanel}-${id}`, text: `${Translate.instance(`delete`)}`, classList: '', style: { top: `-22px`, left: '-13px' }, }), })} ${await BtnIcon.instance({ class: `in flr main-btn-menu action-bar-box ${idPanel}-btn-tool ${idPanel}-btn-edit-${id}`, label: html`<div class="abs center"><i class="fas fa-edit"></i></div>`, useVisibilityHover: true, tooltipHtml: await Badge.instance({ id: `tooltip-${idPanel}-${id}`, text: `${Translate.instance(`edit`)}`, classList: '', style: { top: `-22px`, left: '-5px' }, }), })} </div> <div class="in container-${idPanel}-${id}"> <div class="panel-visibility-icon">${visibilityIcon}</div> ${options.showCreatorProfile && obj.userInfo ? html`<div class="creator-profile-header creator-profile-header-${id}" style="padding: 10px 12px; margin-bottom: 10px; border-bottom: 1px solid ${darkTheme ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.08)'}; display: flex; align-items: center; gap: 10px; background: ${darkTheme ? 'rgba(255,255,255,0.02)' : 'rgba(0,0,0,0.02)'}; border-radius: 4px 4px 0 0;" > <a href="${getProxyPath()}u/${obj.userInfo.username}" class="creator-profile-link-${id}" data-id="${obj.userInfo.username}" style="display: flex;" > ${obj.userInfo.profileImageId && obj.userInfo.profileImageId._id ? html`<img class="creator-avatar" src="${getApiBaseUrl({ id: obj.userInfo.profileImageId._id, endpoint: 'file/blob' })}" alt="${obj.userInfo.username}" style="width: 36px; height: 36px; border-radius: 50%; object-fit: cover; border: 2px solid ${darkTheme ? 'rgba(102, 126, 234, 0.5)' : 'rgba(102, 126, 234, 0.3)'}; flex-shrink: 0; box-shadow: 0 2px 8px rgba(0,0,0,0.15);" title="${obj.userInfo.username}" />` : html`<div class="creator-avatar" style="width: 36px; height: 36px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: 16px; flex-shrink: 0; box-shadow: 0 2px 8px rgba(0,0,0,0.15);" title="${obj.userInfo.username}" > ${(obj.userInfo.username || 'U').charAt(0).toUpperCase()} </div>`} </a> <div style="display: flex; flex-direction: column; min-width: 0; flex: 1;"> <a href="${getProxyPath()}u/${obj.userInfo.username}" class="creator-username creator-profile-link-${id}" data-id="${obj.userInfo.username}" style="font-size: 14px; font-weight: 600; color: ${darkTheme ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.85)'}; line-height: 1.4; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" > ${obj.userInfo.username || 'Unknown'} </a> <span style="font-size: 11px; color: ${darkTheme ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.45)'}; line-height: 1.3; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 500;" >${obj.userInfo.briefDescription || 'Uploader'}</span > </div> </div>` : ''} <div class="in ${idPanel}-head"> <div class="in ${idPanel}-title"> ${options.titleIcon} <a href="?cid=${payload._id}" class="a-title-${idPanel} a-${payload._id}"> ${titleKey ? obj[titleKey] : ''}</a > </div> <div class="in ${idPanel}-subtitle"> ${subTitleKey ? obj[subTitleKey] : ''} <span class="tag-render-${id}"></span> </div> <!-- <div class="in ${idPanel}-tags"></div> --> </div> <div class="fl"> <div class="in fll ${idPanel}-cell ${idPanel}-cell-col-a ${idPanel}-cell-col-a-${id}"> <div class="abs center ${idPanel}-img-spinner-${id}"></div> </div> <div class="in fll ${idPanel}-cell ${idPanel}-cell-col-b"> ${Object.keys(obj) .map((infoKey) => { if (infoKey === 'id') return html``; const formObjData = formData.find((f) => f.model === infoKey); const valueIcon = formObjData?.panel?.icon?.value ? formObjData.panel.icon.value : ''; const keyIcon = formObjData?.panel?.icon?.key ? formObjData.panel.icon.key : ''; if (formObjData && ['datetime-local'].includes(formObjData.inputType) && isValidDate(obj[infoKey])) { obj[infoKey] = `${obj[infoKey]}`.replace('T', ' ').replace('.000Z', ''); } if (formData.find((f) => f.model === infoKey && f.panel && f.panel.type === 'tags')) { // Function to render tags with current theme const renderTags = async () => { let tagRender = html``; for (const tag of obj[infoKey]) { // Use subThemeManager colors for consistent theming const themeColor = darkTheme ? subThemeManager.darkColor : subThemeManager.lightColor; const hasThemeColor = themeColor && themeColor !== null; let tagBg, tagColor; if (darkTheme) { tagBg = hasThemeColor ? darkenHex(themeColor, 0.6) : '#4a4a4a'; tagColor = hasThemeColor ? lightenHex(themeColor, 0.7) : '#ffffff'; } else { tagBg = hasThemeColor ? lightenHex(themeColor, 0.7) : '#a2a2a2'; tagColor = hasThemeColor ? darkenHex(themeColor, 0.5) : '#ffffff'; } tagRender += await Badge.instance({ text: tag, classList: 'inl panel-tag-clickable', style: { margin: '3px', background: tagBg, color: tagColor, cursor: 'pointer', transition: 'all 0.2s ease', }, }); } if (s(`.tag-render-${id}`)) { htmls(`.tag-render-${id}`, tagRender); // Add click handlers to tags for search integration setTimeout(() => { const tagElements = sa(`.tag-render-${id} .panel-tag-clickable`); tagElements.forEach((tagEl) => { tagEl.onclick = (e) => { e.stopPropagation(); const tagText = tagEl.textContent.trim(); // Open search bar if closed if ( !s('.main-body-btn-ui-bar-custom-open').classList.contains('hide') || !s(`.main-body-btn-ui-open`).classList.contains('hide') ) s('.main-body-btn-bar-custom').click(); // Find and populate search box if it exists const searchBox = s('.top-bar-search-box'); if (searchBox) { searchBox.value = tagText; searchBox.focus(); // Trigger input event to start search const inputEvent = new Event('input', { bubbles: true }); searchBox.dispatchEvent(inputEvent); logger.info(`Tag clicked: ${tagText} - search triggered`); } }; }); }, 100); } }; // Initial render setTimeout(renderTags); // Add theme change handler for this tag set const tagThemeHandlerId = `${id}-tags-${infoKey}-theme`; ThemeEvents[tagThemeHandlerId] = renderTags; return html``; } { const formDataObj = formData.find((f) => f.model === infoKey && f.panel && f.panel.type === 'list'); if (obj[infoKey] && obj[infoKey].length > 0 && formDataObj) return html`<div class="in ${idPanel}-row"> <span class="${idPanel}-row-key capitalize ${formObjData.label?.disabled ? 'hide' : ''}"> ${keyIcon} ${Translate.instance(infoKey)}:</span > <span class="${idPanel}-row-value" >${valueIcon} ${obj[infoKey].map((k) => Translate.instance(k)).join(', ')}</span > </div> `; } { const formDataObj = formData.find( (f) => f.model === infoKey && f.panel && f.panel.type === 'info-row-pin', ); if (obj[infoKey] && formDataObj) return html`<div class="in ${idPanel}-row"> <span class="${idPanel}-row-pin-key capitalize ${formObjData.label?.disabled ? 'hide' : ''}"> ${keyIcon} ${formDataObj.translateCode ? Translate.instance(formDataObj.translateCode) : Translate.instance(infoKey)}:</span > <span class="${idPanel}-row-pin-value">${valueIcon} ${obj[infoKey]}</span> </div> `; } { const formDataObj = formData.find( (f) => f.model === infoKey && f.panel && f.panel.type === 'info-row', ); if (obj[infoKey] && formDataObj) return html`<div class="in ${idPanel}-row"> <span class="${idPanel}-row-key capitalize ${formObjData.label?.disabled ? 'hide' : ''}"> ${keyIcon} ${formDataObj.translateCode ? Translate.instance(formDataObj.translateCode) : Translate.instance(infoKey)}:</span > <span class="${idPanel}-row-value"> ${valueIcon} ${obj[infoKey]}</span> </div> `; } return html``; }) .join('')} </div> </div> </div> ${options.share && (options.share.copyLink || options.share.copySourceMd) ? html`<div class="${idPanel}-share-btn-container ${idPanel}-share-btn-container-${id}" style="position: absolute; bottom: 8px; right: 8px; z-index: 2; display: flex; gap: 8px;" > ${options.share.copyLink ? html`<div style="position: relative;"> <button class="btn-icon ${idPanel}-btn-copy-share-${id}" style="background: transparent; color: #888; border: none; border-radius: 50%; width: 40px; height: 40px; cursor: pointer; display: flex; align-items: center; justify-content: center; position: relative; transition: all 0.3s ease;" > <i class="fas fa-link" style="font-size: 20px;"></i> ${obj.totalCopyShareLinkCount && obj.totalCopyShareLinkCount > 0 ? html`<span class="${idPanel}-share-count-${id}" style="position: absolute; top: -4px; right: -4px; background: #666; color: white; border-radius: 10px; padding: 1px 5px; font-size: 10px; font-weight: bold; min-width: 16px; text-align: center;" >${obj.totalCopyShareLinkCount}</span >` : ''} </button> <div class="${idPanel}-share-tooltip-${id}" style="position: absolute; bottom: 50px; right: 0; background: rgba(0,0,0,0.8); color: white; padding: 6px 10px; border-radius: 4px; font-size: 12px; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 0.3s ease;" > ${Translate.instance('copy-share-link')} </div> </div>` : ''} ${options.share.copySourceMd ? html`<div style="position: relative;"> <button class="btn-icon ${idPanel}-btn-copy-source-md-${id}" style="background: transparent; color: #888; border: none; border-radius: 50%; width: 40px; height: 40px; cursor: pointer; display: flex; align-items: center; justify-content: center; position: relative; transition: all 0.3s ease;" > <i class="fas fa-code" style="font-size: 20px;"></i> </button> <div class="${idPanel}-source-md-tooltip-${id}" style="position: absolute; bottom: 50px; right: 0; background: rgba(0,0,0,0.8); color: white; padding: 6px 10px; border-radius: 4px; font-size: 12px; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 0.3s ease;" > Copy Source MD </div> </div>` : ''} </div>` : ''} </div>`; }; let render = ''; let renderForm = html` <div class="in modal" style="top: 0px; z-index: 1; padding-bottom: 5px"> ${await BtnIcon.instance({ class: `inl section-mp btn-custom btn-${idPanel}-close`, label: html`<i class="fa-solid fa-xmark"></i> ${Translate.instance('close')}`, type: 'button', })} </div> ${options?.htmlFormHeader ? await options.htmlFormHeader() : ''}`; for (const modelData of formData) { if (modelData.disableRender) continue; switch (modelData.inputType) { case 'dropdown-checkbox': { renderForm += html`<div class="in section-mp"> ${await DropDown.instance({ id: `${modelData.id}`, label: html`${Translate.instance(modelData.model)}`, type: 'checkbox', value: modelData.dropdown.options[0], resetOption: true, containerClass: `${idPanel}-dropdown-checkbox`, data: modelData.dropdown.options.map((dKey) => { return { value: dKey, data: dKey, checked: false, display: html`${Translate.instance(dKey)}`, onClick: function () { logger.info('DropDown onClick', this.checked); }, }; }), })} </div>`; break; } case 'dropdown': renderForm += html` <div class="in section-mp"> ${await DropDown.instance({ id: `${modelData.id}`, label: html`${Translate.instance(modelData.model)}`, containerClass: `${idPanel}-dropdown`, // type: 'checkbox', value: modelData.dropdown.options[0].replaceAll(' ', '-').toLowerCase(), data: modelData.dropdown.options.map((dKey) => { const key = dKey.replaceAll(' ', '-').toLowerCase(); return { value: key, data: dKey, // checked: true, display: html`${Translate.instance(dKey)}`, onClick: function () {}, }; }), })} </div>`; break; case 'md': { renderForm += html`<div class="in section-mp"> ${await RichText.instance({ id: modelData.id, parentIdModal: options.parentIdModal })} </div>`; break; } case 'checkbox-on-off': { setTimeout(() => { s(`.toggle-form-container-${modelData.id}`).onclick = () => { ToggleSwitch.Tokens[`${modelData.id}`].click(); }; }); renderForm += html`<div class="in section-mp toggle-form-container toggle-form-container-${modelData.id} hover" style="height: 82px;" > <div class="fl"> <div class="in fll" style="width: 70%"> <div class="in"> ${modelData.panel && modelData.panel.icon ? modelData.panel.icon : ''} ${Translate.instance(modelData.model)} </div> </div> <div class="in fll" style="width: 30%"> ${await ToggleSwitch.instance({ id: `${modelData.id}`, containerClass: 'inl', disabledOnClick: true, checked: false, on: { unchecked: () => {}, checked: () => {}, }, })} </div> </div> </div>`; } break; case 'file': setTimeout(() => { if (!s(`.${modelData.id}`)) return; s(`.${modelData.id}`).fileNameInputExtDefaultContent = fileNameInputExtDefaultContent; s(`.${modelData.id}`).onchange = async (e) => { if (!Object.keys(e.target.files).length) return; s(`.${modelData.id}`).inputFiles = e.target.files; let htmlFileRender = ''; for (const fileKey of Object.keys(e.target.files)) { const file = e.target.files[fileKey]; htmlFileRender += html`${await Content.RenderFile({ url: URL.createObjectURL(file), file: { mimetype: file.type, name: file.name, data: { data: await getDataFromInputFile(file), }, }, raw: true, })} <div class="in" style="overflow: hidden">${file.name}</div>`; } htmls(`.file-name-render-${modelData.id}`, htmlFileRender); }; }); renderForm += `${await Input.instance({ inputClass: 'hide', id: `${modelData.id}`, type: modelData.inputType, multiple: true, // autocomplete: 'new-password', label: html`<i class="fa-solid fa-file-arrow-up"></i> ${Translate.instance('select')} ${Translate.instance('file')}`, containerClass: 'in section-mp width-mini-box input-container', placeholder: true, extension: () => html`<div class="file-name-render-${modelData.id}" style="min-height: 50px"> ${fileNameInputExtDefaultContent} </div>`, // disabled: true, // disabledEye: true, })} ${await BtnIcon.instance({ class: `inl section-mp btn-custom btn-${idPanel}-clean-file`, label: html`<i class="fa-solid fa-file-circle-xmark"></i> ${Translate.instance('clear-file')}`, type: 'button', })}`; break; default: renderForm += `${await Input.instance({ id: `${modelData.id}`, type: modelData.inputType, // autocomplete: 'new-password', label: html`<i class="fa-solid fa-pen-to-square"></i> ${Translate.instance(modelData.model)}`, containerClass: 'in section-mp width-mini-box input-container', placeholder: true, // disabled: true, // disabledEye: true, })}`; break; } } let renderFormBtn = html` ${await BtnIcon.instance({ class: `inl section-mp btn-custom btn-${idPanel}-submit`, label: html`<span class="btn-${idPanel}-label-add" ><i class="fas fa-plus"></i> ${Translate.instance('add')}</span ><span class="btn-${idPanel}-label-edit hide" ><i class="fas fa-edit"></i> ${Translate.instance('edit')}</span >`, type: 'submit', })} ${await BtnIcon.instance({ class: `inl section-mp btn-custom btn-${idPanel}-clean`, label: html`<i class="fa-solid fa-broom"></i> ${Translate.instance('clear')}`, type: 'button', })} `; setTimeout(async () => { const validators = await Validator.instance(formData); s(`.${idPanel}-form`).onsubmit = (e) => { e.preventDefault(); s(`.btn-${idPanel}-submit`).click(); }; EventsUI.onClick(`.btn-${idPanel}-submit`, async (e) => { e.preventDefault(); const { errorMessage } = await validators(); if (errorMessage) return; const obj = Input.getValues(formData); obj.id = `${data.length}`; let documents; if (options && options.on && options.on.add) { const { status, data } = await options.on.add({ data: obj, editId: Panel.Tokens[idPanel].editId }); if (status === 'error') return; documents = data; } s(`.btn-${idPanel}-clean`).click(); if (Panel.Tokens[idPanel].editId && s(`.${idPanel}-${Panel.Tokens[idPanel].editId}`)) s(`.${idPanel}-${Panel.Tokens[idPanel].editId}`).remove(); if (Array.isArray(documents)) { htmls(`.${idPanel}-render`, ''); for (const doc of documents) { append(`.${idPanel}-render`, await renderPanel(doc)); } } else htmls(`.${idPanel}-render`, await renderPanel({ ...obj, ...documents })); attachMarkdownLinkHandlers(`.${idPanel}-render`); Input.cleanValues(formData); s(`.btn-${idPanel}-close`).click(); s(`.${scrollClassContainer}`).scrollTop = 0; if (s(`.${scrollClassContainer}`)) s(`.${scrollClassContainer}`).style.overflow = 'auto'; }); s(`.btn-${idPanel}-clean`).onclick = () => { Input.cleanValues(formData); }; if (s(`.btn-${idPanel}-clean-file`)) s(`.btn-${idPanel}-clean-file`).onclick = () => { // Clear file input specifically const fileFormData = formData.find((f) => f.inputType === 'file'); if (fileFormData && s(`.${fileFormData.id}`)) { s(`.${fileFormData.id}`).value = ''; s(`.${fileFormData.id}`).inputFiles = null; htmls(`.file-name-render-${fileFormData.id}`, `${fileNameInputExtDefaultContent}`); } }; s(`.btn-${idPanel}-close`).onclick = (e) => { e.preventDefault(); s(`.${idPanel}-form-body`).style.opacity = 0; s(`.btn-${idPanel}-add`).classList.remove('hide'); s(`.${scrollClassContainer}`).style.overflow = 'auto'; if (options.customButtons) { let customBtnIndex = -1; for (const dataBtn of options.customButtons) { customBtnIndex++; const customBtnIndexFn = customBtnIndex; const btnSelector = `btn-${idPanel}-custom${customBtnIndexFn}`; s(`.${btnSelector}`).classList.remove('hide'); } } s(`.${idPanel}-form-body`).classList.add('hide'); }; s(`.btn-${idPanel}-add`).onclick = async (e) => { e.preventDefault(); // Clean all form inputs and reset data scope Input.cleanValues(formData); // Clean file input specifically const fileFormData = formData.find((f) => f.inputType === 'file'); if (fileFormData && s(`.${fileFormData.id}`)) { s(`.${fileFormData.id}`).value = ''; s(`.${fileFormData.id}`).inputFiles = null; htmls(`.file-name-render-${fileFormData.id}`, `${fileNameInputExtDefaultContent}`); } // Reset edit ID to ensure we're in "add" mode Panel.Tokens[idPanel].editId = undefined; // Update button labels s(`.btn-${idPanel}-label-add`).classList.remove('hide'); s(`.btn-${idPanel}-label-edit`).classList.add('hide'); // Scroll to top s(`.${scrollClassContainer}`).scrollTop = 0; openPanelForm(); if (options.on.initAdd) await options.on.initAdd(); }; if (s(`.${scrollClassContainer}`)) s(`.${scrollClassContainer}`).style.overflow = 'auto'; attachMarkdownLinkHandlers(`.${idPanel}-render`); }); if (data.length > 0) for (const obj of data) render += await renderPanel(obj); else { render += html`<div class="in" style="min-height: 200px"> <div class="abs center"><i class="fas fa-exclamation-circle"></i> ${Translate.instance(`no-result-found`)}</div> </div>`; if (options.on.noResultFound) setTimeout(options.on.noResultFound); } this.Tokens[idPanel] = { idPanel, scrollClassContainer, formData, data, titleKey, subTitleKey, renderPanel }; let customButtonsRender = ''; if (options && options.customButtons) { let customBtnIndex = -1; for (const dataBtn of options.customButtons) { customBtnIndex++; const customBtnIndexFn = customBtnIndex; const btnSelector = `btn-${idPanel}-custom${customBtnIndexFn}`; if (dataBtn.onClick) setTimeout(() => { s(`.${btnSelector}`).onclick = () => dataBtn.onClick(); }); customButtonsRender += ` ${await BtnIcon.instance({ class: `inl section-mp btn-custom ${btnSelector}`, label: dataBtn.label, type: 'button', })}`; } } // Add theme change handler const themeChangeHandler = () => { const styleElement = s(`.${idPanel}-styles`); if (styleElement) { styleElement.textContent = darkTheme ? getDarkStyles(idPanel, scrollClassContainer) : getLightStyles(idPanel, scrollClassContainer); } // Update tag hover styles const tagStyleElement = s(`.${idPanel}-tag-styles`); if (tagStyleElement) { const themeColor = darkTheme ? subThemeManager.darkColor : subThemeManager.lightColor; const hasThemeColor = themeColor && themeColor !== null; let hoverBg; if (darkTheme) { hoverBg = hasThemeColor ? darkenHex(themeColor, 0.5) : '#5a5a5a'; } else { hoverBg = hasThemeColor ? lightenHex(themeColor, 0.6) : '#8a8a8a'; } tagStyleElement.textContent = css` .panel-tag-clickable:hover { background: ${hoverBg} !important; transform: scale(1.05); } .panel-tag-clickable:active { transform: scale(0.98); } `; } }; // Add theme change listener ThemeEvents[`${idPanel}-theme`] = themeChangeHandler; // Initial styles setTimeout(ThemeEvents[`${idPanel}-theme`]); return html` <style> .${idPanel}-head { /* background: white; */ margin-bottom: 10px; } .img-${idPanel} { width: 100%; } .${idPanel}-title { color: ${(() => { const themeColor = darkTheme ? subThemeManager.darkColor : subThemeManager.lightColor; const hasThemeColor = themeColor && themeColor !== null; if (hasThemeColor) { return darkTheme ? lightenHex(themeColor, 0.3) : darkenHex(themeColor, 0.2); } else { return darkTheme ? '#8a85ff' : 'rgba(109, 104, 255, 1)'; } })()}; font-size: 24px; padding: 5px; } .a-title-${idPanel} { color: ${(() => { const themeColor = darkTheme ? subThemeManager.darkColor : subThemeManager.lightColor; const hasThemeColor = themeColor && themeColor !== null; if (hasThemeColor) { return darkTheme ? lightenHex(themeColor, 0.3) : darkenHex(themeColor, 0.2); } else { return darkTheme ? '#8a85ff' : 'rgba(109, 104, 255, 1)'; } })()}; } .a-title-${idPanel}:hover { color: ${(() => { const themeColor = darkTheme ? subThemeManager.darkColor : subThemeManager.lightColor; const hasThemeColor = themeColor && themeColor !== null; if (hasThemeColor) { return darkTheme ? lightenHex(themeColor, 0.5) : lightenHex(themeColor, 0.3); } else { return darkTheme ? '#ffb74d' : '#e89f4c'; } })()}; } .${idPanel}-row { padding: 5px; margin: 5px; font-size: 16px; } .${idPanel}-subtitle { font-size: 17px; margin-left: 20px; top: -7px; } .${idPanel}-tags { font-size: 17px; margin-left: 10px; top: -7px; } .${idPanel}-row-key { } .${idPanel}-row-value { } .${idPanel}-row-pin-key { } .${idPanel}-row-pin-value { font-size: 20px; color: rgb(19 190 84); } .${idPanel}-form-header { } .${idPanel}-form-body { transition: 0.3s; } .btn-${idPanel}-add { padding: 10px; font-size: 20px; } .${idPanel}-dropdown { min-height: 100px; } .${idPanel}-btn-tool { background: none !important; color: #c4c4c4 !important; } .${idPanel}-btn-tool:hover { color: #000000 !important; font-size: 17px !important; } .${idPanel}-share-btn-container button:hover { background: transparent !important; transform: scale(1.1); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4) !important; } .${idPanel}-share-btn-container button:focus { outline: none; background: transparent !important; } .${idPanel}-share-btn-container button:focus { outline: none; background: transparent !important; } .${idPanel}-share-btn-container button:active { transform: scale(0.95); } .${idPanel}-share-btn-container span[class*='share-count'] { animation: ${idPanel}-share-pulse 2s infinite; } @keyframes ${idPanel}-share-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.1); } } </style> <style class="${idPanel}-styles"></style> <style class="${idPanel}-tag-styles"> .panel-tag-clickable:hover { background: ${(() => { const themeColor = darkTheme ? subThemeManager.darkColor : subThemeManager.lightColor; const hasThemeColor = themeColor && themeColor !== null; if (darkTheme) { return hasThemeColor ? darkenHex(themeColor, 0.5) : '#5a5a5a'; } else { return hasThemeColor ? lightenHex(themeColor, 0.6) : '#8a8a8a'; } })()} !important; transform: scale(1.05); } .panel-tag-clickable:active { transform: scale(0.98); } </style> <div class="${idPanel}-container"> <div class="in modal ${idPanel}-form-container ${options.formContainerClass ? options.formContainerClass : ''}"> <div class="in ${idPanel}-form-header"> ${await BtnIcon.instance({ class: `inl section-mp btn-custom btn-${idPanel}-add ${ options?.role?.add ? (!options.role.add() ? 'hide' : '') : '' }`, label: html`<i class="fas fa-plus"></i> ${Translate.instance('add')}`, type: 'button', })} <!-- pagination component --> ${customButtonsRender} </div> <div class="in ${idPanel}-form-body hide" style="opacity: 0"> <form class="in ${idPanel}-form"> <div class="fl">${renderForm}</div> <div class="in">${renderFormBtn}</div> <br /><br /> </form> </div> </div> ${dynamicCol({ id: `${idPanel}-cell`, containerSelector: `${idPanel}-render`, limit: 500, type: 'a-50-b-50', })} <div class="in ${idPanel}-render">${render}</div> </div> `; } } // Function to generate base styles function getBaseStyles(idPanel, scrollClassContainer) { return css` .${scrollClassContainer} { scroll-behavior: smooth; } .${idPanel}-form-container { padding-bottom: 20px; top: 0px; z-index: 1; overflow: auto; } .${idPanel}-form { max-width: 900px; } .${idPanel}-cell { min-height: 200px; } .${idPanel}-container { } .${idPanel} { margin: 10px; transition: 0.3s; border-radius: 10px; padding: 10px; min-height: 400px; } .${idPanel}-head { margin-bottom: 10px; } .img-${idPanel} { width: 100%; } .${idPanel}-row { padding: 5px; margin: 5px; font-size: 16px; } .${idPanel}-subtitle { font-size: 17px; margin-left: 20px; top: -7px; } .${idPanel}-tags { font-size: 17px; margin-left: 10px; top: -7px; } .${idPanel}-form-body { transition: 0.3s; } .btn-${idPanel}-add { padding: 10px; font-size: 20px; } .${idPanel}-dropdown { min-height: 100px; } .panel-visibility-icon { position: absolute; top: 34px; left: 0px; font-size: 14px; opacity: 0.7; transition: opacity 0.2s ease; pointer-events: none;