Skip to content

Commit 4713242

Browse files
Merge pull request #3068 from flyingbee2012/u/biwu/versionbump66
Version bump 9.29.3
2 parents 2c6b633 + 5b7623f commit 4713242

File tree

13 files changed

+1138
-65
lines changed

13 files changed

+1138
-65
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ export function createPasteFragment(
1414
pasteType: PasteType,
1515
root: HTMLElement | undefined
1616
): DocumentFragment {
17+
if (!clipboardData.text && pasteType === 'asPlainText' && root) {
18+
clipboardData.text = root.textContent || clipboardData.text;
19+
}
20+
1721
const { imageDataUri, text } = clipboardData;
1822
const fragment = document.createDocumentFragment();
1923

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
4444
private editor: IEditor | null = null;
4545
private state: SelectionPluginState;
4646
private disposer: (() => void) | null = null;
47+
private logicalRootDisposer: (() => void) | null = null;
4748
private isSafari = false;
4849
private isMac = false;
4950
private scrollTopCache: number = 0;
@@ -120,6 +121,9 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
120121
this.disposer = null;
121122
}
122123

124+
this.logicalRootDisposer?.();
125+
this.logicalRootDisposer = null;
126+
123127
this.detachMouseEvent();
124128
this.editor = null;
125129
}
@@ -155,6 +159,22 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
155159
this.scrollTopCache = event.scrollContainer.scrollTop;
156160
}
157161
break;
162+
163+
case 'logicalRootChanged':
164+
this.logicalRootDisposer?.();
165+
if (this.isSafari) {
166+
this.logicalRootDisposer = this.editor.attachDomEvent({
167+
focus: { beforeDispatch: this.onFocus },
168+
drop: { beforeDispatch: this.onDrop },
169+
});
170+
} else {
171+
this.logicalRootDisposer = this.editor.attachDomEvent({
172+
focus: { beforeDispatch: this.onFocus },
173+
blur: { beforeDispatch: this.onBlur },
174+
drop: { beforeDispatch: this.onDrop },
175+
});
176+
}
177+
break;
158178
}
159179
}
160180

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ describe('createPasteFragment', () => {
126126
imageDataUri: 'test',
127127
},
128128
'asPlainText',
129-
'',
129+
'HTML',
130130
false
131131
);
132132
});

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

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3133,3 +3133,146 @@ describe('SelectionPlugin selectionChange on image selected', () => {
31333133
expect(getDOMSelectionSpy).toHaveBeenCalledTimes(1);
31343134
});
31353135
});
3136+
3137+
describe('SelectionPlugin handle logical root change', () => {
3138+
let plugin: PluginWithState<SelectionPluginState>;
3139+
let disposer: jasmine.Spy;
3140+
let logicalRootDisposer: jasmine.Spy;
3141+
let attachDomEvent: jasmine.Spy;
3142+
let removeEventListenerSpy: jasmine.Spy;
3143+
let addEventListenerSpy: jasmine.Spy;
3144+
let getDocumentSpy: jasmine.Spy;
3145+
let editor: IEditor;
3146+
3147+
beforeEach(() => {
3148+
plugin = createSelectionPlugin({});
3149+
disposer = jasmine.createSpy('disposer');
3150+
logicalRootDisposer = jasmine.createSpy('logicalRootDisposer');
3151+
attachDomEvent = jasmine.createSpy('attachDomEvent').and.returnValue(disposer);
3152+
removeEventListenerSpy = jasmine.createSpy('removeEventListener');
3153+
addEventListenerSpy = jasmine.createSpy('addEventListener');
3154+
getDocumentSpy = jasmine.createSpy('getDocument').and.returnValue({
3155+
removeEventListener: removeEventListenerSpy,
3156+
addEventListener: addEventListenerSpy,
3157+
});
3158+
});
3159+
3160+
it('handles logical root change - non Safari', () => {
3161+
// Setup
3162+
editor = ({
3163+
getDocument: getDocumentSpy,
3164+
attachDomEvent,
3165+
getEnvironment: () => ({
3166+
isSafari: false,
3167+
}),
3168+
getColorManager: () => ({
3169+
getDarkColor: (color: string) => `${DEFAULT_DARK_COLOR_SUFFIX_COLOR}${color}`,
3170+
}),
3171+
} as any) as IEditor;
3172+
3173+
plugin.initialize(editor);
3174+
3175+
// Reset the spy calls before the logicalRootChanged event
3176+
attachDomEvent.calls.reset();
3177+
attachDomEvent.and.returnValue(logicalRootDisposer);
3178+
3179+
// Trigger logicalRootChanged event
3180+
plugin.onPluginEvent?.({
3181+
eventType: 'logicalRootChanged',
3182+
logicalRoot: document.createElement('div'), // Mock logical root element
3183+
});
3184+
3185+
// Verify that attachDomEvent was called for logical root with correct events
3186+
expect(attachDomEvent).toHaveBeenCalledTimes(1);
3187+
expect(attachDomEvent).toHaveBeenCalledWith({
3188+
focus: { beforeDispatch: jasmine.any(Function) },
3189+
blur: { beforeDispatch: jasmine.any(Function) },
3190+
drop: { beforeDispatch: jasmine.any(Function) },
3191+
});
3192+
3193+
// Dispose should clean up all event listeners including logicalRoot disposers
3194+
plugin.dispose();
3195+
expect(logicalRootDisposer).toHaveBeenCalled();
3196+
expect(disposer).toHaveBeenCalled();
3197+
expect(removeEventListenerSpy).toHaveBeenCalled();
3198+
});
3199+
3200+
it('handles logical root change - Safari', () => {
3201+
// Setup
3202+
editor = ({
3203+
getDocument: getDocumentSpy,
3204+
attachDomEvent,
3205+
getEnvironment: () => ({
3206+
isSafari: true,
3207+
}),
3208+
getColorManager: () => ({
3209+
getDarkColor: (color: string) => `${DEFAULT_DARK_COLOR_SUFFIX_COLOR}${color}`,
3210+
}),
3211+
} as any) as IEditor;
3212+
3213+
plugin.initialize(editor);
3214+
3215+
// Reset the spy calls before the logicalRootChanged event
3216+
attachDomEvent.calls.reset();
3217+
attachDomEvent.and.returnValue(logicalRootDisposer);
3218+
3219+
// Trigger logicalRootChanged event
3220+
plugin.onPluginEvent?.({
3221+
eventType: 'logicalRootChanged',
3222+
logicalRoot: document.createElement('div'), // Mock logical root element
3223+
});
3224+
3225+
// Verify that attachDomEvent was called for logical root with correct events
3226+
expect(attachDomEvent).toHaveBeenCalledTimes(1);
3227+
expect(attachDomEvent).toHaveBeenCalledWith({
3228+
focus: { beforeDispatch: jasmine.any(Function) },
3229+
drop: { beforeDispatch: jasmine.any(Function) },
3230+
});
3231+
3232+
// Dispose should clean up all event listeners including logicalRoot disposers
3233+
plugin.dispose();
3234+
expect(logicalRootDisposer).toHaveBeenCalled();
3235+
expect(disposer).toHaveBeenCalled();
3236+
expect(removeEventListenerSpy).toHaveBeenCalled();
3237+
});
3238+
3239+
it('calls previous logical root disposer when logicalRootChanged is triggered again', () => {
3240+
// Setup
3241+
editor = ({
3242+
getDocument: getDocumentSpy,
3243+
attachDomEvent,
3244+
getEnvironment: () => ({
3245+
isSafari: false,
3246+
}),
3247+
getColorManager: () => ({
3248+
getDarkColor: (color: string) => `${DEFAULT_DARK_COLOR_SUFFIX_COLOR}${color}`,
3249+
}),
3250+
} as any) as IEditor;
3251+
3252+
plugin.initialize(editor);
3253+
3254+
// First logical root change
3255+
attachDomEvent.calls.reset();
3256+
attachDomEvent.and.returnValue(logicalRootDisposer);
3257+
plugin.onPluginEvent?.({
3258+
eventType: 'logicalRootChanged',
3259+
logicalRoot: document.createElement('div'), // Mock logical root element
3260+
});
3261+
3262+
// Second logical root change
3263+
const newLogicalRootDisposer = jasmine.createSpy('newLogicalRootDisposer');
3264+
attachDomEvent.and.returnValue(newLogicalRootDisposer);
3265+
plugin.onPluginEvent?.({
3266+
eventType: 'logicalRootChanged',
3267+
logicalRoot: document.createElement('div'), // Mock logical root element
3268+
});
3269+
3270+
// Verify that the old logical root disposer was called
3271+
expect(logicalRootDisposer).toHaveBeenCalled();
3272+
3273+
// Dispose should clean up all event listeners including the new logicalRoot disposer
3274+
plugin.dispose();
3275+
expect(newLogicalRootDisposer).toHaveBeenCalled();
3276+
expect(disposer).toHaveBeenCalled();
3277+
});
3278+
});

packages/roosterjs-content-model-plugins/lib/paste/PowerPoint/processPastedContentFromPowerPoint.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,7 @@ export function processPastedContentFromPowerPoint(
8787
};
8888

8989
// Process the Div element as a list item.
90-
processAsListItem(context, element, group, listMetadata, listItem => {
91-
const currentMarkerSize = listItem.formatHolder.format.fontSize;
92-
const bulletElementSize = bulletElement.parentElement?.style.fontSize;
93-
listItem.formatHolder.format.fontSize = bulletElementSize || currentMarkerSize;
90+
processAsListItem(context, element, group, listMetadata, bulletElement, listItem => {
9491
if (isNewList) {
9592
listItem.levels[
9693
listItem.levels.length - 1

packages/roosterjs-content-model-plugins/lib/paste/WordDesktop/processWordLists.ts

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { getStyles } from '../utils/getStyles';
12
import { processAsListItem, setupListFormat } from '../utils/customListUtils';
23
import {
34
getListStyleTypeFromString,
@@ -21,6 +22,7 @@ const MSO_LIST = 'mso-list';
2122
const MSO_LIST_IGNORE = 'ignore';
2223
const WORD_FIRST_LIST = 'l0';
2324
const TEMPLATE_VALUE_REGEX = /%[0-9a-zA-Z]+/g;
25+
const BULLET_METADATA = 'bullet';
2426

2527
interface WordDesktopListFormat extends DomToModelListFormat {
2628
wordLevel?: number | '';
@@ -32,7 +34,6 @@ interface WordListFormat extends ContentModelListItemFormat {
3234
wordList?: string;
3335
}
3436

35-
const BULLET_METADATA = 'bullet';
3637
/**
3738
* @internal
3839
* @param styles
@@ -95,11 +96,18 @@ export function processWordList(
9596
}
9697
: undefined;
9798

98-
processAsListItem(context, element, group, listFormatMetadata, listItem => {
99-
if (listType == 'OL') {
100-
setStartNumber(listItem, context, listMetadata, element);
99+
processAsListItem(
100+
context,
101+
element,
102+
group,
103+
listFormatMetadata,
104+
getBulletElement(element),
105+
listItem => {
106+
if (listType == 'OL') {
107+
setStartNumber(listItem, context, listMetadata, element);
108+
}
101109
}
102-
});
110+
);
103111

104112
if (
105113
listFormat.levels.length > 0 &&
@@ -226,3 +234,45 @@ function wordListPaddingParser(
226234
format.paddingRight = '0px';
227235
}
228236
}
237+
/**
238+
* Get the bullet element from word list item.
239+
* The first element of the list contains the bullet element, which contains the mso-list:ignore style.
240+
* @example
241+
* <p class=MsoListParagraphCxSpFirst style='text-indent:-18.0pt;mso-list:l0 level1 lfo1'>
242+
* <![if !supportLists]>
243+
* <span lang=EN-US style='mso-fareast-font-family:Aptos;mso-fareast-theme-font:minor-latin; mso-bidi-font-family:Aptos;mso-bidi-theme-font:minor-latin;color:#C00000; mso-ansi-language:EN-US'>
244+
* <span style='mso-list:Ignore'> <-- This is the bullet element
245+
* 1.
246+
* <span style='font:7.0pt "Times New Roman"'>
247+
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
248+
* </span>
249+
* </span>
250+
* </span>
251+
* <![endif]>
252+
* <span lang=EN-US style='color:#C00000; mso-ansi-language:EN-US'>
253+
* Content in list<o:p></o:p>
254+
* </span>
255+
* </p>
256+
* @returns
257+
*/
258+
function getBulletElement(element: HTMLElement): HTMLElement | undefined {
259+
const firstChild = element.firstElementChild;
260+
let isBulletElement = false;
261+
262+
if (firstChild) {
263+
for (let i = 0; i < firstChild.childNodes.length; i++) {
264+
const child = firstChild.childNodes[i];
265+
if (isNodeOfType(child, 'ELEMENT_NODE')) {
266+
const styles = getStyles(child);
267+
const wordListStyle = styles[MSO_LIST] || '';
268+
269+
if (wordListStyle.toLowerCase() === MSO_LIST_IGNORE) {
270+
isBulletElement = true;
271+
break;
272+
}
273+
}
274+
}
275+
}
276+
277+
return firstChild && isBulletElement ? (firstChild as HTMLElement) : undefined;
278+
}

packages/roosterjs-content-model-plugins/lib/paste/utils/customListUtils.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {
1717
ContentModelListItemLevelFormat,
1818
ContentModelListItemFormat,
1919
FormatParser,
20+
ContentModelSegmentFormat,
2021
} from 'roosterjs-content-model-types';
2122

2223
const removeMargin = (format: ContentModelListItemFormat): void => {
@@ -61,13 +62,13 @@ export function processAsListItem(
6162
element: HTMLElement,
6263
group: ContentModelBlockGroup,
6364
listFormatMetadata: ListMetadataFormat | undefined,
65+
bulletElement: HTMLElement | undefined,
6466
beforeProcessingChildren?: (listItem: ContentModelListItem) => void
6567
) {
6668
const listFormat = context.listFormat;
67-
if (listFormatMetadata) {
68-
updateListMetadata(listFormat.levels[listFormat.levels.length - 1], metadata =>
69-
Object.assign({}, metadata, listFormatMetadata)
70-
);
69+
const lastLevel = listFormat.levels[listFormat.levels.length - 1];
70+
if (listFormatMetadata && lastLevel) {
71+
updateListMetadata(lastLevel, metadata => Object.assign({}, metadata, listFormatMetadata));
7172
}
7273

7374
const listItem = createListItem(listFormat.levels, context.segmentFormat);
@@ -80,6 +81,11 @@ export function processAsListItem(
8081
listItem.format,
8182
context
8283
);
84+
if (bulletElement) {
85+
const format: ContentModelSegmentFormat = { ...context.segmentFormat };
86+
parseFormat(bulletElement, context.formatParsers.segmentOnBlock, format, context);
87+
listItem.formatHolder.format = format;
88+
}
8389

8490
beforeProcessingChildren?.(listItem);
8591

0 commit comments

Comments
 (0)