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