@file-viewer/renderer-image 2.2.4 → 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 +273 -26
- 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');
|
|
@@ -94,38 +109,101 @@ const waitForImageReady = async (image) => {
|
|
|
94
109
|
const roundImageScale = (value) => {
|
|
95
110
|
return Number(value.toFixed(3));
|
|
96
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
|
+
};
|
|
97
126
|
const createLightbox = (documentRef, src, t) => {
|
|
98
127
|
const lightbox = documentRef.createElement('div');
|
|
99
128
|
lightbox.className = 'image-lightbox';
|
|
100
|
-
lightbox.
|
|
129
|
+
lightbox.dataset.open = 'false';
|
|
101
130
|
lightbox.setAttribute('role', 'dialog');
|
|
102
131
|
lightbox.setAttribute('aria-modal', 'true');
|
|
132
|
+
lightbox.setAttribute('aria-hidden', 'true');
|
|
103
133
|
const image = documentRef.createElement('img');
|
|
104
134
|
image.alt = t('image.lightbox.alt');
|
|
105
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);
|
|
106
157
|
const closeButton = documentRef.createElement('button');
|
|
107
158
|
closeButton.type = 'button';
|
|
108
159
|
closeButton.setAttribute('aria-label', t('image.lightbox.close'));
|
|
109
|
-
closeButton.textContent = '
|
|
160
|
+
closeButton.textContent = '×';
|
|
161
|
+
let previousFocus = null;
|
|
110
162
|
const close = () => {
|
|
111
|
-
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
|
+
}
|
|
112
177
|
};
|
|
113
178
|
closeButton.addEventListener('click', close);
|
|
114
|
-
image.addEventListener('click', close);
|
|
115
179
|
lightbox.addEventListener('click', event => {
|
|
116
180
|
if (event.target === lightbox) {
|
|
117
181
|
close();
|
|
118
182
|
}
|
|
119
183
|
});
|
|
184
|
+
documentRef.addEventListener('keydown', onKeyDown);
|
|
120
185
|
lightbox.append(image, closeButton);
|
|
121
186
|
return {
|
|
122
187
|
element: lightbox,
|
|
123
|
-
|
|
124
|
-
|
|
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 });
|
|
125
201
|
},
|
|
126
202
|
destroy() {
|
|
127
203
|
closeButton.removeEventListener('click', close);
|
|
128
|
-
|
|
204
|
+
documentRef.removeEventListener('keydown', onKeyDown);
|
|
205
|
+
image.removeEventListener('load', syncImageSize);
|
|
206
|
+
ownerWindow === null || ownerWindow === void 0 ? void 0 : ownerWindow.removeEventListener('resize', syncImageSize);
|
|
129
207
|
lightbox.remove();
|
|
130
208
|
},
|
|
131
209
|
};
|
|
@@ -138,16 +216,44 @@ export default async function renderImage(buffer, target, type, context) {
|
|
|
138
216
|
let userZoom = 1;
|
|
139
217
|
let fitScale = 1;
|
|
140
218
|
let currentScale = 1;
|
|
219
|
+
let currentRotation = 0;
|
|
141
220
|
let viewportHeight = 0;
|
|
221
|
+
let scrollStateFrame = 0;
|
|
222
|
+
let destroyed = false;
|
|
142
223
|
const zoomEmitter = createZoomChangeEmitter();
|
|
224
|
+
const viewStateEmitter = createFileViewerViewStateChangeEmitter();
|
|
143
225
|
const root = documentRef.createElement('div');
|
|
144
226
|
root.className = 'image-viewer';
|
|
145
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);
|
|
146
247
|
const stage = documentRef.createElement('div');
|
|
147
248
|
stage.className = 'image-stage';
|
|
249
|
+
const frame = documentRef.createElement('div');
|
|
250
|
+
frame.className = 'image-frame';
|
|
148
251
|
const image = documentRef.createElement('img');
|
|
149
252
|
image.alt = t('image.alt');
|
|
150
253
|
image.src = src;
|
|
254
|
+
image.tabIndex = 0;
|
|
255
|
+
image.setAttribute('role', 'button');
|
|
256
|
+
image.setAttribute('aria-haspopup', 'dialog');
|
|
151
257
|
const normalizedType = (type || '').trim().toLowerCase();
|
|
152
258
|
(_a = context === null || context === void 0 ? void 0 : context.registerThumbnailAdapter) === null || _a === void 0 ? void 0 : _a.call(context, {
|
|
153
259
|
capture: () => normalizedType === 'heic' || normalizedType === 'heif'
|
|
@@ -155,11 +261,19 @@ export default async function renderImage(buffer, target, type, context) {
|
|
|
155
261
|
: new Blob([buffer], { type: getImageBlobType(normalizedType) }),
|
|
156
262
|
getTarget: () => image,
|
|
157
263
|
});
|
|
158
|
-
|
|
159
|
-
|
|
264
|
+
frame.append(image);
|
|
265
|
+
stage.append(frame);
|
|
266
|
+
root.append(toolbar, stage);
|
|
160
267
|
const lightbox = createLightbox(documentRef, src, t);
|
|
161
|
-
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
|
+
};
|
|
162
275
|
image.addEventListener('click', openLightbox);
|
|
276
|
+
image.addEventListener('keydown', openLightboxFromKeyboard);
|
|
163
277
|
const getMinScale = () => Math.min(0.1, fitScale || 0.1);
|
|
164
278
|
const clampScale = (value) => {
|
|
165
279
|
const minScale = getMinScale();
|
|
@@ -173,20 +287,30 @@ export default async function renderImage(buffer, target, type, context) {
|
|
|
173
287
|
}
|
|
174
288
|
const availableWidth = Math.max((root.clientWidth || 0) - 48, 1);
|
|
175
289
|
const availableHeight = Math.max((root.clientHeight || viewportHeight || 0) - 48, 1);
|
|
176
|
-
|
|
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);
|
|
177
294
|
};
|
|
178
295
|
const applyImageZoom = () => {
|
|
179
296
|
fitScale = computeFitScale();
|
|
180
297
|
currentScale = clampScale(fitScale * userZoom);
|
|
181
298
|
if (image.naturalWidth && image.naturalHeight) {
|
|
182
|
-
|
|
183
|
-
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`);
|
|
184
305
|
return;
|
|
185
306
|
}
|
|
186
307
|
image.style.width = 'auto';
|
|
187
308
|
image.style.height = viewportHeight > 0
|
|
188
309
|
? `${Math.max(1, Math.round(viewportHeight * userZoom))}px`
|
|
189
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`);
|
|
190
314
|
};
|
|
191
315
|
const updateViewportSize = () => {
|
|
192
316
|
viewportHeight = root.clientHeight || 0;
|
|
@@ -205,13 +329,58 @@ export default async function renderImage(buffer, target, type, context) {
|
|
|
205
329
|
minScale: getMinScale(),
|
|
206
330
|
maxScale: 5,
|
|
207
331
|
});
|
|
208
|
-
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) => {
|
|
209
361
|
const nextScale = clampScale(scale);
|
|
210
362
|
userZoom = nextScale / Math.max(fitScale, 0.001);
|
|
211
363
|
applyImageZoom();
|
|
212
364
|
zoomEmitter.emit();
|
|
365
|
+
if (notifyViewState) {
|
|
366
|
+
emitViewStateChange(action, source);
|
|
367
|
+
}
|
|
213
368
|
return getZoomState();
|
|
214
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
|
+
};
|
|
215
384
|
const fitImage = (request) => {
|
|
216
385
|
var _a, _b;
|
|
217
386
|
const naturalWidth = image.naturalWidth || 0;
|
|
@@ -227,12 +396,13 @@ export default async function renderImage(buffer, target, type, context) {
|
|
|
227
396
|
};
|
|
228
397
|
}
|
|
229
398
|
const mode = request.mode === 'auto' ? 'scale-down' : request.mode;
|
|
399
|
+
const swapsAxes = currentRotation % 180 !== 0;
|
|
230
400
|
const scale = resolveFileViewerFitScale({
|
|
231
401
|
mode,
|
|
232
402
|
viewportWidth: Math.max(1, request.viewportWidth || root.clientWidth || 0),
|
|
233
403
|
viewportHeight: Math.max(1, request.viewportHeight || root.clientHeight || viewportHeight || 0),
|
|
234
|
-
contentWidth: naturalWidth,
|
|
235
|
-
contentHeight: naturalHeight,
|
|
404
|
+
contentWidth: swapsAxes ? naturalHeight : naturalWidth,
|
|
405
|
+
contentHeight: swapsAxes ? naturalWidth : naturalHeight,
|
|
236
406
|
currentScale,
|
|
237
407
|
minScale: (_a = request.minScale) !== null && _a !== void 0 ? _a : getMinScale(),
|
|
238
408
|
maxScale: (_b = request.maxScale) !== null && _b !== void 0 ? _b : 5,
|
|
@@ -247,7 +417,7 @@ export default async function renderImage(buffer, target, type, context) {
|
|
|
247
417
|
provider: 'zoom',
|
|
248
418
|
};
|
|
249
419
|
}
|
|
250
|
-
const state = setZoom(scale);
|
|
420
|
+
const state = setZoom(scale, 'fit', request.source);
|
|
251
421
|
return {
|
|
252
422
|
applied: true,
|
|
253
423
|
mode: request.mode,
|
|
@@ -257,13 +427,64 @@ export default async function renderImage(buffer, target, type, context) {
|
|
|
257
427
|
provider: 'zoom',
|
|
258
428
|
};
|
|
259
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
|
+
};
|
|
260
480
|
registerFileViewerZoomProvider(root, {
|
|
261
|
-
zoomIn: () => setZoom(currentScale + 0.15),
|
|
262
|
-
zoomOut: () => setZoom(currentScale - 0.15),
|
|
481
|
+
zoomIn: () => setZoom(currentScale + 0.15, 'zoom-in', 'api'),
|
|
482
|
+
zoomOut: () => setZoom(currentScale - 0.15, 'zoom-out', 'api'),
|
|
263
483
|
resetZoom: () => {
|
|
264
484
|
userZoom = 1;
|
|
265
485
|
applyImageZoom();
|
|
266
486
|
zoomEmitter.emit();
|
|
487
|
+
emitViewStateChange('zoom-reset', 'api');
|
|
267
488
|
return getZoomState();
|
|
268
489
|
},
|
|
269
490
|
setZoom,
|
|
@@ -271,22 +492,48 @@ export default async function renderImage(buffer, target, type, context) {
|
|
|
271
492
|
getState: getZoomState,
|
|
272
493
|
subscribe: zoomEmitter.subscribe,
|
|
273
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 });
|
|
274
510
|
target.replaceChildren(createStyle(documentRef), root);
|
|
275
511
|
(((_b = context === null || context === void 0 ? void 0 : context.surface) === null || _b === void 0 ? void 0 : _b.shadowRoot) || target).append(lightbox.element);
|
|
276
512
|
updateViewportSize();
|
|
277
513
|
const cleanup = () => {
|
|
278
514
|
var _a;
|
|
515
|
+
destroyed = true;
|
|
279
516
|
(_a = context === null || context === void 0 ? void 0 : context.registerThumbnailAdapter) === null || _a === void 0 ? void 0 : _a.call(context, null);
|
|
517
|
+
unregisterFileViewerViewStateProvider(root);
|
|
280
518
|
unregisterFileViewerZoomProvider(root);
|
|
519
|
+
if (scrollStateFrame && (targetWindow === null || targetWindow === void 0 ? void 0 : targetWindow.cancelAnimationFrame)) {
|
|
520
|
+
targetWindow.cancelAnimationFrame(scrollStateFrame);
|
|
521
|
+
scrollStateFrame = 0;
|
|
522
|
+
}
|
|
281
523
|
resizeObserver.disconnect();
|
|
524
|
+
root.removeEventListener('scroll', scheduleScrollViewStateChange);
|
|
525
|
+
rotateLeftButton.removeEventListener('click', rotateLeft);
|
|
526
|
+
rotateRightButton.removeEventListener('click', rotateRight);
|
|
282
527
|
image.removeEventListener('load', updateViewportSize);
|
|
283
528
|
image.removeEventListener('click', openLightbox);
|
|
529
|
+
image.removeEventListener('keydown', openLightboxFromKeyboard);
|
|
284
530
|
lightbox.destroy();
|
|
285
531
|
target.replaceChildren();
|
|
286
532
|
};
|
|
287
533
|
try {
|
|
288
534
|
await waitForImageReady(image);
|
|
289
535
|
updateViewportSize();
|
|
536
|
+
targetWindow === null || targetWindow === void 0 ? void 0 : targetWindow.requestAnimationFrame(() => emitViewStateChange('init', 'viewer'));
|
|
290
537
|
}
|
|
291
538
|
catch (error) {
|
|
292
539
|
cleanup();
|
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": {
|