Skip to content

Commit d124342

Browse files
Merge branch 'master' into u/bvalverde/fixTrailingSpaceRemoved
2 parents e1c1419 + abb61fb commit d124342

File tree

7 files changed

+157
-4
lines changed

7 files changed

+157
-4
lines changed

packages/roosterjs-content-model-core/lib/corePlugin/cache/textMutationObserver.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class TextMutationObserverImpl implements TextMutationObserver {
5959
continue;
6060
} else if (!includedNodes.has(target)) {
6161
if (
62+
!this.domHelper.isNodeInEditor(target) ||
6263
findClosestEntityWrapper(target, this.domHelper) ||
6364
findClosestBlockEntityContainer(target, this.domHelper)
6465
) {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ describe('paste with content model & paste plugin', () => {
168168
paste(editor!, clipboardData);
169169

170170
expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(2);
171-
expect(addParserF.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 6);
171+
expect(addParserF.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 7);
172172
expect(WordDesktopFile.processPastedContentFromWordDesktop).toHaveBeenCalledTimes(1);
173173
});
174174

@@ -224,7 +224,7 @@ describe('paste with content model & paste plugin', () => {
224224
paste(editor!, clipboardData, 'asPlainText');
225225

226226
expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(2);
227-
expect(addParserF.addParser).toHaveBeenCalledTimes(11);
227+
expect(addParserF.addParser).toHaveBeenCalledTimes(12);
228228
expect(WordDesktopFile.processPastedContentFromWordDesktop).toHaveBeenCalledTimes(1);
229229
});
230230

packages/roosterjs-content-model-core/test/corePlugin/cache/textMutationObserverTest.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,4 +384,55 @@ describe('TextMutationObserverImpl', () => {
384384
expect(onMutation).toHaveBeenCalledTimes(1);
385385
expect(onMutation).toHaveBeenCalledWith({ type: 'unknown' });
386386
});
387+
388+
it('Ignore changes that is not in editor - add node', () => {
389+
const div = document.createElement('div');
390+
const span1 = document.createElement('span');
391+
const span2 = document.createElement('span');
392+
393+
div.appendChild(span1);
394+
395+
const onMutation = jasmine.createSpy('onMutation');
396+
397+
observer = textMutationObserver.createTextMutationObserver(div, onMutation);
398+
observer.startObserving();
399+
400+
span1.textContent = 'test1';
401+
span2.textContent = 'test2';
402+
403+
observer.flushMutations();
404+
405+
expect(onMutation).toHaveBeenCalledTimes(1);
406+
expect(onMutation).toHaveBeenCalledWith({
407+
type: 'childList',
408+
addedNodes: [span1.firstChild],
409+
removedNodes: [],
410+
});
411+
});
412+
413+
it('Ignore changes that is not in editor - remove node', () => {
414+
const div = document.createElement('div');
415+
const span1 = document.createElement('span');
416+
const span2 = document.createElement('span');
417+
418+
span1.appendChild(span2);
419+
div.appendChild(span1);
420+
421+
const onMutation = jasmine.createSpy('onMutation');
422+
423+
observer = textMutationObserver.createTextMutationObserver(div, onMutation);
424+
observer.startObserving();
425+
426+
div.removeChild(span1);
427+
span1.removeChild(span2);
428+
429+
observer.flushMutations();
430+
431+
expect(onMutation).toHaveBeenCalledTimes(1);
432+
expect(onMutation).toHaveBeenCalledWith({
433+
type: 'childList',
434+
addedNodes: [],
435+
removedNodes: [span1],
436+
});
437+
});
387438
});

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { getStyles } from '../utils/getStyles';
55
import { listLevelParser } from '../parsers/listLevelParser';
66
import { processWordComments } from './processWordComments';
77
import { processWordList } from './processWordLists';
8+
import { adjustWordListMarginParser } from '../parsers/adjustWordListMarginParser';
89
import { removeNegativeTextIndentParser } from '../parsers/removeNegativeTextIndentParser';
910
import { setProcessor } from '../utils/setProcessor';
1011
import { wordContainerParser } from '../parsers/wordContainerParser';
@@ -28,6 +29,7 @@ export function processPastedContentFromWordDesktop(
2829
addParser(domToModelOption, 'block', adjustPercentileLineHeight);
2930
addParser(domToModelOption, 'block', removeNegativeTextIndentParser);
3031
addParser(domToModelOption, 'listItemElement', removeNegativeTextIndentParser);
32+
addParser(domToModelOption, 'listItemElement', adjustWordListMarginParser);
3133
addParser(domToModelOption, 'listLevel', listLevelParser);
3234
addParser(domToModelOption, 'container', wordContainerParser);
3335
addParser(domToModelOption, 'table', wordTableParser);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { parseValueWithUnit } from 'roosterjs-content-model-dom';
2+
import type { FormatParser, MarginFormat } from 'roosterjs-content-model-types';
3+
4+
const MSO_LIST_PARAGRAPH_CLASS = 'MsoListParagraph';
5+
6+
// Default list padding from the HTML user-agent stylesheet (paddingInlineStart for <ul>/<ol>)
7+
const DEFAULT_LIST_PADDING_INLINE_START = '40px';
8+
9+
/**
10+
* @internal
11+
* Parser that subtracts the default list format (paddingInlineStart: 40px) from
12+
* the marginLeft of list item elements that have the MsoListParagraph class,
13+
* since Word adds the full indentation as margin on the paragraph, which
14+
* duplicates the padding the list element already provides.
15+
*/
16+
export const adjustWordListMarginParser: FormatParser<MarginFormat> = (
17+
format: MarginFormat,
18+
element: HTMLElement
19+
): void => {
20+
if (element.classList.contains(MSO_LIST_PARAGRAPH_CLASS) && format.marginLeft) {
21+
const currentPx = parseValueWithUnit(format.marginLeft, element);
22+
const defaultPx = parseValueWithUnit(DEFAULT_LIST_PADDING_INLINE_START);
23+
const result = currentPx - defaultPx;
24+
25+
if (result > 0) {
26+
format.marginLeft = `${result}px`;
27+
}
28+
}
29+
};

packages/roosterjs-content-model-plugins/test/paste/plugin/ContentModelPastePluginTest.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ describe('Content Model Paste Plugin Test', () => {
5858
plugin.initialize(editor);
5959
plugin.onPluginEvent(event);
6060

61-
expect(addParser.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 6);
61+
expect(addParser.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 7);
6262
expect(setProcessor.setProcessor).toHaveBeenCalledTimes(2);
6363
});
6464

packages/roosterjs-content-model-plugins/test/paste/word/processPastedContentFromWordDesktopTest.ts

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1252,7 +1252,7 @@ describe('processPastedContentFromWordDesktopTest', () => {
12521252
],
12531253
blockType: 'BlockGroup',
12541254
format: {
1255-
marginLeft: '1.5in',
1255+
marginLeft: '104px',
12561256
},
12571257
blockGroupType: 'ListItem',
12581258
blocks: [
@@ -1447,6 +1447,76 @@ describe('processPastedContentFromWordDesktopTest', () => {
14471447
);
14481448
});
14491449

1450+
it('adjustWordListMarginParser subtracts default list padding from MsoListParagraph margin', () => {
1451+
// margin-left: 1in = 96px; parser subtracts 40px (default list paddingInlineStart) → 56px
1452+
const source =
1453+
'<p style="margin:0in 0in 0in 1in;font-size:12pt;font-family:Calibri, sans-serif;text-indent:-.25in;mso-list:l0 level1 lfo1" class="MsoListParagraph"><span style="font-family:Symbol;mso-fareast-font-family:Symbol;mso-bidi-font-family:Symbol"><span style="mso-list:Ignore">·<span style="font:7.0pt &quot;Times New Roman&quot;">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\n</span></span></span>TEST</p>';
1454+
spyOn(getStyleMetadata, 'getStyleMetadata').and.returnValue(
1455+
new Map<string, WordMetadata>().set('l0:level1', {
1456+
'mso-level-number-format': 'bullet',
1457+
'mso-level-start-at': '1',
1458+
})
1459+
);
1460+
1461+
runTest(
1462+
source,
1463+
{
1464+
blockGroupType: 'Document',
1465+
blocks: [
1466+
{
1467+
blockType: 'BlockGroup',
1468+
blockGroupType: 'ListItem',
1469+
blocks: [
1470+
{
1471+
blockType: 'Paragraph',
1472+
segments: [
1473+
{
1474+
segmentType: 'Text',
1475+
text: 'TEST',
1476+
format: {
1477+
fontFamily: 'Calibri, sans-serif',
1478+
fontSize: '12pt',
1479+
},
1480+
},
1481+
],
1482+
format: {},
1483+
isImplicit: true,
1484+
segmentFormat: {
1485+
fontFamily: 'Calibri, sans-serif',
1486+
fontSize: '12pt',
1487+
},
1488+
},
1489+
],
1490+
levels: [
1491+
{
1492+
listType: 'UL',
1493+
format: {
1494+
marginTop: '0in',
1495+
marginRight: '0in',
1496+
paddingLeft: '0px',
1497+
wordList: 'l0',
1498+
},
1499+
dataset: {},
1500+
},
1501+
],
1502+
formatHolder: {
1503+
segmentType: 'SelectionMarker',
1504+
isSelected: false,
1505+
format: { fontFamily: 'Symbol', fontSize: '12pt' },
1506+
},
1507+
format: {
1508+
marginTop: '0in',
1509+
marginRight: '0in',
1510+
marginBottom: '0in',
1511+
marginLeft: '56px',
1512+
},
1513+
},
1514+
],
1515+
},
1516+
true /* removeUndefinedValues */
1517+
);
1518+
});
1519+
14501520
/**
14511521
* Test
14521522
* 1. Test

0 commit comments

Comments
 (0)