@activecollab/components
Version:
ActiveCollab Components
218 lines (202 loc) • 8.97 kB
JavaScript
import _extends from "@babel/runtime/helpers/esm/extends";
/**
* StackedCard resize policy — pure math.
*
* The card reports intent; the HOST applies a resize policy (spec §7). This
* module is that policy, packaged as pure functions so the SAME code path
* serves the pointer drag and the keyboard resize. Nothing here touches the
* DOM, React or the card's content: it takes a start size, a delta and a
* policy, and returns a clamped size. `useStackedCardResize` wires events to
* it; the card's intrinsic content minimum is measured by the host and passed
* in as `contentMin`, so this module knows nothing about heroes or footers.
*
* The policy has four parts:
* - bounds — optional min/max per axis; unset means unbounded.
* - proportional — lock the aspect ratio captured at the start of the gesture;
* the scale is clamped by the BINDING axis so the ratio never
* distorts at a bound.
* - step — optional grid snap; proportion wins over the grid.
* - content min — the effective min is max(policyMin, contentMin) per axis,
* so a card can never be crushed below what its content needs.
*/
/** The card's intrinsic minimum, measured from its real content. */
/** The size captured at the start of a gesture (pointer down or a key press). */
/**
* `width` locks the vertical axis: an auto-height card takes a width and lets
* its rows set the height, so the gesture's vertical component is ignored.
*/
/** Which input drove a resize event. Hosts log undo entries per source. */
/** Keyboard resize nudges by the policy step, or this when no step is set. */
export const DEFAULT_KEYBOARD_STEP = 8;
/** Shift + arrow resizes by a larger increment (the familiar coarse nudge). */
export const SHIFT_STEP_MULTIPLIER = 4;
/** Keys the separator handles; everything else falls through to the browser. */
export const RESIZE_KEYS = ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"];
export const isResizeKey = key => RESIZE_KEYS.includes(key);
export const clamp = (v, lo, hi) => Math.min(hi != null ? hi : Number.POSITIVE_INFINITY, Math.max(lo, v));
export const snap = (v, step) => step && step > 0 ? Math.round(v / step) * step : v;
export const makeStart = (size, contentMin) => ({
w: size.w,
h: size.h,
contentMin
});
/**
* The effective bounds a size is clamped to: the policy min floored by the
* content min, and the policy max (unbounded when unset). Independent of any
* live gesture, so a host can also use it for the aria value and Home/End.
*/
export const effectiveBounds = (policy, contentMin) => {
var _policy$minW, _policy$minH, _policy$maxW, _policy$maxH;
return {
wMin: Math.max((_policy$minW = policy.minW) != null ? _policy$minW : 0, contentMin.w),
hMin: Math.max((_policy$minH = policy.minH) != null ? _policy$minH : 0, contentMin.h),
wMax: (_policy$maxW = policy.maxW) != null ? _policy$maxW : Number.POSITIVE_INFINITY,
hMax: (_policy$maxH = policy.maxH) != null ? _policy$maxH : Number.POSITIVE_INFINITY
};
};
/**
* Whether an axis has a floor anyone asked for. `effectiveBounds` reports 0 for
* an unset minimum — correct as a clamp, but not something Home should jump to:
* an axis nobody gave a minimum must not collapse to nothing.
*/
const hasFloor = (policyMin, contentMin) => policyMin !== undefined || contentMin > 0;
/**
* Apply a delta to the start size under the policy — the single path for both
* inputs.
*
* `constrain` is the transient Shift-to-constrain lock: a free-form policy
* behaves proportionally for that one gesture; a proportional policy is already
* locked and ignores it. `axis: "width"` drops the gesture's vertical component;
* the height then holds still in free-form mode, and still follows the ratio in
* proportional mode (that is what proportional means).
*/
export const applyResize = function (start, delta, policy, opts) {
if (opts === void 0) {
opts = {};
}
const _effectiveBounds = effectiveBounds(policy, start.contentMin),
wMin = _effectiveBounds.wMin,
hMin = _effectiveBounds.hMin,
wMax = _effectiveBounds.wMax,
hMax = _effectiveBounds.hMax;
const widthOnly = opts.axis === "width";
const dx = delta.dx;
const dy = widthOnly ? 0 : delta.dy;
// A ratio needs two non-zero sides to be a ratio at all; a width-only card
// parks a placeholder height, so fall back to free-form rather than dividing
// by zero.
const proportional = (policy.proportional || opts.constrain === true) && start.w > 0 && start.h > 0;
let w;
let h;
if (proportional) {
// one scale drives both axes; the corner's dominant direction wins, so a
// vertical drag is as effective as a horizontal one
const sW = (start.w + dx) / start.w;
const sH = (start.h + dy) / start.h;
let scale = Math.abs(sW - 1) >= Math.abs(sH - 1) ? sW : sH;
// step: proportion wins over the grid. Snap through the SCALE (driven off
// the width edge) so both axes move together and the ratio survives; the
// grid yields whenever honouring it would cost the ratio.
if (policy.step) {
scale = snap(start.w * scale, policy.step) / start.w;
}
// Then clamp the scale so both axes stay in bounds (the binding constraint):
// the ratio can never break at a bound — whichever edge is limiting stops
// both. The clamp comes last on purpose: a bound is a hard stop, so the card
// sits exactly on it rather than at the nearest grid line inside it.
const scaleMin = Math.max(wMin / start.w, hMin / start.h);
const scaleMax = Math.min(wMax / start.w, hMax / start.h);
scale = clamp(scale, scaleMin, Math.max(scaleMin, scaleMax));
w = start.w * scale;
h = start.h * scale;
} else {
w = clamp(snap(start.w + dx, policy.step), wMin, wMax);
h = widthOnly ? start.h : clamp(snap(start.h + dy, policy.step), hMin, hMax);
}
return {
w: Math.round(w),
h: Math.round(h)
};
};
/**
* Translate a resize key into the next size, reusing `applyResize` so the
* keyboard clamps exactly like the pointer. Arrows nudge by the step (Shift ×4);
* Home/End jump to the min/max of the allowed range, leaving any axis without
* that bound where it is. Returns null for a key the handle does not own, and
* for a vertical key on a width-only card.
*/
export const keyboardResize = function (key, size, policy, contentMin, opts) {
if (opts === void 0) {
opts = {};
}
const widthOnly = opts.axis === "width";
if (widthOnly && (key === "ArrowUp" || key === "ArrowDown")) return null;
const base = policy.step && policy.step > 0 ? policy.step : DEFAULT_KEYBOARD_STEP;
const step = base * (opts.shiftKey ? SHIFT_STEP_MULTIPLIER : 1);
const start = makeStart(size, contentMin);
const b = effectiveBounds(policy, contentMin);
const apply = delta => applyResize(start, delta, policy, {
axis: opts.axis
});
/**
* Home and End are absolute jumps to a bound, not nudges, so they ignore the
* grid: landing 4px off the minimum because that is where the nearest grid
* line fell would mean the keyboard could never reach the bound the pointer
* stops at.
*/
const jump = delta => applyResize(start, delta, _extends({}, policy, {
step: 0
}), {
axis: opts.axis
});
switch (key) {
case "ArrowRight":
return apply({
dx: step,
dy: 0
});
case "ArrowLeft":
return apply({
dx: -step,
dy: 0
});
case "ArrowDown":
return apply({
dx: 0,
dy: step
});
case "ArrowUp":
return apply({
dx: 0,
dy: -step
});
case "Home":
// jump to the min corner; an axis with no floor of its own stays put
return jump({
dx: hasFloor(policy.minW, contentMin.w) ? b.wMin - size.w : 0,
dy: !widthOnly && hasFloor(policy.minH, contentMin.h) ? b.hMin - size.h : 0
});
case "End":
// jump to the max corner; an unbounded axis simply doesn't move
return jump({
dx: Number.isFinite(b.wMax) ? b.wMax - size.w : 0,
dy: !widthOnly && Number.isFinite(b.hMax) ? b.hMax - size.h : 0
});
default:
return null;
}
};
/**
* The handle's `aria-valuenow`: the current size as a percent (0–100) of its
* allowed range. The corner resizes both axes, but the value must be a single
* number, so it reports the WIDTH axis — the one axis the fixed-box and the
* width-only cards share. Returns 0 when the range isn't finite (no max bound).
*/
export const measurePercent = (size, policy, contentMin) => {
const _effectiveBounds2 = effectiveBounds(policy, contentMin),
wMin = _effectiveBounds2.wMin,
wMax = _effectiveBounds2.wMax;
if (!Number.isFinite(wMax) || wMax <= wMin) return 0;
return Math.round(clamp((size.w - wMin) / (wMax - wMin) * 100, 0, 100));
};
//# sourceMappingURL=resizePolicy.js.map