Skip to content

Commit

Permalink
Standalone editor: Remove dependency to DOM utils (#2151)
Browse files Browse the repository at this point in the history
* Standalone editor: TableOperation

* fix build

* Standalone editor: Remove dependency to DOM utils

* fix build
  • Loading branch information
JiuqingSong authored Oct 17, 2023
1 parent c0f5953 commit a7a7853
Show file tree
Hide file tree
Showing 22 changed files with 185 additions and 80 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const CTRL_CHAR_CODE = 'Control';
const ALT_CHAR_CODE = 'Alt';
const META_CHAR_CODE = 'Meta';

/**
* @internal
* Returns true when the event was fired from a modifier key, otherwise false
* @param event The keyboard event object
*/
export function isModifierKey(event: KeyboardEvent): boolean {
const isCtrlKey = event.ctrlKey || event.key === CTRL_CHAR_CODE;
const isAltKey = event.altKey || event.key === ALT_CHAR_CODE;
const isMetaKey = event.metaKey || event.key === META_CHAR_CODE;

return isCtrlKey || isAltKey || isMetaKey;
}

/**
* @internal
* Returns true when the event was fired from a key that produces a character value, otherwise false
* This detection is not 100% accurate. event.key is not fully supported by all browsers, and in some browsers (e.g. IE),
* event.key is longer than 1 for num pad input. But here we just want to improve performance as much as possible.
* So if we missed some case here it is still acceptable.
* @param event The keyboard event object
*/
export function isCharacterValue(event: KeyboardEvent): boolean {
return !isModifierKey(event) && !!event.key && event.key.length == 1;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* @internal
* Read a file object and invoke a callback function with the data url of this file
* @param file The file to read
* @param callback the callback to invoke with data url of the file.
* If fail to read, dataUrl will be null
*/
export function readFile(file: File, callback: (dataUrl: string | null) => void) {
if (file) {
const reader = new FileReader();
reader.onload = () => {
callback(reader.result as string);
};
reader.onerror = () => {
callback(null);
};
reader.readAsDataURL(file);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { EditorBase } from 'roosterjs-editor-core';
import type { ContentModelEditorCore } from '../publicTypes/ContentModelEditorCore';
import type {
ContentModelEditorOptions,
EditorEnvironment,
IContentModelEditor,
} from '../publicTypes/IContentModelEditor';
import type {
Expand Down Expand Up @@ -65,6 +66,13 @@ export default class ContentModelEditor
return core.api.setContentModel(core, model, option, onNodeCreated);
}

/**
* Get current running environment, such as if editor is running on Mac
*/
getEnvironment(): EditorEnvironment {
return this.getCore().environment;
}

/**
* Get current DOM selection
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { areSameRangeEx } from '../../modelApi/selection/areSameRangeEx';
import { isCharacterValue } from 'roosterjs-editor-dom';
import { isCharacterValue } from '../../domUtils/eventUtils';
import { Keys, PluginEventType } from 'roosterjs-editor-types';
import type ContentModelContentChangedEvent from '../../publicTypes/event/ContentModelContentChangedEvent';
import type { ContentModelCachePluginState } from '../../publicTypes/pluginState/ContentModelCachePluginState';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import paste from '../../publicApi/utils/paste';
import { addRangeToSelection, createElement, extractClipboardItems } from 'roosterjs-editor-dom';
import { addRangeToSelection, extractClipboardItems } from 'roosterjs-editor-dom';
import { ChangeSource, ColorTransformDirection, PluginEventType } from 'roosterjs-editor-types';
import { cloneModel } from '../../modelApi/common/cloneModel';
import { DeleteResult } from '../../modelApi/edit/utils/DeleteSelectionStep';
import { deleteSelection } from '../../modelApi/edit/deleteSelection';
Expand All @@ -23,12 +24,6 @@ import type {
PluginWithState,
ClipboardData,
} from 'roosterjs-editor-types';
import {
ChangeSource,
PluginEventType,
KnownCreateElementDataIndex,
ColorTransformDirection,
} from 'roosterjs-editor-types';

/**
* Copy and paste plugin for handling onCopy and onPaste event
Expand Down Expand Up @@ -213,10 +208,16 @@ export default class ContentModelCopyPastePlugin implements PluginWithState<Copy
const div = editor.getCustomData(
'CopyPasteTempDiv',
() => {
const tempDiv = createElement(
KnownCreateElementDataIndex.CopyPasteTempDiv,
editor.getDocument()
) as HTMLDivElement;
const tempDiv = editor.getDocument().createElement('div');

tempDiv.style.width = '600px';
tempDiv.style.height = '1px';
tempDiv.style.overflow = 'hidden';
tempDiv.style.position = 'fixed';
tempDiv.style.top = '0';
tempDiv.style.left = '0';
tempDiv.style.userSelect = 'text';
tempDiv.contentEditable = 'true';

editor.getDocument().body.appendChild(tempDiv);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import applyDefaultFormat from '../../publicApi/format/applyDefaultFormat';
import applyPendingFormat from '../../publicApi/format/applyPendingFormat';
import { canApplyPendingFormat, clearPendingFormat } from '../../modelApi/format/pendingFormat';
import { getObjectKeys } from 'roosterjs-content-model-dom';
import { isCharacterValue } from 'roosterjs-editor-dom';
import { isCharacterValue } from '../../domUtils/eventUtils';
import { Keys, PluginEventType } from 'roosterjs-editor-types';
import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor';
import type { IEditor, PluginEvent, PluginWithState } from 'roosterjs-editor-types';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export const createContentModelEditorCore: CoreCreator<

const core = createEditorCore(contentDiv, modifiedOptions) as ContentModelEditorCore;

core.environment = {};

promoteToContentModelEditorCore(core, modifiedOptions, pluginState);

return core;
Expand All @@ -67,6 +69,7 @@ export function promoteToContentModelEditorCore(
promoteCorePluginState(cmCore, pluginState);
promoteContentModelInfo(cmCore, options);
promoteCoreApi(cmCore);
promoteEnvironment(cmCore);
}

function promoteCorePluginState(
Expand Down Expand Up @@ -115,6 +118,10 @@ function promoteCoreApi(cmCore: ContentModelEditorCore) {
cmCore.originalApi.setDOMSelection = setDOMSelection;
}

function promoteEnvironment(cmCore: ContentModelEditorCore) {
cmCore.environment.isMac = window.navigator.appVersion.indexOf('Mac') != -1;
}

function getPluginState(options: ContentModelEditorOptions): ContentModelPluginState {
const format = options.defaultFormat || {};
return {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { contains } from 'roosterjs-editor-dom';
import { entityProcessor, hasMetadata, tableProcessor } from 'roosterjs-content-model-dom';
import { getSelectionRootNode } from '../../modelApi/selection/getSelectionRootNode';
import type { DomToModelContext, ElementProcessor } from 'roosterjs-content-model-types';
Expand All @@ -13,6 +12,7 @@ export const tablePreProcessor: ElementProcessor<HTMLTableElement> = (group, ele
};

function shouldUseTableProcessor(element: HTMLTableElement, context: DomToModelContext) {
const selectionRoot = getSelectionRootNode(context.selection);
// Treat table as a real table when:
// 1. It is a roosterjs table (has metadata)
// 2. Table is in selection
Expand All @@ -21,6 +21,6 @@ function shouldUseTableProcessor(element: HTMLTableElement, context: DomToModelC
return (
hasMetadata(element) ||
context.isInSelection ||
contains(element, getSelectionRootNode(context.selection), true /*treatSameNodeAsContain*/)
(selectionRoot && element.contains(selectionRoot))
);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import addParser from '../utils/addParser';
import { findClosestElementAncestor, matchesSelector } from 'roosterjs-editor-dom';
import { setProcessor } from '../utils/setProcessor';
import type ContentModelBeforePasteEvent from '../../../../publicTypes/event/ContentModelBeforePasteEvent';
import type {
Expand Down Expand Up @@ -78,7 +77,8 @@ const wacElementProcessor: ElementProcessor<HTMLElement> = (
context: DomToModelContext
): void => {
const elementTag = element.tagName;
if (matchesSelector(element, WAC_IDENTIFY_SELECTOR)) {

if (element.matches(WAC_IDENTIFY_SELECTOR)) {
element.style.removeProperty('display');
element.style.removeProperty('margin');
}
Expand Down Expand Up @@ -187,7 +187,7 @@ function shouldClearListContext(
return (
context.listFormat.levels.length > 0 &&
LIST_ELEMENT_TAGS.every(tag => tag != elementTag) &&
!findClosestElementAncestor(element, undefined, LIST_ELEMENT_SELECTOR)
!element.closest(LIST_ELEMENT_SELECTOR)
);
}

Expand Down Expand Up @@ -232,11 +232,7 @@ const wacListProcessor: ElementProcessor<HTMLOListElement | HTMLUListElement> =
context: DomToModelContext
): void => {
const lastBlock = group.blocks[group.blocks.length - 1];
const isWrappedInContainer = findClosestElementAncestor(
element,
undefined,
`.${LIST_CONTAINER_ELEMENT_CLASS_NAME}`
);
const isWrappedInContainer = element.closest(`.${LIST_CONTAINER_ELEMENT_CLASS_NAME}`);
if (
isWrappedInContainer?.previousElementSibling?.classList.contains(
LIST_CONTAINER_ELEMENT_CLASS_NAME
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ export {
ContentModelContentChangedEventData,
} from './publicTypes/event/ContentModelContentChangedEvent';

export { IContentModelEditor, ContentModelEditorOptions } from './publicTypes/IContentModelEditor';
export {
IContentModelEditor,
ContentModelEditorOptions,
EditorEnvironment,
} from './publicTypes/IContentModelEditor';
export { InsertPoint } from './publicTypes/selection/InsertPoint';
export { TableSelectionContext } from './publicTypes/selection/TableSelectionContext';
export {
Expand Down Expand Up @@ -96,7 +100,6 @@ export { default as toggleCode } from './publicApi/segment/toggleCode';
export { default as paste } from './publicApi/utils/paste';
export { default as insertEntity } from './publicApi/entity/insertEntity';
export { formatWithContentModel } from './publicApi/utils/formatWithContentModel';
export { default as keyboardDelete } from './publicApi/editing/keyboardDelete';

export { default as ContentModelEditor } from './editor/ContentModelEditor';
export { default as isContentModelEditor } from './editor/isContentModelEditor';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Browser, isModifierKey } from 'roosterjs-editor-dom';
import { ChangeSource, Keys } from 'roosterjs-editor-types';
import { deleteAllSegmentBefore } from '../../modelApi/edit/deleteSteps/deleteAllSegmentBefore';
import { DeleteResult } from '../../modelApi/edit/utils/DeleteSelectionStep';
import { deleteSelection } from '../../modelApi/edit/deleteSelection';
import { formatWithContentModel } from '../utils/formatWithContentModel';
import { isModifierKey } from '../../domUtils/eventUtils';
import { isNodeOfType } from 'roosterjs-content-model-dom';
import type { DeleteSelectionStep } from '../../modelApi/edit/utils/DeleteSelectionStep';
import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor';
Expand All @@ -22,6 +22,7 @@ import {
} from '../../modelApi/edit/deleteSteps/deleteCollapsedSelection';

/**
* @internal
* Do keyboard event handling for DELETE/BACKSPACE key
* @param editor The Content Model Editor
* @param rawEvent DOM keyboard event
Expand All @@ -41,8 +42,11 @@ export default function keyboardDelete(
editor,
which == Keys.DELETE ? 'handleDeleteKey' : 'handleBackspaceKey',
(model, context) => {
const result = deleteSelection(model, getDeleteSteps(rawEvent), context)
.deleteResult;
const result = deleteSelection(
model,
getDeleteSteps(rawEvent, !!editor.getEnvironment().isMac),
context
).deleteResult;

isDeleted = result != DeleteResult.NotDeleted;

Expand All @@ -61,11 +65,11 @@ export default function keyboardDelete(
return isDeleted;
}

function getDeleteSteps(rawEvent: KeyboardEvent): (DeleteSelectionStep | null)[] {
function getDeleteSteps(rawEvent: KeyboardEvent, isMac: boolean): (DeleteSelectionStep | null)[] {
const isForward = rawEvent.which == Keys.DELETE;
const deleteAllSegmentBeforeStep =
shouldDeleteAllSegmentsBefore(rawEvent) && !isForward ? deleteAllSegmentBefore : null;
const deleteWordSelection = shouldDeleteWord(rawEvent, !!Browser.isMac)
const deleteWordSelection = shouldDeleteWord(rawEvent, isMac)
? isForward
? forwardDeleteWordSelection
: backwardDeleteWordSelection
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { contains } from 'roosterjs-editor-dom';
import { getPendingFormat } from '../../modelApi/format/pendingFormat';
import { getSelectionRootNode } from '../../modelApi/selection/getSelectionRootNode';
import { retrieveModelFormatState } from '../../modelApi/common/retrieveModelFormatState';
Expand Down Expand Up @@ -96,7 +95,7 @@ function createNodeStack(root: Node, startNode: Node): Node[] {
const result: Node[] = [];
let node: Node | null = startNode;

while (node && contains(root, node)) {
while (node && root != node && root.contains(node)) {
if (isNodeOfType(node, 'ELEMENT_NODE') && node.tagName == 'TABLE') {
// For table, we can't do a reduced model creation since we need to handle their cells and indexes,
// so clean up whatever we already have, and just put table into the stack
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import formatImageWithContentModel from '../utils/formatImageWithContentModel';
import { getMetadata, readFile } from 'roosterjs-editor-dom';
import { PluginEventType } from 'roosterjs-editor-types';
import { readFile } from '../../domUtils/readFile';
import { updateImageMetadata } from '../../domUtils/metadata/updateImageMetadata';
import type { ContentModelImage } from 'roosterjs-content-model-types';
import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor';

Expand All @@ -14,28 +16,23 @@ export default function changeImage(editor: IContentModelEditor, file: File) {
const selection = editor.getDOMSelection();
readFile(file, dataUrl => {
if (dataUrl && !editor.isDisposed() && selection?.type === 'image') {
formatImageWithContentModel(
editor,
'changeImage',
(image: ContentModelImage) => {
image.src = dataUrl;
image.dataset = {};
image.format.width = '';
image.format.height = '';
image.alt = '';
},
{
formatImageWithContentModel(editor, 'changeImage', (image: ContentModelImage) => {
const originalSrc = updateImageMetadata(image)?.src ?? '';
const previousSrc = image.src;

image.src = dataUrl;
image.dataset = {};
image.format.width = '';
image.format.height = '';
image.alt = '';

editor.triggerPluginEvent(PluginEventType.EditImage, {
image: selection.image,
previousSrc: selection.image.src,
previousSrc,
newSrc: dataUrl,
originalSrc: getImageSrc(selection.image),
}
);
originalSrc,
});
});
}
});
}

const getImageSrc = (image: HTMLImageElement) => {
const obj = getMetadata<{ src: string }>(image);
return (obj && obj.src) || '';
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { addSegment, createContentModelDocument, createImage } from 'roosterjs-content-model-dom';
import { formatWithContentModel } from '../utils/formatWithContentModel';
import { mergeModel } from '../../modelApi/common/mergeModel';
import { readFile } from 'roosterjs-editor-dom';
import { readFile } from '../../domUtils/readFile';
import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor';

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { formatSegmentWithContentModel } from './formatSegmentWithContentModel';
import { PluginEventType } from 'roosterjs-editor-types';
import type { ContentModelImage } from 'roosterjs-content-model-types';
import type { EditImageEventData } from 'roosterjs-editor-types';
import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor';

/**
Expand All @@ -10,18 +8,14 @@ import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'
export default function formatImageWithContentModel(
editor: IContentModelEditor,
apiName: string,
callback: (segment: ContentModelImage) => void,
eventChangeData?: EditImageEventData
callback: (segment: ContentModelImage) => void
) {
formatSegmentWithContentModel(
editor,
apiName,
(_, __, segment) => {
if (segment?.segmentType == 'Image') {
callback(segment);
if (eventChangeData) {
editor.triggerPluginEvent(PluginEventType.EditImage, eventChangeData);
}
}
},
undefined /** segmentHasStyleCallback **/,
Expand Down
Loading

0 comments on commit a7a7853

Please sign in to comment.