Resolve "Lazy load images on the Frontend"
This commit is contained in:
parent
3a26bce80e
commit
52b8a0db68
31 changed files with 287 additions and 68 deletions
|
@ -1,6 +1,7 @@
|
|||
/* 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 { placeholderImage } from './lazy_loader';
|
||||
|
||||
const gfmRules = {
|
||||
// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
|
||||
|
@ -56,6 +57,11 @@ const gfmRules = {
|
|||
return text;
|
||||
},
|
||||
},
|
||||
ImageLazyLoadFilter: {
|
||||
'img'(el, text) {
|
||||
return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
|
||||
},
|
||||
},
|
||||
VideoLinkFilter: {
|
||||
'.video-container'(el) {
|
||||
const videoEl = el.querySelector('video');
|
||||
|
@ -163,7 +169,9 @@ const gfmRules = {
|
|||
return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n');
|
||||
},
|
||||
'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) {
|
||||
// Don't render a Markdown link for the anchor link inside a heading
|
||||
|
|
76
app/assets/javascripts/lazy_loader.js
Normal file
76
app/assets/javascripts/lazy_loader.js
Normal file
|
@ -0,0 +1,76 @@
|
|||
/* 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';
|
|||
import './labels';
|
||||
import './labels_select';
|
||||
import './layout_nav';
|
||||
import LazyLoader from './lazy_loader';
|
||||
import './line_highlighter';
|
||||
import './logo';
|
||||
import './member_expiration_date';
|
||||
|
@ -166,6 +167,11 @@ window.addEventListener('load', function onLoad() {
|
|||
gl.utils.handleLocationHash();
|
||||
}, false);
|
||||
|
||||
gl.lazyLoader = new LazyLoader({
|
||||
scrollContainer: window,
|
||||
observerNode: '#content-body'
|
||||
});
|
||||
|
||||
$(function () {
|
||||
var $body = $('body');
|
||||
var $document = $(document);
|
||||
|
|
|
@ -35,6 +35,8 @@
|
|||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
background: $avatar-background;
|
||||
overflow: hidden;
|
||||
|
||||
&.avatar-inline {
|
||||
float: none;
|
||||
|
|
|
@ -11,8 +11,17 @@
|
|||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
/*max-width: 100%;*/
|
||||
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 {
|
||||
|
|
|
@ -379,7 +379,9 @@ $issue-boards-card-shadow: rgba(186, 186, 186, 0.5);
|
|||
* Avatar
|
||||
*/
|
||||
$avatar_radius: 50%;
|
||||
$avatar-border: $border-color;
|
||||
$avatar-border: $gray-normal;
|
||||
$avatar-border-hover: $gray-darker;
|
||||
$avatar-background: $gray-lightest;
|
||||
$gl-avatar-size: 40px;
|
||||
|
||||
/*
|
||||
|
|
|
@ -11,17 +11,12 @@ module AvatarsHelper
|
|||
def user_avatar_without_link(options = {})
|
||||
avatar_size = options[:size] || 16
|
||||
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)
|
||||
data_attributes = { container: 'body' }
|
||||
|
||||
if options[:lazy]
|
||||
data_attributes[:src] = avatar_url
|
||||
end
|
||||
|
||||
image_tag(
|
||||
options[:lazy] ? '' : avatar_url,
|
||||
class: "avatar has-tooltip s#{avatar_size} #{css_class}",
|
||||
avatar_url,
|
||||
class: %W[avatar has-tooltip s#{avatar_size}].push(*options[:css_class]),
|
||||
alt: "#{user_name}'s avatar",
|
||||
title: user_name,
|
||||
data: data_attributes
|
||||
|
|
|
@ -61,8 +61,8 @@ module EmailsHelper
|
|||
else
|
||||
image_tag(
|
||||
image_url('mailers/gitlab_header_logo.gif'),
|
||||
size: "55x50",
|
||||
alt: "GitLab"
|
||||
size: '55x50',
|
||||
alt: 'GitLab'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
24
app/helpers/lazy_image_tag_helper.rb
Normal file
24
app/helpers/lazy_image_tag_helper.rb
Normal file
|
@ -0,0 +1,24 @@
|
|||
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
|
|||
def version_status_badge
|
||||
if Rails.env.production? && current_application_settings.version_check_enabled
|
||||
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
|
||||
|
|
|
@ -11,7 +11,7 @@ module CacheMarkdownField
|
|||
extend ActiveSupport::Concern
|
||||
|
||||
# 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
|
||||
INVALIDATED_BY = %w[author project].freeze
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
.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 @@
|
|||
.image
|
||||
%span.wrap
|
||||
.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)
|
||||
- else
|
||||
.image
|
||||
|
@ -16,7 +16,7 @@
|
|||
%span.wrap
|
||||
.frame.deleted
|
||||
%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
|
||||
%span.meta-filesize= number_to_human_size(old_blob.size)
|
||||
|
|
||||
|
@ -28,7 +28,7 @@
|
|||
%span.wrap
|
||||
.frame.added
|
||||
%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
|
||||
%span.meta-filesize= number_to_human_size(blob.size)
|
||||
|
|
||||
|
@ -41,10 +41,10 @@
|
|||
.swipe.view.hide
|
||||
.swipe-frame
|
||||
.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
|
||||
.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.top-handle
|
||||
%span.bottom-handle
|
||||
|
@ -52,9 +52,9 @@
|
|||
.onion-skin.view.hide
|
||||
.onion-skin-frame
|
||||
.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
|
||||
%img{ src: blob_raw_path, alt: diff_file.new_path }
|
||||
%img{ 'data-src': blob_raw_path, alt: diff_file.new_path }
|
||||
.controls
|
||||
.transparent
|
||||
.drag-track
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Lazy load images for better Frontend performance
|
||||
merge_request: 12503
|
||||
author:
|
|
@ -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
|
||||
`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
|
||||
|
||||
### Page-specific JavaScript
|
||||
|
|
|
@ -114,7 +114,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
step 'I click on image link' do
|
||||
|
|
|
@ -118,7 +118,7 @@ module Banzai
|
|||
end
|
||||
|
||||
if path
|
||||
content_tag(:img, nil, src: path, class: 'gfm')
|
||||
content_tag(:img, nil, data: { src: path }, class: 'gfm')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
16
lib/banzai/filter/image_lazy_load_filter.rb
Normal file
16
lib/banzai/filter/image_lazy_load_filter.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
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
|
|||
link = doc.document.create_element(
|
||||
'a',
|
||||
class: 'no-attachment-icon',
|
||||
href: img['src'],
|
||||
href: img['data-src'] || img['src'],
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer'
|
||||
)
|
||||
|
|
|
@ -22,6 +22,7 @@ module Banzai
|
|||
|
||||
doc.css('img, video').each do |el|
|
||||
process_link_attr el.attribute('src')
|
||||
process_link_attr el.attribute('data-src')
|
||||
end
|
||||
|
||||
doc
|
||||
|
|
|
@ -16,6 +16,7 @@ module Banzai
|
|||
Filter::MathFilter,
|
||||
Filter::UploadLinkFilter,
|
||||
Filter::VideoLinkFilter,
|
||||
Filter::ImageLazyLoadFilter,
|
||||
Filter::ImageLinkFilter,
|
||||
Filter::EmojiFilter,
|
||||
Filter::TableOfContentsFilter,
|
||||
|
|
|
@ -63,11 +63,11 @@ feature 'Admin Appearance', feature: true do
|
|||
end
|
||||
|
||||
def logo_selector
|
||||
'//img[@src^="/uploads/-/system/appearance/logo"]'
|
||||
'//img[data-src^="/uploads/-/system/appearance/logo"]'
|
||||
end
|
||||
|
||||
def header_logo_selector
|
||||
'//img[@src^="/uploads/-/system/appearance/header_logo"]'
|
||||
'//img[data-src^="/uploads/-/system/appearance/header_logo"]'
|
||||
end
|
||||
|
||||
def logo_fixture
|
||||
|
|
|
@ -100,7 +100,7 @@ describe 'GitLab Markdown', feature: true do
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
it 'permits br elements' do
|
||||
|
|
|
@ -18,7 +18,7 @@ feature 'User uploads avatar to group', feature: true do
|
|||
|
||||
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
|
||||
expect(group.reload.avatar.file).to exist
|
||||
|
|
|
@ -16,7 +16,7 @@ feature 'User uploads avatar to profile', feature: true do
|
|||
|
||||
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
|
||||
expect(user.reload.avatar.file).to exist
|
||||
|
|
|
@ -62,13 +62,13 @@ describe ApplicationHelper do
|
|||
avatar_url = "/uploads/-/system/project/avatar/#{project.id}/banana_sample.gif"
|
||||
|
||||
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)
|
||||
avatar_url = "#{gitlab_host}/uploads/-/system/project/avatar/#{project.id}/banana_sample.gif"
|
||||
|
||||
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
|
||||
|
||||
it 'gives uploaded icon when present' do
|
||||
|
@ -77,7 +77,8 @@ describe ApplicationHelper do
|
|||
allow_any_instance_of(Project).to receive(:avatar_in_git).and_return(true)
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -27,11 +27,11 @@ describe AvatarsHelper do
|
|||
|
||||
it 'displays user avatar' do
|
||||
is_expected.to eq image_tag(
|
||||
avatar_icon(user, 16),
|
||||
class: 'avatar has-tooltip s16 ',
|
||||
LazyImageTagHelper.placeholder_image,
|
||||
class: 'avatar has-tooltip s16 lazy',
|
||||
alt: "#{user.name}'s avatar",
|
||||
title: user.name,
|
||||
data: { container: 'body' }
|
||||
data: { container: 'body', src: avatar_icon(user, 16) }
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -40,22 +40,8 @@ describe AvatarsHelper do
|
|||
|
||||
it 'uses provided css_class' do
|
||||
is_expected.to eq image_tag(
|
||||
avatar_icon(user, 16),
|
||||
class: "avatar has-tooltip s16 #{options[:css_class]}",
|
||||
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 ',
|
||||
LazyImageTagHelper.placeholder_image,
|
||||
class: "avatar has-tooltip s16 #{options[:css_class]} lazy",
|
||||
alt: "#{user.name}'s avatar",
|
||||
title: user.name,
|
||||
data: { container: 'body', src: avatar_icon(user, 16) }
|
||||
|
@ -68,11 +54,11 @@ describe AvatarsHelper do
|
|||
|
||||
it 'uses provided size' do
|
||||
is_expected.to eq image_tag(
|
||||
avatar_icon(user, options[:size]),
|
||||
class: "avatar has-tooltip s#{options[:size]} ",
|
||||
LazyImageTagHelper.placeholder_image,
|
||||
class: "avatar has-tooltip s#{options[:size]} lazy",
|
||||
alt: "#{user.name}'s avatar",
|
||||
title: user.name,
|
||||
data: { container: 'body' }
|
||||
data: { container: 'body', src: avatar_icon(user, options[:size]) }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -82,11 +68,11 @@ describe AvatarsHelper do
|
|||
|
||||
it 'uses provided url' do
|
||||
is_expected.to eq image_tag(
|
||||
options[:url],
|
||||
class: 'avatar has-tooltip s16 ',
|
||||
LazyImageTagHelper.placeholder_image,
|
||||
class: 'avatar has-tooltip s16 lazy',
|
||||
alt: "#{user.name}'s avatar",
|
||||
title: user.name,
|
||||
data: { container: 'body' }
|
||||
data: { container: 'body', src: options[:url] }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -99,22 +85,22 @@ describe AvatarsHelper do
|
|||
|
||||
it 'prefers user parameter' do
|
||||
is_expected.to eq image_tag(
|
||||
avatar_icon(user, 16),
|
||||
class: 'avatar has-tooltip s16 ',
|
||||
LazyImageTagHelper.placeholder_image,
|
||||
class: 'avatar has-tooltip s16 lazy',
|
||||
alt: "#{user.name}'s avatar",
|
||||
title: user.name,
|
||||
data: { container: 'body' }
|
||||
data: { container: 'body', src: avatar_icon(user, 16) }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it 'uses user_name and user_email parameter if user is not present' do
|
||||
is_expected.to eq image_tag(
|
||||
avatar_icon(options[:user_email], 16),
|
||||
class: 'avatar has-tooltip s16 ',
|
||||
LazyImageTagHelper.placeholder_image,
|
||||
class: 'avatar has-tooltip s16 lazy',
|
||||
alt: "#{options[:user_name]}'s avatar",
|
||||
title: options[:user_name],
|
||||
data: { container: 'body' }
|
||||
data: { container: 'body', src: avatar_icon(options[:user_email], 16) }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
57
spec/javascripts/lazy_loader_spec.js
Normal file
57
spec/javascripts/lazy_loader_spec.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
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
|
|||
tag = '[[images/image.jpg]]'
|
||||
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
|
||||
|
||||
it 'does not creates img tag if image does not exist' do
|
||||
|
@ -40,7 +40,7 @@ describe Banzai::Filter::GollumTagsFilter, lib: true do
|
|||
tag = '[[http://example.com/image.jpg]]'
|
||||
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
|
||||
|
||||
it 'does not creates img tag for invalid URL' do
|
||||
|
|
19
spec/lib/banzai/filter/image_lazy_load_filter_spec.rb
Normal file
19
spec/lib/banzai/filter/image_lazy_load_filter_spec.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
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
|
|||
image = actual.at_css('img[alt="Relative Image"]')
|
||||
|
||||
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
|
||||
|
||||
|
@ -70,7 +70,7 @@ module MarkdownMatchers
|
|||
# GollumTagsFilter
|
||||
matcher :parse_gollum_tags do
|
||||
def have_image(src)
|
||||
have_css("img[src$='#{src}']")
|
||||
have_css("img[data-src$='#{src}']")
|
||||
end
|
||||
|
||||
prefix = '/namespace1/gitlabhq/wikis'
|
||||
|
|
Loading…
Reference in a new issue