Skip to content

Commit 4f74d6a

Browse files
authored
Merge pull request #37 from eviltester/17-moveable-size-bar-between-options-and-preview-panel
divider resize
2 parents d707beb + c0ee66b commit 4f74d6a

4 files changed

Lines changed: 278 additions & 1 deletion

File tree

packages/core-ui/js/gui_components/import-export-controls.js

Lines changed: 172 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ class ImportExportControls {
2424
constructor() {
2525
this.previewRowLimit = 10;
2626
this.textEditMode = 'preview';
27+
this.defaultOptionsPanelWidthPx = 272;
28+
this.minOptionsPanelWidthPx = 180;
29+
this.maxOptionsPanelWidthPx = 520;
30+
this.minPreviewPanelWidthPx = 220;
31+
this.currentOptionsPanelWidthPx = null;
32+
this._activeSplitDrag = null;
2733
}
2834

2935
addHTMLtoGui(parentelement) {
@@ -293,6 +299,7 @@ class ImportExportControls {
293299

294300
const edit_area = document.querySelector('div.edit-area');
295301
const optionsparent = document.querySelector('div.options-parent');
302+
const splitter = document.querySelector('div.options-preview-splitter');
296303
const text_area = document.getElementById('markdown');
297304

298305
edit_area.style.width = '100%';
@@ -308,6 +315,9 @@ class ImportExportControls {
308315
console.log('undefined panel type for ' + type);
309316
edit_area.style.display = 'block';
310317
optionsparent.style.display = 'none';
318+
if (splitter) {
319+
splitter.style.display = 'none';
320+
}
311321
text_area.style.width = '100%';
312322
text_area.style.height = '100%';
313323
return;
@@ -318,7 +328,8 @@ class ImportExportControls {
318328
text_area.style.width = '100%';
319329
text_area.style.height = '100%';
320330

321-
optionsparent.style.width = '17em';
331+
const initialWidth = this._clampOptionsPanelWidth(this._getInitialOptionsPanelWidthPx(), edit_area);
332+
this._setOptionsPanelWidth(optionsparent, initialWidth);
322333
optionsparent.style.height = '100%';
323334

324335
optionsparent.innerHTML = '';
@@ -337,6 +348,7 @@ class ImportExportControls {
337348
}
338349

339350
optionsparent.style.display = 'block';
351+
this._configureOptionsPreviewSplitter(edit_area, optionsparent, splitter, text_area);
340352
}
341353

342354
setOptionsApplyDirtyState(optionsparent, isDirty) {
@@ -476,6 +488,165 @@ class ImportExportControls {
476488
}
477489
importButton.disabled = this.isPreviewTextMode();
478490
}
491+
492+
_configureOptionsPreviewSplitter(editArea, optionsParent, splitter, textArea) {
493+
if (!splitter || !editArea || !optionsParent || !textArea) {
494+
return;
495+
}
496+
497+
splitter.style.display = 'block';
498+
textArea.style.flex = '1 1 auto';
499+
this._updateSplitterAriaValues(splitter, optionsParent, editArea);
500+
501+
if (splitter.dataset.splitterInitialised === 'true') {
502+
return;
503+
}
504+
505+
splitter.dataset.splitterInitialised = 'true';
506+
splitter.addEventListener('pointerdown', (event) => this._beginSplitterDrag(event, editArea, optionsParent));
507+
splitter.addEventListener('keydown', (event) =>
508+
this._handleSplitterKeyDown(event, optionsParent, editArea, splitter)
509+
);
510+
}
511+
512+
_beginSplitterDrag(event, editArea, optionsParent) {
513+
if (!event || event.button > 0) {
514+
return;
515+
}
516+
if (this._activeSplitDrag) {
517+
return;
518+
}
519+
520+
const pointerId = event.pointerId;
521+
const startX = event.clientX;
522+
const startWidth = this._readOptionsPanelWidthPx(optionsParent);
523+
524+
this._activeSplitDrag = {
525+
pointerId,
526+
startX,
527+
startWidth,
528+
editArea,
529+
optionsParent,
530+
};
531+
532+
event.preventDefault();
533+
document.body.classList.add('is-resizing-split');
534+
535+
const onMove = (moveEvent) => this._handleSplitterDragMove(moveEvent);
536+
const onEnd = (endEvent) => this._endSplitterDrag(endEvent, onMove, onEnd);
537+
document.addEventListener('pointermove', onMove);
538+
document.addEventListener('pointerup', onEnd);
539+
document.addEventListener('pointercancel', onEnd);
540+
}
541+
542+
_handleSplitterDragMove(event) {
543+
const dragState = this._activeSplitDrag;
544+
if (!dragState || event.pointerId !== dragState.pointerId) {
545+
return;
546+
}
547+
548+
event.preventDefault();
549+
const deltaX = event.clientX - dragState.startX;
550+
const requestedWidth = dragState.startWidth + deltaX;
551+
const boundedWidth = this._clampOptionsPanelWidth(requestedWidth, dragState.editArea);
552+
this._setOptionsPanelWidth(dragState.optionsParent, boundedWidth);
553+
const splitter = document.querySelector('div.options-preview-splitter');
554+
if (splitter) {
555+
this._updateSplitterAriaValues(splitter, dragState.optionsParent, dragState.editArea);
556+
}
557+
}
558+
559+
_endSplitterDrag(event, onMove, onEnd) {
560+
if (!this._activeSplitDrag) {
561+
return;
562+
}
563+
if (event && event.pointerId !== this._activeSplitDrag.pointerId) {
564+
return;
565+
}
566+
567+
document.removeEventListener('pointermove', onMove);
568+
document.removeEventListener('pointerup', onEnd);
569+
document.removeEventListener('pointercancel', onEnd);
570+
document.body.classList.remove('is-resizing-split');
571+
this._activeSplitDrag = null;
572+
}
573+
574+
_setOptionsPanelWidth(optionsParent, widthPx) {
575+
const safeWidth = Math.round(widthPx);
576+
optionsParent.style.width = `${safeWidth}px`;
577+
optionsParent.style.minWidth = `${safeWidth}px`;
578+
optionsParent.style.maxWidth = `${safeWidth}px`;
579+
optionsParent.style.flex = '0 0 auto';
580+
this.currentOptionsPanelWidthPx = safeWidth;
581+
}
582+
583+
_handleSplitterKeyDown(event, optionsParent, editArea, splitter) {
584+
if (!event) {
585+
return;
586+
}
587+
588+
const step = event.shiftKey ? 24 : 12;
589+
let requestedWidth = this._readOptionsPanelWidthPx(optionsParent);
590+
let handled = true;
591+
592+
if (event.key === 'ArrowLeft') {
593+
requestedWidth -= step;
594+
} else if (event.key === 'ArrowRight') {
595+
requestedWidth += step;
596+
} else if (event.key === 'Home') {
597+
requestedWidth = this.minOptionsPanelWidthPx;
598+
} else if (event.key === 'End') {
599+
requestedWidth = this.maxOptionsPanelWidthPx;
600+
} else {
601+
handled = false;
602+
}
603+
604+
if (!handled) {
605+
return;
606+
}
607+
608+
event.preventDefault();
609+
const boundedWidth = this._clampOptionsPanelWidth(requestedWidth, editArea);
610+
this._setOptionsPanelWidth(optionsParent, boundedWidth);
611+
this._updateSplitterAriaValues(splitter, optionsParent, editArea);
612+
}
613+
614+
_readOptionsPanelWidthPx(optionsParent) {
615+
const parsed = Number.parseFloat(optionsParent?.style?.width || '');
616+
if (Number.isFinite(parsed)) {
617+
return parsed;
618+
}
619+
return this._getInitialOptionsPanelWidthPx();
620+
}
621+
622+
_getInitialOptionsPanelWidthPx() {
623+
if (Number.isFinite(this.currentOptionsPanelWidthPx)) {
624+
return this.currentOptionsPanelWidthPx;
625+
}
626+
return this.defaultOptionsPanelWidthPx;
627+
}
628+
629+
_clampOptionsPanelWidth(widthPx, editArea) {
630+
const editWidth = editArea?.getBoundingClientRect?.().width || 0;
631+
const maxByContainer =
632+
editWidth > 0
633+
? Math.max(this.minOptionsPanelWidthPx, editWidth - this.minPreviewPanelWidthPx)
634+
: this.maxOptionsPanelWidthPx;
635+
const maxAllowed = Math.min(this.maxOptionsPanelWidthPx, maxByContainer);
636+
return Math.min(Math.max(widthPx, this.minOptionsPanelWidthPx), maxAllowed);
637+
}
638+
639+
_updateSplitterAriaValues(splitter, optionsParent, editArea) {
640+
if (!splitter) {
641+
return;
642+
}
643+
const min = this.minOptionsPanelWidthPx;
644+
const max = this._clampOptionsPanelWidth(this.maxOptionsPanelWidthPx, editArea);
645+
const now = this._clampOptionsPanelWidth(this._readOptionsPanelWidthPx(optionsParent), editArea);
646+
splitter.setAttribute('aria-valuemin', `${min}`);
647+
splitter.setAttribute('aria-valuemax', `${max}`);
648+
splitter.setAttribute('aria-valuenow', `${now}`);
649+
}
479650
}
480651

481652
export { ImportExportControls };

packages/core-ui/js/gui_components/tabbed-text-control.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ class TabbedTextControl {
5050
5151
<div class="edit-area">
5252
<div class="options-parent" style="display: none"></div>
53+
<div
54+
class="options-preview-splitter"
55+
style="display: none"
56+
role="separator"
57+
tabindex="0"
58+
aria-orientation="vertical"
59+
aria-label="Resize options panel"
60+
></div>
5361
<div id="markdown" style="height: 30%; width:100%;">
5462
<textarea class="textrepresentation" name="Markdown" id="markdownarea"></textarea>
5563
</div>

styles.css

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,61 @@ button:disabled{
208208
resize: vertical;
209209
}
210210

211+
.edit-area{
212+
display: flex;
213+
align-items: stretch;
214+
width: 100%;
215+
min-height: 14rem;
216+
}
217+
218+
.options-parent{
219+
overflow: auto;
220+
}
221+
222+
.options-preview-splitter{
223+
width: 8px;
224+
cursor: col-resize;
225+
background: #f5f8fa;
226+
border-left: 1px solid #d7e2e8;
227+
border-right: 1px solid #d7e2e8;
228+
flex: 0 0 8px;
229+
opacity: 0.7;
230+
transition: background-color 120ms ease, opacity 120ms ease;
231+
touch-action: none;
232+
-webkit-user-select: none;
233+
}
234+
235+
.options-preview-splitter:hover{
236+
background: #ebf2f6;
237+
opacity: 0.95;
238+
}
239+
240+
body.is-resizing-split{
241+
user-select: none;
242+
cursor: col-resize;
243+
}
244+
245+
#markdown{
246+
flex: 1 1 auto;
247+
min-width: 0;
248+
}
249+
250+
@media (max-width: 640px){
251+
.edit-area{
252+
flex-direction: column;
253+
}
254+
255+
.options-preview-splitter{
256+
display: none !important;
257+
}
258+
259+
.options-parent{
260+
width: 100% !important;
261+
min-width: 0 !important;
262+
max-width: 100% !important;
263+
}
264+
}
265+
211266
.testDataDefn{
212267
height: 10em;
213268
width:95%;

tests/utils/import-export-controls-mode.test.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ describe('ImportExportControls file reading and visibility', () => {
169169
<div id="markdown"></div>
170170
<div class="edit-area"></div>
171171
<div class="options-parent"></div>
172+
<div class="options-preview-splitter"></div>
172173
<div id="importExportRoot"></div>
173174
<button id="copyTextButton">Copy</button>
174175
</body></html>`);
@@ -335,4 +336,46 @@ describe('ImportExportControls file reading and visibility', () => {
335336
expect(controls.exporter.setOptionsForType).toHaveBeenCalledWith('csv', { delimiter: '|' });
336337
expect(applyButton.disabled).toBe(true);
337338
});
339+
340+
test('shows splitter when options panel is active and hides for unsupported format', () => {
341+
const splitter = document.querySelector('div.options-preview-splitter');
342+
const editArea = document.querySelector('div.edit-area');
343+
const textAreaContainer = document.getElementById('markdown');
344+
345+
controls.setOptionsViewForFormatType();
346+
347+
expect(splitter.style.display).toBe('block');
348+
expect(editArea.style.display).toBe('flex');
349+
expect(textAreaContainer.style.flex).toBe('1 1 auto');
350+
351+
document.querySelector('li.active-type a').setAttribute('data-type', 'unknown');
352+
controls.setOptionsViewForFormatType();
353+
354+
expect(splitter.style.display).toBe('none');
355+
});
356+
357+
test('splitter drag resizes options panel width and clamps to min/max', () => {
358+
controls.setOptionsViewForFormatType();
359+
const splitter = document.querySelector('div.options-preview-splitter');
360+
const optionsParent = document.querySelector('div.options-parent');
361+
const editArea = document.querySelector('div.edit-area');
362+
363+
editArea.getBoundingClientRect = () => ({ width: 1000 });
364+
365+
splitter.dispatchEvent(
366+
new dom.window.MouseEvent('pointerdown', { bubbles: true, button: 0, clientX: 300, pointerId: 1 })
367+
);
368+
document.dispatchEvent(new dom.window.MouseEvent('pointermove', { bubbles: true, clientX: 380, pointerId: 1 }));
369+
expect(Number.parseFloat(optionsParent.style.width)).toBeGreaterThan(272);
370+
expect(document.body.classList.contains('is-resizing-split')).toBe(true);
371+
372+
document.dispatchEvent(new dom.window.MouseEvent('pointermove', { bubbles: true, clientX: -500, pointerId: 1 }));
373+
expect(Number.parseFloat(optionsParent.style.width)).toBe(controls.minOptionsPanelWidthPx);
374+
375+
document.dispatchEvent(new dom.window.MouseEvent('pointermove', { bubbles: true, clientX: 9999, pointerId: 1 }));
376+
expect(Number.parseFloat(optionsParent.style.width)).toBe(controls.maxOptionsPanelWidthPx);
377+
378+
document.dispatchEvent(new dom.window.MouseEvent('pointerup', { bubbles: true, pointerId: 1 }));
379+
expect(document.body.classList.contains('is-resizing-split')).toBe(false);
380+
});
338381
});

0 commit comments

Comments
 (0)