292 lines
11 KiB
JavaScript
292 lines
11 KiB
JavaScript
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); |