import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import Xarrow from 'react-xarrows'; import { FieldResult } from '../../../fields/Doc'; import { DocCss, DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { DocCast, NumCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; import { DashColor, emptyFunction, lightOrDark, returnFalse } from '../../../Utils'; import { DocumentManager } from '../../util/DocumentManager'; import { SnappingManager } from '../../util/SnappingManager'; import { ViewBoxBaseComponent } from '../DocComponent'; import { EditableView } from '../EditableView'; import { LightboxView } from '../LightboxView'; import { StyleProp } from '../StyleProvider'; import { ComparisonBox } from './ComparisonBox'; import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import './LinkBox.scss'; @observer export class LinkBox extends ViewBoxBaseComponent() { public static LayoutString(fieldKey: string = 'link') { return FieldView.LayoutString(LinkBox, fieldKey); } _disposers: { [name: string]: IReactionDisposer } = {}; @observable _forceAnimate: number = 0; // forces xArrow to animate when a transition animation is detected on something that affects an anchor @observable _hide = false; // don't render if anchor is not visible since that breaks xAnchor constructor(props: FieldViewProps) { super(props); makeObservable(this); } @computed get anchor1() { return this.anchor(1); } // prettier-ignore @computed get anchor2() { return this.anchor(2); } // prettier-ignore anchor = (which: number) => { const anch = DocCast(this.dataDoc['link_anchor_' + which]); const anchor = anch?.layout_unrendered ? DocCast(anch.annotationOn) : anch; return DocumentManager.Instance.getDocumentView(anchor, this.DocumentView?.().containerViewPath?.().lastElement()); }; _hackToSeeIfDeleted: any; componentWillUnmount() { this._hackToSeeIfDeleted && clearTimeout(this._hackToSeeIfDeleted); Object.keys(this._disposers).forEach(key => this._disposers[key]()); } componentDidMount() { this._props.setContentViewBox?.(this); this._disposers.deleting = reaction( () => !this.anchor1 && !this.anchor2 && this.DocumentView?.() && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView!())), empty => empty && ((this._hackToSeeIfDeleted = setTimeout(() => (!this.anchor1 && !this.anchor2) && this._props.removeDocument?.(this.Document) )), 1000) // prettier-ignore ); this._disposers.dragging = reaction( () => SnappingManager.IsDragging, () => setTimeout( action(() => {// need to wait for drag manager to set 'hidden' flag on dragged DOM elements const a = this.anchor1, b = this.anchor2; let a1 = a && document.getElementById(a.ViewGuid); let a2 = b && document.getElementById(b.ViewGuid); // test whether the anchors themselves are hidden,... if (!a1 || !a2 || (a?.ContentDiv as any)?.hidden || (b?.ContentDiv as any)?.hidden) this._hide = true; else { // .. or whether any of their DOM parents are hidden for (; a1 && !a1.hidden; a1 = a1.parentElement); for (; a2 && !a2.hidden; a2 = a2.parentElement); this._hide = a1 || a2 ? true : false; } })) // prettier-ignore ); } render() { TraceMobx(); if (this._hide) return null; const a = this.anchor1; const b = this.anchor2; this._forceAnimate; const docView = this._props.docViewPath().lastElement(); if (a && b) { // text selection bounds are not directly observable, so we have to // force an update when anything that could affect them changes (text edits causing reflow, scrolling) a.Document[a.LayoutFieldKey]; b.Document[b.LayoutFieldKey]; a.Document.layout_scrollTop; b.Document.layout_scrollTop; a.Document[DocCss]; b.Document[DocCss]; const axf = a.screenToViewTransform(); // these force re-render when a or b moves (so do NOT remove) const bxf = b.screenToViewTransform(); const scale = docView?.screenToViewTransform().Scale ?? 1; const at = a.getBounds?.transition; // these force re-render when a or b change size and at the end of an animated transition const bt = b.getBounds?.transition; // inquring getBounds() also causes text anchors to update whether or not they reflow (any size change triggers an invalidation) var foundParent = false; const getAnchor = (field: FieldResult): Element[] => { const docField = DocCast(field); const doc = docField?.layout_unrendered ? DocCast(docField.annotationOn, docField) : docField; const ele = document.getElementById(DocumentView.UniquifyId(LightboxView.Contains(this.DocumentView?.()), doc[Id])); if (ele?.className === 'linkBox-label') foundParent = true; if (ele?.getBoundingClientRect().width) return [ele]; const eles = Array.from(document.getElementsByClassName(doc[Id])).filter(ele => ele?.getBoundingClientRect().width); const annoOn = DocCast(doc.annotationOn); if (eles.length || !annoOn) return eles; const pareles = getAnchor(annoOn); foundParent = pareles.length ? true : false; return pareles; }; // if there's an element in the DOM with a classname containing a link anchor's id (eg a hypertext ), // 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 targetAhyperlinks = getAnchor(this.dataDoc.link_anchor_1); const targetBhyperlinks = getAnchor(this.dataDoc.link_anchor_2); const container = this.DocumentView?.().containerViewPath?.().lastElement()?.ContentDiv; const aid = targetAhyperlinks?.find(alink => container?.contains(alink))?.id ?? targetAhyperlinks?.lastElement()?.id; const bid = targetBhyperlinks?.find(blink => container?.contains(blink))?.id ?? targetBhyperlinks?.lastElement()?.id; if (!aid || !bid) { setTimeout(action(() => (this._forceAnimate = this._forceAnimate + 0.01))); return null; } if (foundParent) { setTimeout( action(() => (this._forceAnimate = this._forceAnimate + 0.01)), 1 ); } if (at || bt) setTimeout(action(() => (this._forceAnimate = this._forceAnimate + 0.01))); // this forces an update during a transition animation const highlight = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Highlighting); const highlightColor = highlight?.highlightIndex ? highlight?.highlightColor : undefined; const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); const fontFamily = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily); const fontSize = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize); const fontColor = (c => (c !== 'transparent' ? c : undefined))(StrCast(this.layoutDoc.link_fontColor)); const { stroke_markerScale, stroke_width, stroke_startMarker, stroke_endMarker, stroke_dash } = this.Document; const strokeWidth = NumCast(stroke_width, 4); const linkDesc = StrCast(this.dataDoc.link_description) || ' '; const labelText = linkDesc.substring(0, 50) + (linkDesc.length > 50 ? '...' : ''); return ( <> {!highlightColor ? null : ( )} linkDesc} SetValue={action(val => { this.Document[DocData].link_description = val; return true; })} /> {/* (this.Document[DocData].link_description = val))} fillWidth /> */} } passProps={{}} /> ); } setTimeout( action(() => (this._forceAnimate = this._forceAnimate + 1)), 2 ); return (
); } }