jupyterlab_advanced_image_viewer_extension 1.1.3 → 1.1.5
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 +1 -1
- package/lib/controller.js +4 -0
- package/lib/index.js +33 -5
- package/package.json +1 -1
- package/src/__tests__/controller.spec.ts +17 -0
- package/src/controller.ts +4 -0
- package/src/index.ts +36 -5
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ The viewer toolbar adds zoom in / out / fit controls and a `help` link that list
|
|
|
23
23
|
- **Arrow-key navigation** - Left and Right open the previous and next image in the same folder, in the order the file browser lists them, advancing within one viewer instead of opening new tabs
|
|
24
24
|
- **Copy to clipboard** - right-click a raster image (PNG/JPG/…) and choose "Copy to Clipboard" to copy it as PNG to the system clipboard
|
|
25
25
|
- **Refresh** - a toolbar refresh button reloads the image from disk, the same control the HTML viewer provides
|
|
26
|
-
- **WebP support** - `.webp` files open in the image viewer with every feature above; stock JupyterLab
|
|
26
|
+
- **WebP and AVIF support** - `.webp` and `.avif` files open in the image viewer with every feature above; stock JupyterLab does not open them and shows a File Load Error
|
|
27
27
|
|
|
28
28
|
## Requirements
|
|
29
29
|
|
package/lib/controller.js
CHANGED
|
@@ -53,7 +53,11 @@ export class ViewerController {
|
|
|
53
53
|
this.zoomAt(factor, e.clientX - r.left, e.clientY - r.top);
|
|
54
54
|
};
|
|
55
55
|
this.onDown = (e) => {
|
|
56
|
+
// preventDefault stops the browser dragging the image out, and with it the
|
|
57
|
+
// focus a click would give the viewer; focus it here, or after a toolbar
|
|
58
|
+
// click the stock keys and Left/Right go to the button instead.
|
|
56
59
|
e.preventDefault();
|
|
60
|
+
this.host.focus();
|
|
57
61
|
this.panning = true;
|
|
58
62
|
this.start = { x: e.clientX, y: e.clientY, tx: this.tx, ty: this.ty };
|
|
59
63
|
this.host.style.cursor = 'grabbing';
|
package/lib/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { PathExt } from '@jupyterlab/coreutils';
|
|
|
3
3
|
import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
|
|
4
4
|
import { IImageTracker } from '@jupyterlab/imageviewer';
|
|
5
5
|
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
6
|
-
import { refreshIcon } from '@jupyterlab/ui-components';
|
|
6
|
+
import { imageIcon, refreshIcon } from '@jupyterlab/ui-components';
|
|
7
7
|
import { Widget } from '@lumino/widgets';
|
|
8
8
|
import { ViewerController } from './controller';
|
|
9
9
|
const PLUGIN_ID = 'jupyterlab_advanced_image_viewer_extension:plugin';
|
|
@@ -22,11 +22,20 @@ const IMAGE_EXTS = new Set([
|
|
|
22
22
|
'.gif',
|
|
23
23
|
'.bmp',
|
|
24
24
|
'.svg',
|
|
25
|
-
'.webp'
|
|
25
|
+
'.webp',
|
|
26
|
+
'.avif'
|
|
26
27
|
]);
|
|
27
28
|
// Raster formats that can be drawn to a canvas and copied as PNG. SVG is
|
|
28
29
|
// excluded - the drawio/SVG extension already adds its own "Copy as PNG".
|
|
29
|
-
const RASTER_EXTS = new Set([
|
|
30
|
+
const RASTER_EXTS = new Set([
|
|
31
|
+
'.png',
|
|
32
|
+
'.jpg',
|
|
33
|
+
'.jpeg',
|
|
34
|
+
'.gif',
|
|
35
|
+
'.bmp',
|
|
36
|
+
'.webp',
|
|
37
|
+
'.avif'
|
|
38
|
+
]);
|
|
30
39
|
const defaults = {
|
|
31
40
|
navEnabled: true,
|
|
32
41
|
zoomStep: 0.1
|
|
@@ -98,6 +107,7 @@ const plugin = {
|
|
|
98
107
|
widget.toolbar.addItem('advanced-refresh', new ToolbarButton({
|
|
99
108
|
icon: refreshIcon,
|
|
100
109
|
tooltip: 'Reload image from disk',
|
|
110
|
+
noFocusOnClick: true,
|
|
101
111
|
onClick: async () => {
|
|
102
112
|
await widget.context.revert();
|
|
103
113
|
widget.update();
|
|
@@ -106,16 +116,19 @@ const plugin = {
|
|
|
106
116
|
widget.toolbar.addItem('advanced-zoom-out', new ToolbarButton({
|
|
107
117
|
label: '-',
|
|
108
118
|
tooltip: 'Zoom out',
|
|
119
|
+
noFocusOnClick: true,
|
|
109
120
|
onClick: () => app.commands.execute(CommandIDs.zoomOut)
|
|
110
121
|
}));
|
|
111
122
|
widget.toolbar.addItem('advanced-zoom-in', new ToolbarButton({
|
|
112
123
|
label: '+',
|
|
113
124
|
tooltip: 'Zoom in',
|
|
125
|
+
noFocusOnClick: true,
|
|
114
126
|
onClick: () => app.commands.execute(CommandIDs.zoomIn)
|
|
115
127
|
}));
|
|
116
128
|
widget.toolbar.addItem('advanced-reset-fit', new ToolbarButton({
|
|
117
129
|
label: 'Fit',
|
|
118
130
|
tooltip: 'Reset to fit',
|
|
131
|
+
noFocusOnClick: true,
|
|
119
132
|
onClick: () => app.commands.execute(CommandIDs.resetFit)
|
|
120
133
|
}));
|
|
121
134
|
widget.toolbar.addItem('advanced-spacer', Toolbar.createSpacerItem());
|
|
@@ -144,6 +157,16 @@ const plugin = {
|
|
|
144
157
|
if (webp) {
|
|
145
158
|
app.docRegistry.addFileType(webp, ['Image']);
|
|
146
159
|
}
|
|
160
|
+
// JupyterLab registers no 'avif' file type at all, so add one with the
|
|
161
|
+
// same shape as its other image types, bound to the same factory.
|
|
162
|
+
app.docRegistry.addFileType({
|
|
163
|
+
name: 'avif',
|
|
164
|
+
displayName: 'Image',
|
|
165
|
+
mimeTypes: ['image/avif'],
|
|
166
|
+
extensions: ['.avif'],
|
|
167
|
+
icon: imageIcon,
|
|
168
|
+
fileFormat: 'base64'
|
|
169
|
+
}, ['Image']);
|
|
147
170
|
app.commands.addCommand(CommandIDs.zoomIn, {
|
|
148
171
|
label: 'Zoom In (Advanced Image Viewer)',
|
|
149
172
|
isEnabled: () => currentController() !== null,
|
|
@@ -226,12 +249,17 @@ const plugin = {
|
|
|
226
249
|
// sort plugin decide what comes next: a C-locale sort puts '.' before
|
|
227
250
|
// '_' where localeCompare puts '_' first, and a re-sort here skipped
|
|
228
251
|
// files. FileBrowser keeps `listing` protected, but it is the only
|
|
229
|
-
// source of the displayed order.
|
|
230
|
-
//
|
|
252
|
+
// source of the displayed order. The listing re-sorts on a refresh only
|
|
253
|
+
// while it is visible, so a hidden one is re-sorted here; re-sorting a
|
|
254
|
+
// visible one would re-render every row on each step. Otherwise fall
|
|
255
|
+
// back to natural name order, the stock file browser default.
|
|
231
256
|
let images = [];
|
|
232
257
|
if (fileBrowser && fileBrowser.model.path === dirPath) {
|
|
233
258
|
const shown = fileBrowser
|
|
234
259
|
.listing;
|
|
260
|
+
if (!shown.isVisible) {
|
|
261
|
+
shown.sort(shown.sortState);
|
|
262
|
+
}
|
|
235
263
|
images = Array.from(shown.sortedItems()).filter(isImage);
|
|
236
264
|
}
|
|
237
265
|
if (!images.some(item => item.path === path)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jupyterlab_advanced_image_viewer_extension",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5",
|
|
4
4
|
"description": "JupyterLab extension for advanced image viewing: fit-to-screen, cursor-anchored wheel zoom, drag-to-pan, and Left/Right arrow keys to move to the previous and next image in a folder",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -297,6 +297,23 @@ describe('ViewerController', () => {
|
|
|
297
297
|
c.dispose();
|
|
298
298
|
});
|
|
299
299
|
|
|
300
|
+
// DEF-KEYS-2: the mousedown handler cancels the browser's image drag, which
|
|
301
|
+
// also cancels the focus a click gives; the viewer must take focus itself.
|
|
302
|
+
it('takes keyboard focus from a toolbar button when the image is pressed', () => {
|
|
303
|
+
host.tabIndex = 0;
|
|
304
|
+
const button = document.createElement('button');
|
|
305
|
+
document.body.appendChild(button);
|
|
306
|
+
new ViewerController(host, img, 0.1);
|
|
307
|
+
button.focus();
|
|
308
|
+
expect(document.activeElement).toBe(button);
|
|
309
|
+
host.dispatchEvent(
|
|
310
|
+
new MouseEvent('mousedown', { clientX: 0, clientY: 0, cancelable: true })
|
|
311
|
+
);
|
|
312
|
+
window.dispatchEvent(new MouseEvent('mouseup'));
|
|
313
|
+
expect(document.activeElement).toBe(host);
|
|
314
|
+
button.remove();
|
|
315
|
+
});
|
|
316
|
+
|
|
300
317
|
it('stops observing the host and the image on dispose', () => {
|
|
301
318
|
stubGeometry(80, 60);
|
|
302
319
|
const c = new ViewerController(host, img, 0.1);
|
package/src/controller.ts
CHANGED
|
@@ -147,7 +147,11 @@ export class ViewerController {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
private onDown = (e: MouseEvent): void => {
|
|
150
|
+
// preventDefault stops the browser dragging the image out, and with it the
|
|
151
|
+
// focus a click would give the viewer; focus it here, or after a toolbar
|
|
152
|
+
// click the stock keys and Left/Right go to the button instead.
|
|
150
153
|
e.preventDefault();
|
|
154
|
+
this.host.focus();
|
|
151
155
|
this.panning = true;
|
|
152
156
|
this.start = { x: e.clientX, y: e.clientY, tx: this.tx, ty: this.ty };
|
|
153
157
|
this.host.style.cursor = 'grabbing';
|
package/src/index.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { IImageTracker, ImageViewer } from '@jupyterlab/imageviewer';
|
|
|
20
20
|
|
|
21
21
|
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
22
22
|
|
|
23
|
-
import { refreshIcon } from '@jupyterlab/ui-components';
|
|
23
|
+
import { imageIcon, refreshIcon } from '@jupyterlab/ui-components';
|
|
24
24
|
|
|
25
25
|
import { IDisposable } from '@lumino/disposable';
|
|
26
26
|
|
|
@@ -46,12 +46,21 @@ const IMAGE_EXTS = new Set([
|
|
|
46
46
|
'.gif',
|
|
47
47
|
'.bmp',
|
|
48
48
|
'.svg',
|
|
49
|
-
'.webp'
|
|
49
|
+
'.webp',
|
|
50
|
+
'.avif'
|
|
50
51
|
]);
|
|
51
52
|
|
|
52
53
|
// Raster formats that can be drawn to a canvas and copied as PNG. SVG is
|
|
53
54
|
// excluded - the drawio/SVG extension already adds its own "Copy as PNG".
|
|
54
|
-
const RASTER_EXTS = new Set([
|
|
55
|
+
const RASTER_EXTS = new Set([
|
|
56
|
+
'.png',
|
|
57
|
+
'.jpg',
|
|
58
|
+
'.jpeg',
|
|
59
|
+
'.gif',
|
|
60
|
+
'.bmp',
|
|
61
|
+
'.webp',
|
|
62
|
+
'.avif'
|
|
63
|
+
]);
|
|
55
64
|
|
|
56
65
|
interface ISettingsState {
|
|
57
66
|
navEnabled: boolean;
|
|
@@ -147,6 +156,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
147
156
|
new ToolbarButton({
|
|
148
157
|
icon: refreshIcon,
|
|
149
158
|
tooltip: 'Reload image from disk',
|
|
159
|
+
noFocusOnClick: true,
|
|
150
160
|
onClick: async () => {
|
|
151
161
|
await widget.context.revert();
|
|
152
162
|
widget.update();
|
|
@@ -158,6 +168,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
158
168
|
new ToolbarButton({
|
|
159
169
|
label: '-',
|
|
160
170
|
tooltip: 'Zoom out',
|
|
171
|
+
noFocusOnClick: true,
|
|
161
172
|
onClick: () => app.commands.execute(CommandIDs.zoomOut)
|
|
162
173
|
})
|
|
163
174
|
);
|
|
@@ -166,6 +177,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
166
177
|
new ToolbarButton({
|
|
167
178
|
label: '+',
|
|
168
179
|
tooltip: 'Zoom in',
|
|
180
|
+
noFocusOnClick: true,
|
|
169
181
|
onClick: () => app.commands.execute(CommandIDs.zoomIn)
|
|
170
182
|
})
|
|
171
183
|
);
|
|
@@ -174,6 +186,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
174
186
|
new ToolbarButton({
|
|
175
187
|
label: 'Fit',
|
|
176
188
|
tooltip: 'Reset to fit',
|
|
189
|
+
noFocusOnClick: true,
|
|
177
190
|
onClick: () => app.commands.execute(CommandIDs.resetFit)
|
|
178
191
|
})
|
|
179
192
|
);
|
|
@@ -205,6 +218,19 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
205
218
|
if (webp) {
|
|
206
219
|
app.docRegistry.addFileType(webp, ['Image']);
|
|
207
220
|
}
|
|
221
|
+
// JupyterLab registers no 'avif' file type at all, so add one with the
|
|
222
|
+
// same shape as its other image types, bound to the same factory.
|
|
223
|
+
app.docRegistry.addFileType(
|
|
224
|
+
{
|
|
225
|
+
name: 'avif',
|
|
226
|
+
displayName: 'Image',
|
|
227
|
+
mimeTypes: ['image/avif'],
|
|
228
|
+
extensions: ['.avif'],
|
|
229
|
+
icon: imageIcon,
|
|
230
|
+
fileFormat: 'base64'
|
|
231
|
+
},
|
|
232
|
+
['Image']
|
|
233
|
+
);
|
|
208
234
|
|
|
209
235
|
app.commands.addCommand(CommandIDs.zoomIn, {
|
|
210
236
|
label: 'Zoom In (Advanced Image Viewer)',
|
|
@@ -296,12 +322,17 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
296
322
|
// sort plugin decide what comes next: a C-locale sort puts '.' before
|
|
297
323
|
// '_' where localeCompare puts '_' first, and a re-sort here skipped
|
|
298
324
|
// files. FileBrowser keeps `listing` protected, but it is the only
|
|
299
|
-
// source of the displayed order.
|
|
300
|
-
//
|
|
325
|
+
// source of the displayed order. The listing re-sorts on a refresh only
|
|
326
|
+
// while it is visible, so a hidden one is re-sorted here; re-sorting a
|
|
327
|
+
// visible one would re-render every row on each step. Otherwise fall
|
|
328
|
+
// back to natural name order, the stock file browser default.
|
|
301
329
|
let images: Array<{ name: string; path: string; type: string }> = [];
|
|
302
330
|
if (fileBrowser && fileBrowser.model.path === dirPath) {
|
|
303
331
|
const shown = (fileBrowser as unknown as { listing: DirListing })
|
|
304
332
|
.listing;
|
|
333
|
+
if (!shown.isVisible) {
|
|
334
|
+
shown.sort(shown.sortState);
|
|
335
|
+
}
|
|
305
336
|
images = Array.from(shown.sortedItems()).filter(isImage);
|
|
306
337
|
}
|
|
307
338
|
if (!images.some(item => item.path === path)) {
|