| import * as d3 from 'd3'; |
| import { isNarrowScreen } from '../utils/responsive'; |
|
|
| export type LayoutState = { |
| sidebar: { |
| width: number; |
| visible: boolean; |
| }; |
| }; |
|
|
| export type LayoutControllerOptions = { |
| sidebarState: LayoutState['sidebar']; |
| sideBar: d3.Selection<any, unknown, any, any>; |
| sidebarBtn: d3.Selection<any, unknown, any, any>; |
| onSidebarToggle?: (visible: boolean) => void; |
| onLayoutChange?: () => void; |
| }; |
|
|
| export class LayoutController { |
| private options: LayoutControllerOptions; |
| private isResizing = false; |
| private startX = 0; |
| private startWidth = 0; |
| private leftPanelRatio = 0.5; |
|
|
| constructor(options: LayoutControllerOptions) { |
| this.options = options; |
| this.initialize(); |
| } |
|
|
| private initialize(): void { |
| this.setupSidebar(); |
| this.setupWindowResize(); |
| this.setupPanelResizer(); |
| this.reLayout(window.innerWidth, window.innerHeight); |
| } |
|
|
| private setupSidebar(): void { |
| this.options.sidebarBtn.on('click', () => { |
| const sb = this.options.sidebarState; |
| sb.visible = !sb.visible; |
| |
| this.options.sidebarBtn.classed('on', sb.visible); |
| this.options.sideBar.classed('hidden', !sb.visible); |
| this.options.sideBar.style('right', |
| sb.visible ? null : `-${this.options.sidebarState.width}px`); |
|
|
| if (this.options.onSidebarToggle) { |
| this.options.onSidebarToggle(sb.visible); |
| } |
| |
| this.reLayout(); |
| }); |
| } |
|
|
| private setupWindowResize(): void { |
| window.onresize = () => { |
| const w = window.innerWidth; |
| const h = window.innerHeight; |
| this.reLayout(w, h); |
| if (this.options.onLayoutChange) { |
| this.options.onLayoutChange(); |
| } |
| }; |
| } |
|
|
| public reLayout(w = window.innerWidth, h = window.innerHeight): void { |
| d3.selectAll('.sidenav') |
| .style('height', (h - 53) + 'px'); |
|
|
| const sb = this.options.sidebarState; |
| const mainWidth = w - (sb.visible ? sb.width : 0); |
| |
| |
| const isMobile = isNarrowScreen(); |
| const mainFrame = d3.selectAll('.main_frame'); |
| |
| if (isMobile) { |
| |
| mainFrame |
| .style('height', null) |
| .style('width', null); |
| } else { |
| |
| mainFrame |
| .style('height', (h - 53) + 'px') |
| .style('width', mainWidth + 'px'); |
| |
| |
| this.updateLeftPanelWidth(mainWidth); |
| } |
| } |
| |
| |
| |
| |
| private updateLeftPanelWidth(containerWidth: number): void { |
| const leftPanel = d3.select('.left_panel'); |
| if (leftPanel.empty()) return; |
| |
| |
| const availableWidth = containerWidth - 8; |
| |
| |
| const leftWidth = availableWidth * this.leftPanelRatio; |
| |
| |
| const minWidth = containerWidth * 0.1; |
| const maxWidth = containerWidth * 0.9; |
| const clampedWidth = Math.max(minWidth, Math.min(maxWidth, leftWidth)); |
| |
| |
| this.leftPanelRatio = clampedWidth / availableWidth; |
| |
| leftPanel.style('flex-basis', clampedWidth + 'px'); |
| } |
|
|
| private setupPanelResizer(): void { |
| const resizer = d3.select('#resizer'); |
| const leftPanel = d3.select('.left_panel'); |
| |
| |
| const sb = this.options.sidebarState; |
| const mainWidth = window.innerWidth - (sb.visible ? sb.width : 0); |
| this.updateLeftPanelWidth(mainWidth); |
|
|
| resizer.on('mousedown', (event: MouseEvent) => { |
| event.preventDefault(); |
| event.stopPropagation(); |
| |
| this.isResizing = true; |
| this.startX = event.clientX; |
| |
| |
| const currentFlexBasis = leftPanel.style('flex-basis'); |
| this.startWidth = parseInt(currentFlexBasis) || (mainWidth * this.leftPanelRatio); |
| |
| d3.select('body') |
| .style('cursor', 'col-resize') |
| .style('user-select', 'none'); |
| |
| d3.select(window) |
| .on('mousemove.resizer', (ev: MouseEvent) => this.handleMouseMove(ev, leftPanel)) |
| .on('mouseup.resizer', () => this.handleMouseUp()); |
| }); |
| } |
|
|
| private handleMouseMove(event: MouseEvent, leftPanel: d3.Selection<any, unknown, any, any>): void { |
| if (!this.isResizing) return; |
| |
| event.preventDefault(); |
| |
| const sb = this.options.sidebarState; |
| const containerWidth = window.innerWidth - (sb.visible ? sb.width : 0); |
| const availableWidth = containerWidth - 8; |
| |
| const deltaX = event.clientX - this.startX; |
| const newWidth = Math.max( |
| containerWidth * 0.1, |
| Math.min(containerWidth * 0.9, this.startWidth + deltaX) |
| ); |
| |
| |
| leftPanel.style('flex-basis', newWidth + 'px'); |
| |
| |
| this.leftPanelRatio = newWidth / availableWidth; |
| } |
|
|
| private handleMouseUp(): void { |
| if (!this.isResizing) return; |
| |
| this.isResizing = false; |
| |
| d3.select('body') |
| .style('cursor', 'default') |
| .style('user-select', 'auto'); |
| |
| d3.select(window) |
| .on('mousemove.resizer', null) |
| .on('mouseup.resizer', null); |
| } |
| } |
|
|
|
|