diff --git a/packages/roosterjs-content-model/lib/editor/coreApi/createPasteModel.ts b/packages/roosterjs-content-model/lib/editor/coreApi/createPasteModel.ts index 9ab7210cf01..bb8498cf854 100644 --- a/packages/roosterjs-content-model/lib/editor/coreApi/createPasteModel.ts +++ b/packages/roosterjs-content-model/lib/editor/coreApi/createPasteModel.ts @@ -28,19 +28,13 @@ export const createPasteModel: CreatePasteModel = ( event ); - return domToContentModel(fragment, core.api.createEditorContext(core), { - processorOverride: { - element: (group, element, context) => { - const wasHandled = - event.elementProcessors.length > 0 && - event.elementProcessors.some(p => p(group, element, context)); + const model = domToContentModel( + fragment, + core.api.createEditorContext(core), + event.domToModelOption + ); - if (!wasHandled) { - context.defaultElementProcessors.element(group, element, context); - } - }, - }, - }); + return model; }; function createBeforePasteEvent( @@ -60,6 +54,6 @@ function createBeforePasteEvent( htmlBefore: '', htmlAfter: '', htmlAttributes: {}, - elementProcessors: [], + domToModelOption: {}, }; } diff --git a/packages/roosterjs-content-model/lib/editor/plugins/PastePlugin/ContentModelPastePlugin.ts b/packages/roosterjs-content-model/lib/editor/plugins/PastePlugin/ContentModelPastePlugin.ts index c0ded3070ae..bb8fe3e21cf 100644 --- a/packages/roosterjs-content-model/lib/editor/plugins/PastePlugin/ContentModelPastePlugin.ts +++ b/packages/roosterjs-content-model/lib/editor/plugins/PastePlugin/ContentModelPastePlugin.ts @@ -1,5 +1,7 @@ import ContentModelBeforePasteEvent from '../../../publicTypes/event/ContentModelBeforePasteEvent'; -import { getPasteSource } from 'roosterjs-editor-dom'; +import { getPasteSource, safeInstanceOf } from 'roosterjs-editor-dom'; +import { handleExcelOnline } from './handleExcelOnline'; +import { handleWacComponents } from './handleWacComponents'; import { IContentModelEditor } from '../../../publicTypes/IContentModelEditor'; import { wordDesktopElementProcessor } from './WordDesktopProcessor/wordDesktopElementProcessor'; import { @@ -10,6 +12,34 @@ import { PluginEventType, } from 'roosterjs-editor-types'; +const WAC_CLASSES = [ + 'BulletListStyle', + 'OutlineElement', + 'NumberListStyle', + 'OutlineElement', + 'WACImageContainer', + 'ListContainerWrapper', + 'BulletListStyle', + 'NumberListStyle', + 'WACImageContainer', + 'Superscript', +]; +const deprecatedColorParser = ( + format: import('c:/Users/bvalverde/Desktop/Newfolder/roosterjs/packages/roosterjs-content-model/lib/index').ContentModelSegmentFormat, + element: HTMLElement, + _context: import('c:/Users/bvalverde/Desktop/Newfolder/roosterjs/packages/roosterjs-content-model/lib/index').DomToModelContext, + defaultStyles: Readonly> +): void => { + console.log(element); + if (DeprecatedColorList.indexOf(element.style.backgroundColor) > -1) { + format.backgroundColor = defaultStyles.backgroundColor; + element.style.backgroundColor = defaultStyles.backgroundColor ?? ''; + } + if (DeprecatedColorList.indexOf(element.style.color) > -1) { + format.textColor = defaultStyles.color; + element.style.color = defaultStyles.color ?? ''; + } +}; /** * ContentModelFormat plugins helps editor to do formatting on top of content model. * This includes: @@ -18,6 +48,12 @@ import { export default class ContentModelFormatPlugin implements EditorPlugin { private editor: IContentModelEditor | null = null; + /** + * Construct a new instance of Paste class + * @param unknownTagReplacement Replace solution of unknown tags, default behavior is to replace with SPAN + */ + constructor(private unknownTagReplacement: string = 'SPAN') {} + /** * Get name of this plugin */ @@ -52,26 +88,118 @@ export default class ContentModelFormatPlugin implements EditorPlugin { * @param event The event to handle: */ onPluginEvent(event: PluginEvent) { - if (!this.editor) { + if (!this.editor || event.eventType != PluginEventType.BeforePaste) { return; } - switch (event.eventType) { - case PluginEventType.BeforePaste: - const ev = event as ContentModelBeforePasteEvent; - if (!ev.elementProcessors) { - return; - } - const pasteSource = getPasteSource(event, false); - switch (pasteSource) { - case KnownPasteSourceType.WordDesktop: - ev.elementProcessors.push(wordDesktopElementProcessor); - break; - - default: - break; - } + const ev = event as ContentModelBeforePasteEvent; + if (!ev.domToModelOption.processorOverride) { + ev.domToModelOption.processorOverride = {}; + } + const pasteSource = getPasteSource(event, false); + switch (pasteSource) { + case KnownPasteSourceType.WordDesktop: + ev.domToModelOption.processorOverride.element = wordDesktopElementProcessor; + break; + case KnownPasteSourceType.ExcelOnline: + handleExcelOnline(ev); + break; + case KnownPasteSourceType.WacComponents: + event.sanitizingOption.additionalAllowedCssClasses.push(...WAC_CLASSES); + + handleWacComponents(ev); + break; + + default: break; } + sanitizeHtmlColorsFromPastedContent(ev); + sanitizeLinks(ev); + + event.sanitizingOption.unknownTagReplacement = this.unknownTagReplacement; } } + +/** + * @internal + * List of deprecated colors that should be removed + */ + +export const DeprecatedColorList: string[] = [ + 'activeborder', + 'activecaption', + 'appworkspace', + 'background', + 'buttonhighlight', + 'buttonshadow', + 'captiontext', + 'inactiveborder', + 'inactivecaption', + 'inactivecaptiontext', + 'infobackground', + 'infotext', + 'menu', + 'menutext', + 'scrollbar', + 'threeddarkshadow', + 'threedface', + 'threedhighlight', + 'threedlightshadow', + 'threedfhadow', + 'window', + 'windowframe', + 'windowtext', +]; +function sanitizeHtmlColorsFromPastedContent(ev: ContentModelBeforePasteEvent) { + if (!ev.domToModelOption.additionalFormatParsers) { + ev.domToModelOption.additionalFormatParsers = {}; + } + if (!ev.domToModelOption.additionalFormatParsers.segment) { + ev.domToModelOption.additionalFormatParsers.segment = []; + } + if (!ev.domToModelOption.additionalFormatParsers.segmentOnBlock) { + ev.domToModelOption.additionalFormatParsers.segmentOnBlock = []; + } + ev.domToModelOption.additionalFormatParsers.segment.push(deprecatedColorParser); + ev.domToModelOption.additionalFormatParsers.segmentOnBlock.push(deprecatedColorParser); +} + +const HTTP = 'http:'; +const HTTPS = 'https:'; +const NOTES = 'notes:'; + +function sanitizeLinks(ev: ContentModelBeforePasteEvent) { + if (!ev.domToModelOption.additionalFormatParsers) { + ev.domToModelOption.additionalFormatParsers = {}; + } + if (!ev.domToModelOption.additionalFormatParsers.link) { + ev.domToModelOption.additionalFormatParsers.link = []; + } + + ev.domToModelOption.additionalFormatParsers.link.push( + (format, element, context, defaultStyle) => { + if (!safeInstanceOf(element, 'HTMLAnchorElement')) { + return; + } + + let url: URL | undefined; + try { + url = new URL(element.href); + } catch { + url = undefined; + } + + if ( + !url || + !( + url.protocol === HTTP || + url.protocol === HTTPS || + url.protocol === NOTES + ) /* whitelist Notes protocol */ + ) { + element.removeAttribute('href'); + format.href = ''; + } + } + ); +} diff --git a/packages/roosterjs-content-model/lib/editor/plugins/PastePlugin/WordDesktopProcessor/wordDesktopElementProcessor.ts b/packages/roosterjs-content-model/lib/editor/plugins/PastePlugin/WordDesktopProcessor/wordDesktopElementProcessor.ts index 1089c6a52f6..8c4de8bc61a 100644 --- a/packages/roosterjs-content-model/lib/editor/plugins/PastePlugin/WordDesktopProcessor/wordDesktopElementProcessor.ts +++ b/packages/roosterjs-content-model/lib/editor/plugins/PastePlugin/WordDesktopProcessor/wordDesktopElementProcessor.ts @@ -2,10 +2,10 @@ import { addBlock } from '../../../../modelApi/common/addBlock'; import { ContentModelBlockGroup } from '../../../../publicTypes/group/ContentModelBlockGroup'; import { createListItem } from '../../../../modelApi/creators/createListItem'; import { DomToModelContext } from '../../../../publicTypes/context/DomToModelContext'; +import { ElementProcessor } from 'roosterjs-content-model/lib/publicTypes'; import { getStyles, safeInstanceOf } from 'roosterjs-editor-dom'; import { NodeType } from 'roosterjs-editor-types'; import { parseFormat } from '../../../../domToModel/utils/parseFormat'; -import { PasteElementProcessor } from '../../../../publicTypes/event/PasteElementProcessor'; const MSO_COMMENT_ANCHOR_HREF_REGEX = /#_msocom_/; const MSO_SPECIAL_CHARACTER = 'mso-special-character'; @@ -15,13 +15,18 @@ const MSO_ELEMENT_COMMENT_LIST = 'comment-list'; const MSO_LIST = 'mso-list'; const MSO_LIST_IGNORE = 'ignore'; -export const wordDesktopElementProcessor: PasteElementProcessor = ( +export const wordDesktopElementProcessor: ElementProcessor = ( group, element, context ) => { const styles = getStyles(element); - return processWordList(styles, group, element, context) || processWordCommand(styles, element); + const wasHandled = + processWordList(styles, group, element, context) || processWordCommand(styles, element); + + if (!wasHandled) { + context.defaultElementProcessors.element(group, element, context); + } }; function processWordCommand(styles: Record, element: HTMLElement) { @@ -77,7 +82,6 @@ function processWordList( addBlock(group, listItem); return true; } - return false; } diff --git a/packages/roosterjs-content-model/lib/editor/plugins/PastePlugin/handleExcelOnline.ts b/packages/roosterjs-content-model/lib/editor/plugins/PastePlugin/handleExcelOnline.ts new file mode 100644 index 00000000000..08c144c05da --- /dev/null +++ b/packages/roosterjs-content-model/lib/editor/plugins/PastePlugin/handleExcelOnline.ts @@ -0,0 +1,30 @@ +import ContentModelBeforePasteEvent from '../../../publicTypes/event/ContentModelBeforePasteEvent'; + +const DEFAULT_BORDER_STYLE = 'solid 1px #d4d4d4'; + +export function handleExcelOnline(ev: ContentModelBeforePasteEvent) { + if (!ev.domToModelOption.formatParserOverride) { + ev.domToModelOption.formatParserOverride = {}; + } + + if (!ev.domToModelOption.processorOverride) { + ev.domToModelOption.processorOverride = {}; + } + + ev.domToModelOption.processorOverride.element = (group, element, context) => { + if (element.tagName.toLowerCase() === 'div' && element.getAttribute('data-ccp-timestamp')) { + context.elementProcessors.child(group, element, context); + } else { + context.defaultElementProcessors.element(group, element, context); + } + }; + + ev.domToModelOption.formatParserOverride.border = (format, element) => { + if (element.style.border === 'none') { + format.borderBottom = DEFAULT_BORDER_STYLE; + format.borderRight = DEFAULT_BORDER_STYLE; + format.borderTop = DEFAULT_BORDER_STYLE; + format.borderLeft = DEFAULT_BORDER_STYLE; + } + }; +} diff --git a/packages/roosterjs-content-model/lib/editor/plugins/PastePlugin/handleWacComponents.ts b/packages/roosterjs-content-model/lib/editor/plugins/PastePlugin/handleWacComponents.ts new file mode 100644 index 00000000000..6248c1ce562 --- /dev/null +++ b/packages/roosterjs-content-model/lib/editor/plugins/PastePlugin/handleWacComponents.ts @@ -0,0 +1,77 @@ +import ContentModelBeforePasteEvent from '../../../publicTypes/event/ContentModelBeforePasteEvent'; +import { matchesSelector } from 'roosterjs-editor-dom'; + +const WAC_IDENTIFY_SELECTOR = + 'ul[class^="BulletListStyle"]>.OutlineElement,ol[class^="NumberListStyle"]>.OutlineElement,span.WACImageContainer'; +const WORD_ONLINE_IDENTIFYING_SELECTOR = + 'div.ListContainerWrapper>ul[class^="BulletListStyle"],div.ListContainerWrapper>ol[class^="NumberListStyle"],span.WACImageContainer > img'; +export const LIST_CONTAINER_ELEMENT_CLASS_NAME = 'ListContainerWrapper'; +const IMAGE_CONTAINER_ELEMENT_CLASS_NAME = 'WACImageContainer'; + +export function handleWacComponents(ev: ContentModelBeforePasteEvent) { + if (!ev.domToModelOption.additionalFormatParsers) { + ev.domToModelOption.additionalFormatParsers = {}; + ev.domToModelOption.additionalFormatParsers.segment = []; + } + if (!ev.domToModelOption.processorOverride) { + ev.domToModelOption.processorOverride = {}; + } + + ev.domToModelOption.additionalFormatParsers.segment!.push((format, element, context) => { + const verticalAlign = element.style.verticalAlign; + if (verticalAlign === 'super') { + format.superOrSubScriptSequence = 'super'; + } + if (verticalAlign === 'sub') { + format.superOrSubScriptSequence = 'sub'; + } + }); + + ev.domToModelOption.processorOverride.element = (group, element, context) => { + if (matchesSelector(element, WAC_IDENTIFY_SELECTOR)) { + element.style.removeProperty('display'); + element.style.removeProperty('margin'); + } + if (element.classList.contains(LIST_CONTAINER_ELEMENT_CLASS_NAME)) { + context.elementProcessors.child(group, element, context); + return; + } + + if (element.parentElement?.classList.contains(IMAGE_CONTAINER_ELEMENT_CLASS_NAME)) { + return; + } + + context.defaultElementProcessors.element(group, element, context); + }; + + ev.domToModelOption.processorOverride.li = (group, element, context) => { + context.defaultElementProcessors.li?.(group, element, context); + const { listFormat } = context; + const listParent = listFormat.listParent; + if (listParent) { + const lastblock = listParent.blocks[listParent.blocks.length - 1]; + if (lastblock.blockType == 'BlockGroup' && lastblock.blockGroupType == 'ListItem') { + const currentLevel = lastblock.levels[lastblock.levels.length - 1]; + + // Get item level from 'data-aria-level' attribute + let level = parseInt(element.getAttribute('data-aria-level') ?? ''); + + if (level > lastblock.levels.length) { + while (level != lastblock.levels.length) { + lastblock.levels.push(currentLevel); + } + } else { + lastblock.levels.splice(level, lastblock.levels.length - 1); + lastblock.levels[level - 1] = currentLevel; + } + } + } + }; +} + +/** + * @internal + */ +export function isWordOnlineWithList(fragment: DocumentFragment): boolean { + return !!(fragment && fragment.querySelector(WORD_ONLINE_IDENTIFYING_SELECTOR)); +} diff --git a/packages/roosterjs-content-model/lib/publicTypes/event/ContentModelBeforePasteEvent.ts b/packages/roosterjs-content-model/lib/publicTypes/event/ContentModelBeforePasteEvent.ts index 2bf27345206..e5e12e70a9a 100644 --- a/packages/roosterjs-content-model/lib/publicTypes/event/ContentModelBeforePasteEvent.ts +++ b/packages/roosterjs-content-model/lib/publicTypes/event/ContentModelBeforePasteEvent.ts @@ -1,4 +1,4 @@ -import { PasteElementProcessor } from './PasteElementProcessor'; +import { DomToModelOption } from '../IContentModelEditor'; import { BeforePasteEvent, BeforePasteEventData, @@ -10,11 +10,9 @@ import { */ export interface ContentModelBeforePasteEventData extends BeforePasteEventData { /** - * Element processors to use when pasting. - * If the a processor function in the array returns true, means that the element procesing was done by the function. - * If all the processors return false, the default processor will be used instead. + * domToModel Options to use when creating the content model from the paste fragment */ - elementProcessors: PasteElementProcessor[]; + domToModelOption: Partial; } /**