]> source.dussan.org Git - sonarqube.git/commitdiff
MMF-1377 Fix issues and improve UI (#692)
authorPascal Mugnier <pascal.mugnier@sonarsource.com>
Tue, 11 Sep 2018 12:56:10 +0000 (14:56 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 19 Sep 2018 08:51:38 +0000 (10:51 +0200)
server/sonar-docs/src/layouts/components/HeadingsLink.js
server/sonar-docs/src/layouts/components/Search.js
server/sonar-docs/src/layouts/components/Sidebar.js
server/sonar-docs/src/layouts/components/icons/ClearIcon.js [new file with mode: 0644]
server/sonar-docs/src/templates/page.css

index d7cf430ac01797d807f92693cced7e77900b67ba..6abd72ceee33504ca4818b4ac9e403406749f38c 100644 (file)
 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 (
               <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>
index 569bf4c4eb9957fabf311585ab980c7a273d8834..05b31278f4e1ce5f6965352b3dff1cedbad7485b 100644 (file)
  */
 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(/&#x3C;(?:.|\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(/&#x3C;(?:.|\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 (
-      <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>
     );
   }
 }
index cf68d376f42feb940b713bba8c3730282c86191b..a9cf8668d2900c6967564c0b222b3d55ad06729e 100644 (file)
@@ -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 {
         </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}
diff --git a/server/sonar-docs/src/layouts/components/icons/ClearIcon.js b/server/sonar-docs/src/layouts/components/icons/ClearIcon.js
new file mode 100644 (file)
index 0000000..1e179fa
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * 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>
+  );
+}
index ddc3612e66d293e36d009163899ee5474d9f57b4..09c716aa2fefe7b2021fdddbf535520363a447be 100644 (file)
@@ -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;