Skip to main content
deleted 2334 characters in body
Source Link

I have a custom colorSyntax plugin wrapper:

function colorSyntax(context: PluginContext, options: PluginOptions): PluginInfo {
  const pluginInfo = originalColorSyntax(context, options);

  if (pluginInfo.wysiwygCommands?.color) {
    pluginInfo.wysiwygCommands.color = (value, state, dispatch) => {
      const { selectedColor } = value as any;
      if (!selectedColor) return false;
      const { tr, selection, schema } = state as any;
      const hasRange = !selection.empty;
      selection.ranges.forEach((range: any) => {
        let start = range.$from.pos;
        let end = range.$to.pos;
        let pos = start;
        let prevStyle = null;
        let spanStart = pos;
        while (pos < end) {
          const node = tr.doc.nodeAt(pos);
          if (node && node.isText) {
            const marks = node.marks || [];
            const spanMark = marks.find((m: any) => m.type === schema.marks.span);
            const style = spanMark?.attrs.htmlAttrs?.style || '';
            if (prevStyle !== null && style !== prevStyle) {
              const mergedStyle = prevStyle.replace(/(?:^|;)\s*color\s*:[^;]+;?/i, '').trim();
              const finalStyle = `${mergedStyle ? mergedStyle + ';' : ''}color: ${selectedColor};`;
              const attrs = { htmlAttrs: { style: finalStyle } };
              tr.addMark(spanStart, pos, schema.marks.span.create(attrs));
              spanStart = pos;
            }
            prevStyle = style;
            pos += node.nodeSize;
          } else {
            pos += 1;
          }
        }
        if (prevStyle !== null && spanStart < end) {
          const mergedStyle = prevStyle.replace(/(?:^|;)\s*color\s*:[^;]+;?/i, '').trim();
          const finalStyle = `${mergedStyle ? mergedStyle + ';' : ''}color: ${selectedColor};`;
          const attrs = { htmlAttrs: { style: finalStyle } };
          tr.addMark(spanStart, end, schema.marks.span.create(attrs));
        }
      });
      // if (hasRange) {
        const caretPos = selection.ranges.reduce((max: number, r: any) => Math.max(max, r.$to.pos), 0);
        tr.setSelection(TextSelection.create(tr.doc, caretPos));
        tr.scrollIntoView();
      // }
      dispatch(tr);
      return true;
    };
  }
  return pluginInfo;
}

I have a custom colorSyntax plugin wrapper:

function colorSyntax(context: PluginContext, options: PluginOptions): PluginInfo {
  const pluginInfo = originalColorSyntax(context, options);

  if (pluginInfo.wysiwygCommands?.color) {
    pluginInfo.wysiwygCommands.color = (value, state, dispatch) => {
      const { selectedColor } = value as any;
      if (!selectedColor) return false;
      const { tr, selection, schema } = state as any;
      const hasRange = !selection.empty;
      selection.ranges.forEach((range: any) => {
        let start = range.$from.pos;
        let end = range.$to.pos;
        let pos = start;
        let prevStyle = null;
        let spanStart = pos;
        while (pos < end) {
          const node = tr.doc.nodeAt(pos);
          if (node && node.isText) {
            const marks = node.marks || [];
            const spanMark = marks.find((m: any) => m.type === schema.marks.span);
            const style = spanMark?.attrs.htmlAttrs?.style || '';
            if (prevStyle !== null && style !== prevStyle) {
              const mergedStyle = prevStyle.replace(/(?:^|;)\s*color\s*:[^;]+;?/i, '').trim();
              const finalStyle = `${mergedStyle ? mergedStyle + ';' : ''}color: ${selectedColor};`;
              const attrs = { htmlAttrs: { style: finalStyle } };
              tr.addMark(spanStart, pos, schema.marks.span.create(attrs));
              spanStart = pos;
            }
            prevStyle = style;
            pos += node.nodeSize;
          } else {
            pos += 1;
          }
        }
        if (prevStyle !== null && spanStart < end) {
          const mergedStyle = prevStyle.replace(/(?:^|;)\s*color\s*:[^;]+;?/i, '').trim();
          const finalStyle = `${mergedStyle ? mergedStyle + ';' : ''}color: ${selectedColor};`;
          const attrs = { htmlAttrs: { style: finalStyle } };
          tr.addMark(spanStart, end, schema.marks.span.create(attrs));
        }
      });
      // if (hasRange) {
        const caretPos = selection.ranges.reduce((max: number, r: any) => Math.max(max, r.$to.pos), 0);
        tr.setSelection(TextSelection.create(tr.doc, caretPos));
        tr.scrollIntoView();
      // }
      dispatch(tr);
      return true;
    };
  }
  return pluginInfo;
}
Source Link

Toast UI Editor: Selection/Cursor jumps to beginning after applying color with custom plugin

Problem

I'm using Toast UI Editor with the color syntax plugin. When I apply a color to selected text:

  1. Issue 1: The cursor jumps to the beginning of the line/paragraph (only on first color change)
  2. Issue 2: When changing text to the same color or partially changing color, the highlight still displays

Environment

  • Toast UI Editor: [version]
  • @toast-ui/editor-plugin-color-syntax: [version]
  • Browser: [browser + version]

I have a custom colorSyntax plugin wrapper:

function colorSyntax(context: PluginContext, options: PluginOptions): PluginInfo {
  const pluginInfo = originalColorSyntax(context, options);

  if (pluginInfo.wysiwygCommands?.color) {
    pluginInfo.wysiwygCommands.color = (value, state, dispatch) => {
      const { selectedColor } = value as any;
      if (!selectedColor) return false;
      const { tr, selection, schema } = state as any;
      const hasRange = !selection.empty;
      selection.ranges.forEach((range: any) => {
        let start = range.$from.pos;
        let end = range.$to.pos;
        let pos = start;
        let prevStyle = null;
        let spanStart = pos;
        while (pos < end) {
          const node = tr.doc.nodeAt(pos);
          if (node && node.isText) {
            const marks = node.marks || [];
            const spanMark = marks.find((m: any) => m.type === schema.marks.span);
            const style = spanMark?.attrs.htmlAttrs?.style || '';
            if (prevStyle !== null && style !== prevStyle) {
              const mergedStyle = prevStyle.replace(/(?:^|;)\s*color\s*:[^;]+;?/i, '').trim();
              const finalStyle = `${mergedStyle ? mergedStyle + ';' : ''}color: ${selectedColor};`;
              const attrs = { htmlAttrs: { style: finalStyle } };
              tr.addMark(spanStart, pos, schema.marks.span.create(attrs));
              spanStart = pos;
            }
            prevStyle = style;
            pos += node.nodeSize;
          } else {
            pos += 1;
          }
        }
        if (prevStyle !== null && spanStart < end) {
          const mergedStyle = prevStyle.replace(/(?:^|;)\s*color\s*:[^;]+;?/i, '').trim();
          const finalStyle = `${mergedStyle ? mergedStyle + ';' : ''}color: ${selectedColor};`;
          const attrs = { htmlAttrs: { style: finalStyle } };
          tr.addMark(spanStart, end, schema.marks.span.create(attrs));
        }
      });
      // if (hasRange) {
        const caretPos = selection.ranges.reduce((max: number, r: any) => Math.max(max, r.$to.pos), 0);
        tr.setSelection(TextSelection.create(tr.doc, caretPos));
        tr.scrollIntoView();
      // }
      dispatch(tr);
      return true;
    };
  }
  return pluginInfo;
}

Expected Behavior

  • Selection/cursor should remain at the end of selected text after color is applied
  • Changing to same color should remove the color (not display highlight)

Actual Behavior

  • Cursor jumps to beginning of line after applying color (first time only)
  • Highlight background still appears even after changing to same color

Attempts Made

  1. Tried setting tr.setSelection() before and after dispatch
  2. Tried saving/restoring selection using editorNode.getSelection()/setSelection()
  3. Tried wrapping dispatch with setTimeout
  4. Tried calling original command instead of custom implementation

None of these approaches solved the cursor jumping issue.

Question

  • How can I prevent the selection from being reset after the color command?
  • Is there a proper way to handle selection preservation in Toast UI Editor plugins?
  • Should I use a different approach (hooks, events, etc.) instead of overriding the command?

Any insights would be appreciated!