@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
JavaScript
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;