├── preview.png ├── test ├── index.html ├── test.js └── qunit │ ├── qunit.css │ └── qunit.js ├── examples ├── nohassle.html ├── basic.html ├── showcase.css ├── showcase.html ├── advanced.html └── showcase.js ├── themes.css ├── colorpicker.min.js ├── doc.html ├── README.md └── colorpicker.js /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidDurman/FlexiColorPicker/HEAD/preview.png -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | FlexiColorPicker Unit Tests 5 | 6 | 7 | 8 | 9 |
10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/nohassle.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /examples/showcase.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Waiting for the Sunrise', arial, serif; 3 | background-color: #1a2529; 4 | color: black; 5 | } 6 | 7 | h1 { 8 | font-size: 22px; 9 | margin: 30px auto; 10 | width: 840px; 11 | color: lightgray; 12 | letter-spacing: 2px; 13 | } 14 | 15 | h1 span { 16 | color: white; 17 | } 18 | 19 | #container { 20 | width: 800px; 21 | margin: 20px auto; 22 | background-color: #d4edfb; 23 | overflow: hidden; 24 | box-shadow: inset -2px -2px 10px gray; 25 | padding: 20px; 26 | border: 1px dashed black; 27 | } 28 | 29 | .cp { 30 | margin: 20px; 31 | } 32 | 33 | .io { 34 | clear: both; 35 | float: left; 36 | margin: 20px; 37 | padding: 20px; 38 | border: 1px solid lightgray; 39 | position: relative; 40 | background-color: white; 41 | } 42 | 43 | .io input { 44 | width: 55px; 45 | border: 1px solid black; 46 | } 47 | 48 | .io label { 49 | font-size: 12px; 50 | font-weight: bold; 51 | } 52 | 53 | #rgb_css, 54 | #hsv_css { 55 | font-size: 9px; 56 | } 57 | 58 | #color { 59 | width: 40px; 60 | height: 40px; 61 | float: left; 62 | margin: 20px; 63 | border: 1px solid black; 64 | } 65 | 66 | #text-color { 67 | float: left; 68 | margin-top: 30px; 69 | } -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | module('Color conversion', { 2 | 3 | setup: function() { 4 | 5 | 6 | }, 7 | 8 | teardown: function() { 9 | } 10 | 11 | }); 12 | 13 | test('from hex and back', function() { 14 | 15 | var hex = '#123321'; 16 | 17 | var rgb = ColorPicker.hex2rgb(hex); 18 | 19 | equal(ColorPicker.rgb2hex(rgb), hex, 'conversion from hex to rgb and back must equal'); 20 | 21 | var hsv = ColorPicker.hex2hsv(hex); 22 | 23 | equal(ColorPicker.hsv2hex(hsv), hex, 'conversion from hex to hsv and back must equal'); 24 | 25 | }); 26 | 27 | test('from rgb and back', function() { 28 | 29 | var rgb = { r: 100, g: 20, b: 180 }; 30 | 31 | var hex = ColorPicker.rgb2hex(rgb); 32 | 33 | deepEqual(ColorPicker.hex2rgb(hex), rgb, 'conversion from rgb to hex and back must equal'); 34 | 35 | var hsv = ColorPicker.rgb2hsv(rgb); 36 | 37 | deepEqual(ColorPicker.hsv2rgb(hsv), rgb, 'conversion from rgb to hsv and back must equal'); 38 | 39 | }); 40 | 41 | test('from hsv and back', function() { 42 | 43 | var hsv = { h: 195, s: .2, v: .8 }; 44 | 45 | var hex = ColorPicker.hsv2hex(hsv); 46 | 47 | deepEqual(ColorPicker.hex2hsv(hex), hsv, 'conversion from hsv to hex and back must equal'); 48 | 49 | var rgb = ColorPicker.hsv2rgb(hsv); 50 | 51 | deepEqual(ColorPicker.rgb2hsv(rgb), hsv, 'conversion from hsv to rgb and back must equal'); 52 | 53 | }); 54 | -------------------------------------------------------------------------------- /examples/showcase.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

Showcase of all the built-in themes and the use of FlexiColorPicker

10 | 11 |
12 | 13 |
14 |
15 |
16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
39 | 40 |
41 |

Lorem ipsum dolor sit amet.

