/
NickRus86
/
Directory
Обзор
Документация
Войти
/
NickRus86
/
Directory
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/app.js
535 строк
15 KB
NickRus86
Начало
22 ноя 2025, 15:26
22 ноя 2025, 15:26
7d5a285
Код
Авторство
О чём код?
import { columnTypes, badgeVariants, initialColumns, initialRows } from "./data.js"; const state = { columns: [...initialColumns], rows: [...initialRows], searchText: "", }; const tableRoot = document.getElementById("table-root"); const modalRoot = document.getElementById("modal-root"); const addRowBtn = document.getElementById("add-row-btn"); const addColumnBtn = document.getElementById("add-column-btn"); const searchInput = document.getElementById("search-input"); function generateId(prefix = "id") { return `${prefix}-${Math.random().toString(36).slice(2, 9)}`; } function render() { tableRoot.innerHTML = ""; const table = document.createElement("table"); table.className = "data-table"; const thead = document.createElement("thead"); const headerRow = document.createElement("tr"); // № const thIndex = document.createElement("th"); thIndex.textContent = "№"; headerRow.appendChild(thIndex); // Пустой столбец под чекбоксы (в будущем) const thSelect = document.createElement("th"); thSelect.textContent = ""; headerRow.appendChild(thSelect); // Колонки state.columns.forEach((col) => { const th = document.createElement("th"); if (col.width) { th.style.width = `${col.width}px`; } const wrapper = document.createElement("div"); wrapper.className = "column-meta"; const title = document.createElement("div"); title.className = "column-title"; title.textContent = col.title; const type = document.createElement("div"); type.className = "column-type"; type.textContent = mapColumnTypeToLabel(col.type); wrapper.appendChild(title); wrapper.appendChild(type); th.appendChild(wrapper); headerRow.appendChild(th); }); thead.appendChild(headerRow); table.appendChild(thead); const tbody = document.createElement("tbody"); const rows = getFilteredRows(); rows.forEach((row, rowIndex) => { const tr = document.createElement("tr"); // № const tdIndex = document.createElement("td"); tdIndex.className = "cell cell--number"; tdIndex.textContent = String(rowIndex + 1); tr.appendChild(tdIndex); // placeholder под чекбокс выделения / будущие действия const tdSelect = document.createElement("td"); tdSelect.className = "cell"; tr.appendChild(tdSelect); state.columns.forEach((col) => { const td = document.createElement("td"); td.className = "cell"; const value = row.values[col.id]; const cellContent = renderCellContent(row, col, value); td.appendChild(cellContent); tr.appendChild(td); }); tbody.appendChild(tr); }); table.appendChild(tbody); tableRoot.appendChild(table); } function mapColumnTypeToLabel(type) { switch (type) { case columnTypes.TEXT: return "Текст"; case columnTypes.DATE: return "Дата"; case columnTypes.CHECKBOX: return "Чекбокс"; case columnTypes.SELECT: return "Список"; case columnTypes.BADGE: return "Бейдж"; default: return type; } } function renderCellContent(row, col, value) { switch (col.type) { case columnTypes.TEXT: { const input = document.createElement("input"); input.className = "cell-input"; input.type = "text"; input.value = value ?? ""; input.addEventListener("change", () => { updateCell(row.id, col.id, input.value); }); return input; } case columnTypes.DATE: { const input = document.createElement("input"); input.className = "cell-input"; input.type = "date"; input.value = value ?? ""; input.addEventListener("change", () => { updateCell(row.id, col.id, input.value || null); }); return input; } case columnTypes.CHECKBOX: { const wrapper = document.createElement("div"); wrapper.className = "cell-checkbox"; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.checked = Boolean(value); checkbox.addEventListener("change", () => { updateCell(row.id, col.id, checkbox.checked); }); wrapper.appendChild(checkbox); return wrapper; } case columnTypes.SELECT: { const select = document.createElement("select"); select.className = "cell-select"; const emptyOption = document.createElement("option"); emptyOption.value = ""; emptyOption.textContent = "—"; select.appendChild(emptyOption); (col.options || []).forEach((opt) => { const option = document.createElement("option"); option.value = opt.value; option.textContent = opt.label; if (opt.value === value) { option.selected = true; } select.appendChild(option); }); select.addEventListener("change", () => { updateCell(row.id, col.id, select.value || null); }); return select; } case columnTypes.BADGE: { const select = document.createElement("select"); select.className = "cell-select"; const emptyOption = document.createElement("option"); emptyOption.value = ""; emptyOption.textContent = "— статус —"; select.appendChild(emptyOption); (col.options || []).forEach((opt) => { const option = document.createElement("option"); option.value = opt.value; option.textContent = opt.label; if (opt.value === value) { option.selected = true; } select.appendChild(option); }); select.addEventListener("change", () => { updateCell(row.id, col.id, select.value || null); render(); }); if (!value) { return select; } const opt = (col.options || []).find((o) => o.value === value); const badge = document.createElement("span"); badge.className = `badge ${mapBadgeVariantToClass(opt?.variant)}`; badge.textContent = opt?.label ?? value; const container = document.createElement("div"); container.style.display = "flex"; container.style.alignItems = "center"; container.style.gap = "6px"; container.appendChild(badge); container.appendChild(select); return container; } default: { const span = document.createElement("span"); span.textContent = value ?? ""; return span; } } } function mapBadgeVariantToClass(variant) { switch (variant) { case badgeVariants.SUCCESS: return "badge--success"; case badgeVariants.WARNING: return "badge--warning"; case badgeVariants.DANGER: return "badge--danger"; case badgeVariants.INFO: default: return "badge--info"; } } function updateCell(rowId, columnId, value) { state.rows = state.rows.map((row) => { if (row.id !== rowId) return row; return { ...row, values: { ...row.values, [columnId]: value, }, }; }); } // Simple full-text search function getFilteredRows() { const search = state.searchText.trim().toLowerCase(); if (!search) return state.rows; return state.rows.filter((row) => { return state.columns.some((col) => { const rawValue = row.values[col.id]; if (rawValue === null || rawValue === undefined) return false; if (col.type === columnTypes.CHECKBOX) { const boolText = rawValue ? "да" : "нет"; return boolText.includes(search); } const text = String(rawValue).toLowerCase(); return text.includes(search); }); }); } /* Modal: add column */ function openAddColumnModal() { modalRoot.innerHTML = ""; const backdrop = document.createElement("div"); backdrop.className = "modal-backdrop"; const modal = document.createElement("div"); modal.className = "modal"; const header = document.createElement("div"); header.className = "modal-header"; const title = document.createElement("div"); title.className = "modal-title"; title.textContent = "Новая колонка"; const closeBtn = document.createElement("button"); closeBtn.className = "modal-close"; closeBtn.innerHTML = "×"; closeBtn.addEventListener("click", closeModal); header.appendChild(title); header.appendChild(closeBtn); const body = document.createElement("div"); body.className = "modal-body"; const nameField = createField("Название колонки", "Например, Подразделение"); const typeField = createSelectField("Тип", [ { value: columnTypes.TEXT, label: "Текст" }, { value: columnTypes.DATE, label: "Дата" }, { value: columnTypes.CHECKBOX, label: "Чекбокс" }, { value: columnTypes.SELECT, label: "Выпадающий список" }, { value: columnTypes.BADGE, label: "Бейдж" }, ]); const optionsField = createTextareaField( "Варианты (для списка/бейджа)", "Каждый вариант с новой строки. Можно через двоеточие указать код: например: active:Активен vacation:В отпуске", ); optionsField.container.style.display = "none"; typeField.select.addEventListener("change", () => { const t = typeField.select.value; if (t === columnTypes.SELECT || t === columnTypes.BADGE) { optionsField.container.style.display = ""; } else { optionsField.container.style.display = "none"; } }); body.appendChild(nameField.container); body.appendChild(typeField.container); body.appendChild(optionsField.container); const footer = document.createElement("div"); footer.className = "modal-footer"; const cancelBtn = document.createElement("button"); cancelBtn.className = "btn btn-secondary"; cancelBtn.textContent = "Отмена"; cancelBtn.addEventListener("click", closeModal); const saveBtn = document.createElement("button"); saveBtn.className = "btn btn-primary"; saveBtn.textContent = "Добавить"; saveBtn.addEventListener("click", () => { const titleValue = nameField.input.value.trim(); const typeValue = typeField.select.value; if (!titleValue) { nameField.input.focus(); return; } const columnId = slugify(titleValue); const newColumn = { id: columnId, title: titleValue, type: typeValue, width: 140, }; if (typeValue === columnTypes.SELECT || typeValue === columnTypes.BADGE) { const options = parseOptions(optionsField.textarea.value, typeValue); newColumn.options = options; } state.columns.push(newColumn); // Проставляем пустые значения во всех строках state.rows = state.rows.map((row) => ({ ...row, values: { ...row.values, [columnId]: null, }, })); closeModal(); render(); }); footer.appendChild(cancelBtn); footer.appendChild(saveBtn); modal.appendChild(header); modal.appendChild(body); modal.appendChild(footer); backdrop.appendChild(modal); modalRoot.appendChild(backdrop); function closeModal() { modalRoot.innerHTML = ""; } } function createField(labelText, placeholder = "") { const container = document.createElement("div"); const label = document.createElement("div"); label.className = "field-label"; label.textContent = labelText; const input = document.createElement("input"); input.className = "field-input"; input.type = "text"; input.placeholder = placeholder; container.appendChild(label); container.appendChild(input); return { container, input }; } function createSelectField(labelText, options) { const container = document.createElement("div"); const label = document.createElement("div"); label.className = "field-label"; label.textContent = labelText; const select = document.createElement("select"); select.className = "field-select"; options.forEach((opt) => { const option = document.createElement("option"); option.value = opt.value; option.textContent = opt.label; select.appendChild(option); }); container.appendChild(label); container.appendChild(select); return { container, select }; } function createTextareaField(labelText, placeholder = "") { const container = document.createElement("div"); const label = document.createElement("div"); label.className = "field-label"; label.textContent = labelText; const textarea = document.createElement("textarea"); textarea.className = "field-textarea"; textarea.rows = 4; textarea.placeholder = placeholder; const helper = document.createElement("div"); helper.className = "field-helper"; helper.textContent = "Можно оставить пустым — тогда колонка будет без фиксированных вариантов."; container.appendChild(label); container.appendChild(textarea); container.appendChild(helper); return { container, textarea }; } function parseOptions(raw, type) { const lines = raw .split(/\r?\n/) .map((l) => l.trim()) .filter(Boolean); if (!lines.length) return []; const options = lines.map((line) => { const [code, label] = line.includes(":") ? line.split(":", 2) : [slugify(line), line]; const opt = { value: code.trim(), label: (label ?? code).trim(), }; if (type === columnTypes.BADGE) { // Простейшее сопоставление по словам const lower = opt.label.toLowerCase(); if (lower.includes("усп") || lower.includes("ok") || lower.includes("готов")) { opt.variant = badgeVariants.SUCCESS; } else if (lower.includes("проблем") || lower.includes("ошиб") || lower.includes("крит")) { opt.variant = badgeVariants.DANGER; } else if (lower.includes("ожид") || lower.includes("в процессе")) { opt.variant = badgeVariants.INFO; } else { opt.variant = badgeVariants.WARNING; } } return opt; }); return options; } function slugify(text) { return text .toLowerCase() .trim() .replace(/\s+/g, "_") .replace(/[^a-z0-9_а-яё]/g, "_") .replace(/_+/g, "_"); } /* Event handlers */ addRowBtn.addEventListener("click", () => { const newRow = { id: generateId("row"), values: {}, }; state.columns.forEach((col) => { if (col.type === columnTypes.CHECKBOX) { newRow.values[col.id] = false; } else { newRow.values[col.id] = null; } }); state.rows = [newRow, ...state.rows]; render(); }); addColumnBtn.addEventListener("click", () => { openAddColumnModal(); }); searchInput.addEventListener("input", () => { state.searchText = searchInput.value; render(); }); // Initial render render();