flex-image-viewer 1.0.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/README.md +160 -0
- package/dist/FlexImageViewer/Content/index.js +193 -0
- package/dist/FlexImageViewer/Footer/Actions.js +115 -0
- package/dist/FlexImageViewer/Footer/index.js +56 -0
- package/dist/FlexImageViewer/Header/Actions.js +80 -0
- package/dist/FlexImageViewer/Header/index.js +42 -0
- package/dist/FlexImageViewer/Info/index.js +52 -0
- package/dist/FlexImageViewer/Thumbnail/index.js +90 -0
- package/dist/FlexImageViewer/index.css +582 -0
- package/dist/FlexImageViewer/index.js +117 -0
- package/dist/components/ActionItem.js +10 -0
- package/dist/components/BaseActions.js +69 -0
- package/dist/components/Icon/index.js +48 -0
- package/dist/components/Icon/svg/arrow-left.js +15 -0
- package/dist/components/Icon/svg/arrow-right.js +15 -0
- package/dist/components/Icon/svg/caret-right.js +15 -0
- package/dist/components/Icon/svg/check.js +15 -0
- package/dist/components/Icon/svg/clear.js +15 -0
- package/dist/components/Icon/svg/close.js +16 -0
- package/dist/components/Icon/svg/default-image.js +17 -0
- package/dist/components/Icon/svg/down.js +15 -0
- package/dist/components/Icon/svg/info-circle.js +20 -0
- package/dist/components/Icon/svg/rotate-left.js +20 -0
- package/dist/components/Icon/svg/rotate-right.js +20 -0
- package/dist/components/Icon/svg/save.js +15 -0
- package/dist/components/Icon/svg/size.js +19 -0
- package/dist/components/Icon/svg/thumbnail.js +19 -0
- package/dist/components/Icon/svg/zoom-adapt.js +24 -0
- package/dist/components/Icon/svg/zoom-in.js +15 -0
- package/dist/components/Icon/svg/zoom-out.js +15 -0
- package/dist/components/Image.js +148 -0
- package/dist/components/Modal.js +98 -0
- package/dist/components/PercentSelect.js +160 -0
- package/dist/components/ThumbnailIamge.js +65 -0
- package/dist/components/index.js +6 -0
- package/dist/context/ViewerContext.js +208 -0
- package/dist/hooks/useImageOperations.js +113 -0
- package/dist/hooks/useImageSource.js +52 -0
- package/dist/index.js +2 -0
- package/dist/rspress.config.d.ts +2 -0
- package/dist/src/FlexImageViewer/Content/index.d.ts +11 -0
- package/dist/src/FlexImageViewer/Footer/Actions.d.ts +13 -0
- package/dist/src/FlexImageViewer/Footer/index.d.ts +8 -0
- package/dist/src/FlexImageViewer/Header/Actions.d.ts +15 -0
- package/dist/src/FlexImageViewer/Header/index.d.ts +11 -0
- package/dist/src/FlexImageViewer/Info/index.d.ts +5 -0
- package/dist/src/FlexImageViewer/Thumbnail/index.d.ts +5 -0
- package/dist/src/FlexImageViewer/index.d.ts +21 -0
- package/dist/src/components/ActionItem.d.ts +6 -0
- package/dist/src/components/BaseActions.d.ts +17 -0
- package/dist/src/components/Icon/index.d.ts +6 -0
- package/dist/src/components/Image.d.ts +18 -0
- package/dist/src/components/Modal.d.ts +9 -0
- package/dist/src/components/PercentSelect.d.ts +17 -0
- package/dist/src/components/ThumbnailIamge.d.ts +8 -0
- package/dist/src/components/index.d.ts +13 -0
- package/dist/src/context/ViewerContext.d.ts +13 -0
- package/dist/src/hooks/useImageOperations.d.ts +12 -0
- package/dist/src/hooks/useImageSource.d.ts +7 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/utils/dict.d.ts +1 -0
- package/dist/src/utils/exif.d.ts +2 -0
- package/dist/src/utils/source.d.ts +1 -0
- package/dist/utils/dict.js +16 -0
- package/dist/utils/exif.js +17 -0
- package/dist/utils/source.js +2 -0
- package/package.json +74 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { useImageSource } from "../hooks/useImageSource.js";
|
|
4
|
+
const MIN_SCALE = 0.1;
|
|
5
|
+
const MAX_SCALE = 8;
|
|
6
|
+
const DEFAULT_WHEEL_ZOOM_STEP = 0.1;
|
|
7
|
+
function Image(props) {
|
|
8
|
+
const { file, angle = 0, scale: oriScale = 1, getSrc, isAdapt = true, onAdapt, wheelZoom = true, wheelZoomStep = DEFAULT_WHEEL_ZOOM_STEP, onWheelZoom, onOrientation, disableRightClick = false, style } = props;
|
|
9
|
+
const containerRef = useRef(null);
|
|
10
|
+
const internalImgRef = useRef(null);
|
|
11
|
+
const scaleRef = useRef(oriScale);
|
|
12
|
+
const wheelZoomStepRef = useRef(wheelZoomStep);
|
|
13
|
+
const onWheelZoomRef = useRef(onWheelZoom);
|
|
14
|
+
const [scale, setScale] = useState(oriScale);
|
|
15
|
+
const [isLoaded, setIsLoaded] = useState(false);
|
|
16
|
+
const [hasError, setHasError] = useState(false);
|
|
17
|
+
const { src, loading, orientation } = useImageSource(file, getSrc);
|
|
18
|
+
useEffect(()=>{
|
|
19
|
+
setIsLoaded(false);
|
|
20
|
+
setHasError(false);
|
|
21
|
+
}, [
|
|
22
|
+
src
|
|
23
|
+
]);
|
|
24
|
+
useEffect(()=>{
|
|
25
|
+
if (void 0 !== orientation) onOrientation?.(orientation);
|
|
26
|
+
}, [
|
|
27
|
+
orientation,
|
|
28
|
+
onOrientation
|
|
29
|
+
]);
|
|
30
|
+
useEffect(()=>{
|
|
31
|
+
scaleRef.current = scale;
|
|
32
|
+
}, [
|
|
33
|
+
scale
|
|
34
|
+
]);
|
|
35
|
+
useEffect(()=>{
|
|
36
|
+
wheelZoomStepRef.current = wheelZoomStep;
|
|
37
|
+
}, [
|
|
38
|
+
wheelZoomStep
|
|
39
|
+
]);
|
|
40
|
+
useEffect(()=>{
|
|
41
|
+
onWheelZoomRef.current = onWheelZoom;
|
|
42
|
+
}, [
|
|
43
|
+
onWheelZoom
|
|
44
|
+
]);
|
|
45
|
+
useEffect(()=>{
|
|
46
|
+
if (!isAdapt) return void setScale(oriScale);
|
|
47
|
+
const container = containerRef.current;
|
|
48
|
+
const img = internalImgRef.current;
|
|
49
|
+
const calculateScale = ()=>{
|
|
50
|
+
if (!img || !container) return;
|
|
51
|
+
const containerWidth = container.clientWidth;
|
|
52
|
+
const containerHeight = container.clientHeight;
|
|
53
|
+
const imgWidth = img.naturalWidth;
|
|
54
|
+
const imgHeight = img.naturalHeight;
|
|
55
|
+
if (!imgWidth || !imgHeight || !containerWidth || !containerHeight) return;
|
|
56
|
+
const normalizedAngle = Math.abs(angle % 360);
|
|
57
|
+
const isRotated90Or270 = 90 === normalizedAngle || 270 === normalizedAngle;
|
|
58
|
+
const effectiveImgWidth = isRotated90Or270 ? imgHeight : imgWidth;
|
|
59
|
+
const effectiveImgHeight = isRotated90Or270 ? imgWidth : imgHeight;
|
|
60
|
+
const scaleX = containerWidth / effectiveImgWidth;
|
|
61
|
+
const scaleY = containerHeight / effectiveImgHeight;
|
|
62
|
+
const newScale = Math.min(scaleX, scaleY);
|
|
63
|
+
setScale(newScale);
|
|
64
|
+
onAdapt?.(newScale);
|
|
65
|
+
setIsLoaded(true);
|
|
66
|
+
};
|
|
67
|
+
calculateScale();
|
|
68
|
+
let resizeObserver = null;
|
|
69
|
+
if (container && "u" > typeof ResizeObserver) {
|
|
70
|
+
resizeObserver = new ResizeObserver(calculateScale);
|
|
71
|
+
resizeObserver.observe(container);
|
|
72
|
+
}
|
|
73
|
+
if (img) img.addEventListener('load', calculateScale);
|
|
74
|
+
return ()=>{
|
|
75
|
+
if (resizeObserver) resizeObserver.disconnect();
|
|
76
|
+
if (img) img.removeEventListener('load', calculateScale);
|
|
77
|
+
};
|
|
78
|
+
}, [
|
|
79
|
+
angle,
|
|
80
|
+
src,
|
|
81
|
+
isAdapt,
|
|
82
|
+
oriScale,
|
|
83
|
+
onAdapt
|
|
84
|
+
]);
|
|
85
|
+
useEffect(()=>{
|
|
86
|
+
const container = containerRef.current;
|
|
87
|
+
if (!container || !wheelZoom) return;
|
|
88
|
+
const handleWheel = (e)=>{
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
const delta = e.deltaY < 0 ? 1 : -1;
|
|
91
|
+
const step = wheelZoomStepRef.current;
|
|
92
|
+
const nextScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scaleRef.current + delta * step));
|
|
93
|
+
if (nextScale !== scaleRef.current) {
|
|
94
|
+
setScale(nextScale);
|
|
95
|
+
onWheelZoomRef.current?.(nextScale);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
container.addEventListener('wheel', handleWheel, {
|
|
99
|
+
passive: false
|
|
100
|
+
});
|
|
101
|
+
return ()=>{
|
|
102
|
+
container.removeEventListener('wheel', handleWheel);
|
|
103
|
+
};
|
|
104
|
+
}, [
|
|
105
|
+
wheelZoom
|
|
106
|
+
]);
|
|
107
|
+
const handleLoad = ()=>{
|
|
108
|
+
setIsLoaded(true);
|
|
109
|
+
setHasError(false);
|
|
110
|
+
};
|
|
111
|
+
const handleError = ()=>{
|
|
112
|
+
setHasError(true);
|
|
113
|
+
setIsLoaded(true);
|
|
114
|
+
};
|
|
115
|
+
return /*#__PURE__*/ jsxs("figure", {
|
|
116
|
+
ref: containerRef,
|
|
117
|
+
className: "flex-image-viewer-container",
|
|
118
|
+
style: style,
|
|
119
|
+
children: [
|
|
120
|
+
/*#__PURE__*/ jsx("img", {
|
|
121
|
+
ref: internalImgRef,
|
|
122
|
+
className: "flex-image-viewer-image",
|
|
123
|
+
src: src,
|
|
124
|
+
alt: file?.alt,
|
|
125
|
+
onLoad: handleLoad,
|
|
126
|
+
onError: handleError,
|
|
127
|
+
onContextMenu: (e)=>{
|
|
128
|
+
if (disableRightClick) e.preventDefault();
|
|
129
|
+
},
|
|
130
|
+
style: {
|
|
131
|
+
transform: `rotate(${angle}deg) scale(${scale})`,
|
|
132
|
+
opacity: isLoaded && !hasError ? 1 : 0
|
|
133
|
+
}
|
|
134
|
+
}),
|
|
135
|
+
loading && /*#__PURE__*/ jsx("div", {
|
|
136
|
+
className: "flex-image-viewer-loading"
|
|
137
|
+
}),
|
|
138
|
+
hasError && /*#__PURE__*/ jsx("div", {
|
|
139
|
+
className: "flex-image-viewer-error",
|
|
140
|
+
style: {
|
|
141
|
+
position: 'absolute'
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
]
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
const components_Image = Image;
|
|
148
|
+
export default components_Image;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
3
|
+
import react_dom from "react-dom";
|
|
4
|
+
const getContainer = null;
|
|
5
|
+
function getFocusableElements(container) {
|
|
6
|
+
const selector = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
7
|
+
return Array.from(container.querySelectorAll(selector));
|
|
8
|
+
}
|
|
9
|
+
function Modal(props) {
|
|
10
|
+
const { visible, onClose, children, getContainer: getContainerProp } = props;
|
|
11
|
+
const modalRef = useRef(null);
|
|
12
|
+
const previousFocusRef = useRef(null);
|
|
13
|
+
const getContainerElement = useCallback(()=>{
|
|
14
|
+
if (!getContainerProp) return document.body;
|
|
15
|
+
if ('string' == typeof getContainerProp) {
|
|
16
|
+
const el = document.querySelector(getContainerProp);
|
|
17
|
+
if (!el || !(el instanceof HTMLElement)) {
|
|
18
|
+
console.warn(`Modal: 选择器 "${getContainerProp}" 未找到有效元素,已回退到 body`);
|
|
19
|
+
return document.body;
|
|
20
|
+
}
|
|
21
|
+
return el;
|
|
22
|
+
}
|
|
23
|
+
if ('function' == typeof getContainerProp) {
|
|
24
|
+
const result = getContainerProp();
|
|
25
|
+
if (result && 1 === result.nodeType) return result;
|
|
26
|
+
console.warn('Modal: getContainer 函数返回无效 DOM 元素,已回退到 body');
|
|
27
|
+
return document.body;
|
|
28
|
+
}
|
|
29
|
+
if (getContainerProp instanceof HTMLElement && 1 === getContainerProp.nodeType) return getContainerProp;
|
|
30
|
+
console.warn('Modal: getContainer 参数无效,已回退到 body');
|
|
31
|
+
return document.body;
|
|
32
|
+
}, [
|
|
33
|
+
getContainerProp
|
|
34
|
+
]);
|
|
35
|
+
useEffect(()=>{
|
|
36
|
+
if (visible) {
|
|
37
|
+
previousFocusRef.current = document.activeElement;
|
|
38
|
+
document.body.style.overflow = 'hidden';
|
|
39
|
+
const timer = setTimeout(()=>{
|
|
40
|
+
if (modalRef.current) {
|
|
41
|
+
const focusable = getFocusableElements(modalRef.current);
|
|
42
|
+
if (focusable.length > 0) focusable[0].focus();
|
|
43
|
+
else modalRef.current.focus();
|
|
44
|
+
}
|
|
45
|
+
}, 0);
|
|
46
|
+
return ()=>{
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
document.body.style.overflow = '';
|
|
51
|
+
if (previousFocusRef.current) {
|
|
52
|
+
previousFocusRef.current.focus();
|
|
53
|
+
previousFocusRef.current = null;
|
|
54
|
+
}
|
|
55
|
+
}, [
|
|
56
|
+
visible
|
|
57
|
+
]);
|
|
58
|
+
useEffect(()=>{
|
|
59
|
+
const handleKeyDown = (e)=>{
|
|
60
|
+
if ('Escape' === e.key && visible && onClose) return void onClose();
|
|
61
|
+
if ('Tab' === e.key && visible && modalRef.current) {
|
|
62
|
+
const focusable = getFocusableElements(modalRef.current);
|
|
63
|
+
if (0 === focusable.length) return void e.preventDefault();
|
|
64
|
+
const first = focusable[0];
|
|
65
|
+
const last = focusable[focusable.length - 1];
|
|
66
|
+
if (e.shiftKey) {
|
|
67
|
+
if (document.activeElement === first) {
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
last.focus();
|
|
70
|
+
}
|
|
71
|
+
} else if (document.activeElement === last) {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
first.focus();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
78
|
+
return ()=>{
|
|
79
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
80
|
+
};
|
|
81
|
+
}, [
|
|
82
|
+
visible,
|
|
83
|
+
onClose
|
|
84
|
+
]);
|
|
85
|
+
if (!visible) return null;
|
|
86
|
+
const targetElement = getContainerElement();
|
|
87
|
+
const modalContent = /*#__PURE__*/ jsx("div", {
|
|
88
|
+
ref: modalRef,
|
|
89
|
+
className: "flex-image-viewer-modal",
|
|
90
|
+
role: "dialog",
|
|
91
|
+
"aria-modal": "true",
|
|
92
|
+
tabIndex: -1,
|
|
93
|
+
children: children
|
|
94
|
+
});
|
|
95
|
+
return /*#__PURE__*/ react_dom.createPortal(modalContent, targetElement);
|
|
96
|
+
}
|
|
97
|
+
export default Modal;
|
|
98
|
+
export { getContainer };
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { min } from "es-toolkit/compat";
|
|
4
|
+
import { Icon } from "./index.js";
|
|
5
|
+
const MAX_SCALE = 800;
|
|
6
|
+
const DEFAULT_PERCENT_OPTIONS = Array.from({
|
|
7
|
+
length: 8
|
|
8
|
+
}, (_, i)=>MAX_SCALE - 100 * i).concat([
|
|
9
|
+
75,
|
|
10
|
+
50,
|
|
11
|
+
25,
|
|
12
|
+
10
|
|
13
|
+
]);
|
|
14
|
+
const unit = MAX_SCALE / 100;
|
|
15
|
+
const getScaleValue = (sliderValue)=>Math.round(sliderValue * unit);
|
|
16
|
+
const getSliderValue = (scale)=>Math.round(scale / unit);
|
|
17
|
+
function PercentSelect({ value = 100, onChange, disabled = false, className = '', style, options = DEFAULT_PERCENT_OPTIONS }) {
|
|
18
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
19
|
+
const [inputValue, setInputValue] = useState(value.toString());
|
|
20
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
21
|
+
const selectRef = useRef(null);
|
|
22
|
+
const inputRef = useRef(null);
|
|
23
|
+
const optionList = options.map((opt)=>({
|
|
24
|
+
value: opt,
|
|
25
|
+
label: `${opt}%`
|
|
26
|
+
}));
|
|
27
|
+
useEffect(()=>{
|
|
28
|
+
if (!isEditing) setInputValue(value.toString());
|
|
29
|
+
}, [
|
|
30
|
+
value,
|
|
31
|
+
isEditing
|
|
32
|
+
]);
|
|
33
|
+
useEffect(()=>{
|
|
34
|
+
const handleClickOutside = (event)=>{
|
|
35
|
+
if (selectRef.current && !selectRef.current.contains(event.target)) {
|
|
36
|
+
setIsOpen(false);
|
|
37
|
+
if (isEditing) handleInputBlur();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
41
|
+
return ()=>document.removeEventListener('mousedown', handleClickOutside);
|
|
42
|
+
}, [
|
|
43
|
+
isEditing,
|
|
44
|
+
inputValue,
|
|
45
|
+
value
|
|
46
|
+
]);
|
|
47
|
+
const handleOptionClick = (optionValue)=>{
|
|
48
|
+
if (disabled) return;
|
|
49
|
+
onChange?.(optionValue);
|
|
50
|
+
setInputValue(optionValue.toString());
|
|
51
|
+
setIsOpen(false);
|
|
52
|
+
setIsEditing(false);
|
|
53
|
+
};
|
|
54
|
+
const handleInputChange = (e)=>{
|
|
55
|
+
const val = e.target.value;
|
|
56
|
+
if ('' === val || /^\d*\.?\d*$/.test(val)) {
|
|
57
|
+
const num = min([
|
|
58
|
+
parseFloat(val),
|
|
59
|
+
MAX_SCALE
|
|
60
|
+
]);
|
|
61
|
+
setInputValue(void 0 === num ? value.toString() : num.toString());
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const handleInputBlur = ()=>{
|
|
65
|
+
const num = min([
|
|
66
|
+
parseFloat(inputValue),
|
|
67
|
+
MAX_SCALE
|
|
68
|
+
]);
|
|
69
|
+
if (void 0 === num || isNaN(num) || num <= 0 || '' === inputValue) setInputValue(value.toString());
|
|
70
|
+
else {
|
|
71
|
+
if (num !== value) onChange?.(num);
|
|
72
|
+
setInputValue(value.toString());
|
|
73
|
+
}
|
|
74
|
+
setIsEditing(false);
|
|
75
|
+
};
|
|
76
|
+
const handleInputKeyDown = (e)=>{
|
|
77
|
+
if ('Enter' === e.key) {
|
|
78
|
+
handleInputBlur();
|
|
79
|
+
inputRef.current?.blur();
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const handleToggle = (e)=>{
|
|
83
|
+
if (disabled) return;
|
|
84
|
+
e.stopPropagation();
|
|
85
|
+
setIsOpen(!isOpen);
|
|
86
|
+
};
|
|
87
|
+
const handleInputClick = (e)=>{
|
|
88
|
+
if (disabled) return;
|
|
89
|
+
e.stopPropagation();
|
|
90
|
+
if (!isEditing) {
|
|
91
|
+
setIsEditing(true);
|
|
92
|
+
setTimeout(()=>{
|
|
93
|
+
inputRef.current?.select();
|
|
94
|
+
}, 0);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
return /*#__PURE__*/ jsxs("div", {
|
|
98
|
+
ref: selectRef,
|
|
99
|
+
className: `flex-image-viewer-percent-select ${isOpen ? 'flex-image-viewer-percent-select-open' : ''} ${disabled ? 'flex-image-viewer-percent-select-disabled' : ''} ${className}`,
|
|
100
|
+
style: style,
|
|
101
|
+
children: [
|
|
102
|
+
/*#__PURE__*/ jsxs("div", {
|
|
103
|
+
className: "flex-image-viewer-percent-select-control",
|
|
104
|
+
children: [
|
|
105
|
+
/*#__PURE__*/ jsx("input", {
|
|
106
|
+
ref: inputRef,
|
|
107
|
+
type: "text",
|
|
108
|
+
readOnly: !isEditing,
|
|
109
|
+
className: `flex-image-viewer-percent-select-input ${isEditing ? 'flex-image-viewer-percent-select-input-editing' : ''}`,
|
|
110
|
+
value: isEditing ? inputValue : value.toString(),
|
|
111
|
+
onChange: handleInputChange,
|
|
112
|
+
onBlur: handleInputBlur,
|
|
113
|
+
onKeyDown: handleInputKeyDown,
|
|
114
|
+
onClick: handleInputClick,
|
|
115
|
+
disabled: disabled,
|
|
116
|
+
placeholder: "缩放比例"
|
|
117
|
+
}),
|
|
118
|
+
/*#__PURE__*/ jsx("span", {
|
|
119
|
+
className: "flex-image-viewer-percent-select-suffix",
|
|
120
|
+
children: "%"
|
|
121
|
+
}),
|
|
122
|
+
/*#__PURE__*/ jsx("div", {
|
|
123
|
+
className: `flex-image-viewer-percent-select-arrow ${isOpen ? 'flex-image-viewer-percent-select-arrow-open' : ''}`,
|
|
124
|
+
onClick: handleToggle,
|
|
125
|
+
children: /*#__PURE__*/ jsx(Icon, {
|
|
126
|
+
className: "flex-image-viewer-percent-select-arrow-icon",
|
|
127
|
+
name: "Down"
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
]
|
|
131
|
+
}),
|
|
132
|
+
isOpen && /*#__PURE__*/ jsx("div", {
|
|
133
|
+
className: "flex-image-viewer-percent-select-dropdown flex-image-viewer-percent-select-dropdown-up",
|
|
134
|
+
children: /*#__PURE__*/ jsx("div", {
|
|
135
|
+
className: "flex-image-viewer-percent-select-options",
|
|
136
|
+
children: optionList.map((option)=>{
|
|
137
|
+
const isSelected = option.value === value;
|
|
138
|
+
return /*#__PURE__*/ jsxs("div", {
|
|
139
|
+
className: `flex-image-viewer-percent-select-option ${isSelected ? 'flex-image-viewer-percent-select-option-selected' : ''}`,
|
|
140
|
+
onClick: ()=>handleOptionClick(option.value),
|
|
141
|
+
children: [
|
|
142
|
+
option.label,
|
|
143
|
+
isSelected && /*#__PURE__*/ jsx("span", {
|
|
144
|
+
className: "flex-image-viewer-percent-select-check",
|
|
145
|
+
children: /*#__PURE__*/ jsx(Icon, {
|
|
146
|
+
className: "flex-image-viewer-percent-select-check-icon",
|
|
147
|
+
name: "Check"
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
]
|
|
151
|
+
}, option.value);
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
]
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
const components_PercentSelect = PercentSelect;
|
|
159
|
+
export default components_PercentSelect;
|
|
160
|
+
export { getScaleValue, getSliderValue };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { useImageSource } from "../hooks/useImageSource.js";
|
|
4
|
+
function ThumbnailImage(props) {
|
|
5
|
+
const { file, angle = 0, getThumbnail } = props;
|
|
6
|
+
const containerRef = useRef(null);
|
|
7
|
+
const imgRef = useRef(null);
|
|
8
|
+
const [scale, setScale] = useState(1);
|
|
9
|
+
const [isLoaded, setIsLoaded] = useState(false);
|
|
10
|
+
const { src, loading } = useImageSource(file, getThumbnail);
|
|
11
|
+
useEffect(()=>{
|
|
12
|
+
const img = imgRef.current;
|
|
13
|
+
if (!img || !containerRef.current) return;
|
|
14
|
+
const container = containerRef.current;
|
|
15
|
+
const calculateScale = ()=>{
|
|
16
|
+
const containerWidth = container.clientWidth;
|
|
17
|
+
const containerHeight = container.clientHeight;
|
|
18
|
+
const imgWidth = img.naturalWidth || img.width;
|
|
19
|
+
const imgHeight = img.naturalHeight || img.height;
|
|
20
|
+
if (!imgWidth || !imgHeight || !containerWidth || !containerHeight) return;
|
|
21
|
+
const normalizedAngle = Math.abs(angle % 360);
|
|
22
|
+
const isRotated90Or270 = 90 === normalizedAngle || 270 === normalizedAngle;
|
|
23
|
+
const effectiveImgWidth = isRotated90Or270 ? imgHeight : imgWidth;
|
|
24
|
+
const effectiveImgHeight = isRotated90Or270 ? imgWidth : imgHeight;
|
|
25
|
+
const scaleX = containerWidth / effectiveImgWidth;
|
|
26
|
+
const scaleY = containerHeight / effectiveImgHeight;
|
|
27
|
+
const newScale = Math.min(scaleX, scaleY);
|
|
28
|
+
setScale(newScale);
|
|
29
|
+
setIsLoaded(true);
|
|
30
|
+
};
|
|
31
|
+
calculateScale();
|
|
32
|
+
img.addEventListener('load', calculateScale);
|
|
33
|
+
return ()=>{
|
|
34
|
+
img.removeEventListener('load', calculateScale);
|
|
35
|
+
};
|
|
36
|
+
}, [
|
|
37
|
+
angle,
|
|
38
|
+
src
|
|
39
|
+
]);
|
|
40
|
+
const handleError = ()=>{
|
|
41
|
+
setIsLoaded(true);
|
|
42
|
+
};
|
|
43
|
+
return /*#__PURE__*/ jsxs("div", {
|
|
44
|
+
ref: containerRef,
|
|
45
|
+
className: "flex-image-viewer-thumbnail-container",
|
|
46
|
+
children: [
|
|
47
|
+
/*#__PURE__*/ jsx("img", {
|
|
48
|
+
ref: imgRef,
|
|
49
|
+
className: "flex-image-viewer-thumbnail-image",
|
|
50
|
+
src: src,
|
|
51
|
+
alt: file?.alt,
|
|
52
|
+
onError: handleError,
|
|
53
|
+
style: {
|
|
54
|
+
transform: `rotate(${angle}deg) scale(${scale})`,
|
|
55
|
+
opacity: isLoaded ? 1 : 0
|
|
56
|
+
}
|
|
57
|
+
}),
|
|
58
|
+
loading && /*#__PURE__*/ jsx("div", {
|
|
59
|
+
className: "flex-image-viewer-loading"
|
|
60
|
+
})
|
|
61
|
+
]
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const ThumbnailIamge = ThumbnailImage;
|
|
65
|
+
export default ThumbnailIamge;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { default as Icon } from "./Icon/index.js";
|
|
2
|
+
export { default as Image } from "./Image.js";
|
|
3
|
+
export { default as Modal } from "./Modal.js";
|
|
4
|
+
export { default as PercentSelect } from "./PercentSelect.js";
|
|
5
|
+
export { default as ActionItem } from "./ActionItem.js";
|
|
6
|
+
export { default as BaseActions } from "./BaseActions.js";
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useReducer } from "react";
|
|
3
|
+
const ZOOM_STEP = 0.1;
|
|
4
|
+
const MIN_SCALE = 0.1;
|
|
5
|
+
const MAX_SCALE = 8;
|
|
6
|
+
const ViewerStateContext = /*#__PURE__*/ createContext(null);
|
|
7
|
+
const ViewerDispatchContext = /*#__PURE__*/ createContext(null);
|
|
8
|
+
function useViewerState() {
|
|
9
|
+
const context = useContext(ViewerStateContext);
|
|
10
|
+
if (!context) throw new Error('useViewerState must be used within a ViewerProvider');
|
|
11
|
+
return context;
|
|
12
|
+
}
|
|
13
|
+
function useViewerDispatch() {
|
|
14
|
+
const context = useContext(ViewerDispatchContext);
|
|
15
|
+
if (!context) throw new Error('useViewerDispatch must be used within a ViewerProvider');
|
|
16
|
+
return context;
|
|
17
|
+
}
|
|
18
|
+
function viewerReducer(state, action) {
|
|
19
|
+
switch(action.type){
|
|
20
|
+
case 'SET_CURRENT_INDEX':
|
|
21
|
+
return {
|
|
22
|
+
...state,
|
|
23
|
+
currentIndex: action.payload
|
|
24
|
+
};
|
|
25
|
+
case 'PREV_IMAGE':
|
|
26
|
+
{
|
|
27
|
+
const total = state.filesLength;
|
|
28
|
+
if (total <= 1) return state;
|
|
29
|
+
let index = state.currentIndex - 1;
|
|
30
|
+
if (state.loop && index < 1) index = total;
|
|
31
|
+
else if (index < 1) index = 1;
|
|
32
|
+
return {
|
|
33
|
+
...state,
|
|
34
|
+
currentIndex: index
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
case 'NEXT_IMAGE':
|
|
38
|
+
{
|
|
39
|
+
const total = state.filesLength;
|
|
40
|
+
if (total <= 1) return state;
|
|
41
|
+
let index = state.currentIndex + 1;
|
|
42
|
+
if (state.loop && index > total) index = 1;
|
|
43
|
+
else if (index > total) index = total;
|
|
44
|
+
return {
|
|
45
|
+
...state,
|
|
46
|
+
currentIndex: index
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
case 'SET_LOOP':
|
|
50
|
+
return {
|
|
51
|
+
...state,
|
|
52
|
+
loop: action.payload
|
|
53
|
+
};
|
|
54
|
+
case 'ROTATE_LEFT':
|
|
55
|
+
{
|
|
56
|
+
const { index } = action.payload;
|
|
57
|
+
const newOptions = state.imageOptions.map((opt, i)=>i === index ? {
|
|
58
|
+
...opt,
|
|
59
|
+
angle: (opt.angle - 90 + 360) % 360
|
|
60
|
+
} : opt);
|
|
61
|
+
return {
|
|
62
|
+
...state,
|
|
63
|
+
imageOptions: newOptions
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
case 'ROTATE_RIGHT':
|
|
67
|
+
{
|
|
68
|
+
const { index } = action.payload;
|
|
69
|
+
const newOptions = state.imageOptions.map((opt, i)=>i === index ? {
|
|
70
|
+
...opt,
|
|
71
|
+
angle: (opt.angle + 90) % 360
|
|
72
|
+
} : opt);
|
|
73
|
+
return {
|
|
74
|
+
...state,
|
|
75
|
+
imageOptions: newOptions
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
case 'SET_ZOOM':
|
|
79
|
+
{
|
|
80
|
+
const { index, scale } = action.payload;
|
|
81
|
+
const newOptions = state.imageOptions.map((opt, i)=>i === index ? {
|
|
82
|
+
...opt,
|
|
83
|
+
scale,
|
|
84
|
+
isAdapt: false
|
|
85
|
+
} : opt);
|
|
86
|
+
return {
|
|
87
|
+
...state,
|
|
88
|
+
imageOptions: newOptions
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
case 'ZOOM_IN':
|
|
92
|
+
{
|
|
93
|
+
const { index, step = ZOOM_STEP } = action.payload;
|
|
94
|
+
const newOptions = state.imageOptions.map((opt, i)=>i === index ? {
|
|
95
|
+
...opt,
|
|
96
|
+
scale: Math.min(MAX_SCALE, opt.scale + step),
|
|
97
|
+
isAdapt: false
|
|
98
|
+
} : opt);
|
|
99
|
+
return {
|
|
100
|
+
...state,
|
|
101
|
+
imageOptions: newOptions
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
case 'ZOOM_OUT':
|
|
105
|
+
{
|
|
106
|
+
const { index, step = ZOOM_STEP } = action.payload;
|
|
107
|
+
const newOptions = state.imageOptions.map((opt, i)=>i === index ? {
|
|
108
|
+
...opt,
|
|
109
|
+
scale: Math.max(MIN_SCALE, opt.scale - step),
|
|
110
|
+
isAdapt: false
|
|
111
|
+
} : opt);
|
|
112
|
+
return {
|
|
113
|
+
...state,
|
|
114
|
+
imageOptions: newOptions
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
case 'ADAPT_ZOOM':
|
|
118
|
+
{
|
|
119
|
+
const { index, scale } = action.payload;
|
|
120
|
+
const newOptions = state.imageOptions.map((opt, i)=>i === index ? {
|
|
121
|
+
...opt,
|
|
122
|
+
scale,
|
|
123
|
+
isAdapt: true
|
|
124
|
+
} : opt);
|
|
125
|
+
return {
|
|
126
|
+
...state,
|
|
127
|
+
imageOptions: newOptions
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
case 'CLEAR_IMAGE':
|
|
131
|
+
{
|
|
132
|
+
const { index, angle, scale } = action.payload;
|
|
133
|
+
const newOptions = state.imageOptions.map((opt, i)=>i === index ? {
|
|
134
|
+
...opt,
|
|
135
|
+
angle,
|
|
136
|
+
scale,
|
|
137
|
+
isAdapt: true
|
|
138
|
+
} : opt);
|
|
139
|
+
return {
|
|
140
|
+
...state,
|
|
141
|
+
imageOptions: newOptions
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
case 'TOGGLE_INFO':
|
|
145
|
+
return {
|
|
146
|
+
...state,
|
|
147
|
+
infoVisible: !state.infoVisible
|
|
148
|
+
};
|
|
149
|
+
case 'TOGGLE_THUMBNAIL':
|
|
150
|
+
return {
|
|
151
|
+
...state,
|
|
152
|
+
thumbnailVisible: !state.thumbnailVisible
|
|
153
|
+
};
|
|
154
|
+
case 'INITIALIZE_FILES':
|
|
155
|
+
{
|
|
156
|
+
const { files } = action.payload;
|
|
157
|
+
const count = files.length;
|
|
158
|
+
return {
|
|
159
|
+
...state,
|
|
160
|
+
filesLength: count,
|
|
161
|
+
files,
|
|
162
|
+
imageOptions: Array.from({
|
|
163
|
+
length: count
|
|
164
|
+
}, (_, i)=>({
|
|
165
|
+
angle: files[i]?.angle ?? 0,
|
|
166
|
+
scale: files[i]?.scale ?? 1,
|
|
167
|
+
isAdapt: true
|
|
168
|
+
}))
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
case 'UPDATE_FILE':
|
|
172
|
+
{
|
|
173
|
+
const { index, file } = action.payload;
|
|
174
|
+
const newFiles = state.files.map((f, i)=>i === index ? {
|
|
175
|
+
...f,
|
|
176
|
+
...file
|
|
177
|
+
} : f);
|
|
178
|
+
return {
|
|
179
|
+
...state,
|
|
180
|
+
files: newFiles
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
default:
|
|
184
|
+
return state;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function createInitialState(initialCurrent = 1, initialFilesLength = 0) {
|
|
188
|
+
return {
|
|
189
|
+
currentIndex: initialCurrent,
|
|
190
|
+
filesLength: initialFilesLength,
|
|
191
|
+
imageOptions: [],
|
|
192
|
+
infoVisible: false,
|
|
193
|
+
thumbnailVisible: false,
|
|
194
|
+
loop: false,
|
|
195
|
+
files: []
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function ViewerProvider({ children, initialCurrent = 1, initialFilesLength = 0 }) {
|
|
199
|
+
const [state, dispatch] = useReducer(viewerReducer, createInitialState(initialCurrent, initialFilesLength));
|
|
200
|
+
return /*#__PURE__*/ jsx(ViewerStateContext.Provider, {
|
|
201
|
+
value: state,
|
|
202
|
+
children: /*#__PURE__*/ jsx(ViewerDispatchContext.Provider, {
|
|
203
|
+
value: dispatch,
|
|
204
|
+
children: children
|
|
205
|
+
})
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
export { ViewerDispatchContext, ViewerProvider, ViewerStateContext, useViewerDispatch, useViewerState, viewerReducer };
|