diff --git a/htdocs/js/PGProblemEditor/pgproblemeditor.js b/htdocs/js/PGProblemEditor/pgproblemeditor.js index 9f8f0162d7..c1a2aacf3d 100644 --- a/htdocs/js/PGProblemEditor/pgproblemeditor.js +++ b/htdocs/js/PGProblemEditor/pgproblemeditor.js @@ -395,27 +395,124 @@ } }); - // Synchronize the heights of the render area and the editor area for wide windows. - if (editorArea && renderArea) { - const codeMirrorResizeObserver = new ResizeObserver((entries) => { - if (document.body.clientWidth < 992) return; - - for (const entry of entries) { - if (entry.borderBoxSize) { - // Note that the blockSize is the height (since width is not resizable). - const height = Array.isArray(entry.borderBoxSize) - ? entry.borderBoxSize[0].blockSize - : entry.borderBoxSize.blockSize; - if (window.getComputedStyle(renderArea).getPropertyValue('height') !== `${height}px`) - renderArea.style.height = `${height}px`; - if (window.getComputedStyle(editorArea).getPropertyValue('height') !== `${height}px`) { - editorArea.style.height = `${height}px`; - } - } + const pgEditContainer = document.getElementById('pgedit-container'); + const codePanel = pgEditContainer?.querySelector('.pgedit-panel.code'); + const renderPanel = pgEditContainer?.querySelector('.pgedit-panel.render'); + + if (pgEditContainer && codePanel && renderPanel) { + if (document.body.clientWidth < 992) { + const initialCodePanelHeight = localStorage.getItem('WW.pgedit.codePanelHeight'); + if (initialCodePanelHeight) codePanel.style.height = initialCodePanelHeight; + } else { + const initialResizeContainerHeight = localStorage.getItem('WW.pgedit.containerHeight'); + if (initialResizeContainerHeight) pgEditContainer.style.height = initialResizeContainerHeight; + const initialCodePanelWidth = localStorage.getItem('WW.pgedit.codePanelWidth'); + if (initialCodePanelWidth) codePanel.style.width = initialCodePanelWidth; + } + + const verticalResizer = pgEditContainer.querySelector('.vertical-resizer'); + + verticalResizer?.addEventListener('pointerdown', (e) => { + verticalResizer.setPointerCapture(e.pointerId); + + const startY = e.clientY; + const startHeight = + document.body.clientWidth < 992 + ? codePanel.getBoundingClientRect().height + : pgEditContainer.getBoundingClientRect().height; + + const onPointerMove = + document.body.clientWidth < 992 + ? (moveEvent) => { + codePanel.style.height = `${startHeight + (moveEvent.clientY - startY)}px`; + localStorage.setItem('WW.pgedit.codePanelHeight', codePanel.style.height); + } + : (moveEvent) => { + pgEditContainer.style.height = `${startHeight + (moveEvent.clientY - startY)}px`; + localStorage.setItem('WW.pgedit.containerHeight', pgEditContainer.style.height); + }; + const onPointerUp = () => { + verticalResizer.releasePointerCapture(e.pointerId); + document.removeEventListener('pointermove', onPointerMove); + document.removeEventListener('pointerup', onPointerUp); + }; + + document.addEventListener('pointermove', onPointerMove); + document.addEventListener('pointerup', onPointerUp); + }); + + const updateHeight = (delta) => { + if (document.body.clientWidth < 992) { + codePanel.style.height = `${codePanel.getBoundingClientRect().height + delta}px`; + localStorage.setItem('WW.pgedit.codePanelHeight', codePanel.style.height); + } else { + pgEditContainer.style.height = `${pgEditContainer.getBoundingClientRect().height + delta}px`; + localStorage.setItem('WW.pgedit.containerHeight', pgEditContainer.style.height); + } + }; + + verticalResizer?.addEventListener('keydown', (e) => { + const step = e.ctrlKey ? 1 : e.altKey ? 50 : 20; + if (e.key === 'ArrowUp') { + updateHeight(-step); + e.preventDefault(); + } else if (e.key === 'ArrowDown') { + updateHeight(step); + e.preventDefault(); + } + }); + + const horizontalResizer = pgEditContainer.querySelector('.horizontal-resizer'); + + horizontalResizer?.addEventListener('pointerdown', (e) => { + horizontalResizer.setPointerCapture(e.pointerId); + + const startX = e.clientX; + const startWidth = codePanel.getBoundingClientRect().width; + + const onPointerMove = (moveEvent) => { + codePanel.style.width = `${startWidth + (moveEvent.clientX - startX)}px`; + localStorage.setItem('WW.pgedit.codePanelWidth', codePanel.style.width); + }; + + const onPointerUp = () => { + horizontalResizer.releasePointerCapture(e.pointerId); + document.removeEventListener('pointermove', onPointerMove); + document.removeEventListener('pointerup', onPointerUp); + }; + + document.addEventListener('pointermove', onPointerMove); + document.addEventListener('pointerup', onPointerUp); + }); + + horizontalResizer?.addEventListener('dblclick', () => { + codePanel.style.width = 'calc(50% - 0.5rem + 1px)'; + localStorage.setItem('WW.pgedit.codePanelWidth', codePanel.style.width); + }); + + horizontalResizer?.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + codePanel.style.width = 'calc(50% - 0.5rem + 1px)'; + localStorage.setItem('WW.pgedit.codePanelWidth', codePanel.style.width); + e.preventDefault(); + } + }); + + const updateWidth = (delta) => { + codePanel.style.width = `${codePanel.getBoundingClientRect().width + delta}px`; + localStorage.setItem('WW.pgedit.codePanelWidth', codePanel.style.width); + }; + + horizontalResizer?.addEventListener('keydown', (e) => { + const step = e.ctrlKey ? 1 : e.altKey ? 50 : 20; + if (e.key === 'ArrowLeft') { + updateWidth(-step); + e.preventDefault(); + } else if (e.key === 'ArrowRight') { + updateWidth(step); + e.preventDefault(); } }); - codeMirrorResizeObserver.observe(editorArea); - codeMirrorResizeObserver.observe(renderArea); } // Save the initial placeholder content of the render area so that it can be put back when a problem is reloaded. @@ -425,12 +522,29 @@ iframe.id = 'pgedit-render-iframe'; iframe.style.colorScheme = 'light'; - // Adjust the height of the iframe when the window is resized and when the iframe loads. + // Adjust editor dimensions when the window is resized and when the iframe loads. const adjustIFrameHeight = () => { if (document.body.clientWidth < 992) { - if (iframe.contentDocument) - renderArea.style.height = `${iframe.contentDocument.documentElement.offsetHeight + 2}px`; - } else renderArea.style.height = `${editorArea.offsetHeight}px`; + if (iframe.contentDocument) { + pgEditContainer.style.height = ''; + codePanel.style.width = '100%'; + codePanel.style.height = localStorage.getItem('WW.pgedit.codePanelHeight') ?? ''; + renderArea.style.height = `${ + iframe.contentDocument.documentElement.offsetHeight + + 2 + + (document.getElementById('author-comment')?.offsetHeight ?? 0) + }px`; + renderPanel.style.width = '100%'; + renderPanel.style.height = renderArea.style.height; + } + } else { + pgEditContainer.style.height = localStorage.getItem('WW.pgedit.containerHeight') ?? ''; + codePanel.style.width = localStorage.getItem('WW.pgedit.codePanelWidth') ?? ''; + renderPanel.style.width = ''; + codePanel.style.height = '100%'; + renderPanel.style.height = '100%'; + renderArea.style.height = '100%'; + } }; window.addEventListener('resize', adjustIFrameHeight); @@ -584,7 +698,8 @@ if (data.pg_flags && data.pg_flags.comment) { // The problem has a comment, so show it. const container = document.createElement('div'); - container.classList.add('px-2', 'mb-2'); + container.id = 'author-comment'; + container.classList.add('p-2'); container.innerHTML = data.pg_flags.comment; iframe.after(container); } diff --git a/htdocs/js/PGProblemEditor/pgproblemeditor.scss b/htdocs/js/PGProblemEditor/pgproblemeditor.scss new file mode 100644 index 0000000000..fec708827f --- /dev/null +++ b/htdocs/js/PGProblemEditor/pgproblemeditor.scss @@ -0,0 +1,174 @@ +#pgedit-container { + display: flex; + flex-direction: column; + width: 100%; + height: 600px; + min-height: 400px; + + .pgedit-inner-container { + display: flex; + flex: 1; + width: 100%; + height: calc(100% - 1rem - 2px); + + .pgedit-panel { + overflow: auto; + } + + .code { + width: calc(50% - 0.5rem); + height: 100%; + min-width: 400px; + + .code-mirror-editor { + min-height: unset; + overflow: unset; + height: 100%; + } + + .text-area-editor { + min-height: unset; + overflow: unset; + height: 100%; + resize: unset; + } + } + + .render { + flex: 1; + min-width: 300px; + height: 100%; + + #pgedit-render-area { + border: 1px solid var(--ww-layout-border-color, #ddd); + height: 100%; + display: flex; + flex-direction: column; + + #pgedit-render-iframe { + flex-grow: 1; + border: none; + width: 100%; + } + } + + #author-comment { + border-top: 1px solid var(--ww-layout-border-color, #ddd); + } + } + } + + .pgedit-resizer { + background-color: #fff; + transition: + background 0.2s, + box-shadow 0.2s ease-in-out; + position: relative; + border: 1px solid var(--ww-layout-border-color, #ddd); + user-select: none; + touch-action: none; + display: flex; + justify-content: center; + align-items: center; + color: black; + + &:focus { + z-index: 19; + box-shadow: 0 0 0.2rem 0.25rem #aaa; + outline: none; + } + + &:hover { + z-index: 19; + background-color: #888; + color: white; + box-shadow: 0 0 0.2rem 0.25rem #888; + } + + &::after { + content: ''; + position: absolute; + background: transparent; + } + } + + .vertical-resizer { + height: 1rem; + width: 100%; + cursor: row-resize; + + i { + transform: scale(4, 1); + } + + &::after { + top: -4px; + left: 0; + right: 0; + bottom: -4px; + } + } + + .horizontal-resizer { + height: 100%; + width: 1rem; + cursor: col-resize; + + i { + transform: scale(1, 4); + } + + &::after { + top: 0; + left: -4px; + right: -4px; + bottom: 0; + } + } + + @media (prefers-color-scheme: dark) { + .pgedit-resizer { + background-color: #000; + color: white; + + &:focus { + box-shadow: 0 0 0.2rem 0.25rem #777; + } + + &:hover { + background-color: #bbb; + color: black; + box-shadow: 0 0 0.2rem 0.25rem #999; + } + } + } + + @media (max-width: 991px) { + height: unset; + min-height: unset; + + .pgedit-inner-container { + flex-direction: column; + row-gap: 1rem; + + .code { + width: 100%; + height: 400px; + min-height: 200px; + min-width: unset; + } + + .render { + flex: unset; + height: 400px; + min-height: 200px; + width: 100%; + min-width: unset; + } + + .horizontal-resizer { + display: none; + } + } + } +} diff --git a/htdocs/js/System/system.scss b/htdocs/js/System/system.scss index ff6afbef75..b436d6f98d 100644 --- a/htdocs/js/System/system.scss +++ b/htdocs/js/System/system.scss @@ -836,7 +836,7 @@ input.changed[type='text'] { } } -/* Styles for the PGProblemEditor Page */ +/* Common styles for pages containing an editor. */ #editor { .tab-content { @@ -844,26 +844,6 @@ input.changed[type='text'] { } } -#pgedit-render-area { - border: 1px solid var(--ww-layout-border-color, #ddd); - min-height: 400px; - height: 600px; - resize: vertical; - display: flex; - flex-direction: column; - - @media only screen and (max-width: 992px) { - min-height: 200px; - height: 300px; - } - - #pgedit-render-iframe { - flex-grow: 1; - border: none; - width: 100%; - } -} - // Fix the style of the save file path input group. // It is forced to be ltr, but the bootstrap rtl style makes that look wrong. /* rtl:raw: diff --git a/templates/ContentGenerator/Instructor/PGProblemEditor.html.ep b/templates/ContentGenerator/Instructor/PGProblemEditor.html.ep index 6f38518d4a..7aa1b69ae1 100644 --- a/templates/ContentGenerator/Instructor/PGProblemEditor.html.ep +++ b/templates/ContentGenerator/Instructor/PGProblemEditor.html.ep @@ -12,6 +12,10 @@ <%= javascript getAssetURL($ce, 'js/PGProblemEditor/pgproblemeditor.js'), defer => undef =%> % end % +% content_for css => begin + <%= stylesheet getAssetURL($ce, 'js/PGProblemEditor/pgproblemeditor.css') =%> +% end +% % unless ($authz->hasPermissions(param('user'), 'access_instructor_tools')) {
<%= maketext('You are not authorized to access instructor tools.') %>
% last; @@ -154,24 +158,43 @@ % } % } -
-
- <%= generate_codemirror_html( - $c, - 'problemContents', - $problemContents, - { course_info => 'html', hardcopy_theme => 'xml' }->{ $c->{file_type} } // 'pg' - ) =%> -
-
-
-
-
<%= maketext('Loading...') %>
- +
+
+
+ <%= generate_codemirror_html( + $c, + 'problemContents', + $problemContents, + { course_info => 'html', hardcopy_theme => 'xml' }->{ $c->{file_type} } // 'pg' + ) =%> +
+ +
+
+
+
<%= maketext('Loading...') %>
+ +
+
<%= $fileInfo->() %> % diff --git a/templates/HelpFiles/InstructorPGProblemEditor.html.ep b/templates/HelpFiles/InstructorPGProblemEditor.html.ep index 515baed70b..67f82091d5 100644 --- a/templates/HelpFiles/InstructorPGProblemEditor.html.ep +++ b/templates/HelpFiles/InstructorPGProblemEditor.html.ep @@ -92,6 +92,12 @@ + <%= maketext('There are resize grips that can be moved to resize the editor windows. This can be done by ' + . 'clicking and dragging or by focusing the resize grip and using the arrow keys. If the resize grip is ' + . 'focused and the Ctrl key is held down then the size is only changed by one pixel for each arrow key ' + . 'press, and if the Alt key is held down then the size is changed by 50 pixels for each arrow key press. ' + . 'With no modifier key the size is changed by 20 pixels. Double clicking on a resize grip or pressing ' + . 'Enter or Space while a resize grip is focused resets the windows to their default sizes.') =%>
<%= maketext('Text Editor Options') %>