@lonli-lokli/react-mosaic-component
Version:
A React Tiling Window Manager
230 lines (229 loc) • 7.01 kB
JavaScript
// libs/react-mosaic-component/src/lib/Mosaic.tsx
import classNames from "classnames";
import { countBy, keys, pickBy } from "lodash-es";
import { HTML5toTouch } from "rdndmb-html5-to-touch";
import React from "react";
import { DndProvider } from "react-dnd";
import { MultiBackend } from "react-dnd-multi-backend";
import { v4 as uuid } from "uuid";
import { useDrop } from "react-dnd";
import { MosaicContext } from "./contextTypes.mjs";
import { MosaicRoot } from "./MosaicRoot.mjs";
import { MosaicZeroState } from "./MosaicZeroState.mjs";
import { RootDropTargets } from "./RootDropTargets.mjs";
import {
MosaicDragType
} from "./types.mjs";
import {
createExpandUpdate,
createHideUpdate,
createRemoveUpdate,
updateTree
} from "./util/mosaicUpdates.mjs";
import {
convertLegacyToNary,
getLeaves,
getParentAndChildIndex,
isSplitNode,
normalizeMosaicTree
} from "./util/mosaicUtilities.mjs";
var DEFAULT_EXPAND_PERCENTAGE = 70;
function isUncontrolled(props) {
return props.initialValue != null;
}
var MosaicWithoutDragDropContext = class extends React.PureComponent {
static defaultProps = {
onChange: () => void 0,
zeroStateView: /* @__PURE__ */ React.createElement(MosaicZeroState, null),
className: "mosaic-blueprint-theme",
blueprintNamespace: "bp5"
};
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.mosaicId && prevState.mosaicId !== nextProps.mosaicId && process.env.NODE_ENV !== "production") {
throw new Error(
"Mosaic does not support updating the mosaicId after instantiation"
);
}
if (isUncontrolled(nextProps) && nextProps.initialValue !== prevState.lastInitialValue) {
return {
lastInitialValue: convertLegacyToNary(nextProps.initialValue),
currentNode: convertLegacyToNary(nextProps.initialValue)
};
}
return null;
}
state = {
currentNode: null,
lastInitialValue: null,
mosaicId: this.props.mosaicId ?? uuid()
};
render() {
const { className } = this.props;
return /* @__PURE__ */ React.createElement(MosaicContext.Provider, { value: this.childContext }, /* @__PURE__ */ React.createElement(MosaicRootWithDragDetection, { className }, this.renderTree(), /* @__PURE__ */ React.createElement(RootDropTargets, null)));
}
getRoot() {
if (isUncontrolled(this.props)) {
return this.state.currentNode;
} else {
return convertLegacyToNary(this.props.value);
}
}
updateRoot = (updates, modifiers) => {
modifiers = {
shouldNormalize: modifiers?.shouldNormalize ?? false,
suppressOnRelease: modifiers?.suppressOnRelease ?? false,
suppressOnChange: modifiers?.suppressOnChange ?? false
};
const currentNode = this.getRoot() || {};
const updatedNode = modifiers.shouldNormalize ? normalizeMosaicTree(updateTree(currentNode, updates)) : updateTree(currentNode, updates);
this.replaceRoot(
updatedNode,
modifiers.suppressOnRelease,
modifiers.suppressOnChange
);
};
replaceRoot = (currentNode, suppressOnRelease = false, suppressOnChange = false) => {
if (!suppressOnChange) {
this.props.onChange(currentNode);
}
if (!suppressOnRelease && this.props.onRelease) {
this.props.onRelease(currentNode);
}
if (isUncontrolled(this.props)) {
this.setState({ currentNode });
}
};
actions = {
updateTree: this.updateRoot,
remove: (path) => {
if (path.length === 0) {
this.replaceRoot(null);
} else {
this.updateRoot([createRemoveUpdate(this.getRoot(), path)], {
shouldNormalize: true
});
}
},
expand: (path, percentage = DEFAULT_EXPAND_PERCENTAGE) => this.updateRoot([createExpandUpdate(path, percentage)]),
getRoot: () => this.getRoot(),
hide: (path, suppressOnChange = false) => {
this.updateRoot([createHideUpdate(this.getRoot(), path)], {
suppressOnChange
});
},
show: (path, suppressOnChange = false) => {
const root = this.getRoot();
if (!root || path.length === 0) {
return;
}
const parentInfo = getParentAndChildIndex(root, path);
if (!parentInfo) {
return;
}
const { parent } = parentInfo;
if (isSplitNode(parent)) {
const equalPercentage = 100 / parent.children.length;
const newPercentages = Array(parent.children.length).fill(
equalPercentage
);
this.updateRoot(
[
{
path: path.slice(0, -1),
// Parent path
spec: {
splitPercentages: { $set: newPercentages }
}
}
],
{ suppressOnChange }
);
}
},
createNode: this.props.createNode,
replaceWith: (path, newNode) => this.updateRoot([
{
path,
spec: {
$set: newNode
}
}
])
};
childContext = {
mosaicActions: this.actions,
mosaicId: this.state.mosaicId,
blueprintNamespace: this.props.blueprintNamespace
};
renderTree() {
const root = this.getRoot();
this.validateTree(root);
if (root === null || root === void 0) {
return this.props.zeroStateView;
} else {
const { renderTile, resize } = this.props;
return /* @__PURE__ */ React.createElement(
MosaicRoot,
{
root,
renderTile,
renderTabToolbar: this.props.renderTabToolbar,
resize,
renderTabTitle: this.props.renderTabTitle,
renderTabButton: this.props.renderTabButton,
canClose: this.props.canClose
}
);
}
}
validateTree(node) {
if (process.env.NODE_ENV !== "production") {
const duplicates = keys(pickBy(countBy(getLeaves(node)), (n) => n > 1));
if (duplicates.length > 0) {
throw new Error(
`Duplicate IDs [${duplicates.join(", ")}] detected. Mosaic does not support leaves with the same ID`
);
}
}
}
};
function MosaicRootWithDragDetection({
className,
children
}) {
const [{ isDragging }] = useDrop({
accept: MosaicDragType.WINDOW,
collect: (monitor) => ({
isDragging: monitor.getItem() !== null && monitor.getItemType() === MosaicDragType.WINDOW
})
});
return /* @__PURE__ */ React.createElement(
"div",
{
className: classNames(className, "mosaic mosaic-drop-target", {
"-dragging": isDragging
})
},
children
);
}
var Mosaic = class extends React.PureComponent {
render() {
return /* @__PURE__ */ React.createElement(
DndProvider,
{
backend: MultiBackend,
options: HTML5toTouch,
context: window,
...this.props.dragAndDropManager && {
manager: this.props.dragAndDropManager
}
},
/* @__PURE__ */ React.createElement(MosaicWithoutDragDropContext, { ...this.props })
);
}
};
export {
Mosaic,
MosaicWithoutDragDropContext
};