Skip to content

Commit 0dd2b5d

Browse files
Merge branch 'master' of https://github.com/microsoft/roosterjs into u/bvalverde/skipVerticalSplitsOnMerge
2 parents d25ebe8 + 4616869 commit 0dd2b5d

7 files changed

Lines changed: 141 additions & 41 deletions

File tree

packages/roosterjs-content-model-core/lib/command/paste/paste.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,20 @@ export function paste(
4242
clipboardData.rawHtml = cleanHtmlComments(clipboardData.rawHtml);
4343
}
4444
const doc = createDOMFromHtml(clipboardData.rawHtml, domCreator);
45-
const pasteType =
46-
typeof pasteTypeOrGetter == 'function'
47-
? pasteTypeOrGetter(doc, clipboardData)
48-
: pasteTypeOrGetter;
4945

5046
// 2. Handle HTML from clipboard
5147
const htmlFromClipboard = retrieveHtmlInfo(doc, clipboardData);
5248

49+
const pasteType =
50+
typeof pasteTypeOrGetter == 'function'
51+
? pasteTypeOrGetter(
52+
doc,
53+
clipboardData,
54+
editor.getEnvironment(),
55+
htmlFromClipboard.metadata
56+
)
57+
: pasteTypeOrGetter;
58+
5359
// 3. Create target fragment
5460
const sourceFragment = createPasteFragment(
5561
editor.getDocument(),

packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -738,11 +738,11 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
738738
const newSelection = this.editor.getDOMSelection();
739739
const domHelper = this.editor.getDOMHelper();
740740

741-
//If am image selection changed to a wider range due a keyboard event, we should update the selection
741+
// If an image selection changed to a wider range due to a keyboard event, we should update the selection
742742
const range = domHelper.getSelectionRange();
743743
if (range) {
744744
const image = isSingleImageInSelection(range);
745-
if (newSelection?.type == 'image' && !image) {
745+
if (newSelection?.type == 'image' && !image && !range.collapsed) {
746746
const sel = this.editor.getDocument().defaultView?.getSelection();
747747
const isReverted = sel
748748
? sel.focusNode != range.endContainer || sel.focusOffset != range.endOffset

packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,30 @@ describe('Paste ', () => {
130130
'normal'
131131
);
132132
});
133+
134+
it('Execute | Callback receives all parameters defined', () => {
135+
let receivedArgs: any[] | undefined;
136+
137+
const cb: PasteTypeOrGetter = (...args) => {
138+
receivedArgs = args;
139+
return 'normal';
140+
};
141+
paste(editor, clipboardData, cb);
142+
143+
expect(receivedArgs).toBeDefined();
144+
expect(receivedArgs!.length).toBe(4);
145+
146+
const [doc, cbClipboardData, environment, htmlAttributes] = receivedArgs!;
147+
148+
expect(doc).toBeDefined();
149+
expect(doc).not.toBeNull();
150+
expect(cbClipboardData).toBeDefined();
151+
expect(cbClipboardData).toBe(clipboardData);
152+
expect(environment).toBeDefined();
153+
expect(environment).not.toBeNull();
154+
expect(htmlAttributes).toBeDefined();
155+
expect(htmlAttributes).not.toBeNull();
156+
});
133157
});
134158

135159
describe('paste with content model & paste plugin', () => {

packages/roosterjs-content-model-core/test/command/paste/retrieveHtmlInfoTest.ts

Lines changed: 54 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -200,35 +200,59 @@ describe('retrieveHtmlInfo', () => {
200200
});
201201

202202
itChromeOnly('Has global CSS rule', () => {
203-
runTest(
204-
'<style>.a {color:red} .b div, .c {font-size: 10pt}</style><div>test</div><style>test {border: none}</style>',
205-
{
206-
htmlBefore: '',
207-
htmlAfter: '',
208-
globalCssRules: [
209-
{
210-
selectors: ['.a'],
211-
text: 'color: red;',
212-
},
213-
{
214-
selectors: ['.b div', '.c'],
215-
text: 'font-size: 10pt;',
216-
},
217-
{
218-
selectors: ['test'],
219-
text:
220-
'border-width: medium; border-style: none; border-color: currentcolor; border-image: initial;',
221-
},
222-
],
223-
metadata: {},
224-
containsBlockElements: true,
225-
},
226-
{
227-
htmlFirstLevelChildTags: ['DIV'],
228-
html:
229-
'<style>.a {color:red} .b div, .c {font-size: 10pt}</style><div>test</div><style>test {border: none}</style>',
230-
},
231-
'<div>test</div>'
232-
);
203+
const rawHtml =
204+
'<style>.a {color:red} .b div, .c {font-size: 10pt}</style><div>test</div><style>test {border: none}</style>';
205+
const doc = new DOMParser().parseFromString(rawHtml, 'text/html');
206+
const clipboardData: Partial<ClipboardData> = {
207+
rawHtml,
208+
};
209+
210+
const result = retrieveHtmlInfo(doc, clipboardData);
211+
212+
expect(result.metadata).toEqual({});
213+
expect(result.containsBlockElements).toBe(true);
214+
expect(result.htmlBefore).toBe('');
215+
expect(result.htmlAfter).toBe('');
216+
expect(clipboardData).toEqual({
217+
rawHtml,
218+
htmlFirstLevelChildTags: ['DIV'],
219+
html: rawHtml,
220+
});
221+
expect(doc.body.innerHTML).toBe('<div>test</div>');
222+
223+
const rules = result.globalCssRules;
224+
225+
expect(rules.length).toBe(3);
226+
expect(rules[0]).toEqual({ selectors: ['.a'], text: 'color: red;' });
227+
expect(rules[1]).toEqual({ selectors: ['.b div', '.c'], text: 'font-size: 10pt;' });
228+
expect(rules[2].selectors).toEqual(['test']);
229+
230+
// The border shorthand is serialized style-by-style, and different Chrome
231+
// versions serialize the border-image longhand as either 'initial' or 'none'.
232+
const declarations = rules[2].text
233+
.split(';')
234+
.map(declaration => declaration.trim())
235+
.filter(declaration => !!declaration);
236+
const expectedDeclarations: Record<string, string | string[]> = {
237+
'border-width': 'medium',
238+
'border-style': 'none',
239+
'border-color': 'currentcolor',
240+
'border-image': ['initial', 'none'],
241+
};
242+
243+
expect(declarations.length).toBe(Object.keys(expectedDeclarations).length);
244+
245+
declarations.forEach(declaration => {
246+
const separatorIndex = declaration.indexOf(':');
247+
const name = declaration.substring(0, separatorIndex).trim();
248+
const value = declaration.substring(separatorIndex + 1).trim();
249+
const expected = expectedDeclarations[name];
250+
251+
if (Array.isArray(expected)) {
252+
expect(expected).toContain(value);
253+
} else {
254+
expect(value).toBe(expected);
255+
}
256+
});
233257
});
234258
});

packages/roosterjs-content-model-core/test/corePlugin/selection/SelectionPluginTest.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3698,6 +3698,38 @@ describe('SelectionPlugin selectionChange on image selected', () => {
36983698
});
36993699
expect(getDOMSelectionSpy).toHaveBeenCalledTimes(1);
37003700
});
3701+
3702+
it('onSelectionChange on image | 5, keeps image selection when range is collapsed', () => {
3703+
spyOn(isSingleImageInSelection, 'isSingleImageInSelection').and.returnValue(null);
3704+
3705+
mockedRange = { startContainer: {}, collapsed: true } as any;
3706+
3707+
const plugin = createSelectionPlugin({});
3708+
const state = plugin.getState();
3709+
const mockedOldSelection = {
3710+
type: 'image',
3711+
image: {} as any,
3712+
} as DOMSelection;
3713+
3714+
state.selection = mockedOldSelection;
3715+
3716+
plugin.initialize(editor);
3717+
3718+
const onSelectionChange = addEventListenerSpy.calls.argsFor(0)[1] as Function;
3719+
const mockedNewSelection = {
3720+
type: 'image',
3721+
image: {} as any,
3722+
} as any;
3723+
3724+
hasFocusSpy.and.returnValue(true);
3725+
isInShadowEditSpy.and.returnValue(false);
3726+
getDOMSelectionSpy.and.returnValue(mockedNewSelection);
3727+
3728+
onSelectionChange();
3729+
3730+
expect(setDOMSelectionSpy).not.toHaveBeenCalled();
3731+
expect(getDOMSelectionSpy).toHaveBeenCalledTimes(1);
3732+
});
37013733
});
37023734

