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

[WIP] implement-start-of-search #213

Open
wants to merge 38 commits into
base: prod
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
d1859dc
implement-start-of-search
kkemple Jun 12, 2020
b65b1ac
Merge branch 'prod' into wip-search
kkemple Jun 12, 2020
59315c2
rename filter for removing markdown formatting
kkemple Jun 12, 2020
c4d6c7e
Merge branch 'wip-search' of https://github.com/kkemple/selfdefined i…
kkemple Jun 12, 2020
5e6a3a7
fix conflicts
kkemple Jun 12, 2020
dc9e6b8
remove unused dev dependencies
kkemple Jun 12, 2020
12de561
readd engine field
kkemple Jun 12, 2020
e908972
move search to serverless function
kkemple Jun 20, 2020
15ed80b
Merge branch 'prod' into wip-search
kkemple Jun 20, 2020
fd6e0a5
update styling for search
kkemple Jul 4, 2020
6b97a9e
Merge branch 'wip-search' of https://github.com/kkemple/selfdefined i…
kkemple Jul 4, 2020
30c3f7e
Merge branch 'prod' into wip-search
kkemple Jul 4, 2020
9ac06a3
update search to show snippets with searched text highlighted
kkemple Aug 1, 2020
9572418
Merge branch 'wip-search' of https://github.com/kkemple/selfdefined i…
kkemple Aug 1, 2020
302f6d5
Merge branch 'prod' into wip-search
kkemple Aug 1, 2020
52ddbc8
rename search json file and permalink
kkemple Aug 8, 2020
1954503
Merge branch 'wip-search' of https://github.com/kkemple/selfdefined i…
kkemple Aug 8, 2020
87866d7
moved search to search page
kkemple Aug 8, 2020
274f3b1
remove unused function in search component
kkemple Aug 8, 2020
0f63bf5
Update 11ty/search.njk
kkemple Aug 10, 2020
44f4c6d
Update 11ty/search.njk
kkemple Aug 10, 2020
4489ffa
refactor HTML, CSS, and add search to results page
kkemple Aug 15, 2020
a268118
move serverless function into project
kkemple Aug 15, 2020
fd6b361
update search url in search results page
kkemple Aug 15, 2020
202a22d
add missing dependencies for search function
kkemple Aug 15, 2020
f5ee15d
add variable declaration for definition in loop in search function
kkemple Aug 15, 2020
953df5d
fix callback signatures in search function
kkemple Aug 15, 2020
3ff2848
Merge branch 'prod' into wip-search
kkemple Aug 21, 2020
665af72
add search results table of contents
Aug 22, 2020
9a527d5
feat(search): make smooth scrolling global default
ovlb Aug 24, 2020
303fd28
feat(search): abstract button and input styles, mk focus more visible
ovlb Aug 24, 2020
b9680a7
Merge branch 'prod' into wip-search
ovlb Aug 24, 2020
5393ea1
🙈 revert to using netlify
ovlb Aug 24, 2020
4bd2312
Merge branch 'wip-search' of github.com:kkemple/selfdefined into wip-…
ovlb Aug 24, 2020
b8eaec3
Merge branch 'prod' into wip-search
ovlb Aug 28, 2020
7e01f98
feat(search): port dark mode, style clean ups
ovlb Aug 28, 2020
17903be
Merge branch 'prod' into wip-search
kkemple Aug 29, 2020
10b14b7
Merge branch 'prod' into wip-search
kkemple Aug 31, 2020
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
22 changes: 22 additions & 0 deletions .eleventy.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const definitionPermalink = require('./11ty/helpers/definitionPermalink');
const renderDefinitionContentNextEntries = require('./11ty/shortcodes/renderDefinitionContentNextEntries');
const findExistingDefinition = require('./11ty/filters/helpers/findExistingDefinition');
const pluginRss = require('@11ty/eleventy-plugin-rss');
const removeMd = require('remove-markdown');

module.exports = function(config) {
// Add a filter using the Config API
Expand Down Expand Up @@ -187,6 +188,27 @@ module.exports = function(config) {
});
});

