UNPKG

@activecollab/components

Version:

ActiveCollab Components

218 lines (202 loc) 8.97 kB
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