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='
';this.fields.each(function(field){if(field.id!="id"){html+='
'+field.get("label")+": "+self.getFieldValue(field)+"
"}});html+="
";return html},fetch:function(){},save:function(){},destroy:function(){this.trigger("destroy",this)}});my.RecordList=Backbone.Collection.extend({constructor:function RecordList(){Backbone.Collection.prototype.constructor.apply(this,arguments)},model:my.Record});my.Field=Backbone.Model.extend({constructor:function Field(){Backbone.Model.prototype.constructor.apply(this,arguments)},defaults:{label:null,type:"string",format:null,is_derived:false},initialize:function(data,options){if("0"in data){throw new Error("Looks like you did not pass a proper hash with id to Field constructor")}if(this.attributes.label===null){this.set({label:this.id})}if(this.attributes.type.toLowerCase()in this._typeMap){this.attributes.type=this._typeMap[this.attributes.type.toLowerCase()]}if(options){this.renderer=options.renderer;this.deriver=options.deriver}if(!this.renderer){this.renderer=this.defaultRenderers[this.get("type")]}this.facets=new my.FacetList},_typeMap:{text:"string","double":"number","float":"number",numeric:"number","int":"integer",datetime:"date-time",bool:"boolean",timestamp:"date-time",json:"object"},defaultRenderers:{object:function(val,field,doc){return JSON.stringify(val)},geo_point:function(val,field,doc){return JSON.stringify(val)},number:function(val,field,doc){var format=field.get("format");if(format==="percentage"){return val+"%"}return val},string:function(val,field,doc){var format=field.get("format");if(format==="markdown"){if(typeof Showdown!=="undefined"){var showdown=new Showdown.converter;out=showdown.makeHtml(val);return out}else{return val}}else if(format=="plain"){return val}else{if(val&&typeof val==="string"){val=val.replace(/(https?:\/\/[^ ]+)/g,'$1')}return val}}}});my.FieldList=Backbone.Collection.extend({constructor:function FieldList(){Backbone.Collection.prototype.constructor.apply(this,arguments)},model:my.Field});my.Query=Backbone.Model.extend({constructor:function Query(){Backbone.Model.prototype.constructor.apply(this,arguments)},defaults:function(){return{size:100,from:0,q:"",facets:{},filters:[]}},_filterTemplates:{term:{type:"term",field:"",term:""},range:{type:"range",from:"",to:""},geo_distance:{type:"geo_distance",distance:10,unit:"km",point:{lon:0,lat:0}}},addFilter:function(filter){var ourfilter=JSON.parse(JSON.stringify(filter));if(_.keys(filter).length<=3){ourfilter=_.defaults(ourfilter,this._filterTemplates[filter.type])}var filters=this.get("filters");filters.push(ourfilter);this.trigger("change:filters:new-blank")},replaceFilter:function(filter){var filters=this.get("filters");var idx=-1;_.each(this.get("filters"),function(f,key,list){if(filter.field==f.field){idx=key}});if(idx>=0){filters.splice(idx,1);this.set({filters:filters})}this.addFilter(filter)},updateFilter:function(index,value){},removeFilter:function(filterIndex){var filters=this.get("filters");filters.splice(filterIndex,1);this.set({filters:filters});this.trigger("change")},addFacet:function(fieldId,size,silent){var facets=this.get("facets");if(_.contains(_.keys(facets),fieldId)){return}facets[fieldId]={terms:{field:fieldId}};if(!_.isUndefined(size)){facets[fieldId].terms.size=size}this.set({facets:facets},{silent:true});if(!silent){this.trigger("facet:add",this)}},addHistogramFacet:function(fieldId){var facets=this.get("facets");facets[fieldId]={date_histogram:{field:fieldId,interval:"day"}};this.set({facets:facets},{silent:true});this.trigger("facet:add",this)},removeFacet:function(fieldId){var facets=this.get("facets");if(!_.contains(_.keys(facets),fieldId)){return}delete facets[fieldId];this.set({facets:facets},{silent:true});this.trigger("facet:remove",this)},clearFacets:function(){var facets=this.get("facets");_.each(_.keys(facets),function(fieldId){delete facets[fieldId]});this.trigger("facet:remove",this)},refreshFacets:function(){this.trigger("facet:add",this)}});my.Facet=Backbone.Model.extend({constructor:function Facet(){Backbone.Model.prototype.constructor.apply(this,arguments)},defaults:function(){return{_type:"terms",total:0,other:0,missing:0,terms:[]}}});my.FacetList=Backbone.Collection.extend({constructor:function FacetList(){Backbone.Collection.prototype.constructor.apply(this,arguments)},model:my.Facet});my.ObjectState=Backbone.Model.extend({})})(this.recline.Model);this.recline=this.recline||{};this.recline.Backend=this.recline.Backend||{};this.recline.Backend.Memory=this.recline.Backend.Memory||{};(function(my){"use strict";my.__type__="memory";var Deferred=typeof jQuery!=="undefined"&&jQuery.Deferred||_.Deferred;my.Store=function(records,fields){var self=this;this.records=records;this.data=this.records;if(fields){this.fields=fields}else{if(records){this.fields=_.map(records[0],function(value,key){return{id:key,type:"string"}})}}this.update=function(doc){_.each(self.records,function(internalDoc,idx){if(doc.id===internalDoc.id){self.records[idx]=doc}})};this.remove=function(doc){var newdocs=_.reject(self.records,function(internalDoc){return doc.id===internalDoc.id});this.records=newdocs};this.save=function(changes,dataset){var self=this;var dfd=new Deferred;_.each(changes.updates,function(record){self.update(record)});_.each(changes.deletes,function(record){self.remove(record)});dfd.resolve();return dfd.promise()},this.query=function(queryObj){var dfd=new Deferred;var numRows=queryObj.size||this.records.length;var start=queryObj.from||0;var results=this.records;results=this._applyFilters(results,queryObj);results=this._applyFreeTextQuery(results,queryObj);_.each(queryObj.sort,function(sortObj){var fieldName=sortObj.field;results=_.sortBy(results,function(doc){var _out=doc[fieldName];return _out});if(sortObj.order=="desc"){results.reverse()}});var facets=this.computeFacets(results,queryObj);var out={total:results.length,hits:results.slice(start,start+numRows),facets:facets};dfd.resolve(out);return dfd.promise()};this._applyFilters=function(results,queryObj){var filters=queryObj.filters;var filterFunctions={term:term,terms:terms,range:range,geo_distance:geo_distance};var dataParsers={integer:function(e){return parseFloat(e,10)},"float":function(e){return parseFloat(e,10)},number:function(e){return parseFloat(e,10)},string:function(e){return e.toString()},date:function(e){return moment(e).valueOf()},datetime:function(e){return new Date(e).valueOf()}};var keyedFields={};_.each(self.fields,function(field){keyedFields[field.id]=field});function getDataParser(filter){var fieldType=keyedFields[filter.field].type||"string";return dataParsers[fieldType]}return _.filter(results,function(record){var passes=_.map(filters,function(filter){return filterFunctions[filter.type](record,filter)});return _.all(passes,_.identity)});function term(record,filter){var parse=getDataParser(filter);var value=parse(record[filter.field]);var term=parse(filter.term);return value===term}function terms(record,filter){var parse=getDataParser(filter);var value=parse(record[filter.field]);var terms=parse(filter.terms).split(",");return _.indexOf(terms,value)>=0}function range(record,filter){var fromnull=_.isUndefined(filter.from)||filter.from===null||filter.from==="";var tonull=_.isUndefined(filter.to)||filter.to===null||filter.to==="";var parse=getDataParser(filter);var value=parse(record[filter.field]);var from=parse(fromnull?"":filter.from);var to=parse(tonull?"":filter.to);if((!fromnull||!tonull)&&value===""){return false}return(fromnull||value>=from)&&(tonull||value<=to)}function geo_distance(){}};this._applyFreeTextQuery=function(results,queryObj){if(queryObj.q){var terms=queryObj.q.split(" ");var patterns=_.map(terms,function(term){return new RegExp(term.toLowerCase())});results=_.filter(results,function(rawdoc){var matches=true;_.each(patterns,function(pattern){var foundmatch=false;_.each(self.fields,function(field){var value=rawdoc[field.id];if(value!==null&&value!==undefined){value=value.toString()}else{value=""}foundmatch=foundmatch||pattern.test(value.toLowerCase())});matches=matches&&foundmatch});return matches})}return results};this.computeFacets=function(records,queryObj){var facetResults={};if(!queryObj.facets){return facetResults}_.each(queryObj.facets,function(query,facetId){facetResults[facetId]=new recline.Model.Facet({id:facetId}).toJSON();facetResults[facetId].termsall={}});_.each(records,function(doc){_.each(queryObj.facets,function(query,facetId){var fieldId=query.terms.field;var val=doc[fieldId];var tmp=facetResults[facetId];if(val){tmp.termsall[val]=tmp.termsall[val]?tmp.termsall[val]+1:1}else{tmp.missing=tmp.missing+1}})});_.each(queryObj.facets,function(query,facetId){var tmp=facetResults[facetId];var terms=_.map(tmp.termsall,function(count,term){return{term:term,count:count}});tmp.terms=_.sortBy(terms,function(item){return-item.count});tmp.terms=tmp.terms.slice(0,10)});return facetResults}}})(this.recline.Backend.Memory); \ No newline at end of file diff --git a/dist/recline.js b/dist/recline.js index 76334781..1fd8e7eb 100644 --- a/dist/recline.js +++ b/dist/recline.js @@ -1064,7 +1064,7 @@ my.Flot = Backbone.View.extend({ template: ' \
\
\ -
\ +
\

