diff --git a/experimental/javascript-wc-indexeddb/.gitignore b/experimental/javascript-wc-indexeddb/.gitignore
new file mode 100644
index 000000000..03e05e4c0
--- /dev/null
+++ b/experimental/javascript-wc-indexeddb/.gitignore
@@ -0,0 +1,2 @@
+.DS_Store
+/node_modules
diff --git a/experimental/javascript-wc-indexeddb/README.md b/experimental/javascript-wc-indexeddb/README.md
new file mode 100644
index 000000000..11351f614
--- /dev/null
+++ b/experimental/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/experimental/javascript-wc-indexeddb/dist/components/todo-app/todo-app.component.js b/experimental/javascript-wc-indexeddb/dist/components/todo-app/todo-app.component.js
new file mode 100644
index 000000000..66d9d1f99
--- /dev/null
+++ b/experimental/javascript-wc-indexeddb/dist/components/todo-app/todo-app.component.js
@@ -0,0 +1,158 @@
+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;
+ #numberOfItems = 0;
+ #numberOfCompletedItems = 0;
+ 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.moveToNextPage = this.moveToNextPage.bind(this);
+ this.moveToPreviousPage = this.moveToPreviousPage.bind(this);
+
+ this.router = useRouter();
+ }
+
+ get isReady() {
+ return this.#isReady;
+ }
+
+ getInstance() {
+ return this;
+ }
+
+ addItem(event) {
+ const { detail: item } = event;
+ this.list.addItem(item, this.#numberOfItems++);
+ this.update("add-item", item.id);
+ }
+
+ toggleItem(event) {
+ if (event.detail.completed)
+ this.#numberOfCompletedItems++;
+ else
+ this.#numberOfCompletedItems--;
+
+ this.list.toggleItem(event.detail.itemNumber, event.detail.completed);
+ this.update("toggle-item", event.detail.id);
+ }
+
+ removeItem(event) {
+ if (event.detail.completed)
+ this.#numberOfCompletedItems--;
+
+ this.#numberOfItems--;
+ this.update("remove-item", event.detail.id);
+ this.list.removeItem(event.detail.itemNumber);
+ }
+
+ updateItem(event) {
+ this.update("update-item", event.detail.id);
+ }
+
+ toggleItems(event) {
+ this.list.toggleItems(event.detail.completed);
+ }
+
+ clearCompletedItems() {
+ this.list.removeCompletedItems();
+ }
+
+ moveToNextPage() {
+ this.list.moveToNextPage();
+ }
+
+ moveToPreviousPage() {
+ // Skeleton implementation of previous page navigation
+ this.list.moveToPreviousPage().then(() => {
+ this.bottombar.reenablePreviousPageButton();
+ window.dispatchEvent(new CustomEvent("previous-page-loaded", {}));
+ });
+ }
+
+ update() {
+ const totalItems = this.#numberOfItems;
+ const completedItems = this.#numberOfCompletedItems;
+ const activeItems = totalItems - completedItems;
+
+ this.list.setAttribute("total-items", totalItems);
+
+ 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);
+ this.bottombar.addEventListener("next-page", this.moveToNextPage);
+ this.bottombar.addEventListener("previous-page", this.moveToPreviousPage);
+ }
+
+ 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);
+ this.bottombar.removeEventListener("next-page", this.moveToNextPage);
+ this.bottombar.removeEventListener("previous-page", this.moveToPreviousPage);
+ }
+
+ 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/experimental/javascript-wc-indexeddb/dist/components/todo-app/todo-app.template.js b/experimental/javascript-wc-indexeddb/dist/components/todo-app/todo-app.template.js
new file mode 100644
index 000000000..1a55a8194
--- /dev/null
+++ b/experimental/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/experimental/javascript-wc-indexeddb/dist/components/todo-bottombar/todo-bottombar.component.js b/experimental/javascript-wc-indexeddb/dist/components/todo-bottombar/todo-bottombar.component.js
new file mode 100644
index 000000000..986b70bdb
--- /dev/null
+++ b/experimental/javascript-wc-indexeddb/dist/components/todo-bottombar/todo-bottombar.component.js
@@ -0,0 +1,126 @@
+import template from "./todo-bottombar.template.js";
+
+import globalStyles from "../../styles/global.constructable.js";
+import bottombarStyles from "../../styles/bottombar.constructable.js";
+
+const customStyles = new CSSStyleSheet();
+customStyles.replaceSync(`
+
+ .clear-completed-button, .clear-completed-button:active,
+ .todo-status,
+ .filter-list
+ {
+ position: unset;
+ transform: unset;
+ }
+
+ .bottombar {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ align-items: center;
+ justify-items: center;
+ }
+
+ .bottombar > * {
+ grid-column: span 1;
+ }
+
+ .filter-list {
+ grid-column: span 3;
+ }
+
+ :host([total-items="0"]) > .bottombar {
+ display: none;
+ }
+`);
+
+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, customStyles];
+ this.shadow.append(node);
+
+ this.clearCompletedItems = this.clearCompletedItems.bind(this);
+ this.MoveToNextPage = this.MoveToNextPage.bind(this);
+ this.MoveToPreviousPage = this.MoveToPreviousPage.bind(this);
+ }
+
+ updateDisplay() {
+ 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"));
+ }
+
+ MoveToNextPage() {
+ this.dispatchEvent(new CustomEvent("next-page"));
+ }
+
+ MoveToPreviousPage() {
+ this.element.querySelector(".previous-page-button").disabled = true;
+ this.dispatchEvent(new CustomEvent("previous-page"));
+ }
+
+ addListeners() {
+ this.clearCompletedButton.addEventListener("click", this.clearCompletedItems);
+ this.element.querySelector(".next-page-button").addEventListener("click", this.MoveToNextPage);
+ this.element.querySelector(".previous-page-button").addEventListener("click", this.MoveToPreviousPage);
+ }
+
+ removeListeners() {
+ this.clearCompletedButton.removeEventListener("click", this.clearCompletedItems);
+ this.getElementById("next-page-button").removeEventListener("click", this.MoveToNextPage);
+ this.getElementById("previous-page-button").removeEventListener("click", this.MoveToPreviousPage);
+ }
+
+ attributeChangedCallback(property, oldValue, newValue) {
+ if (oldValue === newValue)
+ return;
+ this[property] = newValue;
+
+ if (this.isConnected)
+ this.updateDisplay();
+ }
+
+ reenablePreviousPageButton() {
+ this.element.querySelector(".previous-page-button").disabled = false;
+ window.dispatchEvent(new CustomEvent("previous-page-button-enabled", {}));
+ }
+
+ connectedCallback() {
+ this.updateDisplay();
+ this.addListeners();
+ }
+
+ disconnectedCallback() {
+ this.removeListeners();
+ }
+}
+
+customElements.define("todo-bottombar", TodoBottombar);
+
+export default TodoBottombar;
diff --git a/experimental/javascript-wc-indexeddb/dist/components/todo-bottombar/todo-bottombar.template.js b/experimental/javascript-wc-indexeddb/dist/components/todo-bottombar/todo-bottombar.template.js
new file mode 100644
index 000000000..e9259fe30
--- /dev/null
+++ b/experimental/javascript-wc-indexeddb/dist/components/todo-bottombar/todo-bottombar.template.js
@@ -0,0 +1,24 @@
+const template = document.createElement("template");
+
+template.id = "todo-bottombar-template";
+template.innerHTML = `
+
+`;
+
+export default template;
diff --git a/experimental/javascript-wc-indexeddb/dist/components/todo-item/todo-item.component.js b/experimental/javascript-wc-indexeddb/dist/components/todo-item/todo-item.component.js
new file mode 100644
index 000000000..1b498876b
--- /dev/null
+++ b/experimental/javascript-wc-indexeddb/dist/components/todo-item/todo-item.component.js
@@ -0,0 +1,182 @@
+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";
+ this.itemIndex = null;
+
+ 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";
+ 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: { completed: this.toggleInput.checked, itemNumber: this.itemIndex },
+ bubbles: true,
+ })
+ );
+ }
+
+ removeItem() {
+ this.dispatchEvent(
+ new CustomEvent("remove-item", {
+ detail: { completed: this.toggleInput.checked, itemNumber: this.itemIndex },
+ 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.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/experimental/javascript-wc-indexeddb/dist/components/todo-item/todo-item.template.js b/experimental/javascript-wc-indexeddb/dist/components/todo-item/todo-item.template.js
new file mode 100644
index 000000000..9a67675fd
--- /dev/null
+++ b/experimental/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/experimental/javascript-wc-indexeddb/dist/components/todo-list/todo-list.component.js b/experimental/javascript-wc-indexeddb/dist/components/todo-list/todo-list.component.js
new file mode 100644
index 000000000..dbb64e3a3
--- /dev/null
+++ b/experimental/javascript-wc-indexeddb/dist/components/todo-list/todo-list.component.js
@@ -0,0 +1,425 @@
+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 IndexedDBManager {
+ constructor() {
+ this.dbName = "todoDB";
+ this.dbVersion = 1;
+ this.storeName = "todos";
+ this.db = null;
+ this.pendingAdditions = 0;
+ this.totalItemsToggled = 0;
+ this.totalItemsDeleted = 0;
+ this.initDB().then(() => {
+ const newDiv = document.createElement("div");
+ newDiv.classList.add("indexeddb-ready");
+ newDiv.style.display = "none";
+ document.body.append(newDiv);
+ });
+ }
+
+ initDB() {
+ return new Promise((resolve, reject) => {
+ // Delete the existing database first for clean state
+ const deleteRequest = indexedDB.deleteDatabase(this.dbName);
+
+ deleteRequest.onerror = (event) => {
+ // Continue despite error in deletion
+ this.openDatabase(resolve, reject);
+ };
+
+ deleteRequest.onsuccess = () => {
+ this.openDatabase(resolve, reject);
+ };
+
+ deleteRequest.onblocked = () => {
+ // Try opening anyway
+ this.openDatabase(resolve, reject);
+ };
+ });
+ }
+
+ openDatabase(resolve, reject) {
+ const request = indexedDB.open(this.dbName, this.dbVersion);
+
+ request.onerror = (event) => {
+ reject(event.target.error);
+ };
+
+ request.onsuccess = (event) => {
+ this.db = event.target.result;
+ resolve(this.db);
+ };
+
+ request.onupgradeneeded = (event) => {
+ const db = event.target.result;
+
+ // Create object store (since we're always creating a fresh DB now)
+ const store = db.createObjectStore(this.storeName, { keyPath: "itemNumber" });
+ store.createIndex("id", "id", { unique: true });
+ store.createIndex("title", "title", { unique: false });
+ store.createIndex("completed", "completed", { unique: false });
+ store.createIndex("priority", "priority", { unique: false });
+ };
+ }
+
+ addTodo(todo) {
+ return new Promise((resolve, reject) => {
+ // Ensure the database connection is established
+ if (!this.db) {
+ this.initDB()
+ .then(() => this.addTodo(todo))
+ .then(resolve)
+ .catch(reject);
+ return;
+ }
+
+ // Add todo item to IndexedDB
+ const transaction = this.db.transaction(this.storeName, "readwrite");
+ const store = transaction.objectStore(this.storeName);
+
+ const request = store.add(todo);
+ this.pendingAdditions++;
+
+ request.onsuccess = () => {
+ if (--this.pendingAdditions === 0)
+ window.dispatchEvent(new CustomEvent("indexeddb-add-completed", {}));
+
+ resolve(todo);
+ };
+
+ request.onerror = (event) => {
+ reject(event.target.error);
+ };
+ });
+ }
+
+ getTodos(upperItemNumber, count) {
+ return new Promise((resolve, reject) => {
+ // Ensure the database connection is established
+ if (!this.db) {
+ this.initDB()
+ .then(() => this.getTodos(upperItemNumber, count))
+ .then(resolve)
+ .catch(reject);
+ return;
+ }
+
+ const transaction = this.db.transaction(this.storeName, "readonly");
+ const store = transaction.objectStore(this.storeName);
+
+ // Use IDBKeyRange to get items with itemNumber less than upperItemNumber
+ const range = IDBKeyRange.upperBound(upperItemNumber, true); // true = exclusive bound
+
+ // Open a cursor to iterate through records in descending order
+ const request = store.openCursor(range, "prev");
+
+ const items = [];
+ let itemsProcessed = 0;
+
+ request.onsuccess = (event) => {
+ const cursor = event.target.result;
+
+ // Check if we have a valid cursor and haven't reached our count limit
+ if (cursor && itemsProcessed < count) {
+ items.push(cursor.value);
+ itemsProcessed++;
+ cursor.continue(); // Move to next item
+ } else {
+ // We're done - sort items by itemNumber in descending order
+ // for proper display order (newest to oldest)
+ items.sort((a, b) => a.itemNumber - b.itemNumber);
+
+ resolve(items);
+ }
+ };
+
+ request.onerror = (event) => {
+ reject(event.target.error);
+ };
+
+ // Also handle transaction errors
+ transaction.onerror = (event) => {
+ reject(event.target.error);
+ };
+ });
+ }
+
+ toggleTodo(itemNumber, completed) {
+ return new Promise((resolve, reject) => {
+ // Ensure the database connection is established
+ if (!this.db) {
+ this.initDB()
+ .then(() => this.toggleTodo(itemNumber, completed))
+ .then(resolve)
+ .catch(reject);
+ return;
+ }
+
+ // Access the todo item directly by its itemNumber (keyPath)
+ const transaction = this.db.transaction(this.storeName, "readwrite");
+ const store = transaction.objectStore(this.storeName);
+
+ // Get the todo item directly using its primary key (itemNumber)
+ const getRequest = store.get(itemNumber);
+
+ getRequest.onsuccess = (event) => {
+ const todoItem = getRequest.result;
+
+ if (!todoItem) {
+ reject(new Error(`Todo item with itemNumber '${itemNumber}' not found`));
+ return;
+ }
+
+ // Update the completed status
+ todoItem.completed = completed;
+
+ // Save the updated item back to the database
+ const updateRequest = store.put(todoItem);
+
+ updateRequest.onsuccess = () => {
+ if (window.numberOfItemsToAdd && ++this.totalItemsToggled === window.numberOfItemsToAdd)
+ window.dispatchEvent(new CustomEvent("indexeddb-toggle-completed", {}));
+
+ resolve(todoItem);
+ };
+
+ updateRequest.onerror = (event) => {
+ reject(event.target.error);
+ };
+ };
+
+ getRequest.onerror = (event) => {
+ reject(event.target.error);
+ };
+
+ // Handle potential errors in finding the item
+ transaction.onerror = (event) => {
+ reject(event.target.error);
+ };
+
+ // Handle transaction errors
+ transaction.onerror = (event) => {
+ reject(event.target.error);
+ };
+ });
+ }
+
+ removeTodo(itemNumber) {
+ return new Promise((resolve, reject) => {
+ // Ensure the database connection is established
+ if (!this.db) {
+ this.initDB()
+ .then(() => this.removeTodo(itemNumber))
+ .then(resolve)
+ .catch(reject);
+ return;
+ }
+
+ // Access the todo item directly by its itemNumber (keyPath)
+ const transaction = this.db.transaction(this.storeName, "readwrite");
+ const store = transaction.objectStore(this.storeName);
+
+ // Delete the todo item directly using its primary key (itemNumber)
+ const deleteRequest = store.delete(itemNumber);
+
+ deleteRequest.onsuccess = () => {
+ if (window.numberOfItemsToAdd && ++this.totalItemsDeleted === window.numberOfItemsToAdd)
+ window.dispatchEvent(new CustomEvent("indexeddb-remove-completed", {}));
+
+ resolve(itemNumber);
+ };
+
+ deleteRequest.onerror = (event) => {
+ reject(event.target.error);
+ };
+
+ // Handle transaction errors
+ transaction.onerror = (event) => {
+ reject(event.target.error);
+ };
+ });
+ }
+}
+
+const MAX_ON_SCREEN_ITEMS = 10;
+
+const customListStyles = new CSSStyleSheet();
+customListStyles.replaceSync(`
+ .todo-list > todo-item {
+ display: block;
+ }
+
+ .todo-list[route="completed"] > [itemcompleted="false"] {
+ display: none;
+ }
+
+ .todo-list[route="active"] > [itemcompleted="true"] {
+ display: none;
+ }
+
+ :nth-child(${MAX_ON_SCREEN_ITEMS}) ~ todo-item {
+ display: none;
+ }
+`);
+
+class TodoList extends HTMLElement {
+ static get observedAttributes() {
+ return ["total-items"];
+ }
+
+ #route = undefined;
+ #firstItemIndexOnScreen = 0;
+
+ 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, customListStyles];
+ this.shadow.append(node);
+ this.classList.add("show-priority");
+ this.storageManager = new IndexedDBManager();
+
+ if (window.extraTodoListCssToAdopt) {
+ let extraAdoptedStyleSheet = new CSSStyleSheet();
+ extraAdoptedStyleSheet.replaceSync(window.extraTodoListCssToAdopt);
+ this.shadow.adoptedStyleSheets.push(extraAdoptedStyleSheet);
+ }
+ }
+
+ addItem(entry, itemIndex) {
+ const { id, title, completed } = entry;
+ const priority = 4 - (itemIndex % 5);
+ const element = new TodoItem();
+
+ element.setAttribute("itemid", id);
+ element.setAttribute("itemtitle", title);
+ element.setAttribute("itemcompleted", completed);
+ element.setAttribute("data-priority", priority);
+ element.itemIndex = itemIndex;
+
+ this.listNode.append(element);
+
+ this.#addItemToStorage(itemIndex, id, title, priority, completed);
+ }
+
+ removeItem(itemIndex) {
+ this.storageManager.removeTodo(itemIndex);
+ }
+
+ addItems(items) {
+ items.forEach((entry) => this.addItem(entry));
+ }
+
+ removeCompletedItems() {
+ Array.from(this.listNode.children).forEach((element) => {
+ if (element.itemcompleted === "true")
+ element.removeItem();
+ });
+ }
+
+ toggleItems(completed) {
+ Array.from(this.listNode.children).forEach((element) => {
+ if (completed && element.itemcompleted === "false")
+ element.toggleInput.click();
+ else if (!completed && element.itemcompleted === "true")
+ element.toggleInput.click();
+ });
+ }
+
+ toggleItem(itemNumber, completed) {
+ // Update the item in the IndexedDB
+ this.storageManager.toggleTodo(itemNumber, completed);
+ }
+
+ updateStyles() {
+ if (parseInt(this["total-items"]) !== 0)
+ this.listNode.style.display = "block";
+ else
+ this.listNode.style.display = "none";
+ }
+
+ updateRoute(route) {
+ this.#route = route;
+ switch (route) {
+ case "completed":
+ this.listNode.setAttribute("route", "completed");
+ break;
+ case "active":
+ this.listNode.setAttribute("rout", "active");
+ break;
+ default:
+ this.listNode.setAttribute("route", "all");
+ }
+ }
+
+ attributeChangedCallback(property, oldValue, newValue) {
+ if (oldValue === newValue)
+ return;
+ this[property] = newValue;
+ if (this.isConnected)
+ this.updateStyles();
+ }
+
+ connectedCallback() {
+ this.updateStyles();
+ }
+
+ moveToNextPage() {
+ for (let i = 0; i < MAX_ON_SCREEN_ITEMS; i++) {
+ const child = this.listNode.firstChild;
+ if (!child)
+ break;
+ child.remove();
+ }
+ this.#firstItemIndexOnScreen = this.listNode.firstChild.itemIndex;
+ }
+
+ moveToPreviousPage() {
+ return this.storageManager
+ .getTodos(this.#firstItemIndexOnScreen, MAX_ON_SCREEN_ITEMS)
+ .then((items) => {
+ const elements = items.map((item) => {
+ const { id, title, completed, priority } = item;
+ const element = new TodoItem();
+ element.setAttribute("itemid", id);
+ element.setAttribute("itemtitle", title);
+ element.setAttribute("itemcompleted", completed);
+ element.setAttribute("data-priority", priority);
+ element.itemIndex = item.itemNumber;
+ return element;
+ });
+ this.#firstItemIndexOnScreen = items[0].itemNumber;
+ this.listNode.replaceChildren(...elements);
+ })
+ .catch((error) => {
+ // Error retrieving previous todos
+ });
+ }
+
+ #addItemToStorage(itemIndex, id, title, priority, completed) {
+ // Create a todo object with the structure expected by IndexedDB
+ const todoItem = {
+ itemNumber: itemIndex,
+ id,
+ title,
+ completed,
+ priority,
+ };
+
+ // Add the item to IndexedDB and handle the Promise
+ this.storageManager.addTodo(todoItem);
+ }
+}
+
+customElements.define("todo-list", TodoList);
+
+export default TodoList;
diff --git a/experimental/javascript-wc-indexeddb/dist/components/todo-list/todo-list.template.js b/experimental/javascript-wc-indexeddb/dist/components/todo-list/todo-list.template.js
new file mode 100644
index 000000000..e92320b51
--- /dev/null
+++ b/experimental/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/experimental/javascript-wc-indexeddb/dist/components/todo-topbar/todo-topbar.component.js b/experimental/javascript-wc-indexeddb/dist/components/todo-topbar/todo-topbar.component.js
new file mode 100644
index 000000000..fab923447
--- /dev/null
+++ b/experimental/javascript-wc-indexeddb/dist/components/todo-topbar/todo-topbar.component.js
@@ -0,0 +1,141 @@
+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";
+
+const customListStyles = new CSSStyleSheet();
+customListStyles.replaceSync(`
+ .toggle-all-container {
+ display: block;
+ }
+ :host([total-items="0"]) .toggle-all-container {
+ display: none;
+ }
+`);
+
+class TodoTopbar extends HTMLElement {
+ static get observedAttributes() {
+ return ["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, customListStyles];
+ 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 = !parseInt(this["active-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/experimental/javascript-wc-indexeddb/dist/components/todo-topbar/todo-topbar.template.js b/experimental/javascript-wc-indexeddb/dist/components/todo-topbar/todo-topbar.template.js
new file mode 100644
index 000000000..e7e5286a3
--- /dev/null
+++ b/experimental/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/experimental/javascript-wc-indexeddb/dist/hooks/useDoubleClick.js b/experimental/javascript-wc-indexeddb/dist/hooks/useDoubleClick.js
new file mode 100644
index 000000000..a1fe952fe
--- /dev/null
+++ b/experimental/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/experimental/javascript-wc-indexeddb/dist/hooks/useKeyListener.js b/experimental/javascript-wc-indexeddb/dist/hooks/useKeyListener.js
new file mode 100644
index 000000000..453747d54
--- /dev/null
+++ b/experimental/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/experimental/javascript-wc-indexeddb/dist/hooks/useRouter.js b/experimental/javascript-wc-indexeddb/dist/hooks/useRouter.js
new file mode 100644
index 000000000..ab1ab618a
--- /dev/null
+++ b/experimental/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/experimental/javascript-wc-indexeddb/dist/index.html b/experimental/javascript-wc-indexeddb/dist/index.html
new file mode 100644
index 000000000..a8fec9787
--- /dev/null
+++ b/experimental/javascript-wc-indexeddb/dist/index.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+ TodoMVC: JavaScript Web Components
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/experimental/javascript-wc-indexeddb/dist/styles/app.constructable.js b/experimental/javascript-wc-indexeddb/dist/styles/app.constructable.js
new file mode 100644
index 000000000..8ac77f26a
--- /dev/null
+++ b/experimental/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/experimental/javascript-wc-indexeddb/dist/styles/bottombar.constructable.js b/experimental/javascript-wc-indexeddb/dist/styles/bottombar.constructable.js
new file mode 100644
index 000000000..46904de8a
--- /dev/null
+++ b/experimental/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/experimental/javascript-wc-indexeddb/dist/styles/footer.css b/experimental/javascript-wc-indexeddb/dist/styles/footer.css
new file mode 100644
index 000000000..0ff918f43
--- /dev/null
+++ b/experimental/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/experimental/javascript-wc-indexeddb/dist/styles/global.constructable.js b/experimental/javascript-wc-indexeddb/dist/styles/global.constructable.js
new file mode 100644
index 000000000..7ff85b07f
--- /dev/null
+++ b/experimental/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/experimental/javascript-wc-indexeddb/dist/styles/global.css b/experimental/javascript-wc-indexeddb/dist/styles/global.css
new file mode 100644
index 000000000..22ee5e16c
--- /dev/null
+++ b/experimental/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/experimental/javascript-wc-indexeddb/dist/styles/header.css b/experimental/javascript-wc-indexeddb/dist/styles/header.css
new file mode 100644
index 000000000..56d2a4064
--- /dev/null
+++ b/experimental/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/experimental/javascript-wc-indexeddb/dist/styles/main.constructable.js b/experimental/javascript-wc-indexeddb/dist/styles/main.constructable.js
new file mode 100644
index 000000000..66ea0b30c
--- /dev/null
+++ b/experimental/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/experimental/javascript-wc-indexeddb/dist/styles/todo-item.constructable.js b/experimental/javascript-wc-indexeddb/dist/styles/todo-item.constructable.js
new file mode 100644
index 000000000..59dba7f77
--- /dev/null
+++ b/experimental/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/experimental/javascript-wc-indexeddb/dist/styles/todo-list.constructable.js b/experimental/javascript-wc-indexeddb/dist/styles/todo-list.constructable.js
new file mode 100644
index 000000000..3d11133dd
--- /dev/null
+++ b/experimental/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/experimental/javascript-wc-indexeddb/dist/styles/topbar.constructable.js b/experimental/javascript-wc-indexeddb/dist/styles/topbar.constructable.js
new file mode 100644
index 000000000..5ecb8a231
--- /dev/null
+++ b/experimental/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/experimental/javascript-wc-indexeddb/dist/utils/nanoid.js b/experimental/javascript-wc-indexeddb/dist/utils/nanoid.js
new file mode 100644
index 000000000..5df154f1f
--- /dev/null
+++ b/experimental/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/experimental/javascript-wc-indexeddb/index.html b/experimental/javascript-wc-indexeddb/index.html
new file mode 100644
index 000000000..6820a9346
--- /dev/null
+++ b/experimental/javascript-wc-indexeddb/index.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+ TodoMVC: JavaScript Web Components
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/experimental/javascript-wc-indexeddb/package-lock.json b/experimental/javascript-wc-indexeddb/package-lock.json
new file mode 100644
index 000000000..4c68277f8
--- /dev/null
+++ b/experimental/javascript-wc-indexeddb/package-lock.json
@@ -0,0 +1,783 @@
+{
+ "name": "todomvc-javascript-web-components-indexeddb",
+ "version": "1.0.0",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "todomvc-javascript-web-components-indexeddb",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "http-server": "^14.1.1",
+ "todomvc-css": "file:../../resources/todomvc/todomvc-css"
+ },
+ "engines": {
+ "node": ">=18.13.0",
+ "npm": ">=8.19.3"
+ }
+ },
+ "../../resources/todomvc/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"
+ }
+ },
+ "../../todomvc-css": {
+ "version": "1.0.0",
+ "extraneous": true,
+ "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": "../../resources/todomvc/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:../../resources/todomvc/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/experimental/javascript-wc-indexeddb/package.json b/experimental/javascript-wc-indexeddb/package.json
new file mode 100644
index 000000000..c971f95b0
--- /dev/null
+++ b/experimental/javascript-wc-indexeddb/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "todomvc-javascript-web-components-indexeddb",
+ "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:../../resources/todomvc/todomvc-css"
+ }
+}
diff --git a/experimental/javascript-wc-indexeddb/scripts/build.js b/experimental/javascript-wc-indexeddb/scripts/build.js
new file mode 100644
index 000000000..d5fa77557
--- /dev/null
+++ b/experimental/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/experimental/javascript-wc-indexeddb/src/components/todo-app/todo-app.component.js b/experimental/javascript-wc-indexeddb/src/components/todo-app/todo-app.component.js
new file mode 100644
index 000000000..9e8a6010e
--- /dev/null
+++ b/experimental/javascript-wc-indexeddb/src/components/todo-app/todo-app.component.js
@@ -0,0 +1,158 @@
+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;
+ #numberOfItems = 0;
+ #numberOfCompletedItems = 0;
+ 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.moveToNextPage = this.moveToNextPage.bind(this);
+ this.moveToPreviousPage = this.moveToPreviousPage.bind(this);
+
+ this.router = useRouter();
+ }
+
+ get isReady() {
+ return this.#isReady;
+ }
+
+ getInstance() {
+ return this;
+ }
+
+ addItem(event) {
+ const { detail: item } = event;
+ this.list.addItem(item, this.#numberOfItems++);
+ this.update("add-item", item.id);
+ }
+
+ toggleItem(event) {
+ if (event.detail.completed)
+ this.#numberOfCompletedItems++;
+ else
+ this.#numberOfCompletedItems--;
+
+ this.list.toggleItem(event.detail.itemNumber, event.detail.completed);
+ this.update("toggle-item", event.detail.id);
+ }
+
+ removeItem(event) {
+ if (event.detail.completed)
+ this.#numberOfCompletedItems--;
+
+ this.#numberOfItems--;
+ this.update("remove-item", event.detail.id);
+ this.list.removeItem(event.detail.itemNumber);
+ }
+
+ updateItem(event) {
+ this.update("update-item", event.detail.id);
+ }
+
+ toggleItems(event) {
+ this.list.toggleItems(event.detail.completed);
+ }
+
+ clearCompletedItems() {
+ this.list.removeCompletedItems();
+ }
+
+ moveToNextPage() {
+ this.list.moveToNextPage();
+ }
+
+ moveToPreviousPage() {
+ // Skeleton implementation of previous page navigation
+ this.list.moveToPreviousPage().then(() => {
+ this.bottombar.reenablePreviousPageButton();
+ window.dispatchEvent(new CustomEvent("previous-page-loaded", {}));
+ });
+ }
+
+ update() {
+ const totalItems = this.#numberOfItems;
+ const completedItems = this.#numberOfCompletedItems;
+ const activeItems = totalItems - completedItems;
+
+ this.list.setAttribute("total-items", totalItems);
+
+ 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);
+ this.bottombar.addEventListener("next-page", this.moveToNextPage);
+ this.bottombar.addEventListener("previous-page", this.moveToPreviousPage);
+ }
+
+ 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);
+ this.bottombar.removeEventListener("next-page", this.moveToNextPage);
+ this.bottombar.removeEventListener("previous-page", this.moveToPreviousPage);
+ }
+
+ 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/experimental/javascript-wc-indexeddb/src/components/todo-app/todo-app.template.js b/experimental/javascript-wc-indexeddb/src/components/todo-app/todo-app.template.js
new file mode 100644
index 000000000..1a55a8194
--- /dev/null
+++ b/experimental/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/experimental/javascript-wc-indexeddb/src/components/todo-bottombar/todo-bottombar.component.js b/experimental/javascript-wc-indexeddb/src/components/todo-bottombar/todo-bottombar.component.js
new file mode 100644
index 000000000..0b9d2f19a
--- /dev/null
+++ b/experimental/javascript-wc-indexeddb/src/components/todo-bottombar/todo-bottombar.component.js
@@ -0,0 +1,126 @@
+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";
+
+const customStyles = new CSSStyleSheet();
+customStyles.replaceSync(`
+
+ .clear-completed-button, .clear-completed-button:active,
+ .todo-status,
+ .filter-list
+ {
+ position: unset;
+ transform: unset;
+ }
+
+ .bottombar {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ align-items: center;
+ justify-items: center;
+ }
+
+ .bottombar > * {
+ grid-column: span 1;
+ }
+
+ .filter-list {
+ grid-column: span 3;
+ }
+
+ :host([total-items="0"]) > .bottombar {
+ display: none;
+ }
+`);
+
+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, customStyles];
+ this.shadow.append(node);
+
+ this.clearCompletedItems = this.clearCompletedItems.bind(this);
+ this.MoveToNextPage = this.MoveToNextPage.bind(this);
+ this.MoveToPreviousPage = this.MoveToPreviousPage.bind(this);
+ }
+
+ updateDisplay() {
+ 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"));
+ }
+
+ MoveToNextPage() {
+ this.dispatchEvent(new CustomEvent("next-page"));
+ }
+
+ MoveToPreviousPage() {
+ this.element.querySelector(".previous-page-button").disabled = true;
+ this.dispatchEvent(new CustomEvent("previous-page"));
+ }
+
+ addListeners() {
+ this.clearCompletedButton.addEventListener("click", this.clearCompletedItems);
+ this.element.querySelector(".next-page-button").addEventListener("click", this.MoveToNextPage);
+ this.element.querySelector(".previous-page-button").addEventListener("click", this.MoveToPreviousPage);
+ }
+
+ removeListeners() {
+ this.clearCompletedButton.removeEventListener("click", this.clearCompletedItems);
+ this.getElementById("next-page-button").removeEventListener("click", this.MoveToNextPage);
+ this.getElementById("previous-page-button").removeEventListener("click", this.MoveToPreviousPage);
+ }
+
+ attributeChangedCallback(property, oldValue, newValue) {
+ if (oldValue === newValue)
+ return;
+ this[property] = newValue;
+
+ if (this.isConnected)
+ this.updateDisplay();
+ }
+
+ reenablePreviousPageButton() {
+ this.element.querySelector(".previous-page-button").disabled = false;
+ window.dispatchEvent(new CustomEvent("previous-page-button-enabled", {}));
+ }
+
+ connectedCallback() {
+ this.updateDisplay();
+ this.addListeners();
+ }
+
+ disconnectedCallback() {
+ this.removeListeners();
+ }
+}
+
+customElements.define("todo-bottombar", TodoBottombar);
+
+export default TodoBottombar;
diff --git a/experimental/javascript-wc-indexeddb/src/components/todo-bottombar/todo-bottombar.template.js b/experimental/javascript-wc-indexeddb/src/components/todo-bottombar/todo-bottombar.template.js
new file mode 100644
index 000000000..e9259fe30
--- /dev/null
+++ b/experimental/javascript-wc-indexeddb/src/components/todo-bottombar/todo-bottombar.template.js
@@ -0,0 +1,24 @@
+const template = document.createElement("template");
+
+template.id = "todo-bottombar-template";
+template.innerHTML = `
+
+`;
+
+export default template;
diff --git a/experimental/javascript-wc-indexeddb/src/components/todo-item/todo-item.component.js b/experimental/javascript-wc-indexeddb/src/components/todo-item/todo-item.component.js
new file mode 100644
index 000000000..6def10994
--- /dev/null
+++ b/experimental/javascript-wc-indexeddb/src/components/todo-item/todo-item.component.js
@@ -0,0 +1,182 @@
+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";
+ this.itemIndex = null;
+
+ 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";
+ 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: { completed: this.toggleInput.checked, itemNumber: this.itemIndex },
+ bubbles: true,
+ })
+ );
+ }
+
+ removeItem() {
+ this.dispatchEvent(
+ new CustomEvent("remove-item", {
+ detail: { completed: this.toggleInput.checked, itemNumber: this.itemIndex },
+ 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.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/experimental/javascript-wc-indexeddb/src/components/todo-item/todo-item.template.js b/experimental/javascript-wc-indexeddb/src/components/todo-item/todo-item.template.js
new file mode 100644
index 000000000..9a67675fd
--- /dev/null
+++ b/experimental/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/experimental/javascript-wc-indexeddb/src/components/todo-list/todo-list.component.js b/experimental/javascript-wc-indexeddb/src/components/todo-list/todo-list.component.js
new file mode 100644
index 000000000..ca7b31fac
--- /dev/null
+++ b/experimental/javascript-wc-indexeddb/src/components/todo-list/todo-list.component.js
@@ -0,0 +1,425 @@
+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 IndexedDBManager {
+ constructor() {
+ this.dbName = "todoDB";
+ this.dbVersion = 1;
+ this.storeName = "todos";
+ this.db = null;
+ this.pendingAdditions = 0;
+ this.totalItemsToggled = 0;
+ this.totalItemsDeleted = 0;
+ this.initDB().then(() => {
+ const newDiv = document.createElement("div");
+ newDiv.classList.add("indexeddb-ready");
+ newDiv.style.display = "none";
+ document.body.append(newDiv);
+ });
+ }
+
+ initDB() {
+ return new Promise((resolve, reject) => {
+ // Delete the existing database first for clean state
+ const deleteRequest = indexedDB.deleteDatabase(this.dbName);
+
+ deleteRequest.onerror = (event) => {
+ // Continue despite error in deletion
+ this.openDatabase(resolve, reject);
+ };
+
+ deleteRequest.onsuccess = () => {
+ this.openDatabase(resolve, reject);
+ };
+
+ deleteRequest.onblocked = () => {
+ // Try opening anyway
+ this.openDatabase(resolve, reject);
+ };
+ });
+ }
+
+ openDatabase(resolve, reject) {
+ const request = indexedDB.open(this.dbName, this.dbVersion);
+
+ request.onerror = (event) => {
+ reject(event.target.error);
+ };
+
+ request.onsuccess = (event) => {
+ this.db = event.target.result;
+ resolve(this.db);
+ };
+
+ request.onupgradeneeded = (event) => {
+ const db = event.target.result;
+
+ // Create object store (since we're always creating a fresh DB now)
+ const store = db.createObjectStore(this.storeName, { keyPath: "itemNumber" });
+ store.createIndex("id", "id", { unique: true });
+ store.createIndex("title", "title", { unique: false });
+ store.createIndex("completed", "completed", { unique: false });
+ store.createIndex("priority", "priority", { unique: false });
+ };
+ }
+
+ addTodo(todo) {
+ return new Promise((resolve, reject) => {
+ // Ensure the database connection is established
+ if (!this.db) {
+ this.initDB()
+ .then(() => this.addTodo(todo))
+ .then(resolve)
+ .catch(reject);
+ return;
+ }
+
+ // Add todo item to IndexedDB
+ const transaction = this.db.transaction(this.storeName, "readwrite");
+ const store = transaction.objectStore(this.storeName);
+
+ const request = store.add(todo);
+ this.pendingAdditions++;
+
+ request.onsuccess = () => {
+ if (--this.pendingAdditions === 0)
+ window.dispatchEvent(new CustomEvent("indexeddb-add-completed", {}));
+
+ resolve(todo);
+ };
+
+ request.onerror = (event) => {
+ reject(event.target.error);
+ };
+ });
+ }
+
+ getTodos(upperItemNumber, count) {
+ return new Promise((resolve, reject) => {
+ // Ensure the database connection is established
+ if (!this.db) {
+ this.initDB()
+ .then(() => this.getTodos(upperItemNumber, count))
+ .then(resolve)
+ .catch(reject);
+ return;
+ }
+
+ const transaction = this.db.transaction(this.storeName, "readonly");
+ const store = transaction.objectStore(this.storeName);
+
+ // Use IDBKeyRange to get items with itemNumber less than upperItemNumber
+ const range = IDBKeyRange.upperBound(upperItemNumber, true); // true = exclusive bound
+
+ // Open a cursor to iterate through records in descending order
+ const request = store.openCursor(range, "prev");
+
+ const items = [];
+ let itemsProcessed = 0;
+
+ request.onsuccess = (event) => {
+ const cursor = event.target.result;
+
+ // Check if we have a valid cursor and haven't reached our count limit
+ if (cursor && itemsProcessed < count) {
+ items.push(cursor.value);
+ itemsProcessed++;
+ cursor.continue(); // Move to next item
+ } else {
+ // We're done - sort items by itemNumber in descending order
+ // for proper display order (newest to oldest)
+ items.sort((a, b) => a.itemNumber - b.itemNumber);
+
+ resolve(items);
+ }
+ };
+
+ request.onerror = (event) => {
+ reject(event.target.error);
+ };
+
+ // Also handle transaction errors
+ transaction.onerror = (event) => {
+ reject(event.target.error);
+ };
+ });
+ }
+
+ toggleTodo(itemNumber, completed) {
+ return new Promise((resolve, reject) => {
+ // Ensure the database connection is established
+ if (!this.db) {
+ this.initDB()
+ .then(() => this.toggleTodo(itemNumber, completed))
+ .then(resolve)
+ .catch(reject);
+ return;
+ }
+
+ // Access the todo item directly by its itemNumber (keyPath)
+ const transaction = this.db.transaction(this.storeName, "readwrite");
+ const store = transaction.objectStore(this.storeName);
+
+ // Get the todo item directly using its primary key (itemNumber)
+ const getRequest = store.get(itemNumber);
+
+ getRequest.onsuccess = (event) => {
+ const todoItem = getRequest.result;
+
+ if (!todoItem) {
+ reject(new Error(`Todo item with itemNumber '${itemNumber}' not found`));
+ return;
+ }
+
+ // Update the completed status
+ todoItem.completed = completed;
+
+ // Save the updated item back to the database
+ const updateRequest = store.put(todoItem);
+
+ updateRequest.onsuccess = () => {
+ if (window.numberOfItemsToAdd && ++this.totalItemsToggled === window.numberOfItemsToAdd)
+ window.dispatchEvent(new CustomEvent("indexeddb-toggle-completed", {}));
+
+ resolve(todoItem);
+ };
+
+ updateRequest.onerror = (event) => {
+ reject(event.target.error);
+ };
+ };
+
+ getRequest.onerror = (event) => {
+ reject(event.target.error);
+ };
+
+ // Handle potential errors in finding the item
+ transaction.onerror = (event) => {
+ reject(event.target.error);
+ };
+
+ // Handle transaction errors
+ transaction.onerror = (event) => {
+ reject(event.target.error);
+ };
+ });
+ }
+
+ removeTodo(itemNumber) {
+ return new Promise((resolve, reject) => {
+ // Ensure the database connection is established
+ if (!this.db) {
+ this.initDB()
+ .then(() => this.removeTodo(itemNumber))
+ .then(resolve)
+ .catch(reject);
+ return;
+ }
+
+ // Access the todo item directly by its itemNumber (keyPath)
+ const transaction = this.db.transaction(this.storeName, "readwrite");
+ const store = transaction.objectStore(this.storeName);
+
+ // Delete the todo item directly using its primary key (itemNumber)
+ const deleteRequest = store.delete(itemNumber);
+
+ deleteRequest.onsuccess = () => {
+ if (window.numberOfItemsToAdd && ++this.totalItemsDeleted === window.numberOfItemsToAdd)
+ window.dispatchEvent(new CustomEvent("indexeddb-remove-completed", {}));
+
+ resolve(itemNumber);
+ };
+
+ deleteRequest.onerror = (event) => {
+ reject(event.target.error);
+ };
+
+ // Handle transaction errors
+ transaction.onerror = (event) => {
+ reject(event.target.error);
+ };
+ });
+ }
+}
+
+const MAX_ON_SCREEN_ITEMS = 10;
+
+const customListStyles = new CSSStyleSheet();
+customListStyles.replaceSync(`
+ .todo-list > todo-item {
+ display: block;
+ }
+
+ .todo-list[route="completed"] > [itemcompleted="false"] {
+ display: none;
+ }
+
+ .todo-list[route="active"] > [itemcompleted="true"] {
+ display: none;
+ }
+
+ :nth-child(${MAX_ON_SCREEN_ITEMS}) ~ todo-item {
+ display: none;
+ }
+`);
+
+class TodoList extends HTMLElement {
+ static get observedAttributes() {
+ return ["total-items"];
+ }
+
+ #route = undefined;
+ #firstItemIndexOnScreen = 0;
+
+ 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, customListStyles];
+ this.shadow.append(node);
+ this.classList.add("show-priority");
+ this.storageManager = new IndexedDBManager();
+
+ if (window.extraTodoListCssToAdopt) {
+ let extraAdoptedStyleSheet = new CSSStyleSheet();
+ extraAdoptedStyleSheet.replaceSync(window.extraTodoListCssToAdopt);
+ this.shadow.adoptedStyleSheets.push(extraAdoptedStyleSheet);
+ }
+ }
+
+ addItem(entry, itemIndex) {
+ const { id, title, completed } = entry;
+ const priority = 4 - (itemIndex % 5);
+ const element = new TodoItem();
+
+ element.setAttribute("itemid", id);
+ element.setAttribute("itemtitle", title);
+ element.setAttribute("itemcompleted", completed);
+ element.setAttribute("data-priority", priority);
+ element.itemIndex = itemIndex;
+
+ this.listNode.append(element);
+
+ this.#addItemToStorage(itemIndex, id, title, priority, completed);
+ }
+
+ removeItem(itemIndex) {
+ this.storageManager.removeTodo(itemIndex);
+ }
+
+ addItems(items) {
+ items.forEach((entry) => this.addItem(entry));
+ }
+
+ removeCompletedItems() {
+ Array.from(this.listNode.children).forEach((element) => {
+ if (element.itemcompleted === "true")
+ element.removeItem();
+ });
+ }
+
+ toggleItems(completed) {
+ Array.from(this.listNode.children).forEach((element) => {
+ if (completed && element.itemcompleted === "false")
+ element.toggleInput.click();
+ else if (!completed && element.itemcompleted === "true")
+ element.toggleInput.click();
+ });
+ }
+
+ toggleItem(itemNumber, completed) {
+ // Update the item in the IndexedDB
+ this.storageManager.toggleTodo(itemNumber, completed);
+ }
+
+ updateStyles() {
+ if (parseInt(this["total-items"]) !== 0)
+ this.listNode.style.display = "block";
+ else
+ this.listNode.style.display = "none";
+ }
+
+ updateRoute(route) {
+ this.#route = route;
+ switch (route) {
+ case "completed":
+ this.listNode.setAttribute("route", "completed");
+ break;
+ case "active":
+ this.listNode.setAttribute("rout", "active");
+ break;
+ default:
+ this.listNode.setAttribute("route", "all");
+ }
+ }
+
+ attributeChangedCallback(property, oldValue, newValue) {
+ if (oldValue === newValue)
+ return;
+ this[property] = newValue;
+ if (this.isConnected)
+ this.updateStyles();
+ }
+
+ connectedCallback() {
+ this.updateStyles();
+ }
+
+ moveToNextPage() {
+ for (let i = 0; i < MAX_ON_SCREEN_ITEMS; i++) {
+ const child = this.listNode.firstChild;
+ if (!child)
+ break;
+ child.remove();
+ }
+ this.#firstItemIndexOnScreen = this.listNode.firstChild.itemIndex;
+ }
+
+ moveToPreviousPage() {
+ return this.storageManager
+ .getTodos(this.#firstItemIndexOnScreen, MAX_ON_SCREEN_ITEMS)
+ .then((items) => {
+ const elements = items.map((item) => {
+ const { id, title, completed, priority } = item;
+ const element = new TodoItem();
+ element.setAttribute("itemid", id);
+ element.setAttribute("itemtitle", title);
+ element.setAttribute("itemcompleted", completed);
+ element.setAttribute("data-priority", priority);
+ element.itemIndex = item.itemNumber;
+ return element;
+ });
+ this.#firstItemIndexOnScreen = items[0].itemNumber;
+ this.listNode.replaceChildren(...elements);
+ })
+ .catch((error) => {
+ // Error retrieving previous todos
+ });
+ }
+
+ #addItemToStorage(itemIndex, id, title, priority, completed) {
+ // Create a todo object with the structure expected by IndexedDB
+ const todoItem = {
+ itemNumber: itemIndex,
+ id,
+ title,
+ completed,
+ priority,
+ };
+
+ // Add the item to IndexedDB and handle the Promise
+ this.storageManager.addTodo(todoItem);
+ }
+}
+
+customElements.define("todo-list", TodoList);
+
+export default TodoList;
diff --git a/experimental/javascript-wc-indexeddb/src/components/todo-list/todo-list.template.js b/experimental/javascript-wc-indexeddb/src/components/todo-list/todo-list.template.js
new file mode 100644
index 000000000..e92320b51
--- /dev/null
+++ b/experimental/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/experimental/javascript-wc-indexeddb/src/components/todo-topbar/todo-topbar.component.js b/experimental/javascript-wc-indexeddb/src/components/todo-topbar/todo-topbar.component.js
new file mode 100644
index 000000000..052ba5889
--- /dev/null
+++ b/experimental/javascript-wc-indexeddb/src/components/todo-topbar/todo-topbar.component.js
@@ -0,0 +1,141 @@
+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";
+
+const customListStyles = new CSSStyleSheet();
+customListStyles.replaceSync(`
+ .toggle-all-container {
+ display: block;
+ }
+ :host([total-items="0"]) .toggle-all-container {
+ display: none;
+ }
+`);
+
+class TodoTopbar extends HTMLElement {
+ static get observedAttributes() {
+ return ["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, customListStyles];
+ 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 = !parseInt(this["active-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/experimental/javascript-wc-indexeddb/src/components/todo-topbar/todo-topbar.template.js b/experimental/javascript-wc-indexeddb/src/components/todo-topbar/todo-topbar.template.js
new file mode 100644
index 000000000..e7e5286a3
--- /dev/null
+++ b/experimental/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/experimental/javascript-wc-indexeddb/src/hooks/useDoubleClick.js b/experimental/javascript-wc-indexeddb/src/hooks/useDoubleClick.js
new file mode 100644
index 000000000..a1fe952fe
--- /dev/null
+++ b/experimental/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/experimental/javascript-wc-indexeddb/src/hooks/useKeyListener.js b/experimental/javascript-wc-indexeddb/src/hooks/useKeyListener.js
new file mode 100644
index 000000000..453747d54
--- /dev/null
+++ b/experimental/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/experimental/javascript-wc-indexeddb/src/hooks/useRouter.js b/experimental/javascript-wc-indexeddb/src/hooks/useRouter.js
new file mode 100644
index 000000000..ab1ab618a
--- /dev/null
+++ b/experimental/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/experimental/javascript-wc-indexeddb/src/utils/nanoid.js b/experimental/javascript-wc-indexeddb/src/utils/nanoid.js
new file mode 100644
index 000000000..5df154f1f
--- /dev/null
+++ b/experimental/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;
+}
diff --git a/resources/benchmark-runner.mjs b/resources/benchmark-runner.mjs
index 3fd6ec0a9..3857f6347 100644
--- a/resources/benchmark-runner.mjs
+++ b/resources/benchmark-runner.mjs
@@ -5,9 +5,10 @@ import { SUITE_RUNNER_LOOKUP } from "./suite-runner.mjs";
const performance = globalThis.performance;
export class BenchmarkTestStep {
- constructor(testName, testFunction) {
+ constructor(testName, testFunction, ignoreResult = false) {
this.name = testName;
this.run = testFunction;
+ this.ignoreResult = ignoreResult;
}
}
@@ -119,6 +120,18 @@ class Page {
_wrapElement(element) {
return new PageElement(element);
}
+
+ addEventListener(name, listener) {
+ this._frame.contentWindow.addEventListener(name, listener);
+ }
+
+ setGlobalVariable(name, value) {
+ this._frame.contentWindow[name] = value;
+ }
+
+ getGlobalVariable(name) {
+ return this._frame.contentWindow[name];
+ }
}
const NATIVE_OPTIONS = {
diff --git a/resources/suite-runner.mjs b/resources/suite-runner.mjs
index 6a6a953ff..5b67ab274 100644
--- a/resources/suite-runner.mjs
+++ b/resources/suite-runner.mjs
@@ -110,6 +110,10 @@ export class SuiteRunner {
if (this.#suite === WarmupSuite)
return;
+ // Skip recording results if the test step has ignoreResult flag set to true
+ if (test.ignoreResult)
+ return;
+
const total = syncTime + asyncTime;
this.#suiteResults.tests[test.name] = { tests: { Sync: syncTime, Async: asyncTime }, total: total };
this.#suiteResults.total += total;
diff --git a/resources/tests.mjs b/resources/tests.mjs
index d7cf6794a..effa55743 100644
--- a/resources/tests.mjs
+++ b/resources/tests.mjs
@@ -241,6 +241,124 @@ Suites.push({
],
});
+Suites.push({
+ name: "TodoMVC-WebComponents-IndexedDB",
+ url: "experimental/javascript-wc-indexeddb/dist/index.html",
+ tags: ["todomvc", "webcomponents", "experimental"],
+ type: "async",
+ async prepare(page) {
+ await page.waitForElement("todo-app");
+ await page.waitForElement(".indexeddb-ready");
+ page.setGlobalVariable(
+ "addPromise",
+ new Promise((resolve) => {
+ page.addEventListener("indexeddb-add-completed", () => {
+ resolve();
+ });
+ })
+ );
+ page.setGlobalVariable(
+ "completePromise",
+ new Promise((resolve) => {
+ page.addEventListener("indexeddb-toggle-completed", () => {
+ resolve();
+ });
+ })
+ );
+ page.setGlobalVariable(
+ "removePromise",
+ new Promise((resolve) => {
+ page.addEventListener("indexeddb-remove-completed", () => {
+ resolve();
+ });
+ })
+ );
+ },
+ tests: [
+ new BenchmarkTestStep(`Adding${numberOfItemsToAdd}Items`, async (page) => {
+ const input = page.querySelector(".new-todo-input", ["todo-app", "todo-topbar"]);
+ for (let i = 0; i < numberOfItemsToAdd; i++) {
+ input.setValue(getTodoText(defaultLanguage, i));
+ input.dispatchEvent("input");
+ input.enter("keyup");
+ }
+ }),
+ new BenchmarkTestStep(
+ "FinishAddingItemsToDB",
+ async (page) => {
+ await page.getGlobalVariable("addPromise");
+ },
+ true
+ ),
+ new BenchmarkTestStep("CompletingAllItems", async (page) => {
+ const numberOfItemsPerIteration = 10;
+ const numberOfIterations = 10;
+ page.setGlobalVariable("numberOfItemsToAdd", numberOfItemsToAdd);
+ for (let j = 0; j < numberOfIterations; j++) {
+ const items = page.querySelectorAll("todo-item", ["todo-app", "todo-list"]);
+ for (let i = 0; i < numberOfItemsPerIteration; i++) {
+ const item = items[i].querySelectorInShadowRoot(".toggle-todo-input");
+ item.click();
+ }
+ // // Let the layout update???
+ // // Give a change to the pending indexedDB operations to complete in main thread.
+ // await new Promise((resolve) => {
+ // setTimeout(() => {resolve();}, 0);
+ // });
+ if (j < 9) {
+ const nextPageButton = page.querySelector(".next-page-button", ["todo-app", "todo-bottombar"]);
+ nextPageButton.click();
+ }
+ }
+ }),
+ new BenchmarkTestStep(
+ "FinishModifyingItemsInDB",
+ async (page) => {
+ await page.getGlobalVariable("completePromise");
+ },
+ true
+ ),
+ new BenchmarkTestStep("DeletingAllItems", async (page) => {
+ const numberOfItemsPerIteration = 10;
+ const numberOfIterations = 10;
+ page.setGlobalVariable("numberOfItemsToAdd", numberOfItemsToAdd);
+ function iterationFinishedListener() {
+ iterationFinishedListener.promiseResolve();
+ }
+ page.addEventListener("previous-page-loaded", iterationFinishedListener);
+ for (let j = 0; j < numberOfIterations; j++) {
+ const iterationFinishedPromise = new Promise((resolve) => {
+ iterationFinishedListener.promiseResolve = resolve;
+ });
+ const items = page.querySelectorAll("todo-item", ["todo-app", "todo-list"]);
+ for (let i = numberOfItemsPerIteration - 1; i >= 0; i--) {
+ const item = items[i].querySelectorInShadowRoot(".remove-todo-button");
+ item.click();
+ }
+ // Let the layout update???
+ // Give a change to the pending indexedDB operations to complete in main thread.
+ await new Promise((resolve) => {
+ setTimeout(() => {
+ resolve();
+ }, 0);
+ });
+ if (j < 9) {
+ const previousPageButton = page.querySelector(".previous-page-button", ["todo-app", "todo-bottombar"]);
+ previousPageButton.click();
+ await iterationFinishedPromise;
+ }
+ }
+ }),
+ new BenchmarkTestStep(
+ "FinishDeletingItemsFromDB",
+ async (page) => {
+ await page.getGlobalVariable("removePromise");
+ },
+ true
+ ),
+ ],
+});
+
Suites.push({
name: "TodoMVC-WebComponents",
url: "resources/todomvc/vanilla-examples/javascript-web-components/dist/index.html",