Skip to content

Commit 8e54faa

Browse files
Bump RoosterJS 9.24.0 (#2981)
* Support undeletable anchor Step 1: Support hidden properties (#2970) * Support undeletable anchor Step 1: Support hidden properties * improve comments * Support undeletable anchor Step 2: Add undeletable property to link (#2971) * Support undeletable anchor Step 1: Support hidden properties * Support undeletable anchor step 2 * improve comments * Support undeletable anchor Step 3: Support anchor (#2972) * Support undeletable anchor Step 1: Support hidden properties * Support undeletable anchor step 2 * improve comments * Support undeletable anchor Step 3 * Find previously marked paragraph (#2975) * Support undeletable anchor Step 4: Prevent deleting undeletable anchor (#2973) * Support undeletable anchor Step 1: Support hidden properties * Support undeletable anchor step 2 * improve comments * Support undeletable anchor Step 3 * Support undeletable anchor Step 4 * improve * add test * fix typo * Do not enable paragraph map by default (#2978) * Check selection for empty anchor (#2979) Co-authored-by: Julia Roldi <87443959+juliaroldi@users.noreply.github.com> * fix test --------- Co-authored-by: Jiuqing Song <jisong@microsoft.com>
1 parent 2917ddf commit 8e54faa

File tree

6 files changed

+142
-24
lines changed

6 files changed

+142
-24
lines changed

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

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,22 @@ class CachePlugin implements PluginWithState<CachePluginState> {
2525
* @param contentDiv The editor content DIV
2626
*/
2727
constructor(option: EditorOptions, contentDiv: HTMLDivElement) {
28-
this.state = option.disableCache
29-
? {}
30-
: {
31-
domIndexer: new DomIndexerImpl(
32-
option.experimentalFeatures &&
33-
option.experimentalFeatures.indexOf('PersistCache') >= 0
34-
),
35-
textMutationObserver: createTextMutationObserver(contentDiv, this.onMutation),
36-
paragraphMap: createParagraphMap(),
37-
};
28+
this.state = {};
29+
30+
if (!option.disableCache) {
31+
this.state.domIndexer = new DomIndexerImpl(
32+
option.experimentalFeatures &&
33+
option.experimentalFeatures.indexOf('PersistCache') >= 0
34+
);
35+
this.state.textMutationObserver = createTextMutationObserver(
36+
contentDiv,
37+
this.onMutation
38+
);
39+
}
40+
41+
if (option.enableParagraphMap) {
42+
this.state.paragraphMap = createParagraphMap();
43+
}
3844
}
3945

4046
/**

packages/roosterjs-content-model-core/test/corePlugin/cache/CachePluginTest.ts

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ describe('CachePlugin', () => {
7676
expect(plugin.getState()).toEqual({});
7777
});
7878

79-
it('initialize with cache', () => {
79+
it('initialize without paragraph map', () => {
8080
const startObservingSpy = jasmine.createSpy('startObserving');
8181
const stopObservingSpy = jasmine.createSpy('stopObserving');
8282
const mockedObserver = {
@@ -90,6 +90,29 @@ describe('CachePlugin', () => {
9090
disableCache: false,
9191
});
9292
expect(addEventListenerSpy).toHaveBeenCalledWith('selectionchange', jasmine.anything());
93+
expect(plugin.getState()).toEqual({
94+
domIndexer: new DomIndexerImpl(),
95+
textMutationObserver: mockedObserver,
96+
});
97+
expect(resetMapSpy).not.toHaveBeenCalled();
98+
expect(startObservingSpy).toHaveBeenCalledTimes(1);
99+
});
100+
101+
it('initialize with cache and paragraph map', () => {
102+
const startObservingSpy = jasmine.createSpy('startObserving');
103+
const stopObservingSpy = jasmine.createSpy('stopObserving');
104+
const mockedObserver = {
105+
startObserving: startObservingSpy,
106+
stopObserving: stopObservingSpy,
107+
} as any;
108+
spyOn(textMutationObserver, 'createTextMutationObserver').and.returnValue(
109+
mockedObserver
110+
);
111+
init({
112+
disableCache: false,
113+
enableParagraphMap: true,
114+
});
115+
expect(addEventListenerSpy).toHaveBeenCalledWith('selectionchange', jasmine.anything());
93116
expect(plugin.getState()).toEqual({
94117
domIndexer: new DomIndexerImpl(),
95118
textMutationObserver: mockedObserver,
@@ -115,7 +138,9 @@ describe('CachePlugin', () => {
115138
'createTextMutationObserver'
116139
).and.returnValue(mockedObserver);
117140

118-
init({});
141+
init({
142+
enableParagraphMap: true,
143+
});
119144

120145
const state = plugin.getState();
121146

@@ -371,7 +396,7 @@ describe('CachePlugin', () => {
371396

372397
describe('ContentChanged', () => {
373398
beforeEach(() => {
374-
init({ disableCache: true });
399+
init({ disableCache: true, enableParagraphMap: true });
375400
});
376401
afterEach(() => {
377402
plugin.dispose();
@@ -392,9 +417,10 @@ describe('CachePlugin', () => {
392417
expect(state).toEqual({
393418
cachedModel: undefined,
394419
cachedSelection: undefined,
420+
paragraphMap: mockedParagraphMap,
395421
});
396422
expect(reconcileSelectionSpy).not.toHaveBeenCalled();
397-
expect(resetMapSpy).toHaveBeenCalledTimes(0);
423+
expect(resetMapSpy).toHaveBeenCalledTimes(1);
398424
});
399425

400426
it('No domIndexer, has model in event', () => {
@@ -418,9 +444,10 @@ describe('CachePlugin', () => {
418444
expect(state).toEqual({
419445
cachedModel: undefined,
420446
cachedSelection: undefined,
447+
paragraphMap: mockedParagraphMap,
421448
});
422449
expect(reconcileSelectionSpy).not.toHaveBeenCalled();
423-
expect(resetMapSpy).toHaveBeenCalledTimes(0);
450+
expect(resetMapSpy).toHaveBeenCalledTimes(1);
424451
});
425452

426453
it('Has domIndexer, has model in event', () => {
@@ -446,6 +473,7 @@ describe('CachePlugin', () => {
446473
cachedModel: model,
447474
cachedSelection: newRangeEx,
448475
domIndexer,
476+
paragraphMap: mockedParagraphMap,
449477
});
450478
expect(reconcileSelectionSpy).not.toHaveBeenCalled();
451479
expect(resetMapSpy).toHaveBeenCalledTimes(0);
@@ -479,7 +507,7 @@ describe('CachePlugin', () => {
479507
}
480508
);
481509

482-
init({});
510+
init({ enableParagraphMap: true });
483511

484512
mockedIndexer = {
485513
reconcileSelection: reconcileSelectionSpy,

packages/roosterjs-content-model-dom/lib/domToModel/processors/linkProcessor.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,16 @@ export const linkProcessor: ElementProcessor<HTMLElement> = (group, element, con
2727

2828
if (isAnchor && !element.firstChild) {
2929
// Empty anchor, need to make sure it has some child in model
30-
addSegment(
31-
group,
32-
createText('', context.segmentFormat, {
33-
dataset: context.link.dataset,
34-
format: context.link.format,
35-
})
36-
);
30+
const emptyText = createText('', context.segmentFormat, {
31+
dataset: context.link.dataset,
32+
format: context.link.format,
33+
});
34+
35+
if (context.isInSelection) {
36+
emptyText.isSelected = true;
37+
}
38+
39+
addSegment(group, emptyText);
3740
} else {
3841
knownElementProcessor(group, element, context);
3942
}

packages/roosterjs-content-model-dom/test/domToModel/processors/linkProcessorTest.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,4 +359,79 @@ describe('linkProcessor', () => {
359359
dataset: {},
360360
});
361361
});
362+
363+
it('empty anchor', () => {
364+
const group = createContentModelDocument();
365+
const a = document.createElement('a');
366+
367+
a.name = 'name';
368+
369+
linkProcessor(group, a, context);
370+
371+
expect(group).toEqual({
372+
blockGroupType: 'Document',
373+
374+
blocks: [
375+
{
376+
blockType: 'Paragraph',
377+
format: {},
378+
isImplicit: true,
379+
segments: [
380+
{
381+
segmentType: 'Text',
382+
format: {},
383+
link: {
384+
format: { name: 'name' },
385+
dataset: {},
386+
},
387+
text: '',
388+
},
389+
],
390+
},
391+
],
392+
});
393+
expect(context.link).toEqual({
394+
format: {},
395+
dataset: {},
396+
});
397+
});
398+
399+
it('selected empty anchor', () => {
400+
const group = createContentModelDocument();
401+
const a = document.createElement('a');
402+
403+
a.name = 'name';
404+
405+
context.isInSelection = true;
406+
407+
linkProcessor(group, a, context);
408+
409+
expect(group).toEqual({
410+
blockGroupType: 'Document',
411+
412+
blocks: [
413+
{
414+
blockType: 'Paragraph',
415+
format: {},
416+
isImplicit: true,
417+
segments: [
418+
{
419+
segmentType: 'Text',
420+
format: {},
421+
isSelected: true,
422+
link: {
423+
format: { name: 'name' },
424+
dataset: {},
425+
},
426+
text: '',
427+
},
428+
],
429+
},
430+
],
431+
});
432+
expect(context.link).toEqual({
433+
format: {},
434+
dataset: {},
435+
});
436+
});
362437
});

packages/roosterjs-content-model-types/lib/editor/EditorOptions.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,12 @@ export interface EditorBaseOptions {
174174
* @returns A template string to announce, use placeholder such as "{0}" for variables if necessary
175175
*/
176176
announcerStringGetter?: (key: KnownAnnounceStrings) => string;
177+
178+
/**
179+
* Whether to enable paragraph map. Default value is false.
180+
* Paragraph map is used to save a marker for paragraphs so it can be found even content is changed from a previous marked paragraph.
181+
*/
182+
enableParagraphMap?: boolean;
177183
}
178184

179185
/**

versions.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"react": "9.0.2",
3-
"main": "9.23.0",
3+
"main": "9.24.0",
44
"legacyAdapter": "8.63.2",
55
"overrides": {}
66
}

0 commit comments

Comments
 (0)