import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; import { $el, ComfyDialog } from "../../scripts/ui.js"; import { getBestPosition, getPositionStyle, getRect } from './popover-helper.js'; function internalCustomConfirm(message, confirmMessage, cancelMessage) { return new Promise((resolve) => { // transparent bg const modalOverlay = document.createElement('div'); modalOverlay.style.position = 'fixed'; modalOverlay.style.top = 0; modalOverlay.style.left = 0; modalOverlay.style.width = '100%'; modalOverlay.style.height = '100%'; modalOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.8)'; modalOverlay.style.display = 'flex'; modalOverlay.style.alignItems = 'center'; modalOverlay.style.justifyContent = 'center'; modalOverlay.style.zIndex = '1101'; // Modal window container (dark bg) const modalDialog = document.createElement('div'); modalDialog.style.backgroundColor = '#333'; modalDialog.style.padding = '20px'; modalDialog.style.borderRadius = '4px'; modalDialog.style.maxWidth = '400px'; modalDialog.style.width = '80%'; modalDialog.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.5)'; modalDialog.style.color = '#fff'; // Display message const modalMessage = document.createElement('p'); modalMessage.textContent = message; modalMessage.style.margin = '0'; modalMessage.style.padding = '0 0 20px'; modalMessage.style.wordBreak = 'keep-all'; // Button container const modalButtons = document.createElement('div'); modalButtons.style.display = 'flex'; modalButtons.style.justifyContent = 'flex-end'; // Confirm button (green) const confirmButton = document.createElement('button'); if(confirmMessage) confirmButton.textContent = confirmMessage; else confirmButton.textContent = 'Confirm'; confirmButton.style.marginLeft = '10px'; confirmButton.style.backgroundColor = '#28a745'; // green confirmButton.style.color = '#fff'; confirmButton.style.border = 'none'; confirmButton.style.padding = '6px 12px'; confirmButton.style.borderRadius = '4px'; confirmButton.style.cursor = 'pointer'; confirmButton.style.fontWeight = 'bold'; // Cancel button (red) const cancelButton = document.createElement('button'); if(cancelMessage) cancelButton.textContent = cancelMessage; else cancelButton.textContent = 'Cancel'; cancelButton.style.marginLeft = '10px'; cancelButton.style.backgroundColor = '#dc3545'; // red cancelButton.style.color = '#fff'; cancelButton.style.border = 'none'; cancelButton.style.padding = '6px 12px'; cancelButton.style.borderRadius = '4px'; cancelButton.style.cursor = 'pointer'; cancelButton.style.fontWeight = 'bold'; const closeModal = () => { document.body.removeChild(modalOverlay); }; confirmButton.addEventListener('click', () => { closeModal(); resolve(true); }); cancelButton.addEventListener('click', () => { closeModal(); resolve(false); }); modalButtons.appendChild(confirmButton); modalButtons.appendChild(cancelButton); modalDialog.appendChild(modalMessage); modalDialog.appendChild(modalButtons); modalOverlay.appendChild(modalDialog); document.body.appendChild(modalOverlay); }); } export function show_message(msg) { app.ui.dialog.show(msg); app.ui.dialog.element.style.zIndex = 1100; } export async function handle403Response(res, defaultMessage) { try { const data = await res.json(); if(data.error === 'comfyui_outdated') { show_message('ComfyUI version is outdated.
Please update ComfyUI to use Manager normally.'); } else { show_message(defaultMessage || 'This action is not allowed with this security level configuration.'); } } catch { show_message(defaultMessage || 'This action is not allowed with this security level configuration.'); } } export async function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } export async function customConfirm(message) { try { let res = await window['app'].extensionManager.dialog .confirm({ title: 'Confirm', message: message }); return res; } catch { let res = await internalCustomConfirm(message); return res; } } export function customAlert(message) { try { window['app'].extensionManager.toast.addAlert(message); } catch { alert(message); } } export function infoToast(summary, message) { try { app.extensionManager.toast.add({ severity: 'info', summary: summary, detail: message, life: 3000 }) } catch { // do nothing } } export async function customPrompt(title, message) { try { let res = await window['app'].extensionManager.dialog .prompt({ title: title, message: message }); return res; } catch { return prompt(title, message) } } export async function rebootAPI() { if ('electronAPI' in window) { window.electronAPI.restartApp(); return true; } const isConfirmed = await customConfirm("Are you sure you'd like to reboot the server?"); if (isConfirmed) { try { const response = await api.fetchApi("/manager/reboot"); if (response.status == 403) { await handle403Response(response); return false; } } catch(exception) {} } return false; } export var manager_instance = null; export function setManagerInstance(obj) { manager_instance = obj; } export function showToast(message, duration = 3000) { const toast = $el("div.comfy-toast", {textContent: message}); document.body.appendChild(toast); setTimeout(() => { toast.classList.add("comfy-toast-fadeout"); setTimeout(() => toast.remove(), 500); }, duration); } function isValidURL(url) { if(url.includes('&')) return false; const http_pattern = /^(https?|ftp):\/\/[^\s$?#]+$/; const ssh_pattern = /^(.+@|ssh:\/\/).+:.+$/; return http_pattern.test(url) || ssh_pattern.test(url); } export async function install_pip(packages) { if(packages.includes('&')) app.ui.dialog.show(`Invalid PIP package enumeration: '${packages}'`); const res = await api.fetchApi("/customnode/install/pip", { method: "POST", body: packages, }); if(res.status == 403) { await handle403Response(res); return; } if(res.status == 200) { show_message(`PIP package installation is processed.
To apply the pip packages, please click the button in ComfyUI.`); const rebootButton = document.getElementById('cm-reboot-button3'); const self = this; rebootButton.addEventListener("click", rebootAPI); } else { show_message(`Failed to install '${packages}'
See terminal log.`); } } export async function install_via_git_url(url, manager_dialog) { if(!url) { return; } if(!isValidURL(url)) { show_message(`Invalid Git url '${url}'`); return; } show_message(`Wait...

Installing '${url}'`); const res = await api.fetchApi("/customnode/install/git_url", { method: "POST", body: url, }); if(res.status == 403) { await handle403Response(res); return; } if(res.status == 200) { show_message(`'${url}' is installed
To apply the installed custom node, please ComfyUI.`); const rebootButton = document.getElementById('cm-reboot-button4'); const self = this; rebootButton.addEventListener("click", async function() { if(await rebootAPI()) { manager_instance.close(); } }); } else { show_message(`Failed to install '${url}'
See terminal log.`); } } export async function free_models(free_execution_cache) { try { let mode = ""; if(free_execution_cache) { mode = '{"unload_models": true, "free_memory": true}'; } else { mode = '{"unload_models": true}'; } let res = await api.fetchApi(`/free`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: mode }); if (res.status == 200) { if(free_execution_cache) { showToast("'Models' and 'Execution Cache' have been cleared.", 3000); } else { showToast("Models' have been unloaded.", 3000); } } else { showToast('Unloading of models failed. Installed ComfyUI may be an outdated version.', 5000); } } catch (error) { showToast('An error occurred while trying to unload models.', 5000); } } export function md5(inputString) { const hc = '0123456789abcdef'; const rh = n => {let j,s='';for(j=0;j<=3;j++) s+=hc.charAt((n>>(j*8+4))&0x0F)+hc.charAt((n>>(j*8))&0x0F);return s;} const ad = (x,y) => {let l=(x&0xFFFF)+(y&0xFFFF);let m=(x>>16)+(y>>16)+(l>>16);return (m<<16)|(l&0xFFFF);} const rl = (n,c) => (n<>>(32-c)); const cm = (q,a,b,x,s,t) => ad(rl(ad(ad(a,q),ad(x,t)),s),b); const ff = (a,b,c,d,x,s,t) => cm((b&c)|((~b)&d),a,b,x,s,t); const gg = (a,b,c,d,x,s,t) => cm((b&d)|(c&(~d)),a,b,x,s,t); const hh = (a,b,c,d,x,s,t) => cm(b^c^d,a,b,x,s,t); const ii = (a,b,c,d,x,s,t) => cm(c^(b|(~d)),a,b,x,s,t); const sb = x => { let i;const nblk=((x.length+8)>>6)+1;const blks=[];for(i=0;i>2]|=x.charCodeAt(i)<<((i%4)*8);} blks[i>>2]|=0x80<<((i%4)*8);blks[nblk*16-2]=x.length*8;return blks; } let i,x=sb(inputString),a=1732584193,b=-271733879,c=-1732584194,d=271733878,olda,oldb,oldc,oldd; for(i=0;i { err = e; }); if (!res) { return { status: 400, error: new Error("Unknown Error") } } const { status, statusText } = res; if (err) { return { status, error: err } } if (status !== 200) { return { status, error: new Error(statusText || "Unknown Error") } } const data = await res.json(); if (!data) { return { status, error: new Error(`Failed to load data: ${route}`) } } return { status, data } } // https://cenfun.github.io/open-icons/ export const icons = { search: '', conflicts: '', passed: '', download: '', close: '', arrowRight: '' } export function sanitizeHTML(str) { return str .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } export function showTerminal() { try { const panel = app.extensionManager.bottomPanel; const isTerminalVisible = panel.bottomPanelVisible && panel.activeBottomPanelTab.id === 'logs-terminal'; if (!isTerminalVisible) panel.toggleBottomPanelTab('logs-terminal'); } catch(exception) { // do nothing } } let need_restart = false; export function setNeedRestart(value) { need_restart = value; } async function onReconnected(event) { if(need_restart) { setNeedRestart(false); const confirmed = await customConfirm("To apply the changes to the node pack's installation status, you need to refresh the browser. Would you like to refresh?"); if (!confirmed) { return; } window.location.reload(true); } } api.addEventListener('reconnected', onReconnected); const storeId = "comfyui-manager-grid"; let timeId; export function storeColumnWidth(gridId, columnItem) { clearTimeout(timeId); timeId = setTimeout(() => { let data = {}; const dataStr = localStorage.getItem(storeId); if (dataStr) { try { data = JSON.parse(dataStr); } catch (e) {} } if (!data[gridId]) { data[gridId] = {}; } data[gridId][columnItem.id] = columnItem.width; localStorage.setItem(storeId, JSON.stringify(data)); }, 200) } export function restoreColumnWidth(gridId, columns) { const dataStr = localStorage.getItem(storeId); if (!dataStr) { return; } let data; try { data = JSON.parse(dataStr); } catch (e) {} if(!data) { return; } const widthMap = data[gridId]; if (!widthMap) { return; } columns.forEach(columnItem => { const w = widthMap[columnItem.id]; if (w) { columnItem.width = w; } }); } export function getTimeAgo(dateStr) { const date = new Date(dateStr); if (!date || !(date instanceof Date) || isNaN(date.getTime())) { return ""; } const units = [ { max: 2760000, value: 60000, name: 'minute', past: 'a minute ago', future: 'in a minute' }, { max: 72000000, value: 3600000, name: 'hour', past: 'an hour ago', future: 'in an hour' }, { max: 518400000, value: 86400000, name: 'day', past: 'yesterday', future: 'tomorrow' }, { max: 2419200000, value: 604800000, name: 'week', past: 'last week', future: 'in a week' }, { max: 28512000000, value: 2592000000, name: 'month', past: 'last month', future: 'in a month' } ]; const diff = Date.now() - date.getTime(); // less than a minute if (Math.abs(diff) < 60000) return 'just now'; for (let i = 0; i < units.length; i++) { if (Math.abs(diff) < units[i].max) { return format(diff, units[i].value, units[i].name, units[i].past, units[i].future, diff < 0); } } function format(diff, divisor, unit, past, future, isInTheFuture) { const val = Math.round(Math.abs(diff) / divisor); if (isInTheFuture) return val <= 1 ? future : 'in ' + val + ' ' + unit + 's'; return val <= 1 ? past : val + ' ' + unit + 's ago'; } return format(diff, 31536000000, 'year', 'last year', 'in a year', diff < 0); }; export const loadCss = (cssFile) => { const cssPath = import.meta.resolve(cssFile); //console.log(cssPath); const $link = document.createElement("link"); $link.setAttribute("rel", 'stylesheet'); $link.setAttribute("href", cssPath); document.head.appendChild($link); }; export const copyText = (text) => { return new Promise((resolve) => { let err; try { navigator.clipboard.writeText(text); } catch (e) { err = e; } if (err) { resolve(false); } else { resolve(true); } }); }; function renderPopover($elem, target, options = {}) { // async microtask queueMicrotask(() => { const containerRect = getRect(window); const targetRect = getRect(target); const elemRect = getRect($elem); const positionInfo = getBestPosition( containerRect, targetRect, elemRect, options.positions ); const style = getPositionStyle(positionInfo, { bgColor: options.bgColor, borderColor: options.borderColor, borderRadius: options.borderRadius }); $elem.style.top = positionInfo.top + "px"; $elem.style.left = positionInfo.left + "px"; $elem.style.background = style.background; }); } let $popover; export function hidePopover() { if ($popover) { $popover.remove(); $popover = null; } } export function showPopover(target, text, className, options) { hidePopover(); $popover = document.createElement("div"); $popover.className = ['cn-popover', className].filter(it => it).join(" "); document.body.appendChild($popover); $popover.innerHTML = text; $popover.style.display = "block"; renderPopover($popover, target, { borderRadius: 10, ... options }); } let $tooltip; export function hideTooltip(target) { if ($tooltip) { $tooltip.style.display = "none"; $tooltip.innerHTML = ""; $tooltip.style.top = "0px"; $tooltip.style.left = "0px"; } } export function showTooltip(target, text, className = 'cn-tooltip', styleMap = {}) { if (!$tooltip) { $tooltip = document.createElement("div"); $tooltip.className = className; $tooltip.style.cssText = ` pointer-events: none; position: fixed; z-index: 10001; padding: 20px; color: #1e1e1e; max-width: 350px; filter: drop-shadow(1px 5px 5px rgb(0 0 0 / 30%)); ${Object.keys(styleMap).map(k=>k+":"+styleMap[k]+";").join("")} `; document.body.appendChild($tooltip); } $tooltip.innerHTML = text; $tooltip.style.display = "block"; renderPopover($tooltip, target, { positions: ['top', 'bottom', 'right', 'center'], bgColor: "#ffffff", borderColor: "#cccccc", borderRadius: 5 }); } function initTooltip () { const mouseenterHandler = (e) => { const target = e.target; const text = target.getAttribute('tooltip'); if (text) { showTooltip(target, text); } }; const mouseleaveHandler = (e) => { const target = e.target; const text = target.getAttribute('tooltip'); if (text) { hideTooltip(target); } }; document.body.removeEventListener('mouseenter', mouseenterHandler, true); document.body.removeEventListener('mouseleave', mouseleaveHandler, true); document.body.addEventListener('mouseenter', mouseenterHandler, true); document.body.addEventListener('mouseleave', mouseleaveHandler, true); } export async function uninstallNodes(nodeList, options = {}) { const { title = `${nodeList.length} custom nodes`, onProgress = () => {}, onError = () => {}, onSuccess = () => {}, channel = 'default', mode = 'default' } = options; // Check if queue is busy let stats = await api.fetchApi('/manager/queue/status'); stats = await stats.json(); if (stats.is_processing) { customAlert(`[ComfyUI-Manager] There are already tasks in progress. Please try again after it is completed. (${stats.done_count}/${stats.total_count})`); return { success: false, error: 'Queue is busy' }; } // Confirmation dialog for uninstall const confirmed = await customConfirm(`Are you sure uninstall ${title}?`); if (!confirmed) { return { success: false, error: 'User cancelled' }; } let errorMsg = ""; let target_items = []; await api.fetchApi('/manager/queue/reset'); for (const nodeItem of nodeList) { target_items.push(nodeItem); onProgress(`Uninstall ${nodeItem.title || nodeItem.name} ...`); const data = nodeItem.originalData || nodeItem; data.channel = channel; data.mode = mode; data.ui_id = nodeItem.hash || md5(nodeItem.name || nodeItem.title); const res = await api.fetchApi(`/manager/queue/uninstall`, { method: 'POST', body: JSON.stringify(data) }); if (res.status != 200) { errorMsg = `'${nodeItem.title || nodeItem.name}': `; if (res.status == 403) { errorMsg += `This action is not allowed with this security level configuration.\n`; } else if (res.status == 404) { errorMsg += `With the current security level configuration, only custom nodes from the "default channel" can be uninstalled.\n`; } else { errorMsg += await res.text() + '\n'; } break; } } if (errorMsg) { onError(errorMsg); show_message("[Uninstall Errors]\n" + errorMsg); return { success: false, error: errorMsg, targets: target_items }; } else { await api.fetchApi('/manager/queue/start'); onSuccess(target_items); showTerminal(); return { success: true, targets: target_items }; } } // =========================================================================================== // Workflow Utilities Consolidation export async function getWorkflowNodeTypes() { try { const res = await fetchData('/customnode/get_node_types_in_workflows'); if (res.status === 200) { return { success: true, data: res.data }; } else if (res.status === 204) { // No workflows found - return empty list return { success: true, data: [] }; } else { return { success: false, error: res.error }; } } catch (error) { return { success: false, error: error }; } } export function findPackageByCnrId(cnrId, nodePackages, installedOnly = true) { if (!cnrId || !nodePackages) { return null; } // Tier 1: Direct key match if (nodePackages[cnrId]) { const pack = nodePackages[cnrId]; if (!installedOnly || pack.state !== "not-installed") { return { key: cnrId, pack: pack }; } } // Tier 2: Case-insensitive match const cnrIdLower = cnrId.toLowerCase(); for (const packKey of Object.keys(nodePackages)) { if (packKey.toLowerCase() === cnrIdLower) { const pack = nodePackages[packKey]; if (!installedOnly || pack.state !== "not-installed") { return { key: packKey, pack: pack }; } } } // Tier 3: URL/reference contains match for (const packKey of Object.keys(nodePackages)) { const pack = nodePackages[packKey]; // Skip non-installed packages if installedOnly is true if (installedOnly && pack.state === "not-installed") { continue; } // Check if reference URL contains cnr_id if (pack.reference && pack.reference.includes(cnrId)) { return { key: packKey, pack: pack }; } // Check if any file URL contains cnr_id if (pack.files && Array.isArray(pack.files)) { for (const fileUrl of pack.files) { if (fileUrl.includes(cnrId)) { return { key: packKey, pack: pack }; } } } } return null; } export async function analyzeWorkflowUsage(nodePackages) { const result = await getWorkflowNodeTypes(); if (!result.success) { return { success: false, error: result.error }; } const workflowNodeList = result.data; const usageMap = new Map(); const workflowDetailsMap = new Map(); if (workflowNodeList && Array.isArray(workflowNodeList)) { const cnrIdCounts = new Map(); const cnrIdToWorkflows = new Map(); // Process each workflow workflowNodeList.forEach((workflowObj, workflowIndex) => { if (workflowObj.node_types && Array.isArray(workflowObj.node_types)) { const workflowCnrIds = new Set(); // Get workflow filename const workflowFilename = workflowObj.workflow_file_name || workflowObj.filename || workflowObj.file || workflowObj.name || workflowObj.path || `Workflow ${workflowIndex + 1}`; // Count nodes per cnr_id in this workflow const workflowCnrIdCounts = new Map(); workflowObj.node_types.forEach(nodeTypeObj => { const cnrId = nodeTypeObj.cnr_id; if (cnrId && cnrId !== "comfy-core") { // Track unique cnr_ids per workflow workflowCnrIds.add(cnrId); // Count nodes per cnr_id in this specific workflow const workflowNodeCount = workflowCnrIdCounts.get(cnrId) || 0; workflowCnrIdCounts.set(cnrId, workflowNodeCount + 1); } }); // Record workflow details for each unique cnr_id found in this workflow workflowCnrIds.forEach(cnrId => { // Count occurrences of this cnr_id across all workflows const currentCount = cnrIdCounts.get(cnrId) || 0; cnrIdCounts.set(cnrId, currentCount + 1); // Track workflow details if (!cnrIdToWorkflows.has(cnrId)) { cnrIdToWorkflows.set(cnrId, []); } cnrIdToWorkflows.get(cnrId).push({ filename: workflowFilename, nodeCount: workflowCnrIdCounts.get(cnrId) || 0 }); }); } }); // Map cnr_id to installed packages with workflow details cnrIdCounts.forEach((count, cnrId) => { const workflowDetails = cnrIdToWorkflows.get(cnrId) || []; const foundPackage = findPackageByCnrId(cnrId, nodePackages, true); if (foundPackage) { usageMap.set(foundPackage.key, count); workflowDetailsMap.set(foundPackage.key, workflowDetails); } }); } return { success: true, usageMap: usageMap, workflowDetailsMap: workflowDetailsMap }; } // Size formatting utilities - consolidated from model-manager.js and node-usage-analyzer.js export function formatSize(v) { const base = 1000; const units = ['', 'K', 'M', 'G', 'T', 'P']; const space = ''; const postfix = 'B'; if (v <= 0) { return `0${space}${postfix}`; } for (let i = 0, l = units.length; i < l; i++) { const min = Math.pow(base, i); const max = Math.pow(base, i + 1); if (v > min && v <= max) { const unit = units[i]; if (unit) { const n = v / min; const nl = n.toString().split('.')[0].length; const fl = Math.max(3 - nl, 1); v = n.toFixed(fl); } v = v + space + unit + postfix; break; } } return v; } // for size sort export function sizeToBytes(v) { if (typeof v === "number") { return v; } if (typeof v === "string") { const n = parseFloat(v); const unit = v.replace(/[0-9.B]+/g, "").trim().toUpperCase(); if (unit === "K") { return n * 1000; } if (unit === "M") { return n * 1000 * 1000; } if (unit === "G") { return n * 1000 * 1000 * 1000; } if (unit === "T") { return n * 1000 * 1000 * 1000 * 1000; } } return v; } // Flyover component - consolidated from custom-nodes-manager.js and node-usage-analyzer.js export function createFlyover(container, options = {}) { const { enableHover = false, hoverHandler = null, context = null } = options; const $flyover = document.createElement("div"); $flyover.className = "cn-flyover"; $flyover.innerHTML = `
${icons.arrowRight}
${icons.close}
` container.appendChild($flyover); const $flyoverTitle = $flyover.querySelector(".cn-flyover-title"); const $flyoverBody = $flyover.querySelector(".cn-flyover-body"); let width = '50%'; let visible = false; let timeHide; const closeHandler = (e) => { if ($flyover === e.target || $flyover.contains(e.target)) { return; } clearTimeout(timeHide); timeHide = setTimeout(() => { flyover.hide(); }, 100); } const displayHandler = () => { if (visible) { $flyover.classList.remove("cn-slide-in-right"); } else { $flyover.classList.remove("cn-slide-out-right"); $flyover.style.width = '0px'; $flyover.style.display = "none"; } } const flyover = { show: (titleHtml, bodyHtml) => { clearTimeout(timeHide); if (context && context.element) { context.element.removeEventListener("click", closeHandler); } $flyoverTitle.innerHTML = titleHtml; $flyoverBody.innerHTML = bodyHtml; $flyover.style.display = "block"; $flyover.style.width = width; if(!visible) { $flyover.classList.add("cn-slide-in-right"); } visible = true; setTimeout(() => { if (context && context.element) { context.element.addEventListener("click", closeHandler); } }, 100); }, hide: (now) => { visible = false; if (context && context.element) { context.element.removeEventListener("click", closeHandler); } if(now) { displayHandler(); return; } $flyover.classList.add("cn-slide-out-right"); } } $flyover.addEventListener("animationend", (e) => { displayHandler(); }); // Add hover handlers if enabled if (enableHover && hoverHandler) { $flyover.addEventListener("mouseenter", hoverHandler, true); $flyover.addEventListener("mouseleave", hoverHandler, true); } $flyover.addEventListener("click", (e) => { if(e.target.classList.contains("cn-flyover-close")) { flyover.hide(); return; } // Forward other click events to the provided handler or context if (context && context.handleFlyoverClick) { context.handleFlyoverClick(e); } }); return flyover; } // Shared UI State Methods - consolidated from multiple managers export function createUIStateManager(element, selectors) { return { showSelection: (msg) => { const el = element.querySelector(selectors.selection); if (el) el.innerHTML = msg; }, showError: (err) => { const el = element.querySelector(selectors.message); if (el) { const msg = err ? `${err}` : ""; el.innerHTML = msg; } }, showMessage: (msg, color) => { const el = element.querySelector(selectors.message); if (el) { if (color) { msg = `${msg}`; } el.innerHTML = msg; } }, showStatus: (msg, color) => { const el = element.querySelector(selectors.status); if (el) { if (color) { msg = `${msg}`; } el.innerHTML = msg; } }, showLoading: (grid) => { if (grid) { grid.showLoading(); grid.showMask({ opacity: 0.05 }); } }, hideLoading: (grid) => { if (grid) { grid.hideLoading(); grid.hideMask(); } }, showRefresh: () => { const el = element.querySelector(selectors.refresh); if (el) el.style.display = "block"; }, showStop: () => { const el = element.querySelector(selectors.stop); if (el) el.style.display = "block"; }, hideStop: () => { const el = element.querySelector(selectors.stop); if (el) el.style.display = "none"; } }; } initTooltip();