From f97309082b1bbb268da44960a4bcf622af7c94ec Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Fri, 14 Oct 2022 09:39:58 -0700 Subject: [PATCH] Allow insert entity on region root (#1316) * Allow insert entity on region root * improve * fix comment --- .../insertEntity/InsertEntityPane.tsx | 20 ++++++++++- .../lib/format/insertEntity.ts | 17 ++++++++-- .../lib/coreApi/insertNode.ts | 34 ++++++++++++++++++- .../test/coreApi/insertNodeTest.ts | 28 +++++++++++++++ .../lib/interface/InsertOption.ts | 7 ++++ 5 files changed, 102 insertions(+), 4 deletions(-) diff --git a/demo/scripts/controls/sidePane/apiPlayground/insertEntity/InsertEntityPane.tsx b/demo/scripts/controls/sidePane/apiPlayground/insertEntity/InsertEntityPane.tsx index 982b8aedce6..29b52b83a1e 100644 --- a/demo/scripts/controls/sidePane/apiPlayground/insertEntity/InsertEntityPane.tsx +++ b/demo/scripts/controls/sidePane/apiPlayground/insertEntity/InsertEntityPane.tsx @@ -18,6 +18,7 @@ export default class InsertEntityPane extends React.Component(); private styleBlock = React.createRef(); private isReadonly = React.createRef(); + private insertAtRoot = React.createRef(); constructor(props: ApiPaneProps) { super(props); @@ -54,6 +55,10 @@ export default class InsertEntityPane extends React.Component Readonly: +
+ Force insert at root of region:{' '} + +
@@ -77,9 +82,22 @@ export default class InsertEntityPane extends React.Component { + insertEntity( + editor, + entityType, + node, + isBlock, + isReadonly, + undefined /*position*/, + insertAtRoot + ); + }); } }; diff --git a/packages/roosterjs-editor-api/lib/format/insertEntity.ts b/packages/roosterjs-editor-api/lib/format/insertEntity.ts index e59d4b07876..6d7c25bb28b 100644 --- a/packages/roosterjs-editor-api/lib/format/insertEntity.ts +++ b/packages/roosterjs-editor-api/lib/format/insertEntity.ts @@ -1,8 +1,10 @@ +import commitListChains from '../utils/commitListChains'; import { commitEntity, getEntityFromElement, getEntitySelector, Position, + VListChain, wrap, } from 'roosterjs-editor-dom'; import { @@ -21,8 +23,10 @@ import { * @param contentNode Root element of the entity * @param isBlock Whether the entity will be shown as a block * @param isReadonly Whether the entity will be a readonly entity - * @param position (Optional) The position to insert into. If not specified, current position will be used. + * @param position @optional The position to insert into. If not specified, current position will be used. * If isBlock is true, entity will be insert below this position + * @param insertToRegionRoot @optional When pass true, insert the entity at the root level of current region. + * Parent nodes will be split if need */ export default function insertEntity( editor: IEditor, @@ -30,7 +34,8 @@ export default function insertEntity( contentNode: Node, isBlock: boolean, isReadonly: boolean, - position?: NodePosition | ContentPosition.Begin | ContentPosition.End | ContentPosition.DomEnd + position?: NodePosition | ContentPosition.Begin | ContentPosition.End | ContentPosition.DomEnd, + insertToRegionRoot?: boolean ): Entity { const wrapper = wrap(contentNode, isBlock ? 'DIV' : 'SPAN'); @@ -73,13 +78,21 @@ export default function insertEntity( contentPosition = ContentPosition.SelectionStart; } + const regions = insertToRegionRoot && editor.getSelectedRegions(); + const chains = regions && VListChain.createListChains(regions); + editor.insertNode(wrapper, { updateCursor: false, insertOnNewLine: isBlock, replaceSelection: true, position: contentPosition, + insertToRegionRoot: insertToRegionRoot, }); + if (chains) { + commitListChains(editor, chains); + } + if (contentPosition == ContentPosition.SelectionStart) { if (currentRange) { editor.select(currentRange); diff --git a/packages/roosterjs-editor-core/lib/coreApi/insertNode.ts b/packages/roosterjs-editor-core/lib/coreApi/insertNode.ts index c03301c61e4..b1775851c42 100644 --- a/packages/roosterjs-editor-core/lib/coreApi/insertNode.ts +++ b/packages/roosterjs-editor-core/lib/coreApi/insertNode.ts @@ -8,6 +8,7 @@ import { NodeType, PositionType, NodePosition, + RegionType, } from 'roosterjs-editor-types'; import { createRange, @@ -20,6 +21,9 @@ import { toArray, wrap, adjustInsertPosition, + getRegionsFromRange, + splitTextNode, + splitParentNode, } from 'roosterjs-editor-dom'; function getInitialRange( @@ -58,6 +62,7 @@ export const insertNode: InsertNode = ( insertOnNewLine: false, updateCursor: true, replaceSelection: true, + insertToRegionRoot: false, }; let contentDiv = core.contentDiv; @@ -156,7 +161,9 @@ export const insertNode: InsertNode = ( let pos: NodePosition = Position.getStart(range); let blockElement: BlockElement | null; - if ( + if (option.insertOnNewLine && option.insertToRegionRoot) { + pos = adjustInsertPositionRegionRoot(core, range, pos); + } else if ( option.insertOnNewLine && (blockElement = getBlockElementAtNode(contentDiv, pos.normalize().node)) ) { @@ -189,6 +196,31 @@ export const insertNode: InsertNode = ( return true; }; + +function adjustInsertPositionRegionRoot(core: EditorCore, range: Range, position: NodePosition) { + const region = getRegionsFromRange(core.contentDiv, range, RegionType.Table)[0]; + let node: Node | null = position.node; + + if (region) { + if (node.nodeType == NodeType.Text && !position.isAtEnd) { + node = splitTextNode(node as Text, position.offset, true /*returnFirstPart*/); + } + + if (node != region.rootNode) { + while (node && node.parentNode != region.rootNode) { + splitParentNode(node, false /*splitBefore*/); + node = node.parentNode; + } + } + + if (node) { + position = new Position(node, PositionType.After); + } + } + + return position; +} + function adjustInsertPositionNewLine(blockElement: BlockElement, core: EditorCore, pos: Position) { let tempPos = new Position(blockElement.getEndNode(), PositionType.After); if (safeInstanceOf(tempPos.node, 'HTMLTableRowElement')) { diff --git a/packages/roosterjs-editor-core/test/coreApi/insertNodeTest.ts b/packages/roosterjs-editor-core/test/coreApi/insertNodeTest.ts index 2d29e689b6d..0921f429cd3 100644 --- a/packages/roosterjs-editor-core/test/coreApi/insertNodeTest.ts +++ b/packages/roosterjs-editor-core/test/coreApi/insertNodeTest.ts @@ -462,4 +462,32 @@ describe('insertNode', () => { '

' ); }); + + it('Insert node at root of region', () => { + const core = createEditorCore(div, {}); + div.contentEditable = 'true'; + div.innerHTML = + '
textBefore
text
textAfter
'; + div.focus(); + + const text = div.querySelector('#innerDiv')!.firstChild!; + const sel = document.createRange(); + sel.setStart(text, 2); + sel.setEnd(text, 2); + addRange(sel); + + const nodeToInsert = document.createElement('div'); + nodeToInsert.id = 'newDiv'; + + insertNode(core, nodeToInsert, { + position: ContentPosition.SelectionStart, + insertOnNewLine: true, + updateCursor: true, + replaceSelection: true, + insertToRegionRoot: true, + }); + expect(div.innerHTML).toBe( + '
textBefore
te
xt
textAfter
' + ); + }); }); diff --git a/packages/roosterjs-editor-types/lib/interface/InsertOption.ts b/packages/roosterjs-editor-types/lib/interface/InsertOption.ts index 3d95f3ac739..58b9a952c87 100644 --- a/packages/roosterjs-editor-types/lib/interface/InsertOption.ts +++ b/packages/roosterjs-editor-types/lib/interface/InsertOption.ts @@ -21,6 +21,13 @@ export interface InsertOptionBase { * No-op for ContentPosition.Begin, End, and Outside */ replaceSelection?: boolean; + + /** + * Boolean flag for inserting the content onto root node of current region. + * If current position is not at root of region, break parent node until insert can happen at root of region. + * This option only takes effect when insertOnNewLine is true, otherwise it will be ignored. + */ + insertToRegionRoot?: boolean; } /**