Skip to content

Commit

Permalink
Implement Excel non-native paste event handling and related utilities (
Browse files Browse the repository at this point in the history
…#2950)

* Implement Excel non-native event handling and related utilities

* Update import statement for BeforePasteEvent and ClipboardData types

* Fix typo in handleExcelContentFromNotNativeEventTest for handleForNativeEvent parameter

* Remove deprecated Excel non-native event handling and related tests; add new HTML template for non-native Excel content

* Fix tests in pipeline

---------

Co-authored-by: Jiuqing Song <jisong@microsoft.com>
  • Loading branch information
BryanValverdeU and JiuqingSong authored Feb 20, 2025
1 parent 3ddc5b8 commit 7beec85
Show file tree
Hide file tree
Showing 18 changed files with 326 additions and 105 deletions.
15 changes: 5 additions & 10 deletions demo/scripts/controlsV2/demoButtons/pasteButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,14 @@ const createDataTransfer = (

const createDataTransferItems = (data: ClipboardItems) => {
const isTEXT = (type: string) => type.startsWith('text/');
const isIMAGE = (type: string) => type.startsWith('image/');
const dataTransferItems: Promise<DataTransferItem>[] = [];
data.forEach(item => {
item.types.forEach(type => {
if (isTEXT(type) || isIMAGE(type)) {
dataTransferItems.push(
item
.getType(type)
.then(blob =>
createDataTransfer(isTEXT(type) ? 'string' : 'file', type, blob)
)
);
}
dataTransferItems.push(
item
.getType(type)
.then(blob => createDataTransfer(isTEXT(type) ? 'string' : 'file', type, blob))
);
});
});
return dataTransferItems;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,29 @@ 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';
const DEFAULT_BORDER_STYLE = 'solid 1px #d4d4d4';

/**
* @internal
* Convert pasted content from Excel, add borders when source doc doesn't have a border
* @param event The BeforePaste event
* @param domCreator The DOM creator
* @param allowExcelNoBorderTable Allow table copied from Excel without border
* @param isNativeEvent Whether the event is native event
*/

export function processPastedContentFromExcel(
event: BeforePasteEvent,
domCreator: DOMCreator,
allowExcelNoBorderTable?: boolean
allowExcelNoBorderTable: boolean,
isNativeEvent: boolean
) {
const { fragment, htmlBefore, htmlAfter, clipboardData } = event;

validateExcelFragment(fragment, domCreator, htmlBefore, clipboardData, htmlAfter);
// For non native event we already validated that the content contains a table
if (isNativeEvent) {
validateExcelFragment(fragment, domCreator, htmlBefore, clipboardData, htmlAfter);
}

// For Excel Online
const firstChild = fragment.firstChild;
Expand All @@ -54,40 +60,13 @@ export function processPastedContentFromExcel(
}
}

addParser(event.domToModelOption, 'tableCell', (format, element) => {
if (!allowExcelNoBorderTable && element.style.borderStyle === 'none') {
format.borderBottom = DEFAULT_BORDER_STYLE;
format.borderLeft = DEFAULT_BORDER_STYLE;
format.borderRight = DEFAULT_BORDER_STYLE;
format.borderTop = DEFAULT_BORDER_STYLE;
}
});

setProcessor(event.domToModelOption, 'child', childProcessor);
setupExcelTableHandlers(
event,
allowExcelNoBorderTable,
isNativeEvent /* handleForNativeEvent */
);
}

/**
* @internal
* Exported only for unit test
*/
export const childProcessor: ElementProcessor<ParentNode> = (group, element, context) => {
const segmentFormat = { ...context.segmentFormat };
if (
group.blockGroupType === 'TableCell' &&
group.format.textColor &&
!context.segmentFormat.textColor
) {
context.segmentFormat.textColor = group.format.textColor;
}

context.defaultElementProcessors.child(group, element, context);

if (group.blockGroupType === 'TableCell' && group.format.textColor) {
context.segmentFormat = segmentFormat;
delete group.format.textColor;
}
};

/**
* @internal
* Exported only for unit test
Expand Down Expand Up @@ -148,3 +127,50 @@ export function excelHandler(html: string, htmlBefore: string): string {
return html;
}
}

/**
* @internal
* Exported only for unit test
*/
export function setupExcelTableHandlers(
event: BeforePasteEvent,
allowExcelNoBorderTable: boolean | undefined,
isNativeEvent: boolean
) {
addParser(event.domToModelOption, 'tableCell', (format, element) => {
if (
!allowExcelNoBorderTable &&
(element.style.borderStyle === 'none' ||
(!isNativeEvent && element.style.borderStyle == ''))
) {
format.borderBottom = DEFAULT_BORDER_STYLE;
format.borderLeft = DEFAULT_BORDER_STYLE;
format.borderRight = DEFAULT_BORDER_STYLE;
format.borderTop = DEFAULT_BORDER_STYLE;
}
});

setProcessor(event.domToModelOption, 'child', childProcessor);
}

/**
* @internal
* Exported only for unit test
*/
export const childProcessor: ElementProcessor<ParentNode> = (group, element, context) => {
const segmentFormat = { ...context.segmentFormat };
if (
group.blockGroupType === 'TableCell' &&
group.format.textColor &&
!context.segmentFormat.textColor
) {
context.segmentFormat.textColor = group.format.textColor;
}

context.defaultElementProcessors.child(group, element, context);

if (group.blockGroupType === 'TableCell' && group.format.textColor) {
context.segmentFormat = segmentFormat;
delete group.format.textColor;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,14 @@ export class PastePlugin implements EditorPlugin {
break;
case 'excelOnline':
case 'excelDesktop':
case 'excelNonNativeEvent':
if (pasteType === 'normal' || pasteType === 'mergeFormat') {
// Handle HTML copied from Excel
processPastedContentFromExcel(
event,
this.editor.getDOMCreator(),
this.allowExcelNoBorderTable
!!this.allowExcelNoBorderTable,
pasteSource != 'excelNonNativeEvent' /* isNativeEvent */
);
}
break;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { documentContainWacElements } from './documentContainWacElements';
import { isExcelDesktopDocument } from './isExcelDesktopDocument';
import { isExcelNotNativeEvent } from './isExcelNonNativeEvent';
import { isExcelOnlineDocument } from './isExcelOnlineDocument';
import { isGoogleSheetDocument } from './isGoogleSheetDocument';
import { isPowerPointDesktopDocument } from './isPowerPointDesktopDocument';
Expand Down Expand Up @@ -29,7 +30,8 @@ export type KnownPasteSourceType =
| 'googleSheets'
| 'wacComponents'
| 'default'
| 'singleImage';
| 'singleImage'
| 'excelNonNativeEvent';

/**
* @internal
Expand All @@ -44,6 +46,7 @@ const getSourceFunctions = new Map<KnownPasteSourceType, GetSourceFunction>([
['wacComponents', documentContainWacElements],
['googleSheets', isGoogleSheetDocument],
['singleImage', shouldConvertToSingleImage],
['excelNonNativeEvent', isExcelNotNativeEvent],
]);

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { GetSourceFunction, GetSourceInputParams } from './getPasteSource';

const ShadowWorkbookClipboardType = 'web data/shadow-workbook';

/**
* @internal
* When the clipboard content is retrieved programatically, the clipboard html does not contain the usual
* attributes we use to determine if the content is from Excel. This function is used to handle that case.
*/
export const isExcelNotNativeEvent: GetSourceFunction = (props: GetSourceInputParams) => {
const { clipboardData } = props;

return (
clipboardData.types.includes(ShadowWorkbookClipboardType) &&
clipboardData.htmlFirstLevelChildTags?.length == 1 &&
clipboardData.htmlFirstLevelChildTags[0] == 'TABLE'
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { BeforePasteEvent, ClipboardData } from 'roosterjs-content-model-types';

export function createBeforePasteEventMock(
fragment: DocumentFragment,
htmlBefore: string = ''
): BeforePasteEvent {
return {
eventType: 'beforePaste',
clipboardData: <ClipboardData>{},
fragment: fragment,
htmlBefore,
htmlAfter: '',
htmlAttributes: {},
pasteType: 'normal',
domToModelOption: {
additionalAllowedTags: [],
additionalDisallowedTags: [],
additionalFormatParsers: {},
attributeSanitizers: {},
formatParserOverride: {},
processorOverride: {},
styleSanitizers: {},
},
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import * as processPastedContentFromExcel from '../../../lib/paste/Excel/processPastedContentFromExcel';
import { excelContentFromNonNativeTemplate } from './htmlTemplates/htmlFromExcelNonNative';
import { expectEqual, initEditor } from './testUtils';
import { paste } from 'roosterjs-content-model-core';
import type { ClipboardData, IEditor } from 'roosterjs-content-model-types';

describe('Paste from Excel non native', () => {
let editor: IEditor = undefined!;

beforeEach(() => {
editor = initEditor('Paste_from_Excel_non_native');
});

afterEach(() => {
document.getElementById('Paste_from_Excel_non_native')?.remove();
});

it('E2E', () => {
const clipboardData = {
types: ['web data/shadow-workbook', 'image/png', 'text/plain', 'text/html'],
text: '\r\n',
image: {},
files: [{}],
rawHtml: excelContentFromNonNativeTemplate,
customValues: {},
pasteNativeEvent: true,
imageDataUri: null,
} as ClipboardData;
spyOn(processPastedContentFromExcel, 'processPastedContentFromExcel').and.callThrough();

paste(editor, clipboardData);
const model = editor.getContentModelCopy('connected');

expect(processPastedContentFromExcel.processPastedContentFromExcel).toHaveBeenCalled();

expectEqual(model, {
blockGroupType: 'Document',
blocks: [
{
widths: jasmine.anything() as any,
rows: [
{
height: jasmine.anything() as any,
cells: [
{
spanAbove: false,
spanLeft: false,
isHeader: false,
blockGroupType: 'TableCell',
blocks: [],
format: {
borderTop: '1px solid rgb(212, 212, 212)',
borderRight: '1px solid rgb(212, 212, 212)',
borderBottom: '1px solid rgb(212, 212, 212)',
borderLeft: '1px solid rgb(212, 212, 212)',
width: '50pt',
height: '14.4pt',
},
dataset: {},
},
{
spanAbove: false,
spanLeft: false,
isHeader: false,
blockGroupType: 'TableCell',
blocks: [],
format: {
borderTop: '1px solid rgb(212, 212, 212)',
borderRight: '1px solid rgb(212, 212, 212)',
borderBottom: '1px solid rgb(212, 212, 212)',
borderLeft: '1px solid rgb(212, 212, 212)',
width: '50pt',
},
dataset: {},
},
],
format: {},
},
],
blockType: 'Table',
format: {
width: '100pt',
useBorderBox: true,
borderCollapse: true,
},
dataset: {},
},
{
segments: [
{
isSelected: true,
segmentType: 'SelectionMarker',
format: {
backgroundColor: '',
fontFamily: '',
fontSize: '',
fontWeight: '',
italic: false,
letterSpacing: '',
lineHeight: '',
strikethrough: false,
superOrSubScriptSequence: '',
textColor: '',
underline: false,
},
},
{
segmentType: 'Br',
format: {},
},
],
blockType: 'Paragraph',
format: {},
},
],
format: {},
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const excelContentFromNonNativeTemplate = `<table border="0" cellpadding="0" cellspacing="0" width="134" style="border-collapse:
collapse;width:100pt"><tbody><tr height="19" style="height:14.4pt">
<td height="19" width="67" style="height:14.4pt;width:50pt"></td>
<td width="67" style="width:50pt"></td>
</tr></tbody></table>`;
Loading

0 comments on commit 7beec85

Please sign in to comment.