Files
Sheep-Service/web/lib/customElements/menuContainer.js

511 lines
19 KiB
JavaScript
Raw 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.
// =========================================================
// Клас 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);