Skip to content

Commit

Permalink
Merge branch 'master' into u/jisong/colorkey
Browse files Browse the repository at this point in the history
  • Loading branch information
JiuqingSong committed Jun 6, 2024
2 parents a6f9fb5 + 48855d1 commit c3a77ab
Show file tree
Hide file tree
Showing 20 changed files with 2,446 additions and 456 deletions.
9 changes: 9 additions & 0 deletions packages/roosterjs-content-model-core/lib/editor/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type {
CachedElementHandler,
DomToModelOptionForCreateModel,
AnnounceData,
ExperimentalFeature,
} from 'roosterjs-content-model-types';

/**
Expand Down Expand Up @@ -406,6 +407,14 @@ export class Editor implements IEditor {
core.api.announce(core, announceData);
}

/**
* Check if a given feature is enabled
* @param featureName The name of feature to check
*/
isExperimentalFeatureEnabled(featureName: ExperimentalFeature | string): boolean {
return this.getCore().experimentalFeatures.indexOf(featureName) >= 0;
}

/**
* @returns the current EditorCore object
* @throws a standard Error if there's no core object
Expand Down
32 changes: 32 additions & 0 deletions packages/roosterjs-content-model-core/test/editor/EditorTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1116,4 +1116,36 @@ describe('Editor', () => {
expect(resetSpy).toHaveBeenCalledWith();
expect(() => editor.announce(mockedData)).toThrow();
});

it('isExperimentalFeatureEnabled', () => {
const div = document.createElement('div');
const resetSpy = jasmine.createSpy('reset');
const mockedCore = {
plugins: [],
darkColorHandler: {
updateKnownColor: updateKnownColorSpy,
reset: resetSpy,
},
api: {
setContentModel: setContentModelSpy,
},
experimentalFeatures: ['Feature1', 'Feature2'],
} as any;

createEditorCoreSpy.and.returnValue(mockedCore);

const editor = new Editor(div);

const result1 = editor.isExperimentalFeatureEnabled('Feature1');
const result2 = editor.isExperimentalFeatureEnabled('Feature2');
const result3 = editor.isExperimentalFeatureEnabled('Feature3');

expect(result1).toBeTrue();
expect(result2).toBeTrue();
expect(result3).toBeFalse();

editor.dispose();
expect(resetSpy).toHaveBeenCalledWith();
expect(() => editor.isExperimentalFeatureEnabled('Feature4')).toThrow();
});
});
4 changes: 2 additions & 2 deletions packages/roosterjs-content-model-dom/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,6 @@ export { cacheGetEventData } from './domUtils/event/cacheGetEventData';

export { isBlockGroupOfType } from './modelApi/typeCheck/isBlockGroupOfType';

export { getClosestAncestorBlockGroupIndex } from './modelApi/editing/getClosestAncestorBlockGroupIndex';

export { iterateSelections } from './modelApi/selection/iterateSelections';
export {
getFirstSelectedListItem,
Expand Down Expand Up @@ -142,6 +140,8 @@ export { setTableCellBackgroundColor } from './modelApi/editing/setTableCellBack
export { retrieveModelFormatState } from './modelApi/editing/retrieveModelFormatState';
export { getListStyleTypeFromString } from './modelApi/editing/getListStyleTypeFromString';
export { getSegmentTextFormat } from './modelApi/editing/getSegmentTextFormat';
export { getClosestAncestorBlockGroupIndex } from './modelApi/editing/getClosestAncestorBlockGroupIndex';
export { runEditSteps } from './modelApi/editing/runEditSteps';

export { updateImageMetadata, getImageMetadata } from './modelApi/metadata/updateImageMetadata';
export {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { deleteExpandedSelection } from './deleteExpandedSelection';
import { mutateBlock } from '../common/mutate';
import { runEditSteps } from './runEditSteps';
import type {
DeleteSelectionContext,
DeleteSelectionResult,
DeleteSelectionStep,
FormatContentModelContext,
ReadonlyContentModelDocument,
ValidDeleteSelectionContext,
} from 'roosterjs-content-model-types';

/**
Expand All @@ -22,23 +22,16 @@ export function deleteSelection(
formatContext?: FormatContentModelContext
): DeleteSelectionResult {
const context = deleteExpandedSelection(model, formatContext);
const steps = additionalSteps.filter(
(x: DeleteSelectionStep | null): x is DeleteSelectionStep => !!x
);

additionalSteps.forEach(step => {
if (step && isValidDeleteSelectionContext(context)) {
step(context);
}
});
runEditSteps(steps, context);

mergeParagraphAfterDelete(context);
return context;
}

function isValidDeleteSelectionContext(
context: DeleteSelectionContext
): context is ValidDeleteSelectionContext {
return !!context.insertPoint;
}

// If we end up with multiple paragraphs impacted, we need to merge them
function mergeParagraphAfterDelete(context: DeleteSelectionContext) {
const { insertPoint, deleteResult, lastParagraph, lastTableContext } = context;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type {
DeleteSelectionContext,
DeleteSelectionResult,
DeleteSelectionStep,
ValidDeleteSelectionContext,
} from 'roosterjs-content-model-types';

/**
* Run editing steps on top of a given context object which includes current insert point and previous editing result
* @param steps The editing steps to run
* @param context Context for the editing steps.
*/
export function runEditSteps(steps: DeleteSelectionStep[], context: DeleteSelectionResult) {
steps.forEach(step => {
if (step && isValidDeleteSelectionContext(context)) {
step(context);
}
});
}

