Skip to content

Commit

Permalink
Merge pull request #2929 from microsoft/u/nguyenvi/versionbump012425
Browse files Browse the repository at this point in the history
Version bump to main 9.18.0 and legacyAdapter 8.63.1
  • Loading branch information
vinguyen12 authored Jan 24, 2025
2 parents c4bd4b4 + 6816792 commit e3f54c1
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 40 deletions.
21 changes: 17 additions & 4 deletions packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ export type EditOptions = {
* Whether to handle Tab key in keyboard. @default true
*/
handleTabKey?: boolean;

/**
* Whether expanded selection within a text node should be handled by CM when pressing Backspace/Delete key.
* @default true
*/
handleExpandedSelectionOnDelete?: boolean;
};

const BACKSPACE_KEY = 8;
Expand All @@ -33,6 +39,7 @@ const DEAD_KEY = 229;

const DefaultOptions: Partial<EditOptions> = {
handleTabKey: true,
handleExpandedSelectionOnDelete: true,
};

/**
Expand Down Expand Up @@ -164,15 +171,19 @@ export class EditPlugin implements EditorPlugin {
case 'Backspace':
// Use our API to handle BACKSPACE/DELETE key.
// No need to clear cache here since if we rely on browser's behavior, there will be Input event and its handler will reconcile cache
keyboardDelete(editor, rawEvent);
keyboardDelete(editor, rawEvent, this.options.handleExpandedSelectionOnDelete);
break;

case 'Delete':
// Use our API to handle BACKSPACE/DELETE key.
// No need to clear cache here since if we rely on browser's behavior, there will be Input event and its handler will reconcile cache
// And leave it to browser when shift key is pressed so that browser will trigger cut event
if (!event.rawEvent.shiftKey) {
keyboardDelete(editor, rawEvent);
keyboardDelete(
editor,
rawEvent,
this.options.handleExpandedSelectionOnDelete
);
}
break;

Expand Down Expand Up @@ -225,7 +236,8 @@ export class EditPlugin implements EditorPlugin {
key: 'Backspace',
keyCode: BACKSPACE_KEY,
which: BACKSPACE_KEY,
})
}),
this.options.handleExpandedSelectionOnDelete
);
break;
case 'deleteContentForward':
Expand All @@ -235,7 +247,8 @@ export class EditPlugin implements EditorPlugin {
key: 'Delete',
keyCode: DELETE_KEY,
which: DELETE_KEY,
})
}),
this.options.handleExpandedSelectionOnDelete
);
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,18 @@ import type { DOMSelection, DeleteSelectionStep, IEditor } from 'roosterjs-conte
* Do keyboard event handling for DELETE/BACKSPACE key
* @param editor The editor object
* @param rawEvent DOM keyboard event
* @param handleExpandedSelection Whether to handle expanded selection within a text node by CM
* @returns True if the event is handled by content model, otherwise false
*/
export function keyboardDelete(editor: IEditor, rawEvent: KeyboardEvent) {
export function keyboardDelete(
editor: IEditor,
rawEvent: KeyboardEvent,
handleExpandedSelection: boolean = true
) {
let handled = false;
const selection = editor.getDOMSelection();

if (shouldDeleteWithContentModel(selection, rawEvent)) {
if (shouldDeleteWithContentModel(selection, rawEvent, handleExpandedSelection)) {
editor.formatContentModel(
(model, context) => {
const result = deleteSelection(
Expand Down Expand Up @@ -80,11 +85,29 @@ function getDeleteSteps(rawEvent: KeyboardEvent, isMac: boolean): (DeleteSelecti
];
}

function shouldDeleteWithContentModel(selection: DOMSelection | null, rawEvent: KeyboardEvent) {
function shouldDeleteWithContentModel(
selection: DOMSelection | null,
rawEvent: KeyboardEvent,
handleExpandedSelection: boolean
) {
if (!selection) {
return false; // Nothing to delete
} else if (selection.type != 'range' || !selection.range.collapsed) {
return true; // Selection is not collapsed, need to delete all selections
} else if (selection.type != 'range') {
return true;
} else if (!selection.range.collapsed) {
if (handleExpandedSelection) {
return true; // Selection is not collapsed, need to delete all selections
}

const range = selection.range;
const { startContainer, endContainer } = selection.range;
const isInSameTextNode =
startContainer === endContainer && isNodeOfType(startContainer, 'TEXT_NODE');
return !(
isInSameTextNode &&
!isModifierKey(rawEvent) &&
range.endOffset - range.startOffset < (startContainer.nodeValue?.length ?? 0)
);
} else {
const range = selection.range;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { addParser } from '../utils/addParser';
import { isNodeOfType, moveChildNodes } from 'roosterjs-content-model-dom';
import { setProcessor } from '../utils/setProcessor';
import type { BeforePasteEvent, DOMCreator, ElementProcessor } from 'roosterjs-content-model-types';
import type {
BeforePasteEvent,
ClipboardData,
DOMCreator,
ElementProcessor,
} from 'roosterjs-content-model-types';

const LAST_TD_END_REGEX = /<\/\s*td\s*>((?!<\/\s*tr\s*>)[\s\S])*$/i;
const LAST_TR_END_REGEX = /<\/\s*tr\s*>((?!<\/\s*table\s*>)[\s\S])*$/i;
const LAST_TR_REGEX = /<tr[^>]*>[^<]*/i;
const LAST_TABLE_REGEX = /<table[^>]*>[^<]*/i;
const DEFAULT_BORDER_STYLE = 'solid 1px #d4d4d4';
const TABLE_SELECTOR = 'table';

/**
* @internal
Expand All @@ -20,13 +26,9 @@ export function processPastedContentFromExcel(
domCreator: DOMCreator,
allowExcelNoBorderTable?: boolean
) {
const { fragment, htmlBefore, clipboardData } = event;
const html = clipboardData.html ? excelHandler(clipboardData.html, htmlBefore) : undefined;
const { fragment, htmlBefore, htmlAfter, clipboardData } = event;

if (html && clipboardData.html != html) {
const doc = domCreator.htmlToDOM(html);
moveChildNodes(fragment, doc?.body);
}
validateExcelFragment(fragment, domCreator, htmlBefore, clipboardData, htmlAfter);

// For Excel Online
const firstChild = fragment.firstChild;
Expand Down Expand Up @@ -86,22 +88,63 @@ export const childProcessor: ElementProcessor<ParentNode> = (group, element, con
}
};

/**
* @internal
* Exported only for unit test
*/
export function validateExcelFragment(
fragment: DocumentFragment,
domCreator: DOMCreator,
htmlBefore: string,
clipboardData: ClipboardData,
htmlAfter: string
) {
// Clipboard content of Excel may contain the <StartFragment> and EndFragment comment tags inside the table
//
// @example
// <table>
// <!--StartFragment-->
// <tr>...</tr>
// <!--EndFragment-->
// </table>
//
// This causes that the fragment is not properly created and the table is not extracted.
// The content that is before the StartFragment is htmlBefore and the content that is after the EndFragment is htmlAfter.
// So attempt to create a new document fragment with the content of htmlBefore + clipboardData.html + htmlAfter
// If a table is found, replace the fragment with the new fragment
const result =
!fragment.querySelector(TABLE_SELECTOR) &&
domCreator.htmlToDOM(htmlBefore + clipboardData.html + htmlAfter);
if (result && result.querySelector(TABLE_SELECTOR)) {
moveChildNodes(fragment, result?.body);
} else {
// If the table is still not found, try to extract the table from the clipboard data using Regex
const html = clipboardData.html ? excelHandler(clipboardData.html, htmlBefore) : undefined;

if (html && clipboardData.html != html) {
const doc = domCreator.htmlToDOM(html);
moveChildNodes(fragment, doc?.body);
}
}
}

/**
* @internal Export for test only
* @param html Source html
*/

export function excelHandler(html: string, htmlBefore: string): string {
if (html.match(LAST_TD_END_REGEX)) {
const trMatch = htmlBefore.match(LAST_TR_REGEX);
const tr = trMatch ? trMatch[0] : '<TR>';
html = tr + html + '</TR>';
}
if (html.match(LAST_TR_END_REGEX)) {
const tableMatch = htmlBefore.match(LAST_TABLE_REGEX);
const table = tableMatch ? tableMatch[0] : '<TABLE>';
html = table + html + '</TABLE>';
try {
if (html.match(LAST_TD_END_REGEX)) {
const trMatch = htmlBefore.match(LAST_TR_REGEX);
const tr = trMatch ? trMatch[0] : '<TR>';
html = tr + html + '</TR>';
}
if (html.match(LAST_TR_END_REGEX)) {
const tableMatch = htmlBefore.match(LAST_TABLE_REGEX);
const table = tableMatch ? tableMatch[0] : '<TABLE>';
html = table + html + '</TABLE>';
}
} finally {
return html;
}

return html;
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ describe('EditPlugin', () => {
rawEvent,
});

expect(keyboardDeleteSpy).toHaveBeenCalledWith(editor, rawEvent);
expect(keyboardDeleteSpy).toHaveBeenCalledWith(editor, rawEvent, true);
expect(keyboardInputSpy).not.toHaveBeenCalled();
expect(keyboardEnterSpy).not.toHaveBeenCalled();
expect(keyboardTabSpy).not.toHaveBeenCalled();
Expand All @@ -83,7 +83,7 @@ describe('EditPlugin', () => {
rawEvent,
});

expect(keyboardDeleteSpy).toHaveBeenCalledWith(editor, rawEvent);
expect(keyboardDeleteSpy).toHaveBeenCalledWith(editor, rawEvent, true);
expect(keyboardInputSpy).not.toHaveBeenCalled();
expect(keyboardEnterSpy).not.toHaveBeenCalled();
expect(keyboardTabSpy).not.toHaveBeenCalled();
Expand All @@ -106,6 +106,20 @@ describe('EditPlugin', () => {
expect(keyboardTabSpy).not.toHaveBeenCalled();
});

it('handleExpandedSelectionOnDelete disabled', () => {
plugin = new EditPlugin({ handleExpandedSelectionOnDelete: false });
const rawEvent = { key: 'Delete' } as any;

plugin.initialize(editor);

plugin.onPluginEvent({
eventType: 'keyDown',
rawEvent,
});

expect(keyboardDeleteSpy).toHaveBeenCalledWith(editor, rawEvent, false);
});

it('Tab', () => {
plugin = new EditPlugin();
const rawEvent = { key: 'Tab' } as any;
Expand Down Expand Up @@ -259,19 +273,27 @@ describe('EditPlugin', () => {
rawEvent: { key: 'Delete' } as any,
});

expect(keyboardDeleteSpy).toHaveBeenCalledWith(editor, {
key: 'Delete',
} as any);
expect(keyboardDeleteSpy).toHaveBeenCalledWith(
editor,
{
key: 'Delete',
} as any,
true
);

plugin.onPluginEvent({
eventType: 'keyDown',
rawEvent: { key: 'Delete' } as any,
});

expect(keyboardDeleteSpy).toHaveBeenCalledTimes(2);
expect(keyboardDeleteSpy).toHaveBeenCalledWith(editor, {
key: 'Delete',
} as any);
expect(keyboardDeleteSpy).toHaveBeenCalledWith(
editor,
{
key: 'Delete',
} as any,
true
);
expect(keyboardInputSpy).not.toHaveBeenCalled();
expect(keyboardEnterSpy).not.toHaveBeenCalled();
expect(keyboardTabSpy).not.toHaveBeenCalled();
Expand Down Expand Up @@ -309,7 +331,8 @@ describe('EditPlugin', () => {
key: 'Backspace',
keyCode: 8,
which: 8,
})
}),
true
);
});

Expand Down Expand Up @@ -337,7 +360,8 @@ describe('EditPlugin', () => {
key: 'Delete',
keyCode: 46,
which: 46,
})
}),
true
);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,31 @@ describe('keyboardDelete', () => {
expect(formatWithContentModelSpy).not.toHaveBeenCalled();
});

it('No need to delete - handleExpandedSelection disabled', () => {
const rawEvent = { key: 'Backspace' } as any;
const formatWithContentModelSpy = jasmine.createSpy('formatContentModel');
const node = document.createTextNode('test');
const range: DOMSelection = {
type: 'range',
range: ({
collapsed: false,
startContainer: node,
endContainer: node,
startOffset: 1,
endOffset: 3,
} as any) as Range,
isReverted: false,
};
const editor = {
formatContentModel: formatWithContentModelSpy,
getDOMSelection: () => range,
} as any;

keyboardDelete(editor, rawEvent, false /* handleExpandedSelectionOnDelete */);

expect(formatWithContentModelSpy).not.toHaveBeenCalled();
});

it('Backspace from the beginning', () => {
const rawEvent = { key: 'Backspace' } as any;
const formatWithContentModelSpy = jasmine.createSpy('formatContentModel');
Expand Down Expand Up @@ -625,4 +650,29 @@ describe('keyboardDelete', () => {

expect(formatWithContentModelSpy).toHaveBeenCalledTimes(1);
});

it('Delete all the content of text node - handleExpandedSelection disabled', () => {
const rawEvent = { key: 'Backspace' } as any;
const formatWithContentModelSpy = jasmine.createSpy('formatContentModel');
const node = document.createTextNode('test');
const range: DOMSelection = {
type: 'range',
range: ({
collapsed: false,
startContainer: node,
endContainer: node,
startOffset: 0,
endOffset: 4,
} as any) as Range,
isReverted: false,
};
const editor = {
formatContentModel: formatWithContentModelSpy,
getDOMSelection: () => range,
} as any;

keyboardDelete(editor, rawEvent, false /* handleExpandedSelectionOnDelete */);

expect(formatWithContentModelSpy).toHaveBeenCalledTimes(1);
});
});
Loading

0 comments on commit e3f54c1

Please sign in to comment.