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

100% Statements 49/49
91.18% Branches 31/34
100% Functions 3/3
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 110 111 112 1131x 1x 1x 1x 1x 1x 1x               1x 1x         1x             302x   302x 302x 302x 302x 302x     302x   302x 8x         8x 9x 1x       8x             294x 294x       294x 294x 294x     302x 299x 299x     299x     299x   299x 299x       299x             3x   3x   3x           302x 133x 133x 125x       302x 294x     302x    
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 (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);
        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, () => {
            stackFormat(
                context,
                listItem.format.direction ? { direction: listItem.format.direction } : null,
                () => {
                    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;
};