aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
blob: 4995925c8f44e1a8c12931c78930e0ea200e2e3a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import { Doc, Field } from '../../../../fields/Doc';
import { Brushed, DocCss } from '../../../../fields/DocSymbols';
import { Id } from '../../../../fields/FieldSymbols';
import { List } from '../../../../fields/List';
import { Cast, NumCast, StrCast } from '../../../../fields/Types';
import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../Utils';
import { LinkManager } from '../../../util/LinkManager';
import { SelectionManager } from '../../../util/SelectionManager';
import { SettingsManager } from '../../../util/SettingsManager';
import { SnappingManager } from '../../../util/SnappingManager';
import { Colors } from '../../global/globalEnums';
import { DocumentView } from '../../nodes/DocumentView';
import './CollectionFreeFormLinkView.scss';
import * as React from 'react';

export interface CollectionFreeFormLinkViewProps {
    A: DocumentView;
    B: DocumentView;
    LinkDocs: Doc[];
}

// props.screentolocatransform

@observer
export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> {
    @observable _opacity: number = 0;
    @observable _start = 0;
    _anchorDisposer: IReactionDisposer | undefined;
    _timeout: NodeJS.Timeout | undefined;
    componentWillUnmount() {
        this._anchorDisposer?.();
    }
    @action timeout: any = 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.Document, Doc, null)?.link_anchor_1, Doc, null)?.annotationOn, Doc, null)?.layout_scrollTop,
                Cast(Cast(Cast(this.props.A.Document, Doc, null)?.link_anchor_1, Doc, null)?.annotationOn, Doc, null)?.[DocCss],
                this.props.B.props.ScreenToLocalTransform(),
                Cast(Cast(Cast(this.props.A.Document, Doc, null)?.link_anchor_2, Doc, null)?.annotationOn, Doc, null)?.layout_scrollTop,
                Cast(Cast(Cast(this.props.A.Document, Doc, null)?.link_anchor_2, Doc, null)?.annotationOn, Doc, null)?.[DocCss],
            ],
            action(() => {
                this._start = Date.now();
                this._timeout && clearTimeout(this._timeout);
                this._timeout = setTimeout(this.timeout, 25);
                setTimeout(this.placeAnchors, 10); // when docs are dragged, their transforms will update before a render has been performed.  placeanchors needs to come after a render to find things in the dom.  a 0 timeout will still come before the render
            }),
            { 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.link_displayLine || Doc.UserDoc().showLinkLines)) && (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.link_anchor_1 as Doc)[Id])).lastElement();
        const targetBhyperlink = Array.from(window.document.getElementsByClassName((linkDoc.link_anchor_2 as Doc)[Id])).lastElement();
        if ((!targetAhyperlink && !a.width) || (!targetBhyperlink && !b.width)) return;
        if (!targetAhyperlink) {
            if (linkDoc.link_autoMoveAnchors) {
                linkDoc.link_anchor_1_x = ((apt.point.x - aleft) / awidth) * 100;
                linkDoc.link_anchor_1_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.link_anchor_1_x = mpx * 100;
            if (mpy >= 0 && mpy <= 1) linkDoc.link_anchor_1_y = mpy * 100;
            if (getComputedStyle(targetAhyperlink).fontSize === '0px') linkDoc.opacity = 0;
            else linkDoc.opacity = 1;
        }
        if (!targetBhyperlink) {
            if (linkDoc.link_autoMoveAnchors) {
                linkDoc.link_anchor_2_x = ((bpt.point.x - bleft) / bwidth) * 100;
                linkDoc.link_anchor_2_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.link_anchor_2_x = mpx * 100;
            if (mpy >= 0 && mpy <= 1) linkDoc.link_anchor_2_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].link_relationship_OffsetX = NumCast(this.props.LinkDocs[0].link_relationship_OffsetX) + delta[0];
                this.props.LinkDocs[0].link_relationship_OffsetY = NumCast(this.props.LinkDocs[0].link_relationship_OffsetY) + delta[1];
                return false;
            },
            emptyFunction,
            action(() => {
                SelectionManager.DeselectAll();
                SelectionManager.SelectSchemaViewDoc(this.props.LinkDocs[0], true);
                LinkManager.currentLink = this.props.LinkDocs[0];
                this.toggleProperties();
                // OverlayView.Instance.addElement(
                //     <LinkEditor sourceDoc={this.props.A.props.Document} linkDoc={this.props.LinkDocs[0]}
                //         showLinks={action(() => { })}
                //     />, { 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) {
            if (el.className === 'tabDocView-content') break;
            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 ((SettingsManager.propertiesWidth ?? 0) < 100) {
            SettingsManager.propertiesWidth = 250;
        }
    };

    @action
    onClickLine = () => {
        SelectionManager.DeselectAll();
        SelectionManager.SelectSchemaViewDoc(this.props.LinkDocs[0], true);
        LinkManager.currentLink = this.props.LinkDocs[0];
        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 aclipped = aleft !== a.left || atop !== a.top;
        const bclipped = bleft !== b.left || btop !== b.top;
        if (aclipped && bclipped) return undefined;
        const clipped = aclipped || bclipped;
        const pt1inside = NumCast(LinkDocs[0].link_anchor_1_x) % 100 !== 0 && NumCast(LinkDocs[0].link_anchor_1_y) % 100 !== 0;
        const pt2inside = NumCast(LinkDocs[0].link_anchor_2_x) % 100 !== 0 && NumCast(LinkDocs[0].link_anchor_2_y) % 100 !== 0;
        const pt1 = [aleft + a.width / 2, atop + a.height / 2];
        const pt2 = [bleft + b.width / 2, btop + b.width / 2];
        const pt2vec = pt2inside ? [-0.7071, 0.7071] : [(bDocBounds.left + bDocBounds.right) / 2 - pt2[0], (bDocBounds.top + bDocBounds.bottom) / 2 - pt2[1]];
        const pt1vec = pt1inside ? [-0.7071, 0.7071] : [(aDocBounds.left + aDocBounds.right) / 2 - pt1[0], (aDocBounds.top + aDocBounds.bottom) / 2 - pt1[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.SELECTED || A.Document[Brushed];
        const bActive = B.SELECTED || B.Document[Brushed];

        const textX = (Math.min(pt1[0], pt2[0]) + Math.max(pt1[0], pt2[0])) / 2 + NumCast(LinkDocs[0].link_relationship_OffsetX);
        const textY = (pt1[1] + pt2[1]) / 2 + NumCast(LinkDocs[0].link_relationship_OffsetY);
        const link = this.props.LinkDocs[0];
        return {
            a,
            b,
            pt1norm,
            pt2norm,
            aActive,
            bActive,
            textX,
            textY,
            // fully connected
            // pt1,
            // pt2,
            // this code adds space between links
            pt1: link.link_displayArrow ? [pt1[0] + pt1normalized[0] * 3 * NumCast(link.link_displayArrow_scale, 4), pt1[1] + pt1normalized[1] * 3 * NumCast(link.link_displayArrow_scale, 3)] : pt1,
            pt2: link.link_displayArrow ? [pt2[0] + pt2normalized[0] * 3 * NumCast(link.link_displayArrow_scale, 4), pt2[1] + pt2normalized[1] * 3 * NumCast(link.link_displayArrow_scale, 3)] : pt2,
        };
    }

    render() {
        if (!this.renderData) return null;

        const link = this.props.LinkDocs[0];
        const { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1, pt2 } = this.renderData;
        const linkRelationship = Field.toString(link?.link_relationship as any as Field); //get string representing relationship
        const linkRelationshipList = Doc.UserDoc().link_relationshipList as List<string>;
        const linkColorList = Doc.UserDoc().link_ColorList as List<string>;
        const linkRelationshipSizes = Doc.UserDoc().link_relationshipSizes as List<number>;
        const currRelationshipIndex = linkRelationshipList.indexOf(linkRelationship);
        const linkDescription = Field.toString(link.link_description as any as Field).split('\n')[0];

        const linkSize = Doc.noviceMode || 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 ? StrCast(link._backgroundColor, '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';

        const arrowScale = NumCast(link.link_displayArrow_scale, 3);
        return link.opacity === 0 || !a.width || !b.width || (!(Doc.UserDoc().showLinkLines || link.link_displayLine) && !aActive && !bActive) ? null : (
            <>
                <defs>
                    <marker id={`${link[Id] + 'arrowhead'}`} markerWidth={`${4 * arrowScale}`} markerHeight={`${3 * arrowScale}`} refX="0" refY={`${1.5 * arrowScale}`} orient="auto">
                        <polygon points={`0 0, ${3 * arrowScale} ${1.5 * arrowScale}, 0 ${3 * arrowScale}`} fill={stroke} />
                    </marker>
                    <filter id="outline">
                        <feMorphology in="SourceAlpha" result="expanded" operator="dilate" radius="1" />
                        <feFlood floodColor={`${Colors.DARK_GRAY}`} />
                        <feComposite in2="expanded" operator="in" />
                        <feComposite in="SourceGraphic" />
                    </filter>
                    <filter x="0" y="0" width="1" height="1" id={`${link[Id] + 'background'}`}>
                        <feFlood floodColor={`${StrCast(link._backgroundColor, 'white')}`} result="bg" />
                        <feMerge>
                            <feMergeNode in="bg" />
                            <feMergeNode in="SourceGraphic" />
                        </feMerge>
                    </filter>
                </defs>
                <path
                    filter={LinkManager.currentLink === link ? 'url(#outline)' : ''}
                    fill="pink"
                    stroke="antiquewhite"
                    strokeWidth="4"
                    className="collectionfreeformlinkview-linkLine"
                    style={{ pointerEvents: 'visibleStroke', opacity: this._opacity, stroke, strokeWidth }}
                    onClick={this.onClickLine}
                    d={`M ${pt1[0]} ${pt1[1]} C ${pt1[0] + pt1norm[0]} ${pt1[1] + pt1norm[1]}, ${pt2[0] + pt2norm[0]} ${pt2[1] + pt2norm[1]}, ${pt2[0]} ${pt2[1]}`}
                    markerEnd={link.link_displayArrow ? `url(#${link[Id] + 'arrowhead'})` : ''}
                />
                {textX === undefined || !linkDescription ? null : (
                    <text filter={`url(#${link[Id] + 'background'})`} className="collectionfreeformlinkview-linkText" x={textX} y={textY} onPointerDown={this.pointerDown}>
                        <tspan>&nbsp;</tspan>
                        <tspan dy="2">{linkDescription.substring(0, 50) + (linkDescription.length > 50 ? '...' : '')}</tspan>
                        <tspan dy="2">&nbsp;</tspan>
                    </text>
                )}
            </>
        );
    }
}