From d5341a486cc6b0db30b61d28cce5e7101133a242 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Wed, 24 May 2023 22:24:05 -0400 Subject: [PATCH] chore: Upgrade all of prosemirror (#5366) Co-authored-by: Apoorv Mishra --- app/components/Avatar/Avatar.tsx | 3 +- app/editor/components/BlockMenu.tsx | 16 +- app/editor/components/ComponentView.tsx | 8 +- app/editor/components/EmojiMenu.tsx | 19 +- app/editor/components/FloatingToolbar.tsx | 46 ++-- app/editor/components/LinkEditor.tsx | 7 +- app/editor/components/MentionMenu.tsx | 22 +- app/editor/components/SelectionToolbar.tsx | 8 +- app/editor/components/SuggestionsMenu.tsx | 38 ++- app/editor/index.tsx | 22 +- app/scenes/Document/components/Document.tsx | 14 +- .../Document/components/EditableTitle.tsx | 16 +- app/typings/prosemirror-view.d.ts | 14 ++ package.json | 41 ++- server/collaboration/utils/markdownToYDoc.ts | 12 +- server/editor/index.test.ts | 2 +- server/models/helpers/DocumentHelper.tsx | 2 +- server/utils/parseDocumentIds.ts | 3 + server/utils/parseImages.ts | 3 + .../editor/commands/backspaceToParagraph.ts | 15 +- shared/editor/commands/clearNodes.ts | 5 +- shared/editor/commands/codeFence.ts | 22 +- shared/editor/commands/collapseSelection.ts | 5 +- .../commands/deleteEmptyFirstParagraph.ts | 31 +++ shared/editor/commands/moveLeft.ts | 20 +- shared/editor/commands/moveRight.ts | 11 +- shared/editor/commands/splitHeading.ts | 13 +- shared/editor/commands/table.ts | 152 ++++++++++++ shared/editor/commands/toggleBlockType.ts | 7 +- shared/editor/commands/toggleList.ts | 12 +- shared/editor/commands/toggleWrap.ts | 7 +- shared/editor/components/Styles.ts | 6 +- shared/editor/extensions/BlockMenu.tsx | 2 +- shared/editor/extensions/DateTime.ts | 16 +- shared/editor/extensions/Keys.ts | 3 +- shared/editor/extensions/Math.ts | 11 +- shared/editor/extensions/Mermaid.ts | 6 +- shared/editor/extensions/PasteHandler.ts | 4 + shared/editor/extensions/PreventTab.ts | 3 +- shared/editor/extensions/Prism.ts | 33 +-- shared/editor/lib/Extension.ts | 10 +- shared/editor/lib/ExtensionManager.ts | 10 +- shared/editor/lib/chainTransactions.ts | 9 +- shared/editor/lib/uploadPlaceholder.tsx | 6 +- shared/editor/marks/Comment.ts | 8 +- shared/editor/marks/Italic.ts | 2 +- shared/editor/marks/Link.tsx | 18 +- shared/editor/marks/Mark.ts | 7 +- shared/editor/nodes/Blockquote.ts | 9 +- shared/editor/nodes/Embed.tsx | 10 +- shared/editor/nodes/Emoji.tsx | 9 +- shared/editor/nodes/HardBreak.ts | 15 +- shared/editor/nodes/Heading.ts | 8 +- shared/editor/nodes/HorizontalRule.ts | 17 +- shared/editor/nodes/Image.tsx | 22 +- shared/editor/nodes/ListItem.ts | 18 +- shared/editor/nodes/Math.ts | 7 +- shared/editor/nodes/MathBlock.ts | 7 +- shared/editor/nodes/Mention.ts | 9 +- shared/editor/nodes/Node.ts | 7 +- shared/editor/nodes/Paragraph.ts | 2 + shared/editor/nodes/SimpleImage.tsx | 16 +- shared/editor/nodes/Table.ts | 106 +++----- shared/editor/nodes/TableCell.ts | 30 +-- shared/editor/nodes/TableHeadCell.ts | 18 +- shared/editor/plugins/FoldingHeaders.ts | 2 +- shared/editor/queries/findChildren.ts | 57 +++++ shared/editor/queries/findCollapsedNodes.ts | 2 +- shared/editor/queries/findLinkNodes.ts | 4 +- shared/editor/queries/findParentNode.ts | 44 ++++ shared/editor/queries/getColumnIndex.ts | 11 - shared/editor/queries/getRowIndex.ts | 21 -- shared/editor/queries/isNodeActive.ts | 16 +- shared/editor/queries/table.ts | 95 +++++++ shared/editor/types/index.ts | 4 +- tsconfig.json | 1 - yarn.lock | 233 +++++------------- 77 files changed, 875 insertions(+), 675 deletions(-) create mode 100644 app/typings/prosemirror-view.d.ts create mode 100644 shared/editor/commands/deleteEmptyFirstParagraph.ts create mode 100644 shared/editor/commands/table.ts create mode 100644 shared/editor/queries/findChildren.ts create mode 100644 shared/editor/queries/findParentNode.ts delete mode 100644 shared/editor/queries/getColumnIndex.ts delete mode 100644 shared/editor/queries/getRowIndex.ts create mode 100644 shared/editor/queries/table.ts diff --git a/app/components/Avatar/Avatar.tsx b/app/components/Avatar/Avatar.tsx index f212024fa..7a85c9195 100644 --- a/app/components/Avatar/Avatar.tsx +++ b/app/components/Avatar/Avatar.tsx @@ -4,9 +4,10 @@ import useBoolean from "~/hooks/useBoolean"; import Initials from "./Initials"; export enum AvatarSize { - Small = 18, + Small = 16, Medium = 24, Large = 32, + XLarge = 48, } export interface IAvatar { diff --git a/app/editor/components/BlockMenu.tsx b/app/editor/components/BlockMenu.tsx index aca9af54d..06d600d3b 100644 --- a/app/editor/components/BlockMenu.tsx +++ b/app/editor/components/BlockMenu.tsx @@ -1,8 +1,6 @@ -import { findParentNode } from "prosemirror-utils"; import React from "react"; import useDictionary from "~/hooks/useDictionary"; import getMenuItems from "../menus/block"; -import { useEditor } from "./EditorContext"; import SuggestionsMenu, { Props as SuggestionsMenuProps, } from "./SuggestionsMenu"; @@ -10,28 +8,18 @@ import SuggestionsMenuItem from "./SuggestionsMenuItem"; type Props = Omit< SuggestionsMenuProps, - "renderMenuItem" | "items" | "onClearSearch" + "renderMenuItem" | "items" | "trigger" > & Required>; function BlockMenu(props: Props) { - const { view } = useEditor(); const dictionary = useDictionary(); - const clearSearch = React.useCallback(() => { - const { state, dispatch } = view; - const parent = findParentNode((node) => !!node)(state.selection); - - if (parent) { - dispatch(state.tr.insertText("", parent.pos, state.selection.to)); - } - }, [view]); - return ( ( number; - decorations: Decoration<{ - [key: string]: any; - }>[]; + decorations: Decoration[]; isSelected = false; dom: HTMLElement | null; @@ -39,9 +37,7 @@ export default class ComponentView { node: ProsemirrorNode; view: EditorView; getPos: () => number; - decorations: Decoration<{ - [key: string]: any; - }>[]; + decorations: Decoration[]; } ) { this.component = component; diff --git a/app/editor/components/EmojiMenu.tsx b/app/editor/components/EmojiMenu.tsx index 9cdeea313..bab89e990 100644 --- a/app/editor/components/EmojiMenu.tsx +++ b/app/editor/components/EmojiMenu.tsx @@ -1,7 +1,6 @@ import FuzzySearch from "fuzzy-search"; import gemojies from "gemoji"; import React from "react"; -import { useEditor } from "./EditorContext"; import EmojiMenuItem from "./EmojiMenuItem"; import SuggestionsMenu, { Props as SuggestionsMenuProps, @@ -26,12 +25,11 @@ const searcher = new FuzzySearch<{ type Props = Omit< SuggestionsMenuProps, - "renderMenuItem" | "items" | "onLinkToolbarOpen" | "embeds" | "onClearSearch" + "renderMenuItem" | "items" | "onLinkToolbarOpen" | "embeds" | "trigger" >; const EmojiMenu = (props: Props) => { const { search = "" } = props; - const { view } = useEditor(); const items = React.useMemo(() => { const n = search.toLowerCase(); @@ -50,24 +48,11 @@ const EmojiMenu = (props: Props) => { return result.slice(0, 10); }, [search]); - const clearSearch = React.useCallback(() => { - const { state, dispatch } = view; - - // clear search input - dispatch( - state.tr.insertText( - "", - state.selection.$from.pos - (props.search ?? "").length - 1, - state.selection.to - ) - ); - }, [view, props.search]); - return ( ( *:nth-child(${rect.left + 1})` + ); + const bounds = (element as HTMLElement).getBoundingClientRect(); + selectionBounds.top = bounds.top - 16; + selectionBounds.left = bounds.left; + selectionBounds.right = bounds.right; + } else if (isRowSelection) { + const rect = selectedRect(view.state); + const table = view.domAtPos(rect.tableStart); + const element = (table.node as HTMLElement).querySelector( + `tr:nth-child(${rect.top + 1}) > *` + ); + const bounds = (element as HTMLElement).getBoundingClientRect(); + selectionBounds.top = bounds.top; + selectionBounds.left = bounds.left - 10; + selectionBounds.right = bounds.left - 10; } const isImageSelection = diff --git a/app/editor/components/LinkEditor.tsx b/app/editor/components/LinkEditor.tsx index 62d64e5af..11fd8248d 100644 --- a/app/editor/components/LinkEditor.tsx +++ b/app/editor/components/LinkEditor.tsx @@ -6,7 +6,7 @@ import { OpenIcon, } from "outline-icons"; import { Mark } from "prosemirror-model"; -import { setTextSelection } from "prosemirror-utils"; +import { Selection } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import * as React from "react"; import styled from "styled-components"; @@ -285,7 +285,10 @@ class LinkEditor extends React.Component { moveSelectionToEnd = () => { const { to, view } = this.props; const { state, dispatch } = view; - dispatch(setTextSelection(to)(state.tr)); + const nextSelection = Selection.findFrom(state.tr.doc.resolve(to), 1, true); + if (nextSelection) { + dispatch(state.tr.setSelection(nextSelection)); + } view.focus(); }; diff --git a/app/editor/components/MentionMenu.tsx b/app/editor/components/MentionMenu.tsx index f9a48ed16..68fe24ad1 100644 --- a/app/editor/components/MentionMenu.tsx +++ b/app/editor/components/MentionMenu.tsx @@ -8,10 +8,10 @@ import { MentionType } from "@shared/types"; import parseDocumentSlug from "@shared/utils/parseDocumentSlug"; import User from "~/models/User"; import Avatar from "~/components/Avatar"; +import { AvatarSize } from "~/components/Avatar/Avatar"; import Flex from "~/components/Flex"; import useRequest from "~/hooks/useRequest"; import useStores from "~/hooks/useStores"; -import { useEditor } from "./EditorContext"; import MentionMenuItem from "./MentionMenuItem"; import SuggestionsMenu, { Props as SuggestionsMenuProps, @@ -32,7 +32,7 @@ interface MentionItem extends MenuItem { type Props = Omit< SuggestionsMenuProps, - "renderMenuItem" | "items" | "onLinkToolbarOpen" | "embeds" | "onClearSearch" + "renderMenuItem" | "items" | "onLinkToolbarOpen" | "embeds" | "trigger" >; function MentionMenu({ search, isActive, ...rest }: Props) { @@ -42,7 +42,6 @@ function MentionMenu({ search, isActive, ...rest }: Props) { const { users, auth } = useStores(); const location = useLocation(); const documentId = parseDocumentSlug(location.pathname); - const { view } = useEditor(); const { data, loading, request } = useRequest( React.useCallback( () => @@ -80,19 +79,6 @@ function MentionMenu({ search, isActive, ...rest }: Props) { } }, [auth.user?.id, loading, data]); - const clearSearch = () => { - const { state, dispatch } = view; - - // clear search input - dispatch( - state.tr.insertText( - "", - state.selection.$from.pos - (search ?? "").length - 1, - state.selection.to - ) - ); - }; - // Prevent showing the menu until we have data otherwise it will be positioned // incorrectly due to the height being unknown. if (!loaded) { @@ -104,7 +90,7 @@ function MentionMenu({ search, isActive, ...rest }: Props) { {...rest} isActive={isActive} filterable={false} - onClearSearch={clearSearch} + trigger="@" search={search} renderMenuItem={(item, _index, options) => ( } diff --git a/app/editor/components/SelectionToolbar.tsx b/app/editor/components/SelectionToolbar.tsx index 65fced58c..6f03396f1 100644 --- a/app/editor/components/SelectionToolbar.tsx +++ b/app/editor/components/SelectionToolbar.tsx @@ -1,14 +1,12 @@ import { some } from "lodash"; import { EditorState, NodeSelection, TextSelection } from "prosemirror-state"; -import { CellSelection } from "prosemirror-tables"; import * as React from "react"; import createAndInsertLink from "@shared/editor/commands/createAndInsertLink"; import filterExcessSeparators from "@shared/editor/lib/filterExcessSeparators"; -import getColumnIndex from "@shared/editor/queries/getColumnIndex"; import getMarkRange from "@shared/editor/queries/getMarkRange"; -import getRowIndex from "@shared/editor/queries/getRowIndex"; import isMarkActive from "@shared/editor/queries/isMarkActive"; import isNodeActive from "@shared/editor/queries/isNodeActive"; +import { getColumnIndex, getRowIndex } from "@shared/editor/queries/table"; import { MenuItem } from "@shared/editor/types"; import { creatingUrlPrefix } from "@shared/utils/urls"; import useBoolean from "~/hooks/useBoolean"; @@ -197,8 +195,8 @@ export default function SelectionToolbar(props: Props) { return null; } - const colIndex = getColumnIndex(state.selection as unknown as CellSelection); - const rowIndex = getRowIndex(state.selection as unknown as CellSelection); + const colIndex = getColumnIndex(state); + const rowIndex = getRowIndex(state); const isTableSelection = colIndex !== undefined && rowIndex !== undefined; const link = isMarkActive(state.schema.marks.link)(state); const range = getMarkRange(selection.$from, state.schema.marks.link); diff --git a/app/editor/components/SuggestionsMenu.tsx b/app/editor/components/SuggestionsMenu.tsx index df334f86d..263fc955a 100644 --- a/app/editor/components/SuggestionsMenu.tsx +++ b/app/editor/components/SuggestionsMenu.tsx @@ -1,6 +1,5 @@ import commandScore from "command-score"; import { capitalize } from "lodash"; -import { findParentNode } from "prosemirror-utils"; import * as React from "react"; import { Trans } from "react-i18next"; import { VisuallyHidden } from "reakit/VisuallyHidden"; @@ -8,6 +7,7 @@ import styled from "styled-components"; import insertFiles from "@shared/editor/commands/insertFiles"; import { EmbedDescriptor } from "@shared/editor/embeds"; import filterExcessSeparators from "@shared/editor/lib/filterExcessSeparators"; +import { findParentNode } from "@shared/editor/queries/findParentNode"; import { MenuItem } from "@shared/editor/types"; import { depths, s } from "@shared/styles"; import { getEventFiles } from "@shared/utils/files"; @@ -56,12 +56,12 @@ export type Props = { rtl: boolean; isActive: boolean; search: string; + trigger: string; uploadFile?: (file: File) => Promise; onFileUploadStart?: () => void; onFileUploadStop?: () => void; onLinkToolbarOpen?: () => void; onClose: (insertNewLine?: boolean) => void; - onClearSearch: () => void; embeds?: EmbedDescriptor[]; renderMenuItem: ( item: T, @@ -163,6 +163,30 @@ function SuggestionsMenu(props: Props) { [view] ); + const handleClearSearch = React.useCallback(() => { + const { state, dispatch } = view; + const poss = state.doc.cut( + state.selection.from - (props.search ?? "").length - 1, + state.selection.from + ); + const trimTrigger = poss.textContent.startsWith(props.trigger); + + if (!props.search && !trimTrigger) { + return; + } + + // clear search input + dispatch( + state.tr.insertText( + "", + state.selection.from - + (props.search ?? "").length - + (trimTrigger ? 1 : 0), + state.selection.to + ) + ); + }, [props.search, props.trigger, view]); + React.useEffect(() => { if (!props.isActive) { return; @@ -185,7 +209,7 @@ function SuggestionsMenu(props: Props) { const insertNode = React.useCallback( (item: MenuItem | EmbedDescriptor) => { - props.onClearSearch(); + handleClearSearch(); const command = item.name ? commands[item.name] : undefined; @@ -201,7 +225,7 @@ function SuggestionsMenu(props: Props) { props.onClose(); }, - [commands, props, view] + [commands, handleClearSearch, props, view] ); const handleClickItem = React.useCallback( @@ -216,7 +240,7 @@ function SuggestionsMenu(props: Props) { case "embed": return triggerLinkInput(item); case "link": { - props.onClearSearch(); + handleClearSearch(); props.onClose(); props.onLinkToolbarOpen?.(); return; @@ -225,7 +249,7 @@ function SuggestionsMenu(props: Props) { insertNode(item); } }, - [insertNode, props] + [insertNode, handleClearSearch, props] ); const close = React.useCallback(() => { @@ -313,7 +337,7 @@ function SuggestionsMenu(props: Props) { const files = getEventFiles(event); const parent = findParentNode((node) => !!node)(view.state.selection); - props.onClearSearch(); + handleClearSearch(); if (!uploadFile) { throw new Error("uploadFile prop is required to replace files"); diff --git a/app/editor/index.tsx b/app/editor/index.tsx index c311c25cd..bb556ed8e 100644 --- a/app/editor/index.tsx +++ b/app/editor/index.tsx @@ -14,7 +14,7 @@ import { Node as ProsemirrorNode, } from "prosemirror-model"; import { EditorState, Selection, Plugin, Transaction } from "prosemirror-state"; -import { Decoration, EditorView } from "prosemirror-view"; +import { Decoration, EditorView, NodeViewConstructor } from "prosemirror-view"; import * as React from "react"; import styled, { css, DefaultTheme, ThemeProps } from "styled-components"; import Styles from "@shared/editor/components/Styles"; @@ -193,14 +193,7 @@ export class Editor extends React.PureComponent< keymaps: Plugin[]; inputRules: InputRule[]; nodeViews: { - [name: string]: ( - node: ProsemirrorNode, - view: EditorView, - getPos: () => number, - decorations: Decoration<{ - [key: string]: any; - }>[] - ) => ComponentView; + [name: string]: NodeViewConstructor; }; nodes: { [name: string]: NodeSpec }; @@ -350,9 +343,7 @@ export class Editor extends React.PureComponent< node: ProsemirrorNode, view: EditorView, getPos: () => number, - decorations: Decoration<{ - [key: string]: any; - }>[] + decorations: Decoration[] ) => new ComponentView(extension.component, { editor: this, @@ -435,7 +426,7 @@ export class Editor extends React.PureComponent< private createDocument(content: string | object) { // Looks like Markdown if (typeof content === "string") { - return this.parser.parse(content); + return this.parser.parse(content) || undefined; } return ProsemirrorNode.fromJSON(this.schema, content); @@ -464,8 +455,9 @@ export class Editor extends React.PureComponent< nodeViews: this.nodeViews, dispatchTransaction(transaction) { // callback is bound to have the view instance as its this binding - const { state, transactions } = - this.state.applyTransaction(transaction); + const { state, transactions } = ( + this.state as EditorState + ).applyTransaction(transaction); this.updateState(state); diff --git a/app/scenes/Document/components/Document.tsx b/app/scenes/Document/components/Document.tsx index 2d3c3ddd9..6ee2d51f5 100644 --- a/app/scenes/Document/components/Document.tsx +++ b/app/scenes/Document/components/Document.tsx @@ -140,11 +140,15 @@ class DocumentScene extends React.Component { } const { view, parser } = editorRef; - view.dispatch( - view.state.tr - .setSelection(new AllSelection(view.state.doc)) - .replaceSelectionWith(parser.parse(template.text)) - ); + const doc = parser.parse(template.text); + + if (doc) { + view.dispatch( + view.state.tr + .setSelection(new AllSelection(view.state.doc)) + .replaceSelectionWith(doc) + ); + } this.isEditorDirty = true; diff --git a/app/scenes/Document/components/EditableTitle.tsx b/app/scenes/Document/components/EditableTitle.tsx index 994e24cd5..399f4ea81 100644 --- a/app/scenes/Document/components/EditableTitle.tsx +++ b/app/scenes/Document/components/EditableTitle.tsx @@ -143,7 +143,9 @@ const EditableTitle = React.forwardRef( if (isMarkdown(text)) { const paste = pasteParser.parse(normalizePastedMarkdown(content)); - slice = paste.slice(0); + if (paste) { + slice = paste.slice(0); + } } else { const defaultSlice = __parseFromClipboard( view, @@ -165,11 +167,13 @@ const EditableTitle = React.forwardRef( : defaultSlice; } - view.dispatch( - view.state.tr - .setSelection(Selection.atStart(view.state.doc)) - .replaceSelection(slice) - ); + if (slice) { + view.dispatch( + view.state.tr + .setSelection(Selection.atStart(view.state.doc)) + .replaceSelection(slice) + ); + } } }, [editor] diff --git a/app/typings/prosemirror-view.d.ts b/app/typings/prosemirror-view.d.ts new file mode 100644 index 000000000..e07200013 --- /dev/null +++ b/app/typings/prosemirror-view.d.ts @@ -0,0 +1,14 @@ +import "prosemirror-view"; + +declare module "prosemirror-view" { + import { ResolvedPos } from "prosemirror-model"; + import { EditorView } from "prosemirror-view"; + + export function __parseFromClipboard( + view: EditorView, + text: string, + html: string | null, + plainText: boolean, + $context: ResolvedPos + ); +} diff --git a/package.json b/package.json index de7d0dd68..7615f5045 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "dev:watch": "NODE_ENV=development yarn concurrently -n backend,frontend \"yarn dev:backend\" \"yarn vite:dev\"", "lint": "eslint app server shared plugins", "prepare": "husky install", - "postinstall": "rimraf node_modules/prosemirror-view/dist/index.d.ts && yarn patch-package", + "postinstall": "yarn patch-package", "heroku-postbuild": "yarn build && yarn db:migrate", "db:create-migration": "sequelize migration:create", "db:create": "sequelize db:create", @@ -149,20 +149,19 @@ "pg": "^8.8.0", "pg-tsquery": "^8.4.0", "polished": "^4.2.2", - "prosemirror-commands": "1.2.2", - "prosemirror-dropcursor": "^1.6.1", - "prosemirror-gapcursor": "^1.3.1", - "prosemirror-history": "^1.2.0", - "prosemirror-inputrules": "^1.1.3", - "prosemirror-keymap": "^1.1.5", - "prosemirror-markdown": "^1.9.3", - "prosemirror-model": "1.16.1", - "prosemirror-schema-list": "1.1.4", - "prosemirror-state": "1.3.4", - "prosemirror-tables": "^1.1.1", - "prosemirror-transform": "1.2.5", - "prosemirror-utils": "^0.9.6", - "prosemirror-view": "1.26.5", + "prosemirror-commands": "^1.5.2", + "prosemirror-dropcursor": "^1.8.1", + "prosemirror-gapcursor": "^1.3.2", + "prosemirror-history": "^1.3.2", + "prosemirror-inputrules": "^1.2.1", + "prosemirror-keymap": "^1.2.2", + "prosemirror-markdown": "^1.11.0", + "prosemirror-model": "^1.19.1", + "prosemirror-schema-list": "^1.2.3", + "prosemirror-state": "^1.4.3", + "prosemirror-tables": "^1.3.2", + "prosemirror-transform": "^1.7.2", + "prosemirror-view": "^1.31.3", "query-string": "^7.1.1", "quoted-printable": "^1.0.1", "randomstring": "1.2.3", @@ -263,17 +262,6 @@ "@types/node-fetch": "^2.6.2", "@types/nodemailer": "^6.4.7", "@types/passport-oauth2": "^1.4.11", - "@types/prosemirror-commands": "^1.0.4", - "@types/prosemirror-dropcursor": "^1.5.0", - "@types/prosemirror-gapcursor": "^1.3.0", - "@types/prosemirror-history": "^1.0.1", - "@types/prosemirror-inputrules": "^1.0.2", - "@types/prosemirror-keymap": "^1.0.1", - "@types/prosemirror-markdown": "^1.0.3", - "@types/prosemirror-model": "^1.7.2", - "@types/prosemirror-schema-list": "^1.0.3", - "@types/prosemirror-state": "^1.2.4", - "@types/prosemirror-view": "^1.11.4", "@types/quoted-printable": "^1.0.0", "@types/randomstring": "^1.1.8", "@types/react": "^17.0.34", @@ -341,7 +329,6 @@ "body-scroll-lock": "^4.0.0-beta.0", "d3": "^7.0.0", "node-fetch": "^2.6.7", - "prosemirror-transform": "1.2.5", "dot-prop": "^5.2.0", "js-yaml": "^3.14.1", "jpeg-js": "0.4.4", diff --git a/server/collaboration/utils/markdownToYDoc.ts b/server/collaboration/utils/markdownToYDoc.ts index 7bbb6846c..216bfe0ab 100644 --- a/server/collaboration/utils/markdownToYDoc.ts +++ b/server/collaboration/utils/markdownToYDoc.ts @@ -13,13 +13,12 @@ export default function markdownToYDoc( // in the editor embeds were created at runtime by converting links // into embeds where they match. Because we're converting to a CRDT structure // on the server we need to mimic this behavior. - function urlsToEmbeds(node: Node): Node { + function urlsToEmbeds(node: Node): Node | null { if (node.type.name === "paragraph") { // @ts-expect-error ts-migrate(2339) FIXME: Property 'content' does not exist on type 'Fragmen... Remove this comment to see the full error message for (const textNode of node.content.content) { for (const embed of embeds) { if (textNode.text && embed.matcher(textNode.text)) { - // @ts-expect-error ts-migrate(2322) FIXME: Type 'ProsemirrorNode> | null... Remove this comment to see the full error message return schema.nodes.embed.createAndFill({ href: textNode.text, }); @@ -30,14 +29,19 @@ export default function markdownToYDoc( if (node.content) { const contentAsArray = - // @ts-expect-error ts-migrate(2339) FIXME: Property 'content' does not exist on type 'Fragmen... Remove this comment to see the full error message + // @ts-expect-error content node.content instanceof Fragment ? node.content.content : node.content; + // @ts-expect-error content node.content = Fragment.fromArray(contentAsArray.map(urlsToEmbeds)); } return node; } - node = urlsToEmbeds(node); + if (node) { + node = urlsToEmbeds(node); + } + + // @ts-expect-error null node return prosemirrorToYDoc(node, fieldName); } diff --git a/server/editor/index.test.ts b/server/editor/index.test.ts index 528df9078..e5d8411f2 100644 --- a/server/editor/index.test.ts +++ b/server/editor/index.test.ts @@ -3,7 +3,7 @@ import { parser } from "."; test("renders an empty doc", () => { const ast = parser.parse(""); - expect(ast.toJSON()).toEqual({ + expect(ast?.toJSON()).toEqual({ content: [{ type: "paragraph" }], type: "doc", }); diff --git a/server/models/helpers/DocumentHelper.tsx b/server/models/helpers/DocumentHelper.tsx index 6b2578386..b1c501922 100644 --- a/server/models/helpers/DocumentHelper.tsx +++ b/server/models/helpers/DocumentHelper.tsx @@ -54,7 +54,7 @@ export default class DocumentHelper { Y.applyUpdate(ydoc, document.state); return Node.fromJSON(schema, yDocToProsemirrorJSON(ydoc, "default")); } - return parser.parse(document.text); + return parser.parse(document.text) || Node.fromJSON(schema, {}); } /** diff --git a/server/utils/parseDocumentIds.ts b/server/utils/parseDocumentIds.ts index 33f686bc6..2c3dd7506 100644 --- a/server/utils/parseDocumentIds.ts +++ b/server/utils/parseDocumentIds.ts @@ -12,6 +12,9 @@ import { parser } from "@server/editor"; export default function parseDocumentIds(text: string): string[] { const doc = parser.parse(text); const identifiers: string[] = []; + if (!doc) { + return identifiers; + } doc.descendants((node: Node) => { // get text nodes diff --git a/server/utils/parseImages.ts b/server/utils/parseImages.ts index f9531a060..69772c23e 100644 --- a/server/utils/parseImages.ts +++ b/server/utils/parseImages.ts @@ -4,6 +4,9 @@ import { parser } from "@server/editor"; export default function parseImages(text: string): string[] { const doc = parser.parse(text); const images: string[] = []; + if (!doc) { + return images; + } doc.descendants((node: Node) => { if (node.type.name === "image") { diff --git a/shared/editor/commands/backspaceToParagraph.ts b/shared/editor/commands/backspaceToParagraph.ts index e4ad0f0c8..4b794226b 100644 --- a/shared/editor/commands/backspaceToParagraph.ts +++ b/shared/editor/commands/backspaceToParagraph.ts @@ -1,6 +1,5 @@ import { NodeType } from "prosemirror-model"; -import { EditorState } from "prosemirror-state"; -import { Dispatch } from "../types"; +import { Command } from "prosemirror-state"; /** * Converts the current node to a paragraph when pressing backspace at the @@ -9,28 +8,28 @@ import { Dispatch } from "../types"; * @param type The node type * @returns A prosemirror command. */ -export default function backspaceToParagraph(type: NodeType) { - return (state: EditorState, dispatch: Dispatch) => { +export default function backspaceToParagraph(type: NodeType): Command { + return (state, dispatch) => { const { $from, from, to, empty } = state.selection; // if the selection has anything in it then use standard delete behavior if (!empty) { - return null; + return false; } // check we're in a matching node if ($from.parent.type !== type) { - return null; + return false; } // check if we're at the beginning of the heading const $pos = state.doc.resolve(from - 1); if ($pos.parent === $from.parent) { - return null; + return false; } // okay, replace it with a paragraph - dispatch( + dispatch?.( state.tr .setBlockType(from, to, type.schema.nodes.paragraph) .scrollIntoView() diff --git a/shared/editor/commands/clearNodes.ts b/shared/editor/commands/clearNodes.ts index 55a30b92c..defab9cb7 100644 --- a/shared/editor/commands/clearNodes.ts +++ b/shared/editor/commands/clearNodes.ts @@ -1,8 +1,7 @@ -import { EditorState } from "prosemirror-state"; +import { Command } from "prosemirror-state"; import { liftTarget } from "prosemirror-transform"; -import { Dispatch } from "../types"; -const clearNodes = () => (state: EditorState, dispatch?: Dispatch) => { +const clearNodes = (): Command => (state, dispatch) => { const { tr } = state; const { selection } = tr; const { ranges } = selection; diff --git a/shared/editor/commands/codeFence.ts b/shared/editor/commands/codeFence.ts index 2b46d91e6..e1f58eef3 100644 --- a/shared/editor/commands/codeFence.ts +++ b/shared/editor/commands/codeFence.ts @@ -1,6 +1,5 @@ -import { EditorState, TextSelection } from "prosemirror-state"; +import { Command, TextSelection } from "prosemirror-state"; import isInCode from "../queries/isInCode"; -import { Dispatch } from "../types"; /** * Moves the current selection to the previous newline, this is used inside @@ -8,10 +7,7 @@ import { Dispatch } from "../types"; * * @returns A prosemirror command. */ -export const moveToPreviousNewline = ( - state: EditorState, - dispatch: Dispatch -) => { +export const moveToPreviousNewline: Command = (state, dispatch) => { if (!isInCode(state)) { return false; } @@ -34,7 +30,7 @@ export const moveToPreviousNewline = ( return false; } - dispatch( + dispatch?.( state.tr.setSelection( TextSelection.create( state.doc, @@ -52,7 +48,7 @@ export const moveToPreviousNewline = ( * * @returns A prosemirror command. */ -export const moveToNextNewline = (state: EditorState, dispatch: Dispatch) => { +export const moveToNextNewline: Command = (state, dispatch) => { if (!isInCode(state)) { return false; } @@ -69,7 +65,7 @@ export const moveToNextNewline = (state: EditorState, dispatch: Dispatch) => { return false; } - dispatch( + dispatch?.( state.tr.setSelection( TextSelection.create(state.doc, beginningOfNode + endOfLine) ) @@ -85,7 +81,7 @@ export const moveToNextNewline = (state: EditorState, dispatch: Dispatch) => { * * @returns A prosemirror command */ -export const newlineInCode = (state: EditorState, dispatch: Dispatch) => { +export const newlineInCode: Command = (state, dispatch) => { if (!isInCode(state)) { return false; } @@ -101,7 +97,7 @@ export const newlineInCode = (state: EditorState, dispatch: Dispatch) => { newText += " ".repeat(numOfSpaces); } - dispatch(tr.insertText(newText, selection.from, selection.to)); + dispatch?.(tr.insertText(newText, selection.from, selection.to)); return true; }; @@ -110,12 +106,12 @@ export const newlineInCode = (state: EditorState, dispatch: Dispatch) => { * * @returns A prosemirror command */ -export const insertSpaceTab = (state: EditorState, dispatch: Dispatch) => { +export const insertSpaceTab: Command = (state, dispatch) => { if (!isInCode(state)) { return false; } const { tr, selection } = state; - dispatch(tr.insertText(" ", selection.from, selection.to)); + dispatch?.(tr.insertText(" ", selection.from, selection.to)); return true; }; diff --git a/shared/editor/commands/collapseSelection.ts b/shared/editor/commands/collapseSelection.ts index f5d95ed18..8467c0046 100644 --- a/shared/editor/commands/collapseSelection.ts +++ b/shared/editor/commands/collapseSelection.ts @@ -1,7 +1,6 @@ -import { EditorState, TextSelection } from "prosemirror-state"; -import { Dispatch } from "../types"; +import { Command, TextSelection } from "prosemirror-state"; -const collapseSelection = () => (state: EditorState, dispatch?: Dispatch) => { +const collapseSelection = (): Command => (state, dispatch) => { dispatch?.( state.tr.setSelection( TextSelection.create(state.doc, state.tr.selection.from) diff --git a/shared/editor/commands/deleteEmptyFirstParagraph.ts b/shared/editor/commands/deleteEmptyFirstParagraph.ts new file mode 100644 index 000000000..3a706fac5 --- /dev/null +++ b/shared/editor/commands/deleteEmptyFirstParagraph.ts @@ -0,0 +1,31 @@ +import { Command } from "prosemirror-state"; +import isNodeActive from "../queries/isNodeActive"; + +/** + * Deletes the first paragraph node if it is empty and the cursor is at the + * beginning of the document. + */ +const deleteEmptyFirstParagraph: Command = (state, dispatch) => { + if (!isNodeActive(state.schema.nodes.paragraph)(state)) { + return false; + } + + if (state.selection.from !== 1 || state.selection.to !== 1) { + return false; + } + + const parent = state.selection.$from.parent; + if (parent.textContent !== "" || parent.childCount > 0) { + return false; + } + + // delete the empty paragraph node + dispatch?.( + state.tr + .delete(state.selection.from - 1, state.selection.from) + .scrollIntoView() + ); + return true; +}; + +export default deleteEmptyFirstParagraph; diff --git a/shared/editor/commands/moveLeft.ts b/shared/editor/commands/moveLeft.ts index 82c3c4b65..6bd3686cb 100644 --- a/shared/editor/commands/moveLeft.ts +++ b/shared/editor/commands/moveLeft.ts @@ -17,9 +17,13 @@ limitations under the License. // This file is based on the implementation found here: // https://bitbucket.org/atlassian/design-system-mirror/src/master/editor/editor-core/src/plugins/text-formatting/commands/text-formatting.ts -import { Selection, EditorState, TextSelection } from "prosemirror-state"; +import { + Selection, + EditorState, + TextSelection, + Command, +} from "prosemirror-state"; import isMarkActive from "../queries/isMarkActive"; -import { Dispatch } from "../types"; function hasCode(state: EditorState, pos: number) { const { code_inline } = state.schema.marks; @@ -30,8 +34,8 @@ function hasCode(state: EditorState, pos: number) { : false; } -export default function moveLeft() { - return (state: EditorState, dispatch: Dispatch): boolean => { +export default function moveLeft(): Command { + return (state, dispatch): boolean => { const { code_inline } = state.schema.marks; const { empty, $cursor } = state.selection as TextSelection; if (!empty || !$cursor) { @@ -70,14 +74,14 @@ export default function moveLeft() { Selection.near(state.doc.resolve($cursor.pos - 1)) ); - dispatch(tr.removeStoredMark(code_inline)); + dispatch?.(tr.removeStoredMark(code_inline)); return true; } // entering code mark (from right edge): don't move the cursor, just add the mark if (!insideCode && enteringCode) { - dispatch(state.tr.addStoredMark(code_inline.create())); + dispatch?.(state.tr.addStoredMark(code_inline.create())); return true; } @@ -87,7 +91,7 @@ export default function moveLeft() { Selection.near(state.doc.resolve($cursor.pos - 1)) ); - dispatch(tr.addStoredMark(code_inline.create())); + dispatch?.(tr.addStoredMark(code_inline.create())); return true; } @@ -97,7 +101,7 @@ export default function moveLeft() { insideCode && (exitingCode || (!$cursor.nodeBefore && isFirstChild)) ) { - dispatch(state.tr.removeStoredMark(code_inline)); + dispatch?.(state.tr.removeStoredMark(code_inline)); return true; } } diff --git a/shared/editor/commands/moveRight.ts b/shared/editor/commands/moveRight.ts index 5c2c0ccd9..259d5815c 100644 --- a/shared/editor/commands/moveRight.ts +++ b/shared/editor/commands/moveRight.ts @@ -17,12 +17,11 @@ limitations under the License. // This file is based on the implementation found here: // https://bitbucket.org/atlassian/design-system-mirror/src/master/editor/editor-core/src/plugins/text-formatting/commands/text-formatting.ts -import { EditorState, TextSelection } from "prosemirror-state"; +import { Command, TextSelection } from "prosemirror-state"; import isMarkActive from "../queries/isMarkActive"; -import { Dispatch } from "../types"; -export default function moveRight() { - return (state: EditorState, dispatch: Dispatch): boolean => { +export default function moveRight(): Command { + return (state, dispatch): boolean => { const { code_inline } = state.schema.marks; const { empty, $cursor } = state.selection as TextSelection; if (!empty || !$cursor) { @@ -54,14 +53,14 @@ export default function moveRight() { // entering code mark (from the left edge): don't move the cursor, just add the mark if (!insideCode && enteringCode) { - dispatch(state.tr.addStoredMark(code_inline.create())); + dispatch?.(state.tr.addStoredMark(code_inline.create())); return true; } // exiting code mark: don't move the cursor, just remove the mark if (insideCode && exitingCode) { - dispatch(state.tr.removeStoredMark(code_inline)); + dispatch?.(state.tr.removeStoredMark(code_inline)); return true; } diff --git a/shared/editor/commands/splitHeading.ts b/shared/editor/commands/splitHeading.ts index a3bf1d699..61d300a8d 100644 --- a/shared/editor/commands/splitHeading.ts +++ b/shared/editor/commands/splitHeading.ts @@ -1,11 +1,10 @@ import { NodeType } from "prosemirror-model"; -import { EditorState, TextSelection } from "prosemirror-state"; -import { findBlockNodes } from "prosemirror-utils"; +import { Command, TextSelection } from "prosemirror-state"; +import { findBlockNodes } from "../queries/findChildren"; import findCollapsedNodes from "../queries/findCollapsedNodes"; -import { Dispatch } from "../types"; -export default function splitHeading(type: NodeType) { - return (state: EditorState, dispatch: Dispatch): boolean => { +export default function splitHeading(type: NodeType): Command { + return (state, dispatch): boolean => { const { $from, from, $to, to } = state.selection; // check we're in a matching heading node @@ -33,7 +32,7 @@ export default function splitHeading(type: NodeType) { ); // Move the selection into the new heading node and make sure it's on screen - dispatch( + dispatch?.( transaction .setSelection( TextSelection.near(transaction.doc.resolve($from.before())) @@ -75,7 +74,7 @@ export default function splitHeading(type: NodeType) { ); // Move the selection into the new heading node and make sure it's on screen - dispatch( + dispatch?.( transaction .setSelection( TextSelection.near( diff --git a/shared/editor/commands/table.ts b/shared/editor/commands/table.ts new file mode 100644 index 000000000..98c6cd2da --- /dev/null +++ b/shared/editor/commands/table.ts @@ -0,0 +1,152 @@ +import { Fragment, Node, NodeType } from "prosemirror-model"; +import { + Command, + EditorState, + TextSelection, + Transaction, +} from "prosemirror-state"; +import { + CellSelection, + addRow, + isInTable, + selectedRect, + tableNodeTypes, +} from "prosemirror-tables"; +import { getCellsInColumn } from "../queries/table"; + +export function createTable( + state: EditorState, + rowsCount: number, + colsCount: number, + withHeaderRow = true, + cellContent?: Node +) { + const types = tableNodeTypes(state.schema); + const headerCells: Node[] = []; + const cells: Node[] = []; + const rows: Node[] = []; + + const createCell = ( + cellType: NodeType, + cellContent: Fragment | Node | readonly Node[] | null | undefined + ) => + cellContent + ? cellType.createChecked(null, cellContent) + : cellType.createAndFill(); + + for (let index = 0; index < colsCount; index += 1) { + const cell = createCell(types.cell, cellContent); + + if (cell) { + cells.push(cell); + } + + if (withHeaderRow) { + const headerCell = createCell(types.header_cell, cellContent); + + if (headerCell) { + headerCells.push(headerCell); + } + } + } + + for (let index = 0; index < rowsCount; index += 1) { + rows.push( + types.row.createChecked( + null, + withHeaderRow && index === 0 ? headerCells : cells + ) + ); + } + + return types.table.createChecked(null, rows); +} + +export function addRowAfterAndMoveSelection({ + index, +}: { + index?: number; +} = {}): Command { + return (state, dispatch) => { + if (!isInTable(state)) { + return false; + } + + if (dispatch) { + const rect = selectedRect(state); + const indexAfter = index !== undefined ? index + 1 : rect.bottom; + const tr = addRow(state.tr, rect, indexAfter); + const cells = getCellsInColumn(0)(state); + + // Special case when adding row to the end of the table as the calculated + // rect does not include the row that we just added. + if (indexAfter !== rect.map.height) { + const pos = cells[Math.min(cells.length - 1, indexAfter)]; + const $pos = tr.doc.resolve(pos); + dispatch(tr.setSelection(TextSelection.near($pos))); + } else { + const $pos = tr.doc.resolve(rect.tableStart + rect.table.nodeSize); + dispatch(tr.setSelection(TextSelection.near($pos))); + } + } + + return true; + }; +} + +export function setColumnAttr({ + index, + alignment, +}: { + index: number; + alignment: string; +}): Command { + return (state, dispatch) => { + if (dispatch) { + const cells = getCellsInColumn(index)(state) || []; + let transaction = state.tr; + cells.forEach((pos) => { + transaction = transaction.setNodeMarkup(pos, undefined, { + alignment, + }); + }); + dispatch(transaction); + } + return true; + }; +} + +export function selectRow(index: number, expand = false) { + return (state: EditorState): Transaction => { + const rect = selectedRect(state); + const pos = rect.map.positionAt(index, 0, rect.table); + const $pos = state.doc.resolve(rect.tableStart + pos); + const rowSelection = + expand && state.selection instanceof CellSelection + ? CellSelection.rowSelection(state.selection.$anchorCell, $pos) + : CellSelection.rowSelection($pos); + return state.tr.setSelection(rowSelection); + }; +} + +export function selectColumn(index: number, expand = false) { + return (state: EditorState): Transaction => { + const rect = selectedRect(state); + const pos = rect.map.positionAt(0, index, rect.table); + const $pos = state.doc.resolve(rect.tableStart + pos); + const colSelection = + expand && state.selection instanceof CellSelection + ? CellSelection.colSelection(state.selection.$anchorCell, $pos) + : CellSelection.colSelection($pos); + return state.tr.setSelection(colSelection); + }; +} + +export function selectTable(state: EditorState): Transaction { + const rect = selectedRect(state); + const map = rect.map.map; + const $anchor = state.doc.resolve(rect.tableStart + map[0]); + const $head = state.doc.resolve(rect.tableStart + map[map.length - 1]); + const tableSelection = new CellSelection($anchor, $head); + return state.tr.setSelection(tableSelection); +} diff --git a/shared/editor/commands/toggleBlockType.ts b/shared/editor/commands/toggleBlockType.ts index e7ed7c32a..2fd4d4b9c 100644 --- a/shared/editor/commands/toggleBlockType.ts +++ b/shared/editor/commands/toggleBlockType.ts @@ -1,15 +1,14 @@ import { setBlockType } from "prosemirror-commands"; import { NodeType } from "prosemirror-model"; -import { EditorState } from "prosemirror-state"; +import { Command } from "prosemirror-state"; import isNodeActive from "../queries/isNodeActive"; -import { Dispatch } from "../types"; export default function toggleBlockType( type: NodeType, toggleType: NodeType, attrs = {} -) { - return (state: EditorState, dispatch?: Dispatch) => { +): Command { + return (state, dispatch) => { const isActive = isNodeActive(type, attrs)(state); if (isActive) { diff --git a/shared/editor/commands/toggleList.ts b/shared/editor/commands/toggleList.ts index f0019a408..8658ad28f 100644 --- a/shared/editor/commands/toggleList.ts +++ b/shared/editor/commands/toggleList.ts @@ -1,14 +1,16 @@ import { NodeType } from "prosemirror-model"; import { wrapInList, liftListItem } from "prosemirror-schema-list"; -import { EditorState } from "prosemirror-state"; -import { findParentNode } from "prosemirror-utils"; +import { Command } from "prosemirror-state"; import chainTransactions from "../lib/chainTransactions"; +import { findParentNode } from "../queries/findParentNode"; import isList from "../queries/isList"; -import { Dispatch } from "../types"; import clearNodes from "./clearNodes"; -export default function toggleList(listType: NodeType, itemType: NodeType) { - return (state: EditorState, dispatch?: Dispatch) => { +export default function toggleList( + listType: NodeType, + itemType: NodeType +): Command { + return (state, dispatch) => { const { schema, selection } = state; const { $from, $to } = selection; const range = $from.blockRange($to); diff --git a/shared/editor/commands/toggleWrap.ts b/shared/editor/commands/toggleWrap.ts index ede336adc..d1e43b430 100644 --- a/shared/editor/commands/toggleWrap.ts +++ b/shared/editor/commands/toggleWrap.ts @@ -1,14 +1,13 @@ import { wrapIn, lift } from "prosemirror-commands"; import { NodeType } from "prosemirror-model"; -import { EditorState } from "prosemirror-state"; +import { Command } from "prosemirror-state"; import isNodeActive from "../queries/isNodeActive"; -import { Dispatch } from "../types"; export default function toggleWrap( type: NodeType, attrs?: Record -) { - return (state: EditorState, dispatch?: Dispatch) => { +): Command { + return (state, dispatch) => { const isActive = isNodeActive(type)(state); if (isActive) { diff --git a/shared/editor/components/Styles.ts b/shared/editor/components/Styles.ts index 9c01e2447..f0f9deaa0 100644 --- a/shared/editor/components/Styles.ts +++ b/shared/editor/components/Styles.ts @@ -1245,7 +1245,7 @@ table { * https://github.com/ProseMirror/prosemirror/issues/947 */ &::after { content: ""; - cursor: pointer; + cursor: var(--pointer); position: absolute; top: -16px; ${props.rtl ? "right" : "left"}: 0; @@ -1273,7 +1273,7 @@ table { .grip-row { &::after { content: ""; - cursor: pointer; + cursor: var(--pointer); position: absolute; ${props.rtl ? "right" : "left"}: -16px; top: 0; @@ -1302,7 +1302,7 @@ table { .grip-table { &::after { content: ""; - cursor: pointer; + cursor: var(--pointer); background: ${props.theme.tableDivider}; width: 13px; height: 13px; diff --git a/shared/editor/extensions/BlockMenu.tsx b/shared/editor/extensions/BlockMenu.tsx index 9ad40b926..f2173239a 100644 --- a/shared/editor/extensions/BlockMenu.tsx +++ b/shared/editor/extensions/BlockMenu.tsx @@ -1,10 +1,10 @@ import { PlusIcon } from "outline-icons"; import { Plugin } from "prosemirror-state"; -import { findParentNode } from "prosemirror-utils"; import { Decoration, DecorationSet } from "prosemirror-view"; import * as React from "react"; import ReactDOM from "react-dom"; import { SuggestionsMenuType } from "../plugins/Suggestions"; +import { findParentNode } from "../queries/findParentNode"; import { EventType } from "../types"; import Suggestion from "./Suggestion"; diff --git a/shared/editor/extensions/DateTime.ts b/shared/editor/extensions/DateTime.ts index 23debaae1..0e4f8919d 100644 --- a/shared/editor/extensions/DateTime.ts +++ b/shared/editor/extensions/DateTime.ts @@ -1,13 +1,13 @@ import { InputRule } from "prosemirror-inputrules"; import { Schema } from "prosemirror-model"; -import { EditorState } from "prosemirror-state"; +import { Command } from "prosemirror-state"; import { getCurrentDateAsString, getCurrentDateTimeAsString, getCurrentTimeAsString, } from "../../utils/date"; import Extension from "../lib/Extension"; -import { Dispatch, EventType } from "../types"; +import { EventType } from "../types"; /** * An editor extension that adds commands to insert the current date and time. @@ -43,16 +43,16 @@ export default class DateTime extends Extension { commands(_options: { schema: Schema }) { return { - date: () => (state: EditorState, dispatch: Dispatch) => { - dispatch(state.tr.insertText(getCurrentDateAsString() + " ")); + date: (): Command => (state, dispatch) => { + dispatch?.(state.tr.insertText(getCurrentDateAsString() + " ")); return true; }, - time: () => (state: EditorState, dispatch: Dispatch) => { - dispatch(state.tr.insertText(getCurrentTimeAsString() + " ")); + time: (): Command => (state, dispatch) => { + dispatch?.(state.tr.insertText(getCurrentTimeAsString() + " ")); return true; }, - datetime: () => (state: EditorState, dispatch: Dispatch) => { - dispatch(state.tr.insertText(getCurrentDateTimeAsString() + " ")); + datetime: (): Command => (state, dispatch) => { + dispatch?.(state.tr.insertText(getCurrentDateTimeAsString() + " ")); return true; }, }; diff --git a/shared/editor/extensions/Keys.ts b/shared/editor/extensions/Keys.ts index 267a162a7..6c66ce348 100644 --- a/shared/editor/extensions/Keys.ts +++ b/shared/editor/extensions/Keys.ts @@ -5,8 +5,9 @@ import { AllSelection, TextSelection, EditorState, + Command, } from "prosemirror-state"; -import Extension, { Command } from "../lib/Extension"; +import Extension from "../lib/Extension"; import isInCode from "../queries/isInCode"; export default class Keys extends Extension { diff --git a/shared/editor/extensions/Math.ts b/shared/editor/extensions/Math.ts index 22856d1f6..6d28b9bd5 100644 --- a/shared/editor/extensions/Math.ts +++ b/shared/editor/extensions/Math.ts @@ -1,7 +1,6 @@ import { MathView } from "@benrbray/prosemirror-math"; -import { Node as ProseNode } from "prosemirror-model"; import { Plugin, PluginKey, PluginSpec } from "prosemirror-state"; -import { EditorView } from "prosemirror-view"; +import { NodeViewConstructor } from "prosemirror-view"; export interface IMathPluginState { macros: { [cmd: string]: string }; @@ -11,12 +10,8 @@ export interface IMathPluginState { const MATH_PLUGIN_KEY = new PluginKey("prosemirror-math"); -export function createMathView(displayMode: boolean) { - return ( - node: ProseNode, - view: EditorView, - getPos: boolean | (() => number) - ): MathView => { +export function createMathView(displayMode: boolean): NodeViewConstructor { + return (node, view, getPos) => { // dynamically load katex styles and fonts import("katex/dist/katex.min.css"); diff --git a/shared/editor/extensions/Mermaid.ts b/shared/editor/extensions/Mermaid.ts index f181a56fe..c602f7d37 100644 --- a/shared/editor/extensions/Mermaid.ts +++ b/shared/editor/extensions/Mermaid.ts @@ -1,8 +1,8 @@ import { Node } from "prosemirror-model"; import { Plugin, PluginKey, Transaction } from "prosemirror-state"; -import { findBlockNodes } from "prosemirror-utils"; import { Decoration, DecorationSet } from "prosemirror-view"; import { v4 as uuidv4 } from "uuid"; +import { findBlockNodes } from "../queries/findChildren"; type MermaidState = { decorationSet: DecorationSet; @@ -135,7 +135,7 @@ export default function Mermaid({ return new Plugin({ key: new PluginKey("mermaid"), state: { - init: (_: Plugin, { doc }) => { + init: (_, { doc }) => { const pluginState: MermaidState = { decorationSet: DecorationSet.create(doc, []), diagramVisibility: {}, @@ -208,7 +208,7 @@ export default function Mermaid({ }, props: { decorations(state) { - return this.getState(state).decorationSet; + return this.getState(state)?.decorationSet; }, }, }); diff --git a/shared/editor/extensions/PasteHandler.ts b/shared/editor/extensions/PasteHandler.ts index a4da81a1c..e7cd1f859 100644 --- a/shared/editor/extensions/PasteHandler.ts +++ b/shared/editor/extensions/PasteHandler.ts @@ -165,6 +165,10 @@ export default class PasteHandler extends Extension { const paste = this.editor.pasteParser.parse( normalizePastedMarkdown(text) ); + if (!paste) { + return false; + } + const slice = paste.slice(0); const tr = view.state.tr; let currentPos = view.state.selection.from; diff --git a/shared/editor/extensions/PreventTab.ts b/shared/editor/extensions/PreventTab.ts index 86b9620fb..ef7953a66 100644 --- a/shared/editor/extensions/PreventTab.ts +++ b/shared/editor/extensions/PreventTab.ts @@ -1,4 +1,5 @@ -import Extension, { Command } from "../lib/Extension"; +import { Command } from "prosemirror-state"; +import Extension from "../lib/Extension"; export default class PreventTab extends Extension { get name() { diff --git a/shared/editor/extensions/Prism.ts b/shared/editor/extensions/Prism.ts index 8f34708f1..be82cd8a3 100644 --- a/shared/editor/extensions/Prism.ts +++ b/shared/editor/extensions/Prism.ts @@ -1,9 +1,9 @@ import { flattenDeep, padStart } from "lodash"; import { Node } from "prosemirror-model"; import { Plugin, PluginKey, Transaction } from "prosemirror-state"; -import { findBlockNodes } from "prosemirror-utils"; import { Decoration, DecorationSet } from "prosemirror-view"; import refractor from "refractor/core"; +import { findBlockNodes } from "../queries/findChildren"; export const LANGUAGES = { none: "None", // additional entry to disable highlighting @@ -75,18 +75,23 @@ function getDecorations({ function parseNodes( nodes: refractor.RefractorNode[], classNames: string[] = [] - ): any { - return nodes.map((node) => { - if (node.type === "element") { - const classes = [...classNames, ...(node.properties.className || [])]; - return parseNodes(node.children, classes); - } + ): { + text: string; + classes: string[]; + }[] { + return flattenDeep( + nodes.map((node) => { + if (node.type === "element") { + const classes = [...classNames, ...(node.properties.className || [])]; + return parseNodes(node.children, classes); + } - return { - text: node.value, - classes: classNames, - }; - }); + return { + text: node.value, + classes: classNames, + }; + }) + ); } blocks.forEach((block) => { @@ -125,7 +130,7 @@ function getDecorations({ } const nodes = refractor.highlight(block.node.textContent, language); - const newDecorations = flattenDeep(parseNodes(nodes)) + const newDecorations = parseNodes(nodes) .map((node: ParsedNode) => { const from = startPos; const to = from + node.text.length; @@ -180,7 +185,7 @@ export default function Prism({ return new Plugin({ key: new PluginKey("prism"), state: { - init: (_: Plugin, { doc }) => DecorationSet.create(doc, []), + init: (_, { doc }) => DecorationSet.create(doc, []), apply: (transaction: Transaction, decorationSet, oldState, state) => { const nodeName = state.selection.$head.parent.type.name; const previousNodeName = oldState.selection.$head.parent.type.name; diff --git a/shared/editor/lib/Extension.ts b/shared/editor/lib/Extension.ts index c30fdfc58..c20f3161a 100644 --- a/shared/editor/lib/Extension.ts +++ b/shared/editor/lib/Extension.ts @@ -1,16 +1,10 @@ import { PluginSimple } from "markdown-it"; import { InputRule } from "prosemirror-inputrules"; import { NodeType, MarkType, Schema } from "prosemirror-model"; -import { EditorState, Plugin } from "prosemirror-state"; -import { EditorView } from "prosemirror-view"; +import { Command, Plugin } from "prosemirror-state"; import { Editor } from "../../../app/editor"; -import { Dispatch } from "../types"; -export type Command = (state: EditorState, dispatch: Dispatch) => boolean; - -export type CommandFactory = ( - attrs?: Record -) => (state: EditorState, dispatch: Dispatch, view: EditorView) => boolean; +export type CommandFactory = (attrs?: Record) => Command; export default class Extension { options: any; diff --git a/shared/editor/lib/ExtensionManager.ts b/shared/editor/lib/ExtensionManager.ts index 06e7546df..ab7ce298e 100644 --- a/shared/editor/lib/ExtensionManager.ts +++ b/shared/editor/lib/ExtensionManager.ts @@ -1,6 +1,6 @@ import { PluginSimple } from "markdown-it"; import { keymap } from "prosemirror-keymap"; -import { MarkdownParser, TokenConfig } from "prosemirror-markdown"; +import { MarkdownParser } from "prosemirror-markdown"; import { Schema } from "prosemirror-model"; import { EditorView } from "prosemirror-view"; import { Editor } from "~/editor"; @@ -110,19 +110,19 @@ export default class ExtensionManager { rules?: Record; plugins?: PluginSimple[]; }): MarkdownParser { - const tokens: Record = this.extensions + const tokens = this.extensions .filter( (extension) => extension.type === "mark" || extension.type === "node" ) .reduce((nodes, extension: Node | Mark) => { - const md = extension.parseMarkdown(); - if (!md) { + const parseSpec = extension.parseMarkdown(); + if (!parseSpec) { return nodes; } return { ...nodes, - [extension.markdownToken || extension.name]: md, + [extension.markdownToken || extension.name]: parseSpec, }; }, {}); diff --git a/shared/editor/lib/chainTransactions.ts b/shared/editor/lib/chainTransactions.ts index e62c211f4..c23b7fe60 100644 --- a/shared/editor/lib/chainTransactions.ts +++ b/shared/editor/lib/chainTransactions.ts @@ -1,10 +1,7 @@ -import { EditorState, Transaction } from "prosemirror-state"; -import { Dispatch } from "../types"; +import { Command, Transaction } from "prosemirror-state"; -export default function chainTransactions( - ...commands: ((state: EditorState, dispatch?: Dispatch) => boolean)[] -) { - return (state: EditorState, dispatch?: Dispatch): boolean => { +export default function chainTransactions(...commands: Command[]): Command { + return (state, dispatch): boolean => { const dispatcher = (tr: Transaction): void => { state = state.apply(tr); dispatch?.(tr); diff --git a/shared/editor/lib/uploadPlaceholder.tsx b/shared/editor/lib/uploadPlaceholder.tsx index 07c910e97..359d0d8bf 100644 --- a/shared/editor/lib/uploadPlaceholder.tsx +++ b/shared/editor/lib/uploadPlaceholder.tsx @@ -95,7 +95,7 @@ export function findPlaceholder( state: EditorState, id: string ): [number, number] | null { - const decos: DecorationSet = uploadPlaceholder.getState(state); - const found = decos.find(undefined, undefined, (spec) => spec.id === id); - return found.length ? [found[0].from, found[0].to] : null; + const decos = uploadPlaceholder.getState(state); + const found = decos?.find(undefined, undefined, (spec) => spec.id === id); + return found?.length ? [found[0].from, found[0].to] : null; } diff --git a/shared/editor/marks/Comment.ts b/shared/editor/marks/Comment.ts index e7438fe2f..d967a9513 100644 --- a/shared/editor/marks/Comment.ts +++ b/shared/editor/marks/Comment.ts @@ -1,12 +1,10 @@ import { toggleMark } from "prosemirror-commands"; import { MarkSpec, MarkType, Schema } from "prosemirror-model"; -import { EditorState, Plugin } from "prosemirror-state"; +import { Command, Plugin } from "prosemirror-state"; import { v4 as uuidv4 } from "uuid"; import collapseSelection from "../commands/collapseSelection"; -import { Command } from "../lib/Extension"; import chainTransactions from "../lib/chainTransactions"; import isMarkActive from "../queries/isMarkActive"; -import { Dispatch } from "../types"; import Mark from "./Mark"; export default class Comment extends Mark { @@ -32,7 +30,7 @@ export default class Comment extends Mark { keys({ type }: { type: MarkType }): Record { return this.options.onCreateCommentMark ? { - "Mod-Alt-m": (state: EditorState, dispatch: Dispatch) => { + "Mod-Alt-m": (state, dispatch) => { if (isMarkActive(state.schema.marks.comment)(state)) { return false; } @@ -53,7 +51,7 @@ export default class Comment extends Mark { commands({ type }: { type: MarkType; schema: Schema }) { return this.options.onCreateCommentMark - ? () => (state: EditorState, dispatch: Dispatch) => { + ? (): Command => (state, dispatch) => { if (isMarkActive(state.schema.marks.comment)(state)) { return false; } diff --git a/shared/editor/marks/Italic.ts b/shared/editor/marks/Italic.ts index f6a679394..3c3bf0a30 100644 --- a/shared/editor/marks/Italic.ts +++ b/shared/editor/marks/Italic.ts @@ -1,7 +1,7 @@ import { toggleMark } from "prosemirror-commands"; import { InputRule } from "prosemirror-inputrules"; import { MarkSpec, MarkType } from "prosemirror-model"; -import { Command } from "../lib/Extension"; +import { Command } from "prosemirror-state"; import markInputRule from "../lib/markInputRule"; import Mark from "./Mark"; diff --git a/shared/editor/marks/Link.tsx b/shared/editor/marks/Link.tsx index 25a2a7926..864d060d2 100644 --- a/shared/editor/marks/Link.tsx +++ b/shared/editor/marks/Link.tsx @@ -9,7 +9,7 @@ import { Node, Mark as ProsemirrorMark, } from "prosemirror-model"; -import { EditorState, Plugin } from "prosemirror-state"; +import { Command, EditorState, Plugin } from "prosemirror-state"; import { Decoration, DecorationSet, EditorView } from "prosemirror-view"; import * as React from "react"; import ReactDOM from "react-dom"; @@ -17,7 +17,7 @@ import { isExternalUrl, sanitizeUrl } from "../../utils/urls"; import findLinkNodes from "../queries/findLinkNodes"; import getMarkRange from "../queries/getMarkRange"; import isMarkActive from "../queries/isMarkActive"; -import { EventType, Dispatch } from "../types"; +import { EventType } from "../types"; import Mark from "./Mark"; const LINK_INPUT_REGEX = /\[([^[]+)]\((\S+)\)$/; @@ -113,9 +113,9 @@ export default class Link extends Mark { ]; } - keys({ type }: { type: MarkType }) { + keys({ type }: { type: MarkType }): Record { return { - "Mod-k": (state: EditorState, dispatch: Dispatch) => { + "Mod-k": (state, dispatch) => { if (state.selection.empty) { this.editor.events.emit(EventType.LinkToolbarOpen); return true; @@ -123,7 +123,7 @@ export default class Link extends Mark { return toggleMark(type, { href: "" })(state, dispatch); }, - "Mod-Enter": (state: EditorState) => { + "Mod-Enter": (state) => { if (isMarkActive(type)(state)) { const range = getMarkRange( state.selection.$from, @@ -303,7 +303,7 @@ export default class Link extends Mark { ? ">" : "](" + state.esc(mark.attrs.href) + - (mark.attrs.title ? " " + state.quote(mark.attrs.title) : "") + + (mark.attrs.title ? " " + this.quote(mark.attrs.title) : "") + ")"; }, }; @@ -318,4 +318,10 @@ export default class Link extends Mark { }), }; } + + private quote(str: string) { + const wrap = + str.indexOf('"') === -1 ? '""' : str.indexOf("'") === -1 ? "''" : "()"; + return wrap[0] + str + wrap[1]; + } } diff --git a/shared/editor/marks/Mark.ts b/shared/editor/marks/Mark.ts index 8d55dcccf..69ad5dc27 100644 --- a/shared/editor/marks/Mark.ts +++ b/shared/editor/marks/Mark.ts @@ -1,13 +1,14 @@ import { toggleMark } from "prosemirror-commands"; import { InputRule } from "prosemirror-inputrules"; -import { TokenConfig } from "prosemirror-markdown"; +import { ParseSpec } from "prosemirror-markdown"; import { MarkSpec, MarkType, Node as ProsemirrorNode, Schema, } from "prosemirror-model"; -import Extension, { Command, CommandFactory } from "../lib/Extension"; +import { Command } from "prosemirror-state"; +import Extension, { CommandFactory } from "../lib/Extension"; import { MarkdownSerializerState } from "../lib/markdown/serializer"; export default abstract class Mark extends Extension { @@ -35,7 +36,7 @@ export default abstract class Mark extends Extension { throw new Error("toMarkdown not implemented"); } - parseMarkdown(): TokenConfig | void { + parseMarkdown(): ParseSpec | void { return undefined; } diff --git a/shared/editor/nodes/Blockquote.ts b/shared/editor/nodes/Blockquote.ts index 0dde3cedd..564f95f49 100644 --- a/shared/editor/nodes/Blockquote.ts +++ b/shared/editor/nodes/Blockquote.ts @@ -1,10 +1,9 @@ import { wrappingInputRule } from "prosemirror-inputrules"; import { NodeSpec, Node as ProsemirrorNode, NodeType } from "prosemirror-model"; -import { EditorState } from "prosemirror-state"; +import { Command } from "prosemirror-state"; import toggleWrap from "../commands/toggleWrap"; import { MarkdownSerializerState } from "../lib/markdown/serializer"; import isNodeActive from "../queries/isNodeActive"; -import { Dispatch } from "../types"; import Node from "./Node"; export default class Blockquote extends Node { @@ -34,17 +33,17 @@ export default class Blockquote extends Node { return () => toggleWrap(type); } - keys({ type }: { type: NodeType }) { + keys({ type }: { type: NodeType }): Record { return { "Ctrl->": toggleWrap(type), "Mod-]": toggleWrap(type), - "Shift-Enter": (state: EditorState, dispatch: Dispatch) => { + "Shift-Enter": (state, dispatch) => { if (!isNodeActive(type)(state)) { return false; } const { tr, selection } = state; - dispatch(tr.split(selection.to)); + dispatch?.(tr.split(selection.to)); return true; }, }; diff --git a/shared/editor/nodes/Embed.tsx b/shared/editor/nodes/Embed.tsx index f8b3bf9a4..4ed1f57fc 100644 --- a/shared/editor/nodes/Embed.tsx +++ b/shared/editor/nodes/Embed.tsx @@ -1,12 +1,12 @@ import Token from "markdown-it/lib/token"; import { NodeSpec, NodeType, Node as ProsemirrorNode } from "prosemirror-model"; -import { EditorState } from "prosemirror-state"; +import { Command } from "prosemirror-state"; import * as React from "react"; import { sanitizeUrl } from "../../utils/urls"; import DisabledEmbed from "../components/DisabledEmbed"; import { MarkdownSerializerState } from "../lib/markdown/serializer"; import embedsRule from "../rules/embeds"; -import { ComponentProps, Dispatch } from "../types"; +import { ComponentProps } from "../types"; import Node from "./Node"; const cache = {}; @@ -114,9 +114,9 @@ export default class Embed extends Node { } commands({ type }: { type: NodeType }) { - return (attrs: Record) => - (state: EditorState, dispatch: Dispatch) => { - dispatch( + return (attrs: Record): Command => + (state, dispatch) => { + dispatch?.( state.tr.replaceSelectionWith(type.create(attrs)).scrollIntoView() ); return true; diff --git a/shared/editor/nodes/Emoji.tsx b/shared/editor/nodes/Emoji.tsx index 18ad791e1..d432e31bd 100644 --- a/shared/editor/nodes/Emoji.tsx +++ b/shared/editor/nodes/Emoji.tsx @@ -6,12 +6,11 @@ import { NodeType, Schema, } from "prosemirror-model"; -import { EditorState, TextSelection } from "prosemirror-state"; +import { Command, TextSelection } from "prosemirror-state"; import Suggestion from "../extensions/Suggestion"; import { MarkdownSerializerState } from "../lib/markdown/serializer"; import { SuggestionsMenuType } from "../plugins/Suggestions"; import emojiRule from "../rules/emoji"; -import { Dispatch } from "../types"; export default class Emoji extends Suggestion { get type() { @@ -78,8 +77,8 @@ export default class Emoji extends Suggestion { } commands({ type }: { type: NodeType; schema: Schema }) { - return (attrs: Record) => - (state: EditorState, dispatch: Dispatch) => { + return (attrs: Record): Command => + (state, dispatch) => { const { selection } = state; const position = selection instanceof TextSelection @@ -91,7 +90,7 @@ export default class Emoji extends Suggestion { const node = type.create(attrs); const transaction = state.tr.insert(position, node); - dispatch(transaction); + dispatch?.(transaction); return true; }; } diff --git a/shared/editor/nodes/HardBreak.ts b/shared/editor/nodes/HardBreak.ts index 46d04de9d..03feed5de 100644 --- a/shared/editor/nodes/HardBreak.ts +++ b/shared/editor/nodes/HardBreak.ts @@ -1,10 +1,9 @@ import { NodeSpec, NodeType } from "prosemirror-model"; -import { EditorState } from "prosemirror-state"; +import { Command } from "prosemirror-state"; import { isInTable } from "prosemirror-tables"; import { MarkdownSerializerState } from "../lib/markdown/serializer"; import isNodeActive from "../queries/isNodeActive"; import breakRule from "../rules/breaks"; -import { Dispatch } from "../types"; import Node from "./Node"; export default class HardBreak extends Node { @@ -28,22 +27,24 @@ export default class HardBreak extends Node { } commands({ type }: { type: NodeType }) { - return () => (state: EditorState, dispatch: Dispatch) => { - dispatch(state.tr.replaceSelectionWith(type.create()).scrollIntoView()); + return (): Command => (state, dispatch) => { + dispatch?.(state.tr.replaceSelectionWith(type.create()).scrollIntoView()); return true; }; } - keys({ type }: { type: NodeType }) { + keys({ type }: { type: NodeType }): Record { return { - "Shift-Enter": (state: EditorState, dispatch: Dispatch) => { + "Shift-Enter": (state, dispatch) => { if ( !isInTable(state) && !isNodeActive(state.schema.nodes.paragraph)(state) ) { return false; } - dispatch(state.tr.replaceSelectionWith(type.create()).scrollIntoView()); + dispatch?.( + state.tr.replaceSelectionWith(type.create()).scrollIntoView() + ); return true; }, }; diff --git a/shared/editor/nodes/Heading.ts b/shared/editor/nodes/Heading.ts index e624e5fcd..323b229ff 100644 --- a/shared/editor/nodes/Heading.ts +++ b/shared/editor/nodes/Heading.ts @@ -6,13 +6,12 @@ import { NodeType, Schema, } from "prosemirror-model"; -import { Plugin, Selection } from "prosemirror-state"; +import { Command, Plugin, Selection } from "prosemirror-state"; import { Decoration, DecorationSet } from "prosemirror-view"; import Storage from "../../utils/Storage"; import backspaceToParagraph from "../commands/backspaceToParagraph"; import splitHeading from "../commands/splitHeading"; import toggleBlockType from "../commands/toggleBlockType"; -import { Command } from "../lib/Extension"; import headingToSlug, { headingToPersistenceKey } from "../lib/headingToSlug"; import { MarkdownSerializerState } from "../lib/markdown/serializer"; import { FoldingHeadersPlugin } from "../plugins/FoldingHeaders"; @@ -119,7 +118,10 @@ export default class Heading extends Node { handleFoldContent = (event: MouseEvent) => { event.preventDefault(); - if (!(event.currentTarget instanceof HTMLButtonElement)) { + if ( + !(event.currentTarget instanceof HTMLButtonElement) || + event.button !== 0 + ) { return; } diff --git a/shared/editor/nodes/HorizontalRule.ts b/shared/editor/nodes/HorizontalRule.ts index fe1d907f5..dc8b87dd1 100644 --- a/shared/editor/nodes/HorizontalRule.ts +++ b/shared/editor/nodes/HorizontalRule.ts @@ -1,9 +1,8 @@ import Token from "markdown-it/lib/token"; import { InputRule } from "prosemirror-inputrules"; import { NodeSpec, NodeType, Node as ProsemirrorNode } from "prosemirror-model"; -import { EditorState } from "prosemirror-state"; +import { Command } from "prosemirror-state"; import { MarkdownSerializerState } from "../lib/markdown/serializer"; -import { Dispatch } from "../types"; import Node from "./Node"; export default class HorizontalRule extends Node { @@ -28,19 +27,21 @@ export default class HorizontalRule extends Node { } commands({ type }: { type: NodeType }) { - return (attrs: Record) => - (state: EditorState, dispatch: Dispatch) => { - dispatch( + return (attrs: Record): Command => + (state, dispatch) => { + dispatch?.( state.tr.replaceSelectionWith(type.create(attrs)).scrollIntoView() ); return true; }; } - keys({ type }: { type: NodeType }) { + keys({ type }: { type: NodeType }): Record { return { - "Mod-_": (state: EditorState, dispatch: Dispatch) => { - dispatch(state.tr.replaceSelectionWith(type.create()).scrollIntoView()); + "Mod-_": (state, dispatch) => { + dispatch?.( + state.tr.replaceSelectionWith(type.create()).scrollIntoView() + ); return true; }, }; diff --git a/shared/editor/nodes/Image.tsx b/shared/editor/nodes/Image.tsx index 798db3135..356f1ed94 100644 --- a/shared/editor/nodes/Image.tsx +++ b/shared/editor/nodes/Image.tsx @@ -1,12 +1,12 @@ import Token from "markdown-it/lib/token"; import { InputRule } from "prosemirror-inputrules"; import { Node as ProsemirrorNode, NodeSpec, NodeType } from "prosemirror-model"; -import { NodeSelection, EditorState, Plugin } from "prosemirror-state"; +import { NodeSelection, Plugin, Command } from "prosemirror-state"; import * as React from "react"; import { sanitizeUrl } from "../../utils/urls"; import { default as ImageComponent, Caption } from "../components/Image"; import { MarkdownSerializerState } from "../lib/markdown/serializer"; -import { ComponentProps, Dispatch } from "../types"; +import { ComponentProps } from "../types"; import SimpleImage from "./SimpleImage"; const imageSizeRegex = /\s=(\d+)?x(\d+)?$/; @@ -282,7 +282,7 @@ export default class Image extends SimpleImage { commands({ type }: { type: NodeType }) { return { ...super.commands({ type }), - downloadImage: () => (state: EditorState) => { + downloadImage: (): Command => (state) => { if (!(state.selection instanceof NodeSelection)) { return false; } @@ -296,7 +296,7 @@ export default class Image extends SimpleImage { return true; }, - alignRight: () => (state: EditorState, dispatch: Dispatch) => { + alignRight: (): Command => (state, dispatch) => { if (!(state.selection instanceof NodeSelection)) { return false; } @@ -306,10 +306,10 @@ export default class Image extends SimpleImage { layoutClass: "right-50", }; const { selection } = state; - dispatch(state.tr.setNodeMarkup(selection.from, undefined, attrs)); + dispatch?.(state.tr.setNodeMarkup(selection.from, undefined, attrs)); return true; }, - alignLeft: () => (state: EditorState, dispatch: Dispatch) => { + alignLeft: (): Command => (state, dispatch) => { if (!(state.selection instanceof NodeSelection)) { return false; } @@ -319,10 +319,10 @@ export default class Image extends SimpleImage { layoutClass: "left-50", }; const { selection } = state; - dispatch(state.tr.setNodeMarkup(selection.from, undefined, attrs)); + dispatch?.(state.tr.setNodeMarkup(selection.from, undefined, attrs)); return true; }, - alignFullWidth: () => (state: EditorState, dispatch: Dispatch) => { + alignFullWidth: (): Command => (state, dispatch) => { if (!(state.selection instanceof NodeSelection)) { return false; } @@ -332,16 +332,16 @@ export default class Image extends SimpleImage { layoutClass: "full-width", }; const { selection } = state; - dispatch(state.tr.setNodeMarkup(selection.from, undefined, attrs)); + dispatch?.(state.tr.setNodeMarkup(selection.from, undefined, attrs)); return true; }, - alignCenter: () => (state: EditorState, dispatch: Dispatch) => { + alignCenter: (): Command => (state, dispatch) => { if (!(state.selection instanceof NodeSelection)) { return false; } const attrs = { ...state.selection.node.attrs, layoutClass: null }; const { selection } = state; - dispatch(state.tr.setNodeMarkup(selection.from, undefined, attrs)); + dispatch?.(state.tr.setNodeMarkup(selection.from, undefined, attrs)); return true; }, }; diff --git a/shared/editor/nodes/ListItem.ts b/shared/editor/nodes/ListItem.ts index cc720c38b..e2946635f 100644 --- a/shared/editor/nodes/ListItem.ts +++ b/shared/editor/nodes/ListItem.ts @@ -9,14 +9,14 @@ import { EditorState, Plugin, TextSelection, + Command, } from "prosemirror-state"; -import { findParentNodeClosestToPos } from "prosemirror-utils"; import { DecorationSet, Decoration } from "prosemirror-view"; import { MarkdownSerializerState } from "../lib/markdown/serializer"; +import { findParentNodeClosestToPos } from "../queries/findParentNode"; import getParentListItem from "../queries/getParentListItem"; import isInList from "../queries/isInList"; import isList from "../queries/isList"; -import { Dispatch } from "../types"; import Node from "./Node"; export default class ListItem extends Node { @@ -200,14 +200,14 @@ export default class ListItem extends Node { }; } - keys({ type }: { type: NodeType }) { + keys({ type }: { type: NodeType }): Record { return { Enter: splitListItem(type), Tab: sinkListItem(type), "Shift-Tab": liftListItem(type), "Mod-]": sinkListItem(type), "Mod-[": liftListItem(type), - "Shift-Enter": (state: EditorState, dispatch: Dispatch) => { + "Shift-Enter": (state, dispatch) => { if (!isInList(state)) { return false; } @@ -216,10 +216,10 @@ export default class ListItem extends Node { } const { tr, selection } = state; - dispatch(tr.split(selection.to)); + dispatch?.(tr.split(selection.to)); return true; }, - "Alt-ArrowUp": (state: EditorState, dispatch: Dispatch) => { + "Alt-ArrowUp": (state, dispatch) => { if (!state.selection.empty) { return false; } @@ -241,7 +241,7 @@ export default class ListItem extends Node { const { tr } = state; const newPos = pos - $pos.nodeBefore.nodeSize; - dispatch( + dispatch?.( tr .delete(pos, pos + li.nodeSize) .insert(newPos, li) @@ -249,7 +249,7 @@ export default class ListItem extends Node { ); return true; }, - "Alt-ArrowDown": (state: EditorState, dispatch: Dispatch) => { + "Alt-ArrowDown": (state, dispatch) => { if (!state.selection.empty) { return false; } @@ -271,7 +271,7 @@ export default class ListItem extends Node { const { tr } = state; const newPos = pos + li.nodeSize + $pos.nodeAfter.nodeSize; - dispatch( + dispatch?.( tr .insert(newPos, li) .setSelection(TextSelection.near(tr.doc.resolve(newPos))) diff --git a/shared/editor/nodes/Math.ts b/shared/editor/nodes/Math.ts index 08cc6f0fb..b9183f33a 100644 --- a/shared/editor/nodes/Math.ts +++ b/shared/editor/nodes/Math.ts @@ -17,11 +17,10 @@ import { Schema, Node as ProsemirrorNode, } from "prosemirror-model"; -import { EditorState, Plugin } from "prosemirror-state"; +import { Command, Plugin } from "prosemirror-state"; import MathPlugin from "../extensions/Math"; import { MarkdownSerializerState } from "../lib/markdown/serializer"; import mathRule, { REGEX_INLINE_MATH_DOLLARS } from "../rules/math"; -import { Dispatch } from "../types"; import Node from "./Node"; export default class Math extends Node { @@ -34,8 +33,8 @@ export default class Math extends Node { } commands({ type }: { type: NodeType }) { - return () => (state: EditorState, dispatch: Dispatch) => { - dispatch(state.tr.replaceSelectionWith(type.create()).scrollIntoView()); + return (): Command => (state, dispatch) => { + dispatch?.(state.tr.replaceSelectionWith(type.create()).scrollIntoView()); return true; }; } diff --git a/shared/editor/nodes/MathBlock.ts b/shared/editor/nodes/MathBlock.ts index ea2042b87..948e82d91 100644 --- a/shared/editor/nodes/MathBlock.ts +++ b/shared/editor/nodes/MathBlock.ts @@ -4,10 +4,9 @@ import { } from "@benrbray/prosemirror-math"; import { PluginSimple } from "markdown-it"; import { NodeSpec, NodeType, Node as ProsemirrorNode } from "prosemirror-model"; -import { EditorState } from "prosemirror-state"; +import { Command } from "prosemirror-state"; import { MarkdownSerializerState } from "../lib/markdown/serializer"; import mathRule, { REGEX_BLOCK_MATH_DOLLARS } from "../rules/math"; -import { Dispatch } from "../types"; import Node from "./Node"; export default class MathBlock extends Node { @@ -24,8 +23,8 @@ export default class MathBlock extends Node { } commands({ type }: { type: NodeType }) { - return () => (state: EditorState, dispatch: Dispatch) => { - dispatch(state.tr.replaceSelectionWith(type.create()).scrollIntoView()); + return (): Command => (state, dispatch) => { + dispatch?.(state.tr.replaceSelectionWith(type.create()).scrollIntoView()); return true; }; } diff --git a/shared/editor/nodes/Mention.ts b/shared/editor/nodes/Mention.ts index 71ebf71d3..0b0850648 100644 --- a/shared/editor/nodes/Mention.ts +++ b/shared/editor/nodes/Mention.ts @@ -5,12 +5,11 @@ import { NodeType, Schema, } from "prosemirror-model"; -import { EditorState, TextSelection } from "prosemirror-state"; +import { Command, TextSelection } from "prosemirror-state"; import Suggestion from "../extensions/Suggestion"; import { MarkdownSerializerState } from "../lib/markdown/serializer"; import { SuggestionsMenuType } from "../plugins/Suggestions"; import mentionRule from "../rules/mention"; -import { Dispatch } from "../types"; export default class Mention extends Suggestion { get type() { @@ -80,8 +79,8 @@ export default class Mention extends Suggestion { } commands({ type }: { type: NodeType; schema: Schema }) { - return (attrs: Record) => - (state: EditorState, dispatch: Dispatch) => { + return (attrs: Record): Command => + (state, dispatch) => { const { selection } = state; const position = selection instanceof TextSelection @@ -93,7 +92,7 @@ export default class Mention extends Suggestion { const node = type.create(attrs); const transaction = state.tr.insert(position, node); - dispatch(transaction); + dispatch?.(transaction); return true; }; } diff --git a/shared/editor/nodes/Node.ts b/shared/editor/nodes/Node.ts index 11dd755b5..faf03766c 100644 --- a/shared/editor/nodes/Node.ts +++ b/shared/editor/nodes/Node.ts @@ -1,12 +1,13 @@ import { InputRule } from "prosemirror-inputrules"; -import { TokenConfig } from "prosemirror-markdown"; +import { ParseSpec } from "prosemirror-markdown"; import { NodeSpec, Node as ProsemirrorNode, NodeType, Schema, } from "prosemirror-model"; -import Extension, { Command, CommandFactory } from "../lib/Extension"; +import { Command } from "prosemirror-state"; +import Extension, { CommandFactory } from "../lib/Extension"; import { MarkdownSerializerState } from "../lib/markdown/serializer"; export default abstract class Node extends Extension { @@ -41,7 +42,7 @@ export default abstract class Node extends Extension { throw new Error("toMarkdown not implemented"); } - parseMarkdown(): TokenConfig | void { + parseMarkdown(): ParseSpec | void { return undefined; } } diff --git a/shared/editor/nodes/Paragraph.ts b/shared/editor/nodes/Paragraph.ts index ac9de1cc8..a0b2b22ef 100644 --- a/shared/editor/nodes/Paragraph.ts +++ b/shared/editor/nodes/Paragraph.ts @@ -1,5 +1,6 @@ import { setBlockType } from "prosemirror-commands"; import { NodeSpec, NodeType, Node as ProsemirrorNode } from "prosemirror-model"; +import deleteEmptyFirstParagraph from "../commands/deleteEmptyFirstParagraph"; import { MarkdownSerializerState } from "../lib/markdown/serializer"; import Node from "./Node"; @@ -20,6 +21,7 @@ export default class Paragraph extends Node { keys({ type }: { type: NodeType }) { return { "Shift-Ctrl-0": setBlockType(type), + Backspace: deleteEmptyFirstParagraph, }; } diff --git a/shared/editor/nodes/SimpleImage.tsx b/shared/editor/nodes/SimpleImage.tsx index e9038513d..f0748daa3 100644 --- a/shared/editor/nodes/SimpleImage.tsx +++ b/shared/editor/nodes/SimpleImage.tsx @@ -1,7 +1,7 @@ import Token from "markdown-it/lib/token"; import { InputRule } from "prosemirror-inputrules"; import { Node as ProsemirrorNode, NodeSpec, NodeType } from "prosemirror-model"; -import { TextSelection, NodeSelection, EditorState } from "prosemirror-state"; +import { TextSelection, NodeSelection, Command } from "prosemirror-state"; import * as React from "react"; import { getEventFiles } from "../../utils/files"; import { sanitizeUrl } from "../../utils/urls"; @@ -11,7 +11,7 @@ import { default as ImageComponent } from "../components/Image"; import { MarkdownSerializerState } from "../lib/markdown/serializer"; import uploadPlaceholderPlugin from "../lib/uploadPlaceholder"; import uploadPlugin from "../lib/uploadPlugin"; -import { ComponentProps, Dispatch } from "../types"; +import { ComponentProps } from "../types"; import Node from "./Node"; export default class SimpleImage extends Node { @@ -171,11 +171,11 @@ export default class SimpleImage extends Node { commands({ type }: { type: NodeType }) { return { - deleteImage: () => (state: EditorState, dispatch: Dispatch) => { - dispatch(state.tr.deleteSelection()); + deleteImage: (): Command => (state, dispatch) => { + dispatch?.(state.tr.deleteSelection()); return true; }, - replaceImage: () => (state: EditorState) => { + replaceImage: (): Command => (state) => { if (!(state.selection instanceof NodeSelection)) { return false; } @@ -214,8 +214,8 @@ export default class SimpleImage extends Node { return true; }, createImage: - (attrs: Record) => - (state: EditorState, dispatch: Dispatch) => { + (attrs: Record): Command => + (state, dispatch) => { const { selection } = state; const position = selection instanceof TextSelection @@ -227,7 +227,7 @@ export default class SimpleImage extends Node { const node = type.create(attrs); const transaction = state.tr.insert(position, node); - dispatch(transaction); + dispatch?.(transaction); return true; }, }; diff --git a/shared/editor/nodes/Table.ts b/shared/editor/nodes/Table.ts index 991201007..2ff4bc68c 100644 --- a/shared/editor/nodes/Table.ts +++ b/shared/editor/nodes/Table.ts @@ -1,5 +1,6 @@ -import { NodeSpec, Node as ProsemirrorNode, Schema } from "prosemirror-model"; -import { EditorState, Plugin, TextSelection } from "prosemirror-state"; +import { chainCommands } from "prosemirror-commands"; +import { NodeSpec, Node as ProsemirrorNode } from "prosemirror-model"; +import { Command, Plugin, TextSelection } from "prosemirror-state"; import { addColumnAfter, addColumnBefore, @@ -7,25 +8,19 @@ import { deleteRow, deleteTable, goToNextCell, - isInTable, tableEditing, toggleHeaderCell, toggleHeaderColumn, toggleHeaderRow, - CellSelection, } from "prosemirror-tables"; -import { - addRowAt, - createTable, - getCellsInColumn, - moveRow, - setTextSelection, -} from "prosemirror-utils"; import { Decoration, DecorationSet } from "prosemirror-view"; +import { + addRowAfterAndMoveSelection, + setColumnAttr, + createTable, +} from "../commands/table"; import { MarkdownSerializerState } from "../lib/markdown/serializer"; -import { getRowIndexFromText } from "../queries/getRowIndex"; import tablesRule from "../rules/tables"; -import { Dispatch } from "../types"; import Node from "./Node"; export default class Table extends Node { @@ -58,49 +53,32 @@ export default class Table extends Node { return [tablesRule]; } - commands({ schema }: { schema: Schema }) { + commands() { return { createTable: - ({ rowsCount, colsCount }: { rowsCount: number; colsCount: number }) => - (state: EditorState, dispatch: Dispatch) => { - const offset = state.tr.selection.anchor + 1; - const nodes = createTable(schema, rowsCount, colsCount); - const tr = state.tr.replaceSelectionWith(nodes).scrollIntoView(); - const resolvedPos = tr.doc.resolve(offset); - - tr.setSelection(TextSelection.near(resolvedPos)); - dispatch(tr); - return true; - }, - setColumnAttr: - ({ index, alignment }: { index: number; alignment: string }) => - (state: EditorState, dispatch: Dispatch) => { - const cells = getCellsInColumn(index)(state.selection) || []; - let transaction = state.tr; - cells.forEach(({ pos }) => { - transaction = transaction.setNodeMarkup(pos, undefined, { - alignment, - }); - }); - dispatch(transaction); - return true; - }, - addColumnBefore: () => addColumnBefore, - addColumnAfter: () => addColumnAfter, - deleteColumn: () => deleteColumn, - addRowAfter: - ({ index }: { index: number }) => - (state: EditorState, dispatch: Dispatch) => { - if (index === 0) { - // A little hack to avoid cloning the heading row by cloning the row - // beneath and then moving it to the right index. - const tr = addRowAt(index + 2, true)(state.tr); - dispatch(moveRow(index + 2, index + 1)(tr)); - } else { - dispatch(addRowAt(index + 1, true)(state.tr)); + ({ + rowsCount, + colsCount, + }: { + rowsCount: number; + colsCount: number; + }): Command => + (state, dispatch) => { + if (dispatch) { + const offset = state.tr.selection.anchor + 1; + const nodes = createTable(state, rowsCount, colsCount); + const tr = state.tr.replaceSelectionWith(nodes).scrollIntoView(); + const resolvedPos = tr.doc.resolve(offset); + tr.setSelection(TextSelection.near(resolvedPos)); + dispatch(tr); } return true; }, + setColumnAttr, + addColumnBefore: () => addColumnBefore, + addColumnAfter: () => addColumnAfter, + deleteColumn: () => deleteColumn, + addRowAfter: addRowAfterAndMoveSelection, deleteRow: () => deleteRow, deleteTable: () => deleteTable, toggleHeaderColumn: () => toggleHeaderColumn, @@ -111,31 +89,9 @@ export default class Table extends Node { keys() { return { - Tab: goToNextCell(1), + Tab: chainCommands(goToNextCell(1), addRowAfterAndMoveSelection()), "Shift-Tab": goToNextCell(-1), - Enter: (state: EditorState, dispatch: Dispatch) => { - if (!isInTable(state)) { - return false; - } - const index = getRowIndexFromText( - state.selection as unknown as CellSelection - ); - - if (index === 0) { - const cells = getCellsInColumn(0)(state.selection); - if (!cells) { - return false; - } - - const tr = addRowAt(index + 2, true)(state.tr); - dispatch( - setTextSelection(cells[1].pos)(moveRow(index + 2, index + 1)(tr)) - ); - } else { - dispatch(addRowAt(index + 1, true)(state.tr)); - } - return true; - }, + Enter: addRowAfterAndMoveSelection(), }; } diff --git a/shared/editor/nodes/TableCell.ts b/shared/editor/nodes/TableCell.ts index 271ed3ec9..1f66c7ea2 100644 --- a/shared/editor/nodes/TableCell.ts +++ b/shared/editor/nodes/TableCell.ts @@ -1,14 +1,13 @@ import Token from "markdown-it/lib/token"; import { NodeSpec } from "prosemirror-model"; import { Plugin } from "prosemirror-state"; -import { - isTableSelected, - isRowSelected, - getCellsInColumn, - selectRow, - selectTable, -} from "prosemirror-utils"; import { DecorationSet, Decoration } from "prosemirror-view"; +import { selectRow, selectTable } from "../commands/table"; +import { + getCellsInColumn, + isRowSelected, + isTableSelected, +} from "../queries/table"; import Node from "./Node"; export default class TableCell extends Node { @@ -55,17 +54,17 @@ export default class TableCell extends Node { new Plugin({ props: { decorations: (state) => { - const { doc, selection } = state; + const { doc } = state; const decorations: Decoration[] = []; - const cells = getCellsInColumn(0)(selection); + const cells = getCellsInColumn(0)(state); if (cells) { - cells.forEach(({ pos }, index) => { + cells.forEach((pos, index) => { if (index === 0) { decorations.push( Decoration.widget(pos + 1, () => { let className = "grip-table"; - const selected = isTableSelected(selection); + const selected = isTableSelected(state); if (selected) { className += " selected"; } @@ -74,7 +73,7 @@ export default class TableCell extends Node { grip.addEventListener("mousedown", (event) => { event.preventDefault(); event.stopImmediatePropagation(); - this.editor.view.dispatch(selectTable(state.tr)); + this.editor.view.dispatch(selectTable(state)); }); return grip; }) @@ -82,7 +81,7 @@ export default class TableCell extends Node { } decorations.push( Decoration.widget(pos + 1, () => { - const rowSelected = isRowSelected(index)(selection); + const rowSelected = isRowSelected(index)(state); let className = "grip-row"; if (rowSelected) { @@ -100,10 +99,7 @@ export default class TableCell extends Node { event.preventDefault(); event.stopImmediatePropagation(); this.editor.view.dispatch( - selectRow( - index, - event.metaKey || event.shiftKey - )(state.tr) + selectRow(index, event.metaKey || event.shiftKey)(state) ); }); return grip; diff --git a/shared/editor/nodes/TableHeadCell.ts b/shared/editor/nodes/TableHeadCell.ts index 56cb1a719..50e1427bc 100644 --- a/shared/editor/nodes/TableHeadCell.ts +++ b/shared/editor/nodes/TableHeadCell.ts @@ -1,12 +1,10 @@ import Token from "markdown-it/lib/token"; import { NodeSpec } from "prosemirror-model"; import { Plugin } from "prosemirror-state"; -import { - isColumnSelected, - getCellsInRow, - selectColumn, -} from "prosemirror-utils"; import { DecorationSet, Decoration } from "prosemirror-view"; +import { selectColumn } from "../commands/table"; +import { getCellsInRow, isColumnSelected } from "../queries/table"; + import Node from "./Node"; export default class TableHeadCell extends Node { @@ -53,15 +51,15 @@ export default class TableHeadCell extends Node { new Plugin({ props: { decorations: (state) => { - const { doc, selection } = state; + const { doc } = state; const decorations: Decoration[] = []; - const cells = getCellsInRow(0)(selection); + const cells = getCellsInRow(0)(state); if (cells) { - cells.forEach(({ pos }, index) => { + cells.forEach((pos, index) => { decorations.push( Decoration.widget(pos + 1, () => { - const colSelected = isColumnSelected(index)(selection); + const colSelected = isColumnSelected(index)(state); let className = "grip-column"; if (colSelected) { className += " selected"; @@ -80,7 +78,7 @@ export default class TableHeadCell extends Node { selectColumn( index, event.metaKey || event.shiftKey - )(state.tr) + )(state) ); }); return grip; diff --git a/shared/editor/plugins/FoldingHeaders.ts b/shared/editor/plugins/FoldingHeaders.ts index 63be4d934..edf7a8690 100644 --- a/shared/editor/plugins/FoldingHeaders.ts +++ b/shared/editor/plugins/FoldingHeaders.ts @@ -1,8 +1,8 @@ import { Plugin, PluginKey } from "prosemirror-state"; -import { findBlockNodes } from "prosemirror-utils"; import { Decoration, DecorationSet } from "prosemirror-view"; import Storage from "../../utils/Storage"; import { headingToPersistenceKey } from "../lib/headingToSlug"; +import { findBlockNodes } from "../queries/findChildren"; import findCollapsedNodes from "../queries/findCollapsedNodes"; export class FoldingHeadersPlugin extends Plugin { diff --git a/shared/editor/queries/findChildren.ts b/shared/editor/queries/findChildren.ts new file mode 100644 index 000000000..da2717e2b --- /dev/null +++ b/shared/editor/queries/findChildren.ts @@ -0,0 +1,57 @@ +import { Node } from "prosemirror-model"; + +type Predicate = (node: Node) => boolean; + +export type NodeWithPos = { + pos: number; + node: Node; +}; + +export function flatten(node: Node, descend = true): NodeWithPos[] { + if (!node) { + throw new Error('Invalid "node" parameter'); + } + const result: NodeWithPos[] = []; + node.descendants((child, pos) => { + result.push({ node: child, pos }); + if (!descend) { + return false; + } + return undefined; + }); + return result; +} + +/** + * Iterates over descendants of a given `node`, returning child nodes predicate + * returns truthy for. It doesn't descend into a node when descend argument is + * `false` (defaults to `true`). + * + * @param node The node to iterate over + * @param predicate Filtering predicate function + * @param descend Whether to descend into a node + * @returns Child nodes + */ +export function findChildren( + node: Node, + predicate: Predicate, + descend = false +) { + if (!node) { + throw new Error('Invalid "node" parameter'); + } else if (!predicate) { + throw new Error('Invalid "predicate" parameter'); + } + return flatten(node, descend).filter((child) => predicate(child.node)); +} + +/** + * Iterates over descendants of a given `node`, returning child nodes that + * are blocks. + * + * @param node The node to iterate over + * @returns Child nodes that are blocks + */ +export function findBlockNodes(node: Node): NodeWithPos[] { + return findChildren(node, (child) => child.isBlock); +} diff --git a/shared/editor/queries/findCollapsedNodes.ts b/shared/editor/queries/findCollapsedNodes.ts index c9191b561..fec130cc7 100644 --- a/shared/editor/queries/findCollapsedNodes.ts +++ b/shared/editor/queries/findCollapsedNodes.ts @@ -1,5 +1,5 @@ import { Node } from "prosemirror-model"; -import { findBlockNodes, NodeWithPos } from "prosemirror-utils"; +import { findBlockNodes, NodeWithPos } from "./findChildren"; export default function findCollapsedNodes(doc: Node): NodeWithPos[] { const blocks = findBlockNodes(doc); diff --git a/shared/editor/queries/findLinkNodes.ts b/shared/editor/queries/findLinkNodes.ts index d80fe7cf9..a1de32928 100644 --- a/shared/editor/queries/findLinkNodes.ts +++ b/shared/editor/queries/findLinkNodes.ts @@ -1,8 +1,8 @@ import { Node } from "prosemirror-model"; -import { findTextNodes, NodeWithPos } from "prosemirror-utils"; +import { findChildren, NodeWithPos } from "./findChildren"; export default function findLinkNodes(doc: Node): NodeWithPos[] { - const textNodes = findTextNodes(doc); + const textNodes = findChildren(doc, (child) => child.isText); const nodes: NodeWithPos[] = []; for (const nodeWithPos of textNodes) { diff --git a/shared/editor/queries/findParentNode.ts b/shared/editor/queries/findParentNode.ts new file mode 100644 index 000000000..39e38048b --- /dev/null +++ b/shared/editor/queries/findParentNode.ts @@ -0,0 +1,44 @@ +import { Node, ResolvedPos } from "prosemirror-model"; +import { Selection } from "prosemirror-state"; + +type Predicate = (node: Node) => boolean; + +type ContentNodeWithPos = { + pos: number; + start: number; + depth: number; + node: Node; +}; + +export const findParentNode = + (predicate: Predicate) => + ({ $from }: Selection) => + findParentNodeClosestToPos($from, predicate); + +/** + * Iterates over parent nodes starting from the given `$pos`, returning the + * closest node and its start position `predicate` returns truthy for. `start` + * points to the start position of the node, `pos` points directly before the node. + * + * @param $pos position to start from + * @param predicate filtering predicate function + * @returns node and its start position + */ +export const findParentNodeClosestToPos = ( + $pos: ResolvedPos, + predicate: Predicate +): ContentNodeWithPos | undefined => { + for (let i = $pos.depth; i > 0; i--) { + const node = $pos.node(i); + if (predicate(node)) { + return { + pos: i > 0 ? $pos.before(i) : 0, + start: $pos.start(i), + depth: i, + node, + }; + } + } + + return undefined; +}; diff --git a/shared/editor/queries/getColumnIndex.ts b/shared/editor/queries/getColumnIndex.ts deleted file mode 100644 index e82956528..000000000 --- a/shared/editor/queries/getColumnIndex.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { CellSelection } from "prosemirror-tables"; - -export default function getColumnIndex(selection: CellSelection) { - const isColSelection = selection.isColSelection && selection.isColSelection(); - if (!isColSelection) { - return undefined; - } - - const path = (selection.$from as any).path; - return path[path.length - 5]; -} diff --git a/shared/editor/queries/getRowIndex.ts b/shared/editor/queries/getRowIndex.ts deleted file mode 100644 index e11aa2001..000000000 --- a/shared/editor/queries/getRowIndex.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { CellSelection } from "prosemirror-tables"; - -export default function getRowIndex(selection: CellSelection) { - const isRowSelection = selection.isRowSelection && selection.isRowSelection(); - if (!isRowSelection) { - return undefined; - } - - const path = (selection.$from as any).path; - return path[path.length - 8]; -} - -export function getRowIndexFromText(selection: CellSelection) { - const isRowSelection = selection.isRowSelection && selection.isRowSelection(); - const path = (selection.$from as any).path; - if (isRowSelection) { - return path[path.length - 8]; - } else { - return path[path.length - 11]; - } -} diff --git a/shared/editor/queries/isNodeActive.ts b/shared/editor/queries/isNodeActive.ts index f7e93eee0..7369cbcea 100644 --- a/shared/editor/queries/isNodeActive.ts +++ b/shared/editor/queries/isNodeActive.ts @@ -1,6 +1,6 @@ import { NodeType } from "prosemirror-model"; import { EditorState } from "prosemirror-state"; -import { findParentNode, findSelectedNodeOfType } from "prosemirror-utils"; +import { findParentNode } from "./findParentNode"; const isNodeActive = (type: NodeType, attrs: Record = {}) => @@ -9,15 +9,21 @@ const isNodeActive = return false; } - const node = - findSelectedNodeOfType(type)(state.selection) || - findParentNode((node) => node.type === type)(state.selection); + const nodeAfter = state.selection.$from.nodeAfter; + let node = nodeAfter?.type === type ? nodeAfter : undefined; + + if (!node) { + const parent = findParentNode((node) => node.type === type)( + state.selection + ); + node = parent?.node; + } if (!Object.keys(attrs).length || !node) { return !!node; } - return node.node.hasMarkup(type, { ...node.node.attrs, ...attrs }); + return node.hasMarkup(type, { ...node.attrs, ...attrs }); }; export default isNodeActive; diff --git a/shared/editor/queries/table.ts b/shared/editor/queries/table.ts new file mode 100644 index 000000000..9a7822c63 --- /dev/null +++ b/shared/editor/queries/table.ts @@ -0,0 +1,95 @@ +import { EditorState } from "prosemirror-state"; +import { CellSelection, isInTable, selectedRect } from "prosemirror-tables"; + +export function getColumnIndex(state: EditorState): number | undefined { + if (state.selection instanceof CellSelection) { + if (state.selection.isColSelection()) { + const rect = selectedRect(state); + return rect.left; + } + } + + return undefined; +} + +export function getRowIndex(state: EditorState): number | undefined { + if (state.selection instanceof CellSelection) { + if (state.selection.isRowSelection()) { + const rect = selectedRect(state); + return rect.top; + } + } + + return undefined; +} + +export function getCellsInColumn(index: number) { + return (state: EditorState): number[] => { + if (!isInTable(state)) { + return []; + } + + const rect = selectedRect(state); + const cells = []; + + for (let i = index; i < rect.map.map.length; i += rect.map.width) { + const cell = rect.tableStart + rect.map.map[i]; + cells.push(cell); + } + return cells; + }; +} + +export function getCellsInRow(index: number) { + return (state: EditorState): number[] => { + if (!isInTable(state)) { + return []; + } + + const rect = selectedRect(state); + const cells = []; + + for (let i = 0; i < rect.map.width; i += 1) { + const cell = rect.tableStart + rect.map.map[index * rect.map.width + i]; + cells.push(cell); + } + return cells; + }; +} + +export function isColumnSelected(index: number) { + return (state: EditorState): boolean => { + if (state.selection instanceof CellSelection) { + if (state.selection.isColSelection()) { + const rect = selectedRect(state); + return rect.left <= index && rect.right > index; + } + } + + return false; + }; +} + +export function isRowSelected(index: number) { + return (state: EditorState): boolean => { + if (state.selection instanceof CellSelection) { + if (state.selection.isRowSelection()) { + const rect = selectedRect(state); + return rect.top <= index && rect.bottom > index; + } + } + + return false; + }; +} + +export function isTableSelected(state: EditorState): boolean { + const rect = selectedRect(state); + + return ( + rect.top === 0 && + rect.left === 0 && + rect.bottom === rect.map.height && + rect.right === rect.map.width + ); +} diff --git a/shared/editor/types/index.ts b/shared/editor/types/index.ts index cfc9aa048..7d542aee6 100644 --- a/shared/editor/types/index.ts +++ b/shared/editor/types/index.ts @@ -1,5 +1,5 @@ import { Node as ProsemirrorNode } from "prosemirror-model"; -import { EditorState, Transaction } from "prosemirror-state"; +import { EditorState } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import * as React from "react"; import { DefaultTheme } from "styled-components"; @@ -34,5 +34,3 @@ export type ComponentProps = { isEditable: boolean; getPos: () => number; }; - -export type Dispatch = (tr: Transaction) => void; diff --git a/tsconfig.json b/tsconfig.json index 89609b1a9..595813426 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,7 +24,6 @@ "suppressImplicitAnyIndexErrors": true, "target": "es2020", "paths": { - "prosemirror-markdown": ["./node_modules/@types/prosemirror-markdown"], "@server/*": ["./server/*"], "@shared/*": ["./shared/*"], "~/*": ["./app/*"] diff --git a/yarn.lock b/yarn.lock index 775d28c35..b81ad82cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3152,11 +3152,6 @@ dependencies: "@types/node" "*" -"@types/orderedmap@*": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/orderedmap/-/orderedmap-1.0.0.tgz#807455a192bba52cbbb4517044bc82bdbfa8c596" - integrity sha512-dxKo80TqYx3YtBipHwA/SdFmMMyLCnP+5mkEqN0eMjcTBzHkiiX0ES118DsjDBjvD+zeSsSU9jULTZ+frog+Gw== - "@types/pako@^1.0.3": version "1.0.4" resolved "https://registry.yarnpkg.com/@types/pako/-/pako-1.0.4.tgz#b4262aef92680a9331fcdb8420c69cf3dd98d3f3" @@ -3193,104 +3188,6 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== -"@types/prosemirror-commands@*", "@types/prosemirror-commands@^1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@types/prosemirror-commands/-/prosemirror-commands-1.0.4.tgz#d08551415127d93ae62e7239d30db0b5e7208e22" - integrity sha512-utDNYB3EXLjAfYIcRWJe6pn3kcQ5kG4RijbT/0Y/TFOm6yhvYS/D9eJVnijdg9LDjykapcezchxGRqFD5LcyaQ== - dependencies: - "@types/prosemirror-model" "*" - "@types/prosemirror-state" "*" - "@types/prosemirror-view" "*" - -"@types/prosemirror-dropcursor@^1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@types/prosemirror-dropcursor/-/prosemirror-dropcursor-1.5.0.tgz#5633fb20e7c1bf27edbdb64781b09bfb9f150f25" - integrity sha512-Xa13THoY0YkvYP/peH995ahT79w3ErdsmFUIaTY21nshxxnn5mdSgG+RTpkqXwZ85v+n28MvNfLF2gm+c8RZ1A== - dependencies: - prosemirror-dropcursor "*" - -"@types/prosemirror-gapcursor@^1.3.0": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@types/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.0.tgz#c1fcbf2504adc2b060b7a0c05f2d9dbb0631ce2b" - integrity sha512-KbZbwrr2i6+AAOtTTQhbgXlAL1ZTY+FE8PsGz4vqRLeS4ow7sppdI3oHGMn0xmCgqXI+ajEDYENKHUQ2WZkXew== - dependencies: - prosemirror-gapcursor "*" - -"@types/prosemirror-history@^1.0.1": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@types/prosemirror-history/-/prosemirror-history-1.0.3.tgz#f1110efbe758129b5475e466ff077f0a8d9b964f" - integrity sha512-5TloMDRavgLjOAKXp1Li8u0xcsspzbT1Cm9F2pwHOkgvQOz1jWQb2VIXO7RVNsFjLBZdIXlyfSLivro3DuMWXg== - dependencies: - "@types/prosemirror-model" "*" - "@types/prosemirror-state" "*" - -"@types/prosemirror-inputrules@^1.0.2": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@types/prosemirror-inputrules/-/prosemirror-inputrules-1.0.4.tgz#4cb75054d954aa0f6f42099be05eb6c0e6958bae" - integrity sha512-lJIMpOjO47SYozQybUkpV6QmfuQt7GZKHtVrvS+mR5UekA8NMC5HRIVMyaIauJLWhKU6oaNjpVaXdw41kh165g== - dependencies: - "@types/prosemirror-model" "*" - "@types/prosemirror-state" "*" - -"@types/prosemirror-keymap@^1.0.1": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@types/prosemirror-keymap/-/prosemirror-keymap-1.0.4.tgz#f73c79810e8d0e0a20d153d84f998f02e5afbc0c" - integrity sha512-ycevwkqUh+jEQtPwqO7sWGcm+Sybmhu8MpBsM8DlO3+YTKnXbKA6SDz/+q14q1wK3UA8lHJyfR+v+GPxfUSemg== - dependencies: - "@types/prosemirror-commands" "*" - "@types/prosemirror-model" "*" - "@types/prosemirror-state" "*" - "@types/prosemirror-view" "*" - -"@types/prosemirror-markdown@^1.0.3": - version "1.5.6" - resolved "https://registry.yarnpkg.com/@types/prosemirror-markdown/-/prosemirror-markdown-1.5.6.tgz#f985cec020ff37adcc2b490536adb4f82985dfae" - integrity sha512-Zm2YvkDsOrvvTct6GxTHAOQ/eAMwmeUMWoyyS1meNqdM3QHmp+mHln03tTAZKd6iRu1WbIKwHzTz/Mhof3C54Q== - dependencies: - "@types/markdown-it" "*" - "@types/prosemirror-model" "*" - -"@types/prosemirror-model@*", "@types/prosemirror-model@^1.7.2": - version "1.16.0" - resolved "https://registry.yarnpkg.com/@types/prosemirror-model/-/prosemirror-model-1.16.0.tgz#8b22c7431a4c93f7f550fc89c4b0e2d44d42c8b6" - integrity sha512-nv93YLyTEcDDl17OB90EldxZjyJQJll2WSMLDvLzTewbpvE/vtMjHT3j4mik3uSzQ6YD486AcloCO3WODY/lDg== - dependencies: - "@types/orderedmap" "*" - -"@types/prosemirror-schema-list@^1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@types/prosemirror-schema-list/-/prosemirror-schema-list-1.0.3.tgz#bdf1893a7915fbdc5c49b3cac9368e96213d70de" - integrity sha512-uWybOf+M2Ea7rlbs0yLsS4YJYNGXYtn4N+w8HCw3Vvfl6wBAROzlMt0gV/D/VW/7J/LlAjwMezuGe8xi24HzXA== - dependencies: - "@types/orderedmap" "*" - "@types/prosemirror-model" "*" - "@types/prosemirror-state" "*" - -"@types/prosemirror-state@*", "@types/prosemirror-state@^1.2.4": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@types/prosemirror-state/-/prosemirror-state-1.2.8.tgz#65080eeec52f63c50bf7034377f07773b4f6b2ac" - integrity sha512-mq9uyQWcpu8jeamO6Callrdvf/e1H/aRLR2kZWSpZrPHctEsxWHBbluD/wqVjXBRIOoMHLf6ZvOkrkmGLoCHVA== - dependencies: - "@types/prosemirror-model" "*" - "@types/prosemirror-transform" "*" - "@types/prosemirror-view" "*" - -"@types/prosemirror-transform@*": - version "1.1.4" - resolved "https://registry.yarnpkg.com/@types/prosemirror-transform/-/prosemirror-transform-1.1.4.tgz#c3565e81b2ef3ce3254e6927d6f63eb8d7bb20d0" - integrity sha512-HP1PauvkqSgDquZut8HaLOTUDQ6jja/LAy4OA7tTS1XG7wqRnX3gLUyEj0mD6vFd4y8BPkNddNdOh/BeGHlUjg== - dependencies: - "@types/prosemirror-model" "*" - -"@types/prosemirror-view@*", "@types/prosemirror-view@^1.11.4": - version "1.19.2" - resolved "https://registry.yarnpkg.com/@types/prosemirror-view/-/prosemirror-view-1.19.2.tgz#1bab4daf0f1f14313fe0d3f6b57f0a3b4ef6c50d" - integrity sha512-pmh2DuMJzva4D7SxspRKIzkV6FK2o52uAqGjq2dPYcQFPwu4+5RcS1TMjFVCh1R+Ia1Rx8wsCNIId/5+6DB0Bg== - dependencies: - "@types/prosemirror-model" "*" - "@types/prosemirror-state" "*" - "@types/prosemirror-transform" "*" - "@types/qs@*": version "6.9.7" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" @@ -10058,10 +9955,10 @@ ordered-read-streams@^1.0.0: dependencies: readable-stream "^2.0.1" -orderedmap@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-1.1.1.tgz#c618e77611b3b21d0fe3edc92586265e0059c789" - integrity sha512-3Ux8um0zXbVacKUkcytc0u3HgC0b0bBLT+I60r2J/En72cI0nZffqrA7Xtf2Hqs27j1g82llR5Mhbd0Z1XW4AQ== +orderedmap@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-2.1.1.tgz#61481269c44031c449915497bf5a4ad273c512d2" + integrity sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g== os-tmpdir@~1.0.2: version "1.0.2" @@ -10659,94 +10556,97 @@ property-information@^5.0.0: dependencies: xtend "^4.0.0" -prosemirror-commands@1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.2.2.tgz#1bd167372ee20abf488aca9cece63c43fab182c9" - integrity sha512-TX+KpWudMon06frryfpO/u7hsQv2hu8L4VSVbCpi3/7wXHBgl+35mV85qfa3RpT8xD2f3MdeoTqH0vy5JdbXPg== +prosemirror-commands@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.5.2.tgz#e94aeea52286f658cd984270de9b4c3fff580852" + integrity sha512-hgLcPaakxH8tu6YvVAaILV2tXYsW3rAdDR8WNkeKGcgeMVQg3/TMhPdVoh7iAmfgVjZGtcOSjKiQaoeKjzd2mQ== dependencies: prosemirror-model "^1.0.0" prosemirror-state "^1.0.0" prosemirror-transform "^1.0.0" -prosemirror-dropcursor@*, prosemirror-dropcursor@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.6.1.tgz#31f696172105f232bd17543ccf305e0f33e59d1d" - integrity sha512-LtyqQpkIknaT7NnZl3vDr3TpkNcG4ABvGRXx37XJ8tJNUGtcrZBh40A0344rDwlRTfUEmynQS/grUsoSWz+HgA== +prosemirror-dropcursor@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.1.tgz#49b9fb2f583e0d0f4021ff87db825faa2be2832d" + integrity sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw== dependencies: prosemirror-state "^1.0.0" prosemirror-transform "^1.1.0" prosemirror-view "^1.1.0" -prosemirror-gapcursor@*, prosemirror-gapcursor@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.1.tgz#8cfd874592e4504d63720e14ed680c7866e64554" - integrity sha512-GKTeE7ZoMsx5uVfc51/ouwMFPq0o8YrZ7Hx4jTF4EeGbXxBveUV8CGv46mSHuBBeXGmvu50guoV2kSnOeZZnUA== +prosemirror-gapcursor@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.2.tgz#5fa336b83789c6199a7341c9493587e249215cb4" + integrity sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ== dependencies: prosemirror-keymap "^1.0.0" prosemirror-model "^1.0.0" prosemirror-state "^1.0.0" prosemirror-view "^1.0.0" -prosemirror-history@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.2.0.tgz#04cc4df8d2f7b2a46651a2780de191ada6d465ea" - integrity sha512-B9v9xtf4fYbKxQwIr+3wtTDNLDZcmMMmGiI3TAPShnUzvo+Rmv1GiUrsQChY1meetHl7rhML2cppF3FTs7f7UQ== +prosemirror-history@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.3.2.tgz#ce6ad7ab9db83e761aee716f3040d74738311b15" + integrity sha512-/zm0XoU/N/+u7i5zepjmZAEnpvjDtzoPWW6VmKptcAnPadN/SStsBjMImdCEbb3seiNTpveziPTIrXQbHLtU1g== dependencies: prosemirror-state "^1.2.2" prosemirror-transform "^1.0.0" + prosemirror-view "^1.31.0" rope-sequence "^1.3.0" -prosemirror-inputrules@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.1.3.tgz#93f9199ca02473259c30d7e352e4c14022d54638" - integrity sha512-ZaHCLyBtvbyIHv0f5p6boQTIJjlD6o2NPZiEaZWT2DA+j591zS29QQEMT4lBqwcLW3qRSf7ZvoKNbf05YrsStw== +prosemirror-inputrules@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.2.1.tgz#8faf3d78c16150aedac71d326a3e3947417ce557" + integrity sha512-3LrWJX1+ULRh5SZvbIQlwZafOXqp1XuV21MGBu/i5xsztd+9VD15x6OtN6mdqSFI7/8Y77gYUbQ6vwwJ4mr6QQ== dependencies: prosemirror-state "^1.0.0" prosemirror-transform "^1.0.0" -prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.1.2, prosemirror-keymap@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.1.5.tgz#b5984c7d30f5c75956c853126c54e9e624c0327b" - integrity sha512-8SZgPH3K+GLsHL2wKuwBD9rxhsbnVBTwpHCO4VUO5GmqUQlxd/2GtBVWTsyLq4Dp3N9nGgPd3+lZFKUDuVp+Vw== +prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.1.2, prosemirror-keymap@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.2.2.tgz#14a54763a29c7b2704f561088ccf3384d14eb77e" + integrity sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ== dependencies: prosemirror-state "^1.0.0" w3c-keyname "^2.2.0" -prosemirror-markdown@^1.9.3: - version "1.9.3" - resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-1.9.3.tgz#13615fdeea71b744cc7bbbc64480968c8b7a2cee" - integrity sha512-tPJ3jEUTF3C5m60m/Eq8Y3SW6d5av0Ll61HuK1NuDP+jXsFFywG2nw500+nn7GQi5lSXP3n1HQP9zd0fpmAlzw== +prosemirror-markdown@^1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-1.11.0.tgz#75f2d6f14655762b4b8a247436b87ed81e22c7ee" + integrity sha512-yP9mZqPRstjZhhf3yykCQNE3AijxARrHe4e7esV9A+gp4cnGOH4QvrKYPpXLHspNWyvJJ+0URH+iIvV5qP1I2Q== dependencies: markdown-it "^13.0.1" prosemirror-model "^1.0.0" -prosemirror-model@1.16.1, prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.8.1: - version "1.16.1" - resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.16.1.tgz#fb388270bc9609b66298d6a7e15d0cc1d6c61253" - integrity sha512-r1/w0HDU40TtkXp0DyKBnFPYwd8FSlUSJmGCGFv4DeynfeSlyQF2FD0RQbVEMOe6P3PpUSXM6LZBV7W/YNZ4mA== +prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.19.1, prosemirror-model@^1.8.1: + version "1.19.1" + resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.19.1.tgz#7e10cd9584a0a55c87ffbecc68aa9d105b9a0f53" + integrity sha512-RpV0fZfy74DEO9GPRbGcG6xN33KuqEvlLE2V0e5CXUGs3xkZsiJfx1dcYPU57+606NVYCaDN1riFXdXBQRaRcg== dependencies: - orderedmap "^1.1.0" + orderedmap "^2.0.0" -prosemirror-schema-list@1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.1.4.tgz#471f9caf2d2bed93641d2e490434c0d2d4330df1" - integrity sha512-pNTuZflacFOBlxrTcWSdWhjoB8BaucwfJVp/gJNxztOwaN3wQiC65axclXyplf6TKgXD/EkWfS/QAov3/Znadw== +prosemirror-schema-list@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.2.3.tgz#12e3d70cb17780980a3c28588ed7c888121d5e8d" + integrity sha512-HD8yjDOusz7JB3oBFCaMOpEN9Z9DZttLr6tcASjnvKMc0qTyX5xgAN8YiMFFEcwyhF7WZrZ2YQkAwzsn8ICVbQ== + dependencies: + prosemirror-model "^1.0.0" + prosemirror-state "^1.0.0" + prosemirror-transform "^1.0.0" + +prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.3.1, prosemirror-state@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.4.3.tgz#94aecf3ffd54ec37e87aa7179d13508da181a080" + integrity sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q== dependencies: prosemirror-model "^1.0.0" prosemirror-transform "^1.0.0" + prosemirror-view "^1.27.0" -prosemirror-state@1.3.4, prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.3.1: - version "1.3.4" - resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.3.4.tgz#4c6b52628216e753fc901c6d2bfd84ce109e8952" - integrity sha512-Xkkrpd1y/TQ6HKzN3agsQIGRcLckUMA9u3j207L04mt8ToRgpGeyhbVv0HI7omDORIBHjR29b7AwlATFFf2GLA== - dependencies: - prosemirror-model "^1.0.0" - prosemirror-transform "^1.0.0" - -prosemirror-tables@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-1.1.1.tgz#ad66300cc49500455cf1243bb129c9e7d883321e" - integrity sha512-LmCz4jrlqQZRsYRDzCRYf/pQ5CUcSOyqZlAj5kv67ZWBH1SVLP2U9WJEvQfimWgeRlIz0y0PQVqO1arRm1+woA== +prosemirror-tables@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-1.3.2.tgz#ca208c6a55d510af14b652d23e800e00ba6bebd4" + integrity sha512-/9JTeN6s58Zq66HXaxP6uf8PAmc7XXKZFPlOGVtLvxEd6xBP6WtzaJB9wBjiGUzwbdhdMEy7V62yuHqk/3VrnQ== dependencies: prosemirror-keymap "^1.1.2" prosemirror-model "^1.8.1" @@ -10754,22 +10654,17 @@ prosemirror-tables@^1.1.1: prosemirror-transform "^1.2.1" prosemirror-view "^1.13.3" -prosemirror-transform@1.2.5, prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.2.1: - version "1.2.5" - resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.2.5.tgz#7a3e2c61fcdbaf1d0844a2a3bc34fc3524e9809c" - integrity sha512-eqeIaxWtUfOnpA1ERrXCuSIMzqIJtL9Qrs5uJMCjY5RMSaH5o4pc390SAjn/IDPeIlw6auh0hCCXs3wRvGnQug== +prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.2.1, prosemirror-transform@^1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.7.2.tgz#f3e57d8424afa6ab7c2b2319cc0ac58e75f7160b" + integrity sha512-b94lVUdA9NyaYRb2WuGSgb5YANiITa05dtew9eSK+KkYu64BCnU27WhJPE95gAWAnhV57CM3FabWXM23gri8Kg== dependencies: prosemirror-model "^1.0.0" -prosemirror-utils@^0.9.6: - version "0.9.6" - resolved "https://registry.yarnpkg.com/prosemirror-utils/-/prosemirror-utils-0.9.6.tgz#3d97bd85897e3b535555867dc95a51399116a973" - integrity sha512-UC+j9hQQ1POYfMc5p7UFxBTptRiGPR7Kkmbl3jVvU8VgQbkI89tR/GK+3QYC8n+VvBZrtAoCrJItNhWSxX3slA== - -prosemirror-view@1.26.5, prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3: - version "1.26.5" - resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.26.5.tgz#65cb890d0971e94e0cbc09fdb0c5320cd474bc6f" - integrity sha512-SO+AX6WwdbJZHVvuloXI0qfO+YJAnZAat8qrYwfiqTQwL/FewLUnr0m3EXZ6a60hQs8/Q/lzeJXiFR/dOPaaKQ== +prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3, prosemirror-view@^1.27.0, prosemirror-view@^1.31.0, prosemirror-view@^1.31.3: + version "1.31.3" + resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.31.3.tgz#cfe171c4e50a577526d0235d9ec757cdddf6017d" + integrity sha512-UYDa8WxRFZm0xQLXiPJUVTl6H08Fn0IUVDootA7ZlQwzooqVWnBOXLovJyyTKgws1nprfsPhhlvWgt2jo4ZA6g== dependencies: prosemirror-model "^1.16.0" prosemirror-state "^1.0.0"