Додані повідомлення та перепрацьована структура застосунку та api
This commit is contained in:
511
web/lib/customElements/menuContainer.js
Normal file
511
web/lib/customElements/menuContainer.js
Normal file
@@ -0,0 +1,511 @@
|
||||
// =========================================================
|
||||
// Клас 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);
|
||||
Reference in New Issue
Block a user