1
Fork 0
mirror of https://github.com/glassflame/glassflame.github.io.git synced 2024-10-16 06:27:32 +00:00

Complete refactoring?

This commit is contained in:
Steffo 2023-10-27 02:44:49 +02:00
parent 610c849264
commit 803b61f434
Signed by: steffo
GPG key ID: 2A24051445686895
12 changed files with 252 additions and 116 deletions

View file

@ -24,12 +24,15 @@
--color-blue: #53DFDD;
--color-purple: #A882FF;
--color-node: var(--color-gray);
--edge-width: 2px;
--node-group-border-width: 2px;
--node-file-border-width: 2px;
}
body {
margin: 0;
padding: 128px;
width: max-content;
height: max-content;
@ -37,11 +40,18 @@
background-color: var(--color-background);
color: var(--color-foreground);
}
svg line {
stroke: var(--color-node);
stroke-width: var(--edge-width);
}
</style>
<!-- Templates -->
<template id="template-vault">
<style>
.vault {
}
</style>
<div class="vault">
<slot name="vault-child"></slot>
@ -57,19 +67,6 @@
width: max-content;
height: max-content;
}
.canvas * {
left: 0;
top: 0;
}
.canvas *:first-child {
position: relative;
}
.canvas *:not(:first-child) {
position: absolute;
}
</style>
<div class="canvas">
<slot name="canvas-edges">{Canvas edges}</slot>
@ -159,18 +156,19 @@
</template>
<template id="template-edge">
<style>
.edge svg line {
stroke: var(--color-node);
stroke-width: var(--edge-width);
.edge {
}
</style>
<div class="edge">
<div class="canvas-item edge">
<slot name="edge-svg">{Edge SVG}</slot>
</div>
</template>
<template id="template-markdown">
<style>
.markdown
.markdown {
}
</style>
<div class="markdown">
<slot name="markdown-document">{Markdown text}</slot>
@ -203,9 +201,8 @@
cursor: pointer;
}
</style>
<a class="wikilink"><slot name="wikilink-anchor">{Wikilink text}</slot></a>
<a class="wikilink"></a>
</template>
</head>
<body>
</body>
<body is="x-browse"></body>
</html>

View file

@ -1,11 +0,0 @@
export function configFromURL(location) {
const queryString = new URLSearchParams(location.search)
const vref = queryString.get("vref")
const wref = queryString.get("wref")
return {vref, wref}
}
export function configFromWindow() {
return configFromURL(window.location)
}

64
src/elements/browse.mjs Normal file
View file

@ -0,0 +1,64 @@
/**
* The body element for the pages viewer, handling most low-level things.
*/
export class BrowseElement extends HTMLBodyElement {
/**
* Parameters to be used to display *things*.
* @type {{vault: string, path: string, highlight: string}}
*/
parameters
/**
* Recalculate the value of {@link parameters} using the current {@link window.location}.
* @returns {void}
*/
recalculateParameters() {
const location = window.location
const params = new URLSearchParams(location.search)
const vault = params.get("vault")
const path = params.get("path")
const highlight = location.hash.replace(/^#/, "")
this.parameters = {vault, path, highlight}
}
// TODO: Add a landing page
/**
* The vault element, describing to its descendants how to handle various situations.
* @type {VaultElement}
*/
vaultElement
/**
* The display element showing the contents of the specified file.
* @type {DisplayElement}
*/
rootDisplayElement
/**
* Recreate all contents of this element to match the current value of {@link parameters}.
*/
recreateContents() {
if(this.vaultElement) {
this.vaultElement.remove()
this.vaultElement = null
this.rootDisplayElement = null
}
this.vaultElement = document.createElement("x-vault")
this.vaultElement.base = this.parameters.vault
this.vaultElement.cooldownMs = 200
this.rootDisplayElement = document.createElement("x-display")
this.rootDisplayElement.path = this.parameters.path
this.rootDisplayElement.slot = "vault-child"
this.vaultElement.appendChild(this.rootDisplayElement)
this.appendChild(this.vaultElement)
}
connectedCallback() {
this.recalculateParameters()
this.recreateContents()
}
}

View file

@ -40,10 +40,10 @@ export class CanvasElement extends CustomElement {
/**
* Update the values of {@link document} and {@link parsedDocument} from the `document` attribute of the element.
* @throws SyntaxError If `contents` is not valid JSON.
* @throws SyntaxError If `document` is not valid JSON.
*/
recalculateContents() {
this.#document = this.getAttribute("contents")
reparseDocument() {
this.#document = this.getAttribute("document")
this.#parsedDocument = JSON.parse(this.#document)
}
@ -155,31 +155,28 @@ export class CanvasElement extends CustomElement {
this.nodesContainer.slot = this.constructor.NODES_SLOT_NAME
for(const node of this.parsedDocument["nodes"]) {
let {id, type, color, x, y, width, height} = node
x, y, width, height = Number(x), Number(y), Number(width), Number(height)
const {id, type, color, x, y, width, height} = node
const [nodeX, nodeY, nodeWidth, nodeHeight] = [Number(x), Number(y), Number(width), Number(height)]
const element = document.createElement(`${this.constructor.NODE_ELEMENT_NAME_PREFIX}${type}`)
element.setAttribute("id", `node-${id}`)
element.setAttribute("x", `${x - this.minX.x}`)
element.setAttribute("y", `${y - this.minY.y}`)
element.setAttribute("width", `${width}`)
element.setAttribute("height", `${height}`)
if(color) element.setAttribute("color", color)
element.x = nodeX - this.minX.x
element.y = nodeY - this.minY.y
element.width = nodeWidth
element.height = nodeHeight
if(color) element.obsidianColor = color
this.nodeElementsById[id] = element
switch(type) {
case "text":
const {text} = node
element.setAttribute("text", text)
element.setAttribute("document", text)
break
case "file":
const {file} = node
const {name} = fileDetails(file)
element.setAttribute("file", file)
element.setAttribute("file-name", name)
element.setAttribute("path", file)
this.nodeElementsByPath[file] = element
this.nodeElementsByName[name] = element
break
@ -199,6 +196,7 @@ export class CanvasElement extends CustomElement {
this.nodesContainer.style["width"] = `${this.maxX.x + this.maxX.width - this.minX.x}px`
this.nodesContainer.style["height"] = `${this.maxY.y + this.maxY.height - this.minY.y}px`
this.nodesContainer.style["position"] = "relative"
this.appendChild(this.nodesContainer)
}
@ -219,7 +217,7 @@ export class CanvasElement extends CustomElement {
* Name of the slot where the edge container should be placed.
* @type {string}
*/
static EDGES_SLOT_NAME = "canvas-nodes"
static EDGES_SLOT_NAME = "canvas-edges"
/**
* Prefix to the name of the element to create for each edge.
@ -258,13 +256,14 @@ export class CanvasElement extends CustomElement {
this.edgesContainer.style["width"] = `${this.maxX.x + this.maxX.width - this.minX.x}px`
this.edgesContainer.style["height"] = `${this.maxY.y + this.maxY.height - this.minY.y}px`
this.edgesContainer.style["position"] = "absolute"
this.appendChild(this.edgesContainer)
}
onConnect() {
super.onConnect()
this.recalculateContents()
this.reparseDocument()
this.recalculateMinMax()
this.recreateNodes()
this.recreateEdges()

View file

@ -1,4 +1,5 @@
import { CustomElement } from "../base.mjs";
import {MalformedError} from "../../utils/errors.mjs";
/**
@ -12,6 +13,9 @@ export class CanvasItemElement extends CustomElement {
get x() {
return Number(this.getAttribute("x"))
}
set x(value) {
this.setAttribute("x", value.toString())
}
/**
* @returns {number} The Y coordinate of the top left vertex of this element.
@ -19,6 +23,9 @@ export class CanvasItemElement extends CustomElement {
get y() {
return Number(this.getAttribute("y"))
}
set y(value) {
this.setAttribute("y", value.toString())
}
/**
* @returns {number} The horizontal width of this element.
@ -26,6 +33,9 @@ export class CanvasItemElement extends CustomElement {
get width() {
return Number(this.getAttribute("width"))
}
set width(value) {
this.setAttribute("width", value.toString())
}
/**
* @returns {number} The vertical height of this element.
@ -33,6 +43,9 @@ export class CanvasItemElement extends CustomElement {
get height() {
return Number(this.getAttribute("height"))
}
set height(value) {
this.setAttribute("height", value.toString())
}
/**
* The color of this element, as stored in Obsidian Canvas files.
@ -48,6 +61,14 @@ export class CanvasItemElement extends CustomElement {
return color // Hex color specified
}
set obsidianColor(value) {
if(value === null) {
this.removeAttribute("color")
}
else {
this.setAttribute("color", value.toString())
}
}
/**
* Given an Obsidian Canvas color, return its corresponding CSS color.
@ -58,18 +79,23 @@ export class CanvasItemElement extends CustomElement {
if(color === null || color === "") {
return "var(--color-gray)"
}
else if(color.match(/^#[0-9A-F]{3}$|^#[0-9A-F]{6}$/i)) {
return color
else if(typeof color === "string") {
if(color.match(/^#[0-9A-F]{3}$|^#[0-9A-F]{6}$/i)) {
return color
}
else {
throw new MalformedError("String obisidianColor is not an hex code.")
}
}
else {
return {
"0": "var(--color-gray)",
"1": "var(--color-red)",
"2": "var(--color-orange)",
"3": "var(--color-yellow)",
"4": "var(--color-green)",
"5": "var(--color-blue)",
"6": "var(--color-purple)",
0: "var(--color-gray)",
1: "var(--color-red)",
2: "var(--color-orange)",
3: "var(--color-yellow)",
4: "var(--color-green)",
5: "var(--color-blue)",
6: "var(--color-purple)",
}[color]
}
}
@ -80,6 +106,14 @@ export class CanvasItemElement extends CustomElement {
get cssColor() {
return this.constructor.obsidianColorToCssColor(this.obsidianColor)
}
set cssColor(value) {
if(value === null) {
this.removeAttribute("color")
}
else {
this.setAttribute("color", value)
}
}
/**
* The CSS selector of the element in the template representing the canvas item.
@ -108,11 +142,11 @@ export class CanvasItemElement extends CustomElement {
resetCanvasItemCssProperties() {
this.canvasItemElement.style.setProperty("box-sizing", "border-box")
this.canvasItemElement.style.setProperty("position", "absolute")
this.canvasItemElement.style.setProperty("left", `${this.getAttribute("x")}px`)
this.canvasItemElement.style.setProperty("top", `${this.getAttribute("y")}px`)
this.canvasItemElement.style.setProperty("width", `${this.getAttribute("width")}px`)
this.canvasItemElement.style.setProperty("height", `${this.getAttribute("height")}px`)
this.canvasItemElement.style.setProperty("--color-node", this.constructor.obsidianColorToCssColor(this.getAttribute("color")))
this.canvasItemElement.style.setProperty("left", `${this.x}px`)
this.canvasItemElement.style.setProperty("top", `${this.y}px`)
this.canvasItemElement.style.setProperty("width", `${this.width}px`)
this.canvasItemElement.style.setProperty("height", `${this.height}px`)
this.canvasItemElement.style.setProperty("--color-node", this.cssColor)
}
onConnect() {

View file

@ -76,8 +76,7 @@ export class NodeFileElement extends NodeElement {
this.contentsElement = document.createElement("x-display")
this.contentsElement.slot = this.constructor.CONTENTS_ELEMENT_SLOT
this.contentsElement.setAttribute("vault", findFirstAncestor(this, DisplayElement).vault) // TODO: Add a vault attribute to DisplayElement
this.contentsElement.setAttribute("path", this.pathRelativeToVault) // TODO: Add a path attribute to DisplayElement
this.contentsElement.path = this.pathRelativeToVault
this.appendChild(this.contentsElement)
}

View file

@ -30,8 +30,11 @@ export class DisplayElement extends CustomElement {
* Get the path or name of the file this node points to.
* @returns {string} The value in question.
*/
get target() {
return this.getAttribute("target")
get path() {
return this.getAttribute("path")
}
set path(value) {
return this.setAttribute("path", value)
}
/**
@ -84,7 +87,7 @@ export class DisplayElement extends CustomElement {
this.contentsElement = null
}
const {extension} = fileDetails(this.target)
const {extension} = fileDetails(this.path)
switch(extension) {
case "md":
@ -100,30 +103,30 @@ export class DisplayElement extends CustomElement {
this.contentsElement.setAttribute("document", this.document)
this.contentsElement.slot = this.constructor.CONTAINER_ELEMENT_SLOT
this.appendChild(this.loadingElement)
this.appendChild(this.contentsElement)
}
/**
* The plaintext contents of the {@link target} document.
* The plaintext contents of the {@link path} document.
* @type {string}
*/
document
/**
* Reload the {@link target} {@link document}.
* Reload the {@link path} {@link document}.
* @returns {Promise<void>}
*/
async reloadDocument() {
const response = await this.vault.fetchCooldown(this.target)
const response = await this.vault.fetchCooldown(this.path)
// TODO: Add a check that the request was successful
this.document = await response.text()
}
onConnect() {
async onConnect() {
super.onConnect()
this.recalculateVault()
this.recreateLoadingElement()
// noinspection JSIgnoredPromiseFromCall
this.reloadDocument().then(this.recreateContentsElement)
await this.reloadDocument()
this.recreateContentsElement()
}
}

View file

@ -2,3 +2,4 @@ export {CanvasElement, NodeFileElement, NodeGroupElement, NodeTextElement, EdgeE
export {MarkdownElement, HashtagElement, WikilinkElement, FrontMatterElement} from "./markdown/index.mjs"
export {DisplayElement} from "./display.mjs"
export {VaultElement} from "./vault.mjs"
export {BrowseElement} from "./browse.mjs"

View file

@ -6,7 +6,7 @@ import { CustomElement } from "../base.mjs";
*/
export class WikilinkElement extends CustomElement {
static get template() {
return document.getElementById("template-hashtag")
return document.getElementById("template-wikilink")
}
/**
@ -22,9 +22,21 @@ export class WikilinkElement extends CustomElement {
* @returns {string} The text in question.
*/
get text() {
return this.getAttribute("text") ?? this.target
// TODO: Dirty hack to hide "undefined"
const text = this.getAttribute("text")
// noinspection EqualityComparisonWithCoercionJS
if(text == "undefined") {
return this.target
}
return text
}
/**
* The CSS selector of the anchor element.
* @type {string}
*/
static ANCHOR_SELECTOR = "a.wikilink"
/**
* The element displaying the wikilink.
* Can be recreated with {@link recreateTagElement}.
@ -33,30 +45,21 @@ export class WikilinkElement extends CustomElement {
anchorElement
/**
* The name of the slot where {@link anchorElement} should be placed in.
* @type {string}
*/
static ANCHOR_ELEMENT_SLOT = "wikilink-anchor"
/**
* Recreate {@link anchorElement} with the current value of {@link target} and {@link text}.
Update the value of the {@link canvasItemElement} by querying the current {@link instance} with {@link ANCHOR_SELECTOR}.
* @returns {void}
*/
recreateTagElement() {
if(this.anchorElement) {
this.anchorElement.remove()
this.anchorElement = null
}
recalculateAnchorElement() {
this.anchorElement = this.instance.querySelector(this.constructor.ANCHOR_SELECTOR)
}
this.anchorElement = document.createElement("a")
this.anchorElement.slot = this.constructor.ANCHOR_ELEMENT_SLOT
this.anchorElement.href = "#" // TODO: Add href behaviour to the anchor.
resetAnchorElementProperties() {
this.anchorElement.href = this.target
this.anchorElement.innerText = this.text
this.appendChild(this.anchorElement)
}
onConnect() {
super.onConnect()
this.recreateTagElement()
this.recalculateAnchorElement()
this.resetAnchorElementProperties()
}
}

View file

@ -17,6 +17,9 @@ export class VaultElement extends CustomElement {
get base() {
return this.getAttribute("base")
}
set base(value) {
this.setAttribute("base", value)
}
/**
* {@link fetch} the file at the given path ignoring cooldowns.
@ -32,33 +35,59 @@ export class VaultElement extends CustomElement {
* Cooldown between two {@link fetchCooldown} requests in milliseconds, as obtained from the `cooldown` parameter.
*/
get cooldownMs() {
return Number(this.getAttribute("cooldown"))
return Number(this.getAttribute("cooldown") ?? 5000)
}
set cooldownMs(value) {
this.setAttribute("cooldown", value.toString())
}
/**
* Queue containing the `resolve` functions necessary to make the calls to {@link fetchCooldown} proceed beyond the waiting phase.
* To be called by the preceding {@link fetchCooldown} if possible.
* @type {((v: unknown) => void)[]}
* FIFO queue of {@link fetch} promises to be awaited, with a {@link cooldownMs} pause between each of them.
* @type {((v: undefined) => void)[]}
*/
#fetchQueue = []
/**
* Promise that can be called to resume {@link #fetchQueueScheduler} if {@link #fetchQueue} is no longer empty.
* @type {((v: undefined) => void)|null}
*/
#somethingInFetchQueue
/**
* @returns {Promise<void>} A Promise that will wait for this caller's turn in the {@link #fetchQueue}.
*/
fetchQueueTurn() {
async fetchQueueTurn() {
return new Promise(resolve => {
this.#fetchQueue.push(resolve)
console.debug("[Fetch queue] Waiting for my turn...")
if(this.#somethingInFetchQueue !== null) {
console.debug("[Fetch queue] Asking scheduler to resume...")
this.#somethingInFetchQueue(undefined)
this.#somethingInFetchQueue = null
}
})
}
/**
* A promise that will advance the fetch queue after {@link cooldownMs}.
* Resolves promises in the {@link #fetchQueue} with a cooldown.
* @returns {Promise<void>}
*/
async #scheduleNextFetchQueueTurn() {
await sleep(this.cooldownMs)
const resolve = this.#fetchQueue.shift()
resolve()
async #fetchQueueScheduler() {
while(this.isConnected) {
console.debug("[Fetch scheduler] Scheduler running one iteration...")
if(this.#fetchQueue.length === 0) {
const somethingInFetchQueue = new Promise(resolve => {
this.#somethingInFetchQueue = resolve
console.debug("[Fetch scheduler] Nothing to do, waiting...")
})
await somethingInFetchQueue
}
const promise = this.#fetchQueue.shift()
console.debug("[Fetch scheduler] Advancing...")
promise()
console.debug("[Fetch scheduler] Cooling down for:", this.cooldownMs)
await sleep(this.cooldownMs)
}
}
/**
@ -67,20 +96,19 @@ export class VaultElement extends CustomElement {
* @returns {Promise<Response>} The resulting HTTP response.
*/
async fetchCooldown(path) {
// Sit waiting in queue
if(this.#fetchQueue.length > 0) {
await this.fetchQueueTurn()
}
// Await for this request's turn in the fetchQueue
await this.fetchQueueTurn()
// Perform the request
const result = await this.fetchImmediately(path)
// Start the next item in queue
// noinspection ES6MissingAwait
this.#scheduleNextFetchQueueTurn()
// Return the request's result
return result
}
onConnect() {
super.onConnect()
// noinspection JSIgnoredPromiseFromCall
this.#fetchQueueScheduler()
}
}

View file

@ -1,12 +1,26 @@
import { CanvasElement, HashtagElement, NodeFileElement, WikilinkElement, DisplayElement, EdgeElement, NodeGroupElement, NodeTextElement, FrontMatterElement, MarkdownElement } from "./elements/index.mjs";
import {
CanvasElement,
HashtagElement,
NodeFileElement,
WikilinkElement,
DisplayElement,
EdgeElement,
NodeGroupElement,
NodeTextElement,
FrontMatterElement,
MarkdownElement,
VaultElement, BrowseElement
} from "./elements/index.mjs";
customElements.define("x-vault", VaultElement)
customElements.define("x-display", DisplayElement)
customElements.define("x-canvas", CanvasElement)
customElements.define("x-node-group", NodeGroupElement)
customElements.define("x-node-file", NodeFileElement)
customElements.define("x-node-text", NodeTextElement)
customElements.define("x-node-group", NodeGroupElement)
customElements.define("x-edge", EdgeElement)
customElements.define("x-markdown", MarkdownElement)
customElements.define("x-frontmatter", FrontMatterElement)
customElements.define("x-wikilink", WikilinkElement)
customElements.define("x-hashtag", HashtagElement)
customElements.define("x-canvas", CanvasElement)
customElements.define("x-display", DisplayElement)
customElements.define("x-edge", EdgeElement)
customElements.define("x-wikilink", WikilinkElement)
customElements.define("x-browse", BrowseElement, {extends: "body"})

View file

@ -21,3 +21,8 @@ export class FetchError extends Error {
this.response = response
}
}
/**
* A file is maliciously malformed.
*/
export class MalformedError extends Error {}