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 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 opens them in the text editor
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(['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp']);
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. Otherwise fall back to natural name
230
- // order, the stock file browser default.
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",
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(['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp']);
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. Otherwise fall back to natural name
300
- // order, the stock file browser default.
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)) {