Initial WIP on implementing dependsOn support for filter scopes.

Still need to resolve an issue where if the slave filter has values set when the master filter updates, thus triggering a change of the available options to the slave, the original values are still set on the slave but not actually visible in the popup as options because they're no longer valid options. To fix this we'll need the ability to get the browser to refresh the slave filter's selected values (count icon basically since it already forces the options popup to refresh) when its masters update; while at the same rechecking the slave's scope values set on the server to ensure that they're all valid and there aren't values left over from the previous request that are no longer valid but are still being applied to the query.
This commit is contained in:
Luke Towers 2019-11-12 17:02:25 -06:00
parent 85fadbfef3
commit 70107c6376
4 changed files with 144 additions and 33 deletions

View File

@ -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;
}

View File

@ -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, array_keys($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 = [];
@ -644,6 +671,12 @@ class Filter extends WidgetBase
/*
* Set scope value
*/
if ($scope->type === 'group') {
}
$scope->value = $this->getScopeValue($scope, @$config['default']);
return $scope;

View File

@ -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>

View File

@ -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,62 @@
})
}
/*
* 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
})
}, this.dependantUpdateInterval)
}
FilterWidget.prototype.focusSearch = function() {
if (Modernizr.touchevents)
return
@ -369,7 +436,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 +450,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')
})
}