function isValidDeleteSelectionContext(
context: DeleteSelectionContext
): context is ValidDeleteSelectionContext {
return !!context.insertPoint;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { keyboardDelete } from './keyboardDelete';
import { keyboardEnter } from './keyboardEnter';
import { keyboardInput } from './keyboardInput';
import { keyboardTab } from './keyboardTab';
import { parseTableCells } from 'roosterjs-content-model-dom';
Expand All @@ -25,6 +26,7 @@ export class EditPlugin implements EditorPlugin {
private disposer: (() => void) | null = null;
private shouldHandleNextInputEvent = false;
private selectionAfterDelete: DOMSelection | null = null;
private handleNormalEnter = false;

/**
* Get name of this plugin
Expand All @@ -41,6 +43,8 @@ export class EditPlugin implements EditorPlugin {
*/
initialize(editor: IEditor) {
this.editor = editor;
this.handleNormalEnter = this.editor.isExperimentalFeatureEnabled('PersistCache');

if (editor.getEnvironment().isAndroid) {
this.disposer = this.editor.attachDomEvent({
beforeinput: {
Expand Down Expand Up @@ -153,6 +157,9 @@ export class EditPlugin implements EditorPlugin {
break;

case 'Enter':
keyboardEnter(editor, rawEvent, this.handleNormalEnter);
break;

default:
keyboardInput(editor, rawEvent);
break;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,88 +1,103 @@
import {
createParagraph,
createSelectionMarker,
unwrapBlock,
getClosestAncestorBlockGroupIndex,
isBlockGroupOfType,
createFormatContainer,
mutateBlock,
} from 'roosterjs-content-model-dom';
import type {
ContentModelFormatContainer,
DeleteSelectionStep,
ReadonlyContentModelBlockGroup,
ReadonlyContentModelFormatContainer,
ReadonlyContentModelParagraph,
ShallowMutableContentModelFormatContainer,
ShallowMutableContentModelParagraph,
} from 'roosterjs-content-model-types';

/**
* @internal
*/
export const deleteEmptyQuote: DeleteSelectionStep = context => {
const { deleteResult } = context;

if (
deleteResult == 'nothingToDelete' ||
deleteResult == 'notDeleted' ||
deleteResult == 'range'
) {
const { insertPoint, formatContext } = context;
const { path } = insertPoint;
const { path, paragraph } = insertPoint;
const rawEvent = formatContext?.rawEvent as KeyboardEvent;
const index = getClosestAncestorBlockGroupIndex(
path,
['FormatContainer', 'ListItem'],
['TableCell']
['FormatContainer'],
['TableCell', 'ListItem']
);
const quote = path[index];

if (quote && quote.blockGroupType === 'FormatContainer' && quote.tagName == 'blockquote') {
const parent = path[index + 1];
const quoteBlockIndex = parent.blocks.indexOf(quote);
const blockQuote = parent.blocks[quoteBlockIndex];
if (
isBlockGroupOfType<ContentModelFormatContainer>(blockQuote, 'FormatContainer') &&
blockQuote.tagName === 'blockquote'

if (isEmptyQuote(quote)) {
unwrapBlock(parent, quote);
rawEvent?.preventDefault();
context.deleteResult = 'range';
} else if (
rawEvent?.key === 'Enter' &&
quote.blocks.indexOf(paragraph) >= 0 &&
isEmptyParagraph(paragraph)
) {
if (isEmptyQuote(blockQuote)) {
unwrapBlock(parent, blockQuote);
rawEvent?.preventDefault();
context.deleteResult = 'range';
} else if (isSelectionOnEmptyLine(blockQuote) && rawEvent?.key === 'Enter') {
insertNewLine(blockQuote, parent, quoteBlockIndex);
rawEvent?.preventDefault();
context.deleteResult = 'range';
}
insertNewLine(mutateBlock(quote), parent, quoteBlockIndex, paragraph);
rawEvent?.preventDefault();
context.deleteResult = 'range';
}
}
}
};

const isEmptyQuote = (quote: ContentModelFormatContainer) => {
const isEmptyQuote = (quote: ReadonlyContentModelFormatContainer) => {
return (
quote.blocks.length === 1 &&
quote.blocks[0].blockType === 'Paragraph' &&
quote.blocks[0].segments.every(
s => s.segmentType === 'SelectionMarker' || s.segmentType === 'Br'
)
isEmptyParagraph(quote.blocks[0])
);
};

const isSelectionOnEmptyLine = (quote: ContentModelFormatContainer) => {
const quoteLength = quote.blocks.length;
const lastParagraph = quote.blocks[quoteLength - 1];
if (lastParagraph && lastParagraph.blockType === 'Paragraph') {
return lastParagraph.segments.every(
s => s.segmentType === 'SelectionMarker' || s.segmentType === 'Br'
);
}
const isEmptyParagraph = (paragraph: ReadonlyContentModelParagraph) => {
return paragraph.segments.every(
s => s.segmentType === 'SelectionMarker' || s.segmentType === 'Br'
);
};

const insertNewLine = (
quote: ContentModelFormatContainer,
quote: ShallowMutableContentModelFormatContainer,
parent: ReadonlyContentModelBlockGroup,
index: number
quoteIndex: number,
paragraph: ShallowMutableContentModelParagraph
) => {
const quoteLength = quote.blocks.length;
mutateBlock(quote).blocks.splice(quoteLength - 1, 1);
const marker = createSelectionMarker();
const newParagraph = createParagraph(false /* isImplicit */);
newParagraph.segments.push(marker);
mutateBlock(parent).blocks.splice(index + 1, 0, newParagraph);
const paraIndex = quote.blocks.indexOf(paragraph);

if (paraIndex >= 0) {
const mutableParent = mutateBlock(parent);

if (paraIndex < quote.blocks.length - 1) {
const newQuote: ShallowMutableContentModelFormatContainer = createFormatContainer(
quote.tagName,
quote.format
);

newQuote.blocks.push(
...quote.blocks.splice(paraIndex + 1, quote.blocks.length - paraIndex - 1)
);

mutableParent.blocks.splice(quoteIndex + 1, 0, newQuote);
}

mutableParent.blocks.splice(quoteIndex + 1, 0, paragraph);
quote.blocks.splice(paraIndex, 1);

if (quote.blocks.length == 0) {
mutableParent.blocks.splice(quoteIndex, 0);
}
}
};
Loading

0 comments on commit c3a77ab

Please sign in to comment.