All files / mutxt-react/src/MuTxt/behavior selectAllGuard.ts

83.33% Statements 50/60
72.41% Branches 21/29
87.5% Functions 7/8
85.71% Lines 42/49

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 861x       1x 1x 1x 2x   1x         1x 7x 7x 6x 6x 6x 6x 6x     6x 6x 6x       6x 6x               1x 4x 4x 4x 9x   4x 4x 4x 4x                   1x 5x 5x 2x 2x 1x 1x       5x 2x 2x 1x 1x       5x             5x    
import {Editor, type Node, Path, Range, Text, Transforms} from 'slate';
import type {CustomElement} from '../types';
 
/** Sum of plain-text character counts across every `Text` node in the doc. */
export const docPlainTextLength = (editor: Editor): number => {
  let total = 0;
  for (const [node] of Editor.nodes(editor, {at: [], match: Text.isText})) {
    total += (node as {text: string}).text.length;
  }
  return total;
};
 
/** Returns `true` when the selection is an expanded range that covers the
 * entire user-visible document. */
export const isFullDocSelected = (editor: Editor): boolean => {
  const {selection} = editor;
  if (!selection || Range.isCollapsed(selection)) return false;
  const children = editor.children;
  Iif (!children.length) return false;
  const firstIdx = 0;
  const lastIdx = children.length - 1;
  Iif (lastIdx < firstIdx) return false;
  let docStart: ReturnType<typeof Editor.start>;
  let docEnd: ReturnType<typeof Editor.end>;
  try {
    docStart = Editor.start(editor, [firstIdx]);
    docEnd = Editor.end(editor, [lastIdx]);
  } catch {
    return false;
  }
  const [selStart, selEnd] = Range.edges(selection);
  return (
    Path.equals(selStart.path, docStart.path) &&
    selStart.offset === docStart.offset &&
    Path.equals(selEnd.path, docEnd.path) &&
    selEnd.offset === docEnd.offset
  );
};
 
export const resetDocumentContent = (editor: Editor, text = ''): void => {
  Editor.withoutNormalizing(editor, () => {
    const firstIdx = 0;
    for (let i = editor.children.length - 1; i >= firstIdx; i--) {
      Transforms.removeNodes(editor, {at: [i]});
    }
    const paragraph: CustomElement = {type: 'p', children: [{text}]};
    Transforms.insertNodes(editor, paragraph, {at: [firstIdx]});
    const caret = text ? Editor.end(editor, [firstIdx]) : Editor.start(editor, [firstIdx]);
    Transforms.select(editor, caret);
  });
};
 
export interface SelectAllGuardHooks {
  onDelete?: () => boolean;
  onReplaceWithText?: (text: string) => boolean;
  onReplaceWithFragment?: (fragment: Node[]) => boolean;
}
 
export const withSelectAllGuard = <T extends Editor>(editor: T, hooks: SelectAllGuardHooks): T => {
  const {deleteFragment, insertText, insertFragment} = editor;
  editor.deleteFragment = (direction) => {
    Eif (isFullDocSelected(editor)) {
      if (hooks.onDelete?.()) return;
      resetDocumentContent(editor);
      return;
    }
    deleteFragment(direction);
  };
  editor.insertText = (text) => {
    Eif (text && isFullDocSelected(editor)) {
      if (hooks.onReplaceWithText?.(text)) return;
      resetDocumentContent(editor, text);
      return;
    }
    insertText(text);
  };
  editor.insertFragment = (fragment) => {
    if (isFullDocSelected(editor)) {
      if (hooks.onReplaceWithFragment?.(fragment)) return;
      resetDocumentContent(editor);
    }
    insertFragment(fragment);
  };
  return editor;
};