Skip to content

Commit bf2abbb

Browse files
authored
Merge pull request #3004 from microsoft/u/nguyenvi/versionbump041825
U/nguyenvi/versionbump041825
2 parents 6bd559f + ddf94df commit bf2abbb

File tree

67 files changed

+8584
-1139
lines changed

Some content is hidden

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

67 files changed

+8584
-1139
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ There are also some extension packages to provide additional functionalities.
5151
1. [roosterjs-color-utils](https://microsoft.github.io/roosterjs/docs/modules/roosterjs_color_utils.html):
5252
Provide color transformation utility to make editor work under dark mode.
5353

54+
2. [roosterjs-content-model-markdown](https://microsoft.github.io/roosterjs/docs/modules/roosterjs_content_model_types.html):
55+
Defines public APIs to enable conversions between Markdown and ContentModel
56+
5457
To be compatible with old (8.\*) versions, you can use `EditorAdapter` class from the following package which can act as a 8.\* Editor:
5558

5659
1. [roosterjs-editor-adapter](https://microsoft.github.io/roosterjs/docs/modules/roosterjs_editor_adapter.html):

demo/scripts/controlsV2/sidePane/MarkdownPane/MarkdownPane.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import * as React from 'react';
2-
import { convertMarkdownToContentModel } from 'roosterjs-content-model-markdown';
32
import { MarkdownPaneProps } from './MarkdownPanePlugin';
3+
import {
4+
convertMarkdownToContentModel,
5+
convertContentModelToMarkdown,
6+
} from 'roosterjs-content-model-markdown';
47
import {
58
createBr,
69
createParagraph,
@@ -44,6 +47,14 @@ export default class MarkdownPane extends React.Component<MarkdownPaneProps> {
4447
});
4548
};
4649

50+
private generateMarkdown = () => {
51+
const model = this.props.getEditor().getContentModelCopy('disconnected');
52+
if (model) {
53+
const content = convertContentModelToMarkdown(model);
54+
this.html.current.value = content;
55+
}
56+
};
57+
4758
render() {
4859
return (
4960
<div className={styles.container}>
@@ -62,6 +73,9 @@ export default class MarkdownPane extends React.Component<MarkdownPaneProps> {
6273
<button type="button" onClick={this.convert}>
6374
Convert
6475
</button>
76+
<button type="button" onClick={this.generateMarkdown}>
77+
Create Markdown from editor content
78+
</button>
6579
</div>
6680
</div>
6781
);

packages/roosterjs-content-model-api/lib/publicApi/format/getFormatState.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { reducedModelChildProcessor } from '../../modelApi/common/reducedModelChildProcessor';
22
import { retrieveModelFormatState } from 'roosterjs-content-model-dom';
3-
import type { IEditor, ContentModelFormatState, ConflictFormatSolution } from 'roosterjs-content-model-types';
3+
import type {
4+
IEditor,
5+
ContentModelFormatState,
6+
ConflictFormatSolution,
7+
} from 'roosterjs-content-model-types';
48

59
/**
610
* Get current format state
@@ -21,7 +25,13 @@ export function getFormatState(
2125

2226
editor.formatContentModel(
2327
model => {
24-
retrieveModelFormatState(model, pendingFormat, result, conflictSolution);
28+
retrieveModelFormatState(
29+
model,
30+
pendingFormat,
31+
result,
32+
conflictSolution,
33+
editor.getDOMHelper()
34+
);
2535

2636
return false;
2737
},

packages/roosterjs-content-model-api/test/publicApi/format/getFormatStateTest.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import * as retrieveModelFormatState from 'roosterjs-content-model-dom/lib/modelApi/editing/retrieveModelFormatState';
2-
import { ContentModelDocument, ContentModelSegmentFormat } from 'roosterjs-content-model-types';
32
import { ContentModelFormatState } from 'roosterjs-content-model-types';
43
import { getFormatState } from '../../../lib/publicApi/format/getFormatState';
54
import { IEditor } from 'roosterjs-content-model-types';
65
import { reducedModelChildProcessor } from '../../../lib/modelApi/common/reducedModelChildProcessor';
6+
import {
7+
ContentModelDocument,
8+
ContentModelSegmentFormat,
9+
DOMHelper,
10+
} from 'roosterjs-content-model-types';
711
import {
812
createContentModelDocument,
913
createDomToModelContext,
@@ -23,6 +27,7 @@ describe('getFormatState', () => {
2327
expectedModel: ContentModelDocument,
2428
expectedFormat: ContentModelFormatState
2529
) {
30+
const mockedDOMHelper: DOMHelper = {} as any;
2631
const editor = ({
2732
getSnapshotsManager: () => ({
2833
hasNewContent: false,
@@ -31,6 +36,7 @@ describe('getFormatState', () => {
3136
isDarkMode: () => false,
3237
getZoomScale: () => 1,
3338
getPendingFormat: () => pendingFormat,
39+
getDOMHelper: () => mockedDOMHelper,
3440
formatContentModel: (callback: Function) => {
3541
const model = createContentModelDocument();
3642
const editorDiv = document.createElement('div');
@@ -73,7 +79,8 @@ describe('getFormatState', () => {
7379
canRedo: false,
7480
isDarkMode: false,
7581
},
76-
'remove'
82+
'remove',
83+
mockedDOMHelper
7784
);
7885
expect(result).toEqual(expectedFormat);
7986
}

packages/roosterjs-content-model-core/lib/coreApi/setDOMSelection/setDOMSelection.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { areSameSelections } from '../../corePlugin/cache/areSameSelections';
33
import { ensureUniqueId } from '../setEditorStyle/ensureUniqueId';
44
import { findLastedCoInMergedCell } from './findLastedCoInMergedCell';
55
import { findTableCellElement } from './findTableCellElement';
6+
import { toggleCaret } from './toggleCaret';
67
import {
78
getSafeIdSelector,
89
isNodeOfType,
@@ -17,11 +18,9 @@ import type {
1718
} from 'roosterjs-content-model-types';
1819

1920
const DOM_SELECTION_CSS_KEY = '_DOMSelection';
20-
const HIDE_CURSOR_CSS_KEY = '_DOMSelectionHideCursor';
2121
const HIDE_SELECTION_CSS_KEY = '_DOMSelectionHideSelection';
2222
const IMAGE_ID = 'image';
2323
const TABLE_ID = 'table';
24-
const CARET_CSS_RULE = 'caret-color: transparent';
2524
const TRANSPARENT_SELECTION_CSS_RULE = 'background-color: transparent !important;';
2625
const SELECTION_SELECTOR = '*::selection';
2726
const DEFAULT_SELECTION_BORDER_COLOR = '#DB626C';
@@ -44,9 +43,10 @@ export const setDOMSelection: SetDOMSelection = (core, selection, skipSelectionC
4443
const isDarkMode = core.lifecycle.isDarkMode;
4544
core.selection.skipReselectOnFocus = true;
4645
core.api.setEditorStyle(core, DOM_SELECTION_CSS_KEY, null /*cssRule*/);
47-
core.api.setEditorStyle(core, HIDE_CURSOR_CSS_KEY, null /*cssRule*/);
4846
core.api.setEditorStyle(core, HIDE_SELECTION_CSS_KEY, null /*cssRule*/);
4947

48+
toggleCaret(core, false /* hide */);
49+
5050
try {
5151
switch (selection?.type) {
5252
case 'image':
@@ -137,14 +137,15 @@ export const setDOMSelection: SetDOMSelection = (core, selection, skipSelectionC
137137
`background-color:${tableSelectionColor}!important;`,
138138
tableSelectors
139139
);
140-
core.api.setEditorStyle(core, HIDE_CURSOR_CSS_KEY, CARET_CSS_RULE);
141140
core.api.setEditorStyle(
142141
core,
143142
HIDE_SELECTION_CSS_KEY,
144143
TRANSPARENT_SELECTION_CSS_RULE,
145144
[SELECTION_SELECTOR]
146145
);
147146

147+
toggleCaret(core, true /* hide */);
148+
148149
const nodeToSelect = firstCell.cell?.firstElementChild || firstCell.cell;
149150

150151
if (nodeToSelect) {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { EditorCore } from 'roosterjs-content-model-types';
2+
3+
const CARET_CSS_RULE = 'caret-color: transparent';
4+
const HIDE_CURSOR_CSS_KEY = '_DOMSelectionHideCursor';
5+
6+
/**
7+
* @internal Show/Hide caret in editor
8+
* @param core The editor core
9+
* @param isHiding True to hide caret, false to show caret
10+
*/
11+
export function toggleCaret(core: EditorCore, isHiding: boolean) {
12+
core.api.setEditorStyle(core, HIDE_CURSOR_CSS_KEY, isHiding ? CARET_CSS_RULE : null);
13+
}

packages/roosterjs-content-model-core/lib/coreApi/switchShadowEdit/switchShadowEdit.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { iterateSelections, moveChildNodes } from 'roosterjs-content-model-dom';
2+
import { toggleCaret } from '../setDOMSelection/toggleCaret';
23
import type { SwitchShadowEdit } from 'roosterjs-content-model-types';
34

45
/**
@@ -32,10 +33,14 @@ export const switchShadowEdit: SwitchShadowEdit = (editorCore, isOn): void => {
3233
core.cache.cachedModel = model;
3334
}
3435

36+
toggleCaret(core, true /* hide */);
37+
3538
core.lifecycle.shadowEditFragment = fragment;
3639
} else {
3740
core.lifecycle.shadowEditFragment = null;
3841

42+
toggleCaret(core, false /* hide */);
43+
3944
core.api.triggerEvent(
4045
core,
4146
{

packages/roosterjs-content-model-core/lib/corePlugin/entity/entityDelimiterUtils.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -284,8 +284,15 @@ function handleInputOnDelimiter(
284284
});
285285
} else {
286286
if (isEnter) {
287-
rawEvent.preventDefault();
288-
editor.formatContentModel(handleEnterInlineEntity);
287+
editor.formatContentModel((model, context) => {
288+
const result = handleEnterInlineEntity(model, context);
289+
290+
if (result) {
291+
rawEvent.preventDefault();
292+
}
293+
294+
return result;
295+
});
289296
} else {
290297
editor.takeSnapshot();
291298
editor
@@ -331,10 +338,15 @@ export const handleEnterInlineEntity: ContentModelFormatter = model => {
331338
iterateSelections(model, (path, _tableContext, block) => {
332339
if (block?.blockType == 'Paragraph') {
333340
readonlySelectionBlock = block;
334-
selectionBlockParent = path[path.length - 1];
341+
selectionBlockParent = path[0];
335342
}
336343
});
337344

345+
if (selectionBlockParent?.blockGroupType == 'ListItem') {
346+
// No need to handle list item since it will be handled by common enter handler code
347+
return false;
348+
}
349+
338350
if (readonlySelectionBlock && selectionBlockParent) {
339351
const markerIndex = readonlySelectionBlock.segments.findIndex(
340352
segment => segment.segmentType == 'SelectionMarker'

packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { isNodeOfType, parseValueWithUnit, toArray } from 'roosterjs-content-model-dom';
2-
import type { DOMHelper } from 'roosterjs-content-model-types';
2+
import type { ContentModelSegmentFormat, DOMHelper } from 'roosterjs-content-model-types';
33

44
class DOMHelperImpl implements DOMHelper {
55
constructor(private contentDiv: HTMLElement) {}
@@ -88,6 +88,31 @@ class DOMHelperImpl implements DOMHelper {
8888
getClonedRoot(): HTMLElement {
8989
return this.contentDiv.cloneNode(true /*deep*/) as HTMLElement;
9090
}
91+
92+
/**
93+
* Get format of the container element
94+
*/
95+
getContainerFormat(): ContentModelSegmentFormat {
96+
const window = this.contentDiv.ownerDocument.defaultView;
97+
98+
const style = window?.getComputedStyle(this.contentDiv);
99+
100+
return style
101+
? {
102+
fontSize: style.fontSize,
103+
fontFamily: style.fontFamily,
104+
fontWeight: style.fontWeight,
105+
textColor: style.color,
106+
backgroundColor: style.backgroundColor,
107+
italic: style.fontStyle == 'italic',
108+
letterSpacing: style.letterSpacing,
109+
lineHeight: style.lineHeight,
110+
strikethrough: style.textDecoration?.includes('line-through'),
111+
superOrSubScriptSequence: style.verticalAlign,
112+
underline: style.textDecoration?.includes('underline'),
113+
}
114+
: {};
115+
}
91116
}
92117

93118
/**

packages/roosterjs-content-model-core/test/coreApi/switchShadowEdit/switchShadowEditTest.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as iterateSelections from 'roosterjs-content-model-dom/lib/modelApi/selection/iterateSelections';
2+
import * as toggleCaret from '../../../lib/coreApi/setDOMSelection/toggleCaret';
23
import { EditorCore } from 'roosterjs-content-model-types';
34
import { switchShadowEdit } from '../../../lib/coreApi/switchShadowEdit/switchShadowEdit';
45

@@ -11,12 +12,14 @@ describe('switchShadowEdit', () => {
1112
let setContentModel: jasmine.Spy;
1213
let getSelectionRange: jasmine.Spy;
1314
let triggerEvent: jasmine.Spy;
15+
let toggleCaretSpy: jasmine.Spy;
1416

1517
beforeEach(() => {
1618
createContentModel = jasmine.createSpy('createContentModel').and.returnValue(mockedModel);
1719
setContentModel = jasmine.createSpy('setContentModel');
1820
getSelectionRange = jasmine.createSpy('getSelectionRange');
1921
triggerEvent = jasmine.createSpy('triggerEvent');
22+
toggleCaretSpy = spyOn(toggleCaret, 'toggleCaret');
2023

2124
const contentDiv = document.createElement('div');
2225

@@ -50,6 +53,7 @@ describe('switchShadowEdit', () => {
5053
},
5154
false
5255
);
56+
expect(toggleCaretSpy).toHaveBeenCalledWith(core, true);
5357
});
5458

5559
it('with cache, isOn', () => {
@@ -69,6 +73,7 @@ describe('switchShadowEdit', () => {
6973
},
7074
false
7175
);
76+
expect(toggleCaretSpy).toHaveBeenCalledWith(core, true);
7277
});
7378

7479
it('no cache, isOff', () => {
@@ -79,6 +84,7 @@ describe('switchShadowEdit', () => {
7984
expect(core.cache.cachedModel).toBe(undefined);
8085

8186
expect(triggerEvent).not.toHaveBeenCalled();
87+
expect(toggleCaretSpy).not.toHaveBeenCalled();
8288
});
8389

8490
it('with cache, isOff', () => {
@@ -91,6 +97,7 @@ describe('switchShadowEdit', () => {
9197
expect(core.cache.cachedModel).toBe(mockedCachedModel);
9298

9399
expect(triggerEvent).not.toHaveBeenCalled();
100+
expect(toggleCaretSpy).not.toHaveBeenCalled();
94101
});
95102
});
96103

@@ -107,6 +114,7 @@ describe('switchShadowEdit', () => {
107114
expect(core.cache.cachedModel).toBe(undefined);
108115

109116
expect(triggerEvent).not.toHaveBeenCalled();
117+
expect(toggleCaretSpy).not.toHaveBeenCalled();
110118
});
111119

112120
it('with cache, isOn', () => {
@@ -119,6 +127,7 @@ describe('switchShadowEdit', () => {
119127
expect(core.cache.cachedModel).toBe(mockedCachedModel);
120128

121129
expect(triggerEvent).not.toHaveBeenCalled();
130+
expect(toggleCaretSpy).not.toHaveBeenCalled();
122131
});
123132

124133
it('no cache, isOff', () => {
@@ -136,6 +145,7 @@ describe('switchShadowEdit', () => {
136145
},
137146
false
138147
);
148+
expect(toggleCaretSpy).toHaveBeenCalledWith(core, false);
139149
});
140150

141151
it('with cache, isOff', () => {
@@ -159,6 +169,7 @@ describe('switchShadowEdit', () => {
159169
},
160170
false
161171
);
172+
expect(toggleCaretSpy).toHaveBeenCalledWith(core, false);
162173
});
163174
});
164175
});

0 commit comments

Comments
 (0)