Merge branch 'master' into gh-pages
This commit is contained in:
commit
e78ddb7bb5
@ -35,6 +35,8 @@ Possible breaking changes:
|
||||
|
||||
* 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
|
||||
|
||||
|
||||
@ -5,10 +5,10 @@
|
||||
<link rel="stylesheet" href="vendor/slickgrid/2.0.1/slick.grid.css">
|
||||
|
||||
<!-- Recline CSS components -->
|
||||
<link rel="stylesheet" href="css/data-explorer.css">
|
||||
<link rel="stylesheet" href="css/grid.css">
|
||||
<link rel="stylesheet" href="css/graph.css">
|
||||
<link rel="stylesheet" href="css/map.css">
|
||||
<link rel="stylesheet" href="css/multiview.css">
|
||||
<!-- /Recline CSS components -->
|
||||
|
||||
<!-- 3rd party JS libraries -->
|
||||
|
||||
@ -19,11 +19,11 @@
|
||||
<link rel="stylesheet" href="../vendor/timeline/20120520/css/timeline.css">
|
||||
|
||||
<!-- Recline CSS components -->
|
||||
<link rel="stylesheet" href="../css/data-explorer.css">
|
||||
<link rel="stylesheet" href="../css/grid.css">
|
||||
<link rel="stylesheet" href="../css/slickgrid.css">
|
||||
<link rel="stylesheet" href="../css/graph.css">
|
||||
<link rel="stylesheet" href="../css/map.css">
|
||||
<link rel="stylesheet" href="../css/multiview.css">
|
||||
<!-- /Recline CSS components -->
|
||||
|
||||
<!-- Custom CSS for the Data Explorer Online App -->
|
||||
@ -49,7 +49,6 @@
|
||||
<!-- recline library -->
|
||||
<!-- in normal use would just the single recline.js library file. However, for testing it
|
||||
is easier to reference individual files. See built.html for example using just recline.js -->
|
||||
<script type="text/javascript" src="../src/util.js"></script>
|
||||
<script type="text/javascript" src="../src/costco.js"></script>
|
||||
<script type="text/javascript" src="../src/model.js"></script>
|
||||
<script type="text/javascript" src="../src/backend/base.js"></script>
|
||||
@ -58,13 +57,18 @@
|
||||
<script type="text/javascript" src="../src/backend/elasticsearch.js"></script>
|
||||
<script type="text/javascript" src="../src/backend/gdocs.js"></script>
|
||||
<script type="text/javascript" src="../src/backend/csv.js"></script>
|
||||
<script type="text/javascript" src="../src/view.js"></script>
|
||||
|
||||
<script type="text/javascript" src="../src/view-grid.js"></script>
|
||||
<script type="text/javascript" src="../src/view-slickgrid.js"></script>
|
||||
<script type="text/javascript" src="../src/view-transform-dialog.js"></script>
|
||||
<script type="text/javascript" src="../src/view-graph.js"></script>
|
||||
<script type="text/javascript" src="../src/view-map.js"></script>
|
||||
<script type="text/javascript" src="../src/view-transform-dialog.js"></script>
|
||||
<script type="text/javascript" src="../src/view-timeline.js"></script>
|
||||
<script type="text/javascript" src="../src/widget.pager.js"></script>
|
||||
<script type="text/javascript" src="../src/widget.queryeditor.js"></script>
|
||||
<script type="text/javascript" src="../src/widget.filtereditor.js"></script>
|
||||
<script type="text/javascript" src="../src/widget.facetviewer.js"></script>
|
||||
<script type="text/javascript" src="../src/view.multiview.js"></script>
|
||||
|
||||
<!-- non-library javascript specific to this demo -->
|
||||
<script type="text/javascript" src="js/app.js"></script>
|
||||
|
||||
@ -21,7 +21,7 @@ var ExplorerApp = Backbone.View.extend({
|
||||
this.router.route(/explorer/, 'explorer', this.viewExplorer);
|
||||
Backbone.history.start();
|
||||
|
||||
var state = recline.Util.parseQueryString(decodeURIComponent(window.location.search));
|
||||
var state = recline.View.parseQueryString(decodeURIComponent(window.location.search));
|
||||
if (state) {
|
||||
_.each(state, function(value, key) {
|
||||
try {
|
||||
@ -108,7 +108,7 @@ var ExplorerApp = Backbone.View.extend({
|
||||
}
|
||||
];
|
||||
|
||||
this.dataExplorer = new recline.View.DataExplorer({
|
||||
this.dataExplorer = new recline.View.MultiView({
|
||||
model: dataset,
|
||||
el: $el,
|
||||
state: state,
|
||||
@ -145,7 +145,7 @@ var ExplorerApp = Backbone.View.extend({
|
||||
},
|
||||
|
||||
makePermaLink: function(state) {
|
||||
var qs = recline.Util.composeQueryString(state.toJSON());
|
||||
var qs = recline.View.composeQueryString(state.toJSON());
|
||||
return window.location.origin + window.location.pathname + qs;
|
||||
},
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
.header .recline-results-info {
|
||||
line-height: 28px;
|
||||
margin-left: 20px;
|
||||
display: inline;
|
||||
float: left;
|
||||
}
|
||||
|
||||
/**********************************************************
|
||||
@ -39,42 +39,54 @@
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.header .recline-query-editor .input-prepend {
|
||||
.header .input-prepend {
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.recline-query-editor .add-on {
|
||||
.header .add-on {
|
||||
float: left;
|
||||
}
|
||||
|
||||
/* needed for Chrome but not FF */
|
||||
.header .recline-query-editor .add-on {
|
||||
.header .add-on {
|
||||
margin-left: -27px;
|
||||
}
|
||||
|
||||
/* needed for FF but not chrome */
|
||||
.header .recline-query-editor .input-prepend {
|
||||
.header .input-prepend {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.header .recline-query-editor .pagination input {
|
||||
width: 30px;
|
||||
height: 18px;
|
||||
padding: 2px 4px;
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.header .recline-query-editor .pagination a {
|
||||
line-height: 26px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.header .recline-query-editor form button {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/**********************************************************
|
||||
* Query Editor
|
||||
* Pager
|
||||
*********************************************************/
|
||||
|
||||
.header .recline-pager {
|
||||
float: left;
|
||||
margin: auto;
|
||||
display: block;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.header .recline-pager .pagination input {
|
||||
width: 30px;
|
||||
height: 18px;
|
||||
padding: 2px 4px;
|
||||
margin: 0;
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.header .recline-pager .pagination a {
|
||||
line-height: 26px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
/**********************************************************
|
||||
* Filter Editor
|
||||
*********************************************************/
|
||||
|
||||
.recline-filter-editor .filter-term .input-append a {
|
||||
100
library-view.markdown
Normal file
100
library-view.markdown
Normal file
@ -0,0 +1,100 @@
|
||||
---
|
||||
layout: container
|
||||
title: Library - Views
|
||||
---
|
||||
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
Recline Views
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
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).
|
||||
|
||||
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 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:
|
||||
|
||||
<pre>
|
||||
initialize: {
|
||||
model: {a recline.Model.Dataset instance}
|
||||
// el: {do not specify - instead view should create}
|
||||
state: {(optional) Object / Hash specifying initial state}
|
||||
...
|
||||
}
|
||||
</pre>
|
||||
|
||||
Note: Dataset Views in core Recline have a common layout on disk as follows,
|
||||
where ViewName is the named of View class:
|
||||
|
||||
<pre>
|
||||
src/view-{lower-case-ViewName}.js
|
||||
css/{lower-case-ViewName}.css
|
||||
test/view-{lower-case-ViewName}.js
|
||||
</pre>
|
||||
|
||||
### 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.
|
||||
|
||||
22
library.html
22
library.html
@ -13,7 +13,7 @@ title: Library - Home
|
||||
|
||||
<p>Building on <a href="http://backbonejs.com/">Backbone</a>, Recline
|
||||
supplies components and structure to data-heavy applications by providing a
|
||||
set of models (Dataset, Document/Row, Field) and views (Grid, Map, Graph
|
||||
set of models (Dataset, Record/Row, Field) and views (Grid, Map, Graph
|
||||
etc).</p>
|
||||
|
||||
<h2 id="examples">Examples</h2>
|
||||
@ -52,7 +52,7 @@ title: Library - Home
|
||||
<h4>Create a new Backend</h4>
|
||||
</div>
|
||||
<div class="span3 well">
|
||||
<h4>Create a Custom Document Object</h4>
|
||||
<h4>Create a Custom Record Object</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -70,10 +70,10 @@ title: Library - Home
|
||||
<p>There are two main model objects:</p>
|
||||
<ul>
|
||||
<li><a href="docs/model.html#dataset">Dataset</a>: represents the dataset.
|
||||
Holds dataset info and a pointer to list of data items (Documents in our
|
||||
Holds dataset info and a pointer to list of data items (Records in our
|
||||
terminology) which it can load from the relevant Backend.</li>
|
||||
<li><a href="docs/model.html#document">Document</a>: an individual data item
|
||||
(e.g. a row from a relational database or a spreadsheet, a document from from
|
||||
<li><a href="docs/model.html#record">Record</a>: an individual data item
|
||||
(e.g. a row from a relational database or a spreadsheet, a record from from
|
||||
a document DB like CouchDB or MongoDB).</li>
|
||||
</ul>
|
||||
|
||||
@ -84,7 +84,7 @@ title: Library - Home
|
||||
<li><a href="docs/model.html#query">Query</a>: an object to encapsulate a
|
||||
query to the backend (useful both for creating queries and for storing and
|
||||
manipulating query state - e.g. from a query editor).</li>
|
||||
<li><a href="docs/model.html#facte">Facet</a>: Object to store Facet
|
||||
<li><a href="docs/model.html#facet">Facet</a>: Object to store Facet
|
||||
information, that is summary information (e.g. values and counts) about a
|
||||
field obtained by some faceting method on the backend.</li>
|
||||
</ul>
|
||||
@ -97,13 +97,13 @@ title: Library - Home
|
||||
|
||||
<div class="span6">
|
||||
<h4>Backends</h4>
|
||||
<p>Backends connect Dataset and Documents to data from a
|
||||
<p>Backends connect Dataset and Records 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
|
||||
Datasets and individuals Records as well as for bulk loading via a query API
|
||||
and doing bulk transforms on the backend.</p>
|
||||
|
||||
<p>A template Base class can be found <a href="docs/backend/base.html">in the
|
||||
Backend base module of the source docs</a>. It documents both the relevant
|
||||
Backend base module of the source docs</a>. It records both the relevant
|
||||
methods a Backend must have and (optionally) provides a base 'class' for
|
||||
inheritance. You can also find detailed examples of backend implementations in
|
||||
the source documentation below.</p>
|
||||
@ -112,7 +112,7 @@ title: Library - Home
|
||||
<p>Complementing the model are various Views (you can
|
||||
also easily write your own). Each view holds a pointer to a Dataset:</p>
|
||||
<ul>
|
||||
<li>DataExplorer: the parent view which manages the overall app and sets up
|
||||
<li>MultiView: the parent view which manages the overall app and sets up
|
||||
sub views.</li>
|
||||
<li>Grid: the data grid view.</li>
|
||||
<li>Graph: a simple graphing view using <a
|
||||
@ -138,7 +138,7 @@ title: Library - Home
|
||||
<h4>Models and Views (Widgets)</h4>
|
||||
<ul>
|
||||
<li><a href="docs/model.html">Models</a></li>
|
||||
<li><a href="docs/view.html">DataExplorer View (plus common view code)</a></li>
|
||||
<li><a href="docs/view.multiview.html">MultiView View (plus common view code)</a></li>
|
||||
<li><a href="docs/view-grid.html">(Data) Grid View</a></li>
|
||||
<li><a href="docs/view-graph.html">Graph View (based on Flot)</a></li>
|
||||
<li><a href="docs/view-map.html">Map View (based on Leaflet)</a></li>
|
||||
|
||||
2
make
2
make
@ -13,7 +13,7 @@ def docs():
|
||||
print("** Building docs")
|
||||
|
||||
docco_executable = os.environ.get('DOCCO_EXECUTABLE','docco')
|
||||
cmd = '%s src/model.js src/view.js src/view-grid.js src/view-graph.js src/view-map.js' % (docco_executable)
|
||||
cmd = '%s src/*.js' % (docco_executable)
|
||||
os.system(cmd)
|
||||
if os.path.exists('/tmp/recline-docs'):
|
||||
shutil.rmtree('/tmp/recline-docs')
|
||||
|
||||
@ -29,14 +29,14 @@ this.recline.Backend.Base = function() {
|
||||
// ### sync
|
||||
//
|
||||
// An implementation of Backbone.sync that will be used to override
|
||||
// Backbone.sync on operations for Datasets and Documents which are using this backend.
|
||||
// Backbone.sync on operations for Datasets and Records which are using this backend.
|
||||
//
|
||||
// For read-only implementations you will need only to implement read method
|
||||
// for Dataset models (and even this can be a null operation). The read method
|
||||
// should return relevant metadata for the Dataset. We do not require read support
|
||||
// for Documents because they are loaded in bulk by the query method.
|
||||
// for Records because they are loaded in bulk by the query method.
|
||||
//
|
||||
// For backends supporting write operations you must implement update and delete support for Document objects.
|
||||
// For backends supporting write operations you must implement update and delete support for Record objects.
|
||||
//
|
||||
// All code paths should return an object conforming to the jquery promise API.
|
||||
this.sync = function(method, model, options) {
|
||||
@ -44,8 +44,8 @@ this.recline.Backend.Base = function() {
|
||||
|
||||
// ### query
|
||||
//
|
||||
// Query the backend for documents returning them in bulk. This method will
|
||||
// be used by the Dataset.query method to search the backend for documents,
|
||||
// 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.
|
||||
//
|
||||
// @param {recline.model.Dataset} model: Dataset model.
|
||||
@ -71,11 +71,11 @@ this.recline.Backend.Base = function() {
|
||||
// <pre>
|
||||
// {
|
||||
// total: // (required) total number of results (can be null)
|
||||
// hits: [ // (required) one entry for each result document
|
||||
// hits: [ // (required) one entry for each result record
|
||||
// {
|
||||
// _score: // (optional) match score for document
|
||||
// _type: // (optional) document type
|
||||
// _source: // (required) document/row object
|
||||
// _score: // (optional) match score for record
|
||||
// _type: // (optional) record type
|
||||
// _source: // (required) record/row object
|
||||
// }
|
||||
// ],
|
||||
// facets: { // (optional)
|
||||
|
||||
@ -39,7 +39,7 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
|
||||
|
||||
// ### get
|
||||
//
|
||||
// Get document corresponding to specified id
|
||||
// Get record corresponding to specified id
|
||||
//
|
||||
// @return promise compatible deferred object.
|
||||
this.get = function(id) {
|
||||
@ -52,7 +52,7 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
|
||||
|
||||
// ### upsert
|
||||
//
|
||||
// create / update a document to ElasticSearch backend
|
||||
// create / update a record to ElasticSearch backend
|
||||
//
|
||||
// @param {Object} doc an object to insert to the index.
|
||||
// @return deferred supporting promise API
|
||||
@ -72,7 +72,7 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
|
||||
|
||||
// ### delete
|
||||
//
|
||||
// Delete a document from the ElasticSearch backend.
|
||||
// Delete a record from the ElasticSearch backend.
|
||||
//
|
||||
// @param {Object} id id of object to delete
|
||||
// @return deferred supporting promise API
|
||||
@ -154,7 +154,7 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
|
||||
// Backbone sync implementation for this backend.
|
||||
//
|
||||
// URL of ElasticSearch endpoint to use must be specified on the dataset
|
||||
// (and on a Document via its dataset attribute) by the dataset having a
|
||||
// (and on a Record via its dataset attribute) by the dataset having a
|
||||
// url attribute.
|
||||
this.sync = function(method, model, options) {
|
||||
if (model.__type__ == 'Dataset') {
|
||||
@ -180,15 +180,15 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
|
||||
dfd.reject(arguments);
|
||||
});
|
||||
return dfd.promise();
|
||||
} else if (model.__type__ == 'Document') {
|
||||
} else if (model.__type__ == 'Record') {
|
||||
return es.get(model.dataset.id);
|
||||
}
|
||||
} else if (method === 'update') {
|
||||
if (model.__type__ == 'Document') {
|
||||
if (model.__type__ == 'Record') {
|
||||
return es.upsert(model.toJSON());
|
||||
}
|
||||
} else if (method === 'delete') {
|
||||
if (model.__type__ == 'Document') {
|
||||
if (model.__type__ == 'Record') {
|
||||
return es.delete(model.id);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
|
||||
//
|
||||
// Convenience function to create a simple 'in-memory' dataset in one step.
|
||||
//
|
||||
// @param data: list of hashes for each document/row in the data ({key:
|
||||
// @param data: 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 hash
|
||||
// as per recline.Model.Field). If fields not specified they will be taken
|
||||
@ -76,7 +76,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
|
||||
results = results.slice(start, start+numRows);
|
||||
return {
|
||||
total: total,
|
||||
documents: results,
|
||||
records: results,
|
||||
facets: facets
|
||||
};
|
||||
};
|
||||
@ -118,7 +118,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
|
||||
return results;
|
||||
};
|
||||
|
||||
this.computeFacets = function(documents, queryObj) {
|
||||
this.computeFacets = function(records, queryObj) {
|
||||
var facetResults = {};
|
||||
if (!queryObj.facets) {
|
||||
return facetResults;
|
||||
@ -129,7 +129,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
|
||||
facetResults[facetId].termsall = {};
|
||||
});
|
||||
// faceting
|
||||
_.each(documents, function(doc) {
|
||||
_.each(records, function(doc) {
|
||||
_.each(queryObj.facets, function(query, facetId) {
|
||||
var fieldId = query.terms.field;
|
||||
var val = doc[fieldId];
|
||||
@ -172,13 +172,13 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
|
||||
}
|
||||
return dfd.promise();
|
||||
} else if (method === 'update') {
|
||||
if (model.__type__ == 'Document') {
|
||||
if (model.__type__ == 'Record') {
|
||||
model.dataset._dataCache.update(model.toJSON());
|
||||
dfd.resolve(model);
|
||||
}
|
||||
return dfd.promise();
|
||||
} else if (method === 'delete') {
|
||||
if (model.__type__ == 'Document') {
|
||||
if (model.__type__ == 'Record') {
|
||||
model.dataset._dataCache.delete(model.toJSON());
|
||||
dfd.resolve(model);
|
||||
}
|
||||
@ -191,7 +191,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
|
||||
this.query = function(model, queryObj) {
|
||||
var dfd = $.Deferred();
|
||||
var results = model._dataCache.query(queryObj);
|
||||
var hits = _.map(results.documents, function(row) {
|
||||
var hits = _.map(results.records, function(row) {
|
||||
return { _source: row };
|
||||
});
|
||||
var out = {
|
||||
|
||||
42
src/model.js
42
src/model.js
@ -12,11 +12,11 @@ this.recline.Model = this.recline.Model || {};
|
||||
// fields on this Dataset (this can be set explicitly, or, will be set by
|
||||
// Dataset.fetch() or Dataset.query()
|
||||
//
|
||||
// @property {DocumentList} currentDocuments: a `DocumentList` containing the
|
||||
// Documents we have currently loaded for viewing (updated by calling query
|
||||
// @property {RecordList} currentRecords: a `RecordList` containing the
|
||||
// Records we have currently loaded for viewing (updated by calling query
|
||||
// method)
|
||||
//
|
||||
// @property {number} docCount: total number of documents in this dataset
|
||||
// @property {number} docCount: total number of records in this dataset
|
||||
//
|
||||
// @property {Backend} backend: the Backend (instance) for this Dataset.
|
||||
//
|
||||
@ -48,7 +48,7 @@ my.Dataset = Backbone.Model.extend({
|
||||
this.backend = this._backendFromString(backend);
|
||||
}
|
||||
this.fields = new my.FieldList();
|
||||
this.currentDocuments = new my.DocumentList();
|
||||
this.currentRecords = new my.RecordList();
|
||||
this.facets = new my.FacetList();
|
||||
this.docCount = null;
|
||||
this.queryState = new my.Query();
|
||||
@ -58,12 +58,12 @@ my.Dataset = Backbone.Model.extend({
|
||||
|
||||
// ### query
|
||||
//
|
||||
// AJAX method with promise API to get documents from the backend.
|
||||
// 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 DocumentList are used to reset this.currentDocuments and are
|
||||
// Resulting RecordList are used to reset this.currentRecords and are
|
||||
// also returned.
|
||||
query: function(queryObj) {
|
||||
var self = this;
|
||||
@ -73,12 +73,12 @@ my.Dataset = Backbone.Model.extend({
|
||||
this.backend.query(this, actualQuery).done(function(queryResult) {
|
||||
self.docCount = queryResult.total;
|
||||
var docs = _.map(queryResult.hits, function(hit) {
|
||||
var _doc = new my.Document(hit._source);
|
||||
var _doc = new my.Record(hit._source);
|
||||
_doc.backend = self.backend;
|
||||
_doc.dataset = self;
|
||||
return _doc;
|
||||
});
|
||||
self.currentDocuments.reset(docs);
|
||||
self.currentRecords.reset(docs);
|
||||
if (queryResult.facets) {
|
||||
var facets = _.map(queryResult.facets, function(facetResult, facetId) {
|
||||
facetResult.id = facetId;
|
||||
@ -87,7 +87,7 @@ my.Dataset = Backbone.Model.extend({
|
||||
self.facets.reset(facets);
|
||||
}
|
||||
self.trigger('query:done');
|
||||
dfd.resolve(self.currentDocuments);
|
||||
dfd.resolve(self.currentRecords);
|
||||
})
|
||||
.fail(function(arguments) {
|
||||
self.trigger('query:fail', arguments);
|
||||
@ -176,11 +176,11 @@ my.Dataset.restore = function(state) {
|
||||
return dataset;
|
||||
};
|
||||
|
||||
// ## <a id="document">A Document (aka Row)</a>
|
||||
// ## <a id="record">A Record (aka Row)</a>
|
||||
//
|
||||
// A single entry or row in the dataset
|
||||
my.Document = Backbone.Model.extend({
|
||||
__type__: 'Document',
|
||||
my.Record = Backbone.Model.extend({
|
||||
__type__: 'Record',
|
||||
initialize: function() {
|
||||
_.bindAll(this, 'getFieldValue');
|
||||
},
|
||||
@ -188,7 +188,7 @@ my.Document = Backbone.Model.extend({
|
||||
// ### getFieldValue
|
||||
//
|
||||
// For the provided Field get the corresponding rendered computed data value
|
||||
// for this document.
|
||||
// for this record.
|
||||
getFieldValue: function(field) {
|
||||
var val = this.get(field.id);
|
||||
if (field.deriver) {
|
||||
@ -211,17 +211,17 @@ my.Document = Backbone.Model.extend({
|
||||
}
|
||||
});
|
||||
|
||||
// ## A Backbone collection of Documents
|
||||
my.DocumentList = Backbone.Collection.extend({
|
||||
__type__: 'DocumentList',
|
||||
model: my.Document
|
||||
// ## A Backbone collection of Records
|
||||
my.RecordList = Backbone.Collection.extend({
|
||||
__type__: 'RecordList',
|
||||
model: my.Record
|
||||
});
|
||||
|
||||
// ## <a id="field">A Field (aka Column) on a Dataset</a>
|
||||
//
|
||||
// Following (Backbone) attributes as standard:
|
||||
//
|
||||
// * id: a unique identifer for this field- usually this should match the key in the documents hash
|
||||
// * id: a unique identifer for this field- usually this should match the key in the records hash
|
||||
// * label: (optional: defaults to id) the visible label used for this field
|
||||
// * type: (optional: defaults to string) the type of the data in this field. Should be a string as per type names defined by ElasticSearch - see Types list on <http://www.elasticsearch.org/guide/reference/mapping/>
|
||||
// * format: (optional) used to indicate how the data should be formatted. For example:
|
||||
@ -234,13 +234,13 @@ my.DocumentList = Backbone.Collection.extend({
|
||||
//
|
||||
// @property {Function} renderer: a function to render the data for this field.
|
||||
// Signature: function(value, field, doc) where value is the value of this
|
||||
// cell, field is corresponding field object and document is the document
|
||||
// cell, field is corresponding field object and record is the record
|
||||
// object. Note that implementing functions can ignore arguments (e.g.
|
||||
// function(value) would be a valid formatter function).
|
||||
//
|
||||
// @property {Function} deriver: a function to derive/compute the value of data
|
||||
// in this field as a function of this field's value (if any) and the current
|
||||
// document, its signature and behaviour is the same as for renderer. Use of
|
||||
// record, its signature and behaviour is the same as for renderer. Use of
|
||||
// this function 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
|
||||
@ -461,7 +461,7 @@ my.Query = Backbone.Model.extend({
|
||||
// "_type" : "terms",
|
||||
// // total number of tokens in the facet
|
||||
// "total": 5,
|
||||
// // @property {number} number of documents which have no value for the field
|
||||
// // @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,
|
||||
|
||||
79
src/util.js
79
src/util.js
@ -1,79 +0,0 @@
|
||||
/*jshint multistr:true */
|
||||
|
||||
this.recline = this.recline || {};
|
||||
this.recline.Util = this.recline.Util || {};
|
||||
|
||||
(function(my) {
|
||||
// ## 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() {
|
||||
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);
|
||||
};
|
||||
})(this.recline.Util);
|
||||
|
||||
@ -97,8 +97,8 @@ my.Graph = Backbone.View.extend({
|
||||
this.model.bind('change', this.render);
|
||||
this.model.fields.bind('reset', this.render);
|
||||
this.model.fields.bind('add', this.render);
|
||||
this.model.currentDocuments.bind('add', this.redraw);
|
||||
this.model.currentDocuments.bind('reset', this.redraw);
|
||||
this.model.currentRecords.bind('add', this.redraw);
|
||||
this.model.currentRecords.bind('reset', this.redraw);
|
||||
// because we cannot redraw when hidden we may need when becoming visible
|
||||
this.bind('view:show', function() {
|
||||
if (this.needToRedraw) {
|
||||
@ -181,7 +181,7 @@ my.Graph = Backbone.View.extend({
|
||||
// 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[0]);
|
||||
if ((!areWeVisible || this.model.currentDocuments.length === 0)) {
|
||||
if ((!areWeVisible || this.model.currentRecords.length === 0)) {
|
||||
this.needToRedraw = true;
|
||||
return;
|
||||
}
|
||||
@ -209,8 +209,8 @@ my.Graph = Backbone.View.extend({
|
||||
// However, that is non-trivial to work out from a dataset (datasets may
|
||||
// have no field type info). Thus at present we only do this for bars.
|
||||
var tickFormatter = function (val) {
|
||||
if (self.model.currentDocuments.models[val]) {
|
||||
var out = self.model.currentDocuments.models[val].get(self.state.attributes.group);
|
||||
if (self.model.currentRecords.models[val]) {
|
||||
var out = self.model.currentRecords.models[val].get(self.state.attributes.group);
|
||||
// if the value was in fact a number we want that not the
|
||||
if (typeof(out) == 'number') {
|
||||
return val;
|
||||
@ -266,7 +266,7 @@ my.Graph = Backbone.View.extend({
|
||||
tickLength: 1,
|
||||
tickFormatter: tickFormatter,
|
||||
min: -0.5,
|
||||
max: self.model.currentDocuments.length - 0.5
|
||||
max: self.model.currentRecords.length - 0.5
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -304,8 +304,8 @@ my.Graph = Backbone.View.extend({
|
||||
y = _tmp;
|
||||
}
|
||||
// convert back from 'index' value on x-axis (e.g. in cases where non-number values)
|
||||
if (self.model.currentDocuments.models[x]) {
|
||||
x = self.model.currentDocuments.models[x].get(self.state.attributes.group);
|
||||
if (self.model.currentRecords.models[x]) {
|
||||
x = self.model.currentRecords.models[x].get(self.state.attributes.group);
|
||||
} else {
|
||||
x = x.toFixed(2);
|
||||
}
|
||||
@ -339,7 +339,7 @@ my.Graph = Backbone.View.extend({
|
||||
var series = [];
|
||||
_.each(this.state.attributes.series, function(field) {
|
||||
var points = [];
|
||||
_.each(self.model.currentDocuments.models, function(doc, index) {
|
||||
_.each(self.model.currentRecords.models, function(doc, index) {
|
||||
var xfield = self.model.fields.get(self.state.attributes.group);
|
||||
var x = doc.getFieldValue(xfield);
|
||||
// time series
|
||||
|
||||
@ -17,9 +17,9 @@ my.Grid = Backbone.View.extend({
|
||||
var self = this;
|
||||
this.el = $(this.el);
|
||||
_.bindAll(this, 'render', 'onHorizontalScroll');
|
||||
this.model.currentDocuments.bind('add', this.render);
|
||||
this.model.currentDocuments.bind('reset', this.render);
|
||||
this.model.currentDocuments.bind('remove', this.render);
|
||||
this.model.currentRecords.bind('add', this.render);
|
||||
this.model.currentRecords.bind('reset', this.render);
|
||||
this.model.currentRecords.bind('remove', this.render);
|
||||
this.tempState = {};
|
||||
var state = _.extend({
|
||||
hiddenFields: []
|
||||
@ -77,13 +77,13 @@ my.Grid = Backbone.View.extend({
|
||||
showColumn: function() { self.showColumn(e); },
|
||||
deleteRow: function() {
|
||||
var self = this;
|
||||
var doc = _.find(self.model.currentDocuments.models, function(doc) {
|
||||
var doc = _.find(self.model.currentRecords.models, function(doc) {
|
||||
// important this is == as the currentRow will be string (as comes
|
||||
// from DOM) while id may be int
|
||||
return doc.id == self.tempState.currentRow;
|
||||
});
|
||||
doc.destroy().then(function() {
|
||||
self.model.currentDocuments.remove(doc);
|
||||
self.model.currentRecords.remove(doc);
|
||||
self.trigger('recline:flash', {message: "Row deleted successfully"});
|
||||
}).fail(function(err) {
|
||||
self.trigger('recline:flash', {message: "Errorz! " + err});
|
||||
@ -213,7 +213,7 @@ my.Grid = Backbone.View.extend({
|
||||
});
|
||||
var htmls = Mustache.render(this.template, this.toTemplateJSON());
|
||||
this.el.html(htmls);
|
||||
this.model.currentDocuments.forEach(function(doc) {
|
||||
this.model.currentRecords.forEach(function(doc) {
|
||||
var tr = $('<tr />');
|
||||
self.el.find('tbody').append(tr);
|
||||
var newView = new my.GridRow({
|
||||
@ -246,7 +246,7 @@ my.Grid = Backbone.View.extend({
|
||||
}
|
||||
});
|
||||
|
||||
// ## GridRow View for rendering an individual document.
|
||||
// ## 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.
|
||||
//
|
||||
@ -256,7 +256,7 @@ my.Grid = Backbone.View.extend({
|
||||
//
|
||||
// <pre>
|
||||
// var row = new GridRow({
|
||||
// model: dataset-document,
|
||||
// model: dataset-record,
|
||||
// el: dom-element,
|
||||
// fields: mydatasets.fields // a FieldList object
|
||||
// });
|
||||
|
||||
@ -7,7 +7,7 @@ this.recline.View = this.recline.View || {};
|
||||
|
||||
// ## Map view for a Dataset using Leaflet mapping library.
|
||||
//
|
||||
// This view allows to plot gereferenced documents on a map. The location
|
||||
// This view allows to plot gereferenced records on a map. The location
|
||||
// information can be provided either via a field with
|
||||
// [GeoJSON](http://geojson.org) objects or two fields with latitude and
|
||||
// longitude coordinates.
|
||||
@ -115,14 +115,14 @@ my.Map = Backbone.View.extend({
|
||||
self.render()
|
||||
});
|
||||
|
||||
// Listen to changes in the documents
|
||||
this.model.currentDocuments.bind('add', function(doc){self.redraw('add',doc)});
|
||||
this.model.currentDocuments.bind('change', function(doc){
|
||||
// Listen to changes in the records
|
||||
this.model.currentRecords.bind('add', function(doc){self.redraw('add',doc)});
|
||||
this.model.currentRecords.bind('change', function(doc){
|
||||
self.redraw('remove',doc);
|
||||
self.redraw('add',doc);
|
||||
});
|
||||
this.model.currentDocuments.bind('remove', function(doc){self.redraw('remove',doc)});
|
||||
this.model.currentDocuments.bind('reset', function(){self.redraw('reset')});
|
||||
this.model.currentRecords.bind('remove', function(doc){self.redraw('remove',doc)});
|
||||
this.model.currentRecords.bind('reset', function(){self.redraw('reset')});
|
||||
|
||||
this.bind('view:show',function(){
|
||||
// If the div was hidden, Leaflet needs to recalculate some sizes
|
||||
@ -184,9 +184,9 @@ my.Map = Backbone.View.extend({
|
||||
// Actions can be:
|
||||
//
|
||||
// * reset: Clear all features
|
||||
// * add: Add one or n features (documents)
|
||||
// * remove: Remove one or n features (documents)
|
||||
// * refresh: Clear existing features and add all current documents
|
||||
// * 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';
|
||||
@ -201,7 +201,7 @@ my.Map = Backbone.View.extend({
|
||||
if (this.geomReady && this.mapReady){
|
||||
if (action == 'reset' || action == 'refresh'){
|
||||
this.features.clearLayers();
|
||||
this._add(this.model.currentDocuments.models);
|
||||
this._add(this.model.currentRecords.models);
|
||||
} else if (action == 'add' && doc){
|
||||
this._add(doc);
|
||||
} else if (action == 'remove' && doc){
|
||||
@ -266,11 +266,11 @@ my.Map = Backbone.View.extend({
|
||||
|
||||
// Private: Add one or n features to the map
|
||||
//
|
||||
// For each document passed, a GeoJSON geometry will be extracted and added
|
||||
// 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 document fields.
|
||||
// Each feature will have a popup associated with all the record fields.
|
||||
//
|
||||
_add: function(docs){
|
||||
var self = this;
|
||||
@ -281,7 +281,7 @@ my.Map = Backbone.View.extend({
|
||||
var wrongSoFar = 0;
|
||||
_.every(docs,function(doc){
|
||||
count += 1;
|
||||
var feature = self._getGeometryFromDocument(doc);
|
||||
var feature = self._getGeometryFromRecord(doc);
|
||||
if (typeof feature === 'undefined' || feature === null){
|
||||
// Empty field
|
||||
return true;
|
||||
@ -338,22 +338,28 @@ my.Map = Backbone.View.extend({
|
||||
|
||||
},
|
||||
|
||||
// Private: Return a GeoJSON geomtry extracted from the document fields
|
||||
// Private: Return a GeoJSON geomtry extracted from the record fields
|
||||
//
|
||||
_getGeometryFromDocument: function(doc){
|
||||
_getGeometryFromRecord: function(doc){
|
||||
if (this.geomReady){
|
||||
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 {
|
||||
return $.parseJSON(value);
|
||||
value = $.parseJSON(value);
|
||||
} catch(e) {
|
||||
}
|
||||
} else {
|
||||
// We assume that the contents of the field are a valid GeoJSON object
|
||||
return value;
|
||||
}
|
||||
if (value && value.lat) {
|
||||
// not yet geojson so convert
|
||||
value = {
|
||||
"type": "Point",
|
||||
"coordinates": [value.lon || value.lng, value.lat]
|
||||
};
|
||||
}
|
||||
// We now 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'));
|
||||
|
||||
@ -19,9 +19,9 @@ my.SlickGrid = Backbone.View.extend({
|
||||
var self = this;
|
||||
this.el = $(this.el);
|
||||
_.bindAll(this, 'render');
|
||||
this.model.currentDocuments.bind('add', this.render);
|
||||
this.model.currentDocuments.bind('reset', this.render);
|
||||
this.model.currentDocuments.bind('remove', this.render);
|
||||
this.model.currentRecords.bind('add', this.render);
|
||||
this.model.currentRecords.bind('reset', this.render);
|
||||
this.model.currentRecords.bind('remove', this.render);
|
||||
|
||||
var state = _.extend({
|
||||
hiddenColumns: [],
|
||||
@ -110,7 +110,7 @@ my.SlickGrid = Backbone.View.extend({
|
||||
|
||||
var data = [];
|
||||
|
||||
this.model.currentDocuments.each(function(doc){
|
||||
this.model.currentRecords.each(function(doc){
|
||||
var row = {};
|
||||
self.model.fields.each(function(field){
|
||||
row[field.id] = doc.getFieldValue(field);
|
||||
|
||||
@ -31,7 +31,7 @@ my.Timeline = Backbone.View.extend({
|
||||
this.model.fields.bind('reset', function() {
|
||||
self._setupTemporalField();
|
||||
});
|
||||
this.model.currentDocuments.bind('all', function() {
|
||||
this.model.currentRecords.bind('all', function() {
|
||||
self.reloadData();
|
||||
});
|
||||
var stateData = _.extend({
|
||||
@ -78,11 +78,11 @@ my.Timeline = Backbone.View.extend({
|
||||
]
|
||||
}
|
||||
};
|
||||
this.model.currentDocuments.each(function(doc) {
|
||||
this.model.currentRecords.each(function(doc) {
|
||||
var start = doc.get(self.state.get('startField'));
|
||||
if (start) {
|
||||
var end = moment(doc.get(self.state.get('endField')));
|
||||
end = end ? end.toDate() : null;
|
||||
var end = doc.get(self.state.get('endField'));
|
||||
end = end ? moment(end).toDate() : null;
|
||||
var tlEntry = {
|
||||
"startDate": moment(start).toDate(),
|
||||
"endDate": end,
|
||||
|
||||
@ -98,7 +98,7 @@ my.ColumnTransform = Backbone.View.extend({
|
||||
}
|
||||
this.el.modal('hide');
|
||||
this.trigger('recline:flash', {message: "Updating all visible docs. This could take a while...", persist: true, loader: true});
|
||||
var docs = self.model.currentDocuments.map(function(doc) {
|
||||
var docs = self.model.currentRecords.map(function(doc) {
|
||||
return doc.toJSON();
|
||||
});
|
||||
// TODO: notify about failed docs?
|
||||
@ -107,14 +107,14 @@ my.ColumnTransform = Backbone.View.extend({
|
||||
function onCompletedUpdate() {
|
||||
totalToUpdate += -1;
|
||||
if (totalToUpdate === 0) {
|
||||
self.trigger('recline:flash', {message: toUpdate.length + " documents updated successfully"});
|
||||
self.trigger('recline:flash', {message: toUpdate.length + " records updated successfully"});
|
||||
alert('WARNING: We have only updated the docs in this view. (Updating of all docs not yet implemented!)');
|
||||
self.remove();
|
||||
}
|
||||
}
|
||||
// TODO: Very inefficient as we search through all docs every time!
|
||||
_.each(toUpdate, function(editedDoc) {
|
||||
var realDoc = self.model.currentDocuments.get(editedDoc.id);
|
||||
var realDoc = self.model.currentRecords.get(editedDoc.id);
|
||||
realDoc.set(editedDoc);
|
||||
realDoc.save().then(onCompletedUpdate).fail(onCompletedUpdate);
|
||||
});
|
||||
@ -158,7 +158,7 @@ my.ColumnTransform = Backbone.View.extend({
|
||||
var editFunc = costco.evalFunction(e.target.value);
|
||||
if (!editFunc.errorMessage) {
|
||||
errors.text('No syntax error.');
|
||||
var docs = self.model.currentDocuments.map(function(doc) {
|
||||
var docs = self.model.currentRecords.map(function(doc) {
|
||||
return doc.toJSON();
|
||||
});
|
||||
var previewData = costco.previewTransform(docs, editFunc, self.state.currentColumn);
|
||||
|
||||
695
src/view.js
695
src/view.js
@ -1,695 +0,0 @@
|
||||
/*jshint multistr:true */
|
||||
|
||||
// # Recline Views
|
||||
//
|
||||
// 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).
|
||||
//
|
||||
// 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 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
|
||||
// recline-read-only class 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:
|
||||
//
|
||||
// <pre>
|
||||
// initialize: {
|
||||
// model: {a recline.Model.Dataset instance}
|
||||
// // el: {do not specify - instead view should create}
|
||||
// state: {(optional) Object / Hash specifying initial state}
|
||||
// ...
|
||||
// }
|
||||
// </pre>
|
||||
//
|
||||
// Note: Dataset Views in core Recline have a common layout on disk as
|
||||
// follows, where ViewName is the named of View class:
|
||||
//
|
||||
// <pre>
|
||||
// src/view-{lower-case-ViewName}.js
|
||||
// css/{lower-case-ViewName}.css
|
||||
// test/view-{lower-case-ViewName}.js
|
||||
// </pre>
|
||||
//
|
||||
// ### 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.
|
||||
//
|
||||
// ----
|
||||
|
||||
// Standard JS module setup
|
||||
this.recline = this.recline || {};
|
||||
this.recline.View = this.recline.View || {};
|
||||
|
||||
(function($, my) {
|
||||
// ## DataExplorer
|
||||
//
|
||||
// The primary view for the entire application. Usage:
|
||||
//
|
||||
// <pre>
|
||||
// var myExplorer = new model.recline.DataExplorer({
|
||||
// model: {{recline.Model.Dataset instance}}
|
||||
// el: {{an existing dom element}}
|
||||
// views: {{dataset views}}
|
||||
// state: {{state configuration -- see below}}
|
||||
// });
|
||||
// </pre>
|
||||
//
|
||||
// ### 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
|
||||
// DataExplorer 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!).
|
||||
//
|
||||
// <pre>
|
||||
// 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
|
||||
// })
|
||||
// }
|
||||
// ];
|
||||
// </pre>
|
||||
//
|
||||
// **state**: standard state config for this view. This state is slightly
|
||||
// special as it includes config of many of the subviews.
|
||||
//
|
||||
// <pre>
|
||||
// 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
|
||||
// }
|
||||
// </pre>
|
||||
//
|
||||
// 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 DataExplorer with the relevant views themselves.
|
||||
my.DataExplorer = Backbone.View.extend({
|
||||
template: ' \
|
||||
<div class="recline-data-explorer"> \
|
||||
<div class="alert-messages"></div> \
|
||||
\
|
||||
<div class="header"> \
|
||||
<ul class="navigation"> \
|
||||
{{#views}} \
|
||||
<li><a href="#{{id}}" data-view="{{id}}" class="btn">{{label}}</a> \
|
||||
{{/views}} \
|
||||
</ul> \
|
||||
<div class="recline-results-info"> \
|
||||
Results found <span class="doc-count">{{docCount}}</span> \
|
||||
</div> \
|
||||
<div class="menu-right"> \
|
||||
<a href="#" class="btn" data-action="filters">Filters</a> \
|
||||
<a href="#" class="btn" data-action="facets">Facets</a> \
|
||||
</div> \
|
||||
<div class="query-editor-here" style="display:inline;"></div> \
|
||||
<div class="clearfix"></div> \
|
||||
</div> \
|
||||
<div class="data-view-container"></div> \
|
||||
</div> \
|
||||
',
|
||||
events: {
|
||||
'click .menu-right a': '_onMenuClick',
|
||||
'click .navigation a': '_onSwitchView'
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
var self = this;
|
||||
this.el = $(this.el);
|
||||
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.Grid({
|
||||
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')
|
||||
}),
|
||||
}];
|
||||
}
|
||||
// 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.model.bind('query:start', function() {
|
||||
self.notify({loader: true, persist: true});
|
||||
});
|
||||
this.model.bind('query:done', function() {
|
||||
self.clearNotifications();
|
||||
self.el.find('.doc-count').text(self.model.docCount || 'Unknown');
|
||||
});
|
||||
this.model.bind('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
|
||||
this.model.fetch()
|
||||
.done(function(dataset) {
|
||||
self.model.query(self.state.get('query'));
|
||||
})
|
||||
.fail(function(error) {
|
||||
self.notify({message: error.message, category: 'error', persist: true});
|
||||
});
|
||||
},
|
||||
|
||||
setReadOnly: function() {
|
||||
this.el.addClass('recline-read-only');
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var tmplData = this.model.toTemplateJSON();
|
||||
tmplData.views = this.pageViews;
|
||||
var template = Mustache.render(this.template, tmplData);
|
||||
$(this.el).html(template);
|
||||
var $dataViewContainer = this.el.find('.data-view-container');
|
||||
_.each(this.pageViews, function(view, pageName) {
|
||||
$dataViewContainer.append(view.view.el);
|
||||
});
|
||||
var queryEditor = new my.QueryEditor({
|
||||
model: this.model.queryState
|
||||
});
|
||||
this.el.find('.query-editor-here').append(queryEditor.el);
|
||||
var filterEditor = new my.FilterEditor({
|
||||
model: this.model.queryState
|
||||
});
|
||||
this.$filterEditor = filterEditor.el;
|
||||
this.el.find('.header').append(filterEditor.el);
|
||||
var facetViewer = new my.FacetViewer({
|
||||
model: this.model
|
||||
});
|
||||
this.$facetViewer = facetViewer.el;
|
||||
this.el.find('.header').append(facetViewer.el);
|
||||
},
|
||||
|
||||
updateNav: function(pageName) {
|
||||
this.el.find('.navigation li').removeClass('active');
|
||||
this.el.find('.navigation li a').removeClass('disabled');
|
||||
var $el = this.el.find('.navigation li a[data-view="' + pageName + '"]');
|
||||
$el.parent().addClass('active');
|
||||
$el.addClass('disabled');
|
||||
// show the specific page
|
||||
_.each(this.pageViews, function(view, idx) {
|
||||
if (view.id === pageName) {
|
||||
view.view.el.show();
|
||||
view.view.trigger('view:show');
|
||||
} else {
|
||||
view.view.el.hide();
|
||||
view.view.trigger('view:hide');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_onMenuClick: function(e) {
|
||||
e.preventDefault();
|
||||
var action = $(e.target).attr('data-action');
|
||||
if (action === 'filters') {
|
||||
this.$filterEditor.show();
|
||||
} else if (action === 'facets') {
|
||||
this.$facetViewer.show();
|
||||
}
|
||||
},
|
||||
|
||||
_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 = recline.Util.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'),
|
||||
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.model.queryState.bind('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);
|
||||
pageView.view.state.bind('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) {
|
||||
pageView.view.bind('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
|
||||
);
|
||||
if (tmplData.loader) {
|
||||
var _template = ' \
|
||||
<div class="alert alert-info alert-loader"> \
|
||||
{{message}} \
|
||||
<span class="notification-loader"> </span> \
|
||||
</div>';
|
||||
} else {
|
||||
var _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();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ### DataExplorer.restore
|
||||
//
|
||||
// Restore a DataExplorer instance from a serialized state including the associated dataset
|
||||
my.DataExplorer.restore = function(state) {
|
||||
var dataset = recline.Model.Dataset.restore(state);
|
||||
var explorer = new my.DataExplorer({
|
||||
model: dataset,
|
||||
state: state
|
||||
});
|
||||
return explorer;
|
||||
}
|
||||
|
||||
my.QueryEditor = Backbone.View.extend({
|
||||
className: 'recline-query-editor',
|
||||
template: ' \
|
||||
<form action="" method="GET" class="form-inline"> \
|
||||
<div class="input-prepend text-query"> \
|
||||
<span class="add-on"><i class="icon-search"></i></span> \
|
||||
<input type="text" name="q" value="{{q}}" class="span2" placeholder="Search data ..." class="search-query" /> \
|
||||
</div> \
|
||||
<div class="pagination"> \
|
||||
<ul> \
|
||||
<li class="prev action-pagination-update"><a href="">«</a></li> \
|
||||
<li class="active"><a><input name="from" type="text" value="{{from}}" /> – <input name="to" type="text" value="{{to}}" /> </a></li> \
|
||||
<li class="next action-pagination-update"><a href="">»</a></li> \
|
||||
</ul> \
|
||||
</div> \
|
||||
<button type="submit" class="btn">Go »</button> \
|
||||
</form> \
|
||||
',
|
||||
|
||||
events: {
|
||||
'submit form': 'onFormSubmit',
|
||||
'click .action-pagination-update': 'onPaginationUpdate'
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
_.bindAll(this, 'render');
|
||||
this.el = $(this.el);
|
||||
this.model.bind('change', this.render);
|
||||
this.render();
|
||||
},
|
||||
onFormSubmit: function(e) {
|
||||
e.preventDefault();
|
||||
var query = this.el.find('.text-query input').val();
|
||||
var newFrom = parseInt(this.el.find('input[name="from"]').val());
|
||||
var newSize = parseInt(this.el.find('input[name="to"]').val()) - newFrom;
|
||||
this.model.set({size: newSize, from: newFrom, q: query});
|
||||
},
|
||||
onPaginationUpdate: function(e) {
|
||||
e.preventDefault();
|
||||
var $el = $(e.target);
|
||||
var newFrom = 0;
|
||||
if ($el.parent().hasClass('prev')) {
|
||||
newFrom = this.model.get('from') - Math.max(0, this.model.get('size'));
|
||||
} else {
|
||||
newFrom = this.model.get('from') + this.model.get('size');
|
||||
}
|
||||
this.model.set({from: newFrom});
|
||||
},
|
||||
render: function() {
|
||||
var tmplData = this.model.toJSON();
|
||||
tmplData.to = this.model.get('from') + this.model.get('size');
|
||||
var templated = Mustache.render(this.template, tmplData);
|
||||
this.el.html(templated);
|
||||
}
|
||||
});
|
||||
|
||||
my.FilterEditor = Backbone.View.extend({
|
||||
className: 'recline-filter-editor well',
|
||||
template: ' \
|
||||
<a class="close js-hide" href="#">×</a> \
|
||||
<div class="row filters"> \
|
||||
<div class="span1"> \
|
||||
<h3>Filters</h3> \
|
||||
</div> \
|
||||
<div class="span11"> \
|
||||
<form class="form-horizontal"> \
|
||||
<div class="row"> \
|
||||
<div class="span6"> \
|
||||
{{#termFilters}} \
|
||||
<div class="control-group filter-term filter" data-filter-id={{id}}> \
|
||||
<label class="control-label" for="">{{label}}</label> \
|
||||
<div class="controls"> \
|
||||
<div class="input-append"> \
|
||||
<input type="text" value="{{value}}" name="{{fieldId}}" class="span4" data-filter-field="{{fieldId}}" data-filter-id="{{id}}" data-filter-type="term" /> \
|
||||
<a class="btn js-remove-filter"><i class="icon-remove"></i></a> \
|
||||
</div> \
|
||||
</div> \
|
||||
</div> \
|
||||
{{/termFilters}} \
|
||||
</div> \
|
||||
<div class="span4"> \
|
||||
<p>To add a filter use the column menu in the grid view.</p> \
|
||||
<button type="submit" class="btn">Update</button> \
|
||||
</div> \
|
||||
</form> \
|
||||
</div> \
|
||||
</div> \
|
||||
',
|
||||
events: {
|
||||
'click .js-hide': 'onHide',
|
||||
'click .js-remove-filter': 'onRemoveFilter',
|
||||
'submit form': 'onTermFiltersUpdate'
|
||||
},
|
||||
initialize: function() {
|
||||
this.el = $(this.el);
|
||||
_.bindAll(this, 'render');
|
||||
this.model.bind('change', this.render);
|
||||
this.model.bind('change:filters:new-blank', this.render);
|
||||
this.render();
|
||||
},
|
||||
render: function() {
|
||||
var tmplData = $.extend(true, {}, this.model.toJSON());
|
||||
// we will use idx in list as there id ...
|
||||
tmplData.filters = _.map(tmplData.filters, function(filter, idx) {
|
||||
filter.id = idx;
|
||||
return filter;
|
||||
});
|
||||
tmplData.termFilters = _.filter(tmplData.filters, function(filter) {
|
||||
return filter.term !== undefined;
|
||||
});
|
||||
tmplData.termFilters = _.map(tmplData.termFilters, function(filter) {
|
||||
var fieldId = _.keys(filter.term)[0];
|
||||
return {
|
||||
id: filter.id,
|
||||
fieldId: fieldId,
|
||||
label: fieldId,
|
||||
value: filter.term[fieldId]
|
||||
};
|
||||
});
|
||||
var out = Mustache.render(this.template, tmplData);
|
||||
this.el.html(out);
|
||||
// are there actually any facets to show?
|
||||
if (this.model.get('filters').length > 0) {
|
||||
this.el.show();
|
||||
} else {
|
||||
this.el.hide();
|
||||
}
|
||||
},
|
||||
onHide: function(e) {
|
||||
e.preventDefault();
|
||||
this.el.hide();
|
||||
},
|
||||
onRemoveFilter: function(e) {
|
||||
e.preventDefault();
|
||||
var $target = $(e.target);
|
||||
var filterId = $target.closest('.filter').attr('data-filter-id');
|
||||
this.model.removeFilter(filterId);
|
||||
},
|
||||
onTermFiltersUpdate: function(e) {
|
||||
var self = this;
|
||||
e.preventDefault();
|
||||
var filters = self.model.get('filters');
|
||||
var $form = $(e.target);
|
||||
_.each($form.find('input'), function(input) {
|
||||
var $input = $(input);
|
||||
var filterIndex = parseInt($input.attr('data-filter-id'));
|
||||
var value = $input.val();
|
||||
var fieldId = $input.attr('data-filter-field');
|
||||
filters[filterIndex].term[fieldId] = value;
|
||||
});
|
||||
self.model.set({filters: filters});
|
||||
self.model.trigger('change');
|
||||
}
|
||||
});
|
||||
|
||||
my.FacetViewer = Backbone.View.extend({
|
||||
className: 'recline-facet-viewer well',
|
||||
template: ' \
|
||||
<a class="close js-hide" href="#">×</a> \
|
||||
<div class="facets row"> \
|
||||
<div class="span1"> \
|
||||
<h3>Facets</h3> \
|
||||
</div> \
|
||||
{{#facets}} \
|
||||
<div class="facet-summary span2 dropdown" data-facet="{{id}}"> \
|
||||
<a class="btn dropdown-toggle" data-toggle="dropdown" href="#"><i class="icon-chevron-down"></i> {{id}} {{label}}</a> \
|
||||
<ul class="facet-items dropdown-menu"> \
|
||||
{{#terms}} \
|
||||
<li><a class="facet-choice js-facet-filter" data-value="{{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-hide': 'onHide',
|
||||
'click .js-facet-filter': 'onFacetFilter'
|
||||
},
|
||||
initialize: function(model) {
|
||||
_.bindAll(this, 'render');
|
||||
this.el = $(this.el);
|
||||
this.model.facets.bind('all', this.render);
|
||||
this.model.fields.bind('all', this.render);
|
||||
this.render();
|
||||
},
|
||||
render: function() {
|
||||
var tmplData = {
|
||||
facets: this.model.facets.toJSON(),
|
||||
fields: this.model.fields.toJSON()
|
||||
};
|
||||
tmplData.facets = _.map(tmplData.facets, 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) {
|
||||
var $target= $(e.target);
|
||||
var fieldId = $target.closest('.facet-summary').attr('data-facet');
|
||||
var value = $target.attr('data-value');
|
||||
this.model.queryState.addTermFilter(fieldId, value);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
})(jQuery, recline.View);
|
||||
|
||||
452
src/view.multiview.js
Normal file
452
src/view.multiview.js
Normal file
@ -0,0 +1,452 @@
|
||||
/*jshint multistr:true */
|
||||
|
||||
// Standard JS module setup
|
||||
this.recline = this.recline || {};
|
||||
this.recline.View = this.recline.View || {};
|
||||
|
||||
(function($, my) {
|
||||
// ## MultiView
|
||||
//
|
||||
// Manage multiple views together along with query editor etc. Usage:
|
||||
//
|
||||
// <pre>
|
||||
// var myExplorer = new model.recline.MultiView({
|
||||
// model: {{recline.Model.Dataset instance}}
|
||||
// el: {{an existing dom element}}
|
||||
// views: {{dataset views}}
|
||||
// state: {{state configuration -- see below}}
|
||||
// });
|
||||
// </pre>
|
||||
//
|
||||
// ### 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!).
|
||||
//
|
||||
// <pre>
|
||||
// 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
|
||||
// })
|
||||
// }
|
||||
// ];
|
||||
// </pre>
|
||||
//
|
||||
// **state**: standard state config for this view. This state is slightly
|
||||
// special as it includes config of many of the subviews.
|
||||
//
|
||||
// <pre>
|
||||
// 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
|
||||
// }
|
||||
// </pre>
|
||||
//
|
||||
// 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"> \
|
||||
<ul class="navigation"> \
|
||||
{{#views}} \
|
||||
<li><a href="#{{id}}" data-view="{{id}}" class="btn">{{label}}</a> \
|
||||
{{/views}} \
|
||||
</ul> \
|
||||
<div class="recline-results-info"> \
|
||||
Results found <span class="doc-count">{{docCount}}</span> \
|
||||
</div> \
|
||||
<div class="menu-right"> \
|
||||
<a href="#" class="btn" data-action="filters">Filters</a> \
|
||||
<a href="#" class="btn" data-action="facets">Facets</a> \
|
||||
</div> \
|
||||
<div class="query-editor-here" style="display:inline;"></div> \
|
||||
<div class="clearfix"></div> \
|
||||
</div> \
|
||||
<div class="data-view-container"></div> \
|
||||
</div> \
|
||||
',
|
||||
events: {
|
||||
'click .menu-right a': '_onMenuClick',
|
||||
'click .navigation a': '_onSwitchView'
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
var self = this;
|
||||
this.el = $(this.el);
|
||||
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.Grid({
|
||||
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')
|
||||
}),
|
||||
}];
|
||||
}
|
||||
// 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.model.bind('query:start', function() {
|
||||
self.notify({loader: true, persist: true});
|
||||
});
|
||||
this.model.bind('query:done', function() {
|
||||
self.clearNotifications();
|
||||
self.el.find('.doc-count').text(self.model.docCount || 'Unknown');
|
||||
});
|
||||
this.model.bind('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
|
||||
this.model.fetch()
|
||||
.done(function(dataset) {
|
||||
self.model.query(self.state.get('query'));
|
||||
})
|
||||
.fail(function(error) {
|
||||
self.notify({message: error.message, category: 'error', persist: true});
|
||||
});
|
||||
},
|
||||
|
||||
setReadOnly: function() {
|
||||
this.el.addClass('recline-read-only');
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var tmplData = this.model.toTemplateJSON();
|
||||
tmplData.views = this.pageViews;
|
||||
var template = Mustache.render(this.template, tmplData);
|
||||
$(this.el).html(template);
|
||||
var $dataViewContainer = this.el.find('.data-view-container');
|
||||
_.each(this.pageViews, function(view, pageName) {
|
||||
$dataViewContainer.append(view.view.el);
|
||||
});
|
||||
var pager = new recline.View.Pager({
|
||||
model: this.model.queryState
|
||||
});
|
||||
this.el.find('.recline-results-info').after(pager.el);
|
||||
var queryEditor = new recline.View.QueryEditor({
|
||||
model: this.model.queryState
|
||||
});
|
||||
this.el.find('.query-editor-here').append(queryEditor.el);
|
||||
var filterEditor = new recline.View.FilterEditor({
|
||||
model: this.model.queryState
|
||||
});
|
||||
this.$filterEditor = filterEditor.el;
|
||||
this.el.find('.header').append(filterEditor.el);
|
||||
var facetViewer = new recline.View.FacetViewer({
|
||||
model: this.model
|
||||
});
|
||||
this.$facetViewer = facetViewer.el;
|
||||
this.el.find('.header').append(facetViewer.el);
|
||||
},
|
||||
|
||||
updateNav: function(pageName) {
|
||||
this.el.find('.navigation li').removeClass('active');
|
||||
this.el.find('.navigation li a').removeClass('disabled');
|
||||
var $el = this.el.find('.navigation li a[data-view="' + pageName + '"]');
|
||||
$el.parent().addClass('active');
|
||||
$el.addClass('disabled');
|
||||
// show the specific page
|
||||
_.each(this.pageViews, function(view, idx) {
|
||||
if (view.id === pageName) {
|
||||
view.view.el.show();
|
||||
view.view.trigger('view:show');
|
||||
} else {
|
||||
view.view.el.hide();
|
||||
view.view.trigger('view:hide');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_onMenuClick: function(e) {
|
||||
e.preventDefault();
|
||||
var action = $(e.target).attr('data-action');
|
||||
if (action === 'filters') {
|
||||
this.$filterEditor.show();
|
||||
} else if (action === 'facets') {
|
||||
this.$facetViewer.show();
|
||||
}
|
||||
},
|
||||
|
||||
_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'),
|
||||
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.model.queryState.bind('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);
|
||||
pageView.view.state.bind('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) {
|
||||
pageView.view.bind('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
|
||||
);
|
||||
if (tmplData.loader) {
|
||||
var _template = ' \
|
||||
<div class="alert alert-info alert-loader"> \
|
||||
{{message}} \
|
||||
<span class="notification-loader"> </span> \
|
||||
</div>';
|
||||
} else {
|
||||
var _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
|
||||
my.MultiView.restore = function(state) {
|
||||
var dataset = recline.Model.Dataset.restore(state);
|
||||
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() {
|
||||
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);
|
||||
|
||||
80
src/widget.facetviewer.js
Normal file
80
src/widget.facetviewer.js
Normal file
@ -0,0 +1,80 @@
|
||||
/*jshint multistr:true */
|
||||
|
||||
this.recline = this.recline || {};
|
||||
this.recline.View = this.recline.View || {};
|
||||
|
||||
(function($, my) {
|
||||
|
||||
my.FacetViewer = Backbone.View.extend({
|
||||
className: 'recline-facet-viewer well',
|
||||
template: ' \
|
||||
<a class="close js-hide" href="#">×</a> \
|
||||
<div class="facets row"> \
|
||||
<div class="span1"> \
|
||||
<h3>Facets</h3> \
|
||||
</div> \
|
||||
{{#facets}} \
|
||||
<div class="facet-summary span2 dropdown" data-facet="{{id}}"> \
|
||||
<a class="btn dropdown-toggle" data-toggle="dropdown" href="#"><i class="icon-chevron-down"></i> {{id}} {{label}}</a> \
|
||||
<ul class="facet-items dropdown-menu"> \
|
||||
{{#terms}} \
|
||||
<li><a class="facet-choice js-facet-filter" data-value="{{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-hide': 'onHide',
|
||||
'click .js-facet-filter': 'onFacetFilter'
|
||||
},
|
||||
initialize: function(model) {
|
||||
_.bindAll(this, 'render');
|
||||
this.el = $(this.el);
|
||||
this.model.facets.bind('all', this.render);
|
||||
this.model.fields.bind('all', this.render);
|
||||
this.render();
|
||||
},
|
||||
render: function() {
|
||||
var tmplData = {
|
||||
facets: this.model.facets.toJSON(),
|
||||
fields: this.model.fields.toJSON()
|
||||
};
|
||||
tmplData.facets = _.map(tmplData.facets, 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) {
|
||||
var $target= $(e.target);
|
||||
var fieldId = $target.closest('.facet-summary').attr('data-facet');
|
||||
var value = $target.attr('data-value');
|
||||
this.model.queryState.addTermFilter(fieldId, value);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
})(jQuery, recline.View);
|
||||
|
||||
109
src/widget.filtereditor.js
Normal file
109
src/widget.filtereditor.js
Normal file
@ -0,0 +1,109 @@
|
||||
/*jshint multistr:true */
|
||||
|
||||
this.recline = this.recline || {};
|
||||
this.recline.View = this.recline.View || {};
|
||||
|
||||
(function($, my) {
|
||||
|
||||
my.FilterEditor = Backbone.View.extend({
|
||||
className: 'recline-filter-editor well',
|
||||
template: ' \
|
||||
<a class="close js-hide" href="#">×</a> \
|
||||
<div class="row filters"> \
|
||||
<div class="span1"> \
|
||||
<h3>Filters</h3> \
|
||||
</div> \
|
||||
<div class="span11"> \
|
||||
<form class="form-horizontal"> \
|
||||
<div class="row"> \
|
||||
<div class="span6"> \
|
||||
{{#termFilters}} \
|
||||
<div class="control-group filter-term filter" data-filter-id={{id}}> \
|
||||
<label class="control-label" for="">{{label}}</label> \
|
||||
<div class="controls"> \
|
||||
<div class="input-append"> \
|
||||
<input type="text" value="{{value}}" name="{{fieldId}}" class="span4" data-filter-field="{{fieldId}}" data-filter-id="{{id}}" data-filter-type="term" /> \
|
||||
<a class="btn js-remove-filter"><i class="icon-remove"></i></a> \
|
||||
</div> \
|
||||
</div> \
|
||||
</div> \
|
||||
{{/termFilters}} \
|
||||
</div> \
|
||||
<div class="span4"> \
|
||||
<p>To add a filter use the column menu in the grid view.</p> \
|
||||
<button type="submit" class="btn">Update</button> \
|
||||
</div> \
|
||||
</form> \
|
||||
</div> \
|
||||
</div> \
|
||||
',
|
||||
events: {
|
||||
'click .js-hide': 'onHide',
|
||||
'click .js-remove-filter': 'onRemoveFilter',
|
||||
'submit form': 'onTermFiltersUpdate'
|
||||
},
|
||||
initialize: function() {
|
||||
this.el = $(this.el);
|
||||
_.bindAll(this, 'render');
|
||||
this.model.bind('change', this.render);
|
||||
this.model.bind('change:filters:new-blank', this.render);
|
||||
this.render();
|
||||
},
|
||||
render: function() {
|
||||
var tmplData = $.extend(true, {}, this.model.toJSON());
|
||||
// we will use idx in list as there id ...
|
||||
tmplData.filters = _.map(tmplData.filters, function(filter, idx) {
|
||||
filter.id = idx;
|
||||
return filter;
|
||||
});
|
||||
tmplData.termFilters = _.filter(tmplData.filters, function(filter) {
|
||||
return filter.term !== undefined;
|
||||
});
|
||||
tmplData.termFilters = _.map(tmplData.termFilters, function(filter) {
|
||||
var fieldId = _.keys(filter.term)[0];
|
||||
return {
|
||||
id: filter.id,
|
||||
fieldId: fieldId,
|
||||
label: fieldId,
|
||||
value: filter.term[fieldId]
|
||||
};
|
||||
});
|
||||
var out = Mustache.render(this.template, tmplData);
|
||||
this.el.html(out);
|
||||
// are there actually any facets to show?
|
||||
if (this.model.get('filters').length > 0) {
|
||||
this.el.show();
|
||||
} else {
|
||||
this.el.hide();
|
||||
}
|
||||
},
|
||||
onHide: function(e) {
|
||||
e.preventDefault();
|
||||
this.el.hide();
|
||||
},
|
||||
onRemoveFilter: function(e) {
|
||||
e.preventDefault();
|
||||
var $target = $(e.target);
|
||||
var filterId = $target.closest('.filter').attr('data-filter-id');
|
||||
this.model.removeFilter(filterId);
|
||||
},
|
||||
onTermFiltersUpdate: function(e) {
|
||||
var self = this;
|
||||
e.preventDefault();
|
||||
var filters = self.model.get('filters');
|
||||
var $form = $(e.target);
|
||||
_.each($form.find('input'), function(input) {
|
||||
var $input = $(input);
|
||||
var filterIndex = parseInt($input.attr('data-filter-id'));
|
||||
var value = $input.val();
|
||||
var fieldId = $input.attr('data-filter-field');
|
||||
filters[filterIndex].term[fieldId] = value;
|
||||
});
|
||||
self.model.set({filters: filters});
|
||||
self.model.trigger('change');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
})(jQuery, recline.View);
|
||||
|
||||
57
src/widget.pager.js
Normal file
57
src/widget.pager.js
Normal file
@ -0,0 +1,57 @@
|
||||
/*jshint multistr:true */
|
||||
|
||||
this.recline = this.recline || {};
|
||||
this.recline.View = this.recline.View || {};
|
||||
|
||||
(function($, my) {
|
||||
|
||||
my.Pager = Backbone.View.extend({
|
||||
className: 'recline-pager',
|
||||
template: ' \
|
||||
<div class="pagination"> \
|
||||
<ul> \
|
||||
<li class="prev action-pagination-update"><a href="">«</a></li> \
|
||||
<li class="active"><a><input name="from" type="text" value="{{from}}" /> – <input name="to" type="text" value="{{to}}" /> </a></li> \
|
||||
<li class="next action-pagination-update"><a href="">»</a></li> \
|
||||
</ul> \
|
||||
</div> \
|
||||
',
|
||||
|
||||
events: {
|
||||
'click .action-pagination-update': 'onPaginationUpdate',
|
||||
'change input': 'onFormSubmit'
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
_.bindAll(this, 'render');
|
||||
this.el = $(this.el);
|
||||
this.model.bind('change', this.render);
|
||||
this.render();
|
||||
},
|
||||
onFormSubmit: function(e) {
|
||||
e.preventDefault();
|
||||
var newFrom = parseInt(this.el.find('input[name="from"]').val());
|
||||
var newSize = parseInt(this.el.find('input[name="to"]').val()) - newFrom;
|
||||
this.model.set({size: newSize, from: newFrom});
|
||||
},
|
||||
onPaginationUpdate: function(e) {
|
||||
e.preventDefault();
|
||||
var $el = $(e.target);
|
||||
var newFrom = 0;
|
||||
if ($el.parent().hasClass('prev')) {
|
||||
newFrom = this.model.get('from') - Math.max(0, this.model.get('size'));
|
||||
} else {
|
||||
newFrom = this.model.get('from') + this.model.get('size');
|
||||
}
|
||||
this.model.set({from: newFrom});
|
||||
},
|
||||
render: function() {
|
||||
var tmplData = this.model.toJSON();
|
||||
tmplData.to = this.model.get('from') + this.model.get('size');
|
||||
var templated = Mustache.render(this.template, tmplData);
|
||||
this.el.html(templated);
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery, recline.View);
|
||||
|
||||
44
src/widget.queryeditor.js
Normal file
44
src/widget.queryeditor.js
Normal file
@ -0,0 +1,44 @@
|
||||
/*jshint multistr:true */
|
||||
|
||||
this.recline = this.recline || {};
|
||||
this.recline.View = this.recline.View || {};
|
||||
|
||||
(function($, my) {
|
||||
|
||||
my.QueryEditor = Backbone.View.extend({
|
||||
className: 'recline-query-editor',
|
||||
template: ' \
|
||||
<form action="" method="GET" class="form-inline"> \
|
||||
<div class="input-prepend text-query"> \
|
||||
<span class="add-on"><i class="icon-search"></i></span> \
|
||||
<input type="text" name="q" value="{{q}}" class="span2" placeholder="Search data ..." class="search-query" /> \
|
||||
</div> \
|
||||
<button type="submit" class="btn">Go »</button> \
|
||||
</form> \
|
||||
',
|
||||
|
||||
events: {
|
||||
'submit form': 'onFormSubmit'
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
_.bindAll(this, 'render');
|
||||
this.el = $(this.el);
|
||||
this.model.bind('change', this.render);
|
||||
this.render();
|
||||
},
|
||||
onFormSubmit: function(e) {
|
||||
e.preventDefault();
|
||||
var query = this.el.find('.text-query input').val();
|
||||
this.model.set({q: query});
|
||||
},
|
||||
render: function() {
|
||||
var tmplData = this.model.toJSON();
|
||||
tmplData.to = this.model.get('from') + this.model.get('size');
|
||||
var templated = Mustache.render(this.template, tmplData);
|
||||
this.el.html(templated);
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery, recline.View);
|
||||
|
||||
@ -155,11 +155,11 @@ test("write", function() {
|
||||
stop();
|
||||
|
||||
var id = parseInt(Math.random()*100000000).toString();
|
||||
var doc = {
|
||||
var rec = {
|
||||
id: id,
|
||||
title: 'my title'
|
||||
};
|
||||
var jqxhr = backend.upsert(doc);
|
||||
var jqxhr = backend.upsert(rec);
|
||||
jqxhr.done(function(data) {
|
||||
ok(data.ok);
|
||||
equal(data._id, id);
|
||||
@ -167,16 +167,16 @@ test("write", function() {
|
||||
equal(data._version, 1);
|
||||
|
||||
// update
|
||||
doc.title = 'new title';
|
||||
var jqxhr = backend.upsert(doc);
|
||||
rec.title = 'new title';
|
||||
var jqxhr = backend.upsert(rec);
|
||||
jqxhr.done(function(data) {
|
||||
equal(data._version, 2);
|
||||
|
||||
// delete
|
||||
var jqxhr = backend.delete(doc.id);
|
||||
var jqxhr = backend.delete(rec.id);
|
||||
jqxhr.done(function(data) {
|
||||
ok(data.ok);
|
||||
doc = null;
|
||||
rec = null;
|
||||
|
||||
// try to get ...
|
||||
var jqxhr = backend.get(id);
|
||||
@ -233,10 +233,10 @@ test("query", function() {
|
||||
|
||||
dataset.fetch().done(function(dataset) {
|
||||
deepEqual(['_created', '_last_modified', 'end', 'owner', 'start', 'title'], _.pluck(dataset.fields.toJSON(), 'id'));
|
||||
dataset.query().then(function(docList) {
|
||||
dataset.query().then(function(recList) {
|
||||
equal(3, dataset.docCount);
|
||||
equal(3, docList.length);
|
||||
equal('Note 1', docList.models[0].get('title'));
|
||||
equal(3, recList.length);
|
||||
equal('Note 1', recList.models[0].get('title'));
|
||||
start();
|
||||
});
|
||||
});
|
||||
@ -254,14 +254,14 @@ test("write", function() {
|
||||
stop();
|
||||
|
||||
var id = parseInt(Math.random()*100000000).toString();
|
||||
var doc = new recline.Model.Document({
|
||||
var rec = new recline.Model.Record({
|
||||
id: id,
|
||||
title: 'my title'
|
||||
});
|
||||
doc.backend = backend;
|
||||
doc.dataset = dataset;
|
||||
dataset.currentDocuments.add(doc);
|
||||
var jqxhr = doc.save();
|
||||
rec.backend = backend;
|
||||
rec.dataset = dataset;
|
||||
dataset.currentRecords.add(rec);
|
||||
var jqxhr = rec.save();
|
||||
jqxhr.done(function(data) {
|
||||
ok(data.ok);
|
||||
equal(data._id, id);
|
||||
@ -269,29 +269,29 @@ test("write", function() {
|
||||
equal(data._version, 1);
|
||||
|
||||
// update
|
||||
doc.set({title: 'new title'});
|
||||
var jqxhr = doc.save();
|
||||
rec.set({title: 'new title'});
|
||||
var jqxhr = rec.save();
|
||||
jqxhr.done(function(data) {
|
||||
equal(data._version, 2);
|
||||
|
||||
// delete
|
||||
var jqxhr = doc.destroy();
|
||||
var jqxhr = rec.destroy();
|
||||
jqxhr.done(function(data) {
|
||||
ok(data.ok);
|
||||
doc = null;
|
||||
rec = null;
|
||||
|
||||
// try to get ...
|
||||
var olddoc = new recline.Model.Document({id: id});
|
||||
equal(olddoc.get('title'), null);
|
||||
olddoc.dataset = dataset;
|
||||
olddoc.backend = backend;
|
||||
var jqxhr = olddoc.fetch();
|
||||
var oldrec = new recline.Model.Record({id: id});
|
||||
equal(oldrec.get('title'), null);
|
||||
oldrec.dataset = dataset;
|
||||
oldrec.backend = backend;
|
||||
var jqxhr = oldrec.fetch();
|
||||
jqxhr.done(function(data) {
|
||||
// should not be here
|
||||
ok(false, 'Should have got 404');
|
||||
}).error(function(error) {
|
||||
equal(error.status, 404);
|
||||
equal(typeof olddoc.get('title'), 'undefined');
|
||||
equal(typeof oldrec.get('title'), 'undefined');
|
||||
start();
|
||||
});
|
||||
});
|
||||
|
||||
@ -26,7 +26,7 @@ test("parseCSV", function() {
|
||||
'"Other, AN", 12:35\n';
|
||||
var dataset = recline.Backend.CSV.csvToDataset(csv);
|
||||
dataset.query();
|
||||
equal(dataset.currentDocuments.length, 3);
|
||||
equal(dataset.currentRecords.length, 3);
|
||||
});
|
||||
|
||||
test("parseCSVsemicolon", function() {
|
||||
|
||||
@ -30,8 +30,8 @@ test('query', function () {
|
||||
, from: 2
|
||||
};
|
||||
var out = data.query(queryObj);
|
||||
deepEqual(out.documents[0], memoryData[2]);
|
||||
equal(out.documents.length, 4);
|
||||
deepEqual(out.records[0], memoryData[2]);
|
||||
equal(out.records.length, 4);
|
||||
equal(out.total, 6);
|
||||
});
|
||||
|
||||
@ -43,18 +43,18 @@ test('query sort', function () {
|
||||
]
|
||||
};
|
||||
var out = data.query(queryObj);
|
||||
equal(out.documents[0].x, 6);
|
||||
equal(out.records[0].x, 6);
|
||||
});
|
||||
|
||||
test('query string', function () {
|
||||
var data = _wrapData();
|
||||
var out = data.query({q: 'UK'});
|
||||
equal(out.total, 3);
|
||||
deepEqual(_.pluck(out.documents, 'country'), ['UK', 'UK', 'UK']);
|
||||
deepEqual(_.pluck(out.records, 'country'), ['UK', 'UK', 'UK']);
|
||||
|
||||
var out = data.query({q: 'UK 6'})
|
||||
equal(out.total, 1);
|
||||
deepEqual(out.documents[0].id, 1);
|
||||
deepEqual(out.records[0].id, 1);
|
||||
});
|
||||
|
||||
test('filters', function () {
|
||||
@ -63,7 +63,7 @@ test('filters', function () {
|
||||
query.addTermFilter('country', 'UK');
|
||||
var out = data.query(query.toJSON());
|
||||
equal(out.total, 3);
|
||||
deepEqual(_.pluck(out.documents, 'country'), ['UK', 'UK', 'UK']);
|
||||
deepEqual(_.pluck(out.records, 'country'), ['UK', 'UK', 'UK']);
|
||||
});
|
||||
|
||||
test('facet', function () {
|
||||
@ -118,7 +118,7 @@ var memoryData = {
|
||||
, id: 'test-dataset'
|
||||
},
|
||||
fields: [{id: 'x'}, {id: 'y'}, {id: 'z'}, {id: 'country'}, {id: 'label'}],
|
||||
documents: [
|
||||
records: [
|
||||
{id: 0, x: 1, y: 2, z: 3, country: 'DE', label: 'first'}
|
||||
, {id: 1, x: 2, y: 4, z: 6, country: 'UK', label: 'second'}
|
||||
, {id: 2, x: 3, y: 6, z: 9, country: 'US', label: 'third'}
|
||||
@ -129,16 +129,16 @@ var memoryData = {
|
||||
};
|
||||
|
||||
function makeBackendDataset() {
|
||||
var dataset = new recline.Backend.Memory.createDataset(memoryData.documents, null, memoryData.metadata);
|
||||
var dataset = new recline.Backend.Memory.createDataset(memoryData.records, null, memoryData.metadata);
|
||||
return dataset;
|
||||
}
|
||||
|
||||
test('createDataset', function () {
|
||||
var dataset = recline.Backend.Memory.createDataset(memoryData.documents);
|
||||
var dataset = recline.Backend.Memory.createDataset(memoryData.records);
|
||||
equal(dataset.fields.length, 6);
|
||||
deepEqual(['id', 'x', 'y', 'z', 'country', 'label'], dataset.fields.pluck('id'));
|
||||
dataset.query();
|
||||
equal(memoryData.documents.length, dataset.currentDocuments.length);
|
||||
equal(memoryData.records.length, dataset.currentRecords.length);
|
||||
});
|
||||
|
||||
test('basics', function () {
|
||||
@ -162,8 +162,8 @@ test('query', function () {
|
||||
size: 4
|
||||
, from: 2
|
||||
};
|
||||
dataset.query(queryObj).then(function(documentList) {
|
||||
deepEqual(data[2], documentList.models[0].toJSON());
|
||||
dataset.query(queryObj).then(function(recordList) {
|
||||
deepEqual(data[2], recordList.models[0].toJSON());
|
||||
});
|
||||
});
|
||||
|
||||
@ -177,7 +177,7 @@ test('query sort', function () {
|
||||
]
|
||||
};
|
||||
dataset.query(queryObj).then(function() {
|
||||
var doc0 = dataset.currentDocuments.models[0].toJSON();
|
||||
var doc0 = dataset.currentRecords.models[0].toJSON();
|
||||
equal(doc0.x, 6);
|
||||
});
|
||||
});
|
||||
@ -186,13 +186,13 @@ test('query string', function () {
|
||||
var dataset = makeBackendDataset();
|
||||
dataset.fetch();
|
||||
dataset.query({q: 'UK'}).then(function() {
|
||||
equal(dataset.currentDocuments.length, 3);
|
||||
deepEqual(dataset.currentDocuments.pluck('country'), ['UK', 'UK', 'UK']);
|
||||
equal(dataset.currentRecords.length, 3);
|
||||
deepEqual(dataset.currentRecords.pluck('country'), ['UK', 'UK', 'UK']);
|
||||
});
|
||||
|
||||
dataset.query({q: 'UK 6'}).then(function() {
|
||||
equal(dataset.currentDocuments.length, 1);
|
||||
deepEqual(dataset.currentDocuments.models[0].id, 1);
|
||||
equal(dataset.currentRecords.length, 1);
|
||||
deepEqual(dataset.currentRecords.models[0].id, 1);
|
||||
});
|
||||
});
|
||||
|
||||
@ -200,8 +200,8 @@ test('filters', function () {
|
||||
var dataset = makeBackendDataset();
|
||||
dataset.queryState.addTermFilter('country', 'UK');
|
||||
dataset.query().then(function() {
|
||||
equal(dataset.currentDocuments.length, 3);
|
||||
deepEqual(dataset.currentDocuments.pluck('country'), ['UK', 'UK', 'UK']);
|
||||
equal(dataset.currentRecords.length, 3);
|
||||
deepEqual(dataset.currentRecords.pluck('country'), ['UK', 'UK', 'UK']);
|
||||
});
|
||||
});
|
||||
|
||||
@ -247,7 +247,7 @@ test('update and delete', function () {
|
||||
// Test Delete
|
||||
doc1.destroy().then(function() {
|
||||
equal(data.data.length, 5);
|
||||
equal(data.data[0].x, memoryData.documents[1].x);
|
||||
equal(data.data[0].x, memoryData.records[1].x);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -27,7 +27,6 @@
|
||||
|
||||
<script type="text/javascript" src="base.js"></script>
|
||||
|
||||
<script type="text/javascript" src="../src/util.js"></script>
|
||||
<script type="text/javascript" src="../src/model.js"></script>
|
||||
<script type="text/javascript" src="../src/backend/base.js"></script>
|
||||
<script type="text/javascript" src="../src/backend/memory.js"></script>
|
||||
@ -41,20 +40,24 @@
|
||||
<script type="text/javascript" src="backend.elasticsearch.test.js"></script>
|
||||
<script type="text/javascript" src="backend/csv.test.js"></script>
|
||||
|
||||
<script type="text/javascript" src="../src/view.js"></script>
|
||||
<script type="text/javascript" src="../src/view-grid.js"></script>
|
||||
<script type="text/javascript" src="../src/view-slickgrid.js"></script>
|
||||
<script type="text/javascript" src="../src/view-transform-dialog.js"></script>
|
||||
<script type="text/javascript" src="../src/view-graph.js"></script>
|
||||
<script type="text/javascript" src="../src/view-map.js"></script>
|
||||
<script type="text/javascript" src="../src/view-timeline.js"></script>
|
||||
<script type="text/javascript" src="../src/widget.pager.js"></script>
|
||||
<script type="text/javascript" src="../src/widget.queryeditor.js"></script>
|
||||
<script type="text/javascript" src="../src/widget.filtereditor.js"></script>
|
||||
<script type="text/javascript" src="../src/widget.facetviewer.js"></script>
|
||||
<script type="text/javascript" src="../src/view.multiview.js"></script>
|
||||
|
||||
<script type="text/javascript" src="view-grid.test.js"></script>
|
||||
<script type="text/javascript" src="view-slickgrid.test.js"></script>
|
||||
<script type="text/javascript" src="view-graph.test.js"></script>
|
||||
<script type="text/javascript" src="view-map.test.js"></script>
|
||||
<script type="text/javascript" src="view-timeline.test.js"></script>
|
||||
<script type="text/javascript" src="view.test.js"></script>
|
||||
<script type="text/javascript" src="view.multiview.test.js"></script>
|
||||
<script type="text/javascript" src="util.test.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@ -39,7 +39,7 @@ test('Field: basics', function () {
|
||||
});
|
||||
|
||||
test('Field: default renderers', function () {
|
||||
var doc = new recline.Model.Document({
|
||||
var doc = new recline.Model.Record({
|
||||
x: 12.3,
|
||||
myobject: {a: 1, b: 2},
|
||||
link: 'http://abc.com/',
|
||||
@ -74,7 +74,7 @@ test('Field: default renderers', function () {
|
||||
});
|
||||
|
||||
test('Field: custom deriver and renderer', function () {
|
||||
var doc = new recline.Model.Document({x: 123});
|
||||
var doc = new recline.Model.Record({x: 123});
|
||||
var cellRenderer = function(value, field) {
|
||||
return '<span class="field-' + field.id + '">' + value + '</span>';
|
||||
}
|
||||
|
||||
@ -2,10 +2,10 @@
|
||||
module("Util");
|
||||
|
||||
test('parseHashUrl', function () {
|
||||
var out = recline.Util.parseHashUrl('graph?x=y');
|
||||
var out = recline.View.parseHashUrl('graph?x=y');
|
||||
equal(out.path, 'graph');
|
||||
equal(out.query, '?x=y');
|
||||
var out = recline.Util.parseHashUrl('graph');
|
||||
var out = recline.View.parseHashUrl('graph');
|
||||
equal(out.path, 'graph');
|
||||
equal(out.query, '');
|
||||
});
|
||||
@ -15,7 +15,7 @@ test('composeQueryString', function () {
|
||||
x: 'y',
|
||||
a: 'b'
|
||||
};
|
||||
var out = recline.Util.composeQueryString(params);
|
||||
var out = recline.View.composeQueryString(params);
|
||||
equal(out, '?x=y&a=b');
|
||||
});
|
||||
|
||||
|
||||
@ -38,7 +38,7 @@ test('state', function () {
|
||||
test('new GridRow View', function () {
|
||||
var $el = $('<tr />');
|
||||
$('.fixtures .test-datatable').append($el);
|
||||
var doc = new recline.Model.Document({
|
||||
var doc = new recline.Model.Record({
|
||||
'id': 1,
|
||||
'b': '2',
|
||||
'a': '1'
|
||||
|
||||
@ -11,12 +11,12 @@ var GeoJSONFixture = {
|
||||
{id: 'z'},
|
||||
{id: 'geom'}
|
||||
];
|
||||
var documents = [
|
||||
var records = [
|
||||
{id: 0, x: 1, y: 2, z: 3, geom: '{"type":"Point","coordinates":[13.40,52.35]}'},
|
||||
{id: 1, x: 2, y: 4, z: 6, geom: {type:"Point",coordinates:[13.40,52.35]}},
|
||||
{id: 2, x: 3, y: 6, z: 9, geom: {type:"LineString",coordinates:[[100.0, 0.0],[101.0, 1.0]]}}
|
||||
];
|
||||
var dataset = recline.Backend.Memory.createDataset(documents, fields);
|
||||
var dataset = recline.Backend.Memory.createDataset(records, fields);
|
||||
return dataset;
|
||||
}
|
||||
};
|
||||
@ -55,12 +55,12 @@ test('Lat/Lon geom fields', function () {
|
||||
// Check that all markers were created
|
||||
equal(_getFeaturesCount(view.features),6);
|
||||
|
||||
// Delete a document
|
||||
view.model.currentDocuments.remove(view.model.currentDocuments.get('1'));
|
||||
// Delete a record
|
||||
view.model.currentRecords.remove(view.model.currentRecords.get('1'));
|
||||
equal(_getFeaturesCount(view.features),5);
|
||||
|
||||
// Add a new one
|
||||
view.model.currentDocuments.add({id: 7, x: 7, y: 14, z: 21, country: 'KX', label: 'seventh', lat:13.23, lon:23.56}),
|
||||
view.model.currentRecords.add({id: 7, x: 7, y: 14, z: 21, country: 'KX', label: 'seventh', lat:13.23, lon:23.56}),
|
||||
equal(_getFeaturesCount(view.features),6);
|
||||
|
||||
view.remove();
|
||||
@ -79,17 +79,34 @@ test('GeoJSON geom field', function () {
|
||||
// Check that all features were created
|
||||
equal(_getFeaturesCount(view.features),3);
|
||||
|
||||
// Delete a document
|
||||
view.model.currentDocuments.remove(view.model.currentDocuments.get('2'));
|
||||
// Delete a record
|
||||
view.model.currentRecords.remove(view.model.currentRecords.get('2'));
|
||||
equal(_getFeaturesCount(view.features),2);
|
||||
|
||||
// Add it back
|
||||
view.model.currentDocuments.add({id: 2, x: 3, y: 6, z: 9, geom: {type:"LineString",coordinates:[[100.0, 0.0],[101.0, 1.0]]}}),
|
||||
view.model.currentRecords.add({id: 2, x: 3, y: 6, z: 9, geom: {type:"LineString",coordinates:[[100.0, 0.0],[101.0, 1.0]]}}),
|
||||
equal(_getFeaturesCount(view.features),3);
|
||||
|
||||
view.remove();
|
||||
});
|
||||
|
||||
test('geom field non-GeoJSON', function () {
|
||||
var data = [{
|
||||
location: { lon: 47, lat: 53},
|
||||
title: 'abc'
|
||||
}];
|
||||
var dataset = recline.Backend.Memory.createDataset(data);
|
||||
var view = new recline.View.Map({
|
||||
model: dataset
|
||||
});
|
||||
|
||||
//Fire query, otherwise the map won't be initialized
|
||||
dataset.query();
|
||||
|
||||
// Check that all features were created
|
||||
equal(_getFeaturesCount(view.features), 1);
|
||||
});
|
||||
|
||||
test('Popup', function () {
|
||||
var dataset = GeoJSONFixture.getDataset();
|
||||
var view = new recline.View.Map({
|
||||
|
||||
@ -16,7 +16,7 @@ test('basic', function () {
|
||||
assertPresent('.slick-header-column[title="x"]');
|
||||
equal($('.slick-header-column').length,dataset.fields.length);
|
||||
|
||||
equal(dataset.currentDocuments.length,view.grid.getDataLength());
|
||||
equal(dataset.currentRecords.length,view.grid.getDataLength());
|
||||
|
||||
view.remove();
|
||||
});
|
||||
|
||||
@ -17,16 +17,16 @@ test('extract dates and timelineJSON', function () {
|
||||
'headline': '',
|
||||
'date': [
|
||||
{
|
||||
'startDate': '2012-03-20',
|
||||
'startDate': new Date('2012-03-20'),
|
||||
'endDate': null,
|
||||
'headline': '2012-03-20',
|
||||
'text': ''
|
||||
'headline': '1',
|
||||
'text': '<div><strong>Date</strong>: 2012-03-20</div><div><strong>title</strong>: 1</div>'
|
||||
},
|
||||
{
|
||||
'startDate': '2012-03-25',
|
||||
'startDate': new Date('2012-03-25'),
|
||||
'endDate': null,
|
||||
'headline': '2012-03-25',
|
||||
'text': ''
|
||||
'headline': '2',
|
||||
'text': '<div><strong>Date</strong>: 2012-03-25</div><div><strong>title</strong>: 2</div>'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ test('basic explorer functionality', function () {
|
||||
var $el = $('<div class="test-view-explorer-basic" />');
|
||||
$('.fixtures .data-explorer-here').append($el);
|
||||
var dataset = Fixture.getDataset();
|
||||
var explorer = new recline.View.DataExplorer({
|
||||
var explorer = new recline.View.MultiView({
|
||||
model: dataset,
|
||||
el: $el
|
||||
});
|
||||
@ -21,7 +21,7 @@ test('get State', function () {
|
||||
var dataset = Fixture.getDataset();
|
||||
var url = 'xyz';
|
||||
dataset.set({url: url});
|
||||
var explorer = new recline.View.DataExplorer({
|
||||
var explorer = new recline.View.MultiView({
|
||||
model: dataset,
|
||||
el: $el
|
||||
});
|
||||
@ -41,7 +41,7 @@ test('initialize state', function () {
|
||||
var $el = $('<div class="test-view-explorer-init-state" />');
|
||||
$('.fixtures .data-explorer-here').append($el);
|
||||
var dataset = Fixture.getDataset();
|
||||
var explorer = new recline.View.DataExplorer({
|
||||
var explorer = new recline.View.MultiView({
|
||||
model: dataset,
|
||||
el: $el,
|
||||
state: {
|
||||
@ -74,11 +74,11 @@ test('initialize state', function () {
|
||||
|
||||
test('restore (from serialized state)', function() {
|
||||
var dataset = Fixture.getDataset();
|
||||
var explorer = new recline.View.DataExplorer({
|
||||
var explorer = new recline.View.MultiView({
|
||||
model: dataset,
|
||||
});
|
||||
var state = explorer.state.toJSON();
|
||||
var explorerNew = recline.View.DataExplorer.restore(state);
|
||||
var explorerNew = recline.View.MultiView.restore(state);
|
||||
var out = explorerNew.state.toJSON();
|
||||
equal(out.backend, state.backend);
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user