Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use URL parameters for filter states #10834

Merged
merged 6 commits into from
Jun 3, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions util/gh-pages/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -501,9 +501,11 @@ <h1>Clippy Lints</h1>
<div class="col-12 col-md-7 search-control">
<div class="input-group">
<label class="input-group-addon" id="filter-label" for="search-input">Filter:</label>
<input type="text" class="form-control filter-input" placeholder="Keywords or search string" id="search-input" ng-model="search" ng-model-options="{debounce: 50}"/>
<input type="text" class="form-control filter-input" placeholder="Keywords or search string" id="search-input"
ng-model="search" ng-blur="updatePath()" ng-keyup="$event.keyCode == 13 && updatePath()"
ng-model-options="{debounce: 50}" />
<span class="input-group-btn">
<button class="filter-clear btn" type="button" ng-click="search = ''">
<button class="filter-clear btn" type="button" ng-click="search = ''; updatePath();">
Clear
</button>
</span>
Expand All @@ -517,7 +519,8 @@ <h1>Clippy Lints</h1>
<h2 class="panel-title">
<div class="panel-title-name">
<span>{{lint.id}}</span>
<a href="#{{lint.id}}" class="anchor label label-default" ng-click="open[lint.id] = true; $event.stopPropagation()">&para;</a>
<a href="#{{lint.id}}" class="anchor label label-default"
ng-click="openLint(lint); $event.preventDefault(); $event.stopPropagation()">&para;</a>
<a href="" id="clipboard-{{lint.id}}" class="anchor label label-default" ng-click="copyToClipboard(lint); $event.stopPropagation()">
&#128203;
</a>
Expand Down
206 changes: 183 additions & 23 deletions util/gh-pages/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
}
Expand Down Expand Up @@ -106,10 +106,10 @@
}
};
})
.controller("lintList", function ($scope, $http, $timeout) {
.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;
$scope.levels = { ...LEVEL_FILTERS_DEFAULT };
$scope.byLevels = function (lint) {
return $scope.levels[lint.level];
};
Expand Down Expand Up @@ -146,6 +146,167 @@
"=": {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])
);

// 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() {
// 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, defaultValues) => {
if (urlParameters[parameter]) {
const items = urlParameters[parameter].split(',');
for (const key in scopeVariable) {
if (scopeVariable.hasOwnProperty(key)) {
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, LEVEL_FILTERS_DEFAULT);
handleParameter('groups', $scope.groups, GROUPS_FILTER_DEFAULT);

// 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);

// Watch for changes in the URL path and update the search and lint display
$scope.$watch($location.path, function (newPath) {
const searchParameter = newPath.substring(1);
if ($scope.search !== searchParameter) {
$scope.search = searchParameter;
$scope.open[searchParameter] = true;
scrollToLintByURL($scope, $location);
}
});

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($location.search, function (newParameters) {
if (!internalURLChange) {
loadFromURLParameters();
}
internalURLChange = false;
});

$scope.updatePath = function () {
if (debounceTimeout) {
$timeout.cancel(debounceTimeout);
}

$location.path($scope.search);
}

$scope.selectTheme = function (theme) {
setTheme(theme, true);
}
Expand Down Expand Up @@ -173,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) {
Expand Down Expand Up @@ -272,6 +435,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) {
Expand All @@ -296,14 +469,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) {
Expand All @@ -315,7 +487,7 @@
selectGroup($scope, selectedGroup.toLowerCase());
}

scrollToLintByURL($scope);
scrollToLintByURL($scope, $location);

setTimeout(function () {
var el = document.getElementById('filter-input');
Expand All @@ -326,18 +498,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);
});
})();

Expand Down