FEATURE: Use responsive image sizes in post stream (#13343)
This commit is contained in:
parent
e9e2827636
commit
e305365168
|
@ -1,33 +0,0 @@
|
||||||
export default {
|
|
||||||
name: "ensure-image-dimensions",
|
|
||||||
after: "mobile",
|
|
||||||
initialize(container) {
|
|
||||||
if (!window) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This enforces maximum dimensions of images based on site settings
|
|
||||||
// for mobile we use the window width as a safeguard
|
|
||||||
// This rule should never really be at play unless for some reason images do not have dimensions
|
|
||||||
|
|
||||||
const siteSettings = container.lookup("site-settings:main");
|
|
||||||
let width = siteSettings.max_image_width;
|
|
||||||
let height = siteSettings.max_image_height;
|
|
||||||
|
|
||||||
const site = container.lookup("site:main");
|
|
||||||
if (site.mobileView) {
|
|
||||||
width = window.innerWidth - 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
let styles = `max-width:${width}px; max-height:${height}px;`;
|
|
||||||
|
|
||||||
if (siteSettings.disable_image_size_calculations) {
|
|
||||||
styles = "max-width: 100%; height: auto;";
|
|
||||||
}
|
|
||||||
|
|
||||||
const styleTag = document.createElement("style");
|
|
||||||
styleTag.id = "image-sizing-hack";
|
|
||||||
styleTag.innerHTML = `#reply-control .d-editor-preview img:not(.thumbnail):not(.ytp-thumbnail-image):not(.emoji), .cooked img:not(.thumbnail):not(.ytp-thumbnail-image):not(.emoji) {${styles}}`;
|
|
||||||
document.head.appendChild(styleTag);
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -4,10 +4,7 @@ import highlightSyntax from "discourse/lib/highlight-syntax";
|
||||||
import lightbox from "discourse/lib/lightbox";
|
import lightbox from "discourse/lib/lightbox";
|
||||||
import { iconHTML } from "discourse-common/lib/icon-library";
|
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||||
import { setTextDirections } from "discourse/lib/text-direction";
|
import { setTextDirections } from "discourse/lib/text-direction";
|
||||||
import {
|
import { nativeLazyLoading } from "discourse/lib/lazy-load-images";
|
||||||
nativeLazyLoading,
|
|
||||||
setupLazyLoading,
|
|
||||||
} from "discourse/lib/lazy-load-images";
|
|
||||||
import { withPluginApi } from "discourse/lib/plugin-api";
|
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -38,11 +35,7 @@ export default {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (siteSettings.disable_image_size_calculations) {
|
nativeLazyLoading(api);
|
||||||
nativeLazyLoading(api);
|
|
||||||
} else {
|
|
||||||
setupLazyLoading(api);
|
|
||||||
}
|
|
||||||
|
|
||||||
api.decorateCooked(
|
api.decorateCooked(
|
||||||
($elem) => {
|
($elem) => {
|
||||||
|
|
|
@ -1,89 +1,6 @@
|
||||||
const OBSERVER_OPTIONS = {
|
|
||||||
rootMargin: "66%", // load images slightly before they're visible
|
|
||||||
};
|
|
||||||
|
|
||||||
// Min size in pixels for consideration for lazy loading
|
// Min size in pixels for consideration for lazy loading
|
||||||
const MINIMUM_SIZE = 150;
|
const MINIMUM_SIZE = 150;
|
||||||
|
|
||||||
const hiddenData = new WeakMap();
|
|
||||||
|
|
||||||
const LOADING_DATA =
|
|
||||||
"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
|
|
||||||
|
|
||||||
// We hide an image by replacing it with a transparent gif
|
|
||||||
function hide(image) {
|
|
||||||
image.classList.add("d-lazyload");
|
|
||||||
image.classList.add("d-lazyload-hidden");
|
|
||||||
|
|
||||||
hiddenData.set(image, {
|
|
||||||
src: image.src,
|
|
||||||
srcset: image.srcset,
|
|
||||||
width: image.width,
|
|
||||||
height: image.height,
|
|
||||||
className: image.className,
|
|
||||||
});
|
|
||||||
|
|
||||||
image.src = image.dataset.smallUpload || LOADING_DATA;
|
|
||||||
image.removeAttribute("srcset");
|
|
||||||
|
|
||||||
image.removeAttribute("data-small-upload");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore an image when onscreen
|
|
||||||
function show(image) {
|
|
||||||
let imageData = hiddenData.get(image);
|
|
||||||
|
|
||||||
if (imageData) {
|
|
||||||
const copyImg = new Image();
|
|
||||||
copyImg.onload = () => {
|
|
||||||
if (copyImg.srcset) {
|
|
||||||
image.srcset = copyImg.srcset;
|
|
||||||
}
|
|
||||||
image.src = copyImg.src;
|
|
||||||
image.classList.remove("d-lazyload-hidden");
|
|
||||||
|
|
||||||
if (image.onload) {
|
|
||||||
// don't bother fighting with existing handler
|
|
||||||
// this can mean a slight flash on mobile
|
|
||||||
image.parentNode.removeChild(copyImg);
|
|
||||||
} else {
|
|
||||||
image.onload = () => {
|
|
||||||
image.parentNode.removeChild(copyImg);
|
|
||||||
image.onload = null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
copyImg.onload = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (imageData.srcset) {
|
|
||||||
copyImg.srcset = imageData.srcset;
|
|
||||||
}
|
|
||||||
|
|
||||||
copyImg.src = imageData.src;
|
|
||||||
|
|
||||||
// width of image may not match, use computed style which
|
|
||||||
// is the actual size of the image
|
|
||||||
const computedStyle = window.getComputedStyle(image);
|
|
||||||
const actualWidth = parseInt(computedStyle.width, 10);
|
|
||||||
const actualHeight = parseInt(computedStyle.height, 10);
|
|
||||||
|
|
||||||
copyImg.style.position = "absolute";
|
|
||||||
copyImg.style.top = `${image.offsetTop}px`;
|
|
||||||
copyImg.style.left = `${image.offsetLeft}px`;
|
|
||||||
copyImg.style.width = `${actualWidth}px`;
|
|
||||||
copyImg.style.height = `${actualHeight}px`;
|
|
||||||
|
|
||||||
copyImg.className = imageData.className;
|
|
||||||
|
|
||||||
// insert after the current element so styling still will
|
|
||||||
// apply to original image firstChild selectors
|
|
||||||
image.parentNode.insertBefore(copyImg, image.nextSibling);
|
|
||||||
} else {
|
|
||||||
image.classList.remove("d-lazyload-hidden");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function forEachImage(post, callback) {
|
function forEachImage(post, callback) {
|
||||||
post.querySelectorAll("img").forEach((img) => {
|
post.querySelectorAll("img").forEach((img) => {
|
||||||
if (img.width >= MINIMUM_SIZE && img.height >= MINIMUM_SIZE) {
|
if (img.width >= MINIMUM_SIZE && img.height >= MINIMUM_SIZE) {
|
||||||
|
@ -92,36 +9,6 @@ function forEachImage(post, callback) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupLazyLoading(api) {
|
|
||||||
const observer = new IntersectionObserver((entries) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
const { target } = entry;
|
|
||||||
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
show(target);
|
|
||||||
observer.unobserve(target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, OBSERVER_OPTIONS);
|
|
||||||
|
|
||||||
api.decorateCookedElement((post) => forEachImage(post, (img) => hide(img)), {
|
|
||||||
onlyStream: true,
|
|
||||||
id: "discourse-lazy-load",
|
|
||||||
});
|
|
||||||
|
|
||||||
// IntersectionObserver.observe must be called after the cooked
|
|
||||||
// content is adopted by the document element in chrome
|
|
||||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=1073469
|
|
||||||
api.decorateCookedElement(
|
|
||||||
(post) => forEachImage(post, (img) => observer.observe(img)),
|
|
||||||
{
|
|
||||||
onlyStream: true,
|
|
||||||
id: "discourse-lazy-load-after-adopt",
|
|
||||||
afterAdopt: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function nativeLazyLoading(api) {
|
export function nativeLazyLoading(api) {
|
||||||
api.decorateCookedElement(
|
api.decorateCookedElement(
|
||||||
(post) =>
|
(post) =>
|
||||||
|
|
|
@ -57,7 +57,6 @@ export default class PostCooked {
|
||||||
|
|
||||||
this._insertQuoteControls($cookedDiv);
|
this._insertQuoteControls($cookedDiv);
|
||||||
this._showLinkCounts($cookedDiv);
|
this._showLinkCounts($cookedDiv);
|
||||||
this._fixImageSizes($cookedDiv);
|
|
||||||
this._applySearchHighlight($cookedDiv);
|
this._applySearchHighlight($cookedDiv);
|
||||||
|
|
||||||
this._decorateAndAdopt(cookedDiv);
|
this._decorateAndAdopt(cookedDiv);
|
||||||
|
@ -90,44 +89,6 @@ export default class PostCooked {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_fixImageSizes($html) {
|
|
||||||
if (!this.decoratorHelper || !this.decoratorHelper.widget) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let siteSettings = this.decoratorHelper.widget.siteSettings;
|
|
||||||
|
|
||||||
if (siteSettings.disable_image_size_calculations) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxImageWidth = siteSettings.max_image_width;
|
|
||||||
const maxImageHeight = siteSettings.max_image_height;
|
|
||||||
|
|
||||||
let maxWindowWidth;
|
|
||||||
$html.find("img:not(.avatar)").each((idx, img) => {
|
|
||||||
// deferring work only for posts with images
|
|
||||||
// we got to use screen here, cause nothing is rendered yet.
|
|
||||||
// long term we may want to allow for weird margins that are enforced, instead of hardcoding at 70/20
|
|
||||||
maxWindowWidth =
|
|
||||||
maxWindowWidth || $(window).width() - (this.attrs.mobileView ? 20 : 70);
|
|
||||||
if (maxImageWidth < maxWindowWidth) {
|
|
||||||
maxWindowWidth = maxImageWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
const aspect = img.height / img.width;
|
|
||||||
if (img.width > maxWindowWidth) {
|
|
||||||
img.width = maxWindowWidth;
|
|
||||||
img.height = parseInt(maxWindowWidth * aspect, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
// very unlikely but lets fix this too
|
|
||||||
if (img.height > maxImageHeight) {
|
|
||||||
img.height = maxImageHeight;
|
|
||||||
img.width = parseInt(maxWindowWidth / aspect, 10);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_showLinkCounts($html) {
|
_showLinkCounts($html) {
|
||||||
const linkCounts = this.attrs.linkCounts;
|
const linkCounts = this.attrs.linkCounts;
|
||||||
if (!linkCounts) {
|
if (!linkCounts) {
|
||||||
|
|
|
@ -680,11 +680,9 @@ aside.onebox.stackexchange .onebox-body {
|
||||||
color: var(--primary-med-or-secondary-med);
|
color: var(--primary-med-or-secondary-med);
|
||||||
}
|
}
|
||||||
|
|
||||||
.onebox.xkcd .onebox-body {
|
aside.onebox.xkcd .onebox-body img {
|
||||||
img {
|
float: none;
|
||||||
max-width: 100% !important;
|
max-height: unset;
|
||||||
float: none !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// pdf onebox
|
// pdf onebox
|
||||||
|
|
|
@ -195,6 +195,11 @@ $quote-share-maxwidth: 150px;
|
||||||
sup sup {
|
sup sup {
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
img:not(.thumbnail):not(.ytp-thumbnail-image):not(.emoji) {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// add staff color
|
// add staff color
|
||||||
|
@ -202,10 +207,6 @@ $quote-share-maxwidth: 150px;
|
||||||
.regular > .cooked {
|
.regular > .cooked {
|
||||||
background-color: var(--highlight-low-or-medium);
|
background-color: var(--highlight-low-or-medium);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
img:not(.thumbnail) {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.clearfix > .topic-meta-data > .names {
|
.clearfix > .topic-meta-data > .names {
|
||||||
span.user-title {
|
span.user-title {
|
||||||
|
@ -264,15 +265,6 @@ aside.quote {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
|
||||||
// due to #image-sizing-hack large images and lightboxes extend past the
|
|
||||||
// limits blockquotes. Since #image-sizing-hack is inline, we need to use
|
|
||||||
// !important here otherwise it won't work.
|
|
||||||
img {
|
|
||||||
max-width: 100% !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.quote-controls,
|
.quote-controls,
|
||||||
.quote-controls .d-icon {
|
.quote-controls .d-icon {
|
||||||
color: var(--primary-low-mid-or-secondary-high);
|
color: var(--primary-low-mid-or-secondary-high);
|
||||||
|
|
|
@ -2261,11 +2261,6 @@ uncategorized:
|
||||||
create_revision_on_bulk_topic_moves:
|
create_revision_on_bulk_topic_moves:
|
||||||
default: true
|
default: true
|
||||||
|
|
||||||
disable_image_size_calculations:
|
|
||||||
default: false
|
|
||||||
hidden: true
|
|
||||||
client: true
|
|
||||||
|
|
||||||
user_preferences:
|
user_preferences:
|
||||||
default_email_digest_frequency:
|
default_email_digest_frequency:
|
||||||
enum: "DigestEmailSiteSetting"
|
enum: "DigestEmailSiteSetting"
|
||||||
|
|
|
@ -25,10 +25,6 @@ div.poll {
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
// TODO: remove once disable_image_size_calculations is removed
|
|
||||||
// needed to override internal styles in image-sizing hack
|
|
||||||
max-width: 100% !important;
|
|
||||||
height: auto;
|
|
||||||
// Hacky way to stop images without width/height
|
// Hacky way to stop images without width/height
|
||||||
// from causing abrupt unintended scrolling
|
// from causing abrupt unintended scrolling
|
||||||
&:not([width]):not(.emoji),
|
&:not([width]):not(.emoji),
|
||||||
|
|
Loading…
Reference in New Issue