]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/dojo/openils/widget/PCrudFilterPane.js
Link checker: user interface and supporting fixes (part 2)
[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('dijit.form.DropDownButton');
39     dojo.require('dijit.TooltipDialog');
40     dojo.require('dojo.data.ItemFileReadStore');
41     dojo.require('openils.Util');
42
43     dojo.requireLocalization("openils.widget", "PCrudFilterPane");
44
45     var pcFilterLocaleStrings = dojo.i18n.getLocalization(
46         "openils.widget", "PCrudFilterPane"
47     );
48
49     /* These are the operators that make up the central dropdown in each
50      * row of the widget.  When fields of different datatypes are selected,
51      * some of these operators may be masked via the "minimal" and "strict"
52      * properties.
53      */
54     var _operator_store = new dojo.data.ItemFileReadStore(
55         {
56             "data": {
57                 "identifier": "name",
58                 "items": [
59                     {
60                         "name": "=",
61                         "label": pcFilterLocaleStrings.OPERATOR_EQ,
62                         "param_count": 1,
63                         "minimal": true,
64                         "strict": true
65                     }, {
66                         "name": "!=",
67                         "label": pcFilterLocaleStrings.OPERATOR_NE,
68                         "param_count": 1,
69                         "minimal": true,
70                         "strict": true
71                     }, {
72                         "name": "null",
73                         "label": pcFilterLocaleStrings.OPERATOR_IS_NULL,
74                         "param_count": 0,
75                         "minimal": true,
76                         "strict": true
77                     }, {
78                         "name": "not null",
79                         "label": pcFilterLocaleStrings.OPERATOR_IS_NOT_NULL,
80                         "param_count": 0,
81                         "minimal": true,
82                         "strict": true
83                     }, {
84                         "name": ">",
85                         "label": pcFilterLocaleStrings.OPERATOR_GT,
86                         "param_count": 1,
87                         "strict": true
88                     }, {
89                         "name": "<",
90                         "label": pcFilterLocaleStrings.OPERATOR_LT,
91                         "param_count": 1,
92                         "strict": true
93                     }, {
94                         "name": ">=",
95                         "label": pcFilterLocaleStrings.OPERATOR_GTE,
96                         "param_count": 1,
97                         "strict": true
98                     }, {
99                         "name": "<=",
100                         "label": pcFilterLocaleStrings.OPERATOR_LTE,
101                         "param_count": 1,
102                         "strict": true
103                     }, {
104                         "name": "in",
105                         "label": pcFilterLocaleStrings.OPERATOR_IN,
106                         "param_count": null,    /* arbitrary number, special */
107                         "strict": true,
108                         "minimal": true
109                     }, {
110                         "name": "not in",
111                         "label": pcFilterLocaleStrings.OPERATOR_NOT_IN,
112                         "param_count": null,    /* arbitrary number, special */
113                         "strict": true,
114                         "minimal": true
115                     }, {
116                         "name": "between",
117                         "label": pcFilterLocaleStrings.OPERATOR_BETWEEN,
118                         "param_count": 2,
119                         "strict": true
120                     }, {
121                         "name": "not between",
122                         "label": pcFilterLocaleStrings.OPERATOR_NOT_BETWEEN,
123                         "param_count": 2,
124                         "strict": true
125                     }, {
126                         "name": "like",
127                         "label": pcFilterLocaleStrings.OPERATOR_LIKE,
128                         "param_count": 1
129                     }, {
130                         "name": "not like",
131                         "label": pcFilterLocaleStrings.OPERATOR_NOT_LIKE,
132                         "param_count": 1
133                     }
134                 ]
135             }
136         }
137     );
138
139     /* The text datatype supports all the above operators for comparisons. */
140     var _store_query_by_datatype = {"text": {}};
141
142     /* These three datatypes support only minimal operators. */
143     ["bool", "link", "org_unit"].forEach(
144         function(type) {
145             _store_query_by_datatype[type] = {"minimal": true};
146         }
147     );
148
149     /* These datatypes support strict operators (everything save [not] like). */
150     ["float", "id", "int", "interval", "money", "number", "timestamp"].forEach(
151         function(type) {
152             _store_query_by_datatype[type] = {"strict": true};
153         }
154     );
155
156     /* This helps convert things that pcrud won't accept ("not between", "not
157      * like") into proper JSON query expressions.
158      * It returns false if a clause doesn't have any such negative operator,
159      * or it returns true AND gets rid of the "not " part in the clause
160      * object itself.  It's up to the caller to wrap it in {"-not": {}} in
161      * the right place. */
162     function _clause_was_negative(clause) {
163         /* clause objects really only ever have one property */
164         if (clause === null) return false; /* early out for special operator */
165
166         var ops = openils.Util.objectProperties(clause);
167         var op = ops.pop();
168         var matches = op.match(/^not [lb].+$/); /* "not in" needs no change */
169         if (matches) {
170             clause[matches[1]] = clause[op];
171             delete clause[op];
172             return true;
173         }
174         return false;
175     }
176
177     /* Given a value, add it to selector options if it's not already there,
178      * and select it. */
179     function _add_or_at_least_select(value, selector) {
180         var found = false;
181
182         for (var i = 0; i < selector.options.length; i++) {
183             var option = selector.options[i];
184             if (option.value == value) {
185                 found = true;
186                 option.selected = true;
187             }
188         }
189
190         if (!found) {
191             dojo.create(
192                 "option", {
193                     "innerHTML": value,
194                     "value": value,
195                     "selected": "selected"
196                 }, selector
197             );
198         }
199     }
200
201     /* This is not the dijit per se. Search further in this file for
202      * "dojo.declare" for the beginning of the dijit.
203      *
204      * This is, however, the object that represents a collection of filter
205      * rows and knows how to compile a filter from those rows. */
206     function PCrudFilterRowManager() {
207         var self = this;
208
209         this._init = function(
210             container, field_store, fm_class, compact, widget_builders,
211             skip_first_add_row, do_apply
212         ) {
213             this.container = container;
214             this.field_store = field_store;
215             this.fm_class = fm_class;
216             this.compact = compact;
217             this.widget_builders = widget_builders || {};
218             this.skip_first_add_row = skip_first_add_row;
219             this.do_apply = do_apply;
220
221             this.rows = {};
222             this.row_index = 0;
223
224             this._build_table();
225         };
226
227         this._build_table = function() {
228             this.table = dojo.create(
229                 "table", {
230                     "className": "oils-pcrudfilterdialog-table"
231                 }, this.container
232             );
233
234             var tr = dojo.create(
235                 "tr", {
236                     "id": "pcrudfilterdialog-empty",
237                     "className": "hidden"
238                 }, this.table
239             );
240
241             dojo.create(
242                 "td", {
243                     "colspan": 4,
244                     "innerHTML": pcFilterLocaleStrings[
245                         this.compact ? "EMPTY_CASE_COMPACT" : "EMPTY_CASE"
246                     ]
247                 }, tr
248             );
249
250             if (!this.skip_first_add_row)
251                 this.add_row();
252         };
253
254         this._compile_second_pass = function(first_pass) {
255             var and = [];
256             var result = {"-and": and};
257
258             for (var field in first_pass) {
259                 var list = first_pass[field];
260                 if (list.length == 1) {
261                     var obj = {};
262                     var clause = list.pop();
263                     if (_clause_was_negative(clause)) {
264                         obj["-not"] = {};
265                         obj["-not"][field] = clause;
266                     } else {
267                         obj[field] = clause;
268                     }
269                     and.push(obj);
270                 } else {
271                     var or = list.map(
272                         function(clause) {
273                             var obj = {};
274                             if (_clause_was_negative(clause)) {
275                                 obj["-not"] = {};
276                                 obj["-not"][field] = clause;
277                             } else {
278                                 obj[field] = clause;
279                             }
280                             return obj;
281                         }
282                     );
283                     and.push({"-or": or});
284                 }
285             }
286
287             return result;
288         };
289
290         this._validate_initializer = function(initializer, onsuccess) {
291             this.field_store.fetchItemByIdentity({
292                 "identity": initializer.field,
293                 "onItem": dojo.hitch(this, function(item) {
294                     if (item) {
295                         onsuccess();
296                     } else {
297                         console.debug(
298                             "skipping initializer for field " +
299                             initializer.field + " not present here"
300                         );
301                     }
302                 })
303             });
304         };
305
306         this._proceed_add_row = function(initializer) {
307             var row_id_list = openils.Util.objectProperties(this.rows);
308
309             /* Kill initial empty row when adding pre-initialized rows. */
310             if (row_id_list.length == 1 && initializer) {
311                 var existing_row_id = row_id_list.shift();
312                 if (this.rows[existing_row_id].is_unset())
313                     this.remove_row(existing_row_id, true /* no_apply */);
314             }
315
316             this.hide_empty_placeholder();
317             var row_id = this.row_index++;
318             this.rows[row_id] = new PCrudFilterRow(this, row_id, initializer);
319         };
320
321         this.add_row = function(initializer) {
322             if (initializer) {
323                 this._validate_initializer(
324                     initializer,
325                     dojo.hitch(this, function() {
326                         this._proceed_add_row(initializer);
327                     })
328                 );
329             } else {
330                 this._proceed_add_row(initializer);
331             }
332         };
333
334         this.remove_row = function(row_id, no_apply) {
335             this.rows[row_id].destroy();
336             delete this.rows[row_id];
337
338             if (openils.Util.objectProperties(this.rows).length < 1)
339                 this.show_empty_placeholder();
340
341             if (this.compact && !no_apply)
342                 this.do_apply();
343         };
344
345         this.hide_empty_placeholder = function() {
346             openils.Util.hide("pcrudfilterdialog-empty");
347         };
348
349         this.show_empty_placeholder = function() {
350             openils.Util.show("pcrudfilterdialog-empty");
351         };
352
353         this.compile = function() {
354             /* We'll prepare a first-pass data structure that looks like:
355              * {
356              *  field1: [{"op": "one value"}],
357              *  field2: [{"op": "a value"}, {"op": "b value"}],
358              *  field3: [{"op": "first value"}, {"op": ["range start", "range end"]}]
359              * }
360              *
361              * which will be passed to _compile_second_pass() to yield an
362              * actual filter suitable for pcrud (with -and and -or in all the
363              * right places) so the above example would come out like:
364              *
365              * { "-and": [
366              *   {"field1": {"op": "one value"}},
367              *   {"-or": [ {"field2": {"op": "a value"}}, {"field2": {"op": "b value"}} ] },
368              *   {"-or": [
369              *     {"field3": {"op": "first value"}},
370              *     {"field3": {"op": ["range start", "range end"]}}
371              *   ] }
372              * ] }
373              */
374             var first_pass = {};
375
376             for (var row_id in this.rows) {
377                 var row = this.rows[row_id];
378                 var value = row.compile();
379                 var field = row.selected_field;
380
381                 if (typeof(value) != "undefined" &&
382                     typeof(field) != "undefined") {
383                     if (!first_pass[field])
384                         first_pass[field] = [];
385                     first_pass[field].push(value);
386                 }
387             }
388
389             /* Don't return an empty filter: pcrud can't use that. */
390             if (openils.Util.objectProperties(first_pass).length < 1) {
391                 var result = {};
392                 result[fieldmapper[this.fm_class].Identifier] = {"!=": null};
393                 return result;
394             } else {
395                 return this._compile_second_pass(first_pass);
396             }
397         };
398
399         /* This is for generating a data structure so that we can store
400          * a representation of the state of the filter rows.  Not for
401          * generating a query to be used in search.  You want compile() for
402          * that. */
403         this.serialize = function() {
404             var serialized = [];
405             for (var rowkey in this.rows) { /* row order doesn't matter */
406                 var row_ser = this.rows[rowkey].serialize();
407                 if (row_ser)
408                     serialized.push(row_ser);
409             }
410             return dojo.toJson(serialized);
411         };
412
413         this._init.apply(this, arguments);
414     }
415
416     /* As the name implies, objects of this class manage a single row of the
417      * query.  Therefore they know about their own field dropdown, their own
418      * selector dropdown, and their own value widget (or widgets in the case
419      * of between searches, which call for two widgets to define a range),
420      * and not much else. */
421     function PCrudFilterRow() {
422         var self = this;
423
424         this._init = function(filter_row_manager, row_id, initializer) {
425             this.filter_row_manager = filter_row_manager;
426             this.row_id = row_id;
427
428             if (this.filter_row_manager.compact)
429                 this._build_compact();
430             else
431                 this._build();
432
433             if (initializer)
434                 this.initialize(initializer);
435         };
436
437         this._build = function() {
438             this.tr = dojo.create("tr", {}, this.filter_row_manager.table);
439
440             this._create_field_selector();
441             this._create_operator_selector();
442             this._create_value_slot();
443             this._create_remover();
444         };
445
446         this._build_compact = function() {
447             this.tr = dojo.create("tr", {}, this.filter_row_manager.table);
448
449             var td = dojo.create("td", {}, this.tr);
450
451             this._create_field_selector(td);
452             this._create_operator_selector(td);
453
454             dojo.create("br", {}, td);
455             this._create_value_slot(td);
456
457             td = dojo.create(
458                 "td",
459                 {"className": "oils-pcrudfilterdialog-remover-holder"},
460                 this.tr
461             );
462
463             this._create_remover(td);
464         };
465
466         this._create_field_selector = function(use_element) {
467             var td = use_element || dojo.create("td", {}, this.tr);
468
469             this.field_selector = new dijit.form.FilteringSelect(
470                 {
471                     "labelAttr": "label",
472                     "searchAttr": "label",
473                     "scrollOnFocus": false,
474                     "onChange": function(value) {
475                         self.update_selected_field(value);
476                         if (this.and_then) {    /* ugh. also, self != this. */
477                             var once = this.and_then;
478                             delete this.and_then;
479                             once();
480                         }
481                     },
482                     "store": this.filter_row_manager.field_store
483                 }, dojo.create("span", {}, td)
484             );
485         };
486
487         this._create_operator_selector = function(use_element) {
488             var td = use_element || dojo.create("td", {}, this.tr);
489
490             this.operator_selector = new dijit.form.FilteringSelect(
491                 {
492                     "labelAttr": "label",
493                     "searchAttr": "label",
494                     "scrollOnFocus": false,
495                     "onChange": function(value) {
496                         self.update_selected_operator(value);
497                     },
498                     "store": _operator_store
499                 }, dojo.create("span", {}, td)
500             );
501         };
502
503         this._adjust_operator_selector = function() {
504             this.operator_selector.attr(
505                 "query", _store_query_by_datatype[this.selected_field_type]
506             );
507             this.operator_selector.reset();
508         };
509
510         this._create_value_slot = function(use_element) {
511             var how = {"innerHTML": "-"};
512
513             if (use_element)
514                 this.value_slot = dojo.create("span", how, use_element);
515             else
516                 this.value_slot = dojo.create("td", how, this.tr);
517         };
518
519         this._create_remover = function(use_element) {
520             var td = use_element || dojo.create("td", {}, this.tr);
521             var anchor = dojo.create(
522                 "a", {
523                     "className": "oils-pcrudfilterdialog-remover",
524                     "innerHTML": "X",
525                     "href": "#",
526                     "onclick": function() {
527                         self.filter_row_manager.remove_row(self.row_id);
528                     }
529                 }, td
530             );
531         };
532
533         this._clear_value_slot = function() {
534             if (this.value_widgets) {
535                 this.value_widgets.forEach(
536                     function(autowidg) {
537                         if (autowidg.widget)
538                             autowidg.widget.destroy();
539                     }
540                 );
541                 delete this.value_widgets;
542             }
543
544             dojo.empty(this.value_slot);
545         };
546
547         this._rebuild_value_widgets = function() {
548             this._clear_value_slot();
549
550             if (!this.get_selected_operator() || !this.selected_field)
551                 return;
552
553             this.value_widgets = [];
554
555             /* This is where find custom widget builders to deploy shortly. */
556             var widget_builder_key = this.selected_field_fm_class + ":" +
557                 this.selected_field_fm_field;
558             var constr =
559                 this.filter_row_manager.widget_builders[widget_builder_key] ||
560                 openils.widget.AutoFieldWidget;
561
562             /* How many value widgets do we need for this operator? */
563             var param_count =
564                 this.operator_selector.store.getValue(
565                     this.operator_selector.item, "param_count"
566                 );
567
568             if (param_count === null) {
569                 /* When param_count is null, we invoke the special case of
570                  * preparing widgets for building a dynamic set of values.
571                  * All other cases are handled by the else branch. */
572                 this._build_set_value_widgets(constr);
573             } else {
574                 for (var i = 0; i < param_count; i++) {
575                     this.value_widgets.push(
576                         this._build_one_value_widget(constr)
577                     );
578                 }
579             }
580         };
581
582         this._build_set_value_widgets = function(constr) {
583             var value_widget = dojo.create(
584                 "select", {
585                     "multiple": "multiple",
586                     "size": 4,
587                     "style": {
588                         "width": "6em",
589                         "verticalAlign": "middle",
590                         "margin": "0 0.75em"
591                     }
592                 },
593                 this.value_slot
594             );
595             var entry_widget = this._build_one_value_widget(constr);
596             var adder = dojo.create(
597                 "a", {
598                     "href": "javascript:void(0);",
599                     "style": {"verticalAlign": "middle", "margin": "0 0.75em"},
600                     "innerHTML": "[+]", /* XXX i18n? */
601                     "onclick": dojo.hitch(this, function() {
602                         _add_or_at_least_select(
603                             this._value_for_compile(entry_widget),
604                             value_widget
605                         );
606                         entry_widget.widget.attr("value", ""); /* clear */
607                     })
608                 }, this.value_slot
609             );
610             this.value_widgets.push(value_widget);
611         };
612
613
614         /* Create just one value widget (used by higher-level functions
615          * that worry about how many are needed). */
616         this._build_one_value_widget = function(constr) {
617             var widg = new constr({
618                 "fmClass": this.selected_field_fm_class,
619                 "fmField": this.selected_field_fm_field,
620                 "noDisablePkey": true,
621                 "parentNode": dojo.create(
622                     "span", {
623                         "style": {"verticalAlign": "middle"}
624                     }, this.value_slot
625                 ),
626                 "dijitArgs": {"scrollOnFocus": false}
627             });
628
629             widg.build();
630             return widg;
631         };
632
633         this._value_for_serialize = function(widg) {
634             if (!widg.widget)   /* widg is <select> */
635                 return dojo.filter(
636                     widg.options,
637                     function(o) { return o.selected; }
638                 ).map(
639                     function(o) { return o.value; }
640                 );
641             else
642                 return widg.widget.attr("value");
643         };
644
645         this._value_for_compile = function(widg) {
646             if (!widg.widget)   /* widg is <select> */
647                 return dojo.filter(
648                     widg.options,
649                     function(o) { return o.selected; }
650                 ).map(
651                     function(o) { return o.value; }
652                 );
653             else if (widg.useCorrectly)
654                 return widg.widget.attr("value");
655             else if (this.selected_field_is_indirect)
656                 return widg.widget.attr("displayedValue");
657             else
658                 return widg.getFormattedValue();
659         }
660
661         /* for ugly special cases in compilation */
662         this._null_clause = function() {
663             var opname = this.get_selected_operator_name();
664             if (opname == "not null")
665                 return {"!=": null};
666             else if (opname == "null")
667                 return null;
668             else
669                 return;
670         };
671
672         /* wrap s in %'s unless it already contains at least one %. */
673         this._add_like_wildcards = function(s) {
674             return s.indexOf("%") == -1 ? ("%" + s + "%") : s;
675         };
676
677         this.get_selected_operator = function() {
678             if (this.operator_selector)
679                 return this.operator_selector.item;
680         };
681
682         this.get_selected_operator_name = function() {
683             var item = this.get_selected_operator();
684             if (item) {
685                 return this.operator_selector.store.getValue(item, "name");
686             } else {
687                 console.warn(
688                     "Could not determine selected operator. " +
689                     "Something is about to break."
690                 );
691             }
692         };
693
694         this.update_selected_operator = function(value) {
695             this._rebuild_value_widgets();
696         };
697
698         this.update_selected_field = function(value) {
699             if (this.field_selector.item) {
700                 this.selected_field = value;
701                 this.selected_field_type = this.field_selector.item.type;
702
703                 /* This is really about supporting flattenergrid, of which
704                  * we're in the superclass (in a sloppy sad way). From now
705                  * on I won't mix this kind of lazy object with Dojo modules. */
706                 this.selected_field_fm_field = this.field_selector.item.name;
707                 this.selected_field_is_indirect =
708                     this.field_selector.item.indirect || false;
709                 this.selected_field_fm_class =
710                     this.field_selector.item.fmClass ||
711                     this.filter_row_manager.fm_class;
712
713                 this._adjust_operator_selector();
714                 this._rebuild_value_widgets();
715             }
716         };
717
718         this.serialize = function() {
719             if (!this.selected_field)
720                 return;
721
722             var serialized = {
723                 "field": this.selected_field,
724                 "operator": this.get_selected_operator_name()
725             };
726
727             var values;
728
729             if (this.value_widgets) {
730                 values = this.value_widgets.map(
731                     dojo.hitch(
732                         this, function(w) {
733                             return this._value_for_serialize(w);
734                         }
735                     )
736                 );
737             }
738
739             /* The following grew organically to be very silly and confusing.
740              * Could use a rethink (PCrudFilterRow.initialize() would also need
741              * matching changes). */
742             if (values.length == 1) {
743                 if (dojo.isArray(values[0]))
744                     serialized.values = values[0];
745                 else
746                     serialized.value = values[0];
747             } else if (values.length > 1) {
748                 serialized.values = values;
749             }
750
751             return serialized;
752         };
753
754         this.compile = function() {
755             if (this.value_widgets) {
756                 var values = this.value_widgets.map(
757                     dojo.hitch(this, this._value_for_compile)
758                 );
759
760                 if (!values.length) {
761                     return this._null_clause(); /* null/not null */
762                 } else {
763                     var clause = {};
764                     var op = this.get_selected_operator_name();
765
766                     var prep_function = function(o) {
767                         if (dojo.isArray(o) && !o.length)
768                             throw new Error(pcFilterLocaleStrings.EMPTY_LIST);
769
770                         return o;
771                     };
772
773                     if (String(op).match(/like/))
774                         prep_function = this._add_like_wildcards;
775
776                     if (values.length == 1)
777                         clause[op] = prep_function(values.pop());
778                     else
779                         clause[op] = dojo.map(values, prep_function);
780                     return clause;
781                 }
782             } else {
783                 return;
784             }
785         };
786
787         this.destroy = function() {
788             this._clear_value_slot();
789             this.field_selector.destroy();
790             if (this.operator_selector)
791                 this.operator_selector.destroy();
792
793             dojo.destroy(this.tr);
794         };
795
796         this.initialize = function(initializer) {
797             /* and_then is a nasty kludge callback called once at onChange */
798             this.field_selector.and_then = dojo.hitch(
799                 this, function() {
800                     this.operator_selector.attr("value", initializer.operator);
801
802                     /* Caller supplies value for one value, values (array) for
803                      * multiple. */
804                     if (typeof initializer.value !== "undefined" &&
805                             !initializer.values) {
806                         initializer.values = [initializer.value];
807                     }
808                     initializer.values = initializer.values || [];
809
810                     if (initializer.operator.match(/^(not ?)in$/)) {
811                         /* "in" and "not in" need special treatement */
812                         dojo.forEach(
813                             initializer.values, dojo.hitch(this, function(v) {
814                                 _add_or_at_least_select(
815                                     v, this.value_widgets[0]
816                                 );
817                             })
818                         );
819                     } else {
820                         /* other operators work this way: */
821                         for (var i = 0; i < initializer.values.length; i++) {
822                             this.value_widgets[i].widget.attr(
823                                 "value", initializer.values[i]
824                             );
825                         }
826                     }
827                 }
828             );
829             this.field_selector.attr("value", initializer.field);
830         };
831
832         this.is_unset = function() {
833             return !Boolean(this.field_selector.attr("value"));
834         };
835
836         this._init.apply(this, arguments);
837     }
838
839     dojo.declare(
840         "openils.widget.PCrudFilterPane", [openils.widget.AutoWidget],
841         {
842             "useDiv": null, /* should always be null for subclass dialogs */
843             "initializers": null,
844             "widgetBuilders": null,
845             "suppressFilterFields": null,
846             "savedFiltersInterface": null,
847
848             "constructor": function(args) {
849                 for(var k in args)
850                     this[k] = args[k];
851                 this.widgetIndex = 0;
852                 this.widgetCache = {};
853                 this.compact = Boolean(this.useDiv);
854
855                 /* Meaningless in a pane, but better here than in
856                  * PCrudFilterDialog so that we don't need to load i18n
857                  * strings there: */
858                 this.title = this.title || pcFilterLocaleStrings.DEFAULT_DIALOG_TITLE;
859             },
860
861             "_buildSavedFilterControlsIfPerms": function(holder) {
862                 (new openils.User()).getPermOrgList(
863                     "SAVED_FILTER_DIALOG_FILTERS",
864                     dojo.hitch(this, function(id_list) {
865                         this._buildSavedFilterControls(id_list, holder);
866                     }),
867                     true, true
868                 );
869             },
870
871             "_buildSavedFilterControls": function(id_list, holder) {
872                 if (!id_list || !id_list.length) {
873                     console.info("Not showing saved filter controls; no perm");
874                     return;
875                 }
876
877                 var fs_list = (new openils.PermaCrud()).search(
878                     "cfdfs", {
879                         "owning_lib": id_list,
880                         "interface": this.savedFiltersInterface
881                     }, {
882                         "order_by": [
883                             {"class": "cfdfs", "field": "owning_lib"},
884                             {"class": "cfdfs", "field": "name"}
885                         ],
886                         "async": true,
887                         "oncomplete": dojo.hitch(this, function(r) {
888                             if (r = openils.Util.readResponse(r)) {
889                                 this._buildSavedFilterLoader(r, holder);
890                             }
891                         })
892                     }
893                 );
894
895                 this._buildSavedFilterSaver(holder);
896             },
897
898             "_buildSavedFilterLoader": function(fs_list, holder) {
899                 var self = this;
900                 var load_content = dojo.create(
901                     "div", {
902                         "innerHTML": pcFilterLocaleStrings.CHOOSE_FILTER_TO_LOAD
903                     }
904                 );
905
906                 var selector = dojo.create(
907                     "select", {
908                         "multiple": "multiple",
909                         "size": 4,
910                         "style": {
911                             "verticalAlign": "middle", "margin": "0 0.75em"
912                         }
913                     }, load_content, "last"
914                 );
915
916                 dojo.forEach(
917                     fs_list, function(fs) {
918                         dojo.create(
919                             "option", {
920                                 "innerHTML": fs.name(),
921                                 "value": dojo.toJson([fs.id(),
922                                     dojo.fromJson(fs.filters())])
923                             }, selector
924                         );
925                     }
926                 );
927
928                 var applicator = dojo.create(
929                     "a", {
930                         "href": "javascript:void(0);",
931                         "onclick": function() {
932                             dojo.filter(
933                                 selector.options,
934                                 function(o){return o.selected;}
935                             ).map(
936                                 function(o){return dojo.fromJson(o.value)[1];}
937                             ).forEach(
938                                 function(o){
939                                     o.forEach(
940                                         function(p) {
941                                             self.filter_row_manager.add_row(p);
942                                         }
943                                     );
944                                 }
945                             );
946                             dijit.popup.close(self.filter_set_loader.dropDown);
947                         },
948                         "innerHTML": pcFilterLocaleStrings.APPLY
949                     }, load_content, "last"
950                 );
951
952                 this.filter_set_loader = new dijit.form.DropDownButton({
953                     "dropDown": new dijit.TooltipDialog({
954                         "content": load_content
955                     }),
956                     "label": pcFilterLocaleStrings.LOAD_FILTERS
957                 }, dojo.create("span", {}, holder));
958             },
959
960             "_buildSavedFilterSaver": function(holder) {
961                 this.filter_set_loader = new dijit.form.Button({
962                     "onClick": dojo.hitch(
963                         this, function() {
964                             this.saveFilters(
965                                 /* XXX I know some find prompt() objectionable
966                                  * somehow, but I can't seem to type into any
967                                  * text inputs that I put inside TooltipDialog
968                                  * instances, so meh. */
969                                 prompt(
970                                     pcFilterLocaleStrings.NAME_SAVED_FILTER_SET
971                                 )
972                             );
973                         }
974                     ),
975                     "label": pcFilterLocaleStrings.SAVE_FILTERS
976                 }, dojo.create("span", {}, holder));
977             },
978
979             "_buildButtons": function() {
980                 var self = this;
981
982                 var button_holder = dojo.create(
983                     "div", {
984                         "className": "oils-pcrudfilterdialog-buttonholder"
985                     }, this.domNode
986                 );
987
988                 new dijit.form.Button(
989                     {
990                         "label": pcFilterLocaleStrings.ADD_ROW,
991                         "scrollOnFocus": false, /* almost always better */
992                         "onClick": function() {
993                             self.filter_row_manager.add_row();
994                         }
995                     }, dojo.create("span", {}, button_holder)
996                 );
997
998                 this._apply_button = new dijit.form.Button(
999                     {
1000                         "label": pcFilterLocaleStrings.APPLY,
1001                         "scrollOnFocus": false,
1002                         "onClick": function() { self.doApply(); }
1003                     }, dojo.create("span", {}, button_holder)
1004                 );
1005
1006                 if (!this.useDiv) {
1007                     new dijit.form.Button(
1008                         {
1009                             "label": pcFilterLocaleStrings.CANCEL,
1010                             "scrollOnFocus": false,
1011                             "onClick": function() {
1012                                 if (self.onCancel)
1013                                     self.onCancel();
1014                                 self.hide();
1015                             }
1016                         }, dojo.create("span", {}, button_holder)
1017                     );
1018                 }
1019
1020                 if (this.savedFiltersInterface)
1021                     this._buildSavedFilterControlsIfPerms(button_holder);
1022             },
1023
1024             "_buildFieldStore": function() {
1025                 var self = this;
1026                 var realFieldList = this.sortedFieldList.filter(
1027                     function(item) { return !(item.virtual || item.nonIdl); }
1028                 );
1029
1030                 /* Prevent any explicitly unwanted fields from being available
1031                  * in our field dropdowns. */
1032                 if (dojo.isArray(this.suppressFilterFields)) {
1033                     realFieldList = realFieldList.filter(
1034                         function(item) {
1035                             for (
1036                                 var i = 0;
1037                                 i < self.suppressFilterFields.length;
1038                                 i++
1039                             ) {
1040                                 if (item.name == self.suppressFilterFields[i])
1041                                     return false;
1042                             }
1043                             return true;
1044                         }
1045                     );
1046                 }
1047
1048                 this.fieldStore = new dojo.data.ItemFileReadStore({
1049                     "data": {
1050                         "identifier": "name",
1051                         "name": "label",
1052                         "items": realFieldList.map(
1053                             function(item) {
1054                                 return {
1055                                     "label": item.label,
1056                                     "name": item.name,
1057                                     "type": item.datatype
1058                                 };
1059                             }
1060                         )
1061                     }
1062                 });
1063             },
1064
1065             "saveFilters": function(name, oncomplete) {
1066                 var filters_value = this.filter_row_manager.serialize();
1067                 var filter_set = new cfdfs();
1068                 filter_set.name(name);
1069                 filter_set.interface(this.savedFiltersInterface);
1070                 filter_set.owning_lib(openils.User.user.ws_ou());
1071                 filter_set.creator(openils.User.user.id()); /* not reliable */
1072                 filter_set.filters(filters_value);
1073
1074                 (new openils.PermaCrud()).create(
1075                     filter_set, {
1076                         "oncomplete": dojo.hitch(this, function() {
1077                             var selector = dojo.query(
1078                                 "select[multiple]",
1079                                 this.filter_set_loader.dropDown.domNode
1080                             )[0];
1081                             dojo.create(
1082                                 "option", {
1083                                     "innerHTML": name,
1084                                     "value": dojo.toJson([-1,
1085                                         dojo.fromJson(filters_value)])
1086                                 }, selector
1087                             );
1088                             if (oncomplete) oncomplete();
1089                         })
1090                     }
1091                 );
1092             },
1093
1094             "hide": function() {
1095                 try {
1096                     this.inherited(arguments);
1097                 } catch (E) {
1098                     /* When using *FilterPane directly (without a *Dialog
1099                      * subclass), do nothing.  */
1100                     void(0);
1101                 }
1102             },
1103
1104             /* All we really do here is create a data store out of the fields
1105              * from the IDL for our given class, place a few buttons at the
1106              * bottom of the dialog, and hand off to PCrudFilterRowManager to
1107              * do the actual work.
1108              */
1109
1110             "startup": function() {
1111                 if (this.useDiv)
1112                     this.domNode = this.useDiv;
1113
1114                 try {
1115                     this.inherited(arguments);
1116                 } catch (E) {
1117                     /* When using *FilterPane directly (without a *Dialog
1118                      * subclass), there is no startup method in any ancestor
1119                      * class. XXX Refactor?
1120                      */
1121                     void(0);
1122                 }
1123
1124                 this.initAutoEnv();
1125
1126                 this._buildFieldStore();
1127
1128                 this.filter_row_manager = new PCrudFilterRowManager(
1129                     dojo.create("div", {}, this.domNode),
1130                     this.fieldStore, this.fmClass, this.compact,
1131                     this.widgetBuilders,
1132                     Boolean(this.initializers)  /* avoid adding empty row */,
1133                     dojo.hitch(this, function() { this.doApply(); })
1134                 );
1135
1136                 this._buildButtons();
1137
1138                 if (this.initializers) {
1139                     this.initializers.forEach(
1140                         dojo.hitch(this, function(initializer) {
1141                             this.filter_row_manager.add_row(initializer);
1142                         })
1143                     );
1144                 }
1145             },
1146
1147             /* This should just be named 'apply', but that is kind of a special
1148              * word in Javascript, no? */
1149             "doApply": function() {
1150                 this._apply_button.attr("disabled", true);
1151
1152                 var _E; /* Try pretty hard not to leave the apply button
1153                            disabled forever, even if 'apply' blows up. */
1154                 try {
1155                     if (this.onApply)
1156                         this.onApply(this.filter_row_manager.compile());
1157                 } catch (E) {
1158                     _E = E;
1159                 }
1160                 this.hide();
1161                 this._apply_button.attr("disabled", false);
1162
1163                 if (_E) throw _E;
1164             }
1165         }
1166     );
1167 }