mirror of https://github.com/halo-dev/halo
				
				
				
			feat: range selection feature to default editor (#6117)
#### What type of PR is this? /kind feature /area editor /milestone 2.17.x #### What this PR does / why we need it: 为默认编辑器添加 `RangeSelection` 选择器。 <img width="989" alt="image" src="https://github.com/halo-dev/halo/assets/31335418/c976cf99-0d6e-4346-9b05-8b9b0dc95183"> 它的功能基本与 TextSelection 相反,例如: 1. TextSelection 支持光标展示,RangeSelection 不允许空内容,即它并不支持光标。 2. TextSelection 会抛弃被选择的 Node 节点部分偏移量,而 RangeSelection 会扩展偏移量至 Node 节点结束。 3. TextSelection 支持 Text 而 RangeSelection 支持 Node 节点。 `RangeSelection` 可以用于范围选中块节点并进行操作,可用于全选内容并进行删除操作。 #### How to test it? 测试使用点击,拖拽,释放鼠标的操作,能否选中某些节点。 测试删除选中的节点。 #### Which issue(s) this PR fixes: Fixes #5194 #### Does this PR introduce a user-facing change? ```release-note 为默认编辑器添加 RangeSelection 选择器 ```pull/6150/head
							parent
							
								
									c1ba566e08
								
							
						
					
					
						commit
						ba2987b585
					
				| 
						 | 
				
			
			@ -46,6 +46,7 @@ import {
 | 
			
		|||
  ExtensionSearchAndReplace,
 | 
			
		||||
  ExtensionClearFormat,
 | 
			
		||||
  ExtensionFormatBrush,
 | 
			
		||||
  ExtensionRangeSelection,
 | 
			
		||||
} from "../index";
 | 
			
		||||
 | 
			
		||||
const content = useLocalStorage("content", "");
 | 
			
		||||
| 
						 | 
				
			
			@ -114,6 +115,7 @@ const editor = useEditor({
 | 
			
		|||
    ExtensionSearchAndReplace,
 | 
			
		||||
    ExtensionClearFormat,
 | 
			
		||||
    ExtensionFormatBrush,
 | 
			
		||||
    ExtensionRangeSelection,
 | 
			
		||||
  ],
 | 
			
		||||
  parseOptions: {
 | 
			
		||||
    preserveWhitespace: true,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -100,6 +100,9 @@ const getRenderContainer = (node: HTMLElement) => {
 | 
			
		|||
export default CodeBlockLowlight.extend<
 | 
			
		||||
  CustomCodeBlockLowlightOptions & CodeBlockLowlightOptions
 | 
			
		||||
>({
 | 
			
		||||
  // It needs to have a higher priority than range-selection,
 | 
			
		||||
  // otherwise the Mod-a shortcut key will be overridden.
 | 
			
		||||
  priority: 110,
 | 
			
		||||
  addCommands() {
 | 
			
		||||
    return {
 | 
			
		||||
      ...this.parent?.(),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -175,6 +175,7 @@ const Columns = Node.create({
 | 
			
		|||
  isolating: true,
 | 
			
		||||
  allowGapCursor: false,
 | 
			
		||||
  content: "column{1,}",
 | 
			
		||||
  fakeSelection: false,
 | 
			
		||||
 | 
			
		||||
  addOptions() {
 | 
			
		||||
    return {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -45,6 +45,7 @@ import ExtensionTrailingNode from "./trailing-node";
 | 
			
		|||
import ExtensionSearchAndReplace from "./search-and-replace";
 | 
			
		||||
import ExtensionClearFormat from "./clear-format";
 | 
			
		||||
import ExtensionFormatBrush from "./format-brush";
 | 
			
		||||
import { ExtensionRangeSelection, RangeSelection } from "./range-selection";
 | 
			
		||||
 | 
			
		||||
const allExtensions = [
 | 
			
		||||
  ExtensionBlockquote,
 | 
			
		||||
| 
						 | 
				
			
			@ -104,6 +105,7 @@ const allExtensions = [
 | 
			
		|||
  ExtensionSearchAndReplace,
 | 
			
		||||
  ExtensionClearFormat,
 | 
			
		||||
  ExtensionFormatBrush,
 | 
			
		||||
  ExtensionRangeSelection,
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
| 
						 | 
				
			
			@ -153,4 +155,6 @@ export {
 | 
			
		|||
  ExtensionSearchAndReplace,
 | 
			
		||||
  ExtensionClearFormat,
 | 
			
		||||
  ExtensionFormatBrush,
 | 
			
		||||
  ExtensionRangeSelection,
 | 
			
		||||
  RangeSelection,
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,163 @@
 | 
			
		|||
import {
 | 
			
		||||
  Decoration,
 | 
			
		||||
  DecorationSet,
 | 
			
		||||
  EditorView,
 | 
			
		||||
  Extension,
 | 
			
		||||
  Plugin,
 | 
			
		||||
  PluginKey,
 | 
			
		||||
  callOrReturn,
 | 
			
		||||
  getExtensionField,
 | 
			
		||||
  type ParentConfig,
 | 
			
		||||
} from "@/tiptap";
 | 
			
		||||
import RangeSelection from "./range-selection";
 | 
			
		||||
 | 
			
		||||
declare module "@tiptap/core" {
 | 
			
		||||
  export interface NodeConfig<Options, Storage> {
 | 
			
		||||
    /**
 | 
			
		||||
     * Whether to allow displaying a fake selection state on the node.
 | 
			
		||||
     *
 | 
			
		||||
     * Typically, it is only necessary to display a fake selection state on child nodes,
 | 
			
		||||
     * so the parent node can be set to false.
 | 
			
		||||
     *
 | 
			
		||||
     * default: true
 | 
			
		||||
     */
 | 
			
		||||
    fakeSelection?:
 | 
			
		||||
      | boolean
 | 
			
		||||
      | null
 | 
			
		||||
      | ((this: {
 | 
			
		||||
          name: string;
 | 
			
		||||
          options: Options;
 | 
			
		||||
          storage: Storage;
 | 
			
		||||
          parent: ParentConfig<NodeConfig<Options>>["fakeSelection"];
 | 
			
		||||
        }) => boolean | null);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const range = {
 | 
			
		||||
  anchor: 0,
 | 
			
		||||
  head: 0,
 | 
			
		||||
  enable: false,
 | 
			
		||||
};
 | 
			
		||||
const ExtensionRangeSelection = Extension.create({
 | 
			
		||||
  priority: 100,
 | 
			
		||||
  name: "rangeSelectionExtension",
 | 
			
		||||
 | 
			
		||||
  addProseMirrorPlugins() {
 | 
			
		||||
    return [
 | 
			
		||||
      new Plugin({
 | 
			
		||||
        key: new PluginKey("rangeSelectionPlugin"),
 | 
			
		||||
        props: {
 | 
			
		||||
          decorations: ({ doc, selection }) => {
 | 
			
		||||
            const { isEditable, isFocused } = this.editor;
 | 
			
		||||
            if (!isEditable || !isFocused) {
 | 
			
		||||
              return null;
 | 
			
		||||
            }
 | 
			
		||||
            if (!(selection instanceof RangeSelection)) {
 | 
			
		||||
              return null;
 | 
			
		||||
            }
 | 
			
		||||
            const { $from, $to } = selection;
 | 
			
		||||
            const decorations: Decoration[] = [];
 | 
			
		||||
            doc.nodesBetween($from.pos, $to.pos, (node, pos) => {
 | 
			
		||||
              if (node.isText || node.type.name === "paragraph") {
 | 
			
		||||
                return;
 | 
			
		||||
              }
 | 
			
		||||
              let className = "no-selection";
 | 
			
		||||
              if (node.type.spec.fakeSelection) {
 | 
			
		||||
                className = className + " range-fake-selection";
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              decorations.push(
 | 
			
		||||
                Decoration.node(pos, pos + node.nodeSize, {
 | 
			
		||||
                  class: className,
 | 
			
		||||
                })
 | 
			
		||||
              );
 | 
			
		||||
            });
 | 
			
		||||
            return DecorationSet.create(doc, decorations);
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          createSelectionBetween: (view, anchor, head) => {
 | 
			
		||||
            if (anchor.pos === head.pos) {
 | 
			
		||||
              return null;
 | 
			
		||||
            }
 | 
			
		||||
            return RangeSelection.valid(view.state, anchor.pos, head.pos)
 | 
			
		||||
              ? new RangeSelection(anchor, head)
 | 
			
		||||
              : null;
 | 
			
		||||
          },
 | 
			
		||||
          handleDOMEvents: {
 | 
			
		||||
            mousedown: (view: EditorView, event) => {
 | 
			
		||||
              const coords = { left: event.clientX, top: event.clientY };
 | 
			
		||||
              const $pos = view.posAtCoords(coords);
 | 
			
		||||
              if (!$pos || !$pos.pos) {
 | 
			
		||||
                return;
 | 
			
		||||
              }
 | 
			
		||||
              range.enable = true;
 | 
			
		||||
              range.anchor = $pos.pos;
 | 
			
		||||
            },
 | 
			
		||||
            mousemove: (view, event) => {
 | 
			
		||||
              if (!range.enable) {
 | 
			
		||||
                return;
 | 
			
		||||
              }
 | 
			
		||||
              const coords = { left: event.clientX, top: event.clientY };
 | 
			
		||||
              const $pos = view.posAtCoords(coords);
 | 
			
		||||
              if (
 | 
			
		||||
                !$pos ||
 | 
			
		||||
                !$pos.pos ||
 | 
			
		||||
                $pos.pos === range.anchor ||
 | 
			
		||||
                $pos.pos === range.head
 | 
			
		||||
              ) {
 | 
			
		||||
                return;
 | 
			
		||||
              }
 | 
			
		||||
              range.head = $pos.pos;
 | 
			
		||||
              const selection = RangeSelection.between(
 | 
			
		||||
                view.state.doc.resolve(range.anchor),
 | 
			
		||||
                view.state.doc.resolve(range.head)
 | 
			
		||||
              );
 | 
			
		||||
              if (selection) {
 | 
			
		||||
                view.dispatch(view.state.tr.setSelection(selection));
 | 
			
		||||
              }
 | 
			
		||||
            },
 | 
			
		||||
            mouseup: () => {
 | 
			
		||||
              range.enable = false;
 | 
			
		||||
              range.anchor = 0;
 | 
			
		||||
              range.head = 0;
 | 
			
		||||
            },
 | 
			
		||||
            mouseleave: () => {
 | 
			
		||||
              range.enable = false;
 | 
			
		||||
              range.anchor = 0;
 | 
			
		||||
              range.head = 0;
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
    ];
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  addKeyboardShortcuts() {
 | 
			
		||||
    return {
 | 
			
		||||
      "Mod-a": ({ editor }) => {
 | 
			
		||||
        editor.view.dispatch(
 | 
			
		||||
          editor.view.state.tr.setSelection(
 | 
			
		||||
            RangeSelection.allRange(editor.view.state.doc)
 | 
			
		||||
          )
 | 
			
		||||
        );
 | 
			
		||||
        return true;
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  extendNodeSchema(extension) {
 | 
			
		||||
    const context = {
 | 
			
		||||
      name: extension.name,
 | 
			
		||||
      options: extension.options,
 | 
			
		||||
      storage: extension.storage,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      fakeSelection:
 | 
			
		||||
        callOrReturn(getExtensionField(extension, "fakeSelection", context)) ??
 | 
			
		||||
        true,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export { RangeSelection, ExtensionRangeSelection };
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,190 @@
 | 
			
		|||
import {
 | 
			
		||||
  ResolvedPos,
 | 
			
		||||
  Selection,
 | 
			
		||||
  Node,
 | 
			
		||||
  type Mappable,
 | 
			
		||||
  EditorState,
 | 
			
		||||
} from "@/tiptap/pm";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The RangeSelection class represents a selection range within a document.
 | 
			
		||||
 * The content can include text, paragraphs, block-level content, etc.
 | 
			
		||||
 *
 | 
			
		||||
 * It has a starting position and an ending position. When the given range includes block-level content,
 | 
			
		||||
 * the RangeSelection will automatically expand to include the block-level content at the corresponding depth.
 | 
			
		||||
 *
 | 
			
		||||
 * The RangeSelection must not contain empty content.
 | 
			
		||||
 */
 | 
			
		||||
class RangeSelection extends Selection {
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates a RangeSelection between the specified positions.
 | 
			
		||||
   *
 | 
			
		||||
   * @param $anchor - The starting position of the selection.
 | 
			
		||||
   * @param $head - The ending position of the selection.
 | 
			
		||||
   */
 | 
			
		||||
  constructor($anchor: ResolvedPos, $head: ResolvedPos) {
 | 
			
		||||
    checkRangeSelection($anchor, $head);
 | 
			
		||||
    super($anchor, $head);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  map(doc: Node, mapping: Mappable): Selection {
 | 
			
		||||
    const $head = doc.resolve(mapping.map(this.head));
 | 
			
		||||
    const $anchor = doc.resolve(mapping.map(this.anchor));
 | 
			
		||||
    return new RangeSelection($anchor, $head);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  eq(other: Selection): boolean {
 | 
			
		||||
    return (
 | 
			
		||||
      other instanceof RangeSelection &&
 | 
			
		||||
      other.anchor == this.anchor &&
 | 
			
		||||
      other.head == this.head
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBookmark() {
 | 
			
		||||
    return new RangeBookmark(this.anchor, this.head);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  toJSON(): any {
 | 
			
		||||
    return { type: "range", anchor: this.anchor, head: this.head };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Validates if the given positions can form a valid RangeSelection in the given state.
 | 
			
		||||
   *
 | 
			
		||||
   * @param state - The editor state.
 | 
			
		||||
   * @param anchor - The starting position.
 | 
			
		||||
   * @param head - The ending position.
 | 
			
		||||
   * @returns True if the positions form a valid RangeSelection, otherwise false.
 | 
			
		||||
   */
 | 
			
		||||
  static valid(state: EditorState, anchor: number, head: number) {
 | 
			
		||||
    const nodes = rangeNodesBetween(
 | 
			
		||||
      state.doc.resolve(anchor),
 | 
			
		||||
      state.doc.resolve(head)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (nodes.length === 0) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (nodes.reverse()[0].pos < 0) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns a RangeSelection spanning the given positions.
 | 
			
		||||
   *
 | 
			
		||||
   * When the given range includes block-level content, if only a part is included,
 | 
			
		||||
   * the selection will be expanded to encompass the block-level content at the corresponding depth.
 | 
			
		||||
   *
 | 
			
		||||
   * Expansion: If the selection includes all depth nodes of the current block-level content but not the entire last node,
 | 
			
		||||
   * the selection will be expanded to include the node at that depth.
 | 
			
		||||
   *
 | 
			
		||||
   * @param $anchor - The starting position of the selection.
 | 
			
		||||
   * @param $head - The ending position of the selection.
 | 
			
		||||
   * @returns A new RangeSelection that spans the given positions.
 | 
			
		||||
   */
 | 
			
		||||
  static between($anchor: ResolvedPos, $head: ResolvedPos) {
 | 
			
		||||
    checkRangeSelection($anchor, $head);
 | 
			
		||||
 | 
			
		||||
    const doc = $anchor.doc;
 | 
			
		||||
    const dir = $anchor.pos < $head.pos ? 1 : -1;
 | 
			
		||||
    const anchorPos = dir > 0 ? $anchor.pos : $head.pos;
 | 
			
		||||
    const headPos = dir > 0 ? $head.pos : $anchor.pos;
 | 
			
		||||
    const nodes = rangeNodesBetween($anchor, $head);
 | 
			
		||||
 | 
			
		||||
    if (nodes.length === 0) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const lastNode = nodes[nodes.length - 1];
 | 
			
		||||
    if (lastNode.pos < 0) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let fromOffset = 0;
 | 
			
		||||
    nodes.forEach(({ pos }) => {
 | 
			
		||||
      if (pos < 0) {
 | 
			
		||||
        fromOffset = pos;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const toOffset =
 | 
			
		||||
      headPos - anchorPos - lastNode.pos - lastNode.node.nodeSize;
 | 
			
		||||
    const anchor =
 | 
			
		||||
      dir > 0
 | 
			
		||||
        ? anchorPos + fromOffset
 | 
			
		||||
        : headPos - (toOffset > 0 ? 0 : toOffset);
 | 
			
		||||
    const head =
 | 
			
		||||
      dir > 0
 | 
			
		||||
        ? headPos - (toOffset > 0 ? 0 : toOffset)
 | 
			
		||||
        : anchorPos + fromOffset;
 | 
			
		||||
    return new RangeSelection(doc.resolve(anchor), doc.resolve(head));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static fromJSON(doc: Node, json: any) {
 | 
			
		||||
    if (typeof json.anchor != "number" || typeof json.head != "number") {
 | 
			
		||||
      throw new RangeError("Invalid input for RangeSelection.fromJSON");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return new RangeSelection(doc.resolve(json.anchor), doc.resolve(json.head));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static create(doc: Node, anchor: number, head: number) {
 | 
			
		||||
    return new this(doc.resolve(anchor), doc.resolve(head));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static allRange(doc: Node) {
 | 
			
		||||
    return new RangeSelection(doc.resolve(0), doc.resolve(doc.content.size));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Selection.jsonID("range", RangeSelection);
 | 
			
		||||
 | 
			
		||||
class RangeBookmark {
 | 
			
		||||
  constructor(readonly anchor: number, readonly head: number) {}
 | 
			
		||||
 | 
			
		||||
  map(mapping: Mappable) {
 | 
			
		||||
    return new RangeBookmark(mapping.map(this.anchor), mapping.map(this.head));
 | 
			
		||||
  }
 | 
			
		||||
  resolve(doc: Node) {
 | 
			
		||||
    return new RangeSelection(doc.resolve(this.anchor), doc.resolve(this.head));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function checkRangeSelection($anchor: ResolvedPos, $head: ResolvedPos) {
 | 
			
		||||
  if ($anchor.pos === $head.pos) {
 | 
			
		||||
    console.warn("The RangeSelection cannot be empty.");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function rangeNodesBetween($anchor: ResolvedPos, $head: ResolvedPos) {
 | 
			
		||||
  const doc = $anchor.doc;
 | 
			
		||||
  const dir = $anchor.pos < $head.pos ? 1 : -1;
 | 
			
		||||
  const anchorPos = dir > 0 ? $anchor.pos : $head.pos;
 | 
			
		||||
  const headPos = dir > 0 ? $head.pos : $anchor.pos;
 | 
			
		||||
 | 
			
		||||
  const nodes: Array<{
 | 
			
		||||
    node: Node;
 | 
			
		||||
    pos: number;
 | 
			
		||||
    parent: Node | null;
 | 
			
		||||
    index: number;
 | 
			
		||||
  }> = [];
 | 
			
		||||
  doc.nodesBetween(
 | 
			
		||||
    anchorPos,
 | 
			
		||||
    headPos,
 | 
			
		||||
    (node, pos, parent, index) => {
 | 
			
		||||
      if (node.isText || node.type.name === "paragraph") {
 | 
			
		||||
        return true;
 | 
			
		||||
      }
 | 
			
		||||
      nodes.push({ node, pos, parent, index });
 | 
			
		||||
    },
 | 
			
		||||
    -anchorPos
 | 
			
		||||
  );
 | 
			
		||||
  return nodes;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default RangeSelection;
 | 
			
		||||
| 
						 | 
				
			
			@ -208,6 +208,8 @@ class TableView implements NodeView {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
const Table = TiptapTable.extend<ExtensionOptions & TableOptions>({
 | 
			
		||||
  fakeSelection: false,
 | 
			
		||||
 | 
			
		||||
  addExtensions() {
 | 
			
		||||
    return [TableCell, TableRow, TableHeader];
 | 
			
		||||
  },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@ import { TableRow as BuiltInTableRow } from "@tiptap/extension-table-row";
 | 
			
		|||
 | 
			
		||||
const TableRow = BuiltInTableRow.extend({
 | 
			
		||||
  allowGapCursor: false,
 | 
			
		||||
  fakeSelection: false,
 | 
			
		||||
 | 
			
		||||
  addAttributes() {
 | 
			
		||||
    return {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,3 +5,4 @@
 | 
			
		|||
@import "./search.scss";
 | 
			
		||||
@import "./format-brush.scss";
 | 
			
		||||
@import "./node-select.scss";
 | 
			
		||||
@import "./range-selection.scss";
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,33 @@
 | 
			
		|||
.halo-rich-text-editor {
 | 
			
		||||
  $editorRangeFakeSelection: rgba(27, 162, 227, 0.2);
 | 
			
		||||
 | 
			
		||||
  .no-selection {
 | 
			
		||||
    *::selection {
 | 
			
		||||
      background-color: transparent;
 | 
			
		||||
      color: inherit;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .range-fake-selection {
 | 
			
		||||
    position: relative;
 | 
			
		||||
 | 
			
		||||
    &::after {
 | 
			
		||||
      content: " ";
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      top: 0;
 | 
			
		||||
      left: 0;
 | 
			
		||||
      right: 0;
 | 
			
		||||
      bottom: 0;
 | 
			
		||||
      background-color: $editorRangeFakeSelection;
 | 
			
		||||
      pointer-events: none;
 | 
			
		||||
      z-index: 99;
 | 
			
		||||
      caret-color: transparent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.column {
 | 
			
		||||
      &::after {
 | 
			
		||||
        border-radius: 5px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -49,6 +49,7 @@ import {
 | 
			
		|||
  ToolboxItem,
 | 
			
		||||
  lowlight,
 | 
			
		||||
  type AnyExtension,
 | 
			
		||||
  ExtensionRangeSelection,
 | 
			
		||||
} from "@halo-dev/richtext-editor";
 | 
			
		||||
// ui custom extension
 | 
			
		||||
import { i18n } from "@/locales";
 | 
			
		||||
| 
						 | 
				
			
			@ -401,6 +402,7 @@ onMounted(async () => {
 | 
			
		|||
      ExtensionSearchAndReplace,
 | 
			
		||||
      ExtensionClearFormat,
 | 
			
		||||
      ExtensionFormatBrush,
 | 
			
		||||
      ExtensionRangeSelection,
 | 
			
		||||
    ],
 | 
			
		||||
    parseOptions: {
 | 
			
		||||
      preserveWhitespace: true,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue