diff --git a/app/assets/javascripts/discourse/initializers/post-decorations.js.es6 b/app/assets/javascripts/discourse/initializers/post-decorations.js.es6 index 5e845a64c0c..e9e69217daa 100644 --- a/app/assets/javascripts/discourse/initializers/post-decorations.js.es6 +++ b/app/assets/javascripts/discourse/initializers/post-decorations.js.es6 @@ -1,5 +1,6 @@ import highlightSyntax from "discourse/lib/highlight-syntax"; import lightbox from "discourse/lib/lightbox"; +import { setupLazyLoading } from "discourse/lib/lazy-load-images"; import { setTextDirections } from "discourse/lib/text-direction"; import { withPluginApi } from "discourse/lib/plugin-api"; @@ -14,6 +15,8 @@ export default { api.decorateCooked(setTextDirections); } + setupLazyLoading(api); + api.decorateCooked($elem => { const players = $("audio", $elem); if (players.length) { diff --git a/app/assets/javascripts/discourse/lib/lazy-load-images.js.es6 b/app/assets/javascripts/discourse/lib/lazy-load-images.js.es6 new file mode 100644 index 00000000000..4f2b7729b96 --- /dev/null +++ b/app/assets/javascripts/discourse/lib/lazy-load-images.js.es6 @@ -0,0 +1,49 @@ +const OBSERVER_OPTIONS = { + rootMargin: "50%" // load images slightly before they're visible +}; + +// 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"); + image.setAttribute("data-src", image.getAttribute("src")); + image.setAttribute( + "src", + "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" + ); +} + +// Restore an image from the `data-src` attribute +function show(image) { + let dataSrc = image.getAttribute("data-src"); + if (dataSrc) { + image.setAttribute("src", dataSrc); + image.classList.remove("d-lazyload-hidden"); + } +} + +export function setupLazyLoading(api) { + // Old IE don't support this API + if (!("IntersectionObserver" in window)) { + return; + } + + const observer = new IntersectionObserver(entries => { + entries.forEach(entry => { + const { target } = entry; + + if (entry.isIntersecting) { + show(target); + observer.unobserve(target); + } else { + // The Observer is triggered when entries are added. This allows + // us to hide things that start off screen. + hide(target); + } + }); + }, OBSERVER_OPTIONS); + + api.decorateCooked($post => { + $(".lightbox img", $post).each((_, $img) => observer.observe($img)); + }); +} diff --git a/app/assets/stylesheets/common/base/lightbox.scss b/app/assets/stylesheets/common/base/lightbox.scss index db6e3ca516d..5a9e7eed77a 100644 --- a/app/assets/stylesheets/common/base/lightbox.scss +++ b/app/assets/stylesheets/common/base/lightbox.scss @@ -6,6 +6,16 @@ opacity: 0.9; transition: opacity 0.5s; } + background: rgba($primary, 0.25); +} + +.d-lazyload-hidden { + opacity: 0; + box-sizing: border-box; +} + +.cooked img.d-lazyload { + transition: opacity 0.4s 0.75s ease; } .lightbox-wrapper {