diff --git a/modules/backend/classes/FilterScope.php b/modules/backend/classes/FilterScope.php
index d82cb1f81..bf5d062fb 100644
--- a/modules/backend/classes/FilterScope.php
+++ b/modules/backend/classes/FilterScope.php
@@ -51,6 +51,11 @@ class FilterScope
*/
public $options;
+ /**
+ * @var array Other scope names this scope depends on, when the other scopes are modified, this scope will update.
+ */
+ public $dependsOn;
+
/**
* @var string Specifies contextual visibility of this form scope.
*/
@@ -113,33 +118,32 @@ class FilterScope
*/
protected function evalConfig($config)
{
- if (isset($config['options'])) {
- $this->options = $config['options'];
+ if ($config === null) {
+ $config = [];
}
- if (isset($config['context'])) {
- $this->context = $config['context'];
- }
- if (isset($config['default'])) {
- $this->defaults = $config['default'];
- }
- if (isset($config['conditions'])) {
- $this->conditions = $config['conditions'];
- }
- if (isset($config['scope'])) {
- $this->scope = $config['scope'];
- }
- if (isset($config['cssClass'])) {
- $this->cssClass = $config['cssClass'];
- }
- if (isset($config['nameFrom'])) {
- $this->nameFrom = $config['nameFrom'];
- }
- if (isset($config['descriptionFrom'])) {
- $this->descriptionFrom = $config['descriptionFrom'];
- }
- if (array_key_exists('disabled', $config)) {
- $this->disabled = $config['disabled'];
+
+ /*
+ * Standard config:property values
+ */
+ $applyConfigValues = [
+ 'options',
+ 'dependsOn',
+ 'context',
+ 'default',
+ 'conditions',
+ 'scope',
+ 'cssClass',
+ 'nameFrom',
+ 'descriptionFrom',
+ 'disabled',
+ ];
+
+ foreach ($applyConfigValues as $value) {
+ if (array_key_exists($value, $config)) {
+ $this->{$value} = $config[$value];
+ }
}
+
return $config;
}
diff --git a/modules/backend/widgets/Filter.php b/modules/backend/widgets/Filter.php
index 5deb97210..738b81232 100644
--- a/modules/backend/widgets/Filter.php
+++ b/modules/backend/widgets/Filter.php
@@ -171,6 +171,22 @@ class Filter extends WidgetBase
return $this->makePartial('scope_'.$scope->type, $params);
}
+ /**
+ * Returns a HTML encoded value containing the other scopes this scope depends on
+ * @param \Backend\Classes\FilterScope $scope
+ * @return string
+ */
+ protected function getScopeDepends($scope)
+ {
+ if (!$scope->dependsOn) {
+ return '';
+ }
+
+ $dependsOn = is_array($scope->dependsOn) ? $scope->dependsOn : [$scope->dependsOn];
+ $dependsOn = htmlspecialchars(json_encode($dependsOn), ENT_QUOTES, 'UTF-8');
+ return $dependsOn;
+ }
+
//
// AJAX
//
@@ -295,7 +311,14 @@ class Filter extends WidgetBase
$scope = $this->getScope($scopeName);
$activeKeys = $scope->value ? array_keys($scope->value) : [];
$available = $this->getAvailableOptions($scope, $searchQuery);
- $active = $searchQuery ? [] : $this->filterActiveOptions($activeKeys, $available);
+
+ if ($searchQuery) {
+ $active = [];
+ } else {
+ // Ensure that only valid values are set on the current scope
+ $active = $this->filterActiveOptions($activeKeys, $available);
+ $this->setScopeValue($scope, $active);
+ }
return [
'scopeName' => $scopeName,
@@ -426,7 +449,11 @@ class Filter extends WidgetBase
]));
}
- $options = $model->$methodName();
+ if (!empty($scope->dependsOn)) {
+ $options = $model->$methodName($this->getScopes());
+ } else {
+ $options = $model->$methodName();
+ }
}
elseif (!is_array($options)) {
$options = [];
@@ -663,6 +690,15 @@ class Filter extends WidgetBase
$this->defineFilterScopes();
foreach ($this->allScopes as $scope) {
+ // Ensure that only valid values are set scopes of type: group
+ if ($scope->type === 'group') {
+ $activeKeys = $scope->value ? array_keys($scope->value) : [];
+ $available = $this->getAvailableOptions($scope);
+ $active = $this->filterActiveOptions($activeKeys, $available);
+ $value = !empty($active) ? $active : null;
+ $this->setScopeValue($scope, $value);
+ }
+
$this->applyScopeToQuery($scope, $query);
}
@@ -805,6 +841,10 @@ class Filter extends WidgetBase
default:
$value = is_array($scope->value) ? array_keys($scope->value) : $scope->value;
+ if (empty($value)) {
+ break;
+ }
+
/*
* Condition
*/
diff --git a/modules/backend/widgets/filter/partials/_scope_group.htm b/modules/backend/widgets/filter/partials/_scope_group.htm
index e5628d359..c5cc726eb 100644
--- a/modules/backend/widgets/filter/partials/_scope_group.htm
+++ b/modules/backend/widgets/filter/partials/_scope_group.htm
@@ -2,7 +2,9 @@
+ data-scope-name="= $scope->scopeName ?>"
+ getScopeDepends($scope)): ?>data-scope-depends="= $depends ?>"
+>
= e(trans($scope->label)) ?>:
= $scope->value ? count($scope->value) : e(trans('backend::lang.filter.all')) ?>
diff --git a/modules/system/assets/ui/js/filter.js b/modules/system/assets/ui/js/filter.js
index b6251fbef..543575e9a 100644
--- a/modules/system/assets/ui/js/filter.js
+++ b/modules/system/assets/ui/js/filter.js
@@ -19,7 +19,6 @@
+function ($) { "use strict";
var FilterWidget = function (element, options) {
-
this.$el = $(element);
this.options = options || {}
@@ -28,6 +27,12 @@
this.activeScopeName = null
this.isActiveScopeDirty = false
+ /*
+ * Throttle dependency updating
+ */
+ this.dependantUpdateInterval = 300
+ this.dependantUpdateTimers = {}
+
this.init()
}
@@ -89,6 +94,9 @@
FilterWidget.prototype.init = function() {
var self = this
+ this.bindDependants()
+
+ // Setup event handler on type: checkbox scopes
this.$el.on('change', '.filter-scope input[type="checkbox"]', function(){
var $scope = $(this).closest('.filter-scope')
@@ -100,12 +108,14 @@
}
})
+ // Apply classes to type: checkbox scopes that are active from the server
$('.filter-scope input[type="checkbox"]', this.$el).each(function() {
$(this)
.closest('.filter-scope')
.toggleClass('active', $(this).is(':checked'))
})
+ // Setup click handler on type: group scopes
this.$el.on('click', 'a.filter-scope', function(){
var $scope = $(this),
scopeName = $scope.data('scope-name')
@@ -120,6 +130,7 @@
$scope.addClass('filter-scope-open')
})
+ // Setup event handlers on type: group scopes' controls
this.$el.on('show.oc.popover', 'a.filter-scope', function(event){
self.focusSearch()
@@ -144,9 +155,9 @@
e.preventDefault()
self.filterScope(true)
})
-
})
+ // Setup event handler to apply selected options when closing the type: group scope popup
this.$el.on('hide.oc.popover', 'a.filter-scope', function(){
var $scope = $(this)
self.pushOptions(self.activeScopeName)
@@ -158,6 +169,81 @@
})
}
+ /*
+ * Bind dependant scopes
+ */
+ FilterWidget.prototype.bindDependants = function() {
+ if (!$('[data-scope-depends]', this.$el).length) {
+ return;
+ }
+
+ var self = this,
+ scopeMap = {},
+ scopeElements = this.$el.find('.filter-scope')
+
+ /*
+ * Map master and slave scope
+ */
+ scopeElements.filter('[data-scope-depends]').each(function() {
+ var name = $(this).data('scope-name'),
+ depends = $(this).data('scope-depends')
+
+ $.each(depends, function(index, depend){
+ if (!scopeMap[depend]) {
+ scopeMap[depend] = { scopes: [] }
+ }
+
+ scopeMap[depend].scopes.push(name)
+ })
+ })
+
+ /*
+ * When a master is updated, refresh its slaves
+ */
+ $.each(scopeMap, function(scopeName, toRefresh){
+ scopeElements.filter('[data-scope-name="'+scopeName+'"]')
+ .on('change.oc.filterScope', $.proxy(self.onRefreshDependants, self, scopeName, toRefresh))
+ })
+ }
+
+ /*
+ * Refresh a dependancy scope
+ * Uses a throttle to prevent duplicate calls and click spamming
+ */
+ FilterWidget.prototype.onRefreshDependants = function(scopeName, toRefresh) {
+ var self = this,
+ scopeElements = this.$el.find('.filter-scope')
+
+ if (this.dependantUpdateTimers[scopeName] !== undefined) {
+ window.clearTimeout(this.dependantUpdateTimers[scopeName])
+ }
+
+ this.dependantUpdateTimers[scopeName] = window.setTimeout(function() {
+ $.each(toRefresh.scopes, function (index, dependantScope) {
+ self.scopeValues[dependantScope] = null
+ var $scope = self.$el.find('[data-scope-name="'+dependantScope+'"]')
+
+ /*
+ * Request options from server
+ */
+ self.$el.request(self.options.optionsHandler, {
+ data: { scopeName: dependantScope },
+ success: function(data) {
+ self.fillOptions(dependantScope, data.options)
+ self.updateScopeSetting($scope, data.options.active.length)
+ $scope.loadIndicator('hide')
+ }
+ })
+ })
+ }, this.dependantUpdateInterval)
+
+ $.each(toRefresh.scopes, function(index, scope) {
+ scopeElements.filter('[data-scope-name="'+scope+'"]')
+ .addClass('loading-indicator-container')
+ .loadIndicator()
+ })
+ }
+
FilterWidget.prototype.focusSearch = function() {
if (Modernizr.touchevents)
return
@@ -369,7 +455,7 @@
var items = $('#controlFilterPopover .filter-active-items > ul'),
buttonContainer = $('#controlFilterPopover .filter-buttons')
- if(data) {
+ if (data) {
data.active.length > 0 ? buttonContainer.show() : buttonContainer.hide()
} else {
items.children().length > 0 ? buttonContainer.show() : buttonContainer.hide()
@@ -383,16 +469,21 @@
if (!this.isActiveScopeDirty || !this.options.updateHandler)
return
- var data = {
+ var self = this,
+ data = {
scopeName: scopeName,
options: this.scopeValues[scopeName]
}
$.oc.stripeLoadIndicator.show()
+
this.$el.request(this.options.updateHandler, {
data: data
- }).always(function(){
+ }).always(function () {
$.oc.stripeLoadIndicator.hide()
+ }).done(function () {
+ // Trigger dependsOn updates on successful requests
+ self.$el.find('[data-scope-name="'+scopeName+'"]').trigger('change.oc.filterScope')
})
}
diff --git a/modules/system/assets/ui/less/filter.less b/modules/system/assets/ui/less/filter.less
index 071ed0658..3d591af83 100644
--- a/modules/system/assets/ui/less/filter.less
+++ b/modules/system/assets/ui/less/filter.less
@@ -55,6 +55,27 @@
.transition(color 0.6s);
}
+ &.loading-indicator-container.in-progress {
+ pointer-events: none;
+ cursor: default;
+
+ .loading-indicator {
+ background: transparent;
+
+ > span {
+ left: unset;
+ right: 0;
+ top: 10px;
+ background-color: @color-filter-bg;
+ border-radius: 50%;
+ margin-top: 0;
+ width: 20px;
+ height: 20px;
+ background-size: 15px 15px;
+ }
+ }
+ }
+
&:after {
font-size: 14px;
.icon(@angle-down);
diff --git a/modules/system/assets/ui/storm-min.js b/modules/system/assets/ui/storm-min.js
index d556981b3..f3fe8efe3 100644
--- a/modules/system/assets/ui/storm-min.js
+++ b/modules/system/assets/ui/storm-min.js
@@ -3046,6 +3046,8 @@ this.scopeValues={}
this.$activeScope=null
this.activeScopeName=null
this.isActiveScopeDirty=false
+this.dependantUpdateInterval=300
+this.dependantUpdateTimers={}
this.init()}
FilterWidget.DEFAULTS={optionsHandler:null,updateHandler:null}
FilterWidget.prototype.getPopoverTemplate=function(){return' \
@@ -3093,6 +3095,7 @@ FilterWidget.prototype.getPopoverTemplate=function(){return'
\
'}
FilterWidget.prototype.init=function(){var self=this
+this.bindDependants()
this.$el.on('change','.filter-scope input[type="checkbox"]',function(){var $scope=$(this).closest('.filter-scope')
if($scope.hasClass('is-indeterminate')){self.switchToggle($(this))}
else{self.checkboxToggle($(this))}})
@@ -3117,6 +3120,20 @@ self.pushOptions(self.activeScopeName)
self.activeScopeName=null
self.$activeScope=null
setTimeout(function(){$scope.removeClass('filter-scope-open')},200)})}
+FilterWidget.prototype.bindDependants=function(){if(!$('[data-scope-depends]',this.$el).length){return;}
+var self=this,scopeMap={},scopeElements=this.$el.find('.filter-scope')
+scopeElements.filter('[data-scope-depends]').each(function(){var name=$(this).data('scope-name'),depends=$(this).data('scope-depends')
+$.each(depends,function(index,depend){if(!scopeMap[depend]){scopeMap[depend]={scopes:[]}}
+scopeMap[depend].scopes.push(name)})})
+$.each(scopeMap,function(scopeName,toRefresh){scopeElements.filter('[data-scope-name="'+scopeName+'"]').on('change.oc.filterScope',$.proxy(self.onRefreshDependants,self,scopeName,toRefresh))})}
+FilterWidget.prototype.onRefreshDependants=function(scopeName,toRefresh){var self=this,scopeElements=this.$el.find('.filter-scope')
+if(this.dependantUpdateTimers[scopeName]!==undefined){window.clearTimeout(this.dependantUpdateTimers[scopeName])}
+this.dependantUpdateTimers[scopeName]=window.setTimeout(function(){$.each(toRefresh.scopes,function(index,dependantScope){self.scopeValues[dependantScope]=null
+var $scope=self.$el.find('[data-scope-name="'+dependantScope+'"]')
+self.$el.request(self.options.optionsHandler,{data:{scopeName:dependantScope},success:function(data){self.fillOptions(dependantScope,data.options)
+self.updateScopeSetting($scope,data.options.active.length)
+$scope.loadIndicator('hide')}})})},this.dependantUpdateInterval)
+$.each(toRefresh.scopes,function(index,scope){scopeElements.filter('[data-scope-name="'+scope+'"]').addClass('loading-indicator-container').loadIndicator()})}
FilterWidget.prototype.focusSearch=function(){if(Modernizr.touchevents)
return
var $input=$('#controlFilterPopover input.filter-search-input'),length=$input.val().length
@@ -3191,9 +3208,9 @@ FilterWidget.prototype.toggleFilterButtons=function(data)
if(data){data.active.length>0?buttonContainer.show():buttonContainer.hide()}else{items.children().length>0?buttonContainer.show():buttonContainer.hide()}}
FilterWidget.prototype.pushOptions=function(scopeName){if(!this.isActiveScopeDirty||!this.options.updateHandler)
return
-var data={scopeName:scopeName,options:this.scopeValues[scopeName]}
+var self=this,data={scopeName:scopeName,options:this.scopeValues[scopeName]}
$.oc.stripeLoadIndicator.show()
-this.$el.request(this.options.updateHandler,{data:data}).always(function(){$.oc.stripeLoadIndicator.hide()})}
+this.$el.request(this.options.updateHandler,{data:data}).always(function(){$.oc.stripeLoadIndicator.hide()}).done(function(){self.$el.find('[data-scope-name="'+scopeName+'"]').trigger('change.oc.filterScope')})}
FilterWidget.prototype.checkboxToggle=function($el){var isChecked=$el.is(':checked'),$scope=$el.closest('.filter-scope'),scopeName=$scope.data('scope-name')
this.scopeValues[scopeName]=isChecked
if(this.options.updateHandler){var data={scopeName:scopeName,value:isChecked}
@@ -3902,7 +3919,7 @@ $.oc.chartUtils=new ChartUtils();}(window.jQuery);+function($){"use strict";var
this.chartOptions={xaxis:{mode:"time",tickLength:5},selection:{mode:"x"},grid:{markingsColor:"rgba(0,0,0, 0.02)",backgroundColor:{colors:["#fff","#fff"]},borderColor:"#7bafcc",borderWidth:0,color:"#ddd",hoverable:true,clickable:true,labelMargin:10},series:{lines:{show:true,fill:true},points:{show:true}},tooltip:true,tooltipOpts:{defaultTheme:false,content:"%x: %y",dateFormat:"%y-%0m-%0d",shifts:{x:10,y:20}},legend:{show:true,noColumns:2}}
this.defaultDataSetOptions={shadowSize:0}
var parsedOptions={}
-try{parsedOptions=ocJSON("{"+value+"}");}catch(e){throw new Error('Error parsing the data-chart-options attribute value. '+e);}
+try{parsedOptions=ocJSON("{"+options.chartOptions+"}");}catch(e){throw new Error('Error parsing the data-chart-options attribute value. '+e);}
this.chartOptions=$.extend({},this.chartOptions,parsedOptions)
this.options=options
this.$el=$(element)
diff --git a/modules/system/assets/ui/storm.css b/modules/system/assets/ui/storm.css
index e37b2c053..78c309784 100644
--- a/modules/system/assets/ui/storm.css
+++ b/modules/system/assets/ui/storm.css
@@ -4786,6 +4786,9 @@ ul.autocomplete.dropdown-menu.inspector-autocomplete li a {padding:5px 12px;whit
.control-filter >.filter-scope {display:inline-block;padding:10px}
.control-filter >.filter-scope .filter-label {margin-right:5px}
.control-filter >.filter-scope .filter-setting {display:inline-block;margin-right:5px;-webkit-transition:color 0.6s;transition:color 0.6s}
+.control-filter >.filter-scope.loading-indicator-container.in-progress {pointer-events:none;cursor:default}
+.control-filter >.filter-scope.loading-indicator-container.in-progress .loading-indicator {background:transparent}
+.control-filter >.filter-scope.loading-indicator-container.in-progress .loading-indicator >span {left:unset;right:0;top:10px;background-color:#ecf0f1;border-radius:50%;margin-top:0;width:20px;height:20px;background-size:15px 15px}
.control-filter >.filter-scope:after {font-size:14px;font-family:FontAwesome;font-weight:normal;font-style:normal;text-decoration:inherit;-webkit-font-smoothing:antialiased;content:"\f107"}
.control-filter >.filter-scope.active .filter-setting {padding-left:5px;padding-right:5px;color:#FFF;background-color:#6aab55;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-transition:color 1s,background-color 1s;transition:color 1s,background-color 1s}
.control-filter >.filter-scope.checkbox {padding-left:35px}