diff --git a/.changeset/little-shrimps-explain.md b/.changeset/little-shrimps-explain.md new file mode 100644 index 0000000000..70035030eb --- /dev/null +++ b/.changeset/little-shrimps-explain.md @@ -0,0 +1,6 @@ +--- +'@tiptap/extensions': patch +'tiptap-demos': patch +--- + +Fix the `Selection` extension highlighting beyond the selected text on multi-line selections: the native browser selection is now hidden while the editor is blurred, so only the styled `.selection` decoration is shown. diff --git a/demos/src/Extensions/Selection/React/index.tsx b/demos/src/Extensions/Selection/React/index.tsx index a227481dfe..9d449734c4 100644 --- a/demos/src/Extensions/Selection/React/index.tsx +++ b/demos/src/Extensions/Selection/React/index.tsx @@ -24,12 +24,12 @@ export default () => { ], content: `

- The selection extension adds a class to the selection when the editor is blurred. That enables you to visually preserve the selection even though the editor is blurred. By default, it’ll add .selection classname. + The Selection extension adds a class to the current text selection when the editor is blurred, which lets you keep the selected text highlighted even after the editor loses focus. This is especially useful when a selection spans multiple wrapped lines, where only the selected text should be highlighted rather than the empty space at the end of each line. By default it adds a .selection classname that you can style.

`, onCreate: ctx => { - ctx.editor.commands.setTextSelection({ from: 5, to: 30 }) + ctx.editor.commands.setTextSelection({ from: 5, to: 280 }) }, }) diff --git a/demos/src/Extensions/Selection/Vue/index.vue b/demos/src/Extensions/Selection/Vue/index.vue index 4b8d79945b..e523fbca11 100644 --- a/demos/src/Extensions/Selection/Vue/index.vue +++ b/demos/src/Extensions/Selection/Vue/index.vue @@ -37,11 +37,11 @@ export default { ], content: `

- The selection extension adds a class to the selection when the editor is blurred. That enables you to visually preserve the selection even though the editor is blurred. By default, it’ll add .selection classname. + The Selection extension adds a class to the current text selection when the editor is blurred, which lets you keep the selected text highlighted even after the editor loses focus. This is especially useful when a selection spans multiple wrapped lines, where only the selected text should be highlighted rather than the empty space at the end of each line. By default it adds a .selection classname that you can style.

`, onCreate: ({ editor }) => { - editor.commands.setTextSelection({ from: 5, to: 30 }) + editor.commands.setTextSelection({ from: 5, to: 280 }) }, }) }, diff --git a/packages/extensions/__tests__/selection.spec.ts b/packages/extensions/__tests__/selection.spec.ts new file mode 100644 index 0000000000..6456f40bee --- /dev/null +++ b/packages/extensions/__tests__/selection.spec.ts @@ -0,0 +1,45 @@ +import { Editor } from '@tiptap/core' +import Document from '@tiptap/extension-document' +import Paragraph from '@tiptap/extension-paragraph' +import Text from '@tiptap/extension-text' +import { Selection } from '@tiptap/extensions' +import { afterEach, describe, expect, it } from 'vitest' + +const styleSelector = 'style[data-tiptap-style-selection]' + +describe('extension-selection', () => { + let editor: Editor | null = null + + afterEach(() => { + if (editor) { + editor.destroy() + editor = null + } + + document.querySelectorAll(styleSelector).forEach(node => node.remove()) + }) + + it('injects a stylesheet that hides the native selection while the editor is blurred', () => { + editor = new Editor({ + extensions: [Document, Paragraph, Text, Selection], + content: '

Hello world

', + }) + + const styleTag = document.querySelector(styleSelector) + + expect(styleTag).not.toBeNull() + expect(styleTag?.textContent).toContain('.ProseMirror:not(.ProseMirror-focused) *::selection') + expect(styleTag?.textContent).toContain('*::-moz-selection') + expect(styleTag?.textContent).toContain('background: transparent') + }) + + it('does not inject the stylesheet when injectCSS is disabled', () => { + editor = new Editor({ + extensions: [Document, Paragraph, Text, Selection], + content: '

Hello world

', + injectCSS: false, + }) + + expect(document.querySelector(styleSelector)).toBeNull() + }) +}) diff --git a/packages/extensions/src/selection/selection.ts b/packages/extensions/src/selection/selection.ts index f47fcdeb52..d4a8b02421 100644 --- a/packages/extensions/src/selection/selection.ts +++ b/packages/extensions/src/selection/selection.ts @@ -1,7 +1,15 @@ -import { Extension, isNodeSelection } from '@tiptap/core' +import { createStyleTag, Extension, isNodeSelection } from '@tiptap/core' import { Plugin, PluginKey } from '@tiptap/pm/state' import { Decoration, DecorationSet } from '@tiptap/pm/view' +const selectionStyle = `.ProseMirror:not(.ProseMirror-focused) *::selection { + background: transparent; +} + +.ProseMirror:not(.ProseMirror-focused) *::-moz-selection { + background: transparent; +}` + export type SelectionOptions = { /** * The class name that should be added to the selected text. @@ -27,6 +35,10 @@ export const Selection = Extension.create({ addProseMirrorPlugins() { const { editor, options } = this + if (editor.options.injectCSS && typeof document !== 'undefined') { + createStyleTag(selectionStyle, editor.options.injectNonce, 'selection') + } + return [ new Plugin({ key: new PluginKey('selection'),