Додані повідомлення та перепрацьована структура застосунку та api

This commit is contained in:
2026-03-15 00:25:10 +02:00
parent 85483b85bb
commit 4bc9c11512
101 changed files with 5763 additions and 2546 deletions

View File

@@ -0,0 +1,292 @@
const SMART_SELECT_STYLES_CSS = `
:host { display: block; position: relative; width: 100%; font-family: system-ui, sans-serif; }
:host ::-webkit-scrollbar { height: 5px; width: 8px; }
:host ::-webkit-scrollbar-track { background: transparent; }
:host ::-webkit-scrollbar-thumb { background: var(--smart-select-chip-background, #475569); border-radius: 4px; }
@media (hover: hover) {
:host ::-webkit-scrollbar-thumb:hover { opacity: 0.7; }
}
/* Стили для скролла */
.trigger::-webkit-scrollbar { height: 4px; }
.trigger::-webkit-scrollbar-thumb { background: var(--smart-select-chip-background, #475569); border-radius: 4px; }
.wrapper {
min-height: 35px;
border: 1px solid var(--smart-select-border-color, #ccc);
border-radius: var(--smart-select-border-radius-1, 6px);
display: flex;
padding: 0 6px;
flex-direction: column;
justify-content: center;
background: var(--smart-select-background, #fff);
}
.trigger {
display: flex;
gap: 6px;
width: 100%;
overflow-x: auto;
align-items: center;
cursor: pointer;
}
.placeholder-text {
color: var(--smart-select-chip-color);
opacity: 0.4;
pointer-events: none;
user-select: none;
font-size: var(--smart-select-font-size-2);
}
.chip {
background: var(--smart-select-chip-background, #dbe3ea);
color: var(--smart-select-chip-color, #000);
padding: 4px 6px;
border-radius: var(--smart-select-border-radius-2, 4px);
font-size: var(--smart-select-font-size-1, 12px);
display: flex;
align-items: center;
gap: 3px;
white-space: nowrap;
cursor: pointer;
}
.chip button {
display: flex;
position: relative;
background: none;
border: none;
cursor: pointer;
padding: 0;
width: 20px;
height: 15px;
justify-content: flex-end;
fill: var(--smart-select-chip-fill, #e91e63);
}
.chip button svg{ width: 15px; height: 15px; }
.dropdown {
display: none; position: absolute; top: 100%; left: 0; right: 0;
background: var(--smart-select-background, #ffffff);
border: 1px solid var(--smart-select-border-color, #ccc);
border-radius: var(--smart-select-border-radius-1, 6px); z-index: 9999; margin-top: 4px;
flex-direction: column; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1);
}
:host([open]) .dropdown { display: flex; }
input[type="search"] {
margin: 10px;
padding: 8px 10px;
border-radius: var(--smart-select-border-radius-2, 4px);
border: 1px solid var(--smart-select-search-border, #ccc);
background: transparent;
color: var(--smart-select-search-color, #000);
}
.options-list {max-height: 200px; overflow-y: auto; padding: 10px; display: flex; flex-direction: column; gap: 8px; }
::slotted([slot="option"]) {
padding: 8px 12px !important;
cursor: pointer;
border-radius: var(--smart-select-border-radius-2, 4px);
display: block;
color: var(--smart-select-option-color, #000);
font-size: var(--smart-select-font-size-2, 14px);
}
@media (hover: hover) {
::slotted([slot="option"]:hover) {
background: var(--smart-select-hover-background, #475569);
color: var(--smart-select-hover-color, #fff);
}
}
::slotted([slot="option"].selected) {
background: var(--smart-select-selected-background, #dbe3eb) !important;
color: var(--smart-select-selected-color, #000) !important;
}
`;
// Створення об'єкта CSSStyleSheet (якщо підтримується)
let SmartSelectStyles = null;
if (typeof CSSStyleSheet !== 'undefined' && CSSStyleSheet.prototype.replaceSync) {
SmartSelectStyles = new CSSStyleSheet(); // (2) Визначення об'єкта тут
SmartSelectStyles.replaceSync(SMART_SELECT_STYLES_CSS);
}
class SmartSelect extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._selectedValues = new Set();
// Додаємо стилі в конструкторі, якщо це adoptable
if (this.shadowRoot.adoptedStyleSheets) {
this.shadowRoot.adoptedStyleSheets = [SmartSelectStyles];
} else {
// FALLBACK для старих браузерів (наприклад, iOS < 16.4)
const style = document.createElement('style');
style.textContent = SMART_SELECT_STYLES_CSS;
this.shadowRoot.appendChild(style);
}
}
connectedCallback() {
this.render();
const searchInput = this.shadowRoot.querySelector('input[type="search"]');
searchInput.addEventListener('input', (e) => this.handleSearch(e));
const wrapper = this.shadowRoot.querySelector('.wrapper');
wrapper.addEventListener('click', (e) => {
if (e.target.closest('button')) return;
this.toggleAttribute('open');
if (this.hasAttribute('open')) {
setTimeout(() => searchInput.focus(), 50);
}
});
// Слушаем клики по элементам в слоте
this.addEventListener('click', (e) => {
const opt = e.target.closest('[slot="option"]');
if (opt) {
this.toggleValue(opt.getAttribute('data-value'), opt);
}
});
// Слушаем изменение слота для инициализации (фикс Safari)
this.shadowRoot.querySelector('slot').addEventListener('slotchange', () => {
this.syncOptions();
});
this.syncOptions();
}
_formatValue(val) {
const isNumber = this.getAttribute('type') === 'number';
return isNumber ? Number(val) : String(val);
}
syncOptions() {
const options = Array.from(this.querySelectorAll('[slot="option"]'));
options.forEach(opt => {
// Используем data-selected вместо атрибута selected
if (opt.hasAttribute('data-selected')) {
// Зберігаємо вже у потрібному форматі
const val = this._formatValue(opt.getAttribute('data-value'));
this._selectedValues.add(val);
}
});
this.updateDisplay();
}
handleSearch(e) {
const term = e.target.value.toLowerCase().trim();
const options = this.querySelectorAll('[slot="option"]');
options.forEach(opt => {
const text = opt.textContent.toLowerCase();
opt.style.display = text.includes(term) ? '' : 'none';
});
}
toggleValue(val) {
const max = this.getAttribute('max') ? parseInt(this.getAttribute('max')) : null;
const formattedVal = this._formatValue(val);
if (this._selectedValues.has(formattedVal)) {
this._selectedValues.delete(formattedVal);
this._click = {
value: formattedVal,
state: "delete"
};
} else {
if (max && this._selectedValues.size >= max) return;
this._selectedValues.add(formattedVal);
this._click = {
value: formattedVal,
state: "add"
};
}
this.updateDisplay();
this.dispatchEvent(new Event('change', { bubbles: true }));
}
updateDisplay() {
const container = this.shadowRoot.getElementById('tags');
const placeholder = this.shadowRoot.getElementById('placeholder');
const optionsElements = this.querySelectorAll('[slot="option"]');
// Керування видимістю плейсхолдера
if (this._selectedValues.size > 0) {
placeholder.style.display = 'none';
} else {
placeholder.style.display = 'block';
}
container.innerHTML = '';
this._selectedValues.forEach(val => {
// Важливо: при пошуку елемента в DOM атрибут data-value завжди рядок,
// тому використовуємо == для порівняння числа з рядком
const opt = Array.from(optionsElements).find(o => o.getAttribute('data-value') == val);
if (opt) {
const chip = document.createElement('div');
chip.className = 'chip';
chip.innerHTML = `${opt.textContent} <button><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></button>`;
chip.querySelector('button').onclick = (e) => {
e.stopPropagation();
this.toggleValue(val);
};
container.appendChild(chip);
}
});
const max = this.getAttribute('max') ? parseInt(this.getAttribute('max')) : null;
const isFull = max && this._selectedValues.size >= max;
optionsElements.forEach(opt => {
const optVal = this._formatValue(opt.getAttribute('data-value'));
const isSelected = this._selectedValues.has(optVal);
opt.classList.toggle('selected', isSelected);
// Если лимит исчерпан, делаем невыбранные опции полупрозрачными
if (isFull && !isSelected) {
opt.style.opacity = '0.5';
opt.style.cursor = 'not-allowed';
} else {
opt.style.opacity = '1';
opt.style.cursor = 'pointer';
}
});
}
get value() {
return Array.from(this._selectedValues);
}
get getClick() {
return this._click;
}
render() {
this.shadowRoot.innerHTML = `
<div class="wrapper">
<div class="trigger" id="tags"></div>
<div id="placeholder" class="placeholder-text">
${this.getAttribute('placeholder') || 'Оберіть значення...'}
</div>
</div>
<div class="dropdown">
<input type="search" placeholder="Пошук...">
<div class="options-list">
<slot name="option"></slot>
</div>
</div>
`;
}
}
customElements.define('smart-select', SmartSelect);