From 9e73fc872b6e8326de1c322bd0681145dbe0da3a Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Mon, 20 Feb 2023 08:26:02 +0100 Subject: [PATCH 01/11] Correct indent in documentation --- Documentation/AdministratorManual/Index.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Documentation/AdministratorManual/Index.rst b/Documentation/AdministratorManual/Index.rst index b69aa80..1f07460 100644 --- a/Documentation/AdministratorManual/Index.rst +++ b/Documentation/AdministratorManual/Index.rst @@ -1,20 +1,20 @@ .. include:: /Includes.rst.txt -.. _admin-manual: +.. _admin-manual: ==================== Administrator manual ==================== -.. only:: html +.. only:: html - This chapter describes how to manage the extension from a superuser point - of view. + This chapter describes how to manage the extension from a superuser point + of view. -.. toctree:: - :maxdepth: 2 - :titlesonly: +.. toctree:: + :maxdepth: 2 + :titlesonly: - Configuration/Index - Upgrade/Index + Configuration/Index + Upgrade/Index From 00533966dd1e2e35ff828c78c879b1f9e3aa33bc Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Mon, 20 Feb 2023 08:26:36 +0100 Subject: [PATCH 02/11] Remove unneeded int cast from recordStoragePage in task --- Classes/Task/DeutscherWetterdienstTask.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Task/DeutscherWetterdienstTask.php b/Classes/Task/DeutscherWetterdienstTask.php index 460192c..e93e26e 100644 --- a/Classes/Task/DeutscherWetterdienstTask.php +++ b/Classes/Task/DeutscherWetterdienstTask.php @@ -221,7 +221,7 @@ protected function getWeatherAlertInstanceForAlert( bool $isPreliminaryInformation ): WeatherAlert { $weatherAlert = new WeatherAlert(); - $weatherAlert->setPid((int)$this->recordStoragePage); + $weatherAlert->setPid($this->recordStoragePage); $weatherAlert->setDwdWarnCell($this->getDwdWarnCell($warnCellId)); $weatherAlert->setComparisonHash($this->getComparisonHashForAlert($alert)); $weatherAlert->setPreliminaryInformation($isPreliminaryInformation); From fd7e590a852ea28bf712f41dcd6dc9beced1f9aa Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Mon, 20 Feb 2023 08:30:00 +0100 Subject: [PATCH 03/11] Implement new php-cs-fixer config file --- .../{.php_cs.php => php-cs-fixer/config.php} | 48 +++++++++---------- composer.json | 19 ++------ 2 files changed, 26 insertions(+), 41 deletions(-) rename Build/{.php_cs.php => php-cs-fixer/config.php} (70%) diff --git a/Build/.php_cs.php b/Build/php-cs-fixer/config.php similarity index 70% rename from Build/.php_cs.php rename to Build/php-cs-fixer/config.php index a2cc4d2..00c82d0 100644 --- a/Build/.php_cs.php +++ b/Build/php-cs-fixer/config.php @@ -1,21 +1,14 @@ name('*.php') ->exclude('.build') - ->exclude('var') ->in(__DIR__); -$config = new PhpCsFixer\Config(); -return $config +return (new \PhpCsFixer\Config()) + ->setFinder($finder) ->setRiskyAllowed(true) ->setRules([ '@DoctrineAnnotation' => true, - '@PSR2' => true, + '@PER' => true, 'header_comment' => [ - 'header' => $headerComment + 'header' => $headerComment, ], 'array_syntax' => ['syntax' => 'short'], 'blank_line_after_opening_tag' => true, @@ -50,9 +42,11 @@ 'concat_space' => ['spacing' => 'one'], 'declare_equal_normalize' => ['space' => 'none'], 'dir_constant' => true, + 'function_to_constant' => ['functions' => ['get_called_class', 'get_class', 'get_class_this', 'php_sapi_name', 'phpversion', 'pi']], 'function_typehint_space' => true, 'lowercase_cast' => true, 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'], + 'modernize_strpos' => true, 'modernize_types_casting' => true, 'native_function_casing' => true, 'new_with_braces' => true, @@ -67,13 +61,14 @@ 'no_short_bool_cast' => true, 'no_singleline_whitespace_before_semicolons' => true, 'no_superfluous_elseif' => true, - 'no_trailing_comma_in_singleline_array' => true, + 'no_trailing_comma_in_singleline' => true, 'no_unneeded_control_parentheses' => true, 'no_unused_imports' => true, 'no_useless_else' => true, + 'no_useless_nullsafe_operator' => true, 'no_whitespace_in_blank_line' => true, 'ordered_imports' => true, - 'php_unit_construct' => true, + 'php_unit_construct' => ['assertions' => ['assertEquals', 'assertSame', 'assertNotEquals', 'assertNotSame']], 'php_unit_mock_short_will_return' => true, 'php_unit_test_case_static_method_calls' => ['call_type' => 'self'], 'phpdoc_no_access' => true, @@ -84,9 +79,10 @@ 'phpdoc_types' => true, 'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'], 'return_type_declaration' => ['space_before' => 'none'], - 'single_line_comment_style' => false, 'single_quote' => true, + 'single_line_comment_style' => ['comment_types' => ['hash']], 'single_trait_insert_per_statement' => true, - 'whitespace_after_comma_in_array' => true - ]) - ->setFinder($finder); + 'trailing_comma_in_multiline' => ['elements' => ['arrays']], + 'whitespace_after_comma_in_array' => ['ensure_single_space' => true], + 'yoda_style' => ['equal' => false, 'identical' => false, 'less_and_greater' => false], + ]); diff --git a/composer.json b/composer.json index fcd2e69..be0e6fa 100644 --- a/composer.json +++ b/composer.json @@ -3,20 +3,9 @@ "type": "typo3-cms-extension", "description": "Display weather data and weather alerts using various Weather APIs. Default APIs: OpenWeatherMap and Deutscher Wetterdienst", "license": "GPL-2.0-or-later", - "keywords": [ - "typo3", - "TYPO3 CMS", - "weather2", - "weather report", - "weather alert" - ], - "homepage": "http://www.jweiland.net", + "keywords": ["typo3", "TYPO3 CMS", "weather2", "weather report", "weather alert"], + "homepage": "https://www.jweiland.net", "authors": [ - { - "name": "Markus Kugler", - "email": "projects@jweiland.net", - "role": "Developer" - }, { "name": "Stefan Froemken", "email": "projects@jweiland.net", @@ -58,9 +47,9 @@ } }, "scripts": { - "php:fix": ".build/vendor/bin/php-cs-fixer --config=Build/.php_cs.php fix Classes Tests", + "php:fix": ".build/vendor/bin/php-cs-fixer --config=Build/php-cs-fixer/config.php fix Classes Tests", "ci:php:lint": "find *.php Classes Configuration Tests -name '*.php' -print0 | xargs -0 -n 1 -P 4 php -l", - "ci:php:fixer": ".build/vendor/bin/php-cs-fixer --config=Build/.php_cs.php fix --dry-run -v --diff --show-progress=dots Classes Tests", + "ci:php:fixer": ".build/vendor/bin/php-cs-fixer --config=Build/php-cs-fixer/config.php fix --dry-run -v --diff --show-progress=dots Classes Tests", "ci:tests:unit": ".build/vendor/bin/phpunit -c .build/vendor/nimut/testing-framework/res/Configuration/UnitTests.xml Tests/Unit", "ci:tests:functional": "find 'Tests/Functional' -wholename '*Test.php' | parallel --gnu 'echo; echo \"Running functional test suite {}\"; php -d date.timezone=Europe/Berlin .build/vendor/bin/phpunit -c .build/vendor/nimut/testing-framework/res/Configuration/FunctionalTests.xml {}';", "link-extension": [ From 755f409d31f9ed4743ec54a9128ffaf2151c4cd4 Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Mon, 20 Feb 2023 08:38:18 +0100 Subject: [PATCH 04/11] Use let instead of var in JS files --- .../DeutscherWetterdienstTaskModule.js | 10 +- .../JavaScript/OpenWeatherMapTaskModule.js | 36 +- Resources/Public/JavaScript/default.js | 10 +- .../Public/JavaScript/jquery.autocomplete.js | 1832 ++++++++--------- 4 files changed, 942 insertions(+), 946 deletions(-) diff --git a/Resources/Public/JavaScript/DeutscherWetterdienstTaskModule.js b/Resources/Public/JavaScript/DeutscherWetterdienstTaskModule.js index 9892613..eb50aa5 100644 --- a/Resources/Public/JavaScript/DeutscherWetterdienstTaskModule.js +++ b/Resources/Public/JavaScript/DeutscherWetterdienstTaskModule.js @@ -16,13 +16,13 @@ define('TYPO3/CMS/Weather2/DeutscherWetterdienstTaskModule', ['jquery', 'jquery/ if (!$('#dwd_warnCellItem_' + suggestion.data).length) { $('#dwd_selected_warn_cells_ul').append('
  • ' + TYPO3.lang.removeItem + '' + suggestion.value + '
  • '); $('#dwd_warnCellItem_' + suggestion.data + ' .dwd_removeItem').click(function () { - $(this).parent('li').remove(); - }); + $(this).parent('li').remove(); + }); } } - }).keypress(function(e) { - var code = (e.keyCode ? e.keyCode : e.which); - if(code == 13) { + }).keypress(function (e) { + let code = (e.keyCode ? e.keyCode : e.which); + if (code == 13) { return false; } }); diff --git a/Resources/Public/JavaScript/OpenWeatherMapTaskModule.js b/Resources/Public/JavaScript/OpenWeatherMapTaskModule.js index 13c15e0..684c52e 100644 --- a/Resources/Public/JavaScript/OpenWeatherMapTaskModule.js +++ b/Resources/Public/JavaScript/OpenWeatherMapTaskModule.js @@ -3,37 +3,37 @@ * JavaScript to show/hide fields in scheduler */ define('TYPO3/CMS/Weather2/OpenWeatherMapTaskModule', ['jquery'], function ($) { - $(document).ready(function () { + $(document).ready(function () { $('#recordStoragePage').change(function () { $(this).val($(this).val().replace(/[^0-9]/g, '')); }); - - var errorNotificationFields = ['mailConfig_row', 'emailSenderName_row', 'emailSender_row', 'emailReceiver_row']; - $('#errorNotification').click(function () { - toggleErrorNotificationFields(); - }); + let errorNotificationFields = ['mailConfig_row', 'emailSenderName_row', 'emailSender_row', 'emailReceiver_row']; - toggleErrorNotificationFields(); + $('#errorNotification').click(function () { + toggleErrorNotificationFields(); + }); + + toggleErrorNotificationFields(); + + function toggleErrorNotificationFields () { + if ($('#errorNotification').is(':checked')) { + setDisplayAttributeOfElements('', errorNotificationFields); + } else { + setDisplayAttributeOfElements('none', errorNotificationFields); + } + } - function toggleErrorNotificationFields() { - if ($('#errorNotification').is(':checked')) { - setDisplayAttributeOfElements('', errorNotificationFields); - } else { - setDisplayAttributeOfElements('none', errorNotificationFields); - } - } - /** * Sets the display property for each element in array elements * * @param display display property value from css (e.g. block or none) * @param elements array of all elements ['first_element', 'second_element'] */ - function setDisplayAttributeOfElements(display, elements) { + function setDisplayAttributeOfElements (display, elements) { $(elements).each(function (index, value) { $('#' + value).css('display', display); }); } - }); -}); \ No newline at end of file + }); +}); diff --git a/Resources/Public/JavaScript/default.js b/Resources/Public/JavaScript/default.js index 7a5aab7..eb84b25 100644 --- a/Resources/Public/JavaScript/default.js +++ b/Resources/Public/JavaScript/default.js @@ -1,17 +1,17 @@ 'use strict'; -var txWeather2 = '.weather2-item'; +let txWeather2 = '.weather2-item'; jQuery(document).ready(function () { jQuery(txWeather2 + ' .showMore').click(function (e) { e.preventDefault(); - var $weather2Item = $(this).parentsUntil('.weather2-item'); + let $weather2Item = $(this).parentsUntil('.weather2-item'); console.log($weather2Item); - var temp = $(this).html(); + let temp = $(this).html(); $(this).html($(this).attr('toggle-label')); $(this).attr('toggle-label', temp); $weather2Item.next('.secondaryProperties').toggleClass('expanded'); - }); + jQuery(txWeather2 + ' .showMore').show().addClass('notSelectable cursorPointer'); -}); \ No newline at end of file +}); diff --git a/Resources/Public/JavaScript/jquery.autocomplete.js b/Resources/Public/JavaScript/jquery.autocomplete.js index 82daa25..a236290 100644 --- a/Resources/Public/JavaScript/jquery.autocomplete.js +++ b/Resources/Public/JavaScript/jquery.autocomplete.js @@ -1,979 +1,975 @@ /** -* Ajax Autocomplete for jQuery, version %version% -* (c) 2015 Tomas Kirda -* -* Ajax Autocomplete for jQuery is freely distributable under the terms of an MIT-style license. -* For details, see the web site: https://github.com/devbridge/jQuery-Autocomplete -*/ - -/*jslint browser: true, white: true, plusplus: true, vars: true */ -/*global define, window, document, jQuery, exports, require */ + * Ajax Autocomplete for jQuery, version %version% + * (c) 2015 Tomas Kirda + * + * Ajax Autocomplete for jQuery is freely distributable under the terms of an MIT-style license. + * For details, see the web site: https://github.com/devbridge/jQuery-Autocomplete + */ // Expose plugin as an AMD module if AMD loader is present: (function (factory) { - 'use strict'; - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define(['jquery'], factory); - } else if (typeof exports === 'object' && typeof require === 'function') { - // Browserify - factory(require('jquery')); - } else { - // Browser globals - factory(jQuery); - } + 'use strict'; + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else if (typeof exports === 'object' && typeof require === 'function') { + // Browserify + factory(require('jquery')); + } else { + // Browser globals + factory(jQuery); + } }(function ($) { - 'use strict'; - - var - utils = (function () { - return { - escapeRegExChars: function (value) { - return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); - }, - createNode: function (containerClass) { - var div = document.createElement('div'); - div.className = containerClass; - div.style.position = 'absolute'; - div.style.display = 'none'; - return div; - } - }; - }()), - - keys = { - ESC: 27, - TAB: 9, - RETURN: 13, - LEFT: 37, - UP: 38, - RIGHT: 39, - DOWN: 40 - }; - - function Autocomplete(el, options) { - var noop = function () { }, - that = this, - defaults = { - ajaxSettings: {}, - autoSelectFirst: false, - appendTo: document.body, - serviceUrl: null, - lookup: null, - onSelect: null, - width: 'auto', - minChars: 1, - maxHeight: 300, - deferRequestBy: 0, - params: {}, - formatResult: Autocomplete.formatResult, - delimiter: null, - zIndex: 9999, - type: 'GET', - noCache: false, - onSearchStart: noop, - onSearchComplete: noop, - onSearchError: noop, - preserveInput: false, - containerClass: 'autocomplete-suggestions', - tabDisabled: false, - dataType: 'text', - currentRequest: null, - triggerSelectOnValidInput: true, - preventBadQueries: true, - lookupFilter: function (suggestion, originalQuery, queryLowerCase) { - return suggestion.value.toLowerCase().indexOf(queryLowerCase) !== -1; - }, - paramName: 'query', - transformResult: function (response) { - return typeof response === 'string' ? $.parseJSON(response) : response; - }, - showNoSuggestionNotice: false, - noSuggestionNotice: 'No results', - orientation: 'bottom', - forceFixPosition: false - }; - - // Shared variables: - that.element = el; - that.el = $(el); - that.suggestions = []; - that.badQueries = []; - that.selectedIndex = -1; - that.currentValue = that.element.value; - that.intervalId = 0; - that.cachedResponse = {}; - that.onChangeInterval = null; - that.onChange = null; - that.isLocal = false; - that.suggestionsContainer = null; - that.noSuggestionsContainer = null; - that.options = $.extend({}, defaults, options); - that.classes = { - selected: 'autocomplete-selected', - suggestion: 'autocomplete-suggestion' - }; - that.hint = null; - that.hintValue = ''; - that.selection = null; - - // Initialize and set options: - that.initialize(); - that.setOptions(options); - } - - Autocomplete.utils = utils; - - $.Autocomplete = Autocomplete; - - Autocomplete.formatResult = function (suggestion, currentValue) { - var pattern = '(' + utils.escapeRegExChars(currentValue) + ')'; - - return suggestion.value - .replace(new RegExp(pattern, 'gi'), '$1<\/strong>') - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/<(\/?strong)>/g, '<$1>'); + 'use strict'; + + let + utils = (function () { + return { + escapeRegExChars: function (value) { + return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); + }, + createNode: function (containerClass) { + let div = document.createElement('div'); + div.className = containerClass; + div.style.position = 'absolute'; + div.style.display = 'none'; + return div; + } + }; + }()), + + keys = { + ESC: 27, + TAB: 9, + RETURN: 13, + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40 }; - Autocomplete.prototype = { - - killerFn: null, - - initialize: function () { - var that = this, - suggestionSelector = '.' + that.classes.suggestion, - selected = that.classes.selected, - options = that.options, - container; - - // Remove autocomplete attribute to prevent native suggestions: - that.element.setAttribute('autocomplete', 'off'); - - that.killerFn = function (e) { - if ($(e.target).closest('.' + that.options.containerClass).length === 0) { - that.killSuggestions(); - that.disableKillerFn(); - } - }; - - // html() deals with many types: htmlString or Element or Array or jQuery - that.noSuggestionsContainer = $('
    ') - .html(this.options.noSuggestionNotice).get(0); - - that.suggestionsContainer = Autocomplete.utils.createNode(options.containerClass); - - container = $(that.suggestionsContainer); - - container.appendTo(options.appendTo); - - // Only set width if it was provided: - if (options.width !== 'auto') { - container.width(options.width); - } - - // Listen for mouse over event on suggestions list: - container.on('mouseover.autocomplete', suggestionSelector, function () { - that.activate($(this).data('index')); - }); - - // Deselect active element when mouse leaves suggestions container: - container.on('mouseout.autocomplete', function () { - that.selectedIndex = -1; - container.children('.' + selected).removeClass(selected); - }); - - // Listen for click event on suggestions list: - container.on('click.autocomplete', suggestionSelector, function () { - that.select($(this).data('index')); - }); - - that.fixPositionCapture = function () { - if (that.visible) { - that.fixPosition(); - } - }; - - $(window).on('resize.autocomplete', that.fixPositionCapture); - - that.el.on('keydown.autocomplete', function (e) { that.onKeyPress(e); }); - that.el.on('keyup.autocomplete', function (e) { that.onKeyUp(e); }); - that.el.on('blur.autocomplete', function () { that.onBlur(); }); - that.el.on('focus.autocomplete', function () { that.onFocus(); }); - that.el.on('change.autocomplete', function (e) { that.onKeyUp(e); }); - that.el.on('input.autocomplete', function (e) { that.onKeyUp(e); }); - }, - - onFocus: function () { - var that = this; - that.fixPosition(); - if (that.options.minChars === 0 && that.el.val().length === 0) { - that.onValueChange(); - } - }, - - onBlur: function () { - this.enableKillerFn(); - }, - - abortAjax: function () { - var that = this; - if (that.currentRequest) { - that.currentRequest.abort(); - that.currentRequest = null; - } - }, - - setOptions: function (suppliedOptions) { - var that = this, - options = that.options; - - $.extend(options, suppliedOptions); - - that.isLocal = $.isArray(options.lookup); - - if (that.isLocal) { - options.lookup = that.verifySuggestionsFormat(options.lookup); - } - - options.orientation = that.validateOrientation(options.orientation, 'bottom'); - - // Adjust height, width and z-index: - $(that.suggestionsContainer).css({ - 'max-height': options.maxHeight + 'px', - 'width': options.width + 'px', - 'z-index': options.zIndex - }); - }, - - - clearCache: function () { - this.cachedResponse = {}; - this.badQueries = []; - }, - - clear: function () { - this.clearCache(); - this.currentValue = ''; - this.suggestions = []; - }, - - disable: function () { - var that = this; - that.disabled = true; - clearInterval(that.onChangeInterval); - that.abortAjax(); - }, - - enable: function () { - this.disabled = false; - }, - - fixPosition: function () { - // Use only when container has already its content - - var that = this, - $container = $(that.suggestionsContainer), - containerParent = $container.parent().get(0); - // Fix position automatically when appended to body. - // In other cases force parameter must be given. - if (containerParent !== document.body && !that.options.forceFixPosition) { - return; - } - - // Choose orientation - var orientation = that.options.orientation, - containerHeight = $container.outerHeight(), - height = that.el.outerHeight(), - offset = that.el.offset(), - styles = { 'top': offset.top, 'left': offset.left }; - - if (orientation === 'auto') { - var viewPortHeight = $(window).height(), - scrollTop = $(window).scrollTop(), - topOverflow = -scrollTop + offset.top - containerHeight, - bottomOverflow = scrollTop + viewPortHeight - (offset.top + height + containerHeight); - - orientation = (Math.max(topOverflow, bottomOverflow) === topOverflow) ? 'top' : 'bottom'; - } - - if (orientation === 'top') { - styles.top += -containerHeight; - } else { - styles.top += height; - } - - // If container is not positioned to body, - // correct its position using offset parent offset - if(containerParent !== document.body) { - var opacity = $container.css('opacity'), - parentOffsetDiff; - - if (!that.visible){ - $container.css('opacity', 0).show(); - } - - parentOffsetDiff = $container.offsetParent().offset(); - styles.top -= parentOffsetDiff.top; - styles.left -= parentOffsetDiff.left; - - if (!that.visible){ - $container.css('opacity', opacity).hide(); - } - } - - // -2px to account for suggestions border. - if (that.options.width === 'auto') { - styles.width = (that.el.outerWidth() - 2) + 'px'; - } - - $container.css(styles); - }, - - enableKillerFn: function () { - var that = this; - $(document).on('click.autocomplete', that.killerFn); - }, - - disableKillerFn: function () { - var that = this; - $(document).off('click.autocomplete', that.killerFn); - }, - - killSuggestions: function () { - var that = this; - that.stopKillSuggestions(); - that.intervalId = window.setInterval(function () { - if (that.visible) { - that.el.val(that.currentValue); - that.hide(); - } - - that.stopKillSuggestions(); - }, 50); - }, - - stopKillSuggestions: function () { - window.clearInterval(this.intervalId); - }, - - isCursorAtEnd: function () { - var that = this, - valLength = that.el.val().length, - selectionStart = that.element.selectionStart, - range; - - if (typeof selectionStart === 'number') { - return selectionStart === valLength; - } - if (document.selection) { - range = document.selection.createRange(); - range.moveStart('character', -valLength); - return valLength === range.text.length; - } - return true; - }, - - onKeyPress: function (e) { - var that = this; - - // If suggestions are hidden and user presses arrow down, display suggestions: - if (!that.disabled && !that.visible && e.which === keys.DOWN && that.currentValue) { - that.suggest(); - return; - } - - if (that.disabled || !that.visible) { - return; - } - - switch (e.which) { - case keys.ESC: - that.el.val(that.currentValue); - that.hide(); - break; - case keys.RIGHT: - if (that.hint && that.options.onHint && that.isCursorAtEnd()) { - that.selectHint(); - break; - } - return; - case keys.TAB: - if (that.hint && that.options.onHint) { - that.selectHint(); - return; - } - if (that.selectedIndex === -1) { - that.hide(); - return; - } - that.select(that.selectedIndex); - if (that.options.tabDisabled === false) { - return; - } - break; - case keys.RETURN: - if (that.selectedIndex === -1) { - that.hide(); - return; - } - that.select(that.selectedIndex); - break; - case keys.UP: - that.moveUp(); - break; - case keys.DOWN: - that.moveDown(); - break; - default: - return; - } - - // Cancel event if function did not return: - e.stopImmediatePropagation(); - e.preventDefault(); - }, - - onKeyUp: function (e) { - var that = this; - - if (that.disabled) { - return; - } - - switch (e.which) { - case keys.UP: - case keys.DOWN: - return; - } - - clearInterval(that.onChangeInterval); - - if (that.currentValue !== that.el.val()) { - that.findBestHint(); - if (that.options.deferRequestBy > 0) { - // Defer lookup in case when value changes very quickly: - that.onChangeInterval = setInterval(function () { - that.onValueChange(); - }, that.options.deferRequestBy); - } else { - that.onValueChange(); - } - } - }, - - onValueChange: function () { - var that = this, - options = that.options, - value = that.el.val(), - query = that.getQuery(value); - - if (that.selection && that.currentValue !== query) { - that.selection = null; - (options.onInvalidateSelection || $.noop).call(that.element); - } - - clearInterval(that.onChangeInterval); - that.currentValue = value; - that.selectedIndex = -1; - - // Check existing suggestion for the match before proceeding: - if (options.triggerSelectOnValidInput && that.isExactMatch(query)) { - that.select(0); - return; - } - - if (query.length < options.minChars) { - that.hide(); - } else { - that.getSuggestions(query); - } - }, - - isExactMatch: function (query) { - var suggestions = this.suggestions; - - return (suggestions.length === 1 && suggestions[0].value.toLowerCase() === query.toLowerCase()); - }, - - getQuery: function (value) { - var delimiter = this.options.delimiter, - parts; - - if (!delimiter) { - return value; - } - parts = value.split(delimiter); - return $.trim(parts[parts.length - 1]); - }, - - getSuggestionsLocal: function (query) { - var that = this, - options = that.options, - queryLowerCase = query.toLowerCase(), - filter = options.lookupFilter, - limit = parseInt(options.lookupLimit, 10), - data; - - data = { - suggestions: $.grep(options.lookup, function (suggestion) { - return filter(suggestion, query, queryLowerCase); - }) - }; - - if (limit && data.suggestions.length > limit) { - data.suggestions = data.suggestions.slice(0, limit); - } - - return data; - }, - - getSuggestions: function (q) { - var response, - that = this, - options = that.options, - serviceUrl = options.serviceUrl, - params, - cacheKey, - ajaxSettings; - - options.params[options.paramName] = q; - params = options.ignoreParams ? null : options.params; - - if (options.onSearchStart.call(that.element, options.params) === false) { - return; - } - - if ($.isFunction(options.lookup)){ - options.lookup(q, function (data) { - that.suggestions = data.suggestions; - that.suggest(); - options.onSearchComplete.call(that.element, q, data.suggestions); - }); - return; - } - - if (that.isLocal) { - response = that.getSuggestionsLocal(q); - } else { - if ($.isFunction(serviceUrl)) { - serviceUrl = serviceUrl.call(that.element, q); - } - cacheKey = serviceUrl + '?' + $.param(params || {}); - response = that.cachedResponse[cacheKey]; - } - - if (response && $.isArray(response.suggestions)) { - that.suggestions = response.suggestions; - that.suggest(); - options.onSearchComplete.call(that.element, q, response.suggestions); - } else if (!that.isBadQuery(q)) { - that.abortAjax(); - - ajaxSettings = { - url: serviceUrl, - data: params, - type: options.type, - dataType: options.dataType - }; - - $.extend(ajaxSettings, options.ajaxSettings); - - that.currentRequest = $.ajax(ajaxSettings).done(function (data) { - var result; - that.currentRequest = null; - result = options.transformResult(data, q); - that.processResponse(result, q, cacheKey); - options.onSearchComplete.call(that.element, q, result.suggestions); - }).fail(function (jqXHR, textStatus, errorThrown) { - options.onSearchError.call(that.element, q, jqXHR, textStatus, errorThrown); - }); - } else { - options.onSearchComplete.call(that.element, q, []); - } - }, - - isBadQuery: function (q) { - if (!this.options.preventBadQueries){ - return false; - } - - var badQueries = this.badQueries, - i = badQueries.length; - - while (i--) { - if (q.indexOf(badQueries[i]) === 0) { - return true; - } - } - - return false; - }, + function Autocomplete (el, options) { + let noop = function () { }, + that = this, + defaults = { + ajaxSettings: {}, + autoSelectFirst: false, + appendTo: document.body, + serviceUrl: null, + lookup: null, + onSelect: null, + width: 'auto', + minChars: 1, + maxHeight: 300, + deferRequestBy: 0, + params: {}, + formatResult: Autocomplete.formatResult, + delimiter: null, + zIndex: 9999, + type: 'GET', + noCache: false, + onSearchStart: noop, + onSearchComplete: noop, + onSearchError: noop, + preserveInput: false, + containerClass: 'autocomplete-suggestions', + tabDisabled: false, + dataType: 'text', + currentRequest: null, + triggerSelectOnValidInput: true, + preventBadQueries: true, + lookupFilter: function (suggestion, originalQuery, queryLowerCase) { + return suggestion.value.toLowerCase().indexOf(queryLowerCase) !== -1; + }, + paramName: 'query', + transformResult: function (response) { + return typeof response === 'string' ? $.parseJSON(response) : response; + }, + showNoSuggestionNotice: false, + noSuggestionNotice: 'No results', + orientation: 'bottom', + forceFixPosition: false + }; + + // Shared variables: + that.element = el; + that.el = $(el); + that.suggestions = []; + that.badQueries = []; + that.selectedIndex = -1; + that.currentValue = that.element.value; + that.intervalId = 0; + that.cachedResponse = {}; + that.onChangeInterval = null; + that.onChange = null; + that.isLocal = false; + that.suggestionsContainer = null; + that.noSuggestionsContainer = null; + that.options = $.extend({}, defaults, options); + that.classes = { + selected: 'autocomplete-selected', + suggestion: 'autocomplete-suggestion' + }; + that.hint = null; + that.hintValue = ''; + that.selection = null; - hide: function () { - var that = this, - container = $(that.suggestionsContainer); + // Initialize and set options: + that.initialize(); + that.setOptions(options); + } - if ($.isFunction(that.options.onHide) && that.visible) { - that.options.onHide.call(that.element, container); - } + Autocomplete.utils = utils; - that.visible = false; - that.selectedIndex = -1; - clearInterval(that.onChangeInterval); - $(that.suggestionsContainer).hide(); - that.signalHint(null); - }, + $.Autocomplete = Autocomplete; - suggest: function () { - if (this.suggestions.length === 0) { - if (this.options.showNoSuggestionNotice) { - this.noSuggestions(); - } else { - this.hide(); - } - return; - } - - var that = this, - options = that.options, - groupBy = options.groupBy, - formatResult = options.formatResult, - value = that.getQuery(that.currentValue), - className = that.classes.suggestion, - classSelected = that.classes.selected, - container = $(that.suggestionsContainer), - noSuggestionsContainer = $(that.noSuggestionsContainer), - beforeRender = options.beforeRender, - html = '', - category, - formatGroup = typeof options.formatGroup == 'function' ? options.formatGroup : function (suggestion, index) { - var currentCategory = suggestion.data[groupBy]; - - if (category === currentCategory){ - return ''; - } - - category = currentCategory; - - return '
    ' + category + '
    '; - }; - - if (options.triggerSelectOnValidInput && that.isExactMatch(value)) { - that.select(0); - return; - } - - // Build suggestions inner HTML: - $.each(that.suggestions, function (i, suggestion) { - if (groupBy){ - html += formatGroup(suggestion, value, i); - } - - html += '
    ' + formatResult(suggestion, value) + '
    '; - }); - - this.adjustContainerWidth(); - - noSuggestionsContainer.detach(); - container.html(html); - - if ($.isFunction(beforeRender)) { - beforeRender.call(that.element, container); - } - - that.fixPosition(); - container.show(); - - // Select first value by default: - if (options.autoSelectFirst) { - that.selectedIndex = 0; - container.scrollTop(0); - container.children('.' + className).first().addClass(classSelected); - } - - that.visible = true; - that.findBestHint(); - }, + Autocomplete.formatResult = function (suggestion, currentValue) { + let pattern = '(' + utils.escapeRegExChars(currentValue) + ')'; - noSuggestions: function() { - var that = this, - container = $(that.suggestionsContainer), - noSuggestionsContainer = $(that.noSuggestionsContainer); + return suggestion.value + .replace(new RegExp(pattern, 'gi'), '$1<\/strong>') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/<(\/?strong)>/g, '<$1>'); + }; - this.adjustContainerWidth(); + Autocomplete.prototype = { - // Some explicit steps. Be careful here as it easy to get - // noSuggestionsContainer removed from DOM if not detached properly. - noSuggestionsContainer.detach(); - container.empty(); // clean suggestions if any - container.append(noSuggestionsContainer); + killerFn: null, - that.fixPosition(); + initialize: function () { + let that = this, + suggestionSelector = '.' + that.classes.suggestion, + selected = that.classes.selected, + options = that.options, + container; - container.show(); - that.visible = true; - }, + // Remove autocomplete attribute to prevent native suggestions: + that.element.setAttribute('autocomplete', 'off'); - adjustContainerWidth: function() { - var that = this, - options = that.options, - width, - container = $(that.suggestionsContainer); - - // If width is auto, adjust width before displaying suggestions, - // because if instance was created before input had width, it will be zero. - // Also it adjusts if input width has changed. - // -2px to account for suggestions border. - if (options.width === 'auto') { - width = that.el.outerWidth() - 2; - container.width(width > 0 ? width : 300); - } - }, + that.killerFn = function (e) { + if ($(e.target).closest('.' + that.options.containerClass).length === 0) { + that.killSuggestions(); + that.disableKillerFn(); + } + }; - findBestHint: function () { - var that = this, - value = that.el.val().toLowerCase(), - bestMatch = null; + // html() deals with many types: htmlString or Element or Array or jQuery + that.noSuggestionsContainer = $('
    ') + .html(this.options.noSuggestionNotice).get(0); - if (!value) { - return; - } + that.suggestionsContainer = Autocomplete.utils.createNode(options.containerClass); - $.each(that.suggestions, function (i, suggestion) { - var foundMatch = suggestion.value.toLowerCase().indexOf(value) === 0; - if (foundMatch) { - bestMatch = suggestion; - } - return !foundMatch; - }); + container = $(that.suggestionsContainer); - that.signalHint(bestMatch); - }, + container.appendTo(options.appendTo); - signalHint: function (suggestion) { - var hintValue = '', - that = this; - if (suggestion) { - hintValue = that.currentValue + suggestion.value.substr(that.currentValue.length); - } - if (that.hintValue !== hintValue) { - that.hintValue = hintValue; - that.hint = suggestion; - (this.options.onHint || $.noop)(hintValue); - } - }, + // Only set width if it was provided: + if (options.width !== 'auto') { + container.width(options.width); + } - verifySuggestionsFormat: function (suggestions) { - // If suggestions is string array, convert them to supported format: - if (suggestions.length && typeof suggestions[0] === 'string') { - return $.map(suggestions, function (value) { - return { value: value, data: null }; - }); - } + // Listen for mouse over event on suggestions list: + container.on('mouseover.autocomplete', suggestionSelector, function () { + that.activate($(this).data('index')); + }); - return suggestions; - }, - - validateOrientation: function(orientation, fallback) { - orientation = $.trim(orientation || '').toLowerCase(); + // Deselect active element when mouse leaves suggestions container: + container.on('mouseout.autocomplete', function () { + that.selectedIndex = -1; + container.children('.' + selected).removeClass(selected); + }); - if($.inArray(orientation, ['auto', 'bottom', 'top']) === -1){ - orientation = fallback; - } + // Listen for click event on suggestions list: + container.on('click.autocomplete', suggestionSelector, function () { + that.select($(this).data('index')); + }); - return orientation; - }, + that.fixPositionCapture = function () { + if (that.visible) { + that.fixPosition(); + } + }; + + $(window).on('resize.autocomplete', that.fixPositionCapture); + + that.el.on('keydown.autocomplete', function (e) { that.onKeyPress(e); }); + that.el.on('keyup.autocomplete', function (e) { that.onKeyUp(e); }); + that.el.on('blur.autocomplete', function () { that.onBlur(); }); + that.el.on('focus.autocomplete', function () { that.onFocus(); }); + that.el.on('change.autocomplete', function (e) { that.onKeyUp(e); }); + that.el.on('input.autocomplete', function (e) { that.onKeyUp(e); }); + }, + + onFocus: function () { + let that = this; + that.fixPosition(); + if (that.options.minChars === 0 && that.el.val().length === 0) { + that.onValueChange(); + } + }, + + onBlur: function () { + this.enableKillerFn(); + }, + + abortAjax: function () { + let that = this; + if (that.currentRequest) { + that.currentRequest.abort(); + that.currentRequest = null; + } + }, + + setOptions: function (suppliedOptions) { + let that = this, + options = that.options; + + $.extend(options, suppliedOptions); + + that.isLocal = $.isArray(options.lookup); + + if (that.isLocal) { + options.lookup = that.verifySuggestionsFormat(options.lookup); + } + + options.orientation = that.validateOrientation(options.orientation, 'bottom'); + + // Adjust height, width and z-index: + $(that.suggestionsContainer).css({ + 'max-height': options.maxHeight + 'px', + 'width': options.width + 'px', + 'z-index': options.zIndex + }); + }, + + clearCache: function () { + this.cachedResponse = {}; + this.badQueries = []; + }, + + clear: function () { + this.clearCache(); + this.currentValue = ''; + this.suggestions = []; + }, + + disable: function () { + let that = this; + that.disabled = true; + clearInterval(that.onChangeInterval); + that.abortAjax(); + }, + + enable: function () { + this.disabled = false; + }, + + fixPosition: function () { + // Use only when container has already its content + + let that = this, + $container = $(that.suggestionsContainer), + containerParent = $container.parent().get(0); + // Fix position automatically when appended to body. + // In other cases force parameter must be given. + if (containerParent !== document.body && !that.options.forceFixPosition) { + return; + } + + // Choose orientation + let orientation = that.options.orientation, + containerHeight = $container.outerHeight(), + height = that.el.outerHeight(), + offset = that.el.offset(), + styles = { 'top': offset.top, 'left': offset.left }; + + if (orientation === 'auto') { + let viewPortHeight = $(window).height(), + scrollTop = $(window).scrollTop(), + topOverflow = -scrollTop + offset.top - containerHeight, + bottomOverflow = scrollTop + viewPortHeight - (offset.top + height + containerHeight); + + orientation = (Math.max(topOverflow, bottomOverflow) === topOverflow) ? 'top' : 'bottom'; + } + + if (orientation === 'top') { + styles.top += -containerHeight; + } else { + styles.top += height; + } + + // If container is not positioned to body, + // correct its position using offset parent offset + if (containerParent !== document.body) { + let opacity = $container.css('opacity'), + parentOffsetDiff; + + if (!that.visible) { + $container.css('opacity', 0).show(); + } - processResponse: function (result, originalQuery, cacheKey) { - var that = this, - options = that.options; + parentOffsetDiff = $container.offsetParent().offset(); + styles.top -= parentOffsetDiff.top; + styles.left -= parentOffsetDiff.left; - result.suggestions = that.verifySuggestionsFormat(result.suggestions); + if (!that.visible) { + $container.css('opacity', opacity).hide(); + } + } + + // -2px to account for suggestions border. + if (that.options.width === 'auto') { + styles.width = (that.el.outerWidth() - 2) + 'px'; + } + + $container.css(styles); + }, + + enableKillerFn: function () { + let that = this; + $(document).on('click.autocomplete', that.killerFn); + }, + + disableKillerFn: function () { + let that = this; + $(document).off('click.autocomplete', that.killerFn); + }, + + killSuggestions: function () { + let that = this; + that.stopKillSuggestions(); + that.intervalId = window.setInterval(function () { + if (that.visible) { + that.el.val(that.currentValue); + that.hide(); + } - // Cache results if cache is not disabled: - if (!options.noCache) { - that.cachedResponse[cacheKey] = result; - if (options.preventBadQueries && result.suggestions.length === 0) { - that.badQueries.push(originalQuery); - } - } + that.stopKillSuggestions(); + }, 50); + }, + + stopKillSuggestions: function () { + window.clearInterval(this.intervalId); + }, + + isCursorAtEnd: function () { + let that = this, + valLength = that.el.val().length, + selectionStart = that.element.selectionStart, + range; + + if (typeof selectionStart === 'number') { + return selectionStart === valLength; + } + if (document.selection) { + range = document.selection.createRange(); + range.moveStart('character', -valLength); + return valLength === range.text.length; + } + return true; + }, + + onKeyPress: function (e) { + let that = this; + + // If suggestions are hidden and user presses arrow down, display suggestions: + if (!that.disabled && !that.visible && e.which === keys.DOWN && that.currentValue) { + that.suggest(); + return; + } + + if (that.disabled || !that.visible) { + return; + } + + switch (e.which) { + case keys.ESC: + that.el.val(that.currentValue); + that.hide(); + break; + case keys.RIGHT: + if (that.hint && that.options.onHint && that.isCursorAtEnd()) { + that.selectHint(); + break; + } + return; + case keys.TAB: + if (that.hint && that.options.onHint) { + that.selectHint(); + return; + } + if (that.selectedIndex === -1) { + that.hide(); + return; + } + that.select(that.selectedIndex); + if (that.options.tabDisabled === false) { + return; + } + break; + case keys.RETURN: + if (that.selectedIndex === -1) { + that.hide(); + return; + } + that.select(that.selectedIndex); + break; + case keys.UP: + that.moveUp(); + break; + case keys.DOWN: + that.moveDown(); + break; + default: + return; + } + + // Cancel event if function did not return: + e.stopImmediatePropagation(); + e.preventDefault(); + }, + + onKeyUp: function (e) { + let that = this; + + if (that.disabled) { + return; + } + + switch (e.which) { + case keys.UP: + case keys.DOWN: + return; + } + + clearInterval(that.onChangeInterval); + + if (that.currentValue !== that.el.val()) { + that.findBestHint(); + if (that.options.deferRequestBy > 0) { + // Defer lookup in case when value changes very quickly: + that.onChangeInterval = setInterval(function () { + that.onValueChange(); + }, that.options.deferRequestBy); + } else { + that.onValueChange(); + } + } + }, - // Return if originalQuery is not matching current query: - if (originalQuery !== that.getQuery(that.currentValue)) { - return; - } + onValueChange: function () { + let that = this, + options = that.options, + value = that.el.val(), + query = that.getQuery(value); - that.suggestions = result.suggestions; - that.suggest(); - }, + if (that.selection && that.currentValue !== query) { + that.selection = null; + (options.onInvalidateSelection || $.noop).call(that.element); + } + + clearInterval(that.onChangeInterval); + that.currentValue = value; + that.selectedIndex = -1; + + // Check existing suggestion for the match before proceeding: + if (options.triggerSelectOnValidInput && that.isExactMatch(query)) { + that.select(0); + return; + } + + if (query.length < options.minChars) { + that.hide(); + } else { + that.getSuggestions(query); + } + }, + + isExactMatch: function (query) { + let suggestions = this.suggestions; + + return (suggestions.length === 1 && suggestions[0].value.toLowerCase() === query.toLowerCase()); + }, + + getQuery: function (value) { + let delimiter = this.options.delimiter, + parts; + + if (!delimiter) { + return value; + } + parts = value.split(delimiter); + return $.trim(parts[parts.length - 1]); + }, + + getSuggestionsLocal: function (query) { + let that = this, + options = that.options, + queryLowerCase = query.toLowerCase(), + filter = options.lookupFilter, + limit = parseInt(options.lookupLimit, 10), + data; + + data = { + suggestions: $.grep(options.lookup, function (suggestion) { + return filter(suggestion, query, queryLowerCase); + }) + }; + + if (limit && data.suggestions.length > limit) { + data.suggestions = data.suggestions.slice(0, limit); + } + + return data; + }, + + getSuggestions: function (q) { + let response, + that = this, + options = that.options, + serviceUrl = options.serviceUrl, + params, + cacheKey, + ajaxSettings; + + options.params[options.paramName] = q; + params = options.ignoreParams ? null : options.params; + + if (options.onSearchStart.call(that.element, options.params) === false) { + return; + } + + if ($.isFunction(options.lookup)) { + options.lookup(q, function (data) { + that.suggestions = data.suggestions; + that.suggest(); + options.onSearchComplete.call(that.element, q, data.suggestions); + }); + return; + } + + if (that.isLocal) { + response = that.getSuggestionsLocal(q); + } else { + if ($.isFunction(serviceUrl)) { + serviceUrl = serviceUrl.call(that.element, q); + } + cacheKey = serviceUrl + '?' + $.param(params || {}); + response = that.cachedResponse[cacheKey]; + } + + if (response && $.isArray(response.suggestions)) { + that.suggestions = response.suggestions; + that.suggest(); + options.onSearchComplete.call(that.element, q, response.suggestions); + } else if (!that.isBadQuery(q)) { + that.abortAjax(); + + ajaxSettings = { + url: serviceUrl, + data: params, + type: options.type, + dataType: options.dataType + }; - activate: function (index) { - var that = this, - activeItem, - selected = that.classes.selected, - container = $(that.suggestionsContainer), - children = container.find('.' + that.classes.suggestion); + $.extend(ajaxSettings, options.ajaxSettings); - container.find('.' + selected).removeClass(selected); + that.currentRequest = $.ajax(ajaxSettings).done(function (data) { + let result; + that.currentRequest = null; + result = options.transformResult(data, q); + that.processResponse(result, q, cacheKey); + options.onSearchComplete.call(that.element, q, result.suggestions); + }).fail(function (jqXHR, textStatus, errorThrown) { + options.onSearchError.call(that.element, q, jqXHR, textStatus, errorThrown); + }); + } else { + options.onSearchComplete.call(that.element, q, []); + } + }, + + isBadQuery: function (q) { + if (!this.options.preventBadQueries) { + return false; + } + + let badQueries = this.badQueries, + i = badQueries.length; + + while (i--) { + if (q.indexOf(badQueries[i]) === 0) { + return true; + } + } + + return false; + }, + + hide: function () { + let that = this, + container = $(that.suggestionsContainer); + + if ($.isFunction(that.options.onHide) && that.visible) { + that.options.onHide.call(that.element, container); + } + + that.visible = false; + that.selectedIndex = -1; + clearInterval(that.onChangeInterval); + $(that.suggestionsContainer).hide(); + that.signalHint(null); + }, + + suggest: function () { + if (this.suggestions.length === 0) { + if (this.options.showNoSuggestionNotice) { + this.noSuggestions(); + } else { + this.hide(); + } + return; + } + + let that = this, + options = that.options, + groupBy = options.groupBy, + formatResult = options.formatResult, + value = that.getQuery(that.currentValue), + className = that.classes.suggestion, + classSelected = that.classes.selected, + container = $(that.suggestionsContainer), + noSuggestionsContainer = $(that.noSuggestionsContainer), + beforeRender = options.beforeRender, + html = '', + category, + formatGroup = typeof options.formatGroup == 'function' ? options.formatGroup : function (suggestion, index) { + let currentCategory = suggestion.data[groupBy]; + + if (category === currentCategory) { + return ''; + } + + category = currentCategory; + + return '
    ' + category + '
    '; + }; - that.selectedIndex = index; + if (options.triggerSelectOnValidInput && that.isExactMatch(value)) { + that.select(0); + return; + } - if (that.selectedIndex !== -1 && children.length > that.selectedIndex) { - activeItem = children.get(that.selectedIndex); - $(activeItem).addClass(selected); - return activeItem; - } + // Build suggestions inner HTML: + $.each(that.suggestions, function (i, suggestion) { + if (groupBy) { + html += formatGroup(suggestion, value, i); + } - return null; - }, + html += '
    ' + formatResult(suggestion, value) + '
    '; + }); + + this.adjustContainerWidth(); + + noSuggestionsContainer.detach(); + container.html(html); + + if ($.isFunction(beforeRender)) { + beforeRender.call(that.element, container); + } + + that.fixPosition(); + container.show(); + + // Select first value by default: + if (options.autoSelectFirst) { + that.selectedIndex = 0; + container.scrollTop(0); + container.children('.' + className).first().addClass(classSelected); + } + + that.visible = true; + that.findBestHint(); + }, + + noSuggestions: function () { + let that = this, + container = $(that.suggestionsContainer), + noSuggestionsContainer = $(that.noSuggestionsContainer); + + this.adjustContainerWidth(); + + // Some explicit steps. Be careful here as it easy to get + // noSuggestionsContainer removed from DOM if not detached properly. + noSuggestionsContainer.detach(); + container.empty(); // clean suggestions if any + container.append(noSuggestionsContainer); + + that.fixPosition(); + + container.show(); + that.visible = true; + }, + + adjustContainerWidth: function () { + let that = this, + options = that.options, + width, + container = $(that.suggestionsContainer); + + // If width is auto, adjust width before displaying suggestions, + // because if instance was created before input had width, it will be zero. + // Also it adjusts if input width has changed. + // -2px to account for suggestions border. + if (options.width === 'auto') { + width = that.el.outerWidth() - 2; + container.width(width > 0 ? width : 300); + } + }, + + findBestHint: function () { + let that = this, + value = that.el.val().toLowerCase(), + bestMatch = null; + + if (!value) { + return; + } + + $.each(that.suggestions, function (i, suggestion) { + let foundMatch = suggestion.value.toLowerCase().indexOf(value) === 0; + if (foundMatch) { + bestMatch = suggestion; + } + return !foundMatch; + }); + + that.signalHint(bestMatch); + }, + + signalHint: function (suggestion) { + let hintValue = '', + that = this; + if (suggestion) { + hintValue = that.currentValue + suggestion.value.substr(that.currentValue.length); + } + if (that.hintValue !== hintValue) { + that.hintValue = hintValue; + that.hint = suggestion; + (this.options.onHint || $.noop)(hintValue); + } + }, + + verifySuggestionsFormat: function (suggestions) { + // If suggestions is string array, convert them to supported format: + if (suggestions.length && typeof suggestions[0] === 'string') { + return $.map(suggestions, function (value) { + return { value: value, data: null }; + }); + } - selectHint: function () { - var that = this, - i = $.inArray(that.hint, that.suggestions); + return suggestions; + }, - that.select(i); - }, + validateOrientation: function (orientation, fallback) { + orientation = $.trim(orientation || '').toLowerCase(); - select: function (i) { - var that = this; - that.hide(); - that.onSelect(i); - }, + if ($.inArray(orientation, ['auto', 'bottom', 'top']) === -1) { + orientation = fallback; + } - moveUp: function () { - var that = this; + return orientation; + }, - if (that.selectedIndex === -1) { - return; - } + processResponse: function (result, originalQuery, cacheKey) { + let that = this, + options = that.options; - if (that.selectedIndex === 0) { - $(that.suggestionsContainer).children().first().removeClass(that.classes.selected); - that.selectedIndex = -1; - that.el.val(that.currentValue); - that.findBestHint(); - return; - } + result.suggestions = that.verifySuggestionsFormat(result.suggestions); - that.adjustScroll(that.selectedIndex - 1); - }, + // Cache results if cache is not disabled: + if (!options.noCache) { + that.cachedResponse[cacheKey] = result; + if (options.preventBadQueries && result.suggestions.length === 0) { + that.badQueries.push(originalQuery); + } + } - moveDown: function () { - var that = this; + // Return if originalQuery is not matching current query: + if (originalQuery !== that.getQuery(that.currentValue)) { + return; + } - if (that.selectedIndex === (that.suggestions.length - 1)) { - return; - } + that.suggestions = result.suggestions; + that.suggest(); + }, - that.adjustScroll(that.selectedIndex + 1); - }, + activate: function (index) { + let that = this, + activeItem, + selected = that.classes.selected, + container = $(that.suggestionsContainer), + children = container.find('.' + that.classes.suggestion); - adjustScroll: function (index) { - var that = this, - activeItem = that.activate(index); - - if (!activeItem) { - return; - } - - var offsetTop, - upperBound, - lowerBound, - heightDelta = $(activeItem).outerHeight(); - - offsetTop = activeItem.offsetTop; - upperBound = $(that.suggestionsContainer).scrollTop(); - lowerBound = upperBound + that.options.maxHeight - heightDelta; - - if (offsetTop < upperBound) { - $(that.suggestionsContainer).scrollTop(offsetTop); - } else if (offsetTop > lowerBound) { - $(that.suggestionsContainer).scrollTop(offsetTop - that.options.maxHeight + heightDelta); - } - - if (!that.options.preserveInput) { - that.el.val(that.getValue(that.suggestions[index].value)); - } - that.signalHint(null); - }, + container.find('.' + selected).removeClass(selected); - onSelect: function (index) { - var that = this, - onSelectCallback = that.options.onSelect, - suggestion = that.suggestions[index]; + that.selectedIndex = index; - that.currentValue = that.getValue(suggestion.value); + if (that.selectedIndex !== -1 && children.length > that.selectedIndex) { + activeItem = children.get(that.selectedIndex); + $(activeItem).addClass(selected); + return activeItem; + } - if (that.currentValue !== that.el.val() && !that.options.preserveInput) { - that.el.val(that.currentValue); - } + return null; + }, - that.signalHint(null); - that.suggestions = []; - that.selection = suggestion; + selectHint: function () { + let that = this, + i = $.inArray(that.hint, that.suggestions); - if ($.isFunction(onSelectCallback)) { - onSelectCallback.call(that.element, suggestion); - } - }, + that.select(i); + }, - getValue: function (value) { - var that = this, - delimiter = that.options.delimiter, - currentValue, - parts; + select: function (i) { + let that = this; + that.hide(); + that.onSelect(i); + }, - if (!delimiter) { - return value; - } + moveUp: function () { + let that = this; - currentValue = that.currentValue; - parts = currentValue.split(delimiter); + if (that.selectedIndex === -1) { + return; + } - if (parts.length === 1) { - return value; - } + if (that.selectedIndex === 0) { + $(that.suggestionsContainer).children().first().removeClass(that.classes.selected); + that.selectedIndex = -1; + that.el.val(that.currentValue); + that.findBestHint(); + return; + } + + that.adjustScroll(that.selectedIndex - 1); + }, + + moveDown: function () { + let that = this; + + if (that.selectedIndex === (that.suggestions.length - 1)) { + return; + } + + that.adjustScroll(that.selectedIndex + 1); + }, + + adjustScroll: function (index) { + let that = this, + activeItem = that.activate(index); + + if (!activeItem) { + return; + } + + let offsetTop, + upperBound, + lowerBound, + heightDelta = $(activeItem).outerHeight(); + + offsetTop = activeItem.offsetTop; + upperBound = $(that.suggestionsContainer).scrollTop(); + lowerBound = upperBound + that.options.maxHeight - heightDelta; + + if (offsetTop < upperBound) { + $(that.suggestionsContainer).scrollTop(offsetTop); + } else if (offsetTop > lowerBound) { + $(that.suggestionsContainer).scrollTop(offsetTop - that.options.maxHeight + heightDelta); + } + + if (!that.options.preserveInput) { + that.el.val(that.getValue(that.suggestions[index].value)); + } + that.signalHint(null); + }, + + onSelect: function (index) { + let that = this, + onSelectCallback = that.options.onSelect, + suggestion = that.suggestions[index]; + + that.currentValue = that.getValue(suggestion.value); + + if (that.currentValue !== that.el.val() && !that.options.preserveInput) { + that.el.val(that.currentValue); + } + + that.signalHint(null); + that.suggestions = []; + that.selection = suggestion; + + if ($.isFunction(onSelectCallback)) { + onSelectCallback.call(that.element, suggestion); + } + }, + + getValue: function (value) { + let that = this, + delimiter = that.options.delimiter, + currentValue, + parts; + + if (!delimiter) { + return value; + } + + currentValue = that.currentValue; + parts = currentValue.split(delimiter); + + if (parts.length === 1) { + return value; + } + + return currentValue.substr(0, currentValue.length - parts[parts.length - 1].length) + value; + }, + + dispose: function () { + let that = this; + that.el.off('.autocomplete').removeData('autocomplete'); + that.disableKillerFn(); + $(window).off('resize.autocomplete', that.fixPositionCapture); + $(that.suggestionsContainer).remove(); + } + }; + + // Create chainable jQuery plugin: + $.fn.autocomplete = $.fn.devbridgeAutocomplete = function (options, args) { + let dataKey = 'autocomplete'; + // If function invoked without argument return + // instance of the first matched element: + if (arguments.length === 0) { + return this.first().data(dataKey); + } - return currentValue.substr(0, currentValue.length - parts[parts.length - 1].length) + value; - }, + return this.each(function () { + let inputElement = $(this), + instance = inputElement.data(dataKey); - dispose: function () { - var that = this; - that.el.off('.autocomplete').removeData('autocomplete'); - that.disableKillerFn(); - $(window).off('resize.autocomplete', that.fixPositionCapture); - $(that.suggestionsContainer).remove(); + if (typeof options === 'string') { + if (instance && typeof instance[options] === 'function') { + instance[options](args); } - }; - - // Create chainable jQuery plugin: - $.fn.autocomplete = $.fn.devbridgeAutocomplete = function (options, args) { - var dataKey = 'autocomplete'; - // If function invoked without argument return - // instance of the first matched element: - if (arguments.length === 0) { - return this.first().data(dataKey); + } else { + // If instance already exists, destroy it: + if (instance && instance.dispose) { + instance.dispose(); } - - return this.each(function () { - var inputElement = $(this), - instance = inputElement.data(dataKey); - - if (typeof options === 'string') { - if (instance && typeof instance[options] === 'function') { - instance[options](args); - } - } else { - // If instance already exists, destroy it: - if (instance && instance.dispose) { - instance.dispose(); - } - instance = new Autocomplete(this, options); - inputElement.data(dataKey, instance); - } - }); - }; + instance = new Autocomplete(this, options); + inputElement.data(dataKey, instance); + } + }); + }; })); From 8d1cb17df56cc7b89b04eff3dd0bd8bbfe64b399 Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Mon, 20 Feb 2023 08:41:59 +0100 Subject: [PATCH 05/11] Update links in README.md --- README.md | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index cd012aa..e40f8a0 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,30 @@ -# TYPO3 Weather Extension # -Display weather data and weather alerts using various Weather APIs. Default APIs: OpenWeatherMap and Deutscher Wetterdienst +# TYPO3 Weather Extension (weather2) -Read the Extension Manual: https://docs.typo3.org/typo3cms/extensions/weather2/ +Display weather data and weather alerts using various Weather APIs. +Default APIs: OpenWeatherMap and Deutscher Wetterdienst -weather2 on TYPO3 TER: https://typo3.org/extensions/repository/view/weather2 +Read the Extension Manual: https://docs.typo3.org/p/jweiland/weather2/main/en-us/Index.html -#### Requirements #### -- TYPO3 CMS 9.5.17 - 10.4.* -- TYPO3 Extension static_info_tables (>=6.4.0) +weather2 on TYPO3 TER: https://extensions.typo3.org/extension/weather2 -### Installation ### +## Installation -#### Installation using composer #### -Use `composer require jweiland/weather2` in your Composer based TYPO3 installation root. +### Installation using composer + +Use `composer require jweiland/weather2` in your Composer based TYPO3 +installation root. + +### Installation using Extension Manager -#### Installation using Extension Manager #### Just search for `weather2` and click on install. -### Who do I talk to? ### +## Who do I talk to? + +Please use the Issue Tracker to submit bugs/features/etc: + +https://github.com/jweiland-net/weather2 -Please use the Issue Tracker to submit bugs/features/etc +## Contact -### Contact ### -Feel free to open an issue ticket or contact us per mail `projects@jweiland.net`. +Feel free to open an issue ticket or contact us +per mail `projects@jweiland.net`. From dac7e0bfa0140873e78720ce5cbd994251e89a35 Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Mon, 20 Feb 2023 09:25:00 +0100 Subject: [PATCH 06/11] Update lines mentioned by php-cs-fixer --- Classes/Domain/Model/CurrentWeather.php | 5 -- .../Repository/DwdWarnCellRepository.php | 5 ++ Classes/Task/DeutscherWetterdienstTask.php | 8 ++- ...etterdienstTaskAdditionalFieldProvider.php | 20 +++--- .../DeutscherWetterdienstWarnCellTask.php | 2 +- Classes/Task/OpenWeatherMapTask.php | 9 +-- ...nWeatherMapTaskAdditionalFieldProvider.php | 35 +++++----- Classes/Upgrade/EmptyTaskLoggerUpgrade.php | 27 +++----- Classes/UserFunc/FlexFormUserFunc.php | 2 - Classes/Utility/WeatherUtility.php | 1 - .../ConvertMetricToISoUViewHelper.php | 25 ++++--- .../ConvertMetricToImperialViewHelper.php | 25 ++++--- Configuration/Backend/AjaxRoutes.php | 4 +- Configuration/TCA/Overrides/sys_template.php | 10 ++- Configuration/TCA/Overrides/tt_content.php | 5 ++ ...x_weather2_domain_model_currentweather.php | 68 ++++++++++--------- .../tx_weather2_domain_model_dwdwarncell.php | 16 +++-- .../tx_weather2_domain_model_weatheralert.php | 40 ++++++----- .../Task/OpenWeatherMapTaskTest.php | 4 +- .../CurrentWeatherControllerTest.php | 2 - Tests/Unit/Domain/Model/WeatherAlertTest.php | 2 +- ext_emconf.php | 3 +- ext_localconf.php | 7 +- ext_tables.php | 1 - 24 files changed, 171 insertions(+), 155 deletions(-) diff --git a/Classes/Domain/Model/CurrentWeather.php b/Classes/Domain/Model/CurrentWeather.php index b7d2ccf..e89e767 100644 --- a/Classes/Domain/Model/CurrentWeather.php +++ b/Classes/Domain/Model/CurrentWeather.php @@ -78,11 +78,6 @@ class CurrentWeather extends AbstractEntity */ protected $cloudsPercentage = 0; - /** - * @var string - */ - protected $serializedArray = ''; - /** * @var string */ diff --git a/Classes/Domain/Repository/DwdWarnCellRepository.php b/Classes/Domain/Repository/DwdWarnCellRepository.php index ccea191..ded54eb 100644 --- a/Classes/Domain/Repository/DwdWarnCellRepository.php +++ b/Classes/Domain/Repository/DwdWarnCellRepository.php @@ -17,6 +17,11 @@ /** * Repository to find warn cells by name + * + * $warnCellId looks like INT, but don't know if there are values starting with 0. + * Further the values are very huge, which can result in problems on 32bit machines. + * So keep $warnCellId to string + * @method DwdWarnCell findOneByWarnCellId(string $warnCellId); */ class DwdWarnCellRepository extends Repository { diff --git a/Classes/Task/DeutscherWetterdienstTask.php b/Classes/Task/DeutscherWetterdienstTask.php index e93e26e..7cd6cba 100644 --- a/Classes/Task/DeutscherWetterdienstTask.php +++ b/Classes/Task/DeutscherWetterdienstTask.php @@ -11,6 +11,7 @@ namespace JWeiland\Weather2\Task; +use Doctrine\DBAL\DBALException; use JWeiland\Weather2\Domain\Model\DwdWarnCell; use JWeiland\Weather2\Domain\Model\WeatherAlert; use JWeiland\Weather2\Domain\Repository\DwdWarnCellRepository; @@ -21,6 +22,7 @@ use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Http\RequestFactory; use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Object\Exception; use TYPO3\CMS\Extbase\Object\ObjectManager; use TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager; use TYPO3\CMS\Extbase\Service\CacheService; @@ -89,7 +91,7 @@ class DeutscherWetterdienstTask extends AbstractTask /** * @return bool - * @throws \TYPO3\CMS\Extbase\Object\Exception + * @throws Exception */ public function execute(): bool { @@ -188,7 +190,7 @@ protected function getUidOfAlert(array $alert): int $this->dbExtTable, [ 'comparison_hash' => $this->getComparisonHashForAlert($alert), - 'pid' => $this->recordStoragePage + 'pid' => $this->recordStoragePage, ] ) ->fetch(); @@ -264,7 +266,7 @@ protected function getDwdWarnCell(string $warnCellId): DwdWarnCell } /** - * @throws \Doctrine\DBAL\DBALException + * @throws DBALException */ protected function removeOldAlertsFromDb(): void { diff --git a/Classes/Task/DeutscherWetterdienstTaskAdditionalFieldProvider.php b/Classes/Task/DeutscherWetterdienstTaskAdditionalFieldProvider.php index 4dd9cd1..f88be20 100644 --- a/Classes/Task/DeutscherWetterdienstTaskAdditionalFieldProvider.php +++ b/Classes/Task/DeutscherWetterdienstTaskAdditionalFieldProvider.php @@ -50,7 +50,7 @@ class DeutscherWetterdienstTaskAdditionalFieldProvider extends AbstractAdditiona * @var array */ protected $requiredFields = [ - 'dwd_regionSelection' + 'dwd_regionSelection', ]; /** @@ -61,7 +61,7 @@ class DeutscherWetterdienstTaskAdditionalFieldProvider extends AbstractAdditiona protected $insertFields = [ 'dwd_selectedWarnCells', 'dwd_recordStoragePage', - 'dwd_clearCache' + 'dwd_clearCache', ]; public function __construct( @@ -75,10 +75,7 @@ public function __construct( } /** - * @param array $taskInfo * @param DeutscherWetterdienstTask $task - * @param SchedulerModuleController $schedulerModule - * @return array */ public function getAdditionalFields( array &$taskInfo, @@ -112,9 +109,10 @@ public function getAdditionalFields( ); $fieldCode = $this->addFlashMessage($flashMessage, true); } + $additionalFields[$fieldID] = [ 'code' => $fieldCode, - 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_scheduler_deutscherwetterdienst.xlf:warnCells' + 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_scheduler_deutscherwetterdienst.xlf:warnCells', ]; $fieldID = 'dwd_recordStoragePage'; @@ -123,14 +121,14 @@ public function getAdditionalFields( WeatherUtility::translate('buttons.recordStoragePage', 'deutscherwetterdienst') . ''; $additionalFields[$fieldID] = [ 'code' => $fieldCode, - 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_scheduler_deutscherwetterdienst.xlf:recordStoragePage' + 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_scheduler_deutscherwetterdienst.xlf:recordStoragePage', ]; $fieldID = 'dwd_clearCache'; $fieldCode = ''; $additionalFields[$fieldID] = [ 'code' => $fieldCode, - 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_scheduler_deutscherwetterdienst.xlf:clear_cache' + 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_scheduler_deutscherwetterdienst.xlf:clear_cache', ]; return $additionalFields; @@ -148,8 +146,8 @@ protected function initialize(): void $popupSettings = [ 'PopupWindow' => [ 'width' => '800px', - 'height' => '550px' - ] + 'height' => '550px', + ], ]; $this->pageRenderer->addInlineSettingArray('Popup', $popupSettings); $this->pageRenderer->addInlineSetting('FormEngine', 'moduleUrl', (string)$this->uriBuilder->buildUriFromRoute('record_edit')); @@ -209,7 +207,7 @@ public function validateAdditionalFields(array &$submittedData, SchedulerModuleC if (empty($value) && in_array($fieldName, $this->requiredFields, true)) { $isValid = false; - $this->addMessage('Field: ' . $fieldName . ' must not be empty', FlashMessage::ERROR); + $this->addMessage('Field: ' . $fieldName . ' must not be empty', AbstractMessage::ERROR); } else { $submittedData[$fieldName] = $value; } diff --git a/Classes/Task/DeutscherWetterdienstWarnCellTask.php b/Classes/Task/DeutscherWetterdienstWarnCellTask.php index 906b035..bfa9a64 100644 --- a/Classes/Task/DeutscherWetterdienstWarnCellTask.php +++ b/Classes/Task/DeutscherWetterdienstWarnCellTask.php @@ -64,7 +64,7 @@ protected function processResponse(ResponseInterface $response): void 'warn_cell_id' => $warnCellId, 'name' => $name, 'short_name' => $shortName, - 'sign' => $sign + 'sign' => $sign, ]; } } diff --git a/Classes/Task/OpenWeatherMapTask.php b/Classes/Task/OpenWeatherMapTask.php index 0ef960f..e301a21 100644 --- a/Classes/Task/OpenWeatherMapTask.php +++ b/Classes/Task/OpenWeatherMapTask.php @@ -115,9 +115,6 @@ class OpenWeatherMapTask extends AbstractTask /** * This method is the heart of the scheduler task. It will be fired if the scheduler * gets executed - * - * @return bool - * @throws \TYPO3\CMS\Extbase\Object\Exception */ public function execute(): bool { @@ -135,7 +132,7 @@ public function execute(): bool $this->removeOldRecordsFromDb(); $this->url = sprintf( - 'http://api.openweathermap.org/data/2.5/weather?q=%s,%s&units=%s&APPID=%s', + 'https://api.openweathermap.org/data/2.5/weather?q=%s,%s&units=%s&APPID=%s', urlencode($this->city), urlencode($this->country), 'metric', @@ -198,7 +195,7 @@ private function checkResponseCode(ResponseInterface $response): bool } /** @var \stdClass $responseClass */ - $responseClass = json_decode((string)$response->getBody()); + $responseClass = json_decode((string)$response->getBody(), false); switch ($responseClass->cod) { case '200': @@ -344,7 +341,7 @@ protected function removeOldRecordsFromDb(): void $this->dbExtTable, [ 'pid' => $this->recordStoragePage, - 'name' => $this->name + 'name' => $this->name, ] ); } diff --git a/Classes/Task/OpenWeatherMapTaskAdditionalFieldProvider.php b/Classes/Task/OpenWeatherMapTaskAdditionalFieldProvider.php index 15b169f..26d2416 100644 --- a/Classes/Task/OpenWeatherMapTaskAdditionalFieldProvider.php +++ b/Classes/Task/OpenWeatherMapTaskAdditionalFieldProvider.php @@ -14,6 +14,7 @@ use JWeiland\Weather2\Utility\WeatherUtility; use SJBR\StaticInfoTables\Domain\Model\Country; use SJBR\StaticInfoTables\Domain\Repository\CountryRepository; +use TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException; use TYPO3\CMS\Backend\Routing\UriBuilder; use TYPO3\CMS\Core\Http\RequestFactory; use TYPO3\CMS\Core\Messaging\AbstractMessage; @@ -52,7 +53,7 @@ class OpenWeatherMapTaskAdditionalFieldProvider extends AbstractAdditionalFieldP 'name', 'city', 'country', - 'apiKey' + 'apiKey', ]; /** @@ -70,7 +71,7 @@ class OpenWeatherMapTaskAdditionalFieldProvider extends AbstractAdditionalFieldP 'emailSenderName', 'emailSender', 'emailReceiver', - 'recordStoragePage' + 'recordStoragePage', ]; public function __construct( @@ -85,7 +86,7 @@ public function __construct( /** * @param OpenWeatherMapTask|null $task - * @throws \TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException + * @throws RouteNotFoundException */ public function getAdditionalFields( array &$taskInfo, @@ -97,8 +98,8 @@ public function getAdditionalFields( $popupSettings = [ 'PopupWindow' => [ 'width' => '800px', - 'height' => '550px' - ] + 'height' => '550px', + ], ]; $this->pageRenderer->addInlineSettingArray('Popup', $popupSettings); $this->pageRenderer->addInlineSetting('FormEngine', 'moduleUrl', (string)$this->uriBuilder->buildUriFromRoute('record_edit')); @@ -128,7 +129,7 @@ public function getAdditionalFields( $fieldCode = ''; $additionalFields[$fieldID] = [ 'code' => $fieldCode, - 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_scheduler_openweatherapi.xlf:name' + 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_scheduler_openweatherapi.xlf:name', ]; $fieldID = 'recordStoragePage'; @@ -138,7 +139,7 @@ public function getAdditionalFields( $additionalFields[$fieldID] = [ 'code' => $fieldCode, - 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_scheduler_openweatherapi.xlf:record_storage_page' + 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_scheduler_openweatherapi.xlf:record_storage_page', ]; // todo: Add second task to import regions with id from OpenWeatherMap-Servers like DeutschWetterDienstTask @@ -146,63 +147,63 @@ public function getAdditionalFields( $fieldCode = ''; $additionalFields[$fieldID] = [ 'code' => $fieldCode, - 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_scheduler_openweatherapi.xlf:city' + 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_scheduler_openweatherapi.xlf:city', ]; $fieldID = 'country'; $fieldCode = ''; $additionalFields[$fieldID] = [ 'code' => $fieldCode, - 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_scheduler_openweatherapi.xlf:country' + 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_scheduler_openweatherapi.xlf:country', ]; $fieldID = 'apiKey'; $fieldCode = ''; $additionalFields[$fieldID] = [ 'code' => $fieldCode, - 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_scheduler_openweatherapi.xlf:api_key' + 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_scheduler_openweatherapi.xlf:api_key', ]; $fieldID = 'clearCache'; $fieldCode = ''; $additionalFields[$fieldID] = [ 'code' => $fieldCode, - 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_scheduler_openweatherapi.xlf:clear_cache' + 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_scheduler_openweatherapi.xlf:clear_cache', ]; $fieldID = 'errorNotification'; $fieldCode = ''; $additionalFields[$fieldID] = [ 'code' => $fieldCode, - 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_scheduler_openweatherapi.xlf:error_notification' + 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_scheduler_openweatherapi.xlf:error_notification', ]; $fieldID = 'mailConfig'; $fieldCode = $this->checkMailConfiguration(); $additionalFields[$fieldID] = [ 'code' => $fieldCode, - 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_scheduler_openweatherapi.xlf:mail_config' + 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_scheduler_openweatherapi.xlf:mail_config', ]; $fieldID = 'emailSenderName'; $fieldCode = ''; $additionalFields[$fieldID] = [ 'code' => $fieldCode, - 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_scheduler_openweatherapi.xlf:email_sendername' + 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_scheduler_openweatherapi.xlf:email_sendername', ]; $fieldID = 'emailSender'; $fieldCode = ''; $additionalFields[$fieldID] = [ 'code' => $fieldCode, - 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_scheduler_openweatherapi.xlf:email_sender' + 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_scheduler_openweatherapi.xlf:email_sender', ]; $fieldID = 'emailReceiver'; $fieldCode = ''; $additionalFields[$fieldID] = [ 'code' => $fieldCode, - 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_scheduler_openweatherapi.xlf:email_receiver' + 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_scheduler_openweatherapi.xlf:email_receiver', ]; return $additionalFields; @@ -325,7 +326,7 @@ private function checkMailConfiguration(): string $text .= ''; $text .= '

    Transport: ' . $mailConfiguration['transport'] . '

    '; - if ($mailConfiguration['transport'] == 'smtp') { + if ($mailConfiguration['transport'] === 'smtp') { $text .= '

    SMTP Server: ' . $mailConfiguration['transport_smtp_server'] . '

    SMTP Encryption: ' . $mailConfiguration['transport_smtp_encrypt'] . '

    SMTP Username: ' . $mailConfiguration['transport_smtp_username'] . '

    '; } diff --git a/Classes/Upgrade/EmptyTaskLoggerUpgrade.php b/Classes/Upgrade/EmptyTaskLoggerUpgrade.php index d28e96d..91af454 100644 --- a/Classes/Upgrade/EmptyTaskLoggerUpgrade.php +++ b/Classes/Upgrade/EmptyTaskLoggerUpgrade.php @@ -16,6 +16,7 @@ use JWeiland\Weather2\Task\OpenWeatherMapTask; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Install\Updates\DatabaseUpdatedPrerequisite; use TYPO3\CMS\Install\Updates\UpgradeWizardInterface; use TYPO3\CMS\Scheduler\Execution; use TYPO3\CMS\Scheduler\Task\AbstractTask; @@ -30,8 +31,6 @@ class EmptyTaskLoggerUpgrade implements UpgradeWizardInterface /** * Return the identifier for this wizard * This should be the same string as used in the ext_localconf class registration - * - * @return string */ public function getIdentifier(): string { @@ -40,8 +39,6 @@ public function getIdentifier(): string /** * Return the speaking name of this wizard - * - * @return string */ public function getTitle(): string { @@ -50,8 +47,6 @@ public function getTitle(): string /** * Return the description for this wizard - * - * @return string */ public function getDescription(): string { @@ -86,8 +81,8 @@ public function executeUpdate(): bool Execution::class, DeutscherWetterdienstTask::class, DeutscherWetterdienstWarnCellTask::class, - OpenWeatherMapTask::class - ] + OpenWeatherMapTask::class, + ], ] ); if ($task instanceof AbstractTask) { @@ -96,10 +91,10 @@ public function executeUpdate(): bool $connection->update( 'tx_scheduler_task', [ - 'serialized_task_object' => serialize($task) + 'serialized_task_object' => serialize($task), ], [ - 'uid' => (int)$record['uid'] + 'uid' => (int)$record['uid'], ] ); } @@ -110,8 +105,6 @@ public function executeUpdate(): bool /** * Get all scheduler Tasks of weather2 - * - * @return array */ protected function getWeather2SchedulerTasks(): array { @@ -124,20 +117,20 @@ protected function getWeather2SchedulerTasks(): array $queryBuilder->expr()->orX( $queryBuilder->expr()->like( 'serialized_task_object', - $queryBuilder->createNamedParameter('%OpenWeatherMapTask%', \PDO::PARAM_STR) + $queryBuilder->createNamedParameter('%OpenWeatherMapTask%') ), $queryBuilder->expr()->like( 'serialized_task_object', - $queryBuilder->createNamedParameter('%DeutscherWetterdienstTask%', \PDO::PARAM_STR) + $queryBuilder->createNamedParameter('%DeutscherWetterdienstTask%') ), $queryBuilder->expr()->like( 'serialized_task_object', - $queryBuilder->createNamedParameter('%DeutscherWetterdienstWarnCellTask%', \PDO::PARAM_STR) + $queryBuilder->createNamedParameter('%DeutscherWetterdienstWarnCellTask%') ) ), $queryBuilder->expr()->like( 'serialized_task_object', - $queryBuilder->createNamedParameter('%logger"%', \PDO::PARAM_STR) + $queryBuilder->createNamedParameter('%logger"%') ) ) ->execute(); @@ -156,7 +149,7 @@ protected function getWeather2SchedulerTasks(): array public function getPrerequisites(): array { return [ - DatabaseUpdatedPrerequisite::class + DatabaseUpdatedPrerequisite::class, ]; } diff --git a/Classes/UserFunc/FlexFormUserFunc.php b/Classes/UserFunc/FlexFormUserFunc.php index 9c43169..a61c98a 100644 --- a/Classes/UserFunc/FlexFormUserFunc.php +++ b/Classes/UserFunc/FlexFormUserFunc.php @@ -21,8 +21,6 @@ class FlexFormUserFunc { /** * Only display results if name equals in plugin specified name - * - * @param array $fConfig */ public function getSelection(array &$fConfig): void { diff --git a/Classes/Utility/WeatherUtility.php b/Classes/Utility/WeatherUtility.php index fa85f46..2078560 100644 --- a/Classes/Utility/WeatherUtility.php +++ b/Classes/Utility/WeatherUtility.php @@ -21,7 +21,6 @@ class WeatherUtility /** * Returns the translation of $name from locallang for $task * - * @param string $name * @param string $task openweatherapi or deutscherwetterdienst or deutscherwetterdienstJs * @return string|null the translation or null if no translation was found */ diff --git a/Classes/ViewHelpers/ConvertMetricToISoUViewHelper.php b/Classes/ViewHelpers/ConvertMetricToISoUViewHelper.php index c1d2bdb..01b12b2 100644 --- a/Classes/ViewHelpers/ConvertMetricToISoUViewHelper.php +++ b/Classes/ViewHelpers/ConvertMetricToISoUViewHelper.php @@ -27,21 +27,23 @@ class ConvertMetricToISoUViewHelper extends AbstractViewHelper protected $escapeOutput = false; - /** - * Initialize Arguments - */ public function initializeArguments(): void { - $this->registerArgument('as', 'string', 'Holds converted Weather data', false, 'convertedData'); - $this->registerArgument('weatherModel', CurrentWeather::class, 'Current Weather Object', true); + $this->registerArgument( + 'as', + 'string', + 'Holds converted Weather data', + false, + 'convertedData' + ); + $this->registerArgument( + 'weatherModel', + CurrentWeather::class, + 'Current Weather Object', + true + ); } - /** - * @param array $arguments - * @param \Closure $renderChildrenClosure - * @param RenderingContextInterface $renderingContext - * @return string - */ public static function renderStatic( array $arguments, \Closure $renderChildrenClosure, @@ -50,6 +52,7 @@ public static function renderStatic( /** @var CurrentWeather $currentWeather */ $weatherModel = $arguments['weatherModel']; $convertedModel = clone $weatherModel; + /** @var $converter WeatherConverterService */ $converter = GeneralUtility::makeInstance(WeatherConverterService::class); $convertedModel->setTemperatureC((int)$converter->convertCelsiusToKelvin($weatherModel->getTemperatureC())); diff --git a/Classes/ViewHelpers/ConvertMetricToImperialViewHelper.php b/Classes/ViewHelpers/ConvertMetricToImperialViewHelper.php index f7a6ad0..eb67ef6 100644 --- a/Classes/ViewHelpers/ConvertMetricToImperialViewHelper.php +++ b/Classes/ViewHelpers/ConvertMetricToImperialViewHelper.php @@ -27,21 +27,23 @@ class ConvertMetricToImperialViewHelper extends AbstractViewHelper protected $escapeOutput = false; - /** - * Initialize Arguments - */ public function initializeArguments(): void { - $this->registerArgument('as', 'string', 'Holds converted Weather data', false, 'convertedData'); - $this->registerArgument('weatherModel', CurrentWeather::class, 'Current Weather Object', true); + $this->registerArgument( + 'as', + 'string', + 'Holds converted Weather data', + false, + 'convertedData' + ); + $this->registerArgument( + 'weatherModel', + CurrentWeather::class, + 'Current Weather Object', + true + ); } - /** - * @param array $arguments - * @param \Closure $renderChildrenClosure - * @param RenderingContextInterface $renderingContext - * @return string - */ public static function renderStatic( array $arguments, \Closure $renderChildrenClosure, @@ -50,6 +52,7 @@ public static function renderStatic( /** @var CurrentWeather $currentWeather */ $weatherModel = $arguments['weatherModel']; $convertedModel = clone $weatherModel; + /** @var $converter WeatherConverterService */ $converter = GeneralUtility::makeInstance(WeatherConverterService::class); $convertedModel->setTemperatureC((int)$converter->convertCelsiusToFahrenheit($weatherModel->getTemperatureC())); diff --git a/Configuration/Backend/AjaxRoutes.php b/Configuration/Backend/AjaxRoutes.php index b2d1ee8..1d003e0 100644 --- a/Configuration/Backend/AjaxRoutes.php +++ b/Configuration/Backend/AjaxRoutes.php @@ -2,6 +2,6 @@ return [ 'weather2_dwd_warn-cell-search' => [ 'path' => '/weather2/dwd/warn-cell-search', - 'target' => \JWeiland\Weather2\Ajax\DeutscherWetterdienstWarnCellSearch::class . '::renderWarnCells' - ] + 'target' => \JWeiland\Weather2\Ajax\DeutscherWetterdienstWarnCellSearch::class . '::renderWarnCells', + ], ]; diff --git a/Configuration/TCA/Overrides/sys_template.php b/Configuration/TCA/Overrides/sys_template.php index 407b3ee..589ef75 100644 --- a/Configuration/TCA/Overrides/sys_template.php +++ b/Configuration/TCA/Overrides/sys_template.php @@ -1,2 +1,10 @@ [ 'title' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_db.xlf:tx_weather2_domain_model_currentweather', @@ -15,7 +19,7 @@ 'endtime' => 'endtime', ], 'searchFields' => 'name,icon', - 'iconfile' => 'EXT:weather2/Resources/Public/Icons/tx_weather2_domain_model_currentweather.gif' + 'iconfile' => 'EXT:weather2/Resources/Public/Icons/tx_weather2_domain_model_currentweather.gif', ], 'types' => [ '1' => ['showitem' => 'name, measure_timestamp, temperature_c, pressure_hpa, humidity_percentage, min_temp_c, max_temp_c, wind_speed_m_p_s, wind_direction_deg, pop_percentage, rain_volume, snow_volume, clouds_percentage, icon, serialized_array, --div--;LLL:EXT:cms/locallang_ttc.xlf:tabs.access, starttime, endtime'], @@ -32,7 +36,7 @@ 'checkbox' => 0, 'default' => 0, 'range' => [ - 'lower' => mktime(0, 0, 0, date('m'), date('d'), date('Y')) + 'lower' => mktime(0, 0, 0, date('m'), date('d'), date('Y')), ], ], ], @@ -47,7 +51,7 @@ 'checkbox' => 0, 'default' => 0, 'range' => [ - 'lower' => mktime(0, 0, 0, date('m'), date('d'), date('Y')) + 'lower' => mktime(0, 0, 0, date('m'), date('d'), date('Y')), ], ], ], @@ -57,7 +61,7 @@ 'config' => [ 'type' => 'input', 'size' => 30, - 'eval' => 'trim' + 'eval' => 'trim', ], ], 'measure_timestamp' => [ @@ -66,8 +70,8 @@ 'config' => [ 'type' => 'input', 'size' => 4, - 'eval' => 'int' - ] + 'eval' => 'int', + ], ], 'temperature_c' => [ 'exclude' => 1, @@ -75,8 +79,8 @@ 'config' => [ 'type' => 'input', 'size' => 30, - 'eval' => 'double2' - ] + 'eval' => 'double2', + ], ], 'pressure_hpa' => [ 'exclude' => 1, @@ -84,8 +88,8 @@ 'config' => [ 'type' => 'input', 'size' => 30, - 'eval' => 'double2' - ] + 'eval' => 'double2', + ], ], 'humidity_percentage' => [ 'exclude' => 1, @@ -93,8 +97,8 @@ 'config' => [ 'type' => 'input', 'size' => 30, - 'eval' => 'double2' - ] + 'eval' => 'double2', + ], ], 'min_temp_c' => [ 'exclude' => 1, @@ -102,8 +106,8 @@ 'config' => [ 'type' => 'input', 'size' => 30, - 'eval' => 'double2' - ] + 'eval' => 'double2', + ], ], 'max_temp_c' => [ 'exclude' => 1, @@ -111,8 +115,8 @@ 'config' => [ 'type' => 'input', 'size' => 30, - 'eval' => 'double2' - ] + 'eval' => 'double2', + ], ], 'wind_speed_m_p_s' => [ 'exclude' => 1, @@ -120,8 +124,8 @@ 'config' => [ 'type' => 'input', 'size' => 30, - 'eval' => 'double2' - ] + 'eval' => 'double2', + ], ], 'wind_direction_deg' => [ 'exclude' => 1, @@ -129,8 +133,8 @@ 'config' => [ 'type' => 'input', 'size' => 30, - 'eval' => 'double2' - ] + 'eval' => 'double2', + ], ], 'pop_percentage' => [ 'exclude' => 1, @@ -138,8 +142,8 @@ 'config' => [ 'type' => 'input', 'size' => 30, - 'eval' => 'double2' - ] + 'eval' => 'double2', + ], ], 'snow_volume' => [ 'exclude' => 1, @@ -147,8 +151,8 @@ 'config' => [ 'type' => 'input', 'size' => 30, - 'eval' => 'double2' - ] + 'eval' => 'double2', + ], ], 'rain_volume' => [ 'exclude' => 1, @@ -156,8 +160,8 @@ 'config' => [ 'type' => 'input', 'size' => 30, - 'eval' => 'double2' - ] + 'eval' => 'double2', + ], ], 'clouds_percentage' => [ 'exclude' => 1, @@ -165,8 +169,8 @@ 'config' => [ 'type' => 'input', 'size' => 30, - 'eval' => 'double2' - ] + 'eval' => 'double2', + ], ], 'serialized_array' => [ 'exclude' => 1, @@ -174,7 +178,7 @@ 'config' => [ 'type' => 'input', 'size' => 30, - 'eval' => 'trim' + 'eval' => 'trim', ], ], 'icon' => [ @@ -183,7 +187,7 @@ 'config' => [ 'type' => 'input', 'size' => 30, - 'eval' => 'trim' + 'eval' => 'trim', ], ], 'condition_code' => [ @@ -191,8 +195,8 @@ 'config' => [ 'type' => 'input', 'size' => 4, - 'eval' => 'int' - ] + 'eval' => 'int', + ], ], ], ]; diff --git a/Configuration/TCA/tx_weather2_domain_model_dwdwarncell.php b/Configuration/TCA/tx_weather2_domain_model_dwdwarncell.php index fb7a2ab..334766f 100644 --- a/Configuration/TCA/tx_weather2_domain_model_dwdwarncell.php +++ b/Configuration/TCA/tx_weather2_domain_model_dwdwarncell.php @@ -1,4 +1,8 @@ [ 'title' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_db.xlf:tx_weather2_domain_model_dwdwarncell', @@ -10,10 +14,10 @@ 'rootLevel' => 1, 'delete' => 'deleted', 'enablecolumns' => [ - 'disabled' => 'hidden' + 'disabled' => 'hidden', ], 'searchFields' => 'name,warn_cell_id,sign', - 'iconfile' => 'EXT:weather2/Resources/Public/Icons/tx_weather2_domain_model_dwdwarncell.gif' + 'iconfile' => 'EXT:weather2/Resources/Public/Icons/tx_weather2_domain_model_dwdwarncell.gif', ], 'types' => [ '1' => ['showitem' => 'hidden, warn_cell_id, name, short_name, sign'], @@ -24,7 +28,7 @@ 'config' => [ 'type' => 'input', 'size' => 30, - 'eval' => 'trim,required' + 'eval' => 'trim,required', ], ], 'name' => [ @@ -32,7 +36,7 @@ 'config' => [ 'type' => 'input', 'size' => 30, - 'eval' => 'trim,required' + 'eval' => 'trim,required', ], ], 'short_name' => [ @@ -40,7 +44,7 @@ 'config' => [ 'type' => 'input', 'size' => 30, - 'eval' => 'trim,required' + 'eval' => 'trim,required', ], ], 'sign' => [ @@ -48,7 +52,7 @@ 'config' => [ 'type' => 'input', 'size' => 30, - 'eval' => 'trim' + 'eval' => 'trim', ], ], ], diff --git a/Configuration/TCA/tx_weather2_domain_model_weatheralert.php b/Configuration/TCA/tx_weather2_domain_model_weatheralert.php index 3c96c43..39c3b69 100644 --- a/Configuration/TCA/tx_weather2_domain_model_weatheralert.php +++ b/Configuration/TCA/tx_weather2_domain_model_weatheralert.php @@ -1,4 +1,8 @@ [ 'title' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_db.xlf:tx_weather2_domain_model_weatheralert', @@ -16,7 +20,7 @@ 'endtime' => 'endtime', ], 'searchFields' => 'dwd_warn_cell,level,type,title,description,instruction', - 'iconfile' => 'EXT:weather2/Resources/Public/Icons/tx_weather2_domain_model_weatheralert.gif' + 'iconfile' => 'EXT:weather2/Resources/Public/Icons/tx_weather2_domain_model_weatheralert.gif', ], 'types' => [ '1' => ['showitem' => 'dwd_warn_cell, level, type, title, description, instruction, preliminary_information, start_date, end_date,--div--;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.tabs.access, starttime, endtime'], @@ -33,7 +37,7 @@ 'checkbox' => 0, 'default' => 0, 'range' => [ - 'lower' => mktime(0, 0, 0, date('m'), date('d'), date('Y')) + 'lower' => mktime(0, 0, 0, date('m'), date('d'), date('Y')), ], ], ], @@ -48,7 +52,7 @@ 'checkbox' => 0, 'default' => 0, 'range' => [ - 'lower' => mktime(0, 0, 0, date('m'), date('d'), date('Y')) + 'lower' => mktime(0, 0, 0, date('m'), date('d'), date('Y')), ], ], ], @@ -63,7 +67,7 @@ 'foreign_table' => 'tx_weather2_domain_model_dwdwarncell', 'forgein_table_where' => 'ORDER BY name ASC', 'minitems' => 1, - 'maxitems' => 1 + 'maxitems' => 1, ], ], 'level' => [ @@ -81,11 +85,11 @@ ['LLL:EXT:weather2/Resources/Private/Language/locallang_general.xlf:tx_weather2.warningLevels.2', 2], ['LLL:EXT:weather2/Resources/Private/Language/locallang_general.xlf:tx_weather2.warningLevels.3', 3], ['LLL:EXT:weather2/Resources/Private/Language/locallang_general.xlf:tx_weather2.warningLevels.4', 4], - ['LLL:EXT:weather2/Resources/Private/Language/locallang_general.xlf:tx_weather2.warningLevels.5', 5] + ['LLL:EXT:weather2/Resources/Private/Language/locallang_general.xlf:tx_weather2.warningLevels.5', 5], ], 'size' => 1, - 'eval' => 'int,required' - ] + 'eval' => 'int,required', + ], ], 'type' => [ 'exclude' => 1, @@ -107,11 +111,11 @@ ['LLL:EXT:weather2/Resources/Private/Language/locallang_general.xlf:tx_weather2.warningTypes.uv', 9], ['LLL:EXT:weather2/Resources/Private/Language/locallang_general.xlf:tx_weather2.warningTypes.coast', 10], ['LLL:EXT:weather2/Resources/Private/Language/locallang_general.xlf:tx_weather2.warningTypes.lake', 11], - ['LLL:EXT:weather2/Resources/Private/Language/locallang_general.xlf:tx_weather2.warningTypes.sea', 12] + ['LLL:EXT:weather2/Resources/Private/Language/locallang_general.xlf:tx_weather2.warningTypes.sea', 12], ], 'size' => 1, - 'eval' => 'int,required' - ] + 'eval' => 'int,required', + ], ], 'title' => [ 'exclude' => 1, @@ -119,7 +123,7 @@ 'config' => [ 'type' => 'input', 'size' => 30, - 'eval' => 'trim,required' + 'eval' => 'trim,required', ], ], 'description' => [ @@ -127,7 +131,7 @@ 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_db.xlf:tx_weather2_domain_model_weatheralert.description', 'config' => [ 'type' => 'text', - 'eval' => 'trim,required' + 'eval' => 'trim,required', ], ], 'instruction' => [ @@ -135,7 +139,7 @@ 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_db.xlf:tx_weather2_domain_model_weatheralert.instruction', 'config' => [ 'type' => 'text', - 'eval' => 'trim' + 'eval' => 'trim', ], ], 'start_date' => [ @@ -164,16 +168,16 @@ ], 'comparison_hash' => [ 'config' => [ - 'type' => 'passthrough' - ] + 'type' => 'passthrough', + ], ], 'preliminary_information' => [ 'exclude' => 1, 'label' => 'LLL:EXT:weather2/Resources/Private/Language/locallang_db.xlf:tx_weather2_domain_model_weatheralert.preliminary_information', 'config' => [ 'type' => 'check', - 'renderType' => 'check' - ] - ] + 'renderType' => 'check', + ], + ], ], ]; diff --git a/Tests/Functional/Task/OpenWeatherMapTaskTest.php b/Tests/Functional/Task/OpenWeatherMapTaskTest.php index 02597fe..28ed679 100644 --- a/Tests/Functional/Task/OpenWeatherMapTaskTest.php +++ b/Tests/Functional/Task/OpenWeatherMapTaskTest.php @@ -61,7 +61,7 @@ class OpenWeatherMapTaskTest extends FunctionalTestCase * @var string[] */ protected $coreExtensionsToLoad = [ - 'scheduler' + 'scheduler', ]; /** @@ -166,7 +166,7 @@ public function execute(): void 'main' => 'rain', 'icon' => '[ICON]', ], - ] + ], ])); $this->responseProphecy diff --git a/Tests/Unit/Controller/CurrentWeatherControllerTest.php b/Tests/Unit/Controller/CurrentWeatherControllerTest.php index 7e85133..9f69dce 100644 --- a/Tests/Unit/Controller/CurrentWeatherControllerTest.php +++ b/Tests/Unit/Controller/CurrentWeatherControllerTest.php @@ -47,8 +47,6 @@ protected function tearDown(): void */ public function showActionCallsRepositoryFindBySelectionWithSettingAsArgument(): void { - $currentWeather = new \JWeiland\Weather2\Domain\Model\CurrentWeather(); - $currentWeatherRepository = $this->getAccessibleMock( CurrentWeatherRepository::class, ['findBySelection'], diff --git a/Tests/Unit/Domain/Model/WeatherAlertTest.php b/Tests/Unit/Domain/Model/WeatherAlertTest.php index bc141b1..41922ce 100644 --- a/Tests/Unit/Domain/Model/WeatherAlertTest.php +++ b/Tests/Unit/Domain/Model/WeatherAlertTest.php @@ -20,7 +20,7 @@ class WeatherAlertTest extends UnitTestCase { /** - * @var WeatherAlert|\PHPUnit_Framework_MockObject_MockObject + * @var WeatherAlert */ protected $subject; diff --git a/ext_emconf.php b/ext_emconf.php index eb9fab1..747a76a 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -1,4 +1,5 @@ 'Weather Forecasts and Alerts', 'description' => 'Display weather forecasts and weather alerts using various Weather APIs. Default APIs: OpenWeatherMap and Deutscher Wetterdienst', @@ -11,7 +12,7 @@ 'constraints' => [ 'depends' => [ 'typo3' => '10.4.29-11.5.99', - 'static_info_tables' => '6.6.0' + 'static_info_tables' => '6.6.0', ], 'conflicts' => [], 'suggests' => [], diff --git a/ext_localconf.php b/ext_localconf.php index 43902ef..56cbd69 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -1,5 +1,4 @@ 'weather2', 'title' => 'Call openweathermap.org api', 'description' => 'Calls the api of openweathermap.org and saves response into database', - 'additionalFields' => JWeiland\Weather2\Task\OpenWeatherMapTaskAdditionalFieldProvider::class + 'additionalFields' => JWeiland\Weather2\Task\OpenWeatherMapTaskAdditionalFieldProvider::class, ]; $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][JWeiland\Weather2\Task\DeutscherWetterdienstTask::class] = [ 'extension' => 'weather2', 'title' => 'Get weather alerts from Deutscher Wetterdienst', 'description' => 'Calls the Deutscher Wetterdienst api and saves response in weather2 format into database', - 'additionalFields' => JWeiland\Weather2\Task\DeutscherWetterdienstTaskAdditionalFieldProvider::class + 'additionalFields' => JWeiland\Weather2\Task\DeutscherWetterdienstTaskAdditionalFieldProvider::class, ]; $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][JWeiland\Weather2\Task\DeutscherWetterdienstWarnCellTask::class] = [ 'extension' => 'weather2', 'title' => 'Get warn cell records from Deutscher Wetterdienst', - 'description' => 'Calls the Deutscher Wetterdienst api and saves warn cells into database. Required before using DeutscherWetterdienstTask!' + 'description' => 'Calls the Deutscher Wetterdienst api and saves warn cells into database. Required before using DeutscherWetterdienstTask!', ]; // Set logger to NULL in weather2 Tasks before serializing it to DB diff --git a/ext_tables.php b/ext_tables.php index 8c0d627..3f53b3e 100644 --- a/ext_tables.php +++ b/ext_tables.php @@ -1,5 +1,4 @@ Date: Mon, 20 Feb 2023 09:44:46 +0100 Subject: [PATCH 07/11] Replace prophecy in func tests --- .../Task/OpenWeatherMapTaskTest.php | 150 +++++++++--------- 1 file changed, 78 insertions(+), 72 deletions(-) diff --git a/Tests/Functional/Task/OpenWeatherMapTaskTest.php b/Tests/Functional/Task/OpenWeatherMapTaskTest.php index 28ed679..7a10a1c 100644 --- a/Tests/Functional/Task/OpenWeatherMapTaskTest.php +++ b/Tests/Functional/Task/OpenWeatherMapTaskTest.php @@ -15,8 +15,7 @@ use JWeiland\Weather2\Domain\Model\CurrentWeather; use JWeiland\Weather2\Task\OpenWeatherMapTask; use Nimut\TestingFramework\TestCase\FunctionalTestCase; -use Prophecy\Argument; -use Prophecy\Prophecy\ObjectProphecy; +use PHPUnit\Framework\MockObject\MockObject; use Psr\Http\Message\ResponseInterface; use TYPO3\CMS\Core\Http\RequestFactory; use TYPO3\CMS\Core\Http\Stream; @@ -38,19 +37,19 @@ class OpenWeatherMapTaskTest extends FunctionalTestCase protected $stream; /** - * @var ResponseInterface|ObjectProphecy + * @var ResponseInterface|MockObject */ - protected $responseProphecy; + protected $responseMock; /** - * @var RequestFactory|ObjectProphecy + * @var RequestFactory|MockObject */ - protected $requestFactoryProphecy; + protected $requestFactoryMock; /** - * @var PersistenceManagerInterface|ObjectProphecy + * @var PersistenceManagerInterface|MockObject */ - protected $persistenceManagerProphecy; + protected $persistenceManagerMock; /** * @var OpenWeatherMapTask @@ -80,32 +79,35 @@ protected function setUp(): void $this->stream = new Stream('php://temp', 'rw'); - $this->responseProphecy = $this->prophesize(Response::class); - $this->responseProphecy - ->getBody() - ->shouldBeCalled() + $this->responseMock = $this->createMock(Response::class); + $this->responseMock + ->expects(self::atLeastOnce()) + ->method('getBody') ->willReturn($this->stream); - $this->requestFactoryProphecy = $this->prophesize(RequestFactory::class); - $this->requestFactoryProphecy - ->request(Argument::type('string')) - ->shouldBeCalled() - ->willReturn($this->responseProphecy->reveal()); + $this->requestFactoryMock = $this->createMock(RequestFactory::class); + $this->requestFactoryMock + ->expects(self::once()) + ->method('request') + ->with(self::isType('string')) + ->willReturn($this->responseMock); - GeneralUtility::addInstance(RequestFactory::class, $this->requestFactoryProphecy->reveal()); + GeneralUtility::addInstance(RequestFactory::class, $this->requestFactoryMock); - $this->persistenceManagerProphecy = $this->prophesize(PersistenceManager::class); - $this->persistenceManagerProphecy - ->persistAll() - ->shouldBeCalled(); + $this->persistenceManagerMock = $this->createMock(PersistenceManager::class); + $this->persistenceManagerMock + ->expects(self::once()) + ->method('persistAll'); - /** @var ObjectManagerInterface|ObjectProphecy $objectManagerProphecy */ - $objectManagerProphecy = $this->prophesize(ObjectManager::class); - $objectManagerProphecy - ->get(PersistenceManager::class) - ->willReturn($this->persistenceManagerProphecy->reveal()); + /** @var ObjectManagerInterface|MockObject $objectManagerMock */ + $objectManagerMock = $this->createMock(ObjectManager::class); + $objectManagerMock + ->expects(self::once()) + ->method('get') + ->with(self::identicalTo(PersistenceManager::class)) + ->willReturn($this->persistenceManagerMock); - GeneralUtility::setSingletonInstance(ObjectManager::class, $objectManagerProphecy->reveal()); + GeneralUtility::setSingletonInstance(ObjectManager::class, $objectManagerMock); // We have to use GM:makeInstance because of LoggerAwareInterface $this->subject = GeneralUtility::makeInstance(OpenWeatherMapTask::class); @@ -121,9 +123,9 @@ protected function tearDown(): void { unset( $this->subject, - $this->persistenceManagerProphecy, - $this->requestFactoryProphecy, - $this->responseProphecy, + $this->persistenceManagerMock, + $this->requestFactoryMock, + $this->responseMock, $this->stream ); @@ -132,50 +134,55 @@ protected function tearDown(): void /** * @test + * @throws \JsonException */ public function execute(): void { - $this->stream->write(json_encode([ - 'cod' => true, - 'dt' => time(), - 'main' => [ - 'temp' => 14.6, - 'pressure' => 8, - 'humidity' => 12, - 'temp_min' => 13.2, - 'temp_max' => 16.4, - ], - 'wind' => [ - 'speed' => 3.7, - 'deg' => 25, - ], - 'snow' => [ - '1h' => 4, - '3h' => 11, - ], - 'rain' => [ - '1h' => 6, - '3h' => 15, - ], - 'clouds' => [ - 'all' => 11, - ], - 'weather' => [ - 0 => [ - 'id' => 1256, - 'main' => 'rain', - 'icon' => '[ICON]', + $this->stream->write( + json_encode([ + 'cod' => true, + 'dt' => time(), + 'main' => [ + 'temp' => 14.6, + 'pressure' => 8, + 'humidity' => 12, + 'temp_min' => 13.2, + 'temp_max' => 16.4, ], - ], - ])); + 'wind' => [ + 'speed' => 3.7, + 'deg' => 25, + ], + 'snow' => [ + '1h' => 4.0, + '3h' => 11.0, + ], + 'rain' => [ + '1h' => 6.0, + '3h' => 15.0, + ], + 'clouds' => [ + 'all' => 11, + ], + 'weather' => [ + 0 => [ + 'id' => 1256, + 'main' => 'rain', + 'icon' => '[ICON]', + ], + ], + ], JSON_THROW_ON_ERROR) + ); - $this->responseProphecy - ->getStatusCode() - ->shouldBeCalled() + $this->responseMock + ->expects(self::atLeastOnce()) + ->method('getStatusCode') ->willReturn(200); - $this->persistenceManagerProphecy - ->add(Argument::that(static function (CurrentWeather $currentWeather) { + $this->persistenceManagerMock + ->expects(self::once()) + ->method('add') + ->with(self::callback(static function (CurrentWeather $currentWeather) { return $currentWeather->getName() === 'Filderstadt' && $currentWeather->getMeasureTimestamp() instanceof \DateTime && $currentWeather->getTemperatureC() === 14.6 @@ -185,13 +192,12 @@ public function execute(): void && $currentWeather->getMaxTempC() === 16.4 && $currentWeather->getWindSpeedMPS() === 3.7 && $currentWeather->getWindDirectionDeg() === 25 - && $currentWeather->getSnowVolume() === 4 - && $currentWeather->getRainVolume() === 6 + && $currentWeather->getSnowVolume() === 4.0 + && $currentWeather->getRainVolume() === 6.0 && $currentWeather->getCloudsPercentage() === 11 && $currentWeather->getIcon() === '[ICON]' && $currentWeather->getConditionCode() === 1256; - })) - ->shouldBeCalled(); + })); self::assertTrue( $this->subject->execute() From 620e841484d2a00c1077a7a86805a38b1e17f530 Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Mon, 20 Feb 2023 09:45:25 +0100 Subject: [PATCH 08/11] Remove prophecy package from require-dev --- composer.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/composer.json b/composer.json index be0e6fa..7c66345 100644 --- a/composer.json +++ b/composer.json @@ -24,8 +24,7 @@ "require-dev": { "roave/security-advisories": "dev-latest", "friendsofphp/php-cs-fixer": "^3.10", - "nimut/testing-framework": "^6.0", - "phpspec/prophecy-phpunit": "^2.0" + "nimut/testing-framework": "^6.0" }, "autoload": { "psr-4": { From f985b897ecc44594ee684cdd89e75dbacc3dc80f Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Mon, 20 Feb 2023 10:12:19 +0100 Subject: [PATCH 09/11] Update upgrade instructions --- .../AdministratorManual/Upgrade/Index.rst | 28 +++++++++++++++++++ Documentation/Settings.cfg | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/Documentation/AdministratorManual/Upgrade/Index.rst b/Documentation/AdministratorManual/Upgrade/Index.rst index 4350304..46a1d88 100644 --- a/Documentation/AdministratorManual/Upgrade/Index.rst +++ b/Documentation/AdministratorManual/Upgrade/Index.rst @@ -7,6 +7,34 @@ Upgrade If you upgrade EXT:weather2 to a newer version, please read this section carefully! +Update from 3.x to 4.0.0 +======================== + +Add TYPO3 11 compatibility +Remove TYPO3 9 compatibility + +We have removed all TYPO3 columns from `ext_tables.sql`. Please execute +DB compare to update the database columns. + +Execute Flush Cache in Maintenance section of TYPO3 to update dependency +injection cache. + +We require `recordStoragePid` as INT in `weather2` scheduler task. It may +happen that a call to `setPid` will fail, because it is not an INT. That +happens because all scheduler tasks including their earlier variable types +are stored serialized in scheduler. While unserializing the old type (STRING) +will not match the current type (INT) anymore. So please delete that task +and create that task again. Sorry, no UpgradeWizard available for that +operation. + +Update from 2.x to 3.0.0 +======================== + +Add TYPO3 10 compatibility +Remove TYPO3 8 compatibility + +Nothing to do. + Update from 2.x to 2.0.4 ======================== diff --git a/Documentation/Settings.cfg b/Documentation/Settings.cfg index d96b4d3..07c4801 100644 --- a/Documentation/Settings.cfg +++ b/Documentation/Settings.cfg @@ -4,7 +4,7 @@ [general] project = weather2 -version = 4.0.3 +version = 4.0.4 release = 4.0 copyright = since 2013 by jweiland.net From de4622ab96e86e20a9254556986aeb46d2514f2c Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Mon, 20 Feb 2023 10:12:38 +0100 Subject: [PATCH 10/11] Update version to 4.0.4 --- ext_emconf.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext_emconf.php b/ext_emconf.php index 747a76a..489a52e 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -8,7 +8,7 @@ 'author_email' => 'projects@jweiland.net', 'author_company' => 'jweiland.net', 'state' => 'stable', - 'version' => '4.0.3', + 'version' => '4.0.4', 'constraints' => [ 'depends' => [ 'typo3' => '10.4.29-11.5.99', From 0e01a0d90bad10c3056dfb56bfbb2653980880ca Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Mon, 20 Feb 2023 10:15:16 +0100 Subject: [PATCH 11/11] Update lines mentioned by php-cs-fixer --- Classes/Domain/Repository/CurrentWeatherRepository.php | 2 +- Classes/Domain/Repository/WeatherAlertRepository.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Classes/Domain/Repository/CurrentWeatherRepository.php b/Classes/Domain/Repository/CurrentWeatherRepository.php index c18c5db..24cf1b1 100644 --- a/Classes/Domain/Repository/CurrentWeatherRepository.php +++ b/Classes/Domain/Repository/CurrentWeatherRepository.php @@ -31,7 +31,7 @@ public function findBySelection(string $selection): ?CurrentWeather $query->equals('name', trim($selection)) ) ->setOrderings([ - 'uid' => QueryInterface::ORDER_DESCENDING + 'uid' => QueryInterface::ORDER_DESCENDING, ]); /** @var ?CurrentWeather $currentWeather */ diff --git a/Classes/Domain/Repository/WeatherAlertRepository.php b/Classes/Domain/Repository/WeatherAlertRepository.php index 8ed7fd7..86379e9 100644 --- a/Classes/Domain/Repository/WeatherAlertRepository.php +++ b/Classes/Domain/Repository/WeatherAlertRepository.php @@ -40,7 +40,7 @@ public function findByUserSelection( $equalConstraintFields = [ 'type' => GeneralUtility::trimExplode(',', $warningTypes), - 'level' => GeneralUtility::trimExplode(',', $warningLevels) + 'level' => GeneralUtility::trimExplode(',', $warningLevels), ]; $warningConstraints = ['type' => [], 'level' => []];