/
entityPlaceholderUtils.ts
151 lines (126 loc) · 5.42 KB
/
entityPlaceholderUtils.ts
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
import getEntityFromElement from './getEntityFromElement';
import getEntitySelector from './getEntitySelector';
import safeInstanceOf from '../utils/safeInstanceOf';
import { EntityClasses } from 'roosterjs-editor-types';
import type { Entity, KnownEntityItem } from 'roosterjs-editor-types';
const EntityPlaceHolderTagName = 'ENTITY-PLACEHOLDER';
/**
* @deprecated
* Create a placeholder comment node for entity
* @param entity The entity to create placeholder from
* @returns A placeholder comment node as
*/
export function createEntityPlaceholder(entity: Entity): HTMLElement {
const placeholder = entity.wrapper.ownerDocument.createElement(EntityPlaceHolderTagName);
placeholder.id = entity.id;
return placeholder;
}
/**
* Move content from a container into a new Document fragment, and try keep entities to be reusable by creating placeholder
* for them in the document fragment.
* If an entity is directly under root container, the whole entity can be reused and no need to move it at all.
* If an entity is not directly under root container, it is still reusable, but it may need some movement.
* In any case, entities will be replaced with a placeholder in the target document fragment.
* We will use an entity map (the "entities" parameter) to save the map from entity id to its wrapper element.
* @param root The root element
* @param entities A map from entity id to entity wrapper element
* @returns A new document fragment contains all the content and entity placeholders
*/
export function moveContentWithEntityPlaceholders(
root: HTMLDivElement,
entities: Record<string, HTMLElement>
) {
const entitySelector = getEntitySelector();
const fragment = root.ownerDocument.createDocumentFragment();
let next: Node | null = null;
for (let child: Node | null = root.firstChild; child; child = next) {
let entity: Entity | null;
let nodeToAppend = child;
next = child.nextSibling;
if (safeInstanceOf(child, 'HTMLElement')) {
if ((entity = getEntityFromElement(child))) {
nodeToAppend = getPlaceholder(entity, entities);
} else {
child.querySelectorAll<HTMLElement>(entitySelector).forEach(wrapper => {
if ((entity = getEntityFromElement(wrapper))) {
const placeholder = getPlaceholder(entity, entities);
wrapper.parentNode?.replaceChild(placeholder, wrapper);
}
});
}
}
fragment.appendChild(nodeToAppend);
}
fragment.normalize();
return fragment;
}
/**
* Restore HTML content from a document fragment that may contain entity placeholders.
* @param source Source document fragment that contains HTML content and entity placeholders
* @param target Target container, usually to be editor root container
* @param entities A map from entity id to entity wrapper, used for reusing existing DOM structure for entity
* @param insertClonedNode When pass true, merge with a cloned copy of the nodes from source fragment rather than the nodes themselves @default false
*/
export function restoreContentWithEntityPlaceholder(
source: ParentNode,
target: HTMLElement,
entities: Record<string, HTMLElement | KnownEntityItem> | null,
insertClonedNode?: boolean
) {
let anchor = target.firstChild;
const entitySelector = getEntitySelector();
for (let current = source.firstChild; current; ) {
const next = current.nextSibling;
const wrapper = tryGetWrapperFromEntityPlaceholder(entities, current);
if (wrapper) {
anchor = removeUntil(anchor, wrapper);
if (anchor) {
anchor = anchor.nextSibling;
} else {
target.appendChild(wrapper);
}
} else {
const nodeToInsert = insertClonedNode ? current.cloneNode(true /*deep*/) : current;
target.insertBefore(nodeToInsert, anchor);
if (safeInstanceOf(nodeToInsert, 'HTMLElement')) {
nodeToInsert.querySelectorAll(entitySelector).forEach(placeholder => {
const wrapper = tryGetWrapperFromEntityPlaceholder(entities, placeholder);
if (wrapper) {
placeholder.parentNode?.replaceChild(wrapper, placeholder);
}
});
}
}
current = next;
}
removeUntil(anchor);
}
function removeUntil(anchor: ChildNode | null, nodeToStop?: HTMLElement) {
while (anchor && (!nodeToStop || anchor != nodeToStop)) {
const nodeToRemove = anchor;
anchor = anchor.nextSibling;
nodeToRemove.parentNode?.removeChild(nodeToRemove);
}
return anchor;
}
function tryGetWrapperFromEntityPlaceholder(
entities: Record<string, HTMLElement | KnownEntityItem> | null,
node: Node
): HTMLElement | null {
const id =
safeInstanceOf(node, 'HTMLElement') &&
node.classList.contains(EntityClasses.ENTITY_INFO_NAME) &&
getEntityFromElement(node as HTMLElement)?.id;
const item = id ? entities?.[id] : null;
return !item
? null
: safeInstanceOf(item, 'HTMLElement')
? item
: item?.canPersist
? item.element
: null;
}
function getPlaceholder(entity: Entity, entities: Record<string, HTMLElement>) {
entities[entity.id] = entity.wrapper;
return entity.wrapper.cloneNode(true /*deep*/);
}