Skip to content

Commit

Permalink
Merge pull request #34 from nemtsov/feature/preloader
Browse files Browse the repository at this point in the history
Add slide prefetching on hover and on next / prev
  • Loading branch information
nemtsov authored Oct 13, 2016
2 parents 30f5e43 + 828214b commit fdb546b
Show file tree
Hide file tree
Showing 11 changed files with 174 additions and 82 deletions.
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "lightbox",
"version": "5.2.0",
"version": "6.0.0",
"description": "Image lightbox",
"main": "src",
"scripts": {
"test": "npm run eslint && npm run karma -- --single-run",
"eslint": "eslint src/**/*.js test/**/*.js",
"karma": "karma start test/karma.conf.js",
"karma": "env NODE_ENV=test karma start test/karma.conf.js",
"examples-simple": "webpack-dev-server --content-base examples/simple --config ./test/webpack.config.js --entry ./examples/simple"
},
"repository": {
Expand All @@ -20,15 +20,17 @@
},
"homepage": "https://github.com/behance/lightbox#readme",
"dependencies": {
"hoverintent": "^1.0.3",
"idle-timer": "git+https://github.com/nemtsov/idle-timer#8fc0007",
"jquery": "~2.1.1",
"tinycolor2": "^1.4.1"
},
"devDependencies": {
"@claviska/jquery-offscreen": "^1.0.1",
"babel": "^6.5.2",
"babel-core": "^6.17.0",
"babel-loader": "^6.2.5",
"babel-preset-behance": "^3.0.0",
"babel-preset-behance": "^3.1.0",
"css-loader": "^0.25.0",
"es6-shim": "^0.35.1",
"eslint": "^3.7.0",
Expand Down
18 changes: 14 additions & 4 deletions sass/lightbox.scss
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,6 @@ html.lightbox-enabled {
transition: .2s;
width: 100vw;

&.hidden {
opacity: 0;
}

} // .lightbox-content


Expand All @@ -65,6 +61,20 @@ html.lightbox-enabled {
width: 100vw;
z-index: 1001;

.offscreen,
&.offscreen {
height: 1px;
overflow: hidden;
-webkit-transform: translate(-99999px);
-ms-transform: translate(-99999px);
transform: translate(-99999px);
width: 1px;
}

.hidden {
opacity: 0;
}

.lightbox-contents {
@extend %user-select-none;

Expand Down
77 changes: 60 additions & 17 deletions src/ChromeView.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@ const EXTRAS_HIDDEN_CLASS = 'extras-hidden';
const CONTENT_CLASS = 'lightbox-content';
const ENABLED_CLASS = 'lightbox-enabled';
const HIDDEN_CLASS = 'hidden';
const OFFSCREEN_CLASS = 'offscreen';
const TRANSITION_END = 'webkitTransitionEnd ontransitionend msTransitionEnd transitionend';
const JS_SLIDE_CLASS = 'js-slide';
const JS_SLIDE_CONTENT_CLASS = 'js-slide-content';

export default class ChromeView {
constructor($context, controller, props) {
this._$context = $context;
this._controller = controller;
this._props = props;
this._$view = $(lightboxTemplate);
this._$view = $(lightboxTemplate).appendTo($context);
this._$contents = this._$view.find('.js-contents');
this._$prev = this._$view.find('.js-prev');
this._$next = this._$view.find('.js-next');
Expand Down Expand Up @@ -73,24 +77,34 @@ export default class ChromeView {

this.showExtras();

this._$context.append(this._$view);
this._$view.removeClass(OFFSCREEN_CLASS);
}

renderSlide(slide) {
this._maybeHidePrevNext(slide);
this._maybeHidePrevNext();
this._appendSlide(slide);

const $next = $(`<div class="${CONTENT_CLASS} ${HIDDEN_CLASS}">`);
$next.html(this._getSlideContent(slide));
const $current = this._$contents.find(`.${JS_SLIDE_CLASS}[data-slide-is-active]`);
const $new = this._$contents.find(`.${JS_SLIDE_CLASS}[data-slide-id="${slide.id}"]`);

this._$contents.find(`.${HIDDEN_CLASS}`).remove();
this._$contents.append($next);
$current
.removeAttr('data-slide-is-active')
.find(`> .${JS_SLIDE_CONTENT_CLASS}`)
.addClass(HIDDEN_CLASS)
.one(TRANSITION_END, () => $current.remove());

this._$contents.find(`.${CONTENT_CLASS}:lt(1)`).addClass(HIDDEN_CLASS);
setTimeout(() => $next.removeClass(HIDDEN_CLASS), 0);
$new
.attr({ 'data-slide-is-active': true })
.removeClass(OFFSCREEN_CLASS)
.find(`> .${JS_SLIDE_CONTENT_CLASS}`)
.removeClass(HIDDEN_CLASS);

this._appendAdjacentSlides($current, $new);
}

destroy() {
this._$view.remove();
close() {
this._$view.addClass(OFFSCREEN_CLASS);
this._$contents.empty();

$(document)
.add(this._$context)
Expand All @@ -103,6 +117,10 @@ export default class ChromeView {
}
}

destroy() {
this._$view.remove();
}

hideExtras() {
this._$view.addClass(EXTRAS_HIDDEN_CLASS);
}
Expand All @@ -111,12 +129,37 @@ export default class ChromeView {
this._$view.removeClass(EXTRAS_HIDDEN_CLASS);
}

_appendSlide(slide) {
if (!slide || this._$contents.find(`[data-slide-id="${slide.id}"]`).length) { return; }

const $content = $('<div>')
.addClass(`${JS_SLIDE_CONTENT_CLASS} ${CONTENT_CLASS} ${HIDDEN_CLASS}`)
.html(this._getSlideContent(slide));

$('<div>', { 'data-slide-id': slide.id, class: `${JS_SLIDE_CLASS} ${OFFSCREEN_CLASS}` })
.append($content)
.appendTo(this._$contents);
}

_appendAdjacentSlides($current, $new) {
if ($current.length === 0) {
this._appendSlide(this._controller.getPrevSlide());
this._appendSlide(this._controller.getNextSlide());
}
else {
this._appendSlide($current.data('slide-id') < $new.data('slide-id')
? this._controller.getNextSlide()
: this._controller.getPrevSlide());
}
}

_bindToController() {
this._controller.on({
open: slide => { this.init(); this.renderSlide(slide); },
close: () => this.destroy(),
prev: slide => this.renderSlide(slide),
next: slide => this.renderSlide(slide)
close: () => this.close(),
destroy: () => this.destroy(),
activate: slide => this.renderSlide(slide),
prefetch: slide => this._appendSlide(slide)
});
}

Expand All @@ -125,9 +168,9 @@ export default class ChromeView {
return src ? $('<img />', { src }) : slide.content;
}

_maybeHidePrevNext(activeSlide) {
const hasPrev = this._controller.slides[activeSlide.id - 1];
const hasNext = this._controller.slides[activeSlide.id + 1];
_maybeHidePrevNext() {
const hasPrev = this._controller.getPrevSlide();
const hasNext = this._controller.getNextSlide();
if (this._props.isCircular && (hasPrev || hasNext)) { return; }
(hasPrev) ? this._$prev.removeClass(HIDDEN_CLASS) : this._$prev.addClass(HIDDEN_CLASS);
(hasNext) ? this._$next.removeClass(HIDDEN_CLASS) : this._$next.addClass(HIDDEN_CLASS);
Expand Down
88 changes: 66 additions & 22 deletions src/Controller.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import $ from 'jquery';
// due to a bug in hoverintent, just import'ing it fails webpack
import hoverintent from 'hoverintent/dist/hoverintent.min';

const SLIDE_ID_ATTR = 'lightbox-slide-id';
const LINK_CLASS = 'lightbox-link';

export default class Controller {
constructor($context, props) {
this._props = props;
this._$context = $context;
this._$eventNode = $('<e/>');
this._$links = this._$context.find(`:not(a) > ${this._props.slideSelector}`);
this._hoverlisteners = [];
this.slides = this._createSlides(this._$links);
this._isOpen = false;
this._bind();
}

Expand All @@ -26,49 +31,65 @@ export default class Controller {

open(slideId) {
const slide = this.slides[slideId];
if (!slide) { return; }
this.activeSlide = slide;
this._trigger('open', [slide]);
this._isOpen = true;
this._trigger('open', slide);
}

close() {
this._isOpen = false;
this._trigger('close');
}

next() {
const nextId = this.activeSlide.id + 1;
const next = this.slides[nextId];
if (!this._props.isCircular && !next) { return; }
const firstId = 0;
this.activeSlide = this.slides[next ? nextId : firstId];
this._trigger('next', this.activeSlide);
this.activateSlide(this.getNextSlide());
}

prev() {
const prevId = this.activeSlide.id - 1;
const prev = this.slides[prevId];
if (!this._props.isCircular && !prev) { return; }
const lastId = this.slides.length - 1;
this.activeSlide = this.slides[prev ? prevId : lastId];
this._trigger('prev', this.activeSlide);
this.activateSlide(this.getPrevSlide());
}

getNextSlide() {
return this._getSlideByDirection(1);
}

getPrevSlide() {
return this._getSlideByDirection(-1);
}

activateSlide(slide) {
if (!slide) { return; }
this.activeSlide = slide;
this._trigger('activate', slide);
}

destroy() {
this.close();
this._$eventNode.off();
if (this._isOpen) {
this.close();
}
this._removePrefetchOnHover();
this._$links
.removeClass('lightbox-link')
.removeClass(LINK_CLASS)
.removeData(SLIDE_ID_ATTR)
.off('click');
.off('.lightbox');
this._trigger('destroy');
this._$eventNode.off();
}

_bind() {
const self = this;
this._$links.addClass('lightbox-link')
.each((id, el) => $(el).data(SLIDE_ID_ATTR, id))
.click(function(e) {
this._$links.addClass(LINK_CLASS)
.each((id, el) => {
this._addPrefetchOnHover(el, id);
$(el).data(SLIDE_ID_ATTR, id);
})
.on('mousedown.lightbox', function() {
self._trigger('prefetch', self.slides[$(this).data(SLIDE_ID_ATTR)]);
})
.on('click.lightbox', function(e) {
e.stopPropagation();
self.open(self.slides.filter(
slide => slide.id === $(this).data(SLIDE_ID_ATTR))[0].id);
self.open($(this).data(SLIDE_ID_ATTR));
});
}

Expand All @@ -88,4 +109,27 @@ export default class Controller {
};
});
}

_getSlideByDirection(direction) {
const id = this.activeSlide.id + direction;
const slide = this.slides[id];
if (slide) { return slide; }
if (this._props.isCircular) {
return (direction === -1) ? (this.slides.length - 1) : 0;
}
}

_addPrefetchOnHover(el, id) {
this._hoverlisteners.push(
hoverintent(
el,
() => this._trigger('prefetch', this.slides[id]),
() => {}
)
);
}

_removePrefetchOnHover() {
this._hoverlisteners.forEach(listener => listener.remove());
}
}
6 changes: 2 additions & 4 deletions src/templates.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,14 @@ const closeControl = `
`;

export const lightbox = `
<div class="js-lightbox-wrap" id="lightbox-wrap">
<div class="js-lightbox-wrap offscreen" id="lightbox-wrap">
${backdrop}
<div class="js-lightbox-inner-wrap" id="lightbox-inner-wrap">
<div class="js-img-wrap" id="lightbox-img-wrap">
${prevControl}
${nextControl}
${closeControl}
<div class="lightbox-contents js-contents">
<div class="lightbox-content"></div>
</div>
<div class="lightbox-contents js-contents"></div>
</div>
</div>
</div>
Expand Down
7 changes: 0 additions & 7 deletions test/fixtures/css/disable_animations.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,6 @@
-webkit-transition-property: none !important;
transition-property: none !important;

/*CSS transforms*/
-o-transform: none !important;
-moz-transform: none !important;
-ms-transform: none !important;
-webkit-transform: none !important;
transform: none !important;

/*CSS animations*/
-webkit-animation: none !important;
-moz-animation: none !important;
Expand Down
3 changes: 2 additions & 1 deletion test/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module.exports = function(config) {
'test/fixtures/css/disable_animations.css',
require.resolve('es6-shim'),
'node_modules/jquery/dist/jquery.js',
'node_modules/@claviska/jquery-offscreen/jquery.offscreen.js',
'node_modules/jasmine-jquery/lib/jasmine-jquery.js',
'node_modules/jasmine-fixture/dist/jasmine-fixture.js',
{
Expand All @@ -24,7 +25,7 @@ module.exports = function(config) {
'test/specs/**/*.js'
],
preprocessors: {
'test/specs/**/*.js': ['webpack']
'test/specs/**/*.js': ['webpack', 'sourcemap']
},
webpack: webpackConfig,
reporters: ['progress', 'coverage'],
Expand Down
Loading

0 comments on commit fdb546b

Please sign in to comment.