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 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3918x 1x 123x 1x 125x 1x 84x 1x 112x 112x 112x 341x 112x 8x 8x 112x 341x 104x 237x 105x 132x 85x 47x 46x 1x 34x 1x 1696x 1696x 1696x 1x 143x 1x 11x 11x 9x 9x 7x 11x 10x 10x 7x 11x 11x 22x 11x 11x 22x 19x 19x 19x 19x 19x | import { applyFormat } from '../modelToDom/utils/applyFormat'; import { isElementOfType } from './isElementOfType'; import { isNodeOfType } from './isNodeOfType'; import { toArray } from './toArray'; import type { ContentModelEntityFormat, ContentModelSegmentFormat, DOMHelper, ModelToDomContext, } from 'roosterjs-content-model-types'; const ENTITY_INFO_NAME = '_Entity'; const ENTITY_INFO_SELECTOR = '.' + ENTITY_INFO_NAME; const ENTITY_TYPE_PREFIX = '_EType_'; const ENTITY_ID_PREFIX = '_EId_'; const ENTITY_READONLY_PREFIX = '_EReadonly_'; const ZERO_WIDTH_SPACE = '\u200B'; const DELIMITER_BEFORE = 'entityDelimiterBefore'; const DELIMITER_AFTER = 'entityDelimiterAfter'; const BLOCK_ENTITY_CONTAINER = '_E_EBlockEntityContainer'; const BLOCK_ENTITY_CONTAINER_SELECTOR = '.' + BLOCK_ENTITY_CONTAINER; /** * Check if the given DOM Node is an entity wrapper element */ export function isEntityElement(node: Node): boolean { return isNodeOfType(node, 'ELEMENT_NODE') && node.classList.contains(ENTITY_INFO_NAME); } /** * Find the closest entity wrapper element from a given DOM node * @param node The node to start looking for entity wrapper * @param domHelper The DOM helper to use */ export function findClosestEntityWrapper( startNode: Node, domHelper: DOMHelper ): HTMLElement | null { return domHelper.findClosestElementAncestor(startNode, ENTITY_INFO_SELECTOR); } /** * Find the closest block entity wrapper element from a given DOM node * @param node The node to start looking for entity container * @param domHelper The DOM helper * @returns */ export function findClosestBlockEntityContainer( node: Node, domHelper: DOMHelper ): HTMLElement | null { return domHelper.findClosestElementAncestor(node, BLOCK_ENTITY_CONTAINER_SELECTOR); } /** * Get all entity wrapper elements under the given root element * @param root The root element to query from * @returns An array of entity wrapper elements */ export function getAllEntityWrappers(root: HTMLElement): HTMLElement[] { return toArray(root.querySelectorAll('.' + ENTITY_INFO_NAME)) as HTMLElement[]; } /** * Parse entity format from entity wrapper element * @param wrapper The wrapper element to parse entity format from * @returns Entity format */ export function parseEntityFormat(wrapper: HTMLElement): ContentModelEntityFormat { let isEntity = false; const format: ContentModelEntityFormat = {}; wrapper.classList.forEach(name => { isEntity = parseEntityClassName(name, format) || isEntity; }); if (!isEntity) { format.isFakeEntity = true; format.isReadonly = !wrapper.isContentEditable; } return format; } /** * Parse entity class names from entity wrapper element * @param className Class names of entity * @param format The output entity format object */ function parseEntityClassName( className: string, format: ContentModelEntityFormat ): boolean | undefined { if (className == ENTITY_INFO_NAME) { return true; } else if (className.indexOf(ENTITY_TYPE_PREFIX) == 0) { format.entityType = className.substring(ENTITY_TYPE_PREFIX.length); } else if (className.indexOf(ENTITY_ID_PREFIX) == 0) { format.id = className.substring(ENTITY_ID_PREFIX.length); } else if (className.indexOf(ENTITY_READONLY_PREFIX) == 0) { format.isReadonly = className.substring(ENTITY_READONLY_PREFIX.length) == '1'; } } /** * Generate Entity class names for an entity wrapper * @param format The source entity format object * @returns A combined CSS class name string for entity wrapper */ export function generateEntityClassNames(format: ContentModelEntityFormat): string { return format.isFakeEntity ? '' : `${ENTITY_INFO_NAME} ${ENTITY_TYPE_PREFIX}${format.entityType ?? ''} ${ format.id ? `${ENTITY_ID_PREFIX}${format.id} ` : '' }${ENTITY_READONLY_PREFIX}${format.isReadonly ? '1' : '0'}`; } /** * Checks whether the node provided is a Entity delimiter * @param node the node to check * @param isBefore True to match delimiter before entity only, false to match delimiter after entity, or undefined means match both * @return true if it is a delimiter */ export function isEntityDelimiter(element: HTMLElement, isBefore?: boolean): boolean { const matchBefore = isBefore === undefined || isBefore; const matchAfter = isBefore === undefined || !isBefore; return ( isElementOfType(element, 'span') && ((matchAfter && element.classList.contains(DELIMITER_AFTER)) || (matchBefore && element.classList.contains(DELIMITER_BEFORE))) && element.textContent === ZERO_WIDTH_SPACE ); } /** * Check if the given element is a container element of block entity * @param element The element to check * @returns True if the element is a block entity container, otherwise false */ export function isBlockEntityContainer(element: HTMLElement): boolean { return isElementOfType(element, 'div') && element.classList.contains(BLOCK_ENTITY_CONTAINER); } /** * Adds delimiters to the element provided. If the delimiters already exists, will not be added * @param element the node to add the delimiters * @param format format to set to the delimiters, so when typing inside of one the format is not lost * @param context Model to Dom context to use. */ export function addDelimiters( doc: Document, element: HTMLElement, format?: ContentModelSegmentFormat | null, context?: ModelToDomContext ): HTMLElement[] { let [delimiterAfter, delimiterBefore] = getDelimiters(element); if (!delimiterAfter) { delimiterAfter = insertDelimiter(doc, element, true /*isAfter*/); if (context && format) { applyFormat(delimiterAfter, context.formatAppliers.segment, format, context); } } if (!delimiterBefore) { delimiterBefore = insertDelimiter(doc, element, false /*isAfter*/); if (context && format) { applyFormat(delimiterBefore, context.formatAppliers.segment, format, context); } } return [delimiterAfter, delimiterBefore]; } function getDelimiters(entityWrapper: HTMLElement): (HTMLElement | undefined)[] { const result: (HTMLElement | undefined)[] = []; const { nextElementSibling, previousElementSibling } = entityWrapper; result.push( isDelimiter(nextElementSibling, DELIMITER_AFTER), isDelimiter(previousElementSibling, DELIMITER_BEFORE) ); return result; } function isDelimiter(el: Element | null, className: string): HTMLElement | undefined { return el?.classList.contains(className) && el.textContent == ZERO_WIDTH_SPACE ? (el as HTMLElement) : undefined; } function insertDelimiter(doc: Document, element: Element, isAfter: boolean) { const span = doc.createElement('span'); span.className = isAfter ? DELIMITER_AFTER : DELIMITER_BEFORE; span.appendChild(doc.createTextNode(ZERO_WIDTH_SPACE)); element.parentNode?.insertBefore(span, isAfter ? element.nextSibling : element); return span; } |