2023-07-15 22:00:27 +02:00
|
|
|
/*eslint no-console: "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.
|
|
|
|
* @param {Object} to The object to copy to
|
|
|
|
* @param {Object} from The object tp copy from (but only the properties already named in "to")
|
|
|
|
*/
|
|
|
|
const specsCopy = (to, from) => {
|
|
|
|
for(const ix in to){
|
|
|
|
if(from.hasOwnProperty(ix)){
|
|
|
|
if( typeof to[ix] == "object"
|
|
|
|
&& typeof from[ix] == "object")
|
|
|
|
{
|
|
|
|
if(Array.isArray(to[ix])){
|
|
|
|
if(Array.isArray(from[ix])){
|
|
|
|
to[ix] = Array.from(from[ix]);
|
|
|
|
} // else, skip...
|
|
|
|
} else {
|
|
|
|
specsCopy(to[ix], from[ix]); // recursive copy
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
to[ix] = from[ix];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-07-15 23:37:04 +02:00
|
|
|
/**
|
|
|
|
* Get the position of an element relative to another
|
|
|
|
* @param {HTMLElement} el The element whose position to determine
|
|
|
|
* @param {HTMLElement} reference Relative to this element
|
|
|
|
* @returns
|
|
|
|
*/
|
|
|
|
const getElementPosition = (el, reference) => {
|
|
|
|
if(!el || !(el instanceof HTMLElement)){
|
|
|
|
// Always return 0,0 if the element is invalid
|
|
|
|
return {x: 0, y: 0};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!reference || !(reference instanceof HTMLElement)){
|
|
|
|
// Take the document body as reference if the reference is invalud
|
|
|
|
reference = document.querySelector("body");
|
|
|
|
}
|
|
|
|
|
|
|
|
if( el.offsetParent === reference){
|
|
|
|
// easily done if the reference element is also the offsetParent..
|
|
|
|
return {x: el.offsetLeft, y: el.offsetTop};
|
|
|
|
} else {
|
|
|
|
const elR = el.getBoundingClientRect();
|
|
|
|
const refR = reference.getBoundingClientRect();
|
|
|
|
|
|
|
|
return {x: elR.left - refR.left,
|
|
|
|
y: elR.top - refR.top};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2023-07-15 22:00:27 +02:00
|
|
|
/**
|
|
|
|
* Check if an element is a containing element and can thus be a parent for absolute positioning
|
|
|
|
* Which is the cas if the element has
|
|
|
|
* 1 A position value other than static (fixed, absolute, relative, or sticky).
|
|
|
|
* 2 A transform or perspective value other than none
|
|
|
|
* 3 A will-change value of transform or perspective
|
|
|
|
* 4 A filter value other than none or a will-change value of filter (only works on Firefox)
|
|
|
|
* 5 A contain value of layout, paint, strict or content (e.g. contain: paint;)
|
|
|
|
* 6 A container-type value other than normal
|
|
|
|
* 7 A backdrop-filter other than none (e.g. backdrop-filter: blur(10px);)
|
|
|
|
* @param {HTMLElement} el
|
|
|
|
* @returns {Boolean}
|
|
|
|
*/
|
|
|
|
const isContainingElement = (el) => {
|
|
|
|
const cssStyle = getComputedStyle(el);
|
|
|
|
return ["fixed","absolute","relative","sticky",].includes(cssStyle.position)
|
|
|
|
|| cssStyle.transform !== "none"
|
|
|
|
|| cssStyle.perspective !== "none"
|
|
|
|
|| ["transform","perspective"].includes(cssStyle.willChange)
|
|
|
|
|| cssStyle.filter !== "none"
|
|
|
|
|| cssStyle.contain == "paint"
|
|
|
|
|| cssStyle.backdropFilter !== "none";
|
|
|
|
};
|
|
|
|
|
|
|
|
export class SimpleLine {
|
|
|
|
|
|
|
|
constructor(start, end, specs){
|
|
|
|
this.svg = null;
|
|
|
|
|
|
|
|
// setup defaults
|
|
|
|
this.specs = {
|
|
|
|
container: null,
|
|
|
|
class: "",
|
|
|
|
colors: {
|
|
|
|
line : "currentcolor",
|
|
|
|
arrow: "currentcolor",
|
|
|
|
},
|
|
|
|
anchors: {
|
|
|
|
// top, middle, bottom
|
|
|
|
// left, center, right
|
|
|
|
start: ["middle","right"],
|
|
|
|
end: ["middle", "left"],
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
specsCopy(this.specs,specs);
|
|
|
|
|
|
|
|
// Validate start element
|
|
|
|
if(start instanceof HTMLElement) {
|
|
|
|
this.start = start;
|
|
|
|
} else if (typeof this.start === 'string' || this.start instanceof String) {
|
|
|
|
this.startSelector = start;
|
|
|
|
this.start = document.querySelector(start);
|
|
|
|
if(!(this.start instanceof HTMLElement)){
|
|
|
|
console.error("Cannot find start element:",start);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
console.error("Start element not string or dom element",start);
|
2023-07-15 23:37:04 +02:00
|
|
|
return;
|
2023-07-15 22:00:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Validate end element
|
|
|
|
if(end instanceof HTMLElement) {
|
|
|
|
this.end = end;
|
|
|
|
} else if (typeof this.end === 'string' || this.end instanceof String) {
|
|
|
|
this.endSelector = end;
|
|
|
|
this.end = document.querySelector(end);
|
|
|
|
if(!(this.end instanceof HTMLElement)){
|
|
|
|
console.error("Cannot find end element:",end);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
console.error("End element not string or dom element",start);
|
2023-07-15 23:37:04 +02:00
|
|
|
return;
|
2023-07-15 22:00:27 +02:00
|
|
|
}
|
|
|
|
|
2023-07-15 23:37:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
getContainer(){
|
2023-07-15 22:00:27 +02:00
|
|
|
// Validate or determine container
|
2023-07-15 23:37:04 +02:00
|
|
|
let container = this.startstart.offsetParent;
|
|
|
|
if(!container) {
|
|
|
|
if(document.getComputedStyle(this.start).position == "fixed"){
|
|
|
|
container = document.querySelector("body");
|
|
|
|
} else {
|
|
|
|
console.error("Start element has no offsetParent. likely ")
|
2023-07-15 22:00:27 +02:00
|
|
|
}
|
|
|
|
}
|
2023-07-15 23:37:04 +02:00
|
|
|
return container;
|
|
|
|
}
|
|
|
|
|
|
|
|
getAnchorPoint(anchor){
|
|
|
|
|
|
|
|
let el = this.start;
|
|
|
|
if(anchor != "start"){
|
|
|
|
anchor = "end";
|
|
|
|
el = this.end;
|
2023-07-15 22:00:27 +02:00
|
|
|
}
|
|
|
|
|
2023-07-15 23:37:04 +02:00
|
|
|
let x, dirX;
|
|
|
|
let y, dirY;
|
|
|
|
// determine start coordinates
|
|
|
|
if(this.specs.anchors[anchor].includes("left")){
|
|
|
|
x = 0;
|
|
|
|
dirX = -1;
|
|
|
|
} else if (this.specs.anchors[anchor].includes("right")) {
|
|
|
|
x = el.offsetWidth -1;
|
|
|
|
dirX = 1;
|
|
|
|
} else { // center
|
|
|
|
x = el.offsetWidth / 2;
|
|
|
|
dirX = 0;
|
|
|
|
}
|
|
|
|
if(this.specs.anchors[anchor].includes("top")){
|
|
|
|
y = 0;
|
|
|
|
dirY = 1;
|
|
|
|
} else if (this.specs.anchors[anchor].includes("bottom")) {
|
|
|
|
x = el.offsetHeight -1;
|
|
|
|
dirY = -1;
|
|
|
|
} else { // middle
|
|
|
|
x = el.offsetHeight / 2;
|
|
|
|
dirY = 0;
|
|
|
|
}
|
2023-07-15 22:00:27 +02:00
|
|
|
|
2023-07-15 23:37:04 +02:00
|
|
|
const rotation = Math.atan2(dirX,dirY);
|
|
|
|
|
|
|
|
return { x: x, y: y, rot: rotation};
|
2023-07-15 22:00:27 +02:00
|
|
|
}
|
|
|
|
|
2023-07-15 23:37:04 +02:00
|
|
|
|
2023-07-15 22:00:27 +02:00
|
|
|
paintSvg(){
|
2023-07-15 23:37:04 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
// determine proper x, y ,h and w
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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`;
|
|
|
|
|
|
|
|
// Draw the arrows....
|
|
|
|
|
|
|
|
|
2023-07-15 22:00:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|