Jelajahi Sumber

Restructure components for refactoring, autoWidth and "destroy" option.

NaotoshiFujita 5 tahun lalu
induk
melakukan
ac771b6563
51 mengubah file dengan 2333 tambahan dan 1732 penghapusan
  1. 1 1
      dist/css/splide-core.min.css
  2. 0 0
      dist/css/splide.min.css
  3. 501 406
      dist/js/splide.js
  4. 1 1
      dist/js/splide.min.js
  5. TEMPAT SAMPAH
      dist/js/splide.min.js.gz
  6. 487 408
      package-lock.json
  7. 7 7
      package.json
  8. 21 20
      src/js/components/a11y/index.js
  9. 11 5
      src/js/components/arrows/index.js
  10. 12 12
      src/js/components/autoplay/index.js
  11. 54 9
      src/js/components/breakpoints/index.js
  12. 46 24
      src/js/components/clones/index.js
  13. 7 7
      src/js/components/controller/index.js
  14. 8 6
      src/js/components/cover/index.js
  15. 29 26
      src/js/components/drag/index.js
  16. 130 20
      src/js/components/elements/index.js
  17. 196 0
      src/js/components/elements/slide.js
  18. 0 4
      src/js/components/index.js
  19. 20 6
      src/js/components/keyboard/index.js
  20. 57 43
      src/js/components/layout/directions/horizontal.js
  21. 40 56
      src/js/components/layout/directions/vertical.js
  22. 22 159
      src/js/components/layout/index.js
  23. 19 9
      src/js/components/lazyload/index.js
  24. 2 6
      src/js/components/options/index.js
  25. 11 12
      src/js/components/pagination/index.js
  26. 1 1
      src/js/components/slides/index.js
  27. 181 187
      src/js/components/slides/slide.js
  28. 1 3
      src/js/components/sync/index.js
  29. 126 0
      src/js/components/track/directions/horizontal.js
  30. 27 19
      src/js/components/track/directions/vertical.js
  31. 31 47
      src/js/components/track/index.js
  32. 0 100
      src/js/components/track/resolvers/horizontal.js
  33. 9 0
      src/js/constants/defaults.js
  34. 8 1
      src/js/constants/states.js
  35. 16 11
      src/js/core/event.js
  36. 30 22
      src/js/splide.js
  37. 6 11
      src/js/transitions/fade/index.js
  38. 5 4
      src/js/transitions/slide/index.js
  39. 47 15
      src/js/utils/dom.js
  40. 30 12
      src/js/utils/object.js
  41. 16 18
      src/js/utils/time.js
  42. 23 16
      src/js/utils/utils.js
  43. 1 0
      src/sass/core/object/objects/_list.scss
  44. 4 0
      src/sass/core/object/objects/_root.scss
  45. 3 6
      tests/core/splide.test.js
  46. 18 0
      tests/data/html.js
  47. 49 0
      tests/functionality/autowidth.test.js
  48. 3 3
      tests/functionality/elements.test.js
  49. 2 2
      tests/functionality/focus.test.js
  50. 13 6
      tests/functionality/layout.test.js
  51. 1 1
      tests/functionality/loop.test.js

+ 1 - 1
dist/css/splide-core.min.css

@@ -1 +1 @@
-@keyframes splide-loading{0%{transform:rotate(0)}to{transform:rotate(1turn)}}.splide__container{position:relative;box-sizing:border-box}.splide__list{display:flex;margin:0!important;padding:0!important}.splide__pagination{display:inline-flex;align-items:center}.splide__pagination li{list-style-type:none;display:inline-block;line-height:1;margin:0}.splide{position:relative;visibility:hidden}.splide__slide{position:relative;box-sizing:border-box;list-style-type:none!important;margin:0}.splide__slide img{vertical-align:bottom}.splide__slider{position:relative}.splide__spinner{position:absolute;top:0;left:0;right:0;bottom:0;margin:auto;display:inline-block;width:20px;height:20px;border-radius:50%;border:2px solid #999;border-left-color:transparent;animation:splide-loading 1s linear infinite}.splide__track{position:relative;z-index:0;overflow:hidden}.splide--draggable>.splide__track>.splide__list>.splide__slide{-webkit-user-select:none;user-select:none}.splide--fade>.splide__track>.splide__list{display:block}.splide--fade>.splide__track>.splide__list>.splide__slide{position:absolute;top:0;left:0;z-index:0;opacity:0}.splide--fade>.splide__track>.splide__list>.splide__slide.is-active{position:relative;z-index:1;opacity:1}.splide--rtl{direction:rtl}.splide--ttb>.splide__track>.splide__list{display:block}
+@keyframes splide-loading{0%{transform:rotate(0)}to{transform:rotate(1turn)}}.splide__container{position:relative;box-sizing:border-box}.splide__list{display:flex;flex-wrap:wrap;margin:0!important;padding:0!important}.splide__pagination{display:inline-flex;align-items:center}.splide__pagination li{list-style-type:none;display:inline-block;line-height:1;margin:0}.splide{position:relative;visibility:hidden}.splide.is-active{visibility:visible}.splide__slide{position:relative;box-sizing:border-box;list-style-type:none!important;margin:0}.splide__slide img{vertical-align:bottom}.splide__slider{position:relative}.splide__spinner{position:absolute;top:0;left:0;right:0;bottom:0;margin:auto;display:inline-block;width:20px;height:20px;border-radius:50%;border:2px solid #999;border-left-color:transparent;animation:splide-loading 1s linear infinite}.splide__track{position:relative;z-index:0;overflow:hidden}.splide--draggable>.splide__track>.splide__list>.splide__slide{-webkit-user-select:none;user-select:none}.splide--fade>.splide__track>.splide__list{display:block}.splide--fade>.splide__track>.splide__list>.splide__slide{position:absolute;top:0;left:0;z-index:0;opacity:0}.splide--fade>.splide__track>.splide__list>.splide__slide.is-active{position:relative;z-index:1;opacity:1}.splide--rtl{direction:rtl}.splide--ttb>.splide__track>.splide__list{display:block}

File diff ditekan karena terlalu besar
+ 0 - 0
dist/css/splide.min.css


File diff ditekan karena terlalu besar
+ 501 - 406
dist/js/splide.js


File diff ditekan karena terlalu besar
+ 1 - 1
dist/js/splide.min.js


TEMPAT SAMPAH
dist/js/splide.min.js.gz


File diff ditekan karena terlalu besar
+ 487 - 408
package-lock.json


+ 7 - 7
package.json

@@ -1,6 +1,6 @@
 {
   "name": "@splidejs/splide",
-  "version": "1.4.1",
+  "version": "2.0.0",
   "description": "Splide is a lightweight and powerful slider without any dependencies.",
   "author": "Naotoshi Fujita",
   "license": "MIT",
@@ -22,10 +22,10 @@
     "url": "https://github.com/Splidejs/splide/issues"
   },
   "devDependencies": {
-    "@babel/core": "^7.7.7",
-    "@babel/preset-env": "^7.7.7",
-    "autoprefixer": "^9.7.3",
-    "babel-jest": "^24.9.0",
+    "@babel/core": "^7.8.4",
+    "@babel/preset-env": "^7.8.4",
+    "autoprefixer": "^9.7.4",
+    "babel-jest": "^25.1.0",
     "babel-loader": "^8.0.6",
     "cssnano": "^4.1.10",
     "gulp": "^4.0.2",
@@ -34,11 +34,11 @@
     "gulp-gzip": "^1.4.2",
     "gulp-postcss": "^8.0.0",
     "gulp-rename": "^2.0.0",
-    "gulp-rollup": "^2.16.2",
+    "gulp-rollup": "^2.17.0",
     "gulp-sass": "^4.0.2",
     "gulp-sass-glob": "^1.1.0",
     "gulp-uglify": "^3.0.2",
-    "jest": "^24.9.0",
+    "jest": "^25.1.0",
     "merge-stream": "^2.0.0",
     "serialize-javascript": "^2.1.2",
     "uglifyjs-webpack-plugin": "^2.2.0",

+ 21 - 20
src/js/components/a11y/index.js

@@ -26,6 +26,13 @@ export default ( Splide, Components ) => {
 	 */
 	const i18n = Splide.i18n;
 
+	/**
+	 * Hold the Elements component.
+	 *
+	 * @type {Object}
+	 */
+	const Elements = Components.Elements;
+
 	/**
 	 * A11y component object.
 	 *
@@ -65,14 +72,12 @@ export default ( Splide, Components ) => {
 		 * Destroy.
 		 */
 		destroy() {
-			const Elements = Components.Elements;
-			const arrows   = Components.Arrows.arrows;
-
-			Elements.slides
-				.concat( [ arrows.prev, arrows.next, Elements.play, Elements.pause ] )
-				.forEach( elm => {
-					removeAttribute( elm, [ ARIA_HIDDEN, TAB_INDEX, ARIA_CONTROLS, ARIA_LABEL, ARIA_CURRENRT, 'role' ] );
-				} );
+			const arrows = Components.Arrows.arrows;
+
+			removeAttribute(
+				Elements.slides.concat( [ arrows.prev, arrows.next, Elements.play, Elements.pause ] ),
+				[ ARIA_HIDDEN, TAB_INDEX, ARIA_CONTROLS, ARIA_LABEL, ARIA_CURRENRT, 'role' ]
+			);
 		},
 	};
 
@@ -95,7 +100,7 @@ export default ( Splide, Components ) => {
 	 * @param {Element} next - Next arrow element.
 	 */
 	function initArrows( prev, next ) {
-		const controls = Components.Elements.track.id;
+		const controls = Elements.track.id;
 
 		setAttribute( prev, ARIA_CONTROLS, controls );
 		setAttribute( next, ARIA_CONTROLS, controls );
@@ -110,7 +115,7 @@ export default ( Splide, Components ) => {
 	 * @param {number}  nextIndex - Next slide index or -1 when there is no next slide.
 	 */
 	function updateArrows( prev, next, prevIndex, nextIndex ) {
-		const index = Splide.index;
+		const index     = Splide.index;
 		const prevLabel = prevIndex > -1 && index < prevIndex ? i18n.last : i18n.prev;
 		const nextLabel = nextIndex > -1 && index > nextIndex ? i18n.first : i18n.next;
 
@@ -163,16 +168,16 @@ export default ( Splide, Components ) => {
 	 * Initialize autoplay buttons.
 	 */
 	function initAutoplay() {
-		const Elements = Components.Elements;
+		[ 'play', 'pause' ].forEach( name => {
+			const elm = Elements[ name ];
 
-		[ Elements.play, Elements.pause ].forEach( ( elm, index ) => {
 			if ( elm ) {
 				if ( ! isButton( elm ) ) {
 					setAttribute( elm, 'role', 'button' );
 				}
 
 				setAttribute( elm, ARIA_CONTROLS, Elements.track.id );
-				setAttribute( elm, ARIA_LABEL, i18n[ index === 0 ? 'play' : 'pause' ] );
+				setAttribute( elm, ARIA_LABEL, i18n[ name ] );
 			}
 		} );
 	}
@@ -184,18 +189,14 @@ export default ( Splide, Components ) => {
 	 * @param {Splide} main - A main Splide instance.
 	 */
 	function initNavigation( main ) {
-		const Slides = Components.Slides.getSlides( true, true );
-
-		Slides.forEach( Slide => {
-			const slide = Slide.slide;
-
+		Elements.each( ( { slide, realIndex, index } ) => {
 			if ( ! isButton( slide ) ) {
 				setAttribute( slide, 'role', 'button' );
 			}
 
-			const slideIndex = Slide.realIndex > -1 ? Slide.realIndex : Slide.index;
+			const slideIndex = realIndex > -1 ? realIndex : index;
 			const label      = sprintf( i18n.slideX, slideIndex + 1 );
-			const mainSlide  = main.Components.Slides.getSlide( slideIndex );
+			const mainSlide  = main.Components.Elements.getSlide( slideIndex );
 
 			setAttribute( slide, ARIA_LABEL, label );
 

+ 11 - 5
src/js/components/arrows/index.js

@@ -7,6 +7,7 @@
 
 import { create, append, before, domify, remove, removeAttribute } from '../../utils/dom';
 import { XML_NAME_SPACE, PATH, SIZE } from './path';
+import { LOOP } from "../../constants/types";
 
 
 /**
@@ -54,6 +55,13 @@ export default ( Splide, Components, name ) => {
 	 */
 	let created;
 
+	/**
+	 * Hold the Elements component.
+	 *
+	 * @type {Object}
+	 */
+	const Elements = Components.Elements;
+
 	/**
 	 * Arrows component object.
 	 *
@@ -71,8 +79,6 @@ export default ( Splide, Components, name ) => {
 		 * Called when the component is mounted.
 		 */
 		mount() {
-			const Elements = Components.Elements;
-
 			// Attempt to get arrows from HTML source.
 			prev = Elements.arrows.prev;
 			next = Elements.arrows.next;
@@ -104,7 +110,7 @@ export default ( Splide, Components, name ) => {
 		 * Destroy.
 		 */
 		destroy() {
-			[ prev, next ].forEach( elm => { removeAttribute( elm, 'disabled' ) } );
+			removeAttribute( [ prev, next ], 'disabled' );
 
 			if ( created ) {
 				remove( prev.parentElement );
@@ -137,7 +143,7 @@ export default ( Splide, Components, name ) => {
 	 */
 	function updateDisabled() {
 		const { prevIndex, nextIndex } = Components.Controller;
-		const isEnough = Splide.length > Splide.options.perPage;
+		const isEnough = Splide.length > Splide.options.perPage || Splide.is( LOOP );
 
 		prev.disabled = prevIndex < 0 || ! isEnough;
 		next.disabled = nextIndex < 0 || ! isEnough;
@@ -154,7 +160,7 @@ export default ( Splide, Components, name ) => {
 		append( wrapper, prev );
 		append( wrapper, next );
 
-		const slider = Components.Elements.slider;
+		const slider = Elements.slider;
 		const parent = Splide.options.arrows === 'slider' && slider ? slider : root;
 
 		before( wrapper, parent.firstElementChild );

+ 12 - 12
src/js/components/autoplay/index.js

@@ -42,6 +42,13 @@ export default ( Splide, Components, name ) => {
 	 */
 	let interval;
 
+	/**
+	 * Keep the Elements component.
+	 *
+	 * @type {string}
+	 */
+	const Elements = Components.Elements;
+
 	/**
 	 * Autoplay component object.
 	 *
@@ -61,14 +68,13 @@ export default ( Splide, Components, name ) => {
 		 */
 		mount() {
 			const options = Splide.options;
-			const { slides, bar } = Components.Elements;
 
-			if ( slides.length > options.perPage ) {
+			if ( Elements.slides.length > options.perPage ) {
 				interval = createInterval( () => { Splide.go( '>' ) }, options.interval, rate => {
 					Splide.emit( `${ name }:playing`, rate );
 
-					if ( bar ) {
-						applyStyle( bar, { width: `${ rate * 100 }%` } );
+					if ( Elements.bar ) {
+						applyStyle( Elements.bar, { width: `${ rate * 100 }%` } );
 					}
 				} );
 
@@ -115,7 +121,6 @@ export default ( Splide, Components, name ) => {
 	 */
 	function bind() {
 		const options  = Splide.options;
-		const Elements = Components.Elements;
 		const sibling  = Splide.sibling;
 		const elms     = [ Splide.root, sibling ? sibling.root : null ];
 
@@ -135,13 +140,8 @@ export default ( Splide, Components, name ) => {
 				Autoplay.play( PAUSE_FLAGS.FOCUS );
 				Autoplay.play( PAUSE_FLAGS.MANUAL );
 			}, Elements.play )
-			.on( 'move', () => {
-				// Rewind the timer when others move the slide.
-				Autoplay.play();
-			} )
-			.on( 'destroy', () => {
-				Autoplay.pause();
-			} );
+			.on( 'move refresh', () => { Autoplay.play() } ) // Rewind the timer.
+			.on( 'destroy', () => {	Autoplay.pause() } );
 
 		switchOn( [ Elements.pause ], 'click', PAUSE_FLAGS.MANUAL, false );
 	}

+ 54 - 9
src/js/components/breakpoints/index.js

@@ -5,6 +5,16 @@
  * @copyright Naotoshi Fujita. All rights reserved.
  */
 
+import { throttle } from "../../utils/time";
+import { DESTROYED } from "../../constants/states";
+
+/**
+ * Interval time for throttle.
+ *
+ * @type {number}
+ */
+const THROTTLE = 50;
+
 /**
  * The component for updating options according to a current window width.
  *
@@ -20,6 +30,13 @@ export default ( Splide ) => {
 	 */
 	const breakpoints = Splide.options.breakpoints;
 
+	/**
+	 * The check function whose frequency of call is reduced.
+	 *
+	 * @type {Function}
+	 */
+	const throttledCheck = throttle( check, THROTTLE );
+
 	/**
 	 * Keep initial options.
 	 *
@@ -62,7 +79,12 @@ export default ( Splide ) => {
 				.sort( ( n, m ) => parseInt( n ) - parseInt( m ) )
 				.map( point => ( { point, mql: matchMedia( `(max-width:${ point }px)` ) } ) );
 
-			bind();
+			/*
+			 * To keep monitoring resize event after destruction without "completely",
+			 * use native addEventListener instead of Splide.on.
+			 */
+			this.destroy( true );
+			addEventListener( 'resize', throttledCheck );
 		},
 
 		/**
@@ -71,21 +93,44 @@ export default ( Splide ) => {
 		 */
 		mounted() {
 			initialOptions = Splide.options;
+			check();
+		},
+
+		/**
+		 * Destroy.
+		 *
+		 * @param {boolean} completely - Whether to destroy Splide completely.
+		 */
+		destroy( completely ) {
+			if ( completely ) {
+				removeEventListener( 'resize', throttledCheck );
+			}
 		},
 	};
 
 	/**
-	 * Listen some events to update options when media query is changed.
+	 * Check the breakpoint.
 	 */
-	function bind() {
-		Splide.on( 'mounted resize', () => {
-			const point = getPoint();
+	function check() {
+		const point = getPoint();
 
-			if ( point !== prevPoint ) {
-				Splide.options = breakpoints[ point ] || initialOptions;
-				prevPoint = point;
+		if ( point !== prevPoint ) {
+			const options = breakpoints[ point ] || initialOptions;
+			const destroy = options.destroy;
+
+			if ( destroy ) {
+				Splide.options = initialOptions;
+				Splide.destroy( destroy === 'completely' );
+			} else {
+				if ( Splide.State.is( DESTROYED ) ) {
+					Splide.mount();
+				} else {
+					Splide.options = options;
+				}
 			}
-		} );
+
+			prevPoint = point;
+		}
 	}
 
 	/**

+ 46 - 24
src/js/components/clones/index.js

@@ -25,6 +25,13 @@ export default ( Splide, Components ) => {
 	 */
 	let clones = [];
 
+	/**
+	 * Keep Elements component.
+	 *
+	 * @type {Object}
+	 */
+	const Elements = Components.Elements;
+
 	/**
 	 * Clones component object.
 	 *
@@ -74,39 +81,54 @@ export default ( Splide, Components ) => {
 
 	/**
 	 * Generate and append clones.
-	 * Clone count is determined by:
-	 * - Max pages a flick action can move.
-	 * - Whether the slide length is enough for perPage.
 	 */
 	function generateClones() {
-		const Slides = Components.Slides;
-		const { perPage, drag, flickMaxPages } = Splide.options;
-		const length = Slides.length;
-		const count  = perPage * ( drag ? flickMaxPages + 1 : 1 ) + ( length < perPage ? perPage : 0 );
+		const length = Elements.length;
 
-		if ( length ) {
-			let slides = Slides.getSlides( false, false );
+		if ( ! length ) {
+			return;
+		}
 
-			while ( slides.length < count ) {
-				slides = slides.concat( slides );
-			}
+		const count = getCloneCount();
+		let slides  = Elements.slides;
+
+		while ( slides.length < count ) {
+			slides = slides.concat( slides );
+		}
+
+		slides.slice( 0, count ).forEach( ( elm, index ) => {
+			const clone = cloneDeeply( elm );
+			append( Elements.list, clone );
+			clones.push( clone );
 
-			slides.slice( 0, count ).forEach( ( elm, index ) => {
-				const clone = cloneDeeply( elm );
-				append( Components.Elements.list, clone );
-				clones.push( clone );
+			Elements.register( clone, index + length, index );
+		} );
 
-				Slides.register( index + length, index, clone );
-			} );
+		slides.slice( -count ).forEach( ( elm, index ) => {
+			const clone = cloneDeeply( elm );
+			before( clone, slides[0] );
+			clones.push( clone );
 
-			slides.slice( -count ).forEach( ( elm, index ) => {
-				const clone = cloneDeeply( elm );
-				before( clone, slides[0] );
-				clones.push( clone );
+			Elements.register( clone, index - count, index );
+		} );
+	}
 
-				Slides.register( index - count, index, clone );
-			} );
+	/**
+	 * Return half count of clones to be generated.
+	 * Clone count is determined by:
+	 * - Max pages a flick action can move.
+	 * - Whether the slide length is enough for perPage.
+	 *
+	 * @return {number} - Count for clones.
+	 */
+	function getCloneCount() {
+		const options = Splide.options;
+
+		if ( options.autoWidth ) {
+			return Elements.length;
 		}
+
+		return options.perPage * ( options.drag ? options.flickMaxPages + 1 : 1 );
 	}
 
 	/**

+ 7 - 7
src/js/components/controller/index.js

@@ -78,8 +78,8 @@ export default ( Splide, Components ) => {
 			let index = Splide.index;
 
 			const matches   = String( control ).match( /([+\-<>])(\d+)?/ );
-			const indicator = matches ? matches[ 1 ] || '' : '';
-			const number    = matches ? parseInt( matches[ 2 ] ) : 0;
+			const indicator = matches ? matches[1] : '';
+			const number    = matches ? parseInt( matches[2] ) : 0;
 
 			switch ( indicator ) {
 				case '+':
@@ -91,11 +91,11 @@ export default ( Splide, Components ) => {
 					break;
 
 				case '>':
-					index = this.pageToIndex( number > -1 ? number : this.indexToPage( index ) + 1 );
+					index = this.toIndex( number > -1 ? number : this.toPage( index ) + 1 );
 					break;
 
 				case '<':
-					index = this.pageToIndex( number > -1 ? number : this.indexToPage( index ) - 1 );
+					index = this.toIndex( number > -1 ? number : this.toPage( index ) - 1 );
 					break;
 
 				default:
@@ -112,7 +112,7 @@ export default ( Splide, Components ) => {
 		 *
 		 * @return {number} - A computed page number.
 		 */
-		pageToIndex( page ) {
+		toIndex( page ) {
 			if ( hasFocus() ) {
 				return page;
 			}
@@ -138,7 +138,7 @@ export default ( Splide, Components ) => {
 		 *
 		 * @return {number} - A computed page number.
 		 */
-		indexToPage( index ) {
+		toPage( index ) {
 			if ( hasFocus() ) {
 				return index;
 			}
@@ -287,7 +287,7 @@ export default ( Splide, Components ) => {
 	 * @return {boolean} - True if a slider has the focus option.
 	 */
 	function hasFocus() {
-		return Splide.options.focus !== false;
+		return options.focus !== false;
 	}
 
 	return Controller;

+ 8 - 6
src/js/components/cover/index.js

@@ -5,7 +5,7 @@
  * @copyright Naotoshi Fujita. All rights reserved.
  */
 
-import { find, applyStyle } from '../../utils/dom';
+import { applyStyle, child } from '../../utils/dom';
 
 
 /**
@@ -42,7 +42,7 @@ export default ( Splide, Components ) => {
 		 */
 		mount() {
 			apply( false );
-			Splide.on( 'lazyload:loaded', img => { cover( img ) } );
+			Splide.on( 'lazyload:loaded', img => { cover( img, false ) } );
 			Splide.on( 'updated', () => apply( false ) );
 		},
 
@@ -56,10 +56,12 @@ export default ( Splide, Components ) => {
 
 	/**
 	 * Apply "cover" to all slides.
+	 *
+	 * @param {boolean} uncover - If true, "cover" will be clear.
 	 */
 	function apply( uncover ) {
-		Components.Slides.getSlides( true, false ).forEach( slide => {
-			const img = find( slide, 'img' );
+		Components.Elements.each( Slide => {
+			const img = child( Slide.slide, 'img' ) || child( Slide.container, 'img' );
 
 			if ( img && img.src ) {
 				cover( img, uncover );
@@ -71,9 +73,9 @@ export default ( Splide, Components ) => {
 	 * Set background image of the parent element, using source of the given image element.
 	 *
 	 * @param {Element} img     - An image element.
-	 * @param {boolean} uncover - Optional. Reset "cover".
+	 * @param {boolean} uncover - Reset "cover".
 	 */
-	function cover( img, uncover = false ) {
+	function cover( img, uncover ) {
 		applyStyle( img.parentElement, { background: uncover ? '' : `center/cover no-repeat url("${ img.src }")` } );
 		applyStyle( img, { display: uncover ? '' : 'none' } );
 	}

+ 29 - 26
src/js/components/drag/index.js

@@ -11,15 +11,17 @@ import { between } from '../../utils/utils';
 import { IDLE } from '../../constants/states';
 import { each } from "../../utils/object";
 
+const { abs } = Math;
+
 
 /**
  * Adjust how much the track can be pulled on the first or last page.
  * The larger number this is, the farther the track moves.
- * This should be around 5.
+ * This should be around 5 - 9.
  *
  * @type {number}
  */
-const FRICTION_REDUCER = 5;
+const FRICTION_REDUCER = 7;
 
 /**
  * To start dragging the track, the drag angle must be less than this threshold.
@@ -85,7 +87,7 @@ export default ( Splide, Components ) => {
 	 *
 	 * @type {boolean}
 	 */
-	let isDragging = false;
+	let isDragging;
 
 	/**
 	 * Whether the slider direction is vertical or not.
@@ -130,12 +132,15 @@ export default ( Splide, Components ) => {
 			Splide
 				.on( 'touchstart mousedown', start, list )
 				.on( 'touchmove mousemove', move, list, { passive: false } )
-				.on( 'touchend touchcancel mouseleave mouseup dragend', end, list );
-
-			// Prevent dragging an image or anchor itself.
-			each( list.querySelectorAll( 'img, a' ), elm => {
-				Splide.on( 'dragstart', e => { e.preventDefault() }, elm, { passive: false } );
-			} );
+				.on( 'touchend touchcancel mouseleave mouseup dragend', end, list )
+				.on( 'mounted refresh', () => {
+					// Prevent dragging an image or anchor itself.
+					each( list.querySelectorAll( 'img, a' ), elm => {
+						Splide
+							.off( 'dragstart', elm )
+							.on( 'dragstart', e => { e.preventDefault() }, elm, { passive: false } );
+					} );
+				} );
 		},
 	};
 
@@ -186,7 +191,7 @@ export default ( Splide, Components ) => {
 	 */
 	function shouldMove( { offset } ) {
 		if ( Splide.State.is( IDLE ) ) {
-			let angle = Math.atan( Math.abs( offset.y ) / Math.abs( offset.x ) ) * 180 / Math.PI;
+			let angle = Math.atan( abs( offset.y ) / abs( offset.x ) ) * 180 / Math.PI;
 
 			if ( isVertical ) {
 				angle = 90 - angle;
@@ -207,17 +212,16 @@ export default ( Splide, Components ) => {
 	 */
 	function resist( position ) {
 		if ( ! Splide.is( LOOP ) ) {
-			const { trim, toPosition } = Track;
-			const sign  = Controller.isRtl() ? -1 : 1;
-			const start = sign * trim( toPosition( 0 ) );
-			const end   = sign * trim( toPosition( Controller.edgeIndex ) );
+			const sign  = Track.sign;
+			const start = sign * Track.trim( Track.toPosition( 0 ) );
+			const end   = sign * Track.trim( Track.toPosition( Controller.edgeIndex ) );
 
 			position *= sign;
 
-			if ( position > start ) {
-				position = FRICTION_REDUCER * Math.log( position - start ) + start;
-			}	else if ( position < end ) {
-				position = -FRICTION_REDUCER * Math.log( end - position ) + end;
+			if ( position < start ) {
+				position = start - FRICTION_REDUCER * Math.log( start - position );
+			}	else if ( position > end ) {
+				position = end + FRICTION_REDUCER * Math.log( position - end );
 			}
 
 			position *= sign;
@@ -246,7 +250,7 @@ export default ( Splide, Components ) => {
 	 */
 	function go( info ) {
 		const velocity = info.velocity[ axis ];
-		const absV     = Math.abs( velocity );
+		const absV     = abs( velocity );
 
 		if ( absV > 0 ) {
 			const Layout  = Components.Layout;
@@ -255,7 +259,7 @@ export default ( Splide, Components ) => {
 
 			let destination = Track.position;
 
-			if ( absV > options.flickThreshold && Math.abs( info.offset[ axis ] ) < SWIPE_THRESHOLD ) {
+			if ( absV > options.flickThreshold && abs( info.offset[ axis ] ) < SWIPE_THRESHOLD ) {
 				destination += sign * Math.min( absV * options.flickPower, Layout.width * ( options.flickMaxPages || 1 ) );
 			}
 
@@ -263,7 +267,7 @@ export default ( Splide, Components ) => {
 
 			// Do not allow the track to go to a previous position.
 			if ( index === Splide.index ) {
-				index += Controller.isRtl() ? sign : -sign;
+				index += sign * Track.sign;
 			}
 
 			if ( ! Splide.is( LOOP ) ) {
@@ -284,19 +288,18 @@ export default ( Splide, Components ) => {
 	 */
 	function analyze( e, startInfo ) {
 		const { timeStamp, touches } = e;
-		const { clientX, clientY } = touches ? touches[ 0 ] : e;
+		const { clientX, clientY } = touches ? touches[0] : e;
 		const { x: fromX = clientX, y: fromY = clientY } = startInfo.to || {};
 
-		const startTime = startInfo.timeStamp || 0;
+		const startTime = startInfo.time || 0;
 		const offset    = { x: clientX - fromX, y: clientY - fromY };
 		const duration  = timeStamp - startTime;
 		const velocity  = { x: offset.x / duration, y: offset.y / duration };
 
 		return {
-			from: { x: fromX, y: fromY },
-			to  : { x: clientX, y: clientY },
+			to: { x: clientX, y: clientY },
 			offset,
-			timeStamp,
+			time: timeStamp,
 			velocity,
 		};
 	}

+ 130 - 20
src/js/components/elements/index.js

@@ -5,9 +5,23 @@
  * @copyright Naotoshi Fujita. All rights reserved.
  */
 
-import { find, addClass, removeClass, child, remove, append, before, domify } from '../../utils/dom';
+import Slide from './slide';
+import {
+	find,
+	addClass,
+	removeClass,
+	child,
+	remove,
+	append,
+	before,
+	domify,
+	applyStyle,
+	loaded,
+} from '../../utils/dom';
 import { exist } from '../../utils/error';
 import { values } from '../../utils/object';
+import { pad } from "../../utils/utils";
+
 
 /**
  * The property name for UID stored in a window object.
@@ -20,11 +34,12 @@ const UID_NAME = 'uid';
 /**
  * The component for main elements.
  *
- * @param {Splide} Splide - A Splide instance.
+ * @param {Splide} Splide     - A Splide instance.
+ * @param {Object} Components - An object containing components.
  *
  * @return {Object} - The component object.
  */
-export default ( Splide ) => {
+export default ( Splide, Components ) => {
 	/**
 	 * Hold the root element.
 	 *
@@ -39,6 +54,13 @@ export default ( Splide ) => {
 	 */
 	const classes = Splide.classes;
 
+	/**
+	 * Store Slide objects.
+	 *
+	 * @type {Array}
+	 */
+	let Slides = [];
+
 	/*
 	 * Assign unique ID to the root element if it doesn't have the one.
 	 * Note that IE doesn't support padStart() to fill the uid by 0.
@@ -47,7 +69,7 @@ export default ( Splide ) => {
 		window.splide = window.splide || {};
 		let uid = window.splide[ UID_NAME ] || 0;
 		window.splide[ UID_NAME ] = ++uid;
-		root.id = `splide${ uid < 10 ? '0' + uid : uid }`;
+		root.id = `splide${ pad( uid ) }`;
 	}
 
 	/**
@@ -61,15 +83,11 @@ export default ( Splide ) => {
 		 * Collect main elements and store them as member properties.
 		 */
 		mount() {
-			const message = 'was not found.';
-
 			this.slider = child( root, classes.slider );
+			this.track  = find( root, `.${classes.track}` );
+			this.list   = child( this.track, classes.list );
 
-			this.track = find( root, `.${ classes.track }` );
-			exist( this.track, `A track ${ message }` );
-
-			this.list = child( this.track, classes.list );
-			exist( this.list, `A list ${ message }` );
+			exist( this.track && this.list, 'Track or list was not found.' );
 
 			this.slides = values( this.list.children );
 
@@ -84,30 +102,91 @@ export default ( Splide ) => {
 			this.play  = find( autoplay, `.${ classes.play }` );
 			this.pause = find( autoplay, `.${ classes.pause }` );
 
+			this.track.id = this.track.id || `${ root.id }-track`;
+			this.list.id  = this.list.id || `${ root.id }-list`;
+
 			init();
+
+			Splide.on( 'refresh', () => {
+				this.destroy();
+				init();
+			} );
 		},
 
 		/**
 		 * Destroy.
 		 */
 		destroy() {
+			Slides.forEach( Slide => { Slide.destroy() } );
+			Slides = [];
 			removeClass( root, getClasses() );
 		},
 
+		/**
+		 * Register a slide to create a Slide object and handle its behavior.
+		 *
+		 * @param {Element} slide     - A slide element.
+		 * @param {number}  index     - A unique index. This can be negative.
+		 * @param {number}  realIndex - A real index for clones. Set -1 for real slides.
+		 */
+		register( slide, index, realIndex ) {
+			const SlideObject = Slide( Splide, index, realIndex, slide );
+			SlideObject.mount();
+			Slides.push( SlideObject );
+		},
+
+		/**
+		 * Return the Slide object designated by the index.
+		 * Note that "find" is not supported by IE.
+		 *
+		 * @return {Object|undefined} - A Slide object if available. Undefined if not.
+		 */
+		getSlide( index ) {
+			return Slides.filter( Slide => Slide.index === index )[0];
+		},
+
+		/**
+		 * Return all Slide objects.
+		 *
+		 * @param {boolean} includeClones - Whether to include cloned slides or not.
+		 *
+		 * @return {Object[]} - Slide objects.
+		 */
+		getSlides( includeClones ) {
+			return includeClones ? Slides : Slides.filter( Slide => ! Slide.isClone );
+		},
+
+		/**
+		 * Return Slide objects belonging to the given page.
+		 *
+		 * @param {number} page - A page number.
+		 *
+		 * @return {Object[]} - An array containing Slide objects.
+		 */
+		getSlidesByPage( page ) {
+			const idx     = Components.Controller.toIndex( page );
+			const options = Splide.options;
+			const max     = options.focus !== false ? 1 : options.perPage;
+
+			return Slides.filter( ( { index } ) => idx <= index && index < idx + max );
+		},
+
 		/**
 		 * Insert a slide to a slider.
 		 * Need to refresh Splide after adding a slide.
 		 *
-		 * @param {Node|string} slide - A slide element to be added.
-		 * @param {number}      index - A slide will be added at the position.
+		 * @param {Node|string} slide    - A slide element to be added.
+		 * @param {number}      index    - A slide will be added at the position.
+		 * @param {Function}    callback - Called right after the slide is added to the DOM tree.
 		 */
-		add( slide, index ) {
+		add( slide, index, callback ) {
 			if ( typeof slide === 'string' ) {
 				slide = domify( slide );
 			}
 
 			if ( slide instanceof Element ) {
 				const ref = this.slides[ index ];
+				applyStyle( slide, { display: 'none' } );
 
 				if ( ref ) {
 					before( slide, ref );
@@ -116,6 +195,11 @@ export default ( Splide ) => {
 					append( this.list, slide );
 					this.slides.push( slide );
 				}
+
+				loaded( slide, () => {
+					applyStyle( slide, { display: '' } );
+					callback && callback( slide );
+				} );
 			}
 		},
 
@@ -126,20 +210,46 @@ export default ( Splide ) => {
 		 * @param index - Slide index.
 		 */
 		remove( index ) {
-			const slides = this.slides.splice( index, 1 );
-			remove( slides[0] );
+			remove( this.slides.splice( index, 1 )[0] );
+		},
+
+		/**
+		 * Trigger the provided callback for each Slide object.
+		 *
+		 * @param {Function} callback - A callback function. The first argument will be the Slide object.
+		 */
+		each( callback ) {
+			Slides.forEach( callback );
+		},
+
+		/**
+		 * Return slides length without clones.
+		 *
+		 * @return {number} - Slide length.
+		 */
+		get length() {
+			return this.slides.length;
+		},
+
+		/**
+		 * Return "SlideObjects" length including clones.
+		 *
+		 * @return {number} - Slide length including clones.
+		 */
+		get total() {
+			return Slides.length;
 		},
 	};
 
 	/**
 	 * Initialization.
-	 * Assign ID to some elements if it's not available.
 	 */
 	function init() {
-		Elements.track.id = Elements.track.id || `${ root.id }-track`;
-		Elements.list.id  = Elements.list.id || `${ root.id }-list`;
-
 		addClass( root, getClasses() );
+
+		Elements.slides.forEach( ( slide, index ) => {
+			Elements.register( slide, index, -1 );
+		} );
 	}
 
 	/**

+ 196 - 0
src/js/components/elements/slide.js

@@ -0,0 +1,196 @@
+/**
+ * The sub component for handling each slide.
+ *
+ * @author    Naotoshi Fujita
+ * @copyright Naotoshi Fujita. All rights reserved.
+ */
+
+import { find, addClass, removeClass, hasClass, getAttribute, setAttribute } from '../../utils/dom';
+import { SLIDE } from '../../constants/types';
+import { STATUS_CLASSES } from '../../constants/classes';
+import { values } from "../../utils/object";
+import { pad } from "../../utils/utils";
+import { TTB } from "../../constants/directions";
+
+/**
+ * Events for restoring original styles.
+ *
+ * @type {string}
+ */
+const STYLE_RESTORE_EVENTS = 'update.slide';
+
+
+/**
+ * The sub component for handling each slide.
+ *
+ * @param {Splide}  Splide    - A Splide instance.
+ * @param {number}  index     - An unique slide index.
+ * @param {number}  realIndex - Clones should pass a real slide index.
+ * @param {Element} slide     - A slide element.
+ *
+ * @return {Object} - The sub component object.
+ */
+export default ( Splide, index, realIndex, slide ) => {
+	/**
+	 * Events when the slide status is updated.
+	 * Append a namespace to remove listeners later.
+	 *
+	 * @type {string}
+	 */
+	const STATUS_UPDATE_EVENTS = 'ready.slide updated.slide resize.slide '
+		+ ( Splide.options.updateOnMove ? 'move.slide' : 'moved.slide' );
+
+	/**
+	 * Slide sub component object.
+	 *
+	 * @type {Object}
+	 */
+	const Slide = {
+		/**
+		 * Slide element.
+		 *
+		 * @type {Element}
+		 */
+		slide,
+
+		/**
+		 * Slide index.
+		 *
+		 * @type {number}
+		 */
+		index,
+
+		/**
+		 * Real index for clones.
+		 *
+		 * @type {number}
+		 */
+		realIndex,
+
+		/**
+		 * Container element if available.
+		 *
+		 * @type {Element|null}
+		 */
+		container: find( slide, `.${ Splide.classes.container }` ),
+
+		/**
+		 * Whether this is a cloned slide or not.
+		 *
+		 * @type {boolean}
+		 */
+		isClone: realIndex > -1,
+
+		/**
+		 * Hold the original styles.
+		 *
+		 * @type {string}
+		 */
+		styles: getAttribute( slide, 'style' ) || '',
+
+		/**
+		 * Called when the component is mounted.
+		 */
+		mount() {
+			if ( ! this.isClone ) {
+				slide.id = `${ Splide.root.id }-slide${ pad( index + 1 ) }`;
+			}
+
+			Splide
+				.on( STATUS_UPDATE_EVENTS, () => this.update() )
+				.on( STYLE_RESTORE_EVENTS, restoreStyles );
+		},
+
+		/**
+		 * Destroy.
+		 */
+		destroy() {
+			Splide.off( STATUS_UPDATE_EVENTS ).off( STYLE_RESTORE_EVENTS );
+			removeClass( slide, values( STATUS_CLASSES ) );
+			restoreStyles();
+		},
+
+		/**
+		 * Update active and visible status.
+		 */
+		update() {
+			update( this.isActive(), false );
+			update( this.isVisible(), true );
+		},
+
+		/**
+		 * Check whether this slide is active or not.
+		 *
+		 * @return {boolean} - True if the slide is active or false if not.
+		 */
+		isActive() {
+			return Splide.index === index;
+		},
+
+		/**
+		 * Check whether this slide is visible in the viewport or not.
+		 *
+		 * @return {boolean} - True if the slide is visible or false if not.
+		 */
+		isVisible() {
+			const { floor }  = Math;
+			const Components = Splide.Components;
+			const Track      = Components.Track;
+			const prop       = Splide.options.direction === TTB ? 'clientHeight' : 'clientWidth';
+			const position   = floor( ( Track.toPosition( index ) + Track.offset( index ) - Track.position ) * Track.sign );
+			const edge       = floor( position + slide[ prop ] );
+			const size       = Components.Elements.track[ prop ];
+
+			return ( 0 <= position && position <= size && 0 <= edge && edge <= size ) || this.isActive();
+		},
+
+		/**
+		 * Calculate how far this slide is from another slide and
+		 * return true if the distance is within the given number.
+		 *
+		 * @param {number} from   - Index of a target slide.
+		 * @param {number} within - True if the slide is within this number.
+		 *
+		 * @return {boolean} - True if the slide is within the number or false otherwise.
+		 */
+		isWithin( from, within ) {
+			let diff = Math.abs( from - index );
+
+			if ( ! Splide.is( SLIDE ) && ! this.isClone ) {
+				diff = Math.min( diff, Splide.length - diff );
+			}
+
+			return diff < within;
+		},
+	};
+
+	/**
+	 * Update classes for activity or visibility.
+	 *
+	 * @param {boolean} active        - Is active/visible or not.
+	 * @param {boolean} forVisibility - Toggle classes for activity or visibility.
+	 */
+	function update( active, forVisibility ) {
+		const type      = forVisibility ? 'visible' : 'active';
+		const className = STATUS_CLASSES[ type ];
+
+		if ( active ) {
+			addClass( slide, className );
+			Splide.emit( `${ type }`, Slide );
+		} else {
+			if ( hasClass( slide, className ) ) {
+				removeClass( slide, className );
+				Splide.emit( `${ forVisibility ? 'hidden' : 'inactive' }`, Slide );
+			}
+		}
+	}
+
+	/**
+	 * Restore the original styles.
+	 */
+	function restoreStyles() {
+		setAttribute( slide, 'style', Slide.styles );
+	}
+
+	return Slide;
+}

+ 0 - 4
src/js/components/index.js

@@ -8,7 +8,6 @@
 import Options     from './options';
 import Elements    from './elements';
 import Controller  from './controller';
-import Slides      from './slides';
 import Track       from './track';
 import Clones      from './clones';
 import Layout      from './layout';
@@ -28,7 +27,6 @@ export const COMPLETE = {
 	Options,
 	Elements,
 	Controller,
-	Slides,
 	Track,
 	Clones,
 	Layout,
@@ -49,12 +47,10 @@ export const LIGHT = {
 	Options,
 	Elements,
 	Controller,
-	Slides,
 	Track,
 	Clones,
 	Layout,
 	Drag,
-	Autoplay,
 	Arrows,
 	Pagination,
 	A11y,

+ 20 - 6
src/js/components/keyboard/index.js

@@ -11,14 +11,21 @@
  * @type {Object}
  */
 const KEY_MAP = {
-	horizontal: {
+	ltr: {
 		ArrowLeft : '<',
 		ArrowRight: '>',
 		// For IE.
 		Left : '<',
 		Right: '>',
 	},
-	vertical: {
+	rtl: {
+		ArrowLeft : '>',
+		ArrowRight: '<',
+		// For IE.
+		Left : '>',
+		Right: '<',
+	},
+	ttb: {
 		ArrowUp  : '<',
 		ArrowDown: '>',
 		// For IE.
@@ -36,24 +43,31 @@ const KEY_MAP = {
  * @return {Object} - The component object.
  */
 export default ( Splide ) => {
+	/**
+	 * Hold the root element.
+	 *
+	 * @type {Element}
+	 */
+	const root = Splide.root;
+
 	return {
 		/**
 		 * Called when the component is mounted.
 		 */
 		mount() {
-			const map = KEY_MAP[ Splide.options.direction === 'ttb' ? 'vertical' : 'horizontal' ];
+			const map = KEY_MAP[ Splide.options.direction ];
 
 			Splide.on( 'mounted updated', () => {
-				Splide.off( 'keydown', Splide.root );
+				Splide.off( 'keydown', root );
 
 				if ( Splide.options.keyboard ) {
 					Splide.on( 'keydown', e => {
 						if ( map[ e.key ] ) {
 							Splide.go( map[ e.key ] );
 						}
-					}, Splide.root );
+					}, root );
 				}
 			} );
 		},
-	}
+	};
 }

+ 57 - 43
src/js/components/layout/resolvers/horizontal.js → src/js/components/layout/directions/horizontal.js

@@ -9,17 +9,23 @@ import { applyStyle } from "../../../utils/dom";
 import { unit, toPixel } from "../../../utils/utils";
 import { RTL } from '../../../constants/directions';
 
+/**
+ * Max width of a slide.
+ *
+ * @type {number}
+ */
+const SLIDE_MAX_WIDTH = 5000;
+
 
 /**
  * The resolver component for horizontal layout.
  *
  * @param {Splide} Splide     - A Splide instance.
  * @param {Object} Components - An object containing components.
- * @param {Object} options    - Current options.
  *
  * @return {Object} - The resolver object.
  */
-export default ( Splide, Components, options ) => {
+export default ( Splide, Components ) => {
 	/**
 	 * Keep the Elements component.
 	 *
@@ -39,7 +45,14 @@ export default ( Splide, Components, options ) => {
 	 *
 	 * @type {Element}
 	 */
-	const track = Elements.track;
+	let track;
+
+	/**
+	 * Keep the latest options.
+	 *
+	 * @type {Element}
+	 */
+	let options = Splide.options;
 
 	return {
 		/**
@@ -47,7 +60,7 @@ export default ( Splide, Components, options ) => {
 		 *
 		 * @type {string}
 		 */
-		marginProp: options.direction === RTL ? 'marginLeft' : 'marginRight',
+		margin: 'margin' + ( options.direction === RTL ? 'Left' : 'Right' ),
 
 		/**
 		 * Always 0 because the height will be determined by inner contents.
@@ -64,61 +77,56 @@ export default ( Splide, Components, options ) => {
 		listHeight: 0,
 
 		/**
-		 * Gap in px.
-		 *
-		 * @type {number}
+		 * Initialization.
 		 */
-		gap: toPixel( root, options.gap ),
+		init() {
+			options = Splide.options;
+			track   = Elements.track;
+
+			this.gap = toPixel( root, options.gap );
 
-		/**
-		 * An object containing padding left and right in px.
-		 *
-		 * @type {Object}
-		 */
-		padding: ( () => {
 			const padding = options.padding;
 			const { left = padding, right = padding } = padding;
 
-			return {
+			this.padding = {
 				left : toPixel( root, left ),
 				right: toPixel( root, right ),
 			};
-		} )(),
 
-		/**
-		 * Initialization.
-		 */
-		init() {
 			applyStyle( track, {
-				paddingLeft : unit( this.padding.left ),
-				paddingRight: unit( this.padding.right ),
+				paddingLeft : unit( left ),
+				paddingRight: unit( right ),
 			} );
 		},
 
 		/**
-		 * Return slider width without padding.
+		 * Accumulate slide width including the gap to the designated index.
 		 *
-		 * @return {number} - Current slider width.
-		 */
-		get width() {
-			return track.clientWidth - this.padding.left - this.padding.right;
-		},
-
-		/**
-		 * Return list width.
+		 * @param {number|undefined} index - If undefined, width of all slides will be accumulated.
 		 *
-		 * @return {number} - Current list width.
+		 * @return {number} - Accumulated width.
 		 */
-		get listWidth() {
-			return ( this.slideWidth + this.gap ) * Components.Slides.total;
+		totalWidth( index ) {
+			return Elements.getSlides( true )
+				.filter( Slide => Slide.index <= index )
+				.reduce( ( accumulator, Slide ) => {
+					return accumulator + this.slideWidth( Slide.index ) + this.gap;
+				}, 0 );
 		},
 
 		/**
 		 * Return the slide width in px.
 		 *
+		 * @param {number} index - Slide index.
+		 *
 		 * @return {number} - The slide width.
 		 */
-		get slideWidth() {
+		slideWidth( index ) {
+			if ( options.autoWidth ) {
+				const Slide = Elements.getSlide( index );
+				return Slide ? Slide.slide.clientWidth : 0;
+			}
+
 			const width = options.fixedWidth || ( ( this.width + this.gap ) / options.perPage ) - this.gap;
 			return toPixel( root, width );
 		},
@@ -128,22 +136,28 @@ export default ( Splide, Components, options ) => {
 		 *
 		 * @return {number} - The slide height.
 		 */
-		get slideHeight() {
+		slideHeight() {
 			const height = options.height || options.fixedHeight || this.width * options.heightRatio;
 			return toPixel( root, height );
 		},
 
 		/**
-		 * Return the number of slides in the current view.
+		 * Return slider width without padding.
 		 *
-		 * @return {number} - The number of slides in view.
+		 * @return {number} - Current slider width.
 		 */
-		get numInView() {
-			if ( options.fixedWidth ) {
-				return Math.floor( ( this.width + this.gap ) / ( this.slideWidth + this.gap ) ) || 1;
-			}
+		get width() {
+			return track.clientWidth - this.padding.left - this.padding.right;
+		},
 
-			return options.perPage;
+		/**
+		 * Return list width.
+		 *
+		 * @return {number} - Current list width.
+		 */
+		get listWidth() {
+			const total = Elements.total;
+			return options.autoWidth ? total * SLIDE_MAX_WIDTH : this.totalWidth( total );
 		},
 	}
 }

+ 40 - 56
src/js/components/layout/resolvers/vertical.js → src/js/components/layout/directions/vertical.js

@@ -15,11 +15,10 @@ import { exist } from "../../../utils/error";
  *
  * @param {Splide} Splide     - A Splide instance.
  * @param {Object} Components - An object containing components.
- * @param {Object} options    - Current options.
  *
  * @return {Object} - The resolver object.
  */
-export default ( Splide, Components, options ) => {
+export default ( Splide, Components ) => {
 	/**
 	 * Keep the Elements component.
 	 *
@@ -39,7 +38,14 @@ export default ( Splide, Components, options ) => {
 	 *
 	 * @type {Element}
 	 */
-	const track = Elements.track;
+	let track;
+
+	/**
+	 * Keep the latest options.
+	 *
+	 * @type {Element}
+	 */
+	let options;
 
 	return {
 		/**
@@ -47,40 +53,50 @@ export default ( Splide, Components, options ) => {
 		 *
 		 * @type {string}
 		 */
-		marginProp: 'marginBottom',
+		margin: 'marginBottom',
 
 		/**
-		 * Gap in px.
-		 *
-		 * @type {number}
+		 * Init slider styles according to options.
 		 */
-		gap: toPixel( root, options.gap ),
+		init() {
+			options = Splide.options;
+			track   = Elements.track;
+
+			this.gap = toPixel( root, options.gap );
 
-		/**
-		 * An object containing padding left and right in px.
-		 *
-		 * @type {Object}
-		 */
-		padding: ( () => {
 			const padding = options.padding;
 			const { top = padding, bottom = padding } = padding;
 
-			return {
+			this.padding = {
 				top   : toPixel( root, top ),
 				bottom: toPixel( root, bottom ),
 			};
-		} )(),
 
-		/**
-		 * Init slider styles according to options.
-		 */
-		init() {
 			applyStyle( track, {
-				paddingTop   : unit( this.padding.top ),
-				paddingBottom: unit( this.padding.bottom ),
+				paddingTop   : unit( top ),
+				paddingBottom: unit( bottom ),
 			} );
 		},
 
+		/**
+		 * Return the slide width in px.
+		 *
+		 * @return {number} - The slide width.
+		 */
+		slideWidth() {
+			return toPixel( root, options.fixedWidth || this.width );
+		},
+
+		/**
+		 * Return the slide height in px.
+		 *
+		 * @return {number} - The slide height.
+		 */
+		slideHeight() {
+			const height = options.fixedHeight || ( this.height + this.gap ) / options.perPage - this.gap;
+			return toPixel( root, height );
+		},
+
 		/**
 		 * Return slider width without padding.
 		 *
@@ -98,7 +114,7 @@ export default ( Splide, Components, options ) => {
 		get height() {
 			const height = options.height || this.width * options.heightRatio;
 			exist( height, '"height" or "heightRatio" is missing.' );
-			return toPixel( Splide.root, height ) - this.padding.top - this.padding.bottom;
+			return toPixel( root, height ) - this.padding.top - this.padding.bottom;
 		},
 
 		/**
@@ -116,39 +132,7 @@ export default ( Splide, Components, options ) => {
 		 * @return {number} - Current list height.
 		 */
 		get listHeight() {
-			return ( this.slideHeight + this.gap ) * Components.Slides.total;
-		},
-
-		/**
-		 * Return the slide width in px.
-		 *
-		 * @return {number} - The slide width.
-		 */
-		get slideWidth() {
-			return toPixel( Splide.root, options.fixedWidth || this.width );
-		},
-
-		/**
-		 * Return the slide height in px.
-		 *
-		 * @return {number} - The slide height.
-		 */
-		get slideHeight() {
-			const height = options.fixedHeight || ( this.height + this.gap ) / options.perPage - this.gap;
-			return toPixel( Splide.root, height );
-		},
-
-		/**
-		 * Return the number of slides in the current view.
-		 *
-		 * @return {number} - The number of slides in view.
-		 */
-		get numInView() {
-			if ( options.fixedHeight ) {
-				return Math.floor( ( this.height + this.gap ) / ( this.slideHeight + this.gap ) ) || 1;
-			}
-
-			return options.perPage;
+			return ( this.slideHeight() + this.gap ) * Elements.total;
 		},
 	}
 }

+ 22 - 159
src/js/components/layout/index.js

@@ -5,19 +5,21 @@
  * @copyright Naotoshi Fujita. All rights reserved.
  */
 
-import Horizontal from './resolvers/horizontal';
-import Vertical from './resolvers/vertical';
+import Horizontal from './directions/horizontal';
+import Vertical from './directions/vertical';
 
 import { unit } from '../../utils/utils';
 import { throttle } from '../../utils/time';
 import { applyStyle, removeAttribute } from '../../utils/dom';
+import { assign } from "../../utils/object";
+import { TTB } from "../../constants/directions";
 
 /**
  * Interval time for throttle.
  *
  * @type {number}
  */
-const THROTTLE = 50;
+const THROTTLE = 100;
 
 
 /**
@@ -29,47 +31,6 @@ const THROTTLE = 50;
  * @return {Object} - The component object.
  */
 export default ( Splide, Components ) => {
-	/**
-	 * Store the root element.
-	 *
-	 * @type {Element}
-	 */
-	const root = Splide.root;
-
-	/**
-	 * Store the list element.
-	 *
-	 * @type {Element}
-	 */
-	let list;
-
-	/**
-	 * Store the track element.
-	 *
-	 * @type {Element}
-	 */
-	let track;
-
-	/**
-	 * Store all Slide objects.
-	 *
-	 * @type {Object}
-	 */
-	let Slides;
-
-	/**
-	 * Hold a resolver object.
-	 *
-	 * @type {Object}
-	 */
-	let Resolver;
-
-	/**
-	 * Whether the slider is vertical or not.
-	 * @type {boolean}
-	 */
-	const isVertical = Splide.options.direction === 'ttb';
-
 	/**
 	 * Keep the Elements component.
 	 *
@@ -82,14 +43,11 @@ export default ( Splide, Components ) => {
 	 *
 	 * @type {Object}
 	 */
-	const Layout = {
+	const Layout = assign( {
 		/**
 		 * Called when the component is mounted.
 		 */
 		mount() {
-			list  = Elements.list;
-			track = Elements.track;
-
 			bind();
 			init();
 		},
@@ -98,116 +56,18 @@ export default ( Splide, Components ) => {
 		 * Destroy.
 		 */
 		destroy() {
-			Elements.slides
-				.concat( [ list, track ] )
-				.forEach( elm => { removeAttribute( elm, 'style' ) } );
-		},
-
-		/**
-		 * Return slider width without padding.
-		 *
-		 * @return {number} - Current slide width.
-		 */
-		get width() {
-			return Resolver.width;
-		},
-
-		/**
-		 * Return slider height without padding.
-		 *
-		 * @return {number}
-		 */
-		get height() {
-			return Resolver.height;
+			removeAttribute( [ Elements.list, Elements.track ], 'style' );
 		},
-
-		/**
-		 * Return list width.
-		 *
-		 * @return {number} - Current list width.
-		 */
-		get listWidth() {
-			return Resolver.listWidth;
-		},
-
-		/**
-		 * Return list height.
-		 *
-		 * @return {number} - Current list height.
-		 */
-		get listHeight() {
-			return Resolver.listHeight;
-		},
-
-		/**
-		 * Return slide width including gap size.
-		 * Note that slideWidth * perPage is NOT equal to slider width.
-		 *
-		 * @return {number} - Current slide width including gap size.
-		 */
-		get slideWidth() {
-			return Resolver.slideWidth;
-		},
-
-		/**
-		 * Return slide height.
-		 *
-		 * @return {number} - Computed slide height.
-		 */
-		get slideHeight() {
-			return Resolver.slideHeight;
-		},
-
-		/**
-		 * Return gap in px.
-		 *
-		 * @return {Object} - Gap amount in px.
-		 */
-		get gap() {
-			return Resolver.gap;
-		},
-
-		/**
-		 * Return padding object.
-		 *
-		 * @return {Object} - An object containing padding left and right in horizontal mode
-		 *                    or top and bottom in vertical one.
-		 */
-		get padding() {
-			return Resolver.padding;
-		},
-
-		/**
-		 * Return the number of slides in the current view.
-		 *
-		 * @return {number} - The number of slides in view.
-		 */
-		get numInView() {
-			return Resolver.numInView;
-		},
-	};
+	}, Splide.options.direction === TTB ?	Vertical( Splide, Components ) : Horizontal( Splide, Components ) );
 
 	/**
 	 * Init slider styles according to options.
 	 */
 	function init() {
-		const options = Splide.options;
-
-		Slides = Components.Slides.getSlides( true, true );
-
-		if ( isVertical ) {
-			Resolver = Vertical( Splide, Components, options );
-		} else {
-			Resolver = Horizontal( Splide, Components, options );
-		}
+		Layout.init();
 
-		Resolver.init();
-
-		applyStyle( root, { maxWidth: unit( options.width ) } );
-
-		Slides.forEach( Slide => {
-			applyStyle( Slide.slide, { [ Resolver.marginProp ]: unit( Resolver.gap ) } )
-		} );
+		applyStyle( Splide.root, { maxWidth: unit( Splide.options.width ) } );
+		Elements.each( Slide => { Slide.slide.style[ Layout.margin ] = unit( Layout.gap ) } );
 
 		resize();
 	}
@@ -218,8 +78,8 @@ export default ( Splide, Components ) => {
 	 */
 	function bind() {
 		Splide
-			.on( 'resize', throttle( () => { Splide.emit( 'resize' ) }, THROTTLE ), window )
-			.on( 'mounted resize', resize )
+			.on( 'resize load', throttle( () => { Splide.emit( 'resize' ) }, THROTTLE ), window )
+			.on( 'resize', resize )
 			.on( 'updated', init );
 	}
 
@@ -227,15 +87,18 @@ export default ( Splide, Components ) => {
 	 * Resize the list and slides including clones.
 	 */
 	function resize() {
-		applyStyle( list, { width: unit( Layout.listWidth ), height: unit( Layout.listHeight ) } );
-		applyStyle( track, { height: unit( Layout.height ) } );
+		applyStyle( Elements.list, { width: unit( Layout.listWidth ), height: unit( Layout.listHeight ) } );
+		applyStyle( Elements.track, { height: unit( Layout.height ) } );
 
-		const slideWidth  = unit( Resolver.slideWidth );
-		const slideHeight = unit( Resolver.slideHeight );
+		const slideHeight = unit( Layout.slideHeight() );
 
-		Slides.forEach( Slide => {
+		Elements.each( Slide => {
 			applyStyle( Slide.container, { height: slideHeight } );
-			applyStyle( Slide.slide, { width: slideWidth,	height: ! Slide.container ? slideHeight : '' } );
+
+			applyStyle( Slide.slide, {
+				width : Splide.options.autoWidth ? null : unit( Layout.slideWidth( Slide.index ) ),
+				height: Slide.container ? null : slideHeight,
+			} );
 		} );
 	}
 

+ 19 - 9
src/js/components/lazyload/index.js

@@ -6,7 +6,17 @@
  */
 
 import { STATUS_CLASSES } from '../../constants/classes';
-import { create, remove, append, find, addClass, removeClass, setAttribute, getAttribute, applyStyle } from '../../utils/dom';
+import {
+	create,
+	remove,
+	append,
+	find,
+	addClass,
+	removeClass,
+	setAttribute,
+	getAttribute,
+	applyStyle,
+} from '../../utils/dom';
 
 /**
  * The name for a data attribute.
@@ -57,9 +67,9 @@ export default ( Splide, Components, name ) => {
 	/**
 	 * Whether to stop sequential load.
 	 *
-	 * @type {boolean}
+	 * @type {boolean|undefined}
 	 */
-	let stop = false;
+	let interrupted;
 
 	/**
 	 * Lazyload component object.
@@ -78,12 +88,12 @@ export default ( Splide, Components, name ) => {
 		 * Called when the component is mounted.
 		 */
 		mount() {
-			Components.Slides.getSlides( true, true ).forEach( Slide => {
+			Components.Elements.each( Slide => {
 				const img = find( Slide.slide, `[${ SRC_DATA_NAME }]` );
 
 				if ( img ) {
 					images.push( { img, Slide } );
-					applyStyle( img, { visibility: 'hidden' } );
+					applyStyle( img, { display: 'none' } );
 				}
 			} );
 
@@ -100,7 +110,7 @@ export default ( Splide, Components, name ) => {
 		 * Destroy.
 		 */
 		destroy() {
-			stop = true;
+			interrupted = true;
 		},
 	};
 
@@ -172,11 +182,11 @@ export default ( Splide, Components, name ) => {
 
 		if ( ! error ) {
 			remove( spinner );
-			applyStyle( img, { visibility: 'visible' } );
-			Splide.emit( `${ name }:loaded`, img );
+			applyStyle( img, { display: '' } );
+			Splide.emit( `${ name }:loaded`, img ).emit( 'resize' );
 		}
 
-		if ( isSequential && ! stop ) {
+		if ( isSequential && ! interrupted ) {
 			loadNext();
 		}
 	}

+ 2 - 6
src/js/components/options/index.js

@@ -10,6 +10,7 @@ import { error } from '../../utils/error';
 import { getAttribute } from "../../utils/dom";
 import { CREATED } from "../../constants/states";
 
+
 /**
  * The component for initializing options.
  *
@@ -18,18 +19,13 @@ import { CREATED } from "../../constants/states";
  * @return {Object} - The component object.
  */
 export default ( Splide ) => {
-	/**
-	 * Store the root element.
-	 */
-	const root = Splide.root;
-
 	/**
 	 * Retrieve options from the data attribute.
 	 * Note that IE10 doesn't support dataset property.
 	 *
 	 * @type {string}
 	 */
-	const options = getAttribute( root, 'data-splide' );
+	const options = getAttribute( Splide.root, 'data-splide' );
 
 	if ( options ) {
 		try {

+ 11 - 12
src/js/components/pagination/index.js

@@ -43,11 +43,11 @@ export default ( Splide, Components, name ) => {
 	let data = {};
 
 	/**
-	 * Hold a parent element of pagination.
+	 * Hold the Elements component.
 	 *
-	 * @type {Element}
+	 * @type {Object}
 	 */
-	let parent;
+	const Elements = Components.Elements;
 
 	/**
 	 * Pagination component object.
@@ -68,8 +68,8 @@ export default ( Splide, Components, name ) => {
 		mount() {
 			data = createPagination();
 
-			const slider = Components.Elements.slider;
-			parent = Splide.options.pagination === 'slider' && slider ? slider : Splide.root;
+			const slider = Elements.slider;
+			const parent = Splide.options.pagination === 'slider' && slider ? slider : Splide.root;
 			append( parent, data.list );
 
 			bind();
@@ -107,7 +107,7 @@ export default ( Splide, Components, name ) => {
 		 * @return {Object|undefined} - An item object on success or undefined on failure.
 		 */
 		getItem( index ) {
-			return data.items[ Components.Controller.indexToPage( index ) ];
+			return data.items[ Components.Controller.toPage( index ) ];
 		},
 
 		/**
@@ -164,12 +164,11 @@ export default ( Splide, Components, name ) => {
 	 * @return {Object} - An object contains all data.
 	 */
 	function createPagination() {
-		const options = Splide.options;
-		const classes = Splide.classes;
-		const list    = create( 'ul', { class: classes.pagination } );
-		const Slides  = Components.Slides;
+		const options  = Splide.options;
+		const classes  = Splide.classes;
+		const list     = create( 'ul', { class: classes.pagination } );
 
-		const items = Slides.getSlides( false, true )
+		const items = Elements.getSlides( false )
 			.filter( Slide => options.focus !== false || Slide.index % options.perPage === 0 )
 			.map( ( Slide, page ) => {
 				const li     = create( 'li', {} );
@@ -180,7 +179,7 @@ export default ( Splide, Components, name ) => {
 
 				Splide.on( 'click', () => { Splide.go( `>${ page }` ) }, button );
 
-				return { li, button, page, Slides: Slides.getSlidesByPage( page ) };
+				return { li, button, page, Slides: Elements.getSlidesByPage( page ) };
 			} );
 
 		return { list, items };

+ 1 - 1
src/js/components/slides/index.js

@@ -104,7 +104,7 @@ export default ( Splide, Components ) => {
 		 * @return {Object[]} - An array containing Slide objects.
 		 */
 		getSlidesByPage( page ) {
-			const idx     = Components.Controller.pageToIndex( page );
+			const idx     = Components.Controller.toIndex( page );
 			const options = Splide.options;
 			const max     = options.focus !== false ? 1 : options.perPage;
 

+ 181 - 187
src/js/components/slides/slide.js

@@ -1,187 +1,181 @@
-/**
- * The sub component for handling each slide.
- *
- * @author    Naotoshi Fujita
- * @copyright Naotoshi Fujita. All rights reserved.
- */
-
-import { find, addClass, removeClass, hasClass } from '../../utils/dom';
-import { SLIDE } from '../../constants/types';
-import { STATUS_CLASSES } from '../../constants/classes';
-import { CREATED } from "../../constants/states";
-import { each } from "../../utils/object";
-
-
-/**
- * The sub component for handling each slide.
- *
- * @param {number}  index     - An unique slide index.
- * @param {number}  realIndex - Clones should pass a real slide index.
- * @param {Element} slide     - A slide element.
- * @param {Splide}  Splide    - A Splide instance.
- *
- * @return {Object} - The sub component object.
- */
-export default ( index, realIndex, slide, Splide ) => {
-	/**
-	 * Events when the slide status is updated.
-	 * Append a namespace to remove listeners later.
-	 *
-	 * @type {string}
-	 */
-	const statusUpdateEvents = [
-		'mounted', 'updated', 'resize', Splide.options.updateOnMove ? 'move' : 'moved',
-	].reduce( ( acc, cur ) => acc + cur + '.slide ', '' ).trim();
-
-	/**
-	 * Slide sub component object.
-	 *
-	 * @type {Object}
-	 */
-	const Slide = {
-		/**
-		 * Slide element.
-		 *
-		 * @type {Element}
-		 */
-		slide,
-
-		/**
-		 * Slide index.
-		 *
-		 * @type {number}
-		 */
-		index,
-
-		/**
-		 * Real index for clones.
-		 *
-		 * @type {number}
-		 */
-		realIndex,
-
-		/**
-		 * Container element if available.
-		 *
-		 * @type {Element|null}
-		 */
-		container: find( slide, `.${ Splide.classes.container }` ),
-
-		/**
-		 * Whether this is clone or not.
-		 *
-		 * @type {boolean}
-		 */
-		isClone: realIndex > -1,
-
-		/**
-		 * Called when the component is mounted.
-		 */
-		mount() {
-			if ( ! this.isClone ) {
-				const number = index + 1;
-				slide.id = `${ Splide.root.id }-slide${ number < 10 ? '0' + number : number }`;
-			}
-
-			Splide.on( statusUpdateEvents, () => this.update() );
-
-			// Update status immediately on refresh.
-			if ( ! Splide.State.is( CREATED ) ) {
-				this.update();
-			}
-		},
-
-		/**
-		 * Destroy.
-		 */
-		destroy() {
-			Splide.off( statusUpdateEvents );
-			each( STATUS_CLASSES, className => { removeClass( slide, className ) } );
-		},
-
-		/**
-		 * Update active and visible status.
-		 */
-		update() {
-			update( this.isActive(), false );
-			update( this.isVisible(), true );
-		},
-
-		/**
-		 * Check whether this slide is active or not.
-		 *
-		 * @return {boolean} - True if the slide is active or false if not.
-		 */
-		isActive() {
-			return Splide.index === index;
-		},
-
-		/**
-		 * Check whether this slide is visible or not.
-		 *
-		 * @return {boolean} - True if the slide is visible or false if not.
-		 */
-		isVisible() {
-			const { focus, trimSpace }  = Splide.options;
-			const { index: activeIndex, length } = Splide;
-			const isCenter  = 'center' === focus;
-			const numInView = Splide.Components.Layout.numInView;
-			const offset    = isCenter ? numInView / 2 : parseInt( focus ) || 0;
-
-			if ( trimSpace ) {
-				if ( activeIndex < offset ) {
-					return index < numInView;
-				} else if ( activeIndex >= length - ( numInView - offset ) ) {
-					return index >= length - numInView;
-				}
-			}
-
-			const min = activeIndex - offset + ( isCenter && numInView % 2 === 0 ? 1 : 0 );
-
-			return min <= index && index < activeIndex + numInView - offset;
-		},
-
-		/**
-		 * Calculate how far this slide is from another slide and
-		 * return true if the distance is within the given number.
-		 *
-		 * @param {number} from   - Index of a target slide.
-		 * @param {number} within - True if the slide is within this number.
-		 *
-		 * @return {boolean} - True if the slide is within this number or false otherwise.
-		 */
-		isWithin( from, within ) {
-			let diff = Math.abs( from - index );
-
-			if ( ! Splide.is( SLIDE ) && ! this.isClone ) {
-				diff = Math.min( diff, Splide.length - diff );
-			}
-
-			return diff < within;
-		},
-	};
-
-
-	/**
-	 * Update classes for activity or visibility.
-	 *
-	 * @param {boolean} active        - Is active/visible or not.
-	 * @param {boolean} forVisibility - Toggle classes for activity or visibility.
-	 */
-	function update( active, forVisibility ) {
-		const type      = forVisibility ? 'visible' : 'active';
-		const className = STATUS_CLASSES[ type ];
-
-		if ( active ) {
-			addClass( slide, className );
-			Splide.emit( `${ type }`, Slide );
-		} else {
-			if ( hasClass( slide, className ) ) {
-				removeClass( slide, className );
-				Splide.emit( `${ forVisibility ? 'hidden' : 'inactive' }`, Slide );
-			}
-		}
-	}
-
-	return Slide;
-}
+// /**
+//  * The sub component for handling each slide.
+//  *
+//  * @author    Naotoshi Fujita
+//  * @copyright Naotoshi Fujita. All rights reserved.
+//  */
+//
+// import { find, addClass, removeClass, hasClass } from '../../utils/dom';
+// import { SLIDE } from '../../constants/types';
+// import { STATUS_CLASSES } from '../../constants/classes';
+// import { each } from "../../utils/object";
+//
+//
+// /**
+//  * The sub component for handling each slide.
+//  *
+//  * @param {number}  index     - An unique slide index.
+//  * @param {number}  realIndex - Clones should pass a real slide index.
+//  * @param {Element} slide     - A slide element.
+//  * @param {Splide}  Splide    - A Splide instance.
+//  *
+//  * @return {Object} - The sub component object.
+//  */
+// export default ( index, realIndex, slide, Splide ) => {
+// 	/**
+// 	 * Events when the slide status is updated.
+// 	 * Append a namespace to remove listeners later.
+// 	 *
+// 	 * @type {string}
+// 	 */
+// 	const statusUpdateEvents = [
+// 		'mounted', 'updated', 'resize', Splide.options.updateOnMove ? 'move' : 'moved',
+// 	].join( '.slide ' ).trim();
+//
+// 	/**
+// 	 * Slide sub component object.
+// 	 *
+// 	 * @type {Object}
+// 	 */
+// 	const Slide = {
+// 		/**
+// 		 * Slide element.
+// 		 *
+// 		 * @type {Element}
+// 		 */
+// 		slide,
+//
+// 		/**
+// 		 * Slide index.
+// 		 *
+// 		 * @type {number}
+// 		 */
+// 		index,
+//
+// 		/**
+// 		 * Real index for clones.
+// 		 *
+// 		 * @type {number}
+// 		 */
+// 		realIndex,
+//
+// 		/**
+// 		 * Container element if available.
+// 		 *
+// 		 * @type {Element|null}
+// 		 */
+// 		container: find( slide, `.${ Splide.classes.container }` ),
+//
+// 		/**
+// 		 * Whether this is clone or not.
+// 		 *
+// 		 * @type {boolean}
+// 		 */
+// 		isClone: realIndex > -1,
+//
+// 		/**
+// 		 * Called when the component is mounted.
+// 		 */
+// 		mount() {
+// 			if ( ! this.isClone ) {
+// 				const number = index + 1;
+// 				slide.id = `${ Splide.root.id }-slide${ number < 10 ? '0' + number : number }`;
+// 			}
+//
+// 			Splide.on( statusUpdateEvents, () => this.update() );
+// 		},
+//
+// 		/**
+// 		 * Destroy.
+// 		 */
+// 		destroy() {
+// 			Splide.off( statusUpdateEvents );
+// 			each( STATUS_CLASSES, className => { removeClass( slide, className ) } );
+// 		},
+//
+// 		/**
+// 		 * Update active and visible status.
+// 		 */
+// 		update() {
+// 			update( this.isActive(), false );
+// 			update( this.isVisible(), true );
+// 		},
+//
+// 		/**
+// 		 * Check whether this slide is active or not.
+// 		 *
+// 		 * @return {boolean} - True if the slide is active or false if not.
+// 		 */
+// 		isActive() {
+// 			return Splide.index === index;
+// 		},
+//
+// 		/**
+// 		 * Check whether this slide is visible or not.
+// 		 *
+// 		 * @return {boolean} - True if the slide is visible or false if not.
+// 		 */
+// 		isVisible() {
+// 			const { focus, trimSpace }  = Splide.options;
+// 			const { index: activeIndex, length } = Splide;
+// 			const isCenter  = 'center' === focus;
+// 			const numInView = Splide.Components.Layout.numInView;
+// 			const offset    = isCenter ? numInView / 2 : parseInt( focus ) || 0;
+//
+// 			if ( trimSpace && Splide.is( SLIDE ) ) {
+// 				if ( activeIndex < offset ) {
+// 					return index < numInView;
+// 				} else if ( activeIndex >= length - ( numInView - offset ) ) {
+// 					return index >= length - numInView;
+// 				}
+// 			}
+//
+// 			const min = activeIndex - offset + ( isCenter && numInView % 2 === 0 ? 1 : 0 );
+//
+// 			return min <= index && index < activeIndex + numInView - offset;
+// 		},
+//
+// 		/**
+// 		 * Calculate how far this slide is from another slide and
+// 		 * return true if the distance is within the given number.
+// 		 *
+// 		 * @param {number} from   - Index of a target slide.
+// 		 * @param {number} within - True if the slide is within this number.
+// 		 *
+// 		 * @return {boolean} - True if the slide is within this number or false otherwise.
+// 		 */
+// 		isWithin( from, within ) {
+// 			let diff = Math.abs( from - index );
+//
+// 			if ( ! Splide.is( SLIDE ) && ! this.isClone ) {
+// 				diff = Math.min( diff, Splide.length - diff );
+// 			}
+//
+// 			return diff < within;
+// 		},
+// 	};
+//
+//
+// 	/**
+// 	 * Update classes for activity or visibility.
+// 	 *
+// 	 * @param {boolean} active        - Is active/visible or not.
+// 	 * @param {boolean} forVisibility - Toggle classes for activity or visibility.
+// 	 */
+// 	function update( active, forVisibility ) {
+// 		const type      = forVisibility ? 'visible' : 'active';
+// 		const className = STATUS_CLASSES[ type ];
+//
+// 		if ( active ) {
+// 			addClass( slide, className );
+// 			Splide.emit( `${ type }`, Slide );
+// 		} else {
+// 			if ( hasClass( slide, className ) ) {
+// 				removeClass( slide, className );
+// 				Splide.emit( `${ forVisibility ? 'hidden' : 'inactive' }`, Slide );
+// 			}
+// 		}
+// 	}
+//
+// 	return Slide;
+// }

+ 1 - 3
src/js/components/sync/index.js

@@ -112,9 +112,7 @@ export default ( Splide ) => {
 	 * Listen some events on each slide.
 	 */
 	function bind() {
-		const Slides = sibling.Components.Slides.getSlides( true, true );
-
-		Slides.forEach( ( { slide, index } ) => {
+		sibling.Components.Elements.each( ( { slide, index } ) => {
 			/*
 			 * Listen mouseup and touchend events to handle click.
 			 */

+ 126 - 0
src/js/components/track/directions/horizontal.js

@@ -0,0 +1,126 @@
+/**
+ * The resolver component for horizontal move.
+ *
+ * @author    Naotoshi Fujita
+ * @copyright Naotoshi Fujita. All rights reserved.
+ */
+
+import { between } from '../../../utils/utils';
+import { RTL } from "../../../constants/directions";
+import { SLIDE } from "../../../constants/types";
+
+
+/**
+ * The resolver component for horizontal move.
+ *
+ * @param {Splide} Splide     - A Splide instance.
+ * @param {Object} Components - An object containing components.
+ *
+ * @return {Object} - The resolver object.
+ */
+export default ( Splide, Components ) => {
+	/**
+	 * Hold the Layout component.
+	 *
+	 * @type {Object}
+	 */
+	let Layout;
+
+	/**
+	 * Hold the Elements component.
+	 *
+	 * @type {Object}
+	 */
+	let Elements;
+
+	return {
+		/**
+		 * Axis of translate.
+		 *
+		 * @type {string}
+		 */
+		axis: 'X',
+
+		/**
+		 * Sign for the direction.
+		 *
+		 * @type {number}
+		 */
+		sign: Splide.options.direction === RTL ? 1 : -1,
+
+		/**
+		 * Initialization.
+		 */
+		init() {
+			Layout   = Components.Layout;
+			Elements = Components.Elements;
+		},
+
+		/**
+		 * Calculate position by index.
+		 *
+		 * @param {number} index - Slide index.
+		 *
+		 * @return {Object} - Calculated position.
+		 */
+		toPosition( index ) {
+			return this.sign * ( Layout.totalWidth( index - 1 ) + this.offset( index ) );
+		},
+
+		/**
+		 * Calculate the closest slide index from the given position.
+		 *
+		 * @return {number} - The closest slide position.
+		 */
+		toIndex( position ) {
+			position *= this.sign;
+
+			if ( Splide.is( SLIDE ) ) {
+				position = between( position, Layout.totalWidth( Elements.total ), 0 );
+			}
+
+			const Slides = Elements.getSlides( true );
+
+			for ( const i in Slides ) {
+				const Slide = Slides[ i ];
+
+				const slideIndex    = Slide.index;
+				const slidePosition = this.sign * this.toPosition( slideIndex );
+
+				if ( slidePosition < position && position <= slidePosition + Layout.slideWidth( slideIndex ) + Layout.gap ) {
+					return slideIndex;
+				}
+			}
+
+			return 0;
+		},
+
+		/**
+		 * Trim redundant spaces on the left or right edge if necessary.
+		 *
+		 * @param {number} position - Position value to be trimmed.
+		 *
+		 * @return {number} - Trimmed position.
+		 */
+		trim( position ) {
+			const edge = this.sign * ( Layout.totalWidth( Elements.total ) - ( Layout.width + Layout.gap ) );
+			return between( position, edge, 0 );
+		},
+
+		/**
+		 * Return current offset value, considering direction.
+		 *
+		 * @return {number} - Offset amount.
+		 */
+		offset( index ) {
+			const { focus }  = Splide.options;
+			const slideWidth = Layout.slideWidth( index );
+
+			if ( focus === 'center' ) {
+				return - ( Layout.width - slideWidth ) / 2;
+			}
+
+			return - ( parseInt( focus ) || 0 ) * ( slideWidth + Layout.gap );
+		},
+	};
+}

+ 27 - 19
src/js/components/track/resolvers/vertical.js → src/js/components/track/directions/vertical.js

@@ -5,7 +5,6 @@
  * @copyright Naotoshi Fujita. All rights reserved.
  */
 
-import { applyStyle } from '../../../utils/dom';
 import { between } from '../../../utils/utils';
 
 
@@ -23,17 +22,28 @@ export default ( Splide, Components ) => {
 	 *
 	 * @type {Object}
 	 */
-	const Layout = Components.Layout;
+	let Layout;
 
 	return {
 		/**
-		 * Set position with CSS transform.
+		 * Axis of translate.
 		 *
-		 * @param {Element} list    - A list element.
-		 * @param {number} position - A new position value.
+		 * @type {string}
 		 */
-		translate( list, position ) {
-			applyStyle( list, { transform: `translateY(${ position }px)` } );
+		axis: 'Y',
+
+		/**
+		 * Sign for the direction.
+		 *
+		 * @return {number}
+		 */
+		sign: -1,
+
+		/**
+		 * Initialization.
+		 */
+		init() {
+			Layout = Components.Layout;
 		},
 
 		/**
@@ -44,7 +54,7 @@ export default ( Splide, Components ) => {
 		 * @return {Object} - Calculated position.
 		 */
 		toPosition( index ) {
-			return - ( index * ( Layout.slideHeight + Layout.gap ) + this.offset )
+			return - ( ( index + Components.Clones.length / 2 ) * ( Layout.slideHeight() + Layout.gap ) + this.offset() );
 		},
 
 		/**
@@ -53,7 +63,9 @@ export default ( Splide, Components ) => {
 		 * @return {number} - The closest slide index.
 		 */
 		toIndex( position ) {
-			return Math.round( - ( position + this.offset ) / ( Layout.slideHeight + Layout.gap ) );
+			const slideHeight = Layout.slideHeight();
+			const cloneOffset = ( slideHeight + Layout.gap ) * Components.Clones.length / 2;
+			return Math.round( - ( position + cloneOffset + this.offset() ) / ( slideHeight + Layout.gap ) );
 		},
 
 		/**
@@ -69,23 +81,19 @@ export default ( Splide, Components ) => {
 		},
 
 		/**
-		 * Return current offset value, considering direction and a number of clones.
+		 * Return current offset value, considering direction.
 		 *
 		 * @return {number} - Offset amount.
 		 */
-		get offset() {
-			const { height, slideHeight, gap } = Layout;
-			const { focus } = Splide.options;
-
-			let focusOffset;
+		offset() {
+			const { focus }   = Splide.options;
+			const slideHeight = Layout.slideHeight();
 
 			if ( focus === 'center' ) {
-				focusOffset = ( height - slideHeight ) / 2;
-			} else {
-				focusOffset = ( parseInt( focus ) || 0 ) * ( slideHeight + gap );
+				return -( Layout.height - slideHeight ) / 2;
 			}
 
-			return ( slideHeight + gap ) * Components.Clones.length / 2 - focusOffset;
+			return -( parseInt( focus ) || 0 ) * ( slideHeight + Layout.gap );
 		},
 	};
 }  

+ 31 - 47
src/js/components/track/index.js

@@ -5,11 +5,12 @@
  * @copyright Naotoshi Fujita. All rights reserved.
  */
 
-import Vertical from './resolvers/vertical';
-import Horizontal from './resolvers/horizontal';
+import Vertical from './directions/vertical';
+import Horizontal from './directions/horizontal';
 import { applyStyle } from '../../utils/dom';
 import { LOOP, FADE } from '../../constants/types';
 import { TTB } from '../../constants/directions';
+import { assign } from "../../utils/object";
 
 
 /**
@@ -28,13 +29,6 @@ export default ( Splide, Components ) => {
 	 */
 	let list;
 
-	/**
-	 * Store the Resolver for direction.
-	 *
-	 * @type {Object}
-	 */
-	let Resolver;
-
 	/**
 	 * Store the current position.
 	 *
@@ -56,13 +50,18 @@ export default ( Splide, Components ) => {
 	 */
 	const isFade = Splide.is( FADE );
 
-	return {
+	/**
+	 * Track component object.
+	 *
+	 * @type {Object}
+	 */
+	const Track = assign( {
 		/**
 		 * Called when the component is mounted.
 		 */
 		mount() {
-			list     = Components.Elements.list;
-			Resolver = isVertical ? Vertical( Splide, Components ) : Horizontal( Splide, Components );
+			list = Components.Elements.list;
+			this.init();
 		},
 
 		/**
@@ -85,7 +84,7 @@ export default ( Splide, Components ) => {
 		 * @param {boolean} silently  - If true, suppress emitting events.
 		 */
 		go( destIndex, newIndex, silently ) {
-			const newPosition = this.trim( this.toPosition( destIndex ) );
+			const newPosition = getTrimmedPosition( destIndex );
 			const prevIndex   = Splide.index;
 
 			if ( ! silently ) {
@@ -97,7 +96,11 @@ export default ( Splide, Components ) => {
 					this.end( destIndex, newIndex, prevIndex, silently );
 				} );
 			} else {
-				this.end( destIndex, newIndex, prevIndex, silently );
+				if ( destIndex !== prevIndex && Splide.options.trimSpace === 'rewind' ) {
+					Components.Controller.go( destIndex + destIndex - prevIndex, silently );
+				} else {
+					this.end( destIndex, newIndex, prevIndex, silently );
+				}
 			}
 		},
 
@@ -127,8 +130,7 @@ export default ( Splide, Components ) => {
 		 * @param {number} index - A destination index where the track jumps.
 		 */
 		jump( index ) {
-			const position = this.trim( this.toPosition( index ) );
-			this.translate( position );
+			this.translate( getTrimmedPosition( index ) );
 		},
 
 		/**
@@ -138,27 +140,7 @@ export default ( Splide, Components ) => {
 		 */
 		translate( position ) {
 			currPosition = position;
-			Resolver.translate( list, position );
-		},
-
-		/**
-		 * Calculate position by index.
-		 *
-		 * @param {number} index - Slide index.
-		 *
-		 * @return {Object} - Calculated position.
-		 */
-		toPosition( index ) {
-			return Resolver.toPosition( index );
-		},
-
-		/**
-		 * Calculate the closest slide index by the given position.
-		 *
-		 * @return {number} - The closest slide index.
-		 */
-		toIndex( position ) {
-			return Resolver.toIndex( position );
+			applyStyle( list, { transform: `translate${ this.axis }(${ position }px)` } );
 		},
 
 		/**
@@ -173,7 +155,7 @@ export default ( Splide, Components ) => {
 				return position;
 			}
 
-			return Resolver.trim( position );
+			return this._s.trim( position );
 		},
 
 		/**
@@ -198,14 +180,16 @@ export default ( Splide, Components ) => {
 		get position() {
 			return currPosition;
 		},
+	}, isVertical ? Vertical( Splide, Components ) : Horizontal( Splide, Components ) );
 
-		/**
-		 * Return current offset value including focus offset.
-		 *
-		 * @return {number} - Offset amount.
-		 */
-		get offset() {
-			return Resolver.offset;
-		},
-	};
+	/**
+	 * Convert index to the trimmed position.
+	 *
+	 * @return {number} - Trimmed position.
+	 */
+	function getTrimmedPosition( index ) {
+		return Track.trim( Track.toPosition( index ) );
+	}
+
+	return Track;
 }

+ 0 - 100
src/js/components/track/resolvers/horizontal.js

@@ -1,100 +0,0 @@
-/**
- * The resolver component for horizontal move.
- *
- * @author    Naotoshi Fujita
- * @copyright Naotoshi Fujita. All rights reserved.
- */
-
-import { applyStyle } from '../../../utils/dom';
-import { between } from '../../../utils/utils';
-
-
-/**
- * The resolver component for horizontal move.
- *
- * @param {Splide} Splide     - A Splide instance.
- * @param {Object} Components - An object containing components.
- *
- * @return {Object} - The resolver object.
- */
-export default ( Splide, Components ) => {
-	/**
-	 * Hold the Layout object.
-	 *
-	 * @type {Object}
-	 */
-	const Layout = Components.Layout;
-
-	return {
-		/**
-		 * Set position with CSS transform.
-		 *
-		 * @param {Element} list     - A list element.
-		 * @param {number}  position - A new position value.
-		 */
-		translate( list, position ) {
-			applyStyle( list, { transform: `translateX(${ position }px)` } );
-		},
-
-		/**
-		 * Calculate position by index.
-		 *
-		 * @param {number} index - Slide index.
-		 *
-		 * @return {Object} - Calculated position.
-		 */
-		toPosition( index ) {
-			return this.sign * ( index * ( Layout.slideWidth + Layout.gap ) + this.offset )
-		},
-
-		/**
-		 * Calculate the closest slide index from the given position.
-		 *
-		 * @return {number} - The closest slide position.
-		 */
-		toIndex( position ) {
-			return Math.round( ( this.sign * position - this.offset ) / ( Layout.slideWidth + Layout.gap ) );
-		},
-
-		/**
-		 * Trim redundant spaces on the left or right edge if necessary.
-		 *
-		 * @param {number} position - Position value to be trimmed.
-		 *
-		 * @return {number} - Trimmed position.
-		 */
-		trim( position ) {
-			const edge = this.sign * ( Layout.listWidth - ( Layout.width + Layout.gap ) );
-			return between( position, edge, 0 );
-		},
-
-		/**
-		 * Return sign according to the direction.
-		 *
-		 * @return {number} - -1 for LTR or 1 or RTL.
-		 */
-		get sign() {
-			return Components.Controller.isRtl() ? 1 : -1;
-		},
-
-		/**
-		 * Return current offset value, considering direction and a number of clones.
-		 *
-		 * @return {number} - Offset amount.
-		 */
-		get offset() {
-			const { width, slideWidth, gap } = Layout;
-			const { focus } = Splide.options;
-
-			let focusOffset;
-
-			if ( focus === 'center' ) {
-				focusOffset = ( width - slideWidth ) / 2;
-			} else {
-				focusOffset = ( parseInt( focus ) || 0 ) * ( slideWidth + gap );
-			}
-
-			return ( slideWidth + gap ) * Components.Clones.length / 2 - focusOffset;
-		},
-	};
-}

+ 9 - 0
src/js/constants/defaults.js

@@ -73,6 +73,15 @@ export const DEFAULTS = {
 	 */
 	heightRatio: 0,
 
+	/**
+	 * If true, slide width will be determined by the element width itself.
+	 * - perPage/perMove should be 1.
+	 * - lazyLoad should be false.
+	 *
+	 * @type {boolean}
+	 */
+	autoWidth: false,
+
 	/**
 	 * Determine how many slides should be displayed per page.
 	 *

+ 8 - 1
src/js/constants/states.js

@@ -31,4 +31,11 @@ export const IDLE = 3;
  *
  * @type {number}
  */
-export const MOVING = 4;
+export const MOVING = 4;
+
+/**
+ * Splide is moving.
+ *
+ * @type {number}
+ */
+export const DESTROYED = 5;

+ 16 - 11
src/js/core/event.js

@@ -17,7 +17,7 @@ export default () => {
 	 */
 	let data = [];
 
-	return {
+	const Event = {
 		/**
 		 * Subscribe the given event(s).
 		 *
@@ -49,10 +49,7 @@ export default () => {
 					const item = data[ i ];
 
 					if ( item && item.event === event && item.elm === elm ) {
-						if ( elm ) {
-							elm.removeEventListener( event, item.handler, item.options );
-						}
-
+						unsubscribe( item );
 						delete data[ i ];
 						break;
 					}
@@ -79,13 +76,21 @@ export default () => {
 		 * Clear event data.
 		 */
 		destroy() {
-			data.forEach( item => {
-				if ( item.elm ) {
-					item.elm.removeEventListener( item.event, item.handler, item.options );
-				}
-			} );
-
+			data.forEach( unsubscribe );
 			data = [];
 		},
 	};
+
+	/**
+	 * Remove the registered event listener.
+	 *
+	 * @param {Object} item - An object containing event data.
+	 */
+	function unsubscribe( item ) {
+		if ( item.elm ) {
+			item.elm.removeEventListener( item.event, item.handler, item.options );
+		}
+	}
+
+	return Event;
 }

+ 30 - 22
src/js/splide.js

@@ -10,9 +10,8 @@ import State from './core/state';
 import { DEFAULTS } from './constants/defaults';
 
 import compose from './core/composer';
-import { applyStyle } from './utils/dom';
 import { error, exist } from './utils/error';
-import { find } from './utils/dom';
+import { applyStyle, find } from './utils/dom';
 import { merge, each, values } from './utils/object';
 import * as STATES from './constants/states';
 
@@ -33,9 +32,9 @@ export default class Splide {
 	 */
 	constructor( root, options = {}, Components = {} ) {
 		this.root = root instanceof Element ? root : find( document, root );
-		exist( this.root, 'An invalid root element or selector was given.' );
+		exist( this.root, 'An invalid element/selector was given.' );
 
-		this.Components = {};
+		this.Components = null;
 		this.Event      = Event();
 		this.State      = State( STATES.CREATED );
 		this.STATES     = STATES;
@@ -58,7 +57,7 @@ export default class Splide {
 	 * @return {Splide|null} - This instance or null if an exception occurred.
 	 */
 	mount( Extensions = {}, Transition = null ) {
-		this.Components = compose( this, merge( this._c, Extensions ), Transition );
+		this.Components = this.Components || compose( this, merge( this._c, Extensions ), Transition );
 
 		try {
 			each( this.Components, ( component, key ) => {
@@ -75,18 +74,21 @@ export default class Splide {
 			return null;
 		}
 
+		this.State.set( STATES.MOUNTED );
+
 		each( this.Components, component => {
 			component.mounted && component.mounted();
 		} );
 
-		this.State.set( STATES.MOUNTED );
-		this.emit( 'mounted' );
+		// Breakpoints can destroy the Splide.
+		if ( ! this.State.is( STATES.DESTROYED ) ) {
+			this.emit( 'mounted' );
+			this.State.set( STATES.IDLE );
+			this.emit( 'ready' );
+		}
 
-		this.State.set( STATES.IDLE );
 		applyStyle( this.root, { visibility: 'visible' } );
 
-		this.emit( 'ready' );
-
 		return this;
 	}
 
@@ -174,8 +176,7 @@ export default class Splide {
 	 * @param {number}         index - A slide will be added at the position.
 	 */
 	add( slide, index = -1 ) {
-		this.Components.Elements.add( slide, index );
-		this.refresh();
+		this.Components.Elements.add( slide, index, this.refresh.bind( this ) );
 		return this;
 	}
 
@@ -195,25 +196,26 @@ export default class Splide {
 	 * And then call "updated" event.
 	 */
 	refresh() {
-		this.emit( 'refresh' ).emit( 'updated', this.options );
+		this.emit( 'refresh' ).emit( 'updated', this._o );
 		return this;
 	}
 
 	/**
 	 * Destroy the Splide.
+	 * "Completely" boolean is mainly for breakpoints.
+	 *
+	 * @param {boolean} completely - Destroy completely.
 	 */
-	destroy() {
+	destroy( completely = true ) {
 		values( this.Components ).reverse().forEach( component => {
-			component.destroy && component.destroy();
+			component.destroy && component.destroy( completely );
 		} );
 
-		this.emit( 'destroy' );
+		this.emit( 'destroy', completely );
 
 		// Destroy all event handlers, including ones for native events.
 		this.Event.destroy();
-
-		delete this.Components;
-		this.State.set( STATES.CREATED );
+		this.State.set( STATES.DESTROYED );
 
 		return this;
 	}
@@ -238,12 +240,12 @@ export default class Splide {
 
 	/**
 	 * Return length of slides.
-	 * This is an alias of Slides.length.
+	 * This is an alias of Elements.length.
 	 *
 	 * @return {number} - A number of slides.
 	 */
 	get length() {
-		return this.Components.Slides.length;
+		return this.Components.Elements.length;
 	}
 
 	/**
@@ -261,9 +263,15 @@ export default class Splide {
 	 * @param {Object} options - New options.
 	 */
 	set options( options ) {
+		const created = this.State.is( STATES.CREATED );
+
+		if ( ! created ) {
+			this.emit( 'update' );
+		}
+
 		this._o = merge( this._o, options );
 
-		if ( ! this.State.is( STATES.CREATED ) ) {
+		if ( ! created ) {
 			this.emit( 'updated', this._o );
 		}
 	}

+ 6 - 11
src/js/transitions/fade/index.js

@@ -17,8 +17,6 @@ import { applyStyle } from '../../utils/dom';
  * @return {Object} - The component object.
  */
 export default ( Splide, Components ) => {
-	Components.Options.fix( { perPage: 1, gap: 0, padding: 0 } );
-
 	if ( Components.Drag ) {
 		Components.Drag.required = false;
 	}
@@ -38,11 +36,11 @@ export default ( Splide, Components ) => {
 		 * @param {number}    destIndex - Destination slide index that might be clone's.
 		 * @param {number}    newIndex  - New index.
 		 * @param {Object}    coord     - Destination coordinates.
-		 * @param {function}  onEnd     - Callback function must be invoked when transition is completed.
+		 * @param {function}  done      - Callback function must be invoked when transition is completed.
 		 */
-		start( destIndex, newIndex, coord, onEnd ) {
+		start( destIndex, newIndex, coord, done ) {
 			apply( newIndex );
-			onEnd();
+			done();
 		},
 	};
 
@@ -52,14 +50,11 @@ export default ( Splide, Components ) => {
 	 * @param {number} index - A slide index.
 	 */
 	function apply( index ) {
-		const Slide   = Components.Slides.getSlide( index );
 		const options = Splide.options;
 
-		if ( Slide ) {
-			applyStyle( Slide.slide, {
-				transition: `opacity ${ options.speed }ms ${ options.easing }`,
-			} );
-		}
+		applyStyle( Components.Elements.slides[ index ], {
+			transition: `opacity ${ options.speed }ms ${ options.easing }`,
+		} );
 	}
 
 	return Fade;

+ 5 - 4
src/js/transitions/slide/index.js

@@ -51,13 +51,14 @@ export default ( Splide, Components ) => {
 		 * @param {number}   destIndex - Destination slide index that might be clone's.
 		 * @param {number}   newIndex  - New index.
 		 * @param {Object}   coord     - Destination coordinates.
-		 * @param {function} onEnd     - Callback function must be invoked when transition is completed.
+		 * @param {function} done      - Callback function must be invoked when transition is completed.
 		 */
-		start( destIndex, newIndex, coord, onEnd ) {
-			endCallback = onEnd;
+		start( destIndex, newIndex, coord, done ) {
+			const options = Splide.options;
+			endCallback = done;
 
 			applyStyle( list, {
-				transition: `transform ${ Splide.options.speed }ms ${ Splide.options.easing }`,
+				transition: `transform ${ options.speed }ms ${ options.easing }`,
 				transform : `translate(${ coord.x }px,${ coord.y }px)`,
 			} );
 		},

+ 47 - 15
src/js/utils/dom.js

@@ -19,21 +19,21 @@ import { toArray } from "./utils";
  * @return {Element|null} - A found element or null.
  */
 export function find( elm, selector ) {
-	return elm && selector ? elm.querySelector( selector.split( ' ' )[0] ) : null;
+	return elm ? elm.querySelector( selector.split( ' ' )[0] ) : null;
 }
 
 /**
- * Find a first child having the given class.
+ * Find a first child having the given tag or class name.
  *
- * @param {Element} parent    - A parent element.
- * @param {string}  className - A class name.
+ * @param {Element} parent         - A parent element.
+ * @param {string}  tagOrClassName - A tag or class name.
  *
  * @return {Element|null} - A found element on success. Null on failure.
  */
-export function child( parent, className ) {
+export function child( parent, tagOrClassName ) {
 	if ( parent ) {
 		return values( parent.children ).filter( child => {
-			return hasClass( child, className.split( ' ' )[0] );
+			return hasClass( child, tagOrClassName.split( ' ' )[0] ) || child.tagName.toLowerCase() === tagOrClassName;
 		} )[0] || null;
 	}
 
@@ -75,7 +75,11 @@ export function domify( html ) {
  * @param {Element|Element[]} elms - Element(s) to be removed.
  */
 export function remove( elms ) {
-	toArray( elms ).forEach( elm => { elm && elm.parentElement.removeChild( elm ) } );
+	toArray( elms ).forEach( elm => {
+		if ( elm && elm.parentElement ) {
+			elm.parentElement.removeChild( elm );
+		}
+	} );
 }
 
 /**
@@ -111,7 +115,9 @@ export function before( elm, ref ) {
 export function applyStyle( elm, styles ) {
 	if ( elm ) {
 		each( styles, ( value, prop ) => {
-			elm.style[ prop ] = value || '';
+			if ( value !== null ) {
+				elm.style[ prop ] = value;
+			}
 		} );
 	}
 }
@@ -185,20 +191,46 @@ export function setAttribute( elm, name, value ) {
  * @param {Element} elm  - An element where an attribute is assigned.
  * @param {string}  name - Attribute name.
  *
- * @return {string|null} - The value of the given attribute if available. Null if not.
+ * @return {string} - The value of the given attribute if available. An empty string if not.
  */
 export function getAttribute( elm, name ) {
-	return elm ? elm.getAttribute( name ) : null;
+	return elm ? elm.getAttribute( name ) : '';
 }
 
 /**
  * Remove attribute from the given element.
  *
- * @param {Element}      elm   - An element where an attribute is removed.
- * @param {string|Array} names - Attribute name.
+ * @param {Element|Element[]} elms  - An element where an attribute is removed.
+ * @param {string|string[]}      names - Attribute name.
  */
-export function removeAttribute( elm, names ) {
-	if ( elm ) {
-		toArray( names ).forEach( name => { elm.removeAttribute( name ) } );
+export function removeAttribute( elms, names ) {
+	toArray( names ).forEach( name => {
+		toArray( elms ).forEach( elm => elm && elm.removeAttribute( name ) );
+	} );
+}
+
+/**
+ * Trigger the given callback after all images contained by the element are loaded.
+ *
+ * @param {Element}  elm      - Element that may contain images.
+ * @param {Function} callback - Callback function fired right after all images are loaded.
+ */
+export function loaded( elm, callback ) {
+	const images = elm.querySelectorAll( 'img' );
+	const length = images.length;
+
+	if ( length ) {
+		let count = 0;
+
+		each( images, img => {
+			img.onload = img.onerror = () => {
+				if ( ++count === length ) {
+					callback();
+				}
+			};
+		} );
+	} else {
+		// Trigger the callback immediately if there is no image.
+		callback();
 	}
 }

+ 30 - 12
src/js/utils/object.js

@@ -50,19 +50,37 @@ export function isObject( subject ) {
  * @return {Object} - A merged object.
  */
 export function merge( { ...to }, from ) {
-	if ( isObject( to ) && isObject( from ) ) {
-		each( from, ( value, key ) => {
-			if ( isObject( value ) ) {
-				if ( ! isObject( to[ key ] ) ) {
-					to[ key ] = {};
-				}
-
-				to[ key ] = merge( to[ key ], value );
-			} else {
-				to[ key ] = value;
+	each( from, ( value, key ) => {
+		if ( isObject( value ) ) {
+			if ( ! isObject( to[ key ] ) ) {
+				to[ key ] = {};
 			}
-		} );
-	}
+
+			to[ key ] = merge( to[ key ], value );
+		} else {
+			to[ key ] = value;
+		}
+	} );
+
+	return to;
+}
+
+/**
+ * Assign all properties "from" to "to" object.
+ *
+ * @param {Object} to   - An object where properties are assigned.
+ * @param {Object} from - An object whose properties are assigned to "to".
+ *
+ * @return {Object} - An assigned object.
+ */
+export function assign( to, from ) {
+	to._s = from;
+
+	Object.keys( from ).forEach( key => {
+		if ( ! to[ key ] ) {
+			Object.defineProperty( to, key, Object.getOwnPropertyDescriptor( from, key ) );
+		}
+	} );
 
 	return to;
 }

+ 16 - 18
src/js/utils/time.js

@@ -14,7 +14,7 @@
  * @return {Function} - A debounced function.
  */
 export function throttle( func, wait ) {
-	let timeout = null;
+	let timeout;
 
 	// Declare function by the "function" keyword to prevent "this" from being inherited.
 	return function () {
@@ -41,28 +41,26 @@ export function createInterval( callback, interval, progress ) {
 	let start, elapse, rate, pause = true;
 
 	const step = timestamp => {
-		if ( pause ) {
-			return;
-		}
+		if ( ! pause ) {
+			if ( ! start ) {
+				start = timestamp;
+			}
 
-		if ( ! start ) {
-			start = timestamp;
-		}
+			elapse = timestamp - start;
+			rate   = elapse / interval;
 
-		elapse = timestamp - start;
-		rate   = elapse / interval;
+			if ( elapse >= interval ) {
+				start = 0;
+				rate  = 1;
+				callback();
+			}
 
-		if ( elapse >= interval ) {
-			start = 0;
-			rate  = 1;
-			callback();
-		}
+			if ( progress ) {
+				progress( rate );
+			}
 
-		if ( progress ) {
-			progress( rate );
+			requestAnimationFrame( step );
 		}
-
-		requestAnimationFrame( step );
 	};
 
 	return {

+ 23 - 16
src/js/utils/utils.js

@@ -56,13 +56,22 @@ export function sprintf( format, replacements ) {
 export function unit( value ) {
 	const type = typeof value;
 
-	if ( type === 'string' ) {
-		return value;
-	} else if ( type === 'number' && value > 0 ) {
+	if ( type === 'number' && value > 0 ) {
 		return parseFloat( value ) + 'px';
 	}
 
-	return '';
+	return type === 'string' ? value : '';
+}
+
+/**
+ * Pad start with 0.
+ *
+ * @param {number} number - A number to be filled with 0.
+ *
+ * @return {string|number} - Padded number.
+ */
+export function pad( number ) {
+	return number < 10 ? '0' + number : number
 }
 
 /**
@@ -74,22 +83,20 @@ export function unit( value ) {
  * @return {number} - Pixel.
  */
 export function toPixel( root, value ) {
-	if ( typeof value === 'number' ) {
-		return value;
-	}
+	if ( typeof value === 'string' ) {
+		const div = create( 'div', {} );
 
-	const div = create( 'div', {} );
+		applyStyle( div, {
+			position: 'absolute',
+			width: value,
+		} );
 
-	applyStyle( div, {
-		position: 'absolute',
-		width: value,
-	} );
+		append( root, div );
 
-	append( root, div );
+		value = div.clientWidth;
 
-	value = div.clientWidth;
-
-	remove( div );
+		remove( div );
+	}
 
 	return value;
 }

+ 1 - 0
src/sass/core/object/objects/_list.scss

@@ -1,6 +1,7 @@
 .splide {
   &__list {
     display: flex;
+    flex-wrap: wrap;
     margin: 0!important;
     padding: 0 !important;
   }

+ 4 - 0
src/sass/core/object/objects/_root.scss

@@ -1,4 +1,8 @@
 .splide {
   position: relative;
   visibility: hidden;
+
+  &.is-active {
+    visibility: visible;
+  }
 }

+ 3 - 6
tests/core/splide.test.js

@@ -43,8 +43,8 @@ describe( 'Splide ', () => {
 	} );
 
 	test( 'should add a slide dynamically.', () => {
-		const splide = new Splide( '#splide', {}, COMPLETE ).mount();
-		const slide = document.createElement( 'li' );
+		const splide = new Splide( '#splide', { perMove: 1 }, COMPLETE ).mount();
+		const slide  = document.createElement( 'li' );
 		const length = splide.length;
 
 		slide.classList.add( 'splide__slide' );
@@ -53,10 +53,7 @@ describe( 'Splide ', () => {
 		splide.add( slide );
 
 		expect( splide.length ).toBe( length + 1 );
-
-		splide.go( splide.length - 1 );
-
-		expect( slide.classList.contains( 'is-active' ) ).toBeTruthy();
+		expect( parseInt( splide.Components.Elements.slides[ splide.length - 1 ].textContent ) ).toBe( length );
 	} );
 
 	test( 'should remove a slide dynamically.', () => {

+ 18 - 0
tests/data/html.js

@@ -32,4 +32,22 @@ export const sync = minimum + `
       </ul>
     </div>
   </div>
+`;
+
+export const width = `
+  <div id="splide" style="width: 800px" class="splide">
+    <div class="splide__track">
+      <ul class="splide__list">
+      	<li class="splide__slide" style="width: 100px">1</li>
+      	<li class="splide__slide" style="width: 200px">2</li>
+      	<li class="splide__slide" style="width: 300px">3</li>
+      	<li class="splide__slide" style="width: 400px">4</li>
+      	<li class="splide__slide" style="width: 500px">5</li>
+      	<li class="splide__slide" style="width: 600px">6</li>
+      	<li class="splide__slide" style="width: 700px">7</li>
+      	<li class="splide__slide" style="width: 800px">8</li>
+      	<li class="splide__slide" style="width: 900px">9</li>
+      </ul>
+    </div>
+  </div>
 `;

+ 49 - 0
tests/functionality/autowidth.test.js

@@ -0,0 +1,49 @@
+import { width } from '../data/html';
+import Splide from '../../src/js/splide';
+import { COMPLETE } from '../../src/js/components';
+
+
+describe( 'When the autoWidth option is true, ', () => {
+	let splide;
+
+	beforeEach( () => {
+		document.body.innerHTML = width;
+		splide = new Splide( '#splide', { autoWidth: true }, COMPLETE );
+
+		Object.defineProperty( splide.root.querySelector( '.splide__track' ), 'clientWidth', { value: 800 } );
+		splide.root.querySelectorAll( '.splide__slide' ).forEach( slide => {
+			Object.defineProperty( slide, 'clientWidth', { value: parseInt( slide.style.width ) } );
+		} );
+
+		splide.mount();
+	} );
+
+	test( 'Splide should move slides according to their width.', done => {
+		splide.on( 'moved', () => {
+			const slide = splide.Components.Elements.slides[0];
+			expect( Math.abs( splide.Components.Track.position ) ).toBe( slide.clientWidth );
+			done();
+		} );
+
+		splide.go( 1 );
+		splide.Components.Elements.list.dispatchEvent( new Event( 'transitionend' ) );
+	} );
+
+	test( '"is-visible" class is properly toggled by the slide and viewport width.', done => {
+		splide.on( 'moved', () => {
+			const slide1 = splide.Components.Elements.slides[1];
+			const slide2 = splide.Components.Elements.slides[2]; // 300px, active
+			const slide3 = splide.Components.Elements.slides[3]; // 400px
+			const slide4 = splide.Components.Elements.slides[4]; // 500px
+
+			expect( slide1.classList.contains( 'is-visible' ) ).toBeFalsy();
+			expect( slide2.classList.contains( 'is-active' ) && slide2.classList.contains( 'is-visible' ) ).toBeTruthy();
+			expect( slide3.classList.contains( 'is-visible' ) ).toBeTruthy();
+			expect( slide4.classList.contains( 'is-visible' ) ).toBeFalsy(); // Out of the viewport.
+			done();
+		} );
+
+		splide.go( 2 );
+		splide.Components.Elements.list.dispatchEvent( new Event( 'transitionend' ) );
+	} );
+} );

+ 3 - 3
tests/functionality/slide.test.js → tests/functionality/elements.test.js

@@ -15,7 +15,7 @@ describe( 'The "slide" type Splide', () => {
 	test( 'should init index and slide attributes correctly.', () => {
 		expect( splide.index ).toBe( 0 );
 
-		const Slide     = splide.Components.Slides.getSlide( splide.index );
+		const Slide     = splide.Components.Elements.getSlide( splide.index );
 		const classList = Slide.slide.classList;
 
 		expect( classList.contains( STATUS_CLASSES.active ) ).toBe( true );
@@ -31,8 +31,8 @@ describe( 'The "slide" type Splide', () => {
 		splide.on( 'moved', ( newIndex, prevIndex ) => {
 			expect( Track.position ).toBe( -800 );
 
-			const prevSlide   = splide.Components.Slides.getSlide( prevIndex );
-			const newSlide    = splide.Components.Slides.getSlide( newIndex );
+			const prevSlide   = splide.Components.Elements.getSlide( prevIndex );
+			const newSlide    = splide.Components.Elements.getSlide( newIndex );
 			const prevClasses = prevSlide.slide.classList;
 			const newClasses  = newSlide.slide.classList;
 

+ 2 - 2
tests/functionality/focus.test.js

@@ -20,7 +20,7 @@ describe( 'Splide with "focus" option', () => {
 		Object.defineProperty( track, 'clientWidth', { value: width } );
 		splide.options = { focus: 'center', perPage };
 
-		expect( Track.offset ).toBe( - ( width / 2 - slideWidth / 2 ) );
+		expect( Track.offset( 0 ) ).toBe( - ( width / 2 - slideWidth / 2 ) );
 	} );
 
 	test( 'should locate the active slide according to the focus index.', () => {
@@ -33,6 +33,6 @@ describe( 'Splide with "focus" option', () => {
 		Object.defineProperty( track, 'clientWidth', { value: width } );
 		splide.options = { focus, perPage };
 
-		expect( Track.offset ).toBe( - slideWidth * focus );
+		expect( Track.offset( 0 ) ).toBe( - slideWidth * focus );
 	} );
 } );

+ 13 - 6
tests/functionality/layout.test.js

@@ -18,7 +18,7 @@ describe( 'The Layout ', () => {
 		const splide = new Splide( '#splide', { height: 400 }, COMPLETE );
 		splide.mount();
 
-		const slide = splide.Components.Elements.slides[ 0 ];
+		const slide = splide.Components.Elements.slides[0];
 		expect( slide.style.height ).toBe( '400px' );
 	} );
 
@@ -26,7 +26,7 @@ describe( 'The Layout ', () => {
 		const splide = new Splide( '#splide', { fixedHeight: 400 }, COMPLETE );
 		splide.mount();
 
-		const slide = splide.Components.Elements.slides[ 0 ];
+		const slide = splide.Components.Elements.slides[0];
 		expect( slide.style.height ).toBe( '400px' );
 	} );
 
@@ -41,7 +41,7 @@ describe( 'The Layout ', () => {
 
 		splide.emit( 'resize' );
 
-		const slide = splide.Components.Elements.slides[ 0 ];
+		const slide = splide.Components.Elements.slides[0];
 		expect( slide.style.width ).toBe( '400px' );
 
 		// Is the width updated correctly after perPage option is updated?
@@ -53,21 +53,28 @@ describe( 'The Layout ', () => {
 		const splide = new Splide( '#splide', { direction: 'ttb', perPage: 2, height: 400 }, COMPLETE );
 		splide.mount();
 
-		const track  = splide.Components.Elements.track;
+		const track = splide.Components.Elements.track;
 
 		Object.defineProperty( track, 'clientWidth', { value: 800 } );
 
-		const slide = splide.Components.Elements.slides[ 0 ];
+		const slide = splide.Components.Elements.slides[0];
 		expect( slide.style.height ).toBe( '200px' );
 
 		splide.options = { perPage: 4 };
 		expect( slide.style.height ).toBe( '100px' );
 	} );
 
+	test( 'should not set slide width when autoWidth option is true.', () => {
+		const splide = new Splide( '#splide', { autoWidth: true }, COMPLETE );
+		splide.mount();
+		const slide = splide.Components.Elements.slides[0];
+		expect( slide.style.width ).toBeFalsy();
+	} );
+
 	test( 'should set margin according to a gap size.', () => {
 		const splide = new Splide( '#splide', { gap: 10 }, COMPLETE );
 		splide.mount();
-		const slide  = splide.Components.Elements.slides[ 0 ];
+		const slide = splide.Components.Elements.slides[0];
 		expect( slide.style.marginRight ).toBe( '10px' );
 	} );
 } );

+ 1 - 1
tests/functionality/loop.test.js

@@ -23,7 +23,7 @@ describe( 'The "loop" type Splide', () => {
 
 		global.dispatchEvent( new Event( 'resize' ) );
 
-		expect( Track.offset ).toBe( width * Clones.length / 2 );
+		expect( Math.abs( Track.toPosition( 0 ) ) ).toBe( width * Clones.length / 2 );
 	} );
 
 	test( 'should move to clones before the first slide or after the last one then jump to actual slide.', done => {

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini