UNPKG

rc-dock

Version:

dock layout for react component

686 lines (628 loc) 19.6 kB
import { BoxData, DockMode, DropDirection, LayoutData, maximePlaceHolderId, PanelData, placeHolderStyle, TabBase, TabData, TabGroup } from "./DockData"; let _watchObjectChange: WeakMap<any, any> = new WeakMap(); export function getUpdatedObject(obj: any): any { let result = _watchObjectChange.get(obj); if (result) { return getUpdatedObject(result); } return obj; } function clearObjectCache() { _watchObjectChange = new WeakMap(); } function clone<T>(value: T, extra?: any): T { let newValue: any = {...value, ...extra}; if (Array.isArray(newValue.tabs)) { newValue.tabs = newValue.tabs.concat(); } if (Array.isArray(newValue.children)) { newValue.children = newValue.children.concat(); } _watchObjectChange.set(value, newValue); return newValue; } let _idCount = 0; export function nextId() { ++_idCount; return `+${_idCount}`; } let _zCount = 0; export function nextZIndex(current?: number): number { if (current === _zCount) { // already the top return current; } return ++_zCount; } function findInPanel(panel: PanelData, id: string): PanelData | TabData { if (panel.id === id) { return panel; } for (let tab of panel.tabs) { if (tab.id === id) { return tab; } } return null; } function findInBox(box: BoxData, id: string): PanelData | TabData { let result: PanelData | TabData; for (let child of box.children) { if ('children' in child) { if (result = findInBox(child, id)) { break; } } else if ('tabs' in child) { if (result = findInPanel(child, id)) { break; } } } return result; } export function find(layout: LayoutData, id: string): PanelData | TabData { let result = findInBox(layout.dockbox, id); if (!result) { result = findInBox(layout.floatbox, id); } if (!result) { result = findInBox(layout.maxbox, id); } return result; } export function addNextToTab(layout: LayoutData, source: TabData | PanelData, target: TabData, direction: DropDirection): LayoutData { let pos = target.parent.tabs.indexOf(target); if (pos >= 0) { if (direction === 'after-tab') { ++pos; } return addTabToPanel(layout, source, target.parent, pos); } return layout; } export function addTabToPanel(layout: LayoutData, source: TabData | PanelData, panel: PanelData, idx = -1): LayoutData { if (idx === -1) { idx = panel.tabs.length; } let tabs: TabData[]; let activeId: string; if ('tabs' in source) { // source is PanelData tabs = source.tabs; activeId = source.activeId; } else { // source is TabData tabs = [source]; } if (tabs.length) { let newPanel = clone(panel); newPanel.tabs.splice(idx, 0, ...tabs); newPanel.activeId = tabs[tabs.length - 1].id; for (let tab of tabs) { tab.parent = newPanel; } if (activeId) { newPanel.activeId = activeId; } layout = replacePanel(layout, panel, newPanel); } return layout; } export function converToPanel(source: TabData | PanelData): PanelData { if ('tabs' in source) { // source is already PanelData return source; } else { let newPanel: PanelData = {tabs: [source], group: source.group, activeId: source.id}; source.parent = newPanel; return newPanel; } } export function dockPanelToPanel(layout: LayoutData, newPanel: PanelData, panel: PanelData, direction: DropDirection): LayoutData { let box = panel.parent; let dockMode: DockMode = (direction === 'left' || direction === 'right') ? 'horizontal' : 'vertical'; let afterPanel = (direction === 'bottom' || direction === 'right'); let pos = box.children.indexOf(panel); if (pos >= 0) { let newBox = clone(box); if (dockMode === box.mode) { if (afterPanel) { ++pos; } panel.size *= 0.5; newPanel.size = panel.size; newBox.children.splice(pos, 0, newPanel); } else { let newChildBox: BoxData = {mode: dockMode, children: []}; newChildBox.size = panel.size; if (afterPanel) { newChildBox.children = [panel, newPanel]; } else { newChildBox.children = [newPanel, panel]; } panel.parent = newChildBox; panel.size = 200; newPanel.parent = newChildBox; newPanel.size = 200; newBox.children[pos] = newChildBox; newChildBox.parent = newBox; } return replaceBox(layout, box, newBox); } return layout; } export function dockPanelToBox(layout: LayoutData, newPanel: PanelData, box: BoxData, direction: DropDirection): LayoutData { let parentBox = box.parent; let dockMode: DockMode = (direction === 'left' || direction === 'right') ? 'horizontal' : 'vertical'; let afterPanel = (direction === 'bottom' || direction === 'right'); if (parentBox) { let pos = parentBox.children.indexOf(box); if (pos >= 0) { let newParentBox = clone(parentBox); if (dockMode === parentBox.mode) { if (afterPanel) { ++pos; } newPanel.size = box.size * 0.3; box.size *= 0.7; newParentBox.children.splice(pos, 0, newPanel); } else { let newChildBox: BoxData = {mode: dockMode, children: []}; newChildBox.size = box.size; if (afterPanel) { newChildBox.children = [box, newPanel]; } else { newChildBox.children = [newPanel, box]; } box.parent = newChildBox; box.size = 280; newPanel.parent = newChildBox; newPanel.size = 120; newParentBox.children[pos] = newChildBox; } return replaceBox(layout, parentBox, newParentBox); } } else if (box === layout.dockbox) { let newBox = clone(box); if (dockMode === box.mode) { let pos = 0; if (afterPanel) { pos = newBox.children.length; } newPanel.size = box.size * 0.3; box.size *= 0.7; newBox.children.splice(pos, 0, newPanel); return replaceBox(layout, box, newBox); } else { // replace root dockbox let newDockBox: BoxData = {mode: dockMode, children: []}; newDockBox.size = box.size; if (afterPanel) { newDockBox.children = [newBox, newPanel]; } else { newDockBox.children = [newPanel, newBox]; } newBox.size = 280; newPanel.size = 120; return replaceBox(layout, box, newDockBox); } } else if (box === layout.maxbox) { let newBox = clone(box); newBox.children.push(newPanel); return replaceBox(layout, box, newBox); } return layout; } export function floatPanel( layout: LayoutData, newPanel: PanelData, rect?: {left: number, top: number, width: number, height: number} ): LayoutData { let newBox = clone(layout.floatbox); if (rect) { newPanel.x = rect.left; newPanel.y = rect.top; newPanel.w = rect.width; newPanel.h = rect.height; } newBox.children.push(newPanel); return replaceBox(layout, layout.floatbox, newBox); } export function removeFromLayout(layout: LayoutData, source: TabData | PanelData): LayoutData { if (source) { let panelData: PanelData; if ('tabs' in source) { panelData = source; layout = removePanel(layout, panelData); } else { panelData = source.parent; layout = removeTab(layout, source); } if (panelData && panelData.parent && panelData.parent.mode === 'maximize') { let newPanel = layout.maxbox.children[0] as PanelData; if (!newPanel || (newPanel.tabs.length === 0 && !newPanel.panelLock)) { // max panel is gone, remove the place holder let placeHolder = find(layout, maximePlaceHolderId) as PanelData; if (placeHolder) { return removePanel(layout, placeHolder); } } } } return layout; } function removePanel(layout: LayoutData, panel: PanelData): LayoutData { let box = panel.parent; if (box) { let pos = box.children.indexOf(panel); if (pos >= 0) { let newBox = clone(box); newBox.children.splice(pos, 1); return replaceBox(layout, box, newBox); } } return layout; } function removeTab(layout: LayoutData, tab: TabData): LayoutData { let panel = tab.parent; if (panel) { let pos = panel.tabs.indexOf(tab); if (pos >= 0) { let newPanel = clone(panel); newPanel.tabs.splice(pos, 1); if (newPanel.activeId === tab.id) { // update selection id if (newPanel.tabs.length > pos) { newPanel.activeId = newPanel.tabs[pos].id; } else if (newPanel.tabs.length) { newPanel.activeId = newPanel.tabs[0].id; } } return replacePanel(layout, panel, newPanel); } } return layout; } export function moveToFront(layout: LayoutData, source: TabData | PanelData): LayoutData { if (source) { let panelData: PanelData; let needUpdate = false; let changes: any = {}; if ('tabs' in source) { panelData = source; } else { panelData = source.parent; if (panelData.activeId !== source.id) { // move tab to front changes.activeId = source.id; needUpdate = true; } } if (panelData && panelData.parent && panelData.parent.mode === 'float') { // move float panel to front let newZ = nextZIndex(panelData.z); if (newZ !== panelData.z) { changes.z = newZ; needUpdate = true; } } if (needUpdate) { layout = replacePanel(layout, panelData, clone(panelData, changes)); } } return layout; } // maximize or restore the panel export function maximize(layout: LayoutData, source: TabData | PanelData): LayoutData { if (source) { if ('tabs' in source) { if (source.parent.mode === 'maximize') { return restorePanel(layout, source); } else { return maximizePanel(layout, source); } } else { return maximizeTab(layout, source); } } return layout; } function maximizePanel(layout: LayoutData, panel: PanelData): LayoutData { let maxbox = layout.maxbox; if (maxbox.children.length) { // invalid maximize return layout; } let placeHodlerPanel: PanelData = { ...panel, id: maximePlaceHolderId, tabs: [], panelLock: {} }; layout = replacePanel(layout, panel, placeHodlerPanel); layout = dockPanelToBox(layout, panel, layout.maxbox, 'middle'); return layout; } function restorePanel(layout: LayoutData, panel: PanelData): LayoutData { layout = removePanel(layout, panel); let placeHolder = find(layout, maximePlaceHolderId) as PanelData; if (placeHolder) { let {x, y, z, w, h} = placeHolder; panel = {...panel, x, y, z, w, h}; return replacePanel(layout, placeHolder, panel); } else { return dockPanelToBox(layout, panel, layout.dockbox, 'right'); } } function maximizeTab(layout: LayoutData, tab: TabData): LayoutData { // TODO to be implemented return layout; } // move float panel into the screen export function fixFloatPanelPos(layout: LayoutData, layoutWidth?: number, layoutHeight?: number): LayoutData { let layoutChanged = false; if (layout && layout.floatbox && layoutWidth > 200 && layoutHeight > 200) { let newFloatChildren = layout.floatbox.children.concat(); for (let i = 0; i < newFloatChildren.length; ++i) { let panel: PanelData = newFloatChildren[i] as PanelData; let panelChange: any = {}; if (panel.w > layoutWidth) { panelChange.w = layoutWidth; } if (panel.h > layoutHeight) { panelChange.h = layoutHeight; } if (panel.y > layoutHeight - 16) { panelChange.y = Math.max(layoutHeight - 16 - (panel.h >> 1), 0); } else if (panel.y < 0) { panelChange.y = 0; } if (panel.x + panel.w < 16) { panelChange.x = 16 - (panel.w >> 1); } else if (panel.x > layoutWidth - 16) { panelChange.x = layoutWidth - 16 - (panel.w >> 1); } if (Object.keys(panelChange).length) { newFloatChildren[i] = clone(panel, panelChange); layoutChanged = true; } } if (layoutChanged) { let newBox = clone(layout.floatbox); newBox.children = newFloatChildren; return replaceBox(layout, layout.floatbox, newBox); } } return layout; } export function fixLayoutData(layout: LayoutData, loadTab?: (tab: TabBase) => TabData): LayoutData { function fixpanelOrBox(d: PanelData | BoxData) { if (d.id == null) { d.id = nextId(); } else if (d.id.startsWith('+')) { let idnum = Number(d.id); if (idnum > _idCount) { // make sure generated id is unique _idCount = idnum; } } if (!(d.size >= 0)) { d.size = 200; } d.minWidth = 0; d.minHeight = 0; } function fixPanelData(panel: PanelData): PanelData { fixpanelOrBox(panel); let findActiveId = false; if (loadTab) { for (let i = 0; i < panel.tabs.length; ++i) { panel.tabs[i] = loadTab(panel.tabs[i]); } } for (let child of panel.tabs) { child.parent = panel; if (child.id === panel.activeId) { findActiveId = true; } if (child.minWidth > panel.minWidth) panel.minWidth = child.minWidth; if (child.minHeight > panel.minHeight) panel.minHeight = child.minHeight; } if (!findActiveId && panel.tabs.length) { panel.activeId = panel.tabs[0].id; } if (panel.minWidth <= 0) { panel.minWidth = 1; } if (panel.minHeight <= 0) { panel.minHeight = 1; } if (panel.panelLock) { if (panel.minWidth < panel.panelLock.minWidth) { panel.minWidth = panel.panelLock.minWidth; } if (panel.minHeight < panel.panelLock.minHeight) { panel.minHeight = panel.panelLock.minHeight; } } if (panel.group == null && panel.tabs.length) { panel.group = panel.tabs[0].group; } if (panel.z > _zCount) { // make sure next zIndex is on top _zCount = panel.z; } return panel; } function fixBoxData(box: BoxData): BoxData { fixpanelOrBox(box); for (let i = 0; i < box.children.length; ++i) { let child = box.children[i]; child.parent = box; if ('children' in child) { fixBoxData(child); if (child.children.length === 0) { // remove box with no child box.children.splice(i, 1); --i; } else if (child.children.length === 1) { // box with one child should be merged back to parent box let subChild = child.children[0]; if ((subChild as BoxData).mode === box.mode) { // sub child is another box that can be merged into current box let totalSubSize = 0; for (let subsubChild of (subChild as BoxData).children) { totalSubSize += subsubChild.size; } let sizeScale = child.size / totalSubSize; for (let subsubChild of (subChild as BoxData).children) { subsubChild.size *= sizeScale; } // merge children up box.children.splice(i, 1, ...(subChild as BoxData).children); } else { // sub child can be moved up one layer subChild.size = child.size; box.children[i] = subChild; } --i; } } else if ('tabs' in child) { fixPanelData(child); if (child.tabs.length === 0) { // remove panel with no tab if (!child.panelLock) { box.children.splice(i, 1); --i; } else if (child.group === placeHolderStyle && (box.children.length > 1 || box.parent)) { // remove placeHolder Group box.children.splice(i, 1); --i; } } } // merge min size switch (box.mode) { case 'horizontal': if (child.minWidth > 0) box.minWidth += child.minWidth; if (child.minHeight > box.minHeight) box.minHeight = child.minHeight; break; case 'vertical': if (child.minWidth > box.minWidth) box.minWidth = child.minWidth; if (child.minHeight > 0) box.minHeight += child.minHeight; break; } } // add divider size if (box.children.length > 1) { switch (box.mode) { case 'horizontal': box.minWidth += (box.children.length - 1) * 4; break; case 'vertical': box.minHeight += (box.children.length - 1) * 4; break; } } return box; } if (!('floatbox' in layout)) { layout.floatbox = {mode: 'float', children: [], size: 1}; } else { layout.floatbox.mode = 'float'; } if (!('maxbox' in layout)) { layout.maxbox = {mode: 'maximize', children: [], size: 1}; } else { layout.maxbox.mode = 'maximize'; } fixBoxData(layout.dockbox); fixBoxData(layout.floatbox); fixBoxData(layout.maxbox); if (layout.dockbox.children.length === 0) { // add place holder panel when root box is empty let newPanel: PanelData = {id: '+0', group: placeHolderStyle, panelLock: {}, size: 200, tabs: []}; newPanel.parent = layout.dockbox; layout.dockbox.children.push(newPanel); } else { // merge and replace root box when box has only one child while (layout.dockbox.children.length === 1 && 'children' in layout.dockbox.children[0]) { let newDockBox = clone(layout.dockbox.children[0] as BoxData); layout.dockbox = newDockBox; for (let child of newDockBox.children) { child.parent = newDockBox; } } } layout.dockbox.parent = null; layout.floatbox.parent = null; layout.maxbox.parent = null; clearObjectCache(); return layout; } function replacePanel(layout: LayoutData, panel: PanelData, newPanel: PanelData): LayoutData { for (let tab of newPanel.tabs) { tab.parent = newPanel; } let box = panel.parent; if (box) { let pos = box.children.indexOf(panel); if (pos >= 0) { let newBox = clone(box); newBox.children[pos] = newPanel; return replaceBox(layout, box, newBox); } } return layout; } function replaceBox(layout: LayoutData, box: BoxData, newBox: BoxData): LayoutData { for (let child of newBox.children) { child.parent = newBox; } let parentBox = box.parent; if (parentBox) { let pos = parentBox.children.indexOf(box); if (pos >= 0) { let newParentBox = clone(parentBox); newParentBox.children[pos] = newBox; return replaceBox(layout, parentBox, newParentBox); } } else { if (box.id === layout.dockbox.id || box === layout.dockbox) { return {...layout, dockbox: newBox}; } else if (box.id === layout.floatbox.id || box === layout.floatbox) { return {...layout, floatbox: newBox}; } else if (box.id === layout.maxbox.id || box === layout.maxbox) { return {...layout, maxbox: newBox}; } } return layout; } export function getFloatPanelSize(panel: HTMLElement, tabGroup: TabGroup) { if (!panel) { return [300, 300]; } let panelWidth = panel.offsetWidth; let panelHeight = panel.offsetHeight; let [minWidth, maxWidth] = tabGroup.preferredFloatWidth || [100, 600]; let [minHeight, maxHeight] = tabGroup.preferredFloatHeight || [50, 500]; if (!(panelWidth >= minWidth)) { panelWidth = minWidth; } else if (!(panelWidth <= maxWidth)) { panelWidth = maxWidth; } if (!(panelHeight >= minHeight)) { panelHeight = minHeight; } else if (!(panelHeight <= maxHeight)) { panelHeight = maxHeight; } return [panelWidth, panelHeight]; }