5 Commits db7c6e76ea ... 3d70ba8bcd

Author SHA1 Message Date
  Mathieu Lirzin 3d70ba8bcd iframe: Fix links to iframe anchors 6 years ago
  Mathieu Lirzin dcb7701e96 utils: Fix a bug in 'href_hash' 6 years ago
  Mathieu Lirzin c68d5f7941 Refactor to move mutation of the DOM inside the components 6 years ago
  Mathieu Lirzin 7f1cb9af61 sidebar: Instantiate the iframe inside 'Sidebar' constructor 6 years ago
  Mathieu Lirzin be5ce7d37d main: Remove 'Selected_div' class 6 years ago
8 changed files with 172 additions and 212 deletions
  1. 8 12
      js/src/actions.js
  2. 94 5
      js/src/iframe.js
  3. 7 12
      js/src/index.js
  4. 17 139
      js/src/main.js
  5. 23 11
      js/src/reducers.js
  6. 15 30
      js/src/sidebar.js
  7. 4 2
      js/src/toc.js
  8. 4 1
      js/src/utils.js

+ 8 - 12
js/src/actions.js

@@ -16,28 +16,24 @@
    You should have received a copy of the GNU General Public License
    along with GNU Texinfo.  If not, see <http://www.gnu.org/licenses/>.  */
 
-export const LOAD_PAGE = "load-page";
+export const INIT = "init";
 
-export function
-load_page (page)
-{
-  return { type: LOAD_PAGE, page };
-}
+export const SIDEBAR_LOADED = "sidebar-ready";
 
-export const PAGE_READY = "page-ready";
+export const CURRENT_URL = "current-url";
 
 export function
-page_ready (page)
+set_current_url (url)
 {
-  return { type: PAGE_READY, page };
+  return { type: CURRENT_URL, url };
 }
 
-export const CURRENT_URL = "current-url";
+export const NAVIGATE = "navigate";
 
 export function
-set_current_url (url)
+navigate (direction)
 {
-  return { type: CURRENT_URL, url };
+  return { type: NAVIGATE, direction };
 }
 
 export const CACHE_LINKS = "cache-links";

+ 94 - 5
js/src/iframe.js

@@ -17,11 +17,25 @@
    along with GNU Texinfo.  If not, see <http://www.gnu.org/licenses/>.  */
 
 import * as actions from "./actions";
-import { basename, href_hash, navigation_links } from "./utils";
+import { basename, navigation_links } from "./utils";
 import config from "./config";
 import { fix_links } from "./toc";
 import { iframe_dispatch } from "./store";
 
+/* Return an array composed of the filename and the anchor of LINKID.
+   LINKID can have the form "foobar.anchor" or just "foobar".  */
+function
+linkid_split (linkid)
+{
+  if (!linkid.includes ("."))
+    return [linkid, ""];
+  else
+    {
+      let [id, anchor] = linkid.match (/^(.+)\.(.*)$/).slice (1);
+      return [id, "#" + anchor];
+    }
+}
+
 export class
 Pages
 {
@@ -29,17 +43,92 @@ Pages
   {
     index_div.setAttribute ("id", config.INDEX_ID);
     index_div.setAttribute ("node", config.INDEX_ID);
+    index_div.setAttribute ("hidden", "true");
     this.element = document.createElement ("div");
     this.element.setAttribute ("id", "sub-pages");
     this.element.appendChild (index_div);
+    /* Currently created divs.  */
+    this.ids = [];
+  }
+
+  add_divs (linkids)
+  {
+    for (let i = 0; i < linkids.length; i += 1)
+      {
+        let linkid = linkids[i];
+        this.ids.push (linkid);
+        /* Don't create a div if there is an anchor part in LINKID.  */
+        if (!linkid.includes ("."))
+          {
+            let div = document.createElement ("div");
+            div.setAttribute ("id", linkid);
+            div.setAttribute ("node", linkid);
+            div.setAttribute ("hidden", "true");
+            this.element.appendChild (div);
+          }
+      }
+  }
+
+  static load_iframe (linkid)
+  {
+    let path = window.location.pathname + window.location.search;
+
+    if (linkid === config.INDEX_ID)
+      {
+        window.history.pushState ("", document.title, path);
+        return;
+      }
+
+    let [pageid, hash] = linkid_split (linkid);
+    let div = document.getElementById (pageid);
+    if (!div)
+      {
+        let msg = "no iframe container correspond to identifier: " + pageid;
+        throw new ReferenceError (msg);
+      }
+
+    path = path.replace (/#.*/, "") + "#" + linkid;
+    let url = pageid + ".xhtml" + hash;
+
+    /* Select contained iframe or create it if necessary.  */
+    let iframe = div.querySelector ("iframe");
+    if (iframe === null)
+      {
+        iframe = document.createElement ("iframe");
+        iframe.setAttribute ("class", "node");
+        iframe.setAttribute ("name", path);
+        iframe.setAttribute ("src", url);
+        div.appendChild (iframe);
+      }
+    else
+      {
+        let msg = { message_kind: "scroll-to", url: hash };
+        iframe.contentWindow.postMessage (msg, "*");
+      }
+
+    window.history.pushState ("", document.title, path);
   }
 
   render (state)
   {
-    if (state === this.state)
-      return;
+    let new_linkids = Object.keys (state.loaded_nodes)
+                            .filter (id => !this.ids.includes (id));
+    this.add_divs (new_linkids);
+
+    if (state.current !== this.prev_id)
+      {
+        if (this.prev_id)
+          this.prev_div.setAttribute ("hidden", "true");
 
-    this.state = state;
+        let [pageid] = linkid_split (state.current);
+        let div = document.getElementById (pageid);
+        if (!div)
+          throw new Error ("no div with id: " + pageid);
+        Pages.load_iframe (state.current);
+        div.removeAttribute ("hidden");
+        this.prev_id = state.current;
+        this.prev_div = div;
+      }
   }
 }
 
@@ -67,7 +156,7 @@ on_message (event)
   switch (data.message_kind)
     {
     case "scroll-to":
-      window.location.hash = href_hash (data.url);
+      window.location.hash = data.url;
       break;
     default:
       break;

+ 7 - 12
js/src/index.js

@@ -29,11 +29,13 @@
     converter.  */
 
 import "./polyfill";
+import * as actions from "./actions";
 import * as main from "./main";
 import * as pages from "./iframe";
 import * as sidebar from "./sidebar";
 import { absolute_url_p, href_hash } from "./utils";
 import config from "./config";
+import { iframe_dispatch } from "./store";
 import { with_sidebar_query } from "./toc";
 
 /*-------------------------
@@ -50,15 +52,8 @@ on_click (event)
           let href = target.getAttribute ("href");
           if (!absolute_url_p (href))
             {
-              let url = href_hash (href) || config.INDEX_ID;
-              if (url.includes ("."))
-                url = url.replace (/[.]/, ".xhtml#");
-              else
-                url += ".xhtml";
-              let hash = href.replace (/.*#/, "#");
-              if (hash === config.INDEX_NAME)
-                hash = "";
-              top.postMessage ({ message_kind: "load-page", url, hash }, "*");
+              let linkid = href_hash (href) || config.INDEX_ID;
+              iframe_dispatch (actions.set_current_url (linkid));
               event.preventDefault ();
               event.stopPropagation ();
               return;
@@ -79,9 +74,9 @@ on_unload ()
 function
 on_keypress (event)
 {
-  let nav = on_keypress.dict[event.key];
-  if (nav)
-    top.postMessage ({ message_kind: "load-page", nav }, "*");
+  let direction = on_keypress.dict[event.key];
+  if (direction)
+    iframe_dispatch (actions.navigate (direction));
 }
 
 /* Dictionary associating an Event 'key' property to its navigation id.  */

+ 17 - 139
js/src/main.js

@@ -33,15 +33,13 @@ let components = {
   element: null,
   components: [],
 
-  register (id, component)
+  add (component)
   {
     if (this.element === null)
       throw new Error ("element property must be set first");
 
-    this[id] = component;
     this.components.push (component);
-    if (component.element)
-      this.element.appendChild (component.element);
+    this.element.appendChild (component.element);
   },
 
   render (state)
@@ -50,94 +48,19 @@ let components = {
   }
 };
 
-export class
-Selected_div
-{
-  constructor (id)
-  {
-    this.id = id;
-    this.element = null;
-  }
-
-  render (state)
-  {
-    if (state.current === this.id)
-      return;
-
-    if (this.element)
-      this.element.setAttribute ("hidden", "true");
-    let div = document.getElementById (state.current);
-    div.removeAttribute ("hidden");
-
-    this.id = state.current;
-    this.element = div;
-  }
-}
-
-/*------------------------------------------------
-| Auxilary functions for the top-level context.  |
-`-----------------------------------------------*/
-
-/* Return an array compose of the filename and the anchor of NODE_NAME.
-   NODE_NAME can have the form "foobaridm80837412374" or just "foobar".  */
-function
-split_id_anchor (node_name)
-{
-  let rgxp = /idm\d+$/;
-  if (!rgxp.test (node_name))
-    return [node_name, ""];
-  else
-    {
-      let [id, anchor] = node_name.match (/^(.+)(idm\d+)$/).slice (1);
-      return [id, "#" + anchor];
-    }
-}
-
-/* Load URL in its corresponding iframe and make this iframe visible.  Display
-   HASH in the url bar.  */
-function
-load_page (url, hash)
-{
-  var node_name = url.replace (/[.]x?html.*/, "");
-  var path =
-      (window.location.pathname + window.location.search).replace (/#.*/, "")
-      + hash;
-  let [id] = split_id_anchor (node_name);
-  let div = document.getElementById (id);
-  if (!div)
-    {
-      let msg = `no iframe container correspond to identifier "${id}"`;
-      throw new ReferenceError (msg);
-    }
-
-  /* Select contained iframe or create it if necessary.  */
-  let iframe = div.querySelector ("iframe");
-  if (iframe === null)
-    {
-      iframe = document.createElement ("iframe");
-      iframe.setAttribute ("class", "node");
-      iframe.setAttribute ("name", path);
-      iframe.setAttribute ("src", url);
-      div.appendChild (iframe);
-    }
-  else
-    {
-      let msg = { message_kind: "scroll-to", url };
-      iframe.contentWindow.postMessage (msg, "*");
-    }
-
-  window.history.pushState ("", document.title, path);
-  store.dispatch (actions.set_current_url (node_name));
-}
-
 /*--------------------------------------------
 | Event handlers for the top-level context.  |
 `-------------------------------------------*/
 
+/* eslint-disable no-console */
+
 /* Initialize the top level 'config.INDEX_NAME' DOM.  */
 export function
 on_load ()
 {
+  fix_links (document.links);
+  document.body.setAttribute ("class", "mainbar");
+
   /* Move contents of <body> into a a fresh <div> to let the components
      treat the index page like other iframe page.  */
   let index_div = document.createElement ("div");
@@ -146,27 +69,25 @@ on_load ()
 
   /* Instanciate the components.  */
   components.element = document.body;
-  components.register ("sidebar", new Sidebar ());
-  components.register ("selected_div", new Selected_div ());
-  components.register ("pages", new Pages (index_div));
+  components.add (new Sidebar ());
+  components.add (new Pages (index_div));
 
   let initial_state = {
     /* Dictionary associating page ids to next, prev, up, forward,
        backward link ids.  */
     loaded_nodes: {},
     /* page id of the current page.  */
-    current: config.INDEX_ID
+    current: config.INDEX_ID,
+    /* Define if the sidebar iframe is loaded.  */
+    sidebar_loaded: false
   };
 
   store = new Store (global_reducer, initial_state);
   store.subscribe (() => components.render (store.state));
-
-  /* eslint-disable no-console */
   store.subscribe (() => console.log ("state: ", store.state));
-  /* eslint-enable no-console */
 
-  fix_links (document.links);
-  document.body.setAttribute ("class", "mainbar");
+  if (window.location.hash)
+    store.dispatch (actions.set_current_url (window.location.hash.slice (1)));
 
   /* Retrieve NEXT link.  */
   let links = {};
@@ -177,50 +98,7 @@ on_load ()
 export function
 on_message (event)
 {
-  let data = event.data;
-  switch (data.message_kind)
-    {
-    case "action":            /* Handle actions sent from iframes.  */
-      store.dispatch (data.action);
-      break;
-    case "node-list":
-      {
-        let nodes = Object.keys (store.state.loaded_nodes);
-        for (var i = 0; i < nodes.length; i += 1)
-          {
-            let name = nodes[i];
-            if (name === config.INDEX_ID)
-              continue;
-            let div = document.createElement ("div");
-            div.setAttribute ("id", name);
-            div.setAttribute ("node", name);
-            div.setAttribute ("hidden", "true");
-            document.querySelector ("#sub-pages").appendChild (div);
-          }
-        if (window.location.hash)
-          {
-            let hash = window.location.hash;
-            let url = (hash.includes (".")) ?
-                hash.replace (/#(.*)[.](.*)/, "$1.xhtml#$2") :
-                hash.replace (/#/, "") + ".xhtml";
-            load_page (url, hash);
-          }
-        break;
-      }
-    case "load-page":
-      {
-        if (!data.nav)          /* not a navigation link */
-          load_page (data.url, data.hash);
-        else
-        {
-          let ids = store.state.loaded_nodes[store.state.current];
-          let link_id = ids[data.nav];
-          if (link_id)
-            load_page (link_id + ".xhtml", "#" + link_id);
-        }
-        break;
-      }
-    default:
-      break;
-    }
+  /* Handle actions sent from iframes.  */
+  if (event.data.message_kind === "action")
+    store.dispatch (event.data.action);
 }

+ 23 - 11
js/src/reducers.js

@@ -19,8 +19,9 @@
 import {
   CACHE_LINKS,
   CURRENT_URL,
-  LOAD_PAGE,
-  PAGE_READY
+  INIT,
+  NAVIGATE,
+  SIDEBAR_LOADED
 } from "./actions";
 
 export function
@@ -28,6 +29,10 @@ global_reducer (state, action)
 {
   switch (action.type)
     {
+    case INIT:
+      return Object.assign ({}, state, { action });
+    case SIDEBAR_LOADED:
+      return Object.assign ({}, state, { sidebar_loaded: true, action });
     case CACHE_LINKS:
       {
         let clone = Object.assign ({}, state.loaded_nodes);
@@ -39,18 +44,25 @@ global_reducer (state, action)
         return Object.assign ({}, state, { loaded_nodes: clone, action });
       }
     case CURRENT_URL:
-      return Object.assign ({}, state, { current: action.url, action });
-    case LOAD_PAGE:
       {
-        let loaded_nodes = Object.assign ({}, state.loaded_nodes);
-        loaded_nodes[action.page] = { id: action.page, status: "loading" };
-        return Object.assign ({}, state, { loaded_nodes, action });
+        let res = Object.assign ({}, state, { current: action.url, action });
+        if (!res.loaded_nodes[action.url])
+          res.loaded_nodes[action.url] = {};
+        return res;
       }
-    case PAGE_READY:
+    case NAVIGATE:
       {
-        let loaded_nodes = Object.assign ({}, state.loaded_nodes);
-        loaded_nodes[action.page] = { id: action.page, status: "loaded" };
-        return Object.assign ({ action }, state, { loaded_nodes, action });
+        let ids = state.loaded_nodes[state.current];
+        let linkid = ids[action.direction];
+        if (!linkid)
+          return state;
+        else
+          {
+            let res = Object.assign ({}, state, { current: linkid, action });
+            if (!Object.keys (res.loaded_nodes).includes (action.url))
+              res.loaded_nodes[action.url] = {};
+            return res;
+          }
       }
     default:
       return state;

+ 15 - 30
js/src/sidebar.js

@@ -34,41 +34,26 @@ Sidebar
 {
   constructor ()
   {
-    /* Actual state of the sidebar.  */
-    this.state = null;
-    /* Reference to the DOM element.  */
     this.element = document.createElement ("div");
     this.element.setAttribute ("id", "sidebar");
+
+    /* Create iframe. */
+    let iframe = document.createElement ("iframe");
+    iframe.setAttribute ("name", "slider");
+    iframe.setAttribute ("src", (config.TOC_FILENAME
+                                 + "#main=" + config.INDEX_NAME));
+    this.element.appendChild (iframe);
+    this.iframe = iframe;
   }
 
   /* Render 'sidebar' according to STATE which is a new state. */
   render (state)
   {
-    if (this.state && this.state !== state)
-      this.update (state);
-    else                        /* Initial render */
-      {
-        let iframe = document.createElement ("iframe");
-        iframe.setAttribute ("name", "slider");
-        iframe.setAttribute ("src", (config.TOC_FILENAME
-                                     + "#main=" + config.INDEX_NAME));
-        this.element.appendChild (iframe);
-      }
-
-    this.state = state;
-  }
-
-  /* Updating render.  */
-  update (state)
-  {
-    if (state.current !== this.state.current)
-      {
-        /* Update sidebar to highlight the title corresponding to
-           'state.current'.  */
-        let iframe = this.element.querySelector ("iframe");
-        let msg = { message_kind: "update-sidebar", selected: state.current };
-        iframe.contentWindow.postMessage (msg, "*");
-      }
+    /* Update sidebar to highlight the title corresponding to
+       'state.current'.  */
+    let msg = { message_kind: "update-sidebar", selected: state.current };
+    this.iframe.contentWindow.postMessage (msg, "*");
+    this.prev = state.current;
   }
 }
 
@@ -113,6 +98,8 @@ add_header ()
 export function
 on_load ()
 {
+  iframe_dispatch ({ type: actions.SIDEBAR_LOADED });
+
   add_header ();
   document.body.setAttribute ("class", "toc-sidebar");
 
@@ -151,8 +138,6 @@ on_load ()
   /* Get 'backward' and 'forward' link attributes.  */
   let dict = create_link_dict (document.querySelector ("ul"));
   iframe_dispatch (actions.cache_links (dict));
-  /* Create iframe divs in 'config.MAIN_NAME'.  */
-  top.postMessage ({ message_kind: "node-list" }, "*");
 }
 
 /** Handle messages received via the Message API.  */

+ 4 - 2
js/src/toc.js

@@ -38,8 +38,10 @@ with_sidebar_query (href)
   else
     {
       let url = new window.URL (href, window.location);
-      let hash = basename (url.pathname, /[.]x?html/) + url.hash.slice (1);
-      return config.INDEX_NAME + "#" + hash;
+      let new_hash = basename (url.pathname, /[.]x?html/);
+      if (url.hash)
+        new_hash += ("." + url.hash.slice (1));
+      return config.INDEX_NAME + "#" + new_hash;
     }
 }
 

+ 4 - 1
js/src/utils.js

@@ -63,7 +63,10 @@ href_hash (href)
   if (typeof href !== "string")
     throw new TypeError (href + " is not a string");
 
-  return href.replace (/.*#/, "");
+  if (href.includes ("#"))
+    return href.replace (/.*#/, "");
+  else
+    return "";
 }
 
 /** Retrieve PREV, NEXT, and UP links from CONTENT and Return an object