Merge pull request #217 from squidfunk/refactor/search-results

Improved search interface
This commit is contained in:
Martin Donath 2017-03-16 13:04:31 +01:00 committed by GitHub
commit 3c1cfa86e0
15 changed files with 218 additions and 65 deletions

View File

@ -44,7 +44,7 @@ cache:
# Install yarn as Travis doesn't support it out of the box
before_install:
- npm install -g yarn
- npm install -g yarn@v0.22.0
# Install dependencies
install:

View File

@ -90,7 +90,7 @@ let args = yargs
})
.option("optimize", {
describe: chalk.grey("optimize and minify assets"),
default: true,
default: false,
global: true
})
.option("revision", {

View File

@ -26,7 +26,9 @@
declare class Jsx {
static createElement(tag: string, properties?: Object,
...children?: Array<string | number | Array<HTMLElement>>
...children?: Array<
string | number | { __html?: string } | Array<HTMLElement>
>
): HTMLElement
}

View File

@ -24,14 +24,16 @@
* Module
* ------------------------------------------------------------------------- */
export default /* Jsx */ {
/* eslint-disable no-underscore-dangle */
export default /* JSX */ {
/**
* Create a native DOM node from JSX's intermediate representation
*
* @param {string} tag - Tag name
* @param {?Object} properties - Properties
* @param {...(string|number|Array)} children - Child nodes
* @param {Array<string | number | { __html: string } | Array<HTMLElement>>}
* children - Child nodes
* @return {HTMLElement} Native DOM node
*/
createElement(tag, properties, ...children) {
@ -56,8 +58,12 @@ export default /* Jsx */ {
} else if (Array.isArray(node)) {
iterateChildNodes(node)
/* Append raw HTML */
} else if (typeof node.__html !== "undefined") {
el.innerHTML += node.__html
/* Append regular nodes */
} else {
} else if (node instanceof Node) {
el.appendChild(node)
}
})

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -38,7 +38,7 @@
<script src="{{ base_url }}/assets/javascripts/modernizr-56ade86843.js"></script>
{% endblock %}
{% block styles %}
<link rel="stylesheet" href="{{ base_url }}/assets/stylesheets/application-1d1da4857d.css">
<link rel="stylesheet" href="{{ base_url }}/assets/stylesheets/application-76741b64bc.css">
{% if config.extra.palette %}
<link rel="stylesheet" href="{{ base_url }}/assets/stylesheets/application-66fa0d9bba.palette.css">
{% endif %}
@ -151,7 +151,7 @@
{% endblock %}
</div>
{% block scripts %}
<script src="{{ base_url }}/assets/javascripts/application-7aa26ad9ec.js"></script>
<script src="{{ base_url }}/assets/javascripts/application-95c175e0e2.js"></script>
<script>app.initialize({url:{base:"{{ base_url }}"}})</script>
{% for path in extra_javascript %}
<script src="{{ path }}"></script>

View File

@ -5,6 +5,10 @@
"meta.comments": "Comments",
"meta.source": "Source",
"search.placeholder": "Search",
"search.message.placeholder": "Type to start searching",
"search.message.none": "No matching documents",
"search.message.one": "1 matching document",
"search.message.other": "# matching documents",
"source.link.title": "Go to repository",
"toc.title": "Table of contents"
}[key] }}{% endmacro %}

View File

@ -8,7 +8,12 @@
</form>
<div class="md-search__output">
<div class="md-search__scrollwrap" data-md-scrollfix>
<div class="md-search-result" data-md-component="result"></div>
<div class="md-search-result" data-md-component="result">
<div class="md-search-result__meta" data-md-message-none="{{ lang.t('search.message.none') }}" data-md-message-one="{{ lang.t('search.message.one') }}" data-md-message-other="{{ lang.t('search.message.other') }}">
{{ lang.t('search.message.placeholder') }}
</div>
<ol class="md-search-result__list"></ol>
</div>
</div>
</div>
</div>

View File

@ -38,6 +38,7 @@ export default class Result {
* @property {Object} docs_ - Indexed documents
* @property {HTMLElement} meta_ - Search meta information
* @property {HTMLElement} list_ - Search result list
* @property {Object} message_ - Search result messages
* @property {Object} index_ - Search index
*
* @param {(string|HTMLElement)} el - Selector or HTML element
@ -51,20 +52,20 @@ export default class Result {
throw new ReferenceError
this.el_ = ref
/* Set data and create metadata and list elements */
this.data_ = data
this.meta_ = (
<div class="md-search-result__meta">
Type to start searching
</div>
)
this.list_ = (
<ol class="md-search-result__list"></ol>
)
/* Retrieve metadata and list element */
const [meta, list] = this.el_.children
/* Inject created elements */
this.el_.appendChild(this.meta_)
this.el_.appendChild(this.list_)
/* Set data, metadata and list elements */
this.data_ = data
this.meta_ = meta
this.list_ = list
/* Load messages for metadata display */
this.message_ = {
none: this.meta_.dataset.mdMessageNone,
one: this.meta_.dataset.mdMessageOne,
other: this.meta_.dataset.mdMessageOther
}
}
/**
@ -72,7 +73,7 @@ export default class Result {
*
* This is not a reasonable approach, since the summaries kind of suck. It
* would be better to create something more intelligent, highlighting the
* search occurrences and making a better summary out of it
* search occurrences and making a better summary out of it.
*
* @param {string} string - String to be truncated
* @param {number} n - Number of characters
@ -107,12 +108,36 @@ export default class Result {
/* eslint-enable no-invalid-this, lines-around-comment */
})
/* Index documents */
/* Preprocess and index sections and documents */
this.docs_ = data.reduce((docs, doc) => {
this.index_.add(doc)
docs[doc.location] = doc
const [path, hash] = doc.location.split("#")
/* Associate section with parent document */
if (hash) {
doc.parent = docs.get(path)
/* Override page title with document title if first section */
if (doc.parent && !doc.parent.done) {
doc.parent.title = doc.title
doc.parent.text = doc.text
doc.parent.done = true
}
}
/* Some cleanup on the text */
doc.text = doc.text
.replace(/\n/g, " ") /* Remove newlines */
.replace(/\s+/g, " ") /* Compact whitespace */
.replace(/\s+([,.:;!?])/g, /* Correct punctuation */
(_, char) => char)
/* Index sections and documents, but skip top-level headline */
if (!doc.parent || doc.parent.title !== doc.title) {
this.index_.add(doc)
docs.set(doc.location, doc)
}
return docs
}, {})
}, new Map)
}
/* Initialize index after short timeout to account for transition */
@ -132,33 +157,62 @@ export default class Result {
while (this.list_.firstChild)
this.list_.removeChild(this.list_.firstChild)
/* Perform search on index and render documents */
const result = this.index_.search(target.value)
result.forEach(item => {
const doc = this.docs_[item.ref]
/* Perform search on index and group sections by document */
const result = this.index_
.search(target.value)
.reduce((items, item) => {
const doc = this.docs_.get(item.ref)
if (doc.parent) {
const ref = doc.parent.location
items.set(ref, (items.get(ref) || []).concat(item))
}
return items
}, new Map)
/* Check if it's a anchor link on the current page */
let [pathname] = doc.location.split("#")
pathname = pathname.replace(/^(\/?\.{2})+/g, "")
/* Assemble highlight regex from query string */
const match = new RegExp(
`\\b(${target.value.trim().replace(" ", "|")})`, "img")
const highlight = string => `<em>${string}</em>`
/* Render results */
result.forEach((items, ref) => {
const doc = this.docs_.get(ref)
/* Append search result */
this.list_.appendChild(
<li class="md-search-result__item">
<a href={doc.location} title={doc.title}
class="md-search-result__link" data-md-rel={
pathname === document.location.pathname
? "anchor" : ""}>
<article class="md-search-result__article">
class="md-search-result__link">
<article class="md-search-result__article
md-search-result__article--document">
<h1 class="md-search-result__title">
{doc.title}
{{ __html: doc.title.replace(match, highlight) }}
</h1>
<p class="md-search-result__teaser">
{this.truncate_(doc.text, 140)}
</p>
{doc.text.length ?
<p class="md-search-result__teaser">
{{ __html: doc.text.replace(match, highlight) }}
</p> : {}}
</article>
</a>
{items.map(item => {
const section = this.docs_.get(item.ref)
return (
<a href={section.location} title={section.title}
class="md-search-result__link" data-md-rel="anchor">
<article class="md-search-result__article">
<h1 class="md-search-result__title">
{{ __html: section.title.replace(match, highlight) }}
</h1>
{section.text.length ?
<p class="md-search-result__teaser">
{{ __html: section.text.replace(match, highlight) }}
</p> : {}}
</article>
</a>
)
})}
</li>
)
) /* {this.truncate_(doc.text, 140)} */
})
/* Bind click handlers for anchors */
@ -183,8 +237,17 @@ export default class Result {
})
/* Update search metadata */
this.meta_.textContent =
`${result.length} search result${result.length !== 1 ? "s" : ""}`
switch (result.size) {
case 0:
this.meta_.textContent = this.message_.none
break
case 1:
this.meta_.textContent = this.message_.one
break
default:
this.meta_.textContent =
this.message_.other.replace("#", result.size)
}
}
}
}

View File

@ -346,6 +346,8 @@
// Search result
.md-search-result {
color: $md-color-black;
word-break: break-word;
// Search metadata
&__meta {
@ -377,42 +379,101 @@
// Link inside item
&__link {
display: block;
padding: 0 1.6rem;
transition: background 0.25s;
overflow: auto;
overflow: hidden;
// Hovered link
&:hover {
background-color: transparentize($md-color-accent, 0.9);
}
// Add a little spacing on the last link
&:last-child {
padding-bottom: 0.8rem;
}
}
// Article - document or section
&__article {
position: relative;
padding: 0 1.6rem;
overflow: auto;
// [tablet landscape +]: Increase left indent
@include break-from-device(tablet landscape) {
padding-left: 4.8rem;
}
// Document
&--document {
// Icon
&::before {
@extend %md-icon, %md-icon__button;
position: absolute;
left: 0;
color: $md-color-black--lighter;
content: "find_in_page";
// [tablet portrait -]: Hide page icon
@include break-to-device(tablet portrait) {
display: none;
}
}
// Title
.md-search-result__title {
margin: 1.3rem 0;
font-size: ms(0);
font-weight: 400;
line-height: 1.4;
}
}
}
// Search result content
&__article {
margin: 1em 0;
}
// Search result title
// Title
&__title {
margin-top: 0.5em;
margin-bottom: 0;
color: $md-color-black;
font-size: ms(0);
font-weight: 400;
margin: 0.5em 0;
font-size: ms(-1);
font-weight: 700;
line-height: 1.4;
}
// Search result teaser
// stylelint-disable value-no-vendor-prefix, property-no-vendor-prefix
// Teaser
&__teaser {
display: -webkit-box;
max-height: 3.3rem;
margin: 0.5em 0;
color: $md-color-black--light;
font-size: ms(-1);
line-height: 1.4;
word-break: break-word;
text-overflow: ellipsis;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
// [mobile -]: Increase number of lines
@include break-to-device(mobile) {
max-height: 5rem;
-webkit-line-clamp: 3;
}
// [tablet landscape]: Increase number of lines
@include break-at-device(tablet landscape) {
max-height: 5rem;
-webkit-line-clamp: 3;
}
}
// stylelint-enable value-no-vendor-prefix, property-no-vendor-prefix
// Highlighting
em {
font-style: normal;
font-weight: 700;
text-decoration: underline;
}
}

View File

@ -28,6 +28,10 @@
"meta.comments": "Comments",
"meta.source": "Source",
"search.placeholder": "Search",
"search.message.placeholder": "Type to start searching",
"search.message.none": "No matching documents",
"search.message.one": "1 matching document",
"search.message.other": "# matching documents",
"source.link.title": "Go to repository",
"toc.title": "Table of contents"
}[key] }}{% endmacro %}

View File

@ -35,7 +35,15 @@
</form>
<div class="md-search__output">
<div class="md-search__scrollwrap" data-md-scrollfix>
<div class="md-search-result" data-md-component="result"></div>
<div class="md-search-result" data-md-component="result">
<div class="md-search-result__meta"
data-md-message-none="{{ lang.t('search.message.none') }}"
data-md-message-one="{{ lang.t('search.message.one') }}"
data-md-message-other="{{ lang.t('search.message.other') }}">
{{ lang.t('search.message.placeholder') }}
</div>
<ol class="md-search-result__list"></ol>
</div>
</div>
</div>
</div>