Added line drawer alternative to leaderline
This commit is contained in:
parent
854f3ad13e
commit
49c54bb713
1 changed files with 207 additions and 35 deletions
|
@ -1,5 +1,5 @@
|
||||||
/*eslint no-console: "off"*/
|
/*eslint no-console: "off"*/
|
||||||
|
/*eslint no-trailing-spaces: "off"*/
|
||||||
/**
|
/**
|
||||||
* Copies defined properties in to, over from the from object. Does so recursively
|
* 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.
|
* 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
|
* Get the position of an element relative to another
|
||||||
* @param {HTMLElement} el The element whose position to determine
|
* @param {HTMLElement} el The element whose position to determine
|
||||||
|
@ -54,10 +65,7 @@ const getElementPosition = (el, reference) => {
|
||||||
return {x: elR.left - refR.left,
|
return {x: elR.left - refR.left,
|
||||||
y: elR.top - refR.top};
|
y: elR.top - refR.top};
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if an element is a containing element and can thus be a parent for absolute positioning
|
* 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
|
* @param {HTMLElement} el
|
||||||
* @returns {Boolean}
|
* @returns {Boolean}
|
||||||
*/
|
*/
|
||||||
const isContainingElement = (el) => {
|
/*
|
||||||
|
function isContainingElement(el) {
|
||||||
const cssStyle = getComputedStyle(el);
|
const cssStyle = getComputedStyle(el);
|
||||||
return ["fixed","absolute","relative","sticky",].includes(cssStyle.position)
|
return ["fixed","absolute","relative","sticky",].includes(cssStyle.position)
|
||||||
|| cssStyle.transform !== "none"
|
|| cssStyle.transform !== "none"
|
||||||
|
@ -81,35 +90,40 @@ const isContainingElement = (el) => {
|
||||||
|| cssStyle.filter !== "none"
|
|| cssStyle.filter !== "none"
|
||||||
|| cssStyle.contain == "paint"
|
|| cssStyle.contain == "paint"
|
||||||
|| cssStyle.backdropFilter !== "none";
|
|| cssStyle.backdropFilter !== "none";
|
||||||
};
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
export class SimpleLine {
|
export class SimpleLine {
|
||||||
|
static idCounter = 0;
|
||||||
|
|
||||||
constructor(start, end, specs){
|
constructor(start, end, specs){
|
||||||
this.svg = null;
|
this.svg = null;
|
||||||
|
this.id = SimpleLine.idCounter++;
|
||||||
|
|
||||||
// setup defaults
|
// setup defaults
|
||||||
this.specs = {
|
this.specs = {
|
||||||
container: null,
|
autorefresh: 10,
|
||||||
class: "",
|
class: "",
|
||||||
colors: {
|
|
||||||
line : "currentcolor",
|
|
||||||
arrow: "currentcolor",
|
|
||||||
},
|
|
||||||
anchors: {
|
anchors: {
|
||||||
// top, middle, bottom
|
// top, middle, bottom
|
||||||
// left, center, right
|
// left, center, right
|
||||||
start: ["middle","right"],
|
start: ["middle","right"],
|
||||||
end: ["middle", "left"],
|
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
|
// Validate start element
|
||||||
if(start instanceof HTMLElement) {
|
if(start instanceof HTMLElement) {
|
||||||
this.start = start;
|
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.startSelector = start;
|
||||||
this.start = document.querySelector(start);
|
this.start = document.querySelector(start);
|
||||||
if(!(this.start instanceof HTMLElement)){
|
if(!(this.start instanceof HTMLElement)){
|
||||||
|
@ -124,7 +138,7 @@ export class SimpleLine {
|
||||||
// Validate end element
|
// Validate end element
|
||||||
if(end instanceof HTMLElement) {
|
if(end instanceof HTMLElement) {
|
||||||
this.end = end;
|
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.endSelector = end;
|
||||||
this.end = document.querySelector(end);
|
this.end = document.querySelector(end);
|
||||||
if(!(this.end instanceof HTMLElement)){
|
if(!(this.end instanceof HTMLElement)){
|
||||||
|
@ -136,16 +150,55 @@ export class SimpleLine {
|
||||||
return;
|
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(){
|
getContainer(){
|
||||||
// Validate or determine container
|
// Validate or determine container
|
||||||
let container = this.startstart.offsetParent;
|
let container = this.start.offsetParent;
|
||||||
if(!container) {
|
if(!container) {
|
||||||
if(document.getComputedStyle(this.start).position == "fixed"){
|
if(document.getComputedStyle(this.start).position == "fixed"){
|
||||||
container = document.querySelector("body");
|
container = document.querySelector("body");
|
||||||
} else {
|
} else {
|
||||||
console.error("Start element has no offsetParent. likely ")
|
console.error("Start element has no offsetParent. likely ");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return container;
|
return container;
|
||||||
|
@ -176,43 +229,162 @@ export class SimpleLine {
|
||||||
y = 0;
|
y = 0;
|
||||||
dirY = 1;
|
dirY = 1;
|
||||||
} else if (this.specs.anchors[anchor].includes("bottom")) {
|
} else if (this.specs.anchors[anchor].includes("bottom")) {
|
||||||
x = el.offsetHeight -1;
|
y = el.offsetHeight -1;
|
||||||
dirY = -1;
|
dirY = -1;
|
||||||
} else { // middle
|
} else { // middle
|
||||||
x = el.offsetHeight / 2;
|
y = el.offsetHeight / 2;
|
||||||
dirY = 0;
|
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();
|
const container = this.getContainer();
|
||||||
if (!container) { return; } // Do not create any svg if container is empty
|
if (!container) { return; } // Do not create any svg if container is empty
|
||||||
|
|
||||||
if(!this.svg || !(this.svg instanceof HTMLElement)){
|
if(this.svg && (this.svg instanceof SVGElement)){
|
||||||
this.svg = document.createElement("svg");
|
if(container !== this.svg.offsetParent){
|
||||||
container.append(this.svg);
|
// 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
|
// 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}`;
|
// Now convert the coordinates to the svg space
|
||||||
this.svg.attributes["width"] = `${w}px`;
|
const startPos = {
|
||||||
this.svg.attributes["height"] = `${h}px`;
|
x: cStartPos.x - bounds.x,
|
||||||
this.svg.style.position = "absolute";
|
y: cStartPos.y - bounds.y,
|
||||||
this.svg.style.left = `${x}px`;
|
dirx: cStartPos.dirx - bounds.x,
|
||||||
this.svg.style.top = `${y}px`;
|
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);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue