123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383 |
- /*---------------------------------------------------------------------------------------------
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
- // @ts-check
- "use strict";
- (function () {
- /**
- * @param {number} value
- * @param {number} min
- * @param {number} max
- * @return {number}
- */
- function clamp(value, min, max) {
- return Math.min(Math.max(value, min), max);
- }
- function getSettings() {
- const element = document.getElementById('image-preview-settings');
- if (element) {
- const data = element.getAttribute('data-settings');
- if (data) {
- return JSON.parse(data);
- }
- }
- throw new Error(`Could not load settings`);
- }
- /**
- * Enable image-rendering: pixelated for images scaled by more than this.
- */
- const PIXELATION_THRESHOLD = 3;
- const SCALE_PINCH_FACTOR = 0.075;
- const MAX_SCALE = 20;
- const MIN_SCALE = 0.1;
- const zoomLevels = [
- 0.1,
- 0.2,
- 0.3,
- 0.4,
- 0.5,
- 0.6,
- 0.7,
- 0.8,
- 0.9,
- 1,
- 1.5,
- 2,
- 3,
- 5,
- 7,
- 10,
- 15,
- 20
- ];
- const settings = getSettings();
- const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
- // @ts-ignore
- const vscode = acquireVsCodeApi();
- const initialState = vscode.getState() || { scale: 'fit', offsetX: 0, offsetY: 0 };
- // State
- let scale = initialState.scale;
- let ctrlPressed = false;
- let altPressed = false;
- let hasLoadedImage = false;
- let consumeClick = true;
- let isActive = false;
- // Elements
- const container = document.body;
- const image = document.createElement('img');
- function updateScale(newScale) {
- if (!image || !hasLoadedImage || !image.parentElement) {
- return;
- }
- if (newScale === 'fit') {
- scale = 'fit';
- image.classList.add('scale-to-fit');
- image.classList.remove('pixelated');
- // @ts-ignore Non-standard CSS property
- image.style.zoom = 'normal';
- vscode.setState(undefined);
- } else {
- scale = clamp(newScale, MIN_SCALE, MAX_SCALE);
- if (scale >= PIXELATION_THRESHOLD) {
- image.classList.add('pixelated');
- } else {
- image.classList.remove('pixelated');
- }
- const dx = (window.scrollX + container.clientWidth / 2) / container.scrollWidth;
- const dy = (window.scrollY + container.clientHeight / 2) / container.scrollHeight;
- image.classList.remove('scale-to-fit');
- // @ts-ignore Non-standard CSS property
- image.style.zoom = scale;
- const newScrollX = container.scrollWidth * dx - container.clientWidth / 2;
- const newScrollY = container.scrollHeight * dy - container.clientHeight / 2;
- window.scrollTo(newScrollX, newScrollY);
- vscode.setState({ scale: scale, offsetX: newScrollX, offsetY: newScrollY });
- }
- vscode.postMessage({
- type: 'zoom',
- value: scale
- });
- }
- function setActive(value) {
- isActive = value;
- if (value) {
- if (isMac ? altPressed : ctrlPressed) {
- container.classList.remove('zoom-in');
- container.classList.add('zoom-out');
- } else {
- container.classList.remove('zoom-out');
- container.classList.add('zoom-in');
- }
- } else {
- ctrlPressed = false;
- altPressed = false;
- container.classList.remove('zoom-out');
- container.classList.remove('zoom-in');
- }
- }
- function firstZoom() {
- if (!image || !hasLoadedImage) {
- return;
- }
- scale = image.clientWidth / image.naturalWidth;
- updateScale(scale);
- }
- function zoomIn() {
- if (scale === 'fit') {
- firstZoom();
- }
- let i = 0;
- for (; i < zoomLevels.length; ++i) {
- if (zoomLevels[i] > scale) {
- break;
- }
- }
- updateScale(zoomLevels[i] || MAX_SCALE);
- }
- function zoomOut() {
- if (scale === 'fit') {
- firstZoom();
- }
- let i = zoomLevels.length - 1;
- for (; i >= 0; --i) {
- if (zoomLevels[i] < scale) {
- break;
- }
- }
- updateScale(zoomLevels[i] || MIN_SCALE);
- }
- window.addEventListener('keydown', (/** @type {KeyboardEvent} */ e) => {
- if (!image || !hasLoadedImage) {
- return;
- }
- ctrlPressed = e.ctrlKey;
- altPressed = e.altKey;
- if (isMac ? altPressed : ctrlPressed) {
- container.classList.remove('zoom-in');
- container.classList.add('zoom-out');
- }
- });
- window.addEventListener('keyup', (/** @type {KeyboardEvent} */ e) => {
- if (!image || !hasLoadedImage) {
- return;
- }
- ctrlPressed = e.ctrlKey;
- altPressed = e.altKey;
- if (!(isMac ? altPressed : ctrlPressed)) {
- container.classList.remove('zoom-out');
- container.classList.add('zoom-in');
- }
- });
- container.addEventListener('mousedown', (/** @type {MouseEvent} */ e) => {
- if (!image || !hasLoadedImage) {
- return;
- }
- if (e.button !== 0) {
- return;
- }
- ctrlPressed = e.ctrlKey;
- altPressed = e.altKey;
- consumeClick = !isActive;
- });
- container.addEventListener('click', (/** @type {MouseEvent} */ e) => {
- if (!image || !hasLoadedImage) {
- return;
- }
- if (e.button !== 0) {
- return;
- }
- if (consumeClick) {
- consumeClick = false;
- return;
- }
- // left click
- if (scale === 'fit') {
- firstZoom();
- }
- if (!(isMac ? altPressed : ctrlPressed)) { // zoom in
- zoomIn();
- } else {
- zoomOut();
- }
- });
- container.addEventListener('wheel', (/** @type {WheelEvent} */ e) => {
- // Prevent pinch to zoom
- if (e.ctrlKey) {
- e.preventDefault();
- }
- if (!image || !hasLoadedImage) {
- return;
- }
- const isScrollWheelKeyPressed = isMac ? altPressed : ctrlPressed;
- if (!isScrollWheelKeyPressed && !e.ctrlKey) { // pinching is reported as scroll wheel + ctrl
- return;
- }
- if (scale === 'fit') {
- firstZoom();
- }
- const delta = e.deltaY > 0 ? 1 : -1;
- updateScale(scale * (1 - delta * SCALE_PINCH_FACTOR));
- }, { passive: false });
- window.addEventListener('scroll', e => {
- if (!image || !hasLoadedImage || !image.parentElement || scale === 'fit') {
- return;
- }
- const entry = vscode.getState();
- if (entry) {
- vscode.setState({ scale: entry.scale, offsetX: window.scrollX, offsetY: window.scrollY });
- }
- }, { passive: true });
- container.classList.add('image');
- image.classList.add('scale-to-fit');
- image.addEventListener('load', () => {
- if (hasLoadedImage) {
- return;
- }
- hasLoadedImage = true;
- vscode.postMessage({
- type: 'size',
- value: `${image.naturalWidth}x${image.naturalHeight}`,
- });
- document.body.classList.remove('loading');
- document.body.classList.add('ready');
- document.body.append(image);
- updateScale(scale);
- if (initialState.scale !== 'fit') {
- window.scrollTo(initialState.offsetX, initialState.offsetY);
- }
- });
- image.addEventListener('error', e => {
- if (hasLoadedImage) {
- return;
- }
- hasLoadedImage = true;
- document.body.classList.add('error');
- document.body.classList.remove('loading');
- });
- image.src = settings.src;
- document.querySelector('.open-file-link')?.addEventListener('click', (e) => {
- e.preventDefault();
- vscode.postMessage({
- type: 'reopen-as-text',
- });
- });
- window.addEventListener('message', e => {
- if (e.origin !== window.origin) {
- console.error('Dropping message from unknown origin in image preview');
- return;
- }
- switch (e.data.type) {
- case 'setScale': {
- updateScale(e.data.scale);
- break;
- }
- case 'setActive': {
- setActive(e.data.value);
- break;
- }
- case 'zoomIn': {
- zoomIn();
- break;
- }
- case 'zoomOut': {
- zoomOut();
- break;
- }
- case 'copyImage': {
- copyImage();
- break;
- }
- }
- });
- document.addEventListener('copy', () => {
- copyImage();
- });
- async function copyImage(retries = 5) {
- if (!document.hasFocus() && retries > 0) {
- // copyImage is called at the same time as webview.reveal, which means this function is running whilst the webview is gaining focus.
- // Since navigator.clipboard.write requires the document to be focused, we need to wait for focus.
- // 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.
- setTimeout(() => { copyImage(retries - 1); }, 20);
- return;
- }
- try {
- await navigator.clipboard.write([new ClipboardItem({
- 'image/png': new Promise((resolve, reject) => {
- const canvas = document.createElement('canvas');
- canvas.width = image.naturalWidth;
- canvas.height = image.naturalHeight;
- canvas.getContext('2d').drawImage(image, 0, 0);
- canvas.toBlob((blob) => {
- resolve(blob);
- canvas.remove();
- }, 'image/png');
- })
- })]);
- } catch (e) {
- console.error(e);
- }
- }
- }());
|