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
27 changes: 21 additions & 6 deletions packages/roosterjs-editor-api/lib/format/setBackgroundColor.ts
@@ -1,14 +1,29 @@
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 used when converting back to light mode.
**/
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;
}
});
}
}
23 changes: 19 additions & 4 deletions packages/roosterjs-editor-api/lib/format/setTextColor.ts
@@ -1,14 +1,29 @@
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 used when converting back to light mode.
*/
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;
}
});
}
}
8 changes: 4 additions & 4 deletions packages/roosterjs-editor-api/lib/utils/processList.ts
Expand Up @@ -43,13 +43,13 @@ export default function processList(editor: Editor, command: DocumentCommand): N
if (LIs.length == 1 && isNodeEmpty(LIs[0], true /*trim*/)) {
// When there's only one LI child element which has empty content, it means this LI is just created.
// We just format it with current format
applyListFormat(LIs[0], currentFormat);
applyListFormat(LIs[0], currentFormat, editor.isDarkMode());
} else {
// Otherwise, apply the format of first child non-empty element (if any) to LI node
for (let li of LIs) {
let formatNode = getFirstLeafNode(li);
if (formatNode) {
applyListFormat(li, getComputedStyles(formatNode));
applyListFormat(li, getComputedStyles(formatNode), editor.isDarkMode());
}
}
}
Expand All @@ -58,13 +58,13 @@ export default function processList(editor: Editor, command: DocumentCommand): N
return newList;
}

function applyListFormat(node: Node, formats: string[]) {
function applyListFormat(node: Node, formats: string[], isDarkMode: boolean) {
applyFormat(node as HTMLElement, {
fontFamily: formats[0],
fontSize: formats[1],
textColor: formats[2],
backgroundColor: formats[3],
});
}, isDarkMode);
}

function workaroundForChrome(editor: Editor) {
Expand Down
Expand Up @@ -86,7 +86,7 @@ export default class TypeInContainerPlugin implements EditorPlugin {
}

if (formatNode) {
applyFormat(formatNode, this.editor.getDefaultFormat());
applyFormat(formatNode, this.editor.getDefaultFormat(), this.editor.isDarkMode());
}

return result;
Expand Down
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
37 changes: 32 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, DefaultFormat } from 'roosterjs-editor-types';
import { getComputedStyles } from 'roosterjs-editor-dom';

export default function createEditorCore(
Expand Down Expand Up @@ -43,7 +43,7 @@ export default function createEditorCore(
return {
contentDiv,
document: contentDiv.ownerDocument,
defaultFormat: calcDefaultFormat(contentDiv, options.defaultFormat),
defaultFormat: calcDefaultFormat(contentDiv, options),
corePlugins,
currentUndoSnapshot: null,
customData: {},
Expand All @@ -52,10 +52,23 @@ 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 (options.inDarkMode) {
if (!baseFormat.backgroundColors) {
baseFormat.backgroundColors = DARK_MODE_DEFAULT_FORMAT.backgroundColors;
}
if (!baseFormat.textColors) {
baseFormat.textColors = DARK_MODE_DEFAULT_FORMAT.textColors;
}
}

if (baseFormat && Object.keys(baseFormat).length === 0) {
return {};
}
Expand All @@ -65,8 +78,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 || '');
},
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
14 changes: 10 additions & 4 deletions packages/roosterjs-editor-dom/lib/utils/applyFormat.ts
Expand Up @@ -5,21 +5,27 @@ import { DefaultFormat } from 'roosterjs-editor-types';
* @param element The HTML element to apply format to
* @param format The format to apply
*/
export default function applyFormat(element: HTMLElement, format: DefaultFormat) {
export default function applyFormat(element: HTMLElement, format: DefaultFormat, isDarkMode?: boolean) {
if (format) {
let elementStyle = element.style;
let { fontFamily, fontSize, textColor, backgroundColor, bold, italic, underline } = format;
let { fontFamily, fontSize, textColor, textColors, backgroundColor, backgroundColors, bold, italic, underline } = format;

if (fontFamily) {
elementStyle.fontFamily = fontFamily;
}
if (fontSize) {
elementStyle.fontSize = fontSize;
}
if (textColor) {
if (textColor || textColors) {
if (textColors && isDarkMode) {
element.dataset.ogsc = textColors.lightModeColor;
}
elementStyle.color = textColor;
}
if (backgroundColor) {
if (backgroundColor || backgroundColors) {
if (backgroundColors && isDarkMode) {
element.dataset.ogsb = backgroundColors.lightModeColor;
}
elementStyle.backgroundColor = backgroundColor;
}
if (bold) {
Expand Down