511 lines
19 KiB
JavaScript
511 lines
19 KiB
JavaScript
// =========================================================
|
|
// Клас 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);
|
|
|
|
// Створюємо порожній контейнер, який буде замінено на <div> або <a>
|
|
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('<svg') && trimmedIcon.endsWith('</svg>');
|
|
|
|
if (isSVG) {
|
|
iconHTML = `<span class="nav-icon-wrapper">${trimmedIcon}</span>`;
|
|
} else {
|
|
iconHTML = `<img src="${iconContent}" alt="${title} icon" class="nav-icon-img">`;
|
|
}
|
|
}
|
|
const innerContent = `${iconHTML}<span class="nav-title">${title}</span>`;
|
|
|
|
// 2. Визначаємо, який тег використовувати (<a> чи <div>)
|
|
const currentTag = this.containerWrapper.firstChild ? this.containerWrapper.firstChild.tagName.toLowerCase() : null;
|
|
const requiredTag = href ? 'a' : 'div';
|
|
|
|
// Якщо тип тега потрібно змінити, створюємо новий елемент
|
|
if (currentTag !== requiredTag) {
|
|
// Створюємо новий елемент <a> або <div>
|
|
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) {
|
|
// Якщо це тег <a>, дозволяємо браузеру обробляти клік (або 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 - це сам <navigation-container>)
|
|
// або 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');
|
|
|
|
// Слот дозволяє відображати дочірні елементи <nav-item>
|
|
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',
|
|
`<svg viewBox="0 0 24 24"><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>`
|
|
);
|
|
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); |