Javascript for info-style navigation - Week 10 & 11

by Mathieu Lirzin — mar. 15 août 2017

This is an update on the work I am doing this summer for my Google Summer of Code. Please see the first article of this serie of reports for a general introduction on what this project is about.

Welcome message

One element of info user interface is the presence of the welcome message indicating the user how to get help. This little thing can be really important for novice users to discover the keyboard shortcuts.

In the HTML version, we already have a ? icon which allows an help screen to popup, in order to be more explicit and more close to the original info behavior, I have added a welcome message in the HTML too.

Message queues without explicit state

In the report of week 7 & 8 I described a mechanism for ensuring that a message sent to an iframe was received. The idea was to create a dictionnary object pending_messages to keep track of messages not ready to be sent to their iframes and an event listener on the load event sending those waiting messages. Here is an example code of such idea:

var pending_messages = {};

function
get_iframe (id)
{
  var iframe = document.getElementById (id);
  if (!iframe)
    {
      iframe = document.createElement ("iframe");
      iframe.setAttribute ("id", id);
      iframe.setAttribute ("src", linkid_to_url (id));
      document.appendChild (iframe);
      iframe.addEventListener ("load", function () {
        /* Send pending messages.  */
        var msgs = resolve_page.pendings[id];
        if (msgs)
          {
            for (var i = 0; i < msgs.length; i += 1)
              this.contentWindow.postMessage (msgs[i], "*");
          }
        pending_messages[id] = false;
      }, false);
    }
  return iframe;
}

function
send_message (id, msg)
{
  var iframe = get_iframe (id);
  if (pending_messages[id] === false)
    iframe.contentWindow.postMessage (msg, "*");
  else if (pending_messages.hasOwnProperty (id))
    pending_messages[id].push (msg);
  else
    pending_messages[id] = [msg];
}

While this works nice it is a bit complex since it requires to manage the global state of pending_messages explicitly. So I have replaced this solution with something more simple.

var iframe_ready = {}

function
get_iframe (id)
{
  var iframe = document.getElementById (id);
  if (!iframe)
    {
      iframe = document.createElement ("iframe");
      iframe.setAttribute ("id", id);
      iframe.setAttribute ("src", linkid_to_url (id));
      document.appendChild (iframe);
      iframe.addEventListener ("load", function () {
        iframe_ready[id] = true;
      }, false);
    }
  return iframe;
}

function
send_message (id, msg)
{
  var iframe = get_iframe (id);
  if (iframe_ready[id])
    iframe.contentWindow.postMessage (msg, "*");
  else
    {
      iframe.addEventListener ("load", function handler () {
        this.contentWindow.postMessage (msg, "*");
        this.removeEventListener ("load", handler, false);
      }, false);
    }
}

The delayed send of messages is managed directly in the closure of the load event handler that unregisters itself after being called. The only difference is that the order in which the waiting messages will be received is undefined in this implementation. However, In our use case the order doesn't matter.

Global search

The index search using the i key has already been implemented. A complementary command to search throught the manual is the global search which is accessible with the s key in info-mode and info. We have ported that feature to the Web UI which popups a text input in the top right of the screen like for the index search.

Porting such feature to the Web UI was not trivial. The main difficulty is that the content is distributed across multiple HTML pages whereas the info format includes its content in a single file. So the Web UI needs to load pages in the background in their corresponding iframe and search in each of them until one matches. What make this difficult is that all of this has to been done with an asynchronous interface using the Message API while ensuring a particular order of events. Fortunately the fact we are using the Elm architecture via an implementation inspired by Redux to manage the state of the application, has make the work on that feature more approchable by discretizing each search step of the search as a separate Action. Once the different states were defined we could more easily described the transition from one state to the other.

Once the search is done and if a page matches the search, we make this page the current one, highlight the result and scroll to it. If no page match the query then we report that the search failed to the user. The following image shows a positive result of the emacs search in the GNU Hello manual.

The remaining features that need to be implemented are the full support of regular expression, and the incremental search which allows the user to walk through all the results by pressing s and enter again.

Javascript features and portability

During that coding period I have discovered two nice standard features that are portable enough to be used. The first is Element.classList which is nice alternative to Element.setAttribute since it allows to easily add and remove multiple classes for the same element, instead of having to relying on string manipulation. The second is Element.remove which can replace a call of Node.removeChildon the parentElement. Element.remove is not that portable but is available with a short polyfill.

On the portability side, I have discovered that Array.from is not available on Internet Explorer. Since the corresponding polyfill was to heavy to include, and that it was used to ensure that a forEach exists, I decided to simply use basic for loops instead.

Next Step

I am actually already working on porting the Javascript code to work with the HTML generated by makeinfo --html. More details will be given next time.

Follow the developpement

I have updated the live demo of the Kawa manual which is available here. If you have already accessed this page, it is possible that you face invalid cache issues. Make sure that your local cache is cleared.

The development of this project is done in public. You can checkout the "js" directory in the "gsoc-2017" branch of the Git repository and run the build instructions from the README to see what is the current state of the project.