Blog/Components/Pages/Storage.razor.js 8 K · 244 lines · raw · history

1 import {getById, writeError, addClass, writeDebug, writeInfo, h, span} from "/common.module.js";
2
3 /** @member {IDBDatabase} db */
4 let db;
5
6 function InitDB(upgradeCallback) {
7 writeDebug(`db = ${db?.version}`)
8 if (db) {
9 writeDebug("Closing database");
10 db.close();
11 }
12 writeDebug(`db = ${db?.version}`)
13 const DBOpenRequest = db === null || db === undefined
14 ? window.indexedDB.open('mb-storage')
15 : window.indexedDB.open('mb-storage', db.version + 1);
16
17 DBOpenRequest.onupgradeneeded = (e) => {
18 writeDebug(`Upgrading from ${e.oldVersion} to ${e.newVersion}`);
19 if (upgradeCallback) upgradeCallback(e);
20 };
21
22 DBOpenRequest.onerror = () => {
23 writeError('Error loading database.');
24 };
25
26 DBOpenRequest.onsuccess = () => {
27 db = DBOpenRequest.result;
28 writeDebug(`db = ${db.version}`)
29 writeDebug("Database opened successfully.");
30 db.onversionchange = () => {
31 writeInfo(`Version changed in another tab, closing database.`);
32 }
33 showStores();
34 };
35
36 DBOpenRequest.onblocked = () => {
37 writeError(`Please close all other tabs with this site open!`);
38 }
39
40 }
41
42 export function onLoad() {
43 try {
44 db?.close();
45 db = null;
46 InitDB(null);
47
48 const newStoreButton = getById('newStoreButton');
49 newStoreButton.addEventListener('click', async () => {
50 const newStoreName = await getInput("New store", "Store name");
51
52 if (newStoreName) {
53 writeDebug("New store submitted.");
54 InitDB((event) => {
55 writeDebug(`Creating new store: ${newStoreName}, version: ${event.newVersion}`);
56 event.target.result.createObjectStore(newStoreName, {autoIncrement: true});
57 writeDebug("New store created: " + newStoreName);
58 });
59 }
60 });
61
62 } catch (e) {
63 writeError(e.message);
64 throw e;
65 }
66 }
67
68 function showStores() {
69 const storeList = getById("storeList");
70 storeList.replaceChildren();
71
72 let storeNames = db.objectStoreNames;
73
74 for (const storeName of storeNames) {
75 const deleteButton = h("button", "Delete");
76
77 const openButton = h("button", "Open");
78 openButton.addEventListener('click', () => {
79 if (openButton.textContent === "Open") {
80 openButton.closeCallback = openStore(storeName, deleteButton);
81 openButton.textContent = "Close";
82 } else {
83 openButton.closeCallback();
84 openButton.textContent = "Open";
85 openButton.closeCallback = null;
86 }
87 })
88
89 deleteButton.addEventListener('click', async () => {
90 if (await askForConfirmation(`Are you sure you want to delete store '${storeName}'?`)) {
91 InitDB((event) => {
92 writeDebug(`Deleting store: ${storeName}, version: ${event.newVersion}`);
93 event.target.result.deleteObjectStore(storeName);
94 writeDebug("Deleted store: " + storeName);
95 });
96
97 if (openButton.closeCallback) openButton.closeCallback();
98 }
99 })
100
101 const storeElement = span(storeName);
102 storeList.appendChild(storeElement);
103 storeList.appendChild(openButton);
104 storeList.appendChild(deleteButton);
105 }
106 }
107
108 function openStore(storeName) {
109 const openedStores = getById("openedStores");
110
111 const addButton = h("button", "Add");
112 const storeElement = addClass(h("fieldset", h("legend", storeName + " ", addButton)), "docs");
113 const closeStore = () => {
114 openedStores.removeChild(storeElement);
115 };
116
117 openedStores.appendChild(storeElement);
118 const elementList = addClass(h("div"), "grid-stores");
119 storeElement.appendChild(elementList);
120
121 function showItems() {
122
123 /** @member {Node[]} newChildren*/
124 const newChildren = [];
125 const store = db.transaction(storeName).objectStore(storeName);
126 store.openCursor().onsuccess = (event) => {
127 /** @member {IDBCursorWithValue} cursor*/
128 const cursor = event.target.result;
129 if (!cursor) {
130 elementList.replaceChildren(...newChildren);
131 return;
132 }
133 const value = cursor.value;
134 const key = cursor.key;
135 const editItemButton = h("button", "Edit");
136
137 const deleteItemButton = h("button", "Delete");
138 deleteItemButton.addEventListener('click', () => {
139 const transaction = db.transaction(storeName, 'readwrite');
140 transaction.oncomplete = () => {
141 showItems();
142 }
143 const store = transaction.objectStore(storeName);
144 store.delete(key);
145 })
146
147 editItemButton.addEventListener('click', async () => {
148 const value = await getInput("Edit value", "new value");
149
150 if (value) {
151 const transaction = db.transaction(storeName, 'readwrite');
152 transaction.oncomplete = () => {
153 showItems();
154 }
155 const store = transaction.objectStore(storeName);
156 store.put(value, key);
157 }
158 })
159
160 newChildren.push(span(value));
161 newChildren.push(editItemButton);
162 newChildren.push(deleteItemButton);
163
164 cursor.continue();
165 }
166 }
167
168 showItems();
169
170 addButton.addEventListener('click', async () => {
171 const value = await getInput("New value", "value");
172 if (value) {
173 const transaction = db.transaction(storeName, "readwrite");
174 transaction.oncomplete = () => {
175 showItems();
176 }
177 const store = transaction.objectStore(storeName);
178 store.add(value);
179 }
180 })
181
182 return closeStore;
183 }
184
185 /**
186 * @param {string} title
187 * @param {string} placeholder
188 * @return {Promise<string>}
189 */
190 function getInput(title, placeholder) {
191 return new Promise((resolve) => {
192 const inputDialog = getById("inputDialog");
193 const cancelInputButton = getById('cancelInputButton');
194 const inputDialogInput = getById('inputDialogInput');
195 const inputForm = getById('inputForm');
196
197 inputDialogInput.placeholder = placeholder;
198 getById("inputTitle").textContent = title;
199
200 inputDialog.showModal();
201
202 const cancelCallback = () => {
203 inputDialog.close("cancelInput");
204 cancelInputButton.removeEventListener("click", cancelCallback);
205 inputForm.removeEventListener("submit", submitCallback);
206 resolve(null);
207 };
208
209 cancelInputButton.addEventListener('click', cancelCallback);
210
211 const submitCallback = () => {
212 const inputValue = inputDialogInput.value;
213 inputDialogInput.value = '';
214 inputForm.removeEventListener("submit", submitCallback);
215 cancelInputButton.removeEventListener("click", cancelCallback);
216 resolve(inputValue);
217 };
218
219 inputForm.addEventListener('submit', submitCallback);
220 })
221 }
222
223 function askForConfirmation(question) {
224 return new Promise((resolve) => {
225 const confirmDialog = getById("confirmDialog");
226 getById("confirmMessage").textContent = question;
227 confirmDialog.showModal();
228
229 const cancelConfirmButton = getById("cancelConfirmButton");
230 const closeCancelled = () => {
231 confirmDialog.close("cancelConfirmation");
232 resolve(false)
233 };
234 cancelConfirmButton.addEventListener('click', closeCancelled);
235
236 const confirmForm = getById("confirmForm");
237 const submitConfirmation = () => {
238 cancelConfirmButton.removeEventListener('click', closeCancelled);
239 confirmForm.removeEventListener('submit', submitConfirmation);
240 resolve(true);
241 };
242 confirmForm.addEventListener('submit', submitConfirmation);
243 })
244 }