PartPicker: Tab Event atomar, visuelles Feedback
[kivitendo-erp.git] / js / autocomplete_part.js
1 namespace('kivi', function(k){
2   k.PartPicker = function($real, options) {
3     // short circuit in case someone double inits us
4     if ($real.data("part_picker"))
5       return $real.data("part_picker");
6
7     var KEY = {
8       ESCAPE: 27,
9       ENTER:  13,
10       TAB:    9,
11       LEFT:   37,
12       RIGHT:  39,
13       PAGE_UP: 33,
14       PAGE_DOWN: 34,
15     };
16     var CLASSES = {
17       PICKED:       'partpicker-picked',
18       UNDEFINED:    'partpicker-undefined',
19       FAT_SET_ITEM: 'partpicker_fat_set_item',
20     }
21     var o = $.extend({
22       limit: 20,
23       delay: 50,
24       fat_set_item: $real.hasClass(CLASSES.FAT_SET_ITEM),
25     }, options);
26     var STATES = {
27       PICKED:    CLASSES.PICKED,
28       UNDEFINED: CLASSES.UNDEFINED
29     }
30     var real_id = $real.attr('id');
31     var $dummy  = $('#' + real_id + '_name');
32     var $type   = $('#' + real_id + '_type');
33     var $unit   = $('#' + real_id + '_unit');
34     var $convertible_unit = $('#' + real_id + '_convertible_unit');
35     var $column = $('#' + real_id + '_column');
36     var state   = STATES.PICKED;
37     var last_real = $real.val();
38     var last_dummy = $dummy.val();
39     var timer;
40
41     function open_dialog () {
42       k.popup_dialog({
43         url: 'controller.pl?action=Part/part_picker_search',
44         data: $.extend({
45           real_id: real_id,
46         }, ajax_data($dummy.val())),
47         id: 'part_selection',
48         dialog: {
49           title: k.t8('Part picker'),
50           width: 800,
51           height: 800,
52         }
53       });
54       window.clearTimeout(timer);
55       return true;
56     }
57
58     function ajax_data(term) {
59       var data = {
60         'filter.all:substr:multi::ilike': term,
61         'filter.obsolete': 0,
62         'filter.unit_obj.convertible_to': $convertible_unit && $convertible_unit.val() ? $convertible_unit.val() : '',
63         no_paginate:  $('#no_paginate').prop('checked') ? 1 : 0,
64         column:   $column && $column.val() ? $column.val() : '',
65         current:  $real.val(),
66       };
67
68       if ($type && $type.val())
69         data['filter.type'] = $type.val().split(',');
70
71       if ($unit && $unit.val())
72         data['filter.unit'] = $unit.val().split(',');
73
74       return data;
75     }
76
77     function set_item (item) {
78       if (item.id) {
79         $real.val(item.id);
80         // autocomplete ui has name, ajax items have description
81         $dummy.val(item.name ? item.name : item.description);
82       } else {
83         $real.val('');
84         $dummy.val('');
85       }
86       state = STATES.PICKED;
87       last_real = $real.val();
88       last_dummy = $dummy.val();
89       last_unverified_dummy = $dummy.val();
90       $real.trigger('change');
91
92       if (o.fat_set_item && item.id) {
93         $.ajax({
94           url: 'controller.pl?action=Part/show.json',
95           data: { id: item.id },
96           success: function(rsp) {
97             $real.trigger('set_item:PartPicker', rsp);
98           },
99         });
100       } else {
101         $real.trigger('set_item:PartPicker', item);
102       }
103       annotate_state();
104     }
105
106     function make_defined_state () {
107       if (state == STATES.PICKED) {
108         annotate_state();
109         return true
110       } else if (state == STATES.UNDEFINED && $dummy.val() == '')
111         set_item({})
112       else {
113         last_unverified_dummy = $dummy.val();
114         set_item({ id: last_real, name: last_dummy })
115       }
116       annotate_state();
117     }
118
119     function annotate_state () {
120       if (state == STATES.PICKED)
121         $dummy.removeClass(STATES.UNDEFINED).addClass(STATES.PICKED);
122       else if (state == STATES.UNDEFINED && $dummy.val() == '')
123         $dummy.removeClass(STATES.UNDEFINED).addClass(STATES.PICKED);
124       else {
125         last_unverified_dummy = $dummy.val();
126         $dummy.addClass(STATES.UNDEFINED).removeClass(STATES.PICKED);
127       }
128     }
129
130     function update_results () {
131       $.ajax({
132         url: 'controller.pl?action=Part/part_picker_result',
133         data: $.extend({
134             'real_id': $real.val(),
135         }, ajax_data(function(){ var val = $('#part_picker_filter').val(); return val === undefined ? '' : val })),
136         success: function(data){ $('#part_picker_result').html(data) }
137       });
138     };
139
140     function result_timer (event) {
141       if (!$('no_paginate').prop('checked')) {
142         if (event.keyCode == KEY.PAGE_UP) {
143           $('#part_picker_result a.paginate-prev').click();
144           return;
145         }
146         if (event.keyCode == KEY.PAGE_DOWN) {
147           $('#part_picker_result a.paginate-next').click();
148           return;
149         }
150       }
151       window.clearTimeout(timer);
152       timer = window.setTimeout(update_results, 100);
153     }
154
155     function close_popup() {
156       $('#part_selection').dialog('close');
157     };
158
159     $dummy.autocomplete({
160       source: function(req, rsp) {
161         $.ajax($.extend(o, {
162           url:      'controller.pl?action=Part/ajax_autocomplete',
163           dataType: "json",
164           data:     ajax_data(req.term),
165           success:  function (data){ rsp(data) }
166         }));
167       },
168       select: function(event, ui) {
169         set_item(ui.item);
170       },
171     });
172     /*  In case users are impatient and want to skip ahead:
173      *  Capture <enter> key events and check if it's a unique hit.
174      *  If it is, go ahead and assume it was selected. If it wasn't don't do
175      *  anything so that autocompletion kicks in.  For <tab> don't prevent
176      *  propagation. It would be nice to catch it, but javascript is too stupid
177      *  to fire a tab event later on, so we'd have to reimplement the "find
178      *  next active element in tabindex order and focus it".
179      */
180     /* note:
181      *  event.which does not contain tab events in keypressed in firefox but will report 0
182      *  chrome does not fire keypressed at all on tab or escape
183      */
184     $dummy.keydown(function(event){
185       if (event.which == KEY.ENTER || event.which == KEY.TAB) {
186         // if string is empty assume they want to delete
187         if ($dummy.val() == '') {
188           set_item({});
189           return true;
190         } else if (state == STATES.PICKED) {
191           return true;
192         }
193         if (event.which == KEY.TAB) event.preventDefault();
194         $.ajax({
195           url: 'controller.pl?action=Part/ajax_autocomplete',
196           dataType: "json",
197           data: $.extend( ajax_data($dummy.val()), { prefer_exact: 1 } ),
198           success: function (data) {
199             if (data.length == 1) {
200               set_item(data[0]);
201               if (event.which == KEY.ENTER)
202                 $('#update_button').click();
203             } else if (data.length > 1) {
204              if (event.which == KEY.ENTER)
205                 open_dialog();
206             } else {
207             }
208             annotate_state();
209           }
210         });
211         if (event.which == KEY.ENTER)
212           return false;
213       } else {
214         state = STATES.UNDEFINED;
215       }
216     });
217
218     $dummy.blur(function(){
219       window.clearTimeout(timer);
220       timer = window.setTimeout(annotate_state, 100);
221     });
222
223     // now add a picker div after the original input
224     var pcont  = $('<span>').addClass('position-absolute');
225     var picker = $('<div>');
226     $dummy.after(pcont);
227     pcont.append(picker);
228     picker.addClass('icon16 crm--search').click(open_dialog);
229
230     var pp = {
231       real:           function() { return $real },
232       dummy:          function() { return $dummy },
233       type:           function() { return $type },
234       unit:           function() { return $unit },
235       convertible_unit: function() { return $convertible_unit },
236       column:         function() { return $column },
237       update_results: update_results,
238       result_timer:   result_timer,
239       set_item:       set_item,
240       reset:          make_defined_state,
241       is_defined_state: function() { return state == STATES.PICKED },
242       init_results:    function () {
243         $('div.part_picker_part').each(function(){
244           $(this).click(function(){
245             set_item({
246               id:   $(this).children('input.part_picker_id').val(),
247               name: $(this).children('input.part_picker_description').val(),
248               unit: $(this).children('input.part_picker_unit').val(),
249             });
250             close_popup();
251             $dummy.focus();
252             return true;
253           });
254         });
255         $('#part_selection').keydown(function(e){
256            if (e.which == KEY.ESCAPE) {
257              close_popup();
258              $dummy.focus();
259            }
260         });
261       }
262     }
263     $real.data('part_picker', pp);
264     return pp;
265   }
266 });
267
268 $(function(){
269   $('input.part_autocomplete').each(function(i,real){
270     kivi.PartPicker($(real));
271   })
272 });