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..4902b849c 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, 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; 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="scopeName ?>" + getScopeDepends($scope)): ?>data-scope-depends="" +> label)) ?>: 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..f3675beed 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,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') }) }