/** * Пікер вибору людей у стилі Бітрікс: чіпи вибраних + пошук + дерево * відділів з фото/посадами. Працює поверх звичайного * * * * Дерево відділів передається один раз на сторінку через * {{ department_tree|json_script:"department-tree-data" }}. * * Використання: initPeoplePickers(scopeEl) після вставки розмітки в DOM * (в т.ч. після HTMX-свапу фрагмента). */ (function () { function options(select) { return Array.prototype.slice.call(select.options); } function initialsOf(label) { var parts = (label || '').trim().split(/\s+/).filter(Boolean); return parts.slice(0, 2).map(function (w) { return w[0]; }).join('').toUpperCase(); } function buildAvatar(photo, label, size) { size = size || '1.75rem'; if (photo) { var img = document.createElement('img'); img.src = photo; img.className = 'people-picker-avatar'; img.style.width = size; img.style.height = size; return img; } var span = document.createElement('span'); span.className = 'people-picker-avatar people-picker-avatar-fallback'; span.style.width = size; span.style.height = size; span.textContent = initialsOf(label); return span; } function findOption(select, pk) { return options(select).filter(function (o) { return o.value === String(pk); })[0]; } function renderChips(picker) { var select = picker._select; var chips = picker._chips; chips.innerHTML = ''; options(select).filter(function (o) { return o.selected; }).forEach(function (opt) { var chip = document.createElement('span'); chip.className = 'people-picker-chip'; chip.textContent = opt.dataset.label || opt.textContent; var remove = document.createElement('button'); remove.type = 'button'; remove.className = 'people-picker-chip-remove'; remove.innerHTML = '×'; remove.setAttribute('aria-label', 'Видалити'); remove.addEventListener('click', function () { opt.selected = false; select.dispatchEvent(new Event('change')); renderChips(picker); if (picker._refreshTree) picker._refreshTree(); }); chip.appendChild(remove); chips.appendChild(chip); }); } function toggleOption(picker, pk) { var opt = findOption(picker._select, pk); if (!opt) return; opt.selected = !opt.selected; picker._select.dispatchEvent(new Event('change')); renderChips(picker); if (picker._refreshTree) picker._refreshTree(); } function renderSearchResults(picker, query) { var select = picker._select; var results = picker._results; var q = query.trim().toLowerCase(); var matches = options(select).filter(function (o) { if (o.selected) return false; var label = (o.dataset.label || o.textContent).toLowerCase(); return label.indexOf(q) !== -1; }).slice(0, 30); results.innerHTML = ''; matches.forEach(function (opt) { var row = document.createElement('div'); row.className = 'people-picker-row'; row.appendChild(buildAvatar(opt.dataset.photo, opt.dataset.label)); var info = document.createElement('div'); info.className = 'people-picker-row-info'; var name = document.createElement('div'); name.className = 'people-picker-row-name'; name.textContent = opt.dataset.label || opt.textContent; info.appendChild(name); if (opt.dataset.position) { var pos = document.createElement('div'); pos.className = 'people-picker-row-position'; pos.textContent = opt.dataset.position; info.appendChild(pos); } row.appendChild(info); row.addEventListener('mousedown', function (e) { e.preventDefault(); toggleOption(picker, opt.value); picker._search.value = ''; showTree(picker); }); results.appendChild(row); }); } function renderEmployeeRow(picker, emp, container) { var opt = findOption(picker._select, emp.pk); var row = document.createElement('div'); row.className = 'people-picker-row'; if (opt && opt.selected) row.classList.add('is-selected'); row.appendChild(buildAvatar(emp.photo, emp.label)); var info = document.createElement('div'); info.className = 'people-picker-row-info'; var name = document.createElement('div'); name.className = 'people-picker-row-name'; name.textContent = emp.label; info.appendChild(name); if (emp.position) { var pos = document.createElement('div'); pos.className = 'people-picker-row-position'; pos.textContent = emp.position; info.appendChild(pos); } row.appendChild(info); if (opt && opt.selected) { var check = document.createElement('span'); check.className = 'people-picker-row-check'; check.innerHTML = '✓'; row.appendChild(check); } row.addEventListener('mousedown', function (e) { e.preventDefault(); toggleOption(picker, emp.pk); row.classList.toggle('is-selected'); var existingCheck = row.querySelector('.people-picker-row-check'); if (row.classList.contains('is-selected') && !existingCheck) { var c = document.createElement('span'); c.className = 'people-picker-row-check'; c.innerHTML = '✓'; row.appendChild(c); } else if (!row.classList.contains('is-selected') && existingCheck) { existingCheck.remove(); } }); container.appendChild(row); } function renderDeptNode(picker, node, container, depth, expanded) { var row = document.createElement('div'); row.className = 'people-picker-dept'; row.style.paddingLeft = (depth * 0.875) + 'rem'; var toggle = document.createElement('span'); toggle.className = 'people-picker-dept-toggle'; toggle.textContent = expanded ? '▾' : '▸'; row.appendChild(toggle); var folder = document.createElement('span'); folder.className = 'people-picker-dept-icon'; folder.innerHTML = '📁'; row.appendChild(folder); var label = document.createElement('span'); label.className = 'people-picker-dept-label'; label.textContent = node.name; row.appendChild(label); container.appendChild(row); var childrenWrap = document.createElement('div'); childrenWrap.style.display = expanded ? 'block' : 'none'; container.appendChild(childrenWrap); var built = false; function build() { if (built) return; node.employees.forEach(function (emp) { renderEmployeeRow(picker, emp, childrenWrap); }); node.children.forEach(function (child) { renderDeptNode(picker, child, childrenWrap, depth + 1, false); }); built = true; } if (expanded) build(); row.addEventListener('click', function () { build(); var isOpen = childrenWrap.style.display !== 'none'; childrenWrap.style.display = isOpen ? 'none' : 'block'; toggle.textContent = isOpen ? '▸' : '▾'; }); } function showTree(picker) { picker._search.value = ''; picker._results.style.display = 'none'; picker._results.innerHTML = ''; picker._tree.style.display = 'block'; if (picker._refreshTree) picker._refreshTree(); } function buildTreeView(picker, treeData) { var tree = picker._tree; function draw() { tree.innerHTML = ''; treeData.tree.forEach(function (node, i) { renderDeptNode(picker, node, tree, 0, true); }); if (treeData.no_department && treeData.no_department.length) { var wrap = document.createElement('div'); var header = document.createElement('div'); header.className = 'people-picker-dept-label'; header.style.padding = '0.375rem 0.5rem'; header.style.opacity = '0.7'; header.textContent = 'Без відділу'; wrap.appendChild(header); treeData.no_department.forEach(function (emp) { renderEmployeeRow(picker, emp, wrap); }); tree.appendChild(wrap); } } picker._refreshTree = draw; draw(); } function loadTreeData(picker) { var sourceId = picker.dataset.treeSource; if (!sourceId) return null; var el = document.getElementById(sourceId); if (!el) return null; try { return JSON.parse(el.textContent); } catch (e) { return null; } } function build(picker) { var select = picker.querySelector('select[multiple]'); if (!select) return; picker._select = select; var wrap = document.createElement('div'); wrap.className = 'people-picker-ui'; var chips = document.createElement('div'); chips.className = 'people-picker-chips'; picker._chips = chips; var search = document.createElement('input'); search.type = 'text'; search.className = 'input people-picker-search'; search.placeholder = picker.dataset.placeholder || '+ Додати...'; picker._search = search; var dropdown = document.createElement('div'); dropdown.className = 'people-picker-dropdown'; dropdown.style.display = 'none'; var results = document.createElement('div'); results.className = 'people-picker-results'; results.style.display = 'none'; picker._results = results; var tree = document.createElement('div'); tree.className = 'people-picker-tree'; picker._tree = tree; dropdown.appendChild(results); dropdown.appendChild(tree); picker._dropdown = dropdown; var treeData = loadTreeData(picker); if (treeData) buildTreeView(picker, treeData); function openDropdown() { dropdown.style.display = 'block'; if (search.value.trim()) { results.style.display = 'block'; tree.style.display = 'none'; renderSearchResults(picker, search.value); } else { showTree(picker); } } search.addEventListener('focus', openDropdown); search.addEventListener('input', function () { if (search.value.trim()) { results.style.display = 'block'; tree.style.display = 'none'; renderSearchResults(picker, search.value); } else { showTree(picker); } }); search.addEventListener('keydown', function (e) { if (e.key === 'Backspace' && !search.value) { var selected = options(select).filter(function (o) { return o.selected; }); var last = selected[selected.length - 1]; if (last) { last.selected = false; select.dispatchEvent(new Event('change')); renderChips(picker); if (picker._refreshTree) picker._refreshTree(); } } else if (e.key === 'Escape') { dropdown.style.display = 'none'; search.blur(); } }); document.addEventListener('click', function (e) { if (!picker.contains(e.target)) dropdown.style.display = 'none'; }); wrap.appendChild(chips); wrap.appendChild(search); picker.appendChild(wrap); picker.appendChild(dropdown); renderChips(picker); } window.initPeoplePickers = function (scope) { (scope || document).querySelectorAll('.people-picker:not([data-picker-init])').forEach(function (picker) { picker.setAttribute('data-picker-init', '1'); build(picker); }); }; })();