From 2c1719743ddf091c10ca260f127415918fc12e6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Tue, 6 Sep 2022 17:06:28 -0300 Subject: [PATCH 01/41] test --- .../lib/plugins/Picker/PickerPlugin.ts | 4 + .../test/AutoFormat/autoFormatTest.ts | 12 +- .../test/Picker/pickerPluginTest.ts | 107 ++++++++++++++++++ 3 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 packages/roosterjs-editor-plugins/test/Picker/pickerPluginTest.ts diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts b/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts index 38426029752..35fcf4c1898 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts @@ -174,6 +174,7 @@ export default class PickerPlugin { let editor: IEditor; const TEST_ID = 'autoHyphenTest'; - let plugin: EditorPlugin; + let plugin = new AutoFormat(); beforeEach(() => { - plugin = new AutoFormat(); editor = TestHelper.initEditor(TEST_ID, [plugin]); }); @@ -16,7 +15,12 @@ describe('AutoHyphen |', () => { }); const keyDown = (keysTyped: string): PluginEvent => { - return { eventType: PluginEventType.KeyDown, rawEvent: { key: keysTyped } }; + return { + eventType: PluginEventType.KeyDown, + rawEvent: { + key: keysTyped, + }, + }; }; function runTestShouldHandleAutoHyphen( diff --git a/packages/roosterjs-editor-plugins/test/Picker/pickerPluginTest.ts b/packages/roosterjs-editor-plugins/test/Picker/pickerPluginTest.ts new file mode 100644 index 00000000000..456cc807332 --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/Picker/pickerPluginTest.ts @@ -0,0 +1,107 @@ +import * as TestHelper from '../TestHelper'; +import { PickerPlugin } from '../../lib/Picker'; +import { Position, PositionContentSearcher } from 'roosterjs-editor-dom'; +import { + IEditor, + PluginEvent, + PluginEventType, + PickerDataProvider, + PickerPluginOptions, + ChangeSource, +} from 'roosterjs-editor-types'; + +describe('PickerTest |', () => { + let editor: IEditor; + const TEST_ID = 'PickerTest'; + const root = document.createElement('div'); + root.id = 'test'; + root.innerText = '-'; + document.body.appendChild(root); + const options: PickerPluginOptions = { + elementIdPrefix: 'test', + changeSource: ChangeSource.SetContent, + triggerCharacter: '-', + }; + const dataProvider: PickerDataProvider = { + onInitalize: ( + insertNodeCallback: (nodeToInsert: HTMLElement) => void, + setIsSuggestingCallback: (isSuggesting: boolean) => void, + editor?: IEditor + ) => { + editor.focus(); + const editorSearchCursorSpy = spyOn(editor, 'getContentSearcherOfCursor'); + const mockedPosition = new PositionContentSearcher(root, new Position(root, 4)); + spyOn(mockedPosition, 'getSubStringBefore').and.returnValue('-'); + editorSearchCursorSpy.and.returnValue(mockedPosition); + insertNodeCallback(root); + setIsSuggestingCallback(true); + return; + }, + onDispose: () => { + return; + }, + onIsSuggestingChanged: (isSuggesting: boolean) => { + return; + }, + queryStringUpdated: (queryString: string, isExactMatch: boolean) => { + return; + }, + onContentChanged: (elementIds: string[]) => { + return; + }, + onRemove: (nodeRemoved: Node, isBackwards: boolean) => { + const node = document.createTextNode(''); + return node; + }, + }; + + let plugin: PickerPlugin; + beforeEach(() => { + plugin = new PickerPlugin(dataProvider, options); + editor = TestHelper.initEditor(TEST_ID, [plugin]); + editor.focus(); + const editorQueryElements = spyOn(editor, 'queryElements'); + editorQueryElements.and.returnValue([root]); + plugin.initialize(editor); + }); + + afterEach(() => { + document.body.removeChild(root); + editor.dispose(); + }); + + it('PickerPlugin | ContentEvent', () => { + const eventChange: PluginEvent = { + eventType: PluginEventType.ContentChanged, + source: ChangeSource.SetContent, + }; + plugin.onPluginEvent(eventChange); + spyOn(plugin.dataProvider, 'onContentChanged'); + expect(plugin.dataProvider.onContentChanged).toHaveBeenCalled(); + }); + + function keyDownTest(key: string) { + const eventChange: PluginEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: { + key: key, + preventDefault: () => { + return; + }, + stopImmediatePropagation: () => { + return; + }, + }, + }; + + plugin.onPluginEvent(eventChange); + spyOn(eventChange.rawEvent, 'preventDefault'); + spyOn(eventChange.rawEvent, 'stopImmediatePropagation'); + expect(eventChange.rawEvent.preventDefault).toHaveBeenCalled(); + expect(eventChange.rawEvent.stopImmediatePropagation).toHaveBeenCalled(); + } + + it('PickerPlugin | KeyDownESC', () => { + keyDownTest('Esc'); + }); +}); From d2ed2cacaeccfe03a0726185055d15b291823c7e Mon Sep 17 00:00:00 2001 From: Bryan Valverde U Date: Mon, 12 Sep 2022 11:46:41 -0600 Subject: [PATCH 02/41] Move normalizeBlockquote from normalizePlugin to Format API (#1250) --- .../lib/format/setAlignment.ts | 11 +- .../lib/format/setIndentation.ts | 11 +- .../lib/utils/normalizeBlockquote.ts | 48 +++++++++ .../test/utils/normalizeBlockquotes.ts | 102 ++++++++++++++++++ .../lib/corePlugins/NormalizeTablePlugin.ts | 25 ----- .../corePlugins/normalizeTablePluginTest.ts | 89 --------------- 6 files changed, 167 insertions(+), 119 deletions(-) create mode 100644 packages/roosterjs-editor-api/lib/utils/normalizeBlockquote.ts create mode 100644 packages/roosterjs-editor-api/test/utils/normalizeBlockquotes.ts diff --git a/packages/roosterjs-editor-api/lib/format/setAlignment.ts b/packages/roosterjs-editor-api/lib/format/setAlignment.ts index 39f501ff71d..3734d8fcfa4 100644 --- a/packages/roosterjs-editor-api/lib/format/setAlignment.ts +++ b/packages/roosterjs-editor-api/lib/format/setAlignment.ts @@ -1,6 +1,7 @@ import blockFormat from '../utils/blockFormat'; import execCommand from '../utils/execCommand'; import formatUndoSnapshot from '../utils/formatUndoSnapshot'; +import normalizeBlockquote from '../utils/normalizeBlockquote'; import { createVListFromRegion, findClosestElementAncestor, @@ -91,7 +92,15 @@ function alignText(editor: IEditor, alignment: Alignment | CompatibleAlignment) align = 'right'; } execCommand(editor, command); - editor.queryElements('[align]', QueryScope.OnSelection, node => (node.style.textAlign = align)); + const elements = editor.queryElements('[align]', QueryScope.OnSelection, node => { + node.style.textAlign = align; + normalizeBlockquote(node); + }); + + if (elements.length == 0) { + const node = editor.getElementAtCursor(); + normalizeBlockquote(node); + } } function isList(element: HTMLElement) { diff --git a/packages/roosterjs-editor-api/lib/format/setIndentation.ts b/packages/roosterjs-editor-api/lib/format/setIndentation.ts index 1ec3e4c3ba8..4511d425ead 100644 --- a/packages/roosterjs-editor-api/lib/format/setIndentation.ts +++ b/packages/roosterjs-editor-api/lib/format/setIndentation.ts @@ -1,4 +1,5 @@ import blockFormat from '../utils/blockFormat'; +import normalizeBlockquote from '../utils/normalizeBlockquote'; import { BlockElement, ExperimentalFeatures, @@ -108,11 +109,13 @@ export default function setIndentation( }, 'setIndentation' ); -} -function indent(region: RegionBase, blocks: BlockElement[]) { - const nodes = collapseNodesInRegion(region, blocks); - wrap(nodes, KnownCreateElementDataIndex.BlockquoteWrapper); + function indent(region: RegionBase, blocks: BlockElement[]) { + const nodes = collapseNodesInRegion(region, blocks); + wrap(nodes, KnownCreateElementDataIndex.BlockquoteWrapper); + const quotesHandled: Node[] = []; + nodes.forEach(node => normalizeBlockquote(node, quotesHandled)); + } } function outdent(region: RegionBase, blocks: BlockElement[]) { diff --git a/packages/roosterjs-editor-api/lib/utils/normalizeBlockquote.ts b/packages/roosterjs-editor-api/lib/utils/normalizeBlockquote.ts new file mode 100644 index 00000000000..e98575dff60 --- /dev/null +++ b/packages/roosterjs-editor-api/lib/utils/normalizeBlockquote.ts @@ -0,0 +1,48 @@ +import { findClosestElementAncestor, getComputedStyle, safeInstanceOf } from 'roosterjs-editor-dom'; + +/** + * @internal + * @param node start node to normalize + * @param quotesHandled Optional parameter to prevent already modified quotes to be rechecked. + * @returns + */ +export default function normalizeBlockquote(node: Node, quotesHandled?: Node[]): void { + if (safeInstanceOf(node, 'HTMLElement')) { + const alignment = node.style.textAlign; + + let quote = findClosestElementAncestor(node, undefined /* root */, 'blockquote'); + const isNodeRTL = isRTL(node); + + if (quotesHandled) { + if (quotesHandled.indexOf(quote) > -1) { + return; + } + quotesHandled.push(quote); + } + + while (quote) { + if (alignment == 'center') { + if (isNodeRTL) { + delete quote.style.marginInlineEnd; + quote.style.marginInlineStart = 'auto'; + } else { + delete quote.style.marginInlineStart; + quote.style.marginInlineEnd = 'auto'; + } + } else { + delete quote.style.marginInlineStart; + delete quote.style.marginInlineEnd; + } + + quote = findClosestElementAncestor( + quote.parentElement, + undefined /* root */, + 'blockquote' + ); + } + } +} + +function isRTL(el: Element) { + return getComputedStyle(el, 'direction') == 'rtl' || el.getAttribute('dir') == 'rtl'; +} diff --git a/packages/roosterjs-editor-api/test/utils/normalizeBlockquotes.ts b/packages/roosterjs-editor-api/test/utils/normalizeBlockquotes.ts new file mode 100644 index 00000000000..1d1efc02106 --- /dev/null +++ b/packages/roosterjs-editor-api/test/utils/normalizeBlockquotes.ts @@ -0,0 +1,102 @@ +import normalizeBlockquote from '../../lib/utils/normalizeBlockquote'; +import { createElement } from 'roosterjs-editor-dom'; +import { CreateElementData } from 'roosterjs-editor-types'; + +const ID = 'test_id_'; + +describe('Normalize Blockquote |', () => { + function runTest(input: CreateElementData, marginInlineEnd: string, marginInlineStart: string) { + const blockquote = createElement(input, document) as HTMLElement; + + const quotesHandled: HTMLElement[] = []; + blockquote.querySelectorAll('#' + ID).forEach(n => normalizeBlockquote(n, quotesHandled)); + + expect(blockquote.style.marginInlineEnd).toEqual(marginInlineEnd); + expect(blockquote.style.marginInlineStart).toEqual(marginInlineStart); + expect(quotesHandled.length).toEqual(1); + expect(quotesHandled[0]).toEqual(blockquote); + } + it('Normalize centered blockquote', () => { + runTest( + { + tag: 'blockquote', + children: [ + { + tag: 'div', + style: 'text-align: center;', + attributes: { + id: ID, + }, + }, + ], + }, + 'auto', + '' + ); + }); + + it('Normalize centered blockquote, RTL', () => { + runTest( + { + tag: 'blockquote', + children: [ + { + tag: 'div', + style: 'text-align: center;', + attributes: { + dir: 'rtl', + id: ID, + }, + }, + ], + }, + '', + 'auto' + ); + }); + + it('Normalize centered blockquote with no text align center', () => { + runTest( + { + tag: 'blockquote', + children: [ + { + tag: 'div', + attributes: { + dir: 'rtl', + id: ID, + }, + }, + { + tag: 'div', + attributes: { + dir: 'rtl', + id: ID, + }, + }, + ], + }, + '', + '' + ); + }); + + it('Normalize centered blockquote, RTL', () => { + runTest( + { + tag: 'blockquote', + children: [ + { + tag: 'div', + style: 'text-align: left;', + attributes: { + id: ID, + }, + }, + ], + }, + '', + '' + ); + }); +}); diff --git a/packages/roosterjs-editor-core/lib/corePlugins/NormalizeTablePlugin.ts b/packages/roosterjs-editor-core/lib/corePlugins/NormalizeTablePlugin.ts index d608760d4f9..f9b95f2c65d 100644 --- a/packages/roosterjs-editor-core/lib/corePlugins/NormalizeTablePlugin.ts +++ b/packages/roosterjs-editor-core/lib/corePlugins/NormalizeTablePlugin.ts @@ -1,6 +1,5 @@ import { changeElementTag, - getComputedStyle, getTagOfNode, moveChildNodes, safeInstanceOf, @@ -66,7 +65,6 @@ export default class NormalizeTablePlugin implements EditorPlugin { case PluginEventType.EditorReady: case PluginEventType.ContentChanged: this.normalizeTables(this.editor.queryElements('table')); - this.normalizeBlockquotes(this.editor.queryElements('blockquote')); break; case PluginEventType.BeforePaste: @@ -119,29 +117,6 @@ export default class NormalizeTablePlugin implements EditorPlugin { } } } - - private normalizeBlockquotes(elements: HTMLQuoteElement[]) { - elements.forEach((quote: HTMLQuoteElement) => { - const centeredElement = quote.querySelector('[style^="text-align: center"]'); - - if (centeredElement) { - if (isRTL(centeredElement)) { - delete quote.style.marginInlineEnd; - quote.style.marginInlineStart = 'auto'; - } else { - delete quote.style.marginInlineStart; - quote.style.marginInlineEnd = 'auto'; - } - } else { - delete quote.style.marginInlineStart; - delete quote.style.marginInlineEnd; - } - }); - } -} - -function isRTL(el: Element) { - return getComputedStyle(el, 'direction') == 'rtl' || el.getAttribute('dir') == 'rtl'; } function normalizeTables(tables: HTMLTableElement[]) { diff --git a/packages/roosterjs-editor-core/test/corePlugins/normalizeTablePluginTest.ts b/packages/roosterjs-editor-core/test/corePlugins/normalizeTablePluginTest.ts index 846082b71dc..976a4c29ce0 100644 --- a/packages/roosterjs-editor-core/test/corePlugins/normalizeTablePluginTest.ts +++ b/packages/roosterjs-editor-core/test/corePlugins/normalizeTablePluginTest.ts @@ -383,95 +383,6 @@ describe('NormalizeTablePlugin', () => { '' ); }); - - describe('Normalize Blockquote |', () => { - function runTest(input: CreateElementData, expectCallback: (el: HTMLElement) => void) { - const blockquote = createElement(input, document); - - editor.queryElements = () => [blockquote]; - plugin.onPluginEvent({ - eventType: PluginEventType.EditorReady, - }); - - expectCallback(blockquote as HTMLElement); - } - it('Normalize centered blockquote', () => { - runTest( - { - tag: 'blockquote', - children: [ - { - tag: 'div', - style: 'text-align: center;', - }, - ], - }, - el => { - expect(el.style.marginInlineEnd).toEqual('auto'); - expect(el.style.marginInlineStart).toEqual(''); - } - ); - }); - - it('Normalize centered blockquote, RTL', () => { - runTest( - { - tag: 'blockquote', - children: [ - { - tag: 'div', - style: 'text-align: center;', - attributes: { - dir: 'rtl', - }, - }, - ], - }, - el => { - expect(el.style.marginInlineEnd).toEqual(''); - expect(el.style.marginInlineStart).toEqual('auto'); - } - ); - }); - - it('Normalize centered blockquote with no text align center', () => { - runTest( - { - tag: 'blockquote', - children: [ - { - tag: 'div', - attributes: { - dir: 'rtl', - }, - }, - ], - }, - el => { - expect(el.style.marginInlineEnd).toEqual(''); - expect(el.style.marginInlineStart).toEqual(''); - } - ); - }); - - it('Normalize centered blockquote, RTL', () => { - runTest( - { - tag: 'blockquote', - children: [ - { - tag: 'div', - style: 'text-align: left;', - }, - ], - }, - el => { - expect(el.style.marginInlineEnd).toEqual(''); - expect(el.style.marginInlineStart).toEqual(''); - } - ); - }); - }); }); function getTheadWithColgroup( From 0e9b92f7a0293064bce24049f08190dfeba42526 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 12 Sep 2022 13:19:52 -0700 Subject: [PATCH 03/41] Content Model customization step 1: Reorganize some code (#1252) * Reorganize some code * fix build * Move some code * Remove unnecessary change * More fix * fix comment --- .../editor/ExperimentalContentModelEditor.ts | 10 ++-- .../controls/editor/isContentModelEditor.ts | 2 +- .../context/createDomToModelContext.ts | 10 ++-- .../domToModel/processors/ElementProcessor.ts | 11 ---- .../lib/domToModel/processors/brProcessor.ts | 2 +- .../processors/containerProcessor.ts | 2 +- .../processors/generalBlockProcessor.ts | 2 +- .../processors/generalSegmentProcessor.ts | 2 +- .../processors/singleElementProcessor.ts | 9 +-- .../domToModel/processors/tableProcessor.ts | 22 ++----- .../domToModel/processors/textProcessor.ts | 2 +- .../lib/domToModel/utils/parseFormat.ts | 4 +- .../lib/domToModel/utils/stackFormat.ts | 2 +- .../lib/formatHandlers/FormatHandler.ts | 25 ++++++-- .../segment/superOrSubScriptFormatHandler.ts | 3 + .../lib/formatHandlers/utils/color.ts | 3 + packages/roosterjs-content-model/lib/index.ts | 25 ++++++-- .../selection/hasSelectionInSegment.ts | 12 ---- .../modelApi/selection/setSelectionToTable.ts | 17 ++++-- .../lib/modelApi/table/canMergeCells.ts | 3 + .../modelApi/table/createTableStructure.ts | 6 +- .../lib/modelApi/table/getSelectedCells.ts | 12 +--- .../lib/modelApi/table/normalizeTable.ts | 4 +- .../table/setTableCellBackgroundColor.ts | 2 +- .../context/createModelToDomContext.ts | 17 +++--- .../lib/modelToDom/handlers/handleBlock.ts | 9 +-- .../modelToDom/handlers/handleParagraph.ts | 2 +- .../lib/modelToDom/handlers/handleSegment.ts | 9 +-- .../lib/modelToDom/handlers/handleTable.ts | 6 +- .../lib/modelToDom/utils/applyFormat.ts | 4 +- .../lib/publicApi/contentModelToDom.ts | 13 ++-- .../lib/publicApi/domToContentModel.ts | 8 +-- .../selection/hasSelectionInBlock.ts | 7 ++- .../selection/hasSelectionInSegment.ts | 13 ++++ .../IExperimentalContentModelEditor.ts | 6 +- .../publicTypes/context/DomToModelContext.ts | 11 ++++ .../context/DomToModelFormatContext.ts | 11 ++++ .../context/DomToModelSelectionContext.ts} | 35 ++++------- .../EditorContext.ts} | 4 +- .../publicTypes/context/ElementProcessor.ts | 14 +++++ .../publicTypes/context/ModelToDomContext.ts | 7 +++ .../context/ModelToDomSelectionContext.ts} | 29 ++++----- .../context/createDomToModelContextTest.ts | 16 ++--- .../domToModel/processors/brProcessorTest.ts | 2 +- .../processors/containerProcessorTest.ts | 2 +- .../processors/generalBlockProcessorTest.ts | 2 +- .../processors/generalSegmentProcessorTest.ts | 2 +- .../processors/tableProcessorTest.ts | 4 +- .../processors/textProcessorTest.ts | 2 +- .../test/domToModel/utils/parseFormatTest.ts | 42 +------------ .../backgroundColorFormatHandlerTest.ts | 21 +++---- .../common/borderFormatHandlerTest.ts | 21 +++---- .../common/idFormatHandlerTest.ts | 21 +++---- .../common/sizeFormatHandlerTest.ts | 21 +++---- .../common/textAlignFormatHandlerTest.ts | 21 +++---- .../common/verticalAlignFormatHandlerTest.ts | 21 +++---- .../formatHandlers/createFormatContextTest.ts | 60 ++++++++----------- .../segment/boldFormatHandlerTest.ts | 21 +++---- .../segment/fontFamilyFormatHandlerTest.ts | 21 +++---- .../segment/fontSizeFormatHandlerTest.ts | 21 +++---- .../segment/italicFormatHandlerTest.ts | 21 +++---- .../segment/strikeFormatHandleTest.ts | 21 +++---- .../superOrSubScriptFormatHandlerTest.ts | 21 +++---- .../segment/textColorFormatHandlerTest.ts | 21 +++---- .../segment/underlineFormatHandlerTest.ts | 21 +++---- .../tableCellMetadataFormatHandlerTest.ts | 21 +++---- .../table/tableMetadataFormatHandlerTest.ts | 21 +++---- .../table/tableSpacingFormatHandlerTest.ts | 21 +++---- .../modelToDom/handlers/handleBlockTest.ts | 4 +- .../handlers/handleParagraphTest.ts | 2 +- .../modelToDom/handlers/handleSegmentTest.ts | 2 +- .../modelToDom/handlers/handleTableTest.ts | 2 +- .../selection/hasSelectionInBlockTest.ts | 2 +- .../selection/hasSelectionInSegmentTest.ts | 2 +- 74 files changed, 399 insertions(+), 501 deletions(-) delete mode 100644 packages/roosterjs-content-model/lib/domToModel/processors/ElementProcessor.ts delete mode 100644 packages/roosterjs-content-model/lib/modelApi/selection/hasSelectionInSegment.ts rename packages/roosterjs-content-model/lib/{modelApi => publicApi}/selection/hasSelectionInBlock.ts (73%) create mode 100644 packages/roosterjs-content-model/lib/publicApi/selection/hasSelectionInSegment.ts create mode 100644 packages/roosterjs-content-model/lib/publicTypes/context/DomToModelContext.ts create mode 100644 packages/roosterjs-content-model/lib/publicTypes/context/DomToModelFormatContext.ts rename packages/roosterjs-content-model/lib/{domToModel/context/DomToModelContext.ts => publicTypes/context/DomToModelSelectionContext.ts} (59%) rename packages/roosterjs-content-model/lib/publicTypes/{ContentModelContext.ts => context/EditorContext.ts} (84%) create mode 100644 packages/roosterjs-content-model/lib/publicTypes/context/ElementProcessor.ts create mode 100644 packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomContext.ts rename packages/roosterjs-content-model/lib/{modelToDom/context/ModelToDomContext.ts => publicTypes/context/ModelToDomSelectionContext.ts} (68%) rename packages/roosterjs-content-model/test/{modelApi => publicApi}/selection/hasSelectionInBlockTest.ts (98%) rename packages/roosterjs-content-model/test/{modelApi => publicApi}/selection/hasSelectionInSegmentTest.ts (97%) diff --git a/demo/scripts/controls/editor/ExperimentalContentModelEditor.ts b/demo/scripts/controls/editor/ExperimentalContentModelEditor.ts index 99f17b8d668..a130fd0201c 100644 --- a/demo/scripts/controls/editor/ExperimentalContentModelEditor.ts +++ b/demo/scripts/controls/editor/ExperimentalContentModelEditor.ts @@ -2,10 +2,10 @@ import { Editor } from 'roosterjs-editor-core'; import { EditorOptions, SelectionRangeTypes } from 'roosterjs-editor-types'; import { getComputedStyles, Position } from 'roosterjs-editor-dom'; import { - ContentModelContext, ContentModelDocument, contentModelToDom, domToContentModel, + EditorContext, IExperimentalContentModelEditor, } from 'roosterjs-content-model'; @@ -29,9 +29,9 @@ export default class ExperimentalContentModelEditor extends Editor } /** - * Create a ContentModelContext object used by ContentModel API + * Create a EditorContext object used by ContentModel API */ - createContentModelContext(): ContentModelContext { + createEditorContext(): EditorContext { return { isDarkMode: this.isDarkMode(), zoomScale: this.getZoomScale(), @@ -48,7 +48,7 @@ export default class ExperimentalContentModelEditor extends Editor createContentModel(startNode?: HTMLElement): ContentModelDocument { return domToContentModel( startNode || this.contentDiv, - this.createContentModelContext(), + this.createEditorContext(), !!startNode, this.getSelectionRangeEx() ); @@ -63,7 +63,7 @@ export default class ExperimentalContentModelEditor extends Editor model: ContentModelDocument, mergingCallback: (fragment: DocumentFragment) => void = this.defaultMergingCallback ) { - const [fragment, range] = contentModelToDom(model, this.createContentModelContext()); + const [fragment, range] = contentModelToDom(model, this.createEditorContext()); switch (range?.type) { case SelectionRangeTypes.Normal: diff --git a/demo/scripts/controls/editor/isContentModelEditor.ts b/demo/scripts/controls/editor/isContentModelEditor.ts index 89b431b114a..e0aa9709076 100644 --- a/demo/scripts/controls/editor/isContentModelEditor.ts +++ b/demo/scripts/controls/editor/isContentModelEditor.ts @@ -6,5 +6,5 @@ export default function isContentModelEditor( ): editor is IExperimentalContentModelEditor { const experimentalEditor = editor as IExperimentalContentModelEditor; - return !!experimentalEditor.createContentModelContext && 'contentDiv' in experimentalEditor; + return !!experimentalEditor.createEditorContext && 'contentDiv' in experimentalEditor; } diff --git a/packages/roosterjs-content-model/lib/domToModel/context/createDomToModelContext.ts b/packages/roosterjs-content-model/lib/domToModel/context/createDomToModelContext.ts index e2dd94b34b4..a62b7722003 100644 --- a/packages/roosterjs-content-model/lib/domToModel/context/createDomToModelContext.ts +++ b/packages/roosterjs-content-model/lib/domToModel/context/createDomToModelContext.ts @@ -1,21 +1,21 @@ -import { ContentModelContext } from '../../publicTypes/ContentModelContext'; -import { DomToModelContext } from './DomToModelContext'; +import { DomToModelContext } from '../../publicTypes/context/DomToModelContext'; +import { EditorContext } from '../../publicTypes/context/EditorContext'; import { SelectionRangeEx, SelectionRangeTypes } from 'roosterjs-editor-types'; /** * @internal */ export function createDomToModelContext( - contentModelContext?: ContentModelContext, + editorContext?: EditorContext, range?: SelectionRangeEx ): DomToModelContext { const context: DomToModelContext = { - contentModelContext: contentModelContext || { + ...(editorContext || { isDarkMode: false, zoomScale: 1, isRightToLeft: false, getDarkColor: undefined, - }, + }), segmentFormat: {}, isInSelection: false, diff --git a/packages/roosterjs-content-model/lib/domToModel/processors/ElementProcessor.ts b/packages/roosterjs-content-model/lib/domToModel/processors/ElementProcessor.ts deleted file mode 100644 index 120aa6f4301..00000000000 --- a/packages/roosterjs-content-model/lib/domToModel/processors/ElementProcessor.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ContentModelBlockGroup } from '../../publicTypes/block/group/ContentModelBlockGroup'; -import { DomToModelContext } from '../context/DomToModelContext'; - -/** - * @internal - */ -export type ElementProcessor = ( - group: ContentModelBlockGroup, - element: HTMLElement, - context: DomToModelContext -) => void; diff --git a/packages/roosterjs-content-model/lib/domToModel/processors/brProcessor.ts b/packages/roosterjs-content-model/lib/domToModel/processors/brProcessor.ts index 0f6e3578821..ca50d37c0e0 100644 --- a/packages/roosterjs-content-model/lib/domToModel/processors/brProcessor.ts +++ b/packages/roosterjs-content-model/lib/domToModel/processors/brProcessor.ts @@ -1,6 +1,6 @@ import { addSegment } from '../../modelApi/common/addSegment'; import { createBr } from '../../modelApi/creators/createBr'; -import { ElementProcessor } from './ElementProcessor'; +import { ElementProcessor } from '../../publicTypes/context/ElementProcessor'; /** * @internal diff --git a/packages/roosterjs-content-model/lib/domToModel/processors/containerProcessor.ts b/packages/roosterjs-content-model/lib/domToModel/processors/containerProcessor.ts index f1b9e258044..edcd3035ce6 100644 --- a/packages/roosterjs-content-model/lib/domToModel/processors/containerProcessor.ts +++ b/packages/roosterjs-content-model/lib/domToModel/processors/containerProcessor.ts @@ -1,7 +1,7 @@ import { addSegment } from '../../modelApi/common/addSegment'; import { ContentModelBlockGroup } from '../../publicTypes/block/group/ContentModelBlockGroup'; import { createSelectionMarker } from '../../modelApi/creators/createSelectionMarker'; -import { DomToModelContext } from '../context/DomToModelContext'; +import { DomToModelContext } from '../../publicTypes/context/DomToModelContext'; import { isNodeOfType } from '../../domUtils/isNodeOfType'; import { NodeType } from 'roosterjs-editor-types'; import { singleElementProcessor } from './singleElementProcessor'; diff --git a/packages/roosterjs-content-model/lib/domToModel/processors/generalBlockProcessor.ts b/packages/roosterjs-content-model/lib/domToModel/processors/generalBlockProcessor.ts index 5f36848f43a..7c76b0a417f 100644 --- a/packages/roosterjs-content-model/lib/domToModel/processors/generalBlockProcessor.ts +++ b/packages/roosterjs-content-model/lib/domToModel/processors/generalBlockProcessor.ts @@ -1,7 +1,7 @@ import { addBlock } from '../../modelApi/common/addBlock'; import { containerProcessor } from './containerProcessor'; import { createGeneralBlock } from '../../modelApi/creators/createGeneralBlock'; -import { ElementProcessor } from './ElementProcessor'; +import { ElementProcessor } from '../../publicTypes/context/ElementProcessor'; /** * @internal diff --git a/packages/roosterjs-content-model/lib/domToModel/processors/generalSegmentProcessor.ts b/packages/roosterjs-content-model/lib/domToModel/processors/generalSegmentProcessor.ts index b1ff6a8124d..4f676a8e37c 100644 --- a/packages/roosterjs-content-model/lib/domToModel/processors/generalSegmentProcessor.ts +++ b/packages/roosterjs-content-model/lib/domToModel/processors/generalSegmentProcessor.ts @@ -1,7 +1,7 @@ import { addSegment } from '../../modelApi/common/addSegment'; import { containerProcessor } from './containerProcessor'; import { createGeneralSegment } from '../../modelApi/creators/createGeneralSegment'; -import { ElementProcessor } from './ElementProcessor'; +import { ElementProcessor } from '../../publicTypes/context/ElementProcessor'; import { stackFormat } from '../utils/stackFormat'; /** diff --git a/packages/roosterjs-content-model/lib/domToModel/processors/singleElementProcessor.ts b/packages/roosterjs-content-model/lib/domToModel/processors/singleElementProcessor.ts index aad0ca5d3eb..b79745c978f 100644 --- a/packages/roosterjs-content-model/lib/domToModel/processors/singleElementProcessor.ts +++ b/packages/roosterjs-content-model/lib/domToModel/processors/singleElementProcessor.ts @@ -1,6 +1,6 @@ import { brProcessor } from './brProcessor'; import { containerProcessor } from './containerProcessor'; -import { ElementProcessor } from './ElementProcessor'; +import { ElementProcessor } from '../../publicTypes/context/ElementProcessor'; import { generalBlockProcessor } from './generalBlockProcessor'; import { generalSegmentProcessor } from './generalSegmentProcessor'; import { isBlockElement } from 'roosterjs-editor-dom'; @@ -18,12 +18,7 @@ export const knownElementProcessor: ElementProcessor = (group, element, context) generalBlockProcessor(group, element, context); } else { stackFormat(context, { segment: 'shallowClone' }, () => { - parseFormat( - element, - SegmentFormatHandlers, - context.segmentFormat, - context.contentModelContext - ); + parseFormat(element, SegmentFormatHandlers, context.segmentFormat, context); containerProcessor(group, element, context); }); } diff --git a/packages/roosterjs-content-model/lib/domToModel/processors/tableProcessor.ts b/packages/roosterjs-content-model/lib/domToModel/processors/tableProcessor.ts index 8cd9f0a985c..e5e38a19fe2 100644 --- a/packages/roosterjs-content-model/lib/domToModel/processors/tableProcessor.ts +++ b/packages/roosterjs-content-model/lib/domToModel/processors/tableProcessor.ts @@ -2,7 +2,7 @@ import { addBlock } from '../../modelApi/common/addBlock'; import { containerProcessor } from './containerProcessor'; import { createTable } from '../../modelApi/creators/createTable'; import { createTableCell } from '../../modelApi/creators/createTableCell'; -import { ElementProcessor } from './ElementProcessor'; +import { ElementProcessor } from '../../publicTypes/context/ElementProcessor'; import { parseFormat } from '../utils/parseFormat'; import { SegmentFormatHandlers } from '../../formatHandlers/SegmentFormatHandlers'; import { stackFormat } from '../utils/stackFormat'; @@ -30,18 +30,13 @@ export const tableProcessor: ElementProcessor = (group, element, context) => { const hasTableSelection = selectedTable == tableElement && !!firstCell && !!lastCell; stackFormat(context, { segment: 'shallowClone' }, () => { - parseFormat(tableElement, TableFormatHandlers, table.format, context.contentModelContext); - parseFormat( - tableElement, - SegmentFormatHandlers, - context.segmentFormat, - context.contentModelContext - ); + parseFormat(tableElement, TableFormatHandlers, table.format, context); + parseFormat(tableElement, SegmentFormatHandlers, context.segmentFormat, context); addBlock(group, table); const columnPositions: number[] = [0]; const rowPositions: number[] = [0]; - const zoomScale = context.contentModelContext.zoomScale; + const zoomScale = context.zoomScale; for (let row = 0; row < tableElement.rows.length; row++) { const tr = tableElement.rows[row]; @@ -86,17 +81,12 @@ export const tableProcessor: ElementProcessor = (group, element, context) => { if (hasTd) { stackFormat(context, { segment: 'shallowClone' }, () => { - parseFormat( - td, - TableCellFormatHandlers, - cell.format, - context.contentModelContext - ); + parseFormat(td, TableCellFormatHandlers, cell.format, context); parseFormat( td, SegmentFormatHandlers, context.segmentFormat, - context.contentModelContext + context ); containerProcessor(cell, td, context); diff --git a/packages/roosterjs-content-model/lib/domToModel/processors/textProcessor.ts b/packages/roosterjs-content-model/lib/domToModel/processors/textProcessor.ts index 06921080f2f..59c0585fb4d 100644 --- a/packages/roosterjs-content-model/lib/domToModel/processors/textProcessor.ts +++ b/packages/roosterjs-content-model/lib/domToModel/processors/textProcessor.ts @@ -2,7 +2,7 @@ import { addSegment } from '../../modelApi/common/addSegment'; import { areSameFormats } from '../utils/areSameFormats'; import { ContentModelBlockGroup } from '../../publicTypes/block/group/ContentModelBlockGroup'; import { createText } from '../../modelApi/creators/createText'; -import { DomToModelContext } from '../context/DomToModelContext'; +import { DomToModelContext } from '../../publicTypes/context/DomToModelContext'; /** * @internal diff --git a/packages/roosterjs-content-model/lib/domToModel/utils/parseFormat.ts b/packages/roosterjs-content-model/lib/domToModel/utils/parseFormat.ts index f20c665525d..340cd7c210b 100644 --- a/packages/roosterjs-content-model/lib/domToModel/utils/parseFormat.ts +++ b/packages/roosterjs-content-model/lib/domToModel/utils/parseFormat.ts @@ -1,5 +1,5 @@ -import { ContentModelContext } from '../../publicTypes/ContentModelContext'; import { ContentModelFormatBase } from '../../publicTypes/format/ContentModelFormatBase'; +import { DomToModelContext } from '../../publicTypes/context/DomToModelContext'; import { FormatHandler } from '../../formatHandlers/FormatHandler'; type DefaultFormatParserType = @@ -68,7 +68,7 @@ export function parseFormat( element: HTMLElement, handlers: FormatHandler[], format: T, - context: ContentModelContext + context: DomToModelContext ) { const styleItem = DefaultStyleMap[element.tagName]; const defaultStyle = styleItem diff --git a/packages/roosterjs-content-model/lib/domToModel/utils/stackFormat.ts b/packages/roosterjs-content-model/lib/domToModel/utils/stackFormat.ts index 864db7e8fe0..d15f59587eb 100644 --- a/packages/roosterjs-content-model/lib/domToModel/utils/stackFormat.ts +++ b/packages/roosterjs-content-model/lib/domToModel/utils/stackFormat.ts @@ -1,5 +1,5 @@ import { ContentModelFormatBase } from '../../publicTypes/format/ContentModelFormatBase'; -import { DomToModelContext } from '../context/DomToModelContext'; +import { DomToModelContext } from '../../publicTypes/context/DomToModelContext'; /** * @internal diff --git a/packages/roosterjs-content-model/lib/formatHandlers/FormatHandler.ts b/packages/roosterjs-content-model/lib/formatHandlers/FormatHandler.ts index 742a5bf89ab..b24ff57593b 100644 --- a/packages/roosterjs-content-model/lib/formatHandlers/FormatHandler.ts +++ b/packages/roosterjs-content-model/lib/formatHandlers/FormatHandler.ts @@ -1,15 +1,30 @@ -import { ContentModelContext } from '../publicTypes/ContentModelContext'; import { ContentModelFormatBase } from '../publicTypes/format/ContentModelFormatBase'; +import { DomToModelContext } from '../publicTypes/context/DomToModelContext'; +import { ModelToDomContext } from '../publicTypes/context/ModelToDomContext'; /** - * @internal + * Represents an object that will handle a given format */ export interface FormatHandler { + /** + * Parse format from the given HTML element and default style + * @param format The format object to parse into + * @param element The HTML element to parse format from + * @param context The context object that provide related context information + * @param defaultStyle Default CSS style of the given HTML element + */ parse: ( format: TFormat, element: HTMLElement, - context: ContentModelContext, - defaultStyle: Partial + context: DomToModelContext, + defaultStyle: Readonly> ) => void; - apply: (format: TFormat, element: HTMLElement, context: ContentModelContext) => void; + + /** + * Apply format to the given HTML element + * @param format The format object to apply + * @param element The HTML element to apply format to + * @param context The context object that provide related context information + */ + apply: (format: TFormat, element: HTMLElement, context: ModelToDomContext) => void; } diff --git a/packages/roosterjs-content-model/lib/formatHandlers/segment/superOrSubScriptFormatHandler.ts b/packages/roosterjs-content-model/lib/formatHandlers/segment/superOrSubScriptFormatHandler.ts index 1eeec7ed875..3fc96ee72ca 100644 --- a/packages/roosterjs-content-model/lib/formatHandlers/segment/superOrSubScriptFormatHandler.ts +++ b/packages/roosterjs-content-model/lib/formatHandlers/segment/superOrSubScriptFormatHandler.ts @@ -36,6 +36,9 @@ export const superOrSubScriptFormatHandler: FormatHandler( element: HTMLElement, handlers: FormatHandler[], format: T, - context: ContentModelContext + context: ModelToDomContext ) { handlers.forEach(handler => { handler.apply(format, element, context); diff --git a/packages/roosterjs-content-model/lib/publicApi/contentModelToDom.ts b/packages/roosterjs-content-model/lib/publicApi/contentModelToDom.ts index 4ccadea9daf..f7b242e3786 100644 --- a/packages/roosterjs-content-model/lib/publicApi/contentModelToDom.ts +++ b/packages/roosterjs-content-model/lib/publicApi/contentModelToDom.ts @@ -1,10 +1,11 @@ -import { BlockAndSegmentNode, ModelToDomContext } from '../modelToDom/context/ModelToDomContext'; -import { ContentModelContext } from '../publicTypes/ContentModelContext'; import { ContentModelDocument } from '../publicTypes/block/group/ContentModelDocument'; import { createModelToDomContext } from '../modelToDom/context/createModelToDomContext'; import { createRange, Position, toArray } from 'roosterjs-editor-dom'; +import { EditorContext } from '../publicTypes/context/EditorContext'; import { handleBlock } from '../modelToDom/handlers/handleBlock'; import { isNodeOfType } from '../domUtils/isNodeOfType'; +import { ModelToDomBlockAndSegmentNode } from '../publicTypes/context/ModelToDomSelectionContext'; +import { ModelToDomContext } from '../publicTypes/context/ModelToDomContext'; import { optimize } from '../modelToDom/optimizers/optimize'; import { NodePosition, @@ -16,16 +17,16 @@ import { /** * Create DOM tree fragment from Content Model document * @param model The content model document to generate DOM tree from - * @param contentModelContext Content for Content Model + * @param editorContext Content for Content Model editor * @returns A Document Fragment that contains the DOM tree generated from the given model, * and a SelectionRangeEx object that contains selection info from the model if any, or null */ export default function contentModelToDom( model: ContentModelDocument, - contentModelContext: ContentModelContext + editorContext: EditorContext ): [DocumentFragment, SelectionRangeEx | null] { const fragment = model.document.createDocumentFragment(); - const modelToDomContext = createModelToDomContext(contentModelContext); + const modelToDomContext = createModelToDomContext(editorContext); handleBlock(model.document, fragment, model, modelToDomContext); optimize(fragment, 2 /*optimizeLevel*/); @@ -72,7 +73,7 @@ function extractSelectionRange(context: ModelToDomContext): SelectionRangeEx | n return null; } -function calcPosition(pos: BlockAndSegmentNode): NodePosition | undefined { +function calcPosition(pos: ModelToDomBlockAndSegmentNode): NodePosition | undefined { let result: NodePosition | undefined; if (pos.block) { diff --git a/packages/roosterjs-content-model/lib/publicApi/domToContentModel.ts b/packages/roosterjs-content-model/lib/publicApi/domToContentModel.ts index 740ce94e252..8a44f96118d 100644 --- a/packages/roosterjs-content-model/lib/publicApi/domToContentModel.ts +++ b/packages/roosterjs-content-model/lib/publicApi/domToContentModel.ts @@ -1,8 +1,8 @@ import { containerProcessor } from '../domToModel/processors/containerProcessor'; -import { ContentModelContext } from '../publicTypes/ContentModelContext'; import { ContentModelDocument } from '../publicTypes/block/group/ContentModelDocument'; import { createContentModelDocument } from '../modelApi/creators/createContentModelDocument'; import { createDomToModelContext } from '../domToModel/context/createDomToModelContext'; +import { EditorContext } from '../publicTypes/context/EditorContext'; import { normalizeModel } from '../modelApi/common/normalizeContentModel'; import { SelectionRangeEx } from 'roosterjs-editor-types'; import { singleElementProcessor } from '../domToModel/processors/singleElementProcessor'; @@ -10,19 +10,19 @@ import { singleElementProcessor } from '../domToModel/processors/singleElementPr /** * Create Content Model from DOM tree in this editor * @param root Root element of DOM tree to create Content Model from - * @param contentModelContext Context of content model + * @param editorContext Context of content model editor * @param includeRoot True to create content model from the root element itself, false to create from all child nodes of root. @default false * @param range Selection range of the DOM tree. If not passed, the content model will not include selection * @returns A ContentModelDocument object that contains all the models created from the give root element */ export default function domToContentModel( root: HTMLElement, - contentModelContext: ContentModelContext, + editorContext: EditorContext, includeRoot?: boolean, range?: SelectionRangeEx ): ContentModelDocument { const model = createContentModelDocument(root.ownerDocument!); - const domToModelContext = createDomToModelContext(contentModelContext, range); + const domToModelContext = createDomToModelContext(editorContext, range); if (includeRoot) { singleElementProcessor(model, root, domToModelContext); diff --git a/packages/roosterjs-content-model/lib/modelApi/selection/hasSelectionInBlock.ts b/packages/roosterjs-content-model/lib/publicApi/selection/hasSelectionInBlock.ts similarity index 73% rename from packages/roosterjs-content-model/lib/modelApi/selection/hasSelectionInBlock.ts rename to packages/roosterjs-content-model/lib/publicApi/selection/hasSelectionInBlock.ts index 103e104356f..6b114c3ffe8 100644 --- a/packages/roosterjs-content-model/lib/modelApi/selection/hasSelectionInBlock.ts +++ b/packages/roosterjs-content-model/lib/publicApi/selection/hasSelectionInBlock.ts @@ -1,10 +1,11 @@ +import hasSelectionInSegment from './hasSelectionInSegment'; import { ContentModelBlock } from '../../publicTypes/block/ContentModelBlock'; -import { hasSelectionInSegment } from './hasSelectionInSegment'; /** - * @internal + * Check if there is selection within the given block + * @param block The block to check */ -export function hasSelectionInBlock(block: ContentModelBlock): boolean { +export default function hasSelectionInBlock(block: ContentModelBlock): boolean { switch (block.blockType) { case 'Paragraph': return block.segments.some(hasSelectionInSegment); diff --git a/packages/roosterjs-content-model/lib/publicApi/selection/hasSelectionInSegment.ts b/packages/roosterjs-content-model/lib/publicApi/selection/hasSelectionInSegment.ts new file mode 100644 index 00000000000..91641f229a0 --- /dev/null +++ b/packages/roosterjs-content-model/lib/publicApi/selection/hasSelectionInSegment.ts @@ -0,0 +1,13 @@ +import hasSelectionInBlock from './hasSelectionInBlock'; +import { ContentModelSegment } from '../../publicTypes/segment/ContentModelSegment'; + +/** + * Check if there is selection within the given segment + * @param segment The segment to check + */ +export default function hasSelectionInSegment(segment: ContentModelSegment): boolean { + return ( + segment.isSelected || + (segment.segmentType == 'General' && segment.blocks.some(hasSelectionInBlock)) + ); +} diff --git a/packages/roosterjs-content-model/lib/publicTypes/IExperimentalContentModelEditor.ts b/packages/roosterjs-content-model/lib/publicTypes/IExperimentalContentModelEditor.ts index b139ec5543a..4ac46d54781 100644 --- a/packages/roosterjs-content-model/lib/publicTypes/IExperimentalContentModelEditor.ts +++ b/packages/roosterjs-content-model/lib/publicTypes/IExperimentalContentModelEditor.ts @@ -1,5 +1,5 @@ -import { ContentModelContext } from './ContentModelContext'; import { ContentModelDocument } from './block/group/ContentModelDocument'; +import { EditorContext } from './context/EditorContext'; import { IEditor } from 'roosterjs-editor-types'; /** @@ -9,9 +9,9 @@ import { IEditor } from 'roosterjs-editor-types'; */ export interface IExperimentalContentModelEditor extends IEditor { /** - * Create a ContentModelContext object used by ContentModel API + * Create a EditorContext object used by ContentModel API */ - createContentModelContext(): ContentModelContext; + createEditorContext(): EditorContext; /** * Create Content Model from DOM tree in this editor diff --git a/packages/roosterjs-content-model/lib/publicTypes/context/DomToModelContext.ts b/packages/roosterjs-content-model/lib/publicTypes/context/DomToModelContext.ts new file mode 100644 index 00000000000..c06841ac635 --- /dev/null +++ b/packages/roosterjs-content-model/lib/publicTypes/context/DomToModelContext.ts @@ -0,0 +1,11 @@ +import { DomToModelFormatContext } from './DomToModelFormatContext'; +import { DomToModelSelectionContext } from './DomToModelSelectionContext'; +import { EditorContext } from './EditorContext'; + +/** + * Context of DOM to Model conversion, used for parse HTML element according to current context + */ +export interface DomToModelContext + extends EditorContext, + DomToModelSelectionContext, + DomToModelFormatContext {} diff --git a/packages/roosterjs-content-model/lib/publicTypes/context/DomToModelFormatContext.ts b/packages/roosterjs-content-model/lib/publicTypes/context/DomToModelFormatContext.ts new file mode 100644 index 00000000000..63fd7d070f0 --- /dev/null +++ b/packages/roosterjs-content-model/lib/publicTypes/context/DomToModelFormatContext.ts @@ -0,0 +1,11 @@ +import { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; + +/** + * Represents format info used by DOM to Content Model conversion + */ +export interface DomToModelFormatContext { + /** + * Format of current segment + */ + segmentFormat: ContentModelSegmentFormat; +} diff --git a/packages/roosterjs-content-model/lib/domToModel/context/DomToModelContext.ts b/packages/roosterjs-content-model/lib/publicTypes/context/DomToModelSelectionContext.ts similarity index 59% rename from packages/roosterjs-content-model/lib/domToModel/context/DomToModelContext.ts rename to packages/roosterjs-content-model/lib/publicTypes/context/DomToModelSelectionContext.ts index 74fbf6ee72e..46d9e303648 100644 --- a/packages/roosterjs-content-model/lib/domToModel/context/DomToModelContext.ts +++ b/packages/roosterjs-content-model/lib/publicTypes/context/DomToModelSelectionContext.ts @@ -1,11 +1,9 @@ -import { ContentModelContext } from '../../publicTypes/ContentModelContext'; -import { ContentModelSegmentFormat } from '../../publicTypes/format/ContentModelSegmentFormat'; import { Coordinates } from 'roosterjs-editor-types'; /** - * @internal + * Represents regular a selection for DOM to Content Model conversion */ -export interface RegularSelection { +export interface DomToModelRegularSelection { /** * Is the selection collapsed */ @@ -33,9 +31,9 @@ export interface RegularSelection { } /** - * @internal + * Represents regular a table for DOM to Content Model conversion */ -export interface TableSelection { +export interface DomToModelTableSelection { /** * Table where selection is located */ @@ -53,9 +51,9 @@ export interface TableSelection { } /** - * @internal + * Represents regular an image for DOM to Content Model conversion */ -export interface ImageSelection { +export interface DomToModelImageSelection { /** * Selected image */ @@ -63,37 +61,26 @@ export interface ImageSelection { } /** - * @internal - * Context of DOM to Model conversion, used for parse HTML element according to current context + * Represents the selection information of content used by DOM to Content Model conversion */ -export interface DomToModelContext { +export interface DomToModelSelectionContext { /** * Is current context under a selection */ isInSelection: boolean; - /** - * Common context for ContentModel - */ - contentModelContext: ContentModelContext; - /** * Regular selection (selection with a highlight background provided by browser) */ - regularSelection?: RegularSelection; + regularSelection?: DomToModelRegularSelection; /** * Table selection provided by editor */ - tableSelection?: TableSelection; + tableSelection?: DomToModelTableSelection; /** * Image selection provided by editor */ - imageSelection?: ImageSelection; - - /** - * Format of current segment - */ - segmentFormat: ContentModelSegmentFormat; + imageSelection?: DomToModelImageSelection; } diff --git a/packages/roosterjs-content-model/lib/publicTypes/ContentModelContext.ts b/packages/roosterjs-content-model/lib/publicTypes/context/EditorContext.ts similarity index 84% rename from packages/roosterjs-content-model/lib/publicTypes/ContentModelContext.ts rename to packages/roosterjs-content-model/lib/publicTypes/context/EditorContext.ts index 2ec9867fbfa..4d2c0aafaf2 100644 --- a/packages/roosterjs-content-model/lib/publicTypes/ContentModelContext.ts +++ b/packages/roosterjs-content-model/lib/publicTypes/context/EditorContext.ts @@ -1,7 +1,7 @@ /** - * A context interface used by ContentModel PAI + * An editor context interface used by ContentModel PAI */ -export interface ContentModelContext { +export interface EditorContext { /** * Whether current content is in dark mode */ diff --git a/packages/roosterjs-content-model/lib/publicTypes/context/ElementProcessor.ts b/packages/roosterjs-content-model/lib/publicTypes/context/ElementProcessor.ts new file mode 100644 index 00000000000..bcc30efe55a --- /dev/null +++ b/packages/roosterjs-content-model/lib/publicTypes/context/ElementProcessor.ts @@ -0,0 +1,14 @@ +import { ContentModelBlockGroup } from '../block/group/ContentModelBlockGroup'; +import { DomToModelContext } from './DomToModelContext'; + +/** + * A function type to process HTML element when do DOM to Content Model conversion + * @param group Parent content model group + * @param element The element to process + * @param context The context object to provide related information + */ +export type ElementProcessor = ( + group: ContentModelBlockGroup, + element: HTMLElement, + context: DomToModelContext +) => void; diff --git a/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomContext.ts b/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomContext.ts new file mode 100644 index 00000000000..9e98dd71098 --- /dev/null +++ b/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomContext.ts @@ -0,0 +1,7 @@ +import { EditorContext } from './EditorContext'; +import { ModelToDomSelectionContext } from './ModelToDomSelectionContext'; + +/** + * Context of Model to DOM conversion, used for generate HTML DOM tree according to current context + */ +export interface ModelToDomContext extends EditorContext, ModelToDomSelectionContext {} diff --git a/packages/roosterjs-content-model/lib/modelToDom/context/ModelToDomContext.ts b/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomSelectionContext.ts similarity index 68% rename from packages/roosterjs-content-model/lib/modelToDom/context/ModelToDomContext.ts rename to packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomSelectionContext.ts index 6de5de0145f..8854f1cee1a 100644 --- a/packages/roosterjs-content-model/lib/modelToDom/context/ModelToDomContext.ts +++ b/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomSelectionContext.ts @@ -1,11 +1,9 @@ -import { ContentModelContext } from '../../publicTypes/ContentModelContext'; import { Coordinates } from 'roosterjs-editor-types'; /** - * @internal * Represents internal data structure for a selection position, combined by block and segment node */ -export interface BlockAndSegmentNode { +export interface ModelToDomBlockAndSegmentNode { /** * The block element of the selection. When segment is null, it represents the start position of this block element, * otherwise block element will be ignored and we can always retrieve position from segment node @@ -19,31 +17,29 @@ export interface BlockAndSegmentNode { } /** - * @internal * Represents internal data structure for regular selection */ -export interface RegularSelection { +export interface ModelToDomRegularSelection { /** * Start position of selection */ - start?: BlockAndSegmentNode; + start?: ModelToDomBlockAndSegmentNode; /** * End position of selection */ - end?: BlockAndSegmentNode; + end?: ModelToDomBlockAndSegmentNode; /** * Current navigating position */ - current: BlockAndSegmentNode; + current: ModelToDomBlockAndSegmentNode; } /** - * @internal * Represents internal data structure for table selection */ -export interface TableSelection { +export interface ModelToDomTableSelection { /** * Table where selection is located */ @@ -61,21 +57,16 @@ export interface TableSelection { } /** - * @internal + * Represents selection info used by Content Model to DOM conversion */ -export interface ModelToDomContext { - /** - * Common context for ContentModel - */ - readonly contentModelContext: ContentModelContext; - +export interface ModelToDomSelectionContext { /** * Regular selection info */ - regularSelection: RegularSelection; + regularSelection: ModelToDomRegularSelection; /** * Table selection info */ - tableSelection?: TableSelection; + tableSelection?: ModelToDomTableSelection; } diff --git a/packages/roosterjs-content-model/test/domToModel/context/createDomToModelContextTest.ts b/packages/roosterjs-content-model/test/domToModel/context/createDomToModelContextTest.ts index 441636325c9..cae435728f3 100644 --- a/packages/roosterjs-content-model/test/domToModel/context/createDomToModelContextTest.ts +++ b/packages/roosterjs-content-model/test/domToModel/context/createDomToModelContextTest.ts @@ -1,9 +1,9 @@ -import { ContentModelContext } from '../../../lib/publicTypes/ContentModelContext'; import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; +import { EditorContext } from '../../../lib/publicTypes/context/EditorContext'; import { SelectionRangeTypes } from 'roosterjs-editor-types'; describe('createDomToModelContext', () => { - const defaultContentModelContext: ContentModelContext = { + const editorContext: EditorContext = { isDarkMode: false, zoomScale: 1, isRightToLeft: false, @@ -14,24 +14,24 @@ describe('createDomToModelContext', () => { const context = createDomToModelContext(); expect(context).toEqual({ - contentModelContext: defaultContentModelContext, + ...editorContext, segmentFormat: {}, isInSelection: false, }); }); it('with content model context', () => { - const contentModelContext: ContentModelContext = { + const editorContext: EditorContext = { isDarkMode: true, zoomScale: 2, isRightToLeft: true, getDarkColor: () => '', }; - const context = createDomToModelContext(contentModelContext); + const context = createDomToModelContext(editorContext); expect(context).toEqual({ - contentModelContext: contentModelContext, + ...editorContext, segmentFormat: {}, isInSelection: false, }); @@ -52,7 +52,7 @@ describe('createDomToModelContext', () => { }); expect(context).toEqual({ - contentModelContext: defaultContentModelContext, + ...editorContext, segmentFormat: {}, isInSelection: false, regularSelection: { @@ -78,7 +78,7 @@ describe('createDomToModelContext', () => { }); expect(context).toEqual({ - contentModelContext: defaultContentModelContext, + ...editorContext, segmentFormat: {}, isInSelection: false, tableSelection: { diff --git a/packages/roosterjs-content-model/test/domToModel/processors/brProcessorTest.ts b/packages/roosterjs-content-model/test/domToModel/processors/brProcessorTest.ts index 559c901f4f0..0b75f783c24 100644 --- a/packages/roosterjs-content-model/test/domToModel/processors/brProcessorTest.ts +++ b/packages/roosterjs-content-model/test/domToModel/processors/brProcessorTest.ts @@ -1,7 +1,7 @@ import { brProcessor } from '../../../lib/domToModel/processors/brProcessor'; import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; -import { DomToModelContext } from '../../../lib/domToModel/context/DomToModelContext'; +import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; describe('brProcessor', () => { let context: DomToModelContext; diff --git a/packages/roosterjs-content-model/test/domToModel/processors/containerProcessorTest.ts b/packages/roosterjs-content-model/test/domToModel/processors/containerProcessorTest.ts index bd7bec9498c..5b198ff6ffd 100644 --- a/packages/roosterjs-content-model/test/domToModel/processors/containerProcessorTest.ts +++ b/packages/roosterjs-content-model/test/domToModel/processors/containerProcessorTest.ts @@ -7,7 +7,7 @@ import { ContentModelDocument } from '../../../lib/publicTypes/block/group/Conte import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; import { createText } from '../../../lib/modelApi/creators/createText'; -import { DomToModelContext } from '../../../lib/domToModel/context/DomToModelContext'; +import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; describe('containerProcessor', () => { let doc: ContentModelDocument; diff --git a/packages/roosterjs-content-model/test/domToModel/processors/generalBlockProcessorTest.ts b/packages/roosterjs-content-model/test/domToModel/processors/generalBlockProcessorTest.ts index 0900b697964..57553cbb9c8 100644 --- a/packages/roosterjs-content-model/test/domToModel/processors/generalBlockProcessorTest.ts +++ b/packages/roosterjs-content-model/test/domToModel/processors/generalBlockProcessorTest.ts @@ -3,7 +3,7 @@ import * as createGeneralBlock from '../../../lib/modelApi/creators/createGenera import { ContentModelGeneralBlock } from '../../../lib/publicTypes/block/group/ContentModelGeneralBlock'; import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; -import { DomToModelContext } from '../../../lib/domToModel/context/DomToModelContext'; +import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; import { generalBlockProcessor } from '../../../lib/domToModel/processors/generalBlockProcessor'; describe('generalBlockProcessor', () => { diff --git a/packages/roosterjs-content-model/test/domToModel/processors/generalSegmentProcessorTest.ts b/packages/roosterjs-content-model/test/domToModel/processors/generalSegmentProcessorTest.ts index 0d62211cc04..835a177ede9 100644 --- a/packages/roosterjs-content-model/test/domToModel/processors/generalSegmentProcessorTest.ts +++ b/packages/roosterjs-content-model/test/domToModel/processors/generalSegmentProcessorTest.ts @@ -3,7 +3,7 @@ import * as createGeneralSegment from '../../../lib/modelApi/creators/createGene import { ContentModelGeneralSegment } from '../../../lib/publicTypes/segment/ContentModelGeneralSegment'; import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; -import { DomToModelContext } from '../../../lib/domToModel/context/DomToModelContext'; +import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; import { generalSegmentProcessor } from '../../../lib/domToModel/processors/generalSegmentProcessor'; describe('generalSegmentProcessor', () => { diff --git a/packages/roosterjs-content-model/test/domToModel/processors/tableProcessorTest.ts b/packages/roosterjs-content-model/test/domToModel/processors/tableProcessorTest.ts index 86bf6bc306b..ed9ea8700b4 100644 --- a/packages/roosterjs-content-model/test/domToModel/processors/tableProcessorTest.ts +++ b/packages/roosterjs-content-model/test/domToModel/processors/tableProcessorTest.ts @@ -5,7 +5,7 @@ import { ContentModelBlock } from '../../../lib/publicTypes/block/ContentModelBl import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; import { createTableCell } from '../../../lib/modelApi/creators/createTableCell'; -import { DomToModelContext } from '../../../lib/domToModel/context/DomToModelContext'; +import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; import { SegmentFormatHandlers } from '../../../lib/formatHandlers/SegmentFormatHandlers'; import { TableCellFormatHandlers } from '../../../lib/formatHandlers/TableCellFormatHandler'; import { TableFormatHandlers } from '../../../lib/formatHandlers/TableFormatHandlers'; @@ -299,7 +299,7 @@ describe('tableProcessor with format', () => { } as any) as HTMLTableElement; const doc = createContentModelDocument(document); - context.contentModelContext.zoomScale = 2; + context.zoomScale = 2; tableProcessor(doc, mockedTable, context); diff --git a/packages/roosterjs-content-model/test/domToModel/processors/textProcessorTest.ts b/packages/roosterjs-content-model/test/domToModel/processors/textProcessorTest.ts index 5e6bc6a2c3f..44fb5e22ebe 100644 --- a/packages/roosterjs-content-model/test/domToModel/processors/textProcessorTest.ts +++ b/packages/roosterjs-content-model/test/domToModel/processors/textProcessorTest.ts @@ -1,6 +1,6 @@ import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; -import { DomToModelContext } from '../../../lib/domToModel/context/DomToModelContext'; +import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; import { textProcessor } from '../../../lib/domToModel/processors/textProcessor'; describe('textProcessor', () => { diff --git a/packages/roosterjs-content-model/test/domToModel/utils/parseFormatTest.ts b/packages/roosterjs-content-model/test/domToModel/utils/parseFormatTest.ts index ce310d9a1e3..3ae64a70e68 100644 --- a/packages/roosterjs-content-model/test/domToModel/utils/parseFormatTest.ts +++ b/packages/roosterjs-content-model/test/domToModel/utils/parseFormatTest.ts @@ -1,13 +1,9 @@ -import { ContentModelContext } from '../../../lib/publicTypes/ContentModelContext'; +import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; import { FormatHandler } from '../../../lib/formatHandlers/FormatHandler'; import { parseFormat } from '../../../lib/domToModel/utils/parseFormat'; describe('parseFormat', () => { - const defaultContext: ContentModelContext = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + const defaultContext = createDomToModelContext(); it('empty handlers', () => { const element = document.createElement('div'); @@ -41,11 +37,7 @@ describe('parseFormat', () => { }); describe('Default styles', () => { - const defaultContext: ContentModelContext = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + const defaultContext = createDomToModelContext(); function runTest(tag: string, expectResult: Partial) { const element = document.createElement(tag); @@ -94,32 +86,4 @@ describe('Default styles', () => { it('Default style for U', () => { runTest('u', { textDecoration: 'underline' }); }); - - it('Default style for FONT', () => { - const element = document.createElement('font'); - element.face = 'font'; - element.size = '4'; - element.color = 'red'; - - const handlers: FormatHandler[] = [ - { - parse: (format, e, c, defaultStyle) => { - expect(defaultStyle).toEqual({ - fontFamily: 'font', - fontSize: '18px', - color: 'red', - }); - expect(c).toBe(defaultContext); - - format.a = 1; - }, - apply: null!, - }, - ]; - const format = {}; - - parseFormat(element, handlers, format, defaultContext); - - expect(format).toEqual({ a: 1 }); - }); }); diff --git a/packages/roosterjs-content-model/test/formatHandlers/common/backgroundColorFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/common/backgroundColorFormatHandlerTest.ts index 5b0ff0a736b..532676fb79c 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/common/backgroundColorFormatHandlerTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/common/backgroundColorFormatHandlerTest.ts @@ -1,19 +1,18 @@ import { BackgroundColorFormat } from '../../../lib/publicTypes/format/formatParts/BackgroundColorFormat'; import { backgroundColorFormatHandler } from '../../../lib/formatHandlers/common/backgroundColorFormatHandler'; -import { ContentModelContext } from '../../../lib/publicTypes/ContentModelContext'; +import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; +import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; +import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; +import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; describe('backgroundColorFormatHandler.parse', () => { let div: HTMLElement; - let context: ContentModelContext; + let context: DomToModelContext; let format: BackgroundColorFormat; beforeEach(() => { div = document.createElement('div'); - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createDomToModelContext(); format = {}; }); @@ -96,16 +95,12 @@ describe('backgroundColorFormatHandler.parse', () => { describe('backgroundColorFormatHandler.apply', () => { let div: HTMLElement; - let context: ContentModelContext; + let context: ModelToDomContext; let format: BackgroundColorFormat; beforeEach(() => { div = document.createElement('div'); - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createModelToDomContext(); format = {}; }); diff --git a/packages/roosterjs-content-model/test/formatHandlers/common/borderFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/common/borderFormatHandlerTest.ts index 1375f426480..0f200289019 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/common/borderFormatHandlerTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/common/borderFormatHandlerTest.ts @@ -1,20 +1,19 @@ import { BorderFormat } from '../../../lib/publicTypes/format/formatParts/BorderFormat'; import { borderFormatHandler } from '../../../lib/formatHandlers/common/borderFormatHandler'; -import { ContentModelContext } from '../../../lib/publicTypes/ContentModelContext'; +import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; +import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; +import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; +import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; describe('borderFormatHandler.parse', () => { let div: HTMLElement; let format: BorderFormat; - let context: ContentModelContext; + let context: DomToModelContext; beforeEach(() => { div = document.createElement('div'); format = {}; - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createDomToModelContext(); }); it('No border', () => { @@ -103,16 +102,12 @@ describe('borderFormatHandler.parse', () => { describe('borderFormatHandler.apply', () => { let div: HTMLElement; let format: BorderFormat; - let context: ContentModelContext; + let context: ModelToDomContext; beforeEach(() => { div = document.createElement('div'); format = {}; - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createModelToDomContext(); }); it('No border', () => { diff --git a/packages/roosterjs-content-model/test/formatHandlers/common/idFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/common/idFormatHandlerTest.ts index 91cf83ac799..fb80ea2f907 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/common/idFormatHandlerTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/common/idFormatHandlerTest.ts @@ -1,20 +1,19 @@ -import { ContentModelContext } from '../../../lib/publicTypes/ContentModelContext'; +import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; +import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; +import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; import { IdFormat } from '../../../lib/publicTypes/format/formatParts/IdFormat'; import { idFormatHandler } from '../../../lib/formatHandlers/common/idFormatHandler'; +import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; describe('idFormatHandler.parse', () => { let div: HTMLElement; let format: IdFormat; - let context: ContentModelContext; + let context: DomToModelContext; beforeEach(() => { div = document.createElement('div'); format = {}; - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createDomToModelContext(); }); it('No id', () => { @@ -32,16 +31,12 @@ describe('idFormatHandler.parse', () => { describe('idFormatHandler.apply', () => { let div: HTMLElement; let format: IdFormat; - let context: ContentModelContext; + let context: ModelToDomContext; beforeEach(() => { div = document.createElement('div'); format = {}; - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createModelToDomContext(); }); it('No id', () => { diff --git a/packages/roosterjs-content-model/test/formatHandlers/common/sizeFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/common/sizeFormatHandlerTest.ts index aad36a35d48..cb011c9a0fb 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/common/sizeFormatHandlerTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/common/sizeFormatHandlerTest.ts @@ -1,18 +1,17 @@ -import { ContentModelContext } from '../../../lib/publicTypes/ContentModelContext'; +import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; +import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; +import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; +import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; import { SizeFormat } from '../../../lib/publicTypes/format/formatParts/SizeFormat'; import { sizeFormatHandler } from '../../../lib/formatHandlers/common/sizeFormatHandler'; describe('sizeFormatHandler.parse', () => { let format: SizeFormat; - let context: ContentModelContext; + let context: DomToModelContext; beforeEach(() => { format = {}; - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createDomToModelContext(); }); it('Not able to get size', () => { @@ -49,16 +48,12 @@ describe('sizeFormatHandler.parse', () => { describe('sizeFormatHandler.apply', () => { let div: HTMLElement; let format: SizeFormat; - let context: ContentModelContext; + let context: ModelToDomContext; beforeEach(() => { div = document.createElement('div'); format = {}; - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createModelToDomContext(); }); it('No size', () => { diff --git a/packages/roosterjs-content-model/test/formatHandlers/common/textAlignFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/common/textAlignFormatHandlerTest.ts index 7fbe547a83e..ce77874fa5a 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/common/textAlignFormatHandlerTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/common/textAlignFormatHandlerTest.ts @@ -1,20 +1,19 @@ -import { ContentModelContext } from '../../../lib/publicTypes/ContentModelContext'; +import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; +import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; +import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; +import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; import { TextAlignFormat } from '../../../lib/publicTypes/format/formatParts/TextAlignFormat'; import { textAlignFormatHandler } from '../../../lib/formatHandlers/common/textAlignFormatHandler'; describe('textAlignFormatHandler.parse', () => { let div: HTMLElement; let format: TextAlignFormat; - let context: ContentModelContext; + let context: DomToModelContext; beforeEach(() => { div = document.createElement('div'); format = {}; - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createDomToModelContext(); }); function runTest( @@ -76,16 +75,12 @@ describe('textAlignFormatHandler.parse', () => { describe('textAlignFormatHandler.apply', () => { let div: HTMLElement; let format: TextAlignFormat; - let context: ContentModelContext; + let context: ModelToDomContext; beforeEach(() => { div = document.createElement('div'); format = {}; - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createModelToDomContext(); }); it('No alignment', () => { diff --git a/packages/roosterjs-content-model/test/formatHandlers/common/verticalAlignFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/common/verticalAlignFormatHandlerTest.ts index 318b6b6fd0f..8bae8a94d95 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/common/verticalAlignFormatHandlerTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/common/verticalAlignFormatHandlerTest.ts @@ -1,20 +1,19 @@ -import { ContentModelContext } from '../../../lib/publicTypes/ContentModelContext'; +import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; +import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; +import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; +import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; import { VerticalAlignFormat } from '../../../lib/publicTypes/format/formatParts/VerticalAlignFormat'; import { verticalAlignFormatHandler } from '../../../lib/formatHandlers/common/verticalAlignFormatHandler'; describe('verticalAlignFormatHandler.parse', () => { let div: HTMLElement; let format: VerticalAlignFormat; - let context: ContentModelContext; + let context: DomToModelContext; beforeEach(() => { div = document.createElement('div'); format = {}; - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createDomToModelContext(); }); function runTest( @@ -68,16 +67,12 @@ describe('verticalAlignFormatHandler.parse', () => { describe('verticalAlignFormatHandler.apply', () => { let div: HTMLElement; let format: VerticalAlignFormat; - let context: ContentModelContext; + let context: ModelToDomContext; beforeEach(() => { div = document.createElement('div'); format = {}; - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createModelToDomContext(); }); it('No alignment', () => { diff --git a/packages/roosterjs-content-model/test/formatHandlers/createFormatContextTest.ts b/packages/roosterjs-content-model/test/formatHandlers/createFormatContextTest.ts index b21420d9871..62f3ab0e283 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/createFormatContextTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/createFormatContextTest.ts @@ -6,12 +6,10 @@ describe('createFormatContextTest', () => { const context = createDomToModelContext(); expect(context).toEqual({ - contentModelContext: { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - getDarkColor: undefined, - }, + isDarkMode: false, + zoomScale: 1, + isRightToLeft: false, + getDarkColor: undefined, isInSelection: false, segmentFormat: {}, }); @@ -28,12 +26,10 @@ describe('createFormatContextTest', () => { }); expect(context).toEqual({ - contentModelContext: { - isDarkMode: true, - zoomScale: 2, - isRightToLeft: true, - getDarkColor: getDarkColor, - }, + isDarkMode: true, + zoomScale: 2, + isRightToLeft: true, + getDarkColor: getDarkColor, isInSelection: false, segmentFormat: {}, }); @@ -62,12 +58,10 @@ describe('createFormatContextTest', () => { ); expect(context).toEqual({ - contentModelContext: { - isDarkMode: true, - zoomScale: 2, - isRightToLeft: true, - getDarkColor: getDarkColor, - }, + isDarkMode: true, + zoomScale: 2, + isRightToLeft: true, + getDarkColor: getDarkColor, isInSelection: false, regularSelection: { startContainer: text, @@ -111,12 +105,10 @@ describe('createFormatContextTest', () => { ); expect(context).toEqual({ - contentModelContext: { - isDarkMode: true, - zoomScale: 2, - isRightToLeft: true, - getDarkColor: getDarkColor, - }, + isDarkMode: true, + zoomScale: 2, + isRightToLeft: true, + getDarkColor: getDarkColor, isInSelection: false, tableSelection: { table: table, @@ -151,12 +143,10 @@ describe('createFormatContextTest', () => { ); expect(context).toEqual({ - contentModelContext: { - isDarkMode: true, - zoomScale: 2, - isRightToLeft: true, - getDarkColor: getDarkColor, - }, + isDarkMode: true, + zoomScale: 2, + isRightToLeft: true, + getDarkColor: getDarkColor, isInSelection: false, segmentFormat: {}, }); @@ -182,12 +172,10 @@ describe('createFormatContextTest', () => { ); expect(context).toEqual({ - contentModelContext: { - isDarkMode: true, - zoomScale: 2, - isRightToLeft: true, - getDarkColor: getDarkColor, - }, + isDarkMode: true, + zoomScale: 2, + isRightToLeft: true, + getDarkColor: getDarkColor, isInSelection: false, segmentFormat: {}, }); diff --git a/packages/roosterjs-content-model/test/formatHandlers/segment/boldFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/segment/boldFormatHandlerTest.ts index 7efdf90f6d2..4adf946c74f 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/segment/boldFormatHandlerTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/segment/boldFormatHandlerTest.ts @@ -1,19 +1,18 @@ import { BoldFormat } from '../../../lib/publicTypes/format/formatParts/BoldFormat'; import { boldFormatHandler } from '../../../lib/formatHandlers/segment/boldFormatHandler'; -import { ContentModelContext } from '../../../lib/publicTypes/ContentModelContext'; +import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; +import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; +import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; +import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; describe('boldFormatHandler.parse', () => { let div: HTMLElement; - let context: ContentModelContext; + let context: DomToModelContext; let format: BoldFormat; beforeEach(() => { div = document.createElement('div'); - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createDomToModelContext(); format = {}; }); @@ -90,16 +89,12 @@ describe('boldFormatHandler.parse', () => { describe('boldFormatHandler.apply', () => { let div: HTMLElement; let format: BoldFormat; - let context: ContentModelContext; + let context: ModelToDomContext; beforeEach(() => { div = document.createElement('div'); format = {}; - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createModelToDomContext(); }); it('no bold', () => { diff --git a/packages/roosterjs-content-model/test/formatHandlers/segment/fontFamilyFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/segment/fontFamilyFormatHandlerTest.ts index a89e40b01ce..d5b9cf0ac9a 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/segment/fontFamilyFormatHandlerTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/segment/fontFamilyFormatHandlerTest.ts @@ -1,19 +1,18 @@ -import { ContentModelContext } from '../../../lib/publicTypes/ContentModelContext'; +import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; +import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; +import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; import { FontFamilyFormat } from '../../../lib/publicTypes/format/formatParts/FontFamilyFormat'; import { fontFamilyFormatHandler } from '../../../lib/formatHandlers/segment/fontFamilyFormatHandler'; +import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; describe('fontFamilyFormatHandler.parse', () => { let div: HTMLElement; - let context: ContentModelContext; + let context: DomToModelContext; let format: FontFamilyFormat; beforeEach(() => { div = document.createElement('div'); - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createDomToModelContext(); format = {}; }); @@ -47,16 +46,12 @@ describe('fontFamilyFormatHandler.parse', () => { describe('fontFamilyFormatHandler.apply', () => { let div: HTMLElement; let format: FontFamilyFormat; - let context: ContentModelContext; + let context: ModelToDomContext; beforeEach(() => { div = document.createElement('div'); format = {}; - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createModelToDomContext(); }); it('no font', () => { diff --git a/packages/roosterjs-content-model/test/formatHandlers/segment/fontSizeFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/segment/fontSizeFormatHandlerTest.ts index 5735d5831f0..f2f48e563d6 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/segment/fontSizeFormatHandlerTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/segment/fontSizeFormatHandlerTest.ts @@ -1,19 +1,18 @@ -import { ContentModelContext } from '../../../lib/publicTypes/ContentModelContext'; +import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; +import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; +import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; import { FontSizeFormat } from '../../../lib/publicTypes/format/formatParts/FontSizeFormat'; import { fontSizeFormatHandler } from '../../../lib/formatHandlers/segment/fontSizeFormatHandler'; +import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; describe('fontSizeFormatHandler.parse', () => { let div: HTMLElement; - let context: ContentModelContext; + let context: DomToModelContext; let format: FontSizeFormat; beforeEach(() => { div = document.createElement('div'); - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createDomToModelContext(); format = {}; }); @@ -55,16 +54,12 @@ describe('fontSizeFormatHandler.parse', () => { describe('fontSizeFormatHandler.apply', () => { let div: HTMLElement; let format: FontSizeFormat; - let context: ContentModelContext; + let context: ModelToDomContext; beforeEach(() => { div = document.createElement('div'); format = {}; - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createModelToDomContext(); }); it('no font size', () => { diff --git a/packages/roosterjs-content-model/test/formatHandlers/segment/italicFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/segment/italicFormatHandlerTest.ts index 18fc1d98840..c3f24804ef0 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/segment/italicFormatHandlerTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/segment/italicFormatHandlerTest.ts @@ -1,19 +1,18 @@ -import { ContentModelContext } from '../../../lib/publicTypes/ContentModelContext'; +import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; +import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; +import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; import { ItalicFormat } from '../../../lib/publicTypes/format/formatParts/ItalicFormat'; import { italicFormatHandler } from '../../../lib/formatHandlers/segment/italicFormatHandler'; +import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; describe('italicFormatHandler.parse', () => { let div: HTMLElement; - let context: ContentModelContext; + let context: DomToModelContext; let format: ItalicFormat; beforeEach(() => { div = document.createElement('div'); - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createDomToModelContext(); format = {}; }); @@ -83,16 +82,12 @@ describe('italicFormatHandler.parse', () => { describe('italicFormatHandler.apply', () => { let div: HTMLElement; let format: ItalicFormat; - let context: ContentModelContext; + let context: ModelToDomContext; beforeEach(() => { div = document.createElement('div'); format = {}; - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createModelToDomContext(); }); it('no italic', () => { diff --git a/packages/roosterjs-content-model/test/formatHandlers/segment/strikeFormatHandleTest.ts b/packages/roosterjs-content-model/test/formatHandlers/segment/strikeFormatHandleTest.ts index eaf260eca4c..1d7a50de5d8 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/segment/strikeFormatHandleTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/segment/strikeFormatHandleTest.ts @@ -1,19 +1,18 @@ -import { ContentModelContext } from '../../../lib/publicTypes/ContentModelContext'; +import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; +import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; +import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; +import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; import { StrikeFormat } from '../../../lib/publicTypes/format/formatParts/StrikeFormat'; import { strikeFormatHandler } from '../../../lib/formatHandlers/segment/strikeFormatHandler'; describe('strikeFormatHandler.parse', () => { let div: HTMLElement; - let context: ContentModelContext; + let context: DomToModelContext; let format: StrikeFormat; beforeEach(() => { div = document.createElement('div'); - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createDomToModelContext(); format = {}; }); @@ -75,16 +74,12 @@ describe('strikeFormatHandler.parse', () => { describe('strikeFormatHandler.apply', () => { let div: HTMLElement; let format: StrikeFormat; - let context: ContentModelContext; + let context: ModelToDomContext; beforeEach(() => { div = document.createElement('div'); format = {}; - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createModelToDomContext(); }); it('no strikethrough', () => { diff --git a/packages/roosterjs-content-model/test/formatHandlers/segment/superOrSubScriptFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/segment/superOrSubScriptFormatHandlerTest.ts index 71f2cc6f999..86bcf652619 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/segment/superOrSubScriptFormatHandlerTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/segment/superOrSubScriptFormatHandlerTest.ts @@ -1,19 +1,18 @@ -import { ContentModelContext } from '../../../lib/publicTypes/ContentModelContext'; +import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; +import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; +import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; +import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; import { SuperOrSubScriptFormat } from '../../../lib/publicTypes/format/formatParts/SuperOrSubScriptFormat'; import { superOrSubScriptFormatHandler } from '../../../lib/formatHandlers/segment/superOrSubScriptFormatHandler'; describe('superOrSubScriptFormatHandler.parse', () => { let div: HTMLElement; - let context: ContentModelContext; + let context: DomToModelContext; let format: SuperOrSubScriptFormat; beforeEach(() => { div = document.createElement('div'); - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createDomToModelContext(); format = {}; }); @@ -99,16 +98,12 @@ describe('superOrSubScriptFormatHandler.parse', () => { describe('superOrSubScriptFormatHandler.apply', () => { let div: HTMLElement; let format: SuperOrSubScriptFormat; - let context: ContentModelContext; + let context: ModelToDomContext; beforeEach(() => { div = document.createElement('div'); format = {}; - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createModelToDomContext(); }); it('no sub/sup', () => { diff --git a/packages/roosterjs-content-model/test/formatHandlers/segment/textColorFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/segment/textColorFormatHandlerTest.ts index 28c0e8452c4..10bc466d4ca 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/segment/textColorFormatHandlerTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/segment/textColorFormatHandlerTest.ts @@ -1,19 +1,18 @@ -import { ContentModelContext } from '../../../lib/publicTypes/ContentModelContext'; +import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; +import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; +import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; +import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; import { TextColorFormat } from '../../../lib/publicTypes/format/formatParts/TextColorFormat'; import { textColorFormatHandler } from '../../../lib/formatHandlers/segment/textColorFormatHandler'; describe('textColorFormatHandler.parse', () => { let div: HTMLElement; - let context: ContentModelContext; + let context: DomToModelContext; let format: TextColorFormat; beforeEach(() => { div = document.createElement('div'); - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createDomToModelContext(); format = {}; }); @@ -96,16 +95,12 @@ describe('textColorFormatHandler.parse', () => { describe('textColorFormatHandler.apply', () => { let div: HTMLElement; - let context: ContentModelContext; + let context: ModelToDomContext; let format: TextColorFormat; beforeEach(() => { div = document.createElement('div'); - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createModelToDomContext(); format = {}; }); diff --git a/packages/roosterjs-content-model/test/formatHandlers/segment/underlineFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/segment/underlineFormatHandlerTest.ts index a282e2ee53a..bf95bec46f2 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/segment/underlineFormatHandlerTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/segment/underlineFormatHandlerTest.ts @@ -1,19 +1,18 @@ -import { ContentModelContext } from '../../../lib/publicTypes/ContentModelContext'; +import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; +import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; +import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; +import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; import { UnderlineFormat } from '../../../lib/publicTypes/format/formatParts/UnderlineFormat'; import { underlineFormatHandler } from '../../../lib/formatHandlers/segment/underlineFormatHandler'; describe('underlineFormatHandler.parse', () => { let div: HTMLElement; - let context: ContentModelContext; + let context: DomToModelContext; let format: UnderlineFormat; beforeEach(() => { div = document.createElement('div'); - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createDomToModelContext(); format = {}; }); @@ -79,16 +78,12 @@ describe('underlineFormatHandler.parse', () => { describe('underlineFormatHandler.apply', () => { let div: HTMLElement; let format: UnderlineFormat; - let context: ContentModelContext; + let context: ModelToDomContext; beforeEach(() => { div = document.createElement('div'); format = {}; - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createModelToDomContext(); }); it('no underline', () => { diff --git a/packages/roosterjs-content-model/test/formatHandlers/table/tableCellMetadataFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/table/tableCellMetadataFormatHandlerTest.ts index 3d617504215..60a83a83e47 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/table/tableCellMetadataFormatHandlerTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/table/tableCellMetadataFormatHandlerTest.ts @@ -1,20 +1,19 @@ -import { ContentModelContext } from '../../../lib/publicTypes/ContentModelContext'; +import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; +import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; +import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; +import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; import { TableCellMetadataFormat } from '../../../lib/publicTypes/format/formatParts/TableCellMetadataFormat'; import { tableCellMetadataFormatHandler } from '../../../lib/formatHandlers/table/tableCellMetadataFormatHandler'; describe('tableCellMetadataFormatHandler.parse', () => { let div: HTMLElement; let format: TableCellMetadataFormat; - let context: ContentModelContext; + let context: DomToModelContext; beforeEach(() => { div = document.createElement('div'); format = {}; - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createDomToModelContext(); }); function runTest(metadata: any, expectedValue: TableCellMetadataFormat) { @@ -47,16 +46,12 @@ describe('tableCellMetadataFormatHandler.parse', () => { describe('tableCellMetadataFormatHandler.apply', () => { let div: HTMLElement; let format: TableCellMetadataFormat; - let context: ContentModelContext; + let context: ModelToDomContext; beforeEach(() => { div = document.createElement('div'); format = {}; - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createModelToDomContext(); }); function runTest(tableCellFormat: TableCellMetadataFormat | null, expectedValue: any) { diff --git a/packages/roosterjs-content-model/test/formatHandlers/table/tableMetadataFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/table/tableMetadataFormatHandlerTest.ts index e1fdeced54c..89836b24bdb 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/table/tableMetadataFormatHandlerTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/table/tableMetadataFormatHandlerTest.ts @@ -1,4 +1,7 @@ -import { ContentModelContext } from '../../../lib/publicTypes/ContentModelContext'; +import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; +import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; +import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; +import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; import { TableBorderFormat } from 'roosterjs-editor-types'; import { TableMetadataFormat } from '../../../lib/publicTypes/format/formatParts/TableMetadataFormat'; import { tableMetadataFormatHandler } from '../../../lib/formatHandlers/table/tableMetadataFormatHandler'; @@ -6,16 +9,12 @@ import { tableMetadataFormatHandler } from '../../../lib/formatHandlers/table/ta describe('tableMetadataFormatHandler.parse', () => { let div: HTMLElement; let format: TableMetadataFormat; - let context: ContentModelContext; + let context: DomToModelContext; beforeEach(() => { div = document.createElement('div'); format = {}; - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createDomToModelContext(); }); function runTest(metadata: any, expectedValue: TableMetadataFormat) { @@ -87,16 +86,12 @@ describe('tableMetadataFormatHandler.parse', () => { describe('tableMetadataFormatHandler.apply', () => { let div: HTMLElement; let format: TableMetadataFormat; - let context: ContentModelContext; + let context: ModelToDomContext; beforeEach(() => { div = document.createElement('div'); format = {}; - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createModelToDomContext(); }); function runTest(tableFormat: TableMetadataFormat | null, expectedValue: any) { diff --git a/packages/roosterjs-content-model/test/formatHandlers/table/tableSpacingFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/table/tableSpacingFormatHandlerTest.ts index 0ed82c3ecba..4c8ecf635aa 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/table/tableSpacingFormatHandlerTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/table/tableSpacingFormatHandlerTest.ts @@ -1,20 +1,19 @@ -import { ContentModelContext } from '../../../lib/publicTypes/ContentModelContext'; +import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; +import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; +import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; +import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; import { SpacingFormat } from '../../../lib/publicTypes/format/formatParts/SpacingFormat'; import { tableSpacingFormatHandler } from '../../../lib/formatHandlers/table/tableSpacingFormatHandler'; describe('tableSpacingFormatHandler.parse', () => { let div: HTMLElement; let format: SpacingFormat; - let context: ContentModelContext; + let context: DomToModelContext; beforeEach(() => { div = document.createElement('div'); format = {}; - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createDomToModelContext(); }); it('No value', () => { @@ -38,16 +37,12 @@ describe('tableSpacingFormatHandler.parse', () => { describe('tableSpacingFormatHandler.apply', () => { let div: HTMLElement; let format: SpacingFormat; - let context: ContentModelContext; + let context: ModelToDomContext; beforeEach(() => { div = document.createElement('div'); format = {}; - context = { - isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, - }; + context = createModelToDomContext(); }); it('No value', () => { diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleBlockTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleBlockTest.ts index 700d50d907a..e0f1234cf88 100644 --- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleBlockTest.ts +++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleBlockTest.ts @@ -4,7 +4,7 @@ import { ContentModelBlock } from '../../../lib/publicTypes/block/ContentModelBl import { ContentModelGeneralSegment } from '../../../lib/publicTypes/segment/ContentModelGeneralSegment'; import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; import { handleBlock } from '../../../lib/modelToDom/handlers/handleBlock'; -import { ModelToDomContext } from '../../../lib/modelToDom/context/ModelToDomContext'; +import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; import { SegmentFormatHandlers } from '../../../lib/formatHandlers/SegmentFormatHandlers'; describe('handleBlock', () => { @@ -102,7 +102,7 @@ describe('handleBlock', () => { parent.firstChild as HTMLElement, SegmentFormatHandlers, block.format, - context.contentModelContext + context ); }); }); diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleParagraphTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleParagraphTest.ts index 18a58860bdf..3909d723c42 100644 --- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleParagraphTest.ts +++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleParagraphTest.ts @@ -3,7 +3,7 @@ import { ContentModelParagraph } from '../../../lib/publicTypes/block/ContentMod import { ContentModelSegment } from '../../../lib/publicTypes/segment/ContentModelSegment'; import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; import { handleParagraph } from '../../../lib/modelToDom/handlers/handleParagraph'; -import { ModelToDomContext } from '../../../lib/modelToDom/context/ModelToDomContext'; +import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; describe('handleParagraph', () => { let parent: HTMLElement; diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleSegmentTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleSegmentTest.ts index a06eebd4e20..b9c6e6e0f73 100644 --- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleSegmentTest.ts +++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleSegmentTest.ts @@ -2,7 +2,7 @@ import * as handleBlock from '../../../lib/modelToDom/handlers/handleBlock'; import { ContentModelSegment } from '../../../lib/publicTypes/segment/ContentModelSegment'; import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; import { handleSegment } from '../../../lib/modelToDom/handlers/handleSegment'; -import { ModelToDomContext } from '../../../lib/modelToDom/context/ModelToDomContext'; +import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; describe('handleSegment', () => { let parent: HTMLElement; diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleTableTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleTableTest.ts index a409ab43c49..32e5fd11293 100644 --- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleTableTest.ts +++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleTableTest.ts @@ -3,7 +3,7 @@ import { ContentModelTable } from '../../../lib/publicTypes/block/ContentModelTa import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; import { createTableCell } from '../../../lib/modelApi/creators/createTableCell'; import { handleTable } from '../../../lib/modelToDom/handlers/handleTable'; -import { ModelToDomContext } from '../../../lib/modelToDom/context/ModelToDomContext'; +import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; describe('handleTable', () => { let context: ModelToDomContext; diff --git a/packages/roosterjs-content-model/test/modelApi/selection/hasSelectionInBlockTest.ts b/packages/roosterjs-content-model/test/publicApi/selection/hasSelectionInBlockTest.ts similarity index 98% rename from packages/roosterjs-content-model/test/modelApi/selection/hasSelectionInBlockTest.ts rename to packages/roosterjs-content-model/test/publicApi/selection/hasSelectionInBlockTest.ts index 5567ac3918a..7ad8a23a628 100644 --- a/packages/roosterjs-content-model/test/modelApi/selection/hasSelectionInBlockTest.ts +++ b/packages/roosterjs-content-model/test/publicApi/selection/hasSelectionInBlockTest.ts @@ -1,5 +1,5 @@ +import hasSelectionInBlock from '../../../lib/publicApi/selection/hasSelectionInBlock'; import { ContentModelBlock } from '../../../lib/publicTypes/block/ContentModelBlock'; -import { hasSelectionInBlock } from '../../../lib/modelApi/selection/hasSelectionInBlock'; describe('hasSelectionInBlock', () => { it('Empty paragraph block', () => { diff --git a/packages/roosterjs-content-model/test/modelApi/selection/hasSelectionInSegmentTest.ts b/packages/roosterjs-content-model/test/publicApi/selection/hasSelectionInSegmentTest.ts similarity index 97% rename from packages/roosterjs-content-model/test/modelApi/selection/hasSelectionInSegmentTest.ts rename to packages/roosterjs-content-model/test/publicApi/selection/hasSelectionInSegmentTest.ts index b9f31a86861..0072f04f4a9 100644 --- a/packages/roosterjs-content-model/test/modelApi/selection/hasSelectionInSegmentTest.ts +++ b/packages/roosterjs-content-model/test/publicApi/selection/hasSelectionInSegmentTest.ts @@ -1,5 +1,5 @@ +import hasSelectionInSegment from '../../../lib/publicApi/selection/hasSelectionInSegment'; import { ContentModelSegment } from '../../../lib/publicTypes/segment/ContentModelSegment'; -import { hasSelectionInSegment } from '../../../lib/modelApi/selection/hasSelectionInSegment'; describe('hasSelectionInSegment', () => { it('Simple text segment', () => { From c807e3ffc62868d2cb2863b5cfb40afa43fefa31 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Wed, 14 Sep 2022 09:24:51 -0700 Subject: [PATCH 04/41] Content Model customization step 2: Customize element processor and default style (#1253) * Reorganize some code * fix build * Move some code * Remove unnecessary change * More fix * fix comment * Customization 2 * temporarily allow empty interface * ignore a lint rule --- .../editor/ExperimentalContentModelEditor.ts | 25 ++-- .../context/createDomToModelContext.ts | 16 ++- .../domToModel/context/defaultProcessors.ts | 23 ++++ .../lib/domToModel/context/defaultStyles.ts | 36 ++++++ .../domToModel/processors/fontProcessor.ts | 55 +++++++++ .../processors/knownElementProcessor.ts | 36 ++++++ .../processors/singleElementProcessor.ts | 38 +----- .../lib/domToModel/utils/parseFormat.ts | 68 +---------- packages/roosterjs-content-model/lib/index.ts | 7 +- .../context/createModelToDomContext.ts | 6 +- .../lib/publicApi/contentModelToDom.ts | 7 +- .../lib/publicApi/domToContentModel.ts | 7 +- .../IExperimentalContentModelEditor.ts | 32 ++++- .../publicTypes/context/DomToModelContext.ts | 4 +- .../publicTypes/context/DomToModelSettings.ts | 21 ++++ .../context/createDomToModelContextTest.ts | 11 +- .../processors/fontProcessorTest.ts | 113 ++++++++++++++++++ .../formatHandlers/createFormatContextTest.ts | 13 ++ 18 files changed, 397 insertions(+), 121 deletions(-) create mode 100644 packages/roosterjs-content-model/lib/domToModel/context/defaultProcessors.ts create mode 100644 packages/roosterjs-content-model/lib/domToModel/context/defaultStyles.ts create mode 100644 packages/roosterjs-content-model/lib/domToModel/processors/fontProcessor.ts create mode 100644 packages/roosterjs-content-model/lib/domToModel/processors/knownElementProcessor.ts create mode 100644 packages/roosterjs-content-model/lib/publicTypes/context/DomToModelSettings.ts create mode 100644 packages/roosterjs-content-model/test/domToModel/processors/fontProcessorTest.ts diff --git a/demo/scripts/controls/editor/ExperimentalContentModelEditor.ts b/demo/scripts/controls/editor/ExperimentalContentModelEditor.ts index a130fd0201c..069bf431fa0 100644 --- a/demo/scripts/controls/editor/ExperimentalContentModelEditor.ts +++ b/demo/scripts/controls/editor/ExperimentalContentModelEditor.ts @@ -1,12 +1,14 @@ +import { ContentPosition, EditorOptions, SelectionRangeTypes } from 'roosterjs-editor-types'; import { Editor } from 'roosterjs-editor-core'; -import { EditorOptions, SelectionRangeTypes } from 'roosterjs-editor-types'; import { getComputedStyles, Position } from 'roosterjs-editor-dom'; import { + EditorContext, ContentModelDocument, contentModelToDom, domToContentModel, - EditorContext, + DomToModelOption, IExperimentalContentModelEditor, + ModelToDomOption, } from 'roosterjs-content-model'; /** @@ -44,13 +46,15 @@ export default class ExperimentalContentModelEditor extends Editor * Create Content Model from DOM tree in this editor * @param startNode Optional start node. If provided, Content Model will be created from this node (including itself), * otherwise it will create Content Model for the whole content in editor. + * @param option The option to customize the behavior of DOM to Content Model conversion */ - createContentModel(startNode?: HTMLElement): ContentModelDocument { + createContentModel(startNode?: HTMLElement, option?: DomToModelOption): ContentModelDocument { return domToContentModel( startNode || this.contentDiv, this.createEditorContext(), !!startNode, - this.getSelectionRangeEx() + this.getSelectionRangeEx(), + option ); } @@ -58,12 +62,14 @@ export default class ExperimentalContentModelEditor extends Editor * Set content with content model * @param model The content model to set * @param mergingCallback A callback to indicate how should the new content be integrated into existing content + * @param option Additional options to customize the behavior of Content Model to DOM conversion */ setContentModel( model: ContentModelDocument, - mergingCallback: (fragment: DocumentFragment) => void = this.defaultMergingCallback + mergingCallback: (fragment: DocumentFragment) => void = this.defaultMergingCallback, + option?: ModelToDomOption ) { - const [fragment, range] = contentModelToDom(model, this.createEditorContext()); + const [fragment, range] = contentModelToDom(model, this.createEditorContext(), option); switch (range?.type) { case SelectionRangeTypes.Normal: @@ -86,7 +92,10 @@ export default class ExperimentalContentModelEditor extends Editor } private defaultMergingCallback = (fragment: DocumentFragment) => { - this.setContent(''); - this.insertNode(fragment); + while (this.contentDiv.firstChild) { + this.contentDiv.removeChild(this.contentDiv.firstChild); + } + + this.insertNode(fragment, { position: ContentPosition.Begin }); }; } diff --git a/packages/roosterjs-content-model/lib/domToModel/context/createDomToModelContext.ts b/packages/roosterjs-content-model/lib/domToModel/context/createDomToModelContext.ts index a62b7722003..ac871b746b6 100644 --- a/packages/roosterjs-content-model/lib/domToModel/context/createDomToModelContext.ts +++ b/packages/roosterjs-content-model/lib/domToModel/context/createDomToModelContext.ts @@ -1,4 +1,7 @@ +import { defaultProcessorMap } from './defaultProcessors'; +import { defaultStyleMap } from './defaultStyles'; import { DomToModelContext } from '../../publicTypes/context/DomToModelContext'; +import { DomToModelOption } from '../../publicTypes/IExperimentalContentModelEditor'; import { EditorContext } from '../../publicTypes/context/EditorContext'; import { SelectionRangeEx, SelectionRangeTypes } from 'roosterjs-editor-types'; @@ -7,7 +10,8 @@ import { SelectionRangeEx, SelectionRangeTypes } from 'roosterjs-editor-types'; */ export function createDomToModelContext( editorContext?: EditorContext, - range?: SelectionRangeEx + range?: SelectionRangeEx, + options?: DomToModelOption ): DomToModelContext { const context: DomToModelContext = { ...(editorContext || { @@ -19,6 +23,16 @@ export function createDomToModelContext( segmentFormat: {}, isInSelection: false, + + elementProcessors: { + ...defaultProcessorMap, + ...(options?.processorOverride || {}), + }, + + defaultStyles: { + ...defaultStyleMap, + ...(options?.defaultStyleOverride || {}), + }, }; switch (range?.type) { diff --git a/packages/roosterjs-content-model/lib/domToModel/context/defaultProcessors.ts b/packages/roosterjs-content-model/lib/domToModel/context/defaultProcessors.ts new file mode 100644 index 00000000000..826cb2048fd --- /dev/null +++ b/packages/roosterjs-content-model/lib/domToModel/context/defaultProcessors.ts @@ -0,0 +1,23 @@ +import { brProcessor } from '../processors/brProcessor'; +import { ElementProcessor } from '../../publicTypes/context/ElementProcessor'; +import { fontProcessor } from '../processors/fontProcessor'; +import { knownElementProcessor } from '../processors/knownElementProcessor'; +import { tableProcessor } from '../processors/tableProcessor'; + +/** + * @internal + */ +export const defaultProcessorMap: Record = { + B: knownElementProcessor, + BR: brProcessor, + EM: knownElementProcessor, + FONT: fontProcessor, + I: knownElementProcessor, + S: knownElementProcessor, + STRIKE: knownElementProcessor, + STRONG: knownElementProcessor, + SUB: knownElementProcessor, + SUP: knownElementProcessor, + TABLE: tableProcessor, + U: knownElementProcessor, +}; diff --git a/packages/roosterjs-content-model/lib/domToModel/context/defaultStyles.ts b/packages/roosterjs-content-model/lib/domToModel/context/defaultStyles.ts new file mode 100644 index 00000000000..27e08eb8d82 --- /dev/null +++ b/packages/roosterjs-content-model/lib/domToModel/context/defaultStyles.ts @@ -0,0 +1,36 @@ +import { DefaultStyleMap } from '../../publicTypes/context/DomToModelSettings'; + +/** + * @internal + */ +export const defaultStyleMap: DefaultStyleMap = { + B: { + fontWeight: 'bold', + }, + EM: { + fontStyle: 'italic', + }, + I: { + fontStyle: 'italic', + }, + S: { + textDecoration: 'line-through', + }, + STRIKE: { + textDecoration: 'line-through', + }, + STRONG: { + fontWeight: 'bold', + }, + SUB: { + verticalAlign: 'sub', + fontSize: 'smaller', + }, + SUP: { + verticalAlign: 'super', + fontSize: 'smaller', + }, + U: { + textDecoration: 'underline', + }, +}; diff --git a/packages/roosterjs-content-model/lib/domToModel/processors/fontProcessor.ts b/packages/roosterjs-content-model/lib/domToModel/processors/fontProcessor.ts new file mode 100644 index 00000000000..f1e9da9b8b2 --- /dev/null +++ b/packages/roosterjs-content-model/lib/domToModel/processors/fontProcessor.ts @@ -0,0 +1,55 @@ +import { containerProcessor } from './containerProcessor'; +import { ElementProcessor } from '../../publicTypes/context/ElementProcessor'; +import { parseFormat } from '../utils/parseFormat'; +import { SegmentFormatHandlers } from '../../formatHandlers/SegmentFormatHandlers'; +import { stackFormat } from '../utils/stackFormat'; + +const FontSizes = ['10px', '13px', '16px', '18px', '24px', '32px', '48px']; + +function getFontSize(size: string | null) { + const intSize = parseInt(size || ''); + + if (Number.isNaN(intSize)) { + return undefined; + } else if (intSize < 1) { + return FontSizes[0]; + } else if (intSize > FontSizes.length) { + return FontSizes[FontSizes.length - 1]; + } else { + return FontSizes[intSize - 1]; + } +} + +/** + * @internal + */ +export const fontProcessor: ElementProcessor = (group, element, context) => { + stackFormat( + context, + { + segment: 'shallowClone', + }, + () => { + const fontFamily = element.getAttribute('face'); + const fontSize = getFontSize(element.getAttribute('size')); + const textColor = element.getAttribute('color'); + const format = context.segmentFormat; + + if (fontFamily) { + format.fontFamily = fontFamily; + } + + if (fontSize) { + format.fontSize = fontSize; + } + + if (textColor) { + format.textColor = textColor; + } + + parseFormat(element, SegmentFormatHandlers, context.segmentFormat, context); + + containerProcessor(group, element, context); + } + ); +}; diff --git a/packages/roosterjs-content-model/lib/domToModel/processors/knownElementProcessor.ts b/packages/roosterjs-content-model/lib/domToModel/processors/knownElementProcessor.ts new file mode 100644 index 00000000000..4971bd50d3a --- /dev/null +++ b/packages/roosterjs-content-model/lib/domToModel/processors/knownElementProcessor.ts @@ -0,0 +1,36 @@ +import { addBlock } from '../../modelApi/common/addBlock'; +import { containerProcessor } from './containerProcessor'; +import { createParagraph } from '../../modelApi/creators/createParagraph'; +import { ElementProcessor } from '../../publicTypes/context/ElementProcessor'; +import { isBlockElement } from 'roosterjs-editor-dom'; +import { parseFormat } from '../utils/parseFormat'; +import { SegmentFormatHandlers } from '../../formatHandlers/SegmentFormatHandlers'; +import { stackFormat } from '../utils/stackFormat'; + +/** + * @internal + */ +export const knownElementProcessor: ElementProcessor = (group, element, context) => { + if (isBlockElement(element)) { + stackFormat( + context, + { + segment: 'shallowClone', + }, + () => { + parseFormat(element, SegmentFormatHandlers, context.segmentFormat, context); + + addBlock(group, createParagraph(false /*isImplicit*/)); + + containerProcessor(group, element, context); + } + ); + + addBlock(group, createParagraph(false /*isImplicit*/)); + } else { + stackFormat(context, { segment: 'shallowClone' }, () => { + parseFormat(element, SegmentFormatHandlers, context.segmentFormat, context); + containerProcessor(group, element, context); + }); + } +}; diff --git a/packages/roosterjs-content-model/lib/domToModel/processors/singleElementProcessor.ts b/packages/roosterjs-content-model/lib/domToModel/processors/singleElementProcessor.ts index b79745c978f..5786e91e880 100644 --- a/packages/roosterjs-content-model/lib/domToModel/processors/singleElementProcessor.ts +++ b/packages/roosterjs-content-model/lib/domToModel/processors/singleElementProcessor.ts @@ -1,43 +1,7 @@ -import { brProcessor } from './brProcessor'; -import { containerProcessor } from './containerProcessor'; import { ElementProcessor } from '../../publicTypes/context/ElementProcessor'; import { generalBlockProcessor } from './generalBlockProcessor'; import { generalSegmentProcessor } from './generalSegmentProcessor'; import { isBlockElement } from 'roosterjs-editor-dom'; -import { parseFormat } from '../utils/parseFormat'; -import { SegmentFormatHandlers } from '../../formatHandlers/SegmentFormatHandlers'; -import { stackFormat } from '../utils/stackFormat'; -import { tableProcessor } from './tableProcessor'; - -/** - * @internal - */ -export const knownElementProcessor: ElementProcessor = (group, element, context) => { - if (isBlockElement(element)) { - // TODO: Use known block processor instead - generalBlockProcessor(group, element, context); - } else { - stackFormat(context, { segment: 'shallowClone' }, () => { - parseFormat(element, SegmentFormatHandlers, context.segmentFormat, context); - containerProcessor(group, element, context); - }); - } -}; - -const ProcessorMap: Record = { - B: knownElementProcessor, - BR: brProcessor, - EM: knownElementProcessor, - FONT: knownElementProcessor, - I: knownElementProcessor, - S: knownElementProcessor, - STRIKE: knownElementProcessor, - STRONG: knownElementProcessor, - SUB: knownElementProcessor, - SUP: knownElementProcessor, - TABLE: tableProcessor, - U: knownElementProcessor, -}; /** * @internal @@ -47,7 +11,7 @@ const ProcessorMap: Record = { */ export const singleElementProcessor: ElementProcessor = (group, element, context) => { const processor = - ProcessorMap[element.tagName] || + context.elementProcessors[element.tagName] || (isBlockElement(element) ? generalBlockProcessor : generalSegmentProcessor); processor(group, element, context); diff --git a/packages/roosterjs-content-model/lib/domToModel/utils/parseFormat.ts b/packages/roosterjs-content-model/lib/domToModel/utils/parseFormat.ts index 340cd7c210b..1819afdbdb0 100644 --- a/packages/roosterjs-content-model/lib/domToModel/utils/parseFormat.ts +++ b/packages/roosterjs-content-model/lib/domToModel/utils/parseFormat.ts @@ -1,66 +1,8 @@ import { ContentModelFormatBase } from '../../publicTypes/format/ContentModelFormatBase'; +import { defaultStyleMap } from '../context/defaultStyles'; import { DomToModelContext } from '../../publicTypes/context/DomToModelContext'; import { FormatHandler } from '../../formatHandlers/FormatHandler'; -type DefaultFormatParserType = - | Partial - | ((e: HTMLElement) => Partial); - -const FontSizes = ['10px', '13px', '16px', '18px', '24px', '32px', '48px']; - -function getFontSize(size: string | null) { - const intSize = parseInt(size || ''); - - if (Number.isNaN(intSize)) { - return undefined; - } else if (intSize < 1) { - return FontSizes[0]; - } else if (intSize > FontSizes.length) { - return FontSizes[FontSizes.length - 1]; - } else { - return FontSizes[intSize - 1]; - } -} - -const DefaultStyleMap: Record = { - B: { - fontWeight: 'bold', - }, - EM: { - fontStyle: 'italic', - }, - FONT: e => { - return { - fontFamily: e.getAttribute('face') || undefined, - fontSize: getFontSize(e.getAttribute('size')), - color: e.getAttribute('color') || undefined, - }; - }, - I: { - fontStyle: 'italic', - }, - S: { - textDecoration: 'line-through', - }, - STRIKE: { - textDecoration: 'line-through', - }, - STRONG: { - fontWeight: 'bold', - }, - SUB: { - verticalAlign: 'sub', - fontSize: 'smaller', - }, - SUP: { - verticalAlign: 'super', - fontSize: 'smaller', - }, - U: { - textDecoration: 'underline', - }, -}; - /** * @internal */ @@ -70,12 +12,8 @@ export function parseFormat( format: T, context: DomToModelContext ) { - const styleItem = DefaultStyleMap[element.tagName]; - const defaultStyle = styleItem - ? typeof styleItem === 'function' - ? styleItem(element) - : styleItem - : {}; + const styleItem = defaultStyleMap[element.tagName]; + const defaultStyle = styleItem || {}; handlers.forEach(handler => { handler.parse(format, element, context, defaultStyle); diff --git a/packages/roosterjs-content-model/lib/index.ts b/packages/roosterjs-content-model/lib/index.ts index 2a504b97c3f..290aec8edb6 100644 --- a/packages/roosterjs-content-model/lib/index.ts +++ b/packages/roosterjs-content-model/lib/index.ts @@ -62,6 +62,7 @@ export { DomToModelImageSelection, DomToModelSelectionContext, } from './publicTypes/context/DomToModelSelectionContext'; +export { DomToModelSettings, DefaultStyleMap } from './publicTypes/context/DomToModelSettings'; export { DomToModelContext } from './publicTypes/context/DomToModelContext'; export { ModelToDomContext } from './publicTypes/context/ModelToDomContext'; export { @@ -72,4 +73,8 @@ export { } from './publicTypes/context/ModelToDomSelectionContext'; export { ElementProcessor } from './publicTypes/context/ElementProcessor'; -export { IExperimentalContentModelEditor } from './publicTypes/IExperimentalContentModelEditor'; +export { + IExperimentalContentModelEditor, + DomToModelOption, + ModelToDomOption, +} from './publicTypes/IExperimentalContentModelEditor'; diff --git a/packages/roosterjs-content-model/lib/modelToDom/context/createModelToDomContext.ts b/packages/roosterjs-content-model/lib/modelToDom/context/createModelToDomContext.ts index 4f1a550d364..d1296653fd5 100644 --- a/packages/roosterjs-content-model/lib/modelToDom/context/createModelToDomContext.ts +++ b/packages/roosterjs-content-model/lib/modelToDom/context/createModelToDomContext.ts @@ -1,12 +1,16 @@ import { EditorContext } from '../../publicTypes/context/EditorContext'; import { ModelToDomContext } from '../../publicTypes/context/ModelToDomContext'; +import { ModelToDomOption } from '../../publicTypes/IExperimentalContentModelEditor'; /** * @internal * @param editorContext * @returns */ -export function createModelToDomContext(editorContext?: EditorContext): ModelToDomContext { +export function createModelToDomContext( + editorContext?: EditorContext, + options?: ModelToDomOption +): ModelToDomContext { return { ...(editorContext || { isDarkMode: false, diff --git a/packages/roosterjs-content-model/lib/publicApi/contentModelToDom.ts b/packages/roosterjs-content-model/lib/publicApi/contentModelToDom.ts index f7b242e3786..63e99aa1191 100644 --- a/packages/roosterjs-content-model/lib/publicApi/contentModelToDom.ts +++ b/packages/roosterjs-content-model/lib/publicApi/contentModelToDom.ts @@ -6,6 +6,7 @@ import { handleBlock } from '../modelToDom/handlers/handleBlock'; import { isNodeOfType } from '../domUtils/isNodeOfType'; import { ModelToDomBlockAndSegmentNode } from '../publicTypes/context/ModelToDomSelectionContext'; import { ModelToDomContext } from '../publicTypes/context/ModelToDomContext'; +import { ModelToDomOption } from '../publicTypes/IExperimentalContentModelEditor'; import { optimize } from '../modelToDom/optimizers/optimize'; import { NodePosition, @@ -18,15 +19,17 @@ import { * Create DOM tree fragment from Content Model document * @param model The content model document to generate DOM tree from * @param editorContext Content for Content Model editor + * @param option Additional options to customize the behavior of Content Model to DOM conversion * @returns A Document Fragment that contains the DOM tree generated from the given model, * and a SelectionRangeEx object that contains selection info from the model if any, or null */ export default function contentModelToDom( model: ContentModelDocument, - editorContext: EditorContext + editorContext: EditorContext, + option?: ModelToDomOption ): [DocumentFragment, SelectionRangeEx | null] { const fragment = model.document.createDocumentFragment(); - const modelToDomContext = createModelToDomContext(editorContext); + const modelToDomContext = createModelToDomContext(editorContext, option); handleBlock(model.document, fragment, model, modelToDomContext); optimize(fragment, 2 /*optimizeLevel*/); diff --git a/packages/roosterjs-content-model/lib/publicApi/domToContentModel.ts b/packages/roosterjs-content-model/lib/publicApi/domToContentModel.ts index 8a44f96118d..d80b92b2e4f 100644 --- a/packages/roosterjs-content-model/lib/publicApi/domToContentModel.ts +++ b/packages/roosterjs-content-model/lib/publicApi/domToContentModel.ts @@ -2,6 +2,7 @@ import { containerProcessor } from '../domToModel/processors/containerProcessor' import { ContentModelDocument } from '../publicTypes/block/group/ContentModelDocument'; import { createContentModelDocument } from '../modelApi/creators/createContentModelDocument'; import { createDomToModelContext } from '../domToModel/context/createDomToModelContext'; +import { DomToModelOption } from '../publicTypes/IExperimentalContentModelEditor'; import { EditorContext } from '../publicTypes/context/EditorContext'; import { normalizeModel } from '../modelApi/common/normalizeContentModel'; import { SelectionRangeEx } from 'roosterjs-editor-types'; @@ -13,16 +14,18 @@ import { singleElementProcessor } from '../domToModel/processors/singleElementPr * @param editorContext Context of content model editor * @param includeRoot True to create content model from the root element itself, false to create from all child nodes of root. @default false * @param range Selection range of the DOM tree. If not passed, the content model will not include selection + * @param option The option to customize the behavior of DOM to Content Model conversion * @returns A ContentModelDocument object that contains all the models created from the give root element */ export default function domToContentModel( root: HTMLElement, editorContext: EditorContext, includeRoot?: boolean, - range?: SelectionRangeEx + range?: SelectionRangeEx, + option?: DomToModelOption ): ContentModelDocument { const model = createContentModelDocument(root.ownerDocument!); - const domToModelContext = createDomToModelContext(editorContext, range); + const domToModelContext = createDomToModelContext(editorContext, range, option); if (includeRoot) { singleElementProcessor(model, root, domToModelContext); diff --git a/packages/roosterjs-content-model/lib/publicTypes/IExperimentalContentModelEditor.ts b/packages/roosterjs-content-model/lib/publicTypes/IExperimentalContentModelEditor.ts index 4ac46d54781..1ef978260fc 100644 --- a/packages/roosterjs-content-model/lib/publicTypes/IExperimentalContentModelEditor.ts +++ b/packages/roosterjs-content-model/lib/publicTypes/IExperimentalContentModelEditor.ts @@ -1,7 +1,32 @@ import { ContentModelDocument } from './block/group/ContentModelDocument'; +import { DefaultStyleMap } from './context/DomToModelSettings'; import { EditorContext } from './context/EditorContext'; +import { ElementProcessor } from './context/ElementProcessor'; import { IEditor } from 'roosterjs-editor-types'; +/** + * Options for creating DomToModelContext + */ +export interface DomToModelOption { + /** + * Overrides default element processors + */ + processorOverride?: Record; + + /** + * Overrides default element styles + */ + defaultStyleOverride?: DefaultStyleMap; +} + +/** + * Options for creating ModelToDomContext + */ +// tslint:disable no-empty-interface +export interface ModelToDomOption { + // TODO: Add options here +} + /** * !!! This is a temporary interface and will be removed in the future !!! * @@ -17,16 +42,19 @@ export interface IExperimentalContentModelEditor extends IEditor { * Create Content Model from DOM tree in this editor * @param startNode Optional start node. If provided, Content Model will be created from this node (including itself), * otherwise it will create Content Model for the whole content in editor. + * @param option The options to customize the behavior of DOM to Content Model conversion */ - createContentModel(startNode?: HTMLElement): ContentModelDocument; + createContentModel(startNode?: HTMLElement, option?: DomToModelOption): ContentModelDocument; /** * Set content with content model * @param model The content model to set * @param mergingCallback A callback to indicate how should the new content be integrated into existing content + * @param option Additional options to customize the behavior of Content Model to DOM conversion */ setContentModel( model: ContentModelDocument, - mergingCallback?: (fragment: DocumentFragment) => void + mergingCallback?: (fragment: DocumentFragment) => void, + option?: ModelToDomOption ): void; } diff --git a/packages/roosterjs-content-model/lib/publicTypes/context/DomToModelContext.ts b/packages/roosterjs-content-model/lib/publicTypes/context/DomToModelContext.ts index c06841ac635..cf25ba6c5ba 100644 --- a/packages/roosterjs-content-model/lib/publicTypes/context/DomToModelContext.ts +++ b/packages/roosterjs-content-model/lib/publicTypes/context/DomToModelContext.ts @@ -1,5 +1,6 @@ import { DomToModelFormatContext } from './DomToModelFormatContext'; import { DomToModelSelectionContext } from './DomToModelSelectionContext'; +import { DomToModelSettings } from './DomToModelSettings'; import { EditorContext } from './EditorContext'; /** @@ -8,4 +9,5 @@ import { EditorContext } from './EditorContext'; export interface DomToModelContext extends EditorContext, DomToModelSelectionContext, - DomToModelFormatContext {} + DomToModelFormatContext, + DomToModelSettings {} diff --git a/packages/roosterjs-content-model/lib/publicTypes/context/DomToModelSettings.ts b/packages/roosterjs-content-model/lib/publicTypes/context/DomToModelSettings.ts new file mode 100644 index 00000000000..bd374793aaf --- /dev/null +++ b/packages/roosterjs-content-model/lib/publicTypes/context/DomToModelSettings.ts @@ -0,0 +1,21 @@ +import { ElementProcessor } from './ElementProcessor'; + +/** + * A type of Default style map, from tag name string (in upper case) to a static style object + */ +export type DefaultStyleMap = Record>; + +/** + * Represents settings to customize DOM to Content Model conversion + */ +export interface DomToModelSettings { + /** + * Map of element processors + */ + elementProcessors: Record; + + /** + * Map of default styles + */ + defaultStyles: DefaultStyleMap; +} diff --git a/packages/roosterjs-content-model/test/domToModel/context/createDomToModelContextTest.ts b/packages/roosterjs-content-model/test/domToModel/context/createDomToModelContextTest.ts index cae435728f3..495d93e481e 100644 --- a/packages/roosterjs-content-model/test/domToModel/context/createDomToModelContextTest.ts +++ b/packages/roosterjs-content-model/test/domToModel/context/createDomToModelContextTest.ts @@ -1,4 +1,6 @@ import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; +import { defaultProcessorMap } from '../../../lib/domToModel/context/defaultProcessors'; +import { defaultStyleMap } from '../../../lib/domToModel/context/defaultStyles'; import { EditorContext } from '../../../lib/publicTypes/context/EditorContext'; import { SelectionRangeTypes } from 'roosterjs-editor-types'; @@ -9,7 +11,10 @@ describe('createDomToModelContext', () => { isRightToLeft: false, getDarkColor: undefined, }; - + const contextOptions = { + elementProcessors: defaultProcessorMap, + defaultStyles: defaultStyleMap, + }; it('no param', () => { const context = createDomToModelContext(); @@ -17,6 +22,7 @@ describe('createDomToModelContext', () => { ...editorContext, segmentFormat: {}, isInSelection: false, + ...contextOptions, }); }); @@ -34,6 +40,7 @@ describe('createDomToModelContext', () => { ...editorContext, segmentFormat: {}, isInSelection: false, + ...contextOptions, }); }); @@ -62,6 +69,7 @@ describe('createDomToModelContext', () => { endOffset: 1, isSelectionCollapsed: false, }, + ...contextOptions, }); }); @@ -86,6 +94,7 @@ describe('createDomToModelContext', () => { firstCell: { x: 1, y: 2 }, lastCell: { x: 3, y: 4 }, }, + ...contextOptions, }); }); }); diff --git a/packages/roosterjs-content-model/test/domToModel/processors/fontProcessorTest.ts b/packages/roosterjs-content-model/test/domToModel/processors/fontProcessorTest.ts new file mode 100644 index 00000000000..7de65f1dadb --- /dev/null +++ b/packages/roosterjs-content-model/test/domToModel/processors/fontProcessorTest.ts @@ -0,0 +1,113 @@ +import * as stackFormat from '../../../lib/domToModel/utils/stackFormat'; +import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; +import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; +import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; +import { fontProcessor } from '../../../lib/domToModel/processors/fontProcessor'; + +describe('fontProcessor', () => { + let context: DomToModelContext; + + beforeEach(() => { + context = createDomToModelContext(); + spyOn(stackFormat, 'stackFormat').and.callFake((context, options, callback) => callback()); + }); + + it('Empty FONT tag', () => { + const doc = createContentModelDocument(document); + const font = document.createElement('font'); + + fontProcessor(doc, font, context); + + expect(doc).toEqual({ + blockType: 'BlockGroup', + blockGroupType: 'Document', + blocks: [], + document: document, + }); + + expect(context.segmentFormat).toEqual({}); + }); + + it('FONT tag with face attributes', () => { + const doc = createContentModelDocument(document); + const font = document.createElement('font'); + font.face = 'Arial'; + + fontProcessor(doc, font, context); + + expect(context.segmentFormat).toEqual({ + fontFamily: 'Arial', + }); + }); + + it('FONT tag with color attributes', () => { + const doc = createContentModelDocument(document); + const font = document.createElement('font'); + font.color = 'red'; + + fontProcessor(doc, font, context); + + expect(context.segmentFormat).toEqual({ + textColor: 'red', + }); + }); + + it('FONT tag with size attributes 1', () => { + const doc = createContentModelDocument(document); + const font = document.createElement('font'); + font.size = '4'; + + fontProcessor(doc, font, context); + + expect(context.segmentFormat).toEqual({ + fontSize: '18px', + }); + }); + + it('FONT tag with size attributes 2', () => { + const doc = createContentModelDocument(document); + const font = document.createElement('font'); + font.size = '8'; + + fontProcessor(doc, font, context); + + expect(context.segmentFormat).toEqual({ + fontSize: '48px', + }); + }); + + it('FONT tag with all 3 attributes', () => { + const doc = createContentModelDocument(document); + const font = document.createElement('font'); + font.face = 'Arial'; + font.size = '5'; + font.color = 'red'; + + fontProcessor(doc, font, context); + + expect(context.segmentFormat).toEqual({ + fontFamily: 'Arial', + fontSize: '24px', + textColor: 'red', + }); + }); + + it('FONT tag with all 3 attributes and override by CSS', () => { + const doc = createContentModelDocument(document); + const font = document.createElement('font'); + font.face = 'Arial'; + font.size = '5'; + font.color = 'red'; + font.style.fontFamily = 'Roman'; + font.style.fontSize = '100px'; + font.style.color = 'green'; + + fontProcessor(doc, font, context); + + expect(context.segmentFormat).toEqual({ + fontFamily: 'Roman', + fontSize: '100px', + textColor: 'green', + }); + }); +}); diff --git a/packages/roosterjs-content-model/test/formatHandlers/createFormatContextTest.ts b/packages/roosterjs-content-model/test/formatHandlers/createFormatContextTest.ts index 62f3ab0e283..e504eb53c72 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/createFormatContextTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/createFormatContextTest.ts @@ -1,7 +1,14 @@ import { createDomToModelContext } from '../../lib/domToModel/context/createDomToModelContext'; +import { defaultProcessorMap } from '../../lib/domToModel/context/defaultProcessors'; +import { defaultStyleMap } from '../../lib/domToModel/context/defaultStyles'; import { SelectionRangeTypes } from 'roosterjs-editor-types'; describe('createFormatContextTest', () => { + const contextOptions = { + elementProcessors: defaultProcessorMap, + defaultStyles: defaultStyleMap, + }; + it('empty parameter', () => { const context = createDomToModelContext(); @@ -12,6 +19,7 @@ describe('createFormatContextTest', () => { getDarkColor: undefined, isInSelection: false, segmentFormat: {}, + ...contextOptions, }); }); @@ -32,6 +40,7 @@ describe('createFormatContextTest', () => { getDarkColor: getDarkColor, isInSelection: false, segmentFormat: {}, + ...contextOptions, }); }); @@ -71,6 +80,7 @@ describe('createFormatContextTest', () => { isSelectionCollapsed: false, }, segmentFormat: {}, + ...contextOptions, }); }); @@ -122,6 +132,7 @@ describe('createFormatContextTest', () => { }, }, segmentFormat: {}, + ...contextOptions, }); }); @@ -149,6 +160,7 @@ describe('createFormatContextTest', () => { getDarkColor: getDarkColor, isInSelection: false, segmentFormat: {}, + ...contextOptions, }); }); @@ -178,6 +190,7 @@ describe('createFormatContextTest', () => { getDarkColor: getDarkColor, isInSelection: false, segmentFormat: {}, + ...contextOptions, }); }); }); From 172abc30f349b27e04c7068ffda0a73de6bd0518 Mon Sep 17 00:00:00 2001 From: Trevor Shibley Date: Wed, 14 Sep 2022 14:16:18 -0700 Subject: [PATCH 05/41] fix: toggling inner list type #1258 Co-authored-by: Bryan Valverde U --- packages/roosterjs-editor-dom/lib/list/VListItem.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/roosterjs-editor-dom/lib/list/VListItem.ts b/packages/roosterjs-editor-dom/lib/list/VListItem.ts index 2b8993177f6..3b506727765 100644 --- a/packages/roosterjs-editor-dom/lib/list/VListItem.ts +++ b/packages/roosterjs-editor-dom/lib/list/VListItem.ts @@ -318,7 +318,10 @@ export default class VListItem { //apply the styles of the current parent list to the new list if (this.getDeepChildIndex(originalRoot) == stackLength) { const listStyleType = this.node.parentElement?.style.listStyleType; - if (listStyleType) { + if ( + listStyleType && + getTagOfNode(this.node.parentElement) === getTagOfNode(newList) + ) { newList.style.listStyleType = listStyleType; } } From 2facf0bc585ac8bfac7c6543e366119887fc38a9 Mon Sep 17 00:00:00 2001 From: Bryan Valverde U Date: Thu, 15 Sep 2022 16:32:20 -0600 Subject: [PATCH 06/41] Add getVisibleViewport function and hide TableSelector when is not inside of the Visible Rect (#1254) --- .../lib/editor/Editor.ts | 22 +++ .../test/coreApi/createMockEditorCore.ts | 1 + packages/roosterjs-editor-dom/lib/index.ts | 1 + .../lib/utils/getIntersectedRect.ts | 46 ++++++ .../lib/plugins/TableResize/TableResize.ts | 2 +- .../TableResize/editors/CellResizer.ts | 4 +- .../TableResize/editors/TableEditor.ts | 55 ++----- .../TableResize/editors/TableEditorFeature.ts | 12 +- .../TableResize/editors/TableInserter.ts | 7 +- .../TableResize/editors/TableResizer.ts | 4 +- .../TableResize/editors/TableSelector.ts | 25 ++- .../test/TableResize/tableSelectorTest.ts | 151 +++++------------- .../lib/interface/EditorCore.ts | 6 + .../lib/interface/EditorOptions.ts | 6 + .../lib/interface/IEditor.ts | 7 + 15 files changed, 180 insertions(+), 169 deletions(-) create mode 100644 packages/roosterjs-editor-dom/lib/utils/getIntersectedRect.ts diff --git a/packages/roosterjs-editor-core/lib/editor/Editor.ts b/packages/roosterjs-editor-core/lib/editor/Editor.ts index 9c54cb7eae4..bab483adcf0 100644 --- a/packages/roosterjs-editor-core/lib/editor/Editor.ts +++ b/packages/roosterjs-editor-core/lib/editor/Editor.ts @@ -28,6 +28,7 @@ import { PluginEventType, PositionType, QueryScope, + Rect, Region, RegionType, SelectionPath, @@ -59,6 +60,7 @@ import { arrayPush, toArray, getObjectKeys, + getIntersectedRect, } from 'roosterjs-editor-dom'; import type { CompatibleChangeSource, @@ -115,6 +117,17 @@ export default class Editor implements IEditor { trustedHTMLHandler: options.trustedHTMLHandler || ((html: string) => html), zoomScale: zoomScale, sizeTransformer: options.sizeTransformer || ((size: number) => size / zoomScale), + getVisibleViewport: + options.getVisibleViewport || + (() => { + const scrollContainer = this.getScrollContainer(); + + return getIntersectedRect( + scrollContainer == contentDiv + ? [scrollContainer] + : [scrollContainer, contentDiv] + ); + }), }; // 3. Initialize plugins @@ -798,6 +811,8 @@ export default class Editor implements IEditor { } /** + * @deprecated Use getVisibleViewport() instead. + * * Get current relative distance from top-left corner of the given element to top-left corner of editor content DIV. * @param element The element to calculate from. If the given element is not in editor, return value will be null * @param addScroll When pass true, The return value will also add scrollLeft and scrollTop if any. So the value @@ -1014,6 +1029,13 @@ export default class Editor implements IEditor { } } + /** + * Retrieves the rect of the visible viewport of the editor. + */ + getVisibleViewport(): Rect | null { + return this.getCore().getVisibleViewport(); + } + /** * @returns the current EditorCore object * @throws a standard Error if there's no core object diff --git a/packages/roosterjs-editor-core/test/coreApi/createMockEditorCore.ts b/packages/roosterjs-editor-core/test/coreApi/createMockEditorCore.ts index 0882a26a896..63d567b2f27 100644 --- a/packages/roosterjs-editor-core/test/coreApi/createMockEditorCore.ts +++ b/packages/roosterjs-editor-core/test/coreApi/createMockEditorCore.ts @@ -18,5 +18,6 @@ export default function createMockEditorCore( trustedHTMLHandler: (html: string) => html, sizeTransformer: x => x, zoomScale: 1, + getVisibleViewport: () => contentDiv.getBoundingClientRect(), }; } diff --git a/packages/roosterjs-editor-dom/lib/index.ts b/packages/roosterjs-editor-dom/lib/index.ts index ace4bfffa7b..b6d5ccf74d7 100644 --- a/packages/roosterjs-editor-dom/lib/index.ts +++ b/packages/roosterjs-editor-dom/lib/index.ts @@ -48,6 +48,7 @@ export { default as setColor } from './utils/setColor'; export { default as matchesSelector } from './utils/matchesSelector'; export { default as createElement, KnownCreateElementData } from './utils/createElement'; export { default as moveChildNodes } from './utils/moveChildNodes'; +export { default as getIntersectedRect } from './utils/getIntersectedRect'; export { default as VTable } from './table/VTable'; export { default as isWholeTableSelected } from './table/isWholeTableSelected'; diff --git a/packages/roosterjs-editor-dom/lib/utils/getIntersectedRect.ts b/packages/roosterjs-editor-dom/lib/utils/getIntersectedRect.ts new file mode 100644 index 00000000000..2c09ce14a7d --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/utils/getIntersectedRect.ts @@ -0,0 +1,46 @@ +import normalizeRect from './normalizeRect'; +import { Rect } from 'roosterjs-editor-types'; + +/** + * Get the intersected Rect of elements provided + * + * @example + * The result of the following Elements Rects would be: + { + top: Element2.top, + bottom: Element1.bottom, + left: Element2.left, + right: Element2.right + } + +-------------------------+ + | Element 1 | + | +-----------------+ | + | | Element2 | | + | | | | + | | | | + +-------------------------+ + | | + +-----------------+ + + * @param elements Elements to use. + * @param additionalRects additional rects to use + * @returns If the Rect is valid return the rect, if not, return null. + */ +export default function getIntersectedRect( + elements: HTMLElement[], + additionalRects: Rect[] = [] +): Rect | null { + const rects = elements + .map(element => normalizeRect(element.getBoundingClientRect())) + .filter(element => !!element) + .concat(additionalRects) as Rect[]; + + const result: Rect = { + top: Math.max(...rects.map(r => r.top)), + bottom: Math.min(...rects.map(r => r.bottom)), + left: Math.max(...rects.map(r => r.left)), + right: Math.min(...rects.map(r => r.right)), + }; + + return result.top < result.bottom && result.left < result.right ? result : null; +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/TableResize.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/TableResize.ts index ad307d74b68..76a4c266bf7 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/TableResize.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/TableResize.ts @@ -105,7 +105,7 @@ export default class TableResize implements EditorPlugin { this.tableEditor?.onMouseMove(x, y); }; - private setTableEditor(table: HTMLTableElement, e?: MouseEvent) { + private setTableEditor(table: HTMLTableElement | null, e?: MouseEvent) { if (this.tableEditor && !this.tableEditor.isEditing() && table != this.tableEditor.table) { this.disposeTableEditor(); } diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/CellResizer.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/CellResizer.ts index cc15a21e674..b7ec597f883 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/CellResizer.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/CellResizer.ts @@ -17,11 +17,11 @@ export default function createCellResizer( isHorizontal: boolean, onStart: () => void, onEnd: () => false, - onShowHelperElement: ( + onShowHelperElement?: ( elementData: CreateElementData, helperType: 'CellResizer' | 'TableInserter' | 'TableResizer' | 'TableSelector' ) => void -): TableEditFeature { +): TableEditFeature | null { const document = td.ownerDocument; const createElementData = { tag: 'div', diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableEditor.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableEditor.ts index c45062fdaa3..05943864c3b 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableEditor.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableEditor.ts @@ -3,20 +3,13 @@ import createTableInserter from './TableInserter'; import createTableResizer from './TableResizer'; import createTableSelector from './TableSelector'; import TableEditFeature, { disposeTableEditFeature } from './TableEditorFeature'; -import { - getComputedStyle, - normalizeRect, - Position, - safeInstanceOf, - VTable, -} from 'roosterjs-editor-dom'; +import { getComputedStyle, normalizeRect, Position, VTable } from 'roosterjs-editor-dom'; import { ChangeSource, IEditor, NodePosition, TableSelection, CreateElementData, - Rect, } from 'roosterjs-editor-types'; const INSERTER_HOVER_OFFSET = 5; @@ -52,18 +45,18 @@ const INSERTER_HOVER_OFFSET = 5; */ export default class TableEditor { // 1, 2 - Insert a column or a row - private horizontalInserter: TableEditFeature; - private verticalInserter: TableEditFeature; + private horizontalInserter: TableEditFeature | null = null; + private verticalInserter: TableEditFeature | null = null; // 3, 4 - Resize a column or a row from a cell - private horizontalResizer: TableEditFeature; - private verticalResizer: TableEditFeature; + private horizontalResizer: TableEditFeature | null = null; + private verticalResizer: TableEditFeature | null = null; // 5 - Resize whole table - private tableResizer: TableEditFeature; + private tableResizer: TableEditFeature | null; // 6 - Select whole table - private tableSelector: TableEditFeature; + private tableSelector: TableEditFeature | null; private isRTL: boolean; private start: NodePosition; @@ -78,12 +71,13 @@ export default class TableEditor { elementData: CreateElementData, helperType: 'CellResizer' | 'TableInserter' | 'TableResizer' | 'TableSelector' ) => void, - eventTarget?: EventTarget + contentDiv?: EventTarget ) { this.isRTL = getComputedStyle(table, 'direction') == 'rtl'; + const zoomScale = editor.getZoomScale(); this.tableResizer = createTableResizer( table, - editor.getZoomScale(), + zoomScale, this.isRTL, this.onStartTableResize, this.onFinishEditing, @@ -91,10 +85,11 @@ export default class TableEditor { ); this.tableSelector = createTableSelector( table, - editor.getZoomScale(), + zoomScale, + editor, this.onSelect, this.onShowHelperElement, - this.getShouldShowTableSelectorHandler(this.editor.getScrollContainer(), eventTarget) + contentDiv ); this.isCurrentlyEditing = false; } @@ -220,7 +215,7 @@ export default class TableEditor { this.editor, td, this.isRTL, - isHorizontal, + !!isHorizontal, this.onInserted, this.onShowHelperElement ); @@ -338,26 +333,4 @@ export default class TableEditor { } } }; - - private getShouldShowTableSelectorHandler( - scrollContainer: HTMLElement, - eventTarget?: EventTarget - ): (rect: Rect) => boolean { - if (eventTarget && safeInstanceOf(eventTarget, 'HTMLElement') && scrollContainer) { - const scrollContainerRect = normalizeRect(scrollContainer.getBoundingClientRect()); - const containerRect = normalizeRect(eventTarget.getBoundingClientRect()); - - if (scrollContainerRect && containerRect) { - const scrollContainerVisibleTop = - scrollContainer.scrollTop - scrollContainerRect.top; - - return (rect: Rect) => - containerRect.top <= rect.top && - scrollContainerVisibleTop <= rect.top && - scrollContainerRect.top <= rect.top; - } - } - - return () => true; - } } diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableEditorFeature.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableEditorFeature.ts index 3ca3b0153d1..cb65000ed17 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableEditorFeature.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableEditorFeature.ts @@ -12,9 +12,11 @@ export default interface TableEditFeature { /** * @internal */ -export function disposeTableEditFeature(resizer: TableEditFeature) { - resizer.div?.parentNode?.removeChild(resizer.div); - resizer.div = null; - resizer.featureHandler?.dispose(); - resizer.featureHandler = null; +export function disposeTableEditFeature(resizer: TableEditFeature | null) { + if (resizer) { + resizer.div?.parentNode?.removeChild(resizer.div); + resizer.div = null; + resizer.featureHandler?.dispose(); + resizer.featureHandler = null; + } } diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableInserter.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableInserter.ts index 86330917bb3..f4c01846fa1 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableInserter.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableInserter.ts @@ -17,12 +17,13 @@ export default function createTableInserter( isRTL: boolean, isHorizontal: boolean, onInsert: (table: HTMLTableElement) => void, - onShowHelperElement: ( + onShowHelperElement?: ( elementData: CreateElementData, helperType: 'CellResizer' | 'TableInserter' | 'TableResizer' | 'TableSelector' ) => void -): TableEditFeature { +): TableEditFeature | null { const table = editor.getElementAtCursor('table', td); + const tdRect = normalizeRect(td.getBoundingClientRect()); const tableRect = table ? normalizeRect(table.getBoundingClientRect()) : null; @@ -62,6 +63,8 @@ export default function createTableInserter( return { div, featureHandler: handler, node: td }; } + + return null; } class TableInsertHandler implements Disposable { diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableResizer.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableResizer.ts index 6f6ca22cbf2..6b47b4d2780 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableResizer.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableResizer.ts @@ -16,11 +16,11 @@ export default function createTableResizer( isRTL: boolean, onStart: () => void, onDragEnd: () => false, - onShowHelperElement: ( + onShowHelperElement?: ( elementData: CreateElementData, helperType: 'CellResizer' | 'TableInserter' | 'TableResizer' | 'TableSelector' ) => void -): TableEditFeature { +): TableEditFeature | null { const document = table.ownerDocument; const createElementData = { tag: 'div', diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableSelector.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableSelector.ts index 0f7d8f2e2f9..fc3398b64eb 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableSelector.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableSelector.ts @@ -1,7 +1,7 @@ import DragAndDropHelper from '../../../pluginUtils/DragAndDropHelper'; import TableEditorFeature from './TableEditorFeature'; -import { createElement, normalizeRect } from 'roosterjs-editor-dom'; -import { CreateElementData, Rect } from 'roosterjs-editor-types'; +import { createElement, normalizeRect, safeInstanceOf } from 'roosterjs-editor-dom'; +import { CreateElementData, IEditor, Rect } from 'roosterjs-editor-types'; const TABLE_SELECTOR_LENGTH = 12; const TABLE_SELECTOR_ID = '_Table_Selector'; @@ -12,16 +12,18 @@ const TABLE_SELECTOR_ID = '_Table_Selector'; export default function createTableSelector( table: HTMLTableElement, zoomScale: number, + editor: IEditor, onFinishDragging: (table: HTMLTableElement) => void, onShowHelperElement?: ( elementData: CreateElementData, helperType: 'CellResizer' | 'TableInserter' | 'TableResizer' | 'TableSelector' ) => void, - shouldShow?: (rect: Rect) => boolean -): TableEditorFeature { + contentDiv?: EventTarget +): TableEditorFeature | null { const rect = normalizeRect(table.getBoundingClientRect()); - if (rect && shouldShow && !shouldShow(rect)) { - return { div: null, featureHandler: null, node: table }; + + if (!isTableTopVisible(editor, rect, contentDiv)) { + return null; } const document = table.ownerDocument; @@ -84,3 +86,14 @@ function setSelectorDivPosition(context: DragAndDropContext, trigger: HTMLElemen trigger.style.left = `${rect.left - TABLE_SELECTOR_LENGTH - 2}px`; } } + +function isTableTopVisible(editor: IEditor, rect: Rect | null, contentDiv?: EventTarget): boolean { + const visibleViewport = editor.getVisibleViewport(); + if (contentDiv && safeInstanceOf(contentDiv, 'HTMLElement') && visibleViewport && rect) { + const containerRect = normalizeRect(contentDiv.getBoundingClientRect()); + + return containerRect.top <= rect.top && visibleViewport.top <= rect.top; + } + + return true; +} diff --git a/packages/roosterjs-editor-plugins/test/TableResize/tableSelectorTest.ts b/packages/roosterjs-editor-plugins/test/TableResize/tableSelectorTest.ts index 9b0a8e1dff9..63be04f6e3e 100644 --- a/packages/roosterjs-editor-plugins/test/TableResize/tableSelectorTest.ts +++ b/packages/roosterjs-editor-plugins/test/TableResize/tableSelectorTest.ts @@ -1,16 +1,14 @@ +import createTableSelector from '../../lib/plugins/TableResize/editors/TableSelector'; import TableEditor from '../../lib/plugins/TableResize/editors/TableEditor'; import { Editor } from 'roosterjs-editor-core'; import { EditorOptions, IEditor, SelectionRangeTypes } from 'roosterjs-editor-types'; import { TableResize } from '../../lib/TableResize'; export * from 'roosterjs-editor-dom/test/DomTestHelper'; -const TABLE_SELECTOR_ID = '_Table_Selector'; - describe('Table Selector Tests', () => { let editor: IEditor; let id = 'tableSelectionContainerId'; let targetId = 'tableSelectionTestId'; - let targetId2 = 'tableSelectionTestId2'; let tableResize: TableResize; let node: HTMLDivElement; @@ -46,45 +44,12 @@ describe('Table Selector Tests', () => { }); it('Display component on mouse move inside table', () => { - editor.setContent( - `
aw
` - ); - const target = document.getElementById(targetId); - const target2 = document.getElementById(targetId2); - editor.focus(); - editor.select(target); - - simulateMouseEvent('mousemove', target2); - - editor.runAsync(editor => { - const tableSelector = editor.getDocument().getElementById(TABLE_SELECTOR_ID); - if (tableSelector) { - expect(tableSelector).toBeDefined(); - } - }); + runTest(0, true); }); it('Do not display component, top of table is no visible in the container.', () => { //Arrange - editor.setContent( - `
aw
aw
` - ); - node.style.height = '100px'; - node.style.overflowX = 'auto'; - node.scrollTop = 15; - const target = document.getElementById(targetId); - const target2 = document.getElementById(targetId2); - editor.focus(); - editor.select(target); - - //Act - simulateMouseEvent('mousemove', target2); - - //Assert - editor.runAsync(editor => { - const tableSelector = editor.getDocument().getElementById(TABLE_SELECTOR_ID); - expect(tableSelector).toBeNull(); - }); + runTest(15, false); }); it('Do not display component, Top of table is no visible in the scroll container.', () => { @@ -92,25 +57,9 @@ describe('Table Selector Tests', () => { const scrollContainer = document.createElement('div'); document.body.insertBefore(scrollContainer, document.body.childNodes[0]); scrollContainer.append(node); - spyOn(editor, 'getScrollContainer').and.returnValue(scrollContainer); - editor.setContent( - `
aw
aw
` - ); - scrollContainer.style.height = '100px'; - scrollContainer.style.overflowX = 'auto'; - scrollContainer.scrollTop = 15; - const target = document.getElementById(targetId); - const target2 = document.getElementById(targetId2); - editor.focus(); - editor.select(target); - //Act - simulateMouseEvent('mousemove', target2); - - //Assert - const tableSelector = editor.getDocument().getElementById(TABLE_SELECTOR_ID); - expect(tableSelector).toBeNull(); + runTest(15, false); }); it('Display component, Top of table is visible in the scroll container scrolled down.', () => { @@ -119,50 +68,9 @@ describe('Table Selector Tests', () => { scrollContainer.innerHTML = '
'; document.body.insertBefore(scrollContainer, document.body.childNodes[0]); scrollContainer.append(node); - spyOn(editor, 'getScrollContainer').and.returnValue(scrollContainer); - editor.setContent( - `
aw
aw
` - ); - scrollContainer.style.height = '100px'; - scrollContainer.style.overflowX = 'auto'; - scrollContainer.scrollTop = 50; - const target = document.getElementById(targetId); - const target2 = document.getElementById(targetId2); - editor.focus(); - editor.select(target); - //Act - simulateMouseEvent('mousemove', target2); - - //Assert - editor.runAsync(editor => { - const tableSelector = editor.getDocument().getElementById(TABLE_SELECTOR_ID); - expect(tableSelector).toBeDefined(); - }); - }); - - it('Scroll container equals null, display component', () => { - //Arrange - spyOn(editor, 'getScrollContainer').and.returnValue(null); - editor.setContent( - `
aw
aw
` - ); - node.style.height = '100px'; - node.style.overflowX = 'auto'; - node.scrollTop = 15; - const target = document.getElementById(targetId); - const target2 = document.getElementById(targetId2); - editor.focus(); - editor.select(target); - //Act - simulateMouseEvent('mousemove', target2); - - //Assert - editor.runAsync(editor => { - const tableSelector = editor.getDocument().getElementById(TABLE_SELECTOR_ID); - expect(tableSelector).toBeDefined(); - }); + runTest(0, true); }); it('On click event', () => { @@ -171,7 +79,12 @@ describe('Table Selector Tests', () => { ); const table = document.getElementById(targetId) as HTMLTableElement; - const tableEditor = new TableEditor(editor, table, () => {}); + const tableEditor = new TableEditor( + editor, + table, + () => {}, + () => true + ); tableEditor.onSelect(table); @@ -191,16 +104,34 @@ describe('Table Selector Tests', () => { expect(selection.ranges.length).toBe(8); } }); -}); -function simulateMouseEvent(type: string, target: HTMLElement, point?: { x: number; y: number }) { - const rect = target.getBoundingClientRect(); - var event = new MouseEvent(type, { - view: window, - bubbles: true, - cancelable: true, - clientX: rect.left + (point != undefined ? point?.x : 0), - clientY: rect.top + (point != undefined ? point?.y : 0), - }); - target.dispatchEvent(event); -} + function runTest(scrollTop: number, isNotNull: boolean | null) { + //Arrange + editor.setContent( + '
aw
aw
' + ); + + node.style.height = '100px'; + node.style.overflowX = 'auto'; + node.scrollTop = scrollTop; + const target = document.getElementById('table1'); + editor.focus(); + + //Act + const result = createTableSelector( + target as HTMLTableElement, + 1, + editor, + () => {}, + () => {}, + node + ); + + //Assert + if (!isNotNull) { + expect(result).toBeNull(); + } else { + expect(result).toBeDefined(); + } + } +}); diff --git a/packages/roosterjs-editor-types/lib/interface/EditorCore.ts b/packages/roosterjs-editor-types/lib/interface/EditorCore.ts index 7eb7bc3bb97..03fecc26dec 100644 --- a/packages/roosterjs-editor-types/lib/interface/EditorCore.ts +++ b/packages/roosterjs-editor-types/lib/interface/EditorCore.ts @@ -2,6 +2,7 @@ import ClipboardData from './ClipboardData'; import ContentChangedData from './ContentChangedData'; import EditorPlugin from './EditorPlugin'; import NodePosition from './NodePosition'; +import Rect from './Rect'; import TableSelection from './TableSelection'; import { ChangeSource } from '../enum/ChangeSource'; import { ColorTransformDirection } from '../enum/ColorTransformDirection'; @@ -62,6 +63,11 @@ export default interface EditorCore extends PluginState { * @deprecated Use zoomScale instead */ sizeTransformer: SizeTransformer; + + /** + * Retrieves the Visible Viewport of the editor. + */ + getVisibleViewport: () => Rect | null; } /** diff --git a/packages/roosterjs-editor-types/lib/interface/EditorOptions.ts b/packages/roosterjs-editor-types/lib/interface/EditorOptions.ts index 3ce882b2a87..ff9bec70d6c 100644 --- a/packages/roosterjs-editor-types/lib/interface/EditorOptions.ts +++ b/packages/roosterjs-editor-types/lib/interface/EditorOptions.ts @@ -1,6 +1,7 @@ import CorePlugins from './CorePlugins'; import DefaultFormat from './DefaultFormat'; import EditorPlugin from './EditorPlugin'; +import Rect from './Rect'; import Snapshot from './Snapshot'; import UndoSnapshotsService from './UndoSnapshotsService'; import { CoreApiMap } from './EditorCore'; @@ -127,4 +128,9 @@ export default interface EditorOptions { * @deprecated Use zoomScale instead */ sizeTransformer?: SizeTransformer; + + /** + * Retrieves the visible viewport of the Editor. The default viewport is the Rect of the scrollContainer. + */ + getVisibleViewport?: () => Rect | null; } diff --git a/packages/roosterjs-editor-types/lib/interface/IEditor.ts b/packages/roosterjs-editor-types/lib/interface/IEditor.ts index cc2433cc6e4..f8515239f8c 100644 --- a/packages/roosterjs-editor-types/lib/interface/IEditor.ts +++ b/packages/roosterjs-editor-types/lib/interface/IEditor.ts @@ -5,6 +5,7 @@ import DefaultFormat from './DefaultFormat'; import IContentTraverser from './IContentTraverser'; import IPositionContentSearcher from './IPositionContentSearcher'; import NodePosition from './NodePosition'; +import Rect from './Rect'; import Region from './Region'; import SelectionPath from './SelectionPath'; import TableSelection from './TableSelection'; @@ -520,6 +521,8 @@ export default interface IEditor { getEditorDomAttribute(name: string): string | null; /** + * @deprecated Use getVisibleViewport() instead + * * Get current relative distance from top-left corner of the given element to top-left corner of editor content DIV. * @param element The element to calculate from. If the given element is not in editor, return value will be null * @param addScroll When pass true, The return value will also add scrollLeft and scrollTop if any. So the value @@ -629,6 +632,10 @@ export default interface IEditor { */ getSizeTransformer(): SizeTransformer; + /** + * Retrieves the rect of the visible viewport of the editor. + */ + getVisibleViewport(): Rect | null; //#endregion } From f415f4769f60cc3de59ce813db02213d8f825485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Thu, 15 Sep 2022 19:51:45 -0300 Subject: [PATCH 07/41] select image api --- .../sidePane/apiPlayground/apiEntries.ts | 5 + .../getSelection/getSelectionPane.scss | 23 ++ .../getSelection/getSelectionPane.tsx | 245 ++++++++++++++++++ .../lib/coreApi/coreApiMap.ts | 2 + .../lib/coreApi/getSelectionRangeEx.ts | 26 +- .../lib/coreApi/selectImage.ts | 36 +++ .../lib/coreApi/switchShadowEdit.ts | 44 +++- .../lib/corePlugins/DOMEventPlugin.ts | 6 + .../lib/corePlugins/LifecyclePlugin.ts | 1 + .../lib/editor/Editor.ts | 40 ++- .../test/coreApi/focusTest.ts | 1 + .../test/coreApi/getSelectionRangeExTest.ts | 1 + .../test/coreApi/getSelectionRangeTest.ts | 1 + .../test/corePlugins/domEventPluginTest.ts | 2 + .../test/corePlugins/lifecyclePluginTest.ts | 2 + .../test/editor/newEditorTest.ts | 2 + .../lib/plugins/ImageEdit/ImageEdit.ts | 16 ++ .../lib/plugins/Picker/PickerPlugin.ts | 4 +- .../test/Picker/pickerPluginTest.ts | 107 -------- .../corePluginState/DOMEventPluginState.ts | 7 +- .../corePluginState/LifecyclePluginState.ts | 5 + .../lib/enum/SelectionRangeTypes.ts | 4 + .../lib/interface/EditorCore.ts | 25 +- .../lib/interface/SelectionRangeEx.ts | 19 +- .../lib/interface/index.ts | 2 + 25 files changed, 503 insertions(+), 123 deletions(-) create mode 100644 demo/scripts/controls/sidePane/apiPlayground/getSelection/getSelectionPane.scss create mode 100644 demo/scripts/controls/sidePane/apiPlayground/getSelection/getSelectionPane.tsx create mode 100644 packages/roosterjs-editor-core/lib/coreApi/selectImage.ts delete mode 100644 packages/roosterjs-editor-plugins/test/Picker/pickerPluginTest.ts diff --git a/demo/scripts/controls/sidePane/apiPlayground/apiEntries.ts b/demo/scripts/controls/sidePane/apiPlayground/apiEntries.ts index 5c2b324f621..5197cf87145 100644 --- a/demo/scripts/controls/sidePane/apiPlayground/apiEntries.ts +++ b/demo/scripts/controls/sidePane/apiPlayground/apiEntries.ts @@ -3,6 +3,7 @@ import ApiPaneProps, { ApiPlaygroundComponent } from './ApiPaneProps'; import BlockElementsPane from './blockElements/BlockElementsPane'; import GetDarkColorPane from './darkColor/GetDarkColorPane'; import GetSelectedRegionsPane from './region/GetSelectedRegionsPane'; +import GetSelectionPane from './getSelection/getSelectionPane'; import InsertContentPane from './insertContent/InsertContentPane'; import InsertEntityPane from './insertEntity/InsertEntityPane'; import MatchLinkPane from './matchLink/MatchLinkPane'; @@ -59,6 +60,10 @@ const apiEntries: { [key: string]: ApiEntry } = { name: 'getDarkColor', component: GetDarkColorPane, }, + getSelection: { + name: 'getSelection', + component: GetSelectionPane, + }, more: { name: 'Coming soon...', }, diff --git a/demo/scripts/controls/sidePane/apiPlayground/getSelection/getSelectionPane.scss b/demo/scripts/controls/sidePane/apiPlayground/getSelection/getSelectionPane.scss new file mode 100644 index 00000000000..76b8d84e7de --- /dev/null +++ b/demo/scripts/controls/sidePane/apiPlayground/getSelection/getSelectionPane.scss @@ -0,0 +1,23 @@ +.input { + margin-top: 2px; + margin-bottom: 2px; + line-height: 2px; +} + +.coordinates { + width: 30px; + margin-left: 4px; +} + +.button { + margin-top: 2px; + margin-bottom: 2px; +} + +.title { + font-weight: bold; +} + +.containerInfo { + line-height: 20px; +} diff --git a/demo/scripts/controls/sidePane/apiPlayground/getSelection/getSelectionPane.tsx b/demo/scripts/controls/sidePane/apiPlayground/getSelection/getSelectionPane.tsx new file mode 100644 index 00000000000..6db979cfef0 --- /dev/null +++ b/demo/scripts/controls/sidePane/apiPlayground/getSelection/getSelectionPane.tsx @@ -0,0 +1,245 @@ +import * as React from 'react'; +import ApiPaneProps from '../ApiPaneProps'; +import { + PluginEvent, + PluginEventType, + SelectionRangeEx, + SelectionRangeTypes, + TableSelection, +} from 'roosterjs-editor-types'; + +interface SelectionPaneState { + selection: SelectionRangeEx; + selectionMessage: string; + isImageSelectionOption: boolean; + manualSelect: boolean; +} + +const styles = require('./getSelectionPane.scss'); + +export default class GetSelectionPane extends React.Component { + private selectInfo = React.createRef(); + private editor = this.props.getEditor(); + private firstCellX = React.createRef(); + private firstCellY = React.createRef(); + private lastCellX = React.createRef(); + private lastCellY = React.createRef(); + private selectionType: Record = { + [SelectionRangeTypes.Normal]: 'Normal', + [SelectionRangeTypes.TableSelection]: 'Table Selection', + [SelectionRangeTypes.ImageSelection]: 'Image Selection', + }; + + constructor(props: ApiPaneProps) { + super(props); + this.state = { + selection: null, + selectionMessage: '', + isImageSelectionOption: true, + manualSelect: false, + }; + } + + onPluginEvent(e: PluginEvent) { + if (e.eventType == PluginEventType.MouseUp && !this.state.manualSelect) { + this.getSelection(); + } + } + + private getSelection = () => { + this.setState({ + selection: this.editor.getSelectionRangeEx(), + }); + }; + + private selectElement = () => { + const queryInfo = this.selectInfo.current.value; + if (queryInfo) { + if (this.state.isImageSelectionOption) { + const elementToSelect = this.editor + .getDocument() + .querySelector(`img[id$="${queryInfo}"]`); + const select = elementToSelect ? this.editor.select(elementToSelect) : null; + this.setState({ + selection: select ? this.editor.getSelectionRangeEx() : null, + selectionMessage: select ? 'Image Found' : 'Image not found', + }); + } else { + const elementToSelect = this.editor + .getDocument() + .querySelector(`table[id$="${queryInfo}"]`) as HTMLTableElement; + const coordinates = this.getCoordinates(); + const select = + elementToSelect && coordinates + ? this.editor.select(elementToSelect, coordinates) + : null; + this.setState({ + selection: select ? this.editor.getSelectionRangeEx() : null, + selectionMessage: select ? 'Table found' : 'Table not found', + }); + } + } + }; + + private getCoordinates = (): TableSelection => { + if ( + this.firstCellX.current.value && + this.firstCellY.current.value && + this.lastCellX.current.value && + this.lastCellY.current.value + ) { + const coordinates: TableSelection = { + firstCell: { + x: parseInt(this.firstCellX.current.value), + y: parseInt(this.firstCellY.current.value), + }, + lastCell: { + x: parseInt(this.lastCellX.current.value), + y: parseInt(this.lastCellY.current.value), + }, + }; + return coordinates; + } + return null; + }; + + private createSelectionInfo = () => { + return ( + <> +
+ Selection Information +
Selection type: {this.selectionType[this.state.selection.type]}
+
Are collapsed: {`${this.state.selection.areAllCollapsed}`}
+ {this.state.selection.type === SelectionRangeTypes.TableSelection && ( + <> +
Coordinates
+
+ First cell: + X: {this.state.selection.coordinates.firstCell.x} + Y: {this.state.selection.coordinates.firstCell.y} +
+
+ Last cell: + X: {this.state.selection.coordinates.lastCell.x} + Y: {this.state.selection.coordinates.lastCell.y} +
+ + )} + {this.state.selection.type === SelectionRangeTypes.ImageSelection && ( + <> +
Image Id: {this.state.selection.imageId}
+ + )} +
+ + ); + }; + + private selectionOption = (label: string, checked: boolean, onChange: () => void) => { + return ( + <> +
+ +
+ + ); + }; + + private changeSelectionOption = () => { + this.setState({ + isImageSelectionOption: !this.state.isImageSelectionOption, + }); + }; + + private createCoordinatesInput = ( + label: string, + coordinateRef: React.RefObject + ) => { + return ( + <> +
+ +
+ + ); + }; + + private showManualSelection = () => { + this.setState({ + manualSelect: !this.state.manualSelect, + }); + }; + + render() { + return ( + <> + {!this.state.manualSelect && ( + + Click on the screen to get selection information + + )} + {this.state.selection && {this.createSelectionInfo()}} + {this.state.manualSelect && ( +
+
+ Select element type: + {this.selectionOption( + 'Image', + this.state.isImageSelectionOption, + this.changeSelectionOption + )} + {this.selectionOption( + 'Table', + !this.state.isImageSelectionOption, + this.changeSelectionOption + )} + + {!this.state.isImageSelectionOption && ( +
+
Coordinates
+ {this.createCoordinatesInput('First cell X', this.firstCellX)} + {this.createCoordinatesInput('First cell Y', this.firstCellY)} + {this.createCoordinatesInput('Last cell X', this.lastCellX)} + {this.createCoordinatesInput('Last cell X', this.lastCellY)} +
+ )} +
+
{this.state.selectionMessage}
+
+ {this.selectInfo && ( + + )} +
+
+ )} + + + + ); + } +} diff --git a/packages/roosterjs-editor-core/lib/coreApi/coreApiMap.ts b/packages/roosterjs-editor-core/lib/coreApi/coreApiMap.ts index 78ec7429e7e..dd754342636 100644 --- a/packages/roosterjs-editor-core/lib/coreApi/coreApiMap.ts +++ b/packages/roosterjs-editor-core/lib/coreApi/coreApiMap.ts @@ -12,6 +12,7 @@ import { getStyleBasedFormatState } from './getStyleBasedFormatState'; import { hasFocus } from './hasFocus'; import { insertNode } from './insertNode'; import { restoreUndoSnapshot } from './restoreUndoSnapshot'; +import { selectImage } from './selectImage'; import { selectRange } from './selectRange'; import { selectTable } from './selectTable'; import { setContent } from './setContent'; @@ -42,4 +43,5 @@ export const coreApiMap: CoreApiMap = { transformColor, triggerEvent, selectTable, + selectImage, }; diff --git a/packages/roosterjs-editor-core/lib/coreApi/getSelectionRangeEx.ts b/packages/roosterjs-editor-core/lib/coreApi/getSelectionRangeEx.ts index 53826eaea0e..899c0272bcc 100644 --- a/packages/roosterjs-editor-core/lib/coreApi/getSelectionRangeEx.ts +++ b/packages/roosterjs-editor-core/lib/coreApi/getSelectionRangeEx.ts @@ -15,7 +15,11 @@ import { export const getSelectionRangeEx: GetSelectionRangeEx = (core: EditorCore) => { let result: SelectionRangeEx | null = null; if (core.lifecycle.shadowEditFragment) { - const { shadowEditTableSelectionPath, shadowEditSelectionPath } = core.lifecycle; + const { + shadowEditTableSelectionPath, + shadowEditSelectionPath, + shadowEditImageSelectionPath, + } = core.lifecycle; if ((shadowEditTableSelectionPath?.length || 0) > 0) { const ranges = core.lifecycle.shadowEditTableSelectionPath!.map(path => @@ -33,6 +37,21 @@ export const getSelectionRangeEx: GetSelectionRangeEx = (core: EditorCore) => { ) as HTMLTableElement, coordinates: undefined, }; + } else if ((shadowEditImageSelectionPath?.length || 0) > 0) { + const ranges = core.lifecycle.shadowEditImageSelectionPath!.map(path => + createRange(core.contentDiv, path.start, path.end) + ); + return { + type: SelectionRangeTypes.ImageSelection, + ranges, + areAllCollapsed: checkAllCollapsed(ranges), + image: findClosestElementAncestor( + ranges[0].startContainer, + core.contentDiv, + 'image' + ) as HTMLImageElement, + imageId: undefined, + }; } else { const shadowRange = shadowEditSelectionPath && @@ -50,6 +69,10 @@ export const getSelectionRangeEx: GetSelectionRangeEx = (core: EditorCore) => { return core.domEvent.tableSelectionRange; } + if (core.domEvent.imageSelectionRange) { + return core.domEvent.imageSelectionRange; + } + let selection = core.contentDiv.ownerDocument.defaultView?.getSelection(); if (!result && selection && selection.rangeCount > 0) { let range = selection.getRangeAt(0); @@ -61,6 +84,7 @@ export const getSelectionRangeEx: GetSelectionRangeEx = (core: EditorCore) => { return ( core.domEvent.tableSelectionRange ?? + core.domEvent.imageSelectionRange ?? createNormalSelectionEx( core.domEvent.selectionRange ? [core.domEvent.selectionRange] : [] ) diff --git a/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts b/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts new file mode 100644 index 00000000000..79302d85571 --- /dev/null +++ b/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts @@ -0,0 +1,36 @@ +import { createRange } from 'roosterjs-editor-dom'; +import { EditorCore, SelectImage, SelectionRangeTypes } from 'roosterjs-editor-types'; + +/** + * @internal + * Select a image and save data of the selected range + * @param core The EditorCore object + * @param table image to select + * @param imageId + * @returns true if successful + */ +export const selectImage: SelectImage = ( + core: EditorCore, + image: HTMLImageElement | null, + wrapper?: HTMLSpanElement +) => { + const selectedImage = image + ? image + : wrapper + ? (document.querySelector(`img[id$="${wrapper.id}"]`) as HTMLImageElement) + : null; + if (selectedImage) { + const range = wrapper ? createRange(wrapper) : createRange(selectedImage); + core.api.selectRange(core, range); + + return { + type: SelectionRangeTypes.ImageSelection, + ranges: [range], + imageId: selectedImage.id, + image: selectedImage, + areAllCollapsed: range.collapsed, + }; + } + + return null; +}; diff --git a/packages/roosterjs-editor-core/lib/coreApi/switchShadowEdit.ts b/packages/roosterjs-editor-core/lib/coreApi/switchShadowEdit.ts index fbb1e57d1cb..b5fbcc38d1e 100644 --- a/packages/roosterjs-editor-core/lib/coreApi/switchShadowEdit.ts +++ b/packages/roosterjs-editor-core/lib/coreApi/switchShadowEdit.ts @@ -2,6 +2,7 @@ import { createRange, getSelectionPath, moveChildNodes } from 'roosterjs-editor- import { EditorCore, PluginEventType, + SelectionRangeEx, SelectionRangeTypes, SwitchShadowEdit, } from 'roosterjs-editor-types'; @@ -11,22 +12,42 @@ import { */ export const switchShadowEdit: SwitchShadowEdit = (core: EditorCore, isOn: boolean): void => { const { lifecycle, contentDiv } = core; - let { shadowEditFragment, shadowEditSelectionPath, shadowEditTableSelectionPath } = lifecycle; + let { + shadowEditFragment, + shadowEditSelectionPath, + shadowEditTableSelectionPath, + shadowEditImageSelectionPath, + } = lifecycle; const wasInShadowEdit = !!shadowEditFragment; + const getShadowEditSelectionPath = ( + selectionType: SelectionRangeTypes, + shadowEditSelection?: SelectionRangeEx + ) => { + return ( + (shadowEditSelection?.type == selectionType && + shadowEditSelection.ranges + .map(range => getSelectionPath(contentDiv, range)) + .map(w => w!!)) || + null + ); + }; + if (isOn) { if (!wasInShadowEdit) { const selection = core.api.getSelectionRangeEx(core); const range = core.api.getSelectionRange(core, true /*tryGetFromCache*/); shadowEditSelectionPath = range && getSelectionPath(contentDiv, range); - shadowEditTableSelectionPath = - (selection?.type == SelectionRangeTypes.TableSelection && - selection.ranges - .map(range => getSelectionPath(contentDiv, range)) - .map(w => w!!)) || - null; + shadowEditTableSelectionPath = getShadowEditSelectionPath( + SelectionRangeTypes.TableSelection, + selection + ); shadowEditFragment = core.contentDiv.ownerDocument.createDocumentFragment(); + shadowEditImageSelectionPath = getShadowEditSelectionPath( + SelectionRangeTypes.ImageSelection, + selection + ); moveChildNodes(shadowEditFragment, contentDiv); shadowEditFragment.normalize(); @@ -43,6 +64,7 @@ export const switchShadowEdit: SwitchShadowEdit = (core: EditorCore, isOn: boole lifecycle.shadowEditFragment = shadowEditFragment; lifecycle.shadowEditSelectionPath = shadowEditSelectionPath; lifecycle.shadowEditTableSelectionPath = shadowEditTableSelectionPath; + lifecycle.shadowEditImageSelectionPath = shadowEditImageSelectionPath; } moveChildNodes(contentDiv); @@ -79,6 +101,14 @@ export const switchShadowEdit: SwitchShadowEdit = (core: EditorCore, isOn: boole ); } + if (core.domEvent.imageSelectionRange) { + const { image } = core.domEvent.imageSelectionRange; + const imageElement = core.contentDiv.querySelector('#' + image.id); + if (imageElement) { + core.domEvent.imageSelectionRange = core.api.selectImage(core, image); + } + } + if (core.domEvent.tableSelectionRange) { const { table, coordinates } = core.domEvent.tableSelectionRange; const tableId = table.id; diff --git a/packages/roosterjs-editor-core/lib/corePlugins/DOMEventPlugin.ts b/packages/roosterjs-editor-core/lib/corePlugins/DOMEventPlugin.ts index 1ff74ac1981..eb529e9fa89 100644 --- a/packages/roosterjs-editor-core/lib/corePlugins/DOMEventPlugin.ts +++ b/packages/roosterjs-editor-core/lib/corePlugins/DOMEventPlugin.ts @@ -43,6 +43,7 @@ export default class DOMEventPlugin implements PluginWithState>(isContextMenuProvider) || [], tableSelectionRange: null, + imageSelectionRange: null, }; } @@ -152,6 +153,11 @@ export default class DOMEventPlugin implements PluginWithState { diff --git a/packages/roosterjs-editor-core/lib/corePlugins/LifecyclePlugin.ts b/packages/roosterjs-editor-core/lib/corePlugins/LifecyclePlugin.ts index 30bc01185c9..8f4ab12a95e 100644 --- a/packages/roosterjs-editor-core/lib/corePlugins/LifecyclePlugin.ts +++ b/packages/roosterjs-editor-core/lib/corePlugins/LifecyclePlugin.ts @@ -99,6 +99,7 @@ export default class LifecyclePlugin implements PluginWithStatearg2); core.domEvent.tableSelectionRange = selection; - return !!selection; } else { core.api.selectTable(core, null); core.domEvent.tableSelectionRange = null; } + const imageSelection = this.getImageSelection(core, arg1); + if (imageSelection) { + core.domEvent.imageSelectionRange = imageSelection; + return !!imageSelection; + } else { + core.domEvent.imageSelectionRange = null; + } + let range = !arg1 ? null : safeInstanceOf(arg1, 'Range') @@ -470,6 +485,27 @@ export default class Editor implements IEditor { return !!range && this.contains(range) && core.api.selectRange(core, range); } + private getImageSelection( + core: EditorCore, + arg: Range | NodePosition | Node | SelectionPath | HTMLTableElement | null + ) { + if (safeInstanceOf(arg, 'HTMLImageElement')) { + const selection = core.api.selectImage(core, arg); + return selection; + } + if (arg && safeInstanceOf(arg, 'HTMLSpanElement')) { + const argElement = arg; + const argClass = argElement.className; + if (argClass.indexOf('IMAGE_EDIT_WRAPPER') >= 0) { + const selection = core.api.selectImage(core, null /** image **/, argElement); + return selection; + } + } else { + core.api.selectImage(core, null); + return null; + } + } + /** * Get current focused position. Return null if editor doesn't have focus at this time. */ diff --git a/packages/roosterjs-editor-core/test/coreApi/focusTest.ts b/packages/roosterjs-editor-core/test/coreApi/focusTest.ts index ab87e4b8986..cd4c96e3cdc 100644 --- a/packages/roosterjs-editor-core/test/coreApi/focusTest.ts +++ b/packages/roosterjs-editor-core/test/coreApi/focusTest.ts @@ -23,6 +23,7 @@ describe('focus', () => { stopPrintableKeyboardEventPropagation: false, contextMenuProviders: [], tableSelectionRange: null, + imageSelectionRange: null, }; focus(core); diff --git a/packages/roosterjs-editor-core/test/coreApi/getSelectionRangeExTest.ts b/packages/roosterjs-editor-core/test/coreApi/getSelectionRangeExTest.ts index 3e622e0113f..1e909c06c1e 100644 --- a/packages/roosterjs-editor-core/test/coreApi/getSelectionRangeExTest.ts +++ b/packages/roosterjs-editor-core/test/coreApi/getSelectionRangeExTest.ts @@ -52,6 +52,7 @@ describe('getSelectionRangeEx', () => { stopPrintableKeyboardEventPropagation: false, contextMenuProviders: [], tableSelectionRange: null, + imageSelectionRange: null, }; const input = document.createElement('input'); document.body.appendChild(input); diff --git a/packages/roosterjs-editor-core/test/coreApi/getSelectionRangeTest.ts b/packages/roosterjs-editor-core/test/coreApi/getSelectionRangeTest.ts index 1c56c52a6b9..949027f45ff 100644 --- a/packages/roosterjs-editor-core/test/coreApi/getSelectionRangeTest.ts +++ b/packages/roosterjs-editor-core/test/coreApi/getSelectionRangeTest.ts @@ -48,6 +48,7 @@ describe('getSelectionRange', () => { stopPrintableKeyboardEventPropagation: false, contextMenuProviders: [], tableSelectionRange: null, + imageSelectionRange: null, }; const input = document.createElement('input'); document.body.appendChild(input); diff --git a/packages/roosterjs-editor-core/test/corePlugins/domEventPluginTest.ts b/packages/roosterjs-editor-core/test/corePlugins/domEventPluginTest.ts index ea007fbeefe..cb17db99cff 100644 --- a/packages/roosterjs-editor-core/test/corePlugins/domEventPluginTest.ts +++ b/packages/roosterjs-editor-core/test/corePlugins/domEventPluginTest.ts @@ -38,6 +38,7 @@ describe('DOMEventPlugin', () => { stopPrintableKeyboardEventPropagation: true, contextMenuProviders: [], tableSelectionRange: null, + imageSelectionRange: null, }); expect(addDomEventHandler).toHaveBeenCalled(); @@ -88,6 +89,7 @@ describe('DOMEventPlugin', () => { stopPrintableKeyboardEventPropagation: false, contextMenuProviders: [], tableSelectionRange: null, + imageSelectionRange: null, }); expect(addDomEventHandler).toHaveBeenCalled(); diff --git a/packages/roosterjs-editor-core/test/corePlugins/lifecyclePluginTest.ts b/packages/roosterjs-editor-core/test/corePlugins/lifecyclePluginTest.ts index 77f25db6548..2a0bf11304b 100644 --- a/packages/roosterjs-editor-core/test/corePlugins/lifecyclePluginTest.ts +++ b/packages/roosterjs-editor-core/test/corePlugins/lifecyclePluginTest.ts @@ -39,6 +39,7 @@ describe('LifecyclePlugin', () => { shadowEditSelectionPath: null, shadowEditFragment: null, shadowEditTableSelectionPath: null, + shadowEditImageSelectionPath: null, getDarkColor, }); @@ -92,6 +93,7 @@ describe('LifecyclePlugin', () => { shadowEditFragment: null, shadowEditSelectionPath: null, shadowEditTableSelectionPath: null, + shadowEditImageSelectionPath: null, getDarkColor, }); diff --git a/packages/roosterjs-editor-core/test/editor/newEditorTest.ts b/packages/roosterjs-editor-core/test/editor/newEditorTest.ts index 2820aea7419..561654a23c7 100644 --- a/packages/roosterjs-editor-core/test/editor/newEditorTest.ts +++ b/packages/roosterjs-editor-core/test/editor/newEditorTest.ts @@ -63,6 +63,7 @@ describe('Editor', () => { stopPrintableKeyboardEventPropagation: true, contextMenuProviders: [], tableSelectionRange: null, + imageSelectionRange: null, }); if (!Browser.isChrome) { expect(core.edit).toEqual({ @@ -171,6 +172,7 @@ describe('Editor', () => { stopPrintableKeyboardEventPropagation: false, contextMenuProviders: [], tableSelectionRange: null, + imageSelectionRange: null, }); if (!Browser.isChrome) { expect(core.edit).toEqual({ diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts index ade8292ad90..3fc2842d048 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts @@ -102,6 +102,11 @@ const DARK_MODE_BGCOLOR = '#333'; */ const MAX_SMALL_SIZE_IMAGE = 10000; +/** + * Id to the wrapper and image selected + */ +const IMAGE_SELECTED = 'imageSelected'; + /** * ImageEdit plugin provides the ability to edit an inline image in editor, including image resizing, rotation and cropping */ @@ -132,6 +137,8 @@ export default class ImageEdit implements EditorPlugin { */ private wasResized: boolean; + private idNumber = 0; + /** * Create a new instance of ImageEdit * @param options Image editing options @@ -364,6 +371,15 @@ export default class ImageEdit implements EditorPlugin { true /*isReadonly*/ ); + if (!this.image.id) { + this.idNumber = this.idNumber + 1; + const imageId = IMAGE_SELECTED + this.idNumber; + this.image.id = imageId; + wrapper.id = imageId; + } else { + wrapper.id = this.image.id; + } + wrapper.style.position = 'relative'; wrapper.style.maxWidth = '100%'; // keep the same vertical align diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts b/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts index 35fcf4c1898..392a79e6bfe 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts @@ -183,7 +183,6 @@ export default class PickerPlugin { - let editor: IEditor; - const TEST_ID = 'PickerTest'; - const root = document.createElement('div'); - root.id = 'test'; - root.innerText = '-'; - document.body.appendChild(root); - const options: PickerPluginOptions = { - elementIdPrefix: 'test', - changeSource: ChangeSource.SetContent, - triggerCharacter: '-', - }; - const dataProvider: PickerDataProvider = { - onInitalize: ( - insertNodeCallback: (nodeToInsert: HTMLElement) => void, - setIsSuggestingCallback: (isSuggesting: boolean) => void, - editor?: IEditor - ) => { - editor.focus(); - const editorSearchCursorSpy = spyOn(editor, 'getContentSearcherOfCursor'); - const mockedPosition = new PositionContentSearcher(root, new Position(root, 4)); - spyOn(mockedPosition, 'getSubStringBefore').and.returnValue('-'); - editorSearchCursorSpy.and.returnValue(mockedPosition); - insertNodeCallback(root); - setIsSuggestingCallback(true); - return; - }, - onDispose: () => { - return; - }, - onIsSuggestingChanged: (isSuggesting: boolean) => { - return; - }, - queryStringUpdated: (queryString: string, isExactMatch: boolean) => { - return; - }, - onContentChanged: (elementIds: string[]) => { - return; - }, - onRemove: (nodeRemoved: Node, isBackwards: boolean) => { - const node = document.createTextNode(''); - return node; - }, - }; - - let plugin: PickerPlugin; - beforeEach(() => { - plugin = new PickerPlugin(dataProvider, options); - editor = TestHelper.initEditor(TEST_ID, [plugin]); - editor.focus(); - const editorQueryElements = spyOn(editor, 'queryElements'); - editorQueryElements.and.returnValue([root]); - plugin.initialize(editor); - }); - - afterEach(() => { - document.body.removeChild(root); - editor.dispose(); - }); - - it('PickerPlugin | ContentEvent', () => { - const eventChange: PluginEvent = { - eventType: PluginEventType.ContentChanged, - source: ChangeSource.SetContent, - }; - plugin.onPluginEvent(eventChange); - spyOn(plugin.dataProvider, 'onContentChanged'); - expect(plugin.dataProvider.onContentChanged).toHaveBeenCalled(); - }); - - function keyDownTest(key: string) { - const eventChange: PluginEvent = { - eventType: PluginEventType.KeyDown, - rawEvent: { - key: key, - preventDefault: () => { - return; - }, - stopImmediatePropagation: () => { - return; - }, - }, - }; - - plugin.onPluginEvent(eventChange); - spyOn(eventChange.rawEvent, 'preventDefault'); - spyOn(eventChange.rawEvent, 'stopImmediatePropagation'); - expect(eventChange.rawEvent.preventDefault).toHaveBeenCalled(); - expect(eventChange.rawEvent.stopImmediatePropagation).toHaveBeenCalled(); - } - - it('PickerPlugin | KeyDownESC', () => { - keyDownTest('Esc'); - }); -}); diff --git a/packages/roosterjs-editor-types/lib/corePluginState/DOMEventPluginState.ts b/packages/roosterjs-editor-types/lib/corePluginState/DOMEventPluginState.ts index 21a1a90960f..dbc727eeabb 100644 --- a/packages/roosterjs-editor-types/lib/corePluginState/DOMEventPluginState.ts +++ b/packages/roosterjs-editor-types/lib/corePluginState/DOMEventPluginState.ts @@ -1,5 +1,5 @@ import ContextMenuProvider from '../interface/ContextMenuProvider'; -import { TableSelectionRange } from '../interface/SelectionRangeEx'; +import { ImageSelectionRange, TableSelectionRange } from '../interface/SelectionRangeEx'; /** * The state object for DOMEventPlugin @@ -34,4 +34,9 @@ export default interface DOMEventPluginState { * Context menu providers, that can provide context menu items */ contextMenuProviders: ContextMenuProvider[]; + + /** + * Image selection range + */ + imageSelectionRange: ImageSelectionRange | null; } diff --git a/packages/roosterjs-editor-types/lib/corePluginState/LifecyclePluginState.ts b/packages/roosterjs-editor-types/lib/corePluginState/LifecyclePluginState.ts index e0208e5587f..777021e7254 100644 --- a/packages/roosterjs-editor-types/lib/corePluginState/LifecyclePluginState.ts +++ b/packages/roosterjs-editor-types/lib/corePluginState/LifecyclePluginState.ts @@ -52,4 +52,9 @@ export default interface LifecyclePluginState { * Cached table selection path for original content */ shadowEditTableSelectionPath: SelectionPath[] | null; + + /** + * Cached image selection path for original content + */ + shadowEditImageSelectionPath: SelectionPath[] | null; } diff --git a/packages/roosterjs-editor-types/lib/enum/SelectionRangeTypes.ts b/packages/roosterjs-editor-types/lib/enum/SelectionRangeTypes.ts index 63623de9757..258f070bdc0 100644 --- a/packages/roosterjs-editor-types/lib/enum/SelectionRangeTypes.ts +++ b/packages/roosterjs-editor-types/lib/enum/SelectionRangeTypes.ts @@ -10,4 +10,8 @@ export const enum SelectionRangeTypes { * Selection made inside of a single table. */ TableSelection, + /** + * Selection made in a image. + */ + ImageSelection, } diff --git a/packages/roosterjs-editor-types/lib/interface/EditorCore.ts b/packages/roosterjs-editor-types/lib/interface/EditorCore.ts index 7eb7bc3bb97..870184c9a51 100644 --- a/packages/roosterjs-editor-types/lib/interface/EditorCore.ts +++ b/packages/roosterjs-editor-types/lib/interface/EditorCore.ts @@ -8,6 +8,7 @@ import { ColorTransformDirection } from '../enum/ColorTransformDirection'; import { ContentMetadata } from './ContentMetadata'; import { DOMEventHandler } from '../type/domEventHandler'; import { GetContentMode } from '../enum/GetContentMode'; +import { ImageSelectionRange } from './SelectionRangeEx'; import { InsertOption } from './InsertOption'; import { PendableFormatState, StyleBasedFormatState } from './FormatState'; import { PluginEvent } from '../event/PluginEvent'; @@ -19,7 +20,6 @@ import { TrustedHTMLHandler } from '../type/TrustedHTMLHandler'; import type { CompatibleChangeSource } from '../compatibleEnum/ChangeSource'; import type { CompatibleColorTransformDirection } from '../compatibleEnum/ColorTransformDirection'; import type { CompatibleGetContentMode } from '../compatibleEnum/GetContentMode'; - /** * Represents the core data structure of an editor */ @@ -266,6 +266,20 @@ export type SelectTable = ( coordinates?: TableSelection ) => TableSelectionRange | null; +/** + * Select a table and save data of the selected range + * @param core The EditorCore object + * @param table table to select + * @param coordinates first and last cell of the selection, if this parameter is null, instead of + * selecting, will unselect the table. + * @returns true if successful + */ +export type SelectImage = ( + core: EditorCore, + image: HTMLImageElement | null, + wrapper?: HTMLSpanElement +) => ImageSelectionRange | null; + /** * The interface for the map of core API. * Editor can call call API from this map under EditorCore object @@ -431,4 +445,13 @@ export interface CoreApiMap { * @returns true if successful */ selectTable: SelectTable; + + /** + * Select a image and save data of the selected range + * @param core The EditorCore object + * @param image image to select + * @param imageId the id of the image element + * @returns true if successful + */ + selectImage: SelectImage; } diff --git a/packages/roosterjs-editor-types/lib/interface/SelectionRangeEx.ts b/packages/roosterjs-editor-types/lib/interface/SelectionRangeEx.ts index df77a0b201c..3f9b0bc071a 100644 --- a/packages/roosterjs-editor-types/lib/interface/SelectionRangeEx.ts +++ b/packages/roosterjs-editor-types/lib/interface/SelectionRangeEx.ts @@ -41,6 +41,23 @@ export interface TableSelectionRange coordinates: TableSelection | undefined; } +/** + * Represents a selected image. + */ +export interface ImageSelectionRange + extends SelectionRangeExBase< + SelectionRangeTypes.ImageSelection | CompatibleSelectionRangeTypes.ImageSelection + > { + /** + * Selected Image + */ + image: HTMLImageElement; + /** + * Id of the selected Image + */ + imageId: string | undefined; +} + /** * Represents normal selection */ @@ -52,4 +69,4 @@ export interface NormalSelectionRange /** * Types of ranges used in editor api getSelectionRangeEx */ -export type SelectionRangeEx = NormalSelectionRange | TableSelectionRange; +export type SelectionRangeEx = NormalSelectionRange | TableSelectionRange | ImageSelectionRange; diff --git a/packages/roosterjs-editor-types/lib/interface/index.ts b/packages/roosterjs-editor-types/lib/interface/index.ts index a8c6e0571cb..539f41d9fd3 100644 --- a/packages/roosterjs-editor-types/lib/interface/index.ts +++ b/packages/roosterjs-editor-types/lib/interface/index.ts @@ -87,6 +87,7 @@ export { TransformColor, TriggerEvent, SelectTable, + SelectImage, } from './EditorCore'; export { default as EditorOptions } from './EditorOptions'; export { @@ -113,5 +114,6 @@ export { SelectionRangeExBase, NormalSelectionRange, TableSelectionRange, + ImageSelectionRange, SelectionRangeEx, } from './SelectionRangeEx'; From 39752b88940bca7840bd9e959e03da91bb7f7bfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Thu, 15 Sep 2022 19:59:55 -0300 Subject: [PATCH 08/41] remove code --- packages/roosterjs-editor-core/lib/coreApi/selectImage.ts | 6 +++--- .../lib/plugins/Picker/PickerPlugin.ts | 2 -- .../test/AutoFormat/autoFormatTest.ts | 5 +++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts b/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts index 79302d85571..1fc3128aeff 100644 --- a/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts +++ b/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts @@ -5,9 +5,9 @@ import { EditorCore, SelectImage, SelectionRangeTypes } from 'roosterjs-editor-t * @internal * Select a image and save data of the selected range * @param core The EditorCore object - * @param table image to select - * @param imageId - * @returns true if successful + * @param image Image to select + * @param wrapper Selected image wrapper + * @returns Selected image information */ export const selectImage: SelectImage = ( core: EditorCore, diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts b/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts index 392a79e6bfe..38426029752 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts @@ -174,7 +174,6 @@ export default class PickerPlugin { let editor: IEditor; const TEST_ID = 'autoHyphenTest'; - let plugin = new AutoFormat(); + let plugin: EditorPlugin; beforeEach(() => { + plugin = new AutoFormat(); editor = TestHelper.initEditor(TEST_ID, [plugin]); }); From 18d502170353ca81a04839d959038e65d833d6fd Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Thu, 15 Sep 2022 16:56:19 -0700 Subject: [PATCH 09/41] Content Model customization step 3: Customize format handlers (#1255) * Reorganize some code * fix build * Move some code * Remove unnecessary change * More fix * fix comment * Customization 2 * temporarily allow empty interface * ignore a lint rule * Customization step 3 --- .../context/createDomToModelContext.ts | 3 + .../lib/domToModel/utils/parseFormat.ts | 11 +- .../lib/formatHandlers/FormatHandler.ts | 21 +--- .../formatHandlers/SegmentFormatHandlers.ts | 32 ++--- .../formatHandlers/TableCellFormatHandler.ts | 20 ++-- .../lib/formatHandlers/TableFormatHandlers.ts | 23 ++-- .../formatHandlers/defaultFormatHandlers.ts | 68 +++++++++++ packages/roosterjs-content-model/lib/index.ts | 13 +- .../context/createModelToDomContext.ts | 2 + .../lib/modelToDom/utils/applyFormat.ts | 8 +- .../IExperimentalContentModelEditor.ts | 11 +- .../publicTypes/context/DomToModelSettings.ts | 29 +++++ .../publicTypes/context/ModelToDomContext.ts | 6 +- .../publicTypes/context/ModelToDomSettings.ts | 32 +++++ .../format/FormatHandlerTypeMap.ts | 112 ++++++++++++++++++ .../context/createDomToModelContextTest.ts | 2 + .../test/domToModel/utils/parseFormatTest.ts | 38 +++--- .../formatHandlers/createFormatContextTest.ts | 2 + 18 files changed, 332 insertions(+), 101 deletions(-) create mode 100644 packages/roosterjs-content-model/lib/formatHandlers/defaultFormatHandlers.ts create mode 100644 packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomSettings.ts create mode 100644 packages/roosterjs-content-model/lib/publicTypes/format/FormatHandlerTypeMap.ts diff --git a/packages/roosterjs-content-model/lib/domToModel/context/createDomToModelContext.ts b/packages/roosterjs-content-model/lib/domToModel/context/createDomToModelContext.ts index ac871b746b6..60999abd7f7 100644 --- a/packages/roosterjs-content-model/lib/domToModel/context/createDomToModelContext.ts +++ b/packages/roosterjs-content-model/lib/domToModel/context/createDomToModelContext.ts @@ -3,6 +3,7 @@ import { defaultStyleMap } from './defaultStyles'; import { DomToModelContext } from '../../publicTypes/context/DomToModelContext'; import { DomToModelOption } from '../../publicTypes/IExperimentalContentModelEditor'; import { EditorContext } from '../../publicTypes/context/EditorContext'; +import { getFormatParsers } from '../../formatHandlers/defaultFormatHandlers'; import { SelectionRangeEx, SelectionRangeTypes } from 'roosterjs-editor-types'; /** @@ -33,6 +34,8 @@ export function createDomToModelContext( ...defaultStyleMap, ...(options?.defaultStyleOverride || {}), }, + + formatParsers: getFormatParsers(options?.formatParserOverride), }; switch (range?.type) { diff --git a/packages/roosterjs-content-model/lib/domToModel/utils/parseFormat.ts b/packages/roosterjs-content-model/lib/domToModel/utils/parseFormat.ts index 1819afdbdb0..15a5af57fe1 100644 --- a/packages/roosterjs-content-model/lib/domToModel/utils/parseFormat.ts +++ b/packages/roosterjs-content-model/lib/domToModel/utils/parseFormat.ts @@ -1,21 +1,20 @@ import { ContentModelFormatBase } from '../../publicTypes/format/ContentModelFormatBase'; -import { defaultStyleMap } from '../context/defaultStyles'; import { DomToModelContext } from '../../publicTypes/context/DomToModelContext'; -import { FormatHandler } from '../../formatHandlers/FormatHandler'; +import { FormatKey } from '../../publicTypes/format/FormatHandlerTypeMap'; /** * @internal */ export function parseFormat( element: HTMLElement, - handlers: FormatHandler[], + handlerKeys: FormatKey[], format: T, context: DomToModelContext ) { - const styleItem = defaultStyleMap[element.tagName]; + const styleItem = context.defaultStyles[element.tagName]; const defaultStyle = styleItem || {}; - handlers.forEach(handler => { - handler.parse(format, element, context, defaultStyle); + handlerKeys.forEach(key => { + context.formatParsers[key](format, element, context, defaultStyle); }); } diff --git a/packages/roosterjs-content-model/lib/formatHandlers/FormatHandler.ts b/packages/roosterjs-content-model/lib/formatHandlers/FormatHandler.ts index b24ff57593b..1d6cb545cc5 100644 --- a/packages/roosterjs-content-model/lib/formatHandlers/FormatHandler.ts +++ b/packages/roosterjs-content-model/lib/formatHandlers/FormatHandler.ts @@ -1,30 +1,19 @@ import { ContentModelFormatBase } from '../publicTypes/format/ContentModelFormatBase'; -import { DomToModelContext } from '../publicTypes/context/DomToModelContext'; -import { ModelToDomContext } from '../publicTypes/context/ModelToDomContext'; +import { FormatApplier } from '../publicTypes/context/ModelToDomSettings'; +import { FormatParser } from '../publicTypes/context/DomToModelSettings'; /** + * @internal * Represents an object that will handle a given format */ export interface FormatHandler { /** * Parse format from the given HTML element and default style - * @param format The format object to parse into - * @param element The HTML element to parse format from - * @param context The context object that provide related context information - * @param defaultStyle Default CSS style of the given HTML element */ - parse: ( - format: TFormat, - element: HTMLElement, - context: DomToModelContext, - defaultStyle: Readonly> - ) => void; + parse: FormatParser; /** * Apply format to the given HTML element - * @param format The format object to apply - * @param element The HTML element to apply format to - * @param context The context object that provide related context information */ - apply: (format: TFormat, element: HTMLElement, context: ModelToDomContext) => void; + apply: FormatApplier; } diff --git a/packages/roosterjs-content-model/lib/formatHandlers/SegmentFormatHandlers.ts b/packages/roosterjs-content-model/lib/formatHandlers/SegmentFormatHandlers.ts index fd992bf4c25..df8ce8d8e8b 100644 --- a/packages/roosterjs-content-model/lib/formatHandlers/SegmentFormatHandlers.ts +++ b/packages/roosterjs-content-model/lib/formatHandlers/SegmentFormatHandlers.ts @@ -1,27 +1,17 @@ -import { backgroundColorFormatHandler } from './common/backgroundColorFormatHandler'; -import { boldFormatHandler } from './segment/boldFormatHandler'; -import { ContentModelSegmentFormat } from '../publicTypes/format/ContentModelSegmentFormat'; -import { fontFamilyFormatHandler } from './segment/fontFamilyFormatHandler'; -import { fontSizeFormatHandler } from './segment/fontSizeFormatHandler'; -import { FormatHandler } from './FormatHandler'; -import { italicFormatHandler } from './segment/italicFormatHandler'; -import { strikeFormatHandler } from './segment/strikeFormatHandler'; -import { superOrSubScriptFormatHandler } from './segment/superOrSubScriptFormatHandler'; -import { textColorFormatHandler } from './segment/textColorFormatHandler'; -import { underlineFormatHandler } from './segment/underlineFormatHandler'; +import { FormatKey } from '../publicTypes/format/FormatHandlerTypeMap'; /** * @internal * Order by frequency, from not common used to common used, for better optimization */ -export const SegmentFormatHandlers: FormatHandler[] = [ - superOrSubScriptFormatHandler, - strikeFormatHandler, - fontFamilyFormatHandler, - fontSizeFormatHandler, - underlineFormatHandler, - italicFormatHandler, - boldFormatHandler, - textColorFormatHandler, - backgroundColorFormatHandler, +export const SegmentFormatHandlers: FormatKey[] = [ + 'superOrSubScript', + 'strike', + 'fontFamily', + 'fontSize', + 'underline', + 'italic', + 'bold', + 'textColor', + 'backgroundColor', ]; diff --git a/packages/roosterjs-content-model/lib/formatHandlers/TableCellFormatHandler.ts b/packages/roosterjs-content-model/lib/formatHandlers/TableCellFormatHandler.ts index 7ee830f041d..b72301c769e 100644 --- a/packages/roosterjs-content-model/lib/formatHandlers/TableCellFormatHandler.ts +++ b/packages/roosterjs-content-model/lib/formatHandlers/TableCellFormatHandler.ts @@ -1,18 +1,12 @@ -import { backgroundColorFormatHandler } from './common/backgroundColorFormatHandler'; -import { borderFormatHandler } from './common/borderFormatHandler'; -import { ContentModelTableCellFormat } from '../publicTypes/format/ContentModelTableCellFormat'; -import { FormatHandler } from './FormatHandler'; -import { tableCellMetadataFormatHandler } from './table/tableCellMetadataFormatHandler'; -import { textAlignFormatHandler } from './common/textAlignFormatHandler'; -import { verticalAlignFormatHandler } from './common/verticalAlignFormatHandler'; +import { FormatKey } from '../publicTypes/format/FormatHandlerTypeMap'; /** * @internal */ -export const TableCellFormatHandlers: FormatHandler[] = [ - borderFormatHandler, - backgroundColorFormatHandler, - textAlignFormatHandler, - verticalAlignFormatHandler, - tableCellMetadataFormatHandler, +export const TableCellFormatHandlers: FormatKey[] = [ + 'border', + 'backgroundColor', + 'textAlign', + 'verticalAlign', + 'tableCellMetadata', ]; diff --git a/packages/roosterjs-content-model/lib/formatHandlers/TableFormatHandlers.ts b/packages/roosterjs-content-model/lib/formatHandlers/TableFormatHandlers.ts index ebd83951a17..e284cb1ae16 100644 --- a/packages/roosterjs-content-model/lib/formatHandlers/TableFormatHandlers.ts +++ b/packages/roosterjs-content-model/lib/formatHandlers/TableFormatHandlers.ts @@ -1,20 +1,13 @@ -import { backgroundColorFormatHandler } from './common/backgroundColorFormatHandler'; -import { borderFormatHandler } from './common/borderFormatHandler'; -import { ContentModelTableFormat } from '../publicTypes/format/ContentModelTableFormat'; -import { FormatHandler } from './FormatHandler'; -import { idFormatHandler } from './common/idFormatHandler'; -import { marginFormatHandler } from './paragraph/marginFormatHandler'; -import { tableMetadataFormatHandler } from './table/tableMetadataFormatHandler'; -import { tableSpacingFormatHandler } from './table/tableSpacingFormatHandler'; +import { FormatKey } from '../publicTypes/format/FormatHandlerTypeMap'; /** * @internal */ -export const TableFormatHandlers: FormatHandler[] = [ - idFormatHandler, - borderFormatHandler, - tableMetadataFormatHandler, - tableSpacingFormatHandler, - marginFormatHandler, - backgroundColorFormatHandler, +export const TableFormatHandlers: FormatKey[] = [ + 'id', + 'border', + 'tableMetadata', + 'tableSpacing', + 'margin', + 'backgroundColor', ]; diff --git a/packages/roosterjs-content-model/lib/formatHandlers/defaultFormatHandlers.ts b/packages/roosterjs-content-model/lib/formatHandlers/defaultFormatHandlers.ts new file mode 100644 index 00000000000..a9bb54e6561 --- /dev/null +++ b/packages/roosterjs-content-model/lib/formatHandlers/defaultFormatHandlers.ts @@ -0,0 +1,68 @@ +import { backgroundColorFormatHandler } from './common/backgroundColorFormatHandler'; +import { boldFormatHandler } from './segment/boldFormatHandler'; +import { borderFormatHandler } from './common/borderFormatHandler'; +import { fontFamilyFormatHandler } from './segment/fontFamilyFormatHandler'; +import { fontSizeFormatHandler } from './segment/fontSizeFormatHandler'; +import { FormatAppliers } from '../publicTypes/context/ModelToDomSettings'; +import { FormatHandler } from './FormatHandler'; +import { FormatHandlerTypeMap, FormatKey } from '../publicTypes/format/FormatHandlerTypeMap'; +import { FormatParsers } from '../publicTypes/context/DomToModelSettings'; +import { getObjectKeys } from 'roosterjs-editor-dom'; +import { idFormatHandler } from './common/idFormatHandler'; +import { italicFormatHandler } from './segment/italicFormatHandler'; +import { marginFormatHandler } from './paragraph/marginFormatHandler'; +import { strikeFormatHandler } from './segment/strikeFormatHandler'; +import { superOrSubScriptFormatHandler } from './segment/superOrSubScriptFormatHandler'; +import { tableCellMetadataFormatHandler } from './table/tableCellMetadataFormatHandler'; +import { tableMetadataFormatHandler } from './table/tableMetadataFormatHandler'; +import { tableSpacingFormatHandler } from './table/tableSpacingFormatHandler'; +import { textAlignFormatHandler } from './common/textAlignFormatHandler'; +import { textColorFormatHandler } from './segment/textColorFormatHandler'; +import { underlineFormatHandler } from './segment/underlineFormatHandler'; +import { verticalAlignFormatHandler } from './common/verticalAlignFormatHandler'; + +type FormatHandlers = { + [Key in FormatKey]: FormatHandler; +}; + +const defaultFormatHandlerMap: FormatHandlers = { + backgroundColor: backgroundColorFormatHandler, + bold: boldFormatHandler, + border: borderFormatHandler, + fontFamily: fontFamilyFormatHandler, + fontSize: fontSizeFormatHandler, + id: idFormatHandler, + italic: italicFormatHandler, + margin: marginFormatHandler, + strike: strikeFormatHandler, + superOrSubScript: superOrSubScriptFormatHandler, + tableCellMetadata: tableCellMetadataFormatHandler, + tableMetadata: tableMetadataFormatHandler, + tableSpacing: tableSpacingFormatHandler, + textAlign: textAlignFormatHandler, + textColor: textColorFormatHandler, + underline: underlineFormatHandler, + verticalAlign: verticalAlignFormatHandler, +}; + +/** + * @internal + */ +export function getFormatParsers(option?: Partial): FormatParsers { + return getObjectKeys(defaultFormatHandlerMap).reduce((parsers, key) => { + parsers[key] = option?.[key] || defaultFormatHandlerMap[key].parse; + + return parsers; + }, {}); +} + +/** + * @internal + */ +export function getFormatAppliers(option?: Partial): FormatAppliers { + return getObjectKeys(defaultFormatHandlerMap).reduce((parsers, key) => { + parsers[key] = option?.[key] || defaultFormatHandlerMap[key].apply; + + return parsers; + }, {}); +} diff --git a/packages/roosterjs-content-model/lib/index.ts b/packages/roosterjs-content-model/lib/index.ts index 290aec8edb6..220f3fabc54 100644 --- a/packages/roosterjs-content-model/lib/index.ts +++ b/packages/roosterjs-content-model/lib/index.ts @@ -29,6 +29,7 @@ export { ContentModelBr } from './publicTypes/segment/ContentModelBr'; export { ContentModelGeneralSegment } from './publicTypes/segment/ContentModelGeneralSegment'; export { ContentModelSegment } from './publicTypes/segment/ContentModelSegment'; +export { FormatHandlerTypeMap, FormatKey } from './publicTypes/format/FormatHandlerTypeMap'; export { ContentModelTableFormat } from './publicTypes/format/ContentModelTableFormat'; export { ContentModelTableCellFormat } from './publicTypes/format/ContentModelTableCellFormat'; export { ContentModelSegmentFormat } from './publicTypes/format/ContentModelSegmentFormat'; @@ -62,7 +63,12 @@ export { DomToModelImageSelection, DomToModelSelectionContext, } from './publicTypes/context/DomToModelSelectionContext'; -export { DomToModelSettings, DefaultStyleMap } from './publicTypes/context/DomToModelSettings'; +export { + DomToModelSettings, + DefaultStyleMap, + FormatParser, + FormatParsers, +} from './publicTypes/context/DomToModelSettings'; export { DomToModelContext } from './publicTypes/context/DomToModelContext'; export { ModelToDomContext } from './publicTypes/context/ModelToDomContext'; export { @@ -71,6 +77,11 @@ export { ModelToDomTableSelection, ModelToDomSelectionContext, } from './publicTypes/context/ModelToDomSelectionContext'; +export { + ModelToDomSettings, + FormatApplier, + FormatAppliers, +} from './publicTypes/context/ModelToDomSettings'; export { ElementProcessor } from './publicTypes/context/ElementProcessor'; export { diff --git a/packages/roosterjs-content-model/lib/modelToDom/context/createModelToDomContext.ts b/packages/roosterjs-content-model/lib/modelToDom/context/createModelToDomContext.ts index d1296653fd5..2a19e0030a1 100644 --- a/packages/roosterjs-content-model/lib/modelToDom/context/createModelToDomContext.ts +++ b/packages/roosterjs-content-model/lib/modelToDom/context/createModelToDomContext.ts @@ -1,4 +1,5 @@ import { EditorContext } from '../../publicTypes/context/EditorContext'; +import { getFormatAppliers } from '../../formatHandlers/defaultFormatHandlers'; import { ModelToDomContext } from '../../publicTypes/context/ModelToDomContext'; import { ModelToDomOption } from '../../publicTypes/IExperimentalContentModelEditor'; @@ -24,5 +25,6 @@ export function createModelToDomContext( segment: null, }, }, + formatAppliers: getFormatAppliers(options?.formatApplierOverride), }; } diff --git a/packages/roosterjs-content-model/lib/modelToDom/utils/applyFormat.ts b/packages/roosterjs-content-model/lib/modelToDom/utils/applyFormat.ts index 726f35e7de4..3c2f5c521f1 100644 --- a/packages/roosterjs-content-model/lib/modelToDom/utils/applyFormat.ts +++ b/packages/roosterjs-content-model/lib/modelToDom/utils/applyFormat.ts @@ -1,5 +1,5 @@ import { ContentModelFormatBase } from '../../publicTypes/format/ContentModelFormatBase'; -import { FormatHandler } from '../../formatHandlers/FormatHandler'; +import { FormatKey } from '../../publicTypes/format/FormatHandlerTypeMap'; import { ModelToDomContext } from '../../publicTypes/context/ModelToDomContext'; /** @@ -7,11 +7,11 @@ import { ModelToDomContext } from '../../publicTypes/context/ModelToDomContext'; */ export function applyFormat( element: HTMLElement, - handlers: FormatHandler[], + handlerKeys: FormatKey[], format: T, context: ModelToDomContext ) { - handlers.forEach(handler => { - handler.apply(format, element, context); + handlerKeys.forEach(key => { + context.formatAppliers[key](format, element, context); }); } diff --git a/packages/roosterjs-content-model/lib/publicTypes/IExperimentalContentModelEditor.ts b/packages/roosterjs-content-model/lib/publicTypes/IExperimentalContentModelEditor.ts index 1ef978260fc..4d57c670ef1 100644 --- a/packages/roosterjs-content-model/lib/publicTypes/IExperimentalContentModelEditor.ts +++ b/packages/roosterjs-content-model/lib/publicTypes/IExperimentalContentModelEditor.ts @@ -1,7 +1,8 @@ import { ContentModelDocument } from './block/group/ContentModelDocument'; -import { DefaultStyleMap } from './context/DomToModelSettings'; +import { DefaultStyleMap, FormatParsers } from './context/DomToModelSettings'; import { EditorContext } from './context/EditorContext'; import { ElementProcessor } from './context/ElementProcessor'; +import { FormatAppliers } from './context/ModelToDomSettings'; import { IEditor } from 'roosterjs-editor-types'; /** @@ -17,14 +18,18 @@ export interface DomToModelOption { * Overrides default element styles */ defaultStyleOverride?: DefaultStyleMap; + + /** + * Overrides default format handlers + */ + formatParserOverride?: Partial; } /** * Options for creating ModelToDomContext */ -// tslint:disable no-empty-interface export interface ModelToDomOption { - // TODO: Add options here + formatApplierOverride?: Partial; } /** diff --git a/packages/roosterjs-content-model/lib/publicTypes/context/DomToModelSettings.ts b/packages/roosterjs-content-model/lib/publicTypes/context/DomToModelSettings.ts index bd374793aaf..49004d138de 100644 --- a/packages/roosterjs-content-model/lib/publicTypes/context/DomToModelSettings.ts +++ b/packages/roosterjs-content-model/lib/publicTypes/context/DomToModelSettings.ts @@ -1,10 +1,34 @@ +import { ContentModelFormatBase } from '../format/ContentModelFormatBase'; +import { DomToModelContext } from './DomToModelContext'; import { ElementProcessor } from './ElementProcessor'; +import { FormatHandlerTypeMap, FormatKey } from '../format/FormatHandlerTypeMap'; /** * A type of Default style map, from tag name string (in upper case) to a static style object */ export type DefaultStyleMap = Record>; +/** + * Parse format from the given HTML element and default style + * @param format The format object to parse into + * @param element The HTML element to parse format from + * @param context The context object that provide related context information + * @param defaultStyle Default CSS style of the given HTML element + */ +export type FormatParser = ( + format: TFormat, + element: HTMLElement, + context: DomToModelContext, + defaultStyle: Readonly> +) => void; + +/** + * All format parsers + */ +export type FormatParsers = { + [Key in FormatKey]: FormatParser; +}; + /** * Represents settings to customize DOM to Content Model conversion */ @@ -18,4 +42,9 @@ export interface DomToModelSettings { * Map of default styles */ defaultStyles: DefaultStyleMap; + + /** + * Map of format parsers + */ + formatParsers: FormatParsers; } diff --git a/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomContext.ts b/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomContext.ts index 9e98dd71098..b2bb9737976 100644 --- a/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomContext.ts +++ b/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomContext.ts @@ -1,7 +1,11 @@ import { EditorContext } from './EditorContext'; import { ModelToDomSelectionContext } from './ModelToDomSelectionContext'; +import { ModelToDomSettings } from './ModelToDomSettings'; /** * Context of Model to DOM conversion, used for generate HTML DOM tree according to current context */ -export interface ModelToDomContext extends EditorContext, ModelToDomSelectionContext {} +export interface ModelToDomContext + extends EditorContext, + ModelToDomSelectionContext, + ModelToDomSettings {} diff --git a/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomSettings.ts b/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomSettings.ts new file mode 100644 index 00000000000..ff00b2c18f1 --- /dev/null +++ b/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomSettings.ts @@ -0,0 +1,32 @@ +import { ContentModelFormatBase } from '../format/ContentModelFormatBase'; +import { FormatHandlerTypeMap, FormatKey } from '../format/FormatHandlerTypeMap'; +import { ModelToDomContext } from './ModelToDomContext'; + +/** + * Apply format to the given HTML element + * @param format The format object to apply + * @param element The HTML element to apply format to + * @param context The context object that provide related context information + */ +export type FormatApplier = ( + format: TFormat, + element: HTMLElement, + context: ModelToDomContext +) => void; + +/** + * All format appliers + */ +export type FormatAppliers = { + [Key in FormatKey]: FormatApplier; +}; + +/** + * Represents settings to customize DOM to Content Model conversion + */ +export interface ModelToDomSettings { + /** + * Map of format appliers + */ + formatAppliers: FormatAppliers; +} diff --git a/packages/roosterjs-content-model/lib/publicTypes/format/FormatHandlerTypeMap.ts b/packages/roosterjs-content-model/lib/publicTypes/format/FormatHandlerTypeMap.ts new file mode 100644 index 00000000000..f50c6c363f5 --- /dev/null +++ b/packages/roosterjs-content-model/lib/publicTypes/format/FormatHandlerTypeMap.ts @@ -0,0 +1,112 @@ +import { BackgroundColorFormat } from './formatParts/BackgroundColorFormat'; +import { BoldFormat } from './formatParts/BoldFormat'; +import { BorderFormat } from './formatParts/BorderFormat'; +import { FontFamilyFormat } from './formatParts/FontFamilyFormat'; +import { FontSizeFormat } from './formatParts/FontSizeFormat'; +import { IdFormat } from './formatParts/IdFormat'; +import { ItalicFormat } from './formatParts/ItalicFormat'; +import { MarginFormat } from './formatParts/MarginFormat'; +import { SpacingFormat } from './formatParts/SpacingFormat'; +import { StrikeFormat } from './formatParts/StrikeFormat'; +import { SuperOrSubScriptFormat } from './formatParts/SuperOrSubScriptFormat'; +import { TableCellMetadataFormat } from './formatParts/TableCellMetadataFormat'; +import { TableMetadataFormat } from './formatParts/TableMetadataFormat'; +import { TextAlignFormat } from './formatParts/TextAlignFormat'; +import { TextColorFormat } from './formatParts/TextColorFormat'; +import { UnderlineFormat } from './formatParts/UnderlineFormat'; +import { VerticalAlignFormat } from './formatParts/VerticalAlignFormat'; + +/** + * Represents a record of all format handlers + */ +export interface FormatHandlerTypeMap { + /** + * Format for BackgroundColorFormat + */ + backgroundColor: BackgroundColorFormat; + + /** + * Format for BoldFormat + */ + bold: BoldFormat; + + /** + * Format for BorderFormat + */ + border: BorderFormat; + + /** + * Format for FontFamilyFormat + */ + fontFamily: FontFamilyFormat; + + /** + * Format for FontSizeFormat + */ + fontSize: FontSizeFormat; + + /** + * Format for IdFormat + */ + id: IdFormat; + + /** + * Format for ItalicFormat + */ + italic: ItalicFormat; + + /** + * Format for MarginFormat + */ + margin: MarginFormat; + + /** + * Format for StrikeFormat + */ + strike: StrikeFormat; + + /** + * Format for SuperOrSubScriptFormat + */ + superOrSubScript: SuperOrSubScriptFormat; + + /** + * Format for TableCellMetadataFormat + */ + tableCellMetadata: TableCellMetadataFormat; + + /** + * Format for TableMetadataFormat + */ + tableMetadata: TableMetadataFormat; + + /** + * Format for SpacingFormat + */ + tableSpacing: SpacingFormat; + + /** + * Format for TextAlignFormat + */ + textAlign: TextAlignFormat; + + /** + * Format for TextColorFormat + */ + textColor: TextColorFormat; + + /** + * Format for UnderlineFormat + */ + underline: UnderlineFormat; + + /** + * Format for VerticalAlignFormat + */ + verticalAlign: VerticalAlignFormat; +} + +/** + * Key of all format handler + */ +export type FormatKey = keyof FormatHandlerTypeMap; diff --git a/packages/roosterjs-content-model/test/domToModel/context/createDomToModelContextTest.ts b/packages/roosterjs-content-model/test/domToModel/context/createDomToModelContextTest.ts index 495d93e481e..3d927841d6b 100644 --- a/packages/roosterjs-content-model/test/domToModel/context/createDomToModelContextTest.ts +++ b/packages/roosterjs-content-model/test/domToModel/context/createDomToModelContextTest.ts @@ -2,6 +2,7 @@ import { createDomToModelContext } from '../../../lib/domToModel/context/createD import { defaultProcessorMap } from '../../../lib/domToModel/context/defaultProcessors'; import { defaultStyleMap } from '../../../lib/domToModel/context/defaultStyles'; import { EditorContext } from '../../../lib/publicTypes/context/EditorContext'; +import { getFormatParsers } from '../../../lib/formatHandlers/defaultFormatHandlers'; import { SelectionRangeTypes } from 'roosterjs-editor-types'; describe('createDomToModelContext', () => { @@ -14,6 +15,7 @@ describe('createDomToModelContext', () => { const contextOptions = { elementProcessors: defaultProcessorMap, defaultStyles: defaultStyleMap, + formatParsers: getFormatParsers(), }; it('no param', () => { const context = createDomToModelContext(); diff --git a/packages/roosterjs-content-model/test/domToModel/utils/parseFormatTest.ts b/packages/roosterjs-content-model/test/domToModel/utils/parseFormatTest.ts index 3ae64a70e68..42c95ac65d5 100644 --- a/packages/roosterjs-content-model/test/domToModel/utils/parseFormatTest.ts +++ b/packages/roosterjs-content-model/test/domToModel/utils/parseFormatTest.ts @@ -1,15 +1,13 @@ import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; -import { FormatHandler } from '../../../lib/formatHandlers/FormatHandler'; +import { FormatKey } from '../../../lib/publicTypes/format/FormatHandlerTypeMap'; import { parseFormat } from '../../../lib/domToModel/utils/parseFormat'; describe('parseFormat', () => { - const defaultContext = createDomToModelContext(); - it('empty handlers', () => { const element = document.createElement('div'); - const handlers: FormatHandler[] = []; + const handlers: FormatKey[] = []; const format = {}; - + const defaultContext = createDomToModelContext(); parseFormat(element, handlers, format, defaultContext); expect(format).toEqual({}); @@ -17,46 +15,44 @@ describe('parseFormat', () => { it('one handlers', () => { const element = document.createElement('div'); - const handlers: FormatHandler[] = [ - { - parse: (format, e, c, defaultStyle) => { + const defaultContext = createDomToModelContext(undefined, undefined, { + formatParserOverride: { + id: (format, e, c, defaultStyle) => { expect(e).toBe(element); expect(c).toBe(defaultContext); - format.a = 1; + format.id = '1'; }, - apply: null!, }, - ]; + }); + const handlers: FormatKey[] = ['id']; const format = {}; parseFormat(element, handlers, format, defaultContext); - expect(format).toEqual({ a: 1 }); + expect(format).toEqual({ id: '1' }); }); }); describe('Default styles', () => { - const defaultContext = createDomToModelContext(); - function runTest(tag: string, expectResult: Partial) { const element = document.createElement(tag); - const handlers: FormatHandler[] = [ - { - parse: (format, e, c, defaultStyle) => { + const defaultContext = createDomToModelContext(undefined, undefined, { + formatParserOverride: { + id: (format, e, c, defaultStyle) => { expect(defaultStyle).toEqual(expectResult); expect(c).toBe(defaultContext); - format.a = 1; + format.id = '1'; }, - apply: null!, }, - ]; + }); + const handlers: FormatKey[] = ['id']; const format = {}; parseFormat(element, handlers, format, defaultContext); - expect(format).toEqual({ a: 1 }); + expect(format).toEqual({ id: '1' }); } it('Default style for B', () => { diff --git a/packages/roosterjs-content-model/test/formatHandlers/createFormatContextTest.ts b/packages/roosterjs-content-model/test/formatHandlers/createFormatContextTest.ts index e504eb53c72..4747e7d39e7 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/createFormatContextTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/createFormatContextTest.ts @@ -1,12 +1,14 @@ import { createDomToModelContext } from '../../lib/domToModel/context/createDomToModelContext'; import { defaultProcessorMap } from '../../lib/domToModel/context/defaultProcessors'; import { defaultStyleMap } from '../../lib/domToModel/context/defaultStyles'; +import { getFormatParsers } from '../../lib/formatHandlers/defaultFormatHandlers'; import { SelectionRangeTypes } from 'roosterjs-editor-types'; describe('createFormatContextTest', () => { const contextOptions = { elementProcessors: defaultProcessorMap, defaultStyles: defaultStyleMap, + formatParsers: getFormatParsers(), }; it('empty parameter', () => { From 52c26a2437506596a55b04fb90af7a1169d96fba Mon Sep 17 00:00:00 2001 From: Bryan Valverde U Date: Fri, 16 Sep 2022 11:35:56 -0600 Subject: [PATCH 10/41] Fix Cell Shade metadata (#1263) --- .../lib/table/applyCellShading.ts | 10 +---- .../test/table/applyCellShadingTest.ts | 15 +++++--- packages/roosterjs-editor-dom/lib/index.ts | 1 + .../roosterjs-editor-dom/lib/table/VTable.ts | 6 +-- .../lib/table/applyTableFormat.ts | 6 +-- .../lib/table/tableCellInfo.ts | 37 +++++++++++++++++++ .../lib/interface/TableCellMetadataFormat.ts | 9 +++++ .../lib/interface/index.ts | 1 + 8 files changed, 65 insertions(+), 20 deletions(-) create mode 100644 packages/roosterjs-editor-dom/lib/table/tableCellInfo.ts create mode 100644 packages/roosterjs-editor-types/lib/interface/TableCellMetadataFormat.ts diff --git a/packages/roosterjs-editor-api/lib/table/applyCellShading.ts b/packages/roosterjs-editor-api/lib/table/applyCellShading.ts index 0ea5b1d195f..bdb13872085 100644 --- a/packages/roosterjs-editor-api/lib/table/applyCellShading.ts +++ b/packages/roosterjs-editor-api/lib/table/applyCellShading.ts @@ -1,9 +1,6 @@ import formatUndoSnapshot from '../utils/formatUndoSnapshot'; import { IEditor, ModeIndependentColor } from 'roosterjs-editor-types'; -import { safeInstanceOf, setColor } from 'roosterjs-editor-dom'; - -const TEMP_BACKGROUND_COLOR = 'originalBackgroundColor'; -const CELL_SHADE = 'cellShade'; +import { safeInstanceOf, saveTableCellMetadata, setColor } from 'roosterjs-editor-dom'; /** * Set background color of cells. @@ -25,10 +22,7 @@ export default function applyCellShading(editor: IEditor, color: string | ModeIn editor.isDarkMode(), true /** shouldAdaptFontColor */ ); - region.rootNode.dataset[CELL_SHADE] = 'true'; - - region.rootNode.dataset[TEMP_BACKGROUND_COLOR] = - region.rootNode.style.backgroundColor; + saveTableCellMetadata(region.rootNode, { bgColorOverride: true }); } }); }, diff --git a/packages/roosterjs-editor-api/test/table/applyCellShadingTest.ts b/packages/roosterjs-editor-api/test/table/applyCellShadingTest.ts index 7343330071c..4b29e85c05d 100644 --- a/packages/roosterjs-editor-api/test/table/applyCellShadingTest.ts +++ b/packages/roosterjs-editor-api/test/table/applyCellShadingTest.ts @@ -52,11 +52,10 @@ describe('applyCellShading', () => { it('applyCellShading in collapsed range in cell', () => { // Arrange - editor.setContent(TestHelper.tableSelectionContents[1]); - const expected = Browser.isFirefox - ? '

















' - : '

















'; - const cell = document.querySelector('#test_tsc').querySelector('td'); + editor.setContent( + '
' + ); + let cell = document.querySelector('#test_tsc').querySelector('td'); const range = new Range(); range.setStart(cell, 0); editor.select(range); @@ -64,7 +63,11 @@ describe('applyCellShading', () => { // Act applyCellShading(editor, '#00ffff'); + cell = document.querySelector('#test_tsc').querySelector('td'); // Assert - expect(editor.getContent()).toBe(expected); + expect(cell?.style.backgroundColor.replace(' ', '')).toEqual( + 'rgb(0, 255, 255)'.replace(' ', '') + ); + expect(cell?.getAttribute('data-editing-info')).toBeDefined(); }); }); diff --git a/packages/roosterjs-editor-dom/lib/index.ts b/packages/roosterjs-editor-dom/lib/index.ts index b6d5ccf74d7..9f7baa56d4c 100644 --- a/packages/roosterjs-editor-dom/lib/index.ts +++ b/packages/roosterjs-editor-dom/lib/index.ts @@ -59,6 +59,7 @@ export { default as createVListFromRegion } from './list/createVListFromRegion'; export { default as VListChain } from './list/VListChain'; export { default as setListItemStyle } from './list/setListItemStyle'; export { getTableFormatInfo } from './table/tableFormatInfo'; +export { saveTableCellMetadata } from './table/tableCellInfo'; export { default as getRegionsFromRange } from './region/getRegionsFromRange'; export { default as getSelectedBlockElementsInRegion } from './region/getSelectedBlockElementsInRegion'; diff --git a/packages/roosterjs-editor-dom/lib/table/VTable.ts b/packages/roosterjs-editor-dom/lib/table/VTable.ts index 6b25d526860..1f0b96c1563 100644 --- a/packages/roosterjs-editor-dom/lib/table/VTable.ts +++ b/packages/roosterjs-editor-dom/lib/table/VTable.ts @@ -5,6 +5,7 @@ import normalizeRect from '../utils/normalizeRect'; import safeInstanceOf from '../utils/safeInstanceOf'; import toArray from '../jsUtils/toArray'; import { getTableFormatInfo, saveTableInfo } from './tableFormatInfo'; +import { removeMetadata } from '../metadata/metadata'; import { SizeTransformer, TableBorderFormat, @@ -15,7 +16,6 @@ import { } from 'roosterjs-editor-types'; import type { CompatibleTableOperation } from 'roosterjs-editor-types/lib/compatibleTypes'; -const CELL_SHADE = 'cellShade'; const DEFAULT_FORMAT: Required = { topBorderColor: '#ABABAB', bottomBorderColor: '#ABABAB', @@ -197,8 +197,8 @@ export default class VTable { private deleteCellShadeDataset(cells: VCell[][] | null) { cells?.forEach(row => { row.forEach(cell => { - if (cell.td && cell.td.dataset[CELL_SHADE]) { - delete cell.td.dataset[CELL_SHADE]; + if (cell.td) { + removeMetadata(cell.td); } }); }); diff --git a/packages/roosterjs-editor-dom/lib/table/applyTableFormat.ts b/packages/roosterjs-editor-dom/lib/table/applyTableFormat.ts index a5c6dcf4615..852cd57084c 100644 --- a/packages/roosterjs-editor-dom/lib/table/applyTableFormat.ts +++ b/packages/roosterjs-editor-dom/lib/table/applyTableFormat.ts @@ -1,10 +1,10 @@ import changeElementTag from '../utils/changeElementTag'; import setColor from '../utils/setColor'; +import { getTableCellMetadata } from './tableCellInfo'; import { TableBorderFormat, TableFormat, VCell } from 'roosterjs-editor-types'; const TRANSPARENT = 'transparent'; const TABLE_CELL_TAG_NAME = 'TD'; const TABLE_HEADER_TAG_NAME = 'TH'; -const CELL_SHADE = 'cellShade'; /** * @internal @@ -35,8 +35,8 @@ function hasCellShade(cell: VCell) { if (!cell.td) { return false; } - const colorShade = cell.td.dataset[CELL_SHADE]; - return colorShade ? true : false; + + return !!getTableCellMetadata(cell.td)?.bgColorOverride; } /** diff --git a/packages/roosterjs-editor-dom/lib/table/tableCellInfo.ts b/packages/roosterjs-editor-dom/lib/table/tableCellInfo.ts new file mode 100644 index 00000000000..ff284262c1b --- /dev/null +++ b/packages/roosterjs-editor-dom/lib/table/tableCellInfo.ts @@ -0,0 +1,37 @@ +import { createBooleanDefinition, createObjectDefinition } from '../metadata/definitionCreators'; +import { getMetadata, setMetadata } from '../metadata/metadata'; +import { TableCellMetadataFormat } from 'roosterjs-editor-types'; + +const BooleanDefinition = createBooleanDefinition( + false /** isOptional */, + undefined /** value */, + true /** allowNull */ +); + +const TableCellFormatMetadata = createObjectDefinition>( + { + bgColorOverride: BooleanDefinition, + }, + false /* isOptional */, + true /** allowNull */ +); + +/** + * @internal + * Get the format info of a table cell + * @param cell The table cell to use + */ +export function getTableCellMetadata(cell: HTMLTableCellElement) { + return getMetadata(cell, TableCellFormatMetadata); +} + +/** + * Add metadata to a cell + * @param cell The table cell to add the metadata + * @param format The format of the table + */ +export function saveTableCellMetadata(cell: HTMLTableCellElement, format: TableCellMetadataFormat) { + if (cell && format) { + setMetadata(cell, format, TableCellFormatMetadata); + } +} diff --git a/packages/roosterjs-editor-types/lib/interface/TableCellMetadataFormat.ts b/packages/roosterjs-editor-types/lib/interface/TableCellMetadataFormat.ts new file mode 100644 index 00000000000..243ac70f964 --- /dev/null +++ b/packages/roosterjs-editor-types/lib/interface/TableCellMetadataFormat.ts @@ -0,0 +1,9 @@ +/** + * Format of table cell that stored as metadata + */ +export default interface TableCellMetadataFormat { + /** + * Override default background color + */ + bgColorOverride?: boolean; +} diff --git a/packages/roosterjs-editor-types/lib/interface/index.ts b/packages/roosterjs-editor-types/lib/interface/index.ts index a8c6e0571cb..eb2b033544d 100644 --- a/packages/roosterjs-editor-types/lib/interface/index.ts +++ b/packages/roosterjs-editor-types/lib/interface/index.ts @@ -42,6 +42,7 @@ export { } from './ContentMetadata'; export { default as Snapshot } from './Snapshot'; export { default as TableFormat } from './TableFormat'; +export { default as TableCellMetadataFormat } from './TableCellMetadataFormat'; export { default as TableSelection } from './TableSelection'; export { default as Coordinates } from './Coordinates'; export { default as HtmlSanitizerOptions } from './HtmlSanitizerOptions'; From 770d5a021a8cb710346f37de2899978c4a18f52d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Fri, 16 Sep 2022 17:55:59 -0300 Subject: [PATCH 11/41] fix maintain list chain --- .../ContentEdit/features/listFeatures.ts | 40 +++---- .../utils/getAutoNumberingListStyle.ts | 28 ++--- .../lib/plugins/Picker/PickerPlugin.ts | 3 - .../test/Picker/pickerPluginTest.ts | 107 ------------------ 4 files changed, 30 insertions(+), 148 deletions(-) delete mode 100644 packages/roosterjs-editor-plugins/test/Picker/pickerPluginTest.ts diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts index 095f0a25e86..decc04d477f 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts @@ -159,7 +159,10 @@ function isAListPattern(textBeforeCursor: string) { const AutoBullet: BuildInEditFeature = { keys: [Keys.SPACE], shouldHandleEvent: (event, editor) => { - if (!cacheGetListElement(event, editor)) { + if ( + !cacheGetListElement(event, editor) && + !editor.isFeatureEnabled(ExperimentalFeatures.AutoFormatList) + ) { let searcher = editor.getContentSearcherOfCursor(event); let textBeforeCursor = searcher.getSubStringBefore(4); @@ -261,29 +264,16 @@ const AutoNumberingList: BuildInEditFeature = { event.rawEvent.preventDefault(); editor.addUndoSnapshot( () => { - let regions: RegionBase[]; - let searcher = editor.getContentSearcherOfCursor(); - let textBeforeCursor = searcher.getSubStringBefore(5); - let textRange = searcher.getRangeFromText(textBeforeCursor, true /*exactMatch*/); + const searcher = editor.getContentSearcherOfCursor(); + const textBeforeCursor = searcher.getSubStringBefore(5); + const textRange = searcher.getRangeFromText(textBeforeCursor, true /*exactMatch*/); - if (!textRange) { - // no op if the range can't be found - } else if ((regions = editor.getSelectedRegions()) && regions.length == 1) { - const num = parseInt(textBeforeCursor); - const listStyle = getAutoNumberingListStyle(textBeforeCursor, num); - prepareAutoBullet(editor, textRange); - toggleNumbering( - editor, - num, - listStyle, - 'autoToggleList' /** apiNameOverride */ - ); - } else { + if (textRange) { const listStyle = getAutoNumberingListStyle(textBeforeCursor); prepareAutoBullet(editor, textRange); toggleNumbering( editor, - undefined /* startNumber*/, + undefined /** startNumber */, listStyle, 'autoToggleList' /** apiNameOverride */ ); @@ -374,14 +364,20 @@ function cacheGetListElement(event: PluginKeyboardEvent, editor: IEditor) { function shouldTriggerList( event: PluginKeyboardEvent, editor: IEditor, - getListStyle: (text: string) => number + getListStyle: (text: string, isTheFirstItem?: boolean) => number ) { const searcher = editor.getContentSearcherOfCursor(event); const textBeforeCursor = searcher.getSubStringBefore(4); const itHasSpace = /\s/g.test(textBeforeCursor); - + const element = editor.getElementAtCursor(); + const previousNode = editor.getBodyTraverser(element).getPreviousBlockElement(); + const isLi = previousNode + ? getTagOfNode(previousNode?.collapseToSingleElement()) === 'LI' + : false; return ( - !itHasSpace && !searcher.getNearestNonTextInlineElement() && getListStyle(textBeforeCursor) + !itHasSpace && + !searcher.getNearestNonTextInlineElement() && + getListStyle(textBeforeCursor, !isLi) ); } diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/utils/getAutoNumberingListStyle.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/utils/getAutoNumberingListStyle.ts index ae60df9b1a5..87e5d3bf226 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/utils/getAutoNumberingListStyle.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/utils/getAutoNumberingListStyle.ts @@ -21,14 +21,6 @@ const characters: Record = { ')': Character.Parenthesis, }; -const numberingTriggers: Record = { - '1': NumberingTypes.Decimal, - i: NumberingTypes.LowerRoman, - I: NumberingTypes.UpperRoman, - a: NumberingTypes.LowerAlpha, - A: NumberingTypes.UpperAlpha, -}; - const identifyNumberingType = (text: string) => { if (!isNaN(parseInt(text))) { return NumberingTypes.Decimal; @@ -92,18 +84,15 @@ const DecimalsTypes: Record = { const identifyNumberingListType = ( numbering: string, - isDoubleParenthesis: boolean, - startNumber?: number + isDoubleParenthesis: boolean ): NumberingListType | null => { const separatorCharacter = isDoubleParenthesis ? Character.DoubleParenthesis : characters[numbering[1]]; // if separator is not valid, no need to check if the number is valid. if (separatorCharacter) { - const number = numbering.length === 3 ? numbering[1] : numbering[0]; - const numberingType = startNumber - ? identifyNumberingType(number) - : numberingTriggers[number]; + const number = numbering[numbering.length - 2]; + const numberingType = identifyNumberingType(number); return numberingType ? numberingListTypes[numberingType](separatorCharacter) : null; } return null; @@ -117,15 +106,22 @@ const identifyNumberingListType = ( */ export default function getAutoNumberingListStyle( textBeforeCursor: string, - startNumber?: number + isTheFirstItem?: boolean ): NumberingListType { const trigger = textBeforeCursor.trim(); + //Only the staring items ['1', 'a', 'A', 'I', 'i'] must trigger a new list. All the other triggers is used to keep the list chain. + const listIndex = trigger[trigger.length - 2]; + const numberingTriggers = ['1', 'a', 'A', 'I', 'i']; + if (isTheFirstItem && numberingTriggers.indexOf(listIndex) < 0) { + return null; + } + // the marker must be a combination of 2 or 3 characters, so if the length is less than 2, no need to check // If the marker length is 3, the marker style is double parenthesis such as (1), (A). const isDoubleParenthesis = trigger.length === 3 && trigger[0] === '(' && trigger[2] === ')'; const numberingType = trigger.length === 2 || isDoubleParenthesis - ? identifyNumberingListType(trigger, isDoubleParenthesis, startNumber) + ? identifyNumberingListType(trigger, isDoubleParenthesis) : null; return numberingType; } diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts b/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts index 35fcf4c1898..40617495462 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts @@ -183,7 +183,6 @@ export default class PickerPlugin { - let editor: IEditor; - const TEST_ID = 'PickerTest'; - const root = document.createElement('div'); - root.id = 'test'; - root.innerText = '-'; - document.body.appendChild(root); - const options: PickerPluginOptions = { - elementIdPrefix: 'test', - changeSource: ChangeSource.SetContent, - triggerCharacter: '-', - }; - const dataProvider: PickerDataProvider = { - onInitalize: ( - insertNodeCallback: (nodeToInsert: HTMLElement) => void, - setIsSuggestingCallback: (isSuggesting: boolean) => void, - editor?: IEditor - ) => { - editor.focus(); - const editorSearchCursorSpy = spyOn(editor, 'getContentSearcherOfCursor'); - const mockedPosition = new PositionContentSearcher(root, new Position(root, 4)); - spyOn(mockedPosition, 'getSubStringBefore').and.returnValue('-'); - editorSearchCursorSpy.and.returnValue(mockedPosition); - insertNodeCallback(root); - setIsSuggestingCallback(true); - return; - }, - onDispose: () => { - return; - }, - onIsSuggestingChanged: (isSuggesting: boolean) => { - return; - }, - queryStringUpdated: (queryString: string, isExactMatch: boolean) => { - return; - }, - onContentChanged: (elementIds: string[]) => { - return; - }, - onRemove: (nodeRemoved: Node, isBackwards: boolean) => { - const node = document.createTextNode(''); - return node; - }, - }; - - let plugin: PickerPlugin; - beforeEach(() => { - plugin = new PickerPlugin(dataProvider, options); - editor = TestHelper.initEditor(TEST_ID, [plugin]); - editor.focus(); - const editorQueryElements = spyOn(editor, 'queryElements'); - editorQueryElements.and.returnValue([root]); - plugin.initialize(editor); - }); - - afterEach(() => { - document.body.removeChild(root); - editor.dispose(); - }); - - it('PickerPlugin | ContentEvent', () => { - const eventChange: PluginEvent = { - eventType: PluginEventType.ContentChanged, - source: ChangeSource.SetContent, - }; - plugin.onPluginEvent(eventChange); - spyOn(plugin.dataProvider, 'onContentChanged'); - expect(plugin.dataProvider.onContentChanged).toHaveBeenCalled(); - }); - - function keyDownTest(key: string) { - const eventChange: PluginEvent = { - eventType: PluginEventType.KeyDown, - rawEvent: { - key: key, - preventDefault: () => { - return; - }, - stopImmediatePropagation: () => { - return; - }, - }, - }; - - plugin.onPluginEvent(eventChange); - spyOn(eventChange.rawEvent, 'preventDefault'); - spyOn(eventChange.rawEvent, 'stopImmediatePropagation'); - expect(eventChange.rawEvent.preventDefault).toHaveBeenCalled(); - expect(eventChange.rawEvent.stopImmediatePropagation).toHaveBeenCalled(); - } - - it('PickerPlugin | KeyDownESC', () => { - keyDownTest('Esc'); - }); -}); From e4cc2168cff84659facd33935856ca8d09b3fd79 Mon Sep 17 00:00:00 2001 From: Bryan Valverde U Date: Fri, 16 Sep 2022 16:57:37 -0600 Subject: [PATCH 12/41] Remove Table Selection on Delete Row/Columns (#1260) --- .../selection/collapseTableSelection.ts | 18 ++++++++ .../lib/modelApi/table/deleteTableColumn.ts | 4 +- .../lib/modelApi/table/deleteTableRow.ts | 4 +- .../selection/collapseTableSelectionTest.ts | 39 +++++++++++++++++ .../modelApi/table/deleteTableColumnTest.ts | 43 +++++++++++++++++++ .../test/modelApi/table/deleteTableRowTest.ts | 41 ++++++++++++++++++ 6 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 packages/roosterjs-content-model/lib/modelApi/selection/collapseTableSelection.ts create mode 100644 packages/roosterjs-content-model/test/modelApi/selection/collapseTableSelectionTest.ts diff --git a/packages/roosterjs-content-model/lib/modelApi/selection/collapseTableSelection.ts b/packages/roosterjs-content-model/lib/modelApi/selection/collapseTableSelection.ts new file mode 100644 index 00000000000..1d5d6e41f6f --- /dev/null +++ b/packages/roosterjs-content-model/lib/modelApi/selection/collapseTableSelection.ts @@ -0,0 +1,18 @@ +import { addSegment } from '../common/addSegment'; +import { ContentModelTableCell } from '../../publicTypes/block/group/ContentModelTableCell'; +import { createSelectionMarker } from '../creators/createSelectionMarker'; +import { TableSelectionCoordinates } from './setSelectionToTable'; + +/** + * @internal + */ +export function collapseTableSelection( + cells: ContentModelTableCell[][], + selection: TableSelectionCoordinates +) { + const { firstCol, firstRow } = selection; + const cell = cells[firstRow]?.[firstCol]; + if (cell) { + addSegment(cell, createSelectionMarker()); + } +} diff --git a/packages/roosterjs-content-model/lib/modelApi/table/deleteTableColumn.ts b/packages/roosterjs-content-model/lib/modelApi/table/deleteTableColumn.ts index a5509d5fcd4..2ae4fa075da 100644 --- a/packages/roosterjs-content-model/lib/modelApi/table/deleteTableColumn.ts +++ b/packages/roosterjs-content-model/lib/modelApi/table/deleteTableColumn.ts @@ -1,6 +1,6 @@ +import { collapseTableSelection } from '../selection/collapseTableSelection'; import { ContentModelTable } from '../../publicTypes/block/ContentModelTable'; import { getSelectedCells } from './getSelectedCells'; -import { setSelectionToTable } from '../selection/setSelectionToTable'; /** * @internal @@ -21,6 +21,6 @@ export function deleteTableColumn(table: ContentModelTable) { } table.widths.splice(sel.firstCol, sel.lastCol - sel.firstCol + 1); - setSelectionToTable(table.cells, sel); + collapseTableSelection(table.cells, sel); } } diff --git a/packages/roosterjs-content-model/lib/modelApi/table/deleteTableRow.ts b/packages/roosterjs-content-model/lib/modelApi/table/deleteTableRow.ts index f1944ce9273..8d8ce376c0e 100644 --- a/packages/roosterjs-content-model/lib/modelApi/table/deleteTableRow.ts +++ b/packages/roosterjs-content-model/lib/modelApi/table/deleteTableRow.ts @@ -1,6 +1,6 @@ +import { collapseTableSelection } from '../selection/collapseTableSelection'; import { ContentModelTable } from '../../publicTypes/block/ContentModelTable'; import { getSelectedCells } from './getSelectedCells'; -import { setSelectionToTable } from '../selection/setSelectionToTable'; /** * @internal @@ -20,6 +20,6 @@ export function deleteTableRow(table: ContentModelTable) { table.cells.splice(sel.firstRow, sel.lastRow - sel.firstRow + 1); table.heights.splice(sel.firstRow, sel.lastRow - sel.firstRow + 1); - setSelectionToTable(table.cells, sel); + collapseTableSelection(table.cells, sel); } } diff --git a/packages/roosterjs-content-model/test/modelApi/selection/collapseTableSelectionTest.ts b/packages/roosterjs-content-model/test/modelApi/selection/collapseTableSelectionTest.ts new file mode 100644 index 00000000000..510e80524b1 --- /dev/null +++ b/packages/roosterjs-content-model/test/modelApi/selection/collapseTableSelectionTest.ts @@ -0,0 +1,39 @@ +import * as addSegment from '../../../lib/modelApi/common/addSegment'; +import * as createSelectionMarker from '../../../lib/modelApi/creators/createSelectionMarker'; +import { collapseTableSelection } from '../../../lib/modelApi/selection/collapseTableSelection'; +import { ContentModelSelectionMarker } from '../../../lib/publicTypes/segment/ContentModelSelectionMarker'; +import { createTable } from '../../../lib/modelApi/creators/createTable'; +import { createTableCell } from '../../../lib/modelApi/creators/createTableCell'; + +describe('collapseTableSelection', () => { + it('Collapse Selection to first cell', () => { + const selectionMarker = {}; + spyOn(createSelectionMarker, 'createSelectionMarker').and.returnValue(selectionMarker); + spyOn(addSegment, 'addSegment'); + + const table = createTable(1); + const cell1 = createTableCell(); + const cell2 = createTableCell(); + table.cells[0].push(cell1, cell2); + + collapseTableSelection(table.cells, { firstCol: 0, firstRow: 0, lastCol: 0, lastRow: 0 }); + + expect(addSegment.addSegment).toHaveBeenCalledWith(table.cells[0][0], selectionMarker); + }); + + it('First cell undefined, do not collapse selection', () => { + const selectionMarker = {}; + spyOn(createSelectionMarker, 'createSelectionMarker').and.returnValue(selectionMarker); + spyOn(addSegment, 'addSegment'); + + const table = createTable(1); + const cell1 = createTableCell(); + const cell2 = createTableCell(); + table.cells[0].push(cell1, cell2); + + collapseTableSelection(table.cells, { firstCol: 1, firstRow: 1, lastCol: 0, lastRow: 0 }); + + expect(createSelectionMarker.createSelectionMarker).not.toHaveBeenCalled(); + expect(addSegment.addSegment).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/roosterjs-content-model/test/modelApi/table/deleteTableColumnTest.ts b/packages/roosterjs-content-model/test/modelApi/table/deleteTableColumnTest.ts index 3cc1ad85183..c9cec6b53c3 100644 --- a/packages/roosterjs-content-model/test/modelApi/table/deleteTableColumnTest.ts +++ b/packages/roosterjs-content-model/test/modelApi/table/deleteTableColumnTest.ts @@ -1,3 +1,4 @@ +import hasSelectionInBlock from '../../../lib/publicApi/selection/hasSelectionInBlock'; import { createTable } from '../../../lib/modelApi/creators/createTable'; import { createTableCell } from '../../../lib/modelApi/creators/createTableCell'; import { deleteTableColumn } from '../../../lib/modelApi/table/deleteTableColumn'; @@ -47,6 +48,8 @@ describe('deleteTableColumn', () => { widths: [], heights: [], }); + + expect(cell2.blocks.some(hasSelectionInBlock)).toBeTrue(); }); it('table with selection in middle', () => { @@ -66,6 +69,9 @@ describe('deleteTableColumn', () => { widths: [], heights: [], }); + + expect(cell1.blocks.some(hasSelectionInBlock)).toBeFalse(); + expect(cell3.blocks.some(hasSelectionInBlock)).toBeTrue(); }); it('table with selection at end', () => { @@ -85,6 +91,9 @@ describe('deleteTableColumn', () => { widths: [], heights: [], }); + + expect(cell1.blocks.some(hasSelectionInBlock)).toBeFalse(); + expect(cell2.blocks.some(hasSelectionInBlock)).toBeFalse(); }); it('table with multiple selection', () => { @@ -105,6 +114,8 @@ describe('deleteTableColumn', () => { widths: [], heights: [], }); + + expect(cell1.blocks.some(hasSelectionInBlock)).toBeFalse(); }); it('table with full selection', () => { @@ -149,6 +160,10 @@ describe('deleteTableColumn', () => { expect(cell1.spanLeft).toBeFalse(); expect(cell3.spanLeft).toBeFalse(); expect(cell4.spanLeft).toBeFalse(); + + expect(cell1.blocks.some(hasSelectionInBlock)).toBeFalse(); + expect(cell3.blocks.some(hasSelectionInBlock)).toBeTrue(); + expect(cell4.blocks.some(hasSelectionInBlock)).toBeFalse(); }); it('table with selection spanned cell - 2', () => { @@ -173,6 +188,10 @@ describe('deleteTableColumn', () => { expect(cell1.spanLeft).toBeFalse(); expect(cell3.spanLeft).toBeFalse(); expect(cell4.spanLeft).toBeFalse(); + + expect(cell1.blocks.some(hasSelectionInBlock)).toBeFalse(); + expect(cell3.blocks.some(hasSelectionInBlock)).toBeTrue(); + expect(cell4.blocks.some(hasSelectionInBlock)).toBeFalse(); }); it('table with selection spanned cell - 3', () => { @@ -197,6 +216,10 @@ describe('deleteTableColumn', () => { expect(cell1.spanLeft).toBeFalse(); expect(cell3.spanLeft).toBeTrue(); expect(cell4.spanLeft).toBeFalse(); + + expect(cell1.blocks.some(hasSelectionInBlock)).toBeFalse(); + expect(cell3.blocks.some(hasSelectionInBlock)).toBeTrue(); + expect(cell4.blocks.some(hasSelectionInBlock)).toBeFalse(); }); it('table with selection spanned cell - 4', () => { @@ -221,6 +244,10 @@ describe('deleteTableColumn', () => { expect(cell1.spanLeft).toBeFalse(); expect(cell2.spanLeft).toBeTrue(); expect(cell4.spanLeft).toBeFalse(); + + expect(cell1.blocks.some(hasSelectionInBlock)).toBeFalse(); + expect(cell2.blocks.some(hasSelectionInBlock)).toBeFalse(); + expect(cell4.blocks.some(hasSelectionInBlock)).toBeTrue(); }); it('table with selection and multi columns', () => { @@ -242,6 +269,9 @@ describe('deleteTableColumn', () => { widths: [], heights: [], }); + + expect(cell2.blocks.some(hasSelectionInBlock)).toBeTrue(); + expect(cell4.blocks.some(hasSelectionInBlock)).toBeFalse(); }); it('table with selection and multi columns and colspan', () => { @@ -266,6 +296,9 @@ describe('deleteTableColumn', () => { expect(cell2.spanAbove).toBeFalse(); expect(cell4.spanAbove).toBeTrue(); + + expect(cell2.blocks.some(hasSelectionInBlock)).toBeTrue(); + expect(cell4.blocks.some(hasSelectionInBlock)).toBeFalse(); }); it('table with selection and multi columns and colspan', () => { @@ -290,6 +323,9 @@ describe('deleteTableColumn', () => { expect(cell2.spanAbove).toBeFalse(); expect(cell4.spanAbove).toBeFalse(); + + expect(cell2.blocks.some(hasSelectionInBlock)).toBeTrue(); + expect(cell4.blocks.some(hasSelectionInBlock)).toBeFalse(); }); it('table with selection and multi columns and multi span', () => { @@ -334,5 +370,12 @@ describe('deleteTableColumn', () => { expect(cell8.spanAbove).toBeFalse(); expect(cell9.spanLeft).toBeFalse(); expect(cell9.spanAbove).toBeFalse(); + + expect(cell8.blocks.some(hasSelectionInBlock)).toBeTrue(); + expect(cell2.blocks.some(hasSelectionInBlock)).toBeFalse(); + expect(cell3.blocks.some(hasSelectionInBlock)).toBeFalse(); + expect(cell5.blocks.some(hasSelectionInBlock)).toBeFalse(); + expect(cell6.blocks.some(hasSelectionInBlock)).toBeFalse(); + expect(cell9.blocks.some(hasSelectionInBlock)).toBeFalse(); }); }); diff --git a/packages/roosterjs-content-model/test/modelApi/table/deleteTableRowTest.ts b/packages/roosterjs-content-model/test/modelApi/table/deleteTableRowTest.ts index cb9f81062eb..59d322242c0 100644 --- a/packages/roosterjs-content-model/test/modelApi/table/deleteTableRowTest.ts +++ b/packages/roosterjs-content-model/test/modelApi/table/deleteTableRowTest.ts @@ -1,3 +1,4 @@ +import hasSelectionInBlock from '../../../lib/publicApi/selection/hasSelectionInBlock'; import { createTable } from '../../../lib/modelApi/creators/createTable'; import { createTableCell } from '../../../lib/modelApi/creators/createTableCell'; import { deleteTableRow } from '../../../lib/modelApi/table/deleteTableRow'; @@ -68,6 +69,9 @@ describe('deleteTableRow', () => { widths: [], heights: [], }); + + expect(cell3.blocks.some(hasSelectionInBlock)).toBeTrue(); + expect(cell1.blocks.some(hasSelectionInBlock)).toBeFalse(); }); it('table with selection at end', () => { @@ -89,6 +93,9 @@ describe('deleteTableRow', () => { widths: [], heights: [], }); + + expect(cell3.blocks.some(hasSelectionInBlock)).toBeFalse(); + expect(cell1.blocks.some(hasSelectionInBlock)).toBeFalse(); }); it('table with multiple selection', () => { @@ -111,6 +118,8 @@ describe('deleteTableRow', () => { widths: [], heights: [], }); + + expect(cell1.blocks.some(hasSelectionInBlock)).toBeFalse(); }); it('table with full selection', () => { @@ -160,6 +169,10 @@ describe('deleteTableRow', () => { expect(cell1.spanAbove).toBeFalse(); expect(cell3.spanAbove).toBeFalse(); expect(cell4.spanAbove).toBeFalse(); + + expect(cell1.blocks.some(hasSelectionInBlock)).toBeFalse(); + expect(cell3.blocks.some(hasSelectionInBlock)).toBeTrue(); + expect(cell4.blocks.some(hasSelectionInBlock)).toBeFalse(); }); it('table with selection spanned cell - 2', () => { @@ -187,6 +200,10 @@ describe('deleteTableRow', () => { expect(cell1.spanAbove).toBeFalse(); expect(cell3.spanAbove).toBeFalse(); expect(cell4.spanAbove).toBeFalse(); + + expect(cell1.blocks.some(hasSelectionInBlock)).toBeFalse(); + expect(cell3.blocks.some(hasSelectionInBlock)).toBeTrue(); + expect(cell4.blocks.some(hasSelectionInBlock)).toBeFalse(); }); it('table with selection spanned cell - 3', () => { @@ -214,6 +231,10 @@ describe('deleteTableRow', () => { expect(cell1.spanAbove).toBeFalse(); expect(cell3.spanAbove).toBeTrue(); expect(cell4.spanAbove).toBeFalse(); + + expect(cell1.blocks.some(hasSelectionInBlock)).toBeFalse(); + expect(cell3.blocks.some(hasSelectionInBlock)).toBeTrue(); + expect(cell4.blocks.some(hasSelectionInBlock)).toBeFalse(); }); it('table with selection spanned cell - 4', () => { @@ -241,6 +262,10 @@ describe('deleteTableRow', () => { expect(cell1.spanAbove).toBeFalse(); expect(cell2.spanAbove).toBeTrue(); expect(cell4.spanAbove).toBeFalse(); + + expect(cell1.blocks.some(hasSelectionInBlock)).toBeFalse(); + expect(cell2.blocks.some(hasSelectionInBlock)).toBeFalse(); + expect(cell4.blocks.some(hasSelectionInBlock)).toBeTrue(); }); it('table with selection and multi columns', () => { @@ -262,6 +287,9 @@ describe('deleteTableRow', () => { widths: [], heights: [], }); + + expect(cell3.blocks.some(hasSelectionInBlock)).toBeTrue(); + expect(cell4.blocks.some(hasSelectionInBlock)).toBeFalse(); }); it('table with selection and multi columns and colspan', () => { @@ -286,6 +314,9 @@ describe('deleteTableRow', () => { expect(cell3.spanLeft).toBeFalse(); expect(cell4.spanLeft).toBeTrue(); + + expect(cell3.blocks.some(hasSelectionInBlock)).toBeTrue(); + expect(cell4.blocks.some(hasSelectionInBlock)).toBeFalse(); }); it('table with selection and multi columns and colspan', () => { @@ -310,6 +341,9 @@ describe('deleteTableRow', () => { expect(cell3.spanLeft).toBeFalse(); expect(cell4.spanLeft).toBeFalse(); + + expect(cell3.blocks.some(hasSelectionInBlock)).toBeTrue(); + expect(cell4.blocks.some(hasSelectionInBlock)).toBeFalse(); }); it('table with selection and multi columns and multi span', () => { @@ -353,5 +387,12 @@ describe('deleteTableRow', () => { expect(cell8.spanAbove).toBeFalse(); expect(cell9.spanLeft).toBeFalse(); expect(cell9.spanAbove).toBeFalse(); + + expect(cell6.blocks.some(hasSelectionInBlock)).toBeTrue(); + expect(cell4.blocks.some(hasSelectionInBlock)).toBeFalse(); + expect(cell5.blocks.some(hasSelectionInBlock)).toBeFalse(); + expect(cell7.blocks.some(hasSelectionInBlock)).toBeFalse(); + expect(cell8.blocks.some(hasSelectionInBlock)).toBeFalse(); + expect(cell9.blocks.some(hasSelectionInBlock)).toBeFalse(); }); }); From 5391146a9ff1b7d759584d2f630e724e692ac5b3 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Fri, 16 Sep 2022 19:16:24 -0700 Subject: [PATCH 13/41] ContentModel: BlockGroup is not Block (#1259) * Reorganize some code * fix build * Move some code * Remove unnecessary change * More fix * fix comment * Customization 2 * temporarily allow empty interface * ignore a lint rule * Customization step 3 * BlockGroup is not Block --- .../model/ContentModelDocumentView.tsx | 4 +- .../model/ContentModelTableCellView.tsx | 4 +- .../model/ContentModelTableView.tsx | 4 +- packages/roosterjs-content-model/lib/index.ts | 1 + .../lib/modelApi/common/isEmpty.ts | 77 +++++++++++++++++++ .../modelApi/common/normalizeContentModel.ts | 24 +----- .../creators/createContentModelDocument.ts | 1 - .../lib/modelApi/creators/createTableCell.ts | 1 - .../lib/modelApi/table/getSelectedCells.ts | 4 +- .../table/setTableCellBackgroundColor.ts | 4 +- .../lib/modelToDom/handlers/handleBlock.ts | 40 +--------- .../modelToDom/handlers/handleBlockGroup.ts | 56 ++++++++++++++ .../lib/modelToDom/handlers/handleTable.ts | 7 +- .../lib/publicApi/contentModelToDom.ts | 4 +- .../selection/hasSelectionInBlock.ts | 13 +--- .../selection/hasSelectionInBlockGroup.ts | 18 +++++ .../publicTypes/block/ContentModelBlock.ts | 7 +- .../block/group/ContentModelBlockGroupBase.ts | 4 +- .../block/group/ContentModelGeneralBlock.ts | 5 +- .../domToModel/processors/brProcessorTest.ts | 2 - .../processors/containerProcessorTest.ts | 5 -- .../processors/fontProcessorTest.ts | 1 - .../processors/generalBlockProcessorTest.ts | 1 - .../processors/generalSegmentProcessorTest.ts | 2 - .../processors/tableProcessorTest.ts | 5 -- .../processors/textProcessorTest.ts | 4 - .../test/modelApi/common/addBlockTest.ts | 1 - .../test/modelApi/common/addSegmentTest.ts | 4 - .../test/modelApi/creators/creatorsTest.ts | 8 -- .../modelApi/table/applyTableFormatTest.ts | 1 - .../table/createTableStructureTest.ts | 1 - .../test/modelApi/table/normalizeTableTest.ts | 13 ---- .../table/setTableCellBackgroundColorTest.ts | 16 ---- .../selection/hasSelectionInBlockTest.ts | 11 +-- 34 files changed, 190 insertions(+), 163 deletions(-) create mode 100644 packages/roosterjs-content-model/lib/modelApi/common/isEmpty.ts create mode 100644 packages/roosterjs-content-model/lib/modelToDom/handlers/handleBlockGroup.ts create mode 100644 packages/roosterjs-content-model/lib/publicApi/selection/hasSelectionInBlockGroup.ts diff --git a/demo/scripts/controls/contentModel/components/model/ContentModelDocumentView.tsx b/demo/scripts/controls/contentModel/components/model/ContentModelDocumentView.tsx index 3733ebf253c..7f1f737abfb 100644 --- a/demo/scripts/controls/contentModel/components/model/ContentModelDocumentView.tsx +++ b/demo/scripts/controls/contentModel/components/model/ContentModelDocumentView.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { BlockGroupContentView } from './BlockGroupContentView'; -import { ContentModelDocument, hasSelectionInBlock } from 'roosterjs-content-model'; +import { ContentModelDocument, hasSelectionInBlockGroup } from 'roosterjs-content-model'; import { ContentModelView } from '../ContentModelView'; const styles = require('./ContentModelDocumentView.scss'); @@ -16,7 +16,7 @@ export function ContentModelDocumentView(props: { doc: ContentModelDocument }) { title="Document" subTitle={doc.document.location.href} className={styles.modelDocument} - hasSelection={hasSelectionInBlock(doc)} + hasSelection={hasSelectionInBlockGroup(doc)} jsonSource={doc} getContent={getContent} /> diff --git a/demo/scripts/controls/contentModel/components/model/ContentModelTableCellView.tsx b/demo/scripts/controls/contentModel/components/model/ContentModelTableCellView.tsx index 0ac165120e8..cd8d19ef09e 100644 --- a/demo/scripts/controls/contentModel/components/model/ContentModelTableCellView.tsx +++ b/demo/scripts/controls/contentModel/components/model/ContentModelTableCellView.tsx @@ -12,7 +12,7 @@ import { VerticalAlignFormatRenderer } from '../format/formatPart/VerticalAlignF import { ContentModelTableCell, ContentModelTableCellFormat, - hasSelectionInBlock, + hasSelectionInBlockGroup, } from 'roosterjs-content-model'; const styles = require('./ContentModelTableCellView.scss'); @@ -99,7 +99,7 @@ export function ContentModelTableCellView(props: { cell: ContentModelTableCell } title={isHeader ? 'TableCellHeader' : 'TableCell'} subTitle={subTitle} className={styles.modelTableCell} - hasSelection={hasSelectionInBlock(cell)} + hasSelection={hasSelectionInBlockGroup(cell)} isSelected={cell.isSelected} jsonSource={cell} getContent={getContent} diff --git a/demo/scripts/controls/contentModel/components/model/ContentModelTableView.tsx b/demo/scripts/controls/contentModel/components/model/ContentModelTableView.tsx index f048e5c2a9d..187091b1891 100644 --- a/demo/scripts/controls/contentModel/components/model/ContentModelTableView.tsx +++ b/demo/scripts/controls/contentModel/components/model/ContentModelTableView.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { applyTableFormat } from 'roosterjs-content-model/lib/modelApi/table/applyTableFormat'; import { BackgroundColorFormatRenderer } from '../format/formatPart/BackgroundColorFormatRenderer'; import { BorderFormatRenderers } from '../format/formatPart/BorderFormatRenderers'; -import { ContentModelBlockView } from './ContentModelBlockView'; +import { ContentModelBlockGroupView } from './ContentModelBlockGroupView'; import { ContentModelView } from '../ContentModelView'; import { FormatRenderer } from '../format/utils/FormatRenderer'; import { FormatView } from '../format/FormatView'; @@ -48,7 +48,7 @@ export function ContentModelTableView(props: { table: ContentModelTable }) { {table.cells.map((row, i) => (
{row.map((cell, j) => ( - + ))}
))} diff --git a/packages/roosterjs-content-model/lib/index.ts b/packages/roosterjs-content-model/lib/index.ts index 220f3fabc54..a23506a6b1b 100644 --- a/packages/roosterjs-content-model/lib/index.ts +++ b/packages/roosterjs-content-model/lib/index.ts @@ -6,6 +6,7 @@ export { default as setTableCellShade } from './publicApi/table/setTableCellShad export { default as editTable } from './publicApi/table/editTable'; export { default as hasSelectionInBlock } from './publicApi/selection/hasSelectionInBlock'; export { default as hasSelectionInSegment } from './publicApi/selection/hasSelectionInSegment'; +export { default as hasSelectionInBlockGroup } from './publicApi/selection/hasSelectionInBlockGroup'; export { extractBorderValues, combineBorderValue, BorderIndex } from './domUtils/borderValues'; diff --git a/packages/roosterjs-content-model/lib/modelApi/common/isEmpty.ts b/packages/roosterjs-content-model/lib/modelApi/common/isEmpty.ts new file mode 100644 index 00000000000..34db78ff968 --- /dev/null +++ b/packages/roosterjs-content-model/lib/modelApi/common/isEmpty.ts @@ -0,0 +1,77 @@ +import { ContentModelBlock } from '../../publicTypes/block/ContentModelBlock'; +import { ContentModelBlockGroup } from '../../publicTypes/block/group/ContentModelBlockGroup'; +import { ContentModelSegment } from '../../publicTypes/segment/ContentModelSegment'; + +/** + * @internal + */ +export function isBlockEmpty(block: ContentModelBlock): boolean { + switch (block.blockType) { + case 'Paragraph': + return block.segments.length == 0; + + case 'Table': + return block.cells.every(row => row.length == 0); + + case 'BlockGroup': + return isBlockGroupEmpty(block); + + default: + return false; + } +} + +/** + * @internal + */ +export function isBlockGroupEmpty(group: ContentModelBlockGroup): boolean { + return group.blocks.every(isBlockEmpty); +} + +/** + * @internal + */ +export function isSegmentEmpty(segment: ContentModelSegment): boolean { + switch (segment.segmentType) { + case 'Text': + return !segment.text || /^[\r\n]*$/.test(segment.text); + + default: + return false; + } +} + +/** + * @internal + */ +export function isEmpty( + model: ContentModelBlock | ContentModelBlockGroup | ContentModelSegment +): boolean { + if (isBlockGroup(model)) { + return isBlockGroupEmpty(model); + } else if (isBlock(model)) { + return isBlockEmpty(model); + } else if (isSegment(model)) { + return isSegmentEmpty(model); + } + + return false; +} + +function isSegment( + model: ContentModelBlock | ContentModelBlockGroup | ContentModelSegment +): model is ContentModelSegment { + return typeof (model).segmentType === 'string'; +} + +function isBlock( + model: ContentModelBlock | ContentModelBlockGroup | ContentModelSegment +): model is ContentModelBlock { + return typeof (model).blockType === 'string'; +} + +function isBlockGroup( + model: ContentModelBlock | ContentModelBlockGroup | ContentModelSegment +): model is ContentModelBlockGroup { + return typeof (model).blockGroupType === 'string'; +} diff --git a/packages/roosterjs-content-model/lib/modelApi/common/normalizeContentModel.ts b/packages/roosterjs-content-model/lib/modelApi/common/normalizeContentModel.ts index 321a13cf6f6..47dcc633350 100644 --- a/packages/roosterjs-content-model/lib/modelApi/common/normalizeContentModel.ts +++ b/packages/roosterjs-content-model/lib/modelApi/common/normalizeContentModel.ts @@ -1,6 +1,5 @@ -import { ContentModelBlock } from '../../publicTypes/block/ContentModelBlock'; import { ContentModelBlockGroup } from '../../publicTypes/block/group/ContentModelBlockGroup'; -import { ContentModelSegment } from '../../publicTypes/segment/ContentModelSegment'; +import { isBlockEmpty, isSegmentEmpty } from './isEmpty'; /** * @internal @@ -15,7 +14,7 @@ export function normalizeModel(group: ContentModelBlockGroup) { break; case 'Paragraph': for (let j = block.segments.length - 1; j >= 0; j--) { - if (isEmptySegment(block.segments[j])) { + if (isSegmentEmpty(block.segments[j])) { block.segments.splice(j, 1); } } @@ -29,25 +28,8 @@ export function normalizeModel(group: ContentModelBlockGroup) { break; } - if (isEmptyBlock(block)) { + if (isBlockEmpty(block)) { group.blocks.splice(i, 1); } } } - -function isEmptySegment(segment: ContentModelSegment) { - return segment.segmentType == 'Text' && (!segment.text || /^[\r\n]*$/.test(segment.text)); -} - -function isEmptyBlock(block: ContentModelBlock) { - switch (block.blockType) { - case 'Paragraph': - return block.segments.length == 0; - - case 'Table': - return block.cells.length == 0 || block.cells.every(row => row.length == 0); - - default: - return false; - } -} diff --git a/packages/roosterjs-content-model/lib/modelApi/creators/createContentModelDocument.ts b/packages/roosterjs-content-model/lib/modelApi/creators/createContentModelDocument.ts index 9b17ab6fc59..2014737b2e3 100644 --- a/packages/roosterjs-content-model/lib/modelApi/creators/createContentModelDocument.ts +++ b/packages/roosterjs-content-model/lib/modelApi/creators/createContentModelDocument.ts @@ -5,7 +5,6 @@ import { ContentModelDocument } from '../../publicTypes/block/group/ContentModel */ export function createContentModelDocument(doc: Document): ContentModelDocument { return { - blockType: 'BlockGroup', blockGroupType: 'Document', blocks: [], document: doc, diff --git a/packages/roosterjs-content-model/lib/modelApi/creators/createTableCell.ts b/packages/roosterjs-content-model/lib/modelApi/creators/createTableCell.ts index 78096dd73d2..641bf592e5c 100644 --- a/packages/roosterjs-content-model/lib/modelApi/creators/createTableCell.ts +++ b/packages/roosterjs-content-model/lib/modelApi/creators/createTableCell.ts @@ -15,7 +15,6 @@ export function createTableCell( const spanAbove = typeof spanAboveOrRowSpan === 'number' ? spanAboveOrRowSpan > 1 : !!spanAboveOrRowSpan; return { - blockType: 'BlockGroup', blockGroupType: 'TableCell', blocks: [], format: format ? { ...format } : {}, diff --git a/packages/roosterjs-content-model/lib/modelApi/table/getSelectedCells.ts b/packages/roosterjs-content-model/lib/modelApi/table/getSelectedCells.ts index b9c97a86dfa..77fda1f765c 100644 --- a/packages/roosterjs-content-model/lib/modelApi/table/getSelectedCells.ts +++ b/packages/roosterjs-content-model/lib/modelApi/table/getSelectedCells.ts @@ -1,4 +1,4 @@ -import hasSelectionInBlock from '../../publicApi/selection/hasSelectionInBlock'; +import hasSelectionInBlockGroup from '../../publicApi/selection/hasSelectionInBlockGroup'; import { ContentModelTable } from '../../publicTypes/block/ContentModelTable'; import { TableSelectionCoordinates } from '../selection/setSelectionToTable'; @@ -14,7 +14,7 @@ export function getSelectedCells(table: ContentModelTable): TableSelectionCoordi table.cells.forEach((row, rowIndex) => row.forEach((cell, colIndex) => { - if (hasSelectionInBlock(cell)) { + if (hasSelectionInBlockGroup(cell)) { hasSelection = true; if (firstRow < 0) { diff --git a/packages/roosterjs-content-model/lib/modelApi/table/setTableCellBackgroundColor.ts b/packages/roosterjs-content-model/lib/modelApi/table/setTableCellBackgroundColor.ts index dcb79efc83b..72b638836da 100644 --- a/packages/roosterjs-content-model/lib/modelApi/table/setTableCellBackgroundColor.ts +++ b/packages/roosterjs-content-model/lib/modelApi/table/setTableCellBackgroundColor.ts @@ -1,4 +1,4 @@ -import hasSelectionInBlock from '../../publicApi/selection/hasSelectionInBlock'; +import hasSelectionInBlockGroup from '../../publicApi/selection/hasSelectionInBlockGroup'; import { ContentModelTable } from '../../publicTypes/block/ContentModelTable'; /** @@ -7,7 +7,7 @@ import { ContentModelTable } from '../../publicTypes/block/ContentModelTable'; export function setTableCellBackgroundColor(table: ContentModelTable, color: string) { table.cells.forEach(row => row.forEach(cell => { - if (hasSelectionInBlock(cell)) { + if (hasSelectionInBlockGroup(cell)) { cell.format.backgroundColor = color; cell.format.bgColorOverride = true; } diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleBlock.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleBlock.ts index 29cfb2fd648..d62efc93e64 100644 --- a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleBlock.ts +++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleBlock.ts @@ -1,14 +1,8 @@ -import { applyFormat } from '../utils/applyFormat'; import { ContentModelBlock } from '../../publicTypes/block/ContentModelBlock'; -import { ContentModelBlockGroup } from '../../publicTypes/block/group/ContentModelBlockGroup'; -import { ContentModelGeneralBlock } from '../../publicTypes/block/group/ContentModelGeneralBlock'; -import { ContentModelGeneralSegment } from '../../publicTypes/segment/ContentModelGeneralSegment'; +import { handleBlockGroup } from './handleBlockGroup'; import { handleParagraph } from './handleParagraph'; import { handleTable } from './handleTable'; -import { isNodeOfType } from '../../domUtils/isNodeOfType'; import { ModelToDomContext } from '../../publicTypes/context/ModelToDomContext'; -import { NodeType } from 'roosterjs-editor-types'; -import { SegmentFormatHandlers } from '../../formatHandlers/SegmentFormatHandlers'; /** * @internal @@ -25,40 +19,10 @@ export function handleBlock( break; case 'BlockGroup': - switch (block.blockGroupType) { - case 'General': - const newParent = block.element.cloneNode(); - parent.appendChild(newParent); - - handleBlockGroup(doc, newParent, block, context); - - if (isGeneralSegment(block) && isNodeOfType(newParent, NodeType.Element)) { - context.regularSelection.current.segment = newParent; - applyFormat(newParent, SegmentFormatHandlers, block.format, context); - } - - break; - default: - handleBlockGroup(doc, parent, block, context); - break; - } - + handleBlockGroup(doc, parent, block, context); break; case 'Paragraph': handleParagraph(doc, parent, block, context); break; } } - -function handleBlockGroup( - doc: Document, - parent: Node, - group: ContentModelBlockGroup, - context: ModelToDomContext -) { - group.blocks.forEach(childBlock => handleBlock(doc, parent, childBlock, context)); -} - -function isGeneralSegment(block: ContentModelGeneralBlock): block is ContentModelGeneralSegment { - return (block as ContentModelGeneralSegment).segmentType == 'General'; -} diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleBlockGroup.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleBlockGroup.ts new file mode 100644 index 00000000000..f2665137a9c --- /dev/null +++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleBlockGroup.ts @@ -0,0 +1,56 @@ +import { applyFormat } from '../utils/applyFormat'; +import { ContentModelBlockGroup } from '../../publicTypes/block/group/ContentModelBlockGroup'; +import { ContentModelGeneralBlock } from '../../publicTypes/block/group/ContentModelGeneralBlock'; +import { ContentModelGeneralSegment } from '../../publicTypes/segment/ContentModelGeneralSegment'; +import { handleBlock } from './handleBlock'; +import { isNodeOfType } from '../../domUtils/isNodeOfType'; +import { ModelToDomContext } from '../../publicTypes/context/ModelToDomContext'; +import { NodeType } from 'roosterjs-editor-types'; +import { SegmentFormatHandlers } from '../../formatHandlers/SegmentFormatHandlers'; + +/** + * @internal + */ +export function handleBlockGroup( + doc: Document, + parent: Node, + group: ContentModelBlockGroup, + context: ModelToDomContext +) { + switch (group.blockGroupType) { + case 'General': + const newParent = group.element.cloneNode(); + parent.appendChild(newParent); + + handleBlockGroupChildren(doc, newParent, group, context); + + if (isGeneralSegment(group) && isNodeOfType(newParent, NodeType.Element)) { + context.regularSelection.current.segment = newParent; + applyFormat(newParent, SegmentFormatHandlers, group.format, context); + } + + break; + + default: + handleBlockGroupChildren(doc, parent, group, context); + break; + } +} + +/** + * @internal + */ +export function handleBlockGroupChildren( + doc: Document, + parent: Node, + group: ContentModelBlockGroup, + context: ModelToDomContext +) { + group.blocks.forEach(childBlock => { + handleBlock(doc, parent, childBlock, context); + }); +} + +function isGeneralSegment(block: ContentModelGeneralBlock): block is ContentModelGeneralSegment { + return (block as ContentModelGeneralSegment).segmentType == 'General'; +} diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleTable.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleTable.ts index cefb7ef88c5..fb8bf4a3144 100644 --- a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleTable.ts +++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleTable.ts @@ -1,6 +1,7 @@ import { applyFormat } from '../utils/applyFormat'; import { ContentModelTable } from '../../publicTypes/block/ContentModelTable'; -import { handleBlock } from './handleBlock'; +import { handleBlockGroup } from './handleBlockGroup'; +import { isBlockEmpty } from '../../modelApi/common/isEmpty'; import { ModelToDomContext } from '../../publicTypes/context/ModelToDomContext'; import { TableCellFormatHandlers } from '../../formatHandlers/TableCellFormatHandler'; import { TableFormatHandlers } from '../../formatHandlers/TableFormatHandlers'; @@ -14,7 +15,7 @@ export function handleTable( table: ContentModelTable, context: ModelToDomContext ) { - if (table.cells.length == 0 || table.cells.every(c => c.length == 0)) { + if (isBlockEmpty(table)) { // Empty table, do not create TABLE element and just return return; } @@ -81,7 +82,7 @@ export function handleTable( td.colSpan = colSpan; } - handleBlock(doc, td, cell, context); + handleBlockGroup(doc, td, cell, context); } } } diff --git a/packages/roosterjs-content-model/lib/publicApi/contentModelToDom.ts b/packages/roosterjs-content-model/lib/publicApi/contentModelToDom.ts index 63e99aa1191..54bbe48bff0 100644 --- a/packages/roosterjs-content-model/lib/publicApi/contentModelToDom.ts +++ b/packages/roosterjs-content-model/lib/publicApi/contentModelToDom.ts @@ -2,7 +2,7 @@ import { ContentModelDocument } from '../publicTypes/block/group/ContentModelDoc import { createModelToDomContext } from '../modelToDom/context/createModelToDomContext'; import { createRange, Position, toArray } from 'roosterjs-editor-dom'; import { EditorContext } from '../publicTypes/context/EditorContext'; -import { handleBlock } from '../modelToDom/handlers/handleBlock'; +import { handleBlockGroup } from '../modelToDom/handlers/handleBlockGroup'; import { isNodeOfType } from '../domUtils/isNodeOfType'; import { ModelToDomBlockAndSegmentNode } from '../publicTypes/context/ModelToDomSelectionContext'; import { ModelToDomContext } from '../publicTypes/context/ModelToDomContext'; @@ -31,7 +31,7 @@ export default function contentModelToDom( const fragment = model.document.createDocumentFragment(); const modelToDomContext = createModelToDomContext(editorContext, option); - handleBlock(model.document, fragment, model, modelToDomContext); + handleBlockGroup(model.document, fragment, model, modelToDomContext); optimize(fragment, 2 /*optimizeLevel*/); const range = extractSelectionRange(modelToDomContext); diff --git a/packages/roosterjs-content-model/lib/publicApi/selection/hasSelectionInBlock.ts b/packages/roosterjs-content-model/lib/publicApi/selection/hasSelectionInBlock.ts index 6b114c3ffe8..280b21308d4 100644 --- a/packages/roosterjs-content-model/lib/publicApi/selection/hasSelectionInBlock.ts +++ b/packages/roosterjs-content-model/lib/publicApi/selection/hasSelectionInBlock.ts @@ -1,3 +1,4 @@ +import hasSelectionInBlockGroup from './hasSelectionInBlockGroup'; import hasSelectionInSegment from './hasSelectionInSegment'; import { ContentModelBlock } from '../../publicTypes/block/ContentModelBlock'; @@ -11,18 +12,10 @@ export default function hasSelectionInBlock(block: ContentModelBlock): boolean { return block.segments.some(hasSelectionInSegment); case 'Table': - return block.cells.some(row => row.some(hasSelectionInBlock)); + return block.cells.some(row => row.some(hasSelectionInBlockGroup)); case 'BlockGroup': - if (block.blockGroupType == 'TableCell' && block.isSelected) { - return true; - } - - if (block.blocks.some(hasSelectionInBlock)) { - return true; - } - - return false; + return hasSelectionInBlockGroup(block); default: return false; diff --git a/packages/roosterjs-content-model/lib/publicApi/selection/hasSelectionInBlockGroup.ts b/packages/roosterjs-content-model/lib/publicApi/selection/hasSelectionInBlockGroup.ts new file mode 100644 index 00000000000..379e512a3ff --- /dev/null +++ b/packages/roosterjs-content-model/lib/publicApi/selection/hasSelectionInBlockGroup.ts @@ -0,0 +1,18 @@ +import hasSelectionInBlock from './hasSelectionInBlock'; +import { ContentModelBlockGroup } from '../../publicTypes/block/group/ContentModelBlockGroup'; + +/** + * Check if there is selection within the given block + * @param block The block to check + */ +export default function hasSelectionInBlockGroup(group: ContentModelBlockGroup): boolean { + if (group.blockGroupType == 'TableCell' && group.isSelected) { + return true; + } + + if (group.blocks.some(hasSelectionInBlock)) { + return true; + } + + return false; +} diff --git a/packages/roosterjs-content-model/lib/publicTypes/block/ContentModelBlock.ts b/packages/roosterjs-content-model/lib/publicTypes/block/ContentModelBlock.ts index 40ae4befced..3b746343b57 100644 --- a/packages/roosterjs-content-model/lib/publicTypes/block/ContentModelBlock.ts +++ b/packages/roosterjs-content-model/lib/publicTypes/block/ContentModelBlock.ts @@ -1,8 +1,11 @@ -import { ContentModelBlockGroup } from './group/ContentModelBlockGroup'; +import { ContentModelGeneralBlock } from './group/ContentModelGeneralBlock'; import { ContentModelParagraph } from './ContentModelParagraph'; import { ContentModelTable } from './ContentModelTable'; /** * A union type of Content Model Block */ -export type ContentModelBlock = ContentModelBlockGroup | ContentModelTable | ContentModelParagraph; +export type ContentModelBlock = + | ContentModelGeneralBlock + | ContentModelTable + | ContentModelParagraph; diff --git a/packages/roosterjs-content-model/lib/publicTypes/block/group/ContentModelBlockGroupBase.ts b/packages/roosterjs-content-model/lib/publicTypes/block/group/ContentModelBlockGroupBase.ts index 1c84966add3..17432295bc0 100644 --- a/packages/roosterjs-content-model/lib/publicTypes/block/group/ContentModelBlockGroupBase.ts +++ b/packages/roosterjs-content-model/lib/publicTypes/block/group/ContentModelBlockGroupBase.ts @@ -1,12 +1,10 @@ import { ContentModelBlock } from '../ContentModelBlock'; -import { ContentModelBlockBase } from '../ContentModelBlockBase'; import { ContentModelBlockGroupType } from '../../enum/BlockGroupType'; /** * Base type of Content Model Block Group */ -export interface ContentModelBlockGroupBase - extends ContentModelBlockBase<'BlockGroup'> { +export interface ContentModelBlockGroupBase { /** * Type of this block group */ diff --git a/packages/roosterjs-content-model/lib/publicTypes/block/group/ContentModelGeneralBlock.ts b/packages/roosterjs-content-model/lib/publicTypes/block/group/ContentModelGeneralBlock.ts index 2c40f2b4654..aa7919bc430 100644 --- a/packages/roosterjs-content-model/lib/publicTypes/block/group/ContentModelGeneralBlock.ts +++ b/packages/roosterjs-content-model/lib/publicTypes/block/group/ContentModelGeneralBlock.ts @@ -1,9 +1,12 @@ +import { ContentModelBlockBase } from '../ContentModelBlockBase'; import { ContentModelBlockGroupBase } from './ContentModelBlockGroupBase'; /** * Content Model for general Block element */ -export interface ContentModelGeneralBlock extends ContentModelBlockGroupBase<'General'> { +export interface ContentModelGeneralBlock + extends ContentModelBlockGroupBase<'General'>, + ContentModelBlockBase<'BlockGroup'> { /** * A reference to original HTML node that this model was created from */ diff --git a/packages/roosterjs-content-model/test/domToModel/processors/brProcessorTest.ts b/packages/roosterjs-content-model/test/domToModel/processors/brProcessorTest.ts index 0b75f783c24..27b576f62eb 100644 --- a/packages/roosterjs-content-model/test/domToModel/processors/brProcessorTest.ts +++ b/packages/roosterjs-content-model/test/domToModel/processors/brProcessorTest.ts @@ -17,7 +17,6 @@ describe('brProcessor', () => { brProcessor(doc, br, context); expect(doc).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'Document', blocks: [ { @@ -44,7 +43,6 @@ describe('brProcessor', () => { brProcessor(doc, br, context); expect(doc).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'Document', blocks: [ { diff --git a/packages/roosterjs-content-model/test/domToModel/processors/containerProcessorTest.ts b/packages/roosterjs-content-model/test/domToModel/processors/containerProcessorTest.ts index 5b198ff6ffd..b13b3ec248d 100644 --- a/packages/roosterjs-content-model/test/domToModel/processors/containerProcessorTest.ts +++ b/packages/roosterjs-content-model/test/domToModel/processors/containerProcessorTest.ts @@ -27,7 +27,6 @@ describe('containerProcessor', () => { containerProcessor(doc, fragment, context); expect(doc).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'Document', blocks: [], document: document, @@ -43,7 +42,6 @@ describe('containerProcessor', () => { containerProcessor(doc, div, context); expect(doc).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'Document', blocks: [], document: document, @@ -60,7 +58,6 @@ describe('containerProcessor', () => { containerProcessor(doc, div, context); expect(doc).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'Document', blocks: [], document: document, @@ -79,7 +76,6 @@ describe('containerProcessor', () => { containerProcessor(doc, div, context); expect(doc).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'Document', blocks: [], document: document, @@ -106,7 +102,6 @@ describe('containerProcessor', () => { containerProcessor(doc, div, context); expect(doc).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'Document', blocks: [], document: document, diff --git a/packages/roosterjs-content-model/test/domToModel/processors/fontProcessorTest.ts b/packages/roosterjs-content-model/test/domToModel/processors/fontProcessorTest.ts index 7de65f1dadb..a4f5affe3f4 100644 --- a/packages/roosterjs-content-model/test/domToModel/processors/fontProcessorTest.ts +++ b/packages/roosterjs-content-model/test/domToModel/processors/fontProcessorTest.ts @@ -19,7 +19,6 @@ describe('fontProcessor', () => { fontProcessor(doc, font, context); expect(doc).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'Document', blocks: [], document: document, diff --git a/packages/roosterjs-content-model/test/domToModel/processors/generalBlockProcessorTest.ts b/packages/roosterjs-content-model/test/domToModel/processors/generalBlockProcessorTest.ts index 57553cbb9c8..b202fefda48 100644 --- a/packages/roosterjs-content-model/test/domToModel/processors/generalBlockProcessorTest.ts +++ b/packages/roosterjs-content-model/test/domToModel/processors/generalBlockProcessorTest.ts @@ -28,7 +28,6 @@ describe('generalBlockProcessor', () => { generalBlockProcessor(doc, div, context); expect(doc).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'Document', blocks: [block], document: document, diff --git a/packages/roosterjs-content-model/test/domToModel/processors/generalSegmentProcessorTest.ts b/packages/roosterjs-content-model/test/domToModel/processors/generalSegmentProcessorTest.ts index 835a177ede9..9a6a9236f6b 100644 --- a/packages/roosterjs-content-model/test/domToModel/processors/generalSegmentProcessorTest.ts +++ b/packages/roosterjs-content-model/test/domToModel/processors/generalSegmentProcessorTest.ts @@ -31,7 +31,6 @@ describe('generalSegmentProcessor', () => { generalSegmentProcessor(doc, span, context); expect(doc).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'Document', blocks: [ { @@ -56,7 +55,6 @@ describe('generalSegmentProcessor', () => { generalSegmentProcessor(doc, span, context); expect(doc).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'Document', blocks: [ { diff --git a/packages/roosterjs-content-model/test/domToModel/processors/tableProcessorTest.ts b/packages/roosterjs-content-model/test/domToModel/processors/tableProcessorTest.ts index ed9ea8700b4..2977d86d8a3 100644 --- a/packages/roosterjs-content-model/test/domToModel/processors/tableProcessorTest.ts +++ b/packages/roosterjs-content-model/test/domToModel/processors/tableProcessorTest.ts @@ -36,7 +36,6 @@ describe('tableProcessor', () => { cells: [ [ { - blockType: 'BlockGroup', blockGroupType: 'TableCell', spanAbove: false, spanLeft: false, @@ -227,7 +226,6 @@ describe('tableProcessor with format', () => { expect(parseFormat.parseFormat).toHaveBeenCalledTimes(4); expect(context.segmentFormat).toEqual({ a: 'b' } as any); expect(doc).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'Document', document: document, blocks: [ @@ -236,7 +234,6 @@ describe('tableProcessor with format', () => { cells: [ [ { - blockType: 'BlockGroup', blockGroupType: 'TableCell', blocks: [ { @@ -304,7 +301,6 @@ describe('tableProcessor with format', () => { tableProcessor(doc, mockedTable, context); expect(doc).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'Document', document: document, blocks: [ @@ -316,7 +312,6 @@ describe('tableProcessor with format', () => { cells: [ [ { - blockType: 'BlockGroup', blockGroupType: 'TableCell', format: {}, blocks: [], diff --git a/packages/roosterjs-content-model/test/domToModel/processors/textProcessorTest.ts b/packages/roosterjs-content-model/test/domToModel/processors/textProcessorTest.ts index 44fb5e22ebe..8682143a913 100644 --- a/packages/roosterjs-content-model/test/domToModel/processors/textProcessorTest.ts +++ b/packages/roosterjs-content-model/test/domToModel/processors/textProcessorTest.ts @@ -15,7 +15,6 @@ describe('textProcessor', () => { textProcessor(doc, 'test', context); expect(doc).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'Document', blocks: [ { @@ -44,7 +43,6 @@ describe('textProcessor', () => { textProcessor(doc, 'test', context); expect(doc).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'Document', blocks: [ { @@ -78,7 +76,6 @@ describe('textProcessor', () => { textProcessor(doc, 'test1', context); expect(doc).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'Document', blocks: [ { @@ -115,7 +112,6 @@ describe('textProcessor', () => { textProcessor(doc, 'test', context); expect(doc).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'Document', blocks: [ { diff --git a/packages/roosterjs-content-model/test/modelApi/common/addBlockTest.ts b/packages/roosterjs-content-model/test/modelApi/common/addBlockTest.ts index 42010a0d0e8..075c4fb1f47 100644 --- a/packages/roosterjs-content-model/test/modelApi/common/addBlockTest.ts +++ b/packages/roosterjs-content-model/test/modelApi/common/addBlockTest.ts @@ -13,7 +13,6 @@ describe('addBlock', () => { addBlock(doc, block); expect(doc).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'Document', blocks: [block], document: document, diff --git a/packages/roosterjs-content-model/test/modelApi/common/addSegmentTest.ts b/packages/roosterjs-content-model/test/modelApi/common/addSegmentTest.ts index df9f7b7659f..7052d8ef7d3 100644 --- a/packages/roosterjs-content-model/test/modelApi/common/addSegmentTest.ts +++ b/packages/roosterjs-content-model/test/modelApi/common/addSegmentTest.ts @@ -14,7 +14,6 @@ describe('addSegment', () => { addSegment(doc, segment); expect(doc).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'Document', blocks: [ { @@ -42,7 +41,6 @@ describe('addSegment', () => { addSegment(doc, segment); expect(doc).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'Document', blocks: [ { @@ -79,7 +77,6 @@ describe('addSegment', () => { addSegment(doc, segment); expect(doc).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'Document', blocks: [ { @@ -118,7 +115,6 @@ describe('addSegment', () => { addSegment(doc, segment); expect(doc).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'Document', blocks: [ block, diff --git a/packages/roosterjs-content-model/test/modelApi/creators/creatorsTest.ts b/packages/roosterjs-content-model/test/modelApi/creators/creatorsTest.ts index ffce950925d..5759dfff6ea 100644 --- a/packages/roosterjs-content-model/test/modelApi/creators/creatorsTest.ts +++ b/packages/roosterjs-content-model/test/modelApi/creators/creatorsTest.ts @@ -14,7 +14,6 @@ describe('Creators', () => { const result = createContentModelDocument(document); expect(result).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'Document', blocks: [], document: document, @@ -26,7 +25,6 @@ describe('Creators', () => { const result = createContentModelDocument(anotherDoc); expect(result).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'Document', blocks: [], document: anotherDoc, @@ -139,7 +137,6 @@ describe('Creators', () => { it('createTableCell from Table Cell - no span', () => { const tdModel = createTableCell(1 /*colSpan*/, 1 /*rowSpan*/, false /*isHeader*/); expect(tdModel).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'TableCell', blocks: [], spanLeft: false, @@ -152,7 +149,6 @@ describe('Creators', () => { it('createTableCell from Table Cell - span left', () => { const tdModel = createTableCell(2 /*colSpan*/, 1 /*rowSpan*/, false /*isHeader*/); expect(tdModel).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'TableCell', blocks: [], spanLeft: true, @@ -165,7 +161,6 @@ describe('Creators', () => { it('createTableCell from Table Cell - span above', () => { const tdModel = createTableCell(1 /*colSpan*/, 3 /*rowSpan*/, false /*isHeader*/); expect(tdModel).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'TableCell', blocks: [], spanLeft: false, @@ -178,7 +173,6 @@ describe('Creators', () => { it('createTableCell from Table Header', () => { const tdModel = createTableCell(1 /*colSpan*/, 1 /*rowSpan*/, true /*isHeader*/); expect(tdModel).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'TableCell', blocks: [], spanLeft: false, @@ -195,7 +189,6 @@ describe('Creators', () => { const tdModel = createTableCell(1 /*colSpan*/, 1 /*rowSpan*/, true /*isHeader*/, format); expect(tdModel).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'TableCell', blocks: [], spanLeft: false, @@ -208,7 +201,6 @@ describe('Creators', () => { format.textAlign = 'end'; expect(tdModel).toEqual({ - blockType: 'BlockGroup', blockGroupType: 'TableCell', blocks: [], spanLeft: false, diff --git a/packages/roosterjs-content-model/test/modelApi/table/applyTableFormatTest.ts b/packages/roosterjs-content-model/test/modelApi/table/applyTableFormatTest.ts index 584c29512d8..503a46eb5aa 100644 --- a/packages/roosterjs-content-model/test/modelApi/table/applyTableFormatTest.ts +++ b/packages/roosterjs-content-model/test/modelApi/table/applyTableFormatTest.ts @@ -10,7 +10,6 @@ const T = 'transparent'; describe('applyTableFormat', () => { function createCell(): ContentModelTableCell { return { - blockType: 'BlockGroup', blockGroupType: 'TableCell', blocks: [], isHeader: false, diff --git a/packages/roosterjs-content-model/test/modelApi/table/createTableStructureTest.ts b/packages/roosterjs-content-model/test/modelApi/table/createTableStructureTest.ts index 6a744afa57b..a194ffb18d7 100644 --- a/packages/roosterjs-content-model/test/modelApi/table/createTableStructureTest.ts +++ b/packages/roosterjs-content-model/test/modelApi/table/createTableStructureTest.ts @@ -13,7 +13,6 @@ describe('createTableStructure', () => { cells: [ [ { - blockType: 'BlockGroup', blockGroupType: 'TableCell', blocks: [], format: {}, diff --git a/packages/roosterjs-content-model/test/modelApi/table/normalizeTableTest.ts b/packages/roosterjs-content-model/test/modelApi/table/normalizeTableTest.ts index 76f622f871f..c36cc5476e1 100644 --- a/packages/roosterjs-content-model/test/modelApi/table/normalizeTableTest.ts +++ b/packages/roosterjs-content-model/test/modelApi/table/normalizeTableTest.ts @@ -34,7 +34,6 @@ describe('normalizeTable', () => { cells: [ [ { - blockType: 'BlockGroup', blockGroupType: 'TableCell', spanAbove: false, spanLeft: false, @@ -86,7 +85,6 @@ describe('normalizeTable', () => { cells: [ [ { - blockType: 'BlockGroup', blockGroupType: 'TableCell', spanLeft: false, spanAbove: false, @@ -102,7 +100,6 @@ describe('normalizeTable', () => { ], [ { - blockType: 'BlockGroup', blockGroupType: 'TableCell', spanLeft: false, spanAbove: false, @@ -160,7 +157,6 @@ describe('normalizeTable', () => { cells: [ [ { - blockType: 'BlockGroup', blockGroupType: 'TableCell', spanLeft: false, spanAbove: false, @@ -190,7 +186,6 @@ describe('normalizeTable', () => { ], }, { - blockType: 'BlockGroup', blockGroupType: 'TableCell', spanLeft: false, spanAbove: false, @@ -242,7 +237,6 @@ describe('normalizeTable', () => { cells: [ [ { - blockType: 'BlockGroup', blockGroupType: 'TableCell', spanLeft: false, spanAbove: false, @@ -301,7 +295,6 @@ describe('normalizeTable', () => { cells: [ [ { - blockType: 'BlockGroup', blockGroupType: 'TableCell', spanLeft: false, spanAbove: false, @@ -310,7 +303,6 @@ describe('normalizeTable', () => { blocks: [block1, block2], }, { - blockType: 'BlockGroup', blockGroupType: 'TableCell', spanLeft: true, spanAbove: false, @@ -321,7 +313,6 @@ describe('normalizeTable', () => { ], [ { - blockType: 'BlockGroup', blockGroupType: 'TableCell', spanLeft: false, spanAbove: false, @@ -330,7 +321,6 @@ describe('normalizeTable', () => { blocks: [block3], }, { - blockType: 'BlockGroup', blockGroupType: 'TableCell', spanLeft: false, spanAbove: false, @@ -384,7 +374,6 @@ describe('normalizeTable', () => { cells: [ [ { - blockType: 'BlockGroup', blockGroupType: 'TableCell', spanLeft: false, spanAbove: false, @@ -414,7 +403,6 @@ describe('normalizeTable', () => { ], }, { - blockType: 'BlockGroup', blockGroupType: 'TableCell', spanLeft: false, spanAbove: false, @@ -489,7 +477,6 @@ describe('normalizeTable', () => { cells: [ [ { - blockType: 'BlockGroup', blockGroupType: 'TableCell', spanLeft: false, spanAbove: false, diff --git a/packages/roosterjs-content-model/test/modelApi/table/setTableCellBackgroundColorTest.ts b/packages/roosterjs-content-model/test/modelApi/table/setTableCellBackgroundColorTest.ts index 11dd4ac2b36..ba91b734791 100644 --- a/packages/roosterjs-content-model/test/modelApi/table/setTableCellBackgroundColorTest.ts +++ b/packages/roosterjs-content-model/test/modelApi/table/setTableCellBackgroundColorTest.ts @@ -28,7 +28,6 @@ describe('setTableCellBackgroundColor', () => { cells: [ [ { - blockType: 'BlockGroup', blockGroupType: 'TableCell', blocks: [], spanAbove: false, @@ -49,7 +48,6 @@ describe('setTableCellBackgroundColor', () => { cells: [ [ { - blockType: 'BlockGroup', blockGroupType: 'TableCell', blocks: [], spanAbove: false, @@ -70,7 +68,6 @@ describe('setTableCellBackgroundColor', () => { cells: [ [ { - blockType: 'BlockGroup', blockGroupType: 'TableCell', blocks: [], spanAbove: false, @@ -78,7 +75,6 @@ describe('setTableCellBackgroundColor', () => { format: {}, }, { - blockType: 'BlockGroup', blockGroupType: 'TableCell', blocks: [], spanAbove: false, @@ -87,7 +83,6 @@ describe('setTableCellBackgroundColor', () => { isSelected: true, }, { - blockType: 'BlockGroup', blockGroupType: 'TableCell', blocks: [], spanAbove: false, @@ -109,7 +104,6 @@ describe('setTableCellBackgroundColor', () => { cells: [ [ { - blockType: 'BlockGroup', blockGroupType: 'TableCell', blocks: [], spanAbove: false, @@ -117,7 +111,6 @@ describe('setTableCellBackgroundColor', () => { format: {}, }, { - blockType: 'BlockGroup', blockGroupType: 'TableCell', blocks: [], spanAbove: false, @@ -129,7 +122,6 @@ describe('setTableCellBackgroundColor', () => { isSelected: true, }, { - blockType: 'BlockGroup', blockGroupType: 'TableCell', blocks: [], spanAbove: false, @@ -154,7 +146,6 @@ describe('setTableCellBackgroundColor', () => { cells: [ [ { - blockType: 'BlockGroup', blockGroupType: 'TableCell', blocks: [], spanAbove: false, @@ -162,7 +153,6 @@ describe('setTableCellBackgroundColor', () => { format: {}, }, { - blockType: 'BlockGroup', blockGroupType: 'TableCell', blocks: [ { @@ -194,7 +184,6 @@ describe('setTableCellBackgroundColor', () => { cells: [ [ { - blockType: 'BlockGroup', blockGroupType: 'TableCell', blocks: [], spanAbove: false, @@ -202,7 +191,6 @@ describe('setTableCellBackgroundColor', () => { format: {}, }, { - blockType: 'BlockGroup', blockGroupType: 'TableCell', blocks: [ { @@ -237,7 +225,6 @@ describe('setTableCellBackgroundColor', () => { cells: [ [ { - blockType: 'BlockGroup', blockGroupType: 'TableCell', blocks: [ { @@ -246,7 +233,6 @@ describe('setTableCellBackgroundColor', () => { cells: [ [ { - blockType: 'BlockGroup', blockGroupType: 'TableCell', spanAbove: false, spanLeft: false, @@ -278,7 +264,6 @@ describe('setTableCellBackgroundColor', () => { cells: [ [ { - blockType: 'BlockGroup', blockGroupType: 'TableCell', blocks: [ { @@ -287,7 +272,6 @@ describe('setTableCellBackgroundColor', () => { cells: [ [ { - blockType: 'BlockGroup', blockGroupType: 'TableCell', spanAbove: false, spanLeft: false, diff --git a/packages/roosterjs-content-model/test/publicApi/selection/hasSelectionInBlockTest.ts b/packages/roosterjs-content-model/test/publicApi/selection/hasSelectionInBlockTest.ts index 7ad8a23a628..d4f0c9df85c 100644 --- a/packages/roosterjs-content-model/test/publicApi/selection/hasSelectionInBlockTest.ts +++ b/packages/roosterjs-content-model/test/publicApi/selection/hasSelectionInBlockTest.ts @@ -1,5 +1,7 @@ import hasSelectionInBlock from '../../../lib/publicApi/selection/hasSelectionInBlock'; +import hasSelectionInBlockGroup from '../../../lib/publicApi/selection/hasSelectionInBlockGroup'; import { ContentModelBlock } from '../../../lib/publicTypes/block/ContentModelBlock'; +import { ContentModelTableCell } from '../../../lib/publicTypes/block/group/ContentModelTableCell'; describe('hasSelectionInBlock', () => { it('Empty paragraph block', () => { @@ -55,7 +57,6 @@ describe('hasSelectionInBlock', () => { cells: [ [ { - blockType: 'BlockGroup', blockGroupType: 'TableCell', blocks: [], format: {}, @@ -80,7 +81,6 @@ describe('hasSelectionInBlock', () => { cells: [ [ { - blockType: 'BlockGroup', blockGroupType: 'TableCell', blocks: [], format: {}, @@ -88,7 +88,6 @@ describe('hasSelectionInBlock', () => { spanLeft: false, }, { - blockType: 'BlockGroup', blockGroupType: 'TableCell', blocks: [], format: {}, @@ -114,7 +113,6 @@ describe('hasSelectionInBlock', () => { cells: [ [ { - blockType: 'BlockGroup', blockGroupType: 'TableCell', blocks: [ { @@ -144,8 +142,7 @@ describe('hasSelectionInBlock', () => { }); it('Table cell with selected content', () => { - const block: ContentModelBlock = { - blockType: 'BlockGroup', + const block: ContentModelTableCell = { blockGroupType: 'TableCell', format: {}, spanAbove: false, @@ -164,7 +161,7 @@ describe('hasSelectionInBlock', () => { ], }; - const result = hasSelectionInBlock(block); + const result = hasSelectionInBlockGroup(block); expect(result).toBeTrue(); }); From c3314b2deb2a5b0c41b1dcb5a5ec95619a3399d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Mon, 19 Sep 2022 11:47:21 -0300 Subject: [PATCH 14/41] fix comments --- .../plugins/ContentEdit/utils/getAutoNumberingListStyle.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/utils/getAutoNumberingListStyle.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/utils/getAutoNumberingListStyle.ts index 87e5d3bf226..901fc3e5cdb 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/utils/getAutoNumberingListStyle.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/utils/getAutoNumberingListStyle.ts @@ -21,6 +21,8 @@ const characters: Record = { ')': Character.Parenthesis, }; +const numberingTriggers = ['1', 'a', 'A', 'I', 'i']; + const identifyNumberingType = (text: string) => { if (!isNaN(parseInt(text))) { return NumberingTypes.Decimal; @@ -101,7 +103,7 @@ const identifyNumberingListType = ( /** * @internal * @param textBeforeCursor The trigger character - * @param startNumber (Optional) Start number of the list + * @param isTheFirstItem (Optional) Is the start number of a list. * @returns The style of a numbering list triggered by a string */ export default function getAutoNumberingListStyle( @@ -110,8 +112,9 @@ export default function getAutoNumberingListStyle( ): NumberingListType { const trigger = textBeforeCursor.trim(); //Only the staring items ['1', 'a', 'A', 'I', 'i'] must trigger a new list. All the other triggers is used to keep the list chain. + //The index is always the character before the last character const listIndex = trigger[trigger.length - 2]; - const numberingTriggers = ['1', 'a', 'A', 'I', 'i']; + if (isTheFirstItem && numberingTriggers.indexOf(listIndex) < 0) { return null; } From 935cd091f04ed203e551548ca3608bf8a808a53e Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 19 Sep 2022 13:37:53 -0700 Subject: [PATCH 15/41] Content Model: Add a temporary processor for DIV and SPAN (#1268) * Add a temporary processor for DIV and SPAN * fix build --- .../domToModel/context/defaultProcessors.ts | 3 + .../processors/generalBlockProcessor.ts | 14 ---- .../domToModel/processors/generalProcessor.ts | 52 +++++++++++++++ .../processors/generalSegmentProcessor.ts | 28 -------- .../processors/singleElementProcessor.ts | 9 +-- .../processors/tempContainerProcessor.ts | 14 ++++ .../processors/containerProcessorTest.ts | 64 ++++--------------- .../processors/generalBlockProcessorTest.ts | 40 ------------ ...ocessorTest.ts => generalProcessorTest.ts} | 36 +++++++++-- 9 files changed, 115 insertions(+), 145 deletions(-) delete mode 100644 packages/roosterjs-content-model/lib/domToModel/processors/generalBlockProcessor.ts create mode 100644 packages/roosterjs-content-model/lib/domToModel/processors/generalProcessor.ts delete mode 100644 packages/roosterjs-content-model/lib/domToModel/processors/generalSegmentProcessor.ts create mode 100644 packages/roosterjs-content-model/lib/domToModel/processors/tempContainerProcessor.ts delete mode 100644 packages/roosterjs-content-model/test/domToModel/processors/generalBlockProcessorTest.ts rename packages/roosterjs-content-model/test/domToModel/processors/{generalSegmentProcessorTest.ts => generalProcessorTest.ts} (67%) diff --git a/packages/roosterjs-content-model/lib/domToModel/context/defaultProcessors.ts b/packages/roosterjs-content-model/lib/domToModel/context/defaultProcessors.ts index 826cb2048fd..2301e3f5d90 100644 --- a/packages/roosterjs-content-model/lib/domToModel/context/defaultProcessors.ts +++ b/packages/roosterjs-content-model/lib/domToModel/context/defaultProcessors.ts @@ -3,6 +3,7 @@ import { ElementProcessor } from '../../publicTypes/context/ElementProcessor'; import { fontProcessor } from '../processors/fontProcessor'; import { knownElementProcessor } from '../processors/knownElementProcessor'; import { tableProcessor } from '../processors/tableProcessor'; +import { tempContainerProcessor } from '../processors/tempContainerProcessor'; /** * @internal @@ -10,10 +11,12 @@ import { tableProcessor } from '../processors/tableProcessor'; export const defaultProcessorMap: Record = { B: knownElementProcessor, BR: brProcessor, + DIV: tempContainerProcessor, EM: knownElementProcessor, FONT: fontProcessor, I: knownElementProcessor, S: knownElementProcessor, + SPAN: tempContainerProcessor, STRIKE: knownElementProcessor, STRONG: knownElementProcessor, SUB: knownElementProcessor, diff --git a/packages/roosterjs-content-model/lib/domToModel/processors/generalBlockProcessor.ts b/packages/roosterjs-content-model/lib/domToModel/processors/generalBlockProcessor.ts deleted file mode 100644 index 7c76b0a417f..00000000000 --- a/packages/roosterjs-content-model/lib/domToModel/processors/generalBlockProcessor.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { addBlock } from '../../modelApi/common/addBlock'; -import { containerProcessor } from './containerProcessor'; -import { createGeneralBlock } from '../../modelApi/creators/createGeneralBlock'; -import { ElementProcessor } from '../../publicTypes/context/ElementProcessor'; - -/** - * @internal - */ -export const generalBlockProcessor: ElementProcessor = (group, element, context) => { - const block = createGeneralBlock(element); - - addBlock(group, block); - containerProcessor(block, element, context); -}; diff --git a/packages/roosterjs-content-model/lib/domToModel/processors/generalProcessor.ts b/packages/roosterjs-content-model/lib/domToModel/processors/generalProcessor.ts new file mode 100644 index 00000000000..05db05e2802 --- /dev/null +++ b/packages/roosterjs-content-model/lib/domToModel/processors/generalProcessor.ts @@ -0,0 +1,52 @@ +import { addBlock } from '../../modelApi/common/addBlock'; +import { addSegment } from '../../modelApi/common/addSegment'; +import { containerProcessor } from './containerProcessor'; +import { createGeneralBlock } from '../../modelApi/creators/createGeneralBlock'; +import { createGeneralSegment } from '../../modelApi/creators/createGeneralSegment'; +import { ElementProcessor } from '../../publicTypes/context/ElementProcessor'; +import { isBlockElement } from 'roosterjs-editor-dom'; +import { stackFormat } from '../utils/stackFormat'; + +const generalBlockProcessor: ElementProcessor = (group, element, context) => { + const block = createGeneralBlock(element); + + stackFormat( + context, + { + segment: 'empty', + }, + () => { + addBlock(group, block); + containerProcessor(block, element, context); + } + ); +}; + +const generalSegmentProcessor: ElementProcessor = (group, element, context) => { + const segment = createGeneralSegment(element, context.segmentFormat); + + if (context.isInSelection && !element.firstChild) { + segment.isSelected = true; + } + + stackFormat( + context, + { + segment: + 'empty' /*clearFormat, General segment will include all properties and styles when generate back to HTML, so no need to carry over existing segment format*/, + }, + () => { + addSegment(group, segment); + containerProcessor(segment, element, context); + } + ); +}; + +/** + * @internal + */ +export const generalProcessor: ElementProcessor = (group, element, context) => { + const processor = isBlockElement(element) ? generalBlockProcessor : generalSegmentProcessor; + + processor(group, element, context); +}; diff --git a/packages/roosterjs-content-model/lib/domToModel/processors/generalSegmentProcessor.ts b/packages/roosterjs-content-model/lib/domToModel/processors/generalSegmentProcessor.ts deleted file mode 100644 index 4f676a8e37c..00000000000 --- a/packages/roosterjs-content-model/lib/domToModel/processors/generalSegmentProcessor.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { addSegment } from '../../modelApi/common/addSegment'; -import { containerProcessor } from './containerProcessor'; -import { createGeneralSegment } from '../../modelApi/creators/createGeneralSegment'; -import { ElementProcessor } from '../../publicTypes/context/ElementProcessor'; -import { stackFormat } from '../utils/stackFormat'; - -/** - * @internal - */ -export const generalSegmentProcessor: ElementProcessor = (group, element, context) => { - const segment = createGeneralSegment(element, context.segmentFormat); - - if (context.isInSelection) { - segment.isSelected = true; - } - - stackFormat( - context, - { - segment: - 'empty' /*clearFormat, General segment will include all properties and styles when generate back to HTML, so no need to carry over existing segment format*/, - }, - () => { - addSegment(group, segment); - containerProcessor(segment, element, context); - } - ); -}; diff --git a/packages/roosterjs-content-model/lib/domToModel/processors/singleElementProcessor.ts b/packages/roosterjs-content-model/lib/domToModel/processors/singleElementProcessor.ts index 5786e91e880..db13d564ad7 100644 --- a/packages/roosterjs-content-model/lib/domToModel/processors/singleElementProcessor.ts +++ b/packages/roosterjs-content-model/lib/domToModel/processors/singleElementProcessor.ts @@ -1,7 +1,5 @@ import { ElementProcessor } from '../../publicTypes/context/ElementProcessor'; -import { generalBlockProcessor } from './generalBlockProcessor'; -import { generalSegmentProcessor } from './generalSegmentProcessor'; -import { isBlockElement } from 'roosterjs-editor-dom'; +import { generalProcessor } from './generalProcessor'; /** * @internal @@ -10,9 +8,6 @@ import { isBlockElement } from 'roosterjs-editor-dom'; * @param context */ export const singleElementProcessor: ElementProcessor = (group, element, context) => { - const processor = - context.elementProcessors[element.tagName] || - (isBlockElement(element) ? generalBlockProcessor : generalSegmentProcessor); - + const processor = context.elementProcessors[element.tagName] || generalProcessor; processor(group, element, context); }; diff --git a/packages/roosterjs-content-model/lib/domToModel/processors/tempContainerProcessor.ts b/packages/roosterjs-content-model/lib/domToModel/processors/tempContainerProcessor.ts new file mode 100644 index 00000000000..78e0cf3a59d --- /dev/null +++ b/packages/roosterjs-content-model/lib/domToModel/processors/tempContainerProcessor.ts @@ -0,0 +1,14 @@ +import { ElementProcessor } from '../../publicTypes/context/ElementProcessor'; +import { generalProcessor } from './generalProcessor'; +import { knownElementProcessor } from './knownElementProcessor'; + +/** + * @internal + * A temp processor to handle DIV and SPAN that don't have any attribute, to reduce unnecessary general blocks/segments + */ +export const tempContainerProcessor: ElementProcessor = (group, element, context) => { + const processor: ElementProcessor = + element.attributes.length == 0 ? knownElementProcessor : generalProcessor; + + processor(group, element, context); +}; diff --git a/packages/roosterjs-content-model/test/domToModel/processors/containerProcessorTest.ts b/packages/roosterjs-content-model/test/domToModel/processors/containerProcessorTest.ts index b13b3ec248d..568ba636b24 100644 --- a/packages/roosterjs-content-model/test/domToModel/processors/containerProcessorTest.ts +++ b/packages/roosterjs-content-model/test/domToModel/processors/containerProcessorTest.ts @@ -1,12 +1,8 @@ -import * as generalBlockProcessor from '../../../lib/domToModel/processors/generalBlockProcessor'; -import * as generalSegmentProcessor from '../../../lib/domToModel/processors/generalSegmentProcessor'; import * as textProcessor from '../../../lib/domToModel/processors/textProcessor'; -import { addSegment } from '../../../lib/modelApi/common/addSegment'; import { containerProcessor } from '../../../lib/domToModel/processors/containerProcessor'; import { ContentModelDocument } from '../../../lib/publicTypes/block/group/ContentModelDocument'; import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; -import { createText } from '../../../lib/modelApi/creators/createText'; import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; describe('containerProcessor', () => { @@ -16,8 +12,6 @@ describe('containerProcessor', () => { beforeEach(() => { doc = createContentModelDocument(document); context = createDomToModelContext(); - spyOn(generalBlockProcessor, 'generalBlockProcessor'); - spyOn(generalSegmentProcessor, 'generalSegmentProcessor'); spyOn(textProcessor, 'textProcessor'); }); @@ -31,8 +25,6 @@ describe('containerProcessor', () => { blocks: [], document: document, }); - expect(generalBlockProcessor.generalBlockProcessor).not.toHaveBeenCalled(); - expect(generalSegmentProcessor.generalSegmentProcessor).not.toHaveBeenCalled(); expect(textProcessor.textProcessor).not.toHaveBeenCalled(); }); @@ -46,8 +38,6 @@ describe('containerProcessor', () => { blocks: [], document: document, }); - expect(generalBlockProcessor.generalBlockProcessor).not.toHaveBeenCalled(); - expect(generalSegmentProcessor.generalSegmentProcessor).not.toHaveBeenCalled(); expect(textProcessor.textProcessor).not.toHaveBeenCalled(); }); @@ -62,8 +52,6 @@ describe('containerProcessor', () => { blocks: [], document: document, }); - expect(generalBlockProcessor.generalBlockProcessor).not.toHaveBeenCalled(); - expect(generalSegmentProcessor.generalSegmentProcessor).not.toHaveBeenCalled(); expect(textProcessor.textProcessor).toHaveBeenCalledTimes(1); expect(textProcessor.textProcessor).toHaveBeenCalledWith(doc, 'test', context); }); @@ -80,13 +68,6 @@ describe('containerProcessor', () => { blocks: [], document: document, }); - expect(generalBlockProcessor.generalBlockProcessor).not.toHaveBeenCalled(); - expect(generalSegmentProcessor.generalSegmentProcessor).toHaveBeenCalledTimes(1); - expect(generalSegmentProcessor.generalSegmentProcessor).toHaveBeenCalledWith( - doc, - span, - context - ); expect(textProcessor.textProcessor).not.toHaveBeenCalled(); }); @@ -103,21 +84,18 @@ describe('containerProcessor', () => { expect(doc).toEqual({ blockGroupType: 'Document', - blocks: [], + blocks: [ + { + blockType: 'Paragraph', + segments: [], + }, + { + blockType: 'Paragraph', + segments: [], + }, + ], document: document, }); - expect(generalBlockProcessor.generalBlockProcessor).toHaveBeenCalledTimes(1); - expect(generalBlockProcessor.generalBlockProcessor).toHaveBeenCalledWith( - doc, - innerDiv, - context - ); - expect(generalSegmentProcessor.generalSegmentProcessor).toHaveBeenCalledTimes(1); - expect(generalSegmentProcessor.generalSegmentProcessor).toHaveBeenCalledWith( - doc, - span, - context - ); expect(textProcessor.textProcessor).toHaveBeenCalledTimes(1); expect(textProcessor.textProcessor).toHaveBeenCalledWith(doc, 'test', context); }); @@ -130,17 +108,6 @@ describe('containerProcessor', () => { beforeEach(() => { doc = createContentModelDocument(document); context = createDomToModelContext(); - spyOn(generalSegmentProcessor, 'generalSegmentProcessor').and.callFake( - (group, element, context) => { - const segment = createText(element.textContent!) as any; - - if (context.isInSelection) { - segment.isSelected = true; - } - - addSegment(group, segment); - } - ); }); it('Process a DIV with element selection', () => { @@ -196,8 +163,7 @@ describe('containerProcessor', () => { isSelected: true, format: {}, }, - { segmentType: 'Text', text: 'test2', format: {} }, - { segmentType: 'Text', text: 'test3', format: {} }, + { segmentType: 'Text', text: 'test2test3', format: {} }, ], isImplicit: true, }); @@ -280,12 +246,8 @@ describe('containerProcessor', () => { expect(doc.blocks[0]).toEqual({ blockType: 'Paragraph', segments: [ - { segmentType: 'Text', text: 'test1test2', format: {} }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, + { segmentType: 'Text', text: 'test1', format: {} }, + { segmentType: 'Text', text: 'test2', format: {}, isSelected: true }, { segmentType: 'Text', text: 'test3', format: {} }, ], isImplicit: true, diff --git a/packages/roosterjs-content-model/test/domToModel/processors/generalBlockProcessorTest.ts b/packages/roosterjs-content-model/test/domToModel/processors/generalBlockProcessorTest.ts deleted file mode 100644 index b202fefda48..00000000000 --- a/packages/roosterjs-content-model/test/domToModel/processors/generalBlockProcessorTest.ts +++ /dev/null @@ -1,40 +0,0 @@ -import * as containerProcessor from '../../../lib/domToModel/processors/containerProcessor'; -import * as createGeneralBlock from '../../../lib/modelApi/creators/createGeneralBlock'; -import { ContentModelGeneralBlock } from '../../../lib/publicTypes/block/group/ContentModelGeneralBlock'; -import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; -import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; -import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; -import { generalBlockProcessor } from '../../../lib/domToModel/processors/generalBlockProcessor'; - -describe('generalBlockProcessor', () => { - let context: DomToModelContext; - - beforeEach(() => { - spyOn(containerProcessor, 'containerProcessor'); - context = createDomToModelContext(); - }); - - it('Process a DIV element', () => { - const doc = createContentModelDocument(document); - const div = document.createElement('div'); - const block: ContentModelGeneralBlock = { - blockType: 'BlockGroup', - blockGroupType: 'General', - element: div, - blocks: [], - }; - - spyOn(createGeneralBlock, 'createGeneralBlock').and.returnValue(block); - generalBlockProcessor(doc, div, context); - - expect(doc).toEqual({ - blockGroupType: 'Document', - blocks: [block], - document: document, - }); - expect(createGeneralBlock.createGeneralBlock).toHaveBeenCalledTimes(1); - expect(createGeneralBlock.createGeneralBlock).toHaveBeenCalledWith(div); - expect(containerProcessor.containerProcessor).toHaveBeenCalledTimes(1); - expect(containerProcessor.containerProcessor).toHaveBeenCalledWith(block, div, context); - }); -}); diff --git a/packages/roosterjs-content-model/test/domToModel/processors/generalSegmentProcessorTest.ts b/packages/roosterjs-content-model/test/domToModel/processors/generalProcessorTest.ts similarity index 67% rename from packages/roosterjs-content-model/test/domToModel/processors/generalSegmentProcessorTest.ts rename to packages/roosterjs-content-model/test/domToModel/processors/generalProcessorTest.ts index 9a6a9236f6b..c53ba6a18a8 100644 --- a/packages/roosterjs-content-model/test/domToModel/processors/generalSegmentProcessorTest.ts +++ b/packages/roosterjs-content-model/test/domToModel/processors/generalProcessorTest.ts @@ -1,17 +1,43 @@ import * as containerProcessor from '../../../lib/domToModel/processors/containerProcessor'; +import * as createGeneralBlock from '../../../lib/modelApi/creators/createGeneralBlock'; import * as createGeneralSegment from '../../../lib/modelApi/creators/createGeneralSegment'; +import { ContentModelGeneralBlock } from '../../../lib/publicTypes/block/group/ContentModelGeneralBlock'; import { ContentModelGeneralSegment } from '../../../lib/publicTypes/segment/ContentModelGeneralSegment'; import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; -import { generalSegmentProcessor } from '../../../lib/domToModel/processors/generalSegmentProcessor'; +import { generalProcessor } from '../../../lib/domToModel/processors/generalProcessor'; -describe('generalSegmentProcessor', () => { +describe('generalProcessor', () => { let context: DomToModelContext; beforeEach(() => { - context = createDomToModelContext(); spyOn(containerProcessor, 'containerProcessor'); + context = createDomToModelContext(); + }); + + it('Process a DIV element', () => { + const doc = createContentModelDocument(document); + const div = document.createElement('div'); + const block: ContentModelGeneralBlock = { + blockType: 'BlockGroup', + blockGroupType: 'General', + element: div, + blocks: [], + }; + + spyOn(createGeneralBlock, 'createGeneralBlock').and.returnValue(block); + generalProcessor(doc, div, context); + + expect(doc).toEqual({ + blockGroupType: 'Document', + blocks: [block], + document: document, + }); + expect(createGeneralBlock.createGeneralBlock).toHaveBeenCalledTimes(1); + expect(createGeneralBlock.createGeneralBlock).toHaveBeenCalledWith(div); + expect(containerProcessor.containerProcessor).toHaveBeenCalledTimes(1); + expect(containerProcessor.containerProcessor).toHaveBeenCalledWith(block, div, context); }); it('Process a SPAN element', () => { @@ -28,7 +54,7 @@ describe('generalSegmentProcessor', () => { spyOn(createGeneralSegment, 'createGeneralSegment').and.returnValue(segment); - generalSegmentProcessor(doc, span, context); + generalProcessor(doc, span, context); expect(doc).toEqual({ blockGroupType: 'Document', @@ -52,7 +78,7 @@ describe('generalSegmentProcessor', () => { const span = document.createElement('span'); context.segmentFormat = { a: 'b' } as any; - generalSegmentProcessor(doc, span, context); + generalProcessor(doc, span, context); expect(doc).toEqual({ blockGroupType: 'Document', From 9ce1bd91b5049ff38818fe217566ea15b6d8d7a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Mon, 19 Sep 2022 20:25:04 -0300 Subject: [PATCH 16/41] fix selectImageApi --- .../getSelection/getSelectionPane.tsx | 6 +++--- .../lib/coreApi/selectImage.ts | 19 ++++--------------- .../lib/corePlugins/DOMEventPlugin.ts | 8 +++----- .../lib/editor/Editor.ts | 12 +++++++----- .../lib/plugins/ImageEdit/ImageEdit.ts | 4 +--- .../lib/interface/EditorCore.ts | 3 +-- .../lib/interface/SelectionRangeEx.ts | 4 ---- 7 files changed, 19 insertions(+), 37 deletions(-) diff --git a/demo/scripts/controls/sidePane/apiPlayground/getSelection/getSelectionPane.tsx b/demo/scripts/controls/sidePane/apiPlayground/getSelection/getSelectionPane.tsx index 6db979cfef0..b535f3a8000 100644 --- a/demo/scripts/controls/sidePane/apiPlayground/getSelection/getSelectionPane.tsx +++ b/demo/scripts/controls/sidePane/apiPlayground/getSelection/getSelectionPane.tsx @@ -42,11 +42,11 @@ export default class GetSelectionPane extends React.Component { + private updateSelection = () => { this.setState({ selection: this.editor.getSelectionRangeEx(), }); @@ -127,7 +127,7 @@ export default class GetSelectionPane extends React.Component -
Image Id: {this.state.selection.imageId}
+
Image Id: {this.state.selection.image.id}
)}
diff --git a/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts b/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts index 1fc3128aeff..bb351b13c0e 100644 --- a/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts +++ b/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts @@ -9,25 +9,14 @@ import { EditorCore, SelectImage, SelectionRangeTypes } from 'roosterjs-editor-t * @param wrapper Selected image wrapper * @returns Selected image information */ -export const selectImage: SelectImage = ( - core: EditorCore, - image: HTMLImageElement | null, - wrapper?: HTMLSpanElement -) => { - const selectedImage = image - ? image - : wrapper - ? (document.querySelector(`img[id$="${wrapper.id}"]`) as HTMLImageElement) - : null; - if (selectedImage) { - const range = wrapper ? createRange(wrapper) : createRange(selectedImage); - core.api.selectRange(core, range); +export const selectImage: SelectImage = (core: EditorCore, image: HTMLImageElement | null) => { + if (image) { + const range = createRange(image); return { type: SelectionRangeTypes.ImageSelection, ranges: [range], - imageId: selectedImage.id, - image: selectedImage, + image: image, areAllCollapsed: range.collapsed, }; } diff --git a/packages/roosterjs-editor-core/lib/corePlugins/DOMEventPlugin.ts b/packages/roosterjs-editor-core/lib/corePlugins/DOMEventPlugin.ts index eb529e9fa89..bb8dd376879 100644 --- a/packages/roosterjs-editor-core/lib/corePlugins/DOMEventPlugin.ts +++ b/packages/roosterjs-editor-core/lib/corePlugins/DOMEventPlugin.ts @@ -146,18 +146,16 @@ export default class DOMEventPlugin implements PluginWithState { const { table, coordinates } = this.state.tableSelectionRange || {}; + const { image } = this.state.imageSelectionRange || {}; if (table && coordinates) { this.editor.select(table, coordinates); + } else if (image) { + this.editor.select(image); } else { this.editor.select(this.state.selectionRange); } - const { image } = this.state.imageSelectionRange || {}; - if (image) { - this.editor.select(image); - } - this.state.selectionRange = null; }; private onKeyDownDocument = (event: KeyboardEvent) => { diff --git a/packages/roosterjs-editor-core/lib/editor/Editor.ts b/packages/roosterjs-editor-core/lib/editor/Editor.ts index 1f039abed01..7ee61f04354 100644 --- a/packages/roosterjs-editor-core/lib/editor/Editor.ts +++ b/packages/roosterjs-editor-core/lib/editor/Editor.ts @@ -500,22 +500,24 @@ export default class Editor implements IEditor { private getImageSelection( core: EditorCore, - arg: Range | NodePosition | Node | SelectionPath | HTMLTableElement | null + arg: Range | NodePosition | Node | SelectionPath | HTMLTableElement | null, + arg2?: NodePosition | number | PositionType | TableSelection ) { - if (safeInstanceOf(arg, 'HTMLImageElement')) { + if (safeInstanceOf(arg, 'HTMLImageElement') && !arg2) { const selection = core.api.selectImage(core, arg); return selection; } - if (arg && safeInstanceOf(arg, 'HTMLSpanElement')) { + + if (arg && safeInstanceOf(arg, 'HTMLSpanElement') && !arg2) { const argElement = arg; const argClass = argElement.className; if (argClass.indexOf('IMAGE_EDIT_WRAPPER') >= 0) { - const selection = core.api.selectImage(core, null /** image **/, argElement); + const image = argElement.getElementsByTagName('img')[0]; + const selection = core.api.selectImage(core, image); return selection; } } else { core.api.selectImage(core, null); - return null; } } diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts index 3fc2842d048..8a94e24ccef 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts @@ -183,6 +183,7 @@ export default class ImageEdit implements EditorPlugin { this.disposer(); this.disposer = null; this.editor = null; + this.idNumber = 0; } /** @@ -375,9 +376,6 @@ export default class ImageEdit implements EditorPlugin { this.idNumber = this.idNumber + 1; const imageId = IMAGE_SELECTED + this.idNumber; this.image.id = imageId; - wrapper.id = imageId; - } else { - wrapper.id = this.image.id; } wrapper.style.position = 'relative'; diff --git a/packages/roosterjs-editor-types/lib/interface/EditorCore.ts b/packages/roosterjs-editor-types/lib/interface/EditorCore.ts index 94f39363ffe..a0f44ac39e2 100644 --- a/packages/roosterjs-editor-types/lib/interface/EditorCore.ts +++ b/packages/roosterjs-editor-types/lib/interface/EditorCore.ts @@ -282,8 +282,7 @@ export type SelectTable = ( */ export type SelectImage = ( core: EditorCore, - image: HTMLImageElement | null, - wrapper?: HTMLSpanElement + image: HTMLImageElement | null ) => ImageSelectionRange | null; /** diff --git a/packages/roosterjs-editor-types/lib/interface/SelectionRangeEx.ts b/packages/roosterjs-editor-types/lib/interface/SelectionRangeEx.ts index 3f9b0bc071a..4fb23a010cd 100644 --- a/packages/roosterjs-editor-types/lib/interface/SelectionRangeEx.ts +++ b/packages/roosterjs-editor-types/lib/interface/SelectionRangeEx.ts @@ -52,10 +52,6 @@ export interface ImageSelectionRange * Selected Image */ image: HTMLImageElement; - /** - * Id of the selected Image - */ - imageId: string | undefined; } /** From c6064bf1b1c0ba66a403a1494591476ffc4b1e00 Mon Sep 17 00:00:00 2001 From: Bi Wu Date: Mon, 19 Sep 2022 17:32:05 -0700 Subject: [PATCH 17/41] add initial test --- .../ContentEdit/features/markdownFeatures.ts | 13 +- .../features/markdownFeaturesTest.ts | 132 ++++++++++++++++++ 2 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 packages/roosterjs-editor-plugins/test/ContentEdit/features/markdownFeaturesTest.ts diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/markdownFeatures.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/markdownFeatures.ts index 3f08301f087..41dc82b3b69 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/markdownFeatures.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/markdownFeatures.ts @@ -43,7 +43,7 @@ function cacheGetRangeForMarkdownOperation( let startPosition: NodePosition; let endPosition: NodePosition; - searcher.forEachTextInlineElement(textInlineElement => { + searcher?.forEachTextInlineElement(textInlineElement => { if (endPosition && startPosition) { return true; } @@ -102,7 +102,8 @@ function handleMarkdownEvent( ); // set the removal range to include the typed last character. - range.setEnd(range.endContainer, range.endOffset + 1); + const lastIndex: number = (range.endContainer as Text).length; + range.setEnd(range.endContainer, lastIndex); // extract content and put it into a new element. const elementToWrap = editor.getDocument().createElement(elementTag); @@ -130,7 +131,7 @@ const MarkdownBold: BuildInEditFeature = generateBasicMarkd Keys.EIGHT_ASTERISK, '*', 'b', - true + true /* useShiftKey */ ); /** @@ -140,7 +141,7 @@ const MarkdownItalic: BuildInEditFeature = generateBasicMar Keys.DASH_UNDERSCORE, '_', 'i', - true + true /* useShiftKey */ ); /** @@ -150,7 +151,7 @@ const MarkdownStrikethrough: BuildInEditFeature = generateB Keys.GRAVE_TILDE, '~', 's', - true + true /* useShiftKey */ ); /** @@ -160,7 +161,7 @@ const MarkdownInlineCode: BuildInEditFeature = generateBasi Keys.GRAVE_TILDE, '`', 'code', - false + false /* useShiftKey */ ); /** diff --git a/packages/roosterjs-editor-plugins/test/ContentEdit/features/markdownFeaturesTest.ts b/packages/roosterjs-editor-plugins/test/ContentEdit/features/markdownFeaturesTest.ts new file mode 100644 index 00000000000..212fe05bc9c --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/ContentEdit/features/markdownFeaturesTest.ts @@ -0,0 +1,132 @@ +import * as TestHelper from '../../../../roosterjs-editor-api/test/TestHelper'; +import { MarkdownFeatures } from '../../../lib/plugins/ContentEdit/features/markdownFeatures'; +import { + IEditor, + PluginEventType, + PluginKeyboardEvent, + PositionType, + //Keys, +} from 'roosterjs-editor-types'; + +describe('MarkdownFeatures | ', () => { + let editor: IEditor; + const TEST_ID = 'MarkDownFeatureTest'; + const TEST_ELEMENT_ID = 'MarkDownFeatureTestElementId'; + // const leftKey = Keys.LEFT; + //const rightKey = Keys.RIGHT; + + beforeEach(done => { + editor = TestHelper.initEditor(TEST_ID); + editor.runAsync = (callback: (editor: IEditor) => void) => { + callback(editor); + return () => {}; + }; + done(); + }); + + afterEach(() => { + let element = document.getElementById(TEST_ID); + if (element) { + element.parentElement?.removeChild(element); + } + editor.dispose(); + }); + + const keyboardEvent = (whichInput?: number) => { + return new KeyboardEvent('keydown', { + shiftKey: true, + altKey: false, + ctrlKey: false, + cancelable: true, + which: whichInput, + }); + }; + + /*function runShouldHandleEvent( + content: string, + shouldHandleExpect: boolean, + rawKeyboardEvent: KeyboardEvent + //selectContentCallback: (element: HTMLElement) => void, + //rtl: boolean = false + ) { + const keyboardPluginEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: rawKeyboardEvent, + }; + editor.setContent(content); + const markdownBold = MarkdownFeatures.markdownBold; + //const element = document.getElementById(TEST_ELEMENT_ID); + + editor.focus(); + //selectContentCallback(element); + const result = markdownBold.shouldHandleEvent(keyboardPluginEvent, editor); + + expect(!!result).toBe(shouldHandleExpect); + }*/ + + /*function runHandleEvent(rawEvent: KeyboardEvent, expected: boolean) { + const keyboardPluginEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: rawEvent, + }; + const cycleCursorMove = CursorFeatures.noCycleCursorMove; + cycleCursorMove.handleEvent(keyboardPluginEvent, editor); + + expect(keyboardPluginEvent.rawEvent.defaultPrevented).toBe(expected); + }*/ + + function runHandleEvent(rawEvent: KeyboardEvent, expected: boolean) { + const keyboardPluginEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: rawEvent, + }; + const markdownBold = MarkdownFeatures.markdownBold; + markdownBold.handleEvent(keyboardPluginEvent, editor); + + const element = document.getElementById(TEST_ELEMENT_ID); + const styledContent = element?.innerHTML; + expect(styledContent).toBeTruthy; + } + + describe('MarkdownBold | ', () => { + const markdownBold = MarkdownFeatures.markdownBold; + describe('Should Handle Event | ', () => { + it('Should handle, is Markdown style bolding', () => { + editor.setContent(`
*abcd
`); + editor.focus(); + const element = document.getElementById(TEST_ELEMENT_ID); + editor.select(element, PositionType.End); + //editor.select(element, PositionType.End, element, PositionType.Begin); + //editor.select(document.getElementById('TEST_ELEMENT_ID')!, 0); + // + const keyboardPluginEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: keyboardEvent(Number('*')), + }; + const shouldHandleEvent = markdownBold.shouldHandleEvent( + keyboardPluginEvent, + editor, + false + ); + expect(!!shouldHandleEvent).toBe(true); + }); + }); + }); + + describe('Handle Event | ', () => { + it('HandleEvent 2', () => { + editor.setContent(`
*abcd
`); + editor.focus(); + const element = document.getElementById(TEST_ELEMENT_ID); + editor.select(element, PositionType.End); + const rawEvent = new KeyboardEvent('keydown', { + shiftKey: true, + altKey: false, + ctrlKey: false, + cancelable: false, + }); + + runHandleEvent(rawEvent, false); + }); + }); +}); From 1635031c83f8409be0362950695978db1808f04c Mon Sep 17 00:00:00 2001 From: Bi Wu Date: Mon, 19 Sep 2022 18:52:43 -0700 Subject: [PATCH 18/41] refactor 1 --- .../features/markdownFeaturesTest.ts | 134 +++++++++--------- 1 file changed, 64 insertions(+), 70 deletions(-) diff --git a/packages/roosterjs-editor-plugins/test/ContentEdit/features/markdownFeaturesTest.ts b/packages/roosterjs-editor-plugins/test/ContentEdit/features/markdownFeaturesTest.ts index 212fe05bc9c..e23f7b8858b 100644 --- a/packages/roosterjs-editor-plugins/test/ContentEdit/features/markdownFeaturesTest.ts +++ b/packages/roosterjs-editor-plugins/test/ContentEdit/features/markdownFeaturesTest.ts @@ -1,19 +1,17 @@ -import * as TestHelper from '../../../../roosterjs-editor-api/test/TestHelper'; -import { MarkdownFeatures } from '../../../lib/plugins/ContentEdit/features/markdownFeatures'; import { + BuildInEditFeature, IEditor, PluginEventType, PluginKeyboardEvent, PositionType, - //Keys, } from 'roosterjs-editor-types'; +import * as TestHelper from '../../../../roosterjs-editor-api/test/TestHelper'; +import { MarkdownFeatures } from '../../../lib/plugins/ContentEdit/features/markdownFeatures'; describe('MarkdownFeatures | ', () => { let editor: IEditor; const TEST_ID = 'MarkDownFeatureTest'; const TEST_ELEMENT_ID = 'MarkDownFeatureTestElementId'; - // const leftKey = Keys.LEFT; - //const rightKey = Keys.RIGHT; beforeEach(done => { editor = TestHelper.initEditor(TEST_ID); @@ -42,91 +40,87 @@ describe('MarkdownFeatures | ', () => { }); }; - /*function runShouldHandleEvent( + function runShouldHandleEvent( content: string, shouldHandleExpect: boolean, - rawKeyboardEvent: KeyboardEvent - //selectContentCallback: (element: HTMLElement) => void, - //rtl: boolean = false + markdownFeature: BuildInEditFeature ) { + editor.setContent(`
${content}
`); + const element = document.getElementById(TEST_ELEMENT_ID); + editor.select(element, PositionType.End); const keyboardPluginEvent: PluginKeyboardEvent = { eventType: PluginEventType.KeyDown, - rawEvent: rawKeyboardEvent, + rawEvent: keyboardEvent(), }; - editor.setContent(content); - const markdownBold = MarkdownFeatures.markdownBold; - //const element = document.getElementById(TEST_ELEMENT_ID); - - editor.focus(); - //selectContentCallback(element); - const result = markdownBold.shouldHandleEvent(keyboardPluginEvent, editor); - - expect(!!result).toBe(shouldHandleExpect); - }*/ + const shouldHandleEvent = markdownFeature.shouldHandleEvent( + keyboardPluginEvent, + editor, + false /* ctrlOrMeta */ + ); + expect(!!shouldHandleEvent).toBe(shouldHandleExpect); + } - /*function runHandleEvent(rawEvent: KeyboardEvent, expected: boolean) { + function runHandleEvent( + markdownFeature: BuildInEditFeature, + testContent: string, + expectedContent: string + ) { + editor.setContent(`
${testContent}
`); + const element = document.getElementById(TEST_ELEMENT_ID); + editor.select(element, PositionType.End); const keyboardPluginEvent: PluginKeyboardEvent = { eventType: PluginEventType.KeyDown, - rawEvent: rawEvent, + rawEvent: new KeyboardEvent('keydown', { + shiftKey: true, + altKey: false, + ctrlKey: false, + cancelable: false, + }), }; - const cycleCursorMove = CursorFeatures.noCycleCursorMove; - cycleCursorMove.handleEvent(keyboardPluginEvent, editor); + markdownFeature.handleEvent(keyboardPluginEvent, editor); + const styledContent: string = element!.innerHTML; - expect(keyboardPluginEvent.rawEvent.defaultPrevented).toBe(expected); - }*/ + expect(styledContent).toContain(expectedContent); + } - function runHandleEvent(rawEvent: KeyboardEvent, expected: boolean) { - const keyboardPluginEvent: PluginKeyboardEvent = { - eventType: PluginEventType.KeyDown, - rawEvent: rawEvent, - }; - const markdownBold = MarkdownFeatures.markdownBold; - markdownBold.handleEvent(keyboardPluginEvent, editor); + describe('Should Handle Event | ', () => { + describe('MarkdownBold | ', () => { + const markdownBold = MarkdownFeatures.markdownBold; - const element = document.getElementById(TEST_ELEMENT_ID); - const styledContent = element?.innerHTML; - expect(styledContent).toBeTruthy; - } + fit('Should handle in normal scenario 1', () => { + runShouldHandleEvent('*abcd', true /* shouldHandleExpect */, markdownBold); + }); - describe('MarkdownBold | ', () => { - const markdownBold = MarkdownFeatures.markdownBold; - describe('Should Handle Event | ', () => { - it('Should handle, is Markdown style bolding', () => { - editor.setContent(`
*abcd
`); - editor.focus(); - const element = document.getElementById(TEST_ELEMENT_ID); - editor.select(element, PositionType.End); - //editor.select(element, PositionType.End, element, PositionType.Begin); - //editor.select(document.getElementById('TEST_ELEMENT_ID')!, 0); - // - const keyboardPluginEvent: PluginKeyboardEvent = { - eventType: PluginEventType.KeyDown, - rawEvent: keyboardEvent(Number('*')), - }; - const shouldHandleEvent = markdownBold.shouldHandleEvent( - keyboardPluginEvent, - editor, - false + fit('Should handle in normal scenario 2', () => { + runShouldHandleEvent('*abcd~', true /* shouldHandleExpect */, markdownBold); + }); + + fit('Should handle in normal scenario 3', () => { + runShouldHandleEvent( + '*abcd defi 1234', + true /* shouldHandleExpect */, + markdownBold ); - expect(!!shouldHandleEvent).toBe(true); + }); + + fit('Should not handle because of preceding whitespace', () => { + runShouldHandleEvent('*abcd ', false /* shouldHandleExpect */, markdownBold); + }); + + fit('Should not handle because of preceding trigger character', () => { + runShouldHandleEvent('*abcd*', false /* shouldHandleExpect */, markdownBold); + }); + + fit('Should not handle because of multiple whitespace', () => { + runShouldHandleEvent('*abcd ', false /* shouldHandleExpect */, markdownBold); }); }); }); describe('Handle Event | ', () => { - it('HandleEvent 2', () => { - editor.setContent(`
*abcd
`); - editor.focus(); - const element = document.getElementById(TEST_ELEMENT_ID); - editor.select(element, PositionType.End); - const rawEvent = new KeyboardEvent('keydown', { - shiftKey: true, - altKey: false, - ctrlKey: false, - cancelable: false, - }); - - runHandleEvent(rawEvent, false); + const markdownBold = MarkdownFeatures.markdownBold; + fit('HandleEvent 2', () => { + runHandleEvent(markdownBold, '*abcd', 'abcd'); }); }); }); From 6a8fbd7ce48974264cc79c3ae0b35966ec82ad9e Mon Sep 17 00:00:00 2001 From: Bi Wu Date: Mon, 19 Sep 2022 22:10:03 -0700 Subject: [PATCH 19/41] improve --- .../features/markdownFeaturesTest.ts | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/packages/roosterjs-editor-plugins/test/ContentEdit/features/markdownFeaturesTest.ts b/packages/roosterjs-editor-plugins/test/ContentEdit/features/markdownFeaturesTest.ts index e23f7b8858b..83c8871251d 100644 --- a/packages/roosterjs-editor-plugins/test/ContentEdit/features/markdownFeaturesTest.ts +++ b/packages/roosterjs-editor-plugins/test/ContentEdit/features/markdownFeaturesTest.ts @@ -12,6 +12,8 @@ describe('MarkdownFeatures | ', () => { let editor: IEditor; const TEST_ID = 'MarkDownFeatureTest'; const TEST_ELEMENT_ID = 'MarkDownFeatureTestElementId'; + // here we only test bolding as the logic for other styling is the same + const markdownBold = MarkdownFeatures.markdownBold; beforeEach(done => { editor = TestHelper.initEditor(TEST_ID); @@ -85,17 +87,15 @@ describe('MarkdownFeatures | ', () => { describe('Should Handle Event | ', () => { describe('MarkdownBold | ', () => { - const markdownBold = MarkdownFeatures.markdownBold; - - fit('Should handle in normal scenario 1', () => { + it('Should handle in normal scenario 1', () => { runShouldHandleEvent('*abcd', true /* shouldHandleExpect */, markdownBold); }); - fit('Should handle in normal scenario 2', () => { + it('Should handle in normal scenario 2', () => { runShouldHandleEvent('*abcd~', true /* shouldHandleExpect */, markdownBold); }); - fit('Should handle in normal scenario 3', () => { + it('Should handle in normal scenario 3', () => { runShouldHandleEvent( '*abcd defi 1234', true /* shouldHandleExpect */, @@ -103,24 +103,39 @@ describe('MarkdownFeatures | ', () => { ); }); - fit('Should not handle because of preceding whitespace', () => { + it('Should handle in normal scenario 4', () => { + runShouldHandleEvent('abcd *1234', true /* shouldHandleExpect */, markdownBold); + }); + + it('Should not handle because of preceding whitespace', () => { runShouldHandleEvent('*abcd ', false /* shouldHandleExpect */, markdownBold); }); - fit('Should not handle because of preceding trigger character', () => { + it('Should not handle because of preceding trigger character', () => { runShouldHandleEvent('*abcd*', false /* shouldHandleExpect */, markdownBold); }); - fit('Should not handle because of multiple whitespace', () => { + it('Should not handle because of multiple whitespace', () => { runShouldHandleEvent('*abcd ', false /* shouldHandleExpect */, markdownBold); }); + + it('Should not handle because of wrong starting trigger character', () => { + runShouldHandleEvent('-abcd', false /* shouldHandleExpect */, markdownBold); + }); }); }); describe('Handle Event | ', () => { - const markdownBold = MarkdownFeatures.markdownBold; - fit('HandleEvent 2', () => { + it('markdownBold normal scenario 1', () => { runHandleEvent(markdownBold, '*abcd', 'abcd'); }); + + it('markdownBold normal scenario 2', () => { + runHandleEvent(markdownBold, '*abcd 123', 'abcd 123'); + }); + + it('markdownBold normal scenario 3', () => { + runHandleEvent(markdownBold, 'abcd *123', 'abcd 123'); + }); }); }); From df30f45c8e3ae404c60e72ab9a5398dd414b4357 Mon Sep 17 00:00:00 2001 From: Bi Wu Date: Mon, 19 Sep 2022 22:14:25 -0700 Subject: [PATCH 20/41] remove nest --- .../features/markdownFeaturesTest.ts | 66 +++++++++---------- 1 file changed, 30 insertions(+), 36 deletions(-) diff --git a/packages/roosterjs-editor-plugins/test/ContentEdit/features/markdownFeaturesTest.ts b/packages/roosterjs-editor-plugins/test/ContentEdit/features/markdownFeaturesTest.ts index 83c8871251d..e895cdcdc41 100644 --- a/packages/roosterjs-editor-plugins/test/ContentEdit/features/markdownFeaturesTest.ts +++ b/packages/roosterjs-editor-plugins/test/ContentEdit/features/markdownFeaturesTest.ts @@ -86,42 +86,36 @@ describe('MarkdownFeatures | ', () => { } describe('Should Handle Event | ', () => { - describe('MarkdownBold | ', () => { - it('Should handle in normal scenario 1', () => { - runShouldHandleEvent('*abcd', true /* shouldHandleExpect */, markdownBold); - }); - - it('Should handle in normal scenario 2', () => { - runShouldHandleEvent('*abcd~', true /* shouldHandleExpect */, markdownBold); - }); - - it('Should handle in normal scenario 3', () => { - runShouldHandleEvent( - '*abcd defi 1234', - true /* shouldHandleExpect */, - markdownBold - ); - }); - - it('Should handle in normal scenario 4', () => { - runShouldHandleEvent('abcd *1234', true /* shouldHandleExpect */, markdownBold); - }); - - it('Should not handle because of preceding whitespace', () => { - runShouldHandleEvent('*abcd ', false /* shouldHandleExpect */, markdownBold); - }); - - it('Should not handle because of preceding trigger character', () => { - runShouldHandleEvent('*abcd*', false /* shouldHandleExpect */, markdownBold); - }); - - it('Should not handle because of multiple whitespace', () => { - runShouldHandleEvent('*abcd ', false /* shouldHandleExpect */, markdownBold); - }); - - it('Should not handle because of wrong starting trigger character', () => { - runShouldHandleEvent('-abcd', false /* shouldHandleExpect */, markdownBold); - }); + it('Should handle in normal scenario 1', () => { + runShouldHandleEvent('*abcd', true /* shouldHandleExpect */, markdownBold); + }); + + it('Should handle in normal scenario 2', () => { + runShouldHandleEvent('*abcd~', true /* shouldHandleExpect */, markdownBold); + }); + + it('Should handle in normal scenario 3', () => { + runShouldHandleEvent('*abcd defi 1234', true /* shouldHandleExpect */, markdownBold); + }); + + it('Should handle in normal scenario 4', () => { + runShouldHandleEvent('abcd *1234', true /* shouldHandleExpect */, markdownBold); + }); + + it('Should not handle because of preceding whitespace', () => { + runShouldHandleEvent('*abcd ', false /* shouldHandleExpect */, markdownBold); + }); + + it('Should not handle because of preceding trigger character', () => { + runShouldHandleEvent('*abcd*', false /* shouldHandleExpect */, markdownBold); + }); + + it('Should not handle because of multiple whitespace', () => { + runShouldHandleEvent('*abcd ', false /* shouldHandleExpect */, markdownBold); + }); + + it('Should not handle because of wrong starting trigger character', () => { + runShouldHandleEvent('-abcd', false /* shouldHandleExpect */, markdownBold); }); }); From 9cb9c467d1bfd6d025e7a883a7ab681dc62ae2cf Mon Sep 17 00:00:00 2001 From: Bi Wu Date: Mon, 19 Sep 2022 22:21:00 -0700 Subject: [PATCH 21/41] update comment --- .../test/ContentEdit/features/markdownFeaturesTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/roosterjs-editor-plugins/test/ContentEdit/features/markdownFeaturesTest.ts b/packages/roosterjs-editor-plugins/test/ContentEdit/features/markdownFeaturesTest.ts index e895cdcdc41..b0c309d89e6 100644 --- a/packages/roosterjs-editor-plugins/test/ContentEdit/features/markdownFeaturesTest.ts +++ b/packages/roosterjs-editor-plugins/test/ContentEdit/features/markdownFeaturesTest.ts @@ -12,7 +12,7 @@ describe('MarkdownFeatures | ', () => { let editor: IEditor; const TEST_ID = 'MarkDownFeatureTest'; const TEST_ELEMENT_ID = 'MarkDownFeatureTestElementId'; - // here we only test bolding as the logic for other styling is the same + // Here we only test bolding as the logic for other styling is exactly the same const markdownBold = MarkdownFeatures.markdownBold; beforeEach(done => { From 620a9c68dab2dd0e9faa1bcd7a0ad55fb61e56b3 Mon Sep 17 00:00:00 2001 From: Bryan Valverde U Date: Tue, 20 Sep 2022 09:43:11 -0600 Subject: [PATCH 22/41] Try fix Unstable test by fixing clean up after each test #1277 --- .../test/coreApi/selectTableTest.ts | 50 +++++++++---------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/packages/roosterjs-editor-core/test/coreApi/selectTableTest.ts b/packages/roosterjs-editor-core/test/coreApi/selectTableTest.ts index 099cbd501ef..ae15d499bac 100644 --- a/packages/roosterjs-editor-core/test/coreApi/selectTableTest.ts +++ b/packages/roosterjs-editor-core/test/coreApi/selectTableTest.ts @@ -4,34 +4,32 @@ import { EditorCore, TableSelection } from 'roosterjs-editor-types'; import { selectTable } from '../../lib/coreApi/selectTable'; describe('selectTable |', () => { - let div: HTMLDivElement; + let div: HTMLDivElement | null; let table: HTMLTableElement | null; let core: EditorCore | null; beforeEach(() => { - document.body.innerHTML = ''; div = document.createElement('div'); - div.innerHTML = buildTableHTML(true /* tbody */); + div!.innerHTML = buildTableHTML(true /* tbody */); - table = div.querySelector('table'); - document.body.appendChild(div); + table = div!.querySelector('table'); + document.body.appendChild(div!); - core = createEditorCore(div, {}); + core = createEditorCore(div!, {}); }); afterEach(() => { - document.body.removeChild(div); - let style = document.getElementById('tableStylecontentDiv_0'); - if (style) { - document.head.removeChild(style); - } + let styles = document.querySelectorAll('#tableStylecontentDiv_0'); + styles.forEach(s => s.parentElement?.removeChild(s)); core = null; - div.parentElement?.removeChild(div); + div = null; + table = null; + document.body.innerHTML = ''; }); it('Select Table Cells TR under Table Tag', () => { - div.innerHTML = + div!.innerHTML = '
TestTest
TestTest

'; selectTable(core, table, { @@ -66,9 +64,9 @@ describe('selectTable |', () => { }); it('Select TH and TR in the same row', () => { - div.innerHTML = + div!.innerHTML = '
TestTest
TestTest

'; - table = div.querySelector('table'); + table = div!.querySelector('table'); selectTable(core, table, { firstCell: { x: 0, y: 0 }, @@ -86,9 +84,9 @@ describe('selectTable |', () => { }); it('Select Table Cells THEAD, TBODY', () => { - div.innerHTML = buildTableHTML(true /* tbody */, true /* thead */); + div!.innerHTML = buildTableHTML(true /* tbody */, true /* thead */); - table = div.querySelector('table'); + table = div!.querySelector('table'); selectTable(core, table, { firstCell: { x: 1, y: 1 }, @@ -106,9 +104,9 @@ describe('selectTable |', () => { }); it('Select Table Cells TBODY, TFOOT', () => { - div.innerHTML = buildTableHTML(true /* tbody */, false /* thead */, true /* tfoot */); + div!.innerHTML = buildTableHTML(true /* tbody */, false /* thead */, true /* tfoot */); - table = div.querySelector('table'); + table = div!.querySelector('table'); selectTable(core, table, { firstCell: { x: 1, y: 1 }, @@ -126,8 +124,8 @@ describe('selectTable |', () => { }); it('Select Table Cells THEAD, TBODY, TFOOT', () => { - div.innerHTML = buildTableHTML(true /* tbody */, true /* thead */, true /* tfoot */); - table = div.querySelector('table'); + div!.innerHTML = buildTableHTML(true /* tbody */, true /* thead */, true /* tfoot */); + table = div!.querySelector('table'); selectTable(core, table, { firstCell: { x: 1, y: 1 }, @@ -145,8 +143,8 @@ describe('selectTable |', () => { }); it('Select Table Cells THEAD, TFOOT', () => { - div.innerHTML = buildTableHTML(false /* tbody */, true /* thead */, true /* tfoot */); - table = div.querySelector('table'); + div!.innerHTML = buildTableHTML(false /* tbody */, true /* thead */, true /* tfoot */); + table = div!.querySelector('table'); selectTable(core, table, { firstCell: { x: 1, y: 1 }, @@ -165,9 +163,9 @@ describe('selectTable |', () => { it('remove duplicated ID', () => { const tableHTML = buildTableHTML(true); - div.innerHTML = tableHTML + '' + tableHTML; + div!.innerHTML = tableHTML + '' + tableHTML; - const tables = div.querySelectorAll('table'); + const tables = div!.querySelectorAll('table'); table = tables[0]; tables.forEach(table => (table.id = 'DuplicatedId')); @@ -181,7 +179,7 @@ describe('selectTable |', () => { describe('Null scenarios |', () => { it('Null table selection', () => { - const core = createEditorCore(div, {}); + const core = createEditorCore(div!, {}); selectTable(core, table, null); expect(document.getElementById('tableStylecontentDiv_0')).toBeNull(); From 832571b2ac8b05cf1980c80a09ff04b559788a66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Tue, 20 Sep 2022 14:10:52 -0300 Subject: [PATCH 23/41] selectImageApi --- .../lib/coreApi/selectImage.ts | 6 +-- .../lib/coreApi/switchShadowEdit.ts | 2 +- .../lib/editor/Editor.ts | 6 +-- .../test/coreApi/getSelectionRangeExTest.ts | 28 ++++++++++++++ .../test/coreApi/selectImageTest.ts | 38 +++++++++++++++++++ .../lib/interface/EditorCore.ts | 5 +-- 6 files changed, 73 insertions(+), 12 deletions(-) create mode 100644 packages/roosterjs-editor-core/test/coreApi/selectImageTest.ts diff --git a/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts b/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts index bb351b13c0e..8eb6175cd0c 100644 --- a/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts +++ b/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts @@ -1,15 +1,13 @@ import { createRange } from 'roosterjs-editor-dom'; -import { EditorCore, SelectImage, SelectionRangeTypes } from 'roosterjs-editor-types'; +import { SelectImage, SelectionRangeTypes } from 'roosterjs-editor-types'; /** * @internal * Select a image and save data of the selected range - * @param core The EditorCore object * @param image Image to select - * @param wrapper Selected image wrapper * @returns Selected image information */ -export const selectImage: SelectImage = (core: EditorCore, image: HTMLImageElement | null) => { +export const selectImage: SelectImage = (image: HTMLImageElement | null) => { if (image) { const range = createRange(image); diff --git a/packages/roosterjs-editor-core/lib/coreApi/switchShadowEdit.ts b/packages/roosterjs-editor-core/lib/coreApi/switchShadowEdit.ts index b5fbcc38d1e..3ca4a860ee8 100644 --- a/packages/roosterjs-editor-core/lib/coreApi/switchShadowEdit.ts +++ b/packages/roosterjs-editor-core/lib/coreApi/switchShadowEdit.ts @@ -105,7 +105,7 @@ export const switchShadowEdit: SwitchShadowEdit = (core: EditorCore, isOn: boole const { image } = core.domEvent.imageSelectionRange; const imageElement = core.contentDiv.querySelector('#' + image.id); if (imageElement) { - core.domEvent.imageSelectionRange = core.api.selectImage(core, image); + core.domEvent.imageSelectionRange = core.api.selectImage(image); } } diff --git a/packages/roosterjs-editor-core/lib/editor/Editor.ts b/packages/roosterjs-editor-core/lib/editor/Editor.ts index 7ee61f04354..89d0f40348a 100644 --- a/packages/roosterjs-editor-core/lib/editor/Editor.ts +++ b/packages/roosterjs-editor-core/lib/editor/Editor.ts @@ -504,7 +504,7 @@ export default class Editor implements IEditor { arg2?: NodePosition | number | PositionType | TableSelection ) { if (safeInstanceOf(arg, 'HTMLImageElement') && !arg2) { - const selection = core.api.selectImage(core, arg); + const selection = core.api.selectImage(arg); return selection; } @@ -513,11 +513,11 @@ export default class Editor implements IEditor { const argClass = argElement.className; if (argClass.indexOf('IMAGE_EDIT_WRAPPER') >= 0) { const image = argElement.getElementsByTagName('img')[0]; - const selection = core.api.selectImage(core, image); + const selection = core.api.selectImage(image); return selection; } } else { - core.api.selectImage(core, null); + core.api.selectImage(null); } } diff --git a/packages/roosterjs-editor-core/test/coreApi/getSelectionRangeExTest.ts b/packages/roosterjs-editor-core/test/coreApi/getSelectionRangeExTest.ts index 1e909c06c1e..ee4b7813824 100644 --- a/packages/roosterjs-editor-core/test/coreApi/getSelectionRangeExTest.ts +++ b/packages/roosterjs-editor-core/test/coreApi/getSelectionRangeExTest.ts @@ -1,6 +1,7 @@ import createEditorCore from './createMockEditorCore'; import { focus } from '../../lib/coreApi/focus'; import { getSelectionRangeEx } from '../../lib/coreApi/getSelectionRangeEx'; +import { SelectionRangeTypes } from 'roosterjs-editor-types'; import { selectNode } from '../TestHelper'; describe('getSelectionRangeEx', () => { @@ -86,6 +87,33 @@ describe('getSelectionRangeEx', () => { }); }); + it('image selection', () => { + div.innerHTML = ''; + const image = div.querySelector('img'); + const core = createEditorCore(div, {}); + const range = new Range(); + range.selectNode(image!); + core.domEvent = { + selectionRange: range, + isInIME: false, + scrollContainer: null, + stopPrintableKeyboardEventPropagation: false, + contextMenuProviders: [], + tableSelectionRange: null, + imageSelectionRange: { + type: SelectionRangeTypes.ImageSelection, + ranges: [range], + image: image, + areAllCollapsed: range.collapsed, + }, + }; + focus(core); + + const selectionEx = getSelectionRangeEx(core); + expect(selectionEx.type).toBe(SelectionRangeTypes.ImageSelection); + expect(selectionEx.ranges).toEqual([range]); + }); + function runTest(input: string, id: string, expectedRangesLength: number[][]) { const core = createEditorCore(div, {}); diff --git a/packages/roosterjs-editor-core/test/coreApi/selectImageTest.ts b/packages/roosterjs-editor-core/test/coreApi/selectImageTest.ts new file mode 100644 index 00000000000..eb57af8e1f9 --- /dev/null +++ b/packages/roosterjs-editor-core/test/coreApi/selectImageTest.ts @@ -0,0 +1,38 @@ +import { selectImage } from '../../lib/coreApi/selectImage'; +import { SelectionRangeTypes } from 'roosterjs-editor-types'; + +describe('selectImage |', () => { + let div: HTMLDivElement; + let image: HTMLImageElement | null; + + beforeEach(() => { + document.body.innerHTML = ''; + div = document.createElement('div'); + div.innerHTML = ''; + + image = div.querySelector('img'); + document.body.appendChild(div); + }); + + afterEach(() => { + document.body.removeChild(div); + let style = document.getElementById('imageSelected1'); + if (style) { + document.head.removeChild(style); + } + div.parentElement?.removeChild(div); + }); + + it('selectImage', () => { + const selectedInfo = selectImage(image); + const range = new Range(); + range.selectNode(image!); + + expect(selectedInfo).toEqual({ + type: SelectionRangeTypes.ImageSelection, + ranges: [range], + image: image, + areAllCollapsed: range.collapsed, + }); + }); +}); diff --git a/packages/roosterjs-editor-types/lib/interface/EditorCore.ts b/packages/roosterjs-editor-types/lib/interface/EditorCore.ts index a0f44ac39e2..9e4c52f0de3 100644 --- a/packages/roosterjs-editor-types/lib/interface/EditorCore.ts +++ b/packages/roosterjs-editor-types/lib/interface/EditorCore.ts @@ -280,10 +280,7 @@ export type SelectTable = ( * selecting, will unselect the table. * @returns true if successful */ -export type SelectImage = ( - core: EditorCore, - image: HTMLImageElement | null -) => ImageSelectionRange | null; +export type SelectImage = (image: HTMLImageElement | null) => ImageSelectionRange | null; /** * The interface for the map of core API. From 6f619560f5df7a7c15705d753e7fd71a49f289b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Tue, 20 Sep 2022 14:18:07 -0300 Subject: [PATCH 24/41] tag --- .../roosterjs-editor-core/lib/coreApi/getSelectionRangeEx.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/roosterjs-editor-core/lib/coreApi/getSelectionRangeEx.ts b/packages/roosterjs-editor-core/lib/coreApi/getSelectionRangeEx.ts index 899c0272bcc..1687d6c5feb 100644 --- a/packages/roosterjs-editor-core/lib/coreApi/getSelectionRangeEx.ts +++ b/packages/roosterjs-editor-core/lib/coreApi/getSelectionRangeEx.ts @@ -48,7 +48,7 @@ export const getSelectionRangeEx: GetSelectionRangeEx = (core: EditorCore) => { image: findClosestElementAncestor( ranges[0].startContainer, core.contentDiv, - 'image' + 'img' ) as HTMLImageElement, imageId: undefined, }; From d969c2a86716b09e75eb9e69b662912a3561d93c Mon Sep 17 00:00:00 2001 From: Bryan Valverde U Date: Tue, 20 Sep 2022 11:46:20 -0600 Subject: [PATCH 25/41] Add debug unit test commands to launch.json file #1278 --- .vscode/launch.json | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/.vscode/launch.json b/.vscode/launch.json index 083e93f229d..297b6d99801 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -33,6 +33,44 @@ "args": ["start", "--chrome", "--components", "${fileBasenameNoExtension}"], "console": "integratedTerminal" }, + { + "type": "node", + "request": "launch", + "name": "Debug All Unit Tests", + "program": "${workspaceFolder}/node_modules/karma/bin/karma", + "args": ["start", "--no-single-run"], + "console": "integratedTerminal" + }, + { + "type": "node", + "request": "launch", + "name": "Debug All Unit Tests (Chrome)", + "program": "${workspaceFolder}/node_modules/karma/bin/karma", + "args": ["start", "--chrome", "--no-single-run"], + "console": "integratedTerminal" + }, + { + "type": "node", + "request": "launch", + "name": "Debug current unit test file", + "program": "${workspaceFolder}/node_modules/karma/bin/karma", + "args": ["start", "--components", "${fileBasenameNoExtension}", "--no-single-run"], + "console": "integratedTerminal" + }, + { + "type": "node", + "request": "launch", + "name": "Debug current unit test file (Chrome)", + "program": "${workspaceFolder}/node_modules/karma/bin/karma", + "args": [ + "start", + "--chrome", + "--components", + "${fileBasenameNoExtension}", + "--no-single-run" + ], + "console": "integratedTerminal" + }, { "type": "chrome", "request": "launch", From 82f1ed2bca3b088bb032e1240407484222ce2b3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Tue, 20 Sep 2022 14:56:27 -0300 Subject: [PATCH 26/41] core adding image range --- .../roosterjs-editor-core/lib/coreApi/selectImage.ts | 6 ++++-- .../lib/coreApi/switchShadowEdit.ts | 2 +- packages/roosterjs-editor-core/lib/editor/Editor.ts | 6 +++--- .../test/coreApi/selectImageTest.ts | 8 ++++++-- .../roosterjs-editor-types/lib/interface/EditorCore.ts | 9 +++++---- 5 files changed, 19 insertions(+), 12 deletions(-) diff --git a/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts b/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts index 8eb6175cd0c..2b47db5f8de 100644 --- a/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts +++ b/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts @@ -1,15 +1,17 @@ import { createRange } from 'roosterjs-editor-dom'; -import { SelectImage, SelectionRangeTypes } from 'roosterjs-editor-types'; +import { EditorCore, SelectImage, SelectionRangeTypes } from 'roosterjs-editor-types'; /** * @internal * Select a image and save data of the selected range + * @param core The EditorCore object * @param image Image to select * @returns Selected image information */ -export const selectImage: SelectImage = (image: HTMLImageElement | null) => { +export const selectImage: SelectImage = (core: EditorCore, image: HTMLImageElement | null) => { if (image) { const range = createRange(image); + core.api.selectRange(core, range); return { type: SelectionRangeTypes.ImageSelection, diff --git a/packages/roosterjs-editor-core/lib/coreApi/switchShadowEdit.ts b/packages/roosterjs-editor-core/lib/coreApi/switchShadowEdit.ts index 3ca4a860ee8..b5fbcc38d1e 100644 --- a/packages/roosterjs-editor-core/lib/coreApi/switchShadowEdit.ts +++ b/packages/roosterjs-editor-core/lib/coreApi/switchShadowEdit.ts @@ -105,7 +105,7 @@ export const switchShadowEdit: SwitchShadowEdit = (core: EditorCore, isOn: boole const { image } = core.domEvent.imageSelectionRange; const imageElement = core.contentDiv.querySelector('#' + image.id); if (imageElement) { - core.domEvent.imageSelectionRange = core.api.selectImage(image); + core.domEvent.imageSelectionRange = core.api.selectImage(core, image); } } diff --git a/packages/roosterjs-editor-core/lib/editor/Editor.ts b/packages/roosterjs-editor-core/lib/editor/Editor.ts index 89d0f40348a..7ee61f04354 100644 --- a/packages/roosterjs-editor-core/lib/editor/Editor.ts +++ b/packages/roosterjs-editor-core/lib/editor/Editor.ts @@ -504,7 +504,7 @@ export default class Editor implements IEditor { arg2?: NodePosition | number | PositionType | TableSelection ) { if (safeInstanceOf(arg, 'HTMLImageElement') && !arg2) { - const selection = core.api.selectImage(arg); + const selection = core.api.selectImage(core, arg); return selection; } @@ -513,11 +513,11 @@ export default class Editor implements IEditor { const argClass = argElement.className; if (argClass.indexOf('IMAGE_EDIT_WRAPPER') >= 0) { const image = argElement.getElementsByTagName('img')[0]; - const selection = core.api.selectImage(image); + const selection = core.api.selectImage(core, image); return selection; } } else { - core.api.selectImage(null); + core.api.selectImage(core, null); } } diff --git a/packages/roosterjs-editor-core/test/coreApi/selectImageTest.ts b/packages/roosterjs-editor-core/test/coreApi/selectImageTest.ts index eb57af8e1f9..ccfc4e44d1d 100644 --- a/packages/roosterjs-editor-core/test/coreApi/selectImageTest.ts +++ b/packages/roosterjs-editor-core/test/coreApi/selectImageTest.ts @@ -1,9 +1,11 @@ +import createEditorCore from './createMockEditorCore'; +import { EditorCore, SelectionRangeTypes } from 'roosterjs-editor-types'; import { selectImage } from '../../lib/coreApi/selectImage'; -import { SelectionRangeTypes } from 'roosterjs-editor-types'; describe('selectImage |', () => { let div: HTMLDivElement; let image: HTMLImageElement | null; + let core: EditorCore | null; beforeEach(() => { document.body.innerHTML = ''; @@ -12,6 +14,7 @@ describe('selectImage |', () => { image = div.querySelector('img'); document.body.appendChild(div); + core = createEditorCore(div, {}); }); afterEach(() => { @@ -21,10 +24,11 @@ describe('selectImage |', () => { document.head.removeChild(style); } div.parentElement?.removeChild(div); + core = null; }); it('selectImage', () => { - const selectedInfo = selectImage(image); + const selectedInfo = selectImage(core, image); const range = new Range(); range.selectNode(image!); diff --git a/packages/roosterjs-editor-types/lib/interface/EditorCore.ts b/packages/roosterjs-editor-types/lib/interface/EditorCore.ts index 9e4c52f0de3..46a6f42ba2c 100644 --- a/packages/roosterjs-editor-types/lib/interface/EditorCore.ts +++ b/packages/roosterjs-editor-types/lib/interface/EditorCore.ts @@ -275,12 +275,13 @@ export type SelectTable = ( /** * Select a table and save data of the selected range * @param core The EditorCore object - * @param table table to select - * @param coordinates first and last cell of the selection, if this parameter is null, instead of - * selecting, will unselect the table. + * @param image image to select * @returns true if successful */ -export type SelectImage = (image: HTMLImageElement | null) => ImageSelectionRange | null; +export type SelectImage = ( + core: EditorCore, + image: HTMLImageElement | null +) => ImageSelectionRange | null; /** * The interface for the map of core API. From 3ad948d5c5c9df646e8fdccf54e21d5bd4a616f2 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Tue, 20 Sep 2022 13:59:15 -0700 Subject: [PATCH 27/41] Content Model: improve demo site, and minor bug fixes (#1271) * Content Model: improve demo * add more fix * remove test case fix --- .../components/format/FormatView.tsx | 10 +++++++--- .../components/format/utils/FormatRenderer.ts | 2 +- .../utils/createCheckboxFormatRenderer.tsx | 7 +++++-- .../format/utils/createColorFormatRender.tsx | 18 ++++++++++++++---- .../utils/createDropDownFormatRenderer.tsx | 10 +++++++--- .../format/utils/createTextFormatRenderer.tsx | 10 +++++++--- .../modelToDom/handlers/handleBlockGroup.ts | 5 ++++- .../processors/containerProcessorTest.ts | 1 - .../lib/metadata/validate.ts | 5 +++++ .../lib/enum/BulletListType.ts | 7 ++++++- 10 files changed, 56 insertions(+), 19 deletions(-) diff --git a/demo/scripts/controls/contentModel/components/format/FormatView.tsx b/demo/scripts/controls/contentModel/components/format/FormatView.tsx index 929f4d84a70..5affa4798de 100644 --- a/demo/scripts/controls/contentModel/components/format/FormatView.tsx +++ b/demo/scripts/controls/contentModel/components/format/FormatView.tsx @@ -3,8 +3,12 @@ import { FormatRenderer } from './utils/FormatRenderer'; const styles = require('./FormatView.scss'); -export function FormatView(props: { format: T; renderers: FormatRenderer[] }) { - const { format, renderers } = props; +export function FormatView(props: { + format: T; + renderers: FormatRenderer[]; + onUpdate?: () => void; +}) { + const { format, renderers, onUpdate } = props; - return
{renderers.map(x => x(format))}
; + return
{renderers.map(x => x(format, onUpdate))}
; } diff --git a/demo/scripts/controls/contentModel/components/format/utils/FormatRenderer.ts b/demo/scripts/controls/contentModel/components/format/utils/FormatRenderer.ts index ef535c02b08..13283336e74 100644 --- a/demo/scripts/controls/contentModel/components/format/utils/FormatRenderer.ts +++ b/demo/scripts/controls/contentModel/components/format/utils/FormatRenderer.ts @@ -1 +1 @@ -export type FormatRenderer = (format: T) => JSX.Element; +export type FormatRenderer = (format: T, onUpdate?: () => void) => JSX.Element; diff --git a/demo/scripts/controls/contentModel/components/format/utils/createCheckboxFormatRenderer.tsx b/demo/scripts/controls/contentModel/components/format/utils/createCheckboxFormatRenderer.tsx index 77cefb42a84..148c7327fa0 100644 --- a/demo/scripts/controls/contentModel/components/format/utils/createCheckboxFormatRenderer.tsx +++ b/demo/scripts/controls/contentModel/components/format/utils/createCheckboxFormatRenderer.tsx @@ -9,8 +9,9 @@ function CheckboxFormatItem(props: { format: TFormat; getter: (format: TFormat) => boolean; setter?: (format: TFormat, newValue: boolean) => void; + onUpdate?: () => void; }) { - const { name, getter, setter, format } = props; + const { name, getter, setter, format, onUpdate } = props; const checkbox = React.useRef(null); const [value, setValue] = useProperty(getter(format)); @@ -18,6 +19,7 @@ function CheckboxFormatItem(props: { const newValue = checkbox.current.checked; setValue(newValue); setter?.(format, newValue); + onUpdate?.(); }, [format, setter, setValue]); return ( @@ -35,12 +37,13 @@ export function createCheckboxFormatRenderer( getter: (format: T) => boolean, setter?: (format: T, newValue: boolean) => void ): FormatRenderer { - return (format: T) => ( + return (format: T, onUpdate?: () => void) => ( ); diff --git a/demo/scripts/controls/contentModel/components/format/utils/createColorFormatRender.tsx b/demo/scripts/controls/contentModel/components/format/utils/createColorFormatRender.tsx index 462dcd3fdda..afdd8981d98 100644 --- a/demo/scripts/controls/contentModel/components/format/utils/createColorFormatRender.tsx +++ b/demo/scripts/controls/contentModel/components/format/utils/createColorFormatRender.tsx @@ -11,8 +11,9 @@ function ColorFormatItem(props: { format: T; getter: (format: T) => string; setter?: (format: T, newValue: string) => void; + onUpdate?: () => void; }) { - const { name, getter, setter, format } = props; + const { name, getter, setter, format, onUpdate } = props; const colorPickerBox = React.useRef(null); const colorValueBox = React.useRef(null); const transparentCheckBox = React.useRef(null); @@ -39,6 +40,7 @@ function ColorFormatItem(props: { setValue(newValue); setter?.(format, newValue); + onUpdate?.(); }, [setter, format] ); @@ -90,8 +92,15 @@ export function createColorFormatRenderer( getter: (format: T) => string, setter?: (format: T, newValue: string) => void ): FormatRenderer { - return (format: T) => ( - + return (format: T, onUpdate?: () => void) => ( + ); } @@ -100,7 +109,7 @@ export function createColorFormatRendererGroup( getter: (format: T) => string[], setter?: (format: T, name: V, newValue: string) => void ): FormatRenderer { - return (format: T) => { + return (format: T, onUpdate?: () => void) => { const initValues = getter(format); return ( @@ -111,6 +120,7 @@ export function createColorFormatRendererGroup( getter={() => initValues[index]} setter={(format, newValue) => setter?.(format, name, newValue)} format={format} + onUpdate={onUpdate} key={name} /> ))} diff --git a/demo/scripts/controls/contentModel/components/format/utils/createDropDownFormatRenderer.tsx b/demo/scripts/controls/contentModel/components/format/utils/createDropDownFormatRenderer.tsx index 7cf07350b2d..cb317e6475d 100644 --- a/demo/scripts/controls/contentModel/components/format/utils/createDropDownFormatRenderer.tsx +++ b/demo/scripts/controls/contentModel/components/format/utils/createDropDownFormatRenderer.tsx @@ -10,8 +10,9 @@ function DropDownFormatItem(props: { options: TOption[]; getter: (format: TFormat) => TOption | undefined; setter?: (format: TFormat, newValue: TOption | undefined) => void; + onUpdate?: () => void; }) { - const { name, getter, setter, format, options } = props; + const { name, getter, setter, format, options, onUpdate } = props; const dropDown = React.useRef(null); const [value, setValue] = useProperty(getter(format)); @@ -20,6 +21,7 @@ function DropDownFormatItem(props: { dropDown.current.value == '' ? undefined : (dropDown.current.value as TOption); setValue(newValue); setter?.(format, newValue); + onUpdate?.(); }, [format, setter]); return ( @@ -45,13 +47,14 @@ export function createDropDownFormatRenderer( getter: (format: T) => O, setter?: (format: T, newValue: O) => void ): FormatRenderer { - return (format: T) => ( + return (format: T, onUpdate?: () => void) => ( ); @@ -63,7 +66,7 @@ export function createDropDownFormatRendererGroup O[], setter?: (format: T, name: V, newValue: O) => void ): FormatRenderer { - return (format: T) => { + return (format: T, onUpdate?: () => void) => { const initValues = getter(format); return ( <> @@ -74,6 +77,7 @@ export function createDropDownFormatRendererGroup setter?.(format, name, newValue)} format={format} options={options} + onUpdate={onUpdate} key={name} /> ))} diff --git a/demo/scripts/controls/contentModel/components/format/utils/createTextFormatRenderer.tsx b/demo/scripts/controls/contentModel/components/format/utils/createTextFormatRenderer.tsx index e629e20eb77..2d61fe4a096 100644 --- a/demo/scripts/controls/contentModel/components/format/utils/createTextFormatRenderer.tsx +++ b/demo/scripts/controls/contentModel/components/format/utils/createTextFormatRenderer.tsx @@ -9,9 +9,10 @@ function TextFormatItem(props: { format: T; getter: (format: T) => string; setter?: (format: T, newValue: string) => void; + onUpdate?: () => void; type: 'text' | 'number' | 'multiline'; }) { - const { name, getter, setter, format, type } = props; + const { name, getter, setter, format, type, onUpdate } = props; const textBox = React.useRef(null); const [value, setValue] = useProperty(getter(format)); @@ -19,6 +20,7 @@ function TextFormatItem(props: { (newValue: string) => { setValue(newValue); setter?.(format, newValue); + onUpdate(); }, [setter, format] ); @@ -78,13 +80,14 @@ export function createTextFormatRenderer( setter?: (format: T, newValue: string) => void, type: 'text' | 'number' | 'multiline' = 'text' ): FormatRenderer { - return (format: T) => ( + return (format: T, onUpdate?: () => void) => ( ); @@ -96,7 +99,7 @@ export function createTextFormatRendererGroup( setter?: (format: T, name: V, newValue: string) => void, type: 'text' | 'number' | 'multiline' = 'text' ): FormatRenderer { - return (format: T) => { + return (format: T, onUpdate?: () => void) => { const initValues = getter(format); return ( @@ -108,6 +111,7 @@ export function createTextFormatRendererGroup( setter={(format, newValue) => setter?.(format, name, newValue)} format={format} type={type} + onUpdate={onUpdate} key={name} /> ))} diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleBlockGroup.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleBlockGroup.ts index f2665137a9c..ede444c1be5 100644 --- a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleBlockGroup.ts +++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleBlockGroup.ts @@ -25,7 +25,10 @@ export function handleBlockGroup( handleBlockGroupChildren(doc, newParent, group, context); if (isGeneralSegment(group) && isNodeOfType(newParent, NodeType.Element)) { - context.regularSelection.current.segment = newParent; + if (!group.element.firstChild) { + context.regularSelection.current.segment = newParent; + } + applyFormat(newParent, SegmentFormatHandlers, group.format, context); } diff --git a/packages/roosterjs-content-model/test/domToModel/processors/containerProcessorTest.ts b/packages/roosterjs-content-model/test/domToModel/processors/containerProcessorTest.ts index 568ba636b24..02a4ee5af57 100644 --- a/packages/roosterjs-content-model/test/domToModel/processors/containerProcessorTest.ts +++ b/packages/roosterjs-content-model/test/domToModel/processors/containerProcessorTest.ts @@ -228,7 +228,6 @@ describe('containerProcessor', () => { }); }); - // Skip this test for now, we will reenable it once we are ready to write e2e test case of creating model from dom it('Process a DIV with mixed selection', () => { const div = document.createElement('div'); div.innerHTML = 'test1test2test3'; diff --git a/packages/roosterjs-editor-dom/lib/metadata/validate.ts b/packages/roosterjs-editor-dom/lib/metadata/validate.ts index 643a48be2bb..cabec16e130 100644 --- a/packages/roosterjs-editor-dom/lib/metadata/validate.ts +++ b/packages/roosterjs-editor-dom/lib/metadata/validate.ts @@ -11,6 +11,11 @@ export default function validate(input: any, def: Definition): input is T let result = false; if ((def.isOptional && typeof input === 'undefined') || (def.allowNull && input === null)) { result = true; + } else if ( + (!def.isOptional && typeof input === 'undefined') || + (!def.allowNull && input === null) + ) { + return false; } else { switch (def.type) { case DefinitionType.String: diff --git a/packages/roosterjs-editor-types/lib/enum/BulletListType.ts b/packages/roosterjs-editor-types/lib/enum/BulletListType.ts index f8322cb6616..c9078df0ddc 100644 --- a/packages/roosterjs-editor-types/lib/enum/BulletListType.ts +++ b/packages/roosterjs-editor-types/lib/enum/BulletListType.ts @@ -47,8 +47,13 @@ export const enum BulletListType { */ DoubleLongArrow = 8, + /** + * Bullet type circle + */ + Circle = 9, + /** * Maximum value of the enum */ - Max = 8, + Max = 9, } From 33848765d47869875d6fb77d14ac352e710b34b6 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Tue, 20 Sep 2022 14:51:59 -0700 Subject: [PATCH 28/41] Skip unstable test case (#1280) --- packages/roosterjs-editor-core/test/coreApi/selectTableTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/roosterjs-editor-core/test/coreApi/selectTableTest.ts b/packages/roosterjs-editor-core/test/coreApi/selectTableTest.ts index ae15d499bac..ba7283f67df 100644 --- a/packages/roosterjs-editor-core/test/coreApi/selectTableTest.ts +++ b/packages/roosterjs-editor-core/test/coreApi/selectTableTest.ts @@ -3,7 +3,7 @@ import { Browser } from 'roosterjs-editor-dom'; import { EditorCore, TableSelection } from 'roosterjs-editor-types'; import { selectTable } from '../../lib/coreApi/selectTable'; -describe('selectTable |', () => { +xdescribe('selectTable |', () => { let div: HTMLDivElement | null; let table: HTMLTableElement | null; let core: EditorCore | null; From 9de6dda8f7c4761e22bc2af055a8243008a12fc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Wed, 21 Sep 2022 15:01:57 -0300 Subject: [PATCH 29/41] select image data structure --- .../sidePane/apiPlayground/apiEntries.ts | 5 --- .../editorOptions/ExperimentalFeatures.tsx | 1 + .../lib/coreApi/selectImage.ts | 5 ++- .../lib/coreApi/switchShadowEdit.ts | 2 +- .../lib/editor/Editor.ts | 36 +++++-------------- .../test/coreApi/selectImageTest.ts | 8 ++--- .../lib/plugins/ImageEdit/ImageEdit.ts | 14 -------- .../lib/enum/ExperimentalFeatures.ts | 5 +++ .../lib/interface/EditorCore.ts | 6 +--- 9 files changed, 21 insertions(+), 61 deletions(-) diff --git a/demo/scripts/controls/sidePane/apiPlayground/apiEntries.ts b/demo/scripts/controls/sidePane/apiPlayground/apiEntries.ts index 5197cf87145..5c2b324f621 100644 --- a/demo/scripts/controls/sidePane/apiPlayground/apiEntries.ts +++ b/demo/scripts/controls/sidePane/apiPlayground/apiEntries.ts @@ -3,7 +3,6 @@ import ApiPaneProps, { ApiPlaygroundComponent } from './ApiPaneProps'; import BlockElementsPane from './blockElements/BlockElementsPane'; import GetDarkColorPane from './darkColor/GetDarkColorPane'; import GetSelectedRegionsPane from './region/GetSelectedRegionsPane'; -import GetSelectionPane from './getSelection/getSelectionPane'; import InsertContentPane from './insertContent/InsertContentPane'; import InsertEntityPane from './insertEntity/InsertEntityPane'; import MatchLinkPane from './matchLink/MatchLinkPane'; @@ -60,10 +59,6 @@ const apiEntries: { [key: string]: ApiEntry } = { name: 'getDarkColor', component: GetDarkColorPane, }, - getSelection: { - name: 'getSelection', - component: GetSelectionPane, - }, more: { name: 'Coming soon...', }, diff --git a/demo/scripts/controls/sidePane/editorOptions/ExperimentalFeatures.tsx b/demo/scripts/controls/sidePane/editorOptions/ExperimentalFeatures.tsx index 478f6e6523f..114985ff3e5 100644 --- a/demo/scripts/controls/sidePane/editorOptions/ExperimentalFeatures.tsx +++ b/demo/scripts/controls/sidePane/editorOptions/ExperimentalFeatures.tsx @@ -24,6 +24,7 @@ const FeatureNames: Partial> = { 'Use pending style format to do formatting when selection is collapsed', [ExperimentalFeatures.NormalizeList]: 'Normalize list to make sure it can be displayed correctly in other client', + [ExperimentalFeatures.ImageSelection]: 'The selected image data will be stored by editor core', }; export default class ExperimentalFeaturesPane extends React.Component< diff --git a/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts b/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts index 2b47db5f8de..2d03316fd8c 100644 --- a/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts +++ b/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts @@ -1,5 +1,5 @@ import { createRange } from 'roosterjs-editor-dom'; -import { EditorCore, SelectImage, SelectionRangeTypes } from 'roosterjs-editor-types'; +import { SelectImage, SelectionRangeTypes } from 'roosterjs-editor-types'; /** * @internal @@ -8,10 +8,9 @@ import { EditorCore, SelectImage, SelectionRangeTypes } from 'roosterjs-editor-t * @param image Image to select * @returns Selected image information */ -export const selectImage: SelectImage = (core: EditorCore, image: HTMLImageElement | null) => { +export const selectImage: SelectImage = (image: HTMLImageElement | null) => { if (image) { const range = createRange(image); - core.api.selectRange(core, range); return { type: SelectionRangeTypes.ImageSelection, diff --git a/packages/roosterjs-editor-core/lib/coreApi/switchShadowEdit.ts b/packages/roosterjs-editor-core/lib/coreApi/switchShadowEdit.ts index b5fbcc38d1e..3ca4a860ee8 100644 --- a/packages/roosterjs-editor-core/lib/coreApi/switchShadowEdit.ts +++ b/packages/roosterjs-editor-core/lib/coreApi/switchShadowEdit.ts @@ -105,7 +105,7 @@ export const switchShadowEdit: SwitchShadowEdit = (core: EditorCore, isOn: boole const { image } = core.domEvent.imageSelectionRange; const imageElement = core.contentDiv.querySelector('#' + image.id); if (imageElement) { - core.domEvent.imageSelectionRange = core.api.selectImage(core, image); + core.domEvent.imageSelectionRange = core.api.selectImage(image); } } diff --git a/packages/roosterjs-editor-core/lib/editor/Editor.ts b/packages/roosterjs-editor-core/lib/editor/Editor.ts index 7ee61f04354..cd153cebde4 100644 --- a/packages/roosterjs-editor-core/lib/editor/Editor.ts +++ b/packages/roosterjs-editor-core/lib/editor/Editor.ts @@ -475,11 +475,16 @@ export default class Editor implements IEditor { core.domEvent.tableSelectionRange = null; } - const imageSelection = this.getImageSelection(core, arg1); - if (imageSelection) { - core.domEvent.imageSelectionRange = imageSelection; - return !!imageSelection; + if ( + this.isFeatureEnabled(ExperimentalFeatures.ImageSelection) && + safeInstanceOf(arg1, 'HTMLImageElement') && + !arg2 + ) { + const selection = core.api.selectImage(arg1); + core.domEvent.imageSelectionRange = selection; + return !!selection; } else { + core.api.selectImage(null); core.domEvent.imageSelectionRange = null; } @@ -498,29 +503,6 @@ export default class Editor implements IEditor { return !!range && this.contains(range) && core.api.selectRange(core, range); } - private getImageSelection( - core: EditorCore, - arg: Range | NodePosition | Node | SelectionPath | HTMLTableElement | null, - arg2?: NodePosition | number | PositionType | TableSelection - ) { - if (safeInstanceOf(arg, 'HTMLImageElement') && !arg2) { - const selection = core.api.selectImage(core, arg); - return selection; - } - - if (arg && safeInstanceOf(arg, 'HTMLSpanElement') && !arg2) { - const argElement = arg; - const argClass = argElement.className; - if (argClass.indexOf('IMAGE_EDIT_WRAPPER') >= 0) { - const image = argElement.getElementsByTagName('img')[0]; - const selection = core.api.selectImage(core, image); - return selection; - } - } else { - core.api.selectImage(core, null); - } - } - /** * Get current focused position. Return null if editor doesn't have focus at this time. */ diff --git a/packages/roosterjs-editor-core/test/coreApi/selectImageTest.ts b/packages/roosterjs-editor-core/test/coreApi/selectImageTest.ts index ccfc4e44d1d..eb57af8e1f9 100644 --- a/packages/roosterjs-editor-core/test/coreApi/selectImageTest.ts +++ b/packages/roosterjs-editor-core/test/coreApi/selectImageTest.ts @@ -1,11 +1,9 @@ -import createEditorCore from './createMockEditorCore'; -import { EditorCore, SelectionRangeTypes } from 'roosterjs-editor-types'; import { selectImage } from '../../lib/coreApi/selectImage'; +import { SelectionRangeTypes } from 'roosterjs-editor-types'; describe('selectImage |', () => { let div: HTMLDivElement; let image: HTMLImageElement | null; - let core: EditorCore | null; beforeEach(() => { document.body.innerHTML = ''; @@ -14,7 +12,6 @@ describe('selectImage |', () => { image = div.querySelector('img'); document.body.appendChild(div); - core = createEditorCore(div, {}); }); afterEach(() => { @@ -24,11 +21,10 @@ describe('selectImage |', () => { document.head.removeChild(style); } div.parentElement?.removeChild(div); - core = null; }); it('selectImage', () => { - const selectedInfo = selectImage(core, image); + const selectedInfo = selectImage(image); const range = new Range(); range.selectNode(image!); diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts index 8a94e24ccef..ade8292ad90 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts @@ -102,11 +102,6 @@ const DARK_MODE_BGCOLOR = '#333'; */ const MAX_SMALL_SIZE_IMAGE = 10000; -/** - * Id to the wrapper and image selected - */ -const IMAGE_SELECTED = 'imageSelected'; - /** * ImageEdit plugin provides the ability to edit an inline image in editor, including image resizing, rotation and cropping */ @@ -137,8 +132,6 @@ export default class ImageEdit implements EditorPlugin { */ private wasResized: boolean; - private idNumber = 0; - /** * Create a new instance of ImageEdit * @param options Image editing options @@ -183,7 +176,6 @@ export default class ImageEdit implements EditorPlugin { this.disposer(); this.disposer = null; this.editor = null; - this.idNumber = 0; } /** @@ -372,12 +364,6 @@ export default class ImageEdit implements EditorPlugin { true /*isReadonly*/ ); - if (!this.image.id) { - this.idNumber = this.idNumber + 1; - const imageId = IMAGE_SELECTED + this.idNumber; - this.image.id = imageId; - } - wrapper.style.position = 'relative'; wrapper.style.maxWidth = '100%'; // keep the same vertical align diff --git a/packages/roosterjs-editor-types/lib/enum/ExperimentalFeatures.ts b/packages/roosterjs-editor-types/lib/enum/ExperimentalFeatures.ts index 40660363aee..a91c3bd2e14 100644 --- a/packages/roosterjs-editor-types/lib/enum/ExperimentalFeatures.ts +++ b/packages/roosterjs-editor-types/lib/enum/ExperimentalFeatures.ts @@ -101,4 +101,9 @@ export const enum ExperimentalFeatures { * e.g. We will move list items with "display: block" into previous list item and change tag to be DIV */ NormalizeList = 'NormalizeList', + + /** + * When a html image is selected, the selected image data will be stored by editor core. + */ + ImageSelection = 'ImageSelection', } diff --git a/packages/roosterjs-editor-types/lib/interface/EditorCore.ts b/packages/roosterjs-editor-types/lib/interface/EditorCore.ts index 46a6f42ba2c..b2ea97f8af4 100644 --- a/packages/roosterjs-editor-types/lib/interface/EditorCore.ts +++ b/packages/roosterjs-editor-types/lib/interface/EditorCore.ts @@ -274,14 +274,10 @@ export type SelectTable = ( /** * Select a table and save data of the selected range - * @param core The EditorCore object * @param image image to select * @returns true if successful */ -export type SelectImage = ( - core: EditorCore, - image: HTMLImageElement | null -) => ImageSelectionRange | null; +export type SelectImage = (image: HTMLImageElement | null) => ImageSelectionRange | null; /** * The interface for the map of core API. From 3ff449dca2e5bf1cb3f5bd2555579a51a1730286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Wed, 21 Sep 2022 15:06:24 -0300 Subject: [PATCH 30/41] remove comment --- packages/roosterjs-editor-core/lib/coreApi/selectImage.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts b/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts index 2d03316fd8c..8eb6175cd0c 100644 --- a/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts +++ b/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts @@ -4,7 +4,6 @@ import { SelectImage, SelectionRangeTypes } from 'roosterjs-editor-types'; /** * @internal * Select a image and save data of the selected range - * @param core The EditorCore object * @param image Image to select * @returns Selected image information */ From 802cae4aa9b2db4b95a67191df2cf3280647e836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Wed, 21 Sep 2022 18:47:13 -0300 Subject: [PATCH 31/41] Image Selection plugin --- demo/scripts/controls/BuildInPluginState.ts | 1 + demo/scripts/controls/getToggleablePlugins.ts | 2 + .../editorOptions/EditorOptionsPlugin.ts | 1 + .../lib/coreApi/ensureUniqueId.ts | 27 +++++++ .../lib/coreApi/selectImage.ts | 48 +++++++++++- .../lib/coreApi/selectTable.ts | 24 +----- .../lib/coreApi/switchShadowEdit.ts | 2 +- .../lib/editor/Editor.ts | 6 +- .../test/coreApi/selectImageTest.ts | 8 +- .../lib/ImageSelection.ts | 1 + .../roosterjs-editor-plugins/lib/index.ts | 1 + .../plugins/ImageSelection/ImageSelection.ts | 73 +++++++++++++++++++ .../lib/plugins/ImageSelection/index.ts | 1 + .../lib/plugins/Picker/PickerPlugin.ts | 1 + .../lib/interface/EditorCore.ts | 11 ++- .../lib/interface/EditorOptions.ts | 5 ++ 16 files changed, 181 insertions(+), 31 deletions(-) create mode 100644 packages/roosterjs-editor-core/lib/coreApi/ensureUniqueId.ts create mode 100644 packages/roosterjs-editor-plugins/lib/ImageSelection.ts create mode 100644 packages/roosterjs-editor-plugins/lib/plugins/ImageSelection/ImageSelection.ts create mode 100644 packages/roosterjs-editor-plugins/lib/plugins/ImageSelection/index.ts diff --git a/demo/scripts/controls/BuildInPluginState.ts b/demo/scripts/controls/BuildInPluginState.ts index ebecc5e1d9c..c0bc3636b33 100644 --- a/demo/scripts/controls/BuildInPluginState.ts +++ b/demo/scripts/controls/BuildInPluginState.ts @@ -22,6 +22,7 @@ export interface BuildInPluginList { tableEditMenu: boolean; contextMenu: boolean; autoFormat: boolean; + imageSelection: boolean; } export default interface BuildInPluginState { diff --git a/demo/scripts/controls/getToggleablePlugins.ts b/demo/scripts/controls/getToggleablePlugins.ts index 1161ed69c80..394ab4453fb 100644 --- a/demo/scripts/controls/getToggleablePlugins.ts +++ b/demo/scripts/controls/getToggleablePlugins.ts @@ -6,6 +6,7 @@ import { CutPasteListChain } from 'roosterjs-editor-plugins/lib/CutPasteListChai import { EditorPlugin } from 'roosterjs-editor-types'; import { HyperLink } from 'roosterjs-editor-plugins/lib/HyperLink'; import { ImageEdit } from 'roosterjs-editor-plugins/lib/ImageEdit'; +import { ImageSelection } from 'roosterjs-editor-plugins'; import { Paste } from 'roosterjs-editor-plugins/lib/Paste'; import { TableCellSelection } from 'roosterjs-editor-plugins/lib/TableCellSelection'; import { TableResize } from 'roosterjs-editor-plugins/lib/TableResize'; @@ -55,6 +56,7 @@ export default function getToggleablePlugins(initState: BuildInPluginState) { ? createTableEditMenuProvider() : null, contextMenu: pluginList.contextMenu ? createContextMenuPlugin() : null, + imageSelection: pluginList.imageSelection ? new ImageSelection() : null, }; return Object.values(plugins); diff --git a/demo/scripts/controls/sidePane/editorOptions/EditorOptionsPlugin.ts b/demo/scripts/controls/sidePane/editorOptions/EditorOptionsPlugin.ts index 0cc9c85f719..7c216484f99 100644 --- a/demo/scripts/controls/sidePane/editorOptions/EditorOptionsPlugin.ts +++ b/demo/scripts/controls/sidePane/editorOptions/EditorOptionsPlugin.ts @@ -21,6 +21,7 @@ const initialState: BuildInPluginState = { tableEditMenu: true, contextMenu: true, autoFormat: true, + imageSelection: true, }, contentEditFeatures: getDefaultContentEditFeatureSettings(), defaultFormat: {}, diff --git a/packages/roosterjs-editor-core/lib/coreApi/ensureUniqueId.ts b/packages/roosterjs-editor-core/lib/coreApi/ensureUniqueId.ts new file mode 100644 index 00000000000..3a6cb002668 --- /dev/null +++ b/packages/roosterjs-editor-core/lib/coreApi/ensureUniqueId.ts @@ -0,0 +1,27 @@ +/** + * Add an unique id to element and ensure that is unique + * @param el The HTMLElement that will receive the id + * @param idPrefix The prefix that will antecede the id (Ex: tableSelected01) + */ +export function ensureUniqueId(el: HTMLElement, idPrefix: string) { + const doc = el.ownerDocument; + + if (!el.id) { + let cont = 0; + const getElement = () => doc.getElementById(idPrefix + cont); + //Ensure that there are no elements with the same ID + let element = getElement(); + while (element) { + cont++; + element = getElement(); + } + + el.id = idPrefix + cont; + } else { + const elements = doc.querySelectorAll(`#${el.id}`); + if (elements.length > 1) { + el.removeAttribute('id'); + ensureUniqueId(el, idPrefix); + } + } +} diff --git a/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts b/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts index 8eb6175cd0c..52d68ce76af 100644 --- a/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts +++ b/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts @@ -1,16 +1,25 @@ import { createRange } from 'roosterjs-editor-dom'; -import { SelectImage, SelectionRangeTypes } from 'roosterjs-editor-types'; +import { EditorCore, SelectImage, SelectionRangeTypes } from 'roosterjs-editor-types'; +import { ensureUniqueId } from './ensureUniqueId'; +const IMAGE_ID = 'imageSelected'; +const CONTENT_DIV_ID = 'contentDiv_'; +const STYLE_ID = 'imageStyle'; /** * @internal * Select a image and save data of the selected range * @param image Image to select * @returns Selected image information */ -export const selectImage: SelectImage = (image: HTMLImageElement | null) => { +export const selectImage: SelectImage = (core: EditorCore, image: HTMLImageElement | null) => { + unselect(core); if (image) { const range = createRange(image); + ensureUniqueId(image, IMAGE_ID); + ensureUniqueId(core.contentDiv, CONTENT_DIV_ID); + + select(core, image); return { type: SelectionRangeTypes.ImageSelection, ranges: [range], @@ -21,3 +30,38 @@ export const selectImage: SelectImage = (image: HTMLImageElement | null) => { return null; }; + +const select = (core: EditorCore, image: HTMLImageElement) => { + const styleTagId = STYLE_ID + core.contentDiv.id; + const doc = core.contentDiv.ownerDocument; + let styleTag = doc.getElementById(styleTagId) as HTMLStyleElement; + if (!styleTag) { + styleTag = doc.createElement('style'); + styleTag.id = styleTagId; + doc.head.appendChild(styleTag); + } + + const borderCSS = buildBorderCSS(core, image.id); + styleTag.sheet?.insertRule(borderCSS); +}; + +const buildBorderCSS = (core: EditorCore, imageId: string): string => { + const borderColor = core.imageSelectionBorderColor || '#DB626C'; + return ( + '#' + + core.contentDiv.id + + ' #' + + imageId + + ' { margin: -2px; border: 2px solid' + + borderColor + + '}' + ); +}; + +const unselect = (core: EditorCore) => { + const doc = core.contentDiv.ownerDocument; + let styleTag = doc.getElementById(STYLE_ID + core.contentDiv.id) as HTMLStyleElement; + if (styleTag) { + doc.head.removeChild(styleTag); + } +}; diff --git a/packages/roosterjs-editor-core/lib/coreApi/selectTable.ts b/packages/roosterjs-editor-core/lib/coreApi/selectTable.ts index 4f1be7ecbe4..f32ff140b43 100644 --- a/packages/roosterjs-editor-core/lib/coreApi/selectTable.ts +++ b/packages/roosterjs-editor-core/lib/coreApi/selectTable.ts @@ -1,3 +1,4 @@ +import { ensureUniqueId } from './ensureUniqueId'; import { createRange, getStyles, @@ -185,29 +186,6 @@ function unselect(core: EditorCore) { } } -function ensureUniqueId(el: HTMLElement, idPrefix: string) { - const doc = el.ownerDocument; - - if (!el.id) { - let cont = 0; - const getElement = () => doc.getElementById(idPrefix + cont); - //Ensure that there are no elements with the same ID - let element = getElement(); - while (element) { - cont++; - element = getElement(); - } - - el.id = idPrefix + cont; - } else { - const elements = doc.querySelectorAll(`#${el.id}`); - if (elements.length > 1) { - el.removeAttribute('id'); - ensureUniqueId(el, idPrefix); - } - } -} - function generateCssFromCell( contentDivSelector: string, tableId: string, diff --git a/packages/roosterjs-editor-core/lib/coreApi/switchShadowEdit.ts b/packages/roosterjs-editor-core/lib/coreApi/switchShadowEdit.ts index 3ca4a860ee8..b5fbcc38d1e 100644 --- a/packages/roosterjs-editor-core/lib/coreApi/switchShadowEdit.ts +++ b/packages/roosterjs-editor-core/lib/coreApi/switchShadowEdit.ts @@ -105,7 +105,7 @@ export const switchShadowEdit: SwitchShadowEdit = (core: EditorCore, isOn: boole const { image } = core.domEvent.imageSelectionRange; const imageElement = core.contentDiv.querySelector('#' + image.id); if (imageElement) { - core.domEvent.imageSelectionRange = core.api.selectImage(image); + core.domEvent.imageSelectionRange = core.api.selectImage(core, image); } } diff --git a/packages/roosterjs-editor-core/lib/editor/Editor.ts b/packages/roosterjs-editor-core/lib/editor/Editor.ts index cd153cebde4..c0d0e22cecf 100644 --- a/packages/roosterjs-editor-core/lib/editor/Editor.ts +++ b/packages/roosterjs-editor-core/lib/editor/Editor.ts @@ -128,6 +128,7 @@ export default class Editor implements IEditor { : [scrollContainer, contentDiv] ); }), + imageSelectionBorderColor: options.imageSelectionBorderColor, }; // 3. Initialize plugins @@ -469,6 +470,7 @@ export default class Editor implements IEditor { if (arg1 && 'rows' in arg1) { const selection = core.api.selectTable(core, arg1, arg2); core.domEvent.tableSelectionRange = selection; + return !!selection; } else { core.api.selectTable(core, null); @@ -480,11 +482,11 @@ export default class Editor implements IEditor { safeInstanceOf(arg1, 'HTMLImageElement') && !arg2 ) { - const selection = core.api.selectImage(arg1); + const selection = core.api.selectImage(core, arg1); core.domEvent.imageSelectionRange = selection; return !!selection; } else { - core.api.selectImage(null); + core.api.selectImage(core, null); core.domEvent.imageSelectionRange = null; } diff --git a/packages/roosterjs-editor-core/test/coreApi/selectImageTest.ts b/packages/roosterjs-editor-core/test/coreApi/selectImageTest.ts index eb57af8e1f9..672f82c3908 100644 --- a/packages/roosterjs-editor-core/test/coreApi/selectImageTest.ts +++ b/packages/roosterjs-editor-core/test/coreApi/selectImageTest.ts @@ -1,9 +1,11 @@ +import createEditorCore from './createMockEditorCore'; +import { EditorCore, SelectionRangeTypes } from 'roosterjs-editor-types'; import { selectImage } from '../../lib/coreApi/selectImage'; -import { SelectionRangeTypes } from 'roosterjs-editor-types'; describe('selectImage |', () => { let div: HTMLDivElement; let image: HTMLImageElement | null; + let core: EditorCore | null; beforeEach(() => { document.body.innerHTML = ''; @@ -12,6 +14,7 @@ describe('selectImage |', () => { image = div.querySelector('img'); document.body.appendChild(div); + core = createEditorCore(div!, {}); }); afterEach(() => { @@ -21,10 +24,11 @@ describe('selectImage |', () => { document.head.removeChild(style); } div.parentElement?.removeChild(div); + core = null; }); it('selectImage', () => { - const selectedInfo = selectImage(image); + const selectedInfo = selectImage(core, image); const range = new Range(); range.selectNode(image!); diff --git a/packages/roosterjs-editor-plugins/lib/ImageSelection.ts b/packages/roosterjs-editor-plugins/lib/ImageSelection.ts new file mode 100644 index 00000000000..05654882352 --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/ImageSelection.ts @@ -0,0 +1 @@ +export * from './plugins/ImageSelection/index'; diff --git a/packages/roosterjs-editor-plugins/lib/index.ts b/packages/roosterjs-editor-plugins/lib/index.ts index 5cd296430b2..60d34b82282 100644 --- a/packages/roosterjs-editor-plugins/lib/index.ts +++ b/packages/roosterjs-editor-plugins/lib/index.ts @@ -11,3 +11,4 @@ export * from './TableResize'; export * from './Watermark'; export * from './TableCellSelection'; export * from './AutoFormat'; +export * from './ImageSelection'; diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ImageSelection/ImageSelection.ts b/packages/roosterjs-editor-plugins/lib/plugins/ImageSelection/ImageSelection.ts new file mode 100644 index 00000000000..c486ea0ae63 --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/ImageSelection/ImageSelection.ts @@ -0,0 +1,73 @@ +import { safeInstanceOf } from 'roosterjs-editor-dom'; +import { + EditorPlugin, + IEditor, + PluginEvent, + PluginEventType, + SelectionRangeTypes, +} from 'roosterjs-editor-types'; + +/** + * Detect image selection and help highlight the image + */ +export default class ImageSelection implements EditorPlugin { + private editor: IEditor | null = null; + private image: HTMLImageElement | null = null; + + constructor() {} + + /** + * Get a friendly name of this plugin + */ + getName() { + return 'ImageSelection'; + } + + /** + * Initialize this plugin. This should only be called from Editor + * @param editor Editor instance + */ + initialize(editor: IEditor) { + this.editor = editor; + } + + /** + * Dispose this plugin + */ + dispose() { + this.editor.select(null); + this.editor = null; + this.image = null; + } + + onPluginEvent(event: PluginEvent) { + if (this.editor) { + switch (event.eventType) { + case PluginEventType.EnteredShadowEdit: + const selection = this.editor.getSelectionRangeEx(); + if (selection.type == SelectionRangeTypes.ImageSelection) { + this.editor.select(selection.image); + } + break; + case PluginEventType.LeavingShadowEdit: + if (this.image) { + const image = this.editor.queryElements('#' + this.image.id); + if (image.length == 1) { + this.image = image[0] as HTMLImageElement; + this.editor.select(this.image); + } + } + break; + case PluginEventType.MouseDown: + const target = event.rawEvent.target; + if (safeInstanceOf(target, 'HTMLImageElement')) { + this.image = target; + this.editor.select(this.image); + } else { + this.image = null; + } + break; + } + } + } +} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ImageSelection/index.ts b/packages/roosterjs-editor-plugins/lib/plugins/ImageSelection/index.ts new file mode 100644 index 00000000000..34de89549a4 --- /dev/null +++ b/packages/roosterjs-editor-plugins/lib/plugins/ImageSelection/index.ts @@ -0,0 +1 @@ +export { default as ImageSelection } from './ImageSelection'; diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts b/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts index 38426029752..40617495462 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts @@ -174,6 +174,7 @@ export default class PickerPlugin Rect | null; + + /** + * Color of the border of a selectedImage. Default color: '#DB626C' + */ + imageSelectionBorderColor?: string; } /** @@ -274,10 +279,14 @@ export type SelectTable = ( /** * Select a table and save data of the selected range + * @param core The EditorCore object * @param image image to select * @returns true if successful */ -export type SelectImage = (image: HTMLImageElement | null) => ImageSelectionRange | null; +export type SelectImage = ( + core: EditorCore, + image: HTMLImageElement | null +) => ImageSelectionRange | null; /** * The interface for the map of core API. diff --git a/packages/roosterjs-editor-types/lib/interface/EditorOptions.ts b/packages/roosterjs-editor-types/lib/interface/EditorOptions.ts index ff9bec70d6c..987af512318 100644 --- a/packages/roosterjs-editor-types/lib/interface/EditorOptions.ts +++ b/packages/roosterjs-editor-types/lib/interface/EditorOptions.ts @@ -133,4 +133,9 @@ export default interface EditorOptions { * Retrieves the visible viewport of the Editor. The default viewport is the Rect of the scrollContainer. */ getVisibleViewport?: () => Rect | null; + + /** + * Color of the border of a selectedImage. Default color: '#DB626C' + */ + imageSelectionBorderColor?: string; } From 74f88ff79a3639875ff58901b4fce5179ca90fe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Wed, 21 Sep 2022 18:54:17 -0300 Subject: [PATCH 32/41] remove spaces --- packages/roosterjs-editor-core/lib/editor/Editor.ts | 1 + .../roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/roosterjs-editor-core/lib/editor/Editor.ts b/packages/roosterjs-editor-core/lib/editor/Editor.ts index cd153cebde4..72f2ef4eb09 100644 --- a/packages/roosterjs-editor-core/lib/editor/Editor.ts +++ b/packages/roosterjs-editor-core/lib/editor/Editor.ts @@ -469,6 +469,7 @@ export default class Editor implements IEditor { if (arg1 && 'rows' in arg1) { const selection = core.api.selectTable(core, arg1, arg2); core.domEvent.tableSelectionRange = selection; + return !!selection; } else { core.api.selectTable(core, null); diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts b/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts index 38426029752..40617495462 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts @@ -174,6 +174,7 @@ export default class PickerPlugin Date: Wed, 21 Sep 2022 20:20:49 -0300 Subject: [PATCH 33/41] test --- .../test/coreApi/selectImageTest.ts | 19 +++- .../test/ImageSelection/imageSelectionTest.ts | 90 +++++++++++++++++++ 2 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 packages/roosterjs-editor-plugins/test/ImageSelection/imageSelectionTest.ts diff --git a/packages/roosterjs-editor-core/test/coreApi/selectImageTest.ts b/packages/roosterjs-editor-core/test/coreApi/selectImageTest.ts index 672f82c3908..4fe6b9853d9 100644 --- a/packages/roosterjs-editor-core/test/coreApi/selectImageTest.ts +++ b/packages/roosterjs-editor-core/test/coreApi/selectImageTest.ts @@ -11,7 +11,6 @@ describe('selectImage |', () => { document.body.innerHTML = ''; div = document.createElement('div'); div.innerHTML = ''; - image = div.querySelector('img'); document.body.appendChild(div); core = createEditorCore(div!, {}); @@ -19,7 +18,7 @@ describe('selectImage |', () => { afterEach(() => { document.body.removeChild(div); - let style = document.getElementById('imageSelected1'); + let style = document.getElementById('imageStylecontentDiv_0'); if (style) { document.head.removeChild(style); } @@ -39,4 +38,20 @@ describe('selectImage |', () => { areAllCollapsed: range.collapsed, }); }); + + it('image should have an unique id', () => { + selectImage(core, image); + expect(image!.id).toBe('imageSelected0'); + }); + + it('contentDiv should have an unique id', () => { + selectImage(core, image); + expect(core.contentDiv.id).toBe('contentDiv_0'); + }); + + it('styleTag should be created', () => { + selectImage(core, image); + const style = document.getElementById('imageStylecontentDiv_0'); + expect(style?.tagName).toBe('STYLE'); + }); }); diff --git a/packages/roosterjs-editor-plugins/test/ImageSelection/imageSelectionTest.ts b/packages/roosterjs-editor-plugins/test/ImageSelection/imageSelectionTest.ts new file mode 100644 index 00000000000..2bc8a946241 --- /dev/null +++ b/packages/roosterjs-editor-plugins/test/ImageSelection/imageSelectionTest.ts @@ -0,0 +1,90 @@ +import { Editor } from 'roosterjs-editor-core'; +import { EditorOptions, SelectionRangeTypes } from 'roosterjs-editor-types'; +import { IEditor } from 'roosterjs-editor-types'; +import { ImageSelection } from '../../lib/ImageSelection'; +export * from 'roosterjs-editor-dom/test/DomTestHelper'; + +describe('ImageSelectionPlugin |', () => { + let editor: IEditor; + let id = 'imageSelectionContainerId'; + let imageId = 'imageSelectionId'; + let imageSelection: ImageSelection; + let editorIsFeatureEnabled: any; + + beforeEach(() => { + let node = document.createElement('div'); + node.id = id; + document.body.insertBefore(node, document.body.childNodes[0]); + imageSelection = new ImageSelection(); + + let options: EditorOptions = { + plugins: [imageSelection], + defaultFormat: { + fontFamily: 'Calibri,Arial,Helvetica,sans-serif', + fontSize: '11pt', + textColor: '#000000', + }, + corePluginOverride: {}, + }; + + editor = new Editor(node as HTMLDivElement, options); + + editor.runAsync = callback => { + callback(editor); + return null; + }; + editorIsFeatureEnabled = spyOn(editor, 'isFeatureEnabled'); + }); + + it('should be triggered in mouse down', () => { + editor.setContent(``); + const target = document.getElementById(imageId); + editorIsFeatureEnabled.and.returnValue(true); + simulateMouseEvent(target!); + editor.focus(); + + const selection = editor.getSelectionRangeEx(); + expect(selection.type).toBe(SelectionRangeTypes.ImageSelection); + expect(selection.areAllCollapsed).toBe(false); + }); + + it('should be triggered in shadopw', () => { + editor.setContent(``); + const target = document.getElementById(imageId); + editorIsFeatureEnabled.and.returnValue(true); + editor.focus(); + editor.select(target); + + editor.startShadowEdit(); + + let selection = editor.getSelectionRangeEx(); + expect(selection.type).toBe(SelectionRangeTypes.ImageSelection); + expect(selection.areAllCollapsed).toBe(false); + + editor.stopShadowEdit(); + + selection = editor.getSelectionRangeEx(); + expect(selection.type).toBe(SelectionRangeTypes.ImageSelection); + expect(selection.areAllCollapsed).toBe(false); + }); + + afterEach(() => { + editor.dispose(); + editor = null; + const div = document.getElementById(id); + div.parentNode.removeChild(div); + }); + + function simulateMouseEvent(target: HTMLElement) { + const rect = target.getBoundingClientRect(); + var event = new MouseEvent('mousedown', { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left, + clientY: rect.top, + shiftKey: false, + }); + target.dispatchEvent(event); + } +}); From 6c1d98b67183348ebb5483bfe8db062ba0fab201 Mon Sep 17 00:00:00 2001 From: Trevor Shibley Date: Wed, 21 Sep 2022 20:22:01 -0700 Subject: [PATCH 34/41] Use elements from list stack even if they don't match (#1275) * re-use list stack * maybe fix some tests * flag it * better * optional * move call out of foreach Co-authored-by: Jiuqing Song --- .../apiPlayground/vlist/VListPane.tsx | 6 ++- .../editorOptions/ExperimentalFeatures.tsx | 2 + .../lib/format/setIndentation.ts | 6 ++- .../lib/format/setOrderedListNumbering.ts | 6 ++- .../lib/utils/commitListChains.ts | 7 ++- .../lib/utils/toggleListType.ts | 4 +- .../roosterjs-editor-dom/lib/list/VList.ts | 6 ++- .../lib/list/VListChain.ts | 4 +- .../lib/list/VListItem.ts | 48 +++++++++++++++---- .../ContentEdit/features/listFeatures.ts | 4 +- .../lib/enum/ExperimentalFeatures.ts | 8 ++++ 11 files changed, 78 insertions(+), 23 deletions(-) diff --git a/demo/scripts/controls/sidePane/apiPlayground/vlist/VListPane.tsx b/demo/scripts/controls/sidePane/apiPlayground/vlist/VListPane.tsx index e7513feab17..fdf1c83b3b9 100644 --- a/demo/scripts/controls/sidePane/apiPlayground/vlist/VListPane.tsx +++ b/demo/scripts/controls/sidePane/apiPlayground/vlist/VListPane.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import ApiPaneProps from '../ApiPaneProps'; import VListItem from 'roosterjs-editor-dom/lib/list/VListItem'; import { createVListFromRegion, VList } from 'roosterjs-editor-dom'; -import { IEditor, ListType, PositionType } from 'roosterjs-editor-types'; +import { ExperimentalFeatures, IEditor, ListType, PositionType } from 'roosterjs-editor-types'; interface VListPaneState { vlist: VList; @@ -100,7 +100,9 @@ export default class VListPane extends React.Component { const editor = this.props.getEditor(); editor.addUndoSnapshot(() => { - this.state.vlist?.writeBack(); + this.state.vlist?.writeBack( + editor.isFeatureEnabled(ExperimentalFeatures.ReuseAllAncestorListElements) + ); editor.focus(); editor.select(this.state.vlist.items[0]?.getNode(), PositionType.Begin); }); diff --git a/demo/scripts/controls/sidePane/editorOptions/ExperimentalFeatures.tsx b/demo/scripts/controls/sidePane/editorOptions/ExperimentalFeatures.tsx index 478f6e6523f..c2d74aeac49 100644 --- a/demo/scripts/controls/sidePane/editorOptions/ExperimentalFeatures.tsx +++ b/demo/scripts/controls/sidePane/editorOptions/ExperimentalFeatures.tsx @@ -24,6 +24,8 @@ const FeatureNames: Partial> = { 'Use pending style format to do formatting when selection is collapsed', [ExperimentalFeatures.NormalizeList]: 'Normalize list to make sure it can be displayed correctly in other client', + [ExperimentalFeatures.ReuseAllAncestorListElements]: + "Reuse ancestor list elements even if they don't match the types from the list item.", }; export default class ExperimentalFeaturesPane extends React.Component< diff --git a/packages/roosterjs-editor-api/lib/format/setIndentation.ts b/packages/roosterjs-editor-api/lib/format/setIndentation.ts index 4511d425ead..443e26792e0 100644 --- a/packages/roosterjs-editor-api/lib/format/setIndentation.ts +++ b/packages/roosterjs-editor-api/lib/format/setIndentation.ts @@ -80,7 +80,11 @@ export default function setIndentation( isTabKeyTextFeaturesEnabled /* preventItemRemoval */ ) : vList.setIndentation(start, end, indentation); - vList.writeBack(); + vList.writeBack( + editor.isFeatureEnabled( + ExperimentalFeatures.ReuseAllAncestorListElements + ) + ); blockGroups.push([]); } } else { diff --git a/packages/roosterjs-editor-api/lib/format/setOrderedListNumbering.ts b/packages/roosterjs-editor-api/lib/format/setOrderedListNumbering.ts index b755058aa07..5ee29c5333b 100644 --- a/packages/roosterjs-editor-api/lib/format/setOrderedListNumbering.ts +++ b/packages/roosterjs-editor-api/lib/format/setOrderedListNumbering.ts @@ -1,6 +1,6 @@ import formatUndoSnapshot from '../utils/formatUndoSnapshot'; import { createVListFromRegion } from 'roosterjs-editor-dom'; -import { IEditor } from 'roosterjs-editor-types'; +import { ExperimentalFeatures, IEditor } from 'roosterjs-editor-types'; /** * Resets Ordered List Numbering back to the value of the parameter startNumber @@ -26,7 +26,9 @@ export default function setOrderedListNumbering( ); if (vList) { vList.split(separator, startNumber); - vList.writeBack(); + vList.writeBack( + editor.isFeatureEnabled(ExperimentalFeatures.ReuseAllAncestorListElements) + ); } } }, diff --git a/packages/roosterjs-editor-api/lib/utils/commitListChains.ts b/packages/roosterjs-editor-api/lib/utils/commitListChains.ts index 8d1118f21da..5a9dee6034e 100644 --- a/packages/roosterjs-editor-api/lib/utils/commitListChains.ts +++ b/packages/roosterjs-editor-api/lib/utils/commitListChains.ts @@ -1,4 +1,4 @@ -import { IEditor } from 'roosterjs-editor-types'; +import { ExperimentalFeatures, IEditor } from 'roosterjs-editor-types'; import { Position, VListChain } from 'roosterjs-editor-dom'; /** @@ -11,7 +11,10 @@ export default function commitListChains(editor: IEditor, chains: VListChain[]) const range = editor.getSelectionRange(); const start = range && Position.getStart(range); const end = range && Position.getEnd(range); - chains.forEach(chain => chain.commit()); + const shouldReuseAllAncestorListElements = editor.isFeatureEnabled( + ExperimentalFeatures.ReuseAllAncestorListElements + ); + chains.forEach(chain => chain.commit(shouldReuseAllAncestorListElements)); editor.select(start, end); } } diff --git a/packages/roosterjs-editor-api/lib/utils/toggleListType.ts b/packages/roosterjs-editor-api/lib/utils/toggleListType.ts index 6ad53c06e08..b8ff563fe67 100644 --- a/packages/roosterjs-editor-api/lib/utils/toggleListType.ts +++ b/packages/roosterjs-editor-api/lib/utils/toggleListType.ts @@ -63,7 +63,9 @@ export default function toggleListType( if (editor.isFeatureEnabled(ExperimentalFeatures.AutoFormatList)) { vList.setListStyleType(orderedStyle, unorderedStyle); } - vList.writeBack(); + vList.writeBack( + editor.isFeatureEnabled(ExperimentalFeatures.ReuseAllAncestorListElements) + ); } }, undefined /* beforeRunCallback */, diff --git a/packages/roosterjs-editor-dom/lib/list/VList.ts b/packages/roosterjs-editor-dom/lib/list/VList.ts index 493df956d03..59e00a71911 100644 --- a/packages/roosterjs-editor-dom/lib/list/VList.ts +++ b/packages/roosterjs-editor-dom/lib/list/VList.ts @@ -177,8 +177,10 @@ export default class VList { /** * Write the result back into DOM tree * After that, this VList becomes unavailable because we set this.rootList to null + * + * @param shouldReuseAllAncestorListElements Optional - defaults to false. */ - writeBack() { + writeBack(shouldReuseAllAncestorListElements?: boolean) { if (!this.rootList) { throw new Error('rootList must not be null'); } @@ -200,7 +202,7 @@ export default class VList { start = newListStart; } - item.writeBack(listStack, this.rootList); + item.writeBack(listStack, this.rootList, shouldReuseAllAncestorListElements); const topList = listStack[1]; if (safeInstanceOf(topList, 'HTMLOListElement')) { diff --git a/packages/roosterjs-editor-dom/lib/list/VListChain.ts b/packages/roosterjs-editor-dom/lib/list/VListChain.ts index f945219025b..c9608c5a9f1 100644 --- a/packages/roosterjs-editor-dom/lib/list/VListChain.ts +++ b/packages/roosterjs-editor-dom/lib/list/VListChain.ts @@ -104,7 +104,7 @@ export default class VListChain { * After change the lists, commit the change to all lists in this chain to update the list number, * and clear the temporary dataset values added to list node */ - commit() { + commit(shouldReuseAllAncestorListElements?: boolean) { const lists = this.getLists(); let lastNumber = 0; @@ -119,7 +119,7 @@ export default class VListChain { delete list.dataset[CHAIN_DATASET_NAME]; delete list.dataset[AFTER_CURSOR_DATASET_NAME]; - vlist.writeBack(); + vlist.writeBack(shouldReuseAllAncestorListElements); } } diff --git a/packages/roosterjs-editor-dom/lib/list/VListItem.ts b/packages/roosterjs-editor-dom/lib/list/VListItem.ts index 3b506727765..c83475365b9 100644 --- a/packages/roosterjs-editor-dom/lib/list/VListItem.ts +++ b/packages/roosterjs-editor-dom/lib/list/VListItem.ts @@ -281,19 +281,47 @@ export default class VListItem { * Write the change result back into DOM * @param listStack current stack of list elements * @param originalRoot Original list root element. It will be reused when write back if possible + * @param shouldReuseAllAncestorListElements Optional - defaults to false. If true, only make + * sure the direct parent of this list matches the list types when writing back. */ - writeBack(listStack: Node[], originalRoot?: HTMLOListElement | HTMLUListElement) { + writeBack( + listStack: Node[], + originalRoot?: HTMLOListElement | HTMLUListElement, + shouldReuseAllAncestorListElements: boolean = false + ) { let nextLevel = 1; - // 1. Determine list elements that we can reuse - // e.g.: - // passed in listStack: Fragment > OL > UL > OL - // local listTypes: null > OL > UL > UL > OL - // then Fragment > OL > UL can be reused - for (; nextLevel < listStack.length; nextLevel++) { - if (getListTypeFromNode(listStack[nextLevel]) !== this.listTypes[nextLevel]) { - listStack.splice(nextLevel); - break; + if (shouldReuseAllAncestorListElements) { + // Remove any un-needed lists from the stack. + if (listStack.length > this.listTypes.length) { + listStack.splice(this.listTypes.length); + } + + // 1. If the listStack is the same length as the listTypes for this item, check + // if the last item needs to change, and remove it if needed. We can always re-use + // the other lists even if the type doesn't match - since the display is the same + // as long as the list immediately surrounding the item is correct. + const listStackEndIndex = listStack.length - 1; + if ( + listStackEndIndex === this.listTypes.length - 1 && // they are the same length + getListTypeFromNode(listStack[listStackEndIndex]) !== + this.listTypes[listStackEndIndex] + ) { + listStack.splice(listStackEndIndex); + } + + nextLevel = listStack.length; + } else { + // 1. Determine list elements that we can reuse + // e.g.: + // passed in listStack: Fragment > OL > UL > OL + // local listTypes: null > OL > UL > UL > OL + // then Fragment > OL > UL can be reused + for (; nextLevel < listStack.length; nextLevel++) { + if (getListTypeFromNode(listStack[nextLevel]) !== this.listTypes[nextLevel]) { + listStack.splice(nextLevel); + break; + } } } diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts index decc04d477f..e94b797d98a 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts @@ -75,7 +75,9 @@ const MergeInNewLine: BuildInEditFeature = { blockFormat(editor, (region, start, end) => { const vList = createVListFromRegion(region, false /*includeSiblingList*/, li); vList.setIndentation(start, end, Indentation.Decrease, true /*softOutdent*/); - vList.writeBack(); + vList.writeBack( + editor.isFeatureEnabled(ExperimentalFeatures.ReuseAllAncestorListElements) + ); event.rawEvent.preventDefault(); }); } else { diff --git a/packages/roosterjs-editor-types/lib/enum/ExperimentalFeatures.ts b/packages/roosterjs-editor-types/lib/enum/ExperimentalFeatures.ts index 40660363aee..16dc768c1cd 100644 --- a/packages/roosterjs-editor-types/lib/enum/ExperimentalFeatures.ts +++ b/packages/roosterjs-editor-types/lib/enum/ExperimentalFeatures.ts @@ -101,4 +101,12 @@ export const enum ExperimentalFeatures { * e.g. We will move list items with "display: block" into previous list item and change tag to be DIV */ NormalizeList = 'NormalizeList', + + /** + * With this feature enabled, when writing back a list item we will re-use all + * ancestor list elements, even if they don't match the types currently in the + * listTypes array for that item. The only list that we will ensure is correct + * is the one closest to the item. + */ + ReuseAllAncestorListElements = 'ReuseAllAncestorListElements', } From 5dcbb43a5ec7d50d67ca5d38cece098aa97a60fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Thu, 22 Sep 2022 11:19:15 -0300 Subject: [PATCH 35/41] fix conflicts --- packages/roosterjs-editor-types/lib/enum/ExperimentalFeatures.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/roosterjs-editor-types/lib/enum/ExperimentalFeatures.ts b/packages/roosterjs-editor-types/lib/enum/ExperimentalFeatures.ts index de143478794..0fc099c1be0 100644 --- a/packages/roosterjs-editor-types/lib/enum/ExperimentalFeatures.ts +++ b/packages/roosterjs-editor-types/lib/enum/ExperimentalFeatures.ts @@ -107,6 +107,7 @@ export const enum ExperimentalFeatures { */ ImageSelection = 'ImageSelection', + /** * With this feature enabled, when writing back a list item we will re-use all * ancestor list elements, even if they don't match the types currently in the * listTypes array for that item. The only list that we will ensure is correct From f8198d2c220a2fc363de69e0a74afa4ab2daefb3 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Thu, 22 Sep 2022 10:31:23 -0700 Subject: [PATCH 36/41] Remove 'caret-color' css rule when paste (#1283) * Remove 'caret-color' css rule when paste * fixtest --- .../lib/coreApi/createPasteFragment.ts | 7 ++++++- .../test/coreApi/createPasteFragmentTest.ts | 6 ++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/roosterjs-editor-core/lib/coreApi/createPasteFragment.ts b/packages/roosterjs-editor-core/lib/coreApi/createPasteFragment.ts index ff1c3341819..6443d2d2db9 100644 --- a/packages/roosterjs-editor-core/lib/coreApi/createPasteFragment.ts +++ b/packages/roosterjs-editor-core/lib/coreApi/createPasteFragment.ts @@ -201,11 +201,16 @@ function getCurrentFormat(core: EditorCore, node: Node): DefaultFormat { } function createBeforePasteEvent(core: EditorCore, clipboardData: ClipboardData): BeforePasteEvent { + const options = createDefaultHtmlSanitizerOptions(); + + // Remove "caret-color" style generated by Safari to make sure caret shows in right color after paste + options.cssStyleCallbacks['caret-color'] = () => false; + return { eventType: PluginEventType.BeforePaste, clipboardData, fragment: core.contentDiv.ownerDocument.createDocumentFragment(), - sanitizingOption: createDefaultHtmlSanitizerOptions(), + sanitizingOption: options, htmlBefore: '', htmlAfter: '', htmlAttributes: {}, diff --git a/packages/roosterjs-editor-core/test/coreApi/createPasteFragmentTest.ts b/packages/roosterjs-editor-core/test/coreApi/createPasteFragmentTest.ts index 0350d5441f7..d27f9f0af3f 100644 --- a/packages/roosterjs-editor-core/test/coreApi/createPasteFragmentTest.ts +++ b/packages/roosterjs-editor-core/test/coreApi/createPasteFragmentTest.ts @@ -317,7 +317,9 @@ describe('createPasteFragment', () => { }); it('html input with html attributes and meta', () => { - const sanitizingOption: any = {}; + const sanitizingOption: any = { + cssStyleCallbacks: {}, + }; spyOn(createDefaultHtmlSanitizerOptions, 'default').and.returnValue(sanitizingOption); const triggerEvent = jasmine.createSpy(); @@ -361,7 +363,7 @@ describe('createPasteFragment', () => { }); it('html input, make sure STYLE tags are properly handled', () => { - const sanitizingOption: any = { additionalGlobalStyleNodes: [] }; + const sanitizingOption: any = { additionalGlobalStyleNodes: [], cssStyleCallbacks: {} }; spyOn(createDefaultHtmlSanitizerOptions, 'default').and.returnValue(sanitizingOption); const triggerEvent = jasmine.createSpy(); From 4e3ca79bda7b64942dd2fcff74fc60823caa5c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Thu, 22 Sep 2022 14:40:27 -0300 Subject: [PATCH 37/41] refactor and add tests --- .../lib/coreApi/ensureUniqueId.ts | 27 ------------- .../lib/coreApi/selectImage.ts | 20 +++------- .../lib/coreApi/selectTable.ts | 19 +++------ .../lib/coreApi/utils/addSelectionStyle.ts | 20 ++++++++++ .../lib/coreApi/utils/addUniqueId.ts | 30 ++++++++++++++ .../test/coreApi/utils/addSelectionStyle.ts | 34 ++++++++++++++++ .../test/coreApi/utils/addUniqueIdTest.ts | 40 +++++++++++++++++++ 7 files changed, 135 insertions(+), 55 deletions(-) delete mode 100644 packages/roosterjs-editor-core/lib/coreApi/ensureUniqueId.ts create mode 100644 packages/roosterjs-editor-core/lib/coreApi/utils/addSelectionStyle.ts create mode 100644 packages/roosterjs-editor-core/lib/coreApi/utils/addUniqueId.ts create mode 100644 packages/roosterjs-editor-core/test/coreApi/utils/addSelectionStyle.ts create mode 100644 packages/roosterjs-editor-core/test/coreApi/utils/addUniqueIdTest.ts diff --git a/packages/roosterjs-editor-core/lib/coreApi/ensureUniqueId.ts b/packages/roosterjs-editor-core/lib/coreApi/ensureUniqueId.ts deleted file mode 100644 index 3a6cb002668..00000000000 --- a/packages/roosterjs-editor-core/lib/coreApi/ensureUniqueId.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Add an unique id to element and ensure that is unique - * @param el The HTMLElement that will receive the id - * @param idPrefix The prefix that will antecede the id (Ex: tableSelected01) - */ -export function ensureUniqueId(el: HTMLElement, idPrefix: string) { - const doc = el.ownerDocument; - - if (!el.id) { - let cont = 0; - const getElement = () => doc.getElementById(idPrefix + cont); - //Ensure that there are no elements with the same ID - let element = getElement(); - while (element) { - cont++; - element = getElement(); - } - - el.id = idPrefix + cont; - } else { - const elements = doc.querySelectorAll(`#${el.id}`); - if (elements.length > 1) { - el.removeAttribute('id'); - ensureUniqueId(el, idPrefix); - } - } -} diff --git a/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts b/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts index 52d68ce76af..d66a4dbab7d 100644 --- a/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts +++ b/packages/roosterjs-editor-core/lib/coreApi/selectImage.ts @@ -1,6 +1,7 @@ +import addSelectionStyle from './utils/addSelectionStyle'; +import addUniqueId from './utils/addUniqueId'; import { createRange } from 'roosterjs-editor-dom'; import { EditorCore, SelectImage, SelectionRangeTypes } from 'roosterjs-editor-types'; -import { ensureUniqueId } from './ensureUniqueId'; const IMAGE_ID = 'imageSelected'; const CONTENT_DIV_ID = 'contentDiv_'; @@ -16,8 +17,8 @@ export const selectImage: SelectImage = (core: EditorCore, image: HTMLImageEleme if (image) { const range = createRange(image); - ensureUniqueId(image, IMAGE_ID); - ensureUniqueId(core.contentDiv, CONTENT_DIV_ID); + addUniqueId(image, IMAGE_ID); + addUniqueId(core.contentDiv, CONTENT_DIV_ID); select(core, image); return { @@ -32,17 +33,8 @@ export const selectImage: SelectImage = (core: EditorCore, image: HTMLImageEleme }; const select = (core: EditorCore, image: HTMLImageElement) => { - const styleTagId = STYLE_ID + core.contentDiv.id; - const doc = core.contentDiv.ownerDocument; - let styleTag = doc.getElementById(styleTagId) as HTMLStyleElement; - if (!styleTag) { - styleTag = doc.createElement('style'); - styleTag.id = styleTagId; - doc.head.appendChild(styleTag); - } - const borderCSS = buildBorderCSS(core, image.id); - styleTag.sheet?.insertRule(borderCSS); + addSelectionStyle(core, borderCSS, STYLE_ID); }; const buildBorderCSS = (core: EditorCore, imageId: string): string => { @@ -54,7 +46,7 @@ const buildBorderCSS = (core: EditorCore, imageId: string): string => { imageId + ' { margin: -2px; border: 2px solid' + borderColor + - '}' + ' !important; }' ); }; diff --git a/packages/roosterjs-editor-core/lib/coreApi/selectTable.ts b/packages/roosterjs-editor-core/lib/coreApi/selectTable.ts index f32ff140b43..b560ea62915 100644 --- a/packages/roosterjs-editor-core/lib/coreApi/selectTable.ts +++ b/packages/roosterjs-editor-core/lib/coreApi/selectTable.ts @@ -1,4 +1,5 @@ -import { ensureUniqueId } from './ensureUniqueId'; +import addSelectionStyle from './utils/addSelectionStyle'; +import addUniqueId from './utils/addUniqueId'; import { createRange, getStyles, @@ -38,8 +39,8 @@ export const selectTable: SelectTable = ( unselect(core); if (areValidCoordinates(coordinates) && table) { - ensureUniqueId(table, TABLE_ID); - ensureUniqueId(core.contentDiv, CONTENT_DIV_ID); + addUniqueId(table, TABLE_ID); + addUniqueId(core.contentDiv, CONTENT_DIV_ID); const ranges = select(core, table, coordinates); if (!isMergedCell(table, coordinates)) { @@ -160,19 +161,9 @@ function buildCss( } function select(core: EditorCore, table: HTMLTableElement, coordinates: TableSelection): Range[] { - const doc = core.contentDiv.ownerDocument; const contentDivSelector = '#' + core.contentDiv.id; let { css, ranges } = buildCss(table, coordinates, contentDivSelector); - - let styleElement = doc.getElementById(STYLE_ID + core.contentDiv.id) as HTMLStyleElement; - if (!styleElement) { - styleElement = doc.createElement('style'); - doc.head.appendChild(styleElement); - styleElement.id = STYLE_ID + core.contentDiv.id; - } - - styleElement.sheet?.insertRule(css); - + addSelectionStyle(core, css, STYLE_ID); return ranges; } diff --git a/packages/roosterjs-editor-core/lib/coreApi/utils/addSelectionStyle.ts b/packages/roosterjs-editor-core/lib/coreApi/utils/addSelectionStyle.ts new file mode 100644 index 00000000000..782376f15f7 --- /dev/null +++ b/packages/roosterjs-editor-core/lib/coreApi/utils/addSelectionStyle.ts @@ -0,0 +1,20 @@ +import { EditorCore } from 'roosterjs-editor-types'; + +/** + * Add style to selected elements + * @param core The Editor core object + * @param cssRule The css rule that must added to the selection + * @param styleId the ID of the style tag + */ + +export default function addSelectionStyle(core: EditorCore, cssRule: string, styleId: string) { + const styleTagId = styleId + core.contentDiv.id; + const doc = core.contentDiv.ownerDocument; + let styleTag = doc.getElementById(styleTagId) as HTMLStyleElement; + if (!styleTag) { + styleTag = doc.createElement('style'); + styleTag.id = styleTagId; + doc.head.appendChild(styleTag); + } + styleTag.sheet?.insertRule(cssRule); +} diff --git a/packages/roosterjs-editor-core/lib/coreApi/utils/addUniqueId.ts b/packages/roosterjs-editor-core/lib/coreApi/utils/addUniqueId.ts new file mode 100644 index 00000000000..4e9fab7ef9f --- /dev/null +++ b/packages/roosterjs-editor-core/lib/coreApi/utils/addUniqueId.ts @@ -0,0 +1,30 @@ +/** + * Add an unique id to element and ensure that is unique + * @param el The HTMLElement that will receive the id + * @param idPrefix The prefix that will antecede the id (Ex: tableSelected01) + */ +export default function addUniqueId(el: HTMLElement, idPrefix: string) { + const doc = el.ownerDocument; + if (!el.id) { + applyId(el, idPrefix, doc); + } else { + const elements = doc.querySelectorAll(`#${el.id}`); + if (elements.length > 1) { + el.removeAttribute('id'); + applyId(el, idPrefix, doc); + } + } +} + +function applyId(el: HTMLElement, idPrefix: string, doc: Document) { + let cont = 0; + const getElement = () => doc.getElementById(idPrefix + cont); + //Ensure that there are no elements with the same ID + let element = getElement(); + while (element) { + cont++; + element = getElement(); + } + + el.id = idPrefix + cont; +} diff --git a/packages/roosterjs-editor-core/test/coreApi/utils/addSelectionStyle.ts b/packages/roosterjs-editor-core/test/coreApi/utils/addSelectionStyle.ts new file mode 100644 index 00000000000..48a5dcbc172 --- /dev/null +++ b/packages/roosterjs-editor-core/test/coreApi/utils/addSelectionStyle.ts @@ -0,0 +1,34 @@ +import addSelectionStyle from '../../../lib/coreApi/utils/addSelectionStyle'; +import createEditorCore from '../createMockEditorCore'; +import { EditorCore } from 'roosterjs-editor-types'; + +describe('addSelectionStyle', () => { + let div: HTMLDivElement; + let core: EditorCore | null; + beforeEach(() => { + div = document.createElement('div'); + document.body.appendChild(div); + core = createEditorCore(div!, {}); + }); + + afterEach(() => { + document.body.removeChild(div); + div = null; + core = null; + }); + + it('should add an style ', () => { + core.contentDiv.id = 'contentTest'; + const css = + '#' + + 'contentTest' + + ' #' + + 'test' + + ' { margin: -2px; border: 2px solid' + + '#DB626C' + + ' !important; }'; + addSelectionStyle(core, css, 'test'); + const styleTag = document.getElementById('testcontentTest'); + expect(styleTag?.tagName).toBe('STYLE'); + }); +}); diff --git a/packages/roosterjs-editor-core/test/coreApi/utils/addUniqueIdTest.ts b/packages/roosterjs-editor-core/test/coreApi/utils/addUniqueIdTest.ts new file mode 100644 index 00000000000..e9cfe39bbd8 --- /dev/null +++ b/packages/roosterjs-editor-core/test/coreApi/utils/addUniqueIdTest.ts @@ -0,0 +1,40 @@ +import addUniqueId from '../../../lib/coreApi/utils/addUniqueId'; + +describe('addUniqueId', () => { + let div: HTMLDivElement; + beforeEach(() => { + div = document.createElement('div'); + document.body.appendChild(div); + }); + + afterEach(() => { + document.body.removeChild(div); + div = null; + }); + + it('should add an id ', () => { + addUniqueId(div, 'test'); + expect(div.id).toBe('test0'); + }); + + it('should unique id ', () => { + const span = document.createElement('span'); + document.body.appendChild(span); + addUniqueId(div, 'test'); + addUniqueId(span, 'test'); + expect(div.id).toBe('test0'); + expect(span.id).toBe('test1'); + document.body.removeChild(span); + }); + + it('should replace existing ids', () => { + const span = document.createElement('span'); + span.id = 'test0'; + document.body.appendChild(span); + addUniqueId(div, 'test'); + addUniqueId(span, 'test'); + expect(div.id).toBe('test1'); + expect(span.id).toBe('test0'); + document.body.removeChild(span); + }); +}); From 15c736337d4fb3879acd90558e32fe3c7f95e5c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Thu, 22 Sep 2022 15:38:53 -0300 Subject: [PATCH 38/41] salve only image id instead of the the image element --- .../plugins/ImageSelection/ImageSelection.ts | 23 ++++++++++--------- .../test/ImageSelection/imageSelectionTest.ts | 2 +- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ImageSelection/ImageSelection.ts b/packages/roosterjs-editor-plugins/lib/plugins/ImageSelection/ImageSelection.ts index c486ea0ae63..81123df9ba2 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ImageSelection/ImageSelection.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ImageSelection/ImageSelection.ts @@ -12,7 +12,7 @@ import { */ export default class ImageSelection implements EditorPlugin { private editor: IEditor | null = null; - private image: HTMLImageElement | null = null; + private imageId: string | null = null; constructor() {} @@ -37,7 +37,7 @@ export default class ImageSelection implements EditorPlugin { dispose() { this.editor.select(null); this.editor = null; - this.image = null; + this.imageId = null; } onPluginEvent(event: PluginEvent) { @@ -50,21 +50,22 @@ export default class ImageSelection implements EditorPlugin { } break; case PluginEventType.LeavingShadowEdit: - if (this.image) { - const image = this.editor.queryElements('#' + this.image.id); - if (image.length == 1) { - this.image = image[0] as HTMLImageElement; - this.editor.select(this.image); + if (this.imageId) { + const images = this.editor.queryElements( + '#' + this.imageId + ) as HTMLImageElement[]; + if (images.length == 1) { + const image = images[0]; + this.editor.select(image); } + this.imageId = null; } break; case PluginEventType.MouseDown: const target = event.rawEvent.target; if (safeInstanceOf(target, 'HTMLImageElement')) { - this.image = target; - this.editor.select(this.image); - } else { - this.image = null; + this.editor.select(target); + this.imageId = target.id; } break; } diff --git a/packages/roosterjs-editor-plugins/test/ImageSelection/imageSelectionTest.ts b/packages/roosterjs-editor-plugins/test/ImageSelection/imageSelectionTest.ts index 2bc8a946241..0e27fd076e1 100644 --- a/packages/roosterjs-editor-plugins/test/ImageSelection/imageSelectionTest.ts +++ b/packages/roosterjs-editor-plugins/test/ImageSelection/imageSelectionTest.ts @@ -48,7 +48,7 @@ describe('ImageSelectionPlugin |', () => { expect(selection.areAllCollapsed).toBe(false); }); - it('should be triggered in shadopw', () => { + it('should be triggered in shadow Edit', () => { editor.setContent(``); const target = document.getElementById(imageId); editorIsFeatureEnabled.and.returnValue(true); From aea4982b1b3bf05a00d1247f400630e4c230ad9a Mon Sep 17 00:00:00 2001 From: Ian Elizondo Date: Fri, 23 Sep 2022 10:49:00 -0600 Subject: [PATCH 39/41] Enable strict mode for "packages-ui" (#1285) * Enable strict mode --- .../lib/common/type/LocalizedStrings.ts | 4 +- .../lib/common/utils/createUIUtilities.tsx | 2 +- .../lib/common/utils/getLocalizedString.ts | 6 +- .../menus/createImageEditMenuProvider.tsx | 6 +- .../menus/createTableEditMenuProvider.ts | 9 +- .../plugin/createContextMenuPlugin.tsx | 2 +- .../lib/contextMenu/types/ContextMenuItem.ts | 2 +- .../utils/createContextMenuProvider.ts | 51 +++++++----- .../lib/emoji/components/EmojiIcon.tsx | 2 +- .../lib/emoji/components/EmojiNavBar.tsx | 6 +- .../lib/emoji/components/EmojiPane.tsx | 45 +++++----- .../lib/emoji/components/EmojiStatusBar.tsx | 2 +- .../lib/emoji/plugin/createEmojiPlugin.ts | 82 +++++++++++-------- .../lib/emoji/utils/emojiList.ts | 8 +- .../lib/emoji/utils/searchEmojis.ts | 6 +- .../lib/inputDialog/component/InputDialog.tsx | 23 ++++-- .../inputDialog/component/InputDialogItem.tsx | 4 +- .../lib/inputDialog/type/DialogItem.ts | 2 +- .../lib/inputDialog/utils/showInputDialog.tsx | 2 +- .../component/showPasteOptionPane.tsx | 6 +- .../plugin/createPasteOptionPlugin.ts | 26 +++--- .../lib/ribbon/component/Ribbon.tsx | 42 +++++++--- .../lib/ribbon/component/buttons/bold.ts | 2 +- .../ribbon/component/buttons/bulletedList.ts | 2 +- .../lib/ribbon/component/buttons/fontSize.ts | 2 +- .../lib/ribbon/component/buttons/header.ts | 4 +- .../ribbon/component/buttons/insertImage.ts | 6 +- .../ribbon/component/buttons/insertLink.ts | 6 +- .../ribbon/component/buttons/insertTable.tsx | 10 +-- .../lib/ribbon/component/buttons/italic.ts | 2 +- .../ribbon/component/buttons/numberedList.ts | 2 +- .../lib/ribbon/component/buttons/quote.ts | 2 +- .../ribbon/component/buttons/strikethrough.ts | 2 +- .../lib/ribbon/component/buttons/subscript.ts | 2 +- .../ribbon/component/buttons/superscript.ts | 2 +- .../lib/ribbon/component/buttons/underline.ts | 2 +- .../lib/ribbon/component/getButtons.ts | 6 +- .../lib/ribbon/plugin/createRibbonPlugin.ts | 28 ++++--- .../lib/ribbon/type/RibbonButton.ts | 2 +- .../lib/ribbon/type/RibbonButtonDropDown.ts | 2 +- .../lib/ribbon/type/RibbonPlugin.ts | 4 +- .../lib/ribbon/type/RibbonProps.ts | 2 +- .../lib/rooster/component/Rooster.tsx | 16 ++-- .../plugin/createUpdateContentPlugin.ts | 4 +- packages-ui/tsconfig.json | 2 +- 45 files changed, 256 insertions(+), 194 deletions(-) diff --git a/packages-ui/roosterjs-react/lib/common/type/LocalizedStrings.ts b/packages-ui/roosterjs-react/lib/common/type/LocalizedStrings.ts index b51a2160496..3fa370db2b9 100644 --- a/packages-ui/roosterjs-react/lib/common/type/LocalizedStrings.ts +++ b/packages-ui/roosterjs-react/lib/common/type/LocalizedStrings.ts @@ -1,8 +1,8 @@ /** * Represents a localized string map from the string key to the localized string or a function returns localized string */ -export type LocalizedStrings = { - [key in T]: string | (() => string); +export type LocalizedStrings = { + [key in T]?: V | (() => V); }; /** diff --git a/packages-ui/roosterjs-react/lib/common/utils/createUIUtilities.tsx b/packages-ui/roosterjs-react/lib/common/utils/createUIUtilities.tsx index 95dce05aafc..5350bf6e40d 100644 --- a/packages-ui/roosterjs-react/lib/common/utils/createUIUtilities.tsx +++ b/packages-ui/roosterjs-react/lib/common/utils/createUIUtilities.tsx @@ -22,7 +22,7 @@ export default function createUIUtilities( doc.body.appendChild(div); ReactDOM.render( - + {element} , div diff --git a/packages-ui/roosterjs-react/lib/common/utils/getLocalizedString.ts b/packages-ui/roosterjs-react/lib/common/utils/getLocalizedString.ts index dc791749ecd..9763426bd0a 100644 --- a/packages-ui/roosterjs-react/lib/common/utils/getLocalizedString.ts +++ b/packages-ui/roosterjs-react/lib/common/utils/getLocalizedString.ts @@ -7,10 +7,10 @@ import { LocalizedStrings } from '../type/LocalizedStrings'; * @param defaultString Default unlocalized string, will be used if strings is not specified or the give key doesn't exist in strings * @returns A localized string from the string map, or defaultString */ -export default function getLocalizedString( - strings: Partial>, +export default function getLocalizedString( + strings: LocalizedStrings | undefined, key: T, - defaultString: string + defaultString: R ) { const str = strings?.[key]; diff --git a/packages-ui/roosterjs-react/lib/contextMenu/menus/createImageEditMenuProvider.tsx b/packages-ui/roosterjs-react/lib/contextMenu/menus/createImageEditMenuProvider.tsx index a82a2bfd1c4..877be0f2dbd 100644 --- a/packages-ui/roosterjs-react/lib/contextMenu/menus/createImageEditMenuProvider.tsx +++ b/packages-ui/roosterjs-react/lib/contextMenu/menus/createImageEditMenuProvider.tsx @@ -13,7 +13,7 @@ import { resizeByPercentage, } from 'roosterjs-editor-plugins'; -const ImageAltTextMenuItem: ContextMenuItem = { +const ImageAltTextMenuItem: ContextMenuItem = { key: 'menuNameImageAltText', unlocalizedText: 'Add alternate text', onClick: (_, editor, node, strings, uiUtilities) => { @@ -43,7 +43,7 @@ const ImageAltTextMenuItem: ContextMenuItem = { }, }; -const ImageResizeMenuItem: ContextMenuItem = { +const ImageResizeMenuItem: ContextMenuItem = { key: 'menuNameImageResize', unlocalizedText: 'Size', subItems: { @@ -121,7 +121,7 @@ export default function createImageEditMenuProvider( imageEditPlugin: ImageEdit, strings?: LocalizedStrings ): EditorPlugin { - return createContextMenuProvider( + return createContextMenuProvider( 'imageEdit', [ImageAltTextMenuItem, ImageResizeMenuItem, ImageCropMenuItem, ImageRemoveMenuItem], strings, diff --git a/packages-ui/roosterjs-react/lib/contextMenu/menus/createTableEditMenuProvider.ts b/packages-ui/roosterjs-react/lib/contextMenu/menus/createTableEditMenuProvider.ts index 18c38d40ead..ceae17a4e5c 100644 --- a/packages-ui/roosterjs-react/lib/contextMenu/menus/createTableEditMenuProvider.ts +++ b/packages-ui/roosterjs-react/lib/contextMenu/menus/createTableEditMenuProvider.ts @@ -53,12 +53,15 @@ const TableEditOperationMap: Partialnull, + menuNameTableCellShade: (null), }; function onClick(key: TableEditMenuItemStringKey, editor: IEditor) { editor.focus(); - editTable(editor, TableEditOperationMap[key]); + const operation = TableEditOperationMap[key]; + if (typeof operation === 'number') { + editTable(editor, operation); + } } const TableEditInsertMenuItem: ContextMenuItem = { @@ -164,7 +167,7 @@ export default function createTableEditMenuProvider( ): EditorPlugin { return createContextMenuProvider( 'tableEdit', - [ + []>[ TableEditInsertMenuItem, TableEditDeleteMenuItem, TableEditMergeMenuItem, diff --git a/packages-ui/roosterjs-react/lib/contextMenu/plugin/createContextMenuPlugin.tsx b/packages-ui/roosterjs-react/lib/contextMenu/plugin/createContextMenuPlugin.tsx index 39bfa60a8a0..dbfec75be62 100644 --- a/packages-ui/roosterjs-react/lib/contextMenu/plugin/createContextMenuPlugin.tsx +++ b/packages-ui/roosterjs-react/lib/contextMenu/plugin/createContextMenuPlugin.tsx @@ -4,7 +4,7 @@ import { ContextualMenu, IContextualMenuItem } from '@fluentui/react/lib/Context import { ReactEditorPlugin, UIUtilities } from '../../common/index'; import { renderReactComponent } from '../../common/utils/renderReactComponent'; -function normalizeItems(items: IContextualMenuItem[]) { +function normalizeItems(items: (IContextualMenuItem | null)[]) { let dividerKey = 0; return items.map( item => diff --git a/packages-ui/roosterjs-react/lib/contextMenu/types/ContextMenuItem.ts b/packages-ui/roosterjs-react/lib/contextMenu/types/ContextMenuItem.ts index c5ec2843733..6ea6787faa2 100644 --- a/packages-ui/roosterjs-react/lib/contextMenu/types/ContextMenuItem.ts +++ b/packages-ui/roosterjs-react/lib/contextMenu/types/ContextMenuItem.ts @@ -29,7 +29,7 @@ export default interface ContextMenuItem, + strings: LocalizedStrings | undefined, uiUtilities: UIUtilities, context: TContext ) => void; diff --git a/packages-ui/roosterjs-react/lib/contextMenu/utils/createContextMenuProvider.ts b/packages-ui/roosterjs-react/lib/contextMenu/utils/createContextMenuProvider.ts index 9e6e24bb9a9..130b5772ed8 100644 --- a/packages-ui/roosterjs-react/lib/contextMenu/utils/createContextMenuProvider.ts +++ b/packages-ui/roosterjs-react/lib/contextMenu/utils/createContextMenuProvider.ts @@ -3,14 +3,15 @@ import getLocalizedString from '../../common/utils/getLocalizedString'; import { ContextMenuProvider, EditorPlugin, IEditor } from 'roosterjs-editor-types'; import { IContextualMenuItem } from '@fluentui/react/lib/ContextualMenu'; import { LocalizedStrings, ReactEditorPlugin, UIUtilities } from '../../common/index'; +import { getObjectKeys } from 'roosterjs-editor-dom'; /** * A plugin of editor to provide context menu items */ class ContextMenuProviderImpl implements ContextMenuProvider, ReactEditorPlugin { - private editor: IEditor; - private targetNode: Node; + private editor: IEditor | null = null; + private targetNode: Node | null = null; private uiUtilities: UIUtilities | null = null; /** @@ -53,10 +54,14 @@ class ContextMenuProviderImpl getContextMenuItems(node: Node) { this.targetNode = node; - return this.shouldAddMenuItems(this.editor, node) + return this.editor && this.shouldAddMenuItems?.(this.editor, node) ? this.items .filter( - item => !item.shouldShow || item.shouldShow(this.editor, node, this.context) + item => + !item.shouldShow || + (this.editor && + this.context && + item.shouldShow(this.editor, node, this.context)) ) .map(item => this.convertMenuItems(item)) : []; @@ -75,15 +80,15 @@ class ContextMenuProviderImpl onClick: () => this.onClick(item, item.key), subMenuProps: item.subItems ? { - onItemClick: (_, menuItem) => this.onClick(item, menuItem.data), - items: Object.keys(item.subItems).map((key: keyof typeof item.subItems) => ({ + onItemClick: (_, menuItem) => menuItem && this.onClick(item, menuItem.data), + items: getObjectKeys(item.subItems).map(key => ({ key: key, data: key, - text: getLocalizedString(this.strings, key, item.subItems[key]), + text: getLocalizedString(this.strings, key, item.subItems?.[key]), className: item.itemClassName, onRender: item.itemRender - ? subItem => item.itemRender(subItem, () => this.onClick(item, key)) - : null, + ? subItem => item.itemRender?.(subItem, () => this.onClick(item, key)) + : undefined, })), ...(item.commandBarSubMenuProperties || {}), } @@ -92,14 +97,16 @@ class ContextMenuProviderImpl } private onClick(item: ContextMenuItem, key: TString) { - item.onClick( - key, - this.editor, - this.targetNode, - this.strings, - this.uiUtilities, - this.context - ); + if (this.editor && this.targetNode && this.uiUtilities && this.context) { + item.onClick( + key, + this.editor, + this.targetNode, + this.strings, + this.uiUtilities, + this.context + ); + } } } @@ -112,10 +119,16 @@ class ContextMenuProviderImpl */ export default function createContextMenuProvider( menuName: string, - items: ContextMenuItem[], + items: ContextMenuItem[], strings?: LocalizedStrings, shouldAddMenuItems?: (editor: IEditor, node: Node) => boolean, context?: TContext ): EditorPlugin { - return new ContextMenuProviderImpl(menuName, items, strings, shouldAddMenuItems, context); + return new ContextMenuProviderImpl( + menuName, + items, + strings, + shouldAddMenuItems, + context + ); } diff --git a/packages-ui/roosterjs-react/lib/emoji/components/EmojiIcon.tsx b/packages-ui/roosterjs-react/lib/emoji/components/EmojiIcon.tsx index 636be5d72ce..0d8cd114d4a 100644 --- a/packages-ui/roosterjs-react/lib/emoji/components/EmojiIcon.tsx +++ b/packages-ui/roosterjs-react/lib/emoji/components/EmojiIcon.tsx @@ -24,7 +24,7 @@ export interface EmojiIconProps { */ export default function EmojiIcon(props: EmojiIconProps) { const { emoji, onClick, isSelected, onMouseOver, onFocus, strings, id, classNames } = props; - const content = strings[emoji.description]; + const content = emoji.description && strings[emoji.description]; return (