Commit f39f54c6 authored by Jacob Schatz's avatar Jacob Schatz

Merge branch '34361-lazy-load-images-on-the-frontend' into 'master'

Resolve "Lazy load images on the Frontend"

Closes #34361

See merge request !12503
parents 3a26bce8 52b8a0db
/* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */ /* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */
import './lib/utils/common_utils'; import './lib/utils/common_utils';
import { placeholderImage } from './lazy_loader';
const gfmRules = { const gfmRules = {
// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert // The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
...@@ -56,6 +57,11 @@ const gfmRules = { ...@@ -56,6 +57,11 @@ const gfmRules = {
return text; return text;
}, },
}, },
ImageLazyLoadFilter: {
'img'(el, text) {
return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
},
},
VideoLinkFilter: { VideoLinkFilter: {
'.video-container'(el) { '.video-container'(el) {
const videoEl = el.querySelector('video'); const videoEl = el.querySelector('video');
...@@ -163,7 +169,9 @@ const gfmRules = { ...@@ -163,7 +169,9 @@ const gfmRules = {
return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n'); return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n');
}, },
'img'(el) { 'img'(el) {
return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`; const imageSrc = el.src;
const imageUrl = imageSrc && imageSrc !== placeholderImage ? imageSrc : (el.dataset.src || '');
return `![${el.getAttribute('alt')}](${imageUrl})`;
}, },
'a.anchor'(el, text) { 'a.anchor'(el, text) {
// Don't render a Markdown link for the anchor link inside a heading // Don't render a Markdown link for the anchor link inside a heading
......
/* eslint-disable one-export, one-var, one-var-declaration-per-line */
import _ from 'underscore';
export const placeholderImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
const SCROLL_THRESHOLD = 300;
export default class LazyLoader {
constructor(options = {}) {
this.lazyImages = [];
this.observerNode = options.observerNode || '#content-body';
const throttledScrollCheck = _.throttle(() => this.scrollCheck(), 300);
const debouncedElementsInView = _.debounce(() => this.checkElementsInView(), 300);
window.addEventListener('scroll', throttledScrollCheck);
window.addEventListener('resize', debouncedElementsInView);
const scrollContainer = options.scrollContainer || window;
scrollContainer.addEventListener('load', () => this.loadCheck());
}
searchLazyImages() {
this.lazyImages = [].slice.call(document.querySelectorAll('.lazy'));
this.checkElementsInView();
}
startContentObserver() {
const contentNode = document.querySelector(this.observerNode) || document.querySelector('body');
if (contentNode) {
const observer = new MutationObserver(() => this.searchLazyImages());
observer.observe(contentNode, {
childList: true,
subtree: true,
});
}
}
loadCheck() {
this.searchLazyImages();
this.startContentObserver();
}
scrollCheck() {
requestAnimationFrame(() => this.checkElementsInView());
}
checkElementsInView() {
const scrollTop = pageYOffset;
const visHeight = scrollTop + innerHeight + SCROLL_THRESHOLD;
let imgBoundRect, imgTop, imgBound;
// Loading Images which are in the current viewport or close to them
this.lazyImages = this.lazyImages.filter((selectedImage) => {
if (selectedImage.getAttribute('data-src')) {
imgBoundRect = selectedImage.getBoundingClientRect();
imgTop = scrollTop + imgBoundRect.top;
imgBound = imgTop + imgBoundRect.height;
if (scrollTop < imgBound && visHeight > imgTop) {
LazyLoader.loadImage(selectedImage);
return false;
}
return true;
}
return false;
});
}
static loadImage(img) {
if (img.getAttribute('data-src')) {
img.setAttribute('src', img.getAttribute('data-src'));
img.removeAttribute('data-src');
img.classList.remove('lazy');
img.classList.add('js-lazy-loaded');
}
}
}
...@@ -109,6 +109,7 @@ import './label_manager'; ...@@ -109,6 +109,7 @@ import './label_manager';
import './labels'; import './labels';
import './labels_select'; import './labels_select';
import './layout_nav'; import './layout_nav';
import LazyLoader from './lazy_loader';
import './line_highlighter'; import './line_highlighter';
import './logo'; import './logo';
import './member_expiration_date'; import './member_expiration_date';
...@@ -166,6 +167,11 @@ window.addEventListener('load', function onLoad() { ...@@ -166,6 +167,11 @@ window.addEventListener('load', function onLoad() {
gl.utils.handleLocationHash(); gl.utils.handleLocationHash();
}, false); }, false);
gl.lazyLoader = new LazyLoader({
scrollContainer: window,
observerNode: '#content-body'
});
$(function () { $(function () {
var $body = $('body'); var $body = $('body');
var $document = $(document); var $document = $(document);
......
...@@ -35,6 +35,8 @@ ...@@ -35,6 +35,8 @@
width: 40px; width: 40px;
height: 40px; height: 40px;
padding: 0; padding: 0;
background: $avatar-background;
overflow: hidden;
&.avatar-inline { &.avatar-inline {
float: none; float: none;
......
...@@ -11,8 +11,17 @@ ...@@ -11,8 +11,17 @@
} }
img { img {
max-width: 100%; /*max-width: 100%;*/
margin: 0 0 8px; margin: 0 0 8px;
min-width: 200px;
min-height: 100px;
background-color: $gray-lightest;
}
img.js-lazy-loaded {
min-width: none;
min-height: none;
background-color: none;
} }
p a:not(.no-attachment-icon) img { p a:not(.no-attachment-icon) img {
......
...@@ -379,7 +379,9 @@ $issue-boards-card-shadow: rgba(186, 186, 186, 0.5); ...@@ -379,7 +379,9 @@ $issue-boards-card-shadow: rgba(186, 186, 186, 0.5);
* Avatar * Avatar
*/ */
$avatar_radius: 50%; $avatar_radius: 50%;
$avatar-border: $border-color; $avatar-border: $gray-normal;
$avatar-border-hover: $gray-darker;
$avatar-background: $gray-lightest;
$gl-avatar-size: 40px; $gl-avatar-size: 40px;
/* /*
......
...@@ -11,17 +11,12 @@ module AvatarsHelper ...@@ -11,17 +11,12 @@ module AvatarsHelper
def user_avatar_without_link(options = {}) def user_avatar_without_link(options = {})
avatar_size = options[:size] || 16 avatar_size = options[:size] || 16
user_name = options[:user].try(:name) || options[:user_name] user_name = options[:user].try(:name) || options[:user_name]
css_class = options[:css_class] || ''
avatar_url = options[:url] || avatar_icon(options[:user] || options[:user_email], avatar_size) avatar_url = options[:url] || avatar_icon(options[:user] || options[:user_email], avatar_size)
data_attributes = { container: 'body' } data_attributes = { container: 'body' }
if options[:lazy]
data_attributes[:src] = avatar_url
end
image_tag( image_tag(
options[:lazy] ? '' : avatar_url, avatar_url,
class: "avatar has-tooltip s#{avatar_size} #{css_class}", class: %W[avatar has-tooltip s#{avatar_size}].push(*options[:css_class]),
alt: "#{user_name}'s avatar", alt: "#{user_name}'s avatar",
title: user_name, title: user_name,
data: data_attributes data: data_attributes
......
...@@ -61,8 +61,8 @@ module EmailsHelper ...@@ -61,8 +61,8 @@ module EmailsHelper
else else
image_tag( image_tag(
image_url('mailers/gitlab_header_logo.gif'), image_url('mailers/gitlab_header_logo.gif'),
size: "55x50", size: '55x50',
alt: "GitLab" alt: 'GitLab'
) )
end end
end end
......
module LazyImageTagHelper
def placeholder_image
"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
end
# Override the default ActionView `image_tag` helper to support lazy-loading
def image_tag(source, options = {})
options = options.symbolize_keys
unless options.delete(:lazy) == false
options[:data] ||= {}
options[:data][:src] = path_to_image(source)
options[:class] ||= ""
options[:class] << " lazy"
source = placeholder_image
end
super(source, options)
end
# Required for Banzai::Filter::ImageLazyLoadFilter
module_function :placeholder_image
end
...@@ -2,7 +2,7 @@ module VersionCheckHelper ...@@ -2,7 +2,7 @@ module VersionCheckHelper
def version_status_badge def version_status_badge
if Rails.env.production? && current_application_settings.version_check_enabled if Rails.env.production? && current_application_settings.version_check_enabled
image_url = VersionCheck.new.url image_url = VersionCheck.new.url
image_tag image_url, class: 'js-version-status-badge' image_tag image_url, class: 'js-version-status-badge', lazy: false
end end
end end
end end
...@@ -11,7 +11,7 @@ module CacheMarkdownField ...@@ -11,7 +11,7 @@ module CacheMarkdownField
extend ActiveSupport::Concern extend ActiveSupport::Concern
# Increment this number every time the renderer changes its output # Increment this number every time the renderer changes its output
CACHE_VERSION = 1 CACHE_VERSION = 2
# changes to these attributes cause the cache to be invalidates # changes to these attributes cause the cache to be invalidates
INVALIDATED_BY = %w[author project].freeze INVALIDATED_BY = %w[author project].freeze
......
.file-content.image_file .file-content.image_file
%img{ src: blob_raw_url, alt: viewer.blob.name } %img{ 'data-src': blob_raw_url, alt: viewer.blob.name }
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
.image .image
%span.wrap %span.wrap
.frame{ class: (diff_file.deleted_file? ? 'deleted' : 'added') } .frame{ class: (diff_file.deleted_file? ? 'deleted' : 'added') }
%img{ src: blob_raw_path, alt: diff_file.file_path } %img{ 'data-src': blob_raw_path, alt: diff_file.file_path }
%p.image-info= number_to_human_size(blob.size) %p.image-info= number_to_human_size(blob.size)
- else - else
.image .image
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
%span.wrap %span.wrap
.frame.deleted .frame.deleted
%a{ href: project_blob_path(@project, tree_join(diff_file.old_content_sha, diff_file.old_path)) } %a{ href: project_blob_path(@project, tree_join(diff_file.old_content_sha, diff_file.old_path)) }
%img{ src: old_blob_raw_path, alt: diff_file.old_path } %img{ 'data-src': old_blob_raw_path, alt: diff_file.old_path }
%p.image-info.hide %p.image-info.hide
%span.meta-filesize= number_to_human_size(old_blob.size) %span.meta-filesize= number_to_human_size(old_blob.size)
| |
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
%span.wrap %span.wrap
.frame.added .frame.added
%a{ href: project_blob_path(@project, tree_join(diff_file.content_sha, diff_file.new_path)) } %a{ href: project_blob_path(@project, tree_join(diff_file.content_sha, diff_file.new_path)) }
%img{ src: blob_raw_path, alt: diff_file.new_path } %img{ 'data-src': blob_raw_path, alt: diff_file.new_path }
%p.image-info.hide %p.image-info.hide
%span.meta-filesize= number_to_human_size(blob.size) %span.meta-filesize= number_to_human_size(blob.size)
| |
...@@ -41,10 +41,10 @@ ...@@ -41,10 +41,10 @@
.swipe.view.hide .swipe.view.hide
.swipe-frame .swipe-frame
.frame.deleted .frame.deleted
%img{ src: old_blob_raw_path, alt: diff_file.old_path } %img{ 'data-src': old_blob_raw_path, alt: diff_file.old_path }
.swipe-wrap .swipe-wrap
.frame.added .frame.added
%img{ src: blob_raw_path, alt: diff_file.new_path } %img{ 'data-src': blob_raw_path, alt: diff_file.new_path }
%span.swipe-bar %span.swipe-bar
%span.top-handle %span.top-handle
%span.bottom-handle %span.bottom-handle
...@@ -52,9 +52,9 @@ ...@@ -52,9 +52,9 @@
.onion-skin.view.hide .onion-skin.view.hide
.onion-skin-frame .onion-skin-frame
.frame.deleted .frame.deleted
%img{ src: old_blob_raw_path, alt: diff_file.old_path } %img{ 'data-src': old_blob_raw_path, alt: diff_file.old_path }
.frame.added .frame.added
%img{ src: blob_raw_path, alt: diff_file.new_path } %img{ 'data-src': blob_raw_path, alt: diff_file.new_path }
.controls .controls
.transparent .transparent
.drag-track .drag-track
......
---
title: Lazy load images for better Frontend performance
merge_request: 12503
author:
...@@ -23,6 +23,18 @@ controlled by the server. ...@@ -23,6 +23,18 @@ controlled by the server.
1. The backend code will most likely be using etags. You do not and should not check for status 1. The backend code will most likely be using etags. You do not and should not check for status
`304 Not Modified`. The browser will transform it for you. `304 Not Modified`. The browser will transform it for you.
### Lazy Loading
To improve the time to first render we are using lazy loading for images. This works by setting
the actual image source on the `data-src` attribute. After the HTML is rendered and JavaScript is loaded,
the value of `data-src` will be moved to `src` automatically if the image is in the current viewport.
* Prepare images in HTML for lazy loading by renaming the `src` attribute to `data-src`
* If you are using the Rails `image_tag` helper, all images will be lazy-loaded by default unless `lazy: false` is provided.
If you are asynchronously adding content which contains lazy images then you need to call the function
`gl.lazyLoader.searchLazyImages()` which will search for lazy images and load them if needed.
## Reducing Asset Footprint ## Reducing Asset Footprint
### Page-specific JavaScript ### Page-specific JavaScript
......
...@@ -114,7 +114,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps ...@@ -114,7 +114,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
end end
step 'Image should be shown on the page' do step 'Image should be shown on the page' do
expect(page).to have_xpath("//img[@src=\"image.jpg\"]") expect(page).to have_xpath("//img[@data-src=\"image.jpg\"]")
end end
step 'I click on image link' do step 'I click on image link' do
......
...@@ -118,7 +118,7 @@ module Banzai ...@@ -118,7 +118,7 @@ module Banzai
end end
if path if path
content_tag(:img, nil, src: path, class: 'gfm') content_tag(:img, nil, data: { src: path }, class: 'gfm')
end end
end end
......
module Banzai
module Filter
# HTML filter that moves the value of the src attribute to the data-src attribute so it can be lazy loaded
class ImageLazyLoadFilter < HTML::Pipeline::Filter
def call
doc.xpath('descendant-or-self::img').each do |img|
img['class'] ||= '' << 'lazy'
img['data-src'] = img['src']
img['src'] = LazyImageTagHelper.placeholder_image
end
doc
end
end
end
end
...@@ -10,7 +10,7 @@ module Banzai ...@@ -10,7 +10,7 @@ module Banzai
link = doc.document.create_element( link = doc.document.create_element(
'a', 'a',
class: 'no-attachment-icon', class: 'no-attachment-icon',
href: img['src'], href: img['data-src'] || img['src'],
target: '_blank', target: '_blank',
rel: 'noopener noreferrer' rel: 'noopener noreferrer'
) )
......
...@@ -22,6 +22,7 @@ module Banzai ...@@ -22,6 +22,7 @@ module Banzai
doc.css('img, video').each do |el| doc.css('img, video').each do |el|
process_link_attr el.attribute('src') process_link_attr el.attribute('src')
process_link_attr el.attribute('data-src')
end end
doc doc
......
...@@ -16,6 +16,7 @@ module Banzai ...@@ -16,6 +16,7 @@ module Banzai
Filter::MathFilter, Filter::MathFilter,
Filter::UploadLinkFilter, Filter::UploadLinkFilter,
Filter::VideoLinkFilter, Filter::VideoLinkFilter,
Filter::ImageLazyLoadFilter,
Filter::ImageLinkFilter, Filter::ImageLinkFilter,
Filter::EmojiFilter, Filter::EmojiFilter,
Filter::TableOfContentsFilter, Filter::TableOfContentsFilter,
......
...@@ -63,11 +63,11 @@ feature 'Admin Appearance', feature: true do ...@@ -63,11 +63,11 @@ feature 'Admin Appearance', feature: true do
end end
def logo_selector def logo_selector
'//img[@src^="/uploads/-/system/appearance/logo"]' '//img[data-src^="/uploads/-/system/appearance/logo"]'
end end
def header_logo_selector def header_logo_selector
'//img[@src^="/uploads/-/system/appearance/header_logo"]' '//img[data-src^="/uploads/-/system/appearance/header_logo"]'
end end
def logo_fixture def logo_fixture
......
...@@ -100,7 +100,7 @@ describe 'GitLab Markdown', feature: true do ...@@ -100,7 +100,7 @@ describe 'GitLab Markdown', feature: true do
end end
it 'permits img elements' do it 'permits img elements' do
expect(doc).to have_selector('img[src*="smile.png"]') expect(doc).to have_selector('img[data-src*="smile.png"]')
end end
it 'permits br elements' do it 'permits br elements' do
......
...@@ -18,7 +18,7 @@ feature 'User uploads avatar to group', feature: true do ...@@ -18,7 +18,7 @@ feature 'User uploads avatar to group', feature: true do
visit group_path(group) visit group_path(group)
expect(page).to have_selector(%Q(img[src$="/uploads/-/system/group/avatar/#{group.id}/dk.png"])) expect(page).to have_selector(%Q(img[data-src$="/uploads/-/system/group/avatar/#{group.id}/dk.png"]))
# Cheating here to verify something that isn't user-facing, but is important # Cheating here to verify something that isn't user-facing, but is important
expect(group.reload.avatar.file).to exist expect(group.reload.avatar.file).to exist
......
...@@ -16,7 +16,7 @@ feature 'User uploads avatar to profile', feature: true do ...@@ -16,7 +16,7 @@ feature 'User uploads avatar to profile', feature: true do
visit user_path(user) visit user_path(user)
expect(page).to have_selector(%Q(img[src$="/uploads/-/system/user/avatar/#{user.id}/dk.png"])) expect(page).to have_selector(%Q(img[data-src$="/uploads/-/system/user/avatar/#{user.id}/dk.png"]))
# Cheating here to verify something that isn't user-facing, but is important # Cheating here to verify something that isn't user-facing, but is important
expect(user.reload.avatar.file).to exist expect(user.reload.avatar.file).to exist
......
...@@ -62,13 +62,13 @@ describe ApplicationHelper do ...@@ -62,13 +62,13 @@ describe ApplicationHelper do
avatar_url = "/uploads/-/system/project/avatar/#{project.id}/banana_sample.gif" avatar_url = "/uploads/-/system/project/avatar/#{project.id}/banana_sample.gif"
expect(helper.project_icon(project.full_path).to_s) expect(helper.project_icon(project.full_path).to_s)
.to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />" .to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host) allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host)
avatar_url = "#{gitlab_host}/uploads/-/system/project/avatar/#{project.id}/banana_sample.gif" avatar_url = "#{gitlab_host}/uploads/-/system/project/avatar/#{project.id}/banana_sample.gif"
expect(helper.project_icon(project.full_path).to_s) expect(helper.project_icon(project.full_path).to_s)
.to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />" .to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
end end
it 'gives uploaded icon when present' do it 'gives uploaded icon when present' do
...@@ -77,7 +77,8 @@ describe ApplicationHelper do ...@@ -77,7 +77,8 @@ describe ApplicationHelper do
allow_any_instance_of(Project).to receive(:avatar_in_git).and_return(true) allow_any_instance_of(Project).to receive(:avatar_in_git).and_return(true)
avatar_url = "#{gitlab_host}#{project_avatar_path(project)}" avatar_url = "#{gitlab_host}#{project_avatar_path(project)}"
expect(helper.project_icon(project.full_path).to_s).to match(image_tag(avatar_url)) expect(helper.project_icon(project.full_path).to_s)
.to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
end end
end end
......
...@@ -27,11 +27,11 @@ describe AvatarsHelper do ...@@ -27,11 +27,11 @@ describe AvatarsHelper do
it 'displays user avatar' do it 'displays user avatar' do
is_expected.to eq image_tag( is_expected.to eq image_tag(
avatar_icon(user, 16), LazyImageTagHelper.placeholder_image,
class: 'avatar has-tooltip s16 ', class: 'avatar has-tooltip s16 lazy',
alt: "#{user.name}'s avatar", alt: "#{user.name}'s avatar",
title: user.name, title: user.name,
data: { container: 'body' } data: { container: 'body', src: avatar_icon(user, 16) }
) )
end end
...@@ -40,22 +40,8 @@ describe AvatarsHelper do ...@@ -40,22 +40,8 @@ describe AvatarsHelper do
it 'uses provided css_class' do it 'uses provided css_class' do
is_expected.to eq image_tag( is_expected.to eq image_tag(
avatar_icon(user, 16), LazyImageTagHelper.placeholder_image,
class: "avatar has-tooltip s16 #{options[:css_class]}", class: "avatar has-tooltip s16 #{options[:css_class]} lazy",
alt: "#{user.name}'s avatar",
title: user.name,
data: { container: 'body' }
)
end
end
context 'with lazy parameter' do
let(:options) { { user: user, lazy: true } }
it 'uses data-src instead of src' do
is_expected.to eq image_tag(
'',
class: 'avatar has-tooltip s16 ',
alt: "#{user.name}'s avatar", alt: "#{user.name}'s avatar",
title: user.name, title: user.name,
data: { container: 'body', src: avatar_icon(user, 16) } data: { container: 'body', src: avatar_icon(user, 16) }
...@@ -68,11 +54,11 @@ describe AvatarsHelper do ...@@ -68,11 +54,11 @@ describe AvatarsHelper do
it 'uses provided size' do it 'uses provided size' do
is_expected.to eq image_tag( is_expected.to eq image_tag(
avatar_icon(user, options[:size]), LazyImageTagHelper.placeholder_image,
class: "avatar has-tooltip s#{options[:size]} ", class: "avatar has-tooltip s#{options[:size]} lazy",
alt: "#{user.name}'s avatar", alt: "#{user.name}'s avatar",
title: user.name, title: user.name,
data: { container: 'body' } data: { container: 'body', src: avatar_icon(user, options[:size]) }
) )
end end
end end
...@@ -82,11 +68,11 @@ describe AvatarsHelper do ...@@ -82,11 +68,11 @@ describe AvatarsHelper do
it 'uses provided url' do it 'uses provided url' do
is_expected.to eq image_tag( is_expected.to eq image_tag(
options[:url], LazyImageTagHelper.placeholder_image,
class: 'avatar has-tooltip s16 ', class: 'avatar has-tooltip s16 lazy',
alt: "#{user.name}'s avatar", alt: "#{user.name}'s avatar",
title: user.name, title: user.name,
data: { container: 'body' } data: { container: 'body', src: options[:url] }
) )
end end
end end
...@@ -99,22 +85,22 @@ describe AvatarsHelper do ...@@ -99,22 +85,22 @@ describe AvatarsHelper do
it 'prefers user parameter' do it 'prefers user parameter' do
is_expected.to eq image_tag( is_expected.to eq image_tag(
avatar_icon(user, 16), LazyImageTagHelper.placeholder_image,
class: 'avatar has-tooltip s16 ', class: 'avatar has-tooltip s16 lazy',
alt: "#{user.name}'s avatar", alt: "#{user.name}'s avatar",
title: user.name, title: user.name,
data: { container: 'body' } data: { container: 'body', src: avatar_icon(user, 16) }
) )
end end
end end
it 'uses user_name and user_email parameter if user is not present' do it 'uses user_name and user_email parameter if user is not present' do
is_expected.to eq image_tag( is_expected.to eq image_tag(
avatar_icon(options[:user_email], 16), LazyImageTagHelper.placeholder_image,
class: 'avatar has-tooltip s16 ', class: 'avatar has-tooltip s16 lazy',
alt: "#{options[:user_name]}'s avatar", alt: "#{options[:user_name]}'s avatar",
title: options[:user_name], title: options[:user_name],
data: { container: 'body' } data: { container: 'body', src: avatar_icon(options[:user_email], 16) }
) )
end end
end end
......
import LazyLoader from '~/lazy_loader';
let lazyLoader = null;
describe('LazyLoader', function () {
preloadFixtures('issues/issue_with_comment.html.raw');
beforeEach(function () {
loadFixtures('issues/issue_with_comment.html.raw');
lazyLoader = new LazyLoader({
observerNode: 'body',
});
// Doing everything that happens normally in onload
lazyLoader.loadCheck();
});
describe('behavior', function () {
it('should copy value from data-src to src for img 1', function (done) {
const img = document.querySelectorAll('img[data-src]')[0];
const originalDataSrc = img.getAttribute('data-src');
img.scrollIntoView();
setTimeout(() => {
expect(img.getAttribute('src')).toBe(originalDataSrc);
expect(document.getElementsByClassName('js-lazy-loaded').length).toBeGreaterThan(0);
done();
}, 100);
});
it('should lazy load dynamically added data-src images', function (done) {
const newImg = document.createElement('img');
const testPath = '/img/testimg.png';
newImg.className = 'lazy';
newImg.setAttribute('data-src', testPath);
document.body.appendChild(newImg);
newImg.scrollIntoView();
setTimeout(() => {
expect(newImg.getAttribute('src')).toBe(testPath);
expect(document.getElementsByClassName('js-lazy-loaded').length).toBeGreaterThan(0);
done();
}, 100);
});
it('should not alter normal images', function (done) {
const newImg = document.createElement('img');
const testPath = '/img/testimg.png';
newImg.setAttribute('src', testPath);
document.body.appendChild(newImg);
newImg.scrollIntoView();
setTimeout(() => {
expect(newImg).not.toHaveClass('js-lazy-loaded');
done();
}, 100);
});
});
});
...@@ -22,7 +22,7 @@ describe Banzai::Filter::GollumTagsFilter, lib: true do ...@@ -22,7 +22,7 @@ describe Banzai::Filter::GollumTagsFilter, lib: true do
tag = '[[images/image.jpg]]' tag = '[[images/image.jpg]]'
doc = filter("See #{tag}", project_wiki: project_wiki) doc = filter("See #{tag}", project_wiki: project_wiki)
expect(doc.at_css('img')['src']).to eq "#{project_wiki.wiki_base_path}/images/image.jpg" expect(doc.at_css('img')['data-src']).to eq "#{project_wiki.wiki_base_path}/images/image.jpg"
end end
it 'does not creates img tag if image does not exist' do it 'does not creates img tag if image does not exist' do
...@@ -40,7 +40,7 @@ describe Banzai::Filter::GollumTagsFilter, lib: true do ...@@ -40,7 +40,7 @@ describe Banzai::Filter::GollumTagsFilter, lib: true do
tag = '[[http://example.com/image.jpg]]' tag = '[[http://example.com/image.jpg]]'
doc = filter("See #{tag}", project_wiki: project_wiki) doc = filter("See #{tag}", project_wiki: project_wiki)
expect(doc.at_css('img')['src']).to eq "http://example.com/image.jpg" expect(doc.at_css('img')['data-src']).to eq "http://example.com/image.jpg"
end end
it 'does not creates img tag for invalid URL' do it 'does not creates img tag for invalid URL' do
......
require 'spec_helper'
describe Banzai::Filter::ImageLazyLoadFilter, lib: true do
include FilterSpecHelper
def image(path)
%(<img src="#{path}" />)
end
it 'transforms the image src to a data-src' do
doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
expect(doc.at_css('img')['data-src']).to eq '/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'
end
it 'works with external images' do
doc = filter(image('https://i.imgur.com/DfssX9C.jpg'))
expect(doc.at_css('img')['data-src']).to eq 'https://i.imgur.com/DfssX9C.jpg'
end
end
...@@ -17,7 +17,7 @@ module MarkdownMatchers ...@@ -17,7 +17,7 @@ module MarkdownMatchers
image = actual.at_css('img[alt="Relative Image"]') image = actual.at_css('img[alt="Relative Image"]')
expect(link['href']).to end_with('master/doc/README.md') expect(link['href']).to end_with('master/doc/README.md')
expect(image['src']).to end_with('master/app/assets/images/touch-icon-ipad.png') expect(image['data-src']).to end_with('master/app/assets/images/touch-icon-ipad.png')
end end
end end
...@@ -70,7 +70,7 @@ module MarkdownMatchers ...@@ -70,7 +70,7 @@ module MarkdownMatchers
# GollumTagsFilter # GollumTagsFilter
matcher :parse_gollum_tags do matcher :parse_gollum_tags do
def have_image(src) def have_image(src)
have_css("img[src$='#{src}']") have_css("img[data-src$='#{src}']")
end end
prefix = '/namespace1/gitlabhq/wikis' prefix = '/namespace1/gitlabhq/wikis'
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment