kivi.js: parse_amount darf "00" nicht ablehnen.
[kivitendo-erp.git] / js / kivi.js
1 namespace("kivi", function(ns) {
2   "use strict";
3
4   ns._locale = {};
5   ns._date_format   = {
6     sep: '.',
7     y:   2,
8     m:   1,
9     d:   0
10   };
11   ns._number_format = {
12     decimalSep:  ',',
13     thousandSep: '.'
14   };
15
16   ns.setup_formats = function(params) {
17     var res = (params.dates || "").match(/^([ymd]+)([^a-z])([ymd]+)[^a-z]([ymd]+)$/);
18     if (res) {
19       ns._date_format                      = { sep: res[2] };
20       ns._date_format[res[1].substr(0, 1)] = 0;
21       ns._date_format[res[3].substr(0, 1)] = 1;
22       ns._date_format[res[4].substr(0, 1)] = 2;
23     }
24
25     res = (params.numbers || "").match(/^\d*([^\d]?)\d+([^\d])\d+$/);
26     if (res)
27       ns._number_format = {
28         decimalSep:  res[2],
29         thousandSep: res[1]
30       };
31   };
32
33   ns.parse_date = function(date) {
34     var parts = date.replace(/\s+/g, "").split(ns._date_format.sep);
35     var today = new Date();
36
37     // without separator?
38     // assume fixed pattern, and extract parts again
39     if (parts.length == 1) {
40       date  = parts[0];
41       parts = date.match(/../g);
42       if (date.length == 8) {
43         parts[ns._date_format.y] += parts.splice(ns._date_format.y + 1, 1)
44       }
45       else
46       if (date.length == 6 || date.length == 4) {
47       }
48       else
49       if (date.length == 1 || date.length == 2) {
50         parts = []
51         parts[ ns._date_format.y ] = today.getFullYear();
52         parts[ ns._date_format.m ] = today.getMonth() + 1;
53         parts[ ns._date_format.d ] = date;
54       }
55       else {
56         return undefined;
57       }
58     }
59
60     if (parts.length == 3) {
61       var year = +parts[ ns._date_format.y ] || 0 * 1 || (new Date()).getFullYear();
62       if (year < 100) {
63         year += year > 70 ? 1900 : 2000;
64       }
65       date = new Date(
66         year,
67         (parts[ ns._date_format.m ] || 0) * 1 - 1, // Months are 0-based.
68         (parts[ ns._date_format.d ] || 0) * 1
69       );
70     } else if (parts.length == 2) {
71       date = new Date(
72         (new Date()).getFullYear(),
73         (parts[ (ns._date_format.m > ns._date_format.d) * 1 ] || 0) * 1 - 1, // Months are 0-based.
74         (parts[ (ns._date_format.d > ns._date_format.m) * 1 ] || 0) * 1
75       );
76     } else
77       return undefined;
78
79     return isNaN(date.getTime()) ? undefined : date;
80   };
81
82   ns.format_date = function(date) {
83     if (isNaN(date.getTime()))
84       return undefined;
85
86     var parts = [ "", "", "" ]
87     parts[ ns._date_format.y ] = date.getFullYear();
88     parts[ ns._date_format.m ] = (date.getMonth() <  9 ? "0" : "") + (date.getMonth() + 1); // Months are 0-based, but days are 1-based.
89     parts[ ns._date_format.d ] = (date.getDate()  < 10 ? "0" : "") + date.getDate();
90     return parts.join(ns._date_format.sep);
91   };
92
93   ns.parse_amount = function(amount) {
94     if ((amount === undefined) || (amount === ''))
95       return 0;
96
97     if (ns._number_format.decimalSep == ',')
98       amount = amount.replace(/\./g, "").replace(/,/g, ".");
99
100     amount = amount.replace(/[\',]/g, "");
101
102     // Make sure no code wich is not a math expression ends up in eval().
103     if (!amount.match(/^[0-9 ()\-+*/.]*$/))
104       return 0;
105
106     amount = amount.replace(/^0+(\d+)/, '$1');
107
108     /* jshint -W061 */
109     try {
110       return eval(amount);
111     } catch (err) {
112       return 0;
113     }
114   };
115
116   ns.round_amount = function(amount, places) {
117     var neg  = amount >= 0 ? 1 : -1;
118     var mult = Math.pow(10, places + 1);
119     var temp = Math.abs(amount) * mult;
120     var diff = Math.abs(1 - temp + Math.floor(temp));
121     temp     = Math.floor(temp) + (diff <= 0.00001 ? 1 : 0);
122     var dec  = temp % 10;
123     temp    += dec >= 5 ? 10 - dec: dec * -1;
124
125     return neg * temp / mult;
126   };
127
128   ns.format_amount = function(amount, places) {
129     amount = amount || 0;
130
131     if ((places !== undefined) && (places >= 0))
132       amount = ns.round_amount(amount, Math.abs(places));
133
134     var parts = ("" + Math.abs(amount)).split(/\./);
135     var intg  = parts[0];
136     var dec   = parts.length > 1 ? parts[1] : "";
137     var sign  = amount  < 0      ? "-"      : "";
138
139     if (places !== undefined) {
140       while (dec.length < Math.abs(places))
141         dec += "0";
142
143       if ((places > 0) && (dec.length > Math.abs(places)))
144         dec = d.substr(0, places);
145     }
146
147     if ((ns._number_format.thousandSep !== "") && (intg.length > 3)) {
148       var len   = ((intg.length + 2) % 3) + 1,
149           start = len,
150           res   = intg.substr(0, len);
151       while (start < intg.length) {
152         res   += ns._number_format.thousandSep + intg.substr(start, 3);
153         start += 3;
154       }
155
156       intg = res;
157     }
158
159     var sep = (places !== 0) && (dec !== "") ? ns._number_format.decimalSep : "";
160
161     return sign + intg + sep + dec;
162   };
163
164   ns.t8 = function(text, params) {
165     text = ns._locale[text] || text;
166     var key, value
167
168     if( Object.prototype.toString.call( params ) === '[object Array]' ) {
169       var len = params.length;
170
171       for(var i=0; i<len; ++i) {
172         key = i + 1;
173         value = params[i];
174         text = text.split("#"+ key).join(value);
175       }
176     }
177     else if( typeof params == 'object' ) {
178       for(key in params) {
179         value = params[key];
180         text = text.split("#{"+ key +"}").join(value);
181       }
182     }
183
184     return text;
185   };
186
187   ns.setupLocale = function(locale) {
188     ns._locale = locale;
189   };
190
191   ns.set_focus = function(element) {
192     var $e = $(element).eq(0);
193     if ($e.data('ckeditorInstance'))
194       ns.focus_ckeditor_when_ready($e);
195     else
196       $e.focus();
197   };
198
199   ns.focus_ckeditor_when_ready = function(element) {
200     $(element).data('ckeditorInstance').on('instanceReady', function() { ns.focus_ckeditor(element); });
201   };
202
203   ns.focus_ckeditor = function(element) {
204     $(element).data('ckeditorInstance').focus();
205   };
206
207   ns.selectall_ckeditor = function(element) {
208     var editor   = $(element).ckeditorGet();
209     var editable = editor.editable();
210     if (editable.is('textarea')) {
211       var textarea = editable.$;
212
213       if (CKEDITOR.env.ie)
214         textarea.createTextRange().execCommand('SelectAll');
215       else {
216         textarea.selectionStart = 0;
217         textarea.selectionEnd   = textarea.value.length;
218       }
219     } else {
220       if (editable.is('body'))
221         editor.document.$.execCommand('SelectAll', false, null);
222
223       else {
224         var range = editor.createRange();
225         range.selectNodeContents(editable);
226         range.select();
227       }
228
229       editor.forceNextSelectionCheck();
230       editor.selectionChange();
231     }
232   }
233
234   ns.init_tabwidget = function(element) {
235     var $element   = $(element);
236     var tabsParams = {};
237     var elementId  = $element.attr('id');
238
239     if (elementId) {
240       var cookieName      = 'jquery_ui_tab_'+ elementId;
241       tabsParams.active   = $.cookie(cookieName);
242       tabsParams.activate = function(event, ui) {
243         var i = ui.newTab.parent().children().index(ui.newTab);
244         $.cookie(cookieName, i);
245       };
246     }
247
248     $element.tabs(tabsParams);
249   };
250
251   ns.init_text_editor = function(element) {
252     var layouts = {
253       all:     [ [ 'Bold', 'Italic', 'Underline', 'Strike', '-', 'Subscript', 'Superscript' ], [ 'BulletedList', 'NumberedList' ], [ 'RemoveFormat' ] ],
254       default: [ [ 'Bold', 'Italic', 'Underline', 'Strike', '-', 'Subscript', 'Superscript' ], [ 'BulletedList', 'NumberedList' ], [ 'RemoveFormat' ] ]
255     };
256
257     var $e      = $(element);
258     var buttons = layouts[ $e.data('texteditor-layout') || 'default' ] || layouts['default'];
259     var config  = {
260       entities:      false,
261       language:      'de',
262       removePlugins: 'resize',
263       extraPlugins:  'inline_resize',
264       toolbar:       buttons,
265       disableAutoInline: true,
266       title:         false
267     };
268
269     config.height = $e.height();
270     config.width  = $e.width();
271
272     var editor = CKEDITOR.inline($e.get(0), config);
273     $e.data('ckeditorInstance', editor);
274
275     if ($e.hasClass('texteditor-autofocus'))
276       editor.on('instanceReady', function() { ns.focus_ckeditor($e); });
277   };
278
279   ns.reinit_widgets = function() {
280     ns.run_once_for('.datepicker', 'datepicker', function(elt) {
281       $(elt).datepicker();
282     });
283
284     if (ns.Part) ns.Part.reinit_widgets();
285     if (ns.CustomerVendor) ns.CustomerVendor.reinit_widgets();
286
287     if (ns.ProjectPicker)
288       ns.run_once_for('input.project_autocomplete', 'project_picker', function(elt) {
289         kivi.ProjectPicker($(elt));
290       });
291
292     if (ns.ChartPicker)
293       ns.run_once_for('input.chart_autocomplete', 'chart_picker', function(elt) {
294         kivi.ChartPicker($(elt));
295       });
296
297
298     var func = kivi.get_function_by_name('local_reinit_widgets');
299     if (func)
300       func();
301
302     ns.run_once_for('.tooltipster', 'tooltipster', function(elt) {
303       $(elt).tooltipster({
304         contentAsHTML: false,
305         theme: 'tooltipster-light'
306       })
307     });
308
309     ns.run_once_for('.tooltipster-html', 'tooltipster-html', function(elt) {
310       $(elt).tooltipster({
311         contentAsHTML: true,
312         theme: 'tooltipster-light'
313       })
314     });
315
316     ns.run_once_for('.tabwidget', 'tabwidget', kivi.init_tabwidget);
317     ns.run_once_for('.texteditor', 'texteditor', kivi.init_text_editor);
318   };
319
320   ns.submit_ajax_form = function(url, form_selector, additional_data) {
321     $(form_selector).ajaxSubmit({
322       url:     url,
323       data:    additional_data,
324       success: ns.eval_json_result
325     });
326
327     return true;
328   };
329
330   // This function submits an existing form given by "form_selector"
331   // and sets the "action" input to "action_to_call" before submitting
332   // it. Any existing input named "action" will be removed prior to
333   // submitting.
334   ns.submit_form_with_action = function(form_selector, action_to_call) {
335     $('[name=action]').remove();
336
337     var $form   = $(form_selector);
338     var $hidden = $('<input type=hidden>');
339
340     $hidden.attr('name',  'action');
341     $hidden.attr('value', action_to_call);
342     $form.append($hidden);
343
344     $form.submit();
345   };
346
347   // This function exists solely so that it can be found with
348   // kivi.get_functions_by_name() and called later on. Using something
349   // like "var func = history["back"]" works, but calling it later
350   // with "func.apply()" doesn't.
351   ns.history_back = function() {
352     history.back();
353   };
354
355   // Return a function object by its name (a string). Works both with
356   // global functions (e.g. "check_right_date_format") and those in
357   // namespaces (e.g. "kivi.t8").
358   // Returns null if the object is not found.
359   ns.get_function_by_name = function(name) {
360     var parts = name.match("(.+)\\.([^\\.]+)$");
361     if (!parts)
362       return window[name];
363     return namespace(parts[1])[ parts[2] ];
364   };
365
366   // Open a modal jQuery UI popup dialog. The content can be either
367   // loaded via AJAX (if the parameter 'url' is given) or simply
368   // displayed if it exists in the DOM already (referenced via
369   // 'id') or given via param.html. If an existing DOM div should be used then
370   // the element won't be removed upon closing the dialog which allows
371   // re-opening it later on.
372   //
373   // Parameters:
374   // - id: dialog DIV ID (optional; defaults to 'jqueryui_popup_dialog')
375   // - url, data, type: passed as the first three arguments to the $.ajax() call if an AJAX call is made, otherwise ignored.
376   // - dialog: an optional object of options passed to the $.dialog() call
377   // - load: an optional function that is called after the content has been loaded successfully (only if an AJAX call is made)
378   ns.popup_dialog = function(params) {
379     var dialog;
380
381     params            = params        || { };
382     var id            = params.id     || 'jqueryui_popup_dialog';
383     var custom_close  = params.dialog ? params.dialog.close : undefined;
384     var dialog_params = $.extend(
385       { // kivitendo default parameters:
386           width:  800
387         , height: 500
388         , modal:  true
389       },
390         // User supplied options:
391       params.dialog || { },
392       { // Options that must not be changed:
393         close: function(event, ui) {
394           dialog.dialog('close');
395
396           if (custom_close)
397             custom_close();
398
399           if (params.url || params.html)
400             dialog.remove();
401         }
402       });
403
404     if (!params.url && !params.html) {
405       // Use existing DOM element and show it. No AJAX call.
406       dialog =
407         $('#' + id)
408         .bind('dialogopen', function() {
409           ns.run_once_for('.texteditor-in-dialog,.texteditor-dialog', 'texteditor', kivi.init_text_editor);
410         })
411         .dialog(dialog_params);
412       return true;
413     }
414
415     $('#' + id).remove();
416
417     dialog = $('<div style="display:none" class="loading" id="' + id + '"></div>').appendTo('body');
418     dialog.dialog(dialog_params);
419
420     if (params.html) {
421       dialog.html(params.html);
422     } else {
423       // no html? get it via ajax
424       $.ajax({
425         url:     params.url,
426         data:    params.data,
427         type:    params.type,
428         success: function(new_html) {
429           dialog.html(new_html);
430           dialog.removeClass('loading');
431           if (params.load)
432             params.load();
433         }
434       });
435     }
436
437     return true;
438   };
439
440   // Run code only once for each matched element
441   //
442   // This allows running the function 'code' exactly once for each
443   // element that matches 'selector'. This is achieved by storing the
444   // state with jQuery's 'data' function. The 'identification' is
445   // required for differentiating unambiguously so that different code
446   // functions can still be run on the same elements.
447   //
448   // 'code' can be either a function or the name of one. It must
449   // resolve to a function that receives the jQueryfied element as its
450   // sole argument.
451   //
452   // Returns nothing.
453   ns.run_once_for = function(selector, identification, code) {
454     var attr_name = 'data-run-once-for-' + identification.toLowerCase().replace(/[^a-z]+/g, '-');
455     var fn        = typeof code === 'function' ? code : ns.get_function_by_name(code);
456     if (!fn) {
457       console.error('kivi.run_once_for(..., "' + code + '"): No function by that name found');
458       return;
459     }
460
461     $(selector).filter(function() { return $(this).data(attr_name) !== true; }).each(function(idx, elt) {
462       var $elt = $(elt);
463       $elt.data(attr_name, true);
464       fn($elt);
465     });
466   };
467
468   // Run a function by its name passing it some arguments
469   //
470   // This is a function useful mainly for the ClientJS functionality.
471   // It finds a function by its name and then executes it on an empty
472   // object passing the elements in 'args' (an array) as the function
473   // parameters retuning its result.
474   //
475   // Logs an error to the console and returns 'undefined' if the
476   // function cannot be found.
477   ns.run = function(function_name, args) {
478     var fn = ns.get_function_by_name(function_name);
479     if (fn)
480       return fn.apply({}, args || []);
481
482     console.error('kivi.run("' + function_name + '"): No function by that name found');
483     return undefined;
484   };
485
486   ns.detect_duplicate_ids_in_dom = function() {
487     var ids   = {},
488         found = false;
489
490     $('[id]').each(function() {
491       if (this.id && ids[this.id]) {
492         found = true;
493         console.warn('Duplicate ID #' + this.id);
494       }
495       ids[this.id] = 1;
496     });
497
498     if (!found)
499       console.log('No duplicate IDs found :)');
500   };
501
502   // Verifies that at least one checkbox matching the
503   // "checkbox_selector" is actually checked. If not, an error message
504   // is shown, and false is returned. Otherwise (at least one of them
505   // is checked) nothing is shown and true returned.
506   //
507   // Can be used in checks when clicking buttons.
508   ns.check_if_entries_selected = function(checkbox_selector) {
509     if ($(checkbox_selector + ':checked').length > 0)
510       return true;
511
512     alert(kivi.t8('No entries have been selected.'));
513
514     return false;
515   };
516
517   // Performs various validation steps on the descendants of
518   // 'selector'. Elements that should be validated must have an
519   // attribute named "data-validate" which is set to a space-separated
520   // list of tests to perform. Additionally, the attribute
521   // "data-title" must be set to a human-readable name of the field
522   // that can be shown as part of an error message.
523   //
524   // Supported validation tests are:
525   // - "required": the field must be set (its .val() must not be empty)
526   //
527   // The validation will abort and return "false" as soon as
528   // validation routine fails.
529   //
530   // The function returns "true" if all validations succeed for all
531   // elements.
532   ns.validate_form = function(selector) {
533     var validate_field = function(elt) {
534       var $elt  = $(elt);
535       var tests = $elt.data('validate').split(/ +/);
536       var info  = {
537         title: $elt.data('title'),
538         value: $elt.val(),
539       };
540
541       for (var test_idx in tests) {
542         var test = tests[test_idx];
543
544         if (test === "required") {
545           if ($elt.val() === '') {
546             alert(kivi.t8("The field '#{title}' must be set.", info));
547             return false;
548           }
549
550         } else {
551           var error = "kivi.validate_form: unknown test '" + test + "' for element ID '" + $elt.prop('id') + "'";
552           console.error(error);
553           alert(error);
554
555           return false;
556         }
557       }
558
559       return true;
560     };
561
562     selector = selector || '#form';
563     var ok   = true;
564     var to_check = $(selector + ' [data-validate]').toArray();
565
566     for (var to_check_idx in to_check)
567       if (!validate_field(to_check[to_check_idx]))
568         return false;
569
570     return true;
571   };
572
573   ns.switch_areainput_to_textarea = function(id) {
574     var $input = $('#' + id);
575     if (!$input.length)
576       return;
577
578     var $area = $('<textarea></textarea>');
579
580     $area.prop('rows', 3);
581     $area.prop('cols', $input.prop('size') || 40);
582     $area.prop('name', $input.prop('name'));
583     $area.prop('id',   $input.prop('id'));
584     $area.val($input.val());
585
586     $input.parent().replaceWith($area);
587     $area.focus();
588   };
589 });
590
591 kivi = namespace('kivi');