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:
parent
fd8ea9c494
commit
a1a8b89a2b
10 changed files with 243 additions and 104 deletions
|
@ -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() {}
|
||||
|
||||
static getTemplate() {
|
||||
throw new NotImplementedError("CustomElement.getTemplate has not been overridden.")
|
||||
/**
|
||||
* Get the `<template>` to use when instantiating this element.
|
||||
* @abstract Must be overridden!
|
||||
*/
|
||||
static get template() {
|
||||
throw new NotImplementedError("template 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() {}
|
||||
}
|
||||
|
|
|
@ -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
|
0
src/elements/canvas/canvasitem.mjs
Normal file
0
src/elements/canvas/canvasitem.mjs
Normal 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")
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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"
|
|
@ -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)
|
||||
|
|
|
@ -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
8
src/utils/errors.mjs
Normal 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
34
src/utils/trasversal.mjs
Normal 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
|
||||
}
|
Loading…
Reference in a new issue