diff options
Diffstat (limited to 'src/client/views/nodes/formattedText')
4 files changed, 61 insertions, 65 deletions
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index 3dcc45c96..38dd2e847 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -350,6 +350,7 @@ footnote::before { span { font-family: inherit; background-color: inherit; + display: contents; // fixes problem where extra space is added around <ol> lists when inside a prosemirror span } blockquote { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index c2f3a6e4b..43010b2ed 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -8,7 +8,7 @@ import { history } from 'prosemirror-history'; import { inputRules } from 'prosemirror-inputrules'; import { keymap } from 'prosemirror-keymap'; import { Fragment, Mark, Node, Slice } from 'prosemirror-model'; -import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from 'prosemirror-state'; +import { EditorState, NodeSelection, Plugin, Selection, TextSelection, Transaction } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import * as React from 'react'; import { BsMarkdownFill } from 'react-icons/bs'; @@ -983,10 +983,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB animateRes = (resIndex: number, newText: string) => { if (resIndex < newText.length) { const marks = this._editorView?.state.storedMarks ?? []; - this._editorView?.dispatch(this._editorView.state.tr.setStoredMarks(marks).insertText(newText[resIndex]).setStoredMarks(marks)); - setTimeout(() => { - this.animateRes(resIndex + 1, newText); - }, 20); + this._editorView?.dispatch(this._editorView?.state.tr.insertText(newText[resIndex]).setStoredMarks(marks)); + setTimeout(() => this.animateRes(resIndex + 1, newText), 20); } }; @@ -994,13 +992,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB try { let res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION); if (!res) { - console.error('GPT call failed'); this.animateRes(0, 'Something went wrong.'); - } else { - this.animateRes(0, res); + } else if (this._editorView) { + const { dispatch, state } = this._editorView; + // for no animation, use: dispatch(state.tr.insertText(res)); + // for animted response starting at end of text, use: + dispatch(state.tr.setSelection(Selection.atEnd(state.doc))); + this.animateRes(0, '\n\n' + res); } } catch (err) { - console.error('GPT call failed'); + console.error(err); this.animateRes(0, 'Something went wrong.'); } }); @@ -1484,6 +1485,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size)))); } else if (curText && !FormattedTextBox.DontSelectInitialText) { selectAll(this._editorView.state, this._editorView?.dispatch); + this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data } } if (selectOnLoad) { diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index ec8879487..03c902580 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -257,7 +257,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey }); // backspace = chainCommands(deleteSelection, joinBackward, selectNodeBackward); - const backspace = (state: EditorState, dispatch: (tx: Transaction) => void, view: EditorView, once = true) => { + const backspace = (state: EditorState, dispatch: (tx: Transaction) => void, view: EditorView) => { if (props.onKey?.(event, props)) return true; if (!canEdit(state)) return true; @@ -269,7 +269,10 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey if ( !joinBackward(state, (tx: Transaction) => { dispatch(updateBullets(tx, schema)); - if (once && view.state.selection.$from.depth > 1 && view.state.selection.$from.node(view.state.selection.$from.depth - 1).type === view.state.schema.nodes.list_item) backspace(view.state, view.dispatch, view, false); + if (view.state.selection.$anchor.node(-1)?.type === schema.nodes.list_item) { + // gets rid of an extra paragraph when joining two list items together. + joinBackward(view.state, (tx: Transaction) => view.dispatch(tx)); + } }) ) { if ( @@ -296,7 +299,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey const depth = trange ? liftTarget(trange) : null; if ( depth !== null && - state.selection.$from.node(state.selection.$from.depth - 1)?.type === state.schema.nodes.blockquote && // + state.selection.$from.node(-1)?.type === state.schema.nodes.blockquote && // !state.selection.$from.node().content.size && trange ) { @@ -306,7 +309,13 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); if (!newlineInCode(state, dispatch as any)) { - if (once && view.state.selection.$from.depth > 1 && !view.state.selection.$from.nodeBefore && !view.state.selection.$from.nodeBefore) { + const olNode = view.state.selection.$anchor.node(-2); + const liNode = view.state.selection.$anchor.node(-1); + // prettier-ignore + if (liNode?.type === schema.nodes.list_item && !liNode.textContent && + olNode?.type === schema.nodes.ordered_list && once && view.state.selection.$from.depth === 3) + { + // handles case of hitting enter at then end of a top-level empty list item - the result is to create a paragraph for (let i = 0; i < 10 && view.state.selection.$from.depth > 1 && liftListItem(schema.nodes.list_item)(view.state, view.dispatch); i++); } else if ( !splitListItem(schema.nodes.list_item)(state as any, (tx2: Transaction) => { @@ -314,32 +323,32 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey marks && tx3.ensureMarks([...marks]); marks && tx3.setStoredMarks([...marks]); dispatch(tx3); + // removes an extra paragraph created when selecting text across two list items or splitting an empty list item + !once && view.dispatch(view.state.tr.deleteRange(view.state.selection.from - 5, view.state.selection.from - 2)); }) ) { - const fromattrs = state.selection.$from.node().attrs; - if ( - !splitBlockKeepMarks(state, (tx3: Transaction) => { - const tonode = tx3.selection.$to.node(); - if (tx3.selection.to && tx3.doc.nodeAt(tx3.selection.to - 1)) { - const tx4 = tx3.setNodeMarkup(tx3.selection.to - 1, tonode.type, fromattrs, tonode.marks); - dispatch(tx4); - if ( - view.state.selection.$from.parentOffset && // - !view.state.selection.$from.node().content.size - ) - liftListItem(schema.nodes.list_item)(view.state, view.dispatch); - else if ( - once && - view.state.selection.$from.parentOffset && - view.state.selection.$from.depth > 1 && // - view.state.selection.$from.node(view.state.selection.$from.depth - 1).type === schema.nodes.list_item - ) - enter(view.state, view.dispatch, view, false); - else if (once && depth && !view.state.selection.$from.parentOffset) backspace(view.state, view.dispatch, view, false); - } else dispatch(tx3.insertText('\r\n')); - }) - ) { - return false; + if (once && view.state.selection.$from.node(-2)?.type === schema.nodes.ordered_list && view.state.selection.$from.node(-1)?.type === schema.nodes.list_item && view.state.selection.$from.node(-1)?.textContent === '') { + // handles case of hitting enter on an empty list item which needs to create a second empty paragraph, then split it by calling enter() again + view.dispatch(view.state.tr.insert(view.state.selection.from, schema.nodes.paragraph.create({}))); + enter(view.state, view.dispatch, view, false); + } else { + const fromattrs = state.selection.$from.node().attrs; + if ( + !splitBlockKeepMarks(state, (tx3: Transaction) => { + const tonode = tx3.selection.$to.node(); + if (tx3.selection.to && tx3.doc.nodeAt(tx3.selection.to - 1)) { + const tx4 = tx3.setNodeMarkup(tx3.selection.to - 1, tonode.type, fromattrs, tonode.marks); + dispatch(tx4); + } + + if (view.state.selection.$anchor.nodeAfter?.type === schema.nodes.text && once) { + // if text is selected across list items, then we need to forcibly insert a new line since the splitBlock code joins the two list items. + enter(view.state, dispatch, view, false); + } + }) + ) { + return false; + } } } } diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 4bd4ca72b..cecf106a3 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -404,39 +404,23 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { // 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 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); }; |
