added feature to allow filtering over a number range (#2856)
Original by @purposebuiltscott in #2856
This commit is contained in:
parent
92d88fc4dc
commit
be87fbbb87
|
|
@ -136,6 +136,33 @@ class Filter extends WidgetBase
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
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);
|
return $this->makePartial('scope_'.$scope->type, $params);
|
||||||
|
|
@ -202,6 +229,20 @@ class Filter extends WidgetBase
|
||||||
|
|
||||||
$this->setScopeValue($scope, $dates);
|
$this->setScopeValue($scope, $dates);
|
||||||
break;
|
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;
|
$scopeObj->{$property} = $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Ensure number options are set
|
||||||
|
*/
|
||||||
|
if (!isset($config['minNumber'])) {
|
||||||
|
$scopeObj->minNumber = '0';
|
||||||
|
$scopeObj->maxNumber = '999999999';
|
||||||
|
}
|
||||||
|
|
||||||
$this->allScopes[$name] = $scopeObj;
|
$this->allScopes[$name] = $scopeObj;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -609,6 +658,33 @@ class Filter extends WidgetBase
|
||||||
|
|
||||||
break;
|
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:
|
default:
|
||||||
$value = is_array($scope->value) ? array_keys($scope->value) : $scope->value;
|
$value = is_array($scope->value) ? array_keys($scope->value) : $scope->value;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 ' \
|
||||||
|
<form> \
|
||||||
|
<input type="hidden" name="scopeName" value="{{ scopeName }}" /> \
|
||||||
|
<div id="controlFilterPopoverNum" class="control-filter-popover control-filter-number-popover"> \
|
||||||
|
<div class="filter-search loading-indicator-container size-input-text"> \
|
||||||
|
<input \
|
||||||
|
type="text" \
|
||||||
|
name="number" \
|
||||||
|
value="{{ number }}" \
|
||||||
|
class="form-control align-right" \
|
||||||
|
autocomplete="off" \
|
||||||
|
placeholder="{{ number_placeholder }}" /> \
|
||||||
|
<div class="filter-buttons"> \
|
||||||
|
<button class="btn btn-block btn-secondary" data-trigger="clear"> \
|
||||||
|
{{ reset_button_text }} \
|
||||||
|
</button> \
|
||||||
|
</div> \
|
||||||
|
</div> \
|
||||||
|
</div> \
|
||||||
|
</form> \
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Get popover range template
|
||||||
|
*/
|
||||||
|
FilterWidget.prototype.getPopoverNumberRangeTemplate = function () {
|
||||||
|
return ' \
|
||||||
|
<form> \
|
||||||
|
<input type="hidden" name="scopeName" value="{{ scopeName }}" /> \
|
||||||
|
<div id="controlFilterPopoverNum" class="control-filter-popover control-filter-number-popover --range"> \
|
||||||
|
<div class="filter-search loading-indicator-container size-input-text"> \
|
||||||
|
<div class="field-number"> \
|
||||||
|
<div class="right-align"> \
|
||||||
|
<input \
|
||||||
|
type="text" \
|
||||||
|
name="number" \
|
||||||
|
value="{{ number }}" \
|
||||||
|
class="form-control align-right" \
|
||||||
|
autocomplete="off" \
|
||||||
|
placeholder="{{ min_placeholder }}" /> \
|
||||||
|
</div> \
|
||||||
|
</div> \
|
||||||
|
<div class="field-number"> \
|
||||||
|
<div class="right-align"> \
|
||||||
|
<input \
|
||||||
|
type="text" \
|
||||||
|
name="number" \
|
||||||
|
value="{{ number }}" \
|
||||||
|
class="form-control align-right" \
|
||||||
|
autocomplete="off" \
|
||||||
|
placeholder="{{ max_placeholder }}" /> \
|
||||||
|
</div> \
|
||||||
|
</div> \
|
||||||
|
<div class="filter-buttons"> \
|
||||||
|
<button class="btn btn-block btn-primary" data-trigger="filter"> \
|
||||||
|
{{ filter_button_text }} \
|
||||||
|
</button> \
|
||||||
|
<button class="btn btn-block btn-secondary" data-trigger="clear"> \
|
||||||
|
{{ reset_button_text }} \
|
||||||
|
</button> \
|
||||||
|
</div> \
|
||||||
|
</div> \
|
||||||
|
</div> \
|
||||||
|
</form> \
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
@ -132,6 +132,38 @@
|
||||||
&.active .filter-setting { background-color: darken(@color-filter-bg-active, 5%); }
|
&.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 {
|
.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) {
|
@media (max-width: @screen-xs) {
|
||||||
|
|
@ -270,4 +331,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@
|
||||||
=require js/toolbar.js
|
=require js/toolbar.js
|
||||||
=require js/filter.js
|
=require js/filter.js
|
||||||
=require js/filter.dates.js
|
=require js/filter.dates.js
|
||||||
|
=require js/filter.numbers.js
|
||||||
=require js/select.js
|
=require js/select.js
|
||||||
=require js/loader.base.js
|
=require js/loader.base.js
|
||||||
=require js/loader.cursor.js
|
=require js/loader.cursor.js
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,15 @@ return [
|
||||||
'date_placeholder' => 'Date',
|
'date_placeholder' => 'Date',
|
||||||
'after_placeholder' => 'After',
|
'after_placeholder' => 'After',
|
||||||
'before_placeholder' => 'Before'
|
'before_placeholder' => 'Before'
|
||||||
|
],
|
||||||
|
'numbers' => [
|
||||||
|
'all' => 'all',
|
||||||
|
'filter_button_text' => 'Filter',
|
||||||
|
'reset_button_text' => 'Reset',
|
||||||
|
'min_placeholder' => 'Min',
|
||||||
|
'max_placeholder' => 'Max'
|
||||||
]
|
]
|
||||||
|
|
||||||
],
|
],
|
||||||
'eventlog' => [
|
'eventlog' => [
|
||||||
'show_stacktrace' => 'Show the stacktrace',
|
'show_stacktrace' => 'Show the stacktrace',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue