├── .gitattributes ├── .gitignore ├── background.js ├── blank.html ├── fonts ├── YanoneKaffeesatz-Regular-webfont.eot ├── YanoneKaffeesatz-Regular-webfont.svg ├── YanoneKaffeesatz-Regular-webfont.ttf ├── YanoneKaffeesatz-Regular-webfont.woff ├── satellite-webfont.eot ├── satellite-webfont.svg ├── satellite-webfont.ttf └── satellite-webfont.woff ├── images ├── background.png ├── hero-images │ ├── 001.png │ ├── 002.png │ ├── 003.png │ ├── 004.png │ ├── 005.png │ ├── 006.png │ ├── 007.png │ ├── 008.png │ ├── 009.png │ ├── 010.png │ ├── 011.png │ └── 012.png ├── icon_128.png ├── icon_16.png ├── icon_48.png ├── ioCinemaIcon.png ├── loading.gif ├── printTicketsIcon.png ├── purchaseTicketsIcon.png └── screenshots │ ├── 1280x800.png │ └── 440x280.png ├── manifest.json ├── scripts ├── data.js ├── data.json ├── lib │ ├── StackBlur.js │ ├── jquery-2.1.1.min.js │ ├── sandbox-scripts.js │ ├── sandbox-stripe.js │ ├── sandbox.js │ ├── slider.js │ ├── spreadsheet.gs │ ├── spreadsheet.js │ ├── stillthere.js │ ├── stripe.js │ └── swiper.js ├── main.js ├── objects │ ├── movie.js │ ├── pricing.js │ ├── receipt.js │ ├── session.js │ ├── showing.js │ ├── theater.js │ ├── ticket.js │ └── tickettype.js ├── rotten.js ├── sandbox-rotten.js └── sandbox-scripts.html ├── styles ├── main.css ├── print.css ├── slider.css └── stillthere.css └── window.html /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # ========================= 18 | # Operating System Files 19 | # ========================= 20 | 21 | # OSX 22 | # ========================= 23 | 24 | .DS_Store 25 | .AppleDouble 26 | .LSOverride 27 | 28 | # Icon must ends with two \r. 29 | Icon 30 | 31 | 32 | # Thumbnails 33 | ._* 34 | 35 | # Files that might appear on external disk 36 | .Spotlight-V100 37 | .Trashes 38 | /resources/ 39 | /nbproject/ -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | (function(window, chrome) { 2 | //Background Scope 3 | var background = {}; 4 | window.background = background; 5 | 6 | //Create window and add listeners 7 | chrome.app.runtime.onLaunched.addListener(function() { 8 | chrome.app.window.create('window.html', { 9 | 'state': 'fullscreen', 10 | 'bounds': { 11 | 'width': 1280, 12 | 'height': 1024 13 | } 14 | }, function(window) { 15 | window.onClosed.addListener(function() { 16 | background.setKeyboard(false); 17 | chrome.power.releaseKeepAwake(); 18 | }); 19 | background.setKeyboard(true); 20 | chrome.power.requestKeepAwake('display'); 21 | }); 22 | }); 23 | 24 | /** 25 | * Enables or disables the on-screen keyboard. 26 | *
27 | * If enable is true, the virtual keyboard of the ChromeOS 28 | * accessibility features will be enabled. If false, this function will 29 | * return the setting to its original value. 30 | * @param {boolean} enable true to enable, or false to reset 31 | * @returns {undefined} 32 | */ 33 | background.setKeyboard = function(enable) { 34 | if (chrome.accessibilityFeatures) { 35 | if (enable) { 36 | chrome.accessibilityFeatures.virtualKeyboard.set({ 37 | value: enable 38 | }); 39 | } 40 | else { 41 | chrome.accessibilityFeatures.virtualKeyboard.clear({}); 42 | } 43 | } 44 | }; 45 | })(window, chrome); -------------------------------------------------------------------------------- /blank.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |214 | * The Array will be empty if no Objects were found. 215 | * @param {Array} array the Array to search 216 | * @param {string} property the property name to look for 217 | * @param {Object} value the property value to match 218 | * @returns {Array} an Array of Objects found 219 | */ 220 | data.getArrayByProperty = function(array, property, value) { 221 | var results = []; 222 | for (var i = 0; i < array.length; i++) { 223 | //Loosely compare the value 224 | if (array[i][property] == value) { 225 | results.push(array[i]); 226 | } 227 | } 228 | return results; 229 | }; 230 | 231 | /** 232 | * Saves the given Receipt object to a spreadsheet. 233 | * @param {Receipt|object} receipt the Receipt or receipt storage object to save 234 | * @returns {undefined} 235 | */ 236 | data.saveReceipt = function(receipt) { 237 | if (receipt && receipt.createStorageObject) { 238 | receipt = receipt.createStorageObject(); 239 | } 240 | var row = [ 241 | JSON.stringify(receipt) 242 | ]; 243 | spreadsheet.appendRow(row, function(message, status) { 244 | console.log('Receipt Save Result: (' + status + ') ' + message); 245 | }); 246 | }; 247 | /** 248 | * Retrieves a Receipt object from the spreadsheet by its ID. 249 | * @param {string|number} id the ID of the Receipt to retrieve 250 | * @param {function(Receipt)} callback called upon successfully locating the 251 | * Receipt, the argument may be undefined if a Receipt is not found 252 | * @returns {undefined} 253 | */ 254 | data.getReceiptById = function(id, callback) { 255 | data.getReceiptByProperty('id', id, callback); 256 | }; 257 | /** 258 | * Retrieves a Receipt object from the spreadsheet by the Card object used 259 | * to create the Receipt. 260 | * @param {Card} card the Card used with the Receipt to retrieve 261 | * @param {function(Receipt)} callback called upon successfully locating the 262 | * Receipt, the argument may be undefined if a Receipt is not found 263 | * @returns {undefined} 264 | */ 265 | data.getReceiptByCard = function(card, callback) { 266 | var hash = swiper.generateCardHash(card); 267 | data.getReceiptByProperty('cardHash', hash, callback); 268 | }; 269 | /** 270 | * Retrieves a Receipt from the spreadsheet whose property matches the 271 | * provided value, or undefined if a Receipt could not be found. 272 | * @param {string} property the property name to look for 273 | * @param {Object} value the property value to match 274 | * @param {function(Receipt)} callback called upon successfully locating the 275 | * Receipt, the argument may be undefined if a Receipt is not found 276 | * @returns {undefined} 277 | */ 278 | data.getReceiptByProperty = function(property, value, callback) { 279 | spreadsheet.getAllRows(function(rows) { 280 | rows.reverse(); 281 | for (var i = 0; i < rows.length; i++) { 282 | var receipt = JSON.parse(rows[i]); 283 | //Loosely compare the value 284 | if (receipt[property] == value) { 285 | callback(receipt); 286 | return; 287 | } 288 | } 289 | callback(); 290 | }); 291 | }; -------------------------------------------------------------------------------- /scripts/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "ticketTypes": [ 3 | { 4 | "id": 1, 5 | "name": "Adult" 6 | }, 7 | { 8 | "id": 2, 9 | "name": "Child" 10 | }, 11 | { 12 | "id": 3, 13 | "name": "Senior" 14 | }, 15 | { 16 | "id": 4, 17 | "name": "Student" 18 | } 19 | ], 20 | "pricings": [ 21 | { 22 | "id": 1, 23 | "name": "Standard", 24 | "tickets": [ 25 | { 26 | "id": 1, "price": 10.00 27 | }, 28 | { 29 | "id": 2, "price": 7.00 30 | }, 31 | { 32 | "id": 4, "price": 8.00 33 | }, 34 | { 35 | "id": 3, "price": 6.00 36 | } 37 | ] 38 | }, 39 | { 40 | "id": 2, 41 | "name": "3D", 42 | "tickets": [ 43 | { 44 | "id": 1, "price": 12.00 45 | }, 46 | { 47 | "id": 2, "price": 9.00 48 | }, 49 | { 50 | "id": 4, "price": 10.00 51 | }, 52 | { 53 | "id": 3, "price": 8.00 54 | } 55 | ] 56 | }, 57 | { 58 | "id": 3, 59 | "name": "IMAX", 60 | "tickets": [ 61 | { 62 | "id": 1, "price": 15.00 63 | }, 64 | { 65 | "id": 2, "price": 12.00 66 | }, 67 | { 68 | "id": 4, "price": 13.00 69 | }, 70 | { 71 | "id": 3, "price": 11.00 72 | } 73 | ] 74 | }, 75 | { 76 | "id": 4, 77 | "name": "IMAX 3D", 78 | "tickets": [ 79 | { 80 | "id": 1, "price": 17.00 81 | }, 82 | { 83 | "id": 2, "price": 14.00 84 | }, 85 | { 86 | "id": 4, "price": 15.00 87 | }, 88 | { 89 | "id": 3, "price": 13.00 90 | } 91 | ] 92 | } 93 | ], 94 | "theaters": [ 95 | { 96 | "id": 1, 97 | "name": "Theater 1", 98 | "pricing": "Standard" 99 | }, 100 | { 101 | "id": 2, 102 | "name": "Theater 2", 103 | "pricing": "3D" 104 | }, 105 | { 106 | "id": 3, 107 | "name": "Theater 3", 108 | "pricing": "IMAX" 109 | }, 110 | { 111 | "id": 4, 112 | "name": "Theater 4", 113 | "pricing": "IMAX 3D" 114 | } 115 | ] 116 | } -------------------------------------------------------------------------------- /scripts/lib/StackBlur.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | StackBlur - a fast almost Gaussian Blur For Canvas 4 | 5 | Version: 0.5 6 | Author: Mario Klingemann 7 | Contact: mario@quasimondo.com 8 | Website: http://www.quasimondo.com/StackBlurForCanvas 9 | Twitter: @quasimondo 10 | 11 | In case you find this class useful - especially in commercial projects - 12 | I am not totally unhappy for a small donation to my PayPal account 13 | mario@quasimondo.de 14 | 15 | Or support me on flattr: 16 | https://flattr.com/thing/72791/StackBlur-a-fast-almost-Gaussian-Blur-Effect-for-CanvasJavascript 17 | 18 | Copyright (c) 2010 Mario Klingemann 19 | 20 | Permission is hereby granted, free of charge, to any person 21 | obtaining a copy of this software and associated documentation 22 | files (the "Software"), to deal in the Software without 23 | restriction, including without limitation the rights to use, 24 | copy, modify, merge, publish, distribute, sublicense, and/or sell 25 | copies of the Software, and to permit persons to whom the 26 | Software is furnished to do so, subject to the following 27 | conditions: 28 | 29 | The above copyright notice and this permission notice shall be 30 | included in all copies or substantial portions of the Software. 31 | 32 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 33 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 34 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 35 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 36 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 37 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 38 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 39 | OTHER DEALINGS IN THE SOFTWARE. 40 | */ 41 | 42 | var mul_table = [ 43 | 512,512,456,512,328,456,335,512,405,328,271,456,388,335,292,512, 44 | 454,405,364,328,298,271,496,456,420,388,360,335,312,292,273,512, 45 | 482,454,428,405,383,364,345,328,312,298,284,271,259,496,475,456, 46 | 437,420,404,388,374,360,347,335,323,312,302,292,282,273,265,512, 47 | 497,482,468,454,441,428,417,405,394,383,373,364,354,345,337,328, 48 | 320,312,305,298,291,284,278,271,265,259,507,496,485,475,465,456, 49 | 446,437,428,420,412,404,396,388,381,374,367,360,354,347,341,335, 50 | 329,323,318,312,307,302,297,292,287,282,278,273,269,265,261,512, 51 | 505,497,489,482,475,468,461,454,447,441,435,428,422,417,411,405, 52 | 399,394,389,383,378,373,368,364,359,354,350,345,341,337,332,328, 53 | 324,320,316,312,309,305,301,298,294,291,287,284,281,278,274,271, 54 | 268,265,262,259,257,507,501,496,491,485,480,475,470,465,460,456, 55 | 451,446,442,437,433,428,424,420,416,412,408,404,400,396,392,388, 56 | 385,381,377,374,370,367,363,360,357,354,350,347,344,341,338,335, 57 | 332,329,326,323,320,318,315,312,310,307,304,302,299,297,294,292, 58 | 289,287,285,282,280,278,275,273,271,269,267,265,263,261,259]; 59 | 60 | 61 | var shg_table = [ 62 | 9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17, 63 | 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, 64 | 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, 65 | 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, 66 | 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 67 | 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 68 | 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 69 | 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, 70 | 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 71 | 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 72 | 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 73 | 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 74 | 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 75 | 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 76 | 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 77 | 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24 ]; 78 | 79 | function stackBlurImage( imageID, canvasID, radius, blurAlphaChannel ) 80 | { 81 | 82 | var img = document.getElementById( imageID ); 83 | var w = img.naturalWidth; 84 | var h = img.naturalHeight; 85 | 86 | var canvas = document.getElementById( canvasID ); 87 | 88 | canvas.style.width = w + "px"; 89 | canvas.style.height = h + "px"; 90 | canvas.width = w; 91 | canvas.height = h; 92 | 93 | var context = canvas.getContext("2d"); 94 | context.clearRect( 0, 0, w, h ); 95 | context.drawImage( img, 0, 0 ); 96 | 97 | if ( isNaN(radius) || radius < 1 ) return; 98 | 99 | if ( blurAlphaChannel ) 100 | stackBlurCanvasRGBA( canvasID, 0, 0, w, h, radius ); 101 | else 102 | stackBlurCanvasRGB( canvasID, 0, 0, w, h, radius ); 103 | } 104 | 105 | 106 | function stackBlurCanvasRGBA( id, top_x, top_y, width, height, radius ) 107 | { 108 | if ( isNaN(radius) || radius < 1 ) return; 109 | radius |= 0; 110 | 111 | var canvas = document.getElementById( id ); 112 | var context = canvas.getContext("2d"); 113 | var imageData; 114 | 115 | try { 116 | try { 117 | imageData = context.getImageData( top_x, top_y, width, height ); 118 | } catch(e) { 119 | 120 | // NOTE: this part is supposedly only needed if you want to work with local files 121 | // so it might be okay to remove the whole try/catch block and just use 122 | // imageData = context.getImageData( top_x, top_y, width, height ); 123 | try { 124 | netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead"); 125 | imageData = context.getImageData( top_x, top_y, width, height ); 126 | } catch(e) { 127 | alert("Cannot access local image"); 128 | throw new Error("unable to access local image data: " + e); 129 | return; 130 | } 131 | } 132 | } catch(e) { 133 | alert("Cannot access image"); 134 | throw new Error("unable to access image data: " + e); 135 | } 136 | 137 | var pixels = imageData.data; 138 | 139 | var x, y, i, p, yp, yi, yw, r_sum, g_sum, b_sum, a_sum, 140 | r_out_sum, g_out_sum, b_out_sum, a_out_sum, 141 | r_in_sum, g_in_sum, b_in_sum, a_in_sum, 142 | pr, pg, pb, pa, rbs; 143 | 144 | var div = radius + radius + 1; 145 | var w4 = width << 2; 146 | var widthMinus1 = width - 1; 147 | var heightMinus1 = height - 1; 148 | var radiusPlus1 = radius + 1; 149 | var sumFactor = radiusPlus1 * ( radiusPlus1 + 1 ) / 2; 150 | 151 | var stackStart = new BlurStack(); 152 | var stack = stackStart; 153 | for ( i = 1; i < div; i++ ) 154 | { 155 | stack = stack.next = new BlurStack(); 156 | if ( i == radiusPlus1 ) var stackEnd = stack; 157 | } 158 | stack.next = stackStart; 159 | var stackIn = null; 160 | var stackOut = null; 161 | 162 | yw = yi = 0; 163 | 164 | var mul_sum = mul_table[radius]; 165 | var shg_sum = shg_table[radius]; 166 | 167 | for ( y = 0; y < height; y++ ) 168 | { 169 | r_in_sum = g_in_sum = b_in_sum = a_in_sum = r_sum = g_sum = b_sum = a_sum = 0; 170 | 171 | r_out_sum = radiusPlus1 * ( pr = pixels[yi] ); 172 | g_out_sum = radiusPlus1 * ( pg = pixels[yi+1] ); 173 | b_out_sum = radiusPlus1 * ( pb = pixels[yi+2] ); 174 | a_out_sum = radiusPlus1 * ( pa = pixels[yi+3] ); 175 | 176 | r_sum += sumFactor * pr; 177 | g_sum += sumFactor * pg; 178 | b_sum += sumFactor * pb; 179 | a_sum += sumFactor * pa; 180 | 181 | stack = stackStart; 182 | 183 | for( i = 0; i < radiusPlus1; i++ ) 184 | { 185 | stack.r = pr; 186 | stack.g = pg; 187 | stack.b = pb; 188 | stack.a = pa; 189 | stack = stack.next; 190 | } 191 | 192 | for( i = 1; i < radiusPlus1; i++ ) 193 | { 194 | p = yi + (( widthMinus1 < i ? widthMinus1 : i ) << 2 ); 195 | r_sum += ( stack.r = ( pr = pixels[p])) * ( rbs = radiusPlus1 - i ); 196 | g_sum += ( stack.g = ( pg = pixels[p+1])) * rbs; 197 | b_sum += ( stack.b = ( pb = pixels[p+2])) * rbs; 198 | a_sum += ( stack.a = ( pa = pixels[p+3])) * rbs; 199 | 200 | r_in_sum += pr; 201 | g_in_sum += pg; 202 | b_in_sum += pb; 203 | a_in_sum += pa; 204 | 205 | stack = stack.next; 206 | } 207 | 208 | 209 | stackIn = stackStart; 210 | stackOut = stackEnd; 211 | for ( x = 0; x < width; x++ ) 212 | { 213 | pixels[yi+3] = pa = (a_sum * mul_sum) >> shg_sum; 214 | if ( pa != 0 ) 215 | { 216 | pa = 255 / pa; 217 | pixels[yi] = ((r_sum * mul_sum) >> shg_sum) * pa; 218 | pixels[yi+1] = ((g_sum * mul_sum) >> shg_sum) * pa; 219 | pixels[yi+2] = ((b_sum * mul_sum) >> shg_sum) * pa; 220 | } else { 221 | pixels[yi] = pixels[yi+1] = pixels[yi+2] = 0; 222 | } 223 | 224 | r_sum -= r_out_sum; 225 | g_sum -= g_out_sum; 226 | b_sum -= b_out_sum; 227 | a_sum -= a_out_sum; 228 | 229 | r_out_sum -= stackIn.r; 230 | g_out_sum -= stackIn.g; 231 | b_out_sum -= stackIn.b; 232 | a_out_sum -= stackIn.a; 233 | 234 | p = ( yw + ( ( p = x + radius + 1 ) < widthMinus1 ? p : widthMinus1 ) ) << 2; 235 | 236 | r_in_sum += ( stackIn.r = pixels[p]); 237 | g_in_sum += ( stackIn.g = pixels[p+1]); 238 | b_in_sum += ( stackIn.b = pixels[p+2]); 239 | a_in_sum += ( stackIn.a = pixels[p+3]); 240 | 241 | r_sum += r_in_sum; 242 | g_sum += g_in_sum; 243 | b_sum += b_in_sum; 244 | a_sum += a_in_sum; 245 | 246 | stackIn = stackIn.next; 247 | 248 | r_out_sum += ( pr = stackOut.r ); 249 | g_out_sum += ( pg = stackOut.g ); 250 | b_out_sum += ( pb = stackOut.b ); 251 | a_out_sum += ( pa = stackOut.a ); 252 | 253 | r_in_sum -= pr; 254 | g_in_sum -= pg; 255 | b_in_sum -= pb; 256 | a_in_sum -= pa; 257 | 258 | stackOut = stackOut.next; 259 | 260 | yi += 4; 261 | } 262 | yw += width; 263 | } 264 | 265 | 266 | for ( x = 0; x < width; x++ ) 267 | { 268 | g_in_sum = b_in_sum = a_in_sum = r_in_sum = g_sum = b_sum = a_sum = r_sum = 0; 269 | 270 | yi = x << 2; 271 | r_out_sum = radiusPlus1 * ( pr = pixels[yi]); 272 | g_out_sum = radiusPlus1 * ( pg = pixels[yi+1]); 273 | b_out_sum = radiusPlus1 * ( pb = pixels[yi+2]); 274 | a_out_sum = radiusPlus1 * ( pa = pixels[yi+3]); 275 | 276 | r_sum += sumFactor * pr; 277 | g_sum += sumFactor * pg; 278 | b_sum += sumFactor * pb; 279 | a_sum += sumFactor * pa; 280 | 281 | stack = stackStart; 282 | 283 | for( i = 0; i < radiusPlus1; i++ ) 284 | { 285 | stack.r = pr; 286 | stack.g = pg; 287 | stack.b = pb; 288 | stack.a = pa; 289 | stack = stack.next; 290 | } 291 | 292 | yp = width; 293 | 294 | for( i = 1; i <= radius; i++ ) 295 | { 296 | yi = ( yp + x ) << 2; 297 | 298 | r_sum += ( stack.r = ( pr = pixels[yi])) * ( rbs = radiusPlus1 - i ); 299 | g_sum += ( stack.g = ( pg = pixels[yi+1])) * rbs; 300 | b_sum += ( stack.b = ( pb = pixels[yi+2])) * rbs; 301 | a_sum += ( stack.a = ( pa = pixels[yi+3])) * rbs; 302 | 303 | r_in_sum += pr; 304 | g_in_sum += pg; 305 | b_in_sum += pb; 306 | a_in_sum += pa; 307 | 308 | stack = stack.next; 309 | 310 | if( i < heightMinus1 ) 311 | { 312 | yp += width; 313 | } 314 | } 315 | 316 | yi = x; 317 | stackIn = stackStart; 318 | stackOut = stackEnd; 319 | for ( y = 0; y < height; y++ ) 320 | { 321 | p = yi << 2; 322 | pixels[p+3] = pa = (a_sum * mul_sum) >> shg_sum; 323 | if ( pa > 0 ) 324 | { 325 | pa = 255 / pa; 326 | pixels[p] = ((r_sum * mul_sum) >> shg_sum ) * pa; 327 | pixels[p+1] = ((g_sum * mul_sum) >> shg_sum ) * pa; 328 | pixels[p+2] = ((b_sum * mul_sum) >> shg_sum ) * pa; 329 | } else { 330 | pixels[p] = pixels[p+1] = pixels[p+2] = 0; 331 | } 332 | 333 | r_sum -= r_out_sum; 334 | g_sum -= g_out_sum; 335 | b_sum -= b_out_sum; 336 | a_sum -= a_out_sum; 337 | 338 | r_out_sum -= stackIn.r; 339 | g_out_sum -= stackIn.g; 340 | b_out_sum -= stackIn.b; 341 | a_out_sum -= stackIn.a; 342 | 343 | p = ( x + (( ( p = y + radiusPlus1) < heightMinus1 ? p : heightMinus1 ) * width )) << 2; 344 | 345 | r_sum += ( r_in_sum += ( stackIn.r = pixels[p])); 346 | g_sum += ( g_in_sum += ( stackIn.g = pixels[p+1])); 347 | b_sum += ( b_in_sum += ( stackIn.b = pixels[p+2])); 348 | a_sum += ( a_in_sum += ( stackIn.a = pixels[p+3])); 349 | 350 | stackIn = stackIn.next; 351 | 352 | r_out_sum += ( pr = stackOut.r ); 353 | g_out_sum += ( pg = stackOut.g ); 354 | b_out_sum += ( pb = stackOut.b ); 355 | a_out_sum += ( pa = stackOut.a ); 356 | 357 | r_in_sum -= pr; 358 | g_in_sum -= pg; 359 | b_in_sum -= pb; 360 | a_in_sum -= pa; 361 | 362 | stackOut = stackOut.next; 363 | 364 | yi += width; 365 | } 366 | } 367 | 368 | context.putImageData( imageData, top_x, top_y ); 369 | 370 | } 371 | 372 | 373 | function stackBlurCanvasRGB( id, top_x, top_y, width, height, radius ) 374 | { 375 | if ( isNaN(radius) || radius < 1 ) return; 376 | radius |= 0; 377 | 378 | //var canvas = document.getElementById( id ); 379 | //Edited to simply pass the canvas instead of looking it up by ID 380 | var canvas = id; 381 | var context = canvas.getContext("2d"); 382 | var imageData; 383 | 384 | try { 385 | try { 386 | imageData = context.getImageData( top_x, top_y, width, height ); 387 | } catch(e) { 388 | 389 | // NOTE: this part is supposedly only needed if you want to work with local files 390 | // so it might be okay to remove the whole try/catch block and just use 391 | // imageData = context.getImageData( top_x, top_y, width, height ); 392 | try { 393 | netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead"); 394 | imageData = context.getImageData( top_x, top_y, width, height ); 395 | } catch(e) { 396 | alert("Cannot access local image"); 397 | throw new Error("unable to access local image data: " + e); 398 | return; 399 | } 400 | } 401 | } catch(e) { 402 | alert("Cannot access image"); 403 | throw new Error("unable to access image data: " + e); 404 | } 405 | 406 | var pixels = imageData.data; 407 | 408 | var x, y, i, p, yp, yi, yw, r_sum, g_sum, b_sum, 409 | r_out_sum, g_out_sum, b_out_sum, 410 | r_in_sum, g_in_sum, b_in_sum, 411 | pr, pg, pb, rbs; 412 | 413 | var div = radius + radius + 1; 414 | var w4 = width << 2; 415 | var widthMinus1 = width - 1; 416 | var heightMinus1 = height - 1; 417 | var radiusPlus1 = radius + 1; 418 | var sumFactor = radiusPlus1 * ( radiusPlus1 + 1 ) / 2; 419 | 420 | var stackStart = new BlurStack(); 421 | var stack = stackStart; 422 | for ( i = 1; i < div; i++ ) 423 | { 424 | stack = stack.next = new BlurStack(); 425 | if ( i == radiusPlus1 ) var stackEnd = stack; 426 | } 427 | stack.next = stackStart; 428 | var stackIn = null; 429 | var stackOut = null; 430 | 431 | yw = yi = 0; 432 | 433 | var mul_sum = mul_table[radius]; 434 | var shg_sum = shg_table[radius]; 435 | 436 | for ( y = 0; y < height; y++ ) 437 | { 438 | r_in_sum = g_in_sum = b_in_sum = r_sum = g_sum = b_sum = 0; 439 | 440 | r_out_sum = radiusPlus1 * ( pr = pixels[yi] ); 441 | g_out_sum = radiusPlus1 * ( pg = pixels[yi+1] ); 442 | b_out_sum = radiusPlus1 * ( pb = pixels[yi+2] ); 443 | 444 | r_sum += sumFactor * pr; 445 | g_sum += sumFactor * pg; 446 | b_sum += sumFactor * pb; 447 | 448 | stack = stackStart; 449 | 450 | for( i = 0; i < radiusPlus1; i++ ) 451 | { 452 | stack.r = pr; 453 | stack.g = pg; 454 | stack.b = pb; 455 | stack = stack.next; 456 | } 457 | 458 | for( i = 1; i < radiusPlus1; i++ ) 459 | { 460 | p = yi + (( widthMinus1 < i ? widthMinus1 : i ) << 2 ); 461 | r_sum += ( stack.r = ( pr = pixels[p])) * ( rbs = radiusPlus1 - i ); 462 | g_sum += ( stack.g = ( pg = pixels[p+1])) * rbs; 463 | b_sum += ( stack.b = ( pb = pixels[p+2])) * rbs; 464 | 465 | r_in_sum += pr; 466 | g_in_sum += pg; 467 | b_in_sum += pb; 468 | 469 | stack = stack.next; 470 | } 471 | 472 | 473 | stackIn = stackStart; 474 | stackOut = stackEnd; 475 | for ( x = 0; x < width; x++ ) 476 | { 477 | pixels[yi] = (r_sum * mul_sum) >> shg_sum; 478 | pixels[yi+1] = (g_sum * mul_sum) >> shg_sum; 479 | pixels[yi+2] = (b_sum * mul_sum) >> shg_sum; 480 | 481 | r_sum -= r_out_sum; 482 | g_sum -= g_out_sum; 483 | b_sum -= b_out_sum; 484 | 485 | r_out_sum -= stackIn.r; 486 | g_out_sum -= stackIn.g; 487 | b_out_sum -= stackIn.b; 488 | 489 | p = ( yw + ( ( p = x + radius + 1 ) < widthMinus1 ? p : widthMinus1 ) ) << 2; 490 | 491 | r_in_sum += ( stackIn.r = pixels[p]); 492 | g_in_sum += ( stackIn.g = pixels[p+1]); 493 | b_in_sum += ( stackIn.b = pixels[p+2]); 494 | 495 | r_sum += r_in_sum; 496 | g_sum += g_in_sum; 497 | b_sum += b_in_sum; 498 | 499 | stackIn = stackIn.next; 500 | 501 | r_out_sum += ( pr = stackOut.r ); 502 | g_out_sum += ( pg = stackOut.g ); 503 | b_out_sum += ( pb = stackOut.b ); 504 | 505 | r_in_sum -= pr; 506 | g_in_sum -= pg; 507 | b_in_sum -= pb; 508 | 509 | stackOut = stackOut.next; 510 | 511 | yi += 4; 512 | } 513 | yw += width; 514 | } 515 | 516 | 517 | for ( x = 0; x < width; x++ ) 518 | { 519 | g_in_sum = b_in_sum = r_in_sum = g_sum = b_sum = r_sum = 0; 520 | 521 | yi = x << 2; 522 | r_out_sum = radiusPlus1 * ( pr = pixels[yi]); 523 | g_out_sum = radiusPlus1 * ( pg = pixels[yi+1]); 524 | b_out_sum = radiusPlus1 * ( pb = pixels[yi+2]); 525 | 526 | r_sum += sumFactor * pr; 527 | g_sum += sumFactor * pg; 528 | b_sum += sumFactor * pb; 529 | 530 | stack = stackStart; 531 | 532 | for( i = 0; i < radiusPlus1; i++ ) 533 | { 534 | stack.r = pr; 535 | stack.g = pg; 536 | stack.b = pb; 537 | stack = stack.next; 538 | } 539 | 540 | yp = width; 541 | 542 | for( i = 1; i <= radius; i++ ) 543 | { 544 | yi = ( yp + x ) << 2; 545 | 546 | r_sum += ( stack.r = ( pr = pixels[yi])) * ( rbs = radiusPlus1 - i ); 547 | g_sum += ( stack.g = ( pg = pixels[yi+1])) * rbs; 548 | b_sum += ( stack.b = ( pb = pixels[yi+2])) * rbs; 549 | 550 | r_in_sum += pr; 551 | g_in_sum += pg; 552 | b_in_sum += pb; 553 | 554 | stack = stack.next; 555 | 556 | if( i < heightMinus1 ) 557 | { 558 | yp += width; 559 | } 560 | } 561 | 562 | yi = x; 563 | stackIn = stackStart; 564 | stackOut = stackEnd; 565 | for ( y = 0; y < height; y++ ) 566 | { 567 | p = yi << 2; 568 | pixels[p] = (r_sum * mul_sum) >> shg_sum; 569 | pixels[p+1] = (g_sum * mul_sum) >> shg_sum; 570 | pixels[p+2] = (b_sum * mul_sum) >> shg_sum; 571 | 572 | r_sum -= r_out_sum; 573 | g_sum -= g_out_sum; 574 | b_sum -= b_out_sum; 575 | 576 | r_out_sum -= stackIn.r; 577 | g_out_sum -= stackIn.g; 578 | b_out_sum -= stackIn.b; 579 | 580 | p = ( x + (( ( p = y + radiusPlus1) < heightMinus1 ? p : heightMinus1 ) * width )) << 2; 581 | 582 | r_sum += ( r_in_sum += ( stackIn.r = pixels[p])); 583 | g_sum += ( g_in_sum += ( stackIn.g = pixels[p+1])); 584 | b_sum += ( b_in_sum += ( stackIn.b = pixels[p+2])); 585 | 586 | stackIn = stackIn.next; 587 | 588 | r_out_sum += ( pr = stackOut.r ); 589 | g_out_sum += ( pg = stackOut.g ); 590 | b_out_sum += ( pb = stackOut.b ); 591 | 592 | r_in_sum -= pr; 593 | g_in_sum -= pg; 594 | b_in_sum -= pb; 595 | 596 | stackOut = stackOut.next; 597 | 598 | yi += width; 599 | } 600 | } 601 | 602 | context.putImageData( imageData, top_x, top_y ); 603 | 604 | } 605 | 606 | function BlurStack() 607 | { 608 | this.r = 0; 609 | this.g = 0; 610 | this.b = 0; 611 | this.a = 0; 612 | this.next = null; 613 | } -------------------------------------------------------------------------------- /scripts/lib/sandbox-scripts.js: -------------------------------------------------------------------------------- 1 | (function(window, document, JSON) { 2 | /* Remember that we are within the sandbox-scripts.html window scope */ 3 | //Sandbox scope 4 | var sandbox = {}; 5 | window.sandbox = sandbox; 6 | 7 | /* 8 | * Returns a message from the provided event. 9 | *
10 | * This is the main and only function that sandboxed script implementations 11 | * should call. Upon receiving an event with their messageHandler() 12 | * function, implementations should process the event and return an 13 | * object message using this function. 14 | * @param {Event} event the original Event a message was recieved from 15 | * @param {Object} message the return message 16 | * @returns {undefined} 17 | */ 18 | sandbox.returnMessage = function(event, message) { 19 | message = sandbox.deepCopySafeMessage(message || {}); 20 | //Assign the source/script to the original event's source/script 21 | message.source = event.data.source; 22 | message.script = event.data.script; 23 | 24 | //Send the message back to sandbox.js 25 | event.source.postMessage(message, event.origin); 26 | }; 27 | /** 28 | * Sandbox messages are not allowed to contain functions. This function 29 | * will quickly remove any function objects by stringifying and parsing 30 | * the message as a JSON object. 31 | * @param {Object} object 32 | * @returns {unresolved} 33 | */ 34 | sandbox.deepCopySafeMessage = function(object) { 35 | return JSON.parse(JSON.stringify(object)); 36 | }; 37 | 38 | /** 39 | * This object keeps track of which sandboxed scripts have been 40 | * initialized. 41 | */ 42 | sandbox.initsCalled = {}; 43 | /** 44 | * This object keeps track of the number of attempts an event has been 45 | * posted to a script after initialization. If too many attempts are made, 46 | * the event is dropped. 47 | */ 48 | var eventAttempts = {}; 49 | /** 50 | * Initializes a script from the provided URL. 51 | *
52 | * This function should only be called by sandbox-scripts.js. Individual 53 | * sandboxed script implementations should implement the checkScript() and 54 | * getScriptUrl() functions and let the sandbox do the work. 55 | *
56 | * Note that this is only used for externally loaded scripts, and is 57 | * different from the sandboxed script implementation. An implementation 58 | * (for instance, Google Maps API), may require additional assets to be 59 | * loaded that are hosted externally. These assets are what should be 60 | * passed in the getScriptUrl() function. 61 | * @param {string} url the URL of the script to load 62 | * @returns {undefined} 63 | */ 64 | sandbox.initializeScript = function(url) { 65 | var script = document.createElement('script'); 66 | script.type = 'text/javascript'; 67 | script.src = url; 68 | document.body.appendChild(script); 69 | }; 70 | 71 | /** 72 | * The sandbox-scripts message handler. This is where most all of the 73 | * processing takes place. 74 | * @param {Event} e 'message' event that may be from a sandboxed script or 75 | * from the main window's sandbox.js 76 | * @returns {undefined} 77 | */ 78 | function messageHandler(e) { 79 | //If this is a load check from sandbox.js, let it know we're loaded 80 | if (e.data.loadCheck) { 81 | e.source.postMessage({ 82 | 'loaded': true 83 | }, e.origin); 84 | } 85 | else if (e.data.script) { 86 | //If 'source' is not specified, generate a random source string 87 | if (!e.data.source) { 88 | e.data.source = 'Unknown' + Math.floor((Math.random() * 899) + 100); 89 | } 90 | 91 | /* 92 | * This is where sandbox-scripts tries to locate the global 93 | * variable for the requested script. 94 | *
95 | * In the case of our example sandboxed script, 'sandbox-myscript.js', 96 | * a global variable 'myscript' should be defined. 97 | *
98 | * Additionally, 'myscript' should implement AT LEAST a function 99 | * defined as messageHandler(Event). It SHOULD implement 100 | * checkScript() and getScriptUrl() as needed. See 101 | * 'sandbox-stripe.js' for an example implementation. 102 | */ 103 | var script = window[e.data.script]; 104 | if (script && script.messageHandler) { 105 | //If the script requires loading via a URL, 106 | if (script.checkScript && script.getScriptUrl) { 107 | if (script.checkScript()) { 108 | //If the script is loaded, pass the event to the message 109 | //handler 110 | script.messageHandler(e); 111 | } 112 | else { 113 | //If it is not loaded, check to see whether or not we 114 | //have already called tried to initialize the script 115 | if (!sandbox.initsCalled[script]) { 116 | //If we have not, attempt to initialize the script 117 | //using the provided URL 118 | sandbox.initsCalled[script] = true; 119 | sandbox.initializeScript(script.getScriptUrl()); 120 | } 121 | 122 | //Resend the event while we wait for the script to load, 123 | //but timeout if it takes too long and the script 124 | //still has not loaded. This is usually due to a 125 | //manifest issue. 126 | var attempt = eventAttempts[e.data.source] || 0; 127 | if (attempt > 10) { 128 | throw new Error('Could not send message after too many failed attempts, make sure manifest.json has specified the correct scripts-sandbox.html sandbox permission.') 129 | } 130 | else { 131 | attempt++; 132 | eventAttempts[e.data.source] = attempt; 133 | setTimeout(function() { 134 | messageHandler(e); 135 | }, 100); 136 | } 137 | 138 | } 139 | } 140 | else { 141 | //If the script does not require initialization, pass 142 | //the event to the handler 143 | script.messageHandler(e); 144 | } 145 | } 146 | } 147 | } 148 | 149 | /** 150 | * Initialize the sandbox-scripts and add the message handler. 151 | * @returns {undefined} 152 | */ 153 | var initialize = function() { 154 | window.addEventListener('message', messageHandler); 155 | }; 156 | initialize(); 157 | }(window, document, JSON)); 158 | -------------------------------------------------------------------------------- /scripts/lib/sandbox-stripe.js: -------------------------------------------------------------------------------- 1 | (function(window) { 2 | //Check if sandbox-scripts.js exists 3 | if (!window.sandbox) { 4 | throw new Error('sandbox-scripts.js is not loaded in the "sandbox-scripts" iframe.'); 5 | } 6 | 7 | //Stripe Scope 8 | var stripe = {}; 9 | window.stripe = stripe; 10 | 11 | /** 12 | * Stripe.com JavaScript API script URL 13 | */ 14 | stripe.API_URL = 'https://js.stripe.com/v2/'; 15 | /** 16 | * Stripe.com account test publishable key. 17 | *
18 | * Be sure to use the publishable key and not the secret key. For 19 | * deployment, change this to the live publishable key. 20 | */ 21 | stripe.API_KEY = 'pk_test_Mh1oKuE9T1kalP8Stm9bzFee'; 22 | 23 | /** 24 | * Indicates whether or not the sandboxed script has been loaded. 25 | *
26 | * For this script, the global variable 'Stripe' (uppercase S is different 27 | * from the sandboxe global variable 'stripe') will be undefined until 28 | * the Stripe.com script is loaded. This can be used to indicate whether 29 | * or not the sandboxed script is ready. 30 | * @returns {boolean} true if the Stripe.com script is loaded and ready 31 | */ 32 | stripe.checkScript = function() { 33 | return typeof Stripe !== 'undefined'; 34 | }; 35 | /** 36 | * Retrieves the API script URL that needs to be loaded. 37 | *
38 | * This function is called by sandbox-scripts.js, and refers to the 39 | * Stripe.com script for this library. 40 | * @returns {string} the Stripe.com API script URL 41 | */ 42 | stripe.getScriptUrl = function() { 43 | return stripe.API_URL; 44 | }; 45 | /** 46 | * Stripe sandboxed message handler. 47 | * @param {Event} e message event 48 | * @returns {undefined} 49 | */ 50 | stripe.messageHandler = function(e) { 51 | //Note: when using a sandboxed script with sandbox-scripts.js, there 52 | //is no need to check the event's source/script parameters, or even 53 | //check if this sandboxed script is loaded and ready. 54 | //Sandbox-scripts.js takes care of the event source/script checking, 55 | //it can be safely assume that this script will only receive events with 56 | //the 'stripe' source property. 57 | //Since this scripts implements checkScript() and getScriptUrl(), it 58 | //is also safe to assume that the global variable 'Stripe' will always 59 | //be defined and ready since sandbox-scripts.js will automatically 60 | //initialize and load the Stripe.com API script 61 | Stripe.setPublishableKey(stripe.API_KEY); 62 | chargeCard(e); 63 | }; 64 | 65 | /** 66 | * Charges a card from the provided event. 67 | * @param {Event} event 68 | * @returns {undefined} 69 | */ 70 | var chargeCard = function(event) { 71 | //The response to send back over the sandbox 72 | var eventResponse = {}; 73 | 74 | //Card information 75 | var number = event.data.number; 76 | var expMonth = event.data.expMonth.toString(); 77 | var expYear = event.data.expYear.toString(); 78 | //The amount to be charged 79 | var amount = event.data.amount; 80 | 81 | //Use Stripe.com's card validation functions to ensure the card's 82 | //expiration is valid and the number is a valid credit card number 83 | if (Stripe.validateCardNumber(number) && Stripe.validateExpiry(expMonth, expYear)) { 84 | //Create a charge token 85 | Stripe.card.createToken({ 86 | number: number, 87 | exp_month: expMonth, 88 | exp_year: expYear 89 | }, function(status, response) { 90 | if (response.error) { 91 | eventResponse.success = false; 92 | eventResponse.message = response.error; 93 | } 94 | else { 95 | var token = response.id; 96 | //This is where you would send the token and the amount to your 97 | //own server which would charge the Stripe single-use token 98 | /*$.ajax('https://mystripeserver.com/', { 99 | data: { 100 | token: token, 101 | amount: amount 102 | } 103 | }).success(function(data) { 104 | //Charge was successful 105 | }).fail(function(jqXHR) { 106 | //Charge failed 107 | });*/ 108 | //For testing purposes, assume the transaction was successful 109 | eventResponse.success = true; 110 | eventResponse.message = 'Charge successful'; 111 | 112 | //Send the response back over the sandbox 113 | window.sandbox.returnMessage(event, eventResponse); 114 | } 115 | }); 116 | } 117 | else { 118 | eventResponse.success = false; 119 | eventResponse.message = 'Invalid card'; 120 | 121 | //Send the response back over the sandbox 122 | window.sandbox.returnMessage(event, eventResponse); 123 | } 124 | }; 125 | })(window); -------------------------------------------------------------------------------- /scripts/lib/sandbox.js: -------------------------------------------------------------------------------- 1 | (function(window, document) { 2 | //Sandbox Scope 3 | var sandbox = {}; 4 | window.sandbox = sandbox; 5 | 6 | //Indicates whether or not sandbox-scripts.html has loaded 7 | var loaded = false; 8 | //Indicates whether or not an iframe with the ID of '#sandbox-scripts' was found 9 | var iframeNotFound = false; 10 | 11 | /** 12 | * Sends a message to a sandboxed script. 13 | *
14 | * The message should AT LEAST contain a 'source' property, which should 15 | * be any uniquely identifying String, and a 'script' property, which 16 | * indicates the script to load. 17 | *
18 | * The 'script' property should be equal to the scope object declared in 19 | * the sandbox script's file. Usually this is a lowercase name of the 20 | * script. 21 | *
22 | * For the sandbox script 'sandbox-myscript.js', the 'script' parameter 23 | * should be 'myscript'. 24 | *
25 | * Any additional properties can be set as needed for the individual 26 | * sandboxed script. 27 | * @param {Object} message the message to send to the sandboxed script 28 | * @returns {undefined} 29 | */ 30 | sandbox.message = function(message) { 31 | if (loaded) { 32 | postMessage(message); 33 | } 34 | else if (iframeNotFound) { 35 | //If we can't find the sandbox, throw an error, don't try it again 36 | throw new Error('Cannot send a message when iframe "sandbox-scripts" was not found.'); 37 | } 38 | else { 39 | //Keep trying to send the message if the sandbox has not loaded 40 | setTimeout(function() { 41 | sandbox.message(message); 42 | }, 100); 43 | } 44 | }; 45 | 46 | /** 47 | * Sends a message to the sandbox-scripts.html content window. 48 | * @param {Object} message the message to send 49 | * @returns {undefined} 50 | */ 51 | var postMessage = function(message) { 52 | var iframe = document.getElementById('sandbox-scripts'); 53 | if (iframe) { 54 | var win = iframe.contentWindow; 55 | if (win !== null) { 56 | win.postMessage(message, '*'); 57 | } 58 | } 59 | else { 60 | iframeNotFound = true; 61 | throw new Error('Warning: No iframe with "sandbox-scripts" ID was found, include it for sandbox scripts to work.'); 62 | } 63 | }; 64 | 65 | /** 66 | * Initialize the sandbox. 67 | * @returns {undefined} 68 | */ 69 | var initialize = function() { 70 | //Add a listener to determine if the sandbox is loaded 71 | window.addEventListener('message', function(e) { 72 | if (e.data.loaded) { 73 | loaded = true; 74 | } 75 | }); 76 | //Start the load check as soon as the window is done loading 77 | window.addEventListener('load', loadCheck); 78 | }; 79 | /** 80 | * A load check function that will repeatedly message sandbox-scripts.html 81 | * until it receives a response that sandbox-scripts.js is completely 82 | * loaded. 83 | * @returns {undefined} 84 | */ 85 | var loadCheck = function() { 86 | if (!loaded && !iframeNotFound) { 87 | postMessage({ 88 | 'loadCheck': true 89 | }); 90 | setTimeout(loadCheck, 100); 91 | } 92 | }; 93 | initialize(); 94 | })(window, document); -------------------------------------------------------------------------------- /scripts/lib/slider.js: -------------------------------------------------------------------------------- 1 | (function(window, $) { 2 | //Slider Scope 3 | var slider = {}; 4 | window.slider = slider; 5 | 6 | /** 7 | * Defines before/after open/close event types. 8 | *
9 | * For example: 10 | *
11 | * $(mypage).on(slider.Event.BEFORE_OPEN, functionToCallBefore); 12 | *13 | * Note that these events should be used for quick actions. If a 14 | * long-running function needs to be called before a slide is opened, 15 | * use the "beforeOpen" parameter in slider.slide() to display a processing 16 | * page first. 17 | */ 18 | slider.Event = { 19 | BEFORE_OPEN: 'before-open', 20 | AFTER_OPEN: 'after-open', 21 | BEFORE_CLOSE: 'before-close', 22 | AFTER_CLOSE: 'after-close' 23 | }; 24 | /** 25 | * Defines the direction a slide will be opened. 26 | */ 27 | slider.Direction = { 28 | LEFT: 'left', 29 | RIGHT: 'right' 30 | }; 31 | /** 32 | * The duration (ms) of the slide animation 33 | */ 34 | slider.duration = 350; 35 | /** 36 | * The jQuery selector for the processing page. 37 | *
38 | * If specified, when using slider.slide() with a "beforeOpen" parameter, 39 | * the processing page will be opened first, the beforeOpen function run, 40 | * and then the next page displayed. 41 | *
42 | * If the processing page is not specified, using "beforeOpen" on 43 | * slider.slide() would be equivalent to binding the function to the 44 | * "before-open" trigger, and no processing page will be displayed. 45 | */ 46 | slider.processing; 47 | /** 48 | * The jQuery selector for the hidden storage div. 49 | *
50 | * If specified, when removing slides, slider.js will append their content 51 | * to the storage div. This is important if the pages need to be accessed 52 | * again and are not stored in memory. 53 | */ 54 | slider.storage; 55 | 56 | //Processing jQuery selector 57 | var $process; 58 | 59 | /** 60 | * Slides the provided page into the specified slider. 61 | *
62 | * By default, the direction is left. 63 | *
64 | * If a function is provided to execute before opening the page, a 65 | * processing page (if defined in slider.processing) will be opened 66 | * first, the function executed, and then the page displayed. 67 | * @param {string|jQuery} slideContainer the slider to display the page in 68 | * @param {string|jQuery} page the page to display 69 | * @param {string} direction the direction to slide the page from 70 | * @param {function} beforeOpen optional function to execute before opening 71 | * @param {Object} param optional parameters for the optional function 72 | * @returns {undefined} 73 | */ 74 | slider.slide = function(slideContainer, page, direction, beforeOpen, param) { 75 | //If beforeOpen is specified and a processing page is specified, 76 | if (typeof beforeOpen === 'function' && slider.processing) { 77 | //Define the processing page selector if it has not been defined yet 78 | if (!$process) { 79 | $process = $(slider.processing); 80 | } 81 | 82 | //Wrap the page in a slide and open the page after the process 83 | //slide has opened fully 84 | var processSlide = $('
').addClass('slide').append($process); 85 | processSlide.on(slider.Event.AFTER_OPEN, function(e) { 86 | e.stopPropagation(); //Prevents second trigger 87 | var targetSlide = $('').addClass('slide').append($(page)); 88 | targetSlide.on(slider.Event.BEFORE_OPEN, function() { 89 | beforeOpen(param); 90 | }); 91 | open(slideContainer, targetSlide, direction); 92 | }); 93 | open(slideContainer, processSlide, direction); 94 | } 95 | else { 96 | //If no processing page is specified, execute the beforeOpen 97 | //function immediately and open the page 98 | if (typeof beforeOpen === 'function') { 99 | beforeOpen(param); 100 | } 101 | var targetSlide = $('').addClass('slide').append($(page)); 102 | open(slideContainer, targetSlide, direction); 103 | } 104 | }; 105 | 106 | /** 107 | * Opens a slide in the specified slider. 108 | * @param {string|jQuery} slideContainer the slider to open the slide into 109 | * @param {string|jQuery} target the target slide to open 110 | * @param {string} direction the direction to slide the target from 111 | * @returns {undefined} 112 | */ 113 | var open = function(slideContainer, target, direction) { 114 | //If no direction is specified, default to left 115 | direction = direction === slider.Direction.LEFT ? slider.Direction.LEFT : slider.Direction.RIGHT; 116 | 117 | var $slider = $(slideContainer); 118 | 119 | //Get the current center slide 120 | var center = $slider.find('.slide.center'); 121 | //Trigger events on the slide's wrapped children 122 | center.children().trigger(slider.Event.BEFORE_CLOSE); 123 | center.animate({ 124 | left: direction === slider.Direction.LEFT ? '100%' : '-100%' 125 | }, slider.duration, function() { 126 | center.children().trigger(slider.Event.AFTER_CLOSE); 127 | if (slider.storage) { 128 | //If storage is specified, add the children to storage 129 | center.children().appendTo(slider.storage); 130 | } 131 | center.detach(); 132 | }); 133 | 134 | //Get the incoming target slide and add it to the slider 135 | target = $(target); 136 | target.addClass(direction).appendTo($slider); 137 | //Trigger events on the slide's wrapped children 138 | target.children().trigger(slider.Event.BEFORE_OPEN); 139 | target.animate({ 140 | left: 0 141 | }, slider.duration, function() { 142 | target.removeClass(direction).addClass('center'); 143 | target.children().trigger(slider.Event.AFTER_OPEN); 144 | }); 145 | }; 146 | })(window, jQuery); -------------------------------------------------------------------------------- /scripts/lib/spreadsheet.gs: -------------------------------------------------------------------------------- 1 | //This script should be registered on a Google account at 2 | //https://script.google.com/ 3 | //After creating a new project and deploying the script as a web app, 4 | //retrieve the web app URL and set it as spreadsheet.MACRO_URL in the 5 | //spreadsheet.js library. 6 | //You will need to exectue the app as "me" and allow "Anyone, even anonymous" 7 | //access to the app. Additionally, you will need to run it once in the browser 8 | //to authorize the app access to your Google Drive spreadsheets. 9 | 10 | function doGet(event) { 11 | var response = {}; 12 | response.message = ''; 13 | response.status = 200; 14 | response.data = {}; 15 | 16 | try { 17 | //Retrieve spreadsheet by ID or return error message 18 | if (!event || !event.parameter) { 19 | response.message = 'No parameters given'; 20 | response.status = 400; 21 | return ContentService.createTextOutput(JSON.stringify(response)); 22 | } 23 | 24 | var id = event.parameter.spreadsheetId; 25 | if (!id) { 26 | response.message = 'No spreadsheet ID provided'; 27 | response.status = 400; 28 | return ContentService.createTextOutput(JSON.stringify(response)); 29 | } 30 | var ss; 31 | try { 32 | ss = SpreadsheetApp.openById(id); 33 | } 34 | catch (e) { 35 | response.message = 'Invalid spreadsheet ID'; 36 | response.status = 400; 37 | return ContentService.createTextOutput(JSON.stringify(response)); 38 | } 39 | if (ss.getSheets().length < 1) { 40 | response.message = 'Empty spreadsheet'; 41 | response.status = 400; 42 | return ContentService.createTextOutput(JSON.stringify(response)); 43 | } 44 | 45 | //Retrieve sheet (default first sheet unless sheetIndex is specified) 46 | sheetIndex = 0; 47 | if (event.parameter.sheetIndex) { 48 | if (event.parameter.sheetIndex < ss.getSheets().length - 1) { 49 | sheetIndex = event.parameter.sheetIndex; 50 | } 51 | } 52 | var sheet = ss.getSheets()[sheetIndex]; 53 | 54 | var action = event.parameter.action; 55 | if (action === 'get') { 56 | var range = sheet.getDataRange(); 57 | var values = range.getValues(); 58 | if (values.length < 1) { 59 | response.message = 'Empty spreadsheet'; 60 | response.status = 400; 61 | return ContentService.createTextOutput(JSON.stringify(response)); 62 | } 63 | 64 | response.message = 'Success'; 65 | response.status = 200; 66 | response.data = values; 67 | return ContentService.createTextOutput(JSON.stringify(response)); 68 | } 69 | else if (action === 'post') { 70 | var row = JSON.parse(event.parameter.row); 71 | if (Object.prototype.toString.call(row) !== '[object Array]') { 72 | response.message = 'Invalid row parameter'; 73 | response.status = 400; 74 | return ContentService.createTextOutput(JSON.stringify(response)); 75 | } 76 | sheet.appendRow(row); 77 | response.message = 'Success'; 78 | response.status = 200; 79 | return ContentService.createTextOutput(JSON.stringify(response)); 80 | } 81 | else { 82 | response.message = 'Invalid action'; 83 | response.status = 400; 84 | return ContentService.createTextOutput(JSON.stringify(response)); 85 | } 86 | } 87 | catch (e) { 88 | response.message = 'An unexpected error occurred: ' + e.message; 89 | response.status = 500; 90 | return ContentService.createTextOutput(JSON.stringify(response)); 91 | } 92 | } -------------------------------------------------------------------------------- /scripts/lib/spreadsheet.js: -------------------------------------------------------------------------------- 1 | (function(window, JSON) { 2 | //Spreadsheet Scope 3 | var spreadsheet = {}; 4 | window.spreadsheet = spreadsheet; 5 | 6 | /** 7 | * The web app URL that spreadsheet.gs is deployed to 8 | */ 9 | spreadsheet.MACRO_URL = 'https://script.google.com/macros/s/AKfycbw4AvoXKYaURCkaCmjwi7zQO54GCP45YaNnGQ0d8slA0ZGxiEw/exec'; 10 | /** 11 | * The default spreadsheetId to use when making requests. 12 | *13 | * This should be specified if the app is only communicating with one 14 | * spreadsheet. 15 | */ 16 | spreadsheet.defaultSpreadsheetId; 17 | 18 | /** 19 | * Retrieves all rows in the provided spreadsheet and returns them in the 20 | * provided callback. 21 | * @param {function(Array,string,number)} callback callback function to 22 | * retrieve row information. The rows Array, message, and status are 23 | * returned as the callback parameters 24 | * @param {string} spreadsheetId the spreadsheet ID (optional if using 25 | * a default spreadsheet ID) 26 | * @returns {undefined} 27 | */ 28 | spreadsheet.getAllRows = function(callback, spreadsheetId) { 29 | spreadsheetId = spreadsheetId || spreadsheet.defaultSpreadsheetId; 30 | var params = { 31 | 'spreadsheetId': spreadsheetId, 32 | 'action': 'get' 33 | }; 34 | var url = []; 35 | url.push(spreadsheet.MACRO_URL); 36 | url.push('?'); 37 | for (var key in params) { 38 | url.push(key); 39 | url.push('='); 40 | url.push(params[key]); 41 | url.push('&'); 42 | } 43 | url.pop(); 44 | 45 | var xhr = new XMLHttpRequest(); 46 | xhr.onreadystatechange = function() { 47 | if (xhr.readyState === 4) { 48 | //Always callback an array, message, and status, even if 49 | //the web app returned an error 50 | var rows = []; 51 | var message = xhr.statusText; 52 | var status = xhr.status; 53 | 54 | if (xhr.status === 200) { 55 | var response = JSON.parse(xhr.responseText); 56 | message = response.message; 57 | status = xhr.status; 58 | 59 | if (response.status === 200) { 60 | rows = response.data; 61 | } 62 | } 63 | if (typeof callback === 'function') { 64 | callback(rows, message, status); 65 | } 66 | } 67 | }; 68 | xhr.open('GET', url.join(''), true); 69 | xhr.send(); 70 | }; 71 | 72 | /** 73 | * Adds a row to the provided spreadsheet, and calls the provided 74 | * callback function upon completion. 75 | * @param {Array} row Array of row data to append 76 | * @param {function(string,number)} callback callback function that is 77 | * called with a message and status number indicating the result of 78 | * the appended row 79 | * @param {string} spreadsheetId the spreadsheet ID (optional if using 80 | * a default spreadsheet ID) 81 | * @returns {undefined} 82 | */ 83 | spreadsheet.appendRow = function(row, callback, spreadsheetId) { 84 | if (typeof callback === 'string') { 85 | spreadsheetId = callback; 86 | } 87 | spreadsheetId = spreadsheetId || spreadsheet.defaultSpreadsheetId; 88 | var params = { 89 | 'spreadsheetId': spreadsheetId, 90 | 'action': 'post', 91 | 'row': JSON.stringify(row) 92 | }; 93 | var url = []; 94 | url.push(spreadsheet.MACRO_URL); 95 | url.push('?'); 96 | for (var key in params) { 97 | url.push(key); 98 | url.push('='); 99 | url.push(params[key]); 100 | url.push('&'); 101 | } 102 | url.pop(); 103 | 104 | var xhr = new XMLHttpRequest(); 105 | xhr.onreadystatechange = function() { 106 | if (xhr.readyState === 4) { 107 | var message = xhr.statusText; 108 | var status = xhr.status; 109 | 110 | if (xhr.status === 200) { 111 | var response = JSON.parse(xhr.responseText); 112 | message = response.message; 113 | status = response.status; 114 | } 115 | if (typeof callback === 'function') { 116 | callback(message, status); 117 | } 118 | } 119 | }; 120 | xhr.open('GET', url.join(''), true); 121 | xhr.send(); 122 | }; 123 | })(window, JSON); -------------------------------------------------------------------------------- /scripts/lib/stillthere.js: -------------------------------------------------------------------------------- 1 | (function(window, Date, $) { 2 | //StillThere scope 3 | var stillthere = {}; 4 | window.stillthere = stillthere; 5 | 6 | /** 7 | * Defines event types when a user has timed out. 8 | *
9 | * The LOADED event is fired once when stillthere is finished loading. 10 | * This should be listened to whenever modifying the contents of 11 | * stillthere.overlay. 12 | *
13 | * The STILL_THERE event is fired once when the user has not made a gesture 14 | * for however long (ms) specified in stillthere.timeoutStillThere. A 15 | * progress bar will be displayed indicating the amount of time left 16 | * before the timeout event is triggered. 17 | *
18 | * The TIMEOUT event is fired once after the STILL_THERE event and 19 | * indicates that an app session should be reset. 20 | */ 21 | stillthere.Event = { 22 | 'TIMEOUT': 'timeout', 23 | 'STILL_THERE': 'still-there', 24 | 'LOADED': 'loaded' 25 | }; 26 | 27 | /** 28 | * The jQuery overlay selector 29 | */ 30 | stillthere.overlay; 31 | /** 32 | * Timestamp of the last user gesture 33 | */ 34 | stillthere.lastGesture; 35 | /** 36 | * Amount of time (ms) until the overlay asks if the user is still there 37 | */ 38 | stillthere.timeoutStillThere = 30000; 39 | /** 40 | * Amount of time (ms) until the overlay times out 41 | */ 42 | stillthere.timeout = 60000; 43 | /** 44 | * How often to check for user gesture updates 45 | */ 46 | stillthere.checkInterval = 1000; 47 | 48 | /** 49 | * Map of listener functions. 50 | */ 51 | var listeners = {}; 52 | /** 53 | * Indicates that the STILL_THERE event has fired at least once 54 | * @type Boolean 55 | */ 56 | var stillThereFired = false; 57 | /** 58 | * Indicates that the TIMEOUT event has fired at least once 59 | * @type Boolean 60 | */ 61 | var timeoutFired = false; 62 | 63 | /** 64 | * Adds an event listener to the stillthere overlay. 65 | *
66 | * The type parameter may be "timeout", "still-there", or "loaded" as 67 | * defined in the stillthere.Event object. 68 | * @param {string} type the type of event to listen to 69 | * @param {function)} listener the listener to add 70 | * @returns {undefined} 71 | */ 72 | stillthere.addEventListener = function(type, listener) { 73 | if (typeof listener === 'function') { 74 | listeners[listener] = { 75 | 'type': type, 76 | 'listener': listener 77 | }; 78 | } 79 | }; 80 | /** 81 | * Removes an event listener from the stillthere overlay. 82 | *
83 | * If a listener is not provided, all listeners of the provided type 84 | * are removed. 85 | *
86 | * The type parameter may be "timeout", "still-there", or "loaded" as 87 | * defined in the stillthere.Event object. 88 | * @param {string} type the type of event to listen to 89 | * @param {function} listener the listener to remove 90 | * @returns {undefined} 91 | */ 92 | stillthere.removeEventListener = function(type, listener) { 93 | var removeAll = typeof listener === 'undefined'; 94 | for (var key in listeners) { 95 | if (listeners[key].type === type) { 96 | if (removeAll || listeners[key].listener === listener) { 97 | delete listeners[key]; 98 | } 99 | } 100 | } 101 | }; 102 | /** 103 | * Fires all listeners of the specified type. 104 | * @param {string} type the type of event to fire 105 | * @returns {undefined} 106 | */ 107 | var fireListeners = function(type) { 108 | for (var key in listeners) { 109 | if (listeners[key].type === type) { 110 | listeners[key].listener(); 111 | } 112 | } 113 | }; 114 | 115 | /** 116 | * Forces the display of the "timeout" overlay and triggers the 117 | * TIMEOUT event. 118 | * @returns {undefined} 119 | */ 120 | stillthere.showTimeout = function() { 121 | stillthere.lastGesture = stillthere.timeout + 1; 122 | }; 123 | /** 124 | * Indicates whether or not the overlay is currently visible. 125 | * @returns {boolean} true if the overlay is visible, or false if not 126 | */ 127 | stillthere.isOverlayVisible = function() { 128 | return stillthere.overlay.is(':visible'); 129 | }; 130 | /** 131 | * Shows the overlay if it is not already visible. 132 | * @returns {undefined} 133 | */ 134 | var showOverlay = function() { 135 | if (!stillthere.isOverlayVisible()) { 136 | stillthere.overlay.show(); 137 | } 138 | }; 139 | /** 140 | * Hides the overlay if it is not already hidden. 141 | * @returns {undefined} 142 | */ 143 | var hideOverlay = function() { 144 | if (stillthere.isOverlayVisible()) { 145 | stillthere.overlay.hide(); 146 | } 147 | }; 148 | 149 | /** 150 | * Checks to see if enough time has passed since the last user gesture 151 | * to trigger a STILL_THERE or TIMEOUT event. 152 | * @returns {undefined} 153 | */ 154 | var checkTimeout = function() { 155 | var timePassed = Date.now() - stillthere.lastGesture; 156 | 157 | showOverlay(); 158 | if (timePassed > stillthere.timeout) { 159 | if (!timeoutFired) { 160 | //Only fire event once 161 | fireListeners(stillthere.Event.TIMEOUT); 162 | timeoutFired = true; 163 | stopProgress(); 164 | } 165 | } 166 | else if (timePassed > stillthere.timeoutStillThere) { 167 | if (!stillThereFired) { 168 | //Only fire event once 169 | fireListeners(stillthere.Event.STILL_THERE); 170 | stillThereFired = true; 171 | startProgress(); 172 | } 173 | } 174 | else { 175 | //User still active, reset things 176 | stillThereFired = false; 177 | timeoutFired = false; 178 | hideOverlay(); 179 | } 180 | setTimeout(checkTimeout, stillthere.checkInterval); 181 | }; 182 | /** 183 | * Hides the progress bar. 184 | * @returns {undefined} 185 | */ 186 | var stopProgress = function() { 187 | stillthere.overlay.find('.progress').hide(); 188 | }; 189 | /** 190 | * Displays the progress bar and starts it. 191 | * @param {boolean} update if true, update the progress bar, 192 | * if false, start the progress bar 193 | * @returns {undefined} 194 | */ 195 | var startProgress = function(update) { 196 | var progress = stillthere.overlay.find('.progress'); 197 | var updateInterval = 100; 198 | if (update) { 199 | var value = parseInt(progress.attr('value')); 200 | if (value < progress.attr('max')) { 201 | progress.attr('value', value + 1); 202 | setTimeout(function() { 203 | startProgress(true); 204 | }, updateInterval); 205 | } 206 | } 207 | else { 208 | var max = (stillthere.timeout - stillthere.timeoutStillThere) / updateInterval; 209 | progress.attr('value', 0).attr('max', max); 210 | progress.show(); 211 | startProgress(true); 212 | } 213 | }; 214 | 215 | /** 216 | * Updates the user's last gesture timestamp. 217 | * @returns {undefined} 218 | */ 219 | var updateLastGesture = function() { 220 | stillthere.lastGesture = Date.now(); 221 | }; 222 | 223 | /** 224 | * Initializes the stillthere overlay, appends it to the body of the 225 | * app, and adds listeners for user gestures. 226 | * @returns {undefined} 227 | */ 228 | var initialize = function() { 229 | stillthere.overlay = $('
').addClass('still-there'); 230 | stillthere.overlay.append($('').addClass('background')); 231 | stillthere.overlay.append($('').addClass('foreground') 232 | .append($('').addClass('message')) 233 | .append($('').addClass('progress'))); 234 | stillthere.overlay.appendTo($('body')); 235 | 236 | window.addEventListener('click', updateLastGesture); 237 | window.addEventListener('mousemove', updateLastGesture); 238 | window.addEventListener('touchmove', updateLastGesture); 239 | window.addEventListener('keydown', updateLastGesture); 240 | 241 | updateLastGesture(); 242 | checkTimeout(); 243 | fireListeners(stillthere.Event.LOADED); 244 | }; 245 | 246 | window.addEventListener('load', initialize); 247 | })(window, Date, jQuery); -------------------------------------------------------------------------------- /scripts/lib/stripe.js: -------------------------------------------------------------------------------- 1 | (function(window) { 2 | //Check if sandbox-scripts.js exists 3 | if (!window.sandbox) { 4 | throw new Error('stripe.js requires sandbox. sandbox-scripts.js is not loaded in the "sandbox-scripts" iframe.'); 5 | } 6 | 7 | //Stripe Scope 8 | var stripe = {}; 9 | window.stripe = stripe; 10 | 11 | /** 12 | * This object contains callback functions for different calls to 13 | * stripe.chargeCard(). 14 | *15 | * When a charge is requested, the passed callback function is assigned 16 | * a unique source identifier, which is passed as the 'source' property in 17 | * the sandbox message, and used to retrieve the callback when a response 18 | * has been received. 19 | */ 20 | stripe.callbacks = {}; 21 | 22 | /** 23 | * Charges a credit card. 24 | *
25 | * Note that for testing purposes, this function does not actually charge 26 | * the provided card. 27 | * @param {Card} card the Card to charge (received from swiper.js library) 28 | * @param {number} amount the amount to charge 29 | * @param {function(object)} callback callback function that receives an 30 | * object with a boolean property 'success' that indicates the status 31 | * of the charge, and a String property 'message' that contains a short 32 | * description of the success or failure of a charge 33 | * @returns {undefined} 34 | */ 35 | stripe.chargeCard = function(card, amount, callback) { 36 | var source = 'ChargeMessage' + Math.floor((Math.random() * 899) + 100); 37 | stripe.callbacks[source] = callback; 38 | var message = { 39 | 'source': source, 40 | 'script': 'stripe', 41 | 'number': card.getNumber(), 42 | 'expMonth': card.getExpMonth(), 43 | 'expYear': card.getExpYear(), 44 | 'amount': amount 45 | }; 46 | window.sandbox.message(message); 47 | }; 48 | 49 | //Add message listener for sandbox messages 50 | window.addEventListener('message', function(e) { 51 | //Verify that the message is from sandbox-stripe.js 52 | if (e.data.script === 'stripe') { 53 | //Retrieve the callback associate with the event's source 54 | var callback = stripe.callbacks[e.data.source]; 55 | if (callback !== undefined) { 56 | callback(e.data); 57 | } 58 | } 59 | }); 60 | })(window); -------------------------------------------------------------------------------- /scripts/lib/swiper.js: -------------------------------------------------------------------------------- 1 | (function(window, document, $, Date) { 2 | //Swiper Score 3 | var swiper = {}; 4 | window.swiper = swiper; 5 | 6 | /** 7 | * The swiper event type 8 | */ 9 | swiper.EVENT = 'card-swipe'; 10 | /** 11 | * The character key that indicates a line end (default 13, carriage return) 12 | *
13 | * This can be set if the card reader uses a different line end key. 14 | */ 15 | swiper.LINE_END = 13; 16 | /** 17 | * Splits line data into three tracks 18 | * @type RegExp 19 | */ 20 | var TRACK_REGEX = /(%.*?\?)?(;.*?\?)?/; 21 | /** 22 | * Splits track one into its sub-components 23 | * @type RegExp 24 | */ 25 | var TRACK_1_REGEX = /%([A-Z])([0-9]{1,19})\^([^\^]{2,26})\^([0-9]{4}|\^)([0-9]{3}|\^)([^\?]+)\?/; 26 | /** 27 | * Splits track two into its sub-components 28 | * @type RegExp 29 | */ 30 | var TRACK_2_REGEX = /;([0-9]{1,19})=([0-9]{4}|=)([0-9]{3}|=)([^\?]+)\?/; 31 | /** 32 | * Dummy card buffer (4242-4242-4242-4242 John Doe 01/15 33 | */ 34 | var DUMMY_CARD = '%B4242424242424242^DOE/JOHN^15011011000?;B4242424242424242=15011011000?'.split(''); 35 | 36 | /** 37 | * Indicates that the swiper is scanning for card data. 38 | *
39 | * Set to true before swiping a card. 40 | */ 41 | swiper.scanning = false; 42 | /** 43 | * Configurable delay (ms) before the swiper buffer is cleared. 44 | *
45 | * This delay ensures a user cannot type on a keyboard and submit a card 46 | * swipe. It should not be lower than however long it takes the card 47 | * reader to submit a full line of data to the computer. 48 | */ 49 | swiper.delay = 250; 50 | /** 51 | * Bypass key to submit a dummy card swipe (default back-tick (tilde) ` key) 52 | */ 53 | swiper.bypass = 96; 54 | /** 55 | * When true, the bypass key will submit a dummy card swipe. 56 | *
57 | * Set to false for deployment. 58 | */ 59 | swiper.enableBypass = true; 60 | 61 | /** 62 | * Timestamp when a swipe event started, used with swiper.delay to 63 | * determine valid card swipes. 64 | */ 65 | var swipeStart = 0; 66 | /** 67 | * Array of jQuery selectors that will be triggered when a swipe event 68 | * occurs. 69 | */ 70 | var triggers = []; 71 | /** 72 | * jQuery focus selector that can be set using swiper.setFocus(). 73 | *
74 | * If multiple triggers are used, and a situation occurs where only one 75 | * needs to be triggered, the focus can be set. Whenever the focus is 76 | * set, a card swipe event is triggered on that focus instead of the 77 | * triggers added via swiper.addTrigger. 78 | *
79 | * Focus is lost on a card swipe, so it must be reset every time it is 80 | * needed. 81 | */ 82 | var focus; 83 | /** 84 | * Buffer of character keys for the swipe event. 85 | */ 86 | var buffer = []; 87 | 88 | window.addEventListener('load', function() { 89 | //Register global keypress listener 90 | document.addEventListener('keypress', swiped); 91 | }); 92 | 93 | /** 94 | * Add a jQuery selector to be triggered when a swipe event occurs 95 | * @param {jQuery|string} selector the jQuery selector to be triggered 96 | * @returns {undefined} 97 | */ 98 | swiper.addTrigger = function(selector) { 99 | triggers.push(selector); 100 | }; 101 | /** 102 | * Set a jQuery selector to be focused for the next swipe event. 103 | *
104 | * The next swipe event will only trigger the provided selector. It will 105 | * not trigger any previous selectors added by swiper.addTrigger() until 106 | * the next swipe event. 107 | *
108 | * Focus is lost after a swipe event (even failed attempts). It must be 109 | * reset each time as needed. 110 | * @param {jQuery|string} selector the jQuery selector to trigger next 111 | * @returns {undefined} 112 | */ 113 | swiper.setFocus = function(selector) { 114 | focus = selector; 115 | }; 116 | /** 117 | * Removes the previous set focus. The next swipe event will trigger 118 | * all jQuery selectors added by swiper.addTrigger(). 119 | * @returns {undefined} 120 | */ 121 | swiper.removeFocus = function() { 122 | focus = undefined; 123 | }; 124 | 125 | /** 126 | * The global keyboard listener 127 | * @param {Event} e keypress event 128 | */ 129 | var swiped = function(e) { 130 | if (swiper.scanning) { 131 | e.preventDefault(); 132 | var keyCode = e.keyCode; 133 | 134 | if (swipeStart === 0) { 135 | swipeStart = Date.now(); 136 | } 137 | //If more time has passed since the swipe started (swiper.delay), 138 | //reset the buffer. 139 | if ((Date.now() - swipeStart) > swiper.delay) { 140 | swipeStart = 0; 141 | buffer = []; 142 | } 143 | 144 | //If using a bypass key, set the buffer to dummy card info and 145 | //change the key to indicate line end 146 | if (swiper.enableBypass && keyCode === swiper.bypass) { 147 | buffer = DUMMY_CARD; 148 | keyCode = swiper.LINE_END; 149 | } 150 | 151 | //When the line end key is detected, trigger the swipe event 152 | //and reset the buffer 153 | if (keyCode === swiper.LINE_END) { 154 | //Check if the delay is valid 155 | if ((Date.now() - swipeStart) < swiper.delay) { 156 | var card = new swiper.Card(buffer.join('')); 157 | var event = document.createEvent('HTMLEvents'); 158 | event.initEvent(swiper.EVENT, true, true); 159 | 160 | if (focus) { 161 | $(focus).trigger(swiper.EVENT, card); 162 | } 163 | else { 164 | for (var i = 0; i < triggers.length; i++) { 165 | $(triggers[i]).trigger(swiper.EVENT, card); 166 | } 167 | } 168 | } 169 | swipeStart = 0; 170 | buffer = []; 171 | } 172 | else { 173 | //Add keypress character to buffer 174 | buffer.push(String.fromCharCode(e.keyCode)); 175 | } 176 | } 177 | }; 178 | 179 | /** 180 | * The Card object represents a card read by the card reader. 181 | *
182 | * A card is always returned by the card reader and card.isValid() should 183 | * be checked before using the card object. 184 | * @param {string} line card reader track data 185 | * @returns {undefined} 186 | */ 187 | swiper.Card = function(line) { 188 | 189 | //Protected this scope 190 | var self = this; 191 | 192 | //Card tracks (only track 1 and track 2 are supported) 193 | this.track1 = {}; 194 | this.track1.valid = false; 195 | this.track2 = {}; 196 | this.track2.valid = false; 197 | this.track3 = {}; 198 | this.track3.valid = false; 199 | this.line = line; 200 | 201 | /** 202 | * Indicates if the card has valid track data. This method should be 203 | * called before using the card. 204 | *
205 | * Note that this method only indicates if the card contains 206 | * properly formatted track data. It does not indicate the validity of 207 | * the card number, expiration date, or any information contained on 208 | * the card. 209 | * @returns {boolean} true if the track data is valid, or false if not 210 | */ 211 | this.isValid = function() { 212 | return self.track1.valid || self.track2.valid; 213 | }; 214 | /** 215 | * Retrieves the primary account number for the card, or -1 if the 216 | * card is not valid. 217 | * @returns {number} primary account number, usually 16 digits long 218 | */ 219 | this.getNumber = function() { 220 | if (self.track1.valid) { 221 | return self.track1.number; 222 | } 223 | else if (self.track2.valid) { 224 | return self.track2.number; 225 | } 226 | return -1; 227 | }; 228 | /** 229 | * Retrieves the last four digits of the primary account number, or 230 | * -1 if the card is not valid. 231 | * @returns {number} last 4 digits of the primary account number 232 | */ 233 | this.getLast4 = function() { 234 | var numString = self.getNumber().toString(); 235 | if (numString.length > 4) { 236 | return parseInt(numString.substring(numString.length - 4)); 237 | } 238 | return -1; 239 | }; 240 | /** 241 | * Retrieves the expiration month (1-12) of the card, or -1 if the 242 | * card is invalid. 243 | *
244 | * Note that this method returns a number, so there is no formatting 245 | * for single digit months (1 is returned instead of 01 for January). 246 | * @returns {number} expiration month 247 | */ 248 | this.getExpMonth = function() { 249 | if (self.track1.valid) { 250 | return self.track1.expMonth; 251 | } 252 | else if (self.track2.valid) { 253 | return self.track2.expMonth; 254 | } 255 | return -1; 256 | }; 257 | /** 258 | * Retrieves the expiration year of the card, or -1 259 | * if the card is invalid. 260 | *
261 | * Note that this method returns the four digit year, not that last 262 | * two digits retrieved from the card. The last two digits can be 263 | * retrieved by subtracting 2000 from this value. 264 | * @returns {number} four digit expiration year 265 | */ 266 | this.getExpYear = function() { 267 | if (self.track1.valid) { 268 | return self.track1.expYear; 269 | } 270 | else if (self.track2.valid) { 271 | return self.track2.expYear; 272 | } 273 | return -1; 274 | }; 275 | /** 276 | * Retrieves the last name on the card, or an empty String if the 277 | * card is invalid. 278 | *
279 | * Note that most all card name data is uppercase and will need 280 | * formatting if this is a problem. 281 | * @returns {string} last name 282 | */ 283 | this.getLastName = function() { 284 | if (self.track1.valid) { 285 | return self.track1.nameParts[0]; 286 | } 287 | return ''; 288 | }; 289 | /** 290 | * Retrieves the first name on the card, or an empty String if the 291 | * card is invalid. 292 | *
293 | * Note that most all card name data is uppercase and will need 294 | * formatting if this is a problem. 295 | * @returns {string} first name 296 | */ 297 | this.getFirstName = function() { 298 | if (self.track1.valid) { 299 | return self.track1.nameParts[1]; 300 | } 301 | return ''; 302 | }; 303 | 304 | /** 305 | * Parses line data from a card reader into this Card object. 306 | * @param {string} line line data from card reader 307 | * @returns {Card} this card the line was parsed into 308 | */ 309 | this.parse = function(line) { 310 | var tracks = line.match(TRACK_REGEX); 311 | self.track1.data = tracks[1]; 312 | self.track2.data = tracks[2]; 313 | self.track3.data = tracks[3]; 314 | 315 | if (self.track1.data) { 316 | var t1 = self.track1.data.match(TRACK_1_REGEX); 317 | if (t1 !== null && t1.length === 7) { 318 | self.track1.valid = true; 319 | self.track1.format = t1[1]; 320 | self.track1.number = parseFloat(t1[2]); 321 | self.track1.name = t1[3]; 322 | self.track1.nameParts = self.track1.name.split('/'); 323 | self.track1.exp = parseInt(t1[4]); 324 | self.track1.expYear = parseInt('20' + t1[4].substring(0, 2)); 325 | self.track1.expMonth = parseInt(t1[4].substring(3)); 326 | self.track1.service = parseInt(t1[5]); 327 | self.track1.discretionary = t1[6]; 328 | } 329 | } 330 | if (self.track2.data) { 331 | var t2 = self.track2.data.match(TRACK_2_REGEX); 332 | if (t2 !== null && t2.length === 5) { 333 | self.track2.valid = true; 334 | self.track2.number = parseFloat(t2[1]); 335 | self.track2.exp = parseInt(t2[2]); 336 | self.track2.expYear = parseInt('20' + t2[2].substring(0, 2)); 337 | self.track2.expMonth = parseInt(t2[2].substring(3)); 338 | self.track2.service = parseInt(t2[3]); 339 | self.track2.discretionary = t2[4]; 340 | } 341 | } 342 | return self; 343 | }; 344 | 345 | if (line) { 346 | //Parse the card if line data is provided 347 | self.parse(line); 348 | } 349 | }; 350 | 351 | /** 352 | * Generates a unique hash number for the provided card. This number 353 | * may be negative. 354 | *
355 | * The hash can be used to uniquely identify a card across applications, 356 | * though it is not a substitute for a third-party card hash/fingerprint. 357 | * If the library being used to charge cards has a built-in hash/fingerprint 358 | * system, that should be used instead. 359 | * @param {Card} card the card to generate a hash for 360 | * @returns {number} unique hash number (may be negative) 361 | */ 362 | swiper.generateCardHash = function(card) { 363 | if (card && card.line) { 364 | var s = card.line; 365 | var hash = 0; 366 | for (var i = 0; i < s.length; i++) { 367 | var c = s.charCodeAt(i); 368 | hash = ((hash << 5) - hash) + c; 369 | hash = hash & hash; 370 | } 371 | return hash; 372 | } 373 | return 0; 374 | }; 375 | })(window, document, jQuery, Date); -------------------------------------------------------------------------------- /scripts/objects/movie.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A Movie object defines an individual movie that can be 3 | * attached to Showings. 4 | *
5 | * Each Movie is distinguished by an ID and by the movie title.
6 | * Both fields should be unique.
7 | * @constructor
8 | * @returns {Movie}
9 | */
10 | function Movie() {
11 |
12 | /**
13 | * This Movie
14 | * @type {Movie}
15 | */
16 | var self = this;
17 |
18 | /**
19 | * The ID of the movie, must be unique, -1 by default
20 | * @type {number}
21 | */
22 | this.id = -1;
23 | /**
24 | * The title of the movie, must be unique, blank by default
25 | * @type {string}
26 | */
27 | this.title = '';
28 | /**
29 | * The MPAA rating of the movie
30 | * @type {string}
31 | */
32 | this.rating = '';
33 | /**
34 | * The runtime, usually formatted as XXmin
35 | * @type {string}
36 | */
37 | this.runtime = '';
38 | /**
39 | * A link to the movie poster to be used in a 'src' attribute
40 | * @type {string}
41 | */
42 | this.poster = 'images/loading.gif';
43 | /**
44 | * A link to a blurred out version of the movie poster, to be used in a
45 | * 'src' attribute
46 | * @type {string}
47 | */
48 | this.posterBlur = this.poster;
49 | /**
50 | * A short synopsis of the movie's plot
51 | * @type {string}
52 | */
53 | this.synopsis = '';
54 | /**
55 | * An array of Strings, no larger than 7, of the movie cast members
56 | * @type {Array.
69 | * The provided element's children should implement any number of the
70 | * following tags to retrieve movie data:
71 | *
83 | * If the movie-poster tag is not an img tag, the background-image property
84 | * of the tag will be set to the movie poster.
85 | * @param {!jQuery|string} selector the jQuery selector or a string
86 | * selector to set movie data to
87 | * @returns {jQuery} the jQuery movie data selector
88 | */
89 | this.setMovieData = function(selector) {
90 | var s = $(selector);
91 | s.find('.movie-title').html(self.title);
92 | s.find('.movie-rating').html('Rating: ');
93 | s.find('.movie-runtime').html(self.runtime);
94 |
95 | s.find('.movie-poster').attr('src', self.poster);
96 | s.find('.movie-poster').css('background-image', 'url("' + self.poster + '")');
97 | s.find('.movie-poster.blur').attr('src', self.posterBlur);
98 | s.find('.movie-poster.blur').css('background-image', 'url("' + self.posterBlur + '")');
99 |
100 | s.find('.movie-synopsis').html(self.synopsis);
101 | var cast = [];
102 | var castLimit = (self.cast.length > 7 ) ? 7 : self.cast.length;
103 | for (var i = 0; i < castLimit; i++) {
104 | cast.push('
124 | * This div tag contains the movie poster and the movie's rating.
125 | * @returns {jQuery} jQuery div tag for movie carousel
126 | */
127 | this.getCarouselDiv = function() {
128 | var div = $('
5 | * For example, if there are four Tickets with a single
6 | * TicketType called "Standard", those four Tickets
7 | * would be added to a single Pricing named "Standard".
8 | *
9 | * The Pricing class is used by an individual Theater
10 | * to denote the price scheme of that theater.
11 | * @constructor
12 | * @param {number} id the ID of the pricing scheme, must be unique
13 | * @param {string} name the name of the pricing scheme, must be unique
14 | * @returns {Pricing}
15 | */
16 | function Pricing(id, name) {
17 |
18 | /**
19 | * The ID of this Pricing scheme, default -1
20 | * @type {number}
21 | */
22 | this.id = isNaN(id) ? -1 : id;
23 | /**
24 | * The name of this Pricing scheme, default blank
25 | * @type {string}
26 | */
27 | this.name = typeof name === 'string' ? name : '';
28 | /**
29 | * The Tickets associated with this Pricing scheme
30 | * @type {Array.
54 | * By default, the Receipt's tickets Array can contain multiples of each
55 | * Ticket. This function will condense those duplicates and return the
56 | * results.
57 | * @returns {Array.
4 | * A Session contains only two properties, the current selected Showing, and
5 | * the current Receipt.
6 | * @returns {Session}
7 | */
8 | function Session() {
9 |
10 | /**
11 | * The user's currently selected Showing
12 | * @type {Showing}
13 | */
14 | this.showing = new Showing();
15 | /**
16 | * The user's current Receipt
17 | * @type {Receipt}
18 | */
19 | this.receipt = new Receipt(this.showing);
20 | }
--------------------------------------------------------------------------------
/scripts/objects/showing.js:
--------------------------------------------------------------------------------
1 | /**
2 | * The Showing class represents a showtime for a specific Movie in a specific
3 | * Theater.
4 | * @param {number} id the ID of this Showing
5 | * @param {Movie} movie the Movie this Showing is for
6 | * @param {string} date a string description of the date of the Showing
7 | * @param {string} time a string description of the time of the Showing
8 | * @param {Theater} theater the Theater the Showing is in
9 | * @returns {Showing}
10 | */
11 | function Showing(id, movie, date, time, theater) {
12 |
13 | /**
14 | * This Showing
15 | * @type {Showing}
16 | */
17 | var self = this;
18 |
19 | /**
20 | * The ID of this Showing, defaults to -1
21 | * @type {number}
22 | */
23 | this.id = isNaN(id) ? -1 : id;
24 | /**
25 | * The Movie for this Showing, defaults to a new Movie object
26 | * @type {Movie}
27 | */
28 | this.movie = typeof movie === 'undefined' ? new Movie() : movie;
29 | /**
30 | * The date of this Showing as a string, usually MMM d. For example, Jan 1
31 | * @type {string}
32 | */
33 | this.date = typeof date === 'string' ? date : '';
34 | /**
35 | * The time of this Showing as a string, usually hh:mm. For example, 11:30am
36 | * @type {string}
37 | */
38 | this.time = typeof time === 'string' ? time : '';
39 | /**
40 | * The Theater this Showing is located in, defaults to a new Theater object
41 | * @type {Theater}
42 | */
43 | this.theater = typeof theater === 'undefined' ? new Theater() : theater;
44 |
45 | /**
46 | * Sets the provided tag's showing data related children to this Showing's
47 | * information.
48 | *
49 | * The provided element's children should implement any number of the
50 | * following tags to retrieve showing data:
51 | *
5 | * Each Theater has a Pricing scheme that defines the cost of Tickets for
6 | * the available seats.
7 | * @param {number} id the Theater ID, must be unique
8 | * @param {string} name the name of the Theater, must be unique
9 | * @param {Pricing} pricing the Pricing scheme for the Theater
10 | * @returns {Theater}
11 | */
12 | function Theater(id, name, pricing) {
13 |
14 | /**
15 | * This Theater
16 | * @type {Theater}
17 | */
18 | var self = this;
19 |
20 | /**
21 | * The Theater ID, defaults to -1
22 | * @type {number}
23 | */
24 | this.id = isNaN(id) ? -1 : id;
25 | /**
26 | * The Theater name, such as "Theater 4"
27 | * @type {string}
28 | */
29 | this.name = typeof name === 'string' ? name : '';
30 | /**
31 | * The Pricing scheme for this Theater
32 | * @type {Pricing}
33 | */
34 | this.pricing = typeof pricing === 'undefined' ? new Pricing() : pricing;
35 |
36 | /**
37 | * Sets the provided tag's theater data related children to this Theater's
38 | * information.
39 | *
40 | * The provided element's children should implement any number of the
41 | * following tags to retrieve theater data:
42 | *
4 | * TicketTypes can be universal, such as "Adult" or "Child", but the price for
5 | * each Ticket can change depending on the Theater type.
6 | * @param {TicketType} ticketType
7 | * @param {number} price
8 | * @returns {Ticket}
9 | */
10 | function Ticket(ticketType, price) {
11 |
12 | /**
13 | * This Ticket
14 | * @type {Ticket}
15 | */
16 | var self = this;
17 | /**
18 | * The TicketType of this Ticket
19 | * @type {TicketType}
20 | */
21 | this.ticketType = typeof ticketType === 'undefined' ? new TicketType() : ticketType;
22 | /**
23 | * The price of this Ticket, defaults to 0
24 | * @type number
25 | */
26 | this.price = typeof price === 'undefined' ? 0.00 : parseFloat(price);
27 |
28 | /**
29 | * Creates a ticket div that to display a read-only line of this
30 | * Ticket's information. This can be used in creating a summary of this
31 | * Ticket.
32 | * @returns {jQuery} the ticket summary jQuery div selector
33 | */
34 | this.createTicketDiv = function() {
35 | var quantity = typeof self.quantity === 'undefined' ? 1 : self.quantity;
36 | return $('
5 | * Individual prices for each TicketType is set in the Ticket class. TicketType
6 | * objects only define the name of the type of tickets available.
7 | * @param {number} id the ID of the TicketType, must be unique
8 | * @param {string} name the name of the TicketType, must be unique
9 | * @returns {TicketType}
10 | */
11 | function TicketType(id, name) {
12 |
13 | /**
14 | * The ID of this TicketType, defaults to -1
15 | * @type {number}
16 | */
17 | this.id = isNaN(id) ? -1 : id;
18 | /**
19 | * The name of this TicketType
20 | * @type {string}
21 | */
22 | this.name = typeof name === 'string' ? name : '';
23 | }
--------------------------------------------------------------------------------
/scripts/rotten.js:
--------------------------------------------------------------------------------
1 | (function(window, document, Math) {
2 | //Check if sandbox-scripts.js exists
3 | if (!window.sandbox) {
4 | throw new Error('rotten.js requires sandbox. sandbox-scripts.js is not loaded in the "sandbox-scripts" iframe.');
5 | }
6 |
7 | //Rotten Tomatoes Scope
8 | var rotten = {};
9 | window.rotten = rotten;
10 |
11 | /**
12 | * Called each time an image is successfully loaded in order to give
13 | * user feedback on the status of the movie image loading/resizing.
14 | * @param {number} index the number of images loaded
15 | * @param {number} length the max number of images to load
16 | * @param {string} message a short message about the status of loading
17 | * @returns {undefined}
18 | */
19 | rotten.progress = function(index, length, message) {};
20 | /**
21 | * Called when all movie data images have been loaded.
22 | * @returns {undefined}
23 | */
24 | rotten.finished = function() {};
25 | /**
26 | * Current number of images loaded.
27 | */
28 | var imagesLoaded = 0;
29 | /**
30 | * Checks if all movie data images have been loaded, and calls
31 | * rotten.finished if true.
32 | * @returns {undefined}
33 | */
34 | var checkFinished = function() {
35 | if (imagesLoaded === data.movies.length) {
36 | rotten.finished();
37 | }
38 | };
39 |
40 | /**
41 | * Retrieves movie data for movies currently in theaters using the
42 | * Rotten Tomatoes API and parses the data into the data.movies Array.
43 | * @param {Function} callback an optional function that is called upon
44 | * completing the movie retrieval
45 | * @returns {undefined}
46 | */
47 | rotten.getInTheatersData = function(callback) {
48 | window.addEventListener('message', function(e) {
49 | if (e.data.script === 'rotten') {
50 | rotten.parseMovieData(e.data.movies);
51 | if (typeof callback === 'function') {
52 | rotten.finished = callback;
53 | }
54 | }
55 | });
56 | window.sandbox.message({
57 | 'script': 'rotten'
58 | });
59 | };
60 |
61 | /**
62 | * Parses the provided array of movie data from Rotten Tomatoes into the
63 | * data.movies array.
64 | *
65 | * This function is automatically called by rotten.getInTheatersData.
66 | * @param {Array} movies
67 | * @returns {undefined}
68 | */
69 | rotten.parseMovieData = function(movies) {
70 | window.data.movies = [];
71 |
72 | for (var i = 0; i < movies.length; i++) {
73 | var m = movies[i];
74 | var movie = new Movie();
75 | movie.id = m.id;
76 | movie.title = m.title;
77 | movie.rating = m.mpaa_rating;
78 | movie.runtime = m.runtime + ' min';
79 | //Original is the highest resolution, but this can be replaced
80 | //with detailed if the network is an issue.
81 | //We're replacing "_tmb" with "_ori" due to a bug in the
82 | //Rotten Tomatoes API that sometimes returns only thumbnail images
83 | //(noted as https://link/to/thumb_tmb.jpg). This bypasses that bug
84 | //and retrieves the original size.
85 | m.posters.original = m.posters.original.replace('_tmb', '_ori');
86 |
87 | //Rotten Tomatoes API movie URLs are referencing
88 | //resizing.flixster.com for thumbnail sizes. This regex will
89 | //strip the resizing url part and return the original link, with
90 | //the larger size.
91 | var parts = /\/\d+x\d+\/(.+)$/.exec(m.posters.original);
92 | var originalPoster = parts[1] || parts[0];
93 | if (m.posters.original.indexOf('https://' > -1)) {
94 | originalPoster = 'https://' + originalPoster;
95 | } else {
96 | originalPoster = 'http://' + originalPoster;
97 | }
98 |
99 | rotten.getMoviePoster(originalPoster, movie);
100 | movie.synopsis = m.synopsis;
101 | for (var j = 0; j < m.abridged_cast.length; j++) {
102 | movie.cast.push(m.abridged_cast[j].name);
103 | }
104 | //No director data from Rotten Tomatoes
105 | //movie.directors = m.directors.slice();
106 | window.data.movies.push(movie);
107 | }
108 | };
109 |
110 | /**
111 | * Retrieves an external movie poster image and creates a blob that is
112 | * accessible within the chrome app.
113 | * @param {string} url the URL of the movie poster to retrieve
114 | * @param {Movie} movie the Movie to set the posterUrl property
115 | * @returns {undefined}
116 | */
117 | rotten.getMoviePoster = function(url, movie) {
118 | var xhr = new XMLHttpRequest();
119 | xhr.responseType = 'blob';
120 | xhr.onreadystatechange = function() {
121 | if (xhr.readyState === 4) {
122 | if (xhr.status === 200) {
123 | var blob = xhr.response;
124 | var blobUrl = window.URL.createObjectURL(blob);
125 | var image = new Image();
126 |
127 | image.src = blobUrl;
128 | image.onload = function() {
129 | var message = 'Loading "' + movie.title + '" ...';
130 | rotten.progress(imagesLoaded + 1, data.movies.length, message);
131 |
132 | var response = resizeImage(image, 300, true);
133 | movie.poster = response.normal;
134 | movie.posterBlur = response.blur;
135 |
136 | imagesLoaded++;
137 | checkFinished();
138 | };
139 | }
140 | }
141 | };
142 | xhr.open('GET', url);
143 | xhr.send();
144 | };
145 | /**
146 | * Resizes an image if its width is greater than the max width provided.
147 | *
148 | * This method returns an object with two data URLs, "normal" and "blur".
149 | * The "normal" data URL is the resized image, and the "blur" data URL is
150 | * the resized image with a blur filter applied.
151 | * @param {Image} img the Image to resize
152 | * @param {number} maxWidth the max width of the image
153 | * @returns {Object} Object with two properties, "normal" and "blur" which
154 | * contain data URLs to the normal and blurred images
155 | */
156 | var resizeImage = function(img, maxWidth) {
157 | var canvas = document.createElement('canvas');
158 |
159 | var width = img.width;
160 | var height = img.height;
161 |
162 | if (width > maxWidth) {
163 | height = Math.round(height *= maxWidth / width);
164 | width = maxWidth;
165 | }
166 |
167 | canvas.width = width;
168 | canvas.height = height;
169 | var ctx = canvas.getContext("2d");
170 | ctx.drawImage(img, 0, 0, width, height);
171 |
172 | var response = {};
173 | response.normal = canvas.toDataURL('image/jpeg');
174 |
175 | //See StackBlur.js for blur implementation
176 | stackBlurCanvasRGB(canvas, 0, 0, width, height, 5, 1);
177 | response.blur = canvas.toDataURL('image/jpeg');
178 | return response;
179 | };
180 | })(window, document, Math);
181 |
--------------------------------------------------------------------------------
/scripts/sandbox-rotten.js:
--------------------------------------------------------------------------------
1 | (function(window, document) {
2 | //Requires sandbox-script.js
3 | if (!window.sandbox) {
4 | throw new Error('sandbox-scripts.js is not loaded in the "sandbox-scripts" iframe.');
5 | }
6 |
7 | //Rotten Tomatoes Scope
8 | var rotten = {};
9 | window.rotten = rotten;
10 |
11 | /**
12 | * The Rotten Tomatoes API key (replace with your own)
13 | */
14 | rotten.API_KEY = 'mzjrz3v6uy5d3kg6tw63yk78';
15 | /**
16 | * URL to retrieve movies that are currently in theaters
17 | */
18 | rotten.IN_THEATERS_URL = 'http://api.rottentomatoes.com/api/public/v1.0/lists/movies/in_theaters.json';
19 | /**
20 | * Used to store Events waiting for a response from Rotten Tomatoes
21 | */
22 | rotten.events = {};
23 |
24 | /**
25 | * Rotten Tomatoes sandboxed message handler.
26 | *
27 | * Note that unlike sandbox-stripe.js, sandbox-rotten.js does not
28 | * implement checkScript() or getScriptUrl(). This is because this API
29 | * does not require an external script to be loaded in order to work.
30 | *
31 | * The reason Rotten Tomatoes' API must be sandboxed is to avoid the
32 | * same-origin policy that prevents it from running regularly. Sandboxing
33 | * API calls is a good way to avoid dealing with Cross-Origin Policy (CORS).
34 | * @param {Event} e message event
35 | * @returns {undefined}
36 | */
37 | rotten.messageHandler = function(e) {
38 | rotten.events[e.data.source] = e;
39 | getInTheaters();
40 | };
41 |
42 | /**
43 | * Retrieve movie data for movies currently in theaters.
44 | * @returns {undefined}
45 | */
46 | var getInTheaters = function() {
47 | //Define a callback for JSON padding (JSONP)
48 | var param = {
49 | 'apikey': rotten.API_KEY,
50 | 'page_limit': 20,
51 | 'page': 1,
52 | 'callback': 'rotten.callback'
53 | };
54 | var url = [];
55 | url.push(rotten.IN_THEATERS_URL);
56 | url.push('?');
57 | for (var key in param) {
58 | url.push(key);
59 | url.push('=');
60 | url.push(param[key]);
61 | url.push('&');
62 | }
63 | url.pop();
64 |
65 | var script = document.getElementById('jsonp-rotten');
66 | if (script !== null) {
67 | document.body.removeChild(script);
68 | }
69 | script = document.createElement('script');
70 | script.id = 'jsonp-rotten';
71 | script.src = url.join('');
72 | document.body.appendChild(script);
73 | };
74 | /**
75 | * Called when the JSON padding result has been loaded into the
76 | * document.
77 | * @param {Object} response movie data from Rotten Tomatoes
78 | * @returns {undefined}
79 | */
80 | rotten.callback = function(response) {
81 | //Return movie data for all events that requested it. Since the movie
82 | //data is the same no matter what event requests it, it is safe to
83 | //send the same data object to all events without checking their
84 | //source
85 | for (var source in rotten.events) {
86 | window.sandbox.returnMessage(rotten.events[source], {
87 | 'movies': response.movies
88 | });
89 | delete rotten.events[source];
90 | }
91 | };
92 | })(window, document);
--------------------------------------------------------------------------------
/scripts/sandbox-scripts.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
266 | Confirm that the tickets below are correct, then select the “Print Tickets” button to continue.
267 | Please retrieve your tickets from the ticket dispenser below. Thanks for choosing , and enjoy your movie!
72 | *
80 | * It is recommended that all tags are div tags, with the exception of the
81 | * movie-poster class, which should be on an img tag.
82 | * ').addClass('movie-poster').attr('src', self.poster));
130 | var movieData = $('').addClass('movie-data');
131 | movieData.append($('').addClass('movie-rating'));
132 | self.setMovieData(movieData);
133 | div.append(movieData);
134 |
135 | return div;
136 | };
137 | }
--------------------------------------------------------------------------------
/scripts/objects/pricing.js:
--------------------------------------------------------------------------------
1 | /**
2 | * The Pricing class is used to group several Ticket
3 | * objects under one price name scheme.
4 | *
52 | *
56 | * It is recommended that all tags are div tags.
57 | * @param {jQuery|string} selector the jQuery selector or a string
58 | * selector to set showing data to
59 | * @returns {jQuery} the jQuery showing data selector
60 | */
61 | this.setShowingData = function(selector) {
62 | var s = $(selector);
63 | s.find('.showing-date').html(self.date);
64 | s.find('.showing-time').html(self.time);
65 | s.find('.showing-type').html(self.theater.pricing.name);
66 | return s;
67 | };
68 |
69 | /**
70 | * Retrieves a button tag that contains this Showing's ID as a
71 | * "data-id" attribute, along with the Showing's time and type.
72 | * @returns {jQuery} the jQuery button tag
73 | */
74 | this.getShowingButton = function() {
75 | var button = $('').addClass('showing', 'showing-data').attr('data-id', self.id);
76 | button.append($('').addClass('showing-time'));
77 | button.append($('').addClass('showing-type'));
78 | self.setShowingData(button);
79 |
80 | return button;
81 | };
82 | }
--------------------------------------------------------------------------------
/scripts/objects/theater.js:
--------------------------------------------------------------------------------
1 | /**
2 | * The Theater class represents a theater in which Showings for a specific
3 | * Movie are held.
4 | *
43 | *
46 | * It is recommended that all tags are div tags.
47 | * @param {jQuery|string} selector the jQuery selector or a string
48 | * selector to set theater data to
49 | * @returns {jQuery} the jQuery theater data selector
50 | */
51 | this.setTheaterData = function(selector) {
52 | var s = $(selector);
53 | s.find('.theater-name').html(self.name);
54 | s.find('.theater-type').html(self.pricing.name);
55 | return s;
56 | };
57 | }
--------------------------------------------------------------------------------
/scripts/objects/ticket.js:
--------------------------------------------------------------------------------
1 | /**
2 | * The Ticket class represents a TicketType and its associated price.
3 | *
38 |
56 |
87 |
89 |
126 |
128 |
139 |
155 |
140 |
144 | Subtotal:
141 | $
142 |
143 |
145 |
149 | Tax:
146 | $
147 | 0.00
148 |
150 |
154 | Total:
151 | $
152 |
153 |
If the tickets below are not correct, please select the “Cancel” button and alert an attendant at the manned Box Office.
268 |