mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2024-06-14 11:52:32 +03:00
Merge pull request #217 from squidfunk/refactor/search-results
Improved search interface
This commit is contained in:
commit
3c1cfa86e0
@ -44,7 +44,7 @@ cache:
|
|||||||
|
|
||||||
# Install yarn as Travis doesn't support it out of the box
|
# Install yarn as Travis doesn't support it out of the box
|
||||||
before_install:
|
before_install:
|
||||||
- npm install -g yarn
|
- npm install -g yarn@v0.22.0
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
install:
|
install:
|
||||||
|
@ -90,7 +90,7 @@ let args = yargs
|
|||||||
})
|
})
|
||||||
.option("optimize", {
|
.option("optimize", {
|
||||||
describe: chalk.grey("optimize and minify assets"),
|
describe: chalk.grey("optimize and minify assets"),
|
||||||
default: true,
|
default: false,
|
||||||
global: true
|
global: true
|
||||||
})
|
})
|
||||||
.option("revision", {
|
.option("revision", {
|
||||||
|
@ -26,7 +26,9 @@
|
|||||||
|
|
||||||
declare class Jsx {
|
declare class Jsx {
|
||||||
static createElement(tag: string, properties?: Object,
|
static createElement(tag: string, properties?: Object,
|
||||||
...children?: Array<string | number | Array<HTMLElement>>
|
...children?: Array<
|
||||||
|
string | number | { __html?: string } | Array<HTMLElement>
|
||||||
|
>
|
||||||
): HTMLElement
|
): HTMLElement
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,14 +24,16 @@
|
|||||||
* Module
|
* Module
|
||||||
* ------------------------------------------------------------------------- */
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
export default /* Jsx */ {
|
/* eslint-disable no-underscore-dangle */
|
||||||
|
export default /* JSX */ {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a native DOM node from JSX's intermediate representation
|
* Create a native DOM node from JSX's intermediate representation
|
||||||
*
|
*
|
||||||
* @param {string} tag - Tag name
|
* @param {string} tag - Tag name
|
||||||
* @param {?Object} properties - Properties
|
* @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
|
* @return {HTMLElement} Native DOM node
|
||||||
*/
|
*/
|
||||||
createElement(tag, properties, ...children) {
|
createElement(tag, properties, ...children) {
|
||||||
@ -56,8 +58,12 @@ export default /* Jsx */ {
|
|||||||
} else if (Array.isArray(node)) {
|
} else if (Array.isArray(node)) {
|
||||||
iterateChildNodes(node)
|
iterateChildNodes(node)
|
||||||
|
|
||||||
|
/* Append raw HTML */
|
||||||
|
} else if (typeof node.__html !== "undefined") {
|
||||||
|
el.innerHTML += node.__html
|
||||||
|
|
||||||
/* Append regular nodes */
|
/* Append regular nodes */
|
||||||
} else {
|
} else if (node instanceof Node) {
|
||||||
el.appendChild(node)
|
el.appendChild(node)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
File diff suppressed because one or more lines are too long
3
material/assets/javascripts/application-95c175e0e2.js
Normal file
3
material/assets/javascripts/application-95c175e0e2.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
material/assets/stylesheets/application-76741b64bc.css
Normal file
1
material/assets/stylesheets/application-76741b64bc.css
Normal file
File diff suppressed because one or more lines are too long
@ -38,7 +38,7 @@
|
|||||||
<script src="{{ base_url }}/assets/javascripts/modernizr-56ade86843.js"></script>
|
<script src="{{ base_url }}/assets/javascripts/modernizr-56ade86843.js"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block styles %}
|
{% 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 %}
|
{% if config.extra.palette %}
|
||||||
<link rel="stylesheet" href="{{ base_url }}/assets/stylesheets/application-66fa0d9bba.palette.css">
|
<link rel="stylesheet" href="{{ base_url }}/assets/stylesheets/application-66fa0d9bba.palette.css">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -151,7 +151,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
{% block scripts %}
|
{% 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>
|
<script>app.initialize({url:{base:"{{ base_url }}"}})</script>
|
||||||
{% for path in extra_javascript %}
|
{% for path in extra_javascript %}
|
||||||
<script src="{{ path }}"></script>
|
<script src="{{ path }}"></script>
|
||||||
|
@ -5,6 +5,10 @@
|
|||||||
"meta.comments": "Comments",
|
"meta.comments": "Comments",
|
||||||
"meta.source": "Source",
|
"meta.source": "Source",
|
||||||
"search.placeholder": "Search",
|
"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",
|
"source.link.title": "Go to repository",
|
||||||
"toc.title": "Table of contents"
|
"toc.title": "Table of contents"
|
||||||
}[key] }}{% endmacro %}
|
}[key] }}{% endmacro %}
|
||||||
|
@ -8,7 +8,12 @@
|
|||||||
</form>
|
</form>
|
||||||
<div class="md-search__output">
|
<div class="md-search__output">
|
||||||
<div class="md-search__scrollwrap" data-md-scrollfix>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -38,6 +38,7 @@ export default class Result {
|
|||||||
* @property {Object} docs_ - Indexed documents
|
* @property {Object} docs_ - Indexed documents
|
||||||
* @property {HTMLElement} meta_ - Search meta information
|
* @property {HTMLElement} meta_ - Search meta information
|
||||||
* @property {HTMLElement} list_ - Search result list
|
* @property {HTMLElement} list_ - Search result list
|
||||||
|
* @property {Object} message_ - Search result messages
|
||||||
* @property {Object} index_ - Search index
|
* @property {Object} index_ - Search index
|
||||||
*
|
*
|
||||||
* @param {(string|HTMLElement)} el - Selector or HTML element
|
* @param {(string|HTMLElement)} el - Selector or HTML element
|
||||||
@ -51,20 +52,20 @@ export default class Result {
|
|||||||
throw new ReferenceError
|
throw new ReferenceError
|
||||||
this.el_ = ref
|
this.el_ = ref
|
||||||
|
|
||||||
/* Set data and create metadata and list elements */
|
/* Retrieve metadata and list element */
|
||||||
this.data_ = data
|
const [meta, list] = this.el_.children
|
||||||
this.meta_ = (
|
|
||||||
<div class="md-search-result__meta">
|
|
||||||
Type to start searching
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
this.list_ = (
|
|
||||||
<ol class="md-search-result__list"></ol>
|
|
||||||
)
|
|
||||||
|
|
||||||
/* Inject created elements */
|
/* Set data, metadata and list elements */
|
||||||
this.el_.appendChild(this.meta_)
|
this.data_ = data
|
||||||
this.el_.appendChild(this.list_)
|
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
|
* This is not a reasonable approach, since the summaries kind of suck. It
|
||||||
* would be better to create something more intelligent, highlighting the
|
* 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 {string} string - String to be truncated
|
||||||
* @param {number} n - Number of characters
|
* @param {number} n - Number of characters
|
||||||
@ -107,12 +108,36 @@ export default class Result {
|
|||||||
/* eslint-enable no-invalid-this, lines-around-comment */
|
/* eslint-enable no-invalid-this, lines-around-comment */
|
||||||
})
|
})
|
||||||
|
|
||||||
/* Index documents */
|
/* Preprocess and index sections and documents */
|
||||||
this.docs_ = data.reduce((docs, doc) => {
|
this.docs_ = data.reduce((docs, 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)
|
this.index_.add(doc)
|
||||||
docs[doc.location] = doc
|
docs.set(doc.location, doc)
|
||||||
|
}
|
||||||
return docs
|
return docs
|
||||||
}, {})
|
}, new Map)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Initialize index after short timeout to account for transition */
|
/* Initialize index after short timeout to account for transition */
|
||||||
@ -132,33 +157,62 @@ export default class Result {
|
|||||||
while (this.list_.firstChild)
|
while (this.list_.firstChild)
|
||||||
this.list_.removeChild(this.list_.firstChild)
|
this.list_.removeChild(this.list_.firstChild)
|
||||||
|
|
||||||
/* Perform search on index and render documents */
|
/* Perform search on index and group sections by document */
|
||||||
const result = this.index_.search(target.value)
|
const result = this.index_
|
||||||
result.forEach(item => {
|
.search(target.value)
|
||||||
const doc = this.docs_[item.ref]
|
.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 */
|
/* Assemble highlight regex from query string */
|
||||||
let [pathname] = doc.location.split("#")
|
const match = new RegExp(
|
||||||
pathname = pathname.replace(/^(\/?\.{2})+/g, "")
|
`\\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 */
|
/* Append search result */
|
||||||
this.list_.appendChild(
|
this.list_.appendChild(
|
||||||
<li class="md-search-result__item">
|
<li class="md-search-result__item">
|
||||||
<a href={doc.location} title={doc.title}
|
<a href={doc.location} title={doc.title}
|
||||||
class="md-search-result__link" data-md-rel={
|
class="md-search-result__link">
|
||||||
pathname === document.location.pathname
|
<article class="md-search-result__article
|
||||||
? "anchor" : ""}>
|
md-search-result__article--document">
|
||||||
<article class="md-search-result__article">
|
|
||||||
<h1 class="md-search-result__title">
|
<h1 class="md-search-result__title">
|
||||||
{doc.title}
|
{{ __html: doc.title.replace(match, highlight) }}
|
||||||
</h1>
|
</h1>
|
||||||
|
{doc.text.length ?
|
||||||
<p class="md-search-result__teaser">
|
<p class="md-search-result__teaser">
|
||||||
{this.truncate_(doc.text, 140)}
|
{{ __html: doc.text.replace(match, highlight) }}
|
||||||
</p>
|
</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>
|
</article>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
|
||||||
)
|
)
|
||||||
|
})}
|
||||||
|
</li>
|
||||||
|
) /* {this.truncate_(doc.text, 140)} */
|
||||||
})
|
})
|
||||||
|
|
||||||
/* Bind click handlers for anchors */
|
/* Bind click handlers for anchors */
|
||||||
@ -183,8 +237,17 @@ export default class Result {
|
|||||||
})
|
})
|
||||||
|
|
||||||
/* Update search metadata */
|
/* Update search metadata */
|
||||||
|
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.meta_.textContent =
|
||||||
`${result.length} search result${result.length !== 1 ? "s" : ""}`
|
this.message_.other.replace("#", result.size)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -346,6 +346,8 @@
|
|||||||
|
|
||||||
// Search result
|
// Search result
|
||||||
.md-search-result {
|
.md-search-result {
|
||||||
|
color: $md-color-black;
|
||||||
|
word-break: break-word;
|
||||||
|
|
||||||
// Search metadata
|
// Search metadata
|
||||||
&__meta {
|
&__meta {
|
||||||
@ -377,42 +379,101 @@
|
|||||||
// Link inside item
|
// Link inside item
|
||||||
&__link {
|
&__link {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 0 1.6rem;
|
|
||||||
transition: background 0.25s;
|
transition: background 0.25s;
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
|
|
||||||
// Hovered link
|
// Hovered link
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: transparentize($md-color-accent, 0.9);
|
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
|
// [tablet landscape +]: Increase left indent
|
||||||
@include break-from-device(tablet landscape) {
|
@include break-from-device(tablet landscape) {
|
||||||
padding-left: 4.8rem;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search result content
|
// Title
|
||||||
&__article {
|
.md-search-result__title {
|
||||||
margin: 1em 0;
|
margin: 1.3rem 0;
|
||||||
}
|
|
||||||
|
|
||||||
// Search result title
|
|
||||||
&__title {
|
|
||||||
margin-top: 0.5em;
|
|
||||||
margin-bottom: 0;
|
|
||||||
color: $md-color-black;
|
|
||||||
font-size: ms(0);
|
font-size: ms(0);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Search result teaser
|
// Title
|
||||||
|
&__title {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
font-size: ms(-1);
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// stylelint-disable value-no-vendor-prefix, property-no-vendor-prefix
|
||||||
|
|
||||||
|
// Teaser
|
||||||
&__teaser {
|
&__teaser {
|
||||||
|
display: -webkit-box;
|
||||||
|
max-height: 3.3rem;
|
||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
color: $md-color-black--light;
|
color: $md-color-black--light;
|
||||||
font-size: ms(-1);
|
font-size: ms(-1);
|
||||||
line-height: 1.4;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,10 @@
|
|||||||
"meta.comments": "Comments",
|
"meta.comments": "Comments",
|
||||||
"meta.source": "Source",
|
"meta.source": "Source",
|
||||||
"search.placeholder": "Search",
|
"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",
|
"source.link.title": "Go to repository",
|
||||||
"toc.title": "Table of contents"
|
"toc.title": "Table of contents"
|
||||||
}[key] }}{% endmacro %}
|
}[key] }}{% endmacro %}
|
||||||
|
@ -35,7 +35,15 @@
|
|||||||
</form>
|
</form>
|
||||||
<div class="md-search__output">
|
<div class="md-search__output">
|
||||||
<div class="md-search__scrollwrap" data-md-scrollfix>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user