aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
blob: 3e673c0b2bc7c94b48428ab2abad20cf1048bafd (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
import { Mark, ResolvedPos } from "prosemirror-model";
import { EditorState } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { Doc } from "../../../../fields/Doc";
import { DocServer } from "../../../DocServer";
import { LinkDocPreview } from "../LinkDocPreview";
import { FormattedTextBox } from "./FormattedTextBox";
import './FormattedTextBoxComment.scss';
import { schema } from "./schema_rts";

export function findOtherUserMark(marks: Mark[]): Mark | undefined { return marks.find(m => m.attrs.userid && m.attrs.userid !== Doc.CurrentUserEmail); }
export function findUserMark(marks: Mark[]): Mark | undefined { return marks.find(m => m.attrs.userid); }
export function findLinkMark(marks: Mark[]): Mark | undefined { return marks.find(m => m.type === schema.marks.autoLinkAnchor || m.type === schema.marks.linkAnchor); }
export function findStartOfMark(rpos: ResolvedPos, view: EditorView, finder: (marks: Mark[]) => Mark | undefined) {
    let before = 0, nbef = rpos.nodeBefore;
    while (nbef && finder(nbef.marks)) {
        before += nbef.nodeSize;
        rpos = view.state.doc.resolve(rpos.pos - nbef.nodeSize);
        rpos && (nbef = rpos.nodeBefore);
    }
    return before;
}
export function findEndOfMark(rpos: ResolvedPos, view: EditorView, finder: (marks: Mark[]) => Mark | undefined) {
    let after = 0, naft = rpos.nodeAfter;
    while (naft && finder(naft.marks)) {
        after += naft.nodeSize;
        rpos = view.state.doc.resolve(rpos.pos + naft.nodeSize);
        rpos && (naft = rpos.nodeAfter);
    }
    return after;
}

// this view appears when clicking on text that has a hyperlink which is configured to show a preview of its target.
// this will also display metadata information about text when the view is configured to display things like other people who authored text.
// 
export class FormattedTextBoxComment {
    static tooltip: HTMLElement;
    static tooltipText: HTMLElement;
    static startUserMarkRegion: number;
    static endUserMarkRegion: number;
    static userMark: Mark;
    static textBox: FormattedTextBox | undefined;

    constructor(view: any) {
        if (!FormattedTextBoxComment.tooltip) {
            const tooltip = FormattedTextBoxComment.tooltip = document.createElement("div");
            const tooltipText = FormattedTextBoxComment.tooltipText = document.createElement("div");
            tooltip.className = "FormattedTextBox-tooltip";
            tooltipText.className = "FormattedTextBox-tooltipText";
            tooltip.style.display = "none";
            tooltip.appendChild(tooltipText);
            tooltip.onpointerdown = (e: PointerEvent) => {
                const { textBox, startUserMarkRegion, endUserMarkRegion, userMark } = FormattedTextBoxComment;
                false && startUserMarkRegion !== undefined && textBox?.adoptAnnotation(startUserMarkRegion, endUserMarkRegion, userMark);
                e.stopPropagation();
                e.preventDefault();
            };
            document.getElementById("root")?.appendChild(tooltip);
        }
    }
    public static Hide() {
        FormattedTextBoxComment.textBox = undefined;
        FormattedTextBoxComment.tooltip.style.display = "none";
    }
    public static saveMarkRegion(textBox: any, start: number, end: number, mark: Mark) {
        FormattedTextBoxComment.textBox = textBox;
        FormattedTextBoxComment.startUserMarkRegion = start;
        FormattedTextBoxComment.endUserMarkRegion = end;
        FormattedTextBoxComment.userMark = mark;
        FormattedTextBoxComment.tooltip.style.display = "";
    }

    static showCommentbox(view: EditorView, nbef: number) {
        const state = view.state;
        // These are in screen coordinates
        const start = view.coordsAtPos(state.selection.from - nbef), end = view.coordsAtPos(state.selection.from - nbef);
        // The box in which the tooltip is positioned, to use as base
        const box = (document.getElementsByClassName("mainView-container") as any)[0].getBoundingClientRect();
        // Find a center-ish x position from the selection endpoints (when crossing lines, end may be more to the left)
        const left = Math.max((start.left + end.left) / 2, start.left + 3);
        FormattedTextBoxComment.tooltip.style.left = (left - box.left) + "px";
        FormattedTextBoxComment.tooltip.style.bottom = (box.bottom - start.top) + "px";
        FormattedTextBoxComment.tooltip.style.display = "";
    }

    static update(textBox: FormattedTextBox, view: EditorView, lastState?: EditorState, hrefs: string = "", linkDoc: string = "") {
        FormattedTextBoxComment.textBox = textBox;
        if ((hrefs || !lastState?.doc.eq(view.state.doc) || !lastState?.selection.eq(view.state.selection))) {
            FormattedTextBoxComment.setupPreview(view, textBox, hrefs?.trim().split(" ").filter(h => h), linkDoc);
        }
    }

    static setupPreview(view: EditorView, textBox: FormattedTextBox, hrefs?: string[], linkDoc?: string) {
        const state = view.state;
        // this section checks to see if the insertion point is over text entered by a different user.  If so, it sets ths comment text to indicate the user and the modification date
        if (state.selection.$from) {
            const nbef = findStartOfMark(state.selection.$from, view, findOtherUserMark);
            const naft = findEndOfMark(state.selection.$from, view, findOtherUserMark);
            const noselection = state.selection.$from === state.selection.$to;
            let child: any = null;
            state.doc.nodesBetween(state.selection.from, state.selection.to, (node: any, pos: number, parent: any) => !child && node.marks.length && (child = node));
            const mark = child && findOtherUserMark(child.marks);
            if (mark && child && (nbef || naft) && (!mark.attrs.opened || noselection)) {
                FormattedTextBoxComment.saveMarkRegion(textBox, state.selection.$from.pos - nbef, state.selection.$from.pos + naft, mark);
            }
            if (mark && child && ((nbef && naft) || !noselection)) {
                FormattedTextBoxComment.tooltipText.textContent = mark.attrs.userid + " on " + (new Date(mark.attrs.modified * 1000)).toLocaleString();
                FormattedTextBoxComment.showCommentbox(view, nbef);
            } else FormattedTextBoxComment.Hide();
        }

        // this checks if the selection is a hyperlink.  If so, it displays the target doc's text for internal links, and the url of the target for external links. 
        if (state.selection.$from && hrefs?.length) {
            const nbef = findStartOfMark(state.selection.$from, view, findLinkMark);
            const naft = findEndOfMark(state.selection.$from, view, findLinkMark) || nbef;
            nbef && naft && LinkDocPreview.SetLinkInfo({
                docProps: textBox.props,
                linkSrc: textBox.rootDoc,
                linkDoc: linkDoc ? DocServer.GetCachedRefField(linkDoc) as Doc : undefined,
                location: ((pos) => [pos.left, pos.top + 25])(view.coordsAtPos(state.selection.from - Math.max(0, nbef - 1))),
                hrefs,
                showHeader: true
            });
        }
    }

    destroy() { }
}