From 81311fc246edab7410db101312d1208482433996 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 bb0e3fc40f..28ceea7f2c 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 4318e11f1f..719da4e774 100644 --- a/htdocs/js/PGProblemEditor/pgproblemeditor.js +++ b/htdocs/js/PGProblemEditor/pgproblemeditor.js @@ -586,6 +586,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..9cd8946f76 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')) { + 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 2d87362580..0feb701f74 100644 --- a/templates/ContentGenerator/GatewayQuiz.html.ep +++ b/templates/ContentGenerator/GatewayQuiz.html.ep @@ -638,10 +638,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 245c24b30a..ec762d307a 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 $_