/*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]; } } } }; /** * 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}; } } /** * 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); return; } // 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); return; } } getContainer(){ // Validate or determine container 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 ") } } return container; } getAnchorPoint(anchor){ let el = this.start; if(anchor != "start"){ anchor = "end"; el = this.end; } 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; } const rotation = Math.atan2(dirX,dirY); return { x: x, y: y, rot: rotation}; } paintSvg(){ 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.... } }