From 49c54bb713f4bbce51ba04a2ba81c59cf6437e28 Mon Sep 17 00:00:00 2001 From: PMKuipers Date: Mon, 17 Jul 2023 14:53:44 +0200 Subject: [PATCH] Added line drawer alternative to leaderline --- amd/src/simpleline.js | 242 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 207 insertions(+), 35 deletions(-) diff --git a/amd/src/simpleline.js b/amd/src/simpleline.js index c5e375d..aa18c5a 100644 --- a/amd/src/simpleline.js +++ b/amd/src/simpleline.js @@ -1,5 +1,5 @@ /*eslint no-console: "off"*/ - +/*eslint no-trailing-spaces: "off"*/ /** * Copies defined properties in to, over from the from object. Does so recursively * Ysed to copy user defined parameters onto a pre-existing object with defaults preset. @@ -27,6 +27,17 @@ const specsCopy = (to, from) => { } }; +const debounce = (func, delay) => { + let timer; + return function(...args) { + const context = this; + clearTimeout(timer); + timer = setTimeout(() => { + func.apply(context, args); + }, delay) + } +} + /** * Get the position of an element relative to another * @param {HTMLElement} el The element whose position to determine @@ -54,10 +65,7 @@ const getElementPosition = (el, reference) => { return {x: elR.left - refR.left, y: elR.top - refR.top}; } -} - - - +}; /** * Check if an element is a containing element and can thus be a parent for absolute positioning @@ -72,7 +80,8 @@ const getElementPosition = (el, reference) => { * @param {HTMLElement} el * @returns {Boolean} */ -const isContainingElement = (el) => { +/* +function isContainingElement(el) { const cssStyle = getComputedStyle(el); return ["fixed","absolute","relative","sticky",].includes(cssStyle.position) || cssStyle.transform !== "none" @@ -81,35 +90,40 @@ const isContainingElement = (el) => { || cssStyle.filter !== "none" || cssStyle.contain == "paint" || cssStyle.backdropFilter !== "none"; -}; +} +*/ export class SimpleLine { + static idCounter = 0; constructor(start, end, specs){ this.svg = null; + this.id = SimpleLine.idCounter++; // setup defaults this.specs = { - container: null, + autorefresh: 10, class: "", - colors: { - line : "currentcolor", - arrow: "currentcolor", - }, anchors: { // top, middle, bottom // left, center, right start: ["middle","right"], end: ["middle", "left"], - } + }, + gravity: { + start: 1, + end: 1, + }, + stroke: "4px", }; - specsCopy(this.specs,specs); - + if(specs && typeof specs == "object"){ + specsCopy(this.specs,specs); + } // Validate start element if(start instanceof HTMLElement) { this.start = start; - } else if (typeof this.start === 'string' || this.start instanceof String) { + } else if (typeof start === 'string' || this.start instanceof String) { this.startSelector = start; this.start = document.querySelector(start); if(!(this.start instanceof HTMLElement)){ @@ -124,7 +138,7 @@ export class SimpleLine { // Validate end element if(end instanceof HTMLElement) { this.end = end; - } else if (typeof this.end === 'string' || this.end instanceof String) { + } else if (typeof end === 'string' || this.end instanceof String) { this.endSelector = end; this.end = document.querySelector(end); if(!(this.end instanceof HTMLElement)){ @@ -136,16 +150,55 @@ export class SimpleLine { return; } + // create observers for both elements + this.resizeObserver = new ResizeObserver(debounce(() => { + this.update(); + },20)); + this.resizeObserver.observe(this.start); + this.resizeObserver.observe(this.end); + + this.positionCheck(); // Initialize refresh + if(this.specs.autorefresh > 0){ + const refresh = () => { + setTimeout(refresh,this.specs.autorefresh); + this.positionCheck(); // Check if position ch + }; + setTimeout(refresh,this.specs.autorefresh); + } + this.update(); // fist draw + } + + positionCheck(){ + const startPos = {x: this.start.offsetLeft, y: this.start.offsetTop}; + const endPos = {x: this.end.offsetLeft, y: this.end.offsetTop}; + + let needUpdate = false; + if(this.startPos){ + needUpdate |= (this.startPos.x != startPos.x || this.startPos.y != startPos.y); + console.info("Offset position changed for",this.start); + } + this.startPos = startPos; + + if(this.endPos){ + needUpdate |= (this.endPos.x != endPos.x || this.endPos.y != endPos.y); + console.info("Offset position changed for",this.end); + } + this.endPos = endPos; + + if(needUpdate){ + this.update(); + } + } getContainer(){ // Validate or determine container - let container = this.startstart.offsetParent; + let container = this.start.offsetParent; if(!container) { if(document.getComputedStyle(this.start).position == "fixed"){ container = document.querySelector("body"); } else { - console.error("Start element has no offsetParent. likely ") + console.error("Start element has no offsetParent. likely "); } } return container; @@ -176,43 +229,162 @@ export class SimpleLine { y = 0; dirY = 1; } else if (this.specs.anchors[anchor].includes("bottom")) { - x = el.offsetHeight -1; + y = el.offsetHeight -1; dirY = -1; } else { // middle - x = el.offsetHeight / 2; + y = el.offsetHeight / 2; dirY = 0; } - const rotation = Math.atan2(dirX,dirY); + return { x: x, y: y, dir: {x: dirX, y: dirY}}; + } - return { x: x, y: y, rot: rotation}; + svgDefs(){ + const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs"); + const marker = document.createElementNS("http://www.w3.org/2000/svg", "marker"); + marker.setAttribute("id",`simpleline-${this.id}-arrow`); + marker.setAttribute("markerUnits",`strokeWidth`); + marker.setAttribute("viewBox",`-8 -8 16 16`); + marker.setAttribute("orient",`auto`); + marker.setAttribute("markerWidth",`4`); + marker.setAttribute("markerHeight",`4`); + defs.appendChild(marker); + const polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon"); + polygon.setAttribute("fill",`currentColor`); + polygon.setAttribute("points",`-8,-8 8,0 -8,8 -5,0`); + marker.append(polygon); + return defs; } - paintSvg(){ + update(){ const container = this.getContainer(); if (!container) { return; } // Do not create any svg if container is empty - if(!this.svg || !(this.svg instanceof HTMLElement)){ - this.svg = document.createElement("svg"); - container.append(this.svg); + if(this.svg && (this.svg instanceof SVGElement)){ + if(container !== this.svg.offsetParent){ + // update the svg's parent if the container was changed + container.appendChild(this.svg); + } + } else { + this.svg = document.createElementNS('http://www.w3.org/2000/svg','svg'); + this.svg.setAttribute("id",`simpleline-${this.id}`); + this.svg.setAttribute("class",`simpleline ${this.specs.class}`); + this.svg.style.position = "absolute"; + this.svg.style.pointerEvents = 'none'; + + container.appendChild(this.svg); + this.svg.appendChild(this.svgDefs()); // create a new defs element and add it to the svg + // Add the marker definitions + } // determine proper x, y ,h and w + const startAnchor = this.getAnchorPoint("start"); + const endAnchor = this.getAnchorPoint("end"); + const elStartPos = getElementPosition(this.start,container); + const elEndPos = getElementPosition(this.end,container); + console.info("startAnchor",startAnchor); + console.info("endAnchor",endAnchor); + console.info("elStartPos",elStartPos); + console.info("elEndPos",elEndPos); + + // Determine basic h/w between start and end anchor to help determine desired control point length + const w = Math.max(elStartPos.x + startAnchor.x,elEndPos.x + endAnchor.x) + - Math.min(elStartPos.x + startAnchor.x,elEndPos.x + endAnchor.x); + const h = Math.max(elStartPos.y + startAnchor.y,elEndPos.x + endAnchor.y) + - Math.min(elStartPos.y + startAnchor.y,elEndPos.x + endAnchor.y); + + // Determine start positions and end positions relative to container + const cStartPos = { + x: elStartPos.x + startAnchor.x, + y: elStartPos.y + startAnchor.y, + dirx: elStartPos.x + startAnchor.x + startAnchor.dir.x * this.specs.gravity.start * (w/2), + diry: elStartPos.y + startAnchor.y + startAnchor.dir.y * this.specs.gravity.start * (h/2), + }; + + const cEndPos = { + x: elEndPos.x + endAnchor.x, + y: elEndPos.y + endAnchor.y, + dirx: elEndPos.x + endAnchor.x + endAnchor.dir.x * this.specs.gravity.end * (w/2), + diry: elEndPos.y + endAnchor.y + endAnchor.dir.y * this.specs.gravity.end * (h/2), + }; + + // determine the bounding rectangle of the + console.info("cStartPos",cStartPos); + console.info("cEndPos",cEndPos); + + const margin = (!isNaN(this.specs.stroke)?5*this.specs.stroke:25); + const bounds = { + x: Math.min(cStartPos.x,cEndPos.x,cStartPos.dirx,cEndPos.dirx) - margin, + y: Math.min(cStartPos.y,cEndPos.y,cStartPos.diry,cEndPos.diry) - margin, + w: Math.max(cStartPos.x,cEndPos.x,cStartPos.dirx,cEndPos.dirx) + - Math.min(cStartPos.x,cEndPos.x,cStartPos.dirx,cEndPos.dirx) + 2*margin, + h: Math.max(cStartPos.y,cEndPos.y,cStartPos.diry,cEndPos.diry) + - Math.min(cStartPos.y,cEndPos.y,cStartPos.diry,cEndPos.diry) + 2*margin, + }; - this.svg.attributes["viewBox"] = `0 0 ${w}, ${h}`; - this.svg.attributes["width"] = `${w}px`; - this.svg.attributes["height"] = `${h}px`; - this.svg.style.position = "absolute"; - this.svg.style.left = `${x}px`; - this.svg.style.top = `${y}px`; + // Now convert the coordinates to the svg space + const startPos = { + x: cStartPos.x - bounds.x, + y: cStartPos.y - bounds.y, + dirx: cStartPos.dirx - bounds.x, + diry: cStartPos.diry - bounds.y, + }; + const endPos = { + x: cEndPos.x - bounds.x, + y: cEndPos.y - bounds.y, + dirx: cEndPos.dirx - bounds.x, + diry: cEndPos.diry - bounds.y, + }; - // Draw the arrows.... + console.info("Bounds",bounds); + console.info("startPos",startPos); + console.info("endPos",endPos); + // Update the svg attributes + this.svg.setAttribute("viewBox",`0 0 ${bounds.w}, ${bounds.h}`); + this.svg.setAttribute("width",`${bounds.w}px`); + this.svg.setAttribute("height",`${bounds.h}px`); + // Reposition the SVG relative to the container + this.svg.style.left = `${bounds.x}px`; + this.svg.style.top = `${bounds.y}px`; + this.svg.style.width = `${bounds.w}px`; + this.svg.style.height = `${bounds.h}px`; + + + // Draw the line + if(!this.line){ + this.line = document.createElementNS("http://www.w3.org/2000/svg", "path"); + this.line.setAttribute("marker-end",`url(#simpleline-${this.id}-arrow)`); + this.svg.appendChild(this.line); + } + let strokeWidth = this.specs.stroke; + if(!isNaN(strokeWidth) && strokeWidth != 0){ + strokeWidth = `${strokeWidth}px`; + } + this.line.style.stroke = "currentColor" + this.line.style.fill = "none" + this.line.style.strokeWidth= strokeWidth; + this.line.setAttribute('d', + `M ${startPos.x} ${startPos.y} + C ${startPos.dirx} ${startPos.diry}, ${endPos.dirx} ${endPos.diry}, ${endPos.x} ${endPos.y}`); + } + + remove(){ + // remove the line + this.svg.remove(); + this.svg = null; + this.line = null; + + this.resizeObserver.unobserve(this.start); + this.resizeObserver.unobserve(this.end); + this.mutationObserver.unobserve(this.start); + this.mutationObserver.unobserve(this.end); + } - }