From b8528d7e918790e3883a0f0445153b6b4bc568ef Mon Sep 17 00:00:00 2001 From: Luis Pardo Date: Fri, 13 Jun 2025 23:23:23 +0000 Subject: [PATCH 01/12] Copy of wc --- .../javascript-wc-indexeddb/.gitignore | 2 + .../javascript-wc-indexeddb/README.md | 37 + .../components/todo-app/todo-app.component.js | 147 ++++ .../components/todo-app/todo-app.template.js | 14 + .../todo-bottombar.component.js | 80 ++ .../todo-bottombar/todo-bottombar.template.js | 22 + .../todo-item/todo-item.component.js | 190 +++++ .../todo-item/todo-item.template.js | 19 + .../todo-list/todo-list.component.js | 129 +++ .../todo-list/todo-list.template.js | 8 + .../todo-topbar/todo-topbar.component.js | 131 +++ .../todo-topbar/todo-topbar.template.js | 17 + .../dist/hooks/useDoubleClick.js | 19 + .../dist/hooks/useKeyListener.js | 23 + .../dist/hooks/useRouter.js | 43 + .../javascript-wc-indexeddb/dist/index.html | 30 + .../dist/styles/app.constructable.js | 15 + .../dist/styles/bottombar.constructable.js | 158 ++++ .../dist/styles/footer.css | 26 + .../dist/styles/global.constructable.js | 86 ++ .../dist/styles/global.css | 88 ++ .../dist/styles/header.css | 21 + .../dist/styles/main.constructable.js | 11 + .../dist/styles/todo-item.constructable.js | 147 ++++ .../dist/styles/todo-list.constructable.js | 15 + .../dist/styles/topbar.constructable.js | 90 +++ .../dist/utils/nanoid.js | 41 + .../javascript-wc-indexeddb/index.html | 30 + .../javascript-wc-indexeddb/package-lock.json | 756 ++++++++++++++++++ .../javascript-wc-indexeddb/package.json | 22 + .../javascript-wc-indexeddb/scripts/build.js | 94 +++ .../components/todo-app/todo-app.component.js | 147 ++++ .../components/todo-app/todo-app.template.js | 14 + .../todo-bottombar.component.js | 80 ++ .../todo-bottombar/todo-bottombar.template.js | 22 + .../todo-item/todo-item.component.js | 190 +++++ .../todo-item/todo-item.template.js | 19 + .../todo-list/todo-list.component.js | 128 +++ .../todo-list/todo-list.template.js | 8 + .../todo-topbar/todo-topbar.component.js | 131 +++ .../todo-topbar/todo-topbar.template.js | 17 + .../src/hooks/useDoubleClick.js | 19 + .../src/hooks/useKeyListener.js | 23 + .../src/hooks/useRouter.js | 43 + .../src/utils/nanoid.js | 41 + 45 files changed, 3393 insertions(+) create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/.gitignore create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/README.md create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-app/todo-app.component.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-app/todo-app.template.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-bottombar/todo-bottombar.component.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-bottombar/todo-bottombar.template.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-item/todo-item.component.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-item/todo-item.template.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-list/todo-list.component.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-list/todo-list.template.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-topbar/todo-topbar.component.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-topbar/todo-topbar.template.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/hooks/useDoubleClick.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/hooks/useKeyListener.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/hooks/useRouter.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/index.html create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/app.constructable.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/bottombar.constructable.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/footer.css create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/global.constructable.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/global.css create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/header.css create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/main.constructable.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/todo-item.constructable.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/todo-list.constructable.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/topbar.constructable.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/utils/nanoid.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/index.html create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/package-lock.json create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/package.json create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/scripts/build.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-app/todo-app.component.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-app/todo-app.template.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-bottombar/todo-bottombar.component.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-bottombar/todo-bottombar.template.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-item/todo-item.component.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-item/todo-item.template.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-list/todo-list.component.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-list/todo-list.template.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-topbar/todo-topbar.component.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-topbar/todo-topbar.template.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/hooks/useDoubleClick.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/hooks/useKeyListener.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/hooks/useRouter.js create mode 100644 resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/utils/nanoid.js diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/.gitignore b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/.gitignore new file mode 100644 index 000000000..03e05e4c0 --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +/node_modules diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/README.md b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/README.md new file mode 100644 index 000000000..11351f614 --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/README.md @@ -0,0 +1,37 @@ +# Speedometer 3.0: TodoMVC: Web Components + +## Description + +A todoMVC application implemented with native web components. +It utilizes custom elements and html templates to build reusable components. + +In contrast to other workloads, this application uses an updated set of css rules and an optimized dom structure to ensure the application follows best practices in regards to accessibility. + +## Built steps + +A simple build script copies all necessary files to a `dist` folder. +It does not rely on compilers or transpilers and serves raw html, css and js files to the user. + +``` +npm run build +``` + +## Requirements + +The only requirement is an installation of Node, to be able to install dependencies and run scripts to serve a local server. + +``` +* Node (min version: 18.13.0) +* NPM (min version: 8.19.3) +``` + +## Local preview + +``` +terminal: +1. npm install +2. npm run dev + +browser: +1. http://localhost:7005/ +``` diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-app/todo-app.component.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-app/todo-app.component.js new file mode 100644 index 000000000..877d0c29d --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-app/todo-app.component.js @@ -0,0 +1,147 @@ +import template from "./todo-app.template.js"; +import { useRouter } from "../../hooks/useRouter.js"; + +import globalStyles from "../../styles/global.constructable.js"; +import appStyles from "../../styles/app.constructable.js"; +import mainStyles from "../../styles/main.constructable.js"; +class TodoApp extends HTMLElement { + #isReady = false; + #data = []; + constructor() { + super(); + + const node = document.importNode(template.content, true); + this.topbar = node.querySelector("todo-topbar"); + this.list = node.querySelector("todo-list"); + this.bottombar = node.querySelector("todo-bottombar"); + + this.shadow = this.attachShadow({ mode: "open" }); + this.htmlDirection = document.dir || "ltr"; + this.setAttribute("dir", this.htmlDirection); + this.shadow.adoptedStyleSheets = [globalStyles, appStyles, mainStyles]; + this.shadow.append(node); + + this.addItem = this.addItem.bind(this); + this.toggleItem = this.toggleItem.bind(this); + this.removeItem = this.removeItem.bind(this); + this.updateItem = this.updateItem.bind(this); + this.toggleItems = this.toggleItems.bind(this); + this.clearCompletedItems = this.clearCompletedItems.bind(this); + this.routeChange = this.routeChange.bind(this); + + this.router = useRouter(); + } + + get isReady() { + return this.#isReady; + } + + getInstance() { + return this; + } + + addItem(event) { + const { detail: item } = event; + + this.#data.push(item); + this.list.addItem(item); + + this.update("add-item", item.id); + } + + toggleItem(event) { + this.#data.forEach((entry) => { + if (entry.id === event.detail.id) + entry.completed = event.detail.completed; + }); + + this.update("toggle-item", event.detail.id); + } + + removeItem(event) { + this.#data.forEach((entry, index) => { + if (entry.id === event.detail.id) + this.#data.splice(index, 1); + }); + + this.update("remove-item", event.detail.id); + } + + updateItem(event) { + this.#data.forEach((entry) => { + if (entry.id === event.detail.id) + entry.title = event.detail.title; + }); + + this.update("update-item", event.detail.id); + } + + toggleItems(event) { + this.list.toggleItems(event.detail.completed); + } + + clearCompletedItems() { + this.list.removeCompletedItems(); + } + + update(type = "", id = "") { + const totalItems = this.#data.length; + const activeItems = this.#data.filter((entry) => !entry.completed).length; + const completedItems = totalItems - activeItems; + + this.list.setAttribute("total-items", totalItems); + this.list.updateElements(type, id); + + this.topbar.setAttribute("total-items", totalItems); + this.topbar.setAttribute("active-items", activeItems); + this.topbar.setAttribute("completed-items", completedItems); + + this.bottombar.setAttribute("total-items", totalItems); + this.bottombar.setAttribute("active-items", activeItems); + } + + addListeners() { + this.topbar.addEventListener("toggle-all", this.toggleItems); + this.topbar.addEventListener("add-item", this.addItem); + + this.list.listNode.addEventListener("toggle-item", this.toggleItem); + this.list.listNode.addEventListener("remove-item", this.removeItem); + this.list.listNode.addEventListener("update-item", this.updateItem); + + this.bottombar.addEventListener("clear-completed-items", this.clearCompletedItems); + } + + removeListeners() { + this.topbar.removeEventListener("toggle-all", this.toggleItems); + this.topbar.removeEventListener("add-item", this.addItem); + + this.list.listNode.removeEventListener("toggle-item", this.toggleItem); + this.list.listNode.removeEventListener("remove-item", this.removeItem); + this.list.listNode.removeEventListener("update-item", this.updateItem); + + this.bottombar.removeEventListener("clear-completed-items", this.clearCompletedItems); + } + + routeChange(route) { + const routeName = route.split("/")[1] || "all"; + this.list.updateRoute(routeName); + this.bottombar.updateRoute(routeName); + this.topbar.updateRoute(routeName); + } + + connectedCallback() { + this.update("connected"); + this.addListeners(); + this.router.initRouter(this.routeChange); + this.#isReady = true; + } + + disconnectedCallback() { + this.removeListeners(); + this.#isReady = false; + } +} + +customElements.define("todo-app", TodoApp); + +export default TodoApp; diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-app/todo-app.template.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-app/todo-app.template.js new file mode 100644 index 000000000..1a55a8194 --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-app/todo-app.template.js @@ -0,0 +1,14 @@ +const template = document.createElement("template"); + +template.id = "todo-app-template"; +template.innerHTML = ` +
+ +
+ +
+ +
+`; + +export default template; diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-bottombar/todo-bottombar.component.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-bottombar/todo-bottombar.component.js new file mode 100644 index 000000000..cc0203c48 --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-bottombar/todo-bottombar.component.js @@ -0,0 +1,80 @@ +import template from "./todo-bottombar.template.js"; + +import globalStyles from "../../styles/global.constructable.js"; +import bottombarStyles from "../../styles/bottombar.constructable.js"; + +class TodoBottombar extends HTMLElement { + static get observedAttributes() { + return ["total-items", "active-items"]; + } + + constructor() { + super(); + + const node = document.importNode(template.content, true); + this.element = node.querySelector(".bottombar"); + this.clearCompletedButton = node.querySelector(".clear-completed-button"); + this.todoStatus = node.querySelector(".todo-status"); + this.filterLinks = node.querySelectorAll(".filter-link"); + + this.shadow = this.attachShadow({ mode: "open" }); + this.htmlDirection = document.dir || "ltr"; + this.setAttribute("dir", this.htmlDirection); + this.shadow.adoptedStyleSheets = [globalStyles, bottombarStyles]; + this.shadow.append(node); + + this.clearCompletedItems = this.clearCompletedItems.bind(this); + } + + updateDisplay() { + if (parseInt(this["total-items"]) !== 0) + this.element.style.display = "block"; + else + this.element.style.display = "none"; + + this.todoStatus.textContent = `${this["active-items"]} ${this["active-items"] === "1" ? "item" : "items"} left!`; + } + + updateRoute(route) { + this.filterLinks.forEach((link) => { + if (link.dataset.route === route) + link.classList.add("selected"); + else + link.classList.remove("selected"); + }); + } + + clearCompletedItems() { + this.dispatchEvent(new CustomEvent("clear-completed-items")); + } + + addListeners() { + this.clearCompletedButton.addEventListener("click", this.clearCompletedItems); + } + + removeListeners() { + this.clearCompletedButton.removeEventListener("click", this.clearCompletedItems); + } + + attributeChangedCallback(property, oldValue, newValue) { + if (oldValue === newValue) + return; + this[property] = newValue; + + if (this.isConnected) + this.updateDisplay(); + } + + connectedCallback() { + this.updateDisplay(); + this.addListeners(); + } + + disconnectedCallback() { + this.removeListeners(); + } +} + +customElements.define("todo-bottombar", TodoBottombar); + +export default TodoBottombar; diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-bottombar/todo-bottombar.template.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-bottombar/todo-bottombar.template.js new file mode 100644 index 000000000..4f34ca92d --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-bottombar/todo-bottombar.template.js @@ -0,0 +1,22 @@ +const template = document.createElement("template"); + +template.id = "todo-bottombar-template"; +template.innerHTML = ` + +`; + +export default template; diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-item/todo-item.component.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-item/todo-item.component.js new file mode 100644 index 000000000..a082628a8 --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-item/todo-item.component.js @@ -0,0 +1,190 @@ +import template from "./todo-item.template.js"; +import { useDoubleClick } from "../../hooks/useDoubleClick.js"; +import { useKeyListener } from "../../hooks/useKeyListener.js"; + +import globalStyles from "../../styles/global.constructable.js"; +import itemStyles from "../../styles/todo-item.constructable.js"; + +class TodoItem extends HTMLElement { + static get observedAttributes() { + return ["itemid", "itemtitle", "itemcompleted"]; + } + + constructor() { + super(); + + // Renamed this.id to this.itemid and this.title to this.itemtitle. + // When the component assigns to this.id or this.title, this causes the browser's implementation of the existing setters to run, which convert these property sets into internal setAttribute calls. This can have surprising consequences. + // [Issue]: https://github.com/WebKit/Speedometer/issues/313 + this.itemid = ""; + this.itemtitle = "Todo Item"; + this.itemcompleted = "false"; + + const node = document.importNode(template.content, true); + this.item = node.querySelector(".todo-item"); + this.toggleLabel = node.querySelector(".toggle-todo-label"); + this.toggleInput = node.querySelector(".toggle-todo-input"); + this.todoText = node.querySelector(".todo-item-text"); + this.todoButton = node.querySelector(".remove-todo-button"); + this.editLabel = node.querySelector(".edit-todo-label"); + this.editInput = node.querySelector(".edit-todo-input"); + + this.shadow = this.attachShadow({ mode: "open" }); + this.htmlDirection = document.dir || "ltr"; + this.setAttribute("dir", this.htmlDirection); + this.shadow.adoptedStyleSheets = [globalStyles, itemStyles]; + this.shadow.append(node); + + this.keysListeners = []; + + this.updateItem = this.updateItem.bind(this); + this.toggleItem = this.toggleItem.bind(this); + this.removeItem = this.removeItem.bind(this); + this.startEdit = this.startEdit.bind(this); + this.stopEdit = this.stopEdit.bind(this); + this.cancelEdit = this.cancelEdit.bind(this); + + if (window.extraTodoItemCssToAdopt) { + let extraAdoptedStyleSheet = new CSSStyleSheet(); + extraAdoptedStyleSheet.replaceSync(window.extraTodoItemCssToAdopt); + this.shadow.adoptedStyleSheets.push(extraAdoptedStyleSheet); + } + } + + update(...args) { + args.forEach((argument) => { + switch (argument) { + case "itemid": + if (this.itemid !== undefined) + this.item.id = `todo-item-${this.itemid}`; + break; + case "itemtitle": + if (this.itemtitle !== undefined) { + this.todoText.textContent = this.itemtitle; + this.editInput.value = this.itemtitle; + } + break; + case "itemcompleted": + this.toggleInput.checked = this.itemcompleted === "true" ? true : false; + break; + } + }); + } + + startEdit() { + this.item.classList.add("editing"); + this.editInput.value = this.itemtitle; + this.editInput.focus(); + } + + stopEdit() { + this.item.classList.remove("editing"); + } + + cancelEdit() { + this.editInput.blur(); + } + + toggleItem() { + // The todo-list checks the "completed" attribute to filter based on route + // (therefore the completed state needs to already be updated before the check) + this.setAttribute("itemcompleted", this.toggleInput.checked); + + this.dispatchEvent( + new CustomEvent("toggle-item", { + detail: { id: this.itemid, completed: this.toggleInput.checked }, + bubbles: true, + }) + ); + } + + removeItem() { + // The todo-list keeps a reference to all elements and updates the list on removal. + // (therefore the removal has to happen after the list is updated) + this.dispatchEvent( + new CustomEvent("remove-item", { + detail: { id: this.itemid }, + bubbles: true, + }) + ); + this.remove(); + } + + updateItem(event) { + if (event.target.value !== this.itemtitle) { + if (!event.target.value.length) { + this.removeItem(); + } else { + this.setAttribute("itemtitle", event.target.value); + this.dispatchEvent( + new CustomEvent("update-item", { + detail: { id: this.itemid, title: event.target.value }, + bubbles: true, + }) + ); + } + } + + this.cancelEdit(); + } + + addListeners() { + this.toggleInput.addEventListener("change", this.toggleItem); + this.todoText.addEventListener("click", useDoubleClick(this.startEdit, 500)); + this.editInput.addEventListener("blur", this.stopEdit); + this.todoButton.addEventListener("click", this.removeItem); + + this.keysListeners.forEach((listener) => listener.connect()); + } + + removeListeners() { + this.toggleInput.removeEventListener("change", this.toggleItem); + this.todoText.removeEventListener("click", this.startEdit); + this.editInput.removeEventListener("blur", this.stopEdit); + this.todoButton.removeEventListener("click", this.removeItem); + + this.keysListeners.forEach((listener) => listener.disconnect()); + } + + attributeChangedCallback(property, oldValue, newValue) { + if (oldValue === newValue) + return; + this[property] = newValue; + + if (this.isConnected) + this.update(property); + } + + connectedCallback() { + this.update("itemid", "itemtitle", "itemcompleted"); + + this.keysListeners.push( + useKeyListener({ + target: this.editInput, + event: "keyup", + callbacks: { + ["Enter"]: this.updateItem, + ["Escape"]: this.cancelEdit, + }, + }), + useKeyListener({ + target: this.todoText, + event: "keyup", + callbacks: { + [" "]: this.startEdit, // this feels weird + }, + }) + ); + + this.addListeners(); + } + + disconnectedCallback() { + this.removeListeners(); + this.keysListeners = []; + } +} + +customElements.define("todo-item", TodoItem); + +export default TodoItem; diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-item/todo-item.template.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-item/todo-item.template.js new file mode 100644 index 000000000..9a67675fd --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-item/todo-item.template.js @@ -0,0 +1,19 @@ +const template = document.createElement("template"); + +template.id = "todo-item-template"; +template.innerHTML = ` +
  • +
    + + + Placeholder Text + +
    +
    + + +
    +
  • +`; + +export default template; diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-list/todo-list.component.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-list/todo-list.component.js new file mode 100644 index 000000000..8e90e291f --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-list/todo-list.component.js @@ -0,0 +1,129 @@ +import template from "./todo-list.template.js"; +import TodoItem from "../todo-item/todo-item.component.js"; + +import globalStyles from "../../styles/global.constructable.js"; +import listStyles from "../../styles/todo-list.constructable.js"; + +class TodoList extends HTMLElement { + static get observedAttributes() { + return ["total-items"]; + } + + #elements = []; + #route = undefined; + + constructor() { + super(); + + const node = document.importNode(template.content, true); + this.listNode = node.querySelector(".todo-list"); + + this.shadow = this.attachShadow({ mode: "open" }); + this.htmlDirection = document.dir || "ltr"; + this.setAttribute("dir", this.htmlDirection); + this.shadow.adoptedStyleSheets = [globalStyles, listStyles]; + this.shadow.append(node); + this.classList.add("show-priority"); + + if (window.extraTodoListCssToAdopt) { + let extraAdoptedStyleSheet = new CSSStyleSheet(); + extraAdoptedStyleSheet.replaceSync(window.extraTodoListCssToAdopt); + this.shadow.adoptedStyleSheets.push(extraAdoptedStyleSheet); + } + } + + addItem(entry) { + const { id, title, completed } = entry; + const element = new TodoItem(); + + element.setAttribute("itemid", id); + element.setAttribute("itemtitle", title); + element.setAttribute("itemcompleted", completed); + + const elementIndex = this.#elements.length; + this.#elements.push(element); + this.listNode.append(element); + element.setAttribute("data-priority", 4 - (elementIndex % 5)); + } + + addItems(items) { + items.forEach((entry) => this.addItem(entry)); + } + + removeCompletedItems() { + this.#elements = this.#elements.filter((element) => { + if (element.itemcompleted === "true") + element.removeItem(); + + return element.itemcompleted === "false"; + }); + } + + toggleItems(completed) { + this.#elements.forEach((element) => { + if (completed && element.itemcompleted === "false") + element.toggleInput.click(); + else if (!completed && element.itemcompleted === "true") + element.toggleInput.click(); + }); + } + + updateStyles() { + if (parseInt(this["total-items"]) !== 0) + this.listNode.style.display = "block"; + else + this.listNode.style.display = "none"; + } + + updateView(element) { + switch (this.#route) { + case "completed": + element.style.display = element.itemcompleted === "true" ? "block" : "none"; + break; + case "active": + element.style.display = element.itemcompleted === "true" ? "none" : "block"; + break; + default: + element.style.display = "block"; + } + } + + updateElements(type = "", id = "") { + switch (type) { + case "route-change": + this.#elements.forEach((element) => this.updateView(element)); + break; + case "toggle-item": + case "add-item": + this.#elements.forEach((element) => { + if (element.itemid === id) + this.updateView(element); + }); + break; + case "remove-item": + this.#elements = this.#elements.filter((element) => element.itemid !== id); + break; + } + } + + updateRoute(route) { + this.#route = route; + this.updateElements("route-change"); + } + + attributeChangedCallback(property, oldValue, newValue) { + if (oldValue === newValue) + return; + this[property] = newValue; + if (this.isConnected) + this.updateStyles(); + } + + connectedCallback() { + this.updateStyles(); + } +} + +customElements.define("todo-list", TodoList); + +export default TodoList; diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-list/todo-list.template.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-list/todo-list.template.js new file mode 100644 index 000000000..dac383b95 --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-list/todo-list.template.js @@ -0,0 +1,8 @@ +const template = document.createElement("template"); + +template.id = "todo-list-template"; +template.innerHTML = ` + +`; + +export default template; diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-topbar/todo-topbar.component.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-topbar/todo-topbar.component.js new file mode 100644 index 000000000..f024d7ca1 --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-topbar/todo-topbar.component.js @@ -0,0 +1,131 @@ +import template from "./todo-topbar.template.js"; +import { useKeyListener } from "../../hooks/useKeyListener.js"; +import { nanoid } from "../../utils/nanoid.js"; + +import globalStyles from "../../styles/global.constructable.js"; +import topbarStyles from "../../styles/topbar.constructable.js"; + +class TodoTopbar extends HTMLElement { + static get observedAttributes() { + return ["total-items", "active-items", "completed-items"]; + } + + #route = undefined; + + constructor() { + super(); + + const node = document.importNode(template.content, true); + this.todoInput = node.querySelector("#new-todo"); + this.toggleInput = node.querySelector("#toggle-all"); + this.toggleContainer = node.querySelector(".toggle-all-container"); + + this.shadow = this.attachShadow({ mode: "open" }); + this.htmlDirection = document.dir || "ltr"; + this.setAttribute("dir", this.htmlDirection); + this.shadow.adoptedStyleSheets = [globalStyles, topbarStyles]; + this.shadow.append(node); + + this.keysListeners = []; + + this.toggleAll = this.toggleAll.bind(this); + this.addItem = this.addItem.bind(this); + } + + toggleAll(event) { + this.dispatchEvent( + new CustomEvent("toggle-all", { + detail: { completed: event.target.checked }, + }) + ); + } + + addItem(event) { + if (!event.target.value.length) + return; + + this.dispatchEvent( + new CustomEvent("add-item", { + detail: { + id: nanoid(), + title: event.target.value, + completed: false, + }, + }) + ); + + event.target.value = ""; + } + + updateDisplay() { + if (!parseInt(this["total-items"])) { + this.toggleContainer.style.display = "none"; + return; + } + + this.toggleContainer.style.display = "block"; + + switch (this.#route) { + case "active": + this.toggleInput.checked = false; + this.toggleInput.disabled = !parseInt(this["active-items"]); + break; + case "completed": + this.toggleInput.checked = parseInt(this["completed-items"]); + this.toggleInput.disabled = !parseInt(this["completed-items"]); + break; + default: + this.toggleInput.checked = this["total-items"] === this["completed-items"]; + this.toggleInput.disabled = false; + } + } + + updateRoute(route) { + this.#route = route; + this.updateDisplay(); + } + + addListeners() { + this.toggleInput.addEventListener("change", this.toggleAll); + this.keysListeners.forEach((listener) => listener.connect()); + } + + removeListeners() { + this.toggleInput.removeEventListener("change", this.toggleAll); + this.keysListeners.forEach((listener) => listener.disconnect()); + } + + attributeChangedCallback(property, oldValue, newValue) { + if (oldValue === newValue) + return; + this[property] = newValue; + + if (this.isConnected) + this.updateDisplay(); + } + + connectedCallback() { + this.keysListeners.push( + useKeyListener({ + target: this.todoInput, + event: "keyup", + callbacks: { + ["Enter"]: this.addItem, + }, + }) + ); + + this.updateDisplay(); + this.addListeners(); + this.todoInput.focus(); + } + + disconnectedCallback() { + this.removeListeners(); + this.keysListeners = []; + } +} + +customElements.define("todo-topbar", TodoTopbar); + +export default TodoTopbar; diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-topbar/todo-topbar.template.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-topbar/todo-topbar.template.js new file mode 100644 index 000000000..ce323b683 --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-topbar/todo-topbar.template.js @@ -0,0 +1,17 @@ +const template = document.createElement("template"); + +template.id = "todo-topbar-template"; +template.innerHTML = ` +
    +
    + + +
    + +
    +`; + +export default template; diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/hooks/useDoubleClick.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/hooks/useDoubleClick.js new file mode 100644 index 000000000..a1fe952fe --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/hooks/useDoubleClick.js @@ -0,0 +1,19 @@ +/** + * A simple function to normalize a double-click and a double-tab action. + * There is currently no comparable tab action to dblclick. + * + * @param {Function} fn + * @param {number} delay + * @returns + */ +export function useDoubleClick(fn, delay) { + let last = 0; + return function (...args) { + const now = new Date().getTime(); + const difference = now - last; + if (difference < delay && difference > 0) + fn.apply(this, args); + + last = now; + }; +} diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/hooks/useKeyListener.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/hooks/useKeyListener.js new file mode 100644 index 000000000..453747d54 --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/hooks/useKeyListener.js @@ -0,0 +1,23 @@ +export function useKeyListener(props) { + const { target, event, callbacks } = props; + + function handleEvent(event) { + Object.keys(callbacks).forEach((key) => { + if (event.key === key) + callbacks[key](event); + }); + } + + function connect() { + target.addEventListener(event, handleEvent); + } + + function disconnect() { + target.removeEventListener(event, handleEvent); + } + + return { + connect, + disconnect, + }; +} diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/hooks/useRouter.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/hooks/useRouter.js new file mode 100644 index 000000000..ab1ab618a --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/hooks/useRouter.js @@ -0,0 +1,43 @@ +/** + * Listens for hash change of the url and calls onChange if available. + * + * @param {Function} callback + * @returns Methods to interact with useRouter. + */ +export const useRouter = (callback) => { + let onChange = callback; + let current = ""; + + /** + * Change event handler. + */ + const handleChange = () => { + current = document.location.hash; + /* istanbul ignore else */ + if (onChange) + onChange(document.location.hash); + }; + + /** + * Initializes router and adds listeners. + * + * @param {Function} callback + */ + const initRouter = (callback) => { + onChange = callback; + window.addEventListener("hashchange", handleChange); + window.addEventListener("load", handleChange); + }; + + /** + * Removes listeners + */ + const disableRouter = () => { + window.removeEventListener("hashchange", handleChange); + window.removeEventListener("load", handleChange); + }; + + const getRoute = () => current.split("/").slice(-1)[0]; + + return { initRouter, getRoute, disableRouter }; +}; diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/index.html b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/index.html new file mode 100644 index 000000000..a8fec9787 --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/index.html @@ -0,0 +1,30 @@ + + + + + + + TodoMVC: JavaScript Web Components + + + + + + + + + + + +
    +

    todos

    +
    + + + + diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/app.constructable.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/app.constructable.js new file mode 100644 index 000000000..8ac77f26a --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/app.constructable.js @@ -0,0 +1,15 @@ +const sheet = new CSSStyleSheet(); +sheet.replaceSync(`:host { + display: block; + box-shadow: none !important; + min-height: 68px; +} + +.app { + background: #fff; + margin: 24px 16px 40px 16px; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); +} +`); +export default sheet; diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/bottombar.constructable.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/bottombar.constructable.js new file mode 100644 index 000000000..46904de8a --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/bottombar.constructable.js @@ -0,0 +1,158 @@ +const sheet = new CSSStyleSheet(); +sheet.replaceSync(`:host { + display: block; + box-shadow: none !important; +} + +.bottombar { + padding: 10px 0; + height: 41px; + text-align: center; + font-size: 15px; + border-top: 1px solid #e6e6e6; + position: relative; +} + +.bottombar::before { + content: ""; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + pointer-events: none; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +.todo-status { + text-align: left; + padding: 3px; + height: 32px; + line-height: 26px; + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); +} + +.todo-count { + font-weight: 300; +} + +.filter-list { + margin: 0; + padding: 0; + list-style: none; + display: inline-block; + position: absolute; + left: 0; + right: 0; + top: 50%; + transform: translateY(-50%); +} + +.filter-item { + display: inline-block; +} + +.filter-link { + color: inherit; + margin: 3px; + padding: 0 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; + cursor: pointer; + display: block; + height: 26px; + line-height: 26px; +} + +.filter-link:hover { + border-color: #db7676; +} + +.filter-link.selected { + border-color: #ce4646; +} + +.clear-completed-button, +.clear-completed-button:active { + text-decoration: none; + cursor: pointer; + padding: 3px; + height: 32px; + line-height: 26px; + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); +} + +.clear-completed-button:hover { + text-decoration: underline; +} + +/* rtl support */ +html[dir="rtl"] .todo-status, +:host([dir="rtl"]) .todo-status { + right: 12px; + left: unset; +} + +html[dir="rtl"] .clear-completed-button, +:host([dir="rtl"]) .clear-completed-button { + left: 12px; + right: unset; +} + +@media (max-width: 430px) { + .bottombar { + height: 120px; + } + + .todo-status { + display: block; + text-align: center; + position: relative; + left: unset; + right: unset; + top: unset; + transform: unset; + } + + .filter-list { + display: block; + position: relative; + left: unset; + right: unset; + top: unset; + transform: unset; + } + + .clear-completed-button, + .clear-completed-button:active { + display: block; + margin: 0 auto; + position: relative; + left: unset; + right: unset; + top: unset; + transform: unset; + } + + html[dir="rtl"] .todo-status, + :host([dir="rtl"]) .todo-status { + right: unset; + left: unset; + } + + html[dir="rtl"] .clear-completed-button, + :host([dir="rtl"]) .clear-completed-button { + left: unset; + right: unset; + } +} +`); +export default sheet; diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/footer.css b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/footer.css new file mode 100644 index 000000000..0ff918f43 --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/footer.css @@ -0,0 +1,26 @@ +:host { + display: block; + box-shadow: none !important; +} + +.footer { + margin: 65px auto 0; + color: #4d4d4d; + font-size: 11px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-align: center; +} + +.footer-text { + line-height: 1; +} + +.footer-link { + color: inherit; + text-decoration: none; + font-weight: 400; +} + +.footer-link:hover { + text-decoration: underline; +} diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/global.constructable.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/global.constructable.js new file mode 100644 index 000000000..7ff85b07f --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/global.constructable.js @@ -0,0 +1,86 @@ +const sheet = new CSSStyleSheet(); +sheet.replaceSync(`*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; +} + +body { + font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #111; + min-width: 300px; + max-width: 582px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 300; +} + +:focus { + box-shadow: inset 0 0 2px 2px #cf7d7d !important; + outline: 0 !important; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +input { + position: relative; + margin: 0; + font-size: inherit; + font-family: inherit; + font-weight: inherit; + color: inherit; + padding: 0; + border: 0; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +input:placeholder-shown { + text-overflow: ellipsis; +} + + +.visually-hidden { + border: 0; + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 1px; + width: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + position: absolute; + white-space: nowrap; +} + +.truncate-singleline { + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: block !important; +} +`); +export default sheet; diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/global.css b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/global.css new file mode 100644 index 000000000..22ee5e16c --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/global.css @@ -0,0 +1,88 @@ +/** defaults */ +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; +} + +body { + font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #111; + min-width: 300px; + max-width: 582px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 300; +} + +:focus { + box-shadow: inset 0 0 2px 2px #cf7d7d !important; + outline: 0 !important; +} + +/** resets */ +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +input { + position: relative; + margin: 0; + font-size: inherit; + font-family: inherit; + font-weight: inherit; + color: inherit; + padding: 0; + border: 0; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +input:placeholder-shown { + text-overflow: ellipsis; +} + +/* utility classes */ + +/* used for things that should be hidden in the ui, +but useful for people who use screen readers */ +.visually-hidden { + border: 0; + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 1px; + width: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + position: absolute; + white-space: nowrap; +} + +.truncate-singleline { + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: block !important; +} diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/header.css b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/header.css new file mode 100644 index 000000000..56d2a4064 --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/header.css @@ -0,0 +1,21 @@ +:host { + display: block; + box-shadow: none !important; +} + +.header { + margin-top: 27px; +} + +.title { + width: 100%; + font-size: 80px; + line-height: 80px; + margin: 0; + font-weight: 200; + text-align: center; + color: #b83f45; + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/main.constructable.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/main.constructable.js new file mode 100644 index 000000000..66ea0b30c --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/main.constructable.js @@ -0,0 +1,11 @@ +const sheet = new CSSStyleSheet(); +sheet.replaceSync(`:host { + display: block; + box-shadow: none !important; +} + +.main { + position: relative; +} +`); +export default sheet; diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/todo-item.constructable.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/todo-item.constructable.js new file mode 100644 index 000000000..59dba7f77 --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/todo-item.constructable.js @@ -0,0 +1,147 @@ +const sheet = new CSSStyleSheet(); +sheet.replaceSync(`:host { + display: block; + box-shadow: none !important; +} + +:host(:last-child) > .todo-item { + border-bottom: none; +} + +.todo-item { + position: relative; + font-size: 24px; + border-bottom: 1px solid #ededed; + height: 60px; +} + +.todo-item.editing { + border-bottom: none; + padding: 0; +} + +.edit-todo-container { + display: none; +} + +.todo-item.editing .edit-todo-container { + display: block; +} + +.edit-todo-input { + padding: 0 16px 0 60px; + width: 100%; + height: 60px; + font-size: 24px; + line-height: 1.4em; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03); + background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20%20style%3D%22opacity%3A%200.2%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23949494%22%20stroke-width%3D%223%22/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: center left; +} + +.display-todo { + position: relative; +} + +.todo-item.editing .display-todo { + display: none; +} + +.toggle-todo-input { + text-align: center; + width: 40px; + + height: auto; + position: absolute; + top: 0; + bottom: 0; + left: 3px; + margin: auto 0; + border: none; appearance: none; + cursor: pointer; +} + +.todo-item-text { + overflow-wrap: break-word; + padding: 0 60px; + display: block; + line-height: 60px; + transition: color 0.4s; + font-weight: 400; + color: #484848; + + background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23949494%22%20stroke-width%3D%223%22/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: center left; +} + +.toggle-todo-input:checked + .todo-item-text { + color: #949494; + text-decoration: line-through; + background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%2359A193%22%20stroke-width%3D%223%22%2F%3E%3Cpath%20fill%3D%22%233EA390%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22%2F%3E%3C%2Fsvg%3E"); +} + +.remove-todo-button { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 30px; + color: #949494; + transition: color 0.2s ease-out; + cursor: pointer; +} + +.remove-todo-button:hover, +.remove-todo-button:focus { + color: #c18585; +} + +.remove-todo-button::after { + content: "×"; + display: block; + height: 100%; + line-height: 1.1; +} + +.todo-item:hover .remove-todo-button { + display: block; +} + +@media screen and (-webkit-min-device-pixel-ratio: 0) { + .toggle-todo-input { + background: none; + height: 40px; + } +} + +@media (max-width: 430px) { + .remove-todo-button { + display: block; + } +} + +html[dir="rtl"] .toggle-todo-input, +:host([dir="rtl"]) .toggle-todo-input { + right: 3px; + left: unset; +} + +html[dir="rtl"] .todo-item-text, +:host([dir="rtl"]) .todo-item-text { + background-position: center right 6px; +} + +html[dir="rtl"] .remove-todo-button, +:host([dir="rtl"]) .remove-todo-button { + left: 10px; + right: unset; +} +`); +export default sheet; diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/todo-list.constructable.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/todo-list.constructable.js new file mode 100644 index 000000000..3d11133dd --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/todo-list.constructable.js @@ -0,0 +1,15 @@ +const sheet = new CSSStyleSheet(); +sheet.replaceSync(`:host { + display: block; + box-shadow: none !important; +} + +.todo-list { + margin: 0; + padding: 0; + list-style: none; + display: block; + border-top: 1px solid #e6e6e6; +} +`); +export default sheet; diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/topbar.constructable.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/topbar.constructable.js new file mode 100644 index 000000000..5ecb8a231 --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/styles/topbar.constructable.js @@ -0,0 +1,90 @@ +const sheet = new CSSStyleSheet(); +sheet.replaceSync(`:host { + display: block; + box-shadow: none !important; +} + +.topbar { + position: relative; +} + +.new-todo-input { + padding: 0 32px 0 60px; + width: 100%; + height: 68px; + font-size: 24px; + line-height: 1.4em; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03); +} + +.new-todo-input::placeholder { + font-style: italic; + font-weight: 400; + color: rgba(0, 0, 0, 0.4); +} + +.toggle-all-container { + width: 45px; + height: 68px; + position: absolute; + left: 0; + top: 0; +} + +.toggle-all-input { + width: 45px; + height: 45px; + font-size: 0; + position: absolute; + top: 11.5px; + left: 0; + border: none; + appearance: none; + cursor: pointer; +} + +.toggle-all-label { + display: flex; + align-items: center; + justify-content: center; + width: 45px; + height: 68px; + font-size: 0; + position: absolute; + top: 0; + left: 0; + cursor: pointer; +} + +.toggle-all-label::before { + content: "❯"; + display: inline-block; + font-size: 22px; + color: #949494; + padding: 10px 27px 10px 27px; + transform: rotate(90deg); +} + +.toggle-all-input:checked + .toggle-all-label::before { + color: #484848; +} + +@media screen and (-webkit-min-device-pixel-ratio: 0) { + .toggle-all-input { + background: none; + } +} + +html[dir="rtl"] .new-todo-input, +:host([dir="rtl"]) .new-todo-input { + padding: 0 60px 0 32px; +} + +html[dir="rtl"] .toggle-all-container, +:host([dir="rtl"]) .toggle-all-container { + right: 0; + left: unset; +} +`); +export default sheet; diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/utils/nanoid.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/utils/nanoid.js new file mode 100644 index 000000000..5df154f1f --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/utils/nanoid.js @@ -0,0 +1,41 @@ +/* Borrowed from https://github.com/ai/nanoid/blob/3.0.2/non-secure/index.js + +The MIT License (MIT) + +Copyright 2017 Andrey Sitnik + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + +// This alphabet uses `A-Za-z0-9_-` symbols. +// The order of characters is optimized for better gzip and brotli compression. +// References to the same file (works both for gzip and brotli): +// `'use`, `andom`, and `rict'` +// References to the brotli default dictionary: +// `-26T`, `1983`, `40px`, `75px`, `bush`, `jack`, `mind`, `very`, and `wolf` +let urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict"; + +export function nanoid(size = 21) { + let id = ""; + // A compact alternative for `for (var i = 0; i < step; i++)`. + let i = size; + while (i--) { + // `| 0` is more compact and faster than `Math.floor()`. + id += urlAlphabet[(Math.random() * 64) | 0]; + } + return id; +} diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/index.html b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/index.html new file mode 100644 index 000000000..6820a9346 --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/index.html @@ -0,0 +1,30 @@ + + + + + + + TodoMVC: JavaScript Web Components + + + + + + + + + + + +
    +

    todos

    +
    + +
    + + + + +
    + + diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/package-lock.json b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/package-lock.json new file mode 100644 index 000000000..34dfaedc3 --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/package-lock.json @@ -0,0 +1,756 @@ +{ + "name": "todomvc-javascript-web-components", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "todomvc-javascript-web-components", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "http-server": "^14.1.1", + "todomvc-css": "file:../../todomvc-css" + }, + "engines": { + "node": ">=18.13.0", + "npm": ">=8.19.3" + } + }, + "../../todomvc-css": { + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@rollup/plugin-babel": "^6.0.3", + "@rollup/plugin-commonjs": "^25.0.0", + "@rollup/plugin-html": "^1.0.2", + "@rollup/plugin-node-resolve": "^15.0.2", + "@rollup/plugin-terser": "^0.4.3", + "@rollup/pluginutils": "^5.0.2", + "fs-extra": "^11.1.1", + "globby": "^13.2.0", + "http-server": "^14.1.1", + "rollup": "^3.23.0", + "rollup-plugin-cleaner": "^1.0.0", + "rollup-plugin-copy-merge": "^1.0.2", + "rollup-plugin-import-css": "^3.2.1", + "strip-comments": "^2.0.1", + "stylelint": "^15.6.2", + "stylelint-config-standard": "^33.0.0" + }, + "engines": { + "node": ">=18.13.0", + "npm": ">=8.19.3" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/corser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", + "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "node_modules/follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-server": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", + "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==", + "dependencies": { + "basic-auth": "^2.0.1", + "chalk": "^4.1.2", + "corser": "^2.0.1", + "he": "^1.2.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy": "^1.18.1", + "mime": "^1.6.0", + "minimist": "^1.2.6", + "opener": "^1.5.1", + "portfinder": "^1.0.28", + "secure-compare": "3.0.1", + "union": "~0.5.0", + "url-join": "^4.0.1" + }, + "bin": { + "http-server": "bin/http-server" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/portfinder": { + "version": "1.0.32", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", + "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", + "dependencies": { + "async": "^2.6.4", + "debug": "^3.2.7", + "mkdirp": "^0.5.6" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/secure-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", + "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==" + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/todomvc-css": { + "resolved": "../../todomvc-css", + "link": true + }, + "node_modules/union": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", + "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", + "dependencies": { + "qs": "^6.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + } + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "requires": { + "lodash": "^4.17.14" + } + }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "corser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", + "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==" + }, + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==" + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + }, + "html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "requires": { + "whatwg-encoding": "^2.0.0" + } + }, + "http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-server": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", + "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==", + "requires": { + "basic-auth": "^2.0.1", + "chalk": "^4.1.2", + "corser": "^2.0.1", + "he": "^1.2.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy": "^1.18.1", + "mime": "^1.6.0", + "minimist": "^1.2.6", + "opener": "^1.5.1", + "portfinder": "^1.0.28", + "secure-compare": "3.0.1", + "union": "~0.5.0", + "url-join": "^4.0.1" + } + }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "requires": { + "minimist": "^1.2.6" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" + }, + "opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==" + }, + "portfinder": { + "version": "1.0.32", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", + "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", + "requires": { + "async": "^2.6.4", + "debug": "^3.2.7", + "mkdirp": "^0.5.6" + } + }, + "qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "secure-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", + "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==" + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "todomvc-css": { + "version": "file:../../todomvc-css", + "requires": { + "@rollup/plugin-babel": "^6.0.3", + "@rollup/plugin-commonjs": "^25.0.0", + "@rollup/plugin-html": "^1.0.2", + "@rollup/plugin-node-resolve": "^15.0.2", + "@rollup/plugin-terser": "^0.4.3", + "@rollup/pluginutils": "^5.0.2", + "fs-extra": "^11.1.1", + "globby": "^13.2.0", + "http-server": "^14.1.1", + "rollup": "^3.23.0", + "rollup-plugin-cleaner": "^1.0.0", + "rollup-plugin-copy-merge": "^1.0.2", + "rollup-plugin-import-css": "^3.2.1", + "strip-comments": "^2.0.1", + "stylelint": "^15.6.2", + "stylelint-config-standard": "^33.0.0" + } + }, + "union": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", + "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", + "requires": { + "qs": "^6.4.0" + } + }, + "url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" + }, + "whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "requires": { + "iconv-lite": "0.6.3" + } + } + } +} diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/package.json b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/package.json new file mode 100644 index 000000000..a04fdab85 --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/package.json @@ -0,0 +1,22 @@ +{ + "name": "todomvc-javascript-web-components", + "version": "1.0.0", + "description": "TodoMVC app written with JavaScript using web components.", + "engines": { + "node": ">=18.13.0", + "npm": ">=8.19.3" + }, + "private": true, + "scripts": { + "dev": "http-server ./ -p 7005 -c-1 --cors -o", + "build": "node scripts/build.js", + "serve": "http-server ./dist -p 7006 -c-1 --cors -o" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "http-server": "^14.1.1", + "todomvc-css": "file:../../todomvc-css" + } +} diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/scripts/build.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/scripts/build.js new file mode 100644 index 000000000..d5fa77557 --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/scripts/build.js @@ -0,0 +1,94 @@ +const fs = require("fs").promises; +const getDirName = require("path").dirname; + +const rootDirectory = "./"; +const sourceDirectory = "./src"; +const targetDirectory = "./dist"; + +const htmlFile = "index.html"; + +const filesToMove = { + index: [ + { src: "node_modules/todomvc-css/dist/global.css", dest: "styles/global.css" }, + { src: "node_modules/todomvc-css/dist/header.css", dest: "styles/header.css" }, + { src: "node_modules/todomvc-css/dist/footer.css", dest: "styles/footer.css" }, + ], + app: [ + { src: "node_modules/todomvc-css/dist/global.constructable.js", dest: "styles/global.constructable.js" }, + { src: "node_modules/todomvc-css/dist/app.constructable.js", dest: "styles/app.constructable.js" }, + { src: "node_modules/todomvc-css/dist/topbar.constructable.js", dest: "styles/topbar.constructable.js" }, + { src: "node_modules/todomvc-css/dist/main.constructable.js", dest: "styles/main.constructable.js" }, + { src: "node_modules/todomvc-css/dist/bottombar.constructable.js", dest: "styles/bottombar.constructable.js" }, + { src: "node_modules/todomvc-css/dist/todo-list.constructable.js", dest: "styles/todo-list.constructable.js" }, + { src: "node_modules/todomvc-css/dist/todo-item.constructable.js", dest: "styles/todo-item.constructable.js" }, + ], +}; + +const importsToRename = { + src: "../../../node_modules/todomvc-css/dist/", + dest: "../../styles/", + files: [ + "components/todo-app/todo-app.component.js", + "components/todo-bottombar/todo-bottombar.component.js", + "components/todo-item/todo-item.component.js", + "components/todo-list/todo-list.component.js", + "components/todo-topbar/todo-topbar.component.js", + ], +}; + +const copy = async (src, dest) => { + await fs.mkdir(getDirName(dest), { recursive: true }); + await fs.copyFile(src, dest); +}; + +const copyFilesToMove = async (files) => { + for (let i = 0; i < files.length; i++) + await copy(files[i].src, `${targetDirectory}/${files[i].dest}`); +}; + +const updateImports = async ({ file, src, dest }) => { + let contents = await fs.readFile(`${targetDirectory}/${file}`, "utf8"); + contents = contents.replaceAll(src, dest); + await fs.writeFile(`${targetDirectory}/${file}`, contents); +}; + +const build = async () => { + // remove dist directory if it exists + await fs.rm(targetDirectory, { recursive: true, force: true }); + + // re-create the directory. + await fs.mkdir(targetDirectory); + + // copy src folder + await fs.cp(sourceDirectory, targetDirectory, { recursive: true }, (err) => { + if (err) + console.error(err); + }); + + // copy files to Move + for (const key in filesToMove) + copyFilesToMove(filesToMove[key]); + + // read html file + let contents = await fs.readFile(`${rootDirectory}/${htmlFile}`, "utf8"); + + // remove base paths from files to move + const filesToMoveForIndex = filesToMove.index; + for (let i = 0; i < filesToMoveForIndex.length; i++) + contents = contents.replace(filesToMoveForIndex[i].src, filesToMoveForIndex[i].dest); + + // remove basePath from source directory + const basePath = `${sourceDirectory.split("/")[1]}/`; + const re = new RegExp(basePath, "g"); + contents = contents.replace(re, ""); + + // write html files + await fs.writeFile(`${targetDirectory}/${htmlFile}`, contents); + + // rename imports in modules + importsToRename.files.forEach((file) => updateImports({ file, src: importsToRename.src, dest: importsToRename.dest })); + + console.log("done!!"); +}; + +build(); diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-app/todo-app.component.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-app/todo-app.component.js new file mode 100644 index 000000000..691980f3e --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-app/todo-app.component.js @@ -0,0 +1,147 @@ +import template from "./todo-app.template.js"; +import { useRouter } from "../../hooks/useRouter.js"; + +import globalStyles from "../../../node_modules/todomvc-css/dist/global.constructable.js"; +import appStyles from "../../../node_modules/todomvc-css/dist/app.constructable.js"; +import mainStyles from "../../../node_modules/todomvc-css/dist/main.constructable.js"; +class TodoApp extends HTMLElement { + #isReady = false; + #data = []; + constructor() { + super(); + + const node = document.importNode(template.content, true); + this.topbar = node.querySelector("todo-topbar"); + this.list = node.querySelector("todo-list"); + this.bottombar = node.querySelector("todo-bottombar"); + + this.shadow = this.attachShadow({ mode: "open" }); + this.htmlDirection = document.dir || "ltr"; + this.setAttribute("dir", this.htmlDirection); + this.shadow.adoptedStyleSheets = [globalStyles, appStyles, mainStyles]; + this.shadow.append(node); + + this.addItem = this.addItem.bind(this); + this.toggleItem = this.toggleItem.bind(this); + this.removeItem = this.removeItem.bind(this); + this.updateItem = this.updateItem.bind(this); + this.toggleItems = this.toggleItems.bind(this); + this.clearCompletedItems = this.clearCompletedItems.bind(this); + this.routeChange = this.routeChange.bind(this); + + this.router = useRouter(); + } + + get isReady() { + return this.#isReady; + } + + getInstance() { + return this; + } + + addItem(event) { + const { detail: item } = event; + + this.#data.push(item); + this.list.addItem(item); + + this.update("add-item", item.id); + } + + toggleItem(event) { + this.#data.forEach((entry) => { + if (entry.id === event.detail.id) + entry.completed = event.detail.completed; + }); + + this.update("toggle-item", event.detail.id); + } + + removeItem(event) { + this.#data.forEach((entry, index) => { + if (entry.id === event.detail.id) + this.#data.splice(index, 1); + }); + + this.update("remove-item", event.detail.id); + } + + updateItem(event) { + this.#data.forEach((entry) => { + if (entry.id === event.detail.id) + entry.title = event.detail.title; + }); + + this.update("update-item", event.detail.id); + } + + toggleItems(event) { + this.list.toggleItems(event.detail.completed); + } + + clearCompletedItems() { + this.list.removeCompletedItems(); + } + + update(type = "", id = "") { + const totalItems = this.#data.length; + const activeItems = this.#data.filter((entry) => !entry.completed).length; + const completedItems = totalItems - activeItems; + + this.list.setAttribute("total-items", totalItems); + this.list.updateElements(type, id); + + this.topbar.setAttribute("total-items", totalItems); + this.topbar.setAttribute("active-items", activeItems); + this.topbar.setAttribute("completed-items", completedItems); + + this.bottombar.setAttribute("total-items", totalItems); + this.bottombar.setAttribute("active-items", activeItems); + } + + addListeners() { + this.topbar.addEventListener("toggle-all", this.toggleItems); + this.topbar.addEventListener("add-item", this.addItem); + + this.list.listNode.addEventListener("toggle-item", this.toggleItem); + this.list.listNode.addEventListener("remove-item", this.removeItem); + this.list.listNode.addEventListener("update-item", this.updateItem); + + this.bottombar.addEventListener("clear-completed-items", this.clearCompletedItems); + } + + removeListeners() { + this.topbar.removeEventListener("toggle-all", this.toggleItems); + this.topbar.removeEventListener("add-item", this.addItem); + + this.list.listNode.removeEventListener("toggle-item", this.toggleItem); + this.list.listNode.removeEventListener("remove-item", this.removeItem); + this.list.listNode.removeEventListener("update-item", this.updateItem); + + this.bottombar.removeEventListener("clear-completed-items", this.clearCompletedItems); + } + + routeChange(route) { + const routeName = route.split("/")[1] || "all"; + this.list.updateRoute(routeName); + this.bottombar.updateRoute(routeName); + this.topbar.updateRoute(routeName); + } + + connectedCallback() { + this.update("connected"); + this.addListeners(); + this.router.initRouter(this.routeChange); + this.#isReady = true; + } + + disconnectedCallback() { + this.removeListeners(); + this.#isReady = false; + } +} + +customElements.define("todo-app", TodoApp); + +export default TodoApp; diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-app/todo-app.template.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-app/todo-app.template.js new file mode 100644 index 000000000..1a55a8194 --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-app/todo-app.template.js @@ -0,0 +1,14 @@ +const template = document.createElement("template"); + +template.id = "todo-app-template"; +template.innerHTML = ` +
    + +
    + +
    + +
    +`; + +export default template; diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-bottombar/todo-bottombar.component.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-bottombar/todo-bottombar.component.js new file mode 100644 index 000000000..f5b8896c5 --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-bottombar/todo-bottombar.component.js @@ -0,0 +1,80 @@ +import template from "./todo-bottombar.template.js"; + +import globalStyles from "../../../node_modules/todomvc-css/dist/global.constructable.js"; +import bottombarStyles from "../../../node_modules/todomvc-css/dist/bottombar.constructable.js"; + +class TodoBottombar extends HTMLElement { + static get observedAttributes() { + return ["total-items", "active-items"]; + } + + constructor() { + super(); + + const node = document.importNode(template.content, true); + this.element = node.querySelector(".bottombar"); + this.clearCompletedButton = node.querySelector(".clear-completed-button"); + this.todoStatus = node.querySelector(".todo-status"); + this.filterLinks = node.querySelectorAll(".filter-link"); + + this.shadow = this.attachShadow({ mode: "open" }); + this.htmlDirection = document.dir || "ltr"; + this.setAttribute("dir", this.htmlDirection); + this.shadow.adoptedStyleSheets = [globalStyles, bottombarStyles]; + this.shadow.append(node); + + this.clearCompletedItems = this.clearCompletedItems.bind(this); + } + + updateDisplay() { + if (parseInt(this["total-items"]) !== 0) + this.element.style.display = "block"; + else + this.element.style.display = "none"; + + this.todoStatus.textContent = `${this["active-items"]} ${this["active-items"] === "1" ? "item" : "items"} left!`; + } + + updateRoute(route) { + this.filterLinks.forEach((link) => { + if (link.dataset.route === route) + link.classList.add("selected"); + else + link.classList.remove("selected"); + }); + } + + clearCompletedItems() { + this.dispatchEvent(new CustomEvent("clear-completed-items")); + } + + addListeners() { + this.clearCompletedButton.addEventListener("click", this.clearCompletedItems); + } + + removeListeners() { + this.clearCompletedButton.removeEventListener("click", this.clearCompletedItems); + } + + attributeChangedCallback(property, oldValue, newValue) { + if (oldValue === newValue) + return; + this[property] = newValue; + + if (this.isConnected) + this.updateDisplay(); + } + + connectedCallback() { + this.updateDisplay(); + this.addListeners(); + } + + disconnectedCallback() { + this.removeListeners(); + } +} + +customElements.define("todo-bottombar", TodoBottombar); + +export default TodoBottombar; diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-bottombar/todo-bottombar.template.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-bottombar/todo-bottombar.template.js new file mode 100644 index 000000000..4f34ca92d --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-bottombar/todo-bottombar.template.js @@ -0,0 +1,22 @@ +const template = document.createElement("template"); + +template.id = "todo-bottombar-template"; +template.innerHTML = ` + +`; + +export default template; diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-item/todo-item.component.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-item/todo-item.component.js new file mode 100644 index 000000000..7a310bf82 --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-item/todo-item.component.js @@ -0,0 +1,190 @@ +import template from "./todo-item.template.js"; +import { useDoubleClick } from "../../hooks/useDoubleClick.js"; +import { useKeyListener } from "../../hooks/useKeyListener.js"; + +import globalStyles from "../../../node_modules/todomvc-css/dist/global.constructable.js"; +import itemStyles from "../../../node_modules/todomvc-css/dist/todo-item.constructable.js"; + +class TodoItem extends HTMLElement { + static get observedAttributes() { + return ["itemid", "itemtitle", "itemcompleted"]; + } + + constructor() { + super(); + + // Renamed this.id to this.itemid and this.title to this.itemtitle. + // When the component assigns to this.id or this.title, this causes the browser's implementation of the existing setters to run, which convert these property sets into internal setAttribute calls. This can have surprising consequences. + // [Issue]: https://github.com/WebKit/Speedometer/issues/313 + this.itemid = ""; + this.itemtitle = "Todo Item"; + this.itemcompleted = "false"; + + const node = document.importNode(template.content, true); + this.item = node.querySelector(".todo-item"); + this.toggleLabel = node.querySelector(".toggle-todo-label"); + this.toggleInput = node.querySelector(".toggle-todo-input"); + this.todoText = node.querySelector(".todo-item-text"); + this.todoButton = node.querySelector(".remove-todo-button"); + this.editLabel = node.querySelector(".edit-todo-label"); + this.editInput = node.querySelector(".edit-todo-input"); + + this.shadow = this.attachShadow({ mode: "open" }); + this.htmlDirection = document.dir || "ltr"; + this.setAttribute("dir", this.htmlDirection); + this.shadow.adoptedStyleSheets = [globalStyles, itemStyles]; + this.shadow.append(node); + + this.keysListeners = []; + + this.updateItem = this.updateItem.bind(this); + this.toggleItem = this.toggleItem.bind(this); + this.removeItem = this.removeItem.bind(this); + this.startEdit = this.startEdit.bind(this); + this.stopEdit = this.stopEdit.bind(this); + this.cancelEdit = this.cancelEdit.bind(this); + + if (window.extraTodoItemCssToAdopt) { + let extraAdoptedStyleSheet = new CSSStyleSheet(); + extraAdoptedStyleSheet.replaceSync(window.extraTodoItemCssToAdopt); + this.shadow.adoptedStyleSheets.push(extraAdoptedStyleSheet); + } + } + + update(...args) { + args.forEach((argument) => { + switch (argument) { + case "itemid": + if (this.itemid !== undefined) + this.item.id = `todo-item-${this.itemid}`; + break; + case "itemtitle": + if (this.itemtitle !== undefined) { + this.todoText.textContent = this.itemtitle; + this.editInput.value = this.itemtitle; + } + break; + case "itemcompleted": + this.toggleInput.checked = this.itemcompleted === "true" ? true : false; + break; + } + }); + } + + startEdit() { + this.item.classList.add("editing"); + this.editInput.value = this.itemtitle; + this.editInput.focus(); + } + + stopEdit() { + this.item.classList.remove("editing"); + } + + cancelEdit() { + this.editInput.blur(); + } + + toggleItem() { + // The todo-list checks the "completed" attribute to filter based on route + // (therefore the completed state needs to already be updated before the check) + this.setAttribute("itemcompleted", this.toggleInput.checked); + + this.dispatchEvent( + new CustomEvent("toggle-item", { + detail: { id: this.itemid, completed: this.toggleInput.checked }, + bubbles: true, + }) + ); + } + + removeItem() { + // The todo-list keeps a reference to all elements and updates the list on removal. + // (therefore the removal has to happen after the list is updated) + this.dispatchEvent( + new CustomEvent("remove-item", { + detail: { id: this.itemid }, + bubbles: true, + }) + ); + this.remove(); + } + + updateItem(event) { + if (event.target.value !== this.itemtitle) { + if (!event.target.value.length) { + this.removeItem(); + } else { + this.setAttribute("itemtitle", event.target.value); + this.dispatchEvent( + new CustomEvent("update-item", { + detail: { id: this.itemid, title: event.target.value }, + bubbles: true, + }) + ); + } + } + + this.cancelEdit(); + } + + addListeners() { + this.toggleInput.addEventListener("change", this.toggleItem); + this.todoText.addEventListener("click", useDoubleClick(this.startEdit, 500)); + this.editInput.addEventListener("blur", this.stopEdit); + this.todoButton.addEventListener("click", this.removeItem); + + this.keysListeners.forEach((listener) => listener.connect()); + } + + removeListeners() { + this.toggleInput.removeEventListener("change", this.toggleItem); + this.todoText.removeEventListener("click", this.startEdit); + this.editInput.removeEventListener("blur", this.stopEdit); + this.todoButton.removeEventListener("click", this.removeItem); + + this.keysListeners.forEach((listener) => listener.disconnect()); + } + + attributeChangedCallback(property, oldValue, newValue) { + if (oldValue === newValue) + return; + this[property] = newValue; + + if (this.isConnected) + this.update(property); + } + + connectedCallback() { + this.update("itemid", "itemtitle", "itemcompleted"); + + this.keysListeners.push( + useKeyListener({ + target: this.editInput, + event: "keyup", + callbacks: { + ["Enter"]: this.updateItem, + ["Escape"]: this.cancelEdit, + }, + }), + useKeyListener({ + target: this.todoText, + event: "keyup", + callbacks: { + [" "]: this.startEdit, // this feels weird + }, + }) + ); + + this.addListeners(); + } + + disconnectedCallback() { + this.removeListeners(); + this.keysListeners = []; + } +} + +customElements.define("todo-item", TodoItem); + +export default TodoItem; diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-item/todo-item.template.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-item/todo-item.template.js new file mode 100644 index 000000000..9a67675fd --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-item/todo-item.template.js @@ -0,0 +1,19 @@ +const template = document.createElement("template"); + +template.id = "todo-item-template"; +template.innerHTML = ` +
  • +
    + + + Placeholder Text + +
    +
    + + +
    +
  • +`; + +export default template; diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-list/todo-list.component.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-list/todo-list.component.js new file mode 100644 index 000000000..1116fbe29 --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-list/todo-list.component.js @@ -0,0 +1,128 @@ +import template from "./todo-list.template.js"; +import TodoItem from "../todo-item/todo-item.component.js"; + +import globalStyles from "../../../node_modules/todomvc-css/dist/global.constructable.js"; +import listStyles from "../../../node_modules/todomvc-css/dist/todo-list.constructable.js"; + +class TodoList extends HTMLElement { + static get observedAttributes() { + return ["total-items"]; + } + + #elements = []; + #route = undefined; + + constructor() { + super(); + const node = document.importNode(template.content, true); + this.listNode = node.querySelector(".todo-list"); + + this.shadow = this.attachShadow({ mode: "open" }); + this.htmlDirection = document.dir || "ltr"; + this.setAttribute("dir", this.htmlDirection); + this.shadow.adoptedStyleSheets = [globalStyles, listStyles]; + this.shadow.append(node); + this.classList.add("show-priority"); + + if (window.extraTodoListCssToAdopt) { + let extraAdoptedStyleSheet = new CSSStyleSheet(); + extraAdoptedStyleSheet.replaceSync(window.extraTodoListCssToAdopt); + this.shadow.adoptedStyleSheets.push(extraAdoptedStyleSheet); + } + } + + addItem(entry) { + const { id, title, completed } = entry; + const element = new TodoItem(); + + element.setAttribute("itemid", id); + element.setAttribute("itemtitle", title); + element.setAttribute("itemcompleted", completed); + + const elementIndex = this.#elements.length; + this.#elements.push(element); + this.listNode.append(element); + element.setAttribute("data-priority", 4 - (elementIndex % 5)); + } + + addItems(items) { + items.forEach((entry) => this.addItem(entry)); + } + + removeCompletedItems() { + this.#elements = this.#elements.filter((element) => { + if (element.itemcompleted === "true") + element.removeItem(); + + return element.itemcompleted === "false"; + }); + } + + toggleItems(completed) { + this.#elements.forEach((element) => { + if (completed && element.itemcompleted === "false") + element.toggleInput.click(); + else if (!completed && element.itemcompleted === "true") + element.toggleInput.click(); + }); + } + + updateStyles() { + if (parseInt(this["total-items"]) !== 0) + this.listNode.style.display = "block"; + else + this.listNode.style.display = "none"; + } + + updateView(element) { + switch (this.#route) { + case "completed": + element.style.display = element.itemcompleted === "true" ? "block" : "none"; + break; + case "active": + element.style.display = element.itemcompleted === "true" ? "none" : "block"; + break; + default: + element.style.display = "block"; + } + } + + updateElements(type = "", id = "") { + switch (type) { + case "route-change": + this.#elements.forEach((element) => this.updateView(element)); + break; + case "toggle-item": + case "add-item": + this.#elements.forEach((element) => { + if (element.itemid === id) + this.updateView(element); + }); + break; + case "remove-item": + this.#elements = this.#elements.filter((element) => element.itemid !== id); + break; + } + } + + updateRoute(route) { + this.#route = route; + this.updateElements("route-change"); + } + + attributeChangedCallback(property, oldValue, newValue) { + if (oldValue === newValue) + return; + this[property] = newValue; + if (this.isConnected) + this.updateStyles(); + } + + connectedCallback() { + this.updateStyles(); + } +} + +customElements.define("todo-list", TodoList); + +export default TodoList; diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-list/todo-list.template.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-list/todo-list.template.js new file mode 100644 index 000000000..dac383b95 --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-list/todo-list.template.js @@ -0,0 +1,8 @@ +const template = document.createElement("template"); + +template.id = "todo-list-template"; +template.innerHTML = ` + +`; + +export default template; diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-topbar/todo-topbar.component.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-topbar/todo-topbar.component.js new file mode 100644 index 000000000..973fb0614 --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-topbar/todo-topbar.component.js @@ -0,0 +1,131 @@ +import template from "./todo-topbar.template.js"; +import { useKeyListener } from "../../hooks/useKeyListener.js"; +import { nanoid } from "../../utils/nanoid.js"; + +import globalStyles from "../../../node_modules/todomvc-css/dist/global.constructable.js"; +import topbarStyles from "../../../node_modules/todomvc-css/dist/topbar.constructable.js"; + +class TodoTopbar extends HTMLElement { + static get observedAttributes() { + return ["total-items", "active-items", "completed-items"]; + } + + #route = undefined; + + constructor() { + super(); + + const node = document.importNode(template.content, true); + this.todoInput = node.querySelector("#new-todo"); + this.toggleInput = node.querySelector("#toggle-all"); + this.toggleContainer = node.querySelector(".toggle-all-container"); + + this.shadow = this.attachShadow({ mode: "open" }); + this.htmlDirection = document.dir || "ltr"; + this.setAttribute("dir", this.htmlDirection); + this.shadow.adoptedStyleSheets = [globalStyles, topbarStyles]; + this.shadow.append(node); + + this.keysListeners = []; + + this.toggleAll = this.toggleAll.bind(this); + this.addItem = this.addItem.bind(this); + } + + toggleAll(event) { + this.dispatchEvent( + new CustomEvent("toggle-all", { + detail: { completed: event.target.checked }, + }) + ); + } + + addItem(event) { + if (!event.target.value.length) + return; + + this.dispatchEvent( + new CustomEvent("add-item", { + detail: { + id: nanoid(), + title: event.target.value, + completed: false, + }, + }) + ); + + event.target.value = ""; + } + + updateDisplay() { + if (!parseInt(this["total-items"])) { + this.toggleContainer.style.display = "none"; + return; + } + + this.toggleContainer.style.display = "block"; + + switch (this.#route) { + case "active": + this.toggleInput.checked = false; + this.toggleInput.disabled = !parseInt(this["active-items"]); + break; + case "completed": + this.toggleInput.checked = parseInt(this["completed-items"]); + this.toggleInput.disabled = !parseInt(this["completed-items"]); + break; + default: + this.toggleInput.checked = this["total-items"] === this["completed-items"]; + this.toggleInput.disabled = false; + } + } + + updateRoute(route) { + this.#route = route; + this.updateDisplay(); + } + + addListeners() { + this.toggleInput.addEventListener("change", this.toggleAll); + this.keysListeners.forEach((listener) => listener.connect()); + } + + removeListeners() { + this.toggleInput.removeEventListener("change", this.toggleAll); + this.keysListeners.forEach((listener) => listener.disconnect()); + } + + attributeChangedCallback(property, oldValue, newValue) { + if (oldValue === newValue) + return; + this[property] = newValue; + + if (this.isConnected) + this.updateDisplay(); + } + + connectedCallback() { + this.keysListeners.push( + useKeyListener({ + target: this.todoInput, + event: "keyup", + callbacks: { + ["Enter"]: this.addItem, + }, + }) + ); + + this.updateDisplay(); + this.addListeners(); + this.todoInput.focus(); + } + + disconnectedCallback() { + this.removeListeners(); + this.keysListeners = []; + } +} + +customElements.define("todo-topbar", TodoTopbar); + +export default TodoTopbar; diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-topbar/todo-topbar.template.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-topbar/todo-topbar.template.js new file mode 100644 index 000000000..ce323b683 --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-topbar/todo-topbar.template.js @@ -0,0 +1,17 @@ +const template = document.createElement("template"); + +template.id = "todo-topbar-template"; +template.innerHTML = ` +
    +
    + + +
    + +
    +`; + +export default template; diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/hooks/useDoubleClick.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/hooks/useDoubleClick.js new file mode 100644 index 000000000..a1fe952fe --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/hooks/useDoubleClick.js @@ -0,0 +1,19 @@ +/** + * A simple function to normalize a double-click and a double-tab action. + * There is currently no comparable tab action to dblclick. + * + * @param {Function} fn + * @param {number} delay + * @returns + */ +export function useDoubleClick(fn, delay) { + let last = 0; + return function (...args) { + const now = new Date().getTime(); + const difference = now - last; + if (difference < delay && difference > 0) + fn.apply(this, args); + + last = now; + }; +} diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/hooks/useKeyListener.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/hooks/useKeyListener.js new file mode 100644 index 000000000..453747d54 --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/hooks/useKeyListener.js @@ -0,0 +1,23 @@ +export function useKeyListener(props) { + const { target, event, callbacks } = props; + + function handleEvent(event) { + Object.keys(callbacks).forEach((key) => { + if (event.key === key) + callbacks[key](event); + }); + } + + function connect() { + target.addEventListener(event, handleEvent); + } + + function disconnect() { + target.removeEventListener(event, handleEvent); + } + + return { + connect, + disconnect, + }; +} diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/hooks/useRouter.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/hooks/useRouter.js new file mode 100644 index 000000000..ab1ab618a --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/hooks/useRouter.js @@ -0,0 +1,43 @@ +/** + * Listens for hash change of the url and calls onChange if available. + * + * @param {Function} callback + * @returns Methods to interact with useRouter. + */ +export const useRouter = (callback) => { + let onChange = callback; + let current = ""; + + /** + * Change event handler. + */ + const handleChange = () => { + current = document.location.hash; + /* istanbul ignore else */ + if (onChange) + onChange(document.location.hash); + }; + + /** + * Initializes router and adds listeners. + * + * @param {Function} callback + */ + const initRouter = (callback) => { + onChange = callback; + window.addEventListener("hashchange", handleChange); + window.addEventListener("load", handleChange); + }; + + /** + * Removes listeners + */ + const disableRouter = () => { + window.removeEventListener("hashchange", handleChange); + window.removeEventListener("load", handleChange); + }; + + const getRoute = () => current.split("/").slice(-1)[0]; + + return { initRouter, getRoute, disableRouter }; +}; diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/utils/nanoid.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/utils/nanoid.js new file mode 100644 index 000000000..5df154f1f --- /dev/null +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/utils/nanoid.js @@ -0,0 +1,41 @@ +/* Borrowed from https://github.com/ai/nanoid/blob/3.0.2/non-secure/index.js + +The MIT License (MIT) + +Copyright 2017 Andrey Sitnik + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + +// This alphabet uses `A-Za-z0-9_-` symbols. +// The order of characters is optimized for better gzip and brotli compression. +// References to the same file (works both for gzip and brotli): +// `'use`, `andom`, and `rict'` +// References to the brotli default dictionary: +// `-26T`, `1983`, `40px`, `75px`, `bush`, `jack`, `mind`, `very`, and `wolf` +let urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict"; + +export function nanoid(size = 21) { + let id = ""; + // A compact alternative for `for (var i = 0; i < step; i++)`. + let i = size; + while (i--) { + // `| 0` is more compact and faster than `Math.floor()`. + id += urlAlphabet[(Math.random() * 64) | 0]; + } + return id; +} From 560d87059561075c723a6879b418598f078aa3ca Mon Sep 17 00:00:00 2001 From: Luis Pardo Date: Fri, 13 Jun 2025 23:51:10 +0000 Subject: [PATCH 02/12] Add Memory cache logic --- .../todo-list/todo-list.component.js | 213 ++++++++++++++++++ 1 file changed, 213 insertions(+) diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-list/todo-list.component.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-list/todo-list.component.js index 1116fbe29..a560f66ed 100644 --- a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-list/todo-list.component.js +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-list/todo-list.component.js @@ -4,6 +4,216 @@ import TodoItem from "../todo-item/todo-item.component.js"; import globalStyles from "../../../node_modules/todomvc-css/dist/global.constructable.js"; import listStyles from "../../../node_modules/todomvc-css/dist/todo-list.constructable.js"; +class FixedSizeQueue { + #buffer; + #capacity; + #size = 0; + #head = 0; + #tail = 0; + + constructor(capacity = 10) { + if (capacity <= 0) { + throw new Error("Queue capacity must be positive"); + } + this.#capacity = capacity; + this.#buffer = new Array(capacity); + } + + /** + * Add an element to the rear of the queue + * @param {*} element - Element to add + * @returns {boolean} - True if element was added, false if queue is full + */ + enqueue(element) { + if (this.isFull()) { + return false; + } + + this.#buffer[this.#tail] = element; + this.#tail = (this.#tail + 1) % this.#capacity; + this.#size++; + return true; + } + + /** + * Add multiple elements to the rear of the queue + * @param {Array} elements - Array of elements to add + * @returns {number} - The number of elements that were added to the queue + */ + enqueueAll(elements) { + // Calculate available space and number of elements we can add + const availableSpace = this.#capacity - this.#size; + const elementsToAdd = Math.min(elements.length, availableSpace); + + // Add as many elements as will fit + for (let i = 0; i < elementsToAdd; i++) { + this.#buffer[this.#tail] = elements[i]; + this.#tail = (this.#tail + 1) % this.#capacity; + this.#size++; + } + + return elementsToAdd; + } + + /** + * Remove and return an element from the front of the queue + * @returns {*} - The element at the front of the queue, or undefined if empty + */ + dequeue() { + if (this.isEmpty()) { + return undefined; + } + + const element = this.#buffer[this.#head]; + this.#buffer[this.#head] = undefined; // Clear reference for GC + this.#head = (this.#head + 1) % this.#capacity; + this.#size--; + return element; + } + + /** + * Remove and return multiple elements from the front of the queue + * @param {number} count - Number of elements to dequeue + * @returns {Array} - Array of dequeued elements (may be fewer than requested if not enough available) + */ + dequeueMany(count) { + if (count <= 0) { + return []; + } + + const elementsToDequeue = Math.min(count, this.#size); + const result = new Array(elementsToDequeue); + + for (let i = 0; i < elementsToDequeue; i++) { + result[i] = this.#buffer[this.#head]; + this.#buffer[this.#head] = undefined; // Clear reference for GC + this.#head = (this.#head + 1) % this.#capacity; + this.#size--; + } + + return result; + } + + /** + * Return the element at the front of the queue without removing it + * @returns {*} - The element at the front of the queue, or undefined if empty + */ + peek() { + return this.isEmpty() ? undefined : this.#buffer[this.#head]; + } + + /** + * Check if the queue is empty + * @returns {boolean} - True if queue is empty + */ + isEmpty() { + return this.#size === 0; + } + + /** + * Check if the queue is full + * @returns {boolean} - True if queue is full + */ + isFull() { + return this.#size === this.#capacity; + } + + /** + * Get the current number of elements in the queue + * @returns {number} - Current size of the queue + */ + size() { + return this.#size; + } + + /** + * Get the maximum capacity of the queue + * @returns {number} - Maximum capacity of the queue + */ + capacity() { + return this.#capacity; + } + + /** + * Clear all elements from the queue + */ + clear() { + this.#buffer.fill(undefined); + this.#size = 0; + this.#head = 0; + this.#tail = 0; + } + + /** + * Convert queue to array (for debugging/testing purposes) + * @returns {Array} - Array representation of queue elements in order + */ + toArray() { + const result = new Array(this.#size); + for (let i = 0; i < this.#size; i++) { + result[i] = this.#buffer[(this.#head + i) % this.#capacity]; + } + return result; + } +} + + +class MemoryCacheManager { + + #cacheMaxSize = 10; + // before cache will be used to populate whole pages of items. + #beforeCache = new Array(); + + // after cache will be used to load the next item in the list, we need fifo access. + #afterCache = new FixedSizeQueue(10); + + constructor() { + this.currentId = -1; + this.cacheMaxSize = 10; + } + + shouldAddToAfterCache() { + return !this.#afterCache.isFull(); + } + + + addToAfterCache(element) { + if (this.#afterCache.isFull()) { + throw new Error("After cache is full, cannot add more items."); + } + this.#afterCache.enqueue(element); + } + + /** + * Add multiple elements to the forward cache + * @param {Array} elements - Array of elements to add to the forward cache + * @returns {number} - The number of elements that were added to the cache + */ + addMultipleToAfterCache(elements) { + return this.#afterCache.enqueueAll(elements); + } + + nextFromAfterCache() { + const result = this.#afterCache.dequeue(); + return result !== undefined ? result : null; + } + + getFromBeforeCache(numberOfItems) { + if (numberOfItems > this.#beforeCache.length) { + throw new Error("Not enough items in before cache to return."); + } + return this.#beforeCache.splice(this.#beforeCache.length - numberOfItems, numberOfItems); + } + + addToBeforeCache(elements) { + if (elements.length + this.#beforeCache.length > this.#cacheMaxSize) { + throw new Error("Before cache is full, cannot add more items."); + } + this.#beforeCache.push(...elements); + } +} + + class TodoList extends HTMLElement { static get observedAttributes() { return ["total-items"]; @@ -11,6 +221,9 @@ class TodoList extends HTMLElement { #elements = []; #route = undefined; + #memoryCacheManager = new MemoryCacheManager(); + #db; + #dbItemId = 0; constructor() { super(); From ace2acb817602a244b6c1a43203eab24f32276b8 Mon Sep 17 00:00:00 2001 From: Luis Pardo Date: Mon, 16 Jun 2025 21:49:12 +0000 Subject: [PATCH 03/12] Add basic indexedDb support --- .../todo-list/todo-list.component.js | 201 +++++++++++++++++- 1 file changed, 190 insertions(+), 11 deletions(-) diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-list/todo-list.component.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-list/todo-list.component.js index a560f66ed..bbbe89aa3 100644 --- a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-list/todo-list.component.js +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-list/todo-list.component.js @@ -213,6 +213,114 @@ class MemoryCacheManager { } } +/** + * Database manager for Todo items using IndexedDB + */ +class TodoDatabase { + #db; + #dbName; + #dbVersion; + #storeName = 'todos'; + #dbReadyCallback; + #numberOfPendingAddRequests = 0; + #numberOfPendingRemoveRequests = 0; + + /** + * Creates a new TodoDatabase instance + * @param {string} dbName - The name of the IndexedDB database + * @param {number} dbVersion - The version of the database + * @param {Function} readyCallback - Callback to invoke when the database is ready + */ + constructor(dbName = 'todosDB', dbVersion = 1, readyCallback = null) { + this.#dbName = dbName; + this.#dbVersion = dbVersion; + this.#dbReadyCallback = readyCallback; + this.initialize(); + } + + /** + * Initializes the IndexedDB database for storing todo items + */ + initialize() { + // Open the database connection + const request = indexedDB.open(this.#dbName, this.#dbVersion); + + // Handle database upgrade or creation + request.onupgradeneeded = (event) => { + this.#db = event.target.result; + + // Create an object store for our todos if it doesn't exist + if (!this.#db.objectStoreNames.contains(this.#storeName)) { + const todoStore = this.#db.createObjectStore(this.#storeName, { keyPath: 'id', autoIncrement: true }); + + // Create indexes for quick searches. + todoStore.createIndex('itemId', 'itemId', { unique: true }); + todoStore.createIndex('completed', 'completed', { unique: false }); + todoStore.createIndex('title', 'title', { unique: false }); + todoStore.createIndex('priority', 'priority', { unique: false }); + } + }; + + // Handle successful database opening + request.onsuccess = (event) => { + this.#db = event.target.result; + console.log('IndexedDB initialized successfully'); + + // Call ready callback if provided + if (this.#dbReadyCallback) { + this.#dbReadyCallback(); + } + }; + + // Handle errors + request.onerror = (event) => { + console.error('Error opening IndexedDB:', event.target.error); + }; + } + + /** + * Gets the database instance + * @returns {IDBDatabase} The IndexedDB database instance + */ + get database() { + return this.#db; + } + + /** + * Adds a todo item to the database + * @param {Object} item - The todo item to add + * @returns {Promise} A promise that resolves to the ID of the added item + */ + addItem(item) { + return new Promise((resolve, reject) => { + if (!this.#db) { + reject(new Error("Database not initialized")); + return; + } + + try { + const transaction = this.#db.transaction(this.#storeName, 'readwrite'); + const store = transaction.objectStore(this.#storeName); + + const request = store.add(item); + + this.#numberOfPendingAddRequests++; + + request.onsuccess = (event) => { + this.#numberOfPendingAddRequests--; + console.log("Item added to database:", event.target.result); + resolve(event.target.result); // Return the generated ID + }; + + request.onerror = (event) => { + reject(event.target.error); + }; + } catch (error) { + reject(error); + } + }); + } +} class TodoList extends HTMLElement { static get observedAttributes() { @@ -221,8 +329,9 @@ class TodoList extends HTMLElement { #elements = []; #route = undefined; + #onScreenMaxNumberOfItems = 10; #memoryCacheManager = new MemoryCacheManager(); - #db; + #todoDatabase; #dbItemId = 0; constructor() { @@ -242,20 +351,90 @@ class TodoList extends HTMLElement { extraAdoptedStyleSheet.replaceSync(window.extraTodoListCssToAdopt); this.shadow.adoptedStyleSheets.push(extraAdoptedStyleSheet); } + + // Initialize TodoDatabase + this.#initializeDatabase(); + } + + /** + * Initializes the IndexedDB database for storing todo items + * @private + */ + #initializeDatabase() { + // Create a new TodoDatabase instance with a callback for when DB is ready + this.#todoDatabase = new TodoDatabase('todosDB', 1, () => { + // Dispatch an event when the database is ready + this.dispatchEvent(new CustomEvent('db-ready')); + }); + } + + /** + * Gets the IndexedDB database instance + * @returns {IDBDatabase} The IndexedDB database instance + */ + get #db() { + return this.#todoDatabase?.database; } + /** + * Adds a todo item based on available space in different storage tiers + * @param {Object} entry - The todo item entry + */ addItem(entry) { const { id, title, completed } = entry; - const element = new TodoItem(); - - element.setAttribute("itemid", id); - element.setAttribute("itemtitle", title); - element.setAttribute("itemcompleted", completed); - - const elementIndex = this.#elements.length; - this.#elements.push(element); - this.listNode.append(element); - element.setAttribute("data-priority", 4 - (elementIndex % 5)); + const priority = 4 - (this.#elements.length % 5); + + // Case 1: If there's space in the on-screen list, add it there + if (this.#elements.length < this.#onScreenMaxNumberOfItems) { + // Create element and add to on-screen list + const element = new TodoItem(); + element.setAttribute("itemid", id); + element.setAttribute("itemtitle", title); + element.setAttribute("itemcompleted", completed); + element.setAttribute("data-priority", priority); + + this.#elements.push(element); + this.listNode.append(element); + this.updateView(element); + } + // Case 2: If there's space in the memory cache, create element and add to cache + else if (this.#memoryCacheManager.shouldAddToAfterCache()) { + const element = new TodoItem(); + element.setAttribute("itemid", id); + element.setAttribute("itemtitle", title); + element.setAttribute("itemcompleted", completed); + element.setAttribute("data-priority", priority); + + // Add the element to the memory cache + this.#memoryCacheManager.addToAfterCache(element); + } + // Case 3: Add to IndexedDB as last resort + else { + this.#addItemToDatabase(entry, priority); + } + } + + /** + * Helper method to add an item to the IndexedDB database + * @private + */ + #addItemToDatabase(entry, priority) { + const { id, title, completed } = entry; + const todoItem = { + itemId: id, + title, + completed, + priority, + createdAt: Date.now() + }; + + this.#todoDatabase.addItem(todoItem) + .then(() => { + // Item successfully added to database + }) + .catch(error => { + console.error("Failed to add item to database:", error); + }); } addItems(items) { From 3d91217bf40d756e3329979b0a9a8beaf421a798 Mon Sep 17 00:00:00 2001 From: Luis Pardo Date: Thu, 19 Jun 2025 18:07:39 +0000 Subject: [PATCH 04/12] Commit before context switching --- .gitignore | 5 + .../todo-list/todo-list.component.js | 509 +++++++++++++++++- .../components/todo-app/todo-app.component.js | 8 + .../todo-bottombar.component.js | 157 +++++- .../todo-bottombar/todo-bottombar.template.js | 19 +- .../todo-list/todo-list.component.js | 187 +++++-- .../todo-list/todo-list.template.js | 2 +- 7 files changed, 806 insertions(+), 81 deletions(-) diff --git a/.gitignore b/.gitignore index fc30e2565..f120e3084 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,8 @@ /resources/charts/node_modules/ /resources/todomvc/big-dom-generator/node_modules/ + +/resources/todomvc/vanilla-examples/todomvc-wc-indexeddb/node_modules/ +/experimental/todomvc-localstorage/node_modules/ + +resources/todomvc/vanilla-examples/todomvc-wc-indexeddb-firt/ \ No newline at end of file diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-list/todo-list.component.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-list/todo-list.component.js index 8e90e291f..e9eff5915 100644 --- a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-list/todo-list.component.js +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/dist/components/todo-list/todo-list.component.js @@ -4,6 +4,385 @@ import TodoItem from "../todo-item/todo-item.component.js"; import globalStyles from "../../styles/global.constructable.js"; import listStyles from "../../styles/todo-list.constructable.js"; +class FixedSizeQueue { + #buffer; + #capacity; + #size = 0; + #head = 0; + #tail = 0; + + constructor(capacity = 10) { + if (capacity <= 0) { + throw new Error("Queue capacity must be positive"); + } + this.#capacity = capacity; + this.#buffer = new Array(capacity); + } + + /** + * Add an element to the rear of the queue + * @param {*} element - Element to add + * @returns {boolean} - True if element was added, false if queue is full + */ + enqueue(element) { + if (this.isFull()) { + return false; + } + + this.#buffer[this.#tail] = element; + this.#tail = (this.#tail + 1) % this.#capacity; + this.#size++; + return true; + } + + /** + * Add multiple elements to the rear of the queue + * @param {Array} elements - Array of elements to add + * @returns {number} - The number of elements that were added to the queue + */ + enqueueAll(elements) { + // Calculate available space and number of elements we can add + const availableSpace = this.#capacity - this.#size; + const elementsToAdd = Math.min(elements.length, availableSpace); + + // Add as many elements as will fit + for (let i = 0; i < elementsToAdd; i++) { + this.#buffer[this.#tail] = elements[i]; + this.#tail = (this.#tail + 1) % this.#capacity; + this.#size++; + } + + return elementsToAdd; + } + + /** + * Remove and return an element from the front of the queue + * @returns {*} - The element at the front of the queue, or undefined if empty + */ + dequeue() { + if (this.isEmpty()) { + return undefined; + } + + const element = this.#buffer[this.#head]; + this.#buffer[this.#head] = undefined; // Clear reference for GC + this.#head = (this.#head + 1) % this.#capacity; + this.#size--; + return element; + } + + /** + * Remove and return multiple elements from the front of the queue + * @param {number} count - Number of elements to dequeue + * @returns {Array} - Array of dequeued elements (may be fewer than requested if not enough available) + */ + dequeueMany(count) { + if (count <= 0) { + return []; + } + + const elementsToDequeue = Math.min(count, this.#size); + const result = new Array(elementsToDequeue); + + for (let i = 0; i < elementsToDequeue; i++) { + result[i] = this.#buffer[this.#head]; + this.#buffer[this.#head] = undefined; // Clear reference for GC + this.#head = (this.#head + 1) % this.#capacity; + this.#size--; + } + + return result; + } + + /** + * Return the element at the front of the queue without removing it + * @returns {*} - The element at the front of the queue, or undefined if empty + */ + peek() { + return this.isEmpty() ? undefined : this.#buffer[this.#head]; + } + + /** + * Check if the queue is empty + * @returns {boolean} - True if queue is empty + */ + isEmpty() { + return this.#size === 0; + } + + /** + * Check if the queue is full + * @returns {boolean} - True if queue is full + */ + isFull() { + return this.#size === this.#capacity; + } + + /** + * Get the current number of elements in the queue + * @returns {number} - Current size of the queue + */ + size() { + return this.#size; + } + + /** + * Get the maximum capacity of the queue + * @returns {number} - Maximum capacity of the queue + */ + capacity() { + return this.#capacity; + } + + /** + * Clear all elements from the queue + */ + clear() { + this.#buffer.fill(undefined); + this.#size = 0; + this.#head = 0; + this.#tail = 0; + } + + /** + * Convert queue to array (for debugging/testing purposes) + * @returns {Array} - Array representation of queue elements in order + */ + toArray() { + const result = new Array(this.#size); + for (let i = 0; i < this.#size; i++) { + result[i] = this.#buffer[(this.#head + i) % this.#capacity]; + } + return result; + } +} + + +class MemoryCacheManager { + + #cacheMaxSize = 10; + pendingPromise = null; + hasPendingUpdate = false; + + // after cache will be used to load the next item in the list, we need fifo access. + #afterCache = new FixedSizeQueue(10); + + constructor() { + this.currentId = -1; + this.cacheMaxSize = 10; + } + + shouldAddToAfterCache() { + return !this.#afterCache.isFull(); + } + + + addToAfterCache(element) { + if (this.#afterCache.isFull()) { + throw new Error("After cache is full, cannot add more items."); + } + this.#afterCache.enqueue(element); + } + + /** + * Add multiple elements to the forward cache + * @param {Array} elements - Array of elements to add to the forward cache + * @returns {number} - The number of elements that were added to the cache + */ + addMultipleToAfterCache(elements) { + return this.#afterCache.enqueueAll(elements); + } + + nextFromAfterCache() { + const result = this.#afterCache.dequeue(); + return result !== undefined ? result : null; + } + + /** + * Check if the forward cache (after cache) is empty + * @returns {boolean} - True if the forward cache is empty + */ + isForwardCacheEmpty() { + return this.#afterCache.isEmpty(); + } + + /** + * Retrieves a given number of elements from the forward cache (afterCache) + * + * @param {number} count - The number of elements to retrieve + * @returns {Array} - The next 'count' elements from the afterCache + */ + getElementsFromAfterCache(count) { + return this.#afterCache.dequeueMany(count); + } + + chainToPendingPromise(promise) { + if(!this.pendingPromise) { + throw(new Error("No pending promise to chain to.")); + } + this.pendingPromise = this.pendingPromise.then(() => promise); + } +} + +/** + * Database manager for Todo items using IndexedDB + */ +class TodoDatabase { + #db; + #dbName; + #dbVersion; + #storeName = 'todos'; + #dbReadyCallback; + #numberOfPendingAddRequests = 0; + #numberOfPendingRemoveRequests = 0; + + /** + * Creates a new TodoDatabase instance + * @param {string} dbName - The name of the IndexedDB database + * @param {number} dbVersion - The version of the database + * @param {Function} readyCallback - Callback to invoke when the database is ready + */ + constructor(dbName = 'todosDB', dbVersion = 1, readyCallback = null) { + this.#dbName = dbName; + this.#dbVersion = dbVersion; + this.#dbReadyCallback = readyCallback; + this.initialize(); + } + + /** + * Initializes the IndexedDB database for storing todo items + */ + initialize() { + // Open the database connection + const request = indexedDB.open(this.#dbName, this.#dbVersion); + + // Handle database upgrade or creation + request.onupgradeneeded = (event) => { + this.#db = event.target.result; + + // Create an object store for our todos if it doesn't exist + if (!this.#db.objectStoreNames.contains(this.#storeName)) { + const todoStore = this.#db.createObjectStore(this.#storeName, { keyPath: 'orderedId'}); + + // Create indexes for quick searches. + todoStore.createIndex('itemId', 'itemId', { unique: true }); + todoStore.createIndex('completed', 'completed', { unique: false }); + todoStore.createIndex('title', 'title', { unique: false }); + todoStore.createIndex('priority', 'priority', { unique: false }); + } + }; + + // Handle successful database opening + request.onsuccess = (event) => { + this.#db = event.target.result; + console.log('IndexedDB initialized successfully'); + + // Call ready callback if provided + if (this.#dbReadyCallback) { + this.#dbReadyCallback(); + } + }; + + // Handle errors + request.onerror = (event) => { + console.error('Error opening IndexedDB:', event.target.error); + }; + } + + /** + * Gets the database instance + * @returns {IDBDatabase} The IndexedDB database instance + */ + get database() { + return this.#db; + } + + /** + * Adds a todo item to the database + * @param {Object} item - The todo item to add + * @returns {Promise} A promise that resolves to the ID of the added item + */ + addItem(item) { + return new Promise((resolve, reject) => { + if (!this.#db) { + reject(new Error("Database not initialized")); + return; + } + + try { + const transaction = this.#db.transaction(this.#storeName, 'readwrite'); + const store = transaction.objectStore(this.#storeName); + + const request = store.add(item); + + this.#numberOfPendingAddRequests++; + + request.onsuccess = (event) => { + this.#numberOfPendingAddRequests--; + console.log("Item added to database:", event.target.result); + resolve(event.target.result); // Return the generated ID + }; + + request.onerror = (event) => { + reject(event.target.error); + }; + } catch (error) { + reject(error); + } + }); + } + + /** + * Reads items from the database where orderedId is greater than lastUsedOrderedId + * and executes a callback with the results + * + * @param {number} lastUsedOrderedId - The last orderedId that was used + * @param {number} count - Maximum number of items to retrieve + * @param {Function} callback - Callback function to execute with the retrieved items + */ + readItemsAndExecuteCallback(lastUsedOrderedId, count, callback) { + if (!this.#db) { + throw(new Error("Database not initialized")); + } + + const transaction = this.#db.transaction(this.#storeName, 'readonly'); + const store = transaction.objectStore(this.#storeName); + + // Create a key range for orderedId > lastUsedOrderedId + const keyRange = IDBKeyRange.lowerBound(lastUsedOrderedId, true); + + // Open a cursor with the key range + const request = store.openCursor(keyRange); + + // Array to store the retrieved items + const items = []; + + request.onsuccess = (event) => { + const cursor = event.target.result; + + if (cursor && items.length < count) { + // Add this item to our results + items.push(cursor.value); + + // Move to the next item + cursor.continue(); + } else { + // We've either reached the end of the store or collected enough items + callback(null, items); + } + }; + + request.onerror = (event) => { + callback(event.target.error, null); + }; + + // Handle transaction errors + transaction.onerror = (event) => { + callback(event.target.error, null); + }; + } +} + class TodoList extends HTMLElement { static get observedAttributes() { return ["total-items"]; @@ -11,10 +390,14 @@ class TodoList extends HTMLElement { #elements = []; #route = undefined; + #onScreenMaxNumberOfItems = 10; + #memoryCacheManager = new MemoryCacheManager(); + #todoDatabase; + #currentPageNumber = 1; + #incrementalItemId = 0; constructor() { super(); - const node = document.importNode(template.content, true); this.listNode = node.querySelector(".todo-list"); @@ -30,20 +413,93 @@ class TodoList extends HTMLElement { extraAdoptedStyleSheet.replaceSync(window.extraTodoListCssToAdopt); this.shadow.adoptedStyleSheets.push(extraAdoptedStyleSheet); } + + // Initialize TodoDatabase + this.#initializeDatabase(); + } + + /** + * Initializes the IndexedDB database for storing todo items + * @private + */ + #initializeDatabase() { + // Create a new TodoDatabase instance with a callback for when DB is ready + this.#todoDatabase = new TodoDatabase('todosDB', 1, () => { + // Dispatch an event when the database is ready + this.dispatchEvent(new CustomEvent('db-ready')); + }); + } + + /** + * Gets the IndexedDB database instance + * @returns {IDBDatabase} The IndexedDB database instance + */ + get #db() { + return this.#todoDatabase?.database; } + /** + * Adds a todo item based on available space in different storage tiers + * @param {Object} entry - The todo item entry + */ addItem(entry) { const { id, title, completed } = entry; - const element = new TodoItem(); - - element.setAttribute("itemid", id); - element.setAttribute("itemtitle", title); - element.setAttribute("itemcompleted", completed); - - const elementIndex = this.#elements.length; - this.#elements.push(element); - this.listNode.append(element); - element.setAttribute("data-priority", 4 - (elementIndex % 5)); + const priority = 4 - (this.#elements.length % 5); + + // Case 1: If there's space in the on-screen list, add it there + if (this.#elements.length < this.#onScreenMaxNumberOfItems) { + // Create element and add to on-screen list + const element = new TodoItem(); + element.setAttribute("itemid", id); + element.setAttribute("itemtitle", title); + element.setAttribute("itemcompleted", completed); + element.setAttribute("data-priority", priority); + element.orderedId = this.#incrementalItemId++; + + this.#elements.push(element); + this.listNode.append(element); + this.updateView(element); + } + // Case 2: If there's space in the memory cache, create element and add to cache + else if (this.#memoryCacheManager.shouldAddToAfterCache()) { + const element = new TodoItem(); + element.setAttribute("itemid", id); + element.setAttribute("itemtitle", title); + element.setAttribute("itemcompleted", completed); + element.setAttribute("data-priority", priority); + element.orderedId = this.#incrementalItemId++; + + // Add the element to the memory cache + this.#memoryCacheManager.addToAfterCache(element); + } + // Case 3: Add to IndexedDB as last resort + else { + this.#addItemToDatabase(entry, priority); + } + } + + /** + * Helper method to add an item to the IndexedDB database + * @private + */ + #addItemToDatabase(entry, priority) { + const { id, title, completed } = entry; + const todoItem = { + orderedId: this.#incrementalItemId++, + itemId: id, + title, + completed, + priority, + createdAt: Date.now() + }; + + this.#todoDatabase.addItem(todoItem) + .then(() => { + // Item successfully added to database + }) + .catch(error => { + console.error("Failed to add item to database:", error); + }); } addItems(items) { @@ -119,6 +575,37 @@ class TodoList extends HTMLElement { this.updateStyles(); } + async moveToNextPage() { + if (this.#memoryCacheManager.pendingPromise !== null) { + console.log("There is a pending udpate, cannot move to next page."); + this.#memoryCacheManager.chainToPendingPromise( + (async () => { + await this.moveToNextPage.bind(this); + })()); + return; + } + const nextItems = this.#memoryCacheManager.getElementsFromAfterCache( + this.#onScreenMaxNumberOfItems); + this.listNode.replaceChildren(...nextItems); + if (nextItems.length < this.#onScreenMaxNumberOfItems) { + // There were not enough items in the cache, then there are not + // enough items in the db storage. + return; + } + let resolvePendingPromise; + this.#memoryCacheManager.hasPendingUpdate = new Promise((resolve) => { + resolvePendingPromise = resolve; + }); + this.#db.readItemsAndExecuteCallback( + nextItems.at(-1).orderedId, + this.#onScreenMaxNumberOfItems, + (newItems) => { + this.#memoryCacheManager.addMultipleToAfterCache(newItems); + resolvePendingPromise(); + }); + + } + connectedCallback() { this.updateStyles(); } diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-app/todo-app.component.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-app/todo-app.component.js index 691980f3e..1f88ef4e8 100644 --- a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-app/todo-app.component.js +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-app/todo-app.component.js @@ -27,6 +27,7 @@ class TodoApp extends HTMLElement { this.updateItem = this.updateItem.bind(this); this.toggleItems = this.toggleItems.bind(this); this.clearCompletedItems = this.clearCompletedItems.bind(this); + this.moveToNextPage = this.moveToNextPage.bind(this); this.routeChange = this.routeChange.bind(this); this.router = useRouter(); @@ -84,6 +85,10 @@ class TodoApp extends HTMLElement { this.list.removeCompletedItems(); } + moveToNextPage() { + this.list.moveToNextPage(); + } + update(type = "", id = "") { const totalItems = this.#data.length; const activeItems = this.#data.filter((entry) => !entry.completed).length; @@ -109,6 +114,7 @@ class TodoApp extends HTMLElement { this.list.listNode.addEventListener("update-item", this.updateItem); this.bottombar.addEventListener("clear-completed-items", this.clearCompletedItems); + this.bottombar.addEventListener("move-to-next-page", this.moveToNextPage); } removeListeners() { @@ -120,11 +126,13 @@ class TodoApp extends HTMLElement { this.list.listNode.removeEventListener("update-item", this.updateItem); this.bottombar.removeEventListener("clear-completed-items", this.clearCompletedItems); + this.bottombar.removeEventListener("go-to-next-page", this.moveToNextPage); } routeChange(route) { const routeName = route.split("/")[1] || "all"; this.list.updateRoute(routeName); + console.log(this.bottombar, "bottombar"); this.bottombar.updateRoute(routeName); this.topbar.updateRoute(routeName); } diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-bottombar/todo-bottombar.component.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-bottombar/todo-bottombar.component.js index f5b8896c5..d6aa9130a 100644 --- a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-bottombar/todo-bottombar.component.js +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-bottombar/todo-bottombar.component.js @@ -1,7 +1,144 @@ import template from "./todo-bottombar.template.js"; import globalStyles from "../../../node_modules/todomvc-css/dist/global.constructable.js"; -import bottombarStyles from "../../../node_modules/todomvc-css/dist/bottombar.constructable.js"; +// import bottombarStyles from "../../../node_modules/todomvc-css/dist/bottombar.constructable.js"; + +const bottombarStyles = new CSSStyleSheet(); +bottombarStyles.replaceSync(`:host { + display: block; + box-shadow: none !important; +} + +.bottombar { + padding: 10px 0; + height: 41px; + text-align: center; + font-size: 15px; + border-top: 1px solid #e6e6e6; + display: flex; + flex-direction: row; +} + +.bottombar.hidden-footer { + display: none; +} + +.bottombar::before { + content: ""; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + pointer-events: none; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +.todo-count { + font-weight: 300; +} + +.filter-item { + display: inline-block; +} + +.filter-link { + color: inherit; + margin: 3px; + padding: 0 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; + cursor: pointer; + display: block; + height: 26px; + line-height: 26px; +} + +.filter-link:hover { + border-color: #db7676; +} + +.filter-link.selected { + border-color: #ce4646; +} + +.clear-completed-button, +.clear-completed-button:active { + text-decoration: none; + cursor: pointer; + padding: 3px; + height: 32px; + line-height: 26px; + right: 12px; + top: 50%; + transform: translateY(-50%); +} + +.clear-completed-button:hover { + text-decoration: underline; +} + +/* rtl support */ +html[dir="rtl"] .todo-status, +:host([dir="rtl"]) .todo-status { + right: 12px; + left: unset; +} + +html[dir="rtl"] .clear-completed-button, +:host([dir="rtl"]) .clear-completed-button { + left: 12px; + right: unset; +} + +@media (max-width: 430px) { + .bottombar { + height: 120px; + } + + .todo-status { + display: block; + text-align: center; + position: relative; + left: unset; + right: unset; + top: unset; + transform: unset; + } + + .filter-list { + display: block; + position: relative; + left: unset; + right: unset; + top: unset; + transform: unset; + } + + .clear-completed-button, + .clear-completed-button:active { + display: block; + margin: 0 auto; + position: relative; + left: unset; + right: unset; + top: unset; + transform: unset; + } + + html[dir="rtl"] .todo-status, + :host([dir="rtl"]) .todo-status { + right: unset; + left: unset; + } + + html[dir="rtl"] .clear-completed-button, + :host([dir="rtl"]) .clear-completed-button { + left: unset; + right: unset; + } +`); class TodoBottombar extends HTMLElement { static get observedAttributes() { @@ -14,6 +151,7 @@ class TodoBottombar extends HTMLElement { const node = document.importNode(template.content, true); this.element = node.querySelector(".bottombar"); this.clearCompletedButton = node.querySelector(".clear-completed-button"); + this.nextPageButton = node.getElementById("next-page-button"); this.todoStatus = node.querySelector(".todo-status"); this.filterLinks = node.querySelectorAll(".filter-link"); @@ -24,13 +162,16 @@ class TodoBottombar extends HTMLElement { this.shadow.append(node); this.clearCompletedItems = this.clearCompletedItems.bind(this); + this.moveToNextPage = this.moveToNextPage.bind(this); } updateDisplay() { - if (parseInt(this["total-items"]) !== 0) - this.element.style.display = "block"; - else - this.element.style.display = "none"; + if (parseInt(this["total-items"]) !== 0) { + this.element.classList.remove("hidden-footer"); + } + else { + this.element.classList.add("hidden-footer"); + } this.todoStatus.textContent = `${this["active-items"]} ${this["active-items"] === "1" ? "item" : "items"} left!`; } @@ -48,12 +189,18 @@ class TodoBottombar extends HTMLElement { this.dispatchEvent(new CustomEvent("clear-completed-items")); } + moveToNextPage() { + this.dispatchEvent(new CustomEvent("move-to-next-page")); + } + addListeners() { this.clearCompletedButton.addEventListener("click", this.clearCompletedItems); + this.nextPageButton.addEventListener("click", this.moveToNextPage); } removeListeners() { this.clearCompletedButton.removeEventListener("click", this.clearCompletedItems); + this.nextPageButton.removeEventListener("click", this.moveToNextPage); } attributeChangedCallback(property, oldValue, newValue) { diff --git a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-bottombar/todo-bottombar.template.js b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-bottombar/todo-bottombar.template.js index 4f34ca92d..4562a84cd 100644 --- a/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-bottombar/todo-bottombar.template.js +++ b/resources/todomvc/vanilla-examples/javascript-wc-indexeddb/src/components/todo-bottombar/todo-bottombar.template.js @@ -2,20 +2,21 @@ const template = document.createElement("template"); template.id = "todo-bottombar-template"; template.innerHTML = ` -