aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStanley Yip <33562077+yipstanley@users.noreply.github.com>2020-01-09 18:55:14 -0500
committerGitHub <noreply@github.com>2020-01-09 18:55:14 -0500
commit29a1f1dae11e7208977faf0c17ec80b6bad97223 (patch)
tree46204c2bdaa18cc352868b8d67450f8f2a1a3506
parent6a45fd58601a2b03ed234f05b9b0a1b91d25a54d (diff)
parenteec70e08bf5fa5f3a0a4e3b07bdc05777f71d2ef (diff)
Merge pull request #329 from browngraphicslab/textbox_fawn_fix
rich text menu
-rw-r--r--.bash_profile3
-rw-r--r--package-lock.json89
-rw-r--r--src/client/util/ProseMirrorEditorView.tsx74
-rw-r--r--src/client/util/RichTextMenu.scss121
-rw-r--r--src/client/util/RichTextMenu.tsx854
-rw-r--r--src/client/util/SelectionManager.ts2
-rw-r--r--src/client/util/TooltipTextMenu.tsx12
-rw-r--r--src/client/views/AntimodeMenu.scss15
-rw-r--r--src/client/views/AntimodeMenu.tsx34
-rw-r--r--src/client/views/DocumentDecorations.tsx4
-rw-r--r--src/client/views/MainView.tsx2
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx38
-rw-r--r--src/client/views/pdf/PDFMenu.tsx2
13 files changed, 1172 insertions, 78 deletions
diff --git a/.bash_profile b/.bash_profile
new file mode 100644
index 000000000..d54c83f48
--- /dev/null
+++ b/.bash_profile
@@ -0,0 +1,3 @@
+export PATH="/usr/local/bin:$PATH"
+export PATH="$PATH:/usr/local/mongodb/bin"
+
diff --git a/package-lock.json b/package-lock.json
index 649f40a97..cf61e6aab 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1978,7 +1978,7 @@
},
"util": {
"version": "0.10.3",
- "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
+ "resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz",
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
"dev": true,
"requires": {
@@ -2561,7 +2561,7 @@
},
"browserify-aes": {
"version": "1.2.0",
- "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
+ "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
"integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
"requires": {
"buffer-xor": "^1.0.3",
@@ -2595,7 +2595,7 @@
},
"browserify-rsa": {
"version": "4.0.1",
- "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
+ "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
"integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
"requires": {
"bn.js": "^4.1.0",
@@ -2779,7 +2779,7 @@
},
"camelcase-keys": {
"version": "2.1.0",
- "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
+ "resolved": "http://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
"integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
"requires": {
"camelcase": "^2.0.0",
@@ -3563,7 +3563,7 @@
},
"create-hash": {
"version": "1.2.0",
- "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
+ "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
"integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
"requires": {
"cipher-base": "^1.0.1",
@@ -3575,7 +3575,7 @@
},
"create-hmac": {
"version": "1.1.7",
- "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
+ "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
"integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
"requires": {
"cipher-base": "^1.0.3",
@@ -4118,7 +4118,7 @@
},
"diffie-hellman": {
"version": "5.0.3",
- "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
+ "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
"integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
"requires": {
"bn.js": "^4.1.0",
@@ -5361,8 +5361,7 @@
},
"ansi-regex": {
"version": "2.1.1",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"aproba": {
"version": "1.2.0",
@@ -5380,13 +5379,11 @@
},
"balanced-match": {
"version": "1.0.0",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
- "optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -5399,18 +5396,15 @@
},
"code-point-at": {
"version": "1.1.0",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"concat-map": {
"version": "0.0.1",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"console-control-strings": {
"version": "1.1.0",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"core-util-is": {
"version": "1.0.2",
@@ -5513,8 +5507,7 @@
},
"inherits": {
"version": "2.0.3",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"ini": {
"version": "1.3.5",
@@ -5524,7 +5517,6 @@
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
- "optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -5537,20 +5529,17 @@
"minimatch": {
"version": "3.0.4",
"bundled": true,
- "optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
- "optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@@ -5567,7 +5556,6 @@
"mkdirp": {
"version": "0.5.1",
"bundled": true,
- "optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -5640,8 +5628,7 @@
},
"number-is-nan": {
"version": "1.0.1",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"object-assign": {
"version": "4.1.1",
@@ -5651,7 +5638,6 @@
"once": {
"version": "1.4.0",
"bundled": true,
- "optional": true,
"requires": {
"wrappy": "1"
}
@@ -5727,8 +5713,7 @@
},
"safe-buffer": {
"version": "5.1.2",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -5758,7 +5743,6 @@
"string-width": {
"version": "1.0.2",
"bundled": true,
- "optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -5776,7 +5760,6 @@
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
- "optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -5815,13 +5798,11 @@
},
"wrappy": {
"version": "1.0.2",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"yallist": {
"version": "3.0.3",
- "bundled": true,
- "optional": true
+ "bundled": true
}
}
},
@@ -7073,7 +7054,7 @@
"babel-runtime": "^6.22.0",
"bluebird": "^3.4.7",
"boolify-string": "^2.0.2",
- "emit-logger": "github:chocolateboy/emit-logger#better-emitter-name",
+ "emit-logger": "github:chocolateboy/emit-logger#b9d25a2d939e42f29c940861e9648bd0fb810070",
"lodash": "^4.17.4",
"semver": "^5.0.3",
"source-map-support": "^0.5.9"
@@ -7081,7 +7062,7 @@
},
"is-accessor-descriptor": {
"version": "0.1.6",
- "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
+ "resolved": "http://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
"integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
"requires": {
"kind-of": "^3.0.2"
@@ -7130,7 +7111,7 @@
},
"is-data-descriptor": {
"version": "0.1.4",
- "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
+ "resolved": "http://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
"integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
"requires": {
"kind-of": "^3.0.2"
@@ -7915,7 +7896,7 @@
},
"load-json-file": {
"version": "1.1.0",
- "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
+ "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
"integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
"requires": {
"graceful-fs": "^4.1.2",
@@ -8229,7 +8210,7 @@
},
"media-typer": {
"version": "0.3.0",
- "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
"mem": {
@@ -8261,7 +8242,7 @@
},
"meow": {
"version": "3.7.0",
- "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
+ "resolved": "http://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
"integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
"requires": {
"camelcase-keys": "^2.0.0",
@@ -8454,7 +8435,7 @@
},
"mkdirp": {
"version": "0.5.1",
- "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+ "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"requires": {
"minimist": "0.0.8"
@@ -8800,7 +8781,7 @@
},
"next-tick": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
+ "resolved": "http://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
"integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
},
"nice-try": {
@@ -8964,7 +8945,7 @@
},
"semver": {
"version": "5.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
+ "resolved": "http://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
"integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8="
},
"tar": {
@@ -12573,7 +12554,7 @@
},
"os-homedir": {
"version": "1.0.2",
- "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+ "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
},
"os-locale": {
@@ -12586,7 +12567,7 @@
},
"os-tmpdir": {
"version": "1.0.2",
- "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
},
"osenv": {
@@ -12826,7 +12807,7 @@
},
"path-is-absolute": {
"version": "1.0.1",
- "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"path-is-inside": {
@@ -14491,7 +14472,7 @@
},
"safe-regex": {
"version": "1.1.0",
- "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
+ "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
"integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
"requires": {
"ret": "~0.1.10"
@@ -14756,7 +14737,7 @@
},
"sha.js": {
"version": "2.4.11",
- "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
+ "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
"requires": {
"inherits": "^2.0.1",
@@ -15499,7 +15480,7 @@
},
"string_decoder": {
"version": "1.1.1",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
@@ -15545,7 +15526,7 @@
},
"strip-eof": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
+ "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
"integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8="
},
"strip-indent": {
@@ -16298,7 +16279,7 @@
},
"tty-browserify": {
"version": "0.0.0",
- "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
+ "resolved": "http://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
"integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=",
"dev": true
},
diff --git a/src/client/util/ProseMirrorEditorView.tsx b/src/client/util/ProseMirrorEditorView.tsx
new file mode 100644
index 000000000..3e5fd0604
--- /dev/null
+++ b/src/client/util/ProseMirrorEditorView.tsx
@@ -0,0 +1,74 @@
+import React from "react";
+import { EditorView } from "prosemirror-view";
+import { EditorState } from "prosemirror-state";
+
+export interface ProseMirrorEditorViewProps {
+ /* EditorState instance to use. */
+ editorState: EditorState;
+ /* Called when EditorView produces new EditorState. */
+ onEditorState: (editorState: EditorState) => any;
+}
+
+/**
+ * This wraps ProseMirror's EditorView into React component.
+ * This code was found on https://discuss.prosemirror.net/t/using-with-react/904
+ */
+export class ProseMirrorEditorView extends React.Component<ProseMirrorEditorViewProps> {
+
+ private _editorView?: EditorView;
+
+ _createEditorView = (element: HTMLDivElement | null) => {
+ if (element != null) {
+ this._editorView = new EditorView(element, {
+ state: this.props.editorState,
+ dispatchTransaction: this.dispatchTransaction,
+ });
+ }
+ };
+
+ dispatchTransaction = (tx: any) => {
+ // In case EditorView makes any modification to a state we funnel those
+ // modifications up to the parent and apply to the EditorView itself.
+ const editorState = this.props.editorState.apply(tx);
+ if (this._editorView != null) {
+ this._editorView.updateState(editorState);
+ }
+ this.props.onEditorState(editorState);
+ };
+
+ focus() {
+ if (this._editorView) {
+ this._editorView.focus();
+ }
+ }
+
+ componentWillReceiveProps(nextProps: { editorState: EditorState<any>; }) {
+ // In case we receive new EditorState through props — we apply it to the
+ // EditorView instance.
+ if (this._editorView) {
+ if (nextProps.editorState !== this.props.editorState) {
+ this._editorView.updateState(nextProps.editorState);
+ }
+ }
+ }
+
+ componentWillUnmount() {
+ if (this._editorView) {
+ this._editorView.destroy();
+ }
+ }
+
+ shouldComponentUpdate() {
+ // Note that EditorView manages its DOM itself so we'd ratrher don't mess
+ // with it.
+ return false;
+ }
+
+ render() {
+ // Render just an empty div which is then used as a container for an
+ // EditorView instance.
+ return (
+ <div ref={this._createEditorView} />
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/util/RichTextMenu.scss b/src/client/util/RichTextMenu.scss
new file mode 100644
index 000000000..43cc23ecd
--- /dev/null
+++ b/src/client/util/RichTextMenu.scss
@@ -0,0 +1,121 @@
+@import "../views/globalCssVariables";
+
+.button-dropdown-wrapper {
+ position: relative;
+
+ .dropdown-button {
+ width: 15px;
+ padding-left: 5px;
+ padding-right: 5px;
+ }
+
+ .dropdown-button-combined {
+ width: 50px;
+ display: flex;
+ justify-content: space-between;
+
+ svg:nth-child(2) {
+ margin-top: 2px;
+ }
+ }
+
+ .dropdown {
+ position: absolute;
+ top: 35px;
+ left: 0;
+ background-color: #323232;
+ color: $light-color-secondary;
+ border: 1px solid #4d4d4d;
+ border-radius: 0 6px 6px 6px;
+ box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
+ min-width: 150px;
+ padding: 5px;
+ font-size: 12px;
+ z-index: 10001;
+
+ button {
+ background-color: #323232;
+ border: 1px solid black;
+ border-radius: 1px;
+ padding: 6px;
+ margin: 5px 0;
+ font-size: 10px;
+
+ &:hover {
+ background-color: black;
+ }
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+
+ 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;
+ font-size: 12px;
+ height: 100%;
+ margin-right: 3px;
+
+ &:focus,
+ &:hover {
+ background-color: black;
+ }
+
+ &::-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
new file mode 100644
index 000000000..639772faa
--- /dev/null
+++ b/src/client/util/RichTextMenu.tsx
@@ -0,0 +1,854 @@
+import React = require("react");
+import AntimodeMenu from "../views/AntimodeMenu";
+import { observable, action, } from "mobx";
+import { observer } from "mobx-react";
+import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos, Schema } from "prosemirror-model";
+import { schema } from "./RichTextSchema";
+import { EditorView } from "prosemirror-view";
+import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
+import { faBold, faItalic, faUnderline, faStrikethrough, faSubscript, faSuperscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller, faSleigh } from "@fortawesome/free-solid-svg-icons";
+import { MenuItem, Dropdown } from "prosemirror-menu";
+import { updateBullets } from "./ProsemirrorExampleTransfer";
+import { FieldViewProps } from "../views/nodes/FieldView";
+import { NumCast, Cast, StrCast } from "../../new_fields/Types";
+import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox";
+import { unimplementedFunction, Utils } from "../../Utils";
+import { wrapInList } from "prosemirror-schema-list";
+import { PastelSchemaPalette, DarkPastelSchemaPalette } from '../../new_fields/SchemaHeaderField';
+import "./RichTextMenu.scss";
+import { DocServer } from "../DocServer";
+import { Doc } from "../../new_fields/Doc";
+import { SelectionManager } from "./SelectionManager";
+import { LinkManager } from "./LinkManager";
+const { toggleMark, setBlockType } = require("prosemirror-commands");
+
+library.add(faBold, faItalic, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller);
+
+@observer
+export default class RichTextMenu extends AntimodeMenu {
+ static Instance: RichTextMenu;
+ public overMenu: boolean = false; // kind of hacky way to prevent selects not being selectable
+
+ private view?: EditorView;
+ private editorProps: FieldViewProps & FormattedTextBoxProps | undefined;
+
+ private fontSizeOptions: { mark: Mark | null, title: string, label: string, command: any, hidden?: boolean, style?: {} }[];
+ private fontFamilyOptions: { mark: Mark | null, title: string, label: string, command: any, hidden?: boolean, style?: {} }[];
+ private listTypeOptions: { node: NodeType | any | null, title: string, label: string, command: any, style?: {} }[];
+ private fontColors: (string | undefined)[];
+ private highlightColors: (string | undefined)[];
+
+ @observable private boldActive: boolean = false;
+ @observable private italicsActive: boolean = false;
+ @observable private underlineActive: boolean = false;
+ @observable private strikethroughActive: boolean = false;
+ @observable private subscriptActive: boolean = false;
+ @observable private superscriptActive: boolean = false;
+
+ @observable private activeFontSize: string = "";
+ @observable private activeFontFamily: string = "";
+ @observable private activeListType: string = "";
+
+ @observable private brushIsEmpty: boolean = true;
+ @observable private brushMarks: Set<Mark> = new Set();
+ @observable private showBrushDropdown: boolean = false;
+
+ @observable private activeFontColor: string = "black";
+ @observable private showColorDropdown: boolean = false;
+
+ @observable private activeHighlightColor: string = "transparent";
+ @observable private showHighlightDropdown: boolean = false;
+
+ @observable private currentLink: string | undefined = "";
+ @observable private showLinkDropdown: boolean = false;
+
+ constructor(props: Readonly<{}>) {
+ 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, style: { fontFamily: "Times New Roman" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Arial" }), title: "Set font family", label: "Arial", command: this.changeFontFamily, style: { fontFamily: "Arial" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Georgia" }), title: "Set font family", label: "Georgia", command: this.changeFontFamily, style: { fontFamily: "Georgia" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Comic Sans MS" }), title: "Set font family", label: "Comic Sans MS", command: this.changeFontFamily, style: { fontFamily: "Comic Sans MS" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Tahoma" }), title: "Set font family", label: "Tahoma", command: this.changeFontFamily, style: { fontFamily: "Tahoma" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Impact" }), title: "Set font family", label: "Impact", command: this.changeFontFamily, style: { fontFamily: "Impact" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Crimson Text" }), title: "Set font family", label: "Crimson Text", command: this.changeFontFamily, style: { fontFamily: "Crimson Text" } },
+ { mark: null, title: "", label: "various", command: unimplementedFunction, hidden: true },
+ // { mark: null, title: "", label: "default", command: unimplementedFunction, hidden: true },
+ ];
+
+ 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
+ changeView(view: EditorView) {
+ this.view = view;
+ }
+
+ update(view: EditorView, lastState: EditorState | undefined) {
+ this.updateFromDash(view, lastState, this.editorProps);
+ }
+
+ @action
+ public async updateFromDash(view: EditorView, lastState: EditorState | undefined, props: any) {
+ if (!view) {
+ console.log("no editor? why?");
+ return;
+ }
+ this.view = view;
+ const state = view.state;
+ 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;
+
+ // update active marks
+ const activeMarks = this.getActiveMarksOnSelection();
+ this.setActiveMarkButtons(activeMarks);
+
+ // update active font family and size
+ const active = this.getActiveFontStylesOnSelection();
+ const activeFamilies = active && active.get("families");
+ const activeSizes = active && active.get("sizes");
+
+ this.activeFontFamily = !activeFamilies || activeFamilies.length === 0 ? "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);
+ }
+
+ setMark = (mark: Mark, state: EditorState<any>, dispatch: any) => {
+ if (mark) {
+ const node = (state.selection as NodeSelection).node;
+ if (node ?.type === schema.nodes.ordered_list) {
+ let attrs = node.attrs;
+ if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, setFontFamily: mark.attrs.family };
+ if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, setFontSize: mark.attrs.fontSize };
+ if (mark.type === schema.marks.pFontColor) attrs = { ...attrs, setFontColor: mark.attrs.color };
+ const tr = updateBullets(state.tr.setNodeMarkup(state.selection.from, node.type, attrs), state.schema);
+ dispatch(tr.setSelection(new NodeSelection(tr.doc.resolve(state.selection.from))));
+ } else {
+ toggleMark(mark.type, mark.attrs)(state, (tx: any) => {
+ const { from, $from, to, empty } = tx.selection;
+ if (!tx.doc.rangeHasMark(from, to, mark.type)) {
+ toggleMark(mark.type, mark.attrs)({ tr: tx, doc: tx.doc, selection: tx.selection, storedMarks: tx.storedMarks }, dispatch);
+ } else dispatch(tx);
+ });
+ }
+ }
+ }
+
+ // finds font sizes and families in selection
+ getActiveFontStylesOnSelection() {
+ if (!this.view) return;
+
+ const activeFamilies: string[] = [];
+ const activeSizes: string[] = [];
+ const state = this.view.state;
+ const pos = this.view.state.selection.$from;
+ const ref_node = this.reference_node(pos);
+ if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) {
+ ref_node.marks.forEach(m => {
+ m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family);
+ m.type === state.schema.marks.pFontSize && activeSizes.push(String(m.attrs.fontSize) + "pt");
+ });
+ }
+
+ const styles = new Map<String, String[]>();
+ styles.set("families", activeFamilies);
+ styles.set("sizes", activeSizes);
+ return styles;
+ }
+
+ getMarksInSelection(state: EditorState<any>) {
+ const found = new Set<Mark>();
+ const { from, to } = state.selection as TextSelection;
+ state.doc.nodesBetween(from, to, (node) => node.marks.forEach(m => found.add(m)));
+ return found;
+ }
+
+ //finds all active marks on selection in given group
+ getActiveMarksOnSelection() {
+ if (!this.view) return;
+
+ const markGroup = [schema.marks.strong, schema.marks.em, schema.marks.underline, schema.marks.strikethrough, schema.marks.superscript, schema.marks.subscript];
+ if (this.view.state.storedMarks) return this.view.state.storedMarks.map(mark => mark.type);
+ //current selection
+ const { empty, ranges, $to } = this.view.state.selection as TextSelection;
+ const state = this.view.state;
+ let activeMarks: MarkType[] = [];
+ if (!empty) {
+ activeMarks = markGroup.filter(mark => {
+ const has = false;
+ for (let i = 0; !has && i < ranges.length; i++) {
+ return state.doc.rangeHasMark(ranges[i].$from.pos, ranges[i].$to.pos, mark);
+ }
+ return false;
+ });
+ }
+ else {
+ const pos = this.view.state.selection.$from;
+ const ref_node: ProsNode | null = this.reference_node(pos);
+ if (ref_node !== null && ref_node !== this.view.state.doc) {
+ if (ref_node.isText) {
+ }
+ else {
+ return [];
+ }
+ activeMarks = markGroup.filter(mark_type => {
+ if (mark_type === state.schema.marks.pFontSize) {
+ return ref_node.marks.some(m => m.type.name === state.schema.marks.pFontSize.name);
+ }
+ const mark = state.schema.mark(mark_type);
+ return ref_node.marks.includes(mark);
+ });
+ }
+ }
+ return activeMarks;
+ }
+
+ destroy() {
+ }
+
+ @action
+ setActiveMarkButtons(activeMarks: MarkType[] | undefined) {
+ if (!activeMarks) return;
+
+ this.boldActive = false;
+ this.italicsActive = false;
+ this.underlineActive = false;
+ this.strikethroughActive = false;
+ this.subscriptActive = false;
+ this.superscriptActive = false;
+
+ activeMarks.forEach(mark => {
+ switch (mark.name) {
+ case "strong": this.boldActive = true; break;
+ case "em": this.italicsActive = true; break;
+ case "underline": this.underlineActive = true; break;
+ case "strikethrough": this.strikethroughActive = true; break;
+ case "subscript": this.subscriptActive = true; break;
+ case "superscript": this.superscriptActive = true; break;
+ }
+ });
+ }
+
+ createButton(faIcon: string, title: string, isActive: boolean = false, command?: any, onclick?: any) {
+ const self = this;
+ function onClick(e: React.PointerEvent) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.view && command && command(self.view!.state, self.view!.dispatch, self.view);
+ self.view && onclick && onclick(self.view!.state, self.view!.dispatch, self.view);
+ self.setActiveMarkButtons(self.getActiveMarksOnSelection());
+ }
+
+ return (
+ <button className={"antimodeMenu-button" + (isActive ? " active" : "")} title={title} onPointerDown={onClick}>
+ <FontAwesomeIcon icon={faIcon as IconProp} size="lg" />
+ </button>
+ );
+ }
+
+ createMarksDropdown(activeOption: string, options: { mark: Mark | null, title: string, label: string, command: (mark: Mark, view: EditorView) => void, hidden?: boolean, style?: {} }[]): JSX.Element {
+ const items = options.map(({ title, label, hidden, style }) => {
+ if (hidden) {
+ return label === activeOption ?
+ <option value={label} title={title} style={style ? style : {}} selected hidden>{label}</option> :
+ <option value={label} title={title} style={style ? style : {}} hidden>{label}</option>;
+ }
+ return label === activeOption ?
+ <option value={label} title={title} style={style ? style : {}} selected>{label}</option> :
+ <option value={label} title={title} style={style ? style : {}}>{label}</option>;
+ });
+
+ const self = this;
+ function onChange(e: React.ChangeEvent<HTMLSelectElement>) {
+ e.stopPropagation();
+ e.preventDefault();
+ options.forEach(({ label, mark, command }) => {
+ if (e.target.value === label) {
+ self.view && mark && command(mark, self.view);
+ }
+ });
+ }
+ 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, style?: {} }[]): JSX.Element {
+ const items = options.map(({ title, label, hidden, style }) => {
+ if (hidden) {
+ return label === activeOption ?
+ <option value={label} title={title} style={style ? style : {}} selected hidden>{label}</option> :
+ <option value={label} title={title} style={style ? style : {}} hidden>{label}</option>;
+ }
+ return label === activeOption ?
+ <option value={label} title={title} style={style ? style : {}} selected>{label}</option> :
+ <option value={label} title={title} style={style ? style : {}}>{label}</option>;
+ });
+
+ const self = this;
+ function onChange(val: string) {
+ options.forEach(({ label, node, command }) => {
+ if (val === label) {
+ self.view && node && command(node);
+ }
+ });
+ }
+ return <select onChange={e => onChange(e.target.value)}>{items}</select>;
+ }
+
+ changeFontSize = (mark: Mark, view: EditorView) => {
+ const size = mark.attrs.fontSize;
+ if (this.editorProps) {
+ const ruleProvider = this.editorProps.ruleProvider;
+ const heading = NumCast(this.editorProps.Document.heading);
+ if (ruleProvider && heading) {
+ ruleProvider["ruleSize_" + heading] = size;
+ }
+ }
+ this.setMark(view.state.schema.marks.pFontSize.create({ fontSize: size }), view.state, view.dispatch);
+ }
+
+ changeFontFamily = (mark: Mark, view: EditorView) => {
+ const fontName = mark.attrs.family;
+ if (this.editorProps) {
+ const ruleProvider = this.editorProps.ruleProvider;
+ const heading = NumCast(this.editorProps.Document.heading);
+ if (ruleProvider && heading) {
+ ruleProvider["ruleFont_" + heading] = fontName;
+ }
+ }
+ this.setMark(view.state.schema.marks.pFontFamily.create({ family: fontName }), view.state, view.dispatch);
+ }
+
+ // TODO: remove doesn't work
+ //remove all node type and apply the passed-in one to the selected text
+ changeListType = (nodeType: NodeType | undefined) => {
+ if (!this.view) return;
+
+ if (nodeType === schema.nodes.bullet_list) {
+ wrapInList(nodeType)(this.view.state, this.view.dispatch);
+ } else {
+ const marks = this.view.state.storedMarks || (this.view.state.selection.$to.parentOffset && this.view.state.selection.$from.marks());
+ if (!wrapInList(schema.nodes.ordered_list)(this.view.state, (tx2: any) => {
+ const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle);
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
+
+ this.view!.dispatch(tx2);
+ })) {
+ const tx2 = this.view.state.tr;
+ const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle);
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
+
+ this.view.dispatch(tx3);
+ }
+ }
+ }
+
+ insertSummarizer(state: EditorState<any>, dispatch: any) {
+ if (state.selection.empty) return false;
+ const mark = state.schema.marks.summarize.create();
+ const tr = state.tr;
+ tr.addMark(state.selection.from, state.selection.to, mark);
+ const content = tr.selection.content();
+ const newNode = state.schema.nodes.summary.create({ visibility: false, text: content, textslice: content.toJSON() });
+ dispatch && dispatch(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark));
+ return true;
+ }
+
+ @action toggleBrushDropdown() { this.showBrushDropdown = !this.showBrushDropdown; }
+
+ createBrushButton() {
+ const self = this;
+ function onBrushClick(e: React.PointerEvent) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.view && self.fillBrush(self.view.state, self.view.dispatch);
+ }
+
+ let label = "Stored marks: ";
+ if (this.brushMarks && this.brushMarks.size > 0) {
+ this.brushMarks.forEach((mark: Mark) => {
+ const markType = mark.type;
+ label += markType.name;
+ label += ", ";
+ });
+ label = label.substring(0, label.length - 2);
+ } else {
+ label = "No marks are currently stored";
+ }
+
+ 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 (
+ <ButtonDropdown view={this.view} button={button} dropdownContent={dropdownContent} />
+ );
+ }
+
+ @action
+ clearBrush() {
+ RichTextMenu.Instance.brushIsEmpty = true;
+ RichTextMenu.Instance.brushMarks = new Set();
+ }
+
+ @action
+ fillBrush(state: EditorState<any>, dispatch: any) {
+ if (!this.view) return;
+
+ if (this.brushIsEmpty) {
+ const selected_marks = this.getMarksInSelection(this.view.state);
+ if (selected_marks.size >= 0) {
+ this.brushMarks = selected_marks;
+ this.brushIsEmpty = !this.brushIsEmpty;
+ }
+ }
+ else {
+ const { from, to, $from } = this.view.state.selection;
+ if (!this.view.state.selection.empty && $from && $from.nodeAfter) {
+ if (this.brushMarks && to - from > 0) {
+ this.view.dispatch(this.view.state.tr.removeMark(from, to));
+ Array.from(this.brushMarks).filter(m => m.type !== schema.marks.user_mark).forEach((mark: Mark) => {
+ this.setMark(mark, this.view!.state, this.view!.dispatch);
+ });
+ }
+ }
+ else {
+ this.brushIsEmpty = !this.brushIsEmpty;
+ }
+ }
+ }
+
+ @action toggleColorDropdown() { this.showColorDropdown = !this.showColorDropdown; }
+ @action setActiveColor(color: string) { this.activeFontColor = color; }
+
+ createColorButton() {
+ const self = this;
+ function onColorClick(e: React.PointerEvent) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch);
+ }
+ function changeColor(e: React.PointerEvent, color: string) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.setActiveColor(color);
+ self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch);
+ }
+
+ const 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 (
+ <ButtonDropdown view={this.view} button={button} dropdownContent={dropdownContent} />
+ );
+ }
+
+ public insertColor(color: String, state: EditorState<any>, dispatch: any) {
+ const colorMark = state.schema.mark(state.schema.marks.pFontColor, { color: color });
+ if (state.selection.empty) {
+ dispatch(state.tr.addStoredMark(colorMark));
+ return false;
+ }
+ this.setMark(colorMark, state, dispatch);
+ }
+
+ @action toggleHighlightDropdown() { this.showHighlightDropdown = !this.showHighlightDropdown; }
+ @action setActiveHighlight(color: string) { this.activeHighlightColor = color; }
+
+ createHighlighterButton() {
+ const self = this;
+ function onHighlightClick(e: React.PointerEvent) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch);
+ }
+ function changeHighlight(e: React.PointerEvent, color: string) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.setActiveHighlight(color);
+ self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch);
+ }
+
+ const 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 (
+ <ButtonDropdown view={this.view} button={button} dropdownContent={dropdownContent} />
+ );
+ }
+
+ insertHighlight(color: String, state: EditorState<any>, dispatch: any) {
+ if (state.selection.empty) return false;
+ toggleMark(state.schema.marks.marker, { highlight: color })(state, dispatch);
+ }
+
+ @action toggleLinkDropdown() { this.showLinkDropdown = !this.showLinkDropdown; }
+ @action setCurrentLink(link: string) { this.currentLink = link; }
+
+ createLinkButton() {
+ const self = this;
+
+ function onLinkChange(e: React.ChangeEvent<HTMLInputElement>) {
+ self.setCurrentLink(e.target.value);
+ }
+
+ const link = this.currentLink ? this.currentLink : "";
+
+ const button = <FontAwesomeIcon icon="link" size="lg" />
+
+ 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 (
+ <ButtonDropdown view={this.view} button={button} dropdownContent={dropdownContent} openDropdownOnButton={true} />
+ );
+ }
+
+ async getTextLinkTargetTitle() {
+ if (!this.view) return;
+
+ const node = this.view.state.selection.$from.nodeAfter;
+ const link = node && node.marks.find(m => m.type.name === "link");
+ if (link) {
+ const href = link.attrs.href;
+ if (href) {
+ if (href.indexOf(Utils.prepend("/doc/")) === 0) {
+ const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+ if (linkclicked) {
+ const linkDoc = await DocServer.GetRefField(linkclicked);
+ if (linkDoc instanceof Doc) {
+ const anchor1 = await Cast(linkDoc.anchor1, Doc);
+ const anchor2 = await Cast(linkDoc.anchor2, Doc);
+ const currentDoc = SelectionManager.SelectedDocuments().length && SelectionManager.SelectedDocuments()[0].props.Document;
+ if (currentDoc && anchor1 && anchor2) {
+ if (Doc.AreProtosEqual(currentDoc, anchor1)) {
+ return StrCast(anchor2.title);
+ }
+ if (Doc.AreProtosEqual(currentDoc, anchor2)) {
+ return StrCast(anchor1.title);
+ }
+ }
+ }
+ }
+ } else {
+ return href;
+ }
+ } else {
+ return link.attrs.title;
+ }
+ }
+ }
+
+ // TODO: should check for valid URL
+ makeLinkToURL = (target: String, lcoation: string) => {
+ if (!this.view) return;
+
+ let node = this.view.state.selection.$from.nodeAfter;
+ let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: target, location: location });
+ this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link));
+ this.view.dispatch(this.view.state.tr.addMark(this.view.state.selection.from, this.view.state.selection.to, link));
+ node = this.view.state.selection.$from.nodeAfter;
+ link = node && node.marks.find(m => m.type.name === "link");
+ }
+
+ deleteLink = () => {
+ if (!this.view) return;
+
+ const node = this.view.state.selection.$from.nodeAfter;
+ const link = node && node.marks.find(m => m.type === this.view!.state.schema.marks.link);
+ const href = link!.attrs.href;
+ if (href) {
+ if (href.indexOf(Utils.prepend("/doc/")) === 0) {
+ const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+ if (linkclicked) {
+ DocServer.GetRefField(linkclicked).then(async linkDoc => {
+ if (linkDoc instanceof Doc) {
+ LinkManager.Instance.deleteLink(linkDoc);
+ this.view!.dispatch(this.view!.state.tr.removeMark(this.view!.state.selection.from, this.view!.state.selection.to, this.view!.state.schema.marks.link));
+ }
+ });
+ }
+ } else {
+ if (node) {
+ const extension = this.linkExtend(this.view!.state.selection.$anchor, href);
+ this.view!.dispatch(this.view!.state.tr.removeMark(extension.from, extension.to, this.view!.state.schema.marks.link));
+ }
+ }
+ }
+ }
+
+ linkExtend($start: ResolvedPos, href: string) {
+ const mark = this.view!.state.schema.marks.link;
+
+ let startIndex = $start.index();
+ let endIndex = $start.indexAfter();
+
+ while (startIndex > 0 && $start.parent.child(startIndex - 1).marks.filter(m => m.type === mark && m.attrs.href === href).length) startIndex--;
+ while (endIndex < $start.parent.childCount && $start.parent.child(endIndex).marks.filter(m => m.type === mark && m.attrs.href === href).length) endIndex++;
+
+ let startPos = $start.start();
+ let endPos = startPos;
+ for (let i = 0; i < endIndex; i++) {
+ const size = $start.parent.child(i).nodeSize;
+ if (i < startIndex) startPos += size;
+ endPos += size;
+ }
+ return { from: startPos, to: endPos };
+ }
+
+ reference_node(pos: ResolvedPos<any>): ProsNode | null {
+ if (!this.view) return null;
+
+ let ref_node: ProsNode = this.view.state.doc;
+ if (pos.nodeBefore !== null && pos.nodeBefore !== undefined) {
+ ref_node = pos.nodeBefore;
+ }
+ else if (pos.nodeAfter !== null && pos.nodeAfter !== undefined) {
+ ref_node = pos.nodeAfter;
+ }
+ else if (pos.pos > 0) {
+ let skip = false;
+ for (let i: number = pos.pos - 1; i > 0; i--) {
+ this.view.state.doc.nodesBetween(i, pos.pos, (node: ProsNode) => {
+ if (node.isLeaf && !skip) {
+ ref_node = node;
+ skip = true;
+ }
+
+ });
+ }
+ }
+ if (!ref_node.isLeaf && ref_node.childCount > 0) {
+ ref_node = ref_node.child(0);
+ }
+ return ref_node;
+ }
+
+ @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 row1 = <div className="antimodeMenu-row">{[
+ this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)),
+ this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)),
+ this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)),
+ this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)),
+ this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)),
+ this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)),
+ this.createColorButton(),
+ this.createHighlighterButton(),
+ this.createLinkButton(),
+ this.createBrushButton(),
+ this.createButton("indent", "Summarize", undefined, this.insertSummarizer),
+ ]}</div>;
+
+ const row2 = <div className="antimodeMenu-row row-2">
+ <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>;
+
+ return (
+ <div className="richTextMenu" onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
+ {this.getElementWithRows([row1, row2], 2, false)}
+ </div>
+ );
+ }
+}
+
+interface ButtonDropdownProps {
+ 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 ?
+ <button className="antimodeMenu-button dropdown-button-combined" onPointerDown={this.onDropdownClick}>
+ {this.props.button}
+ <FontAwesomeIcon icon="caret-down" size="sm" />
+ </button> :
+ <>
+ {this.props.button}
+ <button className="dropdown-button antimodeMenu-button" onPointerDown={this.onDropdownClick}>
+ <FontAwesomeIcon icon="caret-down" size="sm" />
+ </button>
+ </>}
+
+ {this.showDropdown ? this.props.dropdownContent : <></>}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 4612f10f4..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 {
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index e56930624..1c15dca7f 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -93,7 +93,7 @@ export class TooltipTextMenu {
{ mark: schema.marks.subscript, dom: svgIcon("subscript", "Subscript", "M496 448h-16V304a16 16 0 0 0-16-16h-48a16 16 0 0 0-14.29 8.83l-16 32A16 16 0 0 0 400 352h16v96h-16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h96a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM336 64h-67a16 16 0 0 0-13.14 6.87l-79.9 115-79.9-115A16 16 0 0 0 83 64H16A16 16 0 0 0 0 80v48a16 16 0 0 0 16 16h33.48l77.81 112-77.81 112H16a16 16 0 0 0-16 16v48a16 16 0 0 0 16 16h67a16 16 0 0 0 13.14-6.87l79.9-115 79.9 115A16 16 0 0 0 269 448h67a16 16 0 0 0 16-16v-48a16 16 0 0 0-16-16h-33.48l-77.81-112 77.81-112H336a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16z") },
];
- basicItems.map(({ dom, mark }) => this.basicTools?.appendChild(dom.cloneNode(true)));
+ basicItems.map(({ dom, mark }) => this.basicTools ?.appendChild(dom.cloneNode(true)));
basicItems.concat(items).forEach(({ dom, mark }) => {
this.tooltip.appendChild(dom);
this._marksToDoms.set(mark, dom);
@@ -474,7 +474,7 @@ export class TooltipTextMenu {
const node = self.view.state.selection.$from.nodeAfter;
const link = node && node.marks.find(m => m.type === self.view.state.schema.marks.link);
const href = link!.attrs.href;
- if (href?.indexOf(Utils.prepend("/doc/")) === 0) {
+ if (href ?.indexOf(Utils.prepend("/doc/")) === 0) {
const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
linkclicked && DocServer.GetRefField(linkclicked).then(async linkDoc => {
if (linkDoc instanceof Doc) {
@@ -500,7 +500,7 @@ export class TooltipTextMenu {
const link = this.view.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + linkDocId), title: title, location: location, targetId: targetDocId });
this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link).
addMark(this.view.state.selection.from, this.view.state.selection.to, link));
- return this.view.state.selection.$from.nodeAfter?.text || "";
+ return this.view.state.selection.$from.nodeAfter ?.text || "";
}
// SUMMARIZER TOOL
@@ -510,7 +510,7 @@ export class TooltipTextMenu {
const tr = state.tr.addMark(state.selection.from, state.selection.to, mark);
const content = tr.selection.content();
const newNode = state.schema.nodes.summary.create({ visibility: false, text: content, textslice: content.toJSON() });
- dispatch?.(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark));
+ dispatch ?.(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark));
}
}
@@ -737,7 +737,7 @@ export class TooltipTextMenu {
// get marks in the selection
const selected_marks = new Set<Mark>();
const { from, to } = state.selection as TextSelection;
- state.doc.nodesBetween(from, to, (node) => node.marks?.forEach(m => selected_marks.add(m)));
+ state.doc.nodesBetween(from, to, (node) => node.marks ?.forEach(m => selected_marks.add(m)));
if (this._brushdom && selected_marks.size >= 0) {
TooltipTextMenuManager.Instance._brushMarks = selected_marks;
@@ -849,7 +849,7 @@ export class TooltipTextMenu {
static setMark = (mark: Mark, state: EditorState<any>, dispatch: any) => {
if (mark) {
const node = (state.selection as NodeSelection).node;
- if (node?.type === schema.nodes.ordered_list) {
+ if (node ?.type === schema.nodes.ordered_list) {
let attrs = node.attrs;
if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, setFontFamily: mark.attrs.family };
if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, setFontSize: mark.attrs.fontSize };
diff --git a/src/client/views/AntimodeMenu.scss b/src/client/views/AntimodeMenu.scss
index f3da5f284..d4a76ee17 100644
--- a/src/client/views/AntimodeMenu.scss
+++ b/src/client/views/AntimodeMenu.scss
@@ -5,13 +5,26 @@
background: #323232;
box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
border-radius: 0px 6px 6px 6px;
- overflow: hidden;
+ // overflow: hidden;
display: flex;
+ &.with-rows {
+ flex-direction: column
+ }
+
+ .antimodeMenu-row {
+ display: flex;
+ height: 35px;
+ }
+
.antimodeMenu-button {
background-color: transparent;
width: 35px;
height: 35px;
+
+ &.active {
+ background-color: #121212;
+ }
}
.antimodeMenu-button:hover {
diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx
index 408df8bc2..4625eb92f 100644
--- a/src/client/views/AntimodeMenu.tsx
+++ b/src/client/views/AntimodeMenu.tsx
@@ -18,9 +18,13 @@ export default abstract class AntimodeMenu extends React.Component {
@observable protected _opacity: number = 1;
@observable protected _transition: string = "opacity 0.5s";
@observable protected _transitionDelay: string = "";
+ @observable protected _canFade: boolean = true;
@observable public Pinned: boolean = false;
+ 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
@@ -62,7 +66,7 @@ export default abstract class AntimodeMenu extends React.Component {
@action
protected pointerLeave = (e: React.PointerEvent) => {
- if (!this.Pinned) {
+ if (!this.Pinned && this._canFade) {
this._transition = "opacity 0.5s";
this._transitionDelay = "1s";
this._opacity = 0.2;
@@ -88,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();
@@ -97,8 +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;
+ 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();
@@ -116,6 +126,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}
@@ -125,4 +139,14 @@ export default abstract class AntimodeMenu extends React.Component {
</div>
);
}
+
+ protected getElementWithRows(rows: JSX.Element[], numRows: number, hasDragger: boolean = true) {
+ return (
+ <div className="antimodeMenu-cont with-rows" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu}
+ style={{ left: this._left, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay, height: 35 * numRows + "px" }}>
+ {rows}
+ {hasDragger ? <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/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 4bc24fa93..799b3695c 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -26,6 +26,8 @@ import { IconBox } from "./nodes/IconBox";
import React = require("react");
import { DocumentType } from '../documents/DocumentTypes';
import { ScriptField } from '../../new_fields/ScriptField';
+import { render } from 'react-dom';
+import RichTextMenu from '../util/RichTextMenu';
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -591,6 +593,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
}}>
{minimizeIcon}
+ {/* <RichTextMenu /> */}
+
{this._edtingTitle ?
<input ref={this._keyinput} className="title" type="text" name="dynbox" value={this._accumulatedTitle} onBlur={e => this.titleBlur(true)} onChange={this.titleChanged} onKeyPress={this.titleEntered} /> :
<div className="title" onPointerDown={this.onTitleDown} ><span>{`${this.selectionTitle}`}</span></div>}
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index a1196ee1c..305966160 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -40,6 +40,7 @@ import InkSelectDecorations from './InkSelectDecorations';
import { Scripting } from '../util/Scripting';
import { AudioBox } from './nodes/AudioBox';
import { TraceMobx } from '../../new_fields/util';
+import RichTextMenu from '../util/RichTextMenu';
@observer
export class MainView extends React.Component {
@@ -516,6 +517,7 @@ export class MainView extends React.Component {
<ContextMenu />
<PDFMenu />
<MarqueeOptionsMenu />
+ <RichTextMenu />
<OverlayView />
</div >);
}
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 7555a594b..8e28cf928 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -47,6 +47,8 @@ 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';
+import { DocumentDecorations } from '../DocumentDecorations';
library.add(faEdit);
library.add(faSmile, faTextHeight, faUpload);
@@ -904,11 +906,18 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
this.tryUpdateHeight();
// see if we need to preserve the insertion point
- const prosediv = this.ProseRef?.children?.[0] as any;
- const keeplocation = prosediv?.keeplocation;
+ const prosediv = this.ProseRef ?.children ?.[0] as any;
+ const keeplocation = prosediv ?.keeplocation;
prosediv && (prosediv.keeplocation = undefined);
- const pos = this._editorView?.state.selection.$from.pos || 1;
- keeplocation && setTimeout(() => this._editorView?.dispatch(this._editorView?.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos))));
+ const pos = this._editorView ?.state.selection.$from.pos || 1;
+ keeplocation && setTimeout(() => this._editorView ?.dispatch(this._editorView ?.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos))));
+
+ // jump rich text menu to this textbox
+ if (this._ref.current) {
+ const x = Math.min(Math.max(this._ref.current!.getBoundingClientRect().left, 0), window.innerWidth - RichTextMenu.Instance.width);
+ const y = this._ref.current!.getBoundingClientRect().top - RichTextMenu.Instance.height - 50;
+ 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
@@ -924,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();
}
@@ -987,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;
}
@@ -1032,7 +1041,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;
}
});
}
@@ -1052,6 +1063,9 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
this._undoTyping = undefined;
}
this.doLinkOnDeselect();
+
+ // move the richtextmenu offscreen
+ if (!RichTextMenu.Instance.Pinned && !RichTextMenu.Instance.overMenu) RichTextMenu.Instance.jumpTo(-300, -300);
}
_lastTimedMark: Mark | undefined = undefined;
@@ -1073,7 +1087,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();
@@ -1095,7 +1109,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) {
@@ -1121,7 +1135,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();
}
@@ -1155,7 +1171,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
{this.props.Document.hideSidebar ? (null) : this.sidebarWidthPercent === "0%" ?
<div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} onClick={e => this.toggleSidebar()} /> :
<div className={"formattedTextBox-sidebar" + (InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : "")}
- style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${StrCast(this.extensionDoc?.backgroundColor, "transparent")}` }}>
+ style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${StrCast(this.extensionDoc ?.backgroundColor, "transparent")}` }}>
<CollectionFreeFormView {...this.props}
PanelHeight={this.props.PanelHeight}
PanelWidth={() => this.sidebarWidth}
diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx
index 503696ae9..05c70b74a 100644
--- a/src/client/views/pdf/PDFMenu.tsx
+++ b/src/client/views/pdf/PDFMenu.tsx
@@ -98,7 +98,7 @@ export default class PDFMenu extends AntimodeMenu {
}
render() {
- const buttons = this.Status === "pdf" ?
+ const buttons = this.Status === "pdf" ?
[
<button key="1" className="antimodeMenu-button" title="Click to Highlight" onClick={this.highlightClicked} style={this.Highlighting ? { backgroundColor: "#121212" } : {}}>
<FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} /></button>,