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

100% Statements 31/31
100% Branches 20/20
100% Functions 4/4
100% Lines 30/30

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 1001x                                 1x             17x 17x   17x     9x     9x       1x 1x         7x 7x 7x   7x   3x         7x       7x             1x 25x                 1x 17x       15x   15x 15x   15x 8x   8x 4x 4x         15x 4x      
import {
    mutateBlock,
    normalizeContentModel,
    setParagraphNotImplicit,
} from 'roosterjs-content-model-dom';
import type {
    DeleteResult,
    FormatContentModelContext,
    IEditor,
    ReadonlyContentModelBlockGroup,
    ReadonlyContentModelDocument,
} from 'roosterjs-content-model-types';
 
/**
 * @internal
 * @return True means content is changed, so need to rewrite content model to editor. Otherwise false
 */
export function handleKeyboardEventResult(
    editor: IEditor,
    model: ReadonlyContentModelDocument,
    rawEvent: KeyboardEvent,
    result: DeleteResult,
    context: FormatContentModelContext
): boolean {
    context.skipUndoSnapshot = true;
    context.clearModelCache = false;
 
    switch (result) {
        case 'notDeleted':
            // We have not delete anything, we will let browser handle this event, so that current cached model may be invalid
            context.clearModelCache = true;
 
            // Return false here since we didn't do any change to Content Model, so no need to rewrite with Content Model
            return false;
 
        case 'nothingToDelete':
            // We known there is nothing to delete, no need to let browser keep handling the event
            rawEvent.preventDefault();
            return false;
 
        case 'range':
        case 'singleChar':
            // We have deleted what we need from content model, no need to let browser keep handling the event
            rawEvent.preventDefault();
            normalizeContentModel(model);
            deleteEmptyBlockGroups(model);
 
            if (result == 'range') {
                // A range is about to be deleted, so add an undo snapshot immediately
                context.skipUndoSnapshot = false;
            }
 
            // Trigger an event to let plugins know the content is about to be changed by Content Model keyboard editing.
            // So plugins can do proper handling. e.g. UndoPlugin can decide whether take a snapshot before this change happens.
            editor.triggerEvent('beforeKeyboardEditing', {
                rawEvent,
            });
 
            return true;
    }
}
 
/**
 * @internal
 */
export function shouldDeleteWord(rawEvent: KeyboardEvent, isMac: boolean) {
    return (
        (isMac && rawEvent.altKey && !rawEvent.metaKey) ||
        (!isMac && rawEvent.ctrlKey && !rawEvent.altKey)
    );
}
 
/**
 * @internal
 */
export function shouldDeleteAllSegmentsBefore(rawEvent: KeyboardEvent) {
    return rawEvent.metaKey && !rawEvent.altKey;
}
 
function deleteEmptyBlockGroups(group: ReadonlyContentModelBlockGroup) {
    let modified = false;
 
    for (let i = group.blocks.length - 1; i >= 0; i--) {
        const block = group.blocks[i];
 
        if (block.blockType == 'BlockGroup') {
            deleteEmptyBlockGroups(block);
 
            if (block.blocks.length == 0) {
                mutateBlock(group).blocks.splice(i, 1);
                modified = true;
            }
        }
    }
 
    if (modified) {
        group.blocks.forEach(setParagraphNotImplicit);
    }
}