Skip to content

Commit

Permalink
Standalone editor: Remove more dependencies (#2127)
Browse files Browse the repository at this point in the history
* Standalone editor: decouple utilities

* Standalone editor: Remove more dependencies

* fix build

* remove unnecessary code
  • Loading branch information
JiuqingSong authored Oct 6, 2023
1 parent bee10b7 commit 9dfda12
Show file tree
Hide file tree
Showing 37 changed files with 733 additions and 186 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Replace all child nodes of the given target node to the child nodes of source node.
* @param target Target node, all child nodes of this node will be removed if keepExistingChildren is not set to true
* @param source (Optional) source node, all child nodes of this node will be move to target node
* @param keepExistingChildren (Optional) When set to true, all existing child nodes of target will be kept
*/
export function moveChildNodes(target: Node, source?: Node, keepExistingChildren?: boolean) {
if (!target) {
return;
}

while (!keepExistingChildren && target.firstChild) {
target.removeChild(target.firstChild);
}

while (source?.firstChild) {
target.appendChild(source.firstChild);
}
}

/**
* Wrap all child nodes of the given parent element using a new element with the given tag name
* @param parent The parent element
* @param tagName The tag name of new wrapper
* @returns New wrapper element
*/
export function wrapAllChildNodes<T extends keyof HTMLElementTagNameMap>(
parent: HTMLElement,
tagName: T
): HTMLElementTagNameMap[T] {
const newElement = parent.ownerDocument.createElement(tagName);

moveChildNodes(newElement, parent);
parent.appendChild(newElement);

return newElement;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { moveChildNodes } from 'roosterjs-editor-dom';
import { wrapAllChildNodes } from '../../domUtils/moveChildNodes';
import type { BoldFormat } from 'roosterjs-content-model-types';
import type { FormatHandler } from '../FormatHandler';

Expand All @@ -25,9 +25,7 @@ export const boldFormatHandler: FormatHandler<BoldFormat> = {
(!blockFontWeight && format.fontWeight && format.fontWeight != 'normal')
) {
if (format.fontWeight == 'bold') {
const b = element.ownerDocument.createElement('b');
moveChildNodes(b, element);
element.appendChild(b);
wrapAllChildNodes(element, 'b');
} else {
element.style.fontWeight = format.fontWeight || 'normal';
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { moveChildNodes } from 'roosterjs-editor-dom';
import { wrapAllChildNodes } from '../../domUtils/moveChildNodes';
import type { FormatHandler } from '../FormatHandler';
import type { ItalicFormat } from 'roosterjs-content-model-types';

Expand All @@ -24,9 +24,7 @@ export const italicFormatHandler: FormatHandler<ItalicFormat> = {

if (!!implicitItalic != !!format.italic) {
if (format.italic) {
const i = element.ownerDocument.createElement('i');
moveChildNodes(i, element);
element.appendChild(i);
wrapAllChildNodes(element, 'i');
} else {
element.style.fontStyle = 'normal';
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { moveChildNodes } from 'roosterjs-editor-dom';
import { wrapAllChildNodes } from '../../domUtils/moveChildNodes';
import type { FormatHandler } from '../FormatHandler';
import type { StrikeFormat } from 'roosterjs-content-model-types';

Expand All @@ -15,9 +15,7 @@ export const strikeFormatHandler: FormatHandler<StrikeFormat> = {
},
apply: (format, element) => {
if (format.strikethrough) {
const strike = element.ownerDocument.createElement('s');
moveChildNodes(strike, element);
element.appendChild(strike);
wrapAllChildNodes(element, 's');
}
},
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { moveChildNodes } from 'roosterjs-editor-dom';
import { wrapAllChildNodes } from '../../domUtils/moveChildNodes';
import type { FormatHandler } from '../FormatHandler';
import type { SuperOrSubScriptFormat } from 'roosterjs-content-model-types';

Expand Down Expand Up @@ -27,9 +27,7 @@ export const superOrSubScriptFormatHandler: FormatHandler<SuperOrSubScriptFormat
const tagName = value == 'super' ? 'sup' : value == 'sub' ? 'sub' : null;

if (tagName) {
const wrapper = element.ownerDocument.createElement(tagName);
moveChildNodes(wrapper, element);
element.appendChild(wrapper);
wrapAllChildNodes(element, tagName);
}
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { moveChildNodes } from 'roosterjs-editor-dom';
import { wrapAllChildNodes } from '../../domUtils/moveChildNodes';
import type { FormatHandler } from '../FormatHandler';
import type { UnderlineFormat } from 'roosterjs-content-model-types';

Expand All @@ -24,9 +24,7 @@ export const underlineFormatHandler: FormatHandler<UnderlineFormat> = {

if (!!blockUnderline != !!format.underline) {
if (format.underline) {
const u = element.ownerDocument.createElement('u');
moveChildNodes(u, element);
element.appendChild(u);
wrapAllChildNodes(element, 'u');
} else {
element.style.textDecoration = 'none';
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export { isNodeOfType, NodeTypeMap } from './domUtils/isNodeOfType';
export { isElementOfType } from './domUtils/isElementOfType';
export { getObjectKeys } from './domUtils/getObjectKeys';
export { default as toArray } from './domUtils/toArray';
export { moveChildNodes, wrapAllChildNodes } from './domUtils/moveChildNodes';

export { createBr } from './modelApi/creators/createBr';
export { createListItem } from './modelApi/creators/createListItem';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import toArray from '../domUtils/toArray';
import { createRange, Position } from 'roosterjs-editor-dom';
import { isNodeOfType } from '../domUtils/isNodeOfType';
import type {
ContentModelDocument,
Expand All @@ -8,7 +7,6 @@ import type {
ModelToDomContext,
OnNodeCreated,
} from 'roosterjs-content-model-types';
import type { NodePosition } from 'roosterjs-editor-types';

/**
* Create DOM tree fragment from Content Model document
Expand All @@ -32,32 +30,37 @@ export function contentModelToDom(

context.modelHandlers.blockGroupChildren(doc, root, model, context);

const range = extractSelectionRange(context);
const range = extractSelectionRange(doc, context);

root.normalize();

return range;
}

function extractSelectionRange(context: ModelToDomContext): DOMSelection | null {
function extractSelectionRange(doc: Document, context: ModelToDomContext): DOMSelection | null {
const {
regularSelection: { start, end },
tableSelection,
imageSelection,
} = context;

let startPosition: NodePosition | undefined;
let endPosition: NodePosition | undefined;
let startPosition: { container: Node; offset: number } | undefined;
let endPosition: { container: Node; offset: number } | undefined;

if (imageSelection) {
return imageSelection;
} else if (
(startPosition = start && calcPosition(start)) &&
(endPosition = end && calcPosition(end))
) {
const range = doc.createRange();

range.setStart(startPosition.container, startPosition.offset);
range.setEnd(endPosition.container, endPosition.offset);

return {
type: 'range',
range: createRange(startPosition, endPosition),
range,
};
} else if (tableSelection) {
return tableSelection;
Expand All @@ -66,26 +69,43 @@ function extractSelectionRange(context: ModelToDomContext): DOMSelection | null
}
}

function calcPosition(pos: ModelToDomBlockAndSegmentNode): NodePosition | undefined {
let result: NodePosition | undefined;
function calcPosition(
pos: ModelToDomBlockAndSegmentNode
): { container: Node; offset: number } | undefined {
let result: { container: Node; offset: number } | undefined;

if (pos.block) {
if (!pos.segment) {
result = new Position(pos.block, 0);
result = { container: pos.block, offset: 0 };
} else if (isNodeOfType(pos.segment, 'TEXT_NODE')) {
result = new Position(pos.segment, pos.segment.nodeValue?.length || 0);
} else {
result = new Position(
pos.segment.parentNode!,
toArray(pos.segment.parentNode!.childNodes as NodeListOf<Node>).indexOf(
pos.segment!
) + 1
);
result = { container: pos.segment, offset: pos.segment.nodeValue?.length || 0 };
} else if (pos.segment.parentNode) {
result = {
container: pos.segment.parentNode,
offset:
toArray(pos.segment.parentNode.childNodes as NodeListOf<Node>).indexOf(
pos.segment
) + 1,
};
}
}

if (isNodeOfType(result?.node, 'DOCUMENT_FRAGMENT_NODE')) {
result = result?.normalize();
if (result && isNodeOfType(result.container, 'DOCUMENT_FRAGMENT_NODE')) {
const childNodes = result.container.childNodes;

if (childNodes.length > result.offset) {
result = { container: childNodes[result.offset], offset: 0 };
} else if (result.container.lastChild) {
const container = result.container.lastChild;
result = {
container,
offset: isNodeOfType(container, 'TEXT_NODE')
? container.nodeValue?.length ?? 0
: container.childNodes.length,
};
} else {
result = undefined;
}
}

return result;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { applyFormat } from '../utils/applyFormat';
import { moveChildNodes } from 'roosterjs-editor-dom';
import { isNodeOfType } from '../../domUtils/isNodeOfType';
import { stackFormat } from '../utils/stackFormat';
import { wrapAllChildNodes } from '../../domUtils/moveChildNodes';
import type {
ContentModelSegment,
ContentModelSegmentHandler,
Expand All @@ -18,32 +19,28 @@ export const handleSegmentDecorator: ContentModelSegmentHandler<ContentModelSegm
) => {
const { code, link } = segment;

if (link) {
stackFormat(context, 'a', () => {
const a = document.createElement('a');
if (isNodeOfType(parent, 'ELEMENT_NODE')) {
if (link) {
stackFormat(context, 'a', () => {
const a = wrapAllChildNodes(parent, 'a');

moveChildNodes(a, parent);
parent.appendChild(a);
applyFormat(a, context.formatAppliers.link, link.format, context);
applyFormat(a, context.formatAppliers.dataset, link.dataset, context);

applyFormat(a, context.formatAppliers.link, link.format, context);
applyFormat(a, context.formatAppliers.dataset, link.dataset, context);
segmentNodes?.push(a);
context.onNodeCreated?.(link, a);
});
}

segmentNodes?.push(a);
context.onNodeCreated?.(link, a);
});
}

if (code) {
stackFormat(context, 'code', () => {
const codeNode = document.createElement('code');

moveChildNodes(codeNode, parent);
parent.appendChild(codeNode);
if (code) {
stackFormat(context, 'code', () => {
const codeNode = wrapAllChildNodes(parent, 'code');

applyFormat(codeNode, context.formatAppliers.code, code.format, context);
applyFormat(codeNode, context.formatAppliers.code, code.format, context);

segmentNodes?.push(codeNode);
context.onNodeCreated?.(code, codeNode);
});
segmentNodes?.push(codeNode);
context.onNodeCreated?.(code, codeNode);
});
}
}
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { applyFormat } from '../utils/applyFormat';
import { hasMetadata } from '../../domUtils/metadata/updateMetadata';
import { isBlockEmpty } from '../../modelApi/common/isEmpty';
import { moveChildNodes } from 'roosterjs-editor-dom';
import { moveChildNodes } from '../../domUtils/moveChildNodes';
import { reuseCachedElement } from '../utils/reuseCachedElement';
import type {
ContentModelBlockHandler,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { moveChildNodes, wrapAllChildNodes } from '../../lib/domUtils/moveChildNodes';

describe('moveChildNodes', () => {
function htmlToDom(html: string) {
let element = document.createElement('DIV');
element.innerHTML = html;

return element.firstChild as HTMLElement;
}

function runTest(
targetHtml: string,
sourceHtml: string,
keepExisting: boolean,
expectedTargetHtml: string
) {
const source = htmlToDom(sourceHtml);
const target = htmlToDom(targetHtml);

moveChildNodes(target, source, keepExisting);

const targetResult = target ? target.outerHTML : '';

expect(targetResult).toBe(expectedTargetHtml);
}

it('null input', () => {
runTest(null!, null!, true, '');
runTest(null!, null!, false, '');
});

it('null target input', () => {
runTest(null!, '<div><span>test</span></div>', true, '');
runTest(null!, '<div><span>test</span></div>', false, '');
});

it('null source input', () => {
runTest('<div><span>test</span></div>', null!, true, '<div><span>test</span></div>');
runTest('<div><span>test</span></div>', null!, false, '<div></div>');
});

it('null source input', () => {
runTest('<div><span>test</span></div>', null!, true, '<div><span>test</span></div>');
runTest('<div><span>test</span></div>', null!, false, '<div></div>');
});

it('regular case', () => {
runTest(
'<div><span>test1</span><span>test2</span></div>',
'<div><span>test3</span><span>test4</span></div>',
false,
'<div><span>test3</span><span>test4</span></div>'
);
runTest(
'<div><span>test1</span><span>test2</span></div>',
'<div><span>test3</span><span>test4</span></div>',
true,
'<div><span>test1</span><span>test2</span><span>test3</span><span>test4</span></div>'
);
});
});

describe('wrapAllChildNodes', () => {
it('Single element, no child', () => {
const div = document.createElement('div');
const result = wrapAllChildNodes(div, 'span');

expect(div.innerHTML).toBe('<span></span>');
expect(result).toBe(div.firstChild as HTMLElement);
});

it('Single element, with child nodes', () => {
const div = document.createElement('div');

div.innerHTML = 'test<b>test2</b>test3';
const result = wrapAllChildNodes(div, 'span');

expect(div.innerHTML).toBe('<span>test<b>test2</b>test3</span>');
expect(result).toBe(div.firstChild as HTMLElement);
});
});
Loading

0 comments on commit 9dfda12

Please sign in to comment.