Coverage

38%
157
60
97

VisualHash.js

38%
157
60
97
LineHitsSource
1/*jslint browser: true, indent: 4 */
2/*global Sha1, md5, Crypto, module, exports, define*/
3
4/*!
5 * VisualHash
6 *
7 * @author Luis Couto
8 * @organization 15minuteslate.net
9 * @contact couto@15minuteslate.net
10 * @version 0.0.1
11 *
12 * @license 2012 - MIT (http://couto.mit-license.org/)
13 */
141(function (w, d) {
15
161 'use strict';
17
18 /**
19 * is - a Set of validation functions
20 *
21 * @class
22 * @private
23 */
241 var is = {
25 tester : Object.prototype.toString,
26
27 /**
28 * func
29 * tests if value given is Function
30 *
31 * @static
32 * @param {String|Object|Array|Boolean|Number} obj
33 * @returns Boolean
34 */
35 func : function (obj) {
3614 return (this.tester.call(obj) === '[object Function]');
37 },
38
39 /**
40 * string
41 * tests if value given is String
42 *
43 * @static
44 * @param {String|Object|Array|Boolean|Number} obj
45 * @returns Boolean
46 */
47 string : function (obj) {
487 return (this.tester.call(obj) === '[object String]');
49 },
50
51 //<validation>
52 /**
53 * element
54 * tests if value given is a DOM Element
55 *
56 * @static
57 * @param {String|Object|Array|Boolean|Number} obj
58 * @returns Boolean
59 */
6026 element : function (obj) { return !!(obj && obj.nodeType === 1); },
61
62 /**
63 * object
64 * tests if value given is Object
65 *
66 * @static
67 * @param {String|Object|Array|Boolean|Number} obj
68 * @returns Boolean
69 */
70 object : function (obj) {
718 return this.tester.call(obj) === '[object Object]';
72 },
73
74 /**
75 * array
76 * tests if value given is Array
77 *
78 * @static
79 * @param {String|Object|Array|Boolean|Number} obj
80 * @returns Boolean
81 */
82 array : function (obj) {
830 return (this.tester.call(obj) === '[object Array]');
84 },
85
86 /**
87 * number
88 * tests if value given is Number
89 *
90 * @static
91 * @param {String|Object|Array|Boolean|Number} obj
92 * @returns Boolean
93 */
94 number : function (obj) {
950 return (this.tester.call(obj) === '[object Number]');
96 }
97 //</validation>
98 };
99
100 /**
101 * merge
102 * Merges to objects into one.
103 * Doesn't overwrite existing properties
104 * Changes apply directly to target object.
105 *
106 * @private
107 * @param {Object} target Object that will receive the new properties
108 * @param {Object} source Object that will give its properties
109 * @returns Object
110 */
1111 function merge(target, source) {
1122 var k;
113
114 //<validation>
1152 if (!is.object(target) || !is.object(source)) {
1160 throw new Error('Argument given must be of type Object');
117 }
118 //</validation>
119
1202 for (k in source) {
1218 if (source.hasOwnProperty(k) && !target[k]) {
1228 target[k] = source[k];
123 }
124 }
125
1262 return target;
127 }
128
129 /**
130 * bind
131 * Fixes the context for the given function
132 *
133 * @private
134 * @param {Function} fn function whom context will be fixed
135 * @param {Object} context object that will serve as context
136 * @returns a function with the given context
137 */
1381 function bind(fn, context) {
1397 var slice = Array.prototype.slice,
140 args;
141
1427 if (!is.func(fn)) {
1430 return undefined;
144 }
145
1467 args = slice.call(arguments, 2);
1477 return function () {
1480 return fn.apply(context, args.concat(slice.call(arguments)));
149 };
150 }
151 /**
152 * addEvent
153 * cross-browser addEvent function
154 *
155 * @private
156 * @param {DOM Elment} elm DOM Element to attach the event listener
157 * @param {String} evType event name that it's going to be attached
158 * @param {Function} fn Function that will serve as callback
159 * @returns undefined
160 */
1611 function addEvent(elm, evType, fn) {
162 //<validation>
1637 if (!is.element(elm)) {
1640 throw new Error('addEvent requires elm parameter to be a DOM Element');
165 }
166
1677 if (!is.string(evType)) {
1680 throw new Error('addEvent requires evTtype parameter to be a String');
169 }
170
1717 if (!is.func(fn)) {
1720 throw new Error('addEvent requires evTtype parameter to be a Function');
173 }
174 //</validation>
175
1767 if (elm.addEventListener) {
1777 elm.addEventListener(evType, fn);
1780 } else if (elm.attachEvent) {
1790 elm.attachEvent('on' + evType);
180 } else {
1810 elm['on' + evType] = fn;
182 }
183 }
184 /**
185 * removeEvent
186 * cross-browser removeEvent function
187 *
188 * @private
189 * @param {DOM Elment} elm DOM Element that has the listener attached
190 * @param {String} evType event name that was attached
191 * @param {Function} fn Function that served as callback
192 * @returns true || handler
193 */
1941 function removeEvent(elm, evType, fn) {
195
196 //<validation>
1970 if (!is.element(elm)) {
1980 throw new Error('removeEvent requires elm parameter to be a DOM Element');
199 }
200
2010 if (!is.string(evType)) {
2020 throw new Error('removeEvent requires evTtype parameter to be a String');
203 }
204
2050 if (!is.func(fn)) {
2060 throw new Error('removeEvent requires evTtype parameter to be a Function');
207 }
208 //</validation>
209
2100 if (elm.removeEventListener) {
2110 elm.removeEventListener(evType, fn);
2120 } else if (elm.detachEvent) {
2130 elm.detachEvent('on' + evType, fn);
214 } else {
2150 elm['on' + evType] = null;
216 }
217 }
218
219 /**
220 * indexOf
221 * given an array, it will search for the string value.
222 * if the browser supports indexOf on arrays it will use the browser version
223 * instead
224 *
225 * @public
226 * @param {Array} arr Array where to find the string
227 * @param {String|Number|Boolean} val value to search for
228 * @returns -1 if not found, index position otherwise
229 */
2301 function indexOf(arr, val) {
2310 var i = arr.length - 1;
232
2330 if (Array.prototype.indexOf) { return arr.indexOf(val); }
234
2350 for (i; i >= 0; i -= 1) { if (arr[i] === val) { return i; } }
236
2370 return -1;
238 }
239
240 /**
241 * VisualHash
242 *
243 * @class
244 * @param {DOM element} inputEl inputElement
245 * @param {Object} options
246 * @return {Object} VisualHash instance
247 *
248 * @example
249 *
250 * var coloredhash = new VisualHash(d.querySelectorAll('input')[0], {
251 * stripes : 3, // default
252 * className : 'visual-hasher' // default
253 * stripesClass : 'visual-hasher-stripe' // default
254 * appendTo : d.getElementById('color_placeholder'),
255 * hashFunction : MD5,
256 * onInput : function() {
257 * console.log('A new color was typed')
258 * }
259 * });
260 *
261 * coloredhash.destroy();
262 */
2631 function VisualHash(inputEl, options) {
264
265 // Ensure that the function is called as a constructor
266 //<validation>
26714 if (!(this instanceof VisualHash)) {
2681 return new VisualHash(inputEl, options);
269 }
270
271 // yeah... predicting a lot of people passing a jQuery object
27213 if (inputEl && inputEl.jquery) { inputEl = inputEl.get(0); }
273
27413 if (!inputEl || !is.element(inputEl)) {
2754 throw new Error('VisualHash constructor needs at least one argument and has to be a DOM element');
276 }
277
2789 if (options && !is.object(options)) {
2792 throw new Error('VisualHash requires the options parameter to be of type object');
280 }
281 //</validation>
282
2837 this.inputEl = inputEl;
2847 this.options = (options) ? merge(options, this.defaults) : this.defaults;
285
2867 this.container = d.createElement('div');
2877 this.container.setAttribute('class', this.options.className);
288
2897 this.stripes = (function (placeholder, size, className) {
2907 var stripes = [],
291 stripe;
292
2937 while (size) {
29421 stripe = d.createElement('div');
29521 stripe.setAttribute('class', className + ' stripe-' + size);
29621 stripes.push(stripe);
29721 placeholder.appendChild(stripe);
298
29921 size -= 1;
300 }
301
3027 return stripes;
303
304 }(this.container, this.options.stripes, this.options.stripesClass));
305
3067 this.inputHandler = bind(this.inputHandler, this);
3077 addEvent(this.inputEl, 'input', this.inputHandler);
308
30914 if (this.options.appendTo) { this.append(); }
310 }
311
3121 VisualHash.prototype = {
313
314 /**
315 * @constructor
316 * Set the constructor property back to the correct Function
317 */
318 constructor : VisualHash,
319
320 /**
321 * @property {Object} defaults
322 * @readonly
323 */
324 defaults : {
325 'stripes' : 3,
326 'appendTo' : d.body,
327 'className' : 'visual-hasher',
328 'stripesClass' : 'visual-hasher-stripe'
329 },
330
331 /**
332 * toHash
333 * This is not the best option maybe, but I don't think
334 * that VisualHash should have the responsability to hash a string
335 * I could implement (aka copy+paste) a MD5 or SHAx implementation
336 * but that would cause a huge increase of file size.
337 * I do accept suggestions and opinions on this matter&#226;&#128;&#166;
338 *
339 * This function will use the given hashFunction given on the constructor
340 * or it will use any of the following functions if present
341 *
342 * Sha1 (from Chris Veness) - http://www.movable-type.co.uk/scripts/sha1.html
343 * md5 (Joseph Myers) - http://www.myersdaily.org/joseph/javascript/md5-text.html
344 * Crypto Collection (MD5, SHA1 & SHA256) - http://code.google.com/p/crypto-js/
345 *
346 * @method
347 * @public
348 * @param {String} str String to be converted to hash
349 * @returns {String} Hashed value of the given string
350 */
351 toHash : function (str) {
352
3530 if ('hashFunction' in this.options && is.func(this.options.hashFunction)) {
3540 return this.options.hashFunction(str);
355 }
356
357
358
3590 if (Sha1 && is.func(Sha1.hash)) { return Sha1.hash(str); }
3600 if (md5 && is.func(md5)) { return md5(str); }
3610 if (Crypto && is.object(Crypto)) {
3620 if (Crypto.MD5 && is.func(Crypto.MD5)) { return Crypto.MD5(str); }
3630 if (Crypto.SHA1 && is.func(Crypto.SHA1)) { return Crypto.SHA1(str); }
3640 if (Crypto.SHA256 && is.func(Crypto.SHA256)) { return Crypto.MD5(str); }
365 }
366
3670 throw new Error('No hash implementation was found');
368
369 },
370
371 /**
372 * split
373 * Given a string it, will split in the given amount of chunks with the given size
374 *
375 * @method
376 * @public
377 * @param {String} str String to be splitted
378 * @returns {Array} Array with the splitted string
379 */
380 split : function (str, size, chunks) {
3810 var parts = [],
382 begin = 0,
383 end = size,
384 interval = size;
385
386 //<validation>
3870 if (!is.string(str)) {
3880 throw new Error('split function must be called with str parameter being of type String');
389 }
390
3910 if (!is.number(size)) {
3920 throw new Error('split function must be called with size parameter being of type Number');
393 }
394
3950 if (!is.number(chunks)) {
3960 throw new Error('split function must be called with chunks parameter being of type Number');
397 }
398 //</validation>
399
4000 while (chunks) {
4010 parts.push(str.substring(begin, end));
402
4030 begin = end + 1;
4040 end = end + interval + 1;
405
4060 chunks -= 1;
407 }
408
4090 return parts;
410 },
411
412 /**
413 * fillColors
414 * Given an array or array-like of elements and an array of colors,
415 * it will set the background-color of each elements with the
416 * respective color.
417 *
418 * @method
419 * @public
420 * @param {Array} elements Array of node Elements
421 * @param {Array} colors Array of colors
422 * @returns VisualHash instance (this)
423 */
424 fillColors : function (elements, colors) {
4250 var i = elements.length - 1,
426 currentStyle = '',
427 currentEl,
428 splittedStyle = [],
429 splittedIdx = 0;
430
431 //<validation>
4320 if (!is.array(elements)) {
4330 throw new Error('fillColors function must be called with elements parameter being of type Array or Static Node');
434 }
435
4360 if (!is.array(colors)) {
4370 throw new Error('fillColors function must be called with colors parameter being of type Array');
438 }
439 //</validation>
440
4410 for (i; i >= 0; i -= 1) {
4420 currentEl = elements[i];
4430 currentStyle = currentEl.getAttribute('style');
444
4450 if (currentStyle) {
4460 splittedStyle = currentStyle.split(/(\;|:)/gi);
4470 splittedIdx = indexOf(splittedStyle, 'background-color');
448
4490 if (splittedIdx !== -1) {
4500 splittedStyle[splittedIdx + 2] = '#' + colors[i];
451 } else {
4520 splittedStyle[splittedStyle.length] = ';background-color: #' + colors[i] + ';';
453 }
454
4550 currentEl.setAttribute('style', splittedStyle.join(''));
456 } else {
4570 currentEl.setAttribute('style', 'background-color: #' + colors[i] + ';');
458 }
459 }
460
4610 return this;
462
463 },
464
465 /**
466 * clearColors
467 * Given an array or array-like of elements it will remove the
468 * background-color property of the its style attribute
469 *
470 * @method
471 * @public
472 * @param {Array} elements Array of node Elements
473 * @returns VisualHash instance (this)
474 */
475 clearColors : function (elements) {
4760 var i = elements.length - 1,
477 currentStyle = '',
478 currentEl,
479 splittedStyle = [],
480 splittedIdx = 0;
481
482 //<validation>
4830 if (!is.array(elements)) {
4840 throw new Error('clearColors function must be called with elements parameter being of type Array or Static Node');
485 }
486 //</validation>
487
4880 for (i; i >= 0; i -= 1) {
4890 currentEl = elements[i];
4900 currentStyle = currentEl.getAttribute('style');
491
4920 if (currentStyle) {
4930 splittedStyle = currentStyle.split(/(\;|:)/gi);
4940 splittedIdx = indexOf(splittedStyle, 'background-color');
495
4960 if (splittedIdx !== -1) {
497 // It's faster than another loop
498 // and since are just a few lines is still managable
4990 splittedStyle[splittedIdx] = '';
5000 splittedStyle[splittedIdx + 1] = '';
5010 splittedStyle[splittedIdx + 2] = '';
5020 splittedStyle[splittedIdx + 3] = '';
5030 currentEl.setAttribute('style', splittedStyle.join(''));
504 }
505 }
506 }
507
5080 return this;
509 },
510
511 /**
512 * append
513 * Appends the VisualHash element inside the given element, if no element
514 * is given it fallbacks to the options property (that ultimately gets
515 * its value from the defaults property)
516 *
517 * @method
518 * @public
519 * @param {DOM Element} element Element where the VisualHash will be appended
520 * @returns VisualHash instance (this)
521 */
522 append : function (element) {
52314 if (!element) { element = this.options.appendTo; }
524 //<validation>
5257 if (!is.element(element)) { throw new Error('append function must be called with element parameter being of type Element or an Element must be given in the constructor options'); }
526 //</validation>
52714 if (element) { element.appendChild(this.container); }
528
5297 return this;
530 },
531
532 /**
533 * destroy
534 * Removes all event listeners used by the VisualHash, and removes the
535 * DOM element created
536 *
537 * @method
538 * @public
539 * @returns undefined
540 */
541 destroy : function () {
5420 removeEvent(this.inputEl, 'input', this.inputHandler);
5430 this.container.parentNode.removeChild(this.container);
5440 delete this.container;
5450 delete this.stripes;
5460 delete this.inputEl;
5470 delete this.options;
548 },
549
550 /**
551 * inputHandler
552 * Handler used when a new input is given in the inputbox
553 * Calls the onInput handler if given
554 *
555 * @method
556 * @protected
557 * @returns undefined
558 */
559 inputHandler : function (evt) {
560
5610 var str = evt.target.value,
562 hash = '',
563 splittedHash = [];
564
5650 if (str) {
5660 hash = this.toHash(str);
5670 splittedHash = this.split(hash, 6, this.options.stripes);
5680 this.fillColors(this.stripes, splittedHash);
5690 } else { this.clearColors(this.stripes); }
570
5710 if (this.options.onInput) {
5720 this.options.onInput.call(this, str, splittedHash);
573 }
574
575 }
576 };
577
5781 if (typeof define === 'function' && define.amd) {
5790 define('VisualHash', [], function () { return VisualHash; });
5801 } else if (typeof module !== 'undefined' && typeof exports !== 'undefined' && module.exports) {
5811 module.exports = VisualHash;
582 } else {
5830 w.VisualHash = VisualHash;
584 }
585
5861 return VisualHash;
587
588}(window, document));