Skip to content

Commit

Permalink
Allow insert entity on region root (#1316)
Browse files Browse the repository at this point in the history
* Allow insert entity on region root

* improve

* fix comment
  • Loading branch information
JiuqingSong authored Oct 14, 2022
1 parent ed65ea9 commit f973090
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export default class InsertEntityPane extends React.Component<ApiPaneProps, Inse
private styleInline = React.createRef<HTMLInputElement>();
private styleBlock = React.createRef<HTMLInputElement>();
private isReadonly = React.createRef<HTMLInputElement>();
private insertAtRoot = React.createRef<HTMLInputElement>();

constructor(props: ApiPaneProps) {
super(props);
Expand Down Expand Up @@ -54,6 +55,10 @@ export default class InsertEntityPane extends React.Component<ApiPaneProps, Inse
<div>
Readonly: <input type="checkbox" ref={this.isReadonly} />
</div>
<div>
Force insert at root of region:{' '}
<input type="checkbox" ref={this.insertAtRoot} />
</div>
<div>
<button onClick={this.insertEntity}>Insert Entity</button>
</div>
Expand All @@ -77,9 +82,22 @@ export default class InsertEntityPane extends React.Component<ApiPaneProps, Inse
node.dataset.hydratedHtml = this.hydratedHtml.current.value.trim();
const isBlock = this.styleBlock.current.checked;
const isReadonly = this.isReadonly.current.checked;
const insertAtRoot = this.insertAtRoot.current.checked;

if (node) {
insertEntity(this.props.getEditor(), entityType, node, isBlock, isReadonly);
const editor = this.props.getEditor();

editor.addUndoSnapshot(() => {
insertEntity(
editor,
entityType,
node,
isBlock,
isReadonly,
undefined /*position*/,
insertAtRoot
);
});
}
};

Expand Down
17 changes: 15 additions & 2 deletions packages/roosterjs-editor-api/lib/format/insertEntity.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import commitListChains from '../utils/commitListChains';
import {
commitEntity,
getEntityFromElement,
getEntitySelector,
Position,
VListChain,
wrap,
} from 'roosterjs-editor-dom';
import {
Expand All @@ -21,16 +23,19 @@ 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,
type: string,
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');

Expand Down Expand Up @@ -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);
Expand Down
34 changes: 33 additions & 1 deletion packages/roosterjs-editor-core/lib/coreApi/insertNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
NodeType,
PositionType,
NodePosition,
RegionType,
} from 'roosterjs-editor-types';
import {
createRange,
Expand All @@ -20,6 +21,9 @@ import {
toArray,
wrap,
adjustInsertPosition,
getRegionsFromRange,
splitTextNode,
splitParentNode,
} from 'roosterjs-editor-dom';

function getInitialRange(
Expand Down Expand Up @@ -58,6 +62,7 @@ export const insertNode: InsertNode = (
insertOnNewLine: false,
updateCursor: true,
replaceSelection: true,
insertToRegionRoot: false,
};
let contentDiv = core.contentDiv;

Expand Down Expand Up @@ -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))
) {
Expand Down Expand Up @@ -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')) {
Expand Down
28 changes: 28 additions & 0 deletions packages/roosterjs-editor-core/test/coreApi/insertNodeTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,4 +462,32 @@ describe('insertNode', () => {
'<div id="div"><table></table><br><table id="table1"></table></div>'
);
});

it('Insert node at root of region', () => {
const core = createEditorCore(div, {});
div.contentEditable = 'true';
div.innerHTML =
'<div><div>textBefore</div><div id="innerDiv">text</div><div>textAfter</div></div>';
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(
'<div><div>textBefore</div><div id="innerDiv">te</div></div><div id="newDiv"></div><div><div>xt</div><div>textAfter</div></div>'
);
});
});
7 changes: 7 additions & 0 deletions packages/roosterjs-editor-types/lib/interface/InsertOption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down

0 comments on commit f973090

Please sign in to comment.