├── js ├── .DS_Store ├── jquery │ ├── component.json │ └── composer.json └── jquery.hotkeys │ ├── component.json │ ├── composer.json │ └── jquery.hotkeys.js ├── icons ├── icon128.png ├── icon16.png ├── icon19.png ├── icon256.png ├── icon48.png ├── icon512.png ├── icon.graffle └── icon.svg ├── images ├── actionshot.gif ├── promo-small.png ├── screenshot.png ├── promo-small@2x.png ├── promo-small.graffle │ ├── data.plist │ └── image2.png └── screenshot.graffle │ ├── data.plist │ └── image5.png ├── tests ├── lib │ ├── jasmine-2.3.4 │ │ ├── jasmine_favicon.png │ │ ├── boot.js │ │ ├── console.js │ │ ├── jasmine-html.js │ │ └── jasmine.css │ ├── mock.js │ └── jasmine-jquery.js ├── test1.html ├── SpecRunner.html ├── spec │ └── spec.js ├── test3.html ├── test2.html ├── test8.html ├── test9.html ├── test6.html ├── test5.html └── test7.html ├── .gitignore ├── src ├── bg │ ├── background.html │ └── background.js ├── shared │ └── shared.js ├── inject │ ├── inject.css │ └── inject.js └── options │ ├── index.html │ ├── options.js │ └── options.css ├── LICENSE ├── manifest.json └── README.md /js/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesandres/ColumnCopy/HEAD/js/.DS_Store -------------------------------------------------------------------------------- /icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesandres/ColumnCopy/HEAD/icons/icon128.png -------------------------------------------------------------------------------- /icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesandres/ColumnCopy/HEAD/icons/icon16.png -------------------------------------------------------------------------------- /icons/icon19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesandres/ColumnCopy/HEAD/icons/icon19.png -------------------------------------------------------------------------------- /icons/icon256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesandres/ColumnCopy/HEAD/icons/icon256.png -------------------------------------------------------------------------------- /icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesandres/ColumnCopy/HEAD/icons/icon48.png -------------------------------------------------------------------------------- /icons/icon512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesandres/ColumnCopy/HEAD/icons/icon512.png -------------------------------------------------------------------------------- /icons/icon.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesandres/ColumnCopy/HEAD/icons/icon.graffle -------------------------------------------------------------------------------- /images/actionshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesandres/ColumnCopy/HEAD/images/actionshot.gif -------------------------------------------------------------------------------- /images/promo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesandres/ColumnCopy/HEAD/images/promo-small.png -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesandres/ColumnCopy/HEAD/images/screenshot.png -------------------------------------------------------------------------------- /images/promo-small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesandres/ColumnCopy/HEAD/images/promo-small@2x.png -------------------------------------------------------------------------------- /images/promo-small.graffle/data.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesandres/ColumnCopy/HEAD/images/promo-small.graffle/data.plist -------------------------------------------------------------------------------- /images/promo-small.graffle/image2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesandres/ColumnCopy/HEAD/images/promo-small.graffle/image2.png -------------------------------------------------------------------------------- /images/screenshot.graffle/data.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesandres/ColumnCopy/HEAD/images/screenshot.graffle/data.plist -------------------------------------------------------------------------------- /images/screenshot.graffle/image5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesandres/ColumnCopy/HEAD/images/screenshot.graffle/image5.png -------------------------------------------------------------------------------- /tests/lib/jasmine-2.3.4/jasmine_favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesandres/ColumnCopy/HEAD/tests/lib/jasmine-2.3.4/jasmine_favicon.png -------------------------------------------------------------------------------- /tests/lib/mock.js: -------------------------------------------------------------------------------- 1 | chrome = { 2 | extension: { 3 | sendRequest: function(params, callback) { 4 | var options = {}; 5 | return options; 6 | } 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Keep junk OUT 2 | *.sql 3 | *.sql.gz 4 | *.sql.bz2 5 | *.kpf 6 | *.komodoproject 7 | *.proj 8 | *.tmp 9 | .DS_Store 10 | Icon 11 | Thumbs.db 12 | .bundle 13 | .sass-cache 14 | *.sublime-project 15 | *.sublime-workspace 16 | ._* -------------------------------------------------------------------------------- /js/jquery/component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery", 3 | "version": "3.1.1", 4 | "main": "./jquery.js", 5 | "dependencies": {}, 6 | "gitHead": "ee2e377494a882f043e6d8abc67ac6370ee83d9c", 7 | "_id": "jquery@3.1.1", 8 | "readme": "ERROR: No README.md file found!", 9 | "description": "ERROR: No README.md file found!", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:jquery/jquery.git" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/bg/background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /js/jquery.hotkeys/component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery.hotkeys", 3 | "version": "0.2.0", 4 | "main": "./jquery.hotkeys.js", 5 | "dependencies": { 6 | "jquery": "3.1.1" 7 | }, 8 | "gitHead": "f24f1da275aab7881ab501055c256add6f690de4", 9 | "_id": "jquery.hotkeys@0.2.0", 10 | "readme": "ERROR: No README.md file found!", 11 | "description": "ERROR: No README.md file found!", 12 | "repository": { 13 | "type": "git", 14 | "url": "git@github.com:jeresig/jquery.hotkeys.git" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /js/jquery.hotkeys/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "components/jquery.hotkeys", 3 | "description": "jQuery Hotkeys Plugin", 4 | "type": "component", 5 | "homepage": "https://github.com/jeresig/jquery.hotkeys", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Tzury Bar Yochay", 10 | "email": "tzury@reblaze.com" 11 | }, 12 | { 13 | "name": "John Resig", 14 | "email": "jeresig@gmail.com" 15 | } 16 | ], 17 | "extra": { 18 | "js": "jquery.hotkeys.js" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /js/jquery/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "components/jquery", 3 | "description": "jQuery JavaScript Library", 4 | "type": "component", 5 | "homepage": "http://jquery.com", 6 | "license": "MIT", 7 | "support": { 8 | "irc": "irc://irc.freenode.org/jquery", 9 | "issues": "http://bugs.jquery.com", 10 | "forum": "http://forum.jquery.com", 11 | "wiki": "http://docs.jquery.com/", 12 | "source": "https://github.com/jquery/jquery" 13 | }, 14 | "authors": [ 15 | { 16 | "name": "John Resig", 17 | "email": "jeresig@gmail.com" 18 | } 19 | ], 20 | "extra": { 21 | "js": "jquery.js" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/test1.html: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | ColumnCopy - Test 1 - Simple 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
Column 1Column 2Column 3
123
102030
100200300
44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/shared/shared.js: -------------------------------------------------------------------------------- 1 | function getDefaultOptions () { 2 | var defaultOptions = { 3 | columnSeparator: "\t", 4 | rowSeparator: "\n", 5 | cellWrapper: '"', 6 | 7 | columnHotkey: 'alt', 8 | tableHotkey: 'alt+shift', 9 | 10 | hyperlinkMode: 'off' 11 | }; 12 | 13 | 14 | if (window.navigator.userAgent.match(/Windows/) !== null) { 15 | defaultOptions.rowSeparator = "\r\n"; // Yuck. 16 | } 17 | 18 | if (window.navigator.userAgent.match(/Linux/) !== null) { 19 | defaultOptions.columnHotkey = 'ctrl'; 20 | defaultOptions.tableHotkey = 'ctrl+shift'; 21 | } 22 | 23 | return defaultOptions; 24 | } 25 | 26 | function getOptions () { 27 | var options = {}; 28 | 29 | if (localStorage.options) { 30 | options = $.extend(getDefaultOptions(), JSON.parse(localStorage.options)); 31 | } else { 32 | options = getDefaultOptions(); 33 | } 34 | 35 | return options; 36 | } 37 | -------------------------------------------------------------------------------- /tests/SpecRunner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Jasmine Spec Runner v2.3.4 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/inject/inject.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | /** 4 | * Based on work by Daniel Eden in the animate.css library 5 | * http://daneden.me/animate 6 | */ 7 | 8 | .CC-animated { 9 | -webkit-animation-duration: 1s; 10 | animation-duration: 1s; 11 | -webkit-animation-fill-mode: both; 12 | animation-fill-mode: both; 13 | } 14 | 15 | @-webkit-keyframes CC-copiedToClipboard { 16 | 0% { -webkit-transform: scale(1); box-shadow: none; } 17 | 50% { -webkit-transform: scale(1.1); box-shadow: 0px 0px 5px 2px rgba(0, 128, 255, 0.5); } 18 | 100% { -webkit-transform: scale(1); box-shadow: none;} 19 | } 20 | @keyframes CC-copiedToClipboard { 21 | 0% { transform: scale(1); box-shadow: none; } 22 | 50% { transform: scale(1.1); box-shadow: 0px 0px 5px 2px rgba(0, 128, 255, 0.5); } 23 | 100% { transform: scale(1); box-shadow: none;} 24 | } 25 | 26 | .CC-copiedToClipboard { 27 | -webkit-animation-name: CC-copiedToClipboard; 28 | animation-name: CC-copiedToClipboard; 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 James Andres 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ColumnCopy", 3 | "version": "0.5.0", 4 | "manifest_version": 2, 5 | "description": "Enables copying columns from tables.", 6 | "homepage_url": "https://github.com/jamesandres/ColumnCopy", 7 | "icons": { 8 | "16": "icons/icon16.png", 9 | "48": "icons/icon48.png", 10 | "128": "icons/icon128.png" 11 | }, 12 | "background": { 13 | "page": "src/bg/background.html", 14 | "persistent": true 15 | }, 16 | "options_page": "src/options/index.html", 17 | "permissions": [ 18 | "clipboardWrite", 19 | "contextMenus" 20 | ], 21 | "content_scripts": [ 22 | { 23 | "matches": [ 24 | "file://*/*", 25 | "http://*/*", 26 | "https://*/*" 27 | ], 28 | "js": [ 29 | "js/jquery/jquery.js", 30 | "js/jquery.hotkeys/jquery.hotkeys.js", 31 | "src/inject/inject.js" 32 | ], 33 | "css": [ 34 | "src/inject/inject.css" 35 | ] 36 | } 37 | ], 38 | "web_accessible_resources": [ 39 | "js/jquery/jquery.js", 40 | "js/jquery/jquery.min.map" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /tests/spec/spec.js: -------------------------------------------------------------------------------- 1 | describe('ColumnCopy', function() { 2 | function runTest(fixtureName) { 3 | var rows; 4 | var spec; 5 | 6 | beforeEach(function() { 7 | var fixture, m, $table, _ColumnCopy 8 | 9 | jasmine.getFixtures().fixturesPath = './'; 10 | fixture = readFixtures(fixtureName); 11 | setFixtures(fixture); 12 | m = //.exec(fixture); 13 | spec = JSON.parse(m[1]); 14 | 15 | $table = $('table:first'); 16 | 17 | _ColumnCopy = new ColumnCopy(); 18 | 19 | _ColumnCopy.options = getDefaultOptions(); 20 | 21 | rows = _ColumnCopy.getValuesForTable($table); 22 | }); 23 | 24 | it('correctly get values via ColumnCopy.getValuesForTable: ' + fixtureName, function() { 25 | expect(rows).toEqual(spec.rows); 26 | }); 27 | } 28 | 29 | runTest('test1.html'); 30 | runTest('test2.html'); 31 | runTest('test3.html'); 32 | runTest('test4.html'); 33 | runTest('test5.html'); 34 | runTest('test6.html'); 35 | runTest('test7.html'); 36 | runTest('test8.html'); 37 | runTest('test9.html'); 38 | }); 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![image](https://raw.github.com/jamesandres/ColumnCopy/master/icons/icon48.png) ColumnCopy 2 | 3 | A Google Chrome extension which enables selecting columns from table. 4 | 5 | ## Installation 6 | 7 | 1. Download a the [latest ColumnCopy zip](https://github.com/jamesandres/ColumnCopy/archive/master.zip). 8 | 2. Go to the extensions page in Chrome, it's a tab on the options page 9 | 3. Tick the "Developer mode" checkbox 10 | 4. Click "Load unpacked extension" and choose the ColumnCopy folder you just unzipped 11 | 12 | 13 | ## Usage 14 | 15 | 1. Enable the extension. 16 | 2. Alt + Click on any table cell. The column contents is now in your clipboard. 17 | 3. Shift + Alt + Click on any table cell. The table contents is now in your clipboard. 18 | 19 | 20 | ## Credits 21 | 22 | "Pinstriped Suit" graphic used in promotional images by Alex Berkowitz from 23 | SubtlePatterns.com. See: http://subtlepatterns.com/pinstriped-suit/ 24 | 25 | 26 | ## License 27 | 28 | You may use, modify and distribute ColumnCopy freely under the MIT license. See the LICENCE file contained in this project for more details. 29 | -------------------------------------------------------------------------------- /src/bg/background.js: -------------------------------------------------------------------------------- 1 | // Bind the context handlers 2 | var contexts = { 3 | copyColumn: chrome.contextMenus.create({ 4 | 'title': 'Copy this column', 5 | 'contexts': ['all'], 6 | 'onclick': handleContextMenuClick 7 | }), 8 | copyTable: chrome.contextMenus.create({ 9 | 'title': 'Copy entire table', 10 | 'contexts': ['all'], 11 | 'onclick': handleContextMenuClick 12 | }), 13 | }; 14 | 15 | function handleContextMenuClick(info, tab) { 16 | switch (info.menuItemId) { 17 | case contexts.copyColumn: 18 | chrome.tabs.sendMessage(tab.id, { columnCopyContextMenuClick: 'copyColumn' }); 19 | break; 20 | case contexts.copyTable: 21 | chrome.tabs.sendMessage(tab.id, { columnCopyContextMenuClick: 'copyTable' }); 22 | break; 23 | } 24 | } 25 | 26 | chrome.extension.onMessage.addListener(function(message, sender, sendResponse) { 27 | if (message.toCopy) { 28 | var textarea = document.getElementById("clipboardBridge"); 29 | textarea.value = message.toCopy; 30 | textarea.focus(); 31 | textarea.select(); 32 | document.execCommand('copy'); 33 | } 34 | }); 35 | 36 | chrome.extension.onRequest.addListener(function(request, sender, sendResponse) { 37 | if (request.method === 'getOptions') { 38 | sendResponse({ options: getOptions() }); 39 | } 40 | else { 41 | sendResponse({}); 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /tests/test3.html: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | ColumnCopy - Test 3 - Complex and dirty 14 | 15 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 |
DateIDNameAmount
15 April 2013Paid in 5 days98765Super clientUSD $29.99
1 April 2013Paid in 72 days12345A. ConglomerateUSD $29.99
1 January 2013Paid in 15 days38502 / Super client / USD $29.99
72 | 73 | 74 | -------------------------------------------------------------------------------- /tests/test2.html: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | ColumnCopy - Test 2 - Colspans 14 | 15 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
DateIDNameAmount
15 April 2013Paid in 5 days98765Super clientUSD $29.99
1 April 2013Paid in 72 days12345A. ConglomerateUSD $29.99
1 January 2013Paid in 15 days38502Super clientUSD $29.99
74 | 75 | 76 | -------------------------------------------------------------------------------- /tests/test8.html: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | ColumnCopy - Test 8 - Hyperlinks 14 | 15 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
DateIDNameAmount
15 April 2013Paid in 5 days98765Super clientUSD $29.99
1 April 2013Paid in 72 days12345A. ConglomerateUSD $29.99
1 January 2013Paid in 15 days38502Super clientUSD $29.99
74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /tests/test9.html: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | ColumnCopy - Test 10 - Quoting 14 | 15 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 74 | 75 | 76 |
DateIDNameAmount
15 April 2013Paid in 5 days98765Super clientUSD $29.99 57 | GBP £22.93
1 April 2013Paid in 72 days12345A. "Conglomerate"USD $29.99 65 | GBP £22.93
1 January 2013Paid in 15 days38502Super clientUSD $29.99 73 | GBP £22.93
77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/options/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ColumnCopy Options 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | ColumnCopy icon 17 |

ColumnCopy

18 | 19 |
20 |
21 | Keyboard shortcuts 22 | 23 |
24 | 25 | Click + 26 | 27 | 28 | Alt 29 | 30 |
31 | 32 |
33 | 34 | Click + 35 | 36 | Alt+Shift 37 | 38 |
39 |
40 | 41 |
42 | Options 43 | 44 |
45 | 46 | 47 | Copy raw text, not hyperlinks 48 |
49 | 50 | Create Excel style CSV hyperlinks, ie: =HYPERLINK("http://example.com", "Example") 51 |
52 |
53 | 54 |
55 | 56 | 57 | 58 |
59 |
60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /tests/test6.html: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | ColumnCopy - Test 6 - Inner HTML 14 | 15 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 |
DateIDNameAmount
15 April 201398765Super client
1 April 201312345A. Conglomerate
1 January 201338502Super client
81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /tests/test5.html: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | ColumnCopy - Test 3 - DOM modifications 15 | 16 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 78 | 79 | 80 |
DateIDNameAmount
15 April 2013Paid in 5 days98765Super clientUSD $29.99
1 April 2013Paid in 72 days12345A. ConglomerateUSD $29.99
1 January 2013Paid in 15 days38502Super clientUSD $29.99
75 | 76 | 77 |
81 | 82 | 83 | 84 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /tests/test7.html: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | ColumnCopy - Test 7 - Nested tables 14 | 15 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 118 | 119 | 120 | 121 |
DateIDClientAmount
15 April 2013Paid in 5 days98765 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 |
Super clientVancouver
72 |
USD $29.99
1 April 2013Paid in 72 days12345 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 |
A. ConglomerateLos Angeles
B. ConglomerateNew York
92 |
USD $29.99
1 January 2013Paid in 15 days38502 100 | 101 | 102 | 103 | 104 | 114 | 115 | 116 |
Super client 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 |
San FranciscoCalifornia
113 |
117 |
USD $29.99
122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /src/options/options.js: -------------------------------------------------------------------------------- 1 | jQuery(function ($) { 2 | 3 | var $focusedInput, 4 | options = getOptions(); 5 | 6 | 7 | // Initially load previous options 8 | if (options) { 9 | init(options); 10 | } 11 | 12 | 13 | // Fake input focusing 14 | $(document).on('click', function () { 15 | $focusedInput = null; 16 | $('.input').removeClass('focus'); 17 | }); 18 | 19 | $('.input').on('click', function (e) { 20 | $focusedInput = $(this); 21 | 22 | $('.input').removeClass('focus'); 23 | $focusedInput.addClass('focus'); 24 | 25 | e.stopPropagation(); 26 | }); 27 | 28 | 29 | // Hotkey -> focusedInput handling 30 | $(document).on('keyup', null, '', function (e) { 31 | 32 | }); 33 | 34 | 35 | $(document).on('keydown', null, '', function (e) { 36 | var possible = hotkeysHandler(e), 37 | hotkey; 38 | 39 | if (!$focusedInput) { 40 | return; 41 | } 42 | 43 | for (hotkey in possible) { 44 | if (possible.hasOwnProperty(hotkey) && possible[hotkey]) { 45 | saveOption($focusedInput.attr('id'), hotkey); 46 | $focusedInput.html(valueToKeyboardKeys(hotkey)); 47 | break; 48 | } 49 | }; 50 | }); 51 | 52 | 53 | $('input[name="hyperlinkMode"]').click(function () { 54 | $('input[name="hyperlinkMode"]').not(this).removeAttr('checked'); 55 | saveOption('hyperlinkMode', $(this).val()); 56 | }); 57 | 58 | 59 | $('#resetDefault').click(function (e) { 60 | if (confirm('Are you sure you want to reset to defaults?')) { 61 | var defaultOptions = getDefaultOptions(), i; 62 | 63 | for (i in defaultOptions) { 64 | if (defaultOptions.hasOwnProperty(i)) { 65 | saveOption(i, defaultOptions[i]); 66 | } 67 | } 68 | 69 | init(defaultOptions); 70 | 71 | $('body').trigger('click'); // Unfocus all inputs 72 | } 73 | 74 | return false; 75 | }); 76 | 77 | 78 | /** 79 | * Initialization 80 | */ 81 | function init(defaults) { 82 | var key; 83 | 84 | for (key in defaults) { 85 | if (defaults.hasOwnProperty(key)) { 86 | $('#' + key).html(valueToKeyboardKeys(defaults[key])); 87 | } 88 | } 89 | 90 | $('input[name="hyperlinkMode"]').removeAttr('checked'); 91 | $('#hyperlinkMode-' + defaults.hyperlinkMode).attr('checked', 'checked'); 92 | } 93 | 94 | /** 95 | * Creates keyboard keys markup for a hotkey value. 96 | */ 97 | function valueToKeyboardKeys(value) { 98 | var parts = value.split('+'), 99 | i; 100 | 101 | for (i = parts.length - 1; i >= 0; i--) { 102 | parts[i] = '' + toTitleCase(parts[i]) + ''; 103 | } 104 | 105 | return parts.join('+'); 106 | } 107 | 108 | /** 109 | * Saves a hotkey to localStorage. 110 | */ 111 | function saveOption(key, value) { 112 | options[key] = value; 113 | localStorage.options = JSON.stringify(options); 114 | } 115 | 116 | /** 117 | * Modified version of handleObj.handler from jquery.hotkeys.js. 118 | * 119 | * This version captures all keypresses and returns the combos. 120 | */ 121 | function hotkeysHandler(event) { 122 | var textAcceptingInputTypes = ["text", "password", "number", "email", "url", "range", "date", "month", "week", "time", "datetime", "datetime-local", "search", "color"]; 123 | 124 | // Don't fire in text-accepting inputs that we didn't directly bind to 125 | if (this !== event.target && (/textarea|select/i.test(event.target.nodeName) || 126 | jQuery.inArray(event.target.type, textAcceptingInputTypes) > -1)) { 127 | return; 128 | } 129 | 130 | // Keypress represents characters, not special keys 131 | var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[event.which], 132 | character = String.fromCharCode(event.which).toLowerCase(), 133 | key, modif = "", possible = {}; 134 | 135 | // check combinations (alt|ctrl|shift+anything) 136 | if (event.altKey && special !== "alt") { 137 | modif += "alt+"; 138 | } 139 | 140 | if (event.ctrlKey && special !== "ctrl") { 141 | modif += "ctrl+"; 142 | } 143 | 144 | // TODO: Need to make sure this works consistently across platforms 145 | if (event.metaKey && !event.ctrlKey && special !== "meta") { 146 | modif += "meta+"; 147 | } 148 | 149 | if (event.shiftKey && special !== "shift") { 150 | modif += "shift+"; 151 | } 152 | 153 | if (special) { 154 | possible[modif + special] = true; 155 | 156 | } else { 157 | possible[modif + character] = true; 158 | possible[modif + jQuery.hotkeys.shiftNums[character]] = true; 159 | 160 | // "$" can be triggered as "Shift+4" or "Shift+$" or just "$" 161 | if (modif === "shift+") { 162 | possible[jQuery.hotkeys.shiftNums[character]] = true; 163 | } 164 | } 165 | 166 | return possible; 167 | }; 168 | 169 | /** 170 | * See: http://stackoverflow.com/a/196991/806988 171 | */ 172 | function toTitleCase(str) { 173 | return str.replace(/\w\S*/g, function(txt) { 174 | return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); 175 | }); 176 | } 177 | }); 178 | -------------------------------------------------------------------------------- /tests/lib/jasmine-2.3.4/boot.js: -------------------------------------------------------------------------------- 1 | /** 2 | Starting with version 2.0, this file "boots" Jasmine, performing all of the necessary initialization before executing the loaded environment and all of a project's specs. This file should be loaded after `jasmine.js` and `jasmine_html.js`, but before any project source files or spec files are loaded. Thus this file can also be used to customize Jasmine for a project. 3 | 4 | If a project is using Jasmine via the standalone distribution, this file can be customized directly. If a project is using Jasmine via the [Ruby gem][jasmine-gem], this file can be copied into the support directory via `jasmine copy_boot_js`. Other environments (e.g., Python) will have different mechanisms. 5 | 6 | The location of `boot.js` can be specified and/or overridden in `jasmine.yml`. 7 | 8 | [jasmine-gem]: http://github.com/pivotal/jasmine-gem 9 | */ 10 | 11 | (function() { 12 | 13 | /** 14 | * ## Require & Instantiate 15 | * 16 | * Require Jasmine's core files. Specifically, this requires and attaches all of Jasmine's code to the `jasmine` reference. 17 | */ 18 | window.jasmine = jasmineRequire.core(jasmineRequire); 19 | 20 | /** 21 | * Since this is being run in a browser and the results should populate to an HTML page, require the HTML-specific Jasmine code, injecting the same reference. 22 | */ 23 | jasmineRequire.html(jasmine); 24 | 25 | /** 26 | * Create the Jasmine environment. This is used to run all specs in a project. 27 | */ 28 | var env = jasmine.getEnv(); 29 | 30 | /** 31 | * ## The Global Interface 32 | * 33 | * Build up the functions that will be exposed as the Jasmine public interface. A project can customize, rename or alias any of these functions as desired, provided the implementation remains unchanged. 34 | */ 35 | var jasmineInterface = jasmineRequire.interface(jasmine, env); 36 | 37 | /** 38 | * Add all of the Jasmine global/public interface to the global scope, so a project can use the public interface directly. For example, calling `describe` in specs instead of `jasmine.getEnv().describe`. 39 | */ 40 | extend(window, jasmineInterface); 41 | 42 | /** 43 | * ## Runner Parameters 44 | * 45 | * More browser specific code - wrap the query string in an object and to allow for getting/setting parameters from the runner user interface. 46 | */ 47 | 48 | var queryString = new jasmine.QueryString({ 49 | getWindowLocation: function() { return window.location; } 50 | }); 51 | 52 | var catchingExceptions = queryString.getParam("catch"); 53 | env.catchExceptions(typeof catchingExceptions === "undefined" ? true : catchingExceptions); 54 | 55 | var throwingExpectationFailures = queryString.getParam("throwFailures"); 56 | env.throwOnExpectationFailure(throwingExpectationFailures); 57 | 58 | /** 59 | * ## Reporters 60 | * The `HtmlReporter` builds all of the HTML UI for the runner page. This reporter paints the dots, stars, and x's for specs, as well as all spec names and all failures (if any). 61 | */ 62 | var htmlReporter = new jasmine.HtmlReporter({ 63 | env: env, 64 | onRaiseExceptionsClick: function() { queryString.navigateWithNewParam("catch", !env.catchingExceptions()); }, 65 | onThrowExpectationsClick: function() { queryString.navigateWithNewParam("throwFailures", !env.throwingExpectationFailures()); }, 66 | addToExistingQueryString: function(key, value) { return queryString.fullStringWithNewParam(key, value); }, 67 | getContainer: function() { return document.body; }, 68 | createElement: function() { return document.createElement.apply(document, arguments); }, 69 | createTextNode: function() { return document.createTextNode.apply(document, arguments); }, 70 | timer: new jasmine.Timer() 71 | }); 72 | 73 | /** 74 | * The `jsApiReporter` also receives spec results, and is used by any environment that needs to extract the results from JavaScript. 75 | */ 76 | env.addReporter(jasmineInterface.jsApiReporter); 77 | env.addReporter(htmlReporter); 78 | 79 | /** 80 | * Filter which specs will be run by matching the start of the full name against the `spec` query param. 81 | */ 82 | var specFilter = new jasmine.HtmlSpecFilter({ 83 | filterString: function() { return queryString.getParam("spec"); } 84 | }); 85 | 86 | env.specFilter = function(spec) { 87 | return specFilter.matches(spec.getFullName()); 88 | }; 89 | 90 | /** 91 | * Setting up timing functions to be able to be overridden. Certain browsers (Safari, IE 8, phantomjs) require this hack. 92 | */ 93 | window.setTimeout = window.setTimeout; 94 | window.setInterval = window.setInterval; 95 | window.clearTimeout = window.clearTimeout; 96 | window.clearInterval = window.clearInterval; 97 | 98 | /** 99 | * ## Execution 100 | * 101 | * Replace the browser window's `onload`, ensure it's called, and then run all of the loaded specs. This includes initializing the `HtmlReporter` instance and then executing the loaded Jasmine environment. All of this will happen after all of the specs are loaded. 102 | */ 103 | var currentWindowOnload = window.onload; 104 | 105 | window.onload = function() { 106 | if (currentWindowOnload) { 107 | currentWindowOnload(); 108 | } 109 | htmlReporter.initialize(); 110 | env.execute(); 111 | }; 112 | 113 | /** 114 | * Helper function for readability above. 115 | */ 116 | function extend(destination, source) { 117 | for (var property in source) destination[property] = source[property]; 118 | return destination; 119 | } 120 | 121 | }()); 122 | -------------------------------------------------------------------------------- /icons/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 2013-04-16 23:44ZCanvas 1Layer 1 4 | -------------------------------------------------------------------------------- /js/jquery.hotkeys/jquery.hotkeys.js: -------------------------------------------------------------------------------- 1 | /*jslint browser: true*/ 2 | /*jslint jquery: true*/ 3 | 4 | /* 5 | * jQuery Hotkeys Plugin 6 | * Copyright 2010, John Resig 7 | * Dual licensed under the MIT or GPL Version 2 licenses. 8 | * 9 | * Based upon the plugin by Tzury Bar Yochay: 10 | * https://github.com/tzuryby/jquery.hotkeys 11 | * 12 | * Original idea by: 13 | * Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/ 14 | */ 15 | 16 | /* 17 | * One small change is: now keys are passed by object { keys: '...' } 18 | * Might be useful, when you want to pass some other data to your handler 19 | */ 20 | 21 | (function(jQuery) { 22 | 23 | jQuery.hotkeys = { 24 | version: "0.2.0", 25 | 26 | specialKeys: { 27 | 8: "backspace", 28 | 9: "tab", 29 | 10: "return", 30 | 13: "return", 31 | 16: "shift", 32 | 17: "ctrl", 33 | 18: "alt", 34 | 19: "pause", 35 | 20: "capslock", 36 | 27: "esc", 37 | 32: "space", 38 | 33: "pageup", 39 | 34: "pagedown", 40 | 35: "end", 41 | 36: "home", 42 | 37: "left", 43 | 38: "up", 44 | 39: "right", 45 | 40: "down", 46 | 45: "insert", 47 | 46: "del", 48 | 59: ";", 49 | 61: "=", 50 | 96: "0", 51 | 97: "1", 52 | 98: "2", 53 | 99: "3", 54 | 100: "4", 55 | 101: "5", 56 | 102: "6", 57 | 103: "7", 58 | 104: "8", 59 | 105: "9", 60 | 106: "*", 61 | 107: "+", 62 | 109: "-", 63 | 110: ".", 64 | 111: "/", 65 | 112: "f1", 66 | 113: "f2", 67 | 114: "f3", 68 | 115: "f4", 69 | 116: "f5", 70 | 117: "f6", 71 | 118: "f7", 72 | 119: "f8", 73 | 120: "f9", 74 | 121: "f10", 75 | 122: "f11", 76 | 123: "f12", 77 | 144: "numlock", 78 | 145: "scroll", 79 | 173: "-", 80 | 186: ";", 81 | 187: "=", 82 | 188: ",", 83 | 189: "-", 84 | 190: ".", 85 | 191: "/", 86 | 192: "`", 87 | 219: "[", 88 | 220: "\\", 89 | 221: "]", 90 | 222: "'" 91 | }, 92 | 93 | shiftNums: { 94 | "`": "~", 95 | "1": "!", 96 | "2": "@", 97 | "3": "#", 98 | "4": "$", 99 | "5": "%", 100 | "6": "^", 101 | "7": "&", 102 | "8": "*", 103 | "9": "(", 104 | "0": ")", 105 | "-": "_", 106 | "=": "+", 107 | ";": ": ", 108 | "'": "\"", 109 | ",": "<", 110 | ".": ">", 111 | "/": "?", 112 | "\\": "|" 113 | }, 114 | 115 | // excludes: button, checkbox, file, hidden, image, password, radio, reset, search, submit, url 116 | textAcceptingInputTypes: [ 117 | "text", "password", "number", "email", "url", "range", "date", "month", "week", "time", "datetime", 118 | "datetime-local", "search", "color", "tel"], 119 | 120 | // default input types not to bind to unless bound directly 121 | textInputTypes: /textarea|input|select/i, 122 | 123 | options: { 124 | filterInputAcceptingElements: true, 125 | filterTextInputs: true, 126 | filterContentEditable: true 127 | } 128 | }; 129 | 130 | function keyHandler(handleObj) { 131 | if (typeof handleObj.data === "string") { 132 | handleObj.data = { 133 | keys: handleObj.data 134 | }; 135 | } 136 | 137 | // Only care when a possible input has been specified 138 | if (!handleObj.data || !handleObj.data.keys || typeof handleObj.data.keys !== "string") { 139 | return; 140 | } 141 | 142 | var origHandler = handleObj.handler, 143 | keys = handleObj.data.keys.toLowerCase().split(" "); 144 | 145 | handleObj.handler = function(event) { 146 | // Don't fire in text-accepting inputs that we didn't directly bind to 147 | if (this !== event.target && 148 | (jQuery.hotkeys.options.filterInputAcceptingElements && 149 | jQuery.hotkeys.textInputTypes.test(event.target.nodeName) || 150 | (jQuery.hotkeys.options.filterContentEditable && jQuery(event.target).attr('contenteditable')) || 151 | (jQuery.hotkeys.options.filterTextInputs && 152 | jQuery.inArray(event.target.type, jQuery.hotkeys.textAcceptingInputTypes) > -1))) { 153 | return; 154 | } 155 | 156 | var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[event.which], 157 | character = String.fromCharCode(event.which).toLowerCase(), 158 | modif = "", 159 | possible = {}; 160 | 161 | jQuery.each(["alt", "ctrl", "shift"], function(index, specialKey) { 162 | 163 | if (event[specialKey + 'Key'] && special !== specialKey) { 164 | modif += specialKey + '+'; 165 | } 166 | }); 167 | 168 | // metaKey is triggered off ctrlKey erronously 169 | if (event.metaKey && !event.ctrlKey && special !== "meta") { 170 | modif += "meta+"; 171 | } 172 | 173 | if (event.metaKey && special !== "meta" && modif.indexOf("alt+ctrl+shift+") > -1) { 174 | modif = modif.replace("alt+ctrl+shift+", "hyper+"); 175 | } 176 | 177 | if (special) { 178 | possible[modif + special] = true; 179 | } 180 | else { 181 | possible[modif + character] = true; 182 | possible[modif + jQuery.hotkeys.shiftNums[character]] = true; 183 | 184 | // "$" can be triggered as "Shift+4" or "Shift+$" or just "$" 185 | if (modif === "shift+") { 186 | possible[jQuery.hotkeys.shiftNums[character]] = true; 187 | } 188 | } 189 | 190 | for (var i = 0, l = keys.length; i < l; i++) { 191 | if (possible[keys[i]]) { 192 | return origHandler.apply(this, arguments); 193 | } 194 | } 195 | }; 196 | } 197 | 198 | jQuery.each(["keydown", "keyup", "keypress"], function() { 199 | jQuery.event.special[this] = { 200 | add: keyHandler 201 | }; 202 | }); 203 | 204 | })(jQuery || this.jQuery || window.jQuery); 205 | -------------------------------------------------------------------------------- /tests/lib/jasmine-2.3.4/console.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2008-2015 Pivotal Labs 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | */ 23 | function getJasmineRequireObj() { 24 | if (typeof module !== 'undefined' && module.exports) { 25 | return exports; 26 | } else { 27 | window.jasmineRequire = window.jasmineRequire || {}; 28 | return window.jasmineRequire; 29 | } 30 | } 31 | 32 | getJasmineRequireObj().console = function(jRequire, j$) { 33 | j$.ConsoleReporter = jRequire.ConsoleReporter(); 34 | }; 35 | 36 | getJasmineRequireObj().ConsoleReporter = function() { 37 | 38 | var noopTimer = { 39 | start: function(){}, 40 | elapsed: function(){ return 0; } 41 | }; 42 | 43 | function ConsoleReporter(options) { 44 | var print = options.print, 45 | showColors = options.showColors || false, 46 | onComplete = options.onComplete || function() {}, 47 | timer = options.timer || noopTimer, 48 | specCount, 49 | failureCount, 50 | failedSpecs = [], 51 | pendingCount, 52 | ansi = { 53 | green: '\x1B[32m', 54 | red: '\x1B[31m', 55 | yellow: '\x1B[33m', 56 | none: '\x1B[0m' 57 | }, 58 | failedSuites = []; 59 | 60 | print('ConsoleReporter is deprecated and will be removed in a future version.'); 61 | 62 | this.jasmineStarted = function() { 63 | specCount = 0; 64 | failureCount = 0; 65 | pendingCount = 0; 66 | print('Started'); 67 | printNewline(); 68 | timer.start(); 69 | }; 70 | 71 | this.jasmineDone = function() { 72 | printNewline(); 73 | for (var i = 0; i < failedSpecs.length; i++) { 74 | specFailureDetails(failedSpecs[i]); 75 | } 76 | 77 | if(specCount > 0) { 78 | printNewline(); 79 | 80 | var specCounts = specCount + ' ' + plural('spec', specCount) + ', ' + 81 | failureCount + ' ' + plural('failure', failureCount); 82 | 83 | if (pendingCount) { 84 | specCounts += ', ' + pendingCount + ' pending ' + plural('spec', pendingCount); 85 | } 86 | 87 | print(specCounts); 88 | } else { 89 | print('No specs found'); 90 | } 91 | 92 | printNewline(); 93 | var seconds = timer.elapsed() / 1000; 94 | print('Finished in ' + seconds + ' ' + plural('second', seconds)); 95 | printNewline(); 96 | 97 | for(i = 0; i < failedSuites.length; i++) { 98 | suiteFailureDetails(failedSuites[i]); 99 | } 100 | 101 | onComplete(failureCount === 0); 102 | }; 103 | 104 | this.specDone = function(result) { 105 | specCount++; 106 | 107 | if (result.status == 'pending') { 108 | pendingCount++; 109 | print(colored('yellow', '*')); 110 | return; 111 | } 112 | 113 | if (result.status == 'passed') { 114 | print(colored('green', '.')); 115 | return; 116 | } 117 | 118 | if (result.status == 'failed') { 119 | failureCount++; 120 | failedSpecs.push(result); 121 | print(colored('red', 'F')); 122 | } 123 | }; 124 | 125 | this.suiteDone = function(result) { 126 | if (result.failedExpectations && result.failedExpectations.length > 0) { 127 | failureCount++; 128 | failedSuites.push(result); 129 | } 130 | }; 131 | 132 | return this; 133 | 134 | function printNewline() { 135 | print('\n'); 136 | } 137 | 138 | function colored(color, str) { 139 | return showColors ? (ansi[color] + str + ansi.none) : str; 140 | } 141 | 142 | function plural(str, count) { 143 | return count == 1 ? str : str + 's'; 144 | } 145 | 146 | function repeat(thing, times) { 147 | var arr = []; 148 | for (var i = 0; i < times; i++) { 149 | arr.push(thing); 150 | } 151 | return arr; 152 | } 153 | 154 | function indent(str, spaces) { 155 | var lines = (str || '').split('\n'); 156 | var newArr = []; 157 | for (var i = 0; i < lines.length; i++) { 158 | newArr.push(repeat(' ', spaces).join('') + lines[i]); 159 | } 160 | return newArr.join('\n'); 161 | } 162 | 163 | function specFailureDetails(result) { 164 | printNewline(); 165 | print(result.fullName); 166 | 167 | for (var i = 0; i < result.failedExpectations.length; i++) { 168 | var failedExpectation = result.failedExpectations[i]; 169 | printNewline(); 170 | print(indent(failedExpectation.message, 2)); 171 | print(indent(failedExpectation.stack, 2)); 172 | } 173 | 174 | printNewline(); 175 | } 176 | 177 | function suiteFailureDetails(result) { 178 | for (var i = 0; i < result.failedExpectations.length; i++) { 179 | printNewline(); 180 | print(colored('red', 'An error was thrown in an afterAll')); 181 | printNewline(); 182 | print(colored('red', 'AfterAll ' + result.failedExpectations[i].message)); 183 | 184 | } 185 | printNewline(); 186 | } 187 | } 188 | 189 | return ConsoleReporter; 190 | }; 191 | -------------------------------------------------------------------------------- /src/inject/inject.js: -------------------------------------------------------------------------------- 1 | /*jslint browser: true, nomen: true, plusplus: true, todo: true, white: true, indent: 2 */ 2 | (function (window, document, $) { 3 | 'use strict'; 4 | 5 | /** 6 | * The ColumnCopy function object. 7 | */ 8 | function ColumnCopy() { 9 | var that = this; 10 | 11 | chrome.extension.sendRequest({ method: 'getOptions' }, function(response) { 12 | that.options = $.extend({}, response.options); 13 | that.init(); 14 | }); 15 | } 16 | 17 | ColumnCopy.prototype.init = function () { 18 | this.bindHandlers(); 19 | }; 20 | 21 | ColumnCopy.prototype.bindHandlers = function () { 22 | var that = this; 23 | 24 | // Reset all hotkeys on key release 25 | this.activeHotkeysReset(); 26 | $(document).on('keyup', null, '', function () { that.activeHotkeysReset(); }); 27 | 28 | // Set the column hotkey when its combo is pressed. Same for table hotkey. 29 | $(document).on('keydown', null, this.options.columnHotkey, function () { that.activeHotkey.column = true; }); 30 | $(document).on('keydown', null, this.options.tableHotkey, function () { that.activeHotkey.table = true; }); 31 | 32 | // Finally, execute the routine on click 33 | $(document).on('click', 'th,td', function (e) { that.handleCellClick(e, this); }); 34 | 35 | // Rube Goldberg routine to determine which element was under the cursor when 36 | // the context menu was invoked 37 | this.activeCellReset(); 38 | $(document).on('contextmenu', null, function (e) { 39 | var cellParent; 40 | 41 | that.activeCellReset(); 42 | 43 | if (['TH', 'TD'].indexOf(e.target.tagName) >= 0) { 44 | that.activeCell = e.target; 45 | } else { 46 | cellParent = $(e.target).parents('th,td:first'); 47 | 48 | if (cellParent.length > 0) { 49 | that.activeCell = cellParent[0]; 50 | } 51 | } 52 | }); 53 | 54 | chrome.runtime.onMessage.addListener(function(request) { 55 | if (typeof request === 'object' && request.hasOwnProperty('columnCopyContextMenuClick')) { 56 | if (that.activeCell !== null) { 57 | that.handleContextMenuClick(request.columnCopyContextMenuClick, that.activeCell); 58 | } 59 | } 60 | }); 61 | }; 62 | 63 | ColumnCopy.prototype.activeCellReset = function () { 64 | this.activeCell = null; 65 | }; 66 | 67 | ColumnCopy.prototype.activeHotkeysReset = function () { 68 | this.activeHotkey = { 69 | 'column': false, 70 | 'table': false 71 | }; 72 | }; 73 | 74 | ColumnCopy.prototype.handleContextMenuClick = function (type, cell) { 75 | var $table = $(cell).parents('table:first'); 76 | 77 | switch (type) { 78 | case 'copyColumn': 79 | this.buildColspanMap($table); 80 | this.copyColumnContainingCell(cell, $table); 81 | break; 82 | 83 | case 'copyTable': 84 | this.copyTableContainingCell(cell, $table); 85 | break; 86 | } 87 | }; 88 | 89 | // TODO: Different key map for $.client.os === 'Windows'? 90 | ColumnCopy.prototype.handleCellClick = function (e, cell) { 91 | var $table = $(cell).parents('table:first'); 92 | 93 | if (this.activeHotkey.table) { 94 | this.copyTableContainingCell(cell, $table); 95 | 96 | // Need to stop bubbling here to support nested tables. For example, 97 | // consider the case: table > tr > td > table > tr > td{*ColumnCopy here*}. 98 | e.stopPropagation(); 99 | } else if (this.activeHotkey.column) { 100 | this.buildColspanMap($table); 101 | this.copyColumnContainingCell(cell, $table); 102 | 103 | e.stopPropagation(); 104 | } 105 | }; 106 | 107 | ColumnCopy.prototype.copyTableContainingCell = function (cell, $table) { 108 | if ($table) { 109 | this.copiedToClipboardAnimation($table); 110 | this.copyValuesToClipboard(this.getValuesForTable($table)); 111 | } 112 | }; 113 | 114 | ColumnCopy.prototype.copyColumnContainingCell = function (cell, $table) { 115 | var data = this.getColumnContainingCell(cell, $table); 116 | 117 | if (data && data.column && data.values) { 118 | this.copiedToClipboardAnimation(data.column); 119 | this.copyValuesToClipboard(data.values); 120 | } 121 | }; 122 | 123 | ColumnCopy.prototype.getColumnContainingCell = function (cell, $table) { 124 | var that = this, 125 | $cell = $(cell), 126 | // The column span map for this cell 127 | cellMap = $cell.data('_ColumnCopy') || [], 128 | column = [], 129 | values = [], 130 | row; 131 | 132 | // Unknown error, cell not found in row, cell is not inside a row, or similar. 133 | if (!cellMap || cellMap.length <= 0) { 134 | return false; 135 | } 136 | 137 | $('>tr, >thead>tr, >tbody>tr', $table).each(function () { 138 | row = []; 139 | 140 | $('>td, >th', this).each(function () { 141 | var $this = $(this), 142 | map = $this.data('_ColumnCopy'), 143 | i; 144 | 145 | for (i = map.length - 1; i >= 0; i--) { 146 | if (cellMap.indexOf(map[i]) !== -1) { 147 | row.push(that.wrapCell(that.getCellText($this[0]).trim())); 148 | column.push(this); 149 | break; 150 | } 151 | } 152 | }); 153 | 154 | values.push(row.join(that.options.columnSeparator)); 155 | }); 156 | 157 | return { column: $(column), values: values }; 158 | }; 159 | 160 | ColumnCopy.prototype.buildColspanMap = function ($table) { 161 | var column; 162 | 163 | $('>tr, >thead>tr, >tbody>tr', $table).each(function () { 164 | column = 0; 165 | 166 | $('>td, >th', this).each(function () { 167 | var $this = $(this), 168 | cs = $this.attr('colspan') || 1, 169 | map = [], 170 | i; 171 | 172 | for (i = 0; i < cs; i++) { 173 | map.push(column); 174 | column += 1; 175 | } 176 | 177 | $(this).data('_ColumnCopy', map); 178 | }); 179 | }); 180 | }; 181 | 182 | ColumnCopy.prototype.getValuesForTable = function ($table) { 183 | var that = this, 184 | values = [], 185 | row; 186 | 187 | $('>tr, >thead>tr, >tbody>tr', $table).each(function () { 188 | row = []; 189 | 190 | $('>td, >th', this).each(function () { 191 | row.push(that.wrapCell(that.getCellText(this).trim())); 192 | }); 193 | 194 | values.push(row.join(that.options.columnSeparator)); 195 | }); 196 | 197 | return values; 198 | }; 199 | 200 | /** 201 | * An similar function to jQuery.text(). This recursively digs through 202 | * children of a DOM node and retrieves all text nodes and values of relevant 203 | * form elements. 204 | * 205 | * Original concept by James Padolsey. 206 | * See: http://james.padolsey.com/javascript/replacing-text-in-the-dom-its-not-that-simple/ 207 | */ 208 | ColumnCopy.prototype.getCellText = function (cell) { 209 | var next, suffix, href, result = []; 210 | 211 | if (cell.nodeType === 1) { // Element node 212 | if (cell.nodeName === 'INPUT') { 213 | switch (cell.type) { 214 | case 'button': 215 | case 'checkbox': 216 | case 'file': 217 | case 'hidden': 218 | case 'image': 219 | case 'password': 220 | case 'radio': 221 | case 'range': 222 | case 'reset': 223 | case 'search': 224 | case 'submit': 225 | // Skip these input types, note that TEXTAREA contents are a text node 226 | // and will be captured by the recursion. 227 | break; 228 | default: 229 | result.push(cell.value.trim()); 230 | break; 231 | } 232 | } 233 | else if (this.options.hyperlinkMode === 'excel' && cell.nodeName === 'A') { 234 | href = cell.getAttribute('href').trim(); 235 | 236 | if (href) { 237 | result.push('=HYPERLINK("' + href + '","'); 238 | suffix = '")'; 239 | } 240 | } 241 | 242 | if (cell = cell.firstChild) { 243 | do { 244 | next = cell.nextSibling; 245 | result.push(this.getCellText(cell).trim()); 246 | } while(cell = next); 247 | } 248 | 249 | if (suffix) { 250 | result.push(suffix); 251 | } 252 | } else if (cell.nodeType === 3) { // Text node 253 | return cell.data.trim(); 254 | } 255 | 256 | return result.join(' '); 257 | }; 258 | 259 | ColumnCopy.prototype.wrapCell = function (cellText) { 260 | var cellWrapper, rowSeparator, escapedCellText; 261 | cellWrapper = this.options.cellWrapper; 262 | rowSeparator = this.options.rowSeparator; 263 | 264 | if (cellText.indexOf(cellWrapper) === -1 && cellText.indexOf(rowSeparator) === -1) { 265 | return cellText; 266 | } 267 | 268 | function escapeRegExp(str) { 269 | return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); 270 | } 271 | 272 | escapedCellText = cellText.replace(RegExp(escapeRegExp(cellWrapper), 'g'), '\\' + cellWrapper); 273 | return cellWrapper + escapedCellText + cellWrapper; 274 | }; 275 | 276 | ColumnCopy.prototype.copiedToClipboardAnimation = function ($column) { 277 | $column.addClass('CC-animated CC-copiedToClipboard'); 278 | 279 | setTimeout(function () { 280 | $column.removeClass('CC-animated'); 281 | $column.removeClass('CC-copiedToClipboard'); 282 | }, 1000); 283 | }; 284 | 285 | ColumnCopy.prototype.copyValuesToClipboard = function (values) { 286 | // Ping the background.html page, this is where the clipboard 287 | // communication happens 288 | // See: http://stackoverflow.com/a/8807145/806988 289 | chrome.extension.sendMessage({ toCopy: values.join(this.options.rowSeparator) }); 290 | }; 291 | 292 | // Expose for testing 293 | window.ColumnCopy = ColumnCopy; 294 | 295 | var _ColumnCopy = new ColumnCopy(); 296 | }(window, document, jQuery)); 297 | -------------------------------------------------------------------------------- /src/options/options.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | 5 | font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; 6 | } 7 | 8 | #container { 9 | min-width: 640px; 10 | width: 60%; 11 | margin: 4em auto; 12 | } 13 | 14 | #icon { 15 | float: left; 16 | margin-right: 1em; 17 | height: 40px; 18 | } 19 | 20 | fieldset { 21 | border: none; 22 | border-radius: 4px; 23 | background-color: #fff 24 | } 25 | 26 | .form-item { 27 | margin-bottom: 2em; 28 | } 29 | 30 | .input { 31 | display: inline-block; 32 | height: 40px; 33 | min-width: 400px; 34 | line-height: 40px; 35 | padding: 4px 12px; 36 | border: 1px solid #ccc; 37 | border-radius: 4px; 38 | box-shadow: inset 0 1px 1px rgba(0,0,0,0.075); 39 | transition: border linear .2s,box-shadow linear .2s; 40 | 41 | background-color: #fff; 42 | color: #555; 43 | } 44 | 45 | .input.focus { 46 | border-color: rgba(82,168,236,0.8); 47 | outline: 0; 48 | outline: thin dotted 9; 49 | box-shadow: inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6); 50 | } 51 | 52 | .key,.sep { 53 | display: inline-block; 54 | height: 16px; 55 | line-height: 16px; 56 | padding: 10px; 57 | } 58 | 59 | .key { 60 | border: 1px solid #eee; 61 | border-radius: 4px; 62 | box-shadow: 3px 3px 1px rgba(0,0,0,0.4); 63 | } 64 | 65 | 66 | /* 67 | * These styles from Twitter Bootstrap. 68 | */ 69 | h1 { 70 | font-size: 38.5px; 71 | line-height: 40px; 72 | margin: 10px 0; 73 | font-family: inherit; 74 | font-weight: bold; 75 | color: inherit; 76 | text-rendering: optimizelegibility; 77 | } 78 | 79 | .form-item { 80 | margin-bottom: 2em; 81 | } 82 | 83 | legend { 84 | display: block; 85 | font-weight: bold; 86 | font-size: 24px; 87 | line-height: 40px; 88 | margin-bottom: 0.5em; 89 | } 90 | 91 | label { 92 | display: block; 93 | font-weight: bold; 94 | margin-bottom: 0.5em; 95 | } 96 | 97 | button, { 98 | margin: 0; 99 | font-size: 100%; 100 | vertical-align: middle; 101 | } 102 | 103 | button { 104 | *overflow: visible; 105 | line-height: normal; 106 | } 107 | 108 | button { 109 | cursor: pointer; 110 | -webkit-appearance: button; 111 | } 112 | 113 | label, 114 | button { 115 | cursor: pointer; 116 | } 117 | 118 | button { 119 | font-size: 14px; 120 | font-weight: normal; 121 | line-height: 20px; 122 | } 123 | 124 | button { 125 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 126 | } 127 | 128 | .btn { 129 | display: inline-block; 130 | *display: inline; 131 | padding: 4px 12px; 132 | margin-bottom: 0; 133 | *margin-left: .3em; 134 | font-size: 14px; 135 | line-height: 20px; 136 | color: #333333; 137 | text-align: center; 138 | text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); 139 | vertical-align: middle; 140 | cursor: pointer; 141 | background-color: #f5f5f5; 142 | *background-color: #e6e6e6; 143 | background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); 144 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); 145 | background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); 146 | background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); 147 | background-image: linear-gradient(to bottom, #ffffff, #e6e6e6); 148 | background-repeat: repeat-x; 149 | border: 1px solid #cccccc; 150 | *border: 0; 151 | border-color: #e6e6e6 #e6e6e6 #bfbfbf; 152 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 153 | border-bottom-color: #b3b3b3; 154 | -webkit-border-radius: 4px; 155 | -moz-border-radius: 4px; 156 | border-radius: 4px; 157 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe6e6e6', GradientType=0); 158 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); 159 | *zoom: 1; 160 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); 161 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); 162 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); 163 | } 164 | 165 | .btn:hover, 166 | .btn:focus, 167 | .btn:active, 168 | .btn.active, 169 | .btn.disabled, 170 | .btn[disabled] { 171 | color: #333333; 172 | background-color: #e6e6e6; 173 | *background-color: #d9d9d9; 174 | } 175 | 176 | .btn:active, 177 | .btn.active { 178 | background-color: #cccccc \9; 179 | } 180 | 181 | .btn:first-child { 182 | *margin-left: 0; 183 | } 184 | 185 | .btn:hover, 186 | .btn:focus { 187 | color: #333333; 188 | text-decoration: none; 189 | background-position: 0 -15px; 190 | -webkit-transition: background-position 0.1s linear; 191 | -moz-transition: background-position 0.1s linear; 192 | -o-transition: background-position 0.1s linear; 193 | transition: background-position 0.1s linear; 194 | } 195 | 196 | .btn:focus { 197 | outline: thin dotted #333; 198 | outline: 5px auto -webkit-focus-ring-color; 199 | outline-offset: -2px; 200 | } 201 | 202 | .btn.active, 203 | .btn:active { 204 | background-image: none; 205 | outline: 0; 206 | -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); 207 | -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); 208 | box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); 209 | } 210 | 211 | .btn.disabled, 212 | .btn[disabled] { 213 | cursor: default; 214 | background-image: none; 215 | opacity: 0.65; 216 | filter: alpha(opacity=65); 217 | -webkit-box-shadow: none; 218 | -moz-box-shadow: none; 219 | box-shadow: none; 220 | } 221 | 222 | .btn-primary.active, 223 | .btn-warning.active, 224 | .btn-danger.active, 225 | .btn-success.active, 226 | .btn-info.active, 227 | .btn-inverse.active { 228 | color: rgba(255, 255, 255, 0.75); 229 | } 230 | 231 | .btn-primary { 232 | color: #ffffff; 233 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 234 | background-color: #006dcc; 235 | *background-color: #0044cc; 236 | background-image: -moz-linear-gradient(top, #0088cc, #0044cc); 237 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); 238 | background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); 239 | background-image: -o-linear-gradient(top, #0088cc, #0044cc); 240 | background-image: linear-gradient(to bottom, #0088cc, #0044cc); 241 | background-repeat: repeat-x; 242 | border-color: #0044cc #0044cc #002a80; 243 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 244 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0044cc', GradientType=0); 245 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); 246 | } 247 | 248 | .btn-primary:hover, 249 | .btn-primary:focus, 250 | .btn-primary:active, 251 | .btn-primary.active, 252 | .btn-primary.disabled, 253 | .btn-primary[disabled] { 254 | color: #ffffff; 255 | background-color: #0044cc; 256 | *background-color: #003bb3; 257 | } 258 | 259 | .btn-primary:active, 260 | .btn-primary.active { 261 | background-color: #003399 \9; 262 | } 263 | 264 | .btn-warning { 265 | color: #ffffff; 266 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 267 | background-color: #faa732; 268 | *background-color: #f89406; 269 | background-image: -moz-linear-gradient(top, #fbb450, #f89406); 270 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); 271 | background-image: -webkit-linear-gradient(top, #fbb450, #f89406); 272 | background-image: -o-linear-gradient(top, #fbb450, #f89406); 273 | background-image: linear-gradient(to bottom, #fbb450, #f89406); 274 | background-repeat: repeat-x; 275 | border-color: #f89406 #f89406 #ad6704; 276 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 277 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0); 278 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); 279 | } 280 | 281 | .btn-warning:hover, 282 | .btn-warning:focus, 283 | .btn-warning:active, 284 | .btn-warning.active, 285 | .btn-warning.disabled, 286 | .btn-warning[disabled] { 287 | color: #ffffff; 288 | background-color: #f89406; 289 | *background-color: #df8505; 290 | } 291 | 292 | .btn-warning:active, 293 | .btn-warning.active { 294 | background-color: #c67605 \9; 295 | } 296 | 297 | .btn-danger { 298 | color: #ffffff; 299 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 300 | background-color: #da4f49; 301 | *background-color: #bd362f; 302 | background-image: -moz-linear-gradient(top, #ee5f5b, #bd362f); 303 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f)); 304 | background-image: -webkit-linear-gradient(top, #ee5f5b, #bd362f); 305 | background-image: -o-linear-gradient(top, #ee5f5b, #bd362f); 306 | background-image: linear-gradient(to bottom, #ee5f5b, #bd362f); 307 | background-repeat: repeat-x; 308 | border-color: #bd362f #bd362f #802420; 309 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 310 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffbd362f', GradientType=0); 311 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); 312 | } 313 | 314 | .btn-danger:hover, 315 | .btn-danger:focus, 316 | .btn-danger:active, 317 | .btn-danger.active, 318 | .btn-danger.disabled, 319 | .btn-danger[disabled] { 320 | color: #ffffff; 321 | background-color: #bd362f; 322 | *background-color: #a9302a; 323 | } 324 | 325 | .btn-danger:active, 326 | .btn-danger.active { 327 | background-color: #942a25 \9; 328 | } 329 | 330 | .btn-success { 331 | color: #ffffff; 332 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 333 | background-color: #5bb75b; 334 | *background-color: #51a351; 335 | background-image: -moz-linear-gradient(top, #62c462, #51a351); 336 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351)); 337 | background-image: -webkit-linear-gradient(top, #62c462, #51a351); 338 | background-image: -o-linear-gradient(top, #62c462, #51a351); 339 | background-image: linear-gradient(to bottom, #62c462, #51a351); 340 | background-repeat: repeat-x; 341 | border-color: #51a351 #51a351 #387038; 342 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 343 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff51a351', GradientType=0); 344 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); 345 | } 346 | 347 | .btn-success:hover, 348 | .btn-success:focus, 349 | .btn-success:active, 350 | .btn-success.active, 351 | .btn-success.disabled, 352 | .btn-success[disabled] { 353 | color: #ffffff; 354 | background-color: #51a351; 355 | *background-color: #499249; 356 | } 357 | 358 | .btn-success:active, 359 | .btn-success.active { 360 | background-color: #408140 \9; 361 | } 362 | 363 | .btn-info { 364 | color: #ffffff; 365 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 366 | background-color: #49afcd; 367 | *background-color: #2f96b4; 368 | background-image: -moz-linear-gradient(top, #5bc0de, #2f96b4); 369 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4)); 370 | background-image: -webkit-linear-gradient(top, #5bc0de, #2f96b4); 371 | background-image: -o-linear-gradient(top, #5bc0de, #2f96b4); 372 | background-image: linear-gradient(to bottom, #5bc0de, #2f96b4); 373 | background-repeat: repeat-x; 374 | border-color: #2f96b4 #2f96b4 #1f6377; 375 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 376 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2f96b4', GradientType=0); 377 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); 378 | } 379 | 380 | .btn-info:hover, 381 | .btn-info:focus, 382 | .btn-info:active, 383 | .btn-info.active, 384 | .btn-info.disabled, 385 | .btn-info[disabled] { 386 | color: #ffffff; 387 | background-color: #2f96b4; 388 | *background-color: #2a85a0; 389 | } 390 | 391 | .btn-info:active, 392 | .btn-info.active { 393 | background-color: #24748c \9; 394 | } 395 | 396 | .btn-inverse { 397 | color: #ffffff; 398 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 399 | background-color: #363636; 400 | *background-color: #222222; 401 | background-image: -moz-linear-gradient(top, #444444, #222222); 402 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#444444), to(#222222)); 403 | background-image: -webkit-linear-gradient(top, #444444, #222222); 404 | background-image: -o-linear-gradient(top, #444444, #222222); 405 | background-image: linear-gradient(to bottom, #444444, #222222); 406 | background-repeat: repeat-x; 407 | border-color: #222222 #222222 #000000; 408 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 409 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444', endColorstr='#ff222222', GradientType=0); 410 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); 411 | } 412 | 413 | .btn-inverse:hover, 414 | .btn-inverse:focus, 415 | .btn-inverse:active, 416 | .btn-inverse.active, 417 | .btn-inverse.disabled, 418 | .btn-inverse[disabled] { 419 | color: #ffffff; 420 | background-color: #222222; 421 | *background-color: #151515; 422 | } 423 | 424 | .btn-inverse:active, 425 | .btn-inverse.active { 426 | background-color: #080808 \9; 427 | } 428 | 429 | button.btn, 430 | input[type="submit"].btn { 431 | *padding-top: 3px; 432 | *padding-bottom: 3px; 433 | } 434 | 435 | button.btn::-moz-focus-inner, 436 | input[type="submit"].btn::-moz-focus-inner { 437 | padding: 0; 438 | border: 0; 439 | } 440 | 441 | button.btn.btn-large, 442 | input[type="submit"].btn.btn-large { 443 | *padding-top: 7px; 444 | *padding-bottom: 7px; 445 | } 446 | 447 | button.btn.btn-small, 448 | input[type="submit"].btn.btn-small { 449 | *padding-top: 3px; 450 | *padding-bottom: 3px; 451 | } 452 | 453 | button.btn.btn-mini, 454 | input[type="submit"].btn.btn-mini { 455 | *padding-top: 1px; 456 | *padding-bottom: 1px; 457 | } 458 | -------------------------------------------------------------------------------- /tests/lib/jasmine-2.3.4/jasmine-html.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2008-2015 Pivotal Labs 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | */ 23 | jasmineRequire.html = function(j$) { 24 | j$.ResultsNode = jasmineRequire.ResultsNode(); 25 | j$.HtmlReporter = jasmineRequire.HtmlReporter(j$); 26 | j$.QueryString = jasmineRequire.QueryString(); 27 | j$.HtmlSpecFilter = jasmineRequire.HtmlSpecFilter(); 28 | }; 29 | 30 | jasmineRequire.HtmlReporter = function(j$) { 31 | 32 | var noopTimer = { 33 | start: function() {}, 34 | elapsed: function() { return 0; } 35 | }; 36 | 37 | function HtmlReporter(options) { 38 | var env = options.env || {}, 39 | getContainer = options.getContainer, 40 | createElement = options.createElement, 41 | createTextNode = options.createTextNode, 42 | onRaiseExceptionsClick = options.onRaiseExceptionsClick || function() {}, 43 | onThrowExpectationsClick = options.onThrowExpectationsClick || function() {}, 44 | addToExistingQueryString = options.addToExistingQueryString || defaultQueryString, 45 | timer = options.timer || noopTimer, 46 | results = [], 47 | specsExecuted = 0, 48 | failureCount = 0, 49 | pendingSpecCount = 0, 50 | htmlReporterMain, 51 | symbols, 52 | failedSuites = []; 53 | 54 | this.initialize = function() { 55 | clearPrior(); 56 | htmlReporterMain = createDom('div', {className: 'jasmine_html-reporter'}, 57 | createDom('div', {className: 'banner'}, 58 | createDom('a', {className: 'title', href: 'http://jasmine.github.io/', target: '_blank'}), 59 | createDom('span', {className: 'version'}, j$.version) 60 | ), 61 | createDom('ul', {className: 'symbol-summary'}), 62 | createDom('div', {className: 'alert'}), 63 | createDom('div', {className: 'results'}, 64 | createDom('div', {className: 'failures'}) 65 | ) 66 | ); 67 | getContainer().appendChild(htmlReporterMain); 68 | 69 | symbols = find('.symbol-summary'); 70 | }; 71 | 72 | var totalSpecsDefined; 73 | this.jasmineStarted = function(options) { 74 | totalSpecsDefined = options.totalSpecsDefined || 0; 75 | timer.start(); 76 | }; 77 | 78 | var summary = createDom('div', {className: 'summary'}); 79 | 80 | var topResults = new j$.ResultsNode({}, '', null), 81 | currentParent = topResults; 82 | 83 | this.suiteStarted = function(result) { 84 | currentParent.addChild(result, 'suite'); 85 | currentParent = currentParent.last(); 86 | }; 87 | 88 | this.suiteDone = function(result) { 89 | if (result.status == 'failed') { 90 | failedSuites.push(result); 91 | } 92 | 93 | if (currentParent == topResults) { 94 | return; 95 | } 96 | 97 | currentParent = currentParent.parent; 98 | }; 99 | 100 | this.specStarted = function(result) { 101 | currentParent.addChild(result, 'spec'); 102 | }; 103 | 104 | var failures = []; 105 | this.specDone = function(result) { 106 | if(noExpectations(result) && typeof console !== 'undefined' && typeof console.error !== 'undefined') { 107 | console.error('Spec \'' + result.fullName + '\' has no expectations.'); 108 | } 109 | 110 | if (result.status != 'disabled') { 111 | specsExecuted++; 112 | } 113 | 114 | symbols.appendChild(createDom('li', { 115 | className: noExpectations(result) ? 'empty' : result.status, 116 | id: 'spec_' + result.id, 117 | title: result.fullName 118 | } 119 | )); 120 | 121 | if (result.status == 'failed') { 122 | failureCount++; 123 | 124 | var failure = 125 | createDom('div', {className: 'spec-detail failed'}, 126 | createDom('div', {className: 'description'}, 127 | createDom('a', {title: result.fullName, href: specHref(result)}, result.fullName) 128 | ), 129 | createDom('div', {className: 'messages'}) 130 | ); 131 | var messages = failure.childNodes[1]; 132 | 133 | for (var i = 0; i < result.failedExpectations.length; i++) { 134 | var expectation = result.failedExpectations[i]; 135 | messages.appendChild(createDom('div', {className: 'result-message'}, expectation.message)); 136 | messages.appendChild(createDom('div', {className: 'stack-trace'}, expectation.stack)); 137 | } 138 | 139 | failures.push(failure); 140 | } 141 | 142 | if (result.status == 'pending') { 143 | pendingSpecCount++; 144 | } 145 | }; 146 | 147 | this.jasmineDone = function() { 148 | var banner = find('.banner'); 149 | var alert = find('.alert'); 150 | alert.appendChild(createDom('span', {className: 'duration'}, 'finished in ' + timer.elapsed() / 1000 + 's')); 151 | 152 | banner.appendChild( 153 | createDom('div', { className: 'run-options' }, 154 | createDom('span', { className: 'trigger' }, 'Options'), 155 | createDom('div', { className: 'payload' }, 156 | createDom('div', { className: 'exceptions' }, 157 | createDom('input', { 158 | className: 'raise', 159 | id: 'raise-exceptions', 160 | type: 'checkbox' 161 | }), 162 | createDom('label', { className: 'label', 'for': 'raise-exceptions' }, 'raise exceptions')), 163 | createDom('div', { className: 'throw-failures' }, 164 | createDom('input', { 165 | className: 'throw', 166 | id: 'throw-failures', 167 | type: 'checkbox' 168 | }), 169 | createDom('label', { className: 'label', 'for': 'throw-failures' }, 'stop spec on expectation failure')) 170 | ) 171 | )); 172 | 173 | var raiseCheckbox = find('#raise-exceptions'); 174 | 175 | raiseCheckbox.checked = !env.catchingExceptions(); 176 | raiseCheckbox.onclick = onRaiseExceptionsClick; 177 | 178 | var throwCheckbox = find('#throw-failures'); 179 | throwCheckbox.checked = env.throwingExpectationFailures(); 180 | throwCheckbox.onclick = onThrowExpectationsClick; 181 | 182 | var optionsMenu = find('.run-options'), 183 | optionsTrigger = optionsMenu.querySelector('.trigger'), 184 | optionsPayload = optionsMenu.querySelector('.payload'), 185 | isOpen = /\bopen\b/; 186 | 187 | optionsTrigger.onclick = function() { 188 | if (isOpen.test(optionsPayload.className)) { 189 | optionsPayload.className = optionsPayload.className.replace(isOpen, ''); 190 | } else { 191 | optionsPayload.className += ' open'; 192 | } 193 | }; 194 | 195 | if (specsExecuted < totalSpecsDefined) { 196 | var skippedMessage = 'Ran ' + specsExecuted + ' of ' + totalSpecsDefined + ' specs - run all'; 197 | alert.appendChild( 198 | createDom('span', {className: 'bar skipped'}, 199 | createDom('a', {href: '?', title: 'Run all specs'}, skippedMessage) 200 | ) 201 | ); 202 | } 203 | var statusBarMessage = ''; 204 | var statusBarClassName = 'bar '; 205 | 206 | if (totalSpecsDefined > 0) { 207 | statusBarMessage += pluralize('spec', specsExecuted) + ', ' + pluralize('failure', failureCount); 208 | if (pendingSpecCount) { statusBarMessage += ', ' + pluralize('pending spec', pendingSpecCount); } 209 | statusBarClassName += (failureCount > 0) ? 'failed' : 'passed'; 210 | } else { 211 | statusBarClassName += 'skipped'; 212 | statusBarMessage += 'No specs found'; 213 | } 214 | 215 | alert.appendChild(createDom('span', {className: statusBarClassName}, statusBarMessage)); 216 | 217 | for(i = 0; i < failedSuites.length; i++) { 218 | var failedSuite = failedSuites[i]; 219 | for(var j = 0; j < failedSuite.failedExpectations.length; j++) { 220 | var errorBarMessage = 'AfterAll ' + failedSuite.failedExpectations[j].message; 221 | var errorBarClassName = 'bar errored'; 222 | alert.appendChild(createDom('span', {className: errorBarClassName}, errorBarMessage)); 223 | } 224 | } 225 | 226 | var results = find('.results'); 227 | results.appendChild(summary); 228 | 229 | summaryList(topResults, summary); 230 | 231 | function summaryList(resultsTree, domParent) { 232 | var specListNode; 233 | for (var i = 0; i < resultsTree.children.length; i++) { 234 | var resultNode = resultsTree.children[i]; 235 | if (resultNode.type == 'suite') { 236 | var suiteListNode = createDom('ul', {className: 'suite', id: 'suite-' + resultNode.result.id}, 237 | createDom('li', {className: 'suite-detail'}, 238 | createDom('a', {href: specHref(resultNode.result)}, resultNode.result.description) 239 | ) 240 | ); 241 | 242 | summaryList(resultNode, suiteListNode); 243 | domParent.appendChild(suiteListNode); 244 | } 245 | if (resultNode.type == 'spec') { 246 | if (domParent.getAttribute('class') != 'specs') { 247 | specListNode = createDom('ul', {className: 'specs'}); 248 | domParent.appendChild(specListNode); 249 | } 250 | var specDescription = resultNode.result.description; 251 | if(noExpectations(resultNode.result)) { 252 | specDescription = 'SPEC HAS NO EXPECTATIONS ' + specDescription; 253 | } 254 | if(resultNode.result.status === 'pending' && resultNode.result.pendingReason !== '') { 255 | specDescription = specDescription + ' PENDING WITH MESSAGE: ' + resultNode.result.pendingReason; 256 | } 257 | specListNode.appendChild( 258 | createDom('li', { 259 | className: resultNode.result.status, 260 | id: 'spec-' + resultNode.result.id 261 | }, 262 | createDom('a', {href: specHref(resultNode.result)}, specDescription) 263 | ) 264 | ); 265 | } 266 | } 267 | } 268 | 269 | if (failures.length) { 270 | alert.appendChild( 271 | createDom('span', {className: 'menu bar spec-list'}, 272 | createDom('span', {}, 'Spec List | '), 273 | createDom('a', {className: 'failures-menu', href: '#'}, 'Failures'))); 274 | alert.appendChild( 275 | createDom('span', {className: 'menu bar failure-list'}, 276 | createDom('a', {className: 'spec-list-menu', href: '#'}, 'Spec List'), 277 | createDom('span', {}, ' | Failures '))); 278 | 279 | find('.failures-menu').onclick = function() { 280 | setMenuModeTo('failure-list'); 281 | }; 282 | find('.spec-list-menu').onclick = function() { 283 | setMenuModeTo('spec-list'); 284 | }; 285 | 286 | setMenuModeTo('failure-list'); 287 | 288 | var failureNode = find('.failures'); 289 | for (var i = 0; i < failures.length; i++) { 290 | failureNode.appendChild(failures[i]); 291 | } 292 | } 293 | }; 294 | 295 | return this; 296 | 297 | function find(selector) { 298 | return getContainer().querySelector('.jasmine_html-reporter ' + selector); 299 | } 300 | 301 | function clearPrior() { 302 | // return the reporter 303 | var oldReporter = find(''); 304 | 305 | if(oldReporter) { 306 | getContainer().removeChild(oldReporter); 307 | } 308 | } 309 | 310 | function createDom(type, attrs, childrenVarArgs) { 311 | var el = createElement(type); 312 | 313 | for (var i = 2; i < arguments.length; i++) { 314 | var child = arguments[i]; 315 | 316 | if (typeof child === 'string') { 317 | el.appendChild(createTextNode(child)); 318 | } else { 319 | if (child) { 320 | el.appendChild(child); 321 | } 322 | } 323 | } 324 | 325 | for (var attr in attrs) { 326 | if (attr == 'className') { 327 | el[attr] = attrs[attr]; 328 | } else { 329 | el.setAttribute(attr, attrs[attr]); 330 | } 331 | } 332 | 333 | return el; 334 | } 335 | 336 | function pluralize(singular, count) { 337 | var word = (count == 1 ? singular : singular + 's'); 338 | 339 | return '' + count + ' ' + word; 340 | } 341 | 342 | function specHref(result) { 343 | return addToExistingQueryString('spec', result.fullName); 344 | } 345 | 346 | function defaultQueryString(key, value) { 347 | return '?' + key + '=' + value; 348 | } 349 | 350 | function setMenuModeTo(mode) { 351 | htmlReporterMain.setAttribute('class', 'jasmine_html-reporter ' + mode); 352 | } 353 | 354 | function noExpectations(result) { 355 | return (result.failedExpectations.length + result.passedExpectations.length) === 0 && 356 | result.status === 'passed'; 357 | } 358 | } 359 | 360 | return HtmlReporter; 361 | }; 362 | 363 | jasmineRequire.HtmlSpecFilter = function() { 364 | function HtmlSpecFilter(options) { 365 | var filterString = options && options.filterString() && options.filterString().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); 366 | var filterPattern = new RegExp(filterString); 367 | 368 | this.matches = function(specName) { 369 | return filterPattern.test(specName); 370 | }; 371 | } 372 | 373 | return HtmlSpecFilter; 374 | }; 375 | 376 | jasmineRequire.ResultsNode = function() { 377 | function ResultsNode(result, type, parent) { 378 | this.result = result; 379 | this.type = type; 380 | this.parent = parent; 381 | 382 | this.children = []; 383 | 384 | this.addChild = function(result, type) { 385 | this.children.push(new ResultsNode(result, type, this)); 386 | }; 387 | 388 | this.last = function() { 389 | return this.children[this.children.length - 1]; 390 | }; 391 | } 392 | 393 | return ResultsNode; 394 | }; 395 | 396 | jasmineRequire.QueryString = function() { 397 | function QueryString(options) { 398 | 399 | this.navigateWithNewParam = function(key, value) { 400 | options.getWindowLocation().search = this.fullStringWithNewParam(key, value); 401 | }; 402 | 403 | this.fullStringWithNewParam = function(key, value) { 404 | var paramMap = queryStringToParamMap(); 405 | paramMap[key] = value; 406 | return toQueryString(paramMap); 407 | }; 408 | 409 | this.getParam = function(key) { 410 | return queryStringToParamMap()[key]; 411 | }; 412 | 413 | return this; 414 | 415 | function toQueryString(paramMap) { 416 | var qStrPairs = []; 417 | for (var prop in paramMap) { 418 | qStrPairs.push(encodeURIComponent(prop) + '=' + encodeURIComponent(paramMap[prop])); 419 | } 420 | return '?' + qStrPairs.join('&'); 421 | } 422 | 423 | function queryStringToParamMap() { 424 | var paramStr = options.getWindowLocation().search.substring(1), 425 | params = [], 426 | paramMap = {}; 427 | 428 | if (paramStr.length > 0) { 429 | params = paramStr.split('&'); 430 | for (var i = 0; i < params.length; i++) { 431 | var p = params[i].split('='); 432 | var value = decodeURIComponent(p[1]); 433 | if (value === 'true' || value === 'false') { 434 | value = JSON.parse(value); 435 | } 436 | paramMap[decodeURIComponent(p[0])] = value; 437 | } 438 | } 439 | 440 | return paramMap; 441 | } 442 | 443 | } 444 | 445 | return QueryString; 446 | }; 447 | -------------------------------------------------------------------------------- /tests/lib/jasmine-2.3.4/jasmine.css: -------------------------------------------------------------------------------- 1 | body { overflow-y: scroll; } 2 | 3 | .jasmine_html-reporter { background-color: #eee; padding: 5px; margin: -8px; font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333; } 4 | .jasmine_html-reporter a { text-decoration: none; } 5 | .jasmine_html-reporter a:hover { text-decoration: underline; } 6 | .jasmine_html-reporter p, .jasmine_html-reporter h1, .jasmine_html-reporter h2, .jasmine_html-reporter h3, .jasmine_html-reporter h4, .jasmine_html-reporter h5, .jasmine_html-reporter h6 { margin: 0; line-height: 14px; } 7 | .jasmine_html-reporter .banner, .jasmine_html-reporter .symbol-summary, .jasmine_html-reporter .summary, .jasmine_html-reporter .result-message, .jasmine_html-reporter .spec .description, .jasmine_html-reporter .spec-detail .description, .jasmine_html-reporter .alert .bar, .jasmine_html-reporter .stack-trace { padding-left: 9px; padding-right: 9px; } 8 | .jasmine_html-reporter .banner { position: relative; } 9 | .jasmine_html-reporter .banner .title { background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAAAZCAMAAACGusnyAAACdlBMVEX/////AP+AgICqVaqAQICZM5mAVYCSSZKAQICOOY6ATYCLRouAQICJO4mSSYCIRIiPQICHPIeOR4CGQ4aMQICGPYaLRoCFQ4WKQICPPYWJRYCOQoSJQICNPoSIRICMQoSHQICHRICKQoOHQICKPoOJO4OJQYOMQICMQ4CIQYKLQICIPoKLQ4CKQICNPoKJQISMQ4KJQoSLQYKJQISLQ4KIQoSKQYKIQICIQISMQoSKQYKLQIOLQoOJQYGLQIOKQIOMQoGKQYOLQYGKQIOLQoGJQYOJQIOKQYGJQIOKQoGKQIGLQIKLQ4KKQoGLQYKJQIGKQYKJQIGKQIKJQoGKQYKLQIGKQYKLQIOJQoKKQoOJQYKKQIOJQoKKQoOKQIOLQoKKQYOLQYKJQIOKQoKKQYKKQoKJQYOKQYKLQIOKQoKLQYOKQYKLQIOJQoGKQYKJQYGJQoGKQYKLQoGLQYGKQoGJQYKKQYGJQIKKQoGJQYKLQIKKQYGLQYKKQYGKQYGKQYKJQYOKQoKJQYOKQYKLQYOLQYOKQYKLQYOKQoKKQYKKQYOKQYOJQYKKQYKLQYKKQIKKQoKKQYKKQYKKQoKJQIKKQYKLQYKKQYKKQIKKQYKKQYKKQYKKQIKKQYKJQYGLQYGKQYKKQYKKQYGKQIKKQYGKQYOJQoKKQYOLQYKKQYOKQoKKQYKKQoKKQYKKQYKJQYKLQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKJQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKLQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKmIDpEAAAA0XRSTlMAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyAiIyQlJycoKissLS4wMTQ1Njc4OTo7PDw+P0BCQ0RISUpLTE1OUFNUVVdYWFlaW15fYGFiY2ZnaGlqa2xtb3BxcnN0dnh5ent8fX5/gIGChIWIioyNjo+QkZOUlZaYmZqbnJ2eoKGio6WmqKmsra6vsLGztre4ubq7vL2+wMHDxMjJysvNzs/Q0dLU1tfY2dvc3t/g4eLj5ebn6Onq6+zt7u/w8vP09fb3+Pn6+/z9/vkVQXAAAAMaSURBVHhe5dXxV1N1GMfxz2ABbDgIAm5VDJOyVDIJLUMaVpBWUZUaGbmqoGpZRSiGiRWp6KoZ5AB0ZY50RImZQIlahKkMYXv/R90dBvET/rJfOr3Ouc8v99zPec59zvf56j+vYKlViSf7250X4Mr3O29Tgq08BdGB4DhcekEJ5YkQKFsgWZdtj9JpV+I8xPjLFqkrsEIqO8PHSpis36jWazcqjEsfJjkvRssVU37SdIOu4XCf5vEJPsnwJpnRNU9JmxhMk8l1gehIrq7hTFjzOD+Vf88629qKMJVNltInFeRexRQyJlNeqd1iGDlSzrIUIyXbyFfm3RYprcQRe7lqtWyGYbfc6dT0R2vmdOOkX3u55C1rP37ftiH+tDby4r/RBT0w8TyEkr+epB9XgPDmSYYWbrhCuFYaIyw3fDQAXTnSkh+ANofiHmWf9l+FY1I90FdQTetstO00o23novzVsJ7uB3/C5TkbjRwZ5JerwV4iRWq9HFbFMaK/d0TYqayRiQPuIxxS3Bu8JWU90/60tKi7vkhaznez0a/TbVOKj5CaOZh6fWG6/Lyv9B/ZLR1gw/S/fpbeVD3MCW1li6SvWDOn65tr99/uvWtBS0XDm4s1t+sOHpG0kpBKx/l77wOSnxLpcx6TXmXLTPQOKYOf9Q1dfr8/SJ2mFdCvl1Yl93DiHUZvXeLJbGSzYu5gVJ2slbSakOR8dxCq5adQ2oFLqsE9Ex3L4qQO0eOPeU5x56bypXp4onSEb5OkICX6lDat55TeoztNKQcJaakrz9KCb95oD69IKq+yKW4XPjknaS52V0TZqE2cTtXjcHSCRmUO88e+85hj3EP74i9p8pylw7lxgMDyyl6OV7ZejnjNMfatu87LxRbH0IS35gt2a4ZjmGpVBdKK3Wr6INk8jWWSGqbA55CKgjBRC6E9w78ydTg3ABS3AFV1QN0Y4Aa2pgEjWnQURj9L0ayK6R2ysEqxHUKzYnLvvyU+i9KM2JHJzE4vyZOyDcOwOsySajeLPc8sNvPJkFlyJd20wpqAzZeAfZ3oWybxd+P/3j+SG3uSBdf2VQAAAABJRU5ErkJggg==') no-repeat; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgdmVyc2lvbj0iMS4xIgogICB3aWR0aD0iNjgxLjk2MjUyIgogICBoZWlnaHQ9IjE4Ny41IgogICBpZD0ic3ZnMiIKICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PG1ldGFkYXRhCiAgICAgaWQ9Im1ldGFkYXRhOCI+PHJkZjpSREY+PGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PjxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz48L2NjOldvcms+PC9yZGY6UkRGPjwvbWV0YWRhdGE+PGRlZnMKICAgICBpZD0iZGVmczYiPjxjbGlwUGF0aAogICAgICAgaWQ9ImNsaXBQYXRoMTgiPjxwYXRoCiAgICAgICAgIGQ9Ik0gMCwxNTAwIDAsMCBsIDU0NTUuNzQsMCAwLDE1MDAgTCAwLDE1MDAgeiIKICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgaWQ9InBhdGgyMCIgLz48L2NsaXBQYXRoPjwvZGVmcz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMjUsMCwwLC0xLjI1LDAsMTg3LjUpIgogICAgIGlkPSJnMTAiPjxnCiAgICAgICB0cmFuc2Zvcm09InNjYWxlKDAuMSwwLjEpIgogICAgICAgaWQ9ImcxMiI+PGcKICAgICAgICAgaWQ9ImcxNCI+PGcKICAgICAgICAgICBjbGlwLXBhdGg9InVybCgjY2xpcFBhdGgxOCkiCiAgICAgICAgICAgaWQ9ImcxNiI+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMTU0NCw1OTkuNDM0IGMgMC45MiwtNDAuMzUyIDI1LjY4LC04MS42MDIgNzEuNTMsLTgxLjYwMiAyNy41MSwwIDQ3LjY4LDEyLjgzMiA2MS40NCwzNS43NTQgMTIuODMsMjIuOTMgMTIuODMsNTYuODUyIDEyLjgzLDgyLjUyNyBsIDAsMzI5LjE4NCAtNzEuNTIsMCAwLDEwNC41NDMgMjY2LjgzLDAgMCwtMTA0LjU0MyAtNzAuNiwwIDAsLTM0NC43NyBjIDAsLTU4LjY5MSAtMy42OCwtMTA0LjUzMSAtNDQuOTMsLTE1Mi4yMTggLTM2LjY4LC00Mi4xOCAtOTYuMjgsLTY2LjAyIC0xNTMuMTQsLTY2LjAyIC0xMTcuMzcsMCAtMjA3LjI0LDc3Ljk0MSAtMjAyLjY0LDE5Ny4xNDUgbCAxMzAuMiwwIgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMjIiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDIzMDEuNCw2NjIuNjk1IGMgMCw4MC43MDMgLTY2Ljk0LDE0NS44MTMgLTE0Ny42MywxNDUuODEzIC04My40NCwwIC0xNDcuNjMsLTY4Ljc4MSAtMTQ3LjYzLC0xNTEuMzAxIDAsLTc5Ljc4NSA2Ni45NCwtMTQ1LjgwMSAxNDUuOCwtMTQ1LjgwMSA4NC4zNSwwIDE0OS40Niw2Ny44NTIgMTQ5LjQ2LDE1MS4yODkgeiBtIC0xLjgzLC0xODEuNTQ3IGMgLTM1Ljc3LC01NC4wOTcgLTkzLjUzLC03OC44NTkgLTE1Ny43MiwtNzguODU5IC0xNDAuMywwIC0yNTEuMjQsMTE2LjQ0OSAtMjUxLjI0LDI1NC45MTggMCwxNDIuMTI5IDExMy43LDI2MC40MSAyNTYuNzQsMjYwLjQxIDYzLjI3LDAgMTE4LjI5LC0yOS4zMzYgMTUyLjIyLC04Mi41MjMgbCAwLDY5LjY4NyAxNzUuMTQsMCAwLC0xMDQuNTI3IC02MS40NCwwIDAsLTI4MC41OTggNjEuNDQsMCAwLC0xMDQuNTI3IC0xNzUuMTQsMCAwLDY2LjAxOSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSAyNjIyLjMzLDU1Ny4yNTggYyAzLjY3LC00NC4wMTYgMzMuMDEsLTczLjM0OCA3OC44NiwtNzMuMzQ4IDMzLjkzLDAgNjYuOTMsMjMuODI0IDY2LjkzLDYwLjUwNCAwLDQ4LjYwNiAtNDUuODQsNTYuODU2IC04My40NCw2Ni45NDEgLTg1LjI4LDIyLjAwNCAtMTc4LjgxLDQ4LjYwNiAtMTc4LjgxLDE1NS44NzkgMCw5My41MzYgNzguODYsMTQ3LjYzMyAxNjUuOTgsMTQ3LjYzMyA0NCwwIDgzLjQzLC05LjE3NiAxMTAuOTQsLTQ0LjAwOCBsIDAsMzMuOTIyIDgyLjUzLDAgMCwtMTMyLjk2NSAtMTA4LjIxLDAgYyAtMS44MywzNC44NTYgLTI4LjQyLDU3Ljc3NCAtNjMuMjYsNTcuNzc0IC0zMC4yNiwwIC02Mi4zNSwtMTcuNDIyIC02Mi4zNSwtNTEuMzQ4IDAsLTQ1Ljg0NyA0NC45MywtNTUuOTMgODAuNjksLTY0LjE4IDg4LjAyLC0yMC4xNzUgMTgyLjQ3LC00Ny42OTUgMTgyLjQ3LC0xNTcuNzM0IDAsLTk5LjAyNyAtODMuNDQsLTE1NC4wMzkgLTE3NS4xMywtMTU0LjAzOSAtNDkuNTMsMCAtOTQuNDYsMTUuNTgyIC0xMjYuNTUsNTMuMTggbCAwLC00MC4zNCAtODUuMjcsMCAwLDE0Mi4xMjkgMTE0LjYyLDAiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGgyNiIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMjk4OC4xOCw4MDAuMjU0IC02My4yNiwwIDAsMTA0LjUyNyAxNjUuMDUsMCAwLC03My4zNTUgYyAzMS4xOCw1MS4zNDcgNzguODYsODUuMjc3IDE0MS4yMSw4NS4yNzcgNjcuODUsMCAxMjQuNzEsLTQxLjI1OCAxNTIuMjEsLTEwMi42OTkgMjYuNiw2Mi4zNTEgOTIuNjIsMTAyLjY5OSAxNjAuNDcsMTAyLjY5OSA1My4xOSwwIDEwNS40NiwtMjIgMTQxLjIxLC02Mi4zNTEgMzguNTIsLTQ0LjkzOCAzOC41MiwtOTMuNTMyIDM4LjUyLC0xNDkuNDU3IGwgMCwtMTg1LjIzOSA2My4yNywwIDAsLTEwNC41MjcgLTIzOC40MiwwIDAsMTA0LjUyNyA2My4yOCwwIDAsMTU3LjcxNSBjIDAsMzIuMTAyIDAsNjAuNTI3IC0xNC42Nyw4OC45NTcgLTE4LjM0LDI2LjU4MiAtNDguNjEsNDAuMzQ0IC03OS43Nyw0MC4zNDQgLTMwLjI2LDAgLTYzLjI4LC0xMi44NDQgLTgyLjUzLC0zNi42NzIgLTIyLjkzLC0yOS4zNTUgLTIyLjkzLC01Ni44NjMgLTIyLjkzLC05Mi42MjkgbCAwLC0xNTcuNzE1IDYzLjI3LDAgMCwtMTA0LjUyNyAtMjM4LjQxLDAgMCwxMDQuNTI3IDYzLjI4LDAgMCwxNTAuMzgzIGMgMCwyOS4zNDggMCw2Ni4wMjMgLTE0LjY3LDkxLjY5OSAtMTUuNTksMjkuMzM2IC00Ny42OSw0NC45MzQgLTgwLjcsNDQuOTM0IC0zMS4xOCwwIC01Ny43NywtMTEuMDA4IC03Ny45NCwtMzUuNzc0IC0yNC43NywtMzAuMjUzIC0yNi42LC02Mi4zNDMgLTI2LjYsLTk5Ljk0MSBsIDAsLTE1MS4zMDEgNjMuMjcsMCAwLC0xMDQuNTI3IC0yMzguNCwwIDAsMTA0LjUyNyA2My4yNiwwIDAsMjgwLjU5OCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDI4IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSAzOTk4LjY2LDk1MS41NDcgLTExMS44NywwIDAsMTE4LjI5MyAxMTEuODcsMCAwLC0xMTguMjkzIHogbSAwLC00MzEuODkxIDYzLjI3LDAgMCwtMTA0LjUyNyAtMjM5LjMzLDAgMCwxMDQuNTI3IDY0LjE5LDAgMCwyODAuNTk4IC02My4yNywwIDAsMTA0LjUyNyAxNzUuMTQsMCAwLC0zODUuMTI1IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzAiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDQxNTkuMTIsODAwLjI1NCAtNjMuMjcsMCAwLDEwNC41MjcgMTc1LjE0LDAgMCwtNjkuNjg3IGMgMjkuMzUsNTQuMTAxIDg0LjM2LDgwLjY5OSAxNDQuODcsODAuNjk5IDUzLjE5LDAgMTA1LjQ1LC0yMi4wMTYgMTQxLjIyLC02MC41MjcgNDAuMzQsLTQ0LjkzNCA0MS4yNiwtODguMDMyIDQxLjI2LC0xNDMuOTU3IGwgMCwtMTkxLjY1MyA2My4yNywwIDAsLTEwNC41MjcgLTIzOC40LDAgMCwxMDQuNTI3IDYzLjI2LDAgMCwxNTguNjM3IGMgMCwzMC4yNjIgMCw2MS40MzQgLTE5LjI2LDg4LjAzNSAtMjAuMTcsMjYuNTgyIC01My4xOCwzOS40MTQgLTg2LjE5LDM5LjQxNCAtMzMuOTMsMCAtNjguNzcsLTEzLjc1IC04OC45NCwtNDEuMjUgLTIxLjA5LC0yNy41IC0yMS4wOSwtNjkuNjg3IC0yMS4wOSwtMTAyLjcwNyBsIDAsLTE0Mi4xMjkgNjMuMjYsMCAwLC0xMDQuNTI3IC0yMzguNCwwIDAsMTA0LjUyNyA2My4yNywwIDAsMjgwLjU5OCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDMyIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA1MDgyLjQ4LDcwMy45NjUgYyAtMTkuMjQsNzAuNjA1IC04MS42LDExNS41NDcgLTE1NC4wNCwxMTUuNTQ3IC02Ni4wNCwwIC0xMjkuMywtNTEuMzQ4IC0xNDMuMDUsLTExNS41NDcgbCAyOTcuMDksMCB6IG0gODUuMjcsLTE0NC44ODMgYyAtMzguNTEsLTkzLjUyMyAtMTI5LjI3LC0xNTYuNzkzIC0yMzEuMDUsLTE1Ni43OTMgLTE0My4wNywwIC0yNTcuNjgsMTExLjg3MSAtMjU3LjY4LDI1NS44MzYgMCwxNDQuODgzIDEwOS4xMiwyNjEuMzI4IDI1NC45MSwyNjEuMzI4IDY3Ljg3LDAgMTM1LjcyLC0zMC4yNTggMTgzLjM5LC03OC44NjMgNDguNjIsLTUxLjM0NCA2OC43OSwtMTEzLjY5NSA2OC43OSwtMTgzLjM4MyBsIC0zLjY3LC0zOS40MzQgLTM5Ni4xMywwIGMgMTQuNjcsLTY3Ljg2MyA3Ny4wMywtMTE3LjM2MyAxNDYuNzIsLTExNy4zNjMgNDguNTksMCA5MC43NiwxOC4zMjggMTE4LjI4LDU4LjY3MiBsIDExNi40NCwwIgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzQiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDY5MC44OTUsODUwLjcwMyA5MC43NSwwIDIyLjU0MywzMS4wMzUgMCwyNDMuMTIyIC0xMzUuODI5LDAgMCwtMjQzLjE0MSAyMi41MzYsLTMxLjAxNiIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDM2IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA2MzIuMzk1LDc0Mi4yNTggMjguMDM5LDg2LjMwNCAtMjIuNTUxLDMxLjA0IC0yMzEuMjIzLDc1LjEyOCAtNDEuOTc2LC0xMjkuMTgzIDIzMS4yNTcsLTc1LjEzNyAzNi40NTQsMTEuODQ4IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzgiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDcxNy40NDksNjUzLjEwNSAtNzMuNDEsNTMuMzYgLTM2LjQ4OCwtMTEuODc1IC0xNDIuOTAzLC0xOTYuNjkyIDEwOS44ODMsLTc5LjgyOCAxNDIuOTE4LDE5Ni43MDMgMCwzOC4zMzIiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGg0MCIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gODI4LjUyLDcwNi40NjUgLTczLjQyNiwtNTMuMzQgMC4wMTEsLTM4LjM1OSBMIDg5OC4wMDQsNDE4LjA3IDEwMDcuOSw0OTcuODk4IDg2NC45NzMsNjk0LjYwOSA4MjguNTIsNzA2LjQ2NSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDQyIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA4MTIuMDg2LDgyOC41ODYgMjguMDU1LC04Ni4zMiAzNi40ODQsLTExLjgzNiAyMzEuMjI1LDc1LjExNyAtNDEuOTcsMTI5LjE4MyAtMjMxLjIzOSwtNzUuMTQgLTIyLjU1NSwtMzEuMDA0IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNDQiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDczNi4zMDEsMTMzNS44OCBjIC0zMjMuMDQ3LDAgLTU4NS44NzUsLTI2Mi43OCAtNTg1Ljg3NSwtNTg1Ljc4MiAwLC0zMjMuMTE4IDI2Mi44MjgsLTU4NS45NzcgNTg1Ljg3NSwtNTg1Ljk3NyAzMjMuMDE5LDAgNTg1LjgwOSwyNjIuODU5IDU4NS44MDksNTg1Ljk3NyAwLDMyMy4wMDIgLTI2Mi43OSw1ODUuNzgyIC01ODUuODA5LDU4NS43ODIgbCAwLDAgeiBtIDAsLTExOC42MSBjIDI1Ny45NzIsMCA0NjcuMTg5LC0yMDkuMTMgNDY3LjE4OSwtNDY3LjE3MiAwLC0yNTguMTI5IC0yMDkuMjE3LC00NjcuMzQ4IC00NjcuMTg5LC00NjcuMzQ4IC0yNTguMDc0LDAgLTQ2Ny4yNTQsMjA5LjIxOSAtNDY3LjI1NCw0NjcuMzQ4IDAsMjU4LjA0MiAyMDkuMTgsNDY3LjE3MiA0NjcuMjU0LDQ2Ny4xNzIiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGg0NiIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMTA5MS4xMyw2MTkuODgzIC0xNzUuNzcxLDU3LjEyMSAxMS42MjksMzUuODA4IDE3NS43NjIsLTU3LjEyMSAtMTEuNjIsLTM1LjgwOCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDQ4IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0iTSA4NjYuOTU3LDkwMi4wNzQgODM2LjUsOTI0LjE5OSA5NDUuMTIxLDEwNzMuNzMgOTc1LjU4NiwxMDUxLjYxIDg2Ni45NTcsOTAyLjA3NCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDUwIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0iTSA2MDcuNDY1LDkwMy40NDUgNDk4Ljg1NSwxMDUyLjk3IDUyOS4zMiwxMDc1LjEgNjM3LjkzLDkyNS41NjYgNjA3LjQ2NSw5MDMuNDQ1IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNTIiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDM4MC42ODgsNjIyLjEyOSAtMTEuNjI2LDM1LjgwMSAxNzUuNzU4LDU3LjA5IDExLjYyMSwtMzUuODAxIC0xNzUuNzUzLC01Ny4wOSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDU0IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA3MTYuMjg5LDM3Ni41OSAzNy42NDA2LDAgMCwxODQuODE2IC0zNy42NDA2LDAgMCwtMTg0LjgxNiB6IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNTYiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjwvZz48L2c+PC9nPjwvZz48L3N2Zz4=') no-repeat, none; -moz-background-size: 100%; -o-background-size: 100%; -webkit-background-size: 100%; background-size: 100%; display: block; float: left; width: 90px; height: 25px; } 10 | .jasmine_html-reporter .banner .version { margin-left: 14px; position: relative; top: 6px; } 11 | .jasmine_html-reporter #jasmine_content { position: fixed; right: 100%; } 12 | .jasmine_html-reporter .version { color: #aaa; } 13 | .jasmine_html-reporter .banner { margin-top: 14px; } 14 | .jasmine_html-reporter .duration { color: #fff; float: right; line-height: 28px; padding-right: 9px; } 15 | .jasmine_html-reporter .symbol-summary { overflow: hidden; *zoom: 1; margin: 14px 0; } 16 | .jasmine_html-reporter .symbol-summary li { display: inline-block; height: 8px; width: 14px; font-size: 16px; } 17 | .jasmine_html-reporter .symbol-summary li.passed { font-size: 14px; } 18 | .jasmine_html-reporter .symbol-summary li.passed:before { color: #007069; content: "\02022"; } 19 | .jasmine_html-reporter .symbol-summary li.failed { line-height: 9px; } 20 | .jasmine_html-reporter .symbol-summary li.failed:before { color: #ca3a11; content: "\d7"; font-weight: bold; margin-left: -1px; } 21 | .jasmine_html-reporter .symbol-summary li.disabled { font-size: 14px; } 22 | .jasmine_html-reporter .symbol-summary li.disabled:before { color: #bababa; content: "\02022"; } 23 | .jasmine_html-reporter .symbol-summary li.pending { line-height: 17px; } 24 | .jasmine_html-reporter .symbol-summary li.pending:before { color: #ba9d37; content: "*"; } 25 | .jasmine_html-reporter .symbol-summary li.empty { font-size: 14px; } 26 | .jasmine_html-reporter .symbol-summary li.empty:before { color: #ba9d37; content: "\02022"; } 27 | .jasmine_html-reporter .run-options { float: right; margin-right: 5px; border: 1px solid #8a4182; color: #8a4182; position: relative; line-height: 20px; } 28 | .jasmine_html-reporter .run-options .trigger { cursor: pointer; padding: 8px 16px; } 29 | .jasmine_html-reporter .run-options .payload { position: absolute; display: none; right: -1px; border: 1px solid #8a4182; background-color: #eee; white-space: nowrap; padding: 4px 8px; } 30 | .jasmine_html-reporter .run-options .payload.open { display: block; } 31 | .jasmine_html-reporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; } 32 | .jasmine_html-reporter .bar.failed { background-color: #ca3a11; } 33 | .jasmine_html-reporter .bar.passed { background-color: #007069; } 34 | .jasmine_html-reporter .bar.skipped { background-color: #bababa; } 35 | .jasmine_html-reporter .bar.errored { background-color: #ca3a11; } 36 | .jasmine_html-reporter .bar.menu { background-color: #fff; color: #aaa; } 37 | .jasmine_html-reporter .bar.menu a { color: #333; } 38 | .jasmine_html-reporter .bar a { color: white; } 39 | .jasmine_html-reporter.spec-list .bar.menu.failure-list, .jasmine_html-reporter.spec-list .results .failures { display: none; } 40 | .jasmine_html-reporter.failure-list .bar.menu.spec-list, .jasmine_html-reporter.failure-list .summary { display: none; } 41 | .jasmine_html-reporter .results { margin-top: 14px; } 42 | .jasmine_html-reporter .summary { margin-top: 14px; } 43 | .jasmine_html-reporter .summary ul { list-style-type: none; margin-left: 14px; padding-top: 0; padding-left: 0; } 44 | .jasmine_html-reporter .summary ul.suite { margin-top: 7px; margin-bottom: 7px; } 45 | .jasmine_html-reporter .summary li.passed a { color: #007069; } 46 | .jasmine_html-reporter .summary li.failed a { color: #ca3a11; } 47 | .jasmine_html-reporter .summary li.empty a { color: #ba9d37; } 48 | .jasmine_html-reporter .summary li.pending a { color: #ba9d37; } 49 | .jasmine_html-reporter .summary li.disabled a { color: #bababa; } 50 | .jasmine_html-reporter .description + .suite { margin-top: 0; } 51 | .jasmine_html-reporter .suite { margin-top: 14px; } 52 | .jasmine_html-reporter .suite a { color: #333; } 53 | .jasmine_html-reporter .failures .spec-detail { margin-bottom: 28px; } 54 | .jasmine_html-reporter .failures .spec-detail .description { background-color: #ca3a11; } 55 | .jasmine_html-reporter .failures .spec-detail .description a { color: white; } 56 | .jasmine_html-reporter .result-message { padding-top: 14px; color: #333; white-space: pre; } 57 | .jasmine_html-reporter .result-message span.result { display: block; } 58 | .jasmine_html-reporter .stack-trace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666; border: 1px solid #ddd; background: white; white-space: pre; } 59 | -------------------------------------------------------------------------------- /tests/lib/jasmine-jquery.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Jasmine-jQuery: a set of jQuery helpers for Jasmine tests. 3 | 4 | Version 2.1.0 5 | 6 | https://github.com/velesin/jasmine-jquery 7 | 8 | Copyright (c) 2010-2014 Wojciech Zawistowski, Travis Jeffery 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining 11 | a copy of this software and associated documentation files (the 12 | "Software"), to deal in the Software without restriction, including 13 | without limitation the rights to use, copy, modify, merge, publish, 14 | distribute, sublicense, and/or sell copies of the Software, and to 15 | permit persons to whom the Software is furnished to do so, subject to 16 | the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be 19 | included in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 25 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 26 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 27 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | */ 29 | 30 | +function (window, jasmine, $) { "use strict"; 31 | 32 | jasmine.spiedEventsKey = function (selector, eventName) { 33 | return [$(selector).selector, eventName].toString() 34 | } 35 | 36 | jasmine.getFixtures = function () { 37 | return jasmine.currentFixtures_ = jasmine.currentFixtures_ || new jasmine.Fixtures() 38 | } 39 | 40 | jasmine.getStyleFixtures = function () { 41 | return jasmine.currentStyleFixtures_ = jasmine.currentStyleFixtures_ || new jasmine.StyleFixtures() 42 | } 43 | 44 | jasmine.Fixtures = function () { 45 | this.containerId = 'jasmine-fixtures' 46 | this.fixturesCache_ = {} 47 | this.fixturesPath = 'spec/javascripts/fixtures' 48 | } 49 | 50 | jasmine.Fixtures.prototype.set = function (html) { 51 | this.cleanUp() 52 | return this.createContainer_(html) 53 | } 54 | 55 | jasmine.Fixtures.prototype.appendSet= function (html) { 56 | this.addToContainer_(html) 57 | } 58 | 59 | jasmine.Fixtures.prototype.preload = function () { 60 | this.read.apply(this, arguments) 61 | } 62 | 63 | jasmine.Fixtures.prototype.load = function () { 64 | this.cleanUp() 65 | this.createContainer_(this.read.apply(this, arguments)) 66 | } 67 | 68 | jasmine.Fixtures.prototype.appendLoad = function () { 69 | this.addToContainer_(this.read.apply(this, arguments)) 70 | } 71 | 72 | jasmine.Fixtures.prototype.read = function () { 73 | var htmlChunks = [] 74 | , fixtureUrls = arguments 75 | 76 | for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) { 77 | htmlChunks.push(this.getFixtureHtml_(fixtureUrls[urlIndex])) 78 | } 79 | 80 | return htmlChunks.join('') 81 | } 82 | 83 | jasmine.Fixtures.prototype.clearCache = function () { 84 | this.fixturesCache_ = {} 85 | } 86 | 87 | jasmine.Fixtures.prototype.cleanUp = function () { 88 | $('#' + this.containerId).remove() 89 | } 90 | 91 | jasmine.Fixtures.prototype.sandbox = function (attributes) { 92 | var attributesToSet = attributes || {} 93 | return $('
').attr(attributesToSet) 94 | } 95 | 96 | jasmine.Fixtures.prototype.createContainer_ = function (html) { 97 | var container = $('
') 98 | .attr('id', this.containerId) 99 | .html(html) 100 | 101 | $(document.body).append(container) 102 | return container 103 | } 104 | 105 | jasmine.Fixtures.prototype.addToContainer_ = function (html){ 106 | var container = $(document.body).find('#'+this.containerId).append(html) 107 | 108 | if (!container.length) { 109 | this.createContainer_(html) 110 | } 111 | } 112 | 113 | jasmine.Fixtures.prototype.getFixtureHtml_ = function (url) { 114 | if (typeof this.fixturesCache_[url] === 'undefined') { 115 | this.loadFixtureIntoCache_(url) 116 | } 117 | return this.fixturesCache_[url] 118 | } 119 | 120 | jasmine.Fixtures.prototype.loadFixtureIntoCache_ = function (relativeUrl) { 121 | var self = this 122 | , url = this.makeFixtureUrl_(relativeUrl) 123 | , htmlText = '' 124 | , request = $.ajax({ 125 | async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded 126 | cache: false, 127 | url: url, 128 | dataType: 'html', 129 | success: function (data, status, $xhr) { 130 | htmlText = $xhr.responseText 131 | } 132 | }).fail(function ($xhr, status, err) { 133 | throw new Error('Fixture could not be loaded: ' + url + ' (status: ' + status + ', message: ' + err.message + ')') 134 | }) 135 | 136 | var scripts = $($.parseHTML(htmlText, true)).find('script[src]') || []; 137 | 138 | scripts.each(function(){ 139 | $.ajax({ 140 | async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded 141 | cache: false, 142 | dataType: 'script', 143 | url: $(this).attr('src'), 144 | success: function (data, status, $xhr) { 145 | htmlText += '' 146 | }, 147 | error: function ($xhr, status, err) { 148 | throw new Error('Script could not be loaded: ' + url + ' (status: ' + status + ', message: ' + err.message + ')') 149 | } 150 | }); 151 | }) 152 | 153 | self.fixturesCache_[relativeUrl] = htmlText; 154 | } 155 | 156 | jasmine.Fixtures.prototype.makeFixtureUrl_ = function (relativeUrl){ 157 | return this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl 158 | } 159 | 160 | jasmine.Fixtures.prototype.proxyCallTo_ = function (methodName, passedArguments) { 161 | return this[methodName].apply(this, passedArguments) 162 | } 163 | 164 | 165 | jasmine.StyleFixtures = function () { 166 | this.fixturesCache_ = {} 167 | this.fixturesNodes_ = [] 168 | this.fixturesPath = 'spec/javascripts/fixtures' 169 | } 170 | 171 | jasmine.StyleFixtures.prototype.set = function (css) { 172 | this.cleanUp() 173 | this.createStyle_(css) 174 | } 175 | 176 | jasmine.StyleFixtures.prototype.appendSet = function (css) { 177 | this.createStyle_(css) 178 | } 179 | 180 | jasmine.StyleFixtures.prototype.preload = function () { 181 | this.read_.apply(this, arguments) 182 | } 183 | 184 | jasmine.StyleFixtures.prototype.load = function () { 185 | this.cleanUp() 186 | this.createStyle_(this.read_.apply(this, arguments)) 187 | } 188 | 189 | jasmine.StyleFixtures.prototype.appendLoad = function () { 190 | this.createStyle_(this.read_.apply(this, arguments)) 191 | } 192 | 193 | jasmine.StyleFixtures.prototype.cleanUp = function () { 194 | while(this.fixturesNodes_.length) { 195 | this.fixturesNodes_.pop().remove() 196 | } 197 | } 198 | 199 | jasmine.StyleFixtures.prototype.createStyle_ = function (html) { 200 | var styleText = $('
').html(html).text() 201 | , style = $('') 202 | 203 | this.fixturesNodes_.push(style) 204 | $('head').append(style) 205 | } 206 | 207 | jasmine.StyleFixtures.prototype.clearCache = jasmine.Fixtures.prototype.clearCache 208 | jasmine.StyleFixtures.prototype.read_ = jasmine.Fixtures.prototype.read 209 | jasmine.StyleFixtures.prototype.getFixtureHtml_ = jasmine.Fixtures.prototype.getFixtureHtml_ 210 | jasmine.StyleFixtures.prototype.loadFixtureIntoCache_ = jasmine.Fixtures.prototype.loadFixtureIntoCache_ 211 | jasmine.StyleFixtures.prototype.makeFixtureUrl_ = jasmine.Fixtures.prototype.makeFixtureUrl_ 212 | jasmine.StyleFixtures.prototype.proxyCallTo_ = jasmine.Fixtures.prototype.proxyCallTo_ 213 | 214 | jasmine.getJSONFixtures = function () { 215 | return jasmine.currentJSONFixtures_ = jasmine.currentJSONFixtures_ || new jasmine.JSONFixtures() 216 | } 217 | 218 | jasmine.JSONFixtures = function () { 219 | this.fixturesCache_ = {} 220 | this.fixturesPath = 'spec/javascripts/fixtures/json' 221 | } 222 | 223 | jasmine.JSONFixtures.prototype.load = function () { 224 | this.read.apply(this, arguments) 225 | return this.fixturesCache_ 226 | } 227 | 228 | jasmine.JSONFixtures.prototype.read = function () { 229 | var fixtureUrls = arguments 230 | 231 | for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) { 232 | this.getFixtureData_(fixtureUrls[urlIndex]) 233 | } 234 | 235 | return this.fixturesCache_ 236 | } 237 | 238 | jasmine.JSONFixtures.prototype.clearCache = function () { 239 | this.fixturesCache_ = {} 240 | } 241 | 242 | jasmine.JSONFixtures.prototype.getFixtureData_ = function (url) { 243 | if (!this.fixturesCache_[url]) this.loadFixtureIntoCache_(url) 244 | return this.fixturesCache_[url] 245 | } 246 | 247 | jasmine.JSONFixtures.prototype.loadFixtureIntoCache_ = function (relativeUrl) { 248 | var self = this 249 | , url = this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl 250 | 251 | $.ajax({ 252 | async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded 253 | cache: false, 254 | dataType: 'json', 255 | url: url, 256 | success: function (data) { 257 | self.fixturesCache_[relativeUrl] = data 258 | }, 259 | error: function ($xhr, status, err) { 260 | throw new Error('JSONFixture could not be loaded: ' + url + ' (status: ' + status + ', message: ' + err.message + ')') 261 | } 262 | }) 263 | } 264 | 265 | jasmine.JSONFixtures.prototype.proxyCallTo_ = function (methodName, passedArguments) { 266 | return this[methodName].apply(this, passedArguments) 267 | } 268 | 269 | jasmine.jQuery = function () {} 270 | 271 | jasmine.jQuery.browserTagCaseIndependentHtml = function (html) { 272 | return $('
').append(html).html() 273 | } 274 | 275 | jasmine.jQuery.elementToString = function (element) { 276 | return $(element).map(function () { return this.outerHTML; }).toArray().join(', ') 277 | } 278 | 279 | var data = { 280 | spiedEvents: {} 281 | , handlers: [] 282 | } 283 | 284 | jasmine.jQuery.events = { 285 | spyOn: function (selector, eventName) { 286 | var handler = function (e) { 287 | var calls = (typeof data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] !== 'undefined') ? data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)].calls : 0 288 | data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] = { 289 | args: jasmine.util.argsToArray(arguments), 290 | calls: ++calls 291 | } 292 | } 293 | 294 | $(selector).on(eventName, handler) 295 | data.handlers.push(handler) 296 | 297 | return { 298 | selector: selector, 299 | eventName: eventName, 300 | handler: handler, 301 | reset: function (){ 302 | delete data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] 303 | }, 304 | calls: { 305 | count: function () { 306 | return data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] ? 307 | data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)].calls : 0; 308 | }, 309 | any: function () { 310 | return data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] ? 311 | !!data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)].calls : false; 312 | } 313 | } 314 | } 315 | }, 316 | 317 | args: function (selector, eventName) { 318 | var actualArgs = data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)].args 319 | 320 | if (!actualArgs) { 321 | throw "There is no spy for " + eventName + " on " + selector.toString() + ". Make sure to create a spy using spyOnEvent." 322 | } 323 | 324 | return actualArgs 325 | }, 326 | 327 | wasTriggered: function (selector, eventName) { 328 | return !!(data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)]) 329 | }, 330 | 331 | wasTriggeredWith: function (selector, eventName, expectedArgs, util, customEqualityTesters) { 332 | var actualArgs = jasmine.jQuery.events.args(selector, eventName).slice(1) 333 | 334 | if (Object.prototype.toString.call(expectedArgs) !== '[object Array]') 335 | actualArgs = actualArgs[0] 336 | 337 | return util.equals(actualArgs, expectedArgs, customEqualityTesters) 338 | }, 339 | 340 | wasPrevented: function (selector, eventName) { 341 | var spiedEvent = data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] 342 | , args = (jasmine.util.isUndefined(spiedEvent)) ? {} : spiedEvent.args 343 | , e = args ? args[0] : undefined 344 | 345 | return e && e.isDefaultPrevented() 346 | }, 347 | 348 | wasStopped: function (selector, eventName) { 349 | var spiedEvent = data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] 350 | , args = (jasmine.util.isUndefined(spiedEvent)) ? {} : spiedEvent.args 351 | , e = args ? args[0] : undefined 352 | 353 | return e && e.isPropagationStopped() 354 | }, 355 | 356 | cleanUp: function () { 357 | data.spiedEvents = {} 358 | data.handlers = [] 359 | } 360 | } 361 | 362 | var hasProperty = function (actualValue, expectedValue) { 363 | if (expectedValue === undefined) 364 | return actualValue !== undefined 365 | 366 | return actualValue === expectedValue 367 | } 368 | 369 | beforeEach(function () { 370 | jasmine.addMatchers({ 371 | toHaveClass: function () { 372 | return { 373 | compare: function (actual, className) { 374 | return { pass: $(actual).hasClass(className) } 375 | } 376 | } 377 | }, 378 | 379 | toHaveCss: function () { 380 | return { 381 | compare: function (actual, css) { 382 | for (var prop in css){ 383 | var value = css[prop] 384 | // see issue #147 on gh 385 | ;if (value === 'auto' && $(actual).get(0).style[prop] === 'auto') continue 386 | if ($(actual).css(prop) !== value) return { pass: false } 387 | } 388 | return { pass: true } 389 | } 390 | } 391 | }, 392 | 393 | toBeVisible: function () { 394 | return { 395 | compare: function (actual) { 396 | return { pass: $(actual).is(':visible') } 397 | } 398 | } 399 | }, 400 | 401 | toBeHidden: function () { 402 | return { 403 | compare: function (actual) { 404 | return { pass: $(actual).is(':hidden') } 405 | } 406 | } 407 | }, 408 | 409 | toBeSelected: function () { 410 | return { 411 | compare: function (actual) { 412 | return { pass: $(actual).is(':selected') } 413 | } 414 | } 415 | }, 416 | 417 | toBeChecked: function () { 418 | return { 419 | compare: function (actual) { 420 | return { pass: $(actual).is(':checked') } 421 | } 422 | } 423 | }, 424 | 425 | toBeEmpty: function () { 426 | return { 427 | compare: function (actual) { 428 | return { pass: $(actual).is(':empty') } 429 | } 430 | } 431 | }, 432 | 433 | toBeInDOM: function () { 434 | return { 435 | compare: function (actual) { 436 | return { pass: $.contains(document.documentElement, $(actual)[0]) } 437 | } 438 | } 439 | }, 440 | 441 | toExist: function () { 442 | return { 443 | compare: function (actual) { 444 | return { pass: $(actual).length } 445 | } 446 | } 447 | }, 448 | 449 | toHaveLength: function () { 450 | return { 451 | compare: function (actual, length) { 452 | return { pass: $(actual).length === length } 453 | } 454 | } 455 | }, 456 | 457 | toHaveAttr: function () { 458 | return { 459 | compare: function (actual, attributeName, expectedAttributeValue) { 460 | return { pass: hasProperty($(actual).attr(attributeName), expectedAttributeValue) } 461 | } 462 | } 463 | }, 464 | 465 | toHaveProp: function () { 466 | return { 467 | compare: function (actual, propertyName, expectedPropertyValue) { 468 | return { pass: hasProperty($(actual).prop(propertyName), expectedPropertyValue) } 469 | } 470 | } 471 | }, 472 | 473 | toHaveId: function () { 474 | return { 475 | compare: function (actual, id) { 476 | return { pass: $(actual).attr('id') == id } 477 | } 478 | } 479 | }, 480 | 481 | toHaveHtml: function () { 482 | return { 483 | compare: function (actual, html) { 484 | return { pass: $(actual).html() == jasmine.jQuery.browserTagCaseIndependentHtml(html) } 485 | } 486 | } 487 | }, 488 | 489 | toContainHtml: function () { 490 | return { 491 | compare: function (actual, html) { 492 | var actualHtml = $(actual).html() 493 | , expectedHtml = jasmine.jQuery.browserTagCaseIndependentHtml(html) 494 | 495 | return { pass: (actualHtml.indexOf(expectedHtml) >= 0) } 496 | } 497 | } 498 | }, 499 | 500 | toHaveText: function () { 501 | return { 502 | compare: function (actual, text) { 503 | var actualText = $(actual).text() 504 | var trimmedText = $.trim(actualText) 505 | 506 | if (text && $.isFunction(text.test)) { 507 | return { pass: text.test(actualText) || text.test(trimmedText) } 508 | } else { 509 | return { pass: (actualText == text || trimmedText == text) } 510 | } 511 | } 512 | } 513 | }, 514 | 515 | toContainText: function () { 516 | return { 517 | compare: function (actual, text) { 518 | var trimmedText = $.trim($(actual).text()) 519 | 520 | if (text && $.isFunction(text.test)) { 521 | return { pass: text.test(trimmedText) } 522 | } else { 523 | return { pass: trimmedText.indexOf(text) != -1 } 524 | } 525 | } 526 | } 527 | }, 528 | 529 | toHaveValue: function () { 530 | return { 531 | compare: function (actual, value) { 532 | return { pass: $(actual).val() === value } 533 | } 534 | } 535 | }, 536 | 537 | toHaveData: function () { 538 | return { 539 | compare: function (actual, key, expectedValue) { 540 | return { pass: hasProperty($(actual).data(key), expectedValue) } 541 | } 542 | } 543 | }, 544 | 545 | toContainElement: function () { 546 | return { 547 | compare: function (actual, selector) { 548 | return { pass: $(actual).find(selector).length } 549 | } 550 | } 551 | }, 552 | 553 | toBeMatchedBy: function () { 554 | return { 555 | compare: function (actual, selector) { 556 | return { pass: $(actual).filter(selector).length } 557 | } 558 | } 559 | }, 560 | 561 | toBeDisabled: function () { 562 | return { 563 | compare: function (actual, selector) { 564 | return { pass: $(actual).is(':disabled') } 565 | } 566 | } 567 | }, 568 | 569 | toBeFocused: function (selector) { 570 | return { 571 | compare: function (actual, selector) { 572 | return { pass: $(actual)[0] === $(actual)[0].ownerDocument.activeElement } 573 | } 574 | } 575 | }, 576 | 577 | toHandle: function () { 578 | return { 579 | compare: function (actual, event) { 580 | if ( !actual || actual.length === 0 ) return { pass: false }; 581 | var events = $._data($(actual).get(0), "events") 582 | 583 | if (!events || !event || typeof event !== "string") { 584 | return { pass: false } 585 | } 586 | 587 | var namespaces = event.split(".") 588 | , eventType = namespaces.shift() 589 | , sortedNamespaces = namespaces.slice(0).sort() 590 | , namespaceRegExp = new RegExp("(^|\\.)" + sortedNamespaces.join("\\.(?:.*\\.)?") + "(\\.|$)") 591 | 592 | if (events[eventType] && namespaces.length) { 593 | for (var i = 0; i < events[eventType].length; i++) { 594 | var namespace = events[eventType][i].namespace 595 | 596 | if (namespaceRegExp.test(namespace)) 597 | return { pass: true } 598 | } 599 | } else { 600 | return { pass: (events[eventType] && events[eventType].length > 0) } 601 | } 602 | 603 | return { pass: false } 604 | } 605 | } 606 | }, 607 | 608 | toHandleWith: function () { 609 | return { 610 | compare: function (actual, eventName, eventHandler) { 611 | if ( !actual || actual.length === 0 ) return { pass: false }; 612 | var normalizedEventName = eventName.split('.')[0] 613 | , stack = $._data($(actual).get(0), "events")[normalizedEventName] 614 | 615 | for (var i = 0; i < stack.length; i++) { 616 | if (stack[i].handler == eventHandler) return { pass: true } 617 | } 618 | 619 | return { pass: false } 620 | } 621 | } 622 | }, 623 | 624 | toHaveBeenTriggeredOn: function () { 625 | return { 626 | compare: function (actual, selector) { 627 | var result = { pass: jasmine.jQuery.events.wasTriggered(selector, actual) } 628 | 629 | result.message = result.pass ? 630 | "Expected event " + $(actual) + " not to have been triggered on " + selector : 631 | "Expected event " + $(actual) + " to have been triggered on " + selector 632 | 633 | return result; 634 | } 635 | } 636 | }, 637 | 638 | toHaveBeenTriggered: function (){ 639 | return { 640 | compare: function (actual) { 641 | var eventName = actual.eventName 642 | , selector = actual.selector 643 | , result = { pass: jasmine.jQuery.events.wasTriggered(selector, eventName) } 644 | 645 | result.message = result.pass ? 646 | "Expected event " + eventName + " not to have been triggered on " + selector : 647 | "Expected event " + eventName + " to have been triggered on " + selector 648 | 649 | return result 650 | } 651 | } 652 | }, 653 | 654 | toHaveBeenTriggeredOnAndWith: function (j$, customEqualityTesters) { 655 | return { 656 | compare: function (actual, selector, expectedArgs) { 657 | var wasTriggered = jasmine.jQuery.events.wasTriggered(selector, actual) 658 | , result = { pass: wasTriggered && jasmine.jQuery.events.wasTriggeredWith(selector, actual, expectedArgs, j$, customEqualityTesters) } 659 | 660 | if (wasTriggered) { 661 | var actualArgs = jasmine.jQuery.events.args(selector, actual, expectedArgs)[1] 662 | result.message = result.pass ? 663 | "Expected event " + actual + " not to have been triggered with " + jasmine.pp(expectedArgs) + " but it was triggered with " + jasmine.pp(actualArgs) : 664 | "Expected event " + actual + " to have been triggered with " + jasmine.pp(expectedArgs) + " but it was triggered with " + jasmine.pp(actualArgs) 665 | 666 | } else { 667 | // todo check on this 668 | result.message = result.pass ? 669 | "Expected event " + actual + " not to have been triggered on " + selector : 670 | "Expected event " + actual + " to have been triggered on " + selector 671 | } 672 | 673 | return result 674 | } 675 | } 676 | }, 677 | 678 | toHaveBeenPreventedOn: function () { 679 | return { 680 | compare: function (actual, selector) { 681 | var result = { pass: jasmine.jQuery.events.wasPrevented(selector, actual) } 682 | 683 | result.message = result.pass ? 684 | "Expected event " + actual + " not to have been prevented on " + selector : 685 | "Expected event " + actual + " to have been prevented on " + selector 686 | 687 | return result 688 | } 689 | } 690 | }, 691 | 692 | toHaveBeenPrevented: function () { 693 | return { 694 | compare: function (actual) { 695 | var eventName = actual.eventName 696 | , selector = actual.selector 697 | , result = { pass: jasmine.jQuery.events.wasPrevented(selector, eventName) } 698 | 699 | result.message = result.pass ? 700 | "Expected event " + eventName + " not to have been prevented on " + selector : 701 | "Expected event " + eventName + " to have been prevented on " + selector 702 | 703 | return result 704 | } 705 | } 706 | }, 707 | 708 | toHaveBeenStoppedOn: function () { 709 | return { 710 | compare: function (actual, selector) { 711 | var result = { pass: jasmine.jQuery.events.wasStopped(selector, actual) } 712 | 713 | result.message = result.pass ? 714 | "Expected event " + actual + " not to have been stopped on " + selector : 715 | "Expected event " + actual + " to have been stopped on " + selector 716 | 717 | return result; 718 | } 719 | } 720 | }, 721 | 722 | toHaveBeenStopped: function () { 723 | return { 724 | compare: function (actual) { 725 | var eventName = actual.eventName 726 | , selector = actual.selector 727 | , result = { pass: jasmine.jQuery.events.wasStopped(selector, eventName) } 728 | 729 | result.message = result.pass ? 730 | "Expected event " + eventName + " not to have been stopped on " + selector : 731 | "Expected event " + eventName + " to have been stopped on " + selector 732 | 733 | return result 734 | } 735 | } 736 | } 737 | }) 738 | 739 | jasmine.getEnv().addCustomEqualityTester(function(a, b) { 740 | if (a && b) { 741 | if (a instanceof $ || jasmine.isDomNode(a)) { 742 | var $a = $(a) 743 | 744 | if (b instanceof $) 745 | return $a.length == b.length && a.is(b) 746 | 747 | return $a.is(b); 748 | } 749 | 750 | if (b instanceof $ || jasmine.isDomNode(b)) { 751 | var $b = $(b) 752 | 753 | if (a instanceof $) 754 | return a.length == $b.length && $b.is(a) 755 | 756 | return $(b).is(a); 757 | } 758 | } 759 | }) 760 | 761 | jasmine.getEnv().addCustomEqualityTester(function (a, b) { 762 | if (a instanceof $ && b instanceof $ && a.size() == b.size()) 763 | return a.is(b) 764 | }) 765 | }) 766 | 767 | afterEach(function () { 768 | jasmine.getFixtures().cleanUp() 769 | jasmine.getStyleFixtures().cleanUp() 770 | jasmine.jQuery.events.cleanUp() 771 | }) 772 | 773 | window.readFixtures = function () { 774 | return jasmine.getFixtures().proxyCallTo_('read', arguments) 775 | } 776 | 777 | window.preloadFixtures = function () { 778 | jasmine.getFixtures().proxyCallTo_('preload', arguments) 779 | } 780 | 781 | window.loadFixtures = function () { 782 | jasmine.getFixtures().proxyCallTo_('load', arguments) 783 | } 784 | 785 | window.appendLoadFixtures = function () { 786 | jasmine.getFixtures().proxyCallTo_('appendLoad', arguments) 787 | } 788 | 789 | window.setFixtures = function (html) { 790 | return jasmine.getFixtures().proxyCallTo_('set', arguments) 791 | } 792 | 793 | window.appendSetFixtures = function () { 794 | jasmine.getFixtures().proxyCallTo_('appendSet', arguments) 795 | } 796 | 797 | window.sandbox = function (attributes) { 798 | return jasmine.getFixtures().sandbox(attributes) 799 | } 800 | 801 | window.spyOnEvent = function (selector, eventName) { 802 | return jasmine.jQuery.events.spyOn(selector, eventName) 803 | } 804 | 805 | window.preloadStyleFixtures = function () { 806 | jasmine.getStyleFixtures().proxyCallTo_('preload', arguments) 807 | } 808 | 809 | window.loadStyleFixtures = function () { 810 | jasmine.getStyleFixtures().proxyCallTo_('load', arguments) 811 | } 812 | 813 | window.appendLoadStyleFixtures = function () { 814 | jasmine.getStyleFixtures().proxyCallTo_('appendLoad', arguments) 815 | } 816 | 817 | window.setStyleFixtures = function (html) { 818 | jasmine.getStyleFixtures().proxyCallTo_('set', arguments) 819 | } 820 | 821 | window.appendSetStyleFixtures = function (html) { 822 | jasmine.getStyleFixtures().proxyCallTo_('appendSet', arguments) 823 | } 824 | 825 | window.loadJSONFixtures = function () { 826 | return jasmine.getJSONFixtures().proxyCallTo_('load', arguments) 827 | } 828 | 829 | window.getJSONFixture = function (url) { 830 | return jasmine.getJSONFixtures().proxyCallTo_('read', arguments)[url] 831 | } 832 | }(window, window.jasmine, window.jQuery); 833 | --------------------------------------------------------------------------------