Skip to content

Commit

Permalink
Implement Excel non-native event handling and related utilities
Browse files Browse the repository at this point in the history
  • Loading branch information
BryanValverdeU committed Feb 18, 2025
1 parent 6388b93 commit c5337af
Show file tree
Hide file tree
Showing 19 changed files with 233 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
@@ -0,0 +1,12 @@
import { setupExcelTableHandlers } from './setupExcelTableHandlers';
import type { BeforePasteEvent } from 'roosterjs-content-model-types';

/**
* @internal
*/
export function handleExcelContentFromNotNativeEvent(
event: BeforePasteEvent,
allowExcelNoBorderTable: boolean
) {
setupExcelTableHandlers(event, allowExcelNoBorderTable, false /* handleForNativeEvent */);
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
import { addParser } from '../utils/addParser';
import { isNodeOfType, moveChildNodes } from 'roosterjs-content-model-dom';
import { setProcessor } from '../utils/setProcessor';
import type {
BeforePasteEvent,
ClipboardData,
DOMCreator,
ElementProcessor,
} from 'roosterjs-content-model-types';
import { setupExcelTableHandlers } from './setupExcelTableHandlers';
import type { BeforePasteEvent, ClipboardData, DOMCreator } 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';

/**
Expand Down Expand Up @@ -54,40 +47,9 @@ 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, true /* 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { addParser } from '../utils/addParser';
import { setProcessor } from '../utils/setProcessor';
import type { BeforePasteEvent, ElementProcessor } from 'roosterjs-content-model-types';

const DEFAULT_BORDER_STYLE = 'solid 1px #d4d4d4';

/**
* @internal
*/
export function setupExcelTableHandlers(
event: BeforePasteEvent,
allowExcelNoBorderTable: boolean | undefined,
handleForNativeEvent: boolean
) {
addParser(event.domToModelOption, 'tableCell', (format, element) => {
if (
!allowExcelNoBorderTable &&
(element.style.borderStyle === 'none' ||
(!handleForNativeEvent && 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 @@ -4,6 +4,7 @@ import { chainSanitizerCallback } from './utils/chainSanitizerCallback';
import { DefaultSanitizers } from './DefaultSanitizers';
import { deprecatedBorderColorParser } from './utils/deprecatedColorParser';
import { getPasteSource } from './pasteSourceValidations/getPasteSource';
import { handleExcelContentFromNotNativeEvent } from './Excel/handleExcelContentFromNotNativeEvent';
import { parseLink } from './utils/linkParser';
import { PastePropertyNames } from './pasteSourceValidations/constants';
import { processPastedContentFromExcel } from './Excel/processPastedContentFromExcel';
Expand Down Expand Up @@ -123,6 +124,9 @@ export class PastePlugin implements EditorPlugin {
case 'powerPointDesktop':
processPastedContentFromPowerPoint(event, this.editor.getDOMCreator());
break;
case 'excelNonNativeEvent':
handleExcelContentFromNotNativeEvent(event, !!this.allowExcelNoBorderTable);
break;
}

addParser(event.domToModelOption, 'link', parseLink);
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 { BeforePasteEvent, ClipboardData } from 'roosterjs';

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,38 @@
import * as addParserFile from '../../../lib/paste/utils/addParser';
import * as setProcessorFile from '../../../lib/paste/utils/setProcessor';
import * as setupExcelTableHandlersFile from '../../../lib/paste/Excel/setupExcelTableHandlers';
import { BeforePasteEvent } from 'roosterjs-content-model-types';
import { handleExcelContentFromNotNativeEvent } from '../../../lib/paste/Excel/handleExcelContentFromNotNativeEvent';

describe('handleExcelContentFromNotNativeEvent', () => {
beforeEach(() => {
spyOn(setupExcelTableHandlersFile, 'setupExcelTableHandlers').and.callThrough();
spyOn(addParserFile, 'addParser').and.callFake(() => {});
spyOn(setProcessorFile, 'setProcessor').and.callFake(() => {});
});

it('should handle Excel content from non-native event', () => {
const fragment = document.createDocumentFragment();
const table = document.createElement('table');
fragment.appendChild(table);

const event: BeforePasteEvent = {
fragment,
clipboardData: {
types: ['web data/shadow-workbook'],
} as any,
} as any;

const allowExcelNoBorderTable = true;

handleExcelContentFromNotNativeEvent(event, allowExcelNoBorderTable);

expect(setupExcelTableHandlersFile.setupExcelTableHandlers).toHaveBeenCalledWith(
event,
allowExcelNoBorderTable,
false /* handleForNativeEvemt */
);
expect(setProcessorFile.setProcessor).toHaveBeenCalledTimes(1);
expect(addParserFile.addParser).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as PastePluginFile from '../../lib/paste/Excel/processPastedContentFromExcel';
import * as PastePluginFile from '../../../lib/paste/Excel/processPastedContentFromExcel';
import * as setupExcelTableHandlersFile from '../../../lib/paste/Excel/setupExcelTableHandlers';
import { ContentModelDocument, DOMCreator } from 'roosterjs-content-model-types';
import { createBeforePasteEventMock } from './processPastedContentFromWordDesktopTest';
import { processPastedContentFromExcel } from '../../lib/paste/Excel/processPastedContentFromExcel';
import { createBeforePasteEventMock } from '../createBeforePasteEventMock';
import { processPastedContentFromExcel } from '../../../lib/paste/Excel/processPastedContentFromExcel';
import {
contentModelToDom,
createDomToModelContext,
Expand Down Expand Up @@ -397,7 +398,7 @@ describe('childProcessorTest', () => {
const tableCell = createTableCell();
tableCell.format.textColor = 'black';

PastePluginFile.childProcessor(tableCell, element, context);
setupExcelTableHandlersFile.childProcessor(tableCell, element, context);

expect(tableCell.format.textColor).toBeUndefined();
expect(context.segmentFormat.textColor).toBeUndefined();
Expand All @@ -417,7 +418,7 @@ describe('childProcessorTest', () => {
const tableCell = createTableCell();
tableCell.format.textColor = 'black';

PastePluginFile.childProcessor(tableCell, element, context);
setupExcelTableHandlersFile.childProcessor(tableCell, element, context);

expect(tableCell.format.textColor).toBeUndefined();
expect(context.segmentFormat.textColor).toEqual('blue');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ClipboardData, DOMCreator } from 'roosterjs-content-model-types';
import { validateExcelFragment } from '../../lib/paste/Excel/processPastedContentFromExcel';
import { validateExcelFragment } from '../../../lib/paste/Excel/processPastedContentFromExcel';

describe('validateExcelFragment', () => {
let domCreator: DOMCreator;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ describe('getPasteSourceTest | ', () => {
const result = getPasteSource(defaultParam(), false /* shouldConvertSingleImage */);
expect(result).toBe('default');
});
it('Is Excel Non-Native Event', () => {
const result = getPasteSource(
excelNonNativeEventParam(),
false /* shouldConvertSingleImage */
);
expect(result).toBe('excelNonNativeEvent');
});
});

function wacParam(): BeforePasteEvent {
Expand Down Expand Up @@ -78,8 +85,9 @@ function googleSheetParam(): BeforePasteEvent {

function converSingleImageParam(): BeforePasteEvent {
const fragment = document.createDocumentFragment();
const clipboardData = <ClipboardData>{
const clipboardData = <any>{
htmlFirstLevelChildTags: ['IMG'],
types: [],
};

return <BeforePasteEvent>{
Expand Down Expand Up @@ -111,5 +119,17 @@ function wordParam(): BeforePasteEvent {
function defaultParam(): BeforePasteEvent {
const fragment = document.createDocumentFragment();

return <BeforePasteEvent>{ htmlAttributes: {}, fragment, clipboardData: {} };
return <BeforePasteEvent>{ htmlAttributes: {}, fragment, clipboardData: <any>{ types: [] } };
}

function excelNonNativeEventParam(): BeforePasteEvent {
const fragment = document.createDocumentFragment();

const clipboardData: ClipboardData = {
types: ['web data/shadow-workbook'],
rawHtml: '',
htmlFirstLevelChildTags: ['TABLE'],
} as any;

return <BeforePasteEvent>{ fragment, clipboardData, htmlAttributes: {} };
}
Loading

0 comments on commit c5337af

Please sign in to comment.