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
6 changes: 6 additions & 0 deletions demo/scripts/controlsV2/mainPane/MainPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,12 @@ 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',
};

function getAnnouncingString(key: KnownAnnounceStrings) {
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',
};
}
}
);
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { adjustWordSelection } from '../../modelApi/selection/adjustWordSelection';
import { createEditorContextForEntity } from './createEditorContextForEntity';
import {
contentModelToDom,
createDomToModelContext,
Expand All @@ -10,15 +12,14 @@ import type {
ContentModelDocument,
ContentModelEntity,
ContentModelSegmentFormat,
FormatContentModelContext,
FormattableRoot,
IEditor,
PluginEventData,
ReadonlyContentModelDocument,
ShallowMutableContentModelParagraph,
ShallowMutableContentModelSegment,
} from 'roosterjs-content-model-types';
import { adjustWordSelection } from '../../modelApi/selection/adjustWordSelection';
import { createEditorContextForEntity } from './createEditorContextForEntity';

/**
* Invoke a callback to format the selected segment using Content Model
Expand All @@ -44,7 +45,11 @@ export function formatSegmentWithContentModel(
paragraph: ShallowMutableContentModelParagraph | null
) => boolean,
includingFormatHolder?: boolean,
afterFormatCallback?: (model: ReadonlyContentModelDocument) => void
afterFormatCallback?: (
model: ReadonlyContentModelDocument,
isTurningOff: boolean,
context: FormatContentModelContext
) => void
) {
editor.formatContentModel(
(model, context) => {
Expand Down Expand Up @@ -107,7 +112,7 @@ export function formatSegmentWithContentModel(
});

// 5. after format is applied to all selections, invoke another callback to do some clean up before write the change back
afterFormatCallback?.(model);
afterFormatCallback?.(model, isTurningOff, context);

// 6. finally merge segments if possible, to avoid fragmentation
formatsAndSegments.forEach(([_, __, paragraph]) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { ContentModelDocument } from 'roosterjs-content-model-types';
import { segmentTestCommon } from './segmentTestCommon';
import { toggleBold } from '../../../lib/publicApi/segment/toggleBold';
import {
ContentModelDocument,
IEditor,
ContentModelFormatter,
FormatContentModelOptions,
ContentModelEntity,
DeletedEntity,
ContentModelImage,
} from 'roosterjs-content-model-types';

describe('toggleBold', () => {
function runTest(
Expand Down Expand Up @@ -356,4 +364,227 @@ describe('toggleBold', () => {
1
);
});

describe('with announceFormatChange option', () => {
function runTestWithAnnounceOption(
testName: string,
model: ContentModelDocument,
result: ContentModelDocument,
expectedAnnounceData: any,
calledTimes: number
) {
it(testName, () => {
let formatResult: boolean | undefined;
let announceData: any;
const formatContentModel = jasmine
.createSpy('formatContentModel')
.and.callFake(
(callback: ContentModelFormatter, options: FormatContentModelOptions) => {
expect(options.apiName).toBe('toggleBold');
const context = {
newEntities: [] as ContentModelEntity[],
deletedEntities: [] as DeletedEntity[],
newImages: [] as ContentModelImage[],
};
formatResult = callback(model, context);
announceData = (context as any).announceData;
}
);
const editor = ({
focus: jasmine.createSpy(),
getPendingFormat: () => null as any,
formatContentModel,
} as any) as IEditor;

toggleBold(editor, { announceFormatChange: true });

expect(formatContentModel).toHaveBeenCalledTimes(1);
expect(formatResult).toBe(calledTimes > 0);
expect(model).toEqual(result);
expect(announceData).toEqual(expectedAnnounceData);
});
}

runTestWithAnnounceOption(
'turn on bold with announce',
{
blockGroupType: 'Document',
blocks: [
{
blockType: 'Paragraph',
format: {},
segments: [
{
segmentType: 'Text',
text: 'test',
format: {},
isSelected: true,
},
],
},
],
},
{
blockGroupType: 'Document',
blocks: [
{
blockType: 'Paragraph',
format: {},
segments: [
{
segmentType: 'Text',
text: 'test',
format: {
fontWeight: 'bold',
},
isSelected: true,
},
],
},
],
},
{
defaultStrings: 'announceBoldOn',
},
1
);

runTestWithAnnounceOption(
'turn off bold with announce',
{
blockGroupType: 'Document',
blocks: [
{
blockType: 'Paragraph',
format: {},
segments: [
{
segmentType: 'Text',
text: 'test',
format: { fontWeight: '700' },
isSelected: true,
},
],
},
],
},
{
blockGroupType: 'Document',
blocks: [
{
blockType: 'Paragraph',
format: {},
segments: [
{
segmentType: 'Text',
text: 'test',
format: {
fontWeight: 'normal',
},
isSelected: true,
},
],
},
],
},
{
defaultStrings: 'announceBoldOff',
},
1
);

it('no announce when option is false', () => {
let formatResult: boolean | undefined;
let announceData: any;
const model: ContentModelDocument = {
blockGroupType: 'Document',
blocks: [
{
blockType: 'Paragraph',
format: {},
segments: [
{
segmentType: 'Text',
text: 'test',
format: {},
isSelected: true,
},
],
},
],
};
const formatContentModel = jasmine
.createSpy('formatContentModel')
.and.callFake(
(callback: ContentModelFormatter, options: FormatContentModelOptions) => {
expect(options.apiName).toBe('toggleBold');
const context = {
newEntities: [] as ContentModelEntity[],
deletedEntities: [] as DeletedEntity[],
newImages: [] as ContentModelImage[],
};
formatResult = callback(model, context);
announceData = (context as any).announceData;
}
);
const editor = ({
focus: jasmine.createSpy(),
getPendingFormat: () => null as any,
formatContentModel,
} as any) as IEditor;

toggleBold(editor, { announceFormatChange: false });

expect(formatContentModel).toHaveBeenCalledTimes(1);
expect(formatResult).toBe(true);
expect(announceData).toBeUndefined();
});

it('no announce when option is not provided', () => {
let formatResult: boolean | undefined;
let announceData: any;
const model: ContentModelDocument = {
blockGroupType: 'Document',
blocks: [
{
blockType: 'Paragraph',
format: {},
segments: [
{
segmentType: 'Text',
text: 'test',
format: {},
isSelected: true,
},
],
},
],
};
const formatContentModel = jasmine
.createSpy('formatContentModel')
.and.callFake(
(callback: ContentModelFormatter, options: FormatContentModelOptions) => {
expect(options.apiName).toBe('toggleBold');
const context = {
newEntities: [] as ContentModelEntity[],
deletedEntities: [] as DeletedEntity[],
newImages: [] as ContentModelImage[],
};
formatResult = callback(model, context);
announceData = (context as any).announceData;
}
);
const editor = ({
focus: jasmine.createSpy(),
getPendingFormat: () => null as any,
formatContentModel,
} as any) as IEditor;

toggleBold(editor);

expect(formatContentModel).toHaveBeenCalledTimes(1);
expect(formatResult).toBe(true);
expect(announceData).toBeUndefined();
});
});
});
Loading
Loading