react-selectable-box
Version:
A React component used hooks that allows you to select elements in the drag area using the mouse
323 lines (321 loc) • 12.4 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_useMergedState = __toESM(require("./hooks/useMergedState"));
var import_useScroll = __toESM(require("./hooks/useScroll"));
var import_utils = require("./utils");
function defaultCompareFn(a, b) {
return a === b;
}
function Selectable({
defaultValue,
value: propsValue,
items,
disabled,
mode = "add",
children,
selectStartRange = "all",
scrollSpeed,
getContainer,
scrollContainer: propsScrollContainer,
dragContainer: propsDragContainer,
boxStyle,
boxClassName,
compareFn = defaultCompareFn,
onStart,
onEnd
}, ref) {
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 [value, setValue] = (0, import_useMergedState.default)(defaultValue || [], {
value: propsValue
});
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 scrollContainer = (0, import_useContainer.default)(propsScrollContainer || getContainer);
const dragContainer = (0, import_useContainer.default)(propsDragContainer || propsScrollContainer || getContainer);
const { smoothScroll, cancelScroll } = (0, import_useScroll.default)(scrollSpeed);
const startCoordsRef = (0, import_useLatest.default)(startCoords);
const isDraggingRef = (0, import_useLatest.default)(isDragging);
const selectStartRangeRef = (0, import_useLatest.default)(selectStartRange);
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 = !!items;
if (process.env.NODE_ENV === "development" && getContainer) {
console.error(
"[react-selectable-box]: getContainer will be deprecated in the future, use scrollContainer instead"
);
}
import_react.default.useImperativeHandle(ref, () => ({
cancel: () => {
setIsCanceled(true);
setIsDragging(false);
}
}));
const handleStart = (0, import_useEvent.default)((event) => {
onStart == null ? void 0 : onStart(event);
});
const handleEnd = (0, import_useEvent.default)(() => {
if (onEnd) {
if (virtual) {
unmountItemsInfo.current.forEach((info, item) => {
if (items.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
},
scrollContainer,
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 || !scrollContainer || !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 } = scrollContainer.getBoundingClientRect();
const x = Math.min(
clientX - left2 + scrollContainer.scrollLeft,
scrollContainer.scrollWidth
);
const y = Math.min(clientY - top2 + scrollContainer.scrollTop, scrollContainer.scrollHeight);
setMoveCoords({
x,
y
});
smoothScroll(e, scrollContainer);
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)) {
setIsDragging(true);
scrollContainerOriginPosition = getComputedStyle(scrollContainer).position;
if (scrollContainer !== document.body && scrollContainerOriginPosition === "static") {
scrollContainer.style.position = "relative";
}
handleStart(e);
}
}
}
};
const scrollListenerElement = scrollContainer === document.body ? document : scrollContainer;
const onScroll = (e) => {
const target = e.target;
scrollInfo.current = { scrollTop: target.scrollTop, scrollLeft: target.scrollLeft };
if (isDraggingRef.current && scrollContainer) {
const containerRect = scrollContainer.getBoundingClientRect();
const x = Math.min(
moveClient.current.x - containerRect.left + scrollContainer.scrollLeft,
scrollContainer.scrollWidth
);
const y = Math.min(
moveClient.current.y - containerRect.top + scrollContainer.scrollTop,
scrollContainer.scrollHeight
);
setMoveCoords({
x,
y
});
}
};
const onMouseUp = () => {
window.removeEventListener("mousemove", onMouseMove);
window.removeEventListener("mouseup", onMouseUp);
window.removeEventListener("touchmove", onMouseMove);
window.removeEventListener("touchend", onMouseUp);
if (isDraggingRef.current) {
scrollContainer.style.position = scrollContainerOriginPosition;
cancelScroll();
setValue(selectingValue.current);
handleEnd();
}
reset();
};
const onMouseDown = (e) => {
const isMouseEvent = e instanceof MouseEvent;
if (isMouseEvent && e.button !== 0) {
return;
}
if (isMouseEvent) {
e.preventDefault();
}
isMouseDowning = true;
if (selectStartRangeRef.current !== "all") {
setStartTarget(e.target);
}
const { clientX, clientY } = (0, import_utils.getClientXY)(e);
const { left: left2, top: top2 } = scrollContainer.getBoundingClientRect();
setStartCoords({
x: clientX - left2 + scrollContainer.scrollLeft,
y: clientY - top2 + scrollContainer.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);
scrollListenerElement.addEventListener("scroll", onScroll);
return () => {
if (scrollContainerOriginPosition) {
scrollContainer.style.position = scrollContainerOriginPosition;
}
cancelScroll();
dragContainer.removeEventListener("mousedown", onMouseDown);
dragContainer.removeEventListener("touchstart", onMouseDown);
scrollListenerElement.removeEventListener("scroll", onScroll);
window.removeEventListener("mousemove", onMouseMove);
window.removeEventListener("mouseup", onMouseUp);
window.removeEventListener("touchmove", onMouseMove);
window.removeEventListener("touchend", onMouseUp);
};
}, [disabled, scrollContainer, dragContainer, isCanceled]);
const contextValue = (0, import_react.useMemo)(
() => ({
value,
selectingValue,
isDragging,
boxPosition,
mode,
scrollContainer,
startTarget,
startInside,
unmountItemsInfo,
virtual,
boxRef,
compareFn
}),
[
value,
isDragging,
boxPosition,
mode,
scrollContainer,
startTarget,
unmountItemsInfo,
virtual,
boxRef,
compareFn
]
);
return /* @__PURE__ */ import_react.default.createElement(import_context.SelectableContext.Provider, { value: contextValue }, children, isDragging && scrollContainer && (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
}
}
),
scrollContainer
));
}
var Selectable_default = import_react.default.forwardRef(Selectable);