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/views/nodes/FormattedTextBox.tsx | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index a08c14436..7e81876a2 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -47,6 +47,7 @@ import { AudioBox } from './AudioBox';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
 import { InkTool } from '../../../new_fields/InkField';
 import { TraceMobx } from '../../../new_fields/util';
+import RichTextMenu from '../../util/RichTextMenu';
 
 library.add(faEdit);
 library.add(faSmile, faTextHeight, faUpload);
@@ -1032,7 +1033,9 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         const self = FormattedTextBox;
         return new Plugin({
             view(newView) {
-                return self.ToolTipTextMenu = FormattedTextBox.getToolTip(newView);
+                // return self.ToolTipTextMenu = FormattedTextBox.getToolTip(newView);
+                RichTextMenu.Instance.changeView(newView);
+                return RichTextMenu.Instance;
             }
         });
     }
@@ -1121,7 +1124,9 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : "";
         const interactive = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground;
         if (this.props.isSelected()) {
-            FormattedTextBox.ToolTipTextMenu!.updateFromDash(this._editorView!, undefined, this.props);
+            // TODO: ftong --> update from dash in richtextmenu
+            RichTextMenu.Instance.updateFromDash(this._editorView!, undefined, this.props);
+            // FormattedTextBox.ToolTipTextMenu!.updateFromDash(this._editorView!, undefined, this.props);
         } else if (FormattedTextBoxComment.textBox === this) {
             FormattedTextBoxComment.Hide();
         }
-- 
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/views/nodes/FormattedTextBox.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/views/nodes/FormattedTextBox.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/views/nodes/FormattedTextBox.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 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/views/nodes/FormattedTextBox.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/views/nodes/FormattedTextBox.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

