From e7313870c59825368cf213e205ed10fff9028606 Mon Sep 17 00:00:00 2001 From: svsptech Date: Mon, 18 May 2026 15:54:16 +0500 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=B2=20=C2=AB?= =?UTF-8?q?/=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crud_d.js | 911 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 911 insertions(+) create mode 100644 crud_d.js diff --git a/crud_d.js b/crud_d.js new file mode 100644 index 0000000..1b74ecd --- /dev/null +++ b/crud_d.js @@ -0,0 +1,911 @@ +(function() { + 'use strict'; + + // Хранилище зарегистрированных диалогов + const dialogs = new Map(); + // Хранилище активных экземпляров диалогов + const activeDialogs = new Map(); + + // Типы полей по умолчанию + const FIELD_TYPES = { + text: (name, params, defaultValue = '') => { + const container = dom.div().cls('mb-3'); + const label = dom.label().cls('form-label').atr('for', `field_${name}`).text(params.label || name); + const input = dom.input().cls('form-control').atr('type', 'text') + .atr('id', `field_${name}`).atr('name', name) + .atr('placeholder', params.placeholder || ''); + + if (params.required) input.atr('required', ''); + if (params.readonly) input.atr('readonly', ''); + if (params.maxlength) input.atr('maxlength', params.maxlength); + + container.child(label); + container.child(input); + + return { + element: container, + fieldName: name, + getValue: () => input.element.value, + setValue: (val) => { input.element.value = val || defaultValue; }, + validate: () => { + if (params.required && !input.element.value.trim()) { + return `${params.label || name} обязательно для заполнения`; + } + return null; + } + }; + }, + + number: (name, params, defaultValue = null) => { + const container = dom.div().cls('mb-3'); + const label = dom.label().cls('form-label').atr('for', `field_${name}`).text(params.label || name); + const input = dom.input().cls('form-control').atr('type', 'number') + .atr('id', `field_${name}`).atr('name', name); + + if (params.required) input.atr('required', ''); + if (params.min !== undefined) input.atr('min', params.min); + if (params.max !== undefined) input.atr('max', params.max); + if (params.step) input.atr('step', params.step); + + container.child(label); + container.child(input); + + return { + element: container, + fieldName: name, + getValue: () => { + const val = input.element.value; + return val ? Number(val) : null; + }, + setValue: (val) => { input.element.value = val !== null && val !== undefined ? val : ''; }, + validate: () => { + if (params.required && input.element.value === '') { + return `${params.label || name} обязательно для заполнения`; + } + return null; + } + }; + }, + + email: (name, params, defaultValue = '') => { + const container = dom.div().cls('mb-3'); + const label = dom.label().cls('form-label').atr('for', `field_${name}`).text(params.label || name); + const input = dom.input().cls('form-control').atr('type', 'email') + .atr('id', `field_${name}`).atr('name', name) + .atr('placeholder', params.placeholder || ''); + + if (params.required) input.atr('required', ''); + + container.child(label); + container.child(input); + + return { + element: container, + fieldName: name, + getValue: () => input.element.value, + setValue: (val) => { input.element.value = val || defaultValue; }, + validate: () => { + if (params.required && !input.element.value.trim()) { + return `${params.label || name} обязательно для заполнения`; + } + if (input.element.value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input.element.value)) { + return 'Неверный формат email'; + } + return null; + } + }; + }, + + textarea: (name, params, defaultValue = '') => { + const container = dom.div().cls('mb-3'); + const label = dom.label().cls('form-label').atr('for', `field_${name}`).text(params.label || name); + const textarea = dom.textarea().cls('form-control') + .atr('id', `field_${name}`).atr('name', name) + .atr('rows', params.rows || 3); + + if (params.required) textarea.atr('required', ''); + + container.child(label); + container.child(textarea); + + return { + element: container, + fieldName: name, + getValue: () => textarea.element.value, + setValue: (val) => { textarea.element.value = val || defaultValue; }, + validate: () => { + if (params.required && !textarea.element.value.trim()) { + return `${params.label || name} обязательно для заполнения`; + } + return null; + } + }; + }, + + select: (name, params, defaultValue = null) => { + const container = dom.div().cls('mb-3'); + const label = dom.label().cls('form-label').atr('for', `field_${name}`).text(params.label || name); + const select = dom.select().cls('form-select') + .atr('id', `field_${name}`).atr('name', name); + + if (params.required) select.atr('required', ''); + + if (params.options) { + params.options.forEach(option => { + const opt = dom.option() + .atr('value', option.value) + .text(option.label); + select.child(opt); + }); + } + + container.child(label); + container.child(select); + + return { + element: container, + fieldName: name, + getValue: () => select.element.value, + setValue: (val) => { + select.element.value = val || defaultValue || ''; + }, + validate: () => { + if (params.required && !select.element.value) { + return `${params.label || name} обязательно для заполнения`; + } + return null; + } + }; + }, + + checkbox: (name, params, defaultValue = false) => { + const container = dom.div().cls('mb-3 form-check'); + const input = dom.input().cls('form-check-input').atr('type', 'checkbox') + .atr('id', `field_${name}`).atr('name', name); + + if (params.required) input.atr('required', ''); + + const label = dom.label().cls('form-check-label').atr('for', `field_${name}`).text(params.label || name); + + container.child(input); + container.child(label); + + return { + element: container, + fieldName: name, + getValue: () => input.element.checked, + setValue: (val) => { input.element.checked = val || defaultValue; }, + validate: () => null + }; + }, + + date: (name, params, defaultValue = '') => { + const container = dom.div().cls('mb-3'); + const label = dom.label().cls('form-label').atr('for', `field_${name}`).text(params.label || name); + const input = dom.input().cls('form-control').atr('type', 'date') + .atr('id', `field_${name}`).atr('name', name); + + if (params.required) input.atr('required', ''); + + container.child(label); + container.child(input); + + return { + element: container, + fieldName: name, + getValue: () => input.element.value, + setValue: (val) => { input.element.value = val || defaultValue; }, + validate: () => { + if (params.required && !input.element.value) { + return `${params.label || name} обязательно для заполнения`; + } + return null; + } + }; + }, + + password: (name, params, defaultValue = '') => { + const container = dom.div().cls('mb-3'); + const label = dom.label().cls('form-label').atr('for', `field_${name}`).text(params.label || name); + const input = dom.input().cls('form-control').atr('type', 'password') + .atr('id', `field_${name}`).atr('name', name) + .atr('placeholder', params.placeholder || ''); + + if (params.required) input.atr('required', ''); + + container.child(label); + container.child(input); + + return { + element: container, + fieldName: name, + getValue: () => input.element.value, + setValue: (val) => { input.element.value = val || defaultValue; }, + validate: () => { + if (params.required && !input.element.value) { + return `${params.label || name} обязательно для заполнения`; + } + return null; + } + }; + } + }; + + // Фабрика полей + function createField(name, fieldConfig) { + const [type, params, defaultValue] = fieldConfig; + + if (FIELD_TYPES[type]) { + return FIELD_TYPES[type](name, params, defaultValue); + } + + if (type === 'custom' && params instanceof HTMLElement) { + return { + element: params, + fieldName: name, + getValue: () => { + const input = params.querySelector('input, select, textarea'); + return input ? input.value : null; + }, + setValue: (val) => { + const input = params.querySelector('input, select, textarea'); + if (input) input.value = val; + }, + validate: () => null + }; + } + + console.warn(`Неизвестный тип поля: ${type}`); + return FIELD_TYPES.text(name, params, defaultValue); + } + + // Функция для расчета Bootstrap col класса + function getColClass(fieldsInRow) { + if (fieldsInRow === 1) return 'col-12'; + if (fieldsInRow === 2) return 'col-md-6'; + if (fieldsInRow === 3) return 'col-md-4'; + if (fieldsInRow === 4) return 'col-md-3'; + const colSize = Math.floor(12 / fieldsInRow); + return `col-md-${colSize}`; + } + + // Функция для рендеринга полей с учетом layout + function renderFieldsWithLayout(container, fields, layout, fieldInstances) { + const fieldNames = Object.keys(fields); + const usedFields = new Set(); + + if (layout && layout.length > 0) { + layout.forEach(rowFields => { + const row = dom.div().cls('row'); + const validFields = rowFields.filter(name => fields[name]); + const colClass = getColClass(validFields.length); + + validFields.forEach(fieldName => { + const field = createField(fieldName, fields[fieldName]); + fieldInstances[fieldName] = field; + usedFields.add(fieldName); + + const col = dom.div().cls(colClass); + const fieldElement = field.element; + if (fieldElement.element) { + fieldElement.element.classList.remove('mb-3'); + } + + col.child(fieldElement); + row.child(col); + }); + + container.child(row); + }); + } + + const remainingFields = fieldNames.filter(name => !usedFields.has(name)); + remainingFields.forEach(fieldName => { + const row = dom.div().cls('row'); + const col = dom.div().cls('col-12'); + + const field = createField(fieldName, fields[fieldName]); + fieldInstances[fieldName] = field; + + const fieldElement = field.element; + if (fieldElement.element) { + fieldElement.element.classList.remove('mb-3'); + } + + col.child(fieldElement); + row.child(col); + container.child(row); + }); + } + + // HTTP запросы + async function apiRequest(method, url, data = null) { + const options = { + method, + headers: { + 'Content-Type': 'application/json', + } + }; + + if (data && method !== 'GET') { + options.body = JSON.stringify(data); + } + + const response = await fetch(url, options); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.message || `Ошибка HTTP: ${response.status}`); + } + + return await response.json(); + } + + // Создание модального окна + function createModal(dialogId, title, fields, options = {}, mode = 'create', itemId = null) { + const modalId = `modal_${dialogId}_${Date.now()}`; + const { width = '', categories = null } = options; + const config = dialogs.get(dialogId); + + // Создаем структуру модального окна + const modal = dom.div().cls('modal fade').atr('id', modalId) + .atr('tabindex', '-1').atr('aria-hidden', 'true'); + + // Настройка ширины диалога + const dialog = dom.div().cls('modal-dialog'); + if (width) { + if (['sm', 'lg', 'xl'].includes(width)) { + dialog.cls(`modal-${width}`); + } else { + dialog.css(`max-width: ${width}`); + } + } + + const content = dom.div().cls('modal-content'); + + // Заголовок + const header = dom.div().cls('modal-header'); + const titleText = mode === 'create' ? title : `Редактирование: ${title}`; + const titleEl = dom.h5().cls('modal-title').text(titleText); + const closeBtn = dom.button().cls('btn-close').atr('data-bs-dismiss', 'modal').atr('aria-label', 'Close'); + header.child(titleEl); + header.child(closeBtn); + + // Тело + const body = dom.div().cls('modal-body'); + const alertContainer = dom.div().cls('alert alert-danger').atr('role', 'alert') + .css('display: none'); + body.child(alertContainer); + + // Индикатор загрузки + const loadingOverlay = dom.div().css('text-align: center; padding: 50px;'); + const spinner = dom.div().cls('spinner-border').atr('role', 'status'); + const loadingText = dom.span().cls('visually-hidden').text('Загрузка...'); + spinner.child(loadingText); + loadingOverlay.child(spinner); + body.child(loadingOverlay); + + // Создаем поля + const fieldInstances = {}; + + if (categories && Object.keys(categories).length > 0) { + const fieldsInCategories = new Set(); + Object.values(categories).forEach(layout => { + layout.forEach(row => { + row.forEach(fieldName => fieldsInCategories.add(fieldName)); + }); + }); + + const remainingFields = {}; + Object.keys(fields).forEach(fieldName => { + if (!fieldsInCategories.has(fieldName)) { + remainingFields[fieldName] = fields[fieldName]; + } + }); + + let finalCategories = { ...categories }; + if (Object.keys(categories).length === 1 && Object.keys(remainingFields).length > 0) { + finalCategories['Дополнительно'] = Object.keys(remainingFields).map(name => [name]); + } else if (Object.keys(remainingFields).length > 0) { + finalCategories['Дополнительно'] = Object.keys(remainingFields).map(name => [name]); + } + + const categoryNames = Object.keys(finalCategories); + + const tabNav = dom.ul().cls('nav nav-tabs').atr('role', 'tablist'); + const tabContent = dom.div().cls('tab-content'); + + categoryNames.forEach((catName, index) => { + const tabId = `tab_${dialogId}_${catName.replace(/[^a-zA-Z0-9]/g, '_')}`; + const isActive = index === 0; + + const navItem = dom.li().cls('nav-item').atr('role', 'presentation'); + const navLink = dom.button() + .cls(`nav-link ${isActive ? 'active' : ''}`) + .atr('id', `${tabId}-tab`) + .atr('data-bs-toggle', 'tab') + .atr('data-bs-target', `#${tabId}`) + .atr('type', 'button') + .atr('role', 'tab') + .atr('aria-selected', isActive ? 'true' : 'false') + .text(catName); + navItem.child(navLink); + tabNav.child(navItem); + + const tabPane = dom.div() + .cls(`tab-pane fade ${isActive ? 'show active' : ''}`) + .atr('id', tabId) + .atr('role', 'tabpanel') + .atr('aria-labelledby', `${tabId}-tab`); + + const catLayout = finalCategories[catName]; + const catFields = {}; + + const catFieldNames = new Set(); + catLayout.forEach(row => { + row.forEach(fieldName => { + if (fields[fieldName]) { + catFieldNames.add(fieldName); + catFields[fieldName] = fields[fieldName]; + } + }); + }); + + renderFieldsWithLayout(tabPane, catFields, catLayout, fieldInstances); + + tabContent.child(tabPane); + }); + + body.child(tabNav); + body.child(tabContent); + + } else { + renderFieldsWithLayout(body, fields, null, fieldInstances); + } + + // Скрываем лоадер + loadingOverlay.element.style.display = 'none'; + + // Футер + const footer = dom.div().cls('modal-footer'); + const cancelBtn = dom.button().cls('btn btn-secondary').atr('data-bs-dismiss', 'modal').text('Отмена'); + + // Кнопка сохранения/создания + const submitBtn = dom.button().cls('btn btn-primary'); + const submitSpinner = dom.span().cls('spinner-border spinner-border-sm').atr('role', 'status') + .atr('aria-hidden', 'true').css('display: none'); + + if (mode === 'create') { + submitBtn.text('Создать'); + submitBtn.child(submitSpinner); + footer.child(cancelBtn); + footer.child(submitBtn); + } else { + submitBtn.text('Сохранить'); + submitBtn.child(submitSpinner); + + // Кнопка удаления + const deleteBtn = dom.button().cls('btn btn-danger').text('Удалить'); + const deleteSpinner = dom.span().cls('spinner-border spinner-border-sm').atr('role', 'status') + .atr('aria-hidden', 'true').css('display: none'); + deleteBtn.child(deleteSpinner); + + footer.child(cancelBtn); + footer.child(deleteBtn); + footer.child(submitBtn); + } + + content.child(header); + content.child(body); + content.child(footer); + dialog.child(content); + modal.child(dialog); + + document.body.appendChild(modal.done()); + + const modalElement = document.getElementById(modalId); + let bootstrapModal = null; + + if (typeof bootstrap !== 'undefined') { + bootstrapModal = new bootstrap.Modal(modalElement); + } + + const modalInstance = { + modalElement, + bootstrapModal, + fieldInstances, + alertContainer: alertContainer.element, + submitBtn: submitBtn.element, + submitSpinner: submitSpinner.element, + loadingOverlay: loadingOverlay.element, + getFormData: () => { + const data = {}; + Object.entries(fieldInstances).forEach(([name, field]) => { + data[name] = field.getValue(); + }); + return data; + }, + validate: () => { + const errors = []; + Object.entries(fieldInstances).forEach(([name, field]) => { + if (field.validate) { + const error = field.validate(); + if (error) errors.push(error); + } + }); + return errors; + }, + showAlert: (message) => { + alertContainer.element.textContent = message; + alertContainer.element.style.display = 'block'; + }, + hideAlert: () => { + alertContainer.element.style.display = 'none'; + }, + setLoading: (loading) => { + submitBtn.element.disabled = loading; + submitSpinner.element.style.display = loading ? 'inline-block' : 'none'; + }, + setValues: (values) => { + Object.entries(values).forEach(([name, value]) => { + if (fieldInstances[name]) { + fieldInstances[name].setValue(value); + } + }); + }, + showLoadingOverlay: () => { + loadingOverlay.element.style.display = 'block'; + // Скрываем все поля + body.element.querySelectorAll('.row, .nav-tabs, .tab-content').forEach(el => { + el.style.display = 'none'; + }); + }, + hideLoadingOverlay: () => { + loadingOverlay.element.style.display = 'none'; + body.element.querySelectorAll('.row, .nav-tabs, .tab-content').forEach(el => { + el.style.display = ''; + }); + }, + destroy: () => { + if (bootstrapModal) { + bootstrapModal.hide(); + bootstrapModal.dispose(); + } + modalElement.remove(); + } + }; + + return modalInstance; + } + + // Основная функция API + window.crud_d = { + // Создание диалога + makeDialog: function(dialogId, fields, options = {}) { + if (typeof dialogId !== 'string') { + throw new Error('dialogId должен быть строкой'); + } + + if (!fields || typeof fields !== 'object') { + throw new Error('fields должен быть объектом'); + } + + // Сохраняем конфигурацию диалога + dialogs.set(dialogId, { + id: dialogId, + fields: fields, + options: options, + endpoints: { + create: null, + update: null, + get: null, + delete: null + } + }); + + const dialogApi = { + // Установка эндпоинтов + setCreateEndpoint: function(endpoint) { + dialogs.get(dialogId).endpoints.create = endpoint; + return this; + }, + + setUpdateEndpoint: function(endpoint) { + dialogs.get(dialogId).endpoints.update = endpoint; + return this; + }, + + setGetEndpoint: function(endpoint) { + dialogs.get(dialogId).endpoints.get = endpoint; + return this; + }, + + setDeleteEndpoint: function(endpoint) { + dialogs.get(dialogId).endpoints.delete = endpoint; + return this; + }, + + // Открытие в режиме создания + openCreate: function(title = 'Создание') { + return this._open(title, 'create'); + }, + + // Открытие в режиме обновления + openUpdate: function(itemId, title = 'Редактирование') { + return this._open(title, 'update', itemId); + }, + + // Внутренний метод открытия + _open: function(title, mode, itemId = null) { + const config = dialogs.get(dialogId); + + // Закрываем предыдущий экземпляр если есть + if (activeDialogs.has(dialogId)) { + activeDialogs.get(dialogId).destroy(); + } + + const modal = createModal(dialogId, title, config.fields, config.options, mode, itemId); + activeDialogs.set(dialogId, modal); + + const api = { + onSuccess: function(callback) { + modal.onSuccess = callback; + return this; + }, + onError: function(callback) { + modal.onError = callback; + return this; + }, + onDelete: function(callback) { + modal.onDelete = callback; + return this; + }, + setValues: function(values) { + modal.setValues(values); + return this; + }, + getFormData: function() { + return modal.getFormData(); + }, + close: function() { + modal.destroy(); + activeDialogs.delete(dialogId); + return this; + }, + show: function() { + if (modal.bootstrapModal) { + modal.bootstrapModal.show(); + } + return this; + } + }; + + // Загрузка данных для режима update + if (mode === 'update' && itemId) { + modal.showLoadingOverlay(); + + const getEndpoint = config.endpoints.get; + if (!getEndpoint) { + console.error('GET endpoint не установлен для диалога ' + dialogId); + modal.hideLoadingOverlay(); + modal.showAlert('Ошибка: не указан endpoint для загрузки данных'); + return api; + } + + const url = getEndpoint.replace('{id}', itemId); + + apiRequest('GET', url) + .then(data => { + modal.setValues(data); + modal.hideLoadingOverlay(); + }) + .catch(error => { + modal.hideLoadingOverlay(); + modal.showAlert('Ошибка загрузки данных: ' + error.message); + }); + } + + // Обработчик сохранения/создания + modal.submitBtn.addEventListener('click', async () => { + const errors = modal.validate(); + if (errors.length > 0) { + modal.showAlert(errors.join('
')); + return; + } + + modal.hideAlert(); + modal.setLoading(true); + + try { + const formData = modal.getFormData(); + let result; + + if (mode === 'create') { + const createEndpoint = config.endpoints.create; + if (!createEndpoint) { + throw new Error('Create endpoint не установлен'); + } + result = await apiRequest('POST', createEndpoint, formData); + } else { + const updateEndpoint = config.endpoints.update; + if (!updateEndpoint) { + throw new Error('Update endpoint не установлен'); + } + const url = updateEndpoint.replace('{id}', itemId); + result = await apiRequest('PUT', url, formData); + } + + if (typeof modal.onSuccess === 'function') { + modal.onSuccess(result); + } + + modal.destroy(); + activeDialogs.delete(dialogId); + } catch (error) { + modal.showAlert(error.message || 'Произошла ошибка'); + modal.setLoading(false); + + if (typeof modal.onError === 'function') { + modal.onError(error); + } + } + }); + + // Обработчик удаления (только для update) + if (mode === 'update') { + const deleteBtn = modal.modalElement.querySelector('.btn-danger'); + const deleteSpinner = deleteBtn.querySelector('.spinner-border'); + + deleteBtn.addEventListener('click', async () => { + if (!confirm('Вы уверены, что хотите удалить этот элемент?')) { + return; + } + + modal.hideAlert(); + deleteBtn.disabled = true; + deleteSpinner.style.display = 'inline-block'; + + try { + const deleteEndpoint = config.endpoints.delete; + if (!deleteEndpoint) { + throw new Error('Delete endpoint не установлен'); + } + const url = deleteEndpoint.replace('{id}', itemId); + await apiRequest('DELETE', url); + + if (typeof modal.onDelete === 'function') { + modal.onDelete(itemId); + } + + modal.destroy(); + activeDialogs.delete(dialogId); + } catch (error) { + modal.showAlert(error.message || 'Ошибка при удалении'); + deleteBtn.disabled = false; + deleteSpinner.style.display = 'none'; + + if (typeof modal.onError === 'function') { + modal.onError(error); + } + } + }); + } + + // Обработчик закрытия + modal.modalElement.addEventListener('hidden.bs.modal', () => { + modal.destroy(); + activeDialogs.delete(dialogId); + }); + + // Показываем модальное окно + if (modal.bootstrapModal) { + modal.bootstrapModal.show(); + } + + return api; + }, + + // Получить конфигурацию диалога + getConfig: function() { + return dialogs.get(dialogId); + }, + + // Удалить регистрацию + destroy: function() { + if (activeDialogs.has(dialogId)) { + activeDialogs.get(dialogId).destroy(); + activeDialogs.delete(dialogId); + } + dialogs.delete(dialogId); + } + }; + + return dialogApi; + }, + + // Глобальные методы для работы по имени диалога + + // Установка эндпоинтов по имени диалога + setCreateEndpoint: function(dialogId, endpoint) { + const dialog = dialogs.get(dialogId); + if (dialog) { + dialog.endpoints.create = endpoint; + } else { + console.warn(`Диалог "${dialogId}" не найден`); + } + }, + + setUpdateEndpoint: function(dialogId, endpoint) { + const dialog = dialogs.get(dialogId); + if (dialog) { + dialog.endpoints.update = endpoint; + } else { + console.warn(`Диалог "${dialogId}" не найден`); + } + }, + + setGetEndpoint: function(dialogId, endpoint) { + const dialog = dialogs.get(dialogId); + if (dialog) { + dialog.endpoints.get = endpoint; + } else { + console.warn(`Диалог "${dialogId}" не найден`); + } + }, + + setDeleteEndpoint: function(dialogId, endpoint) { + const dialog = dialogs.get(dialogId); + if (dialog) { + dialog.endpoints.delete = endpoint; + } else { + console.warn(`Диалог "${dialogId}" не найден`); + } + }, + + // Открытие/закрытие по имени диалога + openCreate: function(dialogId, title = 'Создание') { + const dialog = dialogs.get(dialogId); + if (dialog) { + const api = this.makeDialog(dialogId, dialog.fields, dialog.options); + // Копируем эндпоинты + api.getConfig().endpoints = dialog.endpoints; + return api.openCreate(title); + } else { + console.error(`Диалог "${dialogId}" не найден`); + return null; + } + }, + + openUpdate: function(dialogId, itemId, title = 'Редактирование') { + const dialog = dialogs.get(dialogId); + if (dialog) { + const api = this.makeDialog(dialogId, dialog.fields, dialog.options); + // Копируем эндпоинты + api.getConfig().endpoints = dialog.endpoints; + return api.openUpdate(itemId, title); + } else { + console.error(`Диалог "${dialogId}" не найден`); + return null; + } + }, + + closeDialog: function(dialogId) { + if (activeDialogs.has(dialogId)) { + activeDialogs.get(dialogId).destroy(); + activeDialogs.delete(dialogId); + } + }, + + // Получить список всех зарегистрированных диалогов + getRegisteredDialogs: function() { + return Array.from(dialogs.keys()); + }, + + // Закрыть все активные диалоги + closeAll: function() { + activeDialogs.forEach(modal => modal.destroy()); + activeDialogs.clear(); + } + }; + +})(); \ No newline at end of file