Skip to content

Commit 0a98499

Browse files
committed
Merge branch 'master' into u/ianeli/bump-2-26-2026
2 parents 523941a + 1d54415 commit 0a98499

File tree

21 files changed

+1163
-41
lines changed

21 files changed

+1163
-41
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/lib/corePlugin/selection/SelectionPlugin.ts

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { findTableCellElement } from '../../coreApi/setDOMSelection/findTableCel
33
import { isSingleImageInSelection } from './isSingleImageInSelection';
44
import { normalizePos } from './normalizePos';
55
import {
6+
getDOMInsertPointRect,
7+
getNodePositionFromEvent,
68
isCharacterValue,
79
isElementOfType,
810
isModifierKey,
@@ -364,7 +366,9 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
364366
this.handleSelectionInTable(this.getTabKey(rawEvent));
365367
rawEvent.preventDefault();
366368
} else {
367-
win?.requestAnimationFrame(() => this.handleSelectionInTable(key));
369+
win?.requestAnimationFrame(() =>
370+
this.handleSelectionInTable(key, selection.range)
371+
);
368372
}
369373
}
370374
}
@@ -423,7 +427,8 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
423427
}
424428

425429
private handleSelectionInTable(
426-
key: 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight' | 'TabLeft' | 'TabRight'
430+
key: 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight' | 'TabLeft' | 'TabRight',
431+
rangeBeforeChange?: Range
427432
) {
428433
if (!this.editor || !this.state.tableSelection) {
429434
return;
@@ -468,11 +473,30 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
468473
}
469474

470475
if (collapsed && td) {
471-
this.setRangeSelectionInTable(
472-
td,
473-
key == Up ? td.childNodes.length : 0,
474-
this.editor
475-
);
476+
const textOffset =
477+
(key == 'ArrowUp' || key == 'ArrowDown') && rangeBeforeChange
478+
? this.getTextOffset(
479+
this.editor,
480+
rangeBeforeChange,
481+
td,
482+
key == 'ArrowUp'
483+
)
484+
: null;
485+
if (textOffset) {
486+
this.setRangeSelectionInTable(
487+
textOffset.node,
488+
textOffset.offset,
489+
this.editor,
490+
false /* selectAll */
491+
);
492+
} else {
493+
this.setRangeSelectionInTable(
494+
td,
495+
0,
496+
this.editor,
497+
false /* selectAll */
498+
);
499+
}
476500
} else if (!td && (lastCo.row == -1 || lastCo.row <= parsedTable.length)) {
477501
this.selectBeforeOrAfterElement(
478502
this.editor,
@@ -545,13 +569,35 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
545569
}
546570
}
547571

572+
private getTextOffset(editor: IEditor, range: Range, td: HTMLElement, isKeyUp: boolean) {
573+
const doc = editor.getDocument();
574+
const cursorRect = range
575+
? getDOMInsertPointRect(doc, {
576+
node: range.startContainer,
577+
offset: range.startOffset,
578+
})
579+
: undefined;
580+
const rect = td?.getBoundingClientRect();
581+
const textOffset =
582+
cursorRect && rect
583+
? getNodePositionFromEvent(
584+
doc,
585+
editor.getDOMHelper(),
586+
cursorRect.left,
587+
isKeyUp ? rect.top - 1 : rect.top + 1
588+
)
589+
: null;
590+
return textOffset;
591+
}
592+
548593
private setRangeSelectionInTable(
549594
cell: Node,
550595
nodeOffset: number,
551596
editor: IEditor,
552597
selectAll?: boolean
553598
) {
554-
const range = editor.getDocument().createRange();
599+
const doc = editor.getDocument();
600+
const range = doc.createRange();
555601
if (selectAll && cell.firstChild && cell.lastChild) {
556602
const cellStart = cell.firstChild;
557603
const cellEnd = cell.lastChild;
@@ -569,7 +615,6 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
569615
} else {
570616
// Get deepest editable position in the cell
571617
const { node, offset } = normalizePos(cell, nodeOffset);
572-
573618
range.setStart(node, offset);
574619
range.collapse(true /* toStart */);
575620
}

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
});

0 commit comments

Comments
 (0)