feat(annotator): responsive layout for the loupe (#3568)
This commit is contained in:
@@ -1,61 +1,42 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import {
|
||||
getEffectiveLoupePoint,
|
||||
getExternalDragHandle,
|
||||
toParentViewportPoint,
|
||||
} from '@/app/reader/utils/annotatorUtil';
|
||||
import { getExternalDragHandle, toParentViewportPoint } from '@/app/reader/utils/annotatorUtil';
|
||||
import { Point } from '@/utils/sel';
|
||||
|
||||
describe('getEffectiveLoupePoint', () => {
|
||||
it('returns loupePoint when set (internal handle drag takes priority)', () => {
|
||||
const loupePoint: Point = { x: 150, y: 180 };
|
||||
const externalDragPoint: Point = { x: 280, y: 200 };
|
||||
const result = getEffectiveLoupePoint(loupePoint, externalDragPoint);
|
||||
expect(result).toBe(loupePoint);
|
||||
});
|
||||
|
||||
it('returns externalDragPoint when loupePoint is null (smooth pointer tracking)', () => {
|
||||
const externalDragPoint: Point = { x: 280, y: 200 };
|
||||
const result = getEffectiveLoupePoint(null, externalDragPoint);
|
||||
expect(result).toBe(externalDragPoint);
|
||||
});
|
||||
|
||||
it('returns null when both loupePoint and externalDragPoint are null', () => {
|
||||
const result = getEffectiveLoupePoint(null, null);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when externalDragPoint is undefined', () => {
|
||||
const result = getEffectiveLoupePoint(null, undefined);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getExternalDragHandle', () => {
|
||||
const currentStart: Point = { x: 100, y: 200 };
|
||||
const currentEnd: Point = { x: 300, y: 200 };
|
||||
|
||||
it('forward drag — externalDragPoint closer to end → returns end', () => {
|
||||
const result = getExternalDragHandle({ x: 280, y: 200 }, currentStart, currentEnd);
|
||||
const result = getExternalDragHandle(currentStart, currentEnd, { x: 280, y: 200 });
|
||||
expect(result).toBe('end');
|
||||
});
|
||||
|
||||
it('backward drag — externalDragPoint closer to start → returns start', () => {
|
||||
const result = getExternalDragHandle({ x: 120, y: 200 }, currentStart, currentEnd);
|
||||
const result = getExternalDragHandle(currentStart, currentEnd, { x: 120, y: 200 });
|
||||
expect(result).toBe('start');
|
||||
});
|
||||
|
||||
it('returns null when externalDragPoint is null', () => {
|
||||
const result = getExternalDragHandle(currentStart, currentEnd, null);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when externalDragPoint is undefined', () => {
|
||||
const result = getExternalDragHandle(currentStart, currentEnd);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('vertical text — works with vertical coordinates', () => {
|
||||
const vStart: Point = { x: 200, y: 100 };
|
||||
const vEnd: Point = { x: 200, y: 400 };
|
||||
const result = getExternalDragHandle({ x: 200, y: 350 }, vStart, vEnd);
|
||||
const result = getExternalDragHandle(vStart, vEnd, { x: 200, y: 350 });
|
||||
expect(result).toBe('end');
|
||||
});
|
||||
|
||||
it('equal distance — returns end (deterministic tie-breaking)', () => {
|
||||
// Midpoint between start(100,200) and end(300,200) is (200,200)
|
||||
// distToStart === distToEnd, so !(distToStart < distToEnd) → returns 'end'
|
||||
const result = getExternalDragHandle({ x: 200, y: 200 }, currentStart, currentEnd);
|
||||
const result = getExternalDragHandle(currentStart, currentEnd, { x: 200, y: 200 });
|
||||
expect(result).toBe('end');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,11 +9,7 @@ import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { useAnnotationEditor } from '../../hooks/useAnnotationEditor';
|
||||
import {
|
||||
getEffectiveLoupePoint,
|
||||
getExternalDragHandle,
|
||||
getHighlightColorHex,
|
||||
} from '../../utils/annotatorUtil';
|
||||
import { getExternalDragHandle, getHighlightColorHex } from '../../utils/annotatorUtil';
|
||||
import MagnifierLoupe from './MagnifierLoupe';
|
||||
|
||||
interface HandleProps {
|
||||
@@ -253,17 +249,17 @@ const AnnotationRangeEditor: React.FC<AnnotationRangeEditorProps> = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const effectiveLoupePoint = getEffectiveLoupePoint(loupePoint, externalDragPoint);
|
||||
const loupeDragPoint = externalDragPoint;
|
||||
const effectiveLoupePoint = loupePoint ?? loupeDragPoint;
|
||||
const activeHandle =
|
||||
draggingHandle ??
|
||||
(externalDragPoint ? getExternalDragHandle(externalDragPoint, currentStart, currentEnd) : null);
|
||||
draggingHandle ?? getExternalDragHandle(currentStart, currentEnd, loupeDragPoint);
|
||||
|
||||
const showLoupe = effectiveLoupePoint !== null && appService?.isMobile && !viewSettings?.vertical;
|
||||
|
||||
return (
|
||||
<div className='pointer-events-none fixed inset-0 z-50'>
|
||||
<Handle
|
||||
hidden={activeHandle === 'end'}
|
||||
hidden={activeHandle === 'end' || loupeDragPoint !== null}
|
||||
position={currentStart}
|
||||
isVertical={isVertical}
|
||||
type='start'
|
||||
@@ -273,7 +269,7 @@ const AnnotationRangeEditor: React.FC<AnnotationRangeEditorProps> = ({
|
||||
onDragEnd={handleDragEnd}
|
||||
/>
|
||||
<Handle
|
||||
hidden={activeHandle === 'start'}
|
||||
hidden={activeHandle === 'start' || loupeDragPoint !== null}
|
||||
position={currentEnd}
|
||||
isVertical={isVertical}
|
||||
type='end'
|
||||
|
||||
@@ -134,6 +134,10 @@ export const useTextSelector = (
|
||||
stopInstantAnnotating(ev);
|
||||
const handled = await handleInstantAnnotationPointerUp(doc, index, ev);
|
||||
if (handled) {
|
||||
isTextSelected.current = true;
|
||||
setTimeout(() => {
|
||||
isTextSelected.current = false;
|
||||
}, 200);
|
||||
return;
|
||||
} else {
|
||||
// If instant annotation was not created, we let the event propagate
|
||||
|
||||
@@ -13,18 +13,12 @@ export const getHighlightColorHex = (
|
||||
return customColors?.[color] ?? HIGHLIGHT_COLOR_HEX[color];
|
||||
};
|
||||
|
||||
export function getEffectiveLoupePoint(
|
||||
loupePoint: Point | null,
|
||||
externalDragPoint: Point | null | undefined,
|
||||
): Point | null {
|
||||
return loupePoint ?? externalDragPoint ?? null;
|
||||
}
|
||||
|
||||
export function getExternalDragHandle(
|
||||
externalDragPoint: Point,
|
||||
currentStart: Point,
|
||||
currentEnd: Point,
|
||||
): 'start' | 'end' {
|
||||
externalDragPoint?: Point | null,
|
||||
): 'start' | 'end' | null {
|
||||
if (!externalDragPoint) return null;
|
||||
const distToStart = Math.hypot(
|
||||
externalDragPoint.x - currentStart.x,
|
||||
externalDragPoint.y - currentStart.y,
|
||||
|
||||
+1
-1
Submodule packages/foliate-js updated: b47e523678...d150ae5eaa
Reference in New Issue
Block a user