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

407 lines
22 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.
/**
* Клас NotificationContainer
* Веб-компонент для відображення системних сповіщень.
* Використовує Shadow DOM для інкапсуляції стилів.
*/
class NotificationContainer extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._timeout = 4000;
this._maxVisible = 5;
this._position = 'top-right';
this._mobileBottomEnabled = false;
// *** Вдосконалення: Відстежуємо ноди, що видаляються ***
this._removingNodes = new Set();
this._container = document.createElement('div');
this._container.className = 'app-notification-container';
this.shadowRoot.appendChild(this._container);
this._insertStyles();
// SVG іконки для різних типів сповіщень (залишаємо як є)
this._icons = {
info: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><path d="M 15 3 C 13.895 3 13 3.895 13 5 L 13 5.2929688 C 10.109011 6.1538292 8 8.8293311 8 12 L 8 14.757812 C 8 17.474812 6.921 20.079 5 22 A 1 1 0 0 0 4 23 A 1 1 0 0 0 5 24 L 25 24 A 1 1 0 0 0 26 23 A 1 1 0 0 0 25 22 C 23.079 20.079 22 17.474812 22 14.757812 L 22 12 C 22 8.8293311 19.890989 6.1538292 17 5.2929688 L 17 5 C 17 3.895 16.105 3 15 3 z M 3.9550781 7.9882812 A 1.0001 1.0001 0 0 0 3.1054688 8.5527344 C 3.1054688 8.5527344 2 10.666667 2 13 C 2 15.333333 3.1054687 17.447266 3.1054688 17.447266 A 1.0001165 1.0001165 0 0 0 4.8945312 16.552734 C 4.8945312 16.552734 4 14.666667 4 13 C 4 11.333333 4.8945313 9.4472656 4.8945312 9.4472656 A 1.0001 1.0001 0 0 0 3.9550781 7.9882812 z M 26.015625 7.9882812 A 1.0001 1.0001 0 0 0 25.105469 9.4472656 C 25.105469 9.4472656 26 11.333333 26 13 C 26 14.666667 25.105469 16.552734 25.105469 16.552734 A 1.0001163 1.0001163 0 1 0 26.894531 17.447266 C 26.894531 17.447266 28 15.333333 28 13 C 28 10.666667 26.894531 8.5527344 26.894531 8.5527344 A 1.0001 1.0001 0 0 0 26.015625 7.9882812 z M 12 26 C 12 27.657 13.343 29 15 29 C 16.657 29 18 27.657 18 26 L 12 26 z"/></svg>`,
success: `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8' style="width: 17px;height: 17px;"><path d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/></svg>`,
warn: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"> <path d="M 15 3 C 14.168432 3 13.456063 3.5067238 13.154297 4.2285156 L 2.3007812 22.947266 L 2.3007812 22.949219 A 2 2 0 0 0 2 24 A 2 2 0 0 0 4 26 A 2 2 0 0 0 4.140625 25.994141 L 4.1445312 26 L 15 26 L 25.855469 26 L 25.859375 25.992188 A 2 2 0 0 0 26 26 A 2 2 0 0 0 28 24 A 2 2 0 0 0 27.699219 22.947266 L 27.683594 22.919922 A 2 2 0 0 0 27.681641 22.917969 L 16.845703 4.2285156 C 16.543937 3.5067238 15.831568 3 15 3 z M 13.787109 11.359375 L 16.212891 11.359375 L 16.011719 17.832031 L 13.988281 17.832031 L 13.787109 11.359375 z M 15.003906 19.810547 C 15.825906 19.810547 16.318359 20.252813 16.318359 21.007812 C 16.318359 21.748812 15.825906 22.189453 15.003906 22.189453 C 14.175906 22.189453 13.679688 21.748813 13.679688 21.007812 C 13.679688 20.252813 14.174906 19.810547 15.003906 19.810547 z"/></svg>`,
error: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><path d="M 10.03125 2.9042969 A 1.0001 1.0001 0 0 0 9.0605469 4.2578125 C 9.0605469 4.2578125 9.7494688 5.9996671 11.488281 7.0800781 C 11.080693 7.2039808 10.414387 7.5446908 9.8027344 8.4570312 C 9.4322834 8.3403526 9.1037156 8.2185154 8.8496094 8.0976562 C 8.3997876 7.8837103 8.118603 7.6807271 7.9589844 7.546875 C 7.7993657 7.4130229 7.8046875 7.40625 7.8046875 7.40625 A 1.0001 1.0001 0 0 0 7 7 L 5 7 A 1.0001 1.0001 0 1 0 5 9 L 6.5957031 9 C 6.6368531 9.038228 6.6271651 9.038995 6.6738281 9.078125 C 6.961397 9.3192729 7.3888062 9.6162897 7.9902344 9.9023438 C 9.1930908 10.474451 11.083447 11 13.935547 11 A 1.0001 1.0001 0 0 0 14.140625 10.980469 C 14.430223 10.987386 14.697172 11 15.017578 11 C 15.326932 11 15.582943 10.988887 15.863281 10.982422 A 1.0001 1.0001 0 0 0 16.064453 11 C 18.916553 11 20.806909 10.474451 22.009766 9.9023438 C 22.611194 9.6162897 23.038603 9.3192729 23.326172 9.078125 C 23.372834 9.0389949 23.363147 9.0382279 23.404297 9 L 25 9 A 1.0001 1.0001 0 1 0 25 7 L 23 7 A 1.0001 1.0001 0 0 0 22.195312 7.40625 C 22.195312 7.40625 22.200612 7.41302 22.041016 7.546875 C 21.881397 7.6807271 21.600212 7.8837103 21.150391 8.0976562 C 20.891444 8.2208175 20.55751 8.3444389 20.177734 8.4628906 C 19.558423 7.539139 18.907199 7.1978378 18.517578 7.0761719 C 20.252095 5.9954925 20.939453 4.2578125 20.939453 4.2578125 A 1.0001 1.0001 0 0 0 20.039062 2.9042969 A 1.0001 1.0001 0 0 0 19.060547 3.5761719 C 19.060547 3.5761719 18.556779 5.088719 16.882812 5.6757812 C 16.36708 5.2573881 15.71568 5 15 5 C 14.284868 5 13.632808 5.2576596 13.117188 5.6757812 C 11.443221 5.088719 10.939453 3.5761719 10.939453 3.5761719 A 1.0001 1.0001 0 0 0 10.03125 2.9042969 z M 5.9628906 11 A 1.0001 1.0001 0 0 0 5.6835938 11.050781 L 2.6835938 12.050781 A 1.0005646 1.0005646 0 0 0 3.3164062 13.949219 L 5.9707031 13.064453 C 6.0672386 13.111686 6.3494962 13.235909 6.5917969 13.34375 C 6.2888038 14.107541 6 15.15686 6 16.425781 C 6 17.948961 6.3086267 19.289595 6.7949219 20.453125 L 6.1601562 22.357422 L 3.4453125 24.167969 A 1.0001 1.0001 0 1 0 4.5546875 25.832031 L 7.5546875 23.832031 A 1.0001 1.0001 0 0 0 7.9492188 23.316406 L 8.1367188 22.751953 C 9.8000084 24.902319 11.988204 26 13 26 C 13.742 26 14 25.42 14 25 L 14 16 C 14 15.447 14.448 15 15 15 C 15.552 15 16 15.447 16 16 L 16 25 C 16 25.42 16.258 26 17 26 C 18.011796 26 20.199992 24.902319 21.863281 22.751953 L 22.050781 23.316406 A 1.0001 1.0001 0 0 0 22.445312 23.832031 L 25.445312 25.832031 A 1.0001 1.0001 0 1 0 26.554688 24.167969 L 23.839844 22.357422 L 23.205078 20.453125 C 23.691373 19.289595 24 17.948961 24 16.425781 C 24 15.15686 23.711196 14.107541 23.408203 13.34375 C 23.650504 13.235909 23.932762 13.111686 24.029297 13.064453 L 26.683594 13.949219 A 1.0005646 1.0005646 0 1 0 27.316406 12.050781 L 24.316406 11.050781 A 1.0001 1.0001 0 0 0 24.021484 11 A 1.0001 1.0001 0 0 0 23.552734 11.105469 C 23.552734 11.105469 20.660591 12.508607 17.513672 12.896484 C 16.740098 12.956149 15.915495 13 15 13 C 14.084505 13 13.259902 12.956149 12.486328 12.896484 C 9.3394093 12.508607 6.4472656 11.105469 6.4472656 11.105469 A 1.0001 1.0001 0 0 0 5.9628906 11 z"/></svg>`
};
}
static get observedAttributes() {
return ['timeout', 'max-visible', 'position', 'mobile-position'];
}
connectedCallback() {
this._updateSettings();
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this._updateSettings();
}
}
_updateSettings() {
this._position = this.getAttribute('position') || 'top-right';
this._maxVisible = parseInt(this.getAttribute('max-visible')) || 5;
this._timeout = parseInt(this.getAttribute('timeout')) || 4000;
const mobilePosAttr = this.getAttribute('mobile-position');
this._mobileBottomEnabled = mobilePosAttr === 'bottom' || mobilePosAttr === '';
this._container.setAttribute('data-position', this._position);
this._applyMobileStyles();
}
_applyMobileStyles() {
if (this._mobileBottomEnabled) {
this._container.classList.add('mobile-bottom');
} else {
this._container.classList.remove('mobile-bottom');
}
}
setMobileBottom(enable) {
this._mobileBottomEnabled = !!enable;
this._applyMobileStyles();
}
/**
* Показує нове сповіщення.
*/
show(message, options = {}) {
const {
type = 'info',
timeout = this._timeout,
title,
onClick,
lock
} = options;
const content = typeof message === 'string'
? { title: title || '', text: message }
: message;
// **Оновлена логіка обмеження кількості:**
// Визначаємо кількість "видимих" нод (які не перебувають у процесі видалення)
const totalChildren = this._container.children.length;
const visibleNodesCount = totalChildren - this._removingNodes.size;
while (visibleNodesCount >= this._maxVisible) {
// Шукаємо найстаріший елемент, який ще НЕ видаляється
const first = Array.from(this._container.children).find(n => !this._removingNodes.has(n));
if (first) {
this._removeNode(first);
} else {
// Якщо всі елементи в процесі видалення, виходимо
break;
}
}
// Кінець оновленої логіки обмеження
// Створення DOM елементів (залишаємо як є)
const node = document.createElement('div');
node.className = `app-notification ${type}`;
if (onClick) node.style.cursor = "pointer";
const icon = document.createElement('div');
icon.className = 'icon';
icon.innerHTML = this._icons[type] || this._icons.info;
const body = document.createElement('div');
body.className = 'body';
if (content.title) {
const t = document.createElement('div');
t.className = 'title';
t.textContent = content.title;
body.appendChild(t);
}
const txt = document.createElement('div');
txt.className = 'text';
txt.textContent = content.text || '';
body.appendChild(txt);
node.appendChild(icon);
node.appendChild(body);
// Додаємо кнопку закриття
if (!onClick && !lock) {
const closeDiv = document.createElement('div');
closeDiv.className = 'blockClose';
node.appendChild(closeDiv);
const closeBtn = document.createElement('button');
closeBtn.className = 'close';
closeBtn.setAttribute('aria-label', 'Закрити повідомлення');
closeBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 26"><path d="M 6.65625 4 C 6.367188 4 6.105469 4.113281 5.90625 4.3125 L 4.3125 5.90625 C 3.914063 6.304688 3.914063 7 4.3125 7.5 L 9.8125 13 L 4.3125 18.5 C 3.914063 19 3.914063 19.695313 4.3125 20.09375 L 5.90625 21.6875 C 6.40625 22.085938 7.101563 22.085938 7.5 21.6875 L 13 16.1875 L 18.5 21.6875 C 19 22.085938 19.695313 22.085938 20.09375 21.6875 L 21.6875 20.09375 C 22.085938 19.59375 22.085938 18.898438 21.6875 18.5 L 16.1875 13 L 21.6875 7.5 C 22.085938 7 22.085938 6.304688 21.6875 5.90625 L 20.09375 4.3125 C 19.59375 3.914063 18.898438 3.914063 18.5 4.3125 L 13 9.8125 L 7.5 4.3125 C 7.25 4.113281 6.945313 4 6.65625 4 Z"></path></svg>';
closeDiv.appendChild(closeBtn);
closeBtn.addEventListener('click', (e) => {
e.stopPropagation(); // Запобігаємо спрацьовуванню onClick на самій ноді
this._removeNode(node);
});
}
this._container.appendChild(node);
// Запускаємо анімацію появи через requestAnimationFrame
requestAnimationFrame(() => node.classList.add('show'));
let timer = null;
const startTimer = () => {
if (timeout === 0 || lock) return;
timer = setTimeout(() => this._removeNode(node), timeout);
};
const clearTimer = () => { if (timer) { clearTimeout(timer); timer = null; } };
// Зупинка таймауту при наведенні
node.addEventListener('mouseenter', clearTimer);
node.addEventListener('mouseleave', startTimer);
// Обробка кліку на сповіщенні
if (typeof onClick === 'function') {
node.addEventListener('click', () => {
clearTimer(); // Зупиняємо таймаут, якщо він був
try { onClick(); } catch (e) { console.error(e); }
this._removeNode(node);
});
}
startTimer();
return node;
}
/**
* Приватний метод для видалення ноди з анімацією.
* **Тепер перевіряє, чи нода вже видаляється.**
*/
_removeNode(node) {
if (!node || !node.parentElement || this._removingNodes.has(node)) return;
this._removingNodes.add(node); // Позначаємо як ту, що видаляється
node.classList.remove('show');
// Чекаємо завершення анімації зникнення (200мс)
setTimeout(() => {
if (node && node.parentElement) {
node.parentElement.removeChild(node);
}
this._removingNodes.delete(node); // Видаляємо зі списку після фізичного видалення
}, 200);
}
/**
* Видаляє всі видимі сповіщення.
*/
clearAll() {
if (!this._container) return;
// Використовуємо _removeNode, який тепер безпечно обробляє повторні виклики
Array.from(this._container.children).forEach(n => this._removeNode(n));
}
// Допоміжні методи з фіксованим типом сповіщення
info(message, opts = {}) { return this.show(message, { ...opts, type: 'info' }); }
success(message, opts = {}) { return this.show(message, { ...opts, type: 'success' }); }
warn(message, opts = {}) { return this.show(message, { ...opts, type: 'warn' }); }
error(message, opts = {}) { return this.show(message, { ...opts, type: 'error' }); }
// Метод для сповіщень, що реагують на клік (псевдонім 'click' для 'show' з 'onClick')
click(message, opts = {}) { return this.show(message, { ...opts, onClick: opts.f }); }
/**
* Вставляє необхідні CSS стилі в Shadow DOM (залишаємо як є).
*/
_insertStyles() {
const style = document.createElement('style');
style.textContent = `
/* Контейнер */
.app-notification-container {
position: fixed;
z-index: 9999;
pointer-events: none;
display: flex;
flex-direction: column;
gap: 10px;
padding: 12px;
}
/* Позиціонування контейнера */
.app-notification-container[data-position="top-right"] { top: 8px; right: 8px; align-items: flex-end; }
.app-notification-container[data-position="top-left"] { top: 8px; left: 8px; align-items: flex-start; }
.app-notification-container[data-position="bottom-right"] { bottom: 8px; right: 8px; align-items: flex-end; }
.app-notification-container[data-position="bottom-left"] { bottom: 8px; left: 8px; align-items: flex-start; }
/* Одне сповіщення */
.app-notification {
pointer-events: auto;
min-width: 220px;
max-width: 360px;
background: #111;
color: #fff;
padding: 10px 12px 10px 12px;
border-radius: var(--border-radius, 8px);
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25);
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
font-size: var(--FontSize2, 14px);
line-height: 1.2;
display: flex;
gap: 10px;
align-items: center;
opacity: 0;
transform: translateY(-6px) scale(0.995);
transition: opacity .18s ease, transform .18s ease;
position: relative;
}
.app-notification.show {
opacity: 0.95;
transform: translateY(0) scale(1);
}
.app-notification .icon {
font-size: 18px;
width: 22px;
height: 22px;
display:flex;
align-items:center;
justify-content:center;
border-radius: calc(var(--border-radius, 8px) - 5px);
padding: 8px;
}
.app-notification .icon svg{
width: 20px;
height: 20px;
fill: #fff;
}
.app-notification .body { flex:1; }
.app-notification .title { font-weight: 600; margin-bottom: 4px; font-size: 13px; }
/* Кнопка закриття */
.app-notification .blockClose {
width: 20px;
height: 20px;
}
.app-notification .blockClose .close {
position: absolute;
right: 10px;
top: 10px;
margin-left: 8px;
background: transparent;
border: none;
color: inherit;
cursor: pointer;
padding: 0;
}
.app-notification .blockClose .close svg {
width: 15px;
height: 15px;
fill: #fff;
opacity: 0.8;
display: block;
}
/* Стилі за типами */
.app-notification.info {
background: var(--ColorThemes3, #2196F3);
color: var(--ColorThemes0, #ffffff);
}
.app-notification.info .icon { background: var(--ColorThemes0, #ffffff); }
.app-notification.info .icon svg{fill: var(--ColorThemes3, #2196F3);}
.app-notification.info .close svg{fill: var(--ColorThemes0, #ffffff);}
.app-notification.success { background: #52ac56; }
.app-notification.success .icon { background: #6dc450; }
.app-notification.success .close svg{fill: #fff;}
.app-notification.warn { background: #d18515; }
.app-notification.warn .icon { background: #eaad57; }
.app-notification.warn .close svg{fill: #fff;}
.app-notification.error { background: #9c2424; }
.app-notification.error .icon { background: #c45050; }
.app-notification.error .close svg{fill: #fff;}
/* Адаптивність для мобільних пристроїв */
@media (max-width: 700px) {
.app-notification-container {
left: 0;
right: 0;
width: calc(100% - 24px);
align-items: center !important;
}
.app-notification-container .app-notification {
max-width: calc(100% - 30px);
min-width: calc(100% - 30px);
}
/* Спеціальна мобільна позиція знизу */
.app-notification-container.mobile-bottom {
top: auto;
bottom: 0;
}
}
`;
this.shadowRoot.appendChild(style);
}
}
// Реєструємо веб-компонент у браузері
customElements.define('notification-container', NotificationContainer);
/*
============================
ПРИКЛАД ВИКОРИСТАННЯ
============================
*/
/*
1. Додайте цей елемент у свій HTML:
<notification-container
id="notif-manager"
position="top-right"
max-visible="5"
timeout="4000"
mobile-position>
</notification-container>
2. Отримайте посилання на компонент у JS:
const Notifier = document.getElementById('notif-manager');
3. Приклади викликів:
💡 Базові сповіщення
Notifier.info('Налаштування мобільної позиції змінено.');
Notifier.success('Успішна операція.');
Notifier.warn('Увага: низький рівень заряду батареї.');
Notifier.error('Критична помилка!');
💡 Сповіщення із заголовком
Notifier.info('Це повідомлення має чіткий заголовок.', {
title: 'Важлива інформація'
});
💡 Сповіщення з об'єктом (заголовок та текст)
Notifier.warn({
title: `Metrics`,
text: `З'єднання встановлено`
});
💡 Сповіщення, яке не зникає (timeout: 0 або lock: true)
Notifier.error('Критична помилка! Необхідне втручання.', {
timeout: 0,
lock: true
});
💡 Сповіщення з обробником кліку (автоматично закривається після кліку)
Notifier.info('Натисніть тут, щоб побачити деталі.', {
onClick: () => alert('Ви клацнули! Дякую.'),
lock: false
});
💡 Програмне керування
Notifier.setMobileBottom(true); // Включити примусову позицію знизу для мобільних
Notifier.setMobileBottom(false); // Вимкнути примусову позицію знизу
Notifier.clearAll(); // Видалити всі сповіщення
*/