Hi @marijn , @bZichett , @MakarovCode ,
I really appreciate your comments.
I’ve finally managed to have this feature working 
Multi Range Selection class
The custom MultiRangeSelection class extends the Selection class and stores the multiple ranges as an array in the ranges property.
import { Node } from "prosemirror-model"
import { Mappable } from "prosemirror-transform"
import { Selection, SelectionRange } from 'prosemirror-state';
/** A text Multi Range Selection represents a multiple selection */
export class MultiRangeSelection extends Selection {
ranges: SelectionRange[];
/** Construct a Multi Range Selection including the given Ranges. */
constructor(ranges: SelectionRange[]) {
super(ranges[0].$from, ranges[ranges.length - 1].$to, ranges);
this.ranges = ranges;
}
map(doc: Node, mapping: Mappable): Selection {
return new MultiRangeSelection(this.ranges);
}
eq(other: Selection): boolean {
if (!(other instanceof MultiRangeSelection)) return false;
if (this.ranges.length !== other.ranges.length) return false;
for (let i = 0; i < this.ranges.length; i++) {
if (this.ranges[i].$from !== other.ranges[i].$from ||
this.ranges[i].$to !== other.ranges[i].$to) {
return false;
}
}
return true;
}
toJSON(): any {
return { type: "multiRangeSelection", ranges: this.ranges }
}
/** Create a Multi Range Selection for the selected Ranges. */
static create(ranges: SelectionRange[]) {
return new MultiRangeSelection(ranges);
}
}
Multiple Selection Handlers
The EditorView is set up to listen for keydown events, and when the Ctrl key is pressed, a new MultiRangeSelection instance is created and dispatched to the editor.
/**
* Adds the required Listeners to handle the multiple text selection: ctrl + selection
*/
multipleSelectionHandlers() {
this.editor.view.dom.addEventListener("mousedown", (event) => {
if (!event.ctrlKey) {
this.selectedRanges = [];
}
});
this.editor.view.dom.addEventListener("mouseup", (event) => {
const { dispatch, state } = this.editor.view;
const { tr, selection } = state;
/** If we are pressing ctrl and there is already a selected range */
if (event.ctrlKey) {
/** We add the new selected range */
this.selectedRanges.push(selection.ranges[0]);
/** We must sort the selected ranges by position */
this.selectedRanges = this.selectedRanges.sort((n1, n2) => n1.$from.pos - n2.$from.pos);
/** We set our multi range selection passing the selected ranges */
tr.setSelection(MultiRangeSelection.create(this.selectedRanges));
/** We dispatch the transaction */
dispatch?.(tr);
} else {
/** We store the first selection */
this.selectedRanges = [selection.ranges[0]];
}
});
}
Multiple Selection Decoration
I have created a plugin where we can access the multiple ranges stored in the MultiRangeSelection instance and accessing the ranges property, giving the desired decoration.
To visualize the current selection in ProseMirror, we add a custom decorator to the state
that will be responsible for rendering the selected text with a visual indicator such as a background color.
const multipleSelectionDecoration = (): Plugin => {
return new Plugin({
props: {
decorations(state) {
/** We just need this decorator when the selection is multiple */
if (state.selection instanceof MultiRangeSelection) {
const { doc } = state;
const decorations = [];
/** Goes through all the nodes and add the class multiple-selection in order to make the ::selection background transparent */
state.doc.nodesBetween(0, doc.nodeSize - 2, (node, position) => {
decorations.push(Decoration.inline(position, position + node.nodeSize, { class: "multiple-selection" }));
});
const { ranges } = state.selection;
/**
* The code first accesses the ranges of the current selection, and then loops over each range, creating a new Decoration instance for each range.
* The Decoration class is used to define a range of text that should be decorated with a specific style.
* In this case, we're setting the class property to "selected-text", which we can then use in our CSS stylesheet to style the selected text
*/
for (let i = 0; i < ranges.length; i++) {
const { $from, $to } = ranges[i];
if ($from && $to) {
decorations.push(Decoration.inline($from.pos, $to.pos, { class: "selected-text" }));
}
}
/**
* We use the setMeta method on the transaction to associate the decorations with the current state.
* This will cause the decorations to be rendered whenever the state is updated.
*/
state.tr.setMeta("decorations", decorations);
return DecorationSet.create(doc, decorations);
}
}
}
})
}
Multipe Selection Style
To highlight just the text contained in the selection ranges in ProseMirror, we use the CSS property “background-color” on the selected text nodes. In order to avoid having the whole block selected, we remove the background-color for the ::selection of the other nodes.
.multiple-selection::selection {
background-color: transparent;
}
.selected-text::selection {
background-color: #3297FD;
color: white;
}