Skip to content

Commit ff1129d

Browse files
Update RoosterJs version 9.41.0 and Legacy Adapter 8.65.2 (#3198)
* Bump RoosterJs To 9.41.0 * Update main and legacyAdapter versions in versions.json to 9.41.0 and 8.65.2 respectively * feat: add announcements for table selection and unselection in the editor (#3195) * feat: add announce options for bold, italic, and underline formatting; update shortcuts and tests * refactor: update context initialization in toggleBold, toggleItalic, and toggleUnderline tests for type safety * feat: add announcements for text selection and unselection in the editor * refactor: simplify table selection logic by using TableSelection type in getIsSelectingOrUnselecting function * Update packages/roosterjs-content-model-core/lib/corePlugin/selection/tableSelectionUtils.ts Co-authored-by: Copilot <[email protected]> * Address comments from review * test: add unit tests for bold, italic, and underline shortcuts with announceFormatChange * Move functionality to a new plugin * Fix * Fix * Fix comment --------- Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent 760f478 commit ff1129d

File tree

84 files changed

+6492
-399
lines changed

Some content is hidden

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

84 files changed

+6492
-399
lines changed

demo/scripts/controlsV2/mainPane/MainPane.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import {
5959
Snapshots,
6060
} from 'roosterjs-content-model-types';
6161
import {
62+
AnnouncePlugin,
6263
AutoFormatPlugin,
6364
CustomReplacePlugin,
6465
EditPlugin,
@@ -71,6 +72,9 @@ import {
7172
TableEditPlugin,
7273
WatermarkPlugin,
7374
TouchPlugin,
75+
FindReplacePlugin,
76+
FindReplaceContext,
77+
createFindReplaceContext,
7478
} from 'roosterjs-content-model-plugins';
7579
import DOMPurify = require('dompurify');
7680

@@ -110,6 +114,8 @@ export class MainPane extends React.Component<{}, MainPaneState> {
110114
private samplePickerPlugin: SamplePickerPlugin;
111115
private snapshots: Snapshots;
112116
private markdownPanePlugin: MarkdownPanePlugin;
117+
private findReplacePlugin: FindReplacePlugin;
118+
private findReplaceContext: FindReplaceContext;
113119

114120
protected sidePane = React.createRef<SidePane>();
115121
protected updateContentPlugin: UpdateContentPlugin;
@@ -149,6 +155,9 @@ export class MainPane extends React.Component<{}, MainPaneState> {
149155
this.samplePickerPlugin = new SamplePickerPlugin();
150156
this.markdownPanePlugin = new MarkdownPanePlugin();
151157

158+
this.findReplaceContext = createFindReplaceContext();
159+
this.findReplacePlugin = new FindReplacePlugin(this.findReplaceContext);
160+
152161
this.state = {
153162
showSidePane: window.location.hash != '',
154163
popoutWindow: null,
@@ -287,6 +296,10 @@ export class MainPane extends React.Component<{}, MainPaneState> {
287296
});
288297
}
289298

299+
getFindReplaceContext(): FindReplaceContext {
300+
return this.findReplaceContext;
301+
}
302+
290303
private renderTitleBar() {
291304
return <TitleBar className={styles.noGrow} />;
292305
}
@@ -368,6 +381,7 @@ export class MainPane extends React.Component<{}, MainPaneState> {
368381
...this.getToggleablePlugins(imageEditPlugin),
369382
this.contentModelPanePlugin.getInnerRibbonPlugin(),
370383
this.updateContentPlugin,
384+
this.findReplacePlugin,
371385
];
372386

373387
if (this.state.showSidePane || this.state.popoutWindow) {
@@ -562,6 +576,7 @@ export class MainPane extends React.Component<{}, MainPaneState> {
562576
undeletableLinkChecker: undeletableLinkChecker,
563577
}),
564578
pluginList.touch && new TouchPlugin(),
579+
pluginList.announce && new AnnouncePlugin(),
565580
].filter(x => !!x);
566581
}
567582
}
@@ -570,6 +585,14 @@ const AnnounceStringMap: Record<KnownAnnounceStrings, string> = {
570585
announceListItemBullet: 'Auto corrected Bullet',
571586
announceListItemNumbering: 'Auto corrected {0}',
572587
announceOnFocusLastCell: 'Warning, pressing tab here adds an extra row.',
588+
announceBoldOn: 'Bold On',
589+
announceBoldOff: 'Bold Off',
590+
announceItalicOn: 'Italic On',
591+
announceItalicOff: 'Italic Off',
592+
announceUnderlineOn: 'Underline On',
593+
announceUnderlineOff: 'Underline Off',
594+
selected: '{0}, selected',
595+
unselected: '{0}, unselected',
573596
};
574597

575598
function getAnnouncingString(key: KnownAnnounceStrings) {

demo/scripts/controlsV2/sidePane/apiPlayground/apiEntries.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as React from 'react';
22
import CreateModelFromHtmlPane from './createModelFromHtml/CreateModelFromHtmlPane';
3+
import FindReplacePane from './findReplace/FindReplacePane';
34
import InsertCustomContainerPane from './insertCustomContainer/InsertCustomContainerPane';
45
import InsertEntityPane from './insertEntity/InsertEntityPane';
56
import PastePane from './paste/PastePane';
@@ -34,6 +35,10 @@ const apiEntries: { [key: string]: ApiEntry } = {
3435
name: 'Insert Custom Container',
3536
component: InsertCustomContainerPane,
3637
},
38+
findReplace: {
39+
name: 'Find and Replace',
40+
component: FindReplacePane,
41+
},
3742
more: {
3843
name: 'Coming soon...',
3944
},
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import * as React from 'react';
2+
import { ApiPaneProps, ApiPlaygroundComponent } from '../ApiPaneProps';
3+
import { find, moveHighlight, replace } from 'roosterjs-content-model-plugins';
4+
import { MainPane } from '../../../mainPane/MainPane';
5+
import { PluginEvent } from 'roosterjs-content-model-types';
6+
7+
interface FindReplacePaneState {
8+
showReplace: boolean;
9+
resultCount: number;
10+
currentIndex: number;
11+
passedEnd: boolean;
12+
}
13+
14+
export default class FindReplacePane extends React.Component<ApiPaneProps, FindReplacePaneState>
15+
implements ApiPlaygroundComponent {
16+
private findTextRef = React.createRef<HTMLInputElement>();
17+
private replaceTextRef = React.createRef<HTMLInputElement>();
18+
private matchCaseRef = React.createRef<HTMLInputElement>();
19+
private wholeWordRef = React.createRef<HTMLInputElement>();
20+
private replaceRef = React.createRef<HTMLInputElement>();
21+
22+
constructor(props: ApiPaneProps) {
23+
super(props);
24+
this.state = {
25+
showReplace: false,
26+
resultCount: 0,
27+
currentIndex: -1,
28+
passedEnd: false,
29+
};
30+
}
31+
32+
public onPluginEvent = (e: PluginEvent) => {
33+
if (e.eventType == 'findResultChanged') {
34+
const { markedIndex, ranges, alternativeRange } = e;
35+
36+
this.setState({
37+
currentIndex: markedIndex,
38+
resultCount: ranges.length,
39+
});
40+
41+
if (markedIndex < 0 && alternativeRange) {
42+
this.props.getEditor().setDOMSelection({
43+
type: 'range',
44+
range: alternativeRange,
45+
isReverted: false,
46+
});
47+
}
48+
}
49+
};
50+
51+
render() {
52+
return (
53+
<>
54+
<div>
55+
Find: <input type="text" ref={this.findTextRef} onChange={this.find} />
56+
</div>
57+
<div>
58+
<input
59+
type="checkbox"
60+
ref={this.matchCaseRef}
61+
onClick={this.find}
62+
id="matchCase"
63+
/>
64+
<label htmlFor="matchCase"> Match Case</label>
65+
</div>
66+
<div>
67+
<input
68+
type="checkbox"
69+
ref={this.wholeWordRef}
70+
onClick={this.find}
71+
id="wholeWord"
72+
/>
73+
<label htmlFor="wholeWord"> Whole Word</label>
74+
</div>
75+
<div>
76+
Results Found:{' '}
77+
{this.state.currentIndex < 0
78+
? this.state.resultCount
79+
: `${this.state.currentIndex + 1} / ${this.state.resultCount}`}
80+
</div>
81+
<div>
82+
<button onClick={this.movePrevious}>Previous</button>
83+
<button onClick={this.moveNext}>Next</button>
84+
</div>
85+
{this.state.passedEnd && <div>You have passed the end of the document.</div>}
86+
<hr />
87+
<div>
88+
<input
89+
type="checkbox"
90+
ref={this.replaceRef}
91+
onChange={this.showHideReplace}
92+
id="replace"
93+
/>
94+
<label htmlFor="replace"> Replace</label>
95+
</div>
96+
{this.state.showReplace && (
97+
<div>
98+
<div>
99+
Replace with: <input type="text" ref={this.replaceTextRef} />
100+
</div>
101+
<div>
102+
<button onClick={this.replace} disabled={this.state.resultCount == 0}>
103+
Replace
104+
</button>
105+
<button
106+
onClick={this.replaceAll}
107+
disabled={this.state.resultCount == 0}>
108+
Replace All
109+
</button>
110+
</div>
111+
</div>
112+
)}
113+
</>
114+
);
115+
}
116+
117+
private find = () => {
118+
const context = MainPane.getInstance().getFindReplaceContext();
119+
120+
find(
121+
this.props.getEditor(),
122+
context,
123+
this.findTextRef.current?.value ?? null,
124+
this.matchCaseRef.current?.checked,
125+
this.wholeWordRef.current?.checked
126+
);
127+
128+
this.setState({ resultCount: context.ranges.length, currentIndex: -1, passedEnd: false });
129+
};
130+
131+
private showHideReplace = () => {
132+
this.setState(prevState => ({ showReplace: !prevState.showReplace }));
133+
};
134+
135+
private replace = () => {
136+
const context = MainPane.getInstance().getFindReplaceContext();
137+
138+
replace(this.props.getEditor(), context, this.replaceTextRef.current?.value || '');
139+
140+
this.setState({ resultCount: context.ranges.length, currentIndex: -1, passedEnd: false });
141+
};
142+
143+
private replaceAll = () => {
144+
const context = MainPane.getInstance().getFindReplaceContext();
145+
146+
replace(this.props.getEditor(), context, this.replaceTextRef.current?.value || '', true);
147+
148+
this.setState({ resultCount: context.ranges.length, currentIndex: -1, passedEnd: false });
149+
};
150+
151+
private movePrevious = () => {
152+
const oldIndex = this.state.currentIndex;
153+
const context = MainPane.getInstance().getFindReplaceContext();
154+
155+
moveHighlight(this.props.getEditor(), context, false);
156+
157+
this.setState({
158+
currentIndex: context.markedIndex,
159+
passedEnd: oldIndex < context.markedIndex,
160+
});
161+
};
162+
163+
private moveNext = () => {
164+
const oldIndex = this.state.currentIndex;
165+
const context = MainPane.getInstance().getFindReplaceContext();
166+
167+
moveHighlight(this.props.getEditor(), context, true);
168+
169+
this.setState({
170+
currentIndex: context.markedIndex,
171+
passedEnd: oldIndex > context.markedIndex,
172+
});
173+
};
174+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const initialState: OptionState = {
2323
customReplace: true,
2424
hiddenProperty: true,
2525
touch: true,
26+
announce: true,
2627
},
2728
defaultFormat: {
2829
fontFamily: 'Calibri',

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface BuildInPluginList {
2424
customReplace: boolean;
2525
hiddenProperty: boolean;
2626
touch: boolean;
27+
announce: boolean;
2728
}
2829

2930
export interface OptionState {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,7 @@ export class Plugins extends PluginsBase<keyof BuildInPluginList> {
329329
)}
330330
{this.renderPluginItem('hiddenProperty', 'Hidden Property')}
331331
{this.renderPluginItem('touch', 'Touch')}
332+
{this.renderPluginItem('announce', 'Announce')}
332333
</tbody>
333334
</table>
334335
);

karma.fast.conf.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ module.exports = function (config) {
9999

100100
webpack: {
101101
mode: 'development',
102-
devtool: 'eval-source-map', // Faster than inline-source-map
102+
devtool: 'inline-source-map', // More accurate source maps for debugging
103103
module: {
104104
rules,
105105
},

packages/roosterjs-content-model-api/lib/publicApi/segment/toggleBold.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel';
22
import { isBold } from 'roosterjs-content-model-dom';
3-
import type { IEditor } from 'roosterjs-content-model-types';
3+
import type { AnnouncingOption, IEditor } from 'roosterjs-content-model-types';
44

55
/**
66
* Toggle bold style
77
* @param editor The editor to operate on
88
*/
9-
export function toggleBold(editor: IEditor) {
9+
export function toggleBold(editor: IEditor, options?: AnnouncingOption) {
1010
editor.focus();
1111

1212
formatSegmentWithContentModel(
@@ -20,6 +20,14 @@ export function toggleBold(editor: IEditor) {
2020
typeof format.fontWeight == 'undefined'
2121
? paragraph?.decorator?.format.fontWeight
2222
: format.fontWeight
23-
)
23+
),
24+
undefined /* includeFormatHolder */,
25+
(_model, isTurningOff, context) => {
26+
if (options?.announceFormatChange) {
27+
context.announceData = {
28+
defaultStrings: isTurningOff ? 'announceBoldOff' : 'announceBoldOn',
29+
};
30+
}
31+
}
2432
);
2533
}
Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel';
2-
import type { IEditor } from 'roosterjs-content-model-types';
2+
import type { AnnouncingOption, IEditor } from 'roosterjs-content-model-types';
33

44
/**
55
* Toggle italic style
66
* @param editor The editor to operate on
77
*/
8-
export function toggleItalic(editor: IEditor) {
8+
export function toggleItalic(editor: IEditor, options?: AnnouncingOption) {
99
editor.focus();
1010

1111
formatSegmentWithContentModel(
@@ -14,6 +14,14 @@ export function toggleItalic(editor: IEditor) {
1414
(format, isTurningOn) => {
1515
format.italic = !!isTurningOn;
1616
},
17-
format => !!format.italic
17+
format => !!format.italic,
18+
undefined /* includingFormatHolder */,
19+
(_model, isTurningOff, context) => {
20+
if (options?.announceFormatChange) {
21+
context.announceData = {
22+
defaultStrings: isTurningOff ? 'announceItalicOff' : 'announceItalicOn',
23+
};
24+
}
25+
}
1826
);
1927
}

packages/roosterjs-content-model-api/lib/publicApi/segment/toggleUnderline.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel';
2-
import type { IEditor } from 'roosterjs-content-model-types';
2+
import type { AnnouncingOption, IEditor } from 'roosterjs-content-model-types';
33

44
/**
55
* Toggle underline style
66
* @param editor The editor to operate on
77
*/
8-
export function toggleUnderline(editor: IEditor) {
8+
export function toggleUnderline(editor: IEditor, options?: AnnouncingOption) {
99
editor.focus();
1010

1111
formatSegmentWithContentModel(
@@ -19,6 +19,13 @@ export function toggleUnderline(editor: IEditor) {
1919
}
2020
},
2121
(format, segment) => !!format.underline || !!segment?.link?.format?.underline,
22-
false /*includingFormatHolder*/
22+
false /*includingFormatHolder*/,
23+
(_model, isTurningOff, context) => {
24+
if (options?.announceFormatChange) {
25+
context.announceData = {
26+
defaultStrings: isTurningOff ? 'announceUnderlineOff' : 'announceUnderlineOn',
27+
};
28+
}
29+
}
2330
);
2431
}

0 commit comments

Comments
 (0)