Skip to content

Commit

Permalink
Add background to maintain selection highlight
Browse files Browse the repository at this point in the history
  • Loading branch information
JiuqingSong authored and francismengMS committed Jun 21, 2024
1 parent be7889d commit 3e9397d
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const formatContentModel: FormatContentModel = (
rawEvent,
selectionOverride,
scrollCaretIntoView: scroll,
shouldMaintainSelection,
} = options || {};
const model = core.api.createContentModel(core, domToModelOptions, selectionOverride);
const context: FormatContentModelContext = {
Expand Down Expand Up @@ -58,12 +59,16 @@ export const formatContentModel: FormatContentModel = (

try {
handleImages(core, context);

selection =
core.api.setContentModel(
core,
model,
hasFocus ? undefined : { ignoreSelection: true }, // If editor did not have focus before format, do not set focus after format
hasFocus || shouldMaintainSelection
? {
ignoreSelection: hasFocus, // ..... (move the comment here)
shouldMaintainSelection,
}
: undefined,
onNodeCreated
) ?? undefined;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { DOMSelection, EditorCore } from 'roosterjs-content-model-types';

const SelectionClassName = '__persistedSelection';
/**
* @internal
* Shim class to pass TS interpreter if the TS version does not have context of Highlight API
*/
declare class Highlight {
constructor(textRange: Range);
}

/**
* @internal
* Shim interface to pass TS interpreter if the TS version does not have context of Highlight API
*/
interface WindowWithHighlight extends Window {
Highlight: typeof Highlight;
}

/**
* @internal
* Shim class for HighlightRegistry to pass TS interpreter
*/
interface HighlightRegistryWithMap extends HighlightRegistry {
set(name: string, highlight: Highlight): void;
delete(name: string): void;
}

interface HighlightRegistry {}

interface CSSShim {
highlights: HighlightRegistry;
}

declare const CSS: CSSShim;

/**
* @internal
* @param win current window that Highlight is being used.
* @returns boolean indicates if Highlight api is available
*/
function isHighlightRegistryWithMap(
highlight: HighlightRegistry
): highlight is HighlightRegistryWithMap {
return !!(highlight as HighlightRegistryWithMap).set;
}

/**
* @internal
* @param win current window that Highlight is being used.
* @returns boolean indicates if Highlight api is available
*/
export function isWindowWithHighlight(win: Window): win is WindowWithHighlight {
return !!(win as WindowWithHighlight).Highlight;
}

/**
* @internal
* Persist highlight of a indicated selection object
* @param core The editor core object
* @param shouldMaintainSelection The flag indicate if the selection should be persisted
* @param selection The selection object that needs to be persisted.
*/
export function persistHighlight(
core: EditorCore,
shouldMaintainSelection: boolean,
selection: DOMSelection | null
) {
const currentWindow = core.logicalRoot.ownerDocument.defaultView;

if (
currentWindow &&
isWindowWithHighlight(currentWindow) &&
isHighlightRegistryWithMap(CSS.highlights)
) {
if (shouldMaintainSelection) {
if (selection && selection.type == 'range') {
const highlight = new currentWindow.Highlight(selection.range);
CSS.highlights.set(SelectionClassName, highlight);
}
} else {
CSS.highlights.delete(SelectionClassName);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { updateCache } from '../../corePlugin/cache/updateCache';
import { isWindowWithHighlight, persistHighlight } from './persistHighlight';
import {
contentModelToDom,
createModelToDomContext,
createModelToDomContextWithConfig,
} from 'roosterjs-content-model-dom';
import type { SetContentModel } from 'roosterjs-content-model-types';

const SelectionClassName = '__persistedSelection';
const SelectionSelector = '.' + SelectionClassName;
/**
* @internal
* Set content with content model
Expand All @@ -15,6 +18,18 @@ import type { SetContentModel } from 'roosterjs-content-model-types';
*/
export const setContentModel: SetContentModel = (core, model, option, onNodeCreated) => {
const editorContext = core.api.createEditorContext(core, true /*saveIndex*/);
const currentWindow = core.logicalRoot.ownerDocument.defaultView;
if (currentWindow && isWindowWithHighlight(currentWindow)) {
if (option?.shouldMaintainSelection) {
editorContext.selectionClassName = SelectionClassName;
core.api.setEditorStyle(core, SelectionClassName, 'background-color: #ddd!important', [
SelectionSelector,
]);
} else {
core.api.setEditorStyle(core, SelectionClassName, null /*rule*/);
}
}

const modelToDomContext = option
? createModelToDomContext(
editorContext,
Expand All @@ -36,6 +51,8 @@ export const setContentModel: SetContentModel = (core, model, option, onNodeCrea
modelToDomContext
);

persistHighlight(core, !!option?.shouldMaintainSelection, selection);

if (!core.lifecycle.shadowEditFragment) {
// Clear pending mutations since we will use our latest model object to replace existing cache
core.cache.textMutationObserver?.flushMutations(true /*ignoreMutations*/);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,9 @@ export function handleSegmentCommon(

applyFormat(containerNode, context.formatAppliers.elementBasedSegment, segment.format, context);

if (segment.isSelected && context.selectionClassName) {
containerNode.className = context.selectionClassName;
}

context.onNodeCreated?.(segment, segmentNode);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext';
import { createText } from '../../../lib/modelApi/creators/createText';
import { expectHtml } from '../../testUtils';
import { handleSegmentCommon } from '../../../lib/modelToDom/utils/handleSegmentCommon';

describe('handleSegmentCommon', () => {
Expand Down Expand Up @@ -60,4 +61,42 @@ describe('handleSegmentCommon', () => {
expect(segmentNodes.length).toBe(1);
expect(segmentNodes[0]).toBe(parent);
});

it('selected text', () => {
const txt = document.createTextNode('test');
const container = document.createElement('span');
const segment = createText('test', {
textColor: 'red',
fontSize: '10pt',
lineHeight: '2',
fontWeight: 'bold',
});
const onNodeCreated = jasmine.createSpy('onNodeCreated');
const context = createModelToDomContext();

segment.isSelected = true;
context.onNodeCreated = onNodeCreated;
context.selectionClassName = 'test';

segment.link = {
dataset: {},
format: {
href: 'href',
},
};
container.appendChild(txt);
const segmentNodes: Node[] = [];

handleSegmentCommon(document, txt, container, segment, context, segmentNodes);

expect(context.regularSelection.current.segment).toBe(txt);
expectHtml(container.outerHTML, [
'<span class="test" style="font-size: 10pt; color: red; line-height: 2;"><b><a href="href">test</a></b></span>',
'<span style="font-size: 10pt; color: red; line-height: 2;" class="test"><b><a href="href">test</a></b></span>',
]);
expect(onNodeCreated).toHaveBeenCalledWith(segment, txt);
expect(segmentNodes.length).toBe(2);
expect(segmentNodes[0]).toBe(txt);
expect(segmentNodes[1]).toBe(txt.parentNode!);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,9 @@ export interface EditorContext {
* Enabled experimental features
*/
experimentalFeatures?: ReadonlyArray<string>;

/**
* Optional parameter that indicate the customized classes to be applied on selection block.
*/
selectionClassName?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,9 @@ export interface ModelToDomOption {
* When set to true, selection from content model will not be applied
*/
ignoreSelection?: boolean;

/**
* When set to true, selection will be maintained on text even if cursor has moved away from editor.
*/
shouldMaintainSelection?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ export interface FormatContentModelOptions {
* When pass to true, scroll the editing caret into view after write DOM tree if need
*/
scrollCaretIntoView?: boolean;

/**
* When pass to true, selection is maintained even when focus is moved out of editor.
*/
shouldMaintainSelection?: boolean;
}

/**
Expand Down

0 comments on commit 3e9397d

Please sign in to comment.