diff --git a/application/forms/PreferenceForm.php b/application/forms/PreferenceForm.php index 9df7d1bcb..7f4297478 100644 --- a/application/forms/PreferenceForm.php +++ b/application/forms/PreferenceForm.php @@ -94,6 +94,7 @@ class PreferenceForm extends Form $this->preferences = new Preferences($this->store ? $this->store->load() : array()); $oldTheme = $this->preferences->getValue('icingaweb', 'theme'); + $oldLocale = $this->preferences->getValue('icingaweb', 'language'); $webPreferences = $this->preferences->get('icingaweb', array()); foreach ($this->getValues() as $key => $value) { @@ -118,6 +119,12 @@ class PreferenceForm extends Form $this->getResponse()->setReloadCss(true); } + if (($locale = $this->getElement('language')) !== null + && $locale->getValue() !== $oldLocale + ) { + $this->getResponse()->setHeader('X-Icinga-Redirect-Http', 'yes'); + } + try { if ($this->store && $this->getElement('btn_submit')->isChecked()) { $this->save(); diff --git a/application/layouts/scripts/layout.phtml b/application/layouts/scripts/layout.phtml index 27526ac2c..1d22c9798 100644 --- a/application/layouts/scripts/layout.phtml +++ b/application/layouts/scripts/layout.phtml @@ -11,6 +11,7 @@ if (array_key_exists('_dev', $_GET)) { $cssfile = 'css/icinga.min.css'; } +$timezone = date_default_timezone_get(); $lang = Translator::splitLocaleCode()->language; $isIframe = $this->layout()->isIframe; $showFullscreen = $this->layout()->showFullscreen; @@ -93,7 +94,9 @@ $innerLayoutScript = $this->layout()->innerLayout . '.phtml'; diff --git a/application/views/helpers/FormDateTime.php b/application/views/helpers/FormDateTime.php index 3b5ea7de0..de5eb4b67 100644 --- a/application/views/helpers/FormDateTime.php +++ b/application/views/helpers/FormDateTime.php @@ -49,7 +49,7 @@ class Zend_View_Helper_FormDateTime extends Zend_View_Helper_FormElement $type = $attribs['local'] === true ? 'datetime-local' : 'datetime'; unset($attribs['local']); // Unset local to not render it again in $this->_htmlAttribs($attribs) $html5 = sprintf( - 'view->escape($name), $this->view->escape($id), diff --git a/library/Icinga/Authentication/Auth.php b/library/Icinga/Authentication/Auth.php index 1fa4887be..adcb9935f 100644 --- a/library/Icinga/Authentication/Auth.php +++ b/library/Icinga/Authentication/Auth.php @@ -111,6 +111,13 @@ class Auth } } + // Reload entire layout if the locale changed + if (($locale = $user->getPreferences()->getValue('icingaweb', 'language')) !== null) { + if (setlocale(LC_ALL, 0) !== $locale && $this->getRequest()->isXmlHttpRequest()) { + $this->getResponse()->setHeader('X-Icinga-Redirect-Http', 'yes'); + } + } + $this->user = $user; if ($persist) { $this->persistCurrentUser(); diff --git a/library/Icinga/Web/JavaScript.php b/library/Icinga/Web/JavaScript.php index f3bb8bf94..02c66f702 100644 --- a/library/Icinga/Web/JavaScript.php +++ b/library/Icinga/Web/JavaScript.php @@ -42,7 +42,8 @@ class JavaScript 'js/icinga/behavior/filtereditor.js', 'js/icinga/behavior/selectable.js', 'js/icinga/behavior/modal.js', - 'js/icinga/behavior/input-enrichment.js' + 'js/icinga/behavior/input-enrichment.js', + 'js/icinga/behavior/datetime-picker.js' ]; protected static $vendorFiles = [ diff --git a/public/css/icinga/modal.less b/public/css/icinga/modal.less index e438ec34e..5b2be5014 100644 --- a/public/css/icinga/modal.less +++ b/public/css/icinga/modal.less @@ -37,7 +37,7 @@ > .content { padding: 1em; - > * { + > .icinga-form { width: 100%; } } diff --git a/public/css/themes/solarized-dark.less b/public/css/themes/solarized-dark.less index c4cdd7687..3e6a90f89 100644 --- a/public/css/themes/solarized-dark.less +++ b/public/css/themes/solarized-dark.less @@ -57,6 +57,33 @@ // Form colors @button-primary-color: lighten(@base02, 20); +// Datetime picker colors +@fp-calendarBackground: #3f4458; +@fp-calendarBorderColor: darken(#3f4458, 50%); + +@fp-monthForeground: #fff; +@fp-monthBackground: #3f4458; + +@fp-weekdaysBackground: transparent; +@fp-weekdaysForeground: #fff; + +@fp-dayForeground: fadeout(white, 5%); +@fp-dayHoverBackground: lighten(@fp-calendarBackground, 25%); + +@fp-todayColor: #eee; +@fp-today_fg_color: #3f4458; + +@fp-selectedDayBackground: #80CBC4; + +.icinga-datetime-picker .flatpickr-day.today { + &:hover, + &:focus { + color: @fp-today_fg_color; + } +} + +// Datetime picker colors (end) + #sidebar { background-color: @base02; box-shadow: inset -0.5em 0 1em rgba(0,0,0,0.3); diff --git a/public/js/define.js b/public/js/define.js index 059e3ef80..a3ce8c67a 100644 --- a/public/js/define.js +++ b/public/js/define.js @@ -60,17 +60,33 @@ */ define.resolve = function (name) { var requirements = define.defines[name]['requirements']; - if (requirements.filter(define.has).length < requirements.length) { - return false; - } + var exports, ref; var requiredRefs = []; for (var i = 0; i < requirements.length; i++) { - requiredRefs.push(define.get(requirements[i])); + if (define.has(requirements[i])) { + ref = define.get(requirements[i]); + } else if (requirements[i] === 'exports') { + exports = ref = {}; + } else { + return false; + } + + requiredRefs.push(ref); } var factory = define.defines[name]['factory']; - define.set(name, factory.apply(null, requiredRefs)); + var resolved = factory.apply(null, requiredRefs); + + if (typeof exports === 'object') { + if (typeof resolved !== 'undefined') { + throw new Error('Factory for ' + name + ' returned, although exports were populated'); + } + + resolved = exports; + } + + define.set(name, resolved); for (var definedName in define.defines) { if (define.defines[definedName]['requirements'].indexOf(name) >= 0) { diff --git a/public/js/icinga/behavior/datetime-picker.js b/public/js/icinga/behavior/datetime-picker.js new file mode 100644 index 000000000..9f1c23b94 --- /dev/null +++ b/public/js/icinga/behavior/datetime-picker.js @@ -0,0 +1,186 @@ +/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */ + +/** + * DatetimePicker - Behavior for inputs that should show a date and time picker + */ +;(function(Icinga, $) { + + 'use strict'; + + try { + var Flatpickr = require('icinga/ipl/vendor/flatpickr'); + var notjQuery = require('icinga/ipl/notjQuery'); + } catch (e) { + console.warn('Unable to provide datetime picker. Libraries not available:', e); + return; + } + + Icinga.Behaviors = Icinga.Behaviors || {}; + + /** + * Behavior for datetime pickers. + * + * @param icinga {Icinga} The current Icinga Object + */ + var DatetimePicker = function(icinga) { + Icinga.EventListener.call(this, icinga); + this.icinga = icinga; + + /** + * The formats the server expects + * + * In a syntax flatpickr understands. Based on https://flatpickr.js.org/formatting/ + * + * @type {string} + */ + this.server_full_format = 'Y-m-d\\TH:i:S'; + this.server_date_format = 'Y-m-d'; + this.server_time_format = 'H:i:S'; + + /** + * The flatpickr instances created + * + * @type {Map} + * @private + */ + this._pickers = new Map(); + + this.on('rendered', this.onRendered, this); + this.on('close-column', this.onCloseContainer, this); + this.on('close-modal', this.onCloseContainer, this); + }; + + DatetimePicker.prototype = new Icinga.EventListener(); + + /** + * Add flatpickr widget on selected inputs + * + * @param event {Event} + */ + DatetimePicker.prototype.onRendered = function(event) { + var _this = event.data.self; + var containerId = event.target.dataset.icingaContainerId; + var inputs = event.target.querySelectorAll('input[data-use-datetime-picker]'); + + // Cleanup left-over pickers from the previous content + _this.cleanupPickers(containerId); + + $.each(inputs, function () { + var server_format = _this.server_full_format; + if (this.type === 'date') { + server_format = _this.server_date_format; + } else if (this.type === 'time') { + server_format = _this.server_time_format; + } + + var enableTime = server_format !== _this.server_date_format; + var disableDate = server_format === _this.server_time_format; + var dateTimeFormatter = _this.createFormatter(! disableDate, enableTime); + var options = { + locale: _this.loadFlatpickrLocale(), + appendTo: this.form.parentNode, + altInput: true, + enableTime: enableTime, + noCalendar: disableDate, + dateFormat: server_format, + formatDate: function (date, format, locale) { + return format === this.dateFormat + ? Flatpickr.formatDate(date, format, locale) + : dateTimeFormatter.format(date); + } + }; + + for (name in this.dataset) { + if (name.length > 9 && name.substr(0, 9) === 'flatpickr') { + var value = this.dataset[name]; + if (value === '') { + value = true; + } + + options[name.charAt(9).toLowerCase() + name.substr(10)] = value; + } + } + + var fp = Flatpickr(this, options); + fp.calendarContainer.classList.add('icinga-datetime-picker'); + this.parentNode.insertBefore(_this.renderIcon(), fp.altInput.nextSibling); + + _this._pickers.set(fp, containerId); + }); + }; + + /** + * Cleanup all flatpickr instances in the closed container + * + * @param event {Event} + */ + DatetimePicker.prototype.onCloseContainer = function (event) { + var _this = event.data.self; + var containerId = event.target.dataset.icingaContainerId; + + _this.cleanupPickers(containerId); + }; + + /** + * Destroy all flatpickr instances in the container with the given id + * + * @param containerId {String} + */ + DatetimePicker.prototype.cleanupPickers = function (containerId) { + this._pickers.forEach(function (cId, fp) { + if (cId === containerId) { + this._pickers.delete(fp); + fp.destroy(); + } + }, this); + }; + + DatetimePicker.prototype.createFormatter = function (withDate, withTime) { + var options = {}; + if (withDate) { + options.year = 'numeric'; + options.month = 'numeric'; + options.day = 'numeric'; + } + if (withTime) { + options.hour = 'numeric'; + options.minute = 'numeric'; + options.timeZoneName = 'short'; + options.timeZone = this.icinga.config.timezone; + } + + return new Intl.DateTimeFormat([this.icinga.config.locale, 'en'], options); + }; + + DatetimePicker.prototype.loadFlatpickrLocale = function () { + switch (this.icinga.config.locale) { + case 'ar': + return require('icinga/ipl/vendor/flatpickr/l10n/ar').Arabic; + case 'de': + return require('icinga/ipl/vendor/flatpickr/l10n/de').German; + case 'es': + return require('icinga/ipl/vendor/flatpickr/l10n/es').Spanish; + case 'fi': + return require('icinga/ipl/vendor/flatpickr/l10n/fi').Finnish; + case 'it': + return require('icinga/ipl/vendor/flatpickr/l10n/it').Italian; + case 'ja': + return require('icinga/ipl/vendor/flatpickr/l10n/ja').Japanese; + case 'pt': + return require('icinga/ipl/vendor/flatpickr/l10n/pt').Portuguese; + case 'ru': + return require('icinga/ipl/vendor/flatpickr/l10n/ru').Russian; + case 'uk': + return require('icinga/ipl/vendor/flatpickr/l10n/uk').Ukrainian; + default: + return 'default'; + } + }; + + DatetimePicker.prototype.renderIcon = function () { + return notjQuery.render(''); + }; + + Icinga.Behaviors.DatetimePicker = DatetimePicker; + +})(Icinga, jQuery); diff --git a/public/js/icinga/behavior/modal.js b/public/js/icinga/behavior/modal.js index aba6b693e..cd0652506 100644 --- a/public/js/icinga/behavior/modal.js +++ b/public/js/icinga/behavior/modal.js @@ -190,6 +190,7 @@ $modal.removeClass('active'); // Using `setTimeout` here to let the transition finish setTimeout(function () { + $modal.find('#modal-content').trigger('close-modal'); $modal.remove(); }, 200); };