Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
259 changes: 259 additions & 0 deletions assets/js/resizable-panels.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
/**
* Resizable Panels Feature
* Allows users to adjust the width of side panels (left sidebar and right TOC)
* Preferences are saved to localStorage and restored on page load
* Includes reset functionality to restore default widths
*/

(function() {
'use strict';

const STORAGE_KEY = 'layer5-docs-panel-widths';
const DEFAULT_WIDTHS = {
sidebar: 2, // col-xl-2 = ~16.66%
toc: 2, // col-xl-2 = ~16.66%
main: 8 // col-xl-8 = ~66.66%
};

// CSS class shortcuts for Bootstrap grid columns
const COL_CLASSES = {
'col-1': 8.33,
'col-2': 16.66,
'col-3': 25,
'col-4': 33.33,
'col-5': 41.66,
'col-6': 50,
'col-7': 58.33,
'col-8': 66.66,
'col-9': 75,
'col-10': 83.33,
'col-11': 91.66,
'col-12': 100
};
Comment on lines +19 to +32
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The COL_CLASSES object is defined but never used anywhere in the script. It should be removed to keep the code clean.


class ResizablePanels {
constructor() {
this.sidebar = document.querySelector('.td-sidebar');
this.toc = document.querySelector('.td-sidebar-toc');
this.main = document.querySelector('main[role="main"]');
this.row = document.querySelector('.row.flex-xl-nowrap');

if (!this.row || !this.sidebar || !this.main) {
console.warn('Resizable panels: Required elements not found');
return;
}

this.isResizing = false;
this.currentResizeTarget = null;
this.startX = 0;
this.startWidth = 0;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The property this.startWidth is initialized here but never used. The script uses this.startWidths (plural) for its logic instead. This line can be safely removed.


this.init();
}

init() {
// Load saved widths from localStorage
this.loadSavedWidths();

// Create resize handles
this.createResizeHandles();

// Add event listeners
this.addEventListeners();

// Add reset button
this.addResetButton();
}

loadSavedWidths() {
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
const widths = JSON.parse(saved);
this.applyWidths(widths);
}
} catch (error) {
console.error('Error loading saved panel widths:', error);
}
}

createResizeHandles() {
// Create resize handle between sidebar and main content
const sidebarHandle = document.createElement('div');
sidebarHandle.className = 'resizable-panel-handle resizable-panel-handle--right';
sidebarHandle.setAttribute('data-resize-target', 'sidebar');
sidebarHandle.setAttribute('title', 'Drag to resize sidebar');
this.sidebar.appendChild(sidebarHandle);

// Create resize handle for TOC (if it exists)
if (this.toc) {
const tocHandle = document.createElement('div');
tocHandle.className = 'resizable-panel-handle resizable-panel-handle--left';
tocHandle.setAttribute('data-resize-target', 'toc');
tocHandle.setAttribute('title', 'Drag to resize table of contents');
this.toc.appendChild(tocHandle);
}
}

addEventListeners() {
document.addEventListener('mousedown', (e) => this.onMouseDown(e));
document.addEventListener('mousemove', (e) => this.onMouseMove(e));
document.addEventListener('mouseup', (e) => this.onMouseUp(e));
}
Comment on lines +98 to +102
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The mousemove and mouseup event listeners are attached to the document globally and remain active even when the user is not interacting with the resize handles. For better performance, consider attaching these listeners only within onMouseDown and removing them in onMouseUp.


onMouseDown(e) {
if (!e.target.classList.contains('resizable-panel-handle')) {
return;
}

this.isResizing = true;
this.currentResizeTarget = e.target.getAttribute('data-resize-target');
this.startX = e.clientX;

// Store current widths for delta calculation
this.startWidths = this.getCurrentWidths();

// Add active state
e.target.classList.add('resizable-panel-handle--active');
document.body.style.userSelect = 'none';
document.body.style.cursor = 'col-resize';
}

onMouseMove(e) {
if (!this.isResizing) return;

const delta = e.clientX - this.startX;
const adjustment = delta / window.innerWidth; // Convert pixels to percentage-like ratio

let newWidths = { ...this.startWidths };

if (this.currentResizeTarget === 'sidebar') {
// Resizing left sidebar
const sidebarPercent = (this.startWidths.sidebar * 100) / 12; // Convert col units to percentage
const mainPercent = (this.startWidths.main * 100) / 12;

const newSidebarPercent = sidebarPercent + (adjustment * 100);
const newMainPercent = mainPercent - (adjustment * 100);

// Constrain widths: min 1 col, max 5 cols for sidebar; min 4 cols for main
if (newSidebarPercent >= 8.33 && newSidebarPercent <= 41.66 && newMainPercent >= 33.33) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These magic numbers (8.33, 41.66, 33.33) represent specific Bootstrap column percentages (1/12, 5/12, 4/12). It would be more maintainable to define these as named constants at the top of the file to clarify their meaning and make them easier to adjust if the layout constraints change.

newWidths.sidebar = Math.round((newSidebarPercent / 100) * 12);
newWidths.main = Math.round((newMainPercent / 100) * 12);
}
} else if (this.currentResizeTarget === 'toc') {
// Resizing right TOC panel
const tocPercent = (this.startWidths.toc * 100) / 12;
const mainPercent = (this.startWidths.main * 100) / 12;

const newTocPercent = tocPercent - (adjustment * 100);
const newMainPercent = mainPercent + (adjustment * 100);

// Constrain widths: min 1 col, max 5 cols for toc; min 4 cols for main
if (newTocPercent >= 8.33 && newTocPercent <= 41.66 && newMainPercent >= 33.33) {
newWidths.toc = Math.round((newTocPercent / 100) * 12);
newWidths.main = Math.round((newMainPercent / 100) * 12);
}
}

this.applyWidths(newWidths);
}

onMouseUp(e) {
if (!this.isResizing) return;

this.isResizing = false;
const handle = document.querySelector('.resizable-panel-handle--active');
if (handle) {
handle.classList.remove('resizable-panel-handle--active');
}

document.body.style.userSelect = '';
document.body.style.cursor = '';

// Save widths to localStorage
this.savePanelWidths();
}

applyWidths(widths) {
const { sidebar, toc, main } = widths;

// Update sidebar
this.removeBootstrapColClasses(this.sidebar);
this.sidebar.classList.add(`col-xl-${sidebar}`);

// Update main
this.removeBootstrapColClasses(this.main);
this.main.classList.add(`col-xl-${main}`);

// Update TOC if it exists
if (this.toc) {
this.removeBootstrapColClasses(this.toc);
this.toc.classList.add(`col-xl-${toc}`);
}
}

getCurrentWidths() {
const getColNumber = (element) => {
const classes = element.className.split(' ');
const colClass = classes.find(c => c.match(/col-xl-\d+/));
return colClass ? parseInt(colClass.split('-')[2]) : null;
};

return {
sidebar: getColNumber(this.sidebar) || DEFAULT_WIDTHS.sidebar,
toc: this.toc ? (getColNumber(this.toc) || DEFAULT_WIDTHS.toc) : DEFAULT_WIDTHS.toc,
main: getColNumber(this.main) || DEFAULT_WIDTHS.main
};
}

removeBootstrapColClasses(element) {
const classes = element.className.split(' ').filter(c => !c.match(/col-xl-\d+/));
element.className = classes.join(' ').trim();
}

savePanelWidths() {
try {
const widths = this.getCurrentWidths();
localStorage.setItem(STORAGE_KEY, JSON.stringify(widths));
} catch (error) {
console.error('Error saving panel widths:', error);
}
}

addResetButton() {
// Find the feature-info-container or page-header to add reset button
const pageHeader = document.querySelector('.page-header');
if (!pageHeader) return;

const resetButton = document.createElement('button');
resetButton.id = 'reset-panel-widths';
resetButton.className = 'btn btn-sm btn-outline-secondary ms-2';
resetButton.innerHTML = '<i class="bi bi-arrow-clockwise"></i> Reset Layout';
resetButton.setAttribute('title', 'Reset panel widths to default');

resetButton.addEventListener('click', () => this.resetPanelWidths());

// Find a good place to insert the button
const featureContainer = pageHeader.querySelector('.feature-info-container');
if (featureContainer) {
featureContainer.insertAdjacentElement('beforeend', resetButton);
} else {
pageHeader.insertAdjacentElement('beforeend', resetButton);
}
}

resetPanelWidths() {
this.applyWidths(DEFAULT_WIDTHS);
this.savePanelWidths();
}
}

// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
new ResizablePanels();
});
} else {
new ResizablePanels();
}
})();
Loading
Loading