All files / roosterjs-content-model-plugins/lib/edit/tabUtils handleTabOnParagraph.ts

98.04% Statements 50/51
86.84% Branches 33/38
100% Functions 6/6
97.83% Lines 45/46

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 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 1211x 1x                       1x 1x                                 1x           68x   27x 33x 27x 6x 6x 6x         1x   5x             21x 4x 4x   4x 12x 4x 4x   4x     4x 4x 4x       4x   4x                   17x 37x     17x 9x 9x   9x   8x 8x   8x 8x   8x 3x 5x 1x 1x     4x           22x 22x    
import { setModelIndentation } from 'roosterjs-content-model-api';
import {
    createSelectionMarker,
    createText,
    mutateBlock,
    mutateSegment,
} from 'roosterjs-content-model-dom';
import type {
    FormatContentModelContext,
    ReadonlyContentModelDocument,
    ReadonlyContentModelParagraph,
} from 'roosterjs-content-model-types';
 
const tabSpaces = '    ';
const space = ' ';
 
/**
 * @internal
 The handleTabOnParagraph function will handle the tab key in following scenarios:
 * 1. When the selection is collapsed and the cursor is at the end of a paragraph, add 4 spaces.
 * 2. When the selection is collapsed and the cursor is at the start of a paragraph, call setModelIndention function to indent the whole paragraph
 * 3. When the selection is collapsed and the cursor is at the middle of a paragraph, add 4 spaces.
 * 4. When the selection is not collapsed, replace the selected range with a single space.
 * 5. When the selection is not collapsed, but all segments are selected, call setModelIndention function to indent the whole paragraph
 The handleTabOnParagraph function will handle the shift + tab key in a indented paragraph in following scenarios:
 * 1. When the selection is collapsed and the cursor is at the end of a paragraph, remove 4 spaces.
 * 2. When the selection is collapsed and the cursor is at the start of a paragraph, call setModelIndention function to outdent the whole paragraph
 * 3. When the selection is collapsed and the cursor is at the middle of a paragraph, remove 4 spaces.
 * 4. When the selection is not collapsed, replace the selected range with a 4 space.
 * 5. When the selection is not collapsed, but all segments are selected, call setModelIndention function to outdent the whole paragraph
 */
export function handleTabOnParagraph(
    model: ReadonlyContentModelDocument,
    paragraph: ReadonlyContentModelParagraph,
    rawEvent: KeyboardEvent,
    context?: FormatContentModelContext
) {
    const selectedSegments = paragraph.segments.filter(segment => segment.isSelected);
    const isCollapsed =
        selectedSegments.length === 1 && selectedSegments[0].segmentType === 'SelectionMarker';
    const isAllSelected = paragraph.segments.every(segment => segment.isSelected);
    if ((paragraph.segments[0].segmentType === 'SelectionMarker' && isCollapsed) || isAllSelected) {
        const { marginLeft, marginRight, direction } = paragraph.format;
        const isRtl = direction === 'rtl';
        if (
            rawEvent.shiftKey &&
            ((!isRtl && (!marginLeft || marginLeft == '0px')) ||
                (isRtl && (!marginRight || marginRight == '0px')))
        ) {
            return false;
        }
        setModelIndentation(
            model,
            rawEvent.shiftKey ? 'outdent' : 'indent',
            undefined /*length*/,
            context
        );
    } else {
        if (!isCollapsed) {
            let firstSelectedSegmentIndex: number | undefined = undefined;
            let lastSelectedSegmentIndex: number | undefined = undefined;
 
            paragraph.segments.forEach((segment, index) => {
                if (segment.isSelected) {
                    Eif (!firstSelectedSegmentIndex) {
                        firstSelectedSegmentIndex = index;
                    }
                    lastSelectedSegmentIndex = index;
                }
            });
            Eif (firstSelectedSegmentIndex && lastSelectedSegmentIndex) {
                const firstSelectedSegment = paragraph.segments[firstSelectedSegmentIndex];
                const spaceText = createText(
                    rawEvent.shiftKey ? tabSpaces : space,
                    firstSelectedSegment.format
                );
                const marker = createSelectionMarker(firstSelectedSegment.format);
 
                mutateBlock(paragraph).segments.splice(
                    firstSelectedSegmentIndex,
                    lastSelectedSegmentIndex - firstSelectedSegmentIndex + 1,
                    spaceText,
                    marker
                );
            } else {
                return false;
            }
        } else {
            const markerIndex = paragraph.segments.findIndex(
                segment => segment.segmentType === 'SelectionMarker'
            );
 
            if (!rawEvent.shiftKey) {
                const markerFormat = paragraph.segments[markerIndex].format;
                const tabText = createText(tabSpaces, markerFormat);
 
                mutateBlock(paragraph).segments.splice(markerIndex, 0, tabText);
            } else {
                const tabText = paragraph.segments[markerIndex - 1];
                const tabSpacesLength = tabSpaces.length;
 
                Eif (tabText.segmentType == 'Text') {
                    const tabSpaceTextLength = tabText.text.length - tabSpacesLength;
 
                    if (tabText.text === tabSpaces) {
                        mutateBlock(paragraph).segments.splice(markerIndex - 1, 1);
                    } else if (tabText.text.substring(tabSpaceTextLength) === tabSpaces) {
                        mutateSegment(paragraph, tabText, text => {
                            text.text = text.text.substring(0, tabSpaceTextLength);
                        });
                    } else {
                        return false;
                    }
                }
            }
        }
    }
    rawEvent.preventDefault();
    return true;
}