Skip to content

Unable to select word on double click #7310

@m-salman-afzal

Description

@m-salman-afzal

Affected Packages

Packages used are given in additional

Version(s)

3.13.0

Bug Description

When I double click on a word, it does not select the word and highlights it.

Browser Used

Chrome

Code Example URL

No response

Expected Behavior

Double click should select the word. Although, triple click seems to work and it selects the whole line.

Additional Context (Optional)

Here is my code to define the Editor.

  const defaultExtensions: Extensions = [
    // Extend Document to use single newline separator between blocks instead of double
    // This makes Shift+Enter produce \n instead of \n\n in markdown output
    Document.extend({
      renderMarkdown: (node: JSONContent, h: MarkdownRendererHelpers) => {
        if (!node.content) {
          return '';
        }

        return h.renderChildren(node.content, '\n');
      },
    }),
    Text,
    Paragraph,
    UndoRedo,
    Placeholder.configure({
      placeholder: ({ node }) => {
        // Don't show a placeholder for ordered or unordered lists
        if (
          node.type.name === 'orderedList' ||
          node.type.name === 'bulletList'
        ) {
          return '';
        }

        return placeholder;
      },
    }),
    Markdown.configure({
      indentation: {
        style: 'space',
        size: 2,
      },
      markedOptions: { breaks: true, gfm: true },
    }),

    ListKit.configure({
      taskItem: false,
      taskList: false,
      bulletList: {
        HTMLAttributes: {
          class: 'list-disc ml-2',
        },
      },
      orderedList: {
        HTMLAttributes: {
          class: 'list-decimal ml-4',
        },
      },
      listItem: {
        HTMLAttributes: {
          class: 'ml-4',
        },
      },
    }),
    ModifyTiptapEnterExtension,
    ModifyTiptapBackspaceExtension,
    DoubleClickWordSelectExtension,
    PreserveNewlinesPasteExtension,
    ...additionalExtensions,
  ];

  const editor = useEditor({
    extensions: defaultExtensions,
    content: initialContent,
    autofocus: autoFocus,
    immediatelyRender,
    contentType: 'markdown',
  });
// DoubleClickWordSelectExtension.ts
import { Extension } from '@tiptap/core';
import { Plugin, PluginKey, TextSelection } from '@tiptap/pm/state';

/**
 * Extension that enables double-click to select a word.
 * This fixes cases where the default browser behavior is being intercepted.
 */
export const DoubleClickWordSelectExtension = Extension.create({
  name: 'doubleClickWordSelect',

  addProseMirrorPlugins: () => {
    return [
      new Plugin({
        key: new PluginKey('doubleClickWordSelect'),
        props: {
          handleDoubleClick(view, pos) {
            const { state } = view;
            const { doc } = state;

            // Get the resolved position
            const $pos = doc.resolve(pos);

            // Get the text content of the parent node
            const parent = $pos.parent;
            if (!parent.isTextblock) return false;

            const text = parent.textContent;
            const offset = $pos.parentOffset;

            // Find word boundaries
            let start = offset;
            let end = offset;

            // Word character pattern (letters, numbers, underscores)
            const isWordChar = (char: string) => /[\w\u00C0-\u024F]/.test(char);

            // Find start of word
            while (start > 0 && isWordChar(text[start - 1])) {
              start--;
            }

            // Find end of word
            while (end < text.length && isWordChar(text[end])) {
              end++;
            }

            // If we found a word (start !== end)
            if (start !== end) {
              const from = $pos.start() + start;
              const to = $pos.start() + end;

              const tr = state.tr.setSelection(
                TextSelection.create(doc, from, to)
              );
              view.dispatch(tr);
              return true;
            }

            return false;
          },
        },
      }),
    ];
  },
});
// PreserveNewlinesPasteExtension.ts
import { Extension } from '@tiptap/core';
import { Fragment, Slice } from '@tiptap/pm/model';
import { Plugin, PluginKey } from '@tiptap/pm/state';

/**
 * Extension that preserves multiple consecutive newlines when pasting plain text.
 * By default, TipTap/ProseMirror collapses multiple newlines into single paragraph breaks
 * because it uses /(?:\r\n?|\n)+/ regex which matches "one or more" newlines.
 * This extension manually handles paste to preserve empty lines between paragraphs.
 * ref: https://unpkg.com/[email protected]/src/clipboard.ts, search for /(?:\r\n?|\n)+/
 */

export const PreserveNewlinesPasteExtension = Extension.create({
  name: 'preserveNewlinesPaste',

  addProseMirrorPlugins() {
    return [
      new Plugin({
        key: new PluginKey('preserveNewlinesPaste'),
        props: {
          handlePaste: (view, event) => {
            const text = event.clipboardData?.getData('text/plain');
            const html = event.clipboardData?.getData('text/html');

            // If there's HTML content (e.g., from a rich text source), let default handling occur
            // unless we also have plain text to preserve
            if (!text) {
              return false;
            }

            // If HTML is present but no special newlines in text, let default handle it
            if (html && !text.includes('\n')) {
              return false;
            }

            const { state, dispatch } = view;
            const { schema, tr } = state;

            // Normalize CRLF to LF to handle Windows line endings consistently
            const normalizedText = text
              .replace(/\r\n/g, '\n')
              .replace(/\r/g, '\n');

            // Split on each individual newline (NOT on "one or more" newlines)
            // This preserves empty lines as empty strings in the array
            const lines = normalizedText.split('\n');

            // Multiple lines - create paragraph nodes
            const paragraphType = schema.nodes.paragraph;
            if (!paragraphType) {
              // Fallback if no paragraph type in schema
              return false;
            }

            const paragraphs = lines.map((line) => {
              if (line === '') {
                // Empty line becomes an empty paragraph
                return paragraphType.create();
              }
              // Non-empty line becomes a paragraph with text content
              return paragraphType.create(null, schema.text(line));
            });

            // Create a slice with openStart=1 and openEnd=1
            // This allows the first/last paragraphs to merge with surrounding content
            // when pasting in the middle of existing text
            const fragment = Fragment.fromArray(paragraphs);
            const slice = new Slice(fragment, 1, 1);

            // Replace the current selection with the pasted content
            dispatch(tr.replaceSelection(slice).scrollIntoView());

            return true;
          },
        },
      }),
    ];
  },
});

I've also attached 2 more extensions which should not be required but here we are.

Dependency Updates

  • Yes, I've updated all my dependencies.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Open SourceThe issue or pull reuqest is related to the open source packages of Tiptap.

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions