Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions demo/scripts/controlsV2/mainPane/MainPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import {
Snapshots,
} from 'roosterjs-content-model-types';
import {
AnnouncePlugin,
AutoFormatPlugin,
CustomReplacePlugin,
EditPlugin,
Expand All @@ -71,6 +72,9 @@ import {
TableEditPlugin,
WatermarkPlugin,
TouchPlugin,
FindReplacePlugin,
FindReplaceContext,
createFindReplaceContext,
} from 'roosterjs-content-model-plugins';
import DOMPurify = require('dompurify');

Expand Down Expand Up @@ -110,6 +114,8 @@ export class MainPane extends React.Component<{}, MainPaneState> {
private samplePickerPlugin: SamplePickerPlugin;
private snapshots: Snapshots;
private markdownPanePlugin: MarkdownPanePlugin;
private findReplacePlugin: FindReplacePlugin;
private findReplaceContext: FindReplaceContext;

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

this.findReplaceContext = createFindReplaceContext();
this.findReplacePlugin = new FindReplacePlugin(this.findReplaceContext);

this.state = {
showSidePane: window.location.hash != '',
popoutWindow: null,
Expand Down Expand Up @@ -287,6 +296,10 @@ export class MainPane extends React.Component<{}, MainPaneState> {
});
}

getFindReplaceContext(): FindReplaceContext {
return this.findReplaceContext;
}

private renderTitleBar() {
return <TitleBar className={styles.noGrow} />;
}
Expand Down Expand Up @@ -368,6 +381,7 @@ export class MainPane extends React.Component<{}, MainPaneState> {
...this.getToggleablePlugins(imageEditPlugin),
this.contentModelPanePlugin.getInnerRibbonPlugin(),
this.updateContentPlugin,
this.findReplacePlugin,
];

if (this.state.showSidePane || this.state.popoutWindow) {
Expand Down Expand Up @@ -562,6 +576,7 @@ export class MainPane extends React.Component<{}, MainPaneState> {
undeletableLinkChecker: undeletableLinkChecker,
}),
pluginList.touch && new TouchPlugin(),
pluginList.announce && new AnnouncePlugin(),
].filter(x => !!x);
}
}
Expand All @@ -570,6 +585,14 @@ const AnnounceStringMap: Record<KnownAnnounceStrings, string> = {
announceListItemBullet: 'Auto corrected Bullet',
announceListItemNumbering: 'Auto corrected {0}',
announceOnFocusLastCell: 'Warning, pressing tab here adds an extra row.',
announceBoldOn: 'Bold On',
announceBoldOff: 'Bold Off',
announceItalicOn: 'Italic On',
announceItalicOff: 'Italic Off',
announceUnderlineOn: 'Underline On',
announceUnderlineOff: 'Underline Off',
selected: '{0}, selected',
unselected: '{0}, unselected',
};

function getAnnouncingString(key: KnownAnnounceStrings) {
Expand Down
5 changes: 5 additions & 0 deletions demo/scripts/controlsV2/sidePane/apiPlayground/apiEntries.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react';
import CreateModelFromHtmlPane from './createModelFromHtml/CreateModelFromHtmlPane';
import FindReplacePane from './findReplace/FindReplacePane';
import InsertCustomContainerPane from './insertCustomContainer/InsertCustomContainerPane';
import InsertEntityPane from './insertEntity/InsertEntityPane';
import PastePane from './paste/PastePane';
Expand Down Expand Up @@ -34,6 +35,10 @@ const apiEntries: { [key: string]: ApiEntry } = {
name: 'Insert Custom Container',
component: InsertCustomContainerPane,
},
findReplace: {
name: 'Find and Replace',
component: FindReplacePane,
},
more: {
name: 'Coming soon...',
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import * as React from 'react';
import { ApiPaneProps, ApiPlaygroundComponent } from '../ApiPaneProps';
import { find, moveHighlight, replace } from 'roosterjs-content-model-plugins';
import { MainPane } from '../../../mainPane/MainPane';
import { PluginEvent } from 'roosterjs-content-model-types';

interface FindReplacePaneState {
showReplace: boolean;
resultCount: number;
currentIndex: number;
passedEnd: boolean;
}

export default class FindReplacePane extends React.Component<ApiPaneProps, FindReplacePaneState>
implements ApiPlaygroundComponent {
private findTextRef = React.createRef<HTMLInputElement>();
private replaceTextRef = React.createRef<HTMLInputElement>();
private matchCaseRef = React.createRef<HTMLInputElement>();
private wholeWordRef = React.createRef<HTMLInputElement>();
private replaceRef = React.createRef<HTMLInputElement>();

constructor(props: ApiPaneProps) {
super(props);
this.state = {
showReplace: false,
resultCount: 0,
currentIndex: -1,
passedEnd: false,
};
}

public onPluginEvent = (e: PluginEvent) => {
if (e.eventType == 'findResultChanged') {
const { markedIndex, ranges, alternativeRange } = e;

this.setState({
currentIndex: markedIndex,
resultCount: ranges.length,
});

if (markedIndex < 0 && alternativeRange) {
this.props.getEditor().setDOMSelection({
type: 'range',
range: alternativeRange,
isReverted: false,
});
}
}
};

render() {
return (
<>
<div>
Find: <input type="text" ref={this.findTextRef} onChange={this.find} />
</div>
<div>
<input
type="checkbox"
ref={this.matchCaseRef}
onClick={this.find}
id="matchCase"
/>
<label htmlFor="matchCase"> Match Case</label>
</div>
<div>
<input
type="checkbox"
ref={this.wholeWordRef}
onClick={this.find}
id="wholeWord"
/>
<label htmlFor="wholeWord"> Whole Word</label>
</div>
<div>
Results Found:{' '}
{this.state.currentIndex < 0
? this.state.resultCount
: `${this.state.currentIndex + 1} / ${this.state.resultCount}`}
</div>
<div>
<button onClick={this.movePrevious}>Previous</button>
<button onClick={this.moveNext}>Next</button>
</div>
{this.state.passedEnd && <div>You have passed the end of the document.</div>}
<hr />
<div>
<input
type="checkbox"
ref={this.replaceRef}
onChange={this.showHideReplace}
id="replace"
/>
<label htmlFor="replace"> Replace</label>
</div>
{this.state.showReplace && (
<div>
<div>
Replace with: <input type="text" ref={this.replaceTextRef} />
</div>
<div>
<button onClick={this.replace} disabled={this.state.resultCount == 0}>
Replace
</button>
<button
onClick={this.replaceAll}
disabled={this.state.resultCount == 0}>
Replace All
</button>
</div>
</div>
)}
</>
);
}

private find = () => {
const context = MainPane.getInstance().getFindReplaceContext();

find(
this.props.getEditor(),
context,
this.findTextRef.current?.value ?? null,
this.matchCaseRef.current?.checked,
this.wholeWordRef.current?.checked
);

this.setState({ resultCount: context.ranges.length, currentIndex: -1, passedEnd: false });
};

private showHideReplace = () => {
this.setState(prevState => ({ showReplace: !prevState.showReplace }));
};

private replace = () => {
const context = MainPane.getInstance().getFindReplaceContext();

replace(this.props.getEditor(), context, this.replaceTextRef.current?.value || '');

this.setState({ resultCount: context.ranges.length, currentIndex: -1, passedEnd: false });
};

private replaceAll = () => {
const context = MainPane.getInstance().getFindReplaceContext();

replace(this.props.getEditor(), context, this.replaceTextRef.current?.value || '', true);

this.setState({ resultCount: context.ranges.length, currentIndex: -1, passedEnd: false });
};

private movePrevious = () => {
const oldIndex = this.state.currentIndex;
const context = MainPane.getInstance().getFindReplaceContext();

moveHighlight(this.props.getEditor(), context, false);

this.setState({
currentIndex: context.markedIndex,
passedEnd: oldIndex < context.markedIndex,
});
};

private moveNext = () => {
const oldIndex = this.state.currentIndex;
const context = MainPane.getInstance().getFindReplaceContext();

moveHighlight(this.props.getEditor(), context, true);

this.setState({
currentIndex: context.markedIndex,
passedEnd: oldIndex > context.markedIndex,
});
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const initialState: OptionState = {
customReplace: true,
hiddenProperty: true,
touch: true,
announce: true,
},
defaultFormat: {
fontFamily: 'Calibri',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface BuildInPluginList {
customReplace: boolean;
hiddenProperty: boolean;
touch: boolean;
announce: boolean;
}

export interface OptionState {
Expand Down
1 change: 1 addition & 0 deletions demo/scripts/controlsV2/sidePane/editorOptions/Plugins.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ export class Plugins extends PluginsBase<keyof BuildInPluginList> {
)}
{this.renderPluginItem('hiddenProperty', 'Hidden Property')}
{this.renderPluginItem('touch', 'Touch')}
{this.renderPluginItem('announce', 'Announce')}
</tbody>
</table>
);
Expand Down
2 changes: 1 addition & 1 deletion karma.fast.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ module.exports = function (config) {

webpack: {
mode: 'development',
devtool: 'eval-source-map', // Faster than inline-source-map
devtool: 'inline-source-map', // More accurate source maps for debugging
module: {
rules,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel';
import { isBold } from 'roosterjs-content-model-dom';
import type { IEditor } from 'roosterjs-content-model-types';
import type { AnnouncingOption, IEditor } from 'roosterjs-content-model-types';

/**
* Toggle bold style
* @param editor The editor to operate on
*/
export function toggleBold(editor: IEditor) {
export function toggleBold(editor: IEditor, options?: AnnouncingOption) {
editor.focus();

formatSegmentWithContentModel(
Expand All @@ -20,6 +20,14 @@ export function toggleBold(editor: IEditor) {
typeof format.fontWeight == 'undefined'
? paragraph?.decorator?.format.fontWeight
: format.fontWeight
)
),
undefined /* includeFormatHolder */,
(_model, isTurningOff, context) => {
if (options?.announceFormatChange) {
context.announceData = {
defaultStrings: isTurningOff ? 'announceBoldOff' : 'announceBoldOn',
};
}
}
);
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel';
import type { IEditor } from 'roosterjs-content-model-types';
import type { AnnouncingOption, IEditor } from 'roosterjs-content-model-types';

/**
* Toggle italic style
* @param editor The editor to operate on
*/
export function toggleItalic(editor: IEditor) {
export function toggleItalic(editor: IEditor, options?: AnnouncingOption) {
editor.focus();

formatSegmentWithContentModel(
Expand All @@ -14,6 +14,14 @@ export function toggleItalic(editor: IEditor) {
(format, isTurningOn) => {
format.italic = !!isTurningOn;
},
format => !!format.italic
format => !!format.italic,
undefined /* includingFormatHolder */,
(_model, isTurningOff, context) => {
if (options?.announceFormatChange) {
context.announceData = {
defaultStrings: isTurningOff ? 'announceItalicOff' : 'announceItalicOn',
};
}
}
);
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel';
import type { IEditor } from 'roosterjs-content-model-types';
import type { AnnouncingOption, IEditor } from 'roosterjs-content-model-types';

/**
* Toggle underline style
* @param editor The editor to operate on
*/
export function toggleUnderline(editor: IEditor) {
export function toggleUnderline(editor: IEditor, options?: AnnouncingOption) {
editor.focus();

formatSegmentWithContentModel(
Expand All @@ -19,6 +19,13 @@ export function toggleUnderline(editor: IEditor) {
}
},
(format, segment) => !!format.underline || !!segment?.link?.format?.underline,
false /*includingFormatHolder*/
false /*includingFormatHolder*/,
(_model, isTurningOff, context) => {
if (options?.announceFormatChange) {
context.announceData = {
defaultStrings: isTurningOff ? 'announceUnderlineOff' : 'announceUnderlineOn',
};
}
}
);
}
Loading
Loading