@lonli-lokli/react-mosaic-component
Version:
A React Tiling Window Manager
445 lines (443 loc) • 16.5 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// libs/react-mosaic-component/src/lib/MosaicTabs.tsx
var MosaicTabs_exports = {};
__export(MosaicTabs_exports, {
MosaicTabs: () => MosaicTabs
});
module.exports = __toCommonJS(MosaicTabs_exports);
var import_react = __toESM(require("react"), 1);
var import_classnames = __toESM(require("classnames"), 1);
var import_react_dnd = require("react-dnd");
var import_lodash_es = require("lodash-es");
var import_types = require("./types.cjs");
var import_BoundingBox = require("./util/BoundingBox.cjs");
var import_contextTypes = require("./contextTypes.cjs");
var import_mosaicUpdates = require("./util/mosaicUpdates.cjs");
var import_mosaicUtilities = require("./util/mosaicUtilities.cjs");
var import_OptionalBlueprint = require("./util/OptionalBlueprint.cjs");
var import_DraggableTab = require("./DraggableTab.cjs");
var import_defaultToolbarControls = require("./buttons/defaultToolbarControls.cjs");
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__ */ import_react.default.createElement(
import_DraggableTab.DraggableTab,
{
tabKey,
tabIndex: index,
tabContainerPath: path,
mosaicActions,
mosaicId
},
({ isDragging, connectDragSource, connectDragPreview }) => /* @__PURE__ */ import_react.default.createElement(
"button",
{
className: (0, import_classnames.default)("mosaic-tab-button", {
"-active": isActive,
"-dragging": isDragging
}),
onClick: onTabClick,
title: `${tabKey}`,
ref: (node) => {
connectDragSource(node);
connectDragPreview(node);
}
},
/* @__PURE__ */ import_react.default.createElement("span", { className: "mosaic-tab-button-content" }, renderTabTitle({ tabKey, path, isActive, index, mosaicId })),
closeState !== "noClose" && /* @__PURE__ */ import_react.default.createElement(
"span",
{
className: (0, import_classnames.default)("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__ */ import_react.default.createElement(import_OptionalBlueprint.OptionalBlueprint.Icon, { size: "empty", icon: "CROSS" })
)
)
);
};
var TabDropTarget = ({
tabContainerPath,
insertIndex
}) => {
const [{ isOver, canDrop, draggedMosaicId }, connectDropTarget] = (0, import_react_dnd.useDrop)({
accept: import_types.MosaicDragType.WINDOW,
canDrop: (item) => {
const isTabReorder = item?.isTab && item?.tabContainerPath && (0, import_lodash_es.isEqual)(item.tabContainerPath, tabContainerPath);
const isExternalDrop = !item?.isTab || !(0, import_lodash_es.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__ */ import_react.default.createElement(
"div",
{
className: (0, import_classnames.default)("tab-drop-target", {
"tab-drop-target-hover": isOver,
dragging: isDragging
})
},
isOver ? (
// Show placeholder when hovering during drag
/* @__PURE__ */ import_react.default.createElement("div", { className: "tab-drop-placeholder" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "tab-drop-arrow" }), "``")
) : (
// Subtle indicator when not hovering
/* @__PURE__ */ import_react.default.createElement(
"div",
{
className: (0, import_classnames.default)("tab-drop-indicator", {
"can-drop": canDrop,
default: !canDrop
})
}
)
)
)
);
};
var MosaicTabs = ({
node,
path,
renderTile,
renderTabToolbar,
boundingBox,
renderTabTitle,
renderTabButton,
tabToolbarControls = (0, import_defaultToolbarControls.createDefaultTabsControls)(path),
canClose
}) => {
const { mosaicActions, mosaicId } = import_react.default.useContext(
import_contextTypes.MosaicContext
);
const { tabs, activeTabIndex } = node;
const [, connectDragSource, connectDragPreview] = (0, import_react_dnd.useDrag)({
type: import_types.MosaicDragType.WINDOW,
item: () => {
const hideTimer = (0, import_lodash_es.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 && (0, import_lodash_es.isEqual)(destinationPath, ownPath);
const isTabContainerSelfDrop = dropped && (() => {
const root = mosaicActions.getRoot();
const destinationNode = (0, import_mosaicUtilities.getNodeAtPath)(root, destinationPath);
return (0, import_mosaicUtilities.isTabsNode)(destinationNode) && (0, import_lodash_es.isEqual)(ownPath, destinationPath);
})();
const isChildDrop = dropped && destinationPath.length > ownPath.length && (0, import_lodash_es.isEqual)(
ownPath,
(0, import_lodash_es.drop)(destinationPath, destinationPath.length - ownPath.length)
);
if (dropped && !isSelfDrop && !isChildDrop && !isTabContainerSelfDrop) {
const updates = (0, import_mosaicUpdates.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] = (0, import_react_dnd.useDrop)({
accept: import_types.MosaicDragType.WINDOW,
canDrop: () => {
return false;
},
collect: (monitor) => ({
isOver: monitor.isOver({ shallow: true }),
draggedMosaicId: monitor.getItem()?.mosaicId
})
});
const [
{ isOver: isTabBarOver, draggedMosaicId: tabBarDraggedMosaicId },
connectTabBarDropTarget
] = (0, import_react_dnd.useDrop)({
accept: import_types.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 = (0, import_mosaicUpdates.updateTree)(newTree, [update]);
});
const normalizedTree = (0, import_mosaicUtilities.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 = (0, import_mosaicUpdates.updateTree)(newTree, [update]);
});
const normalizedTree = (0, import_mosaicUtilities.normalizeMosaicTree)(newTree);
mosaicActions.replaceWith([], normalizedTree);
});
};
const renderDefaultToolbar = () => {
const isDragAllowed = path.length > 0;
const dragHandle = isDragAllowed ? connectDragSource(
/* @__PURE__ */ import_react.default.createElement("div", { className: "mosaic-tab-drag-handle", title: "Drag to move tab container" })
) : null;
return connectTabBarDropTarget(
/* @__PURE__ */ import_react.default.createElement(
"div",
{
className: (0, import_classnames.default)("mosaic-tab-bar", {
"tab-bar-drop-target-hover": isTabBarOver && tabBarDraggedMosaicId === mosaicId,
draggable: isDragAllowed
})
},
/* @__PURE__ */ import_react.default.createElement("div", { className: "mosaic-tab-bar-tabs" }, /* @__PURE__ */ import_react.default.createElement(
TabDropTarget,
{
tabContainerPath: path,
insertIndex: 0,
mosaicActions,
mosaicId
}
), tabs.map((tabKey, index) => {
const TabButtonComponent = renderTabButton || DefaultTabButton;
return /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, { key: tabKey }, /* @__PURE__ */ import_react.default.createElement(
TabButtonComponent,
{
tabKey,
index,
isActive: index === activeTabIndex,
path,
mosaicId,
onTabClick: () => onTabClick(index),
mosaicActions,
renderTabTitle,
canClose,
onTabClose,
tabs
}
), /* @__PURE__ */ import_react.default.createElement(
TabDropTarget,
{
tabContainerPath: path,
insertIndex: index + 1,
mosaicActions,
mosaicId
}
));
})),
/* @__PURE__ */ import_react.default.createElement("div", { className: "mosaic-tab-bar-controls" }, /* @__PURE__ */ import_react.default.createElement(
"button",
{
className: "mosaic-tab-add-button",
onClick: addTab,
"aria-label": "Add new tab",
title: "Add new tab"
},
"+"
), dragHandle, /* @__PURE__ */ import_react.default.createElement("div", { className: "mosaic-tab-toolbar-controls" }, tabToolbarControls))
)
);
};
const activeTabKey = tabs[activeTabIndex];
const tilePath = path.concat(activeTabIndex);
const renderPreview = () => /* @__PURE__ */ import_react.default.createElement("div", { className: "mosaic-preview" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "mosaic-tab-bar" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "mosaic-tab-bar-tabs" }, tabs.map((tabKey, index) => /* @__PURE__ */ import_react.default.createElement(
"button",
{
key: tabKey,
className: (0, import_classnames.default)("mosaic-tab-button", {
"-active": index === activeTabIndex
})
},
/* @__PURE__ */ import_react.default.createElement("span", { className: "mosaic-tab-button-content" }, renderTabTitle ? renderTabTitle({ tabKey, path, isActive: index === activeTabIndex, index, mosaicId }) : `Tab ${tabKey}`)
)))), /* @__PURE__ */ import_react.default.createElement("div", { className: "mosaic-window-body" }, /* @__PURE__ */ import_react.default.createElement("h4", null, "Tab Container"), /* @__PURE__ */ import_react.default.createElement(
import_OptionalBlueprint.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__ */ import_react.default.createElement(
"div",
{
className: "mosaic-tabs-container",
style: (0, import_BoundingBox.boundingBoxAsStyles)(boundingBox)
},
renderTabToolbar ? renderTabToolbar({
tabs,
activeTabIndex,
path,
DraggableTab: (props) => /* @__PURE__ */ import_react.default.createElement(
import_DraggableTab.DraggableTab,
{
key: props.tabKey,
...props,
tabContainerPath: path,
mosaicActions,
mosaicId
}
)
}) : renderDefaultToolbar(),
connectDropTarget(
/* @__PURE__ */ import_react.default.createElement("div", { className: "mosaic-tile" }, renderTile(activeTabKey, tilePath))
),
connectDragPreview(renderPreview())
)
);
};