Skip to content

Commit

Permalink
Preserve reverted selection info in Content Model (#2580)
Browse files Browse the repository at this point in the history
* Preserve reverted selection info in Content Model

* improve
  • Loading branch information
JiuqingSong authored Apr 17, 2024
1 parent 74bc863 commit a3e1508
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,41 @@ import { BlockGroupContentView } from './BlockGroupContentView';
import { ContentModelDocument } from 'roosterjs-content-model-types';
import { ContentModelView } from '../ContentModelView';
import { hasSelectionInBlockGroup } from 'roosterjs-content-model-dom';
import { SegmentFormatView } from '../format/SegmentFormatView';
import { useProperty } from '../../hooks/useProperty';

const styles = require('./ContentModelDocumentView.scss');

export function ContentModelDocumentView(props: { doc: ContentModelDocument }) {
const { doc } = props;
const [isReverted, setIsReverted] = useProperty(!!doc.hasRevertedRangeSelection);
const revertedCheckbox = React.useRef<HTMLInputElement>(null);
const onIsRevertedChange = React.useCallback(() => {
const newValue = revertedCheckbox.current.checked;
doc.hasRevertedRangeSelection = newValue;
setIsReverted(newValue);
}, [doc, setIsReverted]);

const getContent = React.useCallback(() => {
return <BlockGroupContentView group={doc} />;
}, [doc]);
return (
<>
<div>
<input
type="checkbox"
checked={isReverted}
ref={revertedCheckbox}
onChange={onIsRevertedChange}
/>
Reverted range selection
</div>
<BlockGroupContentView group={doc} />
</>
);
}, [doc, isReverted]);

const getFormat = React.useCallback(() => {
return doc.format ? <SegmentFormatView format={doc.format} /> : null;
}, [doc.format]);

