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 var ops = openils.Util.objectProperties(clause);
152 var matches = op.match(/^not (\w+)$/);
154 clause[matches[1]] = clause[op];
161 /* This is not the dijit per se. Search further in this file for
162 * "dojo.declare" for the beginning of the dijit.
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() {
169 this._init = function(
170 container, field_store, fm_class, compact, widget_builders,
171 skip_first_add_row, do_apply
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;
187 this._build_table = function() {
188 this.table = dojo.create(
190 "className": "oils-pcrudfilterdialog-table"
194 var tr = dojo.create(
196 "id": "pcrudfilterdialog-empty",
197 "className": "hidden"
204 "innerHTML": pcFilterLocaleStrings[
205 this.compact ? "EMPTY_CASE_COMPACT" : "EMPTY_CASE"
210 if (!this.skip_first_add_row)
214 this._compile_second_pass = function(first_pass) {
216 var result = {"-and": and};
218 for (var field in first_pass) {
219 var list = first_pass[field];
220 if (list.length == 1) {
222 var clause = list.pop();
223 if (_clause_was_negative(clause)) {
225 obj["-not"][field] = clause;
234 if (_clause_was_negative(clause)) {
236 obj["-not"][field] = clause;
243 and.push({"-or": or});
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);
256 this.remove_row = function(row_id) {
257 this.rows[row_id].destroy();
258 delete this.rows[row_id];
260 if (openils.Util.objectProperties(this.rows).length < 1)
261 this.show_empty_placeholder();
267 this.hide_empty_placeholder = function() {
268 openils.Util.hide("pcrudfilterdialog-empty");
271 this.show_empty_placeholder = function() {
272 openils.Util.show("pcrudfilterdialog-empty");
275 this.compile = function() {
276 /* We'll prepare a first-pass data structure that looks like:
278 * field1: [{"op": "one value"}],
279 * field2: [{"op": "a value"}, {"op": "b value"}],
280 * field3: [{"op": "first value"}, {"op": ["range start", "range end"]}]
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:
288 * {"field1": {"op": "one value"}},
289 * {"-or": [ {"field2": {"op": "a value"}}, {"field2": {"op": "b value"}} ] },
291 * {"field3": {"op": "first value"}},
292 * {"field3": {"op": ["range start", "range end"]}}
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;
303 if (typeof(value) != "undefined" &&
304 typeof(field) != "undefined") {
305 if (!first_pass[field])
306 first_pass[field] = [];
307 first_pass[field].push(value);
311 /* Don't return an empty filter: pcrud can't use that. */
312 if (openils.Util.objectProperties(first_pass).length < 1) {
314 result[fieldmapper[this.fm_class].Identifier] = {"!=": null};
317 return this._compile_second_pass(first_pass);
321 this._init.apply(this, arguments);
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() {
332 this._init = function(filter_row_manager, row_id, initializer) {
333 this.filter_row_manager = filter_row_manager;
334 this.row_id = row_id;
336 if (this.filter_row_manager.compact)
337 this._build_compact();
342 this.initialize(initializer);
345 this._build = function() {
346 this.tr = dojo.create("tr", {}, this.filter_row_manager.table);
348 this._create_field_selector();
349 this._create_operator_selector();
350 this._create_value_slot();
351 this._create_remover();
354 this._build_compact = function() {
355 this.tr = dojo.create("tr", {}, this.filter_row_manager.table);
357 var td = dojo.create("td", {}, this.tr);
359 this._create_field_selector(td);
360 this._create_operator_selector(td);
362 dojo.create("br", {}, td);
363 this._create_value_slot(td);
367 {"className": "oils-pcrudfilterdialog-remover-holder"},
371 this._create_remover(td);
374 this._create_field_selector = function(use_element) {
375 var td = use_element || dojo.create("td", {}, this.tr);
377 this.field_selector = new dijit.form.FilteringSelect(
379 "labelAttr": "label",
380 "searchAttr": "label",
381 "scrollOnFocus": false,
382 "onChange": function(value) {
383 self.update_selected_field(value);
385 "store": this.filter_row_manager.field_store
386 }, dojo.create("span", {}, td)
390 this._create_operator_selector = function(use_element) {
391 var td = use_element || dojo.create("td", {}, this.tr);
393 this.operator_selector = new dijit.form.FilteringSelect(
395 "labelAttr": "label",
396 "searchAttr": "label",
397 "scrollOnFocus": false,
398 "onChange": function(value) {
399 self.update_selected_operator(value);
401 "store": _operator_store
402 }, dojo.create("span", {}, td)
406 this._adjust_operator_selector = function() {
407 this.operator_selector.attr(
408 "query", _store_query_by_datatype[this.selected_field_type]
410 this.operator_selector.reset();
413 this._create_value_slot = function(use_element) {
415 this.value_slot = dojo.create(
416 "span", {"innerHTML": "-"}, use_element
419 this.value_slot = dojo.create("td",{"innerHTML":"-"},this.tr);
422 this._create_remover = function(use_element) {
423 var td = use_element || dojo.create("td", {}, this.tr);
424 var anchor = dojo.create(
426 "className": "oils-pcrudfilterdialog-remover",
429 "onclick": function() {
430 self.filter_row_manager.remove_row(self.row_id);
436 this._clear_value_slot = function() {
437 if (this.value_widgets) {
438 this.value_widgets.forEach(
439 function(autowidg) { autowidg.widget.destroy(); }
441 delete this.value_widgets;
444 dojo.empty(this.value_slot);
447 this._rebuild_value_widgets = function() {
448 this._clear_value_slot();
450 if (!this.get_selected_operator() || !this.selected_field)
453 this.value_widgets = [];
455 var param_count = this.operator_selector.item.param_count;
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;
461 this.filter_row_manager.widget_builders[widget_builder_key] ||
462 openils.widget.AutoFieldWidget;
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}
473 this.value_widgets.push(widg);
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")
482 else if (opname == "null")
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;
493 this.get_selected_operator = function() {
494 if (this.operator_selector)
495 return this.operator_selector.item;
498 this.get_selected_operator_name = function() {
499 var op = this.get_selected_operator();
500 return op ? op.name : null;
503 this.update_selected_operator = function(value) {
504 this._rebuild_value_widgets();
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;
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;
522 this._adjust_operator_selector();
523 this._rebuild_value_widgets();
527 this.compile = function() {
528 if (this.value_widgets) {
529 var values = this.value_widgets.map(
531 if (widg.useCorrectly)
532 return widg.widget.attr("value");
533 else if (self.selected_field_is_indirect)
534 return widg.widget.attr("displayedValue");
536 return widg.getFormattedValue();
540 if (!values.length) {
541 return this._null_clause(); /* null/not null */
544 var op = this.get_selected_operator_name();
546 var prep_function = function(o) { return o; /* no-op */ };
547 if (String(op).match(/like/))
548 prep_function = this._add_like_wildcards;
550 if (values.length == 1)
551 clause[op] = prep_function(values.pop());
553 clause[op] = dojo.map(values, prep_function);
561 this.destroy = function() {
562 this._clear_value_slot();
563 this.field_selector.destroy();
564 if (this.operator_selector)
565 this.operator_selector.destroy();
567 dojo.destroy(this.tr);
570 this.initialize = function(initializer) {
571 this.field_selector.attr("value", initializer.field);
572 this.operator_selector.attr("value", initializer.operator);
574 /* Caller supplies value for one value, values (array) for
576 if (!initializer.values || !dojo.isArray(initializer.values))
577 initializer.values = [initializer.values || initializer.value];
579 for (var i = 0; i < initializer.values.length; i++) {
580 this.value_widgets[i].widget.attr(
581 "value", initializer.values[i]
586 this._init.apply(this, arguments);
590 "openils.widget.PCrudFilterPane", [openils.widget.AutoWidget],
592 "useDiv": null, /* should always be null for subclass dialogs */
593 "initializers": null,
595 "widgetBuilders": null,
597 "constructor": function(args) {
600 this.widgetIndex = 0;
601 this.widgetCache = {};
603 /* Meaningless in a pane, but better here than in
604 * PCrudFilterDialog so that we don't need to load i18n
606 this.title = this.title || pcFilterLocaleStrings.DEFAULT_DIALOG_TITLE;
609 "_buildButtons": function() {
612 var button_holder = dojo.create(
614 "className": "oils-pcrudfilterdialog-buttonholder"
618 new dijit.form.Button(
620 "label": pcFilterLocaleStrings.ADD_ROW,
621 "scrollOnFocus": false, /* almost always better */
622 "onClick": function() {
623 self.filter_row_manager.add_row();
625 }, dojo.create("span", {}, button_holder)
628 this._apply_button = new dijit.form.Button(
630 "label": pcFilterLocaleStrings.APPLY,
631 "scrollOnFocus": false,
632 "onClick": function() { self.doApply(); }
633 }, dojo.create("span", {}, button_holder)
637 new dijit.form.Button(
639 "label": pcFilterLocaleStrings.CANCEL,
640 "scrollOnFocus": false,
641 "onClick": function() {
646 }, dojo.create("span", {}, button_holder)
651 "_buildFieldStore": function() {
653 var realFieldList = this.sortedFieldList.filter(
654 function(item) { return !(item.virtual || item.nonIdl); }
657 /* Prevent any explicitly unwanted fields from being available
658 * in our field dropdowns. */
659 if (dojo.isArray(this.suppressFilterFields)) {
660 realFieldList = realFieldList.filter(
664 i < self.suppressFilterFields.length;
667 if (item.name == self.suppressFilterFields[i])
675 this.fieldStore = new dojo.data.ItemFileReadStore({
677 "identifier": "name",
679 "items": realFieldList.map(
684 "type": item.datatype
694 this.inherited(arguments);
696 /* When using *FilterPane directly (without a *Dialog
697 * subclass), do nothing. */
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.
708 "startup": function() {
710 this.domNode = this.useDiv;
713 this.inherited(arguments);
715 /* When using *FilterPane directly (without a *Dialog
716 * subclass), there is no startup method in any ancestor
717 * class. XXX Refactor?
724 this._buildFieldStore();
726 this.filter_row_manager = new PCrudFilterRowManager(
727 dojo.create("div", {}, this.domNode),
728 this.fieldStore, this.fmClass, this.compact,
730 Boolean(this.initializers) /* avoid adding empty row */,
731 dojo.hitch(this, function() { this.doApply(); })
734 this._buildButtons();
736 if (this.initializers) {
737 this.initializers.forEach(
738 dojo.hitch(this, function(initializer) {
739 this.filter_row_manager.add_row(initializer);
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);
750 var _E; /* Try pretty hard not to leave the apply button
751 disabled forever, even if 'apply' blows up. */
754 this.onApply(this.filter_row_manager.compile());
759 this._apply_button.attr("disabled", false);