@neodrag/react
Version:
React library to add dragging to your apps 😉
389 lines (386 loc) • 14.5 kB
JavaScript
import { useRef, useState, useEffect } from 'react';
// ../core/dist/index.js
var DEFAULT_RECOMPUTE_BOUNDS = {
dragStart: true
};
var DEFAULT_DRAG_THRESHOLD = {
delay: 0,
distance: 3
/* DISTANCE */
};
function draggable(node, options = {}) {
let {
bounds,
axis = "both",
gpuAcceleration = true,
legacyTranslate = false,
transform,
applyUserSelectHack = true,
disabled = false,
ignoreMultitouch = false,
recomputeBounds = DEFAULT_RECOMPUTE_BOUNDS,
grid,
threshold = DEFAULT_DRAG_THRESHOLD,
position,
cancel,
handle,
defaultClass = "neodrag",
defaultClassDragging = "neodrag-dragging",
defaultClassDragged = "neodrag-dragged",
defaultPosition = { x: 0, y: 0 },
onDragStart,
onDrag,
onDragEnd
} = options;
let is_interacting = false;
let is_dragging = false;
let start_time = 0;
let meets_time_threshold = false;
let meets_distance_threshold = false;
let translate_x = 0, translate_y = 0;
let initial_x = 0, initial_y = 0;
let client_to_node_offsetX = 0, client_to_node_offsetY = 0;
let { x: x_offset, y: y_offset } = position ? { x: position?.x ?? 0, y: position?.y ?? 0 } : defaultPosition;
set_translate(x_offset, y_offset);
let can_move_in_x;
let can_move_in_y;
let body_original_user_select_val = "";
let computed_bounds;
let node_rect;
let drag_els;
let cancel_els;
let currently_dragged_el;
let is_controlled = !!position;
recomputeBounds = { ...DEFAULT_RECOMPUTE_BOUNDS, ...recomputeBounds };
threshold = { ...DEFAULT_DRAG_THRESHOLD, ...threshold ?? {} };
let active_pointers = /* @__PURE__ */ new Set();
function try_start_drag(event) {
if (is_interacting && !is_dragging && meets_distance_threshold && meets_time_threshold && currently_dragged_el) {
is_dragging = true;
fire_svelte_drag_start_event(event);
node_class_list.add(defaultClassDragging);
if (applyUserSelectHack) {
body_original_user_select_val = body_style.userSelect;
body_style.userSelect = "none";
}
}
}
function reset_state() {
is_dragging = false;
meets_time_threshold = false;
meets_distance_threshold = false;
}
const body_style = document.body.style;
const node_class_list = node.classList;
function set_translate(x_pos = translate_x, y_pos = translate_y) {
if (!transform) {
if (legacyTranslate) {
let common = `${+x_pos}px, ${+y_pos}px`;
return set_style(
node,
"transform",
gpuAcceleration ? `translate3d(${common}, 0)` : `translate(${common})`
);
}
return set_style(node, "translate", `${+x_pos}px ${+y_pos}px`);
}
const transform_called = transform({ offsetX: x_pos, offsetY: y_pos, rootNode: node });
if (is_string(transform_called)) {
set_style(node, "transform", transform_called);
}
}
function get_event_data(event) {
return {
offsetX: translate_x,
offsetY: translate_y,
rootNode: node,
currentNode: currently_dragged_el,
event
};
}
function call_event(eventName, fn, event) {
const data = get_event_data(event);
node.dispatchEvent(new CustomEvent(eventName, { detail: data }));
fn?.(data);
}
function fire_svelte_drag_start_event(event) {
call_event("neodrag:start", onDragStart, event);
}
function fire_svelte_drag_end_event(event) {
call_event("neodrag:end", onDragEnd, event);
}
function fire_svelte_drag_event(event) {
call_event("neodrag", onDrag, event);
}
const listen = addEventListener;
const controller = new AbortController();
const event_options = { signal: controller.signal, capture: false };
set_style(node, "touch-action", "none");
listen(
"pointerdown",
(e) => {
if (disabled) return;
if (e.button === 2) return;
active_pointers.add(e.pointerId);
if (ignoreMultitouch && active_pointers.size > 1) return e.preventDefault();
if (recomputeBounds.dragStart) computed_bounds = compute_bound_rect(bounds, node);
if (is_string(handle) && is_string(cancel) && handle === cancel)
throw new Error("`handle` selector can't be same as `cancel` selector");
node_class_list.add(defaultClass);
drag_els = get_handle_els(handle, node);
cancel_els = get_cancel_elements(cancel, node);
can_move_in_x = /(both|x)/.test(axis);
can_move_in_y = /(both|y)/.test(axis);
if (cancel_element_contains(cancel_els, drag_els))
throw new Error(
"Element being dragged can't be a child of the element on which `cancel` is applied"
);
const event_target = e.composedPath()[0];
if (drag_els.some((el) => el.contains(event_target) || el.shadowRoot?.contains(event_target)) && !cancel_element_contains(cancel_els, [event_target])) {
currently_dragged_el = drag_els.length === 1 ? node : drag_els.find((el) => el.contains(event_target));
is_interacting = true;
start_time = Date.now();
if (!threshold.delay) {
meets_time_threshold = true;
}
} else return;
node_rect = node.getBoundingClientRect();
const { clientX, clientY } = e;
const inverse_scale = calculate_inverse_scale();
if (can_move_in_x) initial_x = clientX - x_offset / inverse_scale;
if (can_move_in_y) initial_y = clientY - y_offset / inverse_scale;
if (computed_bounds) {
client_to_node_offsetX = clientX - node_rect.left;
client_to_node_offsetY = clientY - node_rect.top;
}
},
event_options
);
listen(
"pointermove",
(e) => {
if (!is_interacting || ignoreMultitouch && active_pointers.size > 1) return;
if (!is_dragging) {
if (!meets_time_threshold) {
const elapsed = Date.now() - start_time;
if (elapsed >= threshold.delay) {
meets_time_threshold = true;
try_start_drag(e);
}
}
if (!meets_distance_threshold) {
const delta_x = e.clientX - initial_x;
const delta_y = e.clientY - initial_y;
const distance = Math.sqrt(delta_x ** 2 + delta_y ** 2);
if (distance >= threshold.distance) {
meets_distance_threshold = true;
try_start_drag(e);
}
}
if (!is_dragging) return;
}
if (recomputeBounds.drag) computed_bounds = compute_bound_rect(bounds, node);
e.preventDefault();
node_rect = node.getBoundingClientRect();
let final_x = e.clientX, final_y = e.clientY;
const inverse_scale = calculate_inverse_scale();
if (computed_bounds) {
const virtual_client_bounds = {
left: computed_bounds.left + client_to_node_offsetX,
top: computed_bounds.top + client_to_node_offsetY,
right: computed_bounds.right + client_to_node_offsetX - node_rect.width,
bottom: computed_bounds.bottom + client_to_node_offsetY - node_rect.height
};
final_x = clamp(final_x, virtual_client_bounds.left, virtual_client_bounds.right);
final_y = clamp(final_y, virtual_client_bounds.top, virtual_client_bounds.bottom);
}
if (Array.isArray(grid)) {
let [x_snap, y_snap] = grid;
if (isNaN(+x_snap) || x_snap < 0)
throw new Error("1st argument of `grid` must be a valid positive number");
if (isNaN(+y_snap) || y_snap < 0)
throw new Error("2nd argument of `grid` must be a valid positive number");
let delta_x = final_x - initial_x, delta_y = final_y - initial_y;
[delta_x, delta_y] = snap_to_grid(
[x_snap / inverse_scale, y_snap / inverse_scale],
delta_x,
delta_y
);
final_x = initial_x + delta_x;
final_y = initial_y + delta_y;
}
if (can_move_in_x) translate_x = Math.round((final_x - initial_x) * inverse_scale);
if (can_move_in_y) translate_y = Math.round((final_y - initial_y) * inverse_scale);
x_offset = translate_x;
y_offset = translate_y;
fire_svelte_drag_event(e);
set_translate();
},
event_options
);
listen(
"pointerup",
(e) => {
active_pointers.delete(e.pointerId);
if (!is_interacting) return;
if (is_dragging) {
listen("click", (e2) => e2.stopPropagation(), {
once: true,
signal: controller.signal,
capture: true
});
if (recomputeBounds.dragEnd) computed_bounds = compute_bound_rect(bounds, node);
node_class_list.remove(defaultClassDragging);
node_class_list.add(defaultClassDragged);
if (applyUserSelectHack) body_style.userSelect = body_original_user_select_val;
fire_svelte_drag_end_event(e);
if (can_move_in_x) initial_x = translate_x;
if (can_move_in_y) initial_y = translate_y;
}
is_interacting = false;
reset_state();
},
event_options
);
function calculate_inverse_scale() {
let inverse_scale = node.offsetWidth / node_rect.width;
if (isNaN(inverse_scale)) inverse_scale = 1;
return inverse_scale;
}
return {
destroy: () => controller.abort(),
update: (options2) => {
axis = options2.axis || "both";
disabled = options2.disabled ?? false;
ignoreMultitouch = options2.ignoreMultitouch ?? false;
handle = options2.handle;
bounds = options2.bounds;
recomputeBounds = options2.recomputeBounds ?? DEFAULT_RECOMPUTE_BOUNDS;
cancel = options2.cancel;
applyUserSelectHack = options2.applyUserSelectHack ?? true;
grid = options2.grid;
gpuAcceleration = options2.gpuAcceleration ?? true;
legacyTranslate = options2.legacyTranslate ?? false;
transform = options2.transform;
threshold = { ...DEFAULT_DRAG_THRESHOLD, ...options2.threshold ?? {} };
const dragged = node_class_list.contains(defaultClassDragged);
node_class_list.remove(defaultClass, defaultClassDragged);
defaultClass = options2.defaultClass ?? "neodrag";
defaultClassDragging = options2.defaultClassDragging ?? "neodrag-dragging";
defaultClassDragged = options2.defaultClassDragged ?? "neodrag-dragged";
node_class_list.add(defaultClass);
if (dragged) node_class_list.add(defaultClassDragged);
if (is_controlled) {
x_offset = translate_x = options2.position?.x ?? translate_x;
y_offset = translate_y = options2.position?.y ?? translate_y;
set_translate();
}
}
};
}
var clamp = (val, min, max) => Math.min(Math.max(val, min), max);
var is_string = (val) => typeof val === "string";
var snap_to_grid = ([x_snap, y_snap], pending_x, pending_y) => {
const calc = (val, snap) => snap === 0 ? 0 : Math.ceil(val / snap) * snap;
const x = calc(pending_x, x_snap);
const y = calc(pending_y, y_snap);
return [x, y];
};
function get_handle_els(handle, node) {
if (!handle) return [node];
if (is_HTMLElement(handle)) return [handle];
if (Array.isArray(handle)) return handle;
const handle_els = node.querySelectorAll(handle);
if (handle_els === null)
throw new Error(
"Selector passed for `handle` option should be child of the element on which the action is applied"
);
return Array.from(handle_els.values());
}
function get_cancel_elements(cancel, node) {
if (!cancel) return [];
if (is_HTMLElement(cancel)) return [cancel];
if (Array.isArray(cancel)) return cancel;
const cancel_els = node.querySelectorAll(cancel);
if (cancel_els === null)
throw new Error(
"Selector passed for `cancel` option should be child of the element on which the action is applied"
);
return Array.from(cancel_els.values());
}
var cancel_element_contains = (cancel_elements, drag_elements) => cancel_elements.some((cancelEl) => drag_elements.some((el) => cancelEl.contains(el)));
function compute_bound_rect(bounds, rootNode) {
if (bounds === void 0) return;
if (is_HTMLElement(bounds)) return bounds.getBoundingClientRect();
if (typeof bounds === "object") {
const { top = 0, left = 0, right = 0, bottom = 0 } = bounds;
const computed_right = window.innerWidth - right;
const computed_bottom = window.innerHeight - bottom;
return { top, right: computed_right, bottom: computed_bottom, left };
}
if (bounds === "parent") return rootNode.parentNode.getBoundingClientRect();
const node = document.querySelector(bounds);
if (node === null)
throw new Error("The selector provided for bound doesn't exists in the document.");
return node.getBoundingClientRect();
}
var set_style = (el, style, value) => el.style.setProperty(style, value);
var is_HTMLElement = (obj) => obj instanceof HTMLElement;
function unwrap_handle_cancel(val) {
if (val == void 0 || typeof val === "string" || val instanceof HTMLElement) return val;
if ("current" in val) return val.current;
if (Array.isArray(val)) {
return val.map((v) => v instanceof HTMLElement ? v : v.current);
}
}
function useDraggable(nodeRef, options = {}) {
const update_ref = useRef();
const [isDragging, set_is_dragging] = useState(false);
const [dragState, set_drag_state] = useState();
let { onDragStart, onDrag, onDragEnd, handle, cancel } = options;
let new_handle = unwrap_handle_cancel(handle);
let new_cancel = unwrap_handle_cancel(cancel);
function call_event(arg, cb) {
set_drag_state(arg);
cb?.(arg);
}
function custom_on_drag_start(arg) {
set_is_dragging(true);
call_event(arg, onDragStart);
}
function custom_on_drag(arg) {
call_event(arg, onDrag);
}
function custom_on_drag_end(arg) {
set_is_dragging(false);
call_event(arg, onDragEnd);
}
useEffect(() => {
if (typeof window === "undefined") return;
const node = nodeRef.current;
if (!node) return;
({ onDragStart, onDrag, onDragEnd } = options);
const { update, destroy } = draggable(node, {
...options,
handle: new_handle,
cancel: new_cancel,
onDragStart: custom_on_drag_start,
onDrag: custom_on_drag,
onDragEnd: custom_on_drag_end
});
update_ref.current = update;
return destroy;
}, []);
useEffect(() => {
update_ref.current?.({
...options,
handle: unwrap_handle_cancel(handle),
cancel: unwrap_handle_cancel(cancel),
onDragStart: custom_on_drag_start,
onDrag: custom_on_drag,
onDragEnd: custom_on_drag_end
});
}, [options]);
return { isDragging, dragState };
}
export { useDraggable };