Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ Image Tool supports these configuration parameters:
| uploader | `{{uploadByFile: function, uploadByUrl: function}}` | Optional custom uploading methods. See details below. |
| actions | `array` | Array with custom actions to show in the tool's settings menu. See details below. |
| features | `object` | Allows you to enable/disable additional features such as border, background tunes and caption. See details below. |
| lazyLoading | `boolean` | (default: `false`) Defers loading of images until they approach the viewport, using native `loading="lazy"`. See details below. |

Note that if you don't implement your custom uploader methods, the `endpoints` param is required.

Expand Down Expand Up @@ -295,3 +296,32 @@ var editor = EditorJS({
...
});
```

## Lazy loading images

Set `lazyLoading` to `true` to load images only when they approach the viewport, using the browser's native `loading="lazy"`. It's useful for long documents with many images, or for images served by private or paid endpoints. A preloader is shown in place of each image until it's loaded.

```js
import ImageTool from '@editorjs/image';

var editor = EditorJS({
...

tools: {
...
image: {
class: ImageTool,
config: {
/**
* Load images only when they approach the viewport
*/
lazyLoading: true,
}
}
}

...
});
```

Lazy loading applies to images only. `.mp4` sources are still loaded immediately.
30 changes: 30 additions & 0 deletions dev/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,23 @@
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"></script>
<script type="module">
import ImageTool from "../src/index.ts";

/**
* Long document to test lazy loading: watch the Network tab while scrolling
*/
const lazyBlocks = Array.from({ length: 15 }, (_, i) => [
{
type: "paragraph",
data: { text: `Image #${i + 1} below` },
},
{
type: "image",
data: {
file: { url: `https://picsum.photos/id/${10 + i}/1200/800` },
caption: `picsum #${10 + i}`,
},
},
]).flat();
const editor = new EditorJS({
holder: "editorjs",
data: {
Expand All @@ -28,6 +45,18 @@
caption: "kimitsu no yayiba",
},
},
...lazyBlocks,
{
type: "paragraph",
data: { text: "Broken image below: should fall back to the \"Select an Image\" button" },
},
{
type: "image",
data: {
file: { url: "https://example.invalid/broken.jpg" },
caption: "broken image",
},
},
],
},

Expand All @@ -45,6 +74,7 @@
background: false,
stretch: true,
},
lazyLoading: true,
},
},
},
Expand Down
10 changes: 9 additions & 1 deletion src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,17 @@
display: flex;
border: 1px solid var(--border-color);
background-color: #fff;
position: relative;

/**
* Keep the picture in the layout (but invisible) instead of display: none,
* so native lazy loading (loading="lazy") can detect when it approaches the viewport
*/
&-picture {
display: none;
position: absolute;
top: 0;
left: 0;
visibility: hidden;
}
}

Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export default class ImageTool implements BlockTool {
uploader: config.uploader,
actions: config.actions,
features: config.features || {},
lazyLoading: config.lazyLoading,
};

/**
Expand Down
7 changes: 7 additions & 0 deletions src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,13 @@ export interface ImageConfig {
* Tunes to be enabled.
*/
features?: FeaturesConfig;

/**
* Enables native lazy loading for images (loading="lazy").
* When true, images are only fetched when they approach the viewport.
* @default false
*/
lazyLoading?: boolean;
}

/**
Expand Down
37 changes: 36 additions & 1 deletion src/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,15 @@ export default class Ui {
* Renders tool UI
*/
public render(): HTMLElement {
this.toggleStatus(UiState.Empty);
/**
* Keep the status set before rendering (e.g. "uploading" while a lazy image is pending),
* otherwise the hidden image container would never let the lazy image load
*/
const hasStatus = Object.values(UiState).some(state => this.nodes.wrapper.classList.contains(`${this.CSS.wrapper}--${state}`));

if (!hasStatus) {
this.toggleStatus(UiState.Empty);
}

return this.nodes.wrapper;
}
Expand Down Expand Up @@ -225,6 +233,23 @@ export default class Ui {
* Change event to be listened
*/
eventName = 'loadeddata';
} else if (this.config.lazyLoading === true) {
/**
* Let the browser defer fetching until the image approaches the viewport
*/
attributes.loading = 'lazy';

/**
* Re-add src after loading: some browsers start fetching as soon as src is set
*/
delete attributes.src;
attributes.src = url;

/**
* Show the preloader as a placeholder until the image is loaded.
* The image stays in the layout (but invisible) while uploading, so native lazy loading can trigger
*/
this.toggleStatus(UiState.Uploading);
}

/**
Expand All @@ -246,6 +271,16 @@ export default class Ui {
}
});

/**
* If a lazy image fails to load, leave the preloader and show the empty state,
* so the image can be selected again (same as a failed non-lazy image)
*/
if (attributes.loading === 'lazy') {
this.nodes.imageEl.addEventListener('error', () => {
this.toggleStatus(UiState.Empty);
});
}

this.nodes.imageContainer.appendChild(this.nodes.imageEl);
}

Expand Down