@theia/core 1.74.0-next.9 → 1.74.1
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/README.md +10 -10
- package/i18n/nls.cs.json +143 -16
- package/i18n/nls.de.json +143 -16
- package/i18n/nls.es.json +143 -16
- package/i18n/nls.fr.json +143 -16
- package/i18n/nls.hu.json +143 -16
- package/i18n/nls.it.json +143 -16
- package/i18n/nls.ja.json +143 -16
- package/i18n/nls.json +143 -16
- package/i18n/nls.ko.json +143 -16
- package/i18n/nls.pl.json +143 -16
- package/i18n/nls.pt-br.json +143 -16
- package/i18n/nls.ru.json +143 -16
- package/i18n/nls.tr.json +143 -16
- package/i18n/nls.zh-cn.json +143 -16
- package/i18n/nls.zh-tw.json +143 -16
- package/lib/browser/browser-clipboard-service.d.ts +3 -0
- package/lib/browser/browser-clipboard-service.d.ts.map +1 -1
- package/lib/browser/browser-clipboard-service.js +8 -1
- package/lib/browser/browser-clipboard-service.js.map +1 -1
- package/lib/browser/catalog.json +505 -46
- package/lib/browser/clipboard-service.d.ts +11 -0
- package/lib/browser/clipboard-service.d.ts.map +1 -1
- package/lib/browser/clipboard-service.js.map +1 -1
- package/lib/browser/common-frontend-contribution.d.ts +3 -0
- package/lib/browser/common-frontend-contribution.d.ts.map +1 -1
- package/lib/browser/common-frontend-contribution.js +22 -10
- package/lib/browser/common-frontend-contribution.js.map +1 -1
- package/lib/browser/common-frontend-contribution.spec.d.ts +2 -0
- package/lib/browser/common-frontend-contribution.spec.d.ts.map +1 -0
- package/lib/browser/common-frontend-contribution.spec.js +71 -0
- package/lib/browser/common-frontend-contribution.spec.js.map +1 -0
- package/lib/browser/frontend-application-contribution.d.ts +1 -1
- package/lib/browser/frontend-application-module.d.ts +2 -2
- package/lib/browser/frontend-application-module.d.ts.map +1 -1
- package/lib/browser/frontend-application-module.js +13 -2
- package/lib/browser/frontend-application-module.js.map +1 -1
- package/lib/browser/frontend-application.d.ts.map +1 -1
- package/lib/browser/frontend-application.js +12 -2
- package/lib/browser/frontend-application.js.map +1 -1
- package/lib/browser/index.d.ts +1 -0
- package/lib/browser/index.d.ts.map +1 -1
- package/lib/browser/index.js +1 -0
- package/lib/browser/index.js.map +1 -1
- package/lib/browser/keybinding.d.ts.map +1 -1
- package/lib/browser/keybinding.js +12 -7
- package/lib/browser/keybinding.js.map +1 -1
- package/lib/browser/perspective-service.d.ts +173 -0
- package/lib/browser/perspective-service.d.ts.map +1 -0
- package/lib/browser/perspective-service.js +484 -0
- package/lib/browser/perspective-service.js.map +1 -0
- package/lib/browser/perspective-service.spec.d.ts +2 -0
- package/lib/browser/perspective-service.spec.d.ts.map +1 -0
- package/lib/browser/perspective-service.spec.js +1806 -0
- package/lib/browser/perspective-service.spec.js.map +1 -0
- package/lib/browser/preferences/preference-contribution.d.ts.map +1 -1
- package/lib/browser/preferences/preference-contribution.js +9 -1
- package/lib/browser/preferences/preference-contribution.js.map +1 -1
- package/lib/browser/preferences/preference-service.spec.js +63 -0
- package/lib/browser/preferences/preference-service.spec.js.map +1 -1
- package/lib/browser/shell/application-shell.d.ts +7 -0
- package/lib/browser/shell/application-shell.d.ts.map +1 -1
- package/lib/browser/shell/application-shell.js +38 -28
- package/lib/browser/shell/application-shell.js.map +1 -1
- package/lib/browser/shell/shell-layout-restorer.d.ts +9 -0
- package/lib/browser/shell/shell-layout-restorer.d.ts.map +1 -1
- package/lib/browser/shell/shell-layout-restorer.js +124 -14
- package/lib/browser/shell/shell-layout-restorer.js.map +1 -1
- package/lib/browser/shell/shell-layout-restorer.spec.d.ts +2 -0
- package/lib/browser/shell/shell-layout-restorer.spec.d.ts.map +1 -0
- package/lib/browser/shell/shell-layout-restorer.spec.js +539 -0
- package/lib/browser/shell/shell-layout-restorer.spec.js.map +1 -0
- package/lib/browser/shell/view-contribution.d.ts +2 -0
- package/lib/browser/shell/view-contribution.d.ts.map +1 -1
- package/lib/browser/shell/view-contribution.js +5 -1
- package/lib/browser/shell/view-contribution.js.map +1 -1
- package/lib/browser/status-bar/status-bar.d.ts +2 -0
- package/lib/browser/status-bar/status-bar.d.ts.map +1 -1
- package/lib/browser/status-bar/status-bar.js +11 -3
- package/lib/browser/status-bar/status-bar.js.map +1 -1
- package/lib/browser/tree/tree-consistency.spec.js +103 -0
- package/lib/browser/tree/tree-consistency.spec.js.map +1 -1
- package/lib/browser/tree/tree-widget.d.ts +7 -33
- package/lib/browser/tree/tree-widget.d.ts.map +1 -1
- package/lib/browser/tree/tree-widget.js +7 -54
- package/lib/browser/tree/tree-widget.js.map +1 -1
- package/lib/browser/tree/tree.d.ts.map +1 -1
- package/lib/browser/tree/tree.js +6 -0
- package/lib/browser/tree/tree.js.map +1 -1
- package/lib/browser/widgets/index.d.ts +1 -0
- package/lib/browser/widgets/index.d.ts.map +1 -1
- package/lib/browser/widgets/index.js +1 -0
- package/lib/browser/widgets/index.js.map +1 -1
- package/lib/browser/widgets/select-component.d.ts +15 -2
- package/lib/browser/widgets/select-component.d.ts.map +1 -1
- package/lib/browser/widgets/select-component.js +86 -24
- package/lib/browser/widgets/select-component.js.map +1 -1
- package/lib/browser/widgets/tab-bar-tracker.d.ts +23 -0
- package/lib/browser/widgets/tab-bar-tracker.d.ts.map +1 -0
- package/lib/browser/widgets/tab-bar-tracker.js +65 -0
- package/lib/browser/widgets/tab-bar-tracker.js.map +1 -0
- package/lib/browser/widgets/tab-bar-tracker.spec.d.ts +2 -0
- package/lib/browser/widgets/tab-bar-tracker.spec.d.ts.map +1 -0
- package/lib/browser/widgets/tab-bar-tracker.spec.js +109 -0
- package/lib/browser/widgets/tab-bar-tracker.spec.js.map +1 -0
- package/lib/browser/window/default-secondary-window-service.d.ts +2 -0
- package/lib/browser/window/default-secondary-window-service.d.ts.map +1 -1
- package/lib/browser/window/default-secondary-window-service.js +8 -2
- package/lib/browser/window/default-secondary-window-service.js.map +1 -1
- package/lib/browser/window/default-window-service.d.ts +19 -0
- package/lib/browser/window/default-window-service.d.ts.map +1 -1
- package/lib/browser/window/default-window-service.js +34 -5
- package/lib/browser/window/default-window-service.js.map +1 -1
- package/lib/browser/window/default-window-service.spec.js +22 -0
- package/lib/browser/window/default-window-service.spec.js.map +1 -1
- package/lib/browser/window/secondary-window-service.d.ts +7 -0
- package/lib/browser/window/secondary-window-service.d.ts.map +1 -1
- package/lib/browser/window/window-service.d.ts +1 -1
- package/lib/browser-only/messaging/frontend-only-service-connection-provider.d.ts +13 -2
- package/lib/browser-only/messaging/frontend-only-service-connection-provider.d.ts.map +1 -1
- package/lib/browser-only/messaging/frontend-only-service-connection-provider.js +31 -3
- package/lib/browser-only/messaging/frontend-only-service-connection-provider.js.map +1 -1
- package/lib/common/preferences/index.d.ts +1 -0
- package/lib/common/preferences/index.d.ts.map +1 -1
- package/lib/common/preferences/index.js +1 -0
- package/lib/common/preferences/index.js.map +1 -1
- package/lib/common/preferences/preference-provider.d.ts +15 -0
- package/lib/common/preferences/preference-provider.d.ts.map +1 -1
- package/lib/common/preferences/preference-provider.js +21 -1
- package/lib/common/preferences/preference-provider.js.map +1 -1
- package/lib/common/preferences/preference-provider.spec.js +17 -0
- package/lib/common/preferences/preference-provider.spec.js.map +1 -1
- package/lib/common/preferences/preference-scope.d.ts +6 -1
- package/lib/common/preferences/preference-scope.d.ts.map +1 -1
- package/lib/common/preferences/preference-scope.js +5 -0
- package/lib/common/preferences/preference-scope.js.map +1 -1
- package/lib/common/preferences/preference-scope.spec.js +5 -3
- package/lib/common/preferences/preference-scope.spec.js.map +1 -1
- package/lib/common/preferences/preference-service.d.ts +19 -0
- package/lib/common/preferences/preference-service.d.ts.map +1 -1
- package/lib/common/preferences/preference-service.js +34 -5
- package/lib/common/preferences/preference-service.js.map +1 -1
- package/lib/common/preferences/session-preference-provider.d.ts +17 -0
- package/lib/common/preferences/session-preference-provider.d.ts.map +1 -0
- package/lib/common/preferences/session-preference-provider.js +77 -0
- package/lib/common/preferences/session-preference-provider.js.map +1 -0
- package/lib/common/preferences/session-preference-provider.spec.d.ts +2 -0
- package/lib/common/preferences/session-preference-provider.spec.d.ts.map +1 -0
- package/lib/common/preferences/session-preference-provider.spec.js +71 -0
- package/lib/common/preferences/session-preference-provider.spec.js.map +1 -0
- package/lib/electron-browser/electron-clipboard-service.d.ts +3 -0
- package/lib/electron-browser/electron-clipboard-service.d.ts.map +1 -1
- package/lib/electron-browser/electron-clipboard-service.js +6 -0
- package/lib/electron-browser/electron-clipboard-service.js.map +1 -1
- package/lib/electron-browser/menu/electron-menu-contribution.js +1 -1
- package/lib/electron-browser/menu/electron-menu-contribution.js.map +1 -1
- package/lib/electron-browser/preload.d.ts.map +1 -1
- package/lib/electron-browser/preload.js +2 -1
- package/lib/electron-browser/preload.js.map +1 -1
- package/lib/electron-browser/window/electron-secondary-window-service.js +1 -1
- package/lib/electron-browser/window/electron-secondary-window-service.js.map +1 -1
- package/lib/electron-browser/window/electron-window-service.js +3 -3
- package/lib/electron-browser/window/electron-window-service.js.map +1 -1
- package/lib/electron-common/electron-api.d.ts +2 -0
- package/lib/electron-common/electron-api.d.ts.map +1 -1
- package/lib/electron-common/electron-api.js +2 -1
- package/lib/electron-common/electron-api.js.map +1 -1
- package/lib/electron-main/electron-api-main.d.ts.map +1 -1
- package/lib/electron-main/electron-api-main.js +43 -0
- package/lib/electron-main/electron-api-main.js.map +1 -1
- package/lib/electron-main/electron-main-application.d.ts.map +1 -1
- package/lib/electron-main/electron-main-application.js +3 -0
- package/lib/electron-main/electron-main-application.js.map +1 -1
- package/lib/electron-node/hosting/electron-backend-hosting-module.d.ts.map +1 -1
- package/lib/electron-node/hosting/electron-backend-hosting-module.js +7 -0
- package/lib/electron-node/hosting/electron-backend-hosting-module.js.map +1 -1
- package/lib/electron-node/hosting/electron-backend-hosting-module.spec.d.ts +2 -0
- package/lib/electron-node/hosting/electron-backend-hosting-module.spec.d.ts.map +1 -0
- package/lib/electron-node/hosting/electron-backend-hosting-module.spec.js +39 -0
- package/lib/electron-node/hosting/electron-backend-hosting-module.spec.js.map +1 -0
- package/lib/node/hosting/backend-hosting-module.d.ts.map +1 -1
- package/lib/node/hosting/backend-hosting-module.js +4 -1
- package/lib/node/hosting/backend-hosting-module.js.map +1 -1
- package/lib/node/hosting/browser-connection-token.d.ts +29 -1
- package/lib/node/hosting/browser-connection-token.d.ts.map +1 -1
- package/lib/node/hosting/browser-connection-token.js +25 -1
- package/lib/node/hosting/browser-connection-token.js.map +1 -1
- package/lib/node/hosting/browser-connection-token.spec.js +70 -0
- package/lib/node/hosting/browser-connection-token.spec.js.map +1 -1
- package/lib/node/index.d.ts +1 -0
- package/lib/node/index.d.ts.map +1 -1
- package/lib/node/index.js +3 -1
- package/lib/node/index.js.map +1 -1
- package/package.json +11 -11
- package/src/browser/browser-clipboard-service.ts +7 -1
- package/src/browser/clipboard-service.ts +11 -0
- package/src/browser/common-frontend-contribution.spec.ts +84 -0
- package/src/browser/common-frontend-contribution.ts +22 -10
- package/src/browser/frontend-application-contribution.ts +1 -1
- package/src/browser/frontend-application-module.ts +15 -3
- package/src/browser/frontend-application.ts +12 -4
- package/src/browser/index.ts +1 -0
- package/src/browser/keybinding.ts +12 -7
- package/src/browser/perspective-service.spec.ts +2270 -0
- package/src/browser/perspective-service.ts +625 -0
- package/src/browser/preferences/preference-contribution.ts +9 -1
- package/src/browser/preferences/preference-service.spec.ts +72 -0
- package/src/browser/shell/application-shell.ts +41 -26
- package/src/browser/shell/shell-layout-restorer.spec.ts +746 -0
- package/src/browser/shell/shell-layout-restorer.ts +134 -13
- package/src/browser/shell/view-contribution.ts +6 -1
- package/src/browser/status-bar/status-bar.tsx +13 -3
- package/src/browser/style/select-component.css +16 -0
- package/src/browser/style/sidepanel.css +5 -0
- package/src/browser/style/status-bar.css +1 -1
- package/src/browser/tree/tree-consistency.spec.ts +119 -0
- package/src/browser/tree/tree-widget.tsx +12 -78
- package/src/browser/tree/tree.ts +6 -0
- package/src/browser/widgets/index.ts +1 -0
- package/src/browser/widgets/select-component.tsx +90 -24
- package/src/browser/widgets/tab-bar-tracker.spec.ts +121 -0
- package/src/browser/widgets/tab-bar-tracker.ts +68 -0
- package/src/browser/window/default-secondary-window-service.ts +8 -2
- package/src/browser/window/default-window-service.spec.ts +22 -0
- package/src/browser/window/default-window-service.ts +37 -5
- package/src/browser/window/secondary-window-service.ts +7 -0
- package/src/browser/window/window-service.ts +1 -1
- package/src/browser-only/messaging/frontend-only-service-connection-provider.ts +32 -3
- package/src/common/i18n/nls.metadata.json +4149 -888
- package/src/common/preferences/index.ts +1 -0
- package/src/common/preferences/preference-provider.spec.ts +17 -0
- package/src/common/preferences/preference-provider.ts +22 -1
- package/src/common/preferences/preference-scope.spec.ts +5 -3
- package/src/common/preferences/preference-scope.ts +6 -1
- package/src/common/preferences/preference-service.ts +39 -5
- package/src/common/preferences/session-preference-provider.spec.ts +78 -0
- package/src/common/preferences/session-preference-provider.ts +69 -0
- package/src/electron-browser/electron-clipboard-service.ts +5 -0
- package/src/electron-browser/menu/electron-menu-contribution.ts +1 -1
- package/src/electron-browser/menu/electron-menu-style.css +3 -1
- package/src/electron-browser/preload.ts +5 -2
- package/src/electron-browser/window/electron-secondary-window-service.ts +1 -1
- package/src/electron-browser/window/electron-window-service.ts +3 -3
- package/src/electron-common/electron-api.ts +4 -0
- package/src/electron-main/electron-api-main.ts +56 -3
- package/src/electron-main/electron-main-application.ts +3 -0
- package/src/electron-node/hosting/electron-backend-hosting-module.spec.ts +42 -0
- package/src/electron-node/hosting/electron-backend-hosting-module.ts +8 -0
- package/src/node/hosting/backend-hosting-module.ts +5 -2
- package/src/node/hosting/browser-connection-token.spec.ts +92 -0
- package/src/node/hosting/browser-connection-token.ts +45 -1
- package/src/node/index.ts +1 -0
|
@@ -61,6 +61,10 @@ export class SelectComponent extends React.Component<SelectComponentProps, Selec
|
|
|
61
61
|
protected optimalWidth = 0;
|
|
62
62
|
protected optimalHeight = 0;
|
|
63
63
|
protected resizeObserver: ResizeObserver | undefined;
|
|
64
|
+
/** Window the scroll/wheel/resize listeners are attached to; may differ from the main window in secondary windows. */
|
|
65
|
+
protected listenersWindow: Window | undefined;
|
|
66
|
+
/** Perfect-scrollbar ancestors the `ps-scroll-y` listener was attached to. The field may have moved to another window since, so the elements are remembered. */
|
|
67
|
+
protected psScrollListenerTargets: HTMLElement[] = [];
|
|
64
68
|
|
|
65
69
|
constructor(props: SelectComponentProps) {
|
|
66
70
|
super(props);
|
|
@@ -71,14 +75,28 @@ export class SelectComponent extends React.Component<SelectComponentProps, Selec
|
|
|
71
75
|
hover: selected
|
|
72
76
|
};
|
|
73
77
|
|
|
74
|
-
|
|
78
|
+
this.dropdownElement = this.resolveDropdownContainer(document);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The document that currently hosts this component. Falls back to the main document before
|
|
83
|
+
* the first render. Widgets can be moved to secondary windows, so this must be re-queried
|
|
84
|
+
* instead of captured once.
|
|
85
|
+
*/
|
|
86
|
+
protected getHostDocument(): Document {
|
|
87
|
+
return this.fieldRef.current?.ownerDocument ?? document;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Finds or creates the dropdown portal container in the given document. */
|
|
91
|
+
protected resolveDropdownContainer(hostDocument: Document): HTMLElement {
|
|
92
|
+
let list = hostDocument.getElementById(SELECT_COMPONENT_CONTAINER);
|
|
75
93
|
if (!list) {
|
|
76
|
-
list =
|
|
94
|
+
list = hostDocument.createElement('div');
|
|
77
95
|
list.id = SELECT_COMPONENT_CONTAINER;
|
|
78
96
|
list.className = 'theia-select-component-container';
|
|
79
|
-
|
|
97
|
+
hostDocument.body.appendChild(list);
|
|
80
98
|
}
|
|
81
|
-
|
|
99
|
+
return list;
|
|
82
100
|
}
|
|
83
101
|
|
|
84
102
|
protected getInitialSelectedIndex(props: SelectComponentProps): number {
|
|
@@ -91,7 +109,20 @@ export class SelectComponent extends React.Component<SelectComponentProps, Selec
|
|
|
91
109
|
return selected;
|
|
92
110
|
}
|
|
93
111
|
|
|
94
|
-
override componentDidUpdate(prevProps: SelectComponentProps): void {
|
|
112
|
+
override componentDidUpdate(prevProps: SelectComponentProps, prevState: SelectComponentState): void {
|
|
113
|
+
if (prevState.selected !== this.state.selected && this.state.dimensions) {
|
|
114
|
+
const dropdownContainer = this.dropdownRef?.current;
|
|
115
|
+
const selectedOption = dropdownContainer?.querySelector(`[data-option-index="${this.state.selected}"]`) as HTMLElement | undefined;
|
|
116
|
+
if (dropdownContainer && selectedOption) {
|
|
117
|
+
const optionBottom = selectedOption.offsetTop + selectedOption.offsetHeight;
|
|
118
|
+
const visibleBottom = dropdownContainer.scrollTop + dropdownContainer.clientHeight;
|
|
119
|
+
if (optionBottom > visibleBottom) {
|
|
120
|
+
dropdownContainer.scrollTop = optionBottom - dropdownContainer.clientHeight;
|
|
121
|
+
} else if (selectedOption.offsetTop < dropdownContainer.scrollTop) {
|
|
122
|
+
dropdownContainer.scrollTop = selectedOption.offsetTop;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
95
126
|
if (prevProps.defaultValue !== this.props.defaultValue || prevProps.options !== this.props.options) {
|
|
96
127
|
const selected = this.getInitialSelectedIndex(this.props);
|
|
97
128
|
this.setState({
|
|
@@ -166,12 +197,14 @@ export class SelectComponent extends React.Component<SelectComponentProps, Selec
|
|
|
166
197
|
// neither triggers the `scroll`, `wheel` nor `blur` event
|
|
167
198
|
if (parent.classList.contains('ps')) {
|
|
168
199
|
parent.addEventListener('ps-scroll-y', hide);
|
|
200
|
+
this.psScrollListenerTargets.push(parent);
|
|
169
201
|
}
|
|
170
202
|
parent = parent.parentElement;
|
|
171
203
|
}
|
|
172
204
|
|
|
205
|
+
this.listenersWindow = this.getHostDocument().defaultView ?? window;
|
|
173
206
|
for (const [key, listener] of this.mountedListeners.entries()) {
|
|
174
|
-
|
|
207
|
+
this.listenersWindow.addEventListener(key, listener);
|
|
175
208
|
}
|
|
176
209
|
|
|
177
210
|
// Catch Lumino sash drags globally - observe the closest lm-Widget panel
|
|
@@ -209,18 +242,24 @@ export class SelectComponent extends React.Component<SelectComponentProps, Selec
|
|
|
209
242
|
}
|
|
210
243
|
|
|
211
244
|
override componentWillUnmount(): void {
|
|
245
|
+
this.detachListeners();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
protected detachListeners(): void {
|
|
212
249
|
this.resizeObserver?.disconnect();
|
|
250
|
+
this.resizeObserver = undefined;
|
|
213
251
|
if (this.mountedListeners.size > 0) {
|
|
214
252
|
const eventListener = this.mountedListeners.get('scroll')!;
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
parent.removeEventListener('ps-scroll-y', eventListener);
|
|
218
|
-
parent = parent.parentElement;
|
|
253
|
+
for (const target of this.psScrollListenerTargets) {
|
|
254
|
+
target.removeEventListener('ps-scroll-y', eventListener);
|
|
219
255
|
}
|
|
220
256
|
for (const [key, listener] of this.mountedListeners.entries()) {
|
|
221
|
-
window.removeEventListener(key, listener);
|
|
257
|
+
(this.listenersWindow ?? window).removeEventListener(key, listener);
|
|
222
258
|
}
|
|
259
|
+
this.mountedListeners.clear();
|
|
223
260
|
}
|
|
261
|
+
this.psScrollListenerTargets = [];
|
|
262
|
+
this.listenersWindow = undefined;
|
|
224
263
|
}
|
|
225
264
|
|
|
226
265
|
override render(): React.ReactNode {
|
|
@@ -230,13 +269,18 @@ export class SelectComponent extends React.Component<SelectComponentProps, Selec
|
|
|
230
269
|
selected = this.nextNotSeparator('forwards');
|
|
231
270
|
}
|
|
232
271
|
const selectedItemLabel = options[selected]?.label ?? options[selected]?.value;
|
|
272
|
+
// Expose when the currently shown option is disabled so consumers can style the field to match
|
|
273
|
+
// it (e.g. the option previewed while navigating), instead of a fixed committed-value class.
|
|
274
|
+
const selectionDisabled = !!options[selected]?.disabled;
|
|
275
|
+
const fieldClassName = `theia-select-component${this.props.className ? ` ${this.props.className}` : ''}`
|
|
276
|
+
+ `${selectionDisabled ? ' theia-select-component-selection-disabled' : ''}`;
|
|
233
277
|
return <>
|
|
234
278
|
<div
|
|
235
279
|
id={this.props.id}
|
|
236
280
|
key="select-component"
|
|
237
281
|
ref={this.fieldRef}
|
|
238
282
|
tabIndex={0}
|
|
239
|
-
className={
|
|
283
|
+
className={fieldClassName}
|
|
240
284
|
onClick={e => this.handleClickEvent(e)}
|
|
241
285
|
onBlur={
|
|
242
286
|
() => {
|
|
@@ -254,11 +298,11 @@ export class SelectComponent extends React.Component<SelectComponentProps, Selec
|
|
|
254
298
|
</>;
|
|
255
299
|
}
|
|
256
300
|
|
|
257
|
-
protected nextNotSeparator(direction: 'forwards' | 'backwards'): number {
|
|
301
|
+
protected nextNotSeparator(direction: 'forwards' | 'backwards', startFrom?: number): number {
|
|
258
302
|
const { options } = this.props;
|
|
259
303
|
const step = direction === 'forwards' ? 1 : -1;
|
|
260
304
|
const length = this.props.options.length;
|
|
261
|
-
let selected = this.state.selected;
|
|
305
|
+
let selected = startFrom ?? this.state.selected;
|
|
262
306
|
let count = 0;
|
|
263
307
|
do {
|
|
264
308
|
selected = (selected + step) % length;
|
|
@@ -267,7 +311,8 @@ export class SelectComponent extends React.Component<SelectComponentProps, Selec
|
|
|
267
311
|
}
|
|
268
312
|
count++;
|
|
269
313
|
}
|
|
270
|
-
|
|
314
|
+
// Skip separators and disabled options so keyboard navigation never lands on a non-selectable entry.
|
|
315
|
+
while ((options[selected]?.separator || options[selected]?.disabled) && count < length);
|
|
271
316
|
return selected;
|
|
272
317
|
}
|
|
273
318
|
|
|
@@ -276,23 +321,26 @@ export class SelectComponent extends React.Component<SelectComponentProps, Selec
|
|
|
276
321
|
return;
|
|
277
322
|
}
|
|
278
323
|
if (ev.key === 'ArrowUp') {
|
|
279
|
-
const selected = this.nextNotSeparator('backwards');
|
|
324
|
+
const selected = this.nextNotSeparator('backwards', this.state.hover);
|
|
280
325
|
this.setState({
|
|
281
326
|
selected,
|
|
282
327
|
hover: selected
|
|
283
328
|
});
|
|
284
329
|
} else if (ev.key === 'ArrowDown') {
|
|
285
330
|
if (this.state.dimensions) {
|
|
286
|
-
const selected = this.nextNotSeparator('forwards');
|
|
331
|
+
const selected = this.nextNotSeparator('forwards', this.state.hover);
|
|
287
332
|
this.setState({
|
|
288
333
|
selected,
|
|
289
334
|
hover: selected
|
|
290
335
|
});
|
|
291
336
|
} else {
|
|
292
337
|
this.toggleVisibility();
|
|
338
|
+
// Start on the first selectable option (startFrom -1 evaluates index 0 first), so a
|
|
339
|
+
// leading separator or disabled option is skipped instead of becoming the active row.
|
|
340
|
+
const selected = this.nextNotSeparator('forwards', -1);
|
|
293
341
|
this.setState({
|
|
294
|
-
selected
|
|
295
|
-
hover:
|
|
342
|
+
selected,
|
|
343
|
+
hover: selected,
|
|
296
344
|
});
|
|
297
345
|
}
|
|
298
346
|
} else if (ev.key === 'Enter') {
|
|
@@ -300,7 +348,10 @@ export class SelectComponent extends React.Component<SelectComponentProps, Selec
|
|
|
300
348
|
this.toggleVisibility();
|
|
301
349
|
} else {
|
|
302
350
|
const selected = this.state.selected;
|
|
303
|
-
|
|
351
|
+
const option = this.props.options[selected];
|
|
352
|
+
if (!option?.disabled) {
|
|
353
|
+
this.selectOption(selected, option);
|
|
354
|
+
}
|
|
304
355
|
}
|
|
305
356
|
} else if (ev.key === 'Escape' || ev.key === 'Tab') {
|
|
306
357
|
this.hide(undefined, true);
|
|
@@ -320,6 +371,14 @@ export class SelectComponent extends React.Component<SelectComponentProps, Selec
|
|
|
320
371
|
return;
|
|
321
372
|
}
|
|
322
373
|
if (!this.state.dimensions) {
|
|
374
|
+
// Re-resolve the portal container and listener window against the document that
|
|
375
|
+
// currently hosts the field: the widget may have been moved to a secondary window
|
|
376
|
+
// (or back to the main window) since the last time the dropdown was opened.
|
|
377
|
+
const hostDocument = this.getHostDocument();
|
|
378
|
+
this.dropdownElement = this.resolveDropdownContainer(hostDocument);
|
|
379
|
+
if (this.listenersWindow && this.listenersWindow !== hostDocument.defaultView) {
|
|
380
|
+
this.detachListeners();
|
|
381
|
+
}
|
|
323
382
|
const rect = this.fieldRef.current.getBoundingClientRect();
|
|
324
383
|
this.setState({ dimensions: rect });
|
|
325
384
|
} else {
|
|
@@ -336,7 +395,8 @@ export class SelectComponent extends React.Component<SelectComponentProps, Selec
|
|
|
336
395
|
hover: selectedIndex
|
|
337
396
|
});
|
|
338
397
|
// Force releasing focus of the select element to allow closing via escape later on
|
|
339
|
-
|
|
398
|
+
const hostDocument = this.getHostDocument();
|
|
399
|
+
if (releaseFocus && hostDocument.activeElement && hostDocument.activeElement === this.fieldRef.current) {
|
|
340
400
|
this.fieldRef.current.blur();
|
|
341
401
|
}
|
|
342
402
|
}
|
|
@@ -346,7 +406,9 @@ export class SelectComponent extends React.Component<SelectComponentProps, Selec
|
|
|
346
406
|
return;
|
|
347
407
|
}
|
|
348
408
|
|
|
349
|
-
|
|
409
|
+
// Secondary windows have no 'theia-app-shell' element; use the document element as the boundary there.
|
|
410
|
+
const hostDocument = this.getHostDocument();
|
|
411
|
+
const shellArea = (hostDocument.getElementById('theia-app-shell') ?? hostDocument.documentElement).getBoundingClientRect();
|
|
350
412
|
const maxWidth = this.alignLeft ? shellArea.width - this.state.dimensions.left : this.state.dimensions.right;
|
|
351
413
|
if (this.mountedListeners.size === 0) {
|
|
352
414
|
// Only attach our listeners once we render our dropdown menu
|
|
@@ -401,17 +463,21 @@ export class SelectComponent extends React.Component<SelectComponentProps, Selec
|
|
|
401
463
|
return <div key={index} className="theia-select-component-separator" />;
|
|
402
464
|
}
|
|
403
465
|
const selected = this.state.hover;
|
|
466
|
+
const disabled = !!option.disabled;
|
|
404
467
|
return (
|
|
405
468
|
<div
|
|
406
469
|
key={index}
|
|
407
|
-
|
|
470
|
+
data-option-index={index}
|
|
471
|
+
className={`theia-select-component-option${index === selected ? ' selected' : ''}${disabled ? ' disabled' : ''}`}
|
|
408
472
|
onMouseOver={() => {
|
|
409
473
|
this.setState({
|
|
410
474
|
hover: index
|
|
411
475
|
});
|
|
412
476
|
}}
|
|
413
477
|
onMouseDown={() => {
|
|
414
|
-
|
|
478
|
+
if (!disabled) {
|
|
479
|
+
this.selectOption(index, option);
|
|
480
|
+
}
|
|
415
481
|
}}
|
|
416
482
|
>
|
|
417
483
|
<div key="value" className="theia-select-component-option-value">{option.label ?? option.value}</div>
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 JuliaHub, Inc. and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { enableJSDOM } from '../test/jsdom';
|
|
18
|
+
let disableJSDOM = enableJSDOM();
|
|
19
|
+
|
|
20
|
+
import { expect } from 'chai';
|
|
21
|
+
import { MessageLoop } from '@lumino/messaging';
|
|
22
|
+
import { DockPanel, TabBar, Widget } from '@lumino/widgets';
|
|
23
|
+
import { TabBarTracker } from './tab-bar-tracker';
|
|
24
|
+
|
|
25
|
+
disableJSDOM();
|
|
26
|
+
|
|
27
|
+
describe('TabBarTracker', () => {
|
|
28
|
+
|
|
29
|
+
before(() => {
|
|
30
|
+
disableJSDOM = enableJSDOM();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
after(() => {
|
|
34
|
+
disableJSDOM();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
let panel: DockPanel | undefined;
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
if (panel) {
|
|
41
|
+
Widget.detach(panel);
|
|
42
|
+
panel.dispose();
|
|
43
|
+
panel = undefined;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
function attachInDockPanel(): Widget {
|
|
48
|
+
panel = new DockPanel();
|
|
49
|
+
Widget.attach(panel, document.body);
|
|
50
|
+
const widget = new Widget();
|
|
51
|
+
panel.addWidget(widget);
|
|
52
|
+
MessageLoop.flush();
|
|
53
|
+
return widget;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
it('reports the enclosing tab-bar on the first check', () => {
|
|
57
|
+
const widget = attachInDockPanel();
|
|
58
|
+
const events: Array<[TabBar<Widget> | undefined, TabBar<Widget> | undefined]> = [];
|
|
59
|
+
const tracker = new TabBarTracker(widget, (oldTabBar, newTabBar) => events.push([oldTabBar, newTabBar]));
|
|
60
|
+
tracker.check();
|
|
61
|
+
expect(events).lengthOf(1);
|
|
62
|
+
expect(events[0][0]).undefined;
|
|
63
|
+
expect(events[0][1]).not.undefined;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('does not report again when the tab-bar has not changed', () => {
|
|
67
|
+
const widget = attachInDockPanel();
|
|
68
|
+
let count = 0;
|
|
69
|
+
const tracker = new TabBarTracker(widget, () => count++);
|
|
70
|
+
tracker.check();
|
|
71
|
+
tracker.check();
|
|
72
|
+
expect(count).equal(1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('does nothing when the widget is not inside a DockPanel', () => {
|
|
76
|
+
let count = 0;
|
|
77
|
+
const tracker = new TabBarTracker(new Widget(), () => count++);
|
|
78
|
+
tracker.check();
|
|
79
|
+
expect(count).equal(0);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('stops reporting once disposed', () => {
|
|
83
|
+
const widget = attachInDockPanel();
|
|
84
|
+
let count = 0;
|
|
85
|
+
const tracker = new TabBarTracker(widget, () => count++);
|
|
86
|
+
tracker.dispose();
|
|
87
|
+
tracker.check();
|
|
88
|
+
expect(count).equal(0);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('reports a move between two tab-bars', () => {
|
|
92
|
+
panel = new DockPanel();
|
|
93
|
+
Widget.attach(panel, document.body);
|
|
94
|
+
const other = new Widget();
|
|
95
|
+
const widget = new Widget();
|
|
96
|
+
panel.addWidget(other);
|
|
97
|
+
panel.addWidget(widget, { mode: 'split-right', ref: other });
|
|
98
|
+
MessageLoop.flush();
|
|
99
|
+
const events: Array<[TabBar<Widget> | undefined, TabBar<Widget> | undefined]> = [];
|
|
100
|
+
const tracker = new TabBarTracker(widget, (oldTabBar, newTabBar) => events.push([oldTabBar, newTabBar]));
|
|
101
|
+
tracker.check();
|
|
102
|
+
panel.addWidget(widget, { mode: 'tab-after', ref: other });
|
|
103
|
+
MessageLoop.flush();
|
|
104
|
+
tracker.check();
|
|
105
|
+
expect(events).lengthOf(2);
|
|
106
|
+
expect(events[1][0]).not.undefined;
|
|
107
|
+
expect(events[1][0]).not.equal(events[1][1]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('re-reports the current tab-bar after reset', () => {
|
|
111
|
+
const widget = attachInDockPanel();
|
|
112
|
+
const events: Array<[TabBar<Widget> | undefined, TabBar<Widget> | undefined]> = [];
|
|
113
|
+
const tracker = new TabBarTracker(widget, (oldTabBar, newTabBar) => events.push([oldTabBar, newTabBar]));
|
|
114
|
+
tracker.check();
|
|
115
|
+
tracker.reset();
|
|
116
|
+
tracker.check();
|
|
117
|
+
expect(events).lengthOf(2);
|
|
118
|
+
expect(events[1][0]).undefined;
|
|
119
|
+
expect(events[1][1]).not.undefined;
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 JuliaHub, Inc. and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { find } from '@lumino/algorithm';
|
|
18
|
+
import { DockPanel, TabBar, Widget } from '@lumino/widgets';
|
|
19
|
+
import { Disposable, DisposableCollection } from '../../common';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Detects when a widget is moved between the tab-bars of its enclosing {@link DockPanel} and
|
|
23
|
+
* notifies a callback with the previous and current tab-bar. Call {@link check} from the host
|
|
24
|
+
* widget's `onAfterAttach`.
|
|
25
|
+
*/
|
|
26
|
+
export class TabBarTracker implements Disposable {
|
|
27
|
+
|
|
28
|
+
protected currentTabBar: TabBar<Widget> | undefined;
|
|
29
|
+
protected isDisposed = false;
|
|
30
|
+
protected readonly toDisposeOnTabBarChange = new DisposableCollection();
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
protected readonly widget: Widget,
|
|
34
|
+
protected readonly onTabBarChange: (oldTabBar?: TabBar<Widget>, newTabBar?: TabBar<Widget>) => void
|
|
35
|
+
) { }
|
|
36
|
+
|
|
37
|
+
check(): void {
|
|
38
|
+
if (this.isDisposed) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const parent = this.widget.parent;
|
|
42
|
+
if (parent instanceof DockPanel) {
|
|
43
|
+
const newTabBar = find(parent.tabBars(), tabBar => !!tabBar.titles.find(title => title === this.widget.title));
|
|
44
|
+
if (this.currentTabBar !== newTabBar) {
|
|
45
|
+
this.toDisposeOnTabBarChange.dispose();
|
|
46
|
+
const listener = () => this.check();
|
|
47
|
+
parent.layoutModified.connect(listener);
|
|
48
|
+
this.toDisposeOnTabBarChange.push(Disposable.create(() => parent.layoutModified.disconnect(listener)));
|
|
49
|
+
const last = this.currentTabBar;
|
|
50
|
+
this.currentTabBar = newTabBar;
|
|
51
|
+
this.onTabBarChange(last, newTabBar);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Forgets the current tab-bar so that the next {@link check} reports it again as
|
|
58
|
+
* `(undefined, currentTabBar)`. Listeners on the enclosing panel stay connected.
|
|
59
|
+
*/
|
|
60
|
+
reset(): void {
|
|
61
|
+
this.currentTabBar = undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
dispose(): void {
|
|
65
|
+
this.isDisposed = true;
|
|
66
|
+
this.toDisposeOnTabBarChange.dispose();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -28,6 +28,8 @@ import { WindowFocusService } from './window-focus-service';
|
|
|
28
28
|
export class DefaultSecondaryWindowService implements SecondaryWindowService {
|
|
29
29
|
protected readonly onWindowOpenedEmitter = new Emitter<Window>;
|
|
30
30
|
readonly onWindowOpened: Event<Window> = this.onWindowOpenedEmitter.event;
|
|
31
|
+
protected readonly onWindowLoadedEmitter = new Emitter<Window>;
|
|
32
|
+
readonly onWindowLoaded: Event<Window> = this.onWindowLoadedEmitter.event;
|
|
31
33
|
protected readonly onWindowClosedEmitter = new Emitter<Window>;
|
|
32
34
|
readonly onWindowClosed: Event<Window> = this.onWindowClosedEmitter.event;
|
|
33
35
|
protected readonly beforeWidgetRestoreEmitter = new Emitter<[Widget, Window]>;
|
|
@@ -110,6 +112,10 @@ export class DefaultSecondaryWindowService implements SecondaryWindowService {
|
|
|
110
112
|
this.secondaryWindows.push(newWindow);
|
|
111
113
|
this.onWindowOpenedEmitter.fire(newWindow);
|
|
112
114
|
newWindow.addEventListener('DOMContentLoaded', () => {
|
|
115
|
+
// This fires for the final secondary-window.html document, not for the initial
|
|
116
|
+
// about:blank document the window starts out with, so onWindowLoaded listeners
|
|
117
|
+
// can safely modify the document.
|
|
118
|
+
this.onWindowLoadedEmitter.fire(newWindow);
|
|
113
119
|
const focusRegistration = this.windowFocusService.registerWindow(newWindow);
|
|
114
120
|
newWindow.addEventListener('beforeunload', evt => {
|
|
115
121
|
const widgets = getAllWidgetsFromSecondaryWindow(newWindow) ?? [widget];
|
|
@@ -124,7 +130,7 @@ export class DefaultSecondaryWindowService implements SecondaryWindowService {
|
|
|
124
130
|
}
|
|
125
131
|
}, { capture: true });
|
|
126
132
|
|
|
127
|
-
newWindow.addEventListener('
|
|
133
|
+
newWindow.addEventListener('pagehide', () => {
|
|
128
134
|
focusRegistration.dispose();
|
|
129
135
|
const extIndex = this.secondaryWindows.indexOf(newWindow);
|
|
130
136
|
if (extIndex > -1) {
|
|
@@ -140,7 +146,7 @@ export class DefaultSecondaryWindowService implements SecondaryWindowService {
|
|
|
140
146
|
}
|
|
141
147
|
|
|
142
148
|
protected windowCreated(newWindow: Window, widget: ExtractableWidget, shell: ApplicationShell): void {
|
|
143
|
-
newWindow.addEventListener('
|
|
149
|
+
newWindow.addEventListener('pagehide', () => {
|
|
144
150
|
this.restoreWidgets(newWindow, widget, shell);
|
|
145
151
|
});
|
|
146
152
|
}
|
|
@@ -77,4 +77,26 @@ describe('DefaultWindowService', () => {
|
|
|
77
77
|
assert(windowService['collectContributionUnloadVetoes']().length > 0, 'there should be vetoes');
|
|
78
78
|
assert(frontendContributions.every(contribution => contribution.onWillStopCalled), 'contributions should have been called');
|
|
79
79
|
});
|
|
80
|
+
it('should reload unconditionally when restored from the back/forward cache', () => {
|
|
81
|
+
const frontendContributions: TestFrontendApplicationContribution[] = [
|
|
82
|
+
new TestFrontendApplicationContribution(true),
|
|
83
|
+
];
|
|
84
|
+
const windowService = setupWindowService('always', frontendContributions);
|
|
85
|
+
let reloaded = 0;
|
|
86
|
+
windowService.reload = () => { reloaded++; };
|
|
87
|
+
windowService['handlePageShow']({ persisted: false } as PageTransitionEvent);
|
|
88
|
+
const reloadsBeforeRestore = reloaded;
|
|
89
|
+
windowService['handlePageShow']({ persisted: true } as PageTransitionEvent);
|
|
90
|
+
assert(reloadsBeforeRestore === 0, 'a regular page show should not reload');
|
|
91
|
+
assert(reloaded === 1, 'a restored page should reload');
|
|
92
|
+
assert(windowService['collectContributionUnloadVetoes']().length === 0, 'the recovery reload should not be vetoed');
|
|
93
|
+
});
|
|
94
|
+
it('onUnload should fire at most once, even if `pagehide` fires repeatedly', () => {
|
|
95
|
+
const windowService = setupWindowService('never', []);
|
|
96
|
+
let fired = 0;
|
|
97
|
+
windowService.onUnload(() => fired++);
|
|
98
|
+
windowService['handlePageHide']();
|
|
99
|
+
windowService['handlePageHide']();
|
|
100
|
+
assert(fired === 1, `onUnload should have fired once, but fired ${fired} time(s)`);
|
|
101
|
+
});
|
|
80
102
|
});
|
|
@@ -30,6 +30,7 @@ export class DefaultWindowService implements WindowService, FrontendApplicationC
|
|
|
30
30
|
|
|
31
31
|
protected frontendApplication: FrontendApplication;
|
|
32
32
|
protected allowVetoes = true;
|
|
33
|
+
protected unloaded = false;
|
|
33
34
|
|
|
34
35
|
protected onUnloadEmitter = new Emitter<void>();
|
|
35
36
|
get onUnload(): Event<void> {
|
|
@@ -103,12 +104,43 @@ export class DefaultWindowService implements WindowService, FrontendApplicationC
|
|
|
103
104
|
* Implement the mechanism to detect unloading of the page.
|
|
104
105
|
*/
|
|
105
106
|
protected registerUnloadListeners(): void {
|
|
107
|
+
// If `beforeunload` is cancelled and the user stays, the page is not unloaded, so `pagehide` is not fired.
|
|
106
108
|
window.addEventListener('beforeunload', event => this.handleBeforeUnloadEvent(event));
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
this.registerPageHideListener();
|
|
110
|
+
window.addEventListener('pageshow', event => this.handlePageShow(event));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* `pagehide` is used instead of the deprecated `unload` event, which Chrome may block
|
|
115
|
+
* via permissions policy (https://developer.chrome.com/docs/web-platform/deprecating-unload),
|
|
116
|
+
* silently skipping the handler and thus e.g. losing the layout.
|
|
117
|
+
*/
|
|
118
|
+
protected registerPageHideListener(): void {
|
|
119
|
+
window.addEventListener('pagehide', () => this.handlePageHide());
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* `pagehide` also fires when the page enters the back/forward cache, in which case the frontend
|
|
124
|
+
* has already shut down (state saved, connections closed). Reload to get a working application
|
|
125
|
+
* again if the page is restored from the cache. That shutdown already happened, so the reload
|
|
126
|
+
* must not be vetoed by the contributions.
|
|
127
|
+
*/
|
|
128
|
+
protected handlePageShow(event: PageTransitionEvent): void {
|
|
129
|
+
if (event.persisted) {
|
|
130
|
+
this.setSafeToShutDown();
|
|
131
|
+
this.reload();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Fires {@link onUnload} at most once per document: `pagehide` fires again for the reload that
|
|
137
|
+
* recovers a page restored from the back/forward cache, but the application is already stopped.
|
|
138
|
+
*/
|
|
139
|
+
protected handlePageHide(): void {
|
|
140
|
+
if (!this.unloaded) {
|
|
141
|
+
this.unloaded = true;
|
|
142
|
+
this.onUnloadEmitter.fire();
|
|
143
|
+
}
|
|
112
144
|
}
|
|
113
145
|
|
|
114
146
|
async isSafeToShutDown(stopReason: StopReason): Promise<boolean> {
|
|
@@ -55,6 +55,13 @@ export interface SecondaryWindowService {
|
|
|
55
55
|
*/
|
|
56
56
|
createSecondaryWindow(widget: ExtractableWidget, shell: ApplicationShell): SecondaryWindow | Window | undefined;
|
|
57
57
|
readonly onWindowOpened: Event<Window>;
|
|
58
|
+
/**
|
|
59
|
+
* Emitted once a secondary window's final document has finished loading. Listeners can safely
|
|
60
|
+
* modify the window's document, unlike with {@link onWindowOpened}: at that point the window
|
|
61
|
+
* still shows the initial `about:blank` document, and anything added to it is discarded when
|
|
62
|
+
* the final document replaces it.
|
|
63
|
+
*/
|
|
64
|
+
readonly onWindowLoaded: Event<Window>;
|
|
58
65
|
readonly onWindowClosed: Event<Window>;
|
|
59
66
|
readonly beforeWidgetRestore: Event<[Widget, Window]>;
|
|
60
67
|
|
|
@@ -56,7 +56,7 @@ export interface WindowService {
|
|
|
56
56
|
focus(): void;
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
* Fires when the
|
|
59
|
+
* Fires when the application is shutting down. On this event, the frontend application can save its state and release resource.
|
|
60
60
|
* Saving the state and releasing any resources must be a synchronous call. Any asynchronous calls invoked after emitting this event might be ignored.
|
|
61
61
|
*/
|
|
62
62
|
readonly onUnload: Event<void>;
|
|
@@ -24,6 +24,33 @@ export class FrontendOnlyConnectionSource implements ConnectionSource {
|
|
|
24
24
|
onConnectionDidOpen = new Emitter<Channel>().event;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Proxy factory for services without a frontend-only implementation. There is no backend,
|
|
29
|
+
* so no channel will ever open and requests would otherwise be queued forever, blocking
|
|
30
|
+
* any caller that awaits the result, e.g. during application startup.
|
|
31
|
+
* Instead, requests fail immediately and notifications (fire-and-forget) resolve silently.
|
|
32
|
+
*/
|
|
33
|
+
export class FrontendOnlyRpcProxyFactory<T extends object> extends RpcProxyFactory<T> {
|
|
34
|
+
|
|
35
|
+
constructor(protected readonly path: string, target?: object) {
|
|
36
|
+
super(target);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
override get(target: T, p: PropertyKey, receiver: unknown): unknown {
|
|
40
|
+
if (typeof p !== 'string') {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
// members handled by the base proxy without a connection
|
|
44
|
+
if (['setClient', 'getClient', 'onDidOpenConnection', 'onDidCloseConnection', 'then', 'toJSON'].includes(p)) {
|
|
45
|
+
return super.get(target, p, receiver);
|
|
46
|
+
}
|
|
47
|
+
if (this.isNotification(p)) {
|
|
48
|
+
return () => Promise.resolve();
|
|
49
|
+
}
|
|
50
|
+
return () => Promise.reject(new Error(`Request '${p}' on service '${this.path}' is unavailable: no backend in frontend-only mode.`));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
27
54
|
@injectable()
|
|
28
55
|
export class FrontendOnlyServiceConnectionProvider extends ServiceConnectionProvider {
|
|
29
56
|
|
|
@@ -33,10 +60,12 @@ export class FrontendOnlyServiceConnectionProvider extends ServiceConnectionProv
|
|
|
33
60
|
onSocketDidOpen = Event.None;
|
|
34
61
|
onSocketDidClose = Event.None;
|
|
35
62
|
onIncomingMessageActivity = Event.None;
|
|
36
|
-
override createProxy<T extends object>(path:
|
|
63
|
+
override createProxy<T extends object>(path: string, target?: object): RpcProxy<T> {
|
|
37
64
|
this.logger.debug(`[Frontend-Only Fallback] Created proxy connection for ${path}`);
|
|
38
|
-
|
|
39
|
-
|
|
65
|
+
if (target instanceof RpcProxyFactory) {
|
|
66
|
+
return target.createProxy();
|
|
67
|
+
}
|
|
68
|
+
return new FrontendOnlyRpcProxyFactory<T>(path, target).createProxy();
|
|
40
69
|
}
|
|
41
70
|
override listen(path: string, handler: ServiceConnectionProvider.ConnectionHandler, reconnect: boolean): void {
|
|
42
71
|
this.logger.debug('[Frontend-Only Fallback] Listen to websocket connection requested');
|