BEMQuery.js

'use strict';

function checkConverter( converter ) {
	return typeof converter === 'object' && typeof converter.convert === 'function';
}

function checkSelectorEngine( selectorEngine ) {
	return typeof selectorEngine === 'object' && typeof selectorEngine.find === 'function';
}

function determineContext( context ) {
	if ( context instanceof BEMQuery ) { // eslint-disable-line no-use-before-define
		context = context.elements[ 0 ];
	}

	if ( !( context instanceof HTMLElement ) && context !== document ) {
		context = document;
	}

	return context;
}

function fetchElements( query, context, converter, selectorEngine ) {
	if ( !query ) {
		throw new TypeError( 'Selector must be set.' );
	}

	if ( typeof query === 'string' ) {
		query = converter.convert( query ).CSS;
		return selectorEngine.find( query, context );
	} else if ( query instanceof HTMLElement ) {
		return [
			query
		];
	} else if ( query instanceof BEMQuery ) { // eslint-disable-line no-use-before-define
		return query.elements;
	} else if ( typeof query === 'object' ) {
		return Array.from( query );
	} else {
		throw new TypeError( 'Selector must be a string, object, array or DOM element.' );
	}
}

function defineProperties( obj, elements ) {
	Object.defineProperty( obj, 'elements', {
		value: elements
	} );

	obj.elements.forEach( ( element, index ) => {
		Object.defineProperty( obj, index, {
			enumerable: true,
			get() {
				return new BEMQuery( this.elements[ index ], document, this.converter, this.selectorEngine ); // eslint-disable-line no-use-before-define
			}
		} );
	}, obj );

	Object.defineProperty( obj, 'length', {
		enumerable: true,
		get() {
			return this.elements.length;
		}
	} );
}

/** Class representing elements collection. */
class BEMQuery {
	/**
	 * Creates elements collection.
	 *
	 * @param {String|Iterable|HTMLElement} query Selector or
	 * existing elements collection upon which the new elements collection
	 * should be created.
	 * @param {Document|HTMLElement|BEMQuery} context Context from which
	 * elements should be fetched.
	 * @param {Converter} converter BEM selector converter to be used.
	 * @param {SelectorEngine} selectorEngine CSS selector engine to be used
	 * by the current and descendant `BEMQuery` instances.
	 * @class
	 */
	constructor( query, context, converter, selectorEngine ) {
		if ( !checkConverter( converter ) ) {
			throw new TypeError( 'Converter must be an object with convert method defined.' );
		}

		if ( !checkSelectorEngine( selectorEngine ) ) {
			throw new TypeError( 'SelectorEngine must be an object with find method defined.' );
		}

		this.converter = converter;
		this.selectorEngine = selectorEngine;

		context = determineContext( context );

		defineProperties( this, fetchElements( query, context, converter, selectorEngine ) );
	}

	/**
	 * Gets element with given index.
	 *
	 * @param {Number} index Element's index.
	 * @return {BEMQuery} New BEMQuery instance with fetched element
	 * as an only element in the collection.
	 */
	get( index ) {
		index = Number( index );

		if ( Number.isNaN( index ) ) {
			throw new TypeError( 'Index must be a correct Number.' );
		} else if ( index < 0 ) {
			throw new RangeError( 'Index must be greater or equal to 0.' );
		} else if ( index > ( this.elements.length - 1 ) ) {
			throw new RangeError( 'Index cannot be greater than collection\'s length.' );
		}

		return new BEMQuery( this.elements[ index ], document, this.converter, this.selectorEngine );
	}

	/**
	 * Executes callback on every element in the collection.
	 *
	 * @param {Function} callback Callback to be executed.
	 * @return {BEMQuery} Current `BEMQuery` instance.
	 */
	each( callback ) {
		if ( typeof callback !== 'function' ) {
			throw new TypeError( 'Callback must be a function.' );
		}

		const converter = this.converter;
		const selectorEngine = this.selectorEngine;

		this.elements.forEach( ( element ) => {
			callback( new BEMQuery( element, document, converter, selectorEngine ) );
		} );

		return this;
	}

	/**
	 * Returns iterator for contained elements.
	 *
	 * @return {Iterator} Returned iterator.
	 */
	[ Symbol.iterator ]() {
		let i = 0;
		const elements = this.elements;
		const converter = this.converter;
		const selectorEngine = this.selectorEngine;

		return {
			next() {
				if ( i < elements.length ) {
					const element = elements[ i++ ];

					return {
						value: new BEMQuery( [ element ], document, converter, selectorEngine ),
						done: false
					};
				}

				return {
					done: true
				};
			}
		};
	}
}

export default BEMQuery;