1
Fork 0
mirror of https://github.com/glassflame/glassflame.github.io.git synced 2024-11-22 08:04:27 +00:00

WIP refactor of CanvasElement

This commit is contained in:
Steffo 2023-10-24 15:47:53 +02:00
parent fd8ea9c494
commit a1a8b89a2b
10 changed files with 243 additions and 104 deletions

View file

@ -1,64 +1,61 @@
export class NotImplementedError extends Error {}
import { NotImplementedError } from "../utils/errors.mjs";
/**
* Abstract base utility class to simplify the construction of custom elements.
* @abstract Implementors must override {@link template}.
*/
export class CustomElement extends HTMLElement {
template
shadow
instance
// noinspection JSUnusedGlobalSymbols
connectedCallback() {
// The template to duplicate.
this.template = this.constructor.getTemplate()
// The shadow root, the inner contents of the element..
this.shadow = this.attachShadow({ mode: "open" })
// The element contained inside the shadow root..
this.instance = this.template.content.cloneNode(true)
// Call the custom callback.
this.onConnected()
// Add the instance to the DOM.
this.shadow.appendChild(this.instance)
}
findFirstAncestor(constructor) {
let current = this
// Keep iterating over nodes
while(current) {
// The ancestor has been found!
if(current instanceof constructor) {
return current
}
// Use .host to access the parent of a ShadowRoot
else if(current instanceof ShadowRoot) {
current = current.host
}
// Use .parentNode to access the parent of a HTMLElement
else if(current instanceof HTMLElement) {
current = current.parentNode
}
// Something went wrong?
else {
console.warn("[findFirstAncestor] Reached unknown node:", current)
}
}
// The ancestor has NOT been found...
return null
}
constructor() {
super();
// Prevent accidental instantiation of this class.
if(this.constructor === CustomElement) {
throw new NotImplementedError("CustomElement is being used as-is.")
throw new NotImplementedError("CustomElement is being used as-is, but is an abstract class.")
}
}
onConnected() {}
/**
* Get the `<template>` to use when instantiating this element.
* @abstract Must be overridden!
*/
static get template() {
throw new NotImplementedError("template has not been overridden.")
}
static getTemplate() {
throw new NotImplementedError("CustomElement.getTemplate has not been overridden.")
/**
* The local cloned instance of the template node.
* @type {Node}
*/
#instance
/**
* The local cloned instance of the template node.
* @returns {Node}
*/
get instance() {
return this.#instance
}
// noinspection JSUnusedGlobalSymbols
/**
* Callback automatically called when this element is added to the DOM.
*/
connectedCallback() {
// The template to duplicate.
const template = this.constructor.getTemplate()
// The shadow root, the inner contents of the element..
const shadow = this.attachShadow({ mode: "open" })
// The element contained inside the shadow root..
this.#instance = template.content.cloneNode(true)
// Call the custom callback.
this.onConnect()
// Add the instance to the DOM.
shadow.appendChild(this.#instance)
}
/**
* Do something just before `instance` is added to
* @abstract Will do nothing if not overridden.
*/
onConnect() {}
}

View file

@ -1,20 +1,109 @@
import { fileDetails } from "../utils/file.mjs";
import { CustomElement, NotImplementedError } from "./base.mjs";
import { fileDetails } from "../../utils/file.mjs";
import { CustomElement } from "../base.mjs";
/**
* The renderer of an Obsidian Canvas.
*/
export class CanvasElement extends CustomElement {
static getTemplate() {
static get template() {
return document.getElementById("template-canvas")
}
/**
* Parsed value of the `contents` attribute at the moment of connection to the DOM.
* @type {any}
* The contents of the Canvas, as they were the last time they were updated.
* @type {string}
*/
parsedContents
#contents
/**
* The contents of the Canvas, as they were the last time they were updated.
* @returns {string} The raw contents.
*/
get contents() {
return this.#contents
}
/**
* The parsed contents of the Canvas, as they were the last time they were updated.
* @type {Object}
*/
#parsedContents
/**
* The parsed contents of the Canvas, as they were the last time they were updated.
* @returns {Object} The parsed contents.
*/
get parsedContents() {
return this.#parsedContents
}
/**
* Update the values of {@link contents} and {@link parsedContents} from the `contents` attribute of the element.
* @throws SyntaxError If `contents` is not valid JSON.
*/
updateContents() {
this.#contents = this.getAttribute("contents")
this.#parsedContents = JSON.parse(this.#contents)
}
/**
* The minimum X node found in the items of this Canvas.
* Used to compute this element's rect.
* Can be computed from {@link contents} with {@link computeMinMax}.
* @type {{x: number, width: number}}
*/
minX
/**
* The minimum Y node found in the items of this Canvas.
* Used to compute this element's rect.
* Can be computed from {@link contents} with {@link computeMinMax}.
* @type {{y: number, height: number}}
*/
minY
/**
* The maximum X node found in the items of this Canvas.
* Used to compute this element's rect.
* Can be computed from {@link contents} with {@link computeMinMax}.
* @type {{x: number, width: number}}
*/
maxX
/**
* The maximum Y node found in the items of this Canvas.
* Used to compute this element's rect.
* Can be computed from {@link contents} with {@link computeMinMax}.
* @type {{y: number, height: number}}
*/
maxY
/**
* Compute {@link minX}, {@link minY}, {@link maxX}, {@link maxY} from {@link contents}.
* @returns {void}
*/
computeMinMax() {
// Define initial values.
this.minX = { x: Infinity, width: 0 }
this.minY = { y: Infinity, height: 0 }
this.maxX = { x: -Infinity, width: 0 }
this.maxY = { y: -Infinity, height: 0 }
// Iterate over nodes.
for(const node of this.parsedContents["nodes"]) {
// Convert node values from strings to numbers.
let {x, y, width, height} = node
x, y, width, height = Number(x), Number(y), Number(width), Number(height)
// Update minX.
if(x < this.minX.x) this.minX = node
// Update minY.
if(y < this.minY.y) this.minY = node
// Update maxX.
if(x + width > this.maxX.x + width) this.maxX = node
// Update maxY.
if(y + height > this.maxY.y + height) this.maxY = node
}
}
/**
* `<div>` containing all the {@link NodeElement}s of this Canvas.
@ -22,12 +111,6 @@ export class CanvasElement extends CustomElement {
*/
nodesContainer
/**
* `<div>` containing all the {@link NodeElement}s of this Canvas.
* @type {HTMLDivElement}
*/
edgesContainer
/**
* Mapping associating ids to their respective {@link NodeElement}s of this Canvas.
* @type {{[id: string]: NodeElement}}
@ -47,46 +130,39 @@ export class CanvasElement extends CustomElement {
nodeElementsByPath = {}
/**
* Mapping associating ids to their respective {@link EdgeElement}s of this Canvas.
* @type {{[id: string]: EdgeElement}}
* Name of the slot where the node container should be placed.
* @type {string}
*/
edgeElementsById = {}
static NODES_SLOT_NAME = "canvas-nodes"
onConnected() {
super.onConnected();
/**
* Prefix to the name of the element to create for each node.
* @type {string}
*/
static NODE_ELEMENT_NAME_PREFIX = "x-node-"
this.parsedContents = JSON.parse(this.getAttribute("contents"))
/**
* Destroy and recreate the {@link nodesContainer} with the current {@link parsedContents}, {@link minX}, {@link minY}, {@link maxX}, {@link maxY}.
* @returns {void}
*/
recreateNodes() {
if(this.nodesContainer) {
this.nodesContainer.remove()
this.nodesContainer = null
}
this.nodesContainer = document.createElement("div")
this.nodesContainer.slot = "canvas-nodes"
this.edgesContainer = document.createElement("div")
this.edgesContainer.slot = "canvas-edges"
let minX = { x: Infinity, width: 0 }
let minY = { y: Infinity, height: 0 }
let maxX = { x: -Infinity, width: 0 }
let maxY = { y: -Infinity, height: 0 }
for(const node of this.parsedContents["nodes"]) {
let {x, y, width, height} = node
x, y, width, height = Number(x), Number(y), Number(width), Number(height)
if(x < minX.x) minX = node
if(y < minY.y) minY = node
if(x + width > maxX.x + width) maxX = node
if(y + height > maxY.y + height) maxY = node
}
this.nodesContainer.slot = this.constructor.NODES_SLOT_NAME
for(const node of this.parsedContents["nodes"]) {
let {id, type, color, x, y, width, height} = node
x, y, width, height = Number(x), Number(y), Number(width), Number(height)
const element = document.createElement(`x-node-${type}`)
const element = document.createElement(`${this.constructor.NODE_ELEMENT_NAME_PREFIX}${type}`)
element.setAttribute("id", `node-${id}`)
element.setAttribute("x", `${x - minX.x}`)
element.setAttribute("y", `${y - minY.y}`)
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)
@ -120,6 +196,30 @@ export class CanvasElement extends CustomElement {
this.nodesContainer.appendChild(element)
}
// TODO: You were here last time!
}
/**
* `<div>` containing all the {@link NodeElement}s of this Canvas.
* @type {HTMLDivElement}
*/
edgesContainer
/**
* Mapping associating ids to their respective {@link EdgeElement}s of this Canvas.
* @type {{[id: string]: EdgeElement}}
*/
edgeElementsById = {}
onConnect() {
this.updateContents()
this.computeMinMax()
this.recreateNodes()
this.edgesContainer = document.createElement("div")
this.edgesContainer.slot = "canvas-edges"
for(const edge of this.parsedContents["edges"]) {
let {id, fromNode, fromSide, toNode, toSide, color, toEnd: arrows} = edge

View file

View file

@ -1,5 +1,5 @@
import { fileDetails } from "../utils/file.mjs";
import { CanvasElement } from "./canvas.mjs";
import { CanvasElement } from "./canvas/canvas.mjs";
import { MarkdownElement } from "./markdown.mjs";
import { FetchError } from "./node.mjs";
import { CustomElement } from "./base.mjs";
@ -13,7 +13,7 @@ export class DisplayElement extends CustomElement {
containerSlotted
loadButton
onConnected() {
onConnect() {
this.containerSlotted = document.createElement("div")
this.containerSlotted.slot = "display-container"
this.loadButton = document.createElement("button")

View file

@ -1,4 +1,4 @@
import { CanvasElement, CanvasItemElement } from "./canvas.mjs";
import { CanvasElement, CanvasItemElement } from "./canvas/canvas.mjs";
export class EdgeElement extends CanvasItemElement {
@ -9,7 +9,7 @@ export class EdgeElement extends CanvasItemElement {
svgSlotted
lineElement
onConnected() {
onConnect() {
const canvas = this.findFirstAncestor(CanvasElement)
const fromNode = canvas.nodeElements[this.getAttribute("node-from")]

View file

@ -1,5 +1,5 @@
export {NodeFileElement, NodeGroupElement, NodeTextElement} from "./node.mjs"
export {MarkdownElement, HashtagElement, WikilinkElement, FrontMatterElement} from "./markdown.mjs"
export {CanvasElement} from "./canvas.mjs"
export {CanvasElement} from "./canvas/canvas.mjs"
export {DisplayElement} from "./display.mjs"
export {EdgeElement} from "./edge.mjs"

View file

@ -79,7 +79,7 @@ export class MarkdownElement extends CustomElement {
return document.getElementById("template-markdown")
}
onConnected() {
onConnect() {
const markdown = this.getAttribute("contents")
this.contentsElement = document.createElement("div")
@ -116,7 +116,7 @@ export class WikilinkElement extends CustomElement {
return document.getElementById("template-wikilink")
}
onConnected() {
onConnect() {
const instanceElement = this.instance.querySelector(".wikilink")
const destinationURL = new URL(window.location)

View file

@ -1,4 +1,4 @@
import { CanvasItemElement } from "./canvas.mjs";
import { CanvasItemElement } from "./canvas/canvas.mjs";
import { DisplayElement } from "./display.mjs";
@ -54,7 +54,7 @@ export class NodeGroupElement extends NodeElement {
instanceElement
labelSlotted
onConnected() {
onConnect() {
this.instanceElement = this.instance.querySelector(".node-group")
this.instanceElement.style.setProperty("left", `${this.getAttribute("x")}px`)
@ -80,7 +80,7 @@ export class NodeFileElement extends NodeElement {
nameSlotted
contentsSlotted
onConnected() {
onConnect() {
this.instanceElement = this.instance.querySelector(".node-file")
this.instanceElement.style.setProperty("left", `${this.getAttribute("x")}px`)
@ -116,7 +116,7 @@ export class NodeTextElement extends NodeElement {
instanceElement
contentsSlotted
onConnected() {
onConnect() {
this.instanceElement = this.instance.querySelector(".node-text")
this.instanceElement.style.setProperty("left", `${this.getAttribute("x")}px`)

8
src/utils/errors.mjs Normal file
View file

@ -0,0 +1,8 @@
/**
* Various common errors that can be thrown.
*/
/**
* The called method is abstract, but has not been overridden by the child class.
*/
export class NotImplementedError extends Error {}

34
src/utils/trasversal.mjs Normal file
View file

@ -0,0 +1,34 @@
/**
* Utilities to trasverse the DOM.
*/
/**
* Find the first ancestor which is an `instanceof` the given constructor.
* @param start The node to start at.
* @param constructor The constructor to match.
* @returns {HTMLElement|null} The found ancestor, or `null` if no such ancestor is found.
*/
export function findFirstAncestor(start, constructor) {
let current = start
// Keep iterating over nodes
while(current) {
// The ancestor has been found!
if(current instanceof constructor) {
return current
}
// Use .host to access the parent of a ShadowRoot
else if(current instanceof ShadowRoot) {
current = current.host
}
// Use .parentNode to access the parent of a HTMLElement
else if(current instanceof HTMLElement) {
current = current.parentNode
}
// Something went wrong?
else {
console.warn("[findFirstAncestor] Reached unknown node:", current)
}
}
// The ancestor has NOT been found...
return null
}