Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standalone Editor: CreateStandaloneEditorCore #2218

Merged
merged 1 commit into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading