From a2fae1f1ebc6587533260ef4cf57af835e8d7334 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Mon, 26 Jun 2023 18:28:42 -0400 Subject: [PATCH] fix: Keyboard navigation around inline code marks (#5477) --- package.json | 1 + shared/editor/commands/moveLeft.ts | 111 ------------------ shared/editor/commands/moveRight.ts | 71 ------------ shared/editor/components/Styles.ts | 170 +++++++++++++++------------- shared/editor/marks/Code.ts | 6 +- yarn.lock | 5 + 6 files changed, 102 insertions(+), 262 deletions(-) delete mode 100644 shared/editor/commands/moveLeft.ts delete mode 100644 shared/editor/commands/moveRight.ts diff --git a/package.json b/package.json index c7d1b294a..41ad15db1 100644 --- a/package.json +++ b/package.json @@ -150,6 +150,7 @@ "pg": "^8.8.0", "pg-tsquery": "^8.4.0", "polished": "^4.2.2", + "prosemirror-codemark": "^0.4.2", "prosemirror-commands": "^1.5.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", diff --git a/shared/editor/commands/moveLeft.ts b/shared/editor/commands/moveLeft.ts deleted file mode 100644 index 6bd3686cb..000000000 --- a/shared/editor/commands/moveLeft.ts +++ /dev/null @@ -1,111 +0,0 @@ -/* -Copyright 2020 Atlassian Pty Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -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, - Command, -} from "prosemirror-state"; -import isMarkActive from "../queries/isMarkActive"; - -function hasCode(state: EditorState, pos: number) { - const { code_inline } = state.schema.marks; - const node = pos >= 0 && state.doc.nodeAt(pos); - - return node - ? !!node.marks.filter((mark) => mark.type === code_inline).length - : false; -} - -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) { - return false; - } - - const { storedMarks } = state.tr; - - if (code_inline) { - const insideCode = code_inline && isMarkActive(code_inline)(state); - const currentPosHasCode = hasCode(state, $cursor.pos); - const nextPosHasCode = hasCode(state, $cursor.pos - 1); - const nextNextPosHasCode = hasCode(state, $cursor.pos - 2); - - const exitingCode = - currentPosHasCode && !nextPosHasCode && Array.isArray(storedMarks); - const atLeftEdge = - nextPosHasCode && - !nextNextPosHasCode && - (storedMarks === null || - (Array.isArray(storedMarks) && !!storedMarks.length)); - const atRightEdge = - ((exitingCode && Array.isArray(storedMarks) && !storedMarks.length) || - (!exitingCode && storedMarks === null)) && - !nextPosHasCode && - nextNextPosHasCode; - const enteringCode = - !currentPosHasCode && - nextPosHasCode && - Array.isArray(storedMarks) && - !storedMarks.length; - - // at the right edge: remove code mark and move the cursor to the left - if (!insideCode && atRightEdge) { - const tr = state.tr.setSelection( - Selection.near(state.doc.resolve($cursor.pos - 1)) - ); - - 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())); - return true; - } - - // at the left edge: add code mark and move the cursor to the left - if (insideCode && atLeftEdge) { - const tr = state.tr.setSelection( - Selection.near(state.doc.resolve($cursor.pos - 1)) - ); - - dispatch?.(tr.addStoredMark(code_inline.create())); - return true; - } - - // exiting code mark (or at the beginning of the line): don't move the cursor, just remove the mark - const isFirstChild = $cursor.index($cursor.depth - 1) === 0; - if ( - insideCode && - (exitingCode || (!$cursor.nodeBefore && isFirstChild)) - ) { - dispatch?.(state.tr.removeStoredMark(code_inline)); - return true; - } - } - - return false; - }; -} diff --git a/shared/editor/commands/moveRight.ts b/shared/editor/commands/moveRight.ts deleted file mode 100644 index 259d5815c..000000000 --- a/shared/editor/commands/moveRight.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* -Copyright 2020 Atlassian Pty Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -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 { Command, TextSelection } from "prosemirror-state"; -import isMarkActive from "../queries/isMarkActive"; - -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) { - return false; - } - - const { storedMarks } = state.tr; - if (code_inline) { - const insideCode = isMarkActive(code_inline)(state); - const currentPosHasCode = state.doc.rangeHasMark( - $cursor.pos, - $cursor.pos, - code_inline - ); - const nextPosHasCode = state.doc.rangeHasMark( - $cursor.pos, - $cursor.pos + 1, - code_inline - ); - - const exitingCode = - !currentPosHasCode && - !nextPosHasCode && - (!storedMarks || !!storedMarks.length); - const enteringCode = - !currentPosHasCode && - nextPosHasCode && - (!storedMarks || !storedMarks.length); - - // 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())); - - return true; - } - - // exiting code mark: don't move the cursor, just remove the mark - if (insideCode && exitingCode) { - dispatch?.(state.tr.removeStoredMark(code_inline)); - - return true; - } - } - - return false; - }; -} diff --git a/shared/editor/components/Styles.ts b/shared/editor/components/Styles.ts index c553d3d27..074d6ab78 100644 --- a/shared/editor/components/Styles.ts +++ b/shared/editor/components/Styles.ts @@ -1,6 +1,6 @@ /* eslint-disable no-irregular-whitespace */ import { darken, lighten, transparentize } from "polished"; -import styled, { DefaultTheme } from "styled-components"; +import styled, { DefaultTheme, css } from "styled-components"; export type Props = { rtl: boolean; @@ -11,98 +11,115 @@ export type Props = { theme: DefaultTheme; }; -const mathStyle = (props: Props) => ` -/* Based on https://github.com/benrbray/prosemirror-math/blob/master/style/math.css */ +const codeMarkCursor = () => css` + /* Based on https://github.com/curvenote/editor/blob/main/packages/prosemirror-codemark/src/codemark.css */ + .no-cursor { + caret-color: transparent; + } -.math-node { - min-width: 1em; - min-height: 1em; - font-size: 0.95em; - font-family: ${props.theme.fontFamilyMono}; - cursor: auto; -} + div:focus .fake-cursor, + span:focus .fake-cursor { + margin-right: -1px; + border-left-width: 1px; + border-left-style: solid; + animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite; + position: relative; + z-index: 1; + } +`; -.math-node.empty-math .math-render::before { - content: "(empty math)"; - color: ${props.theme.brand.red}; -} +const mathStyle = (props: Props) => css` + /* Based on https://github.com/benrbray/prosemirror-math/blob/master/style/math.css */ -.math-node .math-render.parse-error::before { - content: "(math error)"; - color: ${props.theme.brand.red}; - cursor: help; -} + .math-node { + min-width: 1em; + min-height: 1em; + font-size: 0.95em; + font-family: ${props.theme.fontFamilyMono}; + cursor: auto; + } -.math-node.ProseMirror-selectednode { - outline: none; -} + .math-node.empty-math .math-render::before { + content: "(empty math)"; + color: ${props.theme.brand.red}; + } -.math-node .math-src { - display: none; - color: ${props.theme.codeStatement}; - tab-size: 4; -} + .math-node .math-render.parse-error::before { + content: "(math error)"; + color: ${props.theme.brand.red}; + cursor: help; + } -.math-node.ProseMirror-selectednode .math-src { - display: inline; -} + .math-node.ProseMirror-selectednode { + outline: none; + } -.math-node.ProseMirror-selectednode .math-render { - display: none; -} + .math-node .math-src { + display: none; + color: ${props.theme.codeStatement}; + tab-size: 4; + } -math-inline { - display: inline; white-space: nowrap; + .math-node.ProseMirror-selectednode .math-src { + display: inline; + } -} + .math-node.ProseMirror-selectednode .math-render { + display: none; + } -math-inline .math-render { - display: inline-block; - font-size: 0.85em; -} + math-inline { + display: inline; + white-space: nowrap; + } -math-inline .math-src .ProseMirror { - display: inline; - margin: 0px 3px; -} + math-inline .math-render { + display: inline-block; + font-size: 0.85em; + } -math-block { - display: block; -} + math-inline .math-src .ProseMirror { + display: inline; + margin: 0px 3px; + } -math-block .math-render { - display: block; -} + math-block { + display: block; + } -math-block.ProseMirror-selectednode { - border-radius: 4px; - border: 1px solid ${props.theme.codeBorder}; - background: ${props.theme.codeBackground}; - padding: 0.75em 1em; - font-family: ${props.theme.fontFamilyMono}; - font-size: 90%; -} + math-block .math-render { + display: block; + } -math-block .math-src .ProseMirror { - width: 100%; - display: block; -} + math-block.ProseMirror-selectednode { + border-radius: 4px; + border: 1px solid ${props.theme.codeBorder}; + background: ${props.theme.codeBackground}; + padding: 0.75em 1em; + font-family: ${props.theme.fontFamilyMono}; + font-size: 90%; + } -math-block .katex-display { - margin: 0; -} + math-block .math-src .ProseMirror { + width: 100%; + display: block; + } -.katex-html *::selection { - background-color: none !important; -} + math-block .katex-display { + margin: 0; + } -.math-node.math-select .math-render { - background-color: #c0c0c0ff; -} + .katex-html *::selection { + background-color: none !important; + } -math-inline.math-select .math-render { - padding-top: 2px; -} + .math-node.math-select .math-render { + background-color: #c0c0c0ff; + } + + math-inline.math-select .math-render { + padding-top: 2px; + } `; const style = (props: Props) => ` @@ -1513,8 +1530,9 @@ del[data-operation-index] { `; const EditorContainer = styled.div` - ${style}; - ${mathStyle}; + ${style} + ${mathStyle} + ${codeMarkCursor} `; export default EditorContainer; diff --git a/shared/editor/marks/Code.ts b/shared/editor/marks/Code.ts index ed9dac321..185f51f46 100644 --- a/shared/editor/marks/Code.ts +++ b/shared/editor/marks/Code.ts @@ -1,3 +1,4 @@ +import codemark from "prosemirror-codemark"; import { toggleMark } from "prosemirror-commands"; import { MarkSpec, @@ -8,8 +9,6 @@ import { } from "prosemirror-model"; import { Plugin } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; -import moveLeft from "../commands/moveLeft"; -import moveRight from "../commands/moveRight"; import markInputRule from "../lib/markInputRule"; import { MarkdownSerializerState } from "../lib/markdown/serializer"; import Mark from "./Mark"; @@ -57,13 +56,12 @@ export default class Code extends Mark { // https://github.com/ProseMirror/prosemirror/issues/515 return { "Mod`": toggleMark(type), - ArrowLeft: moveLeft(), - ArrowRight: moveRight(), }; } get plugins() { return [ + ...codemark({ markType: this.editor.schema.marks.code_inline }), new Plugin({ props: { // Typing a character inside of two backticks will wrap the character diff --git a/yarn.lock b/yarn.lock index ea7025282..7342d4a06 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10588,6 +10588,11 @@ property-information@^5.0.0: dependencies: xtend "^4.0.0" +prosemirror-codemark@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/prosemirror-codemark/-/prosemirror-codemark-0.4.2.tgz#b4d0a57c0f1f6c6667e2a1ae7cfb6ba031dfb2e5" + integrity sha512-4n+PnGQToa/vTjn0OiivUvE8/moLtguUAfry8UA4Q8p47MhqT2Qpf2zBLustX5Upi4mSp3z1ZYBqLLovZC6abA== + prosemirror-commands@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.5.2.tgz#e94aeea52286f658cd984270de9b4c3fff580852"