UNPKG

@lonli-lokli/react-mosaic-component

Version:
416 lines (415 loc) 13.7 kB
// libs/react-mosaic-component/src/lib/MosaicTabs.tsx import React from "react"; import classNames from "classnames"; import { useDrop, useDrag } from "react-dnd"; import { defer, drop as _drop, isEqual } from "lodash-es"; import { MosaicDragType } from "./types.mjs"; import { boundingBoxAsStyles } from "./util/BoundingBox.mjs"; import { MosaicContext } from "./contextTypes.mjs"; import { updateTree, createDragToUpdates } from "./util/mosaicUpdates.mjs"; import { normalizeMosaicTree, getNodeAtPath, isTabsNode } from "./util/mosaicUtilities.mjs"; import { OptionalBlueprint } from "./util/OptionalBlueprint.mjs"; import { DraggableTab } from "./DraggableTab.mjs"; import { createDefaultTabsControls } from "./buttons/defaultToolbarControls.mjs"; var DefaultTabButton = ({ tabKey, index, isActive, path, mosaicId, onTabClick, mosaicActions, renderTabTitle = ({ tabKey: tabKey2 }) => `Tab ${tabKey2}`, canClose = () => "canClose", onTabClose, tabs }) => { const handleCloseClick = (event) => { event.stopPropagation(); onTabClose?.(tabKey, index); }; const closeState = canClose(tabKey, tabs, index, path); return /* @__PURE__ */ React.createElement( DraggableTab, { tabKey, tabIndex: index, tabContainerPath: path, mosaicActions, mosaicId }, ({ isDragging, connectDragSource, connectDragPreview }) => /* @__PURE__ */ React.createElement( "button", { className: classNames("mosaic-tab-button", { "-active": isActive, "-dragging": isDragging }), onClick: onTabClick, title: `${tabKey}`, ref: (node) => { connectDragSource(node); connectDragPreview(node); } }, /* @__PURE__ */ React.createElement("span", { className: "mosaic-tab-button-content" }, renderTabTitle({ tabKey, path, isActive, index, mosaicId })), closeState !== "noClose" && /* @__PURE__ */ React.createElement( "span", { className: classNames("mosaic-tab-close-button", { "-can-close": closeState === "canClose", "-cannot-close": closeState === "cannotClose", "-active-tab": isActive, "-inactive-tab": !isActive }), onClick: closeState === "canClose" ? handleCloseClick : void 0, title: closeState === "canClose" ? "Close tab" : "Cannot close tab" }, /* @__PURE__ */ React.createElement(OptionalBlueprint.Icon, { size: "empty", icon: "CROSS" }) ) ) ); }; var TabDropTarget = ({ tabContainerPath, insertIndex }) => { const [{ isOver, canDrop, draggedMosaicId }, connectDropTarget] = useDrop({ accept: MosaicDragType.WINDOW, canDrop: (item) => { const isTabReorder = item?.isTab && item?.tabContainerPath && isEqual(item.tabContainerPath, tabContainerPath); const isExternalDrop = !item?.isTab || !isEqual(item.tabContainerPath, tabContainerPath); const shouldAccept = isTabReorder || isExternalDrop; return shouldAccept; }, drop: () => { return { path: tabContainerPath, position: void 0, // Custom property to indicate this is a tab reorder operation tabReorderIndex: insertIndex }; }, hover: () => { }, collect: (monitor) => { const result = { isOver: monitor.isOver(), canDrop: monitor.canDrop(), draggedMosaicId: monitor.getItem()?.mosaicId }; return result; } }); const isDragging = draggedMosaicId != null; return connectDropTarget( /* @__PURE__ */ React.createElement( "div", { className: classNames("tab-drop-target", { "tab-drop-target-hover": isOver, dragging: isDragging }) }, isOver ? ( // Show placeholder when hovering during drag /* @__PURE__ */ React.createElement("div", { className: "tab-drop-placeholder" }, /* @__PURE__ */ React.createElement("div", { className: "tab-drop-arrow" }), "``") ) : ( // Subtle indicator when not hovering /* @__PURE__ */ React.createElement( "div", { className: classNames("tab-drop-indicator", { "can-drop": canDrop, default: !canDrop }) } ) ) ) ); }; var MosaicTabs = ({ node, path, renderTile, renderTabToolbar, boundingBox, renderTabTitle, renderTabButton, tabToolbarControls = createDefaultTabsControls(path), canClose }) => { const { mosaicActions, mosaicId } = React.useContext( MosaicContext ); const { tabs, activeTabIndex } = node; const [, connectDragSource, connectDragPreview] = useDrag({ type: MosaicDragType.WINDOW, item: () => { const hideTimer = defer(() => mosaicActions.hide(path)); return { mosaicId, hideTimer }; }, end: ({ hideTimer }, monitor) => { window.clearTimeout(hideTimer); const ownPath = 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 destinationNode = getNodeAtPath(root, destinationPath); return isTabsNode(destinationNode) && isEqual(ownPath, 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 }); } else { mosaicActions.show(ownPath, true); } } }); const [, connectDropTarget] = useDrop({ accept: MosaicDragType.WINDOW, canDrop: () => { return false; }, collect: (monitor) => ({ isOver: monitor.isOver({ shallow: true }), draggedMosaicId: monitor.getItem()?.mosaicId }) }); const [ { isOver: isTabBarOver, draggedMosaicId: tabBarDraggedMosaicId }, connectTabBarDropTarget ] = useDrop({ accept: MosaicDragType.WINDOW, canDrop: (_item, monitor) => { return monitor.isOver({ shallow: true }); }, drop: () => { return { path, position: void 0 // No position needed - always add as new tab }; }, collect: (monitor) => ({ isOver: monitor.isOver({ shallow: true }), draggedMosaicId: monitor.getItem()?.mosaicId }) }); const onTabClick = (index) => { if (index === activeTabIndex) { return; } mosaicActions.updateTree([ { path, spec: { activeTabIndex: { $set: index } } } ]); }; const onTabClose = (tabKey, index) => { const closeState = canClose ? canClose(tabKey, tabs, index, path) : "noClose"; if (closeState !== "canClose") { return; } if (tabs.length <= 1) { return; } const newTabs = tabs.filter((_, i) => i !== index); let newActiveTabIndex = activeTabIndex; if (index === activeTabIndex) { newActiveTabIndex = Math.max(0, index - 1); } else if (index < activeTabIndex) { newActiveTabIndex = activeTabIndex - 1; } const updates = [ { path, spec: { tabs: { $set: newTabs }, activeTabIndex: { $set: newActiveTabIndex } } } ]; let newTree = mosaicActions.getRoot(); if (!newTree) return; updates.forEach((update) => { newTree = updateTree(newTree, [update]); }); const normalizedTree = normalizeMosaicTree(newTree); mosaicActions.replaceWith([], normalizedTree); }; const addTab = () => { if (mosaicActions.createNode == null) { throw new Error( "Operation invalid unless `createNode` is defined on Mosaic" ); } Promise.resolve(mosaicActions.createNode()).then((newNode) => { if (typeof newNode !== "string" && typeof newNode !== "number") { console.error( "createNode() for adding a tab should return a MosaicKey (string or number)." ); return; } const updates = [ { path, // The path to this tabs node spec: { tabs: { $push: [newNode] }, // Set the new tab as active. Its index is the original length of the array. activeTabIndex: { $set: tabs.length } } } ]; let newTree = mosaicActions.getRoot(); if (!newTree) return; updates.forEach((update) => { newTree = updateTree(newTree, [update]); }); const normalizedTree = normalizeMosaicTree(newTree); mosaicActions.replaceWith([], normalizedTree); }); }; const renderDefaultToolbar = () => { const isDragAllowed = path.length > 0; const dragHandle = isDragAllowed ? connectDragSource( /* @__PURE__ */ React.createElement("div", { className: "mosaic-tab-drag-handle", title: "Drag to move tab container" }) ) : null; return connectTabBarDropTarget( /* @__PURE__ */ React.createElement( "div", { className: classNames("mosaic-tab-bar", { "tab-bar-drop-target-hover": isTabBarOver && tabBarDraggedMosaicId === mosaicId, draggable: isDragAllowed }) }, /* @__PURE__ */ React.createElement("div", { className: "mosaic-tab-bar-tabs" }, /* @__PURE__ */ React.createElement( TabDropTarget, { tabContainerPath: path, insertIndex: 0, mosaicActions, mosaicId } ), tabs.map((tabKey, index) => { const TabButtonComponent = renderTabButton || DefaultTabButton; return /* @__PURE__ */ React.createElement(React.Fragment, { key: tabKey }, /* @__PURE__ */ React.createElement( TabButtonComponent, { tabKey, index, isActive: index === activeTabIndex, path, mosaicId, onTabClick: () => onTabClick(index), mosaicActions, renderTabTitle, canClose, onTabClose, tabs } ), /* @__PURE__ */ React.createElement( TabDropTarget, { tabContainerPath: path, insertIndex: index + 1, mosaicActions, mosaicId } )); })), /* @__PURE__ */ React.createElement("div", { className: "mosaic-tab-bar-controls" }, /* @__PURE__ */ React.createElement( "button", { className: "mosaic-tab-add-button", onClick: addTab, "aria-label": "Add new tab", title: "Add new tab" }, "+" ), dragHandle, /* @__PURE__ */ React.createElement("div", { className: "mosaic-tab-toolbar-controls" }, tabToolbarControls)) ) ); }; const activeTabKey = tabs[activeTabIndex]; const tilePath = path.concat(activeTabIndex); const renderPreview = () => /* @__PURE__ */ React.createElement("div", { className: "mosaic-preview" }, /* @__PURE__ */ React.createElement("div", { className: "mosaic-tab-bar" }, /* @__PURE__ */ React.createElement("div", { className: "mosaic-tab-bar-tabs" }, tabs.map((tabKey, index) => /* @__PURE__ */ React.createElement( "button", { key: tabKey, className: classNames("mosaic-tab-button", { "-active": index === activeTabIndex }) }, /* @__PURE__ */ React.createElement("span", { className: "mosaic-tab-button-content" }, renderTabTitle ? renderTabTitle({ tabKey, path, isActive: index === activeTabIndex, index, mosaicId }) : `Tab ${tabKey}`) )))), /* @__PURE__ */ React.createElement("div", { className: "mosaic-window-body" }, /* @__PURE__ */ React.createElement("h4", null, "Tab Container"), /* @__PURE__ */ React.createElement( OptionalBlueprint.Icon, { className: "default-preview-icon", size: "large", icon: "APPLICATION" } ))); return ( // This is the container for the entire tab group. // Its position and size are determined ONLY by the boundingBox. /* @__PURE__ */ React.createElement( "div", { className: "mosaic-tabs-container", style: boundingBoxAsStyles(boundingBox) }, renderTabToolbar ? renderTabToolbar({ tabs, activeTabIndex, path, DraggableTab: (props) => /* @__PURE__ */ React.createElement( DraggableTab, { key: props.tabKey, ...props, tabContainerPath: path, mosaicActions, mosaicId } ) }) : renderDefaultToolbar(), connectDropTarget( /* @__PURE__ */ React.createElement("div", { className: "mosaic-tile" }, renderTile(activeTabKey, tilePath)) ), connectDragPreview(renderPreview()) ) ); }; export { MosaicTabs };