42 | 43 |
44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /examples/advanced.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 29 | 30 | 31 | 32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | 41 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /examples/showcase.js: -------------------------------------------------------------------------------- 1 | // Color pickers in different flavors. 2 | // ----------------------------------- 3 | 4 | var cpDefault = ColorPicker(document.getElementById('default'), updateInputs); 5 | var cpSmall = ColorPicker(document.getElementById('small'), updateInputs); 6 | var cpFancy = ColorPicker(document.getElementById('fancy'), updateInputs); 7 | 8 | // Inputs. 9 | // ------- 10 | 11 | var iHex = document.getElementById('hex'); 12 | var iR = document.getElementById('rgb_r'); 13 | var iG = document.getElementById('rgb_g'); 14 | var iB = document.getElementById('rgb_b'); 15 | var iH = document.getElementById('hsv_h'); 16 | var iS = document.getElementById('hsv_s'); 17 | var iV = document.getElementById('hsv_v'); 18 | 19 | var rgbCSS = document.getElementById('rgb_css'); 20 | var hsvCSS = document.getElementById('hsv_css'); 21 | 22 | var color = document.getElementById('color'); 23 | var textColor = document.getElementById('text-color'); 24 | 25 | function updateInputs(hex) { 26 | 27 | var rgb = ColorPicker.hex2rgb(hex); 28 | var hsv = ColorPicker.hex2hsv(hex); 29 | 30 | iHex.value = hex; 31 | 32 | iR.value = rgb.r; 33 | iG.value = rgb.g; 34 | iB.value = rgb.b; 35 | 36 | iH.value = hsv.h.toFixed(2); 37 | iS.value = hsv.s.toFixed(2); 38 | iV.value = hsv.v.toFixed(2); 39 | 40 | rgbCSS.innerHTML = 'rgb(' + rgb.r + ', ' + rgb.g + ', ' + rgb.b + ')'; 41 | hsvCSS.innerHTML = 'hsv(' + hsv.h.toFixed(2) + ', ' + hsv.s.toFixed(2) + ', ' + hsv.v.toFixed(2) + ')'; 42 | 43 | color.style.backgroundColor = hex; 44 | textColor.style.color = hex; 45 | } 46 | 47 | function updateColorPickers(hex) { 48 | 49 | cpDefault.setHex(hex); 50 | cpSmall.setHex(hex); 51 | cpFancy.setHex(hex); 52 | } 53 | 54 | 55 | var initialHex = '#f4329c'; 56 | updateColorPickers(initialHex); 57 | 58 | 59 | iHex.onchange = function() { updateColorPickers(iHex.value); }; 60 | 61 | iR.onchange = function() { updateColorPickers(ColorPicker.rgb2hex({ r: iR.value, g: iG.value, b: iB.value })); } 62 | iG.onchange = function() { updateColorPickers(ColorPicker.rgb2hex({ r: iR.value, g: iG.value, b: iB.value })); } 63 | iB.onchange = function() { updateColorPickers(ColorPicker.rgb2hex({ r: iR.value, g: iG.value, b: iB.value })); } 64 | 65 | iH.onchange = function() { updateColorPickers(ColorPicker.hsv2hex({ h: iH.value, s: iS.value, v: iV.value })); } 66 | iS.onchange = function() { updateColorPickers(ColorPicker.hsv2hex({ h: iH.value, s: iS.value, v: iV.value })); } 67 | iV.onchange = function() { updateColorPickers(ColorPicker.hsv2hex({ h: iH.value, s: iS.value, v: iV.value })); } 68 | -------------------------------------------------------------------------------- /themes.css: -------------------------------------------------------------------------------- 1 | /* Common stuff */ 2 | .picker-wrapper, 3 | .slide-wrapper { 4 | position: relative; 5 | float: left; 6 | } 7 | .picker-indicator, 8 | .slide-indicator { 9 | position: absolute; 10 | left: 0; 11 | top: 0; 12 | pointer-events: none; 13 | } 14 | .picker, 15 | .slide { 16 | cursor: crosshair; 17 | float: left; 18 | } 19 | 20 | /* Default skin */ 21 | 22 | .cp-default { 23 | background-color: gray; 24 | padding: 12px; 25 | box-shadow: 0 0 40px #000; 26 | border-radius: 15px; 27 | float: left; 28 | } 29 | .cp-default .picker { 30 | width: 200px; 31 | height: 200px; 32 | } 33 | .cp-default .slide { 34 | width: 30px; 35 | height: 200px; 36 | } 37 | .cp-default .slide-wrapper { 38 | margin-left: 10px; 39 | } 40 | .cp-default .picker-indicator { 41 | width: 5px; 42 | height: 5px; 43 | border: 2px solid darkblue; 44 | -moz-border-radius: 4px; 45 | -o-border-radius: 4px; 46 | -webkit-border-radius: 4px; 47 | border-radius: 4px; 48 | opacity: .5; 49 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; 50 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=50); 51 | filter: alpha(opacity=50); 52 | background-color: white; 53 | } 54 | .cp-default .slide-indicator { 55 | width: 100%; 56 | height: 10px; 57 | left: -4px; 58 | opacity: .6; 59 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=60)"; 60 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=60); 61 | filter: alpha(opacity=60); 62 | border: 4px solid lightblue; 63 | -moz-border-radius: 4px; 64 | -o-border-radius: 4px; 65 | -webkit-border-radius: 4px; 66 | border-radius: 4px; 67 | background-color: white; 68 | } 69 | 70 | /* Small skin */ 71 | 72 | .cp-small { 73 | padding: 5px; 74 | background-color: white; 75 | float: left; 76 | border-radius: 5px; 77 | } 78 | .cp-small .picker { 79 | width: 100px; 80 | height: 100px; 81 | } 82 | .cp-small .slide { 83 | width: 15px; 84 | height: 100px; 85 | } 86 | .cp-small .slide-wrapper { 87 | margin-left: 5px; 88 | } 89 | .cp-small .picker-indicator { 90 | width: 1px; 91 | height: 1px; 92 | border: 1px solid black; 93 | background-color: white; 94 | } 95 | .cp-small .slide-indicator { 96 | width: 100%; 97 | height: 2px; 98 | left: 0px; 99 | background-color: black; 100 | } 101 | 102 | /* Fancy skin */ 103 | 104 | .cp-fancy { 105 | padding: 10px; 106 | /* background-color: #C5F7EA; */ 107 | background: -webkit-linear-gradient(top, #aaa 0%, #222 100%); 108 | float: left; 109 | border: 1px solid #999; 110 | box-shadow: inset 0 0 10px white; 111 | } 112 | .cp-fancy .picker { 113 | width: 200px; 114 | height: 200px; 115 | } 116 | .cp-fancy .slide { 117 | width: 30px; 118 | height: 200px; 119 | } 120 | .cp-fancy .slide-wrapper { 121 | margin-left: 10px; 122 | } 123 | .cp-fancy .picker-indicator { 124 | width: 24px; 125 | height: 24px; 126 | background-image: url(http://cdn1.iconfinder.com/data/icons/fugue/bonus/icons-24/target.png); 127 | } 128 | .cp-fancy .slide-indicator { 129 | width: 30px; 130 | height: 31px; 131 | left: 30px; 132 | background-image: url(http://cdn1.iconfinder.com/data/icons/bluecoral/Left.png); 133 | } 134 | 135 | /* Normal skin */ 136 | 137 | .cp-normal { 138 | padding: 10px; 139 | background-color: white; 140 | float: left; 141 | border: 4px solid #d6d6d6; 142 | box-shadow: inset 0 0 10px white; 143 | } 144 | .cp-normal .picker { 145 | width: 200px; 146 | height: 200px; 147 | } 148 | .cp-normal .slide { 149 | width: 30px; 150 | height: 200px; 151 | } 152 | .cp-normal .slide-wrapper { 153 | margin-left: 10px; 154 | } 155 | .cp-normal .picker-indicator { 156 | width: 5px; 157 | height: 5px; 158 | border: 1px solid gray; 159 | opacity: .5; 160 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; 161 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=50); 162 | filter: alpha(opacity=50); 163 | background-color: white; 164 | pointer-events: none; 165 | } 166 | .cp-normal .slide-indicator { 167 | width: 100%; 168 | height: 10px; 169 | left: -4px; 170 | opacity: .6; 171 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=60)"; 172 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=60); 173 | filter: alpha(opacity=60); 174 | border: 4px solid gray; 175 | background-color: white; 176 | pointer-events: none; 177 | } 178 | -------------------------------------------------------------------------------- /test/qunit/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.10.0pre - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright 2012 jQuery Foundation and other contributors 7 | * Dual licensed under the MIT or GPL Version 2 licenses. 8 | * http://jquery.org/license 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 5px 5px 0 0; 42 | -moz-border-radius: 5px 5px 0 0; 43 | -webkit-border-top-right-radius: 5px; 44 | -webkit-border-top-left-radius: 5px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-testrunner-toolbar label { 58 | display: inline-block; 59 | padding: 0 .5em 0 .1em; 60 | } 61 | 62 | #qunit-banner { 63 | height: 5px; 64 | } 65 | 66 | #qunit-testrunner-toolbar { 67 | padding: 0.5em 0 0.5em 2em; 68 | color: #5E740B; 69 | background-color: #eee; 70 | } 71 | 72 | #qunit-userAgent { 73 | padding: 0.5em 0 0.5em 2.5em; 74 | background-color: #2b81af; 75 | color: #fff; 76 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 77 | } 78 | 79 | 80 | /** Tests: Pass/Fail */ 81 | 82 | #qunit-tests { 83 | list-style-position: inside; 84 | } 85 | 86 | #qunit-tests li { 87 | padding: 0.4em 0.5em 0.4em 2.5em; 88 | border-bottom: 1px solid #fff; 89 | list-style-position: inside; 90 | } 91 | 92 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 93 | display: none; 94 | } 95 | 96 | #qunit-tests li strong { 97 | cursor: pointer; 98 | } 99 | 100 | #qunit-tests li a { 101 | padding: 0.5em; 102 | color: #c2ccd1; 103 | text-decoration: none; 104 | } 105 | #qunit-tests li a:hover, 106 | #qunit-tests li a:focus { 107 | color: #000; 108 | } 109 | 110 | #qunit-tests ol { 111 | margin-top: 0.5em; 112 | padding: 0.5em; 113 | 114 | background-color: #fff; 115 | 116 | border-radius: 5px; 117 | -moz-border-radius: 5px; 118 | -webkit-border-radius: 5px; 119 | } 120 | 121 | #qunit-tests table { 122 | border-collapse: collapse; 123 | margin-top: .2em; 124 | } 125 | 126 | #qunit-tests th { 127 | text-align: right; 128 | vertical-align: top; 129 | padding: 0 .5em 0 0; 130 | } 131 | 132 | #qunit-tests td { 133 | vertical-align: top; 134 | } 135 | 136 | #qunit-tests pre { 137 | margin: 0; 138 | white-space: pre-wrap; 139 | word-wrap: break-word; 140 | } 141 | 142 | #qunit-tests del { 143 | background-color: #e0f2be; 144 | color: #374e0c; 145 | text-decoration: none; 146 | } 147 | 148 | #qunit-tests ins { 149 | background-color: #ffcaca; 150 | color: #500; 151 | text-decoration: none; 152 | } 153 | 154 | /*** Test Counts */ 155 | 156 | #qunit-tests b.counts { color: black; } 157 | #qunit-tests b.passed { color: #5E740B; } 158 | #qunit-tests b.failed { color: #710909; } 159 | 160 | #qunit-tests li li { 161 | padding: 5px; 162 | background-color: #fff; 163 | border-bottom: none; 164 | list-style-position: inside; 165 | } 166 | 167 | /*** Passing Styles */ 168 | 169 | #qunit-tests li li.pass { 170 | color: #3c510c; 171 | background-color: #fff; 172 | border-left: 10px solid #C6E746; 173 | } 174 | 175 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 176 | #qunit-tests .pass .test-name { color: #366097; } 177 | 178 | #qunit-tests .pass .test-actual, 179 | #qunit-tests .pass .test-expected { color: #999999; } 180 | 181 | #qunit-banner.qunit-pass { background-color: #C6E746; } 182 | 183 | /*** Failing Styles */ 184 | 185 | #qunit-tests li li.fail { 186 | color: #710909; 187 | background-color: #fff; 188 | border-left: 10px solid #EE5757; 189 | white-space: pre; 190 | } 191 | 192 | #qunit-tests > li:last-child { 193 | border-radius: 0 0 5px 5px; 194 | -moz-border-radius: 0 0 5px 5px; 195 | -webkit-border-bottom-right-radius: 5px; 196 | -webkit-border-bottom-left-radius: 5px; 197 | } 198 | 199 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 200 | #qunit-tests .fail .test-name, 201 | #qunit-tests .fail .module-name { color: #000000; } 202 | 203 | #qunit-tests .fail .test-actual { color: #EE5757; } 204 | #qunit-tests .fail .test-expected { color: green; } 205 | 206 | #qunit-banner.qunit-fail { background-color: #EE5757; } 207 | 208 | 209 | /** Result */ 210 | 211 | #qunit-testresult { 212 | padding: 0.5em 0.5em 0.5em 2.5em; 213 | 214 | color: #2b81af; 215 | background-color: #D2E0E6; 216 | 217 | border-bottom: 1px solid white; 218 | } 219 | #qunit-testresult .module-name { 220 | font-weight: bold; 221 | } 222 | 223 | /** Fixture */ 224 | 225 | #qunit-fixture { 226 | position: absolute; 227 | top: -10000px; 228 | left: -10000px; 229 | width: 1000px; 230 | height: 1000px; 231 | } 232 | -------------------------------------------------------------------------------- /colorpicker.min.js: -------------------------------------------------------------------------------- 1 | (function(s,t,u){var v=(s.SVGAngle||t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1")?"SVG":"VML"),picker,slide,hueOffset=15,svgNS='http://www.w3.org/2000/svg';var w=['
','
','
','
','
','
','
','
'].join('');function mousePosition(a){if(s.event&&s.event.contentOverflow!==u){return{x:s.event.offsetX,y:s.event.offsetY}}if(a.offsetX!==u&&a.offsetY!==u){return{x:a.offsetX,y:a.offsetY}}var b=a.target.parentNode.parentNode;return{x:a.layerX-b.offsetLeft,y:a.layerY-b.offsetTop}}function $(a,b,c){a=t.createElementNS(svgNS,a);for(var d in b)a.setAttribute(d,b[d]);if(Object.prototype.toString.call(c)!='[object Array]')c=[c];var i=0,len=(c[0]&&c.length)||0;for(;i','','','',''].join('');picker=['
','','','','','','','
'].join('');if(!t.namespaces['v'])t.namespaces.add('v','urn:schemas-microsoft-com:vml','#default#VML')}function hsv2rgb(a){var R,G,B,X,C;var h=(a.h%360)/60;C=a.v*a.s;X=C*(1-Math.abs(h%2-1));R=G=B=a.v-C;h=~~h;R+=[C,X,0,0,X,C][h];G+=[X,C,C,X,0,0][h];B+=[0,0,X,C,C,X][h];var r=Math.floor(R*255);var g=Math.floor(G*255);var b=Math.floor(B*255);return{r:r,g:g,b:b,hex:"#"+(16777216|b|(g<<8)|(r<<16)).toString(16).slice(1)}}function rgb2hsv(a){var r=a.r;var g=a.g;var b=a.b;if(a.r>1||a.g>1||a.b>1){r/=255;g/=255;b/=255}var H,S,V,C;V=Math.max(r,g,b);C=V-Math.min(r,g,b);H=(C==0?null:V==r?(g-b)/C+(g 2 | 3 | 4 | Flexi ColorPicker Documentation 5 | 27 | 28 | 29 |
30 |

Flexi ColorPicker Documentation

31 |

32 | Welcome to Flexi ColorPicker Documentation. This document describes 33 | the usage and API of the color picker. The library was built 34 | with simplicity in mind so there should not be anything what 35 | can surprise you. 36 |

37 |

Usage

38 |

The color picker is built on top of HSV color model. 39 | The only two parts of the picker is therefore 40 | the slider, which allows you to select "hue" and 41 | the picker for selecting "saturation" and "value". 42 | Both the slider and the picker have to be specified 43 | as HTML elements. The main advantage is that their 44 | dimensions can be set in CSS. 45 | 46 | The color picker is not dependent on any external library 47 | or CSS stylesheet. You get all the functionality by 48 | including only one tiny JavaScript file (see example below). 49 |

50 |

51 | Once you have your markup ready and CSS width and 52 | height set for both the slider and picker, the 53 | only thing you have to do is to instantiate 54 | the "ColorPicker" object. You can do that 55 | by calling the ColorPicker() function 56 | passing the slider, the picker element and 57 | callback as arguments. The callback is called 58 | whenever the color changes and it is passed 59 | color in hexadecimal, hsv and rgb formats. 60 | Hexadecimal format is the string used most commonly 61 | in CSS. Hsv and Rgb are objects with these properties: 62 | 63 | hsv: { h: /* hue [0,359] (angle) */, s: /* saturation [0,1] */, v: /* value [0,1] */ } 64 | rgb: { r: /* red [0,255] */, g: /* green [0,255] */, b: /* blue [0,255] */ } 65 | 66 |

67 |

Basic example

68 |
 69 |             <html>
 70 |               <head>
 71 |                 <script type="text/javascript" 
 72 |                         src="colorpicker.js"></script>
 73 |                 <style type="text/css">
 74 |                   #picker { width: 200px; height: 200px }
 75 |                   #slide { width: 30px; height: 200px }
 76 |                 </style>
 77 |               </head>
 78 |               <body>
 79 |                 <div id="picker"></div>
 80 |                 <div id="slide"></div>
 81 |                 <script type="text/javascript">
 82 |                   ColorPicker(
 83 |                     document.getElementById('slide'),
 84 |                     document.getElementById('picker'),
 85 |                     function(hex, hsv, rgb) {
 86 |                       console.log(hsv.h, hsv.s, hsv.v);
 87 |                       console.log(rbg.r, rgb.g, rgb.b);
 88 |                       document.body.style.backgroundColor = hex;
 89 |                     });
 90 |                 </script>
 91 |               </body>
 92 |             </html>
 93 |             
94 |

Note that you can set arbitrary dimensions, 95 | position, border and other CSS properties 96 | to the slider and picker element as you would 97 | do with any other HTML element on the page.

98 | 99 |

Advanced usage

100 | 101 |

Indicators

102 |

103 | Flexi color picker allows you to set indicators - elements indicating the currently 104 | selected color. Following the Flexi color picker philosophy, there is no built-in images 105 | for the indicators. Instead, user has freedom to use any element he wants. It could 106 | be an img tag, div tag or any other object which can then 107 | be styled in CSS the usual way.

108 |

109 | The user is expected to create a wrapper around the picker and picker indicator and another wrapper for the slide and slide indicator. 110 | The wrapper has to be a positioned element (in CSS: position relative/absolute/fixed). 111 | The ColorPicker passes coordinates of the indicators as arguments into 112 | the callback. The user can therefore set the left/top coordinate 113 | of an indicator inside the callback (assuming the indicator is absolutely positioned). 114 |

115 |

116 | As the common usage is to center the indicator around the indicator 117 | position, the ColorPicker provides a helper function to do that to save the user from typing. 118 | See below an example and implementation of the helper for the curious ones. 119 |

120 |
121 |             <style type="text/css">
122 |                 #picker-wrapper {
123 |                   width: 200px;
124 |                   height: 200px;
125 |                   position: relative;
126 |                 }
127 |                 #slide-wrapper {
128 |                   width: 30px;
129 |                   height: 200px;
130 |                   position: relative;
131 |                 }
132 |                 #picker-indicator {
133 |                   width: 3px;
134 |                   height: 3px;
135 |                   position: absolute;
136 |                   border: 1px solid white;
137 |                 }
138 |                 #slide-indicator {
139 |                   width: 100%;
140 |                   height: 10px;
141 |                   position: absolute;
142 |                   border: 1px solid black;
143 |                 }
144 |             </style>
145 |             <div id="picker-wrapper">
146 |                 <div id="picker"></div>
147 |                 <div id="picker-indicator"></div>
148 |             </div>
149 |             <div id="slide-wrapper">
150 |                 <div id="slide"></div>
151 |                 <div id="slide-indicator"></div>
152 |             </div>
153 |             <script type="text/javascript">
154 |             ColorPicker(document.getElementById('slide'), document.getElementById('picker'), 
155 |                 function(hex, hsv, rgb, mousePicker, mouseSlide) {
156 |                     ColorPicker.positionIndicators(
157 |                         document.getElementById('slide-indicator'),
158 |                         document.getElementById('picker-indicator'),
159 |                         mouseSlide, mousePicker
160 |                     );
161 |                     document.body.style.backgroundColor = hex;
162 |             });
163 |             </script>
164 |         
165 |

Implementation of the ColorPicker.positionIndicators

166 |

Note that it is not necessary to use the helper to position the indicators. 167 | As you can see below the implementation is straigtforward. 168 |

169 |             ColorPicker.positionIndicators = function(slideIndicator, pickerIndicator, slideIndicatorPosition, pickerIndicatorPosition) {
170 |                 if (slideIndicatorPosition) {
171 |                     pickerIndicator.style.left = '';    // reset the left coordinate
172 |                     pickerIndicator.style.top = '0px';
173 |                     pickerIndicator.style.right = '0px';
174 |                     slideIndicator.style.top = (slideIndicatorPosition.y - slideIndicator.offsetHeight/2) + 'px';
175 |                 }
176 |                 if (pickerIndicatorPosition) {
177 |                     pickerIndicator.style.top = (pickerIndicatorPosition.y - pickerIndicator.offsetHeight/2) + 'px';
178 |                     pickerIndicator.style.left = (pickerIndicatorPosition.x - pickerIndicator.offsetWidth/2) + 'px';
179 |                 } 
180 |             };
181 |         
182 | 183 |

Setting a color

184 |

185 | The color of the color picker can also be set in the program instead 186 | of by the user mouse clicks. The usage is straightforward. The ColorPicker 187 | instance provides three methods: setHsv(hsvObject), setRgb(rgbObject) 188 | and setHex(hexString). See example below as it describes it better. 189 |

