Skip to content

Commit 96e8adf

Browse files
committed
Rework resizing of the problem editor.
The code and rendering panels are now not only vertically resizable, but are horizontally resizable when the window with is at or above the large breakpoint (992 pixels). Furthermore, resizing does not work with the native browser resize via the css `resize` property. Instead it is controlled with JavaScript. The resize grips (which are much more visible now) can be also be focused with the keyboard and when focused the arrow keys can be used to resize the code and render panels. Note that if `Ctrl` is pressed with an arrow key a 1 pixel resize occurs, and if `Alt` is pressed with an arrow key a 50 pixel resize occurs. Without a modifier key the arrow keys perform a 20 pixel resize. In addition, the dimensions are saved to local storage and persist when the page reloads. Unfortunately there will be some flickering of content as the resize occurs after the page loads. Note that the css `resize` property is actually not supported in all browsers, so this actually makes resizing work for those browsers as well. The browsers that do not support the css `resize` property include Firefox for Android, and Safari on IOS. Yeah, those are for mobile devices, and who edits problems on a mobile device? In any case, this makes the resize grips more evident. The native resize grip is rather small in the lower right corner of the CodeMirror editor panel, and many probably don't even know it is there. Note that the code panel has a minmimum width of 400 pixels, and the rendering panel a minimum width of 300 pixels. This works out so that when the window size is 992 pixels the two panels can't really be resized much or at all (when the site navigation menu width of 250 pixels is taken into account) depending on the browser. But at larger window widths resizing can be done. I thought about making it so that the resizing could go all the way to the right and the rendering panel be resized to a width of 0, but decided against it. If that were done, then the rendering would still be occuring even though you can't see it, and that doesn't seem good. I think that this should at least alleviate the request(s) to hide the rendering panel (which I don't think is really a good idea).
1 parent 0b1fa1c commit 96e8adf

5 files changed

Lines changed: 358 additions & 60 deletions

File tree

htdocs/js/PGProblemEditor/pgproblemeditor.js

Lines changed: 139 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -395,27 +395,124 @@
395395
}
396396
});
397397

