Merge pull request #4764 from octobercms/wip/fix-4254
Implemented dependsOn support for filter scopes. Fixes #4254. Documented by https://github.com/octobercms/docs/pull/419.
This commit is contained in:
commit
46fe07c2da
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
<a
|
||||
class="filter-scope <?= $scope->value ? 'active' : '' ?>"
|
||||
href="javascript:;"
|
||||
data-scope-name="<?= $scope->scopeName ?>">
|
||||
data-scope-name="<?= $scope->scopeName ?>"
|
||||
<?php if ($depends = $this->getScopeDepends($scope)): ?>data-scope-depends="<?= $depends ?>"<?php endif ?>
|
||||
>
|
||||
<span class="filter-label"><?= e(trans($scope->label)) ?>:</span>
|
||||
<span class="filter-setting"><?= $scope->value ? count($scope->value) : e(trans('backend::lang.filter.all')) ?></span>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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'
|
|||
</form> \
|
||||
'}
|
||||
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: <strong>%y</strong>",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)
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Reference in New Issue