Skip to content

Commit 8bbbf87

Browse files
authored
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
1 parent f2f13a6 commit 8bbbf87

File tree

17 files changed

+286
-8
lines changed

17 files changed

+286
-8
lines changed

demo/scripts/controlsV2/mainPane/MainPane.tsx

Lines changed: 7 additions & 1 deletion
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';
@@ -377,6 +379,7 @@ export class MainPane extends React.Component<{}, MainPaneState> {
377379
experimentalFeatures={Array.from(
378380
this.state.initState.experimentalFeatures
379381
)}
382+
defaultDomToModelOptions={defaultDomToModelOption}
380383
/>
381384
)}
382385
</div>
@@ -528,7 +531,10 @@ export class MainPane extends React.Component<{}, MainPaneState> {
528531
: linkTitle
529532
),
530533
pluginList.customReplace && new CustomReplacePlugin(customReplacements),
531-
pluginList.hiddenProperty && new HiddenPropertyPlugin({}),
534+
pluginList.hiddenProperty &&
535+
new HiddenPropertyPlugin({
536+
undeletableLinkChecker: undeletableLinkChecker,
537+
}),
532538
].filter(x => !!x);
533539
}
534540
}
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+
};

packages/roosterjs-content-model-dom/lib/domUtils/hiddenProperties/hiddenProperty.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
* @internal
33
*/
44
export interface HiddenProperty {
5-
dummy?: {}; // Temp used by test, will be removed later
6-
7-
// TODO: Add more properties as needed
5+
/**
6+
* Specify we should not delete this element when delete/backspace key is pressed
7+
*/
8+
undeletable?: boolean;
89
}
910

1011
interface NodeWithHiddenProperty extends Node {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { getHiddenProperty, setHiddenProperty } from './hiddenProperty';
2+
import type { HiddenProperty } from './hiddenProperty';
3+
4+
const UndeletableLinkKey: keyof HiddenProperty = 'undeletable';
5+
6+
/**
7+
* Set a hidden property on a link element to indicate whether it is undeletable or not.
8+
* This is used to prevent the link from being deleted when the user tries to delete it.
9+
* @param a The link element to set the property on
10+
* @param undeletable Whether the link is undeletable or not
11+
*/
12+
export function setLinkUndeletable(a: HTMLAnchorElement, undeletable: boolean) {
13+
setHiddenProperty(a, UndeletableLinkKey, undeletable);
14+
}
15+
16+
/**
17+
* Check if a link element is undeletable or not.
18+
* This is used to determine if the link can be deleted when the user tries to delete it.
19+
* @param a The link element to check
20+
* @returns True if the link is undeletable, false otherwise
21+
*/
22+
export function isLinkUndeletable(a: HTMLAnchorElement): boolean {
23+
return !!getHiddenProperty(a, UndeletableLinkKey);
24+
}

packages/roosterjs-content-model-dom/lib/formatHandlers/defaultFormatHandlers.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { textAlignFormatHandler } from './block/textAlignFormatHandler';
3232
import { textColorFormatHandler } from './segment/textColorFormatHandler';
3333
import { textColorOnTableCellFormatHandler } from './table/textColorOnTableCellFormatHandler';
3434
import { textIndentFormatHandler } from './block/textIndentFormatHandler';
35+
import { undeletableLinkFormatHandler } from './segment/undeletableLinkFormatHandler';
3536
import { underlineFormatHandler } from './segment/underlineFormatHandler';
3637
import { verticalAlignFormatHandler } from './common/verticalAlignFormatHandler';
3738
import { whiteSpaceFormatHandler } from './block/whiteSpaceFormatHandler';
@@ -85,6 +86,7 @@ const defaultFormatHandlerMap: FormatHandlers = {
8586
textColor: textColorFormatHandler,
8687
textColorOnTableCell: textColorOnTableCellFormatHandler,
8788
textIndent: textIndentFormatHandler,
89+
undeletableLink: undeletableLinkFormatHandler,
8890
underline: underlineFormatHandler,
8991
verticalAlign: verticalAlignFormatHandler,
9092
whiteSpace: whiteSpaceFormatHandler,
@@ -200,6 +202,7 @@ export const defaultFormatKeysPerCategory: {
200202
'border',
201203
'size',
202204
'textAlign',
205+
'undeletableLink',
203206
],
204207
segmentUnderLink: ['textColor'],
205208
code: ['fontFamily', 'display'],
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { isElementOfType } from '../../domUtils/isElementOfType';
2+
import {
3+
isLinkUndeletable,
4+
setLinkUndeletable,
5+
} from '../../domUtils/hiddenProperties/undeletableLink';
6+
import type { UndeletableFormat } from 'roosterjs-content-model-types';
7+
import type { FormatHandler } from '../FormatHandler';
8+
9+
/**
10+
* @internal
11+
*/
12+
export const undeletableLinkFormatHandler: FormatHandler<UndeletableFormat> = {
13+
parse: (format, element) => {
14+
if (isElementOfType(element, 'a') && isLinkUndeletable(element)) {
15+
format.undeletable = true;
16+
}
17+
},
18+
19+
apply: (format, element) => {
20+
if (format.undeletable && isElementOfType(element, 'a')) {
21+
setLinkUndeletable(element, true);
22+
}
23+
},
24+
};

packages/roosterjs-content-model-dom/lib/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ export { reuseCachedElement } from './domUtils/reuseCachedElement';
3939
export { isWhiteSpacePreserved } from './domUtils/isWhiteSpacePreserved';
4040
export { normalizeRect } from './domUtils/normalizeRect';
4141

42+
export { setLinkUndeletable, isLinkUndeletable } from './domUtils/hiddenProperties/undeletableLink';
43+
4244
export { createBr } from './modelApi/creators/createBr';
4345
export { createListItem } from './modelApi/creators/createListItem';
4446
export { createFormatContainer } from './modelApi/creators/createFormatContainer';
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import {
2+
isLinkUndeletable,
3+
setLinkUndeletable,
4+
} from '../../../lib/domUtils/hiddenProperties/undeletableLink';
5+
6+
describe('setLinkUndeletable', () => {
7+
it('should set the undeletable property on the link element', () => {
8+
const linkElement = document.createElement('a');
9+
10+
setLinkUndeletable(linkElement, true);
11+
12+
expect((linkElement as any).__roosterjsHiddenProperty).toEqual({
13+
undeletable: true,
14+
});
15+
16+
setLinkUndeletable(linkElement, false);
17+
18+
expect((linkElement as any).__roosterjsHiddenProperty).toEqual({
19+
undeletable: false,
20+
});
21+
});
22+
});
23+
24+
describe('isLinkUndeletable', () => {
25+
it('should read link undeletable value', () => {
26+
const linkElement = document.createElement('a');
27+
28+
expect(isLinkUndeletable(linkElement)).toBe(false);
29+
30+
(linkElement as any).__roosterjsHiddenProperty = {
31+
undeletable: true,
32+
};
33+
34+
expect(isLinkUndeletable(linkElement)).toBe(true);
35+
});
36+
});
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { UndeletableFormat } from 'roosterjs-content-model-types';
2+
import { undeletableLinkFormatHandler } from '../../../lib/formatHandlers/segment/undeletableLinkFormatHandler';
3+
4+
describe('undeletableLinkFormatHandler.parse', () => {
5+
it('parse from other node', () => {
6+
const div = document.createElement('div');
7+
const format: UndeletableFormat = {};
8+
9+
undeletableLinkFormatHandler.parse(format, div, {} as any, {});
10+
11+
expect(format).toEqual({});
12+
});
13+
14+
it('parse from a', () => {
15+
const a = document.createElement('a');
16+
const format: UndeletableFormat = {};
17+
18+
undeletableLinkFormatHandler.parse(format, a, {} as any, {});
19+
20+
expect(format).toEqual({});
21+
});
22+
23+
it('parse from a with the hidden property', () => {
24+
const a = document.createElement('a');
25+
const format: UndeletableFormat = {};
26+
27+
(a as any).__roosterjsHiddenProperty = {
28+
undeletable: true,
29+
};
30+
31+
undeletableLinkFormatHandler.parse(format, a, {} as any, {});
32+
33+
expect(format).toEqual({
34+
undeletable: true,
35+
});
36+
});
37+
});
38+
39+
describe('undeletableLinkFormatHandler.apply', () => {
40+
it('apply to other node', () => {
41+
const div = document.createElement('div');
42+
const format: UndeletableFormat = {
43+
undeletable: true,
44+
};
45+
46+
undeletableLinkFormatHandler.apply(format, div, {} as any);
47+
48+
expect((div as any).__roosterjsHiddenProperty).toBeUndefined();
49+
});
50+
51+
it('apply to a without the hidden property', () => {
52+
const a = document.createElement('a');
53+
const format: UndeletableFormat = {
54+
undeletable: true,
55+
};
56+
57+
undeletableLinkFormatHandler.apply(format, a, {} as any);
58+
59+
expect((a as any).__roosterjsHiddenProperty).toEqual({
60+
undeletable: true,
61+
});
62+
});
63+
64+
it('apply to a with the hidden property', () => {
65+
const a = document.createElement('a');
66+
const format: UndeletableFormat = {};
67+
68+
(a as any).__roosterjsHiddenProperty = {
69+
test: 'value',
70+
};
71+
72+
undeletableLinkFormatHandler.apply(format, a, {} as any);
73+
74+
expect((a as any).__roosterjsHiddenProperty).toEqual({
75+
test: 'value',
76+
});
77+
});
78+
});

0 commit comments

Comments
 (0)