import React = require('react'); import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { lift, wrapIn } from 'prosemirror-commands'; import { Mark, MarkType, Node as ProsNode, ResolvedPos } from 'prosemirror-model'; import { wrapInList } from 'prosemirror-schema-list'; import { EditorState, NodeSelection, TextSelection } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import { Doc } from '../../../../fields/Doc'; import { Cast, StrCast } from '../../../../fields/Types'; import { DocServer } from '../../../DocServer'; import { LinkManager } from '../../../util/LinkManager'; import { SelectionManager } from '../../../util/SelectionManager'; import { undoBatch, UndoManager } from '../../../util/UndoManager'; import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; import { FieldViewProps } from '../FieldView'; import { FormattedTextBox } from './FormattedTextBox'; import { updateBullets } from './ProsemirrorExampleTransfer'; import './RichTextMenu.scss'; import { schema } from './schema_rts'; import { EquationBox } from '../EquationBox'; const { toggleMark } = require('prosemirror-commands'); @observer export class RichTextMenu extends AntimodeMenu { @observable static Instance: RichTextMenu; public overMenu: boolean = false; // kind of hacky way to prevent selects not being selectable private _linkToRef = React.createRef(); @observable public view?: EditorView; public editorProps: FieldViewProps | undefined; public _brushMap: Map> = new Map(); @observable private collapsed: boolean = false; @observable private _noLinkActive: boolean = false; @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 = '13px'; @observable private _activeFontFamily: string = ''; @observable private activeListType: string = ''; @observable private _activeAlignment: string = 'left'; @observable private brushMarks: Set = 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; _reaction: IReactionDisposer | undefined; constructor(props: Readonly<{}>) { super(props); runInAction(() => { RichTextMenu.Instance = this; this.updateMenu(undefined, undefined, props); this._canFade = false; this.Pinned = true; }); } @computed get noAutoLink() { return this._noLinkActive; } @computed get bold() { return this._boldActive; } @computed get underline() { return this._underlineActive; } @computed get italics() { return this._italicsActive; } @computed get strikeThrough() { return this._strikethroughActive; } @computed get fontColor() { return this._activeFontColor; } @computed get fontFamily() { return this._activeFontFamily; } @computed get fontSize() { return this._activeFontSize; } @computed get textAlign() { return this._activeAlignment; } _disposer: IReactionDisposer | undefined; componentDidMount() { this._disposer = reaction( () => SelectionManager.Views(), views => this.updateMenu(undefined, undefined, undefined) ); } componentWillUnmount() { this._disposer?.(); } @action public updateMenu(view: EditorView | undefined, lastState: EditorState | undefined, props: any) { if (this._linkToRef.current?.getBoundingClientRect().width) { return; } this.view = view; props && (this.editorProps = props); // Don't do anything if the document/selection didn't change if (view && view.hasFocus()) { if (lastState?.doc.eq(view.state.doc) && lastState.selection.eq(view.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.activeFamilies; const activeSizes = active.activeSizes; const activeColors = active.activeColors; const activeHighlights = active.activeHighlights; this.activeListType = this.getActiveListStyle(); this._activeAlignment = this.getActiveAlignment(); this._activeFontFamily = !activeFamilies.length ? StrCast(this.TextView?.Document.fontFamily, StrCast(Doc.UserDoc().fontFamily, 'Arial')) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various'; this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, StrCast(Doc.UserDoc().fontSize, '10px')) : activeSizes[0]; this._activeFontColor = !activeColors.length ? StrCast(this.TextView?.Document.fontColor, StrCast(Doc.UserDoc().fontColor, 'black')) : activeColors.length > 0 ? String(activeColors[0]) : '...'; this.activeHighlightColor = !activeHighlights.length ? '' : activeHighlights.length > 0 ? String(activeHighlights[0]) : '...'; // update link in current selection this.getTextLinkTargetTitle().then(targetTitle => this.setCurrentLink(targetTitle)); } setMark = (mark: Mark, state: EditorState, dispatch: any, dontToggle: boolean = false) => { 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, fontFamily: mark.attrs.family }; if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, fontSize: mark.attrs.fontSize }; if (mark.type === schema.marks.pFontColor) attrs = { ...attrs, fontColor: 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 if (dontToggle) { const tr = state.tr.addMark(state.selection.from, state.selection.to, mark); dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(state.selection.from), tr.doc.resolve(state.selection.to)))); // bcz: need to redo the selection because ctrl-a selections disappear otherwise } else { toggleMark(mark.type, mark.attrs)(state, dispatch); } } }; // finds font sizes and families in selection getActiveAlignment() { if (this.view && this.TextView?.props.isSelected(true)) { const path = (this.view.state.selection.$from as any).path; for (let i = path.length - 3; i < path.length && i >= 0; i -= 3) { if (path[i]?.type === this.view.state.schema.nodes.paragraph || path[i]?.type === this.view.state.schema.nodes.heading) { return path[i].attrs.align || 'left'; } } } return 'left'; } // finds font sizes and families in selection getActiveListStyle() { if (this.view && this.TextView?.props.isSelected(true)) { const path = (this.view.state.selection.$from as any).path; for (let i = 0; i < path.length; i += 3) { if (path[i].type === this.view.state.schema.nodes.ordered_list) { return path[i].attrs.mapStyle; } } if (this.view.state.selection.$from.nodeAfter?.type === this.view.state.schema.nodes.ordered_list) { return this.view.state.selection.$from.nodeAfter?.attrs.mapStyle; } } return ''; } // finds font sizes and families in selection getActiveFontStylesOnSelection() { const activeFamilies = new Set(); const activeSizes = new Set(); const activeColors = new Set(); const activeHighlights = new Set(); if (this.view && this.TextView?.props.isSelected(true)) { const state = this.view.state; const pos = this.view.state.selection.$from; const marks: Mark[] = [...(state.storedMarks ?? [])]; if (state.storedMarks !== null) { } else if (state.selection.empty) { const ref_node = this.reference_node(pos); marks.push(...(ref_node !== this.view.state.doc && ref_node?.isText ? Array.from(ref_node.marks) : [])); } else { state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { node.marks?.filter(mark => !mark.isInSet(marks)).map(mark => marks.push(mark)); }); } marks.forEach(m => { m.type === state.schema.marks.pFontFamily && activeFamilies.add(m.attrs.family); m.type === state.schema.marks.pFontColor && activeColors.add(m.attrs.color); m.type === state.schema.marks.pFontSize && activeSizes.add(m.attrs.fontSize); m.type === state.schema.marks.marker && activeHighlights.add(String(m.attrs.highlight)); }); } else if (SelectionManager.Views().some(dv => dv.ComponentView instanceof EquationBox)) { SelectionManager.Views().forEach(dv => StrCast(dv.rootDoc._fontSize) && activeSizes.add(StrCast(dv.rootDoc._fontSize))); } return { activeFamilies: Array.from(activeFamilies), activeSizes: Array.from(activeSizes), activeColors: Array.from(activeColors), activeHighlights: Array.from(activeHighlights) }; } getMarksInSelection(state: EditorState) { const found = new Set(); 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() { let activeMarks: MarkType[] = []; if (!this.view || !this.TextView?.props.isSelected(true)) return activeMarks; const markGroup = [schema.marks.noAutoLinkAnchor, 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; 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 mark.isINSet // ref_node.marks.some(m => m.type.name === state.schema.marks.pFontSize.name); // } const mark = state.schema.mark(mark_type); return mark.isInSet(ref_node.marks); }); } } return activeMarks; } destroy() { !this.TextView?.props.isSelected(true) && this.fadeOut(true); } @action setActiveMarkButtons(activeMarks: MarkType[] | undefined) { if (!activeMarks) return; this._noLinkActive = false; this._boldActive = false; this._italicsActive = false; this._underlineActive = false; this._strikethroughActive = false; this._subscriptActive = false; this._superscriptActive = false; activeMarks.forEach(mark => { // prettier-ignore switch (mark.name) { case 'noAutoLinkAnchor': this._noLinkActive = true; break; 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; } }); } toggleNoAutoLinkAnchor = () => { if (this.view) { const mark = this.view.state.schema.mark(this.view.state.schema.marks.noAutoLinkAnchor); this.setMark(mark, this.view.state, this.view.dispatch, false); this.TextView.autoLink(); this.view.focus(); } }; toggleBold = () => { if (this.view) { const mark = this.view.state.schema.mark(this.view.state.schema.marks.strong); this.setMark(mark, this.view.state, this.view.dispatch, false); this.view.focus(); } }; toggleUnderline = () => { if (this.view) { const mark = this.view.state.schema.mark(this.view.state.schema.marks.underline); this.setMark(mark, this.view.state, this.view.dispatch, false); this.view.focus(); } }; toggleItalics = () => { if (this.view) { const mark = this.view.state.schema.mark(this.view.state.schema.marks.em); this.setMark(mark, this.view.state, this.view.dispatch, false); this.view.focus(); } }; setFontSize = (fontSize: string) => { if (this.view) { if (this.view.state.selection.from === 1 && this.view.state.selection.empty && (!this.view.state.doc.nodeAt(1) || !this.view.state.doc.nodeAt(1)?.marks.some(m => m.type.name === fontSize))) { this.TextView.dataDoc.fontSize = fontSize; this.view.focus(); } else { const fmark = this.view.state.schema.marks.pFontSize.create({ fontSize }); this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true); this.view.focus(); } } else if (SelectionManager.Views().some(dv => dv.ComponentView instanceof EquationBox)) { SelectionManager.Views().forEach(dv => (dv.rootDoc._fontSize = fontSize)); } else Doc.UserDoc()._fontSize = fontSize; this.updateMenu(this.view, undefined, this.props); }; setFontFamily = (family: string) => { if (this.view) { const fmark = this.view.state.schema.marks.pFontFamily.create({ family: family }); this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true); this.view.focus(); } else Doc.UserDoc()._fontFamily = family; this.updateMenu(this.view, undefined, this.props); }; setHighlight(color: String, view: EditorView, dispatch: any) { const highlightMark = view.state.schema.mark(view.state.schema.marks.marker, { highlight: color }); if (view.state.selection.empty) return false; view.focus(); this.setMark(highlightMark, view.state, dispatch, false); } setColor(color: string) { if (this.view) { const colorMark = this.view.state.schema.mark(this.view.state.schema.marks.pFontColor, { color }); this.setMark(colorMark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(colorMark)), true); this.view.focus(); } else Doc.UserDoc().fontColor = color; this.updateMenu(this.view, undefined, this.props); } // TODO: remove doesn't work // remove all node type and apply the passed-in one to the selected text changeListType = (mapStyle: string) => { const active = this.view?.state && RichTextMenu.Instance.getActiveListStyle(); const nodeType = this.view?.state.schema.nodes.ordered_list.create({ mapStyle: active === mapStyle ? '' : mapStyle }); if (!this.view || nodeType?.attrs.mapStyle === '') return; const nextIsOL = this.view.state.selection.$from.nodeAfter?.type === schema.nodes.ordered_list; let inList: any = undefined; let fromList = -1; const path: any = Array.from((this.view.state.selection.$from as any).path); for (let i = 0; i < path.length; i++) { if (path[i]?.type === schema.nodes.ordered_list) { inList = path[i]; fromList = path[i - 1]; } } const marks = this.view.state.storedMarks || (this.view.state.selection.$to.parentOffset && this.view.state.selection.$from.marks()); if ( inList || !wrapInList(schema.nodes.ordered_list)(this.view.state, (tx2: any) => { const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle, this.view!.state.selection.from - 1, this.view!.state.selection.to + 1); marks && tx3.ensureMarks([...marks]); marks && tx3.setStoredMarks([...marks]); this.view!.dispatch(tx2); }) ) { const tx2 = this.view.state.tr; if (nodeType && (inList || nextIsOL)) { const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle, inList ? fromList : this.view.state.selection.from, inList ? fromList + inList.nodeSize : this.view.state.selection.to); marks && tx3.ensureMarks([...marks]); marks && tx3.setStoredMarks([...marks]); this.view.dispatch(tx3); } } this.view.focus(); this.updateMenu(this.view, undefined, this.props); }; insertSummarizer(state: EditorState, 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?.(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark)); return true; } align = (view: EditorView, dispatch: any, alignment: 'left' | 'right' | 'center') => { if (this.TextView?.props.isSelected(true)) { var tr = view.state.tr; view.state.doc.nodesBetween(view.state.selection.from, view.state.selection.to, (node, pos, parent, index) => { if ([schema.nodes.paragraph, schema.nodes.heading].includes(node.type)) { tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, align: alignment }, node.marks); return false; } view.focus(); return true; }); view.focus(); dispatch?.(tr); } }; insetParagraph(state: EditorState, dispatch: any) { var tr = state.tr; state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) { const inset = (node.attrs.inset ? Number(node.attrs.inset) : 0) + 10; tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, inset }, node.marks); return false; } return true; }); dispatch?.(tr); return true; } outsetParagraph(state: EditorState, dispatch: any) { var tr = state.tr; state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) { const inset = Math.max(0, (node.attrs.inset ? Number(node.attrs.inset) : 0) - 10); tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, inset }, node.marks); return false; } return true; }); dispatch?.(tr); return true; } indentParagraph(state: EditorState, dispatch: any) { var tr = state.tr; const heading = false; state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) { const nodeval = node.attrs.indent ? Number(node.attrs.indent) : undefined; const indent = !nodeval ? 25 : nodeval < 0 ? 0 : nodeval + 25; tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, indent }, node.marks); return false; } return true; }); !heading && dispatch?.(tr); return true; } hangingIndentParagraph(state: EditorState, dispatch: any) { var tr = state.tr; state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) { const nodeval = node.attrs.indent ? Number(node.attrs.indent) : undefined; const indent = !nodeval ? -25 : nodeval > 0 ? 0 : nodeval - 10; tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, indent }, node.marks); return false; } return true; }); dispatch?.(tr); return true; } insertBlockquote(state: EditorState, dispatch: any) { const path = (state.selection.$from as any).path; if (path.length > 6 && path[path.length - 6].type === schema.nodes.blockquote) { lift(state, dispatch); } else { wrapIn(schema.nodes.blockquote)(state, dispatch); } return true; } insertHorizontalRule(state: EditorState, dispatch: any) { dispatch(state.tr.replaceSelectionWith(state.schema.nodes.horizontal_rule.create()).scrollIntoView()); return true; } @action toggleBrushDropdown() { this.showBrushDropdown = !this.showBrushDropdown; } // todo: add brushes to brushMap to save with a style name onBrushNameKeyPress = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { RichTextMenu.Instance.brushMarks && RichTextMenu.Instance._brushMap.set(this._brushNameRef.current!.value, RichTextMenu.Instance.brushMarks); this._brushNameRef.current!.style.background = 'lightGray'; } }; _brushNameRef = React.createRef(); @action clearBrush() { RichTextMenu.Instance.brushMarks = new Set(); } @action fillBrush(state: EditorState, dispatch: any) { if (!this.view) return; if (!Array.from(this.brushMarks.keys()).length) { const selected_marks = this.getMarksInSelection(this.view.state); if (selected_marks.size >= 0) { this.brushMarks = selected_marks; } } else { const { from, to, $from } = this.view.state.selection; if (!this.view.state.selection.empty && $from && $from.nodeAfter) { if (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); }); } } } } get TextView() { return (this.view as any)?.TextView as FormattedTextBox; } get TextViewFieldKey() { return this.TextView?.props.fieldKey; } @action setActiveHighlight(color: string) { this.activeHighlightColor = color; } @action setCurrentLink(link: string) { this.currentLink = link; } createLinkButton() { const self = this; function onLinkChange(e: React.ChangeEvent) { self.TextView?.endUndoTypingBatch(); UndoManager.RunInBatch(() => self.setCurrentLink(e.target.value), 'link change'); } const link = this.currentLink ? this.currentLink : ''; const button = ( set hyperlink} placement="bottom"> ); const dropdownContent = (

Linked to:

); return ; } 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.allAnchors.length > 0 ? link.attrs.allAnchors[0].href : undefined; if (href) { if (href.indexOf(Doc.localServerPath()) === 0) { const linkclicked = href.replace(Doc.localServerPath(), '').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.Docs().lastElement(); 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 @undoBatch makeLinkToURL = (target: string, lcoation: string) => { ((this.view as any)?.TextView as FormattedTextBox).makeLinkAnchor(undefined, 'onRadd:rightight', target, target); }; @undoBatch @action deleteLink = () => { if (this.view) { const linkAnchor = this.view.state.selection.$from.nodeAfter?.marks.find(m => m.type === this.view!.state.schema.marks.linkAnchor); if (linkAnchor) { const allAnchors = linkAnchor.attrs.allAnchors.slice(); this.TextView.RemoveAnchorFromSelection(allAnchors); // bcz: Argh ... this will remove the link from the document even it's anchored somewhere else in the text which happens if only part of the anchor text was selected. allAnchors .filter((aref: any) => aref?.href.indexOf(Doc.localServerPath()) === 0) .forEach((aref: any) => { const anchorId = aref.href.replace(Doc.localServerPath(), '').split('?')[0]; anchorId && DocServer.GetRefField(anchorId).then(linkDoc => LinkManager.Instance.deleteLink(linkDoc as Doc)); }); } } }; linkExtend($start: ResolvedPos, href: string) { const mark = this.view!.state.schema.marks.linkAnchor; let startIndex = $start.index(); let endIndex = $start.indexAfter(); while (startIndex > 0 && $start.parent.child(startIndex - 1).marks.filter(m => m.type === mark && m.attrs.allAnchors.find((item: { href: string }) => item.href === href)).length) startIndex--; while (endIndex < $start.parent.childCount && $start.parent.child(endIndex).marks.filter(m => m.type === mark && m.attrs.allAnchors.find((item: { href: string }) => item.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): 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; } if (pos.nodeAfter !== null && pos.nodeAfter !== undefined) { if (!pos.nodeBefore || this.view.state.selection.$from.pos !== this.view.state.selection.$to.pos) { ref_node = pos.nodeAfter; } } if (!ref_node && 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() { return null; // TraceMobx(); // const row1 =
{[ // //!this.collapsed ? this.getDragger() : (null), // // !this.Pinned ? (null) :
{[ // // 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.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("align-left", "Align Left", this.activeAlignment === "left", this.alignLeft), // this.createButton("align-center", "Align Center", this.activeAlignment === "center", this.alignCenter), // this.createButton("align-right", "Align Right", this.activeAlignment === "right", this.alignRight), // this.createButton("indent", "Inset More", undefined, this.insetParagraph), // this.createButton("outdent", "Inset Less", undefined, this.outsetParagraph), // this.createButton("hand-point-left", "Hanging Indent", undefined, this.hangingIndentParagraph), // this.createButton("hand-point-right", "Indent", undefined, this.indentParagraph), // ]}
; // const row2 =
// {this.collapsed ? this.getDragger() : (null)} //
//
// {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size", action((val: string) => { // this.activeFontSize = val; // SelectionManager.Views().map(dv => dv.props.Document._fontSize = val); // })), // this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family", action((val: string) => { // this.activeFontFamily = val; // SelectionManager.Views().map(dv => dv.props.Document._fontFamily = val); // })), //
, // this.createNodesDropdown(this.activeListType, this.listTypeOptions, "list type", () => ({})), // this.createButton("sort-amount-down", "Summarize", undefined, this.insertSummarizer), // this.createButton("quote-left", "Blockquote", undefined, this.insertBlockquote), // this.createButton("minus", "Horizontal Rule", undefined, this.insertHorizontalRule) // ]} //
// {/*
// {
// //
} // //
*/} //
; } } interface ButtonDropdownProps { view?: EditorView; button: JSX.Element; dropdownContent: JSX.Element; openDropdownOnButton?: boolean; link?: boolean; pdf?: boolean; } @observer export class ButtonDropdown extends React.Component { @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.toggleDropdown(); }; onBlur = (e: PointerEvent) => { setTimeout(() => { if (this.ref !== null && !this.ref.contains(e.target as Node)) { this.setShowDropdown(false); } }, 0); }; render() { return (
(this.ref = node)}> {!this.props.pdf ? (
{this.props.button}
) : ( <> {this.props.button} )} {this.showDropdown ? this.props.dropdownContent : null}
); } } interface RichTextMenuPluginProps { editorProps: any; } export class RichTextMenuPlugin extends React.Component { render() { return null; } update(view: EditorView, lastState: EditorState | undefined) { RichTextMenu.Instance?.updateMenu(view, lastState, this.props.editorProps); } }