From 70107c63762e56b027a68daa3dd1a8b1d0e37fdd Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Tue, 12 Nov 2019 17:02:25 -0600 Subject: [PATCH] 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. --- modules/backend/classes/FilterScope.php | 54 ++++++------ modules/backend/widgets/Filter.php | 37 ++++++++- .../widgets/filter/partials/_scope_group.htm | 4 +- modules/system/assets/ui/js/filter.js | 82 +++++++++++++++++-- 4 files changed, 144 insertions(+), 33 deletions(-) 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') }) }