]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/dojo/openils/widget/PCrudFilterPane.js
Repair PCrudFilterPane localeStrings namespace pollution
[working/Evergreen.git] / Open-ILS / web / js / dojo / openils / widget / PCrudFilterPane.js
1 if (!dojo._hasResource['openils.widget.PCrudFilterPane']) {
2
3     /* openils.widget.PCrudFilterPane is a dijit that, given a fieldmapper
4      * class, provides a pane in which users can define inclusionary
5      * filters based on fields selected from the fieldmapper class and values
6      * for those fields.  Operators can be selected so that not only equality
7      * comparisons are possible in the filter, but also inequality filters,
8      * likeness (for text fields only) betweenness, and nullity tests.  *
9      * The dijit yields its result in the form of a JSON query suitable for
10      * use as the where clause of a pcrud search, via the onApply callback.
11      *
12      * In addition to its fmClass paramter, note the useful parameter
13      * suppressFilterFields.  Say for instance you're using this dijit
14      * on an fmClass like "brt" which has a field "record" that points to the
15      * bre class.  The AutoWidget provided for users to enter values for
16      * comparisons on the record field would be a dropdown containing all
17      * the bre ID's in the system!  That would be unusable in any realistic
18      * system, unless/until we teach AutoWidget to use a lazy-loading store
19      * for dropdowns.
20      *
21      * The comparisons in each filter row are "and-ed" together in the JSON
22      * query yielded, except for repetitions of the same field, which are
23      * "or-ed" together /within/ the overall "and" group.  Look at comments
24      * within PCrudFilterRowManager.compile() for more information.
25      *
26      * AutoGrid has some ability to use this dijits based on this to offer a
27      * filtering dialog, but be aware that the filtering dialog is /not/ aware
28      * of other fitering measures in place in a given AutoGrid-based interface,
29      * such as (typically) context org unit selectors, and therefore using the
30      * context org unit selector will not respect selected filters in this
31      * dijit, and vice-versa.
32      */
33
34     dojo.provide('openils.widget.PCrudFilterPane');
35     dojo.require('openils.widget.AutoFieldWidget');
36     dojo.require('dijit.form.FilteringSelect');
37     dojo.require('dijit.form.Button');
38     dojo.require('dojo.data.ItemFileReadStore');
39     dojo.require('openils.Util');
40
41     dojo.requireLocalization("openils.widget", "PCrudFilterPane");
42
43     var pcFilterLocaleStrings = dojo.i18n.getLocalization(
44         "openils.widget", "PCrudFilterPane"
45     );
46
47     /* These are the operators that make up the central dropdown in each
48      * row of the widget.  When fields of different datatypes are selected,
49      * some of these operators may be masked via the "minimal" and "strict"
50      * properties.
51      */
52     var _operator_store = new dojo.data.ItemFileReadStore(
53         {
54             "data": {
55                 "identifier": "name",
56                 "items": [
57                     {
58                         "name": "=",
59                         "label": pcFilterLocaleStrings.OPERATOR_EQ,
60                         "param_count": 1,
61                         "minimal": true,
62                         "strict": true
63                     }, {
64                         "name": "!=",
65                         "label": pcFilterLocaleStrings.OPERATOR_NE,
66                         "param_count": 1,
67                         "minimal": true,
68                         "strict": true
69                     }, {
70                         "name": "null",
71                         "label": pcFilterLocaleStrings.OPERATOR_IS_NULL,
72                         "param_count": 0,
73                         "minimal": true,
74                         "strict": true
75                     }, {
76                         "name": "not null",
77                         "label": pcFilterLocaleStrings.OPERATOR_IS_NOT_NULL,
78                         "param_count": 0,
79                         "minimal": true,
80                         "strict": true
81                     }, {
82                         "name": ">",
83                         "label": pcFilterLocaleStrings.OPERATOR_GT,
84                         "param_count": 1,
85                         "strict": true
86                     }, {
87                         "name": "<",
88                         "label": pcFilterLocaleStrings.OPERATOR_LT,
89                         "param_count": 1,
90                         "strict": true
91                     }, {
92                         "name": ">=",
93                         "label": pcFilterLocaleStrings.OPERATOR_GTE,
94                         "param_count": 1,
95                         "strict": true
96                     }, {
97                         "name": "<=",
98                         "label": pcFilterLocaleStrings.OPERATOR_LTE,
99                         "param_count": 1,
100                         "strict": true
101                     }, {
102                         "name": "between",
103                         "label": pcFilterLocaleStrings.OPERATOR_BETWEEN,
104                         "param_count": 2,
105                         "strict": true
106                     }, {
107                         "name": "not between",
108                         "label": pcFilterLocaleStrings.OPERATOR_NOT_BETWEEN,
109                         "param_count": 2,
110                         "strict": true
111                     }, {
112                         "name": "like",
113                         "label": pcFilterLocaleStrings.OPERATOR_LIKE,
114                         "param_count": 1
115                     }, {
116                         "name": "not like",
117                         "label": pcFilterLocaleStrings.OPERATOR_NOT_LIKE,
118                         "param_count": 1
119                     }
120                 ]
121             }
122         }
123     );
124
125     /* The text datatype supports all the above operators for comparisons. */
126     var _store_query_by_datatype = {"text": {}};
127
128     /* These three datatypes support only minimal operators. */
129     ["bool", "link", "org_unit"].forEach(
130         function(type) {
131             _store_query_by_datatype[type] = {"minimal": true};
132         }
133     );
134
135     /* These datatypes support strict operators (everything save [not] like). */
136     ["float", "id", "int", "interval", "money", "number", "timestamp"].forEach(
137         function(type) {
138             _store_query_by_datatype[type] = {"strict": true};
139         }
140     );
141
142     /* This helps convert things that pcrud won't accept ("not between", "not
143      * like") into proper JSON query expressions.
144      * It returns false if a clause doesn't have any such negative operator,
145      * or it returns true AND gets rid of the "not " part in the clause
146      * object itself.  It's up to the caller to wrap it in {"-not": {}} in
147      * the right place. */
148     function _clause_was_negative(clause) {
149         /* clause objects really only ever have one property */
150         var ops = openils.Util.objectProperties(clause);
151         var op = ops.pop();
152         var matches = op.match(/^not (\w+)$/);
153         if (matches) {
154             clause[matches[1]] = clause[op];
155             delete clause[op];
156             return true;
157         }
158         return false;
159     }
160
161     /* This is not the dijit per se. Search further in this file for
162      * "dojo.declare" for the beginning of the dijit.
163      *
164      * This is, however, the object that represents a collection of filter
165      * rows and knows how to compile a filter from those rows. */
166     function PCrudFilterRowManager() {
167         var self = this;
168
169         this._init = function(
170             container, field_store, fm_class, compact, widget_builders,
171             skip_first_add_row, do_apply
172         ) {
173             this.container = container;
174             this.field_store = field_store;
175             this.fm_class = fm_class;
176             this.compact = compact;
177             this.widget_builders = widget_builders || {};
178             this.skip_first_add_row = skip_first_add_row;
179             this.do_apply = do_apply;
180
181             this.rows = {};
182             this.row_index = 0;
183
184             this._build_table();
185         };
186
187         this._build_table = function() {
188             this.table = dojo.create(
189                 "table", {
190                     "className": "oils-pcrudfilterdialog-table"
191                 }, this.container
192             );
193
194             var tr = dojo.create(
195                 "tr", {
196                     "id": "pcrudfilterdialog-empty",
197                     "className": "hidden"
198                 }, this.table
199             );
200
201             dojo.create(
202                 "td", {
203                     "colspan": 4,
204                     "innerHTML": pcFilterLocaleStrings[
205                         this.compact ? "EMPTY_CASE_COMPACT" : "EMPTY_CASE"
206                     ]
207                 }, tr
208             );
209
210             if (!this.skip_first_add_row)
211                 this.add_row();
212         };
213
214         this._compile_second_pass = function(first_pass) {
215             var and = [];
216             var result = {"-and": and};
217
218             for (var field in first_pass) {
219                 var list = first_pass[field];
220                 if (list.length == 1) {
221                     var obj = {};
222                     var clause = list.pop();
223                     if (_clause_was_negative(clause)) {
224                         obj["-not"] = {};
225                         obj["-not"][field] = clause;
226                     } else {
227                         obj[field] = clause;
228                     }
229                     and.push(obj);
230                 } else {
231                     var or = list.map(
232                         function(clause) {
233                             var obj = {};
234                             if (_clause_was_negative(clause)) {
235                                 obj["-not"] = {};
236                                 obj["-not"][field] = clause;
237                             } else {
238                                 obj[field] = clause;
239                             }
240                             return obj;
241                         }
242                     );
243                     and.push({"-or": or});
244                 }
245             }
246
247             return result;
248         };
249
250         this.add_row = function(initializer) {
251             this.hide_empty_placeholder();
252             var row_id = this.row_index++;
253             this.rows[row_id] = new PCrudFilterRow(this, row_id, initializer);
254         };
255
256         this.remove_row = function(row_id) {
257             this.rows[row_id].destroy();
258             delete this.rows[row_id];
259
260             if (openils.Util.objectProperties(this.rows).length < 1)
261                 this.show_empty_placeholder();
262
263             if (this.compact)
264                 this.do_apply();
265         };
266
267         this.hide_empty_placeholder = function() {
268             openils.Util.hide("pcrudfilterdialog-empty");
269         };
270
271         this.show_empty_placeholder = function() {
272             openils.Util.show("pcrudfilterdialog-empty");
273         };
274
275         this.compile = function() {
276             /* We'll prepare a first-pass data structure that looks like:
277              * {
278              *  field1: [{"op": "one value"}],
279              *  field2: [{"op": "a value"}, {"op": "b value"}],
280              *  field3: [{"op": "first value"}, {"op": ["range start", "range end"]}]
281              * }
282              *
283              * which will be passed to _compile_second_pass() to yield an
284              * actual filter suitable for pcrud (with -and and -or in all the
285              * right places) so the above example would come out like:
286              *
287              * { "-and": [
288              *   {"field1": {"op": "one value"}},
289              *   {"-or": [ {"field2": {"op": "a value"}}, {"field2": {"op": "b value"}} ] },
290              *   {"-or": [
291              *     {"field3": {"op": "first value"}},
292              *     {"field3": {"op": ["range start", "range end"]}}
293              *   ] }
294              * ] }
295              */
296             var first_pass = {};
297
298             for (var row_id in this.rows) {
299                 var row = this.rows[row_id];
300                 var value = row.compile();
301                 var field = row.selected_field;
302
303                 if (typeof(value) != "undefined" &&
304                     typeof(field) != "undefined") {
305                     if (!first_pass[field])
306                         first_pass[field] = [];
307                     first_pass[field].push(value);
308                 }
309             }
310
311             /* Don't return an empty filter: pcrud can't use that. */
312             if (openils.Util.objectProperties(first_pass).length < 1) {
313                 var result = {};
314                 result[fieldmapper[this.fm_class].Identifier] = {"!=": null};
315                 return result;
316             } else {
317                 return this._compile_second_pass(first_pass);
318             }
319         };
320
321         this._init.apply(this, arguments);
322     }
323
324     /* As the name implies, objects of this class manage a single row of the
325      * query.  Therefore they know about their own field dropdown, their own
326      * selector dropdown, and their own value widget (or widgets in the case
327      * of between searches, which call for two widgets to define a range),
328      * and not much else. */
329     function PCrudFilterRow() {
330         var self = this;
331
332         this._init = function(filter_row_manager, row_id, initializer) {
333             this.filter_row_manager = filter_row_manager;
334             this.row_id = row_id;
335
336             if (this.filter_row_manager.compact)
337                 this._build_compact();
338             else
339                 this._build();
340
341             if (initializer)
342                 this.initialize(initializer);
343         };
344
345         this._build = function() {
346             this.tr = dojo.create("tr", {}, this.filter_row_manager.table);
347
348             this._create_field_selector();
349             this._create_operator_selector();
350             this._create_value_slot();
351             this._create_remover();
352         };
353
354         this._build_compact = function() {
355             this.tr = dojo.create("tr", {}, this.filter_row_manager.table);
356
357             var td = dojo.create("td", {}, this.tr);
358
359             this._create_field_selector(td);
360             this._create_operator_selector(td);
361
362             dojo.create("br", {}, td);
363             this._create_value_slot(td);
364
365             td = dojo.create(
366                 "td",
367                 {"className": "oils-pcrudfilterdialog-remover-holder"},
368                 this.tr
369             );
370
371             this._create_remover(td);
372         };
373
374         this._create_field_selector = function(use_element) {
375             var td = use_element || dojo.create("td", {}, this.tr);
376
377             this.field_selector = new dijit.form.FilteringSelect(
378                 {
379                     "labelAttr": "label",
380                     "searchAttr": "label",
381                     "scrollOnFocus": false,
382                     "onChange": function(value) {
383                         self.update_selected_field(value);
384                     },
385                     "store": this.filter_row_manager.field_store
386                 }, dojo.create("span", {}, td)
387             );
388         };
389
390         this._create_operator_selector = function(use_element) {
391             var td = use_element || dojo.create("td", {}, this.tr);
392
393             this.operator_selector = new dijit.form.FilteringSelect(
394                 {
395                     "labelAttr": "label",
396                     "searchAttr": "label",
397                     "scrollOnFocus": false,
398                     "onChange": function(value) {
399                         self.update_selected_operator(value);
400                     },
401                     "store": _operator_store
402                 }, dojo.create("span", {}, td)
403             );
404         };
405
406         this._adjust_operator_selector = function() {
407             this.operator_selector.attr(
408                 "query", _store_query_by_datatype[this.selected_field_type]
409             );
410             this.operator_selector.reset();
411         };
412
413         this._create_value_slot = function(use_element) {
414             if (use_element)
415                 this.value_slot = dojo.create(
416                     "span", {"innerHTML": "-"}, use_element
417                 );
418             else
419                 this.value_slot = dojo.create("td",{"innerHTML":"-"},this.tr);
420         };
421
422         this._create_remover = function(use_element) {
423             var td = use_element || dojo.create("td", {}, this.tr);
424             var anchor = dojo.create(
425                 "a", {
426                     "className": "oils-pcrudfilterdialog-remover",
427                     "innerHTML": "X",
428                     "href": "#",
429                     "onclick": function() {
430                         self.filter_row_manager.remove_row(self.row_id);
431                     }
432                 }, td
433             );
434         };
435
436         this._clear_value_slot = function() {
437             if (this.value_widgets) {
438                 this.value_widgets.forEach(
439                     function(autowidg) { autowidg.widget.destroy(); }
440                 );
441                 delete this.value_widgets;
442             }
443
444             dojo.empty(this.value_slot);
445         };
446
447         this._rebuild_value_widgets = function() {
448             this._clear_value_slot();
449
450             if (!this.get_selected_operator() || !this.selected_field)
451                 return;
452
453             this.value_widgets = [];
454
455             var param_count = this.operator_selector.item.param_count;
456
457             /* This is where find and deploy custom widget builders. */
458             var widget_builder_key = this.selected_field_fm_class + ":" +
459                 this.selected_field_fm_field;
460             var constr =
461                 this.filter_row_manager.widget_builders[widget_builder_key] ||
462                 openils.widget.AutoFieldWidget;
463
464             for (var i = 0; i < param_count; i++) {
465                 var widg = new constr({
466                     "fmClass": this.selected_field_fm_class,
467                     "fmField": this.selected_field_fm_field,
468                     "parentNode": dojo.create("span", {}, this.value_slot),
469                     "dijitArgs": {"scrollOnFocus": false}
470                 });
471
472                 widg.build();
473                 this.value_widgets.push(widg);
474             }
475         };
476
477         /* for ugly special cases in compliation */
478         this._null_clause = function() {
479             var opname = this.get_selected_operator_name();
480             if (opname == "not null")
481                 return {"!=": null};
482             else if (opname == "null")
483                 return null;
484             else
485                 return;
486         };
487
488         /* wrap s in %'s unless it already contains at least one %. */
489         this._add_like_wildcards = function(s) {
490             return s.indexOf("%") == -1 ? ("%" + s + "%") : s;
491         };
492
493         this.get_selected_operator = function() {
494             if (this.operator_selector)
495                 return this.operator_selector.item;
496         };
497
498         this.get_selected_operator_name = function() {
499             var op = this.get_selected_operator();
500             return op ? op.name : null;
501         };
502
503         this.update_selected_operator = function(value) {
504             this._rebuild_value_widgets();
505         };
506
507         this.update_selected_field = function(value) {
508             if (this.field_selector.item) {
509                 this.selected_field = value;
510                 this.selected_field_type = this.field_selector.item.type;
511
512                 /* This is really about supporting flattenergrid, of which
513                  * we're in the superclass (in a sloppy sad way). From now
514                  * on I won't mix this kind of lazy object with Dojo modules. */
515                 this.selected_field_fm_field = this.field_selector.item.name;
516                 this.selected_field_is_indirect =
517                     this.field_selector.item.indirect || false;
518                 this.selected_field_fm_class =
519                     this.field_selector.item.fmClass ||
520                     this.filter_row_manager.fm_class;
521
522                 this._adjust_operator_selector();
523                 this._rebuild_value_widgets();
524             }
525         };
526
527         this.compile = function() {
528             if (this.value_widgets) {
529                 var values = this.value_widgets.map(
530                     function(widg) {
531                         if (widg.useCorrectly)
532                             return widg.widget.attr("value");
533                         else if (self.selected_field_is_indirect)
534                             return widg.widget.attr("displayedValue");
535                         else
536                             return widg.getFormattedValue();
537                     }
538                 );
539
540                 if (!values.length) {
541                     return this._null_clause(); /* null/not null */
542                 } else {
543                     var clause = {};
544                     var op = this.get_selected_operator_name();
545
546                     var prep_function = function(o) { return o; /* no-op */ };
547                     if (String(op).match(/like/))
548                         prep_function = this._add_like_wildcards;
549
550                     if (values.length == 1)
551                         clause[op] = prep_function(values.pop());
552                     else
553                         clause[op] = dojo.map(values, prep_function);
554                     return clause;
555                 }
556             } else {
557                 return;
558             }
559         };
560
561         this.destroy = function() {
562             this._clear_value_slot();
563             this.field_selector.destroy();
564             if (this.operator_selector)
565                 this.operator_selector.destroy();
566
567             dojo.destroy(this.tr);
568         };
569
570         this.initialize = function(initializer) {
571             this.field_selector.attr("value", initializer.field);
572             this.operator_selector.attr("value", initializer.operator);
573
574             /* Caller supplies value for one value, values (array) for
575              * multiple. */
576             if (!initializer.values || !dojo.isArray(initializer.values))
577                 initializer.values = [initializer.values || initializer.value];
578
579             for (var i = 0; i < initializer.values.length; i++) {
580                 this.value_widgets[i].widget.attr(
581                     "value", initializer.values[i]
582                 );
583             }
584         };
585
586         this._init.apply(this, arguments);
587     }
588
589     dojo.declare(
590         "openils.widget.PCrudFilterPane", [openils.widget.AutoWidget],
591         {
592             "useDiv": null, /* should always be null for subclass dialogs */
593             "initializers": null,
594             "compact": false,
595             "widgetBuilders": null,
596
597             "constructor": function(args) {
598                 for(var k in args)
599                     this[k] = args[k];
600                 this.widgetIndex = 0;
601                 this.widgetCache = {};
602
603                 /* Meaningless in a pane, but better here than in
604                  * PCrudFilterDialog so that we don't need to load i18n
605                  * strings there: */
606                 this.title = this.title || pcFilterLocaleStrings.DEFAULT_DIALOG_TITLE;
607             },
608
609             "_buildButtons": function() {
610                 var self = this;
611
612                 var button_holder = dojo.create(
613                     "div", {
614                         "className": "oils-pcrudfilterdialog-buttonholder"
615                     }, this.domNode
616                 );
617
618                 new dijit.form.Button(
619                     {
620                         "label": pcFilterLocaleStrings.ADD_ROW,
621                         "scrollOnFocus": false, /* almost always better */
622                         "onClick": function() {
623                             self.filter_row_manager.add_row();
624                         }
625                     }, dojo.create("span", {}, button_holder)
626                 );
627
628                 this._apply_button = new dijit.form.Button(
629                     {
630                         "label": pcFilterLocaleStrings.APPLY,
631                         "scrollOnFocus": false,
632                         "onClick": function() { self.doApply(); }
633                     }, dojo.create("span", {}, button_holder)
634                 );
635
636                 if (!this.useDiv) {
637                     new dijit.form.Button(
638                         {
639                             "label": pcFilterLocaleStrings.CANCEL,
640                             "scrollOnFocus": false,
641                             "onClick": function() {
642                                 if (self.onCancel)
643                                     self.onCancel();
644                                 self.hide();
645                             }
646                         }, dojo.create("span", {}, button_holder)
647                     );
648                 }
649             },
650
651             "_buildFieldStore": function() {
652                 var self = this;
653                 var realFieldList = this.sortedFieldList.filter(
654                     function(item) { return !(item.virtual || item.nonIdl); }
655                 );
656
657                 /* Prevent any explicitly unwanted fields from being available
658                  * in our field dropdowns. */
659                 if (dojo.isArray(this.suppressFilterFields)) {
660                     realFieldList = realFieldList.filter(
661                         function(item) {
662                             for (
663                                 var i = 0;
664                                 i < self.suppressFilterFields.length;
665                                 i++
666                             ) {
667                                 if (item.name == self.suppressFilterFields[i])
668                                     return false;
669                             }
670                             return true;
671                         }
672                     );
673                 }
674
675                 this.fieldStore = new dojo.data.ItemFileReadStore({
676                     "data": {
677                         "identifier": "name",
678                         "name": "label",
679                         "items": realFieldList.map(
680                             function(item) {
681                                 return {
682                                     "label": item.label,
683                                     "name": item.name,
684                                     "type": item.datatype
685                                 };
686                             }
687                         )
688                     }
689                 });
690             },
691
692             "hide": function() {
693                 try {
694                     this.inherited(arguments);
695                 } catch (E) {
696                     /* When using *FilterPane directly (without a *Dialog
697                      * subclass), do nothing.  */
698                     void(0);
699                 }
700             },
701
702             /* All we really do here is create a data store out of the fields
703              * from the IDL for our given class, place a few buttons at the
704              * bottom of the dialog, and hand off to PCrudFilterRowManager to
705              * do the actual work.
706              */
707
708             "startup": function() {
709                 if (this.useDiv)
710                     this.domNode = this.useDiv;
711
712                 try {
713                     this.inherited(arguments);
714                 } catch (E) {
715                     /* When using *FilterPane directly (without a *Dialog
716                      * subclass), there is no startup method in any ancestor
717                      * class. XXX Refactor?
718                      */
719                     void(0);
720                 }
721
722                 this.initAutoEnv();
723
724                 this._buildFieldStore();
725
726                 this.filter_row_manager = new PCrudFilterRowManager(
727                     dojo.create("div", {}, this.domNode),
728                     this.fieldStore, this.fmClass, this.compact,
729                     this.widgetBuilders,
730                     Boolean(this.initializers)  /* avoid adding empty row */,
731                     dojo.hitch(this, function() { this.doApply(); })
732                 );
733
734                 this._buildButtons();
735
736                 if (this.initializers) {
737                     this.initializers.forEach(
738                         dojo.hitch(this, function(initializer) {
739                             this.filter_row_manager.add_row(initializer);
740                         })
741                     );
742                 }
743             },
744
745             /* This should just be named 'apply', but that is kind of a special
746              * word in Javascript, no? */
747             "doApply": function() {
748                 this._apply_button.attr("disabled", true);
749
750                 var _E; /* Try pretty hard not to leave the apply button
751                            disabled forever, even if 'apply' blows up. */
752                 try {
753                     if (this.onApply)
754                         this.onApply(this.filter_row_manager.compile());
755                 } catch (E) {
756                     _E = E;
757                 }
758                 this.hide();
759                 this._apply_button.attr("disabled", false);
760
761                 if (_E) throw _E;
762             }
763         }
764     );
765 }