190 |
191 |             var c = ColorPicker( ... );
192 |             c.setHsv({ h: 310, s: .3, v: .7 });       // hue is an angle <0..359>
193 |             c.setRgb({ r: 230, g: 150, b: 30 });      // integer range <0..255>
194 |             c.setRgb({ r: .7, g: .2, b: .5 });        // float range <0..1>
195 |             c.setHex('#223344');                      // usual HTML hex color string
196 |         
197 | 198 |
199 | 200 | 201 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Demo & discussion 2 | ================= 3 | 4 | Visit: [http://www.daviddurman.com/flexi-color-picker](http://www.daviddurman.com/flexi-color-picker) 5 | 6 | Features 7 | ======== 8 | 9 | - no Flash, images or 1px divs 10 | - no dependency on an external JavaScript library 11 | - dimensions of both the picker and the slider areas can be adjusted arbitrarily 12 | - minimalistic (about 400 loc including comments) 13 | - support for a large scale of browsers (including mobile browsers) 14 | - understandable (no magic behind, code can be easily read and therefore adjusted) 15 | - styleable in CSS 16 | - position of the slider and the picker areas is not hardcoded and can be changed in CSS 17 | - HSV, RGB and HEX output/input 18 | - indicators (pointers to the slider and picker areas) can be arbitrary HTML elements (images, divs, spans, ...) styleable in CSS 19 | - ready-to-use themes (stored in one CSS stylesheet) 20 | 21 | 22 | Description 23 | =========== 24 | 25 | FlexiColorPicker is based on HSV color model. The only two parts of the color picker are therefore 26 | the **slider** for selecting _hue_ value and the **picker** for selecting _saturation_ and _value_. Both 27 | the **slider** and **picker** are HTML elements (usually `
`'s) that serve as wrappers for SVG/VML gradient 28 | rectangles. The **slider** gradient rectangle represents the _hue_ value (gradient with 9 `stop-color`s). The 29 | two overlapping black and white gradient rectangles of the **picker** represent the _saturation_ and _value_ values. 30 | The top level elements (`` in case of SVG enabled browser and `
` in case of VML enabled browser) 31 | that wrap each of the **slider** and **picker** gradient rectangles have set `width` and `height` to `100%` which 32 | means that the color picker components (slider and picker) adjust themselfs automatically to the dimensions of the **slider** and **picker** 33 | HTML elements. 34 | 35 | 36 | API 37 | === 38 | 39 | **`ColorPicker(colorPickerElement, function(hex, hsv, rgb) { /*do something when the color changes */ })`** 40 | 41 | This is the no-hassle form of creating the color picker. This is the recommended call. 42 | 43 | Example: 44 | 45 |
46 | 51 | 52 | 53 | **`ColorPicker.prototype.setHsv(hsv)`** 54 | 55 | Sets HSV value. 56 | 57 | Example: 58 | 59 | var cp = ColorPicker(document.getElementById('mycolorpicker'), function() {}); 60 | cp.setHsv({ h: 180, s: .2, v: .7 }); 61 | 62 | **`ColorPicker.prototype.setRgb(rgb)`** 63 | 64 | Sets RGB value. 65 | 66 | Example: 67 | 68 | var cp = ColorPicker(document.getElementById('mycolorpicker'), function() {}); 69 | cp.setRgb({ r: 120, g: 205, b: 18 }); 70 | 71 | 72 | **`ColorPicker.prototype.setHex(hex)`** 73 | 74 | Sets HEX value. 75 | 76 | Example: 77 | 78 | var cp = ColorPicker(document.getElementById('mycolorpicker'), function() {}); 79 | cp.setHex('#AB12FE'); 80 | 81 | 82 | **`ColorPicker.positionIndicators(sliderIndicator, pickerIndicator, sliderCoordinate, pickerCoordinate)`** 83 | 84 | Positions indicators in the slider and the picker. This is a helper function that is supposed to be called 85 | in the callback function passed as the fourth argument to the `ColorPicker` function. The only thing it does is 86 | setting the `top` and `left` CSS coordinate on the `sliderIndicator` and `pickerIndicator` HTML elements. 87 | If you use the no-hassle form (see above), you don't have to deal with this function at all. 88 | 89 | Example: 90 | 91 | ColorPicker( 92 | document.getElementById('slider'), 93 | document.getElementById('picker'), 94 | 95 | function(hex, hsv, rgb, pickerCoordinate, sliderCoordinate) { 96 | 97 | ColorPicker.positionIndicators( 98 | document.getElementById('slider-indicator'), 99 | document.getElementById('picker-indicator'), 100 | sliderCoordinate, pickerCoordinate 101 | ); 102 | 103 | document.body.style.backgroundColor = hex; 104 | }); 105 | 106 | 107 | **`ColorPicker.fixIndicators(sliderIndicator, pickerIndicator)`** 108 | 109 | This helper function just sets the `pointer-events` CSS property to `'none'` on both the `sliderIndicator` and 110 | `pickerIndicator`. This is necessary otherwise any mouse event (click, mousedown, mousemove) triggered 111 | on the `sliderIndicator` or `pickerIndicator` HTML elements would be cought instead of bypassed to 112 | the slider and the picker area, hence preventing the color picker to catch these UI events in order to change 113 | color. As `pointer-events` CSS property is not supported in all browsers, this function might workaround 114 | this issue in the future. At this time, setting `pointer-events: none` in CSS on the slider and picker indicators 115 | is equivalent. 116 | 117 | Again, if you use the no-hassle form (see above), you don't have to deal with this function at all. 118 | 119 | 120 | Examples 121 | ======== 122 | 123 | The basic example demonstrates the minimalism of the FlexiColorPicker. More useful examples follow. 124 | 125 | Basic 126 | ----- 127 | 128 | 129 | 130 | 131 | 135 | 136 | 137 | 138 |
139 |
140 | 141 | 155 | 156 | 157 | 158 | 159 | Note that you can set arbitrary dimensions, position, border and other CSS properties on the slider and picker 160 | elements as you would do with any other HTML element on the page. 161 | 162 | 163 | Advanced 164 | -------- 165 | 166 | This is an advanced example showing how to work with custom indicators. 167 | 168 | 169 | 170 | 171 | 196 | 197 | 198 | 199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 | 208 | 230 | 231 | 232 | 233 | 234 | 235 | Note how the indicators work. There is no built-in indicators in FlexiColorPicker, instead, the user 236 | has a freedom to set their own indicators as normal HTML elements styled in CSS (or use one of the ready-to-use themes packaged with FlexiColorPicker). 237 | 238 | 239 | No hassle 240 | --------- 241 | 242 | If you don't want to deal with any of the above mentioned details and you're just looking for a copy-paste 243 | (one function call-like) color picker, see this example. 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 |
253 | 254 | 267 | 268 | 269 | 270 | 271 | The ColorPicker function has recognized only two arguments which means that it builds the HTML needed for you 272 | and also fixes and positions indicators automatically. 273 | 274 | 275 | License 276 | ======== 277 | 278 | FlexiColorPicker is licensed under the MIT license: 279 | 280 | Copyright (c) 2011 - 2012 David Durman 281 | 282 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 283 | 284 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 285 | 286 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 287 | 288 | 289 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/DavidDurman/flexicolorpicker/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 290 | 291 | -------------------------------------------------------------------------------- /colorpicker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ColorPicker - pure JavaScript color picker without using images, external CSS or 1px divs. 3 | * Copyright © 2011 David Durman, All rights reserved. 4 | */ 5 | (function(window, document, undefined) { 6 | 7 | var type = (window.SVGAngle || document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure", "1.1") ? "SVG" : "VML"), 8 | picker, slide, hueOffset = 15, svgNS = 'http://www.w3.org/2000/svg'; 9 | 10 | // This HTML snippet is inserted into the innerHTML property of the passed color picker element 11 | // when the no-hassle call to ColorPicker() is used, i.e. ColorPicker(function(hex, hsv, rgb) { ... }); 12 | 13 | var colorpickerHTMLSnippet = [ 14 | 15 | '
', 16 | '
', 17 | '
', 18 | '
', 19 | '
', 20 | '
', 21 | '
', 22 | '
' 23 | 24 | ].join(''); 25 | 26 | /** 27 | * Return mouse position relative to the element el. 28 | */ 29 | function mousePosition(evt) { 30 | // IE: 31 | if (window.event && window.event.contentOverflow !== undefined) { 32 | return { x: window.event.offsetX, y: window.event.offsetY }; 33 | } 34 | // Webkit: 35 | if (evt.offsetX !== undefined && evt.offsetY !== undefined) { 36 | return { x: evt.offsetX, y: evt.offsetY }; 37 | } 38 | // Firefox: 39 | var wrapper = evt.target.parentNode.parentNode; 40 | return { x: evt.layerX - wrapper.offsetLeft, y: evt.layerY - wrapper.offsetTop }; 41 | } 42 | 43 | /** 44 | * Create SVG element. 45 | */ 46 | function $(el, attrs, children) { 47 | el = document.createElementNS(svgNS, el); 48 | for (var key in attrs) 49 | el.setAttribute(key, attrs[key]); 50 | if (Object.prototype.toString.call(children) != '[object Array]') children = [children]; 51 | var i = 0, len = (children[0] && children.length) || 0; 52 | for (; i < len; i++) 53 | el.appendChild(children[i]); 54 | return el; 55 | } 56 | 57 | /** 58 | * Create slide and picker markup depending on the supported technology. 59 | */ 60 | if (type == 'SVG') { 61 | 62 | slide = $('svg', { xmlns: 'http://www.w3.org/2000/svg', version: '1.1', width: '100%', height: '100%' }, 63 | [ 64 | $('defs', {}, 65 | $('linearGradient', { id: 'gradient-hsv', x1: '0%', y1: '100%', x2: '0%', y2: '0%'}, 66 | [ 67 | $('stop', { offset: '0%', 'stop-color': '#FF0000', 'stop-opacity': '1' }), 68 | $('stop', { offset: '13%', 'stop-color': '#FF00FF', 'stop-opacity': '1' }), 69 | $('stop', { offset: '25%', 'stop-color': '#8000FF', 'stop-opacity': '1' }), 70 | $('stop', { offset: '38%', 'stop-color': '#0040FF', 'stop-opacity': '1' }), 71 | $('stop', { offset: '50%', 'stop-color': '#00FFFF', 'stop-opacity': '1' }), 72 | $('stop', { offset: '63%', 'stop-color': '#00FF40', 'stop-opacity': '1' }), 73 | $('stop', { offset: '75%', 'stop-color': '#0BED00', 'stop-opacity': '1' }), 74 | $('stop', { offset: '88%', 'stop-color': '#FFFF00', 'stop-opacity': '1' }), 75 | $('stop', { offset: '100%', 'stop-color': '#FF0000', 'stop-opacity': '1' }) 76 | ] 77 | ) 78 | ), 79 | $('rect', { x: '0', y: '0', width: '100%', height: '100%', fill: 'url(#gradient-hsv)'}) 80 | ] 81 | ); 82 | 83 | picker = $('svg', { xmlns: 'http://www.w3.org/2000/svg', version: '1.1', width: '100%', height: '100%' }, 84 | [ 85 | $('defs', {}, 86 | [ 87 | $('linearGradient', { id: 'gradient-black', x1: '0%', y1: '100%', x2: '0%', y2: '0%'}, 88 | [ 89 | $('stop', { offset: '0%', 'stop-color': '#000000', 'stop-opacity': '1' }), 90 | $('stop', { offset: '100%', 'stop-color': '#CC9A81', 'stop-opacity': '0' }) 91 | ] 92 | ), 93 | $('linearGradient', { id: 'gradient-white', x1: '0%', y1: '100%', x2: '100%', y2: '100%'}, 94 | [ 95 | $('stop', { offset: '0%', 'stop-color': '#FFFFFF', 'stop-opacity': '1' }), 96 | $('stop', { offset: '100%', 'stop-color': '#CC9A81', 'stop-opacity': '0' }) 97 | ] 98 | ) 99 | ] 100 | ), 101 | $('rect', { x: '0', y: '0', width: '100%', height: '100%', fill: 'url(#gradient-white)'}), 102 | $('rect', { x: '0', y: '0', width: '100%', height: '100%', fill: 'url(#gradient-black)'}) 103 | ] 104 | ); 105 | 106 | } else if (type == 'VML') { 107 | slide = [ 108 | '
', 109 | '', 110 | '', 111 | '', 112 | '
' 113 | ].join(''); 114 | 115 | picker = [ 116 | '
', 117 | '', 118 | '', 119 | '', 120 | '', 121 | '', 122 | '', 123 | '
' 124 | ].join(''); 125 | 126 | if (!document.namespaces['v']) 127 | document.namespaces.add('v', 'urn:schemas-microsoft-com:vml', '#default#VML'); 128 | } 129 | 130 | /** 131 | * Convert HSV representation to RGB HEX string. 132 | * Credits to http://www.raphaeljs.com 133 | */ 134 | function hsv2rgb(hsv) { 135 | var R, G, B, X, C; 136 | var h = (hsv.h % 360) / 60; 137 | 138 | C = hsv.v * hsv.s; 139 | X = C * (1 - Math.abs(h % 2 - 1)); 140 | R = G = B = hsv.v - C; 141 | 142 | h = ~~h; 143 | R += [C, X, 0, 0, X, C][h]; 144 | G += [X, C, C, X, 0, 0][h]; 145 | B += [0, 0, X, C, C, X][h]; 146 | 147 | var r = Math.floor(R * 255); 148 | var g = Math.floor(G * 255); 149 | var b = Math.floor(B * 255); 150 | return { r: r, g: g, b: b, hex: "#" + (16777216 | b | (g << 8) | (r << 16)).toString(16).slice(1) }; 151 | } 152 | 153 | /** 154 | * Convert RGB representation to HSV. 155 | * r, g, b can be either in <0,1> range or <0,255> range. 156 | * Credits to http://www.raphaeljs.com 157 | */ 158 | function rgb2hsv(rgb) { 159 | 160 | var r = rgb.r; 161 | var g = rgb.g; 162 | var b = rgb.b; 163 | 164 | if (rgb.r > 1 || rgb.g > 1 || rgb.b > 1) { 165 | r /= 255; 166 | g /= 255; 167 | b /= 255; 168 | } 169 | 170 | var H, S, V, C; 171 | V = Math.max(r, g, b); 172 | C = V - Math.min(r, g, b); 173 | H = (C == 0 ? null : 174 | V == r ? (g - b) / C + (g < b ? 6 : 0) : 175 | V == g ? (b - r) / C + 2 : 176 | (r - g) / C + 4); 177 | H = (H % 6) * 60; 178 | S = C == 0 ? 0 : C / V; 179 | return { h: H, s: S, v: V }; 180 | } 181 | 182 | /** 183 | * Return click event handler for the slider. 184 | * Sets picker background color and calls ctx.callback if provided. 185 | */ 186 | function slideListener(ctx, slideElement, pickerElement) { 187 | return function(evt) { 188 | evt = evt || window.event; 189 | var mouse = mousePosition(evt); 190 | ctx.h = mouse.y / slideElement.offsetHeight * 360 + hueOffset; 191 | var pickerColor = hsv2rgb({ h: ctx.h, s: 1, v: 1 }); 192 | var c = hsv2rgb({ h: ctx.h, s: ctx.s, v: ctx.v }); 193 | pickerElement.style.backgroundColor = pickerColor.hex; 194 | ctx.callback && ctx.callback(c.hex, { h: ctx.h - hueOffset, s: ctx.s, v: ctx.v }, { r: c.r, g: c.g, b: c.b }, undefined, mouse); 195 | } 196 | }; 197 | 198 | /** 199 | * Return click event handler for the picker. 200 | * Calls ctx.callback if provided. 201 | */ 202 | function pickerListener(ctx, pickerElement) { 203 | return function(evt) { 204 | evt = evt || window.event; 205 | var mouse = mousePosition(evt), 206 | width = pickerElement.offsetWidth, 207 | height = pickerElement.offsetHeight; 208 | 209 | ctx.s = mouse.x / width; 210 | ctx.v = (height - mouse.y) / height; 211 | var c = hsv2rgb(ctx); 212 | ctx.callback && ctx.callback(c.hex, { h: ctx.h - hueOffset, s: ctx.s, v: ctx.v }, { r: c.r, g: c.g, b: c.b }, mouse); 213 | } 214 | }; 215 | 216 | var uniqID = 0; 217 | 218 | /** 219 | * ColorPicker. 220 | * @param {DOMElement} slideElement HSV slide element. 221 | * @param {DOMElement} pickerElement HSV picker element. 222 | * @param {Function} callback Called whenever the color is changed provided chosen color in RGB HEX format as the only argument. 223 | */ 224 | function ColorPicker(slideElement, pickerElement, callback) { 225 | 226 | if (!(this instanceof ColorPicker)) return new ColorPicker(slideElement, pickerElement, callback); 227 | 228 | this.h = 0; 229 | this.s = 1; 230 | this.v = 1; 231 | 232 | if (!callback) { 233 | // call of the form ColorPicker(element, funtion(hex, hsv, rgb) { ... }), i.e. the no-hassle call. 234 | 235 | var element = slideElement; 236 | element.innerHTML = colorpickerHTMLSnippet; 237 | 238 | this.slideElement = element.getElementsByClassName('slide')[0]; 239 | this.pickerElement = element.getElementsByClassName('picker')[0]; 240 | var slideIndicator = element.getElementsByClassName('slide-indicator')[0]; 241 | var pickerIndicator = element.getElementsByClassName('picker-indicator')[0]; 242 | 243 | ColorPicker.fixIndicators(slideIndicator, pickerIndicator); 244 | 245 | this.callback = function(hex, hsv, rgb, pickerCoordinate, slideCoordinate) { 246 | 247 | ColorPicker.positionIndicators(slideIndicator, pickerIndicator, slideCoordinate, pickerCoordinate); 248 | 249 | pickerElement(hex, hsv, rgb); 250 | }; 251 | 252 | } else { 253 | 254 | this.callback = callback; 255 | this.pickerElement = pickerElement; 256 | this.slideElement = slideElement; 257 | } 258 | 259 | if (type == 'SVG') { 260 | 261 | // Generate uniq IDs for linearGradients so that we don't have the same IDs within one document. 262 | // Then reference those gradients in the associated rectangles. 263 | 264 | var slideClone = slide.cloneNode(true); 265 | var pickerClone = picker.cloneNode(true); 266 | 267 | var hsvGradient = slideClone.getElementsByTagName('linearGradient')[0]; 268 | 269 | var hsvRect = slideClone.getElementsByTagName('rect')[0]; 270 | 271 | hsvGradient.id = 'gradient-hsv-' + uniqID; 272 | hsvRect.setAttribute('fill', 'url(#' + hsvGradient.id + ')'); 273 | 274 | var blackAndWhiteGradients = [pickerClone.getElementsByTagName('linearGradient')[0], pickerClone.getElementsByTagName('linearGradient')[1]]; 275 | var whiteAndBlackRects = pickerClone.getElementsByTagName('rect'); 276 | 277 | blackAndWhiteGradients[0].id = 'gradient-black-' + uniqID; 278 | blackAndWhiteGradients[1].id = 'gradient-white-' + uniqID; 279 | 280 | whiteAndBlackRects[0].setAttribute('fill', 'url(#' + blackAndWhiteGradients[1].id + ')'); 281 | whiteAndBlackRects[1].setAttribute('fill', 'url(#' + blackAndWhiteGradients[0].id + ')'); 282 | 283 | this.slideElement.appendChild(slideClone); 284 | this.pickerElement.appendChild(pickerClone); 285 | 286 | uniqID++; 287 | 288 | } else { 289 | 290 | this.slideElement.innerHTML = slide; 291 | this.pickerElement.innerHTML = picker; 292 | } 293 | 294 | addEventListener(this.slideElement, 'click', slideListener(this, this.slideElement, this.pickerElement)); 295 | addEventListener(this.pickerElement, 'click', pickerListener(this, this.pickerElement)); 296 | 297 | enableDragging(this, this.slideElement, slideListener(this, this.slideElement, this.pickerElement)); 298 | enableDragging(this, this.pickerElement, pickerListener(this, this.pickerElement)); 299 | }; 300 | 301 | function addEventListener(element, event, listener) { 302 | 303 | if (element.attachEvent) { 304 | 305 | element.attachEvent('on' + event, listener); 306 | 307 | } else if (element.addEventListener) { 308 | 309 | element.addEventListener(event, listener, false); 310 | } 311 | } 312 | 313 | /** 314 | * Enable drag&drop color selection. 315 | * @param {object} ctx ColorPicker instance. 316 | * @param {DOMElement} element HSV slide element or HSV picker element. 317 | * @param {Function} listener Function that will be called whenever mouse is dragged over the element with event object as argument. 318 | */ 319 | function enableDragging(ctx, element, listener) { 320 | 321 | var mousedown = false; 322 | 323 | addEventListener(element, 'mousedown', function(evt) { mousedown = true; }); 324 | addEventListener(element, 'mouseup', function(evt) { mousedown = false; }); 325 | addEventListener(element, 'mouseout', function(evt) { mousedown = false; }); 326 | addEventListener(element, 'mousemove', function(evt) { 327 | 328 | if (mousedown) { 329 | 330 | listener(evt); 331 | } 332 | }); 333 | } 334 | 335 | 336 | ColorPicker.hsv2rgb = function(hsv) { 337 | var rgbHex = hsv2rgb(hsv); 338 | delete rgbHex.hex; 339 | return rgbHex; 340 | }; 341 | 342 | ColorPicker.hsv2hex = function(hsv) { 343 | return hsv2rgb(hsv).hex; 344 | }; 345 | 346 | ColorPicker.rgb2hsv = rgb2hsv; 347 | 348 | ColorPicker.rgb2hex = function(rgb) { 349 | return hsv2rgb(rgb2hsv(rgb)).hex; 350 | }; 351 | 352 | ColorPicker.hex2hsv = function(hex) { 353 | return rgb2hsv(ColorPicker.hex2rgb(hex)); 354 | }; 355 | 356 | ColorPicker.hex2rgb = function(hex) { 357 | return { r: parseInt(hex.substr(1, 2), 16), g: parseInt(hex.substr(3, 2), 16), b: parseInt(hex.substr(5, 2), 16) }; 358 | }; 359 | 360 | /** 361 | * Sets color of the picker in hsv/rgb/hex format. 362 | * @param {object} ctx ColorPicker instance. 363 | * @param {object} hsv Object of the form: { h: , s: , v: }. 364 | * @param {object} rgb Object of the form: { r: , g: , b: }. 365 | * @param {string} hex String of the form: #RRGGBB. 366 | */ 367 | function setColor(ctx, hsv, rgb, hex) { 368 | ctx.h = hsv.h % 360; 369 | ctx.s = hsv.s; 370 | ctx.v = hsv.v; 371 | 372 | var c = hsv2rgb(ctx); 373 | 374 | var mouseSlide = { 375 | y: (ctx.h * ctx.slideElement.offsetHeight) / 360, 376 | x: 0 // not important 377 | }; 378 | 379 | var pickerHeight = ctx.pickerElement.offsetHeight; 380 | 381 | var mousePicker = { 382 | x: ctx.s * ctx.pickerElement.offsetWidth, 383 | y: pickerHeight - ctx.v * pickerHeight 384 | }; 385 | 386 | ctx.pickerElement.style.backgroundColor = hsv2rgb({ h: ctx.h, s: 1, v: 1 }).hex; 387 | ctx.callback && ctx.callback(hex || c.hex, { h: ctx.h, s: ctx.s, v: ctx.v }, rgb || { r: c.r, g: c.g, b: c.b }, mousePicker, mouseSlide); 388 | 389 | return ctx; 390 | }; 391 | 392 | /** 393 | * Sets color of the picker in hsv format. 394 | * @param {object} hsv Object of the form: { h: , s: , v: }. 395 | */ 396 | ColorPicker.prototype.setHsv = function(hsv) { 397 | return setColor(this, hsv); 398 | }; 399 | 400 | /** 401 | * Sets color of the picker in rgb format. 402 | * @param {object} rgb Object of the form: { r: , g: , b: }. 403 | */ 404 | ColorPicker.prototype.setRgb = function(rgb) { 405 | return setColor(this, rgb2hsv(rgb), rgb); 406 | }; 407 | 408 | /** 409 | * Sets color of the picker in hex format. 410 | * @param {string} hex Hex color format #RRGGBB. 411 | */ 412 | ColorPicker.prototype.setHex = function(hex) { 413 | return setColor(this, ColorPicker.hex2hsv(hex), undefined, hex); 414 | }; 415 | 416 | /** 417 | * Helper to position indicators. 418 | * @param {HTMLElement} slideIndicator DOM element representing the indicator of the slide area. 419 | * @param {HTMLElement} pickerIndicator DOM element representing the indicator of the picker area. 420 | * @param {object} mouseSlide Coordinates of the mouse cursor in the slide area. 421 | * @param {object} mousePicker Coordinates of the mouse cursor in the picker area. 422 | */ 423 | ColorPicker.positionIndicators = function(slideIndicator, pickerIndicator, mouseSlide, mousePicker) { 424 | 425 | if (mouseSlide) { 426 | slideIndicator.style.top = (mouseSlide.y - slideIndicator.offsetHeight/2) + 'px'; 427 | } 428 | if (mousePicker) { 429 | pickerIndicator.style.top = (mousePicker.y - pickerIndicator.offsetHeight/2) + 'px'; 430 | pickerIndicator.style.left = (mousePicker.x - pickerIndicator.offsetWidth/2) + 'px'; 431 | } 432 | }; 433 | 434 | /** 435 | * Helper to fix indicators - this is recommended (and needed) for dragable color selection (see enabledDragging()). 436 | */ 437 | ColorPicker.fixIndicators = function(slideIndicator, pickerIndicator) { 438 | 439 | pickerIndicator.style.pointerEvents = 'none'; 440 | slideIndicator.style.pointerEvents = 'none'; 441 | }; 442 | 443 | window.ColorPicker = ColorPicker; 444 | 445 | })(window, window.document); 446 | -------------------------------------------------------------------------------- /test/qunit/qunit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.10.0pre - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright 2012 jQuery Foundation and other contributors 7 | * Dual licensed under the MIT or GPL Version 2 licenses. 8 | * http://jquery.org/license 9 | */ 10 | 11 | (function( window ) { 12 | 13 | var QUnit, 14 | config, 15 | onErrorFnPrev, 16 | testId = 0, 17 | fileName = (sourceFromStacktrace( 0 ) || "" ).replace(/(:\d+)+\)?/, "").replace(/.+\//, ""), 18 | toString = Object.prototype.toString, 19 | hasOwn = Object.prototype.hasOwnProperty, 20 | // Keep a local reference to Date (GH-283) 21 | Date = window.Date, 22 | defined = { 23 | setTimeout: typeof window.setTimeout !== "undefined", 24 | sessionStorage: (function() { 25 | var x = "qunit-test-string"; 26 | try { 27 | sessionStorage.setItem( x, x ); 28 | sessionStorage.removeItem( x ); 29 | return true; 30 | } catch( e ) { 31 | return false; 32 | } 33 | }()) 34 | }; 35 | 36 | function Test( settings ) { 37 | extend( this, settings ); 38 | this.assertions = []; 39 | this.testNumber = ++Test.count; 40 | } 41 | 42 | Test.count = 0; 43 | 44 | Test.prototype = { 45 | init: function() { 46 | var a, b, li, 47 | tests = id( "qunit-tests" ); 48 | 49 | if ( tests ) { 50 | b = document.createElement( "strong" ); 51 | b.innerHTML = this.name; 52 | 53 | // `a` initialized at top of scope 54 | a = document.createElement( "a" ); 55 | a.innerHTML = "Rerun"; 56 | a.href = QUnit.url({ testNumber: this.testNumber }); 57 | 58 | li = document.createElement( "li" ); 59 | li.appendChild( b ); 60 | li.appendChild( a ); 61 | li.className = "running"; 62 | li.id = this.id = "qunit-test-output" + testId++; 63 | 64 | tests.appendChild( li ); 65 | } 66 | }, 67 | setup: function() { 68 | if ( this.module !== config.previousModule ) { 69 | if ( config.previousModule ) { 70 | runLoggingCallbacks( "moduleDone", QUnit, { 71 | name: config.previousModule, 72 | failed: config.moduleStats.bad, 73 | passed: config.moduleStats.all - config.moduleStats.bad, 74 | total: config.moduleStats.all 75 | }); 76 | } 77 | config.previousModule = this.module; 78 | config.moduleStats = { all: 0, bad: 0 }; 79 | runLoggingCallbacks( "moduleStart", QUnit, { 80 | name: this.module 81 | }); 82 | } else if ( config.autorun ) { 83 | runLoggingCallbacks( "moduleStart", QUnit, { 84 | name: this.module 85 | }); 86 | } 87 | 88 | config.current = this; 89 | 90 | this.testEnvironment = extend({ 91 | setup: function() {}, 92 | teardown: function() {} 93 | }, this.moduleTestEnvironment ); 94 | 95 | runLoggingCallbacks( "testStart", QUnit, { 96 | name: this.testName, 97 | module: this.module 98 | }); 99 | 100 | // allow utility functions to access the current test environment 101 | // TODO why?? 102 | QUnit.current_testEnvironment = this.testEnvironment; 103 | 104 | if ( !config.pollution ) { 105 | saveGlobal(); 106 | } 107 | if ( config.notrycatch ) { 108 | this.testEnvironment.setup.call( this.testEnvironment ); 109 | return; 110 | } 111 | try { 112 | this.testEnvironment.setup.call( this.testEnvironment ); 113 | } catch( e ) { 114 | QUnit.pushFailure( "Setup failed on " + this.testName + ": " + e.message, extractStacktrace( e, 1 ) ); 115 | } 116 | }, 117 | run: function() { 118 | config.current = this; 119 | 120 | var running = id( "qunit-testresult" ); 121 | 122 | if ( running ) { 123 | running.innerHTML = "Running:
" + this.name; 124 | } 125 | 126 | if ( this.async ) { 127 | QUnit.stop(); 128 | } 129 | 130 | if ( config.notrycatch ) { 131 | this.callback.call( this.testEnvironment, QUnit.assert ); 132 | return; 133 | } 134 | 135 | try { 136 | this.callback.call( this.testEnvironment, QUnit.assert ); 137 | } catch( e ) { 138 | QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + e.message, extractStacktrace( e, 0 ) ); 139 | // else next test will carry the responsibility 140 | saveGlobal(); 141 | 142 | // Restart the tests if they're blocking 143 | if ( config.blocking ) { 144 | QUnit.start(); 145 | } 146 | } 147 | }, 148 | teardown: function() { 149 | config.current = this; 150 | if ( config.notrycatch ) { 151 | this.testEnvironment.teardown.call( this.testEnvironment ); 152 | return; 153 | } else { 154 | try { 155 | this.testEnvironment.teardown.call( this.testEnvironment ); 156 | } catch( e ) { 157 | QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + e.message, extractStacktrace( e, 1 ) ); 158 | } 159 | } 160 | checkPollution(); 161 | }, 162 | finish: function() { 163 | config.current = this; 164 | if ( config.requireExpects && this.expected == null ) { 165 | QUnit.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack ); 166 | } else if ( this.expected != null && this.expected != this.assertions.length ) { 167 | QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack ); 168 | } else if ( this.expected == null && !this.assertions.length ) { 169 | QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack ); 170 | } 171 | 172 | var assertion, a, b, i, li, ol, 173 | test = this, 174 | good = 0, 175 | bad = 0, 176 | tests = id( "qunit-tests" ); 177 | 178 | config.stats.all += this.assertions.length; 179 | config.moduleStats.all += this.assertions.length; 180 | 181 | if ( tests ) { 182 | ol = document.createElement( "ol" ); 183 | 184 | for ( i = 0; i < this.assertions.length; i++ ) { 185 | assertion = this.assertions[i]; 186 | 187 | li = document.createElement( "li" ); 188 | li.className = assertion.result ? "pass" : "fail"; 189 | li.innerHTML = assertion.message || ( assertion.result ? "okay" : "failed" ); 190 | ol.appendChild( li ); 191 | 192 | if ( assertion.result ) { 193 | good++; 194 | } else { 195 | bad++; 196 | config.stats.bad++; 197 | config.moduleStats.bad++; 198 | } 199 | } 200 | 201 | // store result when possible 202 | if ( QUnit.config.reorder && defined.sessionStorage ) { 203 | if ( bad ) { 204 | sessionStorage.setItem( "qunit-test-" + this.module + "-" + this.testName, bad ); 205 | } else { 206 | sessionStorage.removeItem( "qunit-test-" + this.module + "-" + this.testName ); 207 | } 208 | } 209 | 210 | if ( bad === 0 ) { 211 | ol.style.display = "none"; 212 | } 213 | 214 | // `b` initialized at top of scope 215 | b = document.createElement( "strong" ); 216 | b.innerHTML = this.name + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; 217 | 218 | addEvent(b, "click", function() { 219 | var next = b.nextSibling.nextSibling, 220 | display = next.style.display; 221 | next.style.display = display === "none" ? "block" : "none"; 222 | }); 223 | 224 | addEvent(b, "dblclick", function( e ) { 225 | var target = e && e.target ? e.target : window.event.srcElement; 226 | if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { 227 | target = target.parentNode; 228 | } 229 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 230 | window.location = QUnit.url({ testNumber: test.testNumber }); 231 | } 232 | }); 233 | 234 | // `li` initialized at top of scope 235 | li = id( this.id ); 236 | li.className = bad ? "fail" : "pass"; 237 | li.removeChild( li.firstChild ); 238 | a = li.firstChild; 239 | li.appendChild( b ); 240 | li.appendChild ( a ); 241 | li.appendChild( ol ); 242 | 243 | } else { 244 | for ( i = 0; i < this.assertions.length; i++ ) { 245 | if ( !this.assertions[i].result ) { 246 | bad++; 247 | config.stats.bad++; 248 | config.moduleStats.bad++; 249 | } 250 | } 251 | } 252 | 253 | runLoggingCallbacks( "testDone", QUnit, { 254 | name: this.testName, 255 | module: this.module, 256 | failed: bad, 257 | passed: this.assertions.length - bad, 258 | total: this.assertions.length 259 | }); 260 | 261 | QUnit.reset(); 262 | 263 | config.current = undefined; 264 | }, 265 | 266 | queue: function() { 267 | var bad, 268 | test = this; 269 | 270 | synchronize(function() { 271 | test.init(); 272 | }); 273 | function run() { 274 | // each of these can by async 275 | synchronize(function() { 276 | test.setup(); 277 | }); 278 | synchronize(function() { 279 | test.run(); 280 | }); 281 | synchronize(function() { 282 | test.teardown(); 283 | }); 284 | synchronize(function() { 285 | test.finish(); 286 | }); 287 | } 288 | 289 | // `bad` initialized at top of scope 290 | // defer when previous test run passed, if storage is available 291 | bad = QUnit.config.reorder && defined.sessionStorage && 292 | +sessionStorage.getItem( "qunit-test-" + this.module + "-" + this.testName ); 293 | 294 | if ( bad ) { 295 | run(); 296 | } else { 297 | synchronize( run, true ); 298 | } 299 | } 300 | }; 301 | 302 | // Root QUnit object. 303 | // `QUnit` initialized at top of scope 304 | QUnit = { 305 | 306 | // call on start of module test to prepend name to all tests 307 | module: function( name, testEnvironment ) { 308 | config.currentModule = name; 309 | config.currentModuleTestEnviroment = testEnvironment; 310 | }, 311 | 312 | asyncTest: function( testName, expected, callback ) { 313 | if ( arguments.length === 2 ) { 314 | callback = expected; 315 | expected = null; 316 | } 317 | 318 | QUnit.test( testName, expected, callback, true ); 319 | }, 320 | 321 | test: function( testName, expected, callback, async ) { 322 | var test, 323 | name = "" + escapeInnerText( testName ) + ""; 324 | 325 | if ( arguments.length === 2 ) { 326 | callback = expected; 327 | expected = null; 328 | } 329 | 330 | if ( config.currentModule ) { 331 | name = "" + config.currentModule + ": " + name; 332 | } 333 | 334 | test = new Test({ 335 | name: name, 336 | testName: testName, 337 | expected: expected, 338 | async: async, 339 | callback: callback, 340 | module: config.currentModule, 341 | moduleTestEnvironment: config.currentModuleTestEnviroment, 342 | stack: sourceFromStacktrace( 2 ) 343 | }); 344 | 345 | if ( !validTest( test ) ) { 346 | return; 347 | } 348 | 349 | test.queue(); 350 | }, 351 | 352 | // Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. 353 | expect: function( asserts ) { 354 | config.current.expected = asserts; 355 | }, 356 | 357 | start: function( count ) { 358 | config.semaphore -= count || 1; 359 | // don't start until equal number of stop-calls 360 | if ( config.semaphore > 0 ) { 361 | return; 362 | } 363 | // ignore if start is called more often then stop 364 | if ( config.semaphore < 0 ) { 365 | config.semaphore = 0; 366 | } 367 | // A slight delay, to avoid any current callbacks 368 | if ( defined.setTimeout ) { 369 | window.setTimeout(function() { 370 | if ( config.semaphore > 0 ) { 371 | return; 372 | } 373 | if ( config.timeout ) { 374 | clearTimeout( config.timeout ); 375 | } 376 | 377 | config.blocking = false; 378 | process( true ); 379 | }, 13); 380 | } else { 381 | config.blocking = false; 382 | process( true ); 383 | } 384 | }, 385 | 386 | stop: function( count ) { 387 | config.semaphore += count || 1; 388 | config.blocking = true; 389 | 390 | if ( config.testTimeout && defined.setTimeout ) { 391 | clearTimeout( config.timeout ); 392 | config.timeout = window.setTimeout(function() { 393 | QUnit.ok( false, "Test timed out" ); 394 | config.semaphore = 1; 395 | QUnit.start(); 396 | }, config.testTimeout ); 397 | } 398 | } 399 | }; 400 | 401 | // Asssert helpers 402 | // All of these must call either QUnit.push() or manually do: 403 | // - runLoggingCallbacks( "log", .. ); 404 | // - config.current.assertions.push({ .. }); 405 | QUnit.assert = { 406 | /** 407 | * Asserts rough true-ish result. 408 | * @name ok 409 | * @function 410 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 411 | */ 412 | ok: function( result, msg ) { 413 | if ( !config.current ) { 414 | throw new Error( "ok() assertion outside test context, was " + sourceFromStacktrace(2) ); 415 | } 416 | result = !!result; 417 | 418 | var source, 419 | details = { 420 | result: result, 421 | message: msg 422 | }; 423 | 424 | msg = escapeInnerText( msg || (result ? "okay" : "failed" ) ); 425 | msg = "" + msg + ""; 426 | 427 | if ( !result ) { 428 | source = sourceFromStacktrace( 2 ); 429 | if ( source ) { 430 | details.source = source; 431 | msg += "
Source:
" + escapeInnerText( source ) + "
"; 432 | } 433 | } 434 | runLoggingCallbacks( "log", QUnit, details ); 435 | config.current.assertions.push({ 436 | result: result, 437 | message: msg 438 | }); 439 | }, 440 | 441 | /** 442 | * Assert that the first two arguments are equal, with an optional message. 443 | * Prints out both actual and expected values. 444 | * @name equal 445 | * @function 446 | * @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" ); 447 | */ 448 | equal: function( actual, expected, message ) { 449 | QUnit.push( expected == actual, actual, expected, message ); 450 | }, 451 | 452 | /** 453 | * @name notEqual 454 | * @function 455 | */ 456 | notEqual: function( actual, expected, message ) { 457 | QUnit.push( expected != actual, actual, expected, message ); 458 | }, 459 | 460 | /** 461 | * @name deepEqual 462 | * @function 463 | */ 464 | deepEqual: function( actual, expected, message ) { 465 | QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); 466 | }, 467 | 468 | /** 469 | * @name notDeepEqual 470 | * @function 471 | */ 472 | notDeepEqual: function( actual, expected, message ) { 473 | QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); 474 | }, 475 | 476 | /** 477 | * @name strictEqual 478 | * @function 479 | */ 480 | strictEqual: function( actual, expected, message ) { 481 | QUnit.push( expected === actual, actual, expected, message ); 482 | }, 483 | 484 | /** 485 | * @name notStrictEqual 486 | * @function 487 | */ 488 | notStrictEqual: function( actual, expected, message ) { 489 | QUnit.push( expected !== actual, actual, expected, message ); 490 | }, 491 | 492 | throws: function( block, expected, message ) { 493 | var actual, 494 | ok = false; 495 | 496 | // 'expected' is optional 497 | if ( typeof expected === "string" ) { 498 | message = expected; 499 | expected = null; 500 | } 501 | 502 | config.current.ignoreGlobalErrors = true; 503 | try { 504 | block.call( config.current.testEnvironment ); 505 | } catch (e) { 506 | actual = e; 507 | } 508 | config.current.ignoreGlobalErrors = false; 509 | 510 | if ( actual ) { 511 | // we don't want to validate thrown error 512 | if ( !expected ) { 513 | ok = true; 514 | // expected is a regexp 515 | } else if ( QUnit.objectType( expected ) === "regexp" ) { 516 | ok = expected.test( actual ); 517 | // expected is a constructor 518 | } else if ( actual instanceof expected ) { 519 | ok = true; 520 | // expected is a validation function which returns true is validation passed 521 | } else if ( expected.call( {}, actual ) === true ) { 522 | ok = true; 523 | } 524 | 525 | QUnit.push( ok, actual, null, message ); 526 | } else { 527 | QUnit.pushFailure( message, null, 'No exception was thrown.' ); 528 | } 529 | } 530 | }; 531 | 532 | /** 533 | * @deprecate since 1.8.0 534 | * Kept assertion helpers in root for backwards compatibility 535 | */ 536 | extend( QUnit, QUnit.assert ); 537 | 538 | /** 539 | * @deprecated since 1.9.0 540 | * Kept global "raises()" for backwards compatibility 541 | */ 542 | QUnit.raises = QUnit.assert.throws; 543 | 544 | /** 545 | * @deprecated since 1.0.0, replaced with error pushes since 1.3.0 546 | * Kept to avoid TypeErrors for undefined methods. 547 | */ 548 | QUnit.equals = function() { 549 | QUnit.push( false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead" ); 550 | }; 551 | QUnit.same = function() { 552 | QUnit.push( false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead" ); 553 | }; 554 | 555 | // We want access to the constructor's prototype 556 | (function() { 557 | function F() {} 558 | F.prototype = QUnit; 559 | QUnit = new F(); 560 | // Make F QUnit's constructor so that we can add to the prototype later 561 | QUnit.constructor = F; 562 | }()); 563 | 564 | /** 565 | * Config object: Maintain internal state 566 | * Later exposed as QUnit.config 567 | * `config` initialized at top of scope 568 | */ 569 | config = { 570 | // The queue of tests to run 571 | queue: [], 572 | 573 | // block until document ready 574 | blocking: true, 575 | 576 | // when enabled, show only failing tests 577 | // gets persisted through sessionStorage and can be changed in UI via checkbox 578 | hidepassed: false, 579 | 580 | // by default, run previously failed tests first 581 | // very useful in combination with "Hide passed tests" checked 582 | reorder: true, 583 | 584 | // by default, modify document.title when suite is done 585 | altertitle: true, 586 | 587 | // when enabled, all tests must call expect() 588 | requireExpects: false, 589 | 590 | // add checkboxes that are persisted in the query-string 591 | // when enabled, the id is set to `true` as a `QUnit.config` property 592 | urlConfig: [ 593 | { 594 | id: "noglobals", 595 | label: "Check for Globals", 596 | tooltip: "Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings." 597 | }, 598 | { 599 | id: "notrycatch", 600 | label: "No try-catch", 601 | tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings." 602 | } 603 | ], 604 | 605 | // logging callback queues 606 | begin: [], 607 | done: [], 608 | log: [], 609 | testStart: [], 610 | testDone: [], 611 | moduleStart: [], 612 | moduleDone: [] 613 | }; 614 | 615 | // Initialize more QUnit.config and QUnit.urlParams 616 | (function() { 617 | var i, 618 | location = window.location || { search: "", protocol: "file:" }, 619 | params = location.search.slice( 1 ).split( "&" ), 620 | length = params.length, 621 | urlParams = {}, 622 | current; 623 | 624 | if ( params[ 0 ] ) { 625 | for ( i = 0; i < length; i++ ) { 626 | current = params[ i ].split( "=" ); 627 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 628 | // allow just a key to turn on a flag, e.g., test.html?noglobals 629 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 630 | urlParams[ current[ 0 ] ] = current[ 1 ]; 631 | } 632 | } 633 | 634 | QUnit.urlParams = urlParams; 635 | 636 | // String search anywhere in moduleName+testName 637 | config.filter = urlParams.filter; 638 | 639 | // Exact match of the module name 640 | config.module = urlParams.module; 641 | 642 | config.testNumber = parseInt( urlParams.testNumber, 10 ) || null; 643 | 644 | // Figure out if we're running the tests from a server or not 645 | QUnit.isLocal = location.protocol === "file:"; 646 | }()); 647 | 648 | // Export global variables, unless an 'exports' object exists, 649 | // in that case we assume we're in CommonJS (dealt with on the bottom of the script) 650 | if ( typeof exports === "undefined" ) { 651 | extend( window, QUnit ); 652 | 653 | // Expose QUnit object 654 | window.QUnit = QUnit; 655 | } 656 | 657 | // Extend QUnit object, 658 | // these after set here because they should not be exposed as global functions 659 | extend( QUnit, { 660 | config: config, 661 | 662 | // Initialize the configuration options 663 | init: function() { 664 | extend( config, { 665 | stats: { all: 0, bad: 0 }, 666 | moduleStats: { all: 0, bad: 0 }, 667 | started: +new Date(), 668 | updateRate: 1000, 669 | blocking: false, 670 | autostart: true, 671 | autorun: false, 672 | filter: "", 673 | queue: [], 674 | semaphore: 0 675 | }); 676 | 677 | var tests, banner, result, 678 | qunit = id( "qunit" ); 679 | 680 | if ( qunit ) { 681 | qunit.innerHTML = 682 | "

" + escapeInnerText( document.title ) + "

" + 683 | "

" + 684 | "
" + 685 | "

" + 686 | "
    "; 687 | } 688 | 689 | tests = id( "qunit-tests" ); 690 | banner = id( "qunit-banner" ); 691 | result = id( "qunit-testresult" ); 692 | 693 | if ( tests ) { 694 | tests.innerHTML = ""; 695 | } 696 | 697 | if ( banner ) { 698 | banner.className = ""; 699 | } 700 | 701 | if ( result ) { 702 | result.parentNode.removeChild( result ); 703 | } 704 | 705 | if ( tests ) { 706 | result = document.createElement( "p" ); 707 | result.id = "qunit-testresult"; 708 | result.className = "result"; 709 | tests.parentNode.insertBefore( result, tests ); 710 | result.innerHTML = "Running...
     "; 711 | } 712 | }, 713 | 714 | // Resets the test setup. Useful for tests that modify the DOM. 715 | // If jQuery is available, uses jQuery's html(), otherwise just innerHTML. 716 | reset: function() { 717 | var fixture; 718 | 719 | if ( window.jQuery ) { 720 | jQuery( "#qunit-fixture" ).html( config.fixture ); 721 | } else { 722 | fixture = id( "qunit-fixture" ); 723 | if ( fixture ) { 724 | fixture.innerHTML = config.fixture; 725 | } 726 | } 727 | }, 728 | 729 | // Trigger an event on an element. 730 | // @example triggerEvent( document.body, "click" ); 731 | triggerEvent: function( elem, type, event ) { 732 | if ( document.createEvent ) { 733 | event = document.createEvent( "MouseEvents" ); 734 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 735 | 0, 0, 0, 0, 0, false, false, false, false, 0, null); 736 | 737 | elem.dispatchEvent( event ); 738 | } else if ( elem.fireEvent ) { 739 | elem.fireEvent( "on" + type ); 740 | } 741 | }, 742 | 743 | // Safe object type checking 744 | is: function( type, obj ) { 745 | return QUnit.objectType( obj ) == type; 746 | }, 747 | 748 | objectType: function( obj ) { 749 | if ( typeof obj === "undefined" ) { 750 | return "undefined"; 751 | // consider: typeof null === object 752 | } 753 | if ( obj === null ) { 754 | return "null"; 755 | } 756 | 757 | var type = toString.call( obj ).match(/^\[object\s(.*)\]$/)[1] || ""; 758 | 759 | switch ( type ) { 760 | case "Number": 761 | if ( isNaN(obj) ) { 762 | return "nan"; 763 | } 764 | return "number"; 765 | case "String": 766 | case "Boolean": 767 | case "Array": 768 | case "Date": 769 | case "RegExp": 770 | case "Function": 771 | return type.toLowerCase(); 772 | } 773 | if ( typeof obj === "object" ) { 774 | return "object"; 775 | } 776 | return undefined; 777 | }, 778 | 779 | push: function( result, actual, expected, message ) { 780 | if ( !config.current ) { 781 | throw new Error( "assertion outside test context, was " + sourceFromStacktrace() ); 782 | } 783 | 784 | var output, source, 785 | details = { 786 | result: result, 787 | message: message, 788 | actual: actual, 789 | expected: expected 790 | }; 791 | 792 | message = escapeInnerText( message ) || ( result ? "okay" : "failed" ); 793 | message = "" + message + ""; 794 | output = message; 795 | 796 | if ( !result ) { 797 | expected = escapeInnerText( QUnit.jsDump.parse(expected) ); 798 | actual = escapeInnerText( QUnit.jsDump.parse(actual) ); 799 | output += ""; 800 | 801 | if ( actual != expected ) { 802 | output += ""; 803 | output += ""; 804 | } 805 | 806 | source = sourceFromStacktrace(); 807 | 808 | if ( source ) { 809 | details.source = source; 810 | output += ""; 811 | } 812 | 813 | output += "
    Expected:
    " + expected + "
    Result:
    " + actual + "
    Diff:
    " + QUnit.diff( expected, actual ) + "
    Source:
    " + escapeInnerText( source ) + "
    "; 814 | } 815 | 816 | runLoggingCallbacks( "log", QUnit, details ); 817 | 818 | config.current.assertions.push({ 819 | result: !!result, 820 | message: output 821 | }); 822 | }, 823 | 824 | pushFailure: function( message, source, actual ) { 825 | if ( !config.current ) { 826 | throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace(2) ); 827 | } 828 | 829 | var output, 830 | details = { 831 | result: false, 832 | message: message 833 | }; 834 | 835 | message = escapeInnerText( message ) || "error"; 836 | message = "" + message + ""; 837 | output = message; 838 | 839 | output += ""; 840 | 841 | if ( actual ) { 842 | output += ""; 843 | } 844 | 845 | if ( source ) { 846 | details.source = source; 847 | output += ""; 848 | } 849 | 850 | output += "
    Result:
    " + escapeInnerText( actual ) + "
    Source:
    " + escapeInnerText( source ) + "
    "; 851 | 852 | runLoggingCallbacks( "log", QUnit, details ); 853 | 854 | config.current.assertions.push({ 855 | result: false, 856 | message: output 857 | }); 858 | }, 859 | 860 | url: function( params ) { 861 | params = extend( extend( {}, QUnit.urlParams ), params ); 862 | var key, 863 | querystring = "?"; 864 | 865 | for ( key in params ) { 866 | if ( !hasOwn.call( params, key ) ) { 867 | continue; 868 | } 869 | querystring += encodeURIComponent( key ) + "=" + 870 | encodeURIComponent( params[ key ] ) + "&"; 871 | } 872 | return window.location.pathname + querystring.slice( 0, -1 ); 873 | }, 874 | 875 | extend: extend, 876 | id: id, 877 | addEvent: addEvent 878 | // load, equiv, jsDump, diff: Attached later 879 | }); 880 | 881 | /** 882 | * @deprecated: Created for backwards compatibility with test runner that set the hook function 883 | * into QUnit.{hook}, instead of invoking it and passing the hook function. 884 | * QUnit.constructor is set to the empty F() above so that we can add to it's prototype here. 885 | * Doing this allows us to tell if the following methods have been overwritten on the actual 886 | * QUnit object. 887 | */ 888 | extend( QUnit.constructor.prototype, { 889 | 890 | // Logging callbacks; all receive a single argument with the listed properties 891 | // run test/logs.html for any related changes 892 | begin: registerLoggingCallback( "begin" ), 893 | 894 | // done: { failed, passed, total, runtime } 895 | done: registerLoggingCallback( "done" ), 896 | 897 | // log: { result, actual, expected, message } 898 | log: registerLoggingCallback( "log" ), 899 | 900 | // testStart: { name } 901 | testStart: registerLoggingCallback( "testStart" ), 902 | 903 | // testDone: { name, failed, passed, total } 904 | testDone: registerLoggingCallback( "testDone" ), 905 | 906 | // moduleStart: { name } 907 | moduleStart: registerLoggingCallback( "moduleStart" ), 908 | 909 | // moduleDone: { name, failed, passed, total } 910 | moduleDone: registerLoggingCallback( "moduleDone" ) 911 | }); 912 | 913 | if ( typeof document === "undefined" || document.readyState === "complete" ) { 914 | config.autorun = true; 915 | } 916 | 917 | QUnit.load = function() { 918 | runLoggingCallbacks( "begin", QUnit, {} ); 919 | 920 | // Initialize the config, saving the execution queue 921 | var banner, filter, i, label, len, main, ol, toolbar, userAgent, val, urlConfigCheckboxes, 922 | urlConfigHtml = "", 923 | oldconfig = extend( {}, config ); 924 | 925 | QUnit.init(); 926 | extend(config, oldconfig); 927 | 928 | config.blocking = false; 929 | 930 | len = config.urlConfig.length; 931 | 932 | for ( i = 0; i < len; i++ ) { 933 | val = config.urlConfig[i]; 934 | if ( typeof val === "string" ) { 935 | val = { 936 | id: val, 937 | label: val, 938 | tooltip: "[no tooltip available]" 939 | }; 940 | } 941 | config[ val.id ] = QUnit.urlParams[ val.id ]; 942 | urlConfigHtml += ""; 943 | } 944 | 945 | // `userAgent` initialized at top of scope 946 | userAgent = id( "qunit-userAgent" ); 947 | if ( userAgent ) { 948 | userAgent.innerHTML = navigator.userAgent; 949 | } 950 | 951 | // `banner` initialized at top of scope 952 | banner = id( "qunit-header" ); 953 | if ( banner ) { 954 | banner.innerHTML = "" + banner.innerHTML + " "; 955 | } 956 | 957 | // `toolbar` initialized at top of scope 958 | toolbar = id( "qunit-testrunner-toolbar" ); 959 | if ( toolbar ) { 960 | // `filter` initialized at top of scope 961 | filter = document.createElement( "input" ); 962 | filter.type = "checkbox"; 963 | filter.id = "qunit-filter-pass"; 964 | 965 | addEvent( filter, "click", function() { 966 | var tmp, 967 | ol = document.getElementById( "qunit-tests" ); 968 | 969 | if ( filter.checked ) { 970 | ol.className = ol.className + " hidepass"; 971 | } else { 972 | tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; 973 | ol.className = tmp.replace( / hidepass /, " " ); 974 | } 975 | if ( defined.sessionStorage ) { 976 | if (filter.checked) { 977 | sessionStorage.setItem( "qunit-filter-passed-tests", "true" ); 978 | } else { 979 | sessionStorage.removeItem( "qunit-filter-passed-tests" ); 980 | } 981 | } 982 | }); 983 | 984 | if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem( "qunit-filter-passed-tests" ) ) { 985 | filter.checked = true; 986 | // `ol` initialized at top of scope 987 | ol = document.getElementById( "qunit-tests" ); 988 | ol.className = ol.className + " hidepass"; 989 | } 990 | toolbar.appendChild( filter ); 991 | 992 | // `label` initialized at top of scope 993 | label = document.createElement( "label" ); 994 | label.setAttribute( "for", "qunit-filter-pass" ); 995 | label.setAttribute( "title", "Only show tests and assertons that fail. Stored in sessionStorage." ); 996 | label.innerHTML = "Hide passed tests"; 997 | toolbar.appendChild( label ); 998 | 999 | urlConfigCheckboxes = document.createElement( 'span' ); 1000 | urlConfigCheckboxes.innerHTML = urlConfigHtml; 1001 | addEvent( urlConfigCheckboxes, "change", function( event ) { 1002 | var params = {}; 1003 | params[ event.target.name ] = event.target.checked ? true : undefined; 1004 | window.location = QUnit.url( params ); 1005 | }); 1006 | toolbar.appendChild( urlConfigCheckboxes ); 1007 | } 1008 | 1009 | // `main` initialized at top of scope 1010 | main = id( "qunit-fixture" ); 1011 | if ( main ) { 1012 | config.fixture = main.innerHTML; 1013 | } 1014 | 1015 | if ( config.autostart ) { 1016 | QUnit.start(); 1017 | } 1018 | }; 1019 | 1020 | addEvent( window, "load", QUnit.load ); 1021 | 1022 | // `onErrorFnPrev` initialized at top of scope 1023 | // Preserve other handlers 1024 | onErrorFnPrev = window.onerror; 1025 | 1026 | // Cover uncaught exceptions 1027 | // Returning true will surpress the default browser handler, 1028 | // returning false will let it run. 1029 | window.onerror = function ( error, filePath, linerNr ) { 1030 | var ret = false; 1031 | if ( onErrorFnPrev ) { 1032 | ret = onErrorFnPrev( error, filePath, linerNr ); 1033 | } 1034 | 1035 | // Treat return value as window.onerror itself does, 1036 | // Only do our handling if not surpressed. 1037 | if ( ret !== true ) { 1038 | if ( QUnit.config.current ) { 1039 | if ( QUnit.config.current.ignoreGlobalErrors ) { 1040 | return true; 1041 | } 1042 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 1043 | } else { 1044 | QUnit.test( "global failure", function() { 1045 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 1046 | }); 1047 | } 1048 | return false; 1049 | } 1050 | 1051 | return ret; 1052 | }; 1053 | 1054 | function done() { 1055 | config.autorun = true; 1056 | 1057 | // Log the last module results 1058 | if ( config.currentModule ) { 1059 | runLoggingCallbacks( "moduleDone", QUnit, { 1060 | name: config.currentModule, 1061 | failed: config.moduleStats.bad, 1062 | passed: config.moduleStats.all - config.moduleStats.bad, 1063 | total: config.moduleStats.all 1064 | }); 1065 | } 1066 | 1067 | var i, key, 1068 | banner = id( "qunit-banner" ), 1069 | tests = id( "qunit-tests" ), 1070 | runtime = +new Date() - config.started, 1071 | passed = config.stats.all - config.stats.bad, 1072 | html = [ 1073 | "Tests completed in ", 1074 | runtime, 1075 | " milliseconds.
    ", 1076 | "", 1077 | passed, 1078 | " tests of ", 1079 | config.stats.all, 1080 | " passed, ", 1081 | config.stats.bad, 1082 | " failed." 1083 | ].join( "" ); 1084 | 1085 | if ( banner ) { 1086 | banner.className = ( config.stats.bad ? "qunit-fail" : "qunit-pass" ); 1087 | } 1088 | 1089 | if ( tests ) { 1090 | id( "qunit-testresult" ).innerHTML = html; 1091 | } 1092 | 1093 | if ( config.altertitle && typeof document !== "undefined" && document.title ) { 1094 | // show ✖ for good, ✔ for bad suite result in title 1095 | // use escape sequences in case file gets loaded with non-utf-8-charset 1096 | document.title = [ 1097 | ( config.stats.bad ? "\u2716" : "\u2714" ), 1098 | document.title.replace( /^[\u2714\u2716] /i, "" ) 1099 | ].join( " " ); 1100 | } 1101 | 1102 | // clear own sessionStorage items if all tests passed 1103 | if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) { 1104 | // `key` & `i` initialized at top of scope 1105 | for ( i = 0; i < sessionStorage.length; i++ ) { 1106 | key = sessionStorage.key( i++ ); 1107 | if ( key.indexOf( "qunit-test-" ) === 0 ) { 1108 | sessionStorage.removeItem( key ); 1109 | } 1110 | } 1111 | } 1112 | 1113 | runLoggingCallbacks( "done", QUnit, { 1114 | failed: config.stats.bad, 1115 | passed: passed, 1116 | total: config.stats.all, 1117 | runtime: runtime 1118 | }); 1119 | } 1120 | 1121 | /** @return Boolean: true if this test should be ran */ 1122 | function validTest( test ) { 1123 | var include, 1124 | filter = config.filter && config.filter.toLowerCase(), 1125 | module = config.module && config.module.toLowerCase(), 1126 | fullName = (test.module + ": " + test.testName).toLowerCase(); 1127 | 1128 | if ( config.testNumber ) { 1129 | return test.testNumber === config.testNumber; 1130 | } 1131 | 1132 | if ( module && ( !test.module || test.module.toLowerCase() !== module ) ) { 1133 | return false; 1134 | } 1135 | 1136 | if ( !filter ) { 1137 | return true; 1138 | } 1139 | 1140 | include = filter.charAt( 0 ) !== "!"; 1141 | if ( !include ) { 1142 | filter = filter.slice( 1 ); 1143 | } 1144 | 1145 | // If the filter matches, we need to honour include 1146 | if ( fullName.indexOf( filter ) !== -1 ) { 1147 | return include; 1148 | } 1149 | 1150 | // Otherwise, do the opposite 1151 | return !include; 1152 | } 1153 | 1154 | // so far supports only Firefox, Chrome and Opera (buggy), Safari (for real exceptions) 1155 | // Later Safari and IE10 are supposed to support error.stack as well 1156 | // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack 1157 | function extractStacktrace( e, offset ) { 1158 | offset = offset === undefined ? 3 : offset; 1159 | 1160 | var stack, include, i, regex; 1161 | 1162 | if ( e.stacktrace ) { 1163 | // Opera 1164 | return e.stacktrace.split( "\n" )[ offset + 3 ]; 1165 | } else if ( e.stack ) { 1166 | // Firefox, Chrome 1167 | stack = e.stack.split( "\n" ); 1168 | if (/^error$/i.test( stack[0] ) ) { 1169 | stack.shift(); 1170 | } 1171 | if ( fileName ) { 1172 | include = []; 1173 | for ( i = offset; i < stack.length; i++ ) { 1174 | if ( stack[ i ].indexOf( fileName ) != -1 ) { 1175 | break; 1176 | } 1177 | include.push( stack[ i ] ); 1178 | } 1179 | if ( include.length ) { 1180 | return include.join( "\n" ); 1181 | } 1182 | } 1183 | return stack[ offset ]; 1184 | } else if ( e.sourceURL ) { 1185 | // Safari, PhantomJS 1186 | // hopefully one day Safari provides actual stacktraces 1187 | // exclude useless self-reference for generated Error objects 1188 | if ( /qunit.js$/.test( e.sourceURL ) ) { 1189 | return; 1190 | } 1191 | // for actual exceptions, this is useful 1192 | return e.sourceURL + ":" + e.line; 1193 | } 1194 | } 1195 | function sourceFromStacktrace( offset ) { 1196 | try { 1197 | throw new Error(); 1198 | } catch ( e ) { 1199 | return extractStacktrace( e, offset ); 1200 | } 1201 | } 1202 | 1203 | function escapeInnerText( s ) { 1204 | if ( !s ) { 1205 | return ""; 1206 | } 1207 | s = s + ""; 1208 | return s.replace( /[\&<>]/g, function( s ) { 1209 | switch( s ) { 1210 | case "&": return "&"; 1211 | case "<": return "<"; 1212 | case ">": return ">"; 1213 | default: return s; 1214 | } 1215 | }); 1216 | } 1217 | 1218 | function synchronize( callback, last ) { 1219 | config.queue.push( callback ); 1220 | 1221 | if ( config.autorun && !config.blocking ) { 1222 | process( last ); 1223 | } 1224 | } 1225 | 1226 | function process( last ) { 1227 | function next() { 1228 | process( last ); 1229 | } 1230 | var start = new Date().getTime(); 1231 | config.depth = config.depth ? config.depth + 1 : 1; 1232 | 1233 | while ( config.queue.length && !config.blocking ) { 1234 | if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) { 1235 | config.queue.shift()(); 1236 | } else { 1237 | window.setTimeout( next, 13 ); 1238 | break; 1239 | } 1240 | } 1241 | config.depth--; 1242 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { 1243 | done(); 1244 | } 1245 | } 1246 | 1247 | function saveGlobal() { 1248 | config.pollution = []; 1249 | 1250 | if ( config.noglobals ) { 1251 | for ( var key in window ) { 1252 | // in Opera sometimes DOM element ids show up here, ignore them 1253 | if ( !hasOwn.call( window, key ) || /^qunit-test-output/.test( key ) ) { 1254 | continue; 1255 | } 1256 | config.pollution.push( key ); 1257 | } 1258 | } 1259 | } 1260 | 1261 | function checkPollution( name ) { 1262 | var newGlobals, 1263 | deletedGlobals, 1264 | old = config.pollution; 1265 | 1266 | saveGlobal(); 1267 | 1268 | newGlobals = diff( config.pollution, old ); 1269 | if ( newGlobals.length > 0 ) { 1270 | QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join(", ") ); 1271 | } 1272 | 1273 | deletedGlobals = diff( old, config.pollution ); 1274 | if ( deletedGlobals.length > 0 ) { 1275 | QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join(", ") ); 1276 | } 1277 | } 1278 | 1279 | // returns a new Array with the elements that are in a but not in b 1280 | function diff( a, b ) { 1281 | var i, j, 1282 | result = a.slice(); 1283 | 1284 | for ( i = 0; i < result.length; i++ ) { 1285 | for ( j = 0; j < b.length; j++ ) { 1286 | if ( result[i] === b[j] ) { 1287 | result.splice( i, 1 ); 1288 | i--; 1289 | break; 1290 | } 1291 | } 1292 | } 1293 | return result; 1294 | } 1295 | 1296 | function extend( a, b ) { 1297 | for ( var prop in b ) { 1298 | if ( b[ prop ] === undefined ) { 1299 | delete a[ prop ]; 1300 | 1301 | // Avoid "Member not found" error in IE8 caused by setting window.constructor 1302 | } else if ( prop !== "constructor" || a !== window ) { 1303 | a[ prop ] = b[ prop ]; 1304 | } 1305 | } 1306 | 1307 | return a; 1308 | } 1309 | 1310 | function addEvent( elem, type, fn ) { 1311 | if ( elem.addEventListener ) { 1312 | elem.addEventListener( type, fn, false ); 1313 | } else if ( elem.attachEvent ) { 1314 | elem.attachEvent( "on" + type, fn ); 1315 | } else { 1316 | fn(); 1317 | } 1318 | } 1319 | 1320 | function id( name ) { 1321 | return !!( typeof document !== "undefined" && document && document.getElementById ) && 1322 | document.getElementById( name ); 1323 | } 1324 | 1325 | function registerLoggingCallback( key ) { 1326 | return function( callback ) { 1327 | config[key].push( callback ); 1328 | }; 1329 | } 1330 | 1331 | // Supports deprecated method of completely overwriting logging callbacks 1332 | function runLoggingCallbacks( key, scope, args ) { 1333 | //debugger; 1334 | var i, callbacks; 1335 | if ( QUnit.hasOwnProperty( key ) ) { 1336 | QUnit[ key ].call(scope, args ); 1337 | } else { 1338 | callbacks = config[ key ]; 1339 | for ( i = 0; i < callbacks.length; i++ ) { 1340 | callbacks[ i ].call( scope, args ); 1341 | } 1342 | } 1343 | } 1344 | 1345 | // Test for equality any JavaScript type. 1346 | // Author: Philippe Rathé 1347 | QUnit.equiv = (function() { 1348 | 1349 | // Call the o related callback with the given arguments. 1350 | function bindCallbacks( o, callbacks, args ) { 1351 | var prop = QUnit.objectType( o ); 1352 | if ( prop ) { 1353 | if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) { 1354 | return callbacks[ prop ].apply( callbacks, args ); 1355 | } else { 1356 | return callbacks[ prop ]; // or undefined 1357 | } 1358 | } 1359 | } 1360 | 1361 | // the real equiv function 1362 | var innerEquiv, 1363 | // stack to decide between skip/abort functions 1364 | callers = [], 1365 | // stack to avoiding loops from circular referencing 1366 | parents = [], 1367 | 1368 | getProto = Object.getPrototypeOf || function ( obj ) { 1369 | return obj.__proto__; 1370 | }, 1371 | callbacks = (function () { 1372 | 1373 | // for string, boolean, number and null 1374 | function useStrictEquality( b, a ) { 1375 | if ( b instanceof a.constructor || a instanceof b.constructor ) { 1376 | // to catch short annotaion VS 'new' annotation of a 1377 | // declaration 1378 | // e.g. var i = 1; 1379 | // var j = new Number(1); 1380 | return a == b; 1381 | } else { 1382 | return a === b; 1383 | } 1384 | } 1385 | 1386 | return { 1387 | "string": useStrictEquality, 1388 | "boolean": useStrictEquality, 1389 | "number": useStrictEquality, 1390 | "null": useStrictEquality, 1391 | "undefined": useStrictEquality, 1392 | 1393 | "nan": function( b ) { 1394 | return isNaN( b ); 1395 | }, 1396 | 1397 | "date": function( b, a ) { 1398 | return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf(); 1399 | }, 1400 | 1401 | "regexp": function( b, a ) { 1402 | return QUnit.objectType( b ) === "regexp" && 1403 | // the regex itself 1404 | a.source === b.source && 1405 | // and its modifers 1406 | a.global === b.global && 1407 | // (gmi) ... 1408 | a.ignoreCase === b.ignoreCase && 1409 | a.multiline === b.multiline; 1410 | }, 1411 | 1412 | // - skip when the property is a method of an instance (OOP) 1413 | // - abort otherwise, 1414 | // initial === would have catch identical references anyway 1415 | "function": function() { 1416 | var caller = callers[callers.length - 1]; 1417 | return caller !== Object && typeof caller !== "undefined"; 1418 | }, 1419 | 1420 | "array": function( b, a ) { 1421 | var i, j, len, loop; 1422 | 1423 | // b could be an object literal here 1424 | if ( QUnit.objectType( b ) !== "array" ) { 1425 | return false; 1426 | } 1427 | 1428 | len = a.length; 1429 | if ( len !== b.length ) { 1430 | // safe and faster 1431 | return false; 1432 | } 1433 | 1434 | // track reference to avoid circular references 1435 | parents.push( a ); 1436 | for ( i = 0; i < len; i++ ) { 1437 | loop = false; 1438 | for ( j = 0; j < parents.length; j++ ) { 1439 | if ( parents[j] === a[i] ) { 1440 | loop = true;// dont rewalk array 1441 | } 1442 | } 1443 | if ( !loop && !innerEquiv(a[i], b[i]) ) { 1444 | parents.pop(); 1445 | return false; 1446 | } 1447 | } 1448 | parents.pop(); 1449 | return true; 1450 | }, 1451 | 1452 | "object": function( b, a ) { 1453 | var i, j, loop, 1454 | // Default to true 1455 | eq = true, 1456 | aProperties = [], 1457 | bProperties = []; 1458 | 1459 | // comparing constructors is more strict than using 1460 | // instanceof 1461 | if ( a.constructor !== b.constructor ) { 1462 | // Allow objects with no prototype to be equivalent to 1463 | // objects with Object as their constructor. 1464 | if ( !(( getProto(a) === null && getProto(b) === Object.prototype ) || 1465 | ( getProto(b) === null && getProto(a) === Object.prototype ) ) ) { 1466 | return false; 1467 | } 1468 | } 1469 | 1470 | // stack constructor before traversing properties 1471 | callers.push( a.constructor ); 1472 | // track reference to avoid circular references 1473 | parents.push( a ); 1474 | 1475 | for ( i in a ) { // be strict: don't ensures hasOwnProperty 1476 | // and go deep 1477 | loop = false; 1478 | for ( j = 0; j < parents.length; j++ ) { 1479 | if ( parents[j] === a[i] ) { 1480 | // don't go down the same path twice 1481 | loop = true; 1482 | } 1483 | } 1484 | aProperties.push(i); // collect a's properties 1485 | 1486 | if (!loop && !innerEquiv( a[i], b[i] ) ) { 1487 | eq = false; 1488 | break; 1489 | } 1490 | } 1491 | 1492 | callers.pop(); // unstack, we are done 1493 | parents.pop(); 1494 | 1495 | for ( i in b ) { 1496 | bProperties.push( i ); // collect b's properties 1497 | } 1498 | 1499 | // Ensures identical properties name 1500 | return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); 1501 | } 1502 | }; 1503 | }()); 1504 | 1505 | innerEquiv = function() { // can take multiple arguments 1506 | var args = [].slice.apply( arguments ); 1507 | if ( args.length < 2 ) { 1508 | return true; // end transition 1509 | } 1510 | 1511 | return (function( a, b ) { 1512 | if ( a === b ) { 1513 | return true; // catch the most you can 1514 | } else if ( a === null || b === null || typeof a === "undefined" || 1515 | typeof b === "undefined" || 1516 | QUnit.objectType(a) !== QUnit.objectType(b) ) { 1517 | return false; // don't lose time with error prone cases 1518 | } else { 1519 | return bindCallbacks(a, callbacks, [ b, a ]); 1520 | } 1521 | 1522 | // apply transition with (1..n) arguments 1523 | }( args[0], args[1] ) && arguments.callee.apply( this, args.splice(1, args.length - 1 )) ); 1524 | }; 1525 | 1526 | return innerEquiv; 1527 | }()); 1528 | 1529 | /** 1530 | * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | 1531 | * http://flesler.blogspot.com Licensed under BSD 1532 | * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008 1533 | * 1534 | * @projectDescription Advanced and extensible data dumping for Javascript. 1535 | * @version 1.0.0 1536 | * @author Ariel Flesler 1537 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} 1538 | */ 1539 | QUnit.jsDump = (function() { 1540 | function quote( str ) { 1541 | return '"' + str.toString().replace( /"/g, '\\"' ) + '"'; 1542 | } 1543 | function literal( o ) { 1544 | return o + ""; 1545 | } 1546 | function join( pre, arr, post ) { 1547 | var s = jsDump.separator(), 1548 | base = jsDump.indent(), 1549 | inner = jsDump.indent(1); 1550 | if ( arr.join ) { 1551 | arr = arr.join( "," + s + inner ); 1552 | } 1553 | if ( !arr ) { 1554 | return pre + post; 1555 | } 1556 | return [ pre, inner + arr, base + post ].join(s); 1557 | } 1558 | function array( arr, stack ) { 1559 | var i = arr.length, ret = new Array(i); 1560 | this.up(); 1561 | while ( i-- ) { 1562 | ret[i] = this.parse( arr[i] , undefined , stack); 1563 | } 1564 | this.down(); 1565 | return join( "[", ret, "]" ); 1566 | } 1567 | 1568 | var reName = /^function (\w+)/, 1569 | jsDump = { 1570 | parse: function( obj, type, stack ) { //type is used mostly internally, you can fix a (custom)type in advance 1571 | stack = stack || [ ]; 1572 | var inStack, res, 1573 | parser = this.parsers[ type || this.typeOf(obj) ]; 1574 | 1575 | type = typeof parser; 1576 | inStack = inArray( obj, stack ); 1577 | 1578 | if ( inStack != -1 ) { 1579 | return "recursion(" + (inStack - stack.length) + ")"; 1580 | } 1581 | //else 1582 | if ( type == "function" ) { 1583 | stack.push( obj ); 1584 | res = parser.call( this, obj, stack ); 1585 | stack.pop(); 1586 | return res; 1587 | } 1588 | // else 1589 | return ( type == "string" ) ? parser : this.parsers.error; 1590 | }, 1591 | typeOf: function( obj ) { 1592 | var type; 1593 | if ( obj === null ) { 1594 | type = "null"; 1595 | } else if ( typeof obj === "undefined" ) { 1596 | type = "undefined"; 1597 | } else if ( QUnit.is( "regexp", obj) ) { 1598 | type = "regexp"; 1599 | } else if ( QUnit.is( "date", obj) ) { 1600 | type = "date"; 1601 | } else if ( QUnit.is( "function", obj) ) { 1602 | type = "function"; 1603 | } else if ( typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined" ) { 1604 | type = "window"; 1605 | } else if ( obj.nodeType === 9 ) { 1606 | type = "document"; 1607 | } else if ( obj.nodeType ) { 1608 | type = "node"; 1609 | } else if ( 1610 | // native arrays 1611 | toString.call( obj ) === "[object Array]" || 1612 | // NodeList objects 1613 | ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) ) 1614 | ) { 1615 | type = "array"; 1616 | } else { 1617 | type = typeof obj; 1618 | } 1619 | return type; 1620 | }, 1621 | separator: function() { 1622 | return this.multiline ? this.HTML ? "
    " : "\n" : this.HTML ? " " : " "; 1623 | }, 1624 | indent: function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing 1625 | if ( !this.multiline ) { 1626 | return ""; 1627 | } 1628 | var chr = this.indentChar; 1629 | if ( this.HTML ) { 1630 | chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); 1631 | } 1632 | return new Array( this._depth_ + (extra||0) ).join(chr); 1633 | }, 1634 | up: function( a ) { 1635 | this._depth_ += a || 1; 1636 | }, 1637 | down: function( a ) { 1638 | this._depth_ -= a || 1; 1639 | }, 1640 | setParser: function( name, parser ) { 1641 | this.parsers[name] = parser; 1642 | }, 1643 | // The next 3 are exposed so you can use them 1644 | quote: quote, 1645 | literal: literal, 1646 | join: join, 1647 | // 1648 | _depth_: 1, 1649 | // This is the list of parsers, to modify them, use jsDump.setParser 1650 | parsers: { 1651 | window: "[Window]", 1652 | document: "[Document]", 1653 | error: "[ERROR]", //when no parser is found, shouldn"t happen 1654 | unknown: "[Unknown]", 1655 | "null": "null", 1656 | "undefined": "undefined", 1657 | "function": function( fn ) { 1658 | var ret = "function", 1659 | name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1];//functions never have name in IE 1660 | 1661 | if ( name ) { 1662 | ret += " " + name; 1663 | } 1664 | ret += "( "; 1665 | 1666 | ret = [ ret, QUnit.jsDump.parse( fn, "functionArgs" ), "){" ].join( "" ); 1667 | return join( ret, QUnit.jsDump.parse(fn,"functionCode" ), "}" ); 1668 | }, 1669 | array: array, 1670 | nodelist: array, 1671 | "arguments": array, 1672 | object: function( map, stack ) { 1673 | var ret = [ ], keys, key, val, i; 1674 | QUnit.jsDump.up(); 1675 | if ( Object.keys ) { 1676 | keys = Object.keys( map ); 1677 | } else { 1678 | keys = []; 1679 | for ( key in map ) { 1680 | keys.push( key ); 1681 | } 1682 | } 1683 | keys.sort(); 1684 | for ( i = 0; i < keys.length; i++ ) { 1685 | key = keys[ i ]; 1686 | val = map[ key ]; 1687 | ret.push( QUnit.jsDump.parse( key, "key" ) + ": " + QUnit.jsDump.parse( val, undefined, stack ) ); 1688 | } 1689 | QUnit.jsDump.down(); 1690 | return join( "{", ret, "}" ); 1691 | }, 1692 | node: function( node ) { 1693 | var a, val, 1694 | open = QUnit.jsDump.HTML ? "<" : "<", 1695 | close = QUnit.jsDump.HTML ? ">" : ">", 1696 | tag = node.nodeName.toLowerCase(), 1697 | ret = open + tag; 1698 | 1699 | for ( a in QUnit.jsDump.DOMAttrs ) { 1700 | val = node[ QUnit.jsDump.DOMAttrs[a] ]; 1701 | if ( val ) { 1702 | ret += " " + a + "=" + QUnit.jsDump.parse( val, "attribute" ); 1703 | } 1704 | } 1705 | return ret + close + open + "/" + tag + close; 1706 | }, 1707 | functionArgs: function( fn ) {//function calls it internally, it's the arguments part of the function 1708 | var args, 1709 | l = fn.length; 1710 | 1711 | if ( !l ) { 1712 | return ""; 1713 | } 1714 | 1715 | args = new Array(l); 1716 | while ( l-- ) { 1717 | args[l] = String.fromCharCode(97+l);//97 is 'a' 1718 | } 1719 | return " " + args.join( ", " ) + " "; 1720 | }, 1721 | key: quote, //object calls it internally, the key part of an item in a map 1722 | functionCode: "[code]", //function calls it internally, it's the content of the function 1723 | attribute: quote, //node calls it internally, it's an html attribute value 1724 | string: quote, 1725 | date: quote, 1726 | regexp: literal, //regex 1727 | number: literal, 1728 | "boolean": literal 1729 | }, 1730 | DOMAttrs: { 1731 | //attributes to dump from nodes, name=>realName 1732 | id: "id", 1733 | name: "name", 1734 | "class": "className" 1735 | }, 1736 | HTML: false,//if true, entities are escaped ( <, >, \t, space and \n ) 1737 | indentChar: " ",//indentation unit 1738 | multiline: true //if true, items in a collection, are separated by a \n, else just a space. 1739 | }; 1740 | 1741 | return jsDump; 1742 | }()); 1743 | 1744 | // from Sizzle.js 1745 | function getText( elems ) { 1746 | var i, elem, 1747 | ret = ""; 1748 | 1749 | for ( i = 0; elems[i]; i++ ) { 1750 | elem = elems[i]; 1751 | 1752 | // Get the text from text nodes and CDATA nodes 1753 | if ( elem.nodeType === 3 || elem.nodeType === 4 ) { 1754 | ret += elem.nodeValue; 1755 | 1756 | // Traverse everything else, except comment nodes 1757 | } else if ( elem.nodeType !== 8 ) { 1758 | ret += getText( elem.childNodes ); 1759 | } 1760 | } 1761 | 1762 | return ret; 1763 | } 1764 | 1765 | // from jquery.js 1766 | function inArray( elem, array ) { 1767 | if ( array.indexOf ) { 1768 | return array.indexOf( elem ); 1769 | } 1770 | 1771 | for ( var i = 0, length = array.length; i < length; i++ ) { 1772 | if ( array[ i ] === elem ) { 1773 | return i; 1774 | } 1775 | } 1776 | 1777 | return -1; 1778 | } 1779 | 1780 | /* 1781 | * Javascript Diff Algorithm 1782 | * By John Resig (http://ejohn.org/) 1783 | * Modified by Chu Alan "sprite" 1784 | * 1785 | * Released under the MIT license. 1786 | * 1787 | * More Info: 1788 | * http://ejohn.org/projects/javascript-diff-algorithm/ 1789 | * 1790 | * Usage: QUnit.diff(expected, actual) 1791 | * 1792 | * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick brown fox jumped jumps over" 1793 | */ 1794 | QUnit.diff = (function() { 1795 | function diff( o, n ) { 1796 | var i, 1797 | ns = {}, 1798 | os = {}; 1799 | 1800 | for ( i = 0; i < n.length; i++ ) { 1801 | if ( ns[ n[i] ] == null ) { 1802 | ns[ n[i] ] = { 1803 | rows: [], 1804 | o: null 1805 | }; 1806 | } 1807 | ns[ n[i] ].rows.push( i ); 1808 | } 1809 | 1810 | for ( i = 0; i < o.length; i++ ) { 1811 | if ( os[ o[i] ] == null ) { 1812 | os[ o[i] ] = { 1813 | rows: [], 1814 | n: null 1815 | }; 1816 | } 1817 | os[ o[i] ].rows.push( i ); 1818 | } 1819 | 1820 | for ( i in ns ) { 1821 | if ( !hasOwn.call( ns, i ) ) { 1822 | continue; 1823 | } 1824 | if ( ns[i].rows.length == 1 && typeof os[i] != "undefined" && os[i].rows.length == 1 ) { 1825 | n[ ns[i].rows[0] ] = { 1826 | text: n[ ns[i].rows[0] ], 1827 | row: os[i].rows[0] 1828 | }; 1829 | o[ os[i].rows[0] ] = { 1830 | text: o[ os[i].rows[0] ], 1831 | row: ns[i].rows[0] 1832 | }; 1833 | } 1834 | } 1835 | 1836 | for ( i = 0; i < n.length - 1; i++ ) { 1837 | if ( n[i].text != null && n[ i + 1 ].text == null && n[i].row + 1 < o.length && o[ n[i].row + 1 ].text == null && 1838 | n[ i + 1 ] == o[ n[i].row + 1 ] ) { 1839 | 1840 | n[ i + 1 ] = { 1841 | text: n[ i + 1 ], 1842 | row: n[i].row + 1 1843 | }; 1844 | o[ n[i].row + 1 ] = { 1845 | text: o[ n[i].row + 1 ], 1846 | row: i + 1 1847 | }; 1848 | } 1849 | } 1850 | 1851 | for ( i = n.length - 1; i > 0; i-- ) { 1852 | if ( n[i].text != null && n[ i - 1 ].text == null && n[i].row > 0 && o[ n[i].row - 1 ].text == null && 1853 | n[ i - 1 ] == o[ n[i].row - 1 ]) { 1854 | 1855 | n[ i - 1 ] = { 1856 | text: n[ i - 1 ], 1857 | row: n[i].row - 1 1858 | }; 1859 | o[ n[i].row - 1 ] = { 1860 | text: o[ n[i].row - 1 ], 1861 | row: i - 1 1862 | }; 1863 | } 1864 | } 1865 | 1866 | return { 1867 | o: o, 1868 | n: n 1869 | }; 1870 | } 1871 | 1872 | return function( o, n ) { 1873 | o = o.replace( /\s+$/, "" ); 1874 | n = n.replace( /\s+$/, "" ); 1875 | 1876 | var i, pre, 1877 | str = "", 1878 | out = diff( o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/) ), 1879 | oSpace = o.match(/\s+/g), 1880 | nSpace = n.match(/\s+/g); 1881 | 1882 | if ( oSpace == null ) { 1883 | oSpace = [ " " ]; 1884 | } 1885 | else { 1886 | oSpace.push( " " ); 1887 | } 1888 | 1889 | if ( nSpace == null ) { 1890 | nSpace = [ " " ]; 1891 | } 1892 | else { 1893 | nSpace.push( " " ); 1894 | } 1895 | 1896 | if ( out.n.length === 0 ) { 1897 | for ( i = 0; i < out.o.length; i++ ) { 1898 | str += "" + out.o[i] + oSpace[i] + ""; 1899 | } 1900 | } 1901 | else { 1902 | if ( out.n[0].text == null ) { 1903 | for ( n = 0; n < out.o.length && out.o[n].text == null; n++ ) { 1904 | str += "" + out.o[n] + oSpace[n] + ""; 1905 | } 1906 | } 1907 | 1908 | for ( i = 0; i < out.n.length; i++ ) { 1909 | if (out.n[i].text == null) { 1910 | str += "" + out.n[i] + nSpace[i] + ""; 1911 | } 1912 | else { 1913 | // `pre` initialized at top of scope 1914 | pre = ""; 1915 | 1916 | for ( n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) { 1917 | pre += "" + out.o[n] + oSpace[n] + ""; 1918 | } 1919 | str += " " + out.n[i].text + nSpace[i] + pre; 1920 | } 1921 | } 1922 | } 1923 | 1924 | return str; 1925 | }; 1926 | }()); 1927 | 1928 | // for CommonJS enviroments, export everything 1929 | if ( typeof exports !== "undefined" ) { 1930 | extend(exports, QUnit); 1931 | } 1932 | 1933 | // get at whatever the global object is, like window in browsers 1934 | }( (function() {return this;}.call()) )); 1935 | --------------------------------------------------------------------------------