From 5be2b27ed6a6b347342e11a46b74e104aac36cd4 Mon Sep 17 00:00:00 2001 From: Brian Hetro Date: Thu, 25 May 2023 21:05:47 -0400 Subject: [PATCH 1/6] Use URL parameters for filter states This retains the settings during browser navigation and allows sharing links with additional configuration. --- util/gh-pages/index.html | 3 +- util/gh-pages/script.js | 174 +++++++++++++++++++++++++++++++++------ 2 files changed, 153 insertions(+), 24 deletions(-) diff --git a/util/gh-pages/index.html b/util/gh-pages/index.html index 8791debad723..1b4677a3c0e8 100644 --- a/util/gh-pages/index.html +++ b/util/gh-pages/index.html @@ -517,7 +517,8 @@

Clippy Lints

{{lint.id}} - + 📋 diff --git a/util/gh-pages/script.js b/util/gh-pages/script.js index 1c16ecd6b0b1..3aaf455e12fe 100644 --- a/util/gh-pages/script.js +++ b/util/gh-pages/script.js @@ -24,9 +24,9 @@ target.scrollIntoView(); } - function scrollToLintByURL($scope) { - var removeListener = $scope.$on('ngRepeatFinished', function(ngRepeatFinishedEvent) { - scrollToLint(window.location.hash.slice(1)); + function scrollToLintByURL($scope, $location) { + var removeListener = $scope.$on('ngRepeatFinished', function (ngRepeatFinishedEvent) { + scrollToLint($location.path().substring(1)); removeListener(); }); } @@ -106,10 +106,10 @@ } }; }) - .controller("lintList", function ($scope, $http, $timeout) { + .controller("lintList", function ($scope, $http, $location) { // Level filter var LEVEL_FILTERS_DEFAULT = {allow: true, warn: true, deny: true, none: true}; - $scope.levels = LEVEL_FILTERS_DEFAULT; + $scope.levels = { ...LEVEL_FILTERS_DEFAULT }; $scope.byLevels = function (lint) { return $scope.levels[lint.level]; }; @@ -146,6 +146,137 @@ "=": {enabled: false, minorVersion: null }, }; + // Map the versionFilters to the query parameters in a way that is easier to work with in a URL + const versionFilterKeyMap = { + "≥": "gte", + "≤": "lte", + "=": "eq" + }; + const reverseVersionFilterKeyMap = Object.fromEntries( + Object.entries(versionFilterKeyMap).map(([key, value]) => [value, key]) + ); + + // loadFromURLParameters retrieves filter settings from the URL parameters and assigns them + // to corresponding $scope variables. + function loadFromURLParameters() { + // Extract parameters from URL + const urlParameters = $location.search(); + + // Define a helper function that assigns URL parameters to a provided scope variable + const handleParameter = (parameter, scopeVariable) => { + if (urlParameters[parameter]) { + const items = urlParameters[parameter].split(','); + for (const key in scopeVariable) { + if (scopeVariable.hasOwnProperty(key)) { + scopeVariable[key] = items.includes(key); + } + } + } + }; + + handleParameter('levels', $scope.levels); + handleParameter('groups', $scope.groups); + + // Handle 'versions' parameter separately because it needs additional processing + if (urlParameters.versions) { + const versionFilters = urlParameters.versions.split(','); + for (const versionFilter of versionFilters) { + const [key, minorVersion] = versionFilter.split(':'); + const parsedMinorVersion = parseInt(minorVersion); + + // Map the key from the URL parameter to its original form + const originalKey = reverseVersionFilterKeyMap[key]; + + if (originalKey in $scope.versionFilters && !isNaN(parsedMinorVersion)) { + $scope.versionFilters[originalKey].enabled = true; + $scope.versionFilters[originalKey].minorVersion = parsedMinorVersion; + } + } + } + + // Load the search parameter from the URL path + const searchParameter = $location.path().substring(1); // Remove the leading slash + if (searchParameter) { + $scope.search = searchParameter; + $scope.open[searchParameter] = true; + scrollToLintByURL($scope, $location); + } + + // If there are any filters in the URL, mark that the filters have been changed + if (urlParameters.levels || urlParameters.groups || urlParameters.versions) { + $scope.filtersChanged = true; + } + } + + // updateURLParameter updates the URL parameter with the given key to the given value + function updateURLParameter(filterObj, urlKey, processFilter = filter => filter) { + const parameter = Object.keys(filterObj) + .filter(filter => filterObj[filter]) + .map(processFilter) + .filter(Boolean) // Filters out any falsy values, including null + .join(','); + + $location.search(urlKey, parameter || null); + } + + // updateVersionURLParameter updates the version URL parameter with the given version filters + function updateVersionURLParameter(versionFilters) { + updateURLParameter( + versionFilters, + 'versions', + versionFilter => versionFilters[versionFilter].enabled && versionFilters[versionFilter].minorVersion != null + ? `${versionFilterKeyMap[versionFilter]}:${versionFilters[versionFilter].minorVersion}` + : null + ); + } + + // updateAllURLParameters updates all the URL parameters with the current filter settings + function updateAllURLParameters() { + updateURLParameter($scope.levels, 'levels'); + updateURLParameter($scope.groups, 'groups'); + updateVersionURLParameter($scope.versionFilters); + } + + // Add $watches to automatically update URL parameters when the data changes + $scope.$watch('levels', function (newVal, oldVal) { + if (newVal !== oldVal) { + $scope.filtersChanged = true; + updateURLParameter(newVal, 'levels'); + } + }, true); + + $scope.$watch('groups', function (newVal, oldVal) { + if (newVal !== oldVal) { + $scope.filtersChanged = true; + updateURLParameter(newVal, 'groups'); + } + }, true); + + $scope.$watch('versionFilters', function (newVal, oldVal) { + if (newVal !== oldVal) { + $scope.filtersChanged = true; + updateVersionURLParameter(newVal); + } + }, true); + + $scope.$watch('search', function (newVal, oldVal) { + if (newVal !== oldVal) { + $location.path(newVal); + } + }); + + // Watch for changes in the URL path and update the search and lint display + $scope.$watch(function () { + return $location.path(); + }, function (newPath) { + const searchParameter = newPath.substring(1); + if ($scope.search !== searchParameter) { + $scope.search = searchParameter; + $scope.open[searchParameter] = true; + scrollToLintByURL($scope, $location); + } + }); + $scope.selectTheme = function (theme) { setTheme(theme, true); } @@ -272,6 +403,16 @@ return true; } + // Show details for one lint + $scope.openLint = function (lint) { + $scope.open[lint.id] = true; + $location.path(lint.id); + if ($scope.filtersChanged) { + updateAllURLParameters(); + $scope.filtersChanged = false; + } + }; + $scope.copyToClipboard = function (lint) { const clipboard = document.getElementById("clipboard-" + lint.id); if (clipboard) { @@ -296,14 +437,13 @@ // Get data $scope.open = {}; $scope.loading = true; + $scope.filtersChanged = false; + // This will be used to jump into the source code of the version that this documentation is for. $scope.docVersion = window.location.pathname.split('/')[2] || "master"; - if (window.location.hash.length > 1) { - $scope.search = window.location.hash.slice(1); - $scope.open[window.location.hash.slice(1)] = true; - scrollToLintByURL($scope); - } + // Set up the filters from the URL parameters before we start loading the data + loadFromURLParameters(); $http.get('./lints.json') .success(function (data) { @@ -315,7 +455,7 @@ selectGroup($scope, selectedGroup.toLowerCase()); } - scrollToLintByURL($scope); + scrollToLintByURL($scope, $location); setTimeout(function () { var el = document.getElementById('filter-input'); @@ -326,18 +466,6 @@ $scope.error = data; $scope.loading = false; }); - - window.addEventListener('hashchange', function () { - // trigger re-render - $timeout(function () { - $scope.levels = LEVEL_FILTERS_DEFAULT; - $scope.search = window.location.hash.slice(1); - $scope.open[window.location.hash.slice(1)] = true; - - scrollToLintByURL($scope); - }); - return true; - }, false); }); })(); From 7c1bca4be8974e3e1ff694fec06747aa53fdf337 Mon Sep 17 00:00:00 2001 From: Brian Hetro Date: Fri, 26 May 2023 20:24:29 -0400 Subject: [PATCH 2/6] Handle back/forward when using URL parameters for filter states --- util/gh-pages/script.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/util/gh-pages/script.js b/util/gh-pages/script.js index 3aaf455e12fe..868b63a27dff 100644 --- a/util/gh-pages/script.js +++ b/util/gh-pages/script.js @@ -163,7 +163,7 @@ const urlParameters = $location.search(); // Define a helper function that assigns URL parameters to a provided scope variable - const handleParameter = (parameter, scopeVariable) => { + const handleParameter = (parameter, scopeVariable, defaultValues) => { if (urlParameters[parameter]) { const items = urlParameters[parameter].split(','); for (const key in scopeVariable) { @@ -171,11 +171,17 @@ scopeVariable[key] = items.includes(key); } } + } else if (defaultValues) { + for (const key in defaultValues) { + if (scopeVariable.hasOwnProperty(key)) { + scopeVariable[key] = defaultValues[key]; + } + } } }; - handleParameter('levels', $scope.levels); - handleParameter('groups', $scope.groups); + handleParameter('levels', $scope.levels, LEVEL_FILTERS_DEFAULT); + handleParameter('groups', $scope.groups, GROUPS_FILTER_DEFAULT); // Handle 'versions' parameter separately because it needs additional processing if (urlParameters.versions) { @@ -277,6 +283,12 @@ } }); + $scope.$watch(function () { + return $location.search(); + }, function (newParameters) { + loadFromURLParameters(); + }); + $scope.selectTheme = function (theme) { setTheme(theme, true); } From a865d8432c89fa442e046e22c6439f2483ff9d4c Mon Sep 17 00:00:00 2001 From: Brian Hetro Date: Sat, 27 May 2023 13:50:56 -0400 Subject: [PATCH 3/6] Clippy Lints page - Delay updating of the URL in response to search input Update on blur, enter keypress, and a debounced delay of 1000 ms. This keeps the URL updated, but not distractingly so. --- util/gh-pages/index.html | 6 ++++-- util/gh-pages/script.js | 29 ++++++++++++++++++++++------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/util/gh-pages/index.html b/util/gh-pages/index.html index 1b4677a3c0e8..99e211654d19 100644 --- a/util/gh-pages/index.html +++ b/util/gh-pages/index.html @@ -501,9 +501,11 @@

Clippy Lints

- + - diff --git a/util/gh-pages/script.js b/util/gh-pages/script.js index 868b63a27dff..fd32225dabc3 100644 --- a/util/gh-pages/script.js +++ b/util/gh-pages/script.js @@ -106,7 +106,7 @@ } }; }) - .controller("lintList", function ($scope, $http, $location) { + .controller("lintList", function ($scope, $http, $location, $timeout) { // Level filter var LEVEL_FILTERS_DEFAULT = {allow: true, warn: true, deny: true, none: true}; $scope.levels = { ...LEVEL_FILTERS_DEFAULT }; @@ -265,12 +265,6 @@ } }, true); - $scope.$watch('search', function (newVal, oldVal) { - if (newVal !== oldVal) { - $location.path(newVal); - } - }); - // Watch for changes in the URL path and update the search and lint display $scope.$watch(function () { return $location.path(); @@ -283,12 +277,33 @@ } }); + let debounceTimeout; + $scope.$watch('search', function (newVal, oldVal) { + if (newVal !== oldVal) { + if (debounceTimeout) { + $timeout.cancel(debounceTimeout); + } + + debounceTimeout = $timeout(function () { + $location.path(newVal); + }, 1000); + } + }); + $scope.$watch(function () { return $location.search(); }, function (newParameters) { loadFromURLParameters(); }); + $scope.updatePath = function () { + if (debounceTimeout) { + $timeout.cancel(debounceTimeout); + } + + $location.path($scope.search); + } + $scope.selectTheme = function (theme) { setTheme(theme, true); } From f2e6a99a08c86821da6219e3662c868dc1665266 Mon Sep 17 00:00:00 2001 From: Brian Hetro Date: Sat, 27 May 2023 18:40:20 -0400 Subject: [PATCH 4/6] Clippy Lints page - Do not show groups in URL parameters if reset to defaults --- util/gh-pages/script.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/util/gh-pages/script.js b/util/gh-pages/script.js index fd32225dabc3..bd4293662a0a 100644 --- a/util/gh-pages/script.js +++ b/util/gh-pages/script.js @@ -156,6 +156,10 @@ Object.entries(versionFilterKeyMap).map(([key, value]) => [value, key]) ); + // An internal URL change occurs when we are modifying the URL parameters in a way + // that should not reload parameters from the URL + let internalURLChange = false; + // loadFromURLParameters retrieves filter settings from the URL parameters and assigns them // to corresponding $scope variables. function loadFromURLParameters() { @@ -266,9 +270,7 @@ }, true); // Watch for changes in the URL path and update the search and lint display - $scope.$watch(function () { - return $location.path(); - }, function (newPath) { + $scope.$watch($location.path, function (newPath) { const searchParameter = newPath.substring(1); if ($scope.search !== searchParameter) { $scope.search = searchParameter; @@ -290,10 +292,11 @@ } }); - $scope.$watch(function () { - return $location.search(); - }, function (newParameters) { - loadFromURLParameters(); + $scope.$watch($location.search, function (newParameters) { + if (!internalURLChange) { + loadFromURLParameters(); + } + internalURLChange = false; }); $scope.updatePath = function () { @@ -331,6 +334,8 @@ for (const [key, value] of Object.entries(GROUPS_FILTER_DEFAULT)) { groups[key] = value; } + internalURLChange = true; + $location.search('groups', null); }; $scope.selectedValuesCount = function (obj) { From 2e4ef8e72c5e662cc9fe536ff019ab4d5802db84 Mon Sep 17 00:00:00 2001 From: Brian Hetro Date: Fri, 2 Jun 2023 22:57:04 -0400 Subject: [PATCH 5/6] Clippy Lints page - Fix path watch triggering --- util/gh-pages/script.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/util/gh-pages/script.js b/util/gh-pages/script.js index bd4293662a0a..b961b7d2d968 100644 --- a/util/gh-pages/script.js +++ b/util/gh-pages/script.js @@ -270,7 +270,7 @@ }, true); // Watch for changes in the URL path and update the search and lint display - $scope.$watch($location.path, function (newPath) { + $scope.$watch(function () { return $location.path(); }, function (newPath) { const searchParameter = newPath.substring(1); if ($scope.search !== searchParameter) { $scope.search = searchParameter; @@ -292,12 +292,12 @@ } }); - $scope.$watch($location.search, function (newParameters) { + $scope.$watch(function () { return $location.search(); }, function (newParameters) { if (!internalURLChange) { loadFromURLParameters(); } internalURLChange = false; - }); + }, true); $scope.updatePath = function () { if (debounceTimeout) { From ac279efdbc5f4a0f1e8edaa629154db811428beb Mon Sep 17 00:00:00 2001 From: Brian Hetro Date: Fri, 2 Jun 2023 22:57:45 -0400 Subject: [PATCH 6/6] Clippy Lints page - Do not show filters in URL if configured as default values --- util/gh-pages/script.js | 58 +++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 34 deletions(-) diff --git a/util/gh-pages/script.js b/util/gh-pages/script.js index b961b7d2d968..f59245e556cd 100644 --- a/util/gh-pages/script.js +++ b/util/gh-pages/script.js @@ -156,10 +156,6 @@ Object.entries(versionFilterKeyMap).map(([key, value]) => [value, key]) ); - // An internal URL change occurs when we are modifying the URL parameters in a way - // that should not reload parameters from the URL - let internalURLChange = false; - // loadFromURLParameters retrieves filter settings from the URL parameters and assigns them // to corresponding $scope variables. function loadFromURLParameters() { @@ -211,29 +207,37 @@ $scope.open[searchParameter] = true; scrollToLintByURL($scope, $location); } - - // If there are any filters in the URL, mark that the filters have been changed - if (urlParameters.levels || urlParameters.groups || urlParameters.versions) { - $scope.filtersChanged = true; - } } // updateURLParameter updates the URL parameter with the given key to the given value - function updateURLParameter(filterObj, urlKey, processFilter = filter => filter) { + function updateURLParameter(filterObj, urlKey, defaultValue = {}, processFilter = filter => filter) { const parameter = Object.keys(filterObj) .filter(filter => filterObj[filter]) + .sort() + .map(processFilter) + .filter(Boolean) // Filters out any falsy values, including null + .join(','); + + const defaultParameter = Object.keys(defaultValue) + .filter(filter => defaultValue[filter]) + .sort() .map(processFilter) .filter(Boolean) // Filters out any falsy values, including null .join(','); - $location.search(urlKey, parameter || null); + // if we ended up back at the defaults, just remove it from the URL + if (parameter === defaultParameter) { + $location.search(urlKey, null); + } else { + $location.search(urlKey, parameter || null); + } } // updateVersionURLParameter updates the version URL parameter with the given version filters function updateVersionURLParameter(versionFilters) { updateURLParameter( versionFilters, - 'versions', + 'versions', {}, versionFilter => versionFilters[versionFilter].enabled && versionFilters[versionFilter].minorVersion != null ? `${versionFilterKeyMap[versionFilter]}:${versionFilters[versionFilter].minorVersion}` : null @@ -242,29 +246,26 @@ // updateAllURLParameters updates all the URL parameters with the current filter settings function updateAllURLParameters() { - updateURLParameter($scope.levels, 'levels'); - updateURLParameter($scope.groups, 'groups'); + updateURLParameter($scope.levels, 'levels', LEVEL_FILTERS_DEFAULT); + updateURLParameter($scope.groups, 'groups', GROUPS_FILTER_DEFAULT); updateVersionURLParameter($scope.versionFilters); } // Add $watches to automatically update URL parameters when the data changes $scope.$watch('levels', function (newVal, oldVal) { if (newVal !== oldVal) { - $scope.filtersChanged = true; - updateURLParameter(newVal, 'levels'); + updateURLParameter(newVal, 'levels', LEVEL_FILTERS_DEFAULT); } }, true); $scope.$watch('groups', function (newVal, oldVal) { if (newVal !== oldVal) { - $scope.filtersChanged = true; - updateURLParameter(newVal, 'groups'); + updateURLParameter(newVal, 'groups', GROUPS_FILTER_DEFAULT); } }, true); $scope.$watch('versionFilters', function (newVal, oldVal) { if (newVal !== oldVal) { - $scope.filtersChanged = true; updateVersionURLParameter(newVal); } }, true); @@ -293,10 +294,7 @@ }); $scope.$watch(function () { return $location.search(); }, function (newParameters) { - if (!internalURLChange) { - loadFromURLParameters(); - } - internalURLChange = false; + loadFromURLParameters(); }, true); $scope.updatePath = function () { @@ -330,12 +328,9 @@ }; $scope.resetGroupsToDefault = function () { - const groups = $scope.groups; - for (const [key, value] of Object.entries(GROUPS_FILTER_DEFAULT)) { - groups[key] = value; - } - internalURLChange = true; - $location.search('groups', null); + $scope.groups = { + ...GROUPS_FILTER_DEFAULT + }; }; $scope.selectedValuesCount = function (obj) { @@ -439,10 +434,6 @@ $scope.openLint = function (lint) { $scope.open[lint.id] = true; $location.path(lint.id); - if ($scope.filtersChanged) { - updateAllURLParameters(); - $scope.filtersChanged = false; - } }; $scope.copyToClipboard = function (lint) { @@ -469,7 +460,6 @@ // Get data $scope.open = {}; $scope.loading = true; - $scope.filtersChanged = false; // This will be used to jump into the source code of the version that this documentation is for. $scope.docVersion = window.location.pathname.split('/')[2] || "master";