8 Commits 697b162c81 ... c103e72033

Author SHA1 Message Date
  Mathieu Lirzin c103e72033 toc: Use constants instead of hard-coded file names 6 years ago
  Mathieu Lirzin 93864ac58f toc: Move 'fix_link' inside 'fix_links' scope 6 years ago
  Mathieu Lirzin ef7b3dcabf index: Use 'store.state.loaded_nodes' for creating iframe containers 6 years ago
  Mathieu Lirzin 9cdbda57c6 toc: Add 'fix_links' function from 'index.js' 6 years ago
  Mathieu Lirzin a4ed95916a index: Handle "DOMContentLoaded" instead of "load" events 6 years ago
  Mathieu Lirzin 2c925db12b store: Add 'iframe_dispatch' function 6 years ago
  Mathieu Lirzin 6fcaf4dbc4 toc: Remove 'main_filename' global 6 years ago
  Mathieu Lirzin 2278cd1ebd index: Refactor using 'Main' and 'Pages' components 6 years ago
6 changed files with 160 additions and 139 deletions
  1. 75 0
      js/src/component.js
  2. 2 0
      js/src/config.js
  3. 25 75
      js/src/index.js
  4. 16 44
      js/src/sidebar.js
  5. 16 1
      js/src/store.js
  6. 26 19
      js/src/toc.js

+ 75 - 0
js/src/component.js

@@ -0,0 +1,75 @@
+/* component.js - Composable stateful views that render state objects */
+
+import { Sidebar } from "./sidebar";
+
+import config from "./config";
+
+export class
+Selected_div
+{
+  constructor (id)
+  {
+    this.id = id;
+    this.element = null;
+  }
+
+  render (id)
+  {
+    if (id === this.id)
+      return;
+
+    if (this.element)
+      this.element.setAttribute ("hidden", "true");
+    let div = document.getElementById (id);
+    div.removeAttribute ("hidden");
+
+    this.id = id;
+    this.element = div;
+  }
+};
+
+export class
+Pages
+{
+  constructor (index_div)
+  {
+    index_div.setAttribute ("id", config.INDEX_ID);
+    index_div.setAttribute ("node", config.INDEX_ID);
+    this.element = document.createElement ("div");
+    this.element.setAttribute ("id", "sub-pages");
+    this.element.appendChild (index_div);
+  }
+
+  render (state)
+  {
+    if (state == this.state)
+      return;
+
+    this.state = state;
+  }
+}
+
+export class
+Main_component
+{
+  constructor (root, index_div)
+  {
+    let sidebar = new Sidebar ();
+    root.appendChild (sidebar.element);
+    let pages = new Pages (index_div);
+    root.appendChild (pages.element);
+
+    /* Root DOM element.  */
+    this.root = root;
+    /* Instance of a Sidebar object.  */
+    this.sidebar = sidebar;
+    /* Currently visible page.  */
+    this.selected_div = new Selected_div ();
+  }
+
+  render (state)
+  {
+    this.sidebar.render ({ current: config.INDEX_ID, visible: true });
+    this.selected_div.render (state.current);
+  }
+}

+ 2 - 0
js/src/config.js

@@ -7,6 +7,8 @@ export default {
 
   INDEX_NAME: "index.html",
 
+  OVERALL_INDEX_NAME: "Overall-Index.xhtml",
+
   INDEX_ID: "index",
 
   PACKAGE_LOGO: "kawa-logo.png"

+ 25 - 75
js/src/index.js

@@ -13,6 +13,7 @@
 
 import * as actions from "./actions";
 import * as sidebar from "./sidebar";
+import { Store, iframe_dispatch } from "./store";
 
 import {
   absolute_url_p,
@@ -23,70 +24,37 @@ import {
 
 import {
   clear_toc_styles,
-  fix_link,
-  main_filename,
+  fix_links,
   scan_toc
 } from "./toc";
 
-import Store from "./store";
+import { Main_component } from "./component";
 import config from "./config";
 import { global_reducer } from "./reducers";
 import polyfill from "./polyfill";
 
 /* Global state manager.  */
 let store;
-
-let selected_div = {
-  id: null,
-  element: null,
-
-  render (new_id)
-  {
-    if (new_id === this.id)
-      return;
-
-    if (this.element)
-      this.element.setAttribute ("hidden", "true");
-    var div = document.getElementById (new_id);
-    div.removeAttribute ("hidden");
-
-    this.id = new_id;
-    this.element = div;
-  }
-};
-
-let components = {
-  /* Instance of a Sidebar object.  */
-  sidebar: null,
-  /* Currently visible page.  */
-  selected_div
-};
+/* Main stateful view.   */
+let components;
 
 /* Initialize the top level 'config.INDEX_NAME' DOM.  */
 function
 on_index_load (_event)
 {
-  main_filename.val = basename (window.location.pathname);
+  fix_links (document.links);
+  document.body.setAttribute ("class", "mainbar");
 
-  /* Move contents of <body> into a a fresh <div>.  */
-  var body = document.body;
-  var div = document.createElement ("div");
-  div.setAttribute ("id", config.INDEX_ID);
-  div.setAttribute ("node", config.INDEX_ID);
-  for (let ch = body.firstChild; ch !== null; ch = body.firstChild)
-    div.appendChild (ch);
-  body.appendChild (div);
+  /* 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");
+  for (let ch = document.body.firstChild; ch; ch = document.body.firstChild)
+    index_div.appendChild (ch);
 
-  if (sidebar.use_sidebar (window.location.hash))
-    {
-      let sbi = new sidebar.Sidebar ();
-      sbi.render ({ current: config.INDEX_ID, visible: true });
-      document.body.insertBefore (sbi.element, document.body.firstChild);
-      document.body.setAttribute ("class", "mainbar");
-      components.sidebar = sbi;
-    }
+  /* Instanciate the components.  */
+  components = new Main_component (document.body, index_div);
 
-  fix_links (document.links);
+  /* Retrieve NEXT link.  */
   let links = {};
   links[config.INDEX_ID] = navigation_links (document);
   store.dispatch (actions.cache_links (links));
@@ -97,28 +65,11 @@ on_index_load (_event)
 function
 on_iframe_load (_event)
 {
-  main_filename.val = basename (window.name, /#.*/);
   fix_links (document.links);
   let links = {};
   let url = basename (window.location.pathname, /[.]x?html$/);
   links[url] = navigation_links (document);
-  let action = actions.cache_links (links);
-  top.postMessage ({ message_kind: "action", action }, "*");
-}
-
-/* Modify LINKS to handle the iframe based navigation properly.
-   relative links will be opened inside the corresponding iframe and
-   absolute links will be opened in a new tab.  LINKS must be an array
-   or a collection of nodes.  */
-function
-fix_links (links)
-{
-  for (let i = 0; i < links.length; i += 1)
-    {
-      let href = links[i].getAttribute ("href");
-      if (href)
-        fix_link (links[i], href);
-    }
+  iframe_dispatch (actions.cache_links (links));
 }
 
 /* Retrieve PREV, NEXT, and UP links and Return a object containing references
@@ -181,7 +132,7 @@ load_page (url, hash)
     }
 
   let msg = { message_kind: "update-sidebar", selected: node_name };
-  components.sidebar.element.contentWindow.postMessage (msg, "*");
+  components.sidebar.get_iframe_window ().postMessage (msg, "*");
   window.history.pushState ("", document.title, path);
   store.dispatch (actions.set_current_url (node_name));
 }
@@ -197,16 +148,17 @@ receive_message (event)
       break;
     case "node-list":           /* from sidebar to top frame */
       {
-        for (var i = 0; i < data.length; i += 1)
+        let nodes = Object.keys (store.state.loaded_nodes);
+        for (var i = 0; i < nodes.length; i += 1)
           {
-            let name = data[i];
+            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.body.appendChild (div);
+            document.querySelector ("#sub-pages").appendChild (div);
           }
         if (window.location.hash)
           {
@@ -313,7 +265,7 @@ if (inside_iframe_p () || inside_index_page_p (window.location.pathname))
 
   if (!inside_iframe_p ())
     {
-      window.addEventListener ("load", on_index_load, false);
+      window.addEventListener ("DOMContentLoaded", on_index_load, false);
       window.addEventListener ("message", receive_message, false);
 
       let initial_state = {
@@ -326,18 +278,16 @@ if (inside_iframe_p () || inside_index_page_p (window.location.pathname))
 
       store = new Store (global_reducer, initial_state);
       store.subscribe (() => console.log ("state: ", store.state));
-      store.subscribe (() => {
-        components.selected_div.render (store.state.current);
-      });
+      store.subscribe (() => components.render (store.state));
     }
   else if (window.name == "slider")
     {
-      window.addEventListener ("load", sidebar.on_load, false);
+      window.addEventListener ("DOMContentLoaded", sidebar.on_load, false);
       window.addEventListener ("message", sidebar.on_message, false);
     }
   else
     {
-      window.addEventListener ("load", on_iframe_load, false);
+      window.addEventListener ("DOMContentLoaded", on_iframe_load, false);
       window.addEventListener ("message", receive_message, false);
     }
 

+ 16 - 44
js/src/sidebar.js

@@ -5,14 +5,12 @@ import * as actions from "./actions";
 import {
   clear_toc_styles,
   create_link_dict,
-  fix_link,
-  main_filename,
+  fix_links,
   scan_toc
 } from "./toc";
 
-import { absolute_url_p } from "./utils";
-
 import config from "./config";
+import { iframe_dispatch } from "./store";
 
 /** Sidebar component encapsulating the iframe and its state.  */
 export class
@@ -23,7 +21,8 @@ Sidebar
     /* Actual state of the sidebar.  */
     this.state = null;
     /* Reference to the DOM element.  */
-    this.element = null;
+    this.element = document.createElement ("div");
+    this.element.setAttribute ("id", "sidebar");
   }
 
   /* Render 'sidebar' according to STATE which is a new state. */
@@ -36,8 +35,8 @@ Sidebar
         let iframe = document.createElement ("iframe");
         iframe.setAttribute ("name", "slider");
         iframe.setAttribute ("src", (config.TOC_FILENAME
-                                     + "#main=" + main_filename.val));
-        this.element = iframe;
+                                     + "#main=" + config.INDEX_NAME));
+        this.element.appendChild (iframe);
       }
 
     this.state = state;
@@ -52,6 +51,11 @@ Sidebar
         this.element.contentWindow.postMessage (msg, "*");
       }
   }
+
+  get_iframe_window ()
+  {
+    return this.element.firstChild.contentWindow;
+  }
 }
 
 /** Return true if the side bar containing the table of content should be
@@ -100,33 +104,6 @@ add_header ()
     }
 }
 
-/* Return an Array with all the relative links of the table of content.
-   Exclude the hash part and the file extension from the links.  */
-function
-relative_links (links)
-{
-  let nodes = [];
-  let prev_node = null;
-  links.forEach (link => {
-    let href = link.getAttribute ("href");
-    if (href)
-      {
-        fix_link (link, href);
-        if (!absolute_url_p (href))
-          {
-            let node_name = href.replace (/[.]x?html.*/, "");
-            if (prev_node != node_name)
-              {
-                prev_node = node_name;
-                nodes.push (node_name);
-              }
-          }
-      }
-  });
-
-  return nodes;
-}
-
 /*-----------------------------------------
 | Event handlers for the iframe context.  |
 `----------------------------------------*/
@@ -136,8 +113,6 @@ relative_links (links)
 export function
 on_load (_event)
 {
-  /* Retrieve 'INDEX_NAME' from current window URL. */
-  main_filename.val = window.location.href.replace (/.*#main=/, "");
   add_header ();
   document.body.setAttribute ("class", "toc-sidebar");
 
@@ -149,6 +124,7 @@ on_load (_event)
   document.head.appendChild (base);
 
   let links = Array.from (document.links);
+  fix_links (links);
 
   /* Create a link referencing the Table of content.  */
   let toc_a = document.createElementNS (config.XHTML_NAMESPACE, "a");
@@ -163,12 +139,7 @@ on_load (_event)
     index_li = index_grand;
   index_li.parentNode.insertBefore (toc_li, index_li.nextSibling);
 
-  if (main_filename.val !== null)
-    scan_toc (document.body, main_filename.val);
-
-  let nodes = relative_links ([...links, toc_a]);
-  nodes.message_kind = "node-list";
-  top.postMessage (nodes, "*");
+  scan_toc (document.body, config.INDEX_NAME);
 
   let divs = Array.from (document.querySelectorAll ("div"));
   divs.reverse ()
@@ -179,8 +150,9 @@ on_load (_event)
 
   /* Get 'backward' and 'forward' link attributes.  */
   let dict = create_link_dict (document.querySelector ("ul"));
-  let action = actions.cache_links (dict);
-  top.postMessage ({ message_kind: "action", action }, "*");
+  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.  */

+ 16 - 1
js/src/store.js

@@ -1,6 +1,11 @@
 /* store.js - module for managing the state */
 
-export default class
+export default {
+  Store,
+  iframe_dispatch
+};
+
+export class
 Store
 {
   constructor (reducer, state = {})
@@ -33,3 +38,13 @@ Store
     });
   }
 }
+
+/** Dispatch ACTION to the top-level browsing context.  This function must be
+    used in conjunction with an event listener on "message" events in the
+    top-level browsing context which must forwards ACTION to an actual
+    store.  */
+export function
+iframe_dispatch (action)
+{
+  top.postMessage ({ message_kind: "action", action }, "*");
+}

+ 26 - 19
js/src/toc.js

@@ -3,14 +3,10 @@
 import { absolute_url_p, basename, depth_first_walk } from "./utils";
 import config from "./config";
 
-/** Object storing the value of the */
-export var main_filename = { val: null };
-
 /* Adapt HREF to refer to an anchor in index file.  HREF must be a string
    representing an absolute or relative URL.
 
-   For example if 'main_filename.val' value is "index.html", "foo/bar.html"
-   will be replaced by "index.html#bar".  */
+   For example "foo/bar.html" will be replaced by "config.INDEX_NAME#bar".  */
 var with_sidebar_query = (function () {
   /* DOM element used to access the HTMLAnchorElement interface.  */
   let node = document.createElement ("a");
@@ -18,12 +14,10 @@ var with_sidebar_query = (function () {
   return function (href) {
     node.href = href;
     var node_name = basename (node.pathname, /[.]x?html/);
-    if (href == main_filename.val
-        || href == config.INDEX_NAME
-        || node_name == "start")
-      return main_filename.val;
+    if (href == config.INDEX_NAME || node_name == "start")
+      return config.INDEX_NAME;
     else
-      return main_filename.val + "#" + node_name + node.hash.slice (1);
+      return config.INDEX_NAME + "#" + node_name + node.hash.slice (1);
   };
 } ());
 
@@ -38,7 +32,7 @@ hide_grand_child_nodes (ul)
       let a = li.firstElementChild;
       let li$ = a && a.nextElementSibling;
       /* Never remove Overall-Index.  */
-      if (li$ && a.getAttribute ("href") != "Overall-Index.xhtml")
+      if (li$ && a.getAttribute ("href") != config.OVERALL_INDEX_NAME)
         li$.setAttribute ("toc-detail", "yes");
     }
 }
@@ -49,7 +43,7 @@ scan_toc (node, filename)
 {
   var current = with_sidebar_query (filename);
   var ul = node.querySelector ("ul");
-  if (filename == "index.html")
+  if (filename == config.INDEX_NAME)
     hide_grand_child_nodes (ul);
   else
     scan_toc1 (ul, current);
@@ -142,13 +136,26 @@ create_link_dict (nav)
   return links;
 }
 
-/** If LINK is absolute ensure it is opened in a new tab.  Otherwise modify
-    the 'href' attribute to adapt to the iframe based navigation.  */
+/** Modify LINKS to handle the iframe based navigation properly.  Relative
+    links will be opened inside the corresponding iframe and absolute links
+    will be opened in a new tab.  LINKS must be an array or a collection of
+    nodes.  */
 export function
-fix_link (link, href)
+fix_links (links)
 {
-  if (absolute_url_p (href))
-    link.setAttribute ("target", "_blank");
-  else
-    link.setAttribute ("href", with_sidebar_query (href));
+  function
+  fix_link (link, href)
+  {
+    if (absolute_url_p (href))
+      link.setAttribute ("target", "_blank");
+    else
+      link.setAttribute ("href", with_sidebar_query (href));
+  }
+
+  for (let i = 0; i < links.length; i += 1)
+    {
+      let href = links[i].getAttribute ("href");
+      if (href)
+        fix_link (links[i], href);
+    }
 }