Skip to content

Commit

Permalink
Improve
Browse files Browse the repository at this point in the history
  • Loading branch information
JiuqingSong committed Jun 10, 2024
1 parent 9443a20 commit 30455ff
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import type {
DomIndexer,
DOMSelection,
RangeSelectionForCache,
ReconcileChildListContext,
Selectable,
} from 'roosterjs-content-model-types';

Expand All @@ -40,6 +39,36 @@ interface IndexedTableElement extends HTMLTableElement {
__roosterjsContentModel: TableItem;
}

/**
* Context object used by DomIndexer when reconcile mutations with child list
*/
interface ReconcileChildListContext {
/**
* Index of segment in current paragraph
*/
segIndex: number;

/**
* The current paragraph that we are handling
*/
paragraph?: ContentModelParagraph;

/**
* Text node that is added from mutation but has not been handled. This can happen when we first see an added node then later we see a removed one.
* e.g. Type text in an empty paragraph (<div><br></div>), so a text node will be added and <BR> will be removed.
* Set to a valid text node means we need to handle it later. If it is finally not handled, that means we need to clear cache
* Set to undefined (initial value) means no pending text node is hit yet (valid case)
* Set to null means there was a pending text node which is already handled, so if we see another pending text node,
* we should clear cache since we don't know how to handle it
*/
pendingTextNode?: Text | null;

/**
* Format of the removed segment, this will be used as the format for newly created segment
*/
format?: ContentModelSegmentFormat;
}

function isIndexedSegment(node: Node): node is IndexedSegmentNode {
const { paragraph, segments } = (node as IndexedSegmentNode).__roosterjsContentModel ?? {};

Expand Down Expand Up @@ -302,12 +331,11 @@ export class DomIndexerImpl implements DomIndexer {
return selectable;
}

reconcileChildList(
addedNodes: ArrayLike<Node>,
removedNodes: ArrayLike<Node>,
context: ReconcileChildListContext
): boolean {
reconcileChildList(addedNodes: ArrayLike<Node>, removedNodes: ArrayLike<Node>): boolean {
let canHandle = true;
const context: ReconcileChildListContext = {
segIndex: -1,
};

// First process added nodes
const addedNode = addedNodes[0];
Expand All @@ -327,7 +355,7 @@ export class DomIndexerImpl implements DomIndexer {
canHandle = false;
}

return canHandle;
return canHandle && !context.pendingTextNode;
}

private reconcileAddedNode(node: Text, context: ReconcileChildListContext): boolean {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type {
ContentModelDocument,
DomIndexer,
ReconcileChildListContext,
TextMutationObserver,
} from 'roosterjs-content-model-types';

Expand Down Expand Up @@ -41,12 +40,12 @@ class TextMutationObserverImpl implements TextMutationObserver {
}

private onMutationInternal = (mutations: MutationRecord[]) => {
const context: ReconcileChildListContext = {
segIndex: -1,
};
let canHandle = true;
let firstTarget: Node | null = null;
let lastTextChangeNode: Node | null = null;
let addedNodes: Node[] = [];
let removedNodes: Node[] = [];
let reconcileText = false;

for (let i = 0; i < mutations.length && canHandle; i++) {
const mutation = mutations[i];
Expand All @@ -65,7 +64,7 @@ class TextMutationObserverImpl implements TextMutationObserver {
canHandle = false;
} else {
lastTextChangeNode = mutation.target;
this.onMutation(true /*textOnly*/);
reconcileText = true;
}
break;

Expand All @@ -77,18 +76,21 @@ class TextMutationObserverImpl implements TextMutationObserver {
}

if (canHandle) {
canHandle = this.domIndexer.reconcileChildList(
mutation.addedNodes,
mutation.removedNodes,
context
);
addedNodes = addedNodes.concat(Array.from(mutation.addedNodes));
removedNodes = removedNodes.concat(Array.from(mutation.removedNodes));
}

break;
}
}

if (!canHandle || context.pendingTextNode) {
if (canHandle && (addedNodes.length > 0 || removedNodes.length > 0)) {
canHandle = this.domIndexer.reconcileChildList(addedNodes, removedNodes);
}

if (canHandle && reconcileText) {
this.onMutation(true /*textOnly*/);
} else if (!canHandle) {
this.onMutation(false /*textOnly*/);
}
};
Expand Down
16 changes: 10 additions & 6 deletions packages/roosterjs-content-model-types/lib/context/DomIndexer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { ReconcileChildListContext } from '../parameter/ReconcileChildListContext';
import type { CacheSelection } from '../pluginState/CachePluginState';
import type { ContentModelDocument } from '../contentModel/blockGroup/ContentModelDocument';
import type { ContentModelParagraph } from '../contentModel/block/ContentModelParagraph';
Expand Down Expand Up @@ -49,9 +48,14 @@ export interface DomIndexer {
oldSelection?: CacheSelection
) => boolean;

reconcileChildList: (
addedNodes: ArrayLike<Node>,
removedNodes: ArrayLike<Node>,
context: ReconcileChildListContext
) => boolean;
/**
* When child list of editor content is changed, we can use this method to do sync the change from editor into content model.
* This is mostly used when user start to type in an empty line. In that case browser will remove the existing BR node in the empty line if any,
* and create a new TEXT node for the typed text. Here we use these information to remove original Br segment and create a new Text segment
* in content model. But if we find anything that cannot be handled, return false so caller will invalidate the cached model
* @param addedNodes Nodes added by browser during mutation
* @param removedNodes Nodes removed by browser during mutation
* @returns True if the changed nodes are successfully reconciled, otherwise false
*/
reconcileChildList: (addedNodes: ArrayLike<Node>, removedNodes: ArrayLike<Node>) => boolean;
}
1 change: 0 additions & 1 deletion packages/roosterjs-content-model-types/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,6 @@ export { NodeTypeMap } from './parameter/NodeTypeMap';
export { TypeOfBlockGroup } from './parameter/TypeOfBlockGroup';
export { OperationalBlocks, ReadonlyOperationalBlocks } from './parameter/OperationalBlocks';
export { ParsedTable, ParsedTableCell } from './parameter/ParsedTable';
export { ReconcileChildListContext } from './parameter/ReconcileChildListContext';
export {
ModelToTextCallback,
ModelToTextCallbacks,
Expand Down

This file was deleted.

0 comments on commit 30455ff

Please sign in to comment.