rc-dock
Version:
dock layout for react component
540 lines (539 loc) • 21 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DockLayout = void 0;
const React = __importStar(require("react"));
const ReactDOM = __importStar(require("react-dom"));
const debounce_1 = __importDefault(require("lodash/debounce"));
const DockData_1 = require("./DockData");
const DockBox_1 = require("./DockBox");
const FloatBox_1 = require("./FloatBox");
const DockPanel_1 = require("./DockPanel");
const Algorithm = __importStar(require("./Algorithm"));
const Serializer = __importStar(require("./Serializer"));
const DragManager = __importStar(require("./dragdrop/DragManager"));
const MaxBox_1 = require("./MaxBox");
const WindowBox_1 = require("./WindowBox");
class DockPortalManager extends React.PureComponent {
constructor() {
super(...arguments);
/** @ignore */
this._caches = new Map();
this._isMounted = false;
this.destroyRemovedPane = () => {
this._pendingDestroy = null;
let cacheRemoved = false;
for (let [id, cache] of this._caches) {
if (cache.owner == null) {
this._caches.delete(id);
cacheRemoved = true;
}
}
if (cacheRemoved && this._isMounted) {
this.forceUpdate();
}
};
}
/** @ignore */
getTabCache(id, owner) {
let cache = this._caches.get(id);
if (!cache) {
let div = document.createElement('div');
div.className = 'dock-pane-cache';
cache = { div, id, owner };
this._caches.set(id, cache);
}
else {
cache.owner = owner;
}
return cache;
}
/** @ignore */
removeTabCache(id, owner) {
let cache = this._caches.get(id);
if (cache && cache.owner === owner) {
cache.owner = null;
if (!this._pendingDestroy) {
// it could be reused by another component, so let's wait
this._pendingDestroy = setTimeout(this.destroyRemovedPane, 1);
}
}
}
/** @ignore */
updateTabCache(id, children) {
let cache = this._caches.get(id);
if (cache) {
cache.portal = ReactDOM.createPortal(children, cache.div, cache.id);
this.forceUpdate();
}
}
}
class DockLayout extends DockPortalManager {
constructor(props) {
var _a;
super(props);
/** @ignore */
this.getRef = (r) => {
this._ref = r;
};
/** @ignore */
this.onDragStateChange = (draggingScope) => {
if (draggingScope == null) {
DockPanel_1.DockPanel.droppingPanel = null;
if (this.state.dropRect) {
this.setState({ dropRect: null });
}
}
};
this._onWindowResize = debounce_1.default(() => {
let layout = this.getLayout();
if (this._ref) {
let newLayout = Algorithm.fixFloatPanelPos(layout, this._ref.offsetWidth, this._ref.offsetHeight);
if (layout !== newLayout) {
newLayout = Algorithm.fixLayoutData(newLayout, this.props.groups); // panel parent might need a fix
this.changeLayout(newLayout, null, 'move');
}
}
}, 200);
let { layout, defaultLayout, loadTab } = props;
let preparedLayout;
if (defaultLayout) {
preparedLayout = this.prepareInitData(props.defaultLayout);
}
else if (!loadTab) {
throw new Error('DockLayout.loadTab and DockLayout.defaultLayout should not both be undefined.');
}
if (layout) {
// controlled layout
this.state = {
layout: DockLayout.loadLayoutData(layout, props),
dropRect: null,
};
}
else {
this.state = {
layout: preparedLayout,
dropRect: null,
};
}
DragManager.addDragStateListener(this.onDragStateChange);
(_a = globalThis.addEventListener) === null || _a === void 0 ? void 0 : _a.call(globalThis, 'resize', this._onWindowResize);
}
/** @ignore */
getRootElement() {
return this._ref;
}
/** @ignore */
prepareInitData(data) {
let layout = Object.assign({}, data);
Algorithm.fixLayoutData(layout, this.props.groups, this.props.loadTab);
return layout;
}
/** @ignore */
getDockId() {
return this.props.dockId || this;
}
/** @inheritDoc */
getGroup(name) {
if (name) {
let { groups } = this.props;
if (groups && name in groups) {
return groups[name];
}
if (name === DockData_1.placeHolderStyle) {
return DockData_1.placeHolderGroup;
}
}
return DockData_1.defaultGroup;
}
/**
* @inheritDoc
* @param source @inheritDoc
* @param target @inheritDoc
* @param direction @inheritDoc
* @param floatPosition @inheritDoc
*/
dockMove(source, target, direction, floatPosition) {
let layout = this.getLayout();
if (direction === 'maximize') {
layout = Algorithm.maximize(layout, source);
this.panelToFocus = source.id;
}
else if (direction === 'front') {
layout = Algorithm.moveToFront(layout, source);
}
else {
layout = Algorithm.removeFromLayout(layout, source);
}
if (typeof target === 'string') {
target = this.find(target, Algorithm.Filter.All);
}
else {
target = Algorithm.getUpdatedObject(target); // target might change during removeTab
}
if (direction === 'float') {
let newPanel = Algorithm.converToPanel(source);
newPanel.z = Algorithm.nextZIndex(null);
if (this.state.dropRect || floatPosition) {
layout = Algorithm.floatPanel(layout, newPanel, this.state.dropRect || floatPosition);
}
else {
layout = Algorithm.floatPanel(layout, newPanel);
if (this._ref) {
layout = Algorithm.fixFloatPanelPos(layout, this._ref.offsetWidth, this._ref.offsetHeight);
}
}
}
else if (direction === 'new-window') {
let newPanel = Algorithm.converToPanel(source);
layout = Algorithm.panelToWindow(layout, newPanel);
}
else if (target) {
if ('tabs' in target) {
// panel target
if (direction === 'middle') {
layout = Algorithm.addTabToPanel(layout, source, target);
}
else {
let newPanel = Algorithm.converToPanel(source);
layout = Algorithm.dockPanelToPanel(layout, newPanel, target, direction);
}
}
else if ('children' in target) {
// box target
let newPanel = Algorithm.converToPanel(source);
layout = Algorithm.dockPanelToBox(layout, newPanel, target, direction);
}
else {
// tab target
layout = Algorithm.addNextToTab(layout, source, target, direction);
}
}
if (layout !== this.getLayout()) {
layout = Algorithm.fixLayoutData(layout, this.props.groups);
const currentTabId = source.hasOwnProperty('tabs') ? source.activeId : source.id;
this.changeLayout(layout, currentTabId, direction);
}
this.onDragStateChange(false);
}
/** @inheritDoc */
find(id, filter) {
return Algorithm.find(this.getLayout(), id, filter);
}
/** @ignore */
getLayoutSize() {
if (this._ref) {
return { width: this._ref.offsetWidth, height: this._ref.offsetHeight };
}
return { width: 0, height: 0 };
}
/** @inheritDoc */
updateTab(id, newTab, makeActive = true) {
var _a;
let tab = this.find(id, Algorithm.Filter.AnyTab);
if (!tab) {
return false;
}
let panelData = tab.parent;
let idx = panelData.tabs.indexOf(tab);
if (idx >= 0) {
let { loadTab } = this.props;
let layout = this.getLayout();
if (newTab) {
let activeId = panelData.activeId;
if (loadTab && !('content' in newTab && 'title' in newTab)) {
newTab = loadTab(newTab);
}
layout = Algorithm.removeFromLayout(layout, tab); // remove old tab
panelData = Algorithm.getUpdatedObject(panelData); // panelData might change during removeTab
layout = Algorithm.addTabToPanel(layout, newTab, panelData, idx); // add new tab
panelData = Algorithm.getUpdatedObject(panelData); // panelData might change during addTabToPanel
if (!makeActive) {
// restore the previous activeId
panelData.activeId = activeId;
this.panelToFocus = panelData.id;
}
}
else if (makeActive && panelData.activeId !== id) {
layout = Algorithm.replacePanel(layout, panelData, Object.assign(Object.assign({}, panelData), { activeId: id }));
}
layout = Algorithm.fixLayoutData(layout, this.props.groups);
this.changeLayout(layout, (_a = newTab === null || newTab === void 0 ? void 0 : newTab.id) !== null && _a !== void 0 ? _a : id, 'update');
return true;
}
}
/** @inheritDoc */
navigateToPanel(fromElement, direction) {
if (!direction) {
if (!fromElement) {
fromElement = this._ref.querySelector('.dock-tab-active>.dock-tab-btn');
}
fromElement.focus();
return;
}
let targetTab;
// use panel rect when move left/right, and use tabbar rect for up/down
let selector = (direction === 'ArrowUp' || direction === 'ArrowDown') ?
'.dock>.dock-bar' : '.dock-box>.dock-panel';
let panels = Array.from(this._ref.querySelectorAll(selector));
let currentPanel = panels.find((panel) => panel.contains(fromElement));
let currentRect = currentPanel.getBoundingClientRect();
let matches = [];
for (let panel of panels) {
if (panel !== currentPanel) {
let rect = panel.getBoundingClientRect();
let distance = Algorithm.findNearestPanel(currentRect, rect, direction);
if (distance >= 0) {
matches.push({ panel, rect, distance });
}
}
}
matches.sort((a, b) => a.distance - b.distance);
for (let match of matches) {
targetTab = match.panel.querySelector('.dock-tab-active>.dock-tab-btn');
if (targetTab) {
break;
}
}
if (targetTab) {
targetTab.focus();
}
}
/** @ignore */
useEdgeDrop() {
return this.props.dropMode === 'edge';
}
/** @ignore */
setDropRect(element, direction, source, event, panelSize = [300, 300]) {
let { dropRect } = this.state;
if (dropRect) {
if (direction === 'remove') {
this.setState((oldStates) => {
if (oldStates.dropRect.source === source) {
return { dropRect: null };
}
return {};
});
return;
}
else if (dropRect.element === element && dropRect.direction === direction && direction !== 'float') {
// skip duplicated update except for float dragging
return;
}
}
if (!element) {
this.setState({ dropRect: null });
return;
}
let layoutRect = this._ref.getBoundingClientRect();
let scaleX = this._ref.offsetWidth / layoutRect.width;
let scaleY = this._ref.offsetHeight / layoutRect.height;
let elemRect = element.getBoundingClientRect();
let left = (elemRect.left - layoutRect.left) * scaleX;
let top = (elemRect.top - layoutRect.top) * scaleY;
let width = elemRect.width * scaleX;
let height = elemRect.height * scaleY;
let ratio = 0.5;
if (element.classList.contains('dock-box')) {
ratio = 0.3;
}
switch (direction) {
case 'float': {
let x = (event.clientX - layoutRect.left) * scaleX;
let y = (event.clientY - layoutRect.top) * scaleY;
top = y - 15;
width = panelSize[0];
height = panelSize[1];
left = x - (width >> 1);
break;
}
case 'right':
left += width * (1 - ratio);
case 'left': // tslint:disable-line no-switch-case-fall-through
width *= ratio;
break;
case 'bottom':
top += height * (1 - ratio);
case 'top': // tslint:disable-line no-switch-case-fall-through
height *= ratio;
break;
case 'after-tab':
left += width - 15;
width = 30;
break;
case 'before-tab':
left -= 15;
width = 30;
break;
}
this.setState({ dropRect: { left, top, width, height, element, source, direction } });
}
/** @ignore */
render() {
// clear tempLayout
this.tempLayout = null;
let { style, maximizeTo } = this.props;
let { layout, dropRect } = this.state;
let dropRectStyle;
if (dropRect) {
let { element, direction } = dropRect, rect = __rest(dropRect, ["element", "direction"]);
dropRectStyle = Object.assign(Object.assign({}, rect), { display: 'block' });
if (direction === 'float') {
dropRectStyle.transition = 'none';
}
}
let maximize;
// if (layout.maxbox && layout.maxbox.children.length === 1) {
if (maximizeTo) {
if (typeof maximizeTo === 'string') {
maximizeTo = document.getElementById(maximizeTo);
}
maximize = ReactDOM.createPortal(React.createElement(MaxBox_1.MaxBox, { boxData: layout.maxbox }), maximizeTo);
}
else {
maximize = React.createElement(MaxBox_1.MaxBox, { boxData: layout.maxbox });
}
// }
let portals = [];
for (let [key, cache] of this._caches) {
if (cache.portal) {
portals.push(cache.portal);
}
}
return (React.createElement("div", { ref: this.getRef, className: "dock-layout", style: style },
React.createElement(DockData_1.DockContextProvider, { value: this },
React.createElement(DockBox_1.DockBox, { size: 1, boxData: layout.dockbox }),
React.createElement(FloatBox_1.FloatBox, { boxData: layout.floatbox }),
React.createElement(WindowBox_1.WindowBox, { boxData: layout.windowbox }),
maximize,
portals),
React.createElement("div", { className: "dock-drop-indicator", style: dropRectStyle })));
}
/** @ignore */
componentDidMount() {
this._isMounted = true;
}
/** @ignore
* move focus to panelToFocus
*/
componentDidUpdate(prevProps, prevState, snapshot) {
var _a;
if (this.panelToFocus) {
let panel = this._ref.querySelector(`.dock-panel[data-dockid="${this.panelToFocus}"]`);
if (panel && !panel.contains(this._ref.ownerDocument.activeElement)) {
(_a = panel.querySelector('.dock-bar')) === null || _a === void 0 ? void 0 : _a.focus();
}
this.panelToFocus = null;
}
}
/** @ignore */
componentWillUnmount() {
var _a;
(_a = globalThis.removeEventListener) === null || _a === void 0 ? void 0 : _a.call(globalThis, 'resize', this._onWindowResize);
DragManager.removeDragStateListener(this.onDragStateChange);
this._onWindowResize.cancel();
this._isMounted = false;
}
setLayout(layout) {
this.tempLayout = layout;
this.setState({ layout });
}
getLayout() {
return this.tempLayout || this.state.layout;
}
/** @ignore
* change layout
*/
changeLayout(layoutData, currentTabId, direction, silent = false) {
let { layout, onLayoutChange } = this.props;
let savedLayout;
if (onLayoutChange) {
savedLayout = Serializer.saveLayoutData(layoutData, this.props.saveTab, this.props.afterPanelSaved);
layoutData.loadedFrom = savedLayout;
onLayoutChange(savedLayout, currentTabId, direction);
if (layout) {
// if layout prop is defined, we need to force an update to make sure it's either updated or reverted back
this.forceUpdate();
}
}
if (!layout && !silent) {
// uncontrolled layout when Props.layout is not defined
this.setLayout(layoutData);
}
}
/** @ignore
* some layout change were handled by component silently
* but they should still call this function to trigger onLayoutChange
*/
onSilentChange(currentTabId = null, direction) {
let { onLayoutChange } = this.props;
if (onLayoutChange) {
let layout = this.getLayout();
this.changeLayout(layout, currentTabId, direction, true);
}
}
// public api
saveLayout() {
return Serializer.saveLayoutData(this.getLayout(), this.props.saveTab, this.props.afterPanelSaved);
}
/**
* load layout
* calling this api won't trigger the [[LayoutProps.onLayoutChange]] callback
*/
loadLayout(savedLayout) {
this.setLayout(DockLayout.loadLayoutData(savedLayout, this.props, this._ref.offsetWidth, this._ref.offsetHeight));
}
/** @ignore */
static loadLayoutData(savedLayout, props, width = 0, height = 0) {
let { defaultLayout, loadTab, afterPanelLoaded, groups } = props;
let layout = Serializer.loadLayoutData(savedLayout, defaultLayout, loadTab, afterPanelLoaded);
layout = Algorithm.fixFloatPanelPos(layout, width, height);
layout = Algorithm.fixLayoutData(layout, groups);
layout.loadedFrom = savedLayout;
return layout;
}
static getDerivedStateFromProps(props, state) {
let { layout: layoutToLoad } = props;
let { layout: currentLayout } = state;
if (layoutToLoad && layoutToLoad !== currentLayout.loadedFrom) {
// auto reload on layout prop change
return {
layout: DockLayout.loadLayoutData(layoutToLoad, props),
};
}
return null;
}
}
exports.DockLayout = DockLayout;