refactored javascript to ES6 classes (#759)

This commit is contained in:
Kevin Papst
2019-05-06 15:22:52 +02:00
committed by GitHub
parent 80af3fb010
commit 98f386dad6
37 changed files with 1302 additions and 721 deletions

View File

@@ -62,9 +62,6 @@ require('fullcalendar/dist/fullcalendar.min.css');
require('chart.js/dist/Chart.min');
// ------ Kimai itself ------
require('./js/kimai.js');
require('./js/KimaiDatatableColumnView.js');
require('./js/KimaiActiveRecordsDuration.js');
require('./js/toolbar.js');
require('./js/KimaiWebLoader.js');
require('./images/default_avatar.png');
require('./images/signature.png');

View File

@@ -1,90 +0,0 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiActiveRecordsDuration: updates active records on your personal timesheet view
*/
// Following the UMD template https://github.com/umdjs/umd/blob/master/templates/returnExportsGlobal.js
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(['moment'], function (moment) {
return (root.KimaiActiveRecordsDuration = factory(moment));
});
} else if (typeof module === 'object' && module.exports) {
let moment = (typeof window != 'undefined') ? window.moment : undefined;
if (!moment) {
moment = require('moment');
}
module.exports = factory(moment);
} else {
root.KimaiActiveRecordsDuration = factory(root.moment);
}
}(typeof self !== 'undefined' ? self : this, function (moment) {
class KimaiActiveRecordsDuration {
constructor(selector) {
this.selector = selector;
}
registerUpdates(timeout) {
let self = this;
window.setTimeout(
function() {
self.updateRecords().registerUpdates(timeout);
},
timeout
);
}
updateRecords() {
let durations = [];
for(let record of document.querySelectorAll(this.selector)) {
const since = record.getAttribute('data-since');
const format = record.getAttribute('data-format');
const duration = this.getDuration(since, format);
if (record.getAttribute('data-title') !== null) {
durations.push(duration);
}
record.textContent = duration;
}
if (durations.length === 0) {
return this;
}
let title = durations.shift();
let prefix = ' | ';
for (let duration of durations.slice(0, 2)) {
title += prefix + duration;
}
document.title = title;
return this;
}
getDuration(since, format) {
let duration = moment.duration(moment(new Date()).diff(moment(since)));
let hours = parseInt(duration.asHours());
let minutes = duration.minutes();
let seconds = duration.seconds();
let formatted = '';
// special case for hours, as they can overflow the 24h barrier - Kimai does not support days as duration unit
if (hours.length === 1) {
hours = '0' + hours;
}
return format.replace('%h', hours).replace('%m', ('0'+minutes).substr(-2)).replace('%s', ('0'+seconds).substr(-2));
}
}
return KimaiActiveRecordsDuration;
}));

View File

@@ -0,0 +1,26 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiConfiguration: handling all configuration and runtime settings
*/
export default class KimaiConfiguration {
constructor(configurations) {
this._configurations = configurations;
}
get(name) {
return this._configurations[name];
}
has(name) {
return name in this._configurations;
}
}

View File

@@ -0,0 +1,91 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiContainer
*
* ServiceContainer for Kimai
*/
import KimaiConfiguration from './KimaiConfiguration';
import KimaiTranslation from './KimaiTranslation';
import KimaiPlugin from './KimaiPlugin';
export default class KimaiContainer {
/**
* Create a new Container with the given configurations and translations.
*
* @param {Object} configuration
* @param {Object} translation
*/
constructor(configuration, translation) {
if (!(configuration instanceof KimaiConfiguration)) {
throw new Error('Configuration needs to a KimaiConfiguration instance');
}
this._configuration = configuration;
if (!(translation instanceof KimaiTranslation)) {
throw new Error('Configuration needs to a KimaiTranslation instance');
}
this._translation = translation;
this._plugins = [];
}
/**
* Register a new Plugin.
*
* @param {KimaiPlugin} plugin
* @returns {KimaiPlugin}
*/
registerPlugin(plugin) {
if (!(plugin instanceof KimaiPlugin)) {
throw new Error('Invalid plugin given, needs to be a KimaiPlugin instance');
}
plugin.setContainer(this);
this._plugins.push(plugin);
return plugin;
}
/**
* @param {string} name
* @returns {KimaiPlugin}
*/
getPlugin(name) {
for (let plugin of this._plugins) {
if (plugin.getId() !== null && plugin.getId() === name) {
return plugin;
}
}
throw new Error('Unknown plugin: ' + name);
}
/**
* @returns {Array<KimaiPlugin>}
*/
getPlugins() {
return this._plugins;
}
/**
* @returns {KimaiTranslation}
*/
getTranslation() {
return this._translation;
}
/**
* @returns {KimaiConfiguration}
*/
getConfiguration() {
return this._configuration;
}
}

View File

@@ -1,111 +0,0 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiDatatableColumnView: manages the visibility of data-table columns in cookies
*/
import Cookies from 'js-cookie';
// Following the UMD template https://github.com/umdjs/umd/blob/master/templates/returnExportsGlobal.js
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(['jquery'], function (jquery) {
return (root.KimaiDatatableColumnView = factory(jquery));
});
} else if (typeof module === 'object' && module.exports) {
let jQuery = (typeof window != 'undefined') ? window.jQuery : undefined;
if (!jQuery) {
jQuery = require('jquery');
if (!jQuery.fn) {
jQuery.fn = {};
}
}
module.exports = factory(jQuery);
} else {
root.KimaiDatatableColumnView = factory(root.jQuery);
}
}(typeof self !== 'undefined' ? self : this, function ($) {
/**
* This is my first approach on ES6, so it can be optimized.
* Please: show your JS skills and teach a PHP backend developer how to do it properly, sent a PR!
*
* BTW: I tried to get rid of it, but jQuery is still required for the bootstrap modal ...
*/
class KimaiDatatableColumnView {
constructor(selector) {
this.id = selector;
this.modal = document.getElementById('modal_' + selector);
this.bindButtons();
}
bindButtons() {
let self = this;
this.modal.querySelector('button[data-type=save]').addEventListener('click', function() {
self.saveVisibility();
});
this.modal.querySelector('button[data-type=reset]').addEventListener('click', function() {
self.resetVisibility();
});
for (let checkbox of this.modal.querySelectorAll('form input[type=checkbox]')) {
checkbox.addEventListener('click', function () {
self.changeVisibility(checkbox.getAttribute('name'));
});
}
}
saveVisibility() {
const form = this.modal.getElementsByTagName('form')[0];
let settings = {};
for (let checkbox of form.querySelectorAll('input[type=checkbox]')) {
settings[checkbox.getAttribute('name')] = checkbox.checked;
}
Cookies.set(form.getAttribute('name'), JSON.stringify(settings), {expires: 365});
$(this.modal).modal('toggle');
}
resetVisibility() {
const form = this.modal.getElementsByTagName('form')[0];
Cookies.remove(form.getAttribute('name'));
for (let checkbox of form.querySelectorAll('input[type=checkbox]')) {
if (!checkbox.checked) {
checkbox.click();
}
}
$(this.modal).modal('toggle');
}
changeVisibility(columnName) {
const table = document.getElementById('datatable_' + this.id).getElementsByClassName('dataTable')[0];
let column = 0;
let foundColumn = false;
for (let columnElement of table.getElementsByTagName('th')) {
if (columnElement.getAttribute('data-field') === columnName) {
foundColumn = true;
break;
}
column++;
}
if (!foundColumn) {
console.error('Could not find column: ' + columnName);
return;
}
for (let rowElement of table.getElementsByTagName('tr')) {
rowElement.children[column].classList.toggle('hidden');
}
}
}
return KimaiDatatableColumnView;
}));

91
assets/js/KimaiLoader.js Normal file
View File

@@ -0,0 +1,91 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiLoader: bootstrap the application and all plugins
*/
import moment from 'moment';
import KimaiTranslation from "./KimaiTranslation";
import KimaiConfiguration from "./KimaiConfiguration";
import KimaiContainer from "./KimaiContainer";
import KimaiActiveRecordsDuration from './plugins/KimaiActiveRecordsDuration.js';
import KimaiDatatableColumnView from './plugins/KimaiDatatableColumnView.js';
import KimaiThemeInitializer from "./plugins/KimaiThemeInitializer";
import KimaiJqueryPluginInitializer from "./plugins/KimaiJqueryPluginInitializer";
import KimaiDateRangePicker from "./plugins/KimaiDateRangePicker";
import KimaiDatatable from "./plugins/KimaiDatatable";
import KimaiToolbar from "./plugins/KimaiToolbar";
import KimaiAPI from "./plugins/KimaiAPI";
import KimaiSelectDataAPI from "./plugins/KimaiSelectDataAPI";
import KimaiDateTimePicker from "./plugins/KimaiDateTimePicker";
import KimaiAlternativeLinks from "./plugins/KimaiAlternativeLinks";
import KimaiAjaxModalForm from "./plugins/KimaiAjaxModalForm";
export default class KimaiLoader {
constructor(configurations, translations) {
const defaultTranslations = {
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',
};
translations = Object.assign(defaultTranslations, translations);
const defaultConfigurations = {
locale: 'en',
twentyFourHours: true
};
configurations = Object.assign(defaultConfigurations, configurations);
// set the current locale for all javascript components
moment.locale(configurations['locale']);
const kimai = new KimaiContainer(
new KimaiConfiguration(configurations),
new KimaiTranslation(translations)
);
kimai.registerPlugin(new KimaiAPI());
kimai.registerPlugin(new KimaiActiveRecordsDuration('[data-since]'));
kimai.registerPlugin(new KimaiDatatableColumnView('data-column-visibility'));
kimai.registerPlugin(new KimaiThemeInitializer());
kimai.registerPlugin(new KimaiJqueryPluginInitializer());
kimai.registerPlugin(new KimaiDateRangePicker('.content-wrapper'));
kimai.registerPlugin(new KimaiDateTimePicker('.content-wrapper'));
kimai.registerPlugin(new KimaiDatatable());
kimai.registerPlugin(new KimaiToolbar());
kimai.registerPlugin(new KimaiSelectDataAPI('select[data-related-select]'));
kimai.registerPlugin(new KimaiAlternativeLinks('.alternative-link'));
kimai.registerPlugin(new KimaiAjaxModalForm('.modal-ajax-form'));
//kimai.registerPlugin(new KimaiPauseRecord('li.messages-menu ul.menu li'));
// notify all listeners that Kimai plugins can now be registered
this._sendEvent('kimai.pluginRegister');
// initialize all plugins
kimai.getPlugins().map(plugin => { plugin.init(); });
// notify all listeners that Kimai is now ready to be used
this._sendEvent('kimai.initialized');
}
_sendEvent(name) {
document.dispatchEvent(new Event(name));
}
}

