@atlaskit/pragmatic-drag-and-drop-hitbox
Version:
An optional package for Pragmatic drag and drop that enables the attaching of interaction information to a drop target
148 lines (143 loc) • 3.87 kB
JavaScript
import { isShallowEqual } from './internal/isShallowEqual';
import { stable } from './internal/stable';
import { uniqueKey } from './tree-item';
function getCenter(rect) {
return {
x: (rect.right + rect.left) / 2,
y: (rect.bottom + rect.top) / 2
};
}
function standardHitbox({
client,
borderBox
}) {
const quarterOfHeight = borderBox.height / 4;
// In the top 1/4: reorder-above
// On the line = in the top 1/4 to give this zone a bit more space
if (client.y <= borderBox.top + quarterOfHeight) {
return 'reorder-above';
}
// In the bottom 1/4: reorder-below
// On the line = in the bottom 1/4 to give this zone a bit more space
if (client.y >= borderBox.bottom - quarterOfHeight) {
return 'reorder-below';
}
return 'make-child';
}
function getInstruction({
element,
input,
currentLevel,
indentPerLevel,
mode
}) {
const client = {
x: input.clientX,
y: input.clientY
};
const borderBox = element.getBoundingClientRect();
if (mode === 'standard') {
const type = standardHitbox({
borderBox,
client
});
return {
type,
indentPerLevel,
currentLevel
};
}
const center = getCenter(borderBox);
if (mode === 'expanded') {
// leveraging "standard" hitbox to ensure that the 'reorder-above' hit zone is
// exactly the same for "standard" and "expanded" items
const type = standardHitbox({
borderBox,
client
});
return {
// Use the "standard" hitbox for "reorder above",
// The rest of the item is "make-child"
type: type === 'reorder-above' ? type : 'make-child',
indentPerLevel,
currentLevel
};
}
// `mode` is "last-in-group"
const visibleInset = indentPerLevel * currentLevel;
// Before the left edge of the visible item
if (client.x < borderBox.left + visibleInset) {
// Above the center: `reorder-above`
if (client.y < center.y) {
return {
type: 'reorder-above',
indentPerLevel,
currentLevel
};
}
// On or below the center: `reparent`
// On the center = `reparent` as we are giving a slightly bigger hitbox to this
// action as it is the only place a user can do it
const rawLevel = (client.x - borderBox.left) / indentPerLevel;
// We can get sub pixel negative numbers as getBoundingClientRect gives sub-pixel accuracy,
// where as clientX is rounded to the nearest pixel.
// Using Math.max() ensures we can never get a negative level
const desiredLevel = Math.max(Math.floor(rawLevel), 0);
return {
type: 'reparent',
desiredLevel,
indentPerLevel,
currentLevel
};
}
// On the visible item
return {
type: standardHitbox({
borderBox,
client
}),
indentPerLevel,
currentLevel
};
}
function areInstructionsEqual(a, b) {
// Shortcut
if (a.type !== b.type) {
return false;
}
if (a.type === 'instruction-blocked' && b.type === 'instruction-blocked') {
return areInstructionsEqual(a.desired, b.desired);
}
return isShallowEqual(a, b);
}
// Note: not using `memoize-one` as all we need is a cached value.
// We do not need to avoid executing an expensive function.
const memoizeInstruction = stable(areInstructionsEqual);
function applyInstructionBlock({
desired,
block
}) {
if (block !== null && block !== void 0 && block.includes(desired.type) && desired.type !== 'instruction-blocked') {
const blocked = {
type: 'instruction-blocked',
desired
};
return blocked;
}
return desired;
}
export function attachInstruction(userData, {
block,
...rest
}) {
const desired = getInstruction(rest);
const withBlock = applyInstructionBlock({
desired,
block
});
const memoized = memoizeInstruction(withBlock);
return {
...userData,
[uniqueKey]: memoized
};
}