├── .gitignore ├── README.md ├── demo.html ├── experiments ├── clone-parent.html ├── delegate-drag.html ├── ghost-proxy.html └── non-delegate-drag.html ├── index.html ├── katavorio-GPLv2-LICENSE.txt ├── katavorio-MIT-LICENSE.txt ├── package.json ├── src ├── default-katavorio-helper.js └── katavorio.js └── test ├── index.html ├── katavorio-test.js ├── memory.html ├── qunit-1.11.0.css └── qunit-1.11.0.js /.gitignore: -------------------------------------------------------------------------------- 1 | katavorio.iml 2 | .idea 3 | 4 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | *NOTE* As of May 2022 this project has been archived. No development work has been done for 18 months or so as the drag handling code in jsPlumb has been moved to a module in the 5.x branch. 2 | 3 | ## Katavorio 4 | 5 | 6 | Katavorio is a lightweight drag/drop handler, supporting containment, multiple element drag, custom css classes, 7 | drop filters, drag filters, drag clones, drag handles, constraining movement to a grid, and zooming. 8 | 9 | Katavorio does not work "out of the box" - it was developed as part of jsPlumb 1.6.0, to support a 10 | "no dependency" version (all previous versions of jsPlumb required either jQuery, MooTools or YUI, to provide a 11 | bunch of functionality such as CSS manipulation, getting/setting element positions, supporting drag/drop etc). So, 12 | rather than re-write simple methods such as addClass, removeClass, getPosition etc, Katavorio expects those methods 13 | to be provided in the constructor's options object. 14 | 15 | All is not lost, though, as this project also contains DefaultKatavorioHelper - the set of missing methods. 16 | 17 | #### Installation 18 | 19 | `npm install katavorio` 20 | 21 | NOTE: Katavorio does not follow strict semantic versioning. It is not at all recommended that you use wildcards when specifying a dependency on Katavorio. 22 | 23 | #### Dependencies 24 | 25 | None 26 | 27 | #### Imports 28 | 29 | If you have jsPlumb in your page then you already have Katavorio - it is bundled into jsPlumb. Otherwise you'll need 30 | to import two scripts: 31 | 32 | ``` 33 | node_modules/katavorio/src/default-katavorio-helper.js 34 | node_modules/katavorio/src/katavorio.js 35 | ``` 36 | 37 | 38 | 39 | For more information, take a look in [the wiki](https://github.com/jsplumb/katavorio/wiki). 40 | 41 | ### Changelog 42 | 43 | #### 1.5.1 44 | 45 | 17 Sep 2020 46 | 47 | - added a test in `elementRemoved` to check if an element is in fact draggable/droppable before running the code to de-register it. 48 | 49 | #### 1.5.0 50 | 51 | - Changed package name to @jsplumb/katavorio 52 | 53 | #### 1.4.11 54 | 55 | - support constrain functions in a drag selector 56 | 57 | #### 1.4.10 58 | 59 | - support ghost proxy handling by selectors in a drag. 60 | 61 | #### 1.4.9 62 | 63 | - return grid position from snap method on draggable. 64 | 65 | #### 1.4.8 66 | 67 | - support filter and filterExclude in delegated drag handlers 68 | 69 | #### 1.4.7 70 | 71 | - pass current drag element in callback to "should proxy" function. Required when a delegate drag is occurring. 72 | 73 | #### 1.4.6 74 | 75 | - pass the return value of a delegate to the code that tests if a drag can begin. 76 | 77 | #### 1.4.5 78 | 79 | - fixed an issue with drag stop event for single node drags. 80 | 81 | #### 1.4.4 82 | 83 | - support revert function being passed in to constructor. 84 | 85 | #### 1.4.3 86 | 87 | - added support for "combinator rooted" queries for delegated drags. 88 | 89 | #### 1.4.2 90 | 91 | - added support for provision of `ghostProxyParent` when using a ghost proxy to drag. 92 | 93 | #### 1.4.1 94 | 95 | - add test to ensure event's default not prevented when responding to initial mouse down 96 | 97 | #### 1.4.0 98 | 99 | - Add support for multiple selector definitions on a single Drag object, via the new `addSelector` method. You can make some element draggable and then 100 | attach more listeners to that object, rather than having to create a whole new draggable: 101 | 102 | ``` 103 | let d = katavorioInstance.draggable(someElement, { 104 | selector:".childSelector", 105 | start:function(p) { ... }, 106 | etc 107 | }); 108 | 109 | d.addSelector({ 110 | selector:".someOtherChildSelector", 111 | start:function(p) { ... }, 112 | etc 113 | }); 114 | ``` 115 | 116 | 117 | 118 | #### 1.3.0 119 | 120 | - for delegated draggables (ie when you provide a `selector` in the params), we use the class `katavorio-delegated-draggable` now, instead of 121 | where we previously used the default draggable class of `katavorio-draggable`. This can also be overridden in the constructor by setting the 122 | value of `delegatedDraggable`. 123 | 124 | #### 0.28.0 125 | 126 | - add the ability for a user to specify the parent to use when cloning a node for dragging. 127 | 128 | #### 0.26.0 129 | 130 | - added the ability to remove specific drag/drop handlers (previous we could only completely switch off drag/drop) 131 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 125 | 126 | 127 | 128 |
129 | zoom: 130 | 131 | 132 | 133 | 134 | 135 |
136 | 137 |
138 | 139 | 140 |
141 | 142 |
143 |

these elements are constrained to their parent

144 |
FOO BAR
145 |
BAR BAZ
146 |
BAZ
147 |
BAZ
148 |
FOO BAR BAZ
149 |
FOO
150 |
151 | FILTER FOO 152 |
153 |
154 |
155 | FILTER BAR 156 |
157 |
158 | 159 |
160 | 166 |
167 | 173 |
174 | 175 |
176 | 177 |
FOO QUX
178 |
QUX
179 |
POO QUX
180 |
POO
181 | 182 |
183 |
handle
184 |
185 | 186 | 187 | 188 |
189 |

these elements are constrained and are on a 30x30 grid

