├── .gitignore ├── filter.css ├── package.js ├── filter.html ├── README.md └── filter.js /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .versions 3 | -------------------------------------------------------------------------------- /filter.css: -------------------------------------------------------------------------------- 1 | .filter_flex{ 2 | display: -webkit-box; /* OLD - iOS 6-, Safari 3.1-6 */ 3 | display: -moz-box; /* OLD - Firefox 19- (buggy but mostly works) */ 4 | display: -ms-flexbox; /* TWEENER - IE 10 */ 5 | display: -webkit-flex; /* NEW - Chrome */ 6 | display: flex; 7 | padding-bottom: 20px; 8 | } 9 | .btn .caret { 10 | margin-left: 10px; 11 | } -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: "loredanacirstea:meteor-tabular-filter", 3 | summary: "Collection filter based on aldeed:simple-schema structure.Output: Mongo Selector.", 4 | version: "0.2.2", 5 | git: "https://github.com/loredanacirstea/meteor-tabular-filter.git" 6 | }); 7 | 8 | Package.onUse(function(api) { 9 | api.versionsFrom('1.2.1'); 10 | 11 | api.use(['jquery', 'deps', 'templating', 'ui', 'blaze', 'reactive-var', 'session'], 'client'); 12 | 13 | api.addFiles([ 14 | 'filter.html', 15 | 'filter.js', 16 | 'filter.css' 17 | ], 'client'); 18 | }); 19 | -------------------------------------------------------------------------------- /filter.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 13 | 17 | 18 | 34 | 35 | 38 | 39 | 52 | 53 | 56 | 57 | 64 | 65 | 68 | 69 | 72 | 73 | 76 | 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # meteor-tabular-filter 2 | 3 | https://atmospherejs.com/loredanacirstea/meteor-tabular-filter 4 | 5 | 6 | It only works with bootstrap now, so you will have to add: 7 | 8 | meteor add twbs:bootstrap 9 | meteor add loredanacirstea:meteor-tabular-filter 10 | 11 | 12 | Recommended only for personal use: making quick querries on your data. For this, see demo at http://simple-filter.meteor.com , with source at: https://github.com/loredanacirstea/simple-filter. 13 | 14 | This package is not actively maintained as a default, but I will make small changes if there are easy to fix issues. 15 | 16 | ## Version 17 | 18 | 0.2.2 19 | 20 | - added property exactMatch which means show only = in filter dropdown 21 | 22 | 0.2.1 - Changes: 23 | 24 | You can have custom labels, so `Session.set("schema", "Table");` is now: 25 | 26 | ``` 27 | Session.set("tabular-filter", { 28 | schema: "Table", // the only one required 29 | label: "", 30 | and_label: "", 31 | or_label: "", 32 | input_value_placeholder: "", 33 | regex_value: "", 34 | contains_value: "" 35 | }); 36 | ``` 37 | 38 | You can use i18n and do: `label: i18n('On Field')` 39 | 40 | 41 | 42 | 43 | ## Description 44 | 45 | This package offers a filter which gets data from an [aldeed:simple-schema](https://github.com/aldeed/meteor-simple-schema) object (collection structure). It outputs a `Mongo Selector` based on the chosen options from the `meteor-tabular-filter` UI. It is compatible with [aldeed:tabular](https://github.com/aldeed/meteor-tabular)'s `selector`. 46 | 47 | Basically, meteor-tabular-filter just creates a filter UI from the colection structure and outputs a `Mongo Selector` (such as `{ $and: [ { priority: "medium" }, { state: true } ] }`) after the user chooses the options. 48 | 49 | ## What to expect 50 | 51 | The `AND` / `OR` operations are done in the added order. Example: 52 | 53 | ![](https://raw.githubusercontent.com/loredanacirstea/simple-filter/master/public/simple-filter.png) 54 | 55 | Priority = medium AND Repetitions > 4 OR Deadline > 01/17/2015 AND State = true 56 | 57 | will compute the first AND (Priority AND Repetitions), use the result with OR (result1 OR Deadline) and finally use the result with the second AND (result2 AND State). 58 | 59 | ## Bugs 60 | 61 | When getting an unwanted result (ex. when deleting the first filter), refresh the page. 62 | 63 | 64 | ## How to Use 65 | 66 | (see examples/simple-filter) 67 | 68 | Set a Session variable with the name of your Simple Schema object. 69 | 70 | //examples/simple-filter/client/client.js 71 | 72 | Template.view_table.rendered = function(){ 73 | Session.set("tabular-filter", {schema: "Table", label: 'On Field'}); 74 | } 75 | 76 | Set a template helper (or another method) from the filter's `filter_selector` Session variable. 77 | 78 | //examples/simple-filter/client/client.js 79 | 80 | Template.view_table.helpers({ 81 | selector: function (){ 82 | var select = Session.get("filter_selector"); 83 | return select; 84 | } 85 | }); 86 | 87 | The Simple Schema object should have the form: 88 | 89 | //examples/simple-filter/lib/schema.js 90 | 91 | Schemas = {}; 92 | Schemas.Table = new SimpleSchema({}); 93 | 94 | 95 | Add the `{{> expression_filter}}` template to your .html file 96 | 97 | //examples/simple-filter/client/client.html 98 | 99 |
100 | {{> expression_filter}} 101 | {{> view_table}} 102 |
103 | 104 | The `selector` template helper can be used with [aldeed:meteor-tabular](https://github.com/aldeed/meteor-tabular): 105 | 106 | //examples/simple-filter/client/client.html 107 | 108 | 113 | 114 | ## Omit Schema Fields 115 | 116 | If you have schema fields that you don't want to include, [extend SimpleSchema](https://github.com/aldeed/meteor-simple-schema#extending-the-schema-options) with: 117 | 118 | SimpleSchema.extendOptions({ 119 | tabularFilterOmit: Match.Optional(Boolean) 120 | }); 121 | 122 | and add `tabularFilterOmit: true` to your field, like this: 123 | 124 | deadline: { 125 | type: Date, 126 | label: "Deadline", 127 | tabularFilterOmit: true 128 | }, 129 | 130 | By [Loretek](http://loretek.ro) 131 | -------------------------------------------------------------------------------- /filter.js: -------------------------------------------------------------------------------- 1 | if(Meteor.isClient){ 2 | 3 | Session.set("filter_counter", [1]); 4 | Session.set("filter_selector", ''); 5 | Session.set("filter_queries", {}); 6 | Session.set("filter_ops", {}); 7 | 8 | 9 | var object_values = function(obj){ 10 | var values = []; 11 | var keys = Object.keys(obj); 12 | for(var i in keys){ 13 | values.push(obj[keys[i]]); 14 | } 15 | return values; 16 | } 17 | 18 | dropdown_select = function(elem, htmlid){ 19 | $("#"+htmlid + ":first-child").text(elem.label).append(''); 20 | $("#"+htmlid + ":first-child").val(elem.value); 21 | } 22 | 23 | create_selector = function(field, operator, value, type, options){ 24 | var select = {}; 25 | switch(operator){ 26 | case "=": 27 | if(type == "Number") 28 | value = Number(value); 29 | if(type == "Date") 30 | value = new Date(value); 31 | if(type == "Boolean") 32 | value = Boolean(value); 33 | select[field] = value; 34 | return select; 35 | break; 36 | case ">": 37 | if(type == "Number") 38 | value = Number(value); 39 | if(type == "Date") 40 | value = new Date(value); 41 | select[field] = {$gt: value}; 42 | return select; 43 | break; 44 | case "<": 45 | if(type == "Number") 46 | value = Number(value); 47 | if(type == "Date") 48 | value = new Date(value); 49 | select[field] = {$lt: value}; 50 | return select; 51 | break; 52 | case containsValue: 53 | select[field] = {$regex: value, $options: 'i'}; 54 | return select; 55 | break; 56 | case regExpValue: 57 | select[field] = {$regex: value, $options: options}; 58 | return select; 59 | break; 60 | /* 61 | case "has_key": 62 | select["$where"] = function() { return this.object.attribute }; 63 | return select; 64 | break; 65 | case "has_value": 66 | select["$where"] = function() { return this.object[Object.keys(this.object)[0]] == "complex"}; 67 | return select; 68 | break; 69 | */ 70 | default: 71 | return {}; 72 | } 73 | } 74 | 75 | Template.expression_filter.rendered = function(){ 76 | var no = Session.get("filter_counter"); 77 | no = no[no.length-1]; 78 | Blaze.renderWithData(Template.simple_filter, {"no": no}, document.getElementById('expression_filter')); 79 | } 80 | 81 | Template.filter_fields.helpers({ 82 | title: function() { 83 | var sess = Session.get("tabular-filter") 84 | if(sess && (sess.label || sess.label == '')) 85 | return sess.label 86 | return 'On Field' 87 | }, 88 | fields: function(){ 89 | var input = Session.get("tabular-filter") 90 | if(!input) 91 | input = Session.get("schema") 92 | if(input){ 93 | if(typeof input == 'object') 94 | var schema = input.schema 95 | else 96 | var schema = input 97 | var fieldObj = Schemas[schema].schema(); 98 | var keys = Object.keys(fieldObj); 99 | var fieldArr = [], ind; 100 | for(k in keys){ 101 | if(!fieldObj[keys[k]].tabularFilterOmit && keys[k].substring([keys[k].length-2]) != '.$') { 102 | ind = keys[k].indexOf('.$.') 103 | if(ind !== -1) 104 | fieldObj[keys[k]].value = keys[k].substring(0,ind) + keys[k].substring(ind+2) 105 | else 106 | fieldObj[keys[k]].value = keys[k]; 107 | fieldArr.push(fieldObj[keys[k]]); 108 | } 109 | } 110 | return fieldArr; 111 | } 112 | } 113 | }); 114 | 115 | Template.filter_fields.onCreated(function(){ 116 | if ((ref = Session.get('tabular-filter')) != null ? ref.regex_value : void 0){ 117 | regExpValue = Session.get('tabular-filter').regex_value 118 | } 119 | else{ 120 | regExpValue = "regex" 121 | } 122 | if ((ref = Session.get('tabular-filter')) != null ? ref.contains_value : void 0){ 123 | containsValue = Session.get('tabular-filter').contains_value 124 | } 125 | else{ 126 | containsValue = "contains" 127 | } 128 | operators = { 129 | "StringExact":[ 130 | "=" 131 | ], 132 | "String":[ 133 | "=", 134 | containsValue, 135 | regExpValue 136 | ], 137 | "Number":[ 138 | "=", 139 | ">", 140 | "<" 141 | ], 142 | "Boolean": [ 143 | "=" 144 | ], 145 | "Date": [ 146 | "=", 147 | "<", 148 | ">" 149 | ], 150 | "Object": [ 151 | "has value", 152 | "has key" 153 | ] 154 | } 155 | //filter_operators = ["true", "false", "AND", "OR", "FieldExpr"]; 156 | var andLabel, orLabel; 157 | if ((ref = Session.get('tabular-filter')) != null ? ref.and_label : void 0){ 158 | andLabel = Session.get('tabular-filter').and_label 159 | } 160 | else{ 161 | andLabel = 'AND' 162 | } 163 | 164 | if ((ref = Session.get('tabular-filter')) != null ? ref.or_label : void 0){ 165 | orLabel = Session.get('tabular-filter').or_label 166 | } 167 | else{ 168 | orLabel = 'OR' 169 | } 170 | 171 | filter_operators = [{key: "AND", label: andLabel}, {key: "OR", label: orLabel}]; 172 | }); 173 | 174 | Template.filter_fields.events({ 175 | 'click .dropdown-menu li a': function(event){ 176 | var no = Session.get("filter_counter"); 177 | no = no[no.length-1]; 178 | 179 | if($(event.target.parentElement.parentElement).attr("aria-labelledby") == "field" + no ){ 180 | if(!$('#field_button').length) 181 | Blaze.render(Template.button_locations, document.getElementById('simple_filter' + no )); 182 | Session.set("autoform_options","{}"); 183 | dropdown_select($(this)[0], "field" + no); 184 | var type = $(this)[0].type.name; 185 | 186 | if ($(this)[0].exactMatch){ 187 | type = 'StringExact'; 188 | } 189 | 190 | $("#field" + no + ":first-child").attr("field_type", type); 191 | $('#field_operations' + no ).html(''); 192 | Blaze.renderWithData(Template.filter_operations, {first: operators[type][0], all: operators[type], no: no}, document.getElementById('field_operations' + no )); 193 | $('#field_value' + no ).html(''); 194 | $("#operator" + no + ":first-child").val(operators[type][0]); 195 | if($(this)[0].allowedValues){ 196 | Blaze.renderWithData(Template.filter_value_select, {options: $(this)[0].allowedValues, no: no}, document.getElementById('field_value' + no )); 197 | } 198 | else 199 | if(type == "Date") 200 | Blaze.renderWithData(Template.filter_value_datepicker, {no: no}, document.getElementById('field_value' + no )); 201 | else 202 | if($(this)[0].autoform && $(this)[0].autoform.options){ 203 | var options = $(this)[0].autoform.options(); 204 | var values = []; 205 | var map = {}; 206 | for(o in options){ 207 | values.push(options[o].label); 208 | map[options[o].label] = options[o].value; 209 | } 210 | Session.set("autoform_options", JSON.stringify(map)); 211 | Blaze.renderWithData(Template.filter_value_select, {options: values, no: no}, document.getElementById('field_value' + no )); 212 | } 213 | else 214 | Blaze.renderWithData(Template.filter_value_input, {no:no}, document.getElementById('field_value' + no )); 215 | 216 | if($('#filter_operators' + no ).html() == '') 217 | Blaze.renderWithData(Template.filter_operators, {operators: filter_operators, no:no}, document.getElementById('filter_operators' + no )); 218 | if($('#filter_button_delete' + no ).html() == '') 219 | Blaze.renderWithData(Template.filter_button_delete, {no: no}, document.getElementById('filter_button_delete' + no)); 220 | if($('#field_button_reset').html() == '') 221 | Blaze.render(Template.filter_button_reset, document.getElementById('field_button_reset')); 222 | if($('#field_button').html() == '') 223 | Blaze.render(Template.filter_button, document.getElementById('field_button')); 224 | } 225 | } 226 | }); 227 | 228 | Template.filter_operations.events({ 229 | 'click .dropdown-menu li a': function(event){ 230 | var no = Session.get("filter_counter"); 231 | no = no[no.length-1]; 232 | if($(event.target.parentElement.parentElement).attr("aria-labelledby") == "operator" + no ){ 233 | var val = event.target.textContent; 234 | dropdown_select({"label": val, "value": val}, "operator" + no); 235 | } 236 | } 237 | }); 238 | 239 | op_val = function(op){ 240 | switch(op){ 241 | case "AND": 242 | return "$and"; 243 | break; 244 | case "OR": 245 | return "$or"; 246 | break; 247 | default: 248 | return "$and"; 249 | } 250 | } 251 | 252 | create_filter_selector = function(q, op, no){ 253 | var operator = op_val(op[no]); 254 | 255 | var result = {}; 256 | if(no > 0){ 257 | var query1 = create_filter_selector(q, op, no-1); 258 | result[operator] = [ query1 , q[no+1] ]; 259 | } 260 | else 261 | result[operator] = [ q[no], q[no+1] ] ; 262 | 263 | return result 264 | } 265 | 266 | set_filter_selector = function(no){ 267 | if($('#filter_value' + no ).val()){ 268 | if(Session.get("autoform_options") && Session.get("autoform_options") != "{}"){ 269 | var map = Session.get("autoform_options"); 270 | map = JSON.parse(map); 271 | var value = map[$('#filter_value' + no ).val()]; 272 | } 273 | else 274 | var value = $('#filter_value' + no ).val(); 275 | var field = $("#field" + no + ":first-child").val(); 276 | var op = $("#operator" + no + ":first-child").val(); 277 | var type = $("#field" + no + ":first-child").attr("field_type"); 278 | var selector = create_selector(field, op, value, type); 279 | var queries = Session.get("filter_queries"); 280 | //queries.push(selector); 281 | queries[no] = selector; 282 | Session.set("filter_queries", queries); 283 | } 284 | } 285 | 286 | Template.filter_button.events({ 287 | 'click #call_filter': function(){ 288 | 289 | var no = Session.get("filter_counter"); 290 | no = no[no.length-1]; 291 | set_filter_selector(no); 292 | 293 | if(Object.keys(Session.get("filter_queries")).length == 1){ 294 | Session.set("filter_selector", Session.get("filter_queries")[no]); 295 | } 296 | else{ 297 | var ops = object_values(Session.get("filter_ops")); 298 | Session.set("filter_selector", create_filter_selector(object_values(Session.get("filter_queries")), ops, ops.length-1)); 299 | } 300 | } 301 | }); 302 | 303 | Template.filter_button_delete.events({ 304 | 'click .delete_filter': function(event){ 305 | var no = Number(event.currentTarget.id.match(/[0-9]+$/)[0]); 306 | $('#simple_filter' + no ).remove(); 307 | 308 | Session.set("autoform_options","{}"); 309 | var counter = Session.get("filter_counter"); 310 | counter.splice(counter.indexOf(no),1); 311 | 312 | Session.set("filter_counter", counter); 313 | var queries = Session.get("filter_queries"); 314 | 315 | delete queries[no]; 316 | Session.set("filter_queries", queries); 317 | 318 | var ops = Session.get("filter_ops"); 319 | delete ops[no]; 320 | delete ops[no-1]; 321 | 322 | Session.set("filter_ops", ops); 323 | $('#filter_operators_values' + counter[counter.length-1] ).val('...'); 324 | 325 | if(counter.length == 0){ 326 | Session.set("filter_counter", [1]); 327 | Blaze.renderWithData(Template.simple_filter, {"no": 1}, document.getElementById('expression_filter')); 328 | Session.set("filter_selector",''); 329 | } 330 | else 331 | if(Object.keys(Session.get("filter_queries")).length == 1){ 332 | Session.set("filter_selector", Session.get("filter_queries")[no-1]); 333 | } 334 | else 335 | Session.set("filter_selector", create_filter_selector(object_values(Session.get("filter_queries")), object_values(ops), Object.keys(ops).length-1)); 336 | 337 | var counter = Session.get("filter_counter"); 338 | 339 | Blaze.render(Template.button_locations, document.getElementById('simple_filter' + counter[counter.length-1] )); 340 | Blaze.render(Template.filter_button, document.getElementById('field_button')); 341 | } 342 | }); 343 | 344 | Template.filter_operators.events({ 345 | 'change .filter_operators': function(event){ 346 | // make sure all options are completed ( should disable operators until they are) 347 | var no = Number(event.currentTarget.id.match(/[0-9]+$/)[0]); 348 | set_filter_selector(no); 349 | var add = true; 350 | 351 | var op = $('#filter_operators_values' + no).val(); 352 | var ops = Session.get("filter_ops"); 353 | if(ops[no]) 354 | add = false; 355 | ops[no] = op; 356 | Session.set("filter_ops", ops); 357 | 358 | if(add){ 359 | var counter = Session.get("filter_counter"); 360 | counter.push(counter[counter.length-1]+1); 361 | Session.set("filter_counter", counter); 362 | 363 | Blaze.renderWithData(Template.simple_filter, {"no": counter[counter.length-1]}, document.getElementById('expression_filter')); 364 | $('#field_button').remove(); 365 | Blaze.render(Template.button_locations, document.getElementById('simple_filter' + counter[counter.length-1] )); 366 | } 367 | } 368 | }); 369 | 370 | Template.filter_value_input.helpers({ 371 | input_value_placeholder: function() { 372 | var sess = Session.get("tabular-filter") 373 | if (sess && (sess.input_value_placeholder || sess.input_value_placeholder == '')) 374 | return sess.input_value_placeholder 375 | return 'Input Value' 376 | } 377 | }); 378 | 379 | 380 | } --------------------------------------------------------------------------------