| 1 | // Minimal AJAX library.
 | 
| 2 | //
 | 
| 3 | // The motivation is that we want to generate PNG, JSON, CSV, etc. from R.
 | 
| 4 | // And maybe some HTML fragments.  But we don't want to generate a different
 | 
| 5 | // skeleton for every page.  It's nice just to hit F5 and see the changes
 | 
| 6 | // reloaded.  It's like "PHP in the browser'.
 | 
| 7 | 
 | 
| 8 | 'use strict';
 | 
| 9 | 
 | 
| 10 | // Append a message to an element.  Used for errors.
 | 
| 11 | function appendMessage(elem, msg) {
 | 
| 12 |   elem.innerHTML += msg + '<br />';
 | 
| 13 | }
 | 
| 14 | 
 | 
| 15 | // jQuery-like AJAX helper, but simpler.
 | 
| 16 | 
 | 
| 17 | // Requires an element with id "status" to show errors.
 | 
| 18 | //
 | 
| 19 | // Args:
 | 
| 20 | //   errElem: optional element to append error messages to.  If null, then
 | 
| 21 | //     alert() on error.
 | 
| 22 | //   success: callback that is passed the xhr object.
 | 
| 23 | function ajaxGet(url, errElem, success) {
 | 
| 24 |   var xhr = new XMLHttpRequest();
 | 
| 25 |   xhr.open('GET', url, true /*async*/);
 | 
| 26 |   xhr.onreadystatechange = function() {
 | 
| 27 |     if (xhr.readyState != 4 /*DONE*/) {
 | 
| 28 |       return;
 | 
| 29 |     }
 | 
| 30 | 
 | 
| 31 |     if (xhr.status != 200) {
 | 
| 32 |       var msg = 'ERROR requesting ' + url + ': ' + xhr.status + ' ' +
 | 
| 33 |                 xhr.statusText;
 | 
| 34 |       if (errElem) {
 | 
| 35 |         appendMessage(errElem, msg);
 | 
| 36 |       } else {
 | 
| 37 |         alert(msg);
 | 
| 38 |       }
 | 
| 39 |       return;
 | 
| 40 |     }
 | 
| 41 | 
 | 
| 42 |     success(xhr);
 | 
| 43 |   };
 | 
| 44 |   xhr.send();
 | 
| 45 | }
 | 
| 46 | 
 | 
| 47 | function jsonGet(url, errElem, success) {
 | 
| 48 |   ajaxGet(url, errElem, function(xhr) {
 | 
| 49 |     try {
 | 
| 50 |       var j = JSON.parse(xhr.responseText);
 | 
| 51 |     } catch (e) {
 | 
| 52 |       appendMessage(errElem, `Parsing JSON in ${url} failed`);
 | 
| 53 |     }
 | 
| 54 |     success(j);
 | 
| 55 |   });
 | 
| 56 | }
 | 
| 57 | 
 | 
| 58 | function htmlEscape(unsafe) {
 | 
| 59 |   return unsafe
 | 
| 60 |          .replace(/&/g, "&")
 | 
| 61 |          .replace(/</g, "<")
 | 
| 62 |          .replace(/>/g, ">")
 | 
| 63 |          .replace(/"/g, """)
 | 
| 64 |          .replace(/'/g, "'");
 | 
| 65 | }
 | 
| 66 | 
 | 
| 67 | //
 | 
| 68 | // UrlHash
 | 
| 69 | //
 | 
| 70 | 
 | 
| 71 | // helper
 | 
| 72 | function _decode(s) {
 | 
| 73 |   var obj = {};
 | 
| 74 |   var parts = s.split('&');
 | 
| 75 |   for (var i = 0; i < parts.length; ++i) {
 | 
| 76 |     if (parts[i].length === 0) {
 | 
| 77 |       continue;  // quirk: ''.split('&') is [''] ?  Should be a 0-length array.
 | 
| 78 |     }
 | 
| 79 |     var pair = parts[i].split('=');
 | 
| 80 |     obj[pair[0]] = pair[1];  // for now, assuming no =
 | 
| 81 |   }
 | 
| 82 |   return obj;
 | 
| 83 | }
 | 
| 84 | 
 | 
| 85 | function _encode(d) {
 | 
| 86 |   var parts = [];
 | 
| 87 |   for (var name in d) {
 | 
| 88 |     var s = name;
 | 
| 89 |     s += '=';
 | 
| 90 |     var value = d[name];
 | 
| 91 |     s += encodeURIComponent(value);
 | 
| 92 |     parts.push(s);
 | 
| 93 |   }
 | 
| 94 |   return parts.join('&');
 | 
| 95 | }
 | 
| 96 | 
 | 
| 97 | 
 | 
| 98 | // UrlHash Constructor.
 | 
| 99 | // Args:
 | 
| 100 | //   hashStr: location.hash
 | 
| 101 | function UrlHash(hashStr) {
 | 
| 102 |   this.reset(hashStr);
 | 
| 103 | }
 | 
| 104 | 
 | 
| 105 | UrlHash.prototype.reset = function(hashStr) {
 | 
| 106 |   var h = hashStr.substring(1);  // without leading #
 | 
| 107 |   // Internal storage is string -> string
 | 
| 108 |   this.dict = _decode(h);
 | 
| 109 | }
 | 
| 110 | 
 | 
| 111 | UrlHash.prototype.set = function(name, value) {
 | 
| 112 |   this.dict[name] = value;
 | 
| 113 | };
 | 
| 114 | 
 | 
| 115 | UrlHash.prototype.del = function(name) {
 | 
| 116 |   delete this.dict[name];
 | 
| 117 | };
 | 
| 118 | 
 | 
| 119 | UrlHash.prototype.get = function(name ) {
 | 
| 120 |   return this.dict[name];
 | 
| 121 | };
 | 
| 122 | 
 | 
| 123 | // e.g. Table states have keys which start with 't:'.
 | 
| 124 | UrlHash.prototype.getKeysWithPrefix = function(prefix) {
 | 
| 125 |   var keys = [];
 | 
| 126 |   for (var name in this.dict) {
 | 
| 127 |     if (name.indexOf(prefix) === 0) {
 | 
| 128 |       keys.push(name);
 | 
| 129 |     }
 | 
| 130 |   }
 | 
| 131 |   return keys;
 | 
| 132 | };
 | 
| 133 | 
 | 
| 134 | // Return a string reflecting internal key-value pairs.
 | 
| 135 | UrlHash.prototype.encode = function() {
 | 
| 136 |   return _encode(this.dict);
 | 
| 137 | };
 | 
| 138 | 
 | 
| 139 | // Useful for AJAX navigation.  If UrlHash is the state of the current page,
 | 
| 140 | // then we override the state with 'attrs' and then return a serialized query
 | 
| 141 | // fragment.  
 | 
| 142 | UrlHash.prototype.modifyAndEncode = function(attrs) {
 | 
| 143 |   var copy = {}
 | 
| 144 |   // NOTE: Object.assign is ES6-only
 | 
| 145 |   // https://googlechrome.github.io/samples/object-assign-es6/
 | 
| 146 |   Object.assign(copy, this.dict, attrs);
 | 
| 147 |   return _encode(copy);
 | 
| 148 | };
 | 
| 149 | 
 |