Closes #20211: Use thumbnails for ImageAttachment hover previews to improve page load performance (#21386)

This commit is contained in:
Martin Hauser
2026-02-10 18:01:33 +01:00
committed by GitHub
parent 02ffdd9d5d
commit 23f1c86e9c
6 changed files with 50 additions and 14 deletions

View File

@@ -39,9 +39,20 @@ __all__ = (
)
IMAGEATTACHMENT_IMAGE = """
{% load thumbnail %}
{% if record.image %}
<a href="{{ record.image.url }}" target="_blank" class="image-preview" data-bs-placement="top">
<i class="mdi mdi-image"></i></a>
{% thumbnail record.image "400x400" as tn %}
<a href="{{ record.get_absolute_url }}"
class="image-preview"
data-preview-url="{{ tn.url }}"
data-bs-placement="left"
title="{{ record.filename }}"
rel="noopener noreferrer"
target="_blank"
aria-label="{{ record.filename }}">
<i class="mdi mdi-image"></i>
</a>
{% endthumbnail %}
{% endif %}
<a href="{{ record.get_absolute_url }}">{{ record.filename|truncate_middle:16 }}</a>
"""

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -150,20 +150,22 @@ function initSidebarAccordions(): void {
*/
function initImagePreview(): void {
for (const element of getElements<HTMLAnchorElement>('a.image-preview')) {
// Generate a max-width that's a quarter of the screen's width (note - the actual element
// width will be slightly larger due to the popover body's padding).
const maxWidth = `${Math.round(window.innerWidth / 4)}px`;
// Prefer a thumbnail URL for the popover (so we don't preload full-size images),
// but fall back to the link target if no thumbnail was provided.
const previewUrl = element.dataset.previewUrl ?? element.href;
const image = createElement('img', { src: previewUrl });
// Create an image element that uses the linked image as its `src`.
const image = createElement('img', { src: element.href });
image.style.maxWidth = maxWidth;
// Ensure lazy loading and async decoding
image.loading = 'lazy';
image.decoding = 'async';
// Create a container for the image.
const content = createElement('div', null, null, [image]);
// Initialize the Bootstrap Popper instance.
new Popover(element, {
// Attach this custom class to the popover so that it styling can be controlled via CSS.
// Attach this custom class to the popover so that its styling
// can be controlled via CSS.
customClass: 'image-preview-popover',
trigger: 'hover',
html: true,

View File

@@ -89,6 +89,29 @@ img.plugin-icon {
}
}
// Image preview popover (rendered for <a.image-preview> by initImagePreview())
.image-preview-popover {
--bs-popover-max-width: clamp(240px, 25vw, 640px);
.popover-header {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.popover-body {
display: flex;
justify-content: center;
align-items: center;
}
img {
display: block;
max-width: 100%;
max-height: clamp(160px, 33vh, 640px);
height: auto;
}
}
body[data-bs-theme=dark] {
// Assuming icon is black/white line art, invert it and tone down brightness
img.plugin-icon {