config.addCollection('search', (collection) => {
return collection
.getFilteredByGlob('./11ty/definitions/*.md')
.sort((a, b) => {
// `localeCompare()` is super cool: http://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare
return a.data.title
.toLowerCase()
.localeCompare(b.data.title.toLowerCase());
});
});

// add filter to strip Markdown formatting from search text
config.addFilter('removeMarkdownFormatting', (value) => {
return removeMd(value).replace(/[\s\s+,\n]/g, ' ');
});

// add filter to extract text from sub_terms in search template
config.addFilter('extractSubTermText', (value) => {
return value ? value.map((subterm) => subterm.text).join(',') : '';
});

const mdIt = require('markdown-it')({
html: true
});
Expand Down
7 changes: 7 additions & 0 deletions 11ty/_includes/components/search.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<form method="get" action="/search" class="js-search-form search__form">
<label for="q" class="search__label sub-headline">Look up a definition</label>
<div class="search__actions">
<input type="search" class="search__input" name="q" />
Copy link
Contributor

@mxmason mxmason Aug 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<input type="search" class="search__input" name="q" />
<input type="search" id="q" class="search__input" name="q" />

@kkemple: input:label associations are created with the for attribute pointing to a unique ID*. Thank you to @colabottles for pointing this out; I missed it!

It's worth noting, the title attribute is not a good technique for defining the accessible name of a control because browsers and assistive technologies may not actually report the title to the user. It's better to use aria-label when we cannot use a real <label> element, and we do have a real label here!

*paging @ovlb in case he has some thoughts on a safer unique ID than q :)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dengeist aria-label is the better method, I agree. This is one case that ARIA should be used. Thanks for pointing that out!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re ID: might be safer to namespace it into search-query or alike. Currently it’s the only input we have, but it will probably not stay this way.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does the input name need to change if we change the ID?

Copy link
Collaborator

@ovlb ovlb Aug 25, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessarily. The name is what’s used in FormData, document.forms.formName, or — if old school — when handling a POST request coming from a form action on the server. Whereas the ID is used in HTML to link label and input. I wrote an article where this is explained in more detail: Association of labels and inputs.

<button type="submit" class="-main">Search</button>
</div>
</form>
1 change: 1 addition & 0 deletions 11ty/_includes/components/table-of-content.njk
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<section class="homepage-section homepage-section--toc">
<h2 id="terms-heading" class="visually-hidden">Terms</h2>
{% include 'components/search.njk' %}
<nav class="" aria-labelledby="terms-heading">
<ol class="multi-column u-no-padding-left list">
{% for section in collections.tableOfContent %}
Expand Down
1 change: 0 additions & 1 deletion 11ty/_includes/layouts/index.njk
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
<main>
{{ content | safe }}
{% include 'components/table-of-content.njk' %}

</main>

{% endblock %}
Expand Down
8 changes: 8 additions & 0 deletions 11ty/_includes/layouts/search.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{% extends 'layouts/base.njk' %}
{% set titleWithPath = 'Search « ' %}

{% block content %}
<main>
{{ content | safe }}
</main>
{% endblock %}
14 changes: 14 additions & 0 deletions 11ty/search-data.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
permalink: search-data.json
---
[
{%- for item in collections.search -%}
{
"url" : "{{ item.url }}",
"title" : "{{ item.data.title }}",
"defined" : "{{ item.data.defined }}",
"subterms": "{{item.data.sub_terms | extractSubTermText }}",
"content" : "{{ item.templateContent | removeMarkdownFormatting }}"
}{% if not loop.last %},{% else %}{%- endif -%}
{%- endfor -%}
]
104 changes: 104 additions & 0 deletions 11ty/search.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
---
layout: layouts/search.njk
---

<div class="article-content">
<div>
{% include 'components/sub-page-header.njk' %}
<ul class="js-search-results-menu seach-results-menu" />
</div>

<div class="page">
<header class="introduction">
<h1 class="introduction__title thicc-headline">
Search Results
</h1>
</header>

<section class="search">
{% include 'components/search.njk' %}
<h2 aria-live="polite" role="status" class="js-search-title">Building your search results, one moment.</h2>
<ul class="js-search-results-list" class="u-no-padding-left list" />
</section>
</div>
</div>

