|
|
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); |
|
|
} |
|
|
} |
|
|
|
|
|
|