From be87fbbb8751d6c6b81e473f381fb4c62b5e7ffe Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Sat, 5 Aug 2017 14:01:59 -0600 Subject: [PATCH] added feature to allow filtering over a number range (#2856) Original by @purposebuiltscott in #2856 --- modules/backend/widgets/Filter.php | 76 +++++ modules/system/assets/ui/js/filter.numbers.js | 308 ++++++++++++++++++ modules/system/assets/ui/less/filter.less | 63 +++- modules/system/assets/ui/storm.js | 1 + modules/system/lang/en/client.php | 8 + 5 files changed, 455 insertions(+), 1 deletion(-) create mode 100644 modules/system/assets/ui/js/filter.numbers.js diff --git a/modules/backend/widgets/Filter.php b/modules/backend/widgets/Filter.php index 8d5f712c4..21b3f457d 100644 --- a/modules/backend/widgets/Filter.php +++ b/modules/backend/widgets/Filter.php @@ -136,6 +136,33 @@ class Filter extends WidgetBase } break; + case 'numberrange': + if ($scope->value && is_array($scope->value) && count($scope->value) === 2 && + $scope->value[0] && + $scope->value[1] + ) { + $min = $scope->value[0]; + $max = $scope->value[1]; + + if($min) { + $params['minStr'] = $min; + $params['min'] = $min; + } + else { + $params['minStr'] = ''; + $params['min'] = null; + } + + if($max) { + $params['maxStr'] = $max; + $params['max'] = $max; + } + else { + $params['maxStr'] = '∞'; + $params['max'] = null; + } + } + break } return $this->makePartial('scope_'.$scope->type, $params); @@ -202,6 +229,20 @@ class Filter extends WidgetBase $this->setScopeValue($scope, $dates); break; + + case 'numberrange': + $numbers = $this->numbersFromAjax(post('options.numbers')); + if (!empty($numbers)) { + list($min, $max) = $numbers; + + $numbers = [$min, $max]; + } + else { + $numbers = null; + } + + $this->setScopeValue($scope, $numbers); + break; } /* @@ -486,6 +527,14 @@ class Filter extends WidgetBase $scopeObj->{$property} = $value; } + /* + * Ensure number options are set + */ + if (!isset($config['minNumber'])) { + $scopeObj->minNumber = '0'; + $scopeObj->maxNumber = '999999999'; + } + $this->allScopes[$name] = $scopeObj; } } @@ -609,6 +658,33 @@ class Filter extends WidgetBase break; + case 'numberrange': + if (is_array($scope->value) && count($scope->value) > 1) { + list($min, $max) = array_values($scope->value); + + if ($min && $max) { + + /* + * Condition + * + */ + if ($scopeConditions = $scope->conditions) { + $query->whereRaw(DbDongle::parse(strtr($scopeConditions, [ + ':min' => $min, + ':max' => $max + ]))); + } + /* + * Scope + */ + elseif ($scopeMethod = $scope->scope) { + $query->$scopeMethod($min, $max); + } + } + } + + break; + default: $value = is_array($scope->value) ? array_keys($scope->value) : $scope->value; diff --git a/modules/system/assets/ui/js/filter.numbers.js b/modules/system/assets/ui/js/filter.numbers.js new file mode 100644 index 000000000..fd5d4957e --- /dev/null +++ b/modules/system/assets/ui/js/filter.numbers.js @@ -0,0 +1,308 @@ +/* + * Filter Widget + * + * Data attributes: + * - data-behavior="filter" - enables the filter plugin + * + * Dependences: + * - October Popover (october.popover.js) + * + * Notes: + * Ideally this control would not depend on loader or the AJAX framework, + * then the Filter widget can use events to handle this business logic. + * + * Require: + * - mustache/mustache + * - modernizr/modernizr + * - storm/popover + */ ++function ($) { + "use strict"; + + var FilterWidget = $.fn.filterWidget.Constructor; + + // OVERLOADED MODULE + // ================= + + var overloaded_init = FilterWidget.prototype.init; + + FilterWidget.prototype.init = function () { + overloaded_init.apply(this) + + this.initFilterNumber() + } + + + // NEW MODULE + // ================= + + FilterWidget.prototype.initFilterNumber = function () { + var self = this + + this.$el.on('show.oc.popover', 'a.filter-scope-number', function () { + self.initNumberInputs($(this).hasClass('range')) + }) + + this.$el.on('hide.oc.popover', 'a.filter-scope-number', function () { + var $scope = $(this) + self.pushOptions(self.activeScopeName) + self.activeScopeName = null + self.$activeScope = null + + // Second click closes the filter scope + setTimeout(function () { + $scope.removeClass('filter-scope-open') + }, 200) + }) + + this.$el.on('click', 'a.filter-scope-number', function () { + var $scope = $(this), + scopeName = $scope.data('scope-name') + + // Ignore if already opened + if ($scope.hasClass('filter-scope-open')) return + + // Ignore if another popover is opened + if (null !== self.activeScopeName) return + self.$activeScope = $scope + self.activeScopeName = scopeName + self.isActiveScopeDirty = false + + if ($scope.hasClass('range')) { + self.displayPopoverNumberRange($scope) + } + else { + self.displayPopoverNumber($scope) + } + + $scope.addClass('filter-scope-open') + }) + + $(document).on('click', '#controlFilterPopoverNum [data-trigger="filter"]', function (e) { + e.preventDefault() + e.stopPropagation() + self.filterByNumber() + }) + + $(document).on('click', '#controlFilterPopoverNum [data-trigger="clear"]', function (e) { + e.preventDefault() + e.stopPropagation() + + self.filterByNumber(true) + }) + } + + /* + * Get popover date template + */ + FilterWidget.prototype.getPopoverNumberTemplate = function () { + return ' \ +
\ + \ +
\ + \ +
\ +
\ + ' + } + + /* + * Get popover range template + */ + FilterWidget.prototype.getPopoverNumberRangeTemplate = function () { + return ' \ +
\ + \ +
\ + \ +
\ +
\ + ' + } + + FilterWidget.prototype.displayPopoverNumber = function ($scope) { + var self = this, + scopeName = $scope.data('scope-name'), + data = this.scopeValues[scopeName] + + data = $.extend({}, data, { + filter_button_text: this.getLang('filter.numbers.filter_button_text'), + reset_button_text: this.getLang('filter.numberss.reset_button_text'), + number_placeholder: this.getLang('filter.numberss.number_placeholder', 'Number') + }) + + data.scopeName = scopeName + + // Destroy any popovers already bound + $scope.data('oc.popover', null) + + $scope.ocPopover({ + content: Mustache.render(this.getPopoverNumberTemplate(), data), + modal: false, + highlightModalTarget: true, + closeOnPageClick: true, + placement: 'bottom', + }) + } + + FilterWidget.prototype.displayPopoverNumberRange = function ($scope) { + var self = this, + scopeName = $scope.data('scope-name'), + data = this.scopeValues[scopeName] + + data = $.extend({}, data, { + filter_button_text: this.getLang('filter.numbers.filter_button_text'), + reset_button_text: this.getLang('filter.numbers.reset_button_text'), + min_placeholder: this.getLang('filter.numbers.min_placeholder', 'Min'), + max_placeholder: this.getLang('filter.numbers.max_placeholder', 'Max') + }) + + data.scopeName = scopeName + + // Destroy any popovers already bound + $scope.data('oc.popover', null) + console.log(data); + $scope.ocPopover({ + content: Mustache.render(this.getPopoverNumberRangeTemplate(), data), + modal: false, + highlightModalTarget: true, + closeOnPageClick: true, + placement: 'bottom', + }) + } + + + FilterWidget.prototype.initNumberInputs = function (isRange) { + var self = this, + scopeData = this.$activeScope.data('scope-data'), + $inputs = $('.field-number input', '#controlFilterPopoverNum'), + data = this.scopeValues[this.activeScopeName] + + if (!data) { + data = { + numbers: isRange ? (scopeData.numbers ? scopeData.numbers : []) : (scopeData.number ? [scopeData.number] : []) + } + } + + $inputs.each(function (index, numberinput) { + var defaultValue = '' + + if (0 <= index && index < data.numbers.length) { + defaultValue = data.numbers[index] ? data.numbers[index] : '' + } + + if (!isRange) { + defaults.onSelect = function () { + self.filterByNumber() + } + } + + numberinput.value = '' !== defaultValue ? defaultValue : ''; + + }) + } + + FilterWidget.prototype.updateScopeNumberSetting = function ($scope, numbers) { + var $setting = $scope.find('.filter-setting'), + dateRegex =/\d*/, + reset = false + + if (numbers && numbers.length) { + numbers[0] = numbers[0] && numbers[0].match(dateRegex) ? numbers[0] : null + + if (numbers.length > 1) { + numbers[1] = numbers[1] && numbers[1].match(dateRegex) ? numbers[1] : null + + if(numbers[0] || numbers[1]) { + var min = numbers[0] ? numbers[0] : '', + max = numbers[1] ? numbers[1] : '∞' + + $setting.text(min + ' → ' + max) + } else { + reset = true + } + } + else if(numbers[0]) { + $setting.text(numbers[0]) + } else { + reset = true + } + } + else { + reset = true + } + + if(reset) { + $setting.text(this.getLang('filter.numbers.all', 'all')); + $scope.removeClass('active') + } else { + $scope.addClass('active') + } + } + + FilterWidget.prototype.filterByNumber = function (isReset) { + var self = this, + numbers = [] + + if (!isReset) { + var numberinputs = $('.field-number input', '#controlFilterPopoverNum') + numberinputs.each(function (index, numberinput) { + var number = $(numberinput).val() + numbers.push(number) + }) + } + + this.updateScopeNumberSetting(this.$activeScope, numbers); + this.scopeValues[this.activeScopeName] = { + numbers: numbers + } + this.isActiveScopeDirty = true; + this.$activeScope.data('oc.popover').hide() + } + + +}(window.jQuery); diff --git a/modules/system/assets/ui/less/filter.less b/modules/system/assets/ui/less/filter.less index 3c304c600..9ba4cb26b 100644 --- a/modules/system/assets/ui/less/filter.less +++ b/modules/system/assets/ui/less/filter.less @@ -132,6 +132,38 @@ &.active .filter-setting { background-color: darken(@color-filter-bg-active, 5%); } } } + + > .filter-scope-number { + display: inline-block; + padding: (@padding-standard / 2); + .filter-label {} + .filter-setting { + display: inline-block; + .transition(color 0.6s); + } + + &:after { + font-size: 14px; + .icon(@angle-down); + } + + &.active { + .filter-setting { + padding-left: 5px; + padding-right: 5px; + color: #FFF; + background-color: @color-filter-bg-active; + .border-radius(4px); + .transition(~'color 1s, background-color 1s'); + } + } + + &:hover { + color: #000; + .filter-label { color: @color-filter-text; } + &.active .filter-setting { background-color: darken(@color-filter-bg-active, 5%); } + } + } } .control-filter-popover { @@ -244,6 +276,35 @@ } } } + + &.control-filter-number-popover { + min-width: 190px; + + .filter-buttons { + margin: 0; + padding: 0; + + &:after { + content: ""; + display: block; + clear: both; + } + + .btn { + float: left; + width: 100%; + margin: 0; + border-radius: 0; + text-align: center; + } + } + + &.--range { + .filter-buttons .btn { + width: 50%; + } + } + } } @media (max-width: @screen-xs) { @@ -270,4 +331,4 @@ } } } -} \ No newline at end of file +} diff --git a/modules/system/assets/ui/storm.js b/modules/system/assets/ui/storm.js index 0f477dafb..fe2706c85 100644 --- a/modules/system/assets/ui/storm.js +++ b/modules/system/assets/ui/storm.js @@ -39,6 +39,7 @@ =require js/toolbar.js =require js/filter.js =require js/filter.dates.js +=require js/filter.numbers.js =require js/select.js =require js/loader.base.js =require js/loader.cursor.js diff --git a/modules/system/lang/en/client.php b/modules/system/lang/en/client.php index 8b7dd8a4f..9346e4a86 100644 --- a/modules/system/lang/en/client.php +++ b/modules/system/lang/en/client.php @@ -70,7 +70,15 @@ return [ 'date_placeholder' => 'Date', 'after_placeholder' => 'After', 'before_placeholder' => 'Before' + ], + 'numbers' => [ + 'all' => 'all', + 'filter_button_text' => 'Filter', + 'reset_button_text' => 'Reset', + 'min_placeholder' => 'Min', + 'max_placeholder' => 'Max' ] + ], 'eventlog' => [ 'show_stacktrace' => 'Show the stacktrace',