diff --git a/web/lib/customElements/swipeUpdater.js b/web/lib/customElements/swipeUpdater.js index 27f45fe..02156ec 100644 --- a/web/lib/customElements/swipeUpdater.js +++ b/web/lib/customElements/swipeUpdater.js @@ -19,6 +19,8 @@ class SwipeUpdater extends HTMLElement { // 3. Внутрішній стан this._isReadyToReload = false; // Прапорець, що вказує на готовність до оновлення + this._isStandalone = false; // Кеш для перевірки PWA режиму + this._isTouching = false; // Чи тримає користувач палець на екрані // 4. Створення елементів (Внутрішній HTML) shadow.innerHTML = ` @@ -111,8 +113,10 @@ class SwipeUpdater extends HTMLElement { this._animID = shadow.getElementById('swipe_updater'); this._animIconID = shadow.getElementById('swipe_icon'); - // 6. Прив'язка контексту `this` для обробника подій (важливо для коректної роботи `this.handleScroll`) + // 6. Прив'язка контексту `this` для обробників подій this.handleScroll = this.handleScroll.bind(this); + this.handleTouchStart = this.handleTouchStart.bind(this); + this.handleTouchEnd = this.handleTouchEnd.bind(this); } /** @@ -128,51 +132,81 @@ class SwipeUpdater extends HTMLElement { } } + // Фіксуємо, що користувач торкнувся екрана + handleTouchStart() { + this._isTouching = true; + } + + // Обробка події, коли користувач відпускає екран + handleTouchEnd() { + this._isTouching = false; + const threshold = -165; + + // Якщо палець відпустили, а ефект «гумового скролу» вже повернувся вище порогу + // (тобто користувач передумав і потягнув назад вгору перед тим як відпустити) + if (window.scrollY > threshold && this._isReadyToReload) { + this._resetRefreshState(); + } + } + /** * Обробник події скролу (головна логіка Pull-to-Refresh). * Відстежує прокручування вище верхньої межі сторінки (`window.scrollY < 0`). */ handleScroll() { // Перевірка на режим Standalone (PWA) - функціональність актуальна переважно тут - const isStandalone = window.matchMedia('(display-mode: standalone)').matches; + if (!this._isStandalone) return; - if (isStandalone) { - let scrollY = window.scrollY; - // 1. Анімація іконки під час прокручування за верхню межу (scrollY < 0) - if (scrollY <= -10) { - // Зміщення іконки разом із прокруткою - this._animIconID.style.top = `${scrollY / 1.5}px`; - this._animID.style.zIndex = '115'; // Піднімаємо z-index для видимості над контентом - } else { - this._animID.style.zIndex = '0'; // Повертаємо базовий z-index + const scrollY = window.scrollY; + const threshold = -165; // Поріг прокрутки (наприклад, -125px) для активації оновлення + + // 1. Анімація іконки під час прокручування за верхню межу (scrollY < 0) + if (scrollY <= -10) { + // Зміщення іконки разом із прокруткою + this._animIconID.style.top = `${scrollY / 1.5}px`; + this._animID.style.zIndex = '115'; // Піднімаємо z-index для видимості над контентом + } else { + this._animIconID.style.top = '0px'; + this._animID.style.zIndex = '0'; // Повертаємо базовий z-index + } + + // 2. Логіка активації "готовий до оновлення" + if (scrollY <= threshold) { + if (!this._isReadyToReload) { + this._isReadyToReload = true; + // Поворот іконки на 180 градусів + this._animIconID.style.transform = 'rotate(180deg)'; + this._animIconID.setAttribute('data-state', ''); // Деактивація стану "active" } - - const threshold = -125; // Поріг прокрутки (наприклад, -125px) для активації оновлення - - // 2. Логіка активації "готовий до оновлення" - if (scrollY <= threshold) { - if (!this._isReadyToReload) { - this._isReadyToReload = true; - // Поворот іконки на 180 градусів - this._animIconID.style.transform = 'rotate(180deg)'; - this._animIconID.setAttribute('data-state', ''); // Деактивація стану "active" - } - } - // 3. Логіка виклику оновлення та скидання стану - // Якщо користувач відпускає свайп (scrollY повертається до >= 0) І був готовий до оновлення - else if (scrollY >= 0) { - if (this._isReadyToReload) { - // Виклик користувацької функції (або стандартного window.location.reload()) - this._appReload(); - - // Скидання стану та анімації - this._isReadyToReload = false; - this._animIconID.style.transform = 'rotate(0deg)'; - this._animIconID.setAttribute('data-state', 'active'); - } + } + // Скасування жесту під час руху пальцем вгору (ще до відпускання) + else if (scrollY > threshold && scrollY < 0 && this._isTouching) { + if (this._isReadyToReload) { + this._isReadyToReload = false; + this._animIconID.style.transform = 'rotate(0deg)'; + this._animIconID.setAttribute('data-state', 'active'); } } + // 3. Логіка виклику оновлення, коли сторінка повернулась до нуля + else if (scrollY >= 0) { + if (this._isReadyToReload) { + this._appReload(); + this._resetRefreshState(); + } + } + + } + + /** + * Допоміжний метод для повного скидання графічного стану + */ + _resetRefreshState() { + this._isReadyToReload = false; + this._animIconID.style.transform = 'rotate(0deg)'; + this._animIconID.setAttribute('data-state', 'active'); + this._animIconID.style.top = '0px'; + this._animID.style.zIndex = '0'; } @@ -181,8 +215,15 @@ class SwipeUpdater extends HTMLElement { * Додаємо обробник події прокручування. */ connectedCallback() { - // Прослуховування глобальної події скролу - window.addEventListener('scroll', this.handleScroll); + // Перевіряємо PWA режим ОДИН раз при додаванні компонента на сторінку + this._isStandalone = window.matchMedia('(display-mode: standalone)').matches; + + if (this._isStandalone) { + window.addEventListener('scroll', this.handleScroll); + // passive: true покращує продуктивність скролу на мобільних пристроях + window.addEventListener('touchstart', this.handleTouchStart, { passive: true }); + window.addEventListener('touchend', this.handleTouchEnd, { passive: true }); + } } /** @@ -190,12 +231,17 @@ class SwipeUpdater extends HTMLElement { * Видаляємо обробник події прокручування для запобігання витоку пам'яті. */ disconnectedCallback() { + // Чистимо абсолютно всі додані слухачі window.removeEventListener('scroll', this.handleScroll); + window.removeEventListener('touchstart', this.handleTouchStart); + window.removeEventListener('touchend', this.handleTouchEnd); } } // Реєстрація веб-компонента -customElements.define('swipe-updater', SwipeUpdater); +if (!customElements.get('swipe-updater')) { + customElements.define('swipe-updater', SwipeUpdater); +} /* diff --git a/web/lib/pages/sheeps/index.html b/web/lib/pages/sheeps/index.html index 54abf32..766dd7b 100644 --- a/web/lib/pages/sheeps/index.html +++ b/web/lib/pages/sheeps/index.html @@ -11,7 +11,6 @@ /> - ${butt_add} diff --git a/web/lib/pages/sheeps/script.js b/web/lib/pages/sheeps/script.js index fde0e2b..9a1bbca 100644 --- a/web/lib/pages/sheeps/script.js +++ b/web/lib/pages/sheeps/script.js @@ -162,7 +162,7 @@ const Sheeps = { let html = await fetch('/lib/pages/sheeps/index.html').then((response) => response.text()); app.innerHTML = html; - await Sheeps.sheeps_list.setHTML(); + await Sheeps.sheeps_list.setHTML({}); if (id) Sheeps.editor.setHTML(id); SheepsEvents.init(); @@ -182,7 +182,7 @@ const Sheeps = { return Sheeps.sheeps_list.list }, - setHTML: async (search_value = null) => { + setHTML: async ({search, filter}) => { let block_sheep_list = document.getElementById('block-sheeps-list'); let block_sheep_info = document.getElementById('block-sheep-info'); block_sheep_list.style.display = "flex"; @@ -210,6 +210,9 @@ const Sheeps = {

Всі вісники

+ @@ -224,6 +227,27 @@ const Sheeps = {
`; + let filter_selected = Number(filter) + html += ` +
+ +
+ `; + + html += ` +
+ `; + const accessTemplate = (p) => { let perms = []; if (p.can_view_sheeps) perms.push("View Sheeps"); @@ -242,7 +266,11 @@ const Sheeps = { }; for (const element of list) { - if (search_value && !element.name.toLowerCase().includes(search_value)) { + if (search && !element.name.toLowerCase().includes(search)) { + continue; // пропустити, якщо ім'я не містить рядок пошуку + } + + if (filter && element.group_id !== Number(filter) && Number(filter) !== 0) { continue; // пропустити, якщо ім'я не містить рядок пошуку } @@ -446,18 +474,42 @@ const Sheeps = { }, 10) } }, + filter: { + open: () => { + const sheepFilterForm = document.getElementById("block-sheeps-list-filter"); + const current = sheepFilterForm.dataset.state; + + if(current === 'closed') Sheeps.search.close(); + sheepFilterForm.dataset.state = current === 'open' ? 'closed' : 'open'; + }, + close: () => { + const sheepFilterForm = document.getElementById("block-sheeps-list-filter"); + sheepFilterForm.dataset.state = 'closed' + }, + input: (value) => { + console.log(value); + + filter_value = value || ""; + Sheeps.sheeps_list.setHTML({filter: filter_value}); + } + }, search: { open: () => { const sheepSearchForm = document.getElementById("block-sheeps-list-search"); - sheepSearchForm.classList.toggle('active'); const current = sheepSearchForm.dataset.state; + + if(current === 'closed') Sheeps.filter.close(); sheepSearchForm.dataset.state = current === 'open' ? 'closed' : 'open'; }, + close: () => { + const sheepSearchForm = document.getElementById("block-sheeps-list-search"); + sheepSearchForm.dataset.state = 'closed' + }, input: (value) => { console.log(value); search_value = value?.trim()?.toLowerCase() || ""; - Sheeps.sheeps_list.setHTML(search_value); + Sheeps.sheeps_list.setHTML({search: search_value}); } }, territory: { diff --git a/web/lib/pages/sheeps/style.css b/web/lib/pages/sheeps/style.css index 55a3934..f611b7b 100644 --- a/web/lib/pages/sheeps/style.css +++ b/web/lib/pages/sheeps/style.css @@ -43,6 +43,7 @@ border-radius: calc(var(--border-radius) - 5px); position: relative; z-index: 10; + margin-bottom: -10px; } #block-sheeps-list>.header>h1 { @@ -83,11 +84,12 @@ } +#block-sheeps-list>.filter, #block-sheeps-list>.search { width: calc(100% - 30px); background-color: var(--PrimaryColor); - border-radius: 0px 0px 10px 10px; - margin: -12px 10px 20px 10px; + border-radius: 10px; + margin: -2px 10px 0px 10px; display: flex; flex-direction: row; align-items: center; @@ -102,10 +104,22 @@ } +#block-sheeps-list>.filter[data-state="open"], #block-sheeps-list>.search[data-state="open"] { max-height: 60px; padding: 22px 5px 5px 5px; opacity: 1; + border-radius: 0px 0px 10px 10px; +} + +#block-sheeps-list>.filter>select{ + width: 100%; + padding: 0 5px; + border-radius: calc(var(--border-radius) - 5px - 4px); + height: 30px; + background-color: var(--ColorThemes2); + color: var(--ColorThemes3); + font-size: var(--FontSize2); } #block-sheeps-list>.search>input { diff --git a/web/lib/pages/territory/list/style.css b/web/lib/pages/territory/list/style.css index d74485c..86d4129 100644 --- a/web/lib/pages/territory/list/style.css +++ b/web/lib/pages/territory/list/style.css @@ -77,7 +77,7 @@ .page-territory>.list-controls>button { display: flex; position: relative; - background: var(--ColorThemes3); + background: var(--PrimaryColor); font-size: 18px; cursor: pointer; min-width: 30px; @@ -93,7 +93,7 @@ .page-territory>.list-controls>button>svg { width: 25px; height: 25px; - fill: var(--ColorThemes0); + fill: var(--PrimaryColorText); } #page-territory-search { diff --git a/web/sw.js b/web/sw.js index 8b12e64..64cc83e 100644 --- a/web/sw.js +++ b/web/sw.js @@ -1,4 +1,4 @@ -const STATIC_CACHE_NAME = 'v2.2.4'; +const STATIC_CACHE_NAME = 'v2.2.9'; const FILES_TO_CACHE = [ '/',