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 | | */ |
14 | 1 | (function (w, d) { |
15 | | |
16 | 1 | 'use strict'; |
17 | | |
18 | | /** |
19 | | * is - a Set of validation functions |
20 | | * |
21 | | * @class |
22 | | * @private |
23 | | */ |
24 | 1 | 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) { |
36 | 14 | 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) { |
48 | 7 | 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 | | */ |
60 | 26 | 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) { |
71 | 8 | 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) { |
83 | 0 | 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) { |
95 | 0 | 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 | | */ |
111 | 1 | function merge(target, source) { |
112 | 2 | var k; |
113 | | |
114 | | //<validation> |
115 | 2 | if (!is.object(target) || !is.object(source)) { |
116 | 0 | throw new Error('Argument given must be of type Object'); |
117 | | } |
118 | | //</validation> |
119 | | |
120 | 2 | for (k in source) { |
121 | 8 | if (source.hasOwnProperty(k) && !target[k]) { |
122 | 8 | target[k] = source[k]; |
123 | | } |
124 | | } |
125 | | |
126 | 2 | 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 | | */ |
138 | 1 | function bind(fn, context) { |
139 | 7 | var slice = Array.prototype.slice, |
140 | | args; |
141 | | |
142 | 7 | if (!is.func(fn)) { |
143 | 0 | return undefined; |
144 | | } |
145 | | |
146 | 7 | args = slice.call(arguments, 2); |
147 | 7 | return function () { |
148 | 0 | 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 | | */ |
161 | 1 | function addEvent(elm, evType, fn) { |
162 | | //<validation> |
163 | 7 | if (!is.element(elm)) { |
164 | 0 | throw new Error('addEvent requires elm parameter to be a DOM Element'); |
165 | | } |
166 | | |
167 | 7 | if (!is.string(evType)) { |
168 | 0 | throw new Error('addEvent requires evTtype parameter to be a String'); |
169 | | } |
170 | | |
171 | 7 | if (!is.func(fn)) { |
172 | 0 | throw new Error('addEvent requires evTtype parameter to be a Function'); |
173 | | } |
174 | | //</validation> |
175 | | |
176 | 7 | if (elm.addEventListener) { |
177 | 7 | elm.addEventListener(evType, fn); |
178 | 0 | } else if (elm.attachEvent) { |
179 | 0 | elm.attachEvent('on' + evType); |
180 | | } else { |
181 | 0 | 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 | | */ |
194 | 1 | function removeEvent(elm, evType, fn) { |
195 | | |
196 | | //<validation> |
197 | 0 | if (!is.element(elm)) { |
198 | 0 | throw new Error('removeEvent requires elm parameter to be a DOM Element'); |
199 | | } |
200 | | |
201 | 0 | if (!is.string(evType)) { |
202 | 0 | throw new Error('removeEvent requires evTtype parameter to be a String'); |
203 | | } |
204 | | |
205 | 0 | if (!is.func(fn)) { |
206 | 0 | throw new Error('removeEvent requires evTtype parameter to be a Function'); |
207 | | } |
208 | | //</validation> |
209 | | |
210 | 0 | if (elm.removeEventListener) { |
211 | 0 | elm.removeEventListener(evType, fn); |
212 | 0 | } else if (elm.detachEvent) { |
213 | 0 | elm.detachEvent('on' + evType, fn); |
214 | | } else { |
215 | 0 | 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 | | */ |
230 | 1 | function indexOf(arr, val) { |
231 | 0 | var i = arr.length - 1; |
232 | | |
233 | 0 | if (Array.prototype.indexOf) { return arr.indexOf(val); } |
234 | | |
235 | 0 | for (i; i >= 0; i -= 1) { if (arr[i] === val) { return i; } } |
236 | | |
237 | 0 | 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 | | */ |
263 | 1 | function VisualHash(inputEl, options) { |
264 | | |
265 | | // Ensure that the function is called as a constructor |
266 | | //<validation> |
267 | 14 | if (!(this instanceof VisualHash)) { |
268 | 1 | return new VisualHash(inputEl, options); |
269 | | } |
270 | | |
271 | | // yeah... predicting a lot of people passing a jQuery object |
272 | 13 | if (inputEl && inputEl.jquery) { inputEl = inputEl.get(0); } |
273 | | |
274 | 13 | if (!inputEl || !is.element(inputEl)) { |
275 | 4 | throw new Error('VisualHash constructor needs at least one argument and has to be a DOM element'); |
276 | | } |
277 | | |
278 | 9 | if (options && !is.object(options)) { |
279 | 2 | throw new Error('VisualHash requires the options parameter to be of type object'); |
280 | | } |
281 | | //</validation> |
282 | | |
283 | 7 | this.inputEl = inputEl; |
284 | 7 | this.options = (options) ? merge(options, this.defaults) : this.defaults; |
285 | | |
286 | 7 | this.container = d.createElement('div'); |
287 | 7 | this.container.setAttribute('class', this.options.className); |
288 | | |
289 | 7 | this.stripes = (function (placeholder, size, className) { |
290 | 7 | var stripes = [], |
291 | | stripe; |
292 | | |
293 | 7 | while (size) { |
294 | 21 | stripe = d.createElement('div'); |
295 | 21 | stripe.setAttribute('class', className + ' stripe-' + size); |
296 | 21 | stripes.push(stripe); |
297 | 21 | placeholder.appendChild(stripe); |
298 | | |
299 | 21 | size -= 1; |
300 | | } |
301 | | |
302 | 7 | return stripes; |
303 | | |
304 | | }(this.container, this.options.stripes, this.options.stripesClass)); |
305 | | |
306 | 7 | this.inputHandler = bind(this.inputHandler, this); |
307 | 7 | addEvent(this.inputEl, 'input', this.inputHandler); |
308 | | |
309 | 14 | if (this.options.appendTo) { this.append(); } |
310 | | } |
311 | | |
312 | 1 | 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… |
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 | | |
353 | 0 | if ('hashFunction' in this.options && is.func(this.options.hashFunction)) { |
354 | 0 | return this.options.hashFunction(str); |
355 | | } |
356 | | |
357 | | |
358 | | |
359 | 0 | if (Sha1 && is.func(Sha1.hash)) { return Sha1.hash(str); } |
360 | 0 | if (md5 && is.func(md5)) { return md5(str); } |
361 | 0 | if (Crypto && is.object(Crypto)) { |
362 | 0 | if (Crypto.MD5 && is.func(Crypto.MD5)) { return Crypto.MD5(str); } |
363 | 0 | if (Crypto.SHA1 && is.func(Crypto.SHA1)) { return Crypto.SHA1(str); } |
364 | 0 | if (Crypto.SHA256 && is.func(Crypto.SHA256)) { return Crypto.MD5(str); } |
365 | | } |
366 | | |
367 | 0 | 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) { |
381 | 0 | var parts = [], |
382 | | begin = 0, |
383 | | end = size, |
384 | | interval = size; |
385 | | |
386 | | //<validation> |
387 | 0 | if (!is.string(str)) { |
388 | 0 | throw new Error('split function must be called with str parameter being of type String'); |
389 | | } |
390 | | |
391 | 0 | if (!is.number(size)) { |
392 | 0 | throw new Error('split function must be called with size parameter being of type Number'); |
393 | | } |
394 | | |
395 | 0 | if (!is.number(chunks)) { |
396 | 0 | throw new Error('split function must be called with chunks parameter being of type Number'); |
397 | | } |
398 | | //</validation> |
399 | | |
400 | 0 | while (chunks) { |
401 | 0 | parts.push(str.substring(begin, end)); |
402 | | |
403 | 0 | begin = end + 1; |
404 | 0 | end = end + interval + 1; |
405 | | |
406 | 0 | chunks -= 1; |
407 | | } |
408 | | |
409 | 0 | 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) { |
425 | 0 | var i = elements.length - 1, |
426 | | currentStyle = '', |
427 | | currentEl, |
428 | | splittedStyle = [], |
429 | | splittedIdx = 0; |
430 | | |
431 | | //<validation> |
432 | 0 | if (!is.array(elements)) { |
433 | 0 | throw new Error('fillColors function must be called with elements parameter being of type Array or Static Node'); |
434 | | } |
435 | | |
436 | 0 | if (!is.array(colors)) { |
437 | 0 | throw new Error('fillColors function must be called with colors parameter being of type Array'); |
438 | | } |
439 | | //</validation> |
440 | | |
441 | 0 | for (i; i >= 0; i -= 1) { |
442 | 0 | currentEl = elements[i]; |
443 | 0 | currentStyle = currentEl.getAttribute('style'); |
444 | | |
445 | 0 | if (currentStyle) { |
446 | 0 | splittedStyle = currentStyle.split(/(\;|:)/gi); |
447 | 0 | splittedIdx = indexOf(splittedStyle, 'background-color'); |
448 | | |
449 | 0 | if (splittedIdx !== -1) { |
450 | 0 | splittedStyle[splittedIdx + 2] = '#' + colors[i]; |
451 | | } else { |
452 | 0 | splittedStyle[splittedStyle.length] = ';background-color: #' + colors[i] + ';'; |
453 | | } |
454 | | |
455 | 0 | currentEl.setAttribute('style', splittedStyle.join('')); |
456 | | } else { |
457 | 0 | currentEl.setAttribute('style', 'background-color: #' + colors[i] + ';'); |
458 | | } |
459 | | } |
460 | | |
461 | 0 | 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) { |
476 | 0 | var i = elements.length - 1, |
477 | | currentStyle = '', |
478 | | currentEl, |
479 | | splittedStyle = [], |
480 | | splittedIdx = 0; |
481 | | |
482 | | //<validation> |
483 | 0 | if (!is.array(elements)) { |
484 | 0 | throw new Error('clearColors function must be called with elements parameter being of type Array or Static Node'); |
485 | | } |
486 | | //</validation> |
487 | | |
488 | 0 | for (i; i >= 0; i -= 1) { |
489 | 0 | currentEl = elements[i]; |
490 | 0 | currentStyle = currentEl.getAttribute('style'); |
491 | | |
492 | 0 | if (currentStyle) { |
493 | 0 | splittedStyle = currentStyle.split(/(\;|:)/gi); |
494 | 0 | splittedIdx = indexOf(splittedStyle, 'background-color'); |
495 | | |
496 | 0 | if (splittedIdx !== -1) { |
497 | | // It's faster than another loop |
498 | | // and since are just a few lines is still managable |
499 | 0 | splittedStyle[splittedIdx] = ''; |
500 | 0 | splittedStyle[splittedIdx + 1] = ''; |
501 | 0 | splittedStyle[splittedIdx + 2] = ''; |
502 | 0 | splittedStyle[splittedIdx + 3] = ''; |
503 | 0 | currentEl.setAttribute('style', splittedStyle.join('')); |
504 | | } |
505 | | } |
506 | | } |
507 | | |
508 | 0 | 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) { |
523 | 14 | if (!element) { element = this.options.appendTo; } |
524 | | //<validation> |
525 | 7 | 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> |
527 | 14 | if (element) { element.appendChild(this.container); } |
528 | | |
529 | 7 | 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 () { |
542 | 0 | removeEvent(this.inputEl, 'input', this.inputHandler); |
543 | 0 | this.container.parentNode.removeChild(this.container); |
544 | 0 | delete this.container; |
545 | 0 | delete this.stripes; |
546 | 0 | delete this.inputEl; |
547 | 0 | 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 | | |
561 | 0 | var str = evt.target.value, |
562 | | hash = '', |
563 | | splittedHash = []; |
564 | | |
565 | 0 | if (str) { |
566 | 0 | hash = this.toHash(str); |
567 | 0 | splittedHash = this.split(hash, 6, this.options.stripes); |
568 | 0 | this.fillColors(this.stripes, splittedHash); |
569 | 0 | } else { this.clearColors(this.stripes); } |
570 | | |
571 | 0 | if (this.options.onInput) { |
572 | 0 | this.options.onInput.call(this, str, splittedHash); |
573 | | } |
574 | | |
575 | | } |
576 | | }; |
577 | | |
578 | 1 | if (typeof define === 'function' && define.amd) { |
579 | 0 | define('VisualHash', [], function () { return VisualHash; }); |
580 | 1 | } else if (typeof module !== 'undefined' && typeof exports !== 'undefined' && module.exports) { |
581 | 1 | module.exports = VisualHash; |
582 | | } else { |
583 | 0 | w.VisualHash = VisualHash; |
584 | | } |
585 | | |
586 | 1 | return VisualHash; |
587 | | |
588 | | }(window, document)); |