diff --git a/.gitignore b/.gitignore deleted file mode 100755 index fb371404..00000000 --- a/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -.DS_Store -sandbox/* -.*.swp -.*.swo -_site/* -node_modules/* diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 227a3756..00000000 --- a/.travis.yml +++ /dev/null @@ -1,4 +0,0 @@ -language: node_js -node_js: - - "0.10" -script: phantomjs test/qunit/runner.js test/index.html diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 97df3fc4..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,43 +0,0 @@ -# Contributing to Recline - -We welcome patches and pull requests. There are a few guidelines. - -## General - -* Please run the tests :-) (see below) -* Please do **not** build the dist files (e.g. dist/recline.js) when submitting - patches. dist files will get built automatically and if they are part of a - patch or pull request it makes them harder to review and more likely to - conflict. -* If possible have an issue to which the commits can relate. You can reference - an issue in the commits by just including #{issue-number} somewhere in the - commit message). Note if no issue exists suggest creating one. - -## For larger changes - -* Cleanup your code and affected code parts -* Run the tests from `/test/index.html` in different browsers (at least Chrome and FF) -* Update the documentation and tutorials where necessary -* Update `/_includes/recline-deps.html` if you change required files (e.g. leaflet libraries) -* Try to build the demos in `/demos/` with jekyll and then check out the `/demos/multiview/` which utilizes most aspects of Recline - -You will also probably want to take a quick look at outline of the architecture which can be found in the [documentation online](http://okfnlabs.org/recline). - -## Running tests - -Run the tests by opening `test/index.html` in your browser. - -## Demos and Documentation - -Note that the demos and documentation utilize the [jekyll templating -system][jekyll] and to use them *locally* you will need to build them using -jekyll. Once installed, all you need to do from the command line is run jekyll: - - jekyll serve - -or if you're actively developing and want auto-reloading: - - jekyll serve --watch - -[jekyll]: https://github.com/mojombo/jekyll - diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100755 index a0993c64..00000000 --- a/LICENSE.txt +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2011-2014 Max Ogden, Rufus Pollock and Contributors - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md deleted file mode 100755 index aa062840..00000000 --- a/README.md +++ /dev/null @@ -1,148 +0,0 @@ -[![Build Status](https://travis-ci.org/datopian/recline.png)](https://travis-ci.org/datopian/recline) - -A simple but powerful library for building data applications in pure Javascript and HTML. - -

Recline Website - including Overview, Documentation, Demos etc

- -## Features - -* Open-source (and heavy reuser of existing open-source libraries) -* Pure javascript (no Flash) and designed for integration -- so it is easy to - embed in other sites and applications -* View and edit your data in clean grid interface -* Bulk update/clean your data using an easy scripting UI -* Visualize your data -* And more ... see - -## Contributing - -See CONTRIBUTING.md. - -### Contributors - -* [Rufus Pollock](http://rufuspollock.org/) -* [Max Ogden](http://maxogden.com/) -* [John Glover](https://github.com/johnglover) -* [James Casbon](http://casbon.me/) -* [AdriĆ  Mercader](http://amercader.net/) -* [Dominik Moritz](https://github.com/domoritz) -* [Friedrich Lindenberg](http://pudo.org/) -* [Alioune Dia](https://github.com/aliounedia) -* [kielni](https://github.com/kielni) -* And [many more](https://github.com/okfn/recline/graphs/contributors) - -## Changelog - - -### v0.7 - Summer 2014 (tbc) - -[v0.7 milestone](https://github.com/okfn/recline/issues?milestone=7) - -Possible breaking changes - -* Support for row/add/delete/Reorder for recline slickGrid check `_includes/recline-deps.html` for slcikGrid plugins required #396 -* Upgraded timelinejs lib - #316 -* Removed csv backend (as now in separate repo) #444 - -### v0.6 - Summer 2013 - -[v0.6 milestone](https://github.com/okfn/recline/issues?milestone=5) (more than 40 issues) - -Possible breaking changes - -* Many backends moved to their own repositories #314 -* Upgarde to Backbone v1.0 #351 -* Updated Leaflet to latest version 0.4.4 #220 -* Added marker clustering in map view to handle a large number of markers (and allowed it to disabled) -* Dataset.restore method removed (not used internally except from Multiview.restore) -* Views no longer call render in initialize but must be called client code -* Backend.Memory.Store attribute for holding 'records' renamed to `records` from `data` -* Option to use underscore.deferred vendor library and not use jQuery (jQuery no longer required if just using recline.dataset.js) -* View.el is now the raw DOM element. If you want a jQuery-wrapped version, use view.$el. #350 -* Pager widget now takes Dataset object rather than QueryState object #386 - -### v0.5 - July 5th 2012 (first public release) - -[40 closed issues](https://github.com/okfn/recline/issues?milestone=2&page=1&state=closed) - -Lots of breaking changes to the API from v0.4 (should be very few going forwards) including: - -* State only stores backend (name) and dataset url (in url field) rather than entire dataset object -* Backends heavily reorganized -* Rename Document -> Record -* Rename DataExplorer view to MultiView -* ... - -### v0.4 - April 26th 2012 - -[23 closed issues](https://github.com/okfn/recline/issues?milestone=2&page=1&state=closed) including: - -* Map view using Leaflet - #69, #64, #89, #97 -* Term filter support - #66 -* Faceting support- #62 -* Tidy up CSS and JS - #81 and #78 -* Manage and serialize view and dataset state (plus support for embed and permalinks) - #88, #67 -* Graph view improvements e.g. handle date types correctly - #75 -* Write support for ES backend - #61 -* Remove JQuery-UI dependency in favour of bootstrap modal - #46 -* Improved CSV import support - #92 - -### v0.3 - March 31st 2012 - -[16 closed issues](https://github.com/okfn/recline/issues?milestone=1&state=closed) including: - -* ElasticSearch (and hence DataHub/CKAN) backend - #54 -* Loading of local CSV files - #36 -* Fully worked out Data Query support - #34, #49, #53, #57 -* New Field model object for richer field information - #25 -* Upgrade to Bootstrap v2.0 - #55 -* Recline Data Explorer app improvements e.g. #39 (import menu) -* Graph improvements - #58 (more graph types, graph interaction) - -### v0.2 - Feb 24th 2012 - -[17 closed issues](https://github.com/okfn/recline/issues?milestone=3&state=closed) including: - -* Major refactor of backend and model relationship - #35 and #43 -* Support Google Docs Spreadsheets as a Backend - #15 -* Support for online CSV and Excel files via DataProxy backend - #31 -* Data Explorer is customizable re loaded views - #42 -* Start of documentation - #33 -* Views in separate files - #41 -* Better error reporting from backends on JSONP errors - #30 -* Sorting and show/hide of columns in data grid - #23, #29 -* Support for pagination - #27 -* Split backends into separate files to make them easier to maintain and reuse separately #50 - -### v0.1 - Jan 28th 2012 - -* Core models and structure including Dataset and Document -* Memory and webstore backends -* Grid, Graph and Data Explorer views -* Bootstrap-based theme - #22 - -## Copyright and License - -Copyright 2011 Max Ogden and Rufus Pollock. - -Licensed under the MIT license: - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - - diff --git a/_config.yml b/_config.yml deleted file mode 100644 index d6aefa8a..00000000 --- a/_config.yml +++ /dev/null @@ -1,6 +0,0 @@ -highlighter: pygments -markdown: kramdown - -title: Recline Data Explorer and Library - -exclude: [] diff --git a/_includes/backend-list.html b/_includes/backend-list.html deleted file mode 100644 index 6fcbf737..00000000 --- a/_includes/backend-list.html +++ /dev/null @@ -1,9 +0,0 @@ -* gdocs: Google Docs (Spreadsheet) -* csv: CSV files -* xlsx: Excel files -* solr: SOLR (partial) -* elasticsearch: ElasticSearch -* dataproxy: DataProxy (CSV and XLS on the Web) -* ckan: CKAN – support for CKAN datastore -* couchdb: CouchDB -* memory: Memory (local data) diff --git a/_includes/data.js b/_includes/data.js deleted file mode 100644 index 57065512..00000000 --- a/_includes/data.js +++ /dev/null @@ -1,9 +0,0 @@ -var data = [ - {id: 0, date: '2011-01-01', x: 1, y: 2, z: 3, country: 'DE', geo: {lat:52.56, lon:13.40} }, - {id: 1, date: '2011-02-02', x: 2, y: 4, z: 24, country: 'UK', geo: {lat:54.97, lon:-1.60}}, - {id: 2, date: '2011-03-03', x: 3, y: 6, z: 9, country: 'US', geo: {lat:40.00, lon:-75.5}}, - {id: 3, date: '2011-04-04', x: 4, y: 8, z: 6, country: 'UK', geo: {lat:57.27, lon:-6.20}}, - {id: 4, date: '2011-05-04', x: 5, y: 10, z: 15, country: 'UK', geo: {lat:51.58, lon:0}}, - {id: 5, date: '2011-06-02', x: 6, y: 12, z: 18, country: 'DE', geo: {lat:51.04, lon:7.9}} -]; - diff --git a/_includes/example-backends-csv-disk.js b/_includes/example-backends-csv-disk.js deleted file mode 100644 index 537340b3..00000000 --- a/_includes/example-backends-csv-disk.js +++ /dev/null @@ -1,22 +0,0 @@ -// the file input -var $file = $('.my-file-input')[0]; - -// listen for the file to be submitted -$($file).change(function(e) { - // create the dataset in the usual way but specifying file attribute - var dataset = new recline.Model.Dataset({ - file: $file.files[0], - backend: 'csv' - }); - - // now load - note that this is again async (HTML5 File API is async) - // dataset.fetch().done(function() { console.log('here'); }); - dataset.fetch(); - - // For demonstrations purposes display the data in a grid - var grid = new recline.View.Grid({ - model: dataset - }); - $('#my-csv-disk').append(grid.el); -}); - diff --git a/_includes/example-backends-dataproxy.js b/_includes/example-backends-dataproxy.js deleted file mode 100644 index 9d4aea3c..00000000 --- a/_includes/example-backends-dataproxy.js +++ /dev/null @@ -1,17 +0,0 @@ -var dataset = new recline.Model.Dataset({ - url: 'http://data.london.gov.uk/datafiles/transport/tfl_passengers.csv', - // optional rows parameter specifies how many rows to retrieve - default is a 1000 - // rows: 5000 - backend: 'dataproxy' -}); - -// async again as we fetch via AJAX behind the scenes -// once data is fetched it will be stored in a local MemoryStore so further querying will not involve the DataProxy -dataset.fetch(); - -// For demonstrations purposes display the data in a grid -var grid = new recline.View.Grid({ - model: dataset -}); -$('#my-dataproxy').append(grid.el); - diff --git a/_includes/example-backends-gdocs.js b/_includes/example-backends-gdocs.js deleted file mode 100644 index 5ef56069..00000000 --- a/_includes/example-backends-gdocs.js +++ /dev/null @@ -1,21 +0,0 @@ -// Create a dataset with a Google Docs backend and a url to the Google Doc -var dataset = new recline.Model.Dataset({ - url: 'https://docs.google.com/spreadsheet/ccc?key=0Aon3JiuouxLUdGZPaUZsMjBxeGhfOWRlWm85MmV0UUE#gid=0', - backend: 'gdocs' -}); - -// Optional - display the results in a grid -// Note how we can set this up before any data has arrived -// Views will listen for query completion and update themselves automatically -var grid = new recline.View.Grid({ - model: dataset -}); -$('#my-gdocs').append(grid.el); - -// Now do the query to the backend to load data -dataset.fetch().done(function(dataset) { - if (console) { - console.log(dataset.records); - } -}); - diff --git a/_includes/example-backends-online-csv.js b/_includes/example-backends-online-csv.js deleted file mode 100644 index a03fc362..00000000 --- a/_includes/example-backends-online-csv.js +++ /dev/null @@ -1,21 +0,0 @@ -// Create the dataset in the usual way -// Note the additional options you can specify for parsing the CSV file -var dataset = new recline.Model.Dataset({ - url: '{{page.root}}demos/data/sample.csv', - backend: 'csv', - // delimiter: ',', - // quotechar: '"', - // encoding: 'utf8' -}); - -// remember this is async so if you want to do something you need to call it in done method e.g. -// dataset.fetch.done(function(dataset) { console.log(dataset.recordCount)}); -dataset.fetch(); - -// show the data for illustrations sake -var grid = new recline.View.SlickGrid({ - model: dataset, - el: $('#my-online-csv') -}); -grid.visible = true; - diff --git a/_includes/recline-deps.html b/_includes/recline-deps.html deleted file mode 100644 index fb77d16b..00000000 --- a/_includes/recline-deps.html +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/_includes/tutorial-basics-ex-1.js b/_includes/tutorial-basics-ex-1.js deleted file mode 100644 index 2ca52f8f..00000000 --- a/_includes/tutorial-basics-ex-1.js +++ /dev/null @@ -1,29 +0,0 @@ -// (for convenience) assume availability of jquery -// must have div with class="ex-1" -var $el = $('.ex-1'); - -// total number of records resulting from latest query -$el.append('Total found: ' + dataset.recordCount + '
'); -$el.append('Total returned: ' + dataset.records.length); - -$el.append('
'); - -// get 2nd record in list (note collection indexes off 0!) -// this is an instance of a Record object -var record = dataset.records.at(1); - -// if records have an id you can get by id too ... -// var record = dataset.records.get(record-id); - -// To get record attribute we use 'get' -var recdate = record.get('date'); - -$el.append('Date is: ' + recdate); -$el.append('
'); - -// We can also convert the Record back to simple JS object -var simple = record.toJSON(); - -$el.append('

Record as simple object

'); -$el.append('
' + JSON.stringify(simple, null, 2) + '
'); - diff --git a/_includes/tutorial-basics-ex-2.js b/_includes/tutorial-basics-ex-2.js deleted file mode 100644 index 7e22739d..00000000 --- a/_includes/tutorial-basics-ex-2.js +++ /dev/null @@ -1,19 +0,0 @@ -// must have a div with class="ex-1" -var $el = $('.ex-2'); - -// query with text 'UK' - this will attempt to match any field for UK -// Also limit the query to return a maximum of 2 results using the size attribute - -// query function has asynchronous behaviour and returns a promise object -// (even for the case of in memory data where querying in fact happens synchronously) -// On completion the display function will be called -dataset.query({q: 'UK', size: 2}).done(function() { - $('.ex-2').append('Total found: ' + dataset.recordCount); - $('.ex-2').append(' Total returned: ' + dataset.records.length); - $('.ex-2').append( - $('
').html(
-      JSON.stringify(dataset.records.toJSON(), null, 2)
-    )
-  );
-});
-
diff --git a/_includes/tutorial-basics-ex-events.js b/_includes/tutorial-basics-ex-events.js
deleted file mode 100644
index 0f50bf15..00000000
--- a/_includes/tutorial-basics-ex-events.js
+++ /dev/null
@@ -1,13 +0,0 @@
-function onChange() {
-  $('.ex-events').append('Queried: ' + dataset.queryState.get('q') + '. Records matching: ' + dataset.recordCount);
-  $('.ex-events').append('
'); -} - -dataset.records.bind('reset', onChange); - -dataset.query({q: 'DE'}); -dataset.query({q: 'UK'}); -dataset.query({q: 'US'}); - -dataset.unbind('reset', onChange); - diff --git a/_includes/tutorial-basics-ex-fields-2.js b/_includes/tutorial-basics-ex-fields-2.js deleted file mode 100644 index ed4127c5..00000000 --- a/_includes/tutorial-basics-ex-fields-2.js +++ /dev/null @@ -1,10 +0,0 @@ -var $el = $('.ex-fields-2'); - -dataset.fields.models[6] = new recline.Model.Field({ - id: 'geo', - label: 'Location', - type: 'geo_point' -}); -var rec = dataset.records.at(0); -$el.append(record.summary()); - diff --git a/_includes/tutorial-basics-ex-fields.js b/_includes/tutorial-basics-ex-fields.js deleted file mode 100644 index d6cc38eb..00000000 --- a/_includes/tutorial-basics-ex-fields.js +++ /dev/null @@ -1,20 +0,0 @@ -var $el = $('.ex-fields'); - -// Now list the Fields of this Dataset (these will have be automatically extracted from the data) -$el.append('Fields: '); -// Dataset.fields is a Backbone collection of Fields (i.e. record attributes) -dataset.fields.each(function(field) { - $el.append(field.id + ' || '); -}); - -$el.append('
'); - -// Show all field info -var json = dataset.fields.toJSON(); -$el.append( - $('
')
-    .append(
-      JSON.stringify(json, null, 2)
-    )
-);
-
diff --git a/_includes/tutorial-maps-customize.js b/_includes/tutorial-maps-customize.js
deleted file mode 100644
index 3433e3a7..00000000
--- a/_includes/tutorial-maps-customize.js
+++ /dev/null
@@ -1,17 +0,0 @@
-var $el = $('#map-customize');
-var view = new recline.View.Map({
-  el: $el,
-  model: dataset
-});
-
-view.geoJsonLayerOptions.pointToLayer = function(feature, latlng) {
-  // Look up Record so we can use it to customize size of marker
-  // note that 'this' is specially bound for us to parent view + that feature
-  // stores record cid
-  var record = this.model.records.getByCid(feature.properties.cid);
-  var marker = new L.CircleMarker(latlng, { radius: record.get('x') * 3 });
-  marker.bindPopup(feature.properties.popupContent);
-  return marker;
-}
-view.render();
-
diff --git a/_includes/tutorial-maps-infobox.js b/_includes/tutorial-maps-infobox.js
deleted file mode 100644
index 1baf7729..00000000
--- a/_includes/tutorial-maps-infobox.js
+++ /dev/null
@@ -1,14 +0,0 @@
-// this element will need to exist!
-var $el = $('#map-infobox');
-var view = new recline.View.Map({
-  el: $el,
-  model: dataset
-});
-// record is the recline.Model.Record object
-view.infobox = function(record) {
-  var html = '

' + record.get('country') + ' – ' + record.get('date') + '

'; - html += 'id: ' + record.get('id'); - return html; -} -view.render(); - diff --git a/_includes/tutorial-views-timeline.js b/_includes/tutorial-views-timeline.js deleted file mode 100644 index cfb91cc7..00000000 --- a/_includes/tutorial-views-timeline.js +++ /dev/null @@ -1,15 +0,0 @@ -var $el = $('#mytimeline'); -var timeline = new recline.View.Timeline({ - model: dataset -}); -$el.append(timeline.el); -// set the headline/title for each record with x column -timeline.convertRecord = function(record, fields) { - var out = this._convertRecord(record); - if (out) { - out.headline = record.get('x').toString(); - } - return out; -} -timeline.render(); - diff --git a/_includes/views-list.html b/_includes/views-list.html deleted file mode 100644 index 9cb68d3b..00000000 --- a/_includes/views-list.html +++ /dev/null @@ -1,7 +0,0 @@ -* Grid (simple) -* Grid (SlickGrid) -* Map -* Choropleth Map -* Graph -* Timeline -* Multiview (combines views) \ No newline at end of file diff --git a/_layouts/container.html b/_layouts/container.html deleted file mode 100644 index 4abf7ae8..00000000 --- a/_layouts/container.html +++ /dev/null @@ -1,8 +0,0 @@ ---- -layout: default ---- - -
- {{content}} -
- diff --git a/_layouts/default.html b/_layouts/default.html deleted file mode 100644 index 830a8cad..00000000 --- a/_layouts/default.html +++ /dev/null @@ -1,128 +0,0 @@ - - - - - {{ page.title }} - Recline Data Explorer and Library - - - - - - - {% if page.recline-deps %} - {% include recline-deps.html %} - {% endif %} - - - - - - - - - - {{content}} - - - - - - - diff --git a/css-site/images/header-screen.png b/css-site/images/header-screen.png deleted file mode 100644 index 0aeb8ca1..00000000 Binary files a/css-site/images/header-screen.png and /dev/null differ diff --git a/css-site/images/icons/white.png b/css-site/images/icons/white.png deleted file mode 100644 index 76806990..00000000 Binary files a/css-site/images/icons/white.png and /dev/null differ diff --git a/css-site/images/zigzags.png b/css-site/images/zigzags.png deleted file mode 100644 index 42e40ccb..00000000 Binary files a/css-site/images/zigzags.png and /dev/null differ diff --git a/css-site/pygments.css b/css-site/pygments.css deleted file mode 100644 index 122b4294..00000000 --- a/css-site/pygments.css +++ /dev/null @@ -1,61 +0,0 @@ -.hll { background-color: #ffffcc } -.c { color: #408080; font-style: italic } /* Comment */ -.err { border: 1px solid #FF0000 } /* Error */ -.k { color: #008000; font-weight: bold } /* Keyword */ -.o { color: #666666 } /* Operator */ -.cm { color: #408080; font-style: italic } /* Comment.Multiline */ -.cp { color: #BC7A00 } /* Comment.Preproc */ -.c1 { color: #408080; font-style: italic } /* Comment.Single */ -.cs { color: #408080; font-style: italic } /* Comment.Special */ -.gd { color: #A00000 } /* Generic.Deleted */ -.ge { font-style: italic } /* Generic.Emph */ -.gr { color: #FF0000 } /* Generic.Error */ -.gh { color: #000080; font-weight: bold } /* Generic.Heading */ -.gi { color: #00A000 } /* Generic.Inserted */ -.go { color: #808080 } /* Generic.Output */ -.gp { color: #000080; font-weight: bold } /* Generic.Prompt */ -.gs { font-weight: bold } /* Generic.Strong */ -.gu { color: #800080; font-weight: bold } /* Generic.Subheading */ -.gt { color: #0040D0 } /* Generic.Traceback */ -.kc { color: #008000; font-weight: bold } /* Keyword.Constant */ -.kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ -.kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ -.kp { color: #008000 } /* Keyword.Pseudo */ -.kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ -.kt { color: #B00040 } /* Keyword.Type */ -.m { color: #666666 } /* Literal.Number */ -.s { color: #BA2121 } /* Literal.String */ -.na { color: #7D9029 } /* Name.Attribute */ -.nb { color: #008000 } /* Name.Builtin */ -.nc { color: #0000FF; font-weight: bold } /* Name.Class */ -.no { color: #880000 } /* Name.Constant */ -.nd { color: #AA22FF } /* Name.Decorator */ -.ni { color: #999999; font-weight: bold } /* Name.Entity */ -.ne { color: #D2413A; font-weight: bold } /* Name.Exception */ -.nf { color: #0000FF } /* Name.Function */ -.nl { color: #A0A000 } /* Name.Label */ -.nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ -.nt { color: #008000; font-weight: bold } /* Name.Tag */ -.nv { color: #19177C } /* Name.Variable */ -.ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ -.w { color: #bbbbbb } /* Text.Whitespace */ -.mf { color: #666666 } /* Literal.Number.Float */ -.mh { color: #666666 } /* Literal.Number.Hex */ -.mi { color: #666666 } /* Literal.Number.Integer */ -.mo { color: #666666 } /* Literal.Number.Oct */ -.sb { color: #BA2121 } /* Literal.String.Backtick */ -.sc { color: #BA2121 } /* Literal.String.Char */ -.sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ -.s2 { color: #BA2121 } /* Literal.String.Double */ -.se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ -.sh { color: #BA2121 } /* Literal.String.Heredoc */ -.si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ -.sx { color: #008000 } /* Literal.String.Other */ -.sr { color: #BB6688 } /* Literal.String.Regex */ -.s1 { color: #BA2121 } /* Literal.String.Single */ -.ss { color: #19177C } /* Literal.String.Symbol */ -.bp { color: #008000 } /* Name.Builtin.Pseudo */ -.vc { color: #19177C } /* Name.Variable.Class */ -.vg { color: #19177C } /* Name.Variable.Global */ -.vi { color: #19177C } /* Name.Variable.Instance */ -.il { color: #666666 } /* Literal.Number.Integer.Long */ diff --git a/css-site/style.css b/css-site/style.css deleted file mode 100644 index f0a9e030..00000000 --- a/css-site/style.css +++ /dev/null @@ -1,251 +0,0 @@ -/* -Theme Name: Recline -Description: Layout and styling for reclinejs.com -Author: Sam Smith -Author URI: http://www.mintcanary.com/ -*/ - -/* -------------------------------------------------- - Table of Contents ------------------------------------------------------ -:: General Styles -:: Layout -:: -:: -:: -*/ - -/* --------------------------------------------------- - General Styles ---------------------------------------------------- */ - -@import url(http://fonts.googleapis.com/css?family=PT+Sans:400,400italic,700); - -body, p { - font-family: 'PT Sans',Helvetica,Arial,sans-serif; - font-size: 15px; -} - -h1, h2, h3, h4, h5, h6 { - font-family:'PT Sans',Helvetica, Arial, sans-serif; - font-weight: bold; -} - -h2 { - font-size: 24px; -} - -h3 { - font-size: 20px; -} - -h4 { - font-size: 18px; -} - -a { - color: #c7231d; -} -a:hover { - color: #bc130e; -} - -/* --------------------------------------------------- - Layout ---------------------------------------------------- */ - -.navbar-header{ - -} - -.navbar-header .logo-icon { - height: 34px; -} - -.navbar-brand { - font-family:'PT Sans', Helvetica, Arial, sans-serif; - font-style:italic; - font-size:18px; - font-weight:400; - letter-spacing:-1px; - text-shadow: none !important; - color: #FFF !important; - margin-top: -6px; -} - -.navbar-nav > li > a { - padding: 15px 10px; - font-size: 13px; - text-shadow: none !important; - color: #999 !important; -} - -.navbar-nav > li > a:focus, -.navbar-nav > li > a:hover { - color: #FFF !important; -} - -.navbar .divider-vertical { - height: 50px; - margin: 0 9px; - border-right: 1px solid #ffffff; -} - -.navbar-inverse .divider-vertical { - border-right-color: #222222; -} - -@media (max-width: 767px) { - .navbar-collapse .nav > .divider-vertical { - display: none; - } -} - -.navbar { - height:50px; - background: #303030; /* Old browsers */ - background: -moz-linear-gradient(top, #303030 0%, #2d2d2d 100%); /* FF3.6+ */ - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#303030), color-stop(100%,#2d2d2d)); /* Chrome,Safari4+ */ - background: -webkit-linear-gradient(top, #303030 0%,#2d2d2d 100%); /* Chrome10+,Safari5.1+ */ - background: -o-linear-gradient(top, #303030 0%,#2d2d2d 100%); /* Opera 11.10+ */ - background: -ms-linear-gradient(top, #303030 0%,#2d2d2d 100%); /* IE10+ */ - background: linear-gradient(top, #303030 0%,#2d2d2d 100%); /* W3C */ - filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#303030', endColorstr='#2d2d2d',GradientType=0 ); /* IE6-9 */ - -webkit-box-shadow:none; - -moz-box-shadow: none; - box-shadow: none; -} - -.icon-white{ - color: #FFF; - margin-right: 1px; -} - -a.btn, -button.btn { - white-space: normal !important; -} - -body { - padding-top: 60px; -} - -section { - padding-top:20px; -} - -.home-page.page-header { - margin-top:-10px; - background: #2d2d2d; /* Old browsers */ - background: -moz-linear-gradient(top, #2d2d2d 0%, #040404 100%); /* FF3.6+ */ - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#2d2d2d), color-stop(100%,#040404)); /* Chrome,Safari4+ */ - background: -webkit-linear-gradient(top, #2d2d2d 0%,#040404 100%); /* Chrome10+,Safari5.1+ */ - background: -o-linear-gradient(top, #2d2d2d 0%,#040404 100%); /* Opera 11.10+ */ - background: -ms-linear-gradient(top, #2d2d2d 0%,#040404 100%); /* IE10+ */ - background: linear-gradient(top, #2d2d2d 0%,#040404 100%); /* W3C */ - filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#2d2d2d', endColorstr='#040404',GradientType=0 ); /* IE6-9 */ - padding:0px; - margin-bottom:0px; - border:none; - padding: 60px; - padding-bottom: 200px; - /* hide crocodile top to footer on front page */ - margin-bottom: -30px; -} -.home-page.page-header p { - color:#FFF; -} -.home-page.page-header a { -} -.home-page.page-header .container { - background-image: url(images/header-screen.png); - background-repeat: no-repeat; - background-position: -3px 0px; -} -.home-page.page-header .inner { - padding:0px 0px 0px 40px; - font-size:16px; - line-height: 23px; - width: 400px; -} - -.home-page.page-header:after { - margin-top:-14px; -} - -.home-page.page-header .links { - margin-top: 30px; - margin-bottom: 0; -} - -.home-page.page-header .links a { - margin-left: 25px; -} - -.home-page.page-header .links a:first-child { - margin-left: 0px; -} - -/* --------------------------------------------------- - Footer ---------------------------------------------------- */ - -.recline-footer { - background-color:#040404; - color:#CCC; - margin-top: 30px; -} -.recline-footer:before { - content: " "; - height:14px; - display:block; - background-image: url(images/zigzags.png); - background-repeat: repeat-x; - background-position: center -100px; - margin-top:-34px; -} -.recline-footer:after { - display:none; -} -.recline-footer .row { - margin-top:15px; - margin-bottom:15px; -} -.recline-footer a { - color:#CCC; -} -.recline-footer a.btn { - color: #333333; -} - -.tutorials .well { - height: 60px; -} - -.tutorials:last-child { - margin-bottom: 200px; -} - -/** Code / Pre **/ - -.container pre { - padding: 10px 15px; - border: 1px solid #ccc; - background: white; - color: #444; - - box-shadow: 0 0 15px #ddd; - -moz-box-shadow: 0 0 15px #ddd; - -webkit-box-shadow: 0 0 15px #ddd; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} - -.doc-ex-rendered { - margin-bottom: 20px; -} - -#my-online-csv { - height: 100px; -} diff --git a/css/flot.css b/css/flot.css deleted file mode 100644 index 57adc9f0..00000000 --- a/css/flot.css +++ /dev/null @@ -1,26 +0,0 @@ -.recline-flot .graph { - height: 500px; - overflow: hidden; -} - -.recline-flot .legend table { - width: auto; - margin-bottom: 0; -} - -.recline-flot .legend td { - padding: 5px; - line-height: 13px; -} - -.recline-flot .graph .alert { - width: 450px; -} - -#recline-flot-tooltip { - position: absolute; - background-color: #FEE; - color: #000000; - opacity: 0.8; - border: 1px solid #fdd; -} diff --git a/css/grid.css b/css/grid.css deleted file mode 100644 index 05d6f619..00000000 --- a/css/grid.css +++ /dev/null @@ -1,221 +0,0 @@ -/********************************************************** - * (Data) Grid - *********************************************************/ - -table.recline-grid { - table-layout: fixed; - width: 100%; -} - -.recline-grid .btn-group .dropdown-toggle { - padding: 1px 3px; - line-height: auto; -} - -.recline-grid td, .recline-grid th { - border-left: 1px solid #ccc; - padding: 3px 4px; - text-align: left; - word-wrap: break-word; - white-space: normal; -} - -.recline-grid tbody tr { - vertical-align: top; - border-bottom: solid 1px #ccc; -} - -.recline-grid tbody tr:last-child { - border-bottom: 1px solid #ccc; -} - -.recline-grid tbody td:last-child { - border-right: 1px solid #ccc; -} - -/* direct borrowing from twitter buttons */ -.recline-grid th { - background-color: #e6e6e6; - background-repeat: no-repeat; - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), color-stop(25%, #ffffff), to(#e6e6e6)); - background-image: -webkit-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); - background-image: -moz-linear-gradient(top, #ffffff, #ffffff 25%, #e6e6e6); - background-image: -ms-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); - background-image: -o-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); - background-image: linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0); - text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); - color: #333; - border: 1px solid #ccc; - border-bottom-color: #bbb; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - -webkit-transition: 0.1s linear all; - -moz-transition: 0.1s linear all; - -ms-transition: 0.1s linear all; - -o-transition: 0.1s linear all; - transition: 0.1s linear all; -} - -/********************************************************** - * Fixed Header - http://www.imaputz.com/cssStuff/bigFourVersion.html - *********************************************************/ - -div.table-container { - overflow: auto; -} - -/* Reset overflow value to hidden for all non-IE browsers. */ -html>body div.table-container { - overflow: hidden; -} - -thead.fixed-header tr { - overflow-x: hidden; -} - -/* set table header to a fixed position. WinIE 6.x only */ -/* In WinIE 6.x, any element with a position property set to relative and is a child of */ -/* an element that has an overflow property set, the relative value translates into fixed. */ -/* Ex: parent element DIV with a class of table-container has an overflow property set to auto */ -thead.fixed-header tr { - position: relative -} - -/* set THEAD element to have block level attributes. All other non-IE browsers */ -/* this enables overflow to work on TBODY element. All other non-IE, non-Mozilla browsers */ -html>body thead.fixed-header tr { - display: block -} - -/* define the table content to be scrollable */ -/* set TBODY element to have block level attributes. All other non-IE browsers */ -/* this enables overflow to work on TBODY element. All other non-IE, non-Mozilla browsers */ -/* induced side effect is that child TDs no longer accept width: auto */ -tbody.scroll-content { - display: block; - max-height: 500px; - overflow: auto; -} - -/********************************************************** - * Data Table Menus - *********************************************************/ - -.column-header-menu, a.root-header-menu { - float: right; -} - -div.data-table-cell-content { - line-height: 1.2; - color: #222; - position: relative; -} - -div.data-table-cell-content-numeric { - text-align: right; -} - -a.data-table-cell-edit { - position: absolute; - top: 0; - right: 0; - display: block; - width: 25px; - height: 16px; - text-decoration: none; - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAQCAYAAABUWyyMAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OTNEODNFMjVEMDA5MTFERjk5NzhEQzZDRDUwRkEzMUEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OTNEODNFMjZEMDA5MTFERjk5NzhEQzZDRDUwRkEzMUEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo5M0Q4M0UyM0QwMDkxMURGOTk3OERDNkNENTBGQTMxQSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo5M0Q4M0UyNEQwMDkxMURGOTk3OERDNkNENTBGQTMxQSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PpApVmcAAAKVSURBVHjaYvz//z/DcAAsIGLaxjf2QKoLiM2oaPYpIC7L8hc5COI4F9+hmR17e1UOskAFVgCxBJUDyQxqriQ97GCCCpBkATCUGQxVOTHY7KyMDFoKHMhKJXCwCQJgKDNEOAlisHk5mRh8LPgw7GChNEg2HfvI8OnbPzDbwYAH6BkmhmsPflA12EtnPmN4/vY3mF0YKgb2zJYTn5CV/EfxiKgAC4O7CS8DHzczw5PXvxl2nv7E8PP3fxTx87e/o1hipMrFcOfZT6BmVgYZUTawmKUWN8Pxa1+xOkpNhp2hLk6CQVKYleHcrW8MTYteMHz+/g9FfMW+9yh6Ih0FGA5e/MLAAIx5IzUusFiqtzDD7K1vYUpcmZA1+FnxM7z++AccyqBkAgphmMPY2ZiAHvsMFkcGMkAPgMQ+ff3L8PPXPzD++O0vztDtypBmuPXkJziUebiYwSEMcxiID/IYiEYJLKDjQWLP3v1h+AI0G4RhMQQF5UzoDvr5CxIDoJhQlmKHy527/Y3hLjDkD4BCBgsAxSAoiYEwrqQFchAoWcBiAOQge30euBwoJkAh37/6FVb9oBgEeQCE0ZKWC8488vj1LzCmJTgLTKZn0ZIquYAJOURBsQDKB68//GHgYEWkOlBMgPICKIYcoCGIC4BiFRSDuEIUFBtSQizg5AWKHRgAxQSoZALFECy54QKgZAbLKxgegZVAfFxM4LyiLMUGTPeQ0ujAhS9gz7mb8sI9iisWQclSG7UIRgFlM56CM3R3uhTY0bC0DkpOtx//AGd4mEdxxSIoWaIVwQyMoCYKsGanWTsFWM8wQmt2mtkBqmdgMfKCRna8wMGmth2MMI/EAfEdKltwB2ouAz3sYBwurV8mhmECAAIMAEe3EkMWh/DvAAAAAElFTkSuQmCC); - background-repeat: no-repeat; - visibility: hidden; -} - -a.data-table-cell-edit:hover { - background-position: -25px 0px; -} - -.recline-grid td:hover .data-table-cell-edit { - visibility: visible; -} - -div.data-table-cell-content-numeric > a.data-table-cell-edit { - left: 0px; - right: auto; -} - -.data-table-value-nonstring { - color: #282; -} - -.data-table-error { - color: red; -} - -.data-table-cell-editor-editor { - overflow: hidden; - display: block; - width: 98%; - height: 3em; - font-family: monospace; - margin: 3px 0; -} - -.data-table-cell-copypaste-editor { - overflow: hidden; - display: block; - width: 98%; - height: 10em; - font-family: monospace; - margin: 3px 0; -} - -.data-table-cell-editor-action { - float: left; - vertical-align: bottom; - text-align: center; -} - -.data-table-cell-editor-key { - font-size: 0.8em; - color: #999; -} - -/********************************************************** - * Read-only mode - *********************************************************/ - -.recline-read-only .recline-grid .write-op, -.recline-read-only .recline-grid a.data-table-cell-edit -{ - display: none; -} - -.recline-read-only a.row-header-menu { - display: none; -} - -/************************* - * WCAG 2.0 - *************************/ - -.wcag_hide { - position:absolute; - top:0; - left:-10000px; - width:1px; - height:1px; -} - -.wcag_hide2 { - clip: rect(1px, 1px, 1px, 1px); - display: block; - position: absolute; -} - -.wcag_show_on_focus { - font-size: 0 !important; -} - -.wcag_show_on_focus:focus { - font-size: 1em !important; -} diff --git a/css/map.css b/css/map.css deleted file mode 100644 index f1f2da29..00000000 --- a/css/map.css +++ /dev/null @@ -1,28 +0,0 @@ -.recline-map .map { - height: 500px; -} - -/********************************************************** - * Editor - *********************************************************/ - -.recline-map .editor { - float: right; - width: 200px; - padding-left: 0px; - margin-left: 10px; -} - -.recline-map .editor form { - padding-left: 4px; -} - -.recline-map .editor select { - width: 100%; -} - -.recline-map .editor .editor-options { - margin-top: 10px; - border-top: 1px solid gray; - padding: 5px 0; -} diff --git a/css/multiview.css b/css/multiview.css deleted file mode 100644 index 4b359842..00000000 --- a/css/multiview.css +++ /dev/null @@ -1,330 +0,0 @@ -.recline-data-explorer .data-view-container { - display: block; -} - -.recline-data-explorer .data-view-sidebar { - float: right; - margin-left: 8px; - width: 220px; -} - -.recline-data-explorer .header .navigation { - margin-bottom: 8px; -} - -.recline-data-explorer .header .navigation, -.recline-data-explorer .header .pagination, -.recline-data-explorer .header .pagination form -{ - display: inline; -} - -.recline-data-explorer .header .navigation { - float: left; -} - -.recline-data-explorer .header .menu-right { - float: right; - margin-left: 5px; - padding-left: 5px; -} - -.recline-results-info { - line-height: 35px; - margin-left: 20px; - float: left; -} - -.recline-data-explorer .data-view-sidebar > div { - margin-top: 5px; - margin-bottom: 10px; -} - -.recline-data-explorer .radio, -.recline-data-explorer .checkbox { - padding-left: 20px; -} - -.recline-data-explorer .editor-update-map { - margin: 30px 0px 20px 0px; -} - -.recline-data-explorer label { - font-weight: normal; -} - -/********************************************************** - * Query Editor - *********************************************************/ - -.recline-query-editor { - float: right; - height: 35px; - padding-right: 5px; - margin-right: 5px; - border-right: solid 2px #ddd; -} - -.header .input-prepend { - margin-bottom: auto; -} - -.header .add-on { - float: left; -} - -/* needed for Chrome but not FF */ -.header .add-on { - margin-left: -27px; -} - -/* needed for FF but not chrome */ -.header .input-prepend { - vertical-align: top; -} - -.recline-query-editor form button { - vertical-align: top; -} - -/* label for screen reader */ -.recline-query-editor .form-inline label { - position: absolute; - top:0; - left:-9999px -} - -/********************************************************** - * Pager - *********************************************************/ - -.recline-pager { - float: left; - margin: auto; - display: block; - margin-left: 20px; -} - -.recline-pager .pagination li { - display: inline-block; -} - -.recline-pager .pagination label { - display:none; -} - -.recline-pager .pagination input { - width: 40px; - height: 25px; - padding: 2px 4px; - margin: 0; - margin-top: -2px; - - border: 1px solid #cccccc; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - transition: border linear 0.2s, box-shadow linear 0.2s; - border-radius: 4px; - - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; - -webkit-border-radius: 4px; - - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -moz-transition: border linear 0.2s, box-shadow linear 0.2s; - -moz-border-radius: 4px; - - -o-transition: border linear 0.2s, box-shadow linear 0.2s; -} - -.recline-pager .pagination a { - float: none; - margin-left: -5px; - color: #555; -} - -.recline-pager .pagination .page-range { - height: 34px; - padding: 5px 8px; - margin-left: -5px; - border: 1px solid #ddd; -} - -.recline-pager .pagination .page-range a { - padding: 0px 12px; - border: none; -} - -.recline-pager .pagination .page-range a:hover { - background-color: #ffffff; -} - -.recline-pager .pagination > li:first-child > a { - border-bottom-left-radius: 4px; - border-top-left-radius: 4px; - border-bottom-right-radius: 0px; - border-top-right-radius: 0px; - height: 34px; -} - -.recline-pager .pagination > li:last-child > a { - border-bottom-right-radius: 4px; - border-top-right-radius: 4px; - border-bottom-left-radius: 0px; - border-top-left-radius: 0px; - height: 34px; -} - -/********************************************************** - * Filter Editor - *********************************************************/ - -.recline-filter-editor { - padding: 8px; - display: none; -} - -.recline-filter-editor .filters { - margin: 20px 0px; -} - -.recline-filter-editor h3 { - margin-top: 4px; -} - -.recline-filter-editor .filter { - margin-top: 20px; -} - -.recline-filter-editor .filter .form-group { - margin-bottom: 0px; -} - -.recline-filter-editor .filter input, -.recline-filter-editor .filter label { - margin: 0px; -} - -.recline-filter-editor .js-edit button { - margin: 25px 0px 0px 0px; -} - -.recline-filter-editor .filter-term a { - font-size: 18px; -} - -.recline-filter-editor input, -.recline-filter-editor select -{ - width: 175px; -} - -.recline-filter-editor input { - margin-top: 0.5em; - margin-bottom: 10px; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - border: 1px solid #cccccc; -} - -.recline-filter-editor label { - font-weight: normal; - display: block; -} - -.recline-filter-editor legend { - margin-bottom: 5px; -} - -.recline-filter-editor .add-filter { - margin-top: 1em; - margin-bottom: 2em; -} - -.recline-filter-editor .update-filter { - margin-top: 1em; -} - -/********************************************************** - * Fields Widget - *********************************************************/ - -.recline-fields-view { - display: none; -} - -.recline-fields-view .fields-list { - padding: 0; -} - -.recline-fields-view .panel { - background-color: #f5f5f5; - border: 1px solid #e5e5e5; -} - -.recline-fields-view .panel-group h3 { - padding-left: 10px; -} - -.recline-fields-view .fields-list .panel-heading { - padding: 2px 5px; - margin: 1px 0px 1px 5px; -} - -.recline-fields-view .panel a, -.recline-fields-view .panel h4 { - display: inline; -} - -.recline-fields-view .panel a { - padding: 0; -} - -.recline-fields-view .panel h4 { - word-wrap: break-word -} - -.recline-fields-view .clear { - clear: both; -} - -.recline-fields-view .facet-items { - list-style-type: none; - margin-left: 0; -} - -.recline-fields-view .facet-item .term { - font-weight: bold; -} - -.recline-fields-view .facet-item .count { -} - -/********************************************************** - * Notifications - *********************************************************/ - -.recline-data-explorer .notification-loader { - width: 18px; - margin-left: 5px; - background-image: url(data:image/gif;base64,R0lGODlhEAAQAPQAAP///wAAAPDw8IqKiuDg4EZGRnp6egAAAFhYWCQkJKysrL6+vhQUFJycnAQEBDY2NmhoaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAAFdyAgAgIJIeWoAkRCCMdBkKtIHIngyMKsErPBYbADpkSCwhDmQCBethRB6Vj4kFCkQPG4IlWDgrNRIwnO4UKBXDufzQvDMaoSDBgFb886MiQadgNABAokfCwzBA8LCg0Egl8jAggGAA1kBIA1BAYzlyILczULC2UhACH5BAkKAAAALAAAAAAQABAAAAV2ICACAmlAZTmOREEIyUEQjLKKxPHADhEvqxlgcGgkGI1DYSVAIAWMx+lwSKkICJ0QsHi9RgKBwnVTiRQQgwF4I4UFDQQEwi6/3YSGWRRmjhEETAJfIgMFCnAKM0KDV4EEEAQLiF18TAYNXDaSe3x6mjidN1s3IQAh+QQJCgAAACwAAAAAEAAQAAAFeCAgAgLZDGU5jgRECEUiCI+yioSDwDJyLKsXoHFQxBSHAoAAFBhqtMJg8DgQBgfrEsJAEAg4YhZIEiwgKtHiMBgtpg3wbUZXGO7kOb1MUKRFMysCChAoggJCIg0GC2aNe4gqQldfL4l/Ag1AXySJgn5LcoE3QXI3IQAh+QQJCgAAACwAAAAAEAAQAAAFdiAgAgLZNGU5joQhCEjxIssqEo8bC9BRjy9Ag7GILQ4QEoE0gBAEBcOpcBA0DoxSK/e8LRIHn+i1cK0IyKdg0VAoljYIg+GgnRrwVS/8IAkICyosBIQpBAMoKy9dImxPhS+GKkFrkX+TigtLlIyKXUF+NjagNiEAIfkECQoAAAAsAAAAABAAEAAABWwgIAICaRhlOY4EIgjH8R7LKhKHGwsMvb4AAy3WODBIBBKCsYA9TjuhDNDKEVSERezQEL0WrhXucRUQGuik7bFlngzqVW9LMl9XWvLdjFaJtDFqZ1cEZUB0dUgvL3dgP4WJZn4jkomWNpSTIyEAIfkECQoAAAAsAAAAABAAEAAABX4gIAICuSxlOY6CIgiD8RrEKgqGOwxwUrMlAoSwIzAGpJpgoSDAGifDY5kopBYDlEpAQBwevxfBtRIUGi8xwWkDNBCIwmC9Vq0aiQQDQuK+VgQPDXV9hCJjBwcFYU5pLwwHXQcMKSmNLQcIAExlbH8JBwttaX0ABAcNbWVbKyEAIfkECQoAAAAsAAAAABAAEAAABXkgIAICSRBlOY7CIghN8zbEKsKoIjdFzZaEgUBHKChMJtRwcWpAWoWnifm6ESAMhO8lQK0EEAV3rFopIBCEcGwDKAqPh4HUrY4ICHH1dSoTFgcHUiZjBhAJB2AHDykpKAwHAwdzf19KkASIPl9cDgcnDkdtNwiMJCshACH5BAkKAAAALAAAAAAQABAAAAV3ICACAkkQZTmOAiosiyAoxCq+KPxCNVsSMRgBsiClWrLTSWFoIQZHl6pleBh6suxKMIhlvzbAwkBWfFWrBQTxNLq2RG2yhSUkDs2b63AYDAoJXAcFRwADeAkJDX0AQCsEfAQMDAIPBz0rCgcxky0JRWE1AmwpKyEAIfkECQoAAAAsAAAAABAAEAAABXkgIAICKZzkqJ4nQZxLqZKv4NqNLKK2/Q4Ek4lFXChsg5ypJjs1II3gEDUSRInEGYAw6B6zM4JhrDAtEosVkLUtHA7RHaHAGJQEjsODcEg0FBAFVgkQJQ1pAwcDDw8KcFtSInwJAowCCA6RIwqZAgkPNgVpWndjdyohACH5BAkKAAAALAAAAAAQABAAAAV5ICACAimc5KieLEuUKvm2xAKLqDCfC2GaO9eL0LABWTiBYmA06W6kHgvCqEJiAIJiu3gcvgUsscHUERm+kaCxyxa+zRPk0SgJEgfIvbAdIAQLCAYlCj4DBw0IBQsMCjIqBAcPAooCBg9pKgsJLwUFOhCZKyQDA3YqIQAh+QQJCgAAACwAAAAAEAAQAAAFdSAgAgIpnOSonmxbqiThCrJKEHFbo8JxDDOZYFFb+A41E4H4OhkOipXwBElYITDAckFEOBgMQ3arkMkUBdxIUGZpEb7kaQBRlASPg0FQQHAbEEMGDSVEAA1QBhAED1E0NgwFAooCDWljaQIQCE5qMHcNhCkjIQAh+QQJCgAAACwAAAAAEAAQAAAFeSAgAgIpnOSoLgxxvqgKLEcCC65KEAByKK8cSpA4DAiHQ/DkKhGKh4ZCtCyZGo6F6iYYPAqFgYy02xkSaLEMV34tELyRYNEsCQyHlvWkGCzsPgMCEAY7Cg04Uk48LAsDhRA8MVQPEF0GAgqYYwSRlycNcWskCkApIyEAOwAAAAAAAAAAAA%3D%3D); - display: inline-block; -} - -.recline-data-explorer .alert-loader { - position: absolute; - width: 200px; - left: 50%; - margin-left: -100px; - z-index: 10000; - padding: 40px 0px 40px 0px; - margin-top: -10px; - text-align: center; - font-size: 16px; - font-weight: bold; - -webkit-border-radius: 0px; - -moz-border-radius: 0px; - border-radius: 0px; - border-top: none; -} - diff --git a/css/slickgrid.css b/css/slickgrid.css deleted file mode 100644 index f70a38f1..00000000 --- a/css/slickgrid.css +++ /dev/null @@ -1,188 +0,0 @@ -/* -IMPORTANT: -In order to preserve the uniform grid appearance, all cell styles need to have padding, margin and border sizes. -No built-in (selected, editable, highlight, flashing, invalid, loading, :focus) or user-specified CSS -classes should alter those! -*/ - -.recline-slickgrid .slick-header-columns .slick-header-column { - background-color: #e6e6e6; - background-repeat: no-repeat; - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), color-stop(25%, #ffffff), to(#e6e6e6)); - background-image: -webkit-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); - background-image: -moz-linear-gradient(top, #ffffff, #ffffff 25%, #e6e6e6); - background-image: -ms-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); - background-image: -o-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); - background-image: linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0); - text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); - color: #333; - font-weight: bold; - border-right: 1px solid #ccc; - border-top: 1px solid #ccc; - border-bottom: 1px solid #bbb; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.recline-slickgrid .slick-header-column:hover, .slick-header-column-active { -} - -.recline-slickgrid .slick-header-column.ui-state-default { - height: 26px; -} - -.recline-slickgrid .slick-headerrow { - background: #fafafa; -} - -.recline-slickgrid .slick-headerrow-column { - background: #fafafa; - border-bottom: 0; - height: 100%; -} - -.recline-slickgrid .slick-row.ui-state-active { - background: #F5F7D7; -} - -.recline-slickgrid .slick-row { - position: absolute; - background: white; - border: 0px; - line-height: 20px; -} - -.recline-slickgrid .slick-row.selected { - z-index: 10; - background: #DFE8F6; -} - -.recline-slickgrid .slick-cell { - padding-left: 4px; - padding-right: 4px; -} - -.recline-slickgrid .slick-group { - border-bottom: 2px solid silver; -} - -.recline-slickgrid .slick-group-toggle { - width: 9px; - height: 9px; - margin-right: 5px; -} - -.recline-slickgrid .slick-group-toggle.expanded { - background: url(../images/collapse.gif) no-repeat center center; -} - -.recline-slickgrid .slick-group-toggle.collapsed { - background: url(../images/expand.gif) no-repeat center center; -} - -.recline-slickgrid .slick-group-totals { - color: gray; - background: white; -} - -.recline-slickgrid .slick-cell.selected { - background-color: beige; -} - -.recline-slickgrid .slick-cell.active { - border-color: gray; - border-style: solid; -} - -.recline-slickgrid .slick-sortable-placeholder { - background: silver !important; -} - -.recline-slickgrid .slick-row[row$="1"], .slick-row[row$="3"], .slick-row[row$="5"], .slick-row[row$="7"], .slick-row[row$="9"] { - background: #fafafa; -} - -.recline-slickgrid .slick-row.ui-state-active { - background: #F5F7D7; -} - -.recline-slickgrid .slick-row.loading { - opacity: 0.5; - filter: alpha(opacity = 50); -} - -.recline-slickgrid .slick-cell.invalid { - border-color: red; -} - -.recline-slickgrid .slick-row .slick-cell:first-child, -.recline-slickgrid .slick-header { - border-left: 1px solid #ccc; -} - -/* add one pixel extra as added one pixel to left border of header */ -.recline-slickgrid .slick-row .slick-cell { - margin-right: -1px; -} - -/* Slick grid context menu (not part of the recline-slickgrid div) */ -.slick-contextmenu { - border-radius: 5px -} - -.slick-contextmenu li { - clear: both; - height: 24px; - cursor: pointer; -} - -.slick-contextmenu .divider { - cursor: default; -} - -.slick-contextmenu > li:hover { - background-color: #0088cc; -} - -.slick-contextmenu .divider:hover { - background-color: #E5E5E5; -} - -.slick-contextmenu li:hover > label { - color: white; -} - -.slick-contextmenu input { - float: left; - margin-left: 15px; - margin-top: 5px; -} - -.slick-contextmenu label { - float: left; - margin-right: 15px; - margin-left: 5px; - margin-top: 3px; - color: #555; - cursor: pointer; -} - -.recline-slickgrid .recline-row-delete { - font-size: 12px; - padding: 3px; - width: 29px; - height: 18px; - line-height: 13px; -} - -.recline-cell-reorder { - font-size: 12px; - padding: 1px; - width: 31px; - height: 14px; - line-height: 13px; - cursor: move; - background: url("../images/drag-handle.png") no-repeat center center; -} diff --git a/css/timeline.css b/css/timeline.css deleted file mode 100644 index 0036d16f..00000000 --- a/css/timeline.css +++ /dev/null @@ -1,3 +0,0 @@ -.recline-timeline { - position: relative; -} diff --git a/demo/index.html b/demo/index.html deleted file mode 100644 index a5dec198..00000000 --- a/demo/index.html +++ /dev/null @@ -1,8 +0,0 @@ - diff --git a/demos/data/sample.csv b/demos/data/sample.csv deleted file mode 100644 index f3049990..00000000 --- a/demos/data/sample.csv +++ /dev/null @@ -1,4 +0,0 @@ -Date,Name,Price_Paid,Pricing_Mechanism,Description,Transaction_Type,FDIC_Number,OTS_Number,Type_of_Institution,Total_Assets_Q4_2008,Regulator,City,State,Stock_Symbol,Program,Warrant_Strike_Price,Warrant_Received,Stock_Price_as_of_close_20090604,In_Out_of_Money_as_of_close_20090604,Subsidy_Rate_Estimate_percentage,Subsidy_Rate_Estimate_Date,Subsidy_Rate_Estimate_Source -2009-05-29,AMERICAN PREMIER BANCORP,1800000.00,Par,Preferred Stock w/ Exercised Warrants,Purchase,3175600,,holding company,81126000.00,,Arcadia,CA,,CPP,,,,,,, -2009-05-29,"C.S. BANCSHARES, INC.",24990000.00,Par,Preferred Stock w/ Exercised Warrants,Purchase,1138786,,holding company,197348000.00,,Chillicothe,MO,,CPP,,,,,,, -2009-05-29,CB HOLDING CORP.,4114000.00,Par,Preferred Stock w/ Exercised Warrants,Purchase,3184901,,holding company,164478000.00,,Aledo,IL,,CPP,,,,,,, diff --git a/demos/images/calendar.gif b/demos/images/calendar.gif deleted file mode 100755 index 90fd2e17..00000000 Binary files a/demos/images/calendar.gif and /dev/null differ diff --git a/demos/index.html b/demos/index.html deleted file mode 100644 index 0857a876..00000000 --- a/demos/index.html +++ /dev/null @@ -1,94 +0,0 @@ ---- -layout: container -title: Demos -root: ../ ---- - - - -
-
-
-

Multiview Demo

-

See the integrated multiview in action on a - sample dataset - the multiview incorporates most the main views - (grid, graph, map etc) into one integrated view with search, filtering - and field summaries.

-
-
-
- -
-
- -
- -
-
-
-

Data Explorer

-

The Data Explorer is a full - application for exploring and working with data in your browser. It - utilizes almost every feature of ReclineJS and is the app that ReclineJS - was originally developed to build.

-
-
-
- -
-
- -
- -
-
-
-

Timeliner

-

Create timelines quickly and easily using the Timeliner App created with ReclineJS (source code)

-
-
-
- -
-
- -
- -
-
-
-

Crime Maps

-

See Recline's geo filter and map view capabilities put to good use - exploring crime's in San Francisco. This example shows you the Recline - Data Explorer pre-configured to show thefts near 9th and Mission.

-
-
-
- -
-
- -
- -
-
-
-

Search Demo

-

See how easy it is to build a responsive AJAX-based search interface - to a search backend. It includes full-text search, faceting and ability - to easily customize the display of results.

-
-
-
- -
-
- -
- diff --git a/demos/multiview/app.js b/demos/multiview/app.js deleted file mode 100755 index e905ae92..00000000 --- a/demos/multiview/app.js +++ /dev/null @@ -1,117 +0,0 @@ -jQuery(function($) { - window.multiView = null; - window.explorerDiv = $('.data-explorer-here'); - - // create the demo dataset - var dataset = createDemoDataset(); - // now create the multiview - // this is rather more elaborate than the minimum as we configure the - // MultiView in various ways (see function below) - window.multiview = createMultiView(dataset); - - // last, we'll demonstrate binding to changes in the dataset - // this will print out a summary of each change onto the page in the - // changelog section - dataset.records.bind('all', function(name, obj) { - var $info = $('
'); - $info.html(name + ': ' + JSON.stringify(obj.toJSON())); - $('.changelog').append($info); - $('.changelog').show(); - }); -}); - -// create standard demo dataset -function createDemoDataset() { - var dataset = new recline.Model.Dataset({ - records: [ - {id: 0, date: '2011-01-01', x: 1, y: 2, z: 3, country: 'DE', title: 'first', lat:52.56, lon:13.40}, - {id: 1, date: '2011-02-02', x: 2, y: 4, z: 24, country: 'UK', title: 'second', lat:54.97, lon:-1.60}, - {id: 2, date: '2011-03-03', x: 3, y: 6, z: 9, country: 'US', title: 'third', lat:40.00, lon:-75.5}, - {id: 3, date: '2011-04-04', x: 4, y: 8, z: 6, country: 'UK', title: 'fourth', lat:57.27, lon:-6.20}, - {id: 4, date: '2011-05-04', x: 5, y: 10, z: 15, country: 'UK', title: 'fifth', lat:51.58, lon:0}, - {id: 5, date: '2011-06-02', x: 6, y: 12, z: 18, country: 'DE', title: 'sixth', lat:51.04, lon:7.9} - ], - // let's be really explicit about fields - // Plus take opportunity to set date to be a date field and set some labels - fields: [ - {id: 'id'}, - {id: 'date', type: 'date'}, - {id: 'x', type: 'number'}, - {id: 'y', type: 'number'}, - {id: 'z', type: 'number'}, - {id: 'country', 'label': 'Country'}, - {id: 'title', 'label': 'Title'}, - {id: 'lat'}, - {id: 'lon'} - ] - }); - return dataset; -} - -// make MultivView -// -// creation / initialization in a function so we can call it again and again -var createMultiView = function(dataset, state) { - // remove existing multiview if present - var reload = false; - if (window.multiView) { - window.multiView.remove(); - window.multiView = null; - reload = true; - } - - var $el = $('
'); - $el.appendTo(window.explorerDiv); - - // customize the subviews for the MultiView - var fmt = I18nMessages('recline', recline.View.translations); - var views = [ - { - id: 'grid', - label: fmt.t('Grid'), - view: new recline.View.SlickGrid({ - model: dataset, - state: { - gridOptions: { - editable: true, - // Enable support for row add - enabledAddRow: true, - // Enable support for row delete - enabledDelRow: true, - // Enable support for row ReOrder - enableReOrderRow:true, - autoEdit: false, - enableCellNavigation: true - }, - columnsEditor: [ - { column: 'date', editor: Slick.Editors.Date }, - { column: 'title', editor: Slick.Editors.Text } - ] - } - }) - }, - { - id: 'graph', - label: fmt.t('Graph'), - view: new recline.View.Graph({ - model: dataset - }) - }, - { - id: 'map', - label: fmt.t('Map'), - view: new recline.View.Map({ - model: dataset - }) - } - ]; - - var multiView = new recline.View.MultiView({ - model: dataset, - el: $el, - state: state, - views: views - }); - return multiView; -} - diff --git a/demos/multiview/index.html b/demos/multiview/index.html deleted file mode 100644 index 4028fa3a..00000000 --- a/demos/multiview/index.html +++ /dev/null @@ -1,33 +0,0 @@ ---- -layout: container -title: Demos - Multiview -recline-deps: true -root: ../../ ---- - - - -
-

Changes

-
- -
- -
- - - diff --git a/demos/search/demo.search.app.js b/demos/search/demo.search.app.js deleted file mode 100644 index 9621ffbf..00000000 --- a/demos/search/demo.search.app.js +++ /dev/null @@ -1,270 +0,0 @@ -// (c) Open Knowledge Foundation 2012. Dedicated to the public domain. Please -// use and reuse freely - you don't even need to credit (though a link back to -// ReclineJS.com is always appreciated)! - - -// ## Our main loop - on document ready -jQuery(function($) { - var $el = $('.search-here'); - - // ### Overview - // - // We have a slightly more complex setup than is needed to allow for using - // this demo with different backends - // - // There are 2 alternatives: more complex and a simpler one - // - // If you just want to see how this work skip to the simple case ... - - // ### 1. More complex - use data from a backend configured in query string - - // Check for config from url query string - var config = recline.View.parseQueryString(decodeURIComponent(window.location.search)); - if (config.backend) { - // If we had it hand off to our more complex example setup - setupMoreComplexExample(config); - return; - } - - // ### 2. The simple example case - // - // We will just set up from some example local data (at the bottom of thile file) - - // #### Create our Recline Dataset from sample local data - var dataset = new recline.Model.Dataset({ - records: sampleData - }); - - // #### Custom template - // - // Create a custom template for rendering the records - var template = ' \ -
\ -

\ - {{title}} by {{Author}} \ -

\ -

{{description}}

\ -

${{price}}

\ -
\ - '; - - // #### Set up the search View (using custom template) - var searchView = new SearchView({ - el: $el, - model: dataset, - template: template - }); - searchView.render(); - - // #### Optional - we configure the initial query a bit and set up facets - dataset.queryState.set({ - size: 10 - }, - {silent: true} - ); - dataset.queryState.addFacet('Author'); - - // #### Finally - now do the first query - // - // After this point the Search View will take over handling queries! - dataset.query(); -}); - - -// ## Simple Search View -// -// This is a simple bespoke Backbone view for the Search. It Pulls together -// various Recline UI components and the central Dataset and Query (state) -// object -// -// It also provides simple support for customization e.g. of template for list of results -// -// var view = new SearchView({ -// el: $('some-element'), -// model: dataset -// // EITHER a mustache template (passed a JSON version of recline.Model.Record -// // OR a function which receives a record in JSON form and returns html -// template: mustache-template-or-function -// }); -var SearchView = Backbone.View.extend({ - initialize: function(options) { - this.el = $(this.el); - _.bindAll(this, 'render'); - this.recordTemplate = options.template; - // Every time we do a search the recline.Dataset.records Backbone - // collection will get reset. We want to re-render each time! - this.model.records.bind('reset', this.render); - this.templateResults = options.template; - }, - - // overall template for this view - template: ' \ -
\ -
\ -
\ -

records found

\ -
\ - \ -
\ - {{{results}}} \ -
\ -
\ -
\ - ', - - // render the view - render: function() { - var results = ''; - if (_.isFunction(this.templateResults)) { - var results = _.map(this.model.records.toJSON(), this.templateResults).join('\n'); - } else { - // templateResults is just for one result ... - var tmpl = '{{#records}}' + this.templateResults + '{{/records}}'; - var results = Mustache.render(tmpl, { - records: this.model.records.toJSON() - }); - } - var html = Mustache.render(this.template, { - results: results - }); - this.el.html(html); - - // Set the total records found info - this.el.find('.total span').text(this.model.recordCount); - - // ### Now setup all the extra mini-widgets - // - // Facets, Pager, QueryEditor etc - - var view = new recline.View.FacetViewer({ - model: this.model - }); - view.render(); - this.el.find('.sidebar').append(view.el); - - var pager = new recline.View.Pager({ - model: this.model - }); - this.el.find('.pager-here').append(pager.el); - - var queryEditor = new recline.View.QueryEditor({ - model: this.model.queryState - }); - this.el.find('.query-here').append(queryEditor.el); - } -}); - -// -------------------------------------------------------- -// ## Custom code very specific to this demo - -// e.g. to provide custom templates for the google doc and openspending examples - - -// ### Handle case where we get data from a specific backend -// -// Includes providing custom templates -function setupMoreComplexExample(config) { - var $el = $('.search-here'); - var dataset = new recline.Model.Dataset(config); - // async as may be fetching remote - dataset.fetch().done(function() { - var template = templates[dataset.get('url')] || templates['generic']; - var searchView = new SearchView({ - el: $el, - model: dataset, - template: template - }); - searchView.render(); - - dataset.queryState.set({ - size: 5 - }, - {silent: true} - ); - if (dataset.get('url') in templates) { - // for gdocs example - dataset.queryState.addFacet('cause'); - } - dataset.query(); - }); -}; - -var templates = { - // generic template function - 'generic': function(record) { - var template = '
\ -
    \ - {{#data}} \ -
  • {{key}}: {{value}}
  • \ - {{/data}} \ -
\ -
\ - '; - var data = _.map(_.keys(record), function(key) { - return { key: key, value: record[key] }; - }); - return Mustache.render(template, { - data: data - }); - }, - 'https://docs.google.com/spreadsheet/ccc?key=0Aon3JiuouxLUdExXSTl2Y01xZEszOTBFZjVzcGtzVVE': function(record) { - var template = '
\ -

\ - {{record.incidentsite}} – {{record.datereported}} – {{record.estimatedspillvolumebbl}} barrels \ -

\ -
    \ - {{#data}} \ -
  • {{key}}: {{value}}
  • \ - {{/data}} \ -
\ -
\ - '; - var data = []; - _.each(_.keys(record), function(key) { - data.push({ key: key, value: record[key] }); - }); - return Mustache.render(template, { - record: record, - data: data - }); - } -} - -var sampleData = [ - { - title: 'War and Peace', - description: 'The epic tale of love, war and history', - Author: 'Tolstoy', - price: 7.99 - }, - { - title: 'Anna Karenina', - description: 'How things go wrong in love and ultimately lead to suicide. This is why you should not have affairs, girls!', - Author: 'Tolstoy', - price: 8.50 - }, - { - title: "Fathers and Sons", - description: "Another 19th century Russian novel", - Author: "Turgenev", - price: 11 - } -]; - -var formatAmount = function (num) { - var billion = 1000000000; - var million = 1000000; - var thousand = 1000; - var numabs = Math.abs(num); - if (numabs > billion) { - return (num / billion).toFixed(0) + 'bn'; - } - if (numabs > (million / 2)) { - return (num / million).toFixed(0) + 'm'; - } - if (numabs > thousand) { - return (num / thousand).toFixed(0) + 'k'; - } else { - return num.toFixed(0); - } -}; diff --git a/demos/search/index.html b/demos/search/index.html deleted file mode 100644 index b9cadf5f..00000000 --- a/demos/search/index.html +++ /dev/null @@ -1,84 +0,0 @@ ---- -layout: container -title: Demos - Search -recline-deps: true -root: ../../ ---- - - - - - -
-

This demo shows how Recline can be used to build a search app. It includes faceting as well as search. You can find the source javascript here (plus prettified version of source for readability) – please feel free to reuse!

-

The default setup uses local example data but you can also connect directly to any other backend supported by Recline, for example SOLR, ElasticSearch or even a google docs spreadsheet. Here's an example running against a GDocs spreadsheet (Oil spills in the Niger Delta).

-
- -
- -
- -
- - - diff --git a/dist/recline.css b/dist/recline.css deleted file mode 100644 index f1e976fd..00000000 --- a/dist/recline.css +++ /dev/null @@ -1,796 +0,0 @@ -.recline-flot .graph { - height: 500px; - overflow: hidden; -} - -.recline-flot .legend table { - width: auto; - margin-bottom: 0; -} - -.recline-flot .legend td { - padding: 5px; - line-height: 13px; -} - -.recline-flot .graph .alert { - width: 450px; -} - -#recline-flot-tooltip { - position: absolute; - background-color: #FEE; - color: #000000; - opacity: 0.8; - border: 1px solid #fdd; -} -/********************************************************** - * (Data) Grid - *********************************************************/ - -table.recline-grid { - table-layout: fixed; - width: 100%; -} - -.recline-grid .btn-group .dropdown-toggle { - padding: 1px 3px; - line-height: auto; -} - -.recline-grid td, .recline-grid th { - border-left: 1px solid #ccc; - padding: 3px 4px; - text-align: left; - word-wrap: break-word; - white-space: normal; -} - -.recline-grid tbody tr { - vertical-align: top; - border-bottom: solid 1px #ccc; -} - -.recline-grid tbody tr:last-child { - border-bottom: 1px solid #ccc; -} - -.recline-grid tbody td:last-child { - border-right: 1px solid #ccc; -} - -/* direct borrowing from twitter buttons */ -.recline-grid th { - background-color: #e6e6e6; - background-repeat: no-repeat; - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), color-stop(25%, #ffffff), to(#e6e6e6)); - background-image: -webkit-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); - background-image: -moz-linear-gradient(top, #ffffff, #ffffff 25%, #e6e6e6); - background-image: -ms-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); - background-image: -o-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); - background-image: linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0); - text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); - color: #333; - border: 1px solid #ccc; - border-bottom-color: #bbb; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - -webkit-transition: 0.1s linear all; - -moz-transition: 0.1s linear all; - -ms-transition: 0.1s linear all; - -o-transition: 0.1s linear all; - transition: 0.1s linear all; -} - -/********************************************************** - * Fixed Header - http://www.imaputz.com/cssStuff/bigFourVersion.html - *********************************************************/ - -div.table-container { - overflow: auto; -} - -/* Reset overflow value to hidden for all non-IE browsers. */ -html>body div.table-container { - overflow: hidden; -} - -thead.fixed-header tr { - overflow-x: hidden; -} - -/* set table header to a fixed position. WinIE 6.x only */ -/* In WinIE 6.x, any element with a position property set to relative and is a child of */ -/* an element that has an overflow property set, the relative value translates into fixed. */ -/* Ex: parent element DIV with a class of table-container has an overflow property set to auto */ -thead.fixed-header tr { - position: relative -} - -/* set THEAD element to have block level attributes. All other non-IE browsers */ -/* this enables overflow to work on TBODY element. All other non-IE, non-Mozilla browsers */ -html>body thead.fixed-header tr { - display: block -} - -/* define the table content to be scrollable */ -/* set TBODY element to have block level attributes. All other non-IE browsers */ -/* this enables overflow to work on TBODY element. All other non-IE, non-Mozilla browsers */ -/* induced side effect is that child TDs no longer accept width: auto */ -tbody.scroll-content { - display: block; - max-height: 500px; - overflow: auto; -} - -/********************************************************** - * Data Table Menus - *********************************************************/ - -.column-header-menu, a.root-header-menu { - float: right; -} - -div.data-table-cell-content { - line-height: 1.2; - color: #222; - position: relative; -} - -div.data-table-cell-content-numeric { - text-align: right; -} - -a.data-table-cell-edit { - position: absolute; - top: 0; - right: 0; - display: block; - width: 25px; - height: 16px; - text-decoration: none; - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAQCAYAAABUWyyMAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OTNEODNFMjVEMDA5MTFERjk5NzhEQzZDRDUwRkEzMUEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OTNEODNFMjZEMDA5MTFERjk5NzhEQzZDRDUwRkEzMUEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo5M0Q4M0UyM0QwMDkxMURGOTk3OERDNkNENTBGQTMxQSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo5M0Q4M0UyNEQwMDkxMURGOTk3OERDNkNENTBGQTMxQSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PpApVmcAAAKVSURBVHjaYvz//z/DcAAsIGLaxjf2QKoLiM2oaPYpIC7L8hc5COI4F9+hmR17e1UOskAFVgCxBJUDyQxqriQ97GCCCpBkATCUGQxVOTHY7KyMDFoKHMhKJXCwCQJgKDNEOAlisHk5mRh8LPgw7GChNEg2HfvI8OnbPzDbwYAH6BkmhmsPflA12EtnPmN4/vY3mF0YKgb2zJYTn5CV/EfxiKgAC4O7CS8DHzczw5PXvxl2nv7E8PP3fxTx87e/o1hipMrFcOfZT6BmVgYZUTawmKUWN8Pxa1+xOkpNhp2hLk6CQVKYleHcrW8MTYteMHz+/g9FfMW+9yh6Ih0FGA5e/MLAAIx5IzUusFiqtzDD7K1vYUpcmZA1+FnxM7z++AccyqBkAgphmMPY2ZiAHvsMFkcGMkAPgMQ+ff3L8PPXPzD++O0vztDtypBmuPXkJziUebiYwSEMcxiID/IYiEYJLKDjQWLP3v1h+AI0G4RhMQQF5UzoDvr5CxIDoJhQlmKHy527/Y3hLjDkD4BCBgsAxSAoiYEwrqQFchAoWcBiAOQge30euBwoJkAh37/6FVb9oBgEeQCE0ZKWC8488vj1LzCmJTgLTKZn0ZIquYAJOURBsQDKB68//GHgYEWkOlBMgPICKIYcoCGIC4BiFRSDuEIUFBtSQizg5AWKHRgAxQSoZALFECy54QKgZAbLKxgegZVAfFxM4LyiLMUGTPeQ0ujAhS9gz7mb8sI9iisWQclSG7UIRgFlM56CM3R3uhTY0bC0DkpOtx//AGd4mEdxxSIoWaIVwQyMoCYKsGanWTsFWM8wQmt2mtkBqmdgMfKCRna8wMGmth2MMI/EAfEdKltwB2ouAz3sYBwurV8mhmECAAIMAEe3EkMWh/DvAAAAAElFTkSuQmCC); - background-repeat: no-repeat; - visibility: hidden; -} - -a.data-table-cell-edit:hover { - background-position: -25px 0px; -} - -.recline-grid td:hover .data-table-cell-edit { - visibility: visible; -} - -div.data-table-cell-content-numeric > a.data-table-cell-edit { - left: 0px; - right: auto; -} - -.data-table-value-nonstring { - color: #282; -} - -.data-table-error { - color: red; -} - -.data-table-cell-editor-editor { - overflow: hidden; - display: block; - width: 98%; - height: 3em; - font-family: monospace; - margin: 3px 0; -} - -.data-table-cell-copypaste-editor { - overflow: hidden; - display: block; - width: 98%; - height: 10em; - font-family: monospace; - margin: 3px 0; -} - -.data-table-cell-editor-action { - float: left; - vertical-align: bottom; - text-align: center; -} - -.data-table-cell-editor-key { - font-size: 0.8em; - color: #999; -} - -/********************************************************** - * Read-only mode - *********************************************************/ - -.recline-read-only .recline-grid .write-op, -.recline-read-only .recline-grid a.data-table-cell-edit -{ - display: none; -} - -.recline-read-only a.row-header-menu { - display: none; -} - -/************************* - * WCAG 2.0 - *************************/ - -.wcag_hide { - position:absolute; - top:0; - left:-10000px; - width:1px; - height:1px; -} - -.wcag_hide2 { - clip: rect(1px, 1px, 1px, 1px); - display: block; - position: absolute; -} - -.wcag_show_on_focus { - font-size: 0 !important; -} - -.wcag_show_on_focus:focus { - font-size: 1em !important; -} -.recline-map .map { - height: 500px; -} - -/********************************************************** - * Editor - *********************************************************/ - -.recline-map .editor { - float: right; - width: 200px; - padding-left: 0px; - margin-left: 10px; -} - -.recline-map .editor form { - padding-left: 4px; -} - -.recline-map .editor select { - width: 100%; -} - -.recline-map .editor .editor-options { - margin-top: 10px; - border-top: 1px solid gray; - padding: 5px 0; -} -.recline-data-explorer .data-view-container { - display: block; -} - -.recline-data-explorer .data-view-sidebar { - float: right; - margin-left: 8px; - width: 220px; -} - -.recline-data-explorer .header .navigation { - margin-bottom: 8px; -} - -.recline-data-explorer .header .navigation, -.recline-data-explorer .header .pagination, -.recline-data-explorer .header .pagination form -{ - display: inline; -} - -.recline-data-explorer .header .navigation { - float: left; -} - -.recline-data-explorer .header .menu-right { - float: right; - margin-left: 5px; - padding-left: 5px; -} - -.recline-results-info { - line-height: 35px; - margin-left: 20px; - float: left; -} - -.recline-data-explorer .data-view-sidebar > div { - margin-top: 5px; - margin-bottom: 10px; -} - -.recline-data-explorer .radio, -.recline-data-explorer .checkbox { - padding-left: 20px; -} - -.recline-data-explorer .editor-update-map { - margin: 30px 0px 20px 0px; -} - -.recline-data-explorer label { - font-weight: normal; -} - -/********************************************************** - * Query Editor - *********************************************************/ - -.recline-query-editor { - float: right; - height: 35px; - padding-right: 5px; - margin-right: 5px; - border-right: solid 2px #ddd; -} - -.header .input-prepend { - margin-bottom: auto; -} - -.header .add-on { - float: left; -} - -/* needed for Chrome but not FF */ -.header .add-on { - margin-left: -27px; -} - -/* needed for FF but not chrome */ -.header .input-prepend { - vertical-align: top; -} - -.recline-query-editor form button { - vertical-align: top; -} - -/* label for screen reader */ -.recline-query-editor .form-inline label { - position: absolute; - top:0; - left:-9999px -} - -/********************************************************** - * Pager - *********************************************************/ - -.recline-pager { - float: left; - margin: auto; - display: block; - margin-left: 20px; -} - -.recline-pager .pagination li { - display: inline-block; -} - -.recline-pager .pagination label { - display:none; -} - -.recline-pager .pagination input { - width: 40px; - height: 25px; - padding: 2px 4px; - margin: 0; - margin-top: -2px; - - border: 1px solid #cccccc; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - transition: border linear 0.2s, box-shadow linear 0.2s; - border-radius: 4px; - - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; - -webkit-border-radius: 4px; - - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -moz-transition: border linear 0.2s, box-shadow linear 0.2s; - -moz-border-radius: 4px; - - -o-transition: border linear 0.2s, box-shadow linear 0.2s; -} - -.recline-pager .pagination a { - float: none; - margin-left: -5px; - color: #555; -} - -.recline-pager .pagination .page-range { - height: 34px; - padding: 5px 8px; - margin-left: -5px; - border: 1px solid #ddd; -} - -.recline-pager .pagination .page-range a { - padding: 0px 12px; - border: none; -} - -.recline-pager .pagination .page-range a:hover { - background-color: #ffffff; -} - -.recline-pager .pagination > li:first-child > a { - border-bottom-left-radius: 4px; - border-top-left-radius: 4px; - border-bottom-right-radius: 0px; - border-top-right-radius: 0px; - height: 34px; -} - -.recline-pager .pagination > li:last-child > a { - border-bottom-right-radius: 4px; - border-top-right-radius: 4px; - border-bottom-left-radius: 0px; - border-top-left-radius: 0px; - height: 34px; -} - -/********************************************************** - * Filter Editor - *********************************************************/ - -.recline-filter-editor { - padding: 8px; - display: none; -} - -.recline-filter-editor .filters { - margin: 20px 0px; -} - -.recline-filter-editor h3 { - margin-top: 4px; -} - -.recline-filter-editor .filter { - margin-top: 20px; -} - -.recline-filter-editor .filter .form-group { - margin-bottom: 0px; -} - -.recline-filter-editor .filter input, -.recline-filter-editor .filter label { - margin: 0px; -} - -.recline-filter-editor .js-edit button { - margin: 25px 0px 0px 0px; -} - -.recline-filter-editor .filter-term a { - font-size: 18px; -} - -.recline-filter-editor input, -.recline-filter-editor select -{ - width: 175px; -} - -.recline-filter-editor input { - margin-top: 0.5em; - margin-bottom: 10px; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - border: 1px solid #cccccc; -} - -.recline-filter-editor label { - font-weight: normal; - display: block; -} - -.recline-filter-editor legend { - margin-bottom: 5px; -} - -.recline-filter-editor .add-filter { - margin-top: 1em; - margin-bottom: 2em; -} - -.recline-filter-editor .update-filter { - margin-top: 1em; -} - -/********************************************************** - * Fields Widget - *********************************************************/ - -.recline-fields-view { - display: none; -} - -.recline-fields-view .fields-list { - padding: 0; -} - -.recline-fields-view .panel { - background-color: #f5f5f5; - border: 1px solid #e5e5e5; -} - -.recline-fields-view .panel-group h3 { - padding-left: 10px; -} - -.recline-fields-view .fields-list .panel-heading { - padding: 2px 5px; - margin: 1px 0px 1px 5px; -} - -.recline-fields-view .panel a, -.recline-fields-view .panel h4 { - display: inline; -} - -.recline-fields-view .panel a { - padding: 0; -} - -.recline-fields-view .panel h4 { - word-wrap: break-word -} - -.recline-fields-view .clear { - clear: both; -} - -.recline-fields-view .facet-items { - list-style-type: none; - margin-left: 0; -} - -.recline-fields-view .facet-item .term { - font-weight: bold; -} - -.recline-fields-view .facet-item .count { -} - -/********************************************************** - * Notifications - *********************************************************/ - -.recline-data-explorer .notification-loader { - width: 18px; - margin-left: 5px; - background-image: url(data:image/gif;base64,R0lGODlhEAAQAPQAAP///wAAAPDw8IqKiuDg4EZGRnp6egAAAFhYWCQkJKysrL6+vhQUFJycnAQEBDY2NmhoaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAAFdyAgAgIJIeWoAkRCCMdBkKtIHIngyMKsErPBYbADpkSCwhDmQCBethRB6Vj4kFCkQPG4IlWDgrNRIwnO4UKBXDufzQvDMaoSDBgFb886MiQadgNABAokfCwzBA8LCg0Egl8jAggGAA1kBIA1BAYzlyILczULC2UhACH5BAkKAAAALAAAAAAQABAAAAV2ICACAmlAZTmOREEIyUEQjLKKxPHADhEvqxlgcGgkGI1DYSVAIAWMx+lwSKkICJ0QsHi9RgKBwnVTiRQQgwF4I4UFDQQEwi6/3YSGWRRmjhEETAJfIgMFCnAKM0KDV4EEEAQLiF18TAYNXDaSe3x6mjidN1s3IQAh+QQJCgAAACwAAAAAEAAQAAAFeCAgAgLZDGU5jgRECEUiCI+yioSDwDJyLKsXoHFQxBSHAoAAFBhqtMJg8DgQBgfrEsJAEAg4YhZIEiwgKtHiMBgtpg3wbUZXGO7kOb1MUKRFMysCChAoggJCIg0GC2aNe4gqQldfL4l/Ag1AXySJgn5LcoE3QXI3IQAh+QQJCgAAACwAAAAAEAAQAAAFdiAgAgLZNGU5joQhCEjxIssqEo8bC9BRjy9Ag7GILQ4QEoE0gBAEBcOpcBA0DoxSK/e8LRIHn+i1cK0IyKdg0VAoljYIg+GgnRrwVS/8IAkICyosBIQpBAMoKy9dImxPhS+GKkFrkX+TigtLlIyKXUF+NjagNiEAIfkECQoAAAAsAAAAABAAEAAABWwgIAICaRhlOY4EIgjH8R7LKhKHGwsMvb4AAy3WODBIBBKCsYA9TjuhDNDKEVSERezQEL0WrhXucRUQGuik7bFlngzqVW9LMl9XWvLdjFaJtDFqZ1cEZUB0dUgvL3dgP4WJZn4jkomWNpSTIyEAIfkECQoAAAAsAAAAABAAEAAABX4gIAICuSxlOY6CIgiD8RrEKgqGOwxwUrMlAoSwIzAGpJpgoSDAGifDY5kopBYDlEpAQBwevxfBtRIUGi8xwWkDNBCIwmC9Vq0aiQQDQuK+VgQPDXV9hCJjBwcFYU5pLwwHXQcMKSmNLQcIAExlbH8JBwttaX0ABAcNbWVbKyEAIfkECQoAAAAsAAAAABAAEAAABXkgIAICSRBlOY7CIghN8zbEKsKoIjdFzZaEgUBHKChMJtRwcWpAWoWnifm6ESAMhO8lQK0EEAV3rFopIBCEcGwDKAqPh4HUrY4ICHH1dSoTFgcHUiZjBhAJB2AHDykpKAwHAwdzf19KkASIPl9cDgcnDkdtNwiMJCshACH5BAkKAAAALAAAAAAQABAAAAV3ICACAkkQZTmOAiosiyAoxCq+KPxCNVsSMRgBsiClWrLTSWFoIQZHl6pleBh6suxKMIhlvzbAwkBWfFWrBQTxNLq2RG2yhSUkDs2b63AYDAoJXAcFRwADeAkJDX0AQCsEfAQMDAIPBz0rCgcxky0JRWE1AmwpKyEAIfkECQoAAAAsAAAAABAAEAAABXkgIAICKZzkqJ4nQZxLqZKv4NqNLKK2/Q4Ek4lFXChsg5ypJjs1II3gEDUSRInEGYAw6B6zM4JhrDAtEosVkLUtHA7RHaHAGJQEjsODcEg0FBAFVgkQJQ1pAwcDDw8KcFtSInwJAowCCA6RIwqZAgkPNgVpWndjdyohACH5BAkKAAAALAAAAAAQABAAAAV5ICACAimc5KieLEuUKvm2xAKLqDCfC2GaO9eL0LABWTiBYmA06W6kHgvCqEJiAIJiu3gcvgUsscHUERm+kaCxyxa+zRPk0SgJEgfIvbAdIAQLCAYlCj4DBw0IBQsMCjIqBAcPAooCBg9pKgsJLwUFOhCZKyQDA3YqIQAh+QQJCgAAACwAAAAAEAAQAAAFdSAgAgIpnOSonmxbqiThCrJKEHFbo8JxDDOZYFFb+A41E4H4OhkOipXwBElYITDAckFEOBgMQ3arkMkUBdxIUGZpEb7kaQBRlASPg0FQQHAbEEMGDSVEAA1QBhAED1E0NgwFAooCDWljaQIQCE5qMHcNhCkjIQAh+QQJCgAAACwAAAAAEAAQAAAFeSAgAgIpnOSoLgxxvqgKLEcCC65KEAByKK8cSpA4DAiHQ/DkKhGKh4ZCtCyZGo6F6iYYPAqFgYy02xkSaLEMV34tELyRYNEsCQyHlvWkGCzsPgMCEAY7Cg04Uk48LAsDhRA8MVQPEF0GAgqYYwSRlycNcWskCkApIyEAOwAAAAAAAAAAAA%3D%3D); - display: inline-block; -} - -.recline-data-explorer .alert-loader { - position: absolute; - width: 200px; - left: 50%; - margin-left: -100px; - z-index: 10000; - padding: 40px 0px 40px 0px; - margin-top: -10px; - text-align: center; - font-size: 16px; - font-weight: bold; - -webkit-border-radius: 0px; - -moz-border-radius: 0px; - border-radius: 0px; - border-top: none; -} - -/* -IMPORTANT: -In order to preserve the uniform grid appearance, all cell styles need to have padding, margin and border sizes. -No built-in (selected, editable, highlight, flashing, invalid, loading, :focus) or user-specified CSS -classes should alter those! -*/ - -.recline-slickgrid .slick-header-columns .slick-header-column { - background-color: #e6e6e6; - background-repeat: no-repeat; - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), color-stop(25%, #ffffff), to(#e6e6e6)); - background-image: -webkit-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); - background-image: -moz-linear-gradient(top, #ffffff, #ffffff 25%, #e6e6e6); - background-image: -ms-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); - background-image: -o-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); - background-image: linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0); - text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); - color: #333; - font-weight: bold; - border-right: 1px solid #ccc; - border-top: 1px solid #ccc; - border-bottom: 1px solid #bbb; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.recline-slickgrid .slick-header-column:hover, .slick-header-column-active { -} - -.recline-slickgrid .slick-header-column.ui-state-default { - height: 26px; -} - -.recline-slickgrid .slick-headerrow { - background: #fafafa; -} - -.recline-slickgrid .slick-headerrow-column { - background: #fafafa; - border-bottom: 0; - height: 100%; -} - -.recline-slickgrid .slick-row.ui-state-active { - background: #F5F7D7; -} - -.recline-slickgrid .slick-row { - position: absolute; - background: white; - border: 0px; - line-height: 20px; -} - -.recline-slickgrid .slick-row.selected { - z-index: 10; - background: #DFE8F6; -} - -.recline-slickgrid .slick-cell { - padding-left: 4px; - padding-right: 4px; -} - -.recline-slickgrid .slick-group { - border-bottom: 2px solid silver; -} - -.recline-slickgrid .slick-group-toggle { - width: 9px; - height: 9px; - margin-right: 5px; -} - -.recline-slickgrid .slick-group-toggle.expanded { - background: url(../images/collapse.gif) no-repeat center center; -} - -.recline-slickgrid .slick-group-toggle.collapsed { - background: url(../images/expand.gif) no-repeat center center; -} - -.recline-slickgrid .slick-group-totals { - color: gray; - background: white; -} - -.recline-slickgrid .slick-cell.selected { - background-color: beige; -} - -.recline-slickgrid .slick-cell.active { - border-color: gray; - border-style: solid; -} - -.recline-slickgrid .slick-sortable-placeholder { - background: silver !important; -} - -.recline-slickgrid .slick-row[row$="1"], .slick-row[row$="3"], .slick-row[row$="5"], .slick-row[row$="7"], .slick-row[row$="9"] { - background: #fafafa; -} - -.recline-slickgrid .slick-row.ui-state-active { - background: #F5F7D7; -} - -.recline-slickgrid .slick-row.loading { - opacity: 0.5; - filter: alpha(opacity = 50); -} - -.recline-slickgrid .slick-cell.invalid { - border-color: red; -} - -.recline-slickgrid .slick-row .slick-cell:first-child, -.recline-slickgrid .slick-header { - border-left: 1px solid #ccc; -} - -/* add one pixel extra as added one pixel to left border of header */ -.recline-slickgrid .slick-row .slick-cell { - margin-right: -1px; -} - -/* Slick grid context menu (not part of the recline-slickgrid div) */ -.slick-contextmenu { - border-radius: 5px -} - -.slick-contextmenu li { - clear: both; - height: 24px; - cursor: pointer; -} - -.slick-contextmenu .divider { - cursor: default; -} - -.slick-contextmenu > li:hover { - background-color: #0088cc; -} - -.slick-contextmenu .divider:hover { - background-color: #E5E5E5; -} - -.slick-contextmenu li:hover > label { - color: white; -} - -.slick-contextmenu input { - float: left; - margin-left: 15px; - margin-top: 5px; -} - -.slick-contextmenu label { - float: left; - margin-right: 15px; - margin-left: 5px; - margin-top: 3px; - color: #555; - cursor: pointer; -} - -.recline-slickgrid .recline-row-delete { - font-size: 12px; - padding: 3px; - width: 29px; - height: 18px; - line-height: 13px; -} - -.recline-cell-reorder { - font-size: 12px; - padding: 1px; - width: 31px; - height: 14px; - line-height: 13px; - cursor: move; - background: url("../images/drag-handle.png") no-repeat center center; -} -.recline-timeline { - position: relative; -} diff --git a/dist/recline.dataset.js b/dist/recline.dataset.js deleted file mode 100644 index 96a5ac53..00000000 --- a/dist/recline.dataset.js +++ /dev/null @@ -1,896 +0,0 @@ -// # Recline Backbone Models -this.recline = this.recline || {}; -this.recline.Model = this.recline.Model || {}; - -(function(my) { - "use strict"; - -// use either jQuery or Underscore Deferred depending on what is available -var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred; - -// ## Dataset -my.Dataset = Backbone.Model.extend({ - constructor: function Dataset() { - Backbone.Model.prototype.constructor.apply(this, arguments); - }, - - // ### initialize - initialize: function() { - var self = this; - _.bindAll(this, 'query'); - this.backend = null; - if (this.get('backend')) { - this.backend = this._backendFromString(this.get('backend')); - } else { // try to guess backend ... - if (this.get('records')) { - this.backend = recline.Backend.Memory; - } - } - this.fields = new my.FieldList(); - this.records = new my.RecordList(); - this._changes = { - deletes: [], - updates: [], - creates: [] - }; - this.facets = new my.FacetList(); - this.recordCount = null; - this.queryState = new my.Query(); - this.queryState.bind('change facet:add', function () { - self.query(); // We want to call query() without any arguments. - }); - // store is what we query and save against - // store will either be the backend or be a memory store if Backend fetch - // tells us to use memory store - this._store = this.backend; - - // if backend has a handleQueryResultFunction, use that - this._handleResult = (this.backend != null && _.has(this.backend, 'handleQueryResult')) ? - this.backend.handleQueryResult : this._handleQueryResult; - if (this.backend == recline.Backend.Memory) { - this.fetch(); - } - }, - - sync: function(method, model, options) { - return this.backend.sync(method, model, options); - }, - - // ### fetch - // - // Retrieve dataset and (some) records from the backend. - fetch: function() { - var self = this; - var dfd = new Deferred(); - - if (this.backend !== recline.Backend.Memory) { - this.backend.fetch(this.toJSON()) - .done(handleResults) - .fail(function(args) { - dfd.reject(args); - }); - } else { - // special case where we have been given data directly - handleResults({ - records: this.get('records'), - fields: this.get('fields'), - useMemoryStore: true - }); - } - - function handleResults(results) { - // if explicitly given the fields - // (e.g. var dataset = new Dataset({fields: fields, ...}) - // use that field info over anything we get back by parsing the data - // (results.fields) - var fields = self.get('fields') || results.fields; - - var out = self._normalizeRecordsAndFields(results.records, fields); - if (results.useMemoryStore) { - self._store = new recline.Backend.Memory.Store(out.records, out.fields); - } - - self.set(results.metadata); - self.fields.reset(out.fields); - self.query() - .done(function() { - dfd.resolve(self); - }) - .fail(function(args) { - dfd.reject(args); - }); - } - - return dfd.promise(); - }, - - // ### _normalizeRecordsAndFields - // - // Get a proper set of fields and records from incoming set of fields and records either of which may be null or arrays or objects - // - // e.g. fields = ['a', 'b', 'c'] and records = [ [1,2,3] ] => - // fields = [ {id: a}, {id: b}, {id: c}], records = [ {a: 1}, {b: 2}, {c: 3}] - _normalizeRecordsAndFields: function(records, fields) { - // if no fields get them from records - if (!fields && records && records.length > 0) { - // records is array then fields is first row of records ... - if (records[0] instanceof Array) { - fields = records[0]; - records = records.slice(1); - } else { - fields = _.map(_.keys(records[0]), function(key) { - return {id: key}; - }); - } - } - - // fields is an array of strings (i.e. list of field headings/ids) - if (fields && fields.length > 0 && (fields[0] === null || typeof(fields[0]) != 'object')) { - // Rename duplicate fieldIds as each field name needs to be - // unique. - var seen = {}; - fields = _.map(fields, function(field, index) { - if (field === null) { - field = ''; - } else { - field = field.toString(); - } - // cannot use trim as not supported by IE7 - var fieldId = field.replace(/^\s+|\s+$/g, ''); - if (fieldId === '') { - fieldId = '_noname_'; - field = fieldId; - } - while (fieldId in seen) { - seen[field] += 1; - fieldId = field + seen[field]; - } - if (!(field in seen)) { - seen[field] = 0; - } - // TODO: decide whether to keep original name as label ... - // return { id: fieldId, label: field || fieldId } - return { id: fieldId }; - }); - } - // records is provided as arrays so need to zip together with fields - // NB: this requires you to have fields to match arrays - if (records && records.length > 0 && records[0] instanceof Array) { - records = _.map(records, function(doc) { - var tmp = {}; - _.each(fields, function(field, idx) { - tmp[field.id] = doc[idx]; - }); - return tmp; - }); - } - return { - fields: fields, - records: records - }; - }, - - save: function() { - var self = this; - // TODO: need to reset the changes ... - return this._store.save(this._changes, this.toJSON()); - }, - - // ### query - // - // AJAX method with promise API to get records from the backend. - // - // It will query based on current query state (given by this.queryState) - // updated by queryObj (if provided). - // - // Resulting RecordList are used to reset this.records and are - // also returned. - query: function(queryObj) { - var self = this; - var dfd = new Deferred(); - this.trigger('query:start'); - - if (queryObj) { - var attributes = queryObj; - if (queryObj instanceof my.Query) { - attributes = queryObj.toJSON(); - } - this.queryState.set(attributes, {silent: true}); - } - var actualQuery = this.queryState.toJSON(); - - this._store.query(actualQuery, this.toJSON()) - .done(function(queryResult) { - self._handleResult(queryResult); - self.trigger('query:done'); - dfd.resolve(self.records); - }) - .fail(function(args) { - self.trigger('query:fail', args); - dfd.reject(args); - }); - return dfd.promise(); - }, - - _handleQueryResult: function(queryResult) { - var self = this; - self.recordCount = queryResult.total; - var docs = _.map(queryResult.hits, function(hit) { - var _doc = new my.Record(hit); - _doc.fields = self.fields; - _doc.bind('change', function(doc) { - self._changes.updates.push(doc.toJSON()); - }); - _doc.bind('destroy', function(doc) { - self._changes.deletes.push(doc.toJSON()); - }); - return _doc; - }); - self.records.reset(docs); - if (queryResult.facets) { - var facets = _.map(queryResult.facets, function(facetResult, facetId) { - facetResult.id = facetId; - return new my.Facet(facetResult); - }); - self.facets.reset(facets); - } - }, - - toTemplateJSON: function() { - var data = this.toJSON(); - data.recordCount = this.recordCount; - data.fields = this.fields.toJSON(); - return data; - }, - - // ### getFieldsSummary - // - // Get a summary for each field in the form of a `Facet`. - // - // @return null as this is async function. Provides deferred/promise interface. - getFieldsSummary: function() { - var self = this; - var query = new my.Query(); - query.set({size: 0}); - this.fields.each(function(field) { - query.addFacet(field.id); - }); - var dfd = new Deferred(); - this._store.query(query.toJSON(), this.toJSON()).done(function(queryResult) { - if (queryResult.facets) { - _.each(queryResult.facets, function(facetResult, facetId) { - facetResult.id = facetId; - var facet = new my.Facet(facetResult); - // TODO: probably want replace rather than reset (i.e. just replace the facet with this id) - self.fields.get(facetId).facets.reset(facet); - }); - } - dfd.resolve(queryResult); - }); - return dfd.promise(); - }, - - // Deprecated (as of v0.5) - use record.summary() - recordSummary: function(record) { - return record.summary(); - }, - - // ### _backendFromString(backendString) - // - // Look up a backend module from a backend string (look in recline.Backend) - _backendFromString: function(backendString) { - var backend = null; - if (recline && recline.Backend) { - _.each(_.keys(recline.Backend), function(name) { - if (name.toLowerCase() === backendString.toLowerCase()) { - backend = recline.Backend[name]; - } - }); - } - return backend; - } -}); - - -// ## A Record -// -// A single record (or row) in the dataset -my.Record = Backbone.Model.extend({ - constructor: function Record() { - Backbone.Model.prototype.constructor.apply(this, arguments); - }, - - // ### initialize - // - // Create a Record - // - // You usually will not do this directly but will have records created by - // Dataset e.g. in query method - // - // Certain methods require presence of a fields attribute (identical to that on Dataset) - initialize: function() { - _.bindAll(this, 'getFieldValue'); - }, - - // ### getFieldValue - // - // For the provided Field get the corresponding rendered computed data value - // for this record. - // - // NB: if field is undefined a default '' value will be returned - getFieldValue: function(field) { - var val = this.getFieldValueUnrendered(field); - if (field && !_.isUndefined(field.renderer)) { - val = field.renderer(val, field, this.toJSON()); - } - return val; - }, - - // ### getFieldValueUnrendered - // - // For the provided Field get the corresponding computed data value - // for this record. - // - // NB: if field is undefined a default '' value will be returned - getFieldValueUnrendered: function(field) { - if (!field) { - return ''; - } - var val = this.get(field.id); - if (field.deriver) { - val = field.deriver(val, field, this); - } - return val; - }, - - // ### summary - // - // Get a simple html summary of this record in form of key/value list - summary: function(record) { - var self = this; - var html = '
'; - this.fields.each(function(field) { - if (field.id != 'id') { - html += '
' + field.get('label') + ': ' + self.getFieldValue(field) + '
'; - } - }); - html += '
'; - return html; - }, - - // Override Backbone save, fetch and destroy so they do nothing - // Instead, Dataset object that created this Record should take care of - // handling these changes (discovery will occur via event notifications) - // WARNING: these will not persist *unless* you call save on Dataset - fetch: function() {}, - save: function() {}, - destroy: function() { this.trigger('destroy', this); } -}); - - -// ## A Backbone collection of Records -my.RecordList = Backbone.Collection.extend({ - constructor: function RecordList() { - Backbone.Collection.prototype.constructor.apply(this, arguments); - }, - model: my.Record -}); - - -// ## A Field (aka Column) on a Dataset -my.Field = Backbone.Model.extend({ - constructor: function Field() { - Backbone.Model.prototype.constructor.apply(this, arguments); - }, - // ### defaults - define default values - defaults: { - label: null, - type: 'string', - format: null, - is_derived: false - }, - // ### initialize - // - // @param {Object} data: standard Backbone model attributes - // - // @param {Object} options: renderer and/or deriver functions. - initialize: function(data, options) { - // if a hash not passed in the first argument throw error - if ('0' in data) { - throw new Error('Looks like you did not pass a proper hash with id to Field constructor'); - } - if (this.attributes.label === null) { - this.set({label: this.id}); - } - if (this.attributes.type.toLowerCase() in this._typeMap) { - this.attributes.type = this._typeMap[this.attributes.type.toLowerCase()]; - } - if (options) { - this.renderer = options.renderer; - this.deriver = options.deriver; - } - if (!this.renderer) { - this.renderer = this.defaultRenderers[this.get('type')]; - } - this.facets = new my.FacetList(); - }, - _typeMap: { - 'text': 'string', - 'double': 'number', - 'float': 'number', - 'numeric': 'number', - 'int': 'integer', - 'datetime': 'date-time', - 'bool': 'boolean', - 'timestamp': 'date-time', - 'json': 'object' - }, - defaultRenderers: { - object: function(val, field, doc) { - return JSON.stringify(val); - }, - geo_point: function(val, field, doc) { - return JSON.stringify(val); - }, - 'number': function(val, field, doc) { - var format = field.get('format'); - if (format === 'percentage') { - return val + '%'; - } - return val; - }, - 'string': function(val, field, doc) { - var format = field.get('format'); - if (format === 'markdown') { - if (typeof Showdown !== 'undefined') { - var showdown = new Showdown.converter(); - out = showdown.makeHtml(val); - return out; - } else { - return val; - } - } else if (format == 'plain') { - return val; - } else { - // as this is the default and default type is string may get things - // here that are not actually strings - if (val && typeof val === 'string') { - val = val.replace(/(https?:\/\/[^ ]+)/g, '$1'); - } - return val; - } - } - } -}); - -my.FieldList = Backbone.Collection.extend({ - constructor: function FieldList() { - Backbone.Collection.prototype.constructor.apply(this, arguments); - }, - model: my.Field -}); - -// ## Query -my.Query = Backbone.Model.extend({ - constructor: function Query() { - Backbone.Model.prototype.constructor.apply(this, arguments); - }, - defaults: function() { - return { - size: 100, - from: 0, - q: '', - facets: {}, - filters: [] - }; - }, - _filterTemplates: { - term: { - type: 'term', - // TODO do we need this attribute here? - field: '', - term: '' - }, - range: { - type: 'range', - from: '', - to: '' - }, - geo_distance: { - type: 'geo_distance', - distance: 10, - unit: 'km', - point: { - lon: 0, - lat: 0 - } - } - }, - // ### addFilter(filter) - // - // Add a new filter specified by the filter hash and append to the list of filters - // - // @param filter an object specifying the filter - see _filterTemplates for examples. If only type is provided will generate a filter by cloning _filterTemplates - addFilter: function(filter) { - // crude deep copy - var ourfilter = JSON.parse(JSON.stringify(filter)); - // not fully specified so use template and over-write - if (_.keys(filter).length <= 3) { - ourfilter = _.defaults(ourfilter, this._filterTemplates[filter.type]); - } - var filters = this.get('filters'); - filters.push(ourfilter); - this.trigger('change:filters:new-blank'); - }, - replaceFilter: function(filter) { - // delete filter on the same field, then add - var filters = this.get('filters'); - var idx = -1; - _.each(this.get('filters'), function(f, key, list) { - if (filter.field == f.field) { - idx = key; - } - }); - // trigger just one event (change:filters:new-blank) instead of one for remove and - // one for add - if (idx >= 0) { - filters.splice(idx, 1); - this.set({filters: filters}); - } - this.addFilter(filter); - }, - updateFilter: function(index, value) { - }, - // ### removeFilter - // - // Remove a filter from filters at index filterIndex - removeFilter: function(filterIndex) { - var filters = this.get('filters'); - filters.splice(filterIndex, 1); - this.set({filters: filters}); - this.trigger('change'); - }, - // ### addFacet - // - // Add a Facet to this query - // - // See - addFacet: function(fieldId, size, silent) { - var facets = this.get('facets'); - // Assume id and fieldId should be the same (TODO: this need not be true if we want to add two different type of facets on same field) - if (_.contains(_.keys(facets), fieldId)) { - return; - } - facets[fieldId] = { - terms: { field: fieldId } - }; - if (!_.isUndefined(size)) { - facets[fieldId].terms.size = size; - } - this.set({facets: facets}, {silent: true}); - if (!silent) { - this.trigger('facet:add', this); - } - }, - addHistogramFacet: function(fieldId) { - var facets = this.get('facets'); - facets[fieldId] = { - date_histogram: { - field: fieldId, - interval: 'day' - } - }; - this.set({facets: facets}, {silent: true}); - this.trigger('facet:add', this); - }, - removeFacet: function(fieldId) { - var facets = this.get('facets'); - // Assume id and fieldId should be the same (TODO: this need not be true if we want to add two different type of facets on same field) - if (!_.contains(_.keys(facets), fieldId)) { - return; - } - delete facets[fieldId]; - this.set({facets: facets}, {silent: true}); - this.trigger('facet:remove', this); - }, - clearFacets: function() { - var facets = this.get('facets'); - _.each(_.keys(facets), function(fieldId) { - delete facets[fieldId]; - }); - this.trigger('facet:remove', this); - }, - // trigger a facet add; use this to trigger a single event after adding - // multiple facets - refreshFacets: function() { - this.trigger('facet:add', this); - } - -}); - - -// ## A Facet (Result) -my.Facet = Backbone.Model.extend({ - constructor: function Facet() { - Backbone.Model.prototype.constructor.apply(this, arguments); - }, - defaults: function() { - return { - _type: 'terms', - total: 0, - other: 0, - missing: 0, - terms: [] - }; - } -}); - -// ## A Collection/List of Facets -my.FacetList = Backbone.Collection.extend({ - constructor: function FacetList() { - Backbone.Collection.prototype.constructor.apply(this, arguments); - }, - model: my.Facet -}); - -// ## Object State -// -// Convenience Backbone model for storing (configuration) state of objects like Views. -my.ObjectState = Backbone.Model.extend({ -}); - - -// ## Backbone.sync -// -// Override Backbone.sync to hand off to sync function in relevant backend -// Backbone.sync = function(method, model, options) { -// return model.backend.sync(method, model, options); -// }; - -}(this.recline.Model)); - -this.recline = this.recline || {}; -this.recline.Backend = this.recline.Backend || {}; -this.recline.Backend.Memory = this.recline.Backend.Memory || {}; - -(function(my) { - "use strict"; - my.__type__ = 'memory'; - - // private data - use either jQuery or Underscore Deferred depending on what is available - var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred; - - // ## Data Wrapper - // - // Turn a simple array of JS objects into a mini data-store with - // functionality like querying, faceting, updating (by ID) and deleting (by - // ID). - // - // @param records list of hashes for each record/row in the data ({key: - // value, key: value}) - // @param fields (optional) list of field hashes (each hash defining a field - // as per recline.Model.Field). If fields not specified they will be taken - // from the data. - my.Store = function(records, fields) { - var self = this; - this.records = records; - // backwards compatability (in v0.5 records was named data) - this.data = this.records; - if (fields) { - this.fields = fields; - } else { - if (records) { - this.fields = _.map(records[0], function(value, key) { - return {id: key, type: 'string'}; - }); - } - } - - this.update = function(doc) { - _.each(self.records, function(internalDoc, idx) { - if(doc.id === internalDoc.id) { - self.records[idx] = doc; - } - }); - }; - - this.remove = function(doc) { - var newdocs = _.reject(self.records, function(internalDoc) { - return (doc.id === internalDoc.id); - }); - this.records = newdocs; - }; - - this.save = function(changes, dataset) { - var self = this; - var dfd = new Deferred(); - // TODO _.each(changes.creates) { ... } - _.each(changes.updates, function(record) { - self.update(record); - }); - _.each(changes.deletes, function(record) { - self.remove(record); - }); - dfd.resolve(); - return dfd.promise(); - }, - - this.query = function(queryObj) { - var dfd = new Deferred(); - var numRows = queryObj.size || this.records.length; - var start = queryObj.from || 0; - var results = this.records; - - results = this._applyFilters(results, queryObj); - results = this._applyFreeTextQuery(results, queryObj); - - // TODO: this is not complete sorting! - // What's wrong is we sort on the *last* entry in the sort list if there are multiple sort criteria - _.each(queryObj.sort, function(sortObj) { - var fieldName = sortObj.field; - results = _.sortBy(results, function(doc) { - var _out = doc[fieldName]; - return _out; - }); - if (sortObj.order == 'desc') { - results.reverse(); - } - }); - var facets = this.computeFacets(results, queryObj); - var out = { - total: results.length, - hits: results.slice(start, start+numRows), - facets: facets - }; - dfd.resolve(out); - return dfd.promise(); - }; - - // in place filtering - this._applyFilters = function(results, queryObj) { - var filters = queryObj.filters; - // register filters - var filterFunctions = { - term : term, - terms : terms, - range : range, - geo_distance : geo_distance - }; - var dataParsers = { - integer: function (e) { return parseFloat(e, 10); }, - 'float': function (e) { return parseFloat(e, 10); }, - number: function (e) { return parseFloat(e, 10); }, - string : function (e) { return e.toString(); }, - date : function (e) { return moment(e).valueOf(); }, - datetime : function (e) { return new Date(e).valueOf(); } - }; - var keyedFields = {}; - _.each(self.fields, function(field) { - keyedFields[field.id] = field; - }); - function getDataParser(filter) { - var fieldType = keyedFields[filter.field].type || 'string'; - return dataParsers[fieldType]; - } - - // filter records - return _.filter(results, function (record) { - var passes = _.map(filters, function (filter) { - return filterFunctions[filter.type](record, filter); - }); - - // return only these records that pass all filters - return _.all(passes, _.identity); - }); - - // filters definitions - function term(record, filter) { - var parse = getDataParser(filter); - var value = parse(record[filter.field]); - var term = parse(filter.term); - - return (value === term); - } - - function terms(record, filter) { - var parse = getDataParser(filter); - var value = parse(record[filter.field]); - var terms = parse(filter.terms).split(","); - - return (_.indexOf(terms, value) >= 0); - } - - function range(record, filter) { - var fromnull = (_.isUndefined(filter.from) || filter.from === null || filter.from === ''); - var tonull = (_.isUndefined(filter.to) || filter.to === null || filter.to === ''); - var parse = getDataParser(filter); - var value = parse(record[filter.field]); - var from = parse(fromnull ? '' : filter.from); - var to = parse(tonull ? '' : filter.to); - - // if at least one end of range is set do not allow '' to get through - // note that for strings '' <= {any-character} e.g. '' <= 'a' - if ((!fromnull || !tonull) && value === '') { - return false; - } - return ((fromnull || value >= from) && (tonull || value <= to)); - } - - function geo_distance() { - // TODO code here - } - }; - - // we OR across fields but AND across terms in query string - this._applyFreeTextQuery = function(results, queryObj) { - if (queryObj.q) { - var terms = queryObj.q.split(' '); - var patterns=_.map(terms, function(term) { - return new RegExp(term.toLowerCase()); - }); - results = _.filter(results, function(rawdoc) { - var matches = true; - _.each(patterns, function(pattern) { - var foundmatch = false; - _.each(self.fields, function(field) { - var value = rawdoc[field.id]; - if ((value !== null) && (value !== undefined)) { - value = value.toString(); - } else { - // value can be null (apparently in some cases) - value = ''; - } - // TODO regexes? - foundmatch = foundmatch || (pattern.test(value.toLowerCase())); - // TODO: early out (once we are true should break to spare unnecessary testing) - // if (foundmatch) return true; - }); - matches = matches && foundmatch; - // TODO: early out (once false should break to spare unnecessary testing) - // if (!matches) return false; - }); - return matches; - }); - } - return results; - }; - - this.computeFacets = function(records, queryObj) { - var facetResults = {}; - if (!queryObj.facets) { - return facetResults; - } - _.each(queryObj.facets, function(query, facetId) { - // TODO: remove dependency on recline.Model - facetResults[facetId] = new recline.Model.Facet({id: facetId}).toJSON(); - facetResults[facetId].termsall = {}; - }); - // faceting - _.each(records, function(doc) { - _.each(queryObj.facets, function(query, facetId) { - var fieldId = query.terms.field; - var val = doc[fieldId]; - var tmp = facetResults[facetId]; - if (val) { - tmp.termsall[val] = tmp.termsall[val] ? tmp.termsall[val] + 1 : 1; - } else { - tmp.missing = tmp.missing + 1; - } - }); - }); - _.each(queryObj.facets, function(query, facetId) { - var tmp = facetResults[facetId]; - var terms = _.map(tmp.termsall, function(count, term) { - return { term: term, count: count }; - }); - tmp.terms = _.sortBy(terms, function(item) { - // want descending order - return -item.count; - }); - tmp.terms = tmp.terms.slice(0, 10); - }); - return facetResults; - }; - }; - -}(this.recline.Backend.Memory)); diff --git a/dist/recline.dataset.min.js b/dist/recline.dataset.min.js deleted file mode 100644 index 8ca94447..00000000 --- a/dist/recline.dataset.min.js +++ /dev/null @@ -1 +0,0 @@ -this.recline=this.recline||{};this.recline.Model=this.recline.Model||{};(function(my){"use strict";var Deferred=typeof jQuery!=="undefined"&&jQuery.Deferred||_.Deferred;my.Dataset=Backbone.Model.extend({constructor:function Dataset(){Backbone.Model.prototype.constructor.apply(this,arguments)},initialize:function(){var self=this;_.bindAll(this,"query");this.backend=null;if(this.get("backend")){this.backend=this._backendFromString(this.get("backend"))}else{if(this.get("records")){this.backend=recline.Backend.Memory}}this.fields=new my.FieldList;this.records=new my.RecordList;this._changes={deletes:[],updates:[],creates:[]};this.facets=new my.FacetList;this.recordCount=null;this.queryState=new my.Query;this.queryState.bind("change facet:add",function(){self.query()});this._store=this.backend;this._handleResult=this.backend!=null&&_.has(this.backend,"handleQueryResult")?this.backend.handleQueryResult:this._handleQueryResult;if(this.backend==recline.Backend.Memory){this.fetch()}},sync:function(method,model,options){return this.backend.sync(method,model,options)},fetch:function(){var self=this;var dfd=new Deferred;if(this.backend!==recline.Backend.Memory){this.backend.fetch(this.toJSON()).done(handleResults).fail(function(args){dfd.reject(args)})}else{handleResults({records:this.get("records"),fields:this.get("fields"),useMemoryStore:true})}function handleResults(results){var fields=self.get("fields")||results.fields;var out=self._normalizeRecordsAndFields(results.records,fields);if(results.useMemoryStore){self._store=new recline.Backend.Memory.Store(out.records,out.fields)}self.set(results.metadata);self.fields.reset(out.fields);self.query().done(function(){dfd.resolve(self)}).fail(function(args){dfd.reject(args)})}return dfd.promise()},_normalizeRecordsAndFields:function(records,fields){if(!fields&&records&&records.length>0){if(records[0]instanceof Array){fields=records[0];records=records.slice(1)}else{fields=_.map(_.keys(records[0]),function(key){return{id:key}})}}if(fields&&fields.length>0&&(fields[0]===null||typeof fields[0]!="object")){var seen={};fields=_.map(fields,function(field,index){if(field===null){field=""}else{field=field.toString()}var fieldId=field.replace(/^\s+|\s+$/g,"");if(fieldId===""){fieldId="_noname_";field=fieldId}while(fieldId in seen){seen[field]+=1;fieldId=field+seen[field]}if(!(field in seen)){seen[field]=0}return{id:fieldId}})}if(records&&records.length>0&&records[0]instanceof Array){records=_.map(records,function(doc){var tmp={};_.each(fields,function(field,idx){tmp[field.id]=doc[idx]});return tmp})}return{fields:fields,records:records}},save:function(){var self=this;return this._store.save(this._changes,this.toJSON())},query:function(queryObj){var self=this;var dfd=new Deferred;this.trigger("query:start");if(queryObj){var attributes=queryObj;if(queryObj instanceof my.Query){attributes=queryObj.toJSON()}this.queryState.set(attributes,{silent:true})}var actualQuery=this.queryState.toJSON();this._store.query(actualQuery,this.toJSON()).done(function(queryResult){self._handleResult(queryResult);self.trigger("query:done");dfd.resolve(self.records)}).fail(function(args){self.trigger("query:fail",args);dfd.reject(args)});return dfd.promise()},_handleQueryResult:function(queryResult){var self=this;self.recordCount=queryResult.total;var docs=_.map(queryResult.hits,function(hit){var _doc=new my.Record(hit);_doc.fields=self.fields;_doc.bind("change",function(doc){self._changes.updates.push(doc.toJSON())});_doc.bind("destroy",function(doc){self._changes.deletes.push(doc.toJSON())});return _doc});self.records.reset(docs);if(queryResult.facets){var facets=_.map(queryResult.facets,function(facetResult,facetId){facetResult.id=facetId;return new my.Facet(facetResult)});self.facets.reset(facets)}},toTemplateJSON:function(){var data=this.toJSON();data.recordCount=this.recordCount;data.fields=this.fields.toJSON();return data},getFieldsSummary:function(){var self=this;var query=new my.Query;query.set({size:0});this.fields.each(function(field){query.addFacet(field.id)});var dfd=new Deferred;this._store.query(query.toJSON(),this.toJSON()).done(function(queryResult){if(queryResult.facets){_.each(queryResult.facets,function(facetResult,facetId){facetResult.id=facetId;var facet=new my.Facet(facetResult);self.fields.get(facetId).facets.reset(facet)})}dfd.resolve(queryResult)});return dfd.promise()},recordSummary:function(record){return record.summary()},_backendFromString:function(backendString){var backend=null;if(recline&&recline.Backend){_.each(_.keys(recline.Backend),function(name){if(name.toLowerCase()===backendString.toLowerCase()){backend=recline.Backend[name]}})}return backend}});my.Record=Backbone.Model.extend({constructor:function Record(){Backbone.Model.prototype.constructor.apply(this,arguments)},initialize:function(){_.bindAll(this,"getFieldValue")},getFieldValue:function(field){var val=this.getFieldValueUnrendered(field);if(field&&!_.isUndefined(field.renderer)){val=field.renderer(val,field,this.toJSON())}return val},getFieldValueUnrendered:function(field){if(!field){return""}var val=this.get(field.id);if(field.deriver){val=field.deriver(val,field,this)}return val},summary:function(record){var self=this;var html='
';this.fields.each(function(field){if(field.id!="id"){html+='
'+field.get("label")+": "+self.getFieldValue(field)+"
"}});html+="
";return html},fetch:function(){},save:function(){},destroy:function(){this.trigger("destroy",this)}});my.RecordList=Backbone.Collection.extend({constructor:function RecordList(){Backbone.Collection.prototype.constructor.apply(this,arguments)},model:my.Record});my.Field=Backbone.Model.extend({constructor:function Field(){Backbone.Model.prototype.constructor.apply(this,arguments)},defaults:{label:null,type:"string",format:null,is_derived:false},initialize:function(data,options){if("0"in data){throw new Error("Looks like you did not pass a proper hash with id to Field constructor")}if(this.attributes.label===null){this.set({label:this.id})}if(this.attributes.type.toLowerCase()in this._typeMap){this.attributes.type=this._typeMap[this.attributes.type.toLowerCase()]}if(options){this.renderer=options.renderer;this.deriver=options.deriver}if(!this.renderer){this.renderer=this.defaultRenderers[this.get("type")]}this.facets=new my.FacetList},_typeMap:{text:"string","double":"number","float":"number",numeric:"number","int":"integer",datetime:"date-time",bool:"boolean",timestamp:"date-time",json:"object"},defaultRenderers:{object:function(val,field,doc){return JSON.stringify(val)},geo_point:function(val,field,doc){return JSON.stringify(val)},number:function(val,field,doc){var format=field.get("format");if(format==="percentage"){return val+"%"}return val},string:function(val,field,doc){var format=field.get("format");if(format==="markdown"){if(typeof Showdown!=="undefined"){var showdown=new Showdown.converter;out=showdown.makeHtml(val);return out}else{return val}}else if(format=="plain"){return val}else{if(val&&typeof val==="string"){val=val.replace(/(https?:\/\/[^ ]+)/g,'$1')}return val}}}});my.FieldList=Backbone.Collection.extend({constructor:function FieldList(){Backbone.Collection.prototype.constructor.apply(this,arguments)},model:my.Field});my.Query=Backbone.Model.extend({constructor:function Query(){Backbone.Model.prototype.constructor.apply(this,arguments)},defaults:function(){return{size:100,from:0,q:"",facets:{},filters:[]}},_filterTemplates:{term:{type:"term",field:"",term:""},range:{type:"range",from:"",to:""},geo_distance:{type:"geo_distance",distance:10,unit:"km",point:{lon:0,lat:0}}},addFilter:function(filter){var ourfilter=JSON.parse(JSON.stringify(filter));if(_.keys(filter).length<=3){ourfilter=_.defaults(ourfilter,this._filterTemplates[filter.type])}var filters=this.get("filters");filters.push(ourfilter);this.trigger("change:filters:new-blank")},replaceFilter:function(filter){var filters=this.get("filters");var idx=-1;_.each(this.get("filters"),function(f,key,list){if(filter.field==f.field){idx=key}});if(idx>=0){filters.splice(idx,1);this.set({filters:filters})}this.addFilter(filter)},updateFilter:function(index,value){},removeFilter:function(filterIndex){var filters=this.get("filters");filters.splice(filterIndex,1);this.set({filters:filters});this.trigger("change")},addFacet:function(fieldId,size,silent){var facets=this.get("facets");if(_.contains(_.keys(facets),fieldId)){return}facets[fieldId]={terms:{field:fieldId}};if(!_.isUndefined(size)){facets[fieldId].terms.size=size}this.set({facets:facets},{silent:true});if(!silent){this.trigger("facet:add",this)}},addHistogramFacet:function(fieldId){var facets=this.get("facets");facets[fieldId]={date_histogram:{field:fieldId,interval:"day"}};this.set({facets:facets},{silent:true});this.trigger("facet:add",this)},removeFacet:function(fieldId){var facets=this.get("facets");if(!_.contains(_.keys(facets),fieldId)){return}delete facets[fieldId];this.set({facets:facets},{silent:true});this.trigger("facet:remove",this)},clearFacets:function(){var facets=this.get("facets");_.each(_.keys(facets),function(fieldId){delete facets[fieldId]});this.trigger("facet:remove",this)},refreshFacets:function(){this.trigger("facet:add",this)}});my.Facet=Backbone.Model.extend({constructor:function Facet(){Backbone.Model.prototype.constructor.apply(this,arguments)},defaults:function(){return{_type:"terms",total:0,other:0,missing:0,terms:[]}}});my.FacetList=Backbone.Collection.extend({constructor:function FacetList(){Backbone.Collection.prototype.constructor.apply(this,arguments)},model:my.Facet});my.ObjectState=Backbone.Model.extend({})})(this.recline.Model);this.recline=this.recline||{};this.recline.Backend=this.recline.Backend||{};this.recline.Backend.Memory=this.recline.Backend.Memory||{};(function(my){"use strict";my.__type__="memory";var Deferred=typeof jQuery!=="undefined"&&jQuery.Deferred||_.Deferred;my.Store=function(records,fields){var self=this;this.records=records;this.data=this.records;if(fields){this.fields=fields}else{if(records){this.fields=_.map(records[0],function(value,key){return{id:key,type:"string"}})}}this.update=function(doc){_.each(self.records,function(internalDoc,idx){if(doc.id===internalDoc.id){self.records[idx]=doc}})};this.remove=function(doc){var newdocs=_.reject(self.records,function(internalDoc){return doc.id===internalDoc.id});this.records=newdocs};this.save=function(changes,dataset){var self=this;var dfd=new Deferred;_.each(changes.updates,function(record){self.update(record)});_.each(changes.deletes,function(record){self.remove(record)});dfd.resolve();return dfd.promise()},this.query=function(queryObj){var dfd=new Deferred;var numRows=queryObj.size||this.records.length;var start=queryObj.from||0;var results=this.records;results=this._applyFilters(results,queryObj);results=this._applyFreeTextQuery(results,queryObj);_.each(queryObj.sort,function(sortObj){var fieldName=sortObj.field;results=_.sortBy(results,function(doc){var _out=doc[fieldName];return _out});if(sortObj.order=="desc"){results.reverse()}});var facets=this.computeFacets(results,queryObj);var out={total:results.length,hits:results.slice(start,start+numRows),facets:facets};dfd.resolve(out);return dfd.promise()};this._applyFilters=function(results,queryObj){var filters=queryObj.filters;var filterFunctions={term:term,terms:terms,range:range,geo_distance:geo_distance};var dataParsers={integer:function(e){return parseFloat(e,10)},"float":function(e){return parseFloat(e,10)},number:function(e){return parseFloat(e,10)},string:function(e){return e.toString()},date:function(e){return moment(e).valueOf()},datetime:function(e){return new Date(e).valueOf()}};var keyedFields={};_.each(self.fields,function(field){keyedFields[field.id]=field});function getDataParser(filter){var fieldType=keyedFields[filter.field].type||"string";return dataParsers[fieldType]}return _.filter(results,function(record){var passes=_.map(filters,function(filter){return filterFunctions[filter.type](record,filter)});return _.all(passes,_.identity)});function term(record,filter){var parse=getDataParser(filter);var value=parse(record[filter.field]);var term=parse(filter.term);return value===term}function terms(record,filter){var parse=getDataParser(filter);var value=parse(record[filter.field]);var terms=parse(filter.terms).split(",");return _.indexOf(terms,value)>=0}function range(record,filter){var fromnull=_.isUndefined(filter.from)||filter.from===null||filter.from==="";var tonull=_.isUndefined(filter.to)||filter.to===null||filter.to==="";var parse=getDataParser(filter);var value=parse(record[filter.field]);var from=parse(fromnull?"":filter.from);var to=parse(tonull?"":filter.to);if((!fromnull||!tonull)&&value===""){return false}return(fromnull||value>=from)&&(tonull||value<=to)}function geo_distance(){}};this._applyFreeTextQuery=function(results,queryObj){if(queryObj.q){var terms=queryObj.q.split(" ");var patterns=_.map(terms,function(term){return new RegExp(term.toLowerCase())});results=_.filter(results,function(rawdoc){var matches=true;_.each(patterns,function(pattern){var foundmatch=false;_.each(self.fields,function(field){var value=rawdoc[field.id];if(value!==null&&value!==undefined){value=value.toString()}else{value=""}foundmatch=foundmatch||pattern.test(value.toLowerCase())});matches=matches&&foundmatch});return matches})}return results};this.computeFacets=function(records,queryObj){var facetResults={};if(!queryObj.facets){return facetResults}_.each(queryObj.facets,function(query,facetId){facetResults[facetId]=new recline.Model.Facet({id:facetId}).toJSON();facetResults[facetId].termsall={}});_.each(records,function(doc){_.each(queryObj.facets,function(query,facetId){var fieldId=query.terms.field;var val=doc[fieldId];var tmp=facetResults[facetId];if(val){tmp.termsall[val]=tmp.termsall[val]?tmp.termsall[val]+1:1}else{tmp.missing=tmp.missing+1}})});_.each(queryObj.facets,function(query,facetId){var tmp=facetResults[facetId];var terms=_.map(tmp.termsall,function(count,term){return{term:term,count:count}});tmp.terms=_.sortBy(terms,function(item){return-item.count});tmp.terms=tmp.terms.slice(0,10)});return facetResults}}})(this.recline.Backend.Memory); \ No newline at end of file diff --git a/dist/recline.js b/dist/recline.js deleted file mode 100644 index 7a873345..00000000 --- a/dist/recline.js +++ /dev/null @@ -1,4455 +0,0 @@ -this.recline = this.recline || {}; -this.recline.Backend = this.recline.Backend || {}; -this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {}; - -(function(my) { - "use strict"; - my.__type__ = 'dataproxy'; - // URL for the dataproxy - my.dataproxy_url = '//jsonpdataproxy.appspot.com'; - // Timeout for dataproxy (after this time if no response we error) - // Needed because use JSONP so do not receive e.g. 500 errors - my.timeout = 5000; - - - // use either jQuery or Underscore Deferred depending on what is available - var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred; - - // ## load - // - // Load data from a URL via the [DataProxy](http://github.com/okfn/dataproxy). - // - // Returns array of field names and array of arrays for records - my.fetch = function(dataset) { - var data = { - url: dataset.url, - 'max-results': dataset.size || dataset.rows || 1000, - type: dataset.format || '' - }; - var jqxhr = jQuery.ajax({ - url: my.dataproxy_url, - data: data, - dataType: 'jsonp' - }); - var dfd = new Deferred(); - _wrapInTimeout(jqxhr).done(function(results) { - if (results.error) { - dfd.reject(results.error); - } - - dfd.resolve({ - records: results.data, - fields: results.fields, - useMemoryStore: true - }); - }) - .fail(function(args) { - dfd.reject(args); - }); - return dfd.promise(); - }; - - // ## _wrapInTimeout - // - // Convenience method providing a crude way to catch backend errors on JSONP calls. - // Many of backends use JSONP and so will not get error messages and this is - // a crude way to catch those errors. - var _wrapInTimeout = function(ourFunction) { - var dfd = new Deferred(); - var timer = setTimeout(function() { - dfd.reject({ - message: 'Request Error: Backend did not respond after ' + (my.timeout / 1000) + ' seconds' - }); - }, my.timeout); - ourFunction.done(function(args) { - clearTimeout(timer); - dfd.resolve(args); - }) - .fail(function(args) { - clearTimeout(timer); - dfd.reject(args); - }) - ; - return dfd.promise(); - }; - -}(this.recline.Backend.DataProxy)); -this.recline = this.recline || {}; -this.recline.Backend = this.recline.Backend || {}; -this.recline.Backend.Memory = this.recline.Backend.Memory || {}; - -(function(my) { - "use strict"; - my.__type__ = 'memory'; - - // private data - use either jQuery or Underscore Deferred depending on what is available - var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred; - - // ## Data Wrapper - // - // Turn a simple array of JS objects into a mini data-store with - // functionality like querying, faceting, updating (by ID) and deleting (by - // ID). - // - // @param records list of hashes for each record/row in the data ({key: - // value, key: value}) - // @param fields (optional) list of field hashes (each hash defining a field - // as per recline.Model.Field). If fields not specified they will be taken - // from the data. - my.Store = function(records, fields) { - var self = this; - this.records = records; - // backwards compatability (in v0.5 records was named data) - this.data = this.records; - if (fields) { - this.fields = fields; - } else { - if (records) { - this.fields = _.map(records[0], function(value, key) { - return {id: key, type: 'string'}; - }); - } - } - - this.update = function(doc) { - _.each(self.records, function(internalDoc, idx) { - if(doc.id === internalDoc.id) { - self.records[idx] = doc; - } - }); - }; - - this.remove = function(doc) { - var newdocs = _.reject(self.records, function(internalDoc) { - return (doc.id === internalDoc.id); - }); - this.records = newdocs; - }; - - this.save = function(changes, dataset) { - var self = this; - var dfd = new Deferred(); - // TODO _.each(changes.creates) { ... } - _.each(changes.updates, function(record) { - self.update(record); - }); - _.each(changes.deletes, function(record) { - self.remove(record); - }); - dfd.resolve(); - return dfd.promise(); - }, - - this.query = function(queryObj) { - var dfd = new Deferred(); - var numRows = queryObj.size || this.records.length; - var start = queryObj.from || 0; - var results = this.records; - - results = this._applyFilters(results, queryObj); - results = this._applyFreeTextQuery(results, queryObj); - - // TODO: this is not complete sorting! - // What's wrong is we sort on the *last* entry in the sort list if there are multiple sort criteria - _.each(queryObj.sort, function(sortObj) { - var fieldName = sortObj.field; - results = _.sortBy(results, function(doc) { - var _out = doc[fieldName]; - return _out; - }); - if (sortObj.order == 'desc') { - results.reverse(); - } - }); - var facets = this.computeFacets(results, queryObj); - var out = { - total: results.length, - hits: results.slice(start, start+numRows), - facets: facets - }; - dfd.resolve(out); - return dfd.promise(); - }; - - // in place filtering - this._applyFilters = function(results, queryObj) { - var filters = queryObj.filters; - // register filters - var filterFunctions = { - term : term, - terms : terms, - range : range, - geo_distance : geo_distance - }; - var dataParsers = { - integer: function (e) { return parseFloat(e, 10); }, - 'float': function (e) { return parseFloat(e, 10); }, - number: function (e) { return parseFloat(e, 10); }, - string : function (e) { return e.toString(); }, - date : function (e) { return moment(e).valueOf(); }, - datetime : function (e) { return new Date(e).valueOf(); } - }; - var keyedFields = {}; - _.each(self.fields, function(field) { - keyedFields[field.id] = field; - }); - function getDataParser(filter) { - var fieldType = keyedFields[filter.field].type || 'string'; - return dataParsers[fieldType]; - } - - // filter records - return _.filter(results, function (record) { - var passes = _.map(filters, function (filter) { - return filterFunctions[filter.type](record, filter); - }); - - // return only these records that pass all filters - return _.all(passes, _.identity); - }); - - // filters definitions - function term(record, filter) { - var parse = getDataParser(filter); - var value = parse(record[filter.field]); - var term = parse(filter.term); - - return (value === term); - } - - function terms(record, filter) { - var parse = getDataParser(filter); - var value = parse(record[filter.field]); - var terms = parse(filter.terms).split(","); - - return (_.indexOf(terms, value) >= 0); - } - - function range(record, filter) { - var fromnull = (_.isUndefined(filter.from) || filter.from === null || filter.from === ''); - var tonull = (_.isUndefined(filter.to) || filter.to === null || filter.to === ''); - var parse = getDataParser(filter); - var value = parse(record[filter.field]); - var from = parse(fromnull ? '' : filter.from); - var to = parse(tonull ? '' : filter.to); - - // if at least one end of range is set do not allow '' to get through - // note that for strings '' <= {any-character} e.g. '' <= 'a' - if ((!fromnull || !tonull) && value === '') { - return false; - } - return ((fromnull || value >= from) && (tonull || value <= to)); - } - - function geo_distance() { - // TODO code here - } - }; - - // we OR across fields but AND across terms in query string - this._applyFreeTextQuery = function(results, queryObj) { - if (queryObj.q) { - var terms = queryObj.q.split(' '); - var patterns=_.map(terms, function(term) { - return new RegExp(term.toLowerCase()); - }); - results = _.filter(results, function(rawdoc) { - var matches = true; - _.each(patterns, function(pattern) { - var foundmatch = false; - _.each(self.fields, function(field) { - var value = rawdoc[field.id]; - if ((value !== null) && (value !== undefined)) { - value = value.toString(); - } else { - // value can be null (apparently in some cases) - value = ''; - } - // TODO regexes? - foundmatch = foundmatch || (pattern.test(value.toLowerCase())); - // TODO: early out (once we are true should break to spare unnecessary testing) - // if (foundmatch) return true; - }); - matches = matches && foundmatch; - // TODO: early out (once false should break to spare unnecessary testing) - // if (!matches) return false; - }); - return matches; - }); - } - return results; - }; - - this.computeFacets = function(records, queryObj) { - var facetResults = {}; - if (!queryObj.facets) { - return facetResults; - } - _.each(queryObj.facets, function(query, facetId) { - // TODO: remove dependency on recline.Model - facetResults[facetId] = new recline.Model.Facet({id: facetId}).toJSON(); - facetResults[facetId].termsall = {}; - }); - // faceting - _.each(records, function(doc) { - _.each(queryObj.facets, function(query, facetId) { - var fieldId = query.terms.field; - var val = doc[fieldId]; - var tmp = facetResults[facetId]; - if (val) { - tmp.termsall[val] = tmp.termsall[val] ? tmp.termsall[val] + 1 : 1; - } else { - tmp.missing = tmp.missing + 1; - } - }); - }); - _.each(queryObj.facets, function(query, facetId) { - var tmp = facetResults[facetId]; - var terms = _.map(tmp.termsall, function(count, term) { - return { term: term, count: count }; - }); - tmp.terms = _.sortBy(terms, function(item) { - // want descending order - return -item.count; - }); - tmp.terms = tmp.terms.slice(0, 10); - }); - return facetResults; - }; - }; - -}(this.recline.Backend.Memory)); -// This file adds in full array method support in browsers that don't support it -// see: http://stackoverflow.com/questions/2790001/fixing-javascript-array-functions-in-internet-explorer-indexof-foreach-etc - -// Add ECMA262-5 Array methods if not supported natively -if (!('indexOf' in Array.prototype)) { - Array.prototype.indexOf= function(find, i /*opt*/) { - if (i===undefined) i= 0; - if (i<0) i+= this.length; - if (i<0) i= 0; - for (var n= this.length; ithis.length-1) i= this.length-1; - for (i++; i-->0;) /* i++ because from-argument is sadly inclusive */ - if (i in this && this[i]===find) - return i; - return -1; - }; -} -if (!('forEach' in Array.prototype)) { - Array.prototype.forEach= function(action, that /*opt*/) { - for (var i= 0, n= this.length; iDataset -my.Dataset = Backbone.Model.extend({ - constructor: function Dataset() { - Backbone.Model.prototype.constructor.apply(this, arguments); - }, - - // ### initialize - initialize: function() { - var self = this; - _.bindAll(this, 'query'); - this.backend = null; - if (this.get('backend')) { - this.backend = this._backendFromString(this.get('backend')); - } else { // try to guess backend ... - if (this.get('records')) { - this.backend = recline.Backend.Memory; - } - } - this.fields = new my.FieldList(); - this.records = new my.RecordList(); - this._changes = { - deletes: [], - updates: [], - creates: [] - }; - this.facets = new my.FacetList(); - this.recordCount = null; - this.queryState = new my.Query(); - this.queryState.bind('change facet:add', function () { - self.query(); // We want to call query() without any arguments. - }); - // store is what we query and save against - // store will either be the backend or be a memory store if Backend fetch - // tells us to use memory store - this._store = this.backend; - - // if backend has a handleQueryResultFunction, use that - this._handleResult = (this.backend != null && _.has(this.backend, 'handleQueryResult')) ? - this.backend.handleQueryResult : this._handleQueryResult; - if (this.backend == recline.Backend.Memory) { - this.fetch(); - } - }, - - sync: function(method, model, options) { - return this.backend.sync(method, model, options); - }, - - // ### fetch - // - // Retrieve dataset and (some) records from the backend. - fetch: function() { - var self = this; - var dfd = new Deferred(); - - if (this.backend !== recline.Backend.Memory) { - this.backend.fetch(this.toJSON()) - .done(handleResults) - .fail(function(args) { - dfd.reject(args); - }); - } else { - // special case where we have been given data directly - handleResults({ - records: this.get('records'), - fields: this.get('fields'), - useMemoryStore: true - }); - } - - function handleResults(results) { - // if explicitly given the fields - // (e.g. var dataset = new Dataset({fields: fields, ...}) - // use that field info over anything we get back by parsing the data - // (results.fields) - var fields = self.get('fields') || results.fields; - - var out = self._normalizeRecordsAndFields(results.records, fields); - if (results.useMemoryStore) { - self._store = new recline.Backend.Memory.Store(out.records, out.fields); - } - - self.set(results.metadata); - self.fields.reset(out.fields); - self.query() - .done(function() { - dfd.resolve(self); - }) - .fail(function(args) { - dfd.reject(args); - }); - } - - return dfd.promise(); - }, - - // ### _normalizeRecordsAndFields - // - // Get a proper set of fields and records from incoming set of fields and records either of which may be null or arrays or objects - // - // e.g. fields = ['a', 'b', 'c'] and records = [ [1,2,3] ] => - // fields = [ {id: a}, {id: b}, {id: c}], records = [ {a: 1}, {b: 2}, {c: 3}] - _normalizeRecordsAndFields: function(records, fields) { - // if no fields get them from records - if (!fields && records && records.length > 0) { - // records is array then fields is first row of records ... - if (records[0] instanceof Array) { - fields = records[0]; - records = records.slice(1); - } else { - fields = _.map(_.keys(records[0]), function(key) { - return {id: key}; - }); - } - } - - // fields is an array of strings (i.e. list of field headings/ids) - if (fields && fields.length > 0 && (fields[0] === null || typeof(fields[0]) != 'object')) { - // Rename duplicate fieldIds as each field name needs to be - // unique. - var seen = {}; - fields = _.map(fields, function(field, index) { - if (field === null) { - field = ''; - } else { - field = field.toString(); - } - // cannot use trim as not supported by IE7 - var fieldId = field.replace(/^\s+|\s+$/g, ''); - if (fieldId === '') { - fieldId = '_noname_'; - field = fieldId; - } - while (fieldId in seen) { - seen[field] += 1; - fieldId = field + seen[field]; - } - if (!(field in seen)) { - seen[field] = 0; - } - // TODO: decide whether to keep original name as label ... - // return { id: fieldId, label: field || fieldId } - return { id: fieldId }; - }); - } - // records is provided as arrays so need to zip together with fields - // NB: this requires you to have fields to match arrays - if (records && records.length > 0 && records[0] instanceof Array) { - records = _.map(records, function(doc) { - var tmp = {}; - _.each(fields, function(field, idx) { - tmp[field.id] = doc[idx]; - }); - return tmp; - }); - } - return { - fields: fields, - records: records - }; - }, - - save: function() { - var self = this; - // TODO: need to reset the changes ... - return this._store.save(this._changes, this.toJSON()); - }, - - // ### query - // - // AJAX method with promise API to get records from the backend. - // - // It will query based on current query state (given by this.queryState) - // updated by queryObj (if provided). - // - // Resulting RecordList are used to reset this.records and are - // also returned. - query: function(queryObj) { - var self = this; - var dfd = new Deferred(); - this.trigger('query:start'); - - if (queryObj) { - var attributes = queryObj; - if (queryObj instanceof my.Query) { - attributes = queryObj.toJSON(); - } - this.queryState.set(attributes, {silent: true}); - } - var actualQuery = this.queryState.toJSON(); - - this._store.query(actualQuery, this.toJSON()) - .done(function(queryResult) { - self._handleResult(queryResult); - self.trigger('query:done'); - dfd.resolve(self.records); - }) - .fail(function(args) { - self.trigger('query:fail', args); - dfd.reject(args); - }); - return dfd.promise(); - }, - - _handleQueryResult: function(queryResult) { - var self = this; - self.recordCount = queryResult.total; - var docs = _.map(queryResult.hits, function(hit) { - var _doc = new my.Record(hit); - _doc.fields = self.fields; - _doc.bind('change', function(doc) { - self._changes.updates.push(doc.toJSON()); - }); - _doc.bind('destroy', function(doc) { - self._changes.deletes.push(doc.toJSON()); - }); - return _doc; - }); - self.records.reset(docs); - if (queryResult.facets) { - var facets = _.map(queryResult.facets, function(facetResult, facetId) { - facetResult.id = facetId; - return new my.Facet(facetResult); - }); - self.facets.reset(facets); - } - }, - - toTemplateJSON: function() { - var data = this.toJSON(); - data.recordCount = this.recordCount; - data.fields = this.fields.toJSON(); - return data; - }, - - // ### getFieldsSummary - // - // Get a summary for each field in the form of a `Facet`. - // - // @return null as this is async function. Provides deferred/promise interface. - getFieldsSummary: function() { - var self = this; - var query = new my.Query(); - query.set({size: 0}); - this.fields.each(function(field) { - query.addFacet(field.id); - }); - var dfd = new Deferred(); - this._store.query(query.toJSON(), this.toJSON()).done(function(queryResult) { - if (queryResult.facets) { - _.each(queryResult.facets, function(facetResult, facetId) { - facetResult.id = facetId; - var facet = new my.Facet(facetResult); - // TODO: probably want replace rather than reset (i.e. just replace the facet with this id) - self.fields.get(facetId).facets.reset(facet); - }); - } - dfd.resolve(queryResult); - }); - return dfd.promise(); - }, - - // Deprecated (as of v0.5) - use record.summary() - recordSummary: function(record) { - return record.summary(); - }, - - // ### _backendFromString(backendString) - // - // Look up a backend module from a backend string (look in recline.Backend) - _backendFromString: function(backendString) { - var backend = null; - if (recline && recline.Backend) { - _.each(_.keys(recline.Backend), function(name) { - if (name.toLowerCase() === backendString.toLowerCase()) { - backend = recline.Backend[name]; - } - }); - } - return backend; - } -}); - - -// ## A Record -// -// A single record (or row) in the dataset -my.Record = Backbone.Model.extend({ - constructor: function Record() { - Backbone.Model.prototype.constructor.apply(this, arguments); - }, - - // ### initialize - // - // Create a Record - // - // You usually will not do this directly but will have records created by - // Dataset e.g. in query method - // - // Certain methods require presence of a fields attribute (identical to that on Dataset) - initialize: function() { - _.bindAll(this, 'getFieldValue'); - }, - - // ### getFieldValue - // - // For the provided Field get the corresponding rendered computed data value - // for this record. - // - // NB: if field is undefined a default '' value will be returned - getFieldValue: function(field) { - var val = this.getFieldValueUnrendered(field); - if (field && !_.isUndefined(field.renderer)) { - val = field.renderer(val, field, this.toJSON()); - } - return val; - }, - - // ### getFieldValueUnrendered - // - // For the provided Field get the corresponding computed data value - // for this record. - // - // NB: if field is undefined a default '' value will be returned - getFieldValueUnrendered: function(field) { - if (!field) { - return ''; - } - var val = this.get(field.id); - if (field.deriver) { - val = field.deriver(val, field, this); - } - return val; - }, - - // ### summary - // - // Get a simple html summary of this record in form of key/value list - summary: function(record) { - var self = this; - var html = '
'; - this.fields.each(function(field) { - if (field.id != 'id') { - html += '
' + field.get('label') + ': ' + self.getFieldValue(field) + '
'; - } - }); - html += '
'; - return html; - }, - - // Override Backbone save, fetch and destroy so they do nothing - // Instead, Dataset object that created this Record should take care of - // handling these changes (discovery will occur via event notifications) - // WARNING: these will not persist *unless* you call save on Dataset - fetch: function() {}, - save: function() {}, - destroy: function() { this.trigger('destroy', this); } -}); - - -// ## A Backbone collection of Records -my.RecordList = Backbone.Collection.extend({ - constructor: function RecordList() { - Backbone.Collection.prototype.constructor.apply(this, arguments); - }, - model: my.Record -}); - - -// ## A Field (aka Column) on a Dataset -my.Field = Backbone.Model.extend({ - constructor: function Field() { - Backbone.Model.prototype.constructor.apply(this, arguments); - }, - // ### defaults - define default values - defaults: { - label: null, - type: 'string', - format: null, - is_derived: false - }, - // ### initialize - // - // @param {Object} data: standard Backbone model attributes - // - // @param {Object} options: renderer and/or deriver functions. - initialize: function(data, options) { - // if a hash not passed in the first argument throw error - if ('0' in data) { - throw new Error('Looks like you did not pass a proper hash with id to Field constructor'); - } - if (this.attributes.label === null) { - this.set({label: this.id}); - } - if (this.attributes.type.toLowerCase() in this._typeMap) { - this.attributes.type = this._typeMap[this.attributes.type.toLowerCase()]; - } - if (options) { - this.renderer = options.renderer; - this.deriver = options.deriver; - } - if (!this.renderer) { - this.renderer = this.defaultRenderers[this.get('type')]; - } - this.facets = new my.FacetList(); - }, - _typeMap: { - 'text': 'string', - 'double': 'number', - 'float': 'number', - 'numeric': 'number', - 'int': 'integer', - 'datetime': 'date-time', - 'bool': 'boolean', - 'timestamp': 'date-time', - 'json': 'object' - }, - defaultRenderers: { - object: function(val, field, doc) { - return JSON.stringify(val); - }, - geo_point: function(val, field, doc) { - return JSON.stringify(val); - }, - 'number': function(val, field, doc) { - var format = field.get('format'); - if (format === 'percentage') { - return val + '%'; - } - return val; - }, - 'string': function(val, field, doc) { - var format = field.get('format'); - if (format === 'markdown') { - if (typeof Showdown !== 'undefined') { - var showdown = new Showdown.converter(); - out = showdown.makeHtml(val); - return out; - } else { - return val; - } - } else if (format == 'plain') { - return val; - } else { - // as this is the default and default type is string may get things - // here that are not actually strings - if (val && typeof val === 'string') { - val = val.replace(/(https?:\/\/[^ ]+)/g, '$1'); - } - return val; - } - } - } -}); - -my.FieldList = Backbone.Collection.extend({ - constructor: function FieldList() { - Backbone.Collection.prototype.constructor.apply(this, arguments); - }, - model: my.Field -}); - -// ## Query -my.Query = Backbone.Model.extend({ - constructor: function Query() { - Backbone.Model.prototype.constructor.apply(this, arguments); - }, - defaults: function() { - return { - size: 100, - from: 0, - q: '', - facets: {}, - filters: [] - }; - }, - _filterTemplates: { - term: { - type: 'term', - // TODO do we need this attribute here? - field: '', - term: '' - }, - range: { - type: 'range', - from: '', - to: '' - }, - geo_distance: { - type: 'geo_distance', - distance: 10, - unit: 'km', - point: { - lon: 0, - lat: 0 - } - } - }, - // ### addFilter(filter) - // - // Add a new filter specified by the filter hash and append to the list of filters - // - // @param filter an object specifying the filter - see _filterTemplates for examples. If only type is provided will generate a filter by cloning _filterTemplates - addFilter: function(filter) { - // crude deep copy - var ourfilter = JSON.parse(JSON.stringify(filter)); - // not fully specified so use template and over-write - if (_.keys(filter).length <= 3) { - ourfilter = _.defaults(ourfilter, this._filterTemplates[filter.type]); - } - var filters = this.get('filters'); - filters.push(ourfilter); - this.trigger('change:filters:new-blank'); - }, - replaceFilter: function(filter) { - // delete filter on the same field, then add - var filters = this.get('filters'); - var idx = -1; - _.each(this.get('filters'), function(f, key, list) { - if (filter.field == f.field) { - idx = key; - } - }); - // trigger just one event (change:filters:new-blank) instead of one for remove and - // one for add - if (idx >= 0) { - filters.splice(idx, 1); - this.set({filters: filters}); - } - this.addFilter(filter); - }, - updateFilter: function(index, value) { - }, - // ### removeFilter - // - // Remove a filter from filters at index filterIndex - removeFilter: function(filterIndex) { - var filters = this.get('filters'); - filters.splice(filterIndex, 1); - this.set({filters: filters}); - this.trigger('change'); - }, - // ### addFacet - // - // Add a Facet to this query - // - // See - addFacet: function(fieldId, size, silent) { - var facets = this.get('facets'); - // Assume id and fieldId should be the same (TODO: this need not be true if we want to add two different type of facets on same field) - if (_.contains(_.keys(facets), fieldId)) { - return; - } - facets[fieldId] = { - terms: { field: fieldId } - }; - if (!_.isUndefined(size)) { - facets[fieldId].terms.size = size; - } - this.set({facets: facets}, {silent: true}); - if (!silent) { - this.trigger('facet:add', this); - } - }, - addHistogramFacet: function(fieldId) { - var facets = this.get('facets'); - facets[fieldId] = { - date_histogram: { - field: fieldId, - interval: 'day' - } - }; - this.set({facets: facets}, {silent: true}); - this.trigger('facet:add', this); - }, - removeFacet: function(fieldId) { - var facets = this.get('facets'); - // Assume id and fieldId should be the same (TODO: this need not be true if we want to add two different type of facets on same field) - if (!_.contains(_.keys(facets), fieldId)) { - return; - } - delete facets[fieldId]; - this.set({facets: facets}, {silent: true}); - this.trigger('facet:remove', this); - }, - clearFacets: function() { - var facets = this.get('facets'); - _.each(_.keys(facets), function(fieldId) { - delete facets[fieldId]; - }); - this.trigger('facet:remove', this); - }, - // trigger a facet add; use this to trigger a single event after adding - // multiple facets - refreshFacets: function() { - this.trigger('facet:add', this); - } - -}); - - -// ## A Facet (Result) -my.Facet = Backbone.Model.extend({ - constructor: function Facet() { - Backbone.Model.prototype.constructor.apply(this, arguments); - }, - defaults: function() { - return { - _type: 'terms', - total: 0, - other: 0, - missing: 0, - terms: [] - }; - } -}); - -// ## A Collection/List of Facets -my.FacetList = Backbone.Collection.extend({ - constructor: function FacetList() { - Backbone.Collection.prototype.constructor.apply(this, arguments); - }, - model: my.Facet -}); - -// ## Object State -// -// Convenience Backbone model for storing (configuration) state of objects like Views. -my.ObjectState = Backbone.Model.extend({ -}); - - -// ## Backbone.sync -// -// Override Backbone.sync to hand off to sync function in relevant backend -// Backbone.sync = function(method, model, options) { -// return model.backend.sync(method, model, options); -// }; - -}(this.recline.Model)); - -/*jshint multistr:true */ - -this.recline = this.recline || {}; -this.recline.View = this.recline.View || {}; - -(function($, my) { - "use strict"; -// ## Graph view for a Dataset using Flot graphing library. -// -// Initialization arguments (in a hash in first parameter): -// -// * model: recline.Model.Dataset -// * state: (optional) configuration hash of form: -// -// { -// group: {column name for x-axis}, -// series: [{column name for series A}, {column name series B}, ... ], -// // options are: lines, points, lines-and-points, bars, columns -// graphType: 'lines', -// graphOptions: {custom [flot options]} -// } -// -// NB: should *not* provide an el argument to the view but must let the view -// generate the element itself (you can then append view.el to the DOM. -my.Flot = Backbone.View.extend({ - template: ' \ -
\ -
\ -
\ -

Hey there!

\ -

There\'s no graph here yet because we don\'t know what fields you\'d like to see plotted.

\ -

Please tell us by using the menu on the right and a graph will automatically appear.

\ -
\ -
\ -
\ -', - - initialize: function(options) { - var self = this; - this.graphColors = ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"]; - - _.bindAll(this, 'render', 'redraw', '_toolTip', '_xaxisLabel'); - this.needToRedraw = false; - this.listenTo(this.model, 'change', this.render); - this.listenTo(this.model.fields, 'reset add', this.render); - this.listenTo(this.model.records, 'reset add', this.redraw); - var stateData = _.extend({ - group: null, - // so that at least one series chooser box shows up - series: [], - graphType: 'lines-and-points' - }, - options.state - ); - this.state = new recline.Model.ObjectState(stateData); - this.previousTooltipPoint = {x: null, y: null}; - this.editor = new my.FlotControls({ - model: this.model, - state: this.state.toJSON() - }); - this.listenTo(this.editor.state, 'change', function() { - self.state.set(self.editor.state.toJSON()); - self.redraw(); - }); - this.elSidebar = this.editor.$el; - }, - - render: function() { - var self = this; - var tmplData = this.model.toTemplateJSON(); - var htmls = Mustache.render(this.template, tmplData); - this.$el.html(htmls); - this.$graph = this.$el.find('.panel.graph'); - this.$graph.on("plothover", this._toolTip); - return this; - }, - - remove: function () { - this.editor.remove(); - Backbone.View.prototype.remove.apply(this, arguments); - }, - - redraw: function() { - // There are issues generating a Flot graph if either: - // * The relevant div that graph attaches to his hidden at the moment of creating the plot -- Flot will complain with - // Uncaught Invalid dimensions for plot, width = 0, height = 0 - // * There is no data for the plot -- either same error or may have issues later with errors like 'non-existent node-value' - var areWeVisible = !jQuery.expr.filters.hidden(this.el); - if ((!areWeVisible || this.model.records.length === 0)) { - this.needToRedraw = true; - return; - } - - // check we have something to plot - if (this.state.get('group') && this.state.get('series')) { - var series = this.createSeries(); - var options = this.getGraphOptions(this.state.attributes.graphType, series[0].data.length); - this.plot = $.plot(this.$graph, series, options); - } - }, - - show: function() { - // because we cannot redraw when hidden we may need to when becoming visible - if (this.needToRedraw) { - this.redraw(); - } - }, - - // infoboxes on mouse hover on points/bars etc - _toolTip: function (event, pos, item) { - if (item) { - if (this.previousTooltipPoint.x !== item.dataIndex || - this.previousTooltipPoint.y !== item.seriesIndex) { - this.previousTooltipPoint.x = item.dataIndex; - this.previousTooltipPoint.y = item.seriesIndex; - $("#recline-flot-tooltip").remove(); - - var x = item.datapoint[0].toFixed(2), - y = item.datapoint[1].toFixed(2); - - if (this.state.attributes.graphType === 'bars') { - x = item.datapoint[1].toFixed(2), - y = item.datapoint[0].toFixed(2); - } - - var template = _.template('<%= group %> = <%= x %>, <%= series %> = <%= y %>'); - var content = template({ - group: this.state.attributes.group, - x: this._xaxisLabel(x), - series: item.series.label, - y: y - }); - - // use a different tooltip location offset for bar charts - var xLocation, yLocation; - if (this.state.attributes.graphType === 'bars') { - xLocation = item.pageX + 15; - yLocation = item.pageY - 10; - } else if (this.state.attributes.graphType === 'columns') { - xLocation = item.pageX + 15; - yLocation = item.pageY; - } else { - xLocation = item.pageX + 10; - yLocation = item.pageY - 20; - } - - $('
' + content + '
').css({ - top: yLocation, - left: xLocation - }).appendTo("body").fadeIn(200); - } - } else { - $("#recline-flot-tooltip").remove(); - this.previousTooltipPoint.x = null; - this.previousTooltipPoint.y = null; - } - }, - - _xaxisLabel: function (x) { - if (this._groupFieldIsDateTime()) { - // oddly x comes through as milliseconds *string* (rather than int - // or float) so we have to reparse - x = new Date(parseFloat(x)).toLocaleDateString(); - } else if (this.xvaluesAreIndex) { - x = parseInt(x, 10); - // HACK: deal with bar graph style cases where x-axis items were strings - // In this case x at this point is the index of the item in the list of - // records not its actual x-axis value - x = this.model.records.models[x].get(this.state.attributes.group); - } - - return x; - }, - - // ### getGraphOptions - // - // Get options for Flot Graph - // - // needs to be function as can depend on state - // - // @param typeId graphType id (lines, lines-and-points etc) - // @param numPoints the number of points that will be plotted - getGraphOptions: function(typeId, numPoints) { - var self = this; - var groupFieldIsDateTime = self._groupFieldIsDateTime(); - var xaxis = {}; - - if (!groupFieldIsDateTime) { - xaxis.tickFormatter = function (x) { - // convert x to a string and make sure that it is not too long or the - // tick labels will overlap - // TODO: find a more accurate way of calculating the size of tick labels - var label = self._xaxisLabel(x) || ""; - - if (typeof label !== 'string') { - label = label.toString(); - } - if (self.state.attributes.graphType !== 'bars' && label.length > 10) { - label = label.slice(0, 10) + "..."; - } - - return label; - }; - } - - // for labels case we only want ticks at the label intervals - // HACK: however we also get this case with Date fields. In that case we - // could have a lot of values and so we limit to max 15 (we assume) - if (this.xvaluesAreIndex) { - var numTicks = Math.min(this.model.records.length, 15); - var increment = this.model.records.length / numTicks; - var ticks = []; - for (var i=0; i \ -
\ -
\ -
\ - \ -
\ - \ -
\ -
\ -
\ - \ -
\ - \ -
\ -
\ -
\ -
\ -
\ -
\ - \ -
\ - \ -
\ -
\ -', - templateSeriesEditor: ' \ -
\ -
\ - \ -
\ - \ -
\ -
\ -
\ - ', - events: { - 'change form select': 'onEditorSubmit', - 'click .editor-add': '_onAddSeries', - 'click .action-remove-series': 'removeSeries' - }, - - initialize: function(options) { - var self = this; - _.bindAll(this, 'render'); - this.listenTo(this.model.fields, 'reset add', this.render); - this.state = new recline.Model.ObjectState(options.state); - this.render(); - }, - - render: function() { - var self = this; - var tmplData = this.model.toTemplateJSON(); - var htmls = Mustache.render(this.template, tmplData); - this.$el.html(htmls); - - // set up editor from state - if (this.state.get('graphType')) { - this._selectOption('.editor-type', this.state.get('graphType')); - } - if (this.state.get('group')) { - this._selectOption('.editor-group', this.state.get('group')); - } - // ensure at least one series box shows up - var tmpSeries = [""]; - if (this.state.get('series').length > 0) { - tmpSeries = this.state.get('series'); - } - _.each(tmpSeries, function(series, idx) { - self.addSeries(idx); - self._selectOption('.editor-series.js-series-' + idx, series); - }); - return this; - }, - - // Private: Helper function to select an option from a select list - // - _selectOption: function(id,value){ - var options = this.$el.find(id + ' select > option'); - if (options) { - options.each(function(opt){ - if (this.value == value) { - $(this).attr('selected','selected'); - return false; - } - }); - } - }, - - onEditorSubmit: function(e) { - var select = this.$el.find('.editor-group select'); - var $editor = this; - var $series = this.$el.find('.editor-series select'); - var series = $series.map(function () { - return $(this).val(); - }); - var updatedState = { - series: $.makeArray(series), - group: this.$el.find('.editor-group select').val(), - graphType: this.$el.find('.editor-type select').val() - }; - this.state.set(updatedState); - }, - - // Public: Adds a new empty series select box to the editor. - // - // @param [int] idx index of this series in the list of series - // - // Returns itself. - addSeries: function (idx) { - var data = _.extend({ - seriesIndex: idx, - seriesName: String.fromCharCode(idx + 64 + 1) - }, this.model.toTemplateJSON()); - - var htmls = Mustache.render(this.templateSeriesEditor, data); - this.$el.find('.editor-series-group').append(htmls); - return this; - }, - - _onAddSeries: function(e) { - e.preventDefault(); - this.addSeries(this.state.get('series').length); - }, - - // Public: Removes a series list item from the editor. - // - // Also updates the labels of the remaining series elements. - removeSeries: function (e) { - e.preventDefault(); - var $el = $(e.target); - $el.parent().parent().remove(); - this.onEditorSubmit(); - } -}); - -})(jQuery, recline.View); -this.recline = this.recline || {}; -this.recline.View = this.recline.View || {}; -this.recline.View.Graph = this.recline.View.Flot; -this.recline.View.GraphControls = this.recline.View.FlotControls; -/*jshint multistr:true */ - -this.recline = this.recline || {}; -this.recline.View = this.recline.View || {}; - -(function($, my) { - "use strict"; -// ## (Data) Grid Dataset View -// -// Provides a tabular view on a Dataset. -// -// Initialize it with a `recline.Model.Dataset`. -my.Grid = Backbone.View.extend({ - tagName: "div", - className: "recline-grid-container", - - initialize: function(modelEtc) { - var self = this; - _.bindAll(this, 'render', 'onHorizontalScroll'); - this.listenTo(this.model.records, 'add reset remove', this.render); - this.tempState = {}; - var state = _.extend({ - hiddenFields: [] - }, modelEtc.state - ); - this.state = new recline.Model.ObjectState(state); - }, - - events: { - // does not work here so done at end of render function - // 'scroll .recline-grid tbody': 'onHorizontalScroll' - }, - - // ====================================================== - // Column and row menus - - setColumnSort: function(order) { - var sort = [{}]; - sort[0][this.tempState.currentColumn] = {order: order}; - this.model.query({sort: sort}); - }, - - hideColumn: function() { - var hiddenFields = this.state.get('hiddenFields'); - hiddenFields.push(this.tempState.currentColumn); - this.state.set({hiddenFields: hiddenFields}); - // change event not being triggered (because it is an array?) so trigger manually - this.state.trigger('change'); - this.render(); - }, - - showColumn: function(e) { - var hiddenFields = _.without(this.state.get('hiddenFields'), $(e.target).data('column')); - this.state.set({hiddenFields: hiddenFields}); - this.render(); - }, - - onHorizontalScroll: function(e) { - var currentScroll = $(e.target).scrollLeft(); - this.$el.find('.recline-grid thead tr').scrollLeft(currentScroll); - }, - - // ====================================================== - // #### Templating - template: ' \ -
\ - \ - \ - \ - {{#fields}} \ - \ - {{/fields}} \ - \ - \ - \ - \ -
\ - {{label}} \ -
\ -
\ - ', - - toTemplateJSON: function() { - var self = this; - var modelData = this.model.toJSON(); - modelData.notEmpty = ( this.fields.length > 0 ); - // TODO: move this sort of thing into a toTemplateJSON method on Dataset? - modelData.fields = this.fields.map(function(field) { - return field.toJSON(); - }); - // last header width = scroll bar - border (2px) */ - modelData.lastHeaderWidth = this.scrollbarDimensions.width - 2; - return modelData; - }, - render: function() { - var self = this; - this.fields = new recline.Model.FieldList(this.model.fields.filter(function(field) { - return _.indexOf(self.state.get('hiddenFields'), field.id) == -1; - })); - - this.scrollbarDimensions = this.scrollbarDimensions || this._scrollbarSize(); // skip measurement if already have dimensions - var numFields = this.fields.length; - // compute field widths (-20 for first menu col + 10px for padding on each col and finally 16px for the scrollbar) - var fullWidth = self.$el.width() - 20 - 10 * numFields - this.scrollbarDimensions.width; - var width = parseInt(Math.max(50, fullWidth / numFields), 10); - // if columns extend outside viewport then remainder is 0 - var remainder = Math.max(fullWidth - numFields * width,0); - this.fields.each(function(field, idx) { - // add the remainder to the first field width so we make up full col - if (idx === 0) { - field.set({width: width+remainder}); - } else { - field.set({width: width}); - } - }); - var htmls = Mustache.render(this.template, this.toTemplateJSON()); - this.$el.html(htmls); - this.model.records.forEach(function(doc) { - var tr = $(''); - self.$el.find('tbody').append(tr); - var newView = new my.GridRow({ - model: doc, - el: tr, - fields: self.fields - }); - newView.render(); - }); - // hide extra header col if no scrollbar to avoid unsightly overhang - var $tbody = this.$el.find('tbody')[0]; - if ($tbody.scrollHeight <= $tbody.offsetHeight) { - this.$el.find('th.last-header').hide(); - } - this.$el.find('.recline-grid').toggleClass('no-hidden', (self.state.get('hiddenFields').length === 0)); - this.$el.find('.recline-grid tbody').scroll(this.onHorizontalScroll); - return this; - }, - - // ### _scrollbarSize - // - // Measure width of a vertical scrollbar and height of a horizontal scrollbar. - // - // @return: { width: pixelWidth, height: pixelHeight } - _scrollbarSize: function() { - var $c = $("
").appendTo("body"); - var dim = { width: $c.width() - $c[0].clientWidth + 1, height: $c.height() - $c[0].clientHeight }; - $c.remove(); - return dim; - } -}); - -// ## GridRow View for rendering an individual record. -// -// Since we want this to update in place it is up to creator to provider the element to attach to. -// -// In addition you *must* pass in a FieldList in the constructor options. This should be list of fields for the Grid. -// -// Example: -// -//
-// var row = new GridRow({
-//   model: dataset-record,
-//     el: dom-element,
-//     fields: mydatasets.fields // a FieldList object
-//   });
-// 
-my.GridRow = Backbone.View.extend({ - initialize: function(initData) { - _.bindAll(this, 'render'); - this._fields = initData.fields; - this.listenTo(this.model, 'change', this.render); - }, - - template: ' \ - {{#cells}} \ - \ -
\ -   \ -
{{{value}}}
\ -
\ - \ - {{/cells}} \ - ', - events: { - 'click .data-table-cell-edit': 'onEditClick', - 'click .data-table-cell-editor .okButton': 'onEditorOK', - 'click .data-table-cell-editor .cancelButton': 'onEditorCancel' - }, - - toTemplateJSON: function() { - var self = this; - var doc = this.model; - var cellData = this._fields.map(function(field) { - return { - field: field.id, - width: field.get('width'), - value: doc.getFieldValue(field) - }; - }); - return { id: this.id, cells: cellData }; - }, - - render: function() { - this.$el.attr('data-id', this.model.id); - var html = Mustache.render(this.template, this.toTemplateJSON()); - this.$el.html(html); - return this; - }, - - // =================== - // Cell Editor methods - - cellEditorTemplate: ' \ - \ - ', - - onEditClick: function(e) { - var editing = this.$el.find('.data-table-cell-editor-editor'); - if (editing.length > 0) { - editing.parents('.data-table-cell-value').html(editing.text()).siblings('.data-table-cell-edit').removeClass("hidden"); - } - $(e.target).addClass("hidden"); - var cell = $(e.target).siblings('.data-table-cell-value'); - cell.data("previousContents", cell.text()); - var templated = Mustache.render(this.cellEditorTemplate, {value: cell.text()}); - cell.html(templated); - }, - - onEditorOK: function(e) { - var self = this; - var cell = $(e.target); - var rowId = cell.parents('tr').attr('data-id'); - var field = cell.parents('td').attr('data-field'); - var newValue = cell.parents('.data-table-cell-editor').find('.data-table-cell-editor-editor').val(); - var newData = {}; - newData[field] = newValue; - this.model.set(newData); - this.trigger('recline:flash', {message: "Updating row...", loader: true}); - this.model.save().then(function(response) { - this.trigger('recline:flash', {message: "Row updated successfully", category: 'success'}); - }) - .fail(function() { - this.trigger('recline:flash', { - message: 'Error saving row', - category: 'error', - persist: true - }); - }); - }, - - onEditorCancel: function(e) { - var cell = $(e.target).parents('.data-table-cell-value'); - cell.html(cell.data('previousContents')).siblings('.data-table-cell-edit').removeClass("hidden"); - } -}); - -})(jQuery, recline.View); -/*jshint multistr:true */ - -this.recline = this.recline || {}; -this.recline.View = this.recline.View || {}; - -(function($, my) { - "use strict"; -// ## Map view for a Dataset using Leaflet mapping library. -// -// This view allows to plot gereferenced records on a map. The location -// information can be provided in 2 ways: -// -// 1. Via a single field. This field must be either a geo_point or -// [GeoJSON](http://geojson.org) object -// 2. Via two fields with latitude and longitude coordinates. -// -// Which fields in the data these correspond to can be configured via the state -// (and are guessed if no info is provided). -// -// Initialization arguments are as standard for Dataset Views. State object may -// have the following (optional) configuration options: -// -//
-//   {
-//     // geomField if specified will be used in preference to lat/lon
-//     geomField: {id of field containing geometry in the dataset}
-//     lonField: {id of field containing longitude in the dataset}
-//     latField: {id of field containing latitude in the dataset}
-//     autoZoom: true,
-//     // use cluster support
-//     // cluster: true = always on
-//     // cluster: false = always off
-//     cluster: false
-//   }
-// 
-// -// Useful attributes to know about (if e.g. customizing) -// -// * map: the Leaflet map (L.Map) -// * features: Leaflet GeoJSON layer containing all the features (L.GeoJSON) -my.Map = Backbone.View.extend({ - template: ' \ -
\ -
\ -
\ -', - - // These are the default (case-insensitive) names of field that are used if found. - // If not found, the user will need to define the fields via the editor. - latitudeFieldNames: ['lat','latitude'], - longitudeFieldNames: ['lon','longitude'], - geometryFieldNames: ['geojson', 'geom','the_geom','geometry','spatial','location', 'geo', 'lonlat'], - - initialize: function(options) { - var self = this; - this.options = options; - this.visible = this.$el.is(':visible'); - this.mapReady = false; - // this will be the Leaflet L.Map object (setup below) - this.map = null; - - var stateData = _.extend({ - geomField: null, - lonField: null, - latField: null, - autoZoom: true, - cluster: false - }, - options.state - ); - this.state = new recline.Model.ObjectState(stateData); - - this._clusterOptions = { - zoomToBoundsOnClick: true, - //disableClusteringAtZoom: 10, - maxClusterRadius: 80, - singleMarkerMode: false, - skipDuplicateAddTesting: true, - animateAddingMarkers: false - }; - - // Listen to changes in the fields - this.listenTo(this.model.fields, 'change', function() { - self._setupGeometryField(); - self.render(); - }); - - // Listen to changes in the records - this.listenTo(this.model.records, 'add', function(doc){self.redraw('add',doc);}); - this.listenTo(this.model.records, 'change', function(doc){ - self.redraw('remove',doc); - self.redraw('add',doc); - }); - this.listenTo(this.model.records, 'remove', function(doc){self.redraw('remove',doc);}); - this.listenTo(this.model.records, 'reset', function(){self.redraw('reset');}); - - this.menu = new my.MapMenu({ - model: this.model, - state: this.state.toJSON() - }); - this.listenTo(this.menu.state, 'change', function() { - self.state.set(self.menu.state.toJSON()); - self.redraw(); - }); - this.listenTo(this.state, 'change', function() { - self.redraw(); - }); - this.elSidebar = this.menu.$el; - }, - - // ## Customization Functions - // - // The following methods are designed for overriding in order to customize - // behaviour - - // ### infobox - // - // Function to create infoboxes used in popups. The default behaviour is very simple and just lists all attributes. - // - // Users should override this function to customize behaviour i.e. - // - // view = new View({...}); - // view.infobox = function(record) { - // ... - // } - infobox: function(record) { - var html = ''; - for (var key in record.attributes){ - if (!(this.state.get('geomField') && key == this.state.get('geomField'))){ - html += '
' + key + ': '+ record.attributes[key] + '
'; - } - } - return html; - }, - - // Options to use for the [Leaflet GeoJSON layer](http://leaflet.cloudmade.com/reference.html#geojson) - // See also - // - // e.g. - // - // pointToLayer: function(feature, latLng) - // onEachFeature: function(feature, layer) - // - // See defaults for examples - geoJsonLayerOptions: { - // pointToLayer function to use when creating points - // - // Default behaviour shown here is to create a marker using the - // popupContent set on the feature properties (created via infobox function - // during feature generation) - // - // NB: inside pointToLayer `this` will be set to point to this map view - // instance (which allows e.g. this.markers to work in this default case) - pointToLayer: function (feature, latlng) { - var marker = new L.Marker(latlng); - marker.bindPopup(feature.properties.popupContent); - // this is for cluster case - this.markers.addLayer(marker); - return marker; - }, - // onEachFeature default which adds popup in - onEachFeature: function(feature, layer) { - if (feature.properties && feature.properties.popupContent) { - layer.bindPopup(feature.properties.popupContent); - } - } - }, - - // END: Customization section - // ---- - - // ### Public: Adds the necessary elements to the page. - // - // Also sets up the editor fields and the map if necessary. - render: function() { - var self = this; - var htmls = Mustache.render(this.template, this.model.toTemplateJSON()); - this.$el.html(htmls); - this.$map = this.$el.find('.panel.map'); - this.redraw(); - return this; - }, - - // ### Public: Redraws the features on the map according to the action provided - // - // Actions can be: - // - // * reset: Clear all features - // * add: Add one or n features (records) - // * remove: Remove one or n features (records) - // * refresh: Clear existing features and add all current records - redraw: function(action, doc){ - var self = this; - action = action || 'refresh'; - // try to set things up if not already - if (!self._geomReady()){ - self._setupGeometryField(); - } - if (!self.mapReady){ - self._setupMap(); - } - - if (this._geomReady() && this.mapReady){ - // removing ad re-adding the layer enables faster bulk loading - this.map.removeLayer(this.features); - this.map.removeLayer(this.markers); - - var countBefore = 0; - this.features.eachLayer(function(){countBefore++;}); - - if (action == 'refresh' || action == 'reset') { - this.features.clearLayers(); - // recreate cluster group because of issues with clearLayer - this.map.removeLayer(this.markers); - this.markers = new L.MarkerClusterGroup(this._clusterOptions); - this._add(this.model.records.models); - } else if (action == 'add' && doc){ - this._add(doc); - } else if (action == 'remove' && doc){ - this._remove(doc); - } - - // this must come before zooming! - // if not: errors when using e.g. circle markers like - // "Cannot call method 'project' of undefined" - if (this.state.get('cluster')) { - this.map.addLayer(this.markers); - } else { - this.map.addLayer(this.features); - } - - if (this.state.get('autoZoom')){ - if (this.visible){ - this._zoomToFeatures(); - } else { - this._zoomPending = true; - } - } - } - }, - - show: function() { - // If the div was hidden, Leaflet needs to recalculate some sizes - // to display properly - if (this.map){ - this.map.invalidateSize(); - if (this._zoomPending && this.state.get('autoZoom')) { - this._zoomToFeatures(); - this._zoomPending = false; - } - } - this.visible = true; - }, - - hide: function() { - this.visible = false; - }, - - _geomReady: function() { - return Boolean(this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField'))); - }, - - // Private: Add one or n features to the map - // - // For each record passed, a GeoJSON geometry will be extracted and added - // to the features layer. If an exception is thrown, the process will be - // stopped and an error notification shown. - // - // Each feature will have a popup associated with all the record fields. - // - _add: function(docs){ - var self = this; - - if (!(docs instanceof Array)) docs = [docs]; - - var count = 0; - var wrongSoFar = 0; - _.every(docs, function(doc){ - count += 1; - var feature = self._getGeometryFromRecord(doc); - if (typeof feature === 'undefined' || feature === null){ - // Empty field - return true; - } else if (feature instanceof Object){ - feature.properties = { - popupContent: self.infobox(doc), - // Add a reference to the model id, which will allow us to - // link this Leaflet layer to a Recline doc - cid: doc.cid - }; - - try { - self.features.addData(feature); - } catch (except) { - wrongSoFar += 1; - var msg = 'Wrong geometry value'; - if (except.message) msg += ' (' + except.message + ')'; - if (wrongSoFar <= 10) { - self.trigger('recline:flash', {message: msg, category:'error'}); - } - } - } else { - wrongSoFar += 1; - if (wrongSoFar <= 10) { - self.trigger('recline:flash', {message: 'Wrong geometry value', category:'error'}); - } - } - return true; - }); - }, - - // Private: Remove one or n features from the map - // - _remove: function(docs){ - - var self = this; - - if (!(docs instanceof Array)) docs = [docs]; - - _.each(docs,function(doc){ - for (var key in self.features._layers){ - if (self.features._layers[key].feature.geometry.properties.cid == doc.cid){ - self.features.removeLayer(self.features._layers[key]); - } - } - }); - - }, - - // Private: convert DMS coordinates to decimal - // - // north and east are positive, south and west are negative - // - _parseCoordinateString: function(coord){ - if (typeof(coord) != 'string') { - return(parseFloat(coord)); - } - var dms = coord.split(/[^-?\.\d\w]+/); - var deg = 0; var m = 0; - var toDeg = [1, 60, 3600]; // conversion factors for Deg, min, sec - var i; - for (i = 0; i < dms.length; ++i) { - if (isNaN(parseFloat(dms[i]))) { - continue; - } - deg += parseFloat(dms[i]) / toDeg[m]; - m += 1; - } - if (coord.match(/[SW]/)) { - deg = -1*deg; - } - return(deg); - }, - - // Private: Return a GeoJSON geomtry extracted from the record fields - // - _getGeometryFromRecord: function(doc){ - if (this.state.get('geomField')){ - var value = doc.get(this.state.get('geomField')); - if (typeof(value) === 'string'){ - // We *may* have a GeoJSON string representation - try { - value = $.parseJSON(value); - } catch(e) {} - } - if (typeof(value) === 'string') { - value = value.replace('(', '').replace(')', ''); - var parts = value.split(','); - var lat = this._parseCoordinateString(parts[0]); - var lon = this._parseCoordinateString(parts[1]); - - if (!isNaN(lon) && !isNaN(parseFloat(lat))) { - return { - "type": "Point", - "coordinates": [lon, lat] - }; - } else { - return null; - } - } else if (value && _.isArray(value)) { - // [ lon, lat ] - return { - "type": "Point", - "coordinates": [value[0], value[1]] - }; - } else if (value && value.lat) { - // of form { lat: ..., lon: ...} - return { - "type": "Point", - "coordinates": [value.lon || value.lng, value.lat] - }; - } - // We o/w assume that contents of the field are a valid GeoJSON object - return value; - } else if (this.state.get('lonField') && this.state.get('latField')){ - // We'll create a GeoJSON like point object from the two lat/lon fields - var lon = doc.get(this.state.get('lonField')); - var lat = doc.get(this.state.get('latField')); - lon = this._parseCoordinateString(lon); - lat = this._parseCoordinateString(lat); - - if (!isNaN(parseFloat(lon)) && !isNaN(parseFloat(lat))) { - return { - type: 'Point', - coordinates: [lon,lat] - }; - } - } - return null; - }, - - // Private: Check if there is a field with GeoJSON geometries or alternatively, - // two fields with lat/lon values. - // - // If not found, the user can define them via the UI form. - _setupGeometryField: function(){ - // should not overwrite if we have already set this (e.g. explicitly via state) - if (!this._geomReady()) { - this.state.set({ - geomField: this._checkField(this.geometryFieldNames), - latField: this._checkField(this.latitudeFieldNames), - lonField: this._checkField(this.longitudeFieldNames) - }); - this.menu.state.set(this.state.toJSON()); - } - }, - - // Private: Check if a field in the current model exists in the provided - // list of names. - // - // - _checkField: function(fieldNames){ - var field; - var modelFieldNames = this.model.fields.pluck('id'); - for (var i = 0; i < fieldNames.length; i++){ - for (var j = 0; j < modelFieldNames.length; j++){ - if (modelFieldNames[j].toLowerCase() == fieldNames[i].toLowerCase()) - return modelFieldNames[j]; - } - } - return null; - }, - - // Private: Zoom to map to current features extent if any, or to the full - // extent if none. - // - _zoomToFeatures: function(){ - var bounds = this.features.getBounds(); - if (bounds && bounds.getNorthEast() && bounds.getSouthWest()){ - this.map.fitBounds(bounds); - } else { - this.map.setView([0, 0], 2); - } - }, - - // Private: Sets up the Leaflet map control and the features layer. - // - // The map uses a base layer from [Stamen](http://maps.stamen.com) based - // on [OpenStreetMap data](http://openstreetmap.org) by default, but it can - // be configured passing the `mapTilesURL` and `mapTilesAttribution` options - // (`mapTilesSubdomains` is also supported), eg: - // - // view = new recline.View.Map({ - // model: dataset, - // mapTilesURL: '//{s}.tiles.mapbox.com/v4/mapbox.mapbox-streets-v7/{z}/{x}/{y}.png?access_token=pk.XXXX', - // mapTilesAttribution: '© MapBox etc..', - // mapTilesSubdomains: 'ab' - // }) - // - // - _setupMap: function(){ - var self = this; - this.map = new L.Map(this.$map.get(0)); - var mapUrl = this.options.mapTilesURL || 'https://stamen-tiles-{s}.a.ssl.fastly.net/terrain/{z}/{x}/{y}.png'; - var attribution = this.options.mapTilesAttribution ||'Map tiles by Stamen Design (CC BY 3.0). Data by OpenStreetMap (CC BY SA)'; - var subdomains = this.options.mapTilesSubdomains || 'abc'; - - var bg = new L.TileLayer(mapUrl, {maxZoom: 19, attribution: attribution, subdomains: subdomains}); - this.map.addLayer(bg); - - this.markers = new L.MarkerClusterGroup(this._clusterOptions); - - // rebind this (as needed in e.g. default case above) - this.geoJsonLayerOptions.pointToLayer = _.bind( - this.geoJsonLayerOptions.pointToLayer, - this); - this.features = new L.GeoJSON(null, this.geoJsonLayerOptions); - - this.map.setView([0, 0], 2); - - this.mapReady = true; - }, - - // Private: Helper function to select an option from a select list - // - _selectOption: function(id,value){ - var options = $('.' + id + ' > select > option'); - if (options){ - options.each(function(opt){ - if (this.value == value) { - $(this).attr('selected','selected'); - return false; - } - }); - } - } -}); - -my.MapMenu = Backbone.View.extend({ - className: 'editor', - - template: ' \ -
\ -
\ -
\ - \ - \ -
\ -
\ - \ -
\ - \ -
\ - \ -
\ - \ -
\ -
\ - \ -
\ -
\ - \ -
\ -
\ - \ - \ -
\ - \ -
\ - ', - - // Define here events for UI elements - events: { - 'click .editor-update-map': 'onEditorSubmit', - 'change .editor-field-type': 'onFieldTypeChange', - 'click #editor-auto-zoom': 'onAutoZoomChange', - 'click #editor-cluster': 'onClusteringChange' - }, - - initialize: function(options) { - var self = this; - _.bindAll(this, 'render'); - this.listenTo(this.model.fields, 'change', this.render); - this.state = new recline.Model.ObjectState(options.state); - this.listenTo(this.state, 'change', this.render); - this.render(); - }, - - // ### Public: Adds the necessary elements to the page. - // - // Also sets up the editor fields and the map if necessary. - render: function() { - var self = this; - var htmls = Mustache.render(this.template, this.model.toTemplateJSON()); - this.$el.html(htmls); - - if (this._geomReady() && this.model.fields.length){ - if (this.state.get('geomField')){ - this._selectOption('editor-geom-field',this.state.get('geomField')); - this.$el.find('#editor-field-type-geom').attr('checked','checked').change(); - } else{ - this._selectOption('editor-lon-field',this.state.get('lonField')); - this._selectOption('editor-lat-field',this.state.get('latField')); - this.$el.find('#editor-field-type-latlon').attr('checked','checked').change(); - } - } - if (this.state.get('autoZoom')) { - this.$el.find('#editor-auto-zoom').attr('checked', 'checked'); - } else { - this.$el.find('#editor-auto-zoom').removeAttr('checked'); - } - if (this.state.get('cluster')) { - this.$el.find('#editor-cluster').attr('checked', 'checked'); - } else { - this.$el.find('#editor-cluster').removeAttr('checked'); - } - return this; - }, - - _geomReady: function() { - return Boolean(this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField'))); - }, - - // ## UI Event handlers - // - - // Public: Update map with user options - // - // Right now the only configurable option is what field(s) contains the - // location information. - // - onEditorSubmit: function(e){ - e.preventDefault(); - if (this.$el.find('#editor-field-type-geom').attr('checked')){ - this.state.set({ - geomField: this.$el.find('.editor-geom-field > select > option:selected').val(), - lonField: null, - latField: null - }); - } else { - this.state.set({ - geomField: null, - lonField: this.$el.find('.editor-lon-field > select > option:selected').val(), - latField: this.$el.find('.editor-lat-field > select > option:selected').val() - }); - } - return false; - }, - - // Public: Shows the relevant select lists depending on the location field - // type selected. - // - onFieldTypeChange: function(e){ - if (e.target.value == 'geom'){ - this.$el.find('.editor-field-type-geom').show(); - this.$el.find('.editor-field-type-latlon').hide(); - } else { - this.$el.find('.editor-field-type-geom').hide(); - this.$el.find('.editor-field-type-latlon').show(); - } - }, - - onAutoZoomChange: function(e){ - this.state.set({autoZoom: !this.state.get('autoZoom')}); - }, - - onClusteringChange: function(e){ - this.state.set({cluster: !this.state.get('cluster')}); - }, - - // Private: Helper function to select an option from a select list - // - _selectOption: function(id,value){ - var options = this.$el.find('.' + id + ' > select > option'); - if (options){ - options.each(function(opt){ - if (this.value == value) { - $(this).attr('selected','selected'); - return false; - } - }); - } - } -}); - -})(jQuery, recline.View); -/*jshint multistr:true */ - -// Standard JS module setup -this.recline = this.recline || {}; -this.recline.View = this.recline.View || {}; - -(function($, my) { - "use strict"; -// ## MultiView -// -// Manage multiple views together along with query editor etc. Usage: -// -//
-// var myExplorer = new recline.View.MultiView({
-//   model: {{recline.Model.Dataset instance}}
-//   el: {{an existing dom element}}
-//   views: {{dataset views}}
-//   state: {{state configuration -- see below}}
-// });
-// 
-// -// ### Parameters -// -// **model**: (required) recline.model.Dataset instance. -// -// **el**: (required) DOM element to bind to. NB: the element already -// being in the DOM is important for rendering of some subviews (e.g. -// Graph). -// -// **views**: (optional) the dataset views (Grid, Graph etc) for -// MultiView to show. This is an array of view hashes. If not provided -// initialize with (recline.View.)Grid, Graph, and Map views (with obvious id -// and labels!). -// -//
-// var views = [
-//   {
-//     id: 'grid', // used for routing
-//     label: 'Grid', // used for view switcher
-//     view: new recline.View.Grid({
-//       model: dataset
-//     })
-//   },
-//   {
-//     id: 'graph',
-//     label: 'Graph',
-//     view: new recline.View.Graph({
-//       model: dataset
-//     })
-//   }
-// ];
-// 
-// -// **sidebarViews**: (optional) the sidebar views (Filters, Fields) for -// MultiView to show. This is an array of view hashes. If not provided -// initialize with (recline.View.)FilterEditor and Fields views (with obvious -// id and labels!). -// -//
-// var sidebarViews = [
-//   {
-//     id: 'filterEditor', // used for routing
-//     label: 'Filters', // used for view switcher
-//     view: new recline.View.FilterEditor({
-//       model: dataset
-//     })
-//   },
-//   {
-//     id: 'fieldsView',
-//     label: 'Fields',
-//     view: new recline.View.Fields({
-//       model: dataset
-//     })
-//   }
-// ];
-// 
-// -// **state**: standard state config for this view. This state is slightly -// special as it includes config of many of the subviews. -// -//
-// var state = {
-//     query: {dataset query state - see dataset.queryState object}
-//     'view-{id1}': {view-state for this view}
-//     'view-{id2}': {view-state for }
-//     ...
-//     // Explorer
-//     currentView: id of current view (defaults to first view if not specified)
-//     readOnly: (default: false) run in read-only mode
-// }
-// 
-// -// Note that at present we do *not* serialize information about the actual set -// of views in use -- e.g. those specified by the views argument -- but instead -// expect either that the default views are fine or that the client to have -// initialized the MultiView with the relevant views themselves. -my.MultiView = Backbone.View.extend({ - template: ' \ -
\ -
\ - \ -
\ - \ -
\ - {{recordCount}} records\ -
\ - \ -
\ -
\ -
\ -
\ -
\ - ', - events: { - 'click .menu-right button': '_onMenuClick', - 'click .navigation button': '_onSwitchView' - }, - - initialize: function(options) { - var self = this; - this._setupState(options.state); - - // Hash of 'page' views (i.e. those for whole page) keyed by page name - if (options.views) { - this.pageViews = options.views; - } else { - this.pageViews = [{ - id: 'grid', - label: 'Grid', - view: new my.SlickGrid({ - model: this.model, - state: this.state.get('view-grid') - }) - }, { - id: 'graph', - label: 'Graph', - view: new my.Graph({ - model: this.model, - state: this.state.get('view-graph') - }) - }, { - id: 'map', - label: 'Map', - view: new my.Map({ - model: this.model, - state: this.state.get('view-map') - }) - }, { - id: 'timeline', - label: 'Timeline', - view: new my.Timeline({ - model: this.model, - state: this.state.get('view-timeline') - }) - }]; - } - // Hashes of sidebar elements - if(options.sidebarViews) { - this.sidebarViews = options.sidebarViews; - } else { - this.sidebarViews = [{ - id: 'filterEditor', - label: 'Filters', - view: new my.FilterEditor({ - model: this.model - }) - }, { - id: 'fieldsView', - label: 'Fields', - view: new my.Fields({ - model: this.model - }) - }]; - } - // these must be called after pageViews are created - this.render(); - this._bindStateChanges(); - this._bindFlashNotifications(); - // now do updates based on state (need to come after render) - if (this.state.get('readOnly')) { - this.setReadOnly(); - } - if (this.state.get('currentView')) { - this.updateNav(this.state.get('currentView')); - } else { - this.updateNav(this.pageViews[0].id); - } - this._showHideSidebar(); - - this.listenTo(this.model, 'query:start', function() { - self.notify({loader: true, persist: true}); - }); - this.listenTo(this.model, 'query:done', function() { - self.clearNotifications(); - self.$el.find('.doc-count').text(self.model.recordCount || 'Unknown'); - }); - this.listenTo(this.model, 'query:fail', function(error) { - self.clearNotifications(); - var msg = ''; - if (typeof(error) == 'string') { - msg = error; - } else if (typeof(error) == 'object') { - if (error.title) { - msg = error.title + ': '; - } - if (error.message) { - msg += error.message; - } - } else { - msg = 'There was an error querying the backend'; - } - self.notify({message: msg, category: 'error', persist: true}); - }); - - // retrieve basic data like fields etc - // note this.model and dataset returned are the same - // TODO: set query state ...? - this.model.queryState.set(self.state.get('query'), {silent: true}); - }, - - setReadOnly: function() { - this.$el.addClass('recline-read-only'); - }, - - render: function() { - var tmplData = this.model.toTemplateJSON(); - tmplData.views = this.pageViews; - tmplData.sidebarViews = this.sidebarViews; - var template = Mustache.render(this.template, tmplData); - this.$el.html(template); - - // now create and append other views - var $dataViewContainer = this.$el.find('.data-view-container'); - var $dataSidebar = this.$el.find('.data-view-sidebar'); - - // the main views - _.each(this.pageViews, function(view, pageName) { - view.view.render(); - if (view.view.redraw) { - view.view.redraw(); - } - $dataViewContainer.append(view.view.el); - if (view.view.elSidebar) { - $dataSidebar.append(view.view.elSidebar); - } - }); - - _.each(this.sidebarViews, function(view) { - this['$'+view.id] = view.view.$el; - $dataSidebar.append(view.view.el); - }, this); - - this.pager = new recline.View.Pager({ - model: this.model - }); - this.$el.find('.recline-results-info').after(this.pager.el); - - this.queryEditor = new recline.View.QueryEditor({ - model: this.model.queryState - }); - this.$el.find('.query-editor-here').append(this.queryEditor.el); - - }, - - remove: function () { - _.each(this.pageViews, function (view) { - view.view.remove(); - }); - _.each(this.sidebarViews, function (view) { - view.view.remove(); - }); - this.pager.remove(); - this.queryEditor.remove(); - Backbone.View.prototype.remove.apply(this, arguments); - }, - - // hide the sidebar if empty - _showHideSidebar: function() { - var $dataSidebar = this.$el.find('.data-view-sidebar'); - var visibleChildren = $dataSidebar.children().filter(function() { - return $(this).css("display") != "none"; - }).length; - - if (visibleChildren > 0) { - $dataSidebar.show(); - } else { - $dataSidebar.hide(); - } - }, - - updateNav: function(pageName) { - this.$el.find('.navigation button').removeClass('active'); - var $el = this.$el.find('.navigation button[data-view="' + pageName + '"]'); - $el.addClass('active'); - - // add/remove sidebars and hide inactive views - _.each(this.pageViews, function(view, idx) { - if (view.id === pageName) { - view.view.$el.show(); - if (view.view.elSidebar) { - view.view.elSidebar.show(); - } - } else { - view.view.$el.hide(); - if (view.view.elSidebar) { - view.view.elSidebar.hide(); - } - if (view.view.hide) { - view.view.hide(); - } - } - }); - - this._showHideSidebar(); - - // call view.view.show after sidebar visibility has been determined so - // that views can correctly calculate their maximum width - _.each(this.pageViews, function(view, idx) { - if (view.id === pageName) { - if (view.view.show) { - view.view.show(); - } - } - }); - }, - - _onMenuClick: function(e) { - e.preventDefault(); - var action = $(e.target).attr('data-action'); - this['$'+action].toggle(); - this._showHideSidebar(); - }, - - _onSwitchView: function(e) { - e.preventDefault(); - var viewName = $(e.target).attr('data-view'); - this.updateNav(viewName); - this.state.set({currentView: viewName}); - }, - - // create a state object for this view and do the job of - // - // a) initializing it from both data passed in and other sources (e.g. hash url) - // - // b) ensure the state object is updated in responese to changes in subviews, query etc. - _setupState: function(initialState) { - var self = this; - // get data from the query string / hash url plus some defaults - var qs = my.parseHashQueryString(); - var query = qs.reclineQuery; - query = query ? JSON.parse(query) : self.model.queryState.toJSON(); - // backwards compatability (now named view-graph but was named graph) - var graphState = qs['view-graph'] || qs.graph; - graphState = graphState ? JSON.parse(graphState) : {}; - - // now get default data + hash url plus initial state and initial our state object with it - var stateData = _.extend({ - query: query, - 'view-graph': graphState, - backend: this.model.backend.__type__, - url: this.model.get('url'), - dataset: this.model.toJSON(), - currentView: null, - readOnly: false - }, - initialState); - this.state = new recline.Model.ObjectState(stateData); - }, - - _bindStateChanges: function() { - var self = this; - // finally ensure we update our state object when state of sub-object changes so that state is always up to date - this.listenTo(this.model.queryState, 'change', function() { - self.state.set({query: self.model.queryState.toJSON()}); - }); - _.each(this.pageViews, function(pageView) { - if (pageView.view.state && pageView.view.state.bind) { - var update = {}; - update['view-' + pageView.id] = pageView.view.state.toJSON(); - self.state.set(update); - self.listenTo(pageView.view.state, 'change', function() { - var update = {}; - update['view-' + pageView.id] = pageView.view.state.toJSON(); - // had problems where change not being triggered for e.g. grid view so let's do it explicitly - self.state.set(update, {silent: true}); - self.state.trigger('change'); - }); - } - }); - }, - - _bindFlashNotifications: function() { - var self = this; - _.each(this.pageViews, function(pageView) { - self.listenTo(pageView.view, 'recline:flash', function(flash) { - self.notify(flash); - }); - }); - }, - - // ### notify - // - // Create a notification (a div.alert in div.alert-messsages) using provided - // flash object. Flash attributes (all are optional): - // - // * message: message to show. - // * category: warning (default), success, error - // * persist: if true alert is persistent, o/w hidden after 3s (default = false) - // * loader: if true show loading spinner - notify: function(flash) { - var tmplData = _.extend({ - message: 'Loading', - category: 'warning', - loader: false - }, - flash - ); - var _template; - if (tmplData.loader) { - _template = ' \ -
\ - {{message}} \ -   \ -
'; - } else { - _template = ' \ -
Ɨ \ - {{message}} \ -
'; - } - var _templated = $(Mustache.render(_template, tmplData)); - _templated = $(_templated).appendTo($('.recline-data-explorer .alert-messages')); - if (!flash.persist) { - setTimeout(function() { - $(_templated).fadeOut(1000, function() { - $(this).remove(); - }); - }, 1000); - } - }, - - // ### clearNotifications - // - // Clear all existing notifications - clearNotifications: function() { - var $notifications = $('.recline-data-explorer .alert-messages .alert'); - $notifications.fadeOut(1500, function() { - $(this).remove(); - }); - } -}); - -// ### MultiView.restore -// -// Restore a MultiView instance from a serialized state including the associated dataset -// -// This inverts the state serialization process in Multiview -my.MultiView.restore = function(state) { - // hack-y - restoring a memory dataset does not mean much ... (but useful for testing!) - var datasetInfo; - if (state.backend === 'memory') { - datasetInfo = { - backend: 'memory', - records: [{stub: 'this is a stub dataset because we do not restore memory datasets'}] - }; - } else { - datasetInfo = _.extend({ - url: state.url, - backend: state.backend - }, - state.dataset - ); - } - var dataset = new recline.Model.Dataset(datasetInfo); - var explorer = new my.MultiView({ - model: dataset, - state: state - }); - return explorer; -}; - -// ## Miscellaneous Utilities -var urlPathRegex = /^([^?]+)(\?.*)?/; - -// Parse the Hash section of a URL into path and query string -my.parseHashUrl = function(hashUrl) { - var parsed = urlPathRegex.exec(hashUrl); - if (parsed === null) { - return {}; - } else { - return { - path: parsed[1], - query: parsed[2] || '' - }; - } -}; - -// Parse a URL query string (?xyz=abc...) into a dictionary. -my.parseQueryString = function(q) { - if (!q) { - return {}; - } - var urlParams = {}, - e, d = function (s) { - return unescape(s.replace(/\+/g, " ")); - }, - r = /([^&=]+)=?([^&]*)/g; - - if (q && q.length && q[0] === '?') { - q = q.slice(1); - } - while (e = r.exec(q)) { - // TODO: have values be array as query string allow repetition of keys - urlParams[d(e[1])] = d(e[2]); - } - return urlParams; -}; - -// Parse the query string out of the URL hash -my.parseHashQueryString = function() { - var q = my.parseHashUrl(window.location.hash).query; - return my.parseQueryString(q); -}; - -// Compse a Query String -my.composeQueryString = function(queryParams) { - var queryString = '?'; - var items = []; - $.each(queryParams, function(key, value) { - if (typeof(value) === 'object') { - value = JSON.stringify(value); - } - items.push(key + '=' + encodeURIComponent(value)); - }); - queryString += items.join('&'); - return queryString; -}; - -my.getNewHashForQueryString = function(queryParams) { - var queryPart = my.composeQueryString(queryParams); - if (window.location.hash) { - // slice(1) to remove # at start - return window.location.hash.split('?')[0].slice(1) + queryPart; - } else { - return queryPart; - } -}; - -my.setHashQueryString = function(queryParams) { - window.location.hash = my.getNewHashForQueryString(queryParams); -}; - -})(jQuery, recline.View); - -/*jshint multistr:true */ - -this.recline = this.recline || {}; -this.recline.View = this.recline.View || {}; - -(function($, my) { - "use strict"; - -// ## SlickGrid Dataset View -// -// Provides a tabular view on a Dataset, based on SlickGrid. -// -// https://github.com/mleibman/SlickGrid -// -// Initialize it with a `recline.Model.Dataset`. -// -// Additional options to drive SlickGrid grid can be given through state. -// The following keys allow for customization: -// * gridOptions: to add options at grid level -// * columnsEditor: to add editor for editable columns -// -// For example: -// var grid = new recline.View.SlickGrid({ -// model: dataset, -// el: $el, -// state: { -// gridOptions: { -// editable: true, -// enableAddRow: true -// // Enable support for row delete -// enabledDelRow: true, -// // Enable support for row Reorder -// enableReOrderRow:true, -// ... -// }, -// columnsEditor: [ -// {column: 'date', editor: Slick.Editors.Date }, -// {column: 'title', editor: Slick.Editors.Text} -// ] -// } -// }); -//// NB: you need an explicit height on the element for slickgrid to work -my.SlickGrid = Backbone.View.extend({ - initialize: function(modelEtc) { - var self = this; - this.$el.addClass('recline-slickgrid'); - - // Template for row delete menu , change it if you don't love - this.templates = { - "deleterow" : '' - }; - - _.bindAll(this, 'render', 'onRecordChanged'); - this.listenTo(this.model.records, 'add remove reset', this.render); - this.listenTo(this.model.records, 'change', this.onRecordChanged); - var state = _.extend({ - hiddenColumns: [], - columnsOrder: [], - columnsSort: {}, - columnsWidth: [], - columnsEditor: [], - options: {}, - fitColumns: false - }, modelEtc.state - - ); - this.state = new recline.Model.ObjectState(state); - this._slickHandler = new Slick.EventHandler(); - - //add menu for new row , check if enableAddRow is set to true or not set - if(this.state.get("gridOptions") - && this.state.get("gridOptions").enabledAddRow != undefined - && this.state.get("gridOptions").enabledAddRow == true ){ - this.editor = new my.GridControl() - this.elSidebar = this.editor.$el - this.listenTo(this.editor.state, 'change', function(){ - this.model.records.add(new recline.Model.Record()) - }); - } - }, - - onRecordChanged: function(record) { - // Ignore if the grid is not yet drawn - if (!this.grid) { - return; - } - // Let's find the row corresponding to the index - var row_index = this.grid.getData().getModelRow( record ); - this.grid.invalidateRow(row_index); - this.grid.getData().updateItem(record, row_index); - this.grid.render(); - }, - - render: function() { - var self = this; - var options = _.extend({ - enableCellNavigation: true, - enableColumnReorder: true, - explicitInitialization: true, - syncColumnCellResize: true, - forceFitColumns: this.state.get('fitColumns') - }, self.state.get('gridOptions')); - - // We need all columns, even the hidden ones, to show on the column picker - var columns = []; - - // custom formatter as default one escapes html - // plus this way we distinguish between rendering/formatting and computed value (so e.g. sort still works ...) - // row = row index, cell = cell index, value = value, columnDef = column definition, dataContext = full row values - var formatter = function(row, cell, value, columnDef, dataContext) { - if(columnDef.id == "del"){ - return self.templates.deleterow - } - var field = self.model.fields.get(columnDef.id); - if (field.renderer) { - return field.renderer(value, field, dataContext); - } else { - return value - } - }; - - // we need to be sure that user is entering a valid input , for exemple if - // field is date type and field.format ='YY-MM-DD', we should be sure that - // user enter a correct value - var validator = function(field) { - return function(value){ - if (field.type == "date" && isNaN(Date.parse(value))){ - return { - valid: false, - msg: "A date is required, check field field-date-format" - }; - } else { - return {valid: true, msg :null } - } - } - }; - - // Add column for row reorder support - if (this.state.get("gridOptions") && this.state.get("gridOptions").enableReOrderRow == true) { - columns.push({ - id: "#", - name: "", - width: 22, - behavior: "selectAndMove", - selectable: false, - resizable: false, - cssClass: "recline-cell-reorder" - }) - } - // Add column for row delete support - if (this.state.get("gridOptions") && this.state.get("gridOptions").enabledDelRow == true) { - columns.push({ - id: 'del', - name: '', - field: 'del', - sortable: true, - width: 38, - formatter: formatter, - validator:validator - }) - } - - function sanitizeFieldName(name) { - return $('
').text(name).html(); - } - - _.each(this.model.fields.toJSON(),function(field){ - var column = { - id: field.id, - name: sanitizeFieldName(field.label), - field: field.id, - sortable: true, - minWidth: 80, - formatter: formatter, - validator:validator(field) - }; - var widthInfo = _.find(self.state.get('columnsWidth'),function(c){return c.column === field.id;}); - if (widthInfo){ - column.width = widthInfo.width; - } - var editInfo = _.find(self.state.get('columnsEditor'),function(c){return c.column === field.id;}); - if (editInfo){ - column.editor = editInfo.editor; - } else { - // guess editor type - var typeToEditorMap = { - 'string': Slick.Editors.LongText, - 'integer': Slick.Editors.IntegerEditor, - 'number': Slick.Editors.Text, - // TODO: need a way to ensure we format date in the right way - // Plus what if dates are in distant past or future ... (?) - // 'date': Slick.Editors.DateEditor, - 'date': Slick.Editors.Text, - 'boolean': Slick.Editors.YesNoSelectEditor - // TODO: (?) percent ... - }; - if (field.type in typeToEditorMap) { - column.editor = typeToEditorMap[field.type] - } else { - column.editor = Slick.Editors.LongText; - } - } - columns.push(column); - }); - // Restrict the visible columns - var visibleColumns = _.filter(columns, function(column) { - return _.indexOf(self.state.get('hiddenColumns'), column.id) === -1; - }); - // Order them if there is ordering info on the state - if (this.state.get('columnsOrder') && this.state.get('columnsOrder').length > 0) { - visibleColumns = visibleColumns.sort(function(a,b){ - return _.indexOf(self.state.get('columnsOrder'),a.id) > _.indexOf(self.state.get('columnsOrder'),b.id) ? 1 : -1; - }); - columns = columns.sort(function(a,b){ - return _.indexOf(self.state.get('columnsOrder'),a.id) > _.indexOf(self.state.get('columnsOrder'),b.id) ? 1 : -1; - }); - } - - // Move hidden columns to the end, so they appear at the bottom of the - // column picker - var tempHiddenColumns = []; - for (var i = columns.length -1; i >= 0; i--){ - if (_.indexOf(_.pluck(visibleColumns,'id'),columns[i].id) === -1){ - tempHiddenColumns.push(columns.splice(i,1)[0]); - } - } - columns = columns.concat(tempHiddenColumns); - - // Transform a model object into a row - function toRow(m) { - var row = {}; - self.model.fields.each(function(field) { - var render = ""; - //when adding row from slickgrid the field value is undefined - if(!_.isUndefined(m.getFieldValueUnrendered(field))){ - render =m.getFieldValueUnrendered(field) - } - row[field.id] = render - }); - return row; - } - - function RowSet() { - var models = []; - var rows = []; - - this.push = function(model, row) { - models.push(model); - rows.push(row); - }; - - this.getLength = function() {return rows.length; }; - this.getItem = function(index) {return rows[index];}; - this.getItemMetadata = function(index) {return {};}; - this.getModel = function(index) {return models[index];}; - this.getModelRow = function(m) {return _.indexOf(models, m);}; - this.updateItem = function(m,i) { - rows[i] = toRow(m); - models[i] = m; - }; - } - - var data = new RowSet(); - - this.model.records.each(function(doc){ - data.push(doc, toRow(doc)); - }); - - this.grid = new Slick.Grid(this.el, data, visibleColumns, options); - // Column sorting - var sortInfo = this.model.queryState.get('sort'); - if (sortInfo){ - var column = sortInfo[0].field; - var sortAsc = sortInfo[0].order !== 'desc'; - this.grid.setSortColumn(column, sortAsc); - } - - if (this.state.get("gridOptions") && this.state.get("gridOptions").enableReOrderRow) { - this._setupRowReordering(); - } - - this._slickHandler.subscribe(this.grid.onSort, function(e, args){ - var order = (args.sortAsc) ? 'asc':'desc'; - var sort = [{ - field: args.sortCol.field, - order: order - }]; - self.model.query({sort: sort}); - }); - - this._slickHandler.subscribe(this.grid.onColumnsReordered, function(e, args){ - self.state.set({columnsOrder: _.pluck(self.grid.getColumns(),'id')}); - }); - - this.grid.onColumnsResized.subscribe(function(e, args){ - var columns = args.grid.getColumns(); - var defaultColumnWidth = args.grid.getOptions().defaultColumnWidth; - var columnsWidth = []; - _.each(columns,function(column){ - if (column.width != defaultColumnWidth){ - columnsWidth.push({column:column.id,width:column.width}); - } - }); - self.state.set({columnsWidth:columnsWidth}); - }); - - this._slickHandler.subscribe(this.grid.onCellChange, function (e, args) { - // We need to change the model associated value - var grid = args.grid; - var model = data.getModel(args.row); - var field = grid.getColumns()[args.cell].id; - var v = {}; - v[field] = args.item[field]; - model.set(v); - }); - this._slickHandler.subscribe(this.grid.onClick,function(e, args){ - //try catch , because this fail in qunit , but no - //error on browser. - try{e.preventDefault()}catch(e){} - - // The cell of grid that handle row delete is The first cell (0) if - // The grid ReOrder is not present ie enableReOrderRow == false - // else it is The the second cell (1) , because The 0 is now cell - // that handle row Reoder. - var cell =0 - if(self.state.get("gridOptions") - && self.state.get("gridOptions").enableReOrderRow != undefined - && self.state.get("gridOptions").enableReOrderRow == true ){ - cell =1 - } - if (args.cell == cell && self.state.get("gridOptions").enabledDelRow == true){ - // We need to delete the associated model - var model = data.getModel(args.row); - model.destroy() - } - }) ; - var columnpicker = new Slick.Controls.ColumnPicker(columns, this.grid, - _.extend(options,{state:this.state})); - if (self.visible){ - self.grid.init(); - self.rendered = true; - } else { - // Defer rendering until the view is visible - self.rendered = false; - } - return this; - }, - - // Row reordering support based on - // https://github.com/mleibman/SlickGrid/blob/gh-pages/examples/example9-row-reordering.html - _setupRowReordering: function() { - var self = this; - self.grid.setSelectionModel(new Slick.RowSelectionModel()); - - var moveRowsPlugin = new Slick.RowMoveManager({ - cancelEditOnDrag: true - }); - - moveRowsPlugin.onBeforeMoveRows.subscribe(function (e, data) { - for (var i = 0; i < data.rows.length; i++) { - // no point in moving before or after itself - if (data.rows[i] == data.insertBefore || data.rows[i] == data.insertBefore - 1) { - e.stopPropagation(); - return false; - } - } - return true; - }); - - moveRowsPlugin.onMoveRows.subscribe(function (e, args) { - var extractedRows = [], left, right; - var rows = args.rows; - var insertBefore = args.insertBefore; - - var data = self.model.records.toJSON() - left = data.slice(0, insertBefore); - right= data.slice(insertBefore, data.length); - - rows.sort(function(a,b) { return a-b; }); - - for (var i = 0; i < rows.length; i++) { - extractedRows.push(data[rows[i]]); - } - - rows.reverse(); - - for (var i = 0; i < rows.length; i++) { - var row = rows[i]; - if (row < insertBefore) { - left.splice(row, 1); - } else { - right.splice(row - insertBefore, 1); - } - } - - data = left.concat(extractedRows.concat(right)); - var selectedRows = []; - for (var i = 0; i < rows.length; i++) - selectedRows.push(left.length + i); - - self.model.records.reset(data) - - }); - //register The plugin to handle row Reorder - if(this.state.get("gridOptions") && this.state.get("gridOptions").enableReOrderRow) { - self.grid.registerPlugin(moveRowsPlugin); - } - }, - - remove: function () { - this._slickHandler.unsubscribeAll(); - Backbone.View.prototype.remove.apply(this, arguments); - }, - - show: function() { - // If the div is hidden, SlickGrid will calculate wrongly some - // sizes so we must render it explicitly when the view is visible - if (!this.rendered){ - if (!this.grid){ - this.render(); - } - this.grid.init(); - this.rendered = true; - } - this.visible = true; - }, - - hide: function() { - this.visible = false; - } -}); - -// Add new grid Control to display a new row add menu bouton -// It display a simple side-bar menu ,for user to add new -// row to grid -my.GridControl= Backbone.View.extend({ - className: "recline-row-add", - // Template for row edit menu , change it if you don't love - template: '

', - - initialize: function(options){ - var self = this; - _.bindAll(this, 'render'); - this.state = new recline.Model.ObjectState(); - this.render(); - }, - - render: function() { - var self = this; - this.$el.html(this.template) - }, - - events : { - "click .recline-row-add" : "addNewRow" - }, - - addNewRow : function(e){ - e.preventDefault() - this.state.trigger("change") - } -}); - -})(jQuery, recline.View); - -/* -* Context menu for the column picker, adapted from -* http://mleibman.github.com/SlickGrid/examples/example-grouping -* -*/ -(function ($) { - function SlickColumnPicker(columns, grid, options) { - var $menu; - var columnCheckboxes; - - var defaults = { - fadeSpeed:250 - }; - - function init() { - grid.onHeaderContextMenu.subscribe(handleHeaderContextMenu); - options = $.extend({}, defaults, options); - - $menu = $('
',templateSeriesEditor:'
',events:{"change form select":"onEditorSubmit","click .editor-add":"_onAddSeries","click .action-remove-series":"removeSeries"},initialize:function(options){var self=this;_.bindAll(this,"render");this.listenTo(this.model.fields,"reset add",this.render);this.state=new recline.Model.ObjectState(options.state);this.render()},render:function(){var self=this;var tmplData=this.model.toTemplateJSON();var htmls=Mustache.render(this.template,tmplData);this.$el.html(htmls);if(this.state.get("graphType")){this._selectOption(".editor-type",this.state.get("graphType"))}if(this.state.get("group")){this._selectOption(".editor-group",this.state.get("group"))}var tmpSeries=[""];if(this.state.get("series").length>0){tmpSeries=this.state.get("series")}_.each(tmpSeries,function(series,idx){self.addSeries(idx);self._selectOption(".editor-series.js-series-"+idx,series)});return this},_selectOption:function(id,value){var options=this.$el.find(id+" select > option");if(options){options.each(function(opt){if(this.value==value){$(this).attr("selected","selected");return false}})}},onEditorSubmit:function(e){var select=this.$el.find(".editor-group select");var $editor=this;var $series=this.$el.find(".editor-series select");var series=$series.map(function(){return $(this).val()});var updatedState={series:$.makeArray(series),group:this.$el.find(".editor-group select").val(),graphType:this.$el.find(".editor-type select").val()};this.state.set(updatedState)},addSeries:function(idx){var data=_.extend({seriesIndex:idx,seriesName:String.fromCharCode(idx+64+1)},this.model.toTemplateJSON());var htmls=Mustache.render(this.templateSeriesEditor,data);this.$el.find(".editor-series-group").append(htmls);return this},_onAddSeries:function(e){e.preventDefault();this.addSeries(this.state.get("series").length)},removeSeries:function(e){e.preventDefault();var $el=$(e.target);$el.parent().parent().remove();this.onEditorSubmit()}})})(jQuery,recline.View);this.recline=this.recline||{};this.recline.View=this.recline.View||{};this.recline.View.Graph=this.recline.View.Flot;this.recline.View.GraphControls=this.recline.View.FlotControls;this.recline=this.recline||{};this.recline.View=this.recline.View||{};(function($,my){"use strict";my.Grid=Backbone.View.extend({tagName:"div",className:"recline-grid-container",initialize:function(modelEtc){var self=this;_.bindAll(this,"render","onHorizontalScroll");this.listenTo(this.model.records,"add reset remove",this.render);this.tempState={};var state=_.extend({hiddenFields:[]},modelEtc.state);this.state=new recline.Model.ObjectState(state)},events:{},setColumnSort:function(order){var sort=[{}];sort[0][this.tempState.currentColumn]={order:order};this.model.query({sort:sort})},hideColumn:function(){var hiddenFields=this.state.get("hiddenFields");hiddenFields.push(this.tempState.currentColumn);this.state.set({hiddenFields:hiddenFields});this.state.trigger("change");this.render()},showColumn:function(e){var hiddenFields=_.without(this.state.get("hiddenFields"),$(e.target).data("column"));this.state.set({hiddenFields:hiddenFields});this.render()},onHorizontalScroll:function(e){var currentScroll=$(e.target).scrollLeft();this.$el.find(".recline-grid thead tr").scrollLeft(currentScroll)},template:'
{{#fields}} {{/fields}}
{{label}}
',toTemplateJSON:function(){var self=this;var modelData=this.model.toJSON();modelData.notEmpty=this.fields.length>0;modelData.fields=this.fields.map(function(field){return field.toJSON()});modelData.lastHeaderWidth=this.scrollbarDimensions.width-2;return modelData},render:function(){var self=this;this.fields=new recline.Model.FieldList(this.model.fields.filter(function(field){return _.indexOf(self.state.get("hiddenFields"),field.id)==-1}));this.scrollbarDimensions=this.scrollbarDimensions||this._scrollbarSize();var numFields=this.fields.length;var fullWidth=self.$el.width()-20-10*numFields-this.scrollbarDimensions.width;var width=parseInt(Math.max(50,fullWidth/numFields),10);var remainder=Math.max(fullWidth-numFields*width,0);this.fields.each(function(field,idx){if(idx===0){field.set({width:width+remainder})}else{field.set({width:width})}});var htmls=Mustache.render(this.template,this.toTemplateJSON());this.$el.html(htmls);this.model.records.forEach(function(doc){var tr=$("");self.$el.find("tbody").append(tr);var newView=new my.GridRow({model:doc,el:tr,fields:self.fields});newView.render()});var $tbody=this.$el.find("tbody")[0];if($tbody.scrollHeight<=$tbody.offsetHeight){this.$el.find("th.last-header").hide()}this.$el.find(".recline-grid").toggleClass("no-hidden",self.state.get("hiddenFields").length===0);this.$el.find(".recline-grid tbody").scroll(this.onHorizontalScroll);return this},_scrollbarSize:function(){var $c=$("
").appendTo("body");var dim={width:$c.width()-$c[0].clientWidth+1,height:$c.height()-$c[0].clientHeight};$c.remove();return dim}});my.GridRow=Backbone.View.extend({initialize:function(initData){_.bindAll(this,"render");this._fields=initData.fields;this.listenTo(this.model,"change",this.render)},template:' {{#cells}}
 
{{{value}}}
{{/cells}} ',events:{"click .data-table-cell-edit":"onEditClick","click .data-table-cell-editor .okButton":"onEditorOK","click .data-table-cell-editor .cancelButton":"onEditorCancel"},toTemplateJSON:function(){var self=this;var doc=this.model;var cellData=this._fields.map(function(field){return{field:field.id,width:field.get("width"),value:doc.getFieldValue(field)}});return{id:this.id,cells:cellData}},render:function(){this.$el.attr("data-id",this.model.id); -var html=Mustache.render(this.template,this.toTemplateJSON());this.$el.html(html);return this},cellEditorTemplate:' ',onEditClick:function(e){var editing=this.$el.find(".data-table-cell-editor-editor");if(editing.length>0){editing.parents(".data-table-cell-value").html(editing.text()).siblings(".data-table-cell-edit").removeClass("hidden")}$(e.target).addClass("hidden");var cell=$(e.target).siblings(".data-table-cell-value");cell.data("previousContents",cell.text());var templated=Mustache.render(this.cellEditorTemplate,{value:cell.text()});cell.html(templated)},onEditorOK:function(e){var self=this;var cell=$(e.target);var rowId=cell.parents("tr").attr("data-id");var field=cell.parents("td").attr("data-field");var newValue=cell.parents(".data-table-cell-editor").find(".data-table-cell-editor-editor").val();var newData={};newData[field]=newValue;this.model.set(newData);this.trigger("recline:flash",{message:"Updating row...",loader:true});this.model.save().then(function(response){this.trigger("recline:flash",{message:"Row updated successfully",category:"success"})}).fail(function(){this.trigger("recline:flash",{message:"Error saving row",category:"error",persist:true})})},onEditorCancel:function(e){var cell=$(e.target).parents(".data-table-cell-value");cell.html(cell.data("previousContents")).siblings(".data-table-cell-edit").removeClass("hidden")}})})(jQuery,recline.View);this.recline=this.recline||{};this.recline.View=this.recline.View||{};(function($,my){"use strict";my.Map=Backbone.View.extend({template:'
',latitudeFieldNames:["lat","latitude"],longitudeFieldNames:["lon","longitude"],geometryFieldNames:["geojson","geom","the_geom","geometry","spatial","location","geo","lonlat"],initialize:function(options){var self=this;this.visible=this.$el.is(":visible");this.mapReady=false;this.map=null;var stateData=_.extend({geomField:null,lonField:null,latField:null,autoZoom:true,cluster:false},options.state);this.state=new recline.Model.ObjectState(stateData);this._clusterOptions={zoomToBoundsOnClick:true,maxClusterRadius:80,singleMarkerMode:false,skipDuplicateAddTesting:true,animateAddingMarkers:false};this.listenTo(this.model.fields,"change",function(){self._setupGeometryField();self.render()});this.listenTo(this.model.records,"add",function(doc){self.redraw("add",doc)});this.listenTo(this.model.records,"change",function(doc){self.redraw("remove",doc);self.redraw("add",doc)});this.listenTo(this.model.records,"remove",function(doc){self.redraw("remove",doc)});this.listenTo(this.model.records,"reset",function(){self.redraw("reset")});this.menu=new my.MapMenu({model:this.model,state:this.state.toJSON()});this.listenTo(this.menu.state,"change",function(){self.state.set(self.menu.state.toJSON());self.redraw()});this.listenTo(this.state,"change",function(){self.redraw()});this.elSidebar=this.menu.$el},infobox:function(record){var html="";for(var key in record.attributes){if(!(this.state.get("geomField")&&key==this.state.get("geomField"))){html+="
"+key+": "+record.attributes[key]+"
"}}return html},geoJsonLayerOptions:{pointToLayer:function(feature,latlng){var marker=new L.Marker(latlng);marker.bindPopup(feature.properties.popupContent);this.markers.addLayer(marker);return marker},onEachFeature:function(feature,layer){if(feature.properties&&feature.properties.popupContent){layer.bindPopup(feature.properties.popupContent)}}},render:function(){var self=this;var htmls=Mustache.render(this.template,this.model.toTemplateJSON());this.$el.html(htmls);this.$map=this.$el.find(".panel.map");this.redraw();return this},redraw:function(action,doc){var self=this;action=action||"refresh";if(!self._geomReady()){self._setupGeometryField()}if(!self.mapReady){self._setupMap()}if(this._geomReady()&&this.mapReady){this.map.removeLayer(this.features);this.map.removeLayer(this.markers);var countBefore=0;this.features.eachLayer(function(){countBefore++});if(action=="refresh"||action=="reset"){this.features.clearLayers();this.map.removeLayer(this.markers);this.markers=new L.MarkerClusterGroup(this._clusterOptions);this._add(this.model.records.models)}else if(action=="add"&&doc){this._add(doc)}else if(action=="remove"&&doc){this._remove(doc)}if(this.state.get("cluster")){this.map.addLayer(this.markers)}else{this.map.addLayer(this.features)}if(this.state.get("autoZoom")){if(this.visible){this._zoomToFeatures()}else{this._zoomPending=true}}}},show:function(){if(this.map){this.map.invalidateSize();if(this._zoomPending&&this.state.get("autoZoom")){this._zoomToFeatures();this._zoomPending=false}}this.visible=true},hide:function(){this.visible=false},_geomReady:function(){return Boolean(this.state.get("geomField")||this.state.get("latField")&&this.state.get("lonField"))},_add:function(docs){var self=this;if(!(docs instanceof Array))docs=[docs];var count=0;var wrongSoFar=0;_.every(docs,function(doc){count+=1;var feature=self._getGeometryFromRecord(doc);if(typeof feature==="undefined"||feature===null){return true}else if(feature instanceof Object){feature.properties={popupContent:self.infobox(doc),cid:doc.cid};try{self.features.addData(feature)}catch(except){wrongSoFar+=1;var msg="Wrong geometry value";if(except.message)msg+=" ("+except.message+")";if(wrongSoFar<=10){self.trigger("recline:flash",{message:msg,category:"error"})}}}else{wrongSoFar+=1;if(wrongSoFar<=10){self.trigger("recline:flash",{message:"Wrong geometry value",category:"error"})}}return true})},_remove:function(docs){var self=this;if(!(docs instanceof Array))docs=[docs];_.each(docs,function(doc){for(var key in self.features._layers){if(self.features._layers[key].feature.geometry.properties.cid==doc.cid){self.features.removeLayer(self.features._layers[key])}}})},_parseCoordinateString:function(coord){if(typeof coord!="string"){return parseFloat(coord)}var dms=coord.split(/[^-?\.\d\w]+/);var deg=0;var m=0;var toDeg=[1,60,3600];var i;for(i=0;i select > option");if(options){options.each(function(opt){if(this.value==value){$(this).attr("selected","selected");return false}})}}});my.MapMenu=Backbone.View.extend({className:"editor",template:'
',events:{"click .editor-update-map":"onEditorSubmit","change .editor-field-type":"onFieldTypeChange","click #editor-auto-zoom":"onAutoZoomChange","click #editor-cluster":"onClusteringChange"},initialize:function(options){var self=this;_.bindAll(this,"render");this.listenTo(this.model.fields,"change",this.render);this.state=new recline.Model.ObjectState(options.state);this.listenTo(this.state,"change",this.render);this.render()},render:function(){var self=this;var htmls=Mustache.render(this.template,this.model.toTemplateJSON());this.$el.html(htmls);if(this._geomReady()&&this.model.fields.length){if(this.state.get("geomField")){this._selectOption("editor-geom-field",this.state.get("geomField"));this.$el.find("#editor-field-type-geom").attr("checked","checked").change()}else{this._selectOption("editor-lon-field",this.state.get("lonField"));this._selectOption("editor-lat-field",this.state.get("latField"));this.$el.find("#editor-field-type-latlon").attr("checked","checked").change()}}if(this.state.get("autoZoom")){this.$el.find("#editor-auto-zoom").attr("checked","checked")}else{this.$el.find("#editor-auto-zoom").removeAttr("checked")}if(this.state.get("cluster")){this.$el.find("#editor-cluster").attr("checked","checked")}else{this.$el.find("#editor-cluster").removeAttr("checked")}return this},_geomReady:function(){return Boolean(this.state.get("geomField")||this.state.get("latField")&&this.state.get("lonField"))},onEditorSubmit:function(e){e.preventDefault();if(this.$el.find("#editor-field-type-geom").attr("checked")){this.state.set({geomField:this.$el.find(".editor-geom-field > select > option:selected").val(),lonField:null,latField:null})}else{this.state.set({geomField:null,lonField:this.$el.find(".editor-lon-field > select > option:selected").val(),latField:this.$el.find(".editor-lat-field > select > option:selected").val()})}return false},onFieldTypeChange:function(e){if(e.target.value=="geom"){this.$el.find(".editor-field-type-geom").show();this.$el.find(".editor-field-type-latlon").hide()}else{this.$el.find(".editor-field-type-geom").hide();this.$el.find(".editor-field-type-latlon").show()}},onAutoZoomChange:function(e){this.state.set({autoZoom:!this.state.get("autoZoom")})},onClusteringChange:function(e){this.state.set({cluster:!this.state.get("cluster")})},_selectOption:function(id,value){var options=this.$el.find("."+id+" > select > option");if(options){options.each(function(opt){if(this.value==value){$(this).attr("selected","selected");return false}})}}})})(jQuery,recline.View);this.recline=this.recline||{};this.recline.View=this.recline.View||{};(function($,my){"use strict";my.MultiView=Backbone.View.extend({template:'
{{recordCount}} records
',events:{"click .menu-right button":"_onMenuClick","click .navigation button":"_onSwitchView"},initialize:function(options){var self=this;this._setupState(options.state);if(options.views){this.pageViews=options.views}else{this.pageViews=[{id:"grid",label:"Grid",view:new my.SlickGrid({model:this.model,state:this.state.get("view-grid")})},{id:"graph",label:"Graph",view:new my.Graph({model:this.model,state:this.state.get("view-graph")})},{id:"map",label:"Map",view:new my.Map({model:this.model,state:this.state.get("view-map")})},{id:"timeline",label:"Timeline",view:new my.Timeline({model:this.model,state:this.state.get("view-timeline")})}]}if(options.sidebarViews){this.sidebarViews=options.sidebarViews}else{this.sidebarViews=[{id:"filterEditor",label:"Filters",view:new my.FilterEditor({model:this.model})},{id:"fieldsView",label:"Fields",view:new my.Fields({model:this.model})}]}this.render();this._bindStateChanges();this._bindFlashNotifications();if(this.state.get("readOnly")){this.setReadOnly()}if(this.state.get("currentView")){this.updateNav(this.state.get("currentView"))}else{this.updateNav(this.pageViews[0].id)}this._showHideSidebar();this.listenTo(this.model,"query:start",function(){self.notify({loader:true,persist:true})});this.listenTo(this.model,"query:done",function(){self.clearNotifications();self.$el.find(".doc-count").text(self.model.recordCount||"Unknown")});this.listenTo(this.model,"query:fail",function(error){self.clearNotifications();var msg="";if(typeof error=="string"){msg=error}else if(typeof error=="object"){if(error.title){msg=error.title+": "}if(error.message){msg+=error.message}}else{msg="There was an error querying the backend"}self.notify({message:msg,category:"error",persist:true})});this.model.queryState.set(self.state.get("query"),{silent:true})},setReadOnly:function(){this.$el.addClass("recline-read-only")},render:function(){var tmplData=this.model.toTemplateJSON();tmplData.views=this.pageViews;tmplData.sidebarViews=this.sidebarViews;var template=Mustache.render(this.template,tmplData);this.$el.html(template);var $dataViewContainer=this.$el.find(".data-view-container");var $dataSidebar=this.$el.find(".data-view-sidebar");_.each(this.pageViews,function(view,pageName){view.view.render();if(view.view.redraw){view.view.redraw()}$dataViewContainer.append(view.view.el);if(view.view.elSidebar){$dataSidebar.append(view.view.elSidebar)}});_.each(this.sidebarViews,function(view){this["$"+view.id]=view.view.$el;$dataSidebar.append(view.view.el)},this);this.pager=new recline.View.Pager({model:this.model});this.$el.find(".recline-results-info").after(this.pager.el);this.queryEditor=new recline.View.QueryEditor({model:this.model.queryState});this.$el.find(".query-editor-here").append(this.queryEditor.el)},remove:function(){_.each(this.pageViews,function(view){view.view.remove()});_.each(this.sidebarViews,function(view){view.view.remove()});this.pager.remove();this.queryEditor.remove();Backbone.View.prototype.remove.apply(this,arguments)},_showHideSidebar:function(){var $dataSidebar=this.$el.find(".data-view-sidebar");var visibleChildren=$dataSidebar.children().filter(function(){return $(this).css("display")!="none"}).length;if(visibleChildren>0){$dataSidebar.show()}else{$dataSidebar.hide()}},updateNav:function(pageName){this.$el.find(".navigation button").removeClass("active");var $el=this.$el.find('.navigation button[data-view="'+pageName+'"]');$el.addClass("active");_.each(this.pageViews,function(view,idx){if(view.id===pageName){view.view.$el.show();if(view.view.elSidebar){view.view.elSidebar.show()}}else{view.view.$el.hide();if(view.view.elSidebar){view.view.elSidebar.hide()}if(view.view.hide){view.view.hide()}}});this._showHideSidebar();_.each(this.pageViews,function(view,idx){if(view.id===pageName){if(view.view.show){view.view.show()}}})},_onMenuClick:function(e){e.preventDefault();var action=$(e.target).attr("data-action");this["$"+action].toggle();this._showHideSidebar()},_onSwitchView:function(e){e.preventDefault();var viewName=$(e.target).attr("data-view");this.updateNav(viewName);this.state.set({currentView:viewName})},_setupState:function(initialState){var self=this;var qs=my.parseHashQueryString();var query=qs.reclineQuery;query=query?JSON.parse(query):self.model.queryState.toJSON();var graphState=qs["view-graph"]||qs.graph;graphState=graphState?JSON.parse(graphState):{};var stateData=_.extend({query:query,"view-graph":graphState,backend:this.model.backend.__type__,url:this.model.get("url"),dataset:this.model.toJSON(),currentView:null,readOnly:false},initialState);this.state=new recline.Model.ObjectState(stateData)},_bindStateChanges:function(){var self=this;this.listenTo(this.model.queryState,"change",function(){self.state.set({query:self.model.queryState.toJSON()})});_.each(this.pageViews,function(pageView){if(pageView.view.state&&pageView.view.state.bind){var update={};update["view-"+pageView.id]=pageView.view.state.toJSON();self.state.set(update);self.listenTo(pageView.view.state,"change",function(){var update={};update["view-"+pageView.id]=pageView.view.state.toJSON();self.state.set(update,{silent:true});self.state.trigger("change")})}})},_bindFlashNotifications:function(){var self=this;_.each(this.pageViews,function(pageView){self.listenTo(pageView.view,"recline:flash",function(flash){self.notify(flash)})})},notify:function(flash){var tmplData=_.extend({message:"Loading",category:"warning",loader:false},flash);var _template;if(tmplData.loader){_template='
{{message}}  
'}else{_template='
Ɨ {{message}}
'}var _templated=$(Mustache.render(_template,tmplData));_templated=$(_templated).appendTo($(".recline-data-explorer .alert-messages"));if(!flash.persist){setTimeout(function(){$(_templated).fadeOut(1e3,function(){$(this).remove()})},1e3)}},clearNotifications:function(){var $notifications=$(".recline-data-explorer .alert-messages .alert");$notifications.fadeOut(1500,function(){$(this).remove()})}});my.MultiView.restore=function(state){var datasetInfo;if(state.backend==="memory"){datasetInfo={backend:"memory",records:[{stub:"this is a stub dataset because we do not restore memory datasets"}]}}else{datasetInfo=_.extend({url:state.url,backend:state.backend},state.dataset)}var dataset=new recline.Model.Dataset(datasetInfo);var explorer=new my.MultiView({model:dataset,state:state});return explorer};var urlPathRegex=/^([^?]+)(\?.*)?/;my.parseHashUrl=function(hashUrl){var parsed=urlPathRegex.exec(hashUrl);if(parsed===null){return{}}else{return{path:parsed[1],query:parsed[2]||""}}};my.parseQueryString=function(q){if(!q){return{}}var urlParams={},e,d=function(s){return unescape(s.replace(/\+/g," "))},r=/([^&=]+)=?([^&]*)/g;if(q&&q.length&&q[0]==="?"){q=q.slice(1)}while(e=r.exec(q)){urlParams[d(e[1])]=d(e[2])}return urlParams};my.parseHashQueryString=function(){var q=my.parseHashUrl(window.location.hash).query;return my.parseQueryString(q)};my.composeQueryString=function(queryParams){var queryString="?";var items=[];$.each(queryParams,function(key,value){if(typeof value==="object"){value=JSON.stringify(value)}items.push(key+"="+encodeURIComponent(value))});queryString+=items.join("&");return queryString};my.getNewHashForQueryString=function(queryParams){var queryPart=my.composeQueryString(queryParams);if(window.location.hash){return window.location.hash.split("?")[0].slice(1)+queryPart}else{return queryPart}};my.setHashQueryString=function(queryParams){window.location.hash=my.getNewHashForQueryString(queryParams)}})(jQuery,recline.View);this.recline=this.recline||{};this.recline.View=this.recline.View||{};(function($,my){"use strict";my.SlickGrid=Backbone.View.extend({initialize:function(modelEtc){var self=this;this.$el.addClass("recline-slickgrid");this.templates={deleterow:''};_.bindAll(this,"render","onRecordChanged");this.listenTo(this.model.records,"add remove reset",this.render);this.listenTo(this.model.records,"change",this.onRecordChanged);var state=_.extend({hiddenColumns:[],columnsOrder:[],columnsSort:{},columnsWidth:[],columnsEditor:[],options:{},fitColumns:false},modelEtc.state);this.state=new recline.Model.ObjectState(state);this._slickHandler=new Slick.EventHandler;if(this.state.get("gridOptions")&&this.state.get("gridOptions").enabledAddRow!=undefined&&this.state.get("gridOptions").enabledAddRow==true){this.editor=new my.GridControl;this.elSidebar=this.editor.$el;this.listenTo(this.editor.state,"change",function(){this.model.records.add(new recline.Model.Record)})}},onRecordChanged:function(record){if(!this.grid){return}var row_index=this.grid.getData().getModelRow(record);this.grid.invalidateRow(row_index);this.grid.getData().updateItem(record,row_index);this.grid.render()},render:function(){var self=this;var options=_.extend({enableCellNavigation:true,enableColumnReorder:true,explicitInitialization:true,syncColumnCellResize:true,forceFitColumns:this.state.get("fitColumns")},self.state.get("gridOptions"));var columns=[];var formatter=function(row,cell,value,columnDef,dataContext){if(columnDef.id=="del"){return self.templates.deleterow}var field=self.model.fields.get(columnDef.id);if(field.renderer){return field.renderer(value,field,dataContext)}else{return value}};var validator=function(field){return function(value){if(field.type=="date"&&isNaN(Date.parse(value))){return{valid:false,msg:"A date is required, check field field-date-format"}}else{return{valid:true,msg:null}}}};if(this.state.get("gridOptions")&&this.state.get("gridOptions").enableReOrderRow==true){columns.push({id:"#",name:"",width:22,behavior:"selectAndMove",selectable:false,resizable:false,cssClass:"recline-cell-reorder"})}if(this.state.get("gridOptions")&&this.state.get("gridOptions").enabledDelRow==true){columns.push({id:"del",name:"",field:"del",sortable:true,width:38,formatter:formatter,validator:validator})}function sanitizeFieldName(name){var sanitized;try{sanitized=$(name).text()}catch(e){sanitized=""}return name!==sanitized&&sanitized!==""?sanitized:name}_.each(this.model.fields.toJSON(),function(field){var column={id:field.id,name:sanitizeFieldName(field.label),field:field.id,sortable:true,minWidth:80,formatter:formatter,validator:validator(field)};var widthInfo=_.find(self.state.get("columnsWidth"),function(c){return c.column===field.id});if(widthInfo){column.width=widthInfo.width}var editInfo=_.find(self.state.get("columnsEditor"),function(c){return c.column===field.id});if(editInfo){column.editor=editInfo.editor}else{var typeToEditorMap={string:Slick.Editors.LongText,integer:Slick.Editors.IntegerEditor,number:Slick.Editors.Text,date:Slick.Editors.Text,boolean:Slick.Editors.YesNoSelectEditor};if(field.type in typeToEditorMap){column.editor=typeToEditorMap[field.type]}else{column.editor=Slick.Editors.LongText}}columns.push(column)});var visibleColumns=_.filter(columns,function(column){return _.indexOf(self.state.get("hiddenColumns"),column.id)===-1});if(this.state.get("columnsOrder")&&this.state.get("columnsOrder").length>0){visibleColumns=visibleColumns.sort(function(a,b){return _.indexOf(self.state.get("columnsOrder"),a.id)>_.indexOf(self.state.get("columnsOrder"),b.id)?1:-1});columns=columns.sort(function(a,b){return _.indexOf(self.state.get("columnsOrder"),a.id)>_.indexOf(self.state.get("columnsOrder"),b.id)?1:-1})}var tempHiddenColumns=[];for(var i=columns.length-1;i>=0;i--){if(_.indexOf(_.pluck(visibleColumns,"id"),columns[i].id)===-1){tempHiddenColumns.push(columns.splice(i,1)[0])}}columns=columns.concat(tempHiddenColumns);function toRow(m){var row={};self.model.fields.each(function(field){var render="";if(!_.isUndefined(m.getFieldValueUnrendered(field))){render=m.getFieldValueUnrendered(field)}row[field.id]=render});return row}function RowSet(){var models=[];var rows=[];this.push=function(model,row){models.push(model);rows.push(row)};this.getLength=function(){return rows.length};this.getItem=function(index){return rows[index]};this.getItemMetadata=function(index){return{}};this.getModel=function(index){return models[index]};this.getModelRow=function(m){return _.indexOf(models,m)};this.updateItem=function(m,i){rows[i]=toRow(m);models[i]=m}}var data=new RowSet;this.model.records.each(function(doc){data.push(doc,toRow(doc))});this.grid=new Slick.Grid(this.el,data,visibleColumns,options);var sortInfo=this.model.queryState.get("sort");if(sortInfo){var column=sortInfo[0].field;var sortAsc=sortInfo[0].order!=="desc";this.grid.setSortColumn(column,sortAsc)}if(this.state.get("gridOptions")&&this.state.get("gridOptions").enableReOrderRow){this._setupRowReordering()}this._slickHandler.subscribe(this.grid.onSort,function(e,args){var order=args.sortAsc?"asc":"desc";var sort=[{field:args.sortCol.field,order:order}];self.model.query({sort:sort})});this._slickHandler.subscribe(this.grid.onColumnsReordered,function(e,args){self.state.set({columnsOrder:_.pluck(self.grid.getColumns(),"id")})});this.grid.onColumnsResized.subscribe(function(e,args){var columns=args.grid.getColumns();var defaultColumnWidth=args.grid.getOptions().defaultColumnWidth;var columnsWidth=[];_.each(columns,function(column){if(column.width!=defaultColumnWidth){columnsWidth.push({column:column.id,width:column.width})}});self.state.set({columnsWidth:columnsWidth})});this._slickHandler.subscribe(this.grid.onCellChange,function(e,args){var grid=args.grid;var model=data.getModel(args.row);var field=grid.getColumns()[args.cell].id;var v={};v[field]=args.item[field];model.set(v)});this._slickHandler.subscribe(this.grid.onClick,function(e,args){try{e.preventDefault()}catch(e){}var cell=0;if(self.state.get("gridOptions")&&self.state.get("gridOptions").enableReOrderRow!=undefined&&self.state.get("gridOptions").enableReOrderRow==true){cell=1}if(args.cell==cell&&self.state.get("gridOptions").enabledDelRow==true){var model=data.getModel(args.row);model.destroy()}});var columnpicker=new Slick.Controls.ColumnPicker(columns,this.grid,_.extend(options,{state:this.state}));if(self.visible){self.grid.init();self.rendered=true}else{self.rendered=false}return this},_setupRowReordering:function(){var self=this;self.grid.setSelectionModel(new Slick.RowSelectionModel);var moveRowsPlugin=new Slick.RowMoveManager({cancelEditOnDrag:true});moveRowsPlugin.onBeforeMoveRows.subscribe(function(e,data){for(var i=0;i',initialize:function(options){var self=this;_.bindAll(this,"render");this.state=new recline.Model.ObjectState;this.render()},render:function(){var self=this;this.$el.html(this.template)},events:{"click .recline-row-add":"addNewRow"},addNewRow:function(e){e.preventDefault();this.state.trigger("change")}})})(jQuery,recline.View);(function($){function SlickColumnPicker(columns,grid,options){var $menu;var columnCheckboxes;var defaults={fadeSpeed:250};function init(){grid.onHeaderContextMenu.subscribe(handleHeaderContextMenu);options=$.extend({},defaults,options);$menu=$('
',events:{"click .js-facet-filter":"onFacetFilter"},initialize:function(model){_.bindAll(this,"render");this.listenTo(this.model.facets,"all",this.render);this.listenTo(this.model.fields,"all",this.render);this.render()},render:function(){var tmplData={fields:this.model.fields.toJSON()};tmplData.facets=_.map(this.model.facets.toJSON(),function(facet){if(facet._type==="date_histogram"){facet.entries=_.map(facet.entries,function(entry){entry.term=new Date(entry.time).toDateString();return entry})}return facet});var templated=Mustache.render(this.template,tmplData);this.$el.html(templated);if(this.model.facets.length>0){this.$el.show()}else{this.$el.hide()}},onHide:function(e){e.preventDefault();this.$el.hide()},onFacetFilter:function(e){e.preventDefault();var $target=$(e.target);var fieldId=$target.closest(".facet-summary").attr("data-facet");var value=$target.attr("data-value");this.model.queryState.addFilter({type:"term",field:fieldId,term:value});this.model.query()}})})(jQuery,recline.View);this.recline=this.recline||{};this.recline.View=this.recline.View||{};(function($,my){"use strict";my.Fields=Backbone.View.extend({className:"recline-fields-view",template:'

Fields +

{{#fields}}

{{label}} {{type}} »

{{#facets}}
    {{#terms}}
  • {{term}} [{{count}}]
  • {{/terms}}
{{/facets}}
{{/fields}}
',initialize:function(model){var self=this;_.bindAll(this,"render");this.listenTo(this.model.fields,"reset",function(action){self.model.fields.each(function(field){field.facets.unbind("all",self.render);field.facets.bind("all",self.render)});self.model.getFieldsSummary();self.render()});this.$el.find(".collapse").collapse();this.render()},render:function(){var self=this;var tmplData={fields:[]};this.model.fields.each(function(field){var out=field.toJSON();out.facets=field.facets.toJSON();tmplData.fields.push(out)});var templated=Mustache.render(this.template,tmplData);this.$el.html(templated)}})})(jQuery,recline.View);this.recline=this.recline||{};this.recline.View=this.recline.View||{};(function($,my){"use strict";my.FilterEditor=Backbone.View.extend({className:"recline-filter-editor well",template:'

Filters

Add filter
{{#filters}} {{{filterRender}}} {{/filters}} {{#filters.length}} {{/filters.length}}
',filterTemplates:{term:'
{{field}} {{type}} ×
',range:'
{{field}} {{type}} ×
',geo_distance:'
{{field}} {{type}} ×
'},events:{"click .js-remove-filter":"onRemoveFilter","click .js-add-filter":"onAddFilterShow","submit form.js-edit":"onTermFiltersUpdate","submit form.js-add":"onAddFilter"},initialize:function(){_.bindAll(this,"render");this.listenTo(this.model.fields,"all",this.render);this.listenTo(this.model.queryState,"change change:filters:new-blank",this.render);this.render()},render:function(){var self=this;var tmplData=$.extend(true,{},this.model.queryState.toJSON());tmplData.filters=_.map(tmplData.filters,function(filter,idx){filter.id=idx;return filter});tmplData.fields=this.model.fields.toJSON();tmplData.filterRender=function(){return Mustache.render(self.filterTemplates[this.type],this)};var out=Mustache.render(this.template,tmplData);this.$el.html(out)},onAddFilterShow:function(e){e.preventDefault();var $target=$(e.target);$target.hide();this.$el.find("form.js-add").show()},onAddFilter:function(e){e.preventDefault();var $target=$(e.target);$target.hide();var filterType=$target.find("select.filterType").val();var field=$target.find("select.fields").val();this.model.queryState.addFilter({type:filterType,field:field})},onRemoveFilter:function(e){e.preventDefault();var $target=$(e.target);var filterId=$target.attr("data-filter-id");this.model.queryState.removeFilter(filterId)},onTermFiltersUpdate:function(e){var self=this;e.preventDefault();var filters=self.model.queryState.get("filters");var $form=$(e.target);_.each($form.find("input"),function(input){var $input=$(input);var filterType=$input.attr("data-filter-type");var fieldId=$input.attr("data-filter-field");var filterIndex=parseInt($input.attr("data-filter-id"),10);var name=$input.attr("name");var value=$input.val();switch(filterType){case"term":filters[filterIndex].term=value;break;case"range":filters[filterIndex][name]=value;break;case"geo_distance":if(name==="distance"){filters[filterIndex].distance=parseFloat(value)}else{filters[filterIndex].point[name]=parseFloat(value)}break}});self.model.queryState.set({filters:filters,from:0});self.model.queryState.trigger("change")}})})(jQuery,recline.View);this.recline=this.recline||{};this.recline.View=this.recline.View||{};(function($,my){"use strict";my.Pager=Backbone.View.extend({className:"recline-pager",template:' ',events:{"click .action-pagination-update":"onPaginationUpdate","change input":"onFormSubmit"},initialize:function(){_.bindAll(this,"render");this.listenTo(this.model.queryState,"change",this.render);this.render()},onFormSubmit:function(e){e.preventDefault();var formFrom=parseInt(this.$el.find('input[name="from"]').val())-1;var formTo=parseInt(this.$el.find('input[name="to"]').val())-1;var maxRecord=this.model.recordCount-1;if(this.model.queryState.get("from")!=formFrom){this.model.queryState.set({from:Math.min(maxRecord,Math.max(formFrom,0))})}else if(this.model.queryState.get("to")!=formTo){var to=Math.min(maxRecord,Math.max(formTo,0));this.model.queryState.set({size:Math.min(maxRecord+1,Math.max(to-formFrom+1,1))})}},onPaginationUpdate:function(e){e.preventDefault();var $el=$(e.target);var newFrom=0;var currFrom=this.model.queryState.get("from");var size=this.model.queryState.get("size");var updateQuery=false;if($el.parent().hasClass("prev")){newFrom=Math.max(currFrom-Math.max(0,size),0);updateQuery=newFrom!=currFrom}else{newFrom=Math.max(currFrom+size,0);updateQuery=newFrom
',events:{"submit form":"onFormSubmit"},initialize:function(){_.bindAll(this,"render");this.listenTo(this.model,"change",this.render);this.render()},onFormSubmit:function(e){e.preventDefault();var query=this.$el.find(".search-query").val();this.model.set({q:query})},render:function(){var tmplData=this.model.toJSON();var templated=Mustache.render(this.template,tmplData);this.$el.html(templated)}})})(jQuery,recline.View);this.recline=this.recline||{};this.recline.View=this.recline.View||{};(function($,my){"use strict";my.ValueFilter=Backbone.View.extend({className:"recline-filter-editor well",template:'

Filters

{{#filters}} {{{filterRender}}} {{/filters}} {{#filters.length}} {{/filters.length}}
',filterTemplates:{term:'
{{field}} ×
'},events:{"click .js-remove-filter":"onRemoveFilter","click .js-add-filter":"onAddFilterShow","submit form.js-edit":"onTermFiltersUpdate","submit form.js-add":"onAddFilter"},initialize:function(){_.bindAll(this,"render");this.listenTo(this.model.fields,"all",this.render);this.listenTo(this.model.queryState,"change change:filters:new-blank",this.render);this.render()},render:function(){var self=this;var tmplData=$.extend(true,{},this.model.queryState.toJSON());tmplData.filters=_.map(tmplData.filters,function(filter,idx){filter.id=idx;return filter});tmplData.fields=this.model.fields.toJSON();tmplData.filterRender=function(){return Mustache.render(self.filterTemplates.term,this)};var out=Mustache.render(this.template,tmplData);this.$el.html(out)},updateFilter:function(input){var self=this;var filters=self.model.queryState.get("filters");var $input=$(input);var filterIndex=parseInt($input.attr("data-filter-id"),10);var value=$input.val();filters[filterIndex].term=value},onAddFilterShow:function(e){e.preventDefault();var $target=$(e.target);$target.hide();this.$el.find("form.js-add").show()},onAddFilter:function(e){e.preventDefault();var $target=$(e.target);$target.hide();var field=$target.find("select.fields").val();this.model.queryState.addFilter({type:"term",field:field})},onRemoveFilter:function(e){e.preventDefault();var $target=$(e.target);var filterId=$target.attr("data-filter-id");this.model.queryState.removeFilter(filterId)},onTermFiltersUpdate:function(e){var self=this;e.preventDefault();var filters=self.model.queryState.get("filters");var $form=$(e.target);_.each($form.find("input"),function(input){self.updateFilter(input)});self.model.queryState.set({filters:filters,from:0});self.model.queryState.trigger("change")}})})(jQuery,recline.View); \ No newline at end of file diff --git a/docs/backends.markdown b/docs/backends.markdown deleted file mode 100644 index 09adb183..00000000 --- a/docs/backends.markdown +++ /dev/null @@ -1,148 +0,0 @@ ---- -layout: container -title: Backends -root: ../ ---- - - - -Backends connect Dataset and Documents to data from a specific 'Backend' data -source. They provide methods for loading and saving Datasets and individuals -Documents as well as for bulk loading via a query API and doing bulk transforms -on the backend. - -
Looking for quickstart tutorial rather than reference documentation? See the Backends Tutorial.
- -Backends come in 2 flavours: - -* Loader backends - only implement fetch method. The data is then cached in a - Memory.Store on the Dataset and interacted with there. This is best for - sources which just allow you to load data or where you want to load the data - once and work with it locally. -* Store backends - these support fetch, query and, if write-enabled, save. - These are suitable where the backend contains a lot of data (infeasible to - load locally - for examples a million rows) or where the backend has - capabilities you want to take advantage of. - -Examples of the 2 types of backends are provided by the Google docs backend (a -"Loader" backend) and the ElasticSearch backend (a Store backend). - -# Available Backends - -You can find a list of the available Backends along with examples of how to use -them in the [Backends Tutorial](tutorial-backends.html). - -Note that it's easy to write your own backend - you just need to implement the -Recline Backend API described below. - -# Backend API - -Backend modules must implement the following API: - -{% highlight javascript %} -__type__: 'name-of-backend' // e.g. elasticsearch - -// Initial load of dataset including initial set of records -fetch: function(dataset) - -// Query the backend for records returning them in bulk. -// This method will be used by the Dataset.query method to search the backend -// for records, retrieving the results in bulk. -query: function(queryObj, dataset) - -// Save changes to the backend -save: function(changes, dataset) -{% endhighlight %} - -Details of each function below. Note that: - -* Each backend function takes a dataset object. This is not a Dataset object - but is simple JS object representation resulting from calling - Dataset.toJSON(). - - It is required because the Dataset attributes contain details of specific - backend (e.g. url for ElasticSearch etc). - -* Each function returns a promise API object - that is something conforming to - the jquery promise API and, in particular, having a done and fail function. - -### fetch: function(dataset) - -On success, promise callback must return an object with the following structure: - -{% highlight javascript %} -{ - // (optional) Set of record data - // Either an array of arrays *or* an array of objects corresponding to initial set of records for this object - // May not provided if data only returned by query - records: [...] - - // (optional) Set of field data - // Either an array of string or an array of objects corresponding to Field specification (see `Field` above) - fields: { ... } // as per recline.Model.Field - - // (optional) metadata fields to set on the Dataset object - metadata: { title: ..., id: ... etc } - - // boolean indicating whether to use a local memory store for managing this dataset - useMemoryStore: -} -{% endhighlight %} - -### query: function(queryObj, dataset) - -`queryObj`: JS object following Query specification above. - -#### Callbacks - -On success must return a 'QueryResult' object which has the following structure: - -{% highlight javascript %} -{ - // total number of results (can be null) - total: ... - - // one entry for each result record - hits: [ - { - // JS object that can be used to initialize a Record object - } - ], - - // (optional) - facets: { - // facet results (as per ) - } -} -{% endhighlight %} - -The QueryResult is partially modelled on ElasticSearch - see this issue for more -details. - -### save: function(changes, dataset) - -
The save function is still being revised and -its API and arguments are subject to change
- -`changes`: an object with the following structure: - -{% highlight javascript %} -{ - creates: [ record.toJSON(), record.toJSON(), ... ] - updates: [ ... ] - deletes: [ ... ] -} -{% endhighlight %} - -Each key has an array of records (as simple JS objects resulting from a call to -Record.toJSON()) that are in that state. - -The backend should take appropriate actions for each case. - - diff --git a/docs/extensions.md b/docs/extensions.md deleted file mode 100644 index 1626c434..00000000 --- a/docs/extensions.md +++ /dev/null @@ -1,64 +0,0 @@ -# Extensions - -Extensions are Views, Backends or other pieces of code that build on Recline but which do not ship as part of the core distribution. Here we have: - -* Instructions on creating an extension -* A list of existing extensions - * **If you have created an extension please add it to the list below** - -## Creating an Extension - -At its minimum its just a single JS file. However, we suggest a bit more structure: - -* Put the extension in its own repo named e.g. recline.view.{viewname} or recline.backend.{backendname} -* Including (essential): - * README.md - * the extension code itself name view.{name}.js or backend.{name}.js (or recline.view.{name}.js etc) -* Optional - * [optional but recommended] a test for the extension - * [optional] one or more demo html files showing your extension in action - -You may want to include recline as a submodule for testing purposes if you need some of its code - - git submodule add git://github.com/okfn/recline.git - -Or you can just pull the latest recline.js directly from: http://okfnlabs.org/recline/dist/recline.js - ----- - -## Views - -#### Line graphs in nvd3 - -Line graphs in [nvd3](http://nvd3.org/) - -* https://github.com/Moviri/recline/blob/master/src/extensions/views/view.nvd3.graph.js -* https://github.com/NuCivic/recline.view.nvd3.js - -#### Kartograph - -Integration with [Kartograph](http://kartograph.org/) - -* https://github.com/Moviri/recline/blob/master/src/extensions/views/view.kartograph.js - -#### Map View using Ordnance Survey - -[http://oslabs.github.com/recline-view-osmap/](http://oslabs.github.com/recline-view-osmap/) - -A new map view using the Ordnance Survey OpenSpaces tile service. It supports both the free and paid for (pro) stacks. More info and docs are found on the github project page. - ----- - -## Backends - -#### Google Docs - -This is the official Google Docs backend for Recline (part of the Recline distribution in v0.5) - -https://github.com/okfn/recline.backend.gdocs - -#### CouchDB - -CouchDB backend for Recline. - -https://github.com/okfn/recline.backend.couchdb diff --git a/docs/index.html b/docs/index.html deleted file mode 100644 index b9c50bd6..00000000 --- a/docs/index.html +++ /dev/null @@ -1,69 +0,0 @@ ---- -layout: default -title: Library - Home -root: ../ ---- - -
- - -

Building on Backbone, Recline - supplies components and structure to data-heavy applications by providing a - set of models (Dataset, Record/Row, Field) and views (Grid, Map, Graph - etc).

- - -

Concepts and Structure

- -

The Recline Library consists of 3 parts: Models, Backends and Views

- -
-
-
-

Models

-

Models help you structure your work with data by providing some standard objects such as Dataset and Record – a Dataset being a collection of Records. More »

-
-
-
-
-

Backends

-

Backends connect your Models to data sources (and stores) – for example Google Docs spreadsheets, local CSV files, the DataHub, ElasticSearch etc. More »

-
-
-
-
-

Views

-

Views are user interface components for displaying, editing or interacting with the data. For example, maps, graphs, data grids or a query editor. More »

-
-
-
- - - -

Source Docs (via Docco)

-
-
-

Models

- -
- -
- -
- diff --git a/docs/models.markdown b/docs/models.markdown deleted file mode 100644 index 0b5702b4..00000000 --- a/docs/models.markdown +++ /dev/null @@ -1,325 +0,0 @@ ---- -layout: container -title: Models -root: ../ ---- - - - -Models help you structure your work with data by providing several objects and -functions. The key ones are Dataset and Record -- a Dataset being a collection -of Records. Additionally, there is a a Field object for describing the columns -of a Dataset, a Query object for describing queries, and a Facet object for -holding summary information about a Field (or multiple Fields). - -All the models are Backbone models, that is they extend Backbone.Model. Note, -however, that they do not 'sync' (load/save) like normal Backbone models. - - -

Dataset

- -A Dataset is *the* central object in Recline. Standard usage is: - -{% highlight javascript %} -var dataset = new recline.model.Dataset({ - // general metadata e.g. - id: ... - title: ... - // information about data source e.g. - url: http://url.to.my.data.endpoint/ - // backend string or object - backend: a string identifying the backend we are using - see below -}); - -// initialize dataset with data from the backend. -dataset.fetch(); - -// we will now have the following (and more) set up - see below for details -dataset.fields // collection of Fields (columns) for this Dataset -dataset.records // collection of Records resulting from latest query -dataset.docCount // total number of Records in the last query -{% endhighlight %} - -### Key Attributes - -* records: a collection of `Record`s currently loaded for viewing - (updated by calling query method) - note that this need not - be all the records in the dataset (for example, you may have connected to a - source where the complete dataset contains a million records but you have - only loaded a 1000 records) -* fields: (aka columns) is a Backbone collectoin of `Field`s listing all the - fields on this Dataset (this can be set explicitly, or, will be set by - Dataset.fetch() -* docCount: total number of records in this dataset -* backend: the Backend (instance) for this Dataset. (NB: this is a the backend - attribute on the object itself not the backend in the Backbone attributes - i.e. the result of dataset.get('backend'). The latter is a string identifying - the backend. -* queryState: a `Query` object which stores current queryState. queryState may - be edited by other components (e.g. a query editor view) changes will trigger - a Dataset query. -* facets: a collection of `Facet`s - -### Querying - -{% highlight javascript %} -dataset.query(queryObj) -{% endhighlight %} - -`queryObj` is an object following the query -specification below. - - -

Record (aka Row)

- -A Record is a single entry or row in a dataset. A Record needs little more than -what is provided by the standard Backbone Model object. In general, you will -never create a Record directly -- they will get created for you by Datasets -from query results. - -

Field (aka Column)

- -A Field should have the following attributes as standard: - -{% highlight javascript %} -var field = new Field({ - // a unique identifer for this field- usually this should match the key in the records hash - id: 'my-field-id' - - // (optional: defaults to id) the visible label used for this field - label: 'My Field Name', - - // (optional: defaults to string) the type of the data in this field. - // For list of type names see below - type: 'string', - - // (optional - defaults to null) used to indicate how the data should be - // formatted. See below. - format: null, - - // (default: false) attribute indicating this field has no backend data but - // is just derived from other fields (see below). - is_derived: false -{% endhighlight %} - -#### Types - -The type attribute is a string indicating the type of this field. - -Types are -based on the [type set of json-schmea][types-1] with a few minor additions and -modifications (cf other type lists include those in [Elasticsearch](es-types)). - -The type list is as follows (brackets indicate -possible aliases for specific types - these types will be recognized and -normalized to the default type name for that type): - -* **string (text)**: a string -* **number (double, float, numeric)**: a number including floating point numbers. -* **integer (int)**: an integer. -* **date**: a date. The preferred format is YYYY-MM-DD. -* **time**: a time without a date -* **date-time (datetime, timestamp)**: a date-time. It is recommended this be in ISO 8601 - format of YYYY-MM- DDThh:mm:ssZ in UTC time. -* **boolean (bool)** -* **binary**: base64 representation of binary data. -* **geo_point**: as per - . - That is a field (in these examples named location) that has one of the - following structures: - - location: { - lon: ... - lat: ... - } - - location: [lon,lat] - - location: "lat, lng" - - As bonus there is also support for (beyond the ES style geo_point): - - // geonames style - location: { - lng: ... - lat: ... - } - // found on the web - location: "(lat, lon)" - -* **geojson**: as per -* **array**: an array -* **object (json)**: an object -* **any**: value of field may be any type - -
NB: types are not validated so you can set the type to -whatever value you like (it does not have to be in the above list). However, -using types outside of the specified list may limit functionality.
- -[types-1]: http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1 -[es-types]: http://www.elasticsearch.org/guide/reference/mapping/ - -#### Rendering, types and formats - -One can customize the rendering of fields in the user interface and elsewhere -by setting a renderer function on the field. You do this by setting a field -attribute: - -{% highlight javascript %} -myfield.renderer = myRenderFunction; -{% endhighlight %} - -Your renderer function should have the following signature: - - function(value, field, record) - -Where the arguments passed in are as follows: - -* `value`: the value of the cell (record value for this field) -* `field`: corresponding `Field` object -* `record`: is the `Record` object (as simple JS object) - -Note that implementing functions can ignore arguments (e.g. function(value) -would be a valid formatter function). - -To guide the behaviour of renderers we have type and format information. -Example types and formats are: - -* type=date, format=yyyy-mm-dd -* type=float, format=percentage -* type=string, format=markdown (render as markdown if Showdown available) - -Default renderers are provided - see the source for details, but a few examples -are: - -* type = string - * no format provided: pass through but convert http:// to hyperlinks - * format = plain: do no processing on the source text - * format = markdown: process as markdown (if Showdown library available) -* type = float - - * format = percentage: format as a percentage - -#### Derived fields - -Some fields may be 'dervied' from other fields. This allows you to define an -entirely new value for data in this field. This provides support for a) -'derived/computed' fields: i.e. fields whose data are functions of the data in -other fields b) transforming the value of this field prior to rendering. - -To use derived fields set a `deriver` function on the Field. This function will -be used to derive/compute the value of data in this field as a function of this -field's value (if any) and the current record. It's signature and behaviour is -the same as for renderer. - - -

Query

- -Query instances encapsulate a query to the backend (see query method on backend). Useful both -for creating queries and for storing and manipulating query state - -e.g. from a query editor). - - -

Query Structure and format

- -Query structure should follow that of [ElasticSearch query -language](http://www.elasticsearch.org/guide/reference/api/search/). - -**NB: It is up to specific backends how to implement and support this query -structure. Different backends might choose to implement things differently -or not support certain features. Please check your backend for details.** - -Query object has the following key attributes: - - * size (=limit): number of results to return - * from (=offset): offset into result set - http://www.elasticsearch.org/guide/reference/api/search/from-size.html - * sort: sort order - see below - * query: Query in ES Query DSL - * filter: See filters and Filtered Query - * fields: set of fields to return - http://www.elasticsearch.org/guide/reference/api/search/fields.html - * facets: specification of facets - see http://www.elasticsearch.org/guide/reference/api/search/facets/ - -Additions: - -* q: either straight text or a hash will map directly onto a [query_string - query](http://www.elasticsearch.org/guide/reference/query-dsl/query-string-query.html) - in backend - - * Of course this can be re-interpreted by different backends. E.g. some may - just pass this straight through e.g. for an SQL backend this could be the - full SQL query - -* filters: array of ElasticSearch filters. These will be and-ed together for - execution. - -#### Sort - -Sort structure is inspired by but with some standardization. - -Sort structure must be as follows: - - "sort" : [ - { field: "post_date", "order" : "desc"}, - { field: "user" }, - { "name" : "desc" }, - { "age" : "desc" }, - {"_score": null} - ] - -If order is omitted it is assumed to be "desc" except in the case of _score. -_score is a special case which is used for match score if that is supported by -the backend. - -#### Examples - -
-{
-   q: 'quick brown fox',
-   filters: [
-     { term: { 'owner': 'jones' } }
-   ]
-}
-
- - -

Facet – Summary information (e.g. values and counts) about a field obtained by a 'faceting' or 'group by' method -

- -Structure of a facet follows that of Facet results in ElasticSearch, see: - - -Specifically the object structure of a facet looks like (there is one -addition compared to ElasticSearch: the "id" field which corresponds to the -key used to specify this facet in the facet query): - -{% highlight javascript %} -{ - id: "id-of-facet", - // type of this facet (terms, range, histogram etc) - _type : "terms", - // total number of tokens in the facet - total: 5, - // @property {number} number of records which have no value for the field - missing : 0, - // number of facet values not included in the returned facets - other: 0, - // term object ({term: , count: ...}) - terms: [ { - "term" : "foo", - "count" : 2 - }, { - "term" : "bar", - "count" : 2 - }, { - "term" : "baz", - "count" : 1 - } - ] -} -{% endhighlight %} - diff --git a/docs/src/backend.csv.html b/docs/src/backend.csv.html deleted file mode 100644 index bfc6461a..00000000 --- a/docs/src/backend.csv.html +++ /dev/null @@ -1,240 +0,0 @@ - backend.csv.js

backend.csv.js

this.recline = this.recline || {};
-this.recline.Backend = this.recline.Backend || {};
-this.recline.Backend.CSV = this.recline.Backend.CSV || {};

Note that provision of jQuery is optional (it is only needed if you use fetch on a remote file)

(function(my) {
-  "use strict";
-  my.__type__ = 'csv';

use either jQuery or Underscore Deferred depending on what is available

  var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred;

fetch

- -

fetch supports 3 options depending on the attribute provided on the dataset argument

- -
    -
  1. dataset.file: file is an HTML5 file object. This is opened and parsed with the CSV parser.
  2. -
  3. dataset.data: data is a string in CSV format. This is passed directly to the CSV parser
  4. -
  5. dataset.url: a url to an online CSV file that is ajax accessible (note this usually requires either local or on a server that is CORS enabled). The file is then loaded using jQuery.ajax and parsed using the CSV parser (NB: this requires jQuery)
  6. -
- -

All options generates similar data and use the memory store outcome, that is they return something like:

- -
-{
-  records: [ [...], [...], ... ],
-  metadata: { may be some metadata e.g. file name }
-  useMemoryStore: true
-}
-
  my.fetch = function(dataset) {
-    var dfd = new Deferred();
-    if (dataset.file) {
-      var reader = new FileReader();
-      var encoding = dataset.encoding || 'UTF-8';
-      reader.onload = function(e) {
-        var out = my.extractFields(my.parseCSV(e.target.result, dataset), dataset);
-        out.useMemoryStore = true;
-        out.metadata = {
-          filename: dataset.file.name
-        }
-        dfd.resolve(out);
-      };
-      reader.onerror = function (e) {
-        alert('Failed to load file. Code: ' + e.target.error.code);
-      };
-      reader.readAsText(dataset.file, encoding);
-    } else if (dataset.data) {
-      var out = my.extractFields(my.parseCSV(dataset.data, dataset), dataset);
-      out.useMemoryStore = true;
-      dfd.resolve(out);
-    } else if (dataset.url) {
-      jQuery.get(dataset.url).done(function(data) {
-        var out = my.extractFields(my.parseCSV(data, dataset), dataset);
-        out.useMemoryStore = true;
-        dfd.resolve(out);
-      });
-    }
-    return dfd.promise();
-  };

Convert array of rows in { records: [ ...] , fields: [ ... ] } -@param {Boolean} noHeaderRow If true assume that first row is not a header (i.e. list of fields but is data.

  my.extractFields = function(rows, noFields) {
-    if (noFields.noHeaderRow !== true && rows.length > 0) {
-      return {
-        fields: rows[0],
-        records: rows.slice(1)
-      }
-    } else {
-      return {
-        records: rows
-      }
-    }
-  };

parseCSV

- -

Converts a Comma Separated Values string into an array of arrays. -Each line in the CSV becomes an array.

- -

Empty fields are converted to nulls and non-quoted numbers are converted to integers or floats.

- -

@return The CSV parsed as an array -@type Array

- -

@param {String} s The string to convert -@param {Object} options Options for loading CSV including - @param {Boolean} [trim=false] If set to True leading and trailing - whitespace is stripped off of each non-quoted field as it is imported - @param {String} [delimiter=','] A one-character string used to separate - fields. It defaults to ',' - @param {String} [quotechar='"'] A one-character string used to quote - fields containing special characters, such as the delimiter or - quotechar, or which contain new-line characters. It defaults to '"'

- -

@param {Integer} skipInitialRows A integer number of rows to skip (default 0)

- -

Heavily based on uselesscode's JS CSV parser (MIT Licensed): -http://www.uselesscode.org/javascript/csv/

  my.parseCSV= function(s, options) {

Get rid of any trailing \n

    s = chomp(s);
-
-    var options = options || {};
-    var trm = (options.trim === false) ? false : true;
-    var delimiter = options.delimiter || ',';
-    var quotechar = options.quotechar || '"';
-
-    var cur = '', // The character we are currently processing.
-      inQuote = false,
-      fieldQuoted = false,
-      field = '', // Buffer for building up the current field
-      row = [],
-      out = [],
-      i,
-      processField;
-
-    processField = function (field) {
-      if (fieldQuoted !== true) {

If field is empty set to null

        if (field === '') {
-          field = null;

If the field was not quoted and we are trimming fields, trim it

        } else if (trm === true) {
-          field = trim(field);
-        }

Convert unquoted numbers to their appropriate types

        if (rxIsInt.test(field)) {
-          field = parseInt(field, 10);
-        } else if (rxIsFloat.test(field)) {
-          field = parseFloat(field, 10);
-        }
-      }
-      return field;
-    };
-
-    for (i = 0; i < s.length; i += 1) {
-      cur = s.charAt(i);

If we are at a EOF or EOR

      if (inQuote === false && (cur === delimiter || cur === "\n")) {
-        field = processField(field);

Add the current field to the current row

        row.push(field);

If this is EOR append row to output and flush row

        if (cur === "\n") {
-          out.push(row);
-          row = [];
-        }

Flush the field buffer

        field = '';
-        fieldQuoted = false;
-      } else {

If it's not a quotechar, add it to the field buffer

        if (cur !== quotechar) {
-          field += cur;
-        } else {
-          if (!inQuote) {

We are not in a quote, start a quote

            inQuote = true;
-            fieldQuoted = true;
-          } else {

Next char is quotechar, this is an escaped quotechar

            if (s.charAt(i + 1) === quotechar) {
-              field += quotechar;

Skip the next char

              i += 1;
-            } else {

It's not escaping, so end quote

              inQuote = false;
-            }
-          }
-        }
-      }
-    }

Add the last field

    field = processField(field);
-    row.push(field);
-    out.push(row);

Expose the ability to discard initial rows

    if (options.skipInitialRows) out = out.slice(options.skipInitialRows);
-
-    return out;
-  };

serializeCSV

- -

Convert an Object or a simple array of arrays into a Comma -Separated Values string.

- -

Nulls are converted to empty fields and integers or floats are converted to non-quoted numbers.

- -

@return The array serialized as a CSV -@type String

- -

@param {Object or Array} dataToSerialize The Object or array of arrays to convert. Object structure must be as follows:

- -
{
-  fields: [ {id: .., ...}, {id: ..., 
-  records: [ { record }, { record }, ... ]
-  ... // more attributes we do not care about
-}
-
- -

@param {object} options Options for serializing the CSV file including - delimiter and quotechar (see parseCSV options parameter above for - details on these).

- -

Heavily based on uselesscode's JS CSV serializer (MIT Licensed): -http://www.uselesscode.org/javascript/csv/

  my.serializeCSV= function(dataToSerialize, options) {
-    var a = null;
-    if (dataToSerialize instanceof Array) {
-      a = dataToSerialize;
-    } else {
-      a = [];
-      var fieldNames = _.pluck(dataToSerialize.fields, 'id');
-      a.push(fieldNames);
-      _.each(dataToSerialize.records, function(record, index) {
-        var tmp = _.map(fieldNames, function(fn) {
-          return record[fn];
-        });
-        a.push(tmp);
-      });
-    }
-    var options = options || {};
-    var delimiter = options.delimiter || ',';
-    var quotechar = options.quotechar || '"';
-
-    var cur = '', // The character we are currently processing.
-      field = '', // Buffer for building up the current field
-      row = '',
-      out = '',
-      i,
-      j,
-      processField;
-
-    processField = function (field) {
-      if (field === null) {

If field is null set to empty string

        field = '';
-      } else if (typeof field === "string" && rxNeedsQuoting.test(field)) {

Convert string to delimited string

        field = quotechar + field + quotechar;
-      } else if (typeof field === "number") {

Convert number to string

        field = field.toString(10);
-      }
-
-      return field;
-    };
-
-    for (i = 0; i < a.length; i += 1) {
-      cur = a[i];
-
-      for (j = 0; j < cur.length; j += 1) {
-        field = processField(cur[j]);

If this is EOR append row to output and flush row

        if (j === (cur.length - 1)) {
-          row += field;
-          out += row + "\n";
-          row = '';
-        } else {

Add the current field to the current row

          row += field + delimiter;
-        }

Flush the field buffer

        field = '';
-      }
-    }
-
-    return out;
-  };
-
-  var rxIsInt = /^\d+$/,
-    rxIsFloat = /^\d*\.\d+$|^\d+\.\d*$/,

If a string has leading or trailing space, -contains a comma double quote or a newline -it needs to be quoted in CSV output

    rxNeedsQuoting = /^\s|\s$|,|"|\n/,
-    trim = (function () {

Fx 3.1 has a native trim function, it's about 10x faster, use it if it exists

      if (String.prototype.trim) {
-        return function (s) {
-          return s.trim();
-        };
-      } else {
-        return function (s) {
-          return s.replace(/^\s*/, '').replace(/\s*$/, '');
-        };
-      }
-    }());
-
-  function chomp(s) {
-    if (s.charAt(s.length - 1) !== "\n") {

Does not end with \n, just return string

      return s;
-    } else {

Remove the \n

      return s.substring(0, s.length - 1);
-    }
-  }
-
-
-}(this.recline.Backend.CSV));
-
-
\ No newline at end of file diff --git a/docs/src/backend.dataproxy.html b/docs/src/backend.dataproxy.html deleted file mode 100644 index f954bf0b..00000000 --- a/docs/src/backend.dataproxy.html +++ /dev/null @@ -1,271 +0,0 @@ - - - - - backend.dataproxy.js - - - - - -
-
- - - -
    - -
  • -
    -

    backend.dataproxy.js

    -
    -
  • - - - -
  • -
    - -
    - -
    - -
    - -
    this.recline = this.recline || {};
    -this.recline.Backend = this.recline.Backend || {};
    -this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {};
    -
    -(function(my) {
    -  "use strict";
    -  my.__type__ = 'dataproxy';
    - -
  • - - -
  • -
    - -
    - -
    -

    URL for the dataproxy

    - -
    - -
      my.dataproxy_url = '//jsonpdataproxy.appspot.com';
    - -
  • - - -
  • -
    - -
    - -
    -

    Timeout for dataproxy (after this time if no response we error) -Needed because use JSONP so do not receive e.g. 500 errors

    - -
    - -
      my.timeout = 5000;
    - -
  • - - -
  • -
    - -
    - -
    -

    use either jQuery or Underscore Deferred depending on what is available

    - -
    - -
      var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred;
    - -
  • - - -
  • -
    - -
    - -
    -

    load

    -

    Load data from a URL via the DataProxy.

    -

    Returns array of field names and array of arrays for records

    - -
    - -
      my.fetch = function(dataset) {
    -    var data = {
    -      url: dataset.url,
    -      'max-results':  dataset.size || dataset.rows || 1000,
    -      type: dataset.format || ''
    -    };
    -    var jqxhr = jQuery.ajax({
    -      url: my.dataproxy_url,
    -      data: data,
    -      dataType: 'jsonp'
    -    });
    -    var dfd = new Deferred();
    -    _wrapInTimeout(jqxhr).done(function(results) {
    -      if (results.error) {
    -        dfd.reject(results.error);
    -      }
    -
    -      dfd.resolve({
    -        records: results.data,
    -        fields: results.fields,
    -        useMemoryStore: true
    -      });
    -    })
    -    .fail(function(args) {
    -      dfd.reject(args);
    -    });
    -    return dfd.promise();
    -  };
    - -
  • - - -
  • -
    - -
    - -
    -

    _wrapInTimeout

    -

    Convenience method providing a crude way to catch backend errors on JSONP calls. -Many of backends use JSONP and so will not get error messages and this is -a crude way to catch those errors.

    - -
    - -
      var _wrapInTimeout = function(ourFunction) {
    -    var dfd = new Deferred();
    -    var timer = setTimeout(function() {
    -      dfd.reject({
    -        message: 'Request Error: Backend did not respond after ' + (my.timeout / 1000) + ' seconds'
    -      });
    -    }, my.timeout);
    -    ourFunction.done(function(args) {
    -        clearTimeout(timer);
    -        dfd.resolve(args);
    -      })
    -      .fail(function(args) {
    -        clearTimeout(timer);
    -        dfd.reject(args);
    -      })
    -      ;
    -    return dfd.promise();
    -  };
    -
    -}(this.recline.Backend.DataProxy));
    - -
  • - -
-
- - diff --git a/docs/src/backend.elasticsearch.html b/docs/src/backend.elasticsearch.html deleted file mode 100644 index 61487458..00000000 --- a/docs/src/backend.elasticsearch.html +++ /dev/null @@ -1,242 +0,0 @@ - backend.elasticsearch.js

backend.elasticsearch.js

this.recline = this.recline || {};
-this.recline.Backend = this.recline.Backend || {};
-this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
-
-(function($, my) {
-  my.__type__ = 'elasticsearch';

use either jQuery or Underscore Deferred depending on what is available

  var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred;

ElasticSearch Wrapper

- -

A simple JS wrapper around an ElasticSearch endpoints.

- -

@param {String} endpoint: url for ElasticSearch type/table, e.g. for ES running -on http://localhost:9200 with index twitter and type tweet it would be:

- -
http://localhost:9200/twitter/tweet
- -

@param {Object} options: set of options such as:

- -
    -
  • headers - {dict of headers to add to each request}
  • -
  • dataType: dataType for AJAx requests e.g. set to jsonp to make jsonp requests (default is json requests)
  • -
  my.Wrapper = function(endpoint, options) { 
-    var self = this;
-    this.endpoint = endpoint;
-    this.options = _.extend({
-        dataType: 'json'
-      },
-      options);

mapping

- -

Get ES mapping for this type/table

- -

@return promise compatible deferred object.

    this.mapping = function() {
-      var schemaUrl = self.endpoint + '/_mapping';
-      var jqxhr = makeRequest({
-        url: schemaUrl,
-        dataType: this.options.dataType
-      });
-      return jqxhr;
-    };

get

- -

Get record corresponding to specified id

- -

@return promise compatible deferred object.

    this.get = function(id) {
-      var base = this.endpoint + '/' + id;
-      return makeRequest({
-        url: base,
-        dataType: 'json'
-      });
-    };

upsert

- -

create / update a record to ElasticSearch backend

- -

@param {Object} doc an object to insert to the index. -@return deferred supporting promise API

    this.upsert = function(doc) {
-      var data = JSON.stringify(doc);
-      url = this.endpoint;
-      if (doc.id) {
-        url += '/' + doc.id;
-      }
-      return makeRequest({
-        url: url,
-        type: 'POST',
-        data: data,
-        dataType: 'json'
-      });
-    };

delete

- -

Delete a record from the ElasticSearch backend.

- -

@param {Object} id id of object to delete -@return deferred supporting promise API

    this.remove = function(id) {
-      url = this.endpoint;
-      url += '/' + id;
-      return makeRequest({
-        url: url,
-        type: 'DELETE',
-        dataType: 'json'
-      });
-    };
-
-    this._normalizeQuery = function(queryObj) {
-      var self = this;
-      var queryInfo = (queryObj && queryObj.toJSON) ? queryObj.toJSON() : _.extend({}, queryObj);
-      var out = {
-        constant_score: {
-          query: {}
-        }
-      };
-      if (!queryInfo.q) {
-        out.constant_score.query = {
-          match_all: {}
-        };
-      } else {
-        out.constant_score.query = {
-          query_string: {
-            query: queryInfo.q
-          }
-        };
-      }
-      if (queryInfo.filters && queryInfo.filters.length) {
-        out.constant_score.filter = {
-          and: []
-        };
-        _.each(queryInfo.filters, function(filter) {
-          out.constant_score.filter.and.push(self._convertFilter(filter));
-        });
-      }
-      return out;
-    },

convert from Recline sort structure to ES form -http://www.elasticsearch.org/guide/reference/api/search/sort.html

    this._normalizeSort = function(sort) {
-      var out = _.map(sort, function(sortObj) {
-        var _tmp = {};
-        var _tmp2 = _.clone(sortObj);
-        delete _tmp2['field'];
-        _tmp[sortObj.field] = _tmp2;
-        return _tmp;
-      });
-      return out;
-    },
-
-    this._convertFilter = function(filter) {
-      var out = {};
-      out[filter.type] = {}
-      if (filter.type === 'term') {
-        out.term[filter.field] = filter.term.toLowerCase();
-      } else if (filter.type === 'geo_distance') {
-        out.geo_distance[filter.field] = filter.point;
-        out.geo_distance.distance = filter.distance;
-        out.geo_distance.unit = filter.unit;
-      }
-      return out;
-    },

query

- -

@return deferred supporting promise API

    this.query = function(queryObj) {
-      var esQuery = (queryObj && queryObj.toJSON) ? queryObj.toJSON() : _.extend({}, queryObj);
-      esQuery.query = this._normalizeQuery(queryObj);
-      delete esQuery.q;
-      delete esQuery.filters;
-      if (esQuery.sort && esQuery.sort.length > 0) {
-        esQuery.sort = this._normalizeSort(esQuery.sort);
-      }
-      var data = {source: JSON.stringify(esQuery)};
-      var url = this.endpoint + '/_search';
-      var jqxhr = makeRequest({
-        url: url,
-        data: data,
-        dataType: this.options.dataType
-      });
-      return jqxhr;
-    }
-  };

Recline Connectors

- -

Requires URL of ElasticSearch endpoint to be specified on the dataset -via the url attribute.

ES options which are passed through to options on Wrapper (see Wrapper for details)

  my.esOptions = {};

fetch

  my.fetch = function(dataset) {
-    var es = new my.Wrapper(dataset.url, my.esOptions);
-    var dfd = new Deferred();
-    es.mapping().done(function(schema) {
-
-      if (!schema){
-        dfd.reject({'message':'Elastic Search did not return a mapping'});
-        return;
-      }

only one top level key in ES = the type so we can ignore it

      var key = _.keys(schema)[0];
-      var fieldData = _.map(schema[key].properties, function(dict, fieldName) {
-        dict.id = fieldName;
-        return dict;
-      });
-      dfd.resolve({
-        fields: fieldData
-      });
-    })
-    .fail(function(arguments) {
-      dfd.reject(arguments);
-    });
-    return dfd.promise();
-  };

save

  my.save = function(changes, dataset) {
-    var es = new my.Wrapper(dataset.url, my.esOptions);
-    if (changes.creates.length + changes.updates.length + changes.deletes.length > 1) {
-      var dfd = new Deferred();
-      msg = 'Saving more than one item at a time not yet supported';
-      alert(msg);
-      dfd.reject(msg);
-      return dfd.promise();
-    }
-    if (changes.creates.length > 0) {
-      return es.upsert(changes.creates[0]);
-    }
-    else if (changes.updates.length >0) {
-      return es.upsert(changes.updates[0]);
-    } else if (changes.deletes.length > 0) {
-      return es.remove(changes.deletes[0].id);
-    }
-  };

query

  my.query = function(queryObj, dataset) {
-    var dfd = new Deferred();
-    var es = new my.Wrapper(dataset.url, my.esOptions);
-    var jqxhr = es.query(queryObj);
-    jqxhr.done(function(results) {
-      var out = {
-        total: results.hits.total
-      };
-      out.hits = _.map(results.hits.hits, function(hit) {
-        if (!('id' in hit._source) && hit._id) {
-          hit._source.id = hit._id;
-        }
-        return hit._source;
-      });
-      if (results.facets) {
-        out.facets = results.facets;
-      }
-      dfd.resolve(out);
-    }).fail(function(errorObj) {
-      var out = {
-        title: 'Failed: ' + errorObj.status + ' code',
-        message: errorObj.responseText
-      };
-      dfd.reject(out);
-    });
-    return dfd.promise();
-  };

makeRequest

- -

Just $.ajax but in any headers in the 'headers' attribute of this -Backend instance. Example:

- -
-var jqxhr = this._makeRequest({
-  url: the-url
-});
-
var makeRequest = function(data, headers) {
-  var extras = {};
-  if (headers) {
-    extras = {
-      beforeSend: function(req) {
-        _.each(headers, function(value, key) {
-          req.setRequestHeader(key, value);
-        });
-      }
-    };
-  }
-  var data = _.extend(extras, data);
-  return $.ajax(data);
-};
-
-}(jQuery, this.recline.Backend.ElasticSearch));
-
-
\ No newline at end of file diff --git a/docs/src/backend.gdocs.html b/docs/src/backend.gdocs.html deleted file mode 100644 index 7d07578d..00000000 --- a/docs/src/backend.gdocs.html +++ /dev/null @@ -1,137 +0,0 @@ - backend.gdocs.js

backend.gdocs.js

this.recline = this.recline || {};
-this.recline.Backend = this.recline.Backend || {};
-this.recline.Backend.GDocs = this.recline.Backend.GDocs || {};
-
-(function(my) {
-  my.__type__ = 'gdocs';

use either jQuery or Underscore Deferred depending on what is available

  var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred;

Google spreadsheet backend

- -

Fetch data from a Google Docs spreadsheet.

- -

Dataset must have a url attribute pointing to the Gdocs or its JSON feed e.g.

- -
-var dataset = new recline.Model.Dataset({
-    url: 'https://docs.google.com/spreadsheet/ccc?key=0Aon3JiuouxLUdGlQVDJnbjZRSU1tUUJWOUZXRG53VkE#gid=0'
-  },
-  'gdocs'
-);
-
-var dataset = new recline.Model.Dataset({
-    url: 'https://spreadsheets.google.com/feeds/list/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/od6/public/values?alt=json'
-  },
-  'gdocs'
-);
-
- -

@return object with two attributes

- -
    -
  • fields: array of Field objects
  • -
  • records: array of objects for each row
  • -
  my.fetch = function(dataset) {
-    var dfd  = new Deferred(); 
-    var urls = my.getGDocsAPIUrls(dataset.url);

TODO cover it with tests -get the spreadsheet title

    (function () {
-      var titleDfd = new Deferred();
-
-      jQuery.getJSON(urls.spreadsheet, function (d) {
-          titleDfd.resolve({
-              spreadsheetTitle: d.feed.title.$t
-          });
-      });
-
-      return titleDfd.promise();
-    }()).then(function (response) {

get the actual worksheet data

      jQuery.getJSON(urls.worksheet, function(d) {
-        var result = my.parseData(d);
-        var fields = _.map(result.fields, function(fieldId) {
-          return {id: fieldId};
-        });
-
-        dfd.resolve({
-          metadata: {
-              title: response.spreadsheetTitle +" :: "+ result.worksheetTitle,
-              spreadsheetTitle: response.spreadsheetTitle,
-              worksheetTitle  : result.worksheetTitle
-          },
-          records       : result.records,
-          fields        : fields,
-          useMemoryStore: true
-        });
-      });
-    });
-
-    return dfd.promise();
-  };

parseData

- -

Parse data from Google Docs API into a reasonable form

- -

:options: (optional) optional argument dictionary: -columnsToUse: list of columns to use (specified by field names) -colTypes: dictionary (with column names as keys) specifying types (e.g. range, percent for use in conversion). -:return: tabular data object (hash with keys: field and data).

- -

Issues: seems google docs return columns in rows in random order and not even sure whether consistent across rows.

  my.parseData = function(gdocsSpreadsheet, options) {
-    var options  = options || {};
-    var colTypes = options.colTypes || {};
-    var results = {
-      fields : [],
-      records: []
-    };
-    var entries = gdocsSpreadsheet.feed.entry || [];
-    var key;
-    var colName;

percentage values (e.g. 23.3%)

    var rep = /^([\d\.\-]+)\%$/;
-
-    for(key in entries[0]) {

it's barely possible it has inherited keys starting with 'gsx$'

      if(/^gsx/.test(key)) {
-        colName = key.substr(4);
-        results.fields.push(colName);
-      }
-    }

converts non numberical values that should be numerical (22.3%[string] -> 0.223[float])

    results.records = _.map(entries, function(entry) {
-      var row = {};
-
-      _.each(results.fields, function(col) {
-        var _keyname = 'gsx$' + col;
-        var value = entry[_keyname].$t;
-        var num;
- 

TODO cover this part of code with test -TODO use the regexp only once -if labelled as % and value contains %, convert

        if(colTypes[col] === 'percent' && rep.test(value)) {
-          num   = rep.exec(value)[1];
-          value = parseFloat(num) / 100;
-        }
-
-        row[col] = value;
-      });
-
-      return row;
-    });
-
-    results.worksheetTitle = gdocsSpreadsheet.feed.title.$t;
-    return results;
-  };

Convenience function to get GDocs JSON API Url from standard URL

  my.getGDocsAPIUrls = function(url) {

https://docs.google.com/spreadsheet/ccc?key=XXXX#gid=YYY

    var regex = /.*spreadsheet\/ccc?.*key=([^#?&+]+)[^#]*(#gid=([\d]+).*)?/;
-    var matches = url.match(regex);
-    var key;
-    var worksheet;
-    var urls;
-    
-    if(!!matches) {
-        key = matches[1];

the gid in url is 0-based and feed url is 1-based

        worksheet = parseInt(matches[3]) + 1;
-        if (isNaN(worksheet)) {
-          worksheet = 1;
-        }
-        urls = {
-          worksheet  : 'https://spreadsheets.google.com/feeds/list/'+ key +'/'+ worksheet +'/public/values?alt=json',
-          spreadsheet: 'https://spreadsheets.google.com/feeds/worksheets/'+ key +'/public/basic?alt=json'
-        }
-    }
-    else {

we assume that it's one of the feeds urls

        key = url.split('/')[5];

by default then, take first worksheet

        worksheet = 1;
-        urls = {
-          worksheet  : 'https://spreadsheets.google.com/feeds/list/'+ key +'/'+ worksheet +'/public/values?alt=json',
-          spreadsheet: 'https://spreadsheets.google.com/feeds/worksheets/'+ key +'/public/basic?alt=json'
-        }            
-    }
-
-    return urls;
-  };
-}(this.recline.Backend.GDocs));
-
-
\ No newline at end of file diff --git a/docs/src/backend.memory.html b/docs/src/backend.memory.html deleted file mode 100644 index 256c00c8..00000000 --- a/docs/src/backend.memory.html +++ /dev/null @@ -1,631 +0,0 @@ - - - - - backend.memory.js - - - - - -
-
- - - -
    - -
  • -
    -

    backend.memory.js

    -
    -
  • - - - -
  • -
    - -
    - -
    - -
    - -
    this.recline = this.recline || {};
    -this.recline.Backend = this.recline.Backend || {};
    -this.recline.Backend.Memory = this.recline.Backend.Memory || {};
    -
    -(function(my) {
    -  "use strict";
    -  my.__type__ = 'memory';
    - -
  • - - -
  • -
    - -
    - -
    -

    private data - use either jQuery or Underscore Deferred depending on what is available

    - -
    - -
      var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred;
    - -
  • - - -
  • -
    - -
    - -
    -

    Data Wrapper

    -

    Turn a simple array of JS objects into a mini data-store with -functionality like querying, faceting, updating (by ID) and deleting (by -ID).

    -

    @param records list of hashes for each record/row in the data ({key: -value, key: value}) -@param fields (optional) list of field hashes (each hash defining a field -as per recline.Model.Field). If fields not specified they will be taken -from the data.

    - -
    - -
      my.Store = function(records, fields) {
    -    var self = this;
    -    this.records = records;
    - -
  • - - -
  • -
    - -
    - -
    -

    backwards compatability (in v0.5 records was named data)

    - -
    - -
        this.data = this.records;
    -    if (fields) {
    -      this.fields = fields;
    -    } else {
    -      if (records) {
    -        this.fields = _.map(records[0], function(value, key) {
    -          return {id: key, type: 'string'};
    -        });
    -      }
    -    }
    -
    -    this.update = function(doc) {
    -      _.each(self.records, function(internalDoc, idx) {
    -        if(doc.id === internalDoc.id) {
    -          self.records[idx] = doc;
    -        }
    -      });
    -    };
    -
    -    this.remove = function(doc) {
    -      var newdocs = _.reject(self.records, function(internalDoc) {
    -        return (doc.id === internalDoc.id);
    -      });
    -      this.records = newdocs;
    -    };
    -
    -    this.save = function(changes, dataset) {
    -      var self = this;
    -      var dfd = new Deferred();
    - -
  • - - -
  • -
    - -
    - -
    -

    TODO _.each(changes.creates) { … }

    - -
    - -
          _.each(changes.updates, function(record) {
    -        self.update(record);
    -      });
    -      _.each(changes.deletes, function(record) {
    -        self.remove(record);
    -      });
    -      dfd.resolve();
    -      return dfd.promise();
    -    },
    -
    -    this.query = function(queryObj) {
    -      var dfd = new Deferred();
    -      var numRows = queryObj.size || this.records.length;
    -      var start = queryObj.from || 0;
    -      var results = this.records;
    -      
    -      results = this._applyFilters(results, queryObj);
    -      results = this._applyFreeTextQuery(results, queryObj);
    - -
  • - - -
  • -
    - -
    - -
    -

    TODO: this is not complete sorting! -What’s wrong is we sort on the last entry in the sort list if there are multiple sort criteria

    - -
    - -
          _.each(queryObj.sort, function(sortObj) {
    -        var fieldName = sortObj.field;
    -        results = _.sortBy(results, function(doc) {
    -          var _out = doc[fieldName];
    -          return _out;
    -        });
    -        if (sortObj.order == 'desc') {
    -          results.reverse();
    -        }
    -      });
    -      var facets = this.computeFacets(results, queryObj);
    -      var out = {
    -        total: results.length,
    -        hits: results.slice(start, start+numRows),
    -        facets: facets
    -      };
    -      dfd.resolve(out);
    -      return dfd.promise();
    -    };
    - -
  • - - -
  • -
    - -
    - -
    -

    in place filtering

    - -
    - -
        this._applyFilters = function(results, queryObj) {
    -      var filters = queryObj.filters;
    - -
  • - - -
  • -
    - -
    - -
    -

    register filters

    - -
    - -
          var filterFunctions = {
    -        term         : term,
    -        terms        : terms,
    -        range        : range,
    -        geo_distance : geo_distance
    -      };
    -      var dataParsers = {
    -        integer: function (e) { return parseFloat(e, 10); },
    -        'float': function (e) { return parseFloat(e, 10); },
    -        number: function (e) { return parseFloat(e, 10); },
    -        string : function (e) { return e.toString(); },
    -        date   : function (e) { return moment(e).valueOf(); },
    -        datetime   : function (e) { return new Date(e).valueOf(); }
    -      };
    -      var keyedFields = {};
    -      _.each(self.fields, function(field) {
    -        keyedFields[field.id] = field;
    -      });
    -      function getDataParser(filter) {
    -        var fieldType = keyedFields[filter.field].type || 'string';
    -        return dataParsers[fieldType];
    -      }
    - -
  • - - -
  • -
    - -
    - -
    -

    filter records

    - -
    - -
          return _.filter(results, function (record) {
    -        var passes = _.map(filters, function (filter) {
    -          return filterFunctions[filter.type](record, filter);
    -        });
    - -
  • - - -
  • -
    - -
    - -
    -

    return only these records that pass all filters

    - -
    - -
            return _.all(passes, _.identity);
    -      });
    - -
  • - - -
  • -
    - -
    - -
    -

    filters definitions

    - -
    - -
          function term(record, filter) {
    -        var parse = getDataParser(filter);
    -        var value = parse(record[filter.field]);
    -        var term  = parse(filter.term);
    -
    -        return (value === term);
    -      }
    -
    -      function terms(record, filter) {
    -        var parse = getDataParser(filter);
    -        var value = parse(record[filter.field]);
    -        var terms  = parse(filter.terms).split(",");
    -
    -        return (_.indexOf(terms, value) >= 0);
    -      }
    -
    -      function range(record, filter) {
    -        var fromnull = (_.isUndefined(filter.from) || filter.from === null || filter.from === '');
    -        var tonull = (_.isUndefined(filter.to) || filter.to === null || filter.to === '');
    -        var parse = getDataParser(filter);
    -        var value = parse(record[filter.field]);
    -        var from = parse(fromnull ? '' : filter.from);
    -        var to  = parse(tonull ? '' : filter.to);
    - -
  • - - -
  • -
    - -
    - -
    -

    if at least one end of range is set do not allow ā€˜ā€™ to get through -note that for strings ā€˜ā€™ <= {any-character} e.g. ā€˜ā€™ <= ā€˜a’

    - -
    - -
            if ((!fromnull || !tonull) && value === '') {
    -          return false;
    -        }
    -        return ((fromnull || value >= from) && (tonull || value <= to));
    -      }
    -
    -      function geo_distance() {
    - -
  • - - -
  • -
    - -
    - -
    -

    TODO code here

    - -
    - -
          }
    -    };
    - -
  • - - -
  • -
    - -
    - -
    -

    we OR across fields but AND across terms in query string

    - -
    - -
        this._applyFreeTextQuery = function(results, queryObj) {
    -      if (queryObj.q) {
    -        var terms = queryObj.q.split(' ');
    -        var patterns=_.map(terms, function(term) {
    -          return new RegExp(term.toLowerCase());
    -        });
    -        results = _.filter(results, function(rawdoc) {
    -          var matches = true;
    -          _.each(patterns, function(pattern) {
    -            var foundmatch = false;
    -            _.each(self.fields, function(field) {
    -              var value = rawdoc[field.id];
    -              if ((value !== null) && (value !== undefined)) { 
    -                value = value.toString();
    -              } else {
    - -
  • - - -
  • -
    - -
    - -
    -

    value can be null (apparently in some cases)

    - -
    - -
                    value = '';
    -              }
    - -
  • - - -
  • -
    - -
    - -
    -

    TODO regexes?

    - -
    - -
                  foundmatch = foundmatch || (pattern.test(value.toLowerCase()));
    - -
  • - - -
  • -
    - -
    - -
    -

    TODO: early out (once we are true should break to spare unnecessary testing) -if (foundmatch) return true;

    - -
    - -
                });
    -            matches = matches && foundmatch;
    - -
  • - - -
  • -
    - -
    - -
    -

    TODO: early out (once false should break to spare unnecessary testing) -if (!matches) return false;

    - -
    - -
              });
    -          return matches;
    -        });
    -      }
    -      return results;
    -    };
    -
    -    this.computeFacets = function(records, queryObj) {
    -      var facetResults = {};
    -      if (!queryObj.facets) {
    -        return facetResults;
    -      }
    -      _.each(queryObj.facets, function(query, facetId) {
    - -
  • - - -
  • -
    - -
    - -
    -

    TODO: remove dependency on recline.Model

    - -
    - -
            facetResults[facetId] = new recline.Model.Facet({id: facetId}).toJSON();
    -        facetResults[facetId].termsall = {};
    -      });
    - -
  • - - -
  • -
    - -
    - -
    -

    faceting

    - -
    - -
          _.each(records, function(doc) {
    -        _.each(queryObj.facets, function(query, facetId) {
    -          var fieldId = query.terms.field;
    -          var val = doc[fieldId];
    -          var tmp = facetResults[facetId];
    -          if (val) {
    -            tmp.termsall[val] = tmp.termsall[val] ? tmp.termsall[val] + 1 : 1;
    -          } else {
    -            tmp.missing = tmp.missing + 1;
    -          }
    -        });
    -      });
    -      _.each(queryObj.facets, function(query, facetId) {
    -        var tmp = facetResults[facetId];
    -        var terms = _.map(tmp.termsall, function(count, term) {
    -          return { term: term, count: count };
    -        });
    -        tmp.terms = _.sortBy(terms, function(item) {
    - -
  • - - -
  • -
    - -
    - -
    -

    want descending order

    - -
    - -
              return -item.count;
    -        });
    -        tmp.terms = tmp.terms.slice(0, 10);
    -      });
    -      return facetResults;
    -    };
    -  };
    -
    -}(this.recline.Backend.Memory));
    - -
  • - -
-
- - diff --git a/docs/src/demo.search.app.html b/docs/src/demo.search.app.html deleted file mode 100644 index e97a180f..00000000 --- a/docs/src/demo.search.app.html +++ /dev/null @@ -1,236 +0,0 @@ - demo.search.app.js

demo.search.app.js

(c) Open Knowledge Foundation 2012. Dedicated to the public domain. Please -use and reuse freely - you don't even need to credit (though a link back to -ReclineJS.com is always appreciated)!

Our main loop - on document ready

jQuery(function($) {
-  var $el = $('.search-here');

Overview

- -

We have a slightly more complex setup than is needed to allow for using -this demo with different backends

- -

There are 2 alternatives: more complex and a simpler one

- -

If you just want to see how this work skip to the simple case ...

1. More complex - use data from a backend configured in query string

Check for config from url query string

  var config = recline.View.parseQueryString(decodeURIComponent(window.location.search));
-  if (config.backend) {

If we had it hand off to our more complex example setup

    setupMoreComplexExample(config);
-    return;
-  }

2. The simple example case

- -

We will just set up from some example local data (at the bottom of thile file)

Create our Recline Dataset from sample local data

  var dataset = new recline.Model.Dataset({
-    records: sampleData
-  });

Custom template

- -

Create a custom template for rendering the records

  var template = ' \
-    <div class="record"> \
-      <h3> \
-        {{title}} <em>by {{Author}}</em> \
-      </h3> \
-      <p>{{description}}</p> \
-      <p><code>${{price}}</code></p> \
-    </div> \
-  ';

Set up the search View (using custom template)

  var searchView = new SearchView({
-    el: $el,
-    model: dataset,
-    template: template 
-  });
-  searchView.render();

Optional - we configure the initial query a bit and set up facets

  dataset.queryState.set({
-      size: 10
-    },
-    {silent: true}
-  );
-  dataset.queryState.addFacet('Author');

Finally - now do the first query

- -

After this point the Search View will take over handling queries!

  dataset.query();
-});

Simple Search View

- -

This is a simple bespoke Backbone view for the Search. It Pulls together -various Recline UI components and the central Dataset and Query (state) -object

- -

It also provides simple support for customization e.g. of template for list of results

- -
 var view = new SearchView({
-   el: $('some-element'),
-   model: dataset
-   // EITHER a mustache template (passed a JSON version of recline.Model.Record
-   // OR a function which receives a record in JSON form and returns html
-   template: mustache-template-or-function
- });
-
var SearchView = Backbone.View.extend({
-  initialize: function(options) {
-    this.el = $(this.el);
-    _.bindAll(this, 'render');
-    this.recordTemplate = options.template;

Every time we do a search the recline.Dataset.records Backbone -collection will get reset. We want to re-render each time!

    this.model.records.bind('reset', this.render);
-    this.templateResults = options.template;
-  },

overall template for this view

  template: ' \
-    <div class="controls"> \
-      <div class="query-here"></div> \
-    </div> \
-    <div class="total"><h2><span></span> records found</h2></div> \
-    <div class="body"> \
-      <div class="sidebar"></div> \
-      <div class="results"> \
-        {{{results}}} \
-      </div> \
-    </div> \
-    <div class="pager-here"></div> \
-  ',
- 

render the view

  render: function() {
-    var results = '';
-    if (_.isFunction(this.templateResults)) {
-      var results = _.map(this.model.records.toJSON(), this.templateResults).join('\n');
-    } else {

templateResults is just for one result ...

      var tmpl = '{{#records}}' + this.templateResults + '{{/records}}'; 
-      var results = Mustache.render(tmpl, {
-        records: this.model.records.toJSON()
-      });
-    }
-    var html = Mustache.render(this.template, {
-      results: results
-    });
-    this.el.html(html);

Set the total records found info

    this.el.find('.total span').text(this.model.recordCount);

Now setup all the extra mini-widgets

- -

Facets, Pager, QueryEditor etc

    var view = new recline.View.FacetViewer({
-      model: this.model
-    });
-    view.render();
-    this.el.find('.sidebar').append(view.el);
-
-    var pager = new recline.View.Pager({
-      model: this.model.queryState
-    });
-    this.el.find('.pager-here').append(pager.el);
-
-    var queryEditor = new recline.View.QueryEditor({
-      model: this.model.queryState
-    });
-    this.el.find('.query-here').append(queryEditor.el);
-  }
-});

- -

Custom code very specific to this demo

e.g. to provide custom templates for the google doc and openspending examples

Handle case where we get data from a specific backend

- -

Includes providing custom templates

function setupMoreComplexExample(config) {
-  var $el = $('.search-here');
-  var dataset = new recline.Model.Dataset(config);

async as may be fetching remote

  dataset.fetch().done(function() {
-    var template = templates[dataset.get('url')] || templates['generic'];
-    var searchView = new SearchView({
-      el: $el,
-      model: dataset,
-      template: template 
-    });
-    searchView.render();
-
-    dataset.queryState.set({
-        size: 5
-      },
-      {silent: true}
-    );
-    if (dataset.get('url') in templates) {

for gdocs example

      dataset.queryState.addFacet('cause');
-    }
-    dataset.query();
-  });
-};
-
-var templates = {

generic template function

  'generic': function(record) {
-    var template = '<div class="record"> \
-      <ul> \
-       {{#data}} \
-       <li>{{key}}: {{value}}</li> \
-       {{/data}} \
-     </ul> \
-    </div> \
-    ';
-    var data = _.map(_.keys(record), function(key) {
-      return { key: key, value: record[key] };
-    });
-    return Mustache.render(template, {
-      data: data
-    });
-  },
-  'http://openspending.org/api/search': function(record) {
-    record['time'] = record['time.label_facet']
-    var template = '<div class="record"> \
-      <h3> \
-        <a href="http://openspending.org/{{record.dataset}}/entries/{{record.id}}">{{record.dataset}} {{record.time}}</a> \
-        &ndash; <img src="http://openspending.org/static/img/icons/cd_16x16.png" /> {{amount_formatted}} \
-      </h3> \
-      <ul> \
-       {{#data}} \
-         <li>{{key}}: {{value}}</li> \
-       {{/data}} \
-       </ul> \
-    </div> \
-    ';
-    var data = [];
-    _.each(_.keys(record), function(key) {
-      if (key !='_id' && key != 'id') {
-        data.push({ key: key, value: record[key] });
-      }
-    });
-    return Mustache.render(template, {
-      record: record,
-      amount_formatted: formatAmount(record['amount']),
-      data: data
-    });
-  },
-  'https://docs.google.com/spreadsheet/ccc?key=0Aon3JiuouxLUdExXSTl2Y01xZEszOTBFZjVzcGtzVVE': function(record) {
-    var template = '<div class="record"> \
-      <h3> \
-        {{record.incidentsite}} &ndash; {{record.datereported}} &ndash; {{record.estimatedspillvolumebbl}} barrels \
-      </h3> \
-      <ul> \
-       {{#data}} \
-         <li>{{key}}: {{value}}</li> \
-       {{/data}} \
-       </ul> \
-    </div> \
-    ';
-    var data = [];
-    _.each(_.keys(record), function(key) {
-      data.push({ key: key, value: record[key] });
-    });
-    return Mustache.render(template, {
-      record: record,
-      data: data
-    });
-  }
-}
-
-var sampleData = [
-  {
-    title: 'War and Peace',
-    description: 'The epic tale of love, war and history',
-    Author: 'Tolstoy',
-    price: 7.99
-  },
-  {
-    title: 'Anna Karenina',
-    description: 'How things go wrong in love and ultimately lead to suicide. This is why you should not have affairs, girls!',
-    Author: 'Tolstoy',
-    price: 8.50
-  },
-  {
-    title: "Fathers and Sons",
-    description: "Another 19th century Russian novel",
-    Author: "Turgenev",
-    price: 11
-  }
-];
-
-var formatAmount = function (num) {
-  var billion = 1000000000;
-  var million = 1000000;
-  var thousand = 1000;
-  var numabs = Math.abs(num);
-  if (numabs > billion) {
-    return (num / billion).toFixed(0) + 'bn';
-  }
-  if (numabs > (million / 2)) {
-    return (num / million).toFixed(0) + 'm';
-  }
-  if (numabs > thousand) {
-    return (num / thousand).toFixed(0) + 'k';
-  } else {
-    return num.toFixed(0);
-  }
-};
-
-
\ No newline at end of file diff --git a/docs/src/docco.css b/docs/src/docco.css deleted file mode 100644 index b60f6fa3..00000000 --- a/docs/src/docco.css +++ /dev/null @@ -1,518 +0,0 @@ -/*--------------------- Typography ----------------------------*/ - -@font-face { - font-family: 'aller-light'; - src: url('public/fonts/aller-light.eot'); - src: url('public/fonts/aller-light.eot?#iefix') format('embedded-opentype'), - url('public/fonts/aller-light.woff') format('woff'), - url('public/fonts/aller-light.ttf') format('truetype'); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'aller-bold'; - src: url('public/fonts/aller-bold.eot'); - src: url('public/fonts/aller-bold.eot?#iefix') format('embedded-opentype'), - url('public/fonts/aller-bold.woff') format('woff'), - url('public/fonts/aller-bold.ttf') format('truetype'); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'roboto-black'; - src: url('public/fonts/roboto-black.eot'); - src: url('public/fonts/roboto-black.eot?#iefix') format('embedded-opentype'), - url('public/fonts/roboto-black.woff') format('woff'), - url('public/fonts/roboto-black.ttf') format('truetype'); - font-weight: normal; - font-style: normal; -} - -/*--------------------- Layout ----------------------------*/ -html { height: 100%; } -body { - font-family: "aller-light"; - font-size: 14px; - line-height: 18px; - color: #30404f; - margin: 0; padding: 0; - height:100%; -} -#container { min-height: 100%; } - -a { - color: #000; -} - -b, strong { - font-weight: normal; - font-family: "aller-bold"; -} - -p { - margin: 15px 0 0px; -} - .annotation ul, .annotation ol { - margin: 25px 0; - } - .annotation ul li, .annotation ol li { - font-size: 14px; - line-height: 18px; - margin: 10px 0; - } - -h1, h2, h3, h4, h5, h6 { - color: #112233; - line-height: 1em; - font-weight: normal; - font-family: "roboto-black"; - text-transform: uppercase; - margin: 30px 0 15px 0; -} - -h1 { - margin-top: 40px; -} -h2 { - font-size: 1.26em; -} - -hr { - border: 0; - background: 1px #ddd; - height: 1px; - margin: 20px 0; -} - -pre, tt, code { - font-size: 12px; line-height: 16px; - font-family: Menlo, Monaco, Consolas, "Lucida Console", monospace; - margin: 0; padding: 0; -} - .annotation pre { - display: block; - margin: 0; - padding: 7px 10px; - background: #fcfcfc; - -moz-box-shadow: inset 0 0 10px rgba(0,0,0,0.1); - -webkit-box-shadow: inset 0 0 10px rgba(0,0,0,0.1); - box-shadow: inset 0 0 10px rgba(0,0,0,0.1); - overflow-x: auto; - } - .annotation pre code { - border: 0; - padding: 0; - background: transparent; - } - - -blockquote { - border-left: 5px solid #ccc; - margin: 0; - padding: 1px 0 1px 1em; -} - .sections blockquote p { - font-family: Menlo, Consolas, Monaco, monospace; - font-size: 12px; line-height: 16px; - color: #999; - margin: 10px 0 0; - white-space: pre-wrap; - } - -ul.sections { - list-style: none; - padding:0 0 5px 0;; - margin:0; -} - -/* - Force border-box so that % widths fit the parent - container without overlap because of margin/padding. - - More Info : http://www.quirksmode.org/css/box.html -*/ -ul.sections > li > div { - -moz-box-sizing: border-box; /* firefox */ - -ms-box-sizing: border-box; /* ie */ - -webkit-box-sizing: border-box; /* webkit */ - -khtml-box-sizing: border-box; /* konqueror */ - box-sizing: border-box; /* css3 */ -} - - -/*---------------------- Jump Page -----------------------------*/ -#jump_to, #jump_page { - margin: 0; - background: white; - -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; - -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; - font: 16px Arial; - cursor: pointer; - text-align: right; - list-style: none; -} - -#jump_to a { - text-decoration: none; -} - -#jump_to a.large { - display: none; -} -#jump_to a.small { - font-size: 22px; - font-weight: bold; - color: #676767; -} - -#jump_to, #jump_wrapper { - position: fixed; - right: 0; top: 0; - padding: 10px 15px; - margin:0; -} - -#jump_wrapper { - display: none; - padding:0; -} - -#jump_to:hover #jump_wrapper { - display: block; -} - -#jump_page_wrapper{ - position: fixed; - right: 0; - top: 0; - bottom: 0; -} - -#jump_page { - padding: 5px 0 3px; - margin: 0 0 25px 25px; - max-height: 100%; - overflow: auto; -} - -#jump_page .source { - display: block; - padding: 15px; - text-decoration: none; - border-top: 1px solid #eee; -} - -#jump_page .source:hover { - background: #f5f5ff; -} - -#jump_page .source:first-child { -} - -/*---------------------- Low resolutions (> 320px) ---------------------*/ -@media only screen and (min-width: 320px) { - .pilwrap { display: none; } - - ul.sections > li > div { - display: block; - padding:5px 10px 0 10px; - } - - ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { - padding-left: 30px; - } - - ul.sections > li > div.content { - overflow-x:auto; - -webkit-box-shadow: inset 0 0 5px #e5e5ee; - box-shadow: inset 0 0 5px #e5e5ee; - border: 1px solid #dedede; - margin:5px 10px 5px 10px; - padding-bottom: 5px; - } - - ul.sections > li > div.annotation pre { - margin: 7px 0 7px; - padding-left: 15px; - } - - ul.sections > li > div.annotation p tt, .annotation code { - background: #f8f8ff; - border: 1px solid #dedede; - font-size: 12px; - padding: 0 0.2em; - } -} - -/*---------------------- (> 481px) ---------------------*/ -@media only screen and (min-width: 481px) { - #container { - position: relative; - } - body { - background-color: #F5F5FF; - font-size: 15px; - line-height: 21px; - } - pre, tt, code { - line-height: 18px; - } - p, ul, ol { - margin: 0 0 15px; - } - - - #jump_to { - padding: 5px 10px; - } - #jump_wrapper { - padding: 0; - } - #jump_to, #jump_page { - font: 10px Arial; - text-transform: uppercase; - } - #jump_page .source { - padding: 5px 10px; - } - #jump_to a.large { - display: inline-block; - } - #jump_to a.small { - display: none; - } - - - - #background { - position: absolute; - top: 0; bottom: 0; - width: 350px; - background: #fff; - border-right: 1px solid #e5e5ee; - z-index: -1; - } - - ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { - padding-left: 40px; - } - - ul.sections > li { - white-space: nowrap; - } - - ul.sections > li > div { - display: inline-block; - } - - ul.sections > li > div.annotation { - max-width: 350px; - min-width: 350px; - min-height: 5px; - padding: 13px; - overflow-x: hidden; - white-space: normal; - vertical-align: top; - text-align: left; - } - ul.sections > li > div.annotation pre { - margin: 15px 0 15px; - padding-left: 15px; - } - - ul.sections > li > div.content { - padding: 13px; - vertical-align: top; - border: none; - -webkit-box-shadow: none; - box-shadow: none; - } - - .pilwrap { - position: relative; - display: inline; - } - - .pilcrow { - font: 12px Arial; - text-decoration: none; - color: #454545; - position: absolute; - top: 3px; left: -20px; - padding: 1px 2px; - opacity: 0; - -webkit-transition: opacity 0.2s linear; - } - .for-h1 .pilcrow { - top: 47px; - } - .for-h2 .pilcrow, .for-h3 .pilcrow, .for-h4 .pilcrow { - top: 35px; - } - - ul.sections > li > div.annotation:hover .pilcrow { - opacity: 1; - } -} - -/*---------------------- (> 1025px) ---------------------*/ -@media only screen and (min-width: 1025px) { - - body { - font-size: 16px; - line-height: 24px; - } - - #background { - width: 525px; - } - ul.sections > li > div.annotation { - max-width: 525px; - min-width: 525px; - padding: 10px 25px 1px 50px; - } - ul.sections > li > div.content { - padding: 9px 15px 16px 25px; - } -} - -/*---------------------- Syntax Highlighting -----------------------------*/ - -td.linenos { background-color: #f0f0f0; padding-right: 10px; } -span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } -/* - -github.com style (c) Vasily Polovnyov - -*/ - -pre code { - display: block; padding: 0.5em; - color: #000; - background: #f8f8ff -} - -pre .hljs-comment, -pre .hljs-template_comment, -pre .hljs-diff .hljs-header, -pre .hljs-javadoc { - color: #408080; - font-style: italic -} - -pre .hljs-keyword, -pre .hljs-assignment, -pre .hljs-literal, -pre .hljs-css .hljs-rule .hljs-keyword, -pre .hljs-winutils, -pre .hljs-javascript .hljs-title, -pre .hljs-lisp .hljs-title, -pre .hljs-subst { - color: #954121; - /*font-weight: bold*/ -} - -pre .hljs-number, -pre .hljs-hexcolor { - color: #40a070 -} - -pre .hljs-string, -pre .hljs-tag .hljs-value, -pre .hljs-phpdoc, -pre .hljs-tex .hljs-formula { - color: #219161; -} - -pre .hljs-title, -pre .hljs-id { - color: #19469D; -} -pre .hljs-params { - color: #00F; -} - -pre .hljs-javascript .hljs-title, -pre .hljs-lisp .hljs-title, -pre .hljs-subst { - font-weight: normal -} - -pre .hljs-class .hljs-title, -pre .hljs-haskell .hljs-label, -pre .hljs-tex .hljs-command { - color: #458; - font-weight: bold -} - -pre .hljs-tag, -pre .hljs-tag .hljs-title, -pre .hljs-rules .hljs-property, -pre .hljs-django .hljs-tag .hljs-keyword { - color: #000080; - font-weight: normal -} - -pre .hljs-attribute, -pre .hljs-variable, -pre .hljs-instancevar, -pre .hljs-lisp .hljs-body { - color: #008080 -} - -pre .hljs-regexp { - color: #B68 -} - -pre .hljs-class { - color: #458; - font-weight: bold -} - -pre .hljs-symbol, -pre .hljs-ruby .hljs-symbol .hljs-string, -pre .hljs-ruby .hljs-symbol .hljs-keyword, -pre .hljs-ruby .hljs-symbol .hljs-keymethods, -pre .hljs-lisp .hljs-keyword, -pre .hljs-tex .hljs-special, -pre .hljs-input_number { - color: #990073 -} - -pre .hljs-builtin, -pre .hljs-constructor, -pre .hljs-built_in, -pre .hljs-lisp .hljs-title { - color: #0086b3 -} - -pre .hljs-preprocessor, -pre .hljs-pi, -pre .hljs-doctype, -pre .hljs-shebang, -pre .hljs-cdata { - color: #999; - font-weight: bold -} - -pre .hljs-deletion { - background: #fdd -} - -pre .hljs-addition { - background: #dfd -} - -pre .hljs-diff .hljs-change { - background: #0086b3 -} - -pre .hljs-chunk { - color: #aaa -} - -pre .hljs-tex .hljs-formula { - opacity: 0.5; -} diff --git a/docs/src/ecma-fixes.html b/docs/src/ecma-fixes.html deleted file mode 100644 index 7839ba34..00000000 --- a/docs/src/ecma-fixes.html +++ /dev/null @@ -1,215 +0,0 @@ - - - - - ecma-fixes.js - - - - - -
-
- - - -
    - -
  • -
    -

    ecma-fixes.js

    -
    -
  • - - - -
  • -
    - -
    - -
    -

    This file adds in full array method support in browsers that don’t support it -see: http://stackoverflow.com/questions/2790001/fixing-javascript-array-functions-in-internet-explorer-indexof-foreach-etc

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    Add ECMA262-5 Array methods if not supported natively

    - -
    - -
    if (!('indexOf' in Array.prototype)) {
    -    Array.prototype.indexOf= function(find, i /*opt*/) {
    -        if (i===undefined) i= 0;
    -        if (i<0) i+= this.length;
    -        if (i<0) i= 0;
    -        for (var n= this.length; i<n; i++)
    -            if (i in this && this[i]===find)
    -                return i;
    -        return -1;
    -    };
    -}
    -if (!('lastIndexOf' in Array.prototype)) {
    -    Array.prototype.lastIndexOf= function(find, i /*opt*/) {
    -        if (i===undefined) i= this.length-1;
    -        if (i<0) i+= this.length;
    -        if (i>this.length-1) i= this.length-1;
    -        for (i++; i-->0;) /* i++ because from-argument is sadly inclusive */
    -            if (i in this && this[i]===find)
    -                return i;
    -        return -1;
    -    };
    -}
    -if (!('forEach' in Array.prototype)) {
    -    Array.prototype.forEach= function(action, that /*opt*/) {
    -        for (var i= 0, n= this.length; i<n; i++)
    -            if (i in this)
    -                action.call(that, this[i], i, this);
    -    };
    -}
    -if (!('map' in Array.prototype)) {
    -    Array.prototype.map= function(mapper, that /*opt*/) {
    -        var other= new Array(this.length);
    -        for (var i= 0, n= this.length; i<n; i++)
    -            if (i in this)
    -                other[i]= mapper.call(that, this[i], i, this);
    -        return other;
    -    };
    -}
    -if (!('filter' in Array.prototype)) {
    -    Array.prototype.filter= function(filter, that /*opt*/) {
    -        var other= [], v;
    -        for (var i=0, n= this.length; i<n; i++)
    -            if (i in this && filter.call(that, v= this[i], i, this))
    -                other.push(v);
    -        return other;
    -    };
    -}
    -if (!('every' in Array.prototype)) {
    -    Array.prototype.every= function(tester, that /*opt*/) {
    -        for (var i= 0, n= this.length; i<n; i++)
    -            if (i in this && !tester.call(that, this[i], i, this))
    -                return false;
    -        return true;
    -    };
    -}
    -if (!('some' in Array.prototype)) {
    -    Array.prototype.some= function(tester, that /*opt*/) {
    -        for (var i= 0, n= this.length; i<n; i++)
    -            if (i in this && tester.call(that, this[i], i, this))
    -                return true;
    -        return false;
    -    };
    -}
    - -
  • - -
-
- - diff --git a/docs/src/model.html b/docs/src/model.html deleted file mode 100644 index d35b2ceb..00000000 --- a/docs/src/model.html +++ /dev/null @@ -1,1375 +0,0 @@ - - - - - Recline Backbone Models - - - - - -
-
- - - -
    - - - -
  • -
    - -
    - -
    -

    Recline Backbone Models

    - -
    - -
    this.recline = this.recline || {};
    -this.recline.Model = this.recline.Model || {};
    -
    -(function(my) {
    -  "use strict";
    - -
  • - - -
  • -
    - -
    - -
    -

    use either jQuery or Underscore Deferred depending on what is available

    - -
    - -
    var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred;
    - -
  • - - -
  • -
    - -
    - -
    -

    Dataset

    - -
    - -
    my.Dataset = Backbone.Model.extend({
    -  constructor: function Dataset() {
    -    Backbone.Model.prototype.constructor.apply(this, arguments);
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    initialize

    - -
    - -
      initialize: function() {
    -    var self = this;
    -    _.bindAll(this, 'query');
    -    this.backend = null;
    -    if (this.get('backend')) {
    -      this.backend = this._backendFromString(this.get('backend'));
    -    } else { // try to guess backend ...
    -      if (this.get('records')) {
    -        this.backend = recline.Backend.Memory;
    -      }
    -    }
    -    this.fields = new my.FieldList();
    -    this.records = new my.RecordList();
    -    this._changes = {
    -      deletes: [],
    -      updates: [],
    -      creates: []
    -    };
    -    this.facets = new my.FacetList();
    -    this.recordCount = null;
    -    this.queryState = new my.Query();
    -    this.queryState.bind('change facet:add', function () {
    -      self.query(); // We want to call query() without any arguments.
    -    });
    - -
  • - - -
  • -
    - -
    - -
    -

    store is what we query and save against -store will either be the backend or be a memory store if Backend fetch -tells us to use memory store

    - -
    - -
        this._store = this.backend;
    - -
  • - - -
  • -
    - -
    - -
    -

    if backend has a handleQueryResultFunction, use that

    - -
    - -
        this._handleResult = (this.backend != null && _.has(this.backend, 'handleQueryResult')) ? 
    -      this.backend.handleQueryResult : this._handleQueryResult;
    -    if (this.backend == recline.Backend.Memory) {
    -      this.fetch();
    -    }
    -  },
    -
    -  sync: function(method, model, options) {
    -    return this.backend.sync(method, model, options);
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    fetch

    -

    Retrieve dataset and (some) records from the backend.

    - -
    - -
      fetch: function() {
    -    var self = this;
    -    var dfd = new Deferred();
    -
    -    if (this.backend !== recline.Backend.Memory) {
    -      this.backend.fetch(this.toJSON())
    -        .done(handleResults)
    -        .fail(function(args) {
    -          dfd.reject(args);
    -        });
    -    } else {
    - -
  • - - -
  • -
    - -
    - -
    -

    special case where we have been given data directly

    - -
    - -
          handleResults({
    -        records: this.get('records'),
    -        fields: this.get('fields'),
    -        useMemoryStore: true
    -      });
    -    }
    -
    -    function handleResults(results) {
    - -
  • - - -
  • -
    - -
    - -
    -

    if explicitly given the fields -(e.g. var dataset = new Dataset({fields: fields, …}) -use that field info over anything we get back by parsing the data -(results.fields)

    - -
    - -
          var fields = self.get('fields') || results.fields;
    -
    -      var out = self._normalizeRecordsAndFields(results.records, fields);
    -      if (results.useMemoryStore) {
    -        self._store = new recline.Backend.Memory.Store(out.records, out.fields);
    -      }
    -
    -      self.set(results.metadata);
    -      self.fields.reset(out.fields);
    -      self.query()
    -        .done(function() {
    -          dfd.resolve(self);
    -        })
    -        .fail(function(args) {
    -          dfd.reject(args);
    -        });
    -    }
    -
    -    return dfd.promise();
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    _normalizeRecordsAndFields

    -

    Get a proper set of fields and records from incoming set of fields and records either of which may be null or arrays or objects

    -

    e.g. fields = [ā€˜a’, ā€˜b’, ā€˜c’] and records = [ [1,2,3] ] => -fields = [ {id: a}, {id: b}, {id: c}], records = [ {a: 1}, {b: 2}, {c: 3}]

    - -
    - -
      _normalizeRecordsAndFields: function(records, fields) {
    - -
  • - - -
  • -
    - -
    - -
    -

    if no fields get them from records

    - -
    - -
        if (!fields && records && records.length > 0) {
    - -
  • - - -
  • -
    - -
    - -
    -

    records is array then fields is first row of records …

    - -
    - -
          if (records[0] instanceof Array) {
    -        fields = records[0];
    -        records = records.slice(1);
    -      } else {
    -        fields = _.map(_.keys(records[0]), function(key) {
    -          return {id: key};
    -        });
    -      }
    -    }
    - -
  • - - -
  • -
    - -
    - -
    -

    fields is an array of strings (i.e. list of field headings/ids)

    - -
    - -
        if (fields && fields.length > 0 && (fields[0] === null || typeof(fields[0]) != 'object')) {
    - -
  • - - -
  • -
    - -
    - -
    -

    Rename duplicate fieldIds as each field name needs to be -unique.

    - -
    - -
          var seen = {};
    -      fields = _.map(fields, function(field, index) {
    -        if (field === null) {
    -          field = '';
    -        } else {
    -          field = field.toString();
    -        }
    - -
  • - - -
  • -
    - -
    - -
    -

    cannot use trim as not supported by IE7

    - -
    - -
            var fieldId = field.replace(/^\s+|\s+$/g, '');
    -        if (fieldId === '') {
    -          fieldId = '_noname_';
    -          field = fieldId;
    -        }
    -        while (fieldId in seen) {
    -          seen[field] += 1;
    -          fieldId = field + seen[field];
    -        }
    -        if (!(field in seen)) {
    -          seen[field] = 0;
    -        }
    - -
  • - - -
  • -
    - -
    - -
    -

    TODO: decide whether to keep original name as label … -return { id: fieldId, label: field || fieldId }

    - -
    - -
            return { id: fieldId };
    -      });
    -    }
    - -
  • - - -
  • -
    - -
    - -
    -

    records is provided as arrays so need to zip together with fields -NB: this requires you to have fields to match arrays

    - -
    - -
        if (records && records.length > 0 && records[0] instanceof Array) {
    -      records = _.map(records, function(doc) {
    -        var tmp = {};
    -        _.each(fields, function(field, idx) {
    -          tmp[field.id] = doc[idx];
    -        });
    -        return tmp;
    -      });
    -    }
    -    return {
    -      fields: fields,
    -      records: records
    -    };
    -  },
    -
    -  save: function() {
    -    var self = this;
    - -
  • - - -
  • -
    - -
    - -
    -

    TODO: need to reset the changes …

    - -
    - -
        return this._store.save(this._changes, this.toJSON());
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    query

    -

    AJAX method with promise API to get records from the backend.

    -

    It will query based on current query state (given by this.queryState) -updated by queryObj (if provided).

    -

    Resulting RecordList are used to reset this.records and are -also returned.

    - -
    - -
      query: function(queryObj) {
    -    var self = this;
    -    var dfd = new Deferred();
    -    this.trigger('query:start');
    -
    -    if (queryObj) {
    -      var attributes = queryObj;
    -      if (queryObj instanceof my.Query) {
    -        attributes = queryObj.toJSON();
    -      }
    -      this.queryState.set(attributes, {silent: true});
    -    }
    -    var actualQuery = this.queryState.toJSON();
    -
    -    this._store.query(actualQuery, this.toJSON())
    -      .done(function(queryResult) {
    -        self._handleResult(queryResult);
    -        self.trigger('query:done');
    -        dfd.resolve(self.records);
    -      })
    -      .fail(function(args) {
    -        self.trigger('query:fail', args);
    -        dfd.reject(args);
    -      });
    -    return dfd.promise();
    -  },
    -
    -  _handleQueryResult: function(queryResult) {
    -    var self = this;
    -    self.recordCount = queryResult.total;
    -    var docs = _.map(queryResult.hits, function(hit) {
    -      var _doc = new my.Record(hit);
    -      _doc.fields = self.fields;
    -      _doc.bind('change', function(doc) {
    -        self._changes.updates.push(doc.toJSON());
    -      });
    -      _doc.bind('destroy', function(doc) {
    -        self._changes.deletes.push(doc.toJSON());
    -      });
    -      return _doc;
    -    });
    -    self.records.reset(docs);
    -    if (queryResult.facets) {
    -      var facets = _.map(queryResult.facets, function(facetResult, facetId) {
    -        facetResult.id = facetId;
    -        return new my.Facet(facetResult);
    -      });
    -      self.facets.reset(facets);
    -    }
    -  },
    -
    -  toTemplateJSON: function() {
    -    var data = this.toJSON();
    -    data.recordCount = this.recordCount;
    -    data.fields = this.fields.toJSON();
    -    return data;
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    getFieldsSummary

    -

    Get a summary for each field in the form of a Facet.

    -

    @return null as this is async function. Provides deferred/promise interface.

    - -
    - -
      getFieldsSummary: function() {
    -    var self = this;
    -    var query = new my.Query();
    -    query.set({size: 0});
    -    this.fields.each(function(field) {
    -      query.addFacet(field.id);
    -    });
    -    var dfd = new Deferred();
    -    this._store.query(query.toJSON(), this.toJSON()).done(function(queryResult) {
    -      if (queryResult.facets) {
    -        _.each(queryResult.facets, function(facetResult, facetId) {
    -          facetResult.id = facetId;
    -          var facet = new my.Facet(facetResult);
    - -
  • - - -
  • -
    - -
    - -
    -

    TODO: probably want replace rather than reset (i.e. just replace the facet with this id)

    - -
    - -
              self.fields.get(facetId).facets.reset(facet);
    -        });
    -      }
    -      dfd.resolve(queryResult);
    -    });
    -    return dfd.promise();
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    Deprecated (as of v0.5) - use record.summary()

    - -
    - -
      recordSummary: function(record) {
    -    return record.summary();
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    _backendFromString(backendString)

    -

    Look up a backend module from a backend string (look in recline.Backend)

    - -
    - -
      _backendFromString: function(backendString) {
    -    var backend = null;
    -    if (recline && recline.Backend) {
    -      _.each(_.keys(recline.Backend), function(name) {
    -        if (name.toLowerCase() === backendString.toLowerCase()) {
    -          backend = recline.Backend[name];
    -        }
    -      });
    -    }
    -    return backend;
    -  }
    -});
    - -
  • - - -
  • -
    - -
    - -
    -

    A Record

    -

    A single record (or row) in the dataset

    - -
    - -
    my.Record = Backbone.Model.extend({
    -  constructor: function Record() {
    -    Backbone.Model.prototype.constructor.apply(this, arguments);
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    initialize

    -

    Create a Record

    -

    You usually will not do this directly but will have records created by -Dataset e.g. in query method

    -

    Certain methods require presence of a fields attribute (identical to that on Dataset)

    - -
    - -
      initialize: function() {
    -    _.bindAll(this, 'getFieldValue');
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    getFieldValue

    -

    For the provided Field get the corresponding rendered computed data value -for this record.

    -

    NB: if field is undefined a default ā€˜ā€™ value will be returned

    - -
    - -
      getFieldValue: function(field) {
    -    var val = this.getFieldValueUnrendered(field);
    -    if (field && !_.isUndefined(field.renderer)) {
    -      val = field.renderer(val, field, this.toJSON());
    -    }
    -    return val;
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    getFieldValueUnrendered

    -

    For the provided Field get the corresponding computed data value -for this record.

    -

    NB: if field is undefined a default ā€˜ā€™ value will be returned

    - -
    - -
      getFieldValueUnrendered: function(field) {
    -    if (!field) {
    -      return '';
    -    }
    -    var val = this.get(field.id);
    -    if (field.deriver) {
    -      val = field.deriver(val, field, this);
    -    }
    -    return val;
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    summary

    -

    Get a simple html summary of this record in form of key/value list

    - -
    - -
      summary: function(record) {
    -    var self = this;
    -    var html = '<div class="recline-record-summary">';
    -    this.fields.each(function(field) { 
    -      if (field.id != 'id') {
    -        html += '<div class="' + field.id + '"><strong>' + field.get('label') + '</strong>: ' + self.getFieldValue(field) + '</div>';
    -      }
    -    });
    -    html += '</div>';
    -    return html;
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    Override Backbone save, fetch and destroy so they do nothing -Instead, Dataset object that created this Record should take care of -handling these changes (discovery will occur via event notifications) -WARNING: these will not persist unless you call save on Dataset

    - -
    - -
      fetch: function() {},
    -  save: function() {},
    -  destroy: function() { this.trigger('destroy', this); }
    -});
    - -
  • - - -
  • -
    - -
    - -
    -

    A Backbone collection of Records

    - -
    - -
    my.RecordList = Backbone.Collection.extend({
    -  constructor: function RecordList() {
    -    Backbone.Collection.prototype.constructor.apply(this, arguments);
    -  },
    -  model: my.Record
    -});
    - -
  • - - -
  • - - -
    my.Field = Backbone.Model.extend({
    -  constructor: function Field() {
    -    Backbone.Model.prototype.constructor.apply(this, arguments);
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    defaults - define default values

    - -
    - -
      defaults: {
    -    label: null,
    -    type: 'string',
    -    format: null,
    -    is_derived: false
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    initialize

    -

    @param {Object} data: standard Backbone model attributes

    -

    @param {Object} options: renderer and/or deriver functions.

    - -
    - -
      initialize: function(data, options) {
    - -
  • - - -
  • -
    - -
    - -
    -

    if a hash not passed in the first argument throw error

    - -
    - -
        if ('0' in data) {
    -      throw new Error('Looks like you did not pass a proper hash with id to Field constructor');
    -    }
    -    if (this.attributes.label === null) {
    -      this.set({label: this.id});
    -    }
    -    if (this.attributes.type.toLowerCase() in this._typeMap) {
    -      this.attributes.type = this._typeMap[this.attributes.type.toLowerCase()];
    -    }
    -    if (options) {
    -      this.renderer = options.renderer;
    -      this.deriver = options.deriver;
    -    }
    -    if (!this.renderer) {
    -      this.renderer = this.defaultRenderers[this.get('type')];
    -    }
    -    this.facets = new my.FacetList();
    -  },
    -  _typeMap: {
    -    'text': 'string',
    -    'double': 'number',
    -    'float': 'number',
    -    'numeric': 'number',
    -    'int': 'integer',
    -    'datetime': 'date-time',
    -    'bool': 'boolean',
    -    'timestamp': 'date-time',
    -    'json': 'object'
    -  },
    -  defaultRenderers: {
    -    object: function(val, field, doc) {
    -      return JSON.stringify(val);
    -    },
    -    geo_point: function(val, field, doc) {
    -      return JSON.stringify(val);
    -    },
    -    'number': function(val, field, doc) {
    -      var format = field.get('format'); 
    -      if (format === 'percentage') {
    -        return val + '%';
    -      }
    -      return val;
    -    },
    -    'string': function(val, field, doc) {
    -      var format = field.get('format');
    -      if (format === 'markdown') {
    -        if (typeof Showdown !== 'undefined') {
    -          var showdown = new Showdown.converter();
    -          out = showdown.makeHtml(val);
    -          return out;
    -        } else {
    -          return val;
    -        }
    -      } else if (format == 'plain') {
    -        return val;
    -      } else {
    - -
  • - - -
  • -
    - -
    - -
    -

    as this is the default and default type is string may get things -here that are not actually strings

    - -
    - -
            if (val && typeof val === 'string') {
    -          val = val.replace(/(https?:\/\/[^ ]+)/g, '<a href="$1">$1</a>');
    -        }
    -        return val;
    -      }
    -    }
    -  }
    -});
    -
    -my.FieldList = Backbone.Collection.extend({
    -  constructor: function FieldList() {
    -    Backbone.Collection.prototype.constructor.apply(this, arguments);
    -  },
    -  model: my.Field
    -});
    - -
  • - - -
  • -
    - -
    - -
    -

    Query

    - -
    - -
    my.Query = Backbone.Model.extend({
    -  constructor: function Query() {
    -    Backbone.Model.prototype.constructor.apply(this, arguments);
    -  },
    -  defaults: function() {
    -    return {
    -      size: 100,
    -      from: 0,
    -      q: '',
    -      facets: {},
    -      filters: []
    -    };
    -  },
    -  _filterTemplates: {
    -    term: {
    -      type: 'term',
    - -
  • - - -
  • -
    - -
    - -
    -

    TODO do we need this attribute here?

    - -
    - -
          field: '',
    -      term: ''
    -    },
    -    range: {
    -      type: 'range',
    -      from: '',
    -      to: ''
    -    },
    -    geo_distance: {
    -      type: 'geo_distance',
    -      distance: 10,
    -      unit: 'km',
    -      point: {
    -        lon: 0,
    -        lat: 0
    -      }
    -    }
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    addFilter(filter)

    -

    Add a new filter specified by the filter hash and append to the list of filters

    -

    @param filter an object specifying the filter - see _filterTemplates for examples. If only type is provided will generate a filter by cloning _filterTemplates

    - -
    - -
      addFilter: function(filter) {
    - -
  • - - -
  • -
    - -
    - -
    -

    crude deep copy

    - -
    - -
        var ourfilter = JSON.parse(JSON.stringify(filter));
    - -
  • - - -
  • -
    - -
    - -
    -

    not fully specified so use template and over-write

    - -
    - -
        if (_.keys(filter).length <= 3) {
    -      ourfilter = _.defaults(ourfilter, this._filterTemplates[filter.type]);
    -    }
    -    var filters = this.get('filters');
    -    filters.push(ourfilter);
    -    this.trigger('change:filters:new-blank');
    -  },
    -  replaceFilter: function(filter) {
    - -
  • - - -
  • -
    - -
    - -
    -

    delete filter on the same field, then add

    - -
    - -
        var filters = this.get('filters');
    -    var idx = -1;
    -    _.each(this.get('filters'), function(f, key, list) {
    -      if (filter.field == f.field) {
    -        idx = key;
    -      }
    -    });
    - -
  • - - -
  • -
    - -
    - -
    -

    trigger just one event (change:filters:new-blank) instead of one for remove and -one for add

    - -
    - -
        if (idx >= 0) {
    -      filters.splice(idx, 1);
    -      this.set({filters: filters});
    -    }
    -    this.addFilter(filter);
    -  },
    -  updateFilter: function(index, value) {
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    removeFilter

    -

    Remove a filter from filters at index filterIndex

    - -
    - -
      removeFilter: function(filterIndex) {
    -    var filters = this.get('filters');
    -    filters.splice(filterIndex, 1);
    -    this.set({filters: filters});
    -    this.trigger('change');
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    addFacet

    -

    Add a Facet to this query

    -

    See http://www.elasticsearch.org/guide/reference/api/search/facets/

    - -
    - -
      addFacet: function(fieldId, size, silent) {
    -    var facets = this.get('facets');
    - -
  • - - -
  • -
    - -
    - -
    -

    Assume id and fieldId should be the same (TODO: this need not be true if we want to add two different type of facets on same field)

    - -
    - -
        if (_.contains(_.keys(facets), fieldId)) {
    -      return;
    -    }
    -    facets[fieldId] = {
    -      terms: { field: fieldId }
    -    };
    -    if (!_.isUndefined(size)) {
    -      facets[fieldId].terms.size = size;
    -    }
    -    this.set({facets: facets}, {silent: true});
    -    if (!silent) {
    -      this.trigger('facet:add', this);
    -    }
    -  },
    -  addHistogramFacet: function(fieldId) {
    -    var facets = this.get('facets');
    -    facets[fieldId] = {
    -      date_histogram: {
    -        field: fieldId,
    -        interval: 'day'
    -      }
    -    };
    -    this.set({facets: facets}, {silent: true});
    -    this.trigger('facet:add', this);
    -  },
    -  removeFacet: function(fieldId) {
    -    var facets = this.get('facets');
    - -
  • - - -
  • -
    - -
    - -
    -

    Assume id and fieldId should be the same (TODO: this need not be true if we want to add two different type of facets on same field)

    - -
    - -
        if (!_.contains(_.keys(facets), fieldId)) {
    -      return;
    -    }
    -    delete facets[fieldId];
    -    this.set({facets: facets}, {silent: true});
    -    this.trigger('facet:remove', this);
    -  },
    -  clearFacets: function() {
    -    var facets = this.get('facets');
    -    _.each(_.keys(facets), function(fieldId) {
    -      delete facets[fieldId];
    -    });
    -    this.trigger('facet:remove', this);
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    trigger a facet add; use this to trigger a single event after adding -multiple facets

    - -
    - -
      refreshFacets: function() {
    -    this.trigger('facet:add', this);
    -  }
    -
    -});
    - -
  • - - -
  • -
    - -
    - -
    -

    A Facet (Result)

    - -
    - -
    my.Facet = Backbone.Model.extend({
    -  constructor: function Facet() {
    -    Backbone.Model.prototype.constructor.apply(this, arguments);
    -  },
    -  defaults: function() {
    -    return {
    -      _type: 'terms',
    -      total: 0,
    -      other: 0,
    -      missing: 0,
    -      terms: []
    -    };
    -  }
    -});
    - -
  • - - -
  • -
    - -
    - -
    -

    A Collection/List of Facets

    - -
    - -
    my.FacetList = Backbone.Collection.extend({
    -  constructor: function FacetList() {
    -    Backbone.Collection.prototype.constructor.apply(this, arguments);
    -  },
    -  model: my.Facet
    -});
    - -
  • - - -
  • -
    - -
    - -
    -

    Object State

    -

    Convenience Backbone model for storing (configuration) state of objects like Views.

    - -
    - -
    my.ObjectState = Backbone.Model.extend({
    -});
    - -
  • - - -
  • -
    - -
    - -
    -

    Backbone.sync

    -

    Override Backbone.sync to hand off to sync function in relevant backend -Backbone.sync = function(method, model, options) { - return model.backend.sync(method, model, options); -};

    - -
    - -
    -}(this.recline.Model));
    - -
  • - -
-
- - diff --git a/docs/src/public/fonts/aller-bold.eot b/docs/src/public/fonts/aller-bold.eot deleted file mode 100644 index 1b32532a..00000000 Binary files a/docs/src/public/fonts/aller-bold.eot and /dev/null differ diff --git a/docs/src/public/fonts/aller-bold.ttf b/docs/src/public/fonts/aller-bold.ttf deleted file mode 100644 index dc4cc9c2..00000000 Binary files a/docs/src/public/fonts/aller-bold.ttf and /dev/null differ diff --git a/docs/src/public/fonts/aller-bold.woff b/docs/src/public/fonts/aller-bold.woff deleted file mode 100644 index fa16fd0a..00000000 Binary files a/docs/src/public/fonts/aller-bold.woff and /dev/null differ diff --git a/docs/src/public/fonts/aller-light.eot b/docs/src/public/fonts/aller-light.eot deleted file mode 100644 index 40bd654b..00000000 Binary files a/docs/src/public/fonts/aller-light.eot and /dev/null differ diff --git a/docs/src/public/fonts/aller-light.ttf b/docs/src/public/fonts/aller-light.ttf deleted file mode 100644 index c2c72902..00000000 Binary files a/docs/src/public/fonts/aller-light.ttf and /dev/null differ diff --git a/docs/src/public/fonts/aller-light.woff b/docs/src/public/fonts/aller-light.woff deleted file mode 100644 index 81a09d18..00000000 Binary files a/docs/src/public/fonts/aller-light.woff and /dev/null differ diff --git a/docs/src/public/fonts/roboto-black.eot b/docs/src/public/fonts/roboto-black.eot deleted file mode 100755 index 571ed491..00000000 Binary files a/docs/src/public/fonts/roboto-black.eot and /dev/null differ diff --git a/docs/src/public/fonts/roboto-black.ttf b/docs/src/public/fonts/roboto-black.ttf deleted file mode 100755 index e0300b3e..00000000 Binary files a/docs/src/public/fonts/roboto-black.ttf and /dev/null differ diff --git a/docs/src/public/fonts/roboto-black.woff b/docs/src/public/fonts/roboto-black.woff deleted file mode 100755 index 642e5b60..00000000 Binary files a/docs/src/public/fonts/roboto-black.woff and /dev/null differ diff --git a/docs/src/public/stylesheets/normalize.css b/docs/src/public/stylesheets/normalize.css deleted file mode 100644 index 73abb76f..00000000 --- a/docs/src/public/stylesheets/normalize.css +++ /dev/null @@ -1,375 +0,0 @@ -/*! normalize.css v2.0.1 | MIT License | git.io/normalize */ - -/* ========================================================================== - HTML5 display definitions - ========================================================================== */ - -/* - * Corrects `block` display not defined in IE 8/9. - */ - -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -nav, -section, -summary { - display: block; -} - -/* - * Corrects `inline-block` display not defined in IE 8/9. - */ - -audio, -canvas, -video { - display: inline-block; -} - -/* - * Prevents modern browsers from displaying `audio` without controls. - * Remove excess height in iOS 5 devices. - */ - -audio:not([controls]) { - display: none; - height: 0; -} - -/* - * Addresses styling for `hidden` attribute not present in IE 8/9. - */ - -[hidden] { - display: none; -} - -/* ========================================================================== - Base - ========================================================================== */ - -/* - * 1. Sets default font family to sans-serif. - * 2. Prevents iOS text size adjust after orientation change, without disabling - * user zoom. - */ - -html { - font-family: sans-serif; /* 1 */ - -webkit-text-size-adjust: 100%; /* 2 */ - -ms-text-size-adjust: 100%; /* 2 */ -} - -/* - * Removes default margin. - */ - -body { - margin: 0; -} - -/* ========================================================================== - Links - ========================================================================== */ - -/* - * Addresses `outline` inconsistency between Chrome and other browsers. - */ - -a:focus { - outline: thin dotted; -} - -/* - * Improves readability when focused and also mouse hovered in all browsers. - */ - -a:active, -a:hover { - outline: 0; -} - -/* ========================================================================== - Typography - ========================================================================== */ - -/* - * Addresses `h1` font sizes within `section` and `article` in Firefox 4+, - * Safari 5, and Chrome. - */ - -h1 { - font-size: 2em; -} - -/* - * Addresses styling not present in IE 8/9, Safari 5, and Chrome. - */ - -abbr[title] { - border-bottom: 1px dotted; -} - -/* - * Addresses style set to `bolder` in Firefox 4+, Safari 5, and Chrome. - */ - -b, -strong { - font-weight: bold; -} - -/* - * Addresses styling not present in Safari 5 and Chrome. - */ - -dfn { - font-style: italic; -} - -/* - * Addresses styling not present in IE 8/9. - */ - -mark { - background: #ff0; - color: #000; -} - - -/* - * Corrects font family set oddly in Safari 5 and Chrome. - */ - -code, -kbd, -pre, -samp { - font-family: monospace, serif; - font-size: 1em; -} - -/* - * Improves readability of pre-formatted text in all browsers. - */ - -pre { - white-space: pre; - white-space: pre-wrap; - word-wrap: break-word; -} - -/* - * Sets consistent quote types. - */ - -q { - quotes: "\201C" "\201D" "\2018" "\2019"; -} - -/* - * Addresses inconsistent and variable font size in all browsers. - */ - -small { - font-size: 80%; -} - -/* - * Prevents `sub` and `sup` affecting `line-height` in all browsers. - */ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sup { - top: -0.5em; -} - -sub { - bottom: -0.25em; -} - -/* ========================================================================== - Embedded content - ========================================================================== */ - -/* - * Removes border when inside `a` element in IE 8/9. - */ - -img { - border: 0; -} - -/* - * Corrects overflow displayed oddly in IE 9. - */ - -svg:not(:root) { - overflow: hidden; -} - -/* ========================================================================== - Figures - ========================================================================== */ - -/* - * Addresses margin not present in IE 8/9 and Safari 5. - */ - -figure { - margin: 0; -} - -/* ========================================================================== - Forms - ========================================================================== */ - -/* - * Define consistent border, margin, and padding. - */ - -fieldset { - border: 1px solid #c0c0c0; - margin: 0 2px; - padding: 0.35em 0.625em 0.75em; -} - -/* - * 1. Corrects color not being inherited in IE 8/9. - * 2. Remove padding so people aren't caught out if they zero out fieldsets. - */ - -legend { - border: 0; /* 1 */ - padding: 0; /* 2 */ -} - -/* - * 1. Corrects font family not being inherited in all browsers. - * 2. Corrects font size not being inherited in all browsers. - * 3. Addresses margins set differently in Firefox 4+, Safari 5, and Chrome - */ - -button, -input, -select, -textarea { - font-family: inherit; /* 1 */ - font-size: 100%; /* 2 */ - margin: 0; /* 3 */ -} - -/* - * Addresses Firefox 4+ setting `line-height` on `input` using `!important` in - * the UA stylesheet. - */ - -button, -input { - line-height: normal; -} - -/* - * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` - * and `video` controls. - * 2. Corrects inability to style clickable `input` types in iOS. - * 3. Improves usability and consistency of cursor style between image-type - * `input` and others. - */ - -button, -html input[type="button"], /* 1 */ -input[type="reset"], -input[type="submit"] { - -webkit-appearance: button; /* 2 */ - cursor: pointer; /* 3 */ -} - -/* - * Re-set default cursor for disabled elements. - */ - -button[disabled], -input[disabled] { - cursor: default; -} - -/* - * 1. Addresses box sizing set to `content-box` in IE 8/9. - * 2. Removes excess padding in IE 8/9. - */ - -input[type="checkbox"], -input[type="radio"] { - box-sizing: border-box; /* 1 */ - padding: 0; /* 2 */ -} - -/* - * 1. Addresses `appearance` set to `searchfield` in Safari 5 and Chrome. - * 2. Addresses `box-sizing` set to `border-box` in Safari 5 and Chrome - * (include `-moz` to future-proof). - */ - -input[type="search"] { - -webkit-appearance: textfield; /* 1 */ - -moz-box-sizing: content-box; - -webkit-box-sizing: content-box; /* 2 */ - box-sizing: content-box; -} - -/* - * Removes inner padding and search cancel button in Safari 5 and Chrome - * on OS X. - */ - -input[type="search"]::-webkit-search-cancel-button, -input[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} - -/* - * Removes inner padding and border in Firefox 4+. - */ - -button::-moz-focus-inner, -input::-moz-focus-inner { - border: 0; - padding: 0; -} - -/* - * 1. Removes default vertical scrollbar in IE 8/9. - * 2. Improves readability and alignment in all browsers. - */ - -textarea { - overflow: auto; /* 1 */ - vertical-align: top; /* 2 */ -} - -/* ========================================================================== - Tables - ========================================================================== */ - -/* - * Remove most spacing between table cells. - */ - -table { - border-collapse: collapse; - border-spacing: 0; -} \ No newline at end of file diff --git a/docs/src/view.flot.html b/docs/src/view.flot.html deleted file mode 100644 index a8b69629..00000000 --- a/docs/src/view.flot.html +++ /dev/null @@ -1,865 +0,0 @@ - - - - - view.flot.js - - - - - -
-
- - - -
    - -
  • -
    -

    view.flot.js

    -
    -
  • - - - -
  • -
    - -
    - -
    - -
    - -
    /*jshint multistr:true */
    -
    -this.recline = this.recline || {};
    -this.recline.View = this.recline.View || {};
    -
    -(function($, my) {
    -  "use strict";
    - -
  • - - -
  • -
    - -
    - -
    -

    Graph view for a Dataset using Flot graphing library.

    -

    Initialization arguments (in a hash in first parameter):

    -
      -
    • model: recline.Model.Dataset
    • -
    • state: (optional) configuration hash of form:

      -
       {
      -   group: {column name for x-axis},
      -   series: [{column name for series A}, {column name series B}, ... ],
      -   // options are: lines, points, lines-and-points, bars, columns
      -   graphType: 'lines',
      -   graphOptions: {custom [flot options]}
      - }
      -
    • -
    -

    NB: should not provide an el argument to the view but must let the view -generate the element itself (you can then append view.el to the DOM.

    - -
    - -
    my.Flot = Backbone.View.extend({
    -  template: ' \
    -    <div class="recline-flot"> \
    -      <div class="panel graph" style="display: block;"> \
    -        <div class="js-temp-notice alert alert-warning alert-block"> \
    -          <h3 class="alert-heading">Hey there!</h3> \
    -          <p>There\'s no graph here yet because we don\'t know what fields you\'d like to see plotted.</p> \
    -          <p>Please tell us by <strong>using the menu on the right</strong> and a graph will automatically appear.</p> \
    -        </div> \
    -      </div> \
    -    </div> \
    -',
    -
    -  initialize: function(options) {
    -    var self = this;
    -    this.graphColors = ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"];
    -
    -    _.bindAll(this, 'render', 'redraw', '_toolTip', '_xaxisLabel');
    -    this.needToRedraw = false;
    -    this.listenTo(this.model, 'change', this.render);
    -    this.listenTo(this.model.fields, 'reset add', this.render);
    -    this.listenTo(this.model.records, 'reset add', this.redraw);
    -    var stateData = _.extend({
    -        group: null,
    - -
  • - - -
  • -
    - -
    - -
    -

    so that at least one series chooser box shows up

    - -
    - -
            series: [],
    -        graphType: 'lines-and-points'
    -      },
    -      options.state
    -    );
    -    this.state = new recline.Model.ObjectState(stateData);
    -    this.previousTooltipPoint = {x: null, y: null};
    -    this.editor = new my.FlotControls({
    -      model: this.model,
    -      state: this.state.toJSON()
    -    });
    -    this.listenTo(this.editor.state, 'change', function() {
    -      self.state.set(self.editor.state.toJSON());
    -      self.redraw();
    -    });
    -    this.elSidebar = this.editor.$el;
    -  },
    -
    -  render: function() {
    -    var self = this;
    -    var tmplData = this.model.toTemplateJSON();
    -    var htmls = Mustache.render(this.template, tmplData);
    -    this.$el.html(htmls);
    -    this.$graph = this.$el.find('.panel.graph');
    -    this.$graph.on("plothover", this._toolTip);
    -    return this;
    -  },
    -
    -  remove: function () {
    -    this.editor.remove();
    -    Backbone.View.prototype.remove.apply(this, arguments);
    -  },
    -
    -  redraw: function() {
    - -
  • - - -
  • -
    - -
    - -
    -

    There are issues generating a Flot graph if either:

    -
      -
    • The relevant div that graph attaches to his hidden at the moment of creating the plot — Flot will complain with -Uncaught Invalid dimensions for plot, width = 0, height = 0
    • -
    • There is no data for the plot — either same error or may have issues later with errors like ā€˜non-existent node-value’
    • -
    - -
    - -
        var areWeVisible = !jQuery.expr.filters.hidden(this.el);
    -    if ((!areWeVisible || this.model.records.length === 0)) {
    -      this.needToRedraw = true;
    -      return;
    -    }
    - -
  • - - -
  • -
    - -
    - -
    -

    check we have something to plot

    - -
    - -
        if (this.state.get('group') && this.state.get('series')) {
    -      var series = this.createSeries();
    -      var options = this.getGraphOptions(this.state.attributes.graphType, series[0].data.length);
    -      this.plot = $.plot(this.$graph, series, options);
    -    }
    -  },
    -
    -  show: function() {
    - -
  • - - -
  • -
    - -
    - -
    -

    because we cannot redraw when hidden we may need to when becoming visible

    - -
    - -
        if (this.needToRedraw) {
    -      this.redraw();
    -    }
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    infoboxes on mouse hover on points/bars etc

    - -
    - -
      _toolTip: function (event, pos, item) {
    -    if (item) {
    -      if (this.previousTooltipPoint.x !== item.dataIndex ||
    -          this.previousTooltipPoint.y !== item.seriesIndex) {
    -        this.previousTooltipPoint.x = item.dataIndex;
    -        this.previousTooltipPoint.y = item.seriesIndex;
    -        $("#recline-flot-tooltip").remove();
    -
    -        var x = item.datapoint[0].toFixed(2),
    -            y = item.datapoint[1].toFixed(2);
    -
    -        if (this.state.attributes.graphType === 'bars') {
    -          x = item.datapoint[1].toFixed(2),
    -          y = item.datapoint[0].toFixed(2);
    -        }
    -
    -        var content = _.template('<%= group %> = <%= x %>, <%= series %> = <%= y %>', {
    -          group: this.state.attributes.group,
    -          x: this._xaxisLabel(x),
    -          series: item.series.label,
    -          y: y
    -        });
    - -
  • - - -
  • -
    - -
    - -
    -

    use a different tooltip location offset for bar charts

    - -
    - -
            var xLocation, yLocation;
    -        if (this.state.attributes.graphType === 'bars') {
    -          xLocation = item.pageX + 15;
    -          yLocation = item.pageY - 10;
    -        } else if (this.state.attributes.graphType === 'columns') {
    -          xLocation = item.pageX + 15;
    -          yLocation = item.pageY;
    -        } else {
    -          xLocation = item.pageX + 10;
    -          yLocation = item.pageY - 20;
    -        }
    -
    -        $('<div id="recline-flot-tooltip">' + content + '</div>').css({
    -            top: yLocation,
    -            left: xLocation
    -        }).appendTo("body").fadeIn(200);
    -      }
    -    } else {
    -      $("#recline-flot-tooltip").remove();
    -      this.previousTooltipPoint.x = null;
    -      this.previousTooltipPoint.y = null;
    -    }
    -  },
    -
    -  _xaxisLabel: function (x) {
    -    if (this._groupFieldIsDateTime()) {
    - -
  • - - -
  • -
    - -
    - -
    -

    oddly x comes through as milliseconds string (rather than int -or float) so we have to reparse

    - -
    - -
          x = new Date(parseFloat(x)).toLocaleDateString();
    -    } else if (this.xvaluesAreIndex) {
    -      x = parseInt(x, 10);
    - -
  • - - -
  • -
    - -
    - -
    -

    HACK: deal with bar graph style cases where x-axis items were strings -In this case x at this point is the index of the item in the list of -records not its actual x-axis value

    - -
    - -
          x = this.model.records.models[x].get(this.state.attributes.group);
    -    }
    -
    -    return x;
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    getGraphOptions

    -

    Get options for Flot Graph

    -

    needs to be function as can depend on state

    -

    @param typeId graphType id (lines, lines-and-points etc) -@param numPoints the number of points that will be plotted

    - -
    - -
      getGraphOptions: function(typeId, numPoints) {
    -    var self = this;
    -    var groupFieldIsDateTime = self._groupFieldIsDateTime();
    -    var xaxis = {};
    -
    -    if (!groupFieldIsDateTime) {
    -      xaxis.tickFormatter = function (x) {
    - -
  • - - -
  • -
    - -
    - -
    -

    convert x to a string and make sure that it is not too long or the -tick labels will overlap -TODO: find a more accurate way of calculating the size of tick labels

    - -
    - -
            var label = self._xaxisLabel(x) || "";
    -
    -        if (typeof label !== 'string') {
    -          label = label.toString();
    -        }
    -        if (self.state.attributes.graphType !== 'bars' && label.length > 10) {
    -          label = label.slice(0, 10) + "...";
    -        }
    -
    -        return label;
    -      };
    -    }
    - -
  • - - -
  • -
    - -
    - -
    -

    for labels case we only want ticks at the label intervals -HACK: however we also get this case with Date fields. In that case we -could have a lot of values and so we limit to max 15 (we assume)

    - -
    - -
        if (this.xvaluesAreIndex) {
    -      var numTicks = Math.min(this.model.records.length, 15);
    -      var increment = this.model.records.length / numTicks;
    -      var ticks = [];
    -      for (var i=0; i<numTicks; i++) {
    -        ticks.push(parseInt(i*increment, 10));
    -      }
    -      xaxis.ticks = ticks;
    -    } else if (groupFieldIsDateTime) {
    -      xaxis.mode = 'time';
    -    }
    -
    -    var yaxis = {};
    -    yaxis.autoscale = true;
    -    yaxis.autoscaleMargin = 0.02;
    -
    -    var legend = {};
    -    legend.position = 'ne';
    -
    -    var grid = {};
    -    grid.hoverable = true;
    -    grid.clickable = true;
    -    grid.borderColor = "#aaaaaa";
    -    grid.borderWidth = 1;
    -
    -    var optionsPerGraphType = {
    -      lines: {
    -        legend: legend,
    -        colors: this.graphColors,
    -        lines: { show: true },
    -        xaxis: xaxis,
    -        yaxis: yaxis,
    -        grid: grid
    -      },
    -      points: {
    -        legend: legend,
    -        colors: this.graphColors,
    -        points: { show: true, hitRadius: 5 },
    -        xaxis: xaxis,
    -        yaxis: yaxis,
    -        grid: grid
    -      },
    -      'lines-and-points': {
    -        legend: legend,
    -        colors: this.graphColors,
    -        points: { show: true, hitRadius: 5 },
    -        lines: { show: true },
    -        xaxis: xaxis,
    -        yaxis: yaxis,
    -        grid: grid
    -      },
    -      bars: {
    -        legend: legend,
    -        colors: this.graphColors,
    -        lines: { show: false },
    -        xaxis: yaxis,
    -        yaxis: xaxis,
    -        grid: grid,
    -        bars: {
    -          show: true,
    -          horizontal: true,
    -          shadowSize: 0,
    -          align: 'center',
    -          barWidth: 0.8
    -        }
    -      },
    -      columns: {
    -        legend: legend,
    -        colors: this.graphColors,
    -        lines: { show: false },
    -        xaxis: xaxis,
    -        yaxis: yaxis,
    -        grid: grid,
    -        bars: {
    -          show: true,
    -          horizontal: false,
    -          shadowSize: 0,
    -          align: 'center',
    -          barWidth: 0.8
    -        }
    -      }
    -    };
    -
    -    if (self.state.get('graphOptions')) {
    -      return _.extend(optionsPerGraphType[typeId],
    -                      self.state.get('graphOptions'));
    -    } else {
    -      return optionsPerGraphType[typeId];
    -    }
    -  },
    -
    -  _groupFieldIsDateTime: function() {
    -    var xfield = this.model.fields.get(this.state.attributes.group);
    -    var xtype = xfield.get('type');
    -    var isDateTime = (xtype === 'date' || xtype === 'date-time' || xtype  === 'time');
    -    return isDateTime;
    -  },
    -
    -  createSeries: function() {
    -    var self = this;
    -    self.xvaluesAreIndex = false;
    -    var series = [];
    -    var xfield = self.model.fields.get(self.state.attributes.group);
    -    var isDateTime = self._groupFieldIsDateTime();
    -
    -    _.each(this.state.attributes.series, function(field) {
    -      var points = [];
    -      var fieldLabel = self.model.fields.get(field).get('label');
    -
    -        if (isDateTime){
    -            var cast = function(x){
    -                var _date = moment(String(x));
    -                if (_date.isValid()) {
    -                    x = _date.toDate().getTime();
    -                }
    -                return x
    -            }
    -        } else {
    -            var raw = _.map(self.model.records.models,
    -                            function(doc, index){
    -                                return doc.getFieldValueUnrendered(xfield)
    -                            });
    -
    -            if (_.all(raw, function(x){ return !isNaN(parseFloat(x)) })){
    -                var cast = function(x){ return parseFloat(x) }
    -            } else {
    -                self.xvaluesAreIndex = true
    -            }
    -        }
    -
    -      _.each(self.model.records.models, function(doc, index) {
    -        if(self.xvaluesAreIndex){
    -            var x = index;
    -        }else{
    -            var x = cast(doc.getFieldValueUnrendered(xfield));
    -        }
    -
    -        var yfield = self.model.fields.get(field);
    -        var y = parseFloat(doc.getFieldValueUnrendered(yfield));
    -
    -        if (self.state.attributes.graphType == 'bars') {
    -          points.push([y, x]);
    -        } else {
    -          points.push([x, y]);
    -        }
    -      });
    -      series.push({
    -        data: points,
    -        label: fieldLabel,
    -        hoverable: true
    -      });
    -    });
    -    return series;
    -  }
    -});
    -
    -my.FlotControls = Backbone.View.extend({
    -  className: "editor",
    -  template: ' \
    -  <div class="editor"> \
    -    <form class="form-stacked"> \
    -      <div class="clearfix"> \
    -        <div class="form-group"> \
    -          <label>Graph Type</label> \
    -          <div class="input editor-type"> \
    -            <select class="form-control"> \
    -              <option value="lines-and-points">Lines and Points</option> \
    -              <option value="lines">Lines</option> \
    -              <option value="points">Points</option> \
    -              <option value="bars">Bars</option> \
    -              <option value="columns">Columns</option> \
    -            </select> \
    -          </div> \
    -        </div> \
    -        <div class="form-group"> \
    -          <label>Group Column (Axis 1)</label> \
    -          <div class="input editor-group"> \
    -            <select class="form-control"> \
    -              <option value="">Please choose ...</option> \
    -                {{#fields}} \
    -              <option value="{{id}}">{{label}}</option> \
    -                {{/fields}} \
    -            </select> \
    -          </div> \
    -        </div> \
    -        <div class="editor-series-group"> \
    -        </div> \
    -      </div> \
    -      <div class="editor-buttons"> \
    -        <button class="btn btn-default editor-add">Add Series</button> \
    -      </div> \
    -      <div class="editor-buttons editor-submit" comment="hidden temporarily" style="display: none;"> \
    -        <button class="editor-save">Save</button> \
    -        <input type="hidden" class="editor-id" value="chart-1" /> \
    -      </div> \
    -    </form> \
    -  </div> \
    -',
    -  templateSeriesEditor: ' \
    -    <div class="editor-series js-series-{{seriesIndex}}"> \
    -      <div class="form-group"> \
    -        <label>Series <span>{{seriesName}} (Axis 2)</span> \
    -          [<a href="#remove" class="action-remove-series">Remove</a>] \
    -        </label> \
    -        <div class="input"> \
    -          <select class="form-control"> \
    -          {{#fields}} \
    -          <option value="{{id}}">{{label}}</option> \
    -          {{/fields}} \
    -          </select> \
    -        </div> \
    -      </div> \
    -    </div> \
    -  ',
    -  events: {
    -    'change form select': 'onEditorSubmit',
    -    'click .editor-add': '_onAddSeries',
    -    'click .action-remove-series': 'removeSeries'
    -  },
    -
    -  initialize: function(options) {
    -    var self = this;
    -    _.bindAll(this, 'render');
    -    this.listenTo(this.model.fields, 'reset add', this.render);
    -    this.state = new recline.Model.ObjectState(options.state);
    -    this.render();
    -  },
    -
    -  render: function() {
    -    var self = this;
    -    var tmplData = this.model.toTemplateJSON();
    -    var htmls = Mustache.render(this.template, tmplData);
    -    this.$el.html(htmls);
    - -
  • - - -
  • -
    - -
    - -
    -

    set up editor from state

    - -
    - -
        if (this.state.get('graphType')) {
    -      this._selectOption('.editor-type', this.state.get('graphType'));
    -    }
    -    if (this.state.get('group')) {
    -      this._selectOption('.editor-group', this.state.get('group'));
    -    }
    - -
  • - - -
  • -
    - -
    - -
    -

    ensure at least one series box shows up

    - -
    - -
        var tmpSeries = [""];
    -    if (this.state.get('series').length > 0) {
    -      tmpSeries = this.state.get('series');
    -    }
    -    _.each(tmpSeries, function(series, idx) {
    -      self.addSeries(idx);
    -      self._selectOption('.editor-series.js-series-' + idx, series);
    -    });
    -    return this;
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    Private: Helper function to select an option from a select list

    - -
    - -
      _selectOption: function(id,value){
    -    var options = this.$el.find(id + ' select > option');
    -    if (options) {
    -      options.each(function(opt){
    -        if (this.value == value) {
    -          $(this).attr('selected','selected');
    -          return false;
    -        }
    -      });
    -    }
    -  },
    -
    -  onEditorSubmit: function(e) {
    -    var select = this.$el.find('.editor-group select');
    -    var $editor = this;
    -    var $series = this.$el.find('.editor-series select');
    -    var series = $series.map(function () {
    -      return $(this).val();
    -    });
    -    var updatedState = {
    -      series: $.makeArray(series),
    -      group: this.$el.find('.editor-group select').val(),
    -      graphType: this.$el.find('.editor-type select').val()
    -    };
    -    this.state.set(updatedState);
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    Public: Adds a new empty series select box to the editor.

    -

    @param [int] idx index of this series in the list of series

    -

    Returns itself.

    - -
    - -
      addSeries: function (idx) {
    -    var data = _.extend({
    -      seriesIndex: idx,
    -      seriesName: String.fromCharCode(idx + 64 + 1)
    -    }, this.model.toTemplateJSON());
    -
    -    var htmls = Mustache.render(this.templateSeriesEditor, data);
    -    this.$el.find('.editor-series-group').append(htmls);
    -    return this;
    -  },
    -
    -  _onAddSeries: function(e) {
    -    e.preventDefault();
    -    this.addSeries(this.state.get('series').length);
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    Public: Removes a series list item from the editor.

    -

    Also updates the labels of the remaining series elements.

    - -
    - -
      removeSeries: function (e) {
    -    e.preventDefault();
    -    var $el = $(e.target);
    -    $el.parent().parent().remove();
    -    this.onEditorSubmit();
    -  }
    -});
    -
    -})(jQuery, recline.View);
    - -
  • - -
-
- - diff --git a/docs/src/view.graph.html b/docs/src/view.graph.html deleted file mode 100644 index a6cbb03d..00000000 --- a/docs/src/view.graph.html +++ /dev/null @@ -1,141 +0,0 @@ - - - - - view.graph.js - - - - - -
-
- - - -
    - -
  • -
    -

    view.graph.js

    -
    -
  • - - - -
  • -
    - -
    - -
    - -
    - -
    this.recline = this.recline || {};
    -this.recline.View = this.recline.View || {};
    -this.recline.View.Graph = this.recline.View.Flot;
    -this.recline.View.GraphControls = this.recline.View.FlotControls;
    - -
  • - -
-
- - diff --git a/docs/src/view.grid.html b/docs/src/view.grid.html deleted file mode 100644 index 09a91eca..00000000 --- a/docs/src/view.grid.html +++ /dev/null @@ -1,606 +0,0 @@ - - - - - view.grid.js - - - - - -
-
- - - -
    - -
  • -
    -

    view.grid.js

    -
    -
  • - - - -
  • -
    - -
    - -
    - -
    - -
    /*jshint multistr:true */
    -
    -this.recline = this.recline || {};
    -this.recline.View = this.recline.View || {};
    -
    -(function($, my) {
    -  "use strict";
    - -
  • - - -
  • -
    - -
    - -
    -

    (Data) Grid Dataset View

    -

    Provides a tabular view on a Dataset.

    -

    Initialize it with a recline.Model.Dataset.

    - -
    - -
    my.Grid = Backbone.View.extend({
    -  tagName:  "div",
    -  className: "recline-grid-container",
    -
    -  initialize: function(modelEtc) {
    -    var self = this;
    -    _.bindAll(this, 'render', 'onHorizontalScroll');
    -    this.listenTo(this.model.records, 'add reset remove', this.render);
    -    this.tempState = {};
    -    var state = _.extend({
    -        hiddenFields: []
    -      }, modelEtc.state
    -    ); 
    -    this.state = new recline.Model.ObjectState(state);
    -  },
    -
    -  events: {
    - -
  • - - -
  • -
    - -
    - -
    -

    does not work here so done at end of render function -ā€˜scroll .recline-grid tbody’: ā€˜onHorizontalScroll’

    - -
    - -
      },
    - -
  • - - -
  • -
    - -
    - -
    -

    ======================================================

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    Column and row menus

    - -
    - -
    -  setColumnSort: function(order) {
    -    var sort = [{}];
    -    sort[0][this.tempState.currentColumn] = {order: order};
    -    this.model.query({sort: sort});
    -  },
    -  
    -  hideColumn: function() {
    -    var hiddenFields = this.state.get('hiddenFields');
    -    hiddenFields.push(this.tempState.currentColumn);
    -    this.state.set({hiddenFields: hiddenFields});
    - -
  • - - -
  • -
    - -
    - -
    -

    change event not being triggered (because it is an array?) so trigger manually

    - -
    - -
        this.state.trigger('change');
    -    this.render();
    -  },
    -  
    -  showColumn: function(e) {
    -    var hiddenFields = _.without(this.state.get('hiddenFields'), $(e.target).data('column'));
    -    this.state.set({hiddenFields: hiddenFields});
    -    this.render();
    -  },
    -
    -  onHorizontalScroll: function(e) {
    -    var currentScroll = $(e.target).scrollLeft();
    -    this.$el.find('.recline-grid thead tr').scrollLeft(currentScroll);
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    ======================================================

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    Templating

    - -
    - -
      template: ' \
    -    <div class="table-container"> \
    -    <table class="recline-grid table-striped table-condensed" cellspacing="0"> \
    -      <thead class="fixed-header"> \
    -        <tr> \
    -          {{#fields}} \
    -            <th class="column-header {{#hidden}}hidden{{/hidden}}" data-field="{{id}}" style="width: {{width}}px; max-width: {{width}}px; min-width: {{width}}px;" title="{{label}}"> \
    -              <span class="column-header-name">{{label}}</span> \
    -            </th> \
    -          {{/fields}} \
    -          <th class="last-header" style="width: {{lastHeaderWidth}}px; max-width: {{lastHeaderWidth}}px; min-width: {{lastHeaderWidth}}px; padding: 0; margin: 0;"></th> \
    -        </tr> \
    -      </thead> \
    -      <tbody class="scroll-content"></tbody> \
    -    </table> \
    -    </div> \
    -  ',
    -
    -  toTemplateJSON: function() {
    -    var self = this; 
    -    var modelData = this.model.toJSON();
    -    modelData.notEmpty = ( this.fields.length > 0 );
    - -
  • - - -
  • -
    - -
    - -
    -

    TODO: move this sort of thing into a toTemplateJSON method on Dataset?

    - -
    - -
        modelData.fields = this.fields.map(function(field) {
    -      return field.toJSON();
    -    });
    - -
  • - - -
  • -
    - -
    - -
    -

    last header width = scroll bar - border (2px) */

    - -
    - -
        modelData.lastHeaderWidth = this.scrollbarDimensions.width - 2;
    -    return modelData;
    -  },
    -  render: function() {
    -    var self = this;
    -    this.fields = new recline.Model.FieldList(this.model.fields.filter(function(field) {
    -      return _.indexOf(self.state.get('hiddenFields'), field.id) == -1;
    -    }));
    -
    -    this.scrollbarDimensions = this.scrollbarDimensions || this._scrollbarSize(); // skip measurement if already have dimensions
    -    var numFields = this.fields.length;
    - -
  • - - -
  • -
    - -
    - -
    -

    compute field widths (-20 for first menu col + 10px for padding on each col and finally 16px for the scrollbar)

    - -
    - -
        var fullWidth = self.$el.width() - 20 - 10 * numFields - this.scrollbarDimensions.width;
    -    var width = parseInt(Math.max(50, fullWidth / numFields), 10);
    - -
  • - - -
  • -
    - -
    - -
    -

    if columns extend outside viewport then remainder is 0

    - -
    - -
        var remainder = Math.max(fullWidth - numFields * width,0);
    -    this.fields.each(function(field, idx) {
    - -
  • - - -
  • -
    - -
    - -
    -

    add the remainder to the first field width so we make up full col

    - -
    - -
          if (idx === 0) {
    -        field.set({width: width+remainder});
    -      } else {
    -        field.set({width: width});
    -      }
    -    });
    -    var htmls = Mustache.render(this.template, this.toTemplateJSON());
    -    this.$el.html(htmls);
    -    this.model.records.forEach(function(doc) {
    -      var tr = $('<tr />');
    -      self.$el.find('tbody').append(tr);
    -      var newView = new my.GridRow({
    -          model: doc,
    -          el: tr,
    -          fields: self.fields
    -        });
    -      newView.render();
    -    });
    - -
  • - - -
  • -
    - -
    - -
    -

    hide extra header col if no scrollbar to avoid unsightly overhang

    - -
    - -
        var $tbody = this.$el.find('tbody')[0];
    -    if ($tbody.scrollHeight <= $tbody.offsetHeight) {
    -      this.$el.find('th.last-header').hide();
    -    }
    -    this.$el.find('.recline-grid').toggleClass('no-hidden', (self.state.get('hiddenFields').length === 0));
    -    this.$el.find('.recline-grid tbody').scroll(this.onHorizontalScroll);
    -    return this;
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    _scrollbarSize

    -

    Measure width of a vertical scrollbar and height of a horizontal scrollbar.

    -

    @return: { width: pixelWidth, height: pixelHeight }

    - -
    - -
      _scrollbarSize: function() {
    -    var $c = $("<div style='position:absolute; top:-10000px; left:-10000px; width:100px; height:100px; overflow:scroll;'></div>").appendTo("body");
    -    var dim = { width: $c.width() - $c[0].clientWidth + 1, height: $c.height() - $c[0].clientHeight };
    -    $c.remove();
    -    return dim;
    -  }
    -});
    - -
  • - - -
  • -
    - -
    - -
    -

    GridRow View for rendering an individual record.

    -

    Since we want this to update in place it is up to creator to provider the element to attach to.

    -

    In addition you must pass in a FieldList in the constructor options. This should be list of fields for the Grid.

    -

    Example:

    -
    -var row = new GridRow({
    -  model: dataset-record,
    -    el: dom-element,
    -    fields: mydatasets.fields // a FieldList object
    -  });
    -
    - -
    - -
    my.GridRow = Backbone.View.extend({
    -  initialize: function(initData) {
    -    _.bindAll(this, 'render');
    -    this._fields = initData.fields;
    -    this.listenTo(this.model, 'change', this.render);
    -  },
    -
    -  template: ' \
    -      {{#cells}} \
    -      <td data-field="{{field}}" style="width: {{width}}px; max-width: {{width}}px; min-width: {{width}}px;"> \
    -        <div class="data-table-cell-content"> \
    -          <a href="javascript:{}" class="data-table-cell-edit" title="Edit this cell">&nbsp;</a> \
    -          <div class="data-table-cell-value">{{{value}}}</div> \
    -        </div> \
    -      </td> \
    -      {{/cells}} \
    -    ',
    -  events: {
    -    'click .data-table-cell-edit': 'onEditClick',
    -    'click .data-table-cell-editor .okButton': 'onEditorOK',
    -    'click .data-table-cell-editor .cancelButton': 'onEditorCancel'
    -  },
    -  
    -  toTemplateJSON: function() {
    -    var self = this;
    -    var doc = this.model;
    -    var cellData = this._fields.map(function(field) {
    -      return {
    -        field: field.id,
    -        width: field.get('width'),
    -        value: doc.getFieldValue(field)
    -      };
    -    });
    -    return { id: this.id, cells: cellData };
    -  },
    -
    -  render: function() {
    -    this.$el.attr('data-id', this.model.id);
    -    var html = Mustache.render(this.template, this.toTemplateJSON());
    -    this.$el.html(html);
    -    return this;
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    ===================

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    Cell Editor methods

    - -
    - -
    -  cellEditorTemplate: ' \
    -    <div class="menu-container data-table-cell-editor"> \
    -      <textarea class="data-table-cell-editor-editor" bind="textarea">{{value}}</textarea> \
    -      <div id="data-table-cell-editor-actions"> \
    -        <div class="data-table-cell-editor-action"> \
    -          <button class="okButton btn primary">Update</button> \
    -          <button class="cancelButton btn danger">Cancel</button> \
    -        </div> \
    -      </div> \
    -    </div> \
    -  ',
    -
    -  onEditClick: function(e) {
    -    var editing = this.$el.find('.data-table-cell-editor-editor');
    -    if (editing.length > 0) {
    -      editing.parents('.data-table-cell-value').html(editing.text()).siblings('.data-table-cell-edit').removeClass("hidden");
    -    }
    -    $(e.target).addClass("hidden");
    -    var cell = $(e.target).siblings('.data-table-cell-value');
    -    cell.data("previousContents", cell.text());
    -    var templated = Mustache.render(this.cellEditorTemplate, {value: cell.text()});
    -    cell.html(templated);
    -  },
    -
    -  onEditorOK: function(e) {
    -    var self = this;
    -    var cell = $(e.target);
    -    var rowId = cell.parents('tr').attr('data-id');
    -    var field = cell.parents('td').attr('data-field');
    -    var newValue = cell.parents('.data-table-cell-editor').find('.data-table-cell-editor-editor').val();
    -    var newData = {};
    -    newData[field] = newValue;
    -    this.model.set(newData);
    -    this.trigger('recline:flash', {message: "Updating row...", loader: true});
    -    this.model.save().then(function(response) {
    -        this.trigger('recline:flash', {message: "Row updated successfully", category: 'success'});
    -      })
    -      .fail(function() {
    -        this.trigger('recline:flash', {
    -          message: 'Error saving row',
    -          category: 'error',
    -          persist: true
    -        });
    -      });
    -  },
    -
    -  onEditorCancel: function(e) {
    -    var cell = $(e.target).parents('.data-table-cell-value');
    -    cell.html(cell.data('previousContents')).siblings('.data-table-cell-edit').removeClass("hidden");
    -  }
    -});
    -
    -})(jQuery, recline.View);
    - -
  • - -
-
- - diff --git a/docs/src/view.map.html b/docs/src/view.map.html deleted file mode 100644 index 3bd2ebdd..00000000 --- a/docs/src/view.map.html +++ /dev/null @@ -1,1331 +0,0 @@ - - - - - view.map.js - - - - - -
-
- - - -
    - -
  • -
    -

    view.map.js

    -
    -
  • - - - -
  • -
    - -
    - -
    - -
    - -
    /*jshint multistr:true */
    -
    -this.recline = this.recline || {};
    -this.recline.View = this.recline.View || {};
    -
    -(function($, my) {
    -  "use strict";
    - -
  • - - -
  • -
    - -
    - -
    -

    Map view for a Dataset using Leaflet mapping library.

    -

    This view allows to plot gereferenced records on a map. The location -information can be provided in 2 ways:

    -
      -
    1. Via a single field. This field must be either a geo_point or -GeoJSON object
    2. -
    3. Via two fields with latitude and longitude coordinates.
    4. -
    -

    Which fields in the data these correspond to can be configured via the state -(and are guessed if no info is provided).

    -

    Initialization arguments are as standard for Dataset Views. State object may -have the following (optional) configuration options:

    -
    -  {
    -    // geomField if specified will be used in preference to lat/lon
    -    geomField: {id of field containing geometry in the dataset}
    -    lonField: {id of field containing longitude in the dataset}
    -    latField: {id of field containing latitude in the dataset}
    -    autoZoom: true,
    -    // use cluster support
    -    // cluster: true = always on
    -    // cluster: false = always off
    -    cluster: false
    -  }
    -
    - -

    Useful attributes to know about (if e.g. customizing)

    -
      -
    • map: the Leaflet map (L.Map)
    • -
    • features: Leaflet GeoJSON layer containing all the features (L.GeoJSON)
    • -
    - -
    - -
    my.Map = Backbone.View.extend({
    -  template: ' \
    -    <div class="recline-map"> \
    -      <div class="panel map"></div> \
    -    </div> \
    -',
    - -
  • - - -
  • -
    - -
    - -
    -

    These are the default (case-insensitive) names of field that are used if found. -If not found, the user will need to define the fields via the editor.

    - -
    - -
      latitudeFieldNames: ['lat','latitude'],
    -  longitudeFieldNames: ['lon','longitude'],
    -  geometryFieldNames: ['geojson', 'geom','the_geom','geometry','spatial','location', 'geo', 'lonlat'],
    -
    -  initialize: function(options) {
    -    var self = this;
    -    this.visible = this.$el.is(':visible');
    -    this.mapReady = false;
    - -
  • - - -
  • -
    - -
    - -
    -

    this will be the Leaflet L.Map object (setup below)

    - -
    - -
        this.map = null;
    -
    -    var stateData = _.extend({
    -        geomField: null,
    -        lonField: null,
    -        latField: null,
    -        autoZoom: true,
    -        cluster: false
    -      },
    -      options.state
    -    );
    -    this.state = new recline.Model.ObjectState(stateData);
    -
    -    this._clusterOptions = {
    -      zoomToBoundsOnClick: true,
    - -
  • - - -
  • -
    - -
    - -
    -

    disableClusteringAtZoom: 10,

    - -
    - -
          maxClusterRadius: 80,
    -      singleMarkerMode: false,
    -      skipDuplicateAddTesting: true,
    -      animateAddingMarkers: false
    -    };
    - -
  • - - -
  • -
    - -
    - -
    -

    Listen to changes in the fields

    - -
    - -
        this.listenTo(this.model.fields, 'change', function() {
    -      self._setupGeometryField();
    -      self.render();
    -    });
    - -
  • - - -
  • -
    - -
    - -
    -

    Listen to changes in the records

    - -
    - -
        this.listenTo(this.model.records, 'add', function(doc){self.redraw('add',doc);});
    -    this.listenTo(this.model.records, 'change', function(doc){
    -        self.redraw('remove',doc);
    -        self.redraw('add',doc);
    -    });
    -    this.listenTo(this.model.records, 'remove', function(doc){self.redraw('remove',doc);});
    -    this.listenTo(this.model.records, 'reset', function(){self.redraw('reset');});
    -
    -    this.menu = new my.MapMenu({
    -      model: this.model,
    -      state: this.state.toJSON()
    -    });
    -    this.listenTo(this.menu.state, 'change', function() {
    -      self.state.set(self.menu.state.toJSON());
    -      self.redraw();
    -    });
    -    this.listenTo(this.state, 'change', function() {
    -      self.redraw();
    -    });
    -    this.elSidebar = this.menu.$el;
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    Customization Functions

    -

    The following methods are designed for overriding in order to customize -behaviour

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    infobox

    -

    Function to create infoboxes used in popups. The default behaviour is very simple and just lists all attributes.

    -

    Users should override this function to customize behaviour i.e.

    -
    view = new View({...});
    -view.infobox = function(record) {
    -  ...
    -}
    -
    -
    - -
      infobox: function(record) {
    -    var html = '';
    -    for (var key in record.attributes){
    -      if (!(this.state.get('geomField') && key == this.state.get('geomField'))){
    -        html += '<div><strong>' + key + '</strong>: '+ record.attributes[key] + '</div>';
    -      }
    -    }
    -    return html;
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    Options to use for the Leaflet GeoJSON layer -See also http://leaflet.cloudmade.com/examples/geojson.html

    -

    e.g.

    -
    pointToLayer: function(feature, latLng)
    -onEachFeature: function(feature, layer)
    -

    See defaults for examples

    - -
    - -
      geoJsonLayerOptions: {
    - -
  • - - -
  • -
    - -
    - -
    -

    pointToLayer function to use when creating points

    -

    Default behaviour shown here is to create a marker using the -popupContent set on the feature properties (created via infobox function -during feature generation)

    -

    NB: inside pointToLayer this will be set to point to this map view -instance (which allows e.g. this.markers to work in this default case)

    - -
    - -
        pointToLayer: function (feature, latlng) {
    -      var marker = new L.Marker(latlng);
    -      marker.bindPopup(feature.properties.popupContent);
    - -
  • - - -
  • -
    - -
    - -
    -

    this is for cluster case

    - -
    - -
          this.markers.addLayer(marker);
    -      return marker;
    -    },
    - -
  • - - -
  • -
    - -
    - -
    -

    onEachFeature default which adds popup in

    - -
    - -
        onEachFeature: function(feature, layer) {
    -      if (feature.properties && feature.properties.popupContent) {
    -        layer.bindPopup(feature.properties.popupContent);
    -      }
    -    }
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    END: Customization section

    - -
    - -
  • - - -
  • -
    - -
    - -
    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    Public: Adds the necessary elements to the page.

    -

    Also sets up the editor fields and the map if necessary.

    - -
    - -
      render: function() {
    -    var self = this;
    -    var htmls = Mustache.render(this.template, this.model.toTemplateJSON());
    -    this.$el.html(htmls);
    -    this.$map = this.$el.find('.panel.map');
    -    this.redraw();
    -    return this;
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    Public: Redraws the features on the map according to the action provided

    -

    Actions can be:

    -
      -
    • reset: Clear all features
    • -
    • add: Add one or n features (records)
    • -
    • remove: Remove one or n features (records)
    • -
    • refresh: Clear existing features and add all current records
    • -
    - -
    - -
      redraw: function(action, doc){
    -    var self = this;
    -    action = action || 'refresh';
    - -
  • - - -
  • -
    - -
    - -
    -

    try to set things up if not already

    - -
    - -
        if (!self._geomReady()){
    -      self._setupGeometryField();
    -    }
    -    if (!self.mapReady){
    -      self._setupMap();
    -    }
    -
    -    if (this._geomReady() && this.mapReady){
    - -
  • - - -
  • -
    - -
    - -
    -

    removing ad re-adding the layer enables faster bulk loading

    - -
    - -
          this.map.removeLayer(this.features);
    -      this.map.removeLayer(this.markers);
    -
    -      var countBefore = 0;
    -      this.features.eachLayer(function(){countBefore++;});
    -
    -      if (action == 'refresh' || action == 'reset') {
    -        this.features.clearLayers();
    - -
  • - - -
  • -
    - -
    - -
    -

    recreate cluster group because of issues with clearLayer

    - -
    - -
            this.map.removeLayer(this.markers);
    -        this.markers = new L.MarkerClusterGroup(this._clusterOptions);
    -        this._add(this.model.records.models);
    -      } else if (action == 'add' && doc){
    -        this._add(doc);
    -      } else if (action == 'remove' && doc){
    -        this._remove(doc);
    -      }
    - -
  • - - -
  • -
    - -
    - -
    -

    this must come before zooming! -if not: errors when using e.g. circle markers like -ā€œCannot call method ā€˜project’ of undefinedā€

    - -
    - -
          if (this.state.get('cluster')) {
    -        this.map.addLayer(this.markers);
    -      } else {
    -        this.map.addLayer(this.features);
    -      }
    -
    -      if (this.state.get('autoZoom')){
    -        if (this.visible){
    -          this._zoomToFeatures();
    -        } else {
    -          this._zoomPending = true;
    -        }
    -      }
    -    }
    -  },
    -
    -  show: function() {
    - -
  • - - -
  • -
    - -
    - -
    -

    If the div was hidden, Leaflet needs to recalculate some sizes -to display properly

    - -
    - -
        if (this.map){
    -      this.map.invalidateSize();
    -      if (this._zoomPending && this.state.get('autoZoom')) {
    -        this._zoomToFeatures();
    -        this._zoomPending = false;
    -      }
    -    }
    -    this.visible = true;
    -  },
    -
    -  hide: function() {
    -    this.visible = false;
    -  },
    -
    -  _geomReady: function() {
    -    return Boolean(this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField')));
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    Private: Add one or n features to the map

    -

    For each record passed, a GeoJSON geometry will be extracted and added -to the features layer. If an exception is thrown, the process will be -stopped and an error notification shown.

    -

    Each feature will have a popup associated with all the record fields.

    - -
    - -
      _add: function(docs){
    -    var self = this;
    -
    -    if (!(docs instanceof Array)) docs = [docs];
    -
    -    var count = 0;
    -    var wrongSoFar = 0;
    -    _.every(docs, function(doc){
    -      count += 1;
    -      var feature = self._getGeometryFromRecord(doc);
    -      if (typeof feature === 'undefined' || feature === null){
    - -
  • - - -
  • -
    - -
    - -
    -

    Empty field

    - -
    - -
            return true;
    -      } else if (feature instanceof Object){
    -        feature.properties = {
    -          popupContent: self.infobox(doc),
    - -
  • - - -
  • -
    - -
    - -
    -

    Add a reference to the model id, which will allow us to -link this Leaflet layer to a Recline doc

    - -
    - -
              cid: doc.cid
    -        };
    -
    -        try {
    -          self.features.addData(feature);
    -        } catch (except) {
    -          wrongSoFar += 1;
    -          var msg = 'Wrong geometry value';
    -          if (except.message) msg += ' (' + except.message + ')';
    -          if (wrongSoFar <= 10) {
    -            self.trigger('recline:flash', {message: msg, category:'error'});
    -          }
    -        }
    -      } else {
    -        wrongSoFar += 1;
    -        if (wrongSoFar <= 10) {
    -          self.trigger('recline:flash', {message: 'Wrong geometry value', category:'error'});
    -        }
    -      }
    -      return true;
    -    });
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    Private: Remove one or n features from the map

    - -
    - -
      _remove: function(docs){
    -
    -    var self = this;
    -
    -    if (!(docs instanceof Array)) docs = [docs];
    -
    -    _.each(docs,function(doc){
    -      for (var key in self.features._layers){
    -        if (self.features._layers[key].feature.geometry.properties.cid == doc.cid){
    -          self.features.removeLayer(self.features._layers[key]);
    -        }
    -      }
    -    });
    -
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    Private: convert DMS coordinates to decimal

    -

    north and east are positive, south and west are negative

    - -
    - -
      _parseCoordinateString: function(coord){
    -    if (typeof(coord) != 'string') {
    -      return(parseFloat(coord));
    -    }
    -    var dms = coord.split(/[^-?\.\d\w]+/);
    -    var deg = 0; var m = 0;
    -    var toDeg = [1, 60, 3600]; // conversion factors for Deg, min, sec
    -    var i;
    -    for (i = 0; i < dms.length; ++i) {
    -        if (isNaN(parseFloat(dms[i]))) {
    -          continue;
    -        }
    -        deg += parseFloat(dms[i]) / toDeg[m];
    -        m += 1;
    -    }
    -    if (coord.match(/[SW]/)) {
    -          deg = -1*deg;
    -    }
    -    return(deg);
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    Private: Return a GeoJSON geomtry extracted from the record fields

    - -
    - -
      _getGeometryFromRecord: function(doc){
    -    if (this.state.get('geomField')){
    -      var value = doc.get(this.state.get('geomField'));
    -      if (typeof(value) === 'string'){
    - -
  • - - -
  • -
    - -
    - -
    -

    We may have a GeoJSON string representation

    - -
    - -
            try {
    -          value = $.parseJSON(value);
    -        } catch(e) {}
    -      }
    -      if (typeof(value) === 'string') {
    -        value = value.replace('(', '').replace(')', '');
    -        var parts = value.split(',');
    -        var lat = this._parseCoordinateString(parts[0]);
    -        var lon = this._parseCoordinateString(parts[1]);
    -
    -        if (!isNaN(lon) && !isNaN(parseFloat(lat))) {
    -          return {
    -            "type": "Point",
    -            "coordinates": [lon, lat]
    -          };
    -        } else {
    -          return null;
    -        }
    -      } else if (value && _.isArray(value)) {
    - -
  • - - -
  • -
    - -
    - -
    -

    [ lon, lat ]

    - -
    - -
            return {
    -          "type": "Point",
    -          "coordinates": [value[0], value[1]]
    -        };
    -      } else if (value && value.lat) {
    - -
  • - - -
  • -
    - -
    - -
    -

    of form { lat: …, lon: …}

    - -
    - -
            return {
    -          "type": "Point",
    -          "coordinates": [value.lon || value.lng, value.lat]
    -        };
    -      }
    - -
  • - - -
  • -
    - -
    - -
    -

    We o/w assume that contents of the field are a valid GeoJSON object

    - -
    - -
          return value;
    -    } else if (this.state.get('lonField') && this.state.get('latField')){
    - -
  • - - -
  • -
    - -
    - -
    -

    We’ll create a GeoJSON like point object from the two lat/lon fields

    - -
    - -
          var lon = doc.get(this.state.get('lonField'));
    -      var lat = doc.get(this.state.get('latField'));
    -      lon = this._parseCoordinateString(lon);
    -      lat = this._parseCoordinateString(lat);
    -
    -      if (!isNaN(parseFloat(lon)) && !isNaN(parseFloat(lat))) {
    -        return {
    -          type: 'Point',
    -          coordinates: [lon,lat]
    -        };
    -      }
    -    }
    -    return null;
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    Private: Check if there is a field with GeoJSON geometries or alternatively, -two fields with lat/lon values.

    -

    If not found, the user can define them via the UI form.

    - -
    - -
      _setupGeometryField: function(){
    - -
  • - - -
  • -
    - -
    - -
    -

    should not overwrite if we have already set this (e.g. explicitly via state)

    - -
    - -
        if (!this._geomReady()) {
    -      this.state.set({
    -        geomField: this._checkField(this.geometryFieldNames),
    -        latField: this._checkField(this.latitudeFieldNames),
    -        lonField: this._checkField(this.longitudeFieldNames)
    -      });
    -      this.menu.state.set(this.state.toJSON());
    -    }
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    Private: Check if a field in the current model exists in the provided -list of names.

    - -
    - -
      _checkField: function(fieldNames){
    -    var field;
    -    var modelFieldNames = this.model.fields.pluck('id');
    -    for (var i = 0; i < fieldNames.length; i++){
    -      for (var j = 0; j < modelFieldNames.length; j++){
    -        if (modelFieldNames[j].toLowerCase() == fieldNames[i].toLowerCase())
    -          return modelFieldNames[j];
    -      }
    -    }
    -    return null;
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    Private: Zoom to map to current features extent if any, or to the full -extent if none.

    - -
    - -
      _zoomToFeatures: function(){
    -    var bounds = this.features.getBounds();
    -    if (bounds && bounds.getNorthEast() && bounds.getSouthWest()){
    -      this.map.fitBounds(bounds);
    -    } else {
    -      this.map.setView([0, 0], 2);
    -    }
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    Private: Sets up the Leaflet map control and the features layer.

    -

    The map uses a base layer from OpenStreetMap based -on OpenStreetMap data.

    - -
    - -
      _setupMap: function(){
    -    var self = this;
    -    this.map = new L.Map(this.$map.get(0));
    -
    -    var mapUrl = "http://otile{s}-s.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png";
    -    var osmAttribution = 'Map data &copy; 2011 OpenStreetMap contributors, Tiles Courtesy of <a href="http://www.mapquest.com/" target="_blank">MapQuest</a> <img src="http://developer.mapquest.com/content/osm/mq_logo.png">';
    -    var bg = new L.TileLayer(mapUrl, {maxZoom: 18, attribution: osmAttribution ,subdomains: '1234'});
    -    this.map.addLayer(bg);
    -
    -    this.markers = new L.MarkerClusterGroup(this._clusterOptions);
    - -
  • - - -
  • -
    - -
    - -
    -

    rebind this (as needed in e.g. default case above)

    - -
    - -
        this.geoJsonLayerOptions.pointToLayer =  _.bind(
    -        this.geoJsonLayerOptions.pointToLayer,
    -        this);
    -    this.features = new L.GeoJSON(null, this.geoJsonLayerOptions);
    -
    -    this.map.setView([0, 0], 2);
    -
    -    this.mapReady = true;
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    Private: Helper function to select an option from a select list

    - -
    - -
      _selectOption: function(id,value){
    -    var options = $('.' + id + ' > select > option');
    -    if (options){
    -      options.each(function(opt){
    -        if (this.value == value) {
    -          $(this).attr('selected','selected');
    -          return false;
    -        }
    -      });
    -    }
    -  }
    -});
    -
    -my.MapMenu = Backbone.View.extend({
    -  className: 'editor',
    -
    -  template: ' \
    -    <form class="form-stacked"> \
    -      <div class="clearfix"> \
    -        <div class="editor-field-type"> \
    -            <label class="radio"> \
    -              <input type="radio" id="editor-field-type-latlon" name="editor-field-type" value="latlon" checked="checked"/> \
    -              Latitude / Longitude fields</label> \
    -            <label class="radio"> \
    -              <input type="radio" id="editor-field-type-geom" name="editor-field-type" value="geom" /> \
    -              GeoJSON field</label> \
    -        </div> \
    -        <div class="editor-field-type-latlon"> \
    -          <label>Latitude field</label> \
    -          <div class="input editor-lat-field"> \
    -            <select class="form-control"> \
    -            <option value=""></option> \
    -            {{#fields}} \
    -            <option value="{{id}}">{{label}}</option> \
    -            {{/fields}} \
    -            </select> \
    -          </div> \
    -          <label>Longitude field</label> \
    -          <div class="input editor-lon-field"> \
    -            <select class="form-control"> \
    -            <option value=""></option> \
    -            {{#fields}} \
    -            <option value="{{id}}">{{label}}</option> \
    -            {{/fields}} \
    -            </select> \
    -          </div> \
    -        </div> \
    -        <div class="editor-field-type-geom" style="display:none"> \
    -          <label>Geometry field (GeoJSON)</label> \
    -          <div class="input editor-geom-field"> \
    -            <select class="form-control"> \
    -            <option value=""></option> \
    -            {{#fields}} \
    -            <option value="{{id}}">{{label}}</option> \
    -            {{/fields}} \
    -            </select> \
    -          </div> \
    -        </div> \
    -      </div> \
    -      <div class="editor-buttons"> \
    -        <button class="btn btn-default editor-update-map">Update</button> \
    -      </div> \
    -      <div class="editor-options" > \
    -        <label class="checkbox"> \
    -          <input type="checkbox" id="editor-auto-zoom" value="autozoom" checked="checked" /> \
    -          Auto zoom to features</label> \
    -        <label class="checkbox"> \
    -          <input type="checkbox" id="editor-cluster" value="cluster"/> \
    -          Cluster markers</label> \
    -      </div> \
    -      <input type="hidden" class="editor-id" value="map-1" /> \
    -    </form> \
    -  ',
    - -
  • - - -
  • -
    - -
    - -
    -

    Define here events for UI elements

    - -
    - -
      events: {
    -    'click .editor-update-map': 'onEditorSubmit',
    -    'change .editor-field-type': 'onFieldTypeChange',
    -    'click #editor-auto-zoom': 'onAutoZoomChange',
    -    'click #editor-cluster': 'onClusteringChange'
    -  },
    -
    -  initialize: function(options) {
    -    var self = this;
    -    _.bindAll(this, 'render');
    -    this.listenTo(this.model.fields, 'change', this.render);
    -    this.state = new recline.Model.ObjectState(options.state);
    -    this.listenTo(this.state, 'change', this.render);
    -    this.render();
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    Public: Adds the necessary elements to the page.

    -

    Also sets up the editor fields and the map if necessary.

    - -
    - -
      render: function() {
    -    var self = this;
    -    var htmls = Mustache.render(this.template, this.model.toTemplateJSON());
    -    this.$el.html(htmls);
    -
    -    if (this._geomReady() && this.model.fields.length){
    -      if (this.state.get('geomField')){
    -        this._selectOption('editor-geom-field',this.state.get('geomField'));
    -        this.$el.find('#editor-field-type-geom').attr('checked','checked').change();
    -      } else{
    -        this._selectOption('editor-lon-field',this.state.get('lonField'));
    -        this._selectOption('editor-lat-field',this.state.get('latField'));
    -        this.$el.find('#editor-field-type-latlon').attr('checked','checked').change();
    -      }
    -    }
    -    if (this.state.get('autoZoom')) {
    -      this.$el.find('#editor-auto-zoom').attr('checked', 'checked');
    -    } else {
    -      this.$el.find('#editor-auto-zoom').removeAttr('checked');
    -    }
    -    if (this.state.get('cluster')) {
    -      this.$el.find('#editor-cluster').attr('checked', 'checked');
    -    } else {
    -      this.$el.find('#editor-cluster').removeAttr('checked');
    -    }
    -    return this;
    -  },
    -
    -  _geomReady: function() {
    -    return Boolean(this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField')));
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    UI Event handlers

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    Public: Update map with user options

    -

    Right now the only configurable option is what field(s) contains the -location information.

    - -
    - -
      onEditorSubmit: function(e){
    -    e.preventDefault();
    -    if (this.$el.find('#editor-field-type-geom').attr('checked')){
    -      this.state.set({
    -        geomField: this.$el.find('.editor-geom-field > select > option:selected').val(),
    -        lonField: null,
    -        latField: null
    -      });
    -    } else {
    -      this.state.set({
    -        geomField: null,
    -        lonField: this.$el.find('.editor-lon-field > select > option:selected').val(),
    -        latField: this.$el.find('.editor-lat-field > select > option:selected').val()
    -      });
    -    }
    -    return false;
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    Public: Shows the relevant select lists depending on the location field -type selected.

    - -
    - -
      onFieldTypeChange: function(e){
    -    if (e.target.value == 'geom'){
    -        this.$el.find('.editor-field-type-geom').show();
    -        this.$el.find('.editor-field-type-latlon').hide();
    -    } else {
    -        this.$el.find('.editor-field-type-geom').hide();
    -        this.$el.find('.editor-field-type-latlon').show();
    -    }
    -  },
    -
    -  onAutoZoomChange: function(e){
    -    this.state.set({autoZoom: !this.state.get('autoZoom')});
    -  },
    -
    -  onClusteringChange: function(e){
    -    this.state.set({cluster: !this.state.get('cluster')});
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    Private: Helper function to select an option from a select list

    - -
    - -
      _selectOption: function(id,value){
    -    var options = this.$el.find('.' + id + ' > select > option');
    -    if (options){
    -      options.each(function(opt){
    -        if (this.value == value) {
    -          $(this).attr('selected','selected');
    -          return false;
    -        }
    -      });
    -    }
    -  }
    -});
    -
    -})(jQuery, recline.View);
    - -
  • - -
-
- - diff --git a/docs/src/view.multiview.html b/docs/src/view.multiview.html deleted file mode 100644 index 070be0b1..00000000 --- a/docs/src/view.multiview.html +++ /dev/null @@ -1,1048 +0,0 @@ - - - - - view.multiview.js - - - - - -
-
- - - -
    - -
  • -
    -

    view.multiview.js

    -
    -
  • - - - -
  • -
    - -
    - -
    - -
    - -
    /*jshint multistr:true */
    - -
  • - - -
  • -
    - -
    - -
    -

    Standard JS module setup

    - -
    - -
    this.recline = this.recline || {};
    -this.recline.View = this.recline.View || {};
    -
    -(function($, my) {
    -  "use strict";
    - -
  • - - -
  • -
    - -
    - -
    -

    MultiView

    -

    Manage multiple views together along with query editor etc. Usage:

    -
    -var myExplorer = new recline.View.MultiView({
    -  model: {{recline.Model.Dataset instance}}
    -  el: {{an existing dom element}}
    -  views: {{dataset views}}
    -  state: {{state configuration -- see below}}
    -});
    -
    - -

    Parameters

    -

    model: (required) recline.model.Dataset instance.

    -

    el: (required) DOM element to bind to. NB: the element already -being in the DOM is important for rendering of some subviews (e.g. -Graph).

    -

    views: (optional) the dataset views (Grid, Graph etc) for -MultiView to show. This is an array of view hashes. If not provided -initialize with (recline.View.)Grid, Graph, and Map views (with obvious id -and labels!).

    -
    -var views = [
    -  {
    -    id: 'grid', // used for routing
    -    label: 'Grid', // used for view switcher
    -    view: new recline.View.Grid({
    -      model: dataset
    -    })
    -  },
    -  {
    -    id: 'graph',
    -    label: 'Graph',
    -    view: new recline.View.Graph({
    -      model: dataset
    -    })
    -  }
    -];
    -
    - -

    sidebarViews: (optional) the sidebar views (Filters, Fields) for -MultiView to show. This is an array of view hashes. If not provided -initialize with (recline.View.)FilterEditor and Fields views (with obvious -id and labels!).

    -
    -var sidebarViews = [
    -  {
    -    id: 'filterEditor', // used for routing
    -    label: 'Filters', // used for view switcher
    -    view: new recline.View.FilterEditor({
    -      model: dataset
    -    })
    -  },
    -  {
    -    id: 'fieldsView',
    -    label: 'Fields',
    -    view: new recline.View.Fields({
    -      model: dataset
    -    })
    -  }
    -];
    -
    - -

    state: standard state config for this view. This state is slightly - special as it includes config of many of the subviews.

    -
    -var state = {
    -    query: {dataset query state - see dataset.queryState object}
    -    'view-{id1}': {view-state for this view}
    -    'view-{id2}': {view-state for }
    -    ...
    -    // Explorer
    -    currentView: id of current view (defaults to first view if not specified)
    -    readOnly: (default: false) run in read-only mode
    -}
    -
    - -

    Note that at present we do not serialize information about the actual set -of views in use — e.g. those specified by the views argument — but instead -expect either that the default views are fine or that the client to have -initialized the MultiView with the relevant views themselves.

    - -
    - -
    my.MultiView = Backbone.View.extend({
    -  template: ' \
    -  <div class="recline-data-explorer"> \
    -    <div class="alert-messages"></div> \
    -    \
    -    <div class="header clearfix"> \
    -      <div class="navigation"> \
    -        <div class="btn-group" data-toggle="buttons-radio"> \
    -        {{#views}} \
    -        <button href="#{{id}}" data-view="{{id}}" class="btn btn-default">{{label}}</button> \
    -        {{/views}} \
    -        </div> \
    -      </div> \
    -      <div class="recline-results-info"> \
    -        <span class="doc-count">{{recordCount}}</span> records\
    -      </div> \
    -      <div class="menu-right"> \
    -        <div class="btn-group" data-toggle="buttons-checkbox"> \
    -          {{#sidebarViews}} \
    -          <button href="#" data-action="{{id}}" class="btn btn-default">{{label}}</button> \
    -          {{/sidebarViews}} \
    -        </div> \
    -      </div> \
    -      <div class="query-editor-here" style="display:inline;"></div> \
    -    </div> \
    -    <div class="data-view-sidebar"></div> \
    -    <div class="data-view-container"></div> \
    -  </div> \
    -  ',
    -  events: {
    -    'click .menu-right button': '_onMenuClick',
    -    'click .navigation button': '_onSwitchView'
    -  },
    -
    -  initialize: function(options) {
    -    var self = this;
    -    this._setupState(options.state);
    - -
  • - - -
  • -
    - -
    - -
    -

    Hash of ā€˜page’ views (i.e. those for whole page) keyed by page name

    - -
    - -
        if (options.views) {
    -      this.pageViews = options.views;
    -    } else {
    -      this.pageViews = [{
    -        id: 'grid',
    -        label: 'Grid',
    -        view: new my.SlickGrid({
    -          model: this.model,
    -          state: this.state.get('view-grid')
    -        })
    -      }, {
    -        id: 'graph',
    -        label: 'Graph',
    -        view: new my.Graph({
    -          model: this.model,
    -          state: this.state.get('view-graph')
    -        })
    -      }, {
    -        id: 'map',
    -        label: 'Map',
    -        view: new my.Map({
    -          model: this.model,
    -          state: this.state.get('view-map')
    -        })
    -      }, {
    -        id: 'timeline',
    -        label: 'Timeline',
    -        view: new my.Timeline({
    -          model: this.model,
    -          state: this.state.get('view-timeline')
    -        })
    -      }];
    -    }
    - -
  • - - -
  • -
    - -
    - -
    -

    Hashes of sidebar elements

    - -
    - -
        if(options.sidebarViews) {
    -      this.sidebarViews = options.sidebarViews;
    -    } else {
    -      this.sidebarViews = [{
    -        id: 'filterEditor',
    -        label: 'Filters',
    -        view: new my.FilterEditor({
    -          model: this.model
    -        })
    -      }, {
    -        id: 'fieldsView',
    -        label: 'Fields',
    -        view: new my.Fields({
    -          model: this.model
    -        })
    -      }];
    -    }
    - -
  • - - -
  • -
    - -
    - -
    -

    these must be called after pageViews are created

    - -
    - -
        this.render();
    -    this._bindStateChanges();
    -    this._bindFlashNotifications();
    - -
  • - - -
  • -
    - -
    - -
    -

    now do updates based on state (need to come after render)

    - -
    - -
        if (this.state.get('readOnly')) {
    -      this.setReadOnly();
    -    }
    -    if (this.state.get('currentView')) {
    -      this.updateNav(this.state.get('currentView'));
    -    } else {
    -      this.updateNav(this.pageViews[0].id);
    -    }
    -    this._showHideSidebar();
    -
    -    this.listenTo(this.model, 'query:start', function() {
    -      self.notify({loader: true, persist: true});
    -    });
    -    this.listenTo(this.model, 'query:done', function() {
    -      self.clearNotifications();
    -      self.$el.find('.doc-count').text(self.model.recordCount || 'Unknown');
    -    });
    -    this.listenTo(this.model, 'query:fail', function(error) {
    -      self.clearNotifications();
    -      var msg = '';
    -      if (typeof(error) == 'string') {
    -        msg = error;
    -      } else if (typeof(error) == 'object') {
    -        if (error.title) {
    -          msg = error.title + ': ';
    -        }
    -        if (error.message) {
    -          msg += error.message;
    -        }
    -      } else {
    -        msg = 'There was an error querying the backend';
    -      }
    -      self.notify({message: msg, category: 'error', persist: true});
    -    });
    - -
  • - - -
  • -
    - -
    - -
    -

    retrieve basic data like fields etc -note this.model and dataset returned are the same -TODO: set query state …?

    - -
    - -
        this.model.queryState.set(self.state.get('query'), {silent: true});
    -  },
    -
    -  setReadOnly: function() {
    -    this.$el.addClass('recline-read-only');
    -  },
    -
    -  render: function() {
    -    var tmplData = this.model.toTemplateJSON();
    -    tmplData.views = this.pageViews;
    -    tmplData.sidebarViews = this.sidebarViews;
    -    var template = Mustache.render(this.template, tmplData);
    -    this.$el.html(template);
    - -
  • - - -
  • -
    - -
    - -
    -

    now create and append other views

    - -
    - -
        var $dataViewContainer = this.$el.find('.data-view-container');
    -    var $dataSidebar = this.$el.find('.data-view-sidebar');
    - -
  • - - -
  • -
    - -
    - -
    -

    the main views

    - -
    - -
        _.each(this.pageViews, function(view, pageName) {
    -      view.view.render();
    -      if (view.view.redraw) {
    -        view.view.redraw();
    -      }
    -      $dataViewContainer.append(view.view.el);
    -      if (view.view.elSidebar) {
    -        $dataSidebar.append(view.view.elSidebar);
    -      }
    -    });
    -
    -    _.each(this.sidebarViews, function(view) {
    -      this['$'+view.id] = view.view.$el;
    -      $dataSidebar.append(view.view.el);
    -    }, this);
    -
    -    this.pager = new recline.View.Pager({
    -      model: this.model
    -    });
    -    this.$el.find('.recline-results-info').after(this.pager.el);
    -
    -    this.queryEditor = new recline.View.QueryEditor({
    -      model: this.model.queryState
    -    });
    -    this.$el.find('.query-editor-here').append(this.queryEditor.el);
    -
    -  },
    -
    -  remove: function () {
    -    _.each(this.pageViews, function (view) {
    -      view.view.remove();
    -    });
    -    _.each(this.sidebarViews, function (view) {
    -      view.view.remove();
    -    });
    -    this.pager.remove();
    -    this.queryEditor.remove();
    -    Backbone.View.prototype.remove.apply(this, arguments);
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    hide the sidebar if empty

    - -
    - -
      _showHideSidebar: function() {
    -    var $dataSidebar = this.$el.find('.data-view-sidebar');
    -    var visibleChildren = $dataSidebar.children().filter(function() {
    -      return $(this).css("display") != "none";
    -    }).length;
    -
    -    if (visibleChildren > 0) {
    -      $dataSidebar.show();
    -    } else {
    -      $dataSidebar.hide();
    -    }
    -  },
    -
    -  updateNav: function(pageName) {
    -    this.$el.find('.navigation button').removeClass('active');
    -    var $el = this.$el.find('.navigation button[data-view="' + pageName + '"]');
    -    $el.addClass('active');
    - -
  • - - -
  • -
    - -
    - -
    -

    add/remove sidebars and hide inactive views

    - -
    - -
        _.each(this.pageViews, function(view, idx) {
    -      if (view.id === pageName) {
    -        view.view.$el.show();
    -        if (view.view.elSidebar) {
    -          view.view.elSidebar.show();
    -        }
    -      } else {
    -        view.view.$el.hide();
    -        if (view.view.elSidebar) {
    -          view.view.elSidebar.hide();
    -        }
    -        if (view.view.hide) {
    -          view.view.hide();
    -        }
    -      }
    -    });
    -
    -    this._showHideSidebar();
    - -
  • - - -
  • -
    - -
    - -
    -

    call view.view.show after sidebar visibility has been determined so -that views can correctly calculate their maximum width

    - -
    - -
        _.each(this.pageViews, function(view, idx) {
    -      if (view.id === pageName) {
    -        if (view.view.show) {
    -          view.view.show();
    -        }
    -      }
    -    });
    -  },
    -
    -  _onMenuClick: function(e) {
    -    e.preventDefault();
    -    var action = $(e.target).attr('data-action');
    -    this['$'+action].toggle();
    -    this._showHideSidebar();
    -  },
    -
    -  _onSwitchView: function(e) {
    -    e.preventDefault();
    -    var viewName = $(e.target).attr('data-view');
    -    this.updateNav(viewName);
    -    this.state.set({currentView: viewName});
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    create a state object for this view and do the job of

    -

    a) initializing it from both data passed in and other sources (e.g. hash url)

    -

    b) ensure the state object is updated in responese to changes in subviews, query etc.

    - -
    - -
      _setupState: function(initialState) {
    -    var self = this;
    - -
  • - - -
  • -
    - -
    - -
    -

    get data from the query string / hash url plus some defaults

    - -
    - -
        var qs = my.parseHashQueryString();
    -    var query = qs.reclineQuery;
    -    query = query ? JSON.parse(query) : self.model.queryState.toJSON();
    - -
  • - - -
  • -
    - -
    - -
    -

    backwards compatability (now named view-graph but was named graph)

    - -
    - -
        var graphState = qs['view-graph'] || qs.graph;
    -    graphState = graphState ? JSON.parse(graphState) : {};
    - -
  • - - -
  • -
    - -
    - -
    -

    now get default data + hash url plus initial state and initial our state object with it

    - -
    - -
        var stateData = _.extend({
    -        query: query,
    -        'view-graph': graphState,
    -        backend: this.model.backend.__type__,
    -        url: this.model.get('url'),
    -        dataset: this.model.toJSON(),
    -        currentView: null,
    -        readOnly: false
    -      },
    -      initialState);
    -    this.state = new recline.Model.ObjectState(stateData);
    -  },
    -
    -  _bindStateChanges: function() {
    -    var self = this;
    - -
  • - - -
  • -
    - -
    - -
    -

    finally ensure we update our state object when state of sub-object changes so that state is always up to date

    - -
    - -
        this.listenTo(this.model.queryState, 'change', function() {
    -      self.state.set({query: self.model.queryState.toJSON()});
    -    });
    -    _.each(this.pageViews, function(pageView) {
    -      if (pageView.view.state && pageView.view.state.bind) {
    -        var update = {};
    -        update['view-' + pageView.id] = pageView.view.state.toJSON();
    -        self.state.set(update);
    -        self.listenTo(pageView.view.state, 'change', function() {
    -          var update = {};
    -          update['view-' + pageView.id] = pageView.view.state.toJSON();
    - -
  • - - -
  • -
    - -
    - -
    -

    had problems where change not being triggered for e.g. grid view so let’s do it explicitly

    - -
    - -
              self.state.set(update, {silent: true});
    -          self.state.trigger('change');
    -        });
    -      }
    -    });
    -  },
    -
    -  _bindFlashNotifications: function() {
    -    var self = this;
    -    _.each(this.pageViews, function(pageView) {
    -      self.listenTo(pageView.view, 'recline:flash', function(flash) {
    -        self.notify(flash);
    -      });
    -    });
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    notify

    -

    Create a notification (a div.alert in div.alert-messsages) using provided -flash object. Flash attributes (all are optional):

    -
      -
    • message: message to show.
    • -
    • category: warning (default), success, error
    • -
    • persist: if true alert is persistent, o/w hidden after 3s (default = false)
    • -
    • loader: if true show loading spinner
    • -
    - -
    - -
      notify: function(flash) {
    -    var tmplData = _.extend({
    -      message: 'Loading',
    -      category: 'warning',
    -      loader: false
    -      },
    -      flash
    -    );
    -    var _template;
    -    if (tmplData.loader) {
    -      _template = ' \
    -        <div class="alert alert-info alert-loader"> \
    -          {{message}} \
    -          <span class="notification-loader">&nbsp;</span> \
    -        </div>';
    -    } else {
    -      _template = ' \
    -        <div class="alert alert-{{category}} fade in" data-alert="alert"><a class="close" data-dismiss="alert" href="#">Ɨ</a> \
    -          {{message}} \
    -        </div>';
    -    }
    -    var _templated = $(Mustache.render(_template, tmplData));
    -    _templated = $(_templated).appendTo($('.recline-data-explorer .alert-messages'));
    -    if (!flash.persist) {
    -      setTimeout(function() {
    -        $(_templated).fadeOut(1000, function() {
    -          $(this).remove();
    -        });
    -      }, 1000);
    -    }
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    clearNotifications

    -

    Clear all existing notifications

    - -
    - -
      clearNotifications: function() {
    -    var $notifications = $('.recline-data-explorer .alert-messages .alert');
    -    $notifications.fadeOut(1500, function() {
    -      $(this).remove();
    -    });
    -  }
    -});
    - -
  • - - -
  • -
    - -
    - -
    -

    MultiView.restore

    -

    Restore a MultiView instance from a serialized state including the associated dataset

    -

    This inverts the state serialization process in Multiview

    - -
    - -
    my.MultiView.restore = function(state) {
    - -
  • - - -
  • -
    - -
    - -
    -

    hack-y - restoring a memory dataset does not mean much … (but useful for testing!)

    - -
    - -
      var datasetInfo;
    -  if (state.backend === 'memory') {
    -    datasetInfo = {
    -      backend: 'memory',
    -      records: [{stub: 'this is a stub dataset because we do not restore memory datasets'}]
    -    };
    -  } else {
    -    datasetInfo = _.extend({
    -        url: state.url,
    -        backend: state.backend
    -      },
    -      state.dataset
    -    );
    -  }
    -  var dataset = new recline.Model.Dataset(datasetInfo);
    -  var explorer = new my.MultiView({
    -    model: dataset,
    -    state: state
    -  });
    -  return explorer;
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Miscellaneous Utilities

    - -
    - -
    var urlPathRegex = /^([^?]+)(\?.*)?/;
    - -
  • - - -
  • -
    - -
    - -
    -

    Parse the Hash section of a URL into path and query string

    - -
    - -
    my.parseHashUrl = function(hashUrl) {
    -  var parsed = urlPathRegex.exec(hashUrl);
    -  if (parsed === null) {
    -    return {};
    -  } else {
    -    return {
    -      path: parsed[1],
    -      query: parsed[2] || ''
    -    };
    -  }
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Parse a URL query string (?xyz=abc…) into a dictionary.

    - -
    - -
    my.parseQueryString = function(q) {
    -  if (!q) {
    -    return {};
    -  }
    -  var urlParams = {},
    -    e, d = function (s) {
    -      return unescape(s.replace(/\+/g, " "));
    -    },
    -    r = /([^&=]+)=?([^&]*)/g;
    -
    -  if (q && q.length && q[0] === '?') {
    -    q = q.slice(1);
    -  }
    -  while (e = r.exec(q)) {
    - -
  • - - -
  • -
    - -
    - -
    -

    TODO: have values be array as query string allow repetition of keys

    - -
    - -
        urlParams[d(e[1])] = d(e[2]);
    -  }
    -  return urlParams;
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Parse the query string out of the URL hash

    - -
    - -
    my.parseHashQueryString = function() {
    -  var q = my.parseHashUrl(window.location.hash).query;
    -  return my.parseQueryString(q);
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Compse a Query String

    - -
    - -
    my.composeQueryString = function(queryParams) {
    -  var queryString = '?';
    -  var items = [];
    -  $.each(queryParams, function(key, value) {
    -    if (typeof(value) === 'object') {
    -      value = JSON.stringify(value);
    -    }
    -    items.push(key + '=' + encodeURIComponent(value));
    -  });
    -  queryString += items.join('&');
    -  return queryString;
    -};
    -
    -my.getNewHashForQueryString = function(queryParams) {
    -  var queryPart = my.composeQueryString(queryParams);
    -  if (window.location.hash) {
    - -
  • - - -
  • -
    - -
    - -
    -

    slice(1) to remove # at start

    - -
    - -
        return window.location.hash.split('?')[0].slice(1) + queryPart;
    -  } else {
    -    return queryPart;
    -  }
    -};
    -
    -my.setHashQueryString = function(queryParams) {
    -  window.location.hash = my.getNewHashForQueryString(queryParams);
    -};
    -
    -})(jQuery, recline.View);
    - -
  • - -
-
- - diff --git a/docs/src/view.slickgrid.html b/docs/src/view.slickgrid.html deleted file mode 100644 index 80bb6006..00000000 --- a/docs/src/view.slickgrid.html +++ /dev/null @@ -1,1114 +0,0 @@ - - - - - view.slickgrid.js - - - - - -
-
- - - -
    - -
  • -
    -

    view.slickgrid.js

    -
    -
  • - - - -
  • -
    - -
    - -
    - -
    - -
    /*jshint multistr:true */
    -
    -this.recline = this.recline || {};
    -this.recline.View = this.recline.View || {};
    -
    -(function($, my) {
    -  "use strict";
    - -
  • - - -
  • -
    - -
    - -
    -

    SlickGrid Dataset View

    -

    Provides a tabular view on a Dataset, based on SlickGrid.

    -

    https://github.com/mleibman/SlickGrid

    -

    Initialize it with a recline.Model.Dataset.

    -

    Additional options to drive SlickGrid grid can be given through state. -The following keys allow for customization:

    -
      -
    • gridOptions: to add options at grid level
    • -
    • columnsEditor: to add editor for editable columns
    • -
    -

    For example: - var grid = new recline.View.SlickGrid({ - model: dataset, - el: $el, - state: { - gridOptions: { - editable: true, - enableAddRow: true - // Enable support for row delete - enabledDelRow: true, - // Enable support for row Reorder - enableReOrderRow:true, - … - }, - columnsEditor: [ - {column: ā€˜date’, editor: Slick.Editors.Date }, - {column: ā€˜title’, editor: Slick.Editors.Text} - ] - } - }); -// NB: you need an explicit height on the element for slickgrid to work

    - -
    - -
    my.SlickGrid = Backbone.View.extend({
    -  initialize: function(modelEtc) {
    -    var self = this;
    -    this.$el.addClass('recline-slickgrid');
    - -
  • - - -
  • -
    - -
    - -
    -

    Template for row delete menu , change it if you don’t love

    - -
    - -
        this.templates = {
    -      "deleterow" : '<button href="#" class="recline-row-delete btn btn-default" title="Delete row">X</button>'
    -    };
    -
    -    _.bindAll(this, 'render', 'onRecordChanged');
    -    this.listenTo(this.model.records, 'add remove reset', this.render);
    -    this.listenTo(this.model.records, 'change', this.onRecordChanged);
    -    var state = _.extend({
    -        hiddenColumns: [],
    -        columnsOrder: [],
    -        columnsSort: {},
    -        columnsWidth: [],
    -        columnsEditor: [],
    -        options: {},
    -        fitColumns: false
    -      }, modelEtc.state
    -
    -    );
    -    this.state = new recline.Model.ObjectState(state);
    -    this._slickHandler = new Slick.EventHandler();
    - -
  • - - -
  • -
    - -
    - -
    -

    add menu for new row , check if enableAddRow is set to true or not set

    - -
    - -
        if(this.state.get("gridOptions") 
    -  && this.state.get("gridOptions").enabledAddRow != undefined 
    -      && this.state.get("gridOptions").enabledAddRow == true ){
    -      this.editor    =  new  my.GridControl()
    -      this.elSidebar =  this.editor.$el
    -  this.listenTo(this.editor.state, 'change', function(){   
    -    this.model.records.add(new recline.Model.Record())
    -      });
    -    }
    -  },
    -
    -  onRecordChanged: function(record) {
    - -
  • - - -
  • -
    - -
    - -
    -

    Ignore if the grid is not yet drawn

    - -
    - -
        if (!this.grid) {
    -      return;
    -    }
    - -
  • - - -
  • -
    - -
    - -
    -

    Let’s find the row corresponding to the index

    - -
    - -
        var row_index = this.grid.getData().getModelRow( record );
    -    this.grid.invalidateRow(row_index);
    -    this.grid.getData().updateItem(record, row_index);
    -    this.grid.render();
    -  },
    -
    -  render: function() {
    -    var self = this;
    -    var options = _.extend({
    -      enableCellNavigation: true,
    -      enableColumnReorder: true,
    -      explicitInitialization: true,
    -      syncColumnCellResize: true,
    -      forceFitColumns: this.state.get('fitColumns')
    -    }, self.state.get('gridOptions'));
    - -
  • - - -
  • -
    - -
    - -
    -

    We need all columns, even the hidden ones, to show on the column picker

    - -
    - -
        var columns = [];
    - -
  • - - -
  • -
    - -
    - -
    -

    custom formatter as default one escapes html -plus this way we distinguish between rendering/formatting and computed value (so e.g. sort still works …) -row = row index, cell = cell index, value = value, columnDef = column definition, dataContext = full row values

    - -
    - -
        var formatter = function(row, cell, value, columnDef, dataContext) {
    -      if(columnDef.id == "del"){
    -        return self.templates.deleterow 
    -      }
    -      var field = self.model.fields.get(columnDef.id);
    -      if (field.renderer) {
    -        return  field.renderer(value, field, dataContext);
    -      } else {
    -        return  value 
    -      }
    -    };
    - -
  • - - -
  • -
    - -
    - -
    -

    we need to be sure that user is entering a valid input , for exemple if -field is date type and field.format =’YY-MM-DD’, we should be sure that -user enter a correct value

    - -
    - -
        var validator = function(field) {
    -      return function(value){
    -        if (field.type == "date" && isNaN(Date.parse(value))){
    -          return {
    -            valid: false,
    -            msg: "A date is required, check field field-date-format"
    -          };
    -        } else {
    -          return {valid: true, msg :null } 
    -        }
    -      }
    -    };
    - -
  • - - -
  • -
    - -
    - -
    -

    Add column for row reorder support

    - -
    - -
        if (this.state.get("gridOptions") && this.state.get("gridOptions").enableReOrderRow == true) {
    -      columns.push({
    -        id: "#",
    -        name: "",
    -        width: 22,
    -        behavior: "selectAndMove",
    -        selectable: false,
    -        resizable: false,
    -        cssClass: "recline-cell-reorder"
    -      })
    -    }
    - -
  • - - -
  • -
    - -
    - -
    -

    Add column for row delete support

    - -
    - -
        if (this.state.get("gridOptions") && this.state.get("gridOptions").enabledDelRow == true) {
    -      columns.push({
    -        id: 'del',
    -        name: '',
    -        field: 'del',
    -        sortable: true,
    -        width: 38,
    -        formatter: formatter,
    -        validator:validator
    -      })
    -    }
    -
    -    function sanitizeFieldName(name) {
    -      var sanitized = $(name).text();
    -      return (name !== sanitized && sanitized !== '') ? sanitized : name;
    -    }
    -
    -    _.each(this.model.fields.toJSON(),function(field){
    -      var column = {
    -        id: field.id,
    -        name: sanitizeFieldName(field.label),
    -        field: field.id,
    -        sortable: true,
    -        minWidth: 80,
    -        formatter: formatter,
    -        validator:validator(field)
    -      };
    -      var widthInfo = _.find(self.state.get('columnsWidth'),function(c){return c.column === field.id;});
    -      if (widthInfo){
    -        column.width = widthInfo.width;
    -      }
    -      var editInfo = _.find(self.state.get('columnsEditor'),function(c){return c.column === field.id;});
    -      if (editInfo){
    -        column.editor = editInfo.editor;
    -      } else {
    - -
  • - - -
  • -
    - -
    - -
    -

    guess editor type

    - -
    - -
            var typeToEditorMap = {
    -          'string': Slick.Editors.LongText,
    -          'integer': Slick.Editors.IntegerEditor,
    -          'number': Slick.Editors.Text,
    - -
  • - - -
  • -
    - -
    - -
    -

    TODO: need a way to ensure we format date in the right way -Plus what if dates are in distant past or future … (?) -ā€˜date’: Slick.Editors.DateEditor,

    - -
    - -
              'date': Slick.Editors.Text,
    -          'boolean': Slick.Editors.YesNoSelectEditor
    - -
  • - - -
  • -
    - -
    - -
    -

    TODO: (?) percent …

    - -
    - -
            };
    -        if (field.type in typeToEditorMap) {
    -          column.editor = typeToEditorMap[field.type]
    -        } else {
    -          column.editor = Slick.Editors.LongText;
    -        }
    -      }
    -      columns.push(column);
    -    });
    - -
  • - - -
  • -
    - -
    - -
    -

    Restrict the visible columns

    - -
    - -
        var visibleColumns = _.filter(columns, function(column) {
    -      return _.indexOf(self.state.get('hiddenColumns'), column.id) === -1;
    -    });
    - -
  • - - -
  • -
    - -
    - -
    -

    Order them if there is ordering info on the state

    - -
    - -
        if (this.state.get('columnsOrder') && this.state.get('columnsOrder').length > 0) {
    -      visibleColumns = visibleColumns.sort(function(a,b){
    -        return _.indexOf(self.state.get('columnsOrder'),a.id) > _.indexOf(self.state.get('columnsOrder'),b.id) ? 1 : -1;
    -      });
    -      columns = columns.sort(function(a,b){
    -        return _.indexOf(self.state.get('columnsOrder'),a.id) > _.indexOf(self.state.get('columnsOrder'),b.id) ? 1 : -1;
    -      });
    -    }
    - -
  • - - -
  • -
    - -
    - -
    -

    Move hidden columns to the end, so they appear at the bottom of the -column picker

    - -
    - -
        var tempHiddenColumns = [];
    -    for (var i = columns.length -1; i >= 0; i--){
    -      if (_.indexOf(_.pluck(visibleColumns,'id'),columns[i].id) === -1){
    -        tempHiddenColumns.push(columns.splice(i,1)[0]);
    -      }
    -    }
    -    columns = columns.concat(tempHiddenColumns);
    - -
  • - - -
  • -
    - -
    - -
    -

    Transform a model object into a row

    - -
    - -
        function toRow(m) {
    -      var row = {};
    -      self.model.fields.each(function(field) {
    -        var render = "";
    - -
  • - - -
  • -
    - -
    - -
    -

    when adding row from slickgrid the field value is undefined

    - -
    - -
            if(!_.isUndefined(m.getFieldValueUnrendered(field))){
    -           render =m.getFieldValueUnrendered(field)
    -        }
    -        row[field.id] = render
    -      });
    -      return row;
    -    }
    -
    -    function RowSet() {
    -      var models = [];
    -      var rows = [];
    -
    -      this.push = function(model, row) {
    -        models.push(model);
    -        rows.push(row);
    -      };
    -
    -      this.getLength = function() {return rows.length; };
    -      this.getItem = function(index) {return rows[index];};
    -      this.getItemMetadata = function(index) {return {};};
    -      this.getModel = function(index) {return models[index];};
    -      this.getModelRow = function(m) {return _.indexOf(models, m);};
    -      this.updateItem = function(m,i) {
    -        rows[i] = toRow(m);
    -        models[i] = m;
    -      };
    -    }
    -
    -    var data = new RowSet();
    -
    -    this.model.records.each(function(doc){
    -      data.push(doc, toRow(doc));
    -    });
    -
    -    this.grid = new Slick.Grid(this.el, data, visibleColumns, options);
    - -
  • - - -
  • -
    - -
    - -
    -

    Column sorting

    - -
    - -
        var sortInfo = this.model.queryState.get('sort');
    -    if (sortInfo){
    -      var column = sortInfo[0].field;
    -      var sortAsc = sortInfo[0].order !== 'desc';
    -      this.grid.setSortColumn(column, sortAsc);
    -    }
    -
    -    if (this.state.get("gridOptions") && this.state.get("gridOptions").enableReOrderRow) {
    -      this._setupRowReordering();
    -    }
    -    
    -    this._slickHandler.subscribe(this.grid.onSort, function(e, args){
    -      var order = (args.sortAsc) ? 'asc':'desc';
    -      var sort = [{
    -        field: args.sortCol.field,
    -        order: order
    -      }];
    -      self.model.query({sort: sort});
    -    });
    -    
    -    this._slickHandler.subscribe(this.grid.onColumnsReordered, function(e, args){
    -      self.state.set({columnsOrder: _.pluck(self.grid.getColumns(),'id')});
    -    });
    -    
    -    this.grid.onColumnsResized.subscribe(function(e, args){
    -        var columns = args.grid.getColumns();
    -        var defaultColumnWidth = args.grid.getOptions().defaultColumnWidth;
    -        var columnsWidth = [];
    -        _.each(columns,function(column){
    -          if (column.width != defaultColumnWidth){
    -            columnsWidth.push({column:column.id,width:column.width});
    -          }
    -        });
    -        self.state.set({columnsWidth:columnsWidth});
    -    });
    -    
    -    this._slickHandler.subscribe(this.grid.onCellChange, function (e, args) {
    - -
  • - - -
  • -
    - -
    - -
    -

    We need to change the model associated value

    - -
    - -
          var grid = args.grid;
    -      var model = data.getModel(args.row);
    -      var field = grid.getColumns()[args.cell].id;
    -      var v = {};
    -      v[field] = args.item[field];
    -      model.set(v);
    -    });  
    -    this._slickHandler.subscribe(this.grid.onClick,function(e, args){
    - -
  • - - -
  • -
    - -
    - -
    -

    try catch , because this fail in qunit , but no -error on browser.

    - -
    - -
          try{e.preventDefault()}catch(e){}
    - -
  • - - -
  • -
    - -
    - -
    -

    The cell of grid that handle row delete is The first cell (0) if -The grid ReOrder is not present ie enableReOrderRow == false -else it is The the second cell (1) , because The 0 is now cell -that handle row Reoder.

    - -
    - -
          var cell =0
    -      if(self.state.get("gridOptions") 
    -  && self.state.get("gridOptions").enableReOrderRow != undefined 
    -        && self.state.get("gridOptions").enableReOrderRow == true ){
    -        cell =1
    -      }
    -      if (args.cell == cell && self.state.get("gridOptions").enabledDelRow == true){
    - -
  • - - -
  • -
    - -
    - -
    -

    We need to delete the associated model

    - -
    - -
              var model = data.getModel(args.row);
    -          model.destroy()
    -        }
    -    }) ;
    -    var columnpicker = new Slick.Controls.ColumnPicker(columns, this.grid,
    -                                                       _.extend(options,{state:this.state}));
    -    if (self.visible){
    -      self.grid.init();
    -      self.rendered = true;
    -    } else {
    - -
  • - - -
  • -
    - -
    - -
    -

    Defer rendering until the view is visible

    - -
    - -
          self.rendered = false;
    -    }
    -    return this;
    -  },
    - -
  • - - -
  • - - -
      _setupRowReordering: function() {
    -    var self = this;
    -    self.grid.setSelectionModel(new Slick.RowSelectionModel());
    -
    -    var moveRowsPlugin = new Slick.RowMoveManager({
    -      cancelEditOnDrag: true
    -    });
    -
    -    moveRowsPlugin.onBeforeMoveRows.subscribe(function (e, data) {
    -      for (var i = 0; i < data.rows.length; i++) {
    - -
  • - - -
  • -
    - -
    - -
    -

    no point in moving before or after itself

    - -
    - -
            if (data.rows[i] == data.insertBefore || data.rows[i] == data.insertBefore - 1) {
    -          e.stopPropagation();
    -          return false;
    -        }
    -      }
    -      return true;
    -    });
    -    
    -    moveRowsPlugin.onMoveRows.subscribe(function (e, args) {
    -      var extractedRows = [], left, right;
    -      var rows = args.rows;
    -      var insertBefore = args.insertBefore;
    -
    -      var data = self.model.records.toJSON()      
    -      left = data.slice(0, insertBefore);
    -      right= data.slice(insertBefore, data.length);
    -      
    -      rows.sort(function(a,b) { return a-b; });
    -
    -      for (var i = 0; i < rows.length; i++) {
    -          extractedRows.push(data[rows[i]]);
    -      }
    -
    -      rows.reverse();
    -
    -      for (var i = 0; i < rows.length; i++) {
    -        var row = rows[i];
    -        if (row < insertBefore) {
    -          left.splice(row, 1);
    -        } else {
    -          right.splice(row - insertBefore, 1);
    -        }
    -      }
    -
    -      data = left.concat(extractedRows.concat(right));
    -      var selectedRows = [];
    -      for (var i = 0; i < rows.length; i++)
    -        selectedRows.push(left.length + i);      
    -
    -      self.model.records.reset(data)
    -      
    -    });
    - -
  • - - -
  • -
    - -
    - -
    -

    register The plugin to handle row Reorder

    - -
    - -
        if(this.state.get("gridOptions") && this.state.get("gridOptions").enableReOrderRow) {
    -      self.grid.registerPlugin(moveRowsPlugin);
    -    }
    -  },
    -
    -  remove: function () {
    -    this._slickHandler.unsubscribeAll();
    -    Backbone.View.prototype.remove.apply(this, arguments);
    -  },
    -
    -  show: function() {
    - -
  • - - -
  • -
    - -
    - -
    -

    If the div is hidden, SlickGrid will calculate wrongly some -sizes so we must render it explicitly when the view is visible

    - -
    - -
        if (!this.rendered){
    -      if (!this.grid){
    -        this.render();
    -      }
    -      this.grid.init();
    -      this.rendered = true;
    -    }
    -    this.visible = true;
    -  },
    -
    -  hide: function() {
    -    this.visible = false;
    -  }
    -});
    - -
  • - - -
  • -
    - -
    - -
    -

    Add new grid Control to display a new row add menu bouton -It display a simple side-bar menu ,for user to add new -row to grid

    - -
    - -
    my.GridControl= Backbone.View.extend({
    -  className: "recline-row-add",
    - -
  • - - -
  • -
    - -
    - -
    -

    Template for row edit menu , change it if you don’t love

    - -
    - -
      template: '<h1><button href="#" class="recline-row-add btn btn-default">Add row</button></h1>',
    -  
    -  initialize: function(options){
    -    var self = this;
    -    _.bindAll(this, 'render');
    -    this.state = new recline.Model.ObjectState();
    -    this.render();
    -  },
    -
    -  render: function() {
    -    var self = this;
    -    this.$el.html(this.template)
    -  },
    -
    -  events : {
    -    "click .recline-row-add" : "addNewRow"
    -  },
    -
    -  addNewRow : function(e){
    -    e.preventDefault()
    -    this.state.trigger("change")
    - }
    -});
    -
    -})(jQuery, recline.View);
    -
    -/*
    -* Context menu for the column picker, adapted from
    -* http://mleibman.github.com/SlickGrid/examples/example-grouping
    -*
    -*/
    -(function ($) {
    -  function SlickColumnPicker(columns, grid, options) {
    -    var $menu;
    -    var columnCheckboxes;
    -
    -    var defaults = {
    -      fadeSpeed:250
    -    };
    -
    -    function init() {
    -      grid.onHeaderContextMenu.subscribe(handleHeaderContextMenu);
    -      options = $.extend({}, defaults, options);
    -
    -      $menu = $('<ul class="dropdown-menu slick-contextmenu" style="display:none;position:absolute;z-index:20;" />').appendTo(document.body);
    -
    -      $menu.bind('mouseleave', function (e) {
    -        $(this).fadeOut(options.fadeSpeed);
    -      });
    -      $menu.bind('click', updateColumn);
    -
    -    }
    -
    -    function handleHeaderContextMenu(e, args) {
    -      e.preventDefault();
    -      $menu.empty();
    -      columnCheckboxes = [];
    -
    -      var $li, $input;
    -      for (var i = 0; i < columns.length; i++) {
    -        $li = $('<li />').appendTo($menu);
    -        $input = $('<input type="checkbox" />').data('column-id', columns[i].id).attr('id','slick-column-vis-'+columns[i].id);
    -        columnCheckboxes.push($input);
    -
    -        if (grid.getColumnIndex(columns[i].id) !== null) {
    -          $input.attr('checked', 'checked');
    -        }
    -        $input.appendTo($li);
    -        $('<label />')
    -            .text(columns[i].name)
    -            .attr('for','slick-column-vis-'+columns[i].id)
    -            .appendTo($li);
    -      }
    -      $('<li/>').addClass('divider').appendTo($menu);
    -      $li = $('<li />').data('option', 'autoresize').appendTo($menu);
    -      $input = $('<input type="checkbox" />').data('option', 'autoresize').attr('id','slick-option-autoresize');
    -      $input.appendTo($li);
    -      $('<label />')
    -          .text('Force fit columns')
    -          .attr('for','slick-option-autoresize')
    -          .appendTo($li);
    -      if (grid.getOptions().forceFitColumns) {
    -        $input.attr('checked', 'checked');
    -      }
    -
    -      $menu.css('top', e.pageY - 10)
    -          .css('left', e.pageX - 10)
    -          .fadeIn(options.fadeSpeed);
    -    }
    -
    -    function updateColumn(e) {
    -      var checkbox;
    -
    -      if ($(e.target).data('option') === 'autoresize') {
    -        var checked;
    -        if ($(e.target).is('li')){
    -            checkbox = $(e.target).find('input').first();
    -            checked = !checkbox.is(':checked');
    -            checkbox.attr('checked',checked);
    -        } else {
    -          checked = e.target.checked;
    -        }
    -
    -        if (checked) {
    -          grid.setOptions({forceFitColumns:true});
    -          grid.autosizeColumns();
    -        } else {
    -          grid.setOptions({forceFitColumns:false});
    -        }
    -        options.state.set({fitColumns:checked});
    -        return;
    -      }
    -
    -      if (($(e.target).is('li') && !$(e.target).hasClass('divider')) ||
    -            $(e.target).is('input')) {
    -        if ($(e.target).is('li')){
    -            checkbox = $(e.target).find('input').first();
    -            checkbox.attr('checked',!checkbox.is(':checked'));
    -        }
    -        var visibleColumns = [];
    -        var hiddenColumnsIds = [];
    -        $.each(columnCheckboxes, function (i, e) {
    -          if ($(this).is(':checked')) {
    -            visibleColumns.push(columns[i]);
    -          } else {
    -            hiddenColumnsIds.push(columns[i].id);
    -          }
    -        });
    -
    -        if (!visibleColumns.length) {
    -          $(e.target).attr('checked', 'checked');
    -          return;
    -        }
    -
    -        grid.setColumns(visibleColumns);
    -        options.state.set({hiddenColumns:hiddenColumnsIds});
    -      }
    -    }
    -    init();
    -  }
    - -
  • - - -
  • -
    - -
    - -
    -

    Slick.Controls.ColumnPicker

    - -
    - -
      $.extend(true, window, {
    -    Slick: {
    -      Controls: {
    -        ColumnPicker: SlickColumnPicker
    -      }
    -    }
    -  });
    -
    -})(jQuery);
    - -
  • - -
-
- - diff --git a/docs/src/view.timeline.html b/docs/src/view.timeline.html deleted file mode 100644 index 5a7551f8..00000000 --- a/docs/src/view.timeline.html +++ /dev/null @@ -1,446 +0,0 @@ - - - - - view.timeline.js - - - - - -
-
- - - -
    - -
  • -
    -

    view.timeline.js

    -
    -
  • - - - -
  • -
    - -
    - -
    - -
    - -
    /*jshint multistr:true */
    -
    -this.recline = this.recline || {};
    -this.recline.View = this.recline.View || {};
    -
    -(function($, my) {
    -  "use strict";
    - -
  • - - -
  • -
    - -
    - -
    -

    turn off unnecessary logging from VMM Timeline

    - -
    - -
    if (typeof VMM !== 'undefined') {
    -  VMM.debug = false;
    -}
    - -
  • - - -
  • -
    - -
    - -
    -

    Timeline

    -

    Timeline view using http://timeline.verite.co/

    - -
    - -
    my.Timeline = Backbone.View.extend({
    -  template: ' \
    -    <div class="recline-timeline"> \
    -      <div id="vmm-timeline-id"></div> \
    -    </div> \
    -  ',
    - -
  • - - -
  • -
    - -
    - -
    -

    These are the default (case-insensitive) names of field that are used if found. -If not found, the user will need to define these fields on initialization

    - -
    - -
      startFieldNames: ['date','startdate', 'start', 'start-date'],
    -  endFieldNames: ['end','endDate'],
    -  elementId: '#vmm-timeline-id',
    -
    -  initialize: function(options) {
    -    var self = this;
    -    this.timeline = new VMM.Timeline(this.elementId);
    -    this._timelineIsInitialized = false;
    -    this.listenTo(this.model.fields, 'reset', function() {
    -      self._setupTemporalField();
    -    });
    -    this.listenTo(this.model.records, 'all', function() {
    -      self.reloadData();
    -    });
    -    var stateData = _.extend({
    -        startField: null,
    -        endField: null,
    - -
  • - - -
  • -
    - -
    - -
    -

    by default timelinejs (and browsers) will parse ambiguous dates in US format (mm/dd/yyyy) -set to true to interpret dd/dd/dddd as dd/mm/yyyy

    - -
    - -
            nonUSDates: false,
    -        timelineJSOptions: {}
    -      },
    -      options.state
    -    );
    -    this.state = new recline.Model.ObjectState(stateData);
    -    this._setupTemporalField();
    -  },
    -
    -  render: function() {
    -    var tmplData = {};
    -    var htmls = Mustache.render(this.template, tmplData);
    -    this.$el.html(htmls);
    - -
  • - - -
  • -
    - -
    - -
    -

    can only call _initTimeline once view in DOM as Timeline uses $ -internally to look up element

    - -
    - -
        if ($(this.elementId).length > 0) {
    -      this._initTimeline();
    -    }
    -  },
    -
    -  show: function() {
    - -
  • - - -
  • -
    - -
    - -
    -

    only call _initTimeline once view in DOM as Timeline uses $ internally to look up element

    - -
    - -
        if (this._timelineIsInitialized === false) {
    -      this._initTimeline();
    -    }
    -  },
    -
    -  _initTimeline: function() {
    -    var data = this._timelineJSON();
    -    var config = this.state.get("timelineJSOptions");
    -    config.id = this.elementId;
    -    this.timeline.init(config, data);
    -    this._timelineIsInitialized = true
    -  },
    -
    -  reloadData: function() {
    -    if (this._timelineIsInitialized) {
    -      var data = this._timelineJSON();
    -      this.timeline.reload(data);
    -    }
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    Convert record to JSON for timeline

    -

    Designed to be overridden in client apps

    - -
    - -
      convertRecord: function(record, fields) {
    -    return this._convertRecord(record, fields);
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    Internal method to generate a Timeline formatted entry

    - -
    - -
      _convertRecord: function(record, fields) {
    -    var start = this._parseDate(record.get(this.state.get('startField')));
    -    var end = this._parseDate(record.get(this.state.get('endField')));
    -    if (start) {
    -      var tlEntry = {
    -        "startDate": start,
    -        "endDate": end,
    -        "headline": String(record.get('title') || ''),
    -        "text": record.get('description') || record.summary(),
    -        "tag": record.get('tags')
    -      };
    -      return tlEntry;
    -    } else {
    -      return null;
    -    }
    -  },
    -
    -  _timelineJSON: function() {
    -    var self = this;
    -    var out = {
    -      'timeline': {
    -        'type': 'default',
    -        'headline': '',
    -        'date': [
    -        ]
    -      }
    -    };
    -    this.model.records.each(function(record) {
    -      var newEntry = self.convertRecord(record, self.fields);
    -      if (newEntry) {
    -        out.timeline.date.push(newEntry); 
    -      }
    -    });
    - -
  • - - -
  • -
    - -
    - -
    -

    if no entries create a placeholder entry to prevent Timeline crashing with error

    - -
    - -
        if (out.timeline.date.length === 0) {
    -      var tlEntry = {
    -        "startDate": '2000,1,1',
    -        "headline": 'No data to show!'
    -      };
    -      out.timeline.date.push(tlEntry);
    -    }
    -    return out;
    -  },
    - -
  • - - -
  • -
    - -
    - -
    -

    convert dates into a format TimelineJS will handle -TimelineJS does not document this at all so combo of read the code + -trial and error -Summary (AFAICt): -Preferred: [-]yyyy[,mm,dd,hh,mm,ss] -Supported: mm/dd/yyyy

    - -
    - -
      _parseDate: function(date) {
    -    if (!date) {
    -      return null;
    -    }
    -    var out = $.trim(date);
    -    out = out.replace(/(\d)th/g, '$1');
    -    out = out.replace(/(\d)st/g, '$1');
    -    out = $.trim(out);
    -    if (out.match(/\d\d\d\d-\d\d-\d\d(T.*)?/)) {
    -      out = out.replace(/-/g, ',').replace('T', ',').replace(':',',');
    -    }
    -    if (out.match(/\d\d-\d\d-\d\d.*/)) {
    -      out = out.replace(/-/g, '/');
    -    }
    -    if (this.state.get('nonUSDates')) {
    -      var parts = out.match(/(\d\d)\/(\d\d)\/(\d\d.*)/);
    -      if (parts) {
    -        out = [parts[2], parts[1], parts[3]].join('/');
    -      }
    -    }
    -    return out;
    -  },
    -
    -  _setupTemporalField: function() {
    -    this.state.set({
    -      startField: this._checkField(this.startFieldNames),
    -      endField: this._checkField(this.endFieldNames)
    -    });
    -  },
    -
    -  _checkField: function(possibleFieldNames) {
    -    var modelFieldNames = this.model.fields.pluck('id');
    -    for (var i = 0; i < possibleFieldNames.length; i++){
    -      for (var j = 0; j < modelFieldNames.length; j++){
    -        if (modelFieldNames[j].toLowerCase() == possibleFieldNames[i].toLowerCase())
    -          return modelFieldNames[j];
    -      }
    -    }
    -    return null;
    -  }
    -});
    -
    -})(jQuery, recline.View);
    - -
  • - -
-
- - diff --git a/docs/src/widget.facetviewer.html b/docs/src/widget.facetviewer.html deleted file mode 100644 index d2523dc3..00000000 --- a/docs/src/widget.facetviewer.html +++ /dev/null @@ -1,259 +0,0 @@ - - - - - widget.facetviewer.js - - - - - -
-
- - - -
    - -
  • -
    -

    widget.facetviewer.js

    -
    -
  • - - - -
  • -
    - -
    - -
    - -
    - -
    /*jshint multistr:true */
    -
    -this.recline = this.recline || {};
    -this.recline.View = this.recline.View || {};
    -
    -(function($, my) {
    -  "use strict";
    - -
  • - - -
  • -
    - -
    - -
    -

    FacetViewer

    -

    Widget for displaying facets

    -

    Usage:

    -
     var viewer = new FacetViewer({
    -   model: dataset
    - });
    -
    -
    - -
    my.FacetViewer = Backbone.View.extend({
    -  className: 'recline-facet-viewer', 
    -  template: ' \
    -    <div class="facets"> \
    -      {{#facets}} \
    -      <div class="facet-summary" data-facet="{{id}}"> \
    -        <h3> \
    -          {{id}} \
    -        </h3> \
    -        <ul class="facet-items"> \
    -        {{#terms}} \
    -          <li><a class="facet-choice js-facet-filter" data-value="{{term}}" href="#{{term}}">{{term}} ({{count}})</a></li> \
    -        {{/terms}} \
    -        {{#entries}} \
    -          <li><a class="facet-choice js-facet-filter" data-value="{{time}}">{{term}} ({{count}})</a></li> \
    -        {{/entries}} \
    -        </ul> \
    -      </div> \
    -      {{/facets}} \
    -    </div> \
    -  ',
    -
    -  events: {
    -    'click .js-facet-filter': 'onFacetFilter'
    -  },
    -  initialize: function(model) {
    -    _.bindAll(this, 'render');
    -    this.listenTo(this.model.facets, 'all', this.render);
    -    this.listenTo(this.model.fields, 'all', this.render);
    -    this.render();
    -  },
    -  render: function() {
    -    var tmplData = {
    -      fields: this.model.fields.toJSON()
    -    };
    -    tmplData.facets = _.map(this.model.facets.toJSON(), function(facet) {
    -      if (facet._type === 'date_histogram') {
    -        facet.entries = _.map(facet.entries, function(entry) {
    -          entry.term = new Date(entry.time).toDateString();
    -          return entry;
    -        });
    -      }
    -      return facet;
    -    });
    -    var templated = Mustache.render(this.template, tmplData);
    -    this.$el.html(templated);
    - -
  • - - -
  • -
    - -
    - -
    -

    are there actually any facets to show?

    - -
    - -
        if (this.model.facets.length > 0) {
    -      this.$el.show();
    -    } else {
    -      this.$el.hide();
    -    }
    -  },
    -  onHide: function(e) {
    -    e.preventDefault();
    -    this.$el.hide();
    -  },
    -  onFacetFilter: function(e) {
    -    e.preventDefault();
    -    var $target= $(e.target);
    -    var fieldId = $target.closest('.facet-summary').attr('data-facet');
    -    var value = $target.attr('data-value');
    -    this.model.queryState.addFilter({type: 'term', field: fieldId, term: value});
    - -
  • - - -
  • -
    - -
    - -
    -

    have to trigger explicitly for some reason

    - -
    - -
        this.model.query();
    -  }
    -});
    -
    -
    -})(jQuery, recline.View);
    - -
  • - -
-
- - diff --git a/docs/src/widget.fields.html b/docs/src/widget.fields.html deleted file mode 100644 index 07b34852..00000000 --- a/docs/src/widget.fields.html +++ /dev/null @@ -1,299 +0,0 @@ - - - - - widget.fields.js - - - - - -
-
- - - -
    - -
  • -
    -

    widget.fields.js

    -
    -
  • - - - -
  • -
    - -
    - -
    - -
    - -
    /*jshint multistr:true */
    - -
  • - - -
  • -
    - -
    - -
    -

    Field Info

    -

    For each field

    -

    Id / Label / type / format

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    Editor — to change type (and possibly format) -Editor for show/hide …

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    Summaries of fields

    -

    Top values / number empty -If number: max, min average …

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    Box to boot transform editor …

    - -
    - -
    -this.recline = this.recline || {};
    -this.recline.View = this.recline.View || {};
    -
    -(function($, my) {
    -  "use strict";
    -  
    -my.Fields = Backbone.View.extend({
    -  className: 'recline-fields-view', 
    -  template: ' \
    -    <div class="panel-group fields-list well"> \
    -    <h3>Fields <a href="#" class="js-show-hide">+</a></h3> \
    -    {{#fields}} \
    -      <div class="panel panel-default field"> \
    -        <div class="panel-heading"> \
    -          <i class="glyphicon glyphicon-file"></i> \
    -          <h4> \
    -            {{label}} \
    -            <small> \
    -              {{type}} \
    -              <a class="accordion-toggle" data-toggle="collapse" href="#collapse{{id}}"> &raquo; </a> \
    -            </small> \
    -          </h4> \
    -        </div> \
    -        <div id="collapse{{id}}" class="panel-collapse collapse"> \
    -          <div class="panel-body"> \
    -            {{#facets}} \
    -            <div class="facet-summary" data-facet="{{id}}"> \
    -              <ul class="facet-items"> \
    -              {{#terms}} \
    -                <li class="facet-item"><span class="term">{{term}}</span> <span class="count">[{{count}}]</span></li> \
    -              {{/terms}} \
    -              </ul> \
    -            </div> \
    -            {{/facets}} \
    -            <div class="clear"></div> \
    -          </div> \
    -        </div> \
    -      </div> \
    -    {{/fields}} \
    -    </div> \
    -  ',
    -
    -  initialize: function(model) {
    -    var self = this;
    -    _.bindAll(this, 'render');
    - -
  • - - -
  • -
    - -
    - -
    -

    TODO: this is quite restrictive in terms of when it is re-run -e.g. a change in type will not trigger a re-run atm. -being more liberal (e.g. binding to all) can lead to being called a lot (e.g. for change:width)

    - -
    - -
        this.listenTo(this.model.fields, 'reset', function(action) {
    -      self.model.fields.each(function(field) {
    -        field.facets.unbind('all', self.render);
    -        field.facets.bind('all', self.render);
    -      });
    - -
  • - - -
  • -
    - -
    - -
    -

    fields can get reset or changed in which case we need to recalculate

    - -
    - -
          self.model.getFieldsSummary();
    -      self.render();
    -    });
    -    this.$el.find('.collapse').collapse();
    -    this.render();
    -  },
    -  render: function() {
    -    var self = this;
    -    var tmplData = {
    -      fields: []
    -    };
    -    this.model.fields.each(function(field) {
    -      var out = field.toJSON();
    -      out.facets = field.facets.toJSON();
    -      tmplData.fields.push(out);
    -    });
    -    var templated = Mustache.render(this.template, tmplData);
    -    this.$el.html(templated);
    -  }
    -});
    -
    -})(jQuery, recline.View);
    - -
  • - -
-
- - diff --git a/docs/src/widget.filtereditor.html b/docs/src/widget.filtereditor.html deleted file mode 100644 index a761e3d7..00000000 --- a/docs/src/widget.filtereditor.html +++ /dev/null @@ -1,330 +0,0 @@ - - - - - widget.filtereditor.js - - - - - -
-
- - - -
    - -
  • -
    -

    widget.filtereditor.js

    -
    -
  • - - - -
  • -
    - -
    - -
    - -
    - -
    /*jshint multistr:true */
    -
    -this.recline = this.recline || {};
    -this.recline.View = this.recline.View || {};
    -
    -(function($, my) {
    -  "use strict";
    -
    -my.FilterEditor = Backbone.View.extend({
    -  className: 'recline-filter-editor well', 
    -  template: ' \
    -    <div class="filters"> \
    -      <h3>Filters</h3> \
    -      <a href="#" class="js-add-filter">Add filter</a> \
    -      <form class="form-stacked js-add" style="display: none;"> \
    -        <div class="form-group"> \
    -          <label>Field</label> \
    -          <select class="fields form-control"> \
    -            {{#fields}} \
    -            <option value="{{id}}">{{label}}</option> \
    -            {{/fields}} \
    -          </select> \
    -        </div> \
    -        <div class="form-group"> \
    -          <label>Filter type</label> \
    -          <select class="filterType form-control"> \
    -            <option value="term">Value</option> \
    -            <option value="range">Range</option> \
    -            <option value="geo_distance">Geo distance</option> \
    -          </select> \
    -        </div> \
    -        <button type="submit" class="btn btn-default">Add</button> \
    -      </form> \
    -      <form class="form-stacked js-edit"> \
    -        {{#filters}} \
    -          {{{filterRender}}} \
    -        {{/filters}} \
    -        {{#filters.length}} \
    -        <button type="submit" class="btn btn-default">Update</button> \
    -        {{/filters.length}} \
    -      </form> \
    -    </div> \
    -  ',
    -  filterTemplates: {
    -    term: ' \
    -      <div class="filter-{{type}} filter"> \
    -        <fieldset> \
    -          <legend> \
    -            {{field}} <small>{{type}}</small> \
    -            <a class="js-remove-filter" href="#" title="Remove this filter" data-filter-id="{{id}}">&times;</a> \
    -          </legend> \
    -          <input class="input-sm" type="text" value="{{term}}" name="term" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
    -        </fieldset> \
    -      </div> \
    -    ',
    -    range: ' \
    -      <div class="filter-{{type}} filter"> \
    -        <fieldset> \
    -          <legend> \
    -            {{field}} <small>{{type}}</small> \
    -            <a class="js-remove-filter" href="#" title="Remove this filter" data-filter-id="{{id}}">&times;</a> \
    -          </legend> \
    -          <div class="form-group"> \
    -            <label class="control-label" for="">From</label> \
    -            <input class="input-sm" type="text" value="{{from}}" name="from" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
    -          </div> \
    -          <div class="form-group"> \
    -            <label class="control-label" for="">To</label> \
    -            <input class="input-sm" type="text" value="{{to}}" name="to" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
    -          </div> \
    -        </fieldset> \
    -      </div> \
    -    ',
    -    geo_distance: ' \
    -      <div class="filter-{{type}} filter"> \
    -        <fieldset> \
    -          <legend> \
    -            {{field}} <small>{{type}}</small> \
    -            <a class="js-remove-filter" href="#" title="Remove this filter" data-filter-id="{{id}}">&times;</a> \
    -          </legend> \
    -          <div class="form-group"> \
    -            <label class="control-label" for="">Longitude</label> \
    -            <input class="input-sm" type="text" value="{{point.lon}}" name="lon" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
    -          </div> \
    -          <div class="form-group"> \
    -            <label class="control-label" for="">Latitude</label> \
    -            <input class="input-sm" type="text" value="{{point.lat}}" name="lat" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
    -          </div> \
    -          <div class="form-group"> \
    -            <label class="control-label" for="">Distance (km)</label> \
    -            <input class="input-sm" type="text" value="{{distance}}" name="distance" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
    -          </div> \
    -        </fieldset> \
    -      </div> \
    -    '
    -  },
    -  events: {
    -    'click .js-remove-filter': 'onRemoveFilter',
    -    'click .js-add-filter': 'onAddFilterShow',
    -    'submit form.js-edit': 'onTermFiltersUpdate',
    -    'submit form.js-add': 'onAddFilter'
    -  },
    -  initialize: function() {
    -    _.bindAll(this, 'render');
    -    this.listenTo(this.model.fields, 'all', this.render);
    -    this.listenTo(this.model.queryState, 'change change:filters:new-blank', this.render);
    -    this.render();
    -  },
    -  render: function() {
    -    var self = this;
    -    var tmplData = $.extend(true, {}, this.model.queryState.toJSON());
    - -
  • - - -
  • -
    - -
    - -
    -

    we will use idx in list as there id …

    - -
    - -
        tmplData.filters = _.map(tmplData.filters, function(filter, idx) {
    -      filter.id = idx;
    -      return filter;
    -    });
    -    tmplData.fields = this.model.fields.toJSON();
    -    tmplData.filterRender = function() {
    -      return Mustache.render(self.filterTemplates[this.type], this);
    -    };
    -    var out = Mustache.render(this.template, tmplData);
    -    this.$el.html(out);
    -  },
    -  onAddFilterShow: function(e) {
    -    e.preventDefault();
    -    var $target = $(e.target);
    -    $target.hide();
    -    this.$el.find('form.js-add').show();
    -  },
    -  onAddFilter: function(e) {
    -    e.preventDefault();
    -    var $target = $(e.target);
    -    $target.hide();
    -    var filterType = $target.find('select.filterType').val();
    -    var field      = $target.find('select.fields').val();
    -    this.model.queryState.addFilter({type: filterType, field: field});
    -  },
    -  onRemoveFilter: function(e) {
    -    e.preventDefault();
    -    var $target = $(e.target);
    -    var filterId = $target.attr('data-filter-id');
    -    this.model.queryState.removeFilter(filterId);
    -  },
    -  onTermFiltersUpdate: function(e) {
    -   var self = this;
    -    e.preventDefault();
    -    var filters = self.model.queryState.get('filters');
    -    var $form = $(e.target);
    -    _.each($form.find('input'), function(input) {
    -      var $input = $(input);
    -      var filterType  = $input.attr('data-filter-type');
    -      var fieldId     = $input.attr('data-filter-field');
    -      var filterIndex = parseInt($input.attr('data-filter-id'), 10);
    -      var name        = $input.attr('name');
    -      var value       = $input.val();
    -
    -      switch (filterType) {
    -        case 'term':
    -          filters[filterIndex].term = value;
    -          break;
    -        case 'range':
    -          filters[filterIndex][name] = value;
    -          break;
    -        case 'geo_distance':
    -          if(name === 'distance') {
    -            filters[filterIndex].distance = parseFloat(value);
    -          }
    -          else {
    -            filters[filterIndex].point[name] = parseFloat(value);
    -          }
    -          break;
    -      }
    -    });
    -    self.model.queryState.set({filters: filters, from: 0});
    -    self.model.queryState.trigger('change');
    -  }
    -});
    -
    -
    -})(jQuery, recline.View);
    - -
  • - -
-
- - diff --git a/docs/src/widget.pager.html b/docs/src/widget.pager.html deleted file mode 100644 index e223a41f..00000000 --- a/docs/src/widget.pager.html +++ /dev/null @@ -1,223 +0,0 @@ - - - - - widget.pager.js - - - - - -
-
- - - -
    - -
  • -
    -

    widget.pager.js

    -
    -
  • - - - -
  • -
    - -
    - -
    - -
    - -
    /*jshint multistr:true */
    -
    -this.recline = this.recline || {};
    -this.recline.View = this.recline.View || {};
    -
    -(function($, my) {
    -  "use strict";
    -
    -my.Pager = Backbone.View.extend({
    -  className: 'recline-pager', 
    -  template: ' \
    -    <div class="pagination"> \
    -      <ul class="pagination"> \
    -        <li class="prev action-pagination-update"><a href="" class="btn btn-default">&laquo;</a></li> \
    -        <li class="page-range"><a><label for="from">From</label><input id="from" name="from" type="text" value="{{from}}" /> &ndash; <label for="to">To</label><input id="to" name="to" type="text" value="{{to}}" /> </a></li> \
    -        <li class="next action-pagination-update"><a href="" class="btn btn-default">&raquo;</a></li> \
    -      </ul> \
    -    </div> \
    -  ',
    -
    -  events: {
    -    'click .action-pagination-update': 'onPaginationUpdate',
    -    'change input': 'onFormSubmit'
    -  },
    -
    -  initialize: function() {
    -    _.bindAll(this, 'render');
    -    this.listenTo(this.model.queryState, 'change', this.render);
    -    this.render();
    -  },
    -  onFormSubmit: function(e) {
    -    e.preventDefault();
    - -
  • - - -
  • -
    - -
    - -
    -

    filter is 0-based; form is 1-based

    - -
    - -
        var formFrom = parseInt(this.$el.find('input[name="from"]').val())-1; 
    -    var formTo = parseInt(this.$el.find('input[name="to"]').val())-1; 
    -    var maxRecord = this.model.recordCount-1;
    -    if (this.model.queryState.get('from') != formFrom) { // changed from; update from
    -      this.model.queryState.set({from: Math.min(maxRecord, Math.max(formFrom, 0))});
    -    } else if (this.model.queryState.get('to') != formTo) { // change to; update size
    -      var to = Math.min(maxRecord, Math.max(formTo, 0));
    -      this.model.queryState.set({size: Math.min(maxRecord+1, Math.max(to-formFrom+1, 1))});
    -    }
    -  },
    -  onPaginationUpdate: function(e) {
    -    e.preventDefault();
    -    var $el = $(e.target);
    -    var newFrom = 0;
    -    var currFrom = this.model.queryState.get('from');
    -    var size = this.model.queryState.get('size');
    -    var updateQuery = false;
    -    if ($el.parent().hasClass('prev')) {
    -      newFrom = Math.max(currFrom - Math.max(0, size), 0);
    -      updateQuery = newFrom != currFrom;
    -    } else {
    -      newFrom = Math.max(currFrom + size, 0);
    -      updateQuery = (newFrom < this.model.recordCount);
    -    }
    -    if (updateQuery) {
    -      this.model.queryState.set({from: newFrom});
    -    }
    -  },
    -  render: function() {
    -    var tmplData = this.model.toJSON();
    -    var from = parseInt(this.model.queryState.get('from'));
    -    tmplData.from = from+1;
    -    tmplData.to = Math.min(from+this.model.queryState.get('size'), this.model.recordCount);
    -    var templated = Mustache.render(this.template, tmplData);
    -    this.$el.html(templated);
    -    return this;
    -  }
    -});
    -
    -})(jQuery, recline.View);
    - -
  • - -
-
- - diff --git a/docs/src/widget.queryeditor.html b/docs/src/widget.queryeditor.html deleted file mode 100644 index 3ce92817..00000000 --- a/docs/src/widget.queryeditor.html +++ /dev/null @@ -1,184 +0,0 @@ - - - - - widget.queryeditor.js - - - - - -
-
- - - -
    - -
  • -
    -

    widget.queryeditor.js

    -
    -
  • - - - -
  • -
    - -
    - -
    - -
    - -
    /*jshint multistr:true */
    -
    -this.recline = this.recline || {};
    -this.recline.View = this.recline.View || {};
    -
    -(function($, my) {
    -  "use strict";
    -
    -my.QueryEditor = Backbone.View.extend({
    -  className: 'recline-query-editor', 
    -  template: ' \
    -    <form action="" method="GET" class="form-inline" role="form"> \
    -      <div class="form-group"> \
    -        <div class="input-group text-query"> \
    -          <div class="input-group-addon"> \
    -            <i class="glyphicon glyphicon-search"></i> \
    -          </div> \
    -          <label for="q">Search</label> \
    -          <input class="form-control search-query" type="text" id="q" name="q" value="{{q}}" placeholder="Search data ..."> \
    -        </div> \
    -      </div> \
    -      <button type="submit" class="btn btn-default">Go &raquo;</button> \
    -    </form> \
    -  ',
    -
    -  events: {
    -    'submit form': 'onFormSubmit'
    -  },
    -
    -  initialize: function() {
    -    _.bindAll(this, 'render');
    -    this.listenTo(this.model, 'change', this.render);
    -    this.render();
    -  },
    -  onFormSubmit: function(e) {
    -    e.preventDefault();
    -    var query = this.$el.find('.search-query').val();
    -    this.model.set({q: query});
    -  },
    -  render: function() {
    -    var tmplData = this.model.toJSON();
    -    var templated = Mustache.render(this.template, tmplData);
    -    this.$el.html(templated);
    -  }
    -});
    -
    -})(jQuery, recline.View);
    - -
  • - -
-
- - diff --git a/docs/src/widget.valuefilter.html b/docs/src/widget.valuefilter.html deleted file mode 100644 index 1f72bc0f..00000000 --- a/docs/src/widget.valuefilter.html +++ /dev/null @@ -1,264 +0,0 @@ - - - - - widget.valuefilter.js - - - - - -
-
- - - -
    - -
  • -
    -

    widget.valuefilter.js

    -
    -
  • - - - -
  • -
    - -
    - -
    - -
    - -
    /*jshint multistr:true */
    -
    -this.recline = this.recline || {};
    -this.recline.View = this.recline.View || {};
    -
    -(function($, my) {
    -  "use strict";
    -
    -my.ValueFilter = Backbone.View.extend({
    -  className: 'recline-filter-editor well', 
    -  template: ' \
    -    <div class="filters"> \
    -      <h3>Filters</h3> \
    -      <button class="btn js-add-filter add-filter">Add filter</button> \
    -      <form class="form-stacked js-add" style="display: none;"> \
    -        <fieldset> \
    -          <label>Field</label> \
    -          <select class="fields form-control"> \
    -            {{#fields}} \
    -            <option value="{{id}}">{{label}}</option> \
    -            {{/fields}} \
    -          </select> \
    -          <button type="submit" class="btn">Add</button> \
    -        </fieldset> \
    -      </form> \
    -      <form class="form-stacked js-edit"> \
    -        {{#filters}} \
    -          {{{filterRender}}} \
    -        {{/filters}} \
    -        {{#filters.length}} \
    -        <button type="submit" class="btn update-filter">Update</button> \
    -        {{/filters.length}} \
    -      </form> \
    -    </div> \
    -  ',
    -  filterTemplates: {
    -    term: ' \
    -      <div class="filter-{{type}} filter"> \
    -        <fieldset> \
    -          {{field}} \
    -          <a class="js-remove-filter" href="#" title="Remove this filter" data-filter-id="{{id}}">&times;</a> \
    -          <input type="text" value="{{term}}" name="term" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
    -        </fieldset> \
    -      </div> \
    -    '
    -  },
    -  events: {
    -    'click .js-remove-filter': 'onRemoveFilter',
    -    'click .js-add-filter': 'onAddFilterShow',
    -    'submit form.js-edit': 'onTermFiltersUpdate',
    -    'submit form.js-add': 'onAddFilter'
    -  },
    -  initialize: function() {
    -    _.bindAll(this, 'render');
    -    this.listenTo(this.model.fields, 'all', this.render);
    -    this.listenTo(this.model.queryState, 'change change:filters:new-blank', this.render);
    -    this.render();
    -  },
    -  render: function() {
    -    var self = this;
    -    var tmplData = $.extend(true, {}, this.model.queryState.toJSON());
    - -
  • - - -
  • -
    - -
    - -
    -

    we will use idx in list as the id …

    - -
    - -
        tmplData.filters = _.map(tmplData.filters, function(filter, idx) {
    -      filter.id = idx;
    -      return filter;
    -    });
    -    tmplData.fields = this.model.fields.toJSON();
    -    tmplData.filterRender = function() {
    -      return Mustache.render(self.filterTemplates.term, this);
    -    };
    -    var out = Mustache.render(this.template, tmplData);
    -    this.$el.html(out);
    -  },
    -  updateFilter: function(input) {
    -    var self = this;
    -    var filters = self.model.queryState.get('filters');
    -    var $input = $(input);
    -    var filterIndex = parseInt($input.attr('data-filter-id'), 10);
    -    var value = $input.val();
    -    filters[filterIndex].term = value;
    -  },
    -  onAddFilterShow: function(e) {
    -    e.preventDefault();
    -    var $target = $(e.target);
    -    $target.hide();
    -    this.$el.find('form.js-add').show();
    -  },
    -  onAddFilter: function(e) {
    -    e.preventDefault();
    -    var $target = $(e.target);
    -    $target.hide();
    -    var field = $target.find('select.fields').val();
    -    this.model.queryState.addFilter({type: 'term', field: field});
    -  },
    -  onRemoveFilter: function(e) {
    -    e.preventDefault();
    -    var $target = $(e.target);
    -    var filterId = $target.attr('data-filter-id');
    -    this.model.queryState.removeFilter(filterId);
    -  },
    -  onTermFiltersUpdate: function(e) {
    -    var self = this;
    -    e.preventDefault();
    -    var filters = self.model.queryState.get('filters');
    -    var $form = $(e.target);
    -    _.each($form.find('input'), function(input) {
    -      self.updateFilter(input);
    -    });
    -    self.model.queryState.set({filters: filters, from: 0});
    -    self.model.queryState.trigger('change');
    -  }
    -});
    -
    -})(jQuery, recline.View);
    - -
  • - -
-
- - diff --git a/docs/tutorial-backends.markdown b/docs/tutorial-backends.markdown deleted file mode 100644 index f64abd3d..00000000 --- a/docs/tutorial-backends.markdown +++ /dev/null @@ -1,205 +0,0 @@ ---- -layout: container -title: Loading data from different sources using Backends - Tutorial -recline-deps: true -root: ../ ---- - - - -## Overview - -Backends connect Recline Datasets to data from a specific 'Backend' data -source. - -They provide methods for loading and saving Datasets and individuals -Documents as well as for bulk loading via a query API and doing bulk transforms -on the backend. - -Backends come in 2 flavours: - -* Loader backends - only implement fetch method. The data is then cached in a - Memory.Store on the Dataset and interacted with there. This is best for - sources which just allow you to load data or where you want to load the data - once and work with it locally. -* Store backends - these support fetch, query and, if write-enabled, save. - These are suited to cases where the source datastore contains a lot of data - (infeasible to load locally - for examples a million rows) or where the - backend has, for example, query capabilities you want to take advantage of. - -### Instantiation and Use - -You can use a backend directly e.g. - -{% highlight javascript %} -var backend = recline.Backend.ElasticSearch.fetch({url: ...}); -{% endhighlight %} - -But more usually the backend will be created or loaded for you by Recline and -all you need is provide the identifier for that Backend e.g. - -{% highlight javascript %} -var dataset = recline.Model.Dataset({ - backend: 'backend-identifier' -}); -{% endhighlight %} - -
-

Backend identifiers -How do you know the backend identifier for a given Backend? It's just the name -of the 'class' in recline.Backend module (but case-insensitive). E.g. -recline.Backend.ElasticSearch can be identified as 'ElasticSearch' or -'elasticsearch'.

-
- -## What Backends are available from Recline? - -{% include backend-list.html %} - -**Backend you'd like to see not available?** It's easy to write your own -– see the Backend reference docs for details -of the required API. - -## Preparing your app - -This is as per the [quickstart](tutorial-views.html) but the set of files is -much more limited if you are just using a Backend. Specifically: - -{% highlight html %} - - - - - - - - - - -{% endhighlight %} - - -## Loading Data from Google Docs - -We will be using the [following Google -Doc](https://docs.google.com/spreadsheet/ccc?key=0Aon3JiuouxLUdGZPaUZsMjBxeGhfOWRlWm85MmV0UUE#gid=0). -For Recline to be able to access a Google Spreadsheet it **must** have been -'Published to the Web' (enabled via File -> Publish to the Web menu). - -{% highlight javascript %} -// include the Recline backend for Google Docs - -{% include example-backends-gdocs.js %} -{% endhighlight %} - -### Result - -
 
- - - - - - -## Loading Data from ElasticSearch - -Recline supports ElasticSearch as a full read/write/query backend via the -[ElasticSearch.js library][esjs]. See the library for examples. - -[esjs]: https://github.com/okfn/elasticsearch.js - - -## Loading data from CSV files - -For loading data from CSV files there are 3 cases: - -1. CSV is online but on same domain or supporting CORS (S3 and Google Storage support CORS!) -- we can then load using AJAX (as no problems with same origin policy) -2. CSV is on local disk -- if your browser supports HTML5 File API we can load the CSV file off disk -3. CSV is online but not on same domain -- use DataProxy (see below) - -In all cases we'll need to have loaded the Recline CSV backend (for your own -app you'll probably want this locally): - -{% highlight html %} - -{% endhighlight %} - -### Local online CSV file - -Let's start with first case: loading a "local" online CSV file. We'll be using this [example file]({{page.root}}/demos/data/sample.csv). - -{% highlight javascript %} -{% include example-backends-online-csv.js %} -{% endhighlight %} - -#### Result - -
 
- - - -### CSV file on disk - -This requires your browser to support the HTML5 file API. Suppose we have a file input like: - - - -Then we can load the file into a Recline Dataset as follows: - -{% highlight javascript %} -{% include example-backends-csv-disk.js %} -{% endhighlight %} - -#### Try it out! - -Try it out by clicking on the file input above, selecting a CSV file and seeing what happens. - -
 
- - - - -## Loading data from CSV and Excel files online using DataProxy - -The [DataProxy](http://github.com/okfn/dataproxy) is a web-service run by the Open Knowledge Foundation that converts CSV and Excel files to JSON. It has a convenient JSON-p-able API which means we can use it to load data from online CSV and Excel into Recline Datasets. - -Recline ships with a simple DataProxy "backend" that takes care of fetching data from the DataProxy source. - -The main limitation of the DataProxy is that it can only handle Excel files up to a certain size (5mb) and that as we must use JSONP to access it error information can be limited. - -{% highlight javascript %} -{% include example-backends-dataproxy.js %} -{% endhighlight %} - -### Result - -
 
- - - -### Customizing the timeout - -As we must use JSONP in this backend we have the problem that if DataProxy errors (e.g. 500) this won't be picked up. To deal with this and prevent the case where the request never finishes We have a timeout on the request after which the Backend sends back an error stating that request timed out. - -You can customize the length of this timeout by setting the following constant: - -{% highlight javascript %} -// Customize the timeout (in milliseconds) - default is 5000 -recline.Backend.DataProxy.timeout = 10000; -{% endhighlight %} - diff --git a/docs/tutorial-basics-events.markdown b/docs/tutorial-basics-events.markdown deleted file mode 100644 index 55b40526..00000000 --- a/docs/tutorial-basics-events.markdown +++ /dev/null @@ -1,55 +0,0 @@ ---- -layout: container -title: Tutorial - Dataset Basics - Events -recline-deps: true -root: ../ ---- - - - - -## Preparations - -See first Dataset basics tutorial for getting setup and initializing Dataset. - - - -## Listening for Events - -Often you'll want to listen to events on a Dataset and its associated objects. This is easy to do thanks to the use of Backbone model objects which have a [standard set of events](http://backbonejs.org/#FAQ-events). - -Here's an example to illustrate: - -{% highlight javascript %} -{% include tutorial-basics-ex-events.js %} -{% endhighlight %} - -
 
- - - -Here's a summary of the main objects and their events: - -* Dataset: - - * Standard Backbone events for changes to attributes (note that this will **not** include changes to records) - * *query:start / query:end* called at start and completion of a query - -* Dataset.records: Backbone.Collection of "current" records (i.e. those resulting from latest query) with standard Backbone set of events: *add, reset, remove* - -* Dataset.queryState: queryState is a Query object with standard Backbone Model set of events - -* Dataset.fields: Backbone Collection of Fields. - diff --git a/docs/tutorial-basics-query.markdown b/docs/tutorial-basics-query.markdown deleted file mode 100644 index f44c8522..00000000 --- a/docs/tutorial-basics-query.markdown +++ /dev/null @@ -1,70 +0,0 @@ ---- -layout: container -title: Tutorial - Dataset Basics -recline-deps: true -root: ../ ---- - - - -## Preparations - -See first Dataset basics tutorial for getting setup and initializing a Dataset. - - - -## Querying - -The basic thing we want to do with Datasets is query and filter them. This is -very easy to do: - -{% highlight javascript %} -{% include tutorial-basics-ex-2.js %} -{% endhighlight %} - -This results in the following. Note how recordCount is now 3 (the total number -of records matched by the query) but that records only contains 2 records as we -restricted number of returned records using the size attribute. - -
 
- - - -## Filtering - -A simple unstructured query like the one provided above searches all fieldsfor the value provided. - -Often you want to "filter" results more precisely, for example by specifying a specific value in a specific field. To do this we use "filters". - -{% highlight javascript %} -var query = new recline.Model.Query(); -query.addFilter({type: 'term', field: 2}); -dataset.query(query); -{% endhighlight %} - -## QueryState - -The last run query is stored as a Query -instance in the `queryState` attribute of the Dataset object. Modifying -`queryState` will also resulting in a query being run. This is useful when -building views that want to display or manage the query state (see, for -example, Query Editor or Filter Editor widgets). - -## Full Details of the Query Language - -Full details of the query structure and its options -can be found in the reference documentation. - diff --git a/docs/tutorial-basics.markdown b/docs/tutorial-basics.markdown deleted file mode 100644 index a1e548d9..00000000 --- a/docs/tutorial-basics.markdown +++ /dev/null @@ -1,152 +0,0 @@ ---- -layout: container -title: Tutorial - Dataset Basics -recline-deps: true -root: ../ ---- - - - -## Preparations - -1. [Download ReclineJS]({{page.root}}download.html) and relevant dependencies. - -2. Include the core dependencies for the Dataset model - - {% highlight html %} - - - - -{% endhighlight %} - -## Creating a Dataset - -Here's the example data We are going to work with: - -{% highlight javascript %} -{% include data.js %} -{% endhighlight %} - -In this data we have 6 records / rows. Each record is a javascript object -containing keys and values (values can be 'simple' e.g. a string or float or arrays or hashes - e.g. the geo value here). - -
In this tutorial we are creating datasets with -"local" JS data. However, Recline has a variety of Backends that make it easy -to create Datasets from a variety of online sources and local sources including -Google Spreadsheets, CSV files, etc. See the Backend tutorial for more.
- -We can now create a recline Dataset object from this raw data: - -{% highlight javascript %} -var dataset = new recline.Model.Dataset({ - records: data -}); -{% endhighlight %} - - - -## A Dataset and its Records - -Now that we have created a Dataset, we can use it. - -For example, let's display some information about the Dataset and its records using some of the key Dataset attributes: `recordCount` and `records`. - -{% highlight javascript %} -{% include tutorial-basics-ex-1.js %} -{% endhighlight %} - -Here's the output: - -
 
- - - -### Records - -`Dataset.records` is a Backbone Collection of `Record`s resutling from latest query. This need not (and usually isn't) **all** the records in this Dataset since the latest query need not have matched all records. - -
-Note that on initialization, a Dataset automatically queries for the first 100 records so this is what will usually be available in th records attribute. If you did want all records loaded into records at the start just requery after fetch has completed: - -{% highlight javascript %} -// for the async case need to put inside of done -dataset.fetch().done(function() { - dataset.query({size: dataset.recordCount}); -}); -{% endhighlight %} -
- -As a Backbone Collection it supports all the standard Backbone collection functionality including methods like `each` and `filter`: - -{% highlight javascript %} -dataset.records.each(function(record) { - console.log(record.get('x') * 2); -}); - -var filtered = dataset.records.filter(function(record) { - return (record.get('z') > 4 && record.get('z') < 18); -}); - -// get all the values for a given attribute/field/column -var xvalues = dataset.records.pluck('x'); - -// calls toJSON on all records at once -dataset.records.toJSON(); -{% endhighlight %} - -
Want to know more about Dataset and Records? Check out the reference documentation
- -## Fields - -In addition to Records, a Dataset has Fields stored in the `fields` attribute. Fields provide information about the fields/columns in the Dataset, for example their id (key name in record), label, type etc. - -The Dataset's fields will be automatically computed from records or provided by the backend but they can also be explicitly provided and configured. Let's take a look at the fields on the dataset at present using the following code: - -{% highlight javascript %} -{% include tutorial-basics-ex-fields.js %} -{% endhighlight %} - -Running this results in the following: - -
 
- - - -As can be seen all fields have the default type of 'string'. - -As you may have noticed above the last geo attribute of the dataset just rendered as `[object Object]`. This is because the Dataset is treating that field value as a string. Let's now take a look at the Dataset fields in more detail. - -Let's change the geo field to have type geo\_point and see what affect that has on displaying of the dataset (for good measure we'll also set the label): - -{% highlight javascript %} -{% include tutorial-basics-ex-fields-2.js %} -{% endhighlight %} - -
 
- - - -As can be seen the rendering of the field has changed. This is because the `summary` method uses the Record.getFieldValue function which in turn renders a record field using the Field's renderer function. This function varies depending on the type and can also be customized (see the Field documentation). - diff --git a/docs/tutorial-grids.markdown b/docs/tutorial-grids.markdown deleted file mode 100644 index 20632f5f..00000000 --- a/docs/tutorial-grids.markdown +++ /dev/null @@ -1,52 +0,0 @@ ---- -layout: container -title: Grids - Advanced use of grids in Recline - Tutorial -recline-deps: true -root: ../ ---- - - - -### How much can I do with a simple grid view - -### Benefits of SlickGrid -What does Recline give you out of the box, what does SlickGrid have built in and how to use it (reference to stuff below). - -### Preparing your page - -See the instructions in the [basic views tutorial](tutorial-views.html). - -### Creating a Dataset - -Just like in the main tutorial, here's some example data We are going to work with: - -{% highlight javascript %} -{% include data.js %} -var dataset = new recline.Model.Dataset({ - records: data -}); -{% endhighlight %} - - - -### Accessing SlickGrid features -Show how we can customize SlickGrid grid "normally" - i.e. you can get access to underlying grid and tweak it as you want. - -Suggest we demonstrate using this example: https://github.com/mleibman/SlickGrid/blob/gh-pages/examples/example-plugin-headermenu.html - -Idea here is: we don't want to mimic all that slickgrid can do through recline interface - just let people do it directly themselves ... - - - diff --git a/docs/tutorial-maps.markdown b/docs/tutorial-maps.markdown deleted file mode 100644 index 60ab0a1d..00000000 --- a/docs/tutorial-maps.markdown +++ /dev/null @@ -1,131 +0,0 @@ ---- -layout: container -title: Maps - Customizing Maps in Recline - Tutorial -recline-deps: true -root: ../ ---- - - - -### Preparing your page - -See the instructions in the [basic views tutorial](tutorial-views.html). - -### Creating a Dataset - -Just like in the main tutorial, here's some example data We are going to work with: - -{% highlight javascript %} -{% include data.js %} -var dataset = new recline.Model.Dataset({ - records: data -}); -{% endhighlight %} - - - -### General Pointers - -Check out the Map reference -(source) docs. In particular this has details of the various state options. - -In addition, remember that Recline's map view is just a relatively lightweight -wrapper around Leaflet. This means that pretty much anything you can do with -Leaflet you can do with Recline's map. Specifically a `recline.View.Map` -instance has the following attributes exposed: - - map: the Leaflet map (L.Map) - features: Leaflet GeoJSON layer (L.GeoJSON) containing all the features - -(Recline converts all records in a dataset that yield geospatial info to a -GeoJSON feature and adds it to the features layer). - -### Customizing the infobox - -The default infobox just shows all of the dataset attributes. Usually you'll -want something a bit nicer. All you need to do is override the infobox -function. For example, in our case let's make a nicer title and only show some -data. - -{% highlight javascript %} -{% include tutorial-maps-infobox.js %} -{% endhighlight %} - -
 
- - - -### Customizing the marker - -We're going to show how to replace the default marker with a circular marker. -Even more exciting, we'll show how to have the marker size vary with an -attribute of our data. We do the customization by via over-riding the -pointToLayer function: - -{% highlight javascript %} -{% include tutorial-maps-customize.js %} -{% endhighlight %} - -
 
- - - -### Customing features (which aren't points) - -Leaflet treats points and features differently. To customize features that -aren't point we will need to bind to the feature layers featureparse event. As -the feature layer can get re-rendered you don't do this directly but rather set -the featureparse function on the recline view. For example, for classic popup -behaviour: - -{% highlight javascript %} -view.featureparse = function (e) { - if (e.properties && e.properties.popupContent) { - e.layer.bindPopup(e.properties.popupContent); - } -}; -{% endhighlight %} - - -### Turning on clustering - -You can turn on clustering of markers by setting the cluster option: - - var map = new recline.View.Map({ - model: dataset - state: { - cluster: true; - } - }); - -You could also enable marker clustering only if you have more than a -certain number of markers. Here's an example: - - // var map is recline.View.Map instance - // marker cluster threshold - var threshold = 65; - - // enable clustering if there is a large number of markers - var countAfter = 0; - map.features.eachLayer(function(){countAfter++;}); - if (countAfter > threshold) { - // note the map will auto-redraw when you change state! - map.state.set({cluster: true}); - } - diff --git a/docs/tutorial-multiview.markdown b/docs/tutorial-multiview.markdown deleted file mode 100644 index 68dba87d..00000000 --- a/docs/tutorial-multiview.markdown +++ /dev/null @@ -1,233 +0,0 @@ ---- -layout: container -title: Library - Multiview Tutorial -recline-deps: true -root: ../ ---- - - - -
-The full source code along with all dependencies for the tutorial can be found at this GitHub repository. Do not try to assemble a working example from the code snippets in this page! See it in action via GitHub Pages. - The code is almost identical to that used for the site demo, with the advantage of the code being separated out of the main recline website. You can also see Multiview in action in many CKAN-based data catalogs (resource views) and in the OKFN Labs DataDeck
- -### Multiview - -Multiview, as its name suggests, combines multiple [Recline views](views.html) into one visualization. The different views are synced to one dataset. The [technical documentation for Multiview](src/view.multiview.html) details the nuts and bolts. - -When building a Multiview from scratch, it is advised to start by getting each individual view to work satisfactorily before combining into a Multiview, to aid debugging. - -### Preparing your page - -Before writing any code with Recline, you need to do the following preparation steps on your page: - -* [Download ReclineJS]({{page.root}}download.html) (downloading the master, developer code is recommended as this example is based on that and all dependencies should be available) or the all-in-one demo code. -* Include the Multiview CSS as well as the CSS for each view in the head section of your document, as well as any 3rd party CSS for each view e.g.: - {% highlight html %} - - - - - - - - - - - - - - - {% endhighlight %} - -* Include the relevant Javascript files somewhere on the page (preferably before body close tag). You will need to include any necessary Javascript dependencies for each view as well, e.g.: - {% highlight html %} - - - - - - - - - - - - - - - - - - - - - - - - - - {% endhighlight %} - -### Creating a Dataset - -Here's the function to create an example dataset we are going to work with: - -{% highlight javascript %} -function createDemoDataset() { - var dataset = new recline.Model.Dataset({ - records: [ - {id: 0, date: '2011-01-01', x: 1, y: 2, z: 3, country: 'DE', sometext: 'first', lat:52.56, lon:13.40}, - {id: 1, date: '2011-02-02', x: 2, y: 4, z: 24, country: 'UK', sometext: 'second', lat:54.97, lon:-1.60}, - {id: 2, date: '2011-03-03', x: 3, y: 6, z: 9, country: 'US', sometext: 'third', lat:40.00, lon:-75.5}, - {id: 3, date: '2011-04-04', x: 4, y: 8, z: 6, country: 'UK', sometext: 'fourth', lat:57.27, lon:-6.20}, - {id: 4, date: '2011-05-04', x: 5, y: 10, z: 15, country: 'UK', sometext: 'fifth', lat:51.58, lon:0}, - {id: 5, date: '2011-06-02', x: 6, y: 12, z: 18, country: 'DE', sometext: 'sixth', lat:51.04, lon:7.9} - ], - // let's be really explicit about fields - // Plus take opportunity to set date to be a date field and set some labels - fields: [ - {id: 'id'}, - {id: 'date', type: 'date'}, - {id: 'x', type: 'number'}, - {id: 'y', type: 'number'}, - {id: 'z', type: 'number'}, - {id: 'country', 'label': 'Country'}, - {id: 'sometext', 'label': 'Some text'}, - {id: 'lat'}, - {id: 'lon'} - ] - }); - return dataset; -} -{% endhighlight %} - -In this data we have 6 documents / rows. Each document is a javascript object -containing keys and values (note that all values here are 'simple' but there is -no reason you cannot have objects as values allowing you to nest data. - -### Setting up the Multiview - -To create a Multiview, we first create each view that we want to include, and include these in an array. A function to do everything for SlickGrid, Graph and Map views is shown below: - -{% highlight javascript %} -var createMultiView = function(dataset, state) { - // remove existing multiview if present - var reload = false; - if (window.multiView) { - window.multiView.remove(); - window.multiView = null; - reload = true; - } - - var $el = $('
'); - $el.appendTo(window.explorerDiv); - - // customize the subviews for the MultiView - var views = [ - { - id: 'grid', - label: 'Grid', - view: new recline.View.SlickGrid({ - model: dataset, - state: { - gridOptions: { - editable: true, - // Enable support for row add - enabledAddRow: true, - // Enable support for row delete - enabledDelRow: true, - // Enable support for row ReOrder - enableReOrderRow:true, - autoEdit: false, - enableCellNavigation: true - }, - columnsEditor: [ - { column: 'date', editor: Slick.Editors.Date }, - { column: 'sometext', editor: Slick.Editors.Text } - ] - } - }) - }, - { - id: 'graph', - label: 'Graph', - view: new recline.View.Graph({ - model: dataset - - }) - }, - { - id: 'map', - label: 'Map', - view: new recline.View.Map({ - model: dataset - }) - } - ]; - - var multiView = new recline.View.MultiView({ - model: dataset, - el: $el, - state: state, - views: views - }); - return multiView; -} -{% endhighlight %} - -To tie it all together: -{% highlight javascript %} -jQuery(function($) { - window.multiView = null; - window.explorerDiv = $('.data-explorer-here'); - - // create the demo dataset - var dataset = createDemoDataset(); - // now create the multiview - // this is rather more elaborate than the minimum as we configure the - // MultiView in various ways (see function below) - window.multiview = createMultiView(dataset); - - // last, we'll demonstrate binding to changes in the dataset - // this will print out a summary of each change onto the page in the - // changelog section - dataset.records.bind('all', function(name, obj) { - var $info = $('
'); - $info.html(name + ': ' + JSON.stringify(obj.toJSON())); - $('.changelog').append($info); - $('.changelog').show(); - }); -}); -{% endhighlight %} - -The HTML is very simple: -{% highlight html %} -
- - -
-

Changes

-
- -
-
-
-{% endhighlight %} diff --git a/docs/tutorial-timelines.markdown b/docs/tutorial-timelines.markdown deleted file mode 100644 index d6222e6c..00000000 --- a/docs/tutorial-timelines.markdown +++ /dev/null @@ -1,50 +0,0 @@ ---- -layout: container -title: Timelines - Advanced use of timelines in Recline - Tutorial -recline-deps: true -root: ../ ---- - - - -### Preparing your page - -See the instructions in the [basic views tutorial](tutorial-views.html). - -### Creating a Dataset - -Just like in the main tutorial, here's some example data We are going to work with: - -{% highlight javascript %} -{% include data.js %} -var dataset = new recline.Model.Dataset({ - records: data -}); -{% endhighlight %} - - - -### Underlying library and Recline extensions - - - -### Handling dates - - - -### Customizing the rendering - - - diff --git a/docs/tutorial-views.markdown b/docs/tutorial-views.markdown deleted file mode 100644 index 9e519841..00000000 --- a/docs/tutorial-views.markdown +++ /dev/null @@ -1,316 +0,0 @@ ---- -layout: container -title: Library - Example - Quickstart -recline-deps: true -root: ../ ---- - - - -### Views - -Views display Recline datasets in different ways. This page covers the interesting built-in views. For a full list of views including extensions outside of the Recline.js core, see the [list of currently available views]({{page.root}}docs/views.html#dataset-views-currently-available). - -### Preparing your page - -Before writing any code with Recline, you need to do the following preparation steps on your page: - -* [Download ReclineJS]({{page.root}}download.html) and relevant dependencies. -* Include the relevant CSS in the head section of your document (for view-specific CSS files, see below): - {% highlight html %} - - -{% endhighlight %} - -* Include the relevant Javascript files somewhere on the page (preferably before body close tag; for view-specific Javascript dependencies, see below): - {% highlight html %} - - - - - - - - -{% endhighlight %} - -You're now ready to start working with Recline. - -### Creating a Dataset - -Here's some example data we are going to work with: - -{% highlight javascript %} -{% include data.js %} -{% endhighlight %} - -In this data we have 6 documents / rows. Each document is a javascript object -containing keys and values (note that all values here are 'simple' but there is -no reason you cannot have objects as values allowing you to nest data. - -We can now create a recline Dataset object (and memory backend) from this raw data: - -{% highlight javascript %} -var dataset = new recline.Model.Dataset({ - records: data -}); - -//Depending on the view, it may be important to set the date type -dataset.fields.models[1].attributes.type = 'date'; -{% endhighlight %} - -### Setting up the Grid - -
-The source code along with all dependencies for the grid part of the tutorial can be found at this GitHub repository. See it in action via GitHub Pages. -Although it's not demonstrated here, you can also use the simpler Grid view without SlickGrid. Source code along with all dependencies for that can be found at this GitHub repository. Demo on GitHub Pages. -
- -Let's create a data grid view to display the dataset we have just created. We're going to use the SlickGrid-based grid so we need the following CSS and JS dependencies in addition to those above: - -{% highlight html %} - - - - - - - - - - - - - - - - -{% endhighlight %} - -Now, let's create an HTML element to for the Grid: - -{% highlight html %} -
-{% endhighlight %} - -Now let's set up the Grid: - -{% highlight javascript %} -var $el = $('#mygrid'); -var grid = new recline.View.SlickGrid({ - model: dataset, - el: $el -}); -grid.visible = true; -grid.render(); -{% endhighlight %} - -And hey presto: - -
 
- - - -### Creating a Graph - -
-The source code along with all dependencies for the graph part of the tutorial can be found at this GitHub repository. See it in action via GitHub Pages. -
- -Let's create a graph view to display a line graph for this dataset. - -First, add the additional dependencies for this view. These are the Flot -library and the Recline Flot Graph view: - -{% highlight html %} - - - - - - - - - - - - -{% endhighlight %} - -Next, create a new div for the graph: - -{% highlight html %} -
-{% endhighlight %} - -Now let's create the graph, we will use the same dataset we had earlier, and we will need to set the view 'state' in order to configure the graph with the column to use for the x-axis ("group") and the columns to use for the series to show ("series"). - -
-State: The concept of a state is a common feature of Recline views being an object -which stores information about the state and configuration of a given view. You -can read more about it in the general Views -documentation as well as the documentation of individual views such as the -Graph View. -
- -{% highlight javascript %} -var $el = $('#mygraph'); -var graph = new recline.View.Graph({ - model: dataset, - state: { - graphType: "lines-and-points", - group: "date", - series: ["y", "z"] - } -}); -$el.append(graph.el); -graph.render(); -graph.redraw(); -{% endhighlight %} - -For the axis date formatting to work, it is crucial that the date type is set for that field as shown in the code concerning the dataset above. The result is the following graph: - -
 
- - - -### Creating a Map - -
-The source code along with all dependencies for the map part of the tutorial can be found at this GitHub repository. See it in action via GitHub Pages. -
- -Now, let's create a map of this dataset using the lon/lat information which is -present on these data points. - -First, add the additional dependencies for the map view. These are the Leaflet -library and the Recline Map view: - -{% highlight html %} - - - - - - - - - - - - - -{% endhighlight %} - -Now, create a new div for the map: - -{% highlight html %} -
-{% endhighlight %} - -Now let's create the map, we will use the existing dataset object created -previously: - -{% highlight javascript %} -var $el = $('#mymap'); -var map = new recline.View.Map({ - model: dataset -}); -$el.append(map.el); -map.render(); -{% endhighlight %} - -
 
- - - -### Creating a Timeline - -
-The source code along with all dependencies for the timeline part of the tutorial can be found at this GitHub repository. See it in action via GitHub Pages. -
- -Now, let's create a timeline for this dataset using the date information which is -present on these data points. - -First, add the additional dependencies for the timeline view. The timeline is built on the excellent Verite Timeline widget so that library is the key one for this view: - -{% highlight html %} - - - - - - - -{% endhighlight %} - -Now, create a new div for the map (must have an explicit height for the timeline to render): - -{% highlight html %} - -
-{% endhighlight %} - -Now let's create the timeline, we will use the existing dataset object created -previously: - -{% highlight javascript %} -{% include tutorial-views-timeline.js %} -{% endhighlight %} - - -
-
- - - diff --git a/docs/tutorials.html b/docs/tutorials.html deleted file mode 100644 index 35dae7df..00000000 --- a/docs/tutorials.html +++ /dev/null @@ -1,67 +0,0 @@ ---- -layout: container -title: Tutorials -root: ../ ---- - - - -

Basics – Using the Dataset and its Friends

-
-
-
-
- -
- - -
-
- -

Backends – Loading and Storing Data from Remote Sources

-
- - -

Views – visualize data

-
- diff --git a/docs/views.markdown b/docs/views.markdown deleted file mode 100644 index d29220e5..00000000 --- a/docs/views.markdown +++ /dev/null @@ -1,198 +0,0 @@ ---- -layout: container -title: Library - Views -root: ../ ---- - - - -Recline Views are instances of Backbone Views and they act as 'WUI' (web user -interface) component displaying some model object in the DOM. Like all Backbone -views they have a pointer to a model (or a collection) and have an associated -DOM-style element (usually this element will be bound into the page at some -point). - -
Looking for quickstart tutorial rather than reference documentation? See the Views Tutorial.
- - -Views provided by core Recline are crudely divided into two types: - -* Dataset Views: a View intended for displaying a recline.Model.Dataset in some - fashion. Examples are the Grid, Graph and Map views. -* Widget Views: a widget used for displaying some specific (and smaller) aspect - of a dataset or the application. Examples are QueryEditor and FilterEditor - which both provide a way for editing (a part of) a `recline.Model.Query` - associated to a Dataset. - -## Dataset Views currently available -{% include views-list.html %} - -## Dataset View - -These views are just Backbone views with a few additional conventions: - -1. The model passed to the View should always be a recline.Model.Dataset - instance -2. Views should generate their own root element rather than having it passed - in. -3. Views should apply a css class named 'recline-{view-name-lower-cased} to the - root element (and for all CSS for this view to be qualified using this CSS - class) -4. Read-only mode: CSS for this view should respect/utilize a parent - recline-read-only class in order to trigger read-only behaviour (this class - will usually be set on some parent element of the view's root element). -5. State: state (configuration) information for the view should be stored on an - attribute named state that is an instance of a Backbone Model (or, more - speficially, be an instance of `recline.Model.ObjectState`). In addition, a - state attribute may be specified in the Hash passed to a View on - iniitialization and this information should be used to set the initial state - of the view. - - Example of state would be the set of fields being plotted in a graph view. - - More information about State can be found below. - -To summarize some of this, the initialize function for a Dataset View should -look like: - -
-   initialize: {
-       model: {a recline.Model.Dataset instance}
-       // el: {do not specify - instead view should create}
-       state: {(optional) Object / Hash specifying initial state}
-       ...
-   }
-
- -Note: Dataset Views in core Recline have a common layout on disk as follows, -where ViewName is the named of View class: - -
-src/view-{lower-case-ViewName}.js
-css/{lower-case-ViewName}.css
-test/view-{lower-case-ViewName}.js
-
- -### State - -State information exists in order to support state serialization into the url -or elsewhere and reloading of application from a stored state. - -State is available not only for individual views (as described above) but for -the dataset (e.g. the current query). For an example of pulling together state -from across multiple components see `recline.View.DataExplorer`. - -### Flash Messages / Notifications - -To send 'flash messages' or notifications the convention is that views should -fire an event named `recline:flash` with a payload that is a flash object with -the following attributes (all optional): - -* message: message to show. -* category: warning (default), success, error -* persist: if true alert is persistent, o/w hidden after 3s (default=false) -* loader: if true show a loading message - -Objects or views wishing to bind to flash messages may then subscribe to these -events and take some action such as displaying them to the user. For an example -of such behaviour see the DataExplorer view. - -### Writing your own Views - -See the existing Views. - -## Internationalization - -### Adding translations to templates - -Internationalization is implemented with help of [intl-messageformat](https://www.npmjs.com/package/intl-messageformat) library and supports [ICU Message syntax](http://userguide.icu-project.org/formatparse/messages). - -Translation keys are using Mustache tags prefixed by `t.`. You can specify translated strings in two ways: - -1. Simple text with no variables is rendered using Mustache variable-tag -```javascript -$template = '{{ "{{t.Add_row"}}}}'; // will show "Add row" in defaultLocale (English) -``` - -2. Text with variables or special characters is rendered using Mustache section-tag - -```javascript -$template = '{{ "{{#t.desc"}}}}Add first_row field{{ "{{/t.desc"}}}}'; -// using special chars; will show "Add first_row field" in defaultLocale - -$template = '{{ "{{#t.num_records"}}}}{recordCount, plural, =0 {no records} =1{# record} other {# records}}{{ "{{/t.num_records"}}}}'; -// will show "no records", "1 record" or "x records" in defaultLocale (English) -``` - -When using section-tags in existing templates be sure to remove a bracket from variables inside sections: -```javascript -#template___notranslation = '{{ "{{recordCount"}}}} records'; -#template_withtranslation = '{{ "{{#t.num_records"}}}} {recordCount} records {{ "{{/t.num_records"}}}}'; -``` - - -Then setup Mustache to use translation by injecting tranlation tags in `render` function: - -```javascript -// ============== BEFORE =================== - -my.MultiView = Backbone.View.extend({ - render: function() { - var tmplData = this.model.toTemplateJSON(); - var output = Mustache.render(this.template, tmplData); - ... - } -}); - -// ============== AFTER ==================== - -my.MultiView = Backbone.View.extend({ - render: function() { - var tmplData = this.model.toTemplateJSON(); - tmplData = I18nMessages('recline', recline.View.translations).injectMustache(tmplData); // inject Moustache formatter - var output = Mustache.render(this.template, tmplData); - ... - } -}); -``` - -### Language resolution - -By default the language is detected from the root `lang` attributes - ` and ``. - -If you want to override this functionality then override `I18nMessages.languageResolver` with your implementation. - -```html - - -``` - -Libraries can also ask for specific language: `I18nMessages('recline', recline.View.translations, 'pl')`. Language resolver is not used in that case. - -If you're creating templates using default language other than English (why?) set appropriately appHardcodedLocale: `I18nMessages('recline', recline.View.translations, undefined, 'pl')`. Then missing strings won't be reported in console and underscores in simple translations will be converted to spaces. - -### Adding new language - -Create a copy of `src/i18n/pl.js` and translate all the keys. Long English messages can be found in `en.js`. - -### Overriding defined messages - -If you want to override translations from provided locale do it in a script tag after including recline: - -```html - - -``` \ No newline at end of file diff --git a/download.markdown b/download.markdown deleted file mode 100644 index 049e2e6d..00000000 --- a/download.markdown +++ /dev/null @@ -1,112 +0,0 @@ ---- -layout: container -title: Download ---- - - - -

Latest Code - Master

-

The tutorials on this website will usually be based on the latest (master) codebase. It should also be very stable.

- - -

Most Recent Official Release – v0.5

- - -

After downloading recline you'll want to use it in your project -- see below or tutorials for details.

- -### Changelog - -[View Changelog on Github](https://github.com/okfn/recline#changelog) - -### Dependencies - -Recline has dependencies on some third-party libraries. Specifically, recline.dataset.js depends on: - -* [Underscore](http://documentcloud.github.com/underscore/) >= 1.0 -* [Underscore Deferred](https://github.com/wookiehangover/underscore.deferred) v0.4.0 -* [Backbone](http://backbonejs.org/) >= 0.5.1 - -Those backends which utilize jquery's ajax method depend on jQuery: - -* [JQuery](http://jquery.com/) >= 1.6 - -All the views require, in addition to those needed for recline.dataset.js: - -* [JQuery](http://jquery.com/) >= 1.6 -* [Mustache.js](https://github.com/janl/mustache.js/) >= 0.5.0-dev (required for all views) - -Individual views have additional dependencies such as: - -* [JQuery Flot](http://www.flotcharts.org/) >= 0.7 (required for for graph view) -* [Leaflet](http://leaflet.cloudmade.com/) >= 0.4.4 (required for map view) -* [Leaflet.markercluster](https://github.com/danzel/Leaflet.markercluster) as of 2012-09-12 (required for marker clustering) -* [Verite Timeline](https://github.com/VeriteCo/Timeline/) as of 2012-05-02 (required for the timeline view) -* [Bootstrap](http://twitter.github.com/bootstrap/) >= v3 (default option for CSS and UI JS but you can use your own) - -If you grab the full zipball for Recline this will include all of the relevant -dependencies in the vendor directory and you can also find them at in the -[github repo here](https://github.com/okfn/recline/tree/master/vendor). - -### Example - -Here is an example of the page setup for an app using every Recline component: - -{% highlight html %} - - - - - -{% include recline-deps.html %} -{% endhighlight %} - diff --git a/favicon.ico b/favicon.ico deleted file mode 100644 index a1f8bfae..00000000 Binary files a/favicon.ico and /dev/null differ diff --git a/images/drag-handle.png b/images/drag-handle.png deleted file mode 100644 index ad7531cf..00000000 Binary files a/images/drag-handle.png and /dev/null differ diff --git a/images/logo.png b/images/logo.png deleted file mode 100644 index 423f84ed..00000000 Binary files a/images/logo.png and /dev/null differ diff --git a/images/screenshot-1.jpg b/images/screenshot-1.jpg deleted file mode 100644 index eddf1cea..00000000 Binary files a/images/screenshot-1.jpg and /dev/null differ diff --git a/index.html b/index.html deleted file mode 100644 index aea40126..00000000 --- a/index.html +++ /dev/null @@ -1,54 +0,0 @@ ---- -layout: default -title: Home ---- - - - diff --git a/make b/make deleted file mode 100755 index a01a23e4..00000000 --- a/make +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python -import sys -import shutil -import os - -def cat(): - print("** Combining js files") - cmd = 'ls src/*.js | grep -v couchdb | xargs cat > dist/recline.js' - os.system(cmd) - cmd = 'ls src/**/*.js | grep -v couchdb | xargs cat >> dist/recline.js' - os.system(cmd) - - cmd = 'cat src/model.js src/backend.memory.js > dist/recline.dataset.js' - os.system(cmd) - - print("** Combining css files") - cmd = 'cat css/*.css > dist/recline.css' - os.system(cmd) - -def docs(): - # build docs - print("** Building docs") - - docco_executable = os.environ.get('DOCCO_EXECUTABLE','docco') - if os.path.exists('/tmp/recline-docs'): - shutil.rmtree('/tmp/recline-docs') - os.makedirs('/tmp/recline-docs') - files = '%s/src/*.js' % os.getcwd() - dest = '%s/docs/src' % os.getcwd() - os.system('cd /tmp/recline-docs && %s %s && mv docs/* %s' % (docco_executable,files, dest)) - print("** Docs built ok") - -def minify(): - cmd = 'uglifyjs -o %s %s' % ('dist/recline.min.js', 'dist/recline.js') - os.system(cmd) - - cmd = 'uglifyjs -o %s %s' % ('dist/recline.dataset.min.js', 'dist/recline.dataset.js') - os.system(cmd) - -if __name__ == '__main__': - if not len(sys.argv) > 1: - print 'make cat | docs | minify | all' - sys.exit(1) - action = sys.argv[1] - if action == 'cat': - cat() - elif action == 'docs': - docs() - elif action == 'minify': - minify() - elif action == 'all': - cat() - docs() - minify() - diff --git a/package.json b/package.json deleted file mode 100644 index 7e6a8058..00000000 --- a/package.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "name": "reclinejs", - "description": "A simple but powerful data library for building data-oriented applications in pure Javascript", - "version": "0.7.0", - "contributors": [ - { - "name": "Rufus Pollock", - "email": "rufus.pollock@okfn.org" - }, - { - "name": "AdriĆ  Mercader", - "email": "adria.mercader@okfn.org" - }, - { - "name": "Max Ogden", - "email": "max@maxogden.com" - }, - { - "name": "Krzysztof Madejski", - "email": "krzysztof.madejski@epf.org.pl" - } - ], - "dependencies": { - "backbone": ">=0.5", - "jquery": ">=1.6", - "mustache": ">=0.5.2", - "underscore": ">=1.0", - "intl-messageformat": "1.3.x" - }, - "homepage": "http://reclinejs.com/", - "keywords": [ - "app", - "data", - "explorer", - "grid", - "library", - "table" - ], - "lib": "src", - "main": "dist/recline.js", - "repository": { - "type": "git", - "url": "http://github.com/okfn/recline.git", - "path": "src" - } -} diff --git a/src/backend.dataproxy.js b/src/backend.dataproxy.js deleted file mode 100644 index 92b5ae7c..00000000 --- a/src/backend.dataproxy.js +++ /dev/null @@ -1,76 +0,0 @@ -this.recline = this.recline || {}; -this.recline.Backend = this.recline.Backend || {}; -this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {}; - -(function(my) { - "use strict"; - my.__type__ = 'dataproxy'; - // URL for the dataproxy - my.dataproxy_url = '//jsonpdataproxy.appspot.com'; - // Timeout for dataproxy (after this time if no response we error) - // Needed because use JSONP so do not receive e.g. 500 errors - my.timeout = 5000; - - - // use either jQuery or Underscore Deferred depending on what is available - var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred; - - // ## load - // - // Load data from a URL via the [DataProxy](http://github.com/okfn/dataproxy). - // - // Returns array of field names and array of arrays for records - my.fetch = function(dataset) { - var data = { - url: dataset.url, - 'max-results': dataset.size || dataset.rows || 1000, - type: dataset.format || '' - }; - var jqxhr = jQuery.ajax({ - url: my.dataproxy_url, - data: data, - dataType: 'jsonp' - }); - var dfd = new Deferred(); - _wrapInTimeout(jqxhr).done(function(results) { - if (results.error) { - dfd.reject(results.error); - } - - dfd.resolve({ - records: results.data, - fields: results.fields, - useMemoryStore: true - }); - }) - .fail(function(args) { - dfd.reject(args); - }); - return dfd.promise(); - }; - - // ## _wrapInTimeout - // - // Convenience method providing a crude way to catch backend errors on JSONP calls. - // Many of backends use JSONP and so will not get error messages and this is - // a crude way to catch those errors. - var _wrapInTimeout = function(ourFunction) { - var dfd = new Deferred(); - var timer = setTimeout(function() { - dfd.reject({ - message: 'Request Error: Backend did not respond after ' + (my.timeout / 1000) + ' seconds' - }); - }, my.timeout); - ourFunction.done(function(args) { - clearTimeout(timer); - dfd.resolve(args); - }) - .fail(function(args) { - clearTimeout(timer); - dfd.reject(args); - }) - ; - return dfd.promise(); - }; - -}(this.recline.Backend.DataProxy)); diff --git a/src/backend.memory.js b/src/backend.memory.js deleted file mode 100644 index 8cb64ddf..00000000 --- a/src/backend.memory.js +++ /dev/null @@ -1,245 +0,0 @@ -this.recline = this.recline || {}; -this.recline.Backend = this.recline.Backend || {}; -this.recline.Backend.Memory = this.recline.Backend.Memory || {}; - -(function(my) { - "use strict"; - my.__type__ = 'memory'; - - // private data - use either jQuery or Underscore Deferred depending on what is available - var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred; - - // ## Data Wrapper - // - // Turn a simple array of JS objects into a mini data-store with - // functionality like querying, faceting, updating (by ID) and deleting (by - // ID). - // - // @param records list of hashes for each record/row in the data ({key: - // value, key: value}) - // @param fields (optional) list of field hashes (each hash defining a field - // as per recline.Model.Field). If fields not specified they will be taken - // from the data. - my.Store = function(records, fields) { - var self = this; - this.records = records; - // backwards compatability (in v0.5 records was named data) - this.data = this.records; - if (fields) { - this.fields = fields; - } else { - if (records) { - this.fields = _.map(records[0], function(value, key) { - return {id: key, type: 'string'}; - }); - } - } - - this.update = function(doc) { - _.each(self.records, function(internalDoc, idx) { - if(doc.id === internalDoc.id) { - self.records[idx] = doc; - } - }); - }; - - this.remove = function(doc) { - var newdocs = _.reject(self.records, function(internalDoc) { - return (doc.id === internalDoc.id); - }); - this.records = newdocs; - }; - - this.save = function(changes, dataset) { - var self = this; - var dfd = new Deferred(); - // TODO _.each(changes.creates) { ... } - _.each(changes.updates, function(record) { - self.update(record); - }); - _.each(changes.deletes, function(record) { - self.remove(record); - }); - dfd.resolve(); - return dfd.promise(); - }, - - this.query = function(queryObj) { - var dfd = new Deferred(); - var numRows = queryObj.size || this.records.length; - var start = queryObj.from || 0; - var results = this.records; - - results = this._applyFilters(results, queryObj); - results = this._applyFreeTextQuery(results, queryObj); - - // TODO: this is not complete sorting! - // What's wrong is we sort on the *last* entry in the sort list if there are multiple sort criteria - _.each(queryObj.sort, function(sortObj) { - var fieldName = sortObj.field; - results = _.sortBy(results, function(doc) { - var _out = doc[fieldName]; - return _out; - }); - if (sortObj.order == 'desc') { - results.reverse(); - } - }); - var facets = this.computeFacets(results, queryObj); - var out = { - total: results.length, - hits: results.slice(start, start+numRows), - facets: facets - }; - dfd.resolve(out); - return dfd.promise(); - }; - - // in place filtering - this._applyFilters = function(results, queryObj) { - var filters = queryObj.filters; - // register filters - var filterFunctions = { - term : term, - terms : terms, - range : range, - geo_distance : geo_distance - }; - var dataParsers = { - integer: function (e) { return parseFloat(e, 10); }, - 'float': function (e) { return parseFloat(e, 10); }, - number: function (e) { return parseFloat(e, 10); }, - string : function (e) { return e.toString(); }, - date : function (e) { return moment(e).valueOf(); }, - datetime : function (e) { return new Date(e).valueOf(); } - }; - var keyedFields = {}; - _.each(self.fields, function(field) { - keyedFields[field.id] = field; - }); - function getDataParser(filter) { - var fieldType = keyedFields[filter.field].type || 'string'; - return dataParsers[fieldType]; - } - - // filter records - return _.filter(results, function (record) { - var passes = _.map(filters, function (filter) { - return filterFunctions[filter.type](record, filter); - }); - - // return only these records that pass all filters - return _.all(passes, _.identity); - }); - - // filters definitions - function term(record, filter) { - var parse = getDataParser(filter); - var value = parse(record[filter.field]); - var term = parse(filter.term); - - return (value === term); - } - - function terms(record, filter) { - var parse = getDataParser(filter); - var value = parse(record[filter.field]); - var terms = parse(filter.terms).split(","); - - return (_.indexOf(terms, value) >= 0); - } - - function range(record, filter) { - var fromnull = (_.isUndefined(filter.from) || filter.from === null || filter.from === ''); - var tonull = (_.isUndefined(filter.to) || filter.to === null || filter.to === ''); - var parse = getDataParser(filter); - var value = parse(record[filter.field]); - var from = parse(fromnull ? '' : filter.from); - var to = parse(tonull ? '' : filter.to); - - // if at least one end of range is set do not allow '' to get through - // note that for strings '' <= {any-character} e.g. '' <= 'a' - if ((!fromnull || !tonull) && value === '') { - return false; - } - return ((fromnull || value >= from) && (tonull || value <= to)); - } - - function geo_distance() { - // TODO code here - } - }; - - // we OR across fields but AND across terms in query string - this._applyFreeTextQuery = function(results, queryObj) { - if (queryObj.q) { - var terms = queryObj.q.split(' '); - var patterns=_.map(terms, function(term) { - return new RegExp(term.toLowerCase()); - }); - results = _.filter(results, function(rawdoc) { - var matches = true; - _.each(patterns, function(pattern) { - var foundmatch = false; - _.each(self.fields, function(field) { - var value = rawdoc[field.id]; - if ((value !== null) && (value !== undefined)) { - value = value.toString(); - } else { - // value can be null (apparently in some cases) - value = ''; - } - // TODO regexes? - foundmatch = foundmatch || (pattern.test(value.toLowerCase())); - // TODO: early out (once we are true should break to spare unnecessary testing) - // if (foundmatch) return true; - }); - matches = matches && foundmatch; - // TODO: early out (once false should break to spare unnecessary testing) - // if (!matches) return false; - }); - return matches; - }); - } - return results; - }; - - this.computeFacets = function(records, queryObj) { - var facetResults = {}; - if (!queryObj.facets) { - return facetResults; - } - _.each(queryObj.facets, function(query, facetId) { - // TODO: remove dependency on recline.Model - facetResults[facetId] = new recline.Model.Facet({id: facetId}).toJSON(); - facetResults[facetId].termsall = {}; - }); - // faceting - _.each(records, function(doc) { - _.each(queryObj.facets, function(query, facetId) { - var fieldId = query.terms.field; - var val = doc[fieldId]; - var tmp = facetResults[facetId]; - if (val) { - tmp.termsall[val] = tmp.termsall[val] ? tmp.termsall[val] + 1 : 1; - } else { - tmp.missing = tmp.missing + 1; - } - }); - }); - _.each(queryObj.facets, function(query, facetId) { - var tmp = facetResults[facetId]; - var terms = _.map(tmp.termsall, function(count, term) { - return { term: term, count: count }; - }); - tmp.terms = _.sortBy(terms, function(item) { - // want descending order - return -item.count; - }); - tmp.terms = tmp.terms.slice(0, 10); - }); - return facetResults; - }; - }; - -}(this.recline.Backend.Memory)); diff --git a/src/ecma-fixes.js b/src/ecma-fixes.js deleted file mode 100644 index de8148be..00000000 --- a/src/ecma-fixes.js +++ /dev/null @@ -1,67 +0,0 @@ -// This file adds in full array method support in browsers that don't support it -// see: http://stackoverflow.com/questions/2790001/fixing-javascript-array-functions-in-internet-explorer-indexof-foreach-etc - -// Add ECMA262-5 Array methods if not supported natively -if (!('indexOf' in Array.prototype)) { - Array.prototype.indexOf= function(find, i /*opt*/) { - if (i===undefined) i= 0; - if (i<0) i+= this.length; - if (i<0) i= 0; - for (var n= this.length; ithis.length-1) i= this.length-1; - for (i++; i-->0;) /* i++ because from-argument is sadly inclusive */ - if (i in this && this[i]===find) - return i; - return -1; - }; -} -if (!('forEach' in Array.prototype)) { - Array.prototype.forEach= function(action, that /*opt*/) { - for (var i= 0, n= this.length; iWitamy! \ -

Jakie kolumny powinny zostać narysowane na wykresie?

\ -

Wybierz je używając menu po prawej, a wykres pojawi się automatycznie.

', - Graph_Type: 'Typ wykresu', - Lines_and_Points: 'Linie z punktami', - Lines: 'Linie', - Points: 'Punkty', - Bars: 'Słupki poziome', - Columns: 'Słupki', - flot_Group_Column: 'Kolumna (Oś X)', - Please_choose: 'Proszę wybrać', - Remove: 'Usuń', - Series: 'Seria', - Axis_2: 'Oś Y', - Add_Series: 'Dodaj serię danych', - Save: 'Zapisz', - - map_mapping: 'Źródło koordynatów', - map_mapping_lat_lon: 'Szerokość i długość geo.', - map_mapping_geojson: 'Jedna kolumna typu GeoJSON', - Latitude_field: 'Kolumna szerokości geo. (WGS84)', - Longitude_field: 'Kolumna długości geo. (WGS84)', - Auto_zoom_to_features: 'Kadruj, aby pokazać wszytkie punkty', - Cluster_markers: 'Łącz pobliskie punkty w grupy', - - num_records: '{recordCount} rekordów' -}; \ No newline at end of file diff --git a/src/model.js b/src/model.js deleted file mode 100644 index 1c383b61..00000000 --- a/src/model.js +++ /dev/null @@ -1,651 +0,0 @@ -// # Recline Backbone Models -this.recline = this.recline || {}; -this.recline.Model = this.recline.Model || {}; - -(function(my) { - "use strict"; - -// use either jQuery or Underscore Deferred depending on what is available -var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred; - -// ## Dataset -my.Dataset = Backbone.Model.extend({ - constructor: function Dataset() { - Backbone.Model.prototype.constructor.apply(this, arguments); - }, - - // ### initialize - initialize: function() { - var self = this; - _.bindAll(this, 'query'); - this.backend = null; - if (this.get('backend')) { - this.backend = this._backendFromString(this.get('backend')); - } else { // try to guess backend ... - if (this.get('records')) { - this.backend = recline.Backend.Memory; - } - } - this.fields = new my.FieldList(); - this.records = new my.RecordList(); - this._changes = { - deletes: [], - updates: [], - creates: [] - }; - this.facets = new my.FacetList(); - this.recordCount = null; - this.queryState = new my.Query(); - this.queryState.bind('change facet:add', function () { - self.query(); // We want to call query() without any arguments. - }); - // store is what we query and save against - // store will either be the backend or be a memory store if Backend fetch - // tells us to use memory store - this._store = this.backend; - - // if backend has a handleQueryResultFunction, use that - this._handleResult = (this.backend != null && _.has(this.backend, 'handleQueryResult')) ? - this.backend.handleQueryResult : this._handleQueryResult; - if (this.backend == recline.Backend.Memory) { - this.fetch(); - } - }, - - sync: function(method, model, options) { - return this.backend.sync(method, model, options); - }, - - // ### fetch - // - // Retrieve dataset and (some) records from the backend. - fetch: function() { - var self = this; - var dfd = new Deferred(); - - if (this.backend !== recline.Backend.Memory) { - this.backend.fetch(this.toJSON()) - .done(handleResults) - .fail(function(args) { - dfd.reject(args); - }); - } else { - // special case where we have been given data directly - handleResults({ - records: this.get('records'), - fields: this.get('fields'), - useMemoryStore: true - }); - } - - function handleResults(results) { - // if explicitly given the fields - // (e.g. var dataset = new Dataset({fields: fields, ...}) - // use that field info over anything we get back by parsing the data - // (results.fields) - var fields = self.get('fields') || results.fields; - - var out = self._normalizeRecordsAndFields(results.records, fields); - if (results.useMemoryStore) { - self._store = new recline.Backend.Memory.Store(out.records, out.fields); - } - - self.set(results.metadata); - self.fields.reset(out.fields); - self.query() - .done(function() { - dfd.resolve(self); - }) - .fail(function(args) { - dfd.reject(args); - }); - } - - return dfd.promise(); - }, - - // ### _normalizeRecordsAndFields - // - // Get a proper set of fields and records from incoming set of fields and records either of which may be null or arrays or objects - // - // e.g. fields = ['a', 'b', 'c'] and records = [ [1,2,3] ] => - // fields = [ {id: a}, {id: b}, {id: c}], records = [ {a: 1}, {b: 2}, {c: 3}] - _normalizeRecordsAndFields: function(records, fields) { - // if no fields get them from records - if (!fields && records && records.length > 0) { - // records is array then fields is first row of records ... - if (records[0] instanceof Array) { - fields = records[0]; - records = records.slice(1); - } else { - fields = _.map(_.keys(records[0]), function(key) { - return {id: key}; - }); - } - } - - // fields is an array of strings (i.e. list of field headings/ids) - if (fields && fields.length > 0 && (fields[0] === null || typeof(fields[0]) != 'object')) { - // Rename duplicate fieldIds as each field name needs to be - // unique. - var seen = {}; - fields = _.map(fields, function(field, index) { - if (field === null) { - field = ''; - } else { - field = field.toString(); - } - // cannot use trim as not supported by IE7 - var fieldId = field.replace(/^\s+|\s+$/g, ''); - if (fieldId === '') { - fieldId = '_noname_'; - field = fieldId; - } - while (fieldId in seen) { - seen[field] += 1; - fieldId = field + seen[field]; - } - if (!(field in seen)) { - seen[field] = 0; - } - // TODO: decide whether to keep original name as label ... - // return { id: fieldId, label: field || fieldId } - return { id: fieldId }; - }); - } - // records is provided as arrays so need to zip together with fields - // NB: this requires you to have fields to match arrays - if (records && records.length > 0 && records[0] instanceof Array) { - records = _.map(records, function(doc) { - var tmp = {}; - _.each(fields, function(field, idx) { - tmp[field.id] = doc[idx]; - }); - return tmp; - }); - } - return { - fields: fields, - records: records - }; - }, - - save: function() { - var self = this; - // TODO: need to reset the changes ... - return this._store.save(this._changes, this.toJSON()); - }, - - // ### query - // - // AJAX method with promise API to get records from the backend. - // - // It will query based on current query state (given by this.queryState) - // updated by queryObj (if provided). - // - // Resulting RecordList are used to reset this.records and are - // also returned. - query: function(queryObj) { - var self = this; - var dfd = new Deferred(); - this.trigger('query:start'); - - if (queryObj) { - var attributes = queryObj; - if (queryObj instanceof my.Query) { - attributes = queryObj.toJSON(); - } - this.queryState.set(attributes, {silent: true}); - } - var actualQuery = this.queryState.toJSON(); - - this._store.query(actualQuery, this.toJSON()) - .done(function(queryResult) { - self._handleResult(queryResult); - self.trigger('query:done'); - dfd.resolve(self.records); - }) - .fail(function(args) { - self.trigger('query:fail', args); - dfd.reject(args); - }); - return dfd.promise(); - }, - - _handleQueryResult: function(queryResult) { - var self = this; - self.recordCount = queryResult.total; - var docs = _.map(queryResult.hits, function(hit) { - var _doc = new my.Record(hit); - _doc.fields = self.fields; - _doc.bind('change', function(doc) { - self._changes.updates.push(doc.toJSON()); - }); - _doc.bind('destroy', function(doc) { - self._changes.deletes.push(doc.toJSON()); - }); - return _doc; - }); - self.records.reset(docs); - if (queryResult.facets) { - var facets = _.map(queryResult.facets, function(facetResult, facetId) { - facetResult.id = facetId; - return new my.Facet(facetResult); - }); - self.facets.reset(facets); - } - }, - - toTemplateJSON: function() { - var data = this.toJSON(); - data.recordCount = this.recordCount; - data.fields = this.fields.toJSON(); - return data; - }, - - // ### getFieldsSummary - // - // Get a summary for each field in the form of a `Facet`. - // - // @return null as this is async function. Provides deferred/promise interface. - getFieldsSummary: function() { - var self = this; - var query = new my.Query(); - query.set({size: 0}); - this.fields.each(function(field) { - query.addFacet(field.id); - }); - var dfd = new Deferred(); - this._store.query(query.toJSON(), this.toJSON()).done(function(queryResult) { - if (queryResult.facets) { - _.each(queryResult.facets, function(facetResult, facetId) { - facetResult.id = facetId; - var facet = new my.Facet(facetResult); - // TODO: probably want replace rather than reset (i.e. just replace the facet with this id) - self.fields.get(facetId).facets.reset(facet); - }); - } - dfd.resolve(queryResult); - }); - return dfd.promise(); - }, - - // Deprecated (as of v0.5) - use record.summary() - recordSummary: function(record) { - return record.summary(); - }, - - // ### _backendFromString(backendString) - // - // Look up a backend module from a backend string (look in recline.Backend) - _backendFromString: function(backendString) { - var backend = null; - if (recline && recline.Backend) { - _.each(_.keys(recline.Backend), function(name) { - if (name.toLowerCase() === backendString.toLowerCase()) { - backend = recline.Backend[name]; - } - }); - } - return backend; - } -}); - - -// ## A Record -// -// A single record (or row) in the dataset -my.Record = Backbone.Model.extend({ - constructor: function Record() { - Backbone.Model.prototype.constructor.apply(this, arguments); - }, - - // ### initialize - // - // Create a Record - // - // You usually will not do this directly but will have records created by - // Dataset e.g. in query method - // - // Certain methods require presence of a fields attribute (identical to that on Dataset) - initialize: function() { - _.bindAll(this, 'getFieldValue'); - }, - - // ### getFieldValue - // - // For the provided Field get the corresponding rendered computed data value - // for this record. - // - // NB: if field is undefined a default '' value will be returned - getFieldValue: function(field) { - var val = this.getFieldValueUnrendered(field); - if (field && !_.isUndefined(field.renderer)) { - val = field.renderer(val, field, this.toJSON()); - } - return val; - }, - - // ### getFieldValueUnrendered - // - // For the provided Field get the corresponding computed data value - // for this record. - // - // NB: if field is undefined a default '' value will be returned - getFieldValueUnrendered: function(field) { - if (!field) { - return ''; - } - var val = this.get(field.id); - if (field.deriver) { - val = field.deriver(val, field, this); - } - return val; - }, - - // ### summary - // - // Get a simple html summary of this record in form of key/value list - summary: function(record) { - var self = this; - var html = '
'; - this.fields.each(function(field) { - if (field.id != 'id') { - html += '
' + field.get('label') + ': ' + self.getFieldValue(field) + '
'; - } - }); - html += '
'; - return html; - }, - - // Override Backbone save, fetch and destroy so they do nothing - // Instead, Dataset object that created this Record should take care of - // handling these changes (discovery will occur via event notifications) - // WARNING: these will not persist *unless* you call save on Dataset - fetch: function() {}, - save: function() {}, - destroy: function() { this.trigger('destroy', this); } -}); - - -// ## A Backbone collection of Records -my.RecordList = Backbone.Collection.extend({ - constructor: function RecordList() { - Backbone.Collection.prototype.constructor.apply(this, arguments); - }, - model: my.Record -}); - - -// ## A Field (aka Column) on a Dataset -my.Field = Backbone.Model.extend({ - constructor: function Field() { - Backbone.Model.prototype.constructor.apply(this, arguments); - }, - // ### defaults - define default values - defaults: { - label: null, - type: 'string', - format: null, - is_derived: false - }, - // ### initialize - // - // @param {Object} data: standard Backbone model attributes - // - // @param {Object} options: renderer and/or deriver functions. - initialize: function(data, options) { - // if a hash not passed in the first argument throw error - if ('0' in data) { - throw new Error('Looks like you did not pass a proper hash with id to Field constructor'); - } - if (this.attributes.label === null) { - this.set({label: this.id}); - } - if (this.attributes.type.toLowerCase() in this._typeMap) { - this.attributes.type = this._typeMap[this.attributes.type.toLowerCase()]; - } - if (options) { - this.renderer = options.renderer; - this.deriver = options.deriver; - } - if (!this.renderer) { - this.renderer = this.defaultRenderers[this.get('type')]; - } - this.facets = new my.FacetList(); - }, - _typeMap: { - 'text': 'string', - 'double': 'number', - 'float': 'number', - 'numeric': 'number', - 'int': 'integer', - 'datetime': 'date-time', - 'bool': 'boolean', - 'timestamp': 'date-time', - 'json': 'object' - }, - defaultRenderers: { - object: function(val, field, doc) { - return JSON.stringify(val); - }, - geo_point: function(val, field, doc) { - return JSON.stringify(val); - }, - 'number': function(val, field, doc) { - var format = field.get('format'); - if (format === 'percentage') { - return val + '%'; - } - return val; - }, - 'string': function(val, field, doc) { - var format = field.get('format'); - if (format === 'markdown') { - if (typeof Showdown !== 'undefined') { - var showdown = new Showdown.converter(); - out = showdown.makeHtml(val); - return out; - } else { - return val; - } - } else if (format == 'plain') { - return val; - } else { - // as this is the default and default type is string may get things - // here that are not actually strings - if (val && typeof val === 'string') { - val = val.replace(/(https?:\/\/[^ ]+)/g, '$1'); - } - return val; - } - } - } -}); - -my.FieldList = Backbone.Collection.extend({ - constructor: function FieldList() { - Backbone.Collection.prototype.constructor.apply(this, arguments); - }, - model: my.Field -}); - -// ## Query -my.Query = Backbone.Model.extend({ - constructor: function Query() { - Backbone.Model.prototype.constructor.apply(this, arguments); - }, - defaults: function() { - return { - size: 100, - from: 0, - q: '', - facets: {}, - filters: [] - }; - }, - _filterTemplates: { - term: { - type: 'term', - // TODO do we need this attribute here? - field: '', - term: '' - }, - range: { - type: 'range', - from: '', - to: '' - }, - geo_distance: { - type: 'geo_distance', - distance: 10, - unit: 'km', - point: { - lon: 0, - lat: 0 - } - } - }, - // ### addFilter(filter) - // - // Add a new filter specified by the filter hash and append to the list of filters - // - // @param filter an object specifying the filter - see _filterTemplates for examples. If only type is provided will generate a filter by cloning _filterTemplates - addFilter: function(filter) { - // crude deep copy - var ourfilter = JSON.parse(JSON.stringify(filter)); - // not fully specified so use template and over-write - if (_.keys(filter).length <= 3) { - ourfilter = _.defaults(ourfilter, this._filterTemplates[filter.type]); - } - var filters = this.get('filters'); - filters.push(ourfilter); - this.trigger('change:filters:new-blank'); - }, - replaceFilter: function(filter) { - // delete filter on the same field, then add - var filters = this.get('filters'); - var idx = -1; - _.each(this.get('filters'), function(f, key, list) { - if (filter.field == f.field) { - idx = key; - } - }); - // trigger just one event (change:filters:new-blank) instead of one for remove and - // one for add - if (idx >= 0) { - filters.splice(idx, 1); - this.set({filters: filters}); - } - this.addFilter(filter); - }, - updateFilter: function(index, value) { - }, - // ### removeFilter - // - // Remove a filter from filters at index filterIndex - removeFilter: function(filterIndex) { - var filters = this.get('filters'); - filters.splice(filterIndex, 1); - this.set({filters: filters}); - this.trigger('change'); - }, - // ### addFacet - // - // Add a Facet to this query - // - // See - addFacet: function(fieldId, size, silent) { - var facets = this.get('facets'); - // Assume id and fieldId should be the same (TODO: this need not be true if we want to add two different type of facets on same field) - if (_.contains(_.keys(facets), fieldId)) { - return; - } - facets[fieldId] = { - terms: { field: fieldId } - }; - if (!_.isUndefined(size)) { - facets[fieldId].terms.size = size; - } - this.set({facets: facets}, {silent: true}); - if (!silent) { - this.trigger('facet:add', this); - } - }, - addHistogramFacet: function(fieldId) { - var facets = this.get('facets'); - facets[fieldId] = { - date_histogram: { - field: fieldId, - interval: 'day' - } - }; - this.set({facets: facets}, {silent: true}); - this.trigger('facet:add', this); - }, - removeFacet: function(fieldId) { - var facets = this.get('facets'); - // Assume id and fieldId should be the same (TODO: this need not be true if we want to add two different type of facets on same field) - if (!_.contains(_.keys(facets), fieldId)) { - return; - } - delete facets[fieldId]; - this.set({facets: facets}, {silent: true}); - this.trigger('facet:remove', this); - }, - clearFacets: function() { - var facets = this.get('facets'); - _.each(_.keys(facets), function(fieldId) { - delete facets[fieldId]; - }); - this.trigger('facet:remove', this); - }, - // trigger a facet add; use this to trigger a single event after adding - // multiple facets - refreshFacets: function() { - this.trigger('facet:add', this); - } - -}); - - -// ## A Facet (Result) -my.Facet = Backbone.Model.extend({ - constructor: function Facet() { - Backbone.Model.prototype.constructor.apply(this, arguments); - }, - defaults: function() { - return { - _type: 'terms', - total: 0, - other: 0, - missing: 0, - terms: [] - }; - } -}); - -// ## A Collection/List of Facets -my.FacetList = Backbone.Collection.extend({ - constructor: function FacetList() { - Backbone.Collection.prototype.constructor.apply(this, arguments); - }, - model: my.Facet -}); - -// ## Object State -// -// Convenience Backbone model for storing (configuration) state of objects like Views. -my.ObjectState = Backbone.Model.extend({ -}); - - -// ## Backbone.sync -// -// Override Backbone.sync to hand off to sync function in relevant backend -// Backbone.sync = function(method, model, options) { -// return model.backend.sync(method, model, options); -// }; - -}(this.recline.Model)); - diff --git a/src/view.flot.js b/src/view.flot.js deleted file mode 100644 index 6d468f80..00000000 --- a/src/view.flot.js +++ /dev/null @@ -1,520 +0,0 @@ -/*jshint multistr:true */ - -this.recline = this.recline || {}; -this.recline.View = this.recline.View || {}; - -(function($, my) { - "use strict"; -// ## Graph view for a Dataset using Flot graphing library. -// -// Initialization arguments (in a hash in first parameter): -// -// * model: recline.Model.Dataset -// * state: (optional) configuration hash of form: -// -// { -// group: {column name for x-axis}, -// series: [{column name for series A}, {column name series B}, ... ], -// // options are: lines, points, lines-and-points, bars, columns -// graphType: 'lines', -// graphOptions: {custom [flot options]} -// } -// -// NB: should *not* provide an el argument to the view but must let the view -// generate the element itself (you can then append view.el to the DOM. -my.Flot = Backbone.View.extend({ - template: ' \ -
\ -
\ -
\ - {{#t.flot_info}}

Hey there!

\ -

There\'s no graph here yet because we don\'t know what fields you\'d like to see plotted.

\ -

Please tell us by using the menu on the right and a graph will automatically appear.

{{/t.flot_info}} \ -
\ -
\ -
\ -', - - initialize: function(options) { - var self = this; - this.graphColors = ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"]; - - _.bindAll(this, 'render', 'redraw', '_toolTip', '_xaxisLabel'); - this.needToRedraw = false; - this.listenTo(this.model, 'change', this.render); - this.listenTo(this.model.fields, 'reset add', this.render); - this.listenTo(this.model.records, 'reset add', this.redraw); - var stateData = _.extend({ - group: null, - // so that at least one series chooser box shows up - series: [], - graphType: 'lines-and-points' - }, - options.state - ); - this.state = new recline.Model.ObjectState(stateData); - this.previousTooltipPoint = {x: null, y: null}; - this.editor = new my.FlotControls({ - model: this.model, - state: this.state.toJSON() - }); - this.listenTo(this.editor.state, 'change', function() { - self.state.set(self.editor.state.toJSON()); - self.redraw(); - }); - this.elSidebar = this.editor.$el; - }, - - render: function() { - var self = this; - var tmplData = I18nMessages('recline', recline.View.translations).injectMustache(this.model.toTemplateJSON()); - var htmls = Mustache.render(this.template, tmplData); - this.$el.html(htmls); - this.$graph = this.$el.find('.panel.graph'); - this.$graph.on("plothover", this._toolTip); - return this; - }, - - remove: function () { - this.editor.remove(); - Backbone.View.prototype.remove.apply(this, arguments); - }, - - redraw: function() { - // There are issues generating a Flot graph if either: - // * The relevant div that graph attaches to his hidden at the moment of creating the plot -- Flot will complain with - // Uncaught Invalid dimensions for plot, width = 0, height = 0 - // * There is no data for the plot -- either same error or may have issues later with errors like 'non-existent node-value' - var areWeVisible = !jQuery.expr.filters.hidden(this.el); - if ((!areWeVisible || this.model.records.length === 0)) { - this.needToRedraw = true; - return; - } - - // check we have something to plot - if (this.state.get('group') && this.state.get('series')) { - var series = this.createSeries(); - var options = this.getGraphOptions(this.state.attributes.graphType, series[0].data.length); - this.plot = $.plot(this.$graph, series, options); - } - }, - - show: function() { - // because we cannot redraw when hidden we may need to when becoming visible - if (this.needToRedraw) { - this.redraw(); - } - }, - - // infoboxes on mouse hover on points/bars etc - _toolTip: function (event, pos, item) { - if (item) { - if (this.previousTooltipPoint.x !== item.dataIndex || - this.previousTooltipPoint.y !== item.seriesIndex) { - this.previousTooltipPoint.x = item.dataIndex; - this.previousTooltipPoint.y = item.seriesIndex; - $("#recline-flot-tooltip").remove(); - - var x = item.datapoint[0].toFixed(2), - y = item.datapoint[1].toFixed(2); - - if (this.state.attributes.graphType === 'bars') { - x = item.datapoint[1].toFixed(2), - y = item.datapoint[0].toFixed(2); - } - - var template = _.template('<%= group %> = <%= x %>, <%= series %> = <%= y %>'); - var content = template({ - group: this.state.attributes.group, - x: this._xaxisLabel(x), - series: item.series.label, - y: y - }); - - // use a different tooltip location offset for bar charts - var xLocation, yLocation; - if (this.state.attributes.graphType === 'bars') { - xLocation = item.pageX + 15; - yLocation = item.pageY - 10; - } else if (this.state.attributes.graphType === 'columns') { - xLocation = item.pageX + 15; - yLocation = item.pageY; - } else { - xLocation = item.pageX + 10; - yLocation = item.pageY - 20; - } - - $('
' + content + '
').css({ - top: yLocation, - left: xLocation - }).appendTo("body").fadeIn(200); - } - } else { - $("#recline-flot-tooltip").remove(); - this.previousTooltipPoint.x = null; - this.previousTooltipPoint.y = null; - } - }, - - _xaxisLabel: function (x) { - if (this._groupFieldIsDateTime()) { - // oddly x comes through as milliseconds *string* (rather than int - // or float) so we have to reparse - x = new Date(parseFloat(x)).toLocaleDateString(); - } else if (this.xvaluesAreIndex) { - x = parseInt(x, 10); - // HACK: deal with bar graph style cases where x-axis items were strings - // In this case x at this point is the index of the item in the list of - // records not its actual x-axis value - x = this.model.records.models[x].get(this.state.attributes.group); - } - - return x; - }, - - // ### getGraphOptions - // - // Get options for Flot Graph - // - // needs to be function as can depend on state - // - // @param typeId graphType id (lines, lines-and-points etc) - // @param numPoints the number of points that will be plotted - getGraphOptions: function(typeId, numPoints) { - var self = this; - var groupFieldIsDateTime = self._groupFieldIsDateTime(); - var xaxis = {}; - - if (!groupFieldIsDateTime) { - xaxis.tickFormatter = function (x) { - // convert x to a string and make sure that it is not too long or the - // tick labels will overlap - // TODO: find a more accurate way of calculating the size of tick labels - var label = self._xaxisLabel(x) || ""; - - if (typeof label !== 'string') { - label = label.toString(); - } - if (self.state.attributes.graphType !== 'bars' && label.length > 10) { - label = label.slice(0, 10) + "..."; - } - - return label; - }; - } - - // for labels case we only want ticks at the label intervals - // HACK: however we also get this case with Date fields. In that case we - // could have a lot of values and so we limit to max 15 (we assume) - if (this.xvaluesAreIndex) { - var numTicks = Math.min(this.model.records.length, 15); - var increment = this.model.records.length / numTicks; - var ticks = []; - for (var i=0; i \ -
\ -
\ -
\ - \ -
\ - \ -
\ -
\ -
\ - \ -
\ - \ -
\ -
\ -
\ -
\ -
\ -
\ - \ -
\ - \ -
\ -
\ -', - templateSeriesEditor: ' \ -
\ -
\ -
\ -
\ -
\ - ', - events: { - 'change form select': 'onEditorSubmit', - 'click .editor-add': '_onAddSeries', - 'click .action-remove-series': 'removeSeries' - }, - - initialize: function(options) { - var self = this; - _.bindAll(this, 'render'); - this.listenTo(this.model.fields, 'reset add', this.render); - this.state = new recline.Model.ObjectState(options.state); - this.render(); - }, - - render: function() { - var self = this; - var tmplData = this.model.toTemplateJSON(); - tmplData = I18nMessages('recline', recline.View.translations).injectMustache(tmplData); - var htmls = Mustache.render(this.template, tmplData); - this.$el.html(htmls); - - // set up editor from state - if (this.state.get('graphType')) { - this._selectOption('.editor-type', this.state.get('graphType')); - } - if (this.state.get('group')) { - this._selectOption('.editor-group', this.state.get('group')); - } - // ensure at least one series box shows up - var tmpSeries = [""]; - if (this.state.get('series').length > 0) { - tmpSeries = this.state.get('series'); - } - _.each(tmpSeries, function(series, idx) { - self.addSeries(idx); - self._selectOption('.editor-series.js-series-' + idx, series); - }); - return this; - }, - - // Private: Helper function to select an option from a select list - // - _selectOption: function(id,value){ - var options = this.$el.find(id + ' select > option'); - if (options) { - options.each(function(opt){ - if (this.value == value) { - $(this).attr('selected','selected'); - return false; - } - }); - } - }, - - onEditorSubmit: function(e) { - var select = this.$el.find('.editor-group select'); - var $editor = this; - var $series = this.$el.find('.editor-series select'); - var series = $series.map(function () { - return $(this).val(); - }); - var updatedState = { - series: $.makeArray(series), - group: this.$el.find('.editor-group select').val(), - graphType: this.$el.find('.editor-type select').val() - }; - this.state.set(updatedState); - }, - - // Public: Adds a new empty series select box to the editor. - // - // @param [int] idx index of this series in the list of series - // - // Returns itself. - addSeries: function (idx) { - var data = _.extend({ - seriesIndex: idx, - seriesName: String.fromCharCode(idx + 64 + 1) - }, this.model.toTemplateJSON()); - - data = I18nMessages('recline', recline.View.translations).injectMustache(data); - var htmls = Mustache.render(this.templateSeriesEditor, data); - this.$el.find('.editor-series-group').append(htmls); - return this; - }, - - _onAddSeries: function(e) { - e.preventDefault(); - this.addSeries(this.state.get('series').length); - }, - - // Public: Removes a series list item from the editor. - // - // Also updates the labels of the remaining series elements. - removeSeries: function (e) { - e.preventDefault(); - var $el = $(e.target); - $el.parent().parent().remove(); - this.onEditorSubmit(); - } -}); - -})(jQuery, recline.View); diff --git a/src/view.graph.js b/src/view.graph.js deleted file mode 100644 index fb3d8356..00000000 --- a/src/view.graph.js +++ /dev/null @@ -1,4 +0,0 @@ -this.recline = this.recline || {}; -this.recline.View = this.recline.View || {}; -this.recline.View.Graph = this.recline.View.Flot; -this.recline.View.GraphControls = this.recline.View.FlotControls; diff --git a/src/view.grid.js b/src/view.grid.js deleted file mode 100644 index d2b0792c..00000000 --- a/src/view.grid.js +++ /dev/null @@ -1,273 +0,0 @@ -/*jshint multistr:true */ - -this.recline = this.recline || {}; -this.recline.View = this.recline.View || {}; - -(function($, my) { - "use strict"; -// ## (Data) Grid Dataset View -// -// Provides a tabular view on a Dataset. -// -// Initialize it with a `recline.Model.Dataset`. -my.Grid = Backbone.View.extend({ - tagName: "div", - className: "recline-grid-container", - - initialize: function(modelEtc) { - var self = this; - _.bindAll(this, 'render', 'onHorizontalScroll'); - this.listenTo(this.model.records, 'add reset remove', this.render); - this.tempState = {}; - var state = _.extend({ - hiddenFields: [] - }, modelEtc.state - ); - this.state = new recline.Model.ObjectState(state); - }, - - events: { - // does not work here so done at end of render function - // 'scroll .recline-grid tbody': 'onHorizontalScroll' - }, - - // ====================================================== - // Column and row menus - - setColumnSort: function(order) { - var sort = [{}]; - sort[0][this.tempState.currentColumn] = {order: order}; - this.model.query({sort: sort}); - }, - - hideColumn: function() { - var hiddenFields = this.state.get('hiddenFields'); - hiddenFields.push(this.tempState.currentColumn); - this.state.set({hiddenFields: hiddenFields}); - // change event not being triggered (because it is an array?) so trigger manually - this.state.trigger('change'); - this.render(); - }, - - showColumn: function(e) { - var hiddenFields = _.without(this.state.get('hiddenFields'), $(e.target).data('column')); - this.state.set({hiddenFields: hiddenFields}); - this.render(); - }, - - onHorizontalScroll: function(e) { - var currentScroll = $(e.target).scrollLeft(); - this.$el.find('.recline-grid thead tr').scrollLeft(currentScroll); - }, - - // ====================================================== - // #### Templating - template: ' \ -
\ - \ - \ - \ - {{#fields}} \ - \ - {{/fields}} \ - \ - \ - \ - \ -
\ - {{label}} \ -
\ -
\ - ', - - toTemplateJSON: function() { - var self = this; - var modelData = this.model.toJSON(); - modelData.notEmpty = ( this.fields.length > 0 ); - // TODO: move this sort of thing into a toTemplateJSON method on Dataset? - modelData.fields = this.fields.map(function(field) { - return field.toJSON(); - }); - // last header width = scroll bar - border (2px) */ - modelData.lastHeaderWidth = this.scrollbarDimensions.width - 2; - return modelData; - }, - render: function() { - var self = this; - this.fields = new recline.Model.FieldList(this.model.fields.filter(function(field) { - return _.indexOf(self.state.get('hiddenFields'), field.id) == -1; - })); - - this.scrollbarDimensions = this.scrollbarDimensions || this._scrollbarSize(); // skip measurement if already have dimensions - var numFields = this.fields.length; - // compute field widths (-20 for first menu col + 10px for padding on each col and finally 16px for the scrollbar) - var fullWidth = self.$el.width() - 20 - 10 * numFields - this.scrollbarDimensions.width; - var width = parseInt(Math.max(50, fullWidth / numFields), 10); - // if columns extend outside viewport then remainder is 0 - var remainder = Math.max(fullWidth - numFields * width,0); - this.fields.each(function(field, idx) { - // add the remainder to the first field width so we make up full col - if (idx === 0) { - field.set({width: width+remainder}); - } else { - field.set({width: width}); - } - }); - var tmplData = this.toTemplateJSON(); - tmplData = I18nMessages('recline', recline.View.translations).injectMustache(tmplData); - - var htmls = Mustache.render(this.template, tmplData); - this.$el.html(htmls); - this.model.records.forEach(function(doc) { - var tr = $(''); - self.$el.find('tbody').append(tr); - var newView = new my.GridRow({ - model: doc, - el: tr, - fields: self.fields - }); - newView.render(); - }); - // hide extra header col if no scrollbar to avoid unsightly overhang - var $tbody = this.$el.find('tbody')[0]; - if ($tbody.scrollHeight <= $tbody.offsetHeight) { - this.$el.find('th.last-header').hide(); - } - this.$el.find('.recline-grid').toggleClass('no-hidden', (self.state.get('hiddenFields').length === 0)); - this.$el.find('.recline-grid tbody').scroll(this.onHorizontalScroll); - return this; - }, - - // ### _scrollbarSize - // - // Measure width of a vertical scrollbar and height of a horizontal scrollbar. - // - // @return: { width: pixelWidth, height: pixelHeight } - _scrollbarSize: function() { - var $c = $("
").appendTo("body"); - var dim = { width: $c.width() - $c[0].clientWidth + 1, height: $c.height() - $c[0].clientHeight }; - $c.remove(); - return dim; - } -}); - -// ## GridRow View for rendering an individual record. -// -// Since we want this to update in place it is up to creator to provider the element to attach to. -// -// In addition you *must* pass in a FieldList in the constructor options. This should be list of fields for the Grid. -// -// Example: -// -//
-// var row = new GridRow({
-//   model: dataset-record,
-//     el: dom-element,
-//     fields: mydatasets.fields // a FieldList object
-//   });
-// 
-my.GridRow = Backbone.View.extend({ - initialize: function(initData) { - _.bindAll(this, 'render'); - this._fields = initData.fields; - this.listenTo(this.model, 'change', this.render); - }, - - template: ' \ - {{#cells}} \ - \ -
\ -   \ -
{{{value}}}
\ -
\ - \ - {{/cells}} \ - ', - events: { - 'click .data-table-cell-edit': 'onEditClick', - 'click .data-table-cell-editor .okButton': 'onEditorOK', - 'click .data-table-cell-editor .cancelButton': 'onEditorCancel' - }, - - toTemplateJSON: function() { - var self = this; - var doc = this.model; - var cellData = this._fields.map(function(field) { - return { - field: field.id, - width: field.get('width'), - value: doc.getFieldValue(field) - }; - }); - return { id: this.id, cells: cellData }; - }, - - render: function() { - this.$el.attr('data-id', this.model.id); - var tmplData = this.toTemplateJSON(); - tmplData = I18nMessages('recline', recline.View.translations).injectMustache(tmplData); - var html = Mustache.render(this.template, tmplData); - this.$el.html(html); - return this; - }, - - // =================== - // Cell Editor methods - - cellEditorTemplate: ' \ - \ - ', - - onEditClick: function(e) { - var editing = this.$el.find('.data-table-cell-editor-editor'); - if (editing.length > 0) { - editing.parents('.data-table-cell-value').html(editing.text()).siblings('.data-table-cell-edit').removeClass("hidden"); - } - $(e.target).addClass("hidden"); - var cell = $(e.target).siblings('.data-table-cell-value'); - cell.data("previousContents", cell.text()); - - var tmplData = I18nMessages('recline', recline.View.translations).injectMustache({value: cell.text()}); - var output = Mustache.render(this.cellEditorTemplate, tmplData); - cell.html(output); - }, - - onEditorOK: function(e) { - var self = this; - var cell = $(e.target); - var rowId = cell.parents('tr').attr('data-id'); - var field = cell.parents('td').attr('data-field'); - var newValue = cell.parents('.data-table-cell-editor').find('.data-table-cell-editor-editor').val(); - var newData = {}; - newData[field] = newValue; - this.model.set(newData); - - var fmt = I18nMessages('recline', recline.View.translations); - this.trigger('recline:flash', {message: fmt.t("Updating_row") + "...", loader: true}); - this.model.save().then(function(response) { - this.trigger('recline:flash', {message: fmt.t("Row_updated_successfully"), category: 'success'}); - }) - .fail(function() { - this.trigger('recline:flash', { - message: fmt.t('Error_saving_row'), - category: 'error', - persist: true - }); - }); - }, - - onEditorCancel: function(e) { - var cell = $(e.target).parents('.data-table-cell-value'); - cell.html(cell.data('previousContents')).siblings('.data-table-cell-edit').removeClass("hidden"); - } -}); - -})(jQuery, recline.View); diff --git a/src/view.map.js b/src/view.map.js deleted file mode 100644 index 75d1576f..00000000 --- a/src/view.map.js +++ /dev/null @@ -1,687 +0,0 @@ -/*jshint multistr:true */ - -this.recline = this.recline || {}; -this.recline.View = this.recline.View || {}; - -(function($, my) { - "use strict"; -// ## Map view for a Dataset using Leaflet mapping library. -// -// This view allows to plot gereferenced records on a map. The location -// information can be provided in 2 ways: -// -// 1. Via a single field. This field must be either a geo_point or -// [GeoJSON](http://geojson.org) object -// 2. Via two fields with latitude and longitude coordinates. -// -// Which fields in the data these correspond to can be configured via the state -// (and are guessed if no info is provided). -// -// Initialization arguments are as standard for Dataset Views. State object may -// have the following (optional) configuration options: -// -//
-//   {
-//     // geomField if specified will be used in preference to lat/lon
-//     geomField: {id of field containing geometry in the dataset}
-//     lonField: {id of field containing longitude in the dataset}
-//     latField: {id of field containing latitude in the dataset}
-//     autoZoom: true,
-//     // use cluster support
-//     // cluster: true = always on
-//     // cluster: false = always off
-//     cluster: false
-//   }
-// 
-// -// Useful attributes to know about (if e.g. customizing) -// -// * map: the Leaflet map (L.Map) -// * features: Leaflet GeoJSON layer containing all the features (L.GeoJSON) -my.Map = Backbone.View.extend({ - template: ' \ -
\ -
\ -
\ -', - - // These are the default (case-insensitive) names of field that are used if found. - // If not found, the user will need to define the fields via the editor. - latitudeFieldNames: ['lat','latitude'], - longitudeFieldNames: ['lon','longitude'], - geometryFieldNames: ['geojson', 'geom','the_geom','geometry','spatial','location', 'geo', 'lonlat'], - - initialize: function(options) { - var self = this; - this.options = options; - this.visible = this.$el.is(':visible'); - this.mapReady = false; - // this will be the Leaflet L.Map object (setup below) - this.map = null; - - var stateData = _.extend({ - geomField: null, - lonField: null, - latField: null, - autoZoom: true, - cluster: false - }, - options.state - ); - this.state = new recline.Model.ObjectState(stateData); - - this._clusterOptions = { - zoomToBoundsOnClick: true, - //disableClusteringAtZoom: 10, - maxClusterRadius: 80, - singleMarkerMode: false, - skipDuplicateAddTesting: true, - animateAddingMarkers: false - }; - - // Listen to changes in the fields - this.listenTo(this.model.fields, 'change', function() { - self._setupGeometryField(); - self.render(); - }); - - // Listen to changes in the records - this.listenTo(this.model.records, 'add', function(doc){self.redraw('add',doc);}); - this.listenTo(this.model.records, 'change', function(doc){ - self.redraw('remove',doc); - self.redraw('add',doc); - }); - this.listenTo(this.model.records, 'remove', function(doc){self.redraw('remove',doc);}); - this.listenTo(this.model.records, 'reset', function(){self.redraw('reset');}); - - this.menu = new my.MapMenu({ - model: this.model, - state: this.state.toJSON() - }); - this.listenTo(this.menu.state, 'change', function() { - self.state.set(self.menu.state.toJSON()); - self.redraw(); - }); - this.listenTo(this.state, 'change', function() { - self.redraw(); - }); - this.elSidebar = this.menu.$el; - }, - - // ## Customization Functions - // - // The following methods are designed for overriding in order to customize - // behaviour - - // ### infobox - // - // Function to create infoboxes used in popups. The default behaviour is very simple and just lists all attributes. - // - // Users should override this function to customize behaviour i.e. - // - // view = new View({...}); - // view.infobox = function(record) { - // ... - // } - infobox: function(record) { - var html = ''; - for (var key in record.attributes){ - if (!(this.state.get('geomField') && key == this.state.get('geomField'))){ - html += '
' + key + ': '+ record.attributes[key] + '
'; - } - } - return html; - }, - - // Options to use for the [Leaflet GeoJSON layer](http://leaflet.cloudmade.com/reference.html#geojson) - // See also - // - // e.g. - // - // pointToLayer: function(feature, latLng) - // onEachFeature: function(feature, layer) - // - // See defaults for examples - geoJsonLayerOptions: { - // pointToLayer function to use when creating points - // - // Default behaviour shown here is to create a marker using the - // popupContent set on the feature properties (created via infobox function - // during feature generation) - // - // NB: inside pointToLayer `this` will be set to point to this map view - // instance (which allows e.g. this.markers to work in this default case) - pointToLayer: function (feature, latlng) { - var marker = new L.Marker(latlng); - marker.bindPopup(feature.properties.popupContent); - // this is for cluster case - this.markers.addLayer(marker); - return marker; - }, - // onEachFeature default which adds popup in - onEachFeature: function(feature, layer) { - if (feature.properties && feature.properties.popupContent) { - layer.bindPopup(feature.properties.popupContent); - } - } - }, - - // END: Customization section - // ---- - - // ### Public: Adds the necessary elements to the page. - // - // Also sets up the editor fields and the map if necessary. - render: function() { - var self = this; - var tmplData = I18nMessages('recline', recline.View.translations).injectMustache(this.model.toTemplateJSON()); - var htmls = Mustache.render(this.template, tmplData); - this.$el.html(htmls); - this.$map = this.$el.find('.panel.map'); - this.redraw(); - return this; - }, - - // ### Public: Redraws the features on the map according to the action provided - // - // Actions can be: - // - // * reset: Clear all features - // * add: Add one or n features (records) - // * remove: Remove one or n features (records) - // * refresh: Clear existing features and add all current records - redraw: function(action, doc){ - var self = this; - action = action || 'refresh'; - // try to set things up if not already - if (!self._geomReady()){ - self._setupGeometryField(); - } - if (!self.mapReady){ - self._setupMap(); - } - - if (this._geomReady() && this.mapReady){ - // removing ad re-adding the layer enables faster bulk loading - this.map.removeLayer(this.features); - this.map.removeLayer(this.markers); - - var countBefore = 0; - this.features.eachLayer(function(){countBefore++;}); - - if (action == 'refresh' || action == 'reset') { - this.features.clearLayers(); - // recreate cluster group because of issues with clearLayer - this.map.removeLayer(this.markers); - this.markers = new L.MarkerClusterGroup(this._clusterOptions); - this._add(this.model.records.models); - } else if (action == 'add' && doc){ - this._add(doc); - } else if (action == 'remove' && doc){ - this._remove(doc); - } - - // this must come before zooming! - // if not: errors when using e.g. circle markers like - // "Cannot call method 'project' of undefined" - if (this.state.get('cluster')) { - this.map.addLayer(this.markers); - } else { - this.map.addLayer(this.features); - } - - if (this.state.get('autoZoom')){ - if (this.visible){ - this._zoomToFeatures(); - } else { - this._zoomPending = true; - } - } - } - }, - - show: function() { - // If the div was hidden, Leaflet needs to recalculate some sizes - // to display properly - if (this.map){ - this.map.invalidateSize(); - if (this._zoomPending && this.state.get('autoZoom')) { - this._zoomToFeatures(); - this._zoomPending = false; - } - } - this.visible = true; - }, - - hide: function() { - this.visible = false; - }, - - _geomReady: function() { - return Boolean(this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField'))); - }, - - // Private: Add one or n features to the map - // - // For each record passed, a GeoJSON geometry will be extracted and added - // to the features layer. If an exception is thrown, the process will be - // stopped and an error notification shown. - // - // Each feature will have a popup associated with all the record fields. - // - _add: function(docs){ - var self = this; - - if (!(docs instanceof Array)) docs = [docs]; - - var count = 0; - var wrongSoFar = 0; - _.every(docs, function(doc){ - count += 1; - var feature = self._getGeometryFromRecord(doc); - if (typeof feature === 'undefined' || feature === null){ - // Empty field - return true; - } else if (feature instanceof Object){ - feature.properties = { - popupContent: self.infobox(doc), - // Add a reference to the model id, which will allow us to - // link this Leaflet layer to a Recline doc - cid: doc.cid - }; - - try { - self.features.addData(feature); - } catch (except) { - wrongSoFar += 1; - var msg = 'Wrong geometry value'; - if (except.message) msg += ' (' + except.message + ')'; - if (wrongSoFar <= 10) { - self.trigger('recline:flash', {message: msg, category:'error'}); - } - } - } else { - wrongSoFar += 1; - if (wrongSoFar <= 10) { - self.trigger('recline:flash', {message: 'Wrong geometry value', category:'error'}); - } - } - return true; - }); - }, - - // Private: Remove one or n features from the map - // - _remove: function(docs){ - - var self = this; - - if (!(docs instanceof Array)) docs = [docs]; - - _.each(docs,function(doc){ - for (var key in self.features._layers){ - if (self.features._layers[key].feature.geometry.properties.cid == doc.cid){ - self.features.removeLayer(self.features._layers[key]); - } - } - }); - - }, - - // Private: convert DMS coordinates to decimal - // - // north and east are positive, south and west are negative - // - _parseCoordinateString: function(coord){ - if (typeof(coord) != 'string') { - return(parseFloat(coord)); - } - var dms = coord.split(/[^-?\.\d\w]+/); - var deg = 0; var m = 0; - var toDeg = [1, 60, 3600]; // conversion factors for Deg, min, sec - var i; - for (i = 0; i < dms.length; ++i) { - if (isNaN(parseFloat(dms[i]))) { - continue; - } - deg += parseFloat(dms[i]) / toDeg[m]; - m += 1; - } - if (coord.match(/[SW]/)) { - deg = -1*deg; - } - return(deg); - }, - - // Private: Return a GeoJSON geomtry extracted from the record fields - // - _getGeometryFromRecord: function(doc){ - if (this.state.get('geomField')){ - var value = doc.get(this.state.get('geomField')); - if (typeof(value) === 'string'){ - // We *may* have a GeoJSON string representation - try { - value = $.parseJSON(value); - } catch(e) {} - } - if (typeof(value) === 'string') { - value = value.replace('(', '').replace(')', ''); - var parts = value.split(','); - var lat = this._parseCoordinateString(parts[0]); - var lon = this._parseCoordinateString(parts[1]); - - if (!isNaN(lon) && !isNaN(parseFloat(lat))) { - return { - "type": "Point", - "coordinates": [lon, lat] - }; - } else { - return null; - } - } else if (value && _.isArray(value)) { - // [ lon, lat ] - return { - "type": "Point", - "coordinates": [value[0], value[1]] - }; - } else if (value && value.lat) { - // of form { lat: ..., lon: ...} - return { - "type": "Point", - "coordinates": [value.lon || value.lng, value.lat] - }; - } - // We o/w assume that contents of the field are a valid GeoJSON object - return value; - } else if (this.state.get('lonField') && this.state.get('latField')){ - // We'll create a GeoJSON like point object from the two lat/lon fields - var lon = doc.get(this.state.get('lonField')); - var lat = doc.get(this.state.get('latField')); - lon = this._parseCoordinateString(lon); - lat = this._parseCoordinateString(lat); - - if (!isNaN(parseFloat(lon)) && !isNaN(parseFloat(lat))) { - return { - type: 'Point', - coordinates: [lon,lat] - }; - } - } - return null; - }, - - // Private: Check if there is a field with GeoJSON geometries or alternatively, - // two fields with lat/lon values. - // - // If not found, the user can define them via the UI form. - _setupGeometryField: function(){ - // should not overwrite if we have already set this (e.g. explicitly via state) - if (!this._geomReady()) { - this.state.set({ - geomField: this._checkField(this.geometryFieldNames), - latField: this._checkField(this.latitudeFieldNames), - lonField: this._checkField(this.longitudeFieldNames) - }); - this.menu.state.set(this.state.toJSON()); - } - }, - - // Private: Check if a field in the current model exists in the provided - // list of names. - // - // - _checkField: function(fieldNames){ - var field; - var modelFieldNames = this.model.fields.pluck('id'); - for (var i = 0; i < fieldNames.length; i++){ - for (var j = 0; j < modelFieldNames.length; j++){ - if (modelFieldNames[j].toLowerCase() == fieldNames[i].toLowerCase()) - return modelFieldNames[j]; - } - } - return null; - }, - - // Private: Zoom to map to current features extent if any, or to the full - // extent if none. - // - _zoomToFeatures: function(){ - var bounds = this.features.getBounds(); - if (bounds && bounds.getNorthEast() && bounds.getSouthWest()){ - this.map.fitBounds(bounds); - } else { - this.map.setView([0, 0], 2); - } - }, - - // Private: Sets up the Leaflet map control and the features layer. - // - // The map uses a base layer from [Stamen](http://maps.stamen.com) based - // on [OpenStreetMap data](http://openstreetmap.org) by default, but it can - // be configured passing the `mapTilesURL` and `mapTilesAttribution` options - // (`mapTilesSubdomains` is also supported), eg: - // - // view = new recline.View.Map({ - // model: dataset, - // mapTilesURL: '//{s}.tiles.mapbox.com/v4/mapbox.mapbox-streets-v7/{z}/{x}/{y}.png?access_token=pk.XXXX', - // mapTilesAttribution: '© MapBox etc..', - // mapTilesSubdomains: 'ab' - // }) - // - // - _setupMap: function(){ - var self = this; - this.map = new L.Map(this.$map.get(0)); - var mapUrl = this.options.mapTilesURL || 'https://stamen-tiles-{s}.a.ssl.fastly.net/terrain/{z}/{x}/{y}.png'; - var attribution = this.options.mapTilesAttribution ||'Map tiles by Stamen Design (CC BY 3.0). Data by OpenStreetMap (CC BY SA)'; - var subdomains = this.options.mapTilesSubdomains || 'abc'; - - var bg = new L.TileLayer(mapUrl, {maxZoom: 19, attribution: attribution, subdomains: subdomains}); - this.map.addLayer(bg); - - this.markers = new L.MarkerClusterGroup(this._clusterOptions); - - // rebind this (as needed in e.g. default case above) - this.geoJsonLayerOptions.pointToLayer = _.bind( - this.geoJsonLayerOptions.pointToLayer, - this); - this.features = new L.GeoJSON(null, this.geoJsonLayerOptions); - - this.map.setView([0, 0], 2); - - this.mapReady = true; - }, - - // Private: Helper function to select an option from a select list - // - _selectOption: function(id,value){ - var options = $('.' + id + ' > select > option'); - if (options){ - options.each(function(opt){ - if (this.value == value) { - $(this).attr('selected','selected'); - return false; - } - }); - } - } -}); - -my.MapMenu = Backbone.View.extend({ - className: 'editor', - - template: ' \ -
\ -
\ -
\ - {{t.map_mapping}}: \ - \ - \ -
\ -
\ - \ -
\ - \ -
\ - \ -
\ - \ -
\ -
\ - \ -
\ -
\ - \ -
\ -
\ - \ - \ -
\ - \ -
\ - ', - - // Define here events for UI elements - events: { - 'click .editor-update-map': 'onEditorSubmit', - 'change .editor-field-type': 'onFieldTypeChange', - 'click #editor-auto-zoom': 'onAutoZoomChange', - 'click #editor-cluster': 'onClusteringChange' - }, - - initialize: function(options) { - var self = this; - _.bindAll(this, 'render'); - this.listenTo(this.model.fields, 'change', this.render); - this.state = new recline.Model.ObjectState(options.state); - this.listenTo(this.state, 'change', this.render); - this.render(); - }, - - // ### Public: Adds the necessary elements to the page. - // - // Also sets up the editor fields and the map if necessary. - render: function() { - var self = this; - var tmplData = I18nMessages('recline', recline.View.translations).injectMustache(this.model.toTemplateJSON()); - var htmls = Mustache.render(this.template, tmplData); - this.$el.html(htmls); - - if (this._geomReady() && this.model.fields.length){ - if (this.state.get('geomField')){ - this._selectOption('editor-geom-field',this.state.get('geomField')); - this.$el.find('#editor-field-type-geom').attr('checked','checked').change(); - } else{ - this._selectOption('editor-lon-field',this.state.get('lonField')); - this._selectOption('editor-lat-field',this.state.get('latField')); - this.$el.find('#editor-field-type-latlon').attr('checked','checked').change(); - } - } - if (this.state.get('autoZoom')) { - this.$el.find('#editor-auto-zoom').attr('checked', 'checked'); - } else { - this.$el.find('#editor-auto-zoom').removeAttr('checked'); - } - if (this.state.get('cluster')) { - this.$el.find('#editor-cluster').attr('checked', 'checked'); - } else { - this.$el.find('#editor-cluster').removeAttr('checked'); - } - return this; - }, - - _geomReady: function() { - return Boolean(this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField'))); - }, - - // ## UI Event handlers - // - - // Public: Update map with user options - // - // Right now the only configurable option is what field(s) contains the - // location information. - // - onEditorSubmit: function(e){ - e.preventDefault(); - if (this.$el.find('#editor-field-type-geom').attr('checked')){ - this.state.set({ - geomField: this.$el.find('.editor-geom-field > select > option:selected').val(), - lonField: null, - latField: null - }); - } else { - this.state.set({ - geomField: null, - lonField: this.$el.find('.editor-lon-field > select > option:selected').val(), - latField: this.$el.find('.editor-lat-field > select > option:selected').val() - }); - } - return false; - }, - - // Public: Shows the relevant select lists depending on the location field - // type selected. - // - onFieldTypeChange: function(e){ - if (e.target.value == 'geom'){ - this.$el.find('.editor-field-type-geom').show(); - this.$el.find('.editor-field-type-latlon').hide(); - } else { - this.$el.find('.editor-field-type-geom').hide(); - this.$el.find('.editor-field-type-latlon').show(); - } - }, - - onAutoZoomChange: function(e){ - this.state.set({autoZoom: !this.state.get('autoZoom')}); - }, - - onClusteringChange: function(e){ - this.state.set({cluster: !this.state.get('cluster')}); - }, - - // Private: Helper function to select an option from a select list - // - _selectOption: function(id,value){ - var options = this.$el.find('.' + id + ' > select > option'); - if (options){ - options.each(function(opt){ - if (this.value == value) { - $(this).attr('selected','selected'); - return false; - } - }); - } - } -}); - -})(jQuery, recline.View); diff --git a/src/view.multiview.js b/src/view.multiview.js deleted file mode 100644 index 1a263714..00000000 --- a/src/view.multiview.js +++ /dev/null @@ -1,568 +0,0 @@ -/*jshint multistr:true */ - -// Standard JS module setup -this.recline = this.recline || {}; -this.recline.View = this.recline.View || {}; - -(function($, my) { - "use strict"; -// ## MultiView -// -// Manage multiple views together along with query editor etc. Usage: -// -//
-// var myExplorer = new recline.View.MultiView({
-//   model: {{recline.Model.Dataset instance}}
-//   el: {{an existing dom element}}
-//   views: {{dataset views}}
-//   state: {{state configuration -- see below}}
-// });
-// 
-// -// ### Parameters -// -// **model**: (required) recline.model.Dataset instance. -// -// **el**: (required) DOM element to bind to. NB: the element already -// being in the DOM is important for rendering of some subviews (e.g. -// Graph). -// -// **views**: (optional) the dataset views (Grid, Graph etc) for -// MultiView to show. This is an array of view hashes. If not provided -// initialize with (recline.View.)Grid, Graph, and Map views (with obvious id -// and labels!). -// -//
-// var views = [
-//   {
-//     id: 'grid', // used for routing
-//     label: 'Grid', // used for view switcher
-//     view: new recline.View.Grid({
-//       model: dataset
-//     })
-//   },
-//   {
-//     id: 'graph',
-//     label: 'Graph',
-//     view: new recline.View.Graph({
-//       model: dataset
-//     })
-//   }
-// ];
-// 
-// -// **sidebarViews**: (optional) the sidebar views (Filters, Fields) for -// MultiView to show. This is an array of view hashes. If not provided -// initialize with (recline.View.)FilterEditor and Fields views (with obvious -// id and labels!). -// -//
-// var sidebarViews = [
-//   {
-//     id: 'filterEditor', // used for routing
-//     label: 'Filters', // used for view switcher
-//     view: new recline.View.FilterEditor({
-//       model: dataset
-//     })
-//   },
-//   {
-//     id: 'fieldsView',
-//     label: 'Fields',
-//     view: new recline.View.Fields({
-//       model: dataset
-//     })
-//   }
-// ];
-// 
-// -// **state**: standard state config for this view. This state is slightly -// special as it includes config of many of the subviews. -// -//
-// var state = {
-//     query: {dataset query state - see dataset.queryState object}
-//     'view-{id1}': {view-state for this view}
-//     'view-{id2}': {view-state for }
-//     ...
-//     // Explorer
-//     currentView: id of current view (defaults to first view if not specified)
-//     readOnly: (default: false) run in read-only mode
-// }
-// 
-// -// Note that at present we do *not* serialize information about the actual set -// of views in use -- e.g. those specified by the views argument -- but instead -// expect either that the default views are fine or that the client to have -// initialized the MultiView with the relevant views themselves. -my.MultiView = Backbone.View.extend({ - template: ' \ -
\ -
\ - \ -
\ - \ -
\ - {{#t.num_records}}{recordCount} {recordCount, plural, =1{record} other{records}}{{/t.num_records}}\ -
\ - \ -
\ -
\ -
\ -
\ -
\ - ', - events: { - 'click .menu-right button': '_onMenuClick', - 'click .navigation button': '_onSwitchView' - }, - - initialize: function(options) { - var self = this; - this._setupState(options.state); - var fmt = I18nMessages('recline', recline.View.translations); - - // Hash of 'page' views (i.e. those for whole page) keyed by page name - if (options.views) { - this.pageViews = options.views; - } else { - this.pageViews = [{ - id: 'grid', - label: fmt.t('Grid'), - view: new my.SlickGrid({ - model: this.model, - state: this.state.get('view-grid') - }) - }, { - id: 'graph', - label: fmt.t('Graph'), - view: new my.Graph({ - model: this.model, - state: this.state.get('view-graph') - }) - }, { - id: 'map', - label: fmt.t('Map'), - view: new my.Map({ - model: this.model, - state: this.state.get('view-map') - }) - }, { - id: 'timeline', - label: fmt.t('Timeline'), - view: new my.Timeline({ - model: this.model, - state: this.state.get('view-timeline') - }) - }]; - } - // Hashes of sidebar elements - if(options.sidebarViews) { - this.sidebarViews = options.sidebarViews; - } else { - this.sidebarViews = [{ - id: 'filterEditor', - label: fmt.t('Filters'), - view: new my.FilterEditor({ - model: this.model - }) - }, { - id: 'fieldsView', - label: fmt.t('Fields'), - view: new my.Fields({ - model: this.model - }) - }]; - } - // these must be called after pageViews are created - this.render(); - this._bindStateChanges(); - this._bindFlashNotifications(); - // now do updates based on state (need to come after render) - if (this.state.get('readOnly')) { - this.setReadOnly(); - } - if (this.state.get('currentView')) { - this.updateNav(this.state.get('currentView')); - } else { - this.updateNav(this.pageViews[0].id); - } - this._showHideSidebar(); - - this.listenTo(this.model, 'query:start', function() { - self.notify({loader: true, persist: true}); - }); - this.listenTo(this.model, 'query:done', function() { - self.clearNotifications(); - self.$el.find('.doc-count').text(self.model.recordCount || fmt.t('Unknown')); - }); - this.listenTo(this.model, 'query:fail', function(error) { - self.clearNotifications(); - var msg = ''; - if (typeof(error) == 'string') { - msg = error; - } else if (typeof(error) == 'object') { - if (error.title) { - msg = error.title + ': '; - } - if (error.message) { - msg += error.message; - } - } else { - msg = fmt.t('backend_error', {}, 'There was an error querying the backend'); - } - self.notify({message: msg, category: 'error', persist: true}); - }); - - // retrieve basic data like fields etc - // note this.model and dataset returned are the same - // TODO: set query state ...? - this.model.queryState.set(self.state.get('query'), {silent: true}); - }, - - setReadOnly: function() { - this.$el.addClass('recline-read-only'); - }, - - render: function() { - var tmplData = this.model.toTemplateJSON(); - tmplData.views = this.pageViews; - tmplData.sidebarViews = this.sidebarViews; - tmplData = I18nMessages('recline', recline.View.translations).injectMustache(tmplData); - var template = Mustache.render(this.template, tmplData); - this.$el.html(template); - - // now create and append other views - var $dataViewContainer = this.$el.find('.data-view-container'); - var $dataSidebar = this.$el.find('.data-view-sidebar'); - - // the main views - _.each(this.pageViews, function(view, pageName) { - view.view.render(); - if (view.view.redraw) { - view.view.redraw(); - } - $dataViewContainer.append(view.view.el); - if (view.view.elSidebar) { - $dataSidebar.append(view.view.elSidebar); - } - }); - - _.each(this.sidebarViews, function(view) { - this['$'+view.id] = view.view.$el; - $dataSidebar.append(view.view.el); - }, this); - - this.pager = new recline.View.Pager({ - model: this.model - }); - this.$el.find('.recline-results-info').after(this.pager.el); - - this.queryEditor = new recline.View.QueryEditor({ - model: this.model.queryState - }); - this.$el.find('.query-editor-here').append(this.queryEditor.el); - - }, - - remove: function () { - _.each(this.pageViews, function (view) { - view.view.remove(); - }); - _.each(this.sidebarViews, function (view) { - view.view.remove(); - }); - this.pager.remove(); - this.queryEditor.remove(); - Backbone.View.prototype.remove.apply(this, arguments); - }, - - // hide the sidebar if empty - _showHideSidebar: function() { - var $dataSidebar = this.$el.find('.data-view-sidebar'); - var visibleChildren = $dataSidebar.children().filter(function() { - return $(this).css("display") != "none"; - }).length; - - if (visibleChildren > 0) { - $dataSidebar.show(); - } else { - $dataSidebar.hide(); - } - }, - - updateNav: function(pageName) { - this.$el.find('.navigation button').removeClass('active'); - var $el = this.$el.find('.navigation button[data-view="' + pageName + '"]'); - $el.addClass('active'); - - // add/remove sidebars and hide inactive views - _.each(this.pageViews, function(view, idx) { - if (view.id === pageName) { - view.view.$el.show(); - if (view.view.elSidebar) { - view.view.elSidebar.show(); - } - } else { - view.view.$el.hide(); - if (view.view.elSidebar) { - view.view.elSidebar.hide(); - } - if (view.view.hide) { - view.view.hide(); - } - } - }); - - this._showHideSidebar(); - - // call view.view.show after sidebar visibility has been determined so - // that views can correctly calculate their maximum width - _.each(this.pageViews, function(view, idx) { - if (view.id === pageName) { - if (view.view.show) { - view.view.show(); - } - } - }); - }, - - _onMenuClick: function(e) { - e.preventDefault(); - var action = $(e.target).attr('data-action'); - this['$'+action].toggle(); - this._showHideSidebar(); - }, - - _onSwitchView: function(e) { - e.preventDefault(); - var viewName = $(e.target).attr('data-view'); - this.updateNav(viewName); - this.state.set({currentView: viewName}); - }, - - // create a state object for this view and do the job of - // - // a) initializing it from both data passed in and other sources (e.g. hash url) - // - // b) ensure the state object is updated in responese to changes in subviews, query etc. - _setupState: function(initialState) { - var self = this; - // get data from the query string / hash url plus some defaults - var qs = my.parseHashQueryString(); - var query = qs.reclineQuery; - query = query ? JSON.parse(query) : self.model.queryState.toJSON(); - // backwards compatability (now named view-graph but was named graph) - var graphState = qs['view-graph'] || qs.graph; - graphState = graphState ? JSON.parse(graphState) : {}; - - // now get default data + hash url plus initial state and initial our state object with it - var stateData = _.extend({ - query: query, - 'view-graph': graphState, - backend: this.model.backend.__type__, - url: this.model.get('url'), - dataset: this.model.toJSON(), - currentView: null, - readOnly: false - }, - initialState); - this.state = new recline.Model.ObjectState(stateData); - }, - - _bindStateChanges: function() { - var self = this; - // finally ensure we update our state object when state of sub-object changes so that state is always up to date - this.listenTo(this.model.queryState, 'change', function() { - self.state.set({query: self.model.queryState.toJSON()}); - }); - _.each(this.pageViews, function(pageView) { - if (pageView.view.state && pageView.view.state.bind) { - var update = {}; - update['view-' + pageView.id] = pageView.view.state.toJSON(); - self.state.set(update); - self.listenTo(pageView.view.state, 'change', function() { - var update = {}; - update['view-' + pageView.id] = pageView.view.state.toJSON(); - // had problems where change not being triggered for e.g. grid view so let's do it explicitly - self.state.set(update, {silent: true}); - self.state.trigger('change'); - }); - } - }); - }, - - _bindFlashNotifications: function() { - var self = this; - _.each(this.pageViews, function(pageView) { - self.listenTo(pageView.view, 'recline:flash', function(flash) { - self.notify(flash); - }); - }); - }, - - // ### notify - // - // Create a notification (a div.alert in div.alert-messsages) using provided - // flash object. Flash attributes (all are optional): - // - // * message: message to show. - // * category: warning (default), success, error - // * persist: if true alert is persistent, o/w hidden after 3s (default = false) - // * loader: if true show loading spinner - notify: function(flash) { - var tmplData = _.extend({ - message: 'Loading', - category: 'warning', - loader: false - }, - flash - ); - var _template; - if (tmplData.loader) { - _template = ' \ -
\ - {{message}} \ -   \ -
'; - } else { - _template = ' \ -
Ɨ \ - {{message}} \ -
'; - } - var _templated = $(Mustache.render(_template, tmplData)); - _templated = $(_templated).appendTo($('.recline-data-explorer .alert-messages')); - if (!flash.persist) { - setTimeout(function() { - $(_templated).fadeOut(1000, function() { - $(this).remove(); - }); - }, 1000); - } - }, - - // ### clearNotifications - // - // Clear all existing notifications - clearNotifications: function() { - var $notifications = $('.recline-data-explorer .alert-messages .alert'); - $notifications.fadeOut(1500, function() { - $(this).remove(); - }); - } -}); - -// ### MultiView.restore -// -// Restore a MultiView instance from a serialized state including the associated dataset -// -// This inverts the state serialization process in Multiview -my.MultiView.restore = function(state) { - // hack-y - restoring a memory dataset does not mean much ... (but useful for testing!) - var datasetInfo; - if (state.backend === 'memory') { - datasetInfo = { - backend: 'memory', - records: [{stub: 'this is a stub dataset because we do not restore memory datasets'}] - }; - } else { - datasetInfo = _.extend({ - url: state.url, - backend: state.backend - }, - state.dataset - ); - } - var dataset = new recline.Model.Dataset(datasetInfo); - var explorer = new my.MultiView({ - model: dataset, - state: state - }); - return explorer; -}; - -// ## Miscellaneous Utilities -var urlPathRegex = /^([^?]+)(\?.*)?/; - -// Parse the Hash section of a URL into path and query string -my.parseHashUrl = function(hashUrl) { - var parsed = urlPathRegex.exec(hashUrl); - if (parsed === null) { - return {}; - } else { - return { - path: parsed[1], - query: parsed[2] || '' - }; - } -}; - -// Parse a URL query string (?xyz=abc...) into a dictionary. -my.parseQueryString = function(q) { - if (!q) { - return {}; - } - var urlParams = {}, - e, d = function (s) { - return unescape(s.replace(/\+/g, " ")); - }, - r = /([^&=]+)=?([^&]*)/g; - - if (q && q.length && q[0] === '?') { - q = q.slice(1); - } - while (e = r.exec(q)) { - // TODO: have values be array as query string allow repetition of keys - urlParams[d(e[1])] = d(e[2]); - } - return urlParams; -}; - -// Parse the query string out of the URL hash -my.parseHashQueryString = function() { - var q = my.parseHashUrl(window.location.hash).query; - return my.parseQueryString(q); -}; - -// Compse a Query String -my.composeQueryString = function(queryParams) { - var queryString = '?'; - var items = []; - $.each(queryParams, function(key, value) { - if (typeof(value) === 'object') { - value = JSON.stringify(value); - } - items.push(key + '=' + encodeURIComponent(value)); - }); - queryString += items.join('&'); - return queryString; -}; - -my.getNewHashForQueryString = function(queryParams) { - var queryPart = my.composeQueryString(queryParams); - if (window.location.hash) { - // slice(1) to remove # at start - return window.location.hash.split('?')[0].slice(1) + queryPart; - } else { - return queryPart; - } -}; - -my.setHashQueryString = function(queryParams) { - window.location.hash = my.getNewHashForQueryString(queryParams); -}; - -})(jQuery, recline.View); - diff --git a/src/view.slickgrid.js b/src/view.slickgrid.js deleted file mode 100644 index ad6b6bf0..00000000 --- a/src/view.slickgrid.js +++ /dev/null @@ -1,602 +0,0 @@ -/*jshint multistr:true */ - -this.recline = this.recline || {}; -this.recline.View = this.recline.View || {}; - -(function($, my) { - "use strict"; - -// ## SlickGrid Dataset View -// -// Provides a tabular view on a Dataset, based on SlickGrid. -// -// https://github.com/mleibman/SlickGrid -// -// Initialize it with a `recline.Model.Dataset`. -// -// Additional options to drive SlickGrid grid can be given through state. -// The following keys allow for customization: -// * gridOptions: to add options at grid level -// * columnsEditor: to add editor for editable columns -// -// For example: -// var grid = new recline.View.SlickGrid({ -// model: dataset, -// el: $el, -// state: { -// gridOptions: { -// editable: true, -// enableAddRow: true -// // Enable support for row delete -// enabledDelRow: true, -// // Enable support for row Reorder -// enableReOrderRow:true, -// ... -// }, -// columnsEditor: [ -// {column: 'date', editor: Slick.Editors.Date }, -// {column: 'title', editor: Slick.Editors.Text} -// ] -// } -// }); -//// NB: you need an explicit height on the element for slickgrid to work -my.SlickGrid = Backbone.View.extend({ - initialize: function(modelEtc) { - var self = this; - this.$el.addClass('recline-slickgrid'); - - // Template for row delete menu , change it if you don't love - this.templates = { - deleterow : '', - reorderrows: '
{{t.Reorder_row}}
' - }; - - _.bindAll(this, 'render', 'onRecordChanged'); - this.listenTo(this.model.records, 'add remove reset', this.render); - this.listenTo(this.model.records, 'change', this.onRecordChanged); - var state = _.extend({ - hiddenColumns: [], - columnsOrder: [], - columnsSort: {}, - columnsWidth: [], - columnsEditor: [], - options: {}, - fitColumns: false - }, modelEtc.state - - ); - this.state = new recline.Model.ObjectState(state); - this._slickHandler = new Slick.EventHandler(); - - //add menu for new row , check if enableAddRow is set to true or not set - if(this.state.get("gridOptions") - && this.state.get("gridOptions").enabledAddRow != undefined - && this.state.get("gridOptions").enabledAddRow == true ){ - this.editor = new my.GridControl() - this.elSidebar = this.editor.$el - this.listenTo(this.editor.state, 'change', function(){ - this.model.records.add(new recline.Model.Record()) - }); - } - }, - - onRecordChanged: function(record) { - // Ignore if the grid is not yet drawn - if (!this.grid) { - return; - } - // Let's find the row corresponding to the index - var row_index = this.grid.getData().getModelRow( record ); - this.grid.invalidateRow(row_index); - this.grid.getData().updateItem(record, row_index); - this.grid.render(); - }, - - render: function() { - var self = this; - var options = _.extend({ - enableCellNavigation: true, - enableColumnReorder: true, - explicitInitialization: true, - syncColumnCellResize: true, - forceFitColumns: this.state.get('fitColumns') - }, self.state.get('gridOptions')); - - // We need all columns, even the hidden ones, to show on the column picker - var columns = []; - var fmt = I18nMessages('recline', recline.View.translations); - - // custom formatter as default one escapes html - // plus this way we distinguish between rendering/formatting and computed value (so e.g. sort still works ...) - // row = row index, cell = cell index, value = value, columnDef = column definition, dataContext = full row values - var formatter = function(row, cell, value, columnDef, dataContext) { - if(columnDef.id == "del"){ - var formatted = Mustache.render(self.templates.deleterow, fmt.injectMustache({})); - return formatted; - } - if(columnDef.id == "#"){ - var formatted = Mustache.render(self.templates.reorderrows, fmt.injectMustache({})); - return formatted; - } - - var field = self.model.fields.get(columnDef.id); - if (field.renderer) { - return field.renderer(value, field, dataContext); - } else { - return value - } - }; - - // we need to be sure that user is entering a valid input , for exemple if - // field is date type and field.format ='YY-MM-DD', we should be sure that - // user enter a correct value - var validator = function(field) { - return function(value){ - if (field.type == "date" && isNaN(Date.parse(value))){ - return { - valid: false, - msg: fmt.t('date_required', {}, "A date is required, check field field-date-format") - }; - } else { - return {valid: true, msg :null } - } - } - }; - - // Add column for row reorder support - if (this.state.get("gridOptions") && this.state.get("gridOptions").enableReOrderRow == true) { - columns.push({ - id: "#", - name: "", - width: 22, - behavior: "selectAndMove", - selectable: false, - resizable: false, - cssClass: "recline-cell-reorder", - formatter: formatter - }) - } - // Add column for row delete support - if (this.state.get("gridOptions") && this.state.get("gridOptions").enabledDelRow == true) { - columns.push({ - id: 'del', - name: '', - field: 'del', - sortable: true, - width: 38, - formatter: formatter, - validator:validator - }) - } - - function sanitizeFieldName(name) { - return $('
').text(name).html(); - } - - _.each(this.model.fields.toJSON(),function(field){ - var column = { - id: field.id, - name: sanitizeFieldName(field.label), - field: field.id, - sortable: true, - minWidth: 80, - formatter: formatter, - validator:validator(field) - }; - var widthInfo = _.find(self.state.get('columnsWidth'),function(c){return c.column === field.id;}); - if (widthInfo){ - column.width = widthInfo.width; - } - var editInfo = _.find(self.state.get('columnsEditor'),function(c){return c.column === field.id;}); - if (editInfo){ - column.editor = editInfo.editor; - } else { - // guess editor type - var typeToEditorMap = { - 'string': Slick.Editors.LongText, - 'integer': Slick.Editors.IntegerEditor, - 'number': Slick.Editors.Text, - // TODO: need a way to ensure we format date in the right way - // Plus what if dates are in distant past or future ... (?) - // 'date': Slick.Editors.DateEditor, - 'date': Slick.Editors.Text, - 'boolean': Slick.Editors.YesNoSelectEditor - // TODO: (?) percent ... - }; - if (field.type in typeToEditorMap) { - column.editor = typeToEditorMap[field.type] - } else { - column.editor = Slick.Editors.LongText; - } - } - columns.push(column); - }); - // Restrict the visible columns - var visibleColumns = _.filter(columns, function(column) { - return _.indexOf(self.state.get('hiddenColumns'), column.id) === -1; - }); - // Order them if there is ordering info on the state - if (this.state.get('columnsOrder') && this.state.get('columnsOrder').length > 0) { - visibleColumns = visibleColumns.sort(function(a,b){ - return _.indexOf(self.state.get('columnsOrder'),a.id) > _.indexOf(self.state.get('columnsOrder'),b.id) ? 1 : -1; - }); - columns = columns.sort(function(a,b){ - return _.indexOf(self.state.get('columnsOrder'),a.id) > _.indexOf(self.state.get('columnsOrder'),b.id) ? 1 : -1; - }); - } - - // Move hidden columns to the end, so they appear at the bottom of the - // column picker - var tempHiddenColumns = []; - for (var i = columns.length -1; i >= 0; i--){ - if (_.indexOf(_.pluck(visibleColumns,'id'),columns[i].id) === -1){ - tempHiddenColumns.push(columns.splice(i,1)[0]); - } - } - columns = columns.concat(tempHiddenColumns); - - // Transform a model object into a row - function toRow(m) { - var row = {}; - self.model.fields.each(function(field) { - var render = ""; - //when adding row from slickgrid the field value is undefined - if(!_.isUndefined(m.getFieldValueUnrendered(field))){ - render =m.getFieldValueUnrendered(field) - } - row[field.id] = render - }); - return row; - } - - function RowSet() { - var models = []; - var rows = []; - - this.push = function(model, row) { - models.push(model); - rows.push(row); - }; - - this.getLength = function() {return rows.length; }; - this.getItem = function(index) {return rows[index];}; - this.getItemMetadata = function(index) {return {};}; - this.getModel = function(index) {return models[index];}; - this.getModelRow = function(m) {return _.indexOf(models, m);}; - this.updateItem = function(m,i) { - rows[i] = toRow(m); - models[i] = m; - }; - } - - var data = new RowSet(); - - this.model.records.each(function(doc){ - data.push(doc, toRow(doc)); - }); - - this.grid = new Slick.Grid(this.el, data, visibleColumns, options); - // Column sorting - var sortInfo = this.model.queryState.get('sort'); - if (sortInfo){ - var column = sortInfo[0].field; - var sortAsc = sortInfo[0].order !== 'desc'; - this.grid.setSortColumn(column, sortAsc); - } - - if (this.state.get("gridOptions") && this.state.get("gridOptions").enableReOrderRow) { - this._setupRowReordering(); - } - - this._slickHandler.subscribe(this.grid.onSort, function(e, args){ - var order = (args.sortAsc) ? 'asc':'desc'; - var sort = [{ - field: args.sortCol.field, - order: order - }]; - self.model.query({sort: sort}); - }); - - this._slickHandler.subscribe(this.grid.onColumnsReordered, function(e, args){ - self.state.set({columnsOrder: _.pluck(self.grid.getColumns(),'id')}); - }); - - this.grid.onColumnsResized.subscribe(function(e, args){ - var columns = args.grid.getColumns(); - var defaultColumnWidth = args.grid.getOptions().defaultColumnWidth; - var columnsWidth = []; - _.each(columns,function(column){ - if (column.width != defaultColumnWidth){ - columnsWidth.push({column:column.id,width:column.width}); - } - }); - self.state.set({columnsWidth:columnsWidth}); - }); - - this._slickHandler.subscribe(this.grid.onCellChange, function (e, args) { - // We need to change the model associated value - var grid = args.grid; - var model = data.getModel(args.row); - var field = grid.getColumns()[args.cell].id; - var v = {}; - v[field] = args.item[field]; - model.set(v); - }); - this._slickHandler.subscribe(this.grid.onClick,function(e, args){ - //try catch , because this fail in qunit , but no - //error on browser. - try{e.preventDefault()}catch(e){} - - // The cell of grid that handle row delete is The first cell (0) if - // The grid ReOrder is not present ie enableReOrderRow == false - // else it is The the second cell (1) , because The 0 is now cell - // that handle row Reoder. - var cell =0 - if(self.state.get("gridOptions") - && self.state.get("gridOptions").enableReOrderRow != undefined - && self.state.get("gridOptions").enableReOrderRow == true ){ - cell =1 - } - if (args.cell == cell && self.state.get("gridOptions").enabledDelRow == true){ - // We need to delete the associated model - var model = data.getModel(args.row); - model.destroy() - } - }) ; - var columnpicker = new Slick.Controls.ColumnPicker(columns, this.grid, - _.extend(options,{state:this.state})); - if (self.visible){ - self.grid.init(); - self.rendered = true; - } else { - // Defer rendering until the view is visible - self.rendered = false; - } - return this; - }, - - // Row reordering support based on - // https://github.com/mleibman/SlickGrid/blob/gh-pages/examples/example9-row-reordering.html - _setupRowReordering: function() { - var self = this; - self.grid.setSelectionModel(new Slick.RowSelectionModel()); - - var moveRowsPlugin = new Slick.RowMoveManager({ - cancelEditOnDrag: true - }); - - moveRowsPlugin.onBeforeMoveRows.subscribe(function (e, data) { - for (var i = 0; i < data.rows.length; i++) { - // no point in moving before or after itself - if (data.rows[i] == data.insertBefore || data.rows[i] == data.insertBefore - 1) { - e.stopPropagation(); - return false; - } - } - return true; - }); - - moveRowsPlugin.onMoveRows.subscribe(function (e, args) { - var extractedRows = [], left, right; - var rows = args.rows; - var insertBefore = args.insertBefore; - - var data = self.model.records.toJSON() - left = data.slice(0, insertBefore); - right= data.slice(insertBefore, data.length); - - rows.sort(function(a,b) { return a-b; }); - - for (var i = 0; i < rows.length; i++) { - extractedRows.push(data[rows[i]]); - } - - rows.reverse(); - - for (var i = 0; i < rows.length; i++) { - var row = rows[i]; - if (row < insertBefore) { - left.splice(row, 1); - } else { - right.splice(row - insertBefore, 1); - } - } - - data = left.concat(extractedRows.concat(right)); - var selectedRows = []; - for (var i = 0; i < rows.length; i++) - selectedRows.push(left.length + i); - - self.model.records.reset(data) - - }); - //register The plugin to handle row Reorder - if(this.state.get("gridOptions") && this.state.get("gridOptions").enableReOrderRow) { - self.grid.registerPlugin(moveRowsPlugin); - } - }, - - remove: function () { - this._slickHandler.unsubscribeAll(); - Backbone.View.prototype.remove.apply(this, arguments); - }, - - show: function() { - // If the div is hidden, SlickGrid will calculate wrongly some - // sizes so we must render it explicitly when the view is visible - if (!this.rendered){ - if (!this.grid){ - this.render(); - } - this.grid.init(); - this.rendered = true; - } - this.visible = true; - }, - - hide: function() { - this.visible = false; - } -}); - -// Add new grid Control to display a new row add menu bouton -// It display a simple side-bar menu ,for user to add new -// row to grid -my.GridControl= Backbone.View.extend({ - className: "recline-row-add", - // Template for row edit menu , change it if you don't love - template: '

', - - initialize: function(options){ - var self = this; - _.bindAll(this, 'render'); - this.state = new recline.Model.ObjectState(); - this.render(); - }, - - render: function() { - var self = this; - - var tmplData = I18nMessages('recline', recline.View.translations).injectMustache({}); - var formatted = Mustache.render(this.template, tmplData); - this.$el.html(formatted); - }, - - events : { - "click .recline-row-add" : "addNewRow" - }, - - addNewRow : function(e){ - e.preventDefault() - this.state.trigger("change") - } -}); - -})(jQuery, recline.View); - -/* -* Context menu for the column picker, adapted from -* http://mleibman.github.com/SlickGrid/examples/example-grouping -* -*/ -(function ($) { - function SlickColumnPicker(columns, grid, options) { - var $menu; - var columnCheckboxes; - - var defaults = { - fadeSpeed:250 - }; - - function init() { - grid.onHeaderContextMenu.subscribe(handleHeaderContextMenu); - options = $.extend({}, defaults, options); - - $menu = $('