Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/media-preview/media/imagePreview.js
3292 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
// @ts-check
6
"use strict";
7
8
(function () {
9
/**
10
* @param {number} value
11
* @param {number} min
12
* @param {number} max
13
* @return {number}
14
*/
15
function clamp(value, min, max) {
16
return Math.min(Math.max(value, min), max);
17
}
18
19
function getSettings() {
20
const element = document.getElementById('image-preview-settings');
21
if (element) {
22
const data = element.getAttribute('data-settings');
23
if (data) {
24
return JSON.parse(data);
25
}
26
}
27
28
throw new Error(`Could not load settings`);
29
}
30
31
/**
32
* Enable image-rendering: pixelated for images scaled by more than this.
33
*/
34
const PIXELATION_THRESHOLD = 3;
35
36
const SCALE_PINCH_FACTOR = 0.075;
37
const MAX_SCALE = 20;
38
const MIN_SCALE = 0.1;
39
40
const zoomLevels = [
41
0.1,
42
0.2,
43
0.3,
44
0.4,
45
0.5,
46
0.6,
47
0.7,
48
0.8,
49
0.9,
50
1,
51
1.5,
52
2,
53
3,
54
5,
55
7,
56
10,
57
15,
58
20
59
];
60
61
const settings = getSettings();
62
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
63
64
// @ts-ignore
65
const vscode = acquireVsCodeApi();
66
67
const initialState = vscode.getState() || { scale: 'fit', offsetX: 0, offsetY: 0 };
68
69
// State
70
let scale = initialState.scale;
71
let ctrlPressed = false;
72
let altPressed = false;
73
let hasLoadedImage = false;
74
let consumeClick = true;
75
let isActive = false;
76
77
// Elements
78
const container = document.body;
79
const image = document.createElement('img');
80
81
function updateScale(newScale) {
82
if (!image || !hasLoadedImage || !image.parentElement) {
83
return;
84
}
85
86
if (newScale === 'fit') {
87
scale = 'fit';
88
image.classList.add('scale-to-fit');
89
image.classList.remove('pixelated');
90
// @ts-ignore Non-standard CSS property
91
image.style.zoom = 'normal';
92
vscode.setState(undefined);
93
} else {
94
scale = clamp(newScale, MIN_SCALE, MAX_SCALE);
95
if (scale >= PIXELATION_THRESHOLD) {
96
image.classList.add('pixelated');
97
} else {
98
image.classList.remove('pixelated');
99
}
100
101
const dx = (window.scrollX + container.clientWidth / 2) / container.scrollWidth;
102
const dy = (window.scrollY + container.clientHeight / 2) / container.scrollHeight;
103
104
image.classList.remove('scale-to-fit');
105
// @ts-ignore Non-standard CSS property
106
image.style.zoom = scale;
107
108
const newScrollX = container.scrollWidth * dx - container.clientWidth / 2;
109
const newScrollY = container.scrollHeight * dy - container.clientHeight / 2;
110
111
window.scrollTo(newScrollX, newScrollY);
112
113
vscode.setState({ scale: scale, offsetX: newScrollX, offsetY: newScrollY });
114
}
115
116
vscode.postMessage({
117
type: 'zoom',
118
value: scale
119
});
120
}
121
122
function setActive(value) {
123
isActive = value;
124
if (value) {
125
if (isMac ? altPressed : ctrlPressed) {
126
container.classList.remove('zoom-in');
127
container.classList.add('zoom-out');
128
} else {
129
container.classList.remove('zoom-out');
130
container.classList.add('zoom-in');
131
}
132
} else {
133
ctrlPressed = false;
134
altPressed = false;
135
container.classList.remove('zoom-out');
136
container.classList.remove('zoom-in');
137
}
138
}
139
140
function firstZoom() {
141
if (!image || !hasLoadedImage) {
142
return;
143
}
144
145
scale = image.clientWidth / image.naturalWidth;
146
updateScale(scale);
147
}
148
149
function zoomIn() {
150
if (scale === 'fit') {
151
firstZoom();
152
}
153
154
let i = 0;
155
for (; i < zoomLevels.length; ++i) {
156
if (zoomLevels[i] > scale) {
157
break;
158
}
159
}
160
updateScale(zoomLevels[i] || MAX_SCALE);
161
}
162
163
function zoomOut() {
164
if (scale === 'fit') {
165
firstZoom();
166
}
167
168
let i = zoomLevels.length - 1;
169
for (; i >= 0; --i) {
170
if (zoomLevels[i] < scale) {
171
break;
172
}
173
}
174
updateScale(zoomLevels[i] || MIN_SCALE);
175
}
176
177
window.addEventListener('keydown', (/** @type {KeyboardEvent} */ e) => {
178
if (!image || !hasLoadedImage) {
179
return;
180
}
181
ctrlPressed = e.ctrlKey;
182
altPressed = e.altKey;
183
184
if (isMac ? altPressed : ctrlPressed) {
185
container.classList.remove('zoom-in');
186
container.classList.add('zoom-out');
187
}
188
});
189
190
window.addEventListener('keyup', (/** @type {KeyboardEvent} */ e) => {
191
if (!image || !hasLoadedImage) {
192
return;
193
}
194
195
ctrlPressed = e.ctrlKey;
196
altPressed = e.altKey;
197
198
if (!(isMac ? altPressed : ctrlPressed)) {
199
container.classList.remove('zoom-out');
200
container.classList.add('zoom-in');
201
}
202
});
203
204
container.addEventListener('mousedown', (/** @type {MouseEvent} */ e) => {
205
if (!image || !hasLoadedImage) {
206
return;
207
}
208
209
if (e.button !== 0) {
210
return;
211
}
212
213
ctrlPressed = e.ctrlKey;
214
altPressed = e.altKey;
215
216
consumeClick = !isActive;
217
});
218
219
container.addEventListener('click', (/** @type {MouseEvent} */ e) => {
220
if (!image || !hasLoadedImage) {
221
return;
222
}
223
224
if (e.button !== 0) {
225
return;
226
}
227
228
if (consumeClick) {
229
consumeClick = false;
230
return;
231
}
232
// left click
233
if (scale === 'fit') {
234
firstZoom();
235
}
236
237
if (!(isMac ? altPressed : ctrlPressed)) { // zoom in
238
zoomIn();
239
} else {
240
zoomOut();
241
}
242
});
243
244
container.addEventListener('wheel', (/** @type {WheelEvent} */ e) => {
245
// Prevent pinch to zoom
246
if (e.ctrlKey) {
247
e.preventDefault();
248
}
249
250
if (!image || !hasLoadedImage) {
251
return;
252
}
253
254
const isScrollWheelKeyPressed = isMac ? altPressed : ctrlPressed;
255
if (!isScrollWheelKeyPressed && !e.ctrlKey) { // pinching is reported as scroll wheel + ctrl
256
return;
257
}
258
259
if (scale === 'fit') {
260
firstZoom();
261
}
262
263
const delta = e.deltaY > 0 ? 1 : -1;
264
updateScale(scale * (1 - delta * SCALE_PINCH_FACTOR));
265
}, { passive: false });
266
267
window.addEventListener('scroll', e => {
268
if (!image || !hasLoadedImage || !image.parentElement || scale === 'fit') {
269
return;
270
}
271
272
const entry = vscode.getState();
273
if (entry) {
274
vscode.setState({ scale: entry.scale, offsetX: window.scrollX, offsetY: window.scrollY });
275
}
276
}, { passive: true });
277
278
container.classList.add('image');
279
280
image.classList.add('scale-to-fit');
281
282
image.addEventListener('load', () => {
283
if (hasLoadedImage) {
284
return;
285
}
286
hasLoadedImage = true;
287
288
vscode.postMessage({
289
type: 'size',
290
value: `${image.naturalWidth}x${image.naturalHeight}`,
291
});
292
293
document.body.classList.remove('loading');
294
document.body.classList.add('ready');
295
document.body.append(image);
296
297
updateScale(scale);
298
299
if (initialState.scale !== 'fit') {
300
window.scrollTo(initialState.offsetX, initialState.offsetY);
301
}
302
});
303
304
image.addEventListener('error', e => {
305
if (hasLoadedImage) {
306
return;
307
}
308
309
hasLoadedImage = true;
310
document.body.classList.add('error');
311
document.body.classList.remove('loading');
312
});
313
314
image.src = settings.src;
315
316
document.querySelector('.open-file-link')?.addEventListener('click', (e) => {
317
e.preventDefault();
318
vscode.postMessage({
319
type: 'reopen-as-text',
320
});
321
});
322
323
window.addEventListener('message', e => {
324
if (e.origin !== window.origin) {
325
console.error('Dropping message from unknown origin in image preview');
326
return;
327
}
328
329
switch (e.data.type) {
330
case 'setScale': {
331
updateScale(e.data.scale);
332
break;
333
}
334
case 'setActive': {
335
setActive(e.data.value);
336
break;
337
}
338
case 'zoomIn': {
339
zoomIn();
340
break;
341
}
342
case 'zoomOut': {
343
zoomOut();
344
break;
345
}
346
case 'copyImage': {
347
copyImage();
348
break;
349
}
350
}
351
});
352
353
document.addEventListener('copy', () => {
354
copyImage();
355
});
356
357
async function copyImage(retries = 5) {
358
if (!document.hasFocus() && retries > 0) {
359
// copyImage is called at the same time as webview.reveal, which means this function is running whilst the webview is gaining focus.
360
// Since navigator.clipboard.write requires the document to be focused, we need to wait for focus.
361
// We cannot use a listener, as there is a high chance the focus is gained during the setup of the listener resulting in us missing it.
362
setTimeout(() => { copyImage(retries - 1); }, 20);
363
return;
364
}
365
366
try {
367
await navigator.clipboard.write([new ClipboardItem({
368
'image/png': new Promise((resolve, reject) => {
369
const canvas = document.createElement('canvas');
370
canvas.width = image.naturalWidth;
371
canvas.height = image.naturalHeight;
372
canvas.getContext('2d').drawImage(image, 0, 0);
373
canvas.toBlob((blob) => {
374
resolve(blob);
375
canvas.remove();
376
}, 'image/png');
377
})
378
})]);
379
} catch (e) {
380
console.error(e);
381
}
382
}
383
}());
384
385