/*jshint multistr:true */
-(function($, my) {
- "use strict";diff --git a/dist/recline.css b/dist/recline.css index ed38db58..c716b38c 100644 --- a/dist/recline.css +++ b/dist/recline.css @@ -276,22 +276,42 @@ div.data-table-cell-content-numeric > a.data-table-cell-edit { float: right; margin-left: 5px; padding-left: 5px; - border-left: solid 2px #ddd; } -.header .recline-results-info { - line-height: 28px; +.recline-results-info { + line-height: 35px; margin-left: 20px; float: left; } +.recline-data-explorer .data-view-sidebar > div { + margin-top: 5px; + margin-bottom: 10px; +} + +.recline-data-explorer .radio, +.recline-data-explorer .checkbox { + padding-left: 20px; +} + +.recline-data-explorer .editor-update-map { + margin: 30px 0px 20px 0px; +} + +.recline-data-explorer label { + font-weight: normal; +} + /********************************************************** * Query Editor *********************************************************/ -.header .recline-query-editor { +.recline-query-editor { float: right; - height: 30px; + height: 35px; + padding-right: 5px; + margin-right: 5px; + border-right: solid 2px #ddd; } .header .input-prepend { @@ -312,11 +332,11 @@ div.data-table-cell-content-numeric > a.data-table-cell-edit { vertical-align: top; } -.header .recline-query-editor form button { +.recline-query-editor form button { vertical-align: top; } -.header .recline-query-editor label { +.recline-query-editor label { display:none; } @@ -324,28 +344,78 @@ div.data-table-cell-content-numeric > a.data-table-cell-edit { * Pager *********************************************************/ -.header .recline-pager { +.recline-pager { float: left; margin: auto; display: block; margin-left: 20px; } -.header .recline-pager .pagination label { +.recline-pager .pagination li { + display: inline-block; +} + +.recline-pager .pagination label { display:none; } -.header .recline-pager .pagination input { - width: 30px; - height: 18px; +.recline-pager .pagination input { + width: 40px; + height: 25px; padding: 2px 4px; margin: 0; - margin-top: -4px; + margin-top: -2px; + + border: 1px solid #cccccc; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + transition: border linear 0.2s, box-shadow linear 0.2s; + border-radius: 4px; + + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; + -webkit-border-radius: 4px; + + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-transition: border linear 0.2s, box-shadow linear 0.2s; + -moz-border-radius: 4px; + + -o-transition: border linear 0.2s, box-shadow linear 0.2s; } -.header .recline-pager .pagination a { - line-height: 26px; - padding: 0 6px; +.recline-pager .pagination a { + float: none; + margin-left: -5px; + color: #555; +} + +.recline-pager .pagination .page-range { + height: 34px; + padding: 5px 8px; + margin-left: -5px; + border: 1px solid #ddd; +} + +.recline-pager .pagination .page-range a { + padding: 0px 12px; + border: none; +} + +.recline-pager .pagination .page-range a:hover { + background-color: #ffffff; +} + +.recline-pager .pagination > li:first-child > a { + border-bottom-left-radius: 4px; + border-top-left-radius: 4px; + border-bottom-right-radius: 0px; + border-top-right-radius: 0px; +} + +.recline-pager .pagination > li:last-child > a { + border-bottom-right-radius: 4px; + border-top-right-radius: 4px; + border-bottom-left-radius: 0px; + border-top-left-radius: 0px; } /********************************************************** @@ -357,6 +427,31 @@ div.data-table-cell-content-numeric > a.data-table-cell-edit { display: none; } +.recline-filter-editor .filters { + margin: 20px 0px; +} + +.recline-filter-editor h3 { + margin-top: 4px; +} + +.recline-filter-editor .filter { + margin-top: 20px; +} + +.recline-filter-editor .filter .form-group { + margin-bottom: 0px; +} + +.recline-filter-editor .filter input, +.recline-filter-editor .filter label { + margin: 0px; +} + +.recline-filter-editor .js-edit button { + margin: 25px 0px 0px 0px; +} + .recline-filter-editor .filter-term a { font-size: 18px; } @@ -369,6 +464,20 @@ div.data-table-cell-content-numeric > a.data-table-cell-edit { .recline-filter-editor input { margin-top: 0.5em; + margin-bottom: 10px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + border: 1px solid #cccccc; +} + +.recline-filter-editor label { + font-weight: normal; + display: block; +} + +.recline-filter-editor legend { + margin-bottom: 5px; } .recline-filter-editor .add-filter { @@ -392,22 +501,30 @@ div.data-table-cell-content-numeric > a.data-table-cell-edit { padding: 0; } -.recline-fields-view .fields-list .accordion-heading, -.recline-fields-view .fields-list h3 -{ - margin: 3px 0 3px 5px; +.recline-fields-view .panel { + background-color: #f5f5f5; + border: 1px solid #e5e5e5; } -.recline-fields-view .fields-list .accordion-heading a, -.recline-fields-view .fields-list .accordion-heading h4 { +.recline-fields-view .panel-group h3 { + padding-left: 10px; +} + +.recline-fields-view .fields-list .panel-heading { + padding: 2px 5px; + margin: 1px 0px 1px 5px; +} + +.recline-fields-view .panel a, +.recline-fields-view .panel h4 { display: inline; } -.recline-fields-view .fields-list .accordion-heading a { +.recline-fields-view .panel a { padding: 0; } -.recline-fields-view .fields-list .accordion-heading h4 { +.recline-fields-view .panel h4 { word-wrap: break-word } @@ -486,6 +603,10 @@ classes should alter those! .recline-slickgrid .slick-header-column:hover, .slick-header-column-active { } +.recline-slickgrid .slick-header-column.ui-state-default { + height: 26px; +} + .recline-slickgrid .slick-headerrow { background: #fafafa; } @@ -624,16 +745,16 @@ classes should alter those! .recline-slickgrid .recline-row-delete { font-size: 12px; - padding: 2px; - width: 22px; - height: 14px; + padding: 3px; + width: 29px; + height: 18px; line-height: 13px; } .recline-cell-reorder { font-size: 12px; - padding: 2px; - width: 22px; + padding: 1px; + width: 31px; height: 14px; line-height: 13px; cursor: move; diff --git a/dist/recline.dataset.min.js b/dist/recline.dataset.min.js new file mode 100644 index 00000000..8ca94447 --- /dev/null +++ b/dist/recline.dataset.min.js @@ -0,0 +1 @@ +this.recline=this.recline||{};this.recline.Model=this.recline.Model||{};(function(my){"use strict";var Deferred=typeof jQuery!=="undefined"&&jQuery.Deferred||_.Deferred;my.Dataset=Backbone.Model.extend({constructor:function Dataset(){Backbone.Model.prototype.constructor.apply(this,arguments)},initialize:function(){var self=this;_.bindAll(this,"query");this.backend=null;if(this.get("backend")){this.backend=this._backendFromString(this.get("backend"))}else{if(this.get("records")){this.backend=recline.Backend.Memory}}this.fields=new my.FieldList;this.records=new my.RecordList;this._changes={deletes:[],updates:[],creates:[]};this.facets=new my.FacetList;this.recordCount=null;this.queryState=new my.Query;this.queryState.bind("change facet:add",function(){self.query()});this._store=this.backend;this._handleResult=this.backend!=null&&_.has(this.backend,"handleQueryResult")?this.backend.handleQueryResult:this._handleQueryResult;if(this.backend==recline.Backend.Memory){this.fetch()}},sync:function(method,model,options){return this.backend.sync(method,model,options)},fetch:function(){var self=this;var dfd=new Deferred;if(this.backend!==recline.Backend.Memory){this.backend.fetch(this.toJSON()).done(handleResults).fail(function(args){dfd.reject(args)})}else{handleResults({records:this.get("records"),fields:this.get("fields"),useMemoryStore:true})}function handleResults(results){var fields=self.get("fields")||results.fields;var out=self._normalizeRecordsAndFields(results.records,fields);if(results.useMemoryStore){self._store=new recline.Backend.Memory.Store(out.records,out.fields)}self.set(results.metadata);self.fields.reset(out.fields);self.query().done(function(){dfd.resolve(self)}).fail(function(args){dfd.reject(args)})}return dfd.promise()},_normalizeRecordsAndFields:function(records,fields){if(!fields&&records&&records.length>0){if(records[0]instanceof Array){fields=records[0];records=records.slice(1)}else{fields=_.map(_.keys(records[0]),function(key){return{id:key}})}}if(fields&&fields.length>0&&(fields[0]===null||typeof fields[0]!="object")){var seen={};fields=_.map(fields,function(field,index){if(field===null){field=""}else{field=field.toString()}var fieldId=field.replace(/^\s+|\s+$/g,"");if(fieldId===""){fieldId="_noname_";field=fieldId}while(fieldId in seen){seen[field]+=1;fieldId=field+seen[field]}if(!(field in seen)){seen[field]=0}return{id:fieldId}})}if(records&&records.length>0&&records[0]instanceof Array){records=_.map(records,function(doc){var tmp={};_.each(fields,function(field,idx){tmp[field.id]=doc[idx]});return tmp})}return{fields:fields,records:records}},save:function(){var self=this;return this._store.save(this._changes,this.toJSON())},query:function(queryObj){var self=this;var dfd=new Deferred;this.trigger("query:start");if(queryObj){var attributes=queryObj;if(queryObj instanceof my.Query){attributes=queryObj.toJSON()}this.queryState.set(attributes,{silent:true})}var actualQuery=this.queryState.toJSON();this._store.query(actualQuery,this.toJSON()).done(function(queryResult){self._handleResult(queryResult);self.trigger("query:done");dfd.resolve(self.records)}).fail(function(args){self.trigger("query:fail",args);dfd.reject(args)});return dfd.promise()},_handleQueryResult:function(queryResult){var self=this;self.recordCount=queryResult.total;var docs=_.map(queryResult.hits,function(hit){var _doc=new my.Record(hit);_doc.fields=self.fields;_doc.bind("change",function(doc){self._changes.updates.push(doc.toJSON())});_doc.bind("destroy",function(doc){self._changes.deletes.push(doc.toJSON())});return _doc});self.records.reset(docs);if(queryResult.facets){var facets=_.map(queryResult.facets,function(facetResult,facetId){facetResult.id=facetId;return new my.Facet(facetResult)});self.facets.reset(facets)}},toTemplateJSON:function(){var data=this.toJSON();data.recordCount=this.recordCount;data.fields=this.fields.toJSON();return data},getFieldsSummary:function(){var self=this;var query=new my.Query;query.set({size:0});this.fields.each(function(field){query.addFacet(field.id)});var dfd=new Deferred;this._store.query(query.toJSON(),this.toJSON()).done(function(queryResult){if(queryResult.facets){_.each(queryResult.facets,function(facetResult,facetId){facetResult.id=facetId;var facet=new my.Facet(facetResult);self.fields.get(facetId).facets.reset(facet)})}dfd.resolve(queryResult)});return dfd.promise()},recordSummary:function(record){return record.summary()},_backendFromString:function(backendString){var backend=null;if(recline&&recline.Backend){_.each(_.keys(recline.Backend),function(name){if(name.toLowerCase()===backendString.toLowerCase()){backend=recline.Backend[name]}})}return backend}});my.Record=Backbone.Model.extend({constructor:function Record(){Backbone.Model.prototype.constructor.apply(this,arguments)},initialize:function(){_.bindAll(this,"getFieldValue")},getFieldValue:function(field){var val=this.getFieldValueUnrendered(field);if(field&&!_.isUndefined(field.renderer)){val=field.renderer(val,field,this.toJSON())}return val},getFieldValueUnrendered:function(field){if(!field){return""}var val=this.get(field.id);if(field.deriver){val=field.deriver(val,field,this)}return val},summary:function(record){var self=this;var html='
There\'s no graph here yet because we don\'t know what fields you\'d like to see plotted.
\Please tell us by using the menu on the right and a graph will automatically appear.
\ @@ -1405,30 +1405,34 @@ my.FlotControls = Backbone.View.extend({ view.map.js | |
|---|---|
/*jshint multistr:true */
+
-this.recline = this.recline || {};
-this.recline.View = this.recline.View || {};
+
+
+ | |
Map view for a Dataset using Leaflet mapping library.+this.recline = this.recline || {}; +this.recline.View = this.recline.View || {}; +(function($, my) { + "use strict"; + + + + +
+
+
+ ¶
+
+ Map view for a Dataset using Leaflet mapping library.This view allows to plot gereferenced records on a map. The location information can be provided in 2 ways: -
Which fields in the data these correspond to can be configured via the state (and are guessed if no info is provided). -Initialization arguments are as standard for Dataset Views. State object may have the following (optional) configuration options: -
{
// geomField if specified will be used in preference to lat/lon
@@ -36,522 +172,1160 @@ have the following (optional) configuration options:
Useful attributes to know about (if e.g. customizing) -
| my.Map = Backbone.View.extend({
- template: ' \
- <div class="recline-map"> \
- <div class="panel map"></div> \
- </div> \
-', |
| These are the default (case-insensitive) names of field that are used if found. -If not found, the user will need to define the fields via the editor. | latitudeFieldNames: ['lat','latitude'],
- longitudeFieldNames: ['lon','longitude'],
- geometryFieldNames: ['geojson', 'geom','the_geom','geometry','spatial','location', 'geo', 'lonlat'],
+
- initialize: function(options) {
- var self = this;
- this.visible = true;
- this.mapReady = false; |
| this will be the Leaflet L.Map object (setup below) | this.map = null;
+ my.Map = Backbone.View.extend({
+ template: ' \
+ <div class="recline-map"> \
+ <div class="panel map"></div> \
+ </div> \
+',
+
+
+
+
+ ¶
+
+ These are the default (case-insensitive) names of field that are used if found. +If not found, the user will need to define the fields via the editor. - var stateData = _.extend({ - geomField: null, - lonField: null, - latField: null, - autoZoom: true, - cluster: false - }, - options.state - ); - this.state = new recline.Model.ObjectState(stateData); + latitudeFieldNames: ['lat','latitude'],
+ longitudeFieldNames: ['lon','longitude'],
+ geometryFieldNames: ['geojson', 'geom','the_geom','geometry','spatial','location', 'geo', 'lonlat'],
- this._clusterOptions = {
- zoomToBoundsOnClick: true, |
| disableClusteringAtZoom: 10, | maxClusterRadius: 80,
- singleMarkerMode: false,
- skipDuplicateAddTesting: true,
- animateAddingMarkers: false
- }; |
| Listen to changes in the fields | this.listenTo(this.model.fields, 'change', function() {
- self._setupGeometryField();
- self.render();
- }); |
| Listen to changes in the records | this.listenTo(this.model.records, 'add', function(doc){self.redraw('add',doc);});
- this.listenTo(this.model.records, 'change', function(doc){
- self.redraw('remove',doc);
- self.redraw('add',doc);
- });
- this.listenTo(this.model.records, 'remove', function(doc){self.redraw('remove',doc);});
- this.listenTo(this.model.records, 'reset', function(){self.redraw('reset');});
+ initialize: function(options) {
+ var self = this;
+ this.visible = this.$el.is(':visible');
+ this.mapReady = false;
+
+
+ ¶
+
+ this will be the Leaflet L.Map object (setup below) - this.menu = new my.MapMenu({ - model: this.model, - state: this.state.toJSON() - }); - this.listenTo(this.menu.state, 'change', function() { - self.state.set(self.menu.state.toJSON()); - self.redraw(); - }); - this.listenTo(this.state, 'change', function() { - self.redraw(); - }); - this.elSidebar = this.menu.$el; - }, |
Customization Functions+ + + this.map = null;
+ var stateData = _.extend({
+ geomField: null,
+ lonField: null,
+ latField: null,
+ autoZoom: true,
+ cluster: false
+ },
+ options.state
+ );
+ this.state = new recline.Model.ObjectState(stateData);
+
+ this._clusterOptions = {
+ zoomToBoundsOnClick: true,
+
+
+
+
+ ¶
+
+ disableClusteringAtZoom: 10, + + maxClusterRadius: 80,
+ singleMarkerMode: false,
+ skipDuplicateAddTesting: true,
+ animateAddingMarkers: false
+ };
+
+
+
+
+ ¶
+
+ Listen to changes in the fields + + this.listenTo(this.model.fields, 'change', function() {
+ self._setupGeometryField();
+ self.render();
+ });
+
+
+
+
+ ¶
+
+ Listen to changes in the records + + this.listenTo(this.model.records, 'add', function(doc){self.redraw('add',doc);});
+ this.listenTo(this.model.records, 'change', function(doc){
+ self.redraw('remove',doc);
+ self.redraw('add',doc);
+ });
+ this.listenTo(this.model.records, 'remove', function(doc){self.redraw('remove',doc);});
+ this.listenTo(this.model.records, 'reset', function(){self.redraw('reset');});
+
+ this.menu = new my.MapMenu({
+ model: this.model,
+ state: this.state.toJSON()
+ });
+ this.listenTo(this.menu.state, 'change', function() {
+ self.state.set(self.menu.state.toJSON());
+ self.redraw();
+ });
+ this.listenTo(this.state, 'change', function() {
+ self.redraw();
+ });
+ this.elSidebar = this.menu.$el;
+ },
+
+
+ ¶
+
+ Customization FunctionsThe following methods are designed for overriding in order to customize -behaviour | |
infobox+behaviour + + + + + +
+
+
+ ¶
+
+ infoboxFunction to create infoboxes used in popups. The default behaviour is very simple and just lists all attributes. -Users should override this function to customize behaviour i.e. - - | infobox: function(record) {
- var html = '';
- for (var key in record.attributes){
- if (!(this.state.get('geomField') && key == this.state.get('geomField'))){
- html += '<div><strong>' + key + '</strong>: '+ record.attributes[key] + '</div>';
- }
- }
- return html;
- }, |
| Options to use for the Leaflet GeoJSON layer -See also http://leaflet.cloudmade.com/examples/geojson.html - -e.g. - -
+
+
+ infobox: function(record) {
+ var html = '';
+ for (var key in record.attributes){
+ if (!(this.state.get('geomField') && key == this.state.get('geomField'))){
+ html += '<div><strong>' + key + '</strong>: '+ record.attributes[key] + '</div>';
+ }
+ }
+ return html;
+ },
+
+
+ ¶
+
+ Options to use for the Leaflet GeoJSON layer +See also http://leaflet.cloudmade.com/examples/geojson.html +e.g. +See defaults for examples -See defaults for examples | geoJsonLayerOptions: { |
| pointToLayer function to use when creating points - + + + geoJsonLayerOptions: {
+
+
+ ¶
+
+ pointToLayer function to use when creating points Default behaviour shown here is to create a marker using the popupContent set on the feature properties (created via infobox function during feature generation) -NB: inside pointToLayer | pointToLayer: function (feature, latlng) {
- var marker = new L.Marker(latlng);
- marker.bindPopup(feature.properties.popupContent); |
| this is for cluster case | this.markers.addLayer(marker);
- return marker;
- }, |
| onEachFeature default which adds popup in | onEachFeature: function(feature, layer) {
- if (feature.properties && feature.properties.popupContent) {
- layer.bindPopup(feature.properties.popupContent);
- }
- }
- }, |
END: Customization section | |
Public: Adds the necessary elements to the page.+instance (which allows e.g. this.markers to work in this default case) -Also sets up the editor fields and the map if necessary. | render: function() {
- var self = this;
- var htmls = Mustache.render(this.template, this.model.toTemplateJSON());
- this.$el.html(htmls);
- this.$map = this.$el.find('.panel.map');
- this.redraw();
- return this;
- }, |
Public: Redraws the features on the map according to the action provided+ + + pointToLayer: function (feature, latlng) {
+ var marker = new L.Marker(latlng);
+ marker.bindPopup(feature.properties.popupContent);
+
+
+
+
+ ¶
+
+ this is for cluster case + this.markers.addLayer(marker);
+ return marker;
+ },
+
+
+
+
+ ¶
+
+ onEachFeature default which adds popup in + + onEachFeature: function(feature, layer) {
+ if (feature.properties && feature.properties.popupContent) {
+ layer.bindPopup(feature.properties.popupContent);
+ }
+ }
+ },
+
+
+
+
+ ¶
+
+ END: Customization section+ +
+
+
+
+
+ ¶
+
+
+
+
+
+
+
+ ¶
+
+ Public: Adds the necessary elements to the page.+Also sets up the editor fields and the map if necessary. + + render: function() {
+ var self = this;
+ var htmls = Mustache.render(this.template, this.model.toTemplateJSON());
+ this.$el.html(htmls);
+ this.$map = this.$el.find('.panel.map');
+ this.redraw();
+ return this;
+ },
+
+
+ ¶
+
+ Public: Redraws the features on the map according to the action providedActions can be: -
| redraw: function(action, doc){
- var self = this;
- action = action || 'refresh'; |
| try to set things up if not already | if (!self._geomReady()){
- self._setupGeometryField();
- }
- if (!self.mapReady){
- self._setupMap();
- }
+
- if (this._geomReady() && this.mapReady){ |
| removing ad re-adding the layer enables faster bulk loading | this.map.removeLayer(this.features);
- this.map.removeLayer(this.markers);
+ redraw: function(action, doc){
+ var self = this;
+ action = action || 'refresh';
+
+
+
+
+ ¶
+
+ try to set things up if not already - var countBefore = 0; - this.features.eachLayer(function(){countBefore++;}); + if (!self._geomReady()){
+ self._setupGeometryField();
+ }
+ if (!self.mapReady){
+ self._setupMap();
+ }
- if (action == 'refresh' || action == 'reset') {
- this.features.clearLayers(); |
| recreate cluster group because of issues with clearLayer | this.map.removeLayer(this.markers);
- this.markers = new L.MarkerClusterGroup(this._clusterOptions);
- this._add(this.model.records.models);
- } else if (action == 'add' && doc){
- this._add(doc);
- } else if (action == 'remove' && doc){
- this._remove(doc);
- } |
| this must come before zooming! + if (this._geomReady() && this.mapReady){ + + + + +
+
+
+
+
+ ¶
+
+ removing ad re-adding the layer enables faster bulk loading + + this.map.removeLayer(this.features);
+ this.map.removeLayer(this.markers);
+
+ var countBefore = 0;
+ this.features.eachLayer(function(){countBefore++;});
+
+ if (action == 'refresh' || action == 'reset') {
+ this.features.clearLayers();
+
+
+
+
+ ¶
+
+ recreate cluster group because of issues with clearLayer + + this.map.removeLayer(this.markers);
+ this.markers = new L.MarkerClusterGroup(this._clusterOptions);
+ this._add(this.model.records.models);
+ } else if (action == 'add' && doc){
+ this._add(doc);
+ } else if (action == 'remove' && doc){
+ this._remove(doc);
+ }
+
+
+ ¶
+
+ this must come before zooming! if not: errors when using e.g. circle markers like -"Cannot call method 'project' of undefined" | if (this.state.get('cluster')) {
- this.map.addLayer(this.markers);
- } else {
- this.map.addLayer(this.features);
- }
+“Cannot call method ‘project’ of undefined”
- if (this.state.get('autoZoom')){
- if (this.visible){
- this._zoomToFeatures();
- } else {
- this._zoomPending = true;
- }
- }
- }
- },
+ if (this.state.get('cluster')) {
+ this.map.addLayer(this.markers);
+ } else {
+ this.map.addLayer(this.features);
+ }
- show: function() { |
| If the div was hidden, Leaflet needs to recalculate some sizes -to display properly | if (this.map){
- this.map.invalidateSize();
- if (this._zoomPending && this.state.get('autoZoom')) {
- this._zoomToFeatures();
- this._zoomPending = false;
- }
- }
- this.visible = true;
- },
+ if (this.state.get('autoZoom')){
+ if (this.visible){
+ this._zoomToFeatures();
+ } else {
+ this._zoomPending = true;
+ }
+ }
+ }
+ },
- hide: function() {
- this.visible = false;
- },
+ show: function() {
+
+
+ ¶
+
+ If the div was hidden, Leaflet needs to recalculate some sizes +to display properly - _geomReady: function() { - return Boolean(this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField'))); - }, |
| Private: Add one or n features to the map + + + if (this.map){
+ this.map.invalidateSize();
+ if (this._zoomPending && this.state.get('autoZoom')) {
+ this._zoomToFeatures();
+ this._zoomPending = false;
+ }
+ }
+ this.visible = true;
+ },
+ hide: function() {
+ this.visible = false;
+ },
+
+ _geomReady: function() {
+ return Boolean(this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField')));
+ },
+
+
+ ¶
+
+ Private: Add one or n features to the map For each record passed, a GeoJSON geometry will be extracted and added to the features layer. If an exception is thrown, the process will be stopped and an error notification shown. +Each feature will have a popup associated with all the record fields. -Each feature will have a popup associated with all the record fields. | _add: function(docs){
- var self = this;
+ _add: function(docs){
+ var self = this;
- if (!(docs instanceof Array)) docs = [docs];
+ if (!(docs instanceof Array)) docs = [docs];
- var count = 0;
- var wrongSoFar = 0;
- _.every(docs, function(doc){
- count += 1;
- var feature = self._getGeometryFromRecord(doc);
- if (typeof feature === 'undefined' || feature === null){ |
| Empty field | return true;
- } else if (feature instanceof Object){
- feature.properties = {
- popupContent: self.infobox(doc), |
| Add a reference to the model id, which will allow us to -link this Leaflet layer to a Recline doc | cid: doc.cid
- };
+ var count = 0;
+ var wrongSoFar = 0;
+ _.every(docs, function(doc){
+ count += 1;
+ var feature = self._getGeometryFromRecord(doc);
+ if (typeof feature === 'undefined' || feature === null){
+
+
+ ¶
+
+ Empty field - try { - self.features.addData(feature); - } catch (except) { - wrongSoFar += 1; - var msg = 'Wrong geometry value'; - if (except.message) msg += ' (' + except.message + ')'; - if (wrongSoFar <= 10) { - self.trigger('recline:flash', {message: msg, category:'error'}); - } - } - } else { - wrongSoFar += 1; - if (wrongSoFar <= 10) { - self.trigger('recline:flash', {message: 'Wrong geometry value', category:'error'}); - } - } - return true; - }); - }, |
| Private: Remove one or n features from the map | _remove: function(docs){
+ return true;
+ } else if (feature instanceof Object){
+ feature.properties = {
+ popupContent: self.infobox(doc),
+
+
+
+
+ ¶
+
+ Add a reference to the model id, which will allow us to +link this Leaflet layer to a Recline doc - var self = this; + cid: doc.cid
+ };
- if (!(docs instanceof Array)) docs = [docs];
+ try {
+ self.features.addData(feature);
+ } catch (except) {
+ wrongSoFar += 1;
+ var msg = 'Wrong geometry value';
+ if (except.message) msg += ' (' + except.message + ')';
+ if (wrongSoFar <= 10) {
+ self.trigger('recline:flash', {message: msg, category:'error'});
+ }
+ }
+ } else {
+ wrongSoFar += 1;
+ if (wrongSoFar <= 10) {
+ self.trigger('recline:flash', {message: 'Wrong geometry value', category:'error'});
+ }
+ }
+ return true;
+ });
+ },
+
+
+
+
+ ¶
+
+ Private: Remove one or n features from the map - _.each(docs,function(doc){ - for (var key in self.features._layers){ - if (self.features._layers[key].feature.properties.cid == doc.cid){ - self.features.removeLayer(self.features._layers[key]); - } - } - }); + _remove: function(docs){
- }, |
| Private: convert DMS coordinates to decimal + var self = this; -north and east are positive, south and west are negative | _parseCoordinateString: function(coord){
- if (typeof(coord) != 'string') {
- return(parseFloat(coord));
- }
- var dms = coord.split(/[^\.\d\w]+/);
- var deg = 0; var m = 0;
- var toDeg = [1, 60, 3600]; // conversion factors for Deg, min, sec
- var i;
- for (i = 0; i < dms.length; ++i) {
- if (isNaN(parseFloat(dms[i]))) {
- continue;
- }
- deg += parseFloat(dms[i]) / toDeg[m];
- m += 1;
- }
- if (coord.match(/[SW]/)) {
- deg = -1*deg;
- }
- return(deg);
- }, |
| Private: Return a GeoJSON geomtry extracted from the record fields | _getGeometryFromRecord: function(doc){
- if (this.state.get('geomField')){
- var value = doc.get(this.state.get('geomField'));
- if (typeof(value) === 'string'){ |
| We may have a GeoJSON string representation | try {
- value = $.parseJSON(value);
- } catch(e) {}
- }
- if (typeof(value) === 'string') {
- value = value.replace('(', '').replace(')', '');
- var parts = value.split(',');
- var lat = this._parseCoordinateString(parts[0]);
- var lon = this._parseCoordinateString(parts[1]);
+ if (!(docs instanceof Array)) docs = [docs];
- if (!isNaN(lon) && !isNaN(parseFloat(lat))) {
- return {
- "type": "Point",
- "coordinates": [lon, lat]
- };
- } else {
- return null;
- }
- } else if (value && _.isArray(value)) { |
| [ lon, lat ] | return {
- "type": "Point",
- "coordinates": [value[0], value[1]]
- };
- } else if (value && value.lat) { |
| of form { lat: ..., lon: ...} | return {
- "type": "Point",
- "coordinates": [value.lon || value.lng, value.lat]
- };
- } |
| We o/w assume that contents of the field are a valid GeoJSON object | return value;
- } else if (this.state.get('lonField') && this.state.get('latField')){ |
| We'll create a GeoJSON like point object from the two lat/lon fields | var lon = doc.get(this.state.get('lonField'));
- var lat = doc.get(this.state.get('latField'));
- lon = this._parseCoordinateString(lon);
- lat = this._parseCoordinateString(lat);
+ _.each(docs,function(doc){
+ for (var key in self.features._layers){
+ if (self.features._layers[key].feature.geometry.properties.cid == doc.cid){
+ self.features.removeLayer(self.features._layers[key]);
+ }
+ }
+ });
- if (!isNaN(parseFloat(lon)) && !isNaN(parseFloat(lat))) {
- return {
- type: 'Point',
- coordinates: [lon,lat]
- };
- }
- }
- return null;
- }, |
| Private: Check if there is a field with GeoJSON geometries or alternatively, + }, + + + + +
+
+
+
+
+ ¶
+
+ Private: convert DMS coordinates to decimal +north and east are positive, south and west are negative + + _parseCoordinateString: function(coord){
+ if (typeof(coord) != 'string') {
+ return(parseFloat(coord));
+ }
+ var dms = coord.split(/[^-?\.\d\w]+/);
+ var deg = 0; var m = 0;
+ var toDeg = [1, 60, 3600]; // conversion factors for Deg, min, sec
+ var i;
+ for (i = 0; i < dms.length; ++i) {
+ if (isNaN(parseFloat(dms[i]))) {
+ continue;
+ }
+ deg += parseFloat(dms[i]) / toDeg[m];
+ m += 1;
+ }
+ if (coord.match(/[SW]/)) {
+ deg = -1*deg;
+ }
+ return(deg);
+ },
+
+
+
+
+ ¶
+
+ Private: Return a GeoJSON geomtry extracted from the record fields + + _getGeometryFromRecord: function(doc){
+ if (this.state.get('geomField')){
+ var value = doc.get(this.state.get('geomField'));
+ if (typeof(value) === 'string'){
+
+
+
+
+ ¶
+
+ We may have a GeoJSON string representation + + try {
+ value = $.parseJSON(value);
+ } catch(e) {}
+ }
+ if (typeof(value) === 'string') {
+ value = value.replace('(', '').replace(')', '');
+ var parts = value.split(',');
+ var lat = this._parseCoordinateString(parts[0]);
+ var lon = this._parseCoordinateString(parts[1]);
+
+ if (!isNaN(lon) && !isNaN(parseFloat(lat))) {
+ return {
+ "type": "Point",
+ "coordinates": [lon, lat]
+ };
+ } else {
+ return null;
+ }
+ } else if (value && _.isArray(value)) {
+
+
+
+
+ ¶
+
+ [ lon, lat ] + + return {
+ "type": "Point",
+ "coordinates": [value[0], value[1]]
+ };
+ } else if (value && value.lat) {
+
+
+
+
+ ¶
+
+ of form { lat: …, lon: …} + + return {
+ "type": "Point",
+ "coordinates": [value.lon || value.lng, value.lat]
+ };
+ }
+
+
+
+
+ ¶
+
+ We o/w assume that contents of the field are a valid GeoJSON object + + return value;
+ } else if (this.state.get('lonField') && this.state.get('latField')){
+
+
+
+
+ ¶
+
+ We’ll create a GeoJSON like point object from the two lat/lon fields + + var lon = doc.get(this.state.get('lonField'));
+ var lat = doc.get(this.state.get('latField'));
+ lon = this._parseCoordinateString(lon);
+ lat = this._parseCoordinateString(lat);
+
+ if (!isNaN(parseFloat(lon)) && !isNaN(parseFloat(lat))) {
+ return {
+ type: 'Point',
+ coordinates: [lon,lat]
+ };
+ }
+ }
+ return null;
+ },
+
+
+ ¶
+
+ Private: Check if there is a field with GeoJSON geometries or alternatively, two fields with lat/lon values. +If not found, the user can define them via the UI form. -If not found, the user can define them via the UI form. | _setupGeometryField: function(){ |
| should not overwrite if we have already set this (e.g. explicitly via state) | if (!this._geomReady()) {
- this.state.set({
- geomField: this._checkField(this.geometryFieldNames),
- latField: this._checkField(this.latitudeFieldNames),
- lonField: this._checkField(this.longitudeFieldNames)
- });
- this.menu.state.set(this.state.toJSON());
- }
- }, |
| Private: Check if a field in the current model exists in the provided -list of names. | _checkField: function(fieldNames){
- var field;
- var modelFieldNames = this.model.fields.pluck('id');
- for (var i = 0; i < fieldNames.length; i++){
- for (var j = 0; j < modelFieldNames.length; j++){
- if (modelFieldNames[j].toLowerCase() == fieldNames[i].toLowerCase())
- return modelFieldNames[j];
- }
- }
- return null;
- }, |
| Private: Zoom to map to current features extent if any, or to the full -extent if none. | _zoomToFeatures: function(){
- var bounds = this.features.getBounds();
- if (bounds && bounds.getNorthEast() && bounds.getSouthWest()){
- this.map.fitBounds(bounds);
- } else {
- this.map.setView([0, 0], 2);
- }
- }, |
| Private: Sets up the Leaflet map control and the features layer. + + + _setupGeometryField: function(){
+
+
+
+
+ ¶
+
+ should not overwrite if we have already set this (e.g. explicitly via state) + if (!this._geomReady()) {
+ this.state.set({
+ geomField: this._checkField(this.geometryFieldNames),
+ latField: this._checkField(this.latitudeFieldNames),
+ lonField: this._checkField(this.longitudeFieldNames)
+ });
+ this.menu.state.set(this.state.toJSON());
+ }
+ },
+
+
+
+
+ ¶
+
+ Private: Check if a field in the current model exists in the provided +list of names. + + _checkField: function(fieldNames){
+ var field;
+ var modelFieldNames = this.model.fields.pluck('id');
+ for (var i = 0; i < fieldNames.length; i++){
+ for (var j = 0; j < modelFieldNames.length; j++){
+ if (modelFieldNames[j].toLowerCase() == fieldNames[i].toLowerCase())
+ return modelFieldNames[j];
+ }
+ }
+ return null;
+ },
+
+
+
+
+ ¶
+
+ Private: Zoom to map to current features extent if any, or to the full +extent if none. + + _zoomToFeatures: function(){
+ var bounds = this.features.getBounds();
+ if (bounds && bounds.getNorthEast() && bounds.getSouthWest()){
+ this.map.fitBounds(bounds);
+ } else {
+ this.map.setView([0, 0], 2);
+ }
+ },
+
+
+ ¶
+
+ Private: Sets up the Leaflet map control and the features layer. The map uses a base layer from MapQuest based -on OpenStreetMap. | _setupMap: function(){
- var self = this;
- this.map = new L.Map(this.$map.get(0));
+on OpenStreetMap.
- var mapUrl = "//otile{s}-s.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png";
- var osmAttribution = 'Map data © 2011 OpenStreetMap contributors, Tiles Courtesy of <a href="http://www.mapquest.com/" target="_blank">MapQuest</a> <img src="//developer.mapquest.com/content/osm/mq_logo.png">';
- var bg = new L.TileLayer(mapUrl, {maxZoom: 18, attribution: osmAttribution ,subdomains: '1234'});
- this.map.addLayer(bg);
+ _setupMap: function(){
+ var self = this;
+ this.map = new L.Map(this.$map.get(0));
- this.markers = new L.MarkerClusterGroup(this._clusterOptions); |
| rebind this (as needed in e.g. default case above) | this.geoJsonLayerOptions.pointToLayer = _.bind(
- this.geoJsonLayerOptions.pointToLayer,
- this);
- this.features = new L.GeoJSON(null, this.geoJsonLayerOptions);
+ var mapUrl = "http://otile{s}-s.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png";
+ var osmAttribution = 'Map data © 2011 OpenStreetMap contributors, Tiles Courtesy of <a href="http://www.mapquest.com/" target="_blank">MapQuest</a> <img src="http://developer.mapquest.com/content/osm/mq_logo.png">';
+ var bg = new L.TileLayer(mapUrl, {maxZoom: 18, attribution: osmAttribution ,subdomains: '1234'});
+ this.map.addLayer(bg);
- this.map.setView([0, 0], 2);
+ this.markers = new L.MarkerClusterGroup(this._clusterOptions); |
| Private: Helper function to select an option from a select list | _selectOption: function(id,value){
- var options = $('.' + id + ' > select > option');
- if (options){
- options.each(function(opt){
- if (this.value == value) {
- $(this).attr('selected','selected');
- return false;
- }
- });
- }
- }
-});
+ this.geoJsonLayerOptions.pointToLayer = _.bind(
+ this.geoJsonLayerOptions.pointToLayer,
+ this);
+ this.features = new L.GeoJSON(null, this.geoJsonLayerOptions);
-my.MapMenu = Backbone.View.extend({
- className: 'editor',
+ this.map.setView([0, 0], 2);
- template: ' \
- <form class="form-stacked"> \
- <div class="clearfix"> \
- <div class="editor-field-type"> \
- <label class="radio"> \
- <input type="radio" id="editor-field-type-latlon" name="editor-field-type" value="latlon" checked="checked"/> \
- Latitude / Longitude fields</label> \
- <label class="radio"> \
- <input type="radio" id="editor-field-type-geom" name="editor-field-type" value="geom" /> \
- GeoJSON field</label> \
- </div> \
- <div class="editor-field-type-latlon"> \
- <label>Latitude field</label> \
- <div class="input editor-lat-field"> \
- <select> \
- <option value=""></option> \
- {{#fields}} \
- <option value="{{id}}">{{label}}</option> \
- {{/fields}} \
- </select> \
- </div> \
- <label>Longitude field</label> \
- <div class="input editor-lon-field"> \
- <select> \
- <option value=""></option> \
- {{#fields}} \
- <option value="{{id}}">{{label}}</option> \
- {{/fields}} \
- </select> \
- </div> \
- </div> \
- <div class="editor-field-type-geom" style="display:none"> \
- <label>Geometry field (GeoJSON)</label> \
- <div class="input editor-geom-field"> \
- <select> \
- <option value=""></option> \
- {{#fields}} \
- <option value="{{id}}">{{label}}</option> \
- {{/fields}} \
- </select> \
- </div> \
- </div> \
- </div> \
- <div class="editor-buttons"> \
- <button class="btn editor-update-map">Update</button> \
- </div> \
- <div class="editor-options" > \
- <label class="checkbox"> \
- <input type="checkbox" id="editor-auto-zoom" value="autozoom" checked="checked" /> \
- Auto zoom to features</label> \
- <label class="checkbox"> \
- <input type="checkbox" id="editor-cluster" value="cluster"/> \
- Cluster markers</label> \
- </div> \
- <input type="hidden" class="editor-id" value="map-1" /> \
- </form> \
- ', |
| Define here events for UI elements | events: {
- 'click .editor-update-map': 'onEditorSubmit',
- 'change .editor-field-type': 'onFieldTypeChange',
- 'click #editor-auto-zoom': 'onAutoZoomChange',
- 'click #editor-cluster': 'onClusteringChange'
- },
+ this.mapReady = true;
+ },
+
+
+ ¶
+
+ Private: Helper function to select an option from a select list - initialize: function(options) { - var self = this; - _.bindAll(this, 'render'); - this.listenTo(this.model.fields, 'change', this.render); - this.state = new recline.Model.ObjectState(options.state); - this.listenTo(this.state, 'change', this.render); - this.render(); - }, |
Public: Adds the necessary elements to the page.+ + + _selectOption: function(id,value){
+ var options = $('.' + id + ' > select > option');
+ if (options){
+ options.each(function(opt){
+ if (this.value == value) {
+ $(this).attr('selected','selected');
+ return false;
+ }
+ });
+ }
+ }
+});
- | render: function() {
- var self = this;
- var htmls = Mustache.render(this.template, this.model.toTemplateJSON());
- this.$el.html(htmls);
+my.MapMenu = Backbone.View.extend({
+ className: 'editor',
- if (this._geomReady() && this.model.fields.length){
- if (this.state.get('geomField')){
- this._selectOption('editor-geom-field',this.state.get('geomField'));
- this.$el.find('#editor-field-type-geom').attr('checked','checked').change();
- } else{
- this._selectOption('editor-lon-field',this.state.get('lonField'));
- this._selectOption('editor-lat-field',this.state.get('latField'));
- this.$el.find('#editor-field-type-latlon').attr('checked','checked').change();
- }
- }
- if (this.state.get('autoZoom')) {
- this.$el.find('#editor-auto-zoom').attr('checked', 'checked');
- } else {
- this.$el.find('#editor-auto-zoom').removeAttr('checked');
- }
- if (this.state.get('cluster')) {
- this.$el.find('#editor-cluster').attr('checked', 'checked');
- } else {
- this.$el.find('#editor-cluster').removeAttr('checked');
- }
- return this;
- },
+ template: ' \
+ <form class="form-stacked"> \
+ <div class="clearfix"> \
+ <div class="editor-field-type"> \
+ <label class="radio"> \
+ <input type="radio" id="editor-field-type-latlon" name="editor-field-type" value="latlon" checked="checked"/> \
+ Latitude / Longitude fields</label> \
+ <label class="radio"> \
+ <input type="radio" id="editor-field-type-geom" name="editor-field-type" value="geom" /> \
+ GeoJSON field</label> \
+ </div> \
+ <div class="editor-field-type-latlon"> \
+ <label>Latitude field</label> \
+ <div class="input editor-lat-field"> \
+ <select class="form-control"> \
+ <option value=""></option> \
+ {{#fields}} \
+ <option value="{{id}}">{{label}}</option> \
+ {{/fields}} \
+ </select> \
+ </div> \
+ <label>Longitude field</label> \
+ <div class="input editor-lon-field"> \
+ <select class="form-control"> \
+ <option value=""></option> \
+ {{#fields}} \
+ <option value="{{id}}">{{label}}</option> \
+ {{/fields}} \
+ </select> \
+ </div> \
+ </div> \
+ <div class="editor-field-type-geom" style="display:none"> \
+ <label>Geometry field (GeoJSON)</label> \
+ <div class="input editor-geom-field"> \
+ <select class="form-control"> \
+ <option value=""></option> \
+ {{#fields}} \
+ <option value="{{id}}">{{label}}</option> \
+ {{/fields}} \
+ </select> \
+ </div> \
+ </div> \
+ </div> \
+ <div class="editor-buttons"> \
+ <button class="btn btn-default editor-update-map">Update</button> \
+ </div> \
+ <div class="editor-options" > \
+ <label class="checkbox"> \
+ <input type="checkbox" id="editor-auto-zoom" value="autozoom" checked="checked" /> \
+ Auto zoom to features</label> \
+ <label class="checkbox"> \
+ <input type="checkbox" id="editor-cluster" value="cluster"/> \
+ Cluster markers</label> \
+ </div> \
+ <input type="hidden" class="editor-id" value="map-1" /> \
+ </form> \
+ ',
+
+
+ ¶
+
+ Define here events for UI elements - _geomReady: function() { - return Boolean(this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField'))); - }, |
UI Event handlers | |
| Public: Update map with user options + + + events: {
+ 'click .editor-update-map': 'onEditorSubmit',
+ 'change .editor-field-type': 'onFieldTypeChange',
+ 'click #editor-auto-zoom': 'onAutoZoomChange',
+ 'click #editor-cluster': 'onClusteringChange'
+ },
+ initialize: function(options) {
+ var self = this;
+ _.bindAll(this, 'render');
+ this.listenTo(this.model.fields, 'change', this.render);
+ this.state = new recline.Model.ObjectState(options.state);
+ this.listenTo(this.state, 'change', this.render);
+ this.render();
+ },
+
+
+
+
+ ¶
+
+ Public: Adds the necessary elements to the page.+Also sets up the editor fields and the map if necessary. + + render: function() {
+ var self = this;
+ var htmls = Mustache.render(this.template, this.model.toTemplateJSON());
+ this.$el.html(htmls);
+
+ if (this._geomReady() && this.model.fields.length){
+ if (this.state.get('geomField')){
+ this._selectOption('editor-geom-field',this.state.get('geomField'));
+ this.$el.find('#editor-field-type-geom').attr('checked','checked').change();
+ } else{
+ this._selectOption('editor-lon-field',this.state.get('lonField'));
+ this._selectOption('editor-lat-field',this.state.get('latField'));
+ this.$el.find('#editor-field-type-latlon').attr('checked','checked').change();
+ }
+ }
+ if (this.state.get('autoZoom')) {
+ this.$el.find('#editor-auto-zoom').attr('checked', 'checked');
+ } else {
+ this.$el.find('#editor-auto-zoom').removeAttr('checked');
+ }
+ if (this.state.get('cluster')) {
+ this.$el.find('#editor-cluster').attr('checked', 'checked');
+ } else {
+ this.$el.find('#editor-cluster').removeAttr('checked');
+ }
+ return this;
+ },
+
+ _geomReady: function() {
+ return Boolean(this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField')));
+ },
+
+
+
+
+ ¶
+
+ UI Event handlers+ +
+
+
+ ¶
+
+ Public: Update map with user options Right now the only configurable option is what field(s) contains the -location information. | onEditorSubmit: function(e){
- e.preventDefault();
- if (this.$el.find('#editor-field-type-geom').attr('checked')){
- this.state.set({
- geomField: this.$el.find('.editor-geom-field > select > option:selected').val(),
- lonField: null,
- latField: null
- });
- } else {
- this.state.set({
- geomField: null,
- lonField: this.$el.find('.editor-lon-field > select > option:selected').val(),
- latField: this.$el.find('.editor-lat-field > select > option:selected').val()
- });
- }
- return false;
- }, |
| Public: Shows the relevant select lists depending on the location field -type selected. | onFieldTypeChange: function(e){
- if (e.target.value == 'geom'){
- this.$el.find('.editor-field-type-geom').show();
- this.$el.find('.editor-field-type-latlon').hide();
- } else {
- this.$el.find('.editor-field-type-geom').hide();
- this.$el.find('.editor-field-type-latlon').show();
- }
- },
+location information.
- onAutoZoomChange: function(e){
- this.state.set({autoZoom: !this.state.get('autoZoom')});
- },
+ onEditorSubmit: function(e){
+ e.preventDefault();
+ if (this.$el.find('#editor-field-type-geom').attr('checked')){
+ this.state.set({
+ geomField: this.$el.find('.editor-geom-field > select > option:selected').val(),
+ lonField: null,
+ latField: null
+ });
+ } else {
+ this.state.set({
+ geomField: null,
+ lonField: this.$el.find('.editor-lon-field > select > option:selected').val(),
+ latField: this.$el.find('.editor-lat-field > select > option:selected').val()
+ });
+ }
+ return false;
+ },
+
+
+ ¶
+
+ Public: Shows the relevant select lists depending on the location field +type selected. - onClusteringChange: function(e){ - this.state.set({cluster: !this.state.get('cluster')}); - }, |
| Private: Helper function to select an option from a select list | _selectOption: function(id,value){
- var options = this.$el.find('.' + id + ' > select > option');
- if (options){
- options.each(function(opt){
- if (this.value == value) {
- $(this).attr('selected','selected');
- return false;
- }
- });
- }
- }
-});
+ onFieldTypeChange: function(e){
+ if (e.target.value == 'geom'){
+ this.$el.find('.editor-field-type-geom').show();
+ this.$el.find('.editor-field-type-latlon').hide();
+ } else {
+ this.$el.find('.editor-field-type-geom').hide();
+ this.$el.find('.editor-field-type-latlon').show();
+ }
+ },
-})(jQuery, recline.View);
+ onAutoZoomChange: function(e){
+ this.state.set({autoZoom: !this.state.get('autoZoom')});
+ },
- |
Private: Helper function to select an option from a select list
+ + _selectOption: function(id,value){
+ var options = this.$el.find('.' + id + ' > select > option');
+ if (options){
+ options.each(function(opt){
+ if (this.value == value) {
+ $(this).attr('selected','selected');
+ return false;
+ }
+ });
+ }
+ }
+});
+
+})(jQuery, recline.View); view.multiview.js | |
|---|---|
/*jshint multistr:true */ | |
| Standard JS module setup | this.recline = this.recline || {};
-this.recline.View = this.recline.View || {};
+
-(function($, my) {
- "use strict"; |
MultiView+ + +
+
+
+
| my.MultiView = Backbone.View.extend({
- template: ' \
- <div class="recline-data-explorer"> \
- <div class="alert-messages"></div> \
- \
- <div class="header clearfix"> \
- <div class="navigation"> \
- <div class="btn-group" data-toggle="buttons-radio"> \
- {{#views}} \
- <a href="#{{id}}" data-view="{{id}}" class="btn">{{label}}</a> \
- {{/views}} \
- </div> \
- </div> \
- <div class="recline-results-info"> \
- <span class="doc-count">{{recordCount}}</span> records\
- </div> \
- <div class="menu-right"> \
- <div class="btn-group" data-toggle="buttons-checkbox"> \
- {{#sidebarViews}} \
- <a href="#" data-action="{{id}}" class="btn">{{label}}</a> \
- {{/sidebarViews}} \
- </div> \
- </div> \
- <div class="query-editor-here" style="display:inline;"></div> \
- </div> \
- <div class="data-view-sidebar"></div> \
- <div class="data-view-container"></div> \
- </div> \
- ',
- events: {
- 'click .menu-right a': '_onMenuClick',
- 'click .navigation a': '_onSwitchView'
- },
+initialized the MultiView with the relevant views themselves.
- initialize: function(options) {
- var self = this;
- this._setupState(options.state); |
| Hash of 'page' views (i.e. those for whole page) keyed by page name | if (options.views) {
- this.pageViews = options.views;
- } else {
- this.pageViews = [{
- id: 'grid',
- label: 'Grid',
- view: new my.SlickGrid({
- model: this.model,
- state: this.state.get('view-grid')
- })
- }, {
- id: 'graph',
- label: 'Graph',
- view: new my.Graph({
- model: this.model,
- state: this.state.get('view-graph')
- })
- }, {
- id: 'map',
- label: 'Map',
- view: new my.Map({
- model: this.model,
- state: this.state.get('view-map')
- })
- }, {
- id: 'timeline',
- label: 'Timeline',
- view: new my.Timeline({
- model: this.model,
- state: this.state.get('view-timeline')
- })
- }];
- } |
| Hashes of sidebar elements | if(options.sidebarViews) {
- this.sidebarViews = options.sidebarViews;
- } else {
- this.sidebarViews = [{
- id: 'filterEditor',
- label: 'Filters',
- view: new my.FilterEditor({
- model: this.model
- })
- }, {
- id: 'fieldsView',
- label: 'Fields',
- view: new my.Fields({
- model: this.model
- })
- }];
- } |
| these must be called after pageViews are created | this.render();
- this._bindStateChanges();
- this._bindFlashNotifications(); |
| now do updates based on state (need to come after render) | if (this.state.get('readOnly')) {
- this.setReadOnly();
- }
- if (this.state.get('currentView')) {
- this.updateNav(this.state.get('currentView'));
- } else {
- this.updateNav(this.pageViews[0].id);
- }
- this._showHideSidebar();
+ my.MultiView = Backbone.View.extend({
+ template: ' \
+ <div class="recline-data-explorer"> \
+ <div class="alert-messages"></div> \
+ \
+ <div class="header clearfix"> \
+ <div class="navigation"> \
+ <div class="btn-group" data-toggle="buttons-radio"> \
+ {{#views}} \
+ <button href="#{{id}}" data-view="{{id}}" class="btn btn-default">{{label}}</button> \
+ {{/views}} \
+ </div> \
+ </div> \
+ <div class="recline-results-info"> \
+ <span class="doc-count">{{recordCount}}</span> records\
+ </div> \
+ <div class="menu-right"> \
+ <div class="btn-group" data-toggle="buttons-checkbox"> \
+ {{#sidebarViews}} \
+ <button href="#" data-action="{{id}}" class="btn btn-default">{{label}}</button> \
+ {{/sidebarViews}} \
+ </div> \
+ </div> \
+ <div class="query-editor-here" style="display:inline;"></div> \
+ </div> \
+ <div class="data-view-sidebar"></div> \
+ <div class="data-view-container"></div> \
+ </div> \
+ ',
+ events: {
+ 'click .menu-right button': '_onMenuClick',
+ 'click .navigation button': '_onSwitchView'
+ },
- this.listenTo(this.model, 'query:start', function() {
- self.notify({loader: true, persist: true});
- });
- this.listenTo(this.model, 'query:done', function() {
- self.clearNotifications();
- self.$el.find('.doc-count').text(self.model.recordCount || 'Unknown');
- });
- this.listenTo(this.model, 'query:fail', function(error) {
- self.clearNotifications();
- var msg = '';
- if (typeof(error) == 'string') {
- msg = error;
- } else if (typeof(error) == 'object') {
- if (error.title) {
- msg = error.title + ': ';
- }
- if (error.message) {
- msg += error.message;
- }
- } else {
- msg = 'There was an error querying the backend';
- }
- self.notify({message: msg, category: 'error', persist: true});
- }); |
| retrieve basic data like fields etc + initialize: function(options) { + var self = this; + this._setupState(options.state); + + + + +
+
+
+
+
+ ¶
+
+ Hash of ‘page’ views (i.e. those for whole page) keyed by page name + + if (options.views) {
+ this.pageViews = options.views;
+ } else {
+ this.pageViews = [{
+ id: 'grid',
+ label: 'Grid',
+ view: new my.SlickGrid({
+ model: this.model,
+ state: this.state.get('view-grid')
+ })
+ }, {
+ id: 'graph',
+ label: 'Graph',
+ view: new my.Graph({
+ model: this.model,
+ state: this.state.get('view-graph')
+ })
+ }, {
+ id: 'map',
+ label: 'Map',
+ view: new my.Map({
+ model: this.model,
+ state: this.state.get('view-map')
+ })
+ }, {
+ id: 'timeline',
+ label: 'Timeline',
+ view: new my.Timeline({
+ model: this.model,
+ state: this.state.get('view-timeline')
+ })
+ }];
+ }
+
+
+
+
+ ¶
+
+ Hashes of sidebar elements + + if(options.sidebarViews) {
+ this.sidebarViews = options.sidebarViews;
+ } else {
+ this.sidebarViews = [{
+ id: 'filterEditor',
+ label: 'Filters',
+ view: new my.FilterEditor({
+ model: this.model
+ })
+ }, {
+ id: 'fieldsView',
+ label: 'Fields',
+ view: new my.Fields({
+ model: this.model
+ })
+ }];
+ }
+
+
+
+
+ ¶
+
+ these must be called after pageViews are created + + this.render();
+ this._bindStateChanges();
+ this._bindFlashNotifications();
+
+
+
+
+ ¶
+
+ now do updates based on state (need to come after render) + + if (this.state.get('readOnly')) {
+ this.setReadOnly();
+ }
+ if (this.state.get('currentView')) {
+ this.updateNav(this.state.get('currentView'));
+ } else {
+ this.updateNav(this.pageViews[0].id);
+ }
+ this._showHideSidebar();
+
+ this.listenTo(this.model, 'query:start', function() {
+ self.notify({loader: true, persist: true});
+ });
+ this.listenTo(this.model, 'query:done', function() {
+ self.clearNotifications();
+ self.$el.find('.doc-count').text(self.model.recordCount || 'Unknown');
+ });
+ this.listenTo(this.model, 'query:fail', function(error) {
+ self.clearNotifications();
+ var msg = '';
+ if (typeof(error) == 'string') {
+ msg = error;
+ } else if (typeof(error) == 'object') {
+ if (error.title) {
+ msg = error.title + ': ';
+ }
+ if (error.message) {
+ msg += error.message;
+ }
+ } else {
+ msg = 'There was an error querying the backend';
+ }
+ self.notify({message: msg, category: 'error', persist: true});
+ });
+
+
+ ¶
+
+ retrieve basic data like fields etc note this.model and dataset returned are the same -TODO: set query state ...? | this.model.queryState.set(self.state.get('query'), {silent: true});
- },
+TODO: set query state …?
- setReadOnly: function() {
- this.$el.addClass('recline-read-only');
- },
+ this.model.queryState.set(self.state.get('query'), {silent: true});
+ },
- render: function() {
- var tmplData = this.model.toTemplateJSON();
- tmplData.views = this.pageViews;
- tmplData.sidebarViews = this.sidebarViews;
- var template = Mustache.render(this.template, tmplData);
- this.$el.html(template); |
| now create and append other views | var $dataViewContainer = this.$el.find('.data-view-container');
- var $dataSidebar = this.$el.find('.data-view-sidebar'); |
| the main views | _.each(this.pageViews, function(view, pageName) {
- view.view.render();
- $dataViewContainer.append(view.view.el);
- if (view.view.elSidebar) {
- $dataSidebar.append(view.view.elSidebar);
- }
- });
+ setReadOnly: function() {
+ this.$el.addClass('recline-read-only');
+ },
- _.each(this.sidebarViews, function(view) {
- this['$'+view.id] = view.view.$el;
- $dataSidebar.append(view.view.el);
- }, this);
+ render: function() {
+ var tmplData = this.model.toTemplateJSON();
+ tmplData.views = this.pageViews;
+ tmplData.sidebarViews = this.sidebarViews;
+ var template = Mustache.render(this.template, tmplData);
+ this.$el.html(template);
+
+
+
+
+ ¶
+
+ now create and append other views - this.pager = new recline.View.Pager({ - model: this.model - }); - this.$el.find('.recline-results-info').after(this.pager.el); + var $dataViewContainer = this.$el.find('.data-view-container');
+ var $dataSidebar = this.$el.find('.data-view-sidebar');
+
+
+
+
+ ¶
+
+ the main views - this.queryEditor = new recline.View.QueryEditor({ - model: this.model.queryState - }); - this.$el.find('.query-editor-here').append(this.queryEditor.el); + _.each(this.pageViews, function(view, pageName) {
+ view.view.render();
+ if (view.view.redraw) {
+ view.view.redraw();
+ }
+ $dataViewContainer.append(view.view.el);
+ if (view.view.elSidebar) {
+ $dataSidebar.append(view.view.elSidebar);
+ }
+ });
- },
+ _.each(this.sidebarViews, function(view) {
+ this['$'+view.id] = view.view.$el;
+ $dataSidebar.append(view.view.el);
+ }, this);
- remove: function () {
- _.each(this.pageViews, function (view) {
- view.view.remove();
- });
- _.each(this.sidebarViews, function (view) {
- view.view.remove();
- });
- this.pager.remove();
- this.queryEditor.remove();
- Backbone.View.prototype.remove.apply(this, arguments);
- }, |
| hide the sidebar if empty | _showHideSidebar: function() {
- var $dataSidebar = this.$el.find('.data-view-sidebar');
- var visibleChildren = $dataSidebar.children().filter(function() {
- return $(this).css("display") != "none";
- }).length;
+ this.pager = new recline.View.Pager({
+ model: this.model
+ });
+ this.$el.find('.recline-results-info').after(this.pager.el);
- if (visibleChildren > 0) {
- $dataSidebar.show();
- } else {
- $dataSidebar.hide();
- }
- },
+ this.queryEditor = new recline.View.QueryEditor({
+ model: this.model.queryState
+ });
+ this.$el.find('.query-editor-here').append(this.queryEditor.el);
- updateNav: function(pageName) {
- this.$el.find('.navigation a').removeClass('active');
- var $el = this.$el.find('.navigation a[data-view="' + pageName + '"]');
- $el.addClass('active'); |
| add/remove sidebars and hide inactive views | _.each(this.pageViews, function(view, idx) {
- if (view.id === pageName) {
- view.view.$el.show();
- if (view.view.elSidebar) {
- view.view.elSidebar.show();
- }
- } else {
- view.view.$el.hide();
- if (view.view.elSidebar) {
- view.view.elSidebar.hide();
- }
- if (view.view.hide) {
- view.view.hide();
- }
- }
- });
+ },
- this._showHideSidebar(); |
| call view.view.show after sidebar visibility has been determined so -that views can correctly calculate their maximum width | _.each(this.pageViews, function(view, idx) {
- if (view.id === pageName) {
- if (view.view.show) {
- view.view.show();
- }
- }
- });
- },
+ remove: function () {
+ _.each(this.pageViews, function (view) {
+ view.view.remove();
+ });
+ _.each(this.sidebarViews, function (view) {
+ view.view.remove();
+ });
+ this.pager.remove();
+ this.queryEditor.remove();
+ Backbone.View.prototype.remove.apply(this, arguments);
+ },
+
+
+
+
+ ¶
+
+ hide the sidebar if empty - _onMenuClick: function(e) { - e.preventDefault(); - var action = $(e.target).attr('data-action'); - this['$'+action].toggle(); - this._showHideSidebar(); - }, + _showHideSidebar: function() {
+ var $dataSidebar = this.$el.find('.data-view-sidebar');
+ var visibleChildren = $dataSidebar.children().filter(function() {
+ return $(this).css("display") != "none";
+ }).length;
- _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 + if (visibleChildren > 0) { + $dataSidebar.show(); + } else { + $dataSidebar.hide(); + } + }, + updateNav: function(pageName) { + this.$el.find('.navigation button').removeClass('active'); + var $el = this.$el.find('.navigation button[data-view="' + pageName + '"]'); + $el.addClass('active'); + + + + +
+
+
+
+
+ ¶
+
+ add/remove sidebars and hide inactive views + + _.each(this.pageViews, function(view, idx) {
+ if (view.id === pageName) {
+ view.view.$el.show();
+ if (view.view.elSidebar) {
+ view.view.elSidebar.show();
+ }
+ } else {
+ view.view.$el.hide();
+ if (view.view.elSidebar) {
+ view.view.elSidebar.hide();
+ }
+ if (view.view.hide) {
+ view.view.hide();
+ }
+ }
+ });
+
+ this._showHideSidebar();
+
+
+
+
+ ¶
+
+ call view.view.show after sidebar visibility has been determined so +that views can correctly calculate their maximum width + + _.each(this.pageViews, function(view, idx) {
+ if (view.id === pageName) {
+ if (view.view.show) {
+ view.view.show();
+ }
+ }
+ });
+ },
+
+ _onMenuClick: function(e) {
+ e.preventDefault();
+ var action = $(e.target).attr('data-action');
+ this['$'+action].toggle();
+ this._showHideSidebar();
+ },
+
+ _onSwitchView: function(e) {
+ e.preventDefault();
+ var viewName = $(e.target).attr('data-view');
+ this.updateNav(viewName);
+ this.state.set({currentView: viewName});
+ },
+
+
+ ¶
+
+ create a state object for this view and do the job of a) initializing it from both data passed in and other sources (e.g. hash url) +b) ensure the state object is updated in responese to changes in subviews, query etc. -b) ensure the state object is updated in responese to changes in subviews, query etc. | _setupState: function(initialState) {
- var self = this; |
| get data from the query string / hash url plus some defaults | var qs = my.parseHashQueryString();
- var query = qs.reclineQuery;
- query = query ? JSON.parse(query) : self.model.queryState.toJSON(); |
| backwards compatability (now named view-graph but was named graph) | var graphState = qs['view-graph'] || qs.graph;
- graphState = graphState ? JSON.parse(graphState) : {}; |
| now get default data + hash url plus initial state and initial our state object with it | var stateData = _.extend({
- query: query,
- 'view-graph': graphState,
- backend: this.model.backend.__type__,
- url: this.model.get('url'),
- dataset: this.model.toJSON(),
- currentView: null,
- readOnly: false
- },
- initialState);
- this.state = new recline.Model.ObjectState(stateData);
- },
+ _setupState: function(initialState) {
+ var self = this;
+
+
+ ¶
+
+ get data from the query string / hash url plus some defaults - _bindStateChanges: function() { - var self = this; |
| finally ensure we update our state object when state of sub-object changes so that state is always up to date | this.listenTo(this.model.queryState, 'change', function() {
- self.state.set({query: self.model.queryState.toJSON()});
- });
- _.each(this.pageViews, function(pageView) {
- if (pageView.view.state && pageView.view.state.bind) {
- var update = {};
- update['view-' + pageView.id] = pageView.view.state.toJSON();
- self.state.set(update);
- self.listenTo(pageView.view.state, 'change', function() {
- var update = {};
- update['view-' + pageView.id] = pageView.view.state.toJSON(); |
| had problems where change not being triggered for e.g. grid view so let's do it explicitly | self.state.set(update, {silent: true});
- self.state.trigger('change');
- });
- }
- });
- },
+ 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) - _bindFlashNotifications: function() { - var self = this; - _.each(this.pageViews, function(pageView) { - self.listenTo(pageView.view, 'recline:flash', function(flash) { - self.notify(flash); - }); - }); - }, |
notify+ + + var graphState = qs['view-graph'] || qs.graph;
+ graphState = graphState ? JSON.parse(graphState) : {};
+
+
+
+
+ ¶
+
+ now get default data + hash url plus initial state and initial our state object with it + var stateData = _.extend({
+ query: query,
+ 'view-graph': graphState,
+ backend: this.model.backend.__type__,
+ url: this.model.get('url'),
+ dataset: this.model.toJSON(),
+ currentView: null,
+ readOnly: false
+ },
+ initialState);
+ this.state = new recline.Model.ObjectState(stateData);
+ },
+
+ _bindStateChanges: function() {
+ var self = this;
+
+
+
+
+ ¶
+
+ finally ensure we update our state object when state of sub-object changes so that state is always up to date + + this.listenTo(this.model.queryState, 'change', function() {
+ self.state.set({query: self.model.queryState.toJSON()});
+ });
+ _.each(this.pageViews, function(pageView) {
+ if (pageView.view.state && pageView.view.state.bind) {
+ var update = {};
+ update['view-' + pageView.id] = pageView.view.state.toJSON();
+ self.state.set(update);
+ self.listenTo(pageView.view.state, 'change', function() {
+ var update = {};
+ update['view-' + pageView.id] = pageView.view.state.toJSON();
+
+
+
+
+ ¶
+
+ had problems where change not being triggered for e.g. grid view so let’s do it explicitly + + self.state.set(update, {silent: true});
+ self.state.trigger('change');
+ });
+ }
+ });
+ },
+
+ _bindFlashNotifications: function() {
+ var self = this;
+ _.each(this.pageViews, function(pageView) {
+ self.listenTo(pageView.view, 'recline:flash', function(flash) {
+ self.notify(flash);
+ });
+ });
+ },
+
+
+ ¶
+
+ notifyCreate a notification (a div.alert in div.alert-messsages) using provided flash object. Flash attributes (all are optional): -
| notify: function(flash) {
- var tmplData = _.extend({
- message: 'Loading',
- category: 'warning',
- loader: false
- },
- flash
- );
- var _template;
- if (tmplData.loader) {
- _template = ' \
- <div class="alert alert-info alert-loader"> \
- {{message}} \
- <span class="notification-loader"> </span> \
- </div>';
- } else {
- _template = ' \
- <div class="alert alert-{{category}} fade in" data-alert="alert"><a class="close" data-dismiss="alert" href="#">×</a> \
- {{message}} \
- </div>';
- }
- var _templated = $(Mustache.render(_template, tmplData));
- _templated = $(_templated).appendTo($('.recline-data-explorer .alert-messages'));
- if (!flash.persist) {
- setTimeout(function() {
- $(_templated).fadeOut(1000, function() {
- $(this).remove();
- });
- }, 1000);
- }
- }, |
clearNotifications+ -Clear all existing notifications | clearNotifications: function() {
- var $notifications = $('.recline-data-explorer .alert-messages .alert');
- $notifications.fadeOut(1500, function() {
- $(this).remove();
- });
- }
-}); |
MultiView.restore+ + + notify: function(flash) {
+ var tmplData = _.extend({
+ message: 'Loading',
+ category: 'warning',
+ loader: false
+ },
+ flash
+ );
+ var _template;
+ if (tmplData.loader) {
+ _template = ' \
+ <div class="alert alert-info alert-loader"> \
+ {{message}} \
+ <span class="notification-loader"> </span> \
+ </div>';
+ } else {
+ _template = ' \
+ <div class="alert alert-{{category}} fade in" data-alert="alert"><a class="close" data-dismiss="alert" href="#">×</a> \
+ {{message}} \
+ </div>';
+ }
+ var _templated = $(Mustache.render(_template, tmplData));
+ _templated = $(_templated).appendTo($('.recline-data-explorer .alert-messages'));
+ if (!flash.persist) {
+ setTimeout(function() {
+ $(_templated).fadeOut(1000, function() {
+ $(this).remove();
+ });
+ }, 1000);
+ }
+ }, clearNotifications: function() {
+ var $notifications = $('.recline-data-explorer .alert-messages .alert');
+ $notifications.fadeOut(1500, function() {
+ $(this).remove();
+ });
+ }
+});
+
+
+ ¶
+
+ MultiView.restoreRestore a MultiView instance from a serialized state including the associated dataset +This inverts the state serialization process in Multiview -This inverts the state serialization process in Multiview | my.MultiView.restore = function(state) { |
| hack-y - restoring a memory dataset does not mean much ... (but useful for testing!) | var datasetInfo;
- if (state.backend === 'memory') {
- datasetInfo = {
- backend: 'memory',
- records: [{stub: 'this is a stub dataset because we do not restore memory datasets'}]
- };
- } else {
- datasetInfo = _.extend({
- url: state.url,
- backend: state.backend
- },
- state.dataset
- );
- }
- var dataset = new recline.Model.Dataset(datasetInfo);
- var explorer = new my.MultiView({
- model: dataset,
- state: state
- });
- return explorer;
-}; |
Miscellaneous Utilities | var urlPathRegex = /^([^?]+)(\?.*)?/; |
| Parse the Hash section of a URL into path and query string | my.parseHashUrl = function(hashUrl) {
- var parsed = urlPathRegex.exec(hashUrl);
- if (parsed === null) {
- return {};
- } else {
- return {
- path: parsed[1],
- query: parsed[2] || ''
- };
- }
-}; |
| Parse a URL query string (?xyz=abc...) into a dictionary. | my.parseQueryString = function(q) {
- if (!q) {
- return {};
- }
- var urlParams = {},
- e, d = function (s) {
- return unescape(s.replace(/\+/g, " "));
- },
- r = /([^&=]+)=?([^&]*)/g;
+ my.MultiView.restore = function(state) {
+
+
+ ¶
+
+ hack-y - restoring a memory dataset does not mean much … (but useful for testing!) - if (q && q.length && q[0] === '?') { - q = q.slice(1); - } - while (e = r.exec(q)) { |
| TODO: have values be array as query string allow repetition of keys | urlParams[d(e[1])] = d(e[2]);
- }
- return urlParams;
-}; |
| Parse the query string out of the URL hash | my.parseHashQueryString = function() {
- var q = my.parseHashUrl(window.location.hash).query;
- return my.parseQueryString(q);
-}; |
| Compse a Query String | my.composeQueryString = function(queryParams) {
- var queryString = '?';
- var items = [];
- $.each(queryParams, function(key, value) {
- if (typeof(value) === 'object') {
- value = JSON.stringify(value);
- }
- items.push(key + '=' + encodeURIComponent(value));
- });
- queryString += items.join('&');
- return queryString;
-};
+ var datasetInfo;
+ if (state.backend === 'memory') {
+ datasetInfo = {
+ backend: 'memory',
+ records: [{stub: 'this is a stub dataset because we do not restore memory datasets'}]
+ };
+ } else {
+ datasetInfo = _.extend({
+ url: state.url,
+ backend: state.backend
+ },
+ state.dataset
+ );
+ }
+ var dataset = new recline.Model.Dataset(datasetInfo);
+ var explorer = new my.MultiView({
+ model: dataset,
+ state: state
+ });
+ return explorer;
+};
+
+
+ ¶
+
+ Miscellaneous Utilities-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;
- }
-};
+ var urlPathRegex = /^([^?]+)(\?.*)?/;
+
+
+
+
+ ¶
+
+ Parse the Hash section of a URL into path and query string -my.setHashQueryString = function(queryParams) { - window.location.hash = my.getNewHashForQueryString(queryParams); -}; +my.parseHashUrl = function(hashUrl) {
+ var parsed = urlPathRegex.exec(hashUrl);
+ if (parsed === null) {
+ return {};
+ } else {
+ return {
+ path: parsed[1],
+ query: parsed[2] || ''
+ };
+ }
+};my.parseQueryString = function(q) {
+ if (!q) {
+ return {};
+ }
+ var urlParams = {},
+ e, d = function (s) {
+ return unescape(s.replace(/\+/g, " "));
+ },
+ r = /([^&=]+)=?([^&]*)/g;
- |
TODO: have values be array as query string allow repetition of keys
+ + urlParams[d(e[1])] = d(e[2]);
+ }
+ return urlParams;
+};Parse the query string out of the URL hash
+ +my.parseHashQueryString = function() {
+ var q = my.parseHashUrl(window.location.hash).query;
+ return my.parseQueryString(q);
+};Compse a Query String
+ +my.composeQueryString = function(queryParams) {
+ var queryString = '?';
+ var items = [];
+ $.each(queryParams, function(key, value) {
+ if (typeof(value) === 'object') {
+ value = JSON.stringify(value);
+ }
+ items.push(key + '=' + encodeURIComponent(value));
+ });
+ queryString += items.join('&');
+ return queryString;
+};
+
+my.getNewHashForQueryString = function(queryParams) {
+ var queryPart = my.composeQueryString(queryParams);
+ if (window.location.hash) {slice(1) to remove # at start
+ + return window.location.hash.split('?')[0].slice(1) + queryPart;
+ } else {
+ return queryPart;
+ }
+};
+
+my.setHashQueryString = function(queryParams) {
+ window.location.hash = my.getNewHashForQueryString(queryParams);
+};
+
+})(jQuery, recline.View); view.slickgrid.js | |
|---|---|
/*jshint multistr:true */
+
-this.recline = this.recline || {};
-this.recline.View = this.recline.View || {};
-
-(function($, my) {
- "use strict"; | |
| Add new grid Control to display a new row add menu bouton -It display a simple side-bar menu ,for user to add new -row to grid | my.GridControl= Backbone.View.extend({
- className: "recline-row-add", |
| Template for row edit menu , change it if you don't love | template: '<h1><a href="#" class="recline-row-add btn">Add row</a></h1>',
+
+
+ |
SlickGrid Dataset View+this.recline = this.recline || {}; +this.recline.View = this.recline.View || {}; +(function($, my) { + "use strict"; + + + + +
+
+
+ ¶
+
+ SlickGrid Dataset ViewProvides a tabular view on a Dataset, based on SlickGrid. - -https://github.com/mleibman/SlickGrid - +https://github.com/mleibman/SlickGrid Initialize it with a Additional options to drive SlickGrid grid can be given through state. -The following keys allow for customization: -* gridOptions: to add options at grid level -* columnsEditor: to add editor for editable columns - +The following keys allow for customization: +
For example: var grid = new recline.View.SlickGrid({ model: dataset, @@ -50,377 +162,953 @@ The following keys allow for customization: state: { gridOptions: { editable: true, - enableAddRows: true - ... + enableAddRow: true + // Enable support for row delete + enabledDelRow: true, + // Enable support for row Reorder + enableReOrderRow:true, + … }, columnsEditor: [ - {column: 'date', editor: Slick.Editors.Date }, - {column: 'title', editor: Slick.Editors.Text} + {column: ‘date’, editor: Slick.Editors.Date }, + {column: ‘title’, editor: Slick.Editors.Text} ] } }); -// NB: you need an explicit height on the element for slickgrid to work | my.SlickGrid = Backbone.View.extend({
- initialize: function(modelEtc) {
- var self = this;
- this.$el.addClass('recline-slickgrid');
- |
| Template for row delete menu , change it if you don't love | this.templates = {
- "deleterow" : '<a href="#" class="recline-row-delete btn">X</a>'
- }
- _.bindAll(this, 'render', 'onRecordChanged');
- this.listenTo(this.model.records, 'add remove reset', this.render);
- this.listenTo(this.model.records, 'change', this.onRecordChanged);
- var state = _.extend({
- hiddenColumns: [],
- columnsOrder: [],
- columnsSort: {},
- columnsWidth: [],
- columnsEditor: [],
- options: {},
- fitColumns: false
- }, modelEtc.state
+// NB: you need an explicit height on the element for slickgrid to work
- );
- this.state = new recline.Model.ObjectState(state);
- this._slickHandler = new Slick.EventHandler(); |
| add menu for new row , check if enableAddRow is set to true or not set | if(this.state.get("gridOptions")
- && this.state.get("gridOptions").enabledAddRow != undefined
- && this.state.get("gridOptions").enabledAddRow == true ){
- this.editor = new my.GridControl()
- this.elSidebar = this.editor.$el
- this.listenTo(this.editor.state, 'change', function(){
- this.model.records.add(new recline.Model.Record())
- });
- }
- },
- onRecordChanged: function(record) { |
| Ignore if the grid is not yet drawn | if (!this.grid) {
- return;
- } |
| Let's find the row corresponding to the index | var row_index = this.grid.getData().getModelRow( record );
- this.grid.invalidateRow(row_index);
- this.grid.getData().updateItem(record, row_index);
- this.grid.render();
- },
- render: function() {
- var self = this;
- var options = _.extend({
- enableCellNavigation: true,
- enableColumnReorder: true,
- explicitInitialization: true,
- syncColumnCellResize: true,
- forceFitColumns: this.state.get('fitColumns')
- }, self.state.get('gridOptions')); |
| We need all columns, even the hidden ones, to show on the column picker | var columns = []; |
| custom formatter as default one escapes html -plus this way we distinguish between rendering/formatting and computed value (so e.g. sort still works ...) -row = row index, cell = cell index, value = value, columnDef = column definition, dataContext = full row values | var formatter = function(row, cell, value, columnDef, dataContext) {
- if(columnDef.id == "del"){
- return self.templates.deleterow
- }
- var field = self.model.fields.get(columnDef.id);
- if (field.renderer) {
- return field.renderer(value, field, dataContext);
- }else {
- return value
- }
- }; |
| we need to be sure that user is entering a valid input , for exemple if -field is date type and field.format ='YY-MM-DD', we should be sure that -user enter a correct value | var validator = function(field){
- return function(value){
- if(field.type == "date" && isNaN(Date.parse(value))){
- return {
- valid: false,
- msg: "A date is required, check field field-date-format"};
- }else {
- return {valid: true, msg :null }
- }
- }
- }; |
| Add row delete support , check if enableDelRow is set to true or not set | if(this.state.get("gridOptions")
- && this.state.get("gridOptions").enabledDelRow != undefined
- && this.state.get("gridOptions").enabledDelRow == true ){
- columns.push({
- id: 'del',
- name: 'del',
- field: 'del',
- sortable: true,
- width: 80,
- formatter: formatter,
- validator:validator
- })}
- _.each(this.model.fields.toJSON(),function(field){
- var column = {
- id: field.id,
- name: field.label,
- field: field.id,
- sortable: true,
- minWidth: 80,
- formatter: formatter,
- validator:validator(field)
- };
- var widthInfo = _.find(self.state.get('columnsWidth'),function(c){return c.column === field.id;});
- if (widthInfo){
- column.width = widthInfo.width;
- }
- var editInfo = _.find(self.state.get('columnsEditor'),function(c){return c.column === field.id;});
- if (editInfo){
- column.editor = editInfo.editor;
- } else { |
| guess editor type | var typeToEditorMap = {
- 'string': Slick.Editors.LongText,
- 'integer': Slick.Editors.IntegerEditor,
- 'number': Slick.Editors.Text, |
| TODO: need a way to ensure we format date in the right way -Plus what if dates are in distant past or future ... (?) -'date': Slick.Editors.DateEditor, | 'date': Slick.Editors.Text,
- 'boolean': Slick.Editors.YesNoSelectEditor |
| TODO: (?) percent ... | };
- if (field.type in typeToEditorMap) {
- column.editor = typeToEditorMap[field.type]
- } else {
- column.editor = Slick.Editors.LongText;
- }
- }
- columns.push(column);
- }); |
| Restrict the visible columns | var visibleColumns = _.filter(columns, function(column) {
- return _.indexOf(self.state.get('hiddenColumns'), column.id) === -1;
- }); |
| Order them if there is ordering info on the state | if (this.state.get('columnsOrder') && this.state.get('columnsOrder').length > 0) {
- visibleColumns = visibleColumns.sort(function(a,b){
- return _.indexOf(self.state.get('columnsOrder'),a.id) > _.indexOf(self.state.get('columnsOrder'),b.id) ? 1 : -1;
- });
- columns = columns.sort(function(a,b){
- return _.indexOf(self.state.get('columnsOrder'),a.id) > _.indexOf(self.state.get('columnsOrder'),b.id) ? 1 : -1;
- });
- } |
| Move hidden columns to the end, so they appear at the bottom of the -column picker | var tempHiddenColumns = [];
- for (var i = columns.length -1; i >= 0; i--){
- if (_.indexOf(_.pluck(visibleColumns,'id'),columns[i].id) === -1){
- tempHiddenColumns.push(columns.splice(i,1)[0]);
- }
- }
- columns = columns.concat(tempHiddenColumns); |
| Transform a model object into a row | function toRow(m) {
- var row = {};
- self.model.fields.each(function(field){
- var render = ""; |
| when adding row from slickgrid the field value is undefined | if(!_.isUndefined(m.getFieldValueUnrendered(field))){
- render =m.getFieldValueUnrendered(field)
- }
- row[field.id] = render
- });
- return row;
- }
+ my.SlickGrid = Backbone.View.extend({
+ initialize: function(modelEtc) {
+ var self = this;
+ this.$el.addClass('recline-slickgrid');
+
+
+
+
+ ¶
+
+ Template for row delete menu , change it if you don’t love - function RowSet() { - var models = []; - var rows = []; + this.templates = {
+ "deleterow" : '<button href="#" class="recline-row-delete btn btn-default" title="Delete row">X</button>'
+ };
- this.push = function(model, row) {
- models.push(model);
- rows.push(row);
- };
+ _.bindAll(this, 'render', 'onRecordChanged');
+ this.listenTo(this.model.records, 'add remove reset', this.render);
+ this.listenTo(this.model.records, 'change', this.onRecordChanged);
+ var state = _.extend({
+ hiddenColumns: [],
+ columnsOrder: [],
+ columnsSort: {},
+ columnsWidth: [],
+ columnsEditor: [],
+ options: {},
+ fitColumns: false
+ }, modelEtc.state
- this.getLength = function() {return rows.length; };
- this.getItem = function(index) {return rows[index];};
- this.getItemMetadata = function(index) {return {};};
- this.getModel = function(index) {return models[index];};
- this.getModelRow = function(m) {return _.indexOf(models, m);};
- this.updateItem = function(m,i) {
- rows[i] = toRow(m);
- models[i] = m;
- };
-
- }
+ );
+ this.state = new recline.Model.ObjectState(state);
+ this._slickHandler = new Slick.EventHandler();
+
+
+
+
+ ¶
+
+ add menu for new row , check if enableAddRow is set to true or not set - var data = new RowSet(); + if(this.state.get("gridOptions")
+ && this.state.get("gridOptions").enabledAddRow != undefined
+ && this.state.get("gridOptions").enabledAddRow == true ){
+ this.editor = new my.GridControl()
+ this.elSidebar = this.editor.$el
+ this.listenTo(this.editor.state, 'change', function(){
+ this.model.records.add(new recline.Model.Record())
+ });
+ }
+ },
- this.model.records.each(function(doc){
- data.push(doc, toRow(doc));
- });
+ onRecordChanged: function(record) {
+
+
+ ¶
+
+ Ignore if the grid is not yet drawn - this.grid = new Slick.Grid(this.el, data, visibleColumns, options); |
| Column sorting | var sortInfo = this.model.queryState.get('sort');
- if (sortInfo){
- var column = sortInfo[0].field;
- var sortAsc = sortInfo[0].order !== 'desc';
- this.grid.setSortColumn(column, sortAsc);
- }
+ if (!this.grid) {
+ return;
+ }
+
+
+
+
+ ¶
+
+ Let’s find the row corresponding to the index - this._slickHandler.subscribe(this.grid.onSort, function(e, args){ - var order = (args.sortAsc) ? 'asc':'desc'; - var sort = [{ - field: args.sortCol.field, - order: order - }]; - self.model.query({sort: sort}); - }); + var row_index = this.grid.getData().getModelRow( record );
+ this.grid.invalidateRow(row_index);
+ this.grid.getData().updateItem(record, row_index);
+ this.grid.render();
+ },
- this._slickHandler.subscribe(this.grid.onColumnsReordered, function(e, args){
- self.state.set({columnsOrder: _.pluck(self.grid.getColumns(),'id')});
- });
+ render: function() {
+ var self = this;
+ var options = _.extend({
+ enableCellNavigation: true,
+ enableColumnReorder: true,
+ explicitInitialization: true,
+ syncColumnCellResize: true,
+ forceFitColumns: this.state.get('fitColumns')
+ }, self.state.get('gridOptions'));
+
+
+
+
+ ¶
+
+ We need all columns, even the hidden ones, to show on the column picker - this.grid.onColumnsResized.subscribe(function(e, args){ - var columns = args.grid.getColumns(); - var defaultColumnWidth = args.grid.getOptions().defaultColumnWidth; - var columnsWidth = []; - _.each(columns,function(column){ - if (column.width != defaultColumnWidth){ - columnsWidth.push({column:column.id,width:column.width}); - } - }); - self.state.set({columnsWidth:columnsWidth}); - }); + var columns = [];
+
+
+
+
+ ¶
+
+ custom formatter as default one escapes html +plus this way we distinguish between rendering/formatting and computed value (so e.g. sort still works …) +row = row index, cell = cell index, value = value, columnDef = column definition, dataContext = full row values + + var formatter = function(row, cell, value, columnDef, dataContext) {
+ if(columnDef.id == "del"){
+ return self.templates.deleterow
+ }
+ var field = self.model.fields.get(columnDef.id);
+ if (field.renderer) {
+ return field.renderer(value, field, dataContext);
+ } else {
+ return value
+ }
+ };
+
+
+
+
+ ¶
+
+ we need to be sure that user is entering a valid input , for exemple if +field is date type and field.format =’YY-MM-DD’, we should be sure that +user enter a correct value + + var validator = function(field) {
+ return function(value){
+ if (field.type == "date" && isNaN(Date.parse(value))){
+ return {
+ valid: false,
+ msg: "A date is required, check field field-date-format"
+ };
+ } else {
+ return {valid: true, msg :null }
+ }
+ }
+ };
+
+
+
+
+ ¶
+
+ Add column for row reorder support + + if (this.state.get("gridOptions") && this.state.get("gridOptions").enableReOrderRow == true) {
+ columns.push({
+ id: "#",
+ name: "",
+ width: 22,
+ behavior: "selectAndMove",
+ selectable: false,
+ resizable: false,
+ cssClass: "recline-cell-reorder"
+ })
+ }
+
+
+
+
+ ¶
+
+ Add column for row delete support + + if (this.state.get("gridOptions") && this.state.get("gridOptions").enabledDelRow == true) {
+ columns.push({
+ id: 'del',
+ name: '',
+ field: 'del',
+ sortable: true,
+ width: 38,
+ formatter: formatter,
+ validator:validator
+ })
+ }
+
+ function sanitizeFieldName(name) {
+ var sanitized = $(name).text();
+ return (name !== sanitized && sanitized !== '') ? sanitized : name;
+ }
+
+ _.each(this.model.fields.toJSON(),function(field){
+ var column = {
+ id: field.id,
+ name: sanitizeFieldName(field.label),
+ field: field.id,
+ sortable: true,
+ minWidth: 80,
+ formatter: formatter,
+ validator:validator(field)
+ };
+ var widthInfo = _.find(self.state.get('columnsWidth'),function(c){return c.column === field.id;});
+ if (widthInfo){
+ column.width = widthInfo.width;
+ }
+ var editInfo = _.find(self.state.get('columnsEditor'),function(c){return c.column === field.id;});
+ if (editInfo){
+ column.editor = editInfo.editor;
+ } else {
+
+
+
+
+ ¶
+
+ guess editor type + + var typeToEditorMap = {
+ 'string': Slick.Editors.LongText,
+ 'integer': Slick.Editors.IntegerEditor,
+ 'number': Slick.Editors.Text,
+
+
+
+
+ ¶
+
+ TODO: need a way to ensure we format date in the right way +Plus what if dates are in distant past or future … (?) +‘date’: Slick.Editors.DateEditor, + + 'date': Slick.Editors.Text,
+ 'boolean': Slick.Editors.YesNoSelectEditor
+
+
+
+
+ ¶
+
+ TODO: (?) percent … + + };
+ if (field.type in typeToEditorMap) {
+ column.editor = typeToEditorMap[field.type]
+ } else {
+ column.editor = Slick.Editors.LongText;
+ }
+ }
+ columns.push(column);
+ });
+
+
+
+
+ ¶
+
+ Restrict the visible columns + + var visibleColumns = _.filter(columns, function(column) {
+ return _.indexOf(self.state.get('hiddenColumns'), column.id) === -1;
+ });
+
+
+
+
+ ¶
+
+ Order them if there is ordering info on the state + + if (this.state.get('columnsOrder') && this.state.get('columnsOrder').length > 0) {
+ visibleColumns = visibleColumns.sort(function(a,b){
+ return _.indexOf(self.state.get('columnsOrder'),a.id) > _.indexOf(self.state.get('columnsOrder'),b.id) ? 1 : -1;
+ });
+ columns = columns.sort(function(a,b){
+ return _.indexOf(self.state.get('columnsOrder'),a.id) > _.indexOf(self.state.get('columnsOrder'),b.id) ? 1 : -1;
+ });
+ }
+
+
+
+
+ ¶
+
+ Move hidden columns to the end, so they appear at the bottom of the +column picker + + var tempHiddenColumns = [];
+ for (var i = columns.length -1; i >= 0; i--){
+ if (_.indexOf(_.pluck(visibleColumns,'id'),columns[i].id) === -1){
+ tempHiddenColumns.push(columns.splice(i,1)[0]);
+ }
+ }
+ columns = columns.concat(tempHiddenColumns);
+
+
+
+
+ ¶
+
+ Transform a model object into a row + + function toRow(m) {
+ var row = {};
+ self.model.fields.each(function(field) {
+ var render = "";
+
+
+
+
+ ¶
+
+ when adding row from slickgrid the field value is undefined + + if(!_.isUndefined(m.getFieldValueUnrendered(field))){
+ render =m.getFieldValueUnrendered(field)
+ }
+ row[field.id] = render
+ });
+ return row;
+ }
+
+ function RowSet() {
+ var models = [];
+ var rows = [];
+
+ this.push = function(model, row) {
+ models.push(model);
+ rows.push(row);
+ };
+
+ this.getLength = function() {return rows.length; };
+ this.getItem = function(index) {return rows[index];};
+ this.getItemMetadata = function(index) {return {};};
+ this.getModel = function(index) {return models[index];};
+ this.getModelRow = function(m) {return _.indexOf(models, m);};
+ this.updateItem = function(m,i) {
+ rows[i] = toRow(m);
+ models[i] = m;
+ };
+ }
+
+ var data = new RowSet();
+
+ this.model.records.each(function(doc){
+ data.push(doc, toRow(doc));
+ });
+
+ this.grid = new Slick.Grid(this.el, data, visibleColumns, options);
+
+
+
+
+ ¶
+
+ Column sorting + + var sortInfo = this.model.queryState.get('sort');
+ if (sortInfo){
+ var column = sortInfo[0].field;
+ var sortAsc = sortInfo[0].order !== 'desc';
+ this.grid.setSortColumn(column, sortAsc);
+ }
+
+ if (this.state.get("gridOptions") && this.state.get("gridOptions").enableReOrderRow) {
+ this._setupRowReordering();
+ }
- this._slickHandler.subscribe(this.grid.onCellChange, function (e, args) { |
| We need to change the model associated value | var grid = args.grid;
- var model = data.getModel(args.row);
- var field = grid.getColumns()[args.cell].id;
- var v = {};
- v[field] = args.item[field];
- model.set(v);
- });
- this._slickHandler.subscribe(this.grid.onClick,function(e, args){
- if (args.cell == 0 && self.state.get("gridOptions").enabledDelRow == true){ |
| We need to delete the associated model | var model = data.getModel(args.row);
- model.destroy()
- }
- }) ;
+ this._slickHandler.subscribe(this.grid.onSort, function(e, args){
+ var order = (args.sortAsc) ? 'asc':'desc';
+ var sort = [{
+ field: args.sortCol.field,
+ order: order
+ }];
+ self.model.query({sort: sort});
+ });
- var columnpicker = new Slick.Controls.ColumnPicker(columns, this.grid,
- _.extend(options,{state:this.state}));
+ this._slickHandler.subscribe(this.grid.onColumnsReordered, function(e, args){
+ self.state.set({columnsOrder: _.pluck(self.grid.getColumns(),'id')});
+ });
+
+ this.grid.onColumnsResized.subscribe(function(e, args){
+ var columns = args.grid.getColumns();
+ var defaultColumnWidth = args.grid.getOptions().defaultColumnWidth;
+ var columnsWidth = [];
+ _.each(columns,function(column){
+ if (column.width != defaultColumnWidth){
+ columnsWidth.push({column:column.id,width:column.width});
+ }
+ });
+ self.state.set({columnsWidth:columnsWidth});
+ });
+
+ this._slickHandler.subscribe(this.grid.onCellChange, function (e, args) {
+
+
+ ¶
+
+ We need to change the model associated value - if (self.visible){ - self.grid.init(); - self.rendered = true; - } else { |
| Defer rendering until the view is visible | self.rendered = false;
- }
+ var grid = args.grid;
+ var model = data.getModel(args.row);
+ var field = grid.getColumns()[args.cell].id;
+ var v = {};
+ v[field] = args.item[field];
+ model.set(v);
+ });
+ this._slickHandler.subscribe(this.grid.onClick,function(e, args){
+
+
+
+
+ ¶
+
+ try catch , because this fail in qunit , but no +error on browser. - return this; - }, + try{e.preventDefault()}catch(e){}
+
+
+
+
+ ¶
+
+ The cell of grid that handle row delete is The first cell (0) if +The grid ReOrder is not present ie enableReOrderRow == false +else it is The the second cell (1) , because The 0 is now cell +that handle row Reoder. - remove: function () { - this._slickHandler.unsubscribeAll(); - Backbone.View.prototype.remove.apply(this, arguments); - }, + var cell =0
+ if(self.state.get("gridOptions")
+ && self.state.get("gridOptions").enableReOrderRow != undefined
+ && self.state.get("gridOptions").enableReOrderRow == true ){
+ cell =1
+ }
+ if (args.cell == cell && self.state.get("gridOptions").enabledDelRow == true){ |
| If the div is hidden, SlickGrid will calculate wrongly some -sizes so we must render it explicitly when the view is visible | if (!this.rendered){
- if (!this.grid){
- this.render();
- }
- this.grid.init();
- this.rendered = true;
- }
- this.visible = true;
- },
+ var model = data.getModel(args.row);
+ model.destroy()
+ }
+ }) ;
+ var columnpicker = new Slick.Controls.ColumnPicker(columns, this.grid,
+ _.extend(options,{state:this.state}));
+ if (self.visible){
+ self.grid.init();
+ self.rendered = true;
+ } else {
+
+
+
+
+ ¶
+
+ Defer rendering until the view is visible - hide: function() { - this.visible = false; - } -}); + self.rendered = false;
+ }
+ return this;
+ },
+
+
+
+
+ ¶
+
+ Row reordering support based on +https://github.com/mleibman/SlickGrid/blob/gh-pages/examples/example9-row-reordering.html -})(jQuery, recline.View); + _setupRowReordering: function() {
+ var self = this;
+ self.grid.setSelectionModel(new Slick.RowSelectionModel());
-/*
-* Context menu for the column picker, adapted from
-* http://mleibman.github.com/SlickGrid/examples/example-grouping
-*
-*/
-(function ($) {
- function SlickColumnPicker(columns, grid, options) {
- var $menu;
- var columnCheckboxes;
+ var moveRowsPlugin = new Slick.RowMoveManager({
+ cancelEditOnDrag: true
+ });
- var defaults = {
- fadeSpeed:250
- };
+ moveRowsPlugin.onBeforeMoveRows.subscribe(function (e, data) {
+ for (var i = 0; i < data.rows.length; i++) {
+
+
+
+
+ ¶
+
+ no point in moving before or after itself - function init() { - grid.onHeaderContextMenu.subscribe(handleHeaderContextMenu); - options = $.extend({}, defaults, options); + if (data.rows[i] == data.insertBefore || data.rows[i] == data.insertBefore - 1) {
+ e.stopPropagation();
+ return false;
+ }
+ }
+ return true;
+ });
+
+ moveRowsPlugin.onMoveRows.subscribe(function (e, args) {
+ var extractedRows = [], left, right;
+ var rows = args.rows;
+ var insertBefore = args.insertBefore;
- $menu = $('<ul class="dropdown-menu slick-contextmenu" style="display:none;position:absolute;z-index:20;" />').appendTo(document.body);
+ var data = self.model.records.toJSON()
+ left = data.slice(0, insertBefore);
+ right= data.slice(insertBefore, data.length);
+
+ rows.sort(function(a,b) { return a-b; });
- $menu.bind('mouseleave', function (e) {
- $(this).fadeOut(options.fadeSpeed);
- });
- $menu.bind('click', updateColumn);
+ for (var i = 0; i < rows.length; i++) {
+ extractedRows.push(data[rows[i]]);
+ }
- }
+ rows.reverse();
- function handleHeaderContextMenu(e, args) {
- e.preventDefault();
- $menu.empty();
- columnCheckboxes = [];
+ for (var i = 0; i < rows.length; i++) {
+ var row = rows[i];
+ if (row < insertBefore) {
+ left.splice(row, 1);
+ } else {
+ right.splice(row - insertBefore, 1);
+ }
+ }
- var $li, $input;
- for (var i = 0; i < columns.length; i++) {
- $li = $('<li />').appendTo($menu);
- $input = $('<input type="checkbox" />').data('column-id', columns[i].id).attr('id','slick-column-vis-'+columns[i].id);
- columnCheckboxes.push($input);
+ data = left.concat(extractedRows.concat(right));
+ var selectedRows = [];
+ for (var i = 0; i < rows.length; i++)
+ selectedRows.push(left.length + i);
- if (grid.getColumnIndex(columns[i].id) !== null) {
- $input.attr('checked', 'checked');
- }
- $input.appendTo($li);
- $('<label />')
- .text(columns[i].name)
- .attr('for','slick-column-vis-'+columns[i].id)
- .appendTo($li);
- }
- $('<li/>').addClass('divider').appendTo($menu);
- $li = $('<li />').data('option', 'autoresize').appendTo($menu);
- $input = $('<input type="checkbox" />').data('option', 'autoresize').attr('id','slick-option-autoresize');
- $input.appendTo($li);
- $('<label />')
- .text('Force fit columns')
- .attr('for','slick-option-autoresize')
- .appendTo($li);
- if (grid.getOptions().forceFitColumns) {
- $input.attr('checked', 'checked');
- }
+ self.model.records.reset(data)
+
+ });
+
+
+
+
+ ¶
+
+ register The plugin to handle row Reorder - $menu.css('top', e.pageY - 10) - .css('left', e.pageX - 10) - .fadeIn(options.fadeSpeed); - } + if(this.state.get("gridOptions") && this.state.get("gridOptions").enableReOrderRow) {
+ self.grid.registerPlugin(moveRowsPlugin);
+ }
+ },
- function updateColumn(e) {
- var checkbox;
+ remove: function () {
+ this._slickHandler.unsubscribeAll();
+ Backbone.View.prototype.remove.apply(this, arguments);
+ },
- if ($(e.target).data('option') === 'autoresize') {
- var checked;
- if ($(e.target).is('li')){
- checkbox = $(e.target).find('input').first();
- checked = !checkbox.is(':checked');
- checkbox.attr('checked',checked);
- } else {
- checked = e.target.checked;
- }
+ show: function() {
+
+
+
+
+ ¶
+
+ If the div is hidden, SlickGrid will calculate wrongly some +sizes so we must render it explicitly when the view is visible - if (checked) { - grid.setOptions({forceFitColumns:true}); - grid.autosizeColumns(); - } else { - grid.setOptions({forceFitColumns:false}); - } - options.state.set({fitColumns:checked}); - return; - } + if (!this.rendered){
+ if (!this.grid){
+ this.render();
+ }
+ this.grid.init();
+ this.rendered = true;
+ }
+ this.visible = true;
+ },
- if (($(e.target).is('li') && !$(e.target).hasClass('divider')) ||
- $(e.target).is('input')) {
- if ($(e.target).is('li')){
- checkbox = $(e.target).find('input').first();
- checkbox.attr('checked',!checkbox.is(':checked'));
- }
- var visibleColumns = [];
- var hiddenColumnsIds = [];
- $.each(columnCheckboxes, function (i, e) {
- if ($(this).is(':checked')) {
- visibleColumns.push(columns[i]);
- } else {
- hiddenColumnsIds.push(columns[i].id);
- }
- });
+ hide: function() {
+ this.visible = false;
+ }
+});
+
+
+
+
+ ¶
+
+ Add new grid Control to display a new row add menu bouton +It display a simple side-bar menu ,for user to add new +row to grid - if (!visibleColumns.length) { - $(e.target).attr('checked', 'checked'); - return; - } +my.GridControl= Backbone.View.extend({
+ className: "recline-row-add",
+
+
+ ¶
+
+ Template for row edit menu , change it if you don’t love - grid.setColumns(visibleColumns); - options.state.set({hiddenColumns:hiddenColumnsIds}); - } - } - init(); - } |
| Slick.Controls.ColumnPicker | $.extend(true, window, { Slick:{ Controls:{ ColumnPicker:SlickColumnPicker }}});
-})(jQuery);
+ template: '<h1><button href="#" class="recline-row-add btn btn-default">Add row</button></h1>',
+
+ initialize: function(options){
+ var self = this;
+ _.bindAll(this, 'render');
+ this.state = new recline.Model.ObjectState();
+ this.render();
+ },
- |
Slick.Controls.ColumnPicker
+ + $.extend(true, window, {
+ Slick: {
+ Controls: {
+ ColumnPicker: SlickColumnPicker
+ }
+ }
+ });
+
+})(jQuery); view.timeline.js | |
|---|---|
/*jshint multistr:true */
+
-this.recline = this.recline || {};
-this.recline.View = this.recline.View || {};
+
+
+ | |
| turn off unnecessary logging from VMM Timeline | if (typeof VMM !== 'undefined') {
- VMM.debug = false;
-} |
Timeline+this.recline = this.recline || {}; +this.recline.View = this.recline.View || {}; -Timeline view using http://timeline.verite.co/ | my.Timeline = Backbone.View.extend({
- template: ' \
- <div class="recline-timeline"> \
- <div id="vmm-timeline-id"></div> \
- </div> \
- ', |
| These are the default (case-insensitive) names of field that are used if found. -If not found, the user will need to define these fields on initialization | startFieldNames: ['date','startdate', 'start', 'start-date'],
- endFieldNames: ['end','endDate'],
- elementId: '#vmm-timeline-id',
+(function($, my) {
+ "use strict";
+
+
+ ¶
+
+ turn off unnecessary logging from VMM Timeline - initialize: function(options) { - var self = this; - this.timeline = new VMM.Timeline(this.elementId); - this._timelineIsInitialized = false; - this.listenTo(this.model.fields, 'reset', function() { - self._setupTemporalField(); - }); - this.listenTo(this.model.records, 'all', function() { - self.reloadData(); - }); - var stateData = _.extend({ - startField: null, - endField: null, |
| by default timelinejs (and browsers) will parse ambiguous dates in US format (mm/dd/yyyy) -set to true to interpret dd/dd/dddd as dd/mm/yyyy | nonUSDates: false,
- timelineJSOptions: {}
- },
- options.state
- );
- this.state = new recline.Model.ObjectState(stateData);
- this._setupTemporalField();
- },
+ if (typeof VMM !== 'undefined') {
+ VMM.debug = false;
+}
+
+
+ ¶
+
+ Timeline+Timeline view using http://timeline.verite.co/ - render: function() { - var tmplData = {}; - var htmls = Mustache.render(this.template, tmplData); - this.$el.html(htmls); |
| can only call _initTimeline once view in DOM as Timeline uses $ -internally to look up element | if ($(this.elementId).length > 0) {
- this._initTimeline();
- }
- },
+ my.Timeline = Backbone.View.extend({
+ template: ' \
+ <div class="recline-timeline"> \
+ <div id="vmm-timeline-id"></div> \
+ </div> \
+ ',
+
+
+ ¶
+
+ These are the default (case-insensitive) names of field that are used if found. +If not found, the user will need to define these fields on initialization - show: function() { |
| only call _initTimeline once view in DOM as Timeline uses $ internally to look up element | if (this._timelineIsInitialized === false) {
- this._initTimeline();
- }
- },
+ startFieldNames: ['date','startdate', 'start', 'start-date'],
+ endFieldNames: ['end','endDate'],
+ elementId: '#vmm-timeline-id',
- _initTimeline: function() {
- var data = this._timelineJSON();
- var config = this.state.get("timelineJSOptions");
- config.id = this.elementId;
- this.timeline.init(config, data);
- this._timelineIsInitialized = true
- },
+ initialize: function(options) {
+ var self = this;
+ this.timeline = new VMM.Timeline(this.elementId);
+ this._timelineIsInitialized = false;
+ this.listenTo(this.model.fields, 'reset', function() {
+ self._setupTemporalField();
+ });
+ this.listenTo(this.model.records, 'all', function() {
+ self.reloadData();
+ });
+ var stateData = _.extend({
+ startField: null,
+ endField: null,
+
+
+ ¶
+
+ by default timelinejs (and browsers) will parse ambiguous dates in US format (mm/dd/yyyy) +set to true to interpret dd/dd/dddd as dd/mm/yyyy - reloadData: function() { - if (this._timelineIsInitialized) { - var data = this._timelineJSON(); - this.timeline.reload(data); - } - }, |
| Convert record to JSON for timeline + + + nonUSDates: false,
+ timelineJSOptions: {}
+ },
+ options.state
+ );
+ this.state = new recline.Model.ObjectState(stateData);
+ this._setupTemporalField();
+ },
- | convertRecord: function(record, fields) {
- return this._convertRecord(record, fields);
- }, |
| Internal method to generate a Timeline formatted entry | _convertRecord: function(record, fields) {
- var start = this._parseDate(record.get(this.state.get('startField')));
- var end = this._parseDate(record.get(this.state.get('endField')));
- if (start) {
- var tlEntry = {
- "startDate": start,
- "endDate": end,
- "headline": String(record.get('title') || ''),
- "text": record.get('description') || record.summary()
- };
- return tlEntry;
- } else {
- return null;
- }
- },
+ render: function() {
+ var tmplData = {};
+ var htmls = Mustache.render(this.template, tmplData);
+ this.$el.html(htmls);
+
+
+ ¶
+
+ can only call _initTimeline once view in DOM as Timeline uses $ +internally to look up element - _timelineJSON: function() { - var self = this; - var out = { - 'timeline': { - 'type': 'default', - 'headline': '', - 'date': [ - ] - } - }; - this.model.records.each(function(record) { - var newEntry = self.convertRecord(record, self.fields); - if (newEntry) { - out.timeline.date.push(newEntry); - } - }); |
| if no entries create a placeholder entry to prevent Timeline crashing with error | if (out.timeline.date.length === 0) {
- var tlEntry = {
- "startDate": '2000,1,1',
- "headline": 'No data to show!'
- };
- out.timeline.date.push(tlEntry);
- }
- return out;
- }, |
| convert dates into a format TimelineJS will handle + + + if ($(this.elementId).length > 0) {
+ this._initTimeline();
+ }
+ },
+
+ show: function() {
+
+
+
+
+ ¶
+
+ only call _initTimeline once view in DOM as Timeline uses $ internally to look up element + + if (this._timelineIsInitialized === false) {
+ this._initTimeline();
+ }
+ },
+
+ _initTimeline: function() {
+ var data = this._timelineJSON();
+ var config = this.state.get("timelineJSOptions");
+ config.id = this.elementId;
+ this.timeline.init(config, data);
+ this._timelineIsInitialized = true
+ },
+
+ reloadData: function() {
+ if (this._timelineIsInitialized) {
+ var data = this._timelineJSON();
+ this.timeline.reload(data);
+ }
+ }, convertRecord: function(record, fields) {
+ return this._convertRecord(record, fields);
+ },
+
+
+
+
+ ¶
+
+ Internal method to generate a Timeline formatted entry + + _convertRecord: function(record, fields) {
+ var start = this._parseDate(record.get(this.state.get('startField')));
+ var end = this._parseDate(record.get(this.state.get('endField')));
+ if (start) {
+ var tlEntry = {
+ "startDate": start,
+ "endDate": end,
+ "headline": String(record.get('title') || ''),
+ "text": record.get('description') || record.summary(),
+ "tag": record.get('tags')
+ };
+ return tlEntry;
+ } else {
+ return null;
+ }
+ },
+
+ _timelineJSON: function() {
+ var self = this;
+ var out = {
+ 'timeline': {
+ 'type': 'default',
+ 'headline': '',
+ 'date': [
+ ]
+ }
+ };
+ this.model.records.each(function(record) {
+ var newEntry = self.convertRecord(record, self.fields);
+ if (newEntry) {
+ out.timeline.date.push(newEntry);
+ }
+ });
+
+
+
+
+ ¶
+
+ if no entries create a placeholder entry to prevent Timeline crashing with error + + if (out.timeline.date.length === 0) {
+ var tlEntry = {
+ "startDate": '2000,1,1',
+ "headline": 'No data to show!'
+ };
+ out.timeline.date.push(tlEntry);
+ }
+ return out;
+ },
+
+
+ ¶
+
+ convert dates into a format TimelineJS will handle TimelineJS does not document this at all so combo of read the code + trial and error Summary (AFAICt): Preferred: [-]yyyy[,mm,dd,hh,mm,ss] -Supported: mm/dd/yyyy | _parseDate: function(date) {
- if (!date) {
- return null;
- }
- var out = $.trim(date);
- out = out.replace(/(\d)th/g, '$1');
- out = out.replace(/(\d)st/g, '$1');
- out = $.trim(out);
- if (out.match(/\d\d\d\d-\d\d-\d\d(T.*)?/)) {
- out = out.replace(/-/g, ',').replace('T', ',').replace(':',',');
- }
- if (out.match(/\d\d-\d\d-\d\d.*/)) {
- out = out.replace(/-/g, '/');
- }
- if (this.state.get('nonUSDates')) {
- var parts = out.match(/(\d\d)\/(\d\d)\/(\d\d.*)/);
- if (parts) {
- out = [parts[2], parts[1], parts[3]].join('/');
- }
- }
- return out;
- },
+Supported: mm/dd/yyyy
- _setupTemporalField: function() {
- this.state.set({
- startField: this._checkField(this.startFieldNames),
- endField: this._checkField(this.endFieldNames)
- });
- },
+ _parseDate: function(date) {
+ if (!date) {
+ return null;
+ }
+ var out = $.trim(date);
+ out = out.replace(/(\d)th/g, '$1');
+ out = out.replace(/(\d)st/g, '$1');
+ out = $.trim(out);
+ if (out.match(/\d\d\d\d-\d\d-\d\d(T.*)?/)) {
+ out = out.replace(/-/g, ',').replace('T', ',').replace(':',',');
+ }
+ if (out.match(/\d\d-\d\d-\d\d.*/)) {
+ out = out.replace(/-/g, '/');
+ }
+ if (this.state.get('nonUSDates')) {
+ var parts = out.match(/(\d\d)\/(\d\d)\/(\d\d.*)/);
+ if (parts) {
+ out = [parts[2], parts[1], parts[3]].join('/');
+ }
+ }
+ return out;
+ },
- _checkField: function(possibleFieldNames) {
- var modelFieldNames = this.model.fields.pluck('id');
- for (var i = 0; i < possibleFieldNames.length; i++){
- for (var j = 0; j < modelFieldNames.length; j++){
- if (modelFieldNames[j].toLowerCase() == possibleFieldNames[i].toLowerCase())
- return modelFieldNames[j];
- }
- }
- return null;
- }
-});
+ _setupTemporalField: function() {
+ this.state.set({
+ startField: this._checkField(this.startFieldNames),
+ endField: this._checkField(this.endFieldNames)
+ });
+ },
-})(jQuery, recline.View);
+ _checkField: function(possibleFieldNames) {
+ var modelFieldNames = this.model.fields.pluck('id');
+ for (var i = 0; i < possibleFieldNames.length; i++){
+ for (var j = 0; j < modelFieldNames.length; j++){
+ if (modelFieldNames[j].toLowerCase() == possibleFieldNames[i].toLowerCase())
+ return modelFieldNames[j];
+ }
+ }
+ return null;
+ }
+});
- |
widget.facetviewer.js | |
|---|---|
/*jshint multistr:true */
+
-this.recline = this.recline || {};
-this.recline.View = this.recline.View || {};
+
+
+ | |
FacetViewer+this.recline = this.recline || {}; +this.recline.View = this.recline.View || {}; +(function($, my) { + "use strict"; + + + + +
+
+
+ ¶
+
+ FacetViewerWidget for displaying facets -Usage: - - | my.FacetViewer = Backbone.View.extend({
- className: 'recline-facet-viewer',
- template: ' \
- <div class="facets"> \
- {{#facets}} \
- <div class="facet-summary" data-facet="{{id}}"> \
- <h3> \
- {{id}} \
- </h3> \
- <ul class="facet-items"> \
- {{#terms}} \
- <li><a class="facet-choice js-facet-filter" data-value="{{term}}" href="#{{term}}">{{term}} ({{count}})</a></li> \
- {{/terms}} \
- {{#entries}} \
- <li><a class="facet-choice js-facet-filter" data-value="{{time}}">{{term}} ({{count}})</a></li> \
- {{/entries}} \
- </ul> \
- </div> \
- {{/facets}} \
- </div> \
- ',
+
+ my.FacetViewer = Backbone.View.extend({
+ className: 'recline-facet-viewer',
+ template: ' \
+ <div class="facets"> \
+ {{#facets}} \
+ <div class="facet-summary" data-facet="{{id}}"> \
+ <h3> \
+ {{id}} \
+ </h3> \
+ <ul class="facet-items"> \
+ {{#terms}} \
+ <li><a class="facet-choice js-facet-filter" data-value="{{term}}" href="#{{term}}">{{term}} ({{count}})</a></li> \
+ {{/terms}} \
+ {{#entries}} \
+ <li><a class="facet-choice js-facet-filter" data-value="{{time}}">{{term}} ({{count}})</a></li> \
+ {{/entries}} \
+ </ul> \
+ </div> \
+ {{/facets}} \
+ </div> \
+ ',
- events: {
- 'click .js-facet-filter': 'onFacetFilter'
- },
- initialize: function(model) {
- _.bindAll(this, 'render');
- this.listenTo(this.model.facets, 'all', this.render);
- this.listenTo(this.model.fields, 'all', this.render);
- this.render();
- },
- render: function() {
- var tmplData = {
- fields: this.model.fields.toJSON()
- };
- tmplData.facets = _.map(this.model.facets.toJSON(), function(facet) {
- if (facet._type === 'date_histogram') {
- facet.entries = _.map(facet.entries, function(entry) {
- entry.term = new Date(entry.time).toDateString();
- return entry;
- });
- }
- return facet;
- });
- var templated = Mustache.render(this.template, tmplData);
- this.$el.html(templated); |
| are there actually any facets to show? | if (this.model.facets.length > 0) {
- this.$el.show();
- } else {
- this.$el.hide();
- }
- },
- onHide: function(e) {
- e.preventDefault();
- this.$el.hide();
- },
- onFacetFilter: function(e) {
- e.preventDefault();
- var $target= $(e.target);
- var fieldId = $target.closest('.facet-summary').attr('data-facet');
- var value = $target.attr('data-value');
- this.model.queryState.addFilter({type: 'term', field: fieldId, term: value}); |
| have to trigger explicitly for some reason | this.model.query();
- }
-});
+ events: {
+ 'click .js-facet-filter': 'onFacetFilter'
+ },
+ initialize: function(model) {
+ _.bindAll(this, 'render');
+ this.listenTo(this.model.facets, 'all', this.render);
+ this.listenTo(this.model.fields, 'all', this.render);
+ this.render();
+ },
+ render: function() {
+ var tmplData = {
+ fields: this.model.fields.toJSON()
+ };
+ tmplData.facets = _.map(this.model.facets.toJSON(), function(facet) {
+ if (facet._type === 'date_histogram') {
+ facet.entries = _.map(facet.entries, function(entry) {
+ entry.term = new Date(entry.time).toDateString();
+ return entry;
+ });
+ }
+ return facet;
+ });
+ var templated = Mustache.render(this.template, tmplData);
+ this.$el.html(templated);
+
+
+
+
+ ¶
+
+ are there actually any facets to show? + + if (this.model.facets.length > 0) {
+ this.$el.show();
+ } else {
+ this.$el.hide();
+ }
+ },
+ onHide: function(e) {
+ e.preventDefault();
+ this.$el.hide();
+ },
+ onFacetFilter: function(e) {
+ e.preventDefault();
+ var $target= $(e.target);
+ var fieldId = $target.closest('.facet-summary').attr('data-facet');
+ var value = $target.attr('data-value');
+ this.model.queryState.addFilter({type: 'term', field: fieldId, term: value});
+
+
+
+
+ ¶
+
+ have to trigger explicitly for some reason + + this.model.query();
+ }
+});
-})(jQuery, recline.View);
-
- |
widget.fields.js | |
|---|---|
/*jshint multistr:true */ | |
| Field Info + + + +
+
+
+
| |
| Editor -- to change type (and possibly format) -Editor for show/hide ... | |
| Summaries of fields + + + + + +
+
+
+
+
+ ¶
+
+ Editor — to change type (and possibly format) +Editor for show/hide … + | |
| Box to boot transform editor ... | this.recline = this.recline || {};
-this.recline.View = this.recline.View || {};
+If number: max, min average …
-(function($, my) {
- "use strict";
+
+
+
+
+
+ ¶
+
+ Box to boot transform editor … + +
+this.recline = this.recline || {};
+this.recline.View = this.recline.View || {};
+
+(function($, my) {
+ "use strict";
-my.Fields = Backbone.View.extend({
- className: 'recline-fields-view',
- template: ' \
- <div class="accordion fields-list well"> \
- <h3>Fields <a href="#" class="js-show-hide">+</a></h3> \
- {{#fields}} \
- <div class="accordion-group field"> \
- <div class="accordion-heading"> \
- <i class="icon-file"></i> \
- <h4> \
- {{label}} \
- <small> \
- {{type}} \
- <a class="accordion-toggle" data-toggle="collapse" href="#collapse{{id}}"> » </a> \
- </small> \
- </h4> \
- </div> \
- <div id="collapse{{id}}" class="accordion-body collapse"> \
- <div class="accordion-inner"> \
- {{#facets}} \
- <div class="facet-summary" data-facet="{{id}}"> \
- <ul class="facet-items"> \
- {{#terms}} \
- <li class="facet-item"><span class="term">{{term}}</span> <span class="count">[{{count}}]</span></li> \
- {{/terms}} \
- </ul> \
- </div> \
- {{/facets}} \
- <div class="clear"></div> \
- </div> \
- </div> \
- </div> \
- {{/fields}} \
- </div> \
- ',
+my.Fields = Backbone.View.extend({
+ className: 'recline-fields-view',
+ template: ' \
+ <div class="panel-group fields-list well"> \
+ <h3>Fields <a href="#" class="js-show-hide">+</a></h3> \
+ {{#fields}} \
+ <div class="panel panel-default field"> \
+ <div class="panel-heading"> \
+ <i class="glyphicon glyphicon-file"></i> \
+ <h4> \
+ {{label}} \
+ <small> \
+ {{type}} \
+ <a class="accordion-toggle" data-toggle="collapse" href="#collapse{{id}}"> » </a> \
+ </small> \
+ </h4> \
+ </div> \
+ <div id="collapse{{id}}" class="panel-collapse collapse"> \
+ <div class="panel-body"> \
+ {{#facets}} \
+ <div class="facet-summary" data-facet="{{id}}"> \
+ <ul class="facet-items"> \
+ {{#terms}} \
+ <li class="facet-item"><span class="term">{{term}}</span> <span class="count">[{{count}}]</span></li> \
+ {{/terms}} \
+ </ul> \
+ </div> \
+ {{/facets}} \
+ <div class="clear"></div> \
+ </div> \
+ </div> \
+ </div> \
+ {{/fields}} \
+ </div> \
+ ',
- initialize: function(model) {
- var self = this;
- _.bindAll(this, 'render'); |
| TODO: this is quite restrictive in terms of when it is re-run + initialize: function(model) { + var self = this; + _.bindAll(this, 'render'); + + + + +
+
+
+ ¶
+
+ TODO: this is quite restrictive in terms of when it is re-run e.g. a change in type will not trigger a re-run atm. -being more liberal (e.g. binding to all) can lead to being called a lot (e.g. for change:width) | this.listenTo(this.model.fields, 'reset', function(action) {
- self.model.fields.each(function(field) {
- field.facets.unbind('all', self.render);
- field.facets.bind('all', self.render);
- }); |
| fields can get reset or changed in which case we need to recalculate | self.model.getFieldsSummary();
- self.render();
- });
- this.$el.find('.collapse').collapse();
- this.render();
- },
- render: function() {
- var self = this;
- var tmplData = {
- fields: []
- };
- this.model.fields.each(function(field) {
- var out = field.toJSON();
- out.facets = field.facets.toJSON();
- tmplData.fields.push(out);
- });
- var templated = Mustache.render(this.template, tmplData);
- this.$el.html(templated);
- }
-});
+being more liberal (e.g. binding to all) can lead to being called a lot (e.g. for change:width)
-})(jQuery, recline.View);
+ this.listenTo(this.model.fields, 'reset', function(action) {
+ self.model.fields.each(function(field) {
+ field.facets.unbind('all', self.render);
+ field.facets.bind('all', self.render);
+ });
+
+
+ ¶
+
+ fields can get reset or changed in which case we need to recalculate - |
self.model.getFieldsSummary();
+ self.render();
+ });
+ this.$el.find('.collapse').collapse();
+ this.render();
+ },
+ render: function() {
+ var self = this;
+ var tmplData = {
+ fields: []
+ };
+ this.model.fields.each(function(field) {
+ var out = field.toJSON();
+ out.facets = field.facets.toJSON();
+ tmplData.fields.push(out);
+ });
+ var templated = Mustache.render(this.template, tmplData);
+ this.$el.html(templated);
+ }
+});
+
+})(jQuery, recline.View); widget.filtereditor.js | |
|---|---|
/*jshint multistr:true */
+
-this.recline = this.recline || {};
-this.recline.View = this.recline.View || {};
+
+
+ | |
| we will use idx in list as there id ... | tmplData.filters = _.map(tmplData.filters, function(filter, idx) {
- filter.id = idx;
- return filter;
- });
- tmplData.fields = this.model.fields.toJSON();
- tmplData.filterRender = function() {
- return Mustache.render(self.filterTemplates[this.type], this);
- };
- var out = Mustache.render(this.template, tmplData);
- this.$el.html(out);
- },
- onAddFilterShow: function(e) {
- e.preventDefault();
- var $target = $(e.target);
- $target.hide();
- this.$el.find('form.js-add').show();
- },
- onAddFilter: function(e) {
- e.preventDefault();
- var $target = $(e.target);
- $target.hide();
- var filterType = $target.find('select.filterType').val();
- var field = $target.find('select.fields').val();
- this.model.queryState.addFilter({type: filterType, field: field});
- },
- onRemoveFilter: function(e) {
- e.preventDefault();
- var $target = $(e.target);
- var filterId = $target.attr('data-filter-id');
- this.model.queryState.removeFilter(filterId);
- },
- onTermFiltersUpdate: function(e) {
- var self = this;
- e.preventDefault();
- var filters = self.model.queryState.get('filters');
- var $form = $(e.target);
- _.each($form.find('input'), function(input) {
- var $input = $(input);
- var filterType = $input.attr('data-filter-type');
- var fieldId = $input.attr('data-filter-field');
- var filterIndex = parseInt($input.attr('data-filter-id'), 10);
- var name = $input.attr('name');
- var value = $input.val();
+(function($, my) {
+ "use strict";
- switch (filterType) {
- case 'term':
- filters[filterIndex].term = value;
- break;
- case 'range':
- filters[filterIndex][name] = value;
- break;
- case 'geo_distance':
- if(name === 'distance') {
- filters[filterIndex].distance = parseFloat(value);
- }
- else {
- filters[filterIndex].point[name] = parseFloat(value);
- }
- break;
- }
- });
- self.model.queryState.set({filters: filters, from: 0});
- self.model.queryState.trigger('change');
- }
-});
+my.FilterEditor = Backbone.View.extend({
+ className: 'recline-filter-editor well',
+ template: ' \
+ <div class="filters"> \
+ <h3>Filters</h3> \
+ <a href="#" class="js-add-filter">Add filter</a> \
+ <form class="form-stacked js-add" style="display: none;"> \
+ <div class="form-group"> \
+ <label>Field</label> \
+ <select class="fields form-control"> \
+ {{#fields}} \
+ <option value="{{id}}">{{label}}</option> \
+ {{/fields}} \
+ </select> \
+ </div> \
+ <div class="form-group"> \
+ <label>Filter type</label> \
+ <select class="filterType form-control"> \
+ <option value="term">Value</option> \
+ <option value="range">Range</option> \
+ <option value="geo_distance">Geo distance</option> \
+ </select> \
+ </div> \
+ <button type="submit" class="btn btn-default">Add</button> \
+ </form> \
+ <form class="form-stacked js-edit"> \
+ {{#filters}} \
+ {{{filterRender}}} \
+ {{/filters}} \
+ {{#filters.length}} \
+ <button type="submit" class="btn btn-default">Update</button> \
+ {{/filters.length}} \
+ </form> \
+ </div> \
+ ',
+ filterTemplates: {
+ term: ' \
+ <div class="filter-{{type}} filter"> \
+ <fieldset> \
+ <legend> \
+ {{field}} <small>{{type}}</small> \
+ <a class="js-remove-filter" href="#" title="Remove this filter" data-filter-id="{{id}}">×</a> \
+ </legend> \
+ <input class="input-sm" type="text" value="{{term}}" name="term" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
+ </fieldset> \
+ </div> \
+ ',
+ range: ' \
+ <div class="filter-{{type}} filter"> \
+ <fieldset> \
+ <legend> \
+ {{field}} <small>{{type}}</small> \
+ <a class="js-remove-filter" href="#" title="Remove this filter" data-filter-id="{{id}}">×</a> \
+ </legend> \
+ <div class="form-group"> \
+ <label class="control-label" for="">From</label> \
+ <input class="input-sm" type="text" value="{{from}}" name="from" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
+ </div> \
+ <div class="form-group"> \
+ <label class="control-label" for="">To</label> \
+ <input class="input-sm" type="text" value="{{to}}" name="to" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
+ </div> \
+ </fieldset> \
+ </div> \
+ ',
+ geo_distance: ' \
+ <div class="filter-{{type}} filter"> \
+ <fieldset> \
+ <legend> \
+ {{field}} <small>{{type}}</small> \
+ <a class="js-remove-filter" href="#" title="Remove this filter" data-filter-id="{{id}}">×</a> \
+ </legend> \
+ <div class="form-group"> \
+ <label class="control-label" for="">Longitude</label> \
+ <input class="input-sm" type="text" value="{{point.lon}}" name="lon" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
+ </div> \
+ <div class="form-group"> \
+ <label class="control-label" for="">Latitude</label> \
+ <input class="input-sm" type="text" value="{{point.lat}}" name="lat" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
+ </div> \
+ <div class="form-group"> \
+ <label class="control-label" for="">Distance (km)</label> \
+ <input class="input-sm" type="text" value="{{distance}}" name="distance" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
+ </div> \
+ </fieldset> \
+ </div> \
+ '
+ },
+ events: {
+ 'click .js-remove-filter': 'onRemoveFilter',
+ 'click .js-add-filter': 'onAddFilterShow',
+ 'submit form.js-edit': 'onTermFiltersUpdate',
+ 'submit form.js-add': 'onAddFilter'
+ },
+ initialize: function() {
+ _.bindAll(this, 'render');
+ this.listenTo(this.model.fields, 'all', this.render);
+ this.listenTo(this.model.queryState, 'change change:filters:new-blank', this.render);
+ this.render();
+ },
+ render: function() {
+ var self = this;
+ var tmplData = $.extend(true, {}, this.model.queryState.toJSON());
+
+
+
+
+ ¶
+
+ we will use idx in list as there id … + + tmplData.filters = _.map(tmplData.filters, function(filter, idx) {
+ filter.id = idx;
+ return filter;
+ });
+ tmplData.fields = this.model.fields.toJSON();
+ tmplData.filterRender = function() {
+ return Mustache.render(self.filterTemplates[this.type], this);
+ };
+ var out = Mustache.render(this.template, tmplData);
+ this.$el.html(out);
+ },
+ onAddFilterShow: function(e) {
+ e.preventDefault();
+ var $target = $(e.target);
+ $target.hide();
+ this.$el.find('form.js-add').show();
+ },
+ onAddFilter: function(e) {
+ e.preventDefault();
+ var $target = $(e.target);
+ $target.hide();
+ var filterType = $target.find('select.filterType').val();
+ var field = $target.find('select.fields').val();
+ this.model.queryState.addFilter({type: filterType, field: field});
+ },
+ onRemoveFilter: function(e) {
+ e.preventDefault();
+ var $target = $(e.target);
+ var filterId = $target.attr('data-filter-id');
+ this.model.queryState.removeFilter(filterId);
+ },
+ onTermFiltersUpdate: function(e) {
+ var self = this;
+ e.preventDefault();
+ var filters = self.model.queryState.get('filters');
+ var $form = $(e.target);
+ _.each($form.find('input'), function(input) {
+ var $input = $(input);
+ var filterType = $input.attr('data-filter-type');
+ var fieldId = $input.attr('data-filter-field');
+ var filterIndex = parseInt($input.attr('data-filter-id'), 10);
+ var name = $input.attr('name');
+ var value = $input.val();
+
+ switch (filterType) {
+ case 'term':
+ filters[filterIndex].term = value;
+ break;
+ case 'range':
+ filters[filterIndex][name] = value;
+ break;
+ case 'geo_distance':
+ if(name === 'distance') {
+ filters[filterIndex].distance = parseFloat(value);
+ }
+ else {
+ filters[filterIndex].point[name] = parseFloat(value);
+ }
+ break;
+ }
+ });
+ self.model.queryState.set({filters: filters, from: 0});
+ self.model.queryState.trigger('change');
+ }
+});
-})(jQuery, recline.View);
-
- |
widget.pager.js | |
|---|---|
/*jshint multistr:true */
+
-this.recline = this.recline || {};
-this.recline.View = this.recline.View || {};
+
+
+ |
var formFrom = parseInt(this.$el.find('input[name="from"]').val())-1;
+ var formTo = parseInt(this.$el.find('input[name="to"]').val())-1;
+ var maxRecord = this.model.recordCount-1;
+ if (this.model.queryState.get('from') != formFrom) { // changed from; update from
+ this.model.queryState.set({from: Math.min(maxRecord, Math.max(formFrom, 0))});
+ } else if (this.model.queryState.get('to') != formTo) { // change to; update size
+ var to = Math.min(maxRecord, Math.max(formTo, 0));
+ this.model.queryState.set({size: Math.min(maxRecord+1, Math.max(to-formFrom+1, 1))});
+ }
+ },
+ onPaginationUpdate: function(e) {
+ e.preventDefault();
+ var $el = $(e.target);
+ var newFrom = 0;
+ var currFrom = this.model.queryState.get('from');
+ var size = this.model.queryState.get('size');
+ var updateQuery = false;
+ if ($el.parent().hasClass('prev')) {
+ newFrom = Math.max(currFrom - Math.max(0, size), 0);
+ updateQuery = newFrom != currFrom;
+ } else {
+ newFrom = Math.max(currFrom + size, 0);
+ updateQuery = (newFrom < this.model.recordCount);
+ }
+ if (updateQuery) {
+ this.model.queryState.set({from: newFrom});
+ }
+ },
+ render: function() {
+ var tmplData = this.model.toJSON();
+ var from = parseInt(this.model.queryState.get('from'));
+ tmplData.from = from+1;
+ tmplData.to = Math.min(from+this.model.queryState.get('size'), this.model.recordCount);
+ var templated = Mustache.render(this.template, tmplData);
+ this.$el.html(templated);
+ return this;
+ }
+});
+
+})(jQuery, recline.View); widget.queryeditor.js | |
|---|---|
/*jshint multistr:true */
+
-this.recline = this.recline || {};
-this.recline.View = this.recline.View || {};
+
+
+ |
/*jshint multistr:true */
+
+this.recline = this.recline || {};
+this.recline.View = this.recline.View || {};
+
+(function($, my) {
+ "use strict";
+
+my.ValueFilter = Backbone.View.extend({
+ className: 'recline-filter-editor well',
+ template: ' \
+ <div class="filters"> \
+ <h3>Filters</h3> \
+ <button class="btn js-add-filter add-filter">Add filter</button> \
+ <form class="form-stacked js-add" style="display: none;"> \
+ <fieldset> \
+ <label>Field</label> \
+ <select class="fields form-control"> \
+ {{#fields}} \
+ <option value="{{id}}">{{label}}</option> \
+ {{/fields}} \
+ </select> \
+ <button type="submit" class="btn">Add</button> \
+ </fieldset> \
+ </form> \
+ <form class="form-stacked js-edit"> \
+ {{#filters}} \
+ {{{filterRender}}} \
+ {{/filters}} \
+ {{#filters.length}} \
+ <button type="submit" class="btn update-filter">Update</button> \
+ {{/filters.length}} \
+ </form> \
+ </div> \
+ ',
+ filterTemplates: {
+ term: ' \
+ <div class="filter-{{type}} filter"> \
+ <fieldset> \
+ {{field}} \
+ <a class="js-remove-filter" href="#" title="Remove this filter" data-filter-id="{{id}}">×</a> \
+ <input type="text" value="{{term}}" name="term" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
+ </fieldset> \
+ </div> \
+ '
+ },
+ events: {
+ 'click .js-remove-filter': 'onRemoveFilter',
+ 'click .js-add-filter': 'onAddFilterShow',
+ 'submit form.js-edit': 'onTermFiltersUpdate',
+ 'submit form.js-add': 'onAddFilter'
+ },
+ initialize: function() {
+ _.bindAll(this, 'render');
+ this.listenTo(this.model.fields, 'all', this.render);
+ this.listenTo(this.model.queryState, 'change change:filters:new-blank', this.render);
+ this.render();
+ },
+ render: function() {
+ var self = this;
+ var tmplData = $.extend(true, {}, this.model.queryState.toJSON());we will use idx in list as the id …
+ + tmplData.filters = _.map(tmplData.filters, function(filter, idx) {
+ filter.id = idx;
+ return filter;
+ });
+ tmplData.fields = this.model.fields.toJSON();
+ tmplData.filterRender = function() {
+ return Mustache.render(self.filterTemplates.term, this);
+ };
+ var out = Mustache.render(this.template, tmplData);
+ this.$el.html(out);
+ },
+ updateFilter: function(input) {
+ var self = this;
+ var filters = self.model.queryState.get('filters');
+ var $input = $(input);
+ var filterIndex = parseInt($input.attr('data-filter-id'), 10);
+ var value = $input.val();
+ filters[filterIndex].term = value;
+ },
+ onAddFilterShow: function(e) {
+ e.preventDefault();
+ var $target = $(e.target);
+ $target.hide();
+ this.$el.find('form.js-add').show();
+ },
+ onAddFilter: function(e) {
+ e.preventDefault();
+ var $target = $(e.target);
+ $target.hide();
+ var field = $target.find('select.fields').val();
+ this.model.queryState.addFilter({type: 'term', field: field});
+ },
+ onRemoveFilter: function(e) {
+ e.preventDefault();
+ var $target = $(e.target);
+ var filterId = $target.attr('data-filter-id');
+ this.model.queryState.removeFilter(filterId);
+ },
+ onTermFiltersUpdate: function(e) {
+ var self = this;
+ e.preventDefault();
+ var filters = self.model.queryState.get('filters');
+ var $form = $(e.target);
+ _.each($form.find('input'), function(input) {
+ self.updateFilter(input);
+ });
+ self.model.queryState.set({filters: filters, from: 0});
+ self.model.queryState.trigger('change');
+ }
+});
+
+})(jQuery, recline.View);