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