Path: blob/main/extensions/media-preview/media/imagePreview.js
3292 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/4// @ts-check5"use strict";67(function () {8/**9* @param {number} value10* @param {number} min11* @param {number} max12* @return {number}13*/14function clamp(value, min, max) {15return Math.min(Math.max(value, min), max);16}1718function getSettings() {19const element = document.getElementById('image-preview-settings');20if (element) {21const data = element.getAttribute('data-settings');22if (data) {23return JSON.parse(data);24}25}2627throw new Error(`Could not load settings`);28}2930/**31* Enable image-rendering: pixelated for images scaled by more than this.32*/33const PIXELATION_THRESHOLD = 3;3435const SCALE_PINCH_FACTOR = 0.075;36const MAX_SCALE = 20;37const MIN_SCALE = 0.1;3839const zoomLevels = [400.1,410.2,420.3,430.4,440.5,450.6,460.7,470.8,480.9,491,501.5,512,523,535,547,5510,5615,572058];5960const settings = getSettings();61const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;6263// @ts-ignore64const vscode = acquireVsCodeApi();6566const initialState = vscode.getState() || { scale: 'fit', offsetX: 0, offsetY: 0 };6768// State69let scale = initialState.scale;70let ctrlPressed = false;71let altPressed = false;72let hasLoadedImage = false;73let consumeClick = true;74let isActive = false;7576// Elements77const container = document.body;78const image = document.createElement('img');7980function updateScale(newScale) {81if (!image || !hasLoadedImage || !image.parentElement) {82return;83}8485if (newScale === 'fit') {86scale = 'fit';87image.classList.add('scale-to-fit');88image.classList.remove('pixelated');89// @ts-ignore Non-standard CSS property90image.style.zoom = 'normal';91vscode.setState(undefined);92} else {93scale = clamp(newScale, MIN_SCALE, MAX_SCALE);94if (scale >= PIXELATION_THRESHOLD) {95image.classList.add('pixelated');96} else {97image.classList.remove('pixelated');98}99100const dx = (window.scrollX + container.clientWidth / 2) / container.scrollWidth;101const dy = (window.scrollY + container.clientHeight / 2) / container.scrollHeight;102103image.classList.remove('scale-to-fit');104// @ts-ignore Non-standard CSS property105image.style.zoom = scale;106107const newScrollX = container.scrollWidth * dx - container.clientWidth / 2;108const newScrollY = container.scrollHeight * dy - container.clientHeight / 2;109110window.scrollTo(newScrollX, newScrollY);111112vscode.setState({ scale: scale, offsetX: newScrollX, offsetY: newScrollY });113}114115vscode.postMessage({116type: 'zoom',117value: scale118});119}120121function setActive(value) {122isActive = value;123if (value) {124if (isMac ? altPressed : ctrlPressed) {125container.classList.remove('zoom-in');126container.classList.add('zoom-out');127} else {128container.classList.remove('zoom-out');129container.classList.add('zoom-in');130}131} else {132ctrlPressed = false;133altPressed = false;134container.classList.remove('zoom-out');135container.classList.remove('zoom-in');136}137}138139function firstZoom() {140if (!image || !hasLoadedImage) {141return;142}143144scale = image.clientWidth / image.naturalWidth;145updateScale(scale);146}147148function zoomIn() {149if (scale === 'fit') {150firstZoom();151}152153let i = 0;154for (; i < zoomLevels.length; ++i) {155if (zoomLevels[i] > scale) {156break;157}158}159updateScale(zoomLevels[i] || MAX_SCALE);160}161162function zoomOut() {163if (scale === 'fit') {164firstZoom();165}166167let i = zoomLevels.length - 1;168for (; i >= 0; --i) {169if (zoomLevels[i] < scale) {170break;171}172}173updateScale(zoomLevels[i] || MIN_SCALE);174}175176window.addEventListener('keydown', (/** @type {KeyboardEvent} */ e) => {177if (!image || !hasLoadedImage) {178return;179}180ctrlPressed = e.ctrlKey;181altPressed = e.altKey;182183if (isMac ? altPressed : ctrlPressed) {184container.classList.remove('zoom-in');185container.classList.add('zoom-out');186}187});188189window.addEventListener('keyup', (/** @type {KeyboardEvent} */ e) => {190if (!image || !hasLoadedImage) {191return;192}193194ctrlPressed = e.ctrlKey;195altPressed = e.altKey;196197if (!(isMac ? altPressed : ctrlPressed)) {198container.classList.remove('zoom-out');199container.classList.add('zoom-in');200}201});202203container.addEventListener('mousedown', (/** @type {MouseEvent} */ e) => {204if (!image || !hasLoadedImage) {205return;206}207208if (e.button !== 0) {209return;210}211212ctrlPressed = e.ctrlKey;213altPressed = e.altKey;214215consumeClick = !isActive;216});217218container.addEventListener('click', (/** @type {MouseEvent} */ e) => {219if (!image || !hasLoadedImage) {220return;221}222223if (e.button !== 0) {224return;225}226227if (consumeClick) {228consumeClick = false;229return;230}231// left click232if (scale === 'fit') {233firstZoom();234}235236if (!(isMac ? altPressed : ctrlPressed)) { // zoom in237zoomIn();238} else {239zoomOut();240}241});242243container.addEventListener('wheel', (/** @type {WheelEvent} */ e) => {244// Prevent pinch to zoom245if (e.ctrlKey) {246e.preventDefault();247}248249if (!image || !hasLoadedImage) {250return;251}252253const isScrollWheelKeyPressed = isMac ? altPressed : ctrlPressed;254if (!isScrollWheelKeyPressed && !e.ctrlKey) { // pinching is reported as scroll wheel + ctrl255return;256}257258if (scale === 'fit') {259firstZoom();260}261262const delta = e.deltaY > 0 ? 1 : -1;263updateScale(scale * (1 - delta * SCALE_PINCH_FACTOR));264}, { passive: false });265266window.addEventListener('scroll', e => {267if (!image || !hasLoadedImage || !image.parentElement || scale === 'fit') {268return;269}270271const entry = vscode.getState();272if (entry) {273vscode.setState({ scale: entry.scale, offsetX: window.scrollX, offsetY: window.scrollY });274}275}, { passive: true });276277container.classList.add('image');278279image.classList.add('scale-to-fit');280281image.addEventListener('load', () => {282if (hasLoadedImage) {283return;284}285hasLoadedImage = true;286287vscode.postMessage({288type: 'size',289value: `${image.naturalWidth}x${image.naturalHeight}`,290});291292document.body.classList.remove('loading');293document.body.classList.add('ready');294document.body.append(image);295296updateScale(scale);297298if (initialState.scale !== 'fit') {299window.scrollTo(initialState.offsetX, initialState.offsetY);300}301});302303image.addEventListener('error', e => {304if (hasLoadedImage) {305return;306}307308hasLoadedImage = true;309document.body.classList.add('error');310document.body.classList.remove('loading');311});312313image.src = settings.src;314315document.querySelector('.open-file-link')?.addEventListener('click', (e) => {316e.preventDefault();317vscode.postMessage({318type: 'reopen-as-text',319});320});321322window.addEventListener('message', e => {323if (e.origin !== window.origin) {324console.error('Dropping message from unknown origin in image preview');325return;326}327328switch (e.data.type) {329case 'setScale': {330updateScale(e.data.scale);331break;332}333case 'setActive': {334setActive(e.data.value);335break;336}337case 'zoomIn': {338zoomIn();339break;340}341case 'zoomOut': {342zoomOut();343break;344}345case 'copyImage': {346copyImage();347break;348}349}350});351352document.addEventListener('copy', () => {353copyImage();354});355356async function copyImage(retries = 5) {357if (!document.hasFocus() && retries > 0) {358// copyImage is called at the same time as webview.reveal, which means this function is running whilst the webview is gaining focus.359// Since navigator.clipboard.write requires the document to be focused, we need to wait for focus.360// 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.361setTimeout(() => { copyImage(retries - 1); }, 20);362return;363}364365try {366await navigator.clipboard.write([new ClipboardItem({367'image/png': new Promise((resolve, reject) => {368const canvas = document.createElement('canvas');369canvas.width = image.naturalWidth;370canvas.height = image.naturalHeight;371canvas.getContext('2d').drawImage(image, 0, 0);372canvas.toBlob((blob) => {373resolve(blob);374canvas.remove();375}, 'image/png');376})377})]);378} catch (e) {379console.error(e);380}381}382}());383384385