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 { DocCss, DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { DocCast, NumCast, StrCast } from '../../../fields/Types'; import { DashColor, emptyFunction, lightOrDark, returnFalse } from '../../../Utils'; import { DocumentManager } from '../../util/DocumentManager'; import { LinkManager } from '../../util/LinkManager'; 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 { FieldView, FieldViewProps } from './FieldView'; import './LinkBox.scss'; @observer export class LinkBox extends ViewBoxBaseComponent() { public static LayoutString(fieldKey: string = 'link') { return FieldView.LayoutString(LinkBox, fieldKey); } disposer: IReactionDisposer | undefined; @observable _forceAnimate = 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()); }; componentWillUnmount() { this.disposer?.(); } componentDidMount() { this._props.setContentViewBox?.(this); this.disposer = reaction( () => ({ drag: SnappingManager.IsDragging }), ({ drag }) => { !LightboxView.Contains(this.DocumentView?.()) && setTimeout( // need to wait for drag manager to set 'hidden' flag on dragged DOM elements action(() => { const a = this.anchor1, b = this.anchor2; let a1 = a && document.getElementById(a.Guid); let a2 = b && document.getElementById(b.Guid); // 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 and 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; } }) ); }, { fireImmediately: true } ); } render() { if (this._hide) return null; const a = this.anchor1; const b = this.anchor2; this._forceAnimate; const docView = this._props.docViewPath().lastElement(); if (a && b && !LightboxView.Contains(docView)) { // 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) // 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 targetAhyperlink = Array.from(document.getElementsByClassName(DocCast(this.dataDoc.link_anchor_1)[Id])).lastElement(); const targetBhyperlink = Array.from(document.getElementsByClassName(DocCast(this.dataDoc.link_anchor_2)[Id])).lastElement(); const aid = targetAhyperlink?.id || a.Document[Id]; const bid = targetBhyperlink?.id || b.Document[Id]; if (!document.getElementById(aid) || !document.getElementById(bid)) { setTimeout(action(() => (this._forceAnimate = this._forceAnimate + 0.01))); return null; } 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={{}} /> ); } return (
); } }