提交 dea44c8f 编写于 作者: M Matt Bierner 提交者: GitHub

Preserve Webview Scroll Position (#26426)

* Preserve Webview Scroll Position

Fixes #22995

**Bug**
If you switch away from an editor that users a webview, the scroll position is currently not preserved. This effects our release notes and the markdown preview. The root cause is that the webview is disposed of when the view is hidden.

**Fix**
Add some presisted state to track scrollProgress through the webview. Use this state in the standard html editor and in the release notes.

* Use view state

* Continue prototype memento based approach

* Preserve Webview Scroll Position

Fixes #22995

**Bug**
If you switch away from an editor that users a webview, the scroll position is currently not preserved. This effects our release notes and the markdown preview. The root cause is that the webview is disposed of when the view is hidden.

**Fix**
Add some presisted state to track scrollProgress through the webview. Use this state in the standard html editor and in the release notes.

* Revert changes to ReleaseNotesInput
上级 b8521876
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { BaseEditor } from "vs/workbench/browser/parts/editor/baseEditor";
import URI from "vs/base/common/uri";
import { IStorageService } from "vs/platform/storage/common/storage";
import { Scope } from "vs/workbench/common/memento";
export interface HtmlPreviewEditorViewState {
scrollYPercentage: number;
}
interface HtmlPreviewEditorViewStates {
0?: HtmlPreviewEditorViewState;
1?: HtmlPreviewEditorViewState;
2?: HtmlPreviewEditorViewState;
}
/**
* This class is only intended to be subclassed and not instantiated.
*/
export abstract class WebviewEditor extends BaseEditor {
constructor(
id: string,
telemetryService: ITelemetryService,
themeService: IThemeService,
private storageService: IStorageService
) {
super(id, telemetryService, themeService);
}
private get viewStateStorageKey(): string {
return this.getId() + '.editorViewState';
}
protected saveViewState(resource: URI | string, editorViewState: HtmlPreviewEditorViewState): void {
const memento = this.getMemento(this.storageService, Scope.WORKSPACE);
let editorViewStateMemento = memento[this.viewStateStorageKey];
if (!editorViewStateMemento) {
editorViewStateMemento = Object.create(null);
memento[this.viewStateStorageKey] = editorViewStateMemento;
}
let fileViewState: HtmlPreviewEditorViewStates = editorViewStateMemento[resource.toString()];
if (!fileViewState) {
fileViewState = Object.create(null);
editorViewStateMemento[resource.toString()] = fileViewState;
}
if (typeof this.position === 'number') {
fileViewState[this.position] = editorViewState;
}
}
protected loadViewState(resource: URI | string): HtmlPreviewEditorViewState | null {
const memento = this.getMemento(this.storageService, Scope.WORKSPACE);
const editorViewStateMemento = memento[this.viewStateStorageKey];
if (editorViewStateMemento) {
const fileViewState: HtmlPreviewEditorViewStates = editorViewStateMemento[resource.toString()];
if (fileViewState) {
return fileViewState[this.position];
}
}
return null;
}
}
\ No newline at end of file
......@@ -12,7 +12,6 @@ import { IModel } from 'vs/editor/common/editorCommon';
import { Dimension, Builder } from 'vs/base/browser/builder';
import { empty as EmptyDisposable, IDisposable, dispose, IReference } from 'vs/base/common/lifecycle';
import { EditorOptions, EditorInput } from 'vs/workbench/common/editor';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { Position } from 'vs/platform/editor/common/editor';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
......@@ -24,12 +23,13 @@ import { ITextModelResolverService, ITextEditorModel } from 'vs/editor/common/se
import { Parts, IPartService } from 'vs/workbench/services/part/common/partService';
import Webview from './webview';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { WebviewEditor } from 'vs/workbench/browser/parts/editor/webviewEditor';
/**
* An implementation of editor for showing HTML content in an IFrame by leveraging the HTML input.
*/
export class HtmlPreviewPart extends BaseEditor {
export class HtmlPreviewPart extends WebviewEditor {
static ID: string = 'workbench.editor.htmlPreviewPart';
......@@ -46,15 +46,18 @@ export class HtmlPreviewPart extends BaseEditor {
private _modelChangeSubscription = EmptyDisposable;
private _themeChangeSubscription = EmptyDisposable;
private scrollYPercentage: number = 0;
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@ITextModelResolverService textModelResolverService: ITextModelResolverService,
@IThemeService protected themeService: IThemeService,
@IOpenerService openerService: IOpenerService,
@IWorkspaceContextService contextService: IWorkspaceContextService,
@IPartService private partService: IPartService
@IPartService private partService: IPartService,
@IStorageService storageService: IStorageService
) {
super(HtmlPreviewPart.ID, telemetryService, themeService);
super(HtmlPreviewPart.ID, telemetryService, themeService, storageService);
this._textModelResolverService = textModelResolverService;
this._openerService = openerService;
......@@ -84,11 +87,19 @@ export class HtmlPreviewPart extends BaseEditor {
if (!this._webview) {
this._webview = new Webview(this._container, this.partService.getContainer(Parts.EDITOR_PART));
this._webview.baseUrl = this._baseUrl && this._baseUrl.toString(true);
if (this.input && this.input instanceof HtmlInput) {
const state = this.loadViewState(this.input.getResource());
this.scrollYPercentage = state ? state.scrollYPercentage : 0;
this.webview.initialScrollProgress = this.scrollYPercentage;
}
this._webviewDisposables = [
this._webview,
this._webview.onDidClickLink(uri => this._openerService.open(uri)),
this._webview.onDidLoadContent(data => this.telemetryService.publicLog('previewHtml', data.stats))
this._webview.onDidLoadContent(data => this.telemetryService.publicLog('previewHtml', data.stats)),
this._webview.onDidScroll(data => {
this.scrollYPercentage = data.scrollYPercentage;
})
];
}
return this._webview;
......@@ -143,11 +154,25 @@ export class HtmlPreviewPart extends BaseEditor {
}
public clearInput(): void {
if (this.input instanceof HtmlInput) {
this.saveViewState(this.input.getResource(), {
scrollYPercentage: this.scrollYPercentage
});
}
dispose(this._modelRef);
this._modelRef = undefined;
super.clearInput();
}
public shutdown(): void {
if (this.input instanceof HtmlInput) {
this.saveViewState(this.input.getResource(), {
scrollYPercentage: this.scrollYPercentage
});
}
super.shutdown();
}
public sendMessage(data: any): void {
this.webview.sendMessage(data);
}
......@@ -158,6 +183,12 @@ export class HtmlPreviewPart extends BaseEditor {
return TPromise.as(undefined);
}
if (this.input instanceof HtmlInput) {
this.saveViewState(this.input.getResource(), {
scrollYPercentage: this.scrollYPercentage
});
}
if (this._modelRef) {
this._modelRef.dispose();
}
......@@ -182,12 +213,15 @@ export class HtmlPreviewPart extends BaseEditor {
this._modelChangeSubscription = this.model.onDidChangeContent(() => {
if (this.model) {
this.scrollYPercentage = 0;
this.webview.contents = this.model.getLinesContent();
}
});
const state = this.loadViewState(resourceUri);
this.scrollYPercentage = state ? state.scrollYPercentage : 0;
this.webview.baseUrl = resourceUri.toString(true);
this.webview.contents = this.model.getLinesContent();
this.webview.initialScrollProgress = this.scrollYPercentage;
return undefined;
});
});
......
......@@ -9,7 +9,9 @@
const ipcRenderer = require('electron').ipcRenderer;
const initData = {};
const initData = {
initialScrollProgress: undefined
};
function styleBody(body) {
if (!body) {
......@@ -26,10 +28,14 @@
return document.getElementById('_target');
}
/**
* @param {MouseEvent} event
*/
function handleInnerClick(event) {
if (!event || !event.view || !event.view.document) {
return;
}
/** @type {any} */
var node = event.target;
while (node) {
if (node.tagName === "A" && node.href) {
......@@ -51,6 +57,27 @@
}
}
var isHandlingScroll = false;
function handleInnerScroll(event) {
if (isHandlingScroll) {
return;
}
const progress = event.target.body.scrollTop / event.target.body.clientHeight;
if (isNaN(progress)) {
return;
}
isHandlingScroll = true;
window.requestAnimationFrame(function () {
try {
ipcRenderer.sendToHost('did-scroll', progress);
} catch (e) {
// noop
}
isHandlingScroll = false;
});
}
document.addEventListener("DOMContentLoaded", function (event) {
ipcRenderer.on('baseUrl', function (event, value) {
......@@ -123,7 +150,23 @@
}
// keep current scrollTop around and use later
const scrollTop = frame && frame.contentDocument && frame.contentDocument.body ? frame.contentDocument.body.scrollTop : 0;
let setInitialScrollPosition;
if (frame) {
const scrollY = frame.contentDocument && frame.contentDocument.body ? frame.contentDocument.body.scrollTop : 0;
setInitialScrollPosition = function (body) {
body.scrollTop = scrollY;
}
} else {
// First load
setInitialScrollPosition = function (body, window) {
body.scrollTop = 0;
if (!isNaN(initData.initialScrollProgress)) {
window.addEventListener('load', function() {
body.scrollTop = body.clientHeight * initData.initialScrollProgress
});
}
}
}
const newFrame = document.createElement('iframe');
newFrame.setAttribute('id', '_target');
......@@ -140,17 +183,12 @@
};
newFrame.contentWindow.addEventListener('DOMContentLoaded', function (e) {
/**
* @type {any}
*/
/** @type {any} */
const contentDocument = e.target;
if (contentDocument.body) {
// Workaround for https://github.com/Microsoft/vscode/issues/12865
// check new scrollTop and reset if neccessary
if (scrollTop !== contentDocument.body.scrollTop) {
contentDocument.body.scrollTop = scrollTop;
}
setInitialScrollPosition(contentDocument.body, this);
// Bubble out link clicks
contentDocument.body.addEventListener('click', handleInnerClick);
......@@ -166,6 +204,7 @@
const newFrame = getTarget();
if (newFrame.contentDocument === contentDocument) {
newFrame.style.display = 'block';
this.addEventListener('scroll', handleInnerScroll);
}
});
......@@ -186,6 +225,10 @@
}
});
ipcRenderer.on('initial-scroll-position', function (event, progress) {
initData.initialScrollProgress = progress;
});
// forward messages from the embedded iframe
window.onmessage = function (message) {
ipcRenderer.sendToHost(message.data.command, message.data.data);
......
......@@ -52,6 +52,8 @@ export default class Webview {
private _onDidClickLink = new Emitter<URI>();
private _onDidLoadContent = new Emitter<{ stats: any }>();
private _onDidScroll = new Emitter<{ scrollYPercentage: number }>();
constructor(
private parent: HTMLElement,
private _styleElement: Element
......@@ -108,6 +110,13 @@ export default class Webview {
this.layout();
return;
}
if (event.channel === 'did-scroll') {
if (event.args && typeof event.args[0] === 'number') {
this._onDidScroll.fire({ scrollYPercentage: event.args[0] });
}
return;
}
})
];
......@@ -134,12 +143,20 @@ export default class Webview {
return this._onDidLoadContent.event;
}
get onDidScroll(): Event<{ scrollYPercentage: number }> {
return this._onDidScroll.event;
}
private _send(channel: string, ...args: any[]): void {
this._ready
.then(() => this._webview.send(channel, ...args))
.done(void 0, console.error);
}
set initialScrollProgress(value: number) {
this._send('initial-scroll-position', value);
}
set contents(value: string[]) {
this._send('content', value);
}
......
......@@ -7,5 +7,5 @@
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
export class HtmlInput extends ResourceEditorInput {
// just a marker class
// marker class
}
......@@ -10,7 +10,6 @@ import { marked } from 'vs/base/common/marked/marked';
import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
import { Builder } from 'vs/base/browser/builder';
import { append, $ } from 'vs/base/browser/dom';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { ReleaseNotesInput } from './releaseNotesInput';
......@@ -20,6 +19,8 @@ import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IModeService } from 'vs/editor/common/services/modeService';
import { tokenizeToString } from 'vs/editor/common/modes/textToHtmlTokenizer';
import { IPartService, Parts } from 'vs/workbench/services/part/common/partService';
import { WebviewEditor } from 'vs/workbench/browser/parts/editor/webviewEditor';
import { IStorageService } from "vs/platform/storage/common/storage";
function renderBody(body: string): string {
return `<!DOCTYPE html>
......@@ -33,7 +34,7 @@ function renderBody(body: string): string {
</html>`;
}
export class ReleaseNotesEditor extends BaseEditor {
export class ReleaseNotesEditor extends WebviewEditor {
static ID: string = 'workbench.editor.releaseNotes';
......@@ -41,15 +42,17 @@ export class ReleaseNotesEditor extends BaseEditor {
private webview: WebView;
private contentDisposables: IDisposable[] = [];
private scrollYPercentage: number = 0;
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@IThemeService protected themeService: IThemeService,
@IOpenerService private openerService: IOpenerService,
@IModeService private modeService: IModeService,
@IPartService private partService: IPartService
@IPartService private partService: IPartService,
@IStorageService storageService: IStorageService
) {
super(ReleaseNotesEditor.ID, telemetryService, themeService);
super(ReleaseNotesEditor.ID, telemetryService, themeService, storageService);
}
createEditor(parent: Builder): void {
......@@ -92,10 +95,20 @@ export class ReleaseNotesEditor extends BaseEditor {
.then<void>(body => {
this.webview = new WebView(this.content, this.partService.getContainer(Parts.EDITOR_PART));
this.webview.baseUrl = `https://code.visualstudio.com/raw/`;
if (this.input && this.input instanceof ReleaseNotesInput) {
const state = this.loadViewState(this.input.version);
if (state) {
this.webview.initialScrollProgress = state.scrollYPercentage;
}
}
this.webview.style(this.themeService.getTheme());
this.webview.contents = [body];
this.webview.onDidClickLink(link => this.openerService.open(link), null, this.contentDisposables);
this.webview.onDidScroll(event => {
this.scrollYPercentage = event.scrollYPercentage;
}, null, this.contentDisposables);
this.themeService.onThemeChange(themeId => this.webview.style(themeId), null, this.contentDisposables);
this.contentDisposables.push(this.webview);
this.contentDisposables.push(toDisposable(() => this.webview = null));
......@@ -120,4 +133,28 @@ export class ReleaseNotesEditor extends BaseEditor {
this.contentDisposables = dispose(this.contentDisposables);
super.dispose();
}
protected getViewState() {
return {
scrollYPercentage: this.scrollYPercentage
};
}
public clearInput(): void {
if (this.input instanceof ReleaseNotesInput) {
this.saveViewState(this.input.version, {
scrollYPercentage: this.scrollYPercentage
});
}
super.clearInput();
}
public shutdown(): void {
if (this.input instanceof ReleaseNotesInput) {
this.saveViewState(this.input.version, {
scrollYPercentage: this.scrollYPercentage
});
}
super.shutdown();
}
}
......@@ -13,6 +13,7 @@ export class ReleaseNotesInput extends EditorInput {
static get ID() { return 'workbench.releaseNotes.input'; }
get version(): string { return this._version; }
get text(): string { return this._text; }
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册