From a74f5adaf4dbb698c2aae7b2c9e2964a3015d6ee Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Fri, 6 Sep 2024 07:34:28 -0700 Subject: [PATCH] RoosterJs 9.10.0 (#2786) * image-selection * fixes * safari fix * fix drag and drop * fixes * fixes * Add `` elements to default processors and use knownElementProcessor for this type of element. (#2770) * init * Use alphabet order * Dont remove the MarginTop/Bottom from lists when pasting from Word Online (#2778) * init * remove unneeded function * try fix build * Reconcile table and image selection for cache (#2714) * Improve cache * fix build * improve * add test * Cache and entity 2 * Add test * Reconcile table and image selection for cache * support reconcile entity delimiter * fix build * add test --------- Co-authored-by: Bryan Valverde U * Set segmentFormat text color to black when creating the model of the clipboard content and using Keep source formatting paste type (#2773) * init * add a link to tests and make sure it is handled correctly * fixes image in tables * Bump webpack from 5.84.1 to 5.94.0 (#2780) Bumps [webpack](https://github.com/webpack/webpack) from 5.84.1 to 5.94.0. - [Release notes](https://github.com/webpack/webpack/releases) - [Commits](https://github.com/webpack/webpack/compare/v5.84.1...v5.94.0) --- updated-dependencies: - dependency-name: webpack dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Resolve null sheet in convertInlineCss (#2784) * init * update * Update paste code to add a paragraph when the clipboard contains atleast a block element (#2777) * init * add a link to tests and make sure it is handled correctly * init * try fix build * fix build * Fix 300391: [Mail] The numbers will become a continuous sequence after typing enter at the last line of the numbering above (#2782) Co-authored-by: Bryan Valverde U * Pressing Tab inside a table should select all node contents of the next cell (#2764) * normalise position * revert * empty cell check * select using children * fix tests * fix --------- Signed-off-by: dependabot[bot] Co-authored-by: Julia Roldi (from Dev Box) Co-authored-by: Julia Roldi <87443959+juliaroldi@users.noreply.github.com> Co-authored-by: Bryan Valverde U Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Andres-CT98 <107568016+Andres-CT98@users.noreply.github.com> --- package.json | 2 +- .../paste/generatePasteOptionFromPlugins.ts | 1 + .../lib/command/paste/mergePasteContent.ts | 68 +- .../lib/command/paste/paste.ts | 2 +- .../lib/command/paste/retrieveHtmlInfo.ts | 10 +- .../lib/corePlugin/cache/CachePlugin.ts | 24 +- .../lib/corePlugin/cache/MutationType.ts | 57 + .../lib/corePlugin/cache/domIndexerImpl.ts | 200 +- .../corePlugin/cache/textMutationObserver.ts | 90 +- .../corePlugin/selection/SelectionPlugin.ts | 46 +- .../lib/editor/core/DOMHelperImpl.ts | 4 +- .../generatePasteOptionFromPluginsTest.ts | 9 + .../paste/htmlTemplates/ClipboardContent1.ts | 173 + .../command/paste/mergePasteContentTest.ts | 1422 +++++- .../test/command/paste/pasteTest.ts | 45 +- .../command/paste/retrieveHtmlInfoTest.ts | 9 + .../test/corePlugin/cache/CachePluginTest.ts | 102 +- .../corePlugin/cache/domIndexerImplTest.ts | 299 +- .../cache/textMutationObserverTest.ts | 69 +- .../selection/SelectionPluginTest.ts | 92 +- .../test/editor/core/DOMHelperImplTest.ts | 31 + .../domToModel/context/defaultProcessors.ts | 1 + .../lib/modelApi/editing/mergeModel.ts | 7 + .../lib/modelApi/selection/setSelection.ts | 32 +- .../handlers/handleBlockGroupChildren.ts | 4 + .../lib/modelToDom/handlers/handleEntity.ts | 13 +- .../domToModel/processors/brProcessorTest.ts | 2 + .../processors/entityProcessorTest.ts | 2 + .../processors/generalProcessorTest.ts | 2 + .../processors/imageProcessorTest.ts | 2 + .../processors/tableProcessorTest.ts | 2 + .../processors/textProcessorTest.ts | 6 + .../test/modelApi/editing/mergeModelTest.ts | 35 + .../modelApi/selection/setSelectionTest.ts | 67 + .../handlers/handleBlockGroupChildrenTest.ts | 29 + .../modelToDom/handlers/handleEntityTest.ts | 4 +- .../handlers/handleParagraphTest.ts | 4 + .../modelToDom/handlers/handleTableTest.ts | 2 + .../lib/edit/inputSteps/handleEnterOnList.ts | 6 +- .../lib/imageEdit/ImageEditPlugin.ts | 101 +- .../lib/imageEdit/utils/findEditingImage.ts | 37 +- .../lib/paste/WacComponents/constants.ts | 12 +- .../processPastedContentWacComponents.ts | 6 +- .../edit/inputSteps/handleEnterOnListTest.ts | 112 + .../test/imageEdit/ImageEditPluginTest.ts | 415 +- .../imageEdit/utils/findEditingImageTest.ts | 343 ++ .../test/paste/ContentModelPastePluginTest.ts | 2 +- .../test/paste/e2e/cmPasteFromExcelTest.ts | 10 + .../paste/processPastedContentFromWacTest.ts | 4058 +++++++++++------ .../lib/context/DomIndexer.ts | 16 + .../lib/event/BeforePasteEvent.ts | 5 + .../lib/parameter/DOMHelper.ts | 3 +- .../lib/parameter/MergeModelOption.ts | 5 + yarn.lock | 410 +- 54 files changed, 6578 insertions(+), 1932 deletions(-) create mode 100644 packages/roosterjs-content-model-core/lib/corePlugin/cache/MutationType.ts create mode 100644 packages/roosterjs-content-model-core/test/command/paste/htmlTemplates/ClipboardContent1.ts diff --git a/package.json b/package.json index a600126896a..ab3e23aed45 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "typedoc-plugin-remove-references": "0.0.5", "typescript": "4.4.4", "url-loader": "4.1.0", - "webpack": "5.84.1", + "webpack": "5.94.0", "webpack-cli": "3.3.11", "webpack-dev-server": "3.10.3" }, diff --git a/packages/roosterjs-content-model-core/lib/command/paste/generatePasteOptionFromPlugins.ts b/packages/roosterjs-content-model-core/lib/command/paste/generatePasteOptionFromPlugins.ts index 3b8fd283fb8..3cd712671cc 100644 --- a/packages/roosterjs-content-model-core/lib/command/paste/generatePasteOptionFromPlugins.ts +++ b/packages/roosterjs-content-model-core/lib/command/paste/generatePasteOptionFromPlugins.ts @@ -36,6 +36,7 @@ export function generatePasteOptionFromPlugins( htmlAttributes: htmlFromClipboard.metadata, pasteType: pasteType, domToModelOption, + containsBlockElements: !!htmlFromClipboard.containsBlockElements, }; return pasteType == 'asPlainText' diff --git a/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts b/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts index 1985248838c..fb0e61e966a 100644 --- a/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts +++ b/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts @@ -10,14 +10,18 @@ import { } from 'roosterjs-content-model-dom'; import type { BeforePasteEvent, - ClipboardData, CloneModelOptions, ContentModelDocument, + ContentModelSegmentFormat, IEditor, MergeModelOption, + PasteType, ReadonlyContentModelDocument, + ShallowMutableContentModelDocument, } from 'roosterjs-content-model-types'; +const BlackColor = 'rgb(0,0,0)'; + const CloneOption: CloneModelOptions = { includeCachedElement: (node, type) => (type == 'cache' ? undefined : node), }; @@ -32,12 +36,15 @@ export function cloneModelForPaste(model: ReadonlyContentModelDocument) { /** * @internal */ -export function mergePasteContent( - editor: IEditor, - eventResult: BeforePasteEvent, - clipboardData: ClipboardData -) { - const { fragment, domToModelOption, customizedMerge, pasteType } = eventResult; +export function mergePasteContent(editor: IEditor, eventResult: BeforePasteEvent) { + const { + fragment, + domToModelOption, + customizedMerge, + pasteType, + clipboardData, + containsBlockElements, + } = eventResult; editor.formatContentModel( (model, context) => { @@ -46,7 +53,6 @@ export function mergePasteContent( model.blocks = clonedModel.blocks; } - const selectedSegment = getSelectedSegments(model, true /*includeFormatHolder*/)[0]; const domToModelContext = createDomToModelContextForSanitizing( editor.getDocument(), undefined /*defaultFormat*/, @@ -54,14 +60,13 @@ export function mergePasteContent( domToModelOption ); - domToModelContext.segmentFormat = selectedSegment - ? getSegmentTextFormat(selectedSegment) - : {}; + domToModelContext.segmentFormat = getSegmentFormatForPaste(model, pasteType); const pasteModel = domToContentModel(fragment, domToModelContext); const mergeOption: MergeModelOption = { mergeFormat: pasteType == 'mergeFormat' ? 'keepSourceEmphasisFormat' : 'none', mergeTable: shouldMergeTable(pasteModel), + addParagraphAfterMergedContent: containsBlockElements, }; const insertPoint = customizedMerge @@ -72,7 +77,9 @@ export function mergePasteContent( context.newPendingFormat = { ...EmptySegmentFormat, ...model.format, - ...insertPoint.marker.format, + ...(pasteType == 'normal' && !containsBlockElements + ? getLastSegmentFormat(pasteModel) + : insertPoint.marker.format), }; } @@ -87,6 +94,27 @@ export function mergePasteContent( ); } +function getSegmentFormatForPaste( + model: ShallowMutableContentModelDocument, + pasteType: PasteType +): ContentModelSegmentFormat { + const selectedSegment = getSelectedSegments(model, true /*includeFormatHolder*/)[0]; + + if (selectedSegment) { + const result = getSegmentTextFormat(selectedSegment); + if (pasteType == 'normal') { + // When using normal paste (Keep source formatting) set the default text color to black when creating the + // Model from the clipboard content, so the elements that do not contain any text color in their style + // Are set to black. Otherwise, These segments would get the selected segments format or the default text set in the content. + result.textColor = BlackColor; + } + + return result; + } + + return {}; +} + function shouldMergeTable(pasteModel: ContentModelDocument): boolean | undefined { // If model contains a table and a paragraph element after the table with a single BR segment, remove the Paragraph after the table if ( @@ -101,3 +129,19 @@ function shouldMergeTable(pasteModel: ContentModelDocument): boolean | undefined // Only merge table when the document contain a single table. return pasteModel.blocks.length === 1 && pasteModel.blocks[0].blockType === 'Table'; } + +function getLastSegmentFormat(pasteModel: ContentModelDocument): ContentModelSegmentFormat { + if (pasteModel.blocks.length == 1) { + const [firstBlock] = pasteModel.blocks; + + if (firstBlock.blockType == 'Paragraph') { + const segment = firstBlock.segments[firstBlock.segments.length - 1]; + + return { + ...segment.format, + }; + } + } + + return {}; +} diff --git a/packages/roosterjs-content-model-core/lib/command/paste/paste.ts b/packages/roosterjs-content-model-core/lib/command/paste/paste.ts index aca1944187a..69cd08340d5 100644 --- a/packages/roosterjs-content-model-core/lib/command/paste/paste.ts +++ b/packages/roosterjs-content-model-core/lib/command/paste/paste.ts @@ -67,7 +67,7 @@ export function paste( convertInlineCss(eventResult.fragment, htmlFromClipboard.globalCssRules); // 6. Merge pasted content into main Content Model - mergePasteContent(editor, eventResult, clipboardData); + mergePasteContent(editor, eventResult); } function createDOMFromHtml( diff --git a/packages/roosterjs-content-model-core/lib/command/paste/retrieveHtmlInfo.ts b/packages/roosterjs-content-model-core/lib/command/paste/retrieveHtmlInfo.ts index 95b86bee134..bf69907bd9f 100644 --- a/packages/roosterjs-content-model-core/lib/command/paste/retrieveHtmlInfo.ts +++ b/packages/roosterjs-content-model-core/lib/command/paste/retrieveHtmlInfo.ts @@ -1,4 +1,4 @@ -import { isNodeOfType, toArray } from 'roosterjs-content-model-dom'; +import { isBlockElement, isNodeOfType, toArray } from 'roosterjs-content-model-dom'; import { retrieveCssRules } from '../createModelFromHtml/convertInlineCss'; import type { ClipboardData } from 'roosterjs-content-model-types'; import type { CssRule } from '../createModelFromHtml/convertInlineCss'; @@ -14,6 +14,7 @@ export interface HtmlFromClipboard { globalCssRules: CssRule[]; htmlBefore?: string; htmlAfter?: string; + containsBlockElements?: boolean; } /** @@ -33,6 +34,7 @@ export function retrieveHtmlInfo( ...retrieveHtmlStrings(clipboardData), globalCssRules: retrieveCssRules(doc), metadata: retrieveMetadata(doc), + containsBlockElements: checkBlockElements(doc), }; clipboardData.htmlFirstLevelChildTags = retrieveTopLevelTags(doc); @@ -96,3 +98,9 @@ function retrieveHtmlStrings( return { htmlBefore, htmlAfter }; } + +function checkBlockElements(doc: Document): boolean { + const elements = toArray(doc.body.querySelectorAll('*')); + + return elements.some(el => isNodeOfType(el, 'ELEMENT_NODE') && isBlockElement(el)); +} diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/cache/CachePlugin.ts b/packages/roosterjs-content-model-core/lib/corePlugin/cache/CachePlugin.ts index 4932076f5b2..7ad9cb7f3ec 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/cache/CachePlugin.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/cache/CachePlugin.ts @@ -2,7 +2,7 @@ import { areSameSelections } from './areSameSelections'; import { createTextMutationObserver } from './textMutationObserver'; import { DomIndexerImpl } from './domIndexerImpl'; import { updateCache } from './updateCache'; -import type { Mutation } from './textMutationObserver'; +import type { Mutation } from './MutationType'; import type { CachePluginState, IEditor, @@ -90,6 +90,19 @@ class CachePlugin implements PluginWithState { } switch (event.eventType) { + case 'logicalRootChanged': + this.invalidateCache(); + + if (this.state.textMutationObserver) { + this.state.textMutationObserver.stopObserving(); + this.state.textMutationObserver = createTextMutationObserver( + event.logicalRoot, + this.onMutation + ); + this.state.textMutationObserver.startObserving(); + } + break; + case 'keyDown': case 'input': if (!this.state.textMutationObserver) { @@ -133,6 +146,15 @@ class CachePlugin implements PluginWithState { this.updateCachedModel(this.editor, true /*forceUpdate*/); break; + case 'elementId': + const element = mutation.element; + + if (!this.state.domIndexer?.reconcileElementId(element)) { + this.invalidateCache(); + } + + break; + case 'unknown': this.invalidateCache(); break; diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/cache/MutationType.ts b/packages/roosterjs-content-model-core/lib/corePlugin/cache/MutationType.ts new file mode 100644 index 00000000000..03a8d513716 --- /dev/null +++ b/packages/roosterjs-content-model-core/lib/corePlugin/cache/MutationType.ts @@ -0,0 +1,57 @@ +/** + * @internal Type of mutations + */ +export type MutationType = + /** + * We found some change happened but we cannot handle it, so set mutation type as "unknown" + */ + | 'unknown' + /** + * Element id is changed + */ + | 'elementId' + /** + * Only text is changed + */ + | 'text' + /** + * Child list is changed + */ + | 'childList'; + +/** + * @internal + */ +export interface MutationBase { + type: T; +} + +/** + * @internal + */ +export interface UnknownMutation extends MutationBase<'unknown'> {} + +/** + * @internal + */ +export interface ElementIdMutation extends MutationBase<'elementId'> { + element: HTMLElement; +} + +/** + * @internal + */ +export interface TextMutation extends MutationBase<'text'> {} + +/** + * @internal + */ +export interface ChildListMutation extends MutationBase<'childList'> { + addedNodes: Node[]; + removedNodes: Node[]; +} + +/** + * @internal + */ +export type Mutation = UnknownMutation | ElementIdMutation | TextMutation | ChildListMutation; diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts b/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts index 96bb781bb01..3887b42dbbb 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts @@ -1,20 +1,24 @@ import { EmptySegmentFormat, + createParagraph, createSelectionMarker, createText, getObjectKeys, + isElementOfType, + isEntityDelimiter, isNodeOfType, setSelection, } from 'roosterjs-content-model-dom'; import type { CacheSelection, + ContentModelBlockGroup, ContentModelDocument, + ContentModelEntity, ContentModelParagraph, ContentModelSegment, ContentModelSegmentFormat, ContentModelSelectionMarker, ContentModelTable, - ContentModelTableRow, ContentModelText, DomIndexer, DOMSelection, @@ -34,7 +38,15 @@ export interface SegmentItem { * @internal Export for test only */ export interface TableItem { - tableRows: ContentModelTableRow[]; + table: ContentModelTable; +} + +/** + * @internal Export for test only + */ +export interface BlockEntityDelimiterItem { + entity: ContentModelEntity; + parent: ContentModelBlockGroup; } /** @@ -51,6 +63,13 @@ export interface IndexedTableElement extends HTMLTableElement { __roosterjsContentModel: TableItem; } +/** + * @internal Export for test only + */ +export interface IndexedEntityDelimiter extends Text { + __roosterjsContentModel: BlockEntityDelimiterItem; +} + /** * Context object used by DomIndexer when reconcile mutations with child list */ @@ -92,10 +111,38 @@ function isIndexedSegment(node: Node): node is IndexedSegmentNode { ); } +function isIndexedDelimiter(node: Node): node is IndexedEntityDelimiter { + const { entity, parent } = (node as IndexedEntityDelimiter).__roosterjsContentModel ?? {}; + + return ( + entity?.blockType == 'Entity' && + entity.wrapper && + parent?.blockGroupType && + Array.isArray(parent.blocks) + ); +} + function getIndexedSegmentItem(node: Node | null): SegmentItem | null { return node && isIndexedSegment(node) ? node.__roosterjsContentModel : null; } +function getIndexedTableItem(element: HTMLTableElement): TableItem | null { + const index = (element as IndexedTableElement).__roosterjsContentModel; + const table = index?.table; + + if ( + table?.blockType == 'Table' && + Array.isArray(table.rows) && + table.rows.every( + x => Array.isArray(x?.cells) && x.cells.every(y => y?.blockGroupType == 'TableCell') + ) + ) { + return index; + } else { + return null; + } +} + /** * @internal * Implementation of DomIndexer @@ -140,7 +187,12 @@ export class DomIndexerImpl implements DomIndexer { onTable(tableElement: HTMLTableElement, table: ContentModelTable) { const indexedTable = tableElement as IndexedTableElement; - indexedTable.__roosterjsContentModel = { tableRows: table.rows }; + indexedTable.__roosterjsContentModel = { table }; + } + + onBlockEntity(entity: ContentModelEntity, group: ContentModelBlockGroup) { + this.onBlockEntityDelimiter(entity.wrapper.previousSibling, entity, group); + this.onBlockEntityDelimiter(entity.wrapper.nextSibling, entity, group); } reconcileSelection( @@ -152,11 +204,10 @@ export class DomIndexerImpl implements DomIndexer { if ( oldSelection.type == 'range' && this.isCollapsed(oldSelection) && - isNodeOfType(oldSelection.start.node, 'TEXT_NODE') + isNodeOfType(oldSelection.start.node, 'TEXT_NODE') && + isIndexedSegment(oldSelection.start.node) ) { - if (isIndexedSegment(oldSelection.start.node)) { - this.reconcileTextSelection(oldSelection.start.node); - } + this.reconcileTextSelection(oldSelection.start.node); } else { setSelection(model); } @@ -164,8 +215,38 @@ export class DomIndexerImpl implements DomIndexer { switch (newSelection.type) { case 'image': + const indexedImage = getIndexedSegmentItem(newSelection.image); + const image = indexedImage?.segments[0]; + + if (image) { + image.isSelected = true; + setSelection(model, image); + + return true; + } else { + return false; + } + case 'table': - // For image and table selection, we just clear the cached model since during selecting the element id might be changed + const indexedTable = getIndexedTableItem(newSelection.table); + + if (indexedTable) { + const firstCell = + indexedTable.table.rows[newSelection.firstRow]?.cells[ + newSelection.firstColumn + ]; + const lastCell = + indexedTable.table.rows[newSelection.lastRow]?.cells[ + newSelection.lastColumn + ]; + + if (firstCell && lastCell) { + setSelection(model, firstCell, lastCell); + + return true; + } + } + return false; case 'range': @@ -182,7 +263,11 @@ export class DomIndexerImpl implements DomIndexer { delete model.hasRevertedRangeSelection; if (collapsed) { - return !!this.reconcileNodeSelection(startContainer, startOffset); + return !!this.reconcileNodeSelection( + startContainer, + startOffset, + model.format + ); } else if ( startContainer == endContainer && isNodeOfType(startContainer, 'TEXT_NODE') @@ -249,15 +334,63 @@ export class DomIndexerImpl implements DomIndexer { return canHandle && !context.pendingTextNode; } + reconcileElementId(element: HTMLElement) { + if (isElementOfType(element, 'img')) { + const indexedImg = getIndexedSegmentItem(element); + + if (indexedImg?.segments[0]?.segmentType == 'Image') { + indexedImg.segments[0].format.id = element.id; + + return true; + } else { + return false; + } + } else if (isElementOfType(element, 'table')) { + const indexedTable = getIndexedTableItem(element); + + if (indexedTable) { + indexedTable.table.format.id = element.id; + + return true; + } else { + return false; + } + } else { + return false; + } + } + + private onBlockEntityDelimiter( + node: Node | null, + entity: ContentModelEntity, + parent: ContentModelBlockGroup + ) { + if (isNodeOfType(node, 'ELEMENT_NODE') && isEntityDelimiter(node) && node.firstChild) { + const indexedDelimiter = node.firstChild as IndexedEntityDelimiter; + + indexedDelimiter.__roosterjsContentModel = { entity, parent }; + } + } + private isCollapsed(selection: RangeSelectionForCache): boolean { const { start, end } = selection; return start.node == end.node && start.offset == end.offset; } - private reconcileNodeSelection(node: Node, offset: number): Selectable | undefined { + private reconcileNodeSelection( + node: Node, + offset: number, + defaultFormat?: ContentModelSegmentFormat + ): Selectable | undefined { if (isNodeOfType(node, 'TEXT_NODE')) { - return isIndexedSegment(node) ? this.reconcileTextSelection(node, offset) : undefined; + if (isIndexedSegment(node)) { + return this.reconcileTextSelection(node, offset); + } else if (isIndexedDelimiter(node)) { + return this.reconcileDelimiterSelection(node, defaultFormat); + } else { + return undefined; + } } else if (offset >= node.childNodes.length) { return this.insertMarker(node.lastChild, true /*isAfter*/); } else { @@ -369,11 +502,56 @@ export class DomIndexerImpl implements DomIndexer { if (!this.persistCache) { delete paragraph.cachedElement; } + } else if (first?.segmentType == 'Entity' && first == last) { + const wrapper = first.wrapper; + const index = paragraph.segments.indexOf(first); + const delimiter = textNode.parentElement; + const isBefore = wrapper.previousSibling == delimiter; + const isAfter = wrapper.nextSibling == delimiter; + + if (index >= 0 && delimiter && isEntityDelimiter(delimiter) && (isBefore || isAfter)) { + const marker = createSelectionMarker( + (paragraph.segments[isAfter ? index + 1 : index - 1] ?? first).format + ); + + paragraph.segments.splice(isAfter ? index + 1 : index, 0, marker); + + selectable = marker; + } } return selectable; } + private reconcileDelimiterSelection( + node: IndexedEntityDelimiter, + defaultFormat?: ContentModelSegmentFormat + ) { + let marker: ContentModelSelectionMarker | undefined; + + const { entity, parent } = node.__roosterjsContentModel; + const index = parent.blocks.indexOf(entity); + const delimiter = node.parentElement; + const wrapper = entity.wrapper; + const isBefore = wrapper.previousSibling == delimiter; + const isAfter = wrapper.nextSibling == delimiter; + + if (index >= 0 && delimiter && isEntityDelimiter(delimiter) && (isBefore || isAfter)) { + marker = createSelectionMarker(defaultFormat); + + const para = createParagraph( + true /*isImplicit*/, + undefined /*blockFormat*/, + defaultFormat + ); + + para.segments.push(marker); + parent.blocks.splice(isBefore ? index : index + 1, 0, para); + } + + return marker; + } + private reconcileAddedNode(node: Text, context: ReconcileChildListContext): boolean { let segmentItem: SegmentItem | null = null; let index = -1; diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/cache/textMutationObserver.ts b/packages/roosterjs-content-model-core/lib/corePlugin/cache/textMutationObserver.ts index 4f833395646..d20aa100740 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/cache/textMutationObserver.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/cache/textMutationObserver.ts @@ -1,13 +1,22 @@ -import type { TextMutationObserver } from 'roosterjs-content-model-types'; +import { createDOMHelper } from '../../editor/core/DOMHelperImpl'; +import { + findClosestBlockEntityContainer, + findClosestEntityWrapper, + isNodeOfType, +} from 'roosterjs-content-model-dom'; +import type { DOMHelper, TextMutationObserver } from 'roosterjs-content-model-types'; +import type { Mutation } from './MutationType'; class TextMutationObserverImpl implements TextMutationObserver { private observer: MutationObserver; + private domHelper: DOMHelper; constructor( private contentDiv: HTMLDivElement, private onMutation: (mutation: Mutation) => void ) { this.observer = new MutationObserver(this.onMutationInternal); + this.domHelper = createDOMHelper(contentDiv); } startObserving() { @@ -39,14 +48,40 @@ class TextMutationObserverImpl implements TextMutationObserver { let removedNodes: Node[] = []; let reconcileText = false; + const ignoredNodes = new Set(); + const includedNodes = new Set(); + for (let i = 0; i < mutations.length && canHandle; i++) { const mutation = mutations[i]; + const target = mutation.target; + + if (ignoredNodes.has(target)) { + continue; + } else if (!includedNodes.has(target)) { + if ( + findClosestEntityWrapper(target, this.domHelper) || + findClosestBlockEntityContainer(target, this.domHelper) + ) { + ignoredNodes.add(target); + + continue; + } else { + includedNodes.add(target); + } + } switch (mutation.type) { case 'attributes': - if (mutation.target != this.contentDiv) { - // We cannot handle attributes changes on editor content for now - canHandle = false; + if (this.domHelper.isNodeInEditor(target, true /*excludingSelf*/)) { + if ( + mutation.attributeName == 'id' && + isNodeOfType(target, 'ELEMENT_NODE') + ) { + this.onMutation({ type: 'elementId', element: target }); + } else { + // We cannot handle attributes changes on editor content for now + canHandle = false; + } } break; @@ -94,53 +129,6 @@ class TextMutationObserverImpl implements TextMutationObserver { }; } -/** - * @internal Type of mutations - */ -export type MutationType = - /** - * We found some change happened but we cannot handle it, so set mutation type as "unknown" - */ - | 'unknown' - /** - * Only text is changed - */ - | 'text' - /** - * Child list is changed - */ - | 'childList'; - -/** - * @internal - */ -export interface MutationBase { - type: T; -} - -/** - * @internal - */ -export interface UnknownMutation extends MutationBase<'unknown'> {} - -/** - * @internal - */ -export interface TextMutation extends MutationBase<'text'> {} - -/** - * @internal - */ -export interface ChildListMutation extends MutationBase<'childList'> { - addedNodes: Node[]; - removedNodes: Node[]; -} - -/** - * @internal - */ -export type Mutation = UnknownMutation | TextMutation | ChildListMutation; - /** * @internal */ diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts b/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts index a4ef7d713aa..420514d1466 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts @@ -21,7 +21,6 @@ import type { ParsedTable, TableSelectionInfo, TableCellCoordinate, - RangeSelection, MouseUpEvent, } from 'roosterjs-content-model-types'; @@ -362,6 +361,23 @@ class SelectionPlugin implements PluginWithState { this.selectBeforeOrAfterElement(editor, selection.image); } } + + if ( + (isModifierKey(rawEvent) || rawEvent.shiftKey) && + selection.image && + !this.isSafari + ) { + const range = selection.image.ownerDocument.createRange(); + range.selectNode(selection.image); + this.setDOMSelection( + { + type: 'range', + range, + isReverted: false, + }, + null /* tableSelection */ + ); + } break; case 'range': @@ -543,8 +559,16 @@ class SelectionPlugin implements PluginWithState { selectAll?: boolean ) { const range = editor.getDocument().createRange(); - if (selectAll) { - range.selectNodeContents(cell); + if (selectAll && cell.firstChild && cell.lastChild) { + const cellStart = cell.firstChild; + const cellEnd = cell.lastChild; + // Get first deepest editable position in the cell + const posStart = normalizePos(cellStart, 0); + // Get last deepest editable position in the cell + const posEnd = normalizePos(cellEnd, cellEnd.childNodes.length); + + range.setStart(posStart.node, posStart.offset); + range.setEnd(posEnd.node, posEnd.offset); } else { // Get deepest editable position in the cell const { node, offset } = normalizePos(cell, nodeOffset); @@ -696,7 +720,6 @@ class SelectionPlugin implements PluginWithState { if (this.isSafari) { this.state.selection = newSelection; } - this.trySelectSingleImage(newSelection); } } }; @@ -768,21 +791,6 @@ class SelectionPlugin implements PluginWithState { this.state.mouseDisposer = undefined; } } - - private trySelectSingleImage(selection: RangeSelection) { - if (!selection.range.collapsed) { - const image = isSingleImageInSelection(selection.range); - if (image) { - this.setDOMSelection( - { - type: 'image', - image: image, - }, - null /*tableSelection*/ - ); - } - } - } } /** diff --git a/packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts b/packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts index a638dc10027..a4b9a49474a 100644 --- a/packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts +++ b/packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts @@ -12,8 +12,8 @@ class DOMHelperImpl implements DOMHelper { return this.contentDiv.textContent || ''; } - isNodeInEditor(node: Node): boolean { - return this.contentDiv.contains(node); + isNodeInEditor(node: Node, excludeRoot?: boolean): boolean { + return excludeRoot && node == this.contentDiv ? false : this.contentDiv.contains(node); } calculateZoomScale(): number { diff --git a/packages/roosterjs-content-model-core/test/command/paste/generatePasteOptionFromPluginsTest.ts b/packages/roosterjs-content-model-core/test/command/paste/generatePasteOptionFromPluginsTest.ts index 185b30bf072..7c434d19259 100644 --- a/packages/roosterjs-content-model-core/test/command/paste/generatePasteOptionFromPluginsTest.ts +++ b/packages/roosterjs-content-model-core/test/command/paste/generatePasteOptionFromPluginsTest.ts @@ -55,6 +55,7 @@ describe('generatePasteOptionFromPlugins', () => { htmlBefore, htmlAfter, htmlAttributes: mockedMetadata, + containsBlockElements: false, } as any); expect(triggerPluginEventSpy).toHaveBeenCalledTimes(1); expect(originalEvent).toEqual({ @@ -74,6 +75,7 @@ describe('generatePasteOptionFromPlugins', () => { styleSanitizers: {}, attributeSanitizers: {}, }, + containsBlockElements: false, }); expect(triggerPluginEventSpy).toHaveBeenCalledWith( 'beforePaste', @@ -86,6 +88,7 @@ describe('generatePasteOptionFromPlugins', () => { htmlAttributes: mockedMetadata, pasteType: 'TypeResult', domToModelOption: 'OptionResult', + containsBlockElements: false, }, true ); @@ -126,6 +129,7 @@ describe('generatePasteOptionFromPlugins', () => { htmlBefore, htmlAfter, htmlAttributes: mockedMetadata, + containsBlockElements: false, } as any); expect(triggerPluginEventSpy).toHaveBeenCalledTimes(1); expect(triggerPluginEventSpy).toHaveBeenCalledWith( @@ -140,6 +144,7 @@ describe('generatePasteOptionFromPlugins', () => { pasteType: 'TypeResult', domToModelOption: 'OptionResult', customizedMerge: mockedCustomizedMerge, + containsBlockElements: false, }, true ); @@ -174,6 +179,7 @@ describe('generatePasteOptionFromPlugins', () => { htmlBefore: '', htmlAfter: '', htmlAttributes: mockedMetadata, + containsBlockElements: false, } as any); expect(triggerPluginEventSpy).toHaveBeenCalledTimes(1); expect(triggerPluginEventSpy).toHaveBeenCalledWith( @@ -187,6 +193,7 @@ describe('generatePasteOptionFromPlugins', () => { htmlAttributes: mockedMetadata, pasteType: 'TypeResult', domToModelOption: 'OptionResult', + containsBlockElements: false, }, true ); @@ -207,6 +214,7 @@ describe('generatePasteOptionFromPlugins', () => { styleSanitizers: {}, attributeSanitizers: {}, }, + containsBlockElements: false, }); }); @@ -244,6 +252,7 @@ describe('generatePasteOptionFromPlugins', () => { htmlBefore, htmlAfter, htmlAttributes: mockedMetadata, + containsBlockElements: false, }); expect(triggerPluginEventSpy).toHaveBeenCalledTimes(0); }); diff --git a/packages/roosterjs-content-model-core/test/command/paste/htmlTemplates/ClipboardContent1.ts b/packages/roosterjs-content-model-core/test/command/paste/htmlTemplates/ClipboardContent1.ts new file mode 100644 index 00000000000..d943eb2fdbb --- /dev/null +++ b/packages/roosterjs-content-model-core/test/command/paste/htmlTemplates/ClipboardContent1.ts @@ -0,0 +1,173 @@ +export const template: Readonly = ` + + + + + + + + + + + + + + +

+ Red bold +

+ +

+ Red italic +

+ +

+ Red underline +

+ +

+ Unformatted line +

+ +

+ Text underlink +

+ + + + +`; + +export const inlineTemplate: Readonly = ` + + + Inline text +`; diff --git a/packages/roosterjs-content-model-core/test/command/paste/mergePasteContentTest.ts b/packages/roosterjs-content-model-core/test/command/paste/mergePasteContentTest.ts index a7456ff4fcf..71efb5798c0 100644 --- a/packages/roosterjs-content-model-core/test/command/paste/mergePasteContentTest.ts +++ b/packages/roosterjs-content-model-core/test/command/paste/mergePasteContentTest.ts @@ -1,8 +1,19 @@ import * as createDomToModelContextForSanitizing from '../../../lib/command/createModelFromHtml/createDomToModelContextForSanitizing'; import * as domToContentModel from 'roosterjs-content-model-dom/lib/domToModel/domToContentModel'; +import * as getSegmentTextFormatFile from 'roosterjs-content-model-dom/lib/modelApi/editing/getSegmentTextFormat'; import * as mergeModelFile from 'roosterjs-content-model-dom/lib/modelApi/editing/mergeModel'; -import { createContentModelDocument } from 'roosterjs-content-model-dom'; +import { createPasteFragment } from '../../../lib/command/paste/createPasteFragment'; +import { inlineTemplate, template } from './htmlTemplates/ClipboardContent1'; import { mergePasteContent } from '../../../lib/command/paste/mergePasteContent'; + +import { + addBlock, + createContentModelDocument, + createParagraph, + createSelectionMarker, + createText, + moveChildNodes, +} from 'roosterjs-content-model-dom'; import { ContentModelDocument, ContentModelFormatter, @@ -153,15 +164,17 @@ describe('mergePasteContent', () => { const eventResult = { pasteType: 'normal', domToModelOption: { additionalAllowedTags: [] }, + clipboardData: mockedClipboard, } as any; - mergePasteContent(editor, eventResult, mockedClipboard); + mergePasteContent(editor, eventResult); expect(formatContentModel).toHaveBeenCalledTimes(1); expect(formatResult).toBeTrue(); expect(mergeModelFile.mergeModel).toHaveBeenCalledWith(sourceModel, pasteModel, context, { mergeFormat: 'none', mergeTable: true, + addParagraphAfterMergedContent: undefined, }); expect(context).toEqual({ newEntities: [], @@ -258,9 +271,10 @@ describe('mergePasteContent', () => { pasteType: 'normal', domToModelOption: { additionalAllowedTags: [] }, customizedMerge, + clipboardData: mockedClipboard, } as any; - mergePasteContent(editor, eventResult, mockedClipboard); + mergePasteContent(editor, eventResult); expect(formatContentModel).toHaveBeenCalledTimes(1); expect(formatResult).toBeTrue(); @@ -279,9 +293,10 @@ describe('mergePasteContent', () => { const eventResult = { pasteType: 'mergeFormat', domToModelOption: { additionalAllowedTags: [] }, + clipboardData: mockedClipboard, } as any; - mergePasteContent(editor, eventResult, mockedClipboard); + mergePasteContent(editor, eventResult); expect(formatContentModel).toHaveBeenCalledTimes(1); expect(formatResult).toBeTrue(); @@ -292,6 +307,7 @@ describe('mergePasteContent', () => { { mergeFormat: 'keepSourceEmphasisFormat', mergeTable: false, + addParagraphAfterMergedContent: undefined, } ); }); @@ -361,14 +377,11 @@ describe('mergePasteContent', () => { }, }); - mergePasteContent( - editor, - { - fragment: mockedFragment, - domToModelOption: mockedDefaultDomToModelOptions, - } as any, - mockedClipboard - ); + mergePasteContent(editor, { + fragment: mockedFragment, + domToModelOption: mockedDefaultDomToModelOptions, + clipboardData: mockedClipboard, + } as any); expect(formatContentModel).toHaveBeenCalledTimes(1); expect(formatResult).toBeTrue(); @@ -394,6 +407,7 @@ describe('mergePasteContent', () => { expect(mergeModelSpy).toHaveBeenCalledWith(sourceModel, pasteModel, context, { mergeFormat: 'none', mergeTable: false, + addParagraphAfterMergedContent: undefined, }); expect(createDomToModelContextSpy).toHaveBeenCalledWith( document, @@ -421,9 +435,11 @@ describe('mergePasteContent', () => { const eventResult = { pasteType: 'normal', domToModelOption: { additionalAllowedTags: [] }, + clipboardData: mockedClipboard, + containsBlockElements: true, } as any; - mergePasteContent(editor, eventResult, mockedClipboard); + mergePasteContent(editor, eventResult); expect(formatContentModel).toHaveBeenCalledTimes(1); expect(formatResult).toBeTrue(); @@ -446,4 +462,1384 @@ describe('mergePasteContent', () => { }, }); }); + + it('Merge paste content | Paste Type = normal | Make undefined text color equal to black', () => { + const html = new DOMParser().parseFromString(template, 'text/html'); + const fragment = document.createDocumentFragment(); + moveChildNodes(fragment, html.body); + + spyOn(mergeModelFile, 'mergeModel').and.callThrough(); + spyOn(getSegmentTextFormatFile, 'getSegmentTextFormat').and.returnValue({ + fontSize: '14px', + textColor: 'white', + }); + sourceModel = createContentModelDocument(); + const para = createParagraph(); + const marker = createSelectionMarker(); + marker.format = { + fontSize: '14px', + textColor: 'white', + }; + para.segments.push(marker); + addBlock(sourceModel, para); + + mergePasteContent(editor, { + fragment, + containsBlockElements: true, + domToModelOption: {}, + pasteType: 'normal', + clipboardData: mockedClipboard, + }); + + expect(mergeModelFile.mergeModel).toHaveBeenCalledWith( + sourceModel, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red bold', + format: { + fontSize: '28pt', + textColor: 'rgb(192, 0, 0)', + fontWeight: 'bold', + lineHeight: '115%', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'rgb(0,0,0)', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red italic', + format: { + fontSize: '28pt', + textColor: 'rgb(192, 0, 0)', + italic: true, + lineHeight: '115%', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'rgb(0,0,0)', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red underline', + format: { + fontSize: '28pt', + textColor: 'rgb(192, 0, 0)', + underline: true, + lineHeight: '115%', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'rgb(0,0,0)', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Unformatted line', + format: { + fontSize: '28pt', + textColor: 'rgb(0,0,0)', + lineHeight: '115%', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'rgb(0,0,0)', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Text underlink', + format: { + fontSize: '28pt', + textColor: 'rgb(0,0,0)', + underline: true, + lineHeight: '115%', + }, + link: { + format: { + underline: true, + href: 'https://github.com/microsoft/roosterjs', + }, + dataset: {}, + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { fontSize: '14px', textColor: 'rgb(0,0,0)' }, + }, + ], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + { + blockType: 'Paragraph', + segments: [], + format: {}, + segmentFormat: { fontSize: '14px', textColor: 'white' }, + }, + ], + }, + { + newEntities: [], + deletedEntities: [], + newImages: [], + newPendingFormat: { + backgroundColor: '', + fontFamily: '', + fontSize: '14px', + fontWeight: '', + italic: false, + letterSpacing: '', + lineHeight: '', + strikethrough: false, + superOrSubScriptSequence: '', + textColor: 'white', + underline: false, + }, + }, + { + mergeFormat: 'none', + mergeTable: false, + addParagraphAfterMergedContent: true, + } + ); + + expect(sourceModel).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red bold', + format: { + fontSize: '28pt', + textColor: 'rgb(192, 0, 0)', + fontWeight: 'bold', + lineHeight: '115%', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'rgb(0,0,0)', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red italic', + format: { + fontSize: '28pt', + textColor: 'rgb(192, 0, 0)', + italic: true, + lineHeight: '115%', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'rgb(0,0,0)', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red underline', + format: { + fontSize: '28pt', + textColor: 'rgb(192, 0, 0)', + underline: true, + lineHeight: '115%', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'rgb(0,0,0)', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Unformatted line', + format: { + fontSize: '28pt', + textColor: 'rgb(0,0,0)', + lineHeight: '115%', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'rgb(0,0,0)', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Text underlink', + format: { + fontSize: '28pt', + textColor: 'rgb(0,0,0)', + underline: true, + lineHeight: '115%', + }, + link: { + format: { + underline: true, + href: 'https://github.com/microsoft/roosterjs', + }, + dataset: {}, + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { fontSize: '14px', textColor: 'rgb(0,0,0)' }, + }, + ], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + isSelected: true, + format: { fontSize: '14px', textColor: 'white' }, + }, + { + segmentType: 'Br', + format: { fontSize: '14px', textColor: 'white' }, + }, + ], + format: {}, + segmentFormat: { fontSize: '14px', textColor: 'white' }, + }, + ], + }); + }); + + it('Merge paste content | Paste Type = mergeFormat | Use current format', () => { + const html = new DOMParser().parseFromString(template, 'text/html'); + const fragment = document.createDocumentFragment(); + moveChildNodes(fragment, html.body); + + spyOn(mergeModelFile, 'mergeModel').and.callThrough(); + spyOn(getSegmentTextFormatFile, 'getSegmentTextFormat').and.returnValue({ + fontSize: '14px', + textColor: 'white', + }); + sourceModel = createContentModelDocument(); + const para = createParagraph(); + const marker = createSelectionMarker(); + marker.format = { + fontSize: '14px', + textColor: 'white', + }; + para.segments.push(marker); + sourceModel.blocks.push(para); + + mergePasteContent(editor, { + fragment, + domToModelOption: {}, + pasteType: 'mergeFormat', + clipboardData: mockedClipboard, + } as any); + + expect(mergeModelFile.mergeModel).toHaveBeenCalledWith( + sourceModel, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red bold', + format: { + fontSize: '14px', + textColor: 'white', + fontWeight: 'bold', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red italic', + format: { + fontSize: '14px', + textColor: 'white', + italic: true, + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red underline', + format: { + fontSize: '14px', + textColor: 'white', + underline: true, + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Unformatted line', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Text underlink', + format: { + fontSize: '14px', + textColor: 'white', + underline: true, + }, + link: { + format: { + href: 'https://github.com/microsoft/roosterjs', + underline: true, + }, + dataset: {}, + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + }, + ], + }, + { + newEntities: [], + deletedEntities: [], + newImages: [], + newPendingFormat: { + backgroundColor: '', + fontFamily: '', + fontSize: '14px', + fontWeight: '', + italic: false, + letterSpacing: '', + lineHeight: '', + strikethrough: false, + superOrSubScriptSequence: '', + textColor: 'white', + underline: false, + }, + }, + { + mergeFormat: 'keepSourceEmphasisFormat', + mergeTable: false, + addParagraphAfterMergedContent: undefined, + } + ); + + expect(sourceModel).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red bold', + format: { + fontSize: '14px', + textColor: 'white', + fontWeight: 'bold', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + segmentFormat: { + fontSize: '14px', + textColor: 'white', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red italic', + format: { + fontSize: '14px', + textColor: 'white', + italic: true, + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + segmentFormat: { + fontSize: '14px', + textColor: 'white', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red underline', + format: { + fontSize: '14px', + textColor: 'white', + underline: true, + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + segmentFormat: { + fontSize: '14px', + textColor: 'white', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Unformatted line', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + segmentFormat: { + fontSize: '14px', + textColor: 'white', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Text underlink', + format: { + fontSize: '14px', + textColor: 'white', + underline: true, + }, + link: { + format: { + href: 'https://github.com/microsoft/roosterjs', + underline: true, + }, + dataset: {}, + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + segmentFormat: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + }); + }); + + it('Merge paste content | Paste Type = asPlainText | Use current format', () => { + const fragment = createPasteFragment( + document, + { text: 'Red bold\r\nRed italic\r\nRed underline\r\nUnformatted line\r\n' } as any, + 'asPlainText', + document.body + ); + + spyOn(mergeModelFile, 'mergeModel').and.callThrough(); + spyOn(getSegmentTextFormatFile, 'getSegmentTextFormat').and.returnValue({ + fontSize: '14px', + textColor: 'white', + }); + sourceModel = createContentModelDocument(); + const para = createParagraph(); + const marker = createSelectionMarker(); + marker.format = { + fontSize: '14px', + textColor: 'white', + }; + para.segments.push(marker); + sourceModel.blocks.push(para); + + mergePasteContent(editor, { + fragment, + domToModelOption: {}, + pasteType: 'asPlainText', + clipboardData: mockedClipboard, + } as any); + + expect(mergeModelFile.mergeModel).toHaveBeenCalledWith( + sourceModel, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red bold', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: {}, + isImplicit: true, + segmentFormat: { + fontSize: '14px', + textColor: 'white', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red italic', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: {}, + segmentFormat: { + fontSize: '14px', + textColor: 'white', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red underline', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: {}, + segmentFormat: { + fontSize: '14px', + textColor: 'white', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Unformatted line', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: {}, + segmentFormat: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + }, + { + newEntities: [], + deletedEntities: [], + newImages: [], + newPendingFormat: { + backgroundColor: '', + fontFamily: '', + fontSize: '14px', + fontWeight: '', + italic: false, + letterSpacing: '', + lineHeight: '', + strikethrough: false, + superOrSubScriptSequence: '', + textColor: 'white', + underline: false, + }, + }, + { + mergeFormat: 'none', + mergeTable: false, + addParagraphAfterMergedContent: undefined, + } + ); + + expect(sourceModel).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red bold', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: {}, + segmentFormat: { + fontSize: '14px', + textColor: 'white', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red italic', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: {}, + segmentFormat: { + fontSize: '14px', + textColor: 'white', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red underline', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: {}, + segmentFormat: { + fontSize: '14px', + textColor: 'white', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Unformatted line', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: {}, + segmentFormat: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + }); + }); + + it('Merge paste content | Paste Type = normal | Paste content inline', () => { + const html = new DOMParser().parseFromString(inlineTemplate, 'text/html'); + const fragment = document.createDocumentFragment(); + moveChildNodes(fragment, html.body); + + spyOn(mergeModelFile, 'mergeModel').and.callThrough(); + spyOn(getSegmentTextFormatFile, 'getSegmentTextFormat').and.returnValue({ + fontSize: '14px', + textColor: 'white', + }); + sourceModel = createContentModelDocument({ + fontFamily: 'Aptos', + fontSize: '10pt', + textColor: 'blue', + }); + const para = createParagraph(undefined, undefined, { + fontFamily: 'Aptos', + fontSize: '10pt', + textColor: 'blue', + }); + const marker = createSelectionMarker(); + marker.format = { + fontSize: '14px', + textColor: 'white', + }; + para.segments.push(createText('Text in source'), marker); + addBlock(sourceModel, para); + + mergePasteContent(editor, { + fragment, + containsBlockElements: false, + domToModelOption: {}, + pasteType: 'normal', + clipboardData: mockedClipboard, + }); + + expect(mergeModelFile.mergeModel).toHaveBeenCalledWith( + sourceModel, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Inline text', + format: { + fontSize: '14px', + textColor: 'rgb(0,0,0)', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'rgb(0,0,0)', + }, + }, + ], + format: {}, + isImplicit: true, + segmentFormat: { + fontSize: '14px', + textColor: 'rgb(0,0,0)', + }, + }, + ], + }, + { + newEntities: [], + deletedEntities: [], + newImages: [], + newPendingFormat: { + backgroundColor: '', + fontFamily: 'Aptos', + fontSize: '14px', + fontWeight: '', + italic: false, + letterSpacing: '', + lineHeight: '', + strikethrough: false, + superOrSubScriptSequence: '', + textColor: 'rgb(0,0,0)', + underline: false, + }, + }, + { + mergeFormat: 'none', + mergeTable: false, + addParagraphAfterMergedContent: false, + } + ); + + expect(sourceModel).toEqual({ + blockGroupType: 'Document', + format: { fontFamily: 'Aptos', fontSize: '10pt', textColor: 'blue' }, + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Text in source', + format: { fontFamily: 'Aptos', fontSize: '10pt', textColor: 'blue' }, + }, + { + segmentType: 'Text', + text: 'Inline text', + format: { + fontSize: '14px', + textColor: 'rgb(0,0,0)', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'rgb(0,0,0)', + }, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: { + fontSize: '14px', + textColor: 'white', + fontFamily: 'Aptos', + }, + }, + ], + format: {}, + }, + ], + }); + }); + + it('Merge paste content | Paste Type = mergeFormat | Paste content inline', () => { + const html = new DOMParser().parseFromString(inlineTemplate, 'text/html'); + const fragment = document.createDocumentFragment(); + moveChildNodes(fragment, html.body); + + spyOn(mergeModelFile, 'mergeModel').and.callThrough(); + spyOn(getSegmentTextFormatFile, 'getSegmentTextFormat').and.returnValue({ + fontSize: '14px', + textColor: 'white', + }); + sourceModel = createContentModelDocument({ + fontFamily: 'Aptos', + fontSize: '10pt', + textColor: 'blue', + }); + const para = createParagraph(undefined, undefined, { + fontFamily: 'Aptos', + fontSize: '10pt', + textColor: 'blue', + }); + const marker = createSelectionMarker(); + marker.format = { + fontSize: '14px', + textColor: 'white', + }; + para.segments.push(createText('Text in source'), marker); + addBlock(sourceModel, para); + + mergePasteContent(editor, { + fragment, + containsBlockElements: false, + domToModelOption: {}, + pasteType: 'mergeFormat', + clipboardData: mockedClipboard, + }); + + expect(mergeModelFile.mergeModel).toHaveBeenCalledWith( + sourceModel, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Inline text', + format: { + fontFamily: 'Aptos', + fontSize: '14px', + textColor: 'white', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontFamily: 'Aptos', + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: {}, + isImplicit: true, + segmentFormat: { fontSize: '14px', textColor: 'white' }, + }, + ], + }, + { + newEntities: [], + deletedEntities: [], + newImages: [], + newPendingFormat: { + backgroundColor: '', + fontFamily: 'Aptos', + fontSize: '14px', + fontWeight: '', + italic: false, + letterSpacing: '', + lineHeight: '', + strikethrough: false, + superOrSubScriptSequence: '', + textColor: 'white', + underline: false, + }, + }, + { + mergeFormat: 'keepSourceEmphasisFormat', + mergeTable: false, + addParagraphAfterMergedContent: false, + } + ); + + expect(sourceModel).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'Text', text: 'Text in source', format: {} }, + { + segmentType: 'Text', + text: 'Inline text', + format: { + fontFamily: 'Aptos', + fontSize: '14px', + textColor: 'white', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontFamily: 'Aptos', + fontSize: '14px', + textColor: 'white', + }, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: { fontSize: '14px', textColor: 'white' }, + }, + ], + format: {}, + segmentFormat: { fontFamily: 'Aptos', fontSize: '10pt', textColor: 'blue' }, + }, + ], + format: { fontFamily: 'Aptos', fontSize: '10pt', textColor: 'blue' }, + }); + }); + + it('Merge paste content | Paste Type = mergeFormat | Paste content inline', () => { + const fragment = createPasteFragment( + document, + { text: 'Inline text\r\n' } as any, + 'asPlainText', + document.body + ); + + spyOn(mergeModelFile, 'mergeModel').and.callThrough(); + spyOn(getSegmentTextFormatFile, 'getSegmentTextFormat').and.returnValue({ + fontSize: '14px', + textColor: 'white', + }); + sourceModel = createContentModelDocument({ + fontFamily: 'Aptos', + fontSize: '10pt', + textColor: 'blue', + }); + const para = createParagraph(undefined, undefined, { + fontFamily: 'Aptos', + fontSize: '10pt', + textColor: 'blue', + }); + const marker = createSelectionMarker(); + marker.format = { + fontSize: '14px', + textColor: 'white', + }; + para.segments.push(createText('Text in source'), marker); + addBlock(sourceModel, para); + + mergePasteContent(editor, { + fragment, + containsBlockElements: false, + domToModelOption: {}, + pasteType: 'asPlainText', + clipboardData: mockedClipboard, + }); + + expect(mergeModelFile.mergeModel).toHaveBeenCalledWith( + sourceModel, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Inline text', + format: { fontSize: '14px', textColor: 'white' }, + }, + { segmentType: 'Br', format: { fontSize: '14px', textColor: 'white' } }, + ], + format: {}, + isImplicit: true, + segmentFormat: { fontSize: '14px', textColor: 'white' }, + }, + ], + }, + { + newEntities: [], + deletedEntities: [], + newImages: [], + newPendingFormat: { + backgroundColor: '', + fontFamily: 'Aptos', + fontSize: '14px', + fontWeight: '', + italic: false, + letterSpacing: '', + lineHeight: '', + strikethrough: false, + superOrSubScriptSequence: '', + textColor: 'white', + underline: false, + }, + }, + { mergeFormat: 'none', mergeTable: false, addParagraphAfterMergedContent: false } + ); + + expect(sourceModel).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Text in source', + format: { + fontFamily: 'Aptos', + fontSize: '10pt', + textColor: 'blue', + }, + }, + { + segmentType: 'Text', + text: 'Inline text', + format: { fontSize: '14px', textColor: 'white' }, + }, + { segmentType: 'Br', format: { fontSize: '14px', textColor: 'white' } }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: { + fontFamily: 'Aptos', + fontSize: '14px', + textColor: 'white', + }, + }, + { + segmentType: 'Br', + format: { + fontFamily: 'Aptos', + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: {}, + }, + ], + format: { fontFamily: 'Aptos', fontSize: '10pt', textColor: 'blue' }, + }); + }); }); diff --git a/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts b/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts index 5fbed20a037..a6aab524bb7 100644 --- a/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts +++ b/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts @@ -178,7 +178,7 @@ describe('paste with content model & paste plugin', () => { paste(editor!, clipboardData); expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(2); - expect(addParserF.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 6); + expect(addParserF.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 7); expect(WacComponents.processPastedContentWacComponents).toHaveBeenCalledTimes(1); }); @@ -353,11 +353,22 @@ describe('Paste with clipboardData', () => { textColor: 'rgb(0, 0, 0)', }, }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + { + segments: [ { - segmentType: 'SelectionMarker', isSelected: true, + segmentType: 'SelectionMarker', format: { - textColor: '', backgroundColor: '', fontFamily: '', fontSize: '', @@ -367,18 +378,14 @@ describe('Paste with clipboardData', () => { lineHeight: '', strikethrough: false, superOrSubScriptSequence: '', + textColor: '', underline: false, }, }, + { segmentType: 'Br', format: {} }, ], - format: { - marginTop: '1em', - marginBottom: '1em', - }, - decorator: { - tagName: 'p', - format: {}, - }, + blockType: 'Paragraph', + format: {}, }, ], format: {}, @@ -398,7 +405,13 @@ describe('Paste with clipboardData', () => { blocks: [ { segments: [ - { text: 'Link', segmentType: 'Text', format: {} }, + { + text: 'Link', + segmentType: 'Text', + format: { + textColor: 'rgb(0, 0, 0)', + }, + }, { isSelected: true, segmentType: 'SelectionMarker', @@ -412,12 +425,13 @@ describe('Paste with clipboardData', () => { lineHeight: '', strikethrough: false, superOrSubScriptSequence: '', - textColor: '', + textColor: 'rgb(0,0,0)', underline: false, }, }, ], blockType: 'Paragraph', + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, format: {}, }, ], @@ -441,7 +455,7 @@ describe('Paste with clipboardData', () => { { text: 'Link', segmentType: 'Text', - format: {}, + format: { textColor: 'rgb(0, 0, 0)' }, link: { format: { underline: true, @@ -463,7 +477,7 @@ describe('Paste with clipboardData', () => { lineHeight: '', strikethrough: false, superOrSubScriptSequence: '', - textColor: '', + textColor: 'rgb(0,0,0)', underline: false, }, link: { @@ -476,6 +490,7 @@ describe('Paste with clipboardData', () => { }, ], blockType: 'Paragraph', + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, format: {}, }, ], diff --git a/packages/roosterjs-content-model-core/test/command/paste/retrieveHtmlInfoTest.ts b/packages/roosterjs-content-model-core/test/command/paste/retrieveHtmlInfoTest.ts index 542a9664180..2e6938eff6a 100644 --- a/packages/roosterjs-content-model-core/test/command/paste/retrieveHtmlInfoTest.ts +++ b/packages/roosterjs-content-model-core/test/command/paste/retrieveHtmlInfoTest.ts @@ -44,6 +44,7 @@ describe('retrieveHtmlInfo', () => { htmlAfter: '', globalCssRules: [], metadata: {}, + containsBlockElements: false, }, { htmlFirstLevelChildTags: [], @@ -61,6 +62,7 @@ describe('retrieveHtmlInfo', () => { htmlAfter: '', globalCssRules: [], metadata: {}, + containsBlockElements: false, }, { htmlFirstLevelChildTags: [''], @@ -78,6 +80,7 @@ describe('retrieveHtmlInfo', () => { htmlAfter: '', globalCssRules: [], metadata: {}, + containsBlockElements: true, }, { htmlFirstLevelChildTags: ['DIV'], @@ -95,6 +98,7 @@ describe('retrieveHtmlInfo', () => { htmlAfter: '', globalCssRules: [], metadata: {}, + containsBlockElements: true, }, { htmlFirstLevelChildTags: ['', 'DIV', 'SPAN', ''], @@ -112,6 +116,7 @@ describe('retrieveHtmlInfo', () => { htmlAfter: '', globalCssRules: [], metadata: {}, + containsBlockElements: true, }, { htmlFirstLevelChildTags: ['DIV'], @@ -129,6 +134,7 @@ describe('retrieveHtmlInfo', () => { htmlAfter: '', globalCssRules: [], metadata: {}, + containsBlockElements: true, }, { htmlFirstLevelChildTags: ['DIV'], @@ -146,6 +152,7 @@ describe('retrieveHtmlInfo', () => { htmlAfter: '', globalCssRules: [], metadata: {}, + containsBlockElements: true, }, { htmlFirstLevelChildTags: ['DIV'], @@ -163,6 +170,7 @@ describe('retrieveHtmlInfo', () => { htmlAfter: '', globalCssRules: [], metadata: { a: 'b', 'c:d': 'e', f: 'g', h: 'i' }, + containsBlockElements: true, }, { htmlFirstLevelChildTags: ['DIV'], @@ -181,6 +189,7 @@ describe('retrieveHtmlInfo', () => { htmlAfter: '', globalCssRules: [], metadata: {}, + containsBlockElements: true, }, { htmlFirstLevelChildTags: ['DIV'], diff --git a/packages/roosterjs-content-model-core/test/corePlugin/cache/CachePluginTest.ts b/packages/roosterjs-content-model-core/test/corePlugin/cache/CachePluginTest.ts index c67de4cec50..dc15758c9cd 100644 --- a/packages/roosterjs-content-model-core/test/corePlugin/cache/CachePluginTest.ts +++ b/packages/roosterjs-content-model-core/test/corePlugin/cache/CachePluginTest.ts @@ -1,6 +1,7 @@ import * as textMutationObserver from '../../../lib/corePlugin/cache/textMutationObserver'; import { createCachePlugin } from '../../../lib/corePlugin/cache/CachePlugin'; import { DomIndexerImpl } from '../../../lib/corePlugin/cache/domIndexerImpl'; +import { Mutation } from '../../../lib/corePlugin/cache/MutationType'; import { CachePluginState, DomIndexer, @@ -82,6 +83,52 @@ describe('CachePlugin', () => { }); }); + describe('logicalRootChanged event', () => { + it('Change to a new node', () => { + const startObservingSpy = jasmine.createSpy('startObserving'); + const stopObservingSpy = jasmine.createSpy('stopObserving'); + const mockedObserver = { + startObserving: startObservingSpy, + stopObserving: stopObservingSpy, + } as any; + const mockedNode = 'NODE' as any; + + const textMutationObserverSpy = spyOn( + textMutationObserver, + 'createTextMutationObserver' + ).and.returnValue(mockedObserver); + + init({}); + + const state = plugin.getState(); + + state.cachedModel = 'MODEL' as any; + state.cachedSelection = 'SELECTION' as any; + + expect(stopObservingSpy).toHaveBeenCalledTimes(0); + expect(startObservingSpy).toHaveBeenCalledTimes(1); + expect(textMutationObserverSpy).toHaveBeenCalledTimes(1); + + plugin.onPluginEvent({ + eventType: 'logicalRootChanged', + logicalRoot: mockedNode, + }); + + expect(state).toEqual({ + cachedModel: undefined, + cachedSelection: undefined, + domIndexer: new DomIndexerImpl(), + textMutationObserver: mockedObserver, + }); + expect(stopObservingSpy).toHaveBeenCalledTimes(1); + expect(startObservingSpy).toHaveBeenCalledTimes(2); + expect(textMutationObserverSpy).toHaveBeenCalledTimes(2); + expect(textMutationObserverSpy.calls.argsFor(1)[0]).toBe(mockedNode); + + plugin.dispose(); + }); + }); + describe('KeyDown event', () => { beforeEach(() => { init({ disableCache: true }); @@ -375,15 +422,17 @@ describe('CachePlugin', () => { }); describe('onMutation', () => { - let onMutation: (mutation: textMutationObserver.Mutation) => void; + let onMutation: (mutation: Mutation) => void; let startObservingSpy: jasmine.Spy; let stopObservingSpy: jasmine.Spy; let mockedObserver: any; let reconcileChildListSpy: jasmine.Spy; + let reconcileElementIdSpy: jasmine.Spy; let mockedIndexer: DomIndexer; beforeEach(() => { reconcileChildListSpy = jasmine.createSpy('reconcileChildList'); + reconcileElementIdSpy = jasmine.createSpy('reconcileElementId'); startObservingSpy = jasmine.createSpy('startObserving'); stopObservingSpy = jasmine.createSpy('stopObserving'); @@ -404,6 +453,7 @@ describe('CachePlugin', () => { mockedIndexer = { reconcileSelection: reconcileSelectionSpy, reconcileChildList: reconcileChildListSpy, + reconcileElementId: reconcileElementIdSpy, } as any; }); @@ -530,5 +580,55 @@ describe('CachePlugin', () => { cachedSelection: 'SELECTION' as any, }); }); + + it('elementId, can reconcile', () => { + const state = plugin.getState(); + state.cachedModel = 'MODEL' as any; + state.cachedSelection = 'SELECTION' as any; + state.domIndexer = mockedIndexer; + + const mockedElement = 'ELEMENT' as any; + + reconcileElementIdSpy.and.returnValue(true); + + onMutation({ + type: 'elementId', + element: mockedElement, + }); + + expect(reconcileElementIdSpy).toHaveBeenCalledTimes(1); + expect(reconcileElementIdSpy).toHaveBeenCalledWith(mockedElement); + expect(state).toEqual({ + domIndexer: mockedIndexer, + textMutationObserver: mockedObserver, + cachedModel: 'MODEL' as any, + cachedSelection: 'SELECTION' as any, + }); + }); + + it('elementId, cannot reconcile', () => { + const state = plugin.getState(); + state.cachedModel = 'MODEL' as any; + state.cachedSelection = 'SELECTION' as any; + state.domIndexer = mockedIndexer; + + const mockedElement = 'ELEMENT' as any; + + reconcileElementIdSpy.and.returnValue(false); + + onMutation({ + type: 'elementId', + element: mockedElement, + }); + + expect(reconcileElementIdSpy).toHaveBeenCalledTimes(1); + expect(reconcileElementIdSpy).toHaveBeenCalledWith(mockedElement); + expect(state).toEqual({ + domIndexer: mockedIndexer, + textMutationObserver: mockedObserver, + cachedModel: undefined, + cachedSelection: undefined, + }); + }); }); }); diff --git a/packages/roosterjs-content-model-core/test/corePlugin/cache/domIndexerImplTest.ts b/packages/roosterjs-content-model-core/test/corePlugin/cache/domIndexerImplTest.ts index 698df5f7afc..378a7addb88 100644 --- a/packages/roosterjs-content-model-core/test/corePlugin/cache/domIndexerImplTest.ts +++ b/packages/roosterjs-content-model-core/test/corePlugin/cache/domIndexerImplTest.ts @@ -1,6 +1,14 @@ import * as setSelection from 'roosterjs-content-model-dom/lib/modelApi/selection/setSelection'; import { createRange } from 'roosterjs-content-model-dom/test/testUtils'; -import { DomIndexerImpl, IndexedSegmentNode } from '../../../lib/corePlugin/cache/domIndexerImpl'; +import { + BlockEntityDelimiterItem, + DomIndexerImpl, + IndexedEntityDelimiter, + IndexedSegmentNode, + IndexedTableElement, + SegmentItem, + TableItem, +} from '../../../lib/corePlugin/cache/domIndexerImpl'; import { CacheSelection, ContentModelDocument, @@ -10,6 +18,7 @@ import { import { createBr, createContentModelDocument, + createEntity, createImage, createParagraph, createSelectionMarker, @@ -18,6 +27,8 @@ import { createText, } from 'roosterjs-content-model-dom'; +const ZERO_WIDTH_SPACE = '\u200B'; + describe('domIndexerImpl.onSegment', () => { it('onSegment', () => { const node = {} as any; @@ -185,7 +196,55 @@ describe('domIndexerImpl.onTable', () => { domIndexerImpl.onTable(node, table); expect(node).toEqual({ - __roosterjsContentModel: { tableRows: rows }, + __roosterjsContentModel: { table }, + }); + }); +}); + +describe('domIndexerImpl.onBlockEntity', () => { + it('no delimiter', () => { + const root = document.createElement('div'); + const wrapper = document.createElement('span'); + + root.appendChild(wrapper); + + const group = createContentModelDocument(); + const entity = createEntity(wrapper); + + new DomIndexerImpl().onBlockEntity(entity, group); + + expect(root.innerHTML).toEqual(''); + }); + + it('has delimiters', () => { + const root = document.createElement('div'); + const wrapper = document.createElement('span'); + const delimiter1 = document.createElement('span'); + const delimiter2 = document.createElement('span'); + const text1 = document.createTextNode(ZERO_WIDTH_SPACE); + const text2 = document.createTextNode(ZERO_WIDTH_SPACE); + + delimiter1.className = 'entityDelimiterBefore'; + delimiter1.appendChild(text1); + delimiter2.className = 'entityDelimiterAfter'; + delimiter2.appendChild(text2); + + root.appendChild(delimiter1); + root.appendChild(wrapper); + root.appendChild(delimiter2); + + const group = createContentModelDocument(); + const entity = createEntity(wrapper); + + new DomIndexerImpl().onBlockEntity(entity, group); + + expect((text1 as IndexedEntityDelimiter).__roosterjsContentModel).toEqual({ + entity, + parent: group, + }); + expect((text2 as IndexedEntityDelimiter).__roosterjsContentModel).toEqual({ + entity, + parent: group, }); }); }); @@ -517,7 +576,7 @@ describe('domIndexerImpl.reconcileSelection', () => { const result = domIndexerImpl.reconcileSelection(model, newRangeEx); - expect(result).toBeFalse(); + expect(result).toBeTrue(); expect(node1.__roosterjsContentModel).toEqual({ paragraph, segments: [oldSegment1], @@ -527,7 +586,14 @@ describe('domIndexerImpl.reconcileSelection', () => { format: {}, segments: [oldSegment1], }); - expect(setSelectionSpy).not.toHaveBeenCalled(); + expect(setSelectionSpy).toHaveBeenCalledWith(model, { + segmentType: 'Image', + src: 'test', + format: {}, + dataset: {}, + isSelected: true, + isSelectedAsImageSelection: true, + }); expect(model).toEqual({ blockGroupType: 'Document', blocks: [paragraph], @@ -537,6 +603,8 @@ describe('domIndexerImpl.reconcileSelection', () => { src: 'test', format: {}, dataset: {}, + isSelected: true, + isSelectedAsImageSelection: true, }); expect(model.hasRevertedRangeSelection).toBeFalsy(); }); @@ -574,11 +642,11 @@ describe('domIndexerImpl.reconcileSelection', () => { const result = domIndexerImpl.reconcileSelection(model, newRangeEx); - expect(result).toBeFalse(); + expect(result).toBeTrue(); expect(node1.__roosterjsContentModel).toEqual({ - tableRows: tableModel.rows, + table: tableModel, }); - expect(setSelectionSpy).not.toHaveBeenCalled(); + expect(setSelectionSpy).toHaveBeenCalledWith(model, cell10, cell21); expect(model).toEqual({ blockGroupType: 'Document', blocks: [tableModel], @@ -736,6 +804,136 @@ describe('domIndexerImpl.reconcileSelection', () => { expect(setSelectionSpy).toHaveBeenCalled(); expect(model.hasRevertedRangeSelection).toBeFalsy(); }); + + it('block entity: selection in first delimiter', () => { + const root = document.createElement('div'); + const wrapper = document.createElement('span'); + const delimiter1 = document.createElement('span'); + const delimiter2 = document.createElement('span'); + const text1 = document.createTextNode(ZERO_WIDTH_SPACE); + const text2 = document.createTextNode(ZERO_WIDTH_SPACE); + + delimiter1.className = 'entityDelimiterBefore'; + delimiter2.className = 'entityDelimiterAfter'; + delimiter1.appendChild(text1); + delimiter2.appendChild(text2); + root.appendChild(delimiter1); + root.appendChild(wrapper); + root.appendChild(delimiter2); + + const entity = createEntity(wrapper); + const group = createContentModelDocument(); + + group.blocks.push(entity); + + const index1: BlockEntityDelimiterItem = { + entity: entity, + parent: group, + }; + const index2: BlockEntityDelimiterItem = { + entity: entity, + parent: group, + }; + + (text1 as IndexedEntityDelimiter).__roosterjsContentModel = index1; + (text2 as IndexedEntityDelimiter).__roosterjsContentModel = index2; + + const range = document.createRange(); + range.setStart(text1, 0); + + const indexer = new DomIndexerImpl(); + + const result = indexer.reconcileSelection(group, { + type: 'range', + range: range, + isReverted: false, + }); + + expect(result).toBeTrue(); + expect(group).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'SelectionMarker', isSelected: true, format: {} }], + format: {}, + isImplicit: true, + }, + { + segmentType: 'Entity', + blockType: 'Entity', + format: {}, + entityFormat: { isReadonly: true, id: undefined, entityType: undefined }, + wrapper: wrapper, + }, + ], + }); + }); + + it('block entity: selection in last delimiter', () => { + const root = document.createElement('div'); + const wrapper = document.createElement('span'); + const delimiter1 = document.createElement('span'); + const delimiter2 = document.createElement('span'); + const text1 = document.createTextNode(ZERO_WIDTH_SPACE); + const text2 = document.createTextNode(ZERO_WIDTH_SPACE); + + delimiter1.className = 'entityDelimiterBefore'; + delimiter2.className = 'entityDelimiterAfter'; + delimiter1.appendChild(text1); + delimiter2.appendChild(text2); + root.appendChild(delimiter1); + root.appendChild(wrapper); + root.appendChild(delimiter2); + + const entity = createEntity(wrapper); + const group = createContentModelDocument(); + + group.blocks.push(entity); + + const index1: BlockEntityDelimiterItem = { + entity: entity, + parent: group, + }; + const index2: BlockEntityDelimiterItem = { + entity: entity, + parent: group, + }; + + (text1 as IndexedEntityDelimiter).__roosterjsContentModel = index1; + (text2 as IndexedEntityDelimiter).__roosterjsContentModel = index2; + + const range = document.createRange(); + range.setStart(text2, 1); + + const indexer = new DomIndexerImpl(); + + const result = indexer.reconcileSelection(group, { + type: 'range', + range: range, + isReverted: false, + }); + + expect(result).toBeTrue(); + expect(group).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + segmentType: 'Entity', + blockType: 'Entity', + format: {}, + entityFormat: { isReadonly: true, id: undefined, entityType: undefined }, + wrapper: wrapper, + }, + { + blockType: 'Paragraph', + segments: [{ segmentType: 'SelectionMarker', isSelected: true, format: {} }], + format: {}, + isImplicit: true, + }, + ], + }); + }); }); describe('domIndexerImpl.reconcileChildList', () => { @@ -915,3 +1113,90 @@ describe('domIndexerImpl.reconcileChildList', () => { }); }); }); + +describe('domIndexerImpl.reconcileElementId', () => { + it('unindexed image id', () => { + const img = document.createElement('img'); + const image = createImage('test'); + const para = createParagraph(); + + para.segments.push(image); + + img.id = 'testId'; + + const result = new DomIndexerImpl().reconcileElementId(img); + + expect(result).toBe(false); + expect(image).toEqual({ + segmentType: 'Image', + format: {}, + src: 'test', + dataset: {}, + }); + }); + + it('indexed image id', () => { + const img = document.createElement('img'); + const image = createImage('test'); + const para = createParagraph(); + const segIndex: SegmentItem = { + paragraph: para, + segments: [image], + }; + + para.segments.push(image); + + ((img as Node) as IndexedSegmentNode).__roosterjsContentModel = segIndex; + + img.id = 'testId'; + + const result = new DomIndexerImpl().reconcileElementId(img); + + expect(result).toBe(true); + expect(image).toEqual({ + segmentType: 'Image', + format: { id: 'testId' }, + src: 'test', + dataset: {}, + }); + }); + + it('unindexed table id', () => { + const tb = document.createElement('table'); + + tb.id = 'testId'; + + const result = new DomIndexerImpl().reconcileElementId(tb); + + expect(result).toBe(false); + }); + + it('indexed table id', () => { + const tb = document.createElement('table'); + const table = createTable(1); + const tbIndex: TableItem = { + table: table, + }; + + (tb as IndexedTableElement).__roosterjsContentModel = tbIndex; + + tb.id = 'testId'; + + const result = new DomIndexerImpl().reconcileElementId(tb); + + expect(result).toBe(true); + expect(table).toEqual({ + blockType: 'Table', + format: { id: 'testId' }, + widths: [], + dataset: {}, + rows: [ + { + height: 0, + format: {}, + cells: [], + }, + ], + }); + }); +}); diff --git a/packages/roosterjs-content-model-core/test/corePlugin/cache/textMutationObserverTest.ts b/packages/roosterjs-content-model-core/test/corePlugin/cache/textMutationObserverTest.ts index 16835b04b7a..2ca6cc15e2d 100644 --- a/packages/roosterjs-content-model-core/test/corePlugin/cache/textMutationObserverTest.ts +++ b/packages/roosterjs-content-model-core/test/corePlugin/cache/textMutationObserverTest.ts @@ -1,3 +1,4 @@ +import * as entityUtils from 'roosterjs-content-model-dom/lib/domUtils/entityUtils'; import * as textMutationObserver from '../../../lib/corePlugin/cache/textMutationObserver'; import { TextMutationObserver } from 'roosterjs-content-model-types'; @@ -295,7 +296,7 @@ describe('TextMutationObserverImpl', () => { expect(onMutation).toHaveBeenCalledWith({ type: 'unknown' }); }); - it('attribute change', async () => { + it('id change', async () => { const div = document.createElement('div'); const div1 = document.createElement('div'); @@ -314,6 +315,72 @@ describe('TextMutationObserverImpl', () => { window.setTimeout(resolve, 10); }); + expect(onMutation).toHaveBeenCalledTimes(1); + expect(onMutation).toHaveBeenCalledWith({ type: 'elementId', element: div1 }); + }); + + it('unknown attribute change', async () => { + const div = document.createElement('div'); + const div1 = document.createElement('div'); + + div.appendChild(div1); + + const onMutation = jasmine.createSpy('onMutation'); + observer = textMutationObserver.createTextMutationObserver(div, onMutation); + + observer.startObserving(); + + div1.setAttribute('attr', 'value'); + + observer.flushMutations(); + + await new Promise(resolve => { + window.setTimeout(resolve, 10); + }); + + expect(onMutation).toHaveBeenCalledTimes(1); + expect(onMutation).toHaveBeenCalledWith({ type: 'unknown' }); + }); + + it('Ignore changes under entity', () => { + const div = document.createElement('div'); + const wrapper = document.createElement('div'); + const span1 = document.createElement('span'); + const span2 = document.createElement('span'); + + wrapper.appendChild(span1); + wrapper.className = '_Entity'; + + div.appendChild(wrapper); + div.appendChild(span2); + + const findClosestEntityWrapperSpy = spyOn( + entityUtils, + 'findClosestEntityWrapper' + ).and.callThrough(); + const findClosestBlockEntityContainer = spyOn( + entityUtils, + 'findClosestBlockEntityContainer' + ).and.callThrough(); + + const onMutation = jasmine.createSpy('onMutation'); + + observer = textMutationObserver.createTextMutationObserver(div, onMutation); + observer.startObserving(); + + span1.setAttribute('attr1', 'value1'); + span1.setAttribute('attr2', 'value2'); + span2.setAttribute('attr3', 'value3'); + span2.setAttribute('attr4', 'value4'); + + observer.flushMutations(); + + expect(findClosestEntityWrapperSpy).toHaveBeenCalledTimes(2); + expect(findClosestEntityWrapperSpy).toHaveBeenCalledWith(span1, jasmine.anything()); + expect(findClosestEntityWrapperSpy).toHaveBeenCalledWith(span2, jasmine.anything()); + expect(findClosestBlockEntityContainer).toHaveBeenCalledTimes(1); + expect(findClosestBlockEntityContainer).toHaveBeenCalledWith(span2, jasmine.anything()); + expect(onMutation).toHaveBeenCalledTimes(1); expect(onMutation).toHaveBeenCalledWith({ type: 'unknown' }); }); diff --git a/packages/roosterjs-content-model-core/test/corePlugin/selection/SelectionPluginTest.ts b/packages/roosterjs-content-model-core/test/corePlugin/selection/SelectionPluginTest.ts index d03704d44d7..0d795d323ed 100644 --- a/packages/roosterjs-content-model-core/test/corePlugin/selection/SelectionPluginTest.ts +++ b/packages/roosterjs-content-model-core/test/corePlugin/selection/SelectionPluginTest.ts @@ -649,10 +649,18 @@ describe('SelectionPlugin handle image selection', () => { key: 'A', stopPropagation: stopPropagationSpy, ctrlKey: true, + shiftKey: false, } as any; const mockedImage = { parentNode: { childNodes: [] }, + ownerDocument: { + createRange: () => { + return { + selectNode: (node: any) => {}, + }; + }, + }, } as any; mockedImage.parentNode.childNodes.push(mockedImage); @@ -674,7 +682,7 @@ describe('SelectionPlugin handle image selection', () => { }); expect(stopPropagationSpy).not.toHaveBeenCalled(); - expect(setDOMSelectionSpy).not.toHaveBeenCalled(); + expect(setDOMSelectionSpy).toHaveBeenCalled(); }); it('key down - other key, image has no parent', () => { @@ -1264,6 +1272,10 @@ describe('SelectionPlugin handle table selection', () => { let td4: HTMLTableCellElement; let tr1: HTMLElement; let tr2: HTMLElement; + let td1_text: Node; + let td2_text: Node; + let td3_text: Node; + let td4_text: Node; let table: HTMLTableElement; let div: HTMLElement; @@ -1283,6 +1295,18 @@ describe('SelectionPlugin handle table selection', () => { td3.id = 'td3'; td4.id = 'td4'; + // Craete text nodes + td1_text = document.createTextNode('1'); + td2_text = document.createTextNode('2'); + td3_text = document.createTextNode('3'); + td4_text = document.createTextNode('4'); + + // Add Text to each cell + td1.appendChild(td1_text); + td2.appendChild(td2_text); + td3.appendChild(td3_text); + td4.appendChild(td4_text); + tr1.appendChild(td1); tr1.appendChild(td2); tr2.appendChild(td3); @@ -1395,11 +1419,13 @@ describe('SelectionPlugin handle table selection', () => { }; }); - const selectNodeContentsSpy = jasmine.createSpy('selectNodeContents'); + const setStartSpy = jasmine.createSpy('setStart'); + const setEndSpy = jasmine.createSpy('setEnd'); const collapseSpy = jasmine.createSpy('collapse'); const preventDefaultSpy = jasmine.createSpy('preventDefault'); const mockedRange = { - selectNodeContents: selectNodeContentsSpy, + setStart: setStartSpy, + setEnd: setEndSpy, collapse: collapseSpy, } as any; @@ -1428,7 +1454,8 @@ describe('SelectionPlugin handle table selection', () => { range: mockedRange, isReverted: false, }); - expect(selectNodeContentsSpy).toHaveBeenCalledWith(td2); + expect(setStartSpy).toHaveBeenCalledWith(td2_text, 0); + expect(setEndSpy).toHaveBeenCalledWith(td2_text, 0); expect(collapseSpy).not.toHaveBeenCalled(); expect(announceSpy).not.toHaveBeenCalled(); expect(preventDefaultSpy).toHaveBeenCalledTimes(1); @@ -1466,11 +1493,13 @@ describe('SelectionPlugin handle table selection', () => { }; }); - const selectNodeContentsSpy = jasmine.createSpy('selectNodeContents'); + const setStartSpy = jasmine.createSpy('setStart'); + const setEndSpy = jasmine.createSpy('setEnd'); const collapseSpy = jasmine.createSpy('collapse'); const preventDefaultSpy = jasmine.createSpy('preventDefault'); const mockedRange = { - selectNodeContents: selectNodeContentsSpy, + setStart: setStartSpy, + setEnd: setEndSpy, collapse: collapseSpy, } as any; @@ -1500,7 +1529,8 @@ describe('SelectionPlugin handle table selection', () => { range: mockedRange, isReverted: false, }); - expect(selectNodeContentsSpy).toHaveBeenCalledWith(td1); + expect(setStartSpy).toHaveBeenCalledWith(td1_text, 0); + expect(setEndSpy).toHaveBeenCalledWith(td1_text, 0); expect(collapseSpy).not.toHaveBeenCalled(); expect(announceSpy).not.toHaveBeenCalled(); expect(preventDefaultSpy).toHaveBeenCalledTimes(1); @@ -1538,11 +1568,13 @@ describe('SelectionPlugin handle table selection', () => { }; }); - const selectNodeContentsSpy = jasmine.createSpy('selectNodeContents'); + const setStartSpy = jasmine.createSpy('setStart'); + const setEndSpy = jasmine.createSpy('setEnd'); const collapseSpy = jasmine.createSpy('collapse'); const preventDefaultSpy = jasmine.createSpy('preventDefault'); const mockedRange = { - selectNodeContents: selectNodeContentsSpy, + setStart: setStartSpy, + setEnd: setEndSpy, collapse: collapseSpy, } as any; @@ -1571,7 +1603,8 @@ describe('SelectionPlugin handle table selection', () => { range: mockedRange, isReverted: false, }); - expect(selectNodeContentsSpy).toHaveBeenCalledWith(td3); + expect(setStartSpy).toHaveBeenCalledWith(td3_text, 0); + expect(setEndSpy).toHaveBeenCalledWith(td3_text, 0); expect(collapseSpy).not.toHaveBeenCalled(); expect(announceSpy).not.toHaveBeenCalled(); expect(preventDefaultSpy).toHaveBeenCalledTimes(1); @@ -1584,6 +1617,7 @@ describe('SelectionPlugin handle table selection', () => { getDOMSelectionSpy.and.callFake(() => { time++; + td1.appendChild(document.createTextNode('1')); return time == 1 ? { type: 'range', @@ -1782,7 +1816,7 @@ describe('SelectionPlugin handle table selection', () => { range: mockedRange, isReverted: false, }); - expect(setStartSpy).toHaveBeenCalledWith(td4, 0); + expect(setStartSpy).toHaveBeenCalledWith(td4_text, 0); expect(announceSpy).toHaveBeenCalledWith({ defaultStrings: 'announceOnFocusLastCell', }); @@ -2823,40 +2857,4 @@ describe('SelectionPlugin selectionChange on image selected', () => { expect(setDOMSelectionSpy).not.toHaveBeenCalled(); expect(getDOMSelectionSpy).toHaveBeenCalledTimes(1); }); - - it('onSelectionChange on image | 4', () => { - const image = document.createElement('img'); - spyOn(isSingleImageInSelection, 'isSingleImageInSelection').and.returnValue(image); - - const plugin = createSelectionPlugin({}); - const state = plugin.getState(); - const mockedOldSelection = { - type: 'image', - image: {} as any, - } as DOMSelection; - - state.selection = mockedOldSelection; - - plugin.initialize(editor); - - const onSelectionChange = addEventListenerSpy.calls.argsFor(0)[1] as Function; - const mockedNewSelection = { - type: 'range', - range: {} as any, - } as any; - - hasFocusSpy.and.returnValue(true); - isInShadowEditSpy.and.returnValue(false); - getDOMSelectionSpy.and.returnValue(mockedNewSelection); - getRangeAtSpy.and.returnValue({ startContainer: {} }); - - onSelectionChange(); - - expect(setDOMSelectionSpy).toHaveBeenCalled(); - expect(setDOMSelectionSpy).toHaveBeenCalledWith({ - type: 'image', - image, - }); - expect(getDOMSelectionSpy).toHaveBeenCalledTimes(1); - }); }); diff --git a/packages/roosterjs-content-model-core/test/editor/core/DOMHelperImplTest.ts b/packages/roosterjs-content-model-core/test/editor/core/DOMHelperImplTest.ts index 2bc51a43872..fb490373ed3 100644 --- a/packages/roosterjs-content-model-core/test/editor/core/DOMHelperImplTest.ts +++ b/packages/roosterjs-content-model-core/test/editor/core/DOMHelperImplTest.ts @@ -17,6 +17,37 @@ describe('DOMHelperImpl', () => { expect(result).toBe(mockedResult); expect(containsSpy).toHaveBeenCalledWith(mockedNode); }); + + it('isNodeInEditor, check root node, excludeRoot=false', () => { + const div = document.createElement('div'); + const domHelper = createDOMHelper(div); + + const result = domHelper.isNodeInEditor(div); + + expect(result).toBeTrue(); + }); + + it('isNodeInEditor, check root node, excludeRoot=true', () => { + const div = document.createElement('div'); + const domHelper = createDOMHelper(div); + + const result = domHelper.isNodeInEditor(div, true); + + expect(result).toBeFalse(); + }); + + it('isNodeInEditor, check root node, excludeRoot=true, do not call contains', () => { + const containsSpy = jasmine.createSpy('contains'); + const mockedDiv = { + contains: containsSpy, + } as any; + const domHelper = createDOMHelper(mockedDiv); + + const result = domHelper.isNodeInEditor(mockedDiv, true); + + expect(result).toBeFalse(); + expect(containsSpy).not.toHaveBeenCalled(); + }); }); describe('queryElements', () => { diff --git a/packages/roosterjs-content-model-dom/lib/domToModel/context/defaultProcessors.ts b/packages/roosterjs-content-model-dom/lib/domToModel/context/defaultProcessors.ts index 152334499f5..0783be37e4f 100644 --- a/packages/roosterjs-content-model-dom/lib/domToModel/context/defaultProcessors.ts +++ b/packages/roosterjs-content-model-dom/lib/domToModel/context/defaultProcessors.ts @@ -29,6 +29,7 @@ export const defaultProcessorMap: ElementProcessorMap = { blockquote: knownElementProcessor, br: brProcessor, code: codeProcessor, + del: knownElementProcessor, div: knownElementProcessor, em: knownElementProcessor, font: fontProcessor, diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts index d0f148387eb..3ea33badf72 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts @@ -1,3 +1,4 @@ +import { addBlock } from '../common/addBlock'; import { addSegment } from '../common/addSegment'; import { applyTableFormat } from './applyTableFormat'; import { createListItem } from '../creators/createListItem'; @@ -49,6 +50,12 @@ export function mergeModel( const insertPosition = options?.insertPosition ?? deleteSelection(target, [], context).insertPoint; + if (options?.addParagraphAfterMergedContent) { + const { paragraph, marker } = insertPosition || {}; + const newPara = createParagraph(false /* isImplicit */, paragraph?.format, marker?.format); + addBlock(source, newPara); + } + if (insertPosition) { if (options?.mergeFormat && options.mergeFormat != 'none') { const newFormat: ContentModelSegmentFormat = { diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/selection/setSelection.ts b/packages/roosterjs-content-model-dom/lib/modelApi/selection/setSelection.ts index 1d30893e6e6..16d71b6614e 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/selection/setSelection.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/selection/setSelection.ts @@ -37,10 +37,28 @@ function setSelectionToBlockGroup( setIsSelected(mutateBlock(group), isInSelection); } - group.blocks.forEach(block => { + const blocksToDelete: number[] = []; + + group.blocks.forEach((block, i) => { isInSelection = setSelectionToBlock(block, isInSelection, start, end); + + if (block.blockType == 'Paragraph' && block.segments.length == 0 && block.isImplicit) { + blocksToDelete.push(i); + } }); + let index: number | undefined; + + if (blocksToDelete.length > 0) { + const mutableGroup = mutateBlock(group); + + while ((index = blocksToDelete.pop()) !== undefined) { + if (index >= 0) { + mutableGroup.blocks.splice(index, 1); + } + } + } + return isInSelection; }); } @@ -97,11 +115,15 @@ function setSelectionToBlock( ); }); - let index: number | undefined; + if (segmentsToDelete.length > 0) { + const mutablePara = mutateBlock(block); - while ((index = segmentsToDelete.pop()) !== undefined) { - if (index >= 0) { - mutateBlock(block).segments.splice(index, 1); + let index: number | undefined; + + while ((index = segmentsToDelete.pop()) !== undefined) { + if (index >= 0) { + mutablePara.segments.splice(index, 1); + } } } diff --git a/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleBlockGroupChildren.ts b/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleBlockGroupChildren.ts index 75cd3723db9..8c749f5a039 100644 --- a/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleBlockGroupChildren.ts +++ b/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleBlockGroupChildren.ts @@ -32,6 +32,10 @@ export const handleBlockGroupChildren: ContentModelHandler if (context.addDelimiterForEntity && entityFormat.isReadonly) { const [after, before] = addDelimiters(doc, wrapper, getSegmentFormat(context), context); - newSegments?.push(after, before); + if (newSegments) { + newSegments.push(after, before); + + if (after.firstChild) { + newSegments.push(after.firstChild); + } + + if (before.firstChild) { + newSegments.push(before.firstChild); + } + } + context.regularSelection.current.segment = after; } else { context.regularSelection.current.segment = wrapper; diff --git a/packages/roosterjs-content-model-dom/test/domToModel/processors/brProcessorTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/processors/brProcessorTest.ts index ef681426f59..f92da141de7 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/processors/brProcessorTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/processors/brProcessorTest.ts @@ -75,6 +75,8 @@ describe('brProcessor', () => { onTable: null!, reconcileSelection: null!, reconcileChildList: null!, + onBlockEntity: null!, + reconcileElementId: null!, }; context.domIndexer = domIndexer; diff --git a/packages/roosterjs-content-model-dom/test/domToModel/processors/entityProcessorTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/processors/entityProcessorTest.ts index 0d6feb24ac9..8521782a205 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/processors/entityProcessorTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/processors/entityProcessorTest.ts @@ -260,6 +260,8 @@ describe('entityProcessor', () => { onTable: null!, reconcileSelection: null!, reconcileChildList: null!, + onBlockEntity: null!, + reconcileElementId: null!, }; context.domIndexer = domIndexer; diff --git a/packages/roosterjs-content-model-dom/test/domToModel/processors/generalProcessorTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/processors/generalProcessorTest.ts index 872c1fde02a..a6d516f3e66 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/processors/generalProcessorTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/processors/generalProcessorTest.ts @@ -395,6 +395,8 @@ describe('generalProcessor', () => { onTable: null!, reconcileSelection: null!, reconcileChildList: null!, + onBlockEntity: null!, + reconcileElementId: null!, }; context.domIndexer = domIndexer; diff --git a/packages/roosterjs-content-model-dom/test/domToModel/processors/imageProcessorTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/processors/imageProcessorTest.ts index 3a2be47d866..15bd59f68a7 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/processors/imageProcessorTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/processors/imageProcessorTest.ts @@ -324,6 +324,8 @@ describe('imageProcessor', () => { onTable: null!, reconcileSelection: null!, reconcileChildList: null!, + onBlockEntity: null!, + reconcileElementId: null!, }; context.domIndexer = domIndexer; diff --git a/packages/roosterjs-content-model-dom/test/domToModel/processors/tableProcessorTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/processors/tableProcessorTest.ts index 08cceeab06a..27bcf4221be 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/processors/tableProcessorTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/processors/tableProcessorTest.ts @@ -291,6 +291,8 @@ describe('tableProcessor', () => { onTable: onTableSpy, reconcileSelection: null!, reconcileChildList: null!, + onBlockEntity: null!, + reconcileElementId: null!, }; context.domIndexer = domIndexer; diff --git a/packages/roosterjs-content-model-dom/test/domToModel/processors/textProcessorTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/processors/textProcessorTest.ts index 89ef5687a5d..621e29e17a8 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/processors/textProcessorTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/processors/textProcessorTest.ts @@ -578,6 +578,8 @@ describe('textProcessor', () => { onTable: null!, reconcileSelection: null!, reconcileChildList: null!, + onBlockEntity: null!, + reconcileElementId: null!, }; context.domIndexer = domIndexer; @@ -613,6 +615,8 @@ describe('textProcessor', () => { onTable: null!, reconcileSelection: null!, reconcileChildList: null!, + onBlockEntity: null!, + reconcileElementId: null!, }; context.domIndexer = domIndexer; @@ -659,6 +663,8 @@ describe('textProcessor', () => { onTable: null!, reconcileSelection: null!, reconcileChildList: null!, + onBlockEntity: null!, + reconcileElementId: null!, }; context.domIndexer = domIndexer; diff --git a/packages/roosterjs-content-model-dom/test/modelApi/editing/mergeModelTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/editing/mergeModelTest.ts index 72c4023c39e..f6e6b58e40b 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/editing/mergeModelTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/editing/mergeModelTest.ts @@ -3835,4 +3835,39 @@ describe('mergeModel', () => { verticalAlign: 'top', }); }); + + it('Merge model with addParagraphAfterMergedContent', () => { + const source = createContentModelDocument(); + const para = createParagraph(); + para.segments.push(createText('Merge')); + source.blocks.push(para); + + const target = createContentModelDocument(); + const paraTarget = createParagraph(); + paraTarget.segments.push(createSelectionMarker()); + target.blocks.push(paraTarget); + + mergeModel(target, source, undefined, { + addParagraphAfterMergedContent: true, + }); + + expect(target).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'Merge', format: {} }], + format: {}, + }, + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'SelectionMarker', isSelected: true, format: {} }, + { segmentType: 'Br', format: {} }, + ], + format: {}, + }, + ], + }); + }); }); diff --git a/packages/roosterjs-content-model-dom/test/modelApi/selection/setSelectionTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/selection/setSelectionTest.ts index b776c9a0c33..0762f0af7c6 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/selection/setSelectionTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/selection/setSelectionTest.ts @@ -1,3 +1,4 @@ +import { ContentModelDocument } from 'roosterjs-content-model-types'; import { setSelection } from '../../../lib/modelApi/selection/setSelection'; import { createBr, @@ -937,4 +938,70 @@ describe('setSelection', () => { ], }); }); + + it('delete empty segment after setSelection', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + }, + { + segmentType: 'Br', + format: {}, + }, + ], + }, + ], + }; + + setSelection(model); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + }, + ], + }); + }); + + it('delete empty paragraph after setSelection', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + }, + ], + isImplicit: true, + }, + ], + }; + + setSelection(model); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [], + }); + }); }); diff --git a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleBlockGroupChildrenTest.ts b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleBlockGroupChildrenTest.ts index 489c5d9c097..2a5673a1af6 100644 --- a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleBlockGroupChildrenTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleBlockGroupChildrenTest.ts @@ -1,4 +1,5 @@ import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; +import { createEntity } from '../../../lib'; import { createListItem } from '../../../lib/modelApi/creators/createListItem'; import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; import { createParagraph } from '../../../lib/modelApi/creators/createParagraph'; @@ -379,4 +380,32 @@ describe('handleBlockGroupChildren', () => { '

' ); }); + + it('child contains entity', () => { + const group = createContentModelDocument(); + const paragraph = createParagraph(); + const wrapper = document.createElement('div'); + const entity = createEntity(wrapper); + + group.blocks.push(paragraph, entity); + + const onBlockEntity = jasmine.createSpy('onBlockEntity'); + const onParagraph = jasmine.createSpy('onParagraph'); + + context.domIndexer = { + onBlockEntity, + onParagraph, + } as any; + + handleBlockGroupChildren(document, parent, group, context); + + expect(parent.outerHTML).toBe( + '
' + ); + expect(handleBlock).toHaveBeenCalledTimes(2); + expect(handleBlock).toHaveBeenCalledWith(document, parent, paragraph, context, null); + expect(handleBlock).toHaveBeenCalledWith(document, parent, entity, context, null); + expect(onBlockEntity).toHaveBeenCalledTimes(1); + expect(onBlockEntity).toHaveBeenCalledWith(entity, group); + }); }); diff --git a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleEntityTest.ts b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleEntityTest.ts index cf98d8daffc..41dcf9f20c8 100644 --- a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleEntityTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleEntityTest.ts @@ -329,10 +329,12 @@ describe('handleEntity', () => { '' ); expect(entityUtils.addDelimiters).toHaveBeenCalledTimes(1); - expect(newSegments.length).toBe(3); + expect(newSegments.length).toBe(5); expect(newSegments[0]).toBe(span); expect(newSegments[1]).toBe(span.nextSibling!); expect(newSegments[2]).toBe(span.previousSibling!); + expect(newSegments[3]).toBe(span.nextSibling!.firstChild!); + expect(newSegments[4]).toBe(span.previousSibling!.firstChild!); }); it('Inline entity with newSegments but no delimiter', () => { diff --git a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleParagraphTest.ts b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleParagraphTest.ts index 8fec57ab9aa..2c906304572 100644 --- a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleParagraphTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleParagraphTest.ts @@ -582,6 +582,8 @@ describe('handleParagraph', () => { onTable: null!, reconcileSelection: null!, reconcileChildList: null!, + onBlockEntity: null!, + reconcileElementId: null!, }; context.domIndexer = domIndexer; @@ -626,6 +628,8 @@ describe('handleParagraph', () => { onTable: null!, reconcileSelection: null!, reconcileChildList: null!, + onBlockEntity: null!, + reconcileElementId: null!, }; context.domIndexer = domIndexer; diff --git a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleTableTest.ts b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleTableTest.ts index a27afe8415e..5eac39a4727 100644 --- a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleTableTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleTableTest.ts @@ -602,6 +602,8 @@ describe('handleTable', () => { onTable: onTableSpy, reconcileSelection: null!, reconcileChildList: null!, + onBlockEntity: null!, + reconcileElementId: null!, }; context.domIndexer = domIndexer; diff --git a/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts b/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts index 5a7205a2700..224510a39f6 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts @@ -60,7 +60,11 @@ export const handleEnterOnList: DeleteSelectionStep = context => { nextListItem.levels[0] ) { nextListItem.levels.forEach(level => { - level.format.startNumberOverride = undefined; + // Remove startNumberOverride so that next list item can join current list, unless it is 1. + // List start with 1 means it should be an explicit new list and should never join another list before it + if (level.format.startNumberOverride !== 1) { + level.format.startNumberOverride = undefined; + } }); } } diff --git a/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts b/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts index 22d57145b1d..cea7b9d8b69 100644 --- a/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts @@ -14,13 +14,14 @@ import { Resizer } from './Resizer/resizerContext'; import { Rotator } from './Rotator/rotatorContext'; import { updateRotateHandle } from './Rotator/updateRotateHandle'; import { updateWrapper } from './utils/updateWrapper'; + import { + ChangeSource, getSafeIdSelector, isElementOfType, - isModifierKey, isNodeOfType, + mutateBlock, mutateSegment, - toArray, unwrap, } from 'roosterjs-content-model-dom'; import type { DragAndDropHelper } from '../pluginUtils/DragAndDrop/DragAndDropHelper'; @@ -35,6 +36,7 @@ import type { ImageEditor, ImageMetadataFormat, KeyDownEvent, + MouseDownEvent, MouseUpEvent, PluginEvent, } from 'roosterjs-content-model-types'; @@ -50,6 +52,7 @@ const DefaultOptions: Partial = { }; const MouseRightButton = 2; +const DRAG_ID = '_dragging'; /** * ImageEdit plugin handles the following image editing features: @@ -75,7 +78,6 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { private croppers: HTMLDivElement[] = []; private zoomScale: number = 1; private disposer: (() => void) | null = null; - //EXPOSED FOR TEST ONLY protected isEditing = false; constructor(protected options: ImageEditOptions = DefaultOptions) {} @@ -107,6 +109,16 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { } }, }, + dragstart: { + beforeDispatch: ev => { + if (this.editor) { + const target = ev.target as Node; + if (this.isImageSelection(target)) { + target.id = target.id + DRAG_ID; + } + } + }, + }, }); } @@ -136,16 +148,24 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { return; } switch (event.eventType) { + case 'mouseDown': + this.mouseDownHandler(this.editor, event); + break; case 'mouseUp': this.mouseUpHandler(this.editor, event); break; case 'keyDown': this.keyDownHandler(this.editor, event); break; + case 'contentChanged': + if (event.source == ChangeSource.Drop) { + this.onDropHandler(this.editor); + } + break; } } - private isImageSelection(target: Node) { + private isImageSelection(target: Node): target is HTMLElement { return ( isNodeOfType(target, 'ELEMENT_NODE') && (isElementOfType(target, 'img') || @@ -168,23 +188,44 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { } } - //Sometimes the cursor can be inside the editing image and inside shadow dom, then the cursor need to moved out of shadow dom - private selectBeforeEditingImage(editor: IEditor, element: HTMLElement) { - let parent = element.parentNode; - if (parent && isNodeOfType(parent, 'ELEMENT_NODE') && parent.shadowRoot) { - element = parent; - parent = parent.parentNode; + private mouseDownHandler(editor: IEditor, event: MouseDownEvent) { + if ( + this.isEditing && + this.isImageSelection(event.rawEvent.target as Node) && + event.rawEvent.button !== MouseRightButton + ) { + this.applyFormatWithContentModel( + editor, + this.isCropMode, + this.shadowSpan === event.rawEvent.target + ); } - const index = parent && toArray(parent.childNodes).indexOf(element); - if (index !== null && index >= 0 && parent) { - const doc = editor.getDocument(); - const range = doc.createRange(); - range.setStart(parent, index); - range.collapse(); - editor.setDOMSelection({ - type: 'range', - range, - isReverted: false, + } + + private onDropHandler(editor: IEditor) { + const selection = editor.getDOMSelection(); + if (selection?.type == 'image') { + editor.formatContentModel(model => { + const imageDragged = findEditingImage(model, selection.image.id); + const imageDropped = findEditingImage( + model, + selection.image.id.replace(DRAG_ID, '').trim() + ); + if (imageDragged && imageDropped) { + const draggedIndex = imageDragged.paragraph.segments.indexOf( + imageDragged.image + ); + mutateBlock(imageDragged.paragraph).segments.splice(draggedIndex, 1); + const segment = imageDropped.image; + const paragraph = imageDropped.paragraph; + mutateSegment(paragraph, segment, image => { + image.isSelected = true; + image.isSelectedAsImageSelection = true; + }); + + return true; + } + return false; }); } } @@ -194,15 +235,11 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { if (event.rawEvent.key === 'Escape') { this.removeImageWrapper(); } else { - const selection = editor.getDOMSelection(); - const isImageSelection = selection?.type == 'image'; - if (isImageSelection) { - this.selectBeforeEditingImage(editor, selection.image); - } this.applyFormatWithContentModel( editor, this.isCropMode, - (isModifierKey(event.rawEvent) || event.rawEvent.shiftKey) && isImageSelection //if it's a modifier key over a image, the image should select the image + true /** should selectImage */, + false /* isApiOperation */ ); } } @@ -222,7 +259,6 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { const previousSelectedImage = isApiOperation ? editingImage : findEditingImage(model); - let result = false; if ( shouldSelectImage || @@ -252,21 +288,23 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { this.wasImageResized || this.isCropMode, clonedImage ); - delete image.dataset.isEditing; + image.isSelected = shouldSelectImage; image.isSelectedAsImageSelection = shouldSelectImage; + delete image.dataset.isEditing; } ); + if (shouldSelectImage) { normalizeImageSelection(previousSelectedImage); } + this.cleanInfo(); result = true; } this.isEditing = false; this.isCropMode = false; - if ( editingImage && selection?.type == 'image' && @@ -645,9 +683,4 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { }); } } - - //EXPOSED FOR TEST ONLY - public get isEditingImage() { - return this.isEditing; - } } diff --git a/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts b/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts index d4c2351dd75..689187d5b66 100644 --- a/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts +++ b/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts @@ -1,16 +1,22 @@ -import type { ReadonlyContentModelBlockGroup } from 'roosterjs-content-model-types'; +import type { + ReadonlyContentModelBlockGroup, + ReadonlyContentModelTable, +} from 'roosterjs-content-model-types'; import type { ImageAndParagraph } from '../types/ImageAndParagraph'; /** * @internal */ -export function findEditingImage(group: ReadonlyContentModelBlockGroup): ImageAndParagraph | null { +export function findEditingImage( + group: ReadonlyContentModelBlockGroup, + imageId?: string +): ImageAndParagraph | null { for (let i = 0; i < group.blocks.length; i++) { const block = group.blocks[i]; switch (block.blockType) { case 'BlockGroup': - const result = findEditingImage(block); + const result = findEditingImage(block, imageId); if (result) { return result; @@ -22,7 +28,10 @@ export function findEditingImage(group: ReadonlyContentModelBlockGroup): ImageAn const segment = block.segments[j]; switch (segment.segmentType) { case 'Image': - if (segment.dataset.isEditing) { + if ( + (segment.dataset.isEditing && !imageId) || + segment.format.id == imageId + ) { return { paragraph: block, image: segment, @@ -31,7 +40,7 @@ export function findEditingImage(group: ReadonlyContentModelBlockGroup): ImageAn break; case 'General': - const result = findEditingImage(segment); + const result = findEditingImage(segment, imageId); if (result) { return result; @@ -39,10 +48,28 @@ export function findEditingImage(group: ReadonlyContentModelBlockGroup): ImageAn break; } } + break; + case 'Table': + const imageInTable = findEditingImageOnTable(block, imageId); + if (imageInTable) { + return imageInTable; + } break; } } return null; } + +const findEditingImageOnTable = (table: ReadonlyContentModelTable, imageId?: string) => { + for (const row of table.rows) { + for (const cell of row.cells) { + const result = findEditingImage(cell, imageId); + if (result) { + return result; + } + } + } + return null; +}; diff --git a/packages/roosterjs-content-model-plugins/lib/paste/WacComponents/constants.ts b/packages/roosterjs-content-model-plugins/lib/paste/WacComponents/constants.ts index 027909e7e7f..480e3a5a612 100644 --- a/packages/roosterjs-content-model-plugins/lib/paste/WacComponents/constants.ts +++ b/packages/roosterjs-content-model-plugins/lib/paste/WacComponents/constants.ts @@ -52,9 +52,15 @@ export const TEMP_ELEMENTS_CLASSES: string[] = [ ...WORD_ONLINE_TABLE_TEMP_ELEMENT_CLASSES, 'ListMarkerWrappingSpan', ]; + /** * @internal - **/ -export const WAC_IDENTIFY_SELECTOR: string = - `ul[class^="${BULLET_LIST_STYLE}"]>.${OUTLINE_ELEMENT},ol[class^="${NUMBER_LIST_STYLE}"]>.${OUTLINE_ELEMENT},span.${IMAGE_CONTAINER},span.${IMAGE_BORDER},.${COMMENT_HIGHLIGHT_CLASS},.${COMMENT_HIGHLIGHT_CLICKED_CLASS},` + + */ +export const REMOVE_MARGIN_ELEMENTS: string = + `span.${IMAGE_CONTAINER},span.${IMAGE_BORDER},.${COMMENT_HIGHLIGHT_CLASS},.${COMMENT_HIGHLIGHT_CLICKED_CLASS},` + WORD_ONLINE_TABLE_TEMP_ELEMENT_CLASSES.map(c => `table div[class^="${c}"]`).join(','); + +/** + * @internal + **/ +export const WAC_IDENTIFY_SELECTOR: string = `ul[class^="${BULLET_LIST_STYLE}"]>.${OUTLINE_ELEMENT},ol[class^="${NUMBER_LIST_STYLE}"]>.${OUTLINE_ELEMENT},${REMOVE_MARGIN_ELEMENTS}`; diff --git a/packages/roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents.ts b/packages/roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents.ts index 9c51fb47e96..22851c36417 100644 --- a/packages/roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents.ts +++ b/packages/roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents.ts @@ -5,8 +5,8 @@ import { COMMENT_HIGHLIGHT_CLASS, COMMENT_HIGHLIGHT_CLICKED_CLASS, LIST_CONTAINER_ELEMENT_CLASS_NAME, + REMOVE_MARGIN_ELEMENTS, TEMP_ELEMENTS_CLASSES, - WAC_IDENTIFY_SELECTOR, } from './constants'; import type { BeforePasteEvent, @@ -67,7 +67,7 @@ const wacElementProcessor: ElementProcessor = ( ): void => { const elementTag = element.tagName; - if (element.matches(WAC_IDENTIFY_SELECTOR)) { + if (element.matches(REMOVE_MARGIN_ELEMENTS)) { element.style.removeProperty('display'); element.style.removeProperty('margin'); } @@ -155,6 +155,7 @@ const wacListItemParser: FormatParser = ( } format.marginLeft = undefined; + format.marginRight = undefined; }; /** @@ -218,6 +219,7 @@ const wacCommentParser: FormatParser = ( export function processPastedContentWacComponents(ev: BeforePasteEvent) { addParser(ev.domToModelOption, 'segment', wacSubSuperParser); addParser(ev.domToModelOption, 'listItemThread', wacListItemParser); + addParser(ev.domToModelOption, 'listItemElement', wacListItemParser); addParser(ev.domToModelOption, 'listLevel', wacListLevelParser); addParser(ev.domToModelOption, 'container', wacContainerParser); addParser(ev.domToModelOption, 'table', wacContainerParser); diff --git a/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnListTest.ts b/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnListTest.ts index 81aeb2f74ca..c96f2291b05 100644 --- a/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnListTest.ts +++ b/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnListTest.ts @@ -2439,6 +2439,118 @@ describe('handleEnterOnList - keyboardEnter', () => { runTest(input, true, expected, false, 1); }); + it('Two separate lists, Enter on first one', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + levels: [{ listType: 'OL', format: { startNumberOverride: 1 }, dataset: {} }], + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { segmentType: 'Text', text: 'test', format: {} }, + { segmentType: 'SelectionMarker', format: {}, isSelected: true }, + ], + }, + ], + format: {}, + formatHolder: { segmentType: 'SelectionMarker', format: {} }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + levels: [{ listType: 'OL', format: { startNumberOverride: 1 }, dataset: {} }], + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [{ segmentType: 'Br', format: {} }], + }, + ], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + format: {}, + }, + }, + ], + }; + + const expectedModel: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + levels: [{ listType: 'OL', format: { startNumberOverride: 1 }, dataset: {} }], + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [{ segmentType: 'Text', text: 'test', format: {} }], + }, + ], + format: {}, + formatHolder: { segmentType: 'SelectionMarker', format: {} }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + levels: [ + { + listType: 'OL', + format: { + startNumberOverride: undefined, + displayForDummyItem: undefined, + }, + dataset: {}, + }, + ], + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { segmentType: 'SelectionMarker', format: {}, isSelected: true }, + { segmentType: 'Br', format: {} }, + ], + }, + ], + format: {}, + formatHolder: { segmentType: 'SelectionMarker', format: {}, isSelected: false }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Br', format: {} }], + format: {}, + }, + ], + levels: [ + { + listType: 'OL', + format: { + startNumberOverride: 1, + }, + dataset: {}, + }, + ], + formatHolder: { segmentType: 'SelectionMarker', format: {} }, + format: {}, + }, + ], + }; + + runTest(model, false, expectedModel, false, 1); + }); + it('List item contains multiple blocks', () => { const model: ContentModelDocument = { blockGroupType: 'Document', diff --git a/packages/roosterjs-content-model-plugins/test/imageEdit/ImageEditPluginTest.ts b/packages/roosterjs-content-model-plugins/test/imageEdit/ImageEditPluginTest.ts index fd16083deef..f0bbc6fd7da 100644 --- a/packages/roosterjs-content-model-plugins/test/imageEdit/ImageEditPluginTest.ts +++ b/packages/roosterjs-content-model-plugins/test/imageEdit/ImageEditPluginTest.ts @@ -1,35 +1,56 @@ -import { ContentModelDocument } from 'roosterjs-content-model-types'; +import * as findImage from '../../lib/imageEdit/utils/findEditingImage'; +import * as getSelectedImage from '../../lib/imageEdit/utils/getSelectedImage'; +import { ChangeSource, createImage, createParagraph } from 'roosterjs-content-model-dom'; import { getSelectedImageMetadata } from '../../lib/imageEdit/utils/updateImageEditInfo'; import { ImageEditPlugin } from '../../lib/imageEdit/ImageEditPlugin'; import { initEditor } from '../TestHelper'; +import { + ContentModelDocument, + ContentModelFormatter, + EditorEnvironment, + FormatContentModelOptions, + IEditor, +} from 'roosterjs-content-model-types'; const model: ContentModelDocument = { blockGroupType: 'Document', blocks: [ { - blockType: 'Paragraph', segments: [ { - segmentType: 'Image', - src: 'test', + isSelected: true, + segmentType: 'SelectionMarker', format: { fontFamily: 'Calibri', fontSize: '11pt', textColor: 'rgb(0, 0, 0)', - id: 'image_0', - maxWidth: '1800px', }, - dataset: {}, + }, + { + src: + 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAsJCQcJCQcJCQkJCwkJCQkJCQsJCwsMCwsLDA0QDB...', isSelectedAsImageSelection: true, + segmentType: 'Image', isSelected: true, + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + id: 'image_0', + maxWidth: '1123px', + }, + dataset: { + isEditing: 'true', + }, }, ], - format: {}, segmentFormat: { fontFamily: 'Calibri', fontSize: '11pt', textColor: 'rgb(0, 0, 0)', }, + blockType: 'Paragraph', + format: {}, }, ], format: { @@ -40,209 +61,181 @@ const model: ContentModelDocument = { }; describe('ImageEditPlugin', () => { - it('keyDown', () => { - const model: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Image', - src: 'test', - format: { - fontFamily: 'Calibri', - fontSize: '11pt', - textColor: 'rgb(0, 0, 0)', - id: 'image_0', - maxWidth: '1800px', - }, - dataset: { - isEditing: 'true', - }, - isSelectedAsImageSelection: true, - isSelected: true, - }, - ], - format: {}, - segmentFormat: { - fontFamily: 'Calibri', - fontSize: '11pt', - textColor: 'rgb(0, 0, 0)', + let editor: IEditor; + let mockedEnvironment: EditorEnvironment; + let attachDomEventSpy: jasmine.Spy; + let getDOMSelectionSpy: jasmine.Spy; + let formatContentModelSpy: jasmine.Spy; + let focusSpy: jasmine.Spy; + let isDarkModeSpy: jasmine.Spy; + let setAttributeSpy: jasmine.Spy; + let appendChildSpy: jasmine.Spy; + let getDOMHelperSpy: jasmine.Spy; + let calculateZoomScaleSpy: jasmine.Spy; + let setEditorStyleSpy: jasmine.Spy; + let triggerEventSpy: jasmine.Spy; + let getAttributeSpy: jasmine.Spy; + beforeEach(() => { + attachDomEventSpy = jasmine.createSpy('attachDomEvent'); + getDOMSelectionSpy = jasmine.createSpy('getDOMSelection'); + mockedEnvironment = { + isSafari: false, + } as any; + getAttributeSpy = jasmine.createSpy('getAttribute'); + const image = createImage(''); + const editImage = createImage('test image'); + image.dataset = { + isEditing: 'true', + }; + const paragraph = createParagraph(); + paragraph.segments.push(image); + paragraph.segments.push(editImage); + spyOn(findImage, 'findEditingImage').and.returnValue({ + image, + paragraph, + }); + spyOn(getSelectedImage, 'getSelectedImage').and.returnValue({ + image: editImage, + paragraph, + }); + formatContentModelSpy = jasmine + .createSpy('formatContentModel') + .and.callFake( + (callback: ContentModelFormatter, _options: FormatContentModelOptions) => { + callback(model, { + newEntities: [], + deletedEntities: [], + newImages: [], + }); + } + ); + focusSpy = jasmine.createSpy('focus'); + isDarkModeSpy = jasmine.createSpy('isDarkMode'); + setAttributeSpy = jasmine.createSpy('setAttribute'); + appendChildSpy = jasmine.createSpy('appendChild'); + calculateZoomScaleSpy = jasmine.createSpy('calculateZoomScale'); + getDOMHelperSpy = jasmine.createSpy('getDOMHelper').and.returnValue({ + calculateZoomScale: calculateZoomScaleSpy, + }); + setEditorStyleSpy = jasmine.createSpy('setEditorStyle'); + triggerEventSpy = jasmine.createSpy('triggerEvent').and.returnValue({ + newSrc: '', + }); + editor = { + getEnvironment: () => mockedEnvironment, + attachDomEvent: attachDomEventSpy, + getDOMSelection: getDOMSelectionSpy, + formatContentModel: formatContentModelSpy, + focus: focusSpy, + isDarkMode: isDarkModeSpy, + getDOMHelper: getDOMHelperSpy, + getDocument: () => ({ + createElement: () => ({ + setAttribute: setAttributeSpy, + appendChild: appendChildSpy, + style: { + display: '', }, - }, - ], - format: { - fontFamily: 'Calibri', - fontSize: '11pt', - textColor: '#000000', - }, + + attachShadow: () => ({ + appendChild: appendChildSpy, + }), + }), + }), + setEditorStyle: setEditorStyleSpy, + triggerEvent: triggerEventSpy, + } as any; + }); + + it('keyDown', () => { + const mockedImage = { + getAttribute: getAttributeSpy, }; const plugin = new ImageEditPlugin(); - const editor = initEditor('image_edit', [plugin], model); plugin.initialize(editor); + getDOMSelectionSpy.and.returnValue({ + type: 'image', + image: mockedImage, + }); + const image = createImage(''); + const paragraph = createParagraph(); + paragraph.segments.push(image); plugin.onPluginEvent({ eventType: 'mouseUp', - isClicking: true, + rawEvent: { button: 0, + target: mockedImage, } as any, }); plugin.onPluginEvent({ eventType: 'keyDown', rawEvent: { key: 'k', + target: mockedImage, } as any, }); - expect(plugin.isEditingImage).toBeFalsy(); + expect(formatContentModelSpy).toHaveBeenCalled(); + expect(formatContentModelSpy).toHaveBeenCalledTimes(2); plugin.dispose(); }); it('mouseUp', () => { - const model: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Image', - src: 'test', - format: { - fontFamily: 'Calibri', - fontSize: '11pt', - textColor: 'rgb(0, 0, 0)', - id: 'image_0', - maxWidth: '1800px', - }, - dataset: {}, - isSelectedAsImageSelection: true, - isSelected: true, - }, - ], - format: {}, - segmentFormat: { - fontFamily: 'Calibri', - fontSize: '11pt', - textColor: 'rgb(0, 0, 0)', - }, - }, - ], - format: { - fontFamily: 'Calibri', - fontSize: '11pt', - textColor: '#000000', - }, + const mockedImage = { + getAttribute: getAttributeSpy, }; const plugin = new ImageEditPlugin(); - const editor = initEditor('image_edit', [plugin], model); plugin.initialize(editor); + getDOMSelectionSpy.and.returnValue({ + type: 'image', + image: mockedImage, + }); plugin.onPluginEvent({ eventType: 'mouseUp', - isClicking: true, + rawEvent: { button: 0, + target: mockedImage, } as any, }); - - expect(plugin.isEditingImage).toBeTruthy(); + expect(formatContentModelSpy).toHaveBeenCalled(); plugin.dispose(); }); it('mouseUp - left click - remove selection', () => { - const model: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Image', - src: 'test', - format: { - fontFamily: 'Calibri', - fontSize: '11pt', - textColor: 'rgb(0, 0, 0)', - id: 'image_0', - maxWidth: '1800px', - }, - dataset: { - isEditing: 'true', - }, - isSelectedAsImageSelection: false, - isSelected: false, - }, - ], - format: {}, - segmentFormat: { - fontFamily: 'Calibri', - fontSize: '11pt', - textColor: 'rgb(0, 0, 0)', - }, - }, - ], - format: { - fontFamily: 'Calibri', - fontSize: '11pt', - textColor: '#000000', - }, + const mockedImage = { + getAttribute: getAttributeSpy, }; const plugin = new ImageEditPlugin(); - const editor = initEditor('image_edit', [plugin], model); plugin.initialize(editor); + getDOMSelectionSpy.and.returnValue({ + type: 'image', + image: mockedImage, + }); plugin.onPluginEvent({ eventType: 'mouseUp', - isClicking: true, + rawEvent: { button: 0, } as any, }); - expect(plugin.isEditingImage).toBeFalsy(); + expect(formatContentModelSpy).toHaveBeenCalled(); plugin.dispose(); }); it('mouseUp - right click - remove wrapper', () => { - const model: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Image', - src: 'test', - format: { - fontFamily: 'Calibri', - fontSize: '11pt', - textColor: 'rgb(0, 0, 0)', - id: 'image_0', - maxWidth: '1800px', - }, - dataset: {}, - isSelectedAsImageSelection: true, - isSelected: true, - }, - ], - format: {}, - segmentFormat: { - fontFamily: 'Calibri', - fontSize: '11pt', - textColor: 'rgb(0, 0, 0)', - }, - }, - ], - format: { - fontFamily: 'Calibri', - fontSize: '11pt', - textColor: '#000000', - }, + const mockedImage = { + getAttribute: getAttributeSpy, }; const plugin = new ImageEditPlugin(); - const editor = initEditor('image_edit', [plugin], model); plugin.initialize(editor); + getDOMSelectionSpy.and.returnValue({ + type: 'image', + image: mockedImage, + }); plugin.onPluginEvent({ eventType: 'mouseUp', - isClicking: true, + rawEvent: { button: 0, target: { @@ -252,7 +245,7 @@ describe('ImageEditPlugin', () => { }); plugin.onPluginEvent({ eventType: 'mouseUp', - isClicking: true, + rawEvent: { button: 2, target: { @@ -261,27 +254,37 @@ describe('ImageEditPlugin', () => { } as any, } as any, }); - - expect(plugin.isEditingImage).toBeFalsy(); + expect(formatContentModelSpy).toHaveBeenCalled(); + expect(formatContentModelSpy).toHaveBeenCalledTimes(2); plugin.dispose(); }); it('cropImage', () => { const plugin = new ImageEditPlugin(); - const editor = initEditor('image_edit', [plugin], model); + const mockedImage = { + getAttribute: getAttributeSpy, + }; plugin.initialize(editor); + getDOMSelectionSpy.and.returnValue({ + type: 'image', + image: mockedImage, + }); plugin.cropImage(); - expect(plugin.isEditingImage).toBeTruthy(); + expect(formatContentModelSpy).toHaveBeenCalled(); plugin.dispose(); }); it('flip', () => { const plugin = new ImageEditPlugin(); - const editor = initEditor('image_edit', [plugin], model); const image = new Image(); image.src = 'test'; + getDOMSelectionSpy.and.returnValue({ + type: 'image', + image: image, + }); plugin.initialize(editor); plugin.flipImage('horizontal'); + expect(formatContentModelSpy).toHaveBeenCalled(); const dataset = getSelectedImageMetadata(editor, image); expect(dataset).toBeTruthy(); plugin.dispose(); @@ -289,12 +292,16 @@ describe('ImageEditPlugin', () => { it('rotate', () => { const plugin = new ImageEditPlugin(); - const editor = initEditor('image_edit', [plugin], model); const image = new Image(); image.src = 'test'; + getDOMSelectionSpy.and.returnValue({ + type: 'image', + image: image, + }); plugin.initialize(editor); plugin.rotateImage(Math.PI / 2); const dataset = getSelectedImageMetadata(editor, image); + expect(formatContentModelSpy).toHaveBeenCalled(); expect(dataset).toBeTruthy(); plugin.dispose(); }); @@ -349,4 +356,78 @@ describe('ImageEditPlugin', () => { ['span:has(>img[id="0"])'] ); }); + + it('mouseDown on edit image', () => { + const mockedImage = { + getAttribute: getAttributeSpy, + }; + const plugin = new ImageEditPlugin(); + plugin.initialize(editor); + getDOMSelectionSpy.and.returnValue({ + type: 'image', + image: mockedImage, + }); + plugin.onPluginEvent({ + eventType: 'mouseUp', + rawEvent: { + button: 0, + target: new Image(), + } as any, + }); + getDOMSelectionSpy.and.returnValue({ + type: 'image', + image: mockedImage, + }); + plugin.onPluginEvent({ + eventType: 'mouseDown', + rawEvent: { + button: 0, + target: new Image(), + } as any, + }); + expect(formatContentModelSpy).toHaveBeenCalled(); + expect(formatContentModelSpy).toHaveBeenCalledTimes(2); + plugin.dispose(); + }); + + it('dragImage', () => { + const mockedImage = { + id: 'image_0', + getAttribute: getAttributeSpy, + } as any; + const plugin = new ImageEditPlugin(); + plugin.initialize(editor); + const draggedImage = mockedImage as HTMLImageElement; + getDOMSelectionSpy.and.returnValue({ + type: 'image', + image: draggedImage, + }); + plugin.onPluginEvent({ + eventType: 'mouseUp', + rawEvent: { + button: 0, + target: new Image(), + } as any, + }); + plugin.onPluginEvent({ + eventType: 'mouseDown', + rawEvent: { + button: 0, + target: new Image(), + } as any, + }); + getDOMSelectionSpy.and.returnValue({ + type: 'image', + image: { + id: 'image_0_dragging', + } as any, + }); + plugin.onPluginEvent({ + eventType: 'contentChanged', + source: ChangeSource.Drop, + }); + expect(formatContentModelSpy).toHaveBeenCalled(); + expect(formatContentModelSpy).toHaveBeenCalledTimes(3); + plugin.dispose(); + }); }); diff --git a/packages/roosterjs-content-model-plugins/test/imageEdit/utils/findEditingImageTest.ts b/packages/roosterjs-content-model-plugins/test/imageEdit/utils/findEditingImageTest.ts index c85e55b326c..3639e370da7 100644 --- a/packages/roosterjs-content-model-plugins/test/imageEdit/utils/findEditingImageTest.ts +++ b/packages/roosterjs-content-model-plugins/test/imageEdit/utils/findEditingImageTest.ts @@ -106,4 +106,347 @@ describe('findEditingImage', () => { }, }); }); + + it('editing image in table', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + widths: [153, 120], + rows: [ + { + height: 157, + cells: [ + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [ + { + src: + 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAsJCQcJCQcJCQkJCwkJCQkJCQsJCwsMCwsLDA0QDB...', + isSelectedAsImageSelection: true, + segmentType: 'Image', + isSelected: true, + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + id: 'image_0', + maxWidth: '773px', + }, + dataset: { + isEditing: 'true', + }, + }, + ], + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + blockType: 'Paragraph', + format: {}, + }, + ], + format: { + borderTop: '1px solid rgb(171, 171, 171)', + borderRight: '1px solid rgb(171, 171, 171)', + borderBottom: '1px solid rgb(171, 171, 171)', + borderLeft: '1px solid rgb(171, 171, 171)', + verticalAlign: 'top', + width: '120px', + height: '22px', + useBorderBox: true, + }, + dataset: {}, + }, + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [ + { + segmentType: 'Br', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + }, + ], + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + blockType: 'Paragraph', + format: {}, + }, + ], + format: { + borderTop: '1px solid rgb(171, 171, 171)', + borderRight: '1px solid rgb(171, 171, 171)', + borderBottom: '1px solid rgb(171, 171, 171)', + borderLeft: '1px solid rgb(171, 171, 171)', + verticalAlign: 'top', + width: '120px', + height: '22px', + useBorderBox: true, + }, + dataset: {}, + }, + ], + format: {}, + }, + { + height: 22, + cells: [ + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [ + { + segmentType: 'Br', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + }, + ], + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + blockType: 'Paragraph', + format: {}, + }, + ], + format: { + borderTop: '1px solid rgb(171, 171, 171)', + borderRight: '1px solid rgb(171, 171, 171)', + borderBottom: '1px solid rgb(171, 171, 171)', + borderLeft: '1px solid rgb(171, 171, 171)', + verticalAlign: 'top', + width: '120px', + height: '22px', + useBorderBox: true, + }, + dataset: {}, + }, + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [ + { + segmentType: 'Br', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + }, + ], + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + blockType: 'Paragraph', + format: {}, + }, + ], + format: { + borderTop: '1px solid rgb(171, 171, 171)', + borderRight: '1px solid rgb(171, 171, 171)', + borderBottom: '1px solid rgb(171, 171, 171)', + borderLeft: '1px solid rgb(171, 171, 171)', + verticalAlign: 'top', + width: '120px', + height: '22px', + useBorderBox: true, + }, + dataset: {}, + }, + ], + format: {}, + }, + ], + blockType: 'Table', + format: { + useBorderBox: true, + borderCollapse: true, + }, + dataset: { + editingInfo: + '{"topBorderColor":"#ABABAB","bottomBorderColor":"#ABABAB","verticalBorderColor":"#ABABAB","hasHeaderRow":false,"hasFirstColumn":false,"hasBandedRows":false,"hasBandedColumns":false,"bgColorEven":null,"bgColorOdd":"#ABABAB20","headerRowColor":"#ABABAB","tableBorderFormat":0,"verticalAlign":"top"}', + }, + }, + { + segments: [ + { + segmentType: 'Br', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + }, + ], + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + blockType: 'Paragraph', + format: {}, + }, + ], + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + }; + + const image = findEditingImage(model); + expect(image).toEqual({ + image: { + src: + 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAsJCQcJCQcJCQkJCwkJCQkJCQsJCwsMCwsLDA0QDB...', + isSelectedAsImageSelection: true, + segmentType: 'Image', + isSelected: true, + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + id: 'image_0', + maxWidth: '773px', + }, + dataset: { + isEditing: 'true', + }, + }, + paragraph: { + segments: [ + { + src: + 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAsJCQcJCQcJCQkJCwkJCQkJCQsJCwsMCwsLDA0QDB...', + isSelectedAsImageSelection: true, + segmentType: 'Image', + isSelected: true, + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + id: 'image_0', + maxWidth: '773px', + }, + dataset: { + isEditing: 'true', + }, + }, + ], + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + blockType: 'Paragraph', + format: {}, + }, + }); + }); + + it('editing image | by Id', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Image', + src: 'test', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + id: 'image_0', + maxWidth: '1800px', + }, + dataset: {}, + }, + ], + format: {}, + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + }, + ], + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + }; + + const image = findEditingImage(model, 'image_0'); + expect(image).toEqual({ + image: { + segmentType: 'Image', + src: 'test', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + id: 'image_0', + maxWidth: '1800px', + }, + dataset: {}, + }, + paragraph: { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Image', + src: 'test', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + id: 'image_0', + maxWidth: '1800px', + }, + dataset: {}, + }, + ], + format: {}, + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + }, + }); + }); }); diff --git a/packages/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts b/packages/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts index c7a175d4093..e78f0beb1eb 100644 --- a/packages/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts +++ b/packages/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts @@ -151,7 +151,7 @@ describe('Content Model Paste Plugin Test', () => { plugin.onPluginEvent(event); expect(WacFile.processPastedContentWacComponents).toHaveBeenCalledWith(event); - expect(addParser.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 6); + expect(addParser.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 7); expect(setProcessor.setProcessor).toHaveBeenCalledTimes(2); }); diff --git a/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelTest.ts b/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelTest.ts index 28a3b6c712a..5e4853c83c5 100644 --- a/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelTest.ts +++ b/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelTest.ts @@ -65,6 +65,15 @@ describe(ID, () => { isSelectedAsImageSelection: undefined, isSelected: undefined, }, + ], + format: {}, + cachedElement: undefined, + isImplicit: undefined, + segmentFormat: undefined, + }, + { + blockType: 'Paragraph', + segments: [ { segmentType: 'SelectionMarker', isSelected: true, @@ -82,6 +91,7 @@ describe(ID, () => { underline: false, }, }, + { segmentType: 'Br', isSelected: undefined, format: {} }, ], format: {}, cachedElement: undefined, diff --git a/packages/roosterjs-content-model-plugins/test/paste/processPastedContentFromWacTest.ts b/packages/roosterjs-content-model-plugins/test/paste/processPastedContentFromWacTest.ts index d45f6a5ea42..50ff7a50e80 100644 --- a/packages/roosterjs-content-model-plugins/test/paste/processPastedContentFromWacTest.ts +++ b/packages/roosterjs-content-model-plugins/test/paste/processPastedContentFromWacTest.ts @@ -21,7 +21,12 @@ let div: HTMLElement; let fragment: DocumentFragment; describe('processPastedContentFromWacTest', () => { - function runTest(source?: string, expected?: string, expectedModel?: ContentModelDocument) { + function runTest( + source?: string, + expected?: string, + expectedModel?: ContentModelDocument, + removeUndefined?: boolean + ) { //Act if (source) { div = document.createElement('div'); @@ -37,7 +42,11 @@ describe('processPastedContentFromWacTest', () => { createDomToModelContext(undefined, event.domToModelOption) ); if (expectedModel) { - expect(model).toEqual(expectedModel); + if (removeUndefined) { + expectEqual(model, expectedModel); + } else { + expect(model).toEqual(expectedModel); + } } contentModelToDom( @@ -57,19 +66,37 @@ describe('processPastedContentFromWacTest', () => { ) ); + const innerHTML = div.innerHTML; //Assert if (expected) { - expect(div.innerHTML).toBe(expected); + expect(innerHTML).toBe(expected); } div.parentElement?.removeChild(div); + + return [innerHTML, model]; } it('Single text node', () => { - runTest('test', 'test'); + runTest( + 'test', + 'test', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'test', format: {} }], + format: {}, + isImplicit: true, + }, + ], + }, + true + ); }); it('Empty DIV', () => { - runTest('
', ''); + runTest('
', '', { blockGroupType: 'Document', blocks: [] }, true); }); it('Single DIV', () => { @@ -101,34 +128,262 @@ describe('processPastedContentFromWacTest', () => { it('Single DIV with child LI', () => { runTest( '
  • 1
  • 2
', - '
  • 1
  • 2
' + '
  • 1
  • 2
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: '1', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: '2', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true ); }); it('Single DIV with deeper child LI', () => { runTest( '
  • 1
  • 2
', - '
  • 1
  • 2
' + '
  • 1
  • 2
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: '1', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: '2', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true ); }); it('Single DIV with text and LI', () => { runTest( '
test
  • 1
', - 'test
  • 1
' + 'test
  • 1
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'test', format: {} }], + format: {}, + isImplicit: true, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: '1', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true ); }); it('Single LI', () => { - runTest('
  • 1
', '
  • 1
'); + runTest( + '
  • 1
', + '
  • 1
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: '1', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true + ); }); it('Single LI and text', () => { - runTest('
  • 1
test', '
  • 1
test'); + runTest( + '
  • 1
test', + '
  • 1
test', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: '1', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'test', format: {} }], + format: {}, + isImplicit: true, + }, + ], + }, + true + ); }); it('Multiple LI', () => { - runTest('
  • 1
  • 2
', '
  • 1
  • 2
'); + runTest( + '
  • 1
  • 2
', + '
  • 1
  • 2
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: '1', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: '2', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true + ); }); }); @@ -186,11 +441,14 @@ describe('wordOnlineHandler', () => { ) ); + const innerHTML = div.innerHTML; //Assert if (expected) { - expect(div.innerHTML).toBe(expected); + expect(innerHTML).toBe(expected); } div.parentElement?.removeChild(div); + + return [innerHTML, model]; } describe('HTML with fragment from Word Online', () => { describe('fragments only contain list items', () => { @@ -207,27 +465,12 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'A', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'A', format: {} }], format: {}, isImplicit: true, }, ], - levels: [ - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], formatHolder: { segmentType: 'SelectionMarker', isSelected: false, @@ -241,27 +484,12 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'B', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'B', format: {} }], format: {}, isImplicit: true, }, ], - levels: [ - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], formatHolder: { segmentType: 'SelectionMarker', isSelected: false, @@ -275,34 +503,14 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'C', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'C', format: {} }], format: {}, isImplicit: true, }, ], levels: [ - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, ], formatHolder: { segmentType: 'SelectionMarker', @@ -312,7 +520,8 @@ describe('wordOnlineHandler', () => { format: {}, }, ], - } + }, + true ); }); @@ -333,27 +542,12 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'A', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'A', format: {} }], format: {}, isImplicit: true, }, ], - levels: [ - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], formatHolder: { segmentType: 'SelectionMarker', isSelected: false, @@ -367,34 +561,14 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'B', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'B', format: {} }], format: {}, isImplicit: true, }, ], levels: [ - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, ], formatHolder: { segmentType: 'SelectionMarker', @@ -409,42 +583,15 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'C', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'C', format: {} }], format: {}, isImplicit: true, }, ], levels: [ - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, ], formatHolder: { segmentType: 'SelectionMarker', @@ -454,7 +601,8 @@ describe('wordOnlineHandler', () => { format: {}, }, ], - } + }, + true ); }); @@ -468,7 +616,7 @@ describe('wordOnlineHandler', () => { it('List items on different level but have different branch in each level', () => { runTest( '
  • A
  • B
  • C
  • D
  • E
', - '
  • A
    • B
      • C
    • D
      • E
', + '
  • A
    • B
      • C
    • D
      • E
', { blockGroupType: 'Document', blocks: [ @@ -478,27 +626,12 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'A', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'A', format: {} }], format: {}, isImplicit: true, }, ], - levels: [ - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], formatHolder: { segmentType: 'SelectionMarker', isSelected: false, @@ -512,34 +645,14 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'B', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'B', format: {} }], format: {}, isImplicit: true, }, ], levels: [ - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, ], formatHolder: { segmentType: 'SelectionMarker', @@ -554,43 +667,17 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'C', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'C', format: {} }], format: {}, isImplicit: true, }, ], levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, { listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - marginTop: '0px', - marginRight: '0px', - marginBottom: '0px', - }, + format: { marginTop: '0px', marginBottom: '0px' }, dataset: {}, }, ], @@ -599,12 +686,7 @@ describe('wordOnlineHandler', () => { isSelected: false, format: {}, }, - format: { - marginTop: '0px', - marginRight: '0px', - marginBottom: '0px', - marginLeft: '120px', - }, + format: { marginTop: '0px', marginBottom: '0px' }, }, { blockType: 'BlockGroup', @@ -612,34 +694,14 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'D', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'D', format: {} }], format: {}, isImplicit: true, }, ], levels: [ - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, ], formatHolder: { segmentType: 'SelectionMarker', @@ -654,43 +716,17 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'E', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'E', format: {} }], format: {}, isImplicit: true, }, ], levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, { listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - marginTop: '0px', - marginRight: '0px', - marginBottom: '0px', - }, + format: { marginTop: '0px', marginBottom: '0px' }, dataset: {}, }, ], @@ -699,15 +735,11 @@ describe('wordOnlineHandler', () => { isSelected: false, format: {}, }, - format: { - marginTop: '0px', - marginRight: '0px', - marginBottom: '0px', - marginLeft: '120px', - }, + format: { marginTop: '0px', marginBottom: '0px' }, }, ], - } + }, + true ); }); @@ -722,7 +754,7 @@ describe('wordOnlineHandler', () => { it('List items on different level with different branch with a combination of order and unordered list items', () => { runTest( '
  • A
  • B
  1. C1
  1. C2
  • D
', - '
  • A
    • B
      1. C1
      2. C2
    • D
', + '
  • A
    • B
      1. C1
      2. C2
    • D
', { blockGroupType: 'Document', blocks: [ @@ -732,27 +764,12 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'A', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'A', format: {} }], format: {}, isImplicit: true, }, ], - levels: [ - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], formatHolder: { segmentType: 'SelectionMarker', isSelected: false, @@ -766,34 +783,14 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'B', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'B', format: {} }], format: {}, isImplicit: true, }, ], levels: [ - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, ], formatHolder: { segmentType: 'SelectionMarker', @@ -808,43 +805,17 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'C1', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'C1', format: {} }], format: {}, isImplicit: true, }, ], levels: [ - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, { listType: 'OL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - marginTop: '0px', - marginRight: '0px', - marginBottom: '0px', - }, + format: { marginTop: '0px', marginBottom: '0px' }, dataset: {}, }, ], @@ -853,12 +824,7 @@ describe('wordOnlineHandler', () => { isSelected: false, format: {}, }, - format: { - marginTop: '0px', - marginRight: '0px', - marginBottom: '0px', - marginLeft: '120px', - }, + format: { marginTop: '0px', marginBottom: '0px' }, }, { blockType: 'BlockGroup', @@ -866,43 +832,17 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'C2', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'C2', format: {} }], format: {}, isImplicit: true, }, ], levels: [ - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, { listType: 'OL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - marginTop: '0px', - marginRight: '0px', - marginBottom: '0px', - }, + format: { marginTop: '0px', marginBottom: '0px' }, dataset: {}, }, ], @@ -911,12 +851,7 @@ describe('wordOnlineHandler', () => { isSelected: false, format: {}, }, - format: { - marginTop: '0px', - marginRight: '0px', - marginBottom: '0px', - marginLeft: '120px', - }, + format: { marginTop: '0px', marginBottom: '0px' }, }, { blockType: 'BlockGroup', @@ -924,34 +859,14 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'D', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'D', format: {} }], format: {}, isImplicit: true, }, ], levels: [ - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, ], formatHolder: { segmentType: 'SelectionMarker', @@ -961,7 +876,8 @@ describe('wordOnlineHandler', () => { format: {}, }, ], - } + }, + true ); }); }); @@ -978,7 +894,142 @@ describe('wordOnlineHandler', () => { it('only has text and list', () => { runTest( '

asdfasdf

  • A
  • B
  1. C1
  1. C2
  • D

asdfasdf

', - '

asdfasdf

  • A
    • B
      1. C1
      2. C2
    • D

asdfasdf

' + '

asdfasdf

  • A
    • B
      1. C1
      2. C2
    • D

asdfasdf

', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'asdfasdf', format: {} }], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'B', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C1', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + { + listType: 'OL', + format: { marginTop: '0px', marginBottom: '0px' }, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: { marginTop: '0px', marginBottom: '0px' }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C2', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + { + listType: 'OL', + format: { marginTop: '0px', marginBottom: '0px' }, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: { marginTop: '0px', marginBottom: '0px' }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'D', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'asdfasdf', format: {} }], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + }, + true ); }); @@ -1001,222 +1052,1071 @@ describe('wordOnlineHandler', () => { it('fragments contains text, list and table that consist of list 2', () => { runTest( '

asdfasdf

  • A
  • B
  1. C1
  1. C2
  • D

asdfasdf

asdfasdf

  • A
  • B
  • C
  • D

', - '

asdfasdf

  • A
    • B
      1. C1
      1. C2
    • D

asdfasdf

asdfasdf

  • A
  • B
  • C
  • D
' - ); - }); - // e.g. - // -------------- -------------- - //| text text | text text | - // -------------- -------------- - //| .a | .a | - // -------------- -------------- - it('fragments contains text, list and table that consist of list', () => { - runTest( - '

asdfasdf

asdfasdf222

  • A
  • A
', - '

asdfasdf

asdfasdf222

  • A
  • A
' - ); - }); - }); - - it('does not have list container', () => { - runTest( - '
  • A
  • B
  • C
  • D
  • E
', - '
  • A
    • B
      • C
    • D
      • E
' - ); - }); - - it('does not have BulletListStyle or NumberListStyle but has ListContainerWrapper', () => { - runTest( - '
  • A
  • B
  • C
', - '
  • A
    • B
      • C
' - ); - }); - - it('does not have BulletListStyle or NumberListStyle but has no ListContainerWrapper', () => { - runTest( - '
  • A
  • B
  • C
', - undefined, - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'A', - format: {}, - }, - ], - format: {}, - isImplicit: true, - }, - ], - levels: [ - { - listType: 'UL', - format: { - marginLeft: undefined, - paddingLeft: undefined, + '

asdfasdf

  • A
    • B
      1. C1
      1. C2
    • D

asdfasdf

asdfasdf

  • A
  • B
  • C
  • D
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'asdfasdf', format: {} }], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, }, - dataset: {}, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, }, - ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: false, format: {}, }, - format: {}, - }, - { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'B', - format: {}, - }, - ], - format: {}, - isImplicit: true, - }, - ], - levels: [ - { - listType: 'UL', - format: { - marginLeft: undefined, - paddingLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - marginLeft: undefined, - paddingLeft: undefined, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'B', format: {} }], + format: {}, + isImplicit: true, }, - dataset: {}, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, }, - ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: false, format: {}, }, - format: {}, - }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C1', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + { + listType: 'OL', + format: { marginTop: '0px', marginBottom: '0px' }, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: { marginTop: '0px', marginBottom: '0px' }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C2', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + { + listType: 'OL', + format: { + marginTop: '0px', + marginBottom: '0px', + startNumberOverride: 1, + }, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: { marginTop: '0px', marginBottom: '0px' }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'D', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'asdfasdf', format: {} }], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + { + blockType: 'Table', + rows: [ + { + height: 0, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'asdfasdf', + format: {}, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { tagName: 'p', format: {} }, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + { + height: 0, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'A', + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { + listType: 'UL', + format: {}, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'B', + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { + listType: 'UL', + format: {}, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'C', + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { + listType: 'UL', + format: {}, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'D', + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { + listType: 'UL', + format: {}, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: {}, + widths: [], + dataset: {}, + }, + ], + }, + true + ); + }); + // e.g. + // -------------- -------------- + //| text text | text text | + // -------------- -------------- + //| .a | .a | + // -------------- -------------- + it('fragments contains text, list and table that consist of list', () => { + runTest( + '

asdfasdf

asdfasdf222

  • A
  • A
', + '

asdfasdf

asdfasdf222

  • A
  • A
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Table', + rows: [ + { + height: 0, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'asdfasdf', + format: {}, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { tagName: 'p', format: {} }, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'asdfasdf222', + format: {}, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { tagName: 'p', format: {} }, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + { + height: 0, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'A', + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { + listType: 'UL', + format: {}, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'A', + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { + listType: 'UL', + format: {}, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: {}, + widths: [], + dataset: {}, + }, + ], + }, + true + ); + }); + }); + + it('does not have list container', () => { + runTest( + '
  • A
  • B
  • C
  • D
  • E
', + '
  • A
    • B
      • C
    • D
      • E
', + { + blockGroupType: 'Document', + blocks: [ { blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'C', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'B', format: {} }], format: {}, isImplicit: true, }, ], levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ { - listType: 'UL', - format: { marginLeft: undefined, paddingLeft: undefined }, - dataset: {}, + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C', format: {} }], + format: {}, + isImplicit: true, }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, { listType: 'UL', - format: { marginLeft: undefined, paddingLeft: undefined }, + format: { marginTop: '0px', marginBottom: '0px' }, dataset: {}, }, - { - listType: 'UL', - format: { marginLeft: undefined, paddingLeft: undefined }, - dataset: {}, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: { marginTop: '0px', marginBottom: '0px' }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'D', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'E', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + { + listType: 'UL', + format: { marginTop: '0px', marginBottom: '0px' }, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: { marginTop: '0px', marginBottom: '0px' }, + }, + ], + }, + true + ); + }); + + it('does not have BulletListStyle or NumberListStyle but has ListContainerWrapper', () => { + runTest( + '
  • A
  • B
  • C
', + '
  • A
    • B
      • C
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'B', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true + ); + }); + + it('does not have BulletListStyle or NumberListStyle but has no ListContainerWrapper', () => { + runTest( + '
  • A
  • B
  • C
', + '
  • A
    • B
      • C
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'B', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true + ); + }); + + describe('When html is not strictly formatted as word online, but can be identified as word online only contains one type of list', () => { + // html: + //
+ //
  • text
+ //
  • text
+ //
+ // result: + // .a + // .b + // .c + it('should process html properly, if ListContainerWrapper contains two UL', () => { + runTest( + '
  • A
  • B
  • C
', + '
  • A
  • B
  • C
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'B', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true + ); + }); + + // html: + //
+ //
    + //
  • text
  • + //
  • text
  • + //
  • text
  • + //
    + // result: + // .test + // .test + // .test + it('shuold process html properly, when list items are not in side ul tag', () => { + runTest( + '
    • test

    • test

    • test

    • ', + '
    • test

    • test

    • test

    • ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'General', + element: jasmine.anything() as any, + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'Text', text: 'test', format: {} }, + ], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'General', + element: jasmine.anything() as any, + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'Text', text: 'test', format: {} }, + ], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'General', + element: jasmine.anything() as any, + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'Text', text: 'test', format: {} }, + ], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + format: {}, + }, + ], + }, + true + ); + }); + + // html: + //
      + //
        + //
      • text
      • + //
      • text
      • + //
          + //
        • text
        • + //
        • text
        • + //
        + //
      + //
      + // result: + // .text + // .text + // .text + // .text + // .text + it('should process html properly, if ListContainerWrapper contains list that is already well formatted', () => { + runTest( + '
      • A
        • B
          • C
        • D
          • E
      ', + '
      • A
        • B
          • C
        • D
          • E
      ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'B', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'D', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'E', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, }, - ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: false, format: {}, }, - format: {}, - }, - ], - } - ); - }); - - describe('When html is not strictly formatted as word online, but can be identified as word online only contains one type of list', () => { - // html: - //
      - //
      • text
      - //
      • text
      - //
      - // result: - // .a - // .b - // .c - it('should process html properly, if ListContainerWrapper contains two UL', () => { - runTest( - '
      • A
      • B
      • C
      ', - '
      • A
      • B
      • C
      ' - ); - }); - - // html: - //
      - //
        - //
      • text
      • - //
      • text
      • - //
      • text
      • - //
        - // result: - // .test - // .test - // .test - it('shuold process html properly, when list items are not in side ul tag', () => { - runTest( - '
        • test

        • test

        • test

        • ', - '
        • test

        • test

        • test

        • ' - ); - }); - - // html: - //
          - //
            - //
          • text
          • - //
          • text
          • - //
              - //
            • text
            • - //
            • text
            • - //
            - //
          - //
          - // result: - // .text - // .text - // .text - // .text - // .text - it('should process html properly, if ListContainerWrapper contains list that is already well formatted', () => { - runTest( - '
          • A
            • B
              • C
            • D
              • E
          ', - '
          • A
            • B
              • C
            • D
              • E
          ' + ], + }, + true ); }); @@ -1234,7 +2134,76 @@ describe('wordOnlineHandler', () => { it('should process html properly, if there are multiple list item in ol (word online has one list item in each ol for ordered list)', () => { runTest( '
          1. A
          2. B
          1. C
          ', - '
          1. A
          2. B
          1. C
          ' + '
          1. A
          2. B
          1. C
          ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'B', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { + listType: 'OL', + format: { startNumberOverride: 1 }, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true ); }); @@ -1250,7 +2219,61 @@ describe('wordOnlineHandler', () => { it('shuold process html properly, if list item in a ListContainerWrapper are not inside ol ', () => { runTest( '
          1. test

          2. test

          3. test

          4. ', - '
          5. test

          6. test

          7. test

          8. ' + '
          9. test

          10. test

          11. test

          12. ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'General', + element: jasmine.anything() as any, + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'Text', text: 'test', format: {} }, + ], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'General', + element: jasmine.anything() as any, + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'Text', text: 'test', format: {} }, + ], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'General', + element: jasmine.anything() as any, + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'Text', text: 'test', format: {} }, + ], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + format: {}, + }, + ], + }, + true ); }); }); @@ -1267,7 +2290,51 @@ describe('wordOnlineHandler', () => { it('should process html properly, if ListContainerWrapper contains well formated UL and non formated ol', () => { runTest( '
            • A
            1. B
            ', - '
            • A
            1. B
            ' + '
            • A
            1. B
            ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'B', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true ); }); @@ -1286,7 +2353,70 @@ describe('wordOnlineHandler', () => { it('should process html properly, if ListContainerWrapper contains two OL', () => { runTest( '
            • A
            1. B
            1. C
            ', - '
            • A
            1. B
            2. C
            ' + '
            • A
            1. B
            2. C
            ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'B', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true ); }); @@ -1303,7 +2433,76 @@ describe('wordOnlineHandler', () => { it('should process html properly, if ListContainerWrapper contains two OL and one UL', () => { runTest( '
            • A
            1. B
            1. C
            ', - '
            • A
            1. B
            1. C
            ' + '
            • A
            1. B
            1. C
            ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'B', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { + listType: 'OL', + format: { startNumberOverride: 1 }, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true ); }); @@ -1318,7 +2517,51 @@ describe('wordOnlineHandler', () => { it('should process html properly, if there are list not in the ListContainerWrapper', () => { runTest( '
            1. C
            • A
            ', - '
            1. C
            • A
            ' + '
            1. C
            • A
            ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true ); }); @@ -1337,7 +2580,89 @@ describe('wordOnlineHandler', () => { it('should process html properly, if ListContainerWrapper contains two UL', () => { runTest( '
            1. C
            • A
            • A
            • A
            ', - '
            1. C
            • A
            • A
            • A
            ' + '
            1. C
            • A
            • A
            • A
            ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true ); }); @@ -1349,7 +2674,38 @@ describe('wordOnlineHandler', () => { it('should retain all text, if ListContainerWrapper contains Elements before li and ul', () => { runTest( '

            paragraph

            1. C
            ', - '

            paragraph

            1. C
            ' + '

            paragraph

            1. C
            ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'paragraph', format: {} }], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true ); }); @@ -1361,7 +2717,38 @@ describe('wordOnlineHandler', () => { it('should retain all text, if ListContainerWrapper contains Elements after li and ul', () => { runTest( '
            1. C

            paragraph

            ', - '
            1. C

            paragraph

            ' + '
            1. C

            paragraph

            ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'paragraph', format: {} }], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + }, + true ); }); }); @@ -1419,7 +2806,38 @@ describe('wordOnlineHandler', () => { it('List directly under fragment', () => { runTest( '
            • A

            B

            ', - '
            • A

            B

            ' + '
            • A

            B

            ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'B', format: {} }], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + }, + true ); }); @@ -1432,7 +2850,92 @@ describe('wordOnlineHandler', () => { it('should remove the display and margin styles from the element', () => { runTest( '
            • A

            • B

            • C

              1. D

            ', - '
            • A

            • B

            • C

              1. D

            ' + '
            • A

            • B

            • C

              1. D

            ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'B', format: {} }], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C', format: {} }], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'D', format: {} }], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'OL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true ); }); }); @@ -1537,27 +3040,12 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'List1', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'List1', format: {} }], format: {}, isImplicit: true, }, ], - levels: [ - { - listType: 'UL', - format: { - marginLeft: undefined, - paddingLeft: undefined, - }, - dataset: {}, - }, - ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], formatHolder: { segmentType: 'SelectionMarker', isSelected: false, @@ -1577,27 +3065,12 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'List2', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'List2', format: {} }], format: {}, isImplicit: true, }, ], - levels: [ - { - listType: 'UL', - format: { - marginLeft: undefined, - paddingLeft: undefined, - }, - dataset: {}, - }, - ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], formatHolder: { segmentType: 'SelectionMarker', isSelected: false, @@ -1606,7 +3079,8 @@ describe('wordOnlineHandler', () => { format: {}, }, ], - } + }, + true ); }); @@ -1623,7 +3097,81 @@ describe('wordOnlineHandler', () => { it('Remove temp marker from Word Online', () => { runTest( '

            it went:  

            1. Test

            1. Test. 


            ', - '

            it went:  

            1. Test

            2. Test. 


            ' + '

            it went:  

            1. Test

            2. Test. 


            ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'Text', text: 'it went: ', format: {} }, + { segmentType: 'Text', text: ' ', format: {} }, + ], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'Test', format: {} }], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'Text', text: 'Test.', format: {} }, + { segmentType: 'Text', text: ' ', format: {} }, + ], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Br', format: {} }], + format: {}, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true ); }); @@ -2856,39 +4404,20 @@ describe('wordOnlineHandler', () => { it('Test with multiple list items', () => { runTest( '
            1. _ 

            1. _ 

            1. _ 

            1. _ 

            1. _ 

            1. _ 

            1. _ 

            1. _ 

            1. _ 

            1. _ 

             

             

            _ 

             

            1. _ 

            _ 

            1. _ 

             

            _ 

             

            1. _ 

            1. _ 

             

            _ 

            1. _ 

            1. _ 

            ', - undefined, + '

             

             

             

             

             

             

            ', { blockGroupType: 'Document', blocks: [ { - formatHolder: { - isSelected: false, - segmentType: 'SelectionMarker', - format: { - fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', - fontSize: '12pt', - }, - }, - levels: [ - { - listType: 'OL', - format: { - direction: 'ltr', - }, - dataset: {}, - }, - ], blockType: 'BlockGroup', - format: { - direction: 'ltr', - }, blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -2899,8 +4428,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -2911,12 +4440,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -2926,42 +4449,41 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, + }, + ], + levels: [ + { + listType: 'OL', + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + dataset: {}, }, ], - }, - { formatHolder: { - isSelected: false, segmentType: 'SelectionMarker', + isSelected: false, format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', }, }, - levels: [ - { - listType: 'OL', - format: { - direction: 'ltr', - }, - dataset: {}, - }, - ], + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + }, + { blockType: 'BlockGroup', - format: { - direction: 'ltr', - }, blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -2972,8 +4494,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -2984,12 +4506,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -2999,49 +4515,41 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, + }, + ], + levels: [ + { + listType: 'OL', + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + dataset: {}, }, ], - }, - { formatHolder: { - isSelected: false, segmentType: 'SelectionMarker', + isSelected: false, format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', }, }, - levels: [ - { - listType: 'OL', - format: { - direction: 'ltr', - }, - dataset: {}, - }, - { - listType: 'OL', - format: { - direction: 'ltr', - }, - dataset: {}, - }, - ], + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + }, + { blockType: 'BlockGroup', - format: { - direction: 'ltr', - }, blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3052,8 +4560,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3064,12 +4572,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -3079,49 +4581,51 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], - }, - { - formatHolder: { - isSelected: false, - segmentType: 'SelectionMarker', - format: { - fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', - fontSize: '12pt', - }, - }, levels: [ { listType: 'OL', format: { direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', }, dataset: {}, }, { listType: 'OL', - format: { - direction: 'ltr', - }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, dataset: {}, }, ], - blockType: 'BlockGroup', - format: { - direction: 'ltr', + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + }, }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3132,8 +4636,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3144,12 +4648,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -3159,56 +4657,51 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], - }, - { - formatHolder: { - isSelected: false, - segmentType: 'SelectionMarker', - format: { - fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', - fontSize: '12pt', - }, - }, levels: [ { listType: 'OL', format: { direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', }, dataset: {}, }, { listType: 'OL', - format: { - direction: 'ltr', - }, - dataset: {}, - }, - { - listType: 'OL', - format: { - direction: 'ltr', - }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, dataset: {}, }, ], - blockType: 'BlockGroup', - format: { - direction: 'ltr', + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + }, }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3219,8 +4712,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3231,12 +4724,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -3246,27 +4733,22 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], - }, - { - formatHolder: { - isSelected: false, - segmentType: 'SelectionMarker', - format: { - fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', - fontSize: '12pt', - }, - }, levels: [ { listType: 'OL', format: { direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', }, dataset: {}, }, @@ -3274,28 +4756,38 @@ describe('wordOnlineHandler', () => { listType: 'OL', format: { direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', }, dataset: {}, }, { listType: 'OL', - format: { - direction: 'ltr', - }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, dataset: {}, }, ], - blockType: 'BlockGroup', - format: { - direction: 'ltr', + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + }, }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3306,8 +4798,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3318,12 +4810,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -3333,27 +4819,22 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], - }, - { - formatHolder: { - isSelected: false, - segmentType: 'SelectionMarker', - format: { - fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', - fontSize: '12pt', - }, - }, levels: [ { listType: 'OL', format: { direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', }, dataset: {}, }, @@ -3361,21 +4842,38 @@ describe('wordOnlineHandler', () => { listType: 'OL', format: { direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', }, dataset: {}, }, + { + listType: 'OL', + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + dataset: {}, + }, ], - blockType: 'BlockGroup', - format: { - direction: 'ltr', + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + }, }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3386,8 +4884,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3398,12 +4896,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -3413,49 +4905,51 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], - }, - { - formatHolder: { - isSelected: false, - segmentType: 'SelectionMarker', - format: { - fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', - fontSize: '12pt', - }, - }, levels: [ { listType: 'OL', format: { direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', }, dataset: {}, }, { listType: 'OL', - format: { - direction: 'ltr', - }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, dataset: {}, }, ], - blockType: 'BlockGroup', - format: { - direction: 'ltr', + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + }, }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3466,8 +4960,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3478,12 +4972,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -3493,42 +4981,51 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], - }, - { - formatHolder: { - isSelected: false, - segmentType: 'SelectionMarker', - format: { - fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', - fontSize: '12pt', - }, - }, levels: [ { listType: 'OL', format: { direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', }, dataset: {}, }, + { + listType: 'OL', + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + dataset: {}, + }, ], - blockType: 'BlockGroup', - format: { - direction: 'ltr', + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + }, }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3539,8 +5036,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3551,12 +5048,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -3566,42 +5057,41 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, + }, + ], + levels: [ + { + listType: 'OL', + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + dataset: {}, }, ], - }, - { formatHolder: { - isSelected: false, segmentType: 'SelectionMarker', + isSelected: false, format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', }, }, - levels: [ - { - listType: 'OL', - format: { - direction: 'ltr', - }, - dataset: {}, - }, - ], + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + }, + { blockType: 'BlockGroup', - format: { - direction: 'ltr', - }, blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3612,8 +5102,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3624,12 +5114,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -3639,33 +5123,42 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, + }, + ], + levels: [ + { + listType: 'OL', + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + dataset: {}, }, ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + }, + }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, }, { - tagName: 'div', blockType: 'BlockGroup', - format: { - direction: 'ltr', - textAlign: 'start', - textIndent: '0px', - backgroundColor: 'rgb(255, 255, 255)', - marginTop: '0px', - marginRight: '0px', - marginBottom: '0px', - marginLeft: '0px', - }, blockGroupType: 'FormatContainer', + tagName: 'div', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3676,12 +5169,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { direction: 'ltr', textAlign: 'start', @@ -3692,16 +5179,14 @@ describe('wordOnlineHandler', () => { marginBottom: '10.6667px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], - }, - { - tagName: 'div', - blockType: 'BlockGroup', format: { direction: 'ltr', textAlign: 'start', @@ -3712,13 +5197,18 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'FormatContainer', + tagName: 'div', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3729,12 +5219,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { direction: 'ltr', textAlign: 'start', @@ -3745,16 +5229,14 @@ describe('wordOnlineHandler', () => { marginBottom: '10.6667px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], - }, - { - tagName: 'div', - blockType: 'BlockGroup', format: { direction: 'ltr', textAlign: 'start', @@ -3765,13 +5247,18 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'FormatContainer', + tagName: 'div', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3782,8 +5269,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3794,12 +5281,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { direction: 'ltr', textAlign: 'start', @@ -3810,16 +5291,14 @@ describe('wordOnlineHandler', () => { marginBottom: '10.6667px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], - }, - { - tagName: 'div', - blockType: 'BlockGroup', format: { direction: 'ltr', textAlign: 'start', @@ -3830,76 +5309,67 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'FormatContainer', + tagName: 'div', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', - italic: false, - textColor: 'rgb(0, 0, 0)', - fontWeight: 'normal', - lineHeight: '22.0875px', - }, - }, - ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', - format: { - direction: 'ltr', - textAlign: 'start', - textIndent: '0px', - whiteSpace: 'pre-wrap', - marginTop: '0px', - marginRight: '0px', - marginBottom: '10.6667px', - marginLeft: '0px', - }, - decorator: { - tagName: 'p', - format: {}, - }, - }, - ], - }, - { - formatHolder: { - isSelected: false, - segmentType: 'SelectionMarker', - format: { - fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', - fontSize: '12pt', - }, - }, - levels: [ - { - listType: 'OL', + italic: false, + textColor: 'rgb(0, 0, 0)', + fontWeight: 'normal', + lineHeight: '22.0875px', + }, + }, + ], format: { direction: 'ltr', - startNumberOverride: 1, + textAlign: 'start', + textIndent: '0px', + whiteSpace: 'pre-wrap', + marginTop: '0px', + marginRight: '0px', + marginBottom: '10.6667px', + marginLeft: '0px', }, - dataset: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', + }, + decorator: { tagName: 'p', format: {} }, }, ], - blockType: 'BlockGroup', format: { direction: 'ltr', + textAlign: 'start', + textIndent: '0px', + backgroundColor: 'rgb(255, 255, 255)', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '0px', }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3910,8 +5380,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3922,12 +5392,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -3937,33 +5401,47 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], + levels: [ + { + listType: 'OL', + format: { + direction: 'ltr', + marginTop: '0px', + marginBottom: '0px', + startNumberOverride: 1, + }, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + }, + }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, }, { - tagName: 'div', blockType: 'BlockGroup', - format: { - direction: 'ltr', - textAlign: 'start', - textIndent: '0px', - backgroundColor: 'rgb(255, 255, 255)', - marginTop: '0px', - marginRight: '0px', - marginBottom: '0px', - marginLeft: '0px', - }, blockGroupType: 'FormatContainer', + tagName: 'div', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3974,8 +5452,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3986,12 +5464,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { direction: 'ltr', textAlign: 'start', @@ -4002,42 +5474,35 @@ describe('wordOnlineHandler', () => { marginBottom: '10.6667px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, - }, - }, - ], - }, - { - formatHolder: { - isSelected: false, - segmentType: 'SelectionMarker', - format: { - fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', - fontSize: '12pt', - }, - }, - levels: [ - { - listType: 'OL', - format: { - direction: 'ltr', + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, - dataset: {}, + decorator: { tagName: 'p', format: {} }, }, ], - blockType: 'BlockGroup', format: { direction: 'ltr', + textAlign: 'start', + textIndent: '0px', + backgroundColor: 'rgb(255, 255, 255)', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '0px', }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4048,8 +5513,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4060,12 +5525,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -4075,33 +5534,42 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, + }, + ], + levels: [ + { + listType: 'OL', + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + dataset: {}, }, ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + }, + }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, }, { - tagName: 'div', blockType: 'BlockGroup', - format: { - direction: 'ltr', - textAlign: 'start', - textIndent: '0px', - backgroundColor: 'rgb(255, 255, 255)', - marginTop: '0px', - marginRight: '0px', - marginBottom: '0px', - marginLeft: '0px', - }, blockGroupType: 'FormatContainer', + tagName: 'div', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4112,12 +5580,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { direction: 'ltr', textAlign: 'start', @@ -4128,16 +5590,14 @@ describe('wordOnlineHandler', () => { marginBottom: '10.6667px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], - }, - { - tagName: 'div', - blockType: 'BlockGroup', format: { direction: 'ltr', textAlign: 'start', @@ -4148,13 +5608,18 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'FormatContainer', + tagName: 'div', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4165,8 +5630,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4177,12 +5642,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { direction: 'ltr', textAlign: 'start', @@ -4193,16 +5652,14 @@ describe('wordOnlineHandler', () => { marginBottom: '10.6667px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], - }, - { - tagName: 'div', - blockType: 'BlockGroup', format: { direction: 'ltr', textAlign: 'start', @@ -4213,76 +5670,67 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'FormatContainer', + tagName: 'div', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', italic: false, - textColor: 'rgb(0, 0, 0)', - fontWeight: 'normal', - lineHeight: '22.0875px', - }, - }, - ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', - format: { - direction: 'ltr', - textAlign: 'start', - textIndent: '0px', - whiteSpace: 'pre-wrap', - marginTop: '0px', - marginRight: '0px', - marginBottom: '10.6667px', - marginLeft: '0px', - }, - decorator: { - tagName: 'p', - format: {}, - }, - }, - ], - }, - { - formatHolder: { - isSelected: false, - segmentType: 'SelectionMarker', - format: { - fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', - fontSize: '12pt', - }, - }, - levels: [ - { - listType: 'OL', + textColor: 'rgb(0, 0, 0)', + fontWeight: 'normal', + lineHeight: '22.0875px', + }, + }, + ], format: { direction: 'ltr', - startNumberOverride: 1, + textAlign: 'start', + textIndent: '0px', + whiteSpace: 'pre-wrap', + marginTop: '0px', + marginRight: '0px', + marginBottom: '10.6667px', + marginLeft: '0px', }, - dataset: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', + }, + decorator: { tagName: 'p', format: {} }, }, ], - blockType: 'BlockGroup', format: { direction: 'ltr', + textAlign: 'start', + textIndent: '0px', + backgroundColor: 'rgb(255, 255, 255)', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '0px', }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4293,8 +5741,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4305,12 +5753,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -4320,42 +5762,46 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], - }, - { - formatHolder: { - isSelected: false, - segmentType: 'SelectionMarker', - format: { - fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', - fontSize: '12pt', - }, - }, levels: [ { listType: 'OL', format: { direction: 'ltr', + marginTop: '0px', + marginBottom: '0px', + startNumberOverride: 1, }, dataset: {}, }, ], - blockType: 'BlockGroup', - format: { - direction: 'ltr', + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + }, }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4366,8 +5812,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4378,12 +5824,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -4393,33 +5833,42 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, + }, + ], + levels: [ + { + listType: 'OL', + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + dataset: {}, }, ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + }, + }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, }, { - tagName: 'div', blockType: 'BlockGroup', - format: { - direction: 'ltr', - textAlign: 'start', - textIndent: '0px', - backgroundColor: 'rgb(255, 255, 255)', - marginTop: '0px', - marginRight: '0px', - marginBottom: '0px', - marginLeft: '0px', - }, blockGroupType: 'FormatContainer', + tagName: 'div', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4430,12 +5879,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { direction: 'ltr', textAlign: 'start', @@ -4446,16 +5889,14 @@ describe('wordOnlineHandler', () => { marginBottom: '10.6667px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], - }, - { - tagName: 'div', - blockType: 'BlockGroup', format: { direction: 'ltr', textAlign: 'start', @@ -4466,13 +5907,18 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'FormatContainer', + tagName: 'div', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4483,8 +5929,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4495,12 +5941,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { direction: 'ltr', textAlign: 'start', @@ -4511,43 +5951,35 @@ describe('wordOnlineHandler', () => { marginBottom: '10.6667px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, - }, - }, - ], - }, - { - formatHolder: { - isSelected: false, - segmentType: 'SelectionMarker', - format: { - fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', - fontSize: '12pt', - }, - }, - levels: [ - { - listType: 'OL', - format: { - direction: 'ltr', - startNumberOverride: 1, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, - dataset: {}, + decorator: { tagName: 'p', format: {} }, }, ], - blockType: 'BlockGroup', format: { direction: 'ltr', + textAlign: 'start', + textIndent: '0px', + backgroundColor: 'rgb(255, 255, 255)', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '0px', }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4558,8 +5990,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4570,12 +6002,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -4585,42 +6011,46 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], - }, - { - formatHolder: { - isSelected: false, - segmentType: 'SelectionMarker', - format: { - fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', - fontSize: '12pt', - }, - }, levels: [ { listType: 'OL', format: { direction: 'ltr', + marginTop: '0px', + marginBottom: '0px', + startNumberOverride: 1, }, dataset: {}, }, ], - blockType: 'BlockGroup', - format: { - direction: 'ltr', + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + }, }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4631,8 +6061,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4643,12 +6073,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -4658,12 +6082,30 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, + }, + ], + levels: [ + { + listType: 'OL', + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + dataset: {}, }, ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + }, + }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, }, ], }, diff --git a/packages/roosterjs-content-model-types/lib/context/DomIndexer.ts b/packages/roosterjs-content-model-types/lib/context/DomIndexer.ts index f1d68f845a9..7c600dc0c8f 100644 --- a/packages/roosterjs-content-model-types/lib/context/DomIndexer.ts +++ b/packages/roosterjs-content-model-types/lib/context/DomIndexer.ts @@ -1,9 +1,11 @@ +import type { ContentModelBlockGroup } from '../contentModel/blockGroup/ContentModelBlockGroup'; import type { CacheSelection } from '../pluginState/CachePluginState'; import type { ContentModelDocument } from '../contentModel/blockGroup/ContentModelDocument'; import type { ContentModelParagraph } from '../contentModel/block/ContentModelParagraph'; import type { ContentModelSegment } from '../contentModel/segment/ContentModelSegment'; import type { ContentModelTable } from '../contentModel/block/ContentModelTable'; import type { DOMSelection } from '../selection/DOMSelection'; +import type { ContentModelEntity } from '../contentModel/entity/ContentModelEntity'; /** * Represents an indexer object which provides methods to help build backward relationship @@ -34,6 +36,13 @@ export interface DomIndexer { */ onTable: (tableElement: HTMLTableElement, tableModel: ContentModelTable) => void; + /** + * Invoke when new block entity is created in DOM tree + * @param entity The related entity + * @param parent Parent of entity. For block element, this should be the parent block group. For inline entity, this should be the parent paragraph + */ + onBlockEntity: (entity: ContentModelEntity, group: ContentModelBlockGroup) => void; + /** * When document content or selection is changed by user, we need to use this function to update the content model * to reflect the latest document. This process can fail since the selected node may not have a related model data structure. @@ -48,6 +57,13 @@ export interface DomIndexer { oldSelection?: CacheSelection ) => boolean; + /** + * When id is changed from DOM element, update the new ID to related content model if possible + * @param element The element that has id changed + * @returns True if successfully updated, otherwise false + */ + reconcileElementId: (element: HTMLElement) => boolean; + /** * When child list of editor content is changed, we can use this method to do sync the change from editor into content model. * This is mostly used when user start to type in an empty line. In that case browser will remove the existing BR node in the empty line if any, diff --git a/packages/roosterjs-content-model-types/lib/event/BeforePasteEvent.ts b/packages/roosterjs-content-model-types/lib/event/BeforePasteEvent.ts index 2fbe371b9cc..870a81afcf7 100644 --- a/packages/roosterjs-content-model-types/lib/event/BeforePasteEvent.ts +++ b/packages/roosterjs-content-model-types/lib/event/BeforePasteEvent.ts @@ -62,4 +62,9 @@ export interface BeforePasteEvent extends BasePluginEvent<'beforePaste'> { * customizedMerge Customized merge function to use when merging the paste fragment into the editor */ customizedMerge?: MergePastedContentFunc; + + /** + * Whether the current clipboard contains at least a block element. + */ + readonly containsBlockElements?: boolean; } diff --git a/packages/roosterjs-content-model-types/lib/parameter/DOMHelper.ts b/packages/roosterjs-content-model-types/lib/parameter/DOMHelper.ts index 9bb886a6ac8..91bd2d976ad 100644 --- a/packages/roosterjs-content-model-types/lib/parameter/DOMHelper.ts +++ b/packages/roosterjs-content-model-types/lib/parameter/DOMHelper.ts @@ -5,8 +5,9 @@ export interface DOMHelper { /** * Check if the given DOM node is in editor * @param node The node to check + * @param excludeRoot When pass true, the function will return false if the passed in node is the root node itself */ - isNodeInEditor(node: Node): boolean; + isNodeInEditor(node: Node, excludeRoot?: boolean): boolean; /** * Query HTML elements in editor by tag name. diff --git a/packages/roosterjs-content-model-types/lib/parameter/MergeModelOption.ts b/packages/roosterjs-content-model-types/lib/parameter/MergeModelOption.ts index 90f84d91f52..25a24267e52 100644 --- a/packages/roosterjs-content-model-types/lib/parameter/MergeModelOption.ts +++ b/packages/roosterjs-content-model-types/lib/parameter/MergeModelOption.ts @@ -27,4 +27,9 @@ export interface MergeModelOption { * @default undefined */ mergeFormat?: 'mergeAll' | 'keepSourceEmphasisFormat' | 'none'; + + /** + * Whether to add a paragraph after the merged content. + */ + addParagraphAfterMergedContent?: boolean; } diff --git a/yarn.lock b/yarn.lock index 536a974e869..939728d0c1b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -471,7 +471,7 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== -"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": +"@jridgewell/gen-mapping@^0.3.2": version "0.3.3" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== @@ -480,23 +480,42 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + "@jridgewell/resolve-uri@3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + "@jridgewell/set-array@^1.0.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== -"@jridgewell/source-map@^0.3.2": - version "0.3.3" - resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.3.tgz#8108265659d4c33e72ffe14e33d6cc5eb59f2fda" - integrity sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg== +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/source-map@^0.3.3": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" + integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== dependencies: - "@jridgewell/gen-mapping" "^0.3.0" - "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" "@jridgewell/sourcemap-codec@1.4.14": version "1.4.14" @@ -508,6 +527,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== +"@jridgewell/sourcemap-codec@^1.4.14": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": version "0.3.18" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6" @@ -516,6 +540,14 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@jsdevtools/coverage-istanbul-loader@3.0.5": version "3.0.5" resolved "https://registry.yarnpkg.com/@jsdevtools/coverage-istanbul-loader/-/coverage-istanbul-loader-3.0.5.tgz#2a4bc65d0271df8d4435982db4af35d81754ee26" @@ -614,26 +646,10 @@ dependencies: "@types/trusted-types" "*" -"@types/eslint-scope@^3.7.3": - version "3.7.4" - resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" - integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== - dependencies: - "@types/eslint" "*" - "@types/estree" "*" - -"@types/eslint@*": - version "8.40.0" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.40.0.tgz#ae73dc9ec5237f2794c4f79efd6a4c73b13daf23" - integrity sha512-nbq2mvc/tBrK9zQQuItvjJl++GTN5j06DaPtp3hZCpngmG6Q3xoyEmd0TwZI0gAy/G1X0zhGBbr2imsGFdFV0g== - dependencies: - "@types/estree" "*" - "@types/json-schema" "*" - -"@types/estree@*", "@types/estree@^1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194" - integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA== +"@types/estree@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== "@types/events@*": version "3.0.0" @@ -654,11 +670,6 @@ resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.5.10.tgz#a1a41012012b5da9d4b205ba9eba58f6cce2ab7b" integrity sha512-3F8qpwBAiVc5+HPJeXJpbrl+XjawGmciN5LgiO7Gv1pl1RHtjoMNqZpqEksaPJW05ViKe8snYInRs6xB25Xdew== -"@types/json-schema@*", "@types/json-schema@^7.0.8": - version "7.0.12" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" - integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== - "@types/json-schema@^7.0.12": version "7.0.13" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.13.tgz#02c24f4363176d2d18fc8b70b9f3c54aba178a85" @@ -669,6 +680,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd" integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ== +"@types/json-schema@^7.0.8": + version "7.0.12" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" + integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== + "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" @@ -903,10 +919,10 @@ "@typescript-eslint/types" "6.7.3" eslint-visitor-keys "^3.4.1" -"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" - integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q== +"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" + integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== dependencies: "@webassemblyjs/helper-numbers" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" @@ -921,10 +937,10 @@ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== -"@webassemblyjs/helper-buffer@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093" - integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA== +"@webassemblyjs/helper-buffer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6" + integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== "@webassemblyjs/helper-numbers@1.11.6": version "1.11.6" @@ -940,15 +956,15 @@ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== -"@webassemblyjs/helper-wasm-section@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577" - integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g== +"@webassemblyjs/helper-wasm-section@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf" + integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g== dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-gen" "1.12.1" "@webassemblyjs/ieee754@1.11.6": version "1.11.6" @@ -969,59 +985,59 @@ resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== -"@webassemblyjs/wasm-edit@^1.11.5": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab" - integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw== +"@webassemblyjs/wasm-edit@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" + integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g== dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/helper-wasm-section" "1.11.6" - "@webassemblyjs/wasm-gen" "1.11.6" - "@webassemblyjs/wasm-opt" "1.11.6" - "@webassemblyjs/wasm-parser" "1.11.6" - "@webassemblyjs/wast-printer" "1.11.6" - -"@webassemblyjs/wasm-gen@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268" - integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA== - dependencies: - "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-opt" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + "@webassemblyjs/wast-printer" "1.12.1" + +"@webassemblyjs/wasm-gen@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547" + integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w== + dependencies: + "@webassemblyjs/ast" "1.12.1" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/ieee754" "1.11.6" "@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/utf8" "1.11.6" -"@webassemblyjs/wasm-opt@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2" - integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g== +"@webassemblyjs/wasm-opt@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5" + integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg== dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-buffer" "1.11.6" - "@webassemblyjs/wasm-gen" "1.11.6" - "@webassemblyjs/wasm-parser" "1.11.6" + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" -"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1" - integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ== +"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" + integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== dependencies: - "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/ast" "1.12.1" "@webassemblyjs/helper-api-error" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/ieee754" "1.11.6" "@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/utf8" "1.11.6" -"@webassemblyjs/wast-printer@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20" - integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A== +"@webassemblyjs/wast-printer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" + integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA== dependencies: - "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/ast" "1.12.1" "@xtuc/long" "4.2.2" "@xtuc/ieee754@^1.2.0": @@ -1042,25 +1058,20 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: mime-types "~2.1.34" negotiator "0.6.3" -acorn-import-assertions@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" - integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== +acorn-import-attributes@^1.9.5: + version "1.9.5" + resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" + integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.5.0, acorn@^8.7.1: - version "8.8.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" - integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== - -acorn@^8.9.0: - version "8.10.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" - integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== +acorn@^8.7.1, acorn@^8.8.2, acorn@^8.9.0: + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== address@^1.0.1: version "1.1.2" @@ -1087,7 +1098,7 @@ ajv-keywords@^3.5.2: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv@^6.1.0, ajv@^6.12.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.5.5: +ajv@^6.1.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.5.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -1513,15 +1524,15 @@ braces@^3.0.1, braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" -browserslist@^4.14.5: - version "4.21.7" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.7.tgz#e2b420947e5fb0a58e8f4668ae6e23488127e551" - integrity sha512-BauCXrQ7I2ftSqd2mvKHGo85XR0u7Ru3C/Hxsy/0TkfCtjrmAbPdzLGasmoiBxplpDXlPvdjX9u7srIMfgasNA== +browserslist@^4.21.10: + version "4.23.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.3.tgz#debb029d3c93ebc97ffbc8d9cbb03403e227c800" + integrity sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA== dependencies: - caniuse-lite "^1.0.30001489" - electron-to-chromium "^1.4.411" - node-releases "^2.0.12" - update-browserslist-db "^1.0.11" + caniuse-lite "^1.0.30001646" + electron-to-chromium "^1.5.4" + node-releases "^2.0.18" + update-browserslist-db "^1.1.0" buffer-from@^1.0.0: version "1.1.2" @@ -1581,10 +1592,10 @@ camelcase@^5.0.0, camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -caniuse-lite@^1.0.30001489: - version "1.0.30001492" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001492.tgz#4a06861788a52b4c81fd3344573b68cc87fe062b" - integrity sha512-2efF8SAZwgAX1FJr87KWhvuJxnGJKOnctQa8xLOskAXNXq8oiuqgl6u1kk3fFpsp3GgvzlRjiK1sl63hNtFADw== +caniuse-lite@^1.0.30001646: + version "1.0.30001655" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001655.tgz#0ce881f5a19a2dcfda2ecd927df4d5c1684b982f" + integrity sha512-jRGVy3iSGO5Uutn2owlb5gR6qsGngTw9ZTb4ali9f3glshcNmJ2noam4Mo9zia5P9Dk3jNNydy7vQjuE5dQmfg== caseless@~0.12.0: version "0.12.0" @@ -2254,10 +2265,10 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== -electron-to-chromium@^1.4.411: - version "1.4.414" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.414.tgz#f9eedb6fb01b50439d8228d8ee3a6fa5e0108437" - integrity sha512-RRuCvP6ekngVh2SAJaOKT/hxqc9JAsK+Pe0hP5tGQIfonU2Zy9gMGdJ+mBdyl/vNucMG6gkXYtuM4H/1giws5w== +electron-to-chromium@^1.5.4: + version "1.5.13" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz#1abf0410c5344b2b829b7247e031f02810d442e6" + integrity sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q== emoji-regex@^7.0.1: version "7.0.3" @@ -2321,18 +2332,10 @@ enhanced-resolve@4.1.0: memory-fs "^0.4.0" tapable "^1.0.0" -enhanced-resolve@^5.0.0: - version "5.15.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" - integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== - dependencies: - graceful-fs "^4.2.4" - tapable "^2.2.0" - -enhanced-resolve@^5.14.1: - version "5.14.1" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.14.1.tgz#de684b6803724477a4af5d74ccae5de52c25f6b3" - integrity sha512-Vklwq2vDKtl0y/vtwjSesgJ5MYS7Etuk5txS8VdKL4AOS1aUlD96zqIfsOSLQsdv3xgMRbtkWM8eG9XDfKUPow== +enhanced-resolve@^5.0.0, enhanced-resolve@^5.17.1: + version "5.17.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -2461,6 +2464,11 @@ escalade@^3.1.1: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +escalade@^3.1.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -3324,12 +3332,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.6: - version "4.2.9" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" - integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== - -graceful-fs@^4.2.10, graceful-fs@^4.2.4, graceful-fs@^4.2.9: +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.10, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -4681,36 +4684,17 @@ micromatch@^4.0.4: braces "^3.0.2" picomatch "^2.3.1" -mime-db@1.40.0, "mime-db@>= 1.40.0 < 2": - version "1.40.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" - integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== - -mime-db@1.44.0: - version "1.44.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" - integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== - mime-db@1.52.0: version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19: - version "2.1.24" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" - integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ== - dependencies: - mime-db "1.40.0" - -mime-types@^2.1.26: - version "2.1.27" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" - integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== - dependencies: - mime-db "1.44.0" +"mime-db@>= 1.40.0 < 2": + version "1.40.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" + integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== -mime-types@^2.1.27, mime-types@~2.1.24, mime-types@~2.1.34: +mime-types@^2.1.12, mime-types@^2.1.26, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -4869,16 +4853,11 @@ negotiator@0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== -neo-async@^2.6.0, neo-async@^2.6.2: +neo-async@^2.6.0, neo-async@^2.6.1, neo-async@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== -neo-async@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" - integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== - nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -4889,10 +4868,10 @@ node-forge@0.9.0: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579" integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ== -node-releases@^2.0.12: - version "2.0.12" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.12.tgz#35627cc224a23bfb06fb3380f2b3afaaa7eb1039" - integrity sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ== +node-releases@^2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== normalize-path@^2.1.1: version "2.1.1" @@ -5302,10 +5281,10 @@ picocolors@^0.2.1: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== +picocolors@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" + integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== picomatch@^2.0.4: version "2.2.1" @@ -5924,15 +5903,7 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" -schema-utils@^2.6.1, schema-utils@^2.6.5, schema-utils@^2.6.6: - version "2.6.6" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.6.6.tgz#299fe6bd4a3365dc23d99fd446caff8f1d6c330c" - integrity sha512-wHutF/WPSbIi9x6ctjGGk2Hvl0VOz5l3EKEuKbjPlB30mKZUzb9A5k9yEXRX3pwyqVLPvpfZZEllaFq/M718hA== - dependencies: - ajv "^6.12.0" - ajv-keywords "^3.4.1" - -schema-utils@^2.7.0: +schema-utils@^2.6.1, schema-utils@^2.6.5, schema-utils@^2.6.6, schema-utils@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== @@ -5941,10 +5912,10 @@ schema-utils@^2.7.0: ajv "^6.12.2" ajv-keywords "^3.4.1" -schema-utils@^3.1.1, schema-utils@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.2.tgz#36c10abca6f7577aeae136c804b0c741edeadc99" - integrity sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg== +schema-utils@^3.1.1, schema-utils@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== dependencies: "@types/json-schema" "^7.0.8" ajv "^6.12.5" @@ -6536,24 +6507,24 @@ tapable@^2.1.1, tapable@^2.2.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== -terser-webpack-plugin@^5.3.7: - version "5.3.9" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1" - integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA== +terser-webpack-plugin@^5.3.10: + version "5.3.10" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" + integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== dependencies: - "@jridgewell/trace-mapping" "^0.3.17" + "@jridgewell/trace-mapping" "^0.3.20" jest-worker "^27.4.5" schema-utils "^3.1.1" serialize-javascript "^6.0.1" - terser "^5.16.8" + terser "^5.26.0" -terser@^5.16.8: - version "5.17.6" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.17.6.tgz#d810e75e1bb3350c799cd90ebefe19c9412c12de" - integrity sha512-V8QHcs8YuyLkLHsJO5ucyff1ykrLVsR4dNnS//L5Y3NiSXpbK1J+WMVUs67eI0KTxs9JtHhgEQpXQVHlHI92DQ== +terser@^5.26.0: + version "5.31.6" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.31.6.tgz#c63858a0f0703988d0266a82fcbf2d7ba76422b1" + integrity sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg== dependencies: - "@jridgewell/source-map" "^0.3.2" - acorn "^8.5.0" + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" commander "^2.20.0" source-map-support "~0.5.20" @@ -6922,13 +6893,13 @@ upath@^1.1.1: resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.2.tgz#3db658600edaeeccbe6db5e684d67ee8c2acd068" integrity sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q== -update-browserslist-db@^1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" - integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== +update-browserslist-db@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" + integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ== dependencies: - escalade "^3.1.1" - picocolors "^1.0.0" + escalade "^3.1.2" + picocolors "^1.0.1" uri-js@^4.2.2: version "4.4.1" @@ -7016,10 +6987,10 @@ vscode-textmate@5.2.0: resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.2.0.tgz#01f01760a391e8222fe4f33fbccbd1ad71aed74e" integrity sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ== -watchpack@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" - integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== +watchpack@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da" + integrity sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw== dependencies: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" @@ -7118,34 +7089,33 @@ webpack-sources@^3.2.3: resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -webpack@5.84.1: - version "5.84.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.84.1.tgz#d4493acdeca46b26ffc99d86d784cabfeb925a15" - integrity sha512-ZP4qaZ7vVn/K8WN/p990SGATmrL1qg4heP/MrVneczYtpDGJWlrgZv55vxaV2ul885Kz+25MP2kSXkPe3LZfmg== +webpack@5.94.0: + version "5.94.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.94.0.tgz#77a6089c716e7ab90c1c67574a28da518a20970f" + integrity sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg== dependencies: - "@types/eslint-scope" "^3.7.3" - "@types/estree" "^1.0.0" - "@webassemblyjs/ast" "^1.11.5" - "@webassemblyjs/wasm-edit" "^1.11.5" - "@webassemblyjs/wasm-parser" "^1.11.5" + "@types/estree" "^1.0.5" + "@webassemblyjs/ast" "^1.12.1" + "@webassemblyjs/wasm-edit" "^1.12.1" + "@webassemblyjs/wasm-parser" "^1.12.1" acorn "^8.7.1" - acorn-import-assertions "^1.9.0" - browserslist "^4.14.5" + acorn-import-attributes "^1.9.5" + browserslist "^4.21.10" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.14.1" + enhanced-resolve "^5.17.1" es-module-lexer "^1.2.1" eslint-scope "5.1.1" events "^3.2.0" glob-to-regexp "^0.4.1" - graceful-fs "^4.2.9" + graceful-fs "^4.2.11" json-parse-even-better-errors "^2.3.1" loader-runner "^4.2.0" mime-types "^2.1.27" neo-async "^2.6.2" - schema-utils "^3.1.2" + schema-utils "^3.2.0" tapable "^2.1.1" - terser-webpack-plugin "^5.3.7" - watchpack "^2.4.0" + terser-webpack-plugin "^5.3.10" + watchpack "^2.4.1" webpack-sources "^3.2.3" websocket-driver@>=0.5.1: