import { action, computed, IReactionDisposer, observable, reaction } from "mobx"; import { observer } from "mobx-react"; import { Doc, Field } from "../../../../fields/Doc"; import { Id } from "../../../../fields/FieldSymbols"; import { List } from "../../../../fields/List"; import { Cast, NumCast } from "../../../../fields/Types"; import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../Utils'; import { LinkManager } from "../../../util/LinkManager"; import { SelectionManager } from "../../../util/SelectionManager"; import { SnappingManager } from "../../../util/SnappingManager"; import { DocumentView } from "../../nodes/DocumentView"; import "./CollectionFreeFormLinkView.scss"; import React = require("react"); import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; import { Colors } from "../../global/globalEnums"; export interface CollectionFreeFormLinkViewProps { A: DocumentView; B: DocumentView; LinkDocs: Doc[]; } @observer export class CollectionFreeFormLinkView extends React.Component { @observable _opacity: number = 0; @observable _start = 0; _anchorDisposer: IReactionDisposer | undefined; _timeout: NodeJS.Timeout | undefined; componentWillUnmount() { this._anchorDisposer?.(); } @action timeout = action(() => (Date.now() < this._start++ + 1000) && (this._timeout = setTimeout(this.timeout, 25))); componentDidMount() { this._anchorDisposer = reaction(() => [ this.props.A.props.ScreenToLocalTransform(), Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.anchor1, Doc, null)?.annotationOn, Doc, null)?.scrollTop, Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.anchor1, Doc, null)?.annotationOn, Doc, null)?._highlights, this.props.B.props.ScreenToLocalTransform(), Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.anchor2, Doc, null)?.annotationOn, Doc, null)?.scrollTop, Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.anchor2, Doc, null)?.annotationOn, Doc, null)?._highlights, ], action(() => { this._start = Date.now(); this._timeout && clearTimeout(this._timeout); this._timeout = setTimeout(this.timeout, 25); setTimeout(this.placeAnchors); }) , { fireImmediately: true }); } placeAnchors = () => { const { A, B, LinkDocs } = this.props; const linkDoc = LinkDocs[0]; if (SnappingManager.GetIsDragging() || !A.ContentDiv || !B.ContentDiv) return; setTimeout(action(() => this._opacity = 0.75), 0); // since the render code depends on querying the Dom through getBoudndingClientRect, we need to delay triggering render() setTimeout(action(() => (!LinkDocs.length || !linkDoc.linkDisplay) && (this._opacity = 0.05)), 750); // this will unhighlight the link line. const a = A.ContentDiv.getBoundingClientRect(); const b = B.ContentDiv.getBoundingClientRect(); const { left: aleft, top: atop, width: awidth, height: aheight } = A.ContentDiv.parentElement!.getBoundingClientRect(); const { left: bleft, top: btop, width: bwidth, height: bheight } = B.ContentDiv.parentElement!.getBoundingClientRect(); const apt = Utils.closestPtBetweenRectangles(aleft, atop, awidth, aheight, bleft, btop, bwidth, bheight, a.left + a.width / 2, a.top + a.height / 2); const bpt = Utils.closestPtBetweenRectangles(bleft, btop, bwidth, bheight, aleft, atop, awidth, aheight, apt.point.x, apt.point.y); // really hacky stuff to make the LinkAnchorBox display where we want it to: // if there's an element in the DOM with a classname containing a link anchor's id, // then that DOM element is a hyperlink source for the current anchor and we want to place our link box at it's top right // otherwise, we just use the computed nearest point on the document boundary to the target Document const targetAhyperlink = Array.from(window.document.getElementsByClassName((linkDoc.anchor1 as Doc)[Id])).lastElement(); const targetBhyperlink = Array.from(window.document.getElementsByClassName((linkDoc.anchor2 as Doc)[Id])).lastElement(); if ((!targetAhyperlink && !a.width) || (!targetBhyperlink && !b.width)) return; if (!targetAhyperlink) { if (linkDoc.linkAutoMove) { linkDoc.anchor1_x = (apt.point.x - aleft) / awidth * 100; linkDoc.anchor1_y = (apt.point.y - atop) / aheight * 100; } } else { const m = targetAhyperlink.getBoundingClientRect(); const mp = A.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); const mpx = mp[0] / A.props.PanelWidth(); const mpy = mp[1] / A.props.PanelHeight(); if (mpx >= 0 && mpx <= 1) linkDoc.anchor1_x = mpx * 100; if (mpy >= 0 && mpy <= 1) linkDoc.anchor1_y = mpy * 100; if (getComputedStyle(targetAhyperlink).fontSize === "0px") linkDoc.opacity = 0; else linkDoc.opacity = 1; } if (!targetBhyperlink) { if (linkDoc.linkAutoMove) { linkDoc.anchor2_x = (bpt.point.x - bleft) / bwidth * 100; linkDoc.anchor2_y = (bpt.point.y - btop) / bheight * 100; } } else { const m = targetBhyperlink.getBoundingClientRect(); const mp = B.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); const mpx = mp[0] / B.props.PanelWidth(); const mpy = mp[1] / B.props.PanelHeight(); if (mpx >= 0 && mpx <= 1) linkDoc.anchor2_x = mpx * 100; if (mpy >= 0 && mpy <= 1) linkDoc.anchor2_y = mpy * 100; if (getComputedStyle(targetBhyperlink).fontSize === "0px") linkDoc.opacity = 0; else linkDoc.opacity = 1; } } pointerDown = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, (e, down, delta) => { this.props.LinkDocs[0].linkOffsetX = NumCast(this.props.LinkDocs[0].linkOffsetX) + delta[0]; this.props.LinkDocs[0].linkOffsetY = NumCast(this.props.LinkDocs[0].linkOffsetY) + delta[1]; return false; }, emptyFunction, () => { // OverlayView.Instance.addElement( // { })} // />, { x: 300, y: 300 }); }); } visibleY = (el: any) => { let rect = el.getBoundingClientRect(); const top = rect.top, height = rect.height; var el = el.parentNode; while (el && el !== document.body) { rect = el.getBoundingClientRect?.(); if (rect?.width) { if (top <= rect.bottom === false && getComputedStyle(el).overflow === "hidden") return rect.bottom; // Check if the element is out of view due to a container scrolling if ((top + height) <= rect.top && getComputedStyle(el).overflow === "hidden") return rect.top; } el = el.parentNode; } // Check its within the document viewport return top; //top <= document.documentElement.clientHeight && getComputedStyle(document.documentElement).overflow === "hidden"; } visibleX = (el: any) => { let rect = el.getBoundingClientRect(); const left = rect.left, width = rect.width; var el = el.parentNode; while (el && el !== document.body) { rect = el?.getBoundingClientRect(); if (rect?.width) { if (left <= rect.right === false && getComputedStyle(el).overflow === "hidden") return rect.right; // Check if the element is out of view due to a container scrolling if ((left + width) <= rect.left && getComputedStyle(el).overflow === "hidden") return rect.left; } el = el.parentNode; } // Check its within the document viewport return left; //top <= document.documentElement.clientHeight && getComputedStyle(document.documentElement).overflow === "hidden"; } @action toggleProperties = () => { if (CurrentUserUtils.propertiesWidth > 0) { CurrentUserUtils.propertiesWidth = 0; } else { CurrentUserUtils.propertiesWidth = 250; } } onClickLine = () => { SelectionManager.SelectSchemaViewDoc(this.props.LinkDocs[0], true); this.toggleProperties(); } @computed.struct get renderData() { this._start; SnappingManager.GetIsDragging(); const { A, B, LinkDocs } = this.props; if (!A.ContentDiv || !B.ContentDiv || !LinkDocs.length) return undefined; const acont = A.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); const bcont = B.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); const adiv = acont.length ? acont[0] : A.ContentDiv; const bdiv = bcont.length ? bcont[0] : B.ContentDiv; for (let apdiv = adiv; apdiv; apdiv = apdiv.parentElement as any) if ((apdiv as any).hidden) return; for (let bpdiv = bdiv; bpdiv; bpdiv = bpdiv.parentElement as any) if ((bpdiv as any).hidden) return; const a = adiv.getBoundingClientRect(); const b = bdiv.getBoundingClientRect(); const atop = this.visibleY(adiv); const btop = this.visibleY(bdiv); if (!a.width || !b.width) return undefined; const aDocBounds = (A.props as any).DocumentView?.().getBounds() || { left: 0, right: 0, top: 0, bottom: 0 }; const bDocBounds = (B.props as any).DocumentView?.().getBounds() || { left: 0, right: 0, top: 0, bottom: 0 }; const aleft = this.visibleX(adiv); const bleft = this.visibleX(bdiv); const clipped = aleft !== a.left || atop !== a.top || bleft !== b.left || btop !== b.top; const pt1 = [aleft + a.width / 2, atop + a.height / 2]; const pt2 = [bleft + b.width / 2, btop + b.width / 2]; const pt1vec = [(bDocBounds.left + bDocBounds.right) / 2 - pt1[0], (bDocBounds.top + bDocBounds.bottom) / 2 - pt1[1]]; const pt2vec = [(aDocBounds.left + aDocBounds.right) / 2 - pt2[0], (aDocBounds.top + aDocBounds.bottom) / 2 - pt2[1]]; const pt1len = Math.sqrt((pt1vec[0] * pt1vec[0]) + (pt1vec[1] * pt1vec[1])); const pt2len = Math.sqrt((pt2vec[0] * pt2vec[0]) + (pt2vec[1] * pt2vec[1])); const ptlen = Math.sqrt((pt1[0] - pt2[0]) * (pt1[0] - pt2[0]) + (pt1[1] - pt2[1]) * (pt1[1] - pt2[1])) / 2; const pt1norm = clipped ? [0, 0] : [pt1vec[0] / pt1len * ptlen, pt1vec[1] / pt1len * ptlen]; const pt2norm = clipped ? [0, 0] : [pt2vec[0] / pt2len * ptlen, pt2vec[1] / pt2len * ptlen]; const pt1normlen = Math.sqrt(pt1norm[0] * pt1norm[0] + pt1norm[1] * pt1norm[1]) || 1; const pt2normlen = Math.sqrt(pt2norm[0] * pt2norm[0] + pt2norm[1] * pt2norm[1]) || 1; const pt1normalized = [pt1norm[0] / pt1normlen, pt1norm[1] / pt1normlen]; const pt2normalized = [pt2norm[0] / pt2normlen, pt2norm[1] / pt2normlen]; const aActive = A.isSelected() || Doc.IsBrushed(A.rootDoc); const bActive = B.isSelected() || Doc.IsBrushed(B.rootDoc); const textX = (Math.min(pt1[0], pt2[0]) + Math.max(pt1[0], pt2[0])) / 2 + NumCast(LinkDocs[0].linkOffsetX); const textY = (pt1[1] + pt2[1]) / 2 + NumCast(LinkDocs[0].linkOffsetY); return { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1: [pt1[0] + pt1normalized[0] * 13, pt1[1] + pt1normalized[1] * 13], pt2: [pt2[0] + pt2normalized[0] * 13, pt2[1] + pt2normalized[1] * 13] }; } render() { if (!this.renderData) return (null); const { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1, pt2 } = this.renderData; LinkManager.currentLink = this.props.LinkDocs[0]; const linkRelationship = Field.toString(LinkManager.currentLink?.linkRelationship as any as Field); //get string representing relationship const linkRelationshipList = Doc.UserDoc().linkRelationshipList as List; const linkColorList = Doc.UserDoc().linkColorList as List; const linkRelationshipSizes = Doc.UserDoc().linkRelationshipSizes as List; const currRelationshipIndex = linkRelationshipList.indexOf(linkRelationship); const linkSize = currRelationshipIndex === -1 || currRelationshipIndex >= linkRelationshipSizes.length ? -1 : linkRelationshipSizes[currRelationshipIndex]; //access stroke color using index of the relationship in the color list (default black) const stroke = currRelationshipIndex === -1 || currRelationshipIndex >= linkColorList.length ? "black" : linkColorList[currRelationshipIndex]; // const hexStroke = this.rgbToHex(stroke) //calculate stroke width/thickness based on the relative importance of the relationshipship (i.e. how many links the relationship has) //thickness varies linearly from 3px to 12px for increasing link count const strokeWidth = linkSize === -1 ? "3px" : Math.floor(2 + 10 * (linkSize / Math.max(...linkRelationshipSizes))) + "px"; if (this.props.LinkDocs[0].displayArrow === undefined) { this.props.LinkDocs[0].displayArrow = false; } return this.props.LinkDocs[0].opacity === 0 || !a.width || !b.width || ((!this.props.LinkDocs[0].linkDisplay) && !aActive && !bActive) ? (null) : (<> {textX === undefined ? (null) : {Field.toString(this.props.LinkDocs[0].description as any as Field)} } ); } }