From 323fd15a62377f60630b25e9b9750369903cac3e Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sat, 10 Jun 2023 13:08:08 +0200 Subject: [PATCH] More components, checkbox, items filter. --- site/changes-new.html | 5 +- site/changes-new.js | 6 +- site/misc.js | 19 +++++ site/tailwind.css | 12 +++ site/views/custom-checkbox.js | 36 +++++++++ site/views/index.js | 2 + site/views/items-filter.js | 135 +++++++++++++++++++++++++++++++--- site/views/items-list.js | 70 ++++++++++++++++++ site/views/view.js | 85 ++++++++++++++++++++- 9 files changed, 355 insertions(+), 15 deletions(-) create mode 100644 site/views/custom-checkbox.js create mode 100644 site/views/items-list.js diff --git a/site/changes-new.html b/site/changes-new.html index 1c5633c..875a28d 100644 --- a/site/changes-new.html +++ b/site/changes-new.html @@ -2,9 +2,8 @@

Preisänderungen

- -
- + +
diff --git a/site/changes-new.js b/site/changes-new.js index 011ba63..ee1cf1a 100644 --- a/site/changes-new.js +++ b/site/changes-new.js @@ -3,5 +3,9 @@ require("./views"); (async () => { await model.load(); - document.querySelector("items-filter").model(model.items); + const itemsFilter = document.querySelector("items-filter"); + itemsFilter.model = model.items; + itemsFilter.addEventListener("change", (event) => { + console.log("Filter changed: " + event.target.getAttribute("x-id")); + }); })(); diff --git a/site/misc.js b/site/misc.js index 5fafb33..62bfac6 100644 --- a/site/misc.js +++ b/site/misc.js @@ -21,6 +21,14 @@ if (typeof window !== "undefined") { setupLiveEdit(); } +exports.today = () => { + const currentDate = new Date(); + const year = currentDate.getFullYear(); + const month = String(currentDate.getMonth() + 1).padStart(2, "0"); + const day = String(currentDate.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; +}; + exports.fetchJSON = async (url) => { const response = await fetch(url); return await response.json(); @@ -45,9 +53,20 @@ exports.dom = (element, innerHTML) => { return el; }; +/** + * Searches all children with attribute `x-id` and returns them + * in an object under keys equal to the `x-id` value. + * + * @param {HTMLElement} dom + * @returns Object storing each child under a key equal to its `x-id`. + */ exports.getDynamicElements = (dom) => { const elements = dom.querySelectorAll("[x-id]"); const result = {}; elements.forEach((element) => (result[element.getAttribute("x-id")] = element)); return result; }; + +exports.getBooleanAttribute = (element, name) => { + return element.hasAttribute(name) && (element.getAttribute(name).length == 0 || element.getAttribute(name) === "true"); +}; diff --git a/site/tailwind.css b/site/tailwind.css index a6610c6..87dbbfc 100644 --- a/site/tailwind.css +++ b/site/tailwind.css @@ -173,3 +173,15 @@ thead > tr { @apply border-stone-400; @apply hover:bg-stone-400; } + +/* items filter */ +.items-filter { + @apply bg-stone-200; + @apply rounded-xl; + @apply p-4; + @apply max-w-4xl; + @apply mx-auto; + @apply md:mb-12; + @apply md:mt-6; + @apply block; +} diff --git a/site/views/custom-checkbox.js b/site/views/custom-checkbox.js new file mode 100644 index 0000000..6a082d9 --- /dev/null +++ b/site/views/custom-checkbox.js @@ -0,0 +1,36 @@ +const { dom, getDynamicElements } = require("../misc"); +const { View } = require("./view"); + +class CustomCheckbox extends View { + constructor() { + super(); + const isChecked = this.hasAttribute("checked") && (this.getAttribute("checked").length == 0 || this.getAttribute("checked") === "true"); + const label = this.hasAttribute("label") ? this.getAttribute("label") : ""; + const abbr = this.hasAttribute("abbr") ? this.getAttribute("abbr") : ""; + this.innerHTML = /*html*/ ` + + `; + this.classList.add("customcheckbox"); + this._checkbox = getDynamicElements(this).checkbox; + this.setupEventHandlers(); + } + + get checkbox() { + return this._checkbox; + } + + get checked() { + return this._checkbox.checked; + } + + set checked(value) { + this._checkbox.checked = value; + } +} +customElements.define("custom-checkbox", CustomCheckbox); diff --git a/site/views/index.js b/site/views/index.js index e6b0d3b..37b6747 100644 --- a/site/views/index.js +++ b/site/views/index.js @@ -1,2 +1,4 @@ +require("./custom-checkbox"); require("./carts-list"); require("./items-filter"); +require("./items-list"); diff --git a/site/views/items-filter.js b/site/views/items-filter.js index 3fac787..7aae936 100644 --- a/site/views/items-filter.js +++ b/site/views/items-filter.js @@ -1,22 +1,137 @@ -const { dom, getDynamicElements } = require("../misc"); +const { today, dom, getBooleanAttribute } = require("../misc"); +const { stores, STORE_KEYS, BUDGET_BRANDS } = require("../model/stores"); const { View } = require("./view"); class ItemsFilter extends View { constructor() { super(); + this._hidePriceChanges = getBooleanAttribute(this, "pricechanges") ? "" : "hidden"; + this._hidePriceDirection = getBooleanAttribute(this, "pricedirection") ? "" : "hidden"; + this._hideStores = getBooleanAttribute(this, "stores") ? "" : "hidden"; + this._hideMisc = getBooleanAttribute(this, "misc") ? "" : "hidden"; + this._placeholder = this.hasAttribute("placeholder") ? this.getAttribute("placeholder") : "Produkte suchen... (mind. 3 Zeichen)"; + this.innerHTML = /*html*/ ` -
- - + + +
+ + ${STORE_KEYS.map( + (store) => /*html*/ ` + ` + ).join("")} +
+ +
+ + +
+ +
+ + +
+ +
+ + + +
- -
-
`; + this.classList.add("items-filter"); + + const elements = this.elements; + + const DEBOUNCE_MS = 100; + let timeoutId; + elements.query.addEventListener("input", (event) => { + event.stopPropagation(); + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + this.dispatchEvent( + new CustomEvent("change", { + bubbles: true, + cancelable: true, + }) + ); + }, DEBOUNCE_MS); + }); + + elements.allStores.addEventListener("change", (event) => { + event.stopPropagation(); + const checked = elements.allStores.checked; + STORE_KEYS.forEach((store) => (elements[store].checked = checked)); + this.fireChangeEvent(); + }); + + elements.priceChangesToday.addEventListener("change", (event) => { + event.stopPropagation(); + if (elements.priceChangesToday.checked) elements.priceDirection.classList.remove("hidden"); + else elements.priceDirection.classList.add("hidden"); + this.fireChangeEvent(); + }); + + elements.priceChangesSinceLast.addEventListener("change", (event) => { + event.stopPropagation(); + if (elements.priceChangesSinceLast.checked) elements.priceDirection.classList.add("hidden"); + else elements.priceDirection.classList.remove("hidden"); + this.fireChangeEvent(); + }); + + this.setupEventHandlers(); + } + + render() { + const now = performance.now(); + const elements = this.elements; + const items = this.model.items; + const dates = {}; + for (const item of items) { + if (item.priceHistory.length == 1) continue; + for (let i = 0; i < item.priceHistory.length; i++) { + const price = item.priceHistory[i]; + if (i + 1 < item.priceHistory.length) { + if (item.priceHistory[i].price != item.priceHistory[i + 1].price) + dates[price.date] = dates[price.date] ? dates[price.date] + 1 : 1; + } + } + } + + const priceChangesDates = elements.priceChangesDate; + priceChangesDates.innerHTML = ""; + for (const date of Object.keys(dates).sort((a, b) => b.localeCompare(a))) { + const dateDom = dom("option"); + dateDom.value = date; + dateDom.innerText = `${date} (${dates[date]})`; + priceChangesDates.append(dateDom); + } + console.log("Rendering items filter took " + (performance.now() - now) / 1000); + } + + get checkedStores() { + return STORE_KEYS.filter((store) => elements[store].checked); } } diff --git a/site/views/items-list.js b/site/views/items-list.js new file mode 100644 index 0000000..2826dbe --- /dev/null +++ b/site/views/items-list.js @@ -0,0 +1,70 @@ +const { downloadJSON, dom, getDynamicElements } = require("../misc"); +const { View } = require("./view"); + +class ItemsList extends View { + constructor() { + super(); + + this.innerHTML = /*html*/ ` +
+
+
+ + + Teilen + JSON + + +
+
+ +
+ + + + + + + + + +
KetteNamePreis +
+ `; + + this._itemTemplate = dom( + "tr", + /*html*/ ` + + +
+ + +
+ + + + + + + + + + ` + ); + } + + render() { + const items = model.items; + } +} + +customElements.define("items-list", ItemsList); diff --git a/site/views/view.js b/site/views/view.js index 27c72c5..040b425 100644 --- a/site/views/view.js +++ b/site/views/view.js @@ -3,16 +3,99 @@ class View extends HTMLElement { super(); this._model = null; this._listener = () => this.render(); + this._disableChangeEvent = false; + } + + get elements() { + const elements = this.querySelectorAll("[x-id]"); + const result = {}; + elements.forEach((element) => (result[element.getAttribute("x-id")] = element)); + return result; } set model(model) { if (this._model) this._model.removeListener(this._listener); this._model = model; this._model.addListener(this._listener); - this.render(model); + this.render(); + } + + get model() { + return this._model; + } + + get state() { + const elements = this.elements; + const properties = ["checked", "value"]; + const state = {}; + for (const key of Object.keys(elements)) { + const element = elements[key]; + if (!element.hasAttribute("x-state")) continue; + const elementState = {}; + for (const property of properties) { + if (property in element) { + elementState[property] = element[property]; + } + } + state[key] = elementState; + } + return state; + } + + set state(state) { + const elements = this.elements; + this._disableChangeEvent = true; + for (const key of Object.keys(elements)) { + const elementState = state[key]; + if (elementState) { + const element = elements[key]; + for (const property in elementState) { + element[property] = elementState[property]; + if (element.localName === "input" && element.getAttribute("type") === "radio") { + const changeEvent = new CustomEvent("change", { + bubbles: true, + cancelable: true, + }); + element.dispatchEvent(changeEvent); + } + } + } + } + this._disableChangeEvent = false; + this.fireChangeEvent(); } render() {} + + setupEventHandlers() { + const handler = (event) => { + event.stopPropagation(); + this.fireChangeEvent(); + }; + + const elements = this.elements; + for (const key of Object.keys(elements)) { + const element = elements[key]; + if (element.hasAttribute("x-change")) { + element.addEventListener("change", handler); + } + if (element.hasAttribute("x-click")) { + element.addEventListener("click", handler); + } + if (element.hasAttribute("x-input")) { + element.addEventListener("click", handler); + } + } + } + + fireChangeEvent() { + if (this._disableChangeEvent) return; + const event = new CustomEvent("change", { + bubbles: true, + cancelable: true, + }); + this.dispatchEvent(event); + } } exports.View = View;