Skip to content

Commit

Permalink
Merge pull request #452 from nhsuk/feature/search-rev
Browse files Browse the repository at this point in the history
Search functionality
  • Loading branch information
chrimesdev authored Dec 12, 2019
2 parents 4fd967c + 0e3c59b commit 7d0cfd3
Show file tree
Hide file tree
Showing 14 changed files with 519 additions and 220 deletions.
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
# NHS digital service manual Changelog

## 2.0.0 - Unreleased

:new: **New features**

- Add search functionality to the service manual

:new: **New content**

- NHS service standard

:wrench: **Fixes**

- Update package dependencies to latest versions

## 1.13.4 - 09 December 2019

:wrench: **Fixes**

- Add missing Details component JavaScript polyfill
- Use https on all external links and fix broken link on the test your questions page
- Remove the unnecessary type attribute from JavaScript resources
Expand All @@ -11,6 +27,8 @@

## 1.13.3 - 25 November 2019

:wrench: **Fixes**

- Made it clear in PDF guidance that PDFs can be used for a permanent record
- Added an additional bullet point on PDFs being used as a permanent record.
- Refactor CSS and HTML to clean up the code base and make the spacing more consistent
Expand Down
58 changes: 38 additions & 20 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const locals = require('./app/locals');
const routing = require('./middleware/routing.js');
const PageIndex = require('./middleware/page-index.js');

var pageIndex = new PageIndex(config);
const pageIndex = new PageIndex(config);

