@jupyterlab/apputils-extension 4.6.0-alpha.5 → 4.6.0-beta.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/lib/announcements.js +2 -1
- package/lib/announcements.js.map +1 -1
- package/lib/index.js +21 -8
- package/lib/index.js.map +1 -1
- package/lib/licensesplugin.js +1 -0
- package/lib/licensesplugin.js.map +1 -1
- package/lib/movesectionsplugin.d.ts +7 -0
- package/lib/movesectionsplugin.js +462 -0
- package/lib/movesectionsplugin.js.map +1 -0
- package/lib/notificationplugin.js +1 -0
- package/lib/notificationplugin.js.map +1 -1
- package/lib/palette.d.ts +1 -1
- package/lib/palette.js +8 -6
- package/lib/palette.js.map +1 -1
- package/lib/shortcuts.js +1 -0
- package/lib/shortcuts.js.map +1 -1
- package/package.json +16 -16
- package/src/announcements.ts +2 -1
- package/src/index.ts +24 -11
- package/src/licensesplugin.ts +1 -0
- package/src/movesectionsplugin.ts +621 -0
- package/src/notificationplugin.tsx +1 -0
- package/src/palette.ts +8 -10
- package/src/shortcuts.tsx +1 -0
- package/style/notification.css +3 -1
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
JupyterFrontEnd,
|
|
8
|
+
JupyterFrontEndPlugin
|
|
9
|
+
} from '@jupyterlab/application';
|
|
10
|
+
import {
|
|
11
|
+
type IMovableSectionDestination,
|
|
12
|
+
IMovableSectionRegistry,
|
|
13
|
+
type IMovableSectionSource
|
|
14
|
+
} from '@jupyterlab/apputils';
|
|
15
|
+
import { IStateDB } from '@jupyterlab/statedb';
|
|
16
|
+
import { ITranslator } from '@jupyterlab/translation';
|
|
17
|
+
import type {
|
|
18
|
+
ReadonlyPartialJSONObject,
|
|
19
|
+
ReadonlyPartialJSONValue
|
|
20
|
+
} from '@lumino/coreutils';
|
|
21
|
+
import type { AccordionLayout, AccordionPanel, Widget } from '@lumino/widgets';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* State DB key for persisting moved sections.
|
|
25
|
+
*/
|
|
26
|
+
const MOVE_STATE_KEY = 'section-mover:layout';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Shape of the persisted state.
|
|
30
|
+
* Keyed by target panel ID; each entry records ordered sections, their
|
|
31
|
+
* absolute index in the destination accordion, and whether each was
|
|
32
|
+
* collapsed when the state was saved.
|
|
33
|
+
*/
|
|
34
|
+
interface IMoveSectionsState {
|
|
35
|
+
[targetId: string]: Array<{
|
|
36
|
+
sourceId: string;
|
|
37
|
+
sectionId: string;
|
|
38
|
+
index?: number;
|
|
39
|
+
collapsed?: true;
|
|
40
|
+
}>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
namespace CommandIDs {
|
|
44
|
+
export const moveSectionTo = 'apputils:move-section-to';
|
|
45
|
+
export const moveSectionBack = 'apputils:move-section-back';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Plugin that moves sections between any registered source sidebar
|
|
50
|
+
* and target panel. Sources and targets announce themselves via
|
|
51
|
+
* IMovableSectionRegistry.
|
|
52
|
+
*/
|
|
53
|
+
export const moveSectionsPlugin: JupyterFrontEndPlugin<void> = {
|
|
54
|
+
id: '@jupyterlab/apputils-extension:move-sections',
|
|
55
|
+
description:
|
|
56
|
+
'Enables moving sections between any registered source and target sidebars.',
|
|
57
|
+
requires: [ITranslator],
|
|
58
|
+
optional: [IMovableSectionRegistry, IStateDB],
|
|
59
|
+
autoStart: true,
|
|
60
|
+
activate: (
|
|
61
|
+
app: JupyterFrontEnd,
|
|
62
|
+
translator: ITranslator,
|
|
63
|
+
registry: IMovableSectionRegistry | null,
|
|
64
|
+
stateDB: IStateDB | null
|
|
65
|
+
): void => {
|
|
66
|
+
if (!registry) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const trans = translator.load('jupyterlab');
|
|
71
|
+
|
|
72
|
+
// section title element (in source sidebar) → section identity
|
|
73
|
+
const titleToSection = new WeakMap<
|
|
74
|
+
HTMLElement,
|
|
75
|
+
{ sourceId: string; sectionId: string }
|
|
76
|
+
>();
|
|
77
|
+
|
|
78
|
+
const widgetToInfo = new Map<
|
|
79
|
+
Widget,
|
|
80
|
+
{
|
|
81
|
+
sourceId: string;
|
|
82
|
+
sectionId: string;
|
|
83
|
+
sourceLabel: string;
|
|
84
|
+
targetId: string;
|
|
85
|
+
}
|
|
86
|
+
>();
|
|
87
|
+
|
|
88
|
+
// For context-menu lookup
|
|
89
|
+
const hostedTitleToWidget = new WeakMap<HTMLElement, Widget>();
|
|
90
|
+
|
|
91
|
+
// Keyed by accordion instance so a recreated accordion (after the last
|
|
92
|
+
// hosted section leaves and the FileBrowser disposes it) gets fresh
|
|
93
|
+
// listeners on the next move-in.
|
|
94
|
+
const dragSetupDone = new WeakSet<AccordionPanel>();
|
|
95
|
+
const pendingSections = new Map<
|
|
96
|
+
string,
|
|
97
|
+
Map<string, { targetId: string; collapsed: boolean; index?: number }>
|
|
98
|
+
>();
|
|
99
|
+
|
|
100
|
+
// Capture last right-clicked element before the context menu opens
|
|
101
|
+
let lastContextEl: HTMLElement | null = null;
|
|
102
|
+
document.addEventListener(
|
|
103
|
+
'contextmenu',
|
|
104
|
+
(ev: MouseEvent) => {
|
|
105
|
+
lastContextEl = ev.target as HTMLElement;
|
|
106
|
+
},
|
|
107
|
+
true
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const saveState = async (): Promise<void> => {
|
|
111
|
+
if (!stateDB) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const state: IMoveSectionsState = {};
|
|
115
|
+
for (const [targetId, { panel }] of registry.getTargets()) {
|
|
116
|
+
const accordion = panel.accordionPanel;
|
|
117
|
+
const allWidgets = accordion ? Array.from(accordion.widgets) : [];
|
|
118
|
+
const sections = panel.sections
|
|
119
|
+
.map(w => {
|
|
120
|
+
const info = widgetToInfo.get(w);
|
|
121
|
+
if (!info || info.targetId !== targetId) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
// Detect collapsed state: widget content is hidden when collapsed
|
|
125
|
+
const isCollapsed = w.isHidden;
|
|
126
|
+
const index = allWidgets.indexOf(w);
|
|
127
|
+
return {
|
|
128
|
+
sourceId: info.sourceId,
|
|
129
|
+
sectionId: info.sectionId,
|
|
130
|
+
...(index >= 0 ? { index } : {}),
|
|
131
|
+
...(isCollapsed ? { collapsed: true as const } : {})
|
|
132
|
+
};
|
|
133
|
+
})
|
|
134
|
+
.filter((e): e is NonNullable<typeof e> => e !== null);
|
|
135
|
+
// Apply in saved accordion order so insertWidget on restore lands in
|
|
136
|
+
// the right spot relative to non-moved widgets.
|
|
137
|
+
sections.sort(
|
|
138
|
+
(a, b) =>
|
|
139
|
+
(a.index ?? Number.MAX_SAFE_INTEGER) -
|
|
140
|
+
(b.index ?? Number.MAX_SAFE_INTEGER)
|
|
141
|
+
);
|
|
142
|
+
if (sections.length > 0) {
|
|
143
|
+
state[targetId] = sections;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
await stateDB.save(
|
|
147
|
+
MOVE_STATE_KEY,
|
|
148
|
+
state as unknown as ReadonlyPartialJSONValue
|
|
149
|
+
);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Re-appending all titles in logical to fix keyboard Tab navigation.
|
|
153
|
+
const syncTitleDOMOrder = (accordion: AccordionPanel): void => {
|
|
154
|
+
const layout = accordion.layout as AccordionLayout;
|
|
155
|
+
for (const title of layout.titles) {
|
|
156
|
+
accordion.node.appendChild(title);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const addDragHandle = (widget: Widget, accordion: AccordionPanel): void => {
|
|
161
|
+
const idx = Array.from(accordion.widgets).indexOf(widget);
|
|
162
|
+
if (idx < 0) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const titleEl = accordion.titles[idx];
|
|
166
|
+
if (!titleEl.querySelector('.jp-movable-section-dragHandle')) {
|
|
167
|
+
const handle = document.createElement('span');
|
|
168
|
+
handle.className = 'jp-movable-section-dragHandle';
|
|
169
|
+
titleEl.prepend(handle);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const setupAccordionDrag = (accordion: AccordionPanel): void => {
|
|
174
|
+
if (dragSetupDone.has(accordion)) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
dragSetupDone.add(accordion);
|
|
178
|
+
|
|
179
|
+
// Persist collapse/expand state whenever the user toggles a section.
|
|
180
|
+
accordion.expansionToggled.connect(() => void saveState());
|
|
181
|
+
|
|
182
|
+
let draggedWidget: Widget | null = null;
|
|
183
|
+
let startY = 0;
|
|
184
|
+
let isDragging = false;
|
|
185
|
+
|
|
186
|
+
const indicator = document.createElement('div');
|
|
187
|
+
indicator.className = 'jp-movable-section-dropIndicator';
|
|
188
|
+
indicator.style.display = 'none';
|
|
189
|
+
accordion.node.appendChild(indicator);
|
|
190
|
+
|
|
191
|
+
const getTargetSlot = (clientY: number): number => {
|
|
192
|
+
const layout = accordion.layout as AccordionLayout;
|
|
193
|
+
const titles = Array.from(layout.titles);
|
|
194
|
+
for (let i = 0; i < titles.length; i++) {
|
|
195
|
+
const rect = titles[i].getBoundingClientRect();
|
|
196
|
+
if (clientY < rect.top + rect.height / 2) {
|
|
197
|
+
return i;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return titles.length;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const showIndicator = (targetSlot: number): void => {
|
|
204
|
+
const layout = accordion.layout as AccordionLayout;
|
|
205
|
+
const titles = Array.from(layout.titles);
|
|
206
|
+
const panelRect = accordion.node.getBoundingClientRect();
|
|
207
|
+
let top: number;
|
|
208
|
+
if (targetSlot < titles.length) {
|
|
209
|
+
top = titles[targetSlot].getBoundingClientRect().top - panelRect.top;
|
|
210
|
+
} else {
|
|
211
|
+
const last = titles[titles.length - 1].getBoundingClientRect();
|
|
212
|
+
top = last.bottom - panelRect.top;
|
|
213
|
+
}
|
|
214
|
+
indicator.style.top = `${top}px`;
|
|
215
|
+
indicator.style.display = 'block';
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const endDrag = (clientY?: number): void => {
|
|
219
|
+
indicator.style.display = 'none';
|
|
220
|
+
accordion.node.classList.remove('jp-mod-dragging');
|
|
221
|
+
if (isDragging && draggedWidget && clientY !== undefined) {
|
|
222
|
+
const currentIdx = Array.from(accordion.widgets).indexOf(
|
|
223
|
+
draggedWidget
|
|
224
|
+
);
|
|
225
|
+
const targetSlot = getTargetSlot(clientY);
|
|
226
|
+
const insertIdx =
|
|
227
|
+
targetSlot > currentIdx ? targetSlot - 1 : targetSlot;
|
|
228
|
+
if (insertIdx !== currentIdx) {
|
|
229
|
+
accordion.insertWidget(insertIdx, draggedWidget);
|
|
230
|
+
syncTitleDOMOrder(accordion);
|
|
231
|
+
void saveState();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
draggedWidget = null;
|
|
235
|
+
isDragging = false;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
accordion.node.addEventListener('pointerdown', (event: PointerEvent) => {
|
|
239
|
+
const target = event.target as HTMLElement;
|
|
240
|
+
const titleEl = target.closest(
|
|
241
|
+
'.jp-AccordionPanel-title'
|
|
242
|
+
) as HTMLElement | null;
|
|
243
|
+
if (!titleEl || !target.closest('.jp-movable-section-dragHandle')) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const layout = accordion.layout as AccordionLayout;
|
|
247
|
+
const idx = Array.from(layout.titles).indexOf(titleEl);
|
|
248
|
+
if (idx < 0) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
draggedWidget = accordion.widgets[idx];
|
|
252
|
+
startY = event.clientY;
|
|
253
|
+
accordion.node.setPointerCapture(event.pointerId);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
accordion.node.addEventListener('pointermove', (event: PointerEvent) => {
|
|
257
|
+
if (!draggedWidget) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (!isDragging && Math.abs(event.clientY - startY) > 5) {
|
|
261
|
+
isDragging = true;
|
|
262
|
+
accordion.node.classList.add('jp-mod-dragging');
|
|
263
|
+
}
|
|
264
|
+
if (isDragging) {
|
|
265
|
+
showIndicator(getTargetSlot(event.clientY));
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
accordion.node.addEventListener('pointerup', (event: PointerEvent) => {
|
|
270
|
+
endDrag(event.clientY);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
accordion.node.addEventListener('pointercancel', () => {
|
|
274
|
+
endDrag();
|
|
275
|
+
});
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// Core move operations
|
|
279
|
+
|
|
280
|
+
const applyMoveToTarget = (
|
|
281
|
+
widget: Widget,
|
|
282
|
+
sourceId: string,
|
|
283
|
+
sectionId: string,
|
|
284
|
+
sourceLabel: string,
|
|
285
|
+
targetId: string,
|
|
286
|
+
targetPanel: IMovableSectionDestination,
|
|
287
|
+
collapsed = false,
|
|
288
|
+
index?: number
|
|
289
|
+
): void => {
|
|
290
|
+
// Capture hidden state before reparenting
|
|
291
|
+
const wasHidden = widget.isHidden;
|
|
292
|
+
targetPanel.addSection(widget);
|
|
293
|
+
|
|
294
|
+
widgetToInfo.set(widget, { sourceId, sectionId, sourceLabel, targetId });
|
|
295
|
+
|
|
296
|
+
const accordion = targetPanel.accordionPanel;
|
|
297
|
+
if (accordion) {
|
|
298
|
+
if (typeof index === 'number') {
|
|
299
|
+
const total = accordion.widgets.length;
|
|
300
|
+
const target = Math.max(0, Math.min(index, total - 1));
|
|
301
|
+
if (accordion.widgets[target] !== widget) {
|
|
302
|
+
accordion.insertWidget(target, widget);
|
|
303
|
+
syncTitleDOMOrder(accordion);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
setupAccordionDrag(accordion);
|
|
308
|
+
addDragHandle(widget, accordion);
|
|
309
|
+
|
|
310
|
+
const idx = Array.from(accordion.widgets).indexOf(widget);
|
|
311
|
+
if (idx >= 0) {
|
|
312
|
+
const hostedTitle = accordion.titles[idx];
|
|
313
|
+
hostedTitle.classList.add('jp-hosted-section');
|
|
314
|
+
hostedTitleToWidget.set(hostedTitle, widget);
|
|
315
|
+
|
|
316
|
+
if (wasHidden || collapsed) {
|
|
317
|
+
// Ensure the content widget is actually hidden (needed when
|
|
318
|
+
// restoring state where the widget was expanded in the source).
|
|
319
|
+
widget.hide();
|
|
320
|
+
hostedTitle.classList.remove('lm-mod-expanded');
|
|
321
|
+
hostedTitle.setAttribute('aria-expanded', 'false');
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Move a section from its source sidebar to a target panel.
|
|
329
|
+
* Returns true on success, false if the section is not yet available.
|
|
330
|
+
*/
|
|
331
|
+
const moveSection = (
|
|
332
|
+
sourceId: string,
|
|
333
|
+
sectionId: string,
|
|
334
|
+
targetId: string,
|
|
335
|
+
collapsed = false,
|
|
336
|
+
index?: number
|
|
337
|
+
): boolean => {
|
|
338
|
+
const sourceEntry = registry.getSources().get(sourceId);
|
|
339
|
+
const targetEntry = registry.getTargets().get(targetId);
|
|
340
|
+
if (!sourceEntry || !targetEntry) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
const widget = sourceEntry.sidebar.removeSectionById(sectionId);
|
|
344
|
+
if (!widget) {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
applyMoveToTarget(
|
|
348
|
+
widget,
|
|
349
|
+
sourceId,
|
|
350
|
+
sectionId,
|
|
351
|
+
sourceEntry.label,
|
|
352
|
+
targetId,
|
|
353
|
+
targetEntry.panel,
|
|
354
|
+
collapsed,
|
|
355
|
+
index
|
|
356
|
+
);
|
|
357
|
+
void saveState();
|
|
358
|
+
return true;
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const moveSectionBack = (widget: Widget): void => {
|
|
362
|
+
const info = widgetToInfo.get(widget);
|
|
363
|
+
if (!info) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const sourceEntry = registry.getSources().get(info.sourceId);
|
|
367
|
+
const targetEntry = registry.getTargets().get(info.targetId);
|
|
368
|
+
if (!sourceEntry || !targetEntry) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const isHidden = widget.isHidden;
|
|
372
|
+
targetEntry.panel.removeSectionWidget(widget);
|
|
373
|
+
sourceEntry.sidebar.reinsertSection(widget);
|
|
374
|
+
widgetToInfo.delete(widget);
|
|
375
|
+
|
|
376
|
+
// After reinsertion the accordion assigns a fresh title element.
|
|
377
|
+
// Re-register it so the context menu works on this section again.
|
|
378
|
+
const restored = sourceEntry.sidebar
|
|
379
|
+
.getSections()
|
|
380
|
+
.find(s => s.id === info.sectionId);
|
|
381
|
+
if (restored) {
|
|
382
|
+
restored.titleNode.classList.add('jp-movable-section');
|
|
383
|
+
titleToSection.set(restored.titleNode, {
|
|
384
|
+
sourceId: info.sourceId,
|
|
385
|
+
sectionId: info.sectionId
|
|
386
|
+
});
|
|
387
|
+
if (isHidden) {
|
|
388
|
+
restored.widget.hide();
|
|
389
|
+
restored.titleNode.classList.remove('lm-mod-expanded');
|
|
390
|
+
restored.titleNode.setAttribute('aria-expanded', 'false');
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
void saveState();
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
// Source / target wiring
|
|
398
|
+
|
|
399
|
+
const setupSource = (
|
|
400
|
+
sourceId: string,
|
|
401
|
+
source: IMovableSectionSource
|
|
402
|
+
): void => {
|
|
403
|
+
for (const section of source.getSections()) {
|
|
404
|
+
section.titleNode.classList.add('jp-movable-section');
|
|
405
|
+
titleToSection.set(section.titleNode, {
|
|
406
|
+
sourceId,
|
|
407
|
+
sectionId: section.id
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
source.sectionAdded.connect((_sender, section) => {
|
|
412
|
+
section.titleNode.classList.add('jp-movable-section');
|
|
413
|
+
titleToSection.set(section.titleNode, {
|
|
414
|
+
sourceId,
|
|
415
|
+
sectionId: section.id
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// Fulfill any pending state restoration for this section
|
|
419
|
+
const pending = pendingSections.get(sourceId);
|
|
420
|
+
if (pending?.has(section.id)) {
|
|
421
|
+
const { targetId, collapsed, index } = pending.get(section.id)!;
|
|
422
|
+
pending.delete(section.id);
|
|
423
|
+
if (pending.size === 0) {
|
|
424
|
+
pendingSections.delete(sourceId);
|
|
425
|
+
}
|
|
426
|
+
moveSection(sourceId, section.id, targetId, collapsed, index);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const setupTarget = (
|
|
432
|
+
targetId: string,
|
|
433
|
+
targetLabel: string,
|
|
434
|
+
panel: IMovableSectionDestination
|
|
435
|
+
): void => {
|
|
436
|
+
app.contextMenu.addItem({
|
|
437
|
+
command: CommandIDs.moveSectionTo,
|
|
438
|
+
args: { targetId, targetLabel },
|
|
439
|
+
selector: '.jp-movable-section',
|
|
440
|
+
rank: 10
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
if (panel.accordionPanel && panel.sections.length > 0) {
|
|
444
|
+
setupAccordionDrag(panel.accordionPanel);
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
app.commands.addCommand(CommandIDs.moveSectionTo, {
|
|
449
|
+
label: (args: ReadonlyPartialJSONObject) =>
|
|
450
|
+
trans.__('Move to %1', (args.targetLabel as string) ?? ''),
|
|
451
|
+
describedBy: {
|
|
452
|
+
args: {
|
|
453
|
+
type: 'object',
|
|
454
|
+
properties: {
|
|
455
|
+
targetId: { type: 'string' },
|
|
456
|
+
targetLabel: { type: 'string' }
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
isVisible: (args: ReadonlyPartialJSONObject) => {
|
|
461
|
+
if (!lastContextEl) {
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
const titleEl = lastContextEl.closest(
|
|
465
|
+
'.jp-movable-section'
|
|
466
|
+
) as HTMLElement | null;
|
|
467
|
+
if (!titleEl) {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
// A title currently hosting a moved-in section is not a valid source —
|
|
471
|
+
// it should only offer "Move back".
|
|
472
|
+
if (titleEl.classList.contains('jp-hosted-section')) {
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
const sectionInfo = titleToSection.get(titleEl);
|
|
476
|
+
if (!sectionInfo) {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
const targetId = args.targetId as string;
|
|
480
|
+
if (!targetId) {
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
// Don't offer to move a section to the same panel it originates from
|
|
484
|
+
const sourceEntry = registry.getSources().get(sectionInfo.sourceId);
|
|
485
|
+
const targetEntry = registry.getTargets().get(targetId);
|
|
486
|
+
if (!sourceEntry || !targetEntry) {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
return (
|
|
490
|
+
(sourceEntry.sidebar as unknown) !== (targetEntry.panel as unknown)
|
|
491
|
+
);
|
|
492
|
+
},
|
|
493
|
+
execute: (args: ReadonlyPartialJSONObject) => {
|
|
494
|
+
const targetId = args.targetId as string;
|
|
495
|
+
if (!targetId || !lastContextEl) {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
const titleEl = lastContextEl.closest(
|
|
499
|
+
'.jp-movable-section'
|
|
500
|
+
) as HTMLElement | null;
|
|
501
|
+
if (!titleEl || titleEl.classList.contains('jp-hosted-section')) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
const sectionInfo = titleToSection.get(titleEl);
|
|
505
|
+
if (!sectionInfo) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
moveSection(sectionInfo.sourceId, sectionInfo.sectionId, targetId);
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
app.commands.addCommand(CommandIDs.moveSectionBack, {
|
|
513
|
+
label: () => {
|
|
514
|
+
if (!lastContextEl) {
|
|
515
|
+
return trans.__('Move back');
|
|
516
|
+
}
|
|
517
|
+
const titleEl = lastContextEl.closest(
|
|
518
|
+
'.jp-hosted-section'
|
|
519
|
+
) as HTMLElement | null;
|
|
520
|
+
if (!titleEl) {
|
|
521
|
+
return trans.__('Move back');
|
|
522
|
+
}
|
|
523
|
+
const widget = hostedTitleToWidget.get(titleEl);
|
|
524
|
+
if (!widget) {
|
|
525
|
+
return trans.__('Move back');
|
|
526
|
+
}
|
|
527
|
+
const info = widgetToInfo.get(widget);
|
|
528
|
+
return info
|
|
529
|
+
? trans.__('Move back to %1', info.sourceLabel)
|
|
530
|
+
: trans.__('Move back');
|
|
531
|
+
},
|
|
532
|
+
describedBy: {
|
|
533
|
+
args: {
|
|
534
|
+
type: 'object',
|
|
535
|
+
properties: {}
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
isVisible: () => {
|
|
539
|
+
if (!lastContextEl) {
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
const titleEl = lastContextEl.closest(
|
|
543
|
+
'.jp-hosted-section'
|
|
544
|
+
) as HTMLElement | null;
|
|
545
|
+
return titleEl ? hostedTitleToWidget.has(titleEl) : false;
|
|
546
|
+
},
|
|
547
|
+
execute: () => {
|
|
548
|
+
if (!lastContextEl) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const titleEl = lastContextEl.closest(
|
|
552
|
+
'.jp-hosted-section'
|
|
553
|
+
) as HTMLElement | null;
|
|
554
|
+
if (!titleEl) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const widget = hostedTitleToWidget.get(titleEl);
|
|
558
|
+
if (widget) {
|
|
559
|
+
moveSectionBack(widget);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
app.contextMenu.addItem({
|
|
565
|
+
command: CommandIDs.moveSectionBack,
|
|
566
|
+
selector: '.jp-hosted-section',
|
|
567
|
+
rank: 10
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// Bootstrap from registry
|
|
571
|
+
|
|
572
|
+
for (const [id, { sidebar }] of registry.getSources()) {
|
|
573
|
+
setupSource(id, sidebar);
|
|
574
|
+
}
|
|
575
|
+
for (const [id, { label, panel }] of registry.getTargets()) {
|
|
576
|
+
setupTarget(id, label, panel);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
registry.sourcePanelRegistered.connect((_sender, { id, sidebar }) => {
|
|
580
|
+
setupSource(id, sidebar);
|
|
581
|
+
});
|
|
582
|
+
registry.targetPanelRegistered.connect((_sender, { id, label, panel }) => {
|
|
583
|
+
setupTarget(id, label, panel);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
// State restoration
|
|
587
|
+
|
|
588
|
+
if (stateDB) {
|
|
589
|
+
void stateDB.fetch(MOVE_STATE_KEY).then(value => {
|
|
590
|
+
if (!value) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const state = value as IMoveSectionsState;
|
|
594
|
+
|
|
595
|
+
for (const [targetId, sections] of Object.entries(state)) {
|
|
596
|
+
for (const { sourceId, sectionId, collapsed, index } of sections) {
|
|
597
|
+
if (
|
|
598
|
+
!moveSection(
|
|
599
|
+
sourceId,
|
|
600
|
+
sectionId,
|
|
601
|
+
targetId,
|
|
602
|
+
collapsed ?? false,
|
|
603
|
+
index
|
|
604
|
+
)
|
|
605
|
+
) {
|
|
606
|
+
// Section not yet available — wait for sectionAdded signal
|
|
607
|
+
if (!pendingSections.has(sourceId)) {
|
|
608
|
+
pendingSections.set(sourceId, new Map());
|
|
609
|
+
}
|
|
610
|
+
pendingSections.get(sourceId)!.set(sectionId, {
|
|
611
|
+
targetId,
|
|
612
|
+
collapsed: collapsed ?? false,
|
|
613
|
+
index
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
};
|
package/src/palette.ts
CHANGED
|
@@ -38,6 +38,10 @@ export class Palette implements ICommandPalette {
|
|
|
38
38
|
const trans = this.translator.load('jupyterlab');
|
|
39
39
|
this._palette = palette;
|
|
40
40
|
this._palette.title.label = '';
|
|
41
|
+
this._palette.title.dataset = {
|
|
42
|
+
...this._palette.title.dataset,
|
|
43
|
+
jpTabLabel: trans.__('Commands')
|
|
44
|
+
};
|
|
41
45
|
this._palette.title.caption = trans.__('Command Palette');
|
|
42
46
|
}
|
|
43
47
|
|
|
@@ -90,7 +94,7 @@ export namespace Palette {
|
|
|
90
94
|
): ICommandPalette {
|
|
91
95
|
const { commands, shell } = app;
|
|
92
96
|
const trans = translator.load('jupyterlab');
|
|
93
|
-
const palette = Private.createPalette(app
|
|
97
|
+
const palette = Private.createPalette(app);
|
|
94
98
|
const modalPalette = new ModalCommandPalette({
|
|
95
99
|
commandPalette: palette,
|
|
96
100
|
restore: () => {
|
|
@@ -183,10 +187,9 @@ export namespace Palette {
|
|
|
183
187
|
*/
|
|
184
188
|
export function restore(
|
|
185
189
|
app: JupyterFrontEnd,
|
|
186
|
-
restorer: ILayoutRestorer
|
|
187
|
-
translator: ITranslator
|
|
190
|
+
restorer: ILayoutRestorer
|
|
188
191
|
): void {
|
|
189
|
-
const palette = Private.createPalette(app
|
|
192
|
+
const palette = Private.createPalette(app);
|
|
190
193
|
// Let the application restorer track the command palette for restoration of
|
|
191
194
|
// application state (e.g. setting the command palette as the current side bar
|
|
192
195
|
// widget).
|
|
@@ -206,10 +209,7 @@ namespace Private {
|
|
|
206
209
|
/**
|
|
207
210
|
* Create the application-wide command palette.
|
|
208
211
|
*/
|
|
209
|
-
export function createPalette(
|
|
210
|
-
app: JupyterFrontEnd,
|
|
211
|
-
translator: ITranslator
|
|
212
|
-
): CommandPalette {
|
|
212
|
+
export function createPalette(app: JupyterFrontEnd): CommandPalette {
|
|
213
213
|
if (!palette) {
|
|
214
214
|
// use a renderer tweaked to use inline svg icons
|
|
215
215
|
palette = new CommandPalette({
|
|
@@ -218,8 +218,6 @@ namespace Private {
|
|
|
218
218
|
});
|
|
219
219
|
palette.id = 'command-palette';
|
|
220
220
|
palette.title.icon = paletteIcon;
|
|
221
|
-
const trans = translator.load('jupyterlab');
|
|
222
|
-
palette.title.label = trans.__('Commands');
|
|
223
221
|
}
|
|
224
222
|
|
|
225
223
|
return palette;
|
package/src/shortcuts.tsx
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Copyright (c) Jupyter Development Team.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
5
6
|
|
|
6
7
|
import { CommandRegistry } from '@lumino/commands';
|
|
7
8
|
import { Selector } from '@lumino/domutils';
|
package/style/notification.css
CHANGED
|
@@ -200,7 +200,9 @@
|
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
.jp-toast-button:focus {
|
|
203
|
-
outline:
|
|
203
|
+
outline-width: var(--jp-focus-outline-width);
|
|
204
|
+
outline-color: var(--jp-reject-color-normal, var(--jp-layout-color2));
|
|
205
|
+
outline-style: solid;
|
|
204
206
|
outline-offset: 1px;
|
|
205
207
|
-moz-outline-radius: 0;
|
|
206
208
|
}
|