├── CONTRIBUTING.md ├── LICENSE.txt ├── Makefile ├── README.md ├── VERSION.txt ├── bower.json ├── demo ├── demo-edges.js ├── demo-main.js ├── edges.html └── index.html ├── inc ├── index.js └── lib │ ├── jquery-1.7.1.js │ ├── jquery-loader.js │ ├── jquery.ba-each2.min.js │ └── jquery.domline.min.js ├── index.html ├── package.json ├── src ├── jquery.nearest.js └── jquery.nearest.min.js └── test ├── index.html ├── nearest_test.js └── qunit ├── qunit.css └── qunit.js /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | First of all, thanks for wanting to take the time to make this code better. Every little bit helps. 4 | 5 | ## Basic process 6 | 7 | 1. Fork this repository 8 | 2. Create a new branch for your feature / bug fix - this helps to avoid merge conflicts on the `master` branch 9 | 3. Write code and tests 10 | 4. Create a pull request 11 | 12 | ## Which files to edit 13 | 14 | For the plugin itself: **`src/jquery.nearest.js`** (plus tests, see below) 15 | 16 | For documentation: **`index.html`** 17 | 18 | Don't worry about updating the minified JS file, it gets built automatically as part of a release. 19 | 20 | ## Tests 21 | 22 | Tests are run using QUnit and live in **`test/nearest_test.js`**. Open `test/index.html` in a browser to make sure tests are passing. 23 | 24 | If you're changing the plugin code, at least one test must be present in the pull request: 25 | 26 | * If you add a new feature, write new tests for it. 27 | * If you fix a bug, write a test that shows it was a bug (and makes sure there aren't regressions later). 28 | 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2015 Gilmore Davidson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SRC = src/jquery.nearest.js 2 | MIN = src/jquery.nearest.min.js 3 | VERSION = $(shell cat VERSION.txt) 4 | 5 | $(MIN): uglify 6 | 7 | uglify: 8 | uglifyjs $(SRC) --compress --mangle --comments '/^\!/' --output $(MIN) 9 | @echo "" >> $(MIN) 10 | 11 | install: 12 | npm install -g uglifyjs 13 | 14 | version: 15 | @sed -i '' 's/\("version": \)"\(.*\)"/\1"$(VERSION)"/g' *.json 16 | @sed -i '' 's/\(plugin v\).*$\/\1$(VERSION)/' src/*.js 17 | @sed -i '' 's/\(class="version">v\).*\(<\/span>\)/\1$(VERSION)\2/' index.html 18 | 19 | commit-version: version 20 | git commit -m "Bumped version to $(VERSION)" VERSION.txt *.json src/*.js index.html 21 | 22 | tag: commit-version 23 | git tag v$(VERSION) 24 | 25 | update-pages: 26 | git checkout gh-pages 27 | git merge --no-ff -m "Keeping gh-pages up to date" master 28 | git checkout - 29 | 30 | push: update-pages 31 | git push --tags origin master gh-pages 32 | 33 | .PHONY: uglify install version commit-version tag update-pages push 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | jQuery Nearest Element plugin 2 | ====== 3 | 4 | **Full documentation is at ** 5 | 6 | **Demo:** 7 | 8 | Method signatures: 9 | 10 | * `$.nearest({x, y}, selector)` - find `$(selector)` closest to x/y point on screen 11 | * `$(elem).nearest(selector)` - find `$(selector)` closest to elem 12 | * `$(elemSet).nearest({x, y})` - filter `$(elemSet)` and return closest to x/y point on screen 13 | 14 | Reverse logic: 15 | 16 | * `$.furthest()` 17 | * `$(elem).furthest()` 18 | 19 | Intersecting/touching: 20 | 21 | * `$.touching()` 22 | * `$(elem).touching()` 23 | -------------------------------------------------------------------------------- /VERSION.txt: -------------------------------------------------------------------------------- 1 | 1.4.0 -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-nearest", 3 | "version": "1.4.0", 4 | "main": "src/jquery.nearest.js", 5 | "ignore": [ 6 | "**/.*", 7 | "demo", 8 | "inc", 9 | "test", 10 | "Makefile", 11 | "index.html", 12 | "package.json" 13 | ], 14 | "dependencies": { 15 | "jquery": ">=1.4" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /demo/demo-edges.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | function rand(min, max) { 3 | return Math.round(Math.random() * (max - min)) + min; 4 | } 5 | 6 | var elemCount = 30, 7 | minSize = 50, 8 | maxSize = 150, 9 | $document = $(document), 10 | $menu = $('#menu'), 11 | $dimSelect = $('#dimension-select'), 12 | $container = $('#container'), 13 | $blockWrap = $container.find('.block-wrap'), 14 | $output = $('#code-output'), 15 | $footer = $('#footer'), 16 | menuWidth = $menu.outerWidth(), 17 | contWidth = $container.width(), 18 | contHeight = $container.height(), 19 | footerHeight = $footer.outerHeight(), 20 | corners = ['tl', 'tr', 'br', 'bl'], 21 | edgeBorders = {t: 'top', b: 'bottom', l: 'left', r: 'right'}, 22 | edgeLimits = {}, 23 | detectionOptions = { 24 | edgeX: '', 25 | edgeY: '' 26 | }, 27 | codeTemplate = "$('.block').nearest({\n/#props#/\n});", 28 | i, size, $blocks, $corners, curDrag; 29 | 30 | /*** Drag interactions ***/ 31 | 32 | for (i in edgeBorders) if (edgeBorders.hasOwnProperty(i)) { 33 | edgeLimits[edgeBorders[i]] = { 34 | limit: parseFloat($container.css(edgeBorders[i])) || 0 35 | }; 36 | edgeLimits[edgeBorders[i]].current = edgeLimits[edgeBorders[i]].limit; 37 | } 38 | 39 | // Prevent selecting text while resizing the container 40 | function noSelect(e) { 41 | if (curDrag) { 42 | e.preventDefault(); 43 | } 44 | } 45 | 46 | // Basic handler for any mouse events on drag handles 47 | function handleCornerMouseEvents(e) { 48 | if (e.type === 'mousedown') { 49 | curDrag = { 50 | elem: this, 51 | corner: $(this).data('corner'), 52 | x: e.pageX, 53 | y: e.pageY 54 | }; 55 | $document.on('mousemove', handleCornerMouseEvents); 56 | return; 57 | } 58 | if (e.type === 'mouseup') { 59 | if (curDrag && curDrag.elem === this) { 60 | curDrag = null; 61 | $document.off('mousemove', handleCornerMouseEvents); 62 | } 63 | return; 64 | } 65 | if (e.type === 'mousemove') { 66 | if (curDrag) { 67 | var dx = e.pageX - curDrag.x, 68 | dy = e.pageY - curDrag.y; 69 | 70 | moveContainerCorner(curDrag.corner, dx, dy); 71 | curDrag.x = e.pageX; 72 | curDrag.y = e.pageY; 73 | } 74 | } 75 | } 76 | 77 | // Make sure to catch mouseup outside the drag handles 78 | function handleDocumentMouseEvents(e) { 79 | if (e.type === 'mouseup' && curDrag && curDrag.elem !== this) { 80 | handleCornerMouseEvents.call(curDrag.elem, e); 81 | } 82 | } 83 | 84 | // Move a corner of the container by a certain amount 85 | // x/y are relative, not absolute 86 | function moveContainerCorner(corner, x, y) { 87 | var edgeY = edgeBorders[corner[0]], 88 | edgeX = edgeBorders[corner[1]]; 89 | if (edgeX === 'right') { 90 | x = -x; 91 | } 92 | if (edgeY === 'bottom') { 93 | y = -y; 94 | } 95 | $container.css(edgeX, function (i, val) { 96 | var limiter = edgeLimits[edgeX]; 97 | limiter.current += x; 98 | return (limiter.current < limiter.limit) ? limiter.limit : limiter.current; 99 | }); 100 | $container.css(edgeY, function (i, val) { 101 | var limiter = edgeLimits[edgeY]; 102 | limiter.current += y; 103 | return (limiter.current < limiter.limit) ? limiter.limit : limiter.current; 104 | }); 105 | detectNearest(); 106 | } 107 | 108 | 109 | /*** DOM setup ***/ 110 | 111 | // Setup corner handles 112 | $.each(corners, function (i, corner) { 113 | var c = corner.split(''); 114 | $('
', { 115 | 'class': ['corner', edgeBorders[c[0]], edgeBorders[c[1]]].join(' ') 116 | }).data('corner', c).appendTo($container); 117 | }); 118 | $corners = $container.find('.corner'); 119 | $corners.on('mousedown mouseup', handleCornerMouseEvents); 120 | $document.on({ 121 | selectstart: noSelect, 122 | mouseup: handleDocumentMouseEvents 123 | }); 124 | 125 | // Setup random elements 126 | for (i = 0; i < elemCount; i++) { 127 | size = rand(minSize, maxSize); 128 | $('
' + (i + 1) + '
').css({ 129 | width: size, 130 | height: size 131 | }).appendTo($blockWrap); 132 | } 133 | $blocks = $('.block'); 134 | 135 | 136 | /*** Options interactions ***/ 137 | 138 | // Handle edge selector input clicks 139 | function handleEdgeSelection() { 140 | var $this = $(this), 141 | val = $this.val(), 142 | dims = val.split(''), 143 | isMiddle = val === 'mm', 144 | isCorner = dims[0] !== 'm' && dims[1] !== 'm', 145 | isEdge = !isMiddle && !isCorner, 146 | border; 147 | 148 | $container.css('border-color', '#000'); 149 | $corners.css('background-color', '#000'); 150 | if (isEdge) { 151 | border = edgeBorders[dims[0]] || edgeBorders[dims[1]]; 152 | $container.css('border-' + border + '-color', '#F00'); 153 | } else if (isCorner) { 154 | border = [edgeBorders[dims[0]], edgeBorders[dims[1]]]; 155 | $corners.filter('.' + border.join('.')).css('background-color', '#F00'); 156 | } else { 157 | // TODO: Show middle point with sliders for percentage 158 | } 159 | detectionOptions.edgeX = dims[1]; 160 | detectionOptions.edgeY = dims[0]; 161 | detectNearest(); 162 | showExampleCode(); 163 | } 164 | 165 | // Highlight blocks that are nearest to current edge/point 166 | function detectNearest() { 167 | var edgeX = detectionOptions.edgeX, 168 | edgeY = detectionOptions.edgeY, 169 | opts = { 170 | container: $container, 171 | x: 0, 172 | y: 0, 173 | w: 0, 174 | h: 0 175 | }; 176 | if (edgeX === 'm' && edgeY === 'm') { 177 | // TODO: Detect from middle point option 178 | } else { 179 | opts.x = edgeX === 'r' ? '100%' : 0; 180 | opts.y = edgeY === 'b' ? '100%' : 0; 181 | opts.w = edgeX === 'm' ? '100%' : 0; 182 | opts.h = edgeY === 'm' ? '100%' : 0; 183 | } 184 | $blocks.removeClass('nearest').nearest(opts).addClass('nearest'); 185 | } 186 | 187 | // Setup detection handlers 188 | $dimSelect.on('click', 'input[name=dimension]', handleEdgeSelection); 189 | $(window).resize(detectNearest); 190 | 191 | // Set initial state based on default checked input 192 | var $defaultChecked = $dimSelect.find('input:checked'); 193 | if ($defaultChecked.length) { 194 | handleEdgeSelection.call($defaultChecked[0]); 195 | } 196 | 197 | 198 | /*** Example code ***/ 199 | 200 | function generateExampleCode(x, y, w, h) { 201 | var code = codeTemplate; 202 | var props = [ 203 | ['container', '#container'], 204 | ['x', x], 205 | ['y', y] 206 | ]; 207 | var prop; 208 | w && props.push(['w', w]); 209 | h && props.push(['h', h]); 210 | for (var i = 0, ii = props.length; i < ii; i++) { 211 | prop = props[i]; 212 | // Really basic string encoding because we're working with known strings 213 | if (typeof prop[1] == 'string') { 214 | prop[1] = "'" + prop[1] + "'"; 215 | } 216 | props[i] = ' ' + prop.join(': '); 217 | } 218 | code = code.replace('/#props#/', props.join(',\n')); 219 | return code; 220 | } 221 | 222 | function showExampleCode() { 223 | var edgeX = detectionOptions.edgeX, 224 | edgeY = detectionOptions.edgeY, 225 | x = edgeX === 'r' ? '100%' : 0, 226 | y = edgeY === 'b' ? '100%' : 0, 227 | w = edgeX === 'm' ? '100%' : 0, 228 | h = edgeY === 'm' ? '100%' : 0; 229 | $output.text(generateExampleCode(x, y, w, h)); 230 | } 231 | 232 | }); 233 | -------------------------------------------------------------------------------- /demo/demo-main.js: -------------------------------------------------------------------------------- 1 | // can be overwritten in console 2 | var opts = { 3 | includeSelf: false, 4 | sameX: false, 5 | sameY: false, 6 | showGuides: true, 7 | tolerance: 1 8 | }; 9 | 10 | $(function () { 11 | function rand(min, max) { 12 | return Math.round(Math.random() * (max - min)) + min; 13 | } 14 | 15 | // Setup random elements 16 | var elemCount = 10, 17 | minSize = 50, 18 | maxSize = 150, 19 | $menu = $('#menu'), 20 | $container = $('#container'), 21 | $footer = $('#footer'), 22 | menuWidth = $menu.outerWidth(), 23 | contWidth = $container.width(), 24 | contHeight = $container.height(), 25 | footerHeight = $footer.outerHeight(), 26 | size, x, y, $blocks; 27 | while (elemCount--) { 28 | size = rand(minSize, maxSize); 29 | x = rand(menuWidth, contWidth - size); 30 | y = rand(0, contHeight - size - footerHeight); 31 | $('
').css({ 32 | width: size, 33 | height: size, 34 | left: x, 35 | top: y 36 | }).appendTo($container); 37 | } 38 | $blocks = $('.block'); 39 | 40 | var $guidePointHoriz = $('#pointHoriz'), 41 | $guidePointVert = $('#pointVert'), 42 | $guidePointDiag = $container.find('.guidePointDiag'), 43 | $guideBlockHoriz = $('#blockHoriz'), 44 | $guideBlockVert = $('#blockVert'), 45 | $guideTextDiag = $container.find('.guideText'), 46 | $guidePointAll = $container.find('.guidePoint, .guideText, .guideTol'), 47 | $guideBlockAll = $container.find('.guideBlock'), 48 | $guideTolAll = $container.find('.guideTol'), 49 | $guideTolInner = $('#tolInner'), 50 | $guideTolOuter = $('#tolOuter'), 51 | showGuides; 52 | 53 | function updatePointGuideDisplay() { 54 | showGuides = opts.showGuides && !(opts.sameX && opts.sameY); 55 | if (showGuides) { 56 | $guidePointHoriz.toggle(!opts.sameX); 57 | $guidePointVert.toggle(!opts.sameY); 58 | $guidePointDiag.add($guideTextDiag).toggle(!opts.sameX || !opts.sameY); 59 | (!opts.sameX && opts.sameY) || $guideBlockHoriz.hide(); 60 | (!opts.sameY && opts.sameX) || $guideBlockVert.hide(); 61 | $guideTolAll.show(); 62 | } else { 63 | $guidePointAll.hide(); 64 | $guideBlockAll.hide(); 65 | } 66 | } 67 | 68 | // Controls 69 | $('#menu input').click(function () { 70 | if (this.type === 'checkbox') { 71 | opts[this.name] = this.checked; 72 | updatePointGuideDisplay(); 73 | } 74 | }); 75 | updatePointGuideDisplay(); 76 | var $toleranceRange = $('#toleranceRange'), 77 | $toleranceValue = $('#toleranceValue'); 78 | $toleranceRange.change(function () { 79 | var tolerance = $toleranceRange.val(); 80 | $toleranceValue.text(tolerance); 81 | opts.tolerance = +tolerance; 82 | }); 83 | 84 | // Calculate a tolerance circle 85 | function showTolerance($elem, x, y, dist, invert) { 86 | var wh = dist * 2, 87 | tol = opts.tolerance, 88 | wh2; 89 | if (invert) { 90 | wh -= tol * 2; 91 | } 92 | wh2 = (wh + tol * 2) / 2; 93 | $elem.css({ 94 | borderWidth: tol, 95 | left: x - wh2, 96 | top: y - wh2, 97 | width: wh, 98 | height: wh 99 | }); 100 | } 101 | 102 | // Demo for $.nearest 103 | //* 104 | var lastLineCount = 0; 105 | $(document).mousemove(function (e) { 106 | var x = e.pageX, 107 | y = e.pageY, 108 | point = {x: x, y: y}; 109 | if (opts.showGuides) { 110 | !opts.sameX && $guidePointHoriz.css({top: y}); 111 | !opts.sameY && $guidePointVert.css({left: x}); 112 | } 113 | var $nearest = $blocks.removeClass('nearestFilter nearestFind furthestFind') 114 | .nearest(point, opts) 115 | .addClass('nearestFilter'); 116 | $.nearest(point, $blocks, opts).addClass('nearestFind'); 117 | var $furthest = $.furthest(point, $blocks, opts).addClass('furthestFind'); 118 | var nearestCount = $nearest.length; 119 | 120 | // Add an indicator line for nearest blocks 121 | if (showGuides) { 122 | var minDist = Infinity, 123 | maxDist = 0; 124 | $nearest.each2(function (i, $n) { 125 | var off = $n.offset(), 126 | nx1 = off.left, 127 | ny1 = off.top, 128 | nx2 = nx1 + $n.outerWidth(), 129 | ny2 = ny1 + $n.outerHeight(), 130 | maxX1 = Math.max(x, nx1), 131 | minX2 = Math.min(x, nx2), 132 | maxY1 = Math.max(y, ny1), 133 | minY2 = Math.min(y, ny2), 134 | intersectX = minX2 >= maxX1, 135 | intersectY = minY2 >= maxY1, 136 | from = {x:x, y:y}, 137 | to = { 138 | x: intersectX ? x : nx2 < x ? nx2 : nx1, 139 | y: intersectY ? y : ny2 < y ? ny2 : ny1 140 | }, 141 | $lineElem = $('#pointDiag' + i).show(), 142 | $lineText = $('#textDiag' + i).show(), 143 | lineProps; 144 | // Make sure the line guide exists 145 | if (!$lineElem.length) { 146 | $lineElem = $('#pointDiag0') 147 | .clone() 148 | .attr('id', 'pointDiag' + i) 149 | .insertAfter('#pointDiag0'); 150 | $guidePointDiag = $guidePointDiag.add($lineElem); 151 | $guidePointAll = $guidePointAll.add($lineElem); 152 | } 153 | // Make sure the distance text exists 154 | if (!$lineText.length) { 155 | $lineText = $('#textDiag0') 156 | .clone() 157 | .attr('id', 'textDiag' + i) 158 | .insertAfter('#textDiag0'); 159 | $guideTextDiag = $guideTextDiag.add($lineText); 160 | $guidePointAll = $guidePointAll.add($lineText); 161 | } 162 | // Draw the line and cache its properties 163 | lineProps = $.line(from, to, { 164 | elem: $lineElem, 165 | lineColor: 'red', 166 | lineWidth: 3, 167 | returnValues: true 168 | }); 169 | 170 | // Add distance text 171 | var distX = to.x - from.x, 172 | distY = to.y - from.y, 173 | hypot = Math.sqrt(distX * distX + distY * distY); 174 | $lineText.text((Math.round(hypot * 100) / 100) + 'px'); 175 | var pointX = lineProps.center.x, 176 | pointY = lineProps.center.y, 177 | textW = $lineText.outerWidth(), 178 | textH = $lineText.outerHeight(), 179 | textW2 = textW / 2, 180 | textH2 = textH / 2; 181 | $lineText.css({ 182 | left: pointX - textW2, 183 | top: pointY - textH2 184 | }); 185 | if (hypot < minDist) { 186 | minDist = hypot; 187 | } 188 | }); 189 | // Get furthest distance for tolerance guide 190 | $furthest.each2(function (i, $n) { 191 | var off = $n.offset(), 192 | nx1 = off.left, 193 | ny1 = off.top, 194 | nx2 = nx1 + $n.outerWidth(), 195 | ny2 = ny1 + $n.outerHeight(), 196 | maxX1 = Math.max(x, nx1), 197 | minX2 = Math.min(x, nx2), 198 | maxY1 = Math.max(y, ny1), 199 | minY2 = Math.min(y, ny2), 200 | intersectX = minX2 >= maxX1, 201 | intersectY = minY2 >= maxY1, 202 | from = {x:x, y:y}, 203 | to = { 204 | x: intersectX ? x : nx2 < x ? nx2 : nx1, 205 | y: intersectY ? y : ny2 < y ? ny2 : ny1 206 | }, 207 | distX = to.x - from.x, 208 | distY = to.y - from.y, 209 | hypot = Math.sqrt(distX * distX + distY * distY); 210 | if (hypot > maxDist) { 211 | maxDist = hypot; 212 | } 213 | }); 214 | // Add tolerance guides 215 | if (nearestCount) { 216 | showTolerance($guideTolInner.show(), x, y, minDist); 217 | showTolerance($guideTolOuter.show(), x, y, maxDist, true); 218 | } else { 219 | $guideTolAll.hide(); 220 | } 221 | // Hide any unwanted lines/text 222 | for (var i = nearestCount; i < lastLineCount; i++) { 223 | $('#pointDiag' + i).add('#textDiag' + i).hide(); 224 | } 225 | } 226 | lastLineCount = showGuides ? nearestCount : 0; 227 | }); 228 | 229 | // Demo for $.fn.nearest 230 | $blocks.click(function () { 231 | var $this = $(this); 232 | if (opts.showGuides) { 233 | opts.sameX && !opts.sameY && $guideBlockVert.css({left: $this.css('left'), width: $this.outerWidth()}).show(); 234 | opts.sameY && !opts.sameX && $guideBlockHoriz.css({top: $this.css('top'), height: $this.outerHeight()}).show(); 235 | } 236 | $blocks.removeClass('nearestClick furthestClick').children().text(''); 237 | $this.addClass('nearestClick').children().text('CLICKED'); 238 | $this.nearest($blocks, opts).addClass('nearestClick').children().text('nearest'); 239 | $this.furthest($blocks, opts).addClass('furthestClick').children().text('furthest'); 240 | }); 241 | }); 242 | -------------------------------------------------------------------------------- /demo/edges.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Edges/100% Demo - jQuery Nearest Element plugin 5 | 6 | 31 | 32 | 33 | 59 | 69 |
70 |
71 |
72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Demo - jQuery Nearest Element plugin 5 | 6 | 42 | 43 | 44 | 70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /inc/index.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | var $doc = $(document); 3 | var $body = $('body'); 4 | var $demoElems = $('pre, code').filter(':not(pre code)'); 5 | var $demoOverlay; 6 | 7 | var demoInited = false; 8 | var demoEnabled = false; 9 | var demoClass = 'demo-nearest'; 10 | 11 | function handleMouseMove(e) { 12 | var point = { 13 | x: e.pageX, 14 | y: e.pageY 15 | }; 16 | $demoElems 17 | .removeClass(demoClass) 18 | .nearest(point) 19 | .addClass(demoClass); 20 | } 21 | 22 | function toggleDemo(e) { 23 | demoEnabled = !demoEnabled; 24 | $doc[demoEnabled ? 'on' : 'off']('mousemove', handleMouseMove); 25 | if (demoEnabled) { 26 | if (!demoInited) { 27 | $headers = $('h1, h2, h3, h4, h5, h6').map(function (i, elem) { 28 | return $(elem).wrapInner('').children('span').get(); 29 | }); 30 | $demoElems = $demoElems.add($headers).addClass('demo-set'); 31 | $demoOverlay = $('
').appendTo('#wrapper'); 32 | demoInited = true; 33 | } 34 | setTimeout(function(){ 35 | $body.addClass('demo-enabled'); 36 | }, 0); 37 | if (e) { 38 | handleMouseMove(e); 39 | } 40 | } else { 41 | $demoElems.removeClass(demoClass); 42 | $body.removeClass('demo-enabled'); 43 | } 44 | } 45 | 46 | $doc.on('dblclick', toggleDemo); 47 | })(jQuery); -------------------------------------------------------------------------------- /inc/lib/jquery-loader.js: -------------------------------------------------------------------------------- 1 | // Copied from grunt-init-jquery plugin 2 | (function() { 3 | // Default to the local version. 4 | var path = '../inc/lib/jquery-1.7.1.js'; 5 | // Get any jquery=___ param from the query string. 6 | var jqversion = location.search.match(/[?&]jquery=(.*?)(?=&|$)/); 7 | // If a version was specified, use that version from code.jquery.com. 8 | if (jqversion) { 9 | path = 'http://code.jquery.com/jquery-' + jqversion[1] + '.js'; 10 | } 11 | // This is the only time I'll ever use document.write, I promise! 12 | document.write(''); 13 | }()); 14 | -------------------------------------------------------------------------------- /inc/lib/jquery.ba-each2.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery each2 - v0.2 - 8/02/2010 3 | * http://benalman.com/projects/jquery-misc-plugins/ 4 | * 5 | * Inspired by James Padolsey's quickEach 6 | * http://gist.github.com/500145 7 | * 8 | * Copyright (c) 2010 "Cowboy" Ben Alman 9 | * Dual licensed under the MIT and GPL licenses. 10 | * http://benalman.com/about/license/ 11 | */ 12 | (function(a){var b=a([1]);a.fn.each2=function(d){var c=-1;while((b.context=b[0]=this[++c])&&d.call(b[0],c,b)!==false){}return this}})(jQuery); -------------------------------------------------------------------------------- /inc/lib/jquery.domline.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery DOM Line plugin v0.1.2 3 | * Copyright (c) 2011 Gilmore Davidson 4 | * https://gilmoreorless.github.com/jquery-dom-line/ 5 | * Licensed under the MIT licence 6 | */ 7 | (function(e,t){function n(e){return e.x===t&&e.y===t?!1:(e.x=parseFloat(e.x)||0,e.y=parseFloat(e.y)||0,e)}function i(t,n,i){var s=[t.x,t.y,n.x,n.y,i.w,i.h].join(",");if(r[s])return e.extend({},r[s]);var o=Math.abs(n.x-t.x),u=Math.abs(n.y-t.y),a=!o||!u?o||u:Math.sqrt(o*o+u*u),f=Math.min(t.x,n.x),l=Math.min(t.y,n.y),c=f+o/2,h=l+u/2,p,d=c-a/2,v=h,m=r[s]={width:a};return i.w>1&&(m.width-=i.w-1),i.h>1&&(v-=i.h/2),i.bw&&(d+=i.bw/2),i.bh&&(v+=i.bh/2),e.support.matrixFilter||(d-=i.l,v-=i.t),d=Math.round(d),v=Math.round(v),m.width=Math.round(m.width),o?u?p=(180+Math.atan2(t.y-n.y,t.x-n.x)*180/Math.PI+360)%360:p=t.x",{"class":o.className}),a={position:"absolute",backgroundColor:o.lineColor,width:1,height:o.lineWidth},f,l,c,h=u;return u.css(a),u[0].parentNode||u.appendTo("body"),l={w:u.outerWidth(),h:u.outerHeight(),l:parseFloat(u.css("marginLeft"))||0,t:parseFloat(u.css("marginTop"))||0},e.support.matrixFilter&&(l.bw=(parseFloat(u.css("borderLeftWidth"))||0)+(parseFloat(u.css("borderRightWidth"))||0),l.bh=(parseFloat(u.css("borderTopWidth"))||0)+(parseFloat(u.css("borderBottomWidth"))||0)),f=i(t,r,l),c=f.extra,delete f.extra,u.css(f),o.returnValues&&(h={from:t,to:r,center:c.center,rotation:c.rotation}),h},e.line.defaults={elem:"",className:"jquery-line",lineWidth:1,lineColor:"#000",returnValues:!1}})(jQuery); 8 | /* 9 | * transform: A light jQuery cssHooks for 2d transform 10 | * 11 | * limitations: 12 | * - requires jQuery 1.4.3+ 13 | * - Should you use the *translate* property, then your elements need to be absolutely positionned in a relatively positionned wrapper **or it will fail in IE678**. 14 | * - incompatible with "matrix(...)" transforms 15 | * - transformOrigin is not accessible 16 | * 17 | * latest version and complete README available on Github: 18 | * https://github.com/louisremi/jquery.transform.js 19 | * 20 | * Copyright 2011 @louis_remi 21 | * Licensed under the MIT license. 22 | * 23 | * This saved you an hour of work? 24 | * Send me music http://www.amazon.co.uk/wishlist/HNTU0468LQON 25 | * 26 | */ 27 | if(!jQuery.cssHooks.transform)(function(a){function m(a){return~a.indexOf("deg")?parseInt(a,10)*(Math.PI*2/360):~a.indexOf("grad")?parseInt(a,10)*(Math.PI/200):parseFloat(a)}function l(b){b=b.split(")");var c=[0,0],d=0,e=[1,1],f=[0,0],g=b.length-1,h=a.trim,i,j;while(g--){i=b[g].split("("),j=i[1];switch(h(i[0])){case"translateX":c[0]+=parseInt(j,10);break;case"translateY":c[1]+=parseInt(j,10);break;case"translate":j=j.split(","),c[0]+=parseInt(j[0],10),c[1]+=parseInt(j[1]||0,10);break;case"rotate":d+=m(j);break;case"scaleX":e[0]*=j;case"scaleY":e[1]*=j;case"scale":j=j.split(","),e[0]*=j[0],e[1]*=j.length>1?j[1]:j[0];break;case"skewX":f[0]+=m(j);break;case"skewY":f[1]+=m(j);break;case"skew":j=j.split(","),f[0]+=m(j[0]),f[1]+=m(j[1]||"0")}}return{translate:c,rotate:d,scale:e,skew:f}}var b=document.createElement("div"),c=b.style,d="transform",e="Transform",f=["O"+e,"ms"+e,"Webkit"+e,"Moz"+e,d],g=f.length,h,i,j,k=/Matrix([^)]*)/;while(g--)if(f[g]in c){a.support[d]=h=f[g];continue}h||(a.support.matrixFilter=i=c.filter===""),b=c=null,a.cssNumber[d]=!0,a.cssHooks[d]=j={get:function(b){var c=a.data(b,"transform")||{translate:[0,0],rotate:0,scale:[1,1],skew:[0,0]};c.toString=function(){return"translate("+this.translate[0]+"px,"+this.translate[1]+"px) rotate("+this.rotate+"rad) scale("+this.scale+") skew("+this.skew[0]+"rad,"+this.skew[1]+"rad)"};return c},set:function(b,c,d){typeof c=="string"&&(c=l(c));var e=c.translate,f=c.rotate,g=c.scale,j=c.skew,m=b.style,n,o;a.data(b,"transform",c),!j[0]&&!j[1]&&(j=0);if(h)m[h]="translate("+e[0]+"px,"+e[1]+"px) rotate("+f+"rad) scale("+g+")"+(j?" skew("+j[0]+"rad,"+j[1]+"rad)":"");else if(i){d||(m.zoom=1);var p=Math.cos(f),q=Math.sin(f),r=p*g[0],s=-q*g[1],t=q*g[0],u=p*g[1],v,w,x,y;j&&(v=Math.tan(j[0]),w=Math.tan(j[1]),r+=s*w,s+=r*v,t+=u*w,u+=t*v),x=["Matrix(M11="+r,"M12="+s,"M21="+t,"M22="+u,"SizingMethod='auto expand'"].join(),o=(n=b.currentStyle)&&n.filter||m.filter||"",m.filter=k.test(o)?o.replace(k,x):o+" progid:DXImageTransform.Microsoft."+x+")";if(y=a.transform.centerOrigin)m[y=="margin"?"marginLeft":"left"]=-(b.offsetWidth/2)+b.clientWidth/2+"px",m[y=="margin"?"marginTop":"top"]=-(b.offsetHeight/2)+b.clientHeight/2+"px";m.left=e[0]+"px",m.top=e[1]+"px"}}},a.fx.step.transform=function(a){var b=a.elem,c=a.start,d=a.end,e=a.pos,f={},g;if(!c||typeof d=="string")c||(c=j.get(b)),i&&(b.style.zoom=1),a.end=d=l(d);f.translate=[c.translate[0]+(d.translate[0]-c.translate[0])*e+.5|0,c.translate[1]+(d.translate[1]-c.translate[1])*e+.5|0],f.rotate=c.rotate+(d.rotate-c.rotate)*e,f.scale=[c.scale[0]+(d.scale[0]-c.scale[0])*e,c.scale[1]+(d.scale[1]-c.scale[1])*e],f.skew=[c.skew[0]+(d.skew[0]-c.skew[0])*e,c.skew[1]+(d.skew[1]-c.skew[1])*e],j.set(b,f,!0)},a.transform={centerOrigin:"margin",radToDeg:function(a){return a*180/Math.PI}}})(jQuery) -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | jQuery Nearest Element plugin 5 | 6 | 7 | 8 | 41 | 42 | 43 |
44 |
45 |

jQuery Nearest Element plugin v1.4.0

46 |

Quick Links

47 | 54 |

Available on npm (npm install jquery-nearest) 55 | and Bower (bower install jquery-nearest).

56 |

Intro

57 |

58 | This plugin finds the elements in a page that are closest to a particular point or element, based on pixel dimensions. 59 | This is not to be confused with jQuery's own .closest() method, 60 | which is based on the DOM tree, not pixels. 61 |

62 |

63 | There are also helper methods to find elements that are the furthest away from or touching a point or element. 64 |

65 |

Reference

66 |

Methods

67 |

68 | There are three main methods: 69 |

70 |
    71 |
  • .nearest() returns the elements that have the shortest pixel distance to a point or element.
  • 72 |
  • .furthest() returns the elements that have the longest pixel distance to a point or element.
  • 73 |
  • .touching() returns the elements that are touching a point or element (ie. their pixel distance is 0).
  • 74 |
75 |

Usage

76 |

77 | Each of the three methods has a utility function on the global jQuery object and a jQuery prototype function for working with element sets. 78 | The following examples all use nearest as the method, but furthest and touching work in the same way. 79 |

80 | 81 |

Utility Function

82 |
$.nearest(pointObject, selector[, options]);
83 |

84 | Finds the DOM elements matching selector that are closest to a point or region on the page. 85 |

86 |

87 | pointObject is an object with x and y (numeric) properties that define a point on the page (relative to the top left corner of the page, not the screen).
88 | Optionally, you can also specify w and h properties to define the width and height respectively of a region in the page instead of a single point – in this case the x and y properties define the top left corner of the region. 89 |

90 |

91 | The x, y, w and h properties can also be defined as a percentage string, relative to the container option (defined in the options section). 92 |

93 |

94 | selector is any valid jQuery selector or object that defines an element set to be filtered by the function. 95 |

96 |

97 | options is an optional collection of parameters to pass to the function. More details can be found in the options section. 98 |

99 |
Example
100 |
// Find the image(s) closest to a point 300px in and 100px down from the top left corner
101 | var $closestToPoint = $.nearest({x: 300, y: 100}, 'img');
102 | 
103 | // Find the elements with class 'floating' closest to a particular region in the page
104 | var $elemSet = $('.floating');
105 | var $closestToRegion = $.nearest({x: 300, y: 100, w: 200, h: 200}, $elemSet);
106 | 
107 | // Find the elements with class 'floating' closest to the right-hand side of the document
108 | var $closestToRightEdge = $.nearest({x: '100%', y: 0, h: '100%'}, '.floating');
109 | 
110 | 111 |

Single Element Operation

112 |
$(elem).nearest(selector[, options]);
113 |

114 | Finds the DOM elements matching selector that are closest to elem. 115 | If more than one element is in $(elem) then only the first member will be used as a point of reference. 116 |

117 |

118 | selector is any valid jQuery selector or object that defines an element set to be filtered by the function. 119 |

120 |

121 | options is an optional collection of parameters to pass to the function. More details can be found in the options section. 122 |

123 |
Example
124 |
var $basicExample = $('#something').nearest('.surrounding');
125 | 
126 | // Only the first element in this set will be used for matching
127 | var $set = $('.middle');
128 | // Selector can be any jQuery object
129 | var $convolutedExample = $set.nearest($set.siblings());
130 | 
131 | 132 |

Multiple Element Filter

133 |
$(elemSet).nearest(pointObject[, options]);
134 |

135 | Filters elemSet to return only the members that are closest to the point or region defined by pointObject. 136 |

137 |

138 | This is effectively the same as calling $.nearest(pointObject, elemSet) but with the benefit of not breaking method chains – see chaining for more details. 139 |

140 |

141 | pointObject is an object as defined in the Utility Function reference. 142 |

143 |

144 | options is an optional collection of parameters to pass to the function. More details can be found in the options section. 145 |

146 |
Example
147 |
var $set = $('.filterme');
148 | 
149 | // Filter the set by distance to a point
150 | var $closestToPoint = $set.nearest({x: 300, y: 100});
151 | 
152 | // Filter the set by distance to a region
153 | var $closestToRegion = $set.nearest({x: 300, y: 100, w: 200, h: 200});
154 | 
155 | 156 |

Options

157 |

158 | All methods take an optional options object to further refine the results. 159 | The object can have the following parameters: 160 |

161 |
162 |
includeSelf
163 |
Boolean – default false (not used by the utility methods)
164 |
If true, the reference element used in a Single Element Operation will also 165 | be included in the distance calculations.
166 | Example: $(elem).nearest('.droppable', {includeSelf: true});
167 | 168 |
onlyX
169 |
Boolean – default false (not used by .touching())
170 |
If true, the function will ignore the Y axis and only the X distance from the reference point will be used.
171 | 172 |
onlyY
173 |
Boolean – default false (not used by .touching())
174 |
If true, the function will ignore the X axis and only the Y distance from the reference point will be used.
175 | 176 |
sameX
177 |
Boolean – default false (not used by .touching())
178 |
If true, the function will not check for elements along the X axis, 179 | meaning only elements directly above or below the reference point will be matched.
180 | This will override onlyX: true if both are provided.
181 | 182 |
sameY
183 |
Boolean – default false (not used by .touching())
184 |
If true, the function will not check for elements along the Y axis, 185 | meaning only elements directly to the left or right of the reference point will be matched.
186 | This will override onlyY: true if both are provided.
187 | 188 |
tolerance
189 |
Integer – default 1
190 |
Number of pixels to allow for when calculating distances, to account for fractional pixel values.
191 | For example, a tolerance of 1 would match distances of 23px, 23.45px and 24px as being closest, but not 24.1px.
192 | 193 |
container
194 |
DOM node or jQuery selector – default document
195 |
Acts as a root node, so that only elements within the container will be matched.
196 | Also used with percentage options for x, y, w and h – the top left corner of the container is 0%, the bottom right corner is 100%.
197 | 198 |
directionConstraints
199 |
String array – default empty
200 |
A list of directions to limit the search. Available values: 201 | "left", "right", "top", "bottom"
202 | For example, ["left", "top"] will only match elements to the top left of the reference point.
203 | 204 |
sort
205 |
String – default empty
206 |
Sorts results based on distance, defaults to no sorting (results are returned in DOM order).
207 | Available values: "nearest" or "furthest"
208 | 209 |
210 |
211 |
checkHoriz
212 |
Boolean – default trueDeprecated in 1.3
213 |
The checkHoriz: false option is deprecated and will be removed in a future version. Use sameX: true instead (they are equivalent).
214 | 215 |
checkVert
216 |
Boolean – default trueDeprecated in 1.3
217 |
The checkVert: false option is deprecated and will be removed in a future version. Use sameY: true instead (they are equivalent).
218 |
219 |

220 | A example of using different options can be found on the demonstration page. 221 |

222 | 223 |

Notes

224 |
    225 |
  • 226 | The selector parameter is technically optional in all methods, and will default to div if not present. 227 | However, it is strongly recommended that you pass in a selector, 228 | otherwise the method will loop through every single div element on the page, which – 229 | apart from being slow and computationally expensive – will most likely produce false positives 230 | by matching a reference element's parent nodes. 231 |
  • 232 |
  • 233 | Only set one of the sameX and sameY options to true in a single call, not both. 234 | If you set both to true, the .nearest() and .furthest() methods will fail to work (it is exactly the same as calling .touching() instead). 235 |
  • 236 |
237 | 238 |

Changelog

239 |
240 |
1.4.0
241 |
    242 |
  • New directionConstraints option 243 | (PR #29).
  • 244 |
  • New sort option 245 | (PR #29).
  • 246 |
247 |
1.3.1
248 |
    249 |
  • No code changes. Metadata updates only 250 | (#15, 251 | #16, 252 | #25).
  • 253 |
254 |
1.3.0
255 |
    256 |
  • New onlyX and onlyY options 257 | (PR #7).
  • 258 |
  • New sameX and sameY options as the inverse replacement for checkHoriz and checkVert 259 | (#9).
  • 260 |
  • Deprecated the checkHoriz and checkVert options.
  • 261 |
  • Bug fix for filtering on empty sets.
  • 262 |
  • Better calculation of dimensions for percentage-based options.
  • 263 |
264 |
1.2.2
265 |
    266 |
  • Bug fix to find only children of the container element if the option is present. 267 | (PR #5).
  • 268 |
269 |
1.2.1
270 |
273 |
1.2.0
274 |
    275 |
  • Added to Bower 276 | (#4).
  • 277 |
  • New container option and the ability to use percentage-based strings for x, y, w and h options 278 | (#2).
  • 279 |
280 |
1.1.0
281 |
    282 |
  • Added tolerance option to account for fractional pixel values and “close enough” situations 283 | (#1).
  • 284 |
285 |
1.0.0
286 |
    287 |
  • First stable release.
  • 288 |
289 |
290 |
291 |
292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-nearest", 3 | "title": "jQuery Nearest Element", 4 | "description": "jQuery plugin to find the elements in a page that are closest to (or furthest away from) a particular point or element, based on pixel dimensions", 5 | "version": "1.4.0", 6 | "keywords": ["jquery-plugin", "ecosystem:jquery", "nearest", "furthest", "touching", "pixel", "distance"], 7 | "main": "src/jquery.nearest.js", 8 | "files": [ 9 | "src/jquery.nearest.js", 10 | "src/jquery.nearest.min.js", 11 | "CONTRIBUTING.md", 12 | "LICENSE.txt", 13 | "README.md" 14 | ], 15 | "author": { 16 | "name": "Gilmore Davidson", 17 | "url": "https://shoehornwithteeth.com/" 18 | }, 19 | "homepage": "http://gilmoreorless.github.io/jquery-nearest/", 20 | "repository": { 21 | "type": "git", 22 | "url": "git://github.com/gilmoreorless/jquery-nearest.git" 23 | }, 24 | "bugs": "https://github.com/gilmoreorless/jquery-nearest/issues", 25 | "licenses": [ 26 | { 27 | "type": "MIT", 28 | "url": "http://gilmoreorless.github.io/jquery-nearest/LICENSE.txt" 29 | } 30 | ], 31 | "dependencies": { 32 | "jquery": ">=1.4" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/jquery.nearest.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Nearest plugin v1.4.0 3 | * 4 | * Finds elements closest to a single point based on screen location and pixel dimensions 5 | * http://gilmoreorless.github.io/jquery-nearest/ 6 | * Copyright (c) 2011-2015 Gilmore Davidson under the MIT licence: 7 | * http://gilmoreorless.github.io/jquery-nearest/LICENSE.txt 8 | * 9 | * Requires jQuery 1.4 or above 10 | * Also supports Ben Alman's "each2" plugin for faster looping (if available) 11 | */ 12 | 13 | /** 14 | * Method signatures: 15 | * 16 | * $.nearest({x, y}, selector) - find $(selector) closest to point 17 | * $(elem).nearest(selector) - find $(selector) closest to elem 18 | * $(elemSet).nearest({x, y}) - filter $(elemSet) and return closest to point 19 | * 20 | * Also: 21 | * $.furthest() 22 | * $(elem).furthest() 23 | * 24 | * $.touching() 25 | * $(elem).touching() 26 | */ 27 | ;(function ($, undefined) { 28 | 29 | /** 30 | * Internal method that does the grunt work 31 | * 32 | * @param mixed selector Any valid jQuery selector providing elements to filter 33 | * @param hash options Key/value list of options for matching elements 34 | * @param mixed thisObj (optional) Any valid jQuery selector that represents self 35 | * for the "includeSelf" option 36 | * @return array List of matching elements, can be zero length 37 | */ 38 | var rPerc = /^([\d.]+)%$/; 39 | function nearest(selector, options, thisObj) { 40 | // Normalise selector, dimensions and constraints 41 | selector || (selector = 'div'); // I STRONGLY recommend passing in a selector 42 | var $container = $(options.container), 43 | containerOffset = $container.offset() || {left: 0, top: 0}, 44 | containerWH = [ 45 | $container.width() || 0, 46 | $container.height() || 0 47 | ], 48 | containerProps = { 49 | // prop: [min, max] 50 | x: [containerOffset.left, containerOffset.left + containerWH[0]], 51 | y: [containerOffset.top, containerOffset.top + containerWH[1]], 52 | w: [0, containerWH[0]], 53 | h: [0, containerWH[1]] 54 | }, 55 | directionConstraints = options.directionConstraints, 56 | prop, dims, match; 57 | for (prop in containerProps) if (containerProps.hasOwnProperty(prop)) { 58 | match = rPerc.exec(options[prop]); 59 | if (match) { 60 | dims = containerProps[prop]; 61 | options[prop] = (dims[1] - dims[0]) * match[1] / 100 + dims[0]; 62 | } 63 | } 64 | if (!$.isArray(directionConstraints)) { 65 | directionConstraints = (typeof directionConstraints === 'string') ? [directionConstraints] : []; 66 | } 67 | 68 | // Deprecated options - remove in 2.0 69 | if (options.sameX === false && options.checkHoriz === false) { 70 | options.sameX = !options.checkHoriz; 71 | } 72 | if (options.sameY === false && options.checkVert === false) { 73 | options.sameY = !options.checkVert; 74 | } 75 | 76 | // Get elements and work out x/y points 77 | var $all = $container.find(selector), 78 | cache = [], 79 | furthest = !!options.furthest, 80 | checkX = !options.sameX, 81 | checkY = !options.sameY, 82 | onlyX = !!options.onlyX, 83 | onlyY = !!options.onlyY, 84 | compDist = furthest ? 0 : Infinity, 85 | point1x = parseFloat(options.x) || 0, 86 | point1y = parseFloat(options.y) || 0, 87 | point2x = parseFloat(point1x + options.w) || point1x, 88 | point2y = parseFloat(point1y + options.h) || point1y, 89 | box = { 90 | x1: point1x, 91 | y1: point1y, 92 | x2: point2x, 93 | y2: point2y 94 | }, 95 | tolerance = parseFloat(options.tolerance) || 0, 96 | hasEach2 = !!$.fn.each2, 97 | // Shortcuts to help with compression 98 | min = Math.min, 99 | max = Math.max; 100 | 101 | // Normalise the remaining options 102 | if (!options.includeSelf && thisObj) { 103 | $all = $all.not(thisObj); 104 | } 105 | if (tolerance < 0) { 106 | tolerance = 0; 107 | } 108 | // Loop through all elements and check their positions 109 | $all[hasEach2 ? 'each2' : 'each'](function (i, elem) { 110 | var $this = hasEach2 ? elem : $(this), 111 | off = $this.offset(), 112 | x = off.left, 113 | y = off.top, 114 | w = $this.outerWidth(), 115 | h = $this.outerHeight(), 116 | x2 = x + w, 117 | y2 = y + h, 118 | maxX1 = max(x, point1x), 119 | minX2 = min(x2, point2x), 120 | maxY1 = max(y, point1y), 121 | minY2 = min(y2, point2y), 122 | thisBox = { 123 | x1: x, 124 | y1: y, 125 | x2: x2, 126 | y2: y2 127 | }, 128 | intersectX = minX2 >= maxX1, 129 | intersectY = minY2 >= maxY1, 130 | distX, distY, distT, isValid; 131 | if ( 132 | // .nearest() / .furthest() 133 | (checkX && checkY) || 134 | // .touching() 135 | (!checkX && !checkY && intersectX && intersectY) || 136 | // .nearest({sameY: true}) 137 | (checkX && intersectY) || 138 | // .nearest({sameX: true}) 139 | (checkY && intersectX) || 140 | // .nearest({onlyX: true}) 141 | (checkX && onlyX) || 142 | // .nearest({onlyY: true}) 143 | (checkY && onlyY) 144 | ) { 145 | distX = intersectX ? 0 : maxX1 - minX2; 146 | distY = intersectY ? 0 : maxY1 - minY2; 147 | if (onlyX || onlyY) { 148 | distT = onlyX ? distX : distY; 149 | } else { 150 | distT = intersectX || intersectY ? 151 | max(distX, distY) : 152 | Math.sqrt(distX * distX + distY * distY); 153 | } 154 | 155 | isValid = furthest ? 156 | distT >= compDist - tolerance : 157 | distT <= compDist + tolerance; 158 | if (!checkDirectionConstraints(box, thisBox, directionConstraints)) { 159 | isValid = false; 160 | } 161 | 162 | if (isValid) { 163 | compDist = furthest ? 164 | max(compDist, distT) : 165 | min(compDist, distT); 166 | cache.push({ 167 | node: this, 168 | dist: distT 169 | }); 170 | } 171 | } 172 | }); 173 | 174 | if (options.sort === 'nearest') { 175 | cache.sort(function(a,b) { return a.dist - b.dist; }); 176 | } else if (options.sort === 'furthest') { 177 | cache.sort(function(a,b) { return b.dist - a.dist; }); 178 | } 179 | 180 | // Make sure all cached items are within tolerance range 181 | var len = cache.length, 182 | filtered = [], 183 | compMin, compMax, 184 | i, item; 185 | if (len) { 186 | if (furthest) { 187 | compMin = compDist - tolerance; 188 | compMax = compDist; 189 | } else { 190 | compMin = compDist; 191 | compMax = compDist + tolerance; 192 | } 193 | for (i = 0; i < len; i++) { 194 | item = cache[i]; 195 | if (item.dist >= compMin && item.dist <= compMax) { 196 | filtered.push(item.node); 197 | } 198 | } 199 | } 200 | 201 | return filtered; 202 | } 203 | 204 | function checkDirectionConstraints(refBox, itemBox, constraints) { 205 | var results = { 206 | left: refBox.x1 > itemBox.x1, 207 | right: refBox.x2 < itemBox.x2, 208 | top: refBox.y1 > itemBox.y1, 209 | bottom: refBox.y2 < itemBox.y2 210 | }; 211 | 212 | return constraints.reduce(function(result, direction) { 213 | return result && !!results[direction]; 214 | }, true); 215 | } 216 | 217 | $.each(['nearest', 'furthest', 'touching'], function (i, name) { 218 | 219 | // Internal default options 220 | // Not exposed publicly because they're method-dependent and easily overwritten anyway 221 | var defaults = { 222 | x: 0, // X position of top left corner of point/region 223 | y: 0, // Y position of top left corner of point/region 224 | w: 0, // Width of region 225 | h: 0, // Height of region 226 | tolerance: 1, // Distance tolerance in pixels, mainly to handle fractional pixel rounding bugs 227 | container: document, // Container of objects for calculating %-based dimensions 228 | furthest: name == 'furthest', // Find max distance (true) or min distance (false) 229 | includeSelf: false, // Include 'this' in search results (t/f) - only applies to $(elem).func(selector) syntax 230 | sameX: name === 'touching', // Only match for the same X axis values (t/f) 231 | sameY: name === 'touching', // Only match for the same Y axis values (t/f) 232 | onlyX: false, // Only check X axis variations (t/f) 233 | onlyY: false, // Only check Y axis variations (t/f), 234 | directionConstraints: [], // Array of directions to limit search: 'left', 'right' ,'top', 'bottom' 235 | sort: false // Sort results based on distance: 'nearest', 'furthest' 236 | }; 237 | 238 | /** 239 | * $.nearest() / $.furthest() / $.touching() 240 | * 241 | * Utility functions for finding elements near a specific point or region on screen 242 | * 243 | * @param hash point Co-ordinates for the point or region to measure from 244 | * "x" and "y" keys are required, "w" and "h" keys are optional 245 | * @param mixed selector Any valid jQuery selector that provides elements to filter 246 | * @param hash options (optional) Extra filtering options 247 | * Not technically needed as the options could go on the point object, 248 | * but it's good to have a consistent API 249 | * @return jQuery object containing matching elements in selector 250 | */ 251 | $[name] = function (point, selector, options) { 252 | if (!point || point.x === undefined || point.y === undefined) { 253 | return $([]); 254 | } 255 | var opts = $.extend({}, defaults, point, options || {}); 256 | return $(nearest(selector, opts)); 257 | }; 258 | 259 | /** 260 | * SIGNATURE 1: 261 | * $(elem).nearest(selector) / $(elem).furthest(selector) / $(elem).touching(selector) 262 | * 263 | * Finds all elements in selector that are nearest to/furthest from elem 264 | * 265 | * @param mixed selector Any valid jQuery selector that provides elements to filter 266 | * @param hash options (optional) Extra filtering options 267 | * @return jQuery object containing matching elements in selector 268 | * 269 | * SIGNATURE 2: 270 | * $(elemSet).nearest(point) / $(elemSet).furthest(point) / $(elemSet).touching(point) 271 | * 272 | * Filters elemSet to return only the elements nearest to/furthest from point 273 | * Effectively a wrapper for $.nearest(point, elemSet) but with the benefits of method chaining 274 | * 275 | * @param hash point Co-ordinates for the point or region to measure from 276 | * @return jQuery object containing matching elements in elemSet 277 | */ 278 | $.fn[name] = function (selector, options) { 279 | if (!this.length) { 280 | return this.pushStack([]); 281 | } 282 | var opts; 283 | if (selector && $.isPlainObject(selector)) { 284 | opts = $.extend({}, defaults, selector, options || {}); 285 | return this.pushStack(nearest(this, opts)); 286 | } 287 | var offset = this.offset(), 288 | dimensions = { 289 | x: offset.left, 290 | y: offset.top, 291 | w: this.outerWidth(), 292 | h: this.outerHeight() 293 | }; 294 | opts = $.extend({}, defaults, dimensions, options || {}); 295 | return this.pushStack(nearest(selector, opts, this)); 296 | }; 297 | }); 298 | })(jQuery); 299 | -------------------------------------------------------------------------------- /src/jquery.nearest.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Nearest plugin v1.4.0 3 | * 4 | * Finds elements closest to a single point based on screen location and pixel dimensions 5 | * http://gilmoreorless.github.io/jquery-nearest/ 6 | * Copyright (c) 2011-2015 Gilmore Davidson under the MIT licence: 7 | * http://gilmoreorless.github.io/jquery-nearest/LICENSE.txt 8 | * 9 | * Requires jQuery 1.4 or above 10 | * Also supports Ben Alman's "each2" plugin for faster looping (if available) 11 | */ 12 | !function(t,e){function r(e,r,o){e||(e="div");var s,a,h,u=t(r.container),c=u.offset()||{left:0,top:0},f=[u.width()||0,u.height()||0],d={x:[c.left,c.left+f[0]],y:[c.top,c.top+f[1]],w:[0,f[0]],h:[0,f[1]]},l=r.directionConstraints;for(s in d)d.hasOwnProperty(s)&&(h=i.exec(r[s]),h&&(a=d[s],r[s]=(a[1]-a[0])*h[1]/100+a[0]));t.isArray(l)||(l="string"==typeof l?[l]:[]),r.sameX===!1&&r.checkHoriz===!1&&(r.sameX=!r.checkHoriz),r.sameY===!1&&r.checkVert===!1&&(r.sameY=!r.checkVert);var y=u.find(e),p=[],x=!!r.furthest,m=!r.sameX,v=!r.sameY,g=!!r.onlyX,k=!!r.onlyY,w=x?0:1/0,X=parseFloat(r.x)||0,Y=parseFloat(r.y)||0,F=parseFloat(X+r.w)||X,S=parseFloat(Y+r.h)||Y,H={x1:X,y1:Y,x2:F,y2:S},M=parseFloat(r.tolerance)||0,b=!!t.fn.each2,j=Math.min,z=Math.max;!r.includeSelf&&o&&(y=y.not(o)),0>M&&(M=0),y[b?"each2":"each"](function(e,r){var i,o,s,a,h=b?r:t(this),u=h.offset(),c=u.left,f=u.top,d=h.outerWidth(),y=h.outerHeight(),C=c+d,O=f+y,P=z(c,X),V=j(C,F),W=z(f,Y),q=j(O,S),A={x1:c,y1:f,x2:C,y2:O},Q=V>=P,$=q>=W;(m&&v||!m&&!v&&Q&&$||m&&$||v&&Q||m&&g||v&&k)&&(i=Q?0:P-V,o=$?0:W-q,s=g||k?g?i:o:Q||$?z(i,o):Math.sqrt(i*i+o*o),a=x?s>=w-M:w+M>=s,n(H,A,l)||(a=!1),a&&(w=x?z(w,s):j(w,s),p.push({node:this,dist:s})))}),"nearest"===r.sort?p.sort(function(t,e){return t.dist-e.dist}):"furthest"===r.sort&&p.sort(function(t,e){return e.dist-t.dist});var C,O,P,V,W=p.length,q=[];if(W)for(x?(C=w-M,O=w):(C=w,O=w+M),P=0;W>P;P++)V=p[P],V.dist>=C&&V.dist<=O&&q.push(V.node);return q}function n(t,e,r){var n={left:t.x1>e.x1,right:t.x2e.y1,bottom:t.y2 2 | 3 | 4 | 5 | jQuery Nearest Test Suite 6 | 7 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 |
34 | 35 |
36 |
37 |
38 |
39 | 40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
50 | thing 51 | thing 52 |
thing
53 |
thing
54 |

thing 55 |

56 | 57 | 58 | 59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | 68 | 69 | 70 |
71 |
72 |
73 |
74 | 75 | 76 |
77 |
78 |
79 |
80 |
81 |
82 | 83 | 84 | -------------------------------------------------------------------------------- /test/nearest_test.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 3 | ////////// HELPER FUNCTIONS ////////// 4 | 5 | /** 6 | * Get an x/y point object that accounts for negative offset of #qunit-fixture 7 | */ 8 | function getPoint(x, y) { 9 | var offset = $('#qunit-fixture').offset(); 10 | return { 11 | x: typeof x === 'string' ? x : offset.left + x, 12 | y: typeof y === 'string' ? y : offset.top + y 13 | }; 14 | } 15 | 16 | /** 17 | * Get an x/y/w/h box object that accounts for negative offset of #qunit-fixture 18 | */ 19 | function getBox(x, y, w, h) { 20 | var box = getPoint(x, y); 21 | box.w = w; 22 | box.h = h; 23 | return box; 24 | } 25 | 26 | /** 27 | * Assert the length of a jQuery object, and that the elements have the correct IDs 28 | */ 29 | function assertSet($set, length) { 30 | var ids = Array.prototype.slice.call(arguments, 2); 31 | var suffix = ''; 32 | if (ids.length && typeof ids[0] === 'object') { 33 | if (ids[0].suffix) { 34 | suffix = ids[0].suffix; 35 | } 36 | ids.shift(); 37 | } 38 | var elemStr = length == 1 ? 'element' : 'elements'; 39 | equal($set.length, length, [length, elemStr, 'returned', suffix].join(' ')); 40 | for (var i = 0, ii = ids.length; i < ii; i++) { 41 | equal($set[i].id, ids[i], ['element at index', i, 'has ID of', ids[i], suffix].join(' ')); 42 | } 43 | } 44 | 45 | 46 | 47 | ////////// MODULE $.method basic usage ////////// 48 | 49 | module('$.method: basic'); 50 | 51 | test('empty set', 2, function () { 52 | var $set = $.nearest(); 53 | ok($set.jquery, 'returned set is a jQuery object'); 54 | assertSet($set, 0); 55 | }); 56 | 57 | test('one parameter defaults to div', 2, function () { 58 | var $set = $.nearest(getPoint(510, 10)); // point intersects with a span 59 | assertSet($set, 1); 60 | equal($set[0].nodeName, 'DIV', 'element is a div'); 61 | }); 62 | 63 | test('basic usage: nearest', 2, function () { 64 | var $set = $.nearest(getPoint(0, 0), '.basic-group'); 65 | assertSet($set, 1, 'basic-ref'); 66 | }); 67 | 68 | test('basic usage: furthest', 3, function () { 69 | var $set = $.furthest(getPoint(0, 0), '.basic-group'); 70 | assertSet($set, 1, 'basic-furthest'); 71 | 72 | $set = $.furthest(getPoint(10, 10), '#top-left'); 73 | equal($set[0].id, 'top-left', 'touching element returned if it\'s the only match'); 74 | }); 75 | 76 | test('basic usage: touching', 4, function () { 77 | var $set = $.touching(getPoint(0, 0)); 78 | // Should contain both the parent and child elements 79 | assertSet($set, 2, {suffix: 'when unfiltered'}, 'qunit-fixture', 'top-left'); 80 | 81 | $set = $.touching(getPoint(0, 0), '.basic-group'); 82 | assertSet($set, 0, {suffix: 'when filtered'}); 83 | }); 84 | 85 | test('invalid selector', 1, function () { 86 | assertSet($.nearest(getPoint(0, 0), '#does-not-exist'), 0); 87 | }); 88 | 89 | 90 | 91 | ////////// MODULE $.method dimensions ////////// 92 | 93 | module('$.method: dimensions'); 94 | 95 | var containerSelector = '#qunit-fixture'; 96 | var dimOpts = {container: containerSelector}; 97 | 98 | test('point', 5, function () { 99 | var $set = $.nearest(getPoint(530, 30), '.top-mid'); 100 | assertSet($set, 4, 'tmtl', 'tmbl', 'tmtr', 'tmbr'); 101 | }); 102 | 103 | test('box', 5, function () { 104 | var $set = $.nearest(getBox(525, 25, 10, 10), '.top-mid'); 105 | assertSet($set, 4, 'tmtl', 'tmbl', 'tmtr', 'tmbr'); 106 | }); 107 | 108 | test('x: 100%', 2, function () { 109 | var $set = $.touching(getPoint('100%', 0), '*', dimOpts); 110 | assertSet($set, 1, 'top-right'); 111 | }); 112 | 113 | test('y: 100%', 2, function () { 114 | var $set = $.touching(getPoint(0, '100%'), '*', dimOpts); 115 | assertSet($set, 1, 'bottom-left'); 116 | }); 117 | 118 | test('w: 100%', 3, function () { 119 | var $set = $.touching(getBox(0, 0, '100%', 0), '.corner', dimOpts); 120 | assertSet($set, 2, 'top-left', 'top-right'); 121 | }); 122 | 123 | test('h: 100%', 3, function () { 124 | var $set = $.touching(getBox(0, 0, 0, '100%'), '.corner', dimOpts); 125 | assertSet($set, 2, 'top-left', 'bottom-left'); 126 | }); 127 | 128 | test('x/h: 100%', 3, function () { 129 | var $set = $.touching(getBox('100%', 0, 0, '100%'), '.corner', dimOpts); 130 | assertSet($set, 2, 'top-right', 'bottom-right'); 131 | }); 132 | 133 | test('x/y: 50%', 2, function () { 134 | var $set = $.touching(getPoint('50%', '50%'), '*', dimOpts); 135 | assertSet($set, 1, 'dead-centre'); 136 | }); 137 | 138 | test('x/y: 0 + w/h: 100%', 1, function () { 139 | var $set = $.touching(getBox(0, 0, '100%', '100%'), '*', dimOpts); 140 | equal($set.length, $(containerSelector + ' *').length, '0,0 to 100%,100% returns all elements'); 141 | }); 142 | 143 | 144 | 145 | ////////// MODULE $.method options ////////// 146 | 147 | module('$.method: options', { 148 | setup: function () { 149 | this.midPoint = getPoint(250, 750); 150 | this.midBox = getBox(240, 740, 20, 20); 151 | this.tolPoint = getPoint(700, 200); 152 | this.contPoint = getPoint(700, 700); 153 | } 154 | }); 155 | 156 | // 100,600 -> 400,900 = 300x300; 50% = 250,750 157 | test('x/y sanity check', 5, function () { 158 | var $nearest = $.nearest( this.midPoint, '.xy-group'); 159 | var $furthest = $.furthest(this.midPoint, '.xy-group'); 160 | var $touching = $.touching(this.midPoint, '.xy-group'); 161 | 162 | assertSet($nearest, 1, {suffix: 'for nearest'}, 'xy-nearest-diag'); 163 | assertSet($furthest, 1, {suffix: 'for furthest'}, 'xy-furthest-diag'); 164 | assertSet($touching, 0, {suffix: 'for touching'}); 165 | }); 166 | 167 | test('sameX / checkHoriz', 10, function () { 168 | var $nearest = $.nearest( this.midBox, '.xy-group', {sameX: true}); 169 | var $furthest = $.furthest(this.midBox, '.xy-group', {sameX: true}); 170 | var $touching = $.touching(this.midBox, '.xy-group', {sameX: true}); 171 | 172 | assertSet($nearest, 1, {suffix: 'for nearest'}, 'xy-mid-sameX'); 173 | assertSet($furthest, 1, {suffix: 'for furthest'}, 'xy-mid-sameX'); 174 | assertSet($touching, 0, {suffix: 'for touching'}); // Make sure .touching() is unaffected 175 | 176 | // Old tests for checkHoriz - remove when the option has been removed 177 | var $nearest2 = $.nearest( this.midBox, '.xy-group', {checkHoriz: false}); 178 | var $furthest2 = $.furthest(this.midBox, '.xy-group', {checkHoriz: false}); 179 | var $touching2 = $.touching(this.midBox, '.xy-group', {checkHoriz: false}); 180 | 181 | assertSet($nearest2, 1, {suffix: 'for nearest 2'}, 'xy-mid-sameX'); 182 | assertSet($furthest2, 1, {suffix: 'for furthest 2'}, 'xy-mid-sameX'); 183 | assertSet($touching2, 0, {suffix: 'for touching 2'}); // Make sure .touching() is unaffected 184 | }); 185 | 186 | test('sameY / checkVert', 10, function () { 187 | var $nearest = $.nearest( this.midBox, '.xy-group', {sameY: true}); 188 | var $furthest = $.furthest(this.midBox, '.xy-group', {sameY: true}); 189 | var $touching = $.touching(this.midBox, '.xy-group', {sameY: true}); 190 | 191 | assertSet($nearest, 1, {suffix: 'for nearest'}, 'xy-mid-sameY'); 192 | assertSet($furthest, 1, {suffix: 'for furthest'}, 'xy-mid-sameY'); 193 | assertSet($touching, 0, {suffix: 'for touching'}); // Make sure .touching() is unaffected 194 | 195 | // Old tests for checkVert - remove when the option has been removed 196 | var $nearest2 = $.nearest( this.midBox, '.xy-group', {checkVert: false}); 197 | var $furthest2 = $.furthest(this.midBox, '.xy-group', {checkVert: false}); 198 | var $touching2 = $.touching(this.midBox, '.xy-group', {checkVert: false}); 199 | 200 | assertSet($nearest2, 1, {suffix: 'for nearest 2'}, 'xy-mid-sameY'); 201 | assertSet($furthest2, 1, {suffix: 'for furthest 2'}, 'xy-mid-sameY'); 202 | assertSet($touching2, 0, {suffix: 'for touching 2'}); // Make sure .touching() is unaffected 203 | }); 204 | 205 | test('sameX / checkHoriz + sameY / checkVert (no match)', 6, function () { 206 | var $nearest = $.nearest( this.midBox, '.xy-group', {sameX: true, sameY: true}); 207 | var $furthest = $.furthest(this.midBox, '.xy-group', {sameX: true, sameY: true}); 208 | var $touching = $.touching(this.midBox, '.xy-group', {sameX: true, sameY: true}); 209 | 210 | // All methods should act like .touching() 211 | assertSet($nearest, 0, {suffix: 'for nearest'}); 212 | assertSet($furthest, 0, {suffix: 'for furthest'}); 213 | assertSet($touching, 0, {suffix: 'for touching'}); 214 | 215 | // Old tests for checkHoriz + checkVert - remove when the option has been removed 216 | var $nearest2 = $.nearest( this.midBox, '.xy-group', {checkHoriz: false, checkVert: false}); 217 | var $furthest2 = $.furthest(this.midBox, '.xy-group', {checkHoriz: false, checkVert: false}); 218 | var $touching2 = $.touching(this.midBox, '.xy-group', {checkHoriz: false, checkVert: false}); 219 | 220 | // All methods should act like .touching() 221 | assertSet($nearest2, 0, {suffix: 'for nearest 2'}); 222 | assertSet($furthest2, 0, {suffix: 'for furthest 2'}); 223 | assertSet($touching2, 0, {suffix: 'for touching 2'}); 224 | }); 225 | 226 | test('sameX / checkHoriz + sameY / checkVert (match)', 12, function () { 227 | var point = getPoint(290, 790); 228 | var $nearest = $.nearest( point, '.xy-group', {sameX: true, sameY: true}); 229 | var $furthest = $.furthest(point, '.xy-group', {sameX: true, sameY: true}); 230 | var $touching = $.touching(point, '.xy-group', {sameX: true, sameY: true}); 231 | 232 | // All methods should act like .touching() 233 | assertSet($nearest, 1, {suffix: 'for nearest'}, 'xy-nearest-diag'); 234 | assertSet($furthest, 1, {suffix: 'for furthest'}, 'xy-nearest-diag'); 235 | assertSet($touching, 1, {suffix: 'for touching'}, 'xy-nearest-diag'); 236 | 237 | // Old tests for checkHoriz + checkVert - remove when the option has been removed 238 | var $nearest2 = $.nearest( point, '.xy-group', {checkHoriz: false, checkVert: false}); 239 | var $furthest2 = $.furthest(point, '.xy-group', {checkHoriz: false, checkVert: false}); 240 | var $touching2 = $.touching(point, '.xy-group', {checkHoriz: false, checkVert: false}); 241 | 242 | // All methods should act like .touching() 243 | assertSet($nearest2, 1, {suffix: 'for nearest 2'}, 'xy-nearest-diag'); 244 | assertSet($furthest2, 1, {suffix: 'for furthest 2'}, 'xy-nearest-diag'); 245 | assertSet($touching2, 1, {suffix: 'for touching 2'}, 'xy-nearest-diag'); 246 | }); 247 | 248 | test('onlyX', 5, function () { 249 | var $nearest = $.nearest( this.midBox, '.xy-group.not-same', {onlyX: true}); 250 | var $furthest = $.furthest(this.midBox, '.xy-group.not-same', {onlyX: true}); 251 | var $touching = $.touching(this.midBox, '.xy-group.not-same', {onlyX: true}); 252 | 253 | assertSet($nearest, 1, {suffix: 'for nearest'}, 'xy-closeX'); 254 | assertSet($furthest, 1, {suffix: 'for furthest'}, 'xy-right'); 255 | assertSet($touching, 0, {suffix: 'for touching'}); // Make sure .touching() is unaffected 256 | }); 257 | 258 | test('onlyY', 5, function () { 259 | var $nearest = $.nearest( this.midBox, '.xy-group.not-same', {onlyY: true}); 260 | var $furthest = $.furthest(this.midBox, '.xy-group.not-same', {onlyY: true}); 261 | var $touching = $.touching(this.midBox, '.xy-group.not-same', {onlyY: true}); 262 | 263 | assertSet($nearest, 1, {suffix: 'for nearest'}, 'xy-closeY'); 264 | assertSet($furthest, 1, {suffix: 'for furthest'}, 'xy-bottom'); 265 | assertSet($touching, 0, {suffix: 'for touching'}); // Make sure .touching() is unaffected 266 | }); 267 | 268 | test('sameX overrides onlyX', 1, function () { 269 | var $set = $.nearest(this.midBox, '.xy-group.not-same', {onlyX: true, sameX: true}); 270 | assertSet($set, 0); 271 | }); 272 | 273 | test('sameY overrides onlyY', 1, function () { 274 | var $set = $.nearest(this.midBox, '.xy-group.not-same', {onlyY: true, sameY: true}); 275 | assertSet($set, 0); 276 | }); 277 | 278 | test('tolerance: 0', 4, function () { 279 | var $nearest = $.nearest( this.tolPoint, '.tolerance', {tolerance: 0}); 280 | var $furthest = $.furthest(this.tolPoint, '.tolerance', {tolerance: 0}); 281 | 282 | assertSet($nearest, 1, {suffix: 'for nearest'}, 'tol-14'); 283 | assertSet($furthest, 1, {suffix: 'for furthest'}, 'tol-16'); 284 | }); 285 | 286 | test('tolerance: 0.5', 5, function () { 287 | var $nearest = $.nearest( this.tolPoint, '.tolerance', {tolerance: 0.5}); 288 | var $furthest = $.furthest(this.tolPoint, '.tolerance', {tolerance: 0.5}); 289 | 290 | assertSet($nearest, 2, {suffix: 'for nearest'}, 'tol-14', 'tol-14-and-a-bit'); 291 | assertSet($furthest, 1, {suffix: 'for furthest'}, 'tol-16'); 292 | }); 293 | 294 | test('tolerance: 1 (default)', 7, function () { 295 | var $nearest = $.nearest( this.tolPoint, '.tolerance'); 296 | var $furthest = $.furthest(this.tolPoint, '.tolerance'); 297 | 298 | assertSet($nearest, 3, {suffix: 'for nearest'}, 'tol-14', 'tol-14-and-a-bit', 'tol-15'); 299 | assertSet($furthest, 2, {suffix: 'for furthest'}, 'tol-15', 'tol-16'); 300 | }); 301 | 302 | test('tolerance: large', 7, function () { 303 | var $nearest = $.nearest( this.tolPoint, '.corner', {tolerance: 380}); 304 | var $furthest = $.furthest(this.tolPoint, '.corner', {tolerance: 380}); 305 | 306 | assertSet($nearest, 2, {suffix: 'for nearest'}, 'top-left', 'top-right'); 307 | assertSet($furthest, 3, {suffix: 'for furthest'}, 'top-left', 'bottom-left', 'bottom-right'); 308 | }); 309 | 310 | test('tolerance: out of bounds', 4, function () { 311 | var $set1 = $.nearest(this.tolPoint, '.tolerance', {tolerance: -1}); 312 | var $set2 = $.nearest(this.tolPoint, '.tolerance', {tolerance: 'not a number'}); 313 | // Negative/invalid numbers reset to 0 314 | assertSet($set1, 1, 'tol-14'); 315 | assertSet($set2, 1, 'tol-14'); 316 | }); 317 | 318 | test('container: percentages', 4, function () { 319 | var point = { 320 | x: '25%', 321 | y: '20%', 322 | w: '20%', 323 | h: '30%' 324 | }; 325 | var $nearest = $.nearest( point, 'div', {container: '#cont-option-container'}); 326 | var $furthest = $.furthest(point, 'div', {container: '#cont-option-container'}); 327 | 328 | assertSet($nearest, 1, 'cont-centre'); 329 | assertSet($furthest, 1, 'cont-top-left'); 330 | }); 331 | 332 | test('container: children', 4, function () { 333 | // Sanity check 334 | var $out = $.nearest(this.contPoint, '.cont'); 335 | assertSet($out, 1, {suffix: 'without container'}, 'cont-outside'); 336 | 337 | // With container option 338 | var $in = $.nearest(this.contPoint, '.cont', {container: '#cont-option-container'}); 339 | assertSet($in, 1, {suffix: 'with container'}, 'cont-centre'); 340 | }); 341 | 342 | test('container: invalid', 1, function () { 343 | var $set = $.nearest(this.contPoint, '.cont', {container: '#does-not-exist'}); 344 | assertSet($set, 0); // Make sure it doesn't blow up 345 | }); 346 | 347 | test('directionConstraints: point', 5, function () { 348 | var point = getPoint(300, 300); 349 | var $nearestTopRight = $.nearest(point, '.corner', {directionConstraints: ['right', 'top']}); 350 | var $furthestBottom = $.furthest(point, '.corner', {directionConstraints: ['bottom'], tolerance: 1000}); 351 | 352 | assertSet($nearestTopRight, 1, {suffix: 'for ["right", "top"]'}, 'top-right'); 353 | assertSet($furthestBottom, 2, {suffix: 'for ["bottom"]'}, 'bottom-left', 'bottom-right'); 354 | }); 355 | 356 | test('directionConstraints: box', 6, function () { 357 | // This box intersects with the 'dead-centre' block 358 | var box = getBox(300, 400, 500, 100); 359 | var optsTL = {directionConstraints: ['top', 'left']}; 360 | var optsB = {directionConstraints: ['bottom']}; 361 | var $nearestCorner = $.nearest(box, '.corner', optsTL); 362 | var $nearestBlockTL = $.nearest(box, '.corner, #dead-centre', optsTL); 363 | var $nearestBlockB = $.nearest(box, '.corner, #dead-centre', optsB); 364 | 365 | assertSet($nearestCorner, 1, {suffix: 'for non-intersecting elements'}, 'top-left'); 366 | // An element that is touching is only counted if it intersects in the right direction 367 | assertSet($nearestBlockTL, 1, {suffix: 'for an intersecting element in the wrong direction'}, 'top-left'); 368 | assertSet($nearestBlockB, 1, {suffix: 'for an intersecting element in the right direction'}, 'dead-centre'); 369 | }); 370 | 371 | test('directionConstraints: string parameter converts to array', 3, function () { 372 | var $nearestString = $.nearest(this.midPoint, '.corner', {directionConstraints: 'left', tolerance: 1000}); 373 | assertSet($nearestString, 2, {suffix: 'for "left"'}, 'top-left', 'bottom-left'); 374 | }); 375 | 376 | test('directionConstraints: invalid parameter turns off constraints', 5, function () { 377 | var $nearestString = $.nearest(this.midPoint, '.corner', {directionConstraints: {}, tolerance: 1000}); 378 | assertSet($nearestString, 4, {suffix: 'for {} value'}, 'top-left', 'top-right', 'bottom-left', 'bottom-right'); 379 | }); 380 | 381 | test('sort', 10, function () { 382 | var point = getPoint(400, 200); 383 | var $sortedNearest = $.nearest(point, '.corner', {sort: 'nearest', tolerance: 1000}); 384 | var $sortedFurthest = $.nearest(point, '.corner', {sort: 'furthest', tolerance: 1000}); 385 | 386 | assertSet($sortedNearest, 4, {suffix: 'for nearest'}, 'top-left', 'top-right', 'bottom-left', 'bottom-right'); 387 | assertSet($sortedFurthest, 4, {suffix: 'for furthest'}, 'bottom-right', 'bottom-left', 'top-right', 'top-left'); 388 | }); 389 | 390 | 391 | 392 | ////////// MODULE $.fn.method ////////// 393 | 394 | module('$.fn.method'); 395 | 396 | test('empty set', 2, function () { 397 | var $set = $('#does-not-exist'); 398 | var $result = $set.nearest(); 399 | assertSet($result, 0); 400 | notStrictEqual($result, $set, 'returned set is a copy'); 401 | }); 402 | 403 | test('correct stack order', 1, function () { 404 | var $set = $('.corner'); 405 | strictEqual($set.nearest().end(), $set, '.method().end() returns the original set'); 406 | }); 407 | 408 | test('methods use dimensions of the first element in a set', 2, function () { 409 | var $set = $('.corner'); 410 | var $nearest = $set.nearest('.top-mid', {tolerance: 0}); 411 | assertSet($nearest, 1, 'tmtl'); 412 | }); 413 | 414 | test('basic find usage', 6, function () { 415 | var $elem = $('#basic-ref'); 416 | var $nearest = $elem.nearest( '.basic-group'); 417 | var $furthest = $elem.furthest('.basic-group'); 418 | var $touching = $elem.touching('.basic-group'); 419 | 420 | assertSet($nearest, 1, {suffix: 'for nearest'}, 'basic-touching'); 421 | assertSet($furthest, 1, {suffix: 'for furthest'}, 'basic-furthest'); 422 | assertSet($touching, 1, {suffix: 'for touching'}, 'basic-touching'); 423 | }); 424 | 425 | /** 426 | * This option is not tested with all the other options in the 427 | * `$.method: options` module as it only applies to element operations. 428 | */ 429 | test('option: includeSelf', 5, function () { 430 | var $elem = $('#basic-ref'); 431 | var $withoutSelf = $elem.nearest('.basic-group'); 432 | var $withSelf = $elem.nearest('.basic-group', {includeSelf: true}); 433 | 434 | assertSet($withoutSelf, 1, {suffix: 'when false'}, 'basic-touching'); 435 | assertSet($withSelf, 2, {suffix: 'when true'}, 'basic-ref', 'basic-touching'); 436 | }); 437 | 438 | test('basic filter usage', 5, function () { 439 | var $set = $('.corner'); 440 | var point = getPoint(30, 30); 441 | var $nearest = $set.nearest( point); 442 | var $furthest = $set.furthest(point); 443 | var $touching = $set.touching(point); 444 | 445 | assertSet($nearest, 1, {suffix: 'for nearest'}, 'top-left'); 446 | assertSet($furthest, 1, {suffix: 'for furthest'}, 'bottom-right'); 447 | assertSet($touching, 0, {suffix: 'for touching'}); 448 | }); 449 | 450 | test('filter with directionConstraints', 4, function() { 451 | var $set = $('.corner'); 452 | var point = getPoint(30, 30); 453 | var opts = {directionConstraints: ['top', 'right']}; 454 | var $nearest = $set.nearest( point, opts); 455 | var $furthest = $set.furthest(point, opts); 456 | 457 | assertSet($nearest, 1, {suffix: 'for nearest'}, 'top-right'); 458 | assertSet($furthest, 1, {suffix: 'for furthest'}, 'top-right'); 459 | }); 460 | 461 | }(jQuery)); 462 | -------------------------------------------------------------------------------- /test/qunit/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.11.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://qunitjs.com 5 | * 6 | * Copyright 2012 jQuery Foundation and other contributors 7 | * Released under the MIT license. 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-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { 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 | overflow: hidden; 71 | } 72 | 73 | #qunit-userAgent { 74 | padding: 0.5em 0 0.5em 2.5em; 75 | background-color: #2b81af; 76 | color: #fff; 77 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 78 | } 79 | 80 | #qunit-modulefilter-container { 81 | float: right; 82 | } 83 | 84 | /** Tests: Pass/Fail */ 85 | 86 | #qunit-tests { 87 | list-style-position: inside; 88 | } 89 | 90 | #qunit-tests li { 91 | padding: 0.4em 0.5em 0.4em 2.5em; 92 | border-bottom: 1px solid #fff; 93 | list-style-position: inside; 94 | } 95 | 96 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 97 | display: none; 98 | } 99 | 100 | #qunit-tests li strong { 101 | cursor: pointer; 102 | } 103 | 104 | #qunit-tests li a { 105 | padding: 0.5em; 106 | color: #c2ccd1; 107 | text-decoration: none; 108 | } 109 | #qunit-tests li a:hover, 110 | #qunit-tests li a:focus { 111 | color: #000; 112 | } 113 | 114 | #qunit-tests li .runtime { 115 | float: right; 116 | font-size: smaller; 117 | } 118 | 119 | .qunit-assert-list { 120 | margin-top: 0.5em; 121 | padding: 0.5em; 122 | 123 | background-color: #fff; 124 | 125 | border-radius: 5px; 126 | -moz-border-radius: 5px; 127 | -webkit-border-radius: 5px; 128 | } 129 | 130 | .qunit-collapsed { 131 | display: none; 132 | } 133 | 134 | #qunit-tests table { 135 | border-collapse: collapse; 136 | margin-top: .2em; 137 | } 138 | 139 | #qunit-tests th { 140 | text-align: right; 141 | vertical-align: top; 142 | padding: 0 .5em 0 0; 143 | } 144 | 145 | #qunit-tests td { 146 | vertical-align: top; 147 | } 148 | 149 | #qunit-tests pre { 150 | margin: 0; 151 | white-space: pre-wrap; 152 | word-wrap: break-word; 153 | } 154 | 155 | #qunit-tests del { 156 | background-color: #e0f2be; 157 | color: #374e0c; 158 | text-decoration: none; 159 | } 160 | 161 | #qunit-tests ins { 162 | background-color: #ffcaca; 163 | color: #500; 164 | text-decoration: none; 165 | } 166 | 167 | /*** Test Counts */ 168 | 169 | #qunit-tests b.counts { color: black; } 170 | #qunit-tests b.passed { color: #5E740B; } 171 | #qunit-tests b.failed { color: #710909; } 172 | 173 | #qunit-tests li li { 174 | padding: 5px; 175 | background-color: #fff; 176 | border-bottom: none; 177 | list-style-position: inside; 178 | } 179 | 180 | /*** Passing Styles */ 181 | 182 | #qunit-tests li li.pass { 183 | color: #3c510c; 184 | background-color: #fff; 185 | border-left: 10px solid #C6E746; 186 | } 187 | 188 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 189 | #qunit-tests .pass .test-name { color: #366097; } 190 | 191 | #qunit-tests .pass .test-actual, 192 | #qunit-tests .pass .test-expected { color: #999999; } 193 | 194 | #qunit-banner.qunit-pass { background-color: #C6E746; } 195 | 196 | /*** Failing Styles */ 197 | 198 | #qunit-tests li li.fail { 199 | color: #710909; 200 | background-color: #fff; 201 | border-left: 10px solid #EE5757; 202 | white-space: pre; 203 | } 204 | 205 | #qunit-tests > li:last-child { 206 | border-radius: 0 0 5px 5px; 207 | -moz-border-radius: 0 0 5px 5px; 208 | -webkit-border-bottom-right-radius: 5px; 209 | -webkit-border-bottom-left-radius: 5px; 210 | } 211 | 212 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 213 | #qunit-tests .fail .test-name, 214 | #qunit-tests .fail .module-name { color: #000000; } 215 | 216 | #qunit-tests .fail .test-actual { color: #EE5757; } 217 | #qunit-tests .fail .test-expected { color: green; } 218 | 219 | #qunit-banner.qunit-fail { background-color: #EE5757; } 220 | 221 | 222 | /** Result */ 223 | 224 | #qunit-testresult { 225 | padding: 0.5em 0.5em 0.5em 2.5em; 226 | 227 | color: #2b81af; 228 | background-color: #D2E0E6; 229 | 230 | border-bottom: 1px solid white; 231 | } 232 | #qunit-testresult .module-name { 233 | font-weight: bold; 234 | } 235 | 236 | /** Fixture */ 237 | 238 | #qunit-fixture { 239 | position: absolute; 240 | top: -10000px; 241 | left: -10000px; 242 | width: 1000px; 243 | height: 1000px; 244 | } 245 | -------------------------------------------------------------------------------- /test/qunit/qunit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.11.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://qunitjs.com 5 | * 6 | * Copyright 2012 jQuery Foundation and other contributors 7 | * Released under the MIT license. 8 | * http://jquery.org/license 9 | */ 10 | 11 | (function( window ) { 12 | 13 | var QUnit, 14 | assert, 15 | config, 16 | onErrorFnPrev, 17 | testId = 0, 18 | fileName = (sourceFromStacktrace( 0 ) || "" ).replace(/(:\d+)+\)?/, "").replace(/.+\//, ""), 19 | toString = Object.prototype.toString, 20 | hasOwn = Object.prototype.hasOwnProperty, 21 | // Keep a local reference to Date (GH-283) 22 | Date = window.Date, 23 | defined = { 24 | setTimeout: typeof window.setTimeout !== "undefined", 25 | sessionStorage: (function() { 26 | var x = "qunit-test-string"; 27 | try { 28 | sessionStorage.setItem( x, x ); 29 | sessionStorage.removeItem( x ); 30 | return true; 31 | } catch( e ) { 32 | return false; 33 | } 34 | }()) 35 | }, 36 | /** 37 | * Provides a normalized error string, correcting an issue 38 | * with IE 7 (and prior) where Error.prototype.toString is 39 | * not properly implemented 40 | * 41 | * Based on http://es5.github.com/#x15.11.4.4 42 | * 43 | * @param {String|Error} error 44 | * @return {String} error message 45 | */ 46 | errorString = function( error ) { 47 | var name, message, 48 | errorString = error.toString(); 49 | if ( errorString.substring( 0, 7 ) === "[object" ) { 50 | name = error.name ? error.name.toString() : "Error"; 51 | message = error.message ? error.message.toString() : ""; 52 | if ( name && message ) { 53 | return name + ": " + message; 54 | } else if ( name ) { 55 | return name; 56 | } else if ( message ) { 57 | return message; 58 | } else { 59 | return "Error"; 60 | } 61 | } else { 62 | return errorString; 63 | } 64 | }, 65 | /** 66 | * Makes a clone of an object using only Array or Object as base, 67 | * and copies over the own enumerable properties. 68 | * 69 | * @param {Object} obj 70 | * @return {Object} New object with only the own properties (recursively). 71 | */ 72 | objectValues = function( obj ) { 73 | // Grunt 0.3.x uses an older version of jshint that still has jshint/jshint#392. 74 | /*jshint newcap: false */ 75 | var key, val, 76 | vals = QUnit.is( "array", obj ) ? [] : {}; 77 | for ( key in obj ) { 78 | if ( hasOwn.call( obj, key ) ) { 79 | val = obj[key]; 80 | vals[key] = val === Object(val) ? objectValues(val) : val; 81 | } 82 | } 83 | return vals; 84 | }; 85 | 86 | function Test( settings ) { 87 | extend( this, settings ); 88 | this.assertions = []; 89 | this.testNumber = ++Test.count; 90 | } 91 | 92 | Test.count = 0; 93 | 94 | Test.prototype = { 95 | init: function() { 96 | var a, b, li, 97 | tests = id( "qunit-tests" ); 98 | 99 | if ( tests ) { 100 | b = document.createElement( "strong" ); 101 | b.innerHTML = this.nameHtml; 102 | 103 | // `a` initialized at top of scope 104 | a = document.createElement( "a" ); 105 | a.innerHTML = "Rerun"; 106 | a.href = QUnit.url({ testNumber: this.testNumber }); 107 | 108 | li = document.createElement( "li" ); 109 | li.appendChild( b ); 110 | li.appendChild( a ); 111 | li.className = "running"; 112 | li.id = this.id = "qunit-test-output" + testId++; 113 | 114 | tests.appendChild( li ); 115 | } 116 | }, 117 | setup: function() { 118 | if ( this.module !== config.previousModule ) { 119 | if ( config.previousModule ) { 120 | runLoggingCallbacks( "moduleDone", QUnit, { 121 | name: config.previousModule, 122 | failed: config.moduleStats.bad, 123 | passed: config.moduleStats.all - config.moduleStats.bad, 124 | total: config.moduleStats.all 125 | }); 126 | } 127 | config.previousModule = this.module; 128 | config.moduleStats = { all: 0, bad: 0 }; 129 | runLoggingCallbacks( "moduleStart", QUnit, { 130 | name: this.module 131 | }); 132 | } else if ( config.autorun ) { 133 | runLoggingCallbacks( "moduleStart", QUnit, { 134 | name: this.module 135 | }); 136 | } 137 | 138 | config.current = this; 139 | 140 | this.testEnvironment = extend({ 141 | setup: function() {}, 142 | teardown: function() {} 143 | }, this.moduleTestEnvironment ); 144 | 145 | this.started = +new Date(); 146 | runLoggingCallbacks( "testStart", QUnit, { 147 | name: this.testName, 148 | module: this.module 149 | }); 150 | 151 | // allow utility functions to access the current test environment 152 | // TODO why?? 153 | QUnit.current_testEnvironment = this.testEnvironment; 154 | 155 | if ( !config.pollution ) { 156 | saveGlobal(); 157 | } 158 | if ( config.notrycatch ) { 159 | this.testEnvironment.setup.call( this.testEnvironment ); 160 | return; 161 | } 162 | try { 163 | this.testEnvironment.setup.call( this.testEnvironment ); 164 | } catch( e ) { 165 | QUnit.pushFailure( "Setup failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); 166 | } 167 | }, 168 | run: function() { 169 | config.current = this; 170 | 171 | var running = id( "qunit-testresult" ); 172 | 173 | if ( running ) { 174 | running.innerHTML = "Running:
" + this.nameHtml; 175 | } 176 | 177 | if ( this.async ) { 178 | QUnit.stop(); 179 | } 180 | 181 | this.callbackStarted = +new Date(); 182 | 183 | if ( config.notrycatch ) { 184 | this.callback.call( this.testEnvironment, QUnit.assert ); 185 | this.callbackRuntime = +new Date() - this.callbackStarted; 186 | return; 187 | } 188 | 189 | try { 190 | this.callback.call( this.testEnvironment, QUnit.assert ); 191 | this.callbackRuntime = +new Date() - this.callbackStarted; 192 | } catch( e ) { 193 | this.callbackRuntime = +new Date() - this.callbackStarted; 194 | 195 | QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); 196 | // else next test will carry the responsibility 197 | saveGlobal(); 198 | 199 | // Restart the tests if they're blocking 200 | if ( config.blocking ) { 201 | QUnit.start(); 202 | } 203 | } 204 | }, 205 | teardown: function() { 206 | config.current = this; 207 | if ( config.notrycatch ) { 208 | if ( typeof this.callbackRuntime === "undefined" ) { 209 | this.callbackRuntime = +new Date() - this.callbackStarted; 210 | } 211 | this.testEnvironment.teardown.call( this.testEnvironment ); 212 | return; 213 | } else { 214 | try { 215 | this.testEnvironment.teardown.call( this.testEnvironment ); 216 | } catch( e ) { 217 | QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); 218 | } 219 | } 220 | checkPollution(); 221 | }, 222 | finish: function() { 223 | config.current = this; 224 | if ( config.requireExpects && this.expected === null ) { 225 | QUnit.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack ); 226 | } else if ( this.expected !== null && this.expected !== this.assertions.length ) { 227 | QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack ); 228 | } else if ( this.expected === null && !this.assertions.length ) { 229 | QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack ); 230 | } 231 | 232 | var i, assertion, a, b, time, li, ol, 233 | test = this, 234 | good = 0, 235 | bad = 0, 236 | tests = id( "qunit-tests" ); 237 | 238 | this.runtime = +new Date() - this.started; 239 | config.stats.all += this.assertions.length; 240 | config.moduleStats.all += this.assertions.length; 241 | 242 | if ( tests ) { 243 | ol = document.createElement( "ol" ); 244 | ol.className = "qunit-assert-list"; 245 | 246 | for ( i = 0; i < this.assertions.length; i++ ) { 247 | assertion = this.assertions[i]; 248 | 249 | li = document.createElement( "li" ); 250 | li.className = assertion.result ? "pass" : "fail"; 251 | li.innerHTML = assertion.message || ( assertion.result ? "okay" : "failed" ); 252 | ol.appendChild( li ); 253 | 254 | if ( assertion.result ) { 255 | good++; 256 | } else { 257 | bad++; 258 | config.stats.bad++; 259 | config.moduleStats.bad++; 260 | } 261 | } 262 | 263 | // store result when possible 264 | if ( QUnit.config.reorder && defined.sessionStorage ) { 265 | if ( bad ) { 266 | sessionStorage.setItem( "qunit-test-" + this.module + "-" + this.testName, bad ); 267 | } else { 268 | sessionStorage.removeItem( "qunit-test-" + this.module + "-" + this.testName ); 269 | } 270 | } 271 | 272 | if ( bad === 0 ) { 273 | addClass( ol, "qunit-collapsed" ); 274 | } 275 | 276 | // `b` initialized at top of scope 277 | b = document.createElement( "strong" ); 278 | b.innerHTML = this.nameHtml + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; 279 | 280 | addEvent(b, "click", function() { 281 | var next = b.parentNode.lastChild, 282 | collapsed = hasClass( next, "qunit-collapsed" ); 283 | ( collapsed ? removeClass : addClass )( next, "qunit-collapsed" ); 284 | }); 285 | 286 | addEvent(b, "dblclick", function( e ) { 287 | var target = e && e.target ? e.target : window.event.srcElement; 288 | if ( target.nodeName.toLowerCase() === "span" || target.nodeName.toLowerCase() === "b" ) { 289 | target = target.parentNode; 290 | } 291 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 292 | window.location = QUnit.url({ testNumber: test.testNumber }); 293 | } 294 | }); 295 | 296 | // `time` initialized at top of scope 297 | time = document.createElement( "span" ); 298 | time.className = "runtime"; 299 | time.innerHTML = this.runtime + " ms"; 300 | 301 | // `li` initialized at top of scope 302 | li = id( this.id ); 303 | li.className = bad ? "fail" : "pass"; 304 | li.removeChild( li.firstChild ); 305 | a = li.firstChild; 306 | li.appendChild( b ); 307 | li.appendChild( a ); 308 | li.appendChild( time ); 309 | li.appendChild( ol ); 310 | 311 | } else { 312 | for ( i = 0; i < this.assertions.length; i++ ) { 313 | if ( !this.assertions[i].result ) { 314 | bad++; 315 | config.stats.bad++; 316 | config.moduleStats.bad++; 317 | } 318 | } 319 | } 320 | 321 | runLoggingCallbacks( "testDone", QUnit, { 322 | name: this.testName, 323 | module: this.module, 324 | failed: bad, 325 | passed: this.assertions.length - bad, 326 | total: this.assertions.length, 327 | duration: this.runtime 328 | }); 329 | 330 | QUnit.reset(); 331 | 332 | config.current = undefined; 333 | }, 334 | 335 | queue: function() { 336 | var bad, 337 | test = this; 338 | 339 | synchronize(function() { 340 | test.init(); 341 | }); 342 | function run() { 343 | // each of these can by async 344 | synchronize(function() { 345 | test.setup(); 346 | }); 347 | synchronize(function() { 348 | test.run(); 349 | }); 350 | synchronize(function() { 351 | test.teardown(); 352 | }); 353 | synchronize(function() { 354 | test.finish(); 355 | }); 356 | } 357 | 358 | // `bad` initialized at top of scope 359 | // defer when previous test run passed, if storage is available 360 | bad = QUnit.config.reorder && defined.sessionStorage && 361 | +sessionStorage.getItem( "qunit-test-" + this.module + "-" + this.testName ); 362 | 363 | if ( bad ) { 364 | run(); 365 | } else { 366 | synchronize( run, true ); 367 | } 368 | } 369 | }; 370 | 371 | // Root QUnit object. 372 | // `QUnit` initialized at top of scope 373 | QUnit = { 374 | 375 | // call on start of module test to prepend name to all tests 376 | module: function( name, testEnvironment ) { 377 | config.currentModule = name; 378 | config.currentModuleTestEnvironment = testEnvironment; 379 | config.modules[name] = true; 380 | }, 381 | 382 | asyncTest: function( testName, expected, callback ) { 383 | if ( arguments.length === 2 ) { 384 | callback = expected; 385 | expected = null; 386 | } 387 | 388 | QUnit.test( testName, expected, callback, true ); 389 | }, 390 | 391 | test: function( testName, expected, callback, async ) { 392 | var test, 393 | nameHtml = "" + escapeText( testName ) + ""; 394 | 395 | if ( arguments.length === 2 ) { 396 | callback = expected; 397 | expected = null; 398 | } 399 | 400 | if ( config.currentModule ) { 401 | nameHtml = "" + escapeText( config.currentModule ) + ": " + nameHtml; 402 | } 403 | 404 | test = new Test({ 405 | nameHtml: nameHtml, 406 | testName: testName, 407 | expected: expected, 408 | async: async, 409 | callback: callback, 410 | module: config.currentModule, 411 | moduleTestEnvironment: config.currentModuleTestEnvironment, 412 | stack: sourceFromStacktrace( 2 ) 413 | }); 414 | 415 | if ( !validTest( test ) ) { 416 | return; 417 | } 418 | 419 | test.queue(); 420 | }, 421 | 422 | // Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. 423 | expect: function( asserts ) { 424 | if (arguments.length === 1) { 425 | config.current.expected = asserts; 426 | } else { 427 | return config.current.expected; 428 | } 429 | }, 430 | 431 | start: function( count ) { 432 | // QUnit hasn't been initialized yet. 433 | // Note: RequireJS (et al) may delay onLoad 434 | if ( config.semaphore === undefined ) { 435 | QUnit.begin(function() { 436 | // This is triggered at the top of QUnit.load, push start() to the event loop, to allow QUnit.load to finish first 437 | setTimeout(function() { 438 | QUnit.start( count ); 439 | }); 440 | }); 441 | return; 442 | } 443 | 444 | config.semaphore -= count || 1; 445 | // don't start until equal number of stop-calls 446 | if ( config.semaphore > 0 ) { 447 | return; 448 | } 449 | // ignore if start is called more often then stop 450 | if ( config.semaphore < 0 ) { 451 | config.semaphore = 0; 452 | QUnit.pushFailure( "Called start() while already started (QUnit.config.semaphore was 0 already)", null, sourceFromStacktrace(2) ); 453 | return; 454 | } 455 | // A slight delay, to avoid any current callbacks 456 | if ( defined.setTimeout ) { 457 | window.setTimeout(function() { 458 | if ( config.semaphore > 0 ) { 459 | return; 460 | } 461 | if ( config.timeout ) { 462 | clearTimeout( config.timeout ); 463 | } 464 | 465 | config.blocking = false; 466 | process( true ); 467 | }, 13); 468 | } else { 469 | config.blocking = false; 470 | process( true ); 471 | } 472 | }, 473 | 474 | stop: function( count ) { 475 | config.semaphore += count || 1; 476 | config.blocking = true; 477 | 478 | if ( config.testTimeout && defined.setTimeout ) { 479 | clearTimeout( config.timeout ); 480 | config.timeout = window.setTimeout(function() { 481 | QUnit.ok( false, "Test timed out" ); 482 | config.semaphore = 1; 483 | QUnit.start(); 484 | }, config.testTimeout ); 485 | } 486 | } 487 | }; 488 | 489 | // `assert` initialized at top of scope 490 | // Asssert helpers 491 | // All of these must either call QUnit.push() or manually do: 492 | // - runLoggingCallbacks( "log", .. ); 493 | // - config.current.assertions.push({ .. }); 494 | // We attach it to the QUnit object *after* we expose the public API, 495 | // otherwise `assert` will become a global variable in browsers (#341). 496 | assert = { 497 | /** 498 | * Asserts rough true-ish result. 499 | * @name ok 500 | * @function 501 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 502 | */ 503 | ok: function( result, msg ) { 504 | if ( !config.current ) { 505 | throw new Error( "ok() assertion outside test context, was " + sourceFromStacktrace(2) ); 506 | } 507 | result = !!result; 508 | 509 | var source, 510 | details = { 511 | module: config.current.module, 512 | name: config.current.testName, 513 | result: result, 514 | message: msg 515 | }; 516 | 517 | msg = escapeText( msg || (result ? "okay" : "failed" ) ); 518 | msg = "" + msg + ""; 519 | 520 | if ( !result ) { 521 | source = sourceFromStacktrace( 2 ); 522 | if ( source ) { 523 | details.source = source; 524 | msg += "
Source:
" + escapeText( source ) + "
"; 525 | } 526 | } 527 | runLoggingCallbacks( "log", QUnit, details ); 528 | config.current.assertions.push({ 529 | result: result, 530 | message: msg 531 | }); 532 | }, 533 | 534 | /** 535 | * Assert that the first two arguments are equal, with an optional message. 536 | * Prints out both actual and expected values. 537 | * @name equal 538 | * @function 539 | * @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" ); 540 | */ 541 | equal: function( actual, expected, message ) { 542 | /*jshint eqeqeq:false */ 543 | QUnit.push( expected == actual, actual, expected, message ); 544 | }, 545 | 546 | /** 547 | * @name notEqual 548 | * @function 549 | */ 550 | notEqual: function( actual, expected, message ) { 551 | /*jshint eqeqeq:false */ 552 | QUnit.push( expected != actual, actual, expected, message ); 553 | }, 554 | 555 | /** 556 | * @name propEqual 557 | * @function 558 | */ 559 | propEqual: function( actual, expected, message ) { 560 | actual = objectValues(actual); 561 | expected = objectValues(expected); 562 | QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); 563 | }, 564 | 565 | /** 566 | * @name notPropEqual 567 | * @function 568 | */ 569 | notPropEqual: function( actual, expected, message ) { 570 | actual = objectValues(actual); 571 | expected = objectValues(expected); 572 | QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); 573 | }, 574 | 575 | /** 576 | * @name deepEqual 577 | * @function 578 | */ 579 | deepEqual: function( actual, expected, message ) { 580 | QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); 581 | }, 582 | 583 | /** 584 | * @name notDeepEqual 585 | * @function 586 | */ 587 | notDeepEqual: function( actual, expected, message ) { 588 | QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); 589 | }, 590 | 591 | /** 592 | * @name strictEqual 593 | * @function 594 | */ 595 | strictEqual: function( actual, expected, message ) { 596 | QUnit.push( expected === actual, actual, expected, message ); 597 | }, 598 | 599 | /** 600 | * @name notStrictEqual 601 | * @function 602 | */ 603 | notStrictEqual: function( actual, expected, message ) { 604 | QUnit.push( expected !== actual, actual, expected, message ); 605 | }, 606 | 607 | "throws": function( block, expected, message ) { 608 | var actual, 609 | expectedOutput = expected, 610 | ok = false; 611 | 612 | // 'expected' is optional 613 | if ( typeof expected === "string" ) { 614 | message = expected; 615 | expected = null; 616 | } 617 | 618 | config.current.ignoreGlobalErrors = true; 619 | try { 620 | block.call( config.current.testEnvironment ); 621 | } catch (e) { 622 | actual = e; 623 | } 624 | config.current.ignoreGlobalErrors = false; 625 | 626 | if ( actual ) { 627 | // we don't want to validate thrown error 628 | if ( !expected ) { 629 | ok = true; 630 | expectedOutput = null; 631 | // expected is a regexp 632 | } else if ( QUnit.objectType( expected ) === "regexp" ) { 633 | ok = expected.test( errorString( actual ) ); 634 | // expected is a constructor 635 | } else if ( actual instanceof expected ) { 636 | ok = true; 637 | // expected is a validation function which returns true is validation passed 638 | } else if ( expected.call( {}, actual ) === true ) { 639 | expectedOutput = null; 640 | ok = true; 641 | } 642 | 643 | QUnit.push( ok, actual, expectedOutput, message ); 644 | } else { 645 | QUnit.pushFailure( message, null, 'No exception was thrown.' ); 646 | } 647 | } 648 | }; 649 | 650 | /** 651 | * @deprecate since 1.8.0 652 | * Kept assertion helpers in root for backwards compatibility. 653 | */ 654 | extend( QUnit, assert ); 655 | 656 | /** 657 | * @deprecated since 1.9.0 658 | * Kept root "raises()" for backwards compatibility. 659 | * (Note that we don't introduce assert.raises). 660 | */ 661 | QUnit.raises = assert[ "throws" ]; 662 | 663 | /** 664 | * @deprecated since 1.0.0, replaced with error pushes since 1.3.0 665 | * Kept to avoid TypeErrors for undefined methods. 666 | */ 667 | QUnit.equals = function() { 668 | QUnit.push( false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead" ); 669 | }; 670 | QUnit.same = function() { 671 | QUnit.push( false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead" ); 672 | }; 673 | 674 | // We want access to the constructor's prototype 675 | (function() { 676 | function F() {} 677 | F.prototype = QUnit; 678 | QUnit = new F(); 679 | // Make F QUnit's constructor so that we can add to the prototype later 680 | QUnit.constructor = F; 681 | }()); 682 | 683 | /** 684 | * Config object: Maintain internal state 685 | * Later exposed as QUnit.config 686 | * `config` initialized at top of scope 687 | */ 688 | config = { 689 | // The queue of tests to run 690 | queue: [], 691 | 692 | // block until document ready 693 | blocking: true, 694 | 695 | // when enabled, show only failing tests 696 | // gets persisted through sessionStorage and can be changed in UI via checkbox 697 | hidepassed: false, 698 | 699 | // by default, run previously failed tests first 700 | // very useful in combination with "Hide passed tests" checked 701 | reorder: true, 702 | 703 | // by default, modify document.title when suite is done 704 | altertitle: true, 705 | 706 | // when enabled, all tests must call expect() 707 | requireExpects: false, 708 | 709 | // add checkboxes that are persisted in the query-string 710 | // when enabled, the id is set to `true` as a `QUnit.config` property 711 | urlConfig: [ 712 | { 713 | id: "noglobals", 714 | label: "Check for Globals", 715 | tooltip: "Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings." 716 | }, 717 | { 718 | id: "notrycatch", 719 | label: "No try-catch", 720 | tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings." 721 | } 722 | ], 723 | 724 | // Set of all modules. 725 | modules: {}, 726 | 727 | // logging callback queues 728 | begin: [], 729 | done: [], 730 | log: [], 731 | testStart: [], 732 | testDone: [], 733 | moduleStart: [], 734 | moduleDone: [] 735 | }; 736 | 737 | // Export global variables, unless an 'exports' object exists, 738 | // in that case we assume we're in CommonJS (dealt with on the bottom of the script) 739 | if ( typeof exports === "undefined" ) { 740 | extend( window, QUnit ); 741 | 742 | // Expose QUnit object 743 | window.QUnit = QUnit; 744 | } 745 | 746 | // Initialize more QUnit.config and QUnit.urlParams 747 | (function() { 748 | var i, 749 | location = window.location || { search: "", protocol: "file:" }, 750 | params = location.search.slice( 1 ).split( "&" ), 751 | length = params.length, 752 | urlParams = {}, 753 | current; 754 | 755 | if ( params[ 0 ] ) { 756 | for ( i = 0; i < length; i++ ) { 757 | current = params[ i ].split( "=" ); 758 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 759 | // allow just a key to turn on a flag, e.g., test.html?noglobals 760 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 761 | urlParams[ current[ 0 ] ] = current[ 1 ]; 762 | } 763 | } 764 | 765 | QUnit.urlParams = urlParams; 766 | 767 | // String search anywhere in moduleName+testName 768 | config.filter = urlParams.filter; 769 | 770 | // Exact match of the module name 771 | config.module = urlParams.module; 772 | 773 | config.testNumber = parseInt( urlParams.testNumber, 10 ) || null; 774 | 775 | // Figure out if we're running the tests from a server or not 776 | QUnit.isLocal = location.protocol === "file:"; 777 | }()); 778 | 779 | // Extend QUnit object, 780 | // these after set here because they should not be exposed as global functions 781 | extend( QUnit, { 782 | assert: assert, 783 | 784 | config: config, 785 | 786 | // Initialize the configuration options 787 | init: function() { 788 | extend( config, { 789 | stats: { all: 0, bad: 0 }, 790 | moduleStats: { all: 0, bad: 0 }, 791 | started: +new Date(), 792 | updateRate: 1000, 793 | blocking: false, 794 | autostart: true, 795 | autorun: false, 796 | filter: "", 797 | queue: [], 798 | semaphore: 1 799 | }); 800 | 801 | var tests, banner, result, 802 | qunit = id( "qunit" ); 803 | 804 | if ( qunit ) { 805 | qunit.innerHTML = 806 | "

" + escapeText( document.title ) + "

" + 807 | "

" + 808 | "
" + 809 | "

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