@opensumi/ide-webview 2.21.6 → 2.21.7-next-1670485458.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@opensumi/ide-webview",
3
- "version": "2.21.6",
3
+ "version": "2.21.7-next-1670485458.0",
4
4
  "files": [
5
- "lib"
5
+ "lib",
6
+ "src"
6
7
  ],
7
8
  "license": "MIT",
8
9
  "main": "lib/index.js",
@@ -16,19 +17,19 @@
16
17
  "url": "git@github.com:opensumi/core.git"
17
18
  },
18
19
  "dependencies": {
19
- "@opensumi/ide-core-common": "2.21.6",
20
- "@opensumi/ide-core-node": "2.21.6"
20
+ "@opensumi/ide-core-common": "2.21.7-next-1670485458.0",
21
+ "@opensumi/ide-core-node": "2.21.7-next-1670485458.0"
21
22
  },
22
23
  "devDependencies": {
23
- "@opensumi/ide-core-browser": "2.21.6",
24
- "@opensumi/ide-core-electron-main": "2.21.6",
24
+ "@opensumi/ide-core-browser": "2.21.7-next-1670485458.0",
25
+ "@opensumi/ide-core-electron-main": "2.21.7-next-1670485458.0",
25
26
  "@opensumi/ide-dev-tool": "^1.3.1",
26
- "@opensumi/ide-editor": "2.21.6",
27
- "@opensumi/ide-static-resource": "2.21.6",
28
- "@opensumi/ide-theme": "2.21.6"
27
+ "@opensumi/ide-editor": "2.21.7-next-1670485458.0",
28
+ "@opensumi/ide-static-resource": "2.21.7-next-1670485458.0",
29
+ "@opensumi/ide-theme": "2.21.7-next-1670485458.0"
29
30
  },
30
31
  "peerDependencies": {
31
32
  "electron": "*"
32
33
  },
33
- "gitHead": "a34917dfa6d851e067a431ad700812e58e8cc8a8"
34
+ "gitHead": "8fb92e1814163b5e8c8c2f99ed1bc2fd68de7bd8"
34
35
  }
@@ -0,0 +1,231 @@
1
+ import { Autowired, Injectable } from '@opensumi/di';
2
+ import {
3
+ Event,
4
+ URI,
5
+ Disposable,
6
+ IDisposable,
7
+ Emitter,
8
+ IEventBus,
9
+ MaybeNull,
10
+ AppConfig,
11
+ } from '@opensumi/ide-core-browser';
12
+ import { StaticResourceService } from '@opensumi/ide-static-resource/lib/browser';
13
+ import { ITheme, IThemeService } from '@opensumi/ide-theme';
14
+ import { ThemeChangedEvent } from '@opensumi/ide-theme/lib/common/event';
15
+
16
+ import { IWebview, IWebviewContentOptions, IWebviewContentScrollPosition, IWebviewService } from './types';
17
+
18
+ @Injectable({ multiple: true })
19
+ export abstract class AbstractWebviewPanel extends Disposable implements IWebview {
20
+ protected _html = '';
21
+
22
+ protected _options: IWebviewContentOptions;
23
+
24
+ initialScrollProgress: number;
25
+
26
+ state: any;
27
+
28
+ _onDidFocus: Emitter<void> = new Emitter<void>();
29
+ onDidFocus: Event<void> = this._onDidFocus.event;
30
+
31
+ _onDidBlur: Emitter<void> = new Emitter<void>();
32
+ onDidBlur: Event<void> = this._onDidFocus.event;
33
+
34
+ _onDidClickLink: Emitter<URI> = new Emitter<URI>();
35
+ onDidClickLink: Event<URI> = this._onDidClickLink.event;
36
+
37
+ _onDidScroll: Emitter<IWebviewContentScrollPosition> = new Emitter<IWebviewContentScrollPosition>();
38
+ onDidScroll: Event<IWebviewContentScrollPosition> = this._onDidScroll.event;
39
+
40
+ _onDidUpdateState: Emitter<any> = new Emitter<any>();
41
+ onDidUpdateState: Event<any> = this._onDidUpdateState.event;
42
+
43
+ _onRemove: Emitter<void> = new Emitter<void>();
44
+ onRemove: Event<void> = this._onRemove.event;
45
+
46
+ protected _isListening = true;
47
+
48
+ private _focused = false;
49
+
50
+ protected _ready: Promise<void>;
51
+
52
+ @Autowired(IWebviewService)
53
+ webviewService: IWebviewService;
54
+
55
+ @Autowired(IThemeService)
56
+ themeService: IThemeService;
57
+
58
+ @Autowired(IEventBus)
59
+ eventBus: IEventBus;
60
+
61
+ @Autowired(AppConfig)
62
+ private readonly appConfig: AppConfig;
63
+
64
+ @Autowired(StaticResourceService)
65
+ staticResourceService: StaticResourceService;
66
+
67
+ protected _keybindingDomTarget: HTMLElement | undefined = undefined;
68
+
69
+ public setKeybindingDomTarget(target) {
70
+ this._keybindingDomTarget = target;
71
+ }
72
+
73
+ constructor(public readonly id: string, options: IWebviewContentOptions = {}) {
74
+ super();
75
+ this._options = options;
76
+ }
77
+
78
+ init() {
79
+ this.prepareContainer();
80
+
81
+ this.initEvents();
82
+ }
83
+
84
+ async postMessage(message: any): Promise<void> {
85
+ return this._sendToWebview('message', message);
86
+ }
87
+
88
+ public get options() {
89
+ return this._options;
90
+ }
91
+
92
+ onMessage(listener: (message: any) => any): IDisposable {
93
+ return this._onWebviewMessage('onmessage', listener);
94
+ }
95
+
96
+ protected initEvents() {
97
+ this._onWebviewMessage('did-click-link', (data) => {
98
+ this._onDidClickLink.fire(new URI(data));
99
+ });
100
+
101
+ this._onWebviewMessage('did-scroll', (data) => {
102
+ this._onDidScroll.fire(data);
103
+ });
104
+
105
+ this._onWebviewMessage('do-reload', () => {
106
+ this.doUpdateContent();
107
+ });
108
+
109
+ this._onWebviewMessage('load-resource', () => {
110
+ // TODO: 资源相关
111
+ });
112
+
113
+ this._onWebviewMessage('load-localhost', () => {
114
+ // TODO: 好像是消息转发
115
+ });
116
+
117
+ this._onWebviewMessage('did-focus', () => {
118
+ this.handleFocusChange(true);
119
+ });
120
+
121
+ this._onWebviewMessage('did-blur', () => {
122
+ this.handleFocusChange(false);
123
+ });
124
+
125
+ this._onWebviewMessage('do-update-state', (state) => {
126
+ this.state = state;
127
+ this._onDidUpdateState.fire(state);
128
+ });
129
+
130
+ this._onWebviewMessage('did-keydown', (event) => {
131
+ // Create a fake KeyboardEvent from the data provided
132
+ const emulatedKeyboardEvent = new KeyboardEvent('keydown', event);
133
+ // Force override the target
134
+ Object.defineProperty(emulatedKeyboardEvent, 'target', {
135
+ get: () => this._keybindingDomTarget || this.getDomNode(),
136
+ });
137
+ // And re-dispatch
138
+ window.dispatchEvent(emulatedKeyboardEvent);
139
+ });
140
+
141
+ this.updateStyle();
142
+ }
143
+
144
+ protected updateStyle() {
145
+ this.style(this.themeService.getCurrentThemeSync());
146
+ this.addDispose(
147
+ this.eventBus.on(ThemeChangedEvent, (e) => {
148
+ this.style(e.payload.theme);
149
+ }),
150
+ );
151
+ }
152
+
153
+ getContent(): string {
154
+ return this._html;
155
+ }
156
+
157
+ async setContent(html: string): Promise<void> {
158
+ this._html = html;
159
+ await this.doUpdateContent();
160
+ }
161
+
162
+ protected preprocessHtml(html: string): string {
163
+ if (this.appConfig.isElectronRenderer) {
164
+ // 将vscode-resource:/User/xxx 转换为 vscode-resource:///User/xxx
165
+ return html.replace(
166
+ /(["'])vscode-resource:(\/\/|)([^\s'"]+?)(["'])/gi,
167
+ (_, startQuote, slash, path, endQuote) => `${startQuote}vscode-resource://${path}${endQuote}`,
168
+ );
169
+ }
170
+ return html.replace(
171
+ /(["'])vscode-resource:([^\s'"]+?)(["'])/gi,
172
+ (_, startQuote, path, endQuote) =>
173
+ `${startQuote}${this.staticResourceService.resolveStaticResource(URI.file(path))}${endQuote}`,
174
+ );
175
+ }
176
+
177
+ protected doUpdateContent() {
178
+ return this._sendToWebview('content', {
179
+ contents: this.preprocessHtml(this._html),
180
+ options: this._options,
181
+ state: this.state,
182
+ });
183
+ }
184
+
185
+ public abstract appendTo(container: HTMLElement);
186
+
187
+ protected abstract _sendToWebview(channel: string, data: any);
188
+
189
+ protected abstract _onWebviewMessage(channel: string, listener: (data: any) => any): IDisposable;
190
+
191
+ updateOptions(options: IWebviewContentOptions): void {
192
+ this._options = Object.assign(this._options, options);
193
+ this.doUpdateContent();
194
+ }
195
+
196
+ layout(): void {
197
+ throw new Error('Method not implemented.');
198
+ }
199
+
200
+ focus(): void {
201
+ throw new Error('Method not implemented.');
202
+ }
203
+
204
+ reload(): void {
205
+ throw new Error('Method not implemented.');
206
+ }
207
+
208
+ protected handleFocusChange(isFocused: boolean): void {
209
+ this._focused = isFocused;
210
+ if (this._focused) {
211
+ this._onDidFocus.fire();
212
+ } else {
213
+ this._onDidBlur.fire();
214
+ }
215
+ }
216
+
217
+ private style(theme: ITheme): void {
218
+ const { styles, activeTheme } = this.webviewService.getWebviewThemeData(theme);
219
+ this._sendToWebview('styles', { styles, activeTheme });
220
+ }
221
+
222
+ setListenMessages(listening: boolean): void {
223
+ this._isListening = listening;
224
+ }
225
+
226
+ abstract prepareContainer(): any;
227
+
228
+ abstract getDomNode(): MaybeNull<HTMLElement>;
229
+
230
+ abstract remove(): void;
231
+ }
@@ -0,0 +1,80 @@
1
+ /* istanbul ignore file */
2
+ import { Autowired } from '@opensumi/di';
3
+ import { Domain, URI, CommandContribution, CommandRegistry, AppConfig } from '@opensumi/ide-core-browser';
4
+ import { localize } from '@opensumi/ide-core-common';
5
+ import { ResourceService, IResource } from '@opensumi/ide-editor';
6
+ import { BrowserEditorContribution, EditorComponentRegistry } from '@opensumi/ide-editor/lib/browser';
7
+
8
+ import { EDITOR_WEBVIEW_SCHEME, IWebviewService, IEditorWebviewMetaData, isWebview } from './types';
9
+ import { WebviewServiceImpl } from './webview.service';
10
+
11
+ const WEBVIEW_DEVTOOLS_COMMAND = {
12
+ id: 'workbench.action.webview.openDeveloperTools',
13
+ label: localize('openToolsLabel', 'Open Webview Developer Tools'),
14
+ };
15
+
16
+ @Domain(BrowserEditorContribution, CommandContribution)
17
+ export class WebviewModuleContribution implements BrowserEditorContribution, CommandContribution {
18
+ @Autowired(IWebviewService)
19
+ webviewService: WebviewServiceImpl;
20
+
21
+ @Autowired(EditorComponentRegistry)
22
+ editorComponentRegistry: EditorComponentRegistry;
23
+
24
+ @Autowired(AppConfig)
25
+ private readonly appConfig: AppConfig;
26
+
27
+ registerResource(resourceService: ResourceService) {
28
+ resourceService.registerResourceProvider({
29
+ scheme: EDITOR_WEBVIEW_SCHEME,
30
+ provideResource: async (uri: URI): Promise<IResource<IEditorWebviewMetaData>> => {
31
+ const existingComponent = this.webviewService.editorWebviewComponents.get(uri.path.toString());
32
+ if (existingComponent) {
33
+ return existingComponent.resource;
34
+ } else {
35
+ // try revive, 如果无法恢复,会抛错
36
+ await this.webviewService.tryRestoredWebviewComponent(uri.path.toString());
37
+ return this.webviewService.editorWebviewComponents.get(uri.path.toString())!.resource;
38
+ }
39
+ },
40
+ shouldCloseResource: (resource: IResource<IEditorWebviewMetaData>, openedResources: IResource[][]) => {
41
+ let count = 0;
42
+ for (const resources of openedResources) {
43
+ for (const r of resources) {
44
+ if (r.uri.scheme === EDITOR_WEBVIEW_SCHEME && r.uri.toString() === resource.uri.toString()) {
45
+ count++;
46
+ }
47
+ if (count > 1) {
48
+ return true;
49
+ }
50
+ }
51
+ }
52
+ const component = this.webviewService.editorWebviewComponents.get(resource.uri.path.toString());
53
+ if (component?.webview && isWebview(component.webview)) {
54
+ // 只对类 vscode webview 进行 dispose,
55
+ // loadUrl 的 plainWebview 必须手动 dispose
56
+ this.webviewService.editorWebviewComponents.get(resource.uri.path.toString())!.clear();
57
+ }
58
+
59
+ return true;
60
+ },
61
+ });
62
+ }
63
+
64
+ registerCommands(commandRegistry: CommandRegistry) {
65
+ commandRegistry.registerCommand(WEBVIEW_DEVTOOLS_COMMAND, {
66
+ execute: () => {
67
+ const elements = document.querySelectorAll<Electron.WebviewTag>('webview');
68
+ // eslint-disable-next-line @typescript-eslint/prefer-for-of
69
+ for (let i = 0; i < elements.length; i += 1) {
70
+ try {
71
+ elements[i].openDevTools();
72
+ } catch (e) {
73
+ // noop
74
+ }
75
+ }
76
+ },
77
+ isEnabled: () => this.appConfig.isElectronRenderer,
78
+ });
79
+ }
80
+ }
@@ -0,0 +1,207 @@
1
+ import React from 'react';
2
+
3
+ import { Disposable, DomListener, useInjectable } from '@opensumi/ide-core-browser';
4
+ import { ReactEditorComponent } from '@opensumi/ide-editor/lib/browser';
5
+
6
+ import { IWebview, IPlainWebview, IEditorWebviewMetaData, IWebviewService, isWebview } from './types';
7
+ import { WebviewServiceImpl } from './webview.service';
8
+
9
+ declare const ResizeObserver: any;
10
+ declare const MutationObserver: any;
11
+
12
+ export const EditorWebviewComponentView: ReactEditorComponent<IEditorWebviewMetaData> = ({ resource }) => {
13
+ const webviewService = useInjectable(IWebviewService) as WebviewServiceImpl;
14
+ const webview = webviewService.editorWebviewComponents.get(resource.metadata!.id)?.webview;
15
+ let container: HTMLDivElement | null = null;
16
+
17
+ React.useEffect(() => {
18
+ if (webview && container) {
19
+ const mounter = new WebviewMounter(
20
+ webview,
21
+ container,
22
+ document.getElementById('workbench-editor')!,
23
+ document.getElementById('workbench-editor')!,
24
+ );
25
+ webview.onRemove(() => {
26
+ mounter.dispose();
27
+ });
28
+ return () => {
29
+ webview.remove();
30
+ };
31
+ }
32
+ });
33
+
34
+ return (
35
+ <div
36
+ style={{ height: '100%', width: '100%', position: 'relative' }}
37
+ className='editor-webview-webview-component'
38
+ ref={(el) => (container = el)}
39
+ ></div>
40
+ );
41
+ };
42
+
43
+ /**
44
+ * 同一个ID创建的webview会保存在内存以便重复使用,不要使用这个组件进行大量不同webview的创建
45
+ */
46
+ export const PlainWebview: React.ComponentType<{ id: string; renderRoot?: HTMLElement; appendToChild?: boolean }> = ({
47
+ id,
48
+ renderRoot = document.body,
49
+ appendToChild,
50
+ }) => {
51
+ let container: HTMLDivElement | null = null;
52
+ const webviewService = useInjectable(IWebviewService) as IWebviewService;
53
+
54
+ React.useEffect(() => {
55
+ const component = webviewService.getOrCreatePlainWebviewComponent(id);
56
+ if (component && container) {
57
+ if (appendToChild) {
58
+ component.webview.appendTo(container);
59
+ } else {
60
+ const mounter = new WebviewMounter(
61
+ component.webview,
62
+ container,
63
+ document.getElementById('workbench-editor')!,
64
+ renderRoot,
65
+ );
66
+ component.webview.onRemove(() => {
67
+ mounter.dispose();
68
+ });
69
+ }
70
+
71
+ return () => {
72
+ component.webview.remove();
73
+ };
74
+ }
75
+ }, []);
76
+
77
+ return <div style={{ height: '100%', width: '100%', position: 'relative' }} ref={(el) => (container = el)}></div>;
78
+ };
79
+
80
+ // 将iframe挂载在一个固定的位置,以overlay的形式覆盖在container中,
81
+ // 防止它在DOM树改变时被重载
82
+ export class WebviewMounter extends Disposable {
83
+ private mounting: number;
84
+
85
+ private _container: HTMLElement | null;
86
+
87
+ constructor(
88
+ private webview: IWebview | IPlainWebview,
89
+ private container: HTMLElement,
90
+ mutationRoot: HTMLElement,
91
+ private renderRoot: HTMLElement = document.body,
92
+ ) {
93
+ super();
94
+ if (!this.webview.getDomNode()) {
95
+ return;
96
+ }
97
+ this.webview.appendTo(this.getWebviewRealContainer());
98
+ if (isWebview(this.webview)) {
99
+ this.webview.setKeybindingDomTarget(container);
100
+ }
101
+ const resizeObserver = new ResizeObserver(this.doMount.bind(this));
102
+ const mutationObserver = new MutationObserver((mutations) => {
103
+ const ancestors: Set<HTMLElement> = new Set();
104
+ let ancestor: HTMLElement | null = this.container;
105
+ while (ancestor && ancestor !== mutationRoot) {
106
+ ancestors.add(ancestor);
107
+ ancestor = ancestor.parentElement;
108
+ }
109
+ for (const { addedNodes, removedNodes } of mutations) {
110
+ for (const node of addedNodes) {
111
+ if (ancestors.has(node)) {
112
+ this.doMount();
113
+ return;
114
+ }
115
+ }
116
+ for (const node of removedNodes) {
117
+ if (ancestors.has(node)) {
118
+ this.doMount();
119
+ return;
120
+ }
121
+ }
122
+ }
123
+ });
124
+ resizeObserver.observe(container);
125
+ mutationObserver.observe(mutationRoot, { childList: true, subtree: true });
126
+
127
+ this.doMount();
128
+
129
+ this.addDispose({
130
+ dispose: () => {
131
+ if (this._container) {
132
+ this._container.remove();
133
+ this._container = null;
134
+ this.webview = null as any;
135
+ this.container = null as any;
136
+ }
137
+ resizeObserver.disconnect();
138
+ mutationObserver.disconnect();
139
+ },
140
+ });
141
+
142
+ this.addDispose(
143
+ new DomListener(window, 'resize', () => {
144
+ this.doMount();
145
+ }),
146
+ );
147
+
148
+ // 监听滚动
149
+ let parent = container.parentElement;
150
+ while (parent) {
151
+ this.addDispose(
152
+ new DomListener(parent, 'scroll', () => {
153
+ this.doMount();
154
+ }),
155
+ );
156
+ parent = parent.parentElement;
157
+ }
158
+ }
159
+
160
+ doMount() {
161
+ if (this.mounting) {
162
+ window.cancelAnimationFrame(this.mounting);
163
+ }
164
+ this.mounting = window.requestAnimationFrame(() => {
165
+ if (!this.webview.getDomNode()) {
166
+ return;
167
+ }
168
+ const rect = this.container.getBoundingClientRect();
169
+ if (rect.height === 0 || rect.width === 0) {
170
+ this.webview.getDomNode()!.style.display = 'none';
171
+ if (isWebview(this.webview) && !this.webview.options.longLive) {
172
+ this.webview.setListenMessages(false);
173
+ }
174
+ } else {
175
+ this.webview.getDomNode()!.style.display = '';
176
+ if (isWebview(this.webview)) {
177
+ this.webview.setListenMessages(true);
178
+ }
179
+ }
180
+ const renderRootRects = this.renderRoot.getBoundingClientRect();
181
+ this.webview.getDomNode()!.style.top = rect.top - renderRootRects.top + 'px';
182
+ this.webview.getDomNode()!.style.left = rect.left - renderRootRects.left + 'px';
183
+ this.webview.getDomNode()!.style.height = rect.height + 'px';
184
+ this.webview.getDomNode()!.style.width = rect.width + 'px';
185
+ this.mounting = 0;
186
+ });
187
+ }
188
+
189
+ getWebviewRealContainer() {
190
+ if (this._container) {
191
+ return this._container;
192
+ }
193
+ let mountContainer = this.renderRoot.querySelector(':scope > div[data-webview-container=true]');
194
+ if (!mountContainer) {
195
+ const container = document.createElement('div');
196
+ container.style.zIndex = '2';
197
+ container.style.position = 'absolute';
198
+ container.setAttribute('data-webview-container', 'true');
199
+ container.style.top = '0';
200
+ this.renderRoot.appendChild(container);
201
+ mountContainer = container;
202
+ }
203
+ this._container = document.createElement('div');
204
+ mountContainer!.appendChild(this._container);
205
+ return this._container!;
206
+ }
207
+ }