return (
<ContentModelView
Expand All @@ -19,6 +46,7 @@ export function ContentModelDocumentView(props: { doc: ContentModelDocument }) {
hasSelection={hasSelectionInBlockGroup(doc)}
jsonSource={doc}
getContent={getContent}
getFormat={getFormat}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,18 @@ function reconcileSelection(
collapsed,
} = newRange;

delete model.hasRevertedRangeSelection;

if (collapsed) {
return !!reconcileNodeSelection(startContainer, startOffset);
} else if (
startContainer == endContainer &&
isNodeOfType(startContainer, 'TEXT_NODE')
) {
if (newSelection.isReverted) {
model.hasRevertedRangeSelection = true;
}

return (
isIndexedSegment(startContainer) &&
!!reconcileTextSelection(startContainer, startOffset, endOffset)
Expand All @@ -142,6 +148,10 @@ function reconcileSelection(
const marker2 = reconcileNodeSelection(endContainer, endOffset);

if (marker1 && marker2) {
if (newSelection.isReverted) {
model.hasRevertedRangeSelection = true;
}

setSelection(model, marker1, marker2);
return true;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ describe('domIndexerImpl.reconcileSelection', () => {

expect(result).toBeFalse();
expect(setSelectionSpy).not.toHaveBeenCalled();
expect(model.hasRevertedRangeSelection).toBeFalsy();
});

it('no old range, normal range on non-indexed text, collapsed', () => {
Expand All @@ -208,6 +209,7 @@ describe('domIndexerImpl.reconcileSelection', () => {

expect(result).toBeFalse();
expect(setSelectionSpy).not.toHaveBeenCalled();
expect(model.hasRevertedRangeSelection).toBeFalsy();
});

it('no old range, normal range on indexed text, collapsed', () => {
Expand Down Expand Up @@ -255,6 +257,7 @@ describe('domIndexerImpl.reconcileSelection', () => {
],
});
expect(setSelectionSpy).not.toHaveBeenCalled();
expect(model.hasRevertedRangeSelection).toBeFalsy();
});

it('no old range, normal range on indexed text, expanded on same node', () => {
Expand Down Expand Up @@ -300,6 +303,53 @@ describe('domIndexerImpl.reconcileSelection', () => {
segments: [segment1, segment2, segment3],
});
expect(setSelectionSpy).not.toHaveBeenCalled();
expect(model.hasRevertedRangeSelection).toBeFalsy();
});

it('no old range, normal range on indexed text, expanded on same node, reverted', () => {
const node = document.createTextNode('test') as any;
const newRangeEx: DOMSelection = {
type: 'range',
range: createRange(node, 1, node, 3),
isReverted: true,
};
const paragraph = createParagraph();
const segment = createText('');

paragraph.segments.push(segment);
domIndexerImpl.onSegment(node, paragraph, [segment]);

const result = domIndexerImpl.reconcileSelection(model, newRangeEx);

const segment1: ContentModelSegment = {
segmentType: 'Text',
text: 't',
format: {},
};
const segment2: ContentModelSegment = {
segmentType: 'Text',
text: 'es',
format: {},
isSelected: true,
};
const segment3: ContentModelSegment = {
segmentType: 'Text',
text: 't',
format: {},
};

expect(result).toBeTrue();
expect(node.__roosterjsContentModel).toEqual({
paragraph,
segments: [segment1, segment2, segment3],
});
expect(paragraph).toEqual({
blockType: 'Paragraph',
format: {},
segments: [segment1, segment2, segment3],
});
expect(setSelectionSpy).not.toHaveBeenCalled();
expect(model.hasRevertedRangeSelection).toBeTrue();
});

it('no old range, normal range on indexed text, expanded on different node', () => {
Expand Down Expand Up @@ -370,6 +420,7 @@ describe('domIndexerImpl.reconcileSelection', () => {
blockGroupType: 'Document',
blocks: [paragraph],
});
expect(model.hasRevertedRangeSelection).toBeFalsy();
});

it('no old range, normal range on indexed text, expanded on other type of node', () => {
Expand Down Expand Up @@ -430,6 +481,7 @@ describe('domIndexerImpl.reconcileSelection', () => {
blockGroupType: 'Document',
blocks: [paragraph],
});
expect(model.hasRevertedRangeSelection).toBeFalsy();
});

it('no old range, image range on indexed text', () => {
Expand Down Expand Up @@ -472,6 +524,7 @@ describe('domIndexerImpl.reconcileSelection', () => {
format: {},
dataset: {},
});
expect(model.hasRevertedRangeSelection).toBeFalsy();
});

it('no old range, table range on indexed text', () => {
Expand Down Expand Up @@ -516,6 +569,7 @@ describe('domIndexerImpl.reconcileSelection', () => {
blockGroupType: 'Document',
blocks: [tableModel],
});
expect(model.hasRevertedRangeSelection).toBeFalsy();
});

it('no old range, collapsed range after last node', () => {
Expand Down Expand Up @@ -548,6 +602,7 @@ describe('domIndexerImpl.reconcileSelection', () => {
segments: [segment, createSelectionMarker({ fontFamily: 'Arial' })],
});
expect(setSelectionSpy).not.toHaveBeenCalled();
expect(model.hasRevertedRangeSelection).toBeFalsy();
});

it('has old range - collapsed, expanded new range', () => {
Expand Down Expand Up @@ -606,6 +661,7 @@ describe('domIndexerImpl.reconcileSelection', () => {
segments: [segment1, segment2, segment3],
});
expect(setSelectionSpy).not.toHaveBeenCalled();
expect(model.hasRevertedRangeSelection).toBeFalsy();
});

it('has old range - expanded, expanded new range', () => {
Expand Down Expand Up @@ -664,5 +720,6 @@ describe('domIndexerImpl.reconcileSelection', () => {
segments: [segment1, createSelectionMarker(), segment2],
});
expect(setSelectionSpy).toHaveBeenCalled();
expect(model.hasRevertedRangeSelection).toBeFalsy();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export function domToContentModel(
): ContentModelDocument {
const model = createContentModelDocument(context.defaultFormat);

if (context.selection?.type == 'range' && context.selection.isReverted) {
model.hasRevertedRangeSelection = true;
}

context.elementProcessors.child(model, root, context);

normalizeContentModel(model);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ export function contentModelToDom(

const range = extractSelectionRange(doc, context);

if (model.hasRevertedRangeSelection && range?.type == 'range') {
range.isReverted = true;
}

root.normalize();

return range;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as normalizeContentModel from '../../lib/modelApi/common/normalizeContentModel';
import { domToContentModel } from '../../lib/domToModel/domToContentModel';
import { ContentModelDocument, DomToModelContext } from 'roosterjs-content-model-types';
import { domToContentModel } from '../../lib/domToModel/domToContentModel';

describe('domToContentModel', () => {
it('Not include root', () => {
Expand Down Expand Up @@ -38,4 +38,47 @@ describe('domToContentModel', () => {
expect(normalizeContentModel.normalizeContentModel).toHaveBeenCalledTimes(1);
expect(normalizeContentModel.normalizeContentModel).toHaveBeenCalledWith(result);
});

it('With reverted selection', () => {
const elementProcessor = jasmine.createSpy('elementProcessor');
const childProcessor = jasmine.createSpy('childProcessor');
const mockedRange = 'RANGE' as any;
const mockContext: DomToModelContext = {
elementProcessors: {
element: elementProcessor,
child: childProcessor,
},
defaultStyles: {},
segmentFormat: {},
isDarkMode: false,
defaultFormat: {
fontSize: '10pt',
},
selection: {
type: 'range',
range: mockedRange,
isReverted: true,
},
} as any;

spyOn(normalizeContentModel, 'normalizeContentModel');

const rootElement = document.createElement('div');
const model = domToContentModel(rootElement, mockContext);
const result: ContentModelDocument = {
blockGroupType: 'Document',
blocks: [],
format: {
fontSize: '10pt',
},
hasRevertedRangeSelection: true,
};

expect(model).toEqual(result);
expect(elementProcessor).not.toHaveBeenCalled();
expect(childProcessor).toHaveBeenCalledTimes(1);
expect(childProcessor).toHaveBeenCalledWith(result, rootElement, mockContext);
expect(normalizeContentModel.normalizeContentModel).toHaveBeenCalledTimes(1);
expect(normalizeContentModel.normalizeContentModel).toHaveBeenCalledWith(result);
});
});
Loading

0 comments on commit a3e1508

Please sign in to comment.