From 5defebec14e1970f85b3db2828e771ad0fce6a5a Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Sun, 21 Jun 2026 10:12:48 -0500 Subject: [PATCH] Setup for PG dark mode. Rework all of the problem rendering to support dark mode for PG. This is straightforward for problems in sets. All of the code added to force light mode for problems just needed to be removed. For the problem editor and all of the places that problems are rendered in an iframe via the `render_rpc` end point, a little work was needed to allow the parent to control the color scheme. The child (whose content is rendered via `templates/RPCRenderFormats/default.html.ep`) now sends a message to the parent to signal that it has loaded. This occurs at the beginning of the page load (it is important that this is earlier than the iframe `load` event or flickering will occur), and then the parent posts a message back to the child with the data `{ theme: 'mode' }` where `mode` is either `dark` or `light` to set the mode of the iframe. The problem editor is generally reworked so that everything is properly rendered in dark mode such as hardcopy theme xml and PG critic results. --- htdocs/js/GatewayQuiz/gateway.scss | 3 +-- htdocs/js/PGProblemEditor/pgproblemeditor.js | 9 +++++++++ htdocs/js/RenderProblem/renderproblem.js | 19 ++++++++++++++++++- htdocs/js/System/color-scheme.js | 6 ++++++ lib/WeBWorK/ContentGenerator/Problem.pm | 9 +-------- .../ContentGenerator/GatewayQuiz.html.ep | 3 +-- templates/RPCRenderFormats/default.html.ep | 7 +++++++ 7 files changed, 43 insertions(+), 13 deletions(-) diff --git a/htdocs/js/GatewayQuiz/gateway.scss b/htdocs/js/GatewayQuiz/gateway.scss index 5e5078e615..06690c9c96 100644 --- a/htdocs/js/GatewayQuiz/gateway.scss +++ b/htdocs/js/GatewayQuiz/gateway.scss @@ -108,9 +108,8 @@ div.gwDivider { margin: 0px 0px 10px 0px; } -/* Override the pg style so that the problem-content is not offset in gateway quizzes and force a light color scheme. */ +/* Override the pg style so that the problem-content is not offset in gateway quizzes. */ .problem-content { - color-scheme: light; padding: unset; background-color: unset; border: unset; diff --git a/htdocs/js/PGProblemEditor/pgproblemeditor.js b/htdocs/js/PGProblemEditor/pgproblemeditor.js index 6c96fa53a2..240a5a7729 100644 --- a/htdocs/js/PGProblemEditor/pgproblemeditor.js +++ b/htdocs/js/PGProblemEditor/pgproblemeditor.js @@ -593,6 +593,15 @@ iframe.title = 'Rendered content'; iframe.id = 'pgedit-render-iframe'; + window.addEventListener('message', (event) => { + if (event.data !== 'render-iframe-ready' || iframe.contentWindow !== event.source) return; + iframe.contentWindow.postMessage({ + theme: + localStorage.getItem('WW.color-scheme') ?? + (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') + }); + }); + // Adjust editor dimensions when the window is resized and when the iframe loads. const adjustIFrameHeight = () => { if (document.body.clientWidth < 992) { diff --git a/htdocs/js/RenderProblem/renderproblem.js b/htdocs/js/RenderProblem/renderproblem.js index 5b7345c79c..8a90cb48a7 100644 --- a/htdocs/js/RenderProblem/renderproblem.js +++ b/htdocs/js/RenderProblem/renderproblem.js @@ -1,4 +1,17 @@ (() => { + const renderedIframes = []; + + window.addEventListener('message', (event) => { + if (event.data !== 'render-iframe-ready') return; + renderedIframes + .find((i) => i.contentWindow === event.source) + ?.contentWindow.postMessage({ + theme: + localStorage.getItem('WW.color-scheme') ?? + (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') + }); + }); + // renderElement may either be the id of an html element, or directly an html element. // If it is an html element, then that element must have an id. webworkConfig.renderProblem = (renderElement, renderOptions) => @@ -28,7 +41,10 @@ send_pg_flags: 1, extra_header_text: '', ...renderOptions }; @@ -70,6 +86,7 @@ iframe.style.border = 'none'; while (renderArea.firstChild) renderArea.firstChild.remove(); renderArea.append(iframe); + renderedIframes.push(iframe); if (data.pg_flags && data.pg_flags.comment) { const container = document.createElement('div'); diff --git a/htdocs/js/System/color-scheme.js b/htdocs/js/System/color-scheme.js index f2a325b667..a43f4f8f6a 100644 --- a/htdocs/js/System/color-scheme.js +++ b/htdocs/js/System/color-scheme.js @@ -19,6 +19,12 @@ if (themeValue === 'dark') document.head.append(flatpickrDarkTheme); else flatpickrDarkTheme.remove(); } + + for (const renderArea of document.querySelectorAll('.rpc_render_area,#pgedit-render-area')) { + for (const iframe of renderArea.getElementsByTagName('iframe')) { + iframe.contentDocument.documentElement.dataset.bsTheme = themeValue; + } + } }; setTheme(getPreferredTheme()); diff --git a/lib/WeBWorK/ContentGenerator/Problem.pm b/lib/WeBWorK/ContentGenerator/Problem.pm index fc4d3d2954..69ea93ba07 100644 --- a/lib/WeBWorK/ContentGenerator/Problem.pm +++ b/lib/WeBWorK/ContentGenerator/Problem.pm @@ -977,14 +977,7 @@ sub output_problem_body ($c) { } } - return $c->tag( - 'div', - id => 'output_problem_body', - class => 'text-dark', - style => 'color-scheme: light', - data => { bs_theme => 'light' }, - $c->b($c->{pg}{body_text}) - ); + return $c->tag('div', id => 'output_problem_body', $c->b($c->{pg}{body_text})); } # Output messages about the problem diff --git a/templates/ContentGenerator/GatewayQuiz.html.ep b/templates/ContentGenerator/GatewayQuiz.html.ep index 2c9df191bc..ec0e9d5dd6 100644 --- a/templates/ContentGenerator/GatewayQuiz.html.ep +++ b/templates/ContentGenerator/GatewayQuiz.html.ep @@ -657,10 +657,9 @@ % delete stash->{briefErrorOutput}; % } else {
{flags}, $ce->{perProblemLangAndDirSettingMode}, $ce->{language}) %> - data-bs-theme="light" > <%== $pg->{body_text} =%>
diff --git a/templates/RPCRenderFormats/default.html.ep b/templates/RPCRenderFormats/default.html.ep index 7eefce473c..e4b2a81d63 100644 --- a/templates/RPCRenderFormats/default.html.ep +++ b/templates/RPCRenderFormats/default.html.ep @@ -13,6 +13,13 @@ course: <%= $courseID %> {webworkURLs}{htdocs}/images/favicon.ico" %>" rel="shortcut icon"> + % # Add third party css and javascript as well as css and javascript requested by the problem. % for (@$third_party_css) { %= stylesheet $_