1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Desktop: Fixes #9511: HTML notes are not readable in dark mode

This commit is contained in:
Laurent Cozic 2023-12-29 16:08:09 +00:00
parent 8a05baa97f
commit 754ca39926
7 changed files with 138 additions and 75 deletions

View File

@ -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(),

View File

@ -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';
@ -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');
@ -86,8 +89,6 @@ interface LastOnChangeEventInfo {
contentKey: string;
}
let loadedCssFiles_: string[] = [];
let loadedJsFiles_: string[] = [];
let dispatchDidUpdateIID_: any = null;
let changeId_ = 1;
@ -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');
@ -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 {
@ -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
@ -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);
@ -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 = (themeId: number, editor: any, pluginAssets: any[]) => {
const theme = themeStyle(themeId);
const loadDocumentAssets = (editor: any, pluginAssets: any[]) => {
const theme = themeStyle(props.themeId);
let docHead_: any = null;
let docHead_: HTMLHeadElement = null;
function docHead() {
if (docHead_) return docHead_;
@ -768,59 +764,56 @@ 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', {
id: elementId,
rel: 'stylesheet',
type: 'text/css',
href: cssFile,
class: 'jop-tinymce-css',
});
documentCssElements[cssFile] = 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,
id: filePathToElementId(jsFile),
type: 'text/javascript',
class: 'jop-tinymce-js',
src: jsFile,
});
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);
}
}
};
@ -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);
};
@ -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 () => {};

View File

@ -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'),
@ -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,
@ -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')}/`,
@ -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]);
@ -433,6 +438,7 @@ function NoteEditor(props: NoteEditorProps) {
ref: editorRef,
contentKey: formNote.id,
style: styles.tinyMCE,
whiteBackgroundNoteRendering,
onChange: onBodyChange,
onWillChange: onBodyWillChange,
onMessage: onMessage,

View File

@ -9,6 +9,8 @@ import { ProcessResultsRow } from '@joplin/lib/services/searchengine/SearchEngin
export interface AllAssetsOptions {
contentMaxWidthTarget?: string;
themeId?: number;
whiteBackgroundNoteRendering?: boolean;
}
export interface ToolbarButtonInfos {
@ -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;

View File

@ -13,6 +13,7 @@ interface HookDependencies {
customCss: string;
plugins: PluginStates;
settingValue: (pluginId: string, key: string)=> any;
whiteBackgroundNoteRendering: boolean;
}
export interface MarkupToHtmlOptions {
@ -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 || '',
});
@ -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]);
}

View File

@ -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
@ -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
@ -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',
@ -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);

View File

@ -10,8 +10,44 @@ function formatCssSize(v: any): string {
export interface Options {
contentMaxWidth?: number;
contentMaxWidthTarget?: string;
themeId?: number;
whiteBackgroundNoteRendering?: boolean;
}
// If we are viewing an HTML note, it means it comes from the web clipper or
// emil-to-note, in which case we don't apply any specific theme. We just need
// to ensure the background is white so that we don't end up with a dark theme
// and dark font for example. https://github.com/laurent22/joplin/issues/9511
export const whiteBackgroundNoteStyle = () => {
return `
body {
background-color: #ffffff;
}
/* TinyMCE adds a dashed border for tables that have no borders
to make it easier to view where the cells are and edit them.
However HTML notes may contain many nested tables used for
layout and we also consider that these notes are more or less
read-only. Because of this, we remove the dashed lines in this
case as it makes the note more readable. */
.mce-item-table:not([border]),
.mce-item-table:not([border]) caption,
.mce-item-table:not([border]) td,
.mce-item-table:not([border]) th,
.mce-item-table[border="0"],
.mce-item-table[border="0"] caption,
.mce-item-table[border="0"] td,
.mce-item-table[border="0"] th,
table[style*="border-width: 0px"],
table[style*="border-width: 0px"] caption,
table[style*="border-width: 0px"] td,
table[style*="border-width: 0px"] th {
border: none !important;
}
`;
};
export default function(theme: any, options: Options = null) {
options = {
contentMaxWidth: 0,