diff --git a/packages/core/src/extensions/extension.ts b/packages/core/src/extensions/extension.ts index 019e9002..72ee4e4f 100644 --- a/packages/core/src/extensions/extension.ts +++ b/packages/core/src/extensions/extension.ts @@ -39,6 +39,7 @@ import { defineMeowdownParagraph } from './paragraph.ts' import { definePendingReplacement } from './pending-replacement.ts' import { defineScrollToSelection } from './scroll-to-selection.ts' import { defineSelectDocBoundary } from './select-doc-boundary.ts' +import { defineSystemSubstitutionGuard } from './system-substitution-guard.ts' import { defineTable } from './table.ts' import { defineViewAttributes } from './view-attributes.ts' import { defineWikilink } from './wikilink.ts' @@ -77,6 +78,7 @@ function defineEditorExtensionImpl(options: EditorExtensionOptions) { defineClipboard(), defineScrollToSelection(), defineHiddenRunCaret(), + defineSystemSubstitutionGuard(), defineAtomMarkNavigation({ marks: ATOM_SOURCE_MARK_NAMES.map((name) => ({ name, modes: ['hide', 'focus', 'show'] })), }), diff --git a/packages/core/src/extensions/spell-check.ts b/packages/core/src/extensions/spell-check.ts index a5bad38b..c5855378 100644 --- a/packages/core/src/extensions/spell-check.ts +++ b/packages/core/src/extensions/spell-check.ts @@ -117,6 +117,9 @@ function createSpellCheckPlugin(spellCheck: boolean) { }) } +/** + * @deprecated This will be removed in future versions. + */ export function defineSpellCheckPlugin(spellCheck: boolean): PlainExtension { return definePlugin(createSpellCheckPlugin(spellCheck)) } diff --git a/packages/core/src/extensions/system-substitution-guard.ts b/packages/core/src/extensions/system-substitution-guard.ts new file mode 100644 index 00000000..06801a0e --- /dev/null +++ b/packages/core/src/extensions/system-substitution-guard.ts @@ -0,0 +1,41 @@ +import { definePlugin, Priority, withPriority, type PlainExtension } from '@prosekit/core' +import { Plugin } from '@prosekit/pm/state' + +const EM_DASH = '\u{2014}' + +/** + * Block the macOS "smart dashes" rewrite. With the `spellcheck` attribute on, + * WebKit rewrites already-typed `--` near the caret into an em dash and + * delivers the rewrite as a cancelable `beforeinput` with + * `inputType: 'insertReplacementText'`. That corrupts Markdown such as the + * `-->` closing an image sizing comment, so cancel any replacement that + * inserts an em dash; word-level autocorrect passes through. + */ +export function defineSystemSubstitutionGuard(): PlainExtension { + const plugin = new Plugin({ + props: { + handleDOMEvents: { + beforeinput: (view, event) => { + if (event.inputType !== 'insertReplacementText') return false + + const replacement = event.dataTransfer?.getData('text/plain') || event.data || '' + if (!replacement.includes(EM_DASH)) return false + + event.preventDefault() + + // Restore the selection after the cancelation. See WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=321420 + const { doc, selection } = view.state + requestAnimationFrame(() => { + const { state } = view + if (view.isDestroyed || state.doc !== doc || state.selection.eq(selection)) return + view.dispatch(state.tr.setSelection(selection)) + }) + + return true + }, + }, + }, + }) + + return withPriority(definePlugin(plugin), Priority.highest) +} diff --git a/packages/react/src/components/editor-extensions.tsx b/packages/react/src/components/editor-extensions.tsx index e2f3328d..e5dcdd92 100644 --- a/packages/react/src/components/editor-extensions.tsx +++ b/packages/react/src/components/editor-extensions.tsx @@ -13,7 +13,6 @@ import { definePlaceholder, defineReadonly, defineSearchStatusHandler, - defineSpellCheckPlugin, defineSubstitution, defineTagClickHandler, defineViewAttributes, @@ -106,6 +105,13 @@ export function EditorExtensions({ return editor.use(extension) }, [editor, editorClassName]) + // Set spellCheck + useLayoutEffect(() => { + if (spellCheck == null) return + const extension = defineViewAttributes({ spellcheck: spellCheck ? 'true' : 'false' }) + return editor.use(extension) + }, [editor, spellCheck]) + // Search has no latency requirement, so a slow device may skip the // intermediate values of a fast typist entirely. `literal` turns off // prosemirror-search's escape-sequence handling: a find bar takes text, so a @@ -242,11 +248,5 @@ export function EditorExtensions({ }, [wikilinkEnabled]), ) - useExtension( - useMemo(() => { - return spellCheck == null ? null : defineSpellCheckPlugin(spellCheck) - }, [spellCheck]), - ) - return null } diff --git a/website/src/app.tsx b/website/src/app.tsx index ababcb5e..e2ab7935 100644 --- a/website/src/app.tsx +++ b/website/src/app.tsx @@ -9,12 +9,12 @@ import { import { getId } from '@ocavue/utils' import { clsx } from 'clsx/lite' import { - type CSSProperties, useCallback, useEffect, useLayoutEffect, useRef, useState, + type CSSProperties, } from 'react' import { DemoEditor } from './components/demo-editor.tsx' @@ -160,7 +160,7 @@ Outline your thoughts with nested bullets. Hover a bullet that has children and Drop in an image and it renders right where you wrote it. Paste or drag one in to upload your own: -Small images flow inline ![](https://static.photos/yellow/16x16/3) with the surrounding text. +Small images flow inline ![](https://static.photos/yellow/16x16/3) with the surrounding text. Paste a YouTube or tweet link and it embeds itself. Undo once to get the plain link back: