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

100% Statements 34/34
85.71% Branches 24/28
100% Functions 2/2
100% Lines 32/32

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 761x 1x 1x 1x 1x 1x             1x 1x         1x             253x   253x   253x 253x 253x       253x 253x   253x 250x 250x     250x     250x   250x 250x         3x   3x   3x           253x 111x 111x 103x       253x   253x    
import { applyFormat } from '../utils/applyFormat';
import { applyMetadata } from '../utils/applyMetadata';
import { isGenericRoleElement } from '../../domUtils/isGenericRoleElement';
import { setParagraphNotImplicit } from '../../modelApi/block/setParagraphNotImplicit';
import { stackFormat } from '../utils/stackFormat';
import { unwrap } from '../../domUtils/unwrap';
import type {
    ContentModelBlockHandler,
    ContentModelListItem,
    ModelToDomContext,
} 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 listParent = nodeStack?.[nodeStack?.length - 1]?.node || parent;
    const li = doc.createElement('li');
    const level = listItem.levels[listItem.levels.length - 1];
 
    // 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, refNode?.parentNode == listParent ? refNode : null);
    context.rewriteFromModel.addedBlockElements.push(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);
        }
    }
 
    context.onNodeCreated?.(listItem, li);
 
    return refNode;
};