Skip to content

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

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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions