Skip to content

Feature: useSelectionContext #49

@icedemon72

Description

@icedemon72

I need to extract the selected text (with coordinates, text, pages etc.) and I do it like this

export function selectionToRestriction(e: MouseEvent, root: HTMLElement, zoom: number = 1): SelectionResult | null {
  const sel = window.getSelection();
  if (!sel || sel.isCollapsed) return null;

  const exactText = sel.toString().trim();
  if (!exactText) return null;

  const range = sel.getRangeAt(0);
  const clientRects = Array.from(range.getClientRects());
  if (!clientRects.length) return null;

  const pageNodes = Array.from(root.querySelectorAll<HTMLElement>('[data-rp^="page-"]')).map(node => ({
    page: Number(node.getAttribute('data-rp')!.replace('page-', '')),
    rect: node.getBoundingClientRect()
  }));

  const pageMap = new Map<number, DOMRect[]>();

  for (const r of clientRects) {
    if (r.width < 0.5 || r.height < 0.5) continue;

    const cx = r.left + r.width / 2;
    const cy = r.top + r.height / 2;

    const owningPage = pageNodes.find(
      p => cx >= p.rect.left && cx <= p.rect.right && cy >= p.rect.top && cy <= p.rect.bottom
    );

    if (!owningPage) continue;

    if (!pageMap.has(owningPage.page)) pageMap.set(owningPage.page, []);
    pageMap.get(owningPage.page)!.push(r);
  }

  if (!pageMap.size) return null;

  const containerRect = root.getBoundingClientRect();
  const metadataList: RestrictionMetadata[] = [];

  for (const [page, rects] of pageMap) {
    const coords = rects.map(r => ({
      xStart: (r.left - containerRect.left) / zoom,
      yStart: (r.top - containerRect.top) / zoom,
      width: r.width / zoom,
      height: r.height / zoom
    }));

    const coordsFiltered = coords.filter(r => r.width > 0.5 && r.height > 0.5);
    if (!coordsFiltered.length) continue;

    const merged = mergeOverlappingRects(mergeRectsByLine(coordsFiltered));
    const mergedFiltered = merged.filter(r => r.width > 0.5 && r.height > 0.5);

    if (!mergedFiltered.length) continue;

    const area = mergedFiltered.reduce(
      (acc, r) => {
        const right = r.xStart + r.width;
        const bottom = r.yStart + r.height;

        return {
          xStart: Math.min(acc.xStart, r.xStart),
          yStart: Math.min(acc.yStart, r.yStart),
          width: Math.max(acc.width, right - acc.xStart),
          height: Math.max(acc.height, bottom - acc.yStart)
        };
      },
      { ...mergedFiltered[0] }
    );

    metadataList.push({ page, exactText, textRows: mergedFiltered, area });
  }

  metadataList.sort((a, b) => a.page - b.page);

  return {
    text: exactText,
    popup: {
      text: exactText,
      x: (e.clientX - containerRect.left) / zoom,
      y: (e.clientY - containerRect.top) / zoom
    },
    newRestriction: {
      coordinates: metadataList
    }
  };
}
useEffect(() => {
    const root = rootRef.current;

    if (!root) return;

    const onMouseUp = (e: MouseEvent) => {
      const restriction = selectionToRestriction(e, root, currentZoom);
      if (!restriction) {
        setPopup(null);
        return;
      }
      setPopup({ ...restriction.popup });
      setNewRestriction(restriction.newRestriction);
    };

    root.addEventListener('mouseup', onMouseUp);
    return () => root.removeEventListener('mouseup', onMouseUp);
  }, [rootRef, currentZoom, setPopup, setNewRestriction]);

It would be nice if we had something similar natively

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions