2 Commits 5b950d6210 ... 160485341d

Author SHA1 Message Date
  Mathieu Lirzin 160485341d main: Add Text_input component 6 years ago
  Mathieu Lirzin 809c133b80 store: Call listeners only if state has changed 6 years ago
7 changed files with 162 additions and 15 deletions
  1. 18 0
      js/src/actions.js
  2. 12 8
      js/src/index.js
  3. 6 2
      js/src/main.js
  4. 24 0
      js/src/reducers.js
  5. 6 2
      js/src/store.js
  6. 91 0
      js/src/text_input.js
  7. 5 3
      js/src/utils.js

+ 18 - 0
js/src/actions.js

@@ -43,3 +43,21 @@ cache_links (links)
 {
   return { type: CACHE_LINKS, links };
 }
+
+export const SHOW_COMPONENT = "show-component";
+
+/** Toggle the visibility of COMPONENT.  */
+export function
+show_component (component)
+{
+  return { type: SHOW_COMPONENT, component };
+}
+
+export const HIDE_COMPONENT = "hide-component";
+
+/** Toggle the visibility of COMPONENT.  */
+export function
+hide_component (component)
+{
+  return { type: HIDE_COMPONENT, component };
+}

+ 12 - 8
js/src/index.js

@@ -74,18 +74,22 @@ on_unload ()
 function
 on_keypress (event)
 {
-  let direction = on_keypress.dict[event.key];
-  if (direction)
-    iframe_dispatch (actions.navigate (direction));
+  let value = on_keypress.dict[event.key];
+  if (value)
+    {
+      let [action, arg] = value;
+      iframe_dispatch (action (arg));
+    }
 }
 
 /* Dictionary associating an Event 'key' property to its navigation id.  */
 on_keypress.dict = {
-  n: "next",
-  p: "prev",
-  u: "up",
-  "]": "forward",
-  "[": "backward"
+  m: [actions.show_component, "menu"],
+  n: [actions.navigate, "next"],
+  p: [actions.navigate, "prev"],
+  u: [actions.navigate, "up"],
+  "]": [actions.navigate, "forward"],
+  "[": [actions.navigate, "backward"]
 };
 
 /*--------------------

+ 6 - 2
js/src/main.js

@@ -20,6 +20,7 @@ import * as actions from "./actions";
 import { Pages } from "./iframe";
 import { Sidebar } from "./sidebar";
 import { Store } from "./store";
+import { Text_input } from "./text_input";
 import config from "./config";
 import { fix_links } from "./toc";
 import { global_reducer } from "./reducers";
@@ -71,6 +72,7 @@ on_load ()
   components.element = document.body;
   components.add (new Sidebar ());
   components.add (new Pages (index_div));
+  components.add (new Text_input ("menu"));
 
   let initial_state = {
     /* Dictionary associating page ids to next, prev, up, forward,
@@ -79,7 +81,9 @@ on_load ()
     /* page id of the current page.  */
     current: config.INDEX_ID,
     /* Define if the sidebar iframe is loaded.  */
-    sidebar_loaded: false
+    sidebar_loaded: false,
+    /* Define if the sidebar iframe is loaded.  */
+    text_input_visible: false
   };
 
   store = new Store (global_reducer, initial_state);
@@ -89,7 +93,7 @@ on_load ()
   if (window.location.hash)
     store.dispatch (actions.set_current_url (window.location.hash.slice (1)));
 
-  /* Retrieve NEXT link.  */
+  /* Retrieve NEXT link and local menu.  */
   let links = {};
   links[config.INDEX_ID] = navigation_links (document);
   store.dispatch (actions.cache_links (links));

+ 24 - 0
js/src/reducers.js

@@ -19,8 +19,10 @@
 import {
   CACHE_LINKS,
   CURRENT_URL,
+  HIDE_COMPONENT,
   INIT,
   NAVIGATE,
+  SHOW_COMPONENT,
   SIDEBAR_LOADED
 } from "./actions";
 
@@ -46,6 +48,7 @@ global_reducer (state, action)
     case CURRENT_URL:
       {
         let res = Object.assign ({}, state, { current: action.url, action });
+        res.text_input_visible = false;
         if (!res.loaded_nodes[action.url])
           res.loaded_nodes[action.url] = {};
         return res;
@@ -59,11 +62,32 @@ global_reducer (state, action)
         else
           {
             let res = Object.assign ({}, state, { current: linkid, action });
+            res.text_input_visible = false;
             if (!Object.keys (res.loaded_nodes).includes (action.url))
               res.loaded_nodes[action.url] = {};
             return res;
           }
       }
+    case SHOW_COMPONENT:
+      {
+        if (action.component !== "menu" || state.text_input_visible)
+          return state;
+        else
+          {
+            let text_input_visible = true;
+            return Object.assign ({}, state, { text_input_visible, action });
+          }
+      }
+    case HIDE_COMPONENT:
+      {
+        if (action.component !== "menu" || !state.text_input_visible)
+          return state;
+        else
+          {
+            let text_input_visible = false;
+            return Object.assign ({}, state, { text_input_visible, action });
+          }
+      }
     default:
       return state;
     }

+ 6 - 2
js/src/store.js

@@ -33,8 +33,12 @@ Store
       action (this.dispatch.bind (this));
     else
       {
-        this.state = this.reducer (this.state, action);
-        this.listeners.forEach (listener => listener ());
+        let new_state = this.reducer (this.state, action);
+        if (new_state !== this.state)
+          {
+            this.state = new_state;
+            this.listeners.forEach (listener => listener ());
+          }
       }
   }
 

+ 91 - 0
js/src/text_input.js

@@ -0,0 +1,91 @@
+/* text-input.js - Component for menu navigation
+   Copyright © 2017 Free Software Foundation, Inc.
+
+   This file is part of GNU Texinfo.
+
+   GNU Texinfo is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   GNU Texinfo 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 General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with GNU Texinfo.  If not, see <http://www.gnu.org/licenses/>.  */
+
+import * as actions from "./actions";
+import { iframe_dispatch } from "./store";
+
+export class
+Text_input
+{
+  constructor (id)
+  {
+    let div = document.createElement ("div");
+    div.setAttribute ("id", "menu-input");
+    div.setAttribute ("style", "background:pink;z-index:100;position:fixed");
+    div.setAttribute ("hidden", "true");
+    div.appendChild (document.createTextNode ("menu:"));
+
+    let input = document.createElement ("input");
+    input.setAttribute ("type", "search");
+    input.setAttribute ("list", "menu");
+    div.appendChild (input);
+
+    input.addEventListener ("keypress", event => {
+      if (event.key === "Escape")
+        iframe_dispatch (actions.hide_component (id));
+      else if (event.key === "Enter")
+        {
+          let linkid = this.current_menu[this.input.value];
+          if (linkid)
+            iframe_dispatch (actions.set_current_url (linkid));
+        }
+
+      /* Do not send key events to global "key navigation" handler.  */
+      event.stopPropagation ();
+    });
+
+    this.element = div;
+    this.input = input;
+    this.id = id;
+  }
+
+  render (state)
+  {
+    if (state.text_input_visible)
+      {
+        /* Create a datalist for the menu completions.  */
+        let datalist = document.createElement ("datalist");
+        datalist.setAttribute ("id", "menu");
+
+        let current_menu = state.loaded_nodes[state.current].menu;
+        if (current_menu)
+          {
+            this.current_menu = current_menu;
+            Object.keys (current_menu)
+                  .forEach (title => {
+                    let opt = document.createElement ("option");
+                    opt.setAttribute ("value", title);
+                    datalist.appendChild (opt);
+                  });
+          }
+
+        this.element.appendChild (datalist);
+        this.element.removeAttribute ("hidden");
+        this.input.focus ();
+      }
+    else
+      {
+        this.element.setAttribute ("hidden", "true");
+        this.input.value = "";
+        /* Remove the datalist if found.  */
+        let datalist = this.element.querySelector ("datalist");
+        if (datalist)
+          datalist.parentNode.removeChild (datalist);
+      }
+  }
+}

+ 5 - 3
js/src/utils.js

@@ -69,14 +69,14 @@ href_hash (href)
     return "";
 }
 
-/** Retrieve PREV, NEXT, and UP links from CONTENT and Return an object
-    containing references to those links.  CONTENT must be an object
+/** Retrieve PREV, NEXT, and UP links, and local menu from CONTENT and return
+    an object containing references to those links.  CONTENT must be an object
     implementing the ParentNode interface (Element, Document...).  */
 export function
 navigation_links (content)
 {
   let links = content.querySelectorAll ("footer a");
-  let res = {};
+  let res = { menu: {} };
   /* links have the form MAIN_FILE.html#FRAME-ID.  For convenience
      only store FRAME-ID.  */
   for (let i = 0; i < links.length; i += 1)
@@ -85,6 +85,8 @@ navigation_links (content)
       let nav_id = navigation_links.dict[link.getAttribute ("accesskey")];
       if (nav_id)
         res[nav_id] = href_hash (link.getAttribute ("href"));
+      else                  /* this link is part of local table of content. */
+        res.menu[link.text] = href_hash (link.getAttribute ("href"));
     }
 
   return res;