import * as React from 'react';
export default class HeadingsLink extends React.Component {
+ skipScrollingHandler = false;
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ activeIndex: -1,
+ headers: props.headers.filter(
+ h => h.depth === 2 && h.value.toLowerCase() !== 'table of contents'
+ )
+ };
+ }
+
componentDidMount() {
document.addEventListener('scroll', this.scrollHandler, true);
}
+ componentWillReceiveProps(nextProps) {
+ this.setState({
+ headers: nextProps.headers.filter(
+ h => h.depth === 2 && h.value.toLowerCase() !== 'table of contents'
+ )
+ });
+ }
+
componentWillUnmount() {
document.removeEventListener('scroll', this.scrollHandler, true);
}
- highlightHeading = (index, scrollTo) => {
+ highlightHeading = scrollTop => {
+ let headingIndex = 0;
+ for (let i = 0; i < this.state.headers.length; i++) {
+ if (document.querySelector('#header-' + (i + 1)).offsetTop > scrollTop + 40) {
+ break;
+ }
+ headingIndex = i;
+ }
+ this.setState({ activeIndex: headingIndex });
+ this.markH2(headingIndex + 1, false);
+ };
+
+ markH2 = (index, scrollTo) => {
const previousNode = document.querySelector('.targetted-heading');
if (previousNode) {
previousNode.classList.remove('targetted-heading');
if (node) {
node.classList.add('targetted-heading');
if (scrollTo) {
+ this.skipScrollingHandler = true;
window.scrollTo(0, node.offsetTop - 30);
+ this.highlightHeading(node.offsetTop - 30);
}
}
};
scrollHandler = () => {
- const headings = Array.from(document.querySelectorAll('.headings-container ul li a'));
- const scrollTop = window.pageYOffset | document.body.scrollTop;
- let headingIndex = 0;
- for (let i = 0; i < headings.length; i++) {
- if (document.querySelector('#header-' + (i + 1)).offsetTop > scrollTop + 40) {
- break;
- }
- headingIndex = i;
+ if (this.skipScrollingHandler) {
+ this.skipScrollingHandler = false;
+ return;
}
- headings.forEach(h => h.classList.remove('active'));
- headings[headingIndex].classList.add('active');
- this.highlightHeading(headingIndex + 1, false);
+
+ const scrollTop = window.pageYOffset | document.body.scrollTop;
+ this.highlightHeading(scrollTop);
};
clickHandler = target => {
return event => {
event.stopPropagation();
event.preventDefault();
- this.highlightHeading(target, true);
+ this.markH2(target, true);
};
};
render() {
- const headers = this.props.headers.filter(
- h => h.depth === 2 && h.value.toLowerCase() !== 'table of contents'
- );
+ const { headers } = this.state;
if (headers.length < 1) {
return null;
}
{headers.map((header, index) => {
return (
<li key={index + 1}>
- <a onClick={this.clickHandler(index + 1)} href={'#header-' + (index + 1)}>
+ <a
+ onClick={this.clickHandler(index + 1)}
+ href={'#header-' + (index + 1)}
+ className={this.state.activeIndex === index ? 'active' : ''}>
{header.value}
</a>
</li>
*/
import React, { Component } from 'react';
import lunr, { LunrIndex } from 'lunr';
+import ClearIcon from './icons/ClearIcon';
// Search component
export default class Search extends Component {
index = null;
+ input = null;
constructor(props) {
super(props);
+ this.state = { value: '' };
this.index = lunr(function() {
this.ref('id');
this.field('title', { boost: 10 });
this.add({
id: page.id,
title: page.frontmatter.title,
- text: page.html.replace(/<(?:.|\n)*?>/gm, '')
+ text: page.html.replace(/<(?:.|\n)*?>/gm, '').replace(/<(?:.|\n)*?>/gm, '')
})
);
});
id: page.id,
slug: page.fields.slug,
title: page.frontmatter.title,
- text: page.html.replace(/<(?:.|\n)*?>/gm, '')
+ text: page.html.replace(/<(?:.|\n)*?>/gm, '').replace(/<(?:.|\n)*?>/gm, '')
},
highlights,
longestTerm
});
};
+ handleClear = event => {
+ this.setState({ value: '' });
+ this.props.onResultsChange([], '');
+ if (this.input) {
+ this.input.focus();
+ }
+ };
+
handleChange = event => {
const { value } = event.currentTarget;
+ this.setState({ value });
if (value != '') {
const results = this.getFormattedResults(value, this.index.search(`${value}~1 ${value}*`));
- this.props.onResultsChange(results);
+ this.props.onResultsChange(results, value);
} else {
- this.props.onResultsChange([]);
+ this.props.onResultsChange([], value);
}
};
render() {
return (
- <input
- aria-label="Search"
- className="search-input"
- onChange={this.handleChange}
- placeholder="Search..."
- type="search"
- />
+ <div className="search-container">
+ <input
+ aria-label="Search"
+ className="search-input"
+ onChange={this.handleChange}
+ placeholder="Search..."
+ ref={node => (this.input = node)}
+ type="search"
+ value={this.state.value}
+ />
+ {this.state.value && (
+ <button onClick={this.handleClear}>
+ <ClearIcon size="8" />
+ </button>
+ )}
+ </div>
);
}
}
import SearchEntryResult from './SearchEntryResult';
export default class Sidebar extends React.PureComponent {
- state = { loaded: false, results: [], versions: [] };
+ state = { loaded: false, query: '', results: [], versions: [] };
componentDidMount() {
this.loadVersions();
);
};
- handleSearch = results => {
- this.setState({ results });
+ handleSearch = (results, query) => {
+ this.setState({ results, query });
};
render() {
</div>
<div className="page-indexes">
<Search pages={this.props.pages} onResultsChange={this.handleSearch} />
- {this.state.results.length > 0 && this.renderResults()}
- {this.state.results.length === 0 &&
+ {this.state.query !== '' && this.renderResults()}
+ {this.state.query === '' &&
Object.keys(nodes).map(key => (
<CategoryLink
key={key}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon from './Icon';
+
+export default function ClearIcon({ className, fill = 'currentColor', size }) {
+ return (
+ <Icon className={className} size={size} viewBox="0 0 48 48">
+ <path
+ d="M28.24 24L47.07 5.16A3 3 0 1 0 42.93.83l-.09.1L24 19.76 5.16.93A3 3 0 0 0 .93 5.16L19.76 24 .93 42.84a3 3 0 1 0 4.14 4.33l.09-.1L24 28.24l18.84 18.83a3 3 0 1 0 4.33-4.14l-.1-.09z"
+ style={{ fill }}
+ />
+ </Icon>
+ );
+}
}
}
+.search-container {
+ position: relative;
+}
+
+.search-container button {
+ position: absolute;
+ right: 8px;
+ top: 50%;
+ margin-top: -12px;
+ height: 16px;
+ width: 16px;
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ outline: none;
+ border-radius: 3px;
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
+}
+
+.search-container button svg {
+ position: absolute;
+ top: 4px;
+ left: 4px;
+}
+
+.search-container button:hover,
+.search-container button:focus {
+ background-color: #989898;
+}
+
+.search-container button:hover svg,
+.search-container button:focus svg {
+ color: #fff;
+}
+
+.search-container button:focus {
+ box-shadow: 0 0 0 3px rgba(35, 106, 151, 0.25);
+}
+
.search-input {
border: 1px solid #cfd3d7;
border-radius: 2px;
width: calc(100% - 10px);
margin-left: 10px;
margin-bottom: 10px;
- padding: 0 10px;
+ padding: 0 30px 0 10px;
font-size: 14px;
line-height: 30px;
outline: none;
}
.page-container {
+ width: 900px;
max-width: calc(100% - 220px);
min-width: 320px;
padding-left: 16px;