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