{% block pageScript %}
<script>
function getAllOccurrences(str, match) {
const matchLength = match.length;
const indices = []
let startIndex = 0, index

str = str.toLowerCase();
match = match.toLowerCase();

while ((index = str.indexOf(match, startIndex)) > -1) {
indices.push(index);
startIndex = index + matchLength;
}

return indices.map(index => {
// this needs some cleaning up
// currently there are edge cases that break the selection of the substring
const start = index < 50 ? index : index - 50;
const end = matchLength + 100;
const exerpt = str.substr(start, end)

return exerpt.replace(match, `<span class="search-results-list__list-item__highlight">${match}</span>`)
})
}

// don't use es6+ here because it will not be transpiled
window.addEventListener('DOMContentLoaded', function() {
function buildResultListItem(result, search) {
const occurrences = getAllOccurrences(result.content, search)
return `<h3 id="${result.title}"><a href="${result.url}">${result.title}</a></h3>${occurrences.map(occurrence => `<p>...${occurrence}...</p>`).join('')}`
}

function buildResultMenuItem(result) {
return `<a href="#${result.title}">${result.title}</a>`
}

const init = async () => {
try {
const urlParams = new URLSearchParams(window.location.search);
const $searchTitle = document.querySelector('.js-search-title')
const $resultsContainer = document.querySelector('.js-search-results-list')
const $tableOfContentsContainer = document.querySelector('.js-search-results-menu')

// remove any previous results from the list and menu
$resultsContainer.innerHTML = ''
$tableOfContentsContainer.innerHTML = ''

const value = urlParams.get('q')
const matches = await fetch(`/.netlify/functions/search?q=${value}`, {mode: 'cors'})
// const matches = await fetch(`https://climb-phi-staging.begin.app/self-defined-app-search?q=${value}`, {mode: 'cors'})
const results = await matches.json()

$resultsContainer.innerHTML = ''
$searchTitle.innerText = `We found ${results.length} ${results.length === 1 ? 'definition' : 'definitions'} containing \u275d${value}\u275e`

// better to create a fragment here
results.forEach((result) => {
// build the actual result list item
let $childListElement = document.createElement('li')
$childListElement.classList.add('search-results-list__list-item')
$childListElement.innerHTML = buildResultListItem(result, value)
$resultsContainer.appendChild($childListElement)

// build the table of content item
let $childMenuElement = document.createElement('li')
$childMenuElement.classList.add('search-results-menu__list-item')
$childMenuElement.innerHTML = buildResultMenuItem(result, value)
$tableOfContentsContainer.appendChild($childMenuElement)
})

} catch (error) {
console.log('error in search!', error)
}
}
init()
})
</script>
{% endblock %}
8 changes: 8 additions & 0 deletions assets/css/abstracts/_mixins.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,11 @@
@mixin icon__embed() {
margin-right: 0.35rem;
}

@mixin dark-mode-rule() {
@media (prefers-color-scheme: dark) {
:root:not([data-user-theme='light']) {
@content;
}
}
}
11 changes: 0 additions & 11 deletions assets/css/abstracts/_universal-selector.scss

This file was deleted.

1 change: 1 addition & 0 deletions assets/css/abstracts/_variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ $colors: (
yellow: hsl(50, 100%, 50%),
dark-yellow: hsl(50, 100%, 20%),
pale-yellow: hsl(50, 100%, 86%),
very-blue: hsl(236, 100%, 58%),
black: hsl(0, 0%, 13%),
black-transparent: hsla(0, 0%, 13%, 85%),
white-transparent: hsl(0, 0%, 100%, 85%),
Expand Down
13 changes: 7 additions & 6 deletions assets/css/base.scss
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
@charset 'UTF-8';

// 1. Configuration and helpers
@import 'abstracts/variables', 'abstracts/functions', 'abstracts/mixins',
'abstracts/universal-selector';
@import 'abstracts/variables', 'abstracts/functions', 'abstracts/mixins';

// 2. Vendors
// @import
// 'vendors/normalize';

// 3. Base stuff
@import 'base/custom-properties', 'base/fonts', 'base/base', 'base/selection',
'base/typography', './base/a', 'base/helpers', 'base/radio-buttons', 'base/code';
@import 'base/custom-properties', 'base/fonts', 'base/universal-selector',
'base/base', 'base/selection', 'base/typography', './base/a', 'base/input',
'base/button', 'base/radio-buttons', 'base/helpers', 'base/code';

// 4. Layout-related sections
@import 'structures/header', 'structures/footer', 'structures/grid',
'structures/multi-column', 'structures/table-of-content',
'./structures/definition-content', './structures/definition-navigation';

// 5. Components
@import 'components/word', './components/box', 'components/lists', 'components/definitions',
'components/flag', 'components/alertbox';
@import 'components/word', './components/box', 'components/lists',
'components/definitions', 'components/flag', 'components/alertbox',
'components/search';

// 6. Page-specific styles
@import './layouts/pages';
Expand Down
4 changes: 4 additions & 0 deletions assets/css/base/_base.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ body {
font-size: pxToRem(20);
margin: 0;
padding: 2rem;

@media (prefers-reduced-motion: no-preference) {
scroll-behavior: smooth;
}
}

[hidden] {
Expand Down
19 changes: 19 additions & 0 deletions assets/css/base/_button.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
button {
appearance: none;
border: none;
border-radius: 0.25rem;
cursor: pointer;
font-family: $ext-sans;
font-weight: bold;
padding: 0.5rem 0.75rem;

&.-main {
background: var(--clr-very-blue);
color: white;
}

&:focus {
outline: 0.125rem solid var(--clr-very-blue);
outline-offset: 0.125rem;
}
}
6 changes: 2 additions & 4 deletions assets/css/base/_custom-properties.scss
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,8 @@
--l-gap: 1rem;
}

@media (prefers-color-scheme: dark) {
:root:not([data-user-theme='light']) {
@include color-scheme-dark();
}
@include dark-mode-rule {
@include color-scheme-dark();
}

:root[data-user-theme='dark'] {
Expand Down
13 changes: 13 additions & 0 deletions assets/css/base/_input.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
input[type='text'],
input[type='search'] {
background: var(--clr-background);
border: 0.125rem solid var(--clr-foreground);
border-radius: 0.25rem;
color: inherit;
padding: 0.5rem 0.75rem;

&:focus {
border-color: var(--clr-very-blue);
outline: 2px solid var(--clr-very-blue);
}
}
2 changes: 2 additions & 0 deletions assets/css/base/_universal-selector.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
* {
box-sizing: border-box;
font-family: inherit;
font-size: inherit;
}

*::before {
Expand Down
6 changes: 2 additions & 4 deletions assets/css/components/_box.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,8 @@
a {
color: var(--clr-background);

@media (prefers-color-scheme: dark) {
:root:not([data-user-theme='light']) &:focus {
background-color: var(--clr-pink);
}
@include dark-mode-rule {
background-color: var(--clr-pink);
}
}

Expand Down
20 changes: 20 additions & 0 deletions assets/css/components/_search.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.search__form {
background: transparent;
margin-bottom: 2rem;
}

.search__label {
display: block;
}

.search__actions {
display: flex;
}

.search__input {
margin-right: 0.5rem;
}

.search-results-list__list-item__highlight {
background: var(--clr-pale-yellow);
}
45 changes: 45 additions & 0 deletions functions/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const lunr = require('lunr');
const { default: axios } = require('axios');

exports.handler = async function({ queryStringParameters }, _, callback) {
try {
if (!queryStringParameters) {
callback(new Error('missing query string parameter "q"'));
}

const { data: definitions } = await axios.get(
'https://deploy-preview-213--selfdefined.netlify.app/search-data.json'
);

// create a map based on title for faster retrieval in search results
const definitionMap = definitions.reduce((map, definition) => {
map[definition.title] = definition;
return map;
}, {});

// create the full text search
const idx = lunr(function() {
this.ref('title');
this.field('title');
this.field('subterms');
this.field('content');

// load definitions into lunr index
for (const definition in definitions) {
this.add(definitions[definition]);
}
});

const matches = idx.search(queryStringParameters.q);
const results = matches.map((match) => definitionMap[match.ref]);

console.log(results)
callback(null, {
body: JSON.stringify(results),
statusCode: 200
});
} catch (error) {
console.log(error);
callback(error);
}
};
Loading