398-
// Synchronize the heights of the render area and the editor area for wide windows.
399-
if (editorArea && renderArea) {
400-
const codeMirrorResizeObserver = new ResizeObserver((entries) => {
401-
if (document.body.clientWidth < 992) return;
402-
403-
for (const entry of entries) {
404-
if (entry.borderBoxSize) {
405-
// Note that the blockSize is the height (since width is not resizable).
406-
const height = Array.isArray(entry.borderBoxSize)
407-
? entry.borderBoxSize[0].blockSize
408-
: entry.borderBoxSize.blockSize;
409-
if (window.getComputedStyle(renderArea).getPropertyValue('height') !== `${height}px`)
410-
renderArea.style.height = `${height}px`;
411-
if (window.getComputedStyle(editorArea).getPropertyValue('height') !== `${height}px`) {
412-
editorArea.style.height = `${height}px`;
413-
}
414-
}
398+
const pgEditContainer = document.getElementById('pgedit-container');
399+
const codePanel = pgEditContainer?.querySelector('.pgedit-panel.code');
400+
const renderPanel = pgEditContainer?.querySelector('.pgedit-panel.render');
401+
402+
if (pgEditContainer && codePanel && renderPanel) {
403+
if (document.body.clientWidth < 992) {
404+
const initialCodePanelHeight = localStorage.getItem('WW.pgedit.codePanelHeight');
405+
if (initialCodePanelHeight) codePanel.style.height = initialCodePanelHeight;
406+
} else {
407+
const initialResizeContainerHeight = localStorage.getItem('WW.pgedit.containerHeight');
408+
if (initialResizeContainerHeight) pgEditContainer.style.height = initialResizeContainerHeight;
409+
const initialCodePanelWidth = localStorage.getItem('WW.pgedit.codePanelWidth');
410+
if (initialCodePanelWidth) codePanel.style.width = initialCodePanelWidth;
411+
}
412+
413+
const verticalResizer = pgEditContainer.querySelector('.vertical-resizer');
414+
415+
verticalResizer?.addEventListener('pointerdown', (e) => {
416+
verticalResizer.setPointerCapture(e.pointerId);
417+
418+
const startY = e.clientY;
419+
const startHeight =
420+
document.body.clientWidth < 992
421+
? codePanel.getBoundingClientRect().height
422+
: pgEditContainer.getBoundingClientRect().height;
423+
424+
const onPointerMove =
425+
document.body.clientWidth < 992
426+
? (moveEvent) => {
427+
codePanel.style.height = `${startHeight + (moveEvent.clientY - startY)}px`;
428+
localStorage.setItem('WW.pgedit.codePanelHeight', codePanel.style.height);
429+
}
430+
: (moveEvent) => {
431+
pgEditContainer.style.height = `${startHeight + (moveEvent.clientY - startY)}px`;
432+
localStorage.setItem('WW.pgedit.containerHeight', pgEditContainer.style.height);
433+
};
434+
const onPointerUp = () => {
435+
verticalResizer.releasePointerCapture(e.pointerId);
436+
document.removeEventListener('pointermove', onPointerMove);
437+
document.removeEventListener('pointerup', onPointerUp);
438+
};
439+
440+
document.addEventListener('pointermove', onPointerMove);
441+
document.addEventListener('pointerup', onPointerUp);
442+
});
443+
444+
const updateHeight = (delta) => {
445+
if (document.body.clientWidth < 992) {
446+
codePanel.style.height = `${codePanel.getBoundingClientRect().height + delta}px`;
447+
localStorage.setItem('WW.pgedit.codePanelHeight', codePanel.style.height);
448+
} else {
449+
pgEditContainer.style.height = `${pgEditContainer.getBoundingClientRect().height + delta}px`;
450+
localStorage.setItem('WW.pgedit.containerHeight', pgEditContainer.style.height);
451+
}
452+
};
453+
454+
verticalResizer?.addEventListener('keydown', (e) => {
455+
const step = e.ctrlKey ? 1 : e.altKey ? 50 : 20;
456+
if (e.key === 'ArrowUp') {
457+
updateHeight(-step);
458+
e.preventDefault();
459+
} else if (e.key === 'ArrowDown') {
460+
updateHeight(step);
461+
e.preventDefault();
462+
}
463+
});
464+
465+
const horizontalResizer = pgEditContainer.querySelector('.horizontal-resizer');
466+
467+
horizontalResizer?.addEventListener('pointerdown', (e) => {
468+
horizontalResizer.setPointerCapture(e.pointerId);
469+
470+
const startX = e.clientX;
471+
const startWidth = codePanel.getBoundingClientRect().width;
472+
473+
const onPointerMove = (moveEvent) => {
474+
codePanel.style.width = `${startWidth + (moveEvent.clientX - startX)}px`;
475+
localStorage.setItem('WW.pgedit.codePanelWidth', codePanel.style.width);
476+
};
477+
478+
const onPointerUp = () => {
479+
horizontalResizer.releasePointerCapture(e.pointerId);
480+
document.removeEventListener('pointermove', onPointerMove);
481+
document.removeEventListener('pointerup', onPointerUp);
482+
};
483+
484+
document.addEventListener('pointermove', onPointerMove);
485+
document.addEventListener('pointerup', onPointerUp);
486+
});
487+
488+
horizontalResizer?.addEventListener('dblclick', () => {
489+
codePanel.style.width = 'calc(50% - 0.5rem + 1px)';
490+
localStorage.setItem('WW.pgedit.codePanelWidth', codePanel.style.width);
491+
});
492+
493+
horizontalResizer?.addEventListener('keydown', (e) => {
494+
if (e.key === 'Enter' || e.key === ' ') {
495+
codePanel.style.width = 'calc(50% - 0.5rem + 1px)';
496+
localStorage.setItem('WW.pgedit.codePanelWidth', codePanel.style.width);
497+
e.preventDefault();
498+
}
499+
});
500+
501+
const updateWidth = (delta) => {
502+
codePanel.style.width = `${codePanel.getBoundingClientRect().width + delta}px`;
503+
localStorage.setItem('WW.pgedit.codePanelWidth', codePanel.style.width);
504+
};
505+
506+
horizontalResizer?.addEventListener('keydown', (e) => {
507+
const step = e.ctrlKey ? 1 : e.altKey ? 50 : 20;
508+
if (e.key === 'ArrowLeft') {
509+
updateWidth(-step);
510+
e.preventDefault();
511+
} else if (e.key === 'ArrowRight') {
512+
updateWidth(step);
513+
e.preventDefault();
415514
}
416515
});
417-
codeMirrorResizeObserver.observe(editorArea);
418-
codeMirrorResizeObserver.observe(renderArea);
419516
}
420517

421518
// 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 @@
425522
iframe.id = 'pgedit-render-iframe';
426523
iframe.style.colorScheme = 'light';
427524

428-
// Adjust the height of the iframe when the window is resized and when the iframe loads.
525+
// Adjust editor dimensions when the window is resized and when the iframe loads.
429526
const adjustIFrameHeight = () => {
430527
if (document.body.clientWidth < 992) {
431-
if (iframe.contentDocument)
432-
renderArea.style.height = `${iframe.contentDocument.documentElement.offsetHeight + 2}px`;
433-
} else renderArea.style.height = `${editorArea.offsetHeight}px`;
528+
if (iframe.contentDocument) {
529+
pgEditContainer.style.height = '';
530+
codePanel.style.width = '100%';
531+
codePanel.style.height = localStorage.getItem('WW.pgedit.codePanelHeight') ?? '';
532+
renderArea.style.height = `${
533+
iframe.contentDocument.documentElement.offsetHeight +
534+
2 +
535+
(document.getElementById('author-comment')?.offsetHeight ?? 0)
536+
}px`;
537+
renderPanel.style.width = '100%';
538+
renderPanel.style.height = renderArea.style.height;
539+
}
540+
} else {
541+
pgEditContainer.style.height = localStorage.getItem('WW.pgedit.containerHeight') ?? '';
542+
codePanel.style.width = localStorage.getItem('WW.pgedit.codePanelWidth') ?? '';
543+
renderPanel.style.width = '';
544+
codePanel.style.height = '100%';
545+
renderPanel.style.height = '100%';
546+
renderArea.style.height = '100%';
547+
}
434548
};
435549
window.addEventListener('resize', adjustIFrameHeight);
436550

@@ -584,7 +698,8 @@
584698
if (data.pg_flags && data.pg_flags.comment) {
585699
// The problem has a comment, so show it.
586700
const container = document.createElement('div');
587-
container.classList.add('px-2', 'mb-2');
701+
container.id = 'author-comment';
702+
container.classList.add('p-2');
588703
container.innerHTML = data.pg_flags.comment;
589704
iframe.after(container);
590705
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
#pgedit-container {
2+
display: flex;
3+
flex-direction: column;
4+
width: 100%;
5+
height: 600px;
6+
min-height: 400px;
7+
8+
.pgedit-inner-container {
9+
display: flex;
10+
flex: 1;
11+
width: 100%;
12+
height: calc(100% - 1rem - 2px);
13+
14+
.pgedit-panel {
15+
overflow: auto;
16+
}
17+
18+
.code {
19+
width: calc(50% - 0.5rem);
20+
height: 100%;
21+
min-width: 400px;
22+
23+
.code-mirror-editor {
24+
min-height: unset;
25+
overflow: unset;
26+
height: 100%;
27+
}
28+
29+
.text-area-editor {
30+
min-height: unset;
31+
overflow: unset;
32+
height: 100%;
33+
resize: unset;
34+
}
35+
}
36+
37+
.render {
38+
flex: 1;
39+
min-width: 300px;
40+
height: 100%;
41+
42+
#pgedit-render-area {
43+
border: 1px solid var(--ww-layout-border-color, #ddd);
44+
height: 100%;
45+
display: flex;
46+
flex-direction: column;
47+
48+
#pgedit-render-iframe {
49+
flex-grow: 1;
50+
border: none;
51+
width: 100%;
52+
}
53+
}
54+
55+
#author-comment {
56+
border-top: 1px solid var(--ww-layout-border-color, #ddd);
57+
}
58+
}
59+
}
60+
61+
.pgedit-resizer {
62+
background-color: #fff;
63+
transition:
64+
background 0.2s,
65+
box-shadow 0.2s ease-in-out;
66+
position: relative;
67+
border: 1px solid var(--ww-layout-border-color, #ddd);
68+
user-select: none;
69+
touch-action: none;
70+
display: flex;
71+
justify-content: center;
72+
align-items: center;
73+
color: black;
74+
75+
&:focus {
76+
z-index: 19;
77+
box-shadow: 0 0 0.2rem 0.25rem #aaa;
78+
outline: none;
79+
}
80+
81+
&:hover {
82+
z-index: 19;
83+
background-color: #888;
84+
color: white;
85+
box-shadow: 0 0 0.2rem 0.25rem #888;
86+
}
87+
88+
&::after {
89+
content: '';
90+
position: absolute;
91+
background: transparent;
92+
}
93+
}
94+
95+
.vertical-resizer {
96+
height: 1rem;
97+
width: 100%;
98+
cursor: row-resize;
99+
100+
i {
101+
transform: scale(4, 1);
102+
}
103+
104+
&::after {
105+
top: -4px;
106+
left: 0;
107+
right: 0;
108+
bottom: -4px;
109+
}
110+
}
111+
112+
.horizontal-resizer {
113+
height: 100%;
114+
width: 1rem;
115+
cursor: col-resize;
116+
117+
i {
118+
transform: scale(1, 4);
119+
}
120+
121+
&::after {
122+
top: 0;
123+
left: -4px;
124+
right: -4px;
125+
bottom: 0;
126+
}
127+
}
128+
129+
@media (prefers-color-scheme: dark) {
130+
.pgedit-resizer {
131+
background-color: #000;
132+
color: white;
133+
134+
&:focus {
135+
box-shadow: 0 0 0.2rem 0.25rem #777;
136+
}
137+
138+
&:hover {
139+
background-color: #bbb;
140+
color: black;
141+
box-shadow: 0 0 0.2rem 0.25rem #999;
142+
}
143+
}
144+
}
145+
146+
@media (max-width: 991px) {
147+
height: unset;
148+
min-height: unset;
149+
150+
.pgedit-inner-container {
151+
flex-direction: column;
152+
row-gap: 1rem;
153+
154+
.code {
155+
width: 100%;
156+
height: 400px;
157+
min-height: 200px;
158+
min-width: unset;
159+
}
160+
161+
.render {
162+
flex: unset;
163+
height: 400px;
164+
min-height: 200px;
165+
width: 100%;
166+
min-width: unset;
167+
}
168+
169+
.horizontal-resizer {
170+
display: none;
171+
}
172+
}
173+
}
174+
}

htdocs/js/System/system.scss

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -836,34 +836,14 @@ input.changed[type='text'] {
836836
}
837837
}
838838

839-
/* Styles for the PGProblemEditor Page */
839+
/* Common styles for pages containing an editor. */
840840

841841
#editor {
842842
.tab-content {
843843
min-height: 140px;
844844
}
845845
}
846846

847-
#pgedit-render-area {
848-
border: 1px solid var(--ww-layout-border-color, #ddd);
849-
min-height: 400px;
850-
height: 600px;
851-
resize: vertical;
852-
display: flex;
853-
flex-direction: column;
854-
855-
@media only screen and (max-width: 992px) {
856-
min-height: 200px;
857-
height: 300px;
858-
}
859-
860-
#pgedit-render-iframe {
861-
flex-grow: 1;
862-
border: none;
863-
width: 100%;
864-
}
865-
}
866-
867847
// Fix the style of the save file path input group.
868848
// It is forced to be ltr, but the bootstrap rtl style makes that look wrong.
869849
/* rtl:raw:

0 commit comments

Comments
 (0)