37033735
describe('SelectionPlugin handle logical root change', () => {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ export {
414414
ContentModelFormatter,
415415
} from './parameter/FormatContentModelOptions';
416416
export { ContentModelFormatState } from './parameter/ContentModelFormatState';
417-
export { PasteTypeOrGetter } from './parameter/PasteTypeOrGetter';
417+
export { PasteTypeOrGetter, PasteTypeGetter } from './parameter/PasteTypeOrGetter';
418418
export { ImageFormatState } from './parameter/ImageFormatState';
419419
export { Border } from './parameter/Border';
420420
export { InsertEntityOptions } from './parameter/InsertEntityOptions';
Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
11
import type { ClipboardData } from './ClipboardData';
2+
import type { EditorEnvironment } from './EditorEnvironment';
23
import type { PasteType } from '../enum/PasteType';
34

5+
/**
6+
* A function that returns the Paste Type to use based on the pasted content and clipboard data.
7+
* @param document The document parsed from the clipboard raw HTML, or null when there is no HTML
8+
* @param clipboardData Clipboard data retrieved from clipboard
9+
* @param environment The editor environment information
10+
* @param htmlAttributes The metadata (HTML attributes) retrieved from the pasted content
11+
* @returns The Paste Type to use
12+
*/
13+
export type PasteTypeGetter = (
14+
document: Document | null,
15+
clipboardData: ClipboardData,
16+
environment: EditorEnvironment,
17+
htmlAttributes: Record<string, string>
18+
) => PasteType;
19+
420
/**
521
* Represents the PasteType parameter used to set the paste type to use.
6-
* It can be either the Paste Type value or a callback that retuns the Paste Type to use.
22+
* It can be either the Paste Type value or a callback that returns the Paste Type to use.
723
*/
8-
export type PasteTypeOrGetter =
9-
| PasteType
10-
| ((document: Document | null, clipboardData: ClipboardData) => PasteType);
24+
export type PasteTypeOrGetter = PasteType | PasteTypeGetter;

0 commit comments

Comments
 (0)