From 4e314044aca60a999093b5bea3e55d52fb04d7b9 Mon Sep 17 00:00:00 2001 From: svsptech Date: Tue, 19 May 2026 05:53:16 +0500 Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BD=D0=BE=D0=B2=D1=8B=D1=85=20=D1=84?= =?UTF-8?q?=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=B9=20-=20onOpen=20=D0=B8=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B6=D0=B8=D0=BC=D0=B0=20=D0=BE=D1=82=D0=BA=D1=80?= =?UTF-8?q?=D1=8B=D1=82=D0=B8=D1=8F=20createFrom?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + README.md | 43 +- crud_d.js | 1644 ++++++++++++++++++++++++++-------------------------- 3 files changed, 879 insertions(+), 809 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..62c8935 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ \ No newline at end of file diff --git a/README.md b/README.md index 481ccee..71ca776 100644 --- a/README.md +++ b/README.md @@ -35,10 +35,11 @@ ## Основные концепции -Библиотека работает с двумя основными режимами открытия диалога: +Библиотека работает с тремя основными режимами открытия диалога: - **create** — создание нового элемента (пустая форма) - **update** — редактирование существующего элемента (форма с подгруженными данными) +- **create from** — создание нового элемента из существующего (форма с подгруженными данными) Каждый диалог имеет: - Уникальный строковый идентификатор (dialogId) @@ -143,6 +144,19 @@ dialog.openUpdate(42, 'Редактирование пользователя') }); ``` +#### openCreateFrom(itemId, title) +Открывает диалог в режиме создания с загруженными данными через GET endpoint. + +```javascript +dialog.openUpdate(42, 'Создание копии пользователя') + .onSuccess(function(result) { + console.log('Создана копия:', result); + }) + .onError(function(error) { + console.error('Ошибка:', error); + }); +``` + #### getConfig() Возвращает текущую конфигурацию диалога. @@ -196,6 +210,16 @@ api.onDelete(function(itemId) { }); ``` +#### onOpen(callback) +Устанавливает обработчик успешного открытия. Если диалог открыт +в режиме редактирования, функция будет вызвана после загрузки всех данных. + +```javascript +api.onOpen(function() { + console.log('Диалог открыт и загружен'); +}); +``` + #### setValues(values) Устанавливает значения полей формы. @@ -270,6 +294,14 @@ crud_d.openUpdate('user_form', 42, 'Редактирование') }); ``` +#### openCreateFrom(dialogId, itemId, title) +```javascript +crud_d.openUpdate('user_form', 42, 'Копирование') + .onSuccess(function(result) { + console.log('Создан из объекта:', result); + }); +``` + #### closeDialog(dialogId) ```javascript crud_d.closeDialog('user_form'); @@ -648,6 +680,15 @@ dialog.openCreate('Новый заказ') - При удалении: подтверждение → DELETE запрос → `onDelete`/`onError` - Автоматическое закрытие диалога при успехе +3. **Создание из существующего элемента (create from):** + - GET запрос на get endpoint для загрузки данных + - Заполнение формы полученными данными + - Валидация формы + - POST запрос на create endpoint + - `onSuccess` при успехе + - `onError` при ошибке + - Автоматическое закрытие диалога при успехе + ### Цепочка методов Все методы настройки возвращают `this`, что позволяет использовать цепочку вызовов: diff --git a/crud_d.js b/crud_d.js index 1b74ecd..3f79a7f 100644 --- a/crud_d.js +++ b/crud_d.js @@ -1,911 +1,939 @@ -(function() { - 'use strict'; +'use strict'; - // Хранилище зарегистрированных диалогов - const dialogs = new Map(); - // Хранилище активных экземпляров диалогов - const activeDialogs = new Map(); +// Хранилище зарегистрированных диалогов +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 || ''); +// Типы полей по умолчанию +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); + 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); + 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; + 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} обязательно для заполнения`; } - }; - }, - - 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); - }); + return null; } + }; + }, - container.child(label); - container.child(select); + 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); - 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; + 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; + } + }; + }, - 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); + 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', ''); + if (params.required) input.atr('required', ''); - const label = dom.label().cls('form-check-label').atr('for', `field_${name}`).text(params.label || name); + container.child(label); + container.child(input); - 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; + 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} обязательно для заполнения`; } - }; - }, - - 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; + if (input.element.value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input.element.value)) { + return 'Неверный формат email'; } - }; - } - }; + return null; + } + }; + }, - // Фабрика полей - function createField(name, fieldConfig) { - const [type, params, defaultValue] = fieldConfig; + 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 (FIELD_TYPES[type]) { - return FIELD_TYPES[type](name, params, defaultValue); - } + if (params.required) textarea.atr('required', ''); - 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 - }; - } + container.child(label); + container.child(textarea); - console.warn(`Неизвестный тип поля: ${type}`); - return FIELD_TYPES.text(name, params, defaultValue); - } + 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; + } + }; + }, - // Функция для расчета 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}`; - } + 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); - // Функция для рендеринга полей с учетом layout - function renderFieldsWithLayout(container, fields, layout, fieldInstances) { - const fieldNames = Object.keys(fields); - const usedFields = new Set(); + if (params.required) select.atr('required', ''); - 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); + if (params.options) { + params.options.forEach(option => { + const opt = dom.option() + .atr('value', option.value) + .text(option.label); + select.child(opt); }); } - const remainingFields = fieldNames.filter(name => !usedFields.has(name)); - remainingFields.forEach(fieldName => { - const row = dom.div().cls('row'); - const col = dom.div().cls('col-12'); + container.child(label); + container.child(select); - const field = createField(fieldName, fields[fieldName]); - fieldInstances[fieldName] = field; - - const fieldElement = field.element; - if (fieldElement.element) { - fieldElement.element.classList.remove('mb-3'); + 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); + }); - 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', - } - }; + const remainingFields = fieldNames.filter(name => !usedFields.has(name)); + remainingFields.forEach(fieldName => { + const row = dom.div().cls('row'); + const col = dom.div().cls('col-12'); - if (data && method !== 'GET') { - options.body = JSON.stringify(data); + const field = createField(fieldName, fields[fieldName]); + fieldInstances[fieldName] = field; + + const fieldElement = field.element; + if (fieldElement.element) { + fieldElement.element.classList.remove('mb-3'); } - const response = await fetch(url, options); + col.child(fieldElement); + row.child(col); + container.child(row); + }); +} - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.message || `Ошибка HTTP: ${response.status}`); +// HTTP запросы +async function apiRequest(method, url, data = null) { + const options = { + method, + headers: { + 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content ?? '', + 'Content-Type': 'application/json', } + }; - return await response.json(); + if (data && method !== 'GET') { + options.body = JSON.stringify(data); } - // Создание модального окна - 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 response = await fetch(url, options); - // Создаем структуру модального окна - const modal = dom.div().cls('modal fade').atr('id', modalId) - .atr('tabindex', '-1').atr('aria-hidden', 'true'); + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.message || `Ошибка HTTP: ${response.status}`); + } - // Настройка ширины диалога - 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}`); - } - } + return await response.json(); +} - const content = dom.div().cls('modal-content'); +// Создание модального окна +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 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); + // Создаем структуру модального окна + 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 { - renderFieldsWithLayout(body, fields, null, fieldInstances); + dialog.css(`max-width: ${width}`); + } + } + + const content = dom.div().cls('modal-content'); + + // Заголовок + const header = dom.div().cls('modal-header'); + const titleText = mode === 'create' ? title : mode === 'create_from' ? `Скопировать из: ${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]); } - // Скрываем лоадер - loadingOverlay.element.style.display = 'none'; + const categoryNames = Object.keys(finalCategories); - // Футер - const footer = dom.div().cls('modal-footer'); - const cancelBtn = dom.button().cls('btn btn-secondary').atr('data-bs-dismiss', 'modal').text('Отмена'); + const tabNav = dom.ul().cls('nav nav-tabs').atr('role', 'tablist'); + const tabContent = dom.div().cls('tab-content').cls('mt-3'); - // Кнопка сохранения/создания - const submitBtn = dom.button().cls('btn btn-primary'); - const submitSpinner = dom.span().cls('spinner-border spinner-border-sm').atr('role', 'status') + 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' || mode === 'create_from') { + 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); - 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; + footer.child(cancelBtn); + footer.child(deleteBtn); + footer.child(submitBtn); } - // Основная функция API - window.crud_d = { - // Создание диалога - makeDialog: function(dialogId, fields, options = {}) { - if (typeof dialogId !== 'string') { - throw new Error('dialogId должен быть строкой'); - } + content.child(header); + content.child(body); + content.child(footer); + dialog.child(content); + modal.child(dialog); - if (!fields || typeof fields !== 'object') { - throw new Error('fields должен быть объектом'); - } + document.body.appendChild(modal.done()); - // Сохраняем конфигурацию диалога - dialogs.set(dialogId, { - id: dialogId, - fields: fields, - options: options, - endpoints: { - create: null, - update: null, - get: null, - delete: null + 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(); + } + }; - const dialogApi = { - // Установка эндпоинтов - setCreateEndpoint: function(endpoint) { - dialogs.get(dialogId).endpoints.create = endpoint; - return this; - }, + return modalInstance; +} - setUpdateEndpoint: function(endpoint) { - dialogs.get(dialogId).endpoints.update = endpoint; - return this; - }, +// Основная функция API +window.crud_d = { + // Создание диалога + makeDialog: function(dialogId, fields, options = {}) { + if (typeof dialogId !== 'string') { + throw new Error('dialogId должен быть строкой'); + } - setGetEndpoint: function(endpoint) { - dialogs.get(dialogId).endpoints.get = endpoint; - return this; - }, + if (!fields || typeof fields !== 'object') { + throw new Error('fields должен быть объектом'); + } - setDeleteEndpoint: function(endpoint) { - dialogs.get(dialogId).endpoints.delete = endpoint; - return this; - }, + // Сохраняем конфигурацию диалога + dialogs.set(dialogId, { + id: dialogId, + fields: fields, + options: options, + endpoints: { + create: null, + update: null, + get: null, + delete: null + } + }); - // Открытие в режиме создания - openCreate: function(title = 'Создание') { - return this._open(title, 'create'); - }, + const dialogApi = { + // Установка эндпоинтов + setCreateEndpoint: function(endpoint) { + dialogs.get(dialogId).endpoints.create = endpoint; + return this; + }, - // Открытие в режиме обновления - openUpdate: function(itemId, title = 'Редактирование') { - return this._open(title, 'update', itemId); - }, + setUpdateEndpoint: function(endpoint) { + dialogs.get(dialogId).endpoints.update = endpoint; + return this; + }, - // Внутренний метод открытия - _open: function(title, mode, itemId = null) { - const config = dialogs.get(dialogId); + setGetEndpoint: function(endpoint) { + dialogs.get(dialogId).endpoints.get = endpoint; + return this; + }, - // Закрываем предыдущий экземпляр если есть - if (activeDialogs.has(dialogId)) { - activeDialogs.get(dialogId).destroy(); + setDeleteEndpoint: function(endpoint) { + dialogs.get(dialogId).endpoints.delete = endpoint; + return this; + }, + + // Открытие в режиме создания + openCreate: function(title = 'Создание') { + return this._open(title, 'create'); + }, + + openCreateFrom: function(itemId, title = 'Скопировать из') { + return this._open(title, 'create_from', itemId); + }, + + // Открытие в режиме обновления + 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 = { + onOpen: function(callback) { + modal.onOpen = callback; + return this; + }, + 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' || mode === 'create_from') && itemId) { + modal.showLoadingOverlay(); + + const getEndpoint = config.endpoints.get; + if (!getEndpoint) { + console.error('GET endpoint не установлен для диалога ' + dialogId); + modal.hideLoadingOverlay(); + modal.showAlert('Ошибка: не указан endpoint для загрузки данных'); + return api; } - const modal = createModal(dialogId, title, config.fields, config.options, mode, itemId); - activeDialogs.set(dialogId, modal); + const url = getEndpoint.replace('{id}', itemId); - 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); + apiRequest('GET', url) + .then(data => { + modal.setValues(data); modal.hideLoadingOverlay(); - modal.showAlert('Ошибка: не указан endpoint для загрузки данных'); - return api; + if (typeof modal.onOpen === 'function') { + modal.onOpen(modal); + } + }) + .catch(error => { + modal.hideLoadingOverlay(); + modal.showAlert('Ошибка загрузки данных: ' + error.message); + }); + } + + if (mode === 'create') { + setTimeout(() => { + if (typeof modal.onOpen === 'function') { + modal.onOpen(modal); } + }, 100); + } - 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.submitBtn.addEventListener('click', async () => { - const errors = modal.validate(); - if (errors.length > 0) { - modal.showAlert(errors.join('
')); + modal.hideAlert(); + modal.setLoading(true); + + try { + const formData = modal.getFormData(); + let result; + + if (mode === 'create' || mode === 'create_from') { + 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(); - modal.setLoading(true); + deleteBtn.disabled = true; + deleteSpinner.style.display = 'inline-block'; 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); + 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.onSuccess === 'function') { - modal.onSuccess(result); + if (typeof modal.onDelete === 'function') { + modal.onDelete(itemId); } modal.destroy(); activeDialogs.delete(dialogId); } catch (error) { - modal.showAlert(error.message || 'Произошла ошибка'); - modal.setLoading(false); + modal.showAlert(error.message || 'Ошибка при удалении'); + deleteBtn.disabled = false; + deleteSpinner.style.display = 'none'; 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; - }, + // Обработчик закрытия + modal.modalElement.addEventListener('hidden.bs.modal', () => { + modal.destroy(); + activeDialogs.delete(dialogId); + }); - // Глобальные методы для работы по имени диалога + // Показываем модальное окно + if (modal.bootstrapModal) { + modal.bootstrapModal.show(); + } - // Установка эндпоинтов по имени диалога - setCreateEndpoint: function(dialogId, endpoint) { - const dialog = dialogs.get(dialogId); - if (dialog) { - dialog.endpoints.create = endpoint; - } else { - console.warn(`Диалог "${dialogId}" не найден`); + return api; + }, + + // Получить конфигурацию диалога + getConfig: function() { + return dialogs.get(dialogId); + }, + + // Удалить регистрацию + destroy: function() { + if (activeDialogs.has(dialogId)) { + activeDialogs.get(dialogId).destroy(); + activeDialogs.delete(dialogId); + } + dialogs.delete(dialogId); } - }, + }; - setUpdateEndpoint: function(dialogId, endpoint) { - const dialog = dialogs.get(dialogId); - if (dialog) { - dialog.endpoints.update = endpoint; - } else { - console.warn(`Диалог "${dialogId}" не найден`); - } - }, + return dialogApi; + }, - 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(); + // Установка эндпоинтов по имени диалога + setCreateEndpoint: function(dialogId, endpoint) { + const dialog = dialogs.get(dialogId); + if (dialog) { + dialog.endpoints.create = endpoint; + } else { + console.warn(`Диалог "${dialogId}" не найден`); } - }; + }, -})(); \ No newline at end of file + 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; + } + }, + + openCreateFrom: 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.openCreateFrom(itemId, 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