From 75ac4a37f2faece59951606d86006c098bdb02d2 Mon Sep 17 00:00:00 2001 From: Pascal Mugnier Date: Tue, 11 Sep 2018 14:56:10 +0200 Subject: [PATCH] MMF-1377 Fix issues and improve UI (#692) --- .../src/layouts/components/HeadingsLink.js | 64 ++++++++++++++----- .../src/layouts/components/Search.js | 43 +++++++++---- .../src/layouts/components/Sidebar.js | 10 +-- .../src/layouts/components/icons/ClearIcon.js | 32 ++++++++++ server/sonar-docs/src/templates/page.css | 42 +++++++++++- 5 files changed, 157 insertions(+), 34 deletions(-) create mode 100644 server/sonar-docs/src/layouts/components/icons/ClearIcon.js diff --git a/server/sonar-docs/src/layouts/components/HeadingsLink.js b/server/sonar-docs/src/layouts/components/HeadingsLink.js index d7cf430ac01..6abd72ceee3 100644 --- a/server/sonar-docs/src/layouts/components/HeadingsLink.js +++ b/server/sonar-docs/src/layouts/components/HeadingsLink.js @@ -20,15 +20,47 @@ 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'); @@ -38,38 +70,33 @@ export default class HeadingsLink extends React.Component { 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; } @@ -80,7 +107,10 @@ export default class HeadingsLink extends React.Component { {headers.map((header, index) => { return (
  • - + {header.value}
  • diff --git a/server/sonar-docs/src/layouts/components/Search.js b/server/sonar-docs/src/layouts/components/Search.js index 569bf4c4eb9..05b31278f4e 100644 --- a/server/sonar-docs/src/layouts/components/Search.js +++ b/server/sonar-docs/src/layouts/components/Search.js @@ -19,13 +19,16 @@ */ 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 }); @@ -37,7 +40,7 @@ export default class Search extends Component { this.add({ id: page.id, title: page.frontmatter.title, - text: page.html.replace(/<(?:.|\n)*?>/gm, '') + text: page.html.replace(/<(?:.|\n)*?>/gm, '').replace(/<(?:.|\n)*?>/gm, '') }) ); }); @@ -66,7 +69,7 @@ export default class Search extends Component { 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 @@ -74,25 +77,43 @@ export default class Search extends Component { }); }; + 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 ( - +
    + (this.input = node)} + type="search" + value={this.state.value} + /> + {this.state.value && ( + + )} +
    ); } } diff --git a/server/sonar-docs/src/layouts/components/Sidebar.js b/server/sonar-docs/src/layouts/components/Sidebar.js index cf68d376f42..a9cf8668d29 100644 --- a/server/sonar-docs/src/layouts/components/Sidebar.js +++ b/server/sonar-docs/src/layouts/components/Sidebar.js @@ -27,7 +27,7 @@ import Search from './Search'; 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(); @@ -74,8 +74,8 @@ export default class Sidebar extends React.PureComponent { ); }; - handleSearch = results => { - this.setState({ results }); + handleSearch = (results, query) => { + this.setState({ results, query }); }; render() { @@ -111,8 +111,8 @@ export default class Sidebar extends React.PureComponent {
    - {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 => ( + + + ); +} diff --git a/server/sonar-docs/src/templates/page.css b/server/sonar-docs/src/templates/page.css index ddc3612e66d..09c716aa2fe 100644 --- a/server/sonar-docs/src/templates/page.css +++ b/server/sonar-docs/src/templates/page.css @@ -131,13 +131,52 @@ body > div, } } +.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; @@ -288,6 +327,7 @@ a.search-result .note { } .page-container { + width: 900px; max-width: calc(100% - 220px); min-width: 320px; padding-left: 16px; -- 2.39.5