@umodeapporg/ui 0.24.4 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Form/ControlledTabs.js +1 -1
- package/dist/components/Form/ControlledViewToggle.js +1 -1
- package/dist/components/Navigation/UiNavigationContext.js +1 -1
- package/dist/components/UI/ControlledPopover.d.ts +3 -0
- package/dist/components/UI/ControlledPopover.js +19 -7
- package/dist/components/UI/ControlledPopover.js.map +1 -1
- package/dist/components/UI/ImageAnnotator/AnnotationEditor.d.ts +2 -1
- package/dist/components/UI/ImageAnnotator/AnnotationEditor.js +138 -43
- package/dist/components/UI/ImageAnnotator/AnnotationEditor.js.map +1 -1
- package/dist/components/UI/ImageAnnotator/ImageAnnotationDialog.d.ts +3 -1
- package/dist/components/UI/ImageAnnotator/ImageAnnotationDialog.js +3 -0
- package/dist/components/UI/ImageAnnotator/ImageAnnotationDialog.js.map +1 -1
- package/dist/components/UI/ImageAnnotator/canvas/AnnotationCropFrame.d.ts +16 -0
- package/dist/components/UI/ImageAnnotator/canvas/AnnotationCropFrame.js +156 -0
- package/dist/components/UI/ImageAnnotator/canvas/AnnotationCropFrame.js.map +1 -0
- package/dist/components/UI/ImageAnnotator/canvas/AnnotationStage.d.ts +8 -1
- package/dist/components/UI/ImageAnnotator/canvas/AnnotationStage.js +30 -5
- package/dist/components/UI/ImageAnnotator/canvas/AnnotationStage.js.map +1 -1
- package/dist/components/UI/ImageAnnotator/components/AnnotationCropBar.d.ts +12 -0
- package/dist/components/UI/ImageAnnotator/components/AnnotationCropBar.js +89 -0
- package/dist/components/UI/ImageAnnotator/components/AnnotationCropBar.js.map +1 -0
- package/dist/components/UI/ImageAnnotator/components/AnnotationTextOverlay.d.ts +3 -1
- package/dist/components/UI/ImageAnnotator/components/AnnotationTextOverlay.js +3 -2
- package/dist/components/UI/ImageAnnotator/components/AnnotationTextOverlay.js.map +1 -1
- package/dist/components/UI/ImageAnnotator/components/AnnotationToolbar.js +11 -0
- package/dist/components/UI/ImageAnnotator/components/AnnotationToolbar.js.map +1 -1
- package/dist/components/UI/ImageAnnotator/helpers/annotationCrop.d.ts +12 -0
- package/dist/components/UI/ImageAnnotator/helpers/annotationCrop.js +64 -0
- package/dist/components/UI/ImageAnnotator/helpers/annotationCrop.js.map +1 -0
- package/dist/components/UI/ImageAnnotator/hooks/useAnnotationDocument.d.ts +4 -2
- package/dist/components/UI/ImageAnnotator/hooks/useAnnotationDocument.js +23 -12
- package/dist/components/UI/ImageAnnotator/hooks/useAnnotationDocument.js.map +1 -1
- package/dist/components/UI/ImageAnnotator/hooks/useAnnotationShortcuts.js +2 -1
- package/dist/components/UI/ImageAnnotator/hooks/useAnnotationShortcuts.js.map +1 -1
- package/dist/components/UI/ImageAnnotator/interfaces/annotationDocument.d.ts +10 -1
- package/dist/components/UI/ImageAnnotator/interfaces/annotationDocument.js.map +1 -1
- package/dist/locales/en/ui.json.d.ts +17 -2
- package/dist/locales/en/ui.json.js +1 -1
- package/dist/locales/index.d.ts +30 -0
- package/dist/locales/pt/ui.json.d.ts +17 -2
- package/dist/locales/pt/ui.json.js +1 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import { useUiNavigation } from "../Navigation/UiNavigationContext.js";
|
|
|
6
6
|
import { useUiAccess } from "../UI/access/UiAccessContext.js";
|
|
7
7
|
import ControlledTooltip from "../UI/ControlledTooltip.js";
|
|
8
8
|
import { formatNumber } from "../../helpers/formatHelpers.js";
|
|
9
|
-
import { claimUrlParam,
|
|
9
|
+
import { claimUrlParam, URL_PARAM_PRIORITY, TABS_URL_PARAM, releaseUrlParam, queueUrlParamUpdate } from "../../helpers/urlParamSync.js";
|
|
10
10
|
import LoadingSpinner from "../UI/LoadingSpinner.js";
|
|
11
11
|
import { MaterialSymbol } from "../UI/MaterialSymbol/index.js";
|
|
12
12
|
import ControlledButton from "./ControlledButton.js";
|
|
@@ -8,7 +8,7 @@ import LoadingSpinner from "../UI/LoadingSpinner.js";
|
|
|
8
8
|
import { MaterialSymbol } from "../UI/MaterialSymbol/index.js";
|
|
9
9
|
import { formatNumber } from "../../helpers/formatHelpers.js";
|
|
10
10
|
import { uiLogger } from "../../helpers/uiLogger.js";
|
|
11
|
-
import { TABS_URL_PARAM, claimUrlParam,
|
|
11
|
+
import { TABS_URL_PARAM, claimUrlParam, releaseUrlParam, ownsUrlParam, queueUrlParamUpdate, URL_PARAM_PRIORITY } from "../../helpers/urlParamSync.js";
|
|
12
12
|
const DEFAULT_URL_PARAM_KEY = "view";
|
|
13
13
|
const SIZE_STYLES = {
|
|
14
14
|
sm: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { useContext, useMemo, createContext, forwardRef } from "react";
|
|
3
3
|
const FallbackLink = forwardRef(({ to, children, ...rest }, ref) => /* @__PURE__ */ jsx("a", { ref, href: to, ...rest, children }));
|
|
4
4
|
FallbackLink.displayName = "FallbackLink";
|
|
5
5
|
const createFallbackNavigation = () => {
|
|
@@ -7,6 +7,8 @@ interface IControlledPopoverRootProps {
|
|
|
7
7
|
handler?: (open: boolean) => void;
|
|
8
8
|
placement?: TPopoverPlacement;
|
|
9
9
|
offsetPx?: number;
|
|
10
|
+
/** Sizes the content to the trigger width. Use it when the popover acts as a select dropdown. */
|
|
11
|
+
matchTriggerWidth?: boolean;
|
|
10
12
|
}
|
|
11
13
|
export declare const ControlledPopoverRoot: React.FC<IControlledPopoverRootProps>;
|
|
12
14
|
/**
|
|
@@ -34,6 +36,7 @@ interface IControlledPopoverProps {
|
|
|
34
36
|
onOpenChange?: (open: boolean) => void;
|
|
35
37
|
contentClassName?: string;
|
|
36
38
|
tooltipClassName?: string;
|
|
39
|
+
matchTriggerWidth?: boolean;
|
|
37
40
|
}
|
|
38
41
|
/**
|
|
39
42
|
* A tooltip on the trigger must sit OUTSIDE the handler: the handler clones its child to
|
|
@@ -22,7 +22,8 @@ const ControlledPopoverRoot = ({
|
|
|
22
22
|
open,
|
|
23
23
|
handler,
|
|
24
24
|
placement = "bottom-start",
|
|
25
|
-
offsetPx = 6
|
|
25
|
+
offsetPx = 6,
|
|
26
|
+
matchTriggerWidth = false
|
|
26
27
|
}) => {
|
|
27
28
|
const contentId = useId();
|
|
28
29
|
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
|
|
@@ -35,7 +36,8 @@ const ControlledPopoverRoot = ({
|
|
|
35
36
|
const { setReference, setFloating, floatingRef, referenceRef } = useAnchoredFloating({
|
|
36
37
|
open: isOpen,
|
|
37
38
|
placement,
|
|
38
|
-
offsetPx
|
|
39
|
+
offsetPx,
|
|
40
|
+
matchReferenceWidth: matchTriggerWidth
|
|
39
41
|
});
|
|
40
42
|
return /* @__PURE__ */ jsx(
|
|
41
43
|
PopoverContext.Provider,
|
|
@@ -130,13 +132,23 @@ const ControlledPopover = ({
|
|
|
130
132
|
open,
|
|
131
133
|
onOpenChange,
|
|
132
134
|
contentClassName,
|
|
133
|
-
tooltipClassName
|
|
135
|
+
tooltipClassName,
|
|
136
|
+
matchTriggerWidth
|
|
134
137
|
}) => {
|
|
135
138
|
const handle = /* @__PURE__ */ jsx(ControlledPopoverHandler, { children: trigger });
|
|
136
|
-
return /* @__PURE__ */ jsxs(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
return /* @__PURE__ */ jsxs(
|
|
140
|
+
ControlledPopoverRoot,
|
|
141
|
+
{
|
|
142
|
+
open,
|
|
143
|
+
handler: onOpenChange,
|
|
144
|
+
placement,
|
|
145
|
+
matchTriggerWidth,
|
|
146
|
+
children: [
|
|
147
|
+
triggerTooltip ? /* @__PURE__ */ jsx(ControlledTooltip, { content: triggerTooltip, className: classNames(tooltipClassName), children: handle }) : handle,
|
|
148
|
+
/* @__PURE__ */ jsx(ControlledPopoverContent, { className: contentClassName, children })
|
|
149
|
+
]
|
|
150
|
+
}
|
|
151
|
+
);
|
|
140
152
|
};
|
|
141
153
|
export {
|
|
142
154
|
ControlledPopoverContent,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ControlledPopover.js","sources":["../../../src/components/UI/ControlledPopover.tsx"],"sourcesContent":["import React, { cloneElement, createContext, isValidElement, useContext, useEffect, useId, useState } from 'react';\nimport classNames from 'classnames';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { createPortal } from 'react-dom';\nimport { twMerge } from 'tailwind-merge';\n\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { composeRefs } from '@/helpers/composeRefs';\nimport { isPressOnLayeredSurface } from '@/helpers/layeredSurfaces';\nimport { getPortalHost } from '@/helpers/portalHost';\nimport { TFloatingPlacement, useAnchoredFloating } from '@/hooks/useAnchoredFloating';\nimport { floatingVariants } from '@/motion';\n\nexport type TPopoverPlacement = TFloatingPlacement;\n\ninterface IPopoverContext {\n open: boolean;\n setOpen: (open: boolean) => void;\n placement: TFloatingPlacement;\n offsetPx: number;\n contentId: string;\n setReference: (node: HTMLElement | null) => void;\n setFloating: (node: HTMLElement | null) => void;\n floatingRef: React.MutableRefObject<HTMLElement | null>;\n referenceRef: React.MutableRefObject<HTMLElement | null>;\n}\n\nconst PopoverContext = createContext<IPopoverContext | null>(null);\n\nconst usePopoverContext = (): IPopoverContext => {\n const context = useContext(PopoverContext);\n if (!context) throw new Error('Popover parts must be rendered inside ControlledPopoverRoot');\n return context;\n};\n\ninterface IControlledPopoverRootProps {\n children: React.ReactNode;\n open?: boolean;\n handler?: (open: boolean) => void;\n placement?: TPopoverPlacement;\n offsetPx?: number;\n}\n\nexport const ControlledPopoverRoot: React.FC<IControlledPopoverRootProps> = ({\n children,\n open,\n handler,\n placement = 'bottom-start',\n offsetPx = 6,\n}) => {\n const contentId = useId();\n const [uncontrolledOpen, setUncontrolledOpen] = useState(false);\n\n const isControlled = open !== undefined;\n const isOpen = isControlled ? Boolean(open) : uncontrolledOpen;\n\n const setOpen = (next: boolean) => {\n if (!isControlled) setUncontrolledOpen(next);\n handler?.(next);\n };\n\n const { setReference, setFloating, floatingRef, referenceRef } = useAnchoredFloating({\n open: isOpen,\n placement,\n offsetPx,\n });\n\n return (\n <PopoverContext.Provider\n value={{\n open: isOpen,\n setOpen,\n placement,\n offsetPx,\n contentId,\n setReference,\n setFloating,\n floatingRef,\n referenceRef,\n }}\n >\n {children}\n </PopoverContext.Provider>\n );\n};\n\ninterface IHandlerChildProps {\n onClick?: (event: React.MouseEvent<HTMLElement>) => void;\n ref?: React.Ref<HTMLElement>;\n}\n\n/**\n * Repassa ao filho o que ele recebe de fora, além do que injeta.\n *\n * O gatilho é o alvo natural de um `ControlledTooltip`, e o tooltip se prende clonando o filho e\n * injetando `onPointerEnter`. Aceitando só `children`, este componente engolia essa injeção e o\n * tooltip simplesmente nunca abria, mesmo com a ordem que a documentação manda usar\n * (tooltip POR FORA do handler). Os `onClick` são encadeados: o de fora roda antes de abrir.\n */\nexport const ControlledPopoverHandler = React.forwardRef<\n HTMLElement,\n { children: React.ReactNode } & React.HTMLAttributes<HTMLElement>\n>(({ children, onClick: onOuterClick, ...rest }, forwardedRef) => {\n const { open, setOpen, setReference, contentId } = usePopoverContext();\n\n if (!isValidElement(children)) return <>{children}</>;\n\n const child = children as React.ReactElement<IHandlerChildProps>;\n\n return cloneElement(child, {\n ...rest,\n ref: composeRefs<HTMLElement>(setReference, forwardedRef, (child as { ref?: React.Ref<HTMLElement> }).ref),\n 'aria-expanded': open,\n 'aria-controls': open ? contentId : undefined,\n onClick: (event: React.MouseEvent<HTMLElement>) => {\n onOuterClick?.(event);\n child.props.onClick?.(event);\n setOpen(!open);\n },\n } as IHandlerChildProps);\n});\n\nControlledPopoverHandler.displayName = 'ControlledPopoverHandler';\n\ninterface IControlledPopoverContentProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n 'className' | 'onDrag' | 'onDragStart' | 'onDragEnd' | 'onAnimationStart' | 'onAnimationEnd' | 'onAnimationIteration'\n> {\n children: React.ReactNode;\n className?: string;\n}\n\nexport const ControlledPopoverContent: React.FC<IControlledPopoverContentProps> = ({\n children,\n className,\n ...rest\n}) => {\n const { open, setOpen, contentId, setFloating, floatingRef, referenceRef } = usePopoverContext();\n\n useEffect(() => {\n if (!open) return undefined;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') setOpen(false);\n };\n\n const handlePointerDown = (event: PointerEvent) => {\n const target = event.target as Node | null;\n if (!target) return;\n if (floatingRef.current?.contains(target) || referenceRef.current?.contains(target)) return;\n if (isPressOnLayeredSurface(target, floatingRef.current)) return;\n setOpen(false);\n };\n\n document.addEventListener('keydown', handleKeyDown);\n document.addEventListener('pointerdown', handlePointerDown, true);\n\n return () => {\n document.removeEventListener('keydown', handleKeyDown);\n document.removeEventListener('pointerdown', handlePointerDown, true);\n };\n }, [open, setOpen, floatingRef, referenceRef]);\n\n const portalHost = getPortalHost();\n if (portalHost === null) return null;\n\n return createPortal(\n <AnimatePresence>\n {open && (\n <motion.div\n key=\"popover-content\"\n id={contentId}\n ref={setFloating}\n variants={floatingVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n role=\"dialog\"\n {...rest}\n className={twMerge(\n 'fixed left-0 top-0 z-[9999] overflow-auto rounded-lg border border-border p-2 text-sm text-foreground glass',\n className,\n )}\n >\n {children}\n </motion.div>\n )}\n </AnimatePresence>,\n portalHost,\n );\n};\n\ninterface IControlledPopoverProps {\n trigger: React.ReactNode;\n children: React.ReactNode;\n triggerTooltip?: string;\n placement?: TPopoverPlacement;\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n contentClassName?: string;\n tooltipClassName?: string;\n}\n\n/**\n * A tooltip on the trigger must sit OUTSIDE the handler: the handler clones its child to\n * inject the click handler and the reference ref, and a tooltip in that position swallows\n * both, so the popover stops opening.\n */\nconst ControlledPopover: React.FC<IControlledPopoverProps> = ({\n trigger,\n children,\n triggerTooltip,\n placement = 'bottom-start',\n open,\n onOpenChange,\n contentClassName,\n tooltipClassName,\n}) => {\n const handle = <ControlledPopoverHandler>{trigger}</ControlledPopoverHandler>;\n\n return (\n <ControlledPopoverRoot open={open} handler={onOpenChange} placement={placement}>\n {triggerTooltip ? (\n <ControlledTooltip content={triggerTooltip} className={classNames(tooltipClassName)}>\n {handle}\n </ControlledTooltip>\n ) : (\n handle\n )}\n\n <ControlledPopoverContent className={contentClassName}>{children}</ControlledPopoverContent>\n </ControlledPopoverRoot>\n );\n};\n\nexport default ControlledPopover;\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ControlledPopover.js","sources":["../../../src/components/UI/ControlledPopover.tsx"],"sourcesContent":["import React, { cloneElement, createContext, isValidElement, useContext, useEffect, useId, useState } from 'react';\nimport classNames from 'classnames';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { createPortal } from 'react-dom';\nimport { twMerge } from 'tailwind-merge';\n\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { composeRefs } from '@/helpers/composeRefs';\nimport { isPressOnLayeredSurface } from '@/helpers/layeredSurfaces';\nimport { getPortalHost } from '@/helpers/portalHost';\nimport { TFloatingPlacement, useAnchoredFloating } from '@/hooks/useAnchoredFloating';\nimport { floatingVariants } from '@/motion';\n\nexport type TPopoverPlacement = TFloatingPlacement;\n\ninterface IPopoverContext {\n open: boolean;\n setOpen: (open: boolean) => void;\n placement: TFloatingPlacement;\n offsetPx: number;\n contentId: string;\n setReference: (node: HTMLElement | null) => void;\n setFloating: (node: HTMLElement | null) => void;\n floatingRef: React.MutableRefObject<HTMLElement | null>;\n referenceRef: React.MutableRefObject<HTMLElement | null>;\n}\n\nconst PopoverContext = createContext<IPopoverContext | null>(null);\n\nconst usePopoverContext = (): IPopoverContext => {\n const context = useContext(PopoverContext);\n if (!context) throw new Error('Popover parts must be rendered inside ControlledPopoverRoot');\n return context;\n};\n\ninterface IControlledPopoverRootProps {\n children: React.ReactNode;\n open?: boolean;\n handler?: (open: boolean) => void;\n placement?: TPopoverPlacement;\n offsetPx?: number;\n /** Sizes the content to the trigger width. Use it when the popover acts as a select dropdown. */\n matchTriggerWidth?: boolean;\n}\n\nexport const ControlledPopoverRoot: React.FC<IControlledPopoverRootProps> = ({\n children,\n open,\n handler,\n placement = 'bottom-start',\n offsetPx = 6,\n matchTriggerWidth = false,\n}) => {\n const contentId = useId();\n const [uncontrolledOpen, setUncontrolledOpen] = useState(false);\n\n const isControlled = open !== undefined;\n const isOpen = isControlled ? Boolean(open) : uncontrolledOpen;\n\n const setOpen = (next: boolean) => {\n if (!isControlled) setUncontrolledOpen(next);\n handler?.(next);\n };\n\n const { setReference, setFloating, floatingRef, referenceRef } = useAnchoredFloating({\n open: isOpen,\n placement,\n offsetPx,\n matchReferenceWidth: matchTriggerWidth,\n });\n\n return (\n <PopoverContext.Provider\n value={{\n open: isOpen,\n setOpen,\n placement,\n offsetPx,\n contentId,\n setReference,\n setFloating,\n floatingRef,\n referenceRef,\n }}\n >\n {children}\n </PopoverContext.Provider>\n );\n};\n\ninterface IHandlerChildProps {\n onClick?: (event: React.MouseEvent<HTMLElement>) => void;\n ref?: React.Ref<HTMLElement>;\n}\n\n/**\n * Repassa ao filho o que ele recebe de fora, além do que injeta.\n *\n * O gatilho é o alvo natural de um `ControlledTooltip`, e o tooltip se prende clonando o filho e\n * injetando `onPointerEnter`. Aceitando só `children`, este componente engolia essa injeção e o\n * tooltip simplesmente nunca abria, mesmo com a ordem que a documentação manda usar\n * (tooltip POR FORA do handler). Os `onClick` são encadeados: o de fora roda antes de abrir.\n */\nexport const ControlledPopoverHandler = React.forwardRef<\n HTMLElement,\n { children: React.ReactNode } & React.HTMLAttributes<HTMLElement>\n>(({ children, onClick: onOuterClick, ...rest }, forwardedRef) => {\n const { open, setOpen, setReference, contentId } = usePopoverContext();\n\n if (!isValidElement(children)) return <>{children}</>;\n\n const child = children as React.ReactElement<IHandlerChildProps>;\n\n return cloneElement(child, {\n ...rest,\n ref: composeRefs<HTMLElement>(setReference, forwardedRef, (child as { ref?: React.Ref<HTMLElement> }).ref),\n 'aria-expanded': open,\n 'aria-controls': open ? contentId : undefined,\n onClick: (event: React.MouseEvent<HTMLElement>) => {\n onOuterClick?.(event);\n child.props.onClick?.(event);\n setOpen(!open);\n },\n } as IHandlerChildProps);\n});\n\nControlledPopoverHandler.displayName = 'ControlledPopoverHandler';\n\ninterface IControlledPopoverContentProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n 'className' | 'onDrag' | 'onDragStart' | 'onDragEnd' | 'onAnimationStart' | 'onAnimationEnd' | 'onAnimationIteration'\n> {\n children: React.ReactNode;\n className?: string;\n}\n\nexport const ControlledPopoverContent: React.FC<IControlledPopoverContentProps> = ({\n children,\n className,\n ...rest\n}) => {\n const { open, setOpen, contentId, setFloating, floatingRef, referenceRef } = usePopoverContext();\n\n useEffect(() => {\n if (!open) return undefined;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') setOpen(false);\n };\n\n const handlePointerDown = (event: PointerEvent) => {\n const target = event.target as Node | null;\n if (!target) return;\n if (floatingRef.current?.contains(target) || referenceRef.current?.contains(target)) return;\n if (isPressOnLayeredSurface(target, floatingRef.current)) return;\n setOpen(false);\n };\n\n document.addEventListener('keydown', handleKeyDown);\n document.addEventListener('pointerdown', handlePointerDown, true);\n\n return () => {\n document.removeEventListener('keydown', handleKeyDown);\n document.removeEventListener('pointerdown', handlePointerDown, true);\n };\n }, [open, setOpen, floatingRef, referenceRef]);\n\n const portalHost = getPortalHost();\n if (portalHost === null) return null;\n\n return createPortal(\n <AnimatePresence>\n {open && (\n <motion.div\n key=\"popover-content\"\n id={contentId}\n ref={setFloating}\n variants={floatingVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n role=\"dialog\"\n {...rest}\n className={twMerge(\n 'fixed left-0 top-0 z-[9999] overflow-auto rounded-lg border border-border p-2 text-sm text-foreground glass',\n className,\n )}\n >\n {children}\n </motion.div>\n )}\n </AnimatePresence>,\n portalHost,\n );\n};\n\ninterface IControlledPopoverProps {\n trigger: React.ReactNode;\n children: React.ReactNode;\n triggerTooltip?: string;\n placement?: TPopoverPlacement;\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n contentClassName?: string;\n tooltipClassName?: string;\n matchTriggerWidth?: boolean;\n}\n\n/**\n * A tooltip on the trigger must sit OUTSIDE the handler: the handler clones its child to\n * inject the click handler and the reference ref, and a tooltip in that position swallows\n * both, so the popover stops opening.\n */\nconst ControlledPopover: React.FC<IControlledPopoverProps> = ({\n trigger,\n children,\n triggerTooltip,\n placement = 'bottom-start',\n open,\n onOpenChange,\n contentClassName,\n tooltipClassName,\n matchTriggerWidth,\n}) => {\n const handle = <ControlledPopoverHandler>{trigger}</ControlledPopoverHandler>;\n\n return (\n <ControlledPopoverRoot\n open={open}\n handler={onOpenChange}\n placement={placement}\n matchTriggerWidth={matchTriggerWidth}\n >\n {triggerTooltip ? (\n <ControlledTooltip content={triggerTooltip} className={classNames(tooltipClassName)}>\n {handle}\n </ControlledTooltip>\n ) : (\n handle\n )}\n\n <ControlledPopoverContent className={contentClassName}>{children}</ControlledPopoverContent>\n </ControlledPopoverRoot>\n );\n};\n\nexport default ControlledPopover;\n"],"names":[],"mappings":"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"}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { IAnnotationDocument, IAnnotationSize, TAnnotationElement } from './interfaces/annotationDocument';
|
|
1
|
+
import { IAnnotationCropRect, IAnnotationDocument, IAnnotationSize, TAnnotationElement } from './interfaces/annotationDocument';
|
|
2
2
|
interface IAnnotationEditorProps {
|
|
3
3
|
baseImage: HTMLImageElement;
|
|
4
4
|
imageSize: IAnnotationSize;
|
|
5
5
|
mimeType: string;
|
|
6
6
|
imageName?: string;
|
|
7
7
|
initialElements: Array<TAnnotationElement>;
|
|
8
|
+
initialCrop: IAnnotationCropRect | null;
|
|
8
9
|
onSave: (file: File, document: IAnnotationDocument) => Promise<void> | void;
|
|
9
10
|
onSaved: () => void;
|
|
10
11
|
}
|
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useRef, useState,
|
|
2
|
+
import { useRef, useState, useMemo, useEffect, useCallback } from "react";
|
|
3
3
|
import { useTranslation } from "react-i18next";
|
|
4
4
|
import { uiLogger } from "../../../helpers/uiLogger.js";
|
|
5
5
|
import { useBreakpoint } from "../../../hooks/useBreakpoint.js";
|
|
6
6
|
import { useNotification } from "../../../hooks/useNotification.js";
|
|
7
7
|
import AnnotationStage from "./canvas/AnnotationStage.js";
|
|
8
8
|
import AnnotationActionBar from "./components/AnnotationActionBar.js";
|
|
9
|
+
import AnnotationCropBar from "./components/AnnotationCropBar.js";
|
|
9
10
|
import AnnotationElementActions from "./components/AnnotationElementActions.js";
|
|
10
11
|
import AnnotationStyleBar from "./components/AnnotationStyleBar.js";
|
|
11
12
|
import AnnotationTextOverlay from "./components/AnnotationTextOverlay.js";
|
|
12
13
|
import AnnotationToolbar from "./components/AnnotationToolbar.js";
|
|
13
14
|
import { fitStageToViewport } from "./helpers/annotationCanvas.js";
|
|
14
|
-
import {
|
|
15
|
+
import { fullImageCrop, aspectRatioFor, fitCropToAspect, isFullImageCrop } from "./helpers/annotationCrop.js";
|
|
16
|
+
import { styleFromElement, applyStyleToElement, translateElement, createImageElement, createTextElement, imageMaxDimension } from "./helpers/annotationElements.js";
|
|
15
17
|
import { exportStageToFile } from "./helpers/annotationExport.js";
|
|
16
18
|
import { useAnnotationDocument } from "./hooks/useAnnotationDocument.js";
|
|
17
19
|
import { useAnnotationShortcuts } from "./hooks/useAnnotationShortcuts.js";
|
|
18
20
|
import { useCoarsePointer } from "./hooks/useCoarsePointer.js";
|
|
19
21
|
import { DEFAULT_ANNOTATION_STYLE } from "./annotationStyle.js";
|
|
22
|
+
const CROP_STAGE_GUTTER_PX = 12;
|
|
20
23
|
const ELEMENT_TOOLS = ["freehand", "line", "arrow", "rectangle", "ellipse", "text", "image"];
|
|
21
24
|
const readFileAsDataUrl = (file) => new Promise((resolve, reject) => {
|
|
22
25
|
const reader = new FileReader();
|
|
@@ -37,6 +40,7 @@ const AnnotationEditor = ({
|
|
|
37
40
|
mimeType,
|
|
38
41
|
imageName,
|
|
39
42
|
initialElements,
|
|
43
|
+
initialCrop,
|
|
40
44
|
onSave,
|
|
41
45
|
onSaved
|
|
42
46
|
}) => {
|
|
@@ -52,8 +56,19 @@ const AnnotationEditor = ({
|
|
|
52
56
|
const [style, setStyle] = useState(DEFAULT_ANNOTATION_STYLE);
|
|
53
57
|
const [editingText, setEditingText] = useState(null);
|
|
54
58
|
const [saving, setSaving] = useState(false);
|
|
55
|
-
const
|
|
59
|
+
const [cropDraft, setCropDraft] = useState(null);
|
|
60
|
+
const [cropAspect, setCropAspect] = useState("free");
|
|
61
|
+
const controller = useAnnotationDocument(imageSize, initialElements, initialCrop);
|
|
56
62
|
const { selectedElement, selectedId, updateSelected, addElement, removeElement, updateElement } = controller;
|
|
63
|
+
const isCropping = tool === "crop";
|
|
64
|
+
const viewRect = useMemo(
|
|
65
|
+
() => isCropping ? fullImageCrop(imageSize) : controller.crop ?? fullImageCrop(imageSize),
|
|
66
|
+
[isCropping, controller.crop, imageSize]
|
|
67
|
+
);
|
|
68
|
+
const viewSize = useMemo(
|
|
69
|
+
() => ({ width: viewRect.width, height: viewRect.height }),
|
|
70
|
+
[viewRect.width, viewRect.height]
|
|
71
|
+
);
|
|
57
72
|
useEffect(() => {
|
|
58
73
|
const node = viewportRef.current;
|
|
59
74
|
if (!node) return;
|
|
@@ -64,7 +79,14 @@ const AnnotationEditor = ({
|
|
|
64
79
|
observer.observe(node);
|
|
65
80
|
return () => observer.disconnect();
|
|
66
81
|
}, []);
|
|
67
|
-
const
|
|
82
|
+
const stageGutter = isCropping ? CROP_STAGE_GUTTER_PX : 0;
|
|
83
|
+
const fit = useMemo(
|
|
84
|
+
() => fitStageToViewport(viewSize, {
|
|
85
|
+
width: viewport.width - stageGutter * 2,
|
|
86
|
+
height: viewport.height - stageGutter * 2
|
|
87
|
+
}),
|
|
88
|
+
[viewSize, viewport, stageGutter]
|
|
89
|
+
);
|
|
68
90
|
useEffect(() => {
|
|
69
91
|
if (!selectedId) return;
|
|
70
92
|
setStyle((current) => {
|
|
@@ -99,23 +121,72 @@ const AnnotationEditor = ({
|
|
|
99
121
|
const handleToolChange = useCallback(
|
|
100
122
|
(next) => {
|
|
101
123
|
handleTextCommit();
|
|
124
|
+
if (next === "crop") {
|
|
125
|
+
controller.select(null);
|
|
126
|
+
setCropDraft(controller.crop ?? fullImageCrop(imageSize));
|
|
127
|
+
setCropAspect("free");
|
|
128
|
+
}
|
|
102
129
|
setTool(next);
|
|
103
130
|
},
|
|
104
|
-
[handleTextCommit]
|
|
131
|
+
[handleTextCommit, controller, imageSize]
|
|
105
132
|
);
|
|
133
|
+
const handleCropAspectChange = useCallback(
|
|
134
|
+
(aspect) => {
|
|
135
|
+
setCropAspect(aspect);
|
|
136
|
+
const ratio = aspectRatioFor(aspect, imageSize);
|
|
137
|
+
if (ratio === null) return;
|
|
138
|
+
setCropDraft((current) => fitCropToAspect(current ?? fullImageCrop(imageSize), ratio, imageSize));
|
|
139
|
+
},
|
|
140
|
+
[imageSize]
|
|
141
|
+
);
|
|
142
|
+
const handleCropReset = useCallback(() => {
|
|
143
|
+
setCropAspect("free");
|
|
144
|
+
setCropDraft(fullImageCrop(imageSize));
|
|
145
|
+
}, [imageSize]);
|
|
146
|
+
const handleCropCancel = useCallback(() => {
|
|
147
|
+
setCropDraft(null);
|
|
148
|
+
setTool("select");
|
|
149
|
+
}, []);
|
|
150
|
+
const handleCropApply = useCallback(() => {
|
|
151
|
+
const draft = cropDraft;
|
|
152
|
+
setCropDraft(null);
|
|
153
|
+
setTool("select");
|
|
154
|
+
if (!draft) return;
|
|
155
|
+
const next = isFullImageCrop(draft, imageSize) ? null : draft;
|
|
156
|
+
const current = controller.crop;
|
|
157
|
+
const unchanged = next === current || next !== null && current !== null && next.x === current.x && next.y === current.y && next.width === current.width && next.height === current.height;
|
|
158
|
+
if (!unchanged) controller.setCrop(next);
|
|
159
|
+
}, [cropDraft, imageSize, controller]);
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
if (!isCropping) return;
|
|
162
|
+
const handleKeyDown = (event) => {
|
|
163
|
+
if (event.key === "Escape") {
|
|
164
|
+
event.preventDefault();
|
|
165
|
+
event.stopPropagation();
|
|
166
|
+
handleCropCancel();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (event.key === "Enter") {
|
|
170
|
+
event.preventDefault();
|
|
171
|
+
handleCropApply();
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
window.addEventListener("keydown", handleKeyDown, true);
|
|
175
|
+
return () => window.removeEventListener("keydown", handleKeyDown, true);
|
|
176
|
+
}, [isCropping, handleCropCancel, handleCropApply]);
|
|
106
177
|
const handleInsertImage = useCallback(
|
|
107
178
|
async (file) => {
|
|
108
179
|
try {
|
|
109
180
|
const source = await readFileAsDataUrl(file);
|
|
110
181
|
const natural = await loadNaturalSize(source);
|
|
111
|
-
addElement(createImageElement(source, natural,
|
|
182
|
+
addElement(translateElement(createImageElement(source, natural, viewSize), viewRect.x, viewRect.y));
|
|
112
183
|
setTool("select");
|
|
113
184
|
} catch (error) {
|
|
114
185
|
uiLogger.error("Failed to insert an image into the annotation", error);
|
|
115
186
|
notify({ message: t("imageAnnotator.imageInsertFailed"), type: "error" });
|
|
116
187
|
}
|
|
117
188
|
},
|
|
118
|
-
[addElement,
|
|
189
|
+
[addElement, viewSize, viewRect.x, viewRect.y, notify, t]
|
|
119
190
|
);
|
|
120
191
|
const handleRequestTextEdit = useCallback(
|
|
121
192
|
(id) => {
|
|
@@ -144,14 +215,14 @@ const AnnotationEditor = ({
|
|
|
144
215
|
}, [editingText, controller.elements, removeElement]);
|
|
145
216
|
const handleSave = async () => {
|
|
146
217
|
const stage = stageRef.current;
|
|
147
|
-
if (!stage || saving) return;
|
|
218
|
+
if (!stage || saving || isCropping) return;
|
|
148
219
|
setSaving(true);
|
|
149
220
|
handleTextCommit();
|
|
150
221
|
controller.select(null);
|
|
151
222
|
try {
|
|
152
223
|
await nextFrame();
|
|
153
224
|
await nextFrame();
|
|
154
|
-
const file = await exportStageToFile({ stage, imageSize, mimeType, imageName });
|
|
225
|
+
const file = await exportStageToFile({ stage, imageSize: viewSize, mimeType, imageName });
|
|
155
226
|
await onSave(file, controller.document);
|
|
156
227
|
onSaved();
|
|
157
228
|
} catch (error) {
|
|
@@ -162,7 +233,7 @@ const AnnotationEditor = ({
|
|
|
162
233
|
}
|
|
163
234
|
};
|
|
164
235
|
useAnnotationShortcuts({
|
|
165
|
-
active: !editingText,
|
|
236
|
+
active: !editingText && !isCropping,
|
|
166
237
|
controller,
|
|
167
238
|
imageMaxDimension: imageMaxDimension(imageSize),
|
|
168
239
|
onToolChange: handleToolChange
|
|
@@ -189,10 +260,10 @@ const AnnotationEditor = ({
|
|
|
189
260
|
{
|
|
190
261
|
touch,
|
|
191
262
|
saving,
|
|
192
|
-
canUndo: controller.canUndo,
|
|
193
|
-
canRedo: controller.canRedo,
|
|
194
|
-
canClear:
|
|
195
|
-
canSave: !controller.isEmpty,
|
|
263
|
+
canUndo: controller.canUndo && !isCropping,
|
|
264
|
+
canRedo: controller.canRedo && !isCropping,
|
|
265
|
+
canClear: controller.elements.length > 0 && !isCropping,
|
|
266
|
+
canSave: !controller.isEmpty && !isCropping,
|
|
196
267
|
elementActions: touch ? elementActions : void 0,
|
|
197
268
|
onUndo: controller.undo,
|
|
198
269
|
onRedo: controller.redo,
|
|
@@ -211,7 +282,18 @@ const AnnotationEditor = ({
|
|
|
211
282
|
onInsertImage: handleInsertImage
|
|
212
283
|
}
|
|
213
284
|
),
|
|
214
|
-
/* @__PURE__ */ jsx(
|
|
285
|
+
isCropping ? /* @__PURE__ */ jsx(
|
|
286
|
+
AnnotationCropBar,
|
|
287
|
+
{
|
|
288
|
+
aspect: cropAspect,
|
|
289
|
+
touch,
|
|
290
|
+
canReset: !isFullImageCrop(cropDraft, imageSize),
|
|
291
|
+
onAspectChange: handleCropAspectChange,
|
|
292
|
+
onReset: handleCropReset,
|
|
293
|
+
onCancel: handleCropCancel,
|
|
294
|
+
onApply: handleCropApply
|
|
295
|
+
}
|
|
296
|
+
) : /* @__PURE__ */ jsx(
|
|
215
297
|
AnnotationStyleBar,
|
|
216
298
|
{
|
|
217
299
|
style,
|
|
@@ -228,34 +310,47 @@ const AnnotationEditor = ({
|
|
|
228
310
|
{
|
|
229
311
|
ref: viewportRef,
|
|
230
312
|
className: "relative flex min-h-0 flex-1 items-center justify-center overflow-hidden rounded-xl bg-ink/95 p-3",
|
|
231
|
-
children: fit.width > 1 && /* @__PURE__ */ jsxs(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
313
|
+
children: fit.width > 1 && /* @__PURE__ */ jsxs(
|
|
314
|
+
"div",
|
|
315
|
+
{
|
|
316
|
+
className: "relative",
|
|
317
|
+
style: { width: fit.width + stageGutter * 2, height: fit.height + stageGutter * 2 },
|
|
318
|
+
children: [
|
|
319
|
+
/* @__PURE__ */ jsx(
|
|
320
|
+
AnnotationStage,
|
|
321
|
+
{
|
|
322
|
+
baseImage,
|
|
323
|
+
imageSize,
|
|
324
|
+
fit,
|
|
325
|
+
viewRect,
|
|
326
|
+
cropDraft,
|
|
327
|
+
cropAspectRatio: aspectRatioFor(cropAspect, imageSize),
|
|
328
|
+
gutter: stageGutter,
|
|
329
|
+
onCropDraftChange: setCropDraft,
|
|
330
|
+
tool,
|
|
331
|
+
style,
|
|
332
|
+
controller,
|
|
333
|
+
editingTextId: editingText?.id ?? null,
|
|
334
|
+
stageRef,
|
|
335
|
+
onRequestTextEdit: handleRequestTextEdit,
|
|
336
|
+
onTextPlaced: handleTextPlaced
|
|
337
|
+
}
|
|
338
|
+
),
|
|
339
|
+
editingText && editingElement && /* @__PURE__ */ jsx(
|
|
340
|
+
AnnotationTextOverlay,
|
|
341
|
+
{
|
|
342
|
+
element: editingElement,
|
|
343
|
+
viewScale: fit.scale,
|
|
344
|
+
viewOrigin: viewRect,
|
|
345
|
+
value: editingText.value,
|
|
346
|
+
onChange: (value) => setEditingText((current) => current ? { ...current, value } : current),
|
|
347
|
+
onCommit: handleTextCommit,
|
|
348
|
+
onCancel: handleTextCancel
|
|
349
|
+
}
|
|
350
|
+
)
|
|
351
|
+
]
|
|
352
|
+
}
|
|
353
|
+
)
|
|
259
354
|
}
|
|
260
355
|
),
|
|
261
356
|
touch && /* @__PURE__ */ jsx("div", { className: "shrink-0", children: actionBar })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnnotationEditor.js","sources":["../../../../src/components/UI/ImageAnnotator/AnnotationEditor.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport Konva from 'konva';\nimport { useTranslation } from 'react-i18next';\n\nimport { uiLogger } from '@/helpers/uiLogger';\nimport { useBreakpoint } from '@/hooks/useBreakpoint';\nimport { useNotification } from '@/hooks/useNotification';\n\nimport AnnotationStage from './canvas/AnnotationStage';\nimport AnnotationActionBar from './components/AnnotationActionBar';\nimport AnnotationElementActions from './components/AnnotationElementActions';\nimport AnnotationStyleBar from './components/AnnotationStyleBar';\nimport AnnotationTextOverlay from './components/AnnotationTextOverlay';\nimport AnnotationToolbar from './components/AnnotationToolbar';\nimport { fitStageToViewport } from './helpers/annotationCanvas';\nimport {\n applyStyleToElement,\n createImageElement,\n createTextElement,\n imageMaxDimension,\n styleFromElement,\n} from './helpers/annotationElements';\nimport { exportStageToFile } from './helpers/annotationExport';\nimport { useAnnotationDocument } from './hooks/useAnnotationDocument';\nimport { useAnnotationShortcuts } from './hooks/useAnnotationShortcuts';\nimport { useCoarsePointer } from './hooks/useCoarsePointer';\nimport {\n IAnnotationDocument,\n IAnnotationImageElement,\n IAnnotationPoint,\n IAnnotationSize,\n TAnnotationElement,\n TAnnotationElementType,\n TAnnotationTool,\n} from './interfaces/annotationDocument';\nimport { DEFAULT_ANNOTATION_STYLE, IAnnotationStyle } from './annotationStyle';\n\nconst ELEMENT_TOOLS: Array<TAnnotationTool> = ['freehand', 'line', 'arrow', 'rectangle', 'ellipse', 'text', 'image'];\n\ninterface IAnnotationEditorProps {\n baseImage: HTMLImageElement;\n imageSize: IAnnotationSize;\n mimeType: string;\n imageName?: string;\n initialElements: Array<TAnnotationElement>;\n onSave: (file: File, document: IAnnotationDocument) => Promise<void> | void;\n onSaved: () => void;\n}\n\nconst readFileAsDataUrl = (file: File): Promise<string> =>\n new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(String(reader.result));\n reader.onerror = () => reject(new Error('Could not read the selected image'));\n reader.readAsDataURL(file);\n });\n\nconst loadNaturalSize = (source: string): Promise<IAnnotationSize> =>\n new Promise((resolve, reject) => {\n const element = new Image();\n element.onload = () => resolve({ width: element.naturalWidth, height: element.naturalHeight });\n element.onerror = () => reject(new Error('Could not decode the selected image'));\n element.src = source;\n });\n\nconst nextFrame = (): Promise<void> => new Promise((resolve) => window.requestAnimationFrame(() => resolve()));\n\nconst AnnotationEditor: React.FC<IAnnotationEditorProps> = ({\n baseImage,\n imageSize,\n mimeType,\n imageName,\n initialElements,\n onSave,\n onSaved,\n}) => {\n const { t } = useTranslation('ui');\n const { notify } = useNotification();\n const { isTabletDown } = useBreakpoint();\n const isCoarsePointer = useCoarsePointer();\n const touch = isCoarsePointer || isTabletDown;\n\n const stageRef = useRef<Konva.Stage | null>(null);\n const viewportRef = useRef<HTMLDivElement | null>(null);\n\n const [viewport, setViewport] = useState<IAnnotationSize>({ width: 0, height: 0 });\n const [tool, setTool] = useState<TAnnotationTool>('select');\n const [style, setStyle] = useState<IAnnotationStyle>(DEFAULT_ANNOTATION_STYLE);\n const [editingText, setEditingText] = useState<{ id: string; value: string } | null>(null);\n const [saving, setSaving] = useState(false);\n\n const controller = useAnnotationDocument(imageSize, initialElements);\n const { selectedElement, selectedId, updateSelected, addElement, removeElement, updateElement } = controller;\n\n useEffect(() => {\n const node = viewportRef.current;\n if (!node) return;\n\n const observer = new ResizeObserver((entries) => {\n const rect = entries[0]?.contentRect;\n if (rect) setViewport({ width: rect.width, height: rect.height });\n });\n observer.observe(node);\n\n return () => observer.disconnect();\n }, []);\n\n const fit = useMemo(() => fitStageToViewport(imageSize, viewport), [imageSize, viewport]);\n\n /** Selecting mirrors the element into the toolbar, so the controls never describe a stale style. */\n useEffect(() => {\n if (!selectedId) return;\n setStyle((current) => {\n const element = controller.elements.find((item) => item.id === selectedId);\n return element ? styleFromElement(element, current, imageSize) : current;\n });\n }, [selectedId]);\n\n const handleStyleChange = useCallback(\n (patch: Partial<IAnnotationStyle>) => {\n const next = { ...style, ...patch };\n setStyle(next);\n if (selectedId) updateSelected((element) => applyStyleToElement(element, next, imageSize));\n },\n [style, selectedId, updateSelected, imageSize],\n );\n\n const handleImageStyleChange = useCallback(\n (patch: Partial<IAnnotationImageElement>) => {\n updateSelected((element) => (element.type === 'image' ? { ...element, ...patch } : element));\n },\n [updateSelected],\n );\n\n const handleTextCommit = useCallback(() => {\n if (!editingText) return;\n const trimmed = editingText.value.trim();\n setEditingText(null);\n\n if (!trimmed) {\n removeElement(editingText.id);\n return;\n }\n updateElement(editingText.id, (element) => (element.type === 'text' ? { ...element, text: trimmed } : element));\n }, [editingText, removeElement, updateElement]);\n\n const handleToolChange = useCallback(\n (next: TAnnotationTool) => {\n handleTextCommit();\n setTool(next);\n },\n [handleTextCommit],\n );\n\n const handleInsertImage = useCallback(\n async (file: File) => {\n try {\n const source = await readFileAsDataUrl(file);\n const natural = await loadNaturalSize(source);\n addElement(createImageElement(source, natural, imageSize));\n setTool('select');\n } catch (error) {\n uiLogger.error('Failed to insert an image into the annotation', error);\n notify({ message: t('imageAnnotator.imageInsertFailed'), type: 'error' });\n }\n },\n [addElement, imageSize, notify, t],\n );\n\n const handleRequestTextEdit = useCallback(\n (id: string) => {\n const element = controller.elements.find((item) => item.id === id);\n if (element?.type !== 'text') return;\n setTool('select');\n controller.select(id);\n setEditingText({ id, value: element.text });\n },\n [controller],\n );\n\n /**\n * The element is created here, not in the stage: looking it up right after `addElement` would\n * read the previous render's list and silently skip opening the editor.\n */\n const handleTextPlaced = useCallback(\n (point: IAnnotationPoint) => {\n const element = createTextElement(point, style, imageSize);\n addElement(element);\n setTool('select');\n setEditingText({ id: element.id, value: '' });\n },\n [style, imageSize, addElement],\n );\n\n const handleTextCancel = useCallback(() => {\n if (!editingText) return;\n const element = controller.elements.find((item) => item.id === editingText.id);\n setEditingText(null);\n if (element?.type === 'text' && !element.text.trim()) removeElement(editingText.id);\n }, [editingText, controller.elements, removeElement]);\n\n const handleSave = async () => {\n const stage = stageRef.current;\n if (!stage || saving) return;\n\n setSaving(true);\n handleTextCommit();\n controller.select(null);\n\n try {\n await nextFrame();\n await nextFrame();\n const file = await exportStageToFile({ stage, imageSize, mimeType, imageName });\n await onSave(file, controller.document);\n onSaved();\n } catch (error) {\n uiLogger.error('Failed to save annotated image', error);\n notify({ message: t('imageAnnotator.sendFailed'), type: 'error' });\n } finally {\n setSaving(false);\n }\n };\n\n useAnnotationShortcuts({\n active: !editingText,\n controller,\n imageMaxDimension: imageMaxDimension(imageSize),\n onToolChange: handleToolChange,\n });\n\n const activeType = useMemo<TAnnotationElementType | null>(() => {\n if (selectedElement) return selectedElement.type;\n return ELEMENT_TOOLS.includes(tool) ? (tool as TAnnotationElementType) : null;\n }, [selectedElement, tool]);\n\n const editingElement = useMemo(() => {\n const element = controller.elements.find((item) => item.id === editingText?.id);\n return element?.type === 'text' ? element : null;\n }, [controller.elements, editingText?.id]);\n\n const elementActions = selectedElement ? (\n <AnnotationElementActions\n touch={touch}\n onReorder={controller.reorderSelected}\n onDuplicate={controller.duplicateSelected}\n onDelete={() => selectedId && removeElement(selectedId)}\n />\n ) : null;\n\n const actionBar = (\n <AnnotationActionBar\n touch={touch}\n saving={saving}\n canUndo={controller.canUndo}\n canRedo={controller.canRedo}\n canClear={!controller.isEmpty}\n canSave={!controller.isEmpty}\n elementActions={touch ? elementActions : undefined}\n onUndo={controller.undo}\n onRedo={controller.redo}\n onClear={controller.clear}\n onSave={handleSave}\n />\n );\n\n return (\n <div className=\"flex h-full flex-col gap-y-2\">\n <AnnotationToolbar\n tool={tool}\n touch={touch}\n actions={touch ? undefined : actionBar}\n onToolChange={handleToolChange}\n onInsertImage={handleInsertImage}\n />\n\n <AnnotationStyleBar\n style={style}\n activeType={activeType}\n selectedElement={selectedElement}\n touch={touch}\n elementActions={touch ? undefined : elementActions}\n onStyleChange={handleStyleChange}\n onImageStyleChange={handleImageStyleChange}\n />\n\n <div\n ref={viewportRef}\n className=\"relative flex min-h-0 flex-1 items-center justify-center overflow-hidden rounded-xl bg-ink/95 p-3\"\n >\n {fit.width > 1 && (\n <div className=\"relative\" style={{ width: fit.width, height: fit.height }}>\n <AnnotationStage\n baseImage={baseImage}\n imageSize={imageSize}\n fit={fit}\n tool={tool}\n style={style}\n controller={controller}\n editingTextId={editingText?.id ?? null}\n stageRef={stageRef}\n onRequestTextEdit={handleRequestTextEdit}\n onTextPlaced={handleTextPlaced}\n />\n\n {editingText && editingElement && (\n <AnnotationTextOverlay\n element={editingElement}\n viewScale={fit.scale}\n value={editingText.value}\n onChange={(value) => setEditingText((current) => (current ? { ...current, value } : current))}\n onCommit={handleTextCommit}\n onCancel={handleTextCancel}\n />\n )}\n </div>\n )}\n </div>\n\n {touch && <div className=\"shrink-0\">{actionBar}</div>}\n </div>\n );\n};\n\nexport default AnnotationEditor;\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AnnotationEditor.js","sources":["../../../../src/components/UI/ImageAnnotator/AnnotationEditor.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport Konva from 'konva';\nimport { useTranslation } from 'react-i18next';\n\nimport { uiLogger } from '@/helpers/uiLogger';\nimport { useBreakpoint } from '@/hooks/useBreakpoint';\nimport { useNotification } from '@/hooks/useNotification';\n\nimport AnnotationStage from './canvas/AnnotationStage';\nimport AnnotationActionBar from './components/AnnotationActionBar';\nimport AnnotationCropBar from './components/AnnotationCropBar';\nimport AnnotationElementActions from './components/AnnotationElementActions';\nimport AnnotationStyleBar from './components/AnnotationStyleBar';\nimport AnnotationTextOverlay from './components/AnnotationTextOverlay';\nimport AnnotationToolbar from './components/AnnotationToolbar';\nimport { fitStageToViewport } from './helpers/annotationCanvas';\nimport {\n aspectRatioFor,\n fitCropToAspect,\n fullImageCrop,\n isFullImageCrop,\n TAnnotationCropAspect,\n} from './helpers/annotationCrop';\nimport {\n applyStyleToElement,\n createImageElement,\n createTextElement,\n imageMaxDimension,\n styleFromElement,\n translateElement,\n} from './helpers/annotationElements';\nimport { exportStageToFile } from './helpers/annotationExport';\nimport { useAnnotationDocument } from './hooks/useAnnotationDocument';\nimport { useAnnotationShortcuts } from './hooks/useAnnotationShortcuts';\nimport { useCoarsePointer } from './hooks/useCoarsePointer';\nimport {\n IAnnotationCropRect,\n IAnnotationDocument,\n IAnnotationImageElement,\n IAnnotationPoint,\n IAnnotationSize,\n TAnnotationElement,\n TAnnotationElementType,\n TAnnotationTool,\n} from './interfaces/annotationDocument';\nimport { DEFAULT_ANNOTATION_STYLE, IAnnotationStyle } from './annotationStyle';\n\nconst CROP_STAGE_GUTTER_PX = 12;\n\nconst ELEMENT_TOOLS: Array<TAnnotationTool> = ['freehand', 'line', 'arrow', 'rectangle', 'ellipse', 'text', 'image'];\n\ninterface IAnnotationEditorProps {\n baseImage: HTMLImageElement;\n imageSize: IAnnotationSize;\n mimeType: string;\n imageName?: string;\n initialElements: Array<TAnnotationElement>;\n initialCrop: IAnnotationCropRect | null;\n onSave: (file: File, document: IAnnotationDocument) => Promise<void> | void;\n onSaved: () => void;\n}\n\nconst readFileAsDataUrl = (file: File): Promise<string> =>\n new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(String(reader.result));\n reader.onerror = () => reject(new Error('Could not read the selected image'));\n reader.readAsDataURL(file);\n });\n\nconst loadNaturalSize = (source: string): Promise<IAnnotationSize> =>\n new Promise((resolve, reject) => {\n const element = new Image();\n element.onload = () => resolve({ width: element.naturalWidth, height: element.naturalHeight });\n element.onerror = () => reject(new Error('Could not decode the selected image'));\n element.src = source;\n });\n\nconst nextFrame = (): Promise<void> => new Promise((resolve) => window.requestAnimationFrame(() => resolve()));\n\nconst AnnotationEditor: React.FC<IAnnotationEditorProps> = ({\n baseImage,\n imageSize,\n mimeType,\n imageName,\n initialElements,\n initialCrop,\n onSave,\n onSaved,\n}) => {\n const { t } = useTranslation('ui');\n const { notify } = useNotification();\n const { isTabletDown } = useBreakpoint();\n const isCoarsePointer = useCoarsePointer();\n const touch = isCoarsePointer || isTabletDown;\n\n const stageRef = useRef<Konva.Stage | null>(null);\n const viewportRef = useRef<HTMLDivElement | null>(null);\n\n const [viewport, setViewport] = useState<IAnnotationSize>({ width: 0, height: 0 });\n const [tool, setTool] = useState<TAnnotationTool>('select');\n const [style, setStyle] = useState<IAnnotationStyle>(DEFAULT_ANNOTATION_STYLE);\n const [editingText, setEditingText] = useState<{ id: string; value: string } | null>(null);\n const [saving, setSaving] = useState(false);\n const [cropDraft, setCropDraft] = useState<IAnnotationCropRect | null>(null);\n const [cropAspect, setCropAspect] = useState<TAnnotationCropAspect>('free');\n\n const controller = useAnnotationDocument(imageSize, initialElements, initialCrop);\n const { selectedElement, selectedId, updateSelected, addElement, removeElement, updateElement } = controller;\n\n const isCropping = tool === 'crop';\n const viewRect = useMemo<IAnnotationCropRect>(\n () => (isCropping ? fullImageCrop(imageSize) : (controller.crop ?? fullImageCrop(imageSize))),\n [isCropping, controller.crop, imageSize],\n );\n const viewSize = useMemo<IAnnotationSize>(\n () => ({ width: viewRect.width, height: viewRect.height }),\n [viewRect.width, viewRect.height],\n );\n\n useEffect(() => {\n const node = viewportRef.current;\n if (!node) return;\n\n const observer = new ResizeObserver((entries) => {\n const rect = entries[0]?.contentRect;\n if (rect) setViewport({ width: rect.width, height: rect.height });\n });\n observer.observe(node);\n\n return () => observer.disconnect();\n }, []);\n\n const stageGutter = isCropping ? CROP_STAGE_GUTTER_PX : 0;\n const fit = useMemo(\n () =>\n fitStageToViewport(viewSize, {\n width: viewport.width - stageGutter * 2,\n height: viewport.height - stageGutter * 2,\n }),\n [viewSize, viewport, stageGutter],\n );\n\n /** Selecting mirrors the element into the toolbar, so the controls never describe a stale style. */\n useEffect(() => {\n if (!selectedId) return;\n setStyle((current) => {\n const element = controller.elements.find((item) => item.id === selectedId);\n return element ? styleFromElement(element, current, imageSize) : current;\n });\n }, [selectedId]);\n\n const handleStyleChange = useCallback(\n (patch: Partial<IAnnotationStyle>) => {\n const next = { ...style, ...patch };\n setStyle(next);\n if (selectedId) updateSelected((element) => applyStyleToElement(element, next, imageSize));\n },\n [style, selectedId, updateSelected, imageSize],\n );\n\n const handleImageStyleChange = useCallback(\n (patch: Partial<IAnnotationImageElement>) => {\n updateSelected((element) => (element.type === 'image' ? { ...element, ...patch } : element));\n },\n [updateSelected],\n );\n\n const handleTextCommit = useCallback(() => {\n if (!editingText) return;\n const trimmed = editingText.value.trim();\n setEditingText(null);\n\n if (!trimmed) {\n removeElement(editingText.id);\n return;\n }\n updateElement(editingText.id, (element) => (element.type === 'text' ? { ...element, text: trimmed } : element));\n }, [editingText, removeElement, updateElement]);\n\n const handleToolChange = useCallback(\n (next: TAnnotationTool) => {\n handleTextCommit();\n if (next === 'crop') {\n controller.select(null);\n setCropDraft(controller.crop ?? fullImageCrop(imageSize));\n setCropAspect('free');\n }\n setTool(next);\n },\n [handleTextCommit, controller, imageSize],\n );\n\n const handleCropAspectChange = useCallback(\n (aspect: TAnnotationCropAspect) => {\n setCropAspect(aspect);\n const ratio = aspectRatioFor(aspect, imageSize);\n if (ratio === null) return;\n setCropDraft((current) => fitCropToAspect(current ?? fullImageCrop(imageSize), ratio, imageSize));\n },\n [imageSize],\n );\n\n const handleCropReset = useCallback(() => {\n setCropAspect('free');\n setCropDraft(fullImageCrop(imageSize));\n }, [imageSize]);\n\n const handleCropCancel = useCallback(() => {\n setCropDraft(null);\n setTool('select');\n }, []);\n\n const handleCropApply = useCallback(() => {\n const draft = cropDraft;\n setCropDraft(null);\n setTool('select');\n if (!draft) return;\n const next = isFullImageCrop(draft, imageSize) ? null : draft;\n const current = controller.crop;\n const unchanged =\n next === current ||\n (next !== null &&\n current !== null &&\n next.x === current.x &&\n next.y === current.y &&\n next.width === current.width &&\n next.height === current.height);\n if (!unchanged) controller.setCrop(next);\n }, [cropDraft, imageSize, controller]);\n\n useEffect(() => {\n if (!isCropping) return;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n event.preventDefault();\n event.stopPropagation();\n handleCropCancel();\n return;\n }\n if (event.key === 'Enter') {\n event.preventDefault();\n handleCropApply();\n }\n };\n\n window.addEventListener('keydown', handleKeyDown, true);\n return () => window.removeEventListener('keydown', handleKeyDown, true);\n }, [isCropping, handleCropCancel, handleCropApply]);\n\n const handleInsertImage = useCallback(\n async (file: File) => {\n try {\n const source = await readFileAsDataUrl(file);\n const natural = await loadNaturalSize(source);\n addElement(translateElement(createImageElement(source, natural, viewSize), viewRect.x, viewRect.y));\n setTool('select');\n } catch (error) {\n uiLogger.error('Failed to insert an image into the annotation', error);\n notify({ message: t('imageAnnotator.imageInsertFailed'), type: 'error' });\n }\n },\n [addElement, viewSize, viewRect.x, viewRect.y, notify, t],\n );\n\n const handleRequestTextEdit = useCallback(\n (id: string) => {\n const element = controller.elements.find((item) => item.id === id);\n if (element?.type !== 'text') return;\n setTool('select');\n controller.select(id);\n setEditingText({ id, value: element.text });\n },\n [controller],\n );\n\n /**\n * The element is created here, not in the stage: looking it up right after `addElement` would\n * read the previous render's list and silently skip opening the editor.\n */\n const handleTextPlaced = useCallback(\n (point: IAnnotationPoint) => {\n const element = createTextElement(point, style, imageSize);\n addElement(element);\n setTool('select');\n setEditingText({ id: element.id, value: '' });\n },\n [style, imageSize, addElement],\n );\n\n const handleTextCancel = useCallback(() => {\n if (!editingText) return;\n const element = controller.elements.find((item) => item.id === editingText.id);\n setEditingText(null);\n if (element?.type === 'text' && !element.text.trim()) removeElement(editingText.id);\n }, [editingText, controller.elements, removeElement]);\n\n const handleSave = async () => {\n const stage = stageRef.current;\n if (!stage || saving || isCropping) return;\n\n setSaving(true);\n handleTextCommit();\n controller.select(null);\n\n try {\n await nextFrame();\n await nextFrame();\n const file = await exportStageToFile({ stage, imageSize: viewSize, mimeType, imageName });\n await onSave(file, controller.document);\n onSaved();\n } catch (error) {\n uiLogger.error('Failed to save annotated image', error);\n notify({ message: t('imageAnnotator.sendFailed'), type: 'error' });\n } finally {\n setSaving(false);\n }\n };\n\n useAnnotationShortcuts({\n active: !editingText && !isCropping,\n controller,\n imageMaxDimension: imageMaxDimension(imageSize),\n onToolChange: handleToolChange,\n });\n\n const activeType = useMemo<TAnnotationElementType | null>(() => {\n if (selectedElement) return selectedElement.type;\n return ELEMENT_TOOLS.includes(tool) ? (tool as TAnnotationElementType) : null;\n }, [selectedElement, tool]);\n\n const editingElement = useMemo(() => {\n const element = controller.elements.find((item) => item.id === editingText?.id);\n return element?.type === 'text' ? element : null;\n }, [controller.elements, editingText?.id]);\n\n const elementActions = selectedElement ? (\n <AnnotationElementActions\n touch={touch}\n onReorder={controller.reorderSelected}\n onDuplicate={controller.duplicateSelected}\n onDelete={() => selectedId && removeElement(selectedId)}\n />\n ) : null;\n\n const actionBar = (\n <AnnotationActionBar\n touch={touch}\n saving={saving}\n canUndo={controller.canUndo && !isCropping}\n canRedo={controller.canRedo && !isCropping}\n canClear={controller.elements.length > 0 && !isCropping}\n canSave={!controller.isEmpty && !isCropping}\n elementActions={touch ? elementActions : undefined}\n onUndo={controller.undo}\n onRedo={controller.redo}\n onClear={controller.clear}\n onSave={handleSave}\n />\n );\n\n return (\n <div className=\"flex h-full flex-col gap-y-2\">\n <AnnotationToolbar\n tool={tool}\n touch={touch}\n actions={touch ? undefined : actionBar}\n onToolChange={handleToolChange}\n onInsertImage={handleInsertImage}\n />\n\n {isCropping ? (\n <AnnotationCropBar\n aspect={cropAspect}\n touch={touch}\n canReset={!isFullImageCrop(cropDraft, imageSize)}\n onAspectChange={handleCropAspectChange}\n onReset={handleCropReset}\n onCancel={handleCropCancel}\n onApply={handleCropApply}\n />\n ) : (\n <AnnotationStyleBar\n style={style}\n activeType={activeType}\n selectedElement={selectedElement}\n touch={touch}\n elementActions={touch ? undefined : elementActions}\n onStyleChange={handleStyleChange}\n onImageStyleChange={handleImageStyleChange}\n />\n )}\n\n <div\n ref={viewportRef}\n className=\"relative flex min-h-0 flex-1 items-center justify-center overflow-hidden rounded-xl bg-ink/95 p-3\"\n >\n {fit.width > 1 && (\n <div\n className=\"relative\"\n style={{ width: fit.width + stageGutter * 2, height: fit.height + stageGutter * 2 }}\n >\n <AnnotationStage\n baseImage={baseImage}\n imageSize={imageSize}\n fit={fit}\n viewRect={viewRect}\n cropDraft={cropDraft}\n cropAspectRatio={aspectRatioFor(cropAspect, imageSize)}\n gutter={stageGutter}\n onCropDraftChange={setCropDraft}\n tool={tool}\n style={style}\n controller={controller}\n editingTextId={editingText?.id ?? null}\n stageRef={stageRef}\n onRequestTextEdit={handleRequestTextEdit}\n onTextPlaced={handleTextPlaced}\n />\n\n {editingText && editingElement && (\n <AnnotationTextOverlay\n element={editingElement}\n viewScale={fit.scale}\n viewOrigin={viewRect}\n value={editingText.value}\n onChange={(value) => setEditingText((current) => (current ? { ...current, value } : current))}\n onCommit={handleTextCommit}\n onCancel={handleTextCancel}\n />\n )}\n </div>\n )}\n </div>\n\n {touch && <div className=\"shrink-0\">{actionBar}</div>}\n </div>\n );\n};\n\nexport default AnnotationEditor;\n"],"names":[],"mappings":"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"}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { IAnnotationDocument, TAnnotationElement } from './interfaces/annotationDocument';
|
|
1
|
+
import { IAnnotationCropRect, IAnnotationDocument, TAnnotationElement } from './interfaces/annotationDocument';
|
|
2
2
|
interface IImageAnnotationDialogProps {
|
|
3
3
|
open: boolean;
|
|
4
4
|
imageUrl: string;
|
|
5
5
|
imageName?: string;
|
|
6
6
|
/** Previously saved annotations, when the host persists the document alongside the file. */
|
|
7
7
|
initialElements?: Array<TAnnotationElement>;
|
|
8
|
+
/** Previously saved crop (`document.crop`), restored so a reopened image keeps its framing. */
|
|
9
|
+
initialCrop?: IAnnotationCropRect | null;
|
|
8
10
|
onClose: () => void;
|
|
9
11
|
/** Receives the flattened image plus the editable document; resolving closes the dialog. */
|
|
10
12
|
onSave: (file: File, document: IAnnotationDocument) => Promise<void> | void;
|
|
@@ -5,6 +5,7 @@ import ControlledDialog from "../../Form/ControlledDialog.js";
|
|
|
5
5
|
import LoadingSpinner from "../LoadingSpinner.js";
|
|
6
6
|
import NoDataFound from "../NoDataFound/index.js";
|
|
7
7
|
import { useBreakpoint } from "../../../hooks/useBreakpoint.js";
|
|
8
|
+
import { sanitizeInitialCrop } from "./helpers/annotationCrop.js";
|
|
8
9
|
import { useAnnotationBaseImage } from "./hooks/useAnnotationBaseImage.js";
|
|
9
10
|
const AnnotationEditor = lazy(() => import("./AnnotationEditor.js"));
|
|
10
11
|
const ImageAnnotationDialog = ({
|
|
@@ -12,6 +13,7 @@ const ImageAnnotationDialog = ({
|
|
|
12
13
|
imageUrl,
|
|
13
14
|
imageName,
|
|
14
15
|
initialElements,
|
|
16
|
+
initialCrop,
|
|
15
17
|
onClose,
|
|
16
18
|
onSave
|
|
17
19
|
}) => {
|
|
@@ -42,6 +44,7 @@ const ImageAnnotationDialog = ({
|
|
|
42
44
|
mimeType,
|
|
43
45
|
imageName,
|
|
44
46
|
initialElements: initialElements ?? [],
|
|
47
|
+
initialCrop: sanitizeInitialCrop(initialCrop, size),
|
|
45
48
|
onSave,
|
|
46
49
|
onSaved: onClose
|
|
47
50
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImageAnnotationDialog.js","sources":["../../../../src/components/UI/ImageAnnotator/ImageAnnotationDialog.tsx"],"sourcesContent":["import { lazy, Suspense } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledDialog from '@/components/Form/ControlledDialog';\nimport LoadingSpinner from '@/components/UI/LoadingSpinner';\nimport NoDataFound from '@/components/UI/NoDataFound';\nimport { useBreakpoint } from '@/hooks/useBreakpoint';\n\nimport { useAnnotationBaseImage } from './hooks/useAnnotationBaseImage';\nimport { IAnnotationDocument, TAnnotationElement } from './interfaces/annotationDocument';\n\n/** Konva only reaches the bundle when someone actually opens the editor. */\nconst AnnotationEditor = lazy(() => import('./AnnotationEditor'));\n\ninterface IImageAnnotationDialogProps {\n open: boolean;\n imageUrl: string;\n imageName?: string;\n /** Previously saved annotations, when the host persists the document alongside the file. */\n initialElements?: Array<TAnnotationElement>;\n onClose: () => void;\n /** Receives the flattened image plus the editable document; resolving closes the dialog. */\n onSave: (file: File, document: IAnnotationDocument) => Promise<void> | void;\n}\n\nconst ImageAnnotationDialog: React.FC<IImageAnnotationDialogProps> = ({\n open,\n imageUrl,\n imageName,\n initialElements,\n onClose,\n onSave,\n}) => {\n const { t } = useTranslation('ui');\n const { isMobile } = useBreakpoint();\n const { image, size, mimeType, loading, failed } = useAnnotationBaseImage(open, imageUrl);\n\n return (\n <ControlledDialog\n open={open}\n size={isMobile ? 'xxl' : 'xl'}\n title={t('imageAnnotator.title')}\n onCancel={onClose}\n useFooter={false}\n fitBody\n >\n <div className=\"flex h-[78vh] flex-col\">\n {(loading || (!image && !failed)) && (\n <div className=\"flex flex-1 items-center justify-center\">\n <LoadingSpinner size={8} />\n </div>\n )}\n\n {failed && (\n <div className=\"flex flex-1 items-center justify-center\">\n <NoDataFound variant=\"error\" size=\"sm\" title={t('imageAnnotator.loadError')} />\n </div>\n )}\n\n {image && size && (\n <Suspense\n fallback={\n <div className=\"flex flex-1 items-center justify-center\">\n <LoadingSpinner size={8} />\n </div>\n }\n >\n <AnnotationEditor\n baseImage={image}\n imageSize={size}\n mimeType={mimeType}\n imageName={imageName}\n initialElements={initialElements ?? []}\n onSave={onSave}\n onSaved={onClose}\n />\n </Suspense>\n )}\n </div>\n </ControlledDialog>\n );\n};\n\nexport default ImageAnnotationDialog;\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ImageAnnotationDialog.js","sources":["../../../../src/components/UI/ImageAnnotator/ImageAnnotationDialog.tsx"],"sourcesContent":["import { lazy, Suspense } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledDialog from '@/components/Form/ControlledDialog';\nimport LoadingSpinner from '@/components/UI/LoadingSpinner';\nimport NoDataFound from '@/components/UI/NoDataFound';\nimport { useBreakpoint } from '@/hooks/useBreakpoint';\n\nimport { sanitizeInitialCrop } from './helpers/annotationCrop';\nimport { useAnnotationBaseImage } from './hooks/useAnnotationBaseImage';\nimport { IAnnotationCropRect, IAnnotationDocument, TAnnotationElement } from './interfaces/annotationDocument';\n\n/** Konva only reaches the bundle when someone actually opens the editor. */\nconst AnnotationEditor = lazy(() => import('./AnnotationEditor'));\n\ninterface IImageAnnotationDialogProps {\n open: boolean;\n imageUrl: string;\n imageName?: string;\n /** Previously saved annotations, when the host persists the document alongside the file. */\n initialElements?: Array<TAnnotationElement>;\n /** Previously saved crop (`document.crop`), restored so a reopened image keeps its framing. */\n initialCrop?: IAnnotationCropRect | null;\n onClose: () => void;\n /** Receives the flattened image plus the editable document; resolving closes the dialog. */\n onSave: (file: File, document: IAnnotationDocument) => Promise<void> | void;\n}\n\nconst ImageAnnotationDialog: React.FC<IImageAnnotationDialogProps> = ({\n open,\n imageUrl,\n imageName,\n initialElements,\n initialCrop,\n onClose,\n onSave,\n}) => {\n const { t } = useTranslation('ui');\n const { isMobile } = useBreakpoint();\n const { image, size, mimeType, loading, failed } = useAnnotationBaseImage(open, imageUrl);\n\n return (\n <ControlledDialog\n open={open}\n size={isMobile ? 'xxl' : 'xl'}\n title={t('imageAnnotator.title')}\n onCancel={onClose}\n useFooter={false}\n fitBody\n >\n <div className=\"flex h-[78vh] flex-col\">\n {(loading || (!image && !failed)) && (\n <div className=\"flex flex-1 items-center justify-center\">\n <LoadingSpinner size={8} />\n </div>\n )}\n\n {failed && (\n <div className=\"flex flex-1 items-center justify-center\">\n <NoDataFound variant=\"error\" size=\"sm\" title={t('imageAnnotator.loadError')} />\n </div>\n )}\n\n {image && size && (\n <Suspense\n fallback={\n <div className=\"flex flex-1 items-center justify-center\">\n <LoadingSpinner size={8} />\n </div>\n }\n >\n <AnnotationEditor\n baseImage={image}\n imageSize={size}\n mimeType={mimeType}\n imageName={imageName}\n initialElements={initialElements ?? []}\n initialCrop={sanitizeInitialCrop(initialCrop, size)}\n onSave={onSave}\n onSaved={onClose}\n />\n </Suspense>\n )}\n </div>\n </ControlledDialog>\n );\n};\n\nexport default ImageAnnotationDialog;\n"],"names":[],"mappings":";;;;;;;;;AAaA,MAAM,mBAAmB,KAAK,MAAM,OAAO,uBAAoB,CAAC;AAehE,MAAM,wBAA+D,CAAC;AAAA,EACpE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AACjC,QAAM,EAAE,SAAA,IAAa,cAAA;AACrB,QAAM,EAAE,OAAO,MAAM,UAAU,SAAS,WAAW,uBAAuB,MAAM,QAAQ;AAExF,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA,MAAM,WAAW,QAAQ;AAAA,MACzB,OAAO,EAAE,sBAAsB;AAAA,MAC/B,UAAU;AAAA,MACV,WAAW;AAAA,MACX,SAAO;AAAA,MAEP,UAAA,qBAAC,OAAA,EAAI,WAAU,0BACX,UAAA;AAAA,SAAA,WAAY,CAAC,SAAS,CAAC,WACvB,oBAAC,OAAA,EAAI,WAAU,2CACb,UAAA,oBAAC,gBAAA,EAAe,MAAM,EAAA,CAAG,GAC3B;AAAA,QAGD,UACC,oBAAC,OAAA,EAAI,WAAU,2CACb,UAAA,oBAAC,aAAA,EAAY,SAAQ,SAAQ,MAAK,MAAK,OAAO,EAAE,0BAA0B,GAAG,GAC/E;AAAA,QAGD,SAAS,QACR;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,8BACG,OAAA,EAAI,WAAU,2CACb,UAAA,oBAAC,gBAAA,EAAe,MAAM,EAAA,CAAG,EAAA,CAC3B;AAAA,YAGF,UAAA;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,WAAW;AAAA,gBACX,WAAW;AAAA,gBACX;AAAA,gBACA;AAAA,gBACA,iBAAiB,mBAAmB,CAAA;AAAA,gBACpC,aAAa,oBAAoB,aAAa,IAAI;AAAA,gBAClD;AAAA,gBACA,SAAS;AAAA,cAAA;AAAA,YAAA;AAAA,UACX;AAAA,QAAA;AAAA,MACF,EAAA,CAEJ;AAAA,IAAA;AAAA,EAAA;AAGN;"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { IAnnotationCropRect, IAnnotationSize } from '../interfaces/annotationDocument';
|
|
2
|
+
interface IAnnotationCropFrameProps {
|
|
3
|
+
crop: IAnnotationCropRect;
|
|
4
|
+
imageSize: IAnnotationSize;
|
|
5
|
+
viewScale: number;
|
|
6
|
+
/** Screen-pixel offset of the image inside the stage; absolute bounds start here. */
|
|
7
|
+
gutter: number;
|
|
8
|
+
aspectRatio: number | null;
|
|
9
|
+
onChange: (crop: IAnnotationCropRect) => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Drawn on a stage that shows the whole image, so the frame's local coordinates are image pixels
|
|
13
|
+
* and its absolute coordinates are those pixels times the view scale, shifted by the gutter.
|
|
14
|
+
*/
|
|
15
|
+
declare const AnnotationCropFrame: React.FC<IAnnotationCropFrameProps>;
|
|
16
|
+
export default AnnotationCropFrame;
|