From 59843f9e3d222901421a92fff793a1e031f38f65 Mon Sep 17 00:00:00 2001
From: Xia Li-yao <Lysxia@users.noreply.github.com>
Date: Wed, 27 Feb 2019 21:53:27 -0500
Subject: Menu item controlling which instances are expanded/collapsed (#1007)

Adds a menu item (like "Quick Jump") for options related to displaying
instances. This provides functionality for:

  * expanding/collapsing all instances on the currently opened page
  * controlling whether instances are expanded/collapsed by default
  * controlling whether the state of instances should be "remembered"

This new functionality is implemented in Typescript in `details-helper`.
The built-in-themes style switcher also got a revamp so that all three
of QuickJump, the style switcher, and instance preferences now have
the same style and implementation structure.

See also: https://mail.haskell.org/pipermail/haskell-cafe/2019-January/130495.html

Fixes #698.

Co-authored-by: Lysxia <lysxia@gmail.com>
Co-authored-by: Nathan Collins <conathan@galois.com>
---
 haddock-api/resources/html/js-src/style-menu.tsx | 177 ++++++++++++-----------
 1 file changed, 93 insertions(+), 84 deletions(-)

(limited to 'haddock-api/resources/html/js-src/style-menu.tsx')

diff --git a/haddock-api/resources/html/js-src/style-menu.tsx b/haddock-api/resources/html/js-src/style-menu.tsx
index bab840ca..2eb8344e 100644
--- a/haddock-api/resources/html/js-src/style-menu.tsx
+++ b/haddock-api/resources/html/js-src/style-menu.tsx
@@ -4,91 +4,14 @@ import {getCookie, setCookie, clearCookie} from "./cookies";
 import preact = require("preact");
 
 const { h, Component } = preact;
-const rspace = /\s\s+/g,
-    rtrim = /^\s+|\s+$/g;
-
-function spaced(s: string) { return (" " + s + " ").replace(rspace, " "); }
-function trim(s: string)   { return s.replace(rtrim, ""); }
-
-function hasClass(elem: Element, value: string) {
-  const className = spaced(elem.className || "");
-  return className.indexOf( " " + value + " " ) >= 0;
-}
-
-function addClass(elem: Element, value: string) {
-  const className = spaced(elem.className || "");
-  if ( className.indexOf( " " + value + " " ) < 0 ) {
-    elem.className = trim(className + " " + value);
-  }
-}
-
-function removeClass(elem: Element, value: string) {
-  let className = spaced(elem.className || "");
-  className = className.replace(" " + value + " ", " ");
-  elem.className = trim(className);
-}
-
-function toggleClass(elem: Element, valueOn: string, valueOff: string, bool?: boolean): boolean {
-  if (bool == null) { bool = ! hasClass(elem, valueOn); }
-  if (bool) {
-    removeClass(elem, valueOff);
-    addClass(elem, valueOn);
-  }
-  else {
-    removeClass(elem, valueOn);
-    addClass(elem, valueOff);
-  }
-  return bool;
-}
-
-function makeClassToggle(valueOn: string, valueOff: string): (elem: Element, bool?: boolean) => boolean {
-  return function(elem, bool) {
-    return toggleClass(elem, valueOn, valueOff, bool);
-  }
-}
-
-const toggleShow = makeClassToggle("show", "hide");
 
+// Get all of the styles that are available
 function styles(): HTMLLinkElement[] {
   const es = Array.prototype.slice.call(document.getElementsByTagName("link"));
   return es.filter((a: HTMLLinkElement) => a.rel.indexOf("style") != -1 && a.title);
 }
 
-class StyleMenuButton extends Component<any, any> {
-
-  render(props: { stys: string[] }) {
-    function action() {
-      styleMenu();
-      return false;
-    };
-
-    return <li><div id='style-menu-holder' onClick={action}>
-      <a href='#'>Style &#9662;</a>
-      <ul id='style-menu' class='hide'>
-        {props.stys.map((sty) => {
-              function action() {
-                setActiveStyleSheet(sty);
-                return false;
-              };
-
-              return <li><a href='#' onClick={action}>{sty}</a></li>;
-        })}
-      </ul>
-    </div></li>;
-  }
-
-}
-
-function addStyleMenu() {
-  const stys = styles().map((s) => s.title);
-  if (stys.length > 1) {
-    const pageMenu = document.querySelector('#page-menu') as HTMLUListElement;
-    const dummy = document.createElement('li');
-    pageMenu.appendChild(dummy);
-    preact.render(<StyleMenuButton stys={stys} title="Style"/>, pageMenu, dummy);
-  }
-}
-
+// Set a style (including setting the cookie)
 function setActiveStyleSheet(title: string) {
   const as = styles();
   let found: null | HTMLLinkElement = null;
@@ -110,17 +33,103 @@ function setActiveStyleSheet(title: string) {
   }
 }
 
+// Reset the style based on the cookie
 function resetStyle() {
   const s = getCookie("haddock-style");
   if (s) setActiveStyleSheet(s);
 }
 
-function styleMenu(show?: boolean) {
-  const m = document.getElementById('style-menu');
-  if (m) toggleShow(m, show);
+class StylesButton extends Component<any, any> {
+  render(props: { title: string, onClick: () => void }) {
+    function onClick(e: Event) {
+      e.preventDefault();
+      props.onClick();
+    }
+    return <li><a href="#" onClick={onClick}>{props.title}</a></li>;
+  }
 }
 
-export function init() {
-  addStyleMenu();
+// Add the style menu button
+function addStyleMenu(stys: string[], action: () => void) {
+  if (stys.length > 1) {
+    const pageMenu = document.querySelector('#page-menu') as HTMLUListElement;
+    const dummy = document.createElement('li');
+    pageMenu.appendChild(dummy);
+    preact.render(<StylesButton onClick={action} title="Styles"/>, pageMenu, dummy);
+  }
+}
+
+type StyleProps = {
+  styles: string[]
+  showHideTrigger: (action: () => void) => void
+}
+
+type StyleState = {
+  isVisible: boolean
+}
+
+// Represents the full style dropdown
+class Styles extends Component<StyleProps, StyleState> {
+
+  componentWillMount() {
+    document.addEventListener('mousedown', this.hide.bind(this));
+
+    document.addEventListener('keydown', (e) => {
+      if (this.state.isVisible) {
+        if (e.key === 'Escape') {
+          this.hide();
+        }
+      }
+    })
+  }
+
+  hide() {
+    this.setState({ isVisible: false });
+  }
+
+  show() {
+    if (!this.state.isVisible) {
+      this.setState({ isVisible: true });
+    }
+  }
+
+  toggleVisibility() {
+    if (this.state.isVisible) {
+      this.hide();
+    } else {
+      this.show();
+    }
+  }
+
+  componentDidMount() {
+    this.props.showHideTrigger(this.toggleVisibility.bind(this));
+  }
+
+  render(props: StyleProps, state: StyleState) {
+    const stopPropagation = (e: Event) => { e.stopPropagation(); };
+
+    return <div id="style" class={state.isVisible ? '' : 'hidden'}>
+        <div id="style-menu" class="dropdown-menu" onMouseDown={stopPropagation}>
+          {
+            props.styles.map((sty) =>
+              <button type="button"
+                      onClick={(e) => { this.hide(); setActiveStyleSheet(sty) }}>
+                {sty}
+              </button>
+            )
+          }
+        </div>
+      </div>;
+  }
+}
+
+
+export function init(showHide?: (action: () => void) => void) {
+  const stys = styles().map((s) => s.title);
+  const addStylesButton = (action: () => void) => addStyleMenu(stys, action)
   resetStyle();
+  preact.render(
+    <Styles showHideTrigger={showHide || addStylesButton} styles={stys} />,
+    document.body
+  );
 }
-- 
cgit v1.2.3

