UNPKG

@lonli-lokli/react-mosaic-component

Version:
410 lines (409 loc) 14 kB
// 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 };