1 if (!dojo._hasResource['openils.widget.PCrudFilterPane']) {
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.
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
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.
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.
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');
41 dojo.requireLocalization("openils.widget", "PCrudFilterPane");
43 var pcFilterLocaleStrings = dojo.i18n.getLocalization(
44 "openils.widget", "PCrudFilterPane"
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"
52 var _operator_store = new dojo.data.ItemFileReadStore(
59 "label": pcFilterLocaleStrings.OPERATOR_EQ,
65 "label": pcFilterLocaleStrings.OPERATOR_NE,
71 "label": pcFilterLocaleStrings.OPERATOR_IS_NULL,
77 "label": pcFilterLocaleStrings.OPERATOR_IS_NOT_NULL,
83 "label": pcFilterLocaleStrings.OPERATOR_GT,
88 "label": pcFilterLocaleStrings.OPERATOR_LT,
93 "label": pcFilterLocaleStrings.OPERATOR_GTE,
98 "label": pcFilterLocaleStrings.OPERATOR_LTE,
103 "label": pcFilterLocaleStrings.OPERATOR_BETWEEN,
107 "name": "not between",
108 "label": pcFilterLocaleStrings.OPERATOR_NOT_BETWEEN,
113 "label": pcFilterLocaleStrings.OPERATOR_LIKE,
117 "label": pcFilterLocaleStrings.OPERATOR_NOT_LIKE,
125 /* The text datatype supports all the above operators for comparisons. */
126 var _store_query_by_datatype = {"text": {}};
128 /* These three datatypes support only minimal operators. */
129 ["bool", "link", "org_unit"].forEach(
131 _store_query_by_datatype[type] = {"minimal": true};
135 /* These datatypes support strict operators (everything save [not] like). */
136 ["float", "id", "int", "interval", "money", "number", "timestamp"].forEach(
138 _store_query_by_datatype[type] = {"strict": true};
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 if (clause === null) return false; /* early out for special operator */
152 var ops = openils.Util.objectProperties(clause);
154 var matches = op.match(/^not (\w+)$/);
156 clause[matches[1]] = clause[op];
163 /* This is not the dijit per se. Search further in this file for
164 * "dojo.declare" for the beginning of the dijit.
166 * This is, however, the object that represents a collection of filter
167 * rows and knows how to compile a filter from those rows. */
168 function PCrudFilterRowManager() {
171 this._init = function(
172 container, field_store, fm_class, compact, widget_builders,
173 skip_first_add_row, do_apply
175 this.container = container;
176 this.field_store = field_store;
177 this.fm_class = fm_class;
178 this.compact = compact;
179 this.widget_builders = widget_builders || {};
180 this.skip_first_add_row = skip_first_add_row;
181 this.do_apply = do_apply;
189 this._build_table = function() {
190 this.table = dojo.create(
192 "className": "oils-pcrudfilterdialog-table"
196 var tr = dojo.create(
198 "id": "pcrudfilterdialog-empty",
199 "className": "hidden"
206 "innerHTML": pcFilterLocaleStrings[
207 this.compact ? "EMPTY_CASE_COMPACT" : "EMPTY_CASE"
212 if (!this.skip_first_add_row)
216 this._compile_second_pass = function(first_pass) {
218 var result = {"-and": and};
220 for (var field in first_pass) {
221 var list = first_pass[field];
222 if (list.length == 1) {
224 var clause = list.pop();
225 if (_clause_was_negative(clause)) {
227 obj["-not"][field] = clause;
236 if (_clause_was_negative(clause)) {
238 obj["-not"][field] = clause;
245 and.push({"-or": or});
252 this.add_row = function(initializer) {
253 this.hide_empty_placeholder();
254 var row_id = this.row_index++;
255 this.rows[row_id] = new PCrudFilterRow(this, row_id, initializer);
258 this.remove_row = function(row_id) {
259 this.rows[row_id].destroy();
260 delete this.rows[row_id];
262 if (openils.Util.objectProperties(this.rows).length < 1)
263 this.show_empty_placeholder();
269 this.hide_empty_placeholder = function() {
270 openils.Util.hide("pcrudfilterdialog-empty");
273 this.show_empty_placeholder = function() {
274 openils.Util.show("pcrudfilterdialog-empty");
277 this.compile = function() {
278 /* We'll prepare a first-pass data structure that looks like:
280 * field1: [{"op": "one value"}],
281 * field2: [{"op": "a value"}, {"op": "b value"}],
282 * field3: [{"op": "first value"}, {"op": ["range start", "range end"]}]
285 * which will be passed to _compile_second_pass() to yield an
286 * actual filter suitable for pcrud (with -and and -or in all the
287 * right places) so the above example would come out like:
290 * {"field1": {"op": "one value"}},
291 * {"-or": [ {"field2": {"op": "a value"}}, {"field2": {"op": "b value"}} ] },
293 * {"field3": {"op": "first value"}},
294 * {"field3": {"op": ["range start", "range end"]}}
300 for (var row_id in this.rows) {
301 var row = this.rows[row_id];
302 var value = row.compile();
303 var field = row.selected_field;
305 if (typeof(value) != "undefined" &&
306 typeof(field) != "undefined") {
307 if (!first_pass[field])
308 first_pass[field] = [];
309 first_pass[field].push(value);
313 /* Don't return an empty filter: pcrud can't use that. */
314 if (openils.Util.objectProperties(first_pass).length < 1) {
316 result[fieldmapper[this.fm_class].Identifier] = {"!=": null};
319 return this._compile_second_pass(first_pass);
323 this._init.apply(this, arguments);
326 /* As the name implies, objects of this class manage a single row of the
327 * query. Therefore they know about their own field dropdown, their own
328 * selector dropdown, and their own value widget (or widgets in the case
329 * of between searches, which call for two widgets to define a range),
330 * and not much else. */
331 function PCrudFilterRow() {
334 this._init = function(filter_row_manager, row_id, initializer) {
335 this.filter_row_manager = filter_row_manager;
336 this.row_id = row_id;
338 if (this.filter_row_manager.compact)
339 this._build_compact();
344 this.initialize(initializer);
347 this._build = function() {
348 this.tr = dojo.create("tr", {}, this.filter_row_manager.table);
350 this._create_field_selector();
351 this._create_operator_selector();
352 this._create_value_slot();
353 this._create_remover();
356 this._build_compact = function() {
357 this.tr = dojo.create("tr", {}, this.filter_row_manager.table);
359 var td = dojo.create("td", {}, this.tr);
361 this._create_field_selector(td);
362 this._create_operator_selector(td);
364 dojo.create("br", {}, td);
365 this._create_value_slot(td);
369 {"className": "oils-pcrudfilterdialog-remover-holder"},
373 this._create_remover(td);
376 this._create_field_selector = function(use_element) {
377 var td = use_element || dojo.create("td", {}, this.tr);
379 this.field_selector = new dijit.form.FilteringSelect(
381 "labelAttr": "label",
382 "searchAttr": "label",
383 "scrollOnFocus": false,
384 "onChange": function(value) {
385 self.update_selected_field(value);
387 "store": this.filter_row_manager.field_store
388 }, dojo.create("span", {}, td)
392 this._create_operator_selector = function(use_element) {
393 var td = use_element || dojo.create("td", {}, this.tr);
395 this.operator_selector = new dijit.form.FilteringSelect(
397 "labelAttr": "label",
398 "searchAttr": "label",
399 "scrollOnFocus": false,
400 "onChange": function(value) {
401 self.update_selected_operator(value);
403 "store": _operator_store
404 }, dojo.create("span", {}, td)
408 this._adjust_operator_selector = function() {
409 this.operator_selector.attr(
410 "query", _store_query_by_datatype[this.selected_field_type]
412 this.operator_selector.reset();
415 this._create_value_slot = function(use_element) {
417 this.value_slot = dojo.create(
418 "span", {"innerHTML": "-"}, use_element
421 this.value_slot = dojo.create("td",{"innerHTML":"-"},this.tr);
424 this._create_remover = function(use_element) {
425 var td = use_element || dojo.create("td", {}, this.tr);
426 var anchor = dojo.create(
428 "className": "oils-pcrudfilterdialog-remover",
431 "onclick": function() {
432 self.filter_row_manager.remove_row(self.row_id);
438 this._clear_value_slot = function() {
439 if (this.value_widgets) {
440 this.value_widgets.forEach(
441 function(autowidg) { autowidg.widget.destroy(); }
443 delete this.value_widgets;
446 dojo.empty(this.value_slot);
449 this._rebuild_value_widgets = function() {
450 this._clear_value_slot();
452 if (!this.get_selected_operator() || !this.selected_field)
455 this.value_widgets = [];
457 var param_count = this.operator_selector.item.param_count;
459 /* This is where find and deploy custom widget builders. */
460 var widget_builder_key = this.selected_field_fm_class + ":" +
461 this.selected_field_fm_field;
463 this.filter_row_manager.widget_builders[widget_builder_key] ||
464 openils.widget.AutoFieldWidget;
466 for (var i = 0; i < param_count; i++) {
467 var widg = new constr({
468 "fmClass": this.selected_field_fm_class,
469 "fmField": this.selected_field_fm_field,
470 "parentNode": dojo.create("span", {}, this.value_slot),
471 "dijitArgs": {"scrollOnFocus": false}
475 this.value_widgets.push(widg);
479 /* for ugly special cases in compilation */
480 this._null_clause = function() {
481 var opname = this.get_selected_operator_name();
482 if (opname == "not null")
484 else if (opname == "null")
490 /* wrap s in %'s unless it already contains at least one %. */
491 this._add_like_wildcards = function(s) {
492 return s.indexOf("%") == -1 ? ("%" + s + "%") : s;
495 this.get_selected_operator = function() {
496 if (this.operator_selector)
497 return this.operator_selector.item;
500 this.get_selected_operator_name = function() {
501 var op = this.get_selected_operator();
502 return op ? op.name : null;
505 this.update_selected_operator = function(value) {
506 this._rebuild_value_widgets();
509 this.update_selected_field = function(value) {
510 if (this.field_selector.item) {
511 this.selected_field = value;
512 this.selected_field_type = this.field_selector.item.type;
514 /* This is really about supporting flattenergrid, of which
515 * we're in the superclass (in a sloppy sad way). From now
516 * on I won't mix this kind of lazy object with Dojo modules. */
517 this.selected_field_fm_field = this.field_selector.item.name;
518 this.selected_field_is_indirect =
519 this.field_selector.item.indirect || false;
520 this.selected_field_fm_class =
521 this.field_selector.item.fmClass ||
522 this.filter_row_manager.fm_class;
524 this._adjust_operator_selector();
525 this._rebuild_value_widgets();
529 this.compile = function() {
530 if (this.value_widgets) {
531 var values = this.value_widgets.map(
533 if (widg.useCorrectly)
534 return widg.widget.attr("value");
535 else if (self.selected_field_is_indirect)
536 return widg.widget.attr("displayedValue");
538 return widg.getFormattedValue();
542 if (!values.length) {
543 return this._null_clause(); /* null/not null */
546 var op = this.get_selected_operator_name();
548 var prep_function = function(o) { return o; /* no-op */ };
549 if (String(op).match(/like/))
550 prep_function = this._add_like_wildcards;
552 if (values.length == 1)
553 clause[op] = prep_function(values.pop());
555 clause[op] = dojo.map(values, prep_function);
563 this.destroy = function() {
564 this._clear_value_slot();
565 this.field_selector.destroy();
566 if (this.operator_selector)
567 this.operator_selector.destroy();
569 dojo.destroy(this.tr);
572 this.initialize = function(initializer) {
573 this.field_selector.attr("value", initializer.field);
574 this.operator_selector.attr("value", initializer.operator);
576 /* Caller supplies value for one value, values (array) for
578 if (!initializer.values || !dojo.isArray(initializer.values))
579 initializer.values = [initializer.values || initializer.value];
581 for (var i = 0; i < initializer.values.length; i++) {
582 this.value_widgets[i].widget.attr(
583 "value", initializer.values[i]
588 this._init.apply(this, arguments);
592 "openils.widget.PCrudFilterPane", [openils.widget.AutoWidget],
594 "useDiv": null, /* should always be null for subclass dialogs */
595 "initializers": null,
597 "widgetBuilders": null,
598 "suppressFilterFields": null,
600 "constructor": function(args) {
603 this.widgetIndex = 0;
604 this.widgetCache = {};
606 /* Meaningless in a pane, but better here than in
607 * PCrudFilterDialog so that we don't need to load i18n
609 this.title = this.title || pcFilterLocaleStrings.DEFAULT_DIALOG_TITLE;
612 "_buildButtons": function() {
615 var button_holder = dojo.create(
617 "className": "oils-pcrudfilterdialog-buttonholder"
621 new dijit.form.Button(
623 "label": pcFilterLocaleStrings.ADD_ROW,
624 "scrollOnFocus": false, /* almost always better */
625 "onClick": function() {
626 self.filter_row_manager.add_row();
628 }, dojo.create("span", {}, button_holder)
631 this._apply_button = new dijit.form.Button(
633 "label": pcFilterLocaleStrings.APPLY,
634 "scrollOnFocus": false,
635 "onClick": function() { self.doApply(); }
636 }, dojo.create("span", {}, button_holder)
640 new dijit.form.Button(
642 "label": pcFilterLocaleStrings.CANCEL,
643 "scrollOnFocus": false,
644 "onClick": function() {
649 }, dojo.create("span", {}, button_holder)
654 "_buildFieldStore": function() {
656 var realFieldList = this.sortedFieldList.filter(
657 function(item) { return !(item.virtual || item.nonIdl); }
660 /* Prevent any explicitly unwanted fields from being available
661 * in our field dropdowns. */
662 if (dojo.isArray(this.suppressFilterFields)) {
663 realFieldList = realFieldList.filter(
667 i < self.suppressFilterFields.length;
670 if (item.name == self.suppressFilterFields[i])
678 this.fieldStore = new dojo.data.ItemFileReadStore({
680 "identifier": "name",
682 "items": realFieldList.map(
687 "type": item.datatype
697 this.inherited(arguments);
699 /* When using *FilterPane directly (without a *Dialog
700 * subclass), do nothing. */
705 /* All we really do here is create a data store out of the fields
706 * from the IDL for our given class, place a few buttons at the
707 * bottom of the dialog, and hand off to PCrudFilterRowManager to
708 * do the actual work.
711 "startup": function() {
713 this.domNode = this.useDiv;
716 this.inherited(arguments);
718 /* When using *FilterPane directly (without a *Dialog
719 * subclass), there is no startup method in any ancestor
720 * class. XXX Refactor?
727 this._buildFieldStore();
729 this.filter_row_manager = new PCrudFilterRowManager(
730 dojo.create("div", {}, this.domNode),
731 this.fieldStore, this.fmClass, this.compact,
733 Boolean(this.initializers) /* avoid adding empty row */,
734 dojo.hitch(this, function() { this.doApply(); })
737 this._buildButtons();
739 if (this.initializers) {
740 this.initializers.forEach(
741 dojo.hitch(this, function(initializer) {
742 this.filter_row_manager.add_row(initializer);
748 /* This should just be named 'apply', but that is kind of a special
749 * word in Javascript, no? */
750 "doApply": function() {
751 this._apply_button.attr("disabled", true);
753 var _E; /* Try pretty hard not to leave the apply button
754 disabled forever, even if 'apply' blows up. */
757 this.onApply(this.filter_row_manager.compile());
762 this._apply_button.attr("disabled", false);