K345 classNames source

source code
/**
* @requires Function.prototype.bind()
* @requires K345.isHostMethod()
* @requires K345.isNodeElement()
* @requires K345.toArray()
* @requires K345.isObject()
* @requires Object.forEach()
*/
(function (GNS /* the global namespace */) {
	var hasCList, funcs, pubApi;

	/**
	* tests if classList API is available
	* @private
	* @type Boolean
	*/
	hasCList = (function () {
		var te = document.createElement('p'); /* test element*/

		return ('object|function'.indexOf(typeof te.classList) > -1 &&
			K345.isHostMethod(te.classList, 'contains') &&
			K345.isHostMethod(te.classList, 'add') &&
			K345.isHostMethod(te.classList, 'remove') &&
			K345.isHostMethod(te.classList, 'toggle'));
	})();

	/* remove leading and trailing whitespace */
	function trim(s) {
		if (typeof s === 'string') {
			s = s.replace(/(^\s+|\s+$)/g, '');
		}
		return s;
	}

	/**
	* @namespace internal functions
	* @private
	*/
	funcs = {
		/**/
		reg: undefined,

		/**#@+
		* @private
		* @function
		* @inner
		* @memberOf funcs
		*/

		/**
		* $$DESCRIPTION$$
		*
		* @param {Object|String} addto
		*     $$DESCRIPTION$$
		* @throws TypeError on illegal parameter type
		*/
		setnamespace: function (addto) {
			var pre;

			if (K345.isObject(addto) | addto === GNS) {
				pre = '';
			}
			else if (typeof addto === 'string' && (/^\S+$/).test(addto)) {
				pre = addto;
				addto = GNS;
			}
			else {
				throw new TypeError(
					'classAPI namespace must be either an object or a string'
				);
			}
			Object.forEach(pubApi, function (item) {
				addto[pre + item] = pubApi[item];
			});
		},

		/**
		* Checks if an element's className property contains cl_name
		*
		* @name has
		* @param {Node} el
		*     element to check
		* @param {String} cl_name
		*     class name to check for in el.className
		* @returns {Boolean}
		*     true if element.className contains cl_name
		*/
		has: (hasCList)
			? function (el, cl_name) {
				return el.classList.contains(cl_name);
			}
			: function (el, cl_name) {
				if ((/[\s\x00-\x1F\x7F\xA0]/).test(cl_name)) {
					throw new Error('INVALID_CHARACTER_ERR');
				}
				funcs.reg = new RegExp('(^|\\s)' + cl_name + '(\\s|$)', 'g');
				return funcs.reg.test(el.className);
			},

		/**
		* Adds cl_name to element's className property
		*
		* @name add
		* @param {Node} el
		*     element to add class name to
		* @param {String} cl_name
		*     class name to add to el.className
		*/
		add: (hasCList)
			? function (el, cl_name) {
				el.classList.add(cl_name);
			}
			: function (el, cl_name) {
				if (!funcs.has(el, cl_name)) {
					el.className += ' ' + cl_name;
				}
			},

		/**
		* removes cl_name from element's className property
		*
		* @name remove
		* @param {Node} el
		*     element to remove class name from
		* @param {String} cl_name
		*     class name to remove from el.className
		*/
		remove: (hasCList)
			? function (el, cl_name) {
				el.classList.remove(cl_name);
			}
			: function (el, cl_name) {
				if (funcs.has(el, cl_name)) {
					el.className = el.className.replace(funcs.reg, ' ');
				}
			},

		/**
		* toggles cl_name on element's className property
		*
		* @name toggle
		* @param {Node} el
		*     element to toggle class name on
		* @param {String} cl_name
		*     class name to switch
		*/
		toggle: (hasCList)
			? function (el, cl_name) {
				el.classList.toggle(cl_name);
			}
			: function (el, cl_name) {
				funcs.add_rem_if(el, cl_name, !funcs.has(el, cl_name));
			},

		/**
		* adds »class_name« to class name of »elRef« if »condition« is truthy
		* otherwise »class_name« will be removed
		*
		* @name add_rem_if
		* @param {Node} el
		*     element to toggle class name on
		* @param {String} cl_name
		*     class name to switch
		* @param {Mixed} condition
		*     class name is altered only if condition is truthy
		*/
		add_rem_if: function (el, cl_name, cond) {
			var fname = (cond) ? 'add' : 'remove';

			return funcs.multi.call(funcs[fname], el, cl_name);
		},

		/**
		* check if a class name is a valid non empty string not containing any
		* whitespace characters
		*
		* @name isValid
		* @param {String} cl_name
		*     class name to test
		* @returns {Boolean}
		*     true, if cl_name is valid
		*/
		isValid: function (cl_name) {
			if (typeof cl_name === 'string' && cl_name.length > 0 && !((/\s/).test(cl_name))) {
				return true;
			}
			throw new Error('class names must be non empty strings not containing' +
				' any whitespace.\nparameter was: "' + cl_name + '"');
		},

		/**
		* handle variable number of class names
		*
		* @name multi
		* @param {Node} el
		*     element to apply multiple classname actions to
		* @param {String} cl_name
		*     variable number of class name strings
		* @param {String...} [cl_name]
		*     variable number of class name strings
		* @this {Function}
		*     function reference to apply
		*/
		multi: function (el) {
			var l = arguments.length,
				clname, i;

			for (i = 1; i < l; i++) {
				clname = arguments[i];
				if (funcs.isValid(clname)) {
					this(el, clname);
				}
			}

			/* if remaining className is empty or contains only whitespace,
				remove attribute className */
			if ((/^\s*$/).test(el.className)) {
				try {
					el.removeAttribute('class');
					delete el.className;
				}
				catch (ex) {
					//'NOP'; /* NOP, this branch prevents stop on exception */
				}
			}
		}
	};
	/**#@- */

	/**
	* generic handler for api method calls
	* @private
	*
	* @this {Object}
	*     internal data object, contains name of func to call,
	*     func api name, number of args…
	* @param {Node} el
	*     element to apply class name changes to
	* @param {String} cname...
	*     class name(s) to work with
	* @returns {Mixed}
	*     return value of called function
	*/
	function handler(el, cname) {
		var args = K345.toArray(arguments),
			fname = this.func,
			rv, cond, o, oldname;

		/* check number of passed arguments */
		if (isNaN(this.minArgs)) {
			this.minArgs = 2;
		}
		if (args.length < this.minArgs) {
			throw new Error(this.name + ': method expects at least ' + this.minArgs +
				' arguments; only ' + args.length + ' were passed');
		}

		if (!K345.isNodeElement(el)) {
			if (Array.isArray(el)) {
				o = this;
				return el.map(function (elem) {
					args[0] = elem; /* replace element reference */
					return handler.apply(o, args);
				});
			}
			throw new Error(this.name + ': first argument is not an element' +
				' reference: ' + el);
		}

		oldname = (el.className) ? trim(el.className) : '';
		rv = oldname;

		switch (this.name) {
		case 'getClass':
			rv = oldname.split(/\s+/);
			break;
		case 'setClass':
			el.className = '';
			/*no break statement here*/		case 'addClass':
		case 'removeClass':
		case 'toggleClass':
			funcs.multi.apply(funcs[fname], args);
			break;
		case 'addClassIf':
		case 'removeClassIf':
			cond = args.pop();
			if (cond) {
				funcs.multi.apply(funcs[fname], args);
			}
			break;
		case 'replaceClass':
			if (funcs.isValid(cname) &&
				funcs.isValid(args[2]) &&
				(args[3] !== true || funcs.has(el, cname))
			) {
				funcs.add(el, args[2]);
				funcs.remove(el, cname);
			}
			break;
		default:
			if (fname in funcs && funcs.isValid(cname)) {
				rv = funcs[fname].apply(null, args);
			}
			break;
		}
		return rv;
	}

	/*
	*  P U B L I C  A P I
	* ====================
	*/
	pubApi = {
		/**#@+
		* @public
		* @function
		* @memberOf K345.DOM
		*/

		/**
		* check existance of »class_name«
		*
		* @name	hasClass
		* @param {Node} elRef
		*     reference of element to test
		* @param {String} class_name
		* @returns {Boolean}
		*     »true« if the class name of »elRef« contains »class_name«
		*/
		hasClass: handler.bind({
			func: 'has',
			name: 'hasClass'
		}),

		/**
		* adds all arguments »class_name« to the class name of »elRef« if not
		* already present.
		*
		* @name	addClass
		* @param {Node} elRef
		*     reference of element to alter
		* @param {String} class_name
		* @param {String...} [class_name]
		*     additional class names (optional)
		* @returns {String}
		*     previous value of »elRef.className«
		*/
		addClass: handler.bind({
			func: 'add',
			name: 'addClass'
		}),

		/**
		* Overwrites existing class name of »elRef« with all arguments »class_name«
		*
		* @name	setClass
		* @param {Node} elRef
		*     reference of element to alter
		* @param {String} class_name
		* @param {String...} [class_name]
		*     additional class names (optional)
		* @returns {String}
		*     previous value of »elRef.className«
		*/
		setClass: handler.bind({
			func: 'add',
			name: 'setClass'
		}),

		/**
		* gets class name(s) of »elRef« as Array
		*
		* @name	getClass
		* @param {Node} elRef
		*     reference of element to alter
		* @returns {Array}
		*     Array containing value(s) of »elRef.className«
		*/
		getClass: handler.bind({
			name: 'getClass',
			minArgs: 1
		}),

		/**
		* adds »class_name« to the class name of »elRef« if condition is truthy
		*
		* @name	addClassIf
		* @param {Node} elRef
		*     reference of element to alter
		* @param {String} class_name
		* @param {String...} [class_name]
		*     additional class names (optional)
		* @param {Mixed} condition
		*     class name is altered only if condition is truthy
		*     (true, 1, non-empty string etc...)
		* @returns {String}
		*     previous value of »elRef.className«
		*/
		addClassIf: handler.bind({
			func: 'add',
			name: 'addClassIf',
			minArgs: 3
		}),

		/**
		* removes all arguments »class_name« from the class name of »elRef«
		*
		* @name	removeClass
		* @param {Node} elRef
		*     reference of element to alter
		* @param {String} class_name
		* @param {String...} [class_name]
		*     additional class names (optional)
		* @returns {String}
		*     previous value of »elRef.className«
		*/
		removeClass: handler.bind({
			func: 'remove',
			name: 'removeClass'
		}),

		/**
		* removes »class_name« from the class name of »elRef« if condition is truthy
		*
		* @name	removeClassIf
		* @param {Node} elRef
		*     reference of element to alter
		* @param {String} class_name
		* @param {String...} [class_name]
		*     additional class names (optional)
		* @param {Mixed} condition
		*     class name is altered only if condition is truthy
		*     (true, 1, non-empty string etc...)
		* @returns {String}
		*     previous value of »elRef.className«
		*/
		removeClassIf: handler.bind({
			func: 'remove',
			name: 'removeClassIf',
			minArgs: 3
		}),

		/**
		* removes »class_name1« and adds »class_name2« to the class name of »elRef«.
		* (like seperate calls of »addClass« and »removeClass«)
		* if »strict« is »true« class name will be replaced only if »class_name1« exists
		*
		* @name	replaceClass
		* @param {Node} elRef
		*     reference of element to alter
		* @param {String} class_name1
		*     class name to replace
		* @param {String} class_name2
		*     replacement class name
		* @param {Boolean} strict
		*     if »true«, class name of elRef.« is altered only if »class_name1« is
		*     present in elRef.classname
		* @returns {String}
		*     previous value of »elRef.className«
		*/
		replaceClass: handler.bind({
			func: 'repl',
			name: 'replaceClass',
			minArgs: 3
		}),

		/**
		* for each argument »class_name«:
		* add »class_name« if not in class name of »elRef« and removes it
		* if »class_name« is set
		*
		* @name	toggleClass
		* @param {Node} elRef
		*     reference of element to alter
		* @param {String} class_name
		* @param {String...} [class_name]
		*     additional class names (optional)
		* @returns {String}
		*     previous value of »elRef.className«
		*/
		toggleClass: handler.bind({
			func: 'toggle',
			name: 'toggleClass'
		}),

		/**
		* adds »class_name« to class name of »elRef« if »condition« is truthy
		* otherwise »class_name« will be removed
		*
		* @name	addOrRemoveClassIf
		* @param {Node} elRef
		*     reference of element to alter
		* @param {String} class_name
		* @param {String...} [class_name]
		*     additional class names
		* @param {Mixed} condition
		*     class name is altered only if condition is truthy
		*     (true, 1, non-empty string etc...)
		* @returns {String}
		*     previous value of »elRef.className«
		*/
		addOrRemoveClassIf: handler.bind({
			func: 'add_rem_if',
			name: 'addOrRemoveClassIf',
			minArgs: 3
		})

		/**#@+ */
	};

	/**
	* Add all methods to a custom namespace object or add them to global namespace
	* (with prefix)
	*
	* @param {Object | String} ns
	*     object or prefix char(s).
	*     All chars of prefix must be valid within variable names
	*/
	K345.DOM.classAPI_NS = funcs.setnamespace;

	/* add to namespace K345.DOM */
	funcs.setnamespace(K345.DOM);
})(this);