react-selectable-box
Version:
A React component used hooks that allows you to select elements in the drag area using the mouse
337 lines (335 loc) • 13.6 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);
// src/Selectable.tsx
var Selectable_exports = {};
__export(Selectable_exports, {
default: () => Selectable_default
});
module.exports = __toCommonJS(Selectable_exports);
var import_react = __toESM(require("react"));
var import_react_dom = require("react-dom");
var import_context = require("./context");
var import_useContainer = __toESM(require("./hooks/useContainer"));
var import_useEvent = __toESM(require("./hooks/useEvent"));
var import_useLatest = __toESM(require("./hooks/useLatest"));
var import_useScroll = __toESM(require("./hooks/useScroll"));
var import_utils = require("./utils");
function defaultCompareFn(a, b) {
return a === b;
}
function Selectable({
value,
virtualItems,
disabled,
mode = "add",
children,
selectStartRange = "all",
scrollSpeed,
scrollContainer: propsScrollContainer,
dragContainer: propsDragContainer,
boxStyle,
boxClassName,
compareFn = defaultCompareFn,
allowTextSelection,
onStart,
onEnd
}, ref) {
var _a, _b;
if (process.env.NODE_ENV === "development" && selectStartRange !== "all") {
console.warn(
"[Selectable] selectStartRange is deprecated, please use onStart to prevent selection start instead."
);
}
const [isDragging, setIsDragging] = (0, import_react.useState)(false);
const [startCoords, setStartCoords] = (0, import_react.useState)({ x: 0, y: 0 });
const [moveCoords, setMoveCoords] = (0, import_react.useState)({ x: 0, y: 0 });
const selectingValue = (0, import_react.useRef)([]);
const [startTarget, setStartTarget] = (0, import_react.useState)(null);
const startInside = (0, import_react.useRef)(false);
const moveClient = (0, import_react.useRef)({ x: 0, y: 0 });
const boxRef = (0, import_react.useRef)(null);
const unmountItemsInfo = (0, import_react.useRef)(/* @__PURE__ */ new Map());
const scrollInfo = (0, import_react.useRef)({ scrollTop: 0, scrollLeft: 0 });
const [isCanceled, setIsCanceled] = (0, import_react.useState)(false);
const getInnerScrollContainer = typeof propsScrollContainer === "function" ? propsScrollContainer : (_a = propsScrollContainer == null ? void 0 : propsScrollContainer.inner) == null ? void 0 : _a.getContainer;
const innerScrollContainer = (0, import_useContainer.default)(getInnerScrollContainer);
const outerScrollContainer = (0, import_useContainer.default)(
typeof propsScrollContainer === "object" ? (_b = propsScrollContainer == null ? void 0 : propsScrollContainer.outer) == null ? void 0 : _b.getContainer : void 0,
false
);
const dragContainer = (0, import_useContainer.default)(propsDragContainer || getInnerScrollContainer);
const { smoothScroll, cancelScroll } = (0, import_useScroll.default)(scrollSpeed, propsScrollContainer);
const startCoordsRef = (0, import_useLatest.default)(startCoords);
const isDraggingRef = (0, import_useLatest.default)(isDragging);
const selectStartRangeRef = (0, import_useLatest.default)(selectStartRange);
const allowTextSelectionRef = (0, import_useLatest.default)(allowTextSelection);
const top = Math.max(0, Math.min(startCoords.y, moveCoords.y));
const left = Math.max(0, Math.min(startCoords.x, moveCoords.x));
const width = isDragging ? Math.abs(startCoords.x - Math.max(0, moveCoords.x)) : 0;
const height = isDragging ? Math.abs(startCoords.y - Math.max(0, moveCoords.y)) : 0;
const boxPosition = (0, import_react.useMemo)(() => ({ top, left, width, height }), [top, left, width, height]);
const virtual = !!virtualItems;
import_react.default.useImperativeHandle(ref, () => ({
cancel: () => {
setIsCanceled(true);
setIsDragging(false);
}
}));
const handleStart = (0, import_useEvent.default)((event) => {
return onStart == null ? void 0 : onStart(event);
});
const handleEnd = (0, import_useEvent.default)(() => {
if (onEnd) {
if (virtual) {
unmountItemsInfo.current.forEach((info, item) => {
if (virtualItems.some((i) => compareFn(i, item))) {
const isInRange = (0, import_utils.checkInRange)(
info.rule,
{
width: info.rect.width,
height: info.rect.height,
top: info.rect.top + info.scrollTop - scrollInfo.current.scrollTop,
left: info.rect.left + info.scrollLeft - scrollInfo.current.scrollLeft
},
innerScrollContainer,
boxPosition,
boxRef
);
if (isInRange && !info.disabled) {
selectingValue.current.push(item);
} else {
selectingValue.current = selectingValue.current.filter((i) => !compareFn(i, item));
}
}
});
}
const added = [];
const removed = [];
selectingValue.current.forEach((i) => {
if (value == null ? void 0 : value.some((val) => compareFn(val, i))) {
if (mode === "remove" || mode === "reverse") {
removed.push(i);
}
} else {
if (mode === "add" || mode === "reverse") {
added.push(i);
}
}
});
onEnd(selectingValue.current, { added, removed });
}
});
(0, import_react.useEffect)(() => {
let isMouseDowning = false;
let scrollContainerOriginPosition = "";
const reset = () => {
setIsDragging(false);
setIsCanceled(false);
setStartTarget(null);
isMouseDowning = false;
startInside.current = false;
selectingValue.current = [];
};
if (disabled || !innerScrollContainer || !dragContainer || isCanceled) {
reset();
return;
}
const onMouseMove = (e) => {
if (isMouseDowning) {
if (window.TouchEvent && e instanceof TouchEvent) {
e.preventDefault();
}
const { clientX, clientY } = (0, import_utils.getClientXY)(e);
moveClient.current = { x: clientX, y: clientY };
const { left: left2, top: top2 } = innerScrollContainer.getBoundingClientRect();
const x = Math.min(
clientX - left2 + innerScrollContainer.scrollLeft,
innerScrollContainer.scrollWidth
);
const y = Math.min(
clientY - top2 + innerScrollContainer.scrollTop,
innerScrollContainer.scrollHeight
);
setMoveCoords({
x,
y
});
smoothScroll(e);
if (!isDraggingRef.current) {
let shouldDraggingStart = true;
if (selectStartRangeRef.current === "outside") {
shouldDraggingStart = !startInside.current;
} else if (selectStartRangeRef.current === "inside") {
shouldDraggingStart = startInside.current;
}
const boxWidth = Math.abs(startCoordsRef.current.x - x);
const boxHeight = Math.abs(startCoordsRef.current.y - y);
if (shouldDraggingStart && (boxWidth > 1 || boxHeight > 1)) {
const shouldStart = handleStart(e);
if (shouldStart === false) {
reset();
} else {
setIsDragging(true);
scrollContainerOriginPosition = getComputedStyle(innerScrollContainer).position;
if (innerScrollContainer !== document.body && scrollContainerOriginPosition === "static") {
innerScrollContainer.style.position = "relative";
}
}
}
}
}
};
const innerScrollListenerElement = innerScrollContainer === document.body ? document : innerScrollContainer;
const outerScrollListenerElement = outerScrollContainer === document.body ? document : outerScrollContainer;
const onScroll = (e) => {
const target = e.target;
scrollInfo.current = { scrollTop: target.scrollTop, scrollLeft: target.scrollLeft };
if (isDraggingRef.current) {
const containerRect = innerScrollContainer.getBoundingClientRect();
const x = Math.min(
moveClient.current.x - containerRect.left + innerScrollContainer.scrollLeft,
innerScrollContainer.scrollWidth
);
const y = Math.min(
moveClient.current.y - containerRect.top + innerScrollContainer.scrollTop,
innerScrollContainer.scrollHeight
);
setMoveCoords({
x,
y
});
}
};
const onOuterScroll = () => {
onScroll({ target: innerScrollContainer });
};
const onMouseUp = () => {
window.removeEventListener("mousemove", onMouseMove);
window.removeEventListener("mouseup", onMouseUp);
window.removeEventListener("touchmove", onMouseMove);
window.removeEventListener("touchend", onMouseUp);
if (isDraggingRef.current) {
innerScrollContainer.style.position = scrollContainerOriginPosition;
cancelScroll();
handleEnd();
}
reset();
};
const onMouseDown = (e) => {
const isMouseEvent = e instanceof MouseEvent;
if (isMouseEvent && e.button !== 0) {
return;
}
if (isMouseEvent && !allowTextSelectionRef.current) {
e.preventDefault();
}
isMouseDowning = true;
if (selectStartRangeRef.current !== "all") {
setStartTarget(e.target);
}
const { clientX, clientY } = (0, import_utils.getClientXY)(e);
const { left: left2, top: top2 } = innerScrollContainer.getBoundingClientRect();
setStartCoords({
x: clientX - left2 + innerScrollContainer.scrollLeft,
y: clientY - top2 + innerScrollContainer.scrollTop
});
window.addEventListener("mousemove", onMouseMove, { passive: false });
window.addEventListener("mouseup", onMouseUp);
window.addEventListener("touchmove", onMouseMove, { passive: false });
window.addEventListener("touchend", onMouseUp);
};
dragContainer.addEventListener("mousedown", onMouseDown);
dragContainer.addEventListener("touchstart", onMouseDown);
innerScrollListenerElement.addEventListener("scroll", onScroll);
outerScrollListenerElement == null ? void 0 : outerScrollListenerElement.addEventListener("scroll", onOuterScroll);
return () => {
if (scrollContainerOriginPosition) {
innerScrollContainer.style.position = scrollContainerOriginPosition;
}
cancelScroll();
dragContainer.removeEventListener("mousedown", onMouseDown);
dragContainer.removeEventListener("touchstart", onMouseDown);
innerScrollListenerElement.removeEventListener("scroll", onScroll);
outerScrollListenerElement == null ? void 0 : outerScrollListenerElement.removeEventListener("scroll", onOuterScroll);
window.removeEventListener("mousemove", onMouseMove);
window.removeEventListener("mouseup", onMouseUp);
window.removeEventListener("touchmove", onMouseMove);
window.removeEventListener("touchend", onMouseUp);
};
}, [disabled, innerScrollContainer, outerScrollContainer, dragContainer, isCanceled]);
const contextValue = (0, import_react.useMemo)(
() => ({
value,
selectingValue,
isDragging,
boxPosition,
mode,
innerScrollContainer,
startTarget,
startInside,
unmountItemsInfo,
virtual,
boxRef,
compareFn
}),
[
value,
isDragging,
boxPosition,
mode,
innerScrollContainer,
startTarget,
unmountItemsInfo,
virtual,
boxRef,
compareFn
]
);
return /* @__PURE__ */ import_react.default.createElement(import_context.SelectableContext.Provider, { value: contextValue }, children, isDragging && innerScrollContainer && (0, import_react_dom.createPortal)(
/* @__PURE__ */ import_react.default.createElement(
"div",
{
ref: boxRef,
className: boxClassName,
style: {
position: "absolute",
zIndex: 9999,
top: 0,
left: 0,
transform: `translate(${left}px, ${top}px)`,
width,
height,
backgroundColor: "rgba(22, 119, 255, 0.3)",
...boxStyle
}
}
),
innerScrollContainer
));
}
var Selectable_default = import_react.default.forwardRef(Selectable);