190 |
FOO BAR
191 |
BAR BAZ
192 |
BAZ
193 |
194 | 195 | 299 | 300 | 301 | 302 | -------------------------------------------------------------------------------- /experiments/clone-parent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 51 | 52 | 53 | 54 |
55 |
ITEM ONE
56 |
ITEM TWO
57 |
58 |
59 | 60 |
61 | 62 | 63 | 64 | 65 | 88 | 89 | -------------------------------------------------------------------------------- /experiments/delegate-drag.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 |
56 | 57 | 58 | 59 | 60 | 123 | 124 | -------------------------------------------------------------------------------- /experiments/ghost-proxy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 36 | 37 | 38 | 39 |
40 |
41 |
42 | 43 |
44 | 45 |
46 |
47 |
48 | 49 | 50 | 51 | 52 | 96 | 97 | -------------------------------------------------------------------------------- /experiments/non-delegate-drag.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 |
56 | 57 | 58 | 59 | 60 | 122 | 123 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | -------------------------------------------------------------------------------- /katavorio-GPLv2-LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | -------------------------------------------------------------------------------- /katavorio-MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 - 2016 jsPlumb, https://jsplumbtoolkit.com/ 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jsplumb/katavorio", 3 | "version": "1.5.1", 4 | "description": "Lightweight drag/drop handler", 5 | "main": "src/katavorio.js", 6 | "files": [ 7 | "src/katavorio.js", 8 | "src/default-katavorio-helper.js" 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/jsplumb/katavorio.git" 13 | }, 14 | "author": "jsPlumb ", 15 | "license": "MIT/GPL2", 16 | "devDependencies": { 17 | "mottle": "1.0.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/default-katavorio-helper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name DefaultKatavorioHelper 3 | * @classDesc Default helper for Katavorio. Provides methods to get/set class names, get/set element positions (using absolute 4 | * coordinates), attach/detach event listeners, and get element sizes. Also shims the `indexOf` function if it is 5 | * missing (IE < 9). 6 | */ 7 | 8 | ; 9 | (function () { 10 | var support = { 11 | cl: 'classList' in document.createElement('a'), 12 | io: 'indexOf' in [] 13 | }, 14 | trim = function (str) { 15 | return str == null ? null : (str.replace(/^\s\s*/, '').replace(/\s\s*$/, '')); 16 | }, 17 | _setClassName = function (el, cn) { 18 | cn = trim(cn); 19 | if (typeof el.className.baseVal != "undefined") // SVG 20 | el.className.baseVal = cn; 21 | else 22 | el.className = cn; 23 | }, 24 | _getClassName = function (el) { 25 | return (typeof el.className.baseVal == "undefined") ? el.className : el.className.baseVal; 26 | }, 27 | _findWithFunction = function (a, f) { 28 | if (a) 29 | for (var i = 0; i < a.length; i++) if (f(a[i])) return i; 30 | return -1; 31 | }, 32 | _indexOf = function (l, v) { 33 | return support.io ? l.indexOf(v) : _findWithFunction(l, function (_v) { 34 | return _v == v; 35 | }); 36 | }, 37 | _classManip = function (el, add, clazz) { 38 | if (support.cl) { 39 | el.classList[add ? "add" : "remove"].apply(el.classList, clazz.split(/\s+/)); 40 | } 41 | else { 42 | var classesToAddOrRemove = clazz.split(/\s+/), 43 | className = _getClassName(el), 44 | curClasses = className.split(/\s+/); 45 | 46 | for (var i = 0; i < classesToAddOrRemove.length; i++) { 47 | if (add) { 48 | if (_indexOf(curClasses, classesToAddOrRemove[i]) == -1) 49 | curClasses.push(classesToAddOrRemove[i]); 50 | } 51 | else { 52 | var idx = _indexOf(curClasses, classesToAddOrRemove[i]); 53 | if (idx != -1) 54 | curClasses.splice(idx, 1); 55 | } 56 | } 57 | _setClassName(el, curClasses.join(" ")); 58 | } 59 | }, 60 | _each = function (spec, fn) { 61 | if (spec == null) return; 62 | if (typeof spec === "string") 63 | fn(document.getElementById(spec)); 64 | else if (spec.length != null) { 65 | for (var i = 0; i < spec.length; i++) 66 | fn(typeof spec[i] === "string" ? document.getElementById(spec[i]) : spec[i]); 67 | } 68 | else 69 | fn(spec); // assume it's an element. 70 | }; 71 | 72 | /** 73 | * @name DefaultKatavorioHelper#constructor 74 | * @desc Constructor for DefaultKatavorioHelper. Takes no parameters. 75 | * @function 76 | */ 77 | this.DefaultKatavorioHelper = function () { 78 | 79 | this.addEvent = function (obj, type, fn) { 80 | if (obj.addEventListener) 81 | obj.addEventListener(type, fn, false); 82 | else if (obj.attachEvent) { 83 | obj["e" + type + fn] = fn; 84 | obj[type + fn] = function () { 85 | obj["e" + type + fn](window.event); 86 | }; 87 | obj.attachEvent("on" + type, obj[type + fn]); 88 | } 89 | }; 90 | 91 | this.removeEvent = function (obj, type, fn) { 92 | if (obj.removeEventListener) 93 | obj.removeEventListener(type, fn, false); 94 | else if (obj.detachEvent) { 95 | obj.detachEvent("on" + type, obj[type + fn]); 96 | obj[type + fn] = null; 97 | obj["e" + type + fn] = null; 98 | } 99 | }; 100 | 101 | this.intersects = function (r1, r2) { 102 | var x1 = r1.x, x2 = r1.x + r1.w, y1 = r1.y, y2 = r1.y + r1.h, 103 | a1 = r2.x, a2 = r2.x + r2.w, b1 = r2.y, b2 = r2.y + r2.h; 104 | 105 | return ( (x1 <= a1 && a1 <= x2) && (y1 <= b1 && b1 <= y2) ) || 106 | ( (x1 <= a2 && a2 <= x2) && (y1 <= b1 && b1 <= y2) ) || 107 | ( (x1 <= a1 && a1 <= x2) && (y1 <= b2 && b2 <= y2) ) || 108 | ( (x1 <= a2 && a1 <= x2) && (y1 <= b2 && b2 <= y2) ) || 109 | ( (a1 <= x1 && x1 <= a2) && (b1 <= y1 && y1 <= b2) ) || 110 | ( (a1 <= x2 && x2 <= a2) && (b1 <= y1 && y1 <= b2) ) || 111 | ( (a1 <= x1 && x1 <= a2) && (b1 <= y2 && y2 <= b2) ) || 112 | ( (a1 <= x2 && x1 <= a2) && (b1 <= y2 && y2 <= b2) ); 113 | }; 114 | 115 | this.getPosition = function (el) { 116 | 117 | // var out = [el.offsetLeft, el.offsetTop], op = el.offsetParent; 118 | // while (op != null) { 119 | // out[0] += op.offsetLeft; 120 | // out[1] += op.offsetTop; 121 | // op = op.offsetParent; 122 | // } 123 | // 124 | // return out; 125 | 126 | return [ el.offsetLeft, el.offsetTop ]; 127 | }; 128 | 129 | this.setPosition = function (el, p) { 130 | el.style.left = p[0] + "px"; 131 | el.style.top = p[1] + "px"; 132 | }; 133 | 134 | this.getSize = function (el) { 135 | return [ el.offsetWidth, el.offsetHeight ]; 136 | }; 137 | 138 | this.addClass = function (el, c) { 139 | _each(el, function (e) { 140 | _classManip(e, true, c); 141 | }); 142 | }; 143 | 144 | this.removeClass = function (el, c) { 145 | _each(el, function (e) { 146 | _classManip(e, false, c); 147 | }); 148 | }; 149 | 150 | this.indexOf = _indexOf; 151 | }; 152 | 153 | // thanks MDC 154 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind?redirectlocale=en-US&redirectslug=JavaScript%2FReference%2FGlobal_Objects%2FFunction%2Fbind 155 | if (!Function.prototype.bind) { 156 | Function.prototype.bind = function (oThis) { 157 | if (typeof this !== "function") { 158 | // closest thing possible to the ECMAScript 5 internal IsCallable function 159 | throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); 160 | } 161 | 162 | var aArgs = Array.prototype.slice.call(arguments, 1), 163 | fToBind = this, 164 | fNOP = function () { 165 | }, 166 | fBound = function () { 167 | return fToBind.apply(this instanceof fNOP && oThis ? this : oThis, 168 | aArgs.concat(Array.prototype.slice.call(arguments))); 169 | }; 170 | 171 | fNOP.prototype = this.prototype; 172 | fBound.prototype = new fNOP(); 173 | 174 | return fBound; 175 | }; 176 | } 177 | 178 | }).call(this); -------------------------------------------------------------------------------- /src/katavorio.js: -------------------------------------------------------------------------------- 1 | /** 2 | drag/drop functionality for use with jsPlumb but with 3 | no knowledge of jsPlumb. supports multiple scopes (separated by whitespace), dragging 4 | multiple elements, constrain to parent, drop filters, drag start filters, custom 5 | css classes. 6 | 7 | a lot of the functionality of this script is expected to be plugged in: 8 | 9 | addClass 10 | removeClass 11 | 12 | addEvent 13 | removeEvent 14 | 15 | getPosition 16 | setPosition 17 | getSize 18 | 19 | indexOf 20 | intersects 21 | 22 | the name came from here: 23 | 24 | http://mrsharpoblunto.github.io/foswig.js/ 25 | 26 | copyright 2016 jsPlumb 27 | */ 28 | 29 | ;(function() { 30 | 31 | "use strict"; 32 | var root = this; 33 | 34 | var _suggest = function(list, item, head) { 35 | if (list.indexOf(item) === -1) { 36 | head ? list.unshift(item) : list.push(item); 37 | return true; 38 | } 39 | return false; 40 | }; 41 | 42 | var _vanquish = function(list, item) { 43 | var idx = list.indexOf(item); 44 | if (idx !== -1) list.splice(idx, 1); 45 | }; 46 | 47 | var _difference = function(l1, l2) { 48 | var d = []; 49 | for (var i = 0; i < l1.length; i++) { 50 | if (l2.indexOf(l1[i]) === -1) 51 | d.push(l1[i]); 52 | } 53 | return d; 54 | }; 55 | 56 | var _isString = function(f) { 57 | return f == null ? false : (typeof f === "string" || f.constructor === String); 58 | }; 59 | 60 | var getOffsetRect = function (elem) { 61 | // (1) 62 | var box = elem.getBoundingClientRect(), 63 | body = document.body, 64 | docElem = document.documentElement, 65 | // (2) 66 | scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop, 67 | scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft, 68 | // (3) 69 | clientTop = docElem.clientTop || body.clientTop || 0, 70 | clientLeft = docElem.clientLeft || body.clientLeft || 0, 71 | // (4) 72 | top = box.top + scrollTop - clientTop, 73 | left = box.left + scrollLeft - clientLeft; 74 | 75 | return { top: Math.round(top), left: Math.round(left) }; 76 | }; 77 | 78 | var matchesSelector = function(el, selector, ctx) { 79 | ctx = ctx || el.parentNode; 80 | var possibles = ctx.querySelectorAll(selector); 81 | for (var i = 0; i < possibles.length; i++) { 82 | if (possibles[i] === el) 83 | return true; 84 | } 85 | return false; 86 | }; 87 | 88 | var findDelegateElement = function(parentElement, childElement, selector) { 89 | if (matchesSelector(childElement, selector, parentElement)) { 90 | return childElement; 91 | } else { 92 | var currentParent = childElement.parentNode; 93 | while (currentParent != null && currentParent !== parentElement) { 94 | if (matchesSelector(currentParent, selector, parentElement)) { 95 | return currentParent; 96 | } else { 97 | currentParent = currentParent.parentNode; 98 | } 99 | } 100 | } 101 | }; 102 | 103 | /** 104 | * Finds all elements matching the given selector, for the given parent. In order to support "scoped root" selectors, 105 | * ie. things like "> .someClass", that is .someClass elements that are direct children of `parentElement`, we have to 106 | * jump through a small hoop here: when a delegate draggable is registered, we write a `katavorio-draggable` attribute 107 | * on the element on which the draggable is registered. Then when this method runs, we grab the value of that attribute and 108 | * prepend it as part of the selector we're looking for. So "> .someClass" ends up being written as 109 | * "[katavorio-draggable='...' > .someClass]", which works with querySelectorAll. 110 | * 111 | * @param availableSelectors 112 | * @param parentElement 113 | * @param childElement 114 | * @returns {*} 115 | */ 116 | var findMatchingSelector = function(availableSelectors, parentElement, childElement) { 117 | var el = null; 118 | var draggableId = parentElement.getAttribute("katavorio-draggable"), 119 | prefix = draggableId != null ? "[katavorio-draggable='" + draggableId + "'] " : ""; 120 | 121 | for (var i = 0; i < availableSelectors.length; i++) { 122 | el = findDelegateElement(parentElement, childElement, prefix + availableSelectors[i].selector); 123 | if (el != null) { 124 | if (availableSelectors[i].filter) { 125 | var matches = matchesSelector(childElement, availableSelectors[i].filter, el), 126 | exclude = availableSelectors[i].filterExclude === true; 127 | 128 | if ( (exclude && !matches) || matches) { 129 | return null; 130 | } 131 | 132 | } 133 | return [ availableSelectors[i], el ]; 134 | } 135 | } 136 | return null; 137 | }; 138 | 139 | var iev = (function() { 140 | var rv = -1; 141 | if (navigator.appName === 'Microsoft Internet Explorer') { 142 | var ua = navigator.userAgent, 143 | re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})"); 144 | if (re.exec(ua) != null) 145 | rv = parseFloat(RegExp.$1); 146 | } 147 | return rv; 148 | })(), 149 | DEFAULT_GRID_X = 10, 150 | DEFAULT_GRID_Y = 10, 151 | isIELT9 = iev > -1 && iev < 9, 152 | isIE9 = iev === 9, 153 | _pl = function(e) { 154 | if (isIELT9) { 155 | return [ e.clientX + document.documentElement.scrollLeft, e.clientY + document.documentElement.scrollTop ]; 156 | } 157 | else { 158 | var ts = _touches(e), t = _getTouch(ts, 0); 159 | // for IE9 pageX might be null if the event was synthesized. We try for pageX/pageY first, 160 | // falling back to clientX/clientY if necessary. In every other browser we want to use pageX/pageY. 161 | return isIE9 ? [t.pageX || t.clientX, t.pageY || t.clientY] : [t.pageX, t.pageY]; 162 | } 163 | }, 164 | _getTouch = function(touches, idx) { return touches.item ? touches.item(idx) : touches[idx]; }, 165 | _touches = function(e) { 166 | return e.touches && e.touches.length > 0 ? e.touches : 167 | e.changedTouches && e.changedTouches.length > 0 ? e.changedTouches : 168 | e.targetTouches && e.targetTouches.length > 0 ? e.targetTouches : 169 | [ e ]; 170 | }, 171 | _classes = { 172 | delegatedDraggable:"katavorio-delegated-draggable", // elements that are the delegated drag handler for a bunch of other elements 173 | draggable:"katavorio-draggable", // draggable elements 174 | droppable:"katavorio-droppable", // droppable elements 175 | drag : "katavorio-drag", // elements currently being dragged 176 | selected:"katavorio-drag-selected", // elements in current drag selection 177 | active : "katavorio-drag-active", // droppables that are targets of a currently dragged element 178 | hover : "katavorio-drag-hover", // droppables over which a matching drag element is hovering 179 | noSelect : "katavorio-drag-no-select", // added to the body to provide a hook to suppress text selection 180 | ghostProxy:"katavorio-ghost-proxy", // added to a ghost proxy element in use when a drag has exited the bounds of its parent. 181 | clonedDrag:"katavorio-clone-drag" // added to a node that is a clone of an element created at the start of a drag 182 | }, 183 | _defaultScope = "katavorio-drag-scope", 184 | _events = [ "stop", "start", "drag", "drop", "over", "out", "beforeStart" ], 185 | _devNull = function() {}, 186 | _true = function() { return true; }, 187 | _foreach = function(l, fn, from) { 188 | for (var i = 0; i < l.length; i++) { 189 | if (l[i] != from) 190 | fn(l[i]); 191 | } 192 | }, 193 | _setDroppablesActive = function(dd, val, andHover, drag) { 194 | _foreach(dd, function(e) { 195 | e.setActive(val); 196 | if (val) e.updatePosition(); 197 | if (andHover) e.setHover(drag, val); 198 | }); 199 | }, 200 | _each = function(obj, fn) { 201 | if (obj == null) return; 202 | obj = !_isString(obj) && (obj.tagName == null && obj.length != null) ? obj : [ obj ]; 203 | for (var i = 0; i < obj.length; i++) 204 | fn.apply(obj[i], [ obj[i] ]); 205 | }, 206 | _consume = function(e) { 207 | if (e.stopPropagation) { 208 | e.stopPropagation(); 209 | e.preventDefault(); 210 | } 211 | else { 212 | e.returnValue = false; 213 | } 214 | }, 215 | _defaultInputFilterSelector = "input,textarea,select,button,option", 216 | // 217 | // filters out events on all input elements, like textarea, checkbox, input, select. 218 | _inputFilter = function(e, el, _katavorio) { 219 | var t = e.srcElement || e.target; 220 | return !matchesSelector(t, _katavorio.getInputFilterSelector(), el); 221 | }; 222 | 223 | var Super = function(el, params, css, scope) { 224 | this.params = params || {}; 225 | this.el = el; 226 | this.params.addClass(this.el, this._class); 227 | this.uuid = _uuid(); 228 | var enabled = true; 229 | this.setEnabled = function(e) { enabled = e; }; 230 | this.isEnabled = function() { return enabled; }; 231 | this.toggleEnabled = function() { enabled = !enabled; }; 232 | this.setScope = function(scopes) { 233 | this.scopes = scopes ? scopes.split(/\s+/) : [ scope ]; 234 | }; 235 | this.addScope = function(scopes) { 236 | var m = {}; 237 | _each(this.scopes, function(s) { m[s] = true;}); 238 | _each(scopes ? scopes.split(/\s+/) : [], function(s) { m[s] = true;}); 239 | this.scopes = []; 240 | for (var i in m) this.scopes.push(i); 241 | }; 242 | this.removeScope = function(scopes) { 243 | var m = {}; 244 | _each(this.scopes, function(s) { m[s] = true;}); 245 | _each(scopes ? scopes.split(/\s+/) : [], function(s) { delete m[s];}); 246 | this.scopes = []; 247 | for (var i in m) this.scopes.push(i); 248 | }; 249 | this.toggleScope = function(scopes) { 250 | var m = {}; 251 | _each(this.scopes, function(s) { m[s] = true;}); 252 | _each(scopes ? scopes.split(/\s+/) : [], function(s) { 253 | if (m[s]) delete m[s]; 254 | else m[s] = true; 255 | }); 256 | this.scopes = []; 257 | for (var i in m) this.scopes.push(i); 258 | }; 259 | this.setScope(params.scope); 260 | this.k = params.katavorio; 261 | return params.katavorio; 262 | }; 263 | 264 | var TRUE = function() { return true; }; 265 | var FALSE = function() { return false; }; 266 | 267 | var Drag = function(el, params, css, scope) { 268 | this._class = css.draggable; 269 | var k = Super.apply(this, arguments); 270 | this.rightButtonCanDrag = this.params.rightButtonCanDrag; 271 | var downAt = [0,0], posAtDown = null, pagePosAtDown = null, pageDelta = [0,0], moving = false, initialScroll = [0,0], 272 | consumeStartEvent = this.params.consumeStartEvent !== false, 273 | dragEl = this.el, 274 | clone = this.params.clone, 275 | scroll = this.params.scroll, 276 | _multipleDrop = params.multipleDrop !== false, 277 | isConstrained = false, 278 | //useGhostProxy = params.ghostProxy === true ? TRUE : params.ghostProxy && typeof params.ghostProxy === "function" ? params.ghostProxy : FALSE, 279 | useGhostProxy, 280 | ghostProxy,// = function(el) { return el.cloneNode(true); }, 281 | elementToDrag = null, 282 | availableSelectors = [], 283 | activeSelectorParams = null, // which, if any, selector config is currently active. 284 | ghostProxyParent = params.ghostProxyParent, 285 | currentParentPosition, 286 | ghostParentPosition, 287 | ghostDx, 288 | ghostDy; 289 | 290 | if (params.ghostProxy === true) { 291 | useGhostProxy = TRUE; 292 | } else { 293 | if (params.ghostProxy && typeof params.ghostProxy === "function") { 294 | useGhostProxy = params.ghostProxy; 295 | } else { 296 | useGhostProxy = function(container, dragEl) { 297 | if (activeSelectorParams && activeSelectorParams.useGhostProxy) { 298 | return activeSelectorParams.useGhostProxy(container, dragEl); 299 | } else { 300 | return false; 301 | } 302 | } 303 | } 304 | } 305 | 306 | if (params.makeGhostProxy) { 307 | ghostProxy = params.makeGhostProxy; 308 | } else { 309 | 310 | ghostProxy = function(el) { 311 | if (activeSelectorParams && activeSelectorParams.makeGhostProxy) { 312 | return activeSelectorParams.makeGhostProxy(el); 313 | } else { 314 | return el.cloneNode(true); 315 | } 316 | }; 317 | 318 | } 319 | 320 | // if an initial selector was provided, push the entire set of params as a selector config. 321 | if (params.selector) { 322 | var draggableId = el.getAttribute("katavorio-draggable"); 323 | if (draggableId == null) { 324 | draggableId = "" + new Date().getTime(); 325 | el.setAttribute("katavorio-draggable", draggableId); 326 | } 327 | 328 | availableSelectors.push(params); 329 | } 330 | 331 | var snapThreshold = params.snapThreshold, 332 | _snap = function(pos, gridX, gridY, thresholdX, thresholdY) { 333 | var _dx = Math.floor(pos[0] / gridX), 334 | _dxl = gridX * _dx, 335 | _dxt = _dxl + gridX, 336 | _x = Math.abs(pos[0] - _dxl) <= thresholdX ? _dxl : Math.abs(_dxt - pos[0]) <= thresholdX ? _dxt : pos[0]; 337 | 338 | var _dy = Math.floor(pos[1] / gridY), 339 | _dyl = gridY * _dy, 340 | _dyt = _dyl + gridY, 341 | _y = Math.abs(pos[1] - _dyl) <= thresholdY ? _dyl : Math.abs(_dyt - pos[1]) <= thresholdY ? _dyt : pos[1]; 342 | 343 | return [ _x, _y]; 344 | }; 345 | 346 | this.posses = []; 347 | this.posseRoles = {}; 348 | 349 | this.toGrid = function(pos) { 350 | if (this.params.grid == null) { 351 | return pos; 352 | } 353 | else { 354 | var tx = this.params.grid ? this.params.grid[0] / 2 : snapThreshold ? snapThreshold : DEFAULT_GRID_X / 2, 355 | ty = this.params.grid ? this.params.grid[1] / 2 : snapThreshold ? snapThreshold : DEFAULT_GRID_Y / 2; 356 | 357 | return _snap(pos, this.params.grid[0], this.params.grid[1], tx, ty); 358 | } 359 | }; 360 | 361 | this.snap = function(x, y) { 362 | if (dragEl == null) return; 363 | x = x || (this.params.grid ? this.params.grid[0] : DEFAULT_GRID_X); 364 | y = y || (this.params.grid ? this.params.grid[1] : DEFAULT_GRID_Y); 365 | var p = this.params.getPosition(dragEl), 366 | tx = this.params.grid ? this.params.grid[0] / 2 : snapThreshold, 367 | ty = this.params.grid ? this.params.grid[1] / 2 : snapThreshold, 368 | snapped = _snap(p, x, y, tx, ty); 369 | 370 | this.params.setPosition(dragEl, snapped); 371 | return snapped; 372 | }; 373 | 374 | this.setUseGhostProxy = function(val) { 375 | useGhostProxy = val ? TRUE : FALSE; 376 | }; 377 | 378 | var constrain; 379 | var negativeFilter = function(pos) { 380 | return (params.allowNegative === false) ? [ Math.max (0, pos[0]), Math.max(0, pos[1]) ] : pos; 381 | }; 382 | 383 | var _setConstrain = function(value) { 384 | constrain = typeof value === "function" ? value : value ? function(pos, dragEl, _constrainRect, _size) { 385 | return negativeFilter([ 386 | Math.max(0, Math.min(_constrainRect.w - _size[0], pos[0])), 387 | Math.max(0, Math.min(_constrainRect.h - _size[1], pos[1])) 388 | ]); 389 | }.bind(this) : function(pos) { return negativeFilter(pos); }; 390 | }.bind(this); 391 | 392 | _setConstrain(typeof this.params.constrain === "function" ? this.params.constrain : (this.params.constrain || this.params.containment)); 393 | 394 | 395 | /** 396 | * Sets whether or not the Drag is constrained. A value of 'true' means constrain to parent bounds; a function 397 | * will be executed and returns true if the position is allowed. 398 | * @param value 399 | */ 400 | this.setConstrain = function(value) { 401 | _setConstrain(value); 402 | }; 403 | 404 | /* private */ var _doConstrain = function(pos, dragEl, _constrainRect, _size) { 405 | if (activeSelectorParams != null && activeSelectorParams.constrain && typeof activeSelectorParams.constrain === "function") { 406 | return activeSelectorParams.constrain(pos, dragEl, _constrainRect, _size); 407 | } else { 408 | return constrain(pos, dragEl, _constrainRect, _size); 409 | } 410 | }; 411 | 412 | var revertFunction; 413 | /** 414 | * Sets a function to call on drag stop, which, if it returns true, indicates that the given element should 415 | * revert to its position before the previous drag. 416 | * @param fn 417 | */ 418 | this.setRevert = function(fn) { 419 | revertFunction = fn; 420 | }; 421 | 422 | if (this.params.revert) { 423 | revertFunction = this.params.revert; 424 | } 425 | 426 | var _assignId = function(obj) { 427 | if (typeof obj === "function") { 428 | obj._katavorioId = _uuid(); 429 | return obj._katavorioId; 430 | } else { 431 | return obj; 432 | } 433 | }, 434 | // a map of { spec -> [ fn, exclusion ] } entries. 435 | _filters = {}, 436 | _testFilter = function(e) { 437 | for (var key in _filters) { 438 | var f = _filters[key]; 439 | var rv = f[0](e); 440 | if (f[1]) rv = !rv; 441 | if (!rv) return false; 442 | } 443 | return true; 444 | }, 445 | _setFilter = this.setFilter = function(f, _exclude) { 446 | if (f) { 447 | var key = _assignId(f); 448 | _filters[key] = [ 449 | function(e) { 450 | var t = e.srcElement || e.target, m; 451 | if (_isString(f)) { 452 | m = matchesSelector(t, f, el); 453 | } 454 | else if (typeof f === "function") { 455 | m = f(e, el); 456 | } 457 | return m; 458 | }, 459 | _exclude !== false 460 | ]; 461 | 462 | } 463 | }, 464 | _addFilter = this.addFilter = _setFilter, 465 | _removeFilter = this.removeFilter = function(f) { 466 | var key = typeof f === "function" ? f._katavorioId : f; 467 | delete _filters[key]; 468 | }; 469 | 470 | this.clearAllFilters = function() { 471 | _filters = {}; 472 | }; 473 | 474 | this.canDrag = this.params.canDrag || _true; 475 | 476 | var constrainRect, 477 | matchingDroppables = [], 478 | intersectingDroppables = []; 479 | 480 | this.addSelector = function(params) { 481 | if (params.selector) { 482 | availableSelectors.push(params); 483 | } 484 | }; 485 | 486 | this.downListener = function(e) { 487 | if (e.defaultPrevented) { return; } 488 | var isNotRightClick = this.rightButtonCanDrag || (e.which !== 3 && e.button !== 2); 489 | if (isNotRightClick && this.isEnabled() && this.canDrag()) { 490 | 491 | var _f = _testFilter(e) && _inputFilter(e, this.el, this.k); 492 | if (_f) { 493 | 494 | activeSelectorParams = null; 495 | elementToDrag = null; 496 | 497 | // if (selector) { 498 | // elementToDrag = findDelegateElement(this.el, e.target || e.srcElement, selector); 499 | // if(elementToDrag == null) { 500 | // return; 501 | // } 502 | // } 503 | if (availableSelectors.length > 0) { 504 | var match = findMatchingSelector(availableSelectors, this.el, e.target || e.srcElement); 505 | if (match != null) { 506 | activeSelectorParams = match[0]; 507 | elementToDrag = match[1]; 508 | } 509 | // elementToDrag = findDelegateElement(this.el, e.target || e.srcElement, selector); 510 | if(elementToDrag == null) { 511 | return; 512 | } 513 | } 514 | else { 515 | elementToDrag = this.el; 516 | } 517 | 518 | if (clone) { 519 | dragEl = elementToDrag.cloneNode(true); 520 | this.params.addClass(dragEl, _classes.clonedDrag); 521 | 522 | dragEl.setAttribute("id", null); 523 | dragEl.style.position = "absolute"; 524 | 525 | if (this.params.parent != null) { 526 | var p = this.params.getPosition(this.el); 527 | dragEl.style.left = p[0] + "px"; 528 | dragEl.style.top = p[1] + "px"; 529 | this.params.parent.appendChild(dragEl); 530 | } else { 531 | // the clone node is added to the body; getOffsetRect gives us a value 532 | // relative to the body. 533 | var b = getOffsetRect(elementToDrag); 534 | dragEl.style.left = b.left + "px"; 535 | dragEl.style.top = b.top + "px"; 536 | 537 | document.body.appendChild(dragEl); 538 | } 539 | 540 | } else { 541 | dragEl = elementToDrag; 542 | } 543 | 544 | consumeStartEvent && _consume(e); 545 | downAt = _pl(e); 546 | if (dragEl && dragEl.parentNode) 547 | { 548 | initialScroll = [dragEl.parentNode.scrollLeft, dragEl.parentNode.scrollTop]; 549 | } 550 | // 551 | this.params.bind(document, "mousemove", this.moveListener); 552 | this.params.bind(document, "mouseup", this.upListener); 553 | k.markSelection(this); 554 | k.markPosses(this); 555 | this.params.addClass(document.body, css.noSelect); 556 | _dispatch("beforeStart", {el:this.el, pos:posAtDown, e:e, drag:this}); 557 | } 558 | else if (this.params.consumeFilteredEvents) { 559 | _consume(e); 560 | } 561 | } 562 | }.bind(this); 563 | 564 | this.moveListener = function(e) { 565 | if (downAt) { 566 | if (!moving) { 567 | var _continue = _dispatch("start", {el:this.el, pos:posAtDown, e:e, drag:this}); 568 | if (_continue !== false) { 569 | if (!downAt) { 570 | return; 571 | } 572 | this.mark(true); 573 | moving = true; 574 | } else { 575 | this.abort(); 576 | } 577 | } 578 | 579 | // it is possible that the start event caused the drag to be aborted. So we check 580 | // again that we are currently dragging. 581 | if (downAt) { 582 | intersectingDroppables.length = 0; 583 | var pos = _pl(e), dx = pos[0] - downAt[0], dy = pos[1] - downAt[1], 584 | z = this.params.ignoreZoom ? 1 : k.getZoom(); 585 | if (dragEl && dragEl.parentNode) 586 | { 587 | dx += dragEl.parentNode.scrollLeft - initialScroll[0]; 588 | dy += dragEl.parentNode.scrollTop - initialScroll[1]; 589 | } 590 | dx /= z; 591 | dy /= z; 592 | this.moveBy(dx, dy, e); 593 | k.updateSelection(dx, dy, this); 594 | k.updatePosses(dx, dy, this); 595 | } 596 | } 597 | }.bind(this); 598 | 599 | this.upListener = function(e) { 600 | if (downAt) { 601 | downAt = null; 602 | this.params.unbind(document, "mousemove", this.moveListener); 603 | this.params.unbind(document, "mouseup", this.upListener); 604 | this.params.removeClass(document.body, css.noSelect); 605 | this.unmark(e); 606 | k.unmarkSelection(this, e); 607 | k.unmarkPosses(this, e); 608 | this.stop(e); 609 | 610 | k.notifyPosseDragStop(this, e); 611 | moving = false; 612 | intersectingDroppables.length = 0; 613 | 614 | if (clone) { 615 | dragEl && dragEl.parentNode && dragEl.parentNode.removeChild(dragEl); 616 | dragEl = null; 617 | } else { 618 | if (revertFunction && revertFunction(dragEl, this.params.getPosition(dragEl)) === true) { 619 | this.params.setPosition(dragEl, posAtDown); 620 | _dispatch("revert", dragEl); 621 | } 622 | } 623 | 624 | } 625 | }.bind(this); 626 | 627 | this.getFilters = function() { return _filters; }; 628 | 629 | this.abort = function() { 630 | if (downAt != null) { 631 | this.upListener(); 632 | } 633 | }; 634 | 635 | /** 636 | * Returns the element that was last dragged. This may be some original element from the DOM, or if `clone` is 637 | * set, then its actually a copy of some original DOM element. In some client calls to this method, it is the 638 | * actual element that was dragged that is desired. In others, it is the original DOM element that the user 639 | * wishes to get - in which case, pass true for `retrieveOriginalElement`. 640 | * 641 | * @returns {*} 642 | */ 643 | this.getDragElement = function(retrieveOriginalElement) { 644 | return retrieveOriginalElement ? elementToDrag || this.el : dragEl || this.el; 645 | }; 646 | 647 | var listeners = {"start":[], "drag":[], "stop":[], "over":[], "out":[], "beforeStart":[], "revert":[] }; 648 | if (params.events.start) listeners.start.push(params.events.start); 649 | if (params.events.beforeStart) listeners.beforeStart.push(params.events.beforeStart); 650 | if (params.events.stop) listeners.stop.push(params.events.stop); 651 | if (params.events.drag) listeners.drag.push(params.events.drag); 652 | if (params.events.revert) listeners.revert.push(params.events.revert); 653 | 654 | this.on = function(evt, fn) { 655 | if (listeners[evt]) listeners[evt].push(fn); 656 | }; 657 | 658 | this.off = function(evt, fn) { 659 | if (listeners[evt]) { 660 | var l = []; 661 | for (var i = 0; i < listeners[evt].length; i++) { 662 | if (listeners[evt][i] !== fn) l.push(listeners[evt][i]); 663 | } 664 | listeners[evt] = l; 665 | } 666 | }; 667 | 668 | var _dispatch = function(evt, value) { 669 | var result = null; 670 | if (activeSelectorParams && activeSelectorParams[evt]) { 671 | result = activeSelectorParams[evt](value); 672 | } else if (listeners[evt]) { 673 | for (var i = 0; i < listeners[evt].length; i++) { 674 | try { 675 | var v = listeners[evt][i](value); 676 | if (v != null) { 677 | result = v; 678 | } 679 | } 680 | catch (e) { } 681 | } 682 | } 683 | return result; 684 | }; 685 | 686 | this.notifyStart = function(e) { 687 | _dispatch("start", {el:this.el, pos:this.params.getPosition(dragEl), e:e, drag:this}); 688 | }; 689 | 690 | this.stop = function(e, force) { 691 | if (force || moving) { 692 | var positions = [], 693 | sel = k.getSelection(), 694 | dPos = this.params.getPosition(dragEl); 695 | 696 | if (sel.length > 0) { 697 | for (var i = 0; i < sel.length; i++) { 698 | var p = this.params.getPosition(sel[i].el); 699 | positions.push([ sel[i].el, { left: p[0], top: p[1] }, sel[i] ]); 700 | } 701 | } 702 | else { 703 | positions.push([ dragEl, {left:dPos[0], top:dPos[1]}, this ]); 704 | } 705 | 706 | _dispatch("stop", { 707 | el: dragEl, 708 | pos: ghostProxyOffsets || dPos, 709 | finalPos:dPos, 710 | e: e, 711 | drag: this, 712 | selection:positions 713 | }); 714 | } 715 | }; 716 | 717 | this.mark = function(andNotify) { 718 | posAtDown = this.params.getPosition(dragEl); 719 | pagePosAtDown = this.params.getPosition(dragEl, true); 720 | pageDelta = [pagePosAtDown[0] - posAtDown[0], pagePosAtDown[1] - posAtDown[1]]; 721 | this.size = this.params.getSize(dragEl); 722 | matchingDroppables = k.getMatchingDroppables(this); 723 | _setDroppablesActive(matchingDroppables, true, false, this); 724 | this.params.addClass(dragEl, this.params.dragClass || css.drag); 725 | 726 | var cs; 727 | if (this.params.getConstrainingRectangle) { 728 | cs = this.params.getConstrainingRectangle(dragEl) 729 | } else { 730 | cs = this.params.getSize(dragEl.parentNode); 731 | } 732 | constrainRect = {w: cs[0], h: cs[1]}; 733 | 734 | ghostDx = 0; 735 | ghostDy = 0; 736 | 737 | if (andNotify) { 738 | k.notifySelectionDragStart(this); 739 | } 740 | }; 741 | var ghostProxyOffsets; 742 | this.unmark = function(e, doNotCheckDroppables) { 743 | _setDroppablesActive(matchingDroppables, false, true, this); 744 | 745 | if (isConstrained && useGhostProxy(elementToDrag, dragEl)) { 746 | ghostProxyOffsets = [dragEl.offsetLeft - ghostDx, dragEl.offsetTop - ghostDy]; 747 | dragEl.parentNode.removeChild(dragEl); 748 | dragEl = elementToDrag; 749 | } 750 | else { 751 | ghostProxyOffsets = null; 752 | } 753 | 754 | this.params.removeClass(dragEl, this.params.dragClass || css.drag); 755 | matchingDroppables.length = 0; 756 | isConstrained = false; 757 | if (!doNotCheckDroppables) { 758 | if (intersectingDroppables.length > 0 && ghostProxyOffsets) { 759 | params.setPosition(elementToDrag, ghostProxyOffsets); 760 | } 761 | intersectingDroppables.sort(_rankSort); 762 | for (var i = 0; i < intersectingDroppables.length; i++) { 763 | var retVal = intersectingDroppables[i].drop(this, e); 764 | if (retVal === true) break; 765 | } 766 | } 767 | }; 768 | this.moveBy = function(dx, dy, e) { 769 | intersectingDroppables.length = 0; 770 | 771 | var desiredLoc = this.toGrid([posAtDown[0] + dx, posAtDown[1] + dy]), 772 | cPos = _doConstrain(desiredLoc, dragEl, constrainRect, this.size); 773 | 774 | // if we should use a ghost proxy... 775 | if (useGhostProxy(this.el, dragEl)) { 776 | // and the element has been dragged outside of its parent bounds 777 | if (desiredLoc[0] !== cPos[0] || desiredLoc[1] !== cPos[1]) { 778 | 779 | // ...if ghost proxy not yet created 780 | if (!isConstrained) { 781 | // create it 782 | var gp = ghostProxy(elementToDrag); 783 | params.addClass(gp, _classes.ghostProxy); 784 | 785 | if (ghostProxyParent) { 786 | ghostProxyParent.appendChild(gp); 787 | // find offset between drag el's parent the ghost parent 788 | currentParentPosition = params.getPosition(elementToDrag.parentNode, true); 789 | ghostParentPosition = params.getPosition(params.ghostProxyParent, true); 790 | ghostDx = currentParentPosition[0] - ghostParentPosition[0]; 791 | ghostDy = currentParentPosition[1] - ghostParentPosition[1]; 792 | 793 | } else { 794 | elementToDrag.parentNode.appendChild(gp); 795 | } 796 | 797 | // the ghost proxy is the drag element 798 | dragEl = gp; 799 | // set this flag so we dont recreate the ghost proxy 800 | isConstrained = true; 801 | } 802 | // now the drag position can be the desired position, as the ghost proxy can support it. 803 | cPos = desiredLoc; 804 | } 805 | else { 806 | // if the element is not outside of its parent bounds, and ghost proxy is in place, 807 | if (isConstrained) { 808 | // remove the ghost proxy from the dom 809 | dragEl.parentNode.removeChild(dragEl); 810 | // reset the drag element to the original element 811 | dragEl = elementToDrag; 812 | // clear this flag. 813 | isConstrained = false; 814 | currentParentPosition = null; 815 | ghostParentPosition = null; 816 | ghostDx = 0; 817 | ghostDy = 0; 818 | } 819 | } 820 | } 821 | 822 | var rect = { x:cPos[0], y:cPos[1], w:this.size[0], h:this.size[1]}, 823 | pageRect = { x:rect.x + pageDelta[0], y:rect.y + pageDelta[1], w:rect.w, h:rect.h}, 824 | focusDropElement = null; 825 | 826 | this.params.setPosition(dragEl, [cPos[0] + ghostDx, cPos[1] + ghostDy]); 827 | 828 | for (var i = 0; i < matchingDroppables.length; i++) { 829 | var r2 = { x:matchingDroppables[i].pagePosition[0], y:matchingDroppables[i].pagePosition[1], w:matchingDroppables[i].size[0], h:matchingDroppables[i].size[1]}; 830 | if (this.params.intersects(pageRect, r2) && (_multipleDrop || focusDropElement == null || focusDropElement === matchingDroppables[i].el) && matchingDroppables[i].canDrop(this)) { 831 | if (!focusDropElement) focusDropElement = matchingDroppables[i].el; 832 | intersectingDroppables.push(matchingDroppables[i]); 833 | matchingDroppables[i].setHover(this, true, e); 834 | } 835 | else if (matchingDroppables[i].isHover()) { 836 | matchingDroppables[i].setHover(this, false, e); 837 | } 838 | } 839 | 840 | _dispatch("drag", {el:this.el, pos:cPos, e:e, drag:this}); 841 | 842 | /* test to see if the parent needs to be scrolled (future) 843 | if (scroll) { 844 | var pnsl = dragEl.parentNode.scrollLeft, pnst = dragEl.parentNode.scrollTop; 845 | console.log("scroll!", pnsl, pnst); 846 | }*/ 847 | }; 848 | this.destroy = function() { 849 | this.params.unbind(this.el, "mousedown", this.downListener); 850 | this.params.unbind(document, "mousemove", this.moveListener); 851 | this.params.unbind(document, "mouseup", this.upListener); 852 | this.downListener = null; 853 | this.upListener = null; 854 | this.moveListener = null; 855 | }; 856 | 857 | // init:register mousedown, and perhaps set a filter 858 | this.params.bind(this.el, "mousedown", this.downListener); 859 | 860 | // if handle provided, use that. otherwise, try to set a filter. 861 | // note that a `handle` selector always results in filterExclude being set to false, ie. 862 | // the selector defines the handle element(s). 863 | if (this.params.handle) 864 | _setFilter(this.params.handle, false); 865 | else 866 | _setFilter(this.params.filter, this.params.filterExclude); 867 | }; 868 | 869 | var Drop = function(el, params, css, scope) { 870 | this._class = css.droppable; 871 | this.params = params || {}; 872 | this.rank = params.rank || 0; 873 | this._activeClass = this.params.activeClass || css.active; 874 | this._hoverClass = this.params.hoverClass || css.hover; 875 | Super.apply(this, arguments); 876 | var hover = false; 877 | this.allowLoopback = this.params.allowLoopback !== false; 878 | 879 | this.setActive = function(val) { 880 | this.params[val ? "addClass" : "removeClass"](this.el, this._activeClass); 881 | }; 882 | 883 | this.updatePosition = function() { 884 | this.position = this.params.getPosition(this.el); 885 | this.pagePosition = this.params.getPosition(this.el, true); 886 | this.size = this.params.getSize(this.el); 887 | }; 888 | 889 | this.canDrop = this.params.canDrop || function(drag) { 890 | return true; 891 | }; 892 | 893 | this.isHover = function() { return hover; }; 894 | 895 | this.setHover = function(drag, val, e) { 896 | // if turning off hover but this was not the drag that caused the hover, ignore. 897 | if (val || this.el._katavorioDragHover == null || this.el._katavorioDragHover === drag.el._katavorio) { 898 | this.params[val ? "addClass" : "removeClass"](this.el, this._hoverClass); 899 | this.el._katavorioDragHover = val ? drag.el._katavorio : null; 900 | if (hover !== val) { 901 | this.params.events[val ? "over" : "out"]({el: this.el, e: e, drag: drag, drop: this}); 902 | } 903 | hover = val; 904 | } 905 | }; 906 | 907 | /** 908 | * A drop event. `drag` is the corresponding Drag object, which may be a Drag for some specific element, or it 909 | * may be a Drag on some element acting as a delegate for elements contained within it. 910 | * @param drag 911 | * @param event 912 | * @returns {*} 913 | */ 914 | this.drop = function(drag, event) { 915 | return this.params.events["drop"]({ drag:drag, e:event, drop:this }); 916 | }; 917 | 918 | this.destroy = function() { 919 | this._class = null; 920 | this._activeClass = null; 921 | this._hoverClass = null; 922 | hover = null; 923 | }; 924 | }; 925 | 926 | var _uuid = function() { 927 | return ('xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 928 | var r = Math.random()*16|0, v = c === 'x' ? r : (r&0x3|0x8); 929 | return v.toString(16); 930 | })); 931 | }; 932 | 933 | var _rankSort = function(a,b) { 934 | return a.rank < b.rank ? 1 : a.rank > b.rank ? -1 : 0; 935 | }; 936 | 937 | var _gel = function(el) { 938 | if (el == null) return null; 939 | el = (typeof el === "string" || el.constructor === String) ? document.getElementById(el) : el; 940 | if (el == null) return null; 941 | el._katavorio = el._katavorio || _uuid(); 942 | return el; 943 | }; 944 | 945 | root.Katavorio = function(katavorioParams) { 946 | 947 | var _selection = [], 948 | _selectionMap = {}; 949 | 950 | this._dragsByScope = {}; 951 | this._dropsByScope = {}; 952 | var _zoom = 1, 953 | _reg = function(obj, map) { 954 | _each(obj, function(_obj) { 955 | for(var i = 0; i < _obj.scopes.length; i++) { 956 | map[_obj.scopes[i]] = map[_obj.scopes[i]] || []; 957 | map[_obj.scopes[i]].push(_obj); 958 | } 959 | }); 960 | }, 961 | _unreg = function(obj, map) { 962 | var c = 0; 963 | _each(obj, function(_obj) { 964 | for(var i = 0; i < _obj.scopes.length; i++) { 965 | if (map[_obj.scopes[i]]) { 966 | var idx = katavorioParams.indexOf(map[_obj.scopes[i]], _obj); 967 | if (idx !== -1) { 968 | map[_obj.scopes[i]].splice(idx, 1); 969 | c++; 970 | } 971 | } 972 | } 973 | }); 974 | 975 | return c > 0 ; 976 | }, 977 | _getMatchingDroppables = this.getMatchingDroppables = function(drag) { 978 | var dd = [], _m = {}; 979 | for (var i = 0; i < drag.scopes.length; i++) { 980 | var _dd = this._dropsByScope[drag.scopes[i]]; 981 | if (_dd) { 982 | for (var j = 0; j < _dd.length; j++) { 983 | if (_dd[j].canDrop(drag) && !_m[_dd[j].uuid] && (_dd[j].allowLoopback || _dd[j].el !== drag.el)) { 984 | _m[_dd[j].uuid] = true; 985 | dd.push(_dd[j]); 986 | } 987 | } 988 | } 989 | } 990 | dd.sort(_rankSort); 991 | return dd; 992 | }, 993 | _prepareParams = function(p) { 994 | p = p || {}; 995 | var _p = { 996 | events:{} 997 | }, i; 998 | for (i in katavorioParams) _p[i] = katavorioParams[i]; 999 | for (i in p) _p[i] = p[i]; 1000 | // events 1001 | 1002 | for (i = 0; i < _events.length; i++) { 1003 | _p.events[_events[i]] = p[_events[i]] || _devNull; 1004 | } 1005 | _p.katavorio = this; 1006 | return _p; 1007 | }.bind(this), 1008 | _mistletoe = function(existingDrag, params) { 1009 | for (var i = 0; i < _events.length; i++) { 1010 | if (params[_events[i]]) { 1011 | existingDrag.on(_events[i], params[_events[i]]); 1012 | } 1013 | } 1014 | }.bind(this), 1015 | _css = {}, 1016 | overrideCss = katavorioParams.css || {}, 1017 | _scope = katavorioParams.scope || _defaultScope; 1018 | 1019 | // prepare map of css classes based on defaults frst, then optional overrides 1020 | for (var i in _classes) _css[i] = _classes[i]; 1021 | for (var i in overrideCss) _css[i] = overrideCss[i]; 1022 | 1023 | var inputFilterSelector = katavorioParams.inputFilterSelector || _defaultInputFilterSelector; 1024 | /** 1025 | * Gets the selector identifying which input elements to filter from drag events. 1026 | * @method getInputFilterSelector 1027 | * @return {String} Current input filter selector. 1028 | */ 1029 | this.getInputFilterSelector = function() { return inputFilterSelector; }; 1030 | 1031 | /** 1032 | * Sets the selector identifying which input elements to filter from drag events. 1033 | * @method setInputFilterSelector 1034 | * @param {String} selector Input filter selector to set. 1035 | * @return {Katavorio} Current instance; method may be chained. 1036 | */ 1037 | this.setInputFilterSelector = function(selector) { 1038 | inputFilterSelector = selector; 1039 | return this; 1040 | }; 1041 | 1042 | /** 1043 | * Either makes the given element draggable, or identifies it as an element inside which some identified list 1044 | * of elements may be draggable. 1045 | * @param el 1046 | * @param params 1047 | * @returns {Array} 1048 | */ 1049 | this.draggable = function(el, params) { 1050 | var o = []; 1051 | _each(el, function (_el) { 1052 | _el = _gel(_el); 1053 | if (_el != null) { 1054 | if (_el._katavorioDrag == null) { 1055 | var p = _prepareParams(params); 1056 | _el._katavorioDrag = new Drag(_el, p, _css, _scope); 1057 | _reg(_el._katavorioDrag, this._dragsByScope); 1058 | o.push(_el._katavorioDrag); 1059 | katavorioParams.addClass(_el, p.selector ? _css.delegatedDraggable : _css.draggable); 1060 | } 1061 | else { 1062 | _mistletoe(_el._katavorioDrag, params); 1063 | } 1064 | } 1065 | }.bind(this)); 1066 | return o; 1067 | }; 1068 | 1069 | this.droppable = function(el, params) { 1070 | var o = []; 1071 | _each(el, function(_el) { 1072 | _el = _gel(_el); 1073 | if (_el != null) { 1074 | var drop = new Drop(_el, _prepareParams(params), _css, _scope); 1075 | _el._katavorioDrop = _el._katavorioDrop || []; 1076 | _el._katavorioDrop.push(drop); 1077 | _reg(drop, this._dropsByScope); 1078 | o.push(drop); 1079 | katavorioParams.addClass(_el, _css.droppable); 1080 | } 1081 | }.bind(this)); 1082 | return o; 1083 | }; 1084 | 1085 | /** 1086 | * @name Katavorio#select 1087 | * @function 1088 | * @desc Adds an element to the current selection (for multiple node drag) 1089 | * @param {Element|String} DOM element - or id of the element - to add. 1090 | */ 1091 | this.select = function(el) { 1092 | _each(el, function() { 1093 | var _el = _gel(this); 1094 | if (_el && _el._katavorioDrag) { 1095 | if (!_selectionMap[_el._katavorio]) { 1096 | _selection.push(_el._katavorioDrag); 1097 | _selectionMap[_el._katavorio] = [ _el, _selection.length - 1 ]; 1098 | katavorioParams.addClass(_el, _css.selected); 1099 | } 1100 | } 1101 | }); 1102 | return this; 1103 | }; 1104 | 1105 | /** 1106 | * @name Katavorio#deselect 1107 | * @function 1108 | * @desc Removes an element from the current selection (for multiple node drag) 1109 | * @param {Element|String} DOM element - or id of the element - to remove. 1110 | */ 1111 | this.deselect = function(el) { 1112 | _each(el, function() { 1113 | var _el = _gel(this); 1114 | if (_el && _el._katavorio) { 1115 | var e = _selectionMap[_el._katavorio]; 1116 | if (e) { 1117 | var _s = []; 1118 | for (var i = 0; i < _selection.length; i++) 1119 | if (_selection[i].el !== _el) _s.push(_selection[i]); 1120 | _selection = _s; 1121 | delete _selectionMap[_el._katavorio]; 1122 | katavorioParams.removeClass(_el, _css.selected); 1123 | } 1124 | } 1125 | }); 1126 | return this; 1127 | }; 1128 | 1129 | this.deselectAll = function() { 1130 | for (var i in _selectionMap) { 1131 | var d = _selectionMap[i]; 1132 | katavorioParams.removeClass(d[0], _css.selected); 1133 | } 1134 | 1135 | _selection.length = 0; 1136 | _selectionMap = {}; 1137 | }; 1138 | 1139 | this.markSelection = function(drag) { 1140 | _foreach(_selection, function(e) { e.mark(); }, drag); 1141 | }; 1142 | 1143 | this.markPosses = function(drag) { 1144 | if (drag.posses) { 1145 | _each(drag.posses, function(p) { 1146 | if (drag.posseRoles[p] && _posses[p]) { 1147 | _foreach(_posses[p].members, function (d) { 1148 | d.mark(); 1149 | }, drag); 1150 | } 1151 | }) 1152 | } 1153 | }; 1154 | 1155 | this.unmarkSelection = function(drag, event) { 1156 | _foreach(_selection, function(e) { e.unmark(event); }, drag); 1157 | }; 1158 | 1159 | this.unmarkPosses = function(drag, event) { 1160 | if (drag.posses) { 1161 | _each(drag.posses, function(p) { 1162 | if (drag.posseRoles[p] && _posses[p]) { 1163 | _foreach(_posses[p].members, function (d) { 1164 | d.unmark(event, true); 1165 | }, drag); 1166 | } 1167 | }); 1168 | } 1169 | }; 1170 | 1171 | this.getSelection = function() { return _selection.slice(0); }; 1172 | 1173 | this.updateSelection = function(dx, dy, drag) { 1174 | _foreach(_selection, function(e) { e.moveBy(dx, dy); }, drag); 1175 | }; 1176 | 1177 | var _posseAction = function(fn, drag) { 1178 | if (drag.posses) { 1179 | _each(drag.posses, function(p) { 1180 | if (drag.posseRoles[p] && _posses[p]) { 1181 | _foreach(_posses[p].members, function (e) { 1182 | fn(e); 1183 | }, drag); 1184 | } 1185 | }); 1186 | } 1187 | }; 1188 | 1189 | this.updatePosses = function(dx, dy, drag) { 1190 | _posseAction(function(e) { e.moveBy(dx, dy); }, drag); 1191 | }; 1192 | 1193 | this.notifyPosseDragStop = function(drag, evt) { 1194 | _posseAction(function(e) { e.stop(evt, true); }, drag); 1195 | }; 1196 | 1197 | this.notifySelectionDragStop = function(drag, evt) { 1198 | _foreach(_selection, function(e) { e.stop(evt, true); }, drag); 1199 | }; 1200 | 1201 | this.notifySelectionDragStart = function(drag, evt) { 1202 | _foreach(_selection, function(e) { e.notifyStart(evt);}, drag); 1203 | }; 1204 | 1205 | this.setZoom = function(z) { _zoom = z; }; 1206 | this.getZoom = function() { return _zoom; }; 1207 | 1208 | // does the work of changing scopes 1209 | var _scopeManip = function(kObj, scopes, map, fn) { 1210 | _each(kObj, function(_kObj) { 1211 | _unreg(_kObj, map); // deregister existing scopes 1212 | _kObj[fn](scopes); // set scopes 1213 | _reg(_kObj, map); // register new ones 1214 | }); 1215 | }; 1216 | 1217 | _each([ "set", "add", "remove", "toggle"], function(v) { 1218 | this[v + "Scope"] = function(el, scopes) { 1219 | _scopeManip(el._katavorioDrag, scopes, this._dragsByScope, v + "Scope"); 1220 | _scopeManip(el._katavorioDrop, scopes, this._dropsByScope, v + "Scope"); 1221 | }.bind(this); 1222 | this[v + "DragScope"] = function(el, scopes) { 1223 | _scopeManip(el.constructor === Drag ? el : el._katavorioDrag, scopes, this._dragsByScope, v + "Scope"); 1224 | }.bind(this); 1225 | this[v + "DropScope"] = function(el, scopes) { 1226 | _scopeManip(el.constructor === Drop ? el : el._katavorioDrop, scopes, this._dropsByScope, v + "Scope"); 1227 | }.bind(this); 1228 | }.bind(this)); 1229 | 1230 | this.snapToGrid = function(x, y) { 1231 | for (var s in this._dragsByScope) { 1232 | _foreach(this._dragsByScope[s], function(d) { d.snap(x, y); }); 1233 | } 1234 | }; 1235 | 1236 | this.getDragsForScope = function(s) { return this._dragsByScope[s]; }; 1237 | this.getDropsForScope = function(s) { return this._dropsByScope[s]; }; 1238 | 1239 | var _destroy = function(el, type, map) { 1240 | el = _gel(el); 1241 | if (el[type]) { 1242 | 1243 | // remove from selection, if present. 1244 | var selIdx = _selection.indexOf(el[type]); 1245 | if (selIdx >= 0) { 1246 | _selection.splice(selIdx, 1); 1247 | } 1248 | 1249 | if (_unreg(el[type], map)) { 1250 | _each(el[type], function(kObj) { kObj.destroy() }); 1251 | } 1252 | 1253 | delete el[type]; 1254 | } 1255 | }; 1256 | 1257 | var _removeListener = function(el, type, evt, fn) { 1258 | el = _gel(el); 1259 | if (el[type]) { 1260 | el[type].off(evt, fn); 1261 | } 1262 | }; 1263 | 1264 | this.elementRemoved = function(el) { 1265 | if (el["_katavorioDrag"]) { 1266 | this.destroyDraggable(el); 1267 | } 1268 | if (el["_katavorioDrop"]) { 1269 | this.destroyDroppable(el); 1270 | } 1271 | }; 1272 | 1273 | /** 1274 | * Either completely remove drag functionality from the given element, or remove a specific event handler. If you 1275 | * call this method with a single argument - the element - all drag functionality is removed from it. Otherwise, if 1276 | * you provide an event name and listener function, this function is de-registered (if found). 1277 | * @param el Element to update 1278 | * @param {string} [evt] Optional event name to unsubscribe 1279 | * @param {Function} [fn] Optional function to unsubscribe 1280 | */ 1281 | this.destroyDraggable = function(el, evt, fn) { 1282 | if (arguments.length === 1) { 1283 | _destroy(el, "_katavorioDrag", this._dragsByScope); 1284 | } else { 1285 | _removeListener(el, "_katavorioDrag", evt, fn); 1286 | } 1287 | }; 1288 | 1289 | /** 1290 | * Either completely remove drop functionality from the given element, or remove a specific event handler. If you 1291 | * call this method with a single argument - the element - all drop functionality is removed from it. Otherwise, if 1292 | * you provide an event name and listener function, this function is de-registered (if found). 1293 | * @param el Element to update 1294 | * @param {string} [evt] Optional event name to unsubscribe 1295 | * @param {Function} [fn] Optional function to unsubscribe 1296 | */ 1297 | this.destroyDroppable = function(el, evt, fn) { 1298 | if (arguments.length === 1) { 1299 | _destroy(el, "_katavorioDrop", this._dropsByScope); 1300 | } else { 1301 | _removeListener(el, "_katavorioDrop", evt, fn); 1302 | } 1303 | }; 1304 | 1305 | this.reset = function() { 1306 | this._dragsByScope = {}; 1307 | this._dropsByScope = {}; 1308 | _selection = []; 1309 | _selectionMap = {}; 1310 | _posses = {}; 1311 | }; 1312 | 1313 | // ----- groups 1314 | var _posses = {}; 1315 | 1316 | var _processOneSpec = function(el, _spec, dontAddExisting) { 1317 | var posseId = _isString(_spec) ? _spec : _spec.id; 1318 | var active = _isString(_spec) ? true : _spec.active !== false; 1319 | var posse = _posses[posseId] || (function() { 1320 | var g = {name:posseId, members:[]}; 1321 | _posses[posseId] = g; 1322 | return g; 1323 | })(); 1324 | _each(el, function(_el) { 1325 | if (_el._katavorioDrag) { 1326 | 1327 | if (dontAddExisting && _el._katavorioDrag.posseRoles[posse.name] != null) return; 1328 | 1329 | _suggest(posse.members, _el._katavorioDrag); 1330 | _suggest(_el._katavorioDrag.posses, posse.name); 1331 | _el._katavorioDrag.posseRoles[posse.name] = active; 1332 | } 1333 | }); 1334 | return posse; 1335 | }; 1336 | 1337 | /** 1338 | * Add the given element to the posse with the given id, creating the group if it at first does not exist. 1339 | * @method addToPosse 1340 | * @param {Element} el Element to add. 1341 | * @param {String...|Object...} spec Variable args parameters. Each argument can be a either a String, indicating 1342 | * the ID of a Posse to which the element should be added as an active participant, or an Object containing 1343 | * `{ id:"posseId", active:false/true}`. In the latter case, if `active` is not provided it is assumed to be 1344 | * true. 1345 | * @returns {Posse|Posse[]} The Posse(s) to which the element(s) was/were added. 1346 | */ 1347 | this.addToPosse = function(el, spec) { 1348 | 1349 | var posses = []; 1350 | 1351 | for (var i = 1; i < arguments.length; i++) { 1352 | posses.push(_processOneSpec(el, arguments[i])); 1353 | } 1354 | 1355 | return posses.length === 1 ? posses[0] : posses; 1356 | }; 1357 | 1358 | /** 1359 | * Sets the posse(s) for the element with the given id, creating those that do not yet exist, and removing from 1360 | * the element any current Posses that are not specified by this method call. This method will not change the 1361 | * active/passive state if it is given a posse in which the element is already a member. 1362 | * @method setPosse 1363 | * @param {Element} el Element to set posse(s) on. 1364 | * @param {String...|Object...} spec Variable args parameters. Each argument can be a either a String, indicating 1365 | * the ID of a Posse to which the element should be added as an active participant, or an Object containing 1366 | * `{ id:"posseId", active:false/true}`. In the latter case, if `active` is not provided it is assumed to be 1367 | * true. 1368 | * @returns {Posse|Posse[]} The Posse(s) to which the element(s) now belongs. 1369 | */ 1370 | this.setPosse = function(el, spec) { 1371 | 1372 | var posses = []; 1373 | 1374 | for (var i = 1; i < arguments.length; i++) { 1375 | posses.push(_processOneSpec(el, arguments[i], true).name); 1376 | } 1377 | 1378 | _each(el, function(_el) { 1379 | if (_el._katavorioDrag) { 1380 | var diff = _difference(_el._katavorioDrag.posses, posses); 1381 | var p = []; 1382 | Array.prototype.push.apply(p, _el._katavorioDrag.posses); 1383 | for (var i = 0; i < diff.length; i++) { 1384 | this.removeFromPosse(_el, diff[i]); 1385 | } 1386 | } 1387 | }.bind(this)); 1388 | 1389 | return posses.length === 1 ? posses[0] : posses; 1390 | }; 1391 | 1392 | /** 1393 | * Remove the given element from the given posse(s). 1394 | * @method removeFromPosse 1395 | * @param {Element} el Element to remove. 1396 | * @param {String...} posseId Varargs parameter: one value for each posse to remove the element from. 1397 | */ 1398 | this.removeFromPosse = function(el, posseId) { 1399 | if (arguments.length < 2) throw new TypeError("No posse id provided for remove operation"); 1400 | for(var i = 1; i < arguments.length; i++) { 1401 | posseId = arguments[i]; 1402 | _each(el, function (_el) { 1403 | if (_el._katavorioDrag && _el._katavorioDrag.posses) { 1404 | var d = _el._katavorioDrag; 1405 | _each(posseId, function (p) { 1406 | _vanquish(_posses[p].members, d); 1407 | _vanquish(d.posses, p); 1408 | delete d.posseRoles[p]; 1409 | }); 1410 | } 1411 | }); 1412 | } 1413 | }; 1414 | 1415 | /** 1416 | * Remove the given element from all Posses to which it belongs. 1417 | * @method removeFromAllPosses 1418 | * @param {Element|Element[]} el Element to remove from Posses. 1419 | */ 1420 | this.removeFromAllPosses = function(el) { 1421 | _each(el, function(_el) { 1422 | if (_el._katavorioDrag && _el._katavorioDrag.posses) { 1423 | var d = _el._katavorioDrag; 1424 | _each(d.posses, function(p) { 1425 | _vanquish(_posses[p].members, d); 1426 | }); 1427 | d.posses.length = 0; 1428 | d.posseRoles = {}; 1429 | } 1430 | }); 1431 | }; 1432 | 1433 | /** 1434 | * Changes the participation state for the element in the Posse with the given ID. 1435 | * @param {Element|Element[]} el Element(s) to change state for. 1436 | * @param {String} posseId ID of the Posse to change element state for. 1437 | * @param {Boolean} state True to make active, false to make passive. 1438 | */ 1439 | this.setPosseState = function(el, posseId, state) { 1440 | var posse = _posses[posseId]; 1441 | if (posse) { 1442 | _each(el, function(_el) { 1443 | if (_el._katavorioDrag && _el._katavorioDrag.posses) { 1444 | _el._katavorioDrag.posseRoles[posse.name] = state; 1445 | } 1446 | }); 1447 | } 1448 | }; 1449 | 1450 | }; 1451 | 1452 | root.Katavorio.version = "1.0.0"; 1453 | 1454 | if (typeof exports !== "undefined") { 1455 | exports.Katavorio = root.Katavorio; 1456 | } 1457 | 1458 | }).call(typeof window !== 'undefined' ? window : this); 1459 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Katavorio qUnit tests 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

17 |
18 |

19 |
    20 | 21 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/memory.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 20 | 21 | 22 | 23 | 24 |
    25 | 26 | 27 | 28 | 29 | 30 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /test/qunit-1.11.0.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-1.11.0.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 | --------------------------------------------------------------------------------