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

name & description text search filters #4767

Open
wants to merge 2 commits into
base: gh-pages
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
8 changes: 8 additions & 0 deletions _includes/scripts.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@
<% }) %>
</select>
</div>
<div class="filters-panel cf text-input-container name-search-container">
<label class="filter-label" for="nameSearch">Search in name:</label>
<input type="text" class="name-search" id="nameSearch" value="<%nameSearch%>" autocomplete="off" />
</div>
<div class="filters-panel cf text-input-container">
<label class="filter-label" for="descSearch">Search in description:</label>
<input type="text" class="desc-search" id="descSearch" value="<%descSearch%>" autocomplete="off" />
</div>
<div class="filters-panel cf">
<label class="filter-label" for="label">Filter by label: </label>
<select class="labels-filter" id="label" multiple data-placeholder="Select a label..." >
Expand Down
89 changes: 86 additions & 3 deletions javascripts/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,28 @@ define([
return `about ${Math.round(elapsed / msPerYear)} years ago`;
}

const renderProjects = function (projectService, tags, names, labels, date) {
// TODO: refactor arguments to an options object?
const renderProjects = function (
projectService,
tags,
names,
labels,
date,
nameSearch,
descSearch
) {
const allTags = projectService.getTags();

projectsPanel.html(
compiledtemplateFn({
projects: projectService.get(tags, names, labels, date),
projects: projectService.get(
tags,
names,
labels,
date,
nameSearch,
descSearch
),
relativeTime,
tags: allTags,
popularTags: projectService.getPopularTags(6),
Expand All @@ -78,6 +94,8 @@ define([
selectedNames: names,
labels: projectService.getLabels(),
selectedLabels: labels,
nameSearch: nameSearch,
descSearch: descSearch,
})
);
date = date || 'invalid';
Expand Down Expand Up @@ -116,6 +134,61 @@ define([
encodeURIComponent($(this).val() || '')
);
});

// Hide search container if any names are directly chosen
projectsPanel
.find('.name-search-container')
.css('display', names?.length ? 'none' : 'block');

let searchDebounce = null;
// local function to safely debounce name/description search inputs
function debounceNameDescSearch(elementId, delayMs = 1000) {
clearTimeout(searchDebounce);
searchDebounce = setTimeout(() => {
let newUrl = updateQueryStringParameter(
getFilterUrl(),
'name-search',
encodeURIComponent(
projectsPanel.find('input.name-search').val() || ''
)
);
newUrl = updateQueryStringParameter(
newUrl,
'desc-search',
encodeURIComponent(
projectsPanel.find('input.desc-search').val() || ''
)
);
location.href = updateQueryStringParameter(newUrl, 'focus', elementId);
}, delayMs);
}

projectsPanel
.find('input.name-search')
.val(nameSearch)
.on('change', function () {
debounceNameDescSearch('nameSearch', 0);
})
.on('input', function () {
debounceNameDescSearch('nameSearch');
});

projectsPanel
.find('input.desc-search')
.val(descSearch)
.on('change', function () {
debounceNameDescSearch('descSearch', 0);
})
.on('input', function () {
debounceNameDescSearch('descSearch');
});

// Logic for focusing input fields
const focus = getParameterByName('focus');
if (focus) {
projectsPanel.find(`input#${focus}`).focus();
}

// Logic for checking/unchecking date-buttons
projectsPanel.find('button.radio-btn').each(function () {
$(this).click(function () {
Expand Down Expand Up @@ -336,7 +409,17 @@ define([
const names = prepareForHTML(getParameterByName('names'));
const tags = prepareForHTML(getParameterByName('tags'));
const date = getParameterByName('date');
renderProjects(projectsSvc, tags, names, labels, date);
const nameSearch = getParameterByName('name-search');
const descSearch = getParameterByName('desc-search');
renderProjects(
projectsSvc,
tags,
names,
labels,
date,
nameSearch,
descSearch
);
});

this.get('/', () => {
Expand Down
54 changes: 53 additions & 1 deletion javascripts/projectsService.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,49 @@ define(['underscore', 'tag-builder', 'project-ordering'], (
);
};

/*
* The function here is used for front end filtering when given
* texts to match in project name or description.
* Then it fallsback to show all the projects.
* @param Array projects : An array having all the Projects in _data
* @param Array projectsNameSorted : This is another array showing all the
* projects in a sorted order
* @param string nameSearch : text to find in project name
* @param string descSearch : text to find in project description
*/
const applySearchFilter = function (
projects,
projectNamesSorted,
nameSearch,
descSearch
) {
nameSearch = (nameSearch || '').trim().toLowerCase();
descSearch = (descSearch || '').trim().toLowerCase();

if (!nameSearch && !descSearch) {
return projects;
}

return _.filter(
_.map(projectNamesSorted, (entry) => {
// search in name and in html-escaped description
if (
(!nameSearch || entry.name.toLowerCase().includes(nameSearch)) &&
(!descSearch ||
entry.desc
.replace(/<[^>]*>/g, '')
.toLowerCase()
.includes(descSearch))
) {
return entry;
}

return undefined;
}),
(entry) => entry || false
);
};

/*
* The function here is used for front end filtering when given
* selecting certain projects. It ensures that only the selected projects
Expand Down Expand Up @@ -237,7 +280,8 @@ define(['underscore', 'tag-builder', 'project-ordering'], (
labelsMap[project.upforgrabs.name.toLowerCase()] = project.upforgrabs;
});

this.get = function (tags, names, labels, date) {
// TODO: refactor arguments to an options object?
this.get = function (tags, names, labels, date, nameSearch, descSearch) {
let filteredProjects = projects;
if (names && names.length) {
filteredProjects = applyNamesFilter(
Expand All @@ -246,6 +290,14 @@ define(['underscore', 'tag-builder', 'project-ordering'], (
names
);
}
if (nameSearch || descSearch) {
filteredProjects = applySearchFilter(
filteredProjects,
this.getNames(),
names?.length ? null : nameSearch,
descSearch
);
}
if (date) {
filteredProjects = applyDateFilter(filteredProjects, date);
}
Expand Down
44 changes: 44 additions & 0 deletions stylesheets/stylesheet.css
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,50 @@ ul.popular-tags li {
color: var(--body-color);
}

.text-input-container {
width: 95%;
position: relative;
display: inline-block;
vertical-align: middle;
font-size: 13px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}

.text-input-container input[type="text"] {
box-sizing: border-box;
position: relative;
overflow: hidden;
margin: 0;
padding: 0 5px;
width: 100%;
height: auto;
border: 1px solid #aaa;
background-color: #fff;
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(1%, #eee), color-stop(15%, #fff));
background-image: linear-gradient(#eee 1%, #fff 15%);
cursor: text;
margin: 1px 0;
height: 29px;
outline: 0;
-webkit-box-shadow: none;
box-shadow: none;
color: #222;
font-size: 100%;
font-family: sans-serif;
line-height: normal;
border-radius: 0;
}

.text-input-container input[type="text"]:active,
.text-input-container input[type="text"]:focus {
border: 1px solid #5897fb;
-webkit-box-shadow: 0 0 5px rgba(0, 0, 0, .3);
box-shadow: 0 0 5px rgba(0, 0, 0, .3);
}

@media (max-width: 768px) {
body {
padding: 0 0;
Expand Down
70 changes: 68 additions & 2 deletions tests/spec/ProjectsServiceSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,65 @@ describe('ProjectsService', () => {
const projects = projectsService.get(['oops']);
expect(projects).toHaveLength(0);
});

it('returns match when providing name search term', () => {
let projects = projectsService.get(null, null, null, null, 'Glimpse');
expect(projects).toHaveLength(1);
expect(projects[0].name).toBe('Glimpse');

projects = projectsService.get(null, null, null, null, 'Sha');
expect(projects).toHaveLength(1);
expect(projects[0].name).toBe('LibGit2Sharp');
});

it('trimmed and case-insensitive name search term', () => {
const projects = projectsService.get(
null,
null,
null,
null,
' glimpse '
);
expect(projects).toHaveLength(1);
expect(projects[0].name).toBe('Glimpse');
});

it('returns match when providing description search term', () => {
let projects = projectsService.get(
null,
null,
null,
null,
null,
'web debugging'
);
expect(projects).toHaveLength(1);
expect(projects[0].name).toBe('Glimpse');

projects = projectsService.get(
null,
null,
null,
null,
null,
'might and speed'
);
expect(projects).toHaveLength(1);
expect(projects[0].name).toBe('LibGit2Sharp');
});

it('trimmed and case-insensitive description search term', () => {
const projects = projectsService.get(
null,
null,
null,
null,
null,
' asp.net '
);
expect(projects).toHaveLength(1);
expect(projects[0].name).toBe('Glimpse');
});
});
});

Expand Down Expand Up @@ -169,8 +228,15 @@ describe('ProjectsService', () => {
expect(projects.length).toBe(1);
});

it('Should take all three filters and return a project', () => {
const projects = projectsService.get(['c#'], ['1'], ['1']);
it('Should take all filters and return a project', () => {
const projects = projectsService.get(
['c#'],
['1'],
['1'],
null,
'Sharp',
'might and speed'
);
expect(projects.length).toBe(1);
});
});
Expand Down