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 /* XXX namespace pollution! arg! Fix this whole module sometime. */
44 var localeStrings = dojo.i18n.getLocalization(
45 "openils.widget", "PCrudFilterPane"
48 /* These are the operators that make up the central dropdown in each
49 * row of the widget. When fields of different datatypes are selected,
50 * some of these operators may be masked via the "minimal" and "strict"
53 var _operator_store = new dojo.data.ItemFileReadStore(
60 "label": localeStrings.OPERATOR_EQ,
66 "label": localeStrings.OPERATOR_NE,
72 "label": localeStrings.OPERATOR_IS_NULL,
78 "label": localeStrings.OPERATOR_IS_NOT_NULL,
84 "label": localeStrings.OPERATOR_GT,
89 "label": localeStrings.OPERATOR_LT,
94 "label": localeStrings.OPERATOR_GTE,
99 "label": localeStrings.OPERATOR_LTE,
104 "label": localeStrings.OPERATOR_BETWEEN,
108 "name": "not between",
109 "label": localeStrings.OPERATOR_NOT_BETWEEN,
114 "label": localeStrings.OPERATOR_LIKE,
118 "label": localeStrings.OPERATOR_NOT_LIKE,
126 /* The text datatype supports all the above operators for comparisons. */
127 var _store_query_by_datatype = {"text": {}};
129 /* These three datatypes support only minimal operators. */
130 ["bool", "link", "org_unit"].forEach(
132 _store_query_by_datatype[type] = {"minimal": true};
136 /* These datatypes support strict operators (everything save [not] like). */
137 ["float", "id", "int", "interval", "money", "number", "timestamp"].forEach(
139 _store_query_by_datatype[type] = {"strict": true};
143 /* This helps convert things that pcrud won't accept ("not between", "not
144 * like") into proper JSON query expressions.
145 * It returns false if a clause doesn't have any such negative operator,
146 * or it returns true AND gets rid of the "not " part in the clause
147 * object itself. It's up to the caller to wrap it in {"-not": {}} in
148 * the right place. */
149 function _clause_was_negative(clause) {
150 /* clause objects really only ever have one property */
151 var ops = openils.Util.objectProperties(clause);
153 var matches = op.match(/^not (\w+)$/);
155 clause[matches[1]] = clause[op];
162 /* This is not the dijit per se. Search further in this file for
163 * "dojo.declare" for the beginning of the dijit.
165 * This is, however, the object that represents a collection of filter
166 * rows and knows how to compile a filter from those rows. */
167 function PCrudFilterRowManager() {
170 this._init = function(
171 container, field_store, fm_class, compact, widget_builders,
172 skip_first_add_row, do_apply
174 this.container = container;
175 this.field_store = field_store;
176 this.fm_class = fm_class;
177 this.compact = compact;
178 this.widget_builders = widget_builders || {};
179 this.skip_first_add_row = skip_first_add_row;
180 this.do_apply = do_apply;
188 this._build_table = function() {
189 this.table = dojo.create(
191 "className": "oils-pcrudfilterdialog-table"
195 var tr = dojo.create(
197 "id": "pcrudfilterdialog-empty",
198 "className": "hidden"
205 "innerHTML": localeStrings[
206 this.compact ? "EMPTY_CASE_COMPACT" : "EMPTY_CASE"
211 if (!this.skip_first_add_row)
215 this._compile_second_pass = function(first_pass) {
217 var result = {"-and": and};
219 for (var field in first_pass) {
220 var list = first_pass[field];
221 if (list.length == 1) {
223 var clause = list.pop();
224 if (_clause_was_negative(clause)) {
226 obj["-not"][field] = clause;
235 if (_clause_was_negative(clause)) {
237 obj["-not"][field] = clause;
244 and.push({"-or": or});
251 this.add_row = function(initializer) {
252 this.hide_empty_placeholder();
253 var row_id = this.row_index++;
254 this.rows[row_id] = new PCrudFilterRow(this, row_id, initializer);
257 this.remove_row = function(row_id) {
258 this.rows[row_id].destroy();
259 delete this.rows[row_id];
261 if (openils.Util.objectProperties(this.rows).length < 1)
262 this.show_empty_placeholder();
268 this.hide_empty_placeholder = function() {
269 openils.Util.hide("pcrudfilterdialog-empty");
272 this.show_empty_placeholder = function() {
273 openils.Util.show("pcrudfilterdialog-empty");
276 this.compile = function() {
277 /* We'll prepare a first-pass data structure that looks like:
279 * field1: [{"op": "one value"}],
280 * field2: [{"op": "a value"}, {"op": "b value"}],
281 * field3: [{"op": "first value"}, {"op": ["range start", "range end"]}]
284 * which will be passed to _compile_second_pass() to yield an
285 * actual filter suitable for pcrud (with -and and -or in all the
286 * right places) so the above example would come out like:
289 * {"field1": {"op": "one value"}},
290 * {"-or": [ {"field2": {"op": "a value"}}, {"field2": {"op": "b value"}} ] },
292 * {"field3": {"op": "first value"}},
293 * {"field3": {"op": ["range start", "range end"]}}
299 for (var row_id in this.rows) {
300 var row = this.rows[row_id];
301 var value = row.compile();
302 var field = row.selected_field;
304 if (typeof(value) != "undefined" &&
305 typeof(field) != "undefined") {
306 if (!first_pass[field])
307 first_pass[field] = [];
308 first_pass[field].push(value);
312 /* Don't return an empty filter: pcrud can't use that. */
313 if (openils.Util.objectProperties(first_pass).length < 1) {
315 result[fieldmapper[this.fm_class].Identifier] = {"!=": null};
318 return this._compile_second_pass(first_pass);
322 this._init.apply(this, arguments);
325 /* As the name implies, objects of this class manage a single row of the
326 * query. Therefore they know about their own field dropdown, their own
327 * selector dropdown, and their own value widget (or widgets in the case
328 * of between searches, which call for two widgets to define a range),
329 * and not much else. */
330 function PCrudFilterRow() {
333 this._init = function(filter_row_manager, row_id, initializer) {
334 this.filter_row_manager = filter_row_manager;
335 this.row_id = row_id;
337 if (this.filter_row_manager.compact)
338 this._build_compact();
343 this.initialize(initializer);
346 this._build = function() {
347 this.tr = dojo.create("tr", {}, this.filter_row_manager.table);
349 this._create_field_selector();
350 this._create_operator_selector();
351 this._create_value_slot();
352 this._create_remover();
355 this._build_compact = function() {
356 this.tr = dojo.create("tr", {}, this.filter_row_manager.table);
358 var td = dojo.create("td", {}, this.tr);
360 this._create_field_selector(td);
361 this._create_operator_selector(td);
363 dojo.create("br", {}, td);
364 this._create_value_slot(td);
368 {"className": "oils-pcrudfilterdialog-remover-holder"},
372 this._create_remover(td);
375 this._create_field_selector = function(use_element) {
376 var td = use_element || dojo.create("td", {}, this.tr);
378 this.field_selector = new dijit.form.FilteringSelect(
380 "labelAttr": "label",
381 "searchAttr": "label",
382 "scrollOnFocus": false,
383 "onChange": function(value) {
384 self.update_selected_field(value);
386 "store": this.filter_row_manager.field_store
387 }, dojo.create("span", {}, td)
391 this._create_operator_selector = function(use_element) {
392 var td = use_element || dojo.create("td", {}, this.tr);
394 this.operator_selector = new dijit.form.FilteringSelect(
396 "labelAttr": "label",
397 "searchAttr": "label",
398 "scrollOnFocus": false,
399 "onChange": function(value) {
400 self.update_selected_operator(value);
402 "store": _operator_store
403 }, dojo.create("span", {}, td)
407 this._adjust_operator_selector = function() {
408 this.operator_selector.attr(
409 "query", _store_query_by_datatype[this.selected_field_type]
411 this.operator_selector.reset();
414 this._create_value_slot = function(use_element) {
416 this.value_slot = dojo.create(
417 "span", {"innerHTML": "-"}, use_element
420 this.value_slot = dojo.create("td",{"innerHTML":"-"},this.tr);
423 this._create_remover = function(use_element) {
424 var td = use_element || dojo.create("td", {}, this.tr);
425 var anchor = dojo.create(
427 "className": "oils-pcrudfilterdialog-remover",
430 "onclick": function() {
431 self.filter_row_manager.remove_row(self.row_id);
437 this._clear_value_slot = function() {
438 if (this.value_widgets) {
439 this.value_widgets.forEach(
440 function(autowidg) { autowidg.widget.destroy(); }
442 delete this.value_widgets;
445 dojo.empty(this.value_slot);
448 this._rebuild_value_widgets = function() {
449 this._clear_value_slot();
451 if (!this.get_selected_operator() || !this.selected_field)
454 this.value_widgets = [];
456 var param_count = this.operator_selector.item.param_count;
458 /* This is where find and deploy custom widget builders. */
459 var widget_builder_key = this.selected_field_fm_class + ":" +
460 this.selected_field_fm_field;
462 this.filter_row_manager.widget_builders[widget_builder_key] ||
463 openils.widget.AutoFieldWidget;
465 for (var i = 0; i < param_count; i++) {
466 var widg = new constr({
467 "fmClass": this.selected_field_fm_class,
468 "fmField": this.selected_field_fm_field,
469 "parentNode": dojo.create("span", {}, this.value_slot),
470 "dijitArgs": {"scrollOnFocus": false}
474 this.value_widgets.push(widg);
478 /* for ugly special cases in compliation */
479 this._null_clause = function() {
480 var opname = this.get_selected_operator_name();
481 if (opname == "not null")
483 else if (opname == "null")
489 this.get_selected_operator = function() {
490 if (this.operator_selector)
491 return this.operator_selector.item;
494 this.get_selected_operator_name = function() {
495 var op = this.get_selected_operator();
496 return op ? op.name : null;
499 this.update_selected_operator = function(value) {
500 this._rebuild_value_widgets();
503 this.update_selected_field = function(value) {
504 if (this.field_selector.item) {
505 this.selected_field = value;
506 this.selected_field_type = this.field_selector.item.type;
508 /* This is really about supporting flattenergrid, of which
509 * we're in the superclass (in a sloppy sad way). From now
510 * on I won't mix this kind of lazy object with Dojo modules. */
511 this.selected_field_fm_field = this.field_selector.item.name;
512 this.selected_field_is_indirect =
513 this.field_selector.item.indirect || false;
514 this.selected_field_fm_class =
515 this.field_selector.item.fmClass ||
516 this.filter_row_manager.fm_class;
518 this._adjust_operator_selector();
519 this._rebuild_value_widgets();
523 this.compile = function() {
524 if (this.value_widgets) {
525 var values = this.value_widgets.map(
527 if (widg.useCorrectly)
528 return widg.widget.attr("value");
529 else if (self.selected_field_is_indirect)
530 return widg.widget.attr("displayedValue");
532 return widg.getFormattedValue();
536 if (!values.length) {
537 return this._null_clause(); /* null/not null */
540 var op = this.get_selected_operator_name();
541 if (values.length == 1)
542 clause[op] = values.pop();
552 this.destroy = function() {
553 this._clear_value_slot();
554 this.field_selector.destroy();
555 if (this.operator_selector)
556 this.operator_selector.destroy();
558 dojo.destroy(this.tr);
561 this.initialize = function(initializer) {
562 this.field_selector.attr("value", initializer.field);
563 this.operator_selector.attr("value", initializer.operator);
565 /* Caller supplies value for one value, values (array) for
567 if (!initializer.values || !dojo.isArray(initializer.values))
568 initializer.values = [initializer.values || initializer.value];
570 for (var i = 0; i < initializer.values.length; i++) {
571 this.value_widgets[i].widget.attr(
572 "value", initializer.values[i]
577 this._init.apply(this, arguments);
581 "openils.widget.PCrudFilterPane", [openils.widget.AutoWidget],
583 "useDiv": null, /* should always be null for subclass dialogs */
584 "initializers": null,
586 "widgetBuilders": null,
588 "constructor": function(args) {
591 this.widgetIndex = 0;
592 this.widgetCache = {};
594 /* Meaningless in a pane, but better here than in
595 * PCrudFilterDialog so that we don't need to load i18n
597 this.title = this.title || localeStrings.DEFAULT_DIALOG_TITLE;
600 "_buildButtons": function() {
603 var button_holder = dojo.create(
605 "className": "oils-pcrudfilterdialog-buttonholder"
609 new dijit.form.Button(
611 "label": localeStrings.ADD_ROW,
612 "scrollOnFocus": false, /* almost always better */
613 "onClick": function() {
614 self.filter_row_manager.add_row();
616 }, dojo.create("span", {}, button_holder)
619 this._apply_button = new dijit.form.Button(
621 "label": localeStrings.APPLY,
622 "scrollOnFocus": false,
623 "onClick": function() { self.doApply(); }
624 }, dojo.create("span", {}, button_holder)
628 new dijit.form.Button(
630 "label": localeStrings.CANCEL,
631 "scrollOnFocus": false,
632 "onClick": function() {
637 }, dojo.create("span", {}, button_holder)
642 "_buildFieldStore": function() {
644 var realFieldList = this.sortedFieldList.filter(
645 function(item) { return !(item.virtual || item.nonIdl); }
648 /* Prevent any explicitly unwanted fields from being available
649 * in our field dropdowns. */
650 if (dojo.isArray(this.suppressFilterFields)) {
651 realFieldList = realFieldList.filter(
655 i < self.suppressFilterFields.length;
658 if (item.name == self.suppressFilterFields[i])
666 this.fieldStore = new dojo.data.ItemFileReadStore({
668 "identifier": "name",
670 "items": realFieldList.map(
675 "type": item.datatype
685 this.inherited(arguments);
687 /* When using *FilterPane directly (without a *Dialog
688 * subclass), do nothing. */
693 /* All we really do here is create a data store out of the fields
694 * from the IDL for our given class, place a few buttons at the
695 * bottom of the dialog, and hand off to PCrudFilterRowManager to
696 * do the actual work.
699 "startup": function() {
701 this.domNode = this.useDiv;
704 this.inherited(arguments);
706 /* When using *FilterPane directly (without a *Dialog
707 * subclass), there is no startup method in any ancestor
708 * class. XXX Refactor?
715 this._buildFieldStore();
717 this.filter_row_manager = new PCrudFilterRowManager(
718 dojo.create("div", {}, this.domNode),
719 this.fieldStore, this.fmClass, this.compact,
721 Boolean(this.initializers) /* avoid adding empty row */,
722 dojo.hitch(this, function() { this.doApply(); })
725 this._buildButtons();
727 if (this.initializers) {
728 this.initializers.forEach(
729 dojo.hitch(this, function(initializer) {
730 this.filter_row_manager.add_row(initializer);
736 /* This should just be named 'apply', but that is kind of a special
737 * word in Javascript, no? */
738 "doApply": function() {
739 this._apply_button.attr("disabled", true);
741 var _E; /* Try pretty hard not to leave the apply button
742 disabled forever, even if 'apply' blows up. */
745 this.onApply(this.filter_row_manager.compile());
750 this._apply_button.attr("disabled", false);