Hey there!

\

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

\

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

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

view.flot.js

/*jshint multistr:true */
+
 
-this.recline = this.recline || {};
-this.recline.View = this.recline.View || {};
+
+
+  view.flot.js
+  
+  
+  
+
+
+  

Graph view for a Dataset using Flot graphing library.

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

    Graph view for a Dataset using Flot graphing library.

    Initialization arguments (in a hash in first parameter):

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

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

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

    -

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

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

    so that at least one series chooser box shows up

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

    There are issues generating a Flot graph if either:

    +
      +
    • The relevant div that graph attaches to his hidden at the moment of creating the plot — Flot will complain with +Uncaught Invalid dimensions for plot, width = 0, height = 0
    • +
    • There is no data for the plot — either same error or may have issues later with errors like ‘non-existent node-value’
    -

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

  • my.Flot = Backbone.View.extend({
    -  template: ' \
    -    <div class="recline-flot"> \
    -      <div class="panel graph" style="display: block;"> \
    -        <div class="js-temp-notice alert alert-block"> \
    -          <h3 class="alert-heading">Hey there!</h3> \
    -          <p>There\'s no graph here yet because we don\'t know what fields you\'d like to see plotted.</p> \
    -          <p>Please tell us by <strong>using the menu on the right</strong> and a graph will automatically appear.</p> \
    -        </div> \
    -      </div> \
    -    </div> \
    -',
    +            
    + +
        var areWeVisible = !jQuery.expr.filters.hidden(this.el);
    +    if ((!areWeVisible || this.model.records.length === 0)) {
    +      this.needToRedraw = true;
    +      return;
    +    }
    + + + + +
  • +
    + +
    + +
    +

    check we have something to plot

    - initialize: function(options) { - var self = this; - this.graphColors = ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"]; +
    + +
        if (this.state.get('group') && this.state.get('series')) {
    +      var series = this.createSeries();
    +      var options = this.getGraphOptions(this.state.attributes.graphType, series[0].data.length);
    +      this.plot = $.plot(this.$graph, series, options);
    +    }
    +  },
     
    -    _.bindAll(this, 'render', 'redraw', '_toolTip', '_xaxisLabel');
    -    this.needToRedraw = false;
    -    this.listenTo(this.model, 'change', this.render);
    -    this.listenTo(this.model.fields, 'reset add', this.render);
    -    this.listenTo(this.model.records, 'reset add', this.redraw);
    -    var stateData = _.extend({
    -        group: null,
  • so that at least one series chooser box shows up

            series: [],
    -        graphType: 'lines-and-points'
    -      },
    -      options.state
    -    );
    -    this.state = new recline.Model.ObjectState(stateData);
    -    this.previousTooltipPoint = {x: null, y: null};
    -    this.editor = new my.FlotControls({
    -      model: this.model,
    -      state: this.state.toJSON()
    -    });
    -    this.listenTo(this.editor.state, 'change', function() {
    -      self.state.set(self.editor.state.toJSON());
    -      self.redraw();
    -    });
    -    this.elSidebar = this.editor.$el;
    -  },
    +  show: function() {
    + + + + +
  • +
    + +
    + +
    +

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

    - render: function() { - var self = this; - var tmplData = this.model.toTemplateJSON(); - var htmls = Mustache.render(this.template, tmplData); - this.$el.html(htmls); - this.$graph = this.$el.find('.panel.graph'); - this.$graph.on("plothover", this._toolTip); - return this; - }, +
    + +
        if (this.needToRedraw) {
    +      this.redraw();
    +    }
    +  },
    + +
  • + + +
  • +
    + +
    + +
    +

    infoboxes on mouse hover on points/bars etc

    - remove: function () { - this.editor.remove(); - Backbone.View.prototype.remove.apply(this, arguments); - }, +
    + +
      _toolTip: function (event, pos, item) {
    +    if (item) {
    +      if (this.previousTooltipPoint.x !== item.dataIndex ||
    +          this.previousTooltipPoint.y !== item.seriesIndex) {
    +        this.previousTooltipPoint.x = item.dataIndex;
    +        this.previousTooltipPoint.y = item.seriesIndex;
    +        $("#recline-flot-tooltip").remove();
     
    -  redraw: function() {
  • There are issues generating a Flot graph if either: -* The relevant div that graph attaches to his hidden at the moment of creating the plot -- Flot will complain with - Uncaught Invalid dimensions for plot, width = 0, height = 0 -* There is no data for the plot -- either same error or may have issues later with errors like 'non-existent node-value'

        var areWeVisible = !jQuery.expr.filters.hidden(this.el);
    -    if ((!areWeVisible || this.model.records.length === 0)) {
    -      this.needToRedraw = true;
    -      return;
    -    }

    check we have something to plot

        if (this.state.get('group') && this.state.get('series')) {
    -      var series = this.createSeries();
    -      var options = this.getGraphOptions(this.state.attributes.graphType, series[0].data.length);
    -      this.plot = $.plot(this.$graph, series, options);
    -    }
    -  },
    +        var x = item.datapoint[0].toFixed(2),
    +            y = item.datapoint[1].toFixed(2);
     
    -  show: function() {

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

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

    infoboxes on mouse hover on points/bars etc

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

    use a different tooltip location offset for bar charts

    - if (this.state.attributes.graphType === 'bars') { - x = item.datapoint[1].toFixed(2), - y = item.datapoint[0].toFixed(2); - } +
    + +
            var xLocation, yLocation;
    +        if (this.state.attributes.graphType === 'bars') {
    +          xLocation = item.pageX + 15;
    +          yLocation = item.pageY - 10;
    +        } else if (this.state.attributes.graphType === 'columns') {
    +          xLocation = item.pageX + 15;
    +          yLocation = item.pageY;
    +        } else {
    +          xLocation = item.pageX + 10;
    +          yLocation = item.pageY - 20;
    +        }
     
    -        var content = _.template('<%= group %> = <%= x %>, <%= series %> = <%= y %>', {
    -          group: this.state.attributes.group,
    -          x: this._xaxisLabel(x),
    -          series: item.series.label,
    -          y: y
    -        });
  • use a different tooltip location offset for bar charts

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

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

    - _xaxisLabel: function (x) { - if (this._groupFieldIsDateTime()) {
  • oddly x comes through as milliseconds string (rather than int -or float) so we have to reparse

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

    HACK: deal with bar graph style cases where x-axis items were strings + + +

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

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

  •       x = this.model.records.models[x].get(this.state.attributes.group);
    -    }
    +records not its actual x-axis value

    - return x; - },

    getGraphOptions

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

    getGraphOptions

    Get options for Flot Graph

    -

    needs to be function as can depend on state

    -

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

  •   getGraphOptions: function(typeId, numPoints) {
    -    var self = this;
    -    var groupFieldIsDateTime = self._groupFieldIsDateTime();
    -    var xaxis = {};
    +@param numPoints the number of points that will be plotted

    - if (!groupFieldIsDateTime) { - xaxis.tickFormatter = function (x) {

    convert x to a string and make sure that it is not too long or the + + +

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

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

  •         var label = self._xaxisLabel(x) || "";
    +TODO: find a more accurate way of calculating the size of tick labels

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

    for labels case we only want ticks at the label intervals + if (typeof label !== 'string') { + label = label.toString(); + } + if (self.state.attributes.graphType !== 'bars' && label.length > 10) { + label = label.slice(0, 10) + "..."; + } + + return label; + }; + } + + + + +

  • +
    + +
    + +
    +

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

  •     if (this.xvaluesAreIndex) {
    -      var numTicks = Math.min(this.model.records.length, 15);
    -      var increment = this.model.records.length / numTicks;
    -      var ticks = [];
    -      for (var i=0; i<numTicks; i++) {
    -        ticks.push(parseInt(i*increment, 10));
    -      }
    -      xaxis.ticks = ticks;
    -    } else if (groupFieldIsDateTime) {
    -      xaxis.mode = 'time';
    -    }
    +could have a lot of values and so we limit to max 15 (we assume)

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

    set up editor from state

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

    ensure at least one series box shows up

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

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

      _selectOption: function(id,value){
    -    var options = this.$el.find(id + ' select > option');
    -    if (options) {
    -      options.each(function(opt){
    -        if (this.value == value) {
    -          $(this).attr('selected','selected');
    -          return false;
    -        }
    -      });
    -    }
    -  },
    +  initialize: function(options) {
    +    var self = this;
    +    _.bindAll(this, 'render');
    +    this.listenTo(this.model.fields, 'reset add', this.render);
    +    this.state = new recline.Model.ObjectState(options.state);
    +    this.render();
    +  },
     
    -  onEditorSubmit: function(e) {
    -    var select = this.$el.find('.editor-group select');
    -    var $editor = this;
    -    var $series = this.$el.find('.editor-series select');
    -    var series = $series.map(function () {
    -      return $(this).val();
    -    });
    -    var updatedState = {
    -      series: $.makeArray(series),
    -      group: this.$el.find('.editor-group select').val(),
    -      graphType: this.$el.find('.editor-type select').val()
    -    };
    -    this.state.set(updatedState);
    -  },

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

    + render: function() { + var self = this; + var tmplData = this.model.toTemplateJSON(); + var htmls = Mustache.render(this.template, tmplData); + this.$el.html(htmls); + + + + +
  • +
    + +
    + +
    +

    set up editor from state

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

    ensure at least one series box shows up

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

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

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

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

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

    +

    Returns itself.

    -

    Returns itself.

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

    Public: Removes a series list item from the editor.

    + _onAddSeries: function(e) { + e.preventDefault(); + this.addSeries(this.state.get('series').length); + }, + + + + +
  • +
    + +
    + +
    +

    Public: Removes a series list item from the editor.

    +

    Also updates the labels of the remaining series elements.

    -

    Also updates the labels of the remaining series elements.

  •   removeSeries: function (e) {
    -    e.preventDefault();
    -    var $el = $(e.target);
    -    $el.parent().parent().remove();
    -    this.onEditorSubmit();
    -  }
    -});
    +            
    + +
      removeSeries: function (e) {
    +    e.preventDefault();
    +    var $el = $(e.target);
    +    $el.parent().parent().remove();
    +    this.onEditorSubmit();
    +  }
    +});
     
    -})(jQuery, recline.View);
    -
    -
    \ No newline at end of file +})(jQuery, recline.View);
    + + + + +
    + + diff --git a/docs/src/view.graph.html b/docs/src/view.graph.html index bf5b23a5..a6cbb03d 100644 --- a/docs/src/view.graph.html +++ b/docs/src/view.graph.html @@ -1,6 +1,141 @@ - view.graph.js \ No newline at end of file + + + view.graph.js + + + + + +
    +
    + + + +
      + +
    • +
      +

      view.graph.js

      +
      +
    • + + + +
    • +
      + +
      + +
      + +
      + +
      this.recline = this.recline || {};
      +this.recline.View = this.recline.View || {};
      +this.recline.View.Graph = this.recline.View.Flot;
      +this.recline.View.GraphControls = this.recline.View.FlotControls;
      + +
    • + +
    +
    + + diff --git a/docs/src/view.grid.html b/docs/src/view.grid.html index 7f1fc32d..09a91eca 100644 --- a/docs/src/view.grid.html +++ b/docs/src/view.grid.html @@ -1,232 +1,606 @@ - view.grid.js

    view.grid.js

    /*jshint multistr:true */
    +
     
    -this.recline = this.recline || {};
    -this.recline.View = this.recline.View || {};
    +
    +
    +  view.grid.js
    +  
    +  
    +  
    +
    +
    +  

    (Data) Grid Dataset View

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

    (Data) Grid Dataset View

    Provides a tabular view on a Dataset.

    +

    Initialize it with a recline.Model.Dataset.

    -

    Initialize it with a recline.Model.Dataset.

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

    does not work here so done at end of render function -'scroll .recline-grid tbody': 'onHorizontalScroll'

      },

    ====================================================== -Column and row menus

      setColumnSort: function(order) {
    -    var sort = [{}];
    -    sort[0][this.tempState.currentColumn] = {order: order};
    -    this.model.query({sort: sort});
    -  },
    +  events: {
    + + + + +
  • +
    + +
    + +
    +

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

    + +
    + +
      },
    + +
  • + + +
  • +
    + +
    + +
    +

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

    + +
    + +
  • + + +
  • +
    + +
    + +
    +

    Column and row menus

    + +
    + +
    +  setColumnSort: function(order) {
    +    var sort = [{}];
    +    sort[0][this.tempState.currentColumn] = {order: order};
    +    this.model.query({sort: sort});
    +  },
       
    -  hideColumn: function() {
    -    var hiddenFields = this.state.get('hiddenFields');
    -    hiddenFields.push(this.tempState.currentColumn);
    -    this.state.set({hiddenFields: hiddenFields});
  • change event not being triggered (because it is an array?) so trigger manually

        this.state.trigger('change');
    -    this.render();
    -  },
    +  hideColumn: function() {
    +    var hiddenFields = this.state.get('hiddenFields');
    +    hiddenFields.push(this.tempState.currentColumn);
    +    this.state.set({hiddenFields: hiddenFields});
    + + + + +
  • +
    + +
    + +
    +

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

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

    + onHorizontalScroll: function(e) { + var currentScroll = $(e.target).scrollLeft(); + this.$el.find('.recline-grid thead tr').scrollLeft(currentScroll); + }, + + + + +
  • +
    + +
    + +
    +

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

    -

    Templating

  •   template: ' \
    -    <div class="table-container"> \
    -    <table class="recline-grid table-striped table-condensed" cellspacing="0"> \
    -      <thead class="fixed-header"> \
    -        <tr> \
    -          {{#fields}} \
    -            <th class="column-header {{#hidden}}hidden{{/hidden}}" data-field="{{id}}" style="width: {{width}}px; max-width: {{width}}px; min-width: {{width}}px;" title="{{label}}"> \
    -              <span class="column-header-name">{{label}}</span> \
    -            </th> \
    -          {{/fields}} \
    -          <th class="last-header" style="width: {{lastHeaderWidth}}px; max-width: {{lastHeaderWidth}}px; min-width: {{lastHeaderWidth}}px; padding: 0; margin: 0;"></th> \
    -        </tr> \
    -      </thead> \
    -      <tbody class="scroll-content"></tbody> \
    -    </table> \
    -    </div> \
    -  ',
    +            
    + + + + +
  • +
    + +
    + +
    +

    Templating

    - toTemplateJSON: function() { - var self = this; - var modelData = this.model.toJSON(); - modelData.notEmpty = ( this.fields.length > 0 );
  • TODO: move this sort of thing into a toTemplateJSON method on Dataset?

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

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

        modelData.lastHeaderWidth = this.scrollbarDimensions.width - 2;
    -    return modelData;
    -  },
    -  render: function() {
    -    var self = this;
    -    this.fields = new recline.Model.FieldList(this.model.fields.filter(function(field) {
    -      return _.indexOf(self.state.get('hiddenFields'), field.id) == -1;
    -    }));
    +            
    + +
      template: ' \
    +    <div class="table-container"> \
    +    <table class="recline-grid table-striped table-condensed" cellspacing="0"> \
    +      <thead class="fixed-header"> \
    +        <tr> \
    +          {{#fields}} \
    +            <th class="column-header {{#hidden}}hidden{{/hidden}}" data-field="{{id}}" style="width: {{width}}px; max-width: {{width}}px; min-width: {{width}}px;" title="{{label}}"> \
    +              <span class="column-header-name">{{label}}</span> \
    +            </th> \
    +          {{/fields}} \
    +          <th class="last-header" style="width: {{lastHeaderWidth}}px; max-width: {{lastHeaderWidth}}px; min-width: {{lastHeaderWidth}}px; padding: 0; margin: 0;"></th> \
    +        </tr> \
    +      </thead> \
    +      <tbody class="scroll-content"></tbody> \
    +    </table> \
    +    </div> \
    +  ',
     
    -    this.scrollbarDimensions = this.scrollbarDimensions || this._scrollbarSize(); // skip measurement if already have dimensions
    -    var numFields = this.fields.length;

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

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

    if columns extend outside viewport then remainder is 0

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

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

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

    hide extra header col if no scrollbar to avoid unsightly overhang

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

    _scrollbarSize

    + toTemplateJSON: function() { + var self = this; + var modelData = this.model.toJSON(); + modelData.notEmpty = ( this.fields.length > 0 ); + + + + +
  • +
    + +
    + +
    +

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

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

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

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

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

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

    if columns extend outside viewport then remainder is 0

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

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

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

    hide extra header col if no scrollbar to avoid unsightly overhang

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

    _scrollbarSize

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

    +

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

    -

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

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

    GridRow View for rendering an individual record.

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

    GridRow View for rendering an individual record.

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

    -

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

    -

    Example:

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

    =================== -Cell Editor methods

      cellEditorTemplate: ' \
    -    <div class="menu-container data-table-cell-editor"> \
    -      <textarea class="data-table-cell-editor-editor" bind="textarea">{{value}}</textarea> \
    -      <div id="data-table-cell-editor-actions"> \
    -        <div class="data-table-cell-editor-action"> \
    -          <button class="okButton btn primary">Update</button> \
    -          <button class="cancelButton btn danger">Cancel</button> \
    -        </div> \
    -      </div> \
    -    </div> \
    -  ',
    +  render: function() {
    +    this.$el.attr('data-id', this.model.id);
    +    var html = Mustache.render(this.template, this.toTemplateJSON());
    +    this.$el.html(html);
    +    return this;
    +  },
    + + + + +
  • +
    + +
    + +
    +

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

    - onEditClick: function(e) { - var editing = this.$el.find('.data-table-cell-editor-editor'); - if (editing.length > 0) { - editing.parents('.data-table-cell-value').html(editing.text()).siblings('.data-table-cell-edit').removeClass("hidden"); - } - $(e.target).addClass("hidden"); - var cell = $(e.target).siblings('.data-table-cell-value'); - cell.data("previousContents", cell.text()); - var templated = Mustache.render(this.cellEditorTemplate, {value: cell.text()}); - cell.html(templated); - }, +
    + +
  • + + +
  • +
    + +
    + +
    +

    Cell Editor methods

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

    view.map.js

    /*jshint multistr:true */
    +
     
    -this.recline = this.recline || {};
    -this.recline.View = this.recline.View || {};
    +
    +
    +  view.map.js
    +  
    +  
    +  
    +
    +
    +  

    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:

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

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

    -

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

    -
       {
         // geomField if specified will be used in preference to lat/lon
    @@ -36,522 +172,1160 @@ have the following (optional) configuration options:

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

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

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

      latitudeFieldNames: ['lat','latitude'],
    -  longitudeFieldNames: ['lon','longitude'],
    -  geometryFieldNames: ['geojson', 'geom','the_geom','geometry','spatial','location', 'geo', 'lonlat'],
    +
     
    -  initialize: function(options) {
    -    var self = this;
    -    this.visible = 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 Functions

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

  • infobox

    +behaviour

    + + + + + +
  • +
    + +
    + +
    +

    infobox

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

    -

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

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

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

    - -

    e.g.

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

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

    +

    e.g.

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

    See defaults for examples

    -

    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 this will be set to point to this map view -instance (which allows e.g. this.markers to work in this default case)

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

    this is for cluster case

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

    onEachFeature default which adds popup in

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

    END: Customization section

    Public: Adds the necessary elements to the page.

    +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 provided

    Actions can be:

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

    try to set things up if not already

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

    removing ad re-adding the layer enables faster bulk loading

          this.map.removeLayer(this.features);
    -      this.map.removeLayer(this.markers);
    +            
    + +
      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 &copy; 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 &copy; 2011 OpenStreetMap contributors, Tiles Courtesy of <a href="http://www.mapquest.com/" target="_blank">MapQuest</a> <img src="http://developer.mapquest.com/content/osm/mq_logo.png">';
    +    var bg = new L.TileLayer(mapUrl, {maxZoom: 18, attribution: osmAttribution ,subdomains: '1234'});
    +    this.map.addLayer(bg);
     
    -    this.map.setView([0, 0], 2);
    +    this.markers = new L.MarkerClusterGroup(this._clusterOptions);
    + + + + +
  • +
    + +
    + +
    +

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

    - this.mapReady = true; - },
  • Private: Helper function to select an option from a select list

      _selectOption: function(id,value){
    -    var options = $('.' + id + ' > select > option');
    -    if (options){
    -      options.each(function(opt){
    -        if (this.value == value) {
    -          $(this).attr('selected','selected');
    -          return false;
    -        }
    -      });
    -    }
    -  }
    -});
    +            
    + +
        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;
    +        }
    +      });
    +    }
    +  }
    +});
     
    -

    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);
    +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')});
    +  },
     
    -
    \ No newline at end of file + onClusteringChange: function(e){ + this.state.set({cluster: !this.state.get('cluster')}); + },
    + + + + +
  • +
    + +
    + +
    +

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

    + +
    + +
      _selectOption: function(id,value){
    +    var options = this.$el.find('.' + id + ' > select > option');
    +    if (options){
    +      options.each(function(opt){
    +        if (this.value == value) {
    +          $(this).attr('selected','selected');
    +          return false;
    +        }
    +      });
    +    }
    +  }
    +});
    +
    +})(jQuery, recline.View);
    + +
  • + + + + + diff --git a/docs/src/view.multiview.html b/docs/src/view.multiview.html index a49cd9ab..070be0b1 100644 --- a/docs/src/view.multiview.html +++ b/docs/src/view.multiview.html @@ -1,13 +1,167 @@ - view.multiview.js
    Jump To …

    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

    + + + view.multiview.js + + + + + +
    +
    + + + +
      + +
    • +
      +

      view.multiview.js

      +
      +
    • + + + +
    • +
      + +
      + +
      + +
      + +
      /*jshint multistr:true */
      + +
    • + + +
    • +
      + +
      + +
      +

      Standard JS module setup

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

      MultiView

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

      -
      -var myExplorer = new model.recline.MultiView({
      +var myExplorer = new recline.View.MultiView({
         model: {{recline.Model.Dataset instance}}
         el: {{an existing dom element}}
         views: {{dataset views}}
      @@ -15,19 +169,15 @@ var myExplorer = new model.recline.MultiView({
       });
       
      -

      Parameters

      - +

      Parameters

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

      -

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

      -

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

      -
       var views = [
         {
      @@ -51,13 +201,12 @@ var views = [
       MultiView to show. This is an array of view hashes. If not provided
       initialize with (recline.View.)FilterEditor and Fields views (with obvious 
       id and labels!).

      -
       var sidebarViews = [
         {
           id: 'filterEditor', // used for routing
           label: 'Filters', // used for view switcher
      -    view: new recline.View.FielterEditor({
      +    view: new recline.View.FilterEditor({
             model: dataset
           })
         },
      @@ -73,12 +222,11 @@ var sidebarViews = [
       
       

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

      -
      -state = {
      +var state = {
           query: {dataset query state - see dataset.queryState object}
      -    view-{id1}: {view-state for this view}
      -    view-{id2}: {view-state for }
      +    'view-{id1}': {view-state for this view}
      +    'view-{id2}': {view-state for }
           ...
           // Explorer
           currentView: id of current view (defaults to first view if not specified)
      @@ -87,404 +235,814 @@ state = {
       

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

    my.MultiView = Backbone.View.extend({
    -  template: ' \
    -  <div class="recline-data-explorer"> \
    -    <div class="alert-messages"></div> \
    -    \
    -    <div class="header clearfix"> \
    -      <div class="navigation"> \
    -        <div class="btn-group" data-toggle="buttons-radio"> \
    -        {{#views}} \
    -        <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);
    +      });
    +    });
    +  },
    + +
  • + + +
  • +
    + +
    + +
    +

    notify

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

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

    clearNotifications

    + -

    Clear all existing notifications

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

    MultiView.restore

    + + +
      notify: function(flash) {
    +    var tmplData = _.extend({
    +      message: 'Loading',
    +      category: 'warning',
    +      loader: false
    +      },
    +      flash
    +    );
    +    var _template;
    +    if (tmplData.loader) {
    +      _template = ' \
    +        <div class="alert alert-info alert-loader"> \
    +          {{message}} \
    +          <span class="notification-loader">&nbsp;</span> \
    +        </div>';
    +    } else {
    +      _template = ' \
    +        <div class="alert alert-{{category}} fade in" data-alert="alert"><a class="close" data-dismiss="alert" href="#">×</a> \
    +          {{message}} \
    +        </div>';
    +    }
    +    var _templated = $(Mustache.render(_template, tmplData));
    +    _templated = $(_templated).appendTo($('.recline-data-explorer .alert-messages'));
    +    if (!flash.persist) {
    +      setTimeout(function() {
    +        $(_templated).fadeOut(1000, function() {
    +          $(this).remove();
    +        });
    +      }, 1000);
    +    }
    +  },
    + + + + +
  • +
    + +
    + +
    +

    clearNotifications

    +

    Clear all existing notifications

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

    MultiView.restore

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

    +

    This inverts the state serialization process in Multiview

    -

    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] || ''
    +    };
    +  }
    +};
    + +
  • + + +
  • +
    + +
    + +
    +

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

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

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

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

    Parse the query string out of the URL hash

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

    Compse a Query String

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

    slice(1) to remove # at start

    + +
    + +
        return window.location.hash.split('?')[0].slice(1) + queryPart;
    +  } else {
    +    return queryPart;
    +  }
    +};
    +
    +my.setHashQueryString = function(queryParams) {
    +  window.location.hash = my.getNewHashForQueryString(queryParams);
    +};
    +
    +})(jQuery, recline.View);
    + +
  • + + + + + diff --git a/docs/src/view.slickgrid.html b/docs/src/view.slickgrid.html index 110e8ea5..80bb6006 100644 --- a/docs/src/view.slickgrid.html +++ b/docs/src/view.slickgrid.html @@ -1,48 +1,160 @@ - view.slickgrid.js
    Jump To …

    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>',
    +
    +
    +  view.slickgrid.js
    +  
    +  
    +  
    +
    +
    +  
    +
    - initialize: function(options){ - var self = this; - _.bindAll(this, 'render'); - this.state = new recline.Model.ObjectState(); - this.render(); - }, + + +
      + +
    • +
      +

      view.slickgrid.js

      +
      +
    • + + + +
    • +
      + +
      + +
      + +
      + +
      /*jshint multistr:true */
       
      -    render: function() {
      -      var self = this;
      -      this.$el.html(this.template)
      -    },
      -
      -    events : {
      -      "click .recline-row-add" : "addNewRow"
      -    },
      -
      -    addNewRow : function(e){
      -      e.preventDefault()
      -      this.state.trigger("change")
      -   }
      - }
      - );

    SlickGrid Dataset View

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

    SlickGrid Dataset View

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

    - -

    https://github.com/mleibman/SlickGrid

    - +

    https://github.com/mleibman/SlickGrid

    Initialize it with a recline.Model.Dataset.

    -

    Additional options to drive SlickGrid grid can be given through state. -The following keys allow for customization: -* gridOptions: to add options at grid level -* columnsEditor: to add editor for editable columns

    - +The following keys allow for customization:

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

    For example: var grid = new recline.View.SlickGrid({ model: dataset, @@ -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){
    + +
  • + + +
  • +
    + +
    + +
    +

    We need to delete the associated model

    - show: function() {
  • If the div is hidden, SlickGrid will calculate wrongly some -sizes so we must render it explicitly when the view is visible

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

    Slick.Controls.ColumnPicker

    + +
    + +
      $.extend(true, window, {
    +    Slick: {
    +      Controls: {
    +        ColumnPicker: SlickColumnPicker
    +      }
    +    }
    +  });
    +
    +})(jQuery);
    + +
  • + + + + + diff --git a/docs/src/view.timeline.html b/docs/src/view.timeline.html index f86f6c45..5a7551f8 100644 --- a/docs/src/view.timeline.html +++ b/docs/src/view.timeline.html @@ -1,162 +1,446 @@ - view.timeline.js
    Jump To …

    view.timeline.js

    /*jshint multistr:true */
    +
     
    -this.recline = this.recline || {};
    -this.recline.View = this.recline.View || {};
    +
    +
    +  view.timeline.js
    +  
    +  
    +  
    +
    +
    +  

    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();
    +  },
     
    -

    Designed to be overridden in client apps

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

    Internal method to generate a Timeline formatted entry

      _convertRecord: function(record, fields) {
    -    var start = this._parseDate(record.get(this.state.get('startField')));
    -    var end = this._parseDate(record.get(this.state.get('endField')));
    -    if (start) {
    -      var tlEntry = {
    -        "startDate": start,
    -        "endDate": end,
    -        "headline": String(record.get('title') || ''),
    -        "text": record.get('description') || record.summary()
    -      };
    -      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);
    +    }
    +  },
    + +
  • + + +
  • +
    + +
    + +
    +

    Convert record to JSON for timeline

    +

    Designed to be overridden in client apps

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

    Internal method to generate a Timeline formatted entry

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

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

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

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

  •   _parseDate: function(date) {
    -    if (!date) {
    -      return null;
    -    }
    -    var out = $.trim(date);
    -    out = out.replace(/(\d)th/g, '$1');
    -    out = out.replace(/(\d)st/g, '$1');
    -    out = $.trim(out);
    -    if (out.match(/\d\d\d\d-\d\d-\d\d(T.*)?/)) {
    -      out = out.replace(/-/g, ',').replace('T', ',').replace(':',',');
    -    }
    -    if (out.match(/\d\d-\d\d-\d\d.*/)) {
    -      out = out.replace(/-/g, '/');
    -    }
    -    if (this.state.get('nonUSDates')) {
    -      var parts = out.match(/(\d\d)\/(\d\d)\/(\d\d.*)/);
    -      if (parts) {
    -        out = [parts[2], parts[1], parts[3]].join('/');
    -      }
    -    }
    -    return out;
    -  },
    +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;
    +  }
    +});
     
    -
    \ No newline at end of file +})(jQuery, recline.View); + + + + + + + diff --git a/docs/src/widget.facetviewer.html b/docs/src/widget.facetviewer.html index ddc3ab72..d2523dc3 100644 --- a/docs/src/widget.facetviewer.html +++ b/docs/src/widget.facetviewer.html @@ -1,83 +1,259 @@ - widget.facetviewer.js
    Jump To …

    widget.facetviewer.js

    /*jshint multistr:true */
    +
     
    -this.recline = this.recline || {};
    -this.recline.View = this.recline.View || {};
    +
    +
    +  widget.facetviewer.js
    +  
    +  
    +  
    +
    +
    +  

    FacetViewer

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

    FacetViewer

    Widget for displaying facets

    -

    Usage:

    - -
     var viewer = new FacetViewer({
    +
     var viewer = new FacetViewer({
        model: dataset
      });
    -
  • my.FacetViewer = Backbone.View.extend({
    -  className: 'recline-facet-viewer', 
    -  template: ' \
    -    <div class="facets"> \
    -      {{#facets}} \
    -      <div class="facet-summary" data-facet="{{id}}"> \
    -        <h3> \
    -          {{id}} \
    -        </h3> \
    -        <ul class="facet-items"> \
    -        {{#terms}} \
    -          <li><a class="facet-choice js-facet-filter" data-value="{{term}}" href="#{{term}}">{{term}} ({{count}})</a></li> \
    -        {{/terms}} \
    -        {{#entries}} \
    -          <li><a class="facet-choice js-facet-filter" data-value="{{time}}">{{term}} ({{count}})</a></li> \
    -        {{/entries}} \
    -        </ul> \
    -      </div> \
    -      {{/facets}} \
    -    </div> \
    -  ',
    +
    +
    + +
    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);
    -
    -
  • \ No newline at end of file +})(jQuery, recline.View); + + + + + + + diff --git a/docs/src/widget.fields.html b/docs/src/widget.fields.html index d4fe2282..07b34852 100644 --- a/docs/src/widget.fields.html +++ b/docs/src/widget.fields.html @@ -1,82 +1,299 @@ - widget.fields.js
    Jump To …

    widget.fields.js

    /*jshint multistr:true */

    Field Info

    + + + + widget.fields.js + + + + + +
    +
    + + + +
      + +
    • +
      +

      widget.fields.js

      +
      +
    • + + + +
    • +
      + +
      + +
      + +
      + +
      /*jshint multistr:true */
      + +
    • + + +
    • +
      + +
      + +
      +

      Field Info

      For each field

      +

      Id / Label / type / format

      -

      Id / Label / type / format

    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 …

    +
    + +
  • + + +
  • +
    + +
    + +
    +

    Summaries of fields

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

  • Box to boot transform editor ...

    this.recline = this.recline || {};
    -this.recline.View = this.recline.View || {};
    +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}}"> &raquo; </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}}"> &raquo; </a> \
    +            </small> \
    +          </h4> \
    +        </div> \
    +        <div id="collapse{{id}}" class="panel-collapse collapse"> \
    +          <div class="panel-body"> \
    +            {{#facets}} \
    +            <div class="facet-summary" data-facet="{{id}}"> \
    +              <ul class="facet-items"> \
    +              {{#terms}} \
    +                <li class="facet-item"><span class="term">{{term}}</span> <span class="count">[{{count}}]</span></li> \
    +              {{/terms}} \
    +              </ul> \
    +            </div> \
    +            {{/facets}} \
    +            <div class="clear"></div> \
    +          </div> \
    +        </div> \
    +      </div> \
    +    {{/fields}} \
    +    </div> \
    +  ',
     
    -  initialize: function(model) {
    -    var self = this;
    -    _.bindAll(this, 'render');
  • TODO: this is quite restrictive in terms of when it is re-run + 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

    -
  • \ No newline at end of file + + +
          self.model.getFieldsSummary();
    +      self.render();
    +    });
    +    this.$el.find('.collapse').collapse();
    +    this.render();
    +  },
    +  render: function() {
    +    var self = this;
    +    var tmplData = {
    +      fields: []
    +    };
    +    this.model.fields.each(function(field) {
    +      var out = field.toJSON();
    +      out.facets = field.facets.toJSON();
    +      tmplData.fields.push(out);
    +    });
    +    var templated = Mustache.render(this.template, tmplData);
    +    this.$el.html(templated);
    +  }
    +});
    +
    +})(jQuery, recline.View);
    + + + + + + + diff --git a/docs/src/widget.filtereditor.html b/docs/src/widget.filtereditor.html index d4bf97bb..a761e3d7 100644 --- a/docs/src/widget.filtereditor.html +++ b/docs/src/widget.filtereditor.html @@ -1,168 +1,330 @@ - widget.filtereditor.js
    Jump To …

    widget.filtereditor.js

    /*jshint multistr:true */
    +
     
    -this.recline = this.recline || {};
    -this.recline.View = this.recline.View || {};
    +
    +
    +  widget.filtereditor.js
    +  
    +  
    +  
    +
    +
    +  
    +
    + + + +
      + +
    • +
      +

      widget.filtereditor.js

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

    we will use idx in list as there id …

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

    widget.pager.js

    /*jshint multistr:true */
    +
     
    -this.recline = this.recline || {};
    -this.recline.View = this.recline.View || {};
    +
    +
    +  widget.pager.js
    +  
    +  
    +  
    +
    +
    +  
    +
    + + + +
      + +
    • +
      +

      widget.pager.js

      +
      +
    • + + + +
    • +
      + +
      + +
      + +
      + +
      /*jshint multistr:true */
       
      -(function($, my) {
      -  "use strict";
      +this.recline = this.recline || {};
      +this.recline.View = this.recline.View || {};
       
      -my.Pager = Backbone.View.extend({
      -  className: 'recline-pager', 
      -  template: ' \
      -    <div class="pagination"> \
      -      <ul> \
      -        <li class="prev action-pagination-update"><a href="">&laquo;</a></li> \
      -        <li class="active"><a><input name="from" type="text" value="{{from}}" /> &ndash; <input name="to" type="text" value="{{to}}" /> </a></li> \
      -        <li class="next action-pagination-update"><a href="">&raquo;</a></li> \
      -      </ul> \
      -    </div> \
      -  ',
      +(function($, my) {
      +  "use strict";
       
      -  events: {
      -    'click .action-pagination-update': 'onPaginationUpdate',
      -    'change input': 'onFormSubmit'
      -  },
      +my.Pager = Backbone.View.extend({
      +  className: 'recline-pager', 
      +  template: ' \
      +    <div class="pagination"> \
      +      <ul class="pagination"> \
      +        <li class="prev action-pagination-update"><a href="" class="btn btn-default">&laquo;</a></li> \
      +        <li class="page-range"><a><label for="from">From</label><input name="from" type="text" value="{{from}}" /> &ndash; <label for="to">To</label><input name="to" type="text" value="{{to}}" /> </a></li> \
      +        <li class="next action-pagination-update"><a href="" class="btn btn-default">&raquo;</a></li> \
      +      </ul> \
      +    </div> \
      +  ',
       
      -  initialize: function() {
      -    _.bindAll(this, 'render');
      -    this.listenTo(this.model.queryState, 'change', this.render);
      -    this.render();
      -  },
      -  onFormSubmit: function(e) {
      -    e.preventDefault();
      -    var newFrom = parseInt(this.$el.find('input[name="from"]').val());
      -    newFrom = Math.min(this.model.recordCount, Math.max(newFrom, 1))-1;
      -    var newSize = parseInt(this.$el.find('input[name="to"]').val()) - newFrom;
      -    newSize = Math.min(Math.max(newSize, 1), this.model.recordCount);
      -    this.model.queryState.set({size: newSize, from: newFrom});
      -  },
      -  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), 1)-1;
      -      updateQuery = newFrom != currFrom;
      -    } else {
      -      newFrom = Math.max(currFrom + size, 1);
      -      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;
      -  }
      -});
      +  events: {
      +    'click .action-pagination-update': 'onPaginationUpdate',
      +    'change input': 'onFormSubmit'
      +  },
       
      -})(jQuery, recline.View);
      +  initialize: function() {
      +    _.bindAll(this, 'render');
      +    this.listenTo(this.model.queryState, 'change', this.render);
      +    this.render();
      +  },
      +  onFormSubmit: function(e) {
      +    e.preventDefault();
      + +
    • + + +
    • +
      + +
      + +
      +

      filter is 0-based; form is 1-based

      -
    \ No newline at end of file + + +
        var formFrom = parseInt(this.$el.find('input[name="from"]').val())-1; 
    +    var formTo = parseInt(this.$el.find('input[name="to"]').val())-1; 
    +    var maxRecord = this.model.recordCount-1;
    +    if (this.model.queryState.get('from') != formFrom) { // changed from; update from
    +      this.model.queryState.set({from: Math.min(maxRecord, Math.max(formFrom, 0))});
    +    } else if (this.model.queryState.get('to') != formTo) { // change to; update size
    +      var to = Math.min(maxRecord, Math.max(formTo, 0));
    +      this.model.queryState.set({size: Math.min(maxRecord+1, Math.max(to-formFrom+1, 1))});
    +    }
    +  },
    +  onPaginationUpdate: function(e) {
    +    e.preventDefault();
    +    var $el = $(e.target);
    +    var newFrom = 0;
    +    var currFrom = this.model.queryState.get('from');
    +    var size = this.model.queryState.get('size');
    +    var updateQuery = false;
    +    if ($el.parent().hasClass('prev')) {
    +      newFrom = Math.max(currFrom - Math.max(0, size), 0);
    +      updateQuery = newFrom != currFrom;
    +    } else {
    +      newFrom = Math.max(currFrom + size, 0);
    +      updateQuery = (newFrom < this.model.recordCount);
    +    }
    +    if (updateQuery) {
    +      this.model.queryState.set({from: newFrom});
    +    }
    +  },
    +  render: function() {
    +    var tmplData = this.model.toJSON();
    +    var from = parseInt(this.model.queryState.get('from'));
    +    tmplData.from = from+1;
    +    tmplData.to = Math.min(from+this.model.queryState.get('size'), this.model.recordCount);
    +    var templated = Mustache.render(this.template, tmplData);
    +    this.$el.html(templated);
    +    return this;
    +  }
    +});
    +
    +})(jQuery, recline.View);
    + + + + + + + diff --git a/docs/src/widget.queryeditor.html b/docs/src/widget.queryeditor.html index 79262cf0..37a42d30 100644 --- a/docs/src/widget.queryeditor.html +++ b/docs/src/widget.queryeditor.html @@ -1,44 +1,184 @@ - widget.queryeditor.js
    Jump To …

    widget.queryeditor.js

    /*jshint multistr:true */
    +
     
    -this.recline = this.recline || {};
    -this.recline.View = this.recline.View || {};
    +
    +
    +  widget.queryeditor.js
    +  
    +  
    +  
    +
    +
    +  
    +
    + + + +
      + +
    • +
      +

      widget.queryeditor.js

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