pdf-free.openapps.xyz

Source

This app’s code and revision history are public. Download a copy or explore the files below.

Public
The app’s code and revision history are public.
Owned app · rev 0341fee61377
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone 7716a56ff3432bc0c1e4837c
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
← All files
src/lib/sort.ts
// Drag to reorder, for mouse and touch. A mouse drag starts after a small move. A touch drag
// starts after a short press, so a normal swipe still scrolls the page.
import { useCallback, useEffect, useId, useRef, useState, type MouseEvent, type PointerEvent } from "react";

type Press = {
  id: string;
  el: HTMLElement;
  x: number;
  y: number;
  lastX: number;
  lastY: number;
  touch: boolean;
  active: boolean;
  at: number | null;
  after: boolean;
  timer?: number;
  ghost?: HTMLElement;
  frame?: number;
};

/**
 * `axis` says how items flow: "x" for a wrapping grid (drop before or after by the pointer's
 * x within an item), "y" for a vertical list. `onDrop(id, to)` gets the insertion index,
 * counted in the list as it was, dragged item included.
 */
export function useDragSort(onDrop: (id: string, to: number) => void, axis: "x" | "y") {
  const group = useId();
  // `at` is the item under the pointer and `after` which side of it the dragged item would land.
  const [drag, setDrag] = useState<{ id: string; at: number | null; after: boolean } | null>(null);
  const press = useRef<Press | null>(null);
  const swallowClick = useRef(false);
  const dropRef = useRef(onDrop);
  dropRef.current = onDrop;

  const end = useCallback((drop: boolean) => {
    const p = press.current;
    if (!p) return;
    press.current = null;
    window.clearTimeout(p.timer);
    if (p.frame) cancelAnimationFrame(p.frame);
    p.ghost?.remove();
    document.body.classList.remove("is-sorting");
    setDrag(null);
    if (p.active) {
      swallowClick.current = true;
      window.setTimeout(() => { swallowClick.current = false; }, 0);
      if (drop && p.at !== null) dropRef.current(p.id, p.at + (p.after ? 1 : 0));
    }
  }, []);

  const hitTest = useCallback((p: Press) => {
    const hit = document.elementFromPoint(p.lastX, p.lastY)?.closest<HTMLElement>("[data-sort-index]");
    if (!hit || hit.dataset.sortGroup !== group) return;
    const r = hit.getBoundingClientRect();
    const after = axis === "x" ? p.lastX > r.left + r.width / 2 : p.lastY > r.top + r.height / 2;
    const at = Number(hit.dataset.sortIndex);
    if (at !== p.at || after !== p.after) {
      p.at = at;
      p.after = after;
      setDrag({ id: p.id, at, after });
    }
  }, [axis, group]);

  const activate = useCallback((p: Press) => {
    p.active = true;
    const r = p.el.getBoundingClientRect();
    const ghost = p.el.cloneNode(true) as HTMLElement;
    ghost.classList.add("drag-ghost");
    ghost.removeAttribute("data-sort-index");
    Object.assign(ghost.style, { left: `${r.left}px`, top: `${r.top}px`, width: `${r.width}px`, height: `${r.height}px` });
    document.body.append(ghost);
    document.body.classList.add("is-sorting");
    p.ghost = ghost;
    setDrag({ id: p.id, at: null, after: false });
    if (p.touch) navigator.vibrate?.(10);
    // Scroll when the pointer nears the top or bottom edge, and keep the drop spot current.
    const tick = () => {
      const edge = 90;
      const speed = p.lastY < edge ? -(edge - p.lastY) / 4 : p.lastY > innerHeight - edge ? (p.lastY - innerHeight + edge) / 4 : 0;
      if (speed) window.scrollBy(0, speed);
      hitTest(p);
      p.frame = requestAnimationFrame(tick);
    };
    p.frame = requestAnimationFrame(tick);
  }, [hitTest]);

  useEffect(() => {
    const move = (e: globalThis.PointerEvent) => {
      const p = press.current;
      if (!p) return;
      p.lastX = e.clientX;
      p.lastY = e.clientY;
      const dist = Math.hypot(e.clientX - p.x, e.clientY - p.y);
      if (!p.active) {
        if (p.touch && dist > 10) end(false); // the person is scrolling
        else if (!p.touch && dist > 6) activate(p);
        return;
      }
      p.ghost!.style.transform = `translate(${e.clientX - p.x}px, ${e.clientY - p.y}px)`;
    };
    const up = () => end(true);
    const cancel = () => end(false);
    const touchMove = (e: TouchEvent) => { if (press.current?.active) e.preventDefault(); };
    const key = (e: KeyboardEvent) => { if (e.key === "Escape") end(false); };
    window.addEventListener("pointermove", move);
    window.addEventListener("pointerup", up);
    window.addEventListener("pointercancel", cancel);
    window.addEventListener("touchmove", touchMove, { passive: false });
    window.addEventListener("keydown", key);
    return () => {
      window.removeEventListener("pointermove", move);
      window.removeEventListener("pointerup", up);
      window.removeEventListener("pointercancel", cancel);
      window.removeEventListener("touchmove", touchMove);
      window.removeEventListener("keydown", key);
      end(false);
    };
  }, [activate, end]);

  const onPointerDown = (e: PointerEvent<HTMLElement>, id: string) => {
    if (e.button !== 0 || press.current || (e.target as HTMLElement).closest("[data-no-drag]")) return;
    const p: Press = { id, el: e.currentTarget, x: e.clientX, y: e.clientY, lastX: e.clientX, lastY: e.clientY, touch: e.pointerType !== "mouse", active: false, at: null, after: false };
    if (p.touch) p.timer = window.setTimeout(() => activate(p), 350);
    press.current = p;
  };

  return {
    drag,
    item: (id: string, index: number) => ({
      "data-sort-group": group,
      "data-sort-index": index,
      onPointerDown: (e: PointerEvent<HTMLElement>) => onPointerDown(e, id),
    }),
    // A drag ends with a click on the item under the pointer; that click is not a selection.
    onClickCapture: (e: MouseEvent) => {
      if (swallowClick.current) {
        e.preventDefault();
        e.stopPropagation();
        swallowClick.current = false;
      }
    },
  };
}

Change history

Quality pass: compress, PDF to JPG, phone layout, job links, llms.txt and index.md
0341fee613 · 5 hours ago
Bounded source sync
f00de5deb0 · 1 day ago
Bounded source sync
3c8124e576 · 1 day ago
Bounded source sync
5c14663e0f · 1 day ago
Bounded source sync
ee5c852f40 · 1 day ago