// Initialise applications
const app = express();
Expand Down Expand Up @@ -65,7 +65,7 @@ env.addFilter('highlight', (code, language) => {

// Render standalone design examples
app.get('/service-manual/design-example/:example', (req, res) => {
const displayFullPage = req.query.fullpage === "true";
const displayFullPage = req.query.fullpage === 'true';
const example = req.params.example;
const examplePath = path.join(__dirname, `/app/components/${example}.njk`);

Expand All @@ -74,47 +74,67 @@ app.get('/service-manual/design-example/:example', (req, res) => {

// Wrap the example HTML in a basic html base template.
var baseTemplate = 'includes/design-example-wrapper.njk';
if(displayFullPage) {
if (displayFullPage) {
baseTemplate = 'includes/design-example-wrapper-full.njk';
}

res.render(baseTemplate, { body: exampleHtml });
});

/*
app.get('/service-manual/search', (req, res) => {
var query = req.query['search-field'] || '';
res.render('includes/search.njk', { results: pageIndex.search(query), query: query });
const query = req.query['search-field'] || '';
const resultsPerPage = 10;
let currentPage = parseInt(req.query.page, 10);
const results = pageIndex.search(query);
const maxPage = Math.ceil(results.length / resultsPerPage);
if (!Number.isInteger(currentPage)) {
currentPage = 1;
} else if (currentPage > maxPage || currentPage < 1) {
currentPage = 1;
}

const startingIndex = resultsPerPage * (currentPage - 1);
const endingIndex = startingIndex + resultsPerPage;

res.render('includes/search.njk', {
currentPage,
maxPage,
query,
results: results.slice(startingIndex, endingIndex),
resultsLen: results.length,
});
});

app.get('/service-manual/suggestions', (req, res) => {
const results = pageIndex.search(req.query.search);
const slicedResults = results.slice(0, 10);
res.set({ 'Content-Type': 'application/json' });
res.send(JSON.stringify(pageIndex.search(req.query.search)));
res.send(JSON.stringify(slicedResults));
});
*/

app.get('/', (req, res) => {
app.get('/', (_, res) => {
res.redirect('/service-manual');
});

// The practices pages have moved or been deleted
// Temporary redirects incase anyone still visits /practices pages
app.get('/service-manual/practices/create-content-for-users-with-low-health-literacy', (req, res) => {
app.get('/service-manual/practices/create-content-for-users-with-low-health-literacy', (_, res) => {
res.redirect('/service-manual/content/health-literacy');
});

app.get('/service-manual/practices/create-content-for-users-with-low-health-literacy/use-a-readability-tool-to-prioritise-content', (req, res) => {
app.get('/service-manual/practices/create-content-for-users-with-low-health-literacy/use-a-readability-tool-to-prioritise-content', (_, res) => {
res.redirect('/service-manual/content/health-literacy/use-a-readability-tool-to-prioritise-content');
});

app.get('/service-manual/practices', (req, res) => {
app.get('/service-manual/practices', (_, res) => {
res.redirect('/service-manual');
});

app.get('/service-manual/practices/make-your-service-accessible', (req, res) => {
app.get('/service-manual/practices/make-your-service-accessible', (_, res) => {
res.redirect('/service-manual/accessibility');
});

app.get('/service-manual/content/writing-for-accessibility', (req, res) => {
app.get('/service-manual/content/writing-for-accessibility', (_, res) => {
res.redirect('/service-manual/accessibility/content');
});

Expand All @@ -124,19 +144,19 @@ app.get(/^([^.]+)$/, (req, res, next) => {
});

// Render sitemap.xml in XML format
app.get('/service-manual/sitemap.xml', (req, res) => {
app.get('/service-manual/sitemap.xml', (_, res) => {
res.set({ 'Content-Type': 'application/xml' });
res.render('sitemap.xml');
});

// Render robots.txt in text format
app.get('/service-manual/robots.txt', (req, res) => {
app.get('/service-manual/robots.txt', (_, res) => {
res.set('text/plain');
res.render('robots.txt');
});

// Render 404 page
app.get('*', (req, res) => {
app.get('*', (_, res) => {
res.statusCode = 404;
res.render('page-not-found');
});
Expand All @@ -157,10 +177,8 @@ if (config.env === 'development') {
app.listen(config.port);
}

/*
setTimeout(function(){
setTimeout(() => {
pageIndex.init();
}, 2000);
*/

module.exports = app;
22 changes: 22 additions & 0 deletions app/scripts/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,36 @@
import './polyfills';

// NHS.UK frontend
import AutoComplete from 'nhsuk-frontend/packages/components/header/autoCompleteConfig';
import Header from '../../node_modules/nhsuk-frontend/packages/components/header/header';
import SkipLink from '../../node_modules/nhsuk-frontend/packages/components/skip-link/skip-link';
import Details from '../../node_modules/nhsuk-frontend/packages/components/details/details';
import DesignExample from './design-example';
import {
inputValue,
onConfirm,
source,
suggestion,
} from './search';

// Initialise components
Header();
Details();
SkipLink();

document.querySelectorAll(DesignExample.selector()).forEach((el) => {
new DesignExample(el);
});

// Header autocomplete
AutoComplete({
containerId: 'autocomplete-container',
formId: 'search',
inputId: 'search-field',
onConfirm,
source,
templates: {
inputValue,
suggestion,
},
});
41 changes: 41 additions & 0 deletions app/scripts/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Function to build truncated result with svg for search autocomplete
* @param {string} result String containing individual result from autocomplete source function
* @returns {string} String of HTML containing passed result
*/
export const suggestion = ({ title }) => {
const truncateLength = 36;
const dots = title.length > truncateLength ? '...' : '';
const resultTruncated = title.substring(0, truncateLength) + dots;
return ` <svg class="nhsuk-icon nhsuk-icon__search" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M19.71 18.29l-4.11-4.1a7 7 0 1 0-1.41 1.41l4.1 4.11a1 1 0 0 0 1.42 0 1 1 0 0 0 0-1.42zM5 10a5 5 0 1 1 5 5 5 5 0 0 1-5-5z"></path></svg>
<span class="autocomplete__option-title">${resultTruncated}</span>
`;
};

export const inputValue = (obj) => {
if (obj && obj.title) return obj.title.trim();
return '';
};

/**
* Function to populate the search autocomplete.
* @param {string} query Query to pass to search API
* @param {function} populateResults Callback function passed to source by autocomplete plugin
*/
export const source = (query, populateResults) => {
// Build URL for search endpoint
const url = `/service-manual/suggestions/?search=${query}`;
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = () => {
if (xhr.status === 200) {
const json = JSON.parse(xhr.responseText);
populateResults(json);
}
};
xhr.send();
};

export const onConfirm = ({ url }) => {
window.location = url;
};
49 changes: 47 additions & 2 deletions app/styles/app/_search.scss
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@
width: 100%; /* [2] */

&:focus {
box-shadow: inset 0 0 0 $nhsuk-box-shadow-spread $nhsuk-focus-color;
border: $nhsuk-box-shadow-spread solid $nhsuk-focus-color;
box-shadow: inset 0 0 0 $nhsuk-box-shadow-spread $nhsuk-focus-text-color;
}
}

Expand Down Expand Up @@ -95,7 +96,12 @@
}

&:focus {
box-shadow: inset 0 0 0 $nhsuk-box-shadow-spread $nhsuk-focus-color;
background-color: $nhsuk-focus-color;
border-bottom: 4px solid $nhsuk-text-color;

.nhsuk-icon__search {
fill: $nhsuk-focus-text-color;
}
}

&:active {
Expand All @@ -120,3 +126,42 @@
.app-search-results-item::first-letter {
text-transform: uppercase;
}

.autocomplete__option {
text-decoration: none;

&:focus {
.autocomplete__option-title {
@include nhsuk-focused-text;
}
}

&:hover,
&:focus {
.autocomplete__option-title {
text-decoration: none;
}
}

&-title {
text-decoration: underline;
}
}

// Autocomplete list hotfixes

.autocomplete__option .nhsuk-icon__search {
margin: 2px 4px 2px 0;
}

@include mq($from: tablet) {

.autocomplete__option:last-child {
padding-bottom: 0;
}

.autocomplete__menu {
padding: 16px 8px;
}

}
8 changes: 5 additions & 3 deletions app/views/includes/header.njk
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
},
"showNav": "false",
"showSearch": "false",
"searchAction": "/service-manual/search/"
"searchAction": "/service-manual/search/",
"searchInputName": "search-field"
})
}}
{% else %}
Expand All @@ -22,8 +23,9 @@
"longName": "true"
},
"showNav": "false",
"showSearch": "false",
"searchAction": "/service-manual/search/"
"showSearch": "true",
"searchAction": "/service-manual/search/",
"searchInputName": "search-field"
})
}}
{% endif %}
42 changes: 41 additions & 1 deletion app/views/includes/search.njk
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,54 @@
<div class="nhsuk-grid-row">
<div class="nhsuk-grid-column-two-thirds">
{% if results | length > 0 %}
<p class="nhsuk-u-margin-bottom-3">
{{ resultsLen }} result{%if results | length != 1%}s{%endif%}
</p>
<ul class="nhsuk-list nhsuk-list--border">
{% for item in results %}
<li>
<a href="{{item.url}}" class="app-search-results-item">{{item.title}}</a>
<p class="nhsuk-body-s nhsuk-u-margin-top-1 nhsuk-u-secondary-text-color">{{item.description}}</p>
<p class="nhsuk-body-s nhsuk-u-margin-top-1">{{item.description}}</p>
</li>
{% endfor %}
</ul>
<nav class="nhsuk-pagination" role="navigation" aria-label="Pagination">
<ul class="nhsuk-list nhsuk-pagination__list">
{% if currentPage != 1%}
<li class="nhsuk-pagination-item--previous">
<a class="nhsuk-pagination__link nhsuk-pagination__link--prev"
href="/service-manual/search/?search-field={{query}}&page={{currentPage - 1}}">
<span class="nhsuk-pagination__title">Previous</span>
<span class="nhsuk-u-visually-hidden">:</span>
<span class="nhsuk-pagination__page">{{ currentPage - 1 }} of {{ maxPage }} </span>
<svg class="nhsuk-icon nhsuk-icon__arrow-left" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
aria-hidden="true">
<path
d="M4.1 12.3l2.7 3c.2.2.5.2.7 0 .1-.1.1-.2.1-.3v-2h11c.6 0 1-.4 1-1s-.4-1-1-1h-11V9c0-.2-.1-.4-.3-.5h-.2c-.1 0-.3.1-.4.2l-2.7 3c0 .2 0 .4.1.6z">
</path>
</svg>
</a>
</li>
{% endif %}

{% if currentPage != maxPage %}
<li class="nhsuk-pagination-item--next">
<a class="nhsuk-pagination__link nhsuk-pagination__link--next"
href="/service-manual/search/?search-field={{query}}&page={{currentPage + 1}}">
<span class="nhsuk-pagination__title">Next</span>
<span class="nhsuk-u-visually-hidden">:</span>
<span class="nhsuk-pagination__page">{{ currentPage + 1}} of {{ maxPage }}</span>
<svg class="nhsuk-icon nhsuk-icon__arrow-right" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
aria-hidden="true">
<path
d="M19.6 11.66l-2.73-3A.51.51 0 0 0 16 9v2H5a1 1 0 0 0 0 2h11v2a.5.5 0 0 0 .32.46.39.39 0 0 0 .18 0 .52.52 0 0 0 .37-.16l2.73-3a.5.5 0 0 0 0-.64z">
</path>
</svg>
</a>
</li>
{% endif %}
</ul>
</nav>
{% else %}
{% if query != "" %}
<p>Your search - <span class="nhsuk-u-font-weight-bold">{{query}}</span> - had no matching results.</p>
Expand Down
Loading

0 comments on commit 7d0cfd3

Please sign in to comment.