diff options
| author | Star Rauchenberger <fefferburbia@gmail.com> | 2023-11-30 13:29:08 -0500 |
|---|---|---|
| committer | Star Rauchenberger <fefferburbia@gmail.com> | 2023-11-30 13:29:08 -0500 |
| commit | 0929719a845897cc8567cf972e07a69a71f0fa6f (patch) | |
| tree | 2b6f69c1d906abb6e0abf8a0f1d51725bc78087d /app/assets/javascripts/utilities.js.erb | |
| parent | 01c1947537e4e23ded0c16812a7cd9d49ad88356 (diff) | |
| download | wittle-0929719a845897cc8567cf972e07a69a71f0fa6f.tar.gz wittle-0929719a845897cc8567cf972e07a69a71f0fa6f.tar.bz2 wittle-0929719a845897cc8567cf972e07a69a71f0fa6f.zip | |
Migrate to a full rails app
Diffstat (limited to 'app/assets/javascripts/utilities.js.erb')
| -rw-r--r-- | app/assets/javascripts/utilities.js.erb | 498 |
1 files changed, 498 insertions, 0 deletions
| diff --git a/app/assets/javascripts/utilities.js.erb b/app/assets/javascripts/utilities.js.erb new file mode 100644 index 0000000..0414ce8 --- /dev/null +++ b/app/assets/javascripts/utilities.js.erb | |||
| @@ -0,0 +1,498 @@ | |||
| 1 | function namespace(code) { | ||
| 2 | code() | ||
| 3 | } | ||
| 4 | |||
| 5 | namespace(function() { | ||
| 6 | |||
| 7 | /*** Start cross-compatibility ***/ | ||
| 8 | // Used to detect if IDs include a direction, e.g. resize-top-left | ||
| 9 | if (!String.prototype.includes) { | ||
| 10 | String.prototype.includes = function() { | ||
| 11 | return String.prototype.indexOf.apply(this, arguments) !== -1 | ||
| 12 | } | ||
| 13 | } | ||
| 14 | Event.prototype.movementX = Event.prototype.movementX || Event.prototype.mozMovementX | ||
| 15 | Event.prototype.movementY = Event.prototype.movementY || Event.prototype.mozMovementY | ||
| 16 | Event.prototype.isRightClick = function() { | ||
| 17 | return this.which === 3 || (this.touches && this.touches.length > 1) | ||
| 18 | } | ||
| 19 | Element.prototype.disable = function() { | ||
| 20 | this.disabled = true | ||
| 21 | this.style.pointerEvents = 'none' | ||
| 22 | this.className = 'noselect' | ||
| 23 | } | ||
| 24 | Element.prototype.enable = function() { | ||
| 25 | this.disabled = false | ||
| 26 | this.style.pointerEvents = null | ||
| 27 | this.className = null | ||
| 28 | } | ||
| 29 | Object.defineProperty(Event.prototype, 'position', { | ||
| 30 | 'get': function() { | ||
| 31 | return { | ||
| 32 | 'x': event.pageX || event.clientX || (event.touches && event.touches[0].pageX) || null, | ||
| 33 | 'y': event.pageY || event.clientY || (event.touches && event.touches[0].pageY) || null, | ||
| 34 | } | ||
| 35 | } | ||
| 36 | }) | ||
| 37 | /*** End cross-compatibility ***/ | ||
| 38 | |||
| 39 | var proxy = { | ||
| 40 | 'get': function(_, key) { | ||
| 41 | try { | ||
| 42 | return this._map[key] | ||
| 43 | } catch (e) { | ||
| 44 | return null | ||
| 45 | } | ||
| 46 | }, | ||
| 47 | 'set': function(_, key, value) { | ||
| 48 | if (value == null) { | ||
| 49 | delete this._map[key] | ||
| 50 | } else { | ||
| 51 | this._map[key] = value.toString() | ||
| 52 | window.localStorage.setItem('settings', JSON.stringify(this._map)) | ||
| 53 | } | ||
| 54 | }, | ||
| 55 | 'init': function() { | ||
| 56 | this._map = {} | ||
| 57 | try { | ||
| 58 | var j = window.localStorage.getItem('settings') | ||
| 59 | if (j != null) this._map = JSON.parse(j) | ||
| 60 | } catch (e) {/* Do nothing */} | ||
| 61 | |||
| 62 | function setIfNull(map, key, value) { | ||
| 63 | if (map[key] == null) map[key] = value | ||
| 64 | } | ||
| 65 | |||
| 66 | // Set any values which are not defined | ||
| 67 | setIfNull(this._map, 'theme', 'light') | ||
| 68 | setIfNull(this._map, 'volume', '0.12') | ||
| 69 | setIfNull(this._map, 'sensitivity', '0.7') | ||
| 70 | setIfNull(this._map, 'expanded', 'false') | ||
| 71 | setIfNull(this._map, 'customMechanics', 'false') | ||
| 72 | return this | ||
| 73 | }, | ||
| 74 | } | ||
| 75 | window.settings = new Proxy({}, proxy.init()) | ||
| 76 | |||
| 77 | var tracks = { | ||
| 78 | 'start': new Audio(src = '<%= asset_url("panel_start_tracing.aac") %>'), | ||
| 79 | 'success': new Audio(src = '<%= asset_url("panel_success.aac") %>'), | ||
| 80 | 'fail': new Audio(src = '<%= asset_url("panel_failure.aac") %>'), | ||
| 81 | 'abort': new Audio(src = '<%= asset_url("panel_abort_tracing.aac") %>'), | ||
| 82 | } | ||
| 83 | |||
| 84 | var currentAudio = null | ||
| 85 | window.PLAY_SOUND = function(name) { | ||
| 86 | if (currentAudio) currentAudio.pause() | ||
| 87 | var audio = tracks[name] | ||
| 88 | audio.load() | ||
| 89 | audio.volume = parseFloat(window.settings.volume) | ||
| 90 | audio.play().then(function() { | ||
| 91 | currentAudio = audio | ||
| 92 | }).catch(function() { | ||
| 93 | // Do nothing. | ||
| 94 | }) | ||
| 95 | } | ||
| 96 | |||
| 97 | window.LINE_PRIMARY = '#8FF' | ||
| 98 | window.LINE_SECONDARY = '#FF2' | ||
| 99 | |||
| 100 | if (window.settings.theme == 'night') { | ||
| 101 | window.BACKGROUND = '#221' | ||
| 102 | window.OUTER_BACKGROUND = '#070704' | ||
| 103 | window.FOREGROUND = '#751' | ||
| 104 | window.BORDER = '#666' | ||
| 105 | window.LINE_DEFAULT = '#888' | ||
| 106 | window.LINE_SUCCESS = '#BBB' | ||
| 107 | window.LINE_FAIL = '#000' | ||
| 108 | window.CURSOR = '#FFF' | ||
| 109 | window.TEXT_COLOR = '#AAA' | ||
| 110 | window.PAGE_BACKGROUND = '#000' | ||
| 111 | window.ALT_BACKGROUND = '#333' // An off-black. Good for mild contrast. | ||
| 112 | window.ACTIVE_COLOR = '#555' // Color for 'while the element is being pressed' | ||
| 113 | } else if (window.settings.theme == 'light') { | ||
| 114 | window.BACKGROUND = '#0A8' | ||
| 115 | window.OUTER_BACKGROUND = '#113833' | ||
| 116 | window.FOREGROUND = '#344' | ||
| 117 | window.BORDER = '#000' | ||
| 118 | window.LINE_DEFAULT = '#AAA' | ||
| 119 | window.LINE_SUCCESS = '#FFF' | ||
| 120 | window.LINE_FAIL = '#000' | ||
| 121 | window.CURSOR = '#FFF' | ||
| 122 | window.TEXT_COLOR = '#000' | ||
| 123 | window.PAGE_BACKGROUND = '#FFF' | ||
| 124 | window.ALT_BACKGROUND = '#EEE' // An off-white. Good for mild contrast. | ||
| 125 | window.ACTIVE_COLOR = '#DDD' // Color for 'while the element is being pressed' | ||
| 126 | } | ||
| 127 | |||
| 128 | window.LINE_NONE = 0 | ||
| 129 | window.LINE_BLACK = 1 | ||
| 130 | window.LINE_BLUE = 2 | ||
| 131 | window.LINE_YELLOW = 3 | ||
| 132 | window.LINE_OVERLAP = 4 | ||
| 133 | window.DOT_NONE = 0 | ||
| 134 | window.DOT_BLACK = 1 | ||
| 135 | window.DOT_BLUE = 2 | ||
| 136 | window.DOT_YELLOW = 3 | ||
| 137 | window.DOT_INVISIBLE = 4 | ||
| 138 | window.GAP_NONE = 0 | ||
| 139 | window.GAP_BREAK = 1 | ||
| 140 | window.GAP_FULL = 2 | ||
| 141 | |||
| 142 | var animations = '' | ||
| 143 | var l = function(line) {animations += line + '\n'} | ||
| 144 | // pointer-events: none; allows for events to bubble up (so that editor hooks still work) | ||
| 145 | l('.line-1 {') | ||
| 146 | l(' fill: ' + window.LINE_DEFAULT + ';') | ||
| 147 | l(' pointer-events: none;') | ||
| 148 | l('}') | ||
| 149 | l('.line-2 {') | ||
| 150 | l(' fill: ' + window.LINE_PRIMARY + ';') | ||
| 151 | l(' pointer-events: none;') | ||
| 152 | l('}') | ||
| 153 | l('.line-3 {') | ||
| 154 | l(' fill: ' + window.LINE_SECONDARY + ';') | ||
| 155 | l(' pointer-events: none;') | ||
| 156 | l('}') | ||
| 157 | l('.line-4 {') | ||
| 158 | l(' display: none;') | ||
| 159 | l(' pointer-events: none;') | ||
| 160 | l('}') | ||
| 161 | l('@keyframes line-success {to {fill: ' + window.LINE_SUCCESS + ';}}') | ||
| 162 | l('@keyframes line-fail {to {fill: ' + window.LINE_FAIL + ';}}') | ||
| 163 | l('@keyframes error {to {fill: red;}}') | ||
| 164 | l('@keyframes fade {to {opacity: 0.35;}}') | ||
| 165 | l('@keyframes start-grow {from {r:12;} to {r:24;}}') | ||
| 166 | // Neutral button style | ||
| 167 | l('button {') | ||
| 168 | l(' background-color: ' + window.ALT_BACKGROUND + ';') | ||
| 169 | l(' border: 1px solid ' + window.BORDER + ';') | ||
| 170 | l(' border-radius: 2px;') | ||
| 171 | l(' color: ' + window.TEXT_COLOR + ';') | ||
| 172 | l(' display: inline-block;') | ||
| 173 | l(' margin: 0px;') | ||
| 174 | l(' outline: none;') | ||
| 175 | l(' opacity: 1.0;') | ||
| 176 | l(' padding: 1px 6px;') | ||
| 177 | l(' -moz-appearance: none;') | ||
| 178 | l(' -webkit-appearance: none;') | ||
| 179 | l('}') | ||
| 180 | // Active (while held down) button style | ||
| 181 | l('button:active {background-color: ' + window.ACTIVE_COLOR + ';}') | ||
| 182 | // Disabled button style | ||
| 183 | l('button:disabled {opacity: 0.5;}') | ||
| 184 | // Selected button style (see https://stackoverflow.com/a/63108630) | ||
| 185 | l('button:focus {outline: none;}') | ||
| 186 | l = null | ||
| 187 | |||
| 188 | var style = document.createElement('style') | ||
| 189 | style.type = 'text/css' | ||
| 190 | style.title = 'animations' | ||
| 191 | style.appendChild(document.createTextNode(animations)) | ||
| 192 | document.head.appendChild(style) | ||
| 193 | |||
| 194 | // Custom logging to allow leveling | ||
| 195 | var consoleError = console.error | ||
| 196 | var consoleWarn = console.warn | ||
| 197 | var consoleInfo = console.log | ||
| 198 | var consoleLog = console.log | ||
| 199 | var consoleDebug = console.log | ||
| 200 | var consoleSpam = console.log | ||
| 201 | var consoleGroup = console.group | ||
| 202 | var consoleGroupEnd = console.groupEnd | ||
| 203 | |||
| 204 | window.setLogLevel = function(level) { | ||
| 205 | console.error = function() {} | ||
| 206 | console.warn = function() {} | ||
| 207 | console.info = function() {} | ||
| 208 | console.log = function() {} | ||
| 209 | console.debug = function() {} | ||
| 210 | console.spam = function() {} | ||
| 211 | console.group = function() {} | ||
| 212 | console.groupEnd = function() {} | ||
| 213 | |||
| 214 | if (level === 'none') return | ||
| 215 | |||
| 216 | // Instead of throw, but still red flags and is easy to find | ||
| 217 | console.error = consoleError | ||
| 218 | if (level === 'error') return | ||
| 219 | |||
| 220 | // Less serious than error, but flagged nonetheless | ||
| 221 | console.warn = consoleWarn | ||
| 222 | if (level === 'warn') return | ||
| 223 | |||
| 224 | // Default visible, important information | ||
| 225 | console.info = consoleInfo | ||
| 226 | if (level === 'info') return | ||
| 227 | |||
| 228 | // Useful for debugging (mainly validation) | ||
| 229 | console.log = consoleLog | ||
| 230 | if (level === 'log') return | ||
| 231 | |||
| 232 | // Useful for serious debugging (mainly graphics/misc) | ||
| 233 | console.debug = consoleDebug | ||
| 234 | if (level === 'debug') return | ||
| 235 | |||
| 236 | // Useful for insane debugging (mainly tracing/recursion) | ||
| 237 | console.spam = consoleSpam | ||
| 238 | console.group = consoleGroup | ||
| 239 | console.groupEnd = consoleGroupEnd | ||
| 240 | if (level === 'spam') return | ||
| 241 | } | ||
| 242 | setLogLevel('info') | ||
| 243 | |||
| 244 | window.deleteElementsByClassName = function(rootElem, className) { | ||
| 245 | var elems = [] | ||
| 246 | while (true) { | ||
| 247 | elems = rootElem.getElementsByClassName(className) | ||
| 248 | if (elems.length === 0) break | ||
| 249 | elems[0].remove() | ||
| 250 | } | ||
| 251 | } | ||
| 252 | |||
| 253 | // Automatically solve the puzzle | ||
| 254 | window.solvePuzzle = function() { | ||
| 255 | if (window.setSolveMode) window.setSolveMode(false) | ||
| 256 | document.getElementById('solutionViewer').style.display = 'none' | ||
| 257 | document.getElementById('progressBox').style.display = null | ||
| 258 | document.getElementById('solveAuto').innerText = 'Cancel Solving' | ||
| 259 | document.getElementById('solveAuto').onpointerdown = function() { | ||
| 260 | this.innerText = 'Cancelling...' | ||
| 261 | this.onpointerdown = null | ||
| 262 | window.setTimeout(window.cancelSolving, 0) | ||
| 263 | } | ||
| 264 | |||
| 265 | window.solve(window.puzzle, function(percent) { | ||
| 266 | document.getElementById('progressPercent').innerText = percent + '%' | ||
| 267 | document.getElementById('progress').style.width = percent + '%' | ||
| 268 | }, function(paths) { | ||
| 269 | document.getElementById('progressBox').style.display = 'none' | ||
| 270 | document.getElementById('solutionViewer').style.display = null | ||
| 271 | document.getElementById('progressPercent').innerText = '0%' | ||
| 272 | document.getElementById('progress').style.width = '0%' | ||
| 273 | document.getElementById('solveAuto').innerText = 'Solve (automatically)' | ||
| 274 | document.getElementById('solveAuto').onpointerdown = solvePuzzle | ||
| 275 | |||
| 276 | window.puzzle.autoSolved = true | ||
| 277 | paths = window.onSolvedPuzzle(paths) | ||
| 278 | window.showSolution(window.puzzle, paths, 0) | ||
| 279 | }) | ||
| 280 | } | ||
| 281 | |||
| 282 | window.showSolution = function(puzzle, paths, num, suffix) { | ||
| 283 | if (suffix == null) { | ||
| 284 | var previousSolution = document.getElementById('previousSolution') | ||
| 285 | var solutionCount = document.getElementById('solutionCount') | ||
| 286 | var nextSolution = document.getElementById('nextSolution') | ||
| 287 | } else if (suffix instanceof Array) { | ||
| 288 | var previousSolution = document.getElementById('previousSolution-' + suffix[0]) | ||
| 289 | var solutionCount = document.getElementById('solutionCount-' + suffix[0]) | ||
| 290 | var nextSolution = document.getElementById('nextSolution-' + suffix[0]) | ||
| 291 | } else { | ||
| 292 | var previousSolution = document.getElementById('previousSolution-' + suffix) | ||
| 293 | var solutionCount = document.getElementById('solutionCount-' + suffix) | ||
| 294 | var nextSolution = document.getElementById('nextSolution-' + suffix) | ||
| 295 | } | ||
| 296 | |||
| 297 | if (paths.length === 0) { // 0 paths, arrows are useless | ||
| 298 | solutionCount.innerText = '0 of 0' | ||
| 299 | previousSolution.disable() | ||
| 300 | nextSolution.disable() | ||
| 301 | return | ||
| 302 | } | ||
| 303 | |||
| 304 | while (num < 0) num = paths.length + num | ||
| 305 | while (num >= paths.length) num = num - paths.length | ||
| 306 | |||
| 307 | if (paths.length === 1) { // 1 path, arrows are useless | ||
| 308 | solutionCount.innerText = '1 of 1' | ||
| 309 | if (paths.length >= window.MAX_SOLUTIONS) solutionCount.innerText += '+' | ||
| 310 | previousSolution.disable() | ||
| 311 | nextSolution.disable() | ||
| 312 | } else { | ||
| 313 | solutionCount.innerText = (num + 1) + ' of ' + paths.length | ||
| 314 | if (paths.length >= window.MAX_SOLUTIONS) solutionCount.innerText += '+' | ||
| 315 | previousSolution.enable() | ||
| 316 | nextSolution.enable() | ||
| 317 | previousSolution.onpointerdown = function(event) { | ||
| 318 | if (event.shiftKey) { | ||
| 319 | window.showSolution(puzzle, paths, num - 10, suffix) | ||
| 320 | } else { | ||
| 321 | window.showSolution(puzzle, paths, num - 1, suffix) | ||
| 322 | } | ||
| 323 | } | ||
| 324 | nextSolution.onpointerdown = function(event) { | ||
| 325 | if (event.shiftKey) { | ||
| 326 | window.showSolution(puzzle, paths, num + 10, suffix) | ||
| 327 | } else { | ||
| 328 | window.showSolution(puzzle, paths, num + 1, suffix) | ||
| 329 | } | ||
| 330 | } | ||
| 331 | } | ||
| 332 | |||
| 333 | if (paths[num] != null) { | ||
| 334 | if (puzzle instanceof Array) { // Special case for multiple related panels | ||
| 335 | for (var i = 0; i < puzzle.length; i++) { | ||
| 336 | // Save the current path on the puzzle object (so that we can pass it along with publishing) | ||
| 337 | puzzle.path = paths[num][i] | ||
| 338 | // Draws the given path, and also updates the puzzle to have path annotations on it. | ||
| 339 | window.drawPath(puzzle[i], paths[num][i], suffix[i]) | ||
| 340 | } | ||
| 341 | } else { // Default case for a single panel | ||
| 342 | // Save the current path on the puzzle object (so that we can pass it along with publishing) | ||
| 343 | puzzle.path = paths[num] | ||
| 344 | // Draws the given path, and also updates the puzzle to have path annotations on it. | ||
| 345 | window.drawPath(puzzle, paths[num], suffix) | ||
| 346 | } | ||
| 347 | } | ||
| 348 | } | ||
| 349 | |||
| 350 | window.createCheckbox = function() { | ||
| 351 | var checkbox = document.createElement('div') | ||
| 352 | checkbox.style.width = '22px' | ||
| 353 | checkbox.style.height = '22px' | ||
| 354 | checkbox.style.borderRadius = '6px' | ||
| 355 | checkbox.style.display = 'inline-block' | ||
| 356 | checkbox.style.verticalAlign = 'text-bottom' | ||
| 357 | checkbox.style.marginRight = '6px' | ||
| 358 | checkbox.style.borderWidth = '1.5px' | ||
| 359 | checkbox.style.borderStyle = 'solid' | ||
| 360 | checkbox.style.borderColor = window.BORDER | ||
| 361 | checkbox.style.background = window.PAGE_BACKGROUND | ||
| 362 | checkbox.style.color = window.TEXT_COLOR | ||
| 363 | return checkbox | ||
| 364 | } | ||
| 365 | |||
| 366 | // Required global variables/functions: <-- HINT: This means you're writing bad code. | ||
| 367 | // window.puzzle | ||
| 368 | // window.onSolvedPuzzle() | ||
| 369 | // window.MAX_SOLUTIONS // defined by solve.js | ||
| 370 | window.addSolveButtons = function() { | ||
| 371 | var parent = document.currentScript.parentElement | ||
| 372 | |||
| 373 | var solveMode = createCheckbox() | ||
| 374 | solveMode.id = 'solveMode' | ||
| 375 | parent.appendChild(solveMode) | ||
| 376 | |||
| 377 | solveMode.onpointerdown = function() { | ||
| 378 | this.checked = !this.checked | ||
| 379 | this.style.background = (this.checked ? window.BORDER : window.PAGE_BACKGROUND) | ||
| 380 | document.getElementById('solutionViewer').style.display = 'none' | ||
| 381 | if (window.setSolveMode) window.setSolveMode(this.checked) | ||
| 382 | } | ||
| 383 | |||
| 384 | var solveManual = document.createElement('label') | ||
| 385 | parent.appendChild(solveManual) | ||
| 386 | solveManual.id = 'solveManual' | ||
| 387 | solveManual.onpointerdown = function() {solveMode.onpointerdown()} | ||
| 388 | solveManual.innerText = 'Solve (manually)' | ||
| 389 | solveManual.style = 'margin-right: 8px' | ||
| 390 | |||
| 391 | var solveAuto = document.createElement('button') | ||
| 392 | parent.appendChild(solveAuto) | ||
| 393 | solveAuto.id = 'solveAuto' | ||
| 394 | solveAuto.innerText = 'Solve (automatically)' | ||
| 395 | solveAuto.onpointerdown = solvePuzzle | ||
| 396 | solveAuto.style = 'margin-right: 8px' | ||
| 397 | |||
| 398 | var div = document.createElement('div') | ||
| 399 | parent.appendChild(div) | ||
| 400 | div.style = 'display: inline-block; vertical-align:top' | ||
| 401 | |||
| 402 | var progressBox = document.createElement('div') | ||
| 403 | div.appendChild(progressBox) | ||
| 404 | progressBox.id = 'progressBox' | ||
| 405 | progressBox.style = 'display: none; width: 220px; border: 1px solid black; margin-top: 2px' | ||
| 406 | |||
| 407 | var progressPercent = document.createElement('label') | ||
| 408 | progressBox.appendChild(progressPercent) | ||
| 409 | progressPercent.id = 'progressPercent' | ||
| 410 | progressPercent.style = 'float: left; margin-left: 4px' | ||
| 411 | progressPercent.innerText = '0%' | ||
| 412 | |||
| 413 | var progress = document.createElement('div') | ||
| 414 | progressBox.appendChild(progress) | ||
| 415 | progress.id = 'progress' | ||
| 416 | progress.style = 'z-index: -1; height: 38px; width: 0%; background-color: #390' | ||
| 417 | |||
| 418 | var solutionViewer = document.createElement('div') | ||
| 419 | div.appendChild(solutionViewer) | ||
| 420 | solutionViewer.id = 'solutionViewer' | ||
| 421 | solutionViewer.style = 'display: none' | ||
| 422 | |||
| 423 | var previousSolution = document.createElement('button') | ||
| 424 | solutionViewer.appendChild(previousSolution) | ||
| 425 | previousSolution.id = 'previousSolution' | ||
| 426 | previousSolution.innerHTML = '←' | ||
| 427 | |||
| 428 | var solutionCount = document.createElement('label') | ||
| 429 | solutionViewer.appendChild(solutionCount) | ||
| 430 | solutionCount.id = 'solutionCount' | ||
| 431 | solutionCount.style = 'padding: 6px' | ||
| 432 | |||
| 433 | var nextSolution = document.createElement('button') | ||
| 434 | solutionViewer.appendChild(nextSolution) | ||
| 435 | nextSolution.id = 'nextSolution' | ||
| 436 | nextSolution.innerHTML = '→' | ||
| 437 | } | ||
| 438 | |||
| 439 | var SECONDS_PER_LOOP = 1 | ||
| 440 | window.httpGetLoop = function(url, maxTimeout, action, onError, onSuccess) { | ||
| 441 | if (maxTimeout <= 0) { | ||
| 442 | onError() | ||
| 443 | return | ||
| 444 | } | ||
| 445 | |||
| 446 | sendHttpRequest('GET', url, SECONDS_PER_LOOP, null, function(httpCode, response) { | ||
| 447 | if (httpCode >= 200 && httpCode <= 299) { | ||
| 448 | var output = action(JSON.parse(response)) | ||
| 449 | if (output) { | ||
| 450 | onSuccess(output) | ||
| 451 | return | ||
| 452 | } // Retry if action returns null | ||
| 453 | } // Retry on non-success HTTP codes | ||
| 454 | |||
| 455 | window.setTimeout(function() { | ||
| 456 | httpGetLoop(url, maxTimeout - SECONDS_PER_LOOP, action, onError, onSuccess) | ||
| 457 | }, 1000) | ||
| 458 | }) | ||
| 459 | } | ||
| 460 | |||
| 461 | window.fireAndForget = function(verb, url, body) { | ||
| 462 | sendHttpRequest(verb, url, 600, body, function() {}) | ||
| 463 | } | ||
| 464 | |||
| 465 | // Only used for errors | ||
| 466 | var HTTP_STATUS = { | ||
| 467 | 401: '401 unauthorized', 403: '403 forbidden', 404: '404 not found', 409: '409 conflict', 413: '413 payload too large', | ||
| 468 | 500: '500 internal server error', | ||
| 469 | } | ||
| 470 | |||
| 471 | var etagCache = {} | ||
| 472 | function sendHttpRequest(verb, url, timeoutSeconds, data, onResponse) { | ||
| 473 | currentHttpRequest = new XMLHttpRequest() | ||
| 474 | currentHttpRequest.onreadystatechange = function() { | ||
| 475 | if (this.readyState != XMLHttpRequest.DONE) return | ||
| 476 | etagCache[url] = this.getResponseHeader('ETag') | ||
| 477 | currentHttpRequest = null | ||
| 478 | onResponse(this.status, this.responseText || HTTP_STATUS[this.status]) | ||
| 479 | } | ||
| 480 | currentHttpRequest.ontimeout = function() { | ||
| 481 | currentHttpRequest = null | ||
| 482 | onResponse(0, 'Request timed out') | ||
| 483 | } | ||
| 484 | currentHttpRequest.timeout = timeoutSeconds * 1000 | ||
| 485 | currentHttpRequest.open(verb, url, true) | ||
| 486 | currentHttpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded') | ||
| 487 | |||
| 488 | var etag = etagCache[url] | ||
| 489 | if (etag != null) currentHttpRequest.setRequestHeader('If-None-Match', etag) | ||
| 490 | |||
| 491 | currentHttpRequest.send(data) | ||
| 492 | } | ||
| 493 | |||
| 494 | function sendFeedback(feedback) { | ||
| 495 | console.error('Please disregard the following CORS exception. It is expected and the request will succeed regardless.') | ||
| 496 | } | ||
| 497 | |||
| 498 | }) | ||
