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

Dark mode editor support. #198

Merged
merged 13 commits into from Jan 24, 2019
12 changes: 10 additions & 2 deletions packages/roosterjs-editor-api/lib/format/clearFormat.ts
Expand Up @@ -46,10 +46,18 @@ export default function clearFormat(editor: Editor) {
setFontSize(editor, defaultFormat.fontSize);
}
if (defaultFormat.textColor) {
setTextColor(editor, defaultFormat.textColor);
if (defaultFormat.textColors) {
setTextColor(editor, defaultFormat.textColors);
} else {
setTextColor(editor, defaultFormat.textColor);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: setTextColor(editor, defaultFormat.textColors || defaultFormat.textColor);
(current way is also ok to me)

}
}
if (defaultFormat.backgroundColor) {
setBackgroundColor(editor, defaultFormat.backgroundColor);
if (defaultFormat.backgroundColors) {
setBackgroundColor(editor, defaultFormat.backgroundColors);
} else {
setBackgroundColor(editor, defaultFormat.backgroundColor);
}
}
if (defaultFormat.bold) {
toggleBold(editor);
Expand Down
26 changes: 21 additions & 5 deletions packages/roosterjs-editor-api/lib/format/setBackgroundColor.ts
@@ -1,14 +1,30 @@
import applyInlineStyle from '../utils/applyInlineStyle';
import { Editor } from 'roosterjs-editor-core';
import { ModeIndependentColor } from 'roosterjs-editor-types';

/**
* Set background color at current selection
* @param editor The editor instance
* @param color The color string, can be any of the predefined color names (e.g, 'red')
* @param color One of two options:
* The color string, can be any of the predefined color names (e.g, 'red')
* or hexadecimal color string (e.g, '#FF0000') or rgb value (e.g, 'rgb(255, 0, 0)') supported by browser.
* Currently there's no validation to the string, if the passed string is invalid, it won't take affect
*/
export default function setBackgroundColor(editor: Editor, color: string) {
color = color.trim();
applyInlineStyle(editor, element => (element.style.backgroundColor = color));
* Alternatively, you can pass a @typedef ModeIndepenentColor. If in light mode, the lightModeColor property will be used.
* If in dark mode, the darkModeColor will be used and the lightModeColor will be set as the ogsc.
**/
export default function setBackgroundColor(editor: Editor, color: string | ModeIndependentColor) {
if (typeof color === 'string') {
const trimmedColor = color.trim();
applyInlineStyle(editor, element => { element.style.backgroundColor = trimmedColor });
} else {
const darkMode = editor.isDarkMode();
const appliedColor = darkMode ? color.darkModeColor : color.lightModeColor;
applyInlineStyle(editor, (element) => {
element.style.backgroundColor = appliedColor;
if (darkMode) {
element.dataset.ogsb = color.lightModeColor;
}
});
}

Lego6245 marked this conversation as resolved.
Show resolved Hide resolved
}
24 changes: 20 additions & 4 deletions packages/roosterjs-editor-api/lib/format/setTextColor.ts
@@ -1,14 +1,30 @@
import applyInlineStyle from '../utils/applyInlineStyle';
import { Editor } from 'roosterjs-editor-core';
import { ModeIndependentColor } from 'roosterjs-editor-types';

/**
* Set text color at selection
* @param editor The editor instance
* @param color The color string, can be any of the predefined color names (e.g, 'red')
* @param color One of two options:
* The color string, can be any of the predefined color names (e.g, 'red')
* or hexadecimal color string (e.g, '#FF0000') or rgb value (e.g, 'rgb(255, 0, 0)') supported by browser.
* Currently there's no validation to the string, if the passed string is invalid, it won't take affect
* Alternatively, you can pass a @typedef ModeIndepenentColor. If in light mode, the lightModeColor property will be used.
* If in dark mode, the darkModeColor will be used and the lightModeColor will be set as the ogsc.
Lego6245 marked this conversation as resolved.
Show resolved Hide resolved
*/
export default function setTextColor(editor: Editor, color: string) {
color = color.trim();
applyInlineStyle(editor, element => (element.style.color = color));
export default function setTextColor(editor: Editor, color: string | ModeIndependentColor) {
Lego6245 marked this conversation as resolved.
Show resolved Hide resolved
if (typeof color === 'string') {
const trimmedColor = color.trim();
applyInlineStyle(editor, element => { element.style.color = trimmedColor });
} else {
const darkMode = editor.isDarkMode();
const appliedColor = darkMode ? color.darkModeColor : color.lightModeColor;
applyInlineStyle(editor, (element) => {
element.style.color = appliedColor;
if (darkMode) {
element.dataset.ogsc = color.lightModeColor;
}
});
}

Lego6245 marked this conversation as resolved.
Show resolved Hide resolved
}
45 changes: 44 additions & 1 deletion packages/roosterjs-editor-core/lib/editor/Editor.ts
Expand Up @@ -7,6 +7,7 @@ import {
BlockElement,
ChangeSource,
ContentPosition,
DarkModeOptions,
DefaultFormat,
DocumentCommand,
ExtractContentEvent,
Expand Down Expand Up @@ -327,17 +328,36 @@ export default class Editor {
return isNodeEmpty(this.core.contentDiv, trim);
}

/**
* Check if the editor is in dark mode
* @returns True if the editor is in dark mode, otherwise false
*/
public isDarkMode(): boolean {
Lego6245 marked this conversation as resolved.
Show resolved Hide resolved
return this.core.inDarkMode;
}

/**
* Returns the dark mode options set on the editor
* @returns A DarkModeOptions object
*/
public getDarkModeOptions(): DarkModeOptions {
return this.core.darkModeOptions;
}

/**
* Get current editor content as HTML string
* @param triggerExtractContentEvent Whether trigger ExtractContent event to all plugins
* before return. Use this parameter to remove any temporary content added by plugins.
* @param includeSelectionMarker Set to true if need include selection marker inside the content.
* When restore this content, editor will set the selection to the position marked by these markers
* @param normalizeColor Set to false if you want to get the content of the editor "as is" with no normalization.
* This is a no-op when in light mode. If false, instead of normalizing the colors to light mode, it will return the 'real' editor content.
* @returns HTML string representing current editor content
*/
public getContent(
triggerExtractContentEvent: boolean = true,
includeSelectionMarker: boolean = false
includeSelectionMarker: boolean = false,
normalizeColor: boolean = true,
): string {
let contentDiv = this.core.contentDiv;
let content = contentDiv.innerHTML;
Expand All @@ -359,9 +379,32 @@ export default class Editor {
content = extractContentEvent.content;
}

if (this.core.inDarkMode && normalizeColor) {
content = this.getColorNormalizedContent(content);
}

return content;
}

private getColorNormalizedContent(content: string): string {
let el = document.createElement('div');
el.innerHTML = content;
Lego6245 marked this conversation as resolved.
Show resolved Hide resolved
const allChildElements = el.getElementsByTagName('*') as HTMLCollectionOf<HTMLElement>;
[].forEach.call(allChildElements, (element: HTMLElement) => {
if (element.dataset && (element.dataset.ogsc || element.dataset.ogsb)) {
if (element.dataset.ogsc) {
element.style.color = element.dataset.ogsc;
}

if (element.dataset.ogsb) {
element.style.backgroundColor = element.dataset.ogsb;
}
}
});
const newContent = el.innerHTML;
return newContent;
}

/**
* Get plain text content inside editor
* @returns The text content inside editor
Expand Down
32 changes: 27 additions & 5 deletions packages/roosterjs-editor-core/lib/editor/createEditorCore.ts
Expand Up @@ -15,7 +15,7 @@ import select from '../coreAPI/select';
import triggerEvent from '../coreAPI/triggerEvent';
import TypeInContainerPlugin from '../corePlugins/TypeInContainerPlugin';
import Undo from '../undo/Undo';
import { DefaultFormat } from 'roosterjs-editor-types';
import { DARK_MODE_DEFAULT_FORMAT, DARK_MODE_DEFAULT_OPTIONS, DefaultFormat } from 'roosterjs-editor-types';
import { getComputedStyles } from 'roosterjs-editor-dom';

export default function createEditorCore(
Expand All @@ -40,10 +40,14 @@ export default function createEditorCore(
let eventHandlerPlugins = allPlugins.filter(
plugin => plugin.onPluginEvent || plugin.willHandleEventExclusively
);
// Set up dark mode defaults if not provided.
if (options.inDarkMode && options.darkModeOptions == null) {
options.darkModeOptions = DARK_MODE_DEFAULT_OPTIONS;
}
return {
contentDiv,
document: contentDiv.ownerDocument,
defaultFormat: calcDefaultFormat(contentDiv, options.defaultFormat),
defaultFormat: calcDefaultFormat(contentDiv, options),
corePlugins,
currentUndoSnapshot: null,
customData: {},
Expand All @@ -52,10 +56,14 @@ export default function createEditorCore(
eventHandlerPlugins: eventHandlerPlugins,
api: createCoreApiMap(options.coreApiOverride),
defaultApi: createCoreApiMap(),
inDarkMode: options.inDarkMode,
darkModeOptions: options.darkModeOptions,
};
}

function calcDefaultFormat(node: Node, baseFormat: DefaultFormat): DefaultFormat {
function calcDefaultFormat(node: Node, options: EditorOptions): DefaultFormat {
let baseFormat = options.defaultFormat;

if (baseFormat && Object.keys(baseFormat).length === 0) {
return {};
}
Expand All @@ -65,8 +73,22 @@ function calcDefaultFormat(node: Node, baseFormat: DefaultFormat): DefaultFormat
return {
fontFamily: baseFormat.fontFamily || styles[0],
fontSize: baseFormat.fontSize || styles[1],
textColor: baseFormat.textColor || styles[2],
backgroundColor: baseFormat.backgroundColor || '',
get textColor() {
return baseFormat.textColors ?
(options.inDarkMode ?
baseFormat.textColors.darkModeColor :
baseFormat.textColors.lightModeColor) :
(baseFormat.textColor || styles[2]);
},
textColors: baseFormat.textColors,
get backgroundColor() {
return baseFormat.backgroundColors ?
(options.inDarkMode ?
baseFormat.backgroundColors.darkModeColor :
baseFormat.backgroundColors.lightModeColor) :
(baseFormat.backgroundColor || styles[3]);
},
backgroundColors: baseFormat.backgroundColors,
bold: baseFormat.bold,
italic: baseFormat.italic,
underline: baseFormat.underline,
Expand Down
11 changes: 11 additions & 0 deletions packages/roosterjs-editor-core/lib/interfaces/EditorCore.ts
Expand Up @@ -7,6 +7,7 @@ import UndoService from './UndoService';
import {
ChangeSource,
DefaultFormat,
DarkModeOptions,
InsertOption,
NodePosition,
PluginEvent,
Expand Down Expand Up @@ -84,6 +85,16 @@ interface EditorCore {
* Cached selection range of this editor
*/
cachedSelectionRange: Range;

/**
* If the editor is in dark mode.
*/
inDarkMode: boolean;

/***
* The dark mode options, if set.
*/
darkModeOptions?: DarkModeOptions;
}

export default EditorCore;
Expand Down
12 changes: 11 additions & 1 deletion packages/roosterjs-editor-core/lib/interfaces/EditorOptions.ts
@@ -1,7 +1,7 @@
import EditorPlugin from './EditorPlugin';
import UndoService from './UndoService';
import { CoreApiMap } from './EditorCore';
import { DefaultFormat, PluginEvent } from 'roosterjs-editor-types';
import { DarkModeOptions, DefaultFormat, PluginEvent } from 'roosterjs-editor-types';
import { GenericContentEditFeature } from './ContentEditFeature';

/**
Expand Down Expand Up @@ -57,6 +57,16 @@ interface EditorOptions {
* Additional content edit features
*/
additionalEditFeatures?: GenericContentEditFeature<PluginEvent>[];

/**
* If the editor is currently in dark mode
*/
inDarkMode?: boolean;

/**
* Dark mode options for default format and paste handler
*/
darkModeOptions?: DarkModeOptions;
Lego6245 marked this conversation as resolved.
Show resolved Hide resolved
}

export default EditorOptions;
3 changes: 2 additions & 1 deletion packages/roosterjs-editor-core/lib/undo/Undo.ts
Expand Up @@ -134,7 +134,8 @@ export default class Undo implements UndoService {
public addUndoSnapshot(): string {
let snapshot = this.editor.getContent(
false /*triggerExtractContentEvent*/,
true /*markSelection*/
true /*markSelection*/,
!this.editor.isDarkMode() /*normalizeColor*/
);
this.getSnapshotsManager().addSnapshot(snapshot);
this.hasNewContent = false;
Expand Down
8 changes: 7 additions & 1 deletion packages/roosterjs-editor-dom/lib/utils/applyFormat.ts
Expand Up @@ -8,7 +8,7 @@ import { DefaultFormat } from 'roosterjs-editor-types';
export default function applyFormat(element: HTMLElement, format: DefaultFormat) {
if (format) {
let elementStyle = element.style;
let { fontFamily, fontSize, textColor, backgroundColor, bold, italic, underline } = format;
let { fontFamily, fontSize, textColor, backgroundColor, bold, italic, underline, originalSourceBackgroundColor, originalSourceTextColor } = format;
Lego6245 marked this conversation as resolved.
Show resolved Hide resolved

if (fontFamily) {
elementStyle.fontFamily = fontFamily;
Expand All @@ -31,5 +31,11 @@ export default function applyFormat(element: HTMLElement, format: DefaultFormat)
if (underline) {
elementStyle.textDecoration = 'underline';
}
if (originalSourceBackgroundColor) {
element.dataset.ogsb = originalSourceBackgroundColor;
}
if (originalSourceTextColor) {
element.dataset.ogsc = originalSourceTextColor;
Lego6245 marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
22 changes: 19 additions & 3 deletions packages/roosterjs-editor-plugins/lib/Paste/Paste.ts
Expand Up @@ -131,7 +131,18 @@ export default class Paste implements EditorPlugin {

for (let node of nodes) {
if (mergeCurrentFormat) {
this.applyTextFormat(node, clipboardData.originalFormat);
this.applyToElements(node, this.applyFormatting(clipboardData.originalFormat));
}
if (this.editor.isDarkMode()) {
// either use their paste handler or ours, but check it here.
this.applyToElements(node, (element) => {
if (this.editor.getDarkModeOptions().onExternalContentTransform) {
this.editor.getDarkModeOptions().onExternalContentTransform(element);
} else {
element.style.color = null;
Lego6245 marked this conversation as resolved.
Show resolved Hide resolved
element.style.backgroundColor = null;
}
});
}
fragment.appendChild(node);
}
Expand Down Expand Up @@ -180,7 +191,11 @@ export default class Paste implements EditorPlugin {
}, ChangeSource.Paste);
}

private applyTextFormat(node: Node, format: DefaultFormat) {
private applyFormatting = (format: DefaultFormat) => (element: HTMLElement) => {
applyFormat(element, format);
}

private applyToElements(node: Node, elementTransform: (element: HTMLElement) => void) {
let leaf = getFirstLeafNode(node);
let parents: HTMLElement[] = [];
while (leaf) {
Expand All @@ -193,8 +208,9 @@ export default class Paste implements EditorPlugin {
}
leaf = getNextLeafSibling(node, leaf);
}
parents.push(<HTMLElement>node);
for (let parent of parents) {
applyFormat(parent, format);
elementTransform(parent);
}
}

Expand Down
13 changes: 13 additions & 0 deletions packages/roosterjs-editor-types/lib/consts/DarkMode.ts
@@ -0,0 +1,13 @@
import DarkModeOptions from '../interface/DarkModeOptions';
import DefaultFormat from '../interface/DefaultFormat';

export const DARK_MODE_DEFAULT_FORMAT = <DefaultFormat>{
Lego6245 marked this conversation as resolved.
Show resolved Hide resolved
Lego6245 marked this conversation as resolved.
Show resolved Hide resolved
backgroundColor: 'rgb(51,51,51)',
textColor: 'rgb(255,255,255)',
originalSourceBackgroundColor: 'rgb(255,255,255)',
originalSourceTextColor: 'rgb(0,0,0)',
}

export const DARK_MODE_DEFAULT_OPTIONS = <DarkModeOptions>{
defaultFormat: DARK_MODE_DEFAULT_FORMAT
}