@file-viewer/renderer-image 2.2.3 → 2.2.5
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.en.md +2 -1
- package/README.md +2 -1
- package/dist/image.d.ts +7 -0
- package/dist/image.js +315 -34
- package/package.json +2 -2
package/README.en.md
CHANGED
|
@@ -28,7 +28,8 @@ const options = {
|
|
|
28
28
|
|
|
29
29
|
- Common image formats use native browser decoding without extra runtime work.
|
|
30
30
|
- HEIC / HEIF dynamically imports `heic2any` only when those formats are opened.
|
|
31
|
-
-
|
|
31
|
+
- Rotates left or right in 90° steps and refits both the main view and lightbox without clipping.
|
|
32
|
+
- Includes lightbox viewing, unified zoom and view-state providers, a theme-aware background, and unmount cleanup.
|
|
32
33
|
|
|
33
34
|
## Migration Note
|
|
34
35
|
|
package/README.md
CHANGED
|
@@ -28,7 +28,8 @@ const options = {
|
|
|
28
28
|
|
|
29
29
|
- 普通图片使用浏览器原生解码,不引入额外运行时。
|
|
30
30
|
- HEIC / HEIF 只在命中格式时动态加载 `heic2any` 并转换为 PNG Data URL。
|
|
31
|
-
-
|
|
31
|
+
- 支持左右 90° 旋转;主视图和灯箱都会按旋转后的宽高重新适配,不裁切图片。
|
|
32
|
+
- 支持图片灯箱、统一缩放和 view-state provider、明暗主题背景和组件卸载清理。
|
|
32
33
|
|
|
33
34
|
## 迁移说明
|
|
34
35
|
|
package/dist/image.d.ts
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
import { type FileRenderContext, type FileViewerRenderedInstance } from '@file-viewer/core';
|
|
2
|
+
export declare const normalizeImageRotation: (rotation: number) => number;
|
|
3
|
+
export declare const resolveRotatedImageSize: (naturalWidth: number, naturalHeight: number, scale: number, rotation: number) => {
|
|
4
|
+
imageWidth: number;
|
|
5
|
+
imageHeight: number;
|
|
6
|
+
frameWidth: number;
|
|
7
|
+
frameHeight: number;
|
|
8
|
+
};
|
|
2
9
|
export default function renderImage(buffer: ArrayBuffer, target: HTMLDivElement, type?: string, context?: FileRenderContext): Promise<FileViewerRenderedInstance>;
|
package/dist/image.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createFileViewerTranslator, createFileViewerZoomChangeEmitter as createZoomChangeEmitter, resolveFileViewerFitScale, registerFileViewerZoomProvider, unregisterFileViewerZoomProvider, } from '@file-viewer/core';
|
|
1
|
+
import { createFileViewerTranslator, createFileViewerViewStateChange, createFileViewerViewStateChangeEmitter, createFileViewerZoomChangeEmitter as createZoomChangeEmitter, resolveFileViewerFitScale, registerFileViewerViewStateProvider, registerFileViewerZoomProvider, unregisterFileViewerViewStateProvider, unregisterFileViewerZoomProvider, } from '@file-viewer/core';
|
|
2
2
|
const imageMimeMap = {
|
|
3
3
|
avif: 'image/avif',
|
|
4
4
|
bmp: 'image/bmp',
|
|
@@ -17,15 +17,30 @@ const imageMimeMap = {
|
|
|
17
17
|
};
|
|
18
18
|
const imageStyle = `
|
|
19
19
|
.image-viewer{position:relative;width:100%;height:100%;overflow:auto;background:var(--file-viewer-render-surface-background,#eef1f4);box-sizing:border-box}
|
|
20
|
+
.image-toolbar{position:sticky;top:12px;z-index:5;display:flex;align-items:center;gap:6px;width:max-content;height:42px;margin:12px 12px -54px auto;padding:5px 7px;border:1px solid rgba(148,163,184,.34);border-radius:10px;background:rgba(255,255,255,.92);box-shadow:0 10px 28px rgba(15,23,42,.14);backdrop-filter:blur(12px);box-sizing:border-box}
|
|
21
|
+
.image-toolbar button{display:grid;width:30px;height:30px;place-items:center;padding:0;border:1px solid transparent;border-radius:7px;background:transparent;color:#334155;font:600 19px/1 system-ui,sans-serif;cursor:pointer;box-sizing:border-box}
|
|
22
|
+
.image-toolbar button:hover{border-color:#bfd2ea;background:#edf5ff;color:#1769d8}
|
|
23
|
+
.image-toolbar button:focus-visible{outline:3px solid #60a5fa;outline-offset:1px}
|
|
24
|
+
.image-rotation-meter{min-width:38px;color:#64748b;font:700 12px/1 system-ui,sans-serif;text-align:center}
|
|
20
25
|
.image-stage{min-width:100%;min-height:100%;display:flex;align-items:center;justify-content:center;padding:24px;box-sizing:border-box}
|
|
21
|
-
.image-
|
|
22
|
-
.image-
|
|
23
|
-
.image-
|
|
24
|
-
.image-lightbox
|
|
25
|
-
.image-lightbox
|
|
26
|
+
.image-frame{position:relative;flex:0 0 auto}
|
|
27
|
+
.image-stage img{position:absolute;top:50%;left:50%;display:block;width:auto;max-width:none;margin:0;border:0;box-shadow:0 18px 48px rgba(15,23,42,.16);background:#fff;cursor:zoom-in;transform:translate(-50%,-50%) rotate(var(--image-rotation,0deg));transform-origin:center center;transition:transform .18s ease}
|
|
28
|
+
.image-stage img:focus-visible{outline:3px solid #2563eb;outline-offset:4px}
|
|
29
|
+
.image-lightbox{position:absolute;inset:0;z-index:40;display:flex;align-items:center;justify-content:center;padding:40px;background:rgba(15,23,42,.9);box-sizing:border-box;opacity:0;visibility:hidden;pointer-events:none;transition:opacity .18s ease,visibility 0s linear .18s}
|
|
30
|
+
.image-lightbox[data-open='true']{opacity:1;visibility:visible;pointer-events:auto;transition-delay:0s}
|
|
31
|
+
.image-lightbox img{display:block;max-width:100%;max-height:100%;object-fit:contain;background:#fff;box-shadow:0 30px 80px rgba(0,0,0,.4);cursor:default;transform:rotate(var(--image-rotation,0deg)) scale(.985);transition:transform .18s ease}
|
|
32
|
+
.image-lightbox[data-open='true'] img{transform:rotate(var(--image-rotation,0deg)) scale(1)}
|
|
33
|
+
.image-lightbox button{position:absolute;top:16px;right:16px;display:grid;width:40px;height:40px;place-items:center;padding:0;border:1px solid rgba(255,255,255,.7);border-radius:999px;background:rgba(255,255,255,.96);color:#172033;font:400 27px/1 Arial,sans-serif;cursor:pointer;box-shadow:0 12px 28px rgba(0,0,0,.24);transition:background-color .14s ease,transform .14s ease}
|
|
34
|
+
.image-lightbox button:hover{background:#fff;transform:scale(1.04)}
|
|
35
|
+
.image-lightbox button:focus-visible{outline:3px solid #60a5fa;outline-offset:2px}
|
|
26
36
|
[data-viewer-theme='dark'] .image-viewer{background:var(--file-viewer-render-surface-background,#101820)}
|
|
37
|
+
[data-viewer-theme='dark'] .image-toolbar{border-color:rgba(148,163,184,.28);background:rgba(17,24,39,.88)}
|
|
38
|
+
[data-viewer-theme='dark'] .image-toolbar button{color:#dbe5f2}
|
|
39
|
+
[data-viewer-theme='dark'] .image-rotation-meter{color:#b7c5d7}
|
|
27
40
|
@media (prefers-color-scheme:dark){[data-viewer-theme='system'] .image-viewer{background:var(--file-viewer-render-surface-background,#101820)}}
|
|
28
|
-
@media (
|
|
41
|
+
@media (prefers-color-scheme:dark){[data-viewer-theme='system'] .image-toolbar{border-color:rgba(148,163,184,.28);background:rgba(17,24,39,.88)}[data-viewer-theme='system'] .image-toolbar button{color:#dbe5f2}[data-viewer-theme='system'] .image-rotation-meter{color:#b7c5d7}}
|
|
42
|
+
@media (max-width:767px){.image-toolbar{top:8px;margin:8px 8px -50px auto}.image-stage{padding:12px}.image-lightbox{padding:16px}.image-lightbox button{top:12px;right:12px}}
|
|
43
|
+
@media (prefers-reduced-motion:reduce){.image-stage img,.image-lightbox,.image-lightbox img,.image-lightbox button{transition:none}}
|
|
29
44
|
`;
|
|
30
45
|
const createStyle = (documentRef) => {
|
|
31
46
|
const style = documentRef.createElement('style');
|
|
@@ -68,41 +83,127 @@ const resolveImageUrl = async (buffer, type) => {
|
|
|
68
83
|
}
|
|
69
84
|
return readBlobDataUrl(new Blob([buffer], { type: getImageBlobType(normalizedType) }));
|
|
70
85
|
};
|
|
86
|
+
const waitForImageReady = async (image) => {
|
|
87
|
+
if (image.complete) {
|
|
88
|
+
if (image.naturalWidth > 0 && image.naturalHeight > 0)
|
|
89
|
+
return;
|
|
90
|
+
throw new Error('The browser could not decode this image format.');
|
|
91
|
+
}
|
|
92
|
+
await new Promise((resolve, reject) => {
|
|
93
|
+
const cleanup = () => {
|
|
94
|
+
image.removeEventListener('load', onLoad);
|
|
95
|
+
image.removeEventListener('error', onError);
|
|
96
|
+
};
|
|
97
|
+
const onLoad = () => {
|
|
98
|
+
cleanup();
|
|
99
|
+
resolve();
|
|
100
|
+
};
|
|
101
|
+
const onError = () => {
|
|
102
|
+
cleanup();
|
|
103
|
+
reject(new Error('The browser could not decode this image format.'));
|
|
104
|
+
};
|
|
105
|
+
image.addEventListener('load', onLoad, { once: true });
|
|
106
|
+
image.addEventListener('error', onError, { once: true });
|
|
107
|
+
});
|
|
108
|
+
};
|
|
71
109
|
const roundImageScale = (value) => {
|
|
72
110
|
return Number(value.toFixed(3));
|
|
73
111
|
};
|
|
112
|
+
export const normalizeImageRotation = (rotation) => {
|
|
113
|
+
return ((Math.round(rotation / 90) * 90) % 360 + 360) % 360;
|
|
114
|
+
};
|
|
115
|
+
export const resolveRotatedImageSize = (naturalWidth, naturalHeight, scale, rotation) => {
|
|
116
|
+
const width = Math.max(1, Math.round(naturalWidth * scale));
|
|
117
|
+
const height = Math.max(1, Math.round(naturalHeight * scale));
|
|
118
|
+
const swapsAxes = normalizeImageRotation(rotation) % 180 !== 0;
|
|
119
|
+
return {
|
|
120
|
+
imageWidth: width,
|
|
121
|
+
imageHeight: height,
|
|
122
|
+
frameWidth: swapsAxes ? height : width,
|
|
123
|
+
frameHeight: swapsAxes ? width : height,
|
|
124
|
+
};
|
|
125
|
+
};
|
|
74
126
|
const createLightbox = (documentRef, src, t) => {
|
|
75
127
|
const lightbox = documentRef.createElement('div');
|
|
76
128
|
lightbox.className = 'image-lightbox';
|
|
77
|
-
lightbox.
|
|
129
|
+
lightbox.dataset.open = 'false';
|
|
78
130
|
lightbox.setAttribute('role', 'dialog');
|
|
79
131
|
lightbox.setAttribute('aria-modal', 'true');
|
|
132
|
+
lightbox.setAttribute('aria-hidden', 'true');
|
|
80
133
|
const image = documentRef.createElement('img');
|
|
81
134
|
image.alt = t('image.lightbox.alt');
|
|
82
135
|
image.src = src;
|
|
136
|
+
const ownerWindow = documentRef.defaultView;
|
|
137
|
+
let rotation = 0;
|
|
138
|
+
const syncImageSize = () => {
|
|
139
|
+
if (!image.naturalWidth || !image.naturalHeight)
|
|
140
|
+
return;
|
|
141
|
+
const style = ownerWindow === null || ownerWindow === void 0 ? void 0 : ownerWindow.getComputedStyle(lightbox);
|
|
142
|
+
const horizontalPadding = Number.parseFloat((style === null || style === void 0 ? void 0 : style.paddingLeft) || '0') +
|
|
143
|
+
Number.parseFloat((style === null || style === void 0 ? void 0 : style.paddingRight) || '0');
|
|
144
|
+
const verticalPadding = Number.parseFloat((style === null || style === void 0 ? void 0 : style.paddingTop) || '0') +
|
|
145
|
+
Number.parseFloat((style === null || style === void 0 ? void 0 : style.paddingBottom) || '0');
|
|
146
|
+
const availableWidth = Math.max(1, lightbox.clientWidth - horizontalPadding);
|
|
147
|
+
const availableHeight = Math.max(1, lightbox.clientHeight - verticalPadding);
|
|
148
|
+
const swapsAxes = rotation % 180 !== 0;
|
|
149
|
+
const visualWidth = swapsAxes ? image.naturalHeight : image.naturalWidth;
|
|
150
|
+
const visualHeight = swapsAxes ? image.naturalWidth : image.naturalHeight;
|
|
151
|
+
const scale = Math.min(1, availableWidth / visualWidth, availableHeight / visualHeight);
|
|
152
|
+
image.style.width = `${Math.max(1, Math.round(image.naturalWidth * scale))}px`;
|
|
153
|
+
image.style.height = `${Math.max(1, Math.round(image.naturalHeight * scale))}px`;
|
|
154
|
+
};
|
|
155
|
+
image.addEventListener('load', syncImageSize);
|
|
156
|
+
ownerWindow === null || ownerWindow === void 0 ? void 0 : ownerWindow.addEventListener('resize', syncImageSize);
|
|
83
157
|
const closeButton = documentRef.createElement('button');
|
|
84
158
|
closeButton.type = 'button';
|
|
85
159
|
closeButton.setAttribute('aria-label', t('image.lightbox.close'));
|
|
86
|
-
closeButton.textContent = '
|
|
160
|
+
closeButton.textContent = '×';
|
|
161
|
+
let previousFocus = null;
|
|
87
162
|
const close = () => {
|
|
88
|
-
lightbox.
|
|
163
|
+
if (lightbox.dataset.open !== 'true')
|
|
164
|
+
return;
|
|
165
|
+
lightbox.dataset.open = 'false';
|
|
166
|
+
lightbox.setAttribute('aria-hidden', 'true');
|
|
167
|
+
if (previousFocus === null || previousFocus === void 0 ? void 0 : previousFocus.isConnected) {
|
|
168
|
+
previousFocus.focus({ preventScroll: true });
|
|
169
|
+
}
|
|
170
|
+
previousFocus = null;
|
|
171
|
+
};
|
|
172
|
+
const onKeyDown = (event) => {
|
|
173
|
+
if (event.key === 'Escape' && lightbox.dataset.open === 'true') {
|
|
174
|
+
event.preventDefault();
|
|
175
|
+
close();
|
|
176
|
+
}
|
|
89
177
|
};
|
|
90
178
|
closeButton.addEventListener('click', close);
|
|
91
|
-
image.addEventListener('click', close);
|
|
92
179
|
lightbox.addEventListener('click', event => {
|
|
93
180
|
if (event.target === lightbox) {
|
|
94
181
|
close();
|
|
95
182
|
}
|
|
96
183
|
});
|
|
184
|
+
documentRef.addEventListener('keydown', onKeyDown);
|
|
97
185
|
lightbox.append(image, closeButton);
|
|
98
186
|
return {
|
|
99
187
|
element: lightbox,
|
|
100
|
-
|
|
101
|
-
|
|
188
|
+
setRotation(nextRotation) {
|
|
189
|
+
rotation = normalizeImageRotation(nextRotation);
|
|
190
|
+
lightbox.style.setProperty('--image-rotation', `${rotation}deg`);
|
|
191
|
+
syncImageSize();
|
|
192
|
+
},
|
|
193
|
+
open(invoker) {
|
|
194
|
+
previousFocus =
|
|
195
|
+
invoker ||
|
|
196
|
+
(documentRef.activeElement instanceof HTMLElement ? documentRef.activeElement : null);
|
|
197
|
+
lightbox.dataset.open = 'true';
|
|
198
|
+
lightbox.setAttribute('aria-hidden', 'false');
|
|
199
|
+
syncImageSize();
|
|
200
|
+
closeButton.focus({ preventScroll: true });
|
|
102
201
|
},
|
|
103
202
|
destroy() {
|
|
104
203
|
closeButton.removeEventListener('click', close);
|
|
105
|
-
|
|
204
|
+
documentRef.removeEventListener('keydown', onKeyDown);
|
|
205
|
+
image.removeEventListener('load', syncImageSize);
|
|
206
|
+
ownerWindow === null || ownerWindow === void 0 ? void 0 : ownerWindow.removeEventListener('resize', syncImageSize);
|
|
106
207
|
lightbox.remove();
|
|
107
208
|
},
|
|
108
209
|
};
|
|
@@ -115,16 +216,44 @@ export default async function renderImage(buffer, target, type, context) {
|
|
|
115
216
|
let userZoom = 1;
|
|
116
217
|
let fitScale = 1;
|
|
117
218
|
let currentScale = 1;
|
|
219
|
+
let currentRotation = 0;
|
|
118
220
|
let viewportHeight = 0;
|
|
221
|
+
let scrollStateFrame = 0;
|
|
222
|
+
let destroyed = false;
|
|
119
223
|
const zoomEmitter = createZoomChangeEmitter();
|
|
224
|
+
const viewStateEmitter = createFileViewerViewStateChangeEmitter();
|
|
120
225
|
const root = documentRef.createElement('div');
|
|
121
226
|
root.className = 'image-viewer';
|
|
122
227
|
root.dataset.viewerZoomProvider = 'image';
|
|
228
|
+
const toolbar = documentRef.createElement('div');
|
|
229
|
+
toolbar.className = 'image-toolbar';
|
|
230
|
+
toolbar.setAttribute('role', 'toolbar');
|
|
231
|
+
toolbar.setAttribute('aria-label', t('image.toolbar.rotation'));
|
|
232
|
+
const createRotateButton = (label, icon) => {
|
|
233
|
+
const button = documentRef.createElement('button');
|
|
234
|
+
button.type = 'button';
|
|
235
|
+
button.setAttribute('aria-label', label);
|
|
236
|
+
button.title = label;
|
|
237
|
+
button.textContent = icon;
|
|
238
|
+
return button;
|
|
239
|
+
};
|
|
240
|
+
const rotateLeftButton = createRotateButton(t('image.toolbar.rotateLeft'), '↺');
|
|
241
|
+
const rotationMeter = documentRef.createElement('span');
|
|
242
|
+
rotationMeter.className = 'image-rotation-meter';
|
|
243
|
+
rotationMeter.setAttribute('aria-live', 'polite');
|
|
244
|
+
rotationMeter.textContent = '0°';
|
|
245
|
+
const rotateRightButton = createRotateButton(t('image.toolbar.rotateRight'), '↻');
|
|
246
|
+
toolbar.append(rotateLeftButton, rotationMeter, rotateRightButton);
|
|
123
247
|
const stage = documentRef.createElement('div');
|
|
124
248
|
stage.className = 'image-stage';
|
|
249
|
+
const frame = documentRef.createElement('div');
|
|
250
|
+
frame.className = 'image-frame';
|
|
125
251
|
const image = documentRef.createElement('img');
|
|
126
252
|
image.alt = t('image.alt');
|
|
127
253
|
image.src = src;
|
|
254
|
+
image.tabIndex = 0;
|
|
255
|
+
image.setAttribute('role', 'button');
|
|
256
|
+
image.setAttribute('aria-haspopup', 'dialog');
|
|
128
257
|
const normalizedType = (type || '').trim().toLowerCase();
|
|
129
258
|
(_a = context === null || context === void 0 ? void 0 : context.registerThumbnailAdapter) === null || _a === void 0 ? void 0 : _a.call(context, {
|
|
130
259
|
capture: () => normalizedType === 'heic' || normalizedType === 'heif'
|
|
@@ -132,11 +261,19 @@ export default async function renderImage(buffer, target, type, context) {
|
|
|
132
261
|
: new Blob([buffer], { type: getImageBlobType(normalizedType) }),
|
|
133
262
|
getTarget: () => image,
|
|
134
263
|
});
|
|
135
|
-
|
|
136
|
-
|
|
264
|
+
frame.append(image);
|
|
265
|
+
stage.append(frame);
|
|
266
|
+
root.append(toolbar, stage);
|
|
137
267
|
const lightbox = createLightbox(documentRef, src, t);
|
|
138
|
-
const openLightbox = () => lightbox.open();
|
|
268
|
+
const openLightbox = () => lightbox.open(image);
|
|
269
|
+
const openLightboxFromKeyboard = (event) => {
|
|
270
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
271
|
+
event.preventDefault();
|
|
272
|
+
lightbox.open(image);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
139
275
|
image.addEventListener('click', openLightbox);
|
|
276
|
+
image.addEventListener('keydown', openLightboxFromKeyboard);
|
|
140
277
|
const getMinScale = () => Math.min(0.1, fitScale || 0.1);
|
|
141
278
|
const clampScale = (value) => {
|
|
142
279
|
const minScale = getMinScale();
|
|
@@ -150,20 +287,30 @@ export default async function renderImage(buffer, target, type, context) {
|
|
|
150
287
|
}
|
|
151
288
|
const availableWidth = Math.max((root.clientWidth || 0) - 48, 1);
|
|
152
289
|
const availableHeight = Math.max((root.clientHeight || viewportHeight || 0) - 48, 1);
|
|
153
|
-
|
|
290
|
+
const swapsAxes = currentRotation % 180 !== 0;
|
|
291
|
+
const visualWidth = swapsAxes ? naturalHeight : naturalWidth;
|
|
292
|
+
const visualHeight = swapsAxes ? naturalWidth : naturalHeight;
|
|
293
|
+
return Math.min(1, availableWidth / visualWidth, availableHeight / visualHeight);
|
|
154
294
|
};
|
|
155
295
|
const applyImageZoom = () => {
|
|
156
296
|
fitScale = computeFitScale();
|
|
157
297
|
currentScale = clampScale(fitScale * userZoom);
|
|
158
298
|
if (image.naturalWidth && image.naturalHeight) {
|
|
159
|
-
|
|
160
|
-
image.style.
|
|
299
|
+
const size = resolveRotatedImageSize(image.naturalWidth, image.naturalHeight, currentScale, currentRotation);
|
|
300
|
+
image.style.width = `${size.imageWidth}px`;
|
|
301
|
+
image.style.height = `${size.imageHeight}px`;
|
|
302
|
+
frame.style.width = `${size.frameWidth}px`;
|
|
303
|
+
frame.style.height = `${size.frameHeight}px`;
|
|
304
|
+
image.style.setProperty('--image-rotation', `${currentRotation}deg`);
|
|
161
305
|
return;
|
|
162
306
|
}
|
|
163
307
|
image.style.width = 'auto';
|
|
164
308
|
image.style.height = viewportHeight > 0
|
|
165
309
|
? `${Math.max(1, Math.round(viewportHeight * userZoom))}px`
|
|
166
310
|
: `${userZoom * 100}%`;
|
|
311
|
+
frame.style.width = image.style.width;
|
|
312
|
+
frame.style.height = image.style.height;
|
|
313
|
+
image.style.setProperty('--image-rotation', `${currentRotation}deg`);
|
|
167
314
|
};
|
|
168
315
|
const updateViewportSize = () => {
|
|
169
316
|
viewportHeight = root.clientHeight || 0;
|
|
@@ -182,13 +329,58 @@ export default async function renderImage(buffer, target, type, context) {
|
|
|
182
329
|
minScale: getMinScale(),
|
|
183
330
|
maxScale: 5,
|
|
184
331
|
});
|
|
185
|
-
const
|
|
332
|
+
const readScrollState = () => {
|
|
333
|
+
const maxTop = Math.max(0, root.scrollHeight - root.clientHeight);
|
|
334
|
+
const maxLeft = Math.max(0, root.scrollWidth - root.clientWidth);
|
|
335
|
+
return {
|
|
336
|
+
top: root.scrollTop || 0,
|
|
337
|
+
left: root.scrollLeft || 0,
|
|
338
|
+
width: root.scrollWidth || 0,
|
|
339
|
+
height: root.scrollHeight || 0,
|
|
340
|
+
clientWidth: root.clientWidth || 0,
|
|
341
|
+
clientHeight: root.clientHeight || 0,
|
|
342
|
+
topRatio: maxTop > 0 ? (root.scrollTop || 0) / maxTop : 0,
|
|
343
|
+
leftRatio: maxLeft > 0 ? (root.scrollLeft || 0) / maxLeft : 0,
|
|
344
|
+
};
|
|
345
|
+
};
|
|
346
|
+
const getImageViewState = () => ({
|
|
347
|
+
renderer: 'image',
|
|
348
|
+
scale: currentScale,
|
|
349
|
+
zoom: getZoomState(),
|
|
350
|
+
rotation: currentRotation,
|
|
351
|
+
scroll: readScrollState(),
|
|
352
|
+
});
|
|
353
|
+
const emitViewStateChange = (action, source = 'viewer') => {
|
|
354
|
+
const state = getImageViewState();
|
|
355
|
+
if (!destroyed) {
|
|
356
|
+
viewStateEmitter.emit(createFileViewerViewStateChange(state, { action, source }));
|
|
357
|
+
}
|
|
358
|
+
return state;
|
|
359
|
+
};
|
|
360
|
+
const setZoom = (scale, action = 'zoom-change', source = 'api', notifyViewState = true) => {
|
|
186
361
|
const nextScale = clampScale(scale);
|
|
187
362
|
userZoom = nextScale / Math.max(fitScale, 0.001);
|
|
188
363
|
applyImageZoom();
|
|
189
364
|
zoomEmitter.emit();
|
|
365
|
+
if (notifyViewState) {
|
|
366
|
+
emitViewStateChange(action, source);
|
|
367
|
+
}
|
|
190
368
|
return getZoomState();
|
|
191
369
|
};
|
|
370
|
+
const applyImageRotation = (rotation, action = 'rotation-change', source = 'viewer', notifyViewState = true) => {
|
|
371
|
+
const normalized = normalizeImageRotation(rotation);
|
|
372
|
+
if (normalized === currentRotation) {
|
|
373
|
+
return getImageViewState();
|
|
374
|
+
}
|
|
375
|
+
currentRotation = normalized;
|
|
376
|
+
rotationMeter.textContent = `${currentRotation}°`;
|
|
377
|
+
lightbox.setRotation(currentRotation);
|
|
378
|
+
applyImageZoom();
|
|
379
|
+
zoomEmitter.emit();
|
|
380
|
+
return notifyViewState
|
|
381
|
+
? emitViewStateChange(action, source)
|
|
382
|
+
: getImageViewState();
|
|
383
|
+
};
|
|
192
384
|
const fitImage = (request) => {
|
|
193
385
|
var _a, _b;
|
|
194
386
|
const naturalWidth = image.naturalWidth || 0;
|
|
@@ -204,12 +396,13 @@ export default async function renderImage(buffer, target, type, context) {
|
|
|
204
396
|
};
|
|
205
397
|
}
|
|
206
398
|
const mode = request.mode === 'auto' ? 'scale-down' : request.mode;
|
|
399
|
+
const swapsAxes = currentRotation % 180 !== 0;
|
|
207
400
|
const scale = resolveFileViewerFitScale({
|
|
208
401
|
mode,
|
|
209
402
|
viewportWidth: Math.max(1, request.viewportWidth || root.clientWidth || 0),
|
|
210
403
|
viewportHeight: Math.max(1, request.viewportHeight || root.clientHeight || viewportHeight || 0),
|
|
211
|
-
contentWidth: naturalWidth,
|
|
212
|
-
contentHeight: naturalHeight,
|
|
404
|
+
contentWidth: swapsAxes ? naturalHeight : naturalWidth,
|
|
405
|
+
contentHeight: swapsAxes ? naturalWidth : naturalHeight,
|
|
213
406
|
currentScale,
|
|
214
407
|
minScale: (_a = request.minScale) !== null && _a !== void 0 ? _a : getMinScale(),
|
|
215
408
|
maxScale: (_b = request.maxScale) !== null && _b !== void 0 ? _b : 5,
|
|
@@ -224,7 +417,7 @@ export default async function renderImage(buffer, target, type, context) {
|
|
|
224
417
|
provider: 'zoom',
|
|
225
418
|
};
|
|
226
419
|
}
|
|
227
|
-
const state = setZoom(scale);
|
|
420
|
+
const state = setZoom(scale, 'fit', request.source);
|
|
228
421
|
return {
|
|
229
422
|
applied: true,
|
|
230
423
|
mode: request.mode,
|
|
@@ -234,13 +427,64 @@ export default async function renderImage(buffer, target, type, context) {
|
|
|
234
427
|
provider: 'zoom',
|
|
235
428
|
};
|
|
236
429
|
};
|
|
430
|
+
const restoreScrollState = (scroll) => {
|
|
431
|
+
if (!scroll)
|
|
432
|
+
return;
|
|
433
|
+
const maxTop = Math.max(0, root.scrollHeight - root.clientHeight);
|
|
434
|
+
const maxLeft = Math.max(0, root.scrollWidth - root.clientWidth);
|
|
435
|
+
const top = Number.isFinite(scroll.top)
|
|
436
|
+
? Number(scroll.top)
|
|
437
|
+
: Number.isFinite(scroll.topRatio)
|
|
438
|
+
? Number(scroll.topRatio) * maxTop
|
|
439
|
+
: undefined;
|
|
440
|
+
const left = Number.isFinite(scroll.left)
|
|
441
|
+
? Number(scroll.left)
|
|
442
|
+
: Number.isFinite(scroll.leftRatio)
|
|
443
|
+
? Number(scroll.leftRatio) * maxLeft
|
|
444
|
+
: undefined;
|
|
445
|
+
if (top !== undefined)
|
|
446
|
+
root.scrollTop = Math.min(Math.max(0, top), maxTop);
|
|
447
|
+
if (left !== undefined)
|
|
448
|
+
root.scrollLeft = Math.min(Math.max(0, left), maxLeft);
|
|
449
|
+
};
|
|
450
|
+
const applyImageViewState = async (state, options = {}) => {
|
|
451
|
+
var _a, _b;
|
|
452
|
+
const source = options.source || 'api';
|
|
453
|
+
const action = options.action || 'restore';
|
|
454
|
+
const nextRotation = Number(state.rotation);
|
|
455
|
+
const nextScale = Number((_a = state.scale) !== null && _a !== void 0 ? _a : (_b = state.zoom) === null || _b === void 0 ? void 0 : _b.scale);
|
|
456
|
+
if (Number.isFinite(nextRotation)) {
|
|
457
|
+
applyImageRotation(nextRotation, 'rotation-change', source, false);
|
|
458
|
+
}
|
|
459
|
+
if (Number.isFinite(nextScale)) {
|
|
460
|
+
setZoom(nextScale, 'zoom-change', source, false);
|
|
461
|
+
}
|
|
462
|
+
restoreScrollState(state.scroll);
|
|
463
|
+
return options.notify === false
|
|
464
|
+
? getImageViewState()
|
|
465
|
+
: emitViewStateChange(action, source);
|
|
466
|
+
};
|
|
467
|
+
const targetWindow = documentRef.defaultView;
|
|
468
|
+
const scheduleScrollViewStateChange = () => {
|
|
469
|
+
if (destroyed || scrollStateFrame)
|
|
470
|
+
return;
|
|
471
|
+
if (!(targetWindow === null || targetWindow === void 0 ? void 0 : targetWindow.requestAnimationFrame)) {
|
|
472
|
+
emitViewStateChange('scroll', 'viewer');
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
scrollStateFrame = targetWindow.requestAnimationFrame(() => {
|
|
476
|
+
scrollStateFrame = 0;
|
|
477
|
+
emitViewStateChange('scroll', 'viewer');
|
|
478
|
+
});
|
|
479
|
+
};
|
|
237
480
|
registerFileViewerZoomProvider(root, {
|
|
238
|
-
zoomIn: () => setZoom(currentScale + 0.15),
|
|
239
|
-
zoomOut: () => setZoom(currentScale - 0.15),
|
|
481
|
+
zoomIn: () => setZoom(currentScale + 0.15, 'zoom-in', 'api'),
|
|
482
|
+
zoomOut: () => setZoom(currentScale - 0.15, 'zoom-out', 'api'),
|
|
240
483
|
resetZoom: () => {
|
|
241
484
|
userZoom = 1;
|
|
242
485
|
applyImageZoom();
|
|
243
486
|
zoomEmitter.emit();
|
|
487
|
+
emitViewStateChange('zoom-reset', 'api');
|
|
244
488
|
return getZoomState();
|
|
245
489
|
},
|
|
246
490
|
setZoom,
|
|
@@ -248,20 +492,57 @@ export default async function renderImage(buffer, target, type, context) {
|
|
|
248
492
|
getState: getZoomState,
|
|
249
493
|
subscribe: zoomEmitter.subscribe,
|
|
250
494
|
});
|
|
495
|
+
registerFileViewerViewStateProvider(root, {
|
|
496
|
+
getState: getImageViewState,
|
|
497
|
+
applyState: applyImageViewState,
|
|
498
|
+
fit: fitImage,
|
|
499
|
+
subscribe: viewStateEmitter.subscribe,
|
|
500
|
+
});
|
|
501
|
+
const rotateLeft = () => {
|
|
502
|
+
applyImageRotation(currentRotation - 90, 'rotate-left', 'user');
|
|
503
|
+
};
|
|
504
|
+
const rotateRight = () => {
|
|
505
|
+
applyImageRotation(currentRotation + 90, 'rotate-right', 'user');
|
|
506
|
+
};
|
|
507
|
+
rotateLeftButton.addEventListener('click', rotateLeft);
|
|
508
|
+
rotateRightButton.addEventListener('click', rotateRight);
|
|
509
|
+
root.addEventListener('scroll', scheduleScrollViewStateChange, { passive: true });
|
|
251
510
|
target.replaceChildren(createStyle(documentRef), root);
|
|
252
511
|
(((_b = context === null || context === void 0 ? void 0 : context.surface) === null || _b === void 0 ? void 0 : _b.shadowRoot) || target).append(lightbox.element);
|
|
253
512
|
updateViewportSize();
|
|
513
|
+
const cleanup = () => {
|
|
514
|
+
var _a;
|
|
515
|
+
destroyed = true;
|
|
516
|
+
(_a = context === null || context === void 0 ? void 0 : context.registerThumbnailAdapter) === null || _a === void 0 ? void 0 : _a.call(context, null);
|
|
517
|
+
unregisterFileViewerViewStateProvider(root);
|
|
518
|
+
unregisterFileViewerZoomProvider(root);
|
|
519
|
+
if (scrollStateFrame && (targetWindow === null || targetWindow === void 0 ? void 0 : targetWindow.cancelAnimationFrame)) {
|
|
520
|
+
targetWindow.cancelAnimationFrame(scrollStateFrame);
|
|
521
|
+
scrollStateFrame = 0;
|
|
522
|
+
}
|
|
523
|
+
resizeObserver.disconnect();
|
|
524
|
+
root.removeEventListener('scroll', scheduleScrollViewStateChange);
|
|
525
|
+
rotateLeftButton.removeEventListener('click', rotateLeft);
|
|
526
|
+
rotateRightButton.removeEventListener('click', rotateRight);
|
|
527
|
+
image.removeEventListener('load', updateViewportSize);
|
|
528
|
+
image.removeEventListener('click', openLightbox);
|
|
529
|
+
image.removeEventListener('keydown', openLightboxFromKeyboard);
|
|
530
|
+
lightbox.destroy();
|
|
531
|
+
target.replaceChildren();
|
|
532
|
+
};
|
|
533
|
+
try {
|
|
534
|
+
await waitForImageReady(image);
|
|
535
|
+
updateViewportSize();
|
|
536
|
+
targetWindow === null || targetWindow === void 0 ? void 0 : targetWindow.requestAnimationFrame(() => emitViewStateChange('init', 'viewer'));
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
cleanup();
|
|
540
|
+
throw error;
|
|
541
|
+
}
|
|
254
542
|
return {
|
|
255
543
|
$el: target,
|
|
256
544
|
unmount() {
|
|
257
|
-
|
|
258
|
-
(_a = context === null || context === void 0 ? void 0 : context.registerThumbnailAdapter) === null || _a === void 0 ? void 0 : _a.call(context, null);
|
|
259
|
-
unregisterFileViewerZoomProvider(root);
|
|
260
|
-
resizeObserver.disconnect();
|
|
261
|
-
image.removeEventListener('load', updateViewportSize);
|
|
262
|
-
image.removeEventListener('click', openLightbox);
|
|
263
|
-
lightbox.destroy();
|
|
264
|
-
target.replaceChildren();
|
|
545
|
+
cleanup();
|
|
265
546
|
},
|
|
266
547
|
};
|
|
267
548
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@file-viewer/renderer-image",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Standalone image renderer plugin for File Viewer with native image preview, HEIC/HEIF conversion, lightbox, and unified zoom support.",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"LICENSE"
|
|
55
55
|
],
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@file-viewer/core": "2.2.
|
|
57
|
+
"@file-viewer/core": "2.2.5",
|
|
58
58
|
"heic2any": "^0.0.4"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|