Remove the entire range of the mark, even if mark not fully selected

How to remove the entire mark from the current selection, even if only part of the mark is included in the selection.

For example here if the letter T is just selected from word Text, i want to remove the full link mark:

<Node>Some <a>Text</a></Node>

This functionality is useful for marks like link. Some editors like medium.com story editor implement this behavior.

This is something you’d have to implement yourself. Resolve a position inside the link and scan adjacent inline nodes to include the ones that have the same mark, then delete the range those cover.

Thanks @marijn , I take a fork from TipTap for the unsetMark command here: tiptap/packages/core/src/commands/unsetMark.ts at develop · ueberdosis/tiptap · GitHub, and i create a new option called fullyRemoveMark to full remove the mark.

This is the final result:

import { MarkType } from 'prosemirror-model';
import { EditorState, Transaction } from 'prosemirror-state';

import { getMarkRange } from './getMarkRange'; // From `TipTap`

const unsetMark =
  (
    type: MarkType,
    options: {
      /**
       * Removes the mark even across the current selection. Defaults to `false`.
       */
      extendEmptyMarkRange?: boolean;

      /**
       * Fully remove mark even if the selection includes part of it. Defaults to `false`.
       */
      fullyRemoveMark?: boolean;
    } = {}
  ) =>
  (state: EditorState, dispatch?: (tr: Transaction) => void) => {
    const { extendEmptyMarkRange = false, fullyRemoveMark = false } = options;
    const { selection, tr } = state;
    const { $from, empty, ranges } = selection;

    if (!dispatch) {
      return true;
    }

    if (empty && extendEmptyMarkRange) {
      // Handle empty selection with extended mark range
      let { from, to } = selection;
      const attrs = $from.marks().find(mark => mark.type === type)?.attrs;
      const range = getMarkRange($from, type, attrs);

      if (range) {
        from = range.from;
        to = range.to;
      }

      tr.removeMark(from, to, type);
    } else if (fullyRemoveMark) {
      // Handle full mark removal
      ranges.forEach(({ $from, $to }) => {
        let from = $from.pos;
        let to = $to.pos;

        // Expand the range to fully cover marks of the same type
        const startRange = getMarkRange($from, type);
        const endRange = getMarkRange($to, type);

        if (startRange) {
          from = Math.min(from, startRange.from);
        }
        if (endRange) {
          to = Math.max(to, endRange.to);
        }

        tr.removeMark(from, to, type);
      });
    } else {
      // Handle normal mark removal
      ranges.forEach(({ $from, $to }) => {
        tr.removeMark($from.pos, $to.pos, type);
      });
    }

    tr.removeStoredMark(type);

    dispatch(tr);
    return true;
  };

export default unsetMark;

May i will make a PR for this.