Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standalone editor: Remove dependencies to enums Part 2 #2153

Merged
merged 3 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { areSameRangeEx } from '../../modelApi/selection/areSameRangeEx';
import { isCharacterValue } from '../../domUtils/eventUtils';
import { Keys, PluginEventType } from 'roosterjs-editor-types';
import { PluginEventType } from 'roosterjs-editor-types';
import type ContentModelContentChangedEvent from '../../publicTypes/event/ContentModelContentChangedEvent';
import type { ContentModelCachePluginState } from '../../publicTypes/pluginState/ContentModelCachePluginState';
import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor';
Expand Down Expand Up @@ -167,7 +167,7 @@ export default class ContentModelCachePlugin
// 3. ENTER key is pressed. ENTER key will create new paragraph, so need to update cache to reflect this change
// TODO: Handle ENTER key to better reuse content model

if (rawEvent.which == Keys.ENTER) {
if (rawEvent.key == 'Enter') {
return true;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import paste from '../../publicApi/utils/paste';
import { addRangeToSelection, extractClipboardItems } from 'roosterjs-editor-dom';
import { ChangeSource, ColorTransformDirection, PluginEventType } from 'roosterjs-editor-types';
import { ChangeSource } from '../../publicTypes/event/ContentModelContentChangedEvent';
import { cloneModel } from '../../modelApi/common/cloneModel';
import { ColorTransformDirection, PluginEventType } from 'roosterjs-editor-types';
import { DeleteResult } from '../../modelApi/edit/utils/DeleteSelectionStep';
import { deleteSelection } from '../../modelApi/edit/deleteSelection';
import { formatWithContentModel } from '../../publicApi/utils/formatWithContentModel';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import keyboardDelete from '../../publicApi/editing/keyboardDelete';
import { Keys, PluginEventType } from 'roosterjs-editor-types';
import { PluginEventType } from 'roosterjs-editor-types';
import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor';
import type {
EditorPlugin,
Expand Down Expand Up @@ -62,13 +62,12 @@ export default class ContentModelEditPlugin implements EditorPlugin {

private handleKeyDownEvent(editor: IContentModelEditor, event: PluginKeyDownEvent) {
const rawEvent = event.rawEvent;
const which = rawEvent.which;

if (!rawEvent.defaultPrevented && !event.handledByEditFeature) {
// TODO: Consider use ContentEditFeature and need to hide other conflict features that are not based on Content Model
switch (which) {
case Keys.BACKSPACE:
case Keys.DELETE:
switch (rawEvent.key) {
case 'Backspace':
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
keyboardDelete(editor, rawEvent);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,23 @@ import applyPendingFormat from '../../publicApi/format/applyPendingFormat';
import { canApplyPendingFormat, clearPendingFormat } from '../../modelApi/format/pendingFormat';
import { getObjectKeys } from 'roosterjs-content-model-dom';
import { isCharacterValue } from '../../domUtils/eventUtils';
import { Keys, PluginEventType } from 'roosterjs-editor-types';
import { PluginEventType } from 'roosterjs-editor-types';
import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor';
import type { IEditor, PluginEvent, PluginWithState } from 'roosterjs-editor-types';
import type { ContentModelFormatPluginState } from '../../publicTypes/pluginState/ContentModelFormatPluginState';

// During IME input, KeyDown event will have "Process" as key
const ProcessKey = 'Process';
const CursorMovingKeys = new Set<string>([
'ArrowUp',
'ArrowDown',
'ArrowLeft',
'ArrowRight',
'Home',
'End',
'PageUp',
'PageDown',
]);

/**
* ContentModelFormat plugins helps editor to do formatting on top of content model.
Expand Down Expand Up @@ -92,7 +102,7 @@ export default class ContentModelFormatPlugin
break;

case PluginEventType.KeyDown:
if (event.rawEvent.which >= Keys.PAGEUP && event.rawEvent.which <= Keys.DOWN) {
if (CursorMovingKeys.has(event.rawEvent.key)) {
clearPendingFormat(this.editor);
} else if (
this.hasDefaultFormat &&
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import addParser from './utils/addParser';
import { BorderKeys } from 'roosterjs-content-model-dom';
import { chainSanitizerCallback, getPasteSource } from 'roosterjs-editor-dom';
import { chainSanitizerCallback } from 'roosterjs-editor-dom';
import { deprecatedBorderColorParser } from './utils/deprecatedColorParser';
import { KnownPasteSourceType, PasteType, PluginEventType } from 'roosterjs-editor-types';
import { getPasteSource } from './pasteSourceValidations/getPasteSource';
import { parseLink } from './utils/linkParser';
import { PastePropertyNames } from './pasteSourceValidations/constants';
import { PasteType as OldPasteType, PluginEventType } from 'roosterjs-editor-types';
import { processPastedContentFromExcel } from './Excel/processPastedContentFromExcel';
import { processPastedContentFromPowerPoint } from './PowerPoint/processPastedContentFromPowerPoint';
import { processPastedContentFromWordDesktop } from './WordDesktop/processPastedContentFromWordDesktop';
import { processPastedContentWacComponents } from './WacComponents/processPastedContentWacComponents';
import type { PasteType } from '../../../publicTypes/parameter/PasteType';
import type ContentModelBeforePasteEvent from '../../../publicTypes/event/ContentModelBeforePasteEvent';
import type {
BorderFormat,
Expand All @@ -23,7 +26,14 @@ import type {
PluginEvent,
} from 'roosterjs-editor-types';

const GOOGLE_SHEET_NODE_NAME = 'google-sheets-html-origin';
// Map old PasteType to new PasteType
// TODO: We can remove this once we have standalone editor
const PasteTypeMap: Record<OldPasteType, PasteType> = {
[OldPasteType.AsImage]: 'asImage',
[OldPasteType.AsPlainText]: 'asPlainText',
[OldPasteType.MergeFormat]: 'mergeFormat',
[OldPasteType.Normal]: 'normal',
};

/**
* Paste plugin, handles BeforePaste event and reformat some special content, including:
Expand Down Expand Up @@ -81,28 +91,34 @@ export default class ContentModelPastePlugin implements EditorPlugin {
}

const ev = event as ContentModelBeforePasteEvent;

if (!ev.domToModelOption) {
return;
}

const pasteSource = getPasteSource(ev, false);
const pasteType = PasteTypeMap[ev.pasteType];

switch (pasteSource) {
case KnownPasteSourceType.WordDesktop:
case 'wordDesktop':
processPastedContentFromWordDesktop(ev);
break;
case KnownPasteSourceType.WacComponents:
case 'wacComponents':
processPastedContentWacComponents(ev);
break;
case KnownPasteSourceType.ExcelOnline:
case KnownPasteSourceType.ExcelDesktop:
if (ev.pasteType === PasteType.Normal || ev.pasteType === PasteType.MergeFormat) {
case 'excelOnline':
case 'excelDesktop':
if (pasteType === 'normal' || pasteType === 'mergeFormat') {
// Handle HTML copied from Excel
processPastedContentFromExcel(ev, this.editor.getTrustedHTMLHandler());
}
break;
case KnownPasteSourceType.GoogleSheets:
ev.sanitizingOption.additionalTagReplacements[GOOGLE_SHEET_NODE_NAME] = '*';
case 'googleSheets':
ev.sanitizingOption.additionalTagReplacements[
PastePropertyNames.GOOGLE_SHEET_NODE_NAME
] = '*';
break;
case KnownPasteSourceType.PowerPointDesktop:
case 'powerPointDesktop':
processPastedContentFromPowerPoint(ev, this.editor.getTrustedHTMLHandler());
break;
}
Expand All @@ -113,7 +129,7 @@ export default class ContentModelPastePlugin implements EditorPlugin {
addParser(ev.domToModelOption, 'table', deprecatedBorderColorParser);
sanitizeBlockStyles(ev.sanitizingOption);

if (ev.pasteType === PasteType.MergeFormat) {
if (pasteType === 'mergeFormat') {
addParser(ev.domToModelOption, 'block', blockElementParser);
addParser(ev.domToModelOption, 'listLevel', blockElementParser);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* @internal
*/
export const enum PastePropertyNames {
/**
* Node attribute used to identify if the content is from Google Sheets.
*/
GOOGLE_SHEET_NODE_NAME = 'google-sheets-html-origin',

/**
* Name of the HTMLMeta Property that provides the Office App Source of the pasted content
*/
PROG_ID_NAME = 'ProgId',

/**
* Name of the HTMLMeta Property that identifies pated content as from Excel Desktop
*/
EXCEL_DESKTOP_ATTRIBUTE_NAME = 'xmlns:x',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { GetSourceFunction } from './getPasteSource';

const WORD_ONLINE_TABLE_TEMP_ELEMENT_CLASSES = [
'TableInsertRowGapBlank',
'TableColumnResizeHandle',
'TableCellTopBorderHandle',
'TableCellLeftBorderHandle',
'TableHoverColumnHandle',
'TableHoverRowHandle',
];

const WAC_IDENTIFY_SELECTOR =
'ul[class^="BulletListStyle"]>.OutlineElement,ol[class^="NumberListStyle"]>.OutlineElement,span.WACImageContainer,' +
WORD_ONLINE_TABLE_TEMP_ELEMENT_CLASSES.map(c => `table div[class^="${c}"]`).join(',');

/**
* @internal
* Check whether the fragment provided contain Wac Elements
* @param props Properties related to the PasteEvent
* @returns
*/
export const documentContainWacElements: GetSourceFunction = props => {
const { fragment } = props;
return !!fragment.querySelector(WAC_IDENTIFY_SELECTOR);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { documentContainWacElements } from './documentContainWacElements';
import { isExcelDesktopDocument } from './isExcelDesktopDocument';
import { isExcelOnlineDocument } from './isExcelOnlineDocument';
import { isGoogleSheetDocument } from './isGoogleSheetDocument';
import { isPowerPointDesktopDocument } from './isPowerPointDesktopDocument';
import { isWordDesktopDocument } from './isWordDesktopDocument';
import { shouldConvertToSingleImage } from './shouldConvertToSingleImage';
import type { BeforePasteEvent, ClipboardData } from 'roosterjs-editor-types';

/**
* @internal
*/
export type GetSourceInputParams = {
htmlAttributes: Record<string, string>;
fragment: DocumentFragment;
shouldConvertSingleImage: boolean;
clipboardData: ClipboardData;
};

/**
* @internal
* Represent the types of sources to handle in the Paste Plugin
*/
export type KnownPasteSourceType =
| 'wordDesktop'
| 'excelDesktop'
| 'excelOnline'
| 'powerPointDesktop'
| 'googleSheets'
| 'wacComponents'
| 'default'
| 'singleImage';

/**
* @internal
*/
export type GetSourceFunction = (props: GetSourceInputParams) => boolean;

const getSourceFunctions = new Map<KnownPasteSourceType, GetSourceFunction>([
['wordDesktop', isWordDesktopDocument],
['excelDesktop', isExcelDesktopDocument],
['excelOnline', isExcelOnlineDocument],
['powerPointDesktop', isPowerPointDesktopDocument],
['wacComponents', documentContainWacElements],
['googleSheets', isGoogleSheetDocument],
['singleImage', shouldConvertToSingleImage],
]);

/**
* @internal
* This function tries to get the source of the Pasted content
* @param event the before paste event
* @param shouldConvertSingleImage Whether convert single image is enabled.
* @returns The Type of pasted content, if no type found will return {KnownSourceType.Default}
*/
export function getPasteSource(
event: BeforePasteEvent,
shouldConvertSingleImage: boolean
): KnownPasteSourceType {
const { htmlAttributes, clipboardData, fragment } = event;

let result: KnownPasteSourceType | null = null;
const param: GetSourceInputParams = {
htmlAttributes,
fragment,
shouldConvertSingleImage,
clipboardData,
};

getSourceFunctions.forEach((func, key) => {
if (!result && func(param)) {
result = key;
}
});

return result ?? 'default';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { PastePropertyNames } from './constants';
import type { GetSourceFunction } from './getPasteSource';

const EXCEL_ATTRIBUTE_VALUE = 'urn:schemas-microsoft-com:office:excel';

/**
* @internal
* Checks whether the Array provided contains strings that identify Excel Desktop documents
* @param props Properties related to the PasteEvent
* @returns
*/
export const isExcelDesktopDocument: GetSourceFunction = props => {
const { htmlAttributes } = props;
// The presence of this attribute confirms its origin from Excel Desktop
return htmlAttributes[PastePropertyNames.EXCEL_DESKTOP_ATTRIBUTE_NAME] == EXCEL_ATTRIBUTE_VALUE;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { PastePropertyNames } from './constants';
import type { GetSourceFunction } from './getPasteSource';

// Excel Desktop also has this attribute
const EXCEL_ONLINE_ATTRIBUTE_VALUE = 'Excel.Sheet';

/**
* @internal
* Checks whether the Array provided contains strings that identify Excel Online documents
* @param props Properties related to the PasteEvent
* @returns
*/
export const isExcelOnlineDocument: GetSourceFunction = props => {
const { htmlAttributes } = props;
// The presence of Excel.Sheet confirms its origin from Excel, the absence of EXCEL_DESKTOP_ATTRIBUTE_NAME confirms it is from the Online version
return (
htmlAttributes[PastePropertyNames.PROG_ID_NAME] == EXCEL_ONLINE_ATTRIBUTE_VALUE &&
htmlAttributes[PastePropertyNames.EXCEL_DESKTOP_ATTRIBUTE_NAME] == undefined
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { PastePropertyNames } from './constants';
import type { GetSourceFunction } from './getPasteSource';

/**
* @internal
* Checks whether the fragment provided contain elements from Google sheets
* @param props Properties related to the PasteEvent
* @returns
*/
export const isGoogleSheetDocument: GetSourceFunction = props => {
const { fragment } = props;
return !!fragment.querySelector(PastePropertyNames.GOOGLE_SHEET_NODE_NAME);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { PastePropertyNames } from './constants';
import type { GetSourceFunction } from './getPasteSource';

const POWERPOINT_ATTRIBUTE_VALUE = 'PowerPoint.Slide';

/**
* @internal
* Checks whether the Array provided contains strings that identify Power Point Desktop documents
* @param props Properties related to the PasteEvent
* @returns
*/
export const isPowerPointDesktopDocument: GetSourceFunction = props => {
return props.htmlAttributes[PastePropertyNames.PROG_ID_NAME] == POWERPOINT_ATTRIBUTE_VALUE;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { PastePropertyNames } from './constants';
import type { GetSourceFunction } from './getPasteSource';

const WORD_ATTRIBUTE_NAME = 'xmlns:w';
const WORD_ATTRIBUTE_VALUE = 'urn:schemas-microsoft-com:office:word';
const WORD_PROG_ID = 'Word.Document';

/**
* @internal
* Checks whether the Array provided contains strings that identify Word Desktop documents
* @param props Properties related to the PasteEvent
* @returns
*/
export const isWordDesktopDocument: GetSourceFunction = props => {
const { htmlAttributes } = props;
return (
htmlAttributes[WORD_ATTRIBUTE_NAME] == WORD_ATTRIBUTE_VALUE ||
htmlAttributes[PastePropertyNames.PROG_ID_NAME] == WORD_PROG_ID
);
};
Loading
Loading