Skip to content

Commit

Permalink
Desktop: Fixes #9511: HTML notes are not readable in dark mode
Browse files Browse the repository at this point in the history
  • Loading branch information
laurent22 committed Dec 29, 2023
1 parent 8a05baa commit 754ca39
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,6 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
resourceInfos: props.resourceInfos,
contentMaxWidth: props.contentMaxWidth,
mapsToLine: true,
// Always using useCustomPdfViewer for now, we can add a new setting for it in future if we need to.
useCustomPdfViewer: props.useCustomPdfViewer,
noteId: props.noteId,
vendorDir: bridge().vendorDir(),
Expand Down
116 changes: 58 additions & 58 deletions packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { _, closestSupportedLocale } from '@joplin/lib/locale';
import useContextMenu from './utils/useContextMenu';
import { copyHtmlToClipboard } from '../../utils/clipboardUtils';
import shim from '@joplin/lib/shim';
import { MarkupToHtml } from '@joplin/renderer';
import { MarkupLanguage, MarkupToHtml } from '@joplin/renderer';
import { reg } from '@joplin/lib/registry';
import BaseItem from '@joplin/lib/models/BaseItem';
import setupToolbarButtons from './utils/setupToolbarButtons';
Expand All @@ -28,6 +28,9 @@ import { TinyMceEditorEvents } from './utils/types';
import type { Editor } from 'tinymce';
import { joplinCommandToTinyMceCommands, TinyMceCommand } from './utils/joplinCommandToTinyMceCommands';
import shouldPasteResources from './utils/shouldPasteResources';
import lightTheme from '@joplin/lib/themes/light';
import { Options as NoteStyleOptions } from '@joplin/renderer/noteStyle';
const md5 = require('md5');
const { clipboard } = require('electron');
const supportedLocales = require('./supportedLocales');

Expand Down Expand Up @@ -86,8 +89,6 @@ interface LastOnChangeEventInfo {
contentKey: string;
}

let loadedCssFiles_: string[] = [];
let loadedJsFiles_: string[] = [];
let dispatchDidUpdateIID_: any = null;
let changeId_ = 1;

Expand Down Expand Up @@ -354,6 +355,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {

useEffect(() => {
const theme = themeStyle(props.themeId);
const backgroundColor = props.whiteBackgroundNoteRendering ? lightTheme.backgroundColor : theme.backgroundColor;

const element = document.createElement('style');
element.setAttribute('id', 'tinyMceStyle');
Expand Down Expand Up @@ -503,7 +505,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
}
.joplin-tinymce .tox .tox-edit-area__iframe {
background-color: ${theme.backgroundColor} !important;
background-color: ${backgroundColor} !important;
}
.joplin-tinymce .tox .tox-toolbar__primary {
Expand All @@ -524,7 +526,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
//
// tl;dr: editorReady is used here because the css needs to be re-applied after TinyMCE init
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [editorReady, props.themeId]);
}, [editorReady, props.themeId, lightTheme, props.whiteBackgroundNoteRendering]);

// -----------------------------------------------------------------------------------------
// Enable or disable the editor
Expand All @@ -542,9 +544,6 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
useEffect(() => {
if (!scriptLoaded) return;

loadedCssFiles_ = [];
loadedJsFiles_ = [];

const loadEditor = async () => {
const language = closestSupportedLocale(props.locale, true, supportedLocales);

Expand Down Expand Up @@ -739,13 +738,10 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
// Set the initial content and load the plugin CSS and JS files
// -----------------------------------------------------------------------------------------

const documentCssElements: Record<string, HTMLLinkElement> = {};
const documentScriptElements: Record<string, HTMLScriptElement> = {};

const loadDocumentAssets = (editor: any, pluginAssets: any[]) => {
const theme = themeStyle(props.themeId);
const loadDocumentAssets = (themeId: number, editor: any, pluginAssets: any[]) => {
const theme = themeStyle(themeId);

let docHead_: any = null;
let docHead_: HTMLHeadElement = null;

function docHead() {
if (docHead_) return docHead_;
Expand All @@ -768,58 +764,55 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
.map((a: any) => a.path),
);

const filePathToElementId = (path: string) => {
return `jop-tiny-mce-${md5(escape(path))}`;
};

// Remove all previously loaded files that aren't in the assets this time.
// Note: This is important to ensure that we properly change themes.
// See https://github.com/laurent22/joplin/issues/8520
for (const cssFile of loadedCssFiles_) {
if (!allCssFiles.includes(cssFile)) {
documentCssElements[cssFile]?.remove();
delete documentCssElements[cssFile];
}
}
const existingElements = Array.from(docHead().getElementsByClassName('jop-tinymce-css')).concat(Array.from(docHead().getElementsByClassName('jop-tinymce-js')));

for (const jsFile of loadedJsFiles_) {
if (!allJsFiles.includes(jsFile)) {
documentScriptElements[jsFile]?.remove();
delete documentScriptElements[jsFile];
}
}
const existingIds: string[] = [];
for (const e of existingElements) existingIds.push(e.getAttribute('id'));

const newCssFiles = allCssFiles.filter((path: string) => !loadedCssFiles_.includes(path));
const newJsFiles = allJsFiles.filter((path: string) => !loadedJsFiles_.includes(path));
const processedIds: string[] = [];

loadedCssFiles_ = allCssFiles;
loadedJsFiles_ = allJsFiles;
for (const cssFile of allCssFiles) {
const elementId = filePathToElementId(cssFile);
processedIds.push(elementId);
if (existingIds.includes(elementId)) continue;

// console.info('loadDocumentAssets: files to load', cssFiles, jsFiles);

if (newCssFiles.length) {
for (const cssFile of newCssFiles) {
const style = editor.dom.create('link', {
rel: 'stylesheet',
type: 'text/css',
href: cssFile,
class: 'jop-tinymce-css',
});
const style = editor.dom.create('link', {
id: elementId,
rel: 'stylesheet',
type: 'text/css',
href: cssFile,
class: 'jop-tinymce-css',
});

documentCssElements[cssFile] = style;
docHead().appendChild(style);
}
docHead().appendChild(style);
}

if (newJsFiles.length) {
const editorElementId = editor.dom.uniqueId();
for (const jsFile of allJsFiles) {
const elementId = filePathToElementId(jsFile);
processedIds.push(elementId);
if (existingIds.includes(elementId)) continue;

for (const jsFile of newJsFiles) {
const script = editor.dom.create('script', {
id: editorElementId,
type: 'text/javascript',
src: jsFile,
});
const script = editor.dom.create('script', {
id: filePathToElementId(jsFile),
type: 'text/javascript',
class: 'jop-tinymce-js',
src: jsFile,
});

docHead().appendChild(script);
}

documentScriptElements[jsFile] = script;
docHead().appendChild(script);
// Remove all previously loaded files that aren't in the assets this time.
// Note: This is important to ensure that we properly change themes.
// See https://github.com/laurent22/joplin/issues/8520
for (const existingId of existingIds) {
if (!processedIds.includes(existingId)) {
const element = existingElements.find(e => e.getAttribute('id') === existingId);
if (element) docHead().removeChild(element);
}
}
};
Expand Down Expand Up @@ -898,7 +891,14 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
};
}

await loadDocumentAssets(editor, await props.allAssets(props.contentMarkupLanguage, { contentMaxWidthTarget: '.mce-content-body' }));
const allAssetsOptions: NoteStyleOptions = {
contentMaxWidthTarget: '.mce-content-body',
themeId: props.contentMarkupLanguage === MarkupLanguage.Html ? 1 : null,
whiteBackgroundNoteRendering: props.whiteBackgroundNoteRendering,
};

const allAssets = await props.allAssets(props.contentMarkupLanguage, allAssetsOptions);
await loadDocumentAssets(props.themeId, editor, allAssets);

dispatchDidUpdate(editor);
};
Expand All @@ -909,7 +909,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
cancelled = true;
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [editor, props.markupToHtml, props.allAssets, props.content, props.resourceInfos, props.contentKey]);
}, [editor, props.themeId, props.markupToHtml, props.allAssets, props.content, props.resourceInfos, props.contentKey, props.contentMarkupLanguage, props.whiteBackgroundNoteRendering]);

useEffect(() => {
if (!editor) return () => {};
Expand Down
8 changes: 7 additions & 1 deletion packages/app-desktop/gui/NoteEditor/NoteEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import CodeMirror6 from './NoteBody/CodeMirror/v6/CodeMirror';
import CodeMirror5 from './NoteBody/CodeMirror/v5/CodeMirror';
import { openItemById } from './utils/contextMenu';
import { namespacedKey } from '@joplin/lib/services/plugins/api/JoplinSettings';
import { MarkupLanguage } from '@joplin/renderer';

const commands = [
require('./commands/showRevisions'),
Expand Down Expand Up @@ -165,8 +166,11 @@ function NoteEditor(props: NoteEditorProps) {
return Setting.value(namespacedKey(pluginId, key));
}, []);

const whiteBackgroundNoteRendering = formNote.markup_language === MarkupLanguage.Html;

const markupToHtml = useMarkupToHtml({
themeId: props.themeId,
whiteBackgroundNoteRendering,
customCss: props.customCss,
plugins: props.plugins,
settingValue,
Expand All @@ -178,7 +182,7 @@ function NoteEditor(props: NoteEditorProps) {
...options,
};

const theme = themeStyle(props.themeId);
const theme = themeStyle(options.themeId ? options.themeId : props.themeId);

const markupToHtml = markupLanguageUtils.newMarkupToHtml({}, {
resourceBaseUrl: `file://${Setting.value('resourceDir')}/`,
Expand All @@ -188,6 +192,7 @@ function NoteEditor(props: NoteEditorProps) {
return markupToHtml.allAssets(markupLanguage, theme, {
contentMaxWidth: props.contentMaxWidth,
contentMaxWidthTarget: options.contentMaxWidthTarget,
whiteBackgroundNoteRendering: options.whiteBackgroundNoteRendering,
});
}, [props.themeId, props.customCss, props.contentMaxWidth]);

Expand Down Expand Up @@ -433,6 +438,7 @@ function NoteEditor(props: NoteEditorProps) {
ref: editorRef,
contentKey: formNote.id,
style: styles.tinyMCE,
whiteBackgroundNoteRendering,
onChange: onBodyChange,
onWillChange: onBodyWillChange,
onMessage: onMessage,
Expand Down
10 changes: 10 additions & 0 deletions packages/app-desktop/gui/NoteEditor/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { ProcessResultsRow } from '@joplin/lib/services/searchengine/SearchEngin

export interface AllAssetsOptions {
contentMaxWidthTarget?: string;
themeId?: number;
whiteBackgroundNoteRendering?: boolean;
}

export interface ToolbarButtonInfos {
Expand Down Expand Up @@ -63,6 +65,14 @@ export interface NoteBodyEditorProps {
style: any;
ref: any;
themeId: number;

// When this is true it means the note must always be rendered using a white
// background theme. This applies to the Markdown editor note view, and to
// the RTE. It does not apply to other elements such as the toolbar, dialogs
// or the CodeMirror editor. This is used to correctly render HTML notes and
// avoid cases where black text is rendered over a dark background.
whiteBackgroundNoteRendering: boolean;

content: string;
contentKey: string;
contentMarkupLanguage: number;
Expand Down
9 changes: 6 additions & 3 deletions packages/app-desktop/gui/NoteEditor/utils/useMarkupToHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface HookDependencies {
customCss: string;
plugins: PluginStates;
settingValue: (pluginId: string, key: string)=> any;
whiteBackgroundNoteRendering: boolean;
}

export interface MarkupToHtmlOptions {
Expand All @@ -27,13 +28,14 @@ export interface MarkupToHtmlOptions {
vendorDir?: string;
platformName?: string;
allowedFilePrefixes?: string[];
whiteBackgroundNoteRendering?: boolean;
}

export default function useMarkupToHtml(deps: HookDependencies) {
const { themeId, customCss, plugins } = deps;
const { themeId, customCss, plugins, whiteBackgroundNoteRendering } = deps;

const markupToHtml = useMemo(() => {
return markupLanguageUtils.newMarkupToHtml(deps.plugins, {
return markupLanguageUtils.newMarkupToHtml(plugins, {
resourceBaseUrl: `file://${Setting.value('resourceDir')}/`,
customCss: customCss || '',
});
Expand Down Expand Up @@ -69,10 +71,11 @@ export default function useMarkupToHtml(deps: HookDependencies) {
externalAssetsOnly: true,
codeHighlightCacheKey: 'useMarkupToHtml',
settingValue: deps.settingValue,
whiteBackgroundNoteRendering,
...options,
});

return result;
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [themeId, customCss, markupToHtml]);
}, [themeId, customCss, markupToHtml, whiteBackgroundNoteRendering]);
}
21 changes: 15 additions & 6 deletions packages/renderer/HtmlToHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import linkReplacement from './MdToHtml/linkReplacement';
import utils, { ItemIdToUrlHandler } from './utils';
import InMemoryCache from './InMemoryCache';
import { RenderResult } from './MarkupToHtml';
import noteStyle, { whiteBackgroundNoteStyle } from './noteStyle';
import { Options as NoteStyleOptions } from './noteStyle';
const md5 = require('md5');

// Renderered notes can potentially be quite large (for example
Expand Down Expand Up @@ -39,6 +41,7 @@ interface RenderOptions {
enableLongPress: boolean;
itemIdToUrl?: ItemIdToUrlHandler;
allowedFilePrefixes?: string[];
whiteBackgroundNoteRendering?: boolean;
}

// https://github.com/es-shims/String.prototype.trimStart/blob/main/implementation.js
Expand Down Expand Up @@ -81,14 +84,22 @@ export default class HtmlToHtml {
return this.fsDriver_;
}

public async allAssets(/* theme*/): Promise<any[]> {
return []; // TODO
public async allAssets(theme: any, noteStyleOptions: NoteStyleOptions = null) {
let cssStrings: string[] = [];

if (noteStyleOptions.whiteBackgroundNoteRendering) {
cssStrings = [whiteBackgroundNoteStyle()];
} else {
cssStrings = [noteStyle(theme, noteStyleOptions).join('\n')];
}

return [await this.fsDriver().cacheCssToFile(cssStrings)];
}

// Note: the "theme" variable is ignored and instead the light theme is
// always used for HTML notes.
// See: https://github.com/laurent22/joplin/issues/3698
public async render(markup: string, _theme: any, options: RenderOptions): Promise<RenderResult> {
public async render(markup: string, theme: any, options: RenderOptions): Promise<RenderResult> {
options = {
splitted: false,
postMessageSyntax: 'postMessage',
Expand Down Expand Up @@ -152,9 +163,7 @@ export default class HtmlToHtml {
};
}

// const lightTheme = themeStyle(Setting.THEME_LIGHT);
// let cssStrings = noteStyle(lightTheme);
let cssStrings: string[] = [];
let cssStrings = options.whiteBackgroundNoteRendering ? [whiteBackgroundNoteStyle()] : noteStyle(theme);

if (options.splitted) {
const splitted = splitHtml(html);
Expand Down
Loading

0 comments on commit 754ca39

Please sign in to comment.