Skip to content

Bug: Selection/Cursor jumps to beginning after applying color with custom color syntax plugin #3317

@ntducne

Description

@ntducne

Bug: Selection/Cursor jumps to beginning after applying color with custom color syntax plugin

Description

When applying text color using a custom color syntax plugin wrapper, the cursor/selection position resets unexpectedly. Additionally, when changing text to the same color, the highlight background still displays.

Environment

  • Toast UI Editor Version: 3.x
  • @toast-ui/editor-plugin-color-syntax Version: 3.x
  • Browser: Chrome/Firefox/Safari (all browsers affected)
  • OS: Windows/macOS/Linux

Steps to Reproduce

  1. Create an editor instance with the color syntax plugin
  2. Select a single character or text in the middle of a line
  3. Click a color from the color preset palette
  4. Expected: Cursor stays at the end of the selection
  5. Actual: Cursor jumps to the beginning of the line (first time only)
const editorNode = new Editor({
  el: document.getElementById('editor'),
  plugins: [colorSyntax],
  toolbarItems: [['color']],
});

// Select "test" text and apply color
// Cursor jumps to start of line instead of staying after "test"

Current Behavior

Issue 1 - Cursor Jumps:

  • When color is applied to selected text, cursor position resets
  • Occurs only on the first color change in a session
  • Happens regardless of selection position (beginning, middle, or end of line)
  • Using tr.setSelection() before dispatch does not prevent this

Issue 2 - Highlight Remains:

  • When changing text to the same color already applied
  • Or when changing part of colored text to a different color
  • The highlight/background color still displays even after applying new color
  • Example: Text with color: #000000; → change to color: #ffc90e; → still shows background highlight

Minimal Code Example

// Custom wrapper (attempting to override)
function colorSyntax(context, options) {
  const pluginInfo = originalColorSyntax(context, options);

  if (pluginInfo.wysiwygCommands?.color) {
    const originalCommand = pluginInfo.wysiwygCommands.color;
    
    pluginInfo.wysiwygCommands.color = (value, state, dispatch) => {
      const { selectedColor } = value;
      if (!selectedColor) return false;

      const { tr, selection, schema } = state;
      const from = Math.min(selection.anchor, selection.head);
      const to = Math.max(selection.anchor, selection.head);

      // Save selection
      const savedAnchor = selection.anchor;
      const savedHead = selection.head;

      // Process color
      let pos = from;
      while (pos < to) {
        const node = tr.doc.nodeAt(pos);
        if (node?.isText) {
          const spanMark = node.marks?.find(m => m.type === schema.marks.span);
          const currentStyle = spanMark?.attrs.htmlAttrs?.style || '';
          
          // Remove old color, add new color
          const newStyle = currentStyle
            .replace(/color\s*:\s*[^;]+;?/gi, '')
            .trim() + `;color: ${selectedColor};`;

          tr.removeMark(pos, pos + node.nodeSize, schema.marks.span);
          tr.addMark(pos, pos + node.nodeSize, schema.marks.span.create({
            htmlAttrs: { style: newStyle },
            htmlInline: true
          }));

          pos += node.nodeSize;
        } else {
          pos += 1;
        }
      }

      // Try to restore selection - DOES NOT WORK
      tr.setSelection(state.selection.constructor.create(tr.doc, savedAnchor, savedHead));
      dispatch(tr);

      return true;
    };
  }

  return pluginInfo;
}

Expected Behavior

  1. After applying color, cursor should remain at the end of the selection (anchor: 40, head: 41 → should stay the same)
  2. Changing to the same color should not display highlight
  3. Selection state should be preserved through the transaction

Actual Behavior

  • Cursor jumps to beginning of line
  • Selection is lost after color is applied
  • Highlight background persists even after color change

Attempted Solutions

  1. ✗ Saving/restoring selection with tr.setSelection() before dispatch
  2. ✗ Using editorNode.setSelection(savedSelection) after dispatch
  3. ✗ Wrapping dispatch with setTimeout()
  4. ✗ Not overriding command and calling original - cursor still jumps
  5. ✗ Intercepting UI events at color button level

Questions

  • Is there a known issue with selection preservation in color syntax plugin?
  • Should we be using a different approach (hooks, events) instead of command override?
  • Is the cursor jump behavior expected when modifying marks?
  • How can we properly preserve selection state in custom plugin wrappers?

Additional Context

Document structure (from state.doc):

{
  "type": "paragraph",
  "content": [
    {
      "type": "text",
      "marks": [
        {
          "type": "span",
          "attrs": {
            "htmlAttrs": {
              "style": "font-size: 22px;color: #000000;"
            },
            "htmlInline": true
          }
        }
      ],
      "text": "フォームからデータの登録ができます。"
    }
  ]
}

Selection state (before color apply):

anchor: 40
head: 41

Selection state (after color apply):

anchor: 0  // ❌ Should still be 40
head: 0    // ❌ Should still be 41

Related Issues

  • Similar to selection management issues in ProseMirror plugins
  • Toast UI Editor uses ProseMirror internally for WYSIWYG editing

Suggested Fix

  • Provide documentation on proper selection handling in custom plugins
  • Or: Ensure color command preserves selection state by default
  • Consider adding selection restoration utility function for plugin developers

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions