/* * This file is part of the Kimai time-tracking app. * * Main JS application file for Kimai 2. This file should be included in all pages. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /*! * [KIMAI] Main JS application file for Kimai 2 */ /** global: jQuery */ /** global: moment */ if (typeof jQuery === 'undefined') { throw new Error('Kimai requires jQuery'); } if (typeof moment === 'undefined') { throw new Error('Kimai requires moment.js'); } /* kimai * * @type Object * @description $.kimai is the main object for the template's app. * It's used for implementing functions and options related * to the template. Keeping everything wrapped in an object * prevents conflict with other plugins and is a better * way to organize our code. */ $.kimai = {}; $(function() { "use strict"; $.kimai = { init: function(options) { if (typeof options !== 'undefined') { $.kimai.settings = $.extend({}, $.kimai.defaults, options); } // set the current locale for all javascript components moment.locale($.kimai.settings['locale']); // activate the dropdown functionality $('.dropdown-toggle').dropdown(); // activate the tooltip functionality $('[data-toggle="tooltip"]').tooltip(); // auto hide success messages, as they are just meant as user feedback and not as a permanent information this.activateAutomaticAlertRemove('div.alert-success', 5000); // activate the (daterangepicker) compound field in toolbar this.activateDateRangePicker('.content-wrapper'); // single select boxes in toolbars this.activateDatePicker('.content-wrapper'); // edit timesheet - date with time this.activateDateTimePicker('.content-wrapper'); // some actions can be performed in a modal for a better UX this.activateAjaxFormInModal('.modal-ajax-form'); // activate select boxes that load dynamic data via API this.activateApiSelects('select[data-related-select]'); }, reloadDatatableWithToolbarFilter: function() { // TODO check if toolbar form is present, if not, reload current URL var $form = $('.toolbar form'); var loading = '
'; $('section.content').append(loading); // remove the empty fields to prevent errors var formData = $('.toolbar form :input') .filter(function(index, element) { return $(element).val() != ''; }) .serialize(); $.ajax({ url: $form.attr('action'), type: $form.attr('method'), data: formData, success: function(html) { $('section.content').replaceWith( $(html).find('section.content') ); }, error: function(xhr, err) { $form.submit(); } }); }, pauseRecord: function(selector) { $(selector + ' .pull-left i').hover(function () { var link = $(this).parents('a'); link.attr('href', link.attr('href').replace('/stop', '/pause')); $(this).removeClass('fa-stop-circle').addClass('fa-pause-circle').addClass('text-orange'); },function () { var link = $(this).parents('a'); link.attr('href', link.attr('href').replace('/pause', '/stop')); $(this).removeClass('fa-pause-circle').removeClass('text-orange').addClass('fa-stop-circle'); }); }, activateAutomaticAlertRemove(selector, mseconds) { setTimeout( function() { $(selector).alert('close'); }, mseconds ); }, activateApiSelects: function(selector) { const self = this; $('body').on('change', selector, function(event) { let apiUrl = $(this).attr('data-api-url').replace('-s-', $(this).val()); const targetSelect = '#' + $(this).attr('data-related-select'); // if the related target select does not exist, we do not need to load the related data if ($(targetSelect).length === 0) { return; } if ($(this).val() === '') { if ($(this).attr('data-empty-url') === undefined) { self.updateSelect(targetSelect, {}); $(targetSelect).attr('disabled', 'disabled'); return; } apiUrl = $(this).attr('data-empty-url').replace('-s-', $(this).val()); } $(targetSelect).removeAttr('disabled'); $.ajax({ url: apiUrl, headers: { 'X-AUTH-SESSION': true, 'Content-Type':'application/json' }, method: 'GET', dataType: 'json', success: function(data){ self.updateSelect(targetSelect, data); } }); }); }, updateSelect: function(selectName, data) { var $select = $(selectName); var $emptyOption = $(selectName + ' option[value=""]'); $select.find('option').remove().end().find('optgroup').remove().end(); if ($emptyOption.length !== 0) { $select.append(''); } $.each(data, function(i, obj) { $select.append(''); }); // if we don't trigger the change, the other selects won't be resetted $select.trigger('change'); // if the beta test kimai.theme.select_type is active, this will tell the selects to refresh $('.selectpicker').selectpicker('refresh'); }, activateDatePicker: function(selector) { $(selector + ' input[data-datepickerenable="on"]').each(function(index) { var localeFormat = $(this).data('format'); $(this).daterangepicker({ singleDatePicker: true, showDropdowns: true, autoUpdateInput: false, locale: { format: localeFormat, firstDay: 1, applyLabel: $.kimai.settings['apply'], cancelLabel: $.kimai.settings['cancel'], customRangeLabel: $.kimai.settings['customRange'] } }); $(this).on('apply.daterangepicker', function(ev, picker) { $(this).val(picker.startDate.format(localeFormat)); $(this).trigger("change"); }); }); }, activateDateTimePicker: function(selector) { $(selector + ' input[data-datetimepicker="on"]').each(function(index) { var localeFormat = $(this).data('format'); $(this).daterangepicker({ singleDatePicker: true, timePicker: true, timePicker24Hour: $.kimai.settings['twentyFourHours'], showDropdowns: true, autoUpdateInput: false, locale: { format: localeFormat, firstDay: 1, applyLabel: $.kimai.settings['apply'], cancelLabel: $.kimai.settings['cancel'], customRangeLabel: $.kimai.settings['customRange'] } }); $(this).on('apply.daterangepicker', function(ev, picker) { $(this).val(picker.startDate.format(localeFormat)); $(this).trigger("change"); }); }); }, activateDateRangePicker: function(selector) { $(selector + ' input[data-daterangepickerenable="on"]').each(function(index) { var localeFormat = $(this).data('format'); var separator = $(this).data('separator'); var rangesList = {}; rangesList[$.kimai.settings['today']] = [moment(), moment()]; rangesList[$.kimai.settings['yesterday']] = [moment().subtract(1, 'days'), moment().subtract(1, 'days')]; rangesList[$.kimai.settings['thisWeek']] = [moment().startOf('week'), moment().endOf('week')]; rangesList[$.kimai.settings['lastWeek']] = [moment().subtract(1, 'week').startOf('week'), moment().subtract(1, 'week').endOf('week')]; rangesList[$.kimai.settings['thisMonth']] = [moment().startOf('month'), moment().endOf('month')]; rangesList[$.kimai.settings['lastMonth']] = [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]; rangesList[$.kimai.settings['thisYear']] = [moment().startOf('year'), moment().endOf('year')]; rangesList[$.kimai.settings['lastYear']] = [moment().subtract(1, 'year').startOf('year'), moment().subtract(1, 'year').endOf('year')]; $(this).daterangepicker({ showDropdowns: true, autoUpdateInput: false, autoApply: false, linkedCalendars: false, locale: { separator: separator, format: localeFormat, firstDay: 1, applyLabel: $.kimai.settings['apply'], cancelLabel: $.kimai.settings['cancel'], customRangeLabel: $.kimai.settings['customRange'] }, ranges: rangesList, alwaysShowCalendars: true }); $(this).on('apply.daterangepicker', function(ev, picker) { $(this).val(picker.startDate.format(localeFormat) + ' - ' + picker.endDate.format(localeFormat)); $(this).trigger("change"); }); }); }, ajaxFormInModal: function(html) { // the modal that we use to render the form in var formIdentifier = '#remote_form_modal .modal-content form'; var flashErrorIdentifier = 'div.alert-error'; var $form = $(formIdentifier); var $modal = $('#remote_form_modal'); // will be (re-)activated later $form.off('submit'); // load new form from given content if ($(html).find('#form_modal .modal-content').length > 0 ) { // switch classes, in case the modal type changed $modal.on('hidden.bs.modal', function () { if ($modal.hasClass('modal-danger')) { $modal.removeClass('modal-danger'); } }); if ($(html).find('#form_modal').hasClass('modal-danger')) { $modal.addClass('modal-danger'); } // TODO cleanup widgets before replacing the content? $('#remote_form_modal .modal-content').replaceWith( $(html).find('#form_modal .modal-content') ); // activate new loaded widgets $.kimai.activateDateTimePicker(formIdentifier); } // show error flash messages if ($(html).find(flashErrorIdentifier).length > 0) { $('#remote_form_modal .modal-body').prepend( $(html).find(flashErrorIdentifier) ); } // ----------------------------------------------------------------------- // a fix for firefox focus problems with datepicker in modal // see https://github.com/kevinpapst/kimai2/issues/618 var enforceModalFocusFn = $.fn.modal.Constructor.prototype.enforceFocus; $.fn.modal.Constructor.prototype.enforceFocus = function() {}; $modal.on('hidden.bs.modal', function () { $.fn.modal.Constructor.prototype.enforceFocus = enforceModalFocusFn; }); // ----------------------------------------------------------------------- // workaround for autofocus attribute, as the modal "steals" it $modal.on('shown.bs.modal', function () { $(this).find('input[type=text],textarea,select').filter(':not("[data-datetimepicker=on]")').filter(':visible:first').focus().delay(1000).focus(); }); $modal.modal('show'); // the new form that was loaded via ajax $form = $(formIdentifier); // click handler for modal save button, to send forms via ajax $form.on('submit', function(event){ var btn = $(formIdentifier + ' button[type=submit]').button('loading'); event.preventDefault(); event.stopPropagation(); $.ajax({ url: $form.attr('action'), type: $form.attr('method'), data: $form.serialize(), success: function(html) { btn.button('reset'); var hasFieldError = $(html).find('#form_modal .modal-content .has-error').length > 0; var hasFormError = $(html).find('#form_modal .modal-content ul.list-unstyled li.text-danger').length > 0; var hasFlashError = $(html).find(flashErrorIdentifier).length > 0; if (hasFieldError || hasFormError || hasFlashError) { $.kimai.ajaxFormInModal(html); } else { $.kimai.reloadDatatableWithToolbarFilter(); $modal.modal('hide'); } return false; }, error: function(xhr, err) { // FIXME problem in google and 500 error, keeps on submitting... // what else could we do? submitting again at least gives us the opportunity to see errors, // which maybe would be hidden otherwise... this one is totally up for discussion! $form.submit(); } }); }); }, activateAjaxFormInModal: function(selector) { $('body').on('click', selector, function(event) { // just in case an inner element is editable, than this should not be triggered if (event.target.parentNode.isContentEditable || event.target.isContentEditable) { return; } // handles the "click" on table rows to open an entry for editing: when a button within a row is clicked, // we don't want the table row event to be processed - so we intercept it var target = event.target; if (event.currentTarget.tagName === 'TR') { while (target.tagName !== 'BODY') { if (target.tagName === 'A' || target.tagName === 'BUTTON') { return; } target = target.parentNode; } } event.preventDefault(); event.stopPropagation(); // any element can open the modal - for none elements use "data-href" instead of "href" attribute var href = $(this).attr('data-href'); if (!href) { href = $(this).attr('href'); } $.ajax({ url: href, success: function(html) { $.kimai.ajaxFormInModal(html); }, error: function(xhr, err) { window.location = href; } }); }); } }; // default values $.kimai.defaults = { locale: 'en', today: 'Today', yesterday: 'Yesterday', apply: 'Apply', cancel: 'Cancel', thisWeek: 'This week', lastWeek: 'Last week', thisMonth: 'This month', lastMonth: 'Last month', thisYear: 'This year', lastYear: 'Last year', customRange: 'Custom range', twentyFourHours: true }; // once initialized, here are all values $.kimai.settings = {}; });