Change:
<!-- Do Not Delete This! featurerequesttemplate --<!-- Please read our Rules of Conduct: --<!-- Please search existing issues to avoid creating duplicates. --<!-- Describe the feature you'd like. --Python Notebook implementation has special code to handle click events of links containing an ObjectUrl (href=blob:....). I'm hoping VS Code would handle this natively as these links allow users to save blobs as files. This is generic enough that it need not be something special to the Python implementation. Would benefit anyone using the new VSC API to display HTML that allows saving images. Current Python Notebook implementation: We handle all click events in document If target element === and starts with . Then we get the blob contents and send it to extension On extension side we prompt user to save the file using . /cc
what is the use case for this?
Sure, a user could render a plotly plot and such plots can contain links to save images, and those hrefs have a ref to object urls. This isn't specific to Python extension, any HTML could have markup that allows users to save images . Also, opening such a plot in other notebook viewer, user is able to click such links and save the images. If this isn't address, then VSCode displays an empty page in notebook with an error message (see below). I think this is due to the fact that href results in nativigating the current page to url=blob://. & again, we can address this with a custom renderer, however I believe this is generic enough for VSC to handle. ! !
TIL about the API. Should this also work for URLs?
Good pick. I would say yes. At the end of the day someone could render a link with a download attribute
To do this for blob ULIs, I have to download the contents of the URL in the context of the webview, and pipe it back to vscode to write to a file. Data URLs are easier, I can just open it in a browser. Maybe an experience in vscode would be nicer though. Where are these urls coming from in your case, some 3rd party renderer code or your extension code?
3rd party That's exactly what we have in our python extension when using the webview. I thought this would be generic enough for VSCode to handle, as others could end up with similar scenarios, at the end of the day it's from a 3rd party package.
Yeah, I think it makes sense for us to handle this as well.
Do you have an example of a notebook that has this button? Is this from ipywidgets?
Also can you link me to the code that handles this in your extension?
No, its plot generated by plotly. Here's the code required to generate such a plot:
I thought this was working on Wednesday, but now I get a CORS error when I fetch any blob URL from the webview code. Did you do anything to handle this? I don't understand why it's working in your webview and not mine. Maybe we are making webview changes right now that might affect this.
U might have to setup the csp (for data and blobs), this here's what we have:

Discussion:
const ext = extname(path); return mapExtToMediaMimes[ext.toLowerCase()]; } <ins> export function getExtensionForMimeType(mimeType: string): string | undefined { for (const extension in mapExtToMediaMimes) { if (mapExtToMediaMimes[extension] === mimeType) { return extension; } } return undefined; } </ins>

import { IWebviewService, WebviewElement } from 'vs/workbench/contrib/webview/browser/webview'; import { asWebviewUri } from 'vs/workbench/contrib/webview/common/webviewUri'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; <del> import { dirname } from 'vs/base/common/resources'; </del> <ins> import { dirname, joinPath } from 'vs/base/common/resources'; </ins> import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { Schemas } from 'vs/base/common/network'; <ins> import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IFileService } from 'vs/platform/files/common/files'; import { VSBuffer } from 'vs/base/common/buffer'; import { getExtensionForMimeType } from 'vs/base/common/mime'; </ins> export interface WebviewIntialized { __vscode_notebook_message: boolean;

focusNext?: boolean; } <ins> export interface IClickedDataUrlMessage { __vscode_notebook_message: string; type: 'clicked-data-url'; data: string; downloadName?: string; } </ins> export interface IClearMessage { type: 'clear'; }

return str; } <del> type IMessage = IDimensionMessage | IScrollAckMessage | IWheelMessage | IMouseEnterMessage | IMouseLeaveMessage | IBlurOutputMessage | WebviewIntialized; </del> <ins> type IMessage = IDimensionMessage | IScrollAckMessage | IWheelMessage | IMouseEnterMessage | IMouseLeaveMessage | IBlurOutputMessage | WebviewIntialized | IClickedDataUrlMessage; </ins> let version = 0; export class BackLayerWebView extends Disposable {

@IEnvironmentService private readonly environmentService: IEnvironmentService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, <ins> @IFileDialogService private readonly fileDialogService: IFileDialogService, @IFileService private readonly fileService: IFileService, </ins> ) { super(); this.element = document.createElement('div');

<div id='container' class="widgetarea" style="position: absolute;width:100%;top: 0px"></div> <script> (function () { <ins> const handleInnerClick = (event) => { if (!event || !event.view || !event.view.document) { return; } /** @type {any} */ let node = event.target; while (node) { if (node.tagName && node.tagName.toLowerCase() === 'a' && node.href) { if (node.href.startsWith('blob:')) { handleBlobUrlClick(node.href, node.download); } event.preventDefault(); break; } node = node.parentNode; } }; const handleBlobUrlClick = async (url, downloadName) => { try { const response = await fetch(url); const blob = await response.blob(); const reader = new FileReader(); reader.addEventListener('load', () => { const data = reader.result; vscode.postMessage({ __vscode_notebook_message: true, type: 'clicked-data-url', data, downloadName }); }); reader.readAsDataURL(blob); } catch (e) { console.error(e.message); } }; document.body.addEventListener('click', handleInnerClick); </ins> // eslint-disable-next-line no-undef const vscode = acquireVsCodeApi();

this.notebookEditor.focusNotebookCell(info.cell, 'editor'); } } <ins> } else if (data.type === 'clicked-data-url') { this._onDidClickDataLink(data); </ins> } return; }

})); } <ins> private async _onDidClickDataLink(event: IClickedDataUrlMessage): Promise<void> { const [splitStart, splitData] = event.data.split(';base64,'); if (!splitData || !splitStart) { return; } const defaultDir = dirname(this.documentUri); let defaultName: string; if (event.downloadName) { defaultName = event.downloadName; } else { const mimeType = splitStart.replace(/^data:/, ''); const candidateExtension = mimeType && getExtensionForMimeType(mimeType); defaultName = candidateExtension ? `download${candidateExtension}` : 'download'; } const defaultUri = joinPath(defaultDir, defaultName); const newFileUri = await this.fileDialogService.showSaveDialog({ defaultUri }); if (!newFileUri) { return; } const decoded = atob(splitData); const typedArray = new Uint8Array(decoded.length); for (let i = 0; i < decoded.length; i++) { typedArray[i] = decoded.charCodeAt(i); } const buff = VSBuffer.wrap(typedArray); await this.fileService.writeFile(newFileUri, buff); await this.openerService.open(newFileUri); } </ins> async waitForInitialization() { await this._initalized; }

import { addClass } from 'vs/base/browser/dom'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { Emitter } from 'vs/base/common/event'; <del> import { URI } from 'vs/base/common/uri'; </del> import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; <ins> import { URI } from 'vs/base/common/uri'; </ins> import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; <del> import { WebviewExtensionDescription, WebviewOptions, WebviewContentOptions } from 'vs/workbench/contrib/webview/browser/webview'; </del> <ins> import { WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; </ins> import { areWebviewInputOptionsEqual } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/common/themeing'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';

readonly id: ExtensionIdentifier; } <ins> export interface IDataLinkClickEvent { dataURL: string; downloadName?: string; } </ins> export interface Webview extends IDisposable { html: string; contentOptions: WebviewContentOptions;