Skip to content

Commit 2917ddf

Browse files
Rooster Version bump (#2977)
* 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 * bump version * fix version --------- Co-authored-by: Jiuqing Song <jisong@microsoft.com>
1 parent ed9b6cf commit 2917ddf

File tree

62 files changed

+2729
-55
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+2729
-55
lines changed

demo/scripts/controlsV2/mainPane/MainPane.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import SampleEntityPlugin from '../plugins/SampleEntityPlugin';
44
import { ApiPlaygroundPlugin } from '../sidePane/apiPlayground/ApiPlaygroundPlugin';
55
import { ContentModelPanePlugin } from '../sidePane/contentModel/ContentModelPanePlugin';
66
import { darkModeButton } from '../demoButtons/darkModeButton';
7+
import { defaultDomToModelOption } from '../options/defaultDomToModelOption';
78
import { Editor } from 'roosterjs-content-model-core';
89
import { EditorOptionsPlugin } from '../sidePane/editorOptions/EditorOptionsPlugin';
910
import { EventViewPlugin } from '../sidePane/eventViewer/EventViewPlugin';
@@ -27,6 +28,7 @@ import { SnapshotPlugin } from '../sidePane/snapshot/SnapshotPlugin';
2728
import { ThemeProvider } from '@fluentui/react/lib/Theme';
2829
import { TitleBar } from '../titleBar/TitleBar';
2930
import { trustedHTMLHandler } from '../../utils/trustedHTMLHandler';
31+
import { undeletableLinkChecker } from '../options/demoUndeletableAnchorParser';
3032
import { UpdateContentPlugin } from '../plugins/UpdateContentPlugin';
3133
import { WindowProvider } from '@fluentui/react/lib/WindowProvider';
3234
import { zoomButton } from '../demoButtons/zoomButton';
@@ -58,6 +60,7 @@ import {
5860
AutoFormatPlugin,
5961
CustomReplacePlugin,
6062
EditPlugin,
63+
HiddenPropertyPlugin,
6164
HyperlinkPlugin,
6265
ImageEditPlugin,
6366
MarkdownPlugin,
@@ -376,6 +379,7 @@ export class MainPane extends React.Component<{}, MainPaneState> {
376379
experimentalFeatures={Array.from(
377380
this.state.initState.experimentalFeatures
378381
)}
382+
defaultDomToModelOptions={defaultDomToModelOption}
379383
/>
380384
)}
381385
</div>
@@ -527,6 +531,10 @@ export class MainPane extends React.Component<{}, MainPaneState> {
527531
: linkTitle
528532
),
529533
pluginList.customReplace && new CustomReplacePlugin(customReplacements),
534+
pluginList.hiddenProperty &&
535+
new HiddenPropertyPlugin({
536+
undeletableLinkChecker: undeletableLinkChecker,
537+
}),
530538
].filter(x => !!x);
531539
}
532540
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { demoUndeletableAnchorParser } from './demoUndeletableAnchorParser';
2+
import { DomToModelOption } from 'roosterjs-content-model-types';
3+
4+
export const defaultDomToModelOption: DomToModelOption = {
5+
additionalFormatParsers: {
6+
link: [demoUndeletableAnchorParser],
7+
},
8+
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { FormatParser, UndeletableFormat } from 'roosterjs-content-model-types';
2+
3+
const DemoUndeletableName = 'DemoUndeletable';
4+
5+
export function undeletableLinkChecker(a: HTMLAnchorElement): boolean {
6+
return a.getAttribute('name') == DemoUndeletableName;
7+
}
8+
9+
export const demoUndeletableAnchorParser: FormatParser<UndeletableFormat> = (format, element) => {
10+
if (undeletableLinkChecker(element as HTMLAnchorElement)) {
11+
format.undeletable = true;
12+
}
13+
};

demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const initialState: OptionState = {
2121
imageEditPlugin: true,
2222
hyperlink: true,
2323
customReplace: true,
24+
hiddenProperty: true,
2425
},
2526
defaultFormat: {
2627
fontFamily: 'Calibri',

demo/scripts/controlsV2/sidePane/editorOptions/OptionState.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export interface BuildInPluginList {
2222
hyperlink: boolean;
2323
imageEditPlugin: boolean;
2424
customReplace: boolean;
25+
hiddenProperty: boolean;
2526
}
2627

2728
export interface OptionState {

demo/scripts/controlsV2/sidePane/editorOptions/Plugins.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ export class Plugins extends PluginsBase<keyof BuildInPluginList> {
305305
)}
306306
{this.renderPluginItem('customReplace', 'Custom Replace')}
307307
{this.renderPluginItem('imageEditPlugin', 'ImageEditPlugin')}
308+
{this.renderPluginItem('hiddenProperty', 'Hidden Property')}
308309
</tbody>
309310
</table>
310311
);

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+
}

0 commit comments

Comments
 (0)