PageRenderTime 58ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 1ms

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

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