53
assets/js/KimaiPlugin.js Normal file
View File

@@ -0,0 +1,53 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiPlugin: base class for all plugins
*/
import KimaiContainer from "./KimaiContainer";
export default class KimaiPlugin {
/**
* Overwrite this method to initialize your plugin.
*
* It is called AFTER setContainer() and AFTER DOMContentLoaded was fired.
* You don't have access to the container before this method!
*/
init() {
}
/**
* If you return an ID, you indicate that your plugin can be used by other plugins.
*
* @returns {string|null}
*/
getId() {
return null;
}
/**
* @param {KimaiContainer} core
*/
setContainer(core) {
if (!(core instanceof KimaiContainer)) {
throw new Error('Plugin was given an invalid KimaiContainer');
}
this._core = core;
}
/**
* This function returns null, if xou call it BEFORE init().
*
* @returns {KimaiContainer}
*/
getContainer() {
return this._core;
}
}

View File

@@ -0,0 +1,26 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiTranslation: handling translation strings
*/
export default class KimaiTranslation {
constructor(translations) {
this._translations = translations;
}
get(name) {
return this._translations[name];
}
has(name) {
return name in this._translations;
}
}

View File

@@ -0,0 +1,33 @@
/*
* 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] Wrapper class for loading Kimai app in browser script scope
*/
import KimaiLoader from "./KimaiLoader";
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([], function () {
return (root.KimaiWebLoader = factory());
});
} else if (typeof module === 'object' && module.exports) {
module.exports = factory();
} else {
root.KimaiWebLoader = factory();
}
}(typeof self !== 'undefined' ? self : this, function () {
class KimaiWebLoader extends KimaiLoader {
}
return KimaiWebLoader;
}));

View File

@@ -1,400 +0,0 @@
/*
* 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 = '<div class="overlay"><i class="fas fa-sync fa-spin"></i></div>';
$('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('<option value="">' + $emptyOption.text() + '</option>');
}
$.each(data, function(i, obj) {
$select.append('<option value="' + obj.id + '">' + obj.name + '</option>');
});
// 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 <a> 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 = {};
});

View File

@@ -0,0 +1,34 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiToolbar: some event listener to handle the toolbar/data-table filter, toolbar and navigation
*/
import jQuery from 'jquery';
import KimaiPlugin from "../KimaiPlugin";
export default class KimaiAPI extends KimaiPlugin {
getId() {
return 'api';
}
get(url, callback) {
jQuery.ajax({
url: url,
headers: {
'X-AUTH-SESSION': true,
'Content-Type':'application/json'
},
method: 'GET',
dataType: 'json',
success: callback
});
}
}

View File

@@ -0,0 +1,82 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiActiveRecordsDuration: activate the updates for all active timesheet records on this page
*/
import moment from 'moment';
import KimaiPlugin from '../KimaiPlugin';
export default class KimaiActiveRecordsDuration extends KimaiPlugin {
constructor(selector) {
super();
this.selector = selector;
}
init() {
this.updateRecords();
this.registerUpdates(10000);
}
registerUpdates(interval) {
let self = this;
this._updatesHandler = setInterval(
function() {
self.updateRecords();
},
interval
);
}
unregisterUpdates() {
clearInterval(this._updatesHandler);
}
updateRecords() {
let durations = [];
for(let record of document.querySelectorAll(this.selector)) {
const since = record.getAttribute('data-since');
const format = record.getAttribute('data-format');
const duration = KimaiActiveRecordsDuration._getDuration(since, format);
if (record.getAttribute('data-title') !== null) {
durations.push(duration);
}
record.textContent = duration;
}
if (durations.length === 0) {
return this;
}
let title = durations.shift();
let prefix = ' | ';
for (let duration of durations.slice(0, 2)) {
title += prefix + duration;
}
document.title = title;
return this;
}
static _getDuration(since, format) {
const duration = moment.duration(moment(new Date()).diff(moment(since)));
let hours = parseInt(duration.asHours()).toString();
let minutes = duration.minutes();
let seconds = duration.seconds();
// special case for hours, as they can overflow the 24h barrier - Kimai does not support days as duration unit
if (hours.length === 1) {
hours = '0' + hours;
}
return format.replace('%h', hours).replace('%m', ('0'+minutes).substr(-2)).replace('%s', ('0'+seconds).substr(-2));
}
}

View File

