All files / roosterjs-content-model-dom/lib/modelToDom/handlers handleListItem.ts

100% Statements 49/49
91.67% Branches 33/36
100% Functions 2/2
100% Lines 46/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 1101x 1x 1x 1x 1x 1x 1x               1x 1x         1x             260x   260x 260x 260x 260x 260x     260x   260x 5x         5x 6x 1x       5x             255x 255x       255x 255x   255x 1x       260x 257x 257x     257x     257x   257x 257x         3x   3x   3x           260x 111x 111x 103x       260x 255x     260x    
import { applyFormat } from '../utils/applyFormat';
import { applyMetadata } from '../utils/applyMetadata';
import { isGenericRoleElement } from '../../domUtils/isGenericRoleElement';
import { reuseCachedElement } from '../../domUtils/reuseCachedElement';
import { setParagraphNotImplicit } from '../../modelApi/block/setParagraphNotImplicit';
import { stackFormat } from '../utils/stackFormat';
import { unwrap } from '../../domUtils/unwrap';
import type {
    ContentModelBlockHandler,
    ContentModelListItem,
    ModelToDomContext,
    ModelToDomListStackItem,
} from 'roosterjs-content-model-types';
 
const HtmlRoleAttribute = 'role';
const PresentationRoleValue = 'presentation';
 
/**
 * @internal
 */
export const handleListItem: ContentModelBlockHandler<ContentModelListItem> = (
    doc: Document,
    parent: Node,
    listItem: ContentModelListItem,
    context: ModelToDomContext,
    refNode: Node | null
) => {
    refNode = context.modelHandlers.list(doc, parent, listItem, context, refNode);
 
    const { nodeStack } = context.listFormat;
    const leafLevel: Partial<ModelToDomListStackItem> = nodeStack?.[nodeStack.length - 1] ?? {};
    const itemRefNode = leafLevel.refNode || null;
    const listParent = leafLevel.node || parent;
    const level = listItem.levels[listItem.levels.length - 1];
 
    let li: HTMLLIElement;
    let isNewlyCreated = false;
 
    if (context.allowCacheListItem && listItem.cachedElement) {
        li = listItem.cachedElement;
 
        // Check if the cached LI is used as refNode under another list level,
        // since we know we are going to move it under the current listParent,
        // we need to update the refNode of the previous list level to avoid removing it later
        for (let i = 0; i < nodeStack.length - 1; i++) {
            if (nodeStack[i].refNode === li) {
                nodeStack[i].refNode = li.nextSibling;
            }
        }
 
        leafLevel.refNode = reuseCachedElement(
            listParent,
            li,
            itemRefNode,
            context.rewriteFromModel
        );
    } else {
        li = doc.createElement('li');
        isNewlyCreated = true;
 
        // It is possible listParent is the same with parent param.
        // This happens when outdent a list item to cause it has no list level
        listParent.insertBefore(li, itemRefNode?.parentNode == listParent ? itemRefNode : null);
        context.rewriteFromModel.addedBlockElements.push(li);
 
        if (context.allowCacheListItem) {
            listItem.cachedElement = li;
        }
    }
 
    if (level) {
        applyFormat(li, context.formatAppliers.segment, listItem.formatHolder.format, context);
        applyFormat(li, context.formatAppliers.listItemThread, level.format, context);
 
        // Need to apply metadata after applying listItem format since the list numbers value relies on the result of list thread handling
        applyMetadata(level, context.metadataAppliers.listItem, listItem.format, context);
 
        // Need to apply listItemElement formats after applying metadata since the list numbers value relies on the result of metadata handling
        applyFormat(li, context.formatAppliers.listItemElement, listItem.format, context);
 
        stackFormat(context, listItem.formatHolder.format, () => {
            context.modelHandlers.blockGroupChildren(doc, li, listItem, context);
        });
    } else {
        // There is no level for this list item, that means it should be moved out of the list
        // For each paragraph, make it not implicit so it will have a DIV around it, to avoid more paragraphs connected together
        listItem.blocks.forEach(setParagraphNotImplicit);
 
        context.modelHandlers.blockGroupChildren(doc, li, listItem, context);
 
        unwrap(li);
    }
 
    // Add role="presentation" to all generic role elements inside the LI element
    // This is to make sure the elements are announced correctly by screen readers
    // when using arrow keys to navigate the list.
    for (let index = 0; index < li.children.length; index++) {
        const element = li.children.item(index);
        if (isGenericRoleElement(element)) {
            element.setAttribute(HtmlRoleAttribute, PresentationRoleValue);
        }
    }
 
    if (isNewlyCreated) {
        context.onNodeCreated?.(listItem, li);
    }
 
    return refNode;
};