Загрузить файлы в «/»
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