PageRenderTime 5ms CodeModel.GetById 6ms app.highlight 68ms RepoModel.GetById 1ms app.codeStats 0ms

/ckan/public/scripts/vendor/recline/recline.js

Relevant Search: With Applications for Solr and Elasticsearch

For more in depth reading about search, ranking and generally everything you could ever want to know about how lucene, elasticsearch or solr work under the hood I highly suggest this book. Easily one of the most interesting technical books I have read in a long time. If you are tasked with solving search relevance problems even if not in Solr or Elasticsearch it should be your first reference. Amazon Affiliate Link
https://bitbucket.org/kindly/ckan2
JavaScript | 2045 lines | 1560 code | 143 blank | 342 comment | 156 complexity | f54c772bf759dbf8f5b24688d1ca132e MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1// importScripts('lib/underscore.js'); 
   2
   3onmessage = function(message) {
   4  
   5  function parseCSV(rawCSV) {
   6    var patterns = new RegExp((
   7      // Delimiters.
   8      "(\\,|\\r?\\n|\\r|^)" +
   9      // Quoted fields.
  10      "(?:\"([^\"]*(?:\"\"[^\"]*)*)\"|" +
  11      // Standard fields.
  12      "([^\"\\,\\r\\n]*))"
  13    ), "gi");
  14
  15    var rows = [[]], matches = null;
  16
  17    while (matches = patterns.exec(rawCSV)) {
  18      var delimiter = matches[1];
  19
  20      if (delimiter.length && (delimiter !== ",")) rows.push([]);
  21
  22      if (matches[2]) {
  23        var value = matches[2].replace(new RegExp("\"\"", "g"), "\"");
  24      } else {
  25        var value = matches[3];
  26      }
  27      rows[rows.length - 1].push(value);
  28    }
  29
  30    if(_.isEqual(rows[rows.length -1], [""])) rows.pop();
  31
  32    var docs = [];
  33    var headers = _.first(rows);
  34    _.each(_.rest(rows), function(row, rowIDX) {
  35      var doc = {};
  36      _.each(row, function(cell, idx) {      
  37        doc[headers[idx]] = cell;
  38      })
  39      docs.push(doc);
  40    })
  41
  42    return docs;
  43  }
  44  
  45  var docs = parseCSV(message.data.data);
  46  
  47  var req = new XMLHttpRequest();
  48
  49  req.onprogress = req.upload.onprogress = function(e) {
  50    if(e.lengthComputable) postMessage({ percent: (e.loaded / e.total) * 100 });
  51  };
  52  
  53  req.onreadystatechange = function() { if (req.readyState == 4) postMessage({done: true, response: req.responseText}) };
  54  req.open('POST', message.data.url);
  55  req.setRequestHeader('Content-Type', 'application/json');
  56  req.send(JSON.stringify({docs: docs}));
  57};
  58// adapted from https://github.com/harthur/costco. heather rules
  59
  60var costco = function() {
  61  
  62  function evalFunction(funcString) {
  63    try {
  64      eval("var editFunc = " + funcString);
  65    } catch(e) {
  66      return {errorMessage: e+""};
  67    }
  68    return editFunc;
  69  }
  70  
  71  function previewTransform(docs, editFunc, currentColumn) {
  72    var preview = [];
  73    var updated = mapDocs($.extend(true, {}, docs), editFunc);
  74    for (var i = 0; i < updated.docs.length; i++) {      
  75      var before = docs[i]
  76        , after = updated.docs[i]
  77        ;
  78      if (!after) after = {};
  79      if (currentColumn) {
  80        preview.push({before: JSON.stringify(before[currentColumn]), after: JSON.stringify(after[currentColumn])});      
  81      } else {
  82        preview.push({before: JSON.stringify(before), after: JSON.stringify(after)});      
  83      }
  84    }
  85    return preview;
  86  }
  87
  88  function mapDocs(docs, editFunc) {
  89    var edited = []
  90      , deleted = []
  91      , failed = []
  92      ;
  93    
  94    var updatedDocs = _.map(docs, function(doc) {
  95      try {
  96        var updated = editFunc(_.clone(doc));
  97      } catch(e) {
  98        failed.push(doc);
  99        return;
 100      }
 101      if(updated === null) {
 102        updated = {_deleted: true};
 103        edited.push(updated);
 104        deleted.push(doc);
 105      }
 106      else if(updated && !_.isEqual(updated, doc)) {
 107        edited.push(updated);
 108      }
 109      return updated;      
 110    });
 111    
 112    return {
 113      edited: edited, 
 114      docs: updatedDocs, 
 115      deleted: deleted, 
 116      failed: failed
 117    };
 118  }
 119  
 120  return {
 121    evalFunction: evalFunction,
 122    previewTransform: previewTransform,
 123    mapDocs: mapDocs
 124  };
 125}();
 126// # Recline Backbone Models
 127this.recline = this.recline || {};
 128this.recline.Model = this.recline.Model || {};
 129
 130(function($, my) {
 131
 132// ## A Dataset model
 133//
 134// A model must have the following (Backbone) attributes:
 135//
 136// * fields: (aka columns) is a FieldList listing all the fields on this
 137//   Dataset (this can be set explicitly, or, on fetch() of Dataset
 138//   information from the backend, or as is perhaps most common on the first
 139//   query)
 140// * currentDocuments: a DocumentList containing the Documents we have currently loaded for viewing (you update currentDocuments by calling getRows)
 141// * docCount: total number of documents in this dataset (obtained on a fetch for this Dataset)
 142my.Dataset = Backbone.Model.extend({
 143  __type__: 'Dataset',
 144  initialize: function(model, backend) {
 145    _.bindAll(this, 'query');
 146    this.backend = backend;
 147    if (backend && backend.constructor == String) {
 148      this.backend = my.backends[backend];
 149    }
 150    this.fields = new my.FieldList();
 151    this.currentDocuments = new my.DocumentList();
 152    this.docCount = null;
 153    this.queryState = new my.Query();
 154    this.queryState.bind('change', this.query);
 155  },
 156
 157  // ### query
 158  //
 159  // AJAX method with promise API to get documents from the backend.
 160  //
 161  // It will query based on current query state (given by this.queryState)
 162  // updated by queryObj (if provided).
 163  //
 164  // Resulting DocumentList are used to reset this.currentDocuments and are
 165  // also returned.
 166  query: function(queryObj) {
 167    this.trigger('query:start');
 168    var self = this;
 169    this.queryState.set(queryObj, {silent: true});
 170    var dfd = $.Deferred();
 171    this.backend.query(this, this.queryState.toJSON()).done(function(rows) {
 172      var docs = _.map(rows, function(row) {
 173        var _doc = new my.Document(row);
 174        _doc.backend = self.backend;
 175        _doc.dataset = self;
 176        return _doc;
 177      });
 178      self.currentDocuments.reset(docs);
 179      self.trigger('query:done');
 180      dfd.resolve(self.currentDocuments);
 181    })
 182    .fail(function(arguments) {
 183      self.trigger('query:fail', arguments);
 184      dfd.reject(arguments);
 185    });
 186    return dfd.promise();
 187  },
 188
 189  toTemplateJSON: function() {
 190    var data = this.toJSON();
 191    data.docCount = this.docCount;
 192    data.fields = this.fields.toJSON();
 193    return data;
 194  }
 195});
 196
 197// ## A Document (aka Row)
 198// 
 199// A single entry or row in the dataset
 200my.Document = Backbone.Model.extend({
 201  __type__: 'Document'
 202});
 203
 204// ## A Backbone collection of Documents
 205my.DocumentList = Backbone.Collection.extend({
 206  __type__: 'DocumentList',
 207  model: my.Document
 208});
 209
 210// ## A Field (aka Column) on a Dataset
 211// 
 212// Following attributes as standard:
 213//
 214//  * id: a unique identifer for this field- usually this should match the key in the documents hash
 215//  * label: the visible label used for this field
 216//  * type: the type of the data
 217my.Field = Backbone.Model.extend({
 218  defaults: {
 219    id: null,
 220    label: null,
 221    type: 'String'
 222  },
 223  // In addition to normal backbone initialization via a Hash you can also
 224  // just pass a single argument representing id to the ctor
 225  initialize: function(data) {
 226    // if a hash not passed in the first argument is set as value for key 0
 227    if ('0' in data) {
 228      throw new Error('Looks like you did not pass a proper hash with id to Field constructor');
 229    }
 230    if (this.attributes.label == null) {
 231      this.set({label: this.id});
 232    }
 233  }
 234});
 235
 236my.FieldList = Backbone.Collection.extend({
 237  model: my.Field
 238});
 239
 240// ## A Query object storing Dataset Query state
 241my.Query = Backbone.Model.extend({
 242  defaults: {
 243    size: 100
 244    , from: 0
 245  }
 246});
 247
 248// ## Backend registry
 249//
 250// Backends will register themselves by id into this registry
 251my.backends = {};
 252
 253}(jQuery, this.recline.Model));
 254
 255var util = function() {
 256  var templates = {
 257    transformActions: '<li><a data-action="transform" class="menuAction" href="JavaScript:void(0);">Global transform...</a></li>'
 258    , columnActions: ' \
 259      <li class="write-op"><a data-action="bulkEdit" class="menuAction" href="JavaScript:void(0);">Transform...</a></li> \
 260      <li class="write-op"><a data-action="deleteColumn" class="menuAction" href="JavaScript:void(0);">Delete this column</a></li> \
 261      <li><a data-action="sortAsc" class="menuAction" href="JavaScript:void(0);">Sort ascending</a></li> \
 262      <li><a data-action="sortDesc" class="menuAction" href="JavaScript:void(0);">Sort descending</a></li> \
 263      <li><a data-action="hideColumn" class="menuAction" href="JavaScript:void(0);">Hide this column</a></li> \
 264    '
 265    , rowActions: '<li><a data-action="deleteRow" class="menuAction write-op" href="JavaScript:void(0);">Delete this row</a></li>'
 266    , rootActions: ' \
 267        {{#columns}} \
 268        <li><a data-action="showColumn" data-column="{{.}}" class="menuAction" href="JavaScript:void(0);">Show column: {{.}}</a></li> \
 269        {{/columns}}'
 270    , cellEditor: ' \
 271      <div class="menu-container data-table-cell-editor"> \
 272        <textarea class="data-table-cell-editor-editor" bind="textarea">{{value}}</textarea> \
 273        <div id="data-table-cell-editor-actions"> \
 274          <div class="data-table-cell-editor-action"> \
 275            <button class="okButton btn primary">Update</button> \
 276            <button class="cancelButton btn danger">Cancel</button> \
 277          </div> \
 278        </div> \
 279      </div> \
 280    '
 281    , editPreview: ' \
 282      <div class="expression-preview-table-wrapper"> \
 283        <table> \
 284        <thead> \
 285        <tr> \
 286          <th class="expression-preview-heading"> \
 287            before \
 288          </th> \
 289          <th class="expression-preview-heading"> \
 290            after \
 291          </th> \
 292        </tr> \
 293        </thead> \
 294        <tbody> \
 295        {{#rows}} \
 296        <tr> \
 297          <td class="expression-preview-value"> \
 298            {{before}} \
 299          </td> \
 300          <td class="expression-preview-value"> \
 301            {{after}} \
 302          </td> \
 303        </tr> \
 304        {{/rows}} \
 305        </tbody> \
 306        </table> \
 307      </div> \
 308    '
 309  };
 310
 311  $.fn.serializeObject = function() {
 312    var o = {};
 313    var a = this.serializeArray();
 314    $.each(a, function() {
 315      if (o[this.name]) {
 316        if (!o[this.name].push) {
 317          o[this.name] = [o[this.name]];
 318        }
 319        o[this.name].push(this.value || '');
 320      } else {
 321        o[this.name] = this.value || '';
 322      }
 323    });
 324    return o;
 325  };
 326
 327  function registerEmitter() {
 328    var Emitter = function(obj) {
 329      this.emit = function(obj, channel) { 
 330        if (!channel) var channel = 'data';
 331        this.trigger(channel, obj); 
 332      };
 333    };
 334    MicroEvent.mixin(Emitter);
 335    return new Emitter();
 336  }
 337  
 338  function listenFor(keys) {
 339    var shortcuts = { // from jquery.hotkeys.js
 340			8: "backspace", 9: "tab", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause",
 341			20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home",
 342			37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del", 
 343			96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7",
 344			104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/", 
 345			112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8", 
 346			120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 191: "/", 224: "meta"
 347		}
 348    window.addEventListener("keyup", function(e) { 
 349      var pressed = shortcuts[e.keyCode];
 350      if(_.include(keys, pressed)) app.emitter.emit("keyup", pressed); 
 351    }, false);
 352  }
 353  
 354  function observeExit(elem, callback) {
 355    var cancelButton = elem.find('.cancelButton');
 356    // TODO: remove (commented out as part of Backbon-i-fication
 357    // app.emitter.on('esc', function() { 
 358    //  cancelButton.click();
 359    //  app.emitter.clear('esc');
 360    // });
 361    cancelButton.click(callback);
 362  }
 363  
 364  function show( thing ) {
 365    $('.' + thing ).show();
 366    $('.' + thing + '-overlay').show();
 367  }
 368
 369  function hide( thing ) {
 370    $('.' + thing ).hide();
 371    $('.' + thing + '-overlay').hide();
 372    // TODO: remove or replace (commented out as part of Backbon-i-fication
 373    // if (thing === "dialog") app.emitter.clear('esc'); // todo more elegant solution
 374  }
 375  
 376  function position( thing, elem, offset ) {
 377    var position = $(elem.target).position();
 378    if (offset) {
 379      if (offset.top) position.top += offset.top;
 380      if (offset.left) position.left += offset.left;
 381    }
 382    $('.' + thing + '-overlay').show().click(function(e) {
 383      $(e.target).hide();
 384      $('.' + thing).hide();
 385    });
 386    $('.' + thing).show().css({top: position.top + $(elem.target).height(), left: position.left});
 387  }
 388
 389  function render( template, target, options ) {
 390    if ( !options ) options = {data: {}};
 391    if ( !options.data ) options = {data: options};
 392    var html = $.mustache( templates[template], options.data );
 393    if (target instanceof jQuery) {
 394      var targetDom = target;
 395    } else {
 396      var targetDom = $( "." + target + ":first" );      
 397    }
 398    if( options.append ) {
 399      targetDom.append( html );
 400    } else {
 401      targetDom.html( html );
 402    }
 403    // TODO: remove (commented out as part of Backbon-i-fication
 404    // if (template in app.after) app.after[template]();
 405  }
 406
 407  return {
 408    registerEmitter: registerEmitter,
 409    listenFor: listenFor,
 410    show: show,
 411    hide: hide,
 412    position: position,
 413    render: render,
 414    observeExit: observeExit
 415  };
 416}();
 417this.recline = this.recline || {};
 418this.recline.View = this.recline.View || {};
 419
 420(function($, my) {
 421
 422// ## Graph view for a Dataset using Flot graphing library.
 423//
 424// Initialization arguments:
 425//
 426// * model: recline.Model.Dataset
 427// * config: (optional) graph configuration hash of form:
 428//
 429//        { 
 430//          group: {column name for x-axis},
 431//          series: [{column name for series A}, {column name series B}, ... ],
 432//          graphType: 'line'
 433//        }
 434//
 435// NB: should *not* provide an el argument to the view but must let the view
 436// generate the element itself (you can then append view.el to the DOM.
 437my.FlotGraph = Backbone.View.extend({
 438
 439  tagName:  "div",
 440  className: "data-graph-container",
 441
 442  template: ' \
 443  <div class="editor"> \
 444    <div class="editor-info editor-hide-info"> \
 445      <h3 class="action-toggle-help">Help &raquo;</h3> \
 446      <p>To create a chart select a column (group) to use as the x-axis \
 447         then another column (Series A) to plot against it.</p> \
 448      <p>You can add add \
 449         additional series by clicking the "Add series" button</p> \
 450    </div> \
 451    <form class="form-stacked"> \
 452      <div class="clearfix"> \
 453        <label>Graph Type</label> \
 454        <div class="input editor-type"> \
 455          <select> \
 456          <option value="line">Line</option> \
 457          </select> \
 458        </div> \
 459        <label>Group Column (x-axis)</label> \
 460        <div class="input editor-group"> \
 461          <select> \
 462          {{#fields}} \
 463          <option value="{{id}}">{{label}}</option> \
 464          {{/fields}} \
 465          </select> \
 466        </div> \
 467        <div class="editor-series-group"> \
 468          <div class="editor-series"> \
 469            <label>Series <span>A (y-axis)</span></label> \
 470            <div class="input"> \
 471              <select> \
 472              {{#fields}} \
 473              <option value="{{id}}">{{label}}</option> \
 474              {{/fields}} \
 475              </select> \
 476            </div> \
 477          </div> \
 478        </div> \
 479      </div> \
 480      <div class="editor-buttons"> \
 481        <button class="btn editor-add">Add Series</button> \
 482      </div> \
 483      <div class="editor-buttons editor-submit" comment="hidden temporarily" style="display: none;"> \
 484        <button class="editor-save">Save</button> \
 485        <input type="hidden" class="editor-id" value="chart-1" /> \
 486      </div> \
 487    </form> \
 488  </div> \
 489  <div class="panel graph"></div> \
 490</div> \
 491',
 492
 493  events: {
 494    'change form select': 'onEditorSubmit'
 495    , 'click .editor-add': 'addSeries'
 496    , 'click .action-remove-series': 'removeSeries'
 497    , 'click .action-toggle-help': 'toggleHelp'
 498  },
 499
 500  initialize: function(options, config) {
 501    var self = this;
 502    this.el = $(this.el);
 503    _.bindAll(this, 'render', 'redraw');
 504    // we need the model.fields to render properly
 505    this.model.bind('change', this.render);
 506    this.model.fields.bind('reset', this.render);
 507    this.model.fields.bind('add', this.render);
 508    this.model.currentDocuments.bind('add', this.redraw);
 509    this.model.currentDocuments.bind('reset', this.redraw);
 510    var configFromHash = my.parseHashQueryString().graph;
 511    if (configFromHash) {
 512      configFromHash = JSON.parse(configFromHash);
 513    }
 514    this.chartConfig = _.extend({
 515        group: null,
 516        series: [],
 517        graphType: 'line'
 518      },
 519      configFromHash,
 520      config
 521      );
 522    this.render();
 523  },
 524
 525  render: function() {
 526    htmls = $.mustache(this.template, this.model.toTemplateJSON());
 527    $(this.el).html(htmls);
 528    // now set a load of stuff up
 529    this.$graph = this.el.find('.panel.graph');
 530    // for use later when adding additional series
 531    // could be simpler just to have a common template!
 532    this.$seriesClone = this.el.find('.editor-series').clone();
 533    this._updateSeries();
 534    return this;
 535  },
 536
 537  onEditorSubmit: function(e) {
 538    var select = this.el.find('.editor-group select');
 539    this._getEditorData();
 540    // update navigation
 541    // TODO: make this less invasive (e.g. preserve other keys in query string)
 542    var qs = my.parseHashQueryString();
 543    qs['graph'] = this.chartConfig;
 544    my.setHashQueryString(qs);
 545    this.redraw();
 546  },
 547
 548  redraw: function() {
 549    // There appear to be issues generating a Flot graph if either:
 550
 551    // * The relevant div that graph attaches to his hidden at the moment of creating the plot -- Flot will complain with
 552    //
 553    //   Uncaught Invalid dimensions for plot, width = 0, height = 0
 554    // * There is no data for the plot -- either same error or may have issues later with errors like 'non-existent node-value' 
 555    var areWeVisible = !jQuery.expr.filters.hidden(this.el[0]);
 556    if ((!areWeVisible || this.model.currentDocuments.length == 0)) {
 557      return
 558    }
 559    // create this.plot and cache it
 560    if (!this.plot) {
 561      // only lines for the present
 562      options = {
 563        id: 'line',
 564        name: 'Line Chart'
 565      };
 566      this.plot = $.plot(this.$graph, this.createSeries(), options);
 567    } 
 568    this.plot.setData(this.createSeries());
 569    this.plot.resize();
 570    this.plot.setupGrid();
 571    this.plot.draw();
 572  },
 573
 574  _getEditorData: function() {
 575    $editor = this
 576    var series = this.$series.map(function () {
 577      return $(this).val();
 578    });
 579    this.chartConfig.series = $.makeArray(series)
 580    this.chartConfig.group = this.el.find('.editor-group select').val();
 581  },
 582
 583  createSeries: function () {
 584    var self = this;
 585    var series = [];
 586    if (this.chartConfig) {
 587      $.each(this.chartConfig.series, function (seriesIndex, field) {
 588        var points = [];
 589        $.each(self.model.currentDocuments.models, function (index, doc) {
 590          var x = doc.get(self.chartConfig.group);
 591          var y = doc.get(field);
 592          if (typeof x === 'string') {
 593            x = index;
 594          }
 595          points.push([x, y]);
 596        });
 597        series.push({data: points, label: field});
 598      });
 599    }
 600    return series;
 601  },
 602
 603  // Public: Adds a new empty series select box to the editor.
 604  //
 605  // All but the first select box will have a remove button that allows them
 606  // to be removed.
 607  //
 608  // Returns itself.
 609  addSeries: function (e) {
 610    e.preventDefault();
 611    var element = this.$seriesClone.clone(),
 612        label   = element.find('label'),
 613        index   = this.$series.length;
 614
 615    this.el.find('.editor-series-group').append(element);
 616    this._updateSeries();
 617    label.append(' [<a href="#remove" class="action-remove-series">Remove</a>]');
 618    label.find('span').text(String.fromCharCode(this.$series.length + 64));
 619    return this;
 620  },
 621
 622  // Public: Removes a series list item from the editor.
 623  //
 624  // Also updates the labels of the remaining series elements.
 625  removeSeries: function (e) {
 626    e.preventDefault();
 627    var $el = $(e.target);
 628    $el.parent().parent().remove();
 629    this._updateSeries();
 630    this.$series.each(function (index) {
 631      if (index > 0) {
 632        var labelSpan = $(this).prev().find('span');
 633        labelSpan.text(String.fromCharCode(index + 65));
 634      }
 635    });
 636    this.onEditorSubmit();
 637  },
 638
 639  toggleHelp: function() {
 640    this.el.find('.editor-info').toggleClass('editor-hide-info');
 641  },
 642
 643  // Private: Resets the series property to reference the select elements.
 644  //
 645  // Returns itself.
 646  _updateSeries: function () {
 647    this.$series  = this.el.find('.editor-series select');
 648  }
 649});
 650
 651})(jQuery, recline.View);
 652
 653this.recline = this.recline || {};
 654this.recline.View = this.recline.View || {};
 655
 656(function($, my) {
 657// ## DataGrid
 658//
 659// Provides a tabular view on a Dataset.
 660//
 661// Initialize it with a recline.Dataset object.
 662//
 663// Additional options passed in second arguments. Options:
 664//
 665// * cellRenderer: function used to render individual cells. See DataGridRow for more.
 666my.DataGrid = Backbone.View.extend({
 667  tagName:  "div",
 668  className: "data-table-container",
 669
 670  initialize: function(modelEtc, options) {
 671    var self = this;
 672    this.el = $(this.el);
 673    _.bindAll(this, 'render');
 674    this.model.currentDocuments.bind('add', this.render);
 675    this.model.currentDocuments.bind('reset', this.render);
 676    this.model.currentDocuments.bind('remove', this.render);
 677    this.state = {};
 678    this.hiddenFields = [];
 679    this.options = options;
 680  },
 681
 682  events: {
 683    'click .column-header-menu': 'onColumnHeaderClick'
 684    , 'click .row-header-menu': 'onRowHeaderClick'
 685    , 'click .root-header-menu': 'onRootHeaderClick'
 686    , 'click .data-table-menu li a': 'onMenuClick'
 687  },
 688
 689  // TODO: delete or re-enable (currently this code is not used from anywhere except deprecated or disabled methods (see above)).
 690  // showDialog: function(template, data) {
 691  //   if (!data) data = {};
 692  //   util.show('dialog');
 693  //   util.render(template, 'dialog-content', data);
 694  //   util.observeExit($('.dialog-content'), function() {
 695  //     util.hide('dialog');
 696  //   })
 697  //   $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' });
 698  // },
 699
 700
 701  // ======================================================
 702  // Column and row menus
 703
 704  onColumnHeaderClick: function(e) {
 705    this.state.currentColumn = $(e.target).closest('.column-header').attr('data-field');
 706    util.position('data-table-menu', e);
 707    util.render('columnActions', 'data-table-menu');
 708  },
 709
 710  onRowHeaderClick: function(e) {
 711    this.state.currentRow = $(e.target).parents('tr:first').attr('data-id');
 712    util.position('data-table-menu', e);
 713    util.render('rowActions', 'data-table-menu');
 714  },
 715  
 716  onRootHeaderClick: function(e) {
 717    util.position('data-table-menu', e);
 718    util.render('rootActions', 'data-table-menu', {'columns': this.hiddenFields});
 719  },
 720
 721  onMenuClick: function(e) {
 722    var self = this;
 723    e.preventDefault();
 724    var actions = {
 725      bulkEdit: function() { self.showTransformColumnDialog('bulkEdit', {name: self.state.currentColumn}) },
 726      transform: function() { self.showTransformDialog('transform') },
 727      sortAsc: function() { self.setColumnSort('asc') },
 728      sortDesc: function() { self.setColumnSort('desc') },
 729      hideColumn: function() { self.hideColumn() },
 730      showColumn: function() { self.showColumn(e) },
 731      // TODO: Delete or re-implement ...
 732      csv: function() { window.location.href = app.csvUrl },
 733      json: function() { window.location.href = "_rewrite/api/json" },
 734      urlImport: function() { showDialog('urlImport') },
 735      pasteImport: function() { showDialog('pasteImport') },
 736      uploadImport: function() { showDialog('uploadImport') },
 737      // END TODO
 738      deleteColumn: function() {
 739        var msg = "Are you sure? This will delete '" + self.state.currentColumn + "' from all documents.";
 740        // TODO:
 741        alert('This function needs to be re-implemented');
 742        return;
 743        if (confirm(msg)) costco.deleteColumn(self.state.currentColumn);
 744      },
 745      deleteRow: function() {
 746        var doc = _.find(self.model.currentDocuments.models, function(doc) {
 747          // important this is == as the currentRow will be string (as comes
 748          // from DOM) while id may be int
 749          return doc.id == self.state.currentRow
 750        });
 751        doc.destroy().then(function() { 
 752            self.model.currentDocuments.remove(doc);
 753            my.notify("Row deleted successfully");
 754          })
 755          .fail(function(err) {
 756            my.notify("Errorz! " + err)
 757          })
 758      }
 759    }
 760    util.hide('data-table-menu');
 761    actions[$(e.target).attr('data-action')]();
 762  },
 763
 764  showTransformColumnDialog: function() {
 765    var $el = $('.dialog-content');
 766    util.show('dialog');
 767    var view = new my.ColumnTransform({
 768      model: this.model
 769    });
 770    view.state = this.state;
 771    view.render();
 772    $el.empty();
 773    $el.append(view.el);
 774    util.observeExit($el, function() {
 775      util.hide('dialog');
 776    })
 777    $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' });
 778  },
 779
 780  showTransformDialog: function() {
 781    var $el = $('.dialog-content');
 782    util.show('dialog');
 783    var view = new recline.View.DataTransform({
 784    });
 785    view.render();
 786    $el.empty();
 787    $el.append(view.el);
 788    util.observeExit($el, function() {
 789      util.hide('dialog');
 790    })
 791    $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' });
 792  },
 793
 794  setColumnSort: function(order) {
 795    var sort = [{}];
 796    sort[0][this.state.currentColumn] = {order: order};
 797    this.model.query({sort: sort});
 798  },
 799  
 800  hideColumn: function() {
 801    this.hiddenFields.push(this.state.currentColumn);
 802    this.render();
 803  },
 804  
 805  showColumn: function(e) {
 806    this.hiddenFields = _.without(this.hiddenFields, $(e.target).data('column'));
 807    this.render();
 808  },
 809
 810  // ======================================================
 811  // #### Templating
 812  template: ' \
 813    <div class="data-table-menu-overlay" style="display: none; z-index: 101; ">&nbsp;</div> \
 814    <ul class="data-table-menu"></ul> \
 815    <table class="data-table table-striped" cellspacing="0"> \
 816      <thead> \
 817        <tr> \
 818          {{#notEmpty}} \
 819            <th class="column-header"> \
 820              <div class="column-header-title"> \
 821                <a class="root-header-menu"></a> \
 822                <span class="column-header-name"></span> \
 823              </div> \
 824            </th> \
 825          {{/notEmpty}} \
 826          {{#fields}} \
 827            <th class="column-header {{#hidden}}hidden{{/hidden}}" data-field="{{id}}"> \
 828              <div class="column-header-title"> \
 829                <a class="column-header-menu"></a> \
 830                <span class="column-header-name">{{label}}</span> \
 831              </div> \
 832              </div> \
 833            </th> \
 834          {{/fields}} \
 835        </tr> \
 836      </thead> \
 837      <tbody></tbody> \
 838    </table> \
 839  ',
 840
 841  toTemplateJSON: function() {
 842    var modelData = this.model.toJSON()
 843    modelData.notEmpty = ( this.fields.length > 0 )
 844    // TODO: move this sort of thing into a toTemplateJSON method on Dataset?
 845    modelData.fields = _.map(this.fields, function(field) { return field.toJSON() });
 846    return modelData;
 847  },
 848  render: function() {
 849    var self = this;
 850    this.fields = this.model.fields.filter(function(field) {
 851      return _.indexOf(self.hiddenFields, field.id) == -1;
 852    });
 853    var htmls = $.mustache(this.template, this.toTemplateJSON());
 854    this.el.html(htmls);
 855    this.model.currentDocuments.forEach(function(doc) {
 856      var tr = $('<tr />');
 857      self.el.find('tbody').append(tr);
 858      var newView = new my.DataGridRow({
 859          model: doc,
 860          el: tr,
 861          fields: self.fields,
 862        },
 863        self.options
 864        );
 865      newView.render();
 866    });
 867    this.el.toggleClass('no-hidden', (self.hiddenFields.length == 0));
 868    return this;
 869  }
 870});
 871
 872// ## DataGridRow View for rendering an individual document.
 873//
 874// Since we want this to update in place it is up to creator to provider the element to attach to.
 875//
 876// In addition you *must* pass in a FieldList in the constructor options. This should be list of fields for the DataGrid.
 877//
 878// Additional options can be passed in a second hash argument. Options:
 879//
 880// * cellRenderer: function to render cells. Signature: function(value,
 881//   field, doc) where value is the value of this cell, field is
 882//   corresponding field object and document is the document object. Note
 883//   that implementing functions can ignore arguments (e.g.
 884//   function(value) would be a valid cellRenderer function).
 885//
 886// Example:
 887//
 888// <pre>
 889// var row = new DataGridRow({
 890//   model: dataset-document,
 891//     el: dom-element,
 892//     fields: mydatasets.fields // a FieldList object
 893//   }, {
 894//     cellRenderer: my-cell-renderer-function 
 895//   }
 896// );
 897// </pre>
 898my.DataGridRow = Backbone.View.extend({
 899  initialize: function(initData, options) {
 900    _.bindAll(this, 'render');
 901    this._fields = initData.fields;
 902    if (options && options.cellRenderer) {
 903      this._cellRenderer = options.cellRenderer;
 904    } else {
 905      this._cellRenderer = function(value) {
 906        return value;
 907      }
 908    }
 909    this.el = $(this.el);
 910    this.model.bind('change', this.render);
 911  },
 912
 913  template: ' \
 914      <td><a class="row-header-menu"></a></td> \
 915      {{#cells}} \
 916      <td data-field="{{field}}"> \
 917        <div class="data-table-cell-content"> \
 918          <a href="javascript:{}" class="data-table-cell-edit" title="Edit this cell">&nbsp;</a> \
 919          <div class="data-table-cell-value">{{{value}}}</div> \
 920        </div> \
 921      </td> \
 922      {{/cells}} \
 923    ',
 924  events: {
 925    'click .data-table-cell-edit': 'onEditClick',
 926    'click .data-table-cell-editor .okButton': 'onEditorOK',
 927    'click .data-table-cell-editor .cancelButton': 'onEditorCancel'
 928  },
 929  
 930  toTemplateJSON: function() {
 931    var self = this;
 932    var doc = this.model;
 933    var cellData = this._fields.map(function(field) {
 934      return {
 935        field: field.id,
 936        value: self._cellRenderer(doc.get(field.id), field, doc)
 937      }
 938    })
 939    return { id: this.id, cells: cellData }
 940  },
 941
 942  render: function() {
 943    this.el.attr('data-id', this.model.id);
 944    var html = $.mustache(this.template, this.toTemplateJSON());
 945    $(this.el).html(html);
 946    return this;
 947  },
 948
 949  // ===================
 950  // Cell Editor methods
 951  onEditClick: function(e) {
 952    var editing = this.el.find('.data-table-cell-editor-editor');
 953    if (editing.length > 0) {
 954      editing.parents('.data-table-cell-value').html(editing.text()).siblings('.data-table-cell-edit').removeClass("hidden");
 955    }
 956    $(e.target).addClass("hidden");
 957    var cell = $(e.target).siblings('.data-table-cell-value');
 958    cell.data("previousContents", cell.text());
 959    util.render('cellEditor', cell, {value: cell.text()});
 960  },
 961
 962  onEditorOK: function(e) {
 963    var cell = $(e.target);
 964    var rowId = cell.parents('tr').attr('data-id');
 965    var field = cell.parents('td').attr('data-field');
 966    var newValue = cell.parents('.data-table-cell-editor').find('.data-table-cell-editor-editor').val();
 967    var newData = {};
 968    newData[field] = newValue;
 969    this.model.set(newData);
 970    my.notify("Updating row...", {loader: true});
 971    this.model.save().then(function(response) {
 972        my.notify("Row updated successfully", {category: 'success'});
 973      })
 974      .fail(function() {
 975        my.notify('Error saving row', {
 976          category: 'error',
 977          persist: true
 978        });
 979      });
 980  },
 981
 982  onEditorCancel: function(e) {
 983    var cell = $(e.target).parents('.data-table-cell-value');
 984    cell.html(cell.data('previousContents')).siblings('.data-table-cell-edit').removeClass("hidden");
 985  }
 986});
 987
 988})(jQuery, recline.View);
 989this.recline = this.recline || {};
 990this.recline.View = this.recline.View || {};
 991
 992(function($, my) {
 993// ## DataExplorer
 994//
 995// The primary view for the entire application. Usage:
 996// 
 997// <pre>
 998// var myExplorer = new model.recline.DataExplorer({
 999//   model: {{recline.Model.Dataset instance}}
1000//   el: {{an existing dom element}}
1001//   views: {{page views}}
1002//   config: {{config options -- see below}}
1003// });
1004// </pre> 
1005//
1006// ### Parameters
1007// 
1008// **model**: (required) Dataset instance.
1009//
1010// **el**: (required) DOM element.
1011//
1012// **views**: (optional) the views (Grid, Graph etc) for DataExplorer to
1013// show. This is an array of view hashes. If not provided
1014// just initialize a DataGrid with id 'grid'. Example:
1015//
1016// <pre>
1017// var views = [
1018//   {
1019//     id: 'grid', // used for routing
1020//     label: 'Grid', // used for view switcher
1021//     view: new recline.View.DataGrid({
1022//       model: dataset
1023//     })
1024//   },
1025//   {
1026//     id: 'graph',
1027//     label: 'Graph',
1028//     view: new recline.View.FlotGraph({
1029//       model: dataset
1030//     })
1031//   }
1032// ];
1033// </pre>
1034//
1035// **config**: Config options like:
1036//
1037//   * readOnly: true/false (default: false) value indicating whether to
1038//     operate in read-only mode (hiding all editing options).
1039//
1040// NB: the element already being in the DOM is important for rendering of
1041// FlotGraph subview.
1042my.DataExplorer = Backbone.View.extend({
1043  template: ' \
1044  <div class="data-explorer"> \
1045    <div class="alert-messages"></div> \
1046    \
1047    <div class="header"> \
1048      <ul class="navigation"> \
1049        {{#views}} \
1050        <li><a href="#{{id}}" class="btn">{{label}}</a> \
1051        {{/views}} \
1052      </ul> \
1053      <div class="recline-results-info"> \
1054        Results found <span class="doc-count">{{docCount}}</span> \
1055      </div> \
1056    </div> \
1057    <div class="data-view-container"></div> \
1058    <div class="dialog-overlay" style="display: none; z-index: 101; ">&nbsp;</div> \
1059    <div class="dialog ui-draggable" style="display: none; z-index: 102; top: 101px; "> \
1060      <div class="dialog-frame" style="width: 700px; visibility: visible; "> \
1061        <div class="dialog-content dialog-border"></div> \
1062      </div> \
1063    </div> \
1064  </div> \
1065  ',
1066
1067  initialize: function(options) {
1068    var self = this;
1069    this.el = $(this.el);
1070    this.config = _.extend({
1071        readOnly: false
1072      },
1073      options.config);
1074    if (this.config.readOnly) {
1075      this.setReadOnly();
1076    }
1077    // Hash of 'page' views (i.e. those for whole page) keyed by page name
1078    if (options.views) {
1079      this.pageViews = options.views;
1080    } else {
1081      this.pageViews = [{
1082        id: 'grid',
1083        label: 'Grid',
1084        view: new my.DataGrid({
1085            model: this.model
1086          })
1087      }];
1088    }
1089    // this must be called after pageViews are created
1090    this.render();
1091
1092    this.router = new Backbone.Router();
1093    this.setupRouting();
1094
1095    this.model.bind('query:start', function() {
1096        my.notify('Loading data', {loader: true});
1097      });
1098    this.model.bind('query:done', function() {
1099        my.clearNotifications();
1100        self.el.find('.doc-count').text(self.model.docCount || 'Unknown');
1101        my.notify('Data loaded', {category: 'success'});
1102      });
1103    this.model.bind('query:fail', function(error) {
1104        my.clearNotifications();
1105        var msg = '';
1106        if (typeof(error) == 'string') {
1107          msg = error;
1108        } else if (typeof(error) == 'object') {
1109          if (error.title) {
1110            msg = error.title + ': ';
1111          }
1112          if (error.message) {
1113            msg += error.message;
1114          }
1115        } else {
1116          msg = 'There was an error querying the backend';
1117        }
1118        my.notify(msg, {category: 'error', persist: true});
1119      });
1120
1121    // retrieve basic data like fields etc
1122    // note this.model and dataset returned are the same
1123    this.model.fetch()
1124      .done(function(dataset) {
1125        self.el.find('.doc-count').text(self.model.docCount || 'Unknown');
1126        self.model.query();
1127      })
1128      .fail(function(error) {
1129        my.notify(error.message, {category: 'error', persist: true});
1130      });
1131  },
1132
1133  setReadOnly: function() {
1134    this.el.addClass('read-only');
1135  },
1136
1137  render: function() {
1138    var tmplData = this.model.toTemplateJSON();
1139    tmplData.displayCount = this.config.displayCount;
1140    tmplData.views = this.pageViews;
1141    var template = $.mustache(this.template, tmplData);
1142    $(this.el).html(template);
1143    var $dataViewContainer = this.el.find('.data-view-container');
1144    _.each(this.pageViews, function(view, pageName) {
1145      $dataViewContainer.append(view.view.el)
1146    });
1147    var queryEditor = new my.QueryEditor({
1148      model: this.model.queryState
1149    });
1150    this.el.find('.header').append(queryEditor.el);
1151  },
1152
1153  setupRouting: function() {
1154    var self = this;
1155    // Default route
1156    this.router.route('', this.pageViews[0].id, function() {
1157      self.updateNav(self.pageViews[0].id);
1158    });
1159    $.each(this.pageViews, function(idx, view) {
1160      self.router.route(/^([^?]+)(\?.*)?/, 'view', function(viewId, queryString) {
1161        self.updateNav(viewId, queryString);
1162      });
1163    });
1164  },
1165
1166  updateNav: function(pageName, queryString) {
1167    this.el.find('.navigation li').removeClass('active');
1168    this.el.find('.navigation li a').removeClass('disabled');
1169    var $el = this.el.find('.navigation li a[href=#' + pageName + ']');
1170    $el.parent().addClass('active');
1171    $el.addClass('disabled');
1172    // show the specific page
1173    _.each(this.pageViews, function(view, idx) {
1174      if (view.id === pageName) {
1175        view.view.el.show();
1176      } else {
1177        view.view.el.hide();
1178      }
1179    });
1180  }
1181});
1182
1183
1184my.QueryEditor = Backbone.View.extend({
1185  className: 'recline-query-editor', 
1186  template: ' \
1187    <form action="" method="GET" class="form-inline"> \
1188      <input type="text" name="q" value="{{q}}" class="text-query" /> \
1189      <div class="pagination"> \
1190        <ul> \
1191          <li class="prev action-pagination-update"><a>&laquo;</a></li> \
1192          <li class="active"><a><input name="from" type="text" value="{{from}}" /> &ndash; <input name="to" type="text" value="{{to}}" /> </a></li> \
1193          <li class="next action-pagination-update"><a>&raquo;</a></li> \
1194        </ul> \
1195      </div> \
1196      <button type="submit" class="btn" style="">Update &raquo;</button> \
1197    </form> \
1198  ',
1199
1200  events: {
1201    'submit form': 'onFormSubmit',
1202    'click .action-pagination-update': 'onPaginationUpdate'
1203  },
1204
1205  initialize: function() {
1206    _.bindAll(this, 'render');
1207    this.el = $(this.el);
1208    this.model.bind('change', this.render);
1209    this.render();
1210  },
1211  onFormSubmit: function(e) {
1212    e.preventDefault();
1213    var newFrom = parseInt(this.el.find('input[name="from"]').val());
1214    var newSize = parseInt(this.el.find('input[name="to"]').val()) - newFrom;
1215    var query = this.el.find('.text-query').val();
1216    this.model.set({size: newSize, from: newFrom, q: query});
1217  },
1218  onPaginationUpdate: function(e) {
1219    e.preventDefault();
1220    var $el = $(e.target);
1221    if ($el.parent().hasClass('prev')) {
1222      var newFrom = this.model.get('from') - Math.max(0, this.model.get('size'));
1223    } else {
1224      var newFrom = this.model.get('from') + this.model.get('size');
1225    }
1226    this.model.set({from: newFrom});
1227  },
1228  render: function() {
1229    var tmplData = this.model.toJSON();
1230    tmplData.to = this.model.get('from') + this.model.get('size');
1231    var templated = $.mustache(this.template, tmplData);
1232    this.el.html(templated);
1233  }
1234});
1235
1236
1237/* ========================================================== */
1238// ## Miscellaneous Utilities
1239
1240var urlPathRegex = /^([^?]+)(\?.*)?/;
1241
1242// Parse the Hash section of a URL into path and query string
1243my.parseHashUrl = function(hashUrl) {
1244  var parsed = urlPathRegex.exec(hashUrl);
1245  if (parsed == null) {
1246    return {};
1247  } else {
1248    return {
1249      path: parsed[1],
1250      query: parsed[2] || ''
1251    }
1252  }
1253}
1254
1255// Parse a URL query string (?xyz=abc...) into a dictionary.
1256my.parseQueryString = function(q) {
1257  var urlParams = {},
1258    e, d = function (s) {
1259      return unescape(s.replace(/\+/g, " "));
1260    },
1261    r = /([^&=]+)=?([^&]*)/g;
1262
1263  if (q && q.length && q[0] === '?') {
1264    q = q.slice(1);
1265  }
1266  while (e = r.exec(q)) {
1267    // TODO: have values be array as query string allow repetition of keys
1268    urlParams[d(e[1])] = d(e[2]);
1269  }
1270  return urlParams;
1271}
1272
1273// Parse the query string out of the URL hash
1274my.parseHashQueryString = function() {
1275  q = my.parseHashUrl(window.location.hash).query;
1276  return my.parseQueryString(q);
1277}
1278
1279// Compse a Query String
1280my.composeQueryString = function(queryParams) {
1281  var queryString = '?';
1282  var items = [];
1283  $.each(queryParams, function(key, value) {
1284    items.push(key + '=' + JSON.stringify(value));
1285  });
1286  queryString += items.join('&');
1287  return queryString;
1288}
1289
1290my.setHashQueryString = function(queryParams) {
1291  window.location.hash = window.location.hash.split('?')[0] + my.composeQueryString(queryParams);
1292}
1293
1294// ## notify
1295//
1296// Create a notification (a div.alert in div.alert-messsages) using provide messages and options. Options are:
1297//
1298// * category: warning (default), success, error
1299// * persist: if true alert is persistent, o/w hidden after 3s (default = false)
1300// * loader: if true show loading spinner
1301my.notify = function(message, options) {
1302  if (!options) var options = {};
1303  var tmplData = _.extend({
1304    msg: message,
1305    category: 'warning'
1306    },
1307    options);
1308  var _template = ' \
1309    <div class="alert alert-{{category}} fade in" data-alert="alert"><a class="close" data-dismiss="alert" href="#">×</a> \
1310      {{msg}} \
1311        {{#loader}} \
1312        <span class="notification-loader">&nbsp;</span> \
1313        {{/loader}} \
1314    </div>';
1315  var _templated = $.mustache(_template, tmplData); 
1316  _templated = $(_templated).appendTo($('.data-explorer .alert-messages'));
1317  if (!options.persist) {
1318    setTimeout(function() {
1319      $(_templated).fadeOut(1000, function() {
1320        $(this).remove();
1321      });
1322    }, 1000);
1323  }
1324}
1325
1326// ## clearNotifications
1327//
1328// Clear all existing notifications
1329my.clearNotifications = function() {
1330  var $notifications = $('.data-explorer .alert-messages .alert');
1331  $notifications.remove();
1332}
1333
1334})(jQuery, recline.View);
1335
1336this.recline = this.recline || {};
1337this.recline.View = this.recline.View || {};
1338
1339// Views module following classic module pattern
1340(function($, my) {
1341
1342// View (Dialog) for doing data transformations on whole dataset.
1343my.DataTransform = Backbone.View.extend({
1344  className: 'transform-view',
1345  template: ' \
1346    <div class="dialog-header"> \
1347      Recursive transform on all rows \
1348    </div> \
1349    <div class="dialog-body"> \
1350      <div class="grid-layout layout-full"> \
1351        <p class="info">Traverse and transform objects by visiting every node on a recursive walk using <a href="https://github.com/substack/js-traverse">js-traverse</a>.</p> \
1352        <table> \
1353        <tbody> \
1354        <tr> \
1355          <td colspan="4"> \
1356            <div class="grid-layout layout-tight layout-full"> \
1357              <table rows="4" cols="4"> \
1358              <tbody> \
1359              <tr style="vertical-align: bottom;"> \
1360                <td colspan="4"> \
1361                  Expression \
1362                </td> \
1363              </tr> \
1364              <tr> \
1365                <td colspan="3"> \
1366                  <div class="input-container"> \
1367                    <textarea class="expression-preview-code"></textarea> \
1368                  </div> \
1369                </td> \
1370                <td class="expression-preview-parsing-status" width="150" style="vertical-align: top;"> \
1371                  No syntax error. \
1372                </td> \
1373              </tr> \
1374              <tr> \
1375                <td colspan="4"> \
1376                  <div id="expression-preview-tabs" class="refine-tabs ui-tabs ui-widget ui-widget-content ui-corner-all"> \
1377                    <span>Preview</span> \
1378                    <div id="expression-preview-tabs-preview" class="ui-tabs-panel ui-widget-content ui-corner-bottom"> \
1379                      <div class="expression-preview-container" style="width: 652px; "> \
1380                      </div> \
1381                    </div> \
1382                  </div> \
1383                </td> \
1384              </tr> \
1385              </tbody> \
1386              </table> \
1387            </div> \
1388          </td> \
1389        </tr> \
1390        </tbody> \
1391        </table> \
1392      </div> \
1393    </div> \
1394    <div class="dialog-footer"> \
1395      <button class="okButton button">&nbsp;&nbsp;Update All&nbsp;&nbsp;</button> \
1396      <button class="cancelButton button">Cancel</button> \
1397    </div> \
1398  ',
1399
1400  initialize: function() {
1401    this.el = $(this.el);
1402  },
1403
1404  render: function() {
1405    this.el.html(this.template);
1406  }
1407});
1408
1409
1410// View (Dialog) for doing data transformations (on columns of data).
1411my.ColumnTransform = Backbone.View.extend({
1412  className: 'transform-column-view',
1413  template: ' \
1414    <div class="dialog-header"> \
1415      Functional transform on column {{name}} \
1416    </div> \
1417    <div class="dialog-body"> \
1418      <div class="grid-layout layout-tight layout-full"> \
1419        <table> \
1420        <tbody> \
1421        <tr> \
1422          <td colspan="4"> \
1423            <div class="grid-layout layout-tight layout-full"> \
1424              <table rows="4" cols="4"> \
1425              <tbody> \
1426              <tr style="vertical-align: bottom;"> \
1427                <td colspan="4"> \
1428                  Expression \
1429                </td> \
1430              </tr> \
1431              <tr> \
1432                <td colspan="3"> \
1433                  <div class="input-container"> \
1434                    <textarea class="expression-preview-code"></textarea> \
1435                  </div> \
1436                </td> \
1437                <td class="expression-preview-parsing-status" width="150" style="vertical-align: top;"> \
1438                  No syntax error. \
1439                </td> \
1440              </tr> \
1441              <tr> \
1442                <td colspan="4"> \
1443                  <div id="expression-preview-tabs" class="refine-tabs ui-tabs ui-widget ui-widget-content ui-corner-all"> \
1444                    <span>Preview</span> \
1445                    <div id="expression-preview-tabs-preview" class="ui-tabs-panel ui-widget-content ui-corner-bottom"> \
1446                      <div class="expression-preview-container" style="width: 652px; "> \
1447                      </div> \
1448                    </div> \
1449                  </div> \
1450                </td> \
1451              </tr> \
1452              </tbody> \
1453              </table> \
1454            </div> \
1455          </td> \
1456        </tr> \
1457        </tbody> \
1458        </table> \
1459      </div> \
1460    </div> \
1461    <div class="dialog-footer"> \
1462      <button class="okButton btn primary">&nbsp;&nbsp;Update All&nbsp;&nbsp;</button> \
1463      <button class="cancelButton btn danger">Cancel</button> \
1464    </div> \
1465  ',
1466
1467  events: {
1468    'click .okButton': 'onSubmit'
1469    , 'keydown .expression-preview-code': 'onEditorKeydown'
1470  },
1471
1472  initialize: function() {
1473    this.el = $(this.el);
1474  },
1475
1476  render: function() {
1477    var htmls = $.mustache(this.template, 
1478      {name: this.state.currentColumn}
1479      )
1480    this.el.html(htmls);
1481    // Put in the basic (identity) transform script
1482    // TODO: put this into the template?
1483    var editor = this.el.find('.expression-preview-code');
1484    editor.val("function(doc) {\n  doc['"+ this.state.currentColumn+"'] = doc['"+ this.state.currentColumn+"'];\n  return doc;\n}");
1485    editor.focus().get(0).setSelectionRange(18, 18);
1486    editor.keydown();
1487  },
1488
1489  onSubmit: function(e) {
1490    var self = this;
1491    var funcText = this.el.find('.expression-preview-code').val();
1492    var editFunc = costco.evalFunction(funcText);
1493    if (editFunc.errorMessage) {
1494      my.notify("Error with function! " + editFunc.errorMessage);
1495      return;
1496    }
1497    util.hide('dialog');
1498    my.notify("Updating all visible docs. This could take a while...", {persist: true, loader: true});
1499      var docs = self.model.currentDocuments.map(function(doc) {
1500       return doc.toJSON();
1501      });
1502    // TODO: notify about failed docs? 
1503    var toUpdate = costco.mapDocs(docs, editFunc).edited;
1504    var totalToUpdate = toUpdate.length;
1505    function onCompletedUpdate() {
1506      totalToUpdate += -1;
1507      if (totalToUpdate === 0) {
1508        my.notify(toUpdate.length + " documents updated successfully");
1509        alert('WARNING: We have only updated the docs in this view. (Updating of all docs not yet implemented!)');
1510        self.remove();
1511      }
1512    }
1513    // TODO: Very inefficient as we search through all docs every time!
1514    _.each(toUpdate, function(editedDoc) {
1515      var realDoc = self.model.currentDocuments.get(editedDoc.id);
1516      realDoc.set(editedDoc);
1517      realDoc.save().then(onCompletedUpdate).fail(onCompletedUpdate)
1518    });
1519  },
1520
1521  onEditorKeydown: function(e) {
1522    var self = this;
1523    // if you don't setTimeout it won't grab the latest character if you call e.target.value
1524    window.setTimeout( function() {
1525      var errors = self.el.find('.expression-preview-parsing-status');
1526      var editFunc = costco.evalFunction(e.target.value);
1527      if (!editFunc.errorMessage) {
1528        errors.text('No syntax error.');
1529        var docs = self.model.currentDocuments.map(function(doc) {
1530          return doc.toJSON();
1531        });
1532        var previewData = costco.previewTransform(docs, editFunc, self.state.currentColumn);
1533        util.render('editPreview', 'expression-preview-container', {rows: previewData});
1534      } else {
1535        errors.text(editFunc.errorMessage);
1536      }
1537    }, 1, true);
1538  }
1539});
1540
1541})(jQuery, recline.View);
1542// # Recline Backends
1543//
1544// Backends are connectors to backend data sources and stores
1545//
1546// This is just the base module containing various convenience methods.
1547this.recline = this.recline || {};
1548this.recline.Backend = this.recline.Backend || {};
1549
1550(function($, my) {
1551  // ## Backbone.sync
1552  //
1553  // Override Backbone.sync to hand off to sync function in relevant backend
1554  Backbone.sync = function(method, model, options) {
1555    return model.backend.sync(method, model, options);
1556  }
1557
1558  // ## wrapInTimeout
1559  // 
1560  // Crude way to catch backend errors
1561  // Many of backends use JSONP and so will not get error messages and this is
1562  // a crude way to catch those errors.
1563  my.wrapInTimeout = function(ourFunction) {
1564    var dfd = $.Deferred();
1565    var timeout = 5000;
1566    var timer = setTimeout(function() {
1567      dfd.reject({
1568        message: 'Request Error: Backend did not respond after ' + (timeout / 1000) + ' seconds'
1569      });
1570    }, timeout);
1571    ourFunction.done(function(arguments) {
1572        clearTimeout(timer);
1573        dfd.resolve(arguments);
1574      })
1575      .fail(function(arguments) {
1576        clearTimeout(timer);
1577        dfd.reject(arguments);
1578      })
1579      ;
1580    return dfd.promise();
1581  }
1582}(jQuery, th

Large files files are truncated, but you can click here to view the full file