Skip to content

Commit 356e23d

Browse files
authored
Find previously marked paragraph (#2975)
1 parent ff7ca3b commit 356e23d

File tree

21 files changed

+628
-18
lines changed

21 files changed

+628
-18
lines changed

packages/roosterjs-content-model-core/lib/coreApi/createEditorContext/createEditorContext.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const createEditorContext: CreateEditorContext = (core, saveIndex) => {
2020
domIndexer: saveIndex ? cache.domIndexer : undefined,
2121
zoomScale: domHelper.calculateZoomScale(),
2222
experimentalFeatures: core.experimentalFeatures ?? [],
23+
paragraphMap: core.cache.paragraphMap,
2324
...getRootComputedStyleForContext(logicalRoot.ownerDocument),
2425
};
2526

packages/roosterjs-content-model-core/lib/coreApi/formatContentModel/formatContentModel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export const formatContentModel: FormatContentModel = (
3333
deletedEntities: [],
3434
rawEvent,
3535
newImages: [],
36+
paragraphIndexer: core.cache.paragraphMap,
3637
};
3738

3839
const hasFocus = core.domHelper.hasFocus();

packages/roosterjs-content-model-core/lib/corePlugin/cache/CachePlugin.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { areSameSelections } from './areSameSelections';
2+
import { createParagraphMap } from './ParagraphMapImpl';
23
import { createTextMutationObserver } from './textMutationObserver';
34
import { DomIndexerImpl } from './domIndexerImpl';
45
import { updateCache } from './updateCache';
@@ -32,6 +33,7 @@ class CachePlugin implements PluginWithState<CachePluginState> {
3233
option.experimentalFeatures.indexOf('PersistCache') >= 0
3334
),
3435
textMutationObserver: createTextMutationObserver(contentDiv, this.onMutation),
36+
paragraphMap: createParagraphMap(),
3537
};
3638
}
3739

@@ -172,6 +174,10 @@ class CachePlugin implements PluginWithState<CachePluginState> {
172174
if (!this.editor?.isInShadowEdit()) {
173175
this.state.cachedModel = undefined;
174176
this.state.cachedSelection = undefined;
177+
178+
// Clear paragraph indexer to prevent stale references to old paragraphs
179+
// It will be rebuild next time when we create a new Content Model
180+
this.state.paragraphMap?.clear();
175181
}
176182
}
177183

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { getParagraphMarker, setParagraphMarker } from 'roosterjs-content-model-dom';
2+
import type {
3+
ContentModelParagraph,
4+
ContentModelParagraphCommon,
5+
ParagraphIndexer,
6+
ParagraphMap,
7+
ReadonlyContentModelParagraph,
8+
} from 'roosterjs-content-model-types';
9+
10+
interface ParagraphWithMarker extends ContentModelParagraphCommon {
11+
_marker?: string;
12+
}
13+
14+
/**
15+
* @internal, used by test code only
16+
*/
17+
export interface ParagraphMapReset {
18+
_reset(): void;
19+
_getMap(): { [key: string]: ReadonlyContentModelParagraph };
20+
}
21+
22+
const idPrefix = 'paragraph';
23+
24+
class ParagraphMapImpl implements ParagraphMap, ParagraphIndexer, ParagraphMapReset {
25+
private static prefixNum = 0;
26+
private nextId = 0;
27+
private paragraphMap: { [key: string]: ReadonlyContentModelParagraph } = {};
28+
29+
constructor() {
30+
ParagraphMapImpl.prefixNum++;
31+
}
32+
33+
assignMarkerToModel(element: HTMLElement, paragraph: ContentModelParagraph): void {
34+
const marker = getParagraphMarker(element);
35+
const paragraphWithMarker = paragraph as ParagraphWithMarker;
36+
37+
if (marker) {
38+
paragraphWithMarker._marker = marker;
39+
40+
this.paragraphMap[marker] = paragraph;
41+
} else {
42+
paragraphWithMarker._marker = this.generateId();
43+
44+
this.applyMarkerToDom(element, paragraph);
45+
}
46+
}
47+
48+
applyMarkerToDom(element: HTMLElement, paragraph: ContentModelParagraph): void {
49+
const paragraphWithMarker = paragraph as ParagraphWithMarker;
50+
51+
if (!paragraphWithMarker._marker) {
52+
paragraphWithMarker._marker = this.generateId();
53+
}
54+
55+
const marker = paragraphWithMarker._marker;
56+
57+
if (marker) {
58+
setParagraphMarker(element, marker);
59+
60+
this.paragraphMap[marker] = paragraph;
61+
}
62+
}
63+
64+
/**
65+
* Get paragraph using a previously marked paragraph
66+
* @param markedParagraph The previously marked paragraph to get
67+
*/
68+
getParagraphFromMarker(
69+
markerParagraph: ReadonlyContentModelParagraph
70+
): ReadonlyContentModelParagraph | null {
71+
const marker = (markerParagraph as ParagraphWithMarker)._marker;
72+
73+
return marker ? this.paragraphMap[marker] || null : null;
74+
}
75+
76+
clear() {
77+
this.paragraphMap = {};
78+
}
79+
80+
//#region For test code only
81+
_reset() {
82+
ParagraphMapImpl.prefixNum = 0;
83+
this.nextId = 0;
84+
}
85+
86+
_getMap() {
87+
return this.paragraphMap;
88+
}
89+
//#endregion
90+
91+
private generateId() {
92+
return `${idPrefix}_${ParagraphMapImpl.prefixNum}_${this.nextId++}`;
93+
}
94+
}
95+
96+
/**
97+
* @internal
98+
*/
99+
export function createParagraphMap(): ParagraphMap & ParagraphIndexer {
100+
return new ParagraphMapImpl();
101+
}

