Skip to content

Commit

Permalink
Standalone Editor: CreateStandaloneEditorCore (#2218)
Browse files Browse the repository at this point in the history
  • Loading branch information
JiuqingSong authored Nov 20, 2023
1 parent 700e8e8 commit faae180
Show file tree
Hide file tree
Showing 27 changed files with 902 additions and 159 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ function handleDeletedEntities(core: StandaloneEditorCore, context: FormatWithCo

function handleImages(core: StandaloneEditorCore, context: FormatWithContentModelContext) {
if (context.newImages.length > 0) {
const viewport = core.getVisibleViewport();
const viewport = core.api.getVisibleViewport(core);

if (viewport) {
const { left, right } = viewport;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { Rect } from 'roosterjs-editor-types';
import type { GetVisibleViewport } from 'roosterjs-content-model-types';

/**
* @internal
* Retrieves the rect of the visible viewport of the editor.
* @param core The StandaloneEditorCore object
*/
export const getVisibleViewport: GetVisibleViewport = core => {
const scrollContainer = core.domEvent.scrollContainer;

return getIntersectedRect(
scrollContainer == core.contentDiv ? [scrollContainer] : [scrollContainer, core.contentDiv]
);
};

/**
* Get the intersected Rect of elements provided
*
* @example
* The result of the following Elements Rects would be:
{
top: Element2.top,
bottom: Element1.bottom,
left: Element2.left,
right: Element2.right
}
+-------------------------+
| Element 1 |
| +-----------------+ |
| | Element2 | |
| | | |
| | | |
+-------------------------+
| |
+-----------------+
* @param elements Elements to use.
* @param additionalRects additional rects to use
* @returns If the Rect is valid return the rect, if not, return null.
*/
function getIntersectedRect(elements: HTMLElement[], additionalRects: Rect[] = []): Rect | null {
const rects = elements
.map(element => normalizeRect(element.getBoundingClientRect()))
.concat(additionalRects)
.filter((rect: Rect | null): rect is Rect => !!rect);

const result: Rect = {
top: Math.max(...rects.map(r => r.top)),
bottom: Math.min(...rects.map(r => r.bottom)),
left: Math.max(...rects.map(r => r.left)),
right: Math.min(...rects.map(r => r.right)),
};

return result.top < result.bottom && result.left < result.right ? result : null;
}

function normalizeRect(clientRect: DOMRect): Rect | null {
const { left, right, top, bottom } =
clientRect || <DOMRect>{ left: 0, right: 0, top: 0, bottom: 0 };
return left === 0 && right === 0 && top === 0 && bottom === 0
? null
: {
left: Math.round(left),
right: Math.round(right),
top: Math.round(top),
bottom: Math.round(bottom),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
} from 'roosterjs-content-model-types';

/**
* @internal
* Create core plugins for standalone editor
* @param options Options of editor
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import * as Color from 'color';
import { getObjectKeys, parseColor, setColor } from 'roosterjs-editor-dom';
import type {
ColorKeyAndValue,
DarkColorHandler,
ModeIndependentColor,
} from 'roosterjs-editor-types';

const DefaultLightness = 21.25; // Lightness for #333333
const VARIABLE_REGEX = /^\s*var\(\s*(\-\-[a-zA-Z0-9\-_]+)\s*(?:,\s*(.*))?\)\s*$/;
const VARIABLE_PREFIX = 'var(';
const COLOR_VAR_PREFIX = 'darkColor';
Expand All @@ -28,8 +30,11 @@ const ColorAttributeName: { [key in ColorAttributeEnum]: string }[] = [
*/
export class DarkColorHandlerImpl implements DarkColorHandler {
private knownColors: Record<string, Readonly<ModeIndependentColor>> = {};
readonly baseLightness: number;

constructor(private contentDiv: HTMLElement, private getDarkColor: (color: string) => string) {}
constructor(private contentDiv: HTMLElement, baseDarkColor?: string) {
this.baseLightness = getLightness(baseDarkColor);
}

/**
* Get a copy of known colors
Expand Down Expand Up @@ -61,7 +66,7 @@ export class DarkColorHandlerImpl implements DarkColorHandler {
colorKey || `--${COLOR_VAR_PREFIX}_${lightModeColor.replace(/[^\d\w]/g, '_')}`;

if (!this.knownColors[colorKey]) {
darkModeColor = darkModeColor || this.getDarkColor(lightModeColor);
darkModeColor = darkModeColor || getDarkColor(lightModeColor, this.baseLightness);

this.knownColors[colorKey] = { lightModeColor, darkModeColor };
this.contentDiv.style.setProperty(colorKey, darkModeColor);
Expand Down Expand Up @@ -171,3 +176,30 @@ export class DarkColorHandlerImpl implements DarkColorHandler {
});
}
}

function getDarkColor(color: string, baseLightness: number): string {
try {
const computedColor = Color(color || undefined);
const colorLab = computedColor.lab().array();
const newLValue = (100 - colorLab[0]) * ((100 - baseLightness) / 100) + baseLightness;
color = Color.lab(newLValue, colorLab[1], colorLab[2])
.rgb()
.alpha(computedColor.alpha())
.toString();
} catch {}

return color;
}

function getLightness(color?: string): number {
let result = DefaultLightness;

if (color) {
try {
const computedColor = Color(color || undefined);
result = computedColor.lab().array()[0];
} catch {}
}

return result;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { createStandaloneEditorCorePlugins } from '../corePlugin/createStandaloneEditorCorePlugins';
import { createStandaloneEditorDefaultSettings } from './createStandaloneEditorDefaultSettings';
import { DarkColorHandlerImpl } from './DarkColorHandlerImpl';
import { standaloneCoreApiMap } from './standaloneCoreApiMap';
import type {
EditorEnvironment,
StandaloneEditorCore,
StandaloneEditorCorePlugins,
StandaloneEditorOptions,
UnportedCoreApiMap,
UnportedCorePluginState,
} from 'roosterjs-content-model-types';

/**
* A temporary function to create Standalone Editor core
* @param contentDiv Editor content DIV
* @param options Editor options
*/
export function createStandaloneEditorCore(
contentDiv: HTMLDivElement,
options: StandaloneEditorOptions,
unportedCoreApiMap: UnportedCoreApiMap,
unportedCorePluginState: UnportedCorePluginState
): StandaloneEditorCore {
const corePlugins = createStandaloneEditorCorePlugins(options, contentDiv);

return {
contentDiv,
api: { ...standaloneCoreApiMap, ...unportedCoreApiMap, ...options.coreApiOverride },
originalApi: { ...standaloneCoreApiMap, ...unportedCoreApiMap },
plugins: [
corePlugins.cache,
corePlugins.format,
corePlugins.copyPaste,
corePlugins.domEvent,
// TODO: Add additional plugins here
],
environment: createEditorEnvironment(),
darkColorHandler: new DarkColorHandlerImpl(contentDiv, options.baseDarkColor),
imageSelectionBorderColor: options.imageSelectionBorderColor, // TODO: Move to Selection core plugin
trustedHTMLHandler: options.trustedHTMLHandler || defaultTrustHtmlHandler,
...createStandaloneEditorDefaultSettings(options),
...getPluginState(corePlugins),
...unportedCorePluginState,
};
}

function createEditorEnvironment(): EditorEnvironment {
// It is ok to use global window here since the environment should always be the same for all windows in one session
const userAgent = window.navigator.userAgent;

return {
isMac: window.navigator.appVersion.indexOf('Mac') != -1,
isAndroid: /android/i.test(userAgent),
isSafari:
userAgent.indexOf('Safari') >= 0 &&
userAgent.indexOf('Chrome') < 0 &&
userAgent.indexOf('Android') < 0,
};
}

/**
* @internal export for test only
*/
export function defaultTrustHtmlHandler(html: string) {
return html;
}

function getPluginState(corePlugins: StandaloneEditorCorePlugins) {
return {
domEvent: corePlugins.domEvent.getState(),
copyPaste: corePlugins.copyPaste.getState(),
cache: corePlugins.cache.getState(),
format: corePlugins.format.getState(),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
} from 'roosterjs-content-model-types';

/**
* @internal
* Create default DOM and Content Model conversion settings for a standalone editor
* @param options The editor options
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { createContentModel } from '../coreApi/createContentModel';
import { createEditorContext } from '../coreApi/createEditorContext';
import { formatContentModel } from '../coreApi/formatContentModel';
import { getDOMSelection } from '../coreApi/getDOMSelection';
import { getVisibleViewport } from '../coreApi/getVisibleViewport';
import { setContentModel } from '../coreApi/setContentModel';
import { setDOMSelection } from '../coreApi/setDOMSelection';
import { switchShadowEdit } from '../coreApi/switchShadowEdit';
import type { PortedCoreApiMap } from 'roosterjs-content-model-types';

/**
* @internal
* Core API map for Standalone Content Model Editor
*/
export const standaloneCoreApiMap: PortedCoreApiMap = {
Expand All @@ -18,4 +20,5 @@ export const standaloneCoreApiMap: PortedCoreApiMap = {
setContentModel: setContentModel,
setDOMSelection: setDOMSelection,
switchShadowEdit: switchShadowEdit,
getVisibleViewport: getVisibleViewport,
};
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,9 @@ export { updateTableCellMetadata } from './metadata/updateTableCellMetadata';
export { updateTableMetadata } from './metadata/updateTableMetadata';
export { updateListMetadata } from './metadata/updateListMetadata';

export { standaloneCoreApiMap } from './editor/standaloneCoreApiMap';
export { createStandaloneEditorDefaultSettings } from './editor/createStandaloneEditorDefaultSettings';

export { ChangeSource } from './constants/ChangeSource';
export { BulletListType } from './constants/BulletListType';
export { NumberingListType } from './constants/NumberingListType';
export { TableBorderFormat } from './constants/TableBorderFormat';

export { createStandaloneEditorCorePlugins } from './corePlugin/createStandaloneEditorCorePlugins';
export { createStandaloneEditorCore } from './editor/createStandaloneEditorCore';
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"description": "Content Model for roosterjs (Under development)",
"dependencies": {
"tslib": "^2.3.1",
"color": "^3.0.0",
"roosterjs-editor-types": "",
"roosterjs-editor-dom": "",
"roosterjs-content-model-dom": "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ describe('formatContentModel', () => {
const getVisibleViewportSpy = jasmine
.createSpy('getVisibleViewport')
.and.returnValue({ top: 100, bottom: 200, left: 100, right: 200 });
core.getVisibleViewport = getVisibleViewportSpy;
core.api.getVisibleViewport = getVisibleViewportSpy;

formatContentModel(
core,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { getVisibleViewport } from '../../lib/coreApi/getVisibleViewport';

describe('getVisibleViewport', () => {
it('scrollContainer is same with contentDiv', () => {
const div = {
getBoundingClientRect: () => ({ left: 100, right: 200, top: 300, bottom: 400 }),
};
const core = {
contentDiv: div,
domEvent: {
scrollContainer: div,
},
} as any;

const result = getVisibleViewport(core);

expect(result).toEqual({ left: 100, right: 200, top: 300, bottom: 400 });
});

it('scrollContainer is different than contentDiv', () => {
const div1 = {
getBoundingClientRect: () => ({ left: 100, right: 200, top: 300, bottom: 400 }),
};
const div2 = {
getBoundingClientRect: () => ({ left: 150, right: 250, top: 350, bottom: 450 }),
};
const core = {
contentDiv: div1,
domEvent: {
scrollContainer: div2,
},
} as any;

const result = getVisibleViewport(core);

expect(result).toEqual({ left: 150, right: 200, top: 350, bottom: 400 });
});
});
Loading

0 comments on commit faae180

Please sign in to comment.