diff options
Diffstat (limited to 'src/client/views/nodes/formattedText/RichTextMenu.tsx')
-rw-r--r-- | src/client/views/nodes/formattedText/RichTextMenu.tsx | 290 |
1 files changed, 77 insertions, 213 deletions
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index bee0d72e3..cecf106a3 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -27,7 +27,10 @@ const { toggleMark } = require('prosemirror-commands'); @observer export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { - @observable static Instance: RichTextMenu; + static _instance: { menu: RichTextMenu | undefined } = observable({ menu: undefined }); + static get Instance() { + return RichTextMenu._instance?.menu; + } public overMenu: boolean = false; // kind of hacky way to prevent selects not being selectable private _linkToRef = React.createRef<HTMLInputElement>(); @@ -48,7 +51,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { @observable private _activeFontSize: string = '13px'; @observable private _activeFontFamily: string = ''; - @observable private activeListType: string = ''; + @observable private _activeListType: string = ''; @observable private _activeAlignment: string = 'left'; @observable private brushMarks: Set<Mark> = new Set(); @@ -67,7 +70,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { constructor(props: AntimodeMenuProps) { super(props); makeObservable(this); - RichTextMenu.Instance = this; + RichTextMenu._instance.menu = this; this.updateMenu(undefined, undefined, props, this.layoutDoc); this._canFade = false; this.Pinned = true; @@ -100,6 +103,9 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { @computed get fontSize() { return this._activeFontSize; } + @computed get listStyle() { + return this._activeListType; + } @computed get textAlign() { return this._activeAlignment; } @@ -131,11 +137,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { 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 + this.setActiveMarkButtons(this.getActiveMarksOnSelection()); const active = this.getActiveFontStylesOnSelection(); const activeFamilies = active.activeFamilies; const activeSizes = active.activeSizes; @@ -144,7 +146,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const refDoc = SelectionManager.Views.lastElement()?.layoutDoc ?? Doc.UserDoc(); const refField = (pfx => (pfx ? pfx + '_' : ''))(SelectionManager.Views.lastElement()?.LayoutFieldKey); - this.activeListType = this.getActiveListStyle(); + this._activeListType = this.getActiveListStyle(); this._activeAlignment = this.getActiveAlignment(); this._activeFontFamily = !activeFamilies.length ? StrCast(this.TextView?.Document._text_fontFamily, StrCast(refDoc[refField + 'fontFamily'], 'Arial')) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various'; this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, StrCast(refDoc[refField + 'fontSize'], '10px')) : activeSizes[0]; @@ -161,17 +163,16 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const liTo = numberRange(state.selection.$to.depth + 1).find(i => state.selection.$to.node(i)?.type === state.schema.nodes.list_item); const olFirst = numberRange(state.selection.$from.depth + 1).find(i => state.selection.$from.node(i)?.type === state.schema.nodes.ordered_list); const nodeOl = (liFirst && liTo && state.selection.$from.node(liFirst) !== state.selection.$to.node(liTo) && olFirst) || (!liFirst && !liTo && olFirst); - const newPos = nodeOl ? numberRange(state.selection.from).findIndex(i => state.doc.nodeAt(i)?.type === state.schema.nodes.ordered_list) : state.selection.from; + const fromRange = numberRange(state.selection.from).reverse(); + const newPos = nodeOl ? fromRange.find(i => state.doc.nodeAt(i)?.type === state.schema.nodes.ordered_list) ?? state.selection.from : state.selection.from; const node = (state.selection as NodeSelection).node ?? (newPos >= 0 ? state.doc.nodeAt(newPos) : undefined); - 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(newPos, node.type, attrs), state.schema); - dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(state.selection.from), tr.doc.resolve(state.selection.to)))); - } - { + if (node?.type === schema.nodes.ordered_list || node?.type === schema.nodes.list_item) { + const hasMark = node.marks.some(m => m.type === mark.type); + const otherMarks = node.marks.filter(m => m.type !== mark.type); + const addAnyway = node.marks.filter(m => m.type === mark.type && Object.keys(m.attrs).some(akey => m.attrs[akey] !== mark.attrs[akey])); + const markup = state.tr.setNodeMarkup(newPos, node.type, node.attrs, hasMark && !addAnyway ? otherMarks : [...otherMarks, mark]); + dispatch(updateBullets(markup, state.schema)); + } else { const state = this.view?.state; const tr = this.view?.state.tr; if (tr && state) { @@ -201,16 +202,15 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { // finds font sizes and families in selection getActiveListStyle() { - if (this.view && this.TextView?._props.rootSelected?.()) { - 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; + const state = this.view?.state; + if (state) { + const pos = state.selection.$anchor; + for (let i = 0; i < pos.depth; i++) { + const node = pos.node(i); + if (node.type === schema.nodes.ordered_list) { + return node.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 ''; } @@ -224,11 +224,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { if (this.view && this.TextView?._props.rootSelected?.()) { const state = this.view.state; const pos = this.view.state.selection.$from; - const marks: Mark[] = [...(state.storedMarks ?? [])]; + var 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) : [])); + for (let i = 0; i <= pos.depth; i++) { + marks = [...Array.from(pos.node(i).marks), ...this.view.state.selection.$anchor.marks(), ...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)); @@ -255,41 +256,26 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { //finds all active marks on selection in given group getActiveMarksOnSelection() { - let activeMarks: MarkType[] = []; - if (!this.view || !this.TextView?._props.rootSelected?.()) return activeMarks; + if (!this.view || !this.TextView?._props.rootSelected?.()) return [] as MarkType[]; - 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); - }); + var marks: Mark[] = [...(state.storedMarks ?? [])]; + const pos = this.view.state.selection.$from; + if (state.storedMarks !== null) { + } else if (state.selection.empty) { + for (let i = 0; i <= pos.depth; i++) { + marks = [...Array.from(pos.node(i).marks), ...this.view.state.selection.$anchor.marks(), ...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)); + }); } - 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]; + return markGroup.filter(mark_type => { + const mark = state.schema.mark(mark_type); + return mark.isInSet(marks); + }); } @action @@ -318,16 +304,20 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { }); } - elideSelection = () => { - const state = this.view?.state; - if (!state) return; - if (state.selection.empty) return false; + elideSelection = (txstate: EditorState | undefined = undefined, visibility = false) => { + const state = txstate ?? this.view?.state; + if (!state || 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() }); - this.view?.dispatch?.(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark)); + const tr = state.tr.addMark(state.tr.selection.from, state.selection.to, mark); + const text = tr.selection.content(); + const elideNode = state.schema.nodes.summary.create({ visibility, text, textslice: text.toJSON() }); + const summary = tr.replaceSelectionWith(elideNode).removeMark(tr.selection.from - 1, tr.selection.from, mark); + const expanded = () => { + const endOfElidableText = summary.selection.to + text.content.size; + const res = summary.insert(summary.selection.to, text.content).insert(endOfElidableText, state.schema.nodes.paragraph.create({})); + return res.setSelection(new TextSelection(res.doc.resolve(endOfElidableText + 1))); + }; + this.view?.dispatch?.(visibility ? expanded() : summary); return true; }; @@ -366,7 +356,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { 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.TextView.dataDoc[this.TextView.fieldKey + '_fontSize'] = fontSize; this.view.focus(); } else { const fmark = this.view.state.schema.marks.pFontSize.create({ fontSize }); @@ -381,7 +371,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { setFontFamily = (family: string) => { if (this.view) { - const fmark = this.view.state.schema.marks.pFontFamily.create({ family: family }); + const fmark = this.view.state.schema.marks.pFontFamily.create({ family }); this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true); this.view.focus(); } else if (SelectionManager.Views.length) { @@ -413,40 +403,24 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { // 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 active = this.view?.state && RichTextMenu.Instance?.getActiveListStyle(); + const newMapStyle = active === mapStyle ? '' : mapStyle; + if (!this.view || newMapStyle === '') return; + let inList = this.view.state.selection.$anchor.node(1).type === schema.nodes.ordered_list; const marks = this.view.state.storedMarks || (this.view.state.selection.$to.parentOffset && this.view.state.selection.$from.marks()); - if ( - inList || + if (inList) { + const tx2 = updateBullets(this.view.state.tr, schema, newMapStyle, this.view.state.doc.resolve(this.view.state.selection.$anchor.before(1) + 1).pos, this.view.state.doc.resolve(this.view.state.selection.$anchor.after(1)).pos); + marks && tx2.ensureMarks([...marks]); + marks && tx2.setStoredMarks([...marks]); + this.view.dispatch(tx2); + } else !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); + const tx3 = updateBullets(tx2, schema, newMapStyle, this.view!.state.selection.from - 1, this.view!.state.selection.to + 1); marks && tx3.ensureMarks([...marks]); marks && tx3.setStoredMarks([...marks]); - this.view.dispatch(tx3); - } - } + this.view!.dispatch(tx3); + }); this.view.focus(); this.updateMenu(this.view, undefined, this.props, this.layoutDoc); }; @@ -561,7 +535,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { // 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); + RichTextMenu.Instance?.brushMarks && RichTextMenu.Instance?._brushMap.set(this._brushNameRef.current!.value, RichTextMenu.Instance.brushMarks); this._brushNameRef.current!.style.background = 'lightGray'; } }; @@ -569,7 +543,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { @action clearBrush() { - RichTextMenu.Instance.brushMarks = new Set(); + RichTextMenu.Instance && (RichTextMenu.Instance.brushMarks = new Set()); } @action @@ -705,118 +679,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } }; - 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 = <div className="antimodeMenu-row" key="row 1" style={{ display: this.collapsed ? "none" : undefined }}>{[ - // //!this.collapsed ? this.getDragger() : (null), - // // !this.Pinned ? (null) : <div key="frag1"> {[ - // // 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)), - // // <div className="richTextMenu-divider" key="divider" /> - // // ]}</div>, - // 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(), - // <div className="collectionMenu-divider" key="divider 2" />, - // 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), - // ]}</div>; - - // const row2 = <div className="antimodeMenu-row row-2" key="row2"> - // {this.collapsed ? this.getDragger() : (null)} - // <div key="row 2" style={{ display: this.collapsed ? "none" : undefined }}> - // <div className="collectionMenu-divider" key="divider 3" /> - // {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size", action((val: string) => { - // this.activeFontSize = val; - // SelectionManager.Views.map(dv => dv.Document._text_fontSize = val); - // })), - // this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family", action((val: string) => { - // this.activeFontFamily = val; - // SelectionManager.Views.map(dv => dv.Document._text_fontFamily = val); - // })), - // <div className="collectionMenu-divider" key="divider 4" />, - // 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) - // ]} - // </div> - // {/* <div key="collapser"> - // {<div key="collapser"> - // <button className="antimodeMenu-button" key="collapse menu" title="Collapse menu" onClick={this.toggleCollapse} style={{ backgroundColor: this.collapsed ? "#121212" : "", width: 25 }}> - // <FontAwesomeIcon icon="chevron-left" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.3s", transform: `rotate(${this.collapsed ? 180 : 0}deg)` }} /> - // </button> - // </div> } - // <button className="antimodeMenu-button" key="pin menu" title="Pin menu" onClick={this.toggleMenuPin} style={{ backgroundColor: this.Pinned ? "#121212" : "", display: this.collapsed ? "none" : undefined }}> - // <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.Pinned ? 45 : 0}deg)` }} /> - // </button> - // </div> */} - // </div>; } } |