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 6327x 1x 179x 1x 179x 1x 84x 1x 115x 115x 115x 352x 115x 8x 8x 115x 352x 107x 245x 108x 137x 88x 49x 48x 1x 35x 1x 2891x 2891x 2891x 1x 1811x 1x 12x 12x 10x 10x 8x 12x 11x 11x 8x 12x 12x 24x 12x 12x 24x 21x 21x 21x 21x 21x | 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;
}
|