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