aboutsummaryrefslogtreecommitdiff
path: root/src/client/util/RichTextMenu.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/util/RichTextMenu.tsx')
-rw-r--r--src/client/util/RichTextMenu.tsx337
1 files changed, 337 insertions, 0 deletions
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