CoolFace
Apppublic

fred-dev/comfy_ui_ali

sourceHugging Facemitupdated 1y agoView on Hugging Face
0likes
common.js654 linesDownload Raw Back to js
1import { app } from "../../scripts/app.js";2import { api } from "../../scripts/api.js";3import { $el, ComfyDialog } from "../../scripts/ui.js";4import { getBestPosition, getPositionStyle, getRect } from './popover-helper.js';5 6 7function internalCustomConfirm(message, confirmMessage, cancelMessage) {8	return new Promise((resolve) => {9		// transparent bg10		const modalOverlay = document.createElement('div');11		modalOverlay.style.position = 'fixed';12		modalOverlay.style.top = 0;13		modalOverlay.style.left = 0;14		modalOverlay.style.width = '100%';15		modalOverlay.style.height = '100%';16		modalOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';17		modalOverlay.style.display = 'flex';18		modalOverlay.style.alignItems = 'center';19		modalOverlay.style.justifyContent = 'center';20		modalOverlay.style.zIndex = '1101';21 22		// Modal window container (dark bg)23		const modalDialog = document.createElement('div');24		modalDialog.style.backgroundColor = '#333';25		modalDialog.style.padding = '20px';26		modalDialog.style.borderRadius = '4px';27		modalDialog.style.maxWidth = '400px';28		modalDialog.style.width = '80%';29		modalDialog.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.5)';30		modalDialog.style.color = '#fff';31 32		// Display message33		const modalMessage = document.createElement('p');34		modalMessage.textContent = message;35		modalMessage.style.margin = '0';36		modalMessage.style.padding = '0 0 20px';37		modalMessage.style.wordBreak = 'keep-all';38 39		// Button container40		const modalButtons = document.createElement('div');41		modalButtons.style.display = 'flex';42		modalButtons.style.justifyContent = 'flex-end';43 44		// Confirm button (green)45		const confirmButton = document.createElement('button');46		if(confirmMessage)47			confirmButton.textContent = confirmMessage;48		else49			confirmButton.textContent = 'Confirm';50		confirmButton.style.marginLeft = '10px';51		confirmButton.style.backgroundColor = '#28a745'; // green52		confirmButton.style.color = '#fff';53		confirmButton.style.border = 'none';54		confirmButton.style.padding = '6px 12px';55		confirmButton.style.borderRadius = '4px';56		confirmButton.style.cursor = 'pointer';57		confirmButton.style.fontWeight = 'bold';58 59		// Cancel button (red)60		const cancelButton = document.createElement('button');61		if(cancelMessage)62			cancelButton.textContent = cancelMessage;63		else64			cancelButton.textContent = 'Cancel';65 66		cancelButton.style.marginLeft = '10px';67		cancelButton.style.backgroundColor = '#dc3545'; // red68		cancelButton.style.color = '#fff';69		cancelButton.style.border = 'none';70		cancelButton.style.padding = '6px 12px';71		cancelButton.style.borderRadius = '4px';72		cancelButton.style.cursor = 'pointer';73		cancelButton.style.fontWeight = 'bold';74 75		const closeModal = () => {76			document.body.removeChild(modalOverlay);77		};78 79		confirmButton.addEventListener('click', () => {80			closeModal();81			resolve(true);82		});83 84		cancelButton.addEventListener('click', () => {85			closeModal();86			resolve(false);87		});88 89		modalButtons.appendChild(confirmButton);90		modalButtons.appendChild(cancelButton);91		modalDialog.appendChild(modalMessage);92		modalDialog.appendChild(modalButtons);93		modalOverlay.appendChild(modalDialog);94		document.body.appendChild(modalOverlay);95	});96}97 98export function show_message(msg) {99	app.ui.dialog.show(msg);100	app.ui.dialog.element.style.zIndex = 1100;101}102 103export async function sleep(ms) {104	return new Promise(resolve => setTimeout(resolve, ms));105}106 107export async function customConfirm(message) {108	try {109		let res = await110			window['app'].extensionManager.dialog111			.confirm({112				title: 'Confirm',113				message: message114			});115 116		return res;117	}118	catch {119		let res = await internalCustomConfirm(message);120		return res;121	}122}123 124 125export function customAlert(message) {126	try {127		window['app'].extensionManager.toast.addAlert(message);128	}129	catch {130		alert(message);131	}132}133 134export function infoToast(summary, message) {135	try {136		app.extensionManager.toast.add({137			severity: 'info',138			summary: summary,139			detail: message,140			life: 3000141		})142	}143	catch {144		// do nothing145	}146}147 148 149export async function customPrompt(title, message) {150	try {151		let res = await152				window['app'].extensionManager.dialog153				.prompt({154					title: title,155					message: message156				});157 158		return res;159	}160	catch {161		return prompt(title, message)162	}163}164 165 166export function rebootAPI() {167	if ('electronAPI' in window) {168			window.electronAPI.restartApp();169			return true;170	}171 172	customConfirm("Are you sure you'd like to reboot the server?").then((isConfirmed) => {173		if (isConfirmed) {174			try {175				api.fetchApi("/manager/reboot");176			}177			catch(exception) {}178		}179	});180 181	return false;182}183 184 185export var manager_instance = null;186 187export function setManagerInstance(obj) {188	manager_instance = obj;189}190 191export function showToast(message, duration = 3000) {192	const toast = $el("div.comfy-toast", {textContent: message});193	document.body.appendChild(toast);194	setTimeout(() => {195		toast.classList.add("comfy-toast-fadeout");196		setTimeout(() => toast.remove(), 500);197	}, duration);198}199 200function isValidURL(url) {201	if(url.includes('&'))202		return false;203 204	const http_pattern = /^(https?|ftp):\/\/[^\s$?#]+$/;205	const ssh_pattern = /^(.+@|ssh:\/\/).+:.+$/;206	return http_pattern.test(url) || ssh_pattern.test(url);207}208 209export async function install_pip(packages) {210	if(packages.includes('&'))211		app.ui.dialog.show(`Invalid PIP package enumeration: '${packages}'`);212 213	const res = await api.fetchApi("/customnode/install/pip", {214		method: "POST",215		body: packages,216	});217 218	if(res.status == 403) {219		show_message('This action is not allowed with this security level configuration.');220		return;221	}222 223	if(res.status == 200) {224		show_message(`PIP package installation is processed.<br>To apply the pip packages, please click the <button id='cm-reboot-button3'><font size='3px'>RESTART</font></button> button in ComfyUI.`);225 226		const rebootButton = document.getElementById('cm-reboot-button3');227		const self = this;228 229		rebootButton.addEventListener("click", rebootAPI);230	}231	else {232		show_message(`Failed to install '${packages}'<BR>See terminal log.`);233	}234}235 236export async function install_via_git_url(url, manager_dialog) {237	if(!url) {238		return;239	}240 241	if(!isValidURL(url)) {242		show_message(`Invalid Git url '${url}'`);243		return;244	}245 246	show_message(`Wait...<BR><BR>Installing '${url}'`);247 248	const res = await api.fetchApi("/customnode/install/git_url", {249		method: "POST",250		body: url,251	});252 253	if(res.status == 403) {254		show_message('This action is not allowed with this security level configuration.');255		return;256	}257 258	if(res.status == 200) {259		show_message(`'${url}' is installed<BR>To apply the installed custom node, please <button id='cm-reboot-button4'><font size='3px'>RESTART</font></button> ComfyUI.`);260 261		const rebootButton = document.getElementById('cm-reboot-button4');262		const self = this;263 264		rebootButton.addEventListener("click",265			function() {266				if(rebootAPI()) {267					manager_dialog.close();268				}269			});270	}271	else {272		show_message(`Failed to install '${url}'<BR>See terminal log.`);273	}274}275 276export async function free_models(free_execution_cache) {277	try {278		let mode = "";279		if(free_execution_cache) {280			mode = '{"unload_models": true, "free_memory": true}';281		}282		else {283			mode = '{"unload_models": true}';284		}285 286		let res = await api.fetchApi(`/free`, {287			method: 'POST',288			headers: { 'Content-Type': 'application/json' },289			body: mode290		});291 292		if (res.status == 200) {293			if(free_execution_cache) {294				showToast("'Models' and 'Execution Cache' have been cleared.", 3000);295			}296			else {297				showToast("Models' have been unloaded.", 3000);298			}299		} else {300			showToast('Unloading of models failed. Installed ComfyUI may be an outdated version.', 5000);301		}302	} catch (error) {303		showToast('An error occurred while trying to unload models.', 5000);304	}305}306 307export function md5(inputString) {308	const hc = '0123456789abcdef';309	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;}310	const ad = (x,y) => {let l=(x&0xFFFF)+(y&0xFFFF);let m=(x>>16)+(y>>16)+(l>>16);return (m<<16)|(l&0xFFFF);}311	const rl = (n,c) => (n<<c)|(n>>>(32-c));312	const cm = (q,a,b,x,s,t) => ad(rl(ad(ad(a,q),ad(x,t)),s),b);313	const ff = (a,b,c,d,x,s,t) => cm((b&c)|((~b)&d),a,b,x,s,t);314	const gg = (a,b,c,d,x,s,t) => cm((b&d)|(c&(~d)),a,b,x,s,t);315	const hh = (a,b,c,d,x,s,t) => cm(b^c^d,a,b,x,s,t);316	const ii = (a,b,c,d,x,s,t) => cm(c^(b|(~d)),a,b,x,s,t);317	const sb = x => {318	let i;const nblk=((x.length+8)>>6)+1;const blks=[];for(i=0;i<nblk*16;i++) { blks[i]=0 };319	for(i=0;i<x.length;i++) {blks[i>>2]|=x.charCodeAt(i)<<((i%4)*8);}320		blks[i>>2]|=0x80<<((i%4)*8);blks[nblk*16-2]=x.length*8;return blks;321	}322	let i,x=sb(inputString),a=1732584193,b=-271733879,c=-1732584194,d=271733878,olda,oldb,oldc,oldd;323	for(i=0;i<x.length;i+=16) {olda=a;oldb=b;oldc=c;oldd=d;324		a=ff(a,b,c,d,x[i+ 0], 7, -680876936);d=ff(d,a,b,c,x[i+ 1],12, -389564586);c=ff(c,d,a,b,x[i+ 2],17,  606105819);325		b=ff(b,c,d,a,x[i+ 3],22,-1044525330);a=ff(a,b,c,d,x[i+ 4], 7, -176418897);d=ff(d,a,b,c,x[i+ 5],12, 1200080426);326		c=ff(c,d,a,b,x[i+ 6],17,-1473231341);b=ff(b,c,d,a,x[i+ 7],22,  -45705983);a=ff(a,b,c,d,x[i+ 8], 7, 1770035416);327		d=ff(d,a,b,c,x[i+ 9],12,-1958414417);c=ff(c,d,a,b,x[i+10],17,     -42063);b=ff(b,c,d,a,x[i+11],22,-1990404162);328		a=ff(a,b,c,d,x[i+12], 7, 1804603682);d=ff(d,a,b,c,x[i+13],12,  -40341101);c=ff(c,d,a,b,x[i+14],17,-1502002290);329		b=ff(b,c,d,a,x[i+15],22, 1236535329);a=gg(a,b,c,d,x[i+ 1], 5, -165796510);d=gg(d,a,b,c,x[i+ 6], 9,-1069501632);330		c=gg(c,d,a,b,x[i+11],14,  643717713);b=gg(b,c,d,a,x[i+ 0],20, -373897302);a=gg(a,b,c,d,x[i+ 5], 5, -701558691);331		d=gg(d,a,b,c,x[i+10], 9,   38016083);c=gg(c,d,a,b,x[i+15],14, -660478335);b=gg(b,c,d,a,x[i+ 4],20, -405537848);332		a=gg(a,b,c,d,x[i+ 9], 5,  568446438);d=gg(d,a,b,c,x[i+14], 9,-1019803690);c=gg(c,d,a,b,x[i+ 3],14, -187363961);333		b=gg(b,c,d,a,x[i+ 8],20, 1163531501);a=gg(a,b,c,d,x[i+13], 5,-1444681467);d=gg(d,a,b,c,x[i+ 2], 9,  -51403784);334		c=gg(c,d,a,b,x[i+ 7],14, 1735328473);b=gg(b,c,d,a,x[i+12],20,-1926607734);a=hh(a,b,c,d,x[i+ 5], 4,    -378558);335		d=hh(d,a,b,c,x[i+ 8],11,-2022574463);c=hh(c,d,a,b,x[i+11],16, 1839030562);b=hh(b,c,d,a,x[i+14],23,  -35309556);336		a=hh(a,b,c,d,x[i+ 1], 4,-1530992060);d=hh(d,a,b,c,x[i+ 4],11, 1272893353);c=hh(c,d,a,b,x[i+ 7],16, -155497632);337		b=hh(b,c,d,a,x[i+10],23,-1094730640);a=hh(a,b,c,d,x[i+13], 4,  681279174);d=hh(d,a,b,c,x[i+ 0],11, -358537222);338		c=hh(c,d,a,b,x[i+ 3],16, -722521979);b=hh(b,c,d,a,x[i+ 6],23,   76029189);a=hh(a,b,c,d,x[i+ 9], 4, -640364487);339		d=hh(d,a,b,c,x[i+12],11, -421815835);c=hh(c,d,a,b,x[i+15],16,  530742520);b=hh(b,c,d,a,x[i+ 2],23, -995338651);340		a=ii(a,b,c,d,x[i+ 0], 6, -198630844);d=ii(d,a,b,c,x[i+ 7],10, 1126891415);c=ii(c,d,a,b,x[i+14],15,-1416354905);341		b=ii(b,c,d,a,x[i+ 5],21,  -57434055);a=ii(a,b,c,d,x[i+12], 6, 1700485571);d=ii(d,a,b,c,x[i+ 3],10,-1894986606);342		c=ii(c,d,a,b,x[i+10],15,   -1051523);b=ii(b,c,d,a,x[i+ 1],21,-2054922799);a=ii(a,b,c,d,x[i+ 8], 6, 1873313359);343		d=ii(d,a,b,c,x[i+15],10,  -30611744);c=ii(c,d,a,b,x[i+ 6],15,-1560198380);b=ii(b,c,d,a,x[i+13],21, 1309151649);344		a=ii(a,b,c,d,x[i+ 4], 6, -145523070);d=ii(d,a,b,c,x[i+11],10,-1120210379);c=ii(c,d,a,b,x[i+ 2],15,  718787259);345		b=ii(b,c,d,a,x[i+ 9],21, -343485551);a=ad(a,olda);b=ad(b,oldb);c=ad(c,oldc);d=ad(d,oldd);346	}347	return rh(a)+rh(b)+rh(c)+rh(d);348}349 350export async function fetchData(route, options) {351	let err;352	const res = await api.fetchApi(route, options).catch(e => {353		err = e;354	});355 356	if (!res) {357		return {358			status: 400,359			error: new Error("Unknown Error")360		}361	}362 363	const { status, statusText } = res;364	if (err) {365		return {366			status,367			error: err368		}369	}370 371	if (status !== 200) {372		return {373			status,374			error: new Error(statusText || "Unknown Error")375		}376	}377 378	const data = await res.json();379	if (!data) {380		return {381			status,382			error: new Error(`Failed to load data: ${route}`)383		}384	}385	return {386		status,387		data388	}389}390 391// https://cenfun.github.io/open-icons/392export const icons = {393	search: '<svg viewBox="0 0 24 24" width="100%" height="100%" pointer-events="none" xmlns="http://www.w3.org/2000/svg"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m21 21-4.486-4.494M19 10.5a8.5 8.5 0 1 1-17 0 8.5 8.5 0 0 1 17 0"/></svg>',394	conflicts: '<svg viewBox="0 0 400 400" width="100%" height="100%" pointer-events="none" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="m397.2 350.4.2-.2-180-320-.2.2C213.8 24.2 207.4 20 200 20s-13.8 4.2-17.2 10.4l-.2-.2-180 320 .2.2c-1.6 2.8-2.8 6-2.8 9.6 0 11 9 20 20 20h360c11 0 20-9 20-20 0-3.6-1.2-6.8-2.8-9.6M220 340h-40v-40h40zm0-60h-40V120h40z"/></svg>',395	passed: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 426.667 426.667"><path fill="#6AC259" d="M213.333,0C95.518,0,0,95.514,0,213.333s95.518,213.333,213.333,213.333c117.828,0,213.333-95.514,213.333-213.333S331.157,0,213.333,0z M174.199,322.918l-93.935-93.931l31.309-31.309l62.626,62.622l140.894-140.898l31.309,31.309L174.199,322.918z"/></svg>',396	download: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" width="100%" height="100%" viewBox="0 0 32 32"><path fill="currentColor" d="M26 24v4H6v-4H4v4a2 2 0 0 0 2 2h20a2 2 0 0 0 2-2v-4zm0-10l-1.41-1.41L17 20.17V2h-2v18.17l-7.59-7.58L6 14l10 10l10-10z"></path></svg>',397	close: '<svg xmlns="http://www.w3.org/2000/svg" pointer-events="none" width="100%" height="100%" viewBox="0 0 16 16"><g fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="m7.116 8-4.558 4.558.884.884L8 8.884l4.558 4.558.884-.884L8.884 8l4.558-4.558-.884-.884L8 7.116 3.442 2.558l-.884.884L7.116 8z"/></g></svg>',398	arrowRight: '<svg xmlns="http://www.w3.org/2000/svg" pointer-events="none" width="100%" height="100%" viewBox="0 0 20 20"><path fill="currentColor" fill-rule="evenodd" d="m2.542 2.154 7.254 7.26c.136.14.204.302.204.483a.73.73 0 0 1-.204.5l-7.575 7.398c-.383.317-.724.317-1.022 0-.299-.317-.299-.643 0-.98l7.08-6.918-6.754-6.763c-.237-.343-.215-.654.066-.935.281-.28.598-.295.951-.045Zm9 0 7.254 7.26c.136.14.204.302.204.483a.73.73 0 0 1-.204.5l-7.575 7.398c-.383.317-.724.317-1.022 0-.299-.317-.299-.643 0-.98l7.08-6.918-6.754-6.763c-.237-.343-.215-.654.066-.935.281-.28.598-.295.951-.045Z"/></svg>'399}400 401export function sanitizeHTML(str) {402	return str403		.replace(/&/g, "&amp;")404		.replace(/</g, "&lt;")405		.replace(/>/g, "&gt;")406		.replace(/"/g, "&quot;")407		.replace(/'/g, "&#039;");408}409 410export function showTerminal() {411	try {412		const panel = app.extensionManager.bottomPanel;413		const isTerminalVisible = panel.bottomPanelVisible && panel.activeBottomPanelTab.id === 'logs-terminal';414		if (!isTerminalVisible)415			panel.toggleBottomPanelTab('logs-terminal');416	}417	catch(exception) {418		// do nothing419	}420}421 422let need_restart = false;423 424export function setNeedRestart(value) {425	need_restart = value;426}427 428async function onReconnected(event) {429	if(need_restart) {430		setNeedRestart(false);431 432		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?");433		if (!confirmed) {434			return;435		}436 437		window.location.reload(true);438	}439}440 441api.addEventListener('reconnected', onReconnected);442 443const storeId = "comfyui-manager-grid";444let timeId;445export function storeColumnWidth(gridId, columnItem) {446	clearTimeout(timeId);447	timeId = setTimeout(() => {448		let data = {};449		const dataStr = localStorage.getItem(storeId);450		if (dataStr) {451			try {452				data = JSON.parse(dataStr);453			} catch (e) {}454		}455 456		if (!data[gridId]) {457			data[gridId] =  {};458		}459 460		data[gridId][columnItem.id] = columnItem.width;461 462		localStorage.setItem(storeId, JSON.stringify(data));463 464	}, 200)465}466 467export function restoreColumnWidth(gridId, columns) {468	const dataStr = localStorage.getItem(storeId);469	if (!dataStr) {470		return;471	}472	let data;473	try {474		data = JSON.parse(dataStr);475	} catch (e) {}476	if(!data) {477		return;478	}479	const widthMap = data[gridId];480	if (!widthMap) {481		return;482	}483 484	columns.forEach(columnItem => {485		const w = widthMap[columnItem.id];486		if (w) {487			columnItem.width = w;488		}489	});490 491}492 493export function getTimeAgo(dateStr) {494	const date = new Date(dateStr);495 496	if (!date || !(date instanceof Date) || isNaN(date.getTime())) {497		return "";498	}499 500	const units = [501		{ max: 2760000, value: 60000, name: 'minute', past: 'a minute ago', future: 'in a minute' },502		{ max: 72000000, value: 3600000, name: 'hour', past: 'an hour ago', future: 'in an hour' },503		{ max: 518400000, value: 86400000, name: 'day', past: 'yesterday', future: 'tomorrow' },504		{ max: 2419200000, value: 604800000, name: 'week', past: 'last week', future: 'in a week' },505		{ max: 28512000000, value: 2592000000, name: 'month', past: 'last month', future: 'in a month' }506	];507    const diff = Date.now() - date.getTime();508    // less than a minute509    if (Math.abs(diff) < 60000)510        return 'just now';511    for (let i = 0; i < units.length; i++) {512        if (Math.abs(diff) < units[i].max) {513            return format(diff, units[i].value, units[i].name, units[i].past, units[i].future, diff < 0);514        }515    }516    function format(diff, divisor, unit, past, future, isInTheFuture) {517		const val = Math.round(Math.abs(diff) / divisor);518		if (isInTheFuture)519			return val <= 1 ? future : 'in ' + val + ' ' + unit + 's';520		return val <= 1 ? past : val + ' ' + unit + 's ago';521	}522    return format(diff, 31536000000, 'year', 'last year', 'in a year', diff < 0);523};524 525export const loadCss = (cssFile) => {526	const cssPath = import.meta.resolve(cssFile);527	//console.log(cssPath);528	const $link = document.createElement("link");529	$link.setAttribute("rel", 'stylesheet');530	$link.setAttribute("href", cssPath);531	document.head.appendChild($link);532};533 534export const copyText = (text) => {535	return new Promise((resolve) => {536		let err;537		try {538			navigator.clipboard.writeText(text);539		} catch (e) {540			err = e;541		}542		if (err) {543			resolve(false);544		} else {545			resolve(true);546		}547	});548};549 550function renderPopover($elem, target, options = {}) {551	// async microtask552	queueMicrotask(() => {553		554		const containerRect = getRect(window);555		const targetRect = getRect(target);556		const elemRect = getRect($elem);557 558		const positionInfo = getBestPosition(559			containerRect,560			targetRect,561			elemRect,562			options.positions563		);564		const style = getPositionStyle(positionInfo, {565			bgColor: options.bgColor,566			borderColor: options.borderColor,567			borderRadius: options.borderRadius568		});569 570		$elem.style.top = positionInfo.top + "px";571		$elem.style.left = positionInfo.left + "px";572		$elem.style.background = style.background;573	574	});575}576 577let $popover;578export function hidePopover() {579	if ($popover) {580		$popover.remove();581		$popover = null;582	}583}584export function showPopover(target, text, className, options) {585	hidePopover();586	$popover = document.createElement("div");587	$popover.className = ['cn-popover', className].filter(it => it).join(" ");588	document.body.appendChild($popover);589	$popover.innerHTML = text;590	$popover.style.display = "block";591	renderPopover($popover, target, {592		borderRadius: 10,593		... options594	});595}596 597let $tooltip;598export function hideTooltip(target) {599	if ($tooltip) {600		$tooltip.style.display = "none";601		$tooltip.innerHTML = "";602		$tooltip.style.top = "0px";603		$tooltip.style.left = "0px";604	}605}606export function showTooltip(target, text, className = 'cn-tooltip', styleMap = {}) {607	if (!$tooltip) {608		$tooltip = document.createElement("div");609		$tooltip.className = className;610		$tooltip.style.cssText = `611			pointer-events: none;612			position: fixed;613			z-index: 10001;614			padding: 20px;615			color: #1e1e1e;616			max-width: 350px;617			filter: drop-shadow(1px 5px 5px rgb(0 0 0 / 30%));618			${Object.keys(styleMap).map(k=>k+":"+styleMap[k]+";").join("")}619		`;620		document.body.appendChild($tooltip);621	}622 623	$tooltip.innerHTML = text;624	$tooltip.style.display = "block";625	renderPopover($tooltip, target, {626		positions: ['top', 'bottom', 'right', 'center'],627		bgColor: "#ffffff",628		borderColor: "#cccccc",629		borderRadius: 5630	});631}632 633function initTooltip () {634	const mouseenterHandler = (e) => {635        const target = e.target;636        const text = target.getAttribute('tooltip');637        if (text) {638            showTooltip(target, text);639        }640    };641	const mouseleaveHandler = (e) => {642        const target = e.target;643        const text = target.getAttribute('tooltip');644        if (text) {645            hideTooltip(target);646        }647    };648	document.body.removeEventListener('mouseenter', mouseenterHandler, true);649	document.body.removeEventListener('mouseleave', mouseleaveHandler, true);650	document.body.addEventListener('mouseenter', mouseenterHandler, true);651    document.body.addEventListener('mouseleave', mouseleaveHandler, true);652}653 654initTooltip();