@@ -0,0 +1,137 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiAjaxModalForm
*
* allows to assign the given selector to any element, which then is used as click-handler:
* opening a modal with the content from the URL given in the elements 'data-href' or 'href' attribute
*/
import jQuery from 'jquery';
import KimaiClickHandlerReducedInTableRow from "./KimaiClickHandlerReducedInTableRow";
export default class KimaiAjaxModalForm extends KimaiClickHandlerReducedInTableRow {
constructor(selector) {
super();
this.selector = selector;
}
init() {
const self = this;
this._addClickHandlerReducedInTableRow(this.selector, function(href) {
jQuery.ajax({
url: href,
success: function(html) {
self._openFormInModal(html);
},
error: function(xhr, err) {
console.log('Failed opening modal', err);
window.location = href;
}
});
});
}
_openFormInModal(html) {
const self = this;
// the modal that we use to render the form in
let formIdentifier = '#remote_form_modal .modal-content form';
let flashErrorIdentifier = 'div.alert-error';
let form = jQuery(formIdentifier);
let remoteModal = jQuery('#remote_form_modal');
// will be (re-)activated later
form.off('submit');
// load new form from given content
if (jQuery(html).find('#form_modal .modal-content').length > 0 ) {
// switch classes, in case the modal type changed
remoteModal.on('hidden.bs.modal', function () {
if (remoteModal.hasClass('modal-danger')) {
remoteModal.removeClass('modal-danger');
}
});
if (jQuery(html).find('#form_modal').hasClass('modal-danger')) {
remoteModal.addClass('modal-danger');
}
jQuery('#remote_form_modal .modal-content').replaceWith(
jQuery(html).find('#form_modal .modal-content')
);
// activate new loaded widgets
self.getContainer().getPlugin('date-time-picker').activateDateTimePicker(formIdentifier);
}
// show error flash messages
if (jQuery(html).find(flashErrorIdentifier).length > 0) {
jQuery('#remote_form_modal .modal-body').prepend(
jQuery(html).find(flashErrorIdentifier)
);
}
// -----------------------------------------------------------------------
// a fix for firefox focus problems with datepicker in modal
// see https://github.com/kevinpapst/kimai2/issues/618
let enforceModalFocusFn = jQuery.fn.modal.Constructor.prototype.enforceFocus;
jQuery.fn.modal.Constructor.prototype.enforceFocus = function() {};
remoteModal.on('hidden.bs.modal', function () {
jQuery.fn.modal.Constructor.prototype.enforceFocus = enforceModalFocusFn;
});
// -----------------------------------------------------------------------
// workaround for autofocus attribute, as the modal "steals" it
remoteModal.on('shown.bs.modal', function () {
jQuery(this).find('input[type=text],textarea,select').filter(':not("[data-datetimepicker=on]")').filter(':visible:first').focus().delay(1000).focus();
});
remoteModal.modal('show');
// the new form that was loaded via ajax
form = jQuery(formIdentifier);
// click handler for modal save button, to send forms via ajax
form.on('submit', function(event){
let btn = jQuery(formIdentifier + ' button[type=submit]').button('loading');
event.preventDefault();
event.stopPropagation();
jQuery.ajax({
url: form.attr('action'),
type: form.attr('method'),
data: form.serialize(),
success: function(html) {
btn.button('reset');
let hasFieldError = jQuery(html).find('#form_modal .modal-content .has-error').length > 0;
let hasFormError = jQuery(html).find('#form_modal .modal-content ul.list-unstyled li.text-danger').length > 0;
let hasFlashError = jQuery(html).find(flashErrorIdentifier).length > 0;
if (hasFieldError || hasFormError || hasFlashError) {
self._openFormInModal(html);
} else {
self.getContainer().getPlugin('datatable').reload();
remoteModal.modal('hide');
}
return false;
},
error: function(xhr, err) {
console.log('Failed submitting modal form', 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();
}
});
});
}
}

View File

@@ -0,0 +1,31 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiAlternativeLinks
*
* allows to assign the given selector to any element, which then is used as click-handler
* redirecting to the URL given in the elements 'data-href' or 'href' attribute
*/
import jQuery from 'jquery';
import KimaiClickHandlerReducedInTableRow from "./KimaiClickHandlerReducedInTableRow";
export default class KimaiAlternativeLinks extends KimaiClickHandlerReducedInTableRow {
constructor(selector) {
super();
this.selector = selector;
}
init() {
this._addClickHandlerReducedInTableRow(this.selector, function(href) {
window.location = href;
});
}
}

View File

@@ -0,0 +1,48 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiClickHandlerReducedInTableRow: abstract class
*/
import jQuery from 'jquery';
import KimaiPlugin from "../KimaiPlugin";
export default class KimaiClickHandlerReducedInTableRow extends KimaiPlugin {
_addClickHandlerReducedInTableRow(selector, callback)  {
jQuery('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
let target = event.target;
if (event.currentTarget.matches('tr')) {
while (!target.matches('body')) {
if (target.matches('a') || target.matches ('button')) {
return;
}
target = target.parentNode;
}
}
event.preventDefault();
event.stopPropagation();
let href = jQuery(this).attr('data-href');
if (!href) {
href = jQuery(this).attr('href');
}
callback(href);
});
}
}

View File

@@ -0,0 +1,55 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiDatatable: handles functionality for the datatable
*/
import jQuery from 'jquery';
import KimaiPlugin from "../KimaiPlugin";
export default class KimaiDatatable extends KimaiPlugin {
getId() {
return 'datatable';
}
init() {
const self = this;
document.addEventListener('KimaiDatatableRequestReload', function() {
self.reload();
});
}
reload() {
let form = jQuery('.toolbar form');
let loading = '<div class="overlay"><i class="fas fa-sync fa-spin"></i></div>';
jQuery('section.content').append(loading);
// remove the empty fields to prevent errors
let formData = jQuery('.toolbar form :input')
.filter(function(index, element) {
return jQuery(element).val() != '';
})
.serialize();
jQuery.ajax({
url: form.attr('action'),
type: form.attr('method'),
data: formData,
success: function(html) {
jQuery('section.content').replaceWith(
jQuery(html).find('section.content')
);
},
error: function(xhr, err) {
form.submit();
}
});
}
}

