UNPKG

@lonli-lokli/react-mosaic-component

Version:
346 lines (345 loc) 10.5 kB
// libs/react-mosaic-component/src/lib/MosaicTabs.tsx import React from "react"; import classNames from "classnames"; import { useDrop } from "react-dnd"; import { isEqual } from "lodash-es"; import { MosaicDragType } from "./types.mjs"; import { boundingBoxAsStyles } from "./util/BoundingBox.mjs"; import { MosaicContext } from "./contextTypes.mjs"; import { updateTree } from "./util/mosaicUpdates.mjs"; import { normalizeMosaicTree } 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 buttonRef = React.useRef(null); 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 }) => { const element = /* @__PURE__ */ React.createElement( "button", { ref: buttonRef, className: classNames("mosaic-tab-button", { "-active": isActive, "-dragging": isDragging }), onClick: onTabClick, title: `${tabKey}` }, /* @__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" }) ) ); const previewElement = connectDragPreview(element); return connectDragSource(previewElement || element) || element; } ); }; 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 [, 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 = () => connectTabBarDropTarget( /* @__PURE__ */ React.createElement( "div", { className: classNames("mosaic-tab-bar", { "tab-bar-drop-target-hover": isTabBarOver && tabBarDraggedMosaicId === mosaicId }) }, /* @__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( "button", { className: "mosaic-tab-add-button", onClick: addTab, "aria-label": "Add new tab", title: "Add new tab" }, "+" )), /* @__PURE__ */ React.createElement("div", { className: "mosaic-tab-toolbar-controls" }, tabToolbarControls) ) ); const activeTabKey = tabs[activeTabIndex]; const tilePath = path.concat(activeTabIndex); 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, { ...props, tabContainerPath: path, mosaicActions, mosaicId } ) }) : renderDefaultToolbar(), connectDropTarget( /* @__PURE__ */ React.createElement("div", { className: "mosaic-tile" }, renderTile(activeTabKey, tilePath)) ) ) ); }; export { MosaicTabs };