388 lines
12 KiB
JavaScript
388 lines
12 KiB
JavaScript
import { renderIcons } from './icons.js';
|
|
|
|
/**
|
|
* Format processor name: convert snake_case to Title Case
|
|
* @param {string} name - Processor name in snake_case
|
|
* @returns {string} Formatted name in Title Case
|
|
*/
|
|
export function formatProcessorName(name) {
|
|
return name
|
|
.split('_')
|
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
.join(' ');
|
|
}
|
|
|
|
/**
|
|
* Debounce function execution
|
|
* @param {Function} func - Function to debounce
|
|
* @param {number} wait - Delay in milliseconds
|
|
* @returns {Function} Debounced function
|
|
*/
|
|
export function debounce(func, wait) {
|
|
let timeout;
|
|
return function executedFunction(...args) {
|
|
const later = () => {
|
|
clearTimeout(timeout);
|
|
func(...args);
|
|
};
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Debouncer class for managing multiple debounced operations
|
|
*/
|
|
export class Debouncer {
|
|
constructor() {
|
|
this.timers = new Map();
|
|
}
|
|
|
|
debounce(key, fn, delay = 300) {
|
|
if (this.timers.has(key)) {
|
|
clearTimeout(this.timers.get(key));
|
|
}
|
|
const timer = setTimeout(() => {
|
|
this.timers.delete(key);
|
|
fn();
|
|
}, delay);
|
|
this.timers.set(key, timer);
|
|
}
|
|
|
|
cancel(key) {
|
|
if (this.timers.has(key)) {
|
|
clearTimeout(this.timers.get(key));
|
|
this.timers.delete(key);
|
|
}
|
|
}
|
|
|
|
cancelAll() {
|
|
this.timers.forEach(clearTimeout);
|
|
this.timers.clear();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Request Guard: prevents concurrent execution of the same operation
|
|
*/
|
|
export class RequestGuard {
|
|
constructor() {
|
|
this.locks = new Set();
|
|
}
|
|
|
|
isLocked(key) {
|
|
return this.locks.has(key);
|
|
}
|
|
|
|
async runExclusive(key, fn) {
|
|
if (this.isLocked(key)) {
|
|
return;
|
|
}
|
|
this.locks.add(key);
|
|
try {
|
|
return await fn();
|
|
} finally {
|
|
this.locks.delete(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Button State Manager: manages button states (loading, disabled, normal)
|
|
*/
|
|
export class ButtonState {
|
|
static set(element, { state = 'normal', icon = '', text = '', disabled = false }) {
|
|
if (!element) return;
|
|
|
|
switch (state) {
|
|
case 'loading':
|
|
element.disabled = true;
|
|
element.innerHTML = `<span data-icon="${icon || 'loader'}"></span> ${text || 'Загрузка...'}`;
|
|
break;
|
|
case 'disabled':
|
|
element.disabled = true;
|
|
element.innerHTML = icon ? `<span data-icon="${icon}"></span> ${text}` : text;
|
|
break;
|
|
case 'normal':
|
|
default:
|
|
element.disabled = !!disabled;
|
|
element.innerHTML = icon ? `<span data-icon="${icon}"></span> ${text}` : text;
|
|
break;
|
|
}
|
|
|
|
renderIcons(element);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set button to disabled state during request
|
|
* @param {HTMLElement} button - Button element
|
|
* @param {boolean} loading - Whether to disable button
|
|
* @param {Object} originalState - Original button state to restore
|
|
*/
|
|
export function setButtonLoading(button, loading, originalState = {}) {
|
|
if (!button) return originalState;
|
|
|
|
if (loading) {
|
|
if (!originalState.disabled) {
|
|
originalState.disabled = button.disabled;
|
|
}
|
|
button.disabled = true;
|
|
} else {
|
|
button.disabled = originalState.disabled || false;
|
|
}
|
|
|
|
return originalState;
|
|
}
|
|
|
|
/**
|
|
* Create parameter control HTML for processor settings
|
|
* @param {Object} param - Parameter configuration
|
|
* @param {string} processorId - Processor ID
|
|
* @param {string} idPrefix - Prefix for element IDs
|
|
* @returns {string} HTML string for parameter control
|
|
*/
|
|
export function createParameterControl(param, processorId, idPrefix = 'param') {
|
|
const paramId = `${idPrefix}_${processorId}_${param.name}`;
|
|
const value = param.value;
|
|
const opts = param.options || {};
|
|
|
|
switch (param.type) {
|
|
case 'slider':
|
|
case 'range':
|
|
return `
|
|
<div class="chart-setting" data-param="${param.name}">
|
|
<label class="chart-setting__label">
|
|
${param.label}
|
|
<span class="chart-setting__value">${value}</span>
|
|
</label>
|
|
<input
|
|
type="range"
|
|
class="chart-setting__slider"
|
|
id="${paramId}"
|
|
min="${opts.min ?? 0}"
|
|
max="${opts.max ?? 100}"
|
|
step="${opts.step ?? 1}"
|
|
value="${value}"
|
|
>
|
|
</div>
|
|
`;
|
|
|
|
case 'toggle':
|
|
case 'boolean':
|
|
return `
|
|
<div class="chart-setting" data-param="${param.name}">
|
|
<label class="chart-setting__toggle">
|
|
<input type="checkbox" id="${paramId}" ${value ? 'checked' : ''}>
|
|
<span class="chart-setting__toggle-slider"></span>
|
|
<span class="chart-setting__label">${param.label}</span>
|
|
</label>
|
|
</div>
|
|
`;
|
|
|
|
case 'select':
|
|
case 'dropdown': {
|
|
let choices = [];
|
|
if (Array.isArray(opts)) choices = opts;
|
|
else if (Array.isArray(opts.choices)) choices = opts.choices;
|
|
|
|
const optionsHtml = choices.map(option =>
|
|
`<option value="${option}" ${option === value ? 'selected' : ''}>${option}</option>`
|
|
).join('');
|
|
|
|
return `
|
|
<div class="chart-setting" data-param="${param.name}">
|
|
<label class="chart-setting__label">${param.label}</label>
|
|
<select class="chart-setting__select" id="${paramId}">
|
|
${optionsHtml}
|
|
</select>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
case 'button':
|
|
const buttonText = param.label || 'Нажать';
|
|
const actionDesc = opts.action ? `title="${opts.action}"` : '';
|
|
return `
|
|
<div class="chart-setting" data-param="${param.name}">
|
|
<button
|
|
type="button"
|
|
class="chart-setting__button"
|
|
id="${paramId}"
|
|
data-processor="${processorId}"
|
|
data-param="${param.name}"
|
|
${actionDesc}
|
|
>
|
|
${buttonText}
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
default:
|
|
return `
|
|
<div class="chart-setting" data-param="${param.name}">
|
|
<label class="chart-setting__label">${param.label}</label>
|
|
<input
|
|
type="text"
|
|
class="chart-setting__input"
|
|
id="${paramId}"
|
|
value="${value ?? ''}"
|
|
>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Safe clone object avoiding circular references
|
|
* @param {*} obj - Object to clone
|
|
* @param {WeakSet} seen - Set of already seen objects
|
|
* @returns {*} Cloned object
|
|
*/
|
|
export function safeClone(obj, seen = new WeakSet()) {
|
|
if (obj === null || typeof obj !== 'object') return obj;
|
|
if (seen.has(obj)) return '[Циклическая ссылка]';
|
|
|
|
seen.add(obj);
|
|
|
|
if (Array.isArray(obj)) {
|
|
return obj.map(item => safeClone(item, seen));
|
|
}
|
|
|
|
const cloned = {};
|
|
for (const key in obj) {
|
|
if (obj.hasOwnProperty(key)) {
|
|
try {
|
|
cloned[key] = safeClone(obj[key], seen);
|
|
} catch (e) {
|
|
cloned[key] = `[Ошибка: ${e.message}]`;
|
|
}
|
|
}
|
|
}
|
|
return cloned;
|
|
}
|
|
|
|
/**
|
|
* Download JSON data as a file
|
|
* @param {Object} data - Data to download
|
|
* @param {string} filename - Filename for download
|
|
*/
|
|
export function downloadJSON(data, filename) {
|
|
const jsonString = JSON.stringify(data, null, 2);
|
|
const blob = new Blob([jsonString], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = filename;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
/**
|
|
* Escape HTML to prevent XSS attacks
|
|
* @param {string} unsafe - Unsafe string
|
|
* @returns {string} Escaped string
|
|
*/
|
|
export function escapeHtml(unsafe) {
|
|
if (typeof unsafe !== 'string') return '';
|
|
return unsafe
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
/**
|
|
* Format bytes to human readable format
|
|
* @param {number} bytes - Number of bytes
|
|
* @returns {string} Formatted string
|
|
*/
|
|
export function formatBytes(bytes) {
|
|
if (bytes === 0) return '0 байт';
|
|
|
|
const k = 1024;
|
|
const sizes = ['байт', 'КБ', 'МБ', 'ГБ'];
|
|
const i = Math.min(sizes.length - 1, Math.floor(Math.log(bytes) / Math.log(k)));
|
|
|
|
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
|
}
|
|
|
|
/**
|
|
* Show a confirmation dialog with custom options
|
|
* @param {Object} options - Dialog options
|
|
* @param {string} options.title - Dialog title
|
|
* @param {string} options.message - Dialog message
|
|
* @param {Array<Object>} options.buttons - Array of button configurations
|
|
* @returns {Promise<string>} Resolves with the button value that was clicked
|
|
*/
|
|
export function showConfirmDialog({ title, message, buttons }) {
|
|
return new Promise((resolve) => {
|
|
// Create modal structure
|
|
const modal = document.createElement('div');
|
|
modal.className = 'modal modal--active';
|
|
|
|
const buttonsHtml = buttons.map(btn =>
|
|
`<button class="btn ${btn.class || 'btn--secondary'}" data-value="${btn.value}">${btn.text}</button>`
|
|
).join('');
|
|
|
|
modal.innerHTML = `
|
|
<div class="modal__backdrop" style="background-color: rgba(0, 0, 0, 0.85);"></div>
|
|
<div class="modal__content" style="background-color: #1e293b;">
|
|
<div class="modal__header">
|
|
<h3 class="modal__title">${escapeHtml(title)}</h3>
|
|
</div>
|
|
<div class="modal__body" style="padding: var(--space-6);">
|
|
<p style="margin: 0; color: var(--color-text-secondary);">${escapeHtml(message)}</p>
|
|
</div>
|
|
<div style="padding: var(--space-6); padding-top: 0; display: flex; gap: var(--space-3); justify-content: flex-end;">
|
|
${buttonsHtml}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(modal);
|
|
|
|
// Handle button clicks
|
|
const handleClick = (e) => {
|
|
const button = e.target.closest('[data-value]');
|
|
if (button) {
|
|
const value = button.dataset.value;
|
|
cleanup();
|
|
resolve(value);
|
|
}
|
|
};
|
|
|
|
// Handle backdrop click
|
|
const handleBackdrop = (e) => {
|
|
if (e.target.classList.contains('modal__backdrop')) {
|
|
cleanup();
|
|
resolve(null);
|
|
}
|
|
};
|
|
|
|
// Handle escape key
|
|
const handleEscape = (e) => {
|
|
if (e.key === 'Escape') {
|
|
cleanup();
|
|
resolve(null);
|
|
}
|
|
};
|
|
|
|
const cleanup = () => {
|
|
modal.removeEventListener('click', handleClick);
|
|
modal.removeEventListener('click', handleBackdrop);
|
|
document.removeEventListener('keydown', handleEscape);
|
|
modal.remove();
|
|
};
|
|
|
|
modal.addEventListener('click', handleClick);
|
|
modal.addEventListener('click', handleBackdrop);
|
|
document.addEventListener('keydown', handleEscape);
|
|
});
|
|
}
|