2021-09-06 17:57:07 +02:00
|
|
|
// This component is perhaps a bit of a hack but the approach should be
|
|
|
|
// reliable. It converts the current (JS) theme to CSS, and add it to the HEAD
|
|
|
|
// tag. The component itself doesn't render anything where it's located (just an
|
|
|
|
// empty invisible DIV), so it means it could be put anywhere and would have the
|
|
|
|
// same effect.
|
|
|
|
//
|
|
|
|
// It's still reliable because the lifecyle of adding the CSS and removing on
|
2024-02-26 12:16:23 +02:00
|
|
|
// unmount is handled properly. There should only be one such component on the
|
2021-09-06 17:57:07 +02:00
|
|
|
// page.
|
|
|
|
|
2024-11-08 17:32:05 +02:00
|
|
|
import * as React from 'react';
|
|
|
|
|
|
|
|
import { useEffect, useMemo, useState } from 'react';
|
2021-09-06 17:57:07 +02:00
|
|
|
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
|
|
|
|
import themeToCss from '@joplin/lib/services/style/themeToCss';
|
2023-08-21 17:01:20 +02:00
|
|
|
import { themeStyle } from '@joplin/lib/theme';
|
2024-11-08 17:32:05 +02:00
|
|
|
import useDocument from '../hooks/useDocument';
|
|
|
|
import { connect } from 'react-redux';
|
|
|
|
import { AppState } from '../../app.reducer';
|
2021-09-06 17:57:07 +02:00
|
|
|
|
|
|
|
interface Props {
|
2024-11-08 17:32:05 +02:00
|
|
|
themeId: number;
|
|
|
|
editorFontSetting: string;
|
|
|
|
customChromeCssPaths: string[];
|
2021-09-06 17:57:07 +02:00
|
|
|
}
|
|
|
|
|
2024-11-08 17:32:05 +02:00
|
|
|
const editorFontFromSettings = (settingValue: string) => {
|
|
|
|
const fontFamilies = [];
|
|
|
|
if (settingValue) fontFamilies.push(`"${settingValue}"`);
|
|
|
|
fontFamilies.push('\'Avenir Next\', Avenir, Arial, sans-serif');
|
|
|
|
|
|
|
|
return fontFamilies;
|
|
|
|
};
|
|
|
|
|
|
|
|
const useThemeCss = (themeId: number) => {
|
|
|
|
const [themeCss, setThemeCss] = useState('');
|
2021-09-06 17:57:07 +02:00
|
|
|
|
|
|
|
useAsyncEffect(async (event: AsyncEffectEvent) => {
|
2024-11-08 17:32:05 +02:00
|
|
|
const theme = themeStyle(themeId);
|
2021-09-06 17:57:07 +02:00
|
|
|
const themeCss = themeToCss(theme);
|
|
|
|
if (event.cancelled) return;
|
2024-11-08 17:32:05 +02:00
|
|
|
setThemeCss(themeCss);
|
|
|
|
}, [themeId]);
|
|
|
|
|
|
|
|
return themeCss;
|
|
|
|
};
|
|
|
|
|
|
|
|
const useEditorCss = (editorFontSetting: string) => {
|
|
|
|
return useMemo(() => {
|
|
|
|
const fontFamilies = editorFontFromSettings(editorFontSetting);
|
|
|
|
return `
|
|
|
|
/* The '*' and '!important' parts are necessary to make sure Russian text is displayed properly
|
|
|
|
https://github.com/laurent22/joplin/issues/155
|
|
|
|
|
|
|
|
Note: Be careful about the specificity here. Incorrect specificity can break monospaced fonts in tables. */
|
|
|
|
.CodeMirror5 *, .cm-editor .cm-content { font-family: ${fontFamilies.join(', ')} !important; }
|
|
|
|
`;
|
|
|
|
}, [editorFontSetting]);
|
|
|
|
};
|
2021-09-06 17:57:07 +02:00
|
|
|
|
2024-11-08 17:32:05 +02:00
|
|
|
const useLinkedCss = (doc: Document|null, cssPaths: string[]) => {
|
2021-09-06 17:57:07 +02:00
|
|
|
useEffect(() => {
|
2024-11-08 17:32:05 +02:00
|
|
|
if (!doc) return () => {};
|
|
|
|
|
|
|
|
const elements: HTMLElement[] = [];
|
|
|
|
for (const path of cssPaths) {
|
|
|
|
const element = doc.createElement('link');
|
|
|
|
element.rel = 'stylesheet';
|
|
|
|
element.href = path;
|
|
|
|
element.classList.add('dynamic-linked-stylesheet');
|
|
|
|
doc.head.appendChild(element);
|
|
|
|
|
|
|
|
elements.push(element);
|
|
|
|
}
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
for (const element of elements) {
|
|
|
|
element.remove();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}, [doc, cssPaths]);
|
|
|
|
};
|
|
|
|
|
|
|
|
const useAppliedCss = (doc: Document|null, css: string) => {
|
|
|
|
useEffect(() => {
|
|
|
|
if (!doc) return () => {};
|
|
|
|
|
|
|
|
const element = doc.createElement('style');
|
2021-09-06 17:57:07 +02:00
|
|
|
element.setAttribute('id', 'main-theme-stylesheet-container');
|
2024-11-08 17:32:05 +02:00
|
|
|
doc.head.appendChild(element);
|
|
|
|
element.appendChild(document.createTextNode(css));
|
2021-09-06 17:57:07 +02:00
|
|
|
return () => {
|
2024-11-08 17:32:05 +02:00
|
|
|
doc.head.removeChild(element);
|
2021-09-06 17:57:07 +02:00
|
|
|
};
|
2024-11-08 17:32:05 +02:00
|
|
|
}, [css, doc]);
|
|
|
|
};
|
2021-09-06 17:57:07 +02:00
|
|
|
|
2024-11-08 17:32:05 +02:00
|
|
|
const StyleSheetContainer: React.FC<Props> = props => {
|
|
|
|
const [elementRef, setElementRef] = useState<HTMLElement|null>(null);
|
|
|
|
const doc = useDocument(elementRef);
|
|
|
|
|
|
|
|
const themeCss = useThemeCss(props.themeId);
|
|
|
|
const editorCss = useEditorCss(props.editorFontSetting);
|
|
|
|
|
|
|
|
useAppliedCss(doc, `
|
|
|
|
/* Theme CSS */
|
|
|
|
${themeCss}
|
|
|
|
|
|
|
|
/* Editor font CSS */
|
|
|
|
${editorCss}
|
|
|
|
`);
|
|
|
|
useLinkedCss(doc, props.customChromeCssPaths);
|
|
|
|
|
|
|
|
return <div ref={setElementRef} style={{ display: 'none' }}></div>;
|
|
|
|
};
|
|
|
|
|
|
|
|
export default connect((state: AppState) => {
|
|
|
|
return {
|
|
|
|
themeId: state.settings.theme,
|
|
|
|
editorFontSetting: state.settings['style.editor.fontFamily'] as string,
|
|
|
|
customChromeCssPaths: state.customChromeCssPaths,
|
|
|
|
};
|
|
|
|
})(StyleSheetContainer);
|