Ver Fonte

Splide can be destroyed. Implement "add/remove" functionality.

NaotoshiFujita há 5 anos atrás
pai
commit
f0b73a7d20

Diff do ficheiro suprimidas por serem muito extensas
+ 477 - 223
dist/js/splide.js


Diff do ficheiro suprimidas por serem muito extensas
+ 1 - 1
dist/js/splide.min.js


BIN
dist/js/splide.min.js.gz


+ 1 - 1
package-lock.json

@@ -1,6 +1,6 @@
 {
   "name": "@splidejs/splide",
-  "version": "1.3.3",
+  "version": "1.4.0",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "@splidejs/splide",
-  "version": "1.3.4",
+  "version": "1.4.0",
   "description": "Splide is a lightweight and powerful slider without any dependencies.",
   "author": "Naotoshi Fujita",
   "license": "MIT",

+ 15 - 3
src/js/components/a11y/index.js

@@ -60,6 +60,20 @@ export default ( Splide, Components ) => {
 
 			initAutoplay();
 		},
+
+		/**
+		 * 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' ] );
+				} );
+		},
 	};
 
 	/**
@@ -121,9 +135,7 @@ export default ( Splide, Components ) => {
 			const text     = options.focus === false && options.perPage > 1 ? i18n.pageX : i18n.slideX;
 			const label    = sprintf( text, item.page + 1 );
 			const button   = item.button;
-			const controls = [];
-
-			item.Slides.forEach( Slide => { controls.push( Slide.slide.id ) } );
+			const controls = item.Slides.map( Slide => Slide.slide.id );
 
 			setAttribute( button, ARIA_CONTROLS, controls.join( ' ' ) );
 			setAttribute( button, ARIA_LABEL, label );

+ 79 - 56
src/js/components/arrows/index.js

@@ -5,7 +5,7 @@
  * @copyright Naotoshi Fujita. All rights reserved.
  */
 
-import { create, subscribe } from '../../utils/dom';
+import { create, append, before, domify, remove, removeAttribute } from '../../utils/dom';
 import { XML_NAME_SPACE, PATH, SIZE } from './path';
 
 
@@ -20,11 +20,18 @@ import { XML_NAME_SPACE, PATH, SIZE } from './path';
  */
 export default ( Splide, Components, name ) => {
 	/**
-	 * Keep all created elements.
+	 * Previous arrow element.
 	 *
-	 * @type {Object}
+	 * @type {Element|undefined}
 	 */
-	let arrows;
+	let prev;
+
+	/**
+	 * Next arrow element.
+	 *
+	 * @type {Element|undefined}
+	 */
+	let next;
 
 	/**
 	 * Store the class list.
@@ -40,6 +47,13 @@ export default ( Splide, Components, name ) => {
 	 */
 	const root = Splide.root;
 
+	/**
+	 * Whether arrows are created programmatically or not.
+	 *
+	 * @type {boolean}
+	 */
+	let created;
+
 	/**
 	 * Arrows component object.
 	 *
@@ -57,99 +71,108 @@ export default ( Splide, Components, name ) => {
 		 * Called when the component is mounted.
 		 */
 		mount() {
-			const Elements     = Components.Elements;
-			const arrowsOption = Splide.options.arrows;
+			const Elements = Components.Elements;
 
-			arrows = Elements.arrows;
+			// Attempt to get arrows from HTML source.
+			prev = Elements.arrows.prev;
+			next = Elements.arrows.next;
 
 			// If arrows were not found in HTML, let's generate them.
-			if ( ( ! arrows.prev || ! arrows.next ) && arrowsOption ) {
-				arrows = createArrows();
-				const slider = Elements.slider;
-				const parent = arrowsOption === 'slider' && slider ? slider : root;
-				parent.insertBefore( arrows.wrapper, parent.firstChild );
+			if ( ( ! prev || ! next ) && Splide.options.arrows ) {
+				prev = createArrow( true );
+				next = createArrow( false );
+				created = true;
+
+				appendArrows();
 			}
 
-			if ( arrows ) {
-				listen();
+			if ( prev && next ) {
 				bind();
 			}
 
-			this.arrows = arrows;
+			this.arrows = { prev, next };
 		},
 
 		/**
 		 * Called after all components are mounted.
 		 */
 		mounted() {
-			Splide.emit( `${ name }:mounted`, arrows.prev, arrows.next );
+			Splide.emit( `${ name }:mounted`, prev, next );
+		},
+
+		/**
+		 * Destroy.
+		 */
+		destroy() {
+			[ prev, next ].forEach( elm => { removeAttribute( elm, 'disabled' ) } );
+
+			if ( created ) {
+				remove( prev.parentElement );
+			}
 		},
 	};
 
 	/**
-	 * Subscribe click events.
+	 * Listen native and custom events.
 	 */
-	function listen() {
-		subscribe( arrows.prev, 'click', () => {
-			const perMove = Splide.options.perMove;
-			Splide.go( perMove ? `-${ perMove }` : '<' );
-		} );
-
-		subscribe( arrows.next, 'click', () => {
-			const perMove = Splide.options.perMove;
-			Splide.go( perMove ? `+${ perMove }` : '>' );
-		} );
+	function bind() {
+		Splide
+			.on( 'click', () => onClick( true ), prev )
+			.on( 'click', () => onClick( false ), next )
+			.on( 'mounted move updated', updateDisabled );
 	}
 
 	/**
-	 * Update a disable attribute.
+	 * Called when an arrow is clicked.
+	 *
+	 * @param {boolean} prev - If true, the previous arrow is clicked.
 	 */
-	function bind() {
-		Splide.on( 'mounted move updated', () => {
-			const { prev, next } = arrows;
-			const { prevIndex, nextIndex } = Components.Controller;
-			const hasSlides = Splide.length > 1;
+	function onClick( prev ) {
+		const perMove = Splide.options.perMove;
+		Splide.go( perMove ? `${ prev ? '-' : '+' }${ perMove }` : ( prev ? '<' : '>' ) );
+	}
 
-			prev.disabled = prevIndex < 0 || ! hasSlides;
-			next.disabled = nextIndex < 0 || ! hasSlides;
+	/**
+	 * Update a disabled attribute.
+	 */
+	function updateDisabled() {
+		const { prevIndex, nextIndex } = Components.Controller;
+		const isEnough = Splide.length > Splide.options.perPage;
+
+		prev.disabled = prevIndex < 0 || ! isEnough;
+		next.disabled = nextIndex < 0 || ! isEnough;
 
-			Splide.emit( `${ name }:updated`, prev, next, prevIndex, nextIndex );
-		} );
+		Splide.emit( `${ name }:updated`, prev, next, prevIndex, nextIndex );
 	}
 
 	/**
-	 * Create a wrapper and arrow elements.
-	 *
-	 * @return {Object} - An object contains created elements.
+	 * Create a wrapper element and append arrows.
 	 */
-	function createArrows() {
+	function appendArrows() {
 		const wrapper = create( 'div', { class: classes.arrows } );
-		const prev    = createArrow( true );
-		const next    = createArrow( false );
 
-		wrapper.appendChild( prev );
-		wrapper.appendChild( next );
+		append( wrapper, prev );
+		append( wrapper, next );
+
+		const slider = Components.Elements.slider;
+		const parent = Splide.options.arrows === 'slider' && slider ? slider : root;
 
-		return { wrapper, prev, next };
+		before( wrapper, parent.firstElementChild );
 	}
 
 	/**
 	 * Create an arrow element.
 	 *
-	 * @param {boolean} isPrev - Determine to create a prev arrow or next arrow.
+	 * @param {boolean} prev - Determine to create a prev arrow or next arrow.
 	 *
 	 * @return {Element} - A created arrow element.
 	 */
-	function createArrow( isPrev ) {
-		const arrow = create( 'button', {
-			class: `${ classes.arrow } ${ isPrev ? classes.prev : classes.next }`,
-		} );
-
-		arrow.innerHTML = `<svg xmlns="${ XML_NAME_SPACE }"	viewBox="0 0 ${ SIZE } ${ SIZE }"	width="${ SIZE }"	height="${ SIZE }">`
-			+ `<path d="${ Splide.options.arrowPath || PATH }" />`
-			+ `</svg>`;
+	function createArrow( prev ) {
+		const arrow = `<button class="${ classes.arrow } ${ prev ? classes.prev : classes.next }">`
+			+	`<svg xmlns="${ XML_NAME_SPACE }"	viewBox="0 0 ${ SIZE } ${ SIZE }"	width="${ SIZE }"	height="${ SIZE }">`
+			+ `<path d="${ Splide.options.arrowPath || PATH }" />`;
 
-		return arrow;
+		return domify( arrow );
 	}
 
 	return Arrows;

+ 21 - 13
src/js/components/autoplay/index.js

@@ -5,7 +5,7 @@
  * @copyright Naotoshi Fujita. All rights reserved.
  */
 
-import { applyStyle, subscribe } from '../../utils/dom';
+import { applyStyle } from '../../utils/dom';
 import { createInterval } from '../../utils/time';
 
 /**
@@ -66,7 +66,10 @@ export default ( Splide, Components, name ) => {
 			if ( slides.length > options.perPage ) {
 				interval = createInterval( () => { Splide.go( '>' ) }, options.interval, rate => {
 					Splide.emit( `${ name }:playing`, rate );
-					bar && applyStyle( bar, { width: `${ rate * 100 }%` } );
+
+					if ( bar ) {
+						applyStyle( bar, { width: `${ rate * 100 }%` } );
+					}
 				} );
 
 				bind();
@@ -113,8 +116,8 @@ export default ( Splide, Components, name ) => {
 	function bind() {
 		const options  = Splide.options;
 		const Elements = Components.Elements;
-		const sub      = Splide.sub;
-		const elms     = [ Splide.root, sub ? sub.root : null ];
+		const sibling  = Splide.sibling;
+		const elms     = [ Splide.root, sibling ? sibling.root : null ];
 
 		if ( options.pauseOnHover ) {
 			switchOn( elms, 'mouseleave', PAUSE_FLAGS.HOVER, true );
@@ -126,16 +129,21 @@ export default ( Splide, Components, name ) => {
 			switchOn( elms, 'focusin', PAUSE_FLAGS.FOCUS, false );
 		}
 
-		subscribe( Elements.play, 'click', () => {
-			// Need to be removed a focus flag at first.
-			Autoplay.play( PAUSE_FLAGS.FOCUS );
-			Autoplay.play( PAUSE_FLAGS.MANUAL );
-		} );
+		Splide
+			.on( 'click', () => {
+				// Need to be removed a focus flag at first.
+				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();
+			} );
 
 		switchOn( [ Elements.pause ], 'click', PAUSE_FLAGS.MANUAL, false );
-
-		// Rewind the timer when others move the slide.
-		Splide.on( 'move', () => { Autoplay.play() } );
 	}
 
 	/**
@@ -148,7 +156,7 @@ export default ( Splide, Components, name ) => {
 	 */
 	function switchOn( elms, event, flag, play ) {
 		for ( let i in elms ) {
-			subscribe( elms[ i ], event, () => { Autoplay[ play ? 'play' : 'pause' ]( flag ) } );
+			Splide.on( event, () => { Autoplay[ play ? 'play' : 'pause' ]( flag ) }, elms[ i ] );
 		}
 	}
 

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

@@ -95,7 +95,7 @@ export default ( Splide ) => {
 	 * @return {number|string} - A breakpoint as number or string. -1 if no point matches.
 	 */
 	function getPoint() {
-		const item = map.filter( item => item.mql.matches )[ 0 ];
+		const item = map.filter( item => item.mql.matches )[0];
 		return item ? item.point : -1;
 	}
 

+ 2 - 4
src/js/components/click/index.js

@@ -6,7 +6,6 @@
  */
 
 import { FADE } from "../../constants/types";
-import { subscribe } from "../../utils/dom";
 
 
 /**
@@ -43,9 +42,8 @@ export default ( Splide, Components ) => {
 		 * Called when the component is mounted.
 		 */
 		mount() {
-			subscribe( Components.Elements.track, 'click', click, { capture: true } );
-
 			Splide
+				.on( 'click', onClick, Components.Elements.track, { capture: true } )
 				.on( 'drag', () => { disabled = true } )
 				.on( 'moved', () => { disabled = false } );
 		},
@@ -56,7 +54,7 @@ export default ( Splide, Components ) => {
 	 *
 	 * @param {Event} e - A click event.
 	 */
-	function click( e ) {
+	function onClick( e ) {
 		if ( disabled ) {
 			e.preventDefault();
 			e.stopPropagation();

+ 35 - 20
src/js/components/clones/index.js

@@ -6,7 +6,7 @@
  */
 
 import { LOOP } from '../../constants/types';
-import { addClass, removeAttribute } from '../../utils/dom';
+import { addClass, removeAttribute, append, before, remove } from '../../utils/dom';
 
 
 /**
@@ -23,7 +23,7 @@ export default ( Splide, Components ) => {
 	 *
 	 * @type {Array}
 	 */
-	const clones = [];
+	let clones = [];
 
 	/**
 	 * Clones component object.
@@ -37,9 +37,22 @@ export default ( Splide, Components ) => {
 		mount() {
 			if ( Splide.is( LOOP ) ) {
 				generateClones();
+
+				Splide.on( 'refresh', () => {
+					this.destroy();
+					generateClones();
+				} );
 			}
 		},
 
+		/**
+		 * Destroy.
+		 */
+		destroy() {
+			remove( clones );
+			clones = [];
+		},
+
 		/**
 		 * Return all clones.
 		 *
@@ -66,32 +79,34 @@ export default ( Splide, Components ) => {
 	 * - Whether the slide length is enough for perPage.
 	 */
 	function generateClones() {
-		const { Slides, Elements: { list } }  = Components;
-		const { perPage, drag, flickMaxPages = 1 } = Splide.options;
+		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 );
 
-		let slides = Slides.getSlides( false, false );
+		if ( length ) {
+			let slides = Slides.getSlides( false, false );
 
-		while ( slides.length < count ) {
-			slides = slides.concat( slides );
-		}
+			while ( slides.length < count ) {
+				slides = slides.concat( slides );
+			}
 
-		slides.slice( 0, count ).forEach( ( elm, index ) => {
-			const clone = cloneDeeply( elm );
-			list.appendChild( clone );
-			clones.push( clone );
+			slides.slice( 0, count ).forEach( ( elm, index ) => {
+				const clone = cloneDeeply( elm );
+				append( Components.Elements.list, clone );
+				clones.push( clone );
 
-			Slides.register( index + length, index, clone );
-		} );
+				Slides.register( index + length, index, clone );
+			} );
 
-		slides.slice( -count ).forEach( ( elm, index ) => {
-			const clone = cloneDeeply( elm );
-			list.insertBefore( clone, slides[0] );
-			clones.push( clone );
+			slides.slice( -count ).forEach( ( elm, index ) => {
+				const clone = cloneDeeply( elm );
+				before( clone, slides[0] );
+				clones.push( clone );
 
-			Slides.register( index - count, index, clone );
-		} );
+				Slides.register( index - count, index, clone );
+			} );
+		}
 	}
 
 	/**

+ 19 - 6
src/js/components/controller/index.js

@@ -28,6 +28,13 @@ export default ( Splide, Components ) => {
 	 */
 	let options;
 
+	/**
+	 * True if the slide is LOOP mode.
+	 *
+	 * @type {boolean}
+	 */
+	let isLoop;
+
 	/**
 	 * Controller component object.
 	 *
@@ -39,6 +46,7 @@ export default ( Splide, Components ) => {
 		 */
 		mount() {
 			options = Splide.options;
+			isLoop  = Splide.is( LOOP );
 			bind();
 		},
 
@@ -155,7 +163,7 @@ export default ( Splide, Components ) => {
 		 * @return {number} - A trimmed index.
 		 */
 		trim( index ) {
-			if ( ! Splide.is( LOOP ) ) {
+			if ( ! isLoop ) {
 				index = options.rewind ? this.rewind( index ) : between( index, 0, this.edgeIndex );
 			}
 
@@ -172,7 +180,7 @@ export default ( Splide, Components ) => {
 		rewind( index ) {
 			const edge = this.edgeIndex;
 
-			if ( Splide.is( LOOP ) ) {
+			if ( isLoop ) {
 				while( index > edge ) {
 					index -= edge + 1;
 				}
@@ -218,7 +226,11 @@ export default ( Splide, Components ) => {
 		get edgeIndex() {
 			const length = Splide.length;
 
-			if ( hasFocus() || options.isNavigation || Splide.is( LOOP ) ) {
+			if ( ! length ) {
+				return 0;
+			}
+
+			if ( hasFocus() || options.isNavigation || isLoop ) {
 				return length - 1;
 			}
 
@@ -233,7 +245,7 @@ export default ( Splide, Components ) => {
 		get prevIndex() {
 			let prev = Splide.index - 1;
 
-			if ( Splide.is( LOOP ) || options.rewind ) {
+			if ( isLoop || options.rewind ) {
 				prev = this.rewind( prev );
 			}
 
@@ -248,7 +260,7 @@ export default ( Splide, Components ) => {
 		get nextIndex() {
 			let next = Splide.index + 1;
 
-			if ( Splide.is( LOOP ) || options.rewind ) {
+			if ( isLoop || options.rewind ) {
 				next = this.rewind( next );
 			}
 
@@ -264,7 +276,8 @@ export default ( Splide, Components ) => {
 			.on( 'move', newIndex => { Splide.index = newIndex } )
 			.on( 'updated', newOptions => {
 				options = newOptions;
-				Splide.index = Controller.rewind( Controller.trim( Splide.index ) );
+				const index = between( Splide.index, 0, Controller.edgeIndex );
+				Splide.index = Controller.rewind( Controller.trim( index ) );
 			} );
 	}
 

+ 29 - 18
src/js/components/cover/index.js

@@ -31,40 +31,51 @@ export default ( Splide, Components ) => {
 	 */
 	const Cover = {
 		/**
-		 * To set an image as cover, the height option is required.
+		 * Required only when "cover" option is true.
 		 *
 		 * @type {boolean}
 		 */
-		required: options.cover	&& ( options.height || options.heightRatio || options.fixedHeight ),
+		required: options.cover,
 
 		/**
 		 * Called when the component is mounted.
 		 */
 		mount() {
-			Components.Slides.getSlides( true, false ).forEach( slide => {
-				const img = find( slide, 'img' );
-
-				if ( img && img.src ) {
-					cover( img );
-				}
-			} );
-
+			apply( false );
 			Splide.on( 'lazyload:loaded', img => { cover( img ) } );
+			Splide.on( 'updated', () => apply( false ) );
+		},
+
+		/**
+		 * Destroy.
+		 */
+		destroy() {
+			apply( true );
 		},
 	};
 
+	/**
+	 * Apply "cover" to all slides.
+	 */
+	function apply( uncover ) {
+		Components.Slides.getSlides( true, false ).forEach( slide => {
+			const img = find( slide, 'img' );
+
+			if ( img && img.src ) {
+				cover( img, uncover );
+			}
+		} );
+	}
+
 	/**
 	 * Set background image of the parent element, using source of the given image element.
 	 *
-	 * @param {Element} img - An image element.
+	 * @param {Element} img     - An image element.
+	 * @param {boolean} uncover - Optional. Reset "cover".
 	 */
-	function cover( img ) {
-		const parent = img.parentElement;
-
-		if ( parent ) {
-			applyStyle( parent, { background: `center/cover no-repeat url("${ img.src }")` } );
-			applyStyle( img, { display: 'none' } );
-		}
+	function cover( img, uncover = false ) {
+		applyStyle( img.parentElement, { background: uncover ? '' : `center/cover no-repeat url("${ img.src }")` } );
+		applyStyle( img, { display: uncover ? '' : 'none' } );
 	}
 
 	return Cover;

+ 5 - 5
src/js/components/drag/index.js

@@ -7,7 +7,6 @@
 
 import { LOOP } from '../../constants/types';
 import { TTB } from '../../constants/directions';
-import { subscribe } from '../../utils/dom';
 import { between } from '../../utils/utils';
 import { IDLE } from '../../constants/states';
 import { each } from "../../utils/object";
@@ -128,13 +127,14 @@ export default ( Splide, Components ) => {
 		mount() {
 			const list = Components.Elements.list;
 
-			subscribe( list, 'touchstart mousedown', start );
-			subscribe( list, 'touchmove mousemove', move, { passive: false } );
-			subscribe( list, 'touchend touchcancel mouseleave mouseup dragend', end );
+			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 => {
-				subscribe( elm, 'dragstart', e => { e.preventDefault() }, { passive: false } );
+				Splide.on( 'dragstart', e => { e.preventDefault() }, elm, { passive: false } );
 			} );
 		},
 	};

+ 72 - 34
src/js/components/elements/index.js

@@ -1,11 +1,11 @@
 /**
- * The component for the root element.
+ * The component for main elements.
  *
  * @author    Naotoshi Fujita
  * @copyright Naotoshi Fujita. All rights reserved.
  */
 
-import { find, addClass, child } from '../../utils/dom';
+import { find, addClass, removeClass, child, remove, append, before, domify } from '../../utils/dom';
 import { exist } from '../../utils/error';
 import { values } from '../../utils/object';
 
@@ -14,11 +14,11 @@ import { values } from '../../utils/object';
  *
  * @type {string}
  */
-const UID_NAME = 'splideUid';
+const UID_NAME = 'uid';
 
 
 /**
- * The component for the root element.
+ * The component for main elements.
  *
  * @param {Splide} Splide - A Splide instance.
  *
@@ -44,8 +44,9 @@ export default ( Splide ) => {
 	 * Note that IE doesn't support padStart() to fill the uid by 0.
 	 */
 	if ( ! root.id ) {
-		let uid = window[ UID_NAME ] || 0;
-		window[ UID_NAME ] = ++uid;
+		window.splide = window.splide || {};
+		let uid = window.splide[ UID_NAME ] || 0;
+		window.splide[ UID_NAME ] = ++uid;
 		root.id = `splide${ uid < 10 ? '0' + uid : uid }`;
 	}
 
@@ -71,7 +72,6 @@ export default ( Splide ) => {
 			exist( this.list, `A list ${ message }` );
 
 			this.slides = values( this.list.children );
-			exist( this.slides.length, `A slide ${ message }` );
 
 			const arrows = findParts( classes.arrows );
 			this.arrows = {
@@ -88,23 +88,75 @@ export default ( Splide ) => {
 		},
 
 		/**
-		 * Called after all components are mounted.
+		 * Destroy.
 		 */
-		mounted() {
-			const rootClass = classes.root;
-			const options   = Splide.options;
-
-			addClass(
-				root,
-				`${ rootClass }`,
-				`${ rootClass }--${ options.type }`,
-				`${ rootClass }--${ options.direction }`,
-				options.drag ? `${ rootClass }--draggable` : '',
-				options.isNavigation ? `${ rootClass }--nav` : ''
-			);
+		destroy() {
+			removeClass( root, getClasses() );
+		},
+
+		/**
+		 * 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.
+		 */
+		add( slide, index ) {
+			if ( typeof slide === 'string' ) {
+				slide = domify( slide );
+			}
+
+			if ( slide instanceof Element ) {
+				const ref = this.slides[ index ];
+
+				if ( ref ) {
+					before( slide, ref );
+					this.slides.splice( index, 0, slide );
+				} else {
+					append( this.list, slide );
+					this.slides.push( slide );
+				}
+			}
+		},
+
+		/**
+		 * Remove a slide from a slider.
+		 * Need to refresh Splide after removing a slide.
+		 *
+		 * @param index - Slide index.
+		 */
+		remove( index ) {
+			const slides = this.slides.splice( index, 1 );
+			remove( slides[0] );
 		},
 	};
 
+	/**
+	 * 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() );
+	}
+
+	/**
+	 * Return class names for the root element.
+	 */
+	function getClasses() {
+		const rootClass = classes.root;
+		const options   = Splide.options;
+
+		return [
+			`${ rootClass }--${ options.type }`,
+			`${ rootClass }--${ options.direction }`,
+			options.drag ? `${ rootClass }--draggable` : '',
+			options.isNavigation ? `${ rootClass }--nav` : '',
+		];
+	}
+
 	/**
 	 * Find parts only from children of the root or track.
 	 *
@@ -114,19 +166,5 @@ export default ( Splide ) => {
 		return child( root, className ) || child( Elements.slider, className );
 	}
 
-	/**
-	 * Initialization.
-	 * Assign ID to some elements if it's not available.
-	 */
-	function init() {
-		if ( ! Elements.track.id ) {
-			Elements.track.id = `${ root.id }-track`;
-		}
-
-		if ( ! Elements.list.id ) {
-			Elements.list.id = `${ root.id }-list`;
-		}
-	}
-	
 	return Elements;
 }

+ 3 - 15
src/js/components/keyboard/index.js

@@ -5,8 +5,6 @@
  * @copyright Naotoshi Fujita. All rights reserved.
  */
 
-import { subscribe } from "../../utils/dom";
-
 /**
  * Map a key to a slide control.
  *
@@ -38,13 +36,6 @@ const KEY_MAP = {
  * @return {Object} - The component object.
  */
 export default ( Splide ) => {
-	/**
-	 * Hold functions to remove event listener.
-	 *
-	 * @type {Array|undefined}
-	 */
-	let removers;
-
 	return {
 		/**
 		 * Called when the component is mounted.
@@ -53,17 +44,14 @@ export default ( Splide ) => {
 			const map = KEY_MAP[ Splide.options.direction === 'ttb' ? 'vertical' : 'horizontal' ];
 
 			Splide.on( 'mounted updated', () => {
-				if ( removers ) {
-					removers[ 0 ]();
-					removers = undefined;
-				}
+				Splide.off( 'keydown', Splide.root );
 
 				if ( Splide.options.keyboard ) {
-					removers = subscribe( Splide.root, 'keydown', e => {
+					Splide.on( 'keydown', e => {
 						if ( map[ e.key ] ) {
 							Splide.go( map[ e.key ] );
 						}
-					} );
+					}, Splide.root );
 				}
 			} );
 		},

+ 33 - 16
src/js/components/layout/index.js

@@ -10,7 +10,7 @@ import Vertical from './resolvers/vertical';
 
 import { unit } from '../../utils/utils';
 import { throttle } from '../../utils/time';
-import { subscribe, applyStyle } from '../../utils/dom';
+import { applyStyle, removeAttribute } from '../../utils/dom';
 
 /**
  * Interval time for throttle.
@@ -43,6 +43,13 @@ export default ( Splide, Components ) => {
 	 */
 	let list;
 
+	/**
+	 * Store the track element.
+	 *
+	 * @type {Element}
+	 */
+	let track;
+
 	/**
 	 * Store all Slide objects.
 	 *
@@ -80,13 +87,22 @@ export default ( Splide, Components ) => {
 		 * Called when the component is mounted.
 		 */
 		mount() {
-			list   = Elements.list;
-			Slides = Components.Slides.getSlides( true, true );
+			list  = Elements.list;
+			track = Elements.track;
 
 			bind();
 			init();
 		},
 
+		/**
+		 * Destroy.
+		 */
+		destroy() {
+			Elements.slides
+				.concat( [ list, track ] )
+				.forEach( elm => { removeAttribute( elm, 'style' ) } );
+		},
+
 		/**
 		 * Return slider width without padding.
 		 *
@@ -177,6 +193,8 @@ export default ( Splide, Components ) => {
 	function init() {
 		const options = Splide.options;
 
+		Slides = Components.Slides.getSlides( true, true );
+
 		if ( isVertical ) {
 			Resolver = Vertical( Splide, Components, options );
 		} else {
@@ -187,9 +205,9 @@ export default ( Splide, Components ) => {
 
 		applyStyle( root, { maxWidth: unit( options.width ) } );
 
-		for ( const i in Slides ) {
-			applyStyle( Slides[ i ].slide, { [ Resolver.marginProp ]: unit( Resolver.gap ) } )
-		}
+		Slides.forEach( Slide => {
+			applyStyle( Slide.slide, { [ Resolver.marginProp ]: unit( Resolver.gap ) } )
+		} );
 
 		resize();
 	}
@@ -199,9 +217,10 @@ export default ( Splide, Components ) => {
 	 * Initialize when the component is mounted or options are updated.
 	 */
 	function bind() {
-		const throttledResize = throttle( () => { Splide.emit( 'resize' ) }, THROTTLE );
-		subscribe( window, 'resize', throttledResize );
-		Splide.on( 'mounted resize', resize ).on( 'updated', init );
+		Splide
+			.on( 'resize', throttle( () => { Splide.emit( 'resize' ) }, THROTTLE ), window )
+			.on( 'mounted resize', resize )
+			.on( 'updated', init );
 	}
 
 	/**
@@ -209,17 +228,15 @@ export default ( Splide, Components ) => {
 	 */
 	function resize() {
 		applyStyle( list, { width: unit( Layout.listWidth ), height: unit( Layout.listHeight ) } );
-		applyStyle( Components.Elements.track, { height: unit( Layout.height ) } );
+		applyStyle( track, { height: unit( Layout.height ) } );
 
 		const slideWidth  = unit( Resolver.slideWidth );
 		const slideHeight = unit( Resolver.slideHeight );
 
-		for ( let i in Slides ) {
-			const { slide, container } = Slides[ i ];
-
-			applyStyle( container, { height: slideHeight } );
-			applyStyle( slide, { width: slideWidth,	height: ! container ? slideHeight : '' } );
-		}
+		Slides.forEach( Slide => {
+			applyStyle( Slide.container, { height: slideHeight } );
+			applyStyle( Slide.slide, { width: slideWidth,	height: ! Slide.container ? slideHeight : '' } );
+		} );
 	}
 
 	return Layout;

+ 20 - 8
src/js/components/lazyload/index.js

@@ -6,7 +6,7 @@
  */
 
 import { STATUS_CLASSES } from '../../constants/classes';
-import { create, 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.
@@ -54,6 +54,13 @@ export default ( Splide, Components, name ) => {
 	 */
 	const isSequential = lazyload === 'sequential';
 
+	/**
+	 * Whether to stop sequential load.
+	 *
+	 * @type {boolean}
+	 */
+	let stop = false;
+
 	/**
 	 * Lazyload component object.
 	 *
@@ -84,17 +91,22 @@ export default ( Splide, Components, name ) => {
 				if ( isSequential ) {
 					loadNext();
 				} else {
-					Splide
-						.on( 'mounted', () => { check( Splide.index ) } )
-						.on( `moved.${ name }`, index => { check( index ) } );
+					Splide.on( `mounted moved.${ name }`, index => { check( index || Splide.index ) } );
 				}
 			}
 		},
+
+		/**
+		 * Destroy.
+		 */
+		destroy() {
+			stop = true;
+		},
 	};
 
 	/**
 	 * Check how close each image is from the active slide and
-	 * determine whether to start loading or not according to the distance.
+	 * determine whether to start loading or not, according to the distance.
 	 *
 	 * @param {number} index - Current index.
 	 */
@@ -127,7 +139,7 @@ export default ( Splide, Components, name ) => {
 		addClass( Slide.slide, STATUS_CLASSES.loading );
 
 		const spinner = create( 'span', { class: Splide.classes.spinner } );
-		img.parentElement.appendChild( spinner );
+		append( img.parentElement, spinner );
 
 		img.onload  = () => { loaded( img, spinner, Slide, false ) };
 		img.onerror = () => { loaded( img, spinner, Slide, true ) };
@@ -159,12 +171,12 @@ export default ( Splide, Components, name ) => {
 		removeClass( Slide.slide, STATUS_CLASSES.loading );
 
 		if ( ! error ) {
-			img.parentElement.removeChild( spinner );
+			remove( spinner );
 			applyStyle( img, { visibility: 'visible' } );
 			Splide.emit( `${ name }:loaded`, img );
 		}
 
-		if ( isSequential ) {
+		if ( isSequential && ! stop ) {
 			loadNext();
 		}
 	}

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

@@ -8,6 +8,7 @@
 import { each, merge } from '../../utils/object';
 import { error } from '../../utils/error';
 import { getAttribute } from "../../utils/dom";
+import { CREATED } from "../../constants/states";
 
 /**
  * The component for initializing options.
@@ -34,7 +35,7 @@ export default ( Splide ) => {
 		try {
 			Splide.options = JSON.parse( options );
 		} catch ( e ) {
-			error( '"data-splide" must be a valid JSON.' );
+			error( e.message );
 		}
 	}
 
@@ -43,7 +44,9 @@ export default ( Splide ) => {
 		 * Called when the component is mounted.
 		 */
 		mount() {
-			Splide.index = Splide.options.start;
+			if ( Splide.State.is( CREATED ) ) {
+				Splide.index = Splide.options.start;
+			}
 		},
 
 		/**

+ 18 - 17
src/js/components/pagination/index.js

@@ -5,8 +5,7 @@
  * @copyright Naotoshi Fujita. All rights reserved.
  */
 
-import { create, addClass, removeClass } from '../../utils/dom';
-import { subscribe } from '../../utils/dom';
+import { create, remove, append, addClass, removeClass } from '../../utils/dom';
 import { STATUS_CLASSES } from '../../constants/classes';
 
 /**
@@ -71,7 +70,7 @@ export default ( Splide, Components, name ) => {
 
 			const slider = Components.Elements.slider;
 			parent = Splide.options.pagination === 'slider' && slider ? slider : Splide.root;
-			parent.appendChild( data.list );
+			append( parent, data.list );
 
 			bind();
 		},
@@ -81,7 +80,6 @@ export default ( Splide, Components, name ) => {
 		 */
 		mounted() {
 			const index = Splide.index;
-
 			Splide.emit( `${ name }:mounted`, data, this.getItem( index ) );
 			update( index, -1 );
 		},
@@ -91,12 +89,14 @@ export default ( Splide, Components, name ) => {
 		 * Be aware that node.remove() is not supported by IE.
 		 */
 		destroy() {
-			if ( data && data.list ) {
-				parent.removeChild( data.list );
+			remove( data.list );
+
+			if ( data.items ) {
+				data.items.forEach( item => { Splide.off( 'click', item.button ) } );
 			}
 
-			Splide.off( ATTRIBUTES_UPDATE_EVENT );
-			data = null;
+			Splide.off( ATTRIBUTES_UPDATE_EVENT ).off( UPDATE_EVENT );
+			data = {};
 		},
 
 		/**
@@ -143,15 +143,16 @@ export default ( Splide, Components, name ) => {
 	 * @param {number} prevIndex - Prev index.
 	 */
 	function update( index, prevIndex ) {
-		const prev = Pagination.getItem( prevIndex );
-		const curr = Pagination.getItem( index );
+		const prev   = Pagination.getItem( prevIndex );
+		const curr   = Pagination.getItem( index );
+		const active = STATUS_CLASSES.active;
 
 		if ( prev ) {
-			removeClass( prev.button, STATUS_CLASSES.active );
+			removeClass( prev.button, active );
 		}
 
 		if ( curr ) {
-			addClass( curr.button, STATUS_CLASSES.active );
+			addClass( curr.button, active );
 		}
 
 		Splide.emit( `${ name }:updated`, data, prev, curr );
@@ -171,13 +172,13 @@ export default ( Splide, Components, name ) => {
 		const items = Slides.getSlides( false, true )
 			.filter( Slide => options.focus !== false || Slide.index % options.perPage === 0 )
 			.map( ( Slide, page ) => {
-				const li      = create( 'li', {} );
-				const button  = create( 'button', { class: classes.page } );
+				const li     = create( 'li', {} );
+				const button = create( 'button', { class: classes.page } );
 
-				li.appendChild( button );
-				list.appendChild( li );
+				append( li, button );
+				append( list, li );
 
-				subscribe( button, 'click', () => { Splide.go( `>${ page }` ) } );
+				Splide.on( 'click', () => { Splide.go( `>${ page }` ) }, button );
 
 				return { li, button, page, Slides: Slides.getSlidesByPage( page ) };
 			} );

+ 41 - 16
src/js/components/slides/index.js

@@ -25,22 +25,36 @@ export default ( Splide, Components ) => {
 	let slides = [];
 
 	/**
-	 * Store slide instances.
+	 * Store Slide objects.
 	 *
 	 * @type {Array}
 	 */
-	let Slides = [];
+	let SlideObjects = [];
 
-	return {
+	/**
+	 * Slides component object.
+	 *
+	 * @type {Object}
+	 */
+	const Slides = {
 		/**
 		 * Called when the component is mounted.
 		 */
 		mount() {
-			slides = Components.Elements.slides;
+			init();
 
-			for ( const i in slides ) {
-				this.register( parseInt( i ), -1, slides[ i ] );
-			}
+			Splide.on( 'refresh', () => {
+				this.destroy();
+				init();
+			} );
+		},
+
+		/**
+		 * Destroy.
+		 */
+		destroy() {
+			SlideObjects.forEach( Slide => { Slide.destroy() } );
+			SlideObjects = [];
 		},
 
 		/**
@@ -51,18 +65,19 @@ export default ( Splide, Components ) => {
 		 * @param {Element} slide     - A slide element.
 		 */
 		register( index, realIndex, slide ) {
-			const slideObject = Slide( index, realIndex, slide, Splide );
-			slideObject.init();
-			Slides.push( slideObject );
+			const SlideObject = Slide( index, realIndex, slide, Splide );
+			SlideObject.mount();
+			SlideObjects.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 SlideObjects.filter( Slide => Slide.index === index )[0];
 		},
 
 		/**
@@ -75,10 +90,10 @@ export default ( Splide, Components ) => {
 		 */
 		getSlides( includeClones, objects ) {
 			if ( objects ) {
-				return includeClones ? Slides : Slides.filter( Slide => ! Slide.isClone );
+				return includeClones ? SlideObjects : SlideObjects.filter( Slide => ! Slide.isClone );
 			}
 
-			return includeClones ? Slides.map( Slide => Slide.slide ) : slides;
+			return includeClones ? SlideObjects.map( Slide => Slide.slide ) : slides;
 		},
 
 		/**
@@ -93,7 +108,7 @@ export default ( Splide, Components ) => {
 			const options = Splide.options;
 			const max     = options.focus !== false ? 1 : options.perPage;
 
-			return Slides.filter( ( { index } ) => idx <= index && index < idx + max );
+			return SlideObjects.filter( ( { index } ) => idx <= index && index < idx + max );
 		},
 
 		/**
@@ -106,12 +121,22 @@ export default ( Splide, Components ) => {
 		},
 
 		/**
-		 * Return "Slides" length including clones.
+		 * Return "SlideObjects" length including clones.
 		 *
 		 * @return {number} - Slide length including clones.
 		 */
 		get total() {
-			return Slides.length;
+			return SlideObjects.length;
 		},
 	};
+
+	/**
+	 * Initialization.
+	 */
+	function init() {
+		slides = Components.Elements.slides;
+		slides.forEach( ( slide, index ) => { Slides.register( index, -1, slide ) } );
+	}
+
+	return Slides;
 }

+ 62 - 28
src/js/components/slides/slide.js

@@ -8,6 +8,8 @@
 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";
 
 
 /**
@@ -20,8 +22,23 @@ import { STATUS_CLASSES } from '../../constants/classes';
  *
  * @return {Object} - The sub component object.
  */
-export default function Slide( index, realIndex, slide, Splide ) {
-	return {
+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.
 		 *
@@ -60,41 +77,34 @@ export default function Slide( index, realIndex, slide, Splide ) {
 		/**
 		 * Called when the component is mounted.
 		 */
-		init() {
-			if ( ! slide.id && ! this.isClone ) {
+		mount() {
+			if ( ! this.isClone ) {
 				const number = index + 1;
 				slide.id = `${ Splide.root.id }-slide${ number < 10 ? '0' + number : number }`;
 			}
 
-			const events = 'mounted updated ' + ( Splide.options.updateOnMove ? 'move' : 'moved' );
+			Splide.on( statusUpdateEvents, () => this.update() );
 
-			Splide
-				.on( events, () => {
-					this.update( this.isActive(), false );
-					this.update( this.isVisible(), true );
-				} )
-				.on( 'resize', () => { this.update( this.isVisible(), true ) } );
+			// Update status immediately on refresh.
+			if ( ! Splide.State.is( CREATED ) ) {
+				this.update();
+			}
 		},
 
 		/**
-		 * Update classes for activity or visibility.
-		 *
-		 * @param {boolean} active        - Is active/visible or not.
-		 * @param {boolean} forVisibility - Toggle classes for activity or visibility.
+		 * Destroy.
 		 */
-		update( active, forVisibility ) {
-			const type      = forVisibility ? 'visible' : 'active';
-			const className = STATUS_CLASSES[ type ];
-
-			if ( active ) {
-				addClass( slide, className );
-				Splide.emit( `${ type }`, this );
-			} else {
-				if ( hasClass( slide, className ) ) {
-					removeClass( slide, className );
-					Splide.emit( `${ forVisibility ? 'hidden' : 'inactive' }`, this );
-				}
-			}
+		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 );
 		},
 
 		/**
@@ -150,4 +160,28 @@ export default function Slide( index, realIndex, slide, Splide ) {
 			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;
 }

+ 13 - 11
src/js/components/sync/index.js

@@ -5,7 +5,6 @@
  * @copyright Naotoshi Fujita. All rights reserved.
  */
 
-import { subscribe } from '../../utils/dom';
 import { LOOP } from '../../constants/types';
 import { IDLE } from "../../constants/states";
 
@@ -115,33 +114,36 @@ export default ( Splide ) => {
 	function bind() {
 		const Slides = sibling.Components.Slides.getSlides( true, true );
 
-		Slides.forEach( Slide => {
-			const slide = Slide.slide;
-
+		Slides.forEach( ( { slide, index } ) => {
 			/*
 			 * Listen mouseup and touchend events to handle click.
-			 * Need to check "IDLE" status because slides can be moving by Drag component.
 			 */
-			subscribe( slide, 'mouseup touchend', e => {
+			Splide.on( 'mouseup touchend', e => {
 				// Ignore a middle or right click.
 				if ( ! e.button || e.button === 0 ) {
-					moveSibling( Slide.index );
+					moveSibling( index );
 				}
-			} );
+			}, slide );
 
 			/*
 			 * Subscribe keyup to handle Enter and Space key.
 			 * Note that Array.includes is not supported by IE.
 			 */
-			subscribe( slide, 'keyup', e => {
+			Splide.on( 'keyup', e => {
 				if ( TRIGGER_KEYS.indexOf( e.key ) > -1 ) {
 					e.preventDefault();
-					moveSibling( Slide.index );
+					moveSibling( index );
 				}
-			}, { passive: false } );
+			}, slide, { passive: false } );
 		} );
 	}
 
+	/**
+	 * Move the sibling to the given index.
+	 * Need to check "IDLE" status because slides can be moving by Drag component.
+	 *
+	 * @param {number} index - Target index.
+	 */
 	function moveSibling( index ) {
 		if ( Splide.State.is( IDLE ) ) {
 			sibling.go( index );

+ 10 - 3
src/js/components/track/index.js

@@ -49,6 +49,13 @@ export default ( Splide, Components ) => {
 	 */
 	const isVertical = Splide.options.direction === TTB;
 
+	/**
+	 * Whether the slider type is FADE or not.
+	 *
+	 * @type {boolean}
+	 */
+	const isFade = Splide.is( FADE );
+
 	return {
 		/**
 		 * Called when the component is mounted.
@@ -63,7 +70,7 @@ export default ( Splide, Components ) => {
 		 * The resize event must be registered after the Layout's one is done.
 		 */
 		mounted() {
-			if ( ! Splide.is( FADE ) ) {
+			if ( ! isFade ) {
 				Splide.on( 'mounted resize updated', () => { this.jump( Splide.index ) } );
 			}
 		},
@@ -85,7 +92,7 @@ export default ( Splide, Components ) => {
 				Splide.emit( 'move', newIndex, prevIndex, destIndex );
 			}
 
-			if ( Math.abs( newPosition - currPosition ) >= 1 || Splide.is( FADE ) ) {
+			if ( Math.abs( newPosition - currPosition ) >= 1 || isFade ) {
 				Components.Transition.start( destIndex, newIndex, this.toCoord( newPosition ), () => {
 					this.end( destIndex, newIndex, prevIndex, silently );
 				} );
@@ -105,7 +112,7 @@ export default ( Splide, Components ) => {
 		end( destIndex, newIndex, prevIndex, silently ) {
 			applyStyle( list, { transition: '' } );
 
-			if ( ! Splide.is( FADE ) ) {
+			if ( ! isFade ) {
 				this.jump( newIndex );
 			}
 

+ 48 - 29
src/js/core/event.js

@@ -5,68 +5,87 @@
  * @copyright Naotoshi Fujita. All rights reserved.
  */
 
-import { each } from "../utils/object";
-
 
 /**
  * The function for providing an Event object simply managing events.
  */
 export default () => {
 	/**
-	 * Store all handlers.
+	 * Store all event data.
 	 *
-	 * @type {Object}
+	 * @type {Array}
 	 */
-	const handlers = {};
+	let data = [];
 
 	return {
 		/**
 		 * Subscribe the given event(s).
 		 *
-		 * @param {string}    event   - An event name. Use space to separate multiple events.
-		 *                              Also, namespace is accepted by dot, such as 'resize.{namespace}'.
-		 * @param {function}  handler - A callback function.
+		 * @param {string}   events  - An event name. Use space to separate multiple events.
+		 *                             Also, namespace is accepted by dot, such as 'resize.{namespace}'.
+		 * @param {function} handler - A callback function.
+		 * @param {Element}  elm     - Optional. Native event will be listened to when this arg is provided.
+		 * @param {Object}   options - Optional. Options for addEventListener.
 		 */
-		on( event, handler ) {
-			event.split( ' ' ).forEach( name => {
-				// Prevent an event with a namespace from being registered twice.
-				if ( name.indexOf( '.' ) > -1 && handlers[ name ] ) {
-					return;
-				}
-
-				if ( ! handlers[ name ] ) {
-					handlers[ name ] = [];
+		on( events, handler, elm = null, options = {} ) {
+			events.split( ' ' ).forEach( event => {
+				if ( elm ) {
+					elm.addEventListener( event, handler, options );
 				}
 
-				handlers[ name ].push( handler );
+				data.push( { event, handler, elm, options } );
 			} );
 		},
 
 		/**
-		 * Unsubscribe the given event.
+		 * Unsubscribe the given event(s).
 		 *
-		 * @param {string} event - A event name.
+		 * @param {string}  events - A event name or names split by space.
+		 * @param {Element} elm    - Optional. removeEventListener() will be called when this arg is provided.
 		 */
-		off( event ) {
-			event.split( ' ' ).forEach( name => delete handlers[ name ] );
+		off( events, elm = null ) {
+			events.split( ' ' ).forEach( event => {
+				for ( let i in data ) {
+					const item = data[ i ];
+
+					if ( item && item.event === event && item.elm === elm ) {
+						if ( elm ) {
+							elm.removeEventListener( event, item.handler, item.options );
+						}
+
+						delete data[ i ];
+						break;
+					}
+				}
+			} );
 		},
 
 		/**
 		 * Emit an event.
+		 * This method is only for custom events.
 		 *
 		 * @param {string}  event - An event name.
 		 * @param {*}       args  - Any number of arguments passed to handlers.
 		 */
 		emit( event, ...args ) {
-			each( handlers, ( callbacks, name ) => {
-				if ( name.split( '.' )[ 0 ] === event ) {
-					if ( callbacks ) {
-						for ( const i in callbacks ) {
-							callbacks[ i ]( ...args );
-						}
-					}
+			data.forEach( item => {
+				if ( ! item.elm && item.event.split( '.' )[0] === event ) {
+					item.handler( ...args );
 				}
 			} );
 		},
+
+		/**
+		 * Clear event data.
+		 */
+		destroy() {
+			data.forEach( item => {
+				if ( item.elm ) {
+					item.elm.removeEventListener( item.event, item.handler, item.options );
+				}
+			} );
+
+			data = [];
+		},
 	};
 }

+ 78 - 27
src/js/splide.js

@@ -13,7 +13,7 @@ import compose from './core/composer';
 import { applyStyle } from './utils/dom';
 import { error, exist } from './utils/error';
 import { find } from './utils/dom';
-import { merge, each } from './utils/object';
+import { merge, each, values } from './utils/object';
 import * as STATES from './constants/states';
 
 
@@ -32,7 +32,7 @@ export default class Splide {
 	 * @param {Object}          Components  - Optional. Components.
 	 */
 	constructor( root, options = {}, Components = {} ) {
-		this.root = root instanceof HTMLElement ? root : find( document, root );
+		this.root = root instanceof Element ? root : find( document, root );
 		exist( this.root, 'An invalid root element or selector was given.' );
 
 		this.Components = {};
@@ -40,9 +40,9 @@ export default class Splide {
 		this.State      = State( STATES.CREATED );
 		this.STATES     = STATES;
 
-		this._options    = merge( DEFAULTS, options );
-		this._index      = 0;
-		this._components = Components;
+		this._o = merge( DEFAULTS, options );
+		this._i = 0;
+		this._c = Components;
 
 		this
 			.on( 'move drag', () => this.State.set( STATES.MOVING ) )
@@ -58,7 +58,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._components, Extensions ), Transition );
+		this.Components = compose( this, merge( this._c, Extensions ), Transition );
 
 		try {
 			each( this.Components, ( component, key ) => {
@@ -83,10 +83,10 @@ export default class Splide {
 		this.emit( 'mounted' );
 
 		this.State.set( STATES.IDLE );
-		this.emit( 'ready' );
-
 		applyStyle( this.root, { visibility: 'visible' } );
 
+		this.emit( 'ready' );
+
 		return this;
 	}
 
@@ -105,34 +105,37 @@ export default class Splide {
 	/**
 	 * Register callback fired on the given event(s).
 	 *
-	 * @param {string}    event   - An event name. Use space to separate multiple events.
-	 *                              Also, namespace is accepted by dot, such as 'resize.{namespace}'.
-	 * @param {function}  handler - A callback function.
+	 * @param {string}   events  - An event name. Use space to separate multiple events.
+	 *                             Also, namespace is accepted by dot, such as 'resize.{namespace}'.
+	 * @param {function} handler - A callback function.
+	 * @param {Element}  elm     - Optional. Native event will be listened to when this arg is provided.
+	 * @param {Object}   options - Optional. Options for addEventListener.
 	 *
 	 * @return {Splide} - This instance.
 	 */
-	on( event, handler ) {
-		this.Event.on( event, handler );
+	on( events, handler, elm = null, options = {} ) {
+		this.Event.on( events, handler, elm, options );
 		return this;
 	}
 
 	/**
 	 * Unsubscribe the given event.
 	 *
-	 * @param {string} event - A event name.
+	 * @param {string}  events - A event name.
+	 * @param {Element} elm    - Optional. removeEventListener() will be called when this arg is provided.
 	 *
 	 * @return {Splide} - This instance.
 	 */
-	off( event ) {
-		this.Event.off( event );
+	off( events, elm = null ) {
+		this.Event.off( events, elm );
 		return this;
 	}
 
 	/**
 	 * Emit an event.
 	 *
-	 * @param {string}  event - An event name.
-	 * @param {*}       args  - Any number of arguments passed to handlers.
+	 * @param {string} event - An event name.
+	 * @param {*}      args  - Any number of arguments passed to handlers.
 	 */
 	emit( event, ...args ) {
 		this.Event.emit( event, ...args );
@@ -159,16 +162,64 @@ export default class Splide {
 	 * @return {boolean} - True if the slider type is the provided type or false if not.
 	 */
 	is( type ) {
-		return type === this._options.type;
+		return type === this._o.type;
+	}
+
+	/**
+	 * Insert a slide.
+	 *
+	 * @param {Element|string} slide - A slide element to be added.
+	 * @param {number}         index - A slide will be added at the position.
+	 */
+	add( slide, index = -1 ) {
+		this.Components.Elements.add( slide, index );
+		this.refresh();
+	}
+
+	/**
+	 * Remove the slide designated by the index.
+	 *
+	 * @param {number} index - A slide index.
+	 */
+	remove( index ) {
+		this.Components.Elements.remove( index );
+		this.refresh();
+	}
+
+	/**
+	 * Destroy all Slide objects and clones and recreate them again.
+	 * And then call "updated" event.
+	 */
+	refresh() {
+		this.emit( 'refresh' ).emit( 'updated', this.options );
+	}
+
+	/**
+	 * Destroy the Splide.
+	 */
+	destroy() {
+		values( this.Components ).reverse().forEach( component => {
+			component.destroy && component.destroy();
+		} );
+
+		this.emit( 'destroy' );
+
+		// Destroy all event handlers, including ones for native events.
+		this.Event.destroy();
+
+		delete this.Components;
+		this.State.set( STATES.CREATED );
+
+		return this;
 	}
 
 	/**
 	 * Return the current slide index.
 	 *
 	 * @return {number} - The current slide index.
-	 */
+	 // */
 	get index() {
-		return this._index;
+		return this._i;
 	}
 
 	/**
@@ -177,7 +228,7 @@ export default class Splide {
 	 * @param {number|string} index - A new index.
 	 */
 	set index( index ) {
-		this._index = parseInt( index );
+		this._i = parseInt( index );
 	}
 
 	/**
@@ -196,7 +247,7 @@ export default class Splide {
 	 * @return {Object} - Options object.
 	 */
 	get options() {
-		return this._options;
+		return this._o;
 	}
 
 	/**
@@ -205,10 +256,10 @@ export default class Splide {
 	 * @param {Object} options - New options.
 	 */
 	set options( options ) {
-		this._options = merge( this._options, options );
+		this._o = merge( this._o, options );
 
 		if ( ! this.State.is( STATES.CREATED ) ) {
-			this.emit( 'updated', this._options );
+			this.emit( 'updated', this._o );
 		}
 	}
 
@@ -219,7 +270,7 @@ export default class Splide {
 	 * @return {Object} - An object containing all class list.
 	 */
 	get classes() {
-		return this._options.classes;
+		return this._o.classes;
 	}
 
 	/**
@@ -229,6 +280,6 @@ export default class Splide {
 	 * @return {Object} - An object containing all i18n strings.
 	 */
 	get i18n() {
-		return this._options.i18n;
+		return this._o.i18n;
 	}
 }

+ 3 - 3
src/js/transitions/slide/index.js

@@ -5,7 +5,7 @@
  * @copyright Naotoshi Fujita. All rights reserved.
  */
 
-import { subscribe, applyStyle } from '../../utils/dom';
+import { applyStyle } from '../../utils/dom';
 
 
 /**
@@ -38,11 +38,11 @@ export default ( Splide, Components ) => {
 		mount() {
 			list = Components.Elements.list;
 
-			subscribe( list, 'transitionend', e => {
+			Splide.on( 'transitionend', e => {
 				if ( e.target === list && endCallback ) {
 					endCallback();
 				}
-			} );
+			}, list );
 		},
 
 		/**

+ 80 - 52
src/js/utils/dom.js

@@ -6,6 +6,8 @@
  */
 
 import { each, values } from './object';
+import { toArray } from "./utils";
+
 
 /**
  * Find the first element matching the given selector.
@@ -17,7 +19,7 @@ import { each, values } from './object';
  * @return {Element|null} - A found element or null.
  */
 export function find( elm, selector ) {
-	return elm && selector ? elm.querySelector( selector.split( ' ' )[ 0 ] ) : null;
+	return elm && selector ? elm.querySelector( selector.split( ' ' )[0] ) : null;
 }
 
 /**
@@ -30,15 +32,9 @@ export function find( elm, selector ) {
  */
 export function child( parent, className ) {
 	if ( parent ) {
-		const children = values( parent.children );
-
-		for ( let i in children ) {
-			const child = children[ i ];
-
-			if ( hasClass( child, className.split( ' ' )[ 0 ] ) ) {
-				return child;
-			}
-		}
+		return values( parent.children ).filter( child => {
+			return hasClass( child, className.split( ' ' )[0] );
+		} )[0] || null;
 	}
 
 	return null;
@@ -59,6 +55,53 @@ export function create( tag, attrs ) {
 	return elm;
 }
 
+/**
+ * Convert HTML string to DOM node.
+ *
+ * @param {string} html - HTML string.
+ *
+ * @return {Node} - A created node.
+ */
+export function domify( html ) {
+	const div = create( 'div', {} );
+	div.innerHTML = html;
+
+	return div.firstChild;
+}
+
+/**
+ * Remove a given element from a DOM tree.
+ *
+ * @param {Element|Element[]} elms - Element(s) to be removed.
+ */
+export function remove( elms ) {
+	toArray( elms ).forEach( elm => { elm && elm.parentElement.removeChild( elm ) } );
+}
+
+/**
+ * Append a child to a given element.
+ *
+ * @param {Element} parent - A parent element.
+ * @param {Element} child  - An element to be appended.
+ */
+export function append( parent, child ) {
+	if ( parent ) {
+		parent.appendChild( child );
+	}
+}
+
+/**
+ * Insert an element before the reference element.
+ *
+ * @param {Element|Node} ref - A reference element.
+ * @param {Element}      elm - An element to be inserted.
+ */
+export function before( elm, ref ) {
+	if ( elm && ref && ref.parentElement ) {
+		ref.parentElement.insertBefore( elm, ref );
+	}
+}
+
 /**
  * Apply styles to the given element.
  *
@@ -74,31 +117,41 @@ export function applyStyle( elm, styles ) {
 }
 
 /**
- * Add classes to the element.
+ * Add or remove classes to/from the element.
+ * This function is for internal usage.
  *
- * @param {Element} elm     - An element where classes are added.
- * @param {string}  classes - Class names being added.
+ * @param {Element}         elm     - An element where classes are added.
+ * @param {string|string[]} classes - Class names being added.
+ * @param {boolean}         remove  - Whether to remove or add classes.
  */
-export function addClass( elm, ...classes ) {
+function addOrRemoveClasses( elm, classes, remove ) {
 	if ( elm ) {
-		classes.forEach( name => {
+		toArray( classes ).forEach( name => {
 			if ( name ) {
-				elm.classList.add( name )
+				elm.classList[ remove ? 'remove' : 'add' ]( name );
 			}
 		} );
 	}
 }
 
+/**
+ * Add classes to the element.
+ *
+ * @param {Element}          elm     - An element where classes are added.
+ * @param {string|string[]}  classes - Class names being added.
+ */
+export function addClass( elm, classes ) {
+	addOrRemoveClasses( elm, classes, false );
+}
+
 /**
  * Remove a class from the element.
  *
- * @param {Element} elm       - An element where classes are removed.
- * @param {string}  className - A class name being removed.
+ * @param {Element}         elm     - An element where classes are removed.
+ * @param {string|string[]} classes - A class name being removed.
  */
-export function removeClass( elm, className ) {
-	if ( elm ) {
-		elm.classList.remove( className )
-	}
+export function removeClass( elm, classes ) {
+	addOrRemoveClasses( elm, classes, true );
 }
 
 /**
@@ -135,42 +188,17 @@ export function setAttribute( elm, name, value ) {
  * @return {string|null} - The value of the given attribute if available. Null if not.
  */
 export function getAttribute( elm, name ) {
-	if ( elm ) {
-		return elm.getAttribute( name );
-	}
-
-	return null;
+	return elm ? elm.getAttribute( name ) : null;
 }
 
 /**
  * Remove attribute from the given element.
  *
- * @param {Element} elm   - An element where an attribute is removed.
- * @param {string}  name  - Attribute name.
+ * @param {Element}      elm   - An element where an attribute is removed.
+ * @param {string|Array} names - Attribute name.
  */
-export function removeAttribute( elm, name ) {
+export function removeAttribute( elm, names ) {
 	if ( elm ) {
-		elm.removeAttribute( name );
+		toArray( names ).forEach( name => { elm.removeAttribute( name ) } );
 	}
-}
-
-/**
- * Listen a native event.
- *
- * @param {Element|Window}  elm     - An element or window object.
- * @param {string}          event   - An event name or event names separated with space.
- * @param {function}        handler - Callback function.
- * @param {Object}          options - Optional. Options.
- *
- * @return {function[]} - Functions to stop subscription.
- */
-export function subscribe( elm, event, handler, options = {} ) {
-	if ( elm ) {
-		return event.split( ' ' ).map( e => {
-			elm.addEventListener( e, handler, options );
-			return () => elm.removeEventListener( e, handler );
-		} );
-	}
-
-	return [];
 }

+ 0 - 4
src/js/utils/error.js

@@ -29,13 +29,9 @@ export function error( message ) {
  *
  * @param {*}      subject - A subject to be confirmed.
  * @param {string} message - An error message.
- *
- * @return {*} - A given subject itself.
  */
 export function exist( subject, message ) {
 	if ( ! subject ) {
 		throw new Error( message );
 	}
-
-	return subject;
 }

+ 19 - 9
src/js/utils/utils.js

@@ -5,8 +5,18 @@
  * @copyright Naotoshi Fujita. All rights reserved.
  */
 
-import { create, applyStyle } from "./dom";
+import { create, append, remove, applyStyle } from "./dom";
 
+/**
+ * Convert the given value to array.
+ *
+ * @param {*} value - Any value.
+ *
+ * @return {*[]} - Array containing the given value.
+ */
+export function toArray( value ) {
+	return Array.isArray( value ) ? value : [ value ];
+}
 
 /**
  * Check if the given value is between min and max.
@@ -25,14 +35,14 @@ export function between( value, m1, m2 ) {
 /**
  * The sprintf method with minimum functionality.
  *
- * @param {string} format       - The string format.
- * @param {string} replacements - Replacements accepting multiple arguments.
+ * @param {string}       format       - The string format.
+ * @param {string|Array} replacements - Replacements accepting multiple arguments.
  *
  * @returns {string} - Converted string.
  */
-export function sprintf( format, ...replacements ) {
+export function sprintf( format, replacements ) {
 	let i = 0;
-	return format.replace( /%s/g, () => replacements[ i++ ] );
+	return format.replace( /%s/g, () => toArray( replacements )[ i++ ] );
 }
 
 /**
@@ -75,11 +85,11 @@ export function toPixel( root, value ) {
 		width: value,
 	} );
 
-	root.appendChild( div );
+	append( root, div );
 
-	const px = div.clientWidth;
+	value = div.clientWidth;
 
-	root.removeChild( div );
+	remove( div );
 
-	return px;
+	return value;
 }

+ 27 - 0
tests/core/splide.test.js

@@ -41,4 +41,31 @@ describe( 'Splide ', () => {
 		splide.mount();
 		expect( splide.root.style.visibility ).toBe( 'visible' );
 	} );
+
+	test( 'should add a slide dynamically.', () => {
+		const splide = new Splide( '#splide', {}, COMPLETE ).mount();
+		const slide = document.createElement( 'li' );
+		const length = splide.length;
+
+		slide.classList.add( 'splide__slide' );
+		slide.textContent = `${ length }`;
+
+		splide.add( slide );
+
+		expect( splide.length ).toBe( length + 1 );
+
+		splide.go( splide.length - 1 );
+
+		expect( slide.classList.contains( 'is-active' ) ).toBeTruthy();
+	} );
+
+	test( 'should remove a slide dynamically.', () => {
+		const splide = new Splide( '#splide', {}, COMPLETE ).mount();
+		const length = splide.length;
+
+		splide.remove( 0 );
+
+		expect( splide.length ).toBe( length - 1 );
+		expect( splide.Components.Elements.slides[0].textContent ).toBe( '2' );
+	} );
 } );

+ 6 - 2
tests/functionality/controller.test.js

@@ -39,8 +39,12 @@ describe( 'The Controller', () => {
 		expect( Controller.edgeIndex ).toBe( slides.length - perPage );
 		expect( Controller.trim( 100 ) ).toBe( 0 );
 		expect( Controller.trim( -100 ) ).toBe( Controller.edgeIndex );
+	} );
+
+	test( 'should trim index properly in LOOP mode.', () => {
+		const splide     = new Splide( '#splide', { perPage: 3, type: 'loop' }, COMPLETE ).mount();
+		const Controller = splide.Components.Controller;
 
-		splide.options = { type: 'loop' };
-		expect( Controller.edgeIndex ).toBe( slides.length - 1 );
+		expect( Controller.edgeIndex ).toBe( splide.length - 1 );
 	} );
 } );

+ 27 - 28
tests/utils/dom.test.js

@@ -2,13 +2,14 @@ import {
 	find,
 	child,
 	create,
+	remove,
+	domify,
 	applyStyle,
 	addClass,
 	removeClass,
 	hasClass,
 	setAttribute,
 	removeAttribute,
-	subscribe,
 } from '../../src/js/utils/dom';
 
 describe( 'DOM function ', () => {
@@ -38,6 +39,25 @@ describe( 'DOM function ', () => {
 		expect( elm.classList.contains( 'btn' ) ).toBe( true );
 	} );
 
+	test( '"remove" should remove the given element.', () => {
+		const root  = document.querySelector( '.root' );
+		const slide = child( root, 'slide' );
+
+		expect( root.children.length ).toBe( 3 );
+		remove( slide );
+		expect( root.children.length ).toBe( 2 );
+	} );
+
+	test( '"domify" should convert HTML string to elements.', () => {
+		const root  = document.querySelector( '.root' );
+		const li    = domify( '<li class="child">Forth</li>' );
+
+		root.appendChild( li );
+
+		const items = root.getElementsByTagName( 'li' );
+		expect( items[ items.length - 1 ].textContent ).toBe( 'Forth' );
+	} );
+
 	test( '"applyStyle" should apply a style or styles to an element.', () => {
 		const root = document.querySelector( '.root' );
 		applyStyle( root, { width: '200px', height: '100px' } );
@@ -47,7 +67,7 @@ describe( 'DOM function ', () => {
 
 	test( '"addClass" should add a class or classes to an element.', () => {
 		const root = document.querySelector( '.root' );
-		addClass( root, 'class1', 'class2' );
+		addClass( root, [ 'class1', 'class2' ] );
 		expect( root.classList.contains( 'class1' ) ).toBe( true );
 		expect( root.classList.contains( 'class2' ) ).toBe( true );
 	} );
@@ -75,31 +95,10 @@ describe( 'DOM function ', () => {
 
 	test( '"removeAttribute" should remove an attribute from an element.', () => {
 		const root = document.querySelector( '.root' );
-		root.dataset.root = 'a';
-		removeAttribute( root, 'data-root' );
-		expect( root.dataset.root ).toBeUndefined();
-	} );
-
-	describe( '"subscribe" should', () => {
-		test( 'listen multiple native events.', () => {
-			const callback = jest.fn();
-			subscribe( window, 'resize click', callback );
-
-			global.dispatchEvent( new Event( 'resize' ) );
-			global.dispatchEvent( new Event( 'click' ) );
-
-			expect( callback ).toHaveBeenCalledTimes( 2 );
-		} );
-
-		test( 'return an array containing functions to remove listeners.', () => {
-			const callback = jest.fn();
-			const removers = subscribe( window, 'resize click', callback );
-
-			expect( removers ).toHaveLength( 2 );
-
-			removers.forEach( remover => remover() );
-
-			expect( callback ).not.toHaveBeenCalled();
-		} );
+		root.dataset.a = 'a';
+		root.dataset.b = 'b';
+		removeAttribute( root, [ 'data-a', 'data-b' ] );
+		expect( root.dataset.a ).toBeUndefined();
+		expect( root.dataset.b ).toBeUndefined();
 	} );
 } );

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff