[FIX] behavior of invisible search inputs: they can still be active and should be...
[odoo/odoo.git] / addons / web / static / src / js / search.js
1 openerp.web.search = function(openerp) {
2 var QWeb = openerp.web.qweb,
3       _t =  openerp.web._t;
4
5 openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.SearchView# */{
6     template: "EmptyComponent",
7     /**
8      * @constructs openerp.web.SearchView
9      * @extends openerp.web.Widget
10      *
11      * @param parent
12      * @param element_id
13      * @param dataset
14      * @param view_id
15      * @param defaults
16      */
17     init: function(parent, dataset, view_id, defaults, hidden) {
18         this._super(parent);
19         this.dataset = dataset;
20         this.model = dataset.model;
21         this.view_id = view_id;
22
23         this.defaults = defaults || {};
24         this.has_defaults = !_.isEmpty(this.defaults);
25
26         this.inputs = [];
27         this.enabled_filters = [];
28
29         this.has_focus = false;
30
31         this.hidden = !!hidden;
32         this.headless = this.hidden && !this.has_defaults;
33
34         this.ready = $.Deferred();
35     },
36     start: function() {
37         this._super();
38         if (this.hidden) {
39             this.$element.hide();
40         }
41         if (this.headless) {
42             this.ready.resolve();
43         } else {
44             this.rpc("/web/searchview/load", {
45                 model: this.model,
46                 view_id: this.view_id,
47                 context: this.dataset.get_context()
48             }, this.on_loaded);
49         }
50         return this.ready.promise();
51     },
52     show: function () {
53         this.$element.show();
54     },
55     hide: function () {
56         this.$element.hide();
57     },
58     /**
59      * Builds a list of widget rows (each row is an array of widgets)
60      *
61      * @param {Array} items a list of nodes to convert to widgets
62      * @param {Object} fields a mapping of field names to (ORM) field attributes
63      * @returns Array
64      */
65     make_widgets: function (items, fields) {
66         var rows = [],
67             row = [];
68         rows.push(row);
69         var filters = [];
70         _.each(items, function (item) {
71             if (filters.length && item.tag !== 'filter') {
72                 row.push(
73                     new openerp.web.search.FilterGroup(
74                         filters, this));
75                 filters = [];
76             }
77
78             if (item.tag === 'newline') {
79                 row = [];
80                 rows.push(row);
81             } else if (item.tag === 'filter') {
82                 if (!this.has_focus) {
83                     item.attrs.default_focus = '1';
84                     this.has_focus = true;
85                 }
86                 filters.push(
87                     new openerp.web.search.Filter(
88                         item, this));
89             } else if (item.tag === 'separator') {
90                 // a separator is a no-op
91             } else {
92                 if (item.tag === 'group') {
93                     // TODO: group and field should be fetched from registries, maybe even filters
94                     row.push(
95                         new openerp.web.search.Group(
96                             item, this, fields));
97                 } else if (item.tag === 'field') {
98                     if (!this.has_focus) {
99                         item.attrs.default_focus = '1';
100                         this.has_focus = true;
101                     }
102                     row.push(
103                         this.make_field(
104                             item, fields[item['attrs'].name]));
105                 }
106             }
107         }, this);
108         if (filters.length) {
109             row.push(new openerp.web.search.FilterGroup(filters, this));
110         }
111
112         return rows;
113     },
114     /**
115      * Creates a field for the provided field descriptor item (which comes
116      * from fields_view_get)
117      *
118      * @param {Object} item fields_view_get node for the field
119      * @param {Object} field fields_get result for the field
120      * @returns openerp.web.search.Field
121      */
122     make_field: function (item, field) {
123         try {
124             return new (openerp.web.search.fields.get_any(
125                     [item.attrs.widget, field.type]))
126                 (item, field, this);
127         } catch (e) {
128             if (! e instanceof openerp.web.KeyNotFound) {
129                 throw e;
130             }
131             // KeyNotFound means unknown field type
132             console.group('Unknown field type ' + field.type);
133             console.error('View node', item);
134             console.info('View field', field);
135             console.info('In view', this);
136             console.groupEnd();
137             return null;
138         }
139     },
140     on_loaded: function(data) {
141         this.fields_view = data.fields_view;
142         if (data.fields_view.type !== 'search' ||
143             data.fields_view.arch.tag !== 'search') {
144                 throw new Error(_.str.sprintf(
145                     "Got non-search view after asking for a search view: type %s, arch root %s",
146                     data.fields_view.type, data.fields_view.arch.tag));
147         }
148         var self = this,
149            lines = this.make_widgets(
150                 data.fields_view['arch'].children,
151                 data.fields_view.fields);
152
153         // for extended search view
154         var ext = new openerp.web.search.ExtendedSearch(this, this.model);
155         lines.push([ext]);
156         this.inputs.push(ext);
157         this.extended_search = ext;
158
159         var render = QWeb.render("SearchView", {
160             'view': data.fields_view['arch'],
161             'lines': lines,
162             'defaults': this.defaults
163         });
164
165         this.$element.html(render);
166
167         var f = this.$element.find('form');
168         this.$element.find('form')
169                 .submit(this.do_search)
170                 .bind('reset', this.do_clear);
171         // start() all the widgets
172         var widget_starts = _(lines).chain().flatten().map(function (widget) {
173             return widget.start();
174         }).value();
175
176         $.when.apply(null, widget_starts).then(function () {
177             self.ready.resolve();
178         });
179
180         this.reload_managed_filters();
181     },
182     reload_managed_filters: function() {
183         var self = this;
184         return this.rpc('/web/searchview/get_filters', {
185             model: this.dataset.model
186         }).then(function(result) {
187             self.managed_filters = result;
188             var filters = self.$element.find(".oe_search-view-filters-management");
189             filters.html(QWeb.render("SearchView.managed-filters", {filters: result}));
190             filters.change(self.on_filters_management);
191         });
192     },
193     /**
194      * Handle event when the user make a selection in the filters management select box.
195      */
196     on_filters_management: function(e) {
197         var self = this;
198         var select = this.$element.find(".oe_search-view-filters-management");
199         var val = select.val();
200         switch(val) {
201         case 'advanced_filter':
202             this.extended_search.on_activate();
203             break;
204         case 'add_to_dashboard':
205             this.on_add_to_dashboard();
206             break;
207         case 'manage_filters':
208             this.do_action({
209                 res_model: 'ir.filters',
210                 views: [[false, 'list'], [false, 'form']],
211                 type: 'ir.actions.act_window',
212                 context: {"search_default_user_id": this.session.uid,
213                 "search_default_model_id": this.dataset.model},
214                 target: "current",
215                 limit : 80
216             });
217             break;
218         case 'save_filter':
219             var data = this.build_search_data();
220             var context = new openerp.web.CompoundContext();
221             _.each(data.contexts, function(x) {
222                 context.add(x);
223             });
224             var domain = new openerp.web.CompoundDomain();
225             _.each(data.domains, function(x) {
226                 domain.add(x);
227             });
228             var dial_html = QWeb.render("SearchView.managed-filters.add");
229             var $dial = $(dial_html);
230             $dial.dialog({
231                 modal: true,
232                 title: _t("Filter Entry"),
233                 buttons: [
234                     {text: _t("Cancel"), click: function() {
235                         $(this).dialog("close");
236                     }},
237                     {text: _t("OK"), click: function() {
238                         $(this).dialog("close");
239                         var name = $(this).find("input").val();
240                         self.rpc('/web/searchview/save_filter', {
241                             model: self.dataset.model,
242                             context_to_save: context,
243                             domain: domain,
244                             name: name
245                         }).then(function() {
246                             self.reload_managed_filters();
247                         });
248                     }}
249                 ]
250             });
251             break;
252         }
253         if (val.slice(0, 4) == "get:") {
254             val = val.slice(4);
255             val = parseInt(val, 10);
256             var filter = this.managed_filters[val];
257             this.on_search([filter.domain], [filter.context], []);
258         } else {
259             select.val('');
260         }
261     },
262     on_add_to_dashboard: function() {
263         this.$element.find(".oe_search-view-filters-management")[0].selectedIndex = 0;
264         var self = this,
265             menu = openerp.webclient.menu,
266             $dialog = $(QWeb.render("SearchView.add_to_dashboard", {
267                 dashboards : menu.data.data.children,
268                 selected_menu_id : menu.$element.find('a.active').data('menu')
269             }));
270         $dialog.find('input').val(this.fields_view.name);
271         $dialog.dialog({
272             modal: true,
273             title: _t("Add to Dashboard"),
274             buttons: [
275                 {text: _t("Cancel"), click: function() {
276                     $(this).dialog("close");
277                 }},
278                 {text: _t("OK"), click: function() {
279                     $(this).dialog("close");
280                     var menu_id = $(this).find("select").val(),
281                         title = $(this).find("input").val(),
282                         data = self.build_search_data(),
283                         context = new openerp.web.CompoundContext(),
284                         domain = new openerp.web.CompoundDomain();
285                     _.each(data.contexts, function(x) {
286                         context.add(x);
287                     });
288                     _.each(data.domains, function(x) {
289                            domain.add(x);
290                     });
291                     self.rpc('/web/searchview/add_to_dashboard', {
292                         menu_id: menu_id,
293                         action_id: self.widget_parent.action.id,
294                         context_to_save: context,
295                         domain: domain,
296                         view_mode: self.widget_parent.active_view,
297                         name: title
298                     }, function(r) {
299                         if (r === false) {
300                             self.do_warn("Could not add filter to dashboard");
301                         } else {
302                             self.do_notify("Filter added to dashboard", '');
303                         }
304                     });
305                 }}
306             ]
307         });
308     },
309     /**
310      * Performs the search view collection of widget data.
311      *
312      * If the collection went well (all fields are valid), then triggers
313      * :js:func:`openerp.web.SearchView.on_search`.
314      *
315      * If at least one field failed its validation, triggers
316      * :js:func:`openerp.web.SearchView.on_invalid` instead.
317      *
318      * @param e jQuery event object coming from the "Search" button
319      */
320     do_search: function (e) {
321         if (this.headless && !this.has_defaults) {
322             return this.on_search([], [], []);
323         }
324         // reset filters management
325         var select = this.$element.find(".oe_search-view-filters-management");
326         select.val("_filters");
327
328         if (e && e.preventDefault) { e.preventDefault(); }
329
330         var data = this.build_search_data();
331
332         if (data.errors.length) {
333             this.on_invalid(data.errors);
334             return;
335         }
336
337         this.on_search(data.domains, data.contexts, data.groupbys);
338     },
339     build_search_data: function() {
340         var domains = [],
341            contexts = [],
342              errors = [];
343
344         _.each(this.inputs, function (input) {
345             try {
346                 var domain = input.get_domain();
347                 if (domain) {
348                     domains.push(domain);
349                 }
350
351                 var context = input.get_context();
352                 if (context) {
353                     contexts.push(context);
354                 }
355             } catch (e) {
356                 if (e instanceof openerp.web.search.Invalid) {
357                     errors.push(e);
358                 } else {
359                     throw e;
360                 }
361             }
362         });
363
364         // TODO: do we need to handle *fields* with group_by in their context?
365         var groupbys = _(this.enabled_filters)
366                 .chain()
367                 .map(function (filter) { return filter.get_context();})
368                 .compact()
369                 .value();
370         return {domains: domains, contexts: contexts, errors: errors, groupbys: groupbys};
371     },
372     /**
373      * Triggered after the SearchView has collected all relevant domains and
374      * contexts.
375      *
376      * It is provided with an Array of domains and an Array of contexts, which
377      * may or may not be evaluated (each item can be either a valid domain or
378      * context, or a string to evaluate in order in the sequence)
379      *
380      * It is also passed an array of contexts used for group_by (they are in
381      * the correct order for group_by evaluation, which contexts may not be)
382      *
383      * @event
384      * @param {Array} domains an array of literal domains or domain references
385      * @param {Array} contexts an array of literal contexts or context refs
386      * @param {Array} groupbys ordered contexts which may or may not have group_by keys
387      */
388     on_search: function (domains, contexts, groupbys) {
389     },
390     /**
391      * Triggered after a validation error in the SearchView fields.
392      *
393      * Error objects have three keys:
394      * * ``field`` is the name of the invalid field
395      * * ``value`` is the invalid value
396      * * ``message`` is the (in)validation message provided by the field
397      *
398      * @event
399      * @param {Array} errors a never-empty array of error objects
400      */
401     on_invalid: function (errors) {
402         this.do_notify(_t("Invalid Search"), _t("triggered from search view"));
403     },
404     do_clear: function () {
405         this.$element.find('.filter_label, .filter_icon').removeClass('enabled');
406         this.enabled_filters.splice(0);
407         var string = $('a.searchview_group_string');
408         _.each(string, function(str){
409             $(str).closest('div.searchview_group').removeClass("expanded").addClass('folded');
410          });
411         this.$element.find('table:last').hide();
412
413         $('.searchview_extended_groups_list').empty();
414         _.each(this.inputs, function (input) {
415             if(input.datewidget && input.datewidget.value) {
416                 input.datewidget.set_value(false);
417             }
418         });
419         setTimeout(this.on_clear, 0);
420     },
421     /**
422      * Triggered when the search view gets cleared
423      *
424      * @event
425      */
426     on_clear: function () {
427         this.do_search();
428     },
429     /**
430      * Called by a filter propagating its state changes
431      *
432      * @param {openerp.web.search.Filter} filter a filter which got toggled
433      * @param {Boolean} default_enabled filter got enabled through the default values, at render time.
434      */
435     do_toggle_filter: function (filter, default_enabled) {
436         if (default_enabled || filter.is_enabled()) {
437             this.enabled_filters.push(filter);
438         } else {
439             this.enabled_filters = _.without(
440                 this.enabled_filters, filter);
441         }
442
443         if (!default_enabled) {
444             // selecting a filter after initial loading automatically
445             // triggers refresh
446             this.$element.find('form').submit();
447         }
448     }
449 });
450
451 /** @namespace */
452 openerp.web.search = {};
453 /**
454  * Registry of search fields, called by :js:class:`openerp.web.SearchView` to
455  * find and instantiate its field widgets.
456  */
457 openerp.web.search.fields = new openerp.web.Registry({
458     'char': 'openerp.web.search.CharField',
459     'text': 'openerp.web.search.CharField',
460     'boolean': 'openerp.web.search.BooleanField',
461     'integer': 'openerp.web.search.IntegerField',
462     'float': 'openerp.web.search.FloatField',
463     'selection': 'openerp.web.search.SelectionField',
464     'datetime': 'openerp.web.search.DateTimeField',
465     'date': 'openerp.web.search.DateField',
466     'many2one': 'openerp.web.search.ManyToOneField',
467     'many2many': 'openerp.web.search.CharField',
468     'one2many': 'openerp.web.search.CharField'
469 });
470 openerp.web.search.Invalid = openerp.web.Class.extend( /** @lends openerp.web.search.Invalid# */{
471     /**
472      * Exception thrown by search widgets when they hold invalid values,
473      * which they can not return when asked.
474      *
475      * @constructs openerp.web.search.Invalid
476      * @extends openerp.web.Class
477      *
478      * @param field the name of the field holding an invalid value
479      * @param value the invalid value
480      * @param message validation failure message
481      */
482     init: function (field, value, message) {
483         this.field = field;
484         this.value = value;
485         this.message = message;
486     },
487     toString: function () {
488         return _.str.sprintf(
489             _t("Incorrect value for field %(fieldname)s: [%(value)s] is %(message)s"),
490             {fieldname: this.field, value: this.value, message: this.message}
491         );
492     }
493 });
494 openerp.web.search.Widget = openerp.web.Widget.extend( /** @lends openerp.web.search.Widget# */{
495     template: null,
496     /**
497      * Root class of all search widgets
498      *
499      * @constructs openerp.web.search.Widget
500      * @extends openerp.web.Widget
501      *
502      * @param view the ancestor view of this widget
503      */
504     init: function (view) {
505         this._super(view);
506         this.view = view;
507     },
508     /**
509      * Sets and returns a globally unique identifier for the widget.
510      *
511      * If a prefix is specified, the identifier will be appended to it.
512      *
513      * @params prefix prefix sections, empty/falsy sections will be removed
514      */
515     make_id: function () {
516         this.element_id = _.uniqueId(
517             ['search'].concat(
518                 _.compact(_.toArray(arguments)),
519                 ['']).join('_'));
520         return this.element_id;
521     },
522     /**
523      * "Starts" the widgets. Called at the end of the rendering, this allows
524      * widgets to hook themselves to their view sections.
525      *
526      * On widgets, if they kept a reference to a view and have an element_id,
527      * will fetch and set their root element on $element.
528      */
529     start: function () {
530         this._super();
531         if (this.view && this.element_id) {
532             // id is unique, and no getElementById on elements
533             this.$element = $(document.getElementById(
534                 this.element_id));
535         }
536     },
537     /**
538      * "Stops" the widgets. Called when the view destroys itself, this
539      * lets the widgets clean up after themselves.
540      */
541     stop: function () {
542         delete this.view;
543         this._super();
544     },
545     render: function (defaults) {
546         // FIXME
547         return this._super(_.extend(this, {defaults: defaults}));
548     }
549 });
550 openerp.web.search.add_expand_listener = function($root) {
551     $root.find('a.searchview_group_string').click(function (e) {
552         $root.toggleClass('folded expanded');
553         e.stopPropagation();
554         e.preventDefault();
555     });
556 };
557 openerp.web.search.Group = openerp.web.search.Widget.extend({
558     template: 'SearchView.group',
559     init: function (view_section, view, fields) {
560         this._super(view);
561         this.attrs = view_section.attrs;
562         this.lines = view.make_widgets(
563             view_section.children, fields);
564         this.make_id('group');
565     },
566     start: function () {
567         this._super();
568         openerp.web.search.add_expand_listener(this.$element);
569         var widget_starts = _(this.lines).chain().flatten()
570                 .map(function (widget) { return widget.start(); })
571             .value();
572         return $.when.apply(null, widget_starts);
573     }
574 });
575
576 openerp.web.search.Input = openerp.web.search.Widget.extend( /** @lends openerp.web.search.Input# */{
577     /**
578      * @constructs openerp.web.search.Input
579      * @extends openerp.web.search.Widget
580      *
581      * @param view
582      */
583     init: function (view) {
584         this._super(view);
585         this.view.inputs.push(this);
586         this.style = undefined;
587     },
588     get_context: function () {
589         throw new Error(
590             "get_context not implemented for widget " + this.attrs.type);
591     },
592     get_domain: function () {
593         throw new Error(
594             "get_domain not implemented for widget " + this.attrs.type);
595     },
596     load_attrs: function (attrs) {
597         if (attrs.modifiers) {
598             attrs.modifiers = JSON.parse(attrs.modifiers);
599             attrs.invisible = attrs.modifiers.invisible || false;
600             if (attrs.invisible) {
601                 this.style = 'display: none;'
602             }
603         }
604         this.attrs = attrs;
605     }
606 });
607 openerp.web.search.FilterGroup = openerp.web.search.Input.extend(/** @lends openerp.web.search.FilterGroup# */{
608     template: 'SearchView.filters',
609     /**
610      * Inclusive group of filters, creates a continuous "button" with clickable
611      * sections (the normal display for filters is to be a self-contained button)
612      *
613      * @constructs openerp.web.search.FilterGroup
614      * @extends openerp.web.search.Input
615      *
616      * @param {Array<openerp.web.search.Filter>} filters elements of the group
617      * @param {openerp.web.SearchView} view view in which the filters are contained
618      */
619     init: function (filters, view) {
620         this._super(view);
621         this.filters = filters;
622         this.length = filters.length;
623     },
624     start: function () {
625         this._super();
626         _.each(this.filters, function (filter) {
627             filter.start();
628         });
629     },
630     get_context: function () { },
631     /**
632      * Handles domains-fetching for all the filters within it: groups them.
633      */
634     get_domain: function () {
635         var domains = _(this.filters).chain()
636             .filter(function (filter) { return filter.is_enabled(); })
637             .map(function (filter) { return filter.attrs.domain; })
638             .reject(_.isEmpty)
639             .value();
640
641         if (!domains.length) { return; }
642         if (domains.length === 1) { return domains[0]; }
643         for (var i=domains.length; --i;) {
644             domains.unshift(['|']);
645         }
646         return _.extend(new openerp.web.CompoundDomain(), {
647             __domains: domains
648         });
649     }
650 });
651 openerp.web.search.Filter = openerp.web.search.Input.extend(/** @lends openerp.web.search.Filter# */{
652     template: 'SearchView.filter',
653     /**
654      * Implementation of the OpenERP filters (button with a context and/or
655      * a domain sent as-is to the search view)
656      *
657      * @constructs openerp.web.search.Filter
658      * @extends openerp.web.search.Input
659      *
660      * @param node
661      * @param view
662      */
663     init: function (node, view) {
664         this._super(view);
665         this.load_attrs(node.attrs);
666         this.classes = [this.attrs.string ? 'filter_label' : 'filter_icon'];
667         this.make_id('filter', this.attrs.name);
668     },
669     start: function () {
670         this._super();
671         var self = this;
672         this.$element.click(function (e) {
673             $(this).toggleClass('enabled');
674             self.view.do_toggle_filter(self);
675         });
676     },
677     /**
678      * Returns whether the filter is currently enabled (in use) or not.
679      *
680      * @returns a boolean
681      */
682     is_enabled:function () {
683         return this.$element.hasClass('enabled');
684     },
685     /**
686      * If the filter is present in the defaults (and has a truthy value),
687      * enable the filter.
688      *
689      * @param {Object} defaults the search view's default values
690      */
691     render: function (defaults) {
692         if (this.attrs.name && defaults[this.attrs.name]) {
693             this.classes.push('enabled');
694             this.view.do_toggle_filter(this, true);
695         }
696         return this._super(defaults);
697     },
698     get_context: function () {
699         if (!this.is_enabled()) {
700             return;
701         }
702         return this.attrs.context;
703     },
704     /**
705      * Does not return anything: filter domain is handled at the FilterGroup
706      * level
707      */
708     get_domain: function () { }
709 });
710 openerp.web.search.Field = openerp.web.search.Input.extend( /** @lends openerp.web.search.Field# */ {
711     template: 'SearchView.field',
712     default_operator: '=',
713     /**
714      * @constructs openerp.web.search.Field
715      * @extends openerp.web.search.Input
716      *
717      * @param view_section
718      * @param field
719      * @param view
720      */
721     init: function (view_section, field, view) {
722         this._super(view);
723         this.load_attrs(_.extend({}, field, view_section.attrs));
724         this.filters = new openerp.web.search.FilterGroup(_.compact(_.map(
725             view_section.children, function (filter_node) {
726                 if (filter_node.attrs.string &&
727                         typeof console !== 'undefined' && console.debug) {
728                     console.debug("Filter-in-field with a 'string' attribute "
729                                 + "in view", view);
730                 }
731                 delete filter_node.attrs.string;
732                 return new openerp.web.search.Filter(
733                     filter_node, view);
734         })), view);
735         this.make_id('input', field.type, this.attrs.name);
736     },
737     start: function () {
738         this._super();
739         this.filters.start();
740     },
741     get_context: function () {
742         var val = this.get_value();
743         // A field needs a value to be "active", and a context to send when
744         // active
745         var has_value = (val !== null && val !== '');
746         var context = this.attrs.context;
747         if (!(has_value && context)) {
748             return;
749         }
750         return _.extend(
751             {}, context,
752             {own_values: {self: val}});
753     },
754     /**
755      * Function creating the returned domain for the field, override this
756      * methods in children if you only need to customize the field's domain
757      * without more complex alterations or tests (and without the need to
758      * change override the handling of filter_domain)
759      *
760      * @param {String} name the field's name
761      * @param {String} operator the field's operator (either attribute-specified or default operator for the field
762      * @param {Number|String} value parsed value for the field
763      * @returns {Array<Array>} domain to include in the resulting search
764      */
765     make_domain: function (name, operator, value) {
766         return [[name, operator, value]];
767     },
768     get_domain: function () {
769         var val = this.get_value();
770         if (val === null || val === '') {
771             return;
772         }
773
774         var domain = this.attrs['filter_domain'];
775         if (!domain) {
776             return this.make_domain(
777                 this.attrs.name,
778                 this.attrs.operator || this.default_operator,
779                 val);
780         }
781         return _.extend({}, domain, {own_values: {self: val}});
782     }
783 });
784 /**
785  * Implementation of the ``char`` OpenERP field type:
786  *
787  * * Default operator is ``ilike`` rather than ``=``
788  *
789  * * The Javascript and the HTML values are identical (strings)
790  *
791  * @class
792  * @extends openerp.web.search.Field
793  */
794 openerp.web.search.CharField = openerp.web.search.Field.extend( /** @lends openerp.web.search.CharField# */ {
795     default_operator: 'ilike',
796     get_value: function () {
797         return this.$element.val();
798     }
799 });
800 openerp.web.search.NumberField = openerp.web.search.Field.extend(/** @lends openerp.web.search.NumberField# */{
801     get_value: function () {
802         if (!this.$element.val()) {
803             return null;
804         }
805         var val = this.parse(this.$element.val()),
806           check = Number(this.$element.val());
807         if (isNaN(val) || val !== check) {
808             this.$element.addClass('error');
809             throw new openerp.web.search.Invalid(
810                 this.attrs.name, this.$element.val(), this.error_message);
811         }
812         this.$element.removeClass('error');
813         return val;
814     }
815 });
816 /**
817  * @class
818  * @extends openerp.web.search.NumberField
819  */
820 openerp.web.search.IntegerField = openerp.web.search.NumberField.extend(/** @lends openerp.web.search.IntegerField# */{
821     error_message: _t("not a valid integer"),
822     parse: function (value) {
823         try {
824             return openerp.web.parse_value(value, {'widget': 'integer'});
825         } catch (e) {
826             return NaN;
827         }
828     }
829 });
830 /**
831  * @class
832  * @extends openerp.web.search.NumberField
833  */
834 openerp.web.search.FloatField = openerp.web.search.NumberField.extend(/** @lends openerp.web.search.FloatField# */{
835     error_message: _t("not a valid number"),
836     parse: function (value) {
837         try {
838             return openerp.web.parse_value(value, {'widget': 'float'});
839         } catch (e) {
840             return NaN;
841         }
842     }
843 });
844 /**
845  * @class
846  * @extends openerp.web.search.Field
847  */
848 openerp.web.search.SelectionField = openerp.web.search.Field.extend(/** @lends openerp.web.search.SelectionField# */{
849     // This implementation is a basic <select> field, but it may have to be
850     // altered to be more in line with the GTK client, which uses a combo box
851     // (~ jquery.autocomplete):
852     // * If an option was selected in the list, behave as currently
853     // * If something which is not in the list was entered (via the text input),
854     //   the default domain should become (`ilike` string_value) but **any
855     //   ``context`` or ``filter_domain`` becomes falsy, idem if ``@operator``
856     //   is specified. So at least get_domain needs to be quite a bit
857     //   overridden (if there's no @value and there is no filter_domain and
858     //   there is no @operator, return [[name, 'ilike', str_val]]
859     template: 'SearchView.field.selection',
860     init: function () {
861         this._super.apply(this, arguments);
862         // prepend empty option if there is no empty option in the selection list
863         this.prepend_empty = !_(this.attrs.selection).detect(function (item) {
864             return !item[1];
865         });
866     },
867     get_value: function () {
868         var index = parseInt(this.$element.val(), 10);
869         if (isNaN(index)) { return null; }
870         var value = this.attrs.selection[index][0];
871         if (value === false) { return null; }
872         return value;
873     },
874     /**
875      * The selection field needs a default ``false`` value in case none is
876      * provided, so that selector options with a ``false`` value (convention
877      * for explicitly empty options) get selected by default rather than the
878      * first (value-holding) option in the selection.
879      *
880      * @param {Object} defaults search default values
881      */
882     render: function (defaults) {
883         if (!defaults[this.attrs.name]) {
884             defaults[this.attrs.name] = false;
885         }
886         return this._super(defaults);
887     }
888 });
889 openerp.web.search.BooleanField = openerp.web.search.SelectionField.extend(/** @lends openerp.web.search.BooleanField# */{
890     /**
891      * @constructs openerp.web.search.BooleanField
892      * @extends openerp.web.search.BooleanField
893      */
894     init: function () {
895         this._super.apply(this, arguments);
896         this.attrs.selection = [
897             ['true', 'Yes'],
898             ['false', 'No']
899         ];
900     },
901     /**
902      * Search defaults likely to be boolean values (for a boolean field).
903      *
904      * In the HTML, we only want/get strings, and our strings here are ``true``
905      * and ``false``, so ensure we use precisely those by truth-testing the
906      * default value (iif there is one in the view's defaults).
907      *
908      * @param {Object} defaults default values for this search view
909      * @returns {String} rendered boolean field
910      */
911     render: function (defaults) {
912         var name = this.attrs.name;
913         if (name in defaults) {
914             defaults[name] = defaults[name] ? "true" : "false";
915         }
916         return this._super(defaults);
917     },
918     get_value: function () {
919         switch (this.$element.val()) {
920             case 'false': return false;
921             case 'true': return true;
922             default: return null;
923         }
924     }
925 });
926 /**
927  * @class
928  * @extends openerp.web.search.DateField
929  */
930 openerp.web.search.DateField = openerp.web.search.Field.extend(/** @lends openerp.web.search.DateField# */{
931     template: "SearchView.date",
932     start: function () {
933         this._super();
934         this.datewidget = new openerp.web.DateWidget(this);
935         this.datewidget.prependTo(this.$element);
936         this.datewidget.$element.find("input").attr("size", 15);
937         this.datewidget.$element.find("input").attr("autofocus",
938             this.attrs.default_focus === '1' ? 'autofocus' : undefined);
939         this.datewidget.set_value(this.defaults[this.attrs.name] || false);
940     },
941     get_value: function () {
942         return this.datewidget.get_value() || null;
943     }
944 });
945 /**
946  * Implementation of the ``datetime`` openerp field type:
947  *
948  * * Uses the same widget as the ``date`` field type (a simple date)
949  *
950  * * Builds a slighly more complex, it's a datetime range (includes time)
951  *   spanning the whole day selected by the date widget
952  *
953  * @class
954  * @extends openerp.web.DateField
955  */
956 openerp.web.search.DateTimeField = openerp.web.search.DateField.extend(/** @lends openerp.web.search.DateTimeField# */{
957     make_domain: function (name, operator, value) {
958         return ['&', [name, '>=', value + ' 00:00:00'],
959                      [name, '<=', value + ' 23:59:59']];
960     }
961 });
962 openerp.web.search.ManyToOneField = openerp.web.search.CharField.extend({
963     init: function (view_section, field, view) {
964         this._super(view_section, field, view);
965         var self = this;
966         this.got_name = $.Deferred().then(function () {
967             self.$element.val(self.name);
968         });
969         this.dataset = new openerp.web.DataSet(
970                 this.view, this.attrs['relation']);
971     },
972     start: function () {
973         this._super();
974         this.setup_autocomplete();
975         var started = $.Deferred();
976         this.got_name.then(function () { started.resolve();},
977                            function () { started.resolve(); });
978         return started.promise();
979     },
980     setup_autocomplete: function () {
981         var self = this;
982         this.$element.autocomplete({
983             source: function (req, resp) {
984                 self.dataset.name_search(
985                     req.term, self.attrs.domain, 'ilike', 8, function (data) {
986                         resp(_.map(data, function (result) {
987                             return {id: result[0], label: result[1]}
988                         }));
989                 });
990             },
991             select: function (event, ui) {
992                 self.id = ui.item.id;
993                 self.name = ui.item.label;
994             },
995             delay: 0
996         })
997     },
998     on_name_get: function (name_get) {
999         if (!name_get.length) {
1000             delete this.id;
1001             this.got_name.reject();
1002             return;
1003         }
1004         this.name = name_get[0][1];
1005         this.got_name.resolve();
1006     },
1007     render: function (defaults) {
1008         if (defaults[this.attrs.name]) {
1009             this.id = defaults[this.attrs.name];
1010             if (this.id instanceof Array)
1011                 this.id = this.id[0];
1012             // TODO: maybe this should not be completely removed
1013             delete defaults[this.attrs.name];
1014             this.dataset.name_get([this.id], $.proxy(this, 'on_name_get'));
1015         } else {
1016             this.got_name.reject();
1017         }
1018         return this._super(defaults);
1019     },
1020     make_domain: function (name, operator, value) {
1021         if (this.id && this.name) {
1022             if (value === this.name) {
1023                 return [[name, '=', this.id]];
1024             } else {
1025                 delete this.id;
1026                 delete this.name;
1027             }
1028         }
1029         return this._super(name, operator, value);
1030     }
1031 });
1032
1033 openerp.web.search.ExtendedSearch = openerp.web.OldWidget.extend({
1034     template: 'SearchView.extended_search',
1035     identifier_prefix: 'extended-search',
1036     init: function (parent, model) {
1037         this._super(parent);
1038         this.model = model;
1039     },
1040     add_group: function() {
1041         var group = new openerp.web.search.ExtendedSearchGroup(this, this.fields);
1042         group.appendTo(this.$element.find('.searchview_extended_groups_list'));
1043         this.check_last_element();
1044     },
1045     start: function () {
1046         this.$element = $("#" + this.element_id);
1047         this.$element.closest("table.oe-searchview-render-line").css("display", "none");
1048         var self = this;
1049         this.rpc("/web/searchview/fields_get",
1050             {"model": this.model}, function(data) {
1051             self.fields = data.fields;
1052             if (!('id' in self.fields)) {
1053                 self.fields.id = {
1054                     string: 'ID',
1055                     type: 'id'
1056                 }
1057             }
1058             openerp.web.search.add_expand_listener(self.$element);
1059             self.$element.find('.searchview_extended_add_group').click(function (e) {
1060                 self.add_group();
1061             });
1062         });
1063     },
1064     get_context: function() {
1065         return null;
1066     },
1067     get_domain: function() {
1068         if (!this.$element) {
1069             return null; // not a logical state but sometimes it happens
1070         }
1071         if(this.$element.closest("table.oe-searchview-render-line").css("display") == "none") {
1072             return null;
1073         }
1074         return _.reduce(this.widget_children,
1075             function(mem, x) { return mem.concat(x.get_domain());}, []);
1076     },
1077     on_activate: function() {
1078         this.add_group();
1079         var table = this.$element.closest("table.oe-searchview-render-line");
1080         table.css("display", "");
1081         if(this.$element.hasClass("folded")) {
1082             this.$element.toggleClass("folded expanded");
1083         }
1084     },
1085     hide: function() {
1086         var table = this.$element.closest("table.oe-searchview-render-line");
1087         table.css("display", "none");
1088         if(this.$element.hasClass("expanded")) {
1089             this.$element.toggleClass("folded expanded");
1090         }
1091     },
1092     check_last_element: function() {
1093         _.each(this.widget_children, function(x) {x.set_last_group(false);});
1094         if (this.widget_children.length >= 1) {
1095             this.widget_children[this.widget_children.length - 1].set_last_group(true);
1096         }
1097     }
1098 });
1099
1100 openerp.web.search.ExtendedSearchGroup = openerp.web.OldWidget.extend({
1101     template: 'SearchView.extended_search.group',
1102     identifier_prefix: 'extended-search-group',
1103     init: function (parent, fields) {
1104         this._super(parent);
1105         this.fields = fields;
1106     },
1107     add_prop: function() {
1108         var prop = new openerp.web.search.ExtendedSearchProposition(this, this.fields);
1109         var render = prop.render({'index': this.widget_children.length - 1});
1110         this.$element.find('.searchview_extended_propositions_list').append(render);
1111         prop.start();
1112     },
1113     start: function () {
1114         this.$element = $("#" + this.element_id);
1115         var _this = this;
1116         this.add_prop();
1117         this.$element.find('.searchview_extended_add_proposition').click(function () {
1118             _this.add_prop();
1119         });
1120         this.$element.find('.searchview_extended_delete_group').click(function () {
1121             _this.stop();
1122         });
1123     },
1124     get_domain: function() {
1125         var props = _(this.widget_children).chain().map(function(x) {
1126             return x.get_proposition();
1127         }).compact().value();
1128         var choice = this.$element.find(".searchview_extended_group_choice").val();
1129         var op = choice == "all" ? "&" : "|";
1130         return choice == "none" ? ['!'] : [].concat(
1131             _.map(_.range(_.max([0,props.length - 1])), function() { return op; }),
1132             props);
1133     },
1134     stop: function() {
1135         var parent = this.widget_parent;
1136         if (this.widget_parent.widget_children.length == 1)
1137             this.widget_parent.hide();
1138         this._super();
1139         parent.check_last_element();
1140     },
1141     set_last_group: function(is_last) {
1142         this.$element.toggleClass('last_group', is_last);
1143     }
1144 });
1145
1146 openerp.web.search.ExtendedSearchProposition = openerp.web.OldWidget.extend(/** @lends openerp.web.search.ExtendedSearchProposition# */{
1147     template: 'SearchView.extended_search.proposition',
1148     identifier_prefix: 'extended-search-proposition',
1149     /**
1150      * @constructs openerp.web.search.ExtendedSearchProposition
1151      * @extends openerp.web.OldWidget
1152      *
1153      * @param parent
1154      * @param fields
1155      */
1156     init: function (parent, fields) {
1157         this._super(parent);
1158         this.fields = _(fields).chain()
1159             .map(function(val, key) { return _.extend({}, val, {'name': key}); })
1160             .sortBy(function(field) {return field.string;})
1161             .value();
1162         this.attrs = {_: _, fields: this.fields, selected: null};
1163         this.value = null;
1164     },
1165     start: function () {
1166         this.$element = $("#" + this.element_id);
1167         this.select_field(this.fields.length > 0 ? this.fields[0] : null);
1168         var _this = this;
1169         this.$element.find(".searchview_extended_prop_field").change(function() {
1170             _this.changed();
1171         });
1172         this.$element.find('.searchview_extended_delete_prop').click(function () {
1173             _this.stop();
1174         });
1175     },
1176     stop: function() {
1177         var parent;
1178         if (this.widget_parent.widget_children.length == 1)
1179             parent = this.widget_parent;
1180         this._super();
1181         if (parent)
1182             parent.stop();
1183     },
1184     changed: function() {
1185         var nval = this.$element.find(".searchview_extended_prop_field").val();
1186         if(this.attrs.selected == null || nval != this.attrs.selected.name) {
1187             this.select_field(_.detect(this.fields, function(x) {return x.name == nval;}));
1188         }
1189     },
1190     /**
1191      * Selects the provided field object
1192      *
1193      * @param field a field descriptor object (as returned by fields_get, augmented by the field name)
1194      */
1195     select_field: function(field) {
1196         var _this = this;
1197         if(this.attrs.selected != null) {
1198             this.value.stop();
1199             this.value = null;
1200             this.$element.find('.searchview_extended_prop_op').html('');
1201         }
1202         this.attrs.selected = field;
1203         if(field == null) {
1204             return;
1205         }
1206
1207         var type = field.type;
1208         try {
1209             openerp.web.search.custom_filters.get_object(type);
1210         } catch (e) {
1211             if (! e instanceof openerp.web.KeyNotFound) {
1212                 throw e;
1213             }
1214             type = "char";
1215             console.log('Unknow field type ' + e.key);
1216         }
1217         this.value = new (openerp.web.search.custom_filters.get_object(type))
1218                           (this);
1219         if(this.value.set_field) {
1220             this.value.set_field(field);
1221         }
1222         _.each(this.value.operators, function(operator) {
1223             var option = jQuery('<option>', {value: operator.value})
1224                 .text(operator.text)
1225                 .appendTo(_this.$element.find('.searchview_extended_prop_op'));
1226         });
1227         this.$element.find('.searchview_extended_prop_value').html(
1228             this.value.render({}));
1229         this.value.start();
1230
1231     },
1232     get_proposition: function() {
1233         if ( this.attrs.selected == null)
1234             return null;
1235         var field = this.attrs.selected.name;
1236         var op =  this.$element.find('.searchview_extended_prop_op').val();
1237         var value = this.value.get_value();
1238         return [field, op, value];
1239     }
1240 });
1241
1242 openerp.web.search.ExtendedSearchProposition.Field = openerp.web.OldWidget.extend({
1243     start: function () {
1244         this.$element = $("#" + this.element_id);
1245     }
1246 });
1247 openerp.web.search.ExtendedSearchProposition.Char = openerp.web.search.ExtendedSearchProposition.Field.extend({
1248     template: 'SearchView.extended_search.proposition.char',
1249     identifier_prefix: 'extended-search-proposition-char',
1250     operators: [
1251         {value: "ilike", text: _t("contains")},
1252         {value: "not ilike", text: _t("doesn't contain")},
1253         {value: "=", text: _t("is equal to")},
1254         {value: "!=", text: _t("is not equal to")},
1255         {value: ">", text: _t("greater than")},
1256         {value: "<", text: _t("less than")},
1257         {value: ">=", text: _t("greater or equal than")},
1258         {value: "<=", text: _t("less or equal than")}
1259     ],
1260     get_value: function() {
1261         return this.$element.val();
1262     }
1263 });
1264 openerp.web.search.ExtendedSearchProposition.DateTime = openerp.web.search.ExtendedSearchProposition.Field.extend({
1265     template: 'SearchView.extended_search.proposition.empty',
1266     identifier_prefix: 'extended-search-proposition-datetime',
1267     operators: [
1268         {value: "=", text: _t("is equal to")},
1269         {value: "!=", text: _t("is not equal to")},
1270         {value: ">", text: _t("greater than")},
1271         {value: "<", text: _t("less than")},
1272         {value: ">=", text: _t("greater or equal than")},
1273         {value: "<=", text: _t("less or equal than")}
1274     ],
1275     get_value: function() {
1276         return this.datewidget.get_value();
1277     },
1278     start: function() {
1279         this._super();
1280         this.datewidget = new openerp.web.DateTimeWidget(this);
1281         this.datewidget.prependTo(this.$element);
1282     }
1283 });
1284 openerp.web.search.ExtendedSearchProposition.Date = openerp.web.search.ExtendedSearchProposition.Field.extend({
1285     template: 'SearchView.extended_search.proposition.empty',
1286     identifier_prefix: 'extended-search-proposition-date',
1287     operators: [
1288         {value: "=", text: _t("is equal to")},
1289         {value: "!=", text: _t("is not equal to")},
1290         {value: ">", text: _t("greater than")},
1291         {value: "<", text: _t("less than")},
1292         {value: ">=", text: _t("greater or equal than")},
1293         {value: "<=", text: _t("less or equal than")}
1294     ],
1295     get_value: function() {
1296         return this.datewidget.get_value();
1297     },
1298     start: function() {
1299         this._super();
1300         this.datewidget = new openerp.web.DateWidget(this);
1301         this.datewidget.prependTo(this.$element);
1302     }
1303 });
1304 openerp.web.search.ExtendedSearchProposition.Integer = openerp.web.search.ExtendedSearchProposition.Field.extend({
1305     template: 'SearchView.extended_search.proposition.integer',
1306     identifier_prefix: 'extended-search-proposition-integer',
1307     operators: [
1308         {value: "=", text: _t("is equal to")},
1309         {value: "!=", text: _t("is not equal to")},
1310         {value: ">", text: _t("greater than")},
1311         {value: "<", text: _t("less than")},
1312         {value: ">=", text: _t("greater or equal than")},
1313         {value: "<=", text: _t("less or equal than")}
1314     ],
1315     get_value: function() {
1316         try {
1317             return openerp.web.parse_value(this.$element.val(), {'widget': 'integer'});
1318         } catch (e) {
1319             return "";
1320         }
1321     }
1322 });
1323 openerp.web.search.ExtendedSearchProposition.Id = openerp.web.search.ExtendedSearchProposition.Integer.extend({
1324     operators: [{value: "=", text: _t("is")}]
1325 });
1326 openerp.web.search.ExtendedSearchProposition.Float = openerp.web.search.ExtendedSearchProposition.Field.extend({
1327     template: 'SearchView.extended_search.proposition.float',
1328     identifier_prefix: 'extended-search-proposition-float',
1329     operators: [
1330         {value: "=", text: _t("is equal to")},
1331         {value: "!=", text: _t("is not equal to")},
1332         {value: ">", text: _t("greater than")},
1333         {value: "<", text: _t("less than")},
1334         {value: ">=", text: _t("greater or equal than")},
1335         {value: "<=", text: _t("less or equal than")}
1336     ],
1337     get_value: function() {
1338         try {
1339             return openerp.web.parse_value(this.$element.val(), {'widget': 'float'});
1340         } catch (e) {
1341             return "";
1342         }
1343     }
1344 });
1345 openerp.web.search.ExtendedSearchProposition.Selection = openerp.web.search.ExtendedSearchProposition.Field.extend({
1346     template: 'SearchView.extended_search.proposition.selection',
1347     identifier_prefix: 'extended-search-proposition-selection',
1348     operators: [
1349         {value: "=", text: _t("is")},
1350         {value: "!=", text: _t("is not")}
1351     ],
1352     set_field: function(field) {
1353         this.field = field;
1354     },
1355     get_value: function() {
1356         return this.$element.val();
1357     }
1358 });
1359 openerp.web.search.ExtendedSearchProposition.Boolean = openerp.web.search.ExtendedSearchProposition.Field.extend({
1360     template: 'SearchView.extended_search.proposition.boolean',
1361     identifier_prefix: 'extended-search-proposition-boolean',
1362     operators: [
1363         {value: "=", text: _t("is true")},
1364         {value: "!=", text: _t("is false")}
1365     ],
1366     get_value: function() {
1367         return true;
1368     }
1369 });
1370
1371 openerp.web.search.custom_filters = new openerp.web.Registry({
1372     'char': 'openerp.web.search.ExtendedSearchProposition.Char',
1373     'text': 'openerp.web.search.ExtendedSearchProposition.Char',
1374     'one2many': 'openerp.web.search.ExtendedSearchProposition.Char',
1375     'many2one': 'openerp.web.search.ExtendedSearchProposition.Char',
1376     'many2many': 'openerp.web.search.ExtendedSearchProposition.Char',
1377
1378     'datetime': 'openerp.web.search.ExtendedSearchProposition.DateTime',
1379     'date': 'openerp.web.search.ExtendedSearchProposition.Date',
1380     'integer': 'openerp.web.search.ExtendedSearchProposition.Integer',
1381     'float': 'openerp.web.search.ExtendedSearchProposition.Float',
1382     'boolean': 'openerp.web.search.ExtendedSearchProposition.Boolean',
1383     'selection': 'openerp.web.search.ExtendedSearchProposition.Selection',
1384
1385     'id': 'openerp.web.search.ExtendedSearchProposition.Id'
1386 });
1387
1388 };
1389
1390 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: