remove addClass, add h
- author
- Marijn Besseling <njirambem@gmail.com> · 2026-07-07 15:02 UTC
- commit
- 093ed2adcbdd8855dad0f5cd158a79d5e7ff1420
- parent
- 34a563285b
- tree
- browse at this commit
10 files changed +300 -174
.run/Publish Blog to custom server.run.xml +8 -2
| @@ -1,6 +1,12 @@ | ||
| 1 | 1 | <component name="ProjectRunConfigurationManager"> |
| 2 | 2 | <configuration default="false" name="Publish Blog to custom server" type="DotNetPublishAndUpload" factoryName="Publish to custom server"> |
| 3 | - <riderPublish configuration="Release" platform="Any CPU" ready_to_run="true" runtime="linux-x64" server_id="f1ec4348-1f2d-4563-86c5-2d9f5df54746" serverName="Bes.is" target_framework="net10.0" uuid_high="6481978729836661993" uuid_low="-7312154749475505895" /> | |
| 4 | - <method v="2" /> | |
| 3 | + <riderPublish configuration="Release" platform="Any CPU" ready_to_run="true" server_id="f1ec4348-1f2d-4563-86c5-2d9f5df54746" serverName="Bes.is" target_framework="net10.0" uuid_high="6481978729836661993" uuid_low="-7312154749475505895"> | |
| 4 | + <runtimes> | |
| 5 | + <item value="linux-x64" /> | |
| 6 | + </runtimes> | |
| 7 | + </riderPublish> | |
| 8 | + <method v="2"> | |
| 9 | + <option name="ToolBeforeRunTask" enabled="true" actionId="Tool_External Tools_restore linux" /> | |
| 10 | + </method> | |
| 5 | 11 | </configuration> |
| 6 | 12 | </component> |
| No newline at end of file | ||
Blog/Components/Pages/Calc.razor.js +6 -9
| @@ -1,4 +1,4 @@ | ||
| 1 | -import { getById, div, span, h, writeError } from "/common.module.js" | |
| 1 | +import { getById, h, writeError } from "/common.module.js" | |
| 2 | 2 | |
| 3 | 3 | export function onLoad() { |
| 4 | 4 | console.log('Loaded'); |
| @@ -110,15 +110,14 @@ function parse(word, stack) { | ||
| 110 | 110 | } |
| 111 | 111 | |
| 112 | 112 | function writeLog(stack, words) { |
| 113 | - let log_left = span(`[${stack.join(', ')}]`); | |
| 114 | - let log_right = span(words.join(' ')); | |
| 113 | + let log_left = h("span", `[${stack.join(', ')}]`); | |
| 114 | + let log_right = h("span", words.join(' ')); | |
| 115 | 115 | |
| 116 | 116 | if (words.length === 0) { |
| 117 | 117 | log_left.textContent += " <==" |
| 118 | 118 | } |
| 119 | 119 | |
| 120 | - let log_row = div(log_left, log_right); | |
| 121 | - log_row.className = "flex-spread"; | |
| 120 | + let log_row = h("div", {class: "flex-spread"}, [log_left, log_right]); | |
| 122 | 121 | |
| 123 | 122 | log.appendChild(log_row); |
| 124 | 123 | } |
| @@ -126,14 +125,12 @@ function writeLog(stack, words) { | ||
| 126 | 125 | function resetLog() { |
| 127 | 126 | if (!log.hasChildNodes()) return; |
| 128 | 127 | |
| 129 | - let summary = h("summary"); | |
| 130 | - summary.textContent = log.firstChild.lastChild.textContent; | |
| 128 | + let summary = h("summary", log.firstChild.lastChild.textContent); | |
| 131 | 129 | |
| 132 | 130 | let old_log = log.cloneNode(true); |
| 133 | 131 | old_log.id = ''; |
| 134 | 132 | |
| 135 | - let details = h("details", summary, old_log); | |
| 136 | - details.className = "history" | |
| 133 | + let details = h("details", {class: "history"}, [summary, old_log]); | |
| 137 | 134 | |
| 138 | 135 | log.insertAdjacentElement("afterend", details); |
| 139 | 136 | log.replaceChildren(); //remove children, clear log |
Blog/Components/Pages/Concat.razor.js +7 -10
| @@ -1,4 +1,4 @@ | ||
| 1 | -import { getById, div, span, h, writeError } from "/common.module.js"; | |
| 1 | +import { getById, h, writeError } from "/common.module.js"; | |
| 2 | 2 | import lzString from "/lz-string.module.js"; |
| 3 | 3 | |
| 4 | 4 | let definitions = {}; |
| @@ -229,16 +229,15 @@ function parseString(word, stack, rest) { | ||
| 229 | 229 | |
| 230 | 230 | function writeLog(stack, words) { |
| 231 | 231 | return new Promise((resolve, _reject) => { |
| 232 | - let log_left = span(`[${stack.join(", ")}]`); | |
| 233 | - let log_right = span(words.join(" ")); | |
| 232 | + let log_left = h("span", `[${stack.join(", ")}]`); | |
| 233 | + let log_right = h("span", words.join(" ")); | |
| 234 | 234 | |
| 235 | 235 | if (words.length === 0) { |
| 236 | 236 | log_left.textContent += " <=="; |
| 237 | 237 | } |
| 238 | 238 | |
| 239 | - let log_row = div(log_left, log_right); | |
| 240 | - log_row.className = "flex-spread"; | |
| 241 | - | |
| 239 | + let log_row = h("div", {class: "flex-spread"}, [log_left, log_right]); | |
| 240 | + | |
| 242 | 241 | log.appendChild(log_row); |
| 243 | 242 | resolve(); |
| 244 | 243 | }); |
| @@ -247,14 +246,12 @@ function writeLog(stack, words) { | ||
| 247 | 246 | function resetLog() { |
| 248 | 247 | if (!log.hasChildNodes()) return; |
| 249 | 248 | |
| 250 | - let summary = h("summary"); | |
| 251 | - summary.textContent = log.firstChild.lastChild.textContent; | |
| 249 | + let summary = h("summary", log.firstChild.lastChild.textContent); | |
| 252 | 250 | |
| 253 | 251 | let old_log = log.cloneNode(true); |
| 254 | 252 | old_log.id = ""; |
| 255 | 253 | |
| 256 | - let details = h("details", summary, old_log); | |
| 257 | - details.className = "history"; | |
| 254 | + let details = h("details", {class: "history"}, [summary, old_log]); | |
| 258 | 255 | |
| 259 | 256 | log.insertAdjacentElement("afterend", details); |
| 260 | 257 | log.replaceChildren(); //remove children, clear log |
Blog/Components/Pages/Generator.razor.js +3 -3
| @@ -1,9 +1,9 @@ | ||
| 1 | -import { addClass /*writeInfo*/, div, getById, span } from "/common.module.js"; | |
| 1 | +import { getById, h } from "/common.module.js"; | |
| 2 | 2 | |
| 3 | 3 | function writeInfo(label, msg) { |
| 4 | 4 | log.appendChild( |
| 5 | - div( | |
| 6 | - addClass(div(addClass(span(label), "noselect"), span(msg)), "info"), | |
| 5 | + h("div", | |
| 6 | + | |
| 7 | 7 | ), |
| 8 | 8 | ); |
| 9 | 9 | } |
Blog/Components/Pages/Letterflixd.razor.js +4 -4
| @@ -1,4 +1,4 @@ | ||
| 1 | -import { getById, resetLog, writeError, writeInfo, writeDebug, a, span, div, addClass } from "/common.module.js" | |
| 1 | +import { getById, resetLog, writeError, writeInfo, writeDebug, h } from "/common.module.js" | |
| 2 | 2 | |
| 3 | 3 | let netflix_file_input = undefined; |
| 4 | 4 | |
| @@ -92,8 +92,8 @@ function convertText(data) { | ||
| 92 | 92 | let letterboxd_import = new Blob([letterboxd_output], { type: 'text/csv' }); |
| 93 | 93 | |
| 94 | 94 | const filename = "netflix-letterboxd-import.csv" |
| 95 | - let download_button = a(window.URL.createObjectURL(letterboxd_import), filename) | |
| 96 | - let download_text = span("Download import file ==> ") | |
| 95 | + let download_button = h("a", {href: window.URL.createObjectURL(letterboxd_import)}, filename) | |
| 96 | + let download_text = h("span", {class: "info"}, "Download import file ==> ") | |
| 97 | 97 | download_button.download = filename; |
| 98 | - log.appendChild(div(addClass(download_text, "info"), download_button)); | |
| 98 | + log.appendChild(h("div", {class:"download_button"}, [download_text])); | |
| 99 | 99 | } |
| No newline at end of file | ||
Blog/Components/Pages/Query.razor.js +13 -12
| @@ -1,4 +1,4 @@ | ||
| 1 | -import {getById, writeError, writeDebug, h, span, addClass, div} from '/common.module.js' | |
| 1 | +import {getById, writeError, writeDebug, h, t} from '/common.module.js' | |
| 2 | 2 | import {DACTAL} from "/dactal.js" |
| 3 | 3 | |
| 4 | 4 | const enterEvent = new KeyboardEvent('keydown', { |
| @@ -42,7 +42,7 @@ async function runQuery(query) { | ||
| 42 | 42 | results.replaceChildren(renderValue(result.slice(0, 100))) |
| 43 | 43 | |
| 44 | 44 | if (result.length > 100) { |
| 45 | - results.appendChild(div(span(`Not showing ${result.length - 100} more results.`))) | |
| 45 | + results.appendChild(h("div", [h("span", `Not showing ${result.length - 100} more results.`)])) | |
| 46 | 46 | } |
| 47 | 47 | } catch (e) { |
| 48 | 48 | writeError(e.message) |
| @@ -53,21 +53,22 @@ async function runQuery(query) { | ||
| 53 | 53 | function renderValue(obj) { |
| 54 | 54 | if (Array.isArray(obj)) { |
| 55 | 55 | return h("ol", |
| 56 | - ...obj.map((item) => h("li", renderValue(item))) | |
| 56 | + obj.map((item) => h("li", [renderValue(item)])) | |
| 57 | 57 | ) |
| 58 | 58 | } else if (typeof obj === "object") { |
| 59 | 59 | return h("ul", |
| 60 | - ...Object.entries(obj).map(([name, item]) => | |
| 61 | - addClass(h("li", | |
| 62 | - addClass(span(name), "name"), | |
| 63 | - span(" "), | |
| 64 | - renderValue(item)), | |
| 65 | - "dash"))) | |
| 60 | + Object.entries(obj).map(([name, item]) => | |
| 61 | + h("li", {class: "dash"}, | |
| 62 | + [ | |
| 63 | + h("span", {class: "name"}, name), | |
| 64 | + t(" "), | |
| 65 | + renderValue(item) | |
| 66 | + ]))) | |
| 66 | 67 | } else if (typeof obj === "boolean") { |
| 67 | - return addClass(span(obj), obj ? "json-true" : "json-false") | |
| 68 | + return h("span", {class: obj ? "json-true" : "json-false"}) | |
| 68 | 69 | } else if (typeof obj === "number") { |
| 69 | - return addClass(span(obj), "json-number") | |
| 70 | + return h("span", {class: "json-number"}, obj.toString()) | |
| 70 | 71 | } else if (typeof obj === "string") { |
| 71 | - return span(obj) | |
| 72 | + return h("span", obj) | |
| 72 | 73 | } |
| 73 | 74 | } |
| No newline at end of file | ||
Blog/Components/Pages/Storage.razor.js +46 -44
| @@ -1,4 +1,4 @@ | ||
| 1 | -import {getById, writeError, addClass, writeDebug, writeInfo, h, span} from "/common.module.js"; | |
| 1 | +import {getById, writeError, writeDebug, writeInfo, h } from "/common.module.js"; | |
| 2 | 2 | |
| 3 | 3 | /** @member {IDBDatabase} db */ |
| 4 | 4 | let db; |
| @@ -72,33 +72,34 @@ function showStores() { | ||
| 72 | 72 | let storeNames = db.objectStoreNames; |
| 73 | 73 | |
| 74 | 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; | |
| 75 | + const deleteButton = h("button", { | |
| 76 | + onClick: async () => { | |
| 77 | + if (await askForConfirmation(`Are you sure you want to delete store '${storeName}'?`)) { | |
| 78 | + InitDB((event) => { | |
| 79 | + writeDebug(`Deleting store: ${storeName}, version: ${event.newVersion}`); | |
| 80 | + event.target.result.deleteObjectStore(storeName); | |
| 81 | + writeDebug("Deleted store: " + storeName); | |
| 82 | + }); | |
| 83 | + | |
| 84 | + if (openButton.closeCallback) openButton.closeCallback(); | |
| 85 | + } | |
| 86 | 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(); | |
| 87 | + }, "Delete"); | |
| 88 | + | |
| 89 | + const openButton = h("button", { | |
| 90 | + onClick: () => { | |
| 91 | + if (openButton.textContent === "Open") { | |
| 92 | + openButton.closeCallback = openStore(storeName, deleteButton); | |
| 93 | + openButton.textContent = "Close"; | |
| 94 | + } else { | |
| 95 | + openButton.closeCallback(); | |
| 96 | + openButton.textContent = "Open"; | |
| 97 | + openButton.closeCallback = null; | |
| 98 | + } | |
| 98 | 99 | } |
| 99 | - }) | |
| 100 | + }, "Open"); | |
| 100 | 101 | |
| 101 | - const storeElement = span(storeName); | |
| 102 | + const storeElement = h("span", storeName); | |
| 102 | 103 | storeList.appendChild(storeElement); |
| 103 | 104 | storeList.appendChild(openButton); |
| 104 | 105 | storeList.appendChild(deleteButton); |
| @@ -109,13 +110,13 @@ function openStore(storeName) { | ||
| 109 | 110 | const openedStores = getById("openedStores"); |
| 110 | 111 | |
| 111 | 112 | const addButton = h("button", "Add"); |
| 112 | - const storeElement = addClass(h("fieldset", h("legend", storeName + " ", addButton)), "docs"); | |
| 113 | + const storeElement = h("fieldset", {class: "docs"}, [h("legend", [storeName + " ", addButton])]); | |
| 113 | 114 | const closeStore = () => { |
| 114 | 115 | openedStores.removeChild(storeElement); |
| 115 | 116 | }; |
| 116 | 117 | |
| 117 | 118 | openedStores.appendChild(storeElement); |
| 118 | - const elementList = addClass(h("div"), "grid-stores"); | |
| 119 | + const elementList = h("div", {class:"grid-stores"}); | |
| 119 | 120 | storeElement.appendChild(elementList); |
| 120 | 121 | |
| 121 | 122 | function showItems() { |
| @@ -132,32 +133,33 @@ function openStore(storeName) { | ||
| 132 | 133 | } |
| 133 | 134 | const value = cursor.value; |
| 134 | 135 | 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(); | |
| 136 | + const editItemButton = h("button", { | |
| 137 | + onClick: async () => { | |
| 138 | + const value = await getInput("Edit value", "new value"); | |
| 139 | + | |
| 140 | + if (value) { | |
| 141 | + const transaction = db.transaction(storeName, 'readwrite'); | |
| 142 | + transaction.oncomplete = () => { | |
| 143 | + showItems(); | |
| 144 | + } | |
| 145 | + const store = transaction.objectStore(storeName); | |
| 146 | + store.put(value, key); | |
| 147 | + } | |
| 142 | 148 | } |
| 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 | + }, "Edit"); | |
| 149 | 150 | |
| 150 | - if (value) { | |
| 151 | + const deleteItemButton = h("button", { | |
| 152 | + onClick: () => { | |
| 151 | 153 | const transaction = db.transaction(storeName, 'readwrite'); |
| 152 | 154 | transaction.oncomplete = () => { |
| 153 | 155 | showItems(); |
| 154 | 156 | } |
| 155 | 157 | const store = transaction.objectStore(storeName); |
| 156 | - store.put(value, key); | |
| 158 | + store.delete(key); | |
| 157 | 159 | } |
| 158 | - }) | |
| 160 | + }, "Delete"); | |
| 159 | 161 | |
| 160 | - newChildren.push(span(value)); | |
| 162 | + newChildren.push(h("span", value)); | |
| 161 | 163 | newChildren.push(editItemButton); |
| 162 | 164 | newChildren.push(deleteItemButton); |
| 163 | 165 | |
Blog/wwwroot/Blog.lib.module.js +1 -1
| @@ -32,7 +32,7 @@ function unregisterPageScriptElement(src) { | ||
| 32 | 32 | |
| 33 | 33 | async function initializePageScriptModule(src, pageScriptInfo) { |
| 34 | 34 | if (src.startsWith("./")) { |
| 35 | - src = new URL(src.substr(2), document.baseURI).toString(); | |
| 35 | + src = new URL(src.substring(2), document.baseURI).toString(); | |
| 36 | 36 | } |
| 37 | 37 | |
| 38 | 38 | const module = await import(src); |
Blog/wwwroot/app.css +1 -0
| @@ -145,6 +145,7 @@ label { | ||
| 145 | 145 | display: block; |
| 146 | 146 | } |
| 147 | 147 | |
| 148 | +.textarea, | |
| 148 | 149 | textarea { |
| 149 | 150 | width: 100%; |
| 150 | 151 | display: block; |
Blog/wwwroot/common.module.js +211 -89
| @@ -1,45 +1,221 @@ | ||
| 1 | -function getById(id) { | |
| 2 | - let element = document.getElementById(id); | |
| 3 | - if (element) { | |
| 4 | - return element; | |
| 5 | - } else { | |
| 6 | - throw `Element with id '${id}' not found.`; | |
| 7 | - } | |
| 1 | +/** | |
| 2 | + * Create a text node with the contents | |
| 3 | + * @param {string} text The text content | |
| 4 | + * @returns The text node | |
| 5 | + */ | |
| 6 | +export function t(text) { | |
| 7 | + return document.createTextNode(text); | |
| 8 | 8 | } |
| 9 | 9 | |
| 10 | 10 | /** |
| 11 | - * @param {string} href | |
| 12 | - * @returns {HTMLAnchorElement} | |
| 11 | + * @typedef {"stop"|"prevent"|"self"} EventModifier | |
| 13 | 12 | */ |
| 14 | -function a(href, pageName) { | |
| 15 | - const aElem = document.createElement("a"); | |
| 16 | - aElem.href = href; | |
| 17 | - aElem.text = pageName; | |
| 18 | - return aElem; | |
| 19 | -} | |
| 20 | 13 | |
| 21 | -function div(...children) { | |
| 22 | - const divElem = document.createElement("div"); | |
| 23 | - divElem.replaceChildren(...children); | |
| 24 | - return divElem; | |
| 14 | +/** | |
| 15 | + * | |
| 16 | + * @param {CallableFunction} fn | |
| 17 | + * @param {EventModifier|EventModifier[]} modifiers | |
| 18 | + * @returns {CallableFunction} | |
| 19 | + */ | |
| 20 | +export function withModifiers(fn, modifiers) { | |
| 21 | + if (!Array.isArray(modifiers)) { | |
| 22 | + modifiers = [modifiers]; | |
| 23 | + } | |
| 24 | + return function (/** @type {Event} */ event , /** @type {any} */ ...args) { | |
| 25 | + for (const modifier of modifiers) { | |
| 26 | + if (modifier === "stop") { | |
| 27 | + event.stopPropagation(); | |
| 28 | + } else if (modifier === "prevent") { | |
| 29 | + event.preventDefault(); | |
| 30 | + } else if (modifier === "self") { | |
| 31 | + if (event.target !== event.currentTarget) { | |
| 32 | + return; | |
| 33 | + } | |
| 34 | + } | |
| 35 | + } | |
| 36 | + return fn(event, ...args); | |
| 37 | + } | |
| 25 | 38 | } |
| 26 | 39 | |
| 27 | -function span(text) { | |
| 28 | - const spanElem = document.createElement("span"); | |
| 29 | - spanElem.textContent = text; | |
| 30 | - return spanElem; | |
| 40 | +/** | |
| 41 | + * @typedef {{ style?: Record<string, string>|Record<string, string>[], class?: Record<string, unknown> } & Record<string, string>} HtmlAttrs | |
| 42 | + */ | |
| 43 | + | |
| 44 | +/** | |
| 45 | + * Assign attributes to the node | |
| 46 | + * @param {HTMLElement} node | |
| 47 | + * @param {HtmlAttrs} attrs | |
| 48 | + */ | |
| 49 | +function setAttrs(node, attrs) { | |
| 50 | + for (const key in attrs) { | |
| 51 | + if (!Object.hasOwn(attrs, key)) continue; | |
| 52 | + if (key === "style" && typeof attrs[key] === "object") { | |
| 53 | + let styleAttrs = attrs[key]; | |
| 54 | + if (!Array.isArray(styleAttrs)) { | |
| 55 | + styleAttrs = [styleAttrs]; | |
| 56 | + } | |
| 57 | + for (const styles of styleAttrs) { | |
| 58 | + for (const styleProp in styles) { | |
| 59 | + if (!Object.hasOwn(styles, styleProp)) continue; | |
| 60 | + | |
| 61 | + const val = styles[styleProp]; | |
| 62 | + | |
| 63 | + node.style.setProperty(styleProp, val); | |
| 64 | + } | |
| 65 | + } | |
| 66 | + } else if (key === "class" && typeof attrs[key] === "object" && !Array.isArray(attrs[key])) { | |
| 67 | + for (const className in attrs[key]) { | |
| 68 | + if (!Object.hasOwn(attrs[key], className)) continue; | |
| 69 | + | |
| 70 | + const element = attrs[key][className]; | |
| 71 | + | |
| 72 | + if (element) { | |
| 73 | + node.classList.add(className); | |
| 74 | + } | |
| 75 | + } | |
| 76 | + } else if (typeof attrs[key] === "string") { | |
| 77 | + node.setAttribute(key, attrs[key]); | |
| 78 | + } else if (typeof attrs[key] === "function" && /^on[A-Z]/.test(key)) { | |
| 79 | + const eventSpec = key.substring(2); | |
| 80 | + // Loop through PascalCase substrings | |
| 81 | + const re = /[A-Z][a-z]+/g; | |
| 82 | + const results = [...eventSpec.matchAll(re)]; | |
| 83 | + if (results.length === 0) { | |
| 84 | + throw new Error(`Invalid event listener: on${eventSpec}`); | |
| 85 | + } | |
| 86 | + let eventName = results[0][0].toLowerCase(); | |
| 87 | + let options = /** @type {AddEventListenerOptions} */ ({}); | |
| 88 | + for (const regexResult of results.slice(1)) { | |
| 89 | + let modifier = regexResult[0].toLowerCase(); | |
| 90 | + if (modifier === "capture") { | |
| 91 | + options.capture = true; | |
| 92 | + } else if (modifier === "once") { | |
| 93 | + options.once = true; | |
| 94 | + } else if (modifier === "passive") { | |
| 95 | + options.passive = true; | |
| 96 | + } else { | |
| 97 | + throw new Error(`Unknown modifier: ${modifier}`); | |
| 98 | + } | |
| 99 | + } | |
| 100 | + node.addEventListener(eventName, attrs[key], options); | |
| 101 | + } else { | |
| 102 | + throw new TypeError(`Unsupported attribute type ${typeof attrs[key]}`); | |
| 103 | + } | |
| 104 | + } | |
| 31 | 105 | } |
| 32 | 106 | |
| 33 | -function h(elementTag, ...children) { | |
| 34 | - const elem = document.createElement(elementTag); | |
| 35 | - elem.replaceChildren(...children); | |
| 36 | - return elem; | |
| 107 | +/** | |
| 108 | + * Add the children to the node | |
| 109 | + * @param {HTMLElement} node | |
| 110 | + * @param {Array<HTMLElement|Text|string>} children | |
| 111 | + */ | |
| 112 | +function setChildren(node, children) { | |
| 113 | + for (const c of children) { | |
| 114 | + if (typeof c === "string") { | |
| 115 | + node.appendChild(t(c)); | |
| 116 | + } else { | |
| 117 | + node.appendChild(c); | |
| 118 | + } | |
| 119 | + } | |
| 37 | 120 | } |
| 38 | 121 | |
| 39 | -/** @param {HTMLElement} element */ | |
| 40 | -function addClass(element, newClass) { | |
| 41 | - element.classList.add(newClass); | |
| 42 | - return element; | |
| 122 | +/** | |
| 123 | + * Build a HTMLElement | |
| 124 | + * @overload | |
| 125 | + * @param {keyof HTMLElementTagNameMap} tag | |
| 126 | + * @returns {HTMLElement} | |
| 127 | + */ | |
| 128 | +/** | |
| 129 | + * @overload | |
| 130 | + * @param {keyof HTMLElementTagNameMap} tag | |
| 131 | + * @param {string} text | |
| 132 | + * @returns {HTMLElement} | |
| 133 | + */ | |
| 134 | +/** | |
| 135 | + * @overload | |
| 136 | + * @param {keyof HTMLElementTagNameMap} tag | |
| 137 | + * @param {Array<HTMLElement|Text|string>} children | |
| 138 | + * @returns {HTMLElement} | |
| 139 | + */ | |
| 140 | +/** | |
| 141 | + * @overload | |
| 142 | + * @param {keyof HTMLElementTagNameMap} tag | |
| 143 | + * @param {HtmlAttrs} attrs | |
| 144 | + * @returns {HTMLElement} | |
| 145 | + */ | |
| 146 | +/** | |
| 147 | + * @overload | |
| 148 | + * @param {keyof HTMLElementTagNameMap} tag | |
| 149 | + * @param {HtmlAttrs} attrs | |
| 150 | + * @param {string} text | |
| 151 | + * @returns {HTMLElement} | |
| 152 | + */ | |
| 153 | +/** | |
| 154 | + * @overload | |
| 155 | + * @param {keyof HTMLElementTagNameMap} tag | |
| 156 | + * @param {HtmlAttrs} attrs | |
| 157 | + * @param {Array<HTMLElement|Text|string>} children | |
| 158 | + * @returns {HTMLElement} | |
| 159 | + */ | |
| 160 | +/** | |
| 161 | + * @param {keyof HTMLElementTagNameMap} tag | |
| 162 | + * @param {...(HtmlAttrs|Array<HTMLElement|Text|string>|string)} args | |
| 163 | + * @returns {HTMLElement} | |
| 164 | + */ | |
| 165 | +export function h(tag, ...args) { | |
| 166 | + let node = document.createElement(tag); | |
| 167 | + | |
| 168 | + if (args.length === 0) { | |
| 169 | + return node; | |
| 170 | + } else if (args.length === 1) { | |
| 171 | + const attrs = args[0]; | |
| 172 | + if (typeof attrs === "object" && Array.isArray(attrs)) { | |
| 173 | + // Children | |
| 174 | + setChildren(node, attrs); | |
| 175 | + return node; | |
| 176 | + } else if (typeof attrs === "object") { | |
| 177 | + // Attributes | |
| 178 | + setAttrs(node, attrs); | |
| 179 | + return node; | |
| 180 | + } else if (typeof attrs === "string") { | |
| 181 | + // Text node child | |
| 182 | + const textNode = document.createTextNode(attrs); | |
| 183 | + node.appendChild(textNode); | |
| 184 | + return node; | |
| 185 | + } else { | |
| 186 | + throw new TypeError(`Invalid second argument type: ${typeof attrs}`); | |
| 187 | + } | |
| 188 | + } else if (args.length === 2) { | |
| 189 | + // Second arg must be attributes, third arg must be children | |
| 190 | + const attrs = args[0]; | |
| 191 | + const children = args[1]; | |
| 192 | + | |
| 193 | + if (typeof attrs !== "object" || Array.isArray(attrs)) { | |
| 194 | + throw new TypeError("Second argument must be an object"); | |
| 195 | + } | |
| 196 | + | |
| 197 | + setAttrs(node, attrs); | |
| 198 | + | |
| 199 | + if (typeof children === "string") { | |
| 200 | + node.appendChild(t(children)); | |
| 201 | + } else if (typeof children === "object" && Array.isArray(children)) { | |
| 202 | + setChildren(node, children); | |
| 203 | + } else { | |
| 204 | + throw new TypeError("Third argument must be an array or string"); | |
| 205 | + } | |
| 206 | + | |
| 207 | + return node; | |
| 208 | + } else { | |
| 209 | + throw new Error("Too many arguments"); | |
| 210 | + } | |
| 211 | +} | |
| 212 | +function getById(id) { | |
| 213 | + let element = document.getElementById(id); | |
| 214 | + if (element) { | |
| 215 | + return element; | |
| 216 | + } else { | |
| 217 | + throw `Element with id '${id}' not found.`; | |
| 218 | + } | |
| 43 | 219 | } |
| 44 | 220 | |
| 45 | 221 | function writeError(error) { |
| @@ -56,9 +232,11 @@ function writeDebug(msg) { | ||
| 56 | 232 | |
| 57 | 233 | function writeLog(msg, className) { |
| 58 | 234 | if (typeof msg === "string" || msg instanceof String) { |
| 59 | - log.appendChild(div(addClass(span(msg), className))); | |
| 235 | + // log.appendChild(div(addClass(span(msg), className))); | |
| 236 | + log.appendChild(h("div", {class: className}, [h("span", msg)])); | |
| 60 | 237 | } else { |
| 61 | - log.appendChild(div(addClass(msg, className))); | |
| 238 | + log.appendChild(h("div", {class: className}, msg)); | |
| 239 | + // log.appendChild(div(addClass(msg, className))); | |
| 62 | 240 | } |
| 63 | 241 | } |
| 64 | 242 | |
| @@ -80,64 +258,8 @@ function debounce(callback, wait) { | ||
| 80 | 258 | }; |
| 81 | 259 | } |
| 82 | 260 | |
| 83 | -const stylesheet = new CSSStyleSheet(); | |
| 84 | -stylesheet.replaceSync(` | |
| 85 | - a { | |
| 86 | - color: var(--color); | |
| 87 | - } | |
| 88 | - nav > ul { | |
| 89 | - list-style-type: none; | |
| 90 | - padding-inline-start: 0; | |
| 91 | - } | |
| 92 | - | |
| 93 | - nav > ul > li { | |
| 94 | - display: inline-block; | |
| 95 | - } | |
| 96 | - | |
| 97 | - nav > ul > li > a::after { | |
| 98 | - display: inline-block; | |
| 99 | - color: var(--color); | |
| 100 | - content: ">"; | |
| 101 | - padding: 0 1em; | |
| 102 | - } | |
| 103 | -`); | |
| 104 | - | |
| 105 | -customElements.define("mb-nav", | |
| 106 | - class MBNav extends HTMLElement { | |
| 107 | - constructor() { | |
| 108 | - super(); | |
| 109 | - } | |
| 110 | - | |
| 111 | - get pagename() { | |
| 112 | - return this.getAttribute("pagename"); | |
| 113 | - } | |
| 114 | - | |
| 115 | - set pagename(value) { | |
| 116 | - this.setAttribute("pagename", value); | |
| 117 | - } | |
| 118 | - | |
| 119 | - connectedCallback() { | |
| 120 | - const shadow = this.attachShadow({ mode: "open" }); | |
| 121 | - shadow.adoptedStyleSheets = [stylesheet]; | |
| 122 | - | |
| 123 | - shadow.appendChild( | |
| 124 | - h("nav", | |
| 125 | - h("ul", | |
| 126 | - h("li", a("/", "MB.bes.is")), | |
| 127 | - h("li", span(this.pagename)), | |
| 128 | - ) | |
| 129 | - ) | |
| 130 | - ) | |
| 131 | - } | |
| 132 | - }); | |
| 133 | - | |
| 134 | 261 | export { |
| 135 | 262 | getById, |
| 136 | - a, | |
| 137 | - div, | |
| 138 | - span, | |
| 139 | - h, | |
| 140 | - addClass, | |
| 141 | 263 | writeError, |
| 142 | 264 | writeInfo, |
| 143 | 265 | writeDebug, |