CRUD_Dialogs/crud_d.js

911 lines
37 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(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();
}
};
})();