// ========================================================= // Клас 1: Пункт Меню (nav-item) // ========================================================= class NavigationItem extends HTMLElement { // 1. Відстежувані атрибути static get observedAttributes() { return ['title', 'icon', 'href', 'click']; } constructor() { super(); this.attachShadow({ mode: 'open' }); // Створюємо базову структуру елементів this.renderBaseStructure(); } /** * Створює базовий DOM та стилі. */ renderBaseStructure() { const shadow = this.shadowRoot; shadow.innerHTML = ''; const style = document.createElement('style'); style.textContent = ` .item-wrapper{ width: 184px; height: 54px; list-style-type: none; position: relative; -webkit-transition: width .2s ease 0s; -o-transition: width .2s ease 0s; transition: width .2s ease 0s; z-index: 1; display: flex; align-items: center; justify-content: space-between; } .nav-item { width: 100%; height: 50px; padding: 0 12px; display: flex; flex-direction: row; flex-wrap: nowrap; align-items: center; justify-content: flex-start; border-radius: var(--border-radius, 15px); -webkit-transition: width .2s ease 0s; -o-transition: width .2s ease 0s; transition: width .2s ease 0s; opacity: 0.8; cursor: pointer; border: 2px; border: 2px solid var(--ColorThemes2, #525151); color: var(--ColorThemes3, #f3f3f3); overflow: hidden; text-decoration: none; gap: 15px; } .nav-icon-img, .nav-icon-wrapper { width: 25px; height: 25px; display: flex; align-items: center; justify-content: center; } .nav-icon-wrapper svg { width: 25px; height: 25px; min-width: 25px; min-height: 25px; fill: currentColor; } .nav-title { font-size: var(--FontSize3, 14px); font-weight: 300; white-space: nowrap; } :host([data-state="active"]) .nav-item { color: var(--ColorThemes2, #525151); background: var(--ColorThemes3, #f3f3f3); border: 2px solid var(--ColorThemes3, #f3f3f3); box-shadow: var(--shadow-l1, 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 0px 2px rgba(0, 0, 0, 0.06), 0px 0px 1px rgba(0, 0, 0, 0.04)); } @media (hover: hover) { .nav-item:hover { border: 2px solid var(--ColorThemes3); } } @media (max-width: 1100px) { .item-wrapper{ width: 54px; } .nav-item{ width: 50px; } .nav-title { display: none; } } @media (max-width: 700px), (max-height: 540px) { .item-wrapper{ width: 40px; height: 40px; } .nav-item { width: 40px; height: 40px; padding: 0; border: 0; justify-content: center; background: transparent; color: var(--ColorThemes0, #1c1c19); border-radius: 50%; } :host([data-state="active"]) .nav-item { color: var(--PrimaryColor, #cb9e44); background: transparent; border: 0; box-shadow: none; } .nav-title { display: none; } @media (hover: hover) { .nav-item:hover { border: 0; } } } `; shadow.appendChild(style); // Створюємо порожній контейнер, який буде замінено на
або this.containerWrapper = document.createElement('div'); this.containerWrapper.setAttribute('class', 'item-wrapper'); shadow.appendChild(this.containerWrapper); // Первинне заповнення контентом this.updateContent(); } connectedCallback() { // Обробник кліку додається після того, як this.itemElement створено в updateContent this.containerWrapper.addEventListener('click', this.handleItemClick.bind(this)); } disconnectedCallback() { this.containerWrapper.removeEventListener('click', this.handleItemClick.bind(this)); } attributeChangedCallback(name, oldValue, newValue) { if (oldValue !== newValue) { this.updateContent(); } } /** * Зчитує атрибути та оновлює внутрішній HTML і тег-контейнер. */ updateContent() { const title = this.getAttribute('title') || 'Без назви'; const iconContent = this.getAttribute('icon'); const href = this.getAttribute('href'); // 1. Формуємо HTML для іконки та заголовка let iconHTML = ''; if (iconContent) { const trimmedIcon = iconContent.trim(); const isSVG = trimmedIcon.startsWith(''); if (isSVG) { iconHTML = `${trimmedIcon}`; } else { iconHTML = `${title} icon`; } } const innerContent = `${iconHTML}${title}`; // 2. Визначаємо, який тег використовувати ( чи
) const currentTag = this.containerWrapper.firstChild ? this.containerWrapper.firstChild.tagName.toLowerCase() : null; const requiredTag = href ? 'a' : 'div'; // Якщо тип тега потрібно змінити, створюємо новий елемент if (currentTag !== requiredTag) { // Створюємо новий елемент або
this.itemElement = document.createElement(requiredTag); this.itemElement.setAttribute('class', 'nav-item'); // Замінюємо старий елемент новим this.containerWrapper.innerHTML = ''; this.containerWrapper.appendChild(this.itemElement); } else { // Елемент вже правильний, використовуємо його this.itemElement = this.containerWrapper.firstChild; } // 3. Встановлюємо атрибути this.itemElement.innerHTML = innerContent; if (href) { // Це посилання: встановлюємо href та data-route this.itemElement.setAttribute('href', href); this.itemElement.setAttribute('data-route', href); // <-- ДОДАНО data-route this.itemElement.removeAttribute('role'); this.itemElement.removeAttribute('tabindex'); } else { // Це кнопка: видаляємо посилальні атрибути та встановлюємо роль this.itemElement.removeAttribute('href'); this.itemElement.removeAttribute('data-route'); this.itemElement.setAttribute('role', 'button'); this.itemElement.setAttribute('tabindex', '0'); } } /** * Обробляє клік, виконуючи код з атрибута 'click' для не-посилань. */ handleItemClick(event) { const clickAction = this.getAttribute('click'); const href = this.getAttribute('href'); if (href) { // Якщо це тег , дозволяємо браузеру обробляти клік (або JS-роутеру) return; } if (clickAction) { try { event.preventDefault(); // Запобігаємо стандартній дії (якщо була встановлена роль кнопки) console.log(`Executing click action: ${clickAction}`); eval(clickAction); } catch (e) { console.error(`Error executing click action "${clickAction}":`, e); } } } } // ========================================================= // Клас 2: Контейнер Меню (navigation-container) // ========================================================= class NavigationContainer extends HTMLElement { /** * Обробляє клік на документі, щоб приховати меню, якщо клік був за його межами. */ handleOutsideClick = (event) => { // Перевіряємо, чи містить наш компонент елемент, на який клікнули // (this.shadowRoot.host - це сам ) // або this.menuContainer.contains(event.target) if (!this.contains(event.target) && !this.shadowRoot.contains(event.target)) { // Клік був за межами компонента this.hideHiddenMenu(); } } handleScroll = () => { this.hideHiddenMenu(); } constructor() { super(); const shadow = this.attachShadow({ mode: 'open' }); this.standalone = window.matchMedia('(display-mode: standalone)').matches; this.os = this.detectOS(); // Стилі контейнера const style = document.createElement('style'); style.textContent = ` .navigation-menu{ position: fixed; width: 230px; height: calc(100vh - 60px); min-height: 510px; background: var(--ColorThemes2, #525151); margin: 0; padding: 40px 10px; -webkit-transition: width .2s ease 0s; -o-transition: width .2s ease 0s; transition: width .2s ease 0s; display: flex; flex-direction: column; justify-content: flex-start; } .navigation-items, .navigation-items-hidden { position: relative; display: flex; flex-direction: column; min-height: 55px; align-items: center; justify-content: flex-start; gap: 6px; border-radius: 30px; } .navigation-items-hidden { margin-top: 5px; } .more-button-item { display: none; } @media (max-width: 1100px) { .navigation-menu { width: 100px; } } @media (max-width: 700px), (max-height: 540px) { .navigation-menu { width: calc(100% - 30px); height: 60px; min-height: 60px; padding: 0; z-index: 9991; bottom: 0px; background: transparent; left: 15px; border: 0; margin: 0; bottom: 0px; } .navigation-items { display: flex; flex-direction: row; height: 100%; justify-content: space-around; align-items: center; z-index: 9998; bottom: 10px; box-shadow: 0px -4px 10px rgba(0, 0, 0, 0.2); } .navigation-items::before { content: ""; position: absolute; inset: 0; background: var(--ColorThemes2, #525151); background: var(--ColorThemes3, #f3f3f3); opacity: 0.97; z-index: 0; border-radius: 30px; } .navigation-items-hidden{ flex-direction: row; height: fit-content; justify-content: space-around; align-items: center; position: absolute; bottom: 12px; left: 2px; width: calc(100% - 4px); margin: 0; -webkit-transition: .2s ease 0s; -o-transition: .2s ease 0s; transition: .2s ease 0s; z-index: 9992; opacity: 0; } .navigation-items-hidden::before { content: ""; position: absolute; inset: 0; background: var(--ColorThemes2, #525151); background: var(--ColorThemes3, #f3f3f3); opacity: 0.98; z-index: 0; border-radius: 30px; } .more-button-item { display: flex; } .navigation-menu.expanded .navigation-items-hidden { bottom: 75px; opacity: 1; box-shadow: 0px -4px 10px rgba(0, 0, 0, 0.2); } .navigation-menu[data-os="iOS"] .navigation-items { bottom: 15px; } .navigation-menu[data-os="iOS"] .navigation-items-hidden { bottom: 17px; } .navigation-menu[data-os="iOS"].expanded .navigation-items-hidden { bottom: 80px; opacity: 1; } } @media (max-width: 700px) { .navigation-menu { -webkit-transition: 0s ease 0s; -o-transition: 0s ease 0s; transition: 0s ease 0s; } } `; shadow.appendChild(style); this.menuContainer = document.createElement('menu'); this.menuContainer.setAttribute('class', 'navigation-menu'); if (this.standalone) this.menuContainer.setAttribute('data-os', this.os); this.itemsContainer = document.createElement('items'); this.itemsContainer.setAttribute('class', 'navigation-items'); this.itemsHiddenContainer = document.createElement('items-hidden'); this.itemsHiddenContainer.setAttribute('class', 'navigation-items-hidden'); // Слот дозволяє відображати дочірні елементи const slot = document.createElement('slot'); this.itemsContainer.appendChild(slot); this.menuContainer.appendChild(this.itemsContainer); this.menuContainer.appendChild(this.itemsHiddenContainer); shadow.appendChild(this.menuContainer); // MutationObserver для відстеження динамічного додавання/видалення пунктів this.observer = new MutationObserver(this.handleMutations.bind(this)); // Спостерігаємо за зміною дочірніх елементів this.observer.observe(this, { childList: true, subtree: false }); } connectedCallback() { // Додаємо обробник кліків до документа document.addEventListener('click', this.handleOutsideClick); // Додаємо обробник прокручування до вікна window.addEventListener('scroll', this.handleScroll); // Повторна перевірка елементів на випадок, якщо вони вже були в DOM до реєстрації this.reassignItems(); } disconnectedCallback() { this.observer.disconnect(); // Видаляємо обробник кліків з документа document.removeEventListener('click', this.handleOutsideClick); // Видаляємо обробник прокручування з вікна window.removeEventListener('scroll', this.handleScroll); } handleMutations(mutationsList, observer) { // Логіка оновлення при додаванні/видаленні дочірніх елементів (наприклад, для логування) this.reassignItems(); } reassignItems() { // Отримуємо всі дочірні елементи (які можуть бути nav-item) const allItems = Array.from(this.children); allItems.forEach(item => { // Перевіряємо, чи має елемент атрибут data-hidden if (item.getAttribute('data-hidden') == 'true') { this.itemsHiddenContainer.appendChild(item); this.createMoreButton(); } else if (item.parentNode !== this) { this.appendChild(item); } }); } // --- Утиліти --- hideHiddenMenu() { this.menuContainer.classList.remove('expanded'); } toggleHiddenMenu() { this.menuContainer.classList.toggle('expanded'); } createMoreButton() { let moreButton = this.itemsContainer.querySelector('.more-button-item'); if (!moreButton) { const button = document.createElement('nav-item'); button.setAttribute('class', 'more-button-item'); button.setAttribute('title', 'Більше'); button.setAttribute('icon', `` ); button.addEventListener('click', this.toggleHiddenMenu.bind(this)); this.itemsContainer.appendChild(button); return button; } } detectOS() { const userAgent = navigator.userAgent || navigator.vendor || window.opera; if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) { return 'iOS'; } return 'Other'; } } // ========================================================= // Реєстрація компонентів у браузері // ========================================================= customElements.define('nav-item', NavigationItem); customElements.define('navigation-container', NavigationContainer);