@lonli-lokli/react-mosaic-component
Version:
A React Tiling Window Manager
410 lines (409 loc) • 14 kB
JavaScript
// libs/react-mosaic-component/src/lib/MosaicWindow.tsx
import classNames from "classnames";
import { drop, isEqual, values } from "lodash-es";
import React, { useContext } from "react";
import {
useDrag,
useDrop
} from "react-dnd";
import {
DEFAULT_PANEL_CONTROLS_IN_TABS,
DEFAULT_PANEL_CONTROLS_WITHOUT_CREATION,
DEFAULT_PANEL_CONTROLS_WITH_CREATION
} from "./buttons/defaultToolbarControls.mjs";
import { Separator } from "./buttons/Separator.mjs";
import { MosaicContext, MosaicWindowContext } from "./contextTypes.mjs";
import {
MosaicDropTargetPosition
} from "./internalTypes.mjs";
import { MosaicDropTarget } from "./MosaicDropTarget.mjs";
import {
MosaicDragType
} from "./types.mjs";
import { createDragToUpdates } from "./util/mosaicUpdates.mjs";
import {
getNodeAtPath,
getParentNode,
isTabsNode
} from "./util/mosaicUtilities.mjs";
import { OptionalBlueprint } from "./util/OptionalBlueprint.mjs";
var InternalMosaicWindow = class extends React.Component {
static defaultProps = {
additionalControlButtonText: "More",
draggable: true,
renderPreview: ({ title }) => /* @__PURE__ */ React.createElement("div", { className: "mosaic-preview" }, /* @__PURE__ */ React.createElement("div", { className: "mosaic-window-toolbar" }, /* @__PURE__ */ React.createElement("div", { className: "mosaic-window-title" }, title)), /* @__PURE__ */ React.createElement("div", { className: "mosaic-window-body" }, /* @__PURE__ */ React.createElement("h4", null, title), /* @__PURE__ */ React.createElement(
OptionalBlueprint.Icon,
{
className: "default-preview-icon",
size: "large",
icon: "APPLICATION"
}
))),
renderToolbar: null
};
static contextType = MosaicContext;
state = {
additionalControlsOpen: false
};
rootElement = null;
render() {
const {
className,
isOver,
renderPreview,
additionalControls,
connectDropTarget,
connectDragPreview,
draggedMosaicId,
disableAdditionalControlsOverlay
} = this.props;
const root = this.context.mosaicActions.getRoot();
const parentNode = getParentNode(root, this.props.path);
const isInTabContainer = isTabsNode(parentNode);
return /* @__PURE__ */ React.createElement(MosaicWindowContext.Provider, { value: this.childContext }, connectDropTarget(
/* @__PURE__ */ React.createElement(
"div",
{
className: classNames(
"mosaic-window",
"mosaic-drop-target",
className,
{
"drop-target-hover": isOver && draggedMosaicId === this.context.mosaicId,
"additional-controls-open": this.state.additionalControlsOpen
}
),
ref: (element) => {
this.rootElement = element;
}
},
this.renderToolbar(),
/* @__PURE__ */ React.createElement("div", { className: "mosaic-window-body" }, this.props.children),
!disableAdditionalControlsOverlay && /* @__PURE__ */ React.createElement(
"div",
{
className: "mosaic-window-body-overlay",
onClick: () => {
this.setAdditionalControlsOpen(false);
}
}
),
/* @__PURE__ */ React.createElement("div", { className: "mosaic-window-additional-actions-bar" }, additionalControls),
connectDragPreview(renderPreview(this.props)),
!isInTabContainer && /* @__PURE__ */ React.createElement("div", { className: classNames("drop-target-container", {}) }, values(MosaicDropTargetPosition).map(
this.renderDropTarget
))
)
));
}
getToolbarControls() {
const { toolbarControls, createNode, path } = this.props;
if (toolbarControls) {
return toolbarControls;
}
const root = this.context.mosaicActions.getRoot();
const parentNode = getParentNode(root, path);
if (isTabsNode(parentNode)) {
return DEFAULT_PANEL_CONTROLS_IN_TABS;
} else if (createNode) {
return DEFAULT_PANEL_CONTROLS_WITH_CREATION;
} else {
return DEFAULT_PANEL_CONTROLS_WITHOUT_CREATION;
}
}
renderToolbar() {
const {
title,
draggable,
additionalControls,
additionalControlButtonText,
path,
renderToolbar
} = this.props;
const { additionalControlsOpen } = this.state;
const toolbarControls = this.getToolbarControls();
const root = this.context.mosaicActions.getRoot();
const parentNode = getParentNode(root, path);
const isDragAllowed = draggable && path.length > 0 && !isTabsNode(parentNode);
const connectIfDraggable = isDragAllowed ? this.props.connectDragSource : (el) => el;
if (renderToolbar) {
const connectedToolbar = connectIfDraggable(
renderToolbar(this.props, draggable)
);
return /* @__PURE__ */ React.createElement(
"div",
{
className: classNames("mosaic-window-toolbar", {
draggable: isDragAllowed
})
},
connectedToolbar
);
}
const titleDiv = connectIfDraggable(
/* @__PURE__ */ React.createElement("div", { title, className: "mosaic-window-title" }, title)
);
const hasAdditionalControls = !!additionalControls;
return /* @__PURE__ */ React.createElement(
"div",
{
className: classNames("mosaic-window-toolbar", {
draggable: isDragAllowed
})
},
titleDiv,
/* @__PURE__ */ React.createElement(
"div",
{
className: classNames(
"mosaic-window-controls",
OptionalBlueprint.getClasses("BUTTON_GROUP")
)
},
hasAdditionalControls && /* @__PURE__ */ React.createElement(
"button",
{
onClick: () => this.setAdditionalControlsOpen(!additionalControlsOpen),
className: classNames(
OptionalBlueprint.getClasses(
this.context.blueprintNamespace,
"BUTTON",
"MINIMAL"
),
OptionalBlueprint.getIconClass(
this.context.blueprintNamespace,
"MORE"
),
{
[OptionalBlueprint.getClasses(
this.context.blueprintNamespace,
"ACTIVE"
)]: additionalControlsOpen
}
)
},
/* @__PURE__ */ React.createElement("span", { className: "control-text" }, additionalControlButtonText)
),
hasAdditionalControls && /* @__PURE__ */ React.createElement(Separator, null),
toolbarControls
)
);
}
renderDropTarget = (position) => {
const { path } = this.props;
return /* @__PURE__ */ React.createElement(MosaicDropTarget, { position, path, key: position });
};
checkCreateNode() {
if (this.props.createNode == null) {
throw new Error("Operation invalid unless `createNode` is defined");
}
}
split = (...args) => {
this.checkCreateNode();
const { path } = this.props;
const { mosaicActions } = this.context;
const root = mosaicActions.getRoot();
const parentNode = getParentNode(root, path);
if (isTabsNode(parentNode)) {
if (process.env.NODE_ENV !== "production") {
console.warn(
"Mosaic: Cannot split a panel that is already inside a tab group."
);
}
return Promise.resolve();
}
const direction = this.rootElement.offsetWidth > this.rootElement.offsetHeight ? "row" : "column";
const currentNode = getNodeAtPath(root, path);
if (!currentNode) {
throw new Error("Current node could not be found.");
}
return Promise.resolve(mosaicActions.createNode(...args)).then(
(second) => {
const newSplitNode = {
type: "split",
direction,
children: [currentNode, second],
splitPercentages: [50, 50]
};
mosaicActions.replaceWith(path, newSplitNode);
}
);
};
addTab = (...args) => {
this.checkCreateNode();
const { path, createNode } = this.props;
const { mosaicActions } = this.context;
const root = mosaicActions.getRoot();
const parentNode = getParentNode(root, path);
if (isTabsNode(parentNode)) {
if (process.env.NODE_ENV !== "production") {
console.warn(
`Mosaic: "Add Tab" from a panel toolbar is disabled when already in a tab group. Use the tab bar's "Add" button instead.`
);
}
return Promise.resolve();
}
const currentNode = getNodeAtPath(root, path);
if (!currentNode) {
throw new Error("Current node could not be found.");
}
return Promise.resolve(createNode(...args)).then((newNode) => {
if (typeof newNode !== "string" && typeof newNode !== "number") {
console.error(
"createNode() for addTab should return a MosaicKey (string or number)."
);
return;
}
if (typeof currentNode === "object" && currentNode.type === "tabs") {
const newTabsNode = {
type: "tabs",
tabs: [...currentNode.tabs, newNode],
activeTabIndex: currentNode.tabs.length
// Make the new tab active
};
mosaicActions.replaceWith(path, newTabsNode);
} else {
if (typeof currentNode !== "string" && typeof currentNode !== "number") {
console.error("Cannot create a tab group from a non-panel node.");
return;
}
const newTabsNode = {
type: "tabs",
tabs: [currentNode, newNode],
activeTabIndex: 1
// Make the new tab active
};
mosaicActions.replaceWith(path, newTabsNode);
}
});
};
getRoot = () => {
const { mosaicActions } = this.context;
return mosaicActions.getRoot();
};
swap = (...args) => {
this.checkCreateNode();
const { mosaicActions } = this.context;
const { path, createNode } = this.props;
return Promise.resolve(createNode(...args)).then(
(node) => mosaicActions.replaceWith(path, node)
);
};
setAdditionalControlsOpen = (additionalControlsOpenOption) => {
const additionalControlsOpen = additionalControlsOpenOption === "toggle" ? !this.state.additionalControlsOpen : additionalControlsOpenOption;
this.setState({ additionalControlsOpen });
this.props.onAdditionalControlsToggle?.(additionalControlsOpen);
};
getPath = () => this.props.path;
connectDragSource = (connectedElements) => {
const { connectDragSource } = this.props;
return connectDragSource(connectedElements);
};
childContext = {
// @ts-expect-error we need it
blueprintNamespace: this.context.blueprintNamespace,
mosaicWindowActions: {
split: this.split,
addTab: this.addTab,
getRoot: this.getRoot,
replaceWithNew: this.swap,
setAdditionalControlsOpen: this.setAdditionalControlsOpen,
getPath: this.getPath,
connectDragSource: this.connectDragSource
}
};
};
function ConnectedInternalMosaicWindow(props) {
const { mosaicActions, mosaicId } = useContext(MosaicContext);
const [, connectDragSource, connectDragPreview] = useDrag({
type: MosaicDragType.WINDOW,
item: () => {
if (props.onDragStart) {
props.onDragStart();
}
const hideTimer = window.setTimeout(() => {
mosaicActions.hide(props.path, true);
}, 50);
return {
mosaicId,
hideTimer
};
},
end: (item, monitor) => {
window.clearTimeout(item.hideTimer);
const ownPath = props.path;
const dropResult = monitor.getDropResult() || {};
const { position, path: destinationPath } = dropResult;
const dropped = destinationPath != null;
const isSelfDrop = dropped && isEqual(destinationPath, ownPath);
const isTabContainerSelfDrop = dropped && (() => {
const root = mosaicActions.getRoot();
const ownParentPath = ownPath.slice(0, -1);
const ownParentNode = getNodeAtPath(root, ownParentPath);
const destinationNode = getNodeAtPath(root, destinationPath);
return isTabsNode(ownParentNode) && isTabsNode(destinationNode) && isEqual(ownParentPath, destinationPath);
})();
const isChildDrop = dropped && destinationPath.length > ownPath.length && isEqual(
ownPath,
drop(destinationPath, destinationPath.length - ownPath.length)
);
if (dropped && !isSelfDrop && !isChildDrop && !isTabContainerSelfDrop) {
const updates = createDragToUpdates(
mosaicActions.getRoot(),
ownPath,
destinationPath,
dropResult.tabReorderIndex !== void 0 ? {
type: "tab-reorder",
insertIndex: dropResult.tabReorderIndex
} : position === void 0 ? { type: "tab-container" } : {
type: "split",
position
}
);
mosaicActions.updateTree(updates, {
shouldNormalize: true
});
if (props.onDragEnd) {
props.onDragEnd("drop");
}
} else {
mosaicActions.show(ownPath, true);
if (props.onDragEnd) {
props.onDragEnd("reset");
}
}
}
});
const [{ isOver, draggedMosaicId }, connectDropTarget] = useDrop({
accept: MosaicDragType.WINDOW,
collect: (monitor) => ({
isOver: monitor.isOver(),
draggedMosaicId: monitor.getItem()?.mosaicId
})
});
return /* @__PURE__ */ React.createElement(
InternalMosaicWindow,
{
...props,
connectDragPreview,
connectDragSource,
connectDropTarget,
isOver,
draggedMosaicId
}
);
}
var MosaicWindow = class extends React.PureComponent {
render() {
return /* @__PURE__ */ React.createElement(
ConnectedInternalMosaicWindow,
{
...this.props
}
);
}
};
export {
InternalMosaicWindow,
MosaicWindow
};