From 80d4bf70cb8e29f15d8e51b1e0bd378a26d68fcf Mon Sep 17 00:00:00 2001
From: vellichora <fangrui_tong@brown.edu>
Date: Tue, 7 Jan 2020 03:07:18 -0500
Subject: converted tool tip text menu to react component with basic marks and
 dropdown

---
 src/client/util/RichTextMenu.tsx | 337 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 337 insertions(+)
 create mode 100644 src/client/util/RichTextMenu.tsx

(limited to 'src/client/util/RichTextMenu.tsx')

diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx
new file mode 100644
index 000000000..7e20d126c
--- /dev/null
+++ b/src/client/util/RichTextMenu.tsx
@@ -0,0 +1,337 @@
+import React = require("react");
+import AntimodeMenu from "../views/AntimodeMenu";
+import { observable, action, } from "mobx";
+import { observer } from "mobx-react";
+import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos, Schema } from "prosemirror-model";
+import { schema } from "./RichTextSchema";
+import { EditorView } from "prosemirror-view";
+import { EditorState, NodeSelection } from "prosemirror-state";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
+import { faBold, faItalic, faUnderline, faStrikethrough, faSubscript, faSuperscript } from "@fortawesome/free-solid-svg-icons";
+import { MenuItem, Dropdown } from "prosemirror-menu";
+import { updateBullets } from "./ProsemirrorExampleTransfer";
+import { FieldViewProps } from "../views/nodes/FieldView";
+import { NumCast } from "../../new_fields/Types";
+import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox";
+import { unimplementedFunction } from "../../Utils";
+const { toggleMark, setBlockType } = require("prosemirror-commands");
+
+library.add(faBold, faItalic, faUnderline, faStrikethrough, faSuperscript, faSubscript);
+
+@observer
+export default class RichTextMenu extends AntimodeMenu {
+    static Instance: RichTextMenu;
+
+    private view?: EditorView;
+    private editorProps: FieldViewProps & FormattedTextBoxProps | undefined;
+
+    private _marksToDoms: Map<Mark, JSX.Element> = new Map();
+
+    @observable private activeFontSize: string = "";
+    @observable private activeFontFamily: string = "";
+
+    constructor(props: Readonly<{}>) {
+        super(props);
+        RichTextMenu.Instance = this;
+    }
+
+    @action
+    changeView(view: EditorView) {
+        this.view = view;
+    }
+
+    // update() {
+    //     console.log("update");
+    // }
+
+    update(view: EditorView, lastState: EditorState | undefined) {
+        console.log("update");
+        this.updateFromDash(view, lastState, this.editorProps);
+    }
+
+    @action
+    public async updateFromDash(view: EditorView, lastState: EditorState | undefined, props: any) {
+        if (!view) {
+            console.log("no editor?  why?");
+            return;
+        }
+        this.view = view;
+        const state = view.state;
+        console.log("update from dash");
+        // DocumentDecorations.Instance.showTextBar();
+        props && (this.editorProps = props);
+        // // Don't do anything if the document/selection didn't change
+        // if (lastState && lastState.doc.eq(state.doc) &&
+        //     lastState.selection.eq(state.selection)) return;
+
+        // this.reset_mark_doms();
+
+        // // update link dropdown
+        // const linkDropdown = await this.createLinkDropdown();
+        // const newLinkDropdowndom = linkDropdown.render(this.view).dom;
+        // this._linkDropdownDom && this.tooltip.replaceChild(newLinkDropdowndom, this._linkDropdownDom);
+        // this._linkDropdownDom = newLinkDropdowndom;
+
+        // update active font family and size
+        const active = this.getActiveFontStylesOnSelection();
+        const activeFamilies = active && active.get("families");
+        const activeSizes = active && active.get("sizes");
+
+        this.activeFontFamily = !activeFamilies || activeFamilies.length == 0 ? "default" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various";
+        this.activeFontSize = !activeSizes || activeSizes.length == 0 ? "default" : activeSizes.length === 1 ? String(activeSizes[0]) + "pt" : "various";
+
+        // this.update_mark_doms();
+    }
+
+
+    destroy() {
+        console.log("destroy");
+    }
+
+    createButton(faIcon: string, title: string, command: any) {
+        const self = this;
+        function onClick(e: React.PointerEvent) {
+            // dom.addEventListener("pointerdown", e => {
+            e.preventDefault();
+            self.view && self.view.focus();
+            //     if (dom.contains(e.target as Node)) {
+            e.stopPropagation();
+            //         command(this.view.state, this.view.dispatch, this.view);
+            //     }
+            // });
+            self.view && command(self.view!.state, self.view!.dispatch, self.view);
+        }
+
+        return (
+            <button className="antimodeMenu-button" title="title" onPointerDown={onClick}>
+                <FontAwesomeIcon icon={faIcon as IconProp} size="lg" />
+            </button>
+        );
+    }
+
+    createMarksDropdown(activeOption: string, options: { mark: Mark | null, title: string, label: string, command: (mark: Mark, view: EditorView) => void, hidden?: boolean }[]): JSX.Element {
+        let items = options.map(({ title, label, hidden }) => {
+            if (hidden) {
+                return label === activeOption ?
+                    <option value={label} title={title} selected hidden>{label}</option> :
+                    <option value={label} title={title} hidden>{label}</option>;
+            }
+            return label === activeOption ?
+                <option value={label} title={title} selected>{label}</option> :
+                <option value={label} title={title}>{label}</option>;
+        });
+
+        const self = this;
+        function onChange(val: string) {
+            options.forEach(({ label, mark, command }) => {
+                if (val === label) {
+                    self.view && mark && command(mark, self.view);
+                }
+            });
+        }
+        return <select onChange={e => onChange(e.target.value)}>{items}</select>;
+
+        // let items: MenuItem[] = [];
+        // options.forEach(({ mark, title, label, command }) => {
+        //     const self = this;
+        //     function onSelect() {
+        //         self.view && command(mark, self.view);
+        //     }
+        //     // this.createMarksOption("Set font size", String(mark.attrs.fontSize), onSelect)
+        //     items.push(
+        //         new MenuItem({
+        //             title: title,
+        //             label: label,
+        //             execEvent: "",
+        //             class: "dropdown-item",
+        //             css: "",
+        //             run() { onSelect(); }
+        //         })
+        //     );
+        // });
+
+        // return <div>{(new Dropdown(items, { label: label }) as MenuItem).render(this.view!).dom}</div>;
+    }
+
+    setMark = (mark: Mark, state: EditorState<any>, dispatch: any) => {
+        if (mark) {
+            const node = (state.selection as NodeSelection).node;
+            if (node?.type === schema.nodes.ordered_list) {
+                let attrs = node.attrs;
+                if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, setFontFamily: mark.attrs.family };
+                if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, setFontSize: mark.attrs.fontSize };
+                if (mark.type === schema.marks.pFontColor) attrs = { ...attrs, setFontColor: mark.attrs.color };
+                const tr = updateBullets(state.tr.setNodeMarkup(state.selection.from, node.type, attrs), state.schema);
+                dispatch(tr.setSelection(new NodeSelection(tr.doc.resolve(state.selection.from))));
+            } else {
+                toggleMark(mark.type, mark.attrs)(state, (tx: any) => {
+                    const { from, $from, to, empty } = tx.selection;
+                    if (!tx.doc.rangeHasMark(from, to, mark.type)) {
+                        toggleMark(mark.type, mark.attrs)({ tr: tx, doc: tx.doc, selection: tx.selection, storedMarks: tx.storedMarks }, dispatch);
+                    } else dispatch(tx);
+                });
+            }
+        }
+    }
+
+    changeFontSize = (mark: Mark, view: EditorView) => {
+        console.log("change font size!!");
+        const size = mark.attrs.fontSize;
+        if (this.editorProps) {
+            const ruleProvider = this.editorProps.ruleProvider;
+            const heading = NumCast(this.editorProps.Document.heading);
+            if (ruleProvider && heading) {
+                ruleProvider["ruleSize_" + heading] = size;
+            }
+        }
+        this.setMark(view.state.schema.marks.pFontSize.create({ fontSize: size }), view.state, view.dispatch);
+    }
+
+    changeFontFamily = (mark: Mark, view: EditorView) => {
+        const fontName = mark.attrs.family;
+        // if (fontName) { this.updateFontStyleDropdown(fontName); }
+        if (this.editorProps) {
+            const ruleProvider = this.editorProps.ruleProvider;
+            const heading = NumCast(this.editorProps.Document.heading);
+            if (ruleProvider && heading) {
+                ruleProvider["ruleFont_" + heading] = fontName;
+            }
+        }
+        this.setMark(view.state.schema.marks.pFontFamily.create({ family: fontName }), view.state, view.dispatch);
+    }
+
+    // finds font sizes and families in selection
+    getActiveFontStylesOnSelection() {
+        if (!this.view) return;
+
+        const activeFamilies: string[] = [];
+        const activeSizes: string[] = [];
+        const state = this.view.state;
+        const pos = this.view.state.selection.$from;
+        const ref_node = this.reference_node(pos);
+        if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) {
+            ref_node.marks.forEach(m => {
+                m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family);
+                m.type === state.schema.marks.pFontSize && activeSizes.push(String(m.attrs.fontSize) + "pt");
+            });
+        }
+
+        let styles = new Map<String, String[]>();
+        styles.set("families", activeFamilies);
+        styles.set("sizes", activeSizes);
+        return styles;
+    }
+
+    // changeToFontSize = (mark: Mark, view: EditorView) => {
+    //     const size = mark.attrs.fontSize;
+    //     if (size) { this.updateFontSizeDropdown(String(size) + " pt"); }
+    //     if (this.editorProps) {
+    //         const ruleProvider = this.editorProps.ruleProvider;
+    //         const heading = NumCast(this.editorProps.Document.heading);
+    //         if (ruleProvider && heading) {
+    //             ruleProvider["ruleSize_" + heading] = size;
+    //         }
+    //     }
+    //     this.setMark(view.state.schema.marks.pFontSize.create({ fontSize: size }), view.state, view.dispatch);
+    // }
+
+    reference_node(pos: ResolvedPos<any>): ProsNode | null {
+        if (!this.view) return null;
+
+        let ref_node: ProsNode = this.view.state.doc;
+        if (pos.nodeBefore !== null && pos.nodeBefore !== undefined) {
+            ref_node = pos.nodeBefore;
+        }
+        else if (pos.nodeAfter !== null && pos.nodeAfter !== undefined) {
+            ref_node = pos.nodeAfter;
+        }
+        else if (pos.pos > 0) {
+            let skip = false;
+            for (let i: number = pos.pos - 1; i > 0; i--) {
+                this.view.state.doc.nodesBetween(i, pos.pos, (node: ProsNode) => {
+                    if (node.isLeaf && !skip) {
+                        ref_node = node;
+                        skip = true;
+                    }
+
+                });
+            }
+        }
+        if (!ref_node.isLeaf && ref_node.childCount > 0) {
+            ref_node = ref_node.child(0);
+        }
+        return ref_node;
+    }
+
+    render() {
+        console.log("render");
+        const fontSizeOptions = [
+            { mark: schema.marks.pFontSize.create({ fontSize: 7 }), title: "Set font size", label: "7pt", command: this.changeFontSize },
+            { mark: schema.marks.pFontSize.create({ fontSize: 8 }), title: "Set font size", label: "8pt", command: this.changeFontSize },
+            { mark: schema.marks.pFontSize.create({ fontSize: 9 }), title: "Set font size", label: "8pt", command: this.changeFontSize },
+            { mark: schema.marks.pFontSize.create({ fontSize: 10 }), title: "Set font size", label: "10pt", command: this.changeFontSize },
+            { mark: schema.marks.pFontSize.create({ fontSize: 12 }), title: "Set font size", label: "12pt", command: this.changeFontSize },
+            { mark: schema.marks.pFontSize.create({ fontSize: 14 }), title: "Set font size", label: "14pt", command: this.changeFontSize },
+            { mark: schema.marks.pFontSize.create({ fontSize: 16 }), title: "Set font size", label: "16pt", command: this.changeFontSize },
+            { mark: schema.marks.pFontSize.create({ fontSize: 18 }), title: "Set font size", label: "18pt", command: this.changeFontSize },
+            { mark: schema.marks.pFontSize.create({ fontSize: 20 }), title: "Set font size", label: "20pt", command: this.changeFontSize },
+            { mark: schema.marks.pFontSize.create({ fontSize: 24 }), title: "Set font size", label: "24pt", command: this.changeFontSize },
+            { mark: schema.marks.pFontSize.create({ fontSize: 32 }), title: "Set font size", label: "32pt", command: this.changeFontSize },
+            { mark: schema.marks.pFontSize.create({ fontSize: 48 }), title: "Set font size", label: "48pt", command: this.changeFontSize },
+            { mark: schema.marks.pFontSize.create({ fontSize: 72 }), title: "Set font size", label: "72pt", command: this.changeFontSize },
+            { mark: null, title: "", label: "various", command: unimplementedFunction, hidden: true },
+            { mark: null, title: "", label: "default", command: unimplementedFunction, hidden: true },
+        ]
+
+        const fontFamilyOptions = [
+            { mark: schema.marks.pFontFamily.create({ family: "Times New Roman" }), title: "Set font family", label: "Times New Roman", command: this.changeFontFamily },
+            { mark: schema.marks.pFontFamily.create({ family: "Arial" }), title: "Set font family", label: "Arial", command: this.changeFontFamily },
+            { mark: schema.marks.pFontFamily.create({ family: "Georgia" }), title: "Set font family", label: "Georgia", command: this.changeFontFamily },
+            { mark: schema.marks.pFontFamily.create({ family: "Comic Sans MS" }), title: "Set font family", label: "Comic Sans MS", command: this.changeFontFamily },
+            { mark: schema.marks.pFontFamily.create({ family: "Tahoma" }), title: "Set font family", label: "Tahoma", command: this.changeFontFamily },
+            { mark: schema.marks.pFontFamily.create({ family: "Impact" }), title: "Set font family", label: "Impact", command: this.changeFontFamily },
+            { mark: schema.marks.pFontFamily.create({ family: "Crimson Text" }), title: "Set font family", label: "Crimson Text", command: this.changeFontFamily },
+            { mark: null, title: "", label: "various", command: unimplementedFunction, hidden: true },
+            { mark: null, title: "", label: "default", command: unimplementedFunction, hidden: true },
+        ]
+
+        const buttons = [
+            this.createButton("bold", "Bold", toggleMark(schema.marks.strong)),
+            this.createButton("italic", "Italic", toggleMark(schema.marks.em)),
+            this.createButton("underline", "Underline", toggleMark(schema.marks.underline)),
+            this.createButton("strikethrough", "Strikethrough", toggleMark(schema.marks.strikethrough)),
+            this.createButton("superscript", "Superscript", toggleMark(schema.marks.superscript)),
+            this.createButton("subscript", "Subscript", toggleMark(schema.marks.subscript)),
+            this.createMarksDropdown(this.activeFontSize, fontSizeOptions),
+            this.createMarksDropdown(this.activeFontFamily, fontFamilyOptions),
+        ];
+
+        // this._marksToDoms = new Map();
+        // items.forEach(({ title, dom, command }) => {
+        //     // this.tooltip.appendChild(dom);
+        //     switch (title) {
+        //         case "Bold":
+        //             this._marksToDoms.set(schema.mark(schema.marks.strong), dom);
+        //             // this.basicTools && this.basicTools.appendChild(dom.cloneNode(true));
+        //             break;
+        //         case "Italic":
+        //             this._marksToDoms.set(schema.mark(schema.marks.em), dom);
+        //             // this.basicTools && this.basicTools.appendChild(dom.cloneNode(true));
+        //             break;
+        //     }
+
+        //     //pointer down handler to activate button effects
+        //     dom.addEventListener("pointerdown", e => {
+        //         e.preventDefault();
+        //         this.view.focus();
+        //         if (dom.contains(e.target as Node)) {
+        //             e.stopPropagation();
+        //             command(this.view.state, this.view.dispatch, this.view);
+        //         }
+        //     });
+        // });
+
+        return this.getElement(buttons);
+    }
+}
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 7d58927ba9da5ea146ef6daed336036b01447302 Mon Sep 17 00:00:00 2001
From: vellichora <fangrui_tong@brown.edu>
Date: Tue, 7 Jan 2020 05:23:53 -0500
Subject: added summarizing button to richtextmenu

---
 src/client/util/RichTextMenu.tsx | 215 ++++++++++++++++++++++++---------------
 1 file changed, 134 insertions(+), 81 deletions(-)

(limited to 'src/client/util/RichTextMenu.tsx')

diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx
index 7e20d126c..6b6a2620e 100644
--- a/src/client/util/RichTextMenu.tsx
+++ b/src/client/util/RichTextMenu.tsx
@@ -8,16 +8,17 @@ import { EditorView } from "prosemirror-view";
 import { EditorState, NodeSelection } from "prosemirror-state";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
-import { faBold, faItalic, faUnderline, faStrikethrough, faSubscript, faSuperscript } from "@fortawesome/free-solid-svg-icons";
+import { faBold, faItalic, faUnderline, faStrikethrough, faSubscript, faSuperscript, faIndent } from "@fortawesome/free-solid-svg-icons";
 import { MenuItem, Dropdown } from "prosemirror-menu";
 import { updateBullets } from "./ProsemirrorExampleTransfer";
 import { FieldViewProps } from "../views/nodes/FieldView";
 import { NumCast } from "../../new_fields/Types";
 import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox";
 import { unimplementedFunction } from "../../Utils";
+import { wrapInList } from "prosemirror-schema-list";
 const { toggleMark, setBlockType } = require("prosemirror-commands");
 
-library.add(faBold, faItalic, faUnderline, faStrikethrough, faSuperscript, faSubscript);
+library.add(faBold, faItalic, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent);
 
 @observer
 export default class RichTextMenu extends AntimodeMenu {
@@ -26,10 +27,9 @@ export default class RichTextMenu extends AntimodeMenu {
     private view?: EditorView;
     private editorProps: FieldViewProps & FormattedTextBoxProps | undefined;
 
-    private _marksToDoms: Map<Mark, JSX.Element> = new Map();
-
     @observable private activeFontSize: string = "";
     @observable private activeFontFamily: string = "";
+    @observable private activeListType: string = "";
 
     constructor(props: Readonly<{}>) {
         super(props);
@@ -41,12 +41,7 @@ export default class RichTextMenu extends AntimodeMenu {
         this.view = view;
     }
 
-    // update() {
-    //     console.log("update");
-    // }
-
     update(view: EditorView, lastState: EditorState | undefined) {
-        console.log("update");
         this.updateFromDash(view, lastState, this.editorProps);
     }
 
@@ -58,7 +53,6 @@ export default class RichTextMenu extends AntimodeMenu {
         }
         this.view = view;
         const state = view.state;
-        console.log("update from dash");
         // DocumentDecorations.Instance.showTextBar();
         props && (this.editorProps = props);
         // // Don't do anything if the document/selection didn't change
@@ -84,14 +78,57 @@ export default class RichTextMenu extends AntimodeMenu {
         // this.update_mark_doms();
     }
 
+    setMark = (mark: Mark, state: EditorState<any>, dispatch: any) => {
+        if (mark) {
+            const node = (state.selection as NodeSelection).node;
+            if (node?.type === schema.nodes.ordered_list) {
+                let attrs = node.attrs;
+                if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, setFontFamily: mark.attrs.family };
+                if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, setFontSize: mark.attrs.fontSize };
+                if (mark.type === schema.marks.pFontColor) attrs = { ...attrs, setFontColor: mark.attrs.color };
+                const tr = updateBullets(state.tr.setNodeMarkup(state.selection.from, node.type, attrs), state.schema);
+                dispatch(tr.setSelection(new NodeSelection(tr.doc.resolve(state.selection.from))));
+            } else {
+                toggleMark(mark.type, mark.attrs)(state, (tx: any) => {
+                    const { from, $from, to, empty } = tx.selection;
+                    if (!tx.doc.rangeHasMark(from, to, mark.type)) {
+                        toggleMark(mark.type, mark.attrs)({ tr: tx, doc: tx.doc, selection: tx.selection, storedMarks: tx.storedMarks }, dispatch);
+                    } else dispatch(tx);
+                });
+            }
+        }
+    }
+
+    // finds font sizes and families in selection
+    getActiveFontStylesOnSelection() {
+        if (!this.view) return;
+
+        const activeFamilies: string[] = [];
+        const activeSizes: string[] = [];
+        const state = this.view.state;
+        const pos = this.view.state.selection.$from;
+        const ref_node = this.reference_node(pos);
+        if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) {
+            ref_node.marks.forEach(m => {
+                m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family);
+                m.type === state.schema.marks.pFontSize && activeSizes.push(String(m.attrs.fontSize) + "pt");
+            });
+        }
+
+        let styles = new Map<String, String[]>();
+        styles.set("families", activeFamilies);
+        styles.set("sizes", activeSizes);
+        return styles;
+    }
 
     destroy() {
         console.log("destroy");
     }
 
-    createButton(faIcon: string, title: string, command: any) {
+    createButton(faIcon: string, title: string, command?: any, onclick?: any) {
         const self = this;
         function onClick(e: React.PointerEvent) {
+            console.log("clicked button");
             // dom.addEventListener("pointerdown", e => {
             e.preventDefault();
             self.view && self.view.focus();
@@ -100,7 +137,8 @@ export default class RichTextMenu extends AntimodeMenu {
             //         command(this.view.state, this.view.dispatch, this.view);
             //     }
             // });
-            self.view && command(self.view!.state, self.view!.dispatch, self.view);
+            self.view && command && command(self.view!.state, self.view!.dispatch, self.view);
+            self.view && onclick && onclick(self.view!.state, self.view!.dispatch, self.view);
         }
 
         return (
@@ -131,48 +169,29 @@ export default class RichTextMenu extends AntimodeMenu {
             });
         }
         return <select onChange={e => onChange(e.target.value)}>{items}</select>;
-
-        // let items: MenuItem[] = [];
-        // options.forEach(({ mark, title, label, command }) => {
-        //     const self = this;
-        //     function onSelect() {
-        //         self.view && command(mark, self.view);
-        //     }
-        //     // this.createMarksOption("Set font size", String(mark.attrs.fontSize), onSelect)
-        //     items.push(
-        //         new MenuItem({
-        //             title: title,
-        //             label: label,
-        //             execEvent: "",
-        //             class: "dropdown-item",
-        //             css: "",
-        //             run() { onSelect(); }
-        //         })
-        //     );
-        // });
-
-        // return <div>{(new Dropdown(items, { label: label }) as MenuItem).render(this.view!).dom}</div>;
     }
 
-    setMark = (mark: Mark, state: EditorState<any>, dispatch: any) => {
-        if (mark) {
-            const node = (state.selection as NodeSelection).node;
-            if (node?.type === schema.nodes.ordered_list) {
-                let attrs = node.attrs;
-                if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, setFontFamily: mark.attrs.family };
-                if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, setFontSize: mark.attrs.fontSize };
-                if (mark.type === schema.marks.pFontColor) attrs = { ...attrs, setFontColor: mark.attrs.color };
-                const tr = updateBullets(state.tr.setNodeMarkup(state.selection.from, node.type, attrs), state.schema);
-                dispatch(tr.setSelection(new NodeSelection(tr.doc.resolve(state.selection.from))));
-            } else {
-                toggleMark(mark.type, mark.attrs)(state, (tx: any) => {
-                    const { from, $from, to, empty } = tx.selection;
-                    if (!tx.doc.rangeHasMark(from, to, mark.type)) {
-                        toggleMark(mark.type, mark.attrs)({ tr: tx, doc: tx.doc, selection: tx.selection, storedMarks: tx.storedMarks }, dispatch);
-                    } else dispatch(tx);
-                });
+    createNodesDropdown(activeOption: string, options: { node: NodeType | any | null, title: string, label: string, command: (node: NodeType | any) => void, hidden?: boolean }[]): JSX.Element {
+        let items = options.map(({ title, label, hidden }) => {
+            if (hidden) {
+                return label === activeOption ?
+                    <option value={label} title={title} selected hidden>{label}</option> :
+                    <option value={label} title={title} hidden>{label}</option>;
             }
+            return label === activeOption ?
+                <option value={label} title={title} selected>{label}</option> :
+                <option value={label} title={title}>{label}</option>;
+        });
+
+        const self = this;
+        function onChange(val: string) {
+            options.forEach(({ label, node, command }) => {
+                if (val === label) {
+                    self.view && node && command(node);
+                }
+            });
         }
+        return <select onChange={e => onChange(e.target.value)}>{items}</select>;
     }
 
     changeFontSize = (mark: Mark, view: EditorView) => {
@@ -201,40 +220,44 @@ export default class RichTextMenu extends AntimodeMenu {
         this.setMark(view.state.schema.marks.pFontFamily.create({ family: fontName }), view.state, view.dispatch);
     }
 
-    // finds font sizes and families in selection
-    getActiveFontStylesOnSelection() {
+    // TODO: remove doesn't work
+    //remove all node type and apply the passed-in one to the selected text
+    changeListType = (nodeType: NodeType | undefined) => {
+        console.log("change the list type ");
         if (!this.view) return;
-
-        const activeFamilies: string[] = [];
-        const activeSizes: string[] = [];
-        const state = this.view.state;
-        const pos = this.view.state.selection.$from;
-        const ref_node = this.reference_node(pos);
-        if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) {
-            ref_node.marks.forEach(m => {
-                m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family);
-                m.type === state.schema.marks.pFontSize && activeSizes.push(String(m.attrs.fontSize) + "pt");
-            });
+        console.log("change the list type has view");
+
+        if (nodeType === schema.nodes.bullet_list) {
+            wrapInList(nodeType)(this.view.state, this.view.dispatch);
+        } else {
+            const marks = this.view.state.storedMarks || (this.view.state.selection.$to.parentOffset && this.view.state.selection.$from.marks());
+            if (!wrapInList(schema.nodes.ordered_list)(this.view.state, (tx2: any) => {
+                const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle);
+                marks && tx3.ensureMarks([...marks]);
+                marks && tx3.setStoredMarks([...marks]);
+
+                this.view!.dispatch(tx2);
+            })) {
+                const tx2 = this.view.state.tr;
+                const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle);
+                marks && tx3.ensureMarks([...marks]);
+                marks && tx3.setStoredMarks([...marks]);
+
+                this.view.dispatch(tx3);
+            }
         }
-
-        let styles = new Map<String, String[]>();
-        styles.set("families", activeFamilies);
-        styles.set("sizes", activeSizes);
-        return styles;
     }
 
-    // changeToFontSize = (mark: Mark, view: EditorView) => {
-    //     const size = mark.attrs.fontSize;
-    //     if (size) { this.updateFontSizeDropdown(String(size) + " pt"); }
-    //     if (this.editorProps) {
-    //         const ruleProvider = this.editorProps.ruleProvider;
-    //         const heading = NumCast(this.editorProps.Document.heading);
-    //         if (ruleProvider && heading) {
-    //             ruleProvider["ruleSize_" + heading] = size;
-    //         }
-    //     }
-    //     this.setMark(view.state.schema.marks.pFontSize.create({ fontSize: size }), view.state, view.dispatch);
-    // }
+    insertSummarizer(state: EditorState<any>, dispatch: any) {
+        if (state.selection.empty) return false;
+        const mark = state.schema.marks.summarize.create();
+        const tr = state.tr;
+        tr.addMark(state.selection.from, state.selection.to, mark);
+        const content = tr.selection.content();
+        const newNode = state.schema.nodes.summary.create({ visibility: false, text: content, textslice: content.toJSON() });
+        dispatch && dispatch(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark));
+        return true;
+    }
 
     reference_node(pos: ResolvedPos<any>): ProsNode | null {
         if (!this.view) return null;
@@ -265,7 +288,6 @@ export default class RichTextMenu extends AntimodeMenu {
     }
 
     render() {
-        console.log("render");
         const fontSizeOptions = [
             { mark: schema.marks.pFontSize.create({ fontSize: 7 }), title: "Set font size", label: "7pt", command: this.changeFontSize },
             { mark: schema.marks.pFontSize.create({ fontSize: 8 }), title: "Set font size", label: "8pt", command: this.changeFontSize },
@@ -296,6 +318,35 @@ export default class RichTextMenu extends AntimodeMenu {
             { mark: null, title: "", label: "default", command: unimplementedFunction, hidden: true },
         ]
 
+        // //will display a remove-list-type button if selection is in list, otherwise will show list type dropdown
+        // updateListItemDropdown(label: string, listTypeBtn: any) {
+        //     //remove old btn
+        //     if (listTypeBtn) { this.tooltip.removeChild(listTypeBtn); }
+
+        //     //Make a dropdown of all list types
+        //     const toAdd: MenuItem[] = [];
+        //     this.listTypeToIcon.forEach((icon, type) => {
+        //         toAdd.push(this.dropdownBulletBtn(icon, "color: black; width: 40px;", type, this.view, this.listTypes, this.changeBulletType));
+        //     });
+        //     //option to remove the list formatting
+        //     toAdd.push(this.dropdownBulletBtn("X", "color: black; width: 40px;", undefined, this.view, this.listTypes, this.changeBulletType));
+
+        //     listTypeBtn = (new Dropdown(toAdd, { label: label, css: "color:black; width: 40px;" }) as MenuItem).render(this.view).dom;
+
+        //     //add this new button and return it
+        //     this.tooltip.appendChild(listTypeBtn);
+        //     return listTypeBtn;
+        // }
+
+        const listTypeOptions = [
+            { node: schema.nodes.ordered_list.create({ mapStyle: "bullet" }), title: "Set list type", label: ":", command: this.changeListType },
+            { node: schema.nodes.ordered_list.create({ mapStyle: "decimal" }), title: "Set list type", label: "1.1", command: this.changeListType },
+            { node: schema.nodes.ordered_list.create({ mapStyle: "multi" }), title: "Set list type", label: "1.A", command: this.changeListType },
+            { node: undefined, title: "Set list type", label: "Remove", command: this.changeListType },
+        ]
+
+        // options: { node: NodeType | null, title: string, label: string, command: (node: NodeType) => void, hidden ?: boolean } []
+
         const buttons = [
             this.createButton("bold", "Bold", toggleMark(schema.marks.strong)),
             this.createButton("italic", "Italic", toggleMark(schema.marks.em)),
@@ -303,8 +354,10 @@ export default class RichTextMenu extends AntimodeMenu {
             this.createButton("strikethrough", "Strikethrough", toggleMark(schema.marks.strikethrough)),
             this.createButton("superscript", "Superscript", toggleMark(schema.marks.superscript)),
             this.createButton("subscript", "Subscript", toggleMark(schema.marks.subscript)),
+            this.createButton("indent", "Summarize", undefined, this.insertSummarizer),
             this.createMarksDropdown(this.activeFontSize, fontSizeOptions),
             this.createMarksDropdown(this.activeFontFamily, fontFamilyOptions),
+            this.createNodesDropdown(this.activeListType, listTypeOptions),
         ];
 
         // this._marksToDoms = new Map();
-- 
cgit v1.2.3-70-g09d2


From 4791f95d6be68e590d0a80689d89be4c05e9ffc3 Mon Sep 17 00:00:00 2001
From: vellichora <fangrui_tong@brown.edu>
Date: Tue, 7 Jan 2020 06:43:26 -0500
Subject: starting integrating brush tool into richtextmenu

---
 src/client/util/RichTextMenu.scss  |  10 +++
 src/client/util/RichTextMenu.tsx   | 137 +++++++++++++++++++++++++++++++++++--
 src/client/views/AntimodeMenu.scss |   2 +-
 src/client/views/AntimodeMenu.tsx  |  12 ++--
 4 files changed, 150 insertions(+), 11 deletions(-)
 create mode 100644 src/client/util/RichTextMenu.scss

(limited to 'src/client/util/RichTextMenu.tsx')

diff --git a/src/client/util/RichTextMenu.scss b/src/client/util/RichTextMenu.scss
new file mode 100644
index 000000000..f14703006
--- /dev/null
+++ b/src/client/util/RichTextMenu.scss
@@ -0,0 +1,10 @@
+.button-dropdown-wrapper {
+    position: relative;
+
+    .dropdown {
+        position: absolute;
+        top: 30px;
+        left: 0;
+        background-color: black;
+    }
+}
\ No newline at end of file
diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx
index 6b6a2620e..fa65adca5 100644
--- a/src/client/util/RichTextMenu.tsx
+++ b/src/client/util/RichTextMenu.tsx
@@ -5,10 +5,10 @@ import { observer } from "mobx-react";
 import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos, Schema } from "prosemirror-model";
 import { schema } from "./RichTextSchema";
 import { EditorView } from "prosemirror-view";
-import { EditorState, NodeSelection } from "prosemirror-state";
+import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
-import { faBold, faItalic, faUnderline, faStrikethrough, faSubscript, faSuperscript, faIndent } from "@fortawesome/free-solid-svg-icons";
+import { faBold, faItalic, faUnderline, faStrikethrough, faSubscript, faSuperscript, faIndent, faEyeDropper, faCaretDown } from "@fortawesome/free-solid-svg-icons";
 import { MenuItem, Dropdown } from "prosemirror-menu";
 import { updateBullets } from "./ProsemirrorExampleTransfer";
 import { FieldViewProps } from "../views/nodes/FieldView";
@@ -16,9 +16,10 @@ import { NumCast } from "../../new_fields/Types";
 import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox";
 import { unimplementedFunction } from "../../Utils";
 import { wrapInList } from "prosemirror-schema-list";
+import "./RichTextMenu.scss";
 const { toggleMark, setBlockType } = require("prosemirror-commands");
 
-library.add(faBold, faItalic, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent);
+library.add(faBold, faItalic, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, faCaretDown);
 
 @observer
 export default class RichTextMenu extends AntimodeMenu {
@@ -31,6 +32,9 @@ export default class RichTextMenu extends AntimodeMenu {
     @observable private activeFontFamily: string = "";
     @observable private activeListType: string = "";
 
+    @observable private brushIsEmpty: boolean = true;
+    @observable private brushMarks: Set<Mark> = new Set();
+
     constructor(props: Readonly<{}>) {
         super(props);
         RichTextMenu.Instance = this;
@@ -121,6 +125,13 @@ export default class RichTextMenu extends AntimodeMenu {
         return styles;
     }
 
+    getMarksInSelection(state: EditorState<any>) {
+        const found = new Set<Mark>();
+        const { from, to } = state.selection as TextSelection;
+        state.doc.nodesBetween(from, to, (node) => node.marks?.forEach(m => found.add(m)));
+        return found;
+    }
+
     destroy() {
         console.log("destroy");
     }
@@ -142,7 +153,7 @@ export default class RichTextMenu extends AntimodeMenu {
         }
 
         return (
-            <button className="antimodeMenu-button" title="title" onPointerDown={onClick}>
+            <button className="antimodeMenu-button" title={title} onPointerDown={onClick}>
                 <FontAwesomeIcon icon={faIcon as IconProp} size="lg" />
             </button>
         );
@@ -259,6 +270,123 @@ export default class RichTextMenu extends AntimodeMenu {
         return true;
     }
 
+    createBrushButton() {
+        const self = this;
+        function onClick(e: React.PointerEvent) {
+            console.log("clicked button");
+            // dom.addEventListener("pointerdown", e => {
+            e.preventDefault();
+            self.view && self.view.focus();
+            //     if (dom.contains(e.target as Node)) {
+            e.stopPropagation();
+            //         command(this.view.state, this.view.dispatch, this.view);
+            //     }
+            // });
+
+            self.view && self.brush_function(self.view.state, self.view.dispatch);
+
+            // // update dropdown with marks
+            // const newBrushDropdowndom = self.createBrushDropdown().render(self.view).dom;
+            // self._brushDropdownDom && self.tooltip.replaceChild(newBrushDropdowndom, self._brushDropdownDom);
+            // self._brushDropdownDom = newBrushDropdowndom;
+        }
+
+        let label = "Stored marks: ";
+        if (this.brushMarks && this.brushMarks.size > 0) {
+            this.brushMarks.forEach((mark: Mark) => {
+                const markType = mark.type;
+                label += markType.name;
+                label += ", ";
+            });
+            label = label.substring(0, label.length - 2);
+        } else {
+            label = "No marks are currently stored";
+        }
+
+        return (
+            <div className="button-dropdown-wrapper">
+                <button className="antimodeMenu-button" title="" onPointerDown={onClick}>
+                    <FontAwesomeIcon icon="eye-dropper" size="lg" />
+                    <button className="dropdown-button"><FontAwesomeIcon icon="caret-down" size="sm" /></button>
+                </button>
+                <div className="dropdown">
+                    <p>{label}</p>
+                    <button onPointerDown={this.clearBrush}>Clear brush</button>
+                    {/* <input placeholder="Enter URL"></input> */}
+                </div>
+            </div>
+        );
+    }
+
+    @action
+    clearBrush() {
+        this.brushIsEmpty = true;
+        this.brushMarks = new Set();
+    }
+
+    @action
+    brush_function(state: EditorState<any>, dispatch: any) {
+        if (!this.view) return;
+
+        if (this.brushIsEmpty) {
+            const selected_marks = this.getMarksInSelection(this.view.state);
+            // if (this._brushdom) {
+            if (selected_marks.size >= 0) {
+                this.brushMarks = selected_marks;
+                // const newbrush = this.createBrush(true).render(this.view).dom;
+                // this.tooltip.replaceChild(newbrush, this._brushdom);
+                // this._brushdom = newbrush;
+                this.brushIsEmpty = !this.brushIsEmpty;
+                // TooltipTextMenuManager.Instance._brushIsEmpty = !TooltipTextMenuManager.Instance._brushIsEmpty;
+            }
+            // }
+        }
+        else {
+            const { from, to, $from } = this.view.state.selection;
+            // if (this._brushdom) {
+            if (!this.view.state.selection.empty && $from && $from.nodeAfter) {
+                if (this.brushMarks && to - from > 0) {
+                    this.view.dispatch(this.view.state.tr.removeMark(from, to));
+                    Array.from(this.brushMarks).filter(m => m.type !== schema.marks.user_mark).forEach((mark: Mark) => {
+                        this.setMark(mark, this.view!.state, this.view!.dispatch);
+                    });
+                }
+            }
+            else {
+                // const newbrush = this.createBrush(false).render(this.view).dom;
+                // this.tooltip.replaceChild(newbrush, this._brushdom);
+                // this._brushdom = newbrush;
+                this.brushIsEmpty = !this.brushIsEmpty;
+                // TooltipTextMenuManager.Instance._brushIsEmpty = !TooltipTextMenuManager.Instance._brushIsEmpty;
+            }
+            // }
+        }
+        console.log("brush marks are ", this.brushMarks);
+    }
+
+    // createColorButton() {
+    //     const self = this;
+    //     function onClick(e: React.PointerEvent) {
+    //         console.log("clicked button");
+    //         // dom.addEventListener("pointerdown", e => {
+    //         e.preventDefault();
+    //         self.view && self.view.focus();
+    //         //     if (dom.contains(e.target as Node)) {
+    //         e.stopPropagation();
+    //         //         command(this.view.state, this.view.dispatch, this.view);
+    //         //     }
+    //         // });
+    //         self.view && command && command(self.view!.state, self.view!.dispatch, self.view);
+    //         self.view && onclick && onclick(self.view!.state, self.view!.dispatch, self.view);
+    //     }
+
+    //     return (
+    //         <button className="antimodeMenu-button" title="Set font color" onPointerDown={onClick}>
+    //             <FontAwesomeIcon icon={faIcon as IconProp} size="lg" />
+    //         </button>
+    //     );
+    // }
+
     reference_node(pos: ResolvedPos<any>): ProsNode | null {
         if (!this.view) return null;
 
@@ -354,6 +482,7 @@ export default class RichTextMenu extends AntimodeMenu {
             this.createButton("strikethrough", "Strikethrough", toggleMark(schema.marks.strikethrough)),
             this.createButton("superscript", "Superscript", toggleMark(schema.marks.superscript)),
             this.createButton("subscript", "Subscript", toggleMark(schema.marks.subscript)),
+            this.createBrushButton(),
             this.createButton("indent", "Summarize", undefined, this.insertSummarizer),
             this.createMarksDropdown(this.activeFontSize, fontSizeOptions),
             this.createMarksDropdown(this.activeFontFamily, fontFamilyOptions),
diff --git a/src/client/views/AntimodeMenu.scss b/src/client/views/AntimodeMenu.scss
index f3da5f284..a45f3346a 100644
--- a/src/client/views/AntimodeMenu.scss
+++ b/src/client/views/AntimodeMenu.scss
@@ -5,7 +5,7 @@
     background: #323232;
     box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
     border-radius: 0px 6px 6px 6px;
-    overflow: hidden;
+    // overflow: hidden;    
     display: flex;
 
     .antimodeMenu-button {
diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx
index 408df8bc2..2e9d29ef9 100644
--- a/src/client/views/AntimodeMenu.tsx
+++ b/src/client/views/AntimodeMenu.tsx
@@ -62,12 +62,12 @@ export default abstract class AntimodeMenu extends React.Component {
 
     @action
     protected pointerLeave = (e: React.PointerEvent) => {
-        if (!this.Pinned) {
-            this._transition = "opacity 0.5s";
-            this._transitionDelay = "1s";
-            this._opacity = 0.2;
-            setTimeout(() => this.fadeOut(false), 3000);
-        }
+        // if (!this.Pinned) {
+        //     this._transition = "opacity 0.5s";
+        //     this._transitionDelay = "1s";
+        //     this._opacity = 0.2;
+        //     setTimeout(() => this.fadeOut(false), 3000);
+        // }
     }
 
     @action
-- 
cgit v1.2.3-70-g09d2


From e29ba39e74d2d22a74e767c39c30055ebf2ca5e0 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Tue, 7 Jan 2020 07:14:29 -0500
Subject: styling for brush dropdown

---
 src/client/util/RichTextMenu.scss | 16 ++++++++++++++--
 src/client/util/RichTextMenu.tsx  | 10 +++++-----
 2 files changed, 19 insertions(+), 7 deletions(-)

(limited to 'src/client/util/RichTextMenu.tsx')

diff --git a/src/client/util/RichTextMenu.scss b/src/client/util/RichTextMenu.scss
index f14703006..403e9f88a 100644
--- a/src/client/util/RichTextMenu.scss
+++ b/src/client/util/RichTextMenu.scss
@@ -1,10 +1,22 @@
 .button-dropdown-wrapper {
     position: relative;
 
+    .dropdown-button {
+        width: 20px;
+        padding-left: 7px;
+        padding-right: 7px;
+    }
+
     .dropdown {
         position: absolute;
-        top: 30px;
+        top: 35px;
         left: 0;
-        background-color: black;
+        background-color: #323232;
+        color: $light-color-secondary;
+        border: 1px solid #4d4d4d;
+        border-radius: 0 6px 6px 6px;
+        box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
+        min-width: 150px;
+        padding: 5px;
     }
 }
\ No newline at end of file
diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx
index fa65adca5..ad7b3f2fc 100644
--- a/src/client/util/RichTextMenu.tsx
+++ b/src/client/util/RichTextMenu.tsx
@@ -283,7 +283,7 @@ export default class RichTextMenu extends AntimodeMenu {
             //     }
             // });
 
-            self.view && self.brush_function(self.view.state, self.view.dispatch);
+            self.view && self.fillBrush(self.view.state, self.view.dispatch);
 
             // // update dropdown with marks
             // const newBrushDropdowndom = self.createBrushDropdown().render(self.view).dom;
@@ -307,8 +307,8 @@ export default class RichTextMenu extends AntimodeMenu {
             <div className="button-dropdown-wrapper">
                 <button className="antimodeMenu-button" title="" onPointerDown={onClick}>
                     <FontAwesomeIcon icon="eye-dropper" size="lg" />
-                    <button className="dropdown-button"><FontAwesomeIcon icon="caret-down" size="sm" /></button>
                 </button>
+                <button className="dropdown-button antimodeMenu-button"><FontAwesomeIcon icon="caret-down" size="sm" /></button>
                 <div className="dropdown">
                     <p>{label}</p>
                     <button onPointerDown={this.clearBrush}>Clear brush</button>
@@ -320,12 +320,12 @@ export default class RichTextMenu extends AntimodeMenu {
 
     @action
     clearBrush() {
-        this.brushIsEmpty = true;
-        this.brushMarks = new Set();
+        RichTextMenu.Instance.brushIsEmpty = true;
+        RichTextMenu.Instance.brushMarks = new Set();
     }
 
     @action
-    brush_function(state: EditorState<any>, dispatch: any) {
+    fillBrush(state: EditorState<any>, dispatch: any) {
         if (!this.view) return;
 
         if (this.brushIsEmpty) {
-- 
cgit v1.2.3-70-g09d2


From 372d8623ce5b0fd545739cb53a38f6d3f759e7e4 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Tue, 7 Jan 2020 08:04:32 -0500
Subject: integrated color and highlight tools in richtextmenu

---
 src/client/util/RichTextMenu.scss |   2 +
 src/client/util/RichTextMenu.tsx  | 225 +++++++++++++++++++++++++++-----------
 2 files changed, 164 insertions(+), 63 deletions(-)

(limited to 'src/client/util/RichTextMenu.tsx')

diff --git a/src/client/util/RichTextMenu.scss b/src/client/util/RichTextMenu.scss
index 403e9f88a..9c5c55b62 100644
--- a/src/client/util/RichTextMenu.scss
+++ b/src/client/util/RichTextMenu.scss
@@ -1,3 +1,5 @@
+@import "../views/globalCssVariables";
+
 .button-dropdown-wrapper {
     position: relative;
 
diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx
index ad7b3f2fc..a9de87572 100644
--- a/src/client/util/RichTextMenu.tsx
+++ b/src/client/util/RichTextMenu.tsx
@@ -8,7 +8,7 @@ import { EditorView } from "prosemirror-view";
 import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
-import { faBold, faItalic, faUnderline, faStrikethrough, faSubscript, faSuperscript, faIndent, faEyeDropper, faCaretDown } from "@fortawesome/free-solid-svg-icons";
+import { faBold, faItalic, faUnderline, faStrikethrough, faSubscript, faSuperscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter } from "@fortawesome/free-solid-svg-icons";
 import { MenuItem, Dropdown } from "prosemirror-menu";
 import { updateBullets } from "./ProsemirrorExampleTransfer";
 import { FieldViewProps } from "../views/nodes/FieldView";
@@ -16,10 +16,11 @@ import { NumCast } from "../../new_fields/Types";
 import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox";
 import { unimplementedFunction } from "../../Utils";
 import { wrapInList } from "prosemirror-schema-list";
+import { PastelSchemaPalette, DarkPastelSchemaPalette } from '../../new_fields/SchemaHeaderField';
 import "./RichTextMenu.scss";
 const { toggleMark, setBlockType } = require("prosemirror-commands");
 
-library.add(faBold, faItalic, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, faCaretDown);
+library.add(faBold, faItalic, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter);
 
 @observer
 export default class RichTextMenu extends AntimodeMenu {
@@ -34,6 +35,13 @@ export default class RichTextMenu extends AntimodeMenu {
 
     @observable private brushIsEmpty: boolean = true;
     @observable private brushMarks: Set<Mark> = new Set();
+    @observable private showBrushDropdown: boolean = false;
+
+    @observable private activeFontColor: string = "black";
+    @observable private showColorDropdown: boolean = false;
+
+    @observable private activeHighlightColor: string = "transparent";
+    @observable private showHighlightDropdown: boolean = false;
 
     constructor(props: Readonly<{}>) {
         super(props);
@@ -76,8 +84,8 @@ export default class RichTextMenu extends AntimodeMenu {
         const activeFamilies = active && active.get("families");
         const activeSizes = active && active.get("sizes");
 
-        this.activeFontFamily = !activeFamilies || activeFamilies.length == 0 ? "default" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various";
-        this.activeFontSize = !activeSizes || activeSizes.length == 0 ? "default" : activeSizes.length === 1 ? String(activeSizes[0]) + "pt" : "various";
+        this.activeFontFamily = !activeFamilies || activeFamilies.length === 0 ? "default" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various";
+        this.activeFontSize = !activeSizes || activeSizes.length === 0 ? "default" : activeSizes.length === 1 ? String(activeSizes[0]) + "pt" : "various";
 
         // this.update_mark_doms();
     }
@@ -160,7 +168,7 @@ export default class RichTextMenu extends AntimodeMenu {
     }
 
     createMarksDropdown(activeOption: string, options: { mark: Mark | null, title: string, label: string, command: (mark: Mark, view: EditorView) => void, hidden?: boolean }[]): JSX.Element {
-        let items = options.map(({ title, label, hidden }) => {
+        const items = options.map(({ title, label, hidden }) => {
             if (hidden) {
                 return label === activeOption ?
                     <option value={label} title={title} selected hidden>{label}</option> :
@@ -183,7 +191,7 @@ export default class RichTextMenu extends AntimodeMenu {
     }
 
     createNodesDropdown(activeOption: string, options: { node: NodeType | any | null, title: string, label: string, command: (node: NodeType | any) => void, hidden?: boolean }[]): JSX.Element {
-        let items = options.map(({ title, label, hidden }) => {
+        const items = options.map(({ title, label, hidden }) => {
             if (hidden) {
                 return label === activeOption ?
                     <option value={label} title={title} selected hidden>{label}</option> :
@@ -270,25 +278,22 @@ export default class RichTextMenu extends AntimodeMenu {
         return true;
     }
 
+    @action 
+    toggleBrushDropdown() { this.showBrushDropdown = !this.showBrushDropdown; }
+
     createBrushButton() {
         const self = this;
-        function onClick(e: React.PointerEvent) {
-            console.log("clicked button");
-            // dom.addEventListener("pointerdown", e => {
+        function onBrushClick(e: React.PointerEvent) {
             e.preventDefault();
-            self.view && self.view.focus();
-            //     if (dom.contains(e.target as Node)) {
             e.stopPropagation();
-            //         command(this.view.state, this.view.dispatch, this.view);
-            //     }
-            // });
-
+            self.view && self.view.focus();
             self.view && self.fillBrush(self.view.state, self.view.dispatch);
-
-            // // update dropdown with marks
-            // const newBrushDropdowndom = self.createBrushDropdown().render(self.view).dom;
-            // self._brushDropdownDom && self.tooltip.replaceChild(newBrushDropdowndom, self._brushDropdownDom);
-            // self._brushDropdownDom = newBrushDropdowndom;
+        }
+        function onDropdownClick(e: React.PointerEvent) {
+            e.preventDefault();
+            e.stopPropagation();
+            self.view && self.view.focus();
+            self.toggleBrushDropdown();
         }
 
         let label = "Stored marks: ";
@@ -305,15 +310,17 @@ export default class RichTextMenu extends AntimodeMenu {
 
         return (
             <div className="button-dropdown-wrapper">
-                <button className="antimodeMenu-button" title="" onPointerDown={onClick}>
+                <button className="antimodeMenu-button" title="" onPointerDown={onBrushClick}>
                     <FontAwesomeIcon icon="eye-dropper" size="lg" />
                 </button>
-                <button className="dropdown-button antimodeMenu-button"><FontAwesomeIcon icon="caret-down" size="sm" /></button>
-                <div className="dropdown">
-                    <p>{label}</p>
-                    <button onPointerDown={this.clearBrush}>Clear brush</button>
-                    {/* <input placeholder="Enter URL"></input> */}
-                </div>
+                <button className="dropdown-button antimodeMenu-button" onPointerDown={onDropdownClick}><FontAwesomeIcon icon="caret-down" size="sm" /></button>
+                {this.showBrushDropdown ? 
+                    (<div className="dropdown">
+                        <p>{label}</p>
+                        <button onPointerDown={this.clearBrush}>Clear brush</button>
+                        {/* <input placeholder="Enter URL"></input> */}
+                    </div>) 
+                    : <></> }
             </div>
         );
     }
@@ -330,20 +337,14 @@ export default class RichTextMenu extends AntimodeMenu {
 
         if (this.brushIsEmpty) {
             const selected_marks = this.getMarksInSelection(this.view.state);
-            // if (this._brushdom) {
             if (selected_marks.size >= 0) {
                 this.brushMarks = selected_marks;
-                // const newbrush = this.createBrush(true).render(this.view).dom;
-                // this.tooltip.replaceChild(newbrush, this._brushdom);
-                // this._brushdom = newbrush;
                 this.brushIsEmpty = !this.brushIsEmpty;
-                // TooltipTextMenuManager.Instance._brushIsEmpty = !TooltipTextMenuManager.Instance._brushIsEmpty;
             }
             // }
         }
         else {
             const { from, to, $from } = this.view.state.selection;
-            // if (this._brushdom) {
             if (!this.view.state.selection.empty && $from && $from.nodeAfter) {
                 if (this.brushMarks && to - from > 0) {
                     this.view.dispatch(this.view.state.tr.removeMark(from, to));
@@ -353,39 +354,135 @@ export default class RichTextMenu extends AntimodeMenu {
                 }
             }
             else {
-                // const newbrush = this.createBrush(false).render(this.view).dom;
-                // this.tooltip.replaceChild(newbrush, this._brushdom);
-                // this._brushdom = newbrush;
                 this.brushIsEmpty = !this.brushIsEmpty;
-                // TooltipTextMenuManager.Instance._brushIsEmpty = !TooltipTextMenuManager.Instance._brushIsEmpty;
             }
-            // }
         }
         console.log("brush marks are ", this.brushMarks);
     }
 
-    // createColorButton() {
-    //     const self = this;
-    //     function onClick(e: React.PointerEvent) {
-    //         console.log("clicked button");
-    //         // dom.addEventListener("pointerdown", e => {
-    //         e.preventDefault();
-    //         self.view && self.view.focus();
-    //         //     if (dom.contains(e.target as Node)) {
-    //         e.stopPropagation();
-    //         //         command(this.view.state, this.view.dispatch, this.view);
-    //         //     }
-    //         // });
-    //         self.view && command && command(self.view!.state, self.view!.dispatch, self.view);
-    //         self.view && onclick && onclick(self.view!.state, self.view!.dispatch, self.view);
-    //     }
-
-    //     return (
-    //         <button className="antimodeMenu-button" title="Set font color" onPointerDown={onClick}>
-    //             <FontAwesomeIcon icon={faIcon as IconProp} size="lg" />
-    //         </button>
-    //     );
-    // }
+    @action toggleColorDropdown() { this.showColorDropdown = !this.showColorDropdown; }
+    @action setActiveColor(color: string) { this.activeFontColor = color; }
+
+    createColorButton() {
+        const self = this;
+        function onColorClick(e: React.PointerEvent) {
+            e.preventDefault();
+            e.stopPropagation();
+            self.view && self.view.focus();
+            self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch);
+        }
+        function onDropdownClick(e: React.PointerEvent) {
+            e.preventDefault();
+            e.stopPropagation();
+            self.view && self.view.focus();
+            self.toggleColorDropdown();
+        }
+        function changeColor(e: React.PointerEvent, color: string) {
+            e.preventDefault();
+            e.stopPropagation();
+            self.view && self.view.focus();
+            self.setActiveColor(color);
+            self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch);
+        }
+
+        const colors = [
+            DarkPastelSchemaPalette.get("pink2"),
+            DarkPastelSchemaPalette.get("purple4"),
+            DarkPastelSchemaPalette.get("bluegreen1"),
+            DarkPastelSchemaPalette.get("yellow4"),
+            DarkPastelSchemaPalette.get("red2"),
+            DarkPastelSchemaPalette.get("bluegreen7"),
+            DarkPastelSchemaPalette.get("bluegreen5"),
+            DarkPastelSchemaPalette.get("orange1"),
+            "#757472",
+            "#000"
+        ];
+
+        return (
+            <div className="button-dropdown-wrapper">
+                <button className="antimodeMenu-button" title="" onPointerDown={onColorClick}>
+                    <FontAwesomeIcon icon="palette" size="lg" />
+                </button>
+                <button className="dropdown-button antimodeMenu-button" onPointerDown={onDropdownClick}><FontAwesomeIcon icon="caret-down" size="sm" /></button>
+                {this.showColorDropdown ? 
+                    (<div className="dropdown">
+                        {colors.map(color => {
+                            return <button style={{backgroundColor: color}} onPointerDown={e => changeColor(e, color)}></button>;
+                        })}
+                    </div>) 
+                    : <></> }
+            </div>
+        );
+    }
+
+    public insertColor(color: String, state: EditorState<any>, dispatch: any) {
+        const colorMark = state.schema.mark(state.schema.marks.pFontColor, { color: color });
+        if (state.selection.empty) {
+            dispatch(state.tr.addStoredMark(colorMark));
+            return false;
+        }
+        this.setMark(colorMark, state, dispatch);
+    }
+
+    @action toggleHighlightDropdown() { this.showHighlightDropdown = !this.showHighlightDropdown; }
+    @action setActiveHighlight(color: string) { this.activeHighlightColor = color; }
+
+    createHighlighterButton() {
+        const self = this;
+        function onHighlightClick(e: React.PointerEvent) {
+            e.preventDefault();
+            e.stopPropagation();
+            self.view && self.view.focus();
+            self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch);
+        }
+        function onDropdownClick(e: React.PointerEvent) {
+            e.preventDefault();
+            e.stopPropagation();
+            self.view && self.view.focus();
+            self.toggleHighlightDropdown();
+        }
+        function changeHighlight(e: React.PointerEvent, color: string) {
+            e.preventDefault();
+            e.stopPropagation();
+            self.view && self.view.focus();
+            self.setActiveHighlight(color);
+            self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch);
+        }
+
+        const colors = [
+            PastelSchemaPalette.get("pink2"),
+            PastelSchemaPalette.get("purple4"),
+            PastelSchemaPalette.get("bluegreen1"),
+            PastelSchemaPalette.get("yellow4"),
+            PastelSchemaPalette.get("red2"),
+            PastelSchemaPalette.get("bluegreen7"),
+            PastelSchemaPalette.get("bluegreen5"),
+            PastelSchemaPalette.get("orange1"),
+            "white",
+            "transparent"
+        ];
+
+        return (
+            <div className="button-dropdown-wrapper">
+                <button className="antimodeMenu-button" title="" onPointerDown={onHighlightClick}>
+                    <FontAwesomeIcon icon="highlighter" size="lg" />
+                </button>
+                <button className="dropdown-button antimodeMenu-button" onPointerDown={onDropdownClick}><FontAwesomeIcon icon="caret-down" size="sm" /></button>
+                {this.showHighlightDropdown ? 
+                    (<div className="dropdown">
+                        {colors.map(color => {
+                            return <button style={{backgroundColor: color}} onPointerDown={e => changeHighlight(e, color)}></button>;
+                        })}
+                    </div>) 
+                    : <></> }
+            </div>
+        );
+    }
+
+    insertHighlight(color: String, state: EditorState<any>, dispatch: any) {
+        if (state.selection.empty) return false;
+        toggleMark(state.schema.marks.marker, { highlight: color })(state, dispatch);
+    }
 
     reference_node(pos: ResolvedPos<any>): ProsNode | null {
         if (!this.view) return null;
@@ -432,7 +529,7 @@ export default class RichTextMenu extends AntimodeMenu {
             { mark: schema.marks.pFontSize.create({ fontSize: 72 }), title: "Set font size", label: "72pt", command: this.changeFontSize },
             { mark: null, title: "", label: "various", command: unimplementedFunction, hidden: true },
             { mark: null, title: "", label: "default", command: unimplementedFunction, hidden: true },
-        ]
+        ];
 
         const fontFamilyOptions = [
             { mark: schema.marks.pFontFamily.create({ family: "Times New Roman" }), title: "Set font family", label: "Times New Roman", command: this.changeFontFamily },
@@ -444,7 +541,7 @@ export default class RichTextMenu extends AntimodeMenu {
             { mark: schema.marks.pFontFamily.create({ family: "Crimson Text" }), title: "Set font family", label: "Crimson Text", command: this.changeFontFamily },
             { mark: null, title: "", label: "various", command: unimplementedFunction, hidden: true },
             { mark: null, title: "", label: "default", command: unimplementedFunction, hidden: true },
-        ]
+        ];
 
         // //will display a remove-list-type button if selection is in list, otherwise will show list type dropdown
         // updateListItemDropdown(label: string, listTypeBtn: any) {
@@ -471,7 +568,7 @@ export default class RichTextMenu extends AntimodeMenu {
             { node: schema.nodes.ordered_list.create({ mapStyle: "decimal" }), title: "Set list type", label: "1.1", command: this.changeListType },
             { node: schema.nodes.ordered_list.create({ mapStyle: "multi" }), title: "Set list type", label: "1.A", command: this.changeListType },
             { node: undefined, title: "Set list type", label: "Remove", command: this.changeListType },
-        ]
+        ];
 
         // options: { node: NodeType | null, title: string, label: string, command: (node: NodeType) => void, hidden ?: boolean } []
 
@@ -482,6 +579,8 @@ export default class RichTextMenu extends AntimodeMenu {
             this.createButton("strikethrough", "Strikethrough", toggleMark(schema.marks.strikethrough)),
             this.createButton("superscript", "Superscript", toggleMark(schema.marks.superscript)),
             this.createButton("subscript", "Subscript", toggleMark(schema.marks.subscript)),
+            this.createColorButton(),
+            this.createHighlighterButton(),
             this.createBrushButton(),
             this.createButton("indent", "Summarize", undefined, this.insertSummarizer),
             this.createMarksDropdown(this.activeFontSize, fontSizeOptions),
-- 
cgit v1.2.3-70-g09d2


From 73403134ceae1e5665e8021175a54b0953541d23 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Tue, 7 Jan 2020 08:33:27 -0500
Subject: started integrating link button with richtextmenu

---
 src/client/util/RichTextMenu.tsx | 176 ++++++++++++++++++++++++---------------
 1 file changed, 108 insertions(+), 68 deletions(-)

(limited to 'src/client/util/RichTextMenu.tsx')

diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx
index a9de87572..12bd3348f 100644
--- a/src/client/util/RichTextMenu.tsx
+++ b/src/client/util/RichTextMenu.tsx
@@ -8,7 +8,7 @@ import { EditorView } from "prosemirror-view";
 import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
-import { faBold, faItalic, faUnderline, faStrikethrough, faSubscript, faSuperscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter } from "@fortawesome/free-solid-svg-icons";
+import { faBold, faItalic, faUnderline, faStrikethrough, faSubscript, faSuperscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink } from "@fortawesome/free-solid-svg-icons";
 import { MenuItem, Dropdown } from "prosemirror-menu";
 import { updateBullets } from "./ProsemirrorExampleTransfer";
 import { FieldViewProps } from "../views/nodes/FieldView";
@@ -20,7 +20,7 @@ import { PastelSchemaPalette, DarkPastelSchemaPalette } from '../../new_fields/S
 import "./RichTextMenu.scss";
 const { toggleMark, setBlockType } = require("prosemirror-commands");
 
-library.add(faBold, faItalic, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter);
+library.add(faBold, faItalic, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink);
 
 @observer
 export default class RichTextMenu extends AntimodeMenu {
@@ -43,6 +43,9 @@ export default class RichTextMenu extends AntimodeMenu {
     @observable private activeHighlightColor: string = "transparent";
     @observable private showHighlightDropdown: boolean = false;
 
+    @observable private currentLink: string | undefined = "";
+    @observable private showLinkDropdown: boolean = false;
+
     constructor(props: Readonly<{}>) {
         super(props);
         RichTextMenu.Instance = this;
@@ -67,9 +70,8 @@ export default class RichTextMenu extends AntimodeMenu {
         const state = view.state;
         // DocumentDecorations.Instance.showTextBar();
         props && (this.editorProps = props);
-        // // Don't do anything if the document/selection didn't change
-        // if (lastState && lastState.doc.eq(state.doc) &&
-        //     lastState.selection.eq(state.selection)) return;
+        // Don't do anything if the document/selection didn't change
+        if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) return;
 
         // this.reset_mark_doms();
 
@@ -147,15 +149,9 @@ export default class RichTextMenu extends AntimodeMenu {
     createButton(faIcon: string, title: string, command?: any, onclick?: any) {
         const self = this;
         function onClick(e: React.PointerEvent) {
-            console.log("clicked button");
-            // dom.addEventListener("pointerdown", e => {
             e.preventDefault();
-            self.view && self.view.focus();
-            //     if (dom.contains(e.target as Node)) {
             e.stopPropagation();
-            //         command(this.view.state, this.view.dispatch, this.view);
-            //     }
-            // });
+            self.view && self.view.focus();
             self.view && command && command(self.view!.state, self.view!.dispatch, self.view);
             self.view && onclick && onclick(self.view!.state, self.view!.dispatch, self.view);
         }
@@ -310,9 +306,7 @@ export default class RichTextMenu extends AntimodeMenu {
 
         return (
             <div className="button-dropdown-wrapper">
-                <button className="antimodeMenu-button" title="" onPointerDown={onBrushClick}>
-                    <FontAwesomeIcon icon="eye-dropper" size="lg" />
-                </button>
+                <button className="antimodeMenu-button" title="" onPointerDown={onBrushClick}><FontAwesomeIcon icon="eye-dropper" size="lg" /></button>
                 <button className="dropdown-button antimodeMenu-button" onPointerDown={onDropdownClick}><FontAwesomeIcon icon="caret-down" size="sm" /></button>
                 {this.showBrushDropdown ? 
                     (<div className="dropdown">
@@ -400,9 +394,7 @@ export default class RichTextMenu extends AntimodeMenu {
 
         return (
             <div className="button-dropdown-wrapper">
-                <button className="antimodeMenu-button" title="" onPointerDown={onColorClick}>
-                    <FontAwesomeIcon icon="palette" size="lg" />
-                </button>
+                <button className="antimodeMenu-button" title="" onPointerDown={onColorClick}><FontAwesomeIcon icon="palette" size="lg" /></button>
                 <button className="dropdown-button antimodeMenu-button" onPointerDown={onDropdownClick}><FontAwesomeIcon icon="caret-down" size="sm" /></button>
                 {this.showColorDropdown ? 
                     (<div className="dropdown">
@@ -464,9 +456,7 @@ export default class RichTextMenu extends AntimodeMenu {
 
         return (
             <div className="button-dropdown-wrapper">
-                <button className="antimodeMenu-button" title="" onPointerDown={onHighlightClick}>
-                    <FontAwesomeIcon icon="highlighter" size="lg" />
-                </button>
+                <button className="antimodeMenu-button" title="" onPointerDown={onHighlightClick}><FontAwesomeIcon icon="highlighter" size="lg" /></button>
                 <button className="dropdown-button antimodeMenu-button" onPointerDown={onDropdownClick}><FontAwesomeIcon icon="caret-down" size="sm" /></button>
                 {this.showHighlightDropdown ? 
                     (<div className="dropdown">
@@ -484,6 +474,102 @@ export default class RichTextMenu extends AntimodeMenu {
         toggleMark(state.schema.marks.marker, { highlight: color })(state, dispatch);
     }
 
+    @action toggleLinkDropdown() { this.showLinkDropdown = !this.showLinkDropdown; }
+    @action setCurrentLink(link: string) { this.currentLink = link; }
+
+    createLinkButton() {
+        const self = this;
+        function onDropdownClick(e: React.PointerEvent) {
+            e.preventDefault();
+            e.stopPropagation();
+            self.view && self.view.focus();
+            self.toggleLinkDropdown();
+        }
+        function onLinkChange(e: React.ChangeEvent<HTMLInputElement>) {
+            self.setCurrentLink(e.target.value);
+        }
+
+        const targetTitle = await this.getTextLinkTargetTitle();
+        console.log(targetTitle);
+        // this.setCurrentLink(targetTitle);
+
+        return (
+            <div className="button-dropdown-wrapper">
+                <button className="antimodeMenu-button" title="" onPointerDown={onDropdownClick}><FontAwesomeIcon icon="link" size="lg" /></button>
+                <button className="dropdown-button antimodeMenu-button" onPointerDown={onDropdownClick}><FontAwesomeIcon icon="caret-down" size="sm" /></button>
+                {this.showLinkDropdown ? 
+                    (<div className="dropdown">
+                        <p>Linked to:</p>
+                        {/* <input value={this.currentLink} placeholder="Enter URL" onChange={onLinkChange}/>
+                        <button onPointerDown={e => this.makeLinkToURL("input value", "onRight")}>Apply hyperlink</button>
+                        <button onPointerDown={e => this.deleteLink()}>Remove link</button> */}
+                    </div>) 
+                    : <></> }
+            </div>
+        );
+    }
+
+    async getTextLinkTargetTitle() {
+        const node = this.view.state.selection.$from.nodeAfter;
+        const link = node && node.marks.find(m => m.type.name === "link");
+        if (link) {
+            const href = link.attrs.href;
+            if (href) {
+                if (href.indexOf(Utils.prepend("/doc/")) === 0) {
+                    const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+                    if (linkclicked) {
+                        const linkDoc = await DocServer.GetRefField(linkclicked);
+                        if (linkDoc instanceof Doc) {
+                            const anchor1 = await Cast(linkDoc.anchor1, Doc);
+                            const anchor2 = await Cast(linkDoc.anchor2, Doc);
+                            const currentDoc = SelectionManager.SelectedDocuments().length && SelectionManager.SelectedDocuments()[0].props.Document;
+                            if (currentDoc && anchor1 && anchor2) {
+                                if (Doc.AreProtosEqual(currentDoc, anchor1)) {
+                                    return StrCast(anchor2.title);
+                                }
+                                if (Doc.AreProtosEqual(currentDoc, anchor2)) {
+                                    return StrCast(anchor1.title);
+                                }
+                            }
+                        }
+                    }
+                } else {
+                    return href;
+                }
+            } else {
+                return link.attrs.title;
+            }
+        }
+    }
+
+    makeLinkToURL = (target: String, lcoation: string) => {
+        let node = this.view.state.selection.$from.nodeAfter;
+        let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: target, location: location });
+        this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link));
+        this.view.dispatch(this.view.state.tr.addMark(this.view.state.selection.from, this.view.state.selection.to, link));
+        node = this.view.state.selection.$from.nodeAfter;
+        link = node && node.marks.find(m => m.type.name === "link");
+    }
+
+    deleteLink = () => {
+        const node = this.view.state.selection.$from.nodeAfter;
+        const link = node && node.marks.find(m => m.type === this.view.state.schema.marks.link);
+        const href = link!.attrs.href;
+        if (href) {
+            if (href.indexOf(Utils.prepend("/doc/")) === 0) {
+                const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+                if (linkclicked) {
+                    DocServer.GetRefField(linkclicked).then(async linkDoc => {
+                        if (linkDoc instanceof Doc) {
+                            LinkManager.Instance.deleteLink(linkDoc);
+                            this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link));
+                        }
+                    });
+                }
+            }
+        }
+    }
+
     reference_node(pos: ResolvedPos<any>): ProsNode | null {
         if (!this.view) return null;
 
@@ -543,26 +629,6 @@ export default class RichTextMenu extends AntimodeMenu {
             { mark: null, title: "", label: "default", command: unimplementedFunction, hidden: true },
         ];
 
-        // //will display a remove-list-type button if selection is in list, otherwise will show list type dropdown
-        // updateListItemDropdown(label: string, listTypeBtn: any) {
-        //     //remove old btn
-        //     if (listTypeBtn) { this.tooltip.removeChild(listTypeBtn); }
-
-        //     //Make a dropdown of all list types
-        //     const toAdd: MenuItem[] = [];
-        //     this.listTypeToIcon.forEach((icon, type) => {
-        //         toAdd.push(this.dropdownBulletBtn(icon, "color: black; width: 40px;", type, this.view, this.listTypes, this.changeBulletType));
-        //     });
-        //     //option to remove the list formatting
-        //     toAdd.push(this.dropdownBulletBtn("X", "color: black; width: 40px;", undefined, this.view, this.listTypes, this.changeBulletType));
-
-        //     listTypeBtn = (new Dropdown(toAdd, { label: label, css: "color:black; width: 40px;" }) as MenuItem).render(this.view).dom;
-
-        //     //add this new button and return it
-        //     this.tooltip.appendChild(listTypeBtn);
-        //     return listTypeBtn;
-        // }
-
         const listTypeOptions = [
             { node: schema.nodes.ordered_list.create({ mapStyle: "bullet" }), title: "Set list type", label: ":", command: this.changeListType },
             { node: schema.nodes.ordered_list.create({ mapStyle: "decimal" }), title: "Set list type", label: "1.1", command: this.changeListType },
@@ -570,8 +636,6 @@ export default class RichTextMenu extends AntimodeMenu {
             { node: undefined, title: "Set list type", label: "Remove", command: this.changeListType },
         ];
 
-        // options: { node: NodeType | null, title: string, label: string, command: (node: NodeType) => void, hidden ?: boolean } []
-
         const buttons = [
             this.createButton("bold", "Bold", toggleMark(schema.marks.strong)),
             this.createButton("italic", "Italic", toggleMark(schema.marks.em)),
@@ -581,6 +645,7 @@ export default class RichTextMenu extends AntimodeMenu {
             this.createButton("subscript", "Subscript", toggleMark(schema.marks.subscript)),
             this.createColorButton(),
             this.createHighlighterButton(),
+            this.createLinkButton(),
             this.createBrushButton(),
             this.createButton("indent", "Summarize", undefined, this.insertSummarizer),
             this.createMarksDropdown(this.activeFontSize, fontSizeOptions),
@@ -588,31 +653,6 @@ export default class RichTextMenu extends AntimodeMenu {
             this.createNodesDropdown(this.activeListType, listTypeOptions),
         ];
 
-        // this._marksToDoms = new Map();
-        // items.forEach(({ title, dom, command }) => {
-        //     // this.tooltip.appendChild(dom);
-        //     switch (title) {
-        //         case "Bold":
-        //             this._marksToDoms.set(schema.mark(schema.marks.strong), dom);
-        //             // this.basicTools && this.basicTools.appendChild(dom.cloneNode(true));
-        //             break;
-        //         case "Italic":
-        //             this._marksToDoms.set(schema.mark(schema.marks.em), dom);
-        //             // this.basicTools && this.basicTools.appendChild(dom.cloneNode(true));
-        //             break;
-        //     }
-
-        //     //pointer down handler to activate button effects
-        //     dom.addEventListener("pointerdown", e => {
-        //         e.preventDefault();
-        //         this.view.focus();
-        //         if (dom.contains(e.target as Node)) {
-        //             e.stopPropagation();
-        //             command(this.view.state, this.view.dispatch, this.view);
-        //         }
-        //     });
-        // });
-
         return this.getElement(buttons);
     }
 }
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 34f96cb1fe36cc5448d2cd42c4b208751cdb3403 Mon Sep 17 00:00:00 2001
From: vellichora <fangrui_tong@brown.edu>
Date: Tue, 7 Jan 2020 09:13:43 -0500
Subject: integrated link button on richtextmenu

---
 src/client/util/RichTextMenu.scss | 10 ++++--
 src/client/util/RichTextMenu.tsx  | 69 +++++++++++++++++++++++++--------------
 2 files changed, 51 insertions(+), 28 deletions(-)

(limited to 'src/client/util/RichTextMenu.tsx')

diff --git a/src/client/util/RichTextMenu.scss b/src/client/util/RichTextMenu.scss
index 9c5c55b62..f7414cc7f 100644
--- a/src/client/util/RichTextMenu.scss
+++ b/src/client/util/RichTextMenu.scss
@@ -4,9 +4,9 @@
     position: relative;
 
     .dropdown-button {
-        width: 20px;
-        padding-left: 7px;
-        padding-right: 7px;
+        width: 15px;
+        padding-left: 5px;
+        padding-right: 5px;
     }
 
     .dropdown {
@@ -21,4 +21,8 @@
         min-width: 150px;
         padding: 5px;
     }
+
+    input {
+        color: black;
+    }
 }
\ No newline at end of file
diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx
index 12bd3348f..b6b2c53ff 100644
--- a/src/client/util/RichTextMenu.tsx
+++ b/src/client/util/RichTextMenu.tsx
@@ -12,12 +12,16 @@ import { faBold, faItalic, faUnderline, faStrikethrough, faSubscript, faSuperscr
 import { MenuItem, Dropdown } from "prosemirror-menu";
 import { updateBullets } from "./ProsemirrorExampleTransfer";
 import { FieldViewProps } from "../views/nodes/FieldView";
-import { NumCast } from "../../new_fields/Types";
+import { NumCast, Cast, StrCast } from "../../new_fields/Types";
 import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox";
-import { unimplementedFunction } from "../../Utils";
+import { unimplementedFunction, Utils } from "../../Utils";
 import { wrapInList } from "prosemirror-schema-list";
 import { PastelSchemaPalette, DarkPastelSchemaPalette } from '../../new_fields/SchemaHeaderField';
 import "./RichTextMenu.scss";
+import { DocServer } from "../DocServer";
+import { Doc } from "../../new_fields/Doc";
+import { SelectionManager } from "./SelectionManager";
+import { LinkManager } from "./LinkManager";
 const { toggleMark, setBlockType } = require("prosemirror-commands");
 
 library.add(faBold, faItalic, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink);
@@ -81,6 +85,10 @@ export default class RichTextMenu extends AntimodeMenu {
         // this._linkDropdownDom && this.tooltip.replaceChild(newLinkDropdowndom, this._linkDropdownDom);
         // this._linkDropdownDom = newLinkDropdowndom;
 
+        // const targetTitle = await this.getTextLinkTargetTitle();
+        // // console.log(targetTitle);
+        // this.setCurrentLink(targetTitle);
+
         // update active font family and size
         const active = this.getActiveFontStylesOnSelection();
         const activeFamilies = active && active.get("families");
@@ -89,6 +97,9 @@ export default class RichTextMenu extends AntimodeMenu {
         this.activeFontFamily = !activeFamilies || activeFamilies.length === 0 ? "default" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various";
         this.activeFontSize = !activeSizes || activeSizes.length === 0 ? "default" : activeSizes.length === 1 ? String(activeSizes[0]) + "pt" : "various";
 
+        const targetTitle = await this.getTextLinkTargetTitle();
+        this.setCurrentLink(targetTitle);
+
         // this.update_mark_doms();
     }
 
@@ -274,7 +285,7 @@ export default class RichTextMenu extends AntimodeMenu {
         return true;
     }
 
-    @action 
+    @action
     toggleBrushDropdown() { this.showBrushDropdown = !this.showBrushDropdown; }
 
     createBrushButton() {
@@ -308,13 +319,13 @@ export default class RichTextMenu extends AntimodeMenu {
             <div className="button-dropdown-wrapper">
                 <button className="antimodeMenu-button" title="" onPointerDown={onBrushClick}><FontAwesomeIcon icon="eye-dropper" size="lg" /></button>
                 <button className="dropdown-button antimodeMenu-button" onPointerDown={onDropdownClick}><FontAwesomeIcon icon="caret-down" size="sm" /></button>
-                {this.showBrushDropdown ? 
+                {this.showBrushDropdown ?
                     (<div className="dropdown">
                         <p>{label}</p>
                         <button onPointerDown={this.clearBrush}>Clear brush</button>
                         {/* <input placeholder="Enter URL"></input> */}
-                    </div>) 
-                    : <></> }
+                    </div>)
+                    : <></>}
             </div>
         );
     }
@@ -396,13 +407,13 @@ export default class RichTextMenu extends AntimodeMenu {
             <div className="button-dropdown-wrapper">
                 <button className="antimodeMenu-button" title="" onPointerDown={onColorClick}><FontAwesomeIcon icon="palette" size="lg" /></button>
                 <button className="dropdown-button antimodeMenu-button" onPointerDown={onDropdownClick}><FontAwesomeIcon icon="caret-down" size="sm" /></button>
-                {this.showColorDropdown ? 
+                {this.showColorDropdown ?
                     (<div className="dropdown">
                         {colors.map(color => {
-                            return <button style={{backgroundColor: color}} onPointerDown={e => changeColor(e, color)}></button>;
+                            if (color) return <button style={{ backgroundColor: color }} onPointerDown={e => changeColor(e, color)}></button>;
                         })}
-                    </div>) 
-                    : <></> }
+                    </div>)
+                    : <></>}
             </div>
         );
     }
@@ -458,13 +469,13 @@ export default class RichTextMenu extends AntimodeMenu {
             <div className="button-dropdown-wrapper">
                 <button className="antimodeMenu-button" title="" onPointerDown={onHighlightClick}><FontAwesomeIcon icon="highlighter" size="lg" /></button>
                 <button className="dropdown-button antimodeMenu-button" onPointerDown={onDropdownClick}><FontAwesomeIcon icon="caret-down" size="sm" /></button>
-                {this.showHighlightDropdown ? 
+                {this.showHighlightDropdown ?
                     (<div className="dropdown">
                         {colors.map(color => {
-                            return <button style={{backgroundColor: color}} onPointerDown={e => changeHighlight(e, color)}></button>;
+                            if (color) return <button style={{ backgroundColor: color }} onPointerDown={e => changeHighlight(e, color)}></button>;
                         })}
-                    </div>) 
-                    : <></> }
+                    </div>)
+                    : <></>}
             </div>
         );
     }
@@ -489,27 +500,30 @@ export default class RichTextMenu extends AntimodeMenu {
             self.setCurrentLink(e.target.value);
         }
 
-        const targetTitle = await this.getTextLinkTargetTitle();
-        console.log(targetTitle);
-        // this.setCurrentLink(targetTitle);
+        // const targetTitle = this.getTextLinkTargetTitle();
+        console.log("link curr is ", this.currentLink);
+        // // this.setCurrentLink(targetTitle);
+        const link = this.currentLink ? this.currentLink : "";
 
         return (
             <div className="button-dropdown-wrapper">
                 <button className="antimodeMenu-button" title="" onPointerDown={onDropdownClick}><FontAwesomeIcon icon="link" size="lg" /></button>
                 <button className="dropdown-button antimodeMenu-button" onPointerDown={onDropdownClick}><FontAwesomeIcon icon="caret-down" size="sm" /></button>
-                {this.showLinkDropdown ? 
+                {this.showLinkDropdown ?
                     (<div className="dropdown">
                         <p>Linked to:</p>
-                        {/* <input value={this.currentLink} placeholder="Enter URL" onChange={onLinkChange}/>
-                        <button onPointerDown={e => this.makeLinkToURL("input value", "onRight")}>Apply hyperlink</button>
-                        <button onPointerDown={e => this.deleteLink()}>Remove link</button> */}
-                    </div>) 
-                    : <></> }
+                        <input value={link} placeholder="Enter URL" onChange={onLinkChange} />
+                        <button onPointerDown={e => this.makeLinkToURL(link, "onRight")}>Apply hyperlink</button>
+                        <button onPointerDown={e => this.deleteLink()}>Remove link</button>
+                    </div>)
+                    : <></>}
             </div>
         );
     }
 
     async getTextLinkTargetTitle() {
+        if (!this.view) return;
+
         const node = this.view.state.selection.$from.nodeAfter;
         const link = node && node.marks.find(m => m.type.name === "link");
         if (link) {
@@ -542,7 +556,10 @@ export default class RichTextMenu extends AntimodeMenu {
         }
     }
 
+    // TODO: should check for valid URL
     makeLinkToURL = (target: String, lcoation: string) => {
+        if (!this.view) return;
+
         let node = this.view.state.selection.$from.nodeAfter;
         let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: target, location: location });
         this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link));
@@ -552,8 +569,10 @@ export default class RichTextMenu extends AntimodeMenu {
     }
 
     deleteLink = () => {
+        if (!this.view) return;
+
         const node = this.view.state.selection.$from.nodeAfter;
-        const link = node && node.marks.find(m => m.type === this.view.state.schema.marks.link);
+        const link = node && node.marks.find(m => m.type === this.view!.state.schema.marks.link);
         const href = link!.attrs.href;
         if (href) {
             if (href.indexOf(Utils.prepend("/doc/")) === 0) {
@@ -562,7 +581,7 @@ export default class RichTextMenu extends AntimodeMenu {
                     DocServer.GetRefField(linkclicked).then(async linkDoc => {
                         if (linkDoc instanceof Doc) {
                             LinkManager.Instance.deleteLink(linkDoc);
-                            this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link));
+                            this.view!.dispatch(this.view!.state.tr.removeMark(this.view!.state.selection.from, this.view!.state.selection.to, this.view!.state.schema.marks.link));
                         }
                     });
                 }
-- 
cgit v1.2.3-70-g09d2


From 9614ba541c30dc7d2b5183d5450864354d911643 Mon Sep 17 00:00:00 2001
From: vellichora <fangrui_tong@brown.edu>
Date: Tue, 7 Jan 2020 10:11:14 -0500
Subject: can remove links without needing to select whole link

---
 src/client/util/RichTextMenu.tsx | 32 ++++++++++++++++++++++++++++++++
 1 file changed, 32 insertions(+)

(limited to 'src/client/util/RichTextMenu.tsx')

diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx
index b6b2c53ff..371ba97a9 100644
--- a/src/client/util/RichTextMenu.tsx
+++ b/src/client/util/RichTextMenu.tsx
@@ -574,21 +574,53 @@ export default class RichTextMenu extends AntimodeMenu {
         const node = this.view.state.selection.$from.nodeAfter;
         const link = node && node.marks.find(m => m.type === this.view!.state.schema.marks.link);
         const href = link!.attrs.href;
+        console.log("delete link", node, link, href);
         if (href) {
+            console.log("has href");
             if (href.indexOf(Utils.prepend("/doc/")) === 0) {
                 const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
                 if (linkclicked) {
+                    console.log("linkclicked");
                     DocServer.GetRefField(linkclicked).then(async linkDoc => {
                         if (linkDoc instanceof Doc) {
+                            console.log("is doc");
                             LinkManager.Instance.deleteLink(linkDoc);
+                            console.log("remove the link! ", this.view!.state.selection.from, this.view!.state.selection.to, this.view!.state.schema.marks.link);
                             this.view!.dispatch(this.view!.state.tr.removeMark(this.view!.state.selection.from, this.view!.state.selection.to, this.view!.state.schema.marks.link));
                         }
                     });
                 }
+            } else {
+                console.log("remove the link! ", this.view!.state.selection.from, this.view!.state.selection.to, this.view!.state.schema.marks.link);
+                if (node) {
+                    let extension = this.linkExtend(this.view!.state.selection.$anchor, href);
+                    console.log("remove the link", extension.from, extension.to);
+                    this.view!.dispatch(this.view!.state.tr.removeMark(extension.from, extension.to, this.view!.state.schema.marks.link));
+
+                }
             }
         }
     }
 
+    linkExtend($start: ResolvedPos, href: string) {
+        const mark = this.view!.state.schema.marks.link;
+
+        let startIndex = $start.index();
+        let endIndex = $start.indexAfter();
+
+        while (startIndex > 0 && $start.parent.child(startIndex - 1).marks.filter(m => m.type === mark && m.attrs.href === href).length) startIndex--;
+        while (endIndex < $start.parent.childCount && $start.parent.child(endIndex).marks.filter(m => m.type === mark && m.attrs.href === href).length) endIndex++;
+
+        let startPos = $start.start();
+        let endPos = startPos;
+        for (let i = 0; i < endIndex; i++) {
+            let size = $start.parent.child(i).nodeSize;
+            if (i < startIndex) startPos += size;
+            endPos += size;
+        }
+        return { from: startPos, to: endPos };
+    }
+
     reference_node(pos: ResolvedPos<any>): ProsNode | null {
         if (!this.view) return null;
 
-- 
cgit v1.2.3-70-g09d2


From 80c417f24bcc1109e12645cfc522a820cf22e099 Mon Sep 17 00:00:00 2001
From: vellichora <fangrui_tong@brown.edu>
Date: Tue, 7 Jan 2020 10:51:40 -0500
Subject: pull from master

---
 package-lock.json                        | 77 ++++++++++++++++----------------
 src/client/util/RichTextMenu.tsx         | 20 +--------
 src/client/util/SelectionManager.ts      |  1 +
 src/client/views/DocumentDecorations.tsx | 10 ++++-
 4 files changed, 50 insertions(+), 58 deletions(-)

(limited to 'src/client/util/RichTextMenu.tsx')

diff --git a/package-lock.json b/package-lock.json
index 0fbc4dcf5..2c70ba94d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2423,7 +2423,7 @@
             "resolved": "https://registry.npmjs.org/boolify-string/-/boolify-string-2.0.2.tgz",
             "integrity": "sha1-n4m9l9YKFEijlAF8SjuaPSQNRY4=",
             "requires": {
-                "type-detect": "1.0.0"
+                "type-detect": "^1.0.0"
             },
             "dependencies": {
                 "type-detect": {
@@ -2561,7 +2561,7 @@
         },
         "browserify-aes": {
             "version": "1.2.0",
-            "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
+            "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
             "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
             "requires": {
                 "buffer-xor": "^1.0.3",
@@ -2595,7 +2595,7 @@
         },
         "browserify-rsa": {
             "version": "4.0.1",
-            "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
+            "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
             "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
             "requires": {
                 "bn.js": "^4.1.0",
@@ -2632,7 +2632,7 @@
         },
         "buffer": {
             "version": "4.9.1",
-            "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
+            "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
             "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
             "dev": true,
             "requires": {
@@ -2779,7 +2779,7 @@
         },
         "camelcase-keys": {
             "version": "2.1.0",
-            "resolved": "http://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
+            "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
             "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
             "requires": {
                 "camelcase": "^2.0.0",
@@ -3482,8 +3482,8 @@
             "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
             "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
             "requires": {
-                "object-assign": "4.1.1",
-                "vary": "1.1.2"
+                "object-assign": "^4",
+                "vary": "^1"
             }
         },
         "cosmiconfig": {
@@ -3563,7 +3563,7 @@
         },
         "create-hash": {
             "version": "1.2.0",
-            "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
+            "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
             "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
             "requires": {
                 "cipher-base": "^1.0.1",
@@ -3575,7 +3575,7 @@
         },
         "create-hmac": {
             "version": "1.1.7",
-            "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
+            "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
             "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
             "requires": {
                 "cipher-base": "^1.0.3",
@@ -4118,7 +4118,7 @@
         },
         "diffie-hellman": {
             "version": "5.0.3",
-            "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
+            "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
             "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
             "requires": {
                 "bn.js": "^4.1.0",
@@ -4318,9 +4318,10 @@
         },
         "emit-logger": {
             "version": "github:chocolateboy/emit-logger#b9d25a2d939e42f29c940861e9648bd0fb810070",
+            "from": "github:chocolateboy/emit-logger#better-emitter-name",
             "requires": {
-                "chalk": "1.1.3",
-                "moment": "2.24.0"
+                "chalk": "^1.1.1",
+                "moment": "^2.10.6"
             }
         },
         "emoji-regex": {
@@ -7069,18 +7070,18 @@
             "resolved": "https://registry.npmjs.org/ipc-event-emitter/-/ipc-event-emitter-2.0.2.tgz",
             "integrity": "sha512-hJsN8zCg8MZwl5nbTutqINDO4pJPbKwmCfrTJaRLNE+5H15mJx7Mxo3pXIAi8zlh+N5xpf+PdMOQ0pbtZQvRKA==",
             "requires": {
-                "babel-runtime": "6.26.0",
-                "bluebird": "3.5.5",
-                "boolify-string": "2.0.2",
-                "emit-logger": "github:chocolateboy/emit-logger#b9d25a2d939e42f29c940861e9648bd0fb810070",
-                "lodash": "4.17.15",
-                "semver": "5.7.0",
-                "source-map-support": "0.5.12"
+                "babel-runtime": "^6.22.0",
+                "bluebird": "^3.4.7",
+                "boolify-string": "^2.0.2",
+                "emit-logger": "github:chocolateboy/emit-logger#better-emitter-name",
+                "lodash": "^4.17.4",
+                "semver": "^5.0.3",
+                "source-map-support": "^0.5.9"
             }
         },
         "is-accessor-descriptor": {
             "version": "0.1.6",
-            "resolved": "http://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
+            "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
             "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
             "requires": {
                 "kind-of": "^3.0.2"
@@ -7129,7 +7130,7 @@
         },
         "is-data-descriptor": {
             "version": "0.1.4",
-            "resolved": "http://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
+            "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
             "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
             "requires": {
                 "kind-of": "^3.0.2"
@@ -7914,7 +7915,7 @@
         },
         "load-json-file": {
             "version": "1.1.0",
-            "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
+            "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
             "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
             "requires": {
                 "graceful-fs": "^4.1.2",
@@ -8228,7 +8229,7 @@
         },
         "media-typer": {
             "version": "0.3.0",
-            "resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+            "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
             "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
         },
         "mem": {
@@ -8260,7 +8261,7 @@
         },
         "meow": {
             "version": "3.7.0",
-            "resolved": "http://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
+            "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
             "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
             "requires": {
                 "camelcase-keys": "^2.0.0",
@@ -8453,7 +8454,7 @@
         },
         "mkdirp": {
             "version": "0.5.1",
-            "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+            "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
             "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
             "requires": {
                 "minimist": "0.0.8"
@@ -8799,7 +8800,7 @@
         },
         "next-tick": {
             "version": "1.0.0",
-            "resolved": "http://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
+            "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
             "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
         },
         "nice-try": {
@@ -8963,7 +8964,7 @@
                 },
                 "semver": {
                     "version": "5.3.0",
-                    "resolved": "http://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
+                    "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
                     "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8="
                 },
                 "tar": {
@@ -12572,7 +12573,7 @@
         },
         "os-homedir": {
             "version": "1.0.2",
-            "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+            "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
             "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
         },
         "os-locale": {
@@ -12585,7 +12586,7 @@
         },
         "os-tmpdir": {
             "version": "1.0.2",
-            "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+            "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
             "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
         },
         "osenv": {
@@ -12825,7 +12826,7 @@
         },
         "path-is-absolute": {
             "version": "1.0.1",
-            "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+            "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
             "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
         },
         "path-is-inside": {
@@ -14145,7 +14146,7 @@
         },
         "readable-stream": {
             "version": "2.3.6",
-            "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+            "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
             "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
             "requires": {
                 "core-util-is": "~1.0.0",
@@ -14490,7 +14491,7 @@
         },
         "safe-regex": {
             "version": "1.1.0",
-            "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
+            "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
             "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
             "requires": {
                 "ret": "~0.1.10"
@@ -14755,7 +14756,7 @@
         },
         "sha.js": {
             "version": "2.4.11",
-            "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
+            "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
             "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
             "requires": {
                 "inherits": "^2.0.1",
@@ -15498,7 +15499,7 @@
         },
         "string_decoder": {
             "version": "1.1.1",
-            "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+            "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
             "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
             "requires": {
                 "safe-buffer": "~5.1.0"
@@ -15528,7 +15529,7 @@
         },
         "strip-ansi": {
             "version": "3.0.1",
-            "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
             "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
             "requires": {
                 "ansi-regex": "^2.0.0"
@@ -15544,7 +15545,7 @@
         },
         "strip-eof": {
             "version": "1.0.0",
-            "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
+            "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
             "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8="
         },
         "strip-indent": {
@@ -16297,7 +16298,7 @@
         },
         "tty-browserify": {
             "version": "0.0.0",
-            "resolved": "http://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
+            "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
             "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=",
             "dev": true
         },
@@ -17739,7 +17740,7 @@
         },
         "wrap-ansi": {
             "version": "2.1.0",
-            "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
+            "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
             "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
             "requires": {
                 "string-width": "^1.0.1",
diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx
index 371ba97a9..111822307 100644
--- a/src/client/util/RichTextMenu.tsx
+++ b/src/client/util/RichTextMenu.tsx
@@ -53,6 +53,7 @@ export default class RichTextMenu extends AntimodeMenu {
     constructor(props: Readonly<{}>) {
         super(props);
         RichTextMenu.Instance = this;
+        this.Pinned = true;
     }
 
     @action
@@ -79,16 +80,6 @@ export default class RichTextMenu extends AntimodeMenu {
 
         // this.reset_mark_doms();
 
-        // // update link dropdown
-        // const linkDropdown = await this.createLinkDropdown();
-        // const newLinkDropdowndom = linkDropdown.render(this.view).dom;
-        // this._linkDropdownDom && this.tooltip.replaceChild(newLinkDropdowndom, this._linkDropdownDom);
-        // this._linkDropdownDom = newLinkDropdowndom;
-
-        // const targetTitle = await this.getTextLinkTargetTitle();
-        // // console.log(targetTitle);
-        // this.setCurrentLink(targetTitle);
-
         // update active font family and size
         const active = this.getActiveFontStylesOnSelection();
         const activeFamilies = active && active.get("families");
@@ -97,6 +88,7 @@ export default class RichTextMenu extends AntimodeMenu {
         this.activeFontFamily = !activeFamilies || activeFamilies.length === 0 ? "default" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various";
         this.activeFontSize = !activeSizes || activeSizes.length === 0 ? "default" : activeSizes.length === 1 ? String(activeSizes[0]) + "pt" : "various";
 
+        // update link in current selection
         const targetTitle = await this.getTextLinkTargetTitle();
         this.setCurrentLink(targetTitle);
 
@@ -574,29 +566,21 @@ export default class RichTextMenu extends AntimodeMenu {
         const node = this.view.state.selection.$from.nodeAfter;
         const link = node && node.marks.find(m => m.type === this.view!.state.schema.marks.link);
         const href = link!.attrs.href;
-        console.log("delete link", node, link, href);
         if (href) {
-            console.log("has href");
             if (href.indexOf(Utils.prepend("/doc/")) === 0) {
                 const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
                 if (linkclicked) {
-                    console.log("linkclicked");
                     DocServer.GetRefField(linkclicked).then(async linkDoc => {
                         if (linkDoc instanceof Doc) {
-                            console.log("is doc");
                             LinkManager.Instance.deleteLink(linkDoc);
-                            console.log("remove the link! ", this.view!.state.selection.from, this.view!.state.selection.to, this.view!.state.schema.marks.link);
                             this.view!.dispatch(this.view!.state.tr.removeMark(this.view!.state.selection.from, this.view!.state.selection.to, this.view!.state.schema.marks.link));
                         }
                     });
                 }
             } else {
-                console.log("remove the link! ", this.view!.state.selection.from, this.view!.state.selection.to, this.view!.state.schema.marks.link);
                 if (node) {
                     let extension = this.linkExtend(this.view!.state.selection.$anchor, href);
-                    console.log("remove the link", extension.from, extension.to);
                     this.view!.dispatch(this.view!.state.tr.removeMark(extension.from, extension.to, this.view!.state.schema.marks.link));
-
                 }
             }
         }
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 4612f10f4..0c733ac47 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -14,6 +14,7 @@ export namespace SelectionManager {
 
         @action
         SelectDoc(docView: DocumentView, ctrlPressed: boolean): void {
+            console.log("select doc!!!");
             // if doc is not in SelectedDocuments, add it
             if (!manager.SelectedDocuments.get(docView)) {
                 if (!ctrlPressed) {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 76b6a8834..093761f1f 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -77,6 +77,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         DocumentDecorations.Instance = this;
         this._keyinput = React.createRef();
         reaction(() => SelectionManager.SelectedDocuments().slice(), docs => this.titleBlur(false));
+        console.log("constructing document decorations!!!");
+    }
+
+    componentDidMount() {
+        console.log("mounting deocument deoctirioeon!!!!");
     }
 
     @action titleChanged = (event: any) => this._accumulatedTitle = event.target.value;
@@ -574,8 +579,9 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         if (bounds.y > bounds.b) {
             bounds.y = bounds.b - (this._resizeBorderWidth + this._linkBoxHeight + this._titleHeight);
         }
-        // RichTextMenu.Instance.jumpTo(this._lastX, this._lastY - 50);
-        RichTextMenu.Instance.jumpTo(500, 300);
+        // // RichTextMenu.Instance.jumpTo(this._lastX, this._lastY - 50);
+        // console.log("jump to ");
+        // RichTextMenu.Instance.jumpTo(500, 300);
         return (<div className="documentDecorations">
             <div className="documentDecorations-background" style={{
                 width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
-- 
cgit v1.2.3-70-g09d2


From ced3532239c582938271635802e0d2d62703e148 Mon Sep 17 00:00:00 2001
From: vellichora <fangrui_tong@brown.edu>
Date: Tue, 7 Jan 2020 14:20:36 -0500
Subject: richtextmenu appears whenever formattedtextbox is focused

---
 src/client/util/RichTextMenu.scss           | 64 +++++++++++++++++++++++++++++
 src/client/util/RichTextMenu.tsx            | 58 +++++++++++++++++---------
 src/client/util/SelectionManager.ts         |  3 +-
 src/client/util/TooltipTextMenu.tsx         |  2 +
 src/client/views/DocumentDecorations.tsx    |  8 ----
 src/client/views/nodes/FormattedTextBox.tsx |  7 ++++
 6 files changed, 113 insertions(+), 29 deletions(-)

(limited to 'src/client/util/RichTextMenu.tsx')

diff --git a/src/client/util/RichTextMenu.scss b/src/client/util/RichTextMenu.scss
index f7414cc7f..85d2765e3 100644
--- a/src/client/util/RichTextMenu.scss
+++ b/src/client/util/RichTextMenu.scss
@@ -20,9 +20,73 @@
         box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
         min-width: 150px;
         padding: 5px;
+
+        button {
+            background-color: #323232;
+            border: 1px solid black;
+            border-radius: 1px;
+            padding: 6px;
+            margin: 5px 0;
+
+            &:hover {
+                background-color: black;
+            }
+        }
     }
 
     input {
         color: black;
     }
+}
+
+.link-menu {
+    .divider {
+        background-color: white;
+        height: 1px;
+        width: 100%;
+    }
+}
+
+.color-preview-button {
+    .color-preview {
+        width: 100%;
+        height: 3px;
+        margin-top: 3px;
+    }
+}
+
+.color-wrapper {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: space-between;
+
+    button.color-button {
+        width: 20px;
+        height: 20px;
+        border-radius: 15px !important;
+        margin: 3px;
+        border: 2px solid transparent !important;
+        padding: 3px;
+
+        &.active {
+            border: 2px solid white !important;
+        }
+    }
+}
+
+select {
+    background-color: #323232;
+    color: white;
+    border: 1px solid black;
+    border-top: none;
+    border-bottom: none;
+
+    &:focus,
+    &:hover {
+        background-color: black;
+    }
+
+    &::-ms-expand {
+        color: white;
+    }
 }
\ No newline at end of file
diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx
index 111822307..8c373b818 100644
--- a/src/client/util/RichTextMenu.tsx
+++ b/src/client/util/RichTextMenu.tsx
@@ -29,6 +29,7 @@ library.add(faBold, faItalic, faUnderline, faStrikethrough, faSuperscript, faSub
 @observer
 export default class RichTextMenu extends AntimodeMenu {
     static Instance: RichTextMenu;
+    @observable private isVisible: boolean = false;
 
     private view?: EditorView;
     private editorProps: FieldViewProps & FormattedTextBoxProps | undefined;
@@ -53,7 +54,6 @@ export default class RichTextMenu extends AntimodeMenu {
     constructor(props: Readonly<{}>) {
         super(props);
         RichTextMenu.Instance = this;
-        this.Pinned = true;
     }
 
     @action
@@ -73,6 +73,7 @@ export default class RichTextMenu extends AntimodeMenu {
         }
         this.view = view;
         const state = view.state;
+        this.isVisible = true;
         // DocumentDecorations.Instance.showTextBar();
         props && (this.editorProps = props);
         // Don't do anything if the document/selection didn't change
@@ -146,7 +147,6 @@ export default class RichTextMenu extends AntimodeMenu {
     }
 
     destroy() {
-        console.log("destroy");
     }
 
     createButton(faIcon: string, title: string, command?: any, onclick?: any) {
@@ -213,7 +213,6 @@ export default class RichTextMenu extends AntimodeMenu {
     }
 
     changeFontSize = (mark: Mark, view: EditorView) => {
-        console.log("change font size!!");
         const size = mark.attrs.fontSize;
         if (this.editorProps) {
             const ruleProvider = this.editorProps.ruleProvider;
@@ -241,9 +240,7 @@ export default class RichTextMenu extends AntimodeMenu {
     // TODO: remove doesn't work
     //remove all node type and apply the passed-in one to the selected text
     changeListType = (nodeType: NodeType | undefined) => {
-        console.log("change the list type ");
         if (!this.view) return;
-        console.log("change the list type has view");
 
         if (nodeType === schema.nodes.bullet_list) {
             wrapInList(nodeType)(this.view.state, this.view.dispatch);
@@ -354,7 +351,6 @@ export default class RichTextMenu extends AntimodeMenu {
                 this.brushIsEmpty = !this.brushIsEmpty;
             }
         }
-        console.log("brush marks are ", this.brushMarks);
     }
 
     @action toggleColorDropdown() { this.showColorDropdown = !this.showColorDropdown; }
@@ -397,13 +393,23 @@ export default class RichTextMenu extends AntimodeMenu {
 
         return (
             <div className="button-dropdown-wrapper">
-                <button className="antimodeMenu-button" title="" onPointerDown={onColorClick}><FontAwesomeIcon icon="palette" size="lg" /></button>
+                <button className="antimodeMenu-button color-preview-button" title="" onPointerDown={onColorClick}>
+                    <FontAwesomeIcon icon="palette" size="lg" />
+                    <div className="color-preview" style={{ backgroundColor: this.activeFontColor }}></div>
+                </button>
                 <button className="dropdown-button antimodeMenu-button" onPointerDown={onDropdownClick}><FontAwesomeIcon icon="caret-down" size="sm" /></button>
                 {this.showColorDropdown ?
                     (<div className="dropdown">
-                        {colors.map(color => {
-                            if (color) return <button style={{ backgroundColor: color }} onPointerDown={e => changeColor(e, color)}></button>;
-                        })}
+                        <p>Change font color:</p>
+                        <div className="color-wrapper">
+                            {colors.map(color => {
+                                if (color) {
+                                    return this.activeFontColor === color ?
+                                        <button className="color-button active" style={{ backgroundColor: color }} onPointerDown={e => changeColor(e, color)}></button> :
+                                        <button className="color-button" style={{ backgroundColor: color }} onPointerDown={e => changeColor(e, color)}></button>;
+                                }
+                            })}
+                        </div>
                     </div>)
                     : <></>}
             </div>
@@ -459,13 +465,23 @@ export default class RichTextMenu extends AntimodeMenu {
 
         return (
             <div className="button-dropdown-wrapper">
-                <button className="antimodeMenu-button" title="" onPointerDown={onHighlightClick}><FontAwesomeIcon icon="highlighter" size="lg" /></button>
+                <button className="antimodeMenu-button color-preview-button" title="" onPointerDown={onHighlightClick}>
+                    <FontAwesomeIcon icon="highlighter" size="lg" />
+                    <div className="color-preview" style={{ backgroundColor: this.activeHighlightColor }}></div>
+                </button>
                 <button className="dropdown-button antimodeMenu-button" onPointerDown={onDropdownClick}><FontAwesomeIcon icon="caret-down" size="sm" /></button>
                 {this.showHighlightDropdown ?
                     (<div className="dropdown">
-                        {colors.map(color => {
-                            if (color) return <button style={{ backgroundColor: color }} onPointerDown={e => changeHighlight(e, color)}></button>;
-                        })}
+                        <p>Change highlight color:</p>
+                        <div className="color-wrapper">
+                            {colors.map(color => {
+                                if (color) {
+                                    return this.activeHighlightColor === color ?
+                                        <button className="color-button active" style={{ backgroundColor: color }} onPointerDown={e => changeHighlight(e, color)}>{color === "transparent" ? "X" : ""}</button> :
+                                        <button className="color-button" style={{ backgroundColor: color }} onPointerDown={e => changeHighlight(e, color)}>{color === "transparent" ? "X" : ""}</button>;
+                                }
+                            })}
+                        </div>
                     </div>)
                     : <></>}
             </div>
@@ -492,9 +508,6 @@ export default class RichTextMenu extends AntimodeMenu {
             self.setCurrentLink(e.target.value);
         }
 
-        // const targetTitle = this.getTextLinkTargetTitle();
-        console.log("link curr is ", this.currentLink);
-        // // this.setCurrentLink(targetTitle);
         const link = this.currentLink ? this.currentLink : "";
 
         return (
@@ -502,11 +515,12 @@ export default class RichTextMenu extends AntimodeMenu {
                 <button className="antimodeMenu-button" title="" onPointerDown={onDropdownClick}><FontAwesomeIcon icon="link" size="lg" /></button>
                 <button className="dropdown-button antimodeMenu-button" onPointerDown={onDropdownClick}><FontAwesomeIcon icon="caret-down" size="sm" /></button>
                 {this.showLinkDropdown ?
-                    (<div className="dropdown">
+                    (<div className="dropdown link-menu">
                         <p>Linked to:</p>
                         <input value={link} placeholder="Enter URL" onChange={onLinkChange} />
-                        <button onPointerDown={e => this.makeLinkToURL(link, "onRight")}>Apply hyperlink</button>
-                        <button onPointerDown={e => this.deleteLink()}>Remove link</button>
+                        <button className="make-button" onPointerDown={e => this.makeLinkToURL(link, "onRight")}>Apply hyperlink</button>
+                        <div className="divider"></div>
+                        <button className="remove-button" onPointerDown={e => this.deleteLink()}>Remove link</button>
                     </div>)
                     : <></>}
             </div>
@@ -634,6 +648,10 @@ export default class RichTextMenu extends AntimodeMenu {
     }
 
     render() {
+        // if (!this.isVisible) return <></>;
+        SelectionManager.SelectedDocuments()
+        // if (this.Pinned || )
+
         const fontSizeOptions = [
             { mark: schema.marks.pFontSize.create({ fontSize: 7 }), title: "Set font size", label: "7pt", command: this.changeFontSize },
             { mark: schema.marks.pFontSize.create({ fontSize: 8 }), title: "Set font size", label: "8pt", command: this.changeFontSize },
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 0c733ac47..86a7a620e 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -3,6 +3,8 @@ import { Doc } from "../../new_fields/Doc";
 import { DocumentView } from "../views/nodes/DocumentView";
 import { computedFn } from "mobx-utils";
 import { List } from "../../new_fields/List";
+import { DocumentDecorations } from "../views/DocumentDecorations";
+import RichTextMenu from "./RichTextMenu";
 
 export namespace SelectionManager {
 
@@ -14,7 +16,6 @@ export namespace SelectionManager {
 
         @action
         SelectDoc(docView: DocumentView, ctrlPressed: boolean): void {
-            console.log("select doc!!!");
             // if doc is not in SelectedDocuments, add it
             if (!manager.SelectedDocuments.get(docView)) {
                 if (!ctrlPressed) {
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 33257b658..2c4d08b82 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -20,6 +20,8 @@ import { PastelSchemaPalette, DarkPastelSchemaPalette } from '../../new_fields/S
 const { toggleMark, setBlockType } = require("prosemirror-commands");
 const { openPrompt, TextField } = require("./ProsemirrorCopy/prompt.js");
 
+// deprecated in favor of richtextmenu
+
 //appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc.
 export class TooltipTextMenu {
     public static Toolbar: HTMLDivElement | undefined;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 093761f1f..799b3695c 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -77,11 +77,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         DocumentDecorations.Instance = this;
         this._keyinput = React.createRef();
         reaction(() => SelectionManager.SelectedDocuments().slice(), docs => this.titleBlur(false));
-        console.log("constructing document decorations!!!");
-    }
-
-    componentDidMount() {
-        console.log("mounting deocument deoctirioeon!!!!");
     }
 
     @action titleChanged = (event: any) => this._accumulatedTitle = event.target.value;
@@ -579,9 +574,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         if (bounds.y > bounds.b) {
             bounds.y = bounds.b - (this._resizeBorderWidth + this._linkBoxHeight + this._titleHeight);
         }
-        // // RichTextMenu.Instance.jumpTo(this._lastX, this._lastY - 50);
-        // console.log("jump to ");
-        // RichTextMenu.Instance.jumpTo(500, 300);
         return (<div className="documentDecorations">
             <div className="documentDecorations-background" style={{
                 width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index f00a24d41..7d48a0859 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -48,6 +48,7 @@ import { CollectionFreeFormView } from '../collections/collectionFreeForm/Collec
 import { InkTool } from '../../../new_fields/InkField';
 import { TraceMobx } from '../../../new_fields/util';
 import RichTextMenu from '../../util/RichTextMenu';
+import { DocumentDecorations } from '../DocumentDecorations';
 
 library.add(faEdit);
 library.add(faSmile, faTextHeight, faUpload);
@@ -910,6 +911,9 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         prosediv && (prosediv.keeplocation = undefined);
         const pos = this._editorView?.state.selection.$from.pos || 1;
         keeplocation && setTimeout(() => this._editorView?.dispatch(this._editorView?.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos))));
+
+        // jump rich text menu to this textbox
+        this._ref.current && RichTextMenu.Instance.jumpTo(this._ref.current.getBoundingClientRect().x, this._ref.current?.getBoundingClientRect().y - 70);
     }
     onPointerWheel = (e: React.WheelEvent): void => {
         // if a text note is not selected and scrollable, this prevents us from being able to scroll and zoom out at the same time
@@ -1055,6 +1059,9 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
             this._undoTyping = undefined;
         }
         this.doLinkOnDeselect();
+
+        // move the richtextmenu offscreen
+        if (!RichTextMenu.Instance.Pinned) RichTextMenu.Instance.jumpTo(-300, -300);
     }
 
     _lastTimedMark: Mark | undefined = undefined;
-- 
cgit v1.2.3-70-g09d2


From e9dcc0f18498d9415d204dfd3e61465a70486374 Mon Sep 17 00:00:00 2001
From: vellichora <fangrui_tong@brown.edu>
Date: Tue, 7 Jan 2020 15:08:31 -0500
Subject: kind of hack to get selects in richtextmenu to be selectable again

---
 src/client/util/RichTextMenu.scss           |  9 ++++++--
 src/client/util/RichTextMenu.tsx            | 35 ++++++++++++++++++-----------
 src/client/views/AntimodeMenu.scss          |  9 ++++++++
 src/client/views/AntimodeMenu.tsx           | 10 +++++++++
 src/client/views/nodes/FormattedTextBox.tsx |  5 +++--
 5 files changed, 51 insertions(+), 17 deletions(-)

(limited to 'src/client/util/RichTextMenu.tsx')

diff --git a/src/client/util/RichTextMenu.scss b/src/client/util/RichTextMenu.scss
index 85d2765e3..5a57f4b98 100644
--- a/src/client/util/RichTextMenu.scss
+++ b/src/client/util/RichTextMenu.scss
@@ -20,6 +20,7 @@
         box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
         min-width: 150px;
         padding: 5px;
+        font-size: 12px;
 
         button {
             background-color: #323232;
@@ -27,6 +28,7 @@
             border-radius: 1px;
             padding: 6px;
             margin: 5px 0;
+            font-size: 12px;
 
             &:hover {
                 background-color: black;
@@ -78,8 +80,11 @@ select {
     background-color: #323232;
     color: white;
     border: 1px solid black;
-    border-top: none;
-    border-bottom: none;
+    // border-top: none;
+    // border-bottom: none;
+    font-size: 12px;
+    height: 100%;
+    margin-right: 3px;
 
     &:focus,
     &:hover {
diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx
index 8c373b818..4538a77d6 100644
--- a/src/client/util/RichTextMenu.tsx
+++ b/src/client/util/RichTextMenu.tsx
@@ -29,7 +29,7 @@ library.add(faBold, faItalic, faUnderline, faStrikethrough, faSuperscript, faSub
 @observer
 export default class RichTextMenu extends AntimodeMenu {
     static Instance: RichTextMenu;
-    @observable private isVisible: boolean = false;
+    public overDropdown: boolean = false; // kind of hacky way to prevent selects not being selectable
 
     private view?: EditorView;
     private editorProps: FieldViewProps & FormattedTextBoxProps | undefined;
@@ -73,7 +73,6 @@ export default class RichTextMenu extends AntimodeMenu {
         }
         this.view = view;
         const state = view.state;
-        this.isVisible = true;
         // DocumentDecorations.Instance.showTextBar();
         props && (this.editorProps = props);
         // Don't do anything if the document/selection didn't change
@@ -179,14 +178,19 @@ export default class RichTextMenu extends AntimodeMenu {
         });
 
         const self = this;
-        function onChange(val: string) {
+        function onChange(e: React.ChangeEvent<HTMLSelectElement>) {
+            e.stopPropagation();
+            e.preventDefault();
+            console.log("on change marks");
             options.forEach(({ label, mark, command }) => {
-                if (val === label) {
+                if (e.target.value === label) {
                     self.view && mark && command(mark, self.view);
                 }
             });
         }
-        return <select onChange={e => onChange(e.target.value)}>{items}</select>;
+        function onPointerEnter() { self.overDropdown = true; }
+        function onPointerLeave() { self.overDropdown = false; }
+        return <select onChange={onChange} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave}>{items}</select>;
     }
 
     createNodesDropdown(activeOption: string, options: { node: NodeType | any | null, title: string, label: string, command: (node: NodeType | any) => void, hidden?: boolean }[]): JSX.Element {
@@ -209,7 +213,9 @@ export default class RichTextMenu extends AntimodeMenu {
                 }
             });
         }
-        return <select onChange={e => onChange(e.target.value)}>{items}</select>;
+        function onPointerEnter() { self.overDropdown = true; }
+        function onPointerLeave() { self.overDropdown = false; }
+        return <select onChange={e => onChange(e.target.value)} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave}>{items}</select>;
     }
 
     changeFontSize = (mark: Mark, view: EditorView) => {
@@ -274,8 +280,7 @@ export default class RichTextMenu extends AntimodeMenu {
         return true;
     }
 
-    @action
-    toggleBrushDropdown() { this.showBrushDropdown = !this.showBrushDropdown; }
+    @action toggleBrushDropdown() { this.showBrushDropdown = !this.showBrushDropdown; }
 
     createBrushButton() {
         const self = this;
@@ -648,9 +653,6 @@ export default class RichTextMenu extends AntimodeMenu {
     }
 
     render() {
-        // if (!this.isVisible) return <></>;
-        SelectionManager.SelectedDocuments()
-        // if (this.Pinned || )
 
         const fontSizeOptions = [
             { mark: schema.marks.pFontSize.create({ fontSize: 7 }), title: "Set font size", label: "7pt", command: this.changeFontSize },
@@ -689,7 +691,7 @@ export default class RichTextMenu extends AntimodeMenu {
             { node: undefined, title: "Set list type", label: "Remove", command: this.changeListType },
         ];
 
-        const buttons = [
+        const row1 = <div className="antimodeMenu-row">{[
             this.createButton("bold", "Bold", toggleMark(schema.marks.strong)),
             this.createButton("italic", "Italic", toggleMark(schema.marks.em)),
             this.createButton("underline", "Underline", toggleMark(schema.marks.underline)),
@@ -701,11 +703,18 @@ export default class RichTextMenu extends AntimodeMenu {
             this.createLinkButton(),
             this.createBrushButton(),
             this.createButton("indent", "Summarize", undefined, this.insertSummarizer),
+        ]}</div>
+
+        const row2 = <div className="antimodeMenu-row">{[
             this.createMarksDropdown(this.activeFontSize, fontSizeOptions),
             this.createMarksDropdown(this.activeFontFamily, fontFamilyOptions),
             this.createNodesDropdown(this.activeListType, listTypeOptions),
+        ]}</div>
+
+        const buttons = [
+            row1, row2
         ];
 
-        return this.getElement(buttons);
+        return this.getElementWithRows(buttons, 2);
     }
 }
\ No newline at end of file
diff --git a/src/client/views/AntimodeMenu.scss b/src/client/views/AntimodeMenu.scss
index a45f3346a..f78b1dbfb 100644
--- a/src/client/views/AntimodeMenu.scss
+++ b/src/client/views/AntimodeMenu.scss
@@ -8,6 +8,15 @@
     // overflow: hidden;    
     display: flex;
 
+    &.with-rows {
+        flex-direction: column
+    }
+
+    .antimodeMenu-row {
+        display: flex;
+        height: 35px;
+    }
+
     .antimodeMenu-button {
         background-color: transparent;
         width: 35px;
diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx
index 2e9d29ef9..713a8a189 100644
--- a/src/client/views/AntimodeMenu.tsx
+++ b/src/client/views/AntimodeMenu.tsx
@@ -125,4 +125,14 @@ export default abstract class AntimodeMenu extends React.Component {
             </div>
         );
     }
+
+    protected getElementWithRows(rows: JSX.Element[], numRows: number) {
+        return (
+            <div className="antimodeMenu-cont with-rows" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu}
+                style={{ left: this._left, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay, height: 35 * numRows + "px" }}>
+                {rows}
+                <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} />
+            </div>
+        );
+    }
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 7d48a0859..7291d2e8e 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -913,7 +913,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         keeplocation && setTimeout(() => this._editorView?.dispatch(this._editorView?.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos))));
 
         // jump rich text menu to this textbox
-        this._ref.current && RichTextMenu.Instance.jumpTo(this._ref.current.getBoundingClientRect().x, this._ref.current?.getBoundingClientRect().y - 70);
+        this._ref.current && RichTextMenu.Instance.jumpTo(this._ref.current.getBoundingClientRect().x, this._ref.current?.getBoundingClientRect().y - 105);
     }
     onPointerWheel = (e: React.WheelEvent): void => {
         // if a text note is not selected and scrollable, this prevents us from being able to scroll and zoom out at the same time
@@ -1053,6 +1053,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         });
     }
     onBlur = (e: any) => {
+        console.log("formated blur");
         //DictationManager.Controls.stop(false);
         if (this._undoTyping) {
             this._undoTyping.end();
@@ -1061,7 +1062,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         this.doLinkOnDeselect();
 
         // move the richtextmenu offscreen
-        if (!RichTextMenu.Instance.Pinned) RichTextMenu.Instance.jumpTo(-300, -300);
+        if (!RichTextMenu.Instance.Pinned && !RichTextMenu.Instance.overDropdown) RichTextMenu.Instance.jumpTo(-300, -300);
     }
 
     _lastTimedMark: Mark | undefined = undefined;
-- 
cgit v1.2.3-70-g09d2


From 89d3ec54886a588f3b7c8e46e771830eeefc3c8b Mon Sep 17 00:00:00 2001
From: vellichora <fangrui_tong@brown.edu>
Date: Tue, 7 Jan 2020 15:58:10 -0500
Subject: added logic so that text nodes started on the very left still show
 the full richtextmenu

---
 src/client/util/RichTextMenu.scss           |  9 ++++++
 src/client/util/RichTextMenu.tsx            | 49 ++++++++++++++++++++---------
 src/client/views/AntimodeMenu.tsx           |  6 +++-
 src/client/views/nodes/FormattedTextBox.tsx |  8 +++--
 4 files changed, 55 insertions(+), 17 deletions(-)

(limited to 'src/client/util/RichTextMenu.tsx')

diff --git a/src/client/util/RichTextMenu.scss b/src/client/util/RichTextMenu.scss
index 5a57f4b98..ff9270829 100644
--- a/src/client/util/RichTextMenu.scss
+++ b/src/client/util/RichTextMenu.scss
@@ -94,4 +94,13 @@ select {
     &::-ms-expand {
         color: white;
     }
+}
+
+.row-2 {
+    display: flex;
+    justify-content: space-between;
+
+    >div {
+        display: flex;
+    }
 }
\ No newline at end of file
diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx
index 4538a77d6..ae55dbd30 100644
--- a/src/client/util/RichTextMenu.tsx
+++ b/src/client/util/RichTextMenu.tsx
@@ -29,7 +29,7 @@ library.add(faBold, faItalic, faUnderline, faStrikethrough, faSuperscript, faSub
 @observer
 export default class RichTextMenu extends AntimodeMenu {
     static Instance: RichTextMenu;
-    public overDropdown: boolean = false; // kind of hacky way to prevent selects not being selectable
+    public overMenu: boolean = false; // kind of hacky way to prevent selects not being selectable
 
     private view?: EditorView;
     private editorProps: FieldViewProps & FormattedTextBoxProps | undefined;
@@ -188,9 +188,7 @@ export default class RichTextMenu extends AntimodeMenu {
                 }
             });
         }
-        function onPointerEnter() { self.overDropdown = true; }
-        function onPointerLeave() { self.overDropdown = false; }
-        return <select onChange={onChange} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave}>{items}</select>;
+        return <select onChange={onChange}>{items}</select>;
     }
 
     createNodesDropdown(activeOption: string, options: { node: NodeType | any | null, title: string, label: string, command: (node: NodeType | any) => void, hidden?: boolean }[]): JSX.Element {
@@ -213,9 +211,7 @@ export default class RichTextMenu extends AntimodeMenu {
                 }
             });
         }
-        function onPointerEnter() { self.overDropdown = true; }
-        function onPointerLeave() { self.overDropdown = false; }
-        return <select onChange={e => onChange(e.target.value)} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave}>{items}</select>;
+        return <select onChange={e => onChange(e.target.value)}>{items}</select>;
     }
 
     changeFontSize = (mark: Mark, view: EditorView) => {
@@ -311,7 +307,9 @@ export default class RichTextMenu extends AntimodeMenu {
 
         return (
             <div className="button-dropdown-wrapper">
-                <button className="antimodeMenu-button" title="" onPointerDown={onBrushClick}><FontAwesomeIcon icon="eye-dropper" size="lg" /></button>
+                <button className="antimodeMenu-button" title="" onPointerDown={onBrushClick} style={this.brushMarks && this.brushMarks.size > 0 ? { backgroundColor: "121212" } : {}}>
+                    <FontAwesomeIcon icon="eye-dropper" size="lg" style={{ transition: "transform 0.1s", transform: this.brushMarks && this.brushMarks.size > 0 ? "rotate(45deg)" : "" }} />
+                </button>
                 <button className="dropdown-button antimodeMenu-button" onPointerDown={onDropdownClick}><FontAwesomeIcon icon="caret-down" size="sm" /></button>
                 {this.showBrushDropdown ?
                     (<div className="dropdown">
@@ -652,6 +650,17 @@ export default class RichTextMenu extends AntimodeMenu {
         return ref_node;
     }
 
+    @action onPointerEnter(e: React.PointerEvent) { RichTextMenu.Instance.overMenu = true; }
+    @action onPointerLeave(e: React.PointerEvent) { RichTextMenu.Instance.overMenu = false; }
+
+    @action
+    toggleMenuPin = (e: React.MouseEvent) => {
+        this.Pinned = !this.Pinned;
+        if (!this.Pinned) {
+            this.fadeOut(true);
+        }
+    }
+
     render() {
 
         const fontSizeOptions = [
@@ -705,16 +714,28 @@ export default class RichTextMenu extends AntimodeMenu {
             this.createButton("indent", "Summarize", undefined, this.insertSummarizer),
         ]}</div>
 
-        const row2 = <div className="antimodeMenu-row">{[
-            this.createMarksDropdown(this.activeFontSize, fontSizeOptions),
-            this.createMarksDropdown(this.activeFontFamily, fontFamilyOptions),
-            this.createNodesDropdown(this.activeListType, listTypeOptions),
-        ]}</div>
+        const row2 = <div className="antimodeMenu-row row-2">
+            <div>{[
+                this.createMarksDropdown(this.activeFontSize, fontSizeOptions),
+                this.createMarksDropdown(this.activeFontFamily, fontFamilyOptions),
+                this.createNodesDropdown(this.activeListType, listTypeOptions),
+            ]}</div>
+            <div>
+                <button className="antimodeMenu-button" title="Pin menu" onClick={this.toggleMenuPin} style={this.Pinned ? { backgroundColor: "#121212" } : {}}>
+                    <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this.Pinned ? "rotate(45deg)" : "" }} />
+                </button>
+                {this.getDragger()}
+            </div>
+        </div>
 
         const buttons = [
             row1, row2
         ];
 
-        return this.getElementWithRows(buttons, 2);
+        return (
+            <div className="richTextMenu" onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
+                {this.getElementWithRows(buttons, 2)}
+            </div>
+        );
     }
 }
\ No newline at end of file
diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx
index 713a8a189..62ecdffaf 100644
--- a/src/client/views/AntimodeMenu.tsx
+++ b/src/client/views/AntimodeMenu.tsx
@@ -116,6 +116,10 @@ export default abstract class AntimodeMenu extends React.Component {
         e.preventDefault();
     }
 
+    protected getDragger = () => {
+        return <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} />
+    }
+
     protected getElement(buttons: JSX.Element[]) {
         return (
             <div className="antimodeMenu-cont" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu}
@@ -131,7 +135,7 @@ export default abstract class AntimodeMenu extends React.Component {
             <div className="antimodeMenu-cont with-rows" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu}
                 style={{ left: this._left, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay, height: 35 * numRows + "px" }}>
                 {rows}
-                <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} />
+                {/* <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} /> */}
             </div>
         );
     }
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 7291d2e8e..4712b1974 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -913,7 +913,11 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         keeplocation && setTimeout(() => this._editorView?.dispatch(this._editorView?.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos))));
 
         // jump rich text menu to this textbox
-        this._ref.current && RichTextMenu.Instance.jumpTo(this._ref.current.getBoundingClientRect().x, this._ref.current?.getBoundingClientRect().y - 105);
+        if (this._ref.current) {
+            let x = Math.min(Math.max(this._ref.current!.getBoundingClientRect().x, 0), window.innerWidth - 445);
+            let y = this._ref.current!.getBoundingClientRect().y - 105;
+            RichTextMenu.Instance.jumpTo(x, y);
+        }
     }
     onPointerWheel = (e: React.WheelEvent): void => {
         // if a text note is not selected and scrollable, this prevents us from being able to scroll and zoom out at the same time
@@ -1062,7 +1066,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         this.doLinkOnDeselect();
 
         // move the richtextmenu offscreen
-        if (!RichTextMenu.Instance.Pinned && !RichTextMenu.Instance.overDropdown) RichTextMenu.Instance.jumpTo(-300, -300);
+        if (!RichTextMenu.Instance.Pinned && !RichTextMenu.Instance.overMenu) RichTextMenu.Instance.jumpTo(-300, -300);
     }
 
     _lastTimedMark: Mark | undefined = undefined;
-- 
cgit v1.2.3-70-g09d2


From 531147df4186df3d4326748a5d083579cbc9e4c6 Mon Sep 17 00:00:00 2001
From: vellichora <fangrui_tong@brown.edu>
Date: Thu, 9 Jan 2020 12:01:56 -0500
Subject: minor cleanup

---
 src/client/util/RichTextMenu.tsx  |  3 ++-
 src/client/views/AntimodeMenu.tsx | 25 ++++++++++++++++---------
 2 files changed, 18 insertions(+), 10 deletions(-)

(limited to 'src/client/util/RichTextMenu.tsx')

diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx
index ae55dbd30..af2e7cc48 100644
--- a/src/client/util/RichTextMenu.tsx
+++ b/src/client/util/RichTextMenu.tsx
@@ -54,6 +54,7 @@ export default class RichTextMenu extends AntimodeMenu {
     constructor(props: Readonly<{}>) {
         super(props);
         RichTextMenu.Instance = this;
+        this._canFade = false;
     }
 
     @action
@@ -734,7 +735,7 @@ export default class RichTextMenu extends AntimodeMenu {
 
         return (
             <div className="richTextMenu" onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
-                {this.getElementWithRows(buttons, 2)}
+                {this.getElementWithRows(buttons, 2, false)}
             </div>
         );
     }
diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx
index 62ecdffaf..25bd90d87 100644
--- a/src/client/views/AntimodeMenu.tsx
+++ b/src/client/views/AntimodeMenu.tsx
@@ -18,6 +18,7 @@ export default abstract class AntimodeMenu extends React.Component {
     @observable protected _opacity: number = 1;
     @observable protected _transition: string = "opacity 0.5s";
     @observable protected _transitionDelay: string = "";
+    @observable protected _canFade: boolean = true;
 
     @observable public Pinned: boolean = false;
 
@@ -43,7 +44,7 @@ export default abstract class AntimodeMenu extends React.Component {
      * Called when you want the menu to disappear
      */
     public fadeOut = (forceOut: boolean) => {
-        if (!this.Pinned) {
+        if (!this.Pinned && this._canFade) {
             if (this._opacity === 0.2) {
                 this._transition = "opacity 0.1s";
                 this._transitionDelay = "";
@@ -62,12 +63,12 @@ export default abstract class AntimodeMenu extends React.Component {
 
     @action
     protected pointerLeave = (e: React.PointerEvent) => {
-        // if (!this.Pinned) {
-        //     this._transition = "opacity 0.5s";
-        //     this._transitionDelay = "1s";
-        //     this._opacity = 0.2;
-        //     setTimeout(() => this.fadeOut(false), 3000);
-        // }
+        if (!this.Pinned && this._canFade) {
+            this._transition = "opacity 0.5s";
+            this._transitionDelay = "1s";
+            this._opacity = 0.2;
+            setTimeout(() => this.fadeOut(false), 3000);
+        }
     }
 
     @action
@@ -100,6 +101,12 @@ export default abstract class AntimodeMenu extends React.Component {
         this._left = e.pageX - this._offsetX;
         this._top = e.pageY - this._offsetY;
 
+        // assumes dragger is on right
+        // let width = this._mainCont.current!.getBoundingClientRect().width;
+        // let height = this._mainCont.current!.getBoundingClientRect().width;
+        // this._left = Math.max(width, e.pageX - this._offsetX);
+        // this._top = Math.min(height, e.pageY - this._offsetY);
+
         e.stopPropagation();
         e.preventDefault();
     }
@@ -130,12 +137,12 @@ export default abstract class AntimodeMenu extends React.Component {
         );
     }
 
-    protected getElementWithRows(rows: JSX.Element[], numRows: number) {
+    protected getElementWithRows(rows: JSX.Element[], numRows: number, hasDragger: boolean = true) {
         return (
             <div className="antimodeMenu-cont with-rows" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu}
                 style={{ left: this._left, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay, height: 35 * numRows + "px" }}>
                 {rows}
-                {/* <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} /> */}
+                {hasDragger ? <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} /> : <></>}
             </div>
         );
     }
-- 
cgit v1.2.3-70-g09d2


From 8f030c9a8ca1f61f8430e5ea92c48a7a6b6f079f Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Thu, 9 Jan 2020 13:32:13 -0500
Subject: bounded antimodemenu to the boundaries of the screen and minor
 styling of richtextmenu

---
 package-lock.json                           | 95 ++++++++++++-----------------
 src/client/util/RichTextMenu.scss           |  7 ++-
 src/client/util/RichTextMenu.tsx            | 19 +++---
 src/client/views/AntimodeMenu.tsx           | 25 ++++----
 src/client/views/nodes/FormattedTextBox.tsx | 22 +++----
 5 files changed, 79 insertions(+), 89 deletions(-)

(limited to 'src/client/util/RichTextMenu.tsx')

diff --git a/package-lock.json b/package-lock.json
index 2c70ba94d..cf61e6aab 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2561,7 +2561,7 @@
         },
         "browserify-aes": {
             "version": "1.2.0",
-            "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
+            "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
             "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
             "requires": {
                 "buffer-xor": "^1.0.3",
@@ -2595,7 +2595,7 @@
         },
         "browserify-rsa": {
             "version": "4.0.1",
-            "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
+            "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
             "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
             "requires": {
                 "bn.js": "^4.1.0",
@@ -2632,7 +2632,7 @@
         },
         "buffer": {
             "version": "4.9.1",
-            "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
+            "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
             "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
             "dev": true,
             "requires": {
@@ -2779,7 +2779,7 @@
         },
         "camelcase-keys": {
             "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
+            "resolved": "http://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
             "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
             "requires": {
                 "camelcase": "^2.0.0",
@@ -3563,7 +3563,7 @@
         },
         "create-hash": {
             "version": "1.2.0",
-            "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
+            "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
             "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
             "requires": {
                 "cipher-base": "^1.0.1",
@@ -3575,7 +3575,7 @@
         },
         "create-hmac": {
             "version": "1.1.7",
-            "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
+            "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
             "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
             "requires": {
                 "cipher-base": "^1.0.3",
@@ -4118,7 +4118,7 @@
         },
         "diffie-hellman": {
             "version": "5.0.3",
-            "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
+            "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
             "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
             "requires": {
                 "bn.js": "^4.1.0",
@@ -5361,8 +5361,7 @@
                 },
                 "ansi-regex": {
                     "version": "2.1.1",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "aproba": {
                     "version": "1.2.0",
@@ -5380,13 +5379,11 @@
                 },
                 "balanced-match": {
                     "version": "1.0.0",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "brace-expansion": {
                     "version": "1.1.11",
                     "bundled": true,
-                    "optional": true,
                     "requires": {
                         "balanced-match": "^1.0.0",
                         "concat-map": "0.0.1"
@@ -5399,18 +5396,15 @@
                 },
                 "code-point-at": {
                     "version": "1.1.0",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "concat-map": {
                     "version": "0.0.1",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "console-control-strings": {
                     "version": "1.1.0",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "core-util-is": {
                     "version": "1.0.2",
@@ -5513,8 +5507,7 @@
                 },
                 "inherits": {
                     "version": "2.0.3",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "ini": {
                     "version": "1.3.5",
@@ -5524,7 +5517,6 @@
                 "is-fullwidth-code-point": {
                     "version": "1.0.0",
                     "bundled": true,
-                    "optional": true,
                     "requires": {
                         "number-is-nan": "^1.0.0"
                     }
@@ -5537,20 +5529,17 @@
                 "minimatch": {
                     "version": "3.0.4",
                     "bundled": true,
-                    "optional": true,
                     "requires": {
                         "brace-expansion": "^1.1.7"
                     }
                 },
                 "minimist": {
                     "version": "0.0.8",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "minipass": {
                     "version": "2.3.5",
                     "bundled": true,
-                    "optional": true,
                     "requires": {
                         "safe-buffer": "^5.1.2",
                         "yallist": "^3.0.0"
@@ -5567,7 +5556,6 @@
                 "mkdirp": {
                     "version": "0.5.1",
                     "bundled": true,
-                    "optional": true,
                     "requires": {
                         "minimist": "0.0.8"
                     }
@@ -5640,8 +5628,7 @@
                 },
                 "number-is-nan": {
                     "version": "1.0.1",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "object-assign": {
                     "version": "4.1.1",
@@ -5651,7 +5638,6 @@
                 "once": {
                     "version": "1.4.0",
                     "bundled": true,
-                    "optional": true,
                     "requires": {
                         "wrappy": "1"
                     }
@@ -5727,8 +5713,7 @@
                 },
                 "safe-buffer": {
                     "version": "5.1.2",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "safer-buffer": {
                     "version": "2.1.2",
@@ -5758,7 +5743,6 @@
                 "string-width": {
                     "version": "1.0.2",
                     "bundled": true,
-                    "optional": true,
                     "requires": {
                         "code-point-at": "^1.0.0",
                         "is-fullwidth-code-point": "^1.0.0",
@@ -5776,7 +5760,6 @@
                 "strip-ansi": {
                     "version": "3.0.1",
                     "bundled": true,
-                    "optional": true,
                     "requires": {
                         "ansi-regex": "^2.0.0"
                     }
@@ -5815,13 +5798,11 @@
                 },
                 "wrappy": {
                     "version": "1.0.2",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "yallist": {
                     "version": "3.0.3",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 }
             }
         },
@@ -7073,7 +7054,7 @@
                 "babel-runtime": "^6.22.0",
                 "bluebird": "^3.4.7",
                 "boolify-string": "^2.0.2",
-                "emit-logger": "github:chocolateboy/emit-logger#better-emitter-name",
+                "emit-logger": "github:chocolateboy/emit-logger#b9d25a2d939e42f29c940861e9648bd0fb810070",
                 "lodash": "^4.17.4",
                 "semver": "^5.0.3",
                 "source-map-support": "^0.5.9"
@@ -7081,7 +7062,7 @@
         },
         "is-accessor-descriptor": {
             "version": "0.1.6",
-            "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
+            "resolved": "http://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
             "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
             "requires": {
                 "kind-of": "^3.0.2"
@@ -7130,7 +7111,7 @@
         },
         "is-data-descriptor": {
             "version": "0.1.4",
-            "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
+            "resolved": "http://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
             "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
             "requires": {
                 "kind-of": "^3.0.2"
@@ -7915,7 +7896,7 @@
         },
         "load-json-file": {
             "version": "1.1.0",
-            "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
+            "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
             "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
             "requires": {
                 "graceful-fs": "^4.1.2",
@@ -8229,7 +8210,7 @@
         },
         "media-typer": {
             "version": "0.3.0",
-            "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+            "resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
             "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
         },
         "mem": {
@@ -8261,7 +8242,7 @@
         },
         "meow": {
             "version": "3.7.0",
-            "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
+            "resolved": "http://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
             "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
             "requires": {
                 "camelcase-keys": "^2.0.0",
@@ -8454,7 +8435,7 @@
         },
         "mkdirp": {
             "version": "0.5.1",
-            "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+            "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
             "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
             "requires": {
                 "minimist": "0.0.8"
@@ -8800,7 +8781,7 @@
         },
         "next-tick": {
             "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
+            "resolved": "http://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
             "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
         },
         "nice-try": {
@@ -8964,7 +8945,7 @@
                 },
                 "semver": {
                     "version": "5.3.0",
-                    "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
+                    "resolved": "http://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
                     "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8="
                 },
                 "tar": {
@@ -12573,7 +12554,7 @@
         },
         "os-homedir": {
             "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+            "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
             "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
         },
         "os-locale": {
@@ -12586,7 +12567,7 @@
         },
         "os-tmpdir": {
             "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+            "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
             "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
         },
         "osenv": {
@@ -12826,7 +12807,7 @@
         },
         "path-is-absolute": {
             "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+            "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
             "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
         },
         "path-is-inside": {
@@ -14146,7 +14127,7 @@
         },
         "readable-stream": {
             "version": "2.3.6",
-            "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+            "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
             "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
             "requires": {
                 "core-util-is": "~1.0.0",
@@ -14491,7 +14472,7 @@
         },
         "safe-regex": {
             "version": "1.1.0",
-            "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
+            "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
             "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
             "requires": {
                 "ret": "~0.1.10"
@@ -14756,7 +14737,7 @@
         },
         "sha.js": {
             "version": "2.4.11",
-            "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
+            "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
             "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
             "requires": {
                 "inherits": "^2.0.1",
@@ -15499,7 +15480,7 @@
         },
         "string_decoder": {
             "version": "1.1.1",
-            "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+            "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
             "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
             "requires": {
                 "safe-buffer": "~5.1.0"
@@ -15529,7 +15510,7 @@
         },
         "strip-ansi": {
             "version": "3.0.1",
-            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+            "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
             "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
             "requires": {
                 "ansi-regex": "^2.0.0"
@@ -15545,7 +15526,7 @@
         },
         "strip-eof": {
             "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
+            "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
             "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8="
         },
         "strip-indent": {
@@ -16298,7 +16279,7 @@
         },
         "tty-browserify": {
             "version": "0.0.0",
-            "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
+            "resolved": "http://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
             "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=",
             "dev": true
         },
@@ -17740,7 +17721,7 @@
         },
         "wrap-ansi": {
             "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
+            "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
             "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
             "requires": {
                 "string-width": "^1.0.1",
diff --git a/src/client/util/RichTextMenu.scss b/src/client/util/RichTextMenu.scss
index ff9270829..6383e0b7b 100644
--- a/src/client/util/RichTextMenu.scss
+++ b/src/client/util/RichTextMenu.scss
@@ -21,6 +21,7 @@
         min-width: 150px;
         padding: 5px;
         font-size: 12px;
+        z-index: 10001;
 
         button {
             background-color: #323232;
@@ -28,11 +29,15 @@
             border-radius: 1px;
             padding: 6px;
             margin: 5px 0;
-            font-size: 12px;
+            font-size: 10px;
 
             &:hover {
                 background-color: black;
             }
+
+            &:last-child {
+                margin-bottom: 0;
+            }
         }
     }
 
diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx
index af2e7cc48..ca6b90e36 100644
--- a/src/client/util/RichTextMenu.tsx
+++ b/src/client/util/RichTextMenu.tsx
@@ -8,7 +8,7 @@ import { EditorView } from "prosemirror-view";
 import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
-import { faBold, faItalic, faUnderline, faStrikethrough, faSubscript, faSuperscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink } from "@fortawesome/free-solid-svg-icons";
+import { faBold, faItalic, faUnderline, faStrikethrough, faSubscript, faSuperscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller } from "@fortawesome/free-solid-svg-icons";
 import { MenuItem, Dropdown } from "prosemirror-menu";
 import { updateBullets } from "./ProsemirrorExampleTransfer";
 import { FieldViewProps } from "../views/nodes/FieldView";
@@ -24,7 +24,7 @@ import { SelectionManager } from "./SelectionManager";
 import { LinkManager } from "./LinkManager";
 const { toggleMark, setBlockType } = require("prosemirror-commands");
 
-library.add(faBold, faItalic, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink);
+library.add(faBold, faItalic, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller);
 
 @observer
 export default class RichTextMenu extends AntimodeMenu {
@@ -86,8 +86,9 @@ export default class RichTextMenu extends AntimodeMenu {
         const activeFamilies = active && active.get("families");
         const activeSizes = active && active.get("sizes");
 
-        this.activeFontFamily = !activeFamilies || activeFamilies.length === 0 ? "default" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various";
-        this.activeFontSize = !activeSizes || activeSizes.length === 0 ? "default" : activeSizes.length === 1 ? String(activeSizes[0]) + "pt" : "various";
+        console.log("update from dash, activefontsize", this.activeFontSize, activeSizes, activeSizes && activeSizes.length, activeSizes && String(activeSizes[0]));
+        this.activeFontFamily = !activeFamilies || activeFamilies.length === 0 ? "Arial" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various";
+        this.activeFontSize = !activeSizes || activeSizes.length === 0 ? "13pt" : activeSizes.length === 1 ? String(activeSizes[0]) + "pt" : "various";
 
         // update link in current selection
         const targetTitle = await this.getTextLinkTargetTitle();
@@ -99,7 +100,7 @@ export default class RichTextMenu extends AntimodeMenu {
     setMark = (mark: Mark, state: EditorState<any>, dispatch: any) => {
         if (mark) {
             const node = (state.selection as NodeSelection).node;
-            if (node?.type === schema.nodes.ordered_list) {
+            if (node ?.type === schema.nodes.ordered_list) {
                 let attrs = node.attrs;
                 if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, setFontFamily: mark.attrs.family };
                 if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, setFontSize: mark.attrs.fontSize };
@@ -142,7 +143,7 @@ export default class RichTextMenu extends AntimodeMenu {
     getMarksInSelection(state: EditorState<any>) {
         const found = new Set<Mark>();
         const { from, to } = state.selection as TextSelection;
-        state.doc.nodesBetween(from, to, (node) => node.marks?.forEach(m => found.add(m)));
+        state.doc.nodesBetween(from, to, (node) => node.marks ?.forEach(m => found.add(m)));
         return found;
     }
 
@@ -309,7 +310,7 @@ export default class RichTextMenu extends AntimodeMenu {
         return (
             <div className="button-dropdown-wrapper">
                 <button className="antimodeMenu-button" title="" onPointerDown={onBrushClick} style={this.brushMarks && this.brushMarks.size > 0 ? { backgroundColor: "121212" } : {}}>
-                    <FontAwesomeIcon icon="eye-dropper" size="lg" style={{ transition: "transform 0.1s", transform: this.brushMarks && this.brushMarks.size > 0 ? "rotate(45deg)" : "" }} />
+                    <FontAwesomeIcon icon="paint-roller" size="lg" style={{ transition: "transform 0.1s", transform: this.brushMarks && this.brushMarks.size > 0 ? "rotate(45deg)" : "" }} />
                 </button>
                 <button className="dropdown-button antimodeMenu-button" onPointerDown={onDropdownClick}><FontAwesomeIcon icon="caret-down" size="sm" /></button>
                 {this.showBrushDropdown ?
@@ -679,7 +680,7 @@ export default class RichTextMenu extends AntimodeMenu {
             { mark: schema.marks.pFontSize.create({ fontSize: 48 }), title: "Set font size", label: "48pt", command: this.changeFontSize },
             { mark: schema.marks.pFontSize.create({ fontSize: 72 }), title: "Set font size", label: "72pt", command: this.changeFontSize },
             { mark: null, title: "", label: "various", command: unimplementedFunction, hidden: true },
-            { mark: null, title: "", label: "default", command: unimplementedFunction, hidden: true },
+            { mark: null, title: "", label: "13pt", command: unimplementedFunction, hidden: true }, // this is here because the default size is 13, but there is no actual 13pt option
         ];
 
         const fontFamilyOptions = [
@@ -691,7 +692,7 @@ export default class RichTextMenu extends AntimodeMenu {
             { mark: schema.marks.pFontFamily.create({ family: "Impact" }), title: "Set font family", label: "Impact", command: this.changeFontFamily },
             { mark: schema.marks.pFontFamily.create({ family: "Crimson Text" }), title: "Set font family", label: "Crimson Text", command: this.changeFontFamily },
             { mark: null, title: "", label: "various", command: unimplementedFunction, hidden: true },
-            { mark: null, title: "", label: "default", command: unimplementedFunction, hidden: true },
+            // { mark: null, title: "", label: "default", command: unimplementedFunction, hidden: true },
         ];
 
         const listTypeOptions = [
diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx
index 25bd90d87..e358d88cf 100644
--- a/src/client/views/AntimodeMenu.tsx
+++ b/src/client/views/AntimodeMenu.tsx
@@ -22,6 +22,9 @@ export default abstract class AntimodeMenu extends React.Component {
 
     @observable public Pinned: boolean = false;
 
+    get width() { return this._mainCont.current ? this._mainCont.current.getBoundingClientRect().width : 0; }
+    get height() { return this._mainCont.current ? this._mainCont.current.getBoundingClientRect().height : 0; }
+
     @action
     /**
      * @param x
@@ -44,7 +47,7 @@ export default abstract class AntimodeMenu extends React.Component {
      * Called when you want the menu to disappear
      */
     public fadeOut = (forceOut: boolean) => {
-        if (!this.Pinned && this._canFade) {
+        if (!this.Pinned) {
             if (this._opacity === 0.2) {
                 this._transition = "opacity 0.1s";
                 this._transitionDelay = "";
@@ -89,8 +92,8 @@ export default abstract class AntimodeMenu extends React.Component {
         document.removeEventListener("pointerup", this.dragEnd);
         document.addEventListener("pointerup", this.dragEnd);
 
-        this._offsetX = this._mainCont.current!.getBoundingClientRect().width - e.nativeEvent.offsetX;
-        this._offsetY = e.nativeEvent.offsetY;
+        this._offsetX = e.pageX - this._mainCont.current!.getBoundingClientRect().left;
+        this._offsetY = e.pageY - this._mainCont.current!.getBoundingClientRect().top;
 
         e.stopPropagation();
         e.preventDefault();
@@ -98,14 +101,14 @@ export default abstract class AntimodeMenu extends React.Component {
 
     @action
     protected dragging = (e: PointerEvent) => {
-        this._left = e.pageX - this._offsetX;
-        this._top = e.pageY - this._offsetY;
-
-        // assumes dragger is on right
-        // let width = this._mainCont.current!.getBoundingClientRect().width;
-        // let height = this._mainCont.current!.getBoundingClientRect().width;
-        // this._left = Math.max(width, e.pageX - this._offsetX);
-        // this._top = Math.min(height, e.pageY - this._offsetY);
+        const width = this._mainCont.current!.getBoundingClientRect().width;
+        const height = this._mainCont.current!.getBoundingClientRect().height;
+
+        const left = e.pageX - this._offsetX;
+        const top = e.pageY - this._offsetY;
+
+        this._left = Math.min(Math.max(left, 0), window.innerWidth - width);
+        this._top = Math.min(Math.max(top, 0), window.innerHeight - height);
 
         e.stopPropagation();
         e.preventDefault();
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 4712b1974..027bd492c 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -906,16 +906,16 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         this.tryUpdateHeight();
 
         // see if we need to preserve the insertion point
-        const prosediv = this.ProseRef?.children?.[0] as any;
-        const keeplocation = prosediv?.keeplocation;
+        const prosediv = this.ProseRef ?.children ?.[0] as any;
+        const keeplocation = prosediv ?.keeplocation;
         prosediv && (prosediv.keeplocation = undefined);
-        const pos = this._editorView?.state.selection.$from.pos || 1;
-        keeplocation && setTimeout(() => this._editorView?.dispatch(this._editorView?.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos))));
+        const pos = this._editorView ?.state.selection.$from.pos || 1;
+        keeplocation && setTimeout(() => this._editorView ?.dispatch(this._editorView ?.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos))));
 
         // jump rich text menu to this textbox
         if (this._ref.current) {
-            let x = Math.min(Math.max(this._ref.current!.getBoundingClientRect().x, 0), window.innerWidth - 445);
-            let y = this._ref.current!.getBoundingClientRect().y - 105;
+            const x = Math.min(Math.max(this._ref.current!.getBoundingClientRect().left, 0), window.innerWidth - RichTextMenu.Instance.width);
+            const y = this._ref.current!.getBoundingClientRect().top - RichTextMenu.Instance.height - 50;
             RichTextMenu.Instance.jumpTo(x, y);
         }
     }
@@ -933,7 +933,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         if ((this._editorView!.root as any).getSelection().isCollapsed) { // this is a hack to allow the cursor to be placed at the end of a document when the document ends in an inline dash comment.  Apparently Chrome on Windows has a bug/feature which breaks this when clicking after the end of the text.
             const pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
             const node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos); // get what prosemirror thinks the clicked node is (if it's null, then we didn't click on any text)
-            if (pcords && node?.type === this._editorView!.state.schema.nodes.dashComment) {
+            if (pcords && node ?.type === this._editorView!.state.schema.nodes.dashComment) {
                 this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, pcords.pos + 2)));
                 e.preventDefault();
             }
@@ -996,7 +996,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
                 for (let off = 1; off < 100; off++) {
                     const pos = this._editorView!.posAtCoords({ left: x + off, top: y });
                     const node = pos && this._editorView!.state.doc.nodeAt(pos.pos);
-                    if (node?.type === schema.nodes.list_item) {
+                    if (node ?.type === schema.nodes.list_item) {
                         list_node = node;
                         break;
                     }
@@ -1088,7 +1088,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         }
         if (e.key === "Escape") {
             this._editorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from)));
-            (document.activeElement as any).blur?.();
+            (document.activeElement as any).blur ?.();
             SelectionManager.DeselectAll();
         }
         e.stopPropagation();
@@ -1110,7 +1110,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
 
     @action
     tryUpdateHeight(limitHeight?: number) {
-        let scrollHeight = this._ref.current?.scrollHeight;
+        let scrollHeight = this._ref.current ?.scrollHeight;
         if (!this.layoutDoc.animateToPos && this.layoutDoc.autoHeight && scrollHeight &&
             getComputedStyle(this._ref.current!.parentElement!).top === "0px") {  // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
             if (limitHeight && scrollHeight > limitHeight) {
@@ -1173,7 +1173,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
                 {this.props.Document.hideSidebar ? (null) : this.sidebarWidthPercent === "0%" ?
                     <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} onClick={e => this.toggleSidebar()} /> :
                     <div className={"formattedTextBox-sidebar" + (InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : "")}
-                        style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${StrCast(this.extensionDoc?.backgroundColor, "transparent")}` }}>
+                        style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${StrCast(this.extensionDoc ?.backgroundColor, "transparent")}` }}>
                         <CollectionFreeFormView {...this.props}
                             PanelHeight={this.props.PanelHeight}
                             PanelWidth={() => this.sidebarWidth}
-- 
cgit v1.2.3-70-g09d2


From 3b08b8c340bf889c99f7b2fa76cda0f8b2d4b0a1 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Thu, 9 Jan 2020 15:03:54 -0500
Subject: refactored dropdown buttons into a buttondropdown component for
 richtextmenu

---
 src/client/util/RichTextMenu.tsx            | 382 +++++++++++++++-------------
 src/client/views/nodes/FormattedTextBox.tsx |   1 -
 2 files changed, 206 insertions(+), 177 deletions(-)

(limited to 'src/client/util/RichTextMenu.tsx')

diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx
index ca6b90e36..2d4f0fc1b 100644
--- a/src/client/util/RichTextMenu.tsx
+++ b/src/client/util/RichTextMenu.tsx
@@ -34,6 +34,12 @@ export default class RichTextMenu extends AntimodeMenu {
     private view?: EditorView;
     private editorProps: FieldViewProps & FormattedTextBoxProps | undefined;
 
+    private fontSizeOptions: { mark: Mark | null, title: string, label: string, command: any, hidden?: boolean }[];
+    private fontFamilyOptions: { mark: Mark | null, title: string, label: string, command: any, hidden?: boolean }[];
+    private listTypeOptions: { node: NodeType | any | null, title: string, label: string, command: any }[];
+    private fontColors: (string | undefined)[];
+    private highlightColors: (string | undefined)[];
+
     @observable private activeFontSize: string = "";
     @observable private activeFontFamily: string = "";
     @observable private activeListType: string = "";
@@ -55,6 +61,69 @@ export default class RichTextMenu extends AntimodeMenu {
         super(props);
         RichTextMenu.Instance = this;
         this._canFade = false;
+
+        this.fontSizeOptions = [
+            { mark: schema.marks.pFontSize.create({ fontSize: 7 }), title: "Set font size", label: "7pt", command: this.changeFontSize },
+            { mark: schema.marks.pFontSize.create({ fontSize: 8 }), title: "Set font size", label: "8pt", command: this.changeFontSize },
+            { mark: schema.marks.pFontSize.create({ fontSize: 9 }), title: "Set font size", label: "8pt", command: this.changeFontSize },
+            { mark: schema.marks.pFontSize.create({ fontSize: 10 }), title: "Set font size", label: "10pt", command: this.changeFontSize },
+            { mark: schema.marks.pFontSize.create({ fontSize: 12 }), title: "Set font size", label: "12pt", command: this.changeFontSize },
+            { mark: schema.marks.pFontSize.create({ fontSize: 14 }), title: "Set font size", label: "14pt", command: this.changeFontSize },
+            { mark: schema.marks.pFontSize.create({ fontSize: 16 }), title: "Set font size", label: "16pt", command: this.changeFontSize },
+            { mark: schema.marks.pFontSize.create({ fontSize: 18 }), title: "Set font size", label: "18pt", command: this.changeFontSize },
+            { mark: schema.marks.pFontSize.create({ fontSize: 20 }), title: "Set font size", label: "20pt", command: this.changeFontSize },
+            { mark: schema.marks.pFontSize.create({ fontSize: 24 }), title: "Set font size", label: "24pt", command: this.changeFontSize },
+            { mark: schema.marks.pFontSize.create({ fontSize: 32 }), title: "Set font size", label: "32pt", command: this.changeFontSize },
+            { mark: schema.marks.pFontSize.create({ fontSize: 48 }), title: "Set font size", label: "48pt", command: this.changeFontSize },
+            { mark: schema.marks.pFontSize.create({ fontSize: 72 }), title: "Set font size", label: "72pt", command: this.changeFontSize },
+            { mark: null, title: "", label: "various", command: unimplementedFunction, hidden: true },
+            { mark: null, title: "", label: "13pt", command: unimplementedFunction, hidden: true }, // this is here because the default size is 13, but there is no actual 13pt option
+        ];
+
+        this.fontFamilyOptions = [
+            { mark: schema.marks.pFontFamily.create({ family: "Times New Roman" }), title: "Set font family", label: "Times New Roman", command: this.changeFontFamily },
+            { mark: schema.marks.pFontFamily.create({ family: "Arial" }), title: "Set font family", label: "Arial", command: this.changeFontFamily },
+            { mark: schema.marks.pFontFamily.create({ family: "Georgia" }), title: "Set font family", label: "Georgia", command: this.changeFontFamily },
+            { mark: schema.marks.pFontFamily.create({ family: "Comic Sans MS" }), title: "Set font family", label: "Comic Sans MS", command: this.changeFontFamily },
+            { mark: schema.marks.pFontFamily.create({ family: "Tahoma" }), title: "Set font family", label: "Tahoma", command: this.changeFontFamily },
+            { mark: schema.marks.pFontFamily.create({ family: "Impact" }), title: "Set font family", label: "Impact", command: this.changeFontFamily },
+            { mark: schema.marks.pFontFamily.create({ family: "Crimson Text" }), title: "Set font family", label: "Crimson Text", command: this.changeFontFamily },
+            { mark: null, title: "", label: "various", command: unimplementedFunction, hidden: true },
+            // { mark: null, title: "", label: "default", command: unimplementedFunction, hidden: true },
+        ];
+
+        this.listTypeOptions = [
+            { node: schema.nodes.ordered_list.create({ mapStyle: "bullet" }), title: "Set list type", label: ":", command: this.changeListType },
+            { node: schema.nodes.ordered_list.create({ mapStyle: "decimal" }), title: "Set list type", label: "1.1", command: this.changeListType },
+            { node: schema.nodes.ordered_list.create({ mapStyle: "multi" }), title: "Set list type", label: "1.A", command: this.changeListType },
+            { node: undefined, title: "Set list type", label: "Remove", command: this.changeListType },
+        ];
+
+        this.fontColors = [
+            DarkPastelSchemaPalette.get("pink2"),
+            DarkPastelSchemaPalette.get("purple4"),
+            DarkPastelSchemaPalette.get("bluegreen1"),
+            DarkPastelSchemaPalette.get("yellow4"),
+            DarkPastelSchemaPalette.get("red2"),
+            DarkPastelSchemaPalette.get("bluegreen7"),
+            DarkPastelSchemaPalette.get("bluegreen5"),
+            DarkPastelSchemaPalette.get("orange1"),
+            "#757472",
+            "#000"
+        ];
+
+        this.highlightColors = [
+            PastelSchemaPalette.get("pink2"),
+            PastelSchemaPalette.get("purple4"),
+            PastelSchemaPalette.get("bluegreen1"),
+            PastelSchemaPalette.get("yellow4"),
+            PastelSchemaPalette.get("red2"),
+            PastelSchemaPalette.get("bluegreen7"),
+            PastelSchemaPalette.get("bluegreen5"),
+            PastelSchemaPalette.get("orange1"),
+            "white",
+            "transparent"
+        ];
     }
 
     @action
@@ -74,27 +143,23 @@ export default class RichTextMenu extends AntimodeMenu {
         }
         this.view = view;
         const state = view.state;
-        // DocumentDecorations.Instance.showTextBar();
         props && (this.editorProps = props);
+
         // Don't do anything if the document/selection didn't change
         if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) return;
 
-        // this.reset_mark_doms();
 
         // update active font family and size
         const active = this.getActiveFontStylesOnSelection();
         const activeFamilies = active && active.get("families");
         const activeSizes = active && active.get("sizes");
 
-        console.log("update from dash, activefontsize", this.activeFontSize, activeSizes, activeSizes && activeSizes.length, activeSizes && String(activeSizes[0]));
         this.activeFontFamily = !activeFamilies || activeFamilies.length === 0 ? "Arial" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various";
         this.activeFontSize = !activeSizes || activeSizes.length === 0 ? "13pt" : activeSizes.length === 1 ? String(activeSizes[0]) + "pt" : "various";
 
         // update link in current selection
         const targetTitle = await this.getTextLinkTargetTitle();
         this.setCurrentLink(targetTitle);
-
-        // this.update_mark_doms();
     }
 
     setMark = (mark: Mark, state: EditorState<any>, dispatch: any) => {
@@ -134,7 +199,7 @@ export default class RichTextMenu extends AntimodeMenu {
             });
         }
 
-        let styles = new Map<String, String[]>();
+        const styles = new Map<String, String[]>();
         styles.set("families", activeFamilies);
         styles.set("sizes", activeSizes);
         return styles;
@@ -183,7 +248,6 @@ export default class RichTextMenu extends AntimodeMenu {
         function onChange(e: React.ChangeEvent<HTMLSelectElement>) {
             e.stopPropagation();
             e.preventDefault();
-            console.log("on change marks");
             options.forEach(({ label, mark, command }) => {
                 if (e.target.value === label) {
                     self.view && mark && command(mark, self.view);
@@ -230,7 +294,6 @@ export default class RichTextMenu extends AntimodeMenu {
 
     changeFontFamily = (mark: Mark, view: EditorView) => {
         const fontName = mark.attrs.family;
-        // if (fontName) { this.updateFontStyleDropdown(fontName); }
         if (this.editorProps) {
             const ruleProvider = this.editorProps.ruleProvider;
             const heading = NumCast(this.editorProps.Document.heading);
@@ -288,12 +351,6 @@ export default class RichTextMenu extends AntimodeMenu {
             self.view && self.view.focus();
             self.view && self.fillBrush(self.view.state, self.view.dispatch);
         }
-        function onDropdownClick(e: React.PointerEvent) {
-            e.preventDefault();
-            e.stopPropagation();
-            self.view && self.view.focus();
-            self.toggleBrushDropdown();
-        }
 
         let label = "Stored marks: ";
         if (this.brushMarks && this.brushMarks.size > 0) {
@@ -307,20 +364,20 @@ export default class RichTextMenu extends AntimodeMenu {
             label = "No marks are currently stored";
         }
 
+        const button =
+            <button className="antimodeMenu-button" title="" onPointerDown={onBrushClick} style={this.brushMarks && this.brushMarks.size > 0 ? { backgroundColor: "121212" } : {}}>
+                <FontAwesomeIcon icon="paint-roller" size="lg" style={{ transition: "transform 0.1s", transform: this.brushMarks && this.brushMarks.size > 0 ? "rotate(45deg)" : "" }} />
+            </button>;
+
+        const dropdownContent =
+            <div className="dropdown">
+                <p>{label}</p>
+                <button onPointerDown={this.clearBrush}>Clear brush</button>
+                {/* <input placeholder="Enter URL"></input> */}
+            </div>;
+
         return (
-            <div className="button-dropdown-wrapper">
-                <button className="antimodeMenu-button" title="" onPointerDown={onBrushClick} style={this.brushMarks && this.brushMarks.size > 0 ? { backgroundColor: "121212" } : {}}>
-                    <FontAwesomeIcon icon="paint-roller" size="lg" style={{ transition: "transform 0.1s", transform: this.brushMarks && this.brushMarks.size > 0 ? "rotate(45deg)" : "" }} />
-                </button>
-                <button className="dropdown-button antimodeMenu-button" onPointerDown={onDropdownClick}><FontAwesomeIcon icon="caret-down" size="sm" /></button>
-                {this.showBrushDropdown ?
-                    (<div className="dropdown">
-                        <p>{label}</p>
-                        <button onPointerDown={this.clearBrush}>Clear brush</button>
-                        {/* <input placeholder="Enter URL"></input> */}
-                    </div>)
-                    : <></>}
-            </div>
+            <ButtonDropdown view={this.view} button={button} dropdownContent={dropdownContent} />
         );
     }
 
@@ -340,7 +397,6 @@ export default class RichTextMenu extends AntimodeMenu {
                 this.brushMarks = selected_marks;
                 this.brushIsEmpty = !this.brushIsEmpty;
             }
-            // }
         }
         else {
             const { from, to, $from } = this.view.state.selection;
@@ -369,12 +425,6 @@ export default class RichTextMenu extends AntimodeMenu {
             self.view && self.view.focus();
             self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch);
         }
-        function onDropdownClick(e: React.PointerEvent) {
-            e.preventDefault();
-            e.stopPropagation();
-            self.view && self.view.focus();
-            self.toggleColorDropdown();
-        }
         function changeColor(e: React.PointerEvent, color: string) {
             e.preventDefault();
             e.stopPropagation();
@@ -383,41 +433,28 @@ export default class RichTextMenu extends AntimodeMenu {
             self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch);
         }
 
-        const colors = [
-            DarkPastelSchemaPalette.get("pink2"),
-            DarkPastelSchemaPalette.get("purple4"),
-            DarkPastelSchemaPalette.get("bluegreen1"),
-            DarkPastelSchemaPalette.get("yellow4"),
-            DarkPastelSchemaPalette.get("red2"),
-            DarkPastelSchemaPalette.get("bluegreen7"),
-            DarkPastelSchemaPalette.get("bluegreen5"),
-            DarkPastelSchemaPalette.get("orange1"),
-            "#757472",
-            "#000"
-        ];
+        const button =
+            <button className="antimodeMenu-button color-preview-button" title="" onPointerDown={onColorClick}>
+                <FontAwesomeIcon icon="palette" size="lg" />
+                <div className="color-preview" style={{ backgroundColor: this.activeFontColor }}></div>
+            </button>;
+
+        const dropdownContent =
+            <div className="dropdown">
+                <p>Change font color:</p>
+                <div className="color-wrapper">
+                    {this.fontColors.map(color => {
+                        if (color) {
+                            return this.activeFontColor === color ?
+                                <button className="color-button active" style={{ backgroundColor: color }} onPointerDown={e => changeColor(e, color)}></button> :
+                                <button className="color-button" style={{ backgroundColor: color }} onPointerDown={e => changeColor(e, color)}></button>;
+                        }
+                    })}
+                </div>
+            </div>;
 
         return (
-            <div className="button-dropdown-wrapper">
-                <button className="antimodeMenu-button color-preview-button" title="" onPointerDown={onColorClick}>
-                    <FontAwesomeIcon icon="palette" size="lg" />
-                    <div className="color-preview" style={{ backgroundColor: this.activeFontColor }}></div>
-                </button>
-                <button className="dropdown-button antimodeMenu-button" onPointerDown={onDropdownClick}><FontAwesomeIcon icon="caret-down" size="sm" /></button>
-                {this.showColorDropdown ?
-                    (<div className="dropdown">
-                        <p>Change font color:</p>
-                        <div className="color-wrapper">
-                            {colors.map(color => {
-                                if (color) {
-                                    return this.activeFontColor === color ?
-                                        <button className="color-button active" style={{ backgroundColor: color }} onPointerDown={e => changeColor(e, color)}></button> :
-                                        <button className="color-button" style={{ backgroundColor: color }} onPointerDown={e => changeColor(e, color)}></button>;
-                                }
-                            })}
-                        </div>
-                    </div>)
-                    : <></>}
-            </div>
+            <ButtonDropdown view={this.view} button={button} dropdownContent={dropdownContent} />
         );
     }
 
@@ -441,12 +478,6 @@ export default class RichTextMenu extends AntimodeMenu {
             self.view && self.view.focus();
             self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch);
         }
-        function onDropdownClick(e: React.PointerEvent) {
-            e.preventDefault();
-            e.stopPropagation();
-            self.view && self.view.focus();
-            self.toggleHighlightDropdown();
-        }
         function changeHighlight(e: React.PointerEvent, color: string) {
             e.preventDefault();
             e.stopPropagation();
@@ -455,41 +486,28 @@ export default class RichTextMenu extends AntimodeMenu {
             self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch);
         }
 
-        const colors = [
-            PastelSchemaPalette.get("pink2"),
-            PastelSchemaPalette.get("purple4"),
-            PastelSchemaPalette.get("bluegreen1"),
-            PastelSchemaPalette.get("yellow4"),
-            PastelSchemaPalette.get("red2"),
-            PastelSchemaPalette.get("bluegreen7"),
-            PastelSchemaPalette.get("bluegreen5"),
-            PastelSchemaPalette.get("orange1"),
-            "white",
-            "transparent"
-        ];
+        const button =
+            <button className="antimodeMenu-button color-preview-button" title="" onPointerDown={onHighlightClick}>
+                <FontAwesomeIcon icon="highlighter" size="lg" />
+                <div className="color-preview" style={{ backgroundColor: this.activeHighlightColor }}></div>
+            </button>;
+
+        const dropdownContent =
+            <div className="dropdown">
+                <p>Change highlight color:</p>
+                <div className="color-wrapper">
+                    {this.highlightColors.map(color => {
+                        if (color) {
+                            return this.activeHighlightColor === color ?
+                                <button className="color-button active" style={{ backgroundColor: color }} onPointerDown={e => changeHighlight(e, color)}>{color === "transparent" ? "X" : ""}</button> :
+                                <button className="color-button" style={{ backgroundColor: color }} onPointerDown={e => changeHighlight(e, color)}>{color === "transparent" ? "X" : ""}</button>;
+                        }
+                    })}
+                </div>
+            </div>;
 
         return (
-            <div className="button-dropdown-wrapper">
-                <button className="antimodeMenu-button color-preview-button" title="" onPointerDown={onHighlightClick}>
-                    <FontAwesomeIcon icon="highlighter" size="lg" />
-                    <div className="color-preview" style={{ backgroundColor: this.activeHighlightColor }}></div>
-                </button>
-                <button className="dropdown-button antimodeMenu-button" onPointerDown={onDropdownClick}><FontAwesomeIcon icon="caret-down" size="sm" /></button>
-                {this.showHighlightDropdown ?
-                    (<div className="dropdown">
-                        <p>Change highlight color:</p>
-                        <div className="color-wrapper">
-                            {colors.map(color => {
-                                if (color) {
-                                    return this.activeHighlightColor === color ?
-                                        <button className="color-button active" style={{ backgroundColor: color }} onPointerDown={e => changeHighlight(e, color)}>{color === "transparent" ? "X" : ""}</button> :
-                                        <button className="color-button" style={{ backgroundColor: color }} onPointerDown={e => changeHighlight(e, color)}>{color === "transparent" ? "X" : ""}</button>;
-                                }
-                            })}
-                        </div>
-                    </div>)
-                    : <></>}
-            </div>
+            <ButtonDropdown view={this.view} button={button} dropdownContent={dropdownContent} />
         );
     }
 
@@ -503,32 +521,26 @@ export default class RichTextMenu extends AntimodeMenu {
 
     createLinkButton() {
         const self = this;
-        function onDropdownClick(e: React.PointerEvent) {
-            e.preventDefault();
-            e.stopPropagation();
-            self.view && self.view.focus();
-            self.toggleLinkDropdown();
-        }
+
         function onLinkChange(e: React.ChangeEvent<HTMLInputElement>) {
             self.setCurrentLink(e.target.value);
         }
 
         const link = this.currentLink ? this.currentLink : "";
 
+        const button = <button className="antimodeMenu-button" title=""><FontAwesomeIcon icon="link" size="lg" /></button>;
+
+        const dropdownContent =
+            <div className="dropdown link-menu">
+                <p>Linked to:</p>
+                <input value={link} placeholder="Enter URL" onChange={onLinkChange} />
+                <button className="make-button" onPointerDown={e => this.makeLinkToURL(link, "onRight")}>Apply hyperlink</button>
+                <div className="divider"></div>
+                <button className="remove-button" onPointerDown={e => this.deleteLink()}>Remove link</button>
+            </div>;
+
         return (
-            <div className="button-dropdown-wrapper">
-                <button className="antimodeMenu-button" title="" onPointerDown={onDropdownClick}><FontAwesomeIcon icon="link" size="lg" /></button>
-                <button className="dropdown-button antimodeMenu-button" onPointerDown={onDropdownClick}><FontAwesomeIcon icon="caret-down" size="sm" /></button>
-                {this.showLinkDropdown ?
-                    (<div className="dropdown link-menu">
-                        <p>Linked to:</p>
-                        <input value={link} placeholder="Enter URL" onChange={onLinkChange} />
-                        <button className="make-button" onPointerDown={e => this.makeLinkToURL(link, "onRight")}>Apply hyperlink</button>
-                        <div className="divider"></div>
-                        <button className="remove-button" onPointerDown={e => this.deleteLink()}>Remove link</button>
-                    </div>)
-                    : <></>}
-            </div>
+            <ButtonDropdown view={this.view} button={button} dropdownContent={dropdownContent} openDropdownOnButton={true} />
         );
     }
 
@@ -598,7 +610,7 @@ export default class RichTextMenu extends AntimodeMenu {
                 }
             } else {
                 if (node) {
-                    let extension = this.linkExtend(this.view!.state.selection.$anchor, href);
+                    const extension = this.linkExtend(this.view!.state.selection.$anchor, href);
                     this.view!.dispatch(this.view!.state.tr.removeMark(extension.from, extension.to, this.view!.state.schema.marks.link));
                 }
             }
@@ -617,7 +629,7 @@ export default class RichTextMenu extends AntimodeMenu {
         let startPos = $start.start();
         let endPos = startPos;
         for (let i = 0; i < endIndex; i++) {
-            let size = $start.parent.child(i).nodeSize;
+            const size = $start.parent.child(i).nodeSize;
             if (i < startIndex) startPos += size;
             endPos += size;
         }
@@ -665,43 +677,6 @@ export default class RichTextMenu extends AntimodeMenu {
 
     render() {
 
-        const fontSizeOptions = [
-            { mark: schema.marks.pFontSize.create({ fontSize: 7 }), title: "Set font size", label: "7pt", command: this.changeFontSize },
-            { mark: schema.marks.pFontSize.create({ fontSize: 8 }), title: "Set font size", label: "8pt", command: this.changeFontSize },
-            { mark: schema.marks.pFontSize.create({ fontSize: 9 }), title: "Set font size", label: "8pt", command: this.changeFontSize },
-            { mark: schema.marks.pFontSize.create({ fontSize: 10 }), title: "Set font size", label: "10pt", command: this.changeFontSize },
-            { mark: schema.marks.pFontSize.create({ fontSize: 12 }), title: "Set font size", label: "12pt", command: this.changeFontSize },
-            { mark: schema.marks.pFontSize.create({ fontSize: 14 }), title: "Set font size", label: "14pt", command: this.changeFontSize },
-            { mark: schema.marks.pFontSize.create({ fontSize: 16 }), title: "Set font size", label: "16pt", command: this.changeFontSize },
-            { mark: schema.marks.pFontSize.create({ fontSize: 18 }), title: "Set font size", label: "18pt", command: this.changeFontSize },
-            { mark: schema.marks.pFontSize.create({ fontSize: 20 }), title: "Set font size", label: "20pt", command: this.changeFontSize },
-            { mark: schema.marks.pFontSize.create({ fontSize: 24 }), title: "Set font size", label: "24pt", command: this.changeFontSize },
-            { mark: schema.marks.pFontSize.create({ fontSize: 32 }), title: "Set font size", label: "32pt", command: this.changeFontSize },
-            { mark: schema.marks.pFontSize.create({ fontSize: 48 }), title: "Set font size", label: "48pt", command: this.changeFontSize },
-            { mark: schema.marks.pFontSize.create({ fontSize: 72 }), title: "Set font size", label: "72pt", command: this.changeFontSize },
-            { mark: null, title: "", label: "various", command: unimplementedFunction, hidden: true },
-            { mark: null, title: "", label: "13pt", command: unimplementedFunction, hidden: true }, // this is here because the default size is 13, but there is no actual 13pt option
-        ];
-
-        const fontFamilyOptions = [
-            { mark: schema.marks.pFontFamily.create({ family: "Times New Roman" }), title: "Set font family", label: "Times New Roman", command: this.changeFontFamily },
-            { mark: schema.marks.pFontFamily.create({ family: "Arial" }), title: "Set font family", label: "Arial", command: this.changeFontFamily },
-            { mark: schema.marks.pFontFamily.create({ family: "Georgia" }), title: "Set font family", label: "Georgia", command: this.changeFontFamily },
-            { mark: schema.marks.pFontFamily.create({ family: "Comic Sans MS" }), title: "Set font family", label: "Comic Sans MS", command: this.changeFontFamily },
-            { mark: schema.marks.pFontFamily.create({ family: "Tahoma" }), title: "Set font family", label: "Tahoma", command: this.changeFontFamily },
-            { mark: schema.marks.pFontFamily.create({ family: "Impact" }), title: "Set font family", label: "Impact", command: this.changeFontFamily },
-            { mark: schema.marks.pFontFamily.create({ family: "Crimson Text" }), title: "Set font family", label: "Crimson Text", command: this.changeFontFamily },
-            { mark: null, title: "", label: "various", command: unimplementedFunction, hidden: true },
-            // { mark: null, title: "", label: "default", command: unimplementedFunction, hidden: true },
-        ];
-
-        const listTypeOptions = [
-            { node: schema.nodes.ordered_list.create({ mapStyle: "bullet" }), title: "Set list type", label: ":", command: this.changeListType },
-            { node: schema.nodes.ordered_list.create({ mapStyle: "decimal" }), title: "Set list type", label: "1.1", command: this.changeListType },
-            { node: schema.nodes.ordered_list.create({ mapStyle: "multi" }), title: "Set list type", label: "1.A", command: this.changeListType },
-            { node: undefined, title: "Set list type", label: "Remove", command: this.changeListType },
-        ];
-
         const row1 = <div className="antimodeMenu-row">{[
             this.createButton("bold", "Bold", toggleMark(schema.marks.strong)),
             this.createButton("italic", "Italic", toggleMark(schema.marks.em)),
@@ -714,30 +689,85 @@ export default class RichTextMenu extends AntimodeMenu {
             this.createLinkButton(),
             this.createBrushButton(),
             this.createButton("indent", "Summarize", undefined, this.insertSummarizer),
-        ]}</div>
+        ]}</div>;
 
         const row2 = <div className="antimodeMenu-row row-2">
-            <div>{[
-                this.createMarksDropdown(this.activeFontSize, fontSizeOptions),
-                this.createMarksDropdown(this.activeFontFamily, fontFamilyOptions),
-                this.createNodesDropdown(this.activeListType, listTypeOptions),
-            ]}</div>
+            <div>
+                {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions),
+                this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions),
+                this.createNodesDropdown(this.activeListType, this.listTypeOptions)]}
+            </div>
             <div>
                 <button className="antimodeMenu-button" title="Pin menu" onClick={this.toggleMenuPin} style={this.Pinned ? { backgroundColor: "#121212" } : {}}>
                     <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this.Pinned ? "rotate(45deg)" : "" }} />
                 </button>
                 {this.getDragger()}
             </div>
-        </div>
-
-        const buttons = [
-            row1, row2
-        ];
+        </div>;
 
         return (
             <div className="richTextMenu" onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
-                {this.getElementWithRows(buttons, 2, false)}
+                {this.getElementWithRows([row1, row2], 2, false)}
             </div>
         );
     }
+}
+
+interface ButtonDropdownProps {
+    // Document: Doc;
+    // remove: (self: ImportMetadataEntry) => void;
+    // next: () => void;
+    view?: EditorView;
+    button: JSX.Element;
+    dropdownContent: JSX.Element;
+    openDropdownOnButton?: boolean;
+}
+
+@observer
+class ButtonDropdown extends React.Component<ButtonDropdownProps> {
+
+    @observable private showDropdown: boolean = false;
+    private ref: HTMLDivElement | null = null;
+
+    componentDidMount() {
+        document.addEventListener("pointerdown", this.onBlur);
+    }
+
+    componentWillUnmount() {
+        document.removeEventListener("pointerdown", this.onBlur);
+    }
+
+    @action
+    setShowDropdown(show: boolean) {
+        this.showDropdown = show;
+    }
+    @action
+    toggleDropdown() {
+        this.showDropdown = !this.showDropdown;
+    }
+
+    onDropdownClick = (e: React.PointerEvent) => {
+        e.preventDefault();
+        e.stopPropagation();
+        this.props.view && this.props.view.focus();
+        this.toggleDropdown();
+    }
+
+    onBlur = (e: PointerEvent) => {
+        setTimeout(() => {
+            if (this.ref !== null && !this.ref.contains(e.target as Node)) {
+                this.setShowDropdown(false);
+            }
+        }, 0);
+    }
+
+    render() {
+        return (
+            <div className="button-dropdown-wrapper" ref={node => this.ref = node}>
+                {this.props.openDropdownOnButton ? <div onPointerDown={this.onDropdownClick}>{this.props.button}</div> : this.props.button}
+                <button className="dropdown-button antimodeMenu-button" onPointerDown={this.onDropdownClick}><FontAwesomeIcon icon="caret-down" size="sm" /></button>
+                {this.showDropdown ? this.props.dropdownContent : <></>}
+            </div>
+        )
+    }
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 027bd492c..df055a2ab 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1057,7 +1057,6 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         });
     }
     onBlur = (e: any) => {
-        console.log("formated blur");
         //DictationManager.Controls.stop(false);
         if (this._undoTyping) {
             this._undoTyping.end();
-- 
cgit v1.2.3-70-g09d2


From 47700705b6e0304eaed00843959561b5baa0a31a Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Thu, 9 Jan 2020 15:14:19 -0500
Subject: added styling options to select dropdowns on rich text menu

---
 src/client/util/RichTextMenu.tsx | 45 ++++++++++++++++++++--------------------
 1 file changed, 23 insertions(+), 22 deletions(-)

(limited to 'src/client/util/RichTextMenu.tsx')

diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx
index 2d4f0fc1b..da5979f57 100644
--- a/src/client/util/RichTextMenu.tsx
+++ b/src/client/util/RichTextMenu.tsx
@@ -34,9 +34,9 @@ export default class RichTextMenu extends AntimodeMenu {
     private view?: EditorView;
     private editorProps: FieldViewProps & FormattedTextBoxProps | undefined;
 
-    private fontSizeOptions: { mark: Mark | null, title: string, label: string, command: any, hidden?: boolean }[];
-    private fontFamilyOptions: { mark: Mark | null, title: string, label: string, command: any, hidden?: boolean }[];
-    private listTypeOptions: { node: NodeType | any | null, title: string, label: string, command: any }[];
+    private fontSizeOptions: { mark: Mark | null, title: string, label: string, command: any, hidden?: boolean, style?: {} }[];
+    private fontFamilyOptions: { mark: Mark | null, title: string, label: string, command: any, hidden?: boolean, style?: {} }[];
+    private listTypeOptions: { node: NodeType | any | null, title: string, label: string, command: any, style?: {} }[];
     private fontColors: (string | undefined)[];
     private highlightColors: (string | undefined)[];
 
@@ -81,13 +81,13 @@ export default class RichTextMenu extends AntimodeMenu {
         ];
 
         this.fontFamilyOptions = [
-            { mark: schema.marks.pFontFamily.create({ family: "Times New Roman" }), title: "Set font family", label: "Times New Roman", command: this.changeFontFamily },
-            { mark: schema.marks.pFontFamily.create({ family: "Arial" }), title: "Set font family", label: "Arial", command: this.changeFontFamily },
-            { mark: schema.marks.pFontFamily.create({ family: "Georgia" }), title: "Set font family", label: "Georgia", command: this.changeFontFamily },
-            { mark: schema.marks.pFontFamily.create({ family: "Comic Sans MS" }), title: "Set font family", label: "Comic Sans MS", command: this.changeFontFamily },
-            { mark: schema.marks.pFontFamily.create({ family: "Tahoma" }), title: "Set font family", label: "Tahoma", command: this.changeFontFamily },
-            { mark: schema.marks.pFontFamily.create({ family: "Impact" }), title: "Set font family", label: "Impact", command: this.changeFontFamily },
-            { mark: schema.marks.pFontFamily.create({ family: "Crimson Text" }), title: "Set font family", label: "Crimson Text", command: this.changeFontFamily },
+            { mark: schema.marks.pFontFamily.create({ family: "Times New Roman" }), title: "Set font family", label: "Times New Roman", command: this.changeFontFamily, style: { fontFamily: "Times New Roman" } },
+            { mark: schema.marks.pFontFamily.create({ family: "Arial" }), title: "Set font family", label: "Arial", command: this.changeFontFamily, style: { fontFamily: "Arial" } },
+            { mark: schema.marks.pFontFamily.create({ family: "Georgia" }), title: "Set font family", label: "Georgia", command: this.changeFontFamily, style: { fontFamily: "Georgia" } },
+            { mark: schema.marks.pFontFamily.create({ family: "Comic Sans MS" }), title: "Set font family", label: "Comic Sans MS", command: this.changeFontFamily, style: { fontFamily: "Comic Sans MS" } },
+            { mark: schema.marks.pFontFamily.create({ family: "Tahoma" }), title: "Set font family", label: "Tahoma", command: this.changeFontFamily, style: { fontFamily: "Tahoma" } },
+            { mark: schema.marks.pFontFamily.create({ family: "Impact" }), title: "Set font family", label: "Impact", command: this.changeFontFamily, style: { fontFamily: "Impact" } },
+            { mark: schema.marks.pFontFamily.create({ family: "Crimson Text" }), title: "Set font family", label: "Crimson Text", command: this.changeFontFamily, style: { fontFamily: "Crimson Text" } },
             { mark: null, title: "", label: "various", command: unimplementedFunction, hidden: true },
             // { mark: null, title: "", label: "default", command: unimplementedFunction, hidden: true },
         ];
@@ -154,6 +154,7 @@ export default class RichTextMenu extends AntimodeMenu {
         const activeFamilies = active && active.get("families");
         const activeSizes = active && active.get("sizes");
 
+        console.log("active families", activeFamilies);
         this.activeFontFamily = !activeFamilies || activeFamilies.length === 0 ? "Arial" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various";
         this.activeFontSize = !activeSizes || activeSizes.length === 0 ? "13pt" : activeSizes.length === 1 ? String(activeSizes[0]) + "pt" : "various";
 
@@ -232,16 +233,16 @@ export default class RichTextMenu extends AntimodeMenu {
         );
     }
 
-    createMarksDropdown(activeOption: string, options: { mark: Mark | null, title: string, label: string, command: (mark: Mark, view: EditorView) => void, hidden?: boolean }[]): JSX.Element {
-        const items = options.map(({ title, label, hidden }) => {
+    createMarksDropdown(activeOption: string, options: { mark: Mark | null, title: string, label: string, command: (mark: Mark, view: EditorView) => void, hidden?: boolean, style?: {} }[]): JSX.Element {
+        const items = options.map(({ title, label, hidden, style }) => {
             if (hidden) {
                 return label === activeOption ?
-                    <option value={label} title={title} selected hidden>{label}</option> :
-                    <option value={label} title={title} hidden>{label}</option>;
+                    <option value={label} title={title} style={style ? style : {}} selected hidden>{label}</option> :
+                    <option value={label} title={title} style={style ? style : {}} hidden>{label}</option>;
             }
             return label === activeOption ?
-                <option value={label} title={title} selected>{label}</option> :
-                <option value={label} title={title}>{label}</option>;
+                <option value={label} title={title} style={style ? style : {}} selected>{label}</option> :
+                <option value={label} title={title} style={style ? style : {}}>{label}</option>;
         });
 
         const self = this;
@@ -257,16 +258,16 @@ export default class RichTextMenu extends AntimodeMenu {
         return <select onChange={onChange}>{items}</select>;
     }
 
-    createNodesDropdown(activeOption: string, options: { node: NodeType | any | null, title: string, label: string, command: (node: NodeType | any) => void, hidden?: boolean }[]): JSX.Element {
-        const items = options.map(({ title, label, hidden }) => {
+    createNodesDropdown(activeOption: string, options: { node: NodeType | any | null, title: string, label: string, command: (node: NodeType | any) => void, hidden?: boolean, style?: {} }[]): JSX.Element {
+        const items = options.map(({ title, label, hidden, style }) => {
             if (hidden) {
                 return label === activeOption ?
-                    <option value={label} title={title} selected hidden>{label}</option> :
-                    <option value={label} title={title} hidden>{label}</option>;
+                    <option value={label} title={title} style={style ? style : {}} selected hidden>{label}</option> :
+                    <option value={label} title={title} style={style ? style : {}} hidden>{label}</option>;
             }
             return label === activeOption ?
-                <option value={label} title={title} selected>{label}</option> :
-                <option value={label} title={title}>{label}</option>;
+                <option value={label} title={title} style={style ? style : {}} selected>{label}</option> :
+                <option value={label} title={title} style={style ? style : {}}>{label}</option>;
         });
 
         const self = this;
-- 
cgit v1.2.3-70-g09d2


From e0a8d325d601d305a166c1683dccfcc67e91fe95 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Thu, 9 Jan 2020 15:29:47 -0500
Subject: code cleaning richtextmenu

---
 src/client/util/RichTextMenu.tsx | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

(limited to 'src/client/util/RichTextMenu.tsx')

diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx
index da5979f57..454afe3b7 100644
--- a/src/client/util/RichTextMenu.tsx
+++ b/src/client/util/RichTextMenu.tsx
@@ -155,6 +155,7 @@ export default class RichTextMenu extends AntimodeMenu {
         const activeSizes = active && active.get("sizes");
 
         console.log("active families", activeFamilies);
+        console.log("other active families", this.activeFontFamilyOnSelection());
         this.activeFontFamily = !activeFamilies || activeFamilies.length === 0 ? "Arial" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various";
         this.activeFontSize = !activeSizes || activeSizes.length === 0 ? "13pt" : activeSizes.length === 1 ? String(activeSizes[0]) + "pt" : "various";
 
@@ -184,10 +185,26 @@ export default class RichTextMenu extends AntimodeMenu {
         }
     }
 
+    activeFontFamilyOnSelection() {
+        if (!this.view) return;
+
+        //current selection
+        const state = this.view.state;
+        const activeFamilies: string[] = [];
+        const pos = this.view.state.selection.$from;
+        const ref_node: ProsNode | null = this.reference_node(pos);
+        if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) {
+            ref_node.marks.forEach(m => m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family));
+        }
+        return activeFamilies;
+    }
+
     // finds font sizes and families in selection
     getActiveFontStylesOnSelection() {
         if (!this.view) return;
 
+        console.log("\nget active font styles");
+
         const activeFamilies: string[] = [];
         const activeSizes: string[] = [];
         const state = this.view.state;
@@ -195,6 +212,7 @@ export default class RichTextMenu extends AntimodeMenu {
         const ref_node = this.reference_node(pos);
         if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) {
             ref_node.marks.forEach(m => {
+                console.log("attribute", m.attrs);
                 m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family);
                 m.type === state.schema.marks.pFontSize && activeSizes.push(String(m.attrs.fontSize) + "pt");
             });
-- 
cgit v1.2.3-70-g09d2


From 5abc4de3c3495db71b0a9bcbe2468828f674eff3 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Thu, 9 Jan 2020 16:41:52 -0500
Subject: minor merge

---
 package-lock.json                   |  6 +-----
 src/client/util/RichTextMenu.tsx    |  4 ++--
 src/client/util/TooltipTextMenu.tsx | 14 ++++++--------
 src/client/views/AntimodeMenu.tsx   |  2 +-
 4 files changed, 10 insertions(+), 16 deletions(-)

(limited to 'src/client/util/RichTextMenu.tsx')

diff --git a/package-lock.json b/package-lock.json
index cad3edd82..6aa0ffc02 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7073,11 +7073,7 @@
                 "babel-runtime": "^6.22.0",
                 "bluebird": "^3.4.7",
                 "boolify-string": "^2.0.2",
-<<<<<<< HEAD
-                "emit-logger": "github:chocolateboy/emit-logger#b9d25a2d939e42f29c940861e9648bd0fb810070",
-=======
                 "emit-logger": "github:chocolateboy/emit-logger#better-emitter-name",
->>>>>>> 540bda7295f6ee7c2eed848598de6f5df74b2723
                 "lodash": "^4.17.4",
                 "semver": "^5.0.3",
                 "source-map-support": "^0.5.9"
@@ -18091,4 +18087,4 @@
             }
         }
     }
-}
+}
\ No newline at end of file
diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx
index e367cf350..375c10da3 100644
--- a/src/client/util/RichTextMenu.tsx
+++ b/src/client/util/RichTextMenu.tsx
@@ -208,7 +208,7 @@ export default class RichTextMenu extends AntimodeMenu {
     getMarksInSelection(state: EditorState<any>) {
         const found = new Set<Mark>();
         const { from, to } = state.selection as TextSelection;
-        state.doc.nodesBetween(from, to, (node) => node.marks ?.forEach(m => found.add(m)));
+        state.doc.nodesBetween(from, to, (node) => node.marks.forEach(m => found.add(m)));
         return found;
     }
 
@@ -768,6 +768,6 @@ class ButtonDropdown extends React.Component<ButtonDropdownProps> {
                 <button className="dropdown-button antimodeMenu-button" onPointerDown={this.onDropdownClick}><FontAwesomeIcon icon="caret-down" size="sm" /></button>
                 {this.showDropdown ? this.props.dropdownContent : <></>}
             </div>
-        )
+        );
     }
 }
\ No newline at end of file
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 8177593c2..1c15dca7f 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -18,8 +18,6 @@ import { SelectionManager } from './SelectionManager';
 import { PastelSchemaPalette, DarkPastelSchemaPalette } from '../../new_fields/SchemaHeaderField';
 const { toggleMark } = require("prosemirror-commands");
 
-// deprecated in favor of richtextmenu
-
 //appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc.
 export class TooltipTextMenu {
     public static Toolbar: HTMLDivElement | undefined;
@@ -95,7 +93,7 @@ export class TooltipTextMenu {
             { mark: schema.marks.subscript, dom: svgIcon("subscript", "Subscript", "M496 448h-16V304a16 16 0 0 0-16-16h-48a16 16 0 0 0-14.29 8.83l-16 32A16 16 0 0 0 400 352h16v96h-16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h96a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM336 64h-67a16 16 0 0 0-13.14 6.87l-79.9 115-79.9-115A16 16 0 0 0 83 64H16A16 16 0 0 0 0 80v48a16 16 0 0 0 16 16h33.48l77.81 112-77.81 112H16a16 16 0 0 0-16 16v48a16 16 0 0 0 16 16h67a16 16 0 0 0 13.14-6.87l79.9-115 79.9 115A16 16 0 0 0 269 448h67a16 16 0 0 0 16-16v-48a16 16 0 0 0-16-16h-33.48l-77.81-112 77.81-112H336a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16z") },
         ];
 
-        basicItems.map(({ dom, mark }) => this.basicTools?.appendChild(dom.cloneNode(true)));
+        basicItems.map(({ dom, mark }) => this.basicTools ?.appendChild(dom.cloneNode(true)));
         basicItems.concat(items).forEach(({ dom, mark }) => {
             this.tooltip.appendChild(dom);
             this._marksToDoms.set(mark, dom);
@@ -476,7 +474,7 @@ export class TooltipTextMenu {
                 const node = self.view.state.selection.$from.nodeAfter;
                 const link = node && node.marks.find(m => m.type === self.view.state.schema.marks.link);
                 const href = link!.attrs.href;
-                if (href?.indexOf(Utils.prepend("/doc/")) === 0) {
+                if (href ?.indexOf(Utils.prepend("/doc/")) === 0) {
                     const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
                     linkclicked && DocServer.GetRefField(linkclicked).then(async linkDoc => {
                         if (linkDoc instanceof Doc) {
@@ -502,7 +500,7 @@ export class TooltipTextMenu {
         const link = this.view.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + linkDocId), title: title, location: location, targetId: targetDocId });
         this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link).
             addMark(this.view.state.selection.from, this.view.state.selection.to, link));
-        return this.view.state.selection.$from.nodeAfter?.text || "";
+        return this.view.state.selection.$from.nodeAfter ?.text || "";
     }
 
     // SUMMARIZER TOOL
@@ -512,7 +510,7 @@ export class TooltipTextMenu {
             const tr = state.tr.addMark(state.selection.from, state.selection.to, mark);
             const content = tr.selection.content();
             const newNode = state.schema.nodes.summary.create({ visibility: false, text: content, textslice: content.toJSON() });
-            dispatch?.(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark));
+            dispatch ?.(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark));
         }
     }
 
@@ -739,7 +737,7 @@ export class TooltipTextMenu {
             // get marks in the selection
             const selected_marks = new Set<Mark>();
             const { from, to } = state.selection as TextSelection;
-            state.doc.nodesBetween(from, to, (node) => node.marks?.forEach(m => selected_marks.add(m)));
+            state.doc.nodesBetween(from, to, (node) => node.marks ?.forEach(m => selected_marks.add(m)));
 
             if (this._brushdom && selected_marks.size >= 0) {
                 TooltipTextMenuManager.Instance._brushMarks = selected_marks;
@@ -851,7 +849,7 @@ export class TooltipTextMenu {
     static setMark = (mark: Mark, state: EditorState<any>, dispatch: any) => {
         if (mark) {
             const node = (state.selection as NodeSelection).node;
-            if (node?.type === schema.nodes.ordered_list) {
+            if (node ?.type === schema.nodes.ordered_list) {
                 let attrs = node.attrs;
                 if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, setFontFamily: mark.attrs.family };
                 if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, setFontSize: mark.attrs.fontSize };
diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx
index e358d88cf..4625eb92f 100644
--- a/src/client/views/AntimodeMenu.tsx
+++ b/src/client/views/AntimodeMenu.tsx
@@ -127,7 +127,7 @@ export default abstract class AntimodeMenu extends React.Component {
     }
 
     protected getDragger = () => {
-        return <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} />
+        return <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} />;
     }
 
     protected getElement(buttons: JSX.Element[]) {
-- 
cgit v1.2.3-70-g09d2


From af7e82e1c5314571457724e351aba191b20a0d01 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Thu, 9 Jan 2020 16:48:06 -0500
Subject: minor clean

---
 src/client/util/RichTextMenu.tsx | 3 ---
 1 file changed, 3 deletions(-)

(limited to 'src/client/util/RichTextMenu.tsx')

diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx
index 375c10da3..a5de8d7a9 100644
--- a/src/client/util/RichTextMenu.tsx
+++ b/src/client/util/RichTextMenu.tsx
@@ -714,9 +714,6 @@ export default class RichTextMenu extends AntimodeMenu {
 }
 
 interface ButtonDropdownProps {
-    // Document: Doc;
-    // remove: (self: ImportMetadataEntry) => void;
-    // next: () => void;
     view?: EditorView;
     button: JSX.Element;
     dropdownContent: JSX.Element;
-- 
cgit v1.2.3-70-g09d2


From 579010cc792ce961618406886961d257be2c74db Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Thu, 9 Jan 2020 18:13:28 -0500
Subject: condensed the link button and made buttons dark when they are active
 in the selection

---
 package-lock.json                  | 91 +++++++++++++++-----------------------
 src/client/util/RichTextMenu.scss  | 10 +++++
 src/client/util/RichTextMenu.tsx   | 67 +++++++++++++++++++++++-----
 src/client/views/AntimodeMenu.scss |  4 ++
 4 files changed, 105 insertions(+), 67 deletions(-)

(limited to 'src/client/util/RichTextMenu.tsx')

diff --git a/package-lock.json b/package-lock.json
index 6aa0ffc02..cf61e6aab 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1978,7 +1978,7 @@
                 },
                 "util": {
                     "version": "0.10.3",
-                    "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
+                    "resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz",
                     "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
                     "dev": true,
                     "requires": {
@@ -2561,7 +2561,7 @@
         },
         "browserify-aes": {
             "version": "1.2.0",
-            "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
+            "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
             "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
             "requires": {
                 "buffer-xor": "^1.0.3",
@@ -2595,7 +2595,7 @@
         },
         "browserify-rsa": {
             "version": "4.0.1",
-            "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
+            "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
             "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
             "requires": {
                 "bn.js": "^4.1.0",
@@ -2779,7 +2779,7 @@
         },
         "camelcase-keys": {
             "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
+            "resolved": "http://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
             "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
             "requires": {
                 "camelcase": "^2.0.0",
@@ -3563,7 +3563,7 @@
         },
         "create-hash": {
             "version": "1.2.0",
-            "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
+            "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
             "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
             "requires": {
                 "cipher-base": "^1.0.1",
@@ -3575,7 +3575,7 @@
         },
         "create-hmac": {
             "version": "1.1.7",
-            "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
+            "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
             "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
             "requires": {
                 "cipher-base": "^1.0.3",
@@ -4118,7 +4118,7 @@
         },
         "diffie-hellman": {
             "version": "5.0.3",
-            "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
+            "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
             "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
             "requires": {
                 "bn.js": "^4.1.0",
@@ -5361,8 +5361,7 @@
                 },
                 "ansi-regex": {
                     "version": "2.1.1",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "aproba": {
                     "version": "1.2.0",
@@ -5380,13 +5379,11 @@
                 },
                 "balanced-match": {
                     "version": "1.0.0",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "brace-expansion": {
                     "version": "1.1.11",
                     "bundled": true,
-                    "optional": true,
                     "requires": {
                         "balanced-match": "^1.0.0",
                         "concat-map": "0.0.1"
@@ -5399,18 +5396,15 @@
                 },
                 "code-point-at": {
                     "version": "1.1.0",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "concat-map": {
                     "version": "0.0.1",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "console-control-strings": {
                     "version": "1.1.0",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "core-util-is": {
                     "version": "1.0.2",
@@ -5513,8 +5507,7 @@
                 },
                 "inherits": {
                     "version": "2.0.3",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "ini": {
                     "version": "1.3.5",
@@ -5524,7 +5517,6 @@
                 "is-fullwidth-code-point": {
                     "version": "1.0.0",
                     "bundled": true,
-                    "optional": true,
                     "requires": {
                         "number-is-nan": "^1.0.0"
                     }
@@ -5537,20 +5529,17 @@
                 "minimatch": {
                     "version": "3.0.4",
                     "bundled": true,
-                    "optional": true,
                     "requires": {
                         "brace-expansion": "^1.1.7"
                     }
                 },
                 "minimist": {
                     "version": "0.0.8",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "minipass": {
                     "version": "2.3.5",
                     "bundled": true,
-                    "optional": true,
                     "requires": {
                         "safe-buffer": "^5.1.2",
                         "yallist": "^3.0.0"
@@ -5567,7 +5556,6 @@
                 "mkdirp": {
                     "version": "0.5.1",
                     "bundled": true,
-                    "optional": true,
                     "requires": {
                         "minimist": "0.0.8"
                     }
@@ -5640,8 +5628,7 @@
                 },
                 "number-is-nan": {
                     "version": "1.0.1",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "object-assign": {
                     "version": "4.1.1",
@@ -5651,7 +5638,6 @@
                 "once": {
                     "version": "1.4.0",
                     "bundled": true,
-                    "optional": true,
                     "requires": {
                         "wrappy": "1"
                     }
@@ -5727,8 +5713,7 @@
                 },
                 "safe-buffer": {
                     "version": "5.1.2",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "safer-buffer": {
                     "version": "2.1.2",
@@ -5758,7 +5743,6 @@
                 "string-width": {
                     "version": "1.0.2",
                     "bundled": true,
-                    "optional": true,
                     "requires": {
                         "code-point-at": "^1.0.0",
                         "is-fullwidth-code-point": "^1.0.0",
@@ -5776,7 +5760,6 @@
                 "strip-ansi": {
                     "version": "3.0.1",
                     "bundled": true,
-                    "optional": true,
                     "requires": {
                         "ansi-regex": "^2.0.0"
                     }
@@ -5815,13 +5798,11 @@
                 },
                 "wrappy": {
                     "version": "1.0.2",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "yallist": {
                     "version": "3.0.3",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 }
             }
         },
@@ -7073,7 +7054,7 @@
                 "babel-runtime": "^6.22.0",
                 "bluebird": "^3.4.7",
                 "boolify-string": "^2.0.2",
-                "emit-logger": "github:chocolateboy/emit-logger#better-emitter-name",
+                "emit-logger": "github:chocolateboy/emit-logger#b9d25a2d939e42f29c940861e9648bd0fb810070",
                 "lodash": "^4.17.4",
                 "semver": "^5.0.3",
                 "source-map-support": "^0.5.9"
@@ -7081,7 +7062,7 @@
         },
         "is-accessor-descriptor": {
             "version": "0.1.6",
-            "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
+            "resolved": "http://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
             "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
             "requires": {
                 "kind-of": "^3.0.2"
@@ -7130,7 +7111,7 @@
         },
         "is-data-descriptor": {
             "version": "0.1.4",
-            "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
+            "resolved": "http://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
             "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
             "requires": {
                 "kind-of": "^3.0.2"
@@ -7915,7 +7896,7 @@
         },
         "load-json-file": {
             "version": "1.1.0",
-            "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
+            "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
             "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
             "requires": {
                 "graceful-fs": "^4.1.2",
@@ -8229,7 +8210,7 @@
         },
         "media-typer": {
             "version": "0.3.0",
-            "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+            "resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
             "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
         },
         "mem": {
@@ -8261,7 +8242,7 @@
         },
         "meow": {
             "version": "3.7.0",
-            "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
+            "resolved": "http://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
             "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
             "requires": {
                 "camelcase-keys": "^2.0.0",
@@ -8454,7 +8435,7 @@
         },
         "mkdirp": {
             "version": "0.5.1",
-            "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+            "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
             "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
             "requires": {
                 "minimist": "0.0.8"
@@ -8800,7 +8781,7 @@
         },
         "next-tick": {
             "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
+            "resolved": "http://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
             "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
         },
         "nice-try": {
@@ -8964,7 +8945,7 @@
                 },
                 "semver": {
                     "version": "5.3.0",
-                    "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
+                    "resolved": "http://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
                     "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8="
                 },
                 "tar": {
@@ -12573,7 +12554,7 @@
         },
         "os-homedir": {
             "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+            "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
             "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
         },
         "os-locale": {
@@ -12586,7 +12567,7 @@
         },
         "os-tmpdir": {
             "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+            "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
             "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
         },
         "osenv": {
@@ -12826,7 +12807,7 @@
         },
         "path-is-absolute": {
             "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+            "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
             "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
         },
         "path-is-inside": {
@@ -14491,7 +14472,7 @@
         },
         "safe-regex": {
             "version": "1.1.0",
-            "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
+            "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
             "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
             "requires": {
                 "ret": "~0.1.10"
@@ -14756,7 +14737,7 @@
         },
         "sha.js": {
             "version": "2.4.11",
-            "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
+            "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
             "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
             "requires": {
                 "inherits": "^2.0.1",
@@ -15499,7 +15480,7 @@
         },
         "string_decoder": {
             "version": "1.1.1",
-            "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+            "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
             "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
             "requires": {
                 "safe-buffer": "~5.1.0"
@@ -15545,7 +15526,7 @@
         },
         "strip-eof": {
             "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
+            "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
             "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8="
         },
         "strip-indent": {
@@ -16298,7 +16279,7 @@
         },
         "tty-browserify": {
             "version": "0.0.0",
-            "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
+            "resolved": "http://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
             "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=",
             "dev": true
         },
@@ -18087,4 +18068,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/util/RichTextMenu.scss b/src/client/util/RichTextMenu.scss
index 6383e0b7b..43cc23ecd 100644
--- a/src/client/util/RichTextMenu.scss
+++ b/src/client/util/RichTextMenu.scss
@@ -9,6 +9,16 @@
         padding-right: 5px;
     }
 
+    .dropdown-button-combined {
+        width: 50px;
+        display: flex;
+        justify-content: space-between;
+
+        svg:nth-child(2) {
+            margin-top: 2px;
+        }
+    }
+
     .dropdown {
         position: absolute;
         top: 35px;
diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx
index a5de8d7a9..ec79fbf85 100644
--- a/src/client/util/RichTextMenu.tsx
+++ b/src/client/util/RichTextMenu.tsx
@@ -8,7 +8,7 @@ import { EditorView } from "prosemirror-view";
 import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
-import { faBold, faItalic, faUnderline, faStrikethrough, faSubscript, faSuperscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller } from "@fortawesome/free-solid-svg-icons";
+import { faBold, faItalic, faUnderline, faStrikethrough, faSubscript, faSuperscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller, faSleigh } from "@fortawesome/free-solid-svg-icons";
 import { MenuItem, Dropdown } from "prosemirror-menu";
 import { updateBullets } from "./ProsemirrorExampleTransfer";
 import { FieldViewProps } from "../views/nodes/FieldView";
@@ -40,6 +40,13 @@ export default class RichTextMenu extends AntimodeMenu {
     private fontColors: (string | undefined)[];
     private highlightColors: (string | undefined)[];
 
+    @observable private boldActive: boolean = false;
+    @observable private italicsActive: boolean = false;
+    @observable private underlineActive: boolean = false;
+    @observable private strikethroughActive: boolean = false;
+    @observable private subscriptActive: boolean = false;
+    @observable private superscriptActive: boolean = false;
+
     @observable private activeFontSize: string = "";
     @observable private activeFontFamily: string = "";
     @observable private activeListType: string = "";
@@ -148,6 +155,9 @@ export default class RichTextMenu extends AntimodeMenu {
         // Don't do anything if the document/selection didn't change
         if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) return;
 
+        // update active marks
+        const activeMarks = this.getMarksInSelection(this.view.state);
+        this.setActiveMarkButtons(activeMarks);
 
         // update active font family and size
         const active = this.getActiveFontStylesOnSelection();
@@ -208,14 +218,37 @@ export default class RichTextMenu extends AntimodeMenu {
     getMarksInSelection(state: EditorState<any>) {
         const found = new Set<Mark>();
         const { from, to } = state.selection as TextSelection;
+        console.log(to - from, state.tr.storedMarks);
         state.doc.nodesBetween(from, to, (node) => node.marks.forEach(m => found.add(m)));
+        if (to - from === 0) { state.storedMarks && state.storedMarks.forEach(m => found.add(m)); }
         return found;
     }
 
     destroy() {
     }
 
-    createButton(faIcon: string, title: string, command?: any, onclick?: any) {
+    @action
+    setActiveMarkButtons(activeMarks: Set<Mark>) {
+        this.boldActive = false;
+        this.italicsActive = false;
+        this.underlineActive = false;
+        this.strikethroughActive = false;
+        this.subscriptActive = false;
+        this.superscriptActive = false;
+
+        activeMarks.forEach(mark => {
+            switch (mark.type.name) {
+                case "strong": this.boldActive = true; break;
+                case "em": this.italicsActive = true; break;
+                case "underline": this.underlineActive = true; break;
+                case "strikethrough": this.strikethroughActive = true; break;
+                case "subscript": this.subscriptActive = true; break;
+                case "superscript": this.superscriptActive = true; break;
+            }
+        });
+    }
+
+    createButton(faIcon: string, title: string, isActive: boolean = false, command?: any, onclick?: any) {
         const self = this;
         function onClick(e: React.PointerEvent) {
             e.preventDefault();
@@ -226,7 +259,7 @@ export default class RichTextMenu extends AntimodeMenu {
         }
 
         return (
-            <button className="antimodeMenu-button" title={title} onPointerDown={onClick}>
+            <button className={"antimodeMenu-button" + (isActive ? " active" : "")} title={title} onPointerDown={onClick}>
                 <FontAwesomeIcon icon={faIcon as IconProp} size="lg" />
             </button>
         );
@@ -528,7 +561,7 @@ export default class RichTextMenu extends AntimodeMenu {
 
         const link = this.currentLink ? this.currentLink : "";
 
-        const button = <button className="antimodeMenu-button" title=""><FontAwesomeIcon icon="link" size="lg" /></button>;
+        const button = <FontAwesomeIcon icon="link" size="lg" />
 
         const dropdownContent =
             <div className="dropdown link-menu">
@@ -678,12 +711,12 @@ export default class RichTextMenu extends AntimodeMenu {
     render() {
 
         const row1 = <div className="antimodeMenu-row">{[
-            this.createButton("bold", "Bold", toggleMark(schema.marks.strong)),
-            this.createButton("italic", "Italic", toggleMark(schema.marks.em)),
-            this.createButton("underline", "Underline", toggleMark(schema.marks.underline)),
-            this.createButton("strikethrough", "Strikethrough", toggleMark(schema.marks.strikethrough)),
-            this.createButton("superscript", "Superscript", toggleMark(schema.marks.superscript)),
-            this.createButton("subscript", "Subscript", toggleMark(schema.marks.subscript)),
+            this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)),
+            this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)),
+            this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)),
+            this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)),
+            this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)),
+            this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)),
             this.createColorButton(),
             this.createHighlighterButton(),
             this.createLinkButton(),
@@ -761,8 +794,18 @@ class ButtonDropdown extends React.Component<ButtonDropdownProps> {
     render() {
         return (
             <div className="button-dropdown-wrapper" ref={node => this.ref = node}>
-                {this.props.openDropdownOnButton ? <div onPointerDown={this.onDropdownClick}>{this.props.button}</div> : this.props.button}
-                <button className="dropdown-button antimodeMenu-button" onPointerDown={this.onDropdownClick}><FontAwesomeIcon icon="caret-down" size="sm" /></button>
+                {this.props.openDropdownOnButton ?
+                    <button className="antimodeMenu-button dropdown-button-combined" onPointerDown={this.onDropdownClick}>
+                        {this.props.button}
+                        <FontAwesomeIcon icon="caret-down" size="sm" />
+                    </button> :
+                    <>
+                        {this.props.button}
+                        <button className="dropdown-button antimodeMenu-button" onPointerDown={this.onDropdownClick}>
+                            <FontAwesomeIcon icon="caret-down" size="sm" />
+                        </button>
+                    </>}
+
                 {this.showDropdown ? this.props.dropdownContent : <></>}
             </div>
         );
diff --git a/src/client/views/AntimodeMenu.scss b/src/client/views/AntimodeMenu.scss
index f78b1dbfb..d4a76ee17 100644
--- a/src/client/views/AntimodeMenu.scss
+++ b/src/client/views/AntimodeMenu.scss
@@ -21,6 +21,10 @@
         background-color: transparent;
         width: 35px;
         height: 35px;
+
+        &.active {
+            background-color: #121212;
+        }
     }
 
     .antimodeMenu-button:hover {
-- 
cgit v1.2.3-70-g09d2


From eec70e08bf5fa5f3a0a4e3b07bdc05777f71d2ef Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Thu, 9 Jan 2020 18:50:08 -0500
Subject: buttons show correct active status when togglign marks at the end of
 text

---
 src/client/util/RichTextMenu.tsx | 51 ++++++++++++++++++++++++++++++++++++----
 1 file changed, 46 insertions(+), 5 deletions(-)

(limited to 'src/client/util/RichTextMenu.tsx')

diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx
index ec79fbf85..639772faa 100644
--- a/src/client/util/RichTextMenu.tsx
+++ b/src/client/util/RichTextMenu.tsx
@@ -156,7 +156,7 @@ export default class RichTextMenu extends AntimodeMenu {
         if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) return;
 
         // update active marks
-        const activeMarks = this.getMarksInSelection(this.view.state);
+        const activeMarks = this.getActiveMarksOnSelection();
         this.setActiveMarkButtons(activeMarks);
 
         // update active font family and size
@@ -218,17 +218,57 @@ export default class RichTextMenu extends AntimodeMenu {
     getMarksInSelection(state: EditorState<any>) {
         const found = new Set<Mark>();
         const { from, to } = state.selection as TextSelection;
-        console.log(to - from, state.tr.storedMarks);
         state.doc.nodesBetween(from, to, (node) => node.marks.forEach(m => found.add(m)));
-        if (to - from === 0) { state.storedMarks && state.storedMarks.forEach(m => found.add(m)); }
         return found;
     }
 
+    //finds all active marks on selection in given group
+    getActiveMarksOnSelection() {
+        if (!this.view) return;
+
+        const markGroup = [schema.marks.strong, schema.marks.em, schema.marks.underline, schema.marks.strikethrough, schema.marks.superscript, schema.marks.subscript];
+        if (this.view.state.storedMarks) return this.view.state.storedMarks.map(mark => mark.type);
+        //current selection
+        const { empty, ranges, $to } = this.view.state.selection as TextSelection;
+        const state = this.view.state;
+        let activeMarks: MarkType[] = [];
+        if (!empty) {
+            activeMarks = markGroup.filter(mark => {
+                const has = false;
+                for (let i = 0; !has && i < ranges.length; i++) {
+                    return state.doc.rangeHasMark(ranges[i].$from.pos, ranges[i].$to.pos, mark);
+                }
+                return false;
+            });
+        }
+        else {
+            const pos = this.view.state.selection.$from;
+            const ref_node: ProsNode | null = this.reference_node(pos);
+            if (ref_node !== null && ref_node !== this.view.state.doc) {
+                if (ref_node.isText) {
+                }
+                else {
+                    return [];
+                }
+                activeMarks = markGroup.filter(mark_type => {
+                    if (mark_type === state.schema.marks.pFontSize) {
+                        return ref_node.marks.some(m => m.type.name === state.schema.marks.pFontSize.name);
+                    }
+                    const mark = state.schema.mark(mark_type);
+                    return ref_node.marks.includes(mark);
+                });
+            }
+        }
+        return activeMarks;
+    }
+
     destroy() {
     }
 
     @action
-    setActiveMarkButtons(activeMarks: Set<Mark>) {
+    setActiveMarkButtons(activeMarks: MarkType[] | undefined) {
+        if (!activeMarks) return;
+
         this.boldActive = false;
         this.italicsActive = false;
         this.underlineActive = false;
@@ -237,7 +277,7 @@ export default class RichTextMenu extends AntimodeMenu {
         this.superscriptActive = false;
 
         activeMarks.forEach(mark => {
-            switch (mark.type.name) {
+            switch (mark.name) {
                 case "strong": this.boldActive = true; break;
                 case "em": this.italicsActive = true; break;
                 case "underline": this.underlineActive = true; break;
@@ -256,6 +296,7 @@ export default class RichTextMenu extends AntimodeMenu {
             self.view && self.view.focus();
             self.view && command && command(self.view!.state, self.view!.dispatch, self.view);
             self.view && onclick && onclick(self.view!.state, self.view!.dispatch, self.view);
+            self.setActiveMarkButtons(self.getActiveMarksOnSelection());
         }
 
         return (
-- 
cgit v1.2.3-70-g09d2


From 6162c951e07874fbb12717d4bcd2cd649e41d0d2 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Mon, 13 Jan 2020 00:00:56 -0500
Subject: deleted old Session folder and fixed linter errors

---
 src/client/util/ProseMirrorEditorView.tsx          |   8 +-
 src/client/util/RichTextMenu.tsx                   |  13 +-
 src/client/views/EditableView.tsx                  |   2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   4 +-
 src/client/views/linking/LinkMenuGroup.tsx         |   2 +-
 src/client/views/nodes/DocumentView.tsx            |   2 +-
 src/client/views/nodes/FormattedTextBox.tsx        |  23 +-
 src/pen-gestures/ndollar.ts                        |  66 +-
 src/server/MemoryDatabase.ts                       |   7 +-
 src/server/RouteManager.ts                         |   3 +-
 src/server/Session/session.ts                      | 686 ---------------------
 11 files changed, 67 insertions(+), 749 deletions(-)
 delete mode 100644 src/server/Session/session.ts

(limited to 'src/client/util/RichTextMenu.tsx')

diff --git a/src/client/util/ProseMirrorEditorView.tsx b/src/client/util/ProseMirrorEditorView.tsx
index 3e5fd0604..b42adfbb4 100644
--- a/src/client/util/ProseMirrorEditorView.tsx
+++ b/src/client/util/ProseMirrorEditorView.tsx
@@ -18,23 +18,23 @@ export class ProseMirrorEditorView extends React.Component<ProseMirrorEditorView
     private _editorView?: EditorView;
 
     _createEditorView = (element: HTMLDivElement | null) => {
-        if (element != null) {
+        if (element !== null) {
             this._editorView = new EditorView(element, {
                 state: this.props.editorState,
                 dispatchTransaction: this.dispatchTransaction,
             });
         }
-    };
+    }
 
     dispatchTransaction = (tx: any) => {
         // In case EditorView makes any modification to a state we funnel those
         // modifications up to the parent and apply to the EditorView itself.
         const editorState = this.props.editorState.apply(tx);
-        if (this._editorView != null) {
+        if (this._editorView) {
             this._editorView.updateState(editorState);
         }
         this.props.onEditorState(editorState);
-    };
+    }
 
     focus() {
         if (this._editorView) {
diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx
index 639772faa..419d7caf9 100644
--- a/src/client/util/RichTextMenu.tsx
+++ b/src/client/util/RichTextMenu.tsx
@@ -175,7 +175,7 @@ export default class RichTextMenu extends AntimodeMenu {
     setMark = (mark: Mark, state: EditorState<any>, dispatch: any) => {
         if (mark) {
             const node = (state.selection as NodeSelection).node;
-            if (node ?.type === schema.nodes.ordered_list) {
+            if (node?.type === schema.nodes.ordered_list) {
                 let attrs = node.attrs;
                 if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, setFontFamily: mark.attrs.family };
                 if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, setFontSize: mark.attrs.fontSize };
@@ -294,8 +294,8 @@ export default class RichTextMenu extends AntimodeMenu {
             e.preventDefault();
             e.stopPropagation();
             self.view && self.view.focus();
-            self.view && command && command(self.view!.state, self.view!.dispatch, self.view);
-            self.view && onclick && onclick(self.view!.state, self.view!.dispatch, self.view);
+            self.view && command && command(self.view.state, self.view.dispatch, self.view);
+            self.view && onclick && onclick(self.view.state, self.view.dispatch, self.view);
             self.setActiveMarkButtons(self.getActiveMarksOnSelection());
         }
 
@@ -602,7 +602,7 @@ export default class RichTextMenu extends AntimodeMenu {
 
         const link = this.currentLink ? this.currentLink : "";
 
-        const button = <FontAwesomeIcon icon="link" size="lg" />
+        const button = <FontAwesomeIcon icon="link" size="lg" />;
 
         const dropdownContent =
             <div className="dropdown link-menu">
@@ -684,8 +684,9 @@ export default class RichTextMenu extends AntimodeMenu {
                 }
             } else {
                 if (node) {
-                    const extension = this.linkExtend(this.view!.state.selection.$anchor, href);
-                    this.view!.dispatch(this.view!.state.tr.removeMark(extension.from, extension.to, this.view!.state.schema.marks.link));
+                    const { tr, schema, selection } = this.view.state;
+                    const extension = this.linkExtend(selection.$anchor, href);
+                    this.view.dispatch(tr.removeMark(extension.from, extension.to, schema.marks.link));
                 }
             }
         }
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index 54def38b5..faf02b946 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -36,7 +36,7 @@ export interface EditableProps {
         resetValue: () => void;
         value: string,
         onChange: (e: React.ChangeEvent, { newValue }: { newValue: string }) => void,
-        autosuggestProps: Autosuggest.AutosuggestProps<string>
+        autosuggestProps: Autosuggest.AutosuggestProps<string, any>
 
     };
     oneLine?: boolean;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 936c4413f..e3780261d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -988,8 +988,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         </MarqueeView>;
     }
     @computed get contentScaling() {
-        let hscale = this.nativeHeight ? this.props.PanelHeight() / this.nativeHeight : 1;
-        let wscale = this.nativeWidth ? this.props.PanelWidth() / this.nativeWidth : 1;
+        const hscale = this.nativeHeight ? this.props.PanelHeight() / this.nativeHeight : 1;
+        const wscale = this.nativeWidth ? this.props.PanelWidth() / this.nativeWidth : 1;
         return wscale < hscale ? wscale : hscale;
     }
     render() {
diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx
index abd17ec4d..ace9a9e4c 100644
--- a/src/client/views/linking/LinkMenuGroup.tsx
+++ b/src/client/views/linking/LinkMenuGroup.tsx
@@ -47,7 +47,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
             document.removeEventListener("pointerup", this.onLinkButtonUp);
 
             const targets = this.props.group.map(l => LinkManager.Instance.getOppositeAnchor(l, this.props.sourceDoc)).filter(d => d) as Doc[];
-            DragManager.StartLinkTargetsDrag(this._drag.current!, e.x, e.y, this.props.sourceDoc, targets);
+            DragManager.StartLinkTargetsDrag(this._drag.current, e.x, e.y, this.props.sourceDoc, targets);
         }
         e.stopPropagation();
     }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 10d2e2b3e..ba35366ff 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -196,7 +196,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY);
             } else if (this.props.Document.isButton === "Selector") {  // this should be moved to an OnClick script
                 FormattedTextBoxComment.Hide();
-                this.Document.links?.[0] instanceof Doc && (Doc.UserDoc().SelectedDocs = new List([Doc.LinkOtherAnchor(this.Document.links[0]!, this.props.Document)]));
+                this.Document.links?.[0] instanceof Doc && (Doc.UserDoc().SelectedDocs = new List([Doc.LinkOtherAnchor(this.Document.links[0], this.props.Document)]));
             } else if (this.Document.isButton) {
                 SelectionManager.SelectDoc(this, e.ctrlKey); // don't think this should happen if a button action is actually triggered.
                 this.buttonClick(e.altKey, e.ctrlKey);
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 8e28cf928..8b5c24878 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -906,15 +906,16 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         this.tryUpdateHeight();
 
         // see if we need to preserve the insertion point
-        const prosediv = this.ProseRef ?.children ?.[0] as any;
-        const keeplocation = prosediv ?.keeplocation;
+        const prosediv = this.ProseRef?.children?.[0] as any;
+        const keeplocation = prosediv?.keeplocation;
         prosediv && (prosediv.keeplocation = undefined);
-        const pos = this._editorView ?.state.selection.$from.pos || 1;
-        keeplocation && setTimeout(() => this._editorView ?.dispatch(this._editorView ?.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos))));
+        const pos = this._editorView?.state.selection.$from.pos || 1;
+        keeplocation && setTimeout(() => this._editorView?.dispatch(this._editorView?.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos))));
 
         // jump rich text menu to this textbox
-        if (this._ref.current) {
-            const x = Math.min(Math.max(this._ref.current!.getBoundingClientRect().left, 0), window.innerWidth - RichTextMenu.Instance.width);
+        const { current } = this._ref;
+        if (current) {
+            const x = Math.min(Math.max(current.getBoundingClientRect().left, 0), window.innerWidth - RichTextMenu.Instance.width);
             const y = this._ref.current!.getBoundingClientRect().top - RichTextMenu.Instance.height - 50;
             RichTextMenu.Instance.jumpTo(x, y);
         }
@@ -933,7 +934,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         if ((this._editorView!.root as any).getSelection().isCollapsed) { // this is a hack to allow the cursor to be placed at the end of a document when the document ends in an inline dash comment.  Apparently Chrome on Windows has a bug/feature which breaks this when clicking after the end of the text.
             const pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
             const node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos); // get what prosemirror thinks the clicked node is (if it's null, then we didn't click on any text)
-            if (pcords && node ?.type === this._editorView!.state.schema.nodes.dashComment) {
+            if (pcords && node?.type === this._editorView!.state.schema.nodes.dashComment) {
                 this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, pcords.pos + 2)));
                 e.preventDefault();
             }
@@ -996,7 +997,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
                 for (let off = 1; off < 100; off++) {
                     const pos = this._editorView!.posAtCoords({ left: x + off, top: y });
                     const node = pos && this._editorView!.state.doc.nodeAt(pos.pos);
-                    if (node ?.type === schema.nodes.list_item) {
+                    if (node?.type === schema.nodes.list_item) {
                         list_node = node;
                         break;
                     }
@@ -1087,7 +1088,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         }
         if (e.key === "Escape") {
             this._editorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from)));
-            (document.activeElement as any).blur ?.();
+            (document.activeElement as any).blur?.();
             SelectionManager.DeselectAll();
         }
         e.stopPropagation();
@@ -1109,7 +1110,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
 
     @action
     tryUpdateHeight(limitHeight?: number) {
-        let scrollHeight = this._ref.current ?.scrollHeight;
+        let scrollHeight = this._ref.current?.scrollHeight;
         if (!this.layoutDoc.animateToPos && this.layoutDoc.autoHeight && scrollHeight &&
             getComputedStyle(this._ref.current!.parentElement!).top === "0px") {  // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
             if (limitHeight && scrollHeight > limitHeight) {
@@ -1171,7 +1172,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
                 {this.props.Document.hideSidebar ? (null) : this.sidebarWidthPercent === "0%" ?
                     <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} onClick={e => this.toggleSidebar()} /> :
                     <div className={"formattedTextBox-sidebar" + (InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : "")}
-                        style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${StrCast(this.extensionDoc ?.backgroundColor, "transparent")}` }}>
+                        style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${StrCast(this.extensionDoc?.backgroundColor, "transparent")}` }}>
                         <CollectionFreeFormView {...this.props}
                             PanelHeight={this.props.PanelHeight}
                             PanelWidth={() => this.sidebarWidth}
diff --git a/src/pen-gestures/ndollar.ts b/src/pen-gestures/ndollar.ts
index 872c524d6..ef5ca38c6 100644
--- a/src/pen-gestures/ndollar.ts
+++ b/src/pen-gestures/ndollar.ts
@@ -257,16 +257,16 @@ export class NDollarRecognizer {
         {
             if (!requireSameNoOfStrokes || strokes.length === this.Multistrokes[i].NumStrokes) // optional -- only attempt match when same # of component strokes
             {
-                for (var j = 0; j < this.Multistrokes[i].Unistrokes.length; j++) // for each unistroke within this multistroke
+                for (const unistroke of this.Multistrokes[i].Unistrokes) // for each unistroke within this multistroke
                 {
-                    if (AngleBetweenUnitVectors(candidate.StartUnitVector, this.Multistrokes[i].Unistrokes[j].StartUnitVector) <= AngleSimilarityThreshold) // strokes start in the same direction
+                    if (AngleBetweenUnitVectors(candidate.StartUnitVector, unistroke.StartUnitVector) <= AngleSimilarityThreshold) // strokes start in the same direction
                     {
                         var d;
                         if (useProtractor) {
-                            d = OptimalCosineDistance(this.Multistrokes[i].Unistrokes[j].Vector, candidate.Vector); // Protractor
+                            d = OptimalCosineDistance(unistroke.Vector, candidate.Vector); // Protractor
                         }
                         else {
-                            d = DistanceAtBestAngle(candidate.Points, this.Multistrokes[i].Unistrokes[j], -AngleRange, +AngleRange, AnglePrecision); // Golden Section Search (original $N)
+                            d = DistanceAtBestAngle(candidate.Points, unistroke, -AngleRange, +AngleRange, AnglePrecision); // Golden Section Search (original $N)
                         }
                         if (d < b) {
                             b = d; // best (least) distance
@@ -283,8 +283,8 @@ export class NDollarRecognizer {
     AddGesture = (name: string, useBoundedRotationInvariance: boolean, strokes: any[]) => {
         this.Multistrokes[this.Multistrokes.length] = new Multistroke(name, useBoundedRotationInvariance, strokes);
         var num = 0;
-        for (var i = 0; i < this.Multistrokes.length; i++) {
-            if (this.Multistrokes[i].Name === name) {
+        for (const multistroke of this.Multistrokes) {
+            if (multistroke.Name === name) {
                 num++;
             }
         }
@@ -322,20 +322,20 @@ function HeapPermute(n: number, order: any[], /*out*/ orders: any[]) {
 
 function MakeUnistrokes(strokes: any, orders: any) {
     const unistrokes = new Array(); // array of point arrays
-    for (var r = 0; r < orders.length; r++) {
-        for (var b = 0; b < Math.pow(2, orders[r].length); b++) // use b's bits for directions
+    for (const order of orders) {
+        for (var b = 0; b < Math.pow(2, order.length); b++) // use b's bits for directions
         {
             const unistroke = new Array(); // array of points
-            for (var i = 0; i < orders[r].length; i++) {
+            for (var i = 0; i < order.length; i++) {
                 var pts;
                 if (((b >> i) & 1) === 1) {// is b's bit at index i on?
-                    pts = strokes[orders[r][i]].slice().reverse(); // copy and reverse
+                    pts = strokes[order[i]].slice().reverse(); // copy and reverse
                 }
                 else {
-                    pts = strokes[orders[r][i]].slice(); // copy
+                    pts = strokes[order[i]].slice(); // copy
                 }
-                for (var p = 0; p < pts.length; p++) {
-                    unistroke[unistroke.length] = pts[p]; // append points
+                for (const point of pts) {
+                    unistroke[unistroke.length] = point; // append points
                 }
             }
             unistrokes[unistrokes.length] = unistroke; // add one unistroke to set
@@ -346,9 +346,9 @@ function MakeUnistrokes(strokes: any, orders: any) {
 
 function CombineStrokes(strokes: any) {
     const points = new Array();
-    for (var s = 0; s < strokes.length; s++) {
-        for (var p = 0; p < strokes[s].length; p++) {
-            points[points.length] = new Point(strokes[s][p].X, strokes[s][p].Y);
+    for (const stroke of strokes) {
+        for (const { X, Y } of stroke) {
+            points[points.length] = new Point(X, Y);
         }
     }
     return points;
@@ -384,9 +384,9 @@ function RotateBy(points: any, radians: any) // rotates points around centroid
     const cos = Math.cos(radians);
     const sin = Math.sin(radians);
     const newpoints = new Array();
-    for (var i = 0; i < points.length; i++) {
-        const qx = (points[i].X - c.X) * cos - (points[i].Y - c.Y) * sin + c.X;
-        const qy = (points[i].X - c.X) * sin + (points[i].Y - c.Y) * cos + c.Y;
+    for (const point of points) {
+        const qx = (point.X - c.X) * cos - (point.Y - c.Y) * sin + c.X;
+        const qy = (point.X - c.X) * sin + (point.Y - c.Y) * cos + c.Y;
         newpoints[newpoints.length] = new Point(qx, qy);
     }
     return newpoints;
@@ -396,9 +396,9 @@ function ScaleDimTo(points: any, size: any, ratio1D: any) // scales bbox uniform
     const B = BoundingBox(points);
     const uniformly = Math.min(B.Width / B.Height, B.Height / B.Width) <= ratio1D; // 1D or 2D gesture test
     const newpoints = new Array();
-    for (var i = 0; i < points.length; i++) {
-        const qx = uniformly ? points[i].X * (size / Math.max(B.Width, B.Height)) : points[i].X * (size / B.Width);
-        const qy = uniformly ? points[i].Y * (size / Math.max(B.Width, B.Height)) : points[i].Y * (size / B.Height);
+    for (const { X, Y } of points) {
+        const qx = uniformly ? X * (size / Math.max(B.Width, B.Height)) : X * (size / B.Width);
+        const qy = uniformly ? Y * (size / Math.max(B.Width, B.Height)) : Y * (size / B.Height);
         newpoints[newpoints.length] = new Point(qx, qy);
     }
     return newpoints;
@@ -407,9 +407,9 @@ function TranslateTo(points: any, pt: any) // translates points' centroid
 {
     const c = Centroid(points);
     const newpoints = new Array();
-    for (var i = 0; i < points.length; i++) {
-        const qx = points[i].X + pt.X - c.X;
-        const qy = points[i].Y + pt.Y - c.Y;
+    for (const { X, Y } of points) {
+        const qx = X + pt.X - c.X;
+        const qy = Y + pt.Y - c.Y;
         newpoints[newpoints.length] = new Point(qx, qy);
     }
     return newpoints;
@@ -478,9 +478,9 @@ function DistanceAtAngle(points: any, T: any, radians: any) {
 }
 function Centroid(points: any) {
     var x = 0.0, y = 0.0;
-    for (var i = 0; i < points.length; i++) {
-        x += points[i].X;
-        y += points[i].Y;
+    for (const point of points) {
+        x += point.X;
+        y += point.Y;
     }
     x /= points.length;
     y /= points.length;
@@ -488,11 +488,11 @@ function Centroid(points: any) {
 }
 function BoundingBox(points: any) {
     var minX = +Infinity, maxX = -Infinity, minY = +Infinity, maxY = -Infinity;
-    for (var i = 0; i < points.length; i++) {
-        minX = Math.min(minX, points[i].X);
-        minY = Math.min(minY, points[i].Y);
-        maxX = Math.max(maxX, points[i].X);
-        maxY = Math.max(maxY, points[i].Y);
+    for (const { X, Y } of points) {
+        minX = Math.min(minX, X);
+        minY = Math.min(minY, Y);
+        maxX = Math.max(maxX, X);
+        maxY = Math.max(maxY, Y);
     }
     return new Rectangle(minX, minY, maxX - minX, maxY - minY);
 }
diff --git a/src/server/MemoryDatabase.ts b/src/server/MemoryDatabase.ts
index e7396babf..543f96e7f 100644
--- a/src/server/MemoryDatabase.ts
+++ b/src/server/MemoryDatabase.ts
@@ -7,7 +7,7 @@ export class MemoryDatabase implements IDatabase {
     private db: { [collectionName: string]: { [id: string]: any } } = {};
 
     private getCollection(collectionName: string) {
-        let collection = this.db[collectionName];
+        const collection = this.db[collectionName];
         if (collection) {
             return collection;
         } else {
@@ -17,9 +17,10 @@ export class MemoryDatabase implements IDatabase {
 
     public update(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, _upsert?: boolean, collectionName = DocumentsCollection): Promise<void> {
         const collection = this.getCollection(collectionName);
-        if ("$set" in value) {
+        const set = "$set";
+        if (set in value) {
             let currentVal = collection[id] ?? (collection[id] = {});
-            const val = value["$set"];
+            const val = value[set];
             for (const key in val) {
                 const keys = key.split(".");
                 for (let i = 0; i < keys.length - 1; i++) {
diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts
index a7ee405a7..5afd607fd 100644
--- a/src/server/RouteManager.ts
+++ b/src/server/RouteManager.ts
@@ -86,7 +86,8 @@ export default class RouteManager {
         const { method, subscription, secureHandler: onValidation, publicHandler: onUnauthenticated, errorHandler: onError } = initializer;
         const isRelease = this._isRelease;
         const supervised = async (req: express.Request, res: express.Response) => {
-            let { user, originalUrl: target } = req;
+            let { user } = req;
+            const { originalUrl: target } = req;
             if (process.env.DB === "MEM" && !user) {
                 user = { id: "guest", email: "", userDocumentId: "guestDocId" };
             }
diff --git a/src/server/Session/session.ts b/src/server/Session/session.ts
deleted file mode 100644
index ec3d46ac1..000000000
--- a/src/server/Session/session.ts
+++ /dev/null
@@ -1,686 +0,0 @@
-import { red, cyan, green, yellow, magenta, blue, white, Color, grey, gray, black } from "colors";
-import { on, fork, setupMaster, Worker, isMaster, isWorker } from "cluster";
-import { get } from "request-promise";
-import { Utils } from "../../Utils";
-import Repl, { ReplAction } from "../repl";
-import { readFileSync } from "fs";
-import { validate, ValidationError } from "jsonschema";
-import { configurationSchema } from "./session_config_schema";
-import { exec, ExecOptions } from "child_process";
-
-/**
- * This namespace relies on NodeJS's cluster module, which allows a parent (master) process to share
- * code with its children (workers). A simple `isMaster` flag indicates who is trying to access
- * the code, and thus determines the functionality that actually gets invoked (checked by the caller, not internally).
- * 
- * Think of the master thread as a factory, and the workers as the helpers that actually run the server.
- * 
- * So, when we run `npm start`, given the appropriate check, initializeMaster() is called in the parent process
- * This will spawn off its own child process (by default, mirrors the execution path of its parent),
- * in which initializeWorker() is invoked.
- */
-export namespace Session {
-
-    type ColorLabel = "yellow" | "red" | "cyan" | "green" | "blue" | "magenta" | "grey" | "gray" | "white" | "black";
-    const colorMapping: Map<ColorLabel, Color> = new Map([
-        ["yellow", yellow],
-        ["red", red],
-        ["cyan", cyan],
-        ["green", green],
-        ["blue", blue],
-        ["magenta", magenta],
-        ["grey", grey],
-        ["gray", gray],
-        ["white", white],
-        ["black", black]
-    ]);
-
-    export abstract class AppliedSessionAgent {
-
-        // the following two methods allow the developer to create a custom
-        // session and use the built in customization options for each thread
-        protected abstract async launchMonitor(): Promise<Session.Monitor>;
-        protected abstract async launchServerWorker(): Promise<Session.ServerWorker>;
-
-        private launched = false;
-
-        public killSession = (reason: string, graceful = true, errorCode = 0) => {
-            const target = isMaster ? this.sessionMonitor : this.serverWorker;
-            target.killSession(reason, graceful, errorCode);
-        }
-
-        private sessionMonitorRef: Session.Monitor | undefined;
-        public get sessionMonitor(): Session.Monitor {
-            if (!isMaster) {
-                this.serverWorker.sendMonitorAction("kill", {
-                    graceful: false,
-                    reason: "Cannot access the session monitor directly from the server worker thread.",
-                    errorCode: 1
-                });
-                throw new Error();
-            }
-            return this.sessionMonitorRef!;
-        }
-
-        private serverWorkerRef: Session.ServerWorker | undefined;
-        public get serverWorker(): Session.ServerWorker {
-            if (isMaster) {
-                throw new Error("Cannot access the server worker directly from the session monitor thread");
-            }
-            return this.serverWorkerRef!;
-        }
-
-        public async launch(): Promise<void> {
-            if (!this.launched) {
-                this.launched = true;
-                if (isMaster) {
-                    this.sessionMonitorRef = await this.launchMonitor();
-                } else {
-                    this.serverWorkerRef = await this.launchServerWorker();
-                }
-            } else {
-                throw new Error("Cannot launch a session thread more than once per process.");
-            }
-        }
-
-    }
-
-    interface Identifier {
-        text: string;
-        color: ColorLabel;
-    }
-
-    interface Identifiers {
-        master: Identifier;
-        worker: Identifier;
-        exec: Identifier;
-    }
-
-    interface Configuration {
-        showServerOutput: boolean;
-        identifiers: Identifiers;
-        ports: { [description: string]: number };
-        polling: {
-            route: string;
-            intervalSeconds: number;
-            failureTolerance: number;
-        };
-    }
-
-    const defaultConfig: Configuration = {
-        showServerOutput: false,
-        identifiers: {
-            master: {
-                text: "__monitor__",
-                color: "yellow"
-            },
-            worker: {
-                text: "__server__",
-                color: "magenta"
-            },
-            exec: {
-                text: "__exec__",
-                color: "green"
-            }
-        },
-        ports: { server: 3000 },
-        polling: {
-            route: "/",
-            intervalSeconds: 30,
-            failureTolerance: 0
-        }
-    };
-
-    export type ExitHandler = (reason: Error | boolean) => void | Promise<void>;
-
-    export namespace Monitor {
-
-        export interface NotifierHooks {
-            key?: (key: string) => (boolean | Promise<boolean>);
-            crash?: (error: Error) => (boolean | Promise<boolean>);
-        }
-
-        export interface Action {
-            message: string;
-            args: any;
-        }
-
-        export type ServerMessageHandler = (action: Action) => void | Promise<void>;
-
-    }
-
-    /**
-     * Validates and reads the configuration file, accordingly builds a child process factory
-     * and spawns off an initial process that will respawn as predecessors die.
-     */
-    export class Monitor {
-
-        private static count = 0;
-        private exitHandlers: ExitHandler[] = [];
-        private readonly notifiers: Monitor.NotifierHooks | undefined;
-        private readonly config: Configuration;
-        private onMessage: { [message: string]: Monitor.ServerMessageHandler[] | undefined } = {};
-        private activeWorker: Worker | undefined;
-        private key: string | undefined;
-        private repl: Repl;
-
-        public static Create(notifiers?: Monitor.NotifierHooks) {
-            if (isWorker) {
-                process.send?.({
-                    action: {
-                        message: "kill",
-                        args: {
-                            reason: "cannot create a monitor on the worker process.",
-                            graceful: false,
-                            errorCode: 1
-                        }
-                    }
-                });
-                process.exit(1);
-            } else if (++Monitor.count > 1) {
-                console.error(red("cannot create more than one monitor."));
-                process.exit(1);
-            } else {
-                return new Monitor(notifiers);
-            }
-        }
-
-        /**
-         * Kill this session and its active child
-         * server process, either gracefully (may wait
-         * indefinitely, but at least allows active networking
-         * requests to complete) or immediately.
-         */
-        public killSession = async (reason: string, graceful = true, errorCode = 0) => {
-            this.mainLog(cyan(`exiting session ${graceful ? "clean" : "immediate"}ly`));
-            this.mainLog(`session exit reason: ${(red(reason))}`);
-            await this.executeExitHandlers(true);
-            this.killActiveWorker(graceful, true);
-            process.exit(errorCode);
-        }
-
-        /**
-         * Execute the list of functions registered to be called
-         * whenever the process exits.
-         */
-        public addExitHandler = (handler: ExitHandler) => this.exitHandlers.push(handler);
-
-        /**
-         * Extend the default repl by adding in custom commands
-         * that can invoke application logic external to this module
-         */
-        public addReplCommand = (basename: string, argPatterns: (RegExp | string)[], action: ReplAction) => {
-            this.repl.registerCommand(basename, argPatterns, action);
-        }
-
-        public exec = (command: string, options?: ExecOptions) => {
-            return new Promise<void>(resolve => {
-                exec(command, { ...options, encoding: "utf8" }, (error, stdout, stderr) => {
-                    if (error) {
-                        this.execLog(red(`unable to execute ${white(command)}`));
-                        error.message.split("\n").forEach(line => line.length && this.execLog(red(`(error) ${line}`)));
-                    } else {
-                        let outLines: string[], errorLines: string[];
-                        if ((outLines = stdout.split("\n").filter(line => line.length)).length) {
-                            outLines.forEach(line => line.length && this.execLog(cyan(`(stdout) ${line}`)));
-                        }
-                        if ((errorLines = stderr.split("\n").filter(line => line.length)).length) {
-                            errorLines.forEach(line => line.length && this.execLog(yellow(`(stderr) ${line}`)));
-                        }
-                    }
-                    resolve();
-                });
-            });
-        }
-
-        /**
-         * Add a listener at this message. When the monitor process
-         * receives a message, it will invoke all registered functions.
-         */
-        public addServerMessageListener = (message: string, handler: Monitor.ServerMessageHandler) => {
-            const handlers = this.onMessage[message];
-            if (handlers) {
-                handlers.push(handler);
-            } else {
-                this.onMessage[message] = [handler];
-            }
-        }
-
-        /**
-         * Unregister a given listener at this message.
-         */
-        public removeServerMessageListener = (message: string, handler: Monitor.ServerMessageHandler) => {
-            const handlers = this.onMessage[message];
-            if (handlers) {
-                const index = handlers.indexOf(handler);
-                if (index > -1) {
-                    handlers.splice(index, 1);
-                }
-            }
-        }
-
-        /**
-         * Unregister all listeners at this message.
-         */
-        public clearServerMessageListeners = (message: string) => this.onMessage[message] = undefined;
-
-        private constructor(notifiers?: Monitor.NotifierHooks) {
-            this.notifiers = notifiers;
-
-            console.log(this.timestamp(), cyan("initializing session..."));
-
-            this.config = this.loadAndValidateConfiguration();
-
-            this.initializeSessionKey();
-            // determines whether or not we see the compilation / initialization / runtime output of each child server process
-            const output = this.config.showServerOutput ? "inherit" : "ignore";
-            setupMaster({ stdio: ["ignore", output, output, "ipc"] });
-
-            // handle exceptions in the master thread - there shouldn't be many of these
-            // the IPC (inter process communication) channel closed exception can't seem
-            // to be caught in a try catch, and is inconsequential, so it is ignored
-            process.on("uncaughtException", ({ message, stack }): void => {
-                if (message !== "Channel closed") {
-                    this.mainLog(red(message));
-                    if (stack) {
-                        this.mainLog(`uncaught exception\n${red(stack)}`);
-                    }
-                }
-            });
-
-            // a helpful cluster event called on the master thread each time a child process exits
-            on("exit", ({ process: { pid } }, code, signal) => {
-                const prompt = `server worker with process id ${pid} has exited with code ${code}${signal === null ? "" : `, having encountered signal ${signal}`}.`;
-                this.mainLog(cyan(prompt));
-                // to make this a robust, continuous session, every time a child process dies, we immediately spawn a new one
-                this.spawn();
-            });
-
-            this.repl = this.initializeRepl();
-            this.spawn();
-        }
-
-        /**
-         * Generates a blue UTC string associated with the time
-         * of invocation.
-         */
-        private timestamp = () => blue(`[${new Date().toUTCString()}]`);
-
-        /**
-         * A formatted, identified and timestamped log in color
-         */
-        public mainLog = (...optionalParams: any[]) => {
-            console.log(this.timestamp(), this.config.identifiers.master.text, ...optionalParams);
-        }
-
-        /**
-         * A formatted, identified and timestamped log in color for non-
-         */
-        private execLog = (...optionalParams: any[]) => {
-            console.log(this.timestamp(), this.config.identifiers.exec.text, ...optionalParams);
-        }
-
-        /**
-         * If the caller has indicated an interest
-         * in being notified of this feature, creates
-         * a GUID for this session that can, for example,
-         * be used as authentication for killing the server
-         * (checked externally).
-         */
-        private initializeSessionKey = async (): Promise<void> => {
-            if (this.notifiers?.key) {
-                this.key = Utils.GenerateGuid();
-                const success = await this.notifiers.key(this.key);
-                const statement = success ? green("distributed session key to recipients") : red("distribution of session key failed");
-                this.mainLog(statement);
-            }
-        }
-
-        /**
-         * At any arbitrary layer of nesting within the configuration objects, any single value that
-         * is not specified by the configuration is given the default counterpart. If, within an object,
-         * one peer is given by configuration and two are not, the one is preserved while the two are given
-         * the default value.
-         * @returns the composition of all of the assigned objects, much like Object.assign(), but with more
-         * granularity in the overwriting of nested objects
-         */
-        private preciseAssign = (target: any, ...sources: any[]): any => {
-            for (const source of sources) {
-                this.preciseAssignHelper(target, source);
-            }
-            return target;
-        }
-
-        private preciseAssignHelper = (target: any, source: any) => {
-            Array.from(new Set([...Object.keys(target), ...Object.keys(source)])).map(property => {
-                let targetValue: any, sourceValue: any;
-                if (sourceValue = source[property]) {
-                    if (typeof sourceValue === "object" && typeof (targetValue = target[property]) === "object") {
-                        this.preciseAssignHelper(targetValue, sourceValue);
-                    } else {
-                        target[property] = sourceValue;
-                    }
-                }
-            });
-        }
-
-        /**
-         * Reads in configuration .json file only once, in the master thread
-         * and pass down any variables the pertinent to the child processes as environment variables.
-         */
-        private loadAndValidateConfiguration = (): Configuration => {
-            let config: Configuration;
-            try {
-                console.log(this.timestamp(), cyan("validating configuration..."));
-                config = JSON.parse(readFileSync('./session.config.json', 'utf8'));
-                const options = {
-                    throwError: true,
-                    allowUnknownAttributes: false
-                };
-                // ensure all necessary and no excess information is specified by the configuration file
-                validate(config, configurationSchema, options);
-                config = this.preciseAssign({}, defaultConfig, config);
-            } catch (error) {
-                if (error instanceof ValidationError) {
-                    console.log(red("\nSession configuration failed."));
-                    console.log("The given session.config.json configuration file is invalid.");
-                    console.log(`${error.instance}: ${error.stack}`);
-                    process.exit(0);
-                } else if (error.code === "ENOENT" && error.path === "./session.config.json") {
-                    console.log(cyan("Loading default session parameters..."));
-                    console.log("Consider including a session.config.json configuration file in your project root for customization.");
-                    config = this.preciseAssign({}, defaultConfig);
-                } else {
-                    console.log(red("\nSession configuration failed."));
-                    console.log("The following unknown error occurred during configuration.");
-                    console.log(error.stack);
-                    process.exit(0);
-                }
-            } finally {
-                const { identifiers } = config!;
-                Object.keys(identifiers).forEach(key => {
-                    const resolved = key as keyof Identifiers;
-                    const { text, color } = identifiers[resolved];
-                    identifiers[resolved].text = (colorMapping.get(color) || white)(`${text}:`);
-                });
-                return config!;
-            }
-        }
-
-        /**
-         * Builds the repl that allows the following commands to be typed into stdin of the master thread.
-         */
-        private initializeRepl = (): Repl => {
-            const repl = new Repl({ identifier: () => `${this.timestamp()} ${this.config.identifiers.master.text}` });
-            const boolean = /true|false/;
-            const number = /\d+/;
-            const letters = /[a-zA-Z]+/;
-            repl.registerCommand("exit", [/clean|force/], args => this.killSession("manual exit requested by repl", args[0] === "clean", 0));
-            repl.registerCommand("restart", [/clean|force/], args => this.killActiveWorker(args[0] === "clean"));
-            repl.registerCommand("set", [letters, "port", number, boolean], args => this.setPort(args[0], Number(args[2]), args[3] === "true"));
-            repl.registerCommand("set", [/polling/, number, boolean], args => {
-                const newPollingIntervalSeconds = Math.floor(Number(args[2]));
-                if (newPollingIntervalSeconds < 0) {
-                    this.mainLog(red("the polling interval must be a non-negative integer"));
-                } else {
-                    if (newPollingIntervalSeconds !== this.config.polling.intervalSeconds) {
-                        this.config.polling.intervalSeconds = newPollingIntervalSeconds;
-                        if (args[3] === "true") {
-                            this.activeWorker?.send({ newPollingIntervalSeconds });
-                        }
-                    }
-                }
-            });
-            return repl;
-        }
-
-        private executeExitHandlers = async (reason: Error | boolean) => Promise.all(this.exitHandlers.map(handler => handler(reason)));
-
-        /**
-         * Attempts to kill the active worker gracefully, unless otherwise specified.
-         */
-        private killActiveWorker = (graceful = true, isSessionEnd = false): void => {
-            if (this.activeWorker && !this.activeWorker.isDead()) {
-                if (graceful) {
-                    this.activeWorker.send({ manualExit: { isSessionEnd } });
-                } else {
-                    this.activeWorker.process.kill();
-                }
-            }
-        }
-
-        /**
-         * Allows the caller to set the port at which the target (be it the server,
-         * the websocket, some other custom port) is listening. If an immediate restart
-         * is specified, this monitor will kill the active child and re-launch the server
-         * at the port. Otherwise, the updated port won't be used until / unless the child
-         * dies on its own and triggers a restart.
-         */
-        private setPort = (port: "server" | "socket" | string, value: number, immediateRestart: boolean): void => {
-            if (value > 1023 && value < 65536) {
-                this.config.ports[port] = value;
-                if (immediateRestart) {
-                    this.killActiveWorker();
-                }
-            } else {
-                this.mainLog(red(`${port} is an invalid port number`));
-            }
-        }
-
-        /**
-         * Kills the current active worker and proceeds to spawn a new worker,
-         * feeding in configuration information as environment variables.
-         */
-        private spawn = (): void => {
-            const {
-                polling: {
-                    route,
-                    failureTolerance,
-                    intervalSeconds
-                },
-                ports
-            } = this.config;
-            this.killActiveWorker();
-            this.activeWorker = fork({
-                pollingRoute: route,
-                pollingFailureTolerance: failureTolerance,
-                serverPort: ports.server,
-                socketPort: ports.socket,
-                pollingIntervalSeconds: intervalSeconds,
-                session_key: this.key,
-                DB: process.env.DB
-            });
-            this.mainLog(cyan(`spawned new server worker with process id ${this.activeWorker.process.pid}`));
-            // an IPC message handler that executes actions on the master thread when prompted by the active worker
-            this.activeWorker.on("message", async ({ lifecycle, action }) => {
-                if (action) {
-                    const { message, args } = action as Monitor.Action;
-                    console.log(this.timestamp(), `${this.config.identifiers.worker.text} action requested (${cyan(message)})`);
-                    switch (message) {
-                        case "kill":
-                            const { reason, graceful, errorCode } = args;
-                            this.killSession(reason, graceful, errorCode);
-                            break;
-                        case "notify_crash":
-                            if (this.notifiers?.crash) {
-                                const { error } = args;
-                                const success = await this.notifiers.crash(error);
-                                const statement = success ? green("distributed crash notification to recipients") : red("distribution of crash notification failed");
-                                this.mainLog(statement);
-                            }
-                            break;
-                        case "set_port":
-                            const { port, value, immediateRestart } = args;
-                            this.setPort(port, value, immediateRestart);
-                            break;
-                    }
-                    const handlers = this.onMessage[message];
-                    if (handlers) {
-                        handlers.forEach(handler => handler({ message, args }));
-                    }
-                }
-                if (lifecycle) {
-                    console.log(this.timestamp(), `${this.config.identifiers.worker.text} lifecycle phase (${lifecycle})`);
-                }
-            });
-        }
-
-    }
-
-    /**
-     * Effectively, each worker repairs the connection to the server by reintroducing a consistent state
-     * if its predecessor has died. It itself also polls the server heartbeat, and exits with a notification
-     * email if the server encounters an uncaught exception or if the server cannot be reached.
-     */
-    export class ServerWorker {
-
-        private static count = 0;
-        private shouldServerBeResponsive = false;
-        private exitHandlers: ExitHandler[] = [];
-        private pollingFailureCount = 0;
-        private pollingIntervalSeconds: number;
-        private pollingFailureTolerance: number;
-        private pollTarget: string;
-        private serverPort: number;
-
-        public static Create(work: Function) {
-            if (isMaster) {
-                console.error(red("cannot create a worker on the monitor process."));
-                process.exit(1);
-            } else if (++ServerWorker.count > 1) {
-                process.send?.({
-                    action: {
-                        message: "kill", args: {
-                            reason: "cannot create more than one worker on a given worker process.",
-                            graceful: false,
-                            errorCode: 1
-                        }
-                    }
-                });
-                process.exit(1);
-            } else {
-                return new ServerWorker(work);
-            }
-        }
-
-        /**
-         * Allows developers to invoke application specific logic
-         * by hooking into the exiting of the server process.
-         */
-        public addExitHandler = (handler: ExitHandler) => this.exitHandlers.push(handler);
-
-        /**
-         * Kill the session monitor (parent process) from this
-         * server worker (child process). This will also kill
-         * this process (child process).
-         */
-        public killSession = (reason: string, graceful = true, errorCode = 0) => this.sendMonitorAction("kill", { reason, graceful, errorCode });
-
-        /**
-         * A convenience wrapper to tell the session monitor (parent process)
-         * to carry out the action with the specified message and arguments.
-         */
-        public sendMonitorAction = (message: string, args?: any) => process.send!({ action: { message, args } });
-
-        private constructor(work: Function) {
-            this.lifecycleNotification(green(`initializing process... ${white(`[${process.execPath} ${process.execArgv.join(" ")}]`)}`));
-
-            const { pollingRoute, serverPort, pollingIntervalSeconds, pollingFailureTolerance } = process.env;
-            this.serverPort = Number(serverPort);
-            this.pollingIntervalSeconds = Number(pollingIntervalSeconds);
-            this.pollingFailureTolerance = Number(pollingFailureTolerance);
-            this.pollTarget = `http://localhost:${serverPort}${pollingRoute}`;
-
-            this.configureProcess();
-            work();
-            this.pollServer();
-        }
-
-        /**
-         * Set up message and uncaught exception handlers for this
-         * server process.
-         */
-        private configureProcess = () => {
-            // updates the local values of variables to the those sent from master
-            process.on("message", async ({ newPollingIntervalSeconds, manualExit }) => {
-                if (newPollingIntervalSeconds !== undefined) {
-                    this.pollingIntervalSeconds = newPollingIntervalSeconds;
-                }
-                if (manualExit !== undefined) {
-                    const { isSessionEnd } = manualExit;
-                    await this.executeExitHandlers(isSessionEnd);
-                    process.exit(0);
-                }
-            });
-
-            // one reason to exit, as the process might be in an inconsistent state after such an exception
-            process.on('uncaughtException', this.proactiveUnplannedExit);
-            process.on('unhandledRejection', reason => {
-                const appropriateError = reason instanceof Error ? reason : new Error(`unhandled rejection: ${reason}`);
-                this.proactiveUnplannedExit(appropriateError);
-            });
-        }
-
-        /**
-         * Execute the list of functions registered to be called
-         * whenever the process exits.
-         */
-        private executeExitHandlers = async (reason: Error | boolean) => Promise.all(this.exitHandlers.map(handler => handler(reason)));
-
-        /**
-         * Notify master thread (which will log update in the console) of initialization via IPC.
-         */
-        public lifecycleNotification = (event: string) => process.send?.({ lifecycle: event });
-
-        /**
-         * Called whenever the process has a reason to terminate, either through an uncaught exception
-         * in the process (potentially inconsistent state) or the server cannot be reached.
-         */
-        private proactiveUnplannedExit = async (error: Error): Promise<void> => {
-            this.shouldServerBeResponsive = false;
-            // communicates via IPC to the master thread that it should dispatch a crash notification email
-            this.sendMonitorAction("notify_crash", { error });
-            await this.executeExitHandlers(error);
-            // notify master thread (which will log update in the console) of crash event via IPC
-            this.lifecycleNotification(red(`crash event detected @ ${new Date().toUTCString()}`));
-            this.lifecycleNotification(red(error.message));
-            process.exit(1);
-        }
-
-        /**
-         * This monitors the health of the server by submitting a get request to whatever port / route specified
-         * by the configuration every n seconds, where n is also given by the configuration. 
-         */
-        private pollServer = async (): Promise<void> => {
-            await new Promise<void>(resolve => {
-                setTimeout(async () => {
-                    try {
-                        await get(this.pollTarget);
-                        if (!this.shouldServerBeResponsive) {
-                            // notify monitor thread that the server is up and running
-                            this.lifecycleNotification(green(`listening on ${this.serverPort}...`));
-                        }
-                        this.shouldServerBeResponsive = true;
-                    } catch (error) {
-                        // if we expect the server to be unavailable, i.e. during compilation,
-                        // the listening variable is false, activeExit will return early and the child
-                        // process will continue
-                        if (this.shouldServerBeResponsive) {
-                            if (++this.pollingFailureCount > this.pollingFailureTolerance) {
-                                this.proactiveUnplannedExit(error);
-                            } else {
-                                this.lifecycleNotification(yellow(`the server has encountered ${this.pollingFailureCount} of ${this.pollingFailureTolerance} tolerable failures`));
-                            }
-                        }
-                    } finally {
-                        resolve();
-                    }
-                }, 1000 * this.pollingIntervalSeconds);
-            });
-            // controlled, asynchronous infinite recursion achieves a persistent poll that does not submit a new request until the previous has completed
-            this.pollServer();
-        }
-
-    }
-
-}
-- 
cgit v1.2.3-70-g09d2