packages/roosterjs-content-model-core/test/coreApi/createEditorContext/createEditorContextTest.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ describe('createEditorContext', () => {
99
const calculateZoomScaleSpy = jasmine.createSpy('calculateZoomScale').and.returnValue(1);
1010
const domIndexer = 'DOMINDEXER' as any;
1111
const isRtlSpy = jasmine.createSpy('isRtl');
12+
const mockedParagraphMap = 'PARAMAP' as any;
1213

1314
const div = {
1415
ownerDocument: {},
@@ -26,6 +27,7 @@ describe('createEditorContext', () => {
2627
darkColorHandler,
2728
cache: {
2829
domIndexer: domIndexer,
30+
paragraphMap: mockedParagraphMap,
2931
},
3032
domHelper: {
3133
calculateZoomScale: calculateZoomScaleSpy,
@@ -46,6 +48,7 @@ describe('createEditorContext', () => {
4648
zoomScale: 1,
4749
rootFontSize: 16,
4850
experimentalFeatures: [],
51+
paragraphMap: mockedParagraphMap,
4952
});
5053
});
5154

@@ -56,6 +59,7 @@ describe('createEditorContext', () => {
5659
const calculateZoomScaleSpy = jasmine.createSpy('calculateZoomScale').and.returnValue(1);
5760
const isRtlSpy = jasmine.createSpy('isRtl');
5861
const domIndexer = 'DOMINDEXER' as any;
62+
const mockedParagraphMap = 'PARAMAP' as any;
5963

6064
const div = {
6165
ownerDocument: {},
@@ -73,6 +77,7 @@ describe('createEditorContext', () => {
7377
darkColorHandler,
7478
cache: {
7579
domIndexer,
80+
paragraphMap: mockedParagraphMap,
7681
},
7782
domHelper: {
7883
calculateZoomScale: calculateZoomScaleSpy,
@@ -93,6 +98,7 @@ describe('createEditorContext', () => {
9398
zoomScale: 1,
9499
rootFontSize: 16,
95100
experimentalFeatures: [],
101+
paragraphMap: mockedParagraphMap,
96102
});
97103
});
98104

@@ -102,6 +108,7 @@ describe('createEditorContext', () => {
102108
const darkColorHandler = 'DARKHANDLER' as any;
103109
const mockedPendingFormat = 'PENDINGFORMAT' as any;
104110
const calculateZoomScaleSpy = jasmine.createSpy('calculateZoomScale').and.returnValue(1);
111+
const mockedParagraphMap = 'PARAMAP' as any;
105112

106113
const div = {
107114
ownerDocument: {},
@@ -118,7 +125,7 @@ describe('createEditorContext', () => {
118125
pendingFormat: mockedPendingFormat,
119126
},
120127
darkColorHandler,
121-
cache: {},
128+
cache: { paragraphMap: mockedParagraphMap },
122129
domHelper: {
123130
calculateZoomScale: calculateZoomScaleSpy,
124131
isRightToLeft: jasmine.createSpy('isRtl'),
@@ -138,6 +145,7 @@ describe('createEditorContext', () => {
138145
zoomScale: 1,
139146
rootFontSize: 16,
140147
experimentalFeatures: [],
148+
paragraphMap: mockedParagraphMap,
141149
});
142150
});
143151

@@ -148,7 +156,7 @@ describe('createEditorContext', () => {
148156
const mockedPendingFormat = 'PENDINGFORMAT' as any;
149157
const calculateZoomScaleSpy = jasmine.createSpy('calculateZoomScale').and.returnValue(1);
150158
const isRtlSpy = jasmine.createSpy('isRtl');
151-
159+
const mockedParagraphMap = 'PARAMAP' as any;
152160
const div = {
153161
ownerDocument: {},
154162
};
@@ -165,7 +173,9 @@ describe('createEditorContext', () => {
165173
pendingFormat: mockedPendingFormat,
166174
},
167175
darkColorHandler,
168-
cache: {},
176+
cache: {
177+
paragraphMap: mockedParagraphMap,
178+
},
169179
domHelper: {
170180
calculateZoomScale: calculateZoomScaleSpy,
171181
isRightToLeft: isRtlSpy,
@@ -185,6 +195,7 @@ describe('createEditorContext', () => {
185195
zoomScale: 1,
186196
rootFontSize: 16,
187197
experimentalFeatures: [],
198+
paragraphMap: mockedParagraphMap,
188199
});
189200
});
190201
});
@@ -197,6 +208,7 @@ describe('createEditorContext - checkZoomScale', () => {
197208
const isDarkMode = 'DARKMODE' as any;
198209
const defaultFormat = 'DEFAULTFORMAT' as any;
199210
const darkColorHandler = 'DARKHANDLER' as any;
211+
const mockedParagraphMap = 'PARAMAP' as any;
200212

201213
beforeEach(() => {
202214
calculateZoomScaleSpy = jasmine.createSpy('calculateZoomScale');
@@ -215,7 +227,7 @@ describe('createEditorContext - checkZoomScale', () => {
215227
defaultFormat,
216228
},
217229
darkColorHandler,
218-
cache: {},
230+
cache: { paragraphMap: mockedParagraphMap },
219231
domHelper: {
220232
calculateZoomScale: calculateZoomScaleSpy,
221233
isRightToLeft: isRtlSpy,
@@ -239,6 +251,7 @@ describe('createEditorContext - checkZoomScale', () => {
239251
pendingFormat: undefined,
240252
rootFontSize: 16,
241253
experimentalFeatures: [],
254+
paragraphMap: mockedParagraphMap,
242255
});
243256
});
244257
});
@@ -251,6 +264,7 @@ describe('createEditorContext - checkRootDir', () => {
251264
const isDarkMode = 'DARKMODE' as any;
252265
const defaultFormat = 'DEFAULTFORMAT' as any;
253266
const darkColorHandler = 'DARKHANDLER' as any;
267+
const mockedParagraphMap = 'PARAMAP' as any;
254268

255269
beforeEach(() => {
256270
calculateZoomScaleSpy = jasmine.createSpy('calculateZoomScale').and.returnValue(1);
@@ -268,7 +282,9 @@ describe('createEditorContext - checkRootDir', () => {
268282
defaultFormat,
269283
},
270284
darkColorHandler,
271-
cache: {},
285+
cache: {
286+
paragraphMap: mockedParagraphMap,
287+
},
272288
domHelper: {
273289
calculateZoomScale: calculateZoomScaleSpy,
274290
isRightToLeft: isRtlSpy,
@@ -291,6 +307,7 @@ describe('createEditorContext - checkRootDir', () => {
291307
zoomScale: 1,
292308
rootFontSize: 16,
293309
experimentalFeatures: [],
310+
paragraphMap: mockedParagraphMap,
294311
});
295312
});
296313

@@ -310,6 +327,7 @@ describe('createEditorContext - checkRootDir', () => {
310327
zoomScale: 1,
311328
rootFontSize: 16,
312329
experimentalFeatures: [],
330+
paragraphMap: mockedParagraphMap,
313331
});
314332
});
315333
});

0 commit comments

Comments
 (0)