Skip to content

Commit

Permalink
💄 [#3049] Ensure that the tinymce widgets are re-initialized on theme…
Browse files Browse the repository at this point in the history
… changes

tinyMCE does not handle this itself, you need to explicitly force it
to destroy and re-initialize itself.

Django has two mechanisms w/r to the theme:
* store the selected theme in localStorage (if unset, it's assumed to
  be 'auto')
* set the data-theme attribute on the root html node, which triggers
  the relevant CSS rules to change the appearance

So, whenever the theme changes, the localStorage is updated and the
data-attribute on the HTML node changes.

We can observe the html node changes, and use that to synchronize our
(derived) global state. We don't care about dark/light/auto, only
about the resulting theme of a particular choice - this result is
stored in the global state. Then, our pages with tinymce editors need
to subscribe to the global state changes so that they can re-initialize:

* react component, easy, just make sure the 'key' prop changes and
  react will unmount and remount the node, triggering the re-init in
  the process from @tinymce/react
* the django-tinymce code is trickier, since there we need to get each
  editor instance ourselves and subscribe to the global state to
  ensure we reset the editors. This further customizes the overridden
  code from the upstream package.
  • Loading branch information
sergei-maertens committed Feb 21, 2024
1 parent d98e546 commit 98eda60
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 6 deletions.
6 changes: 5 additions & 1 deletion src/openforms/js/components/admin/form_design/Editor.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import {Editor} from '@tinymce/tinymce-react';
import React, {useContext, useRef} from 'react';
import {useIntl} from 'react-intl';
import {useGlobalState} from 'state-pool';
import getTinyMCEAppearance from 'tinymce_appearance';

import {currentTheme} from 'utils/theme';

import tinyMceConfig from '../../../../conf/tinymce_config.json';
import {TinyMceContext} from './Context';

const TinyMCEEditor = ({content, onEditorChange}) => {
const editorRef = useRef(null);
const tinyMceUrl = useContext(TinyMceContext);
const intl = useIntl();
const [theme] = useGlobalState(currentTheme);

const appearance = getTinyMCEAppearance();
const appearance = getTinyMCEAppearance(theme);
// when appearance changes, the key changes, which re-initializes the editor. tinymce
// does not have a built-in way to change the skin/content_css on the fly.
const key = `${appearance.skin}/${appearance.content_css}`;
Expand Down
23 changes: 21 additions & 2 deletions src/openforms/js/initTinymce.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Taken from https://github.com/jazzband/django-tinymce/blob/master/tinymce/static/django_tinymce/init_tinymce.js
// Updated to add dark theme detection (L3, L8, L38, L67)
// Updated to handle dark/light mode
import {currentTheme} from 'utils/theme';

import getTinyMCEAppearance from './tinymce_appearance';

('use strict');
Expand Down Expand Up @@ -64,7 +66,6 @@ import getTinyMCEAppearance from './tinymce_appearance';
}

function initializeTinyMCE(element, formsetName) {
appearance = getTinyMCEAppearance();
Array.from(element.querySelectorAll('.tinymce')).forEach(area => initTinyMCE(area));
}

Expand All @@ -75,6 +76,7 @@ import getTinyMCEAppearance from './tinymce_appearance';
return;
// throw 'tinyMCE is not loaded. If you customized TINYMCE_JS_URL, double-check its content.';
}
handleThemes();
// initialize the TinyMCE editors on load
initializeTinyMCE(document);

Expand All @@ -91,4 +93,21 @@ import getTinyMCEAppearance from './tinymce_appearance';
});
}
});

// taken from the tinymce react package for inspiration:
// https://github.com/tinymce/tinymce-react/blob/e2b1907eadb751f81e01d8239fdf876e77430d43/src/main/ts/components/Editor.tsx#L139
const resetEditor = el => {
const editor = window.tinyMCE.get(el.id);
editor.remove();
initTinyMCE(el);
};

const handleThemes = () => {
appearance = getTinyMCEAppearance(currentTheme.getValue());

currentTheme.subscribe(newTheme => {
appearance = getTinyMCEAppearance(newTheme);
document.querySelectorAll('.tinymce').forEach(resetEditor);
});
};
}
4 changes: 2 additions & 2 deletions src/openforms/js/tinymce_appearance.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {THEMES, detectTheme} from 'utils/theme';
import {THEMES} from 'utils/theme';

const APPEARANCE = {
[THEMES.light]: {
Expand All @@ -11,7 +11,7 @@ const APPEARANCE = {
},
};

const getTinyMCEAppearance = (theme = detectTheme()) => {
const getTinyMCEAppearance = theme => {
return APPEARANCE[theme];
};

Expand Down
30 changes: 29 additions & 1 deletion src/openforms/js/utils/theme.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import {createGlobalstate} from 'state-pool';

import {onLoaded} from 'utils/dom';

export const THEMES = {
light: 'light',
dark: 'dark',
Expand All @@ -7,7 +11,7 @@ export const THEMES = {
* Detect the active theme in JS so that the correct theme can be applied in places
* that are not simply styled through CSS.
*/
export const detectTheme = () => {
const detectTheme = () => {
// See admin/js/theme.js
const theme = window.localStorage.getItem('theme') || 'auto';

Expand All @@ -20,3 +24,27 @@ export const detectTheme = () => {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
return prefersDark ? THEMES.dark : THEMES.light;
};

/**
* Watches for changes to the data attribute of the html element.
*
* Note that we cannot use the storage event, as that is meant as a synchronization
* mechanism across tabs. It does not work in the same tab/window.
*/
const watchForThemeChanges = () => {
const htmlNode = document.documentElement; // the html node

const observer = new MutationObserver(() => {
const newTheme = detectTheme();
currentTheme.updateValue(() => newTheme);
});

observer.observe(htmlNode, {
attributes: true,
attributeFilter: ['data-theme'],
});
};

onLoaded(watchForThemeChanges);

export const currentTheme = createGlobalstate(detectTheme());

0 comments on commit 98eda60

Please sign in to comment.