View File

@@ -0,0 +1,95 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiDatatableColumnView: manages the visibility of data-table columns in cookies
*/
import Cookies from 'js-cookie';
import jQuery from 'jquery';
import KimaiPlugin from "../KimaiPlugin";
export default class KimaiDatatableColumnView extends KimaiPlugin {
constructor(dataAttribute) {
super();
this.dataAttribute = dataAttribute;
}
getId() {
return 'datatable-column-visibility';
}
init() {
let dataTable = document.querySelector('[' + this.dataAttribute + ']');
if (dataTable === null) {
return;
}
this.id = dataTable.getAttribute(this.dataAttribute);
this.modal = document.getElementById('modal_' + this.id);
this.bindButtons();
}
bindButtons() {
let self = this;
this.modal.querySelector('button[data-type=save]').addEventListener('click', function() {
self.saveVisibility();
});
this.modal.querySelector('button[data-type=reset]').addEventListener('click', function() {
self.resetVisibility();
});
for (let checkbox of this.modal.querySelectorAll('form input[type=checkbox]')) {
checkbox.addEventListener('click', function () {
self.changeVisibility(checkbox.getAttribute('name'));
});
}
}
saveVisibility() {
const form = this.modal.getElementsByTagName('form')[0];
let settings = {};
for (let checkbox of form.querySelectorAll('input[type=checkbox]')) {
settings[checkbox.getAttribute('name')] = checkbox.checked;
}
Cookies.set(form.getAttribute('name'), JSON.stringify(settings), {expires: 365});
jQuery(this.modal).modal('toggle');
}
resetVisibility() {
const form = this.modal.getElementsByTagName('form')[0];
Cookies.remove(form.getAttribute('name'));
for (let checkbox of form.querySelectorAll('input[type=checkbox]')) {
if (!checkbox.checked) {
checkbox.click();
}
}
jQuery(this.modal).modal('toggle');
}
changeVisibility(columnName) {
const table = document.getElementById('datatable_' + this.id).getElementsByClassName('dataTable')[0];
let column = 0;
let foundColumn = false;
for (let columnElement of table.getElementsByTagName('th')) {
if (columnElement.getAttribute('data-field') === columnName) {
foundColumn = true;
break;
}
column++;
}
if (!foundColumn) {
console.error('Could not find column: ' + columnName);
return;
}
for (let rowElement of table.getElementsByTagName('tr')) {
rowElement.children[column].classList.toggle('hidden');
}
}
}

View File

@@ -0,0 +1,54 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiDatePicker: single date selects (currently unused)
*/
import jQuery from 'jquery';
import KimaiPlugin from '../KimaiPlugin';
export default class KimaiDatePicker extends KimaiPlugin {
constructor(selector) {
super();
this.selector = selector;
}
getId() {
return 'date-picker';
}
init() {
this.activateDatePicker(this.selector);
}
activateDatePicker(selector) {
let translator = this.getContainer().getTranslation();
jQuery(selector + ' input[data-datepickerenable="on"]').each(function(index) {
let localeFormat = jQuery(this).data('format');
jQuery(this).daterangepicker({
singleDatePicker: true,
showDropdowns: true,
autoUpdateInput: false,
locale: {
format: localeFormat,
firstDay: 1,
applyLabel: translator.get('apply'),
cancelLabel: translator.get('cancel'),
customRangeLabel: translator.get('customRange')
}
});
jQuery(this).on('apply.daterangepicker', function(ev, picker) {
jQuery(this).val(picker.startDate.format(localeFormat));
jQuery(this).trigger("change");
});
});
}
}

View File

@@ -0,0 +1,71 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiDateRangePicker: activate the (daterange picker) compound field in toolbar
*/
import moment from 'moment';
import jQuery from 'jquery';
import KimaiPlugin from '../KimaiPlugin';
export default class KimaiDateRangePicker extends KimaiPlugin {
constructor(selector) {
super();
this.selector = selector;
}
getId() {
return 'date-range-picker';
}
init() {
this.activateDateRangePicker(this.selector);
}
activateDateRangePicker(selector) {
let translator = this.getContainer().getTranslation();
jQuery(selector + ' input[data-daterangepickerenable="on"]').each(function(index) {
let localeFormat = jQuery(this).data('format');
let separator = jQuery(this).data('separator');
let rangesList = {};
rangesList[translator.get('today')] = [moment(), moment()];
rangesList[translator.get('yesterday')] = [moment().subtract(1, 'days'), moment().subtract(1, 'days')];
rangesList[translator.get('thisWeek')] = [moment().startOf('week'), moment().endOf('week')];
rangesList[translator.get('lastWeek')] = [moment().subtract(1, 'week').startOf('week'), moment().subtract(1, 'week').endOf('week')];
rangesList[translator.get('thisMonth')] = [moment().startOf('month'), moment().endOf('month')];
rangesList[translator.get('lastMonth')] = [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')];
rangesList[translator.get('thisYear')] = [moment().startOf('year'), moment().endOf('year')];
rangesList[translator.get('lastYear')] = [moment().subtract(1, 'year').startOf('year'), moment().subtract(1, 'year').endOf('year')];
jQuery(this).daterangepicker({
showDropdowns: true,
autoUpdateInput: false,
autoApply: false,
linkedCalendars: false,
locale: {
separator: separator,
format: localeFormat,
firstDay: 1,
applyLabel: translator.get('apply'),
cancelLabel: translator.get('cancel'),
customRangeLabel: translator.get('customRange')
},
ranges: rangesList,
alwaysShowCalendars: true
});
jQuery(this).on('apply.daterangepicker', function(ev, picker) {
jQuery(this).val(picker.startDate.format(localeFormat) + ' - ' + picker.endDate.format(localeFormat));
jQuery(this).trigger("change");
});
});
}
}

View File

@@ -0,0 +1,58 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiDateTimePicker: activate the (datetime picker) field in timesheet edit dialog
*/
import jQuery from 'jquery';
import KimaiPlugin from '../KimaiPlugin';
export default class KimaiDateTimePicker extends KimaiPlugin {
constructor(selector) {
super();
this.selector = selector;
}
getId() {
return 'date-time-picker';
}
init() {
this.activateDateTimePicker(this.selector);
}
activateDateTimePicker(selector) {
let translator = this.getContainer().getTranslation();
let configuration = this.getContainer().getConfiguration();
jQuery(selector + ' input[data-datetimepicker="on"]').each(function(index) {
let localeFormat = jQuery(this).data('format');
jQuery(this).daterangepicker({
singleDatePicker: true,
timePicker: true,
timePicker24Hour: configuration.get('twentyFourHours'),
showDropdowns: true,
autoUpdateInput: false,
locale: {
format: localeFormat,
firstDay: 1,
applyLabel: translator.get('apply'),
cancelLabel: translator.get('cancel'),
customRangeLabel: translator.get('customRange')
}
});
jQuery(this).on('apply.daterangepicker', function(ev, picker) {
jQuery(this).val(picker.startDate.format(localeFormat));
jQuery(this).trigger("change");
});
});
}
}

View File

@@ -0,0 +1,24 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiJqueryPluginInitializer: initialize jQuery plugins
*/
import jQuery from 'jquery';
import KimaiPlugin from '../KimaiPlugin';
export default class KimaiJqueryPluginInitializer extends KimaiPlugin {
init() {
// activate the dropdown functionality
jQuery('.dropdown-toggle').dropdown();
// activate the tooltip functionality
jQuery('[data-toggle="tooltip"]').tooltip();
}
}

View File

@@ -0,0 +1,41 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiPauseRecord
*
* allows to pause records
* THIS IS JUST A DRAFT FOR THE DOM, IT IS NOT SUPPORTED IN KIMAI ITSELF!
*/
import jQuery from 'jquery';
import KimaiPlugin from '../KimaiPlugin';
export default class KimaiPauseRecord extends KimaiPlugin {
constructor(selector) {
super();
this.selector = selector;
}
init() {
this.activate(this.selector);
}
activate(selector) {
jQuery(selector + ' .pull-left i').hover(function () {
let link = jQuery(this).parents('a');
link.attr('href', link.attr('href').replace('/stop', '/pause'));
jQuery(this).removeClass('fa-stop-circle').addClass('fa-pause-circle').addClass('text-orange');
},function () {
let link = jQuery(this).parents('a');
link.attr('href', link.attr('href').replace('/pause', '/stop'));
jQuery(this).removeClass('fa-pause-circle').removeClass('text-orange').addClass('fa-stop-circle');
});
}
}

View File

@@ -0,0 +1,81 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiSelectDataAPI: <select> boxes with dynamic data from API
*/
import jQuery from 'jquery';
import KimaiPlugin from "../KimaiPlugin";
export default class KimaiSelectDataAPI extends KimaiPlugin {
constructor(selector) {
super();
this.selector = selector;
}
getId() {
return 'select-data-api';
}
init() {
this.activateApiSelects(this.selector);
}
activateApiSelects(selector) {
const self = this;
const API = this.getContainer().getPlugin('api');
jQuery('body').on('change', selector, function(event) {
let apiUrl = jQuery(this).attr('data-api-url').replace('-s-', jQuery(this).val());
const targetSelect = '#' + jQuery(this).attr('data-related-select');
// if the related target select does not exist, we do not need to load the related data
if (jQuery(targetSelect).length === 0) {
return;
}
if (jQuery(this).val() === '') {
if (jQuery(this).attr('data-empty-url') === undefined) {
self._updateSelect(targetSelect, {});
jQuery(targetSelect).attr('disabled', 'disabled');
return;
}
apiUrl = jQuery(this).attr('data-empty-url').replace('-s-', jQuery(this).val());
}
jQuery(targetSelect).removeAttr('disabled');
API.get(apiUrl, function(data){
self._updateSelect(targetSelect, data);
});
});
}
_updateSelect(selectName, data) {
let select = jQuery(selectName);
let emptyOption = jQuery(selectName + ' option[value=""]');
select.find('option').remove().end().find('optgroup').remove().end();
if (emptyOption.length !== 0) {
select.append('<option value="">' + emptyOption.text() + '</option>');
}
jQuery.each(data, function(i, obj) {
select.append('<option value="' + obj.id + '">' + obj.name + '</option>');
});
// 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
jQuery('.selectpicker').selectpicker('refresh');
}
}

View File

@@ -0,0 +1,48 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiThemeInitializer: initialize theme functionality
*/
import jQuery from 'jquery';
import KimaiPlugin from '../KimaiPlugin';
export default class KimaiThemeInitializer extends KimaiPlugin {
init() {
this.registerAutomaticAlertRemove('div.alert-success', 5000);
}
/**
* auto hide success messages, as they are just meant as user feedback and not as a permanent information
*
* @param {string} selector
* @param {integer} interval
*/
registerAutomaticAlertRemove(selector, interval) {
const self = this;
this._alertRemoveHandler = setInterval(
function() {
self.hideAlert(selector);
},
interval
);
}
unregisterAutomaticAlertRemove() {
clearInterval(this._alertRemoveHandler);
}
/**
* @param {string} selector
*/
hideAlert(selector) {
jQuery(selector).alert('close');
}
}

View File

@@ -0,0 +1,73 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiToolbar: some event listener to handle the toolbar/data-table filter, toolbar and navigation
*/
import jQuery from 'jquery';
import KimaiPlugin from "../KimaiPlugin";
export default class KimaiToolbar extends KimaiPlugin {
init() {
const self = this;
// This catches all clicks on the pagination and prevents the default action, as we want to relad the page via JS
jQuery('body').on('click', 'div.navigation ul.pagination li a', function(event) {
let pager = jQuery(".toolbar form input[name='page']");
if (pager.length === 0) {
return;
}
event.preventDefault();
event.stopPropagation();
let urlParts = jQuery(this).attr('href').split('/');
let page = urlParts[urlParts.length-1];
pager.val(page);
pager.trigger('change');
return false;
});
// Reset the page if any other value is changed, otherwise we might end up with a limited set
// of data which does not support the given page - and it would be just wrong to stay in the same page
jQuery('.toolbar form input').change(function (event) {
switch (event.target.id) {
case 'page':
break;
default:
jQuery('.toolbar form input#page').val(1);
}
self._reloadDatatable();
});
jQuery('.toolbar form select').change(function (event) {
let reload = true;
switch (event.target.id) {
case 'customer':
if (jQuery('.toolbar form select#project').length > 0) {
reload = false;
}
break;
case 'project':
if (jQuery('.toolbar form select#activity').length > 0) {
reload = false;
}
break;
}
jQuery('.toolbar form input#page').val(1);
if (reload) {
self._reloadDatatable();
}
});
}
_reloadDatatable() {
this.getContainer().getPlugin('datatable').reload();
}
}

View File

@@ -1,61 +0,0 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] Toolbar: some helper scripts for data-table filter, toolbar and navigation
*/
$(document).ready(function () {
// This catches all clicks on the pagination and prevents the default action, as we want to relad the page via JS
$('body').on('click', 'div.navigation ul.pagination li a', function(event) {
var $pager = $(".toolbar form input[name='page']");
if ($pager.length === 0) {
return;
}
event.preventDefault();
event.stopPropagation();
var $urlParts = $(this).attr('href').split('/');
var page = $urlParts[$urlParts.length-1];
$pager.val(page);
$pager.trigger('change');
return false;
});
// Reset the page if any other value is changed, otherwise we might end up with a limited set
// of data which does not support the given page - and it would be just wrong to stay in the same page
$('.toolbar form input').change(function (event) {
switch (event.target.id) {
case 'page':
break;
default:
$('.toolbar form input#page').val(1);
}
$.kimai.reloadDatatableWithToolbarFilter();
});
$('.toolbar form select').change(function (event) {
var reload = true;
switch (event.target.id) {
case 'customer':
if ($('.toolbar form select#project').length > 0) {
reload = false;
}
break;
case 'project':
if ($('.toolbar form select#activity').length > 0) {
reload = false;
}
break;
}
$('.toolbar form input#page').val(1);
if (reload) {
$.kimai.reloadDatatableWithToolbarFilter();
}
});
});

View File

@@ -17,6 +17,9 @@
overflow: hidden;
text-overflow: ellipsis;
}
.messages-menu>.dropdown-menu>li .menu>li>a>h4>span {
margin-right: 55px;
}
.start_record {
text-align: center;
}

View File

@@ -20,6 +20,7 @@
"jquery-slimscroll": "^1.3.8",
"jquery-ui": "^1.12.1",
"js-cookie": "^2.2.0",
"moment": "^2.24.0",
"node-sass": "^4.9.0",
"sass-loader": "^7.0.2",
"webpack-notifier": "^1.5.1"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{
"build/app.js": "./app.js?[contenthash]",
"build/app.css": "./app.css?361d4520765f893b9b0fb94bc9f761c1",
"build/app.js": "./app.js?513aa9647de2030610d0",
"build/app.css": "./app.css?b3b98e5f8744bf95f3f8d8352977d4f3",
"build/fonts/fa-solid-900.woff2": "./fonts/fa-solid-900.woff2?e8a92a29",
"build/images/fa-solid-900.svg": "./images/fa-solid-900.svg?666a82cb",
"build/images/glyphicons-halflings-regular.svg": "./images/glyphicons-halflings-regular.svg?89889688",

View File

@@ -171,26 +171,26 @@
{% block javascripts %}
{# no call to parent(), as we use a custom built for the frontend assets and don't want the default <script> #}
<script type="text/javascript">
$(document).ready(function () {
$.kimai.init({
locale: '{{ app.request.locale }}',
apply: '{{ 'daterangepicker.apply'|trans({}, 'daterangepicker') }}',
cancel: '{{ 'daterangepicker.cancel'|trans({}, 'daterangepicker') }}',
today: '{{ 'daterangepicker.today'|trans({}, 'daterangepicker') }}',
yesterday: '{{ 'daterangepicker.yesterday'|trans({}, 'daterangepicker') }}',
lastWeek: '{{ 'daterangepicker.lastWeek'|trans({}, 'daterangepicker') }}',
thisWeek: '{{ 'daterangepicker.thisWeek'|trans({}, 'daterangepicker') }}',
lastMonth: '{{ 'daterangepicker.lastMonth'|trans({}, 'daterangepicker') }}',
thisMonth: '{{ 'daterangepicker.thisMonth'|trans({}, 'daterangepicker') }}',
lastYear: '{{ 'daterangepicker.lastYear'|trans({}, 'daterangepicker') }}',
thisYear: '{{ 'daterangepicker.thisYear'|trans({}, 'daterangepicker') }}',
customRange: '{{ 'daterangepicker.customRange'|trans({}, 'daterangepicker') }}',
twentyFourHours: {{ 'true'|hour24('false') }}
});
{# $.kimai.pauseRecord('li.messages-menu ul.menu li'); #}
});
document.addEventListener('DOMContentLoaded', function() {
new KimaiActiveRecordsDuration('[data-since]').updateRecords().registerUpdates(5000);
new KimaiWebLoader(
{
locale: '{{ app.request.locale }}',
twentyFourHours: {{ 'true'|hour24('false') }}
},
{
apply: '{{ 'daterangepicker.apply'|trans({}, 'daterangepicker') }}',
cancel: '{{ 'daterangepicker.cancel'|trans({}, 'daterangepicker') }}',
today: '{{ 'daterangepicker.today'|trans({}, 'daterangepicker') }}',
yesterday: '{{ 'daterangepicker.yesterday'|trans({}, 'daterangepicker') }}',
lastWeek: '{{ 'daterangepicker.lastWeek'|trans({}, 'daterangepicker') }}',
thisWeek: '{{ 'daterangepicker.thisWeek'|trans({}, 'daterangepicker') }}',
lastMonth: '{{ 'daterangepicker.lastMonth'|trans({}, 'daterangepicker') }}',
thisMonth: '{{ 'daterangepicker.thisMonth'|trans({}, 'daterangepicker') }}',
lastYear: '{{ 'daterangepicker.lastYear'|trans({}, 'daterangepicker') }}',
thisYear: '{{ 'daterangepicker.thisYear'|trans({}, 'daterangepicker') }}',
customRange: '{{ 'daterangepicker.customRange'|trans({}, 'daterangepicker') }}'
}
);
});
</script>
{% set event = trigger(constant('App\\Event\\ThemeEvent::JAVASCRIPT')) %}

View File

@@ -1,6 +1,6 @@
{% macro data_table_column_modal(name, columns) %}
<div class="modal fade" id="modal_{{ name }}" tabindex="-1" role="dialog" aria-labelledby="data_table_modal_label">
<div class="modal fade" id="modal_{{ name }}" data-column-visibility="{{ name }}" tabindex="-1" role="dialog" aria-labelledby="data_table_modal_label">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
@@ -28,11 +28,6 @@
</div>
</div>
</div>
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', function() {
new KimaiDatatableColumnView('{{ name }}');
});
</script>
{% endmacro %}
{% macro data_table_column_class(name, columns, column) %}

View File

@@ -20,7 +20,6 @@
<h4>
<span>{{ entry.activity.name }}</span>
<small>
<i class="{{ 'timesheet'|icon }}"></i>
<span data-title="true" data-since="{{ entry.begin.format(constant('DATE_ISO8601')) }}" data-format="{{ get_format_duration() }}">{{ entry|duration }}</span>
</small>
</h4>

View File

@@ -33,7 +33,7 @@
{{ tables.data_table_header(tableName, columns) }}
{% for entry in entries %}
<tr{% if is_granted('edit', entry) %} class="open-edit" onclick="location.href='{{ path('user_profile_edit', {'username': entry.username}) }}'"{% endif %}>
<tr{% if is_granted('edit', entry) %} class="open-edit alternative-link" data-href="{{ path('user_profile_edit', {'username': entry.username}) }}"{% endif %}>
<td>{{ widgets.username(entry) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'username') }}">{{ entry.username }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'email') }}">{{ entry.email }}</td>

View File

@@ -41,7 +41,7 @@ Encore
// add hash after file name
.configureFilenames({
js: '[name].js?[contenthash]',
js: '[name].js?[chunkhash]',
css: '[name].css?[contenthash]',
images: 'images/[name].[ext]?[hash:8]',
fonts: 'fonts/[name].[ext]?[hash:8]'

View File

@@ -3888,6 +3888,11 @@ moment@^2.10.2, moment@^2.18.1, moment@^2.20.1, moment@^2.9.0:
version "2.22.2"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66"
moment@^2.24.0:
version "2.24.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
morris.js@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/morris.js/-/morris.js-0.5.0.tgz#725767135cfae059aae75999bb2ce6a1c5d1b44b"