| /* info.js - Javascript UI for Texinfo manuals | |
| Copyright (C) 2017-2026 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 <https://www.gnu.org/licenses/>. */ | |
| /* Note that the entire body of this file apart from the first and last | |
| lines are within a function call. */ | |
| (function (features, user_config) { | |
| "use strict"; | |
| /*-------------------. | |
| | Define constants. | | |
| `-------------------*/ | |
| /** To override those default parameters, define 'INFO_CONFIG' in the global | |
| environment before loading this script. */ | |
| var config = { | |
| EXT: ".html", | |
| TOP_NAME: "index.html", | |
| TOP_ID: "index", | |
| CONTENTS_ID: "SEC_Contents", | |
| MAIN_ANCHORS: ["Top"], | |
| WARNING_TIMEOUT: 3000, | |
| SCREEN_MIN_WIDTH: 700, | |
| LOCAL_HTML_PAGE_PATTERN: "^([^:/]*[.](html|htm|xhtml))?([#].*)?$", | |
| SHOW_SIDEBAR_HTML: '<span class="hide-icon">⇛</span>', | |
| HIDE_SIDEBAR_HTML: '<span class="hide-icon">⇚</span><span class="hide-text"></span>', | |
| SHOW_SIDEBAR_TOOLTIP: 'Show navigation sidebar', | |
| HIDE_SIDEBAR_TOOLTIP: 'Hide navigation sidebar', | |
| // hooks: | |
| /** Define a function called after 'DOMContentLoaded' event in | |
| the TOP_NAME context. | |
| @type {function (): void}*/ | |
| on_main_load: null, | |
| /** Define a function called after 'DOMContentLoaded' event in | |
| the iframe context. | |
| @type {(function (): void)} */ | |
| on_iframe_load: null, | |
| DEBUG: 0, | |
| }; | |
| /*-------------------. | |
| | State Management. | | |
| `-------------------*/ | |
| /** A 'store' is an object managing the state of the application and having | |
| a dispatch method which accepts actions as parameter. This method is | |
| the only way to update the state. | |
| @typedef {function (Action): void} Action_consumer | |
| @type {{dispatch: Action_consumer, state?: any}}. */ | |
| var store; | |
| var section_names = [ | |
| 'top', 'chapter', 'unnumbered', 'chapheading', 'appendix', | |
| 'section', 'unnumberedsec', 'heading', 'appendixsec', | |
| 'subsection', 'unnumberedsubsec', 'subheading', 'appendixsubsec', | |
| 'subsubsection', 'unnumberedsubsubsec', 'subsubheading', | |
| 'appendixsubsubsec' ]; | |
| /** Create a store. | |
| @arg {Object} state */ | |
| function | |
| Store (state) | |
| { | |
| this.components = []; | |
| this.state = state; | |
| } | |
| /** @arg {Action} action */ | |
| Store.prototype.dispatch = function dispatch (action) { | |
| var new_state = updater (this.state, action); | |
| if (new_state !== this.state) | |
| { | |
| this.state = new_state; | |
| if (window["INFO_DEBUG"]) | |
| console.log ("state: ", new_state); | |
| this.components.forEach (function (cmpt) { cmpt.render (new_state); }); | |
| /* Ensure that focus is on the current node unless some component is | |
| focused. */ | |
| if (!new_state.focus) | |
| { | |
| var link = linkid_split (new_state.current); | |
| var elem = document.getElementById (link.pageid); | |
| if (link.pageid !== config.TOP_ID && link.pageid !== config.CONTENTS_ID) | |
| elem.querySelector ("iframe").focus (); | |
| else | |
| { | |
| /* Move the focus to top DOM. */ | |
| document.documentElement.focus (); | |
| /* Allow the spacebar scroll in the main page to work. */ | |
| elem.focus (); | |
| } | |
| } | |
| } | |
| }; | |
| /** Build a store delegate that will forward its actions to a "real" | |
| store that can be in a different browsing context. */ | |
| function | |
| Remote_store () | |
| { | |
| /* The browsing context containing the real store. */ | |
| this.delegate = window.parent; | |
| } | |
| /** Dispatch ACTION to the delegate browing context. This method must be | |
| used in conjunction of an event listener on "message" events in the | |
| delegate browsing context which must forwards ACTION to an actual store. | |
| @arg {Action} action */ | |
| Remote_store.prototype.dispatch = function dispatch (action) { | |
| this.delegate.postMessage ({ message_kind: "action", action: action }, "*"); | |
| }; | |
| /** @typedef {{type: string, [x: string]: any}} Action - Payloads | |
| of information meant to be treated by the store which can receive them | |
| using the 'store.dispatch' method. */ | |
| /* Place holder for action creators. */ | |
| var actions = { | |
| /** Return an action that makes LINKID the current page. | |
| @arg {string} linkid - link identifier | |
| @arg {string|false} [history] - method name that will be applied on | |
| the 'window.history' object. */ | |
| set_current_url: function (linkid, history, clicked) { | |
| if (undef_or_null (history)) | |
| history = "pushState"; | |
| return { type: "current-url", url: linkid, | |
| history: history, clicked: clicked }; | |
| }, | |
| /** Set current URL to the node corresponding to POINTER which is an | |
| id refering to a linkid (such as "*TOP*" or "*END*"). */ | |
| set_current_url_pointer: function (pointer) { | |
| return { type: "current-url", pointer: pointer, history: "pushState" }; | |
| }, | |
| /** @arg {string} dir */ | |
| navigate: function (dir) { | |
| return { type: "navigate", direction: dir, history: "pushState" }; | |
| }, | |
| /** @arg {{[id: string]: string}} links */ | |
| cache_links: function (links) { | |
| return { type: "cache-links", links: links }; | |
| }, | |
| /* Send list of index entries. */ | |
| cache_index_links: function (dict) { | |
| return { type: "cache-index-links", links: dict }; | |
| }, | |
| /** Show or hide the help screen. */ | |
| show_help: function () { | |
| return { type: "help", visible: true }; | |
| }, | |
| /** Make the text input INPUT visible. If INPUT is a falsy value then | |
| hide current text input. | |
| @arg {string} input */ | |
| show_text_input: function (input) { | |
| return { type: "input", input: input }; | |
| }, | |
| /** Hide the current current text input. */ | |
| hide_text_input: function () { | |
| return { type: "input", input: null }; | |
| }, | |
| /** @arg {string} msg */ | |
| warn: function (msg) { | |
| return { type: "warning", msg: msg }; | |
| }, | |
| window_title: function (title) { | |
| return { type: "window-title", title: title }; | |
| }, | |
| window_title_of_linkid: function (title, linkid) { | |
| return { type: "window-title-of-linkid", title: title, linkid: linkid }; | |
| }, | |
| /** Search EXP in the whole manual. | |
| @arg {RegExp|string} exp*/ | |
| search: function (exp) { | |
| var rgxp; | |
| if (typeof exp === "object") | |
| rgxp = exp; | |
| else if (exp === "") | |
| rgxp = null; | |
| else | |
| rgxp = new RegExp (exp, "i"); | |
| return { type: "search-init", regexp: rgxp, input: exp }; | |
| } | |
| }; | |
| /** Update STATE based on the type of ACTION. This update is purely | |
| functional since STATE is not modified in place and a new state object | |
| is returned instead. | |
| @arg {Object} state | |
| @arg {Action} action | |
| @return {Object} a new state */ | |
| /* eslint-disable complexity */ | |
| /* A "big" switch has been preferred over splitting each case in a separate | |
| function combined with a dictionary lookup. */ | |
| function | |
| updater (state, action) | |
| { | |
| var result = Object.assign ({}, state, { action: action }); | |
| var linkid; | |
| debug (action.type); | |
| switch (action.type) | |
| { | |
| case "cache-links": | |
| { | |
| var nodes = Object.assign ({}, state.loaded_nodes); | |
| Object | |
| .keys (action.links) | |
| .forEach (function (key) { | |
| if (typeof action.links[key] === "object") | |
| nodes[key] = Object.assign ({}, nodes[key], action.links[key]); | |
| else | |
| nodes[key] = action.links[key]; | |
| }); | |
| return Object.assign (result, { loaded_nodes: nodes }); | |
| } | |
| case "cache-index-links": | |
| { | |
| // Initially result.TI_index is undefined, which is ignored. | |
| result.TI_index = Object.assign ({}, result.TI_index, action.links); | |
| return result; | |
| } | |
| case "section": | |
| { | |
| result.section_hash = action.section_hash; | |
| return result; | |
| } | |
| case "current-url": | |
| { | |
| if (document.body.getAttribute("show-sidebar") == "yes" | |
| && is_narrow_window () | |
| && action.clicked === "in-page") | |
| return state; | |
| linkid = (action.pointer) ? | |
| state.loaded_nodes[action.pointer] : action.url; | |
| debug (":linkid: " + linkid); | |
| if (action.pointer) | |
| debug (":pointer: " + action.pointer); | |
| result.current = linkid; | |
| result.section_hash = null; | |
| result.history = action.history; | |
| result.text_input = null; | |
| result.warning = null; | |
| result.window_title = null; | |
| result.help = false; | |
| result.focus = false; | |
| result.highlight = null; | |
| result.loaded_nodes = Object.assign ({}, result.loaded_nodes); | |
| result.loaded_nodes[linkid] = result.loaded_nodes[linkid] || {}; | |
| return result; | |
| } | |
| case "unfocus": | |
| { | |
| if (!result.focus) | |
| return state; | |
| else | |
| { | |
| result.help = false; | |
| result.text_input = null; | |
| result.focus = false; | |
| return result; | |
| } | |
| } | |
| case "help": | |
| { | |
| result.help = action.visible; | |
| result.focus = true; | |
| return result; | |
| } | |
| case "show-sidebar": // "yes" (show), "no" (hide) or "hide-if-narrow" | |
| { | |
| result.show_sidebar = action.show === "hide-if-narrow" && result.show_sidebar === "no" ? "no" : action.show; | |
| return result; | |
| } | |
| case "navigate": | |
| { | |
| var current = state.current; | |
| var link = linkid_split (state.current); | |
| /* Handle inner 'config.TOP_NAME' anchors specially. */ | |
| if (link.pageid === config.TOP_ID) | |
| current = config.TOP_ID; | |
| var ids = state.loaded_nodes[current]; | |
| linkid = ids[action.direction]; | |
| if (!linkid) | |
| { | |
| // Look for sibling link in ToC. | |
| // Needed for (say) @subsection without corresponding @node. | |
| let toc_current = | |
| document.querySelector ('#slider a[toc-current="yes"]'); | |
| if (toc_current) | |
| { | |
| let item_current = toc_current.parentNode; // 'li' element | |
| let nlink = (action.direction === "next" | |
| ? item_current.nextElementSibling | |
| : action.direction === "prev" | |
| ? item_current.previousElementSibling | |
| : action.direction === "up" | |
| ? item_current.parentNode.parentNode | |
| : null); | |
| if (nlink) | |
| nlink = nlink.firstElementChild; | |
| if (nlink && nlink.matches("a")) | |
| { | |
| let href = link_href (nlink) | |
| if (href) | |
| linkid = href_hash (href) ; | |
| } | |
| } | |
| } | |
| if (!linkid) | |
| { | |
| /* When CURRENT is in index but doesn't have the requested | |
| direction, ask its corresponding 'pageid'. */ | |
| /* This checks if CURRENT is a value in state.TI_index for | |
| some key. */ | |
| var is_index_ref = | |
| Object.keys (state.TI_index) | |
| .reduce (function (acc, val) { | |
| return acc || state.TI_index[val] === current; | |
| }, false); | |
| if (is_index_ref) | |
| { | |
| ids = state.loaded_nodes[link.pageid]; | |
| linkid = ids[action.direction]; | |
| } | |
| } | |
| if (!linkid) | |
| return state; | |
| else | |
| { | |
| result.current = linkid; | |
| result.section_hash = null; | |
| result.history = action.history; | |
| result.text_input = null; | |
| result.warning = null; | |
| result.window_title = null; | |
| result.help = false; | |
| result.highlight = null; | |
| result.focus = false; | |
| result.loaded_nodes = Object.assign ({}, result.loaded_nodes); | |
| result.loaded_nodes[action.url] = result.loaded_nodes[action.url] || {}; | |
| return result; | |
| } | |
| } | |
| case "search-init": | |
| { | |
| result.search = { | |
| regexp: action.regexp || state.search.regexp, | |
| input: action.input || state.search.input, | |
| status: "ready", | |
| current_pageid: linkid_split (state.current).pageid, | |
| found: false | |
| }; | |
| result.focus = false; | |
| result.help = false; | |
| result.text_input = null; | |
| result.warning = null; | |
| return result; | |
| } | |
| case "search-query": | |
| { | |
| result.search = Object.assign ({}, state.search); | |
| result.search.status = "searching"; | |
| return result; | |
| } | |
| case "search-result": | |
| { | |
| result.search = Object.assign ({}, state.search); | |
| if (action.found) | |
| { | |
| result.search.status = "done"; | |
| result.search.found = true; | |
| result.current = result.search.current_pageid; | |
| result.section_hash = null; | |
| result.history = "pushState"; | |
| result.highlight = result.search.regexp; | |
| } | |
| else | |
| { | |
| var fwd = forward_pageid (state, state.search.current_pageid); | |
| if (fwd === null) | |
| { | |
| result.search.status = "done"; | |
| result.warning = "Search failed: \"" + result.search.input + "\""; | |
| result.highlight = null; | |
| } | |
| else | |
| { | |
| result.search.current_pageid = fwd; | |
| result.search.status = "ready"; | |
| } | |
| } | |
| return result; | |
| } | |
| case "input": | |
| { | |
| var needs_update = (state.text_input && !action.input) | |
| || (!state.text_input && action.input) | |
| || (state.text_input && action.input | |
| && state.text_input !== action.input); | |
| if (!needs_update) | |
| return state; | |
| else | |
| { | |
| result.focus = Boolean (action.input); | |
| result.help = false; | |
| result.text_input = action.input; | |
| result.warning = null; | |
| return result; | |
| } | |
| } | |
| case "iframe-ready": | |
| { | |
| result.ready = Object.assign ({}, result.ready); | |
| result.ready[action.id] = true; | |
| return result; | |
| } | |
| case "echo": | |
| { | |
| result.echo = action.msg; | |
| return result; | |
| } | |
| case "window-title": | |
| { | |
| result.window_title = action.title; | |
| return result; | |
| } | |
| case "window-title-of-linkid": | |
| { | |
| return result; | |
| } | |
| case "warning": | |
| { | |
| result.warning = action.msg; | |
| if (action.msg !== null) | |
| result.text_input = null; | |
| return result; | |
| } | |
| default: | |
| console.error ("no reducer for action type:", action.type); | |
| return state; | |
| } | |
| } | |
| /* eslint-enable complexity */ | |
| /*-----------------------. | |
| | Context initializers. | | |
| `-----------------------*/ | |
| /*-----------------------. | |
| | Top-level index page. | | |
| `-----------------------*/ | |
| /** Initialize the index page of the manual which manages the state of the | |
| application. */ | |
| function | |
| init_index_page () | |
| { | |
| /*--------------------------. | |
| | Component for help page. | | |
| `--------------------------*/ | |
| function | |
| Help_page () | |
| { | |
| this.id = "help-screen"; | |
| /* Create help div element.*/ | |
| var div = document.createElement ("div"); | |
| div.setAttribute ("hidden", "true"); | |
| div.classList.add ("modal"); | |
| div.innerHTML = "\ | |
| <div class=\"modal-content\">\ | |
| <span class=\"close\">×</span>\ | |
| <h2>Keyboard Shortcuts</h2>\ | |
| <table id=\"keyboard-shortcuts\">\ | |
| <tbody>\ | |
| <tr><th colspan=\"2\">Moving around</th></tr>\ | |
| <tr><td><code><b>n / p</b></code></td>\ | |
| <td>goto the next / previous section</td></tr>\ | |
| <tr><td><code><b>[ / ]</b></code></td>\ | |
| <td>goto the next / previous sibling</td></tr>\ | |
| <tr><td><code><b>< / ></b></code></td>\ | |
| <td>goto the first / last section</td></tr>\ | |
| <tr><td><code><b>t</b></code></td><td>goto the first section</td></tr>\ | |
| <tr><td><code><b>u</b></code></td>\ | |
| <td>go one level up (parent section)</td></tr>\ | |
| <tr><td><code><b>l / r</b></code></td>\ | |
| <td>go back / forward in history.</td></tr>\ | |
| </tbody>\ | |
| <tbody>\ | |
| <tr><th colspan=\"2\">Searching</th></tr>\ | |
| <tr><td><code><b>i</b></code></td><td>search in the index</td></tr>\ | |
| <tr><td><code><b>s</b></code></td>\ | |
| <td>global text search in the manual</td></tr>\ | |
| <tr><td><code><b>m</b></code></td>\ | |
| <td>search in current node menu</td></tr>\ | |
| </tbody>\ | |
| <tbody>\ | |
| <tr><th colspan=\"2\">Misc</th></tr>\ | |
| <tr><td><code><b>?</b></code></td><td>show this help screen</td></tr>\ | |
| <tr><td><code><b>Esc</b></code></td><td>hide this help screen</td></tr>\ | |
| </tbody>\ | |
| </table>\ | |
| </div>\ | |
| "; | |
| var span = div.querySelector (".close"); | |
| div.addEventListener ("click", function (event) { | |
| if (event.target === span || event.target === div) | |
| store.dispatch ({ type: "unfocus" }); | |
| }, false); | |
| this.element = div; | |
| } | |
| Help_page.prototype.render = function help_page_render (state) { | |
| if (!state.help) | |
| this.element.style.display = "none"; | |
| else | |
| { | |
| this.element.style.display = "block"; | |
| this.element.focus (); | |
| } | |
| }; | |
| /*---------------------------------. | |
| | Components for menu navigation. | | |
| `---------------------------------*/ | |
| /** @arg {string} id */ | |
| function | |
| Search_input (id) | |
| { | |
| this.id = id; | |
| this.render = null; | |
| this.prompt = document.createTextNode ("Text search" + ": "); | |
| /* Create input div element.*/ | |
| var div = document.createElement ("div"); | |
| div.setAttribute ("hidden", "true"); | |
| div.appendChild (this.prompt); | |
| this.element = div; | |
| /* Create input element.*/ | |
| var input = document.createElement ("input"); | |
| input.setAttribute ("type", "search"); | |
| this.input = input; | |
| this.element.appendChild (input); | |
| /* Define a special key handler when 'this.input' is focused and | |
| visible.*/ | |
| this.input.addEventListener ("keydown", (function (event) { | |
| if (is_escape_key (event.key)) | |
| store.dispatch ({ type: "unfocus" }); | |
| else if (event.key === "Enter") | |
| store.dispatch (actions.search (this.input.value)); | |
| event.stopPropagation (); | |
| }).bind (this)); | |
| } | |
| Search_input.prototype.show = function show () { | |
| /* Display previous search. */ | |
| var search = store.state.search; | |
| var input = search && (search.status === "done") && search.input; | |
| if (input) | |
| this.prompt.textContent = "Text search (default " + input + "): "; | |
| this.element.removeAttribute ("hidden"); | |
| this.input.focus (); | |
| }; | |
| Search_input.prototype.hide = function hide () { | |
| this.element.setAttribute ("hidden", "true"); | |
| this.input.value = ""; | |
| }; | |
| /** @arg {string} id */ | |
| function | |
| Text_input (id) | |
| { | |
| this.id = id; | |
| this.data = null; | |
| this.datalist = null; | |
| this.render = null; | |
| /* Create input div element.*/ | |
| var div = document.createElement ("div"); | |
| div.setAttribute ("hidden", "true"); | |
| div.appendChild (document.createTextNode (id + ": ")); | |
| this.element = div; | |
| /* Create input element.*/ | |
| var input = document.createElement ("input"); | |
| input.setAttribute ("type", "search"); | |
| input.setAttribute ("list", id + "-data"); | |
| this.input = input; | |
| this.element.appendChild (input); | |
| /* Define a special key handler when 'this.input' is focused and | |
| visible.*/ | |
| this.input.addEventListener ("keydown", (function (event) { | |
| if (is_escape_key (event.key)) | |
| store.dispatch ({ type: "unfocus" }); | |
| else if (event.key === "Enter") | |
| { | |
| var linkid = this.data[this.input.value]; | |
| if (linkid) | |
| { | |
| debug ("Text input set linkid " + linkid); | |
| hide_sidebar_if_narrow (); | |
| store.dispatch (actions.set_current_url (linkid)); | |
| } | |
| } | |
| event.stopPropagation (); | |
| }).bind (this)); | |
| } | |
| /* Display a text input for searching through DATA.*/ | |
| Text_input.prototype.show = function show (data) { | |
| if (!features || features.datalistelem) | |
| { | |
| var datalist = create_datalist (data); | |
| if (!document.getElementById (this.id + "-data")) | |
| { | |
| datalist.setAttribute ("id", this.id + "-data"); | |
| this.data = data; | |
| this.datalist = datalist; | |
| this.element.appendChild (datalist); | |
| } | |
| } | |
| this.element.removeAttribute ("hidden"); | |
| this.input.focus (); | |
| }; | |
| Text_input.prototype.hide = function hide () { | |
| this.element.setAttribute ("hidden", "true"); | |
| this.input.value = ""; | |
| if (this.datalist) | |
| { | |
| this.datalist.remove (); | |
| this.datalist = null; | |
| } | |
| }; | |
| function | |
| Minibuffer () | |
| { | |
| /* Create global container.*/ | |
| var elem = document.createElement ("div"); | |
| elem.classList.add ("text-input"); | |
| var TI_menu = new Text_input ("menu"); | |
| TI_menu.render = function (state) { | |
| if (state.text_input === "menu") | |
| { | |
| var current_menu = state.loaded_nodes[state.current].TI_menu; | |
| if (current_menu) | |
| this.show (current_menu); | |
| else | |
| store.dispatch (actions.warn ("No menu in this node")); | |
| } | |
| }; | |
| var TI_index = new Text_input ("index"); | |
| TI_index.render = function (state) { | |
| if (state.text_input === "index") { | |
| if (state.TI_index) | |
| this.show (state.TI_index); | |
| else | |
| store.dispatch (actions.warn ("No index in this document")) | |
| } | |
| }; | |
| var search = new Search_input ("regexp-search"); | |
| search.render = function (state) { | |
| if (state.text_input === "regexp-search") | |
| this.show (); | |
| }; | |
| elem.appendChild (TI_menu.element); | |
| elem.appendChild (TI_index.element); | |
| elem.appendChild (search.element); | |
| /* Create a container for warning when no menu in current page.*/ | |
| var warn$ = document.createElement ("div"); | |
| warn$.setAttribute ("hidden", "true"); | |
| elem.appendChild (warn$); | |
| this.element = elem; | |
| this.TI_menu = TI_menu; | |
| this.TI_index = TI_index; | |
| this.search = search; | |
| this.warn = warn$; | |
| this.toid = null; | |
| } | |
| Minibuffer.prototype.render = function minibuffer_render (state) { | |
| if (!state.warning) | |
| { | |
| this.warn.setAttribute ("hidden", "true"); | |
| this.toid = null; | |
| } | |
| else if (!this.toid) | |
| { | |
| console.warn (state.warning); | |
| var toid = window.setTimeout (function () { | |
| store.dispatch ({ type: "warning", msg: null }); | |
| }, config.WARNING_TIMEOUT); | |
| this.warn.innerHTML = state.warning; | |
| this.warn.removeAttribute ("hidden"); | |
| this.toid = toid; | |
| } | |
| if (!state.text_input || state.warning) | |
| { | |
| this.TI_menu.hide (); | |
| this.TI_index.hide (); | |
| this.search.hide (); | |
| } | |
| else | |
| { | |
| this.TI_index.render (state); | |
| this.TI_menu.render (state); | |
| this.search.render (state); | |
| } | |
| }; | |
| function | |
| Echo_area () | |
| { | |
| var elem = document.createElement ("div"); | |
| elem.classList.add ("echo-area"); | |
| elem.setAttribute ("hidden", "true"); | |
| this.element = elem; | |
| this.toid = null; | |
| } | |
| Echo_area.prototype.render = function echo_area_render (state) { | |
| if (!state.echo) | |
| { | |
| this.element.setAttribute ("hidden", "true"); | |
| this.toid = null; | |
| } | |
| else if (!this.toid) | |
| { | |
| console.info (state.echo); | |
| var toid = window.setTimeout (function () { | |
| store.dispatch ({ type: "echo", msg: null }); | |
| }, config.WARNING_TIMEOUT); | |
| this.element.innerHTML = state.echo; | |
| this.element.removeAttribute ("hidden"); | |
| this.toid = toid; | |
| } | |
| }; | |
| /*----------------------------. | |
| | Component for the sidebar. | | |
| `----------------------------*/ | |
| function | |
| Sidebar (contents_node) | |
| { | |
| this.element = document.createElement ("div"); // FIXME unneeded? | |
| this.element.setAttribute ("id", "slider"); | |
| var div = document.createElement ("div"); | |
| div.classList.add ("toc-sidebar"); | |
| var toc = document.querySelector ("#SEC_Contents"); | |
| toc.remove (); | |
| // delete short contents as the links from it go to the table of | |
| // contents, which is no longer in the page. | |
| var shorttoc = document.querySelector ("#SEC_Shortcontents"); | |
| if (shorttoc) | |
| shorttoc.remove (); | |
| // Like n.cloneNode, but also copy _href | |
| function cloneNode(n) | |
| { | |
| let r = n.cloneNode(false); | |
| for (let c = n.firstChild; c; c = c.nextSibling) | |
| { | |
| let d = cloneNode(c); | |
| let h = c._href; | |
| if (h) | |
| d._href = h; | |
| r.appendChild(d); | |
| } | |
| return r; | |
| } | |
| contents_node.appendChild(cloneNode(toc)); | |
| /* Remove table of contents header. */ | |
| toc = toc.querySelector(".contents"); // skip ToC header | |
| /* Move contents of <body> into a a fresh <div> to let the components | |
| treat the index page like other iframe page. */ | |
| var nav = document.createElement ("nav"); | |
| nav.classList.add ("contents"); | |
| for (var ch = toc.firstChild; ch; ch = toc.firstChild) | |
| nav.appendChild (ch); | |
| var div$ = document.createElement ("div"); | |
| div$.classList.add ("toc"); | |
| div$.appendChild (nav); | |
| div.appendChild (div$); | |
| this.element.appendChild (div); | |
| let header = document.createElement ("header"); | |
| div$.parentElement.insertBefore (header, div$); | |
| let hider = document.createElement ("button"); | |
| hider.classList.add ("sidebar-hider"); | |
| hider.innerHTML = config.HIDE_SIDEBAR_HTML; | |
| this.show_sidebar_button = hider; | |
| header.appendChild(hider); | |
| } | |
| /* Render 'sidebar' according to STATE which is a new state. */ | |
| Sidebar.prototype.render = function sidebar_render (state) { | |
| /* Update sidebar to highlight the title corresponding to | |
| 'state.current'.*/ | |
| let currently_showing = document.body.getAttribute("show-sidebar"); | |
| let show = state.show_sidebar; | |
| if (show == "hide-if-narrow") | |
| show = is_narrow_window() || currently_showing == "no" ? "no" : "yes"; | |
| if (show === undefined) | |
| show = "yes"; | |
| if (show !== currently_showing) | |
| { | |
| document.body.setAttribute("show-sidebar", show); | |
| this.show_sidebar_button.innerHTML = show == "yes" ? config.HIDE_SIDEBAR_HTML : config.SHOW_SIDEBAR_HTML; | |
| let tooltip = show == "yes" ? config.HIDE_SIDEBAR_TOOLTIP : config.SHOW_SIDEBAR_TOOLTIP; | |
| if (tooltip) | |
| this.show_sidebar_button.setAttribute("title", tooltip); | |
| else | |
| this.show_sidebar_button.removeAttribute("title"); | |
| } | |
| var msg = { message_kind: "update-sidebar", selected: state.current, section_hash: state.section_hash }; | |
| window.postMessage (msg, "*"); | |
| }; | |
| /*--------------------------. | |
| | Component for the pages. | | |
| `--------------------------*/ | |
| /** @arg {HTMLDivElement} index_div */ | |
| function | |
| Pages (index_div) | |
| { | |
| index_div.setAttribute ("id", config.TOP_ID); | |
| index_div.setAttribute ("node", config.TOP_ID); | |
| index_div.setAttribute ("hidden", "true"); | |
| this.element = document.createElement ("div"); | |
| this.element.setAttribute ("id", "sub-pages"); | |
| this.element.appendChild (index_div); | |
| /** @type {string[]} Currently created divs. */ | |
| this.ids = [config.TOP_ID]; | |
| /** @type {string} */ | |
| this.prev_id = null; | |
| /** @type {HTMLElement} */ | |
| this.prev_div = null; | |
| this.prev_search = null; | |
| this.main_title = document.title; | |
| } | |
| Pages.prototype.add_div = function add_div (pageid) { | |
| var div = document.createElement ("div"); | |
| div.setAttribute ("id", pageid); | |
| div.setAttribute ("node", pageid); | |
| div.setAttribute ("hidden", "true"); | |
| this.ids.push (pageid); | |
| this.element.appendChild (div); | |
| if (linkid_contains_index (pageid)) | |
| load_page (pageid); | |
| return div; | |
| }; | |
| Pages.prototype.render = function pages_render (state) { | |
| var that = this; | |
| /* Create div elements for pages corresponding to newly added | |
| linkids from 'state.loaded_nodes'.*/ | |
| Object.keys (state.loaded_nodes) | |
| .map (function (id) { return id.replace (/\..*$/, ""); }) | |
| .filter (function (id) { return !that.ids.includes (id); }) | |
| .reduce (function (acc, id) { | |
| return ((acc.includes (id)) ? acc : acc.concat ([id])); | |
| }, []) | |
| .forEach (function (id) { return that.add_div (id); }); | |
| /* Blur pages if help screen is on. */ | |
| this.element.classList[(state.help) ? "add" : "remove"] ("blurred"); | |
| let div = this.prev_div; | |
| if (state.current !== this.prev_id) | |
| { | |
| if (this.prev_id) | |
| { | |
| this.prev_div.setAttribute ("hidden", "true"); | |
| /* Remove previous highlights. */ | |
| var old = linkid_split (this.prev_id); | |
| var msg = { message_kind: "highlight", regexp: null }; | |
| post_message (old.pageid, msg); | |
| } | |
| div = resolve_page (state.current, true); | |
| update_history (state.current, state.history); | |
| this.prev_id = state.current; | |
| this.prev_div = div; | |
| } | |
| if (state.action.type === "window-title") | |
| { | |
| div.setAttribute("title", state.action.title); | |
| } | |
| else if (state.action.type === "window-title-of-linkid") | |
| { | |
| var update_div = document.getElementById (state.action.linkid); | |
| if (update_div) | |
| update_div.setAttribute("title", state.action.title); | |
| } | |
| let title = div.getAttribute("title") || this.main_title; | |
| if (title && document.title !== title) | |
| document.title = title; | |
| if (state.search | |
| && (this.prev_search !== state.search) | |
| && state.search.status === "ready") | |
| { | |
| this.prev_search = state.search; | |
| if (state.search.current_pageid === config.TOP_ID) | |
| { | |
| window.setTimeout (function () { | |
| store.dispatch ({ type: "search-query" }); | |
| var res = search (document.getElementById (config.TOP_ID), | |
| state.search.regexp); | |
| store.dispatch ({ type: "search-result", found: res }); | |
| }, 0); | |
| } | |
| else | |
| { | |
| window.setTimeout (function () { | |
| store.dispatch ({ type: "search-query" }); | |
| var msg = { | |
| message_kind: "search", | |
| regexp: state.search.regexp, | |
| id: state.search.current_pageid | |
| }; | |
| post_message (state.search.current_pageid, msg); | |
| }, 0); | |
| } | |
| } | |
| /* Update highlight of current page. */ | |
| if (!state.highlight) | |
| { | |
| if (state.current === config.TOP_ID) | |
| remove_highlight (document.getElementById (config.TOP_ID)); | |
| else | |
| { | |
| var link = linkid_split (state.current); | |
| var msg$ = { message_kind: "highlight", regexp: null }; | |
| post_message (link.pageid, msg$); | |
| } | |
| } | |
| /* Scroll to highlighted search result. */ | |
| if (state.search | |
| && (this.prev_search !== state.search) | |
| && state.search.status === "done" | |
| && state.search.found === true) | |
| { | |
| var link$ = linkid_split (state.current); | |
| var msg$$ = { message_kind: "scroll-to", hash: "#search-result" }; | |
| post_message (link$.pageid, msg$$); | |
| this.prev_search = state.search; | |
| } | |
| }; | |
| /*--------------. | |
| | Utilitaries. | | |
| `--------------*/ | |
| /** Load PAGEID. | |
| @arg {string} pageid */ | |
| function | |
| load_page (pageid) | |
| { | |
| var div = resolve_page (pageid, false); | |
| /* Making the iframe visible triggers the load of the iframe DOM. */ | |
| if (div.hasAttribute ("hidden")) | |
| { | |
| div.removeAttribute ("hidden"); | |
| div.setAttribute ("hidden", "true"); | |
| } | |
| } | |
| /** Return the div element that correspond to LINKID. If VISIBLE is true | |
| then display the div and position the corresponding iframe to the | |
| anchor specified by LINKID. | |
| @arg {string} linkid - link identifier | |
| @arg {boolean} [visible] | |
| @return {HTMLElement} the div element. */ | |
| function | |
| resolve_page (linkid, visible) | |
| { | |
| var msg; | |
| var link = linkid_split (linkid); | |
| var pageid = link.pageid; | |
| var div = document.getElementById (link.pageid); | |
| if (!div) | |
| { | |
| msg = "no iframe container correspond to identifier: " + pageid; | |
| throw new ReferenceError (msg); | |
| } | |
| /* Create iframe if necessary unless the div is refering to the Index | |
| or Contents page. */ | |
| if (pageid === config.TOP_ID || pageid === config.CONTENTS_ID) | |
| { | |
| div.removeAttribute ("hidden"); | |
| /* Unlike iframes, Elements are unlikely to be scrollable (CSSOM | |
| Scroll-behavior), so choose an arbitrary element inside "index" | |
| div and at the top of it. */ | |
| document.getElementById ("icon-bar").scrollIntoView (); | |
| } | |
| else | |
| { | |
| var iframe = div.querySelector ("iframe"); | |
| if (!iframe) | |
| { | |
| iframe = document.createElement ("iframe"); | |
| iframe.classList.add ("node"); | |
| console.trace(); | |
| iframe.setAttribute ("src", linkid_to_url (pageid)); | |
| div.appendChild (iframe); | |
| iframe.addEventListener ("load", function () { | |
| store.dispatch ({ type: "iframe-ready", id: pageid }); | |
| }, false); | |
| } | |
| if (visible) | |
| { | |
| div.removeAttribute ("hidden"); | |
| msg = { message_kind: "scroll-to", hash: link.hash }; | |
| post_message (pageid, msg); | |
| } | |
| } | |
| return div; | |
| } | |
| /** Create a datalist element containing option elements corresponding | |
| to the keys in MENU. */ | |
| function | |
| create_datalist (menu) | |
| { | |
| var datalist = document.createElement ("datalist"); | |
| Object.keys (menu) | |
| .sort () | |
| .forEach (function (title) { | |
| var opt = document.createElement ("option"); | |
| opt.setAttribute ("value", title); | |
| datalist.appendChild (opt); | |
| }); | |
| return datalist; | |
| } | |
| /** Mutate the history of page navigation. Store LINKID in history | |
| state, The actual way to store LINKID depends on HISTORY_MODE. | |
| @arg {string} linkid | |
| @arg {string} history_mode */ | |
| function | |
| update_history (linkid, history_mode) | |
| { | |
| var method = window.history[history_mode]; | |
| if (method) | |
| { | |
| /* Pretend that the current page is the one corresponding to the | |
| LINKID iframe. Handle errors since changing the visible file | |
| name can fail on some browsers with the "file:" protocol. */ | |
| var visible_url = | |
| dirname (window.location.pathname) + linkid_to_url (linkid); | |
| try | |
| { | |
| method.call (window.history, linkid, null, visible_url); | |
| } | |
| catch (err) | |
| { | |
| /* Fallback to changing only the hash part which is safer. */ | |
| visible_url = window.location.pathname; | |
| if (linkid !== config.TOP_ID) | |
| visible_url += ("#" + linkid); | |
| method.call (window.history, linkid, null, visible_url); | |
| } | |
| } | |
| } | |
| /** Send MSG to the browsing context corresponding to PAGEID. This is a | |
| wrapper around 'Window.postMessage' which ensures that MSG is sent | |
| after PAGEID is loaded. | |
| @arg {string} pageid | |
| @arg {any} msg */ | |
| function | |
| post_message (pageid, msg) | |
| { | |
| if (pageid === config.TOP_ID || pageid === config.CONTENTS_ID) | |
| window.postMessage (msg, "*"); | |
| else | |
| { | |
| load_page (pageid); | |
| var iframe = document.getElementById (pageid) | |
| .querySelector ("iframe"); | |
| /* Semantically this would be better to use "promises" however | |
| they are not available in IE. */ | |
| if (store.state.ready[pageid]) | |
| iframe.contentWindow.postMessage (msg, "*"); | |
| else | |
| { | |
| iframe.addEventListener ("load", function handler () { | |
| this.contentWindow.postMessage (msg, "*"); | |
| this.removeEventListener ("load", handler, false); | |
| }, false); | |
| } | |
| } | |
| } | |
| /*-------------------------------------------- | |
| | Event handlers for the top-level context. | | |
| `-------------------------------------------*/ | |
| /* Initialize the top level `config.TOP_NAME' DOM. */ | |
| function | |
| on_load () | |
| { | |
| fix_links (document.links); | |
| add_icons (); | |
| document.body.classList.add ("mainbar"); | |
| /* Move contents of <body> into a a fresh <div> to let the components | |
| treat the index page like other iframe page. */ | |
| var index_div = document.createElement ("div"); | |
| for (var ch = document.body.firstChild; ch; ch = document.body.firstChild) | |
| index_div.appendChild (ch); | |
| var components = []; | |
| function add_component (component) { | |
| components.push (component); | |
| document.body.appendChild (component.element); | |
| } | |
| var pages = new Pages (index_div); | |
| add_component (pages); | |
| var contents_node = pages.add_div(config.CONTENTS_ID); | |
| add_component (new Sidebar (contents_node)); | |
| add_component (new Help_page ()); | |
| add_component (new Minibuffer ()); | |
| add_component (new Echo_area ()); | |
| store.components = components; | |
| if (window.location.hash) | |
| { | |
| var linkid = normalize_hash (window.location.hash); | |
| store.dispatch (actions.set_current_url (linkid, "replaceState")); | |
| } | |
| /* Retrieve NEXT link and local menu. */ | |
| var links = {}; | |
| links[config.TOP_ID] = navigation_links (document); | |
| store.dispatch (actions.cache_links (links)); | |
| store.dispatch ({ type: "iframe-ready", id: config.TOP_ID }); | |
| store.dispatch ({ | |
| type: "echo", | |
| msg: "Welcome to Texinfo documentation viewer 7.3dev, type '?' for help." | |
| }); | |
| /* Call user hook. */ | |
| if (config.on_main_load) | |
| config.on_main_load (); | |
| } | |
| /* Handle messages received via the Message API. | |
| @arg {MessageEvent} event */ | |
| function | |
| on_message (event) | |
| { | |
| var data = event.data; | |
| if (data.message_kind === "action") | |
| { | |
| /* Follow up actions to the store. */ | |
| store.dispatch (data.action); | |
| } | |
| } | |
| /* Event handler for 'popstate' events. */ | |
| function | |
| on_popstate (event) | |
| { | |
| /* When EVENT.STATE is 'null' it means that the user has manually | |
| changed the hash part of the URL bar. */ | |
| var linkid = (event.state === null) ? | |
| normalize_hash (window.location.hash) : event.state; | |
| store.dispatch (actions.set_current_url (linkid, false)); | |
| } | |
| return { | |
| on_load: on_load, | |
| on_message: on_message, | |
| on_popstate: on_popstate | |
| }; | |
| } /* init_index_page */ | |
| /*----------------------------------------. | |
| | Sidebar showing the table of contents. | | |
| `----------------------------------------*/ | |
| /** Initialize the iframe which contains the lateral table of content. */ | |
| function | |
| init_sidebar () | |
| { | |
| /* Keep children but remove grandchildren (Exception: don't remove | |
| anything on the current page; however, that's not a problem in the Kawa | |
| manual). */ | |
| function | |
| hide_grand_child_nodes (ul, excluded) | |
| { | |
| var lis = ul.children; | |
| for (var i = 0; i < lis.length; i += 1) | |
| { | |
| if (lis[i] === excluded) | |
| continue; | |
| var first = lis[i].firstElementChild; | |
| if (first && first.matches ("ul")) | |
| hide_grand_child_nodes (first); | |
| else if (first && first.matches ("a")) | |
| { | |
| var ul$ = first && first.nextElementSibling; | |
| if (ul$) | |
| ul$.setAttribute ("toc-detail", "yes"); | |
| } | |
| } | |
| } | |
| /** Make the parent of ELEMS visible. | |
| @arg {HTMLElement} elem */ | |
| function | |
| mark_parent_elements (elem) | |
| { | |
| if (elem && elem.parentElement && elem.parentElement.parentElement) | |
| { | |
| var pparent = elem.parentElement.parentElement; | |
| for (var sib = pparent.firstElementChild; sib; | |
| sib = sib.nextElementSibling) | |
| { | |
| if (sib !== elem.parentElement | |
| && sib.firstElementChild | |
| && sib.firstElementChild.nextElementSibling) | |
| { | |
| sib.firstElementChild | |
| .nextElementSibling | |
| .setAttribute ("toc-detail", "yes"); | |
| } | |
| } | |
| } | |
| } | |
| /** Scan ToC entries to see which should be hidden. | |
| @arg {HTMLElement} elem | |
| @arg {string} linkid */ | |
| function | |
| scan_toc (elem, linkid, section_hash = null) | |
| { | |
| /** @type {Element} */ | |
| var res; | |
| if (section_hash) | |
| { | |
| let dot = linkid.lastIndexOf('.'); | |
| if (dot >= 0) | |
| linkid = linkid.substring(0, dot+1) + section_hash; | |
| } | |
| var url = with_sidebar_query (linkid_to_url (linkid)); | |
| /** Set CURRENT to the node corresponding to URL linkid. | |
| @arg {Element} elem */ | |
| function | |
| find_current (elem) | |
| { | |
| if (elem.localName === "a" && link_href(elem) == url) | |
| { | |
| elem.setAttribute ("toc-current", "yes"); | |
| var sib = elem.nextElementSibling; | |
| if (sib && sib.matches ("ul")) | |
| hide_grand_child_nodes (sib); | |
| res = elem; | |
| } | |
| } | |
| var ul = elem.querySelector ("ul"); | |
| if (linkid === config.TOP_ID || linkid === config.CONTENTS_ID) | |
| { | |
| hide_grand_child_nodes (ul); | |
| res = document.getElementById("toc-" + config.TOP_ID); | |
| } | |
| else | |
| { | |
| depth_first_walk (ul, find_current, Node.ELEMENT_NODE); | |
| /* Mark every parent node. */ | |
| var current = res; | |
| while (current && current !== ul) | |
| { | |
| mark_parent_elements (current); | |
| /* XXX: Special case for manuals with '@part' commands. */ | |
| if (current.parentElement === ul) | |
| hide_grand_child_nodes (ul, current); | |
| current = current.parentElement; | |
| } | |
| } | |
| return res; | |
| } | |
| /* Build the global dictionary containing navigation links from NAV. NAV | |
| must be an 'ul' DOM element containing the table of content of the | |
| manual. */ | |
| function | |
| create_link_dict (nav) | |
| { | |
| var prev_id = config.TOP_ID; | |
| var links = {}; | |
| function | |
| add_link (elem) | |
| { | |
| let href; | |
| if (elem.matches ("a") && (href = link_href(elem))) | |
| { | |
| var id = href_hash (href); | |
| links[prev_id] = | |
| Object.assign ({}, links[prev_id], { forward: id }); | |
| links[id] = Object.assign ({}, links[id], { backward: prev_id }); | |
| prev_id = id; | |
| } | |
| } | |
| depth_first_walk (nav, add_link, Node.ELEMENT_NODE); | |
| /* Add a reference to the first and last node of the manual. */ | |
| links["*TOP*"] = config.TOP_ID; | |
| links["*END*"] = prev_id; | |
| return links; | |
| } | |
| /* Add a link from the sidebar to the main index file. ELEM is the first | |
| sibling of the newly created header. */ | |
| function | |
| add_header (elem) | |
| { | |
| var h1 = document.querySelector ("h1.settitle"); | |
| if (!h1) | |
| h1 = document.querySelector ("h1.top"); | |
| if (h1) | |
| { | |
| var a = document.createElement ("a"); | |
| a.setAttribute ("href", config.TOP_NAME); | |
| a.setAttribute ("id", "toc-" + config.TOP_ID); | |
| let header = elem.previousSibling; | |
| header.appendChild (a); | |
| if (window.sidebarLinkAppendContents) | |
| window.sidebarLinkAppendContents(a, h1.textContent); | |
| else | |
| { | |
| var div = document.createElement ("div"); | |
| a.appendChild (div); | |
| var span = document.createElement ("span"); | |
| span.innerHTML = h1.innerHTML; | |
| var anchor = span.querySelector("a.copiable-link"); | |
| if (anchor) | |
| anchor.parentNode.removeChild(anchor); | |
| div.appendChild (span); | |
| } | |
| } | |
| } | |
| /*------------------------------------------ | |
| | Event handlers for the sidebar context. | | |
| `-----------------------------------------*/ | |
| /* Initialize TOC_FILENAME which must be loaded in the context of an | |
| iframe. */ | |
| function | |
| on_load () | |
| { | |
| var toc_div = document.getElementById ("slider"); | |
| add_header (toc_div.querySelector (".toc")); | |
| /* Specify the base URL to use for all relative URLs. */ | |
| /* FIXME: Add base also for sub-pages. */ | |
| var base = document.createElement ("base"); | |
| base.setAttribute ("href", | |
| window.location.href.replace (/[/][^/]*$/, "/")); | |
| document.head.appendChild (base); | |
| scan_toc (toc_div, config.TOP_NAME); | |
| /* Get 'backward' and 'forward' link attributes. */ | |
| var dict = create_link_dict (toc_div.querySelector ("nav.contents ul")); | |
| store.dispatch (actions.cache_links (dict)); | |
| } | |
| /* Handle messages received via the Message API. */ | |
| function | |
| on_message (event) | |
| { | |
| var data = event.data; | |
| if (data.message_kind === "update-sidebar") | |
| { | |
| var toc_div = document.getElementById ("slider"); | |
| /* Reset previous calls to 'scan_toc'. */ | |
| depth_first_walk (toc_div, function clear_toc_styles (elem) { | |
| elem.removeAttribute ("toc-detail"); | |
| elem.removeAttribute ("toc-current"); | |
| }, Node.ELEMENT_NODE); | |
| /* Remove the hash part for the main page. */ | |
| var pageid = linkid_split (data.selected).pageid; | |
| debug ("sidebar - " + pageid); | |
| /* Highlight the current LINKID in the table of content. */ | |
| var elem = scan_toc (toc_div, pageid, data.section_hash); | |
| if (elem) | |
| elem.scrollIntoView (true); | |
| else | |
| debug ("sidebar - no elem " + pageid); | |
| } | |
| } | |
| return { | |
| on_load: on_load, | |
| on_message: on_message | |
| }; | |
| } /* init_sidebar */ | |
| /*-----------------------------------------. | |
| | Iframes containing pages of the manual. | | |
| `-----------------------------------------*/ | |
| /** Initialize iframes which contain pages of the manual. */ | |
| function | |
| init_iframe () | |
| { | |
| /* Initialize the DOM for generic pages loaded in the context of an | |
| iframe. */ | |
| function | |
| on_load () | |
| { | |
| fix_links (document.links); | |
| var links = {}; | |
| var linkid = basename (window.location.pathname, /[.]x?html$/); | |
| links[linkid] = navigation_links (document); | |
| store.dispatch (actions.cache_links (links)); | |
| if (document.title) | |
| store.dispatch (actions.window_title_of_linkid (document.title, linkid)); | |
| if (linkid_contains_index (linkid)) | |
| { | |
| /* Scan links that should be added to the index. */ | |
| var index_entries = document.querySelectorAll | |
| ("td.printindex-index-entry" | |
| + ", td.printindex-index-subentry-level-1" | |
| + ", td.printindex-index-subentry-level-2"); | |
| var dict = {}; | |
| var text0 = "", text1 = ""; // for subentries | |
| for (var i = 0; i < index_entries.length; i += 1) | |
| { | |
| var entry = index_entries[i]; | |
| var entry_cl = entry.classList; | |
| var text = entry.textContent; | |
| if (entry_cl.contains("printindex-index-subentry-level-2")) | |
| { | |
| text = text0 + "; " + text1 + "; " + text; | |
| } | |
| else if (entry_cl.contains("printindex-index-subentry-level-1")) | |
| { | |
| text1 = text; | |
| text = text0 + "; " + text; | |
| } | |
| else | |
| { | |
| text0 = text; | |
| } | |
| var link = entry.firstChild; | |
| if (link && link.nodeName == 'A') | |
| { | |
| dict[text] = href_hash (link_href (link)); | |
| } | |
| } | |
| store.dispatch (actions.cache_index_links (dict)); | |
| } | |
| add_icons (); | |
| /* Call user hook. */ | |
| if (config.on_iframe_load) | |
| config.on_iframe_load (); | |
| } | |
| /* Handle messages received via the Message API. */ | |
| function | |
| on_message (event) | |
| { | |
| var data = event.data; | |
| if (data.message_kind === "highlight") | |
| remove_highlight (document.body); | |
| else if (data.message_kind === "search") | |
| { | |
| var found = search (document.body, data.regexp); | |
| store.dispatch ({ type: "search-result", found: found }); | |
| } | |
| else if (data.message_kind === "scroll-to") | |
| { | |
| /* Scroll to the anchor corresponding to HASH. */ | |
| if (data.hash) | |
| { | |
| let elem = document.getElementById(data.hash.substring(1)); | |
| // Check if hash reference is to a sectioing element. | |
| // If not we need to find the outer sectioning element, | |
| // so we can update the sidebar's ToC correctly. | |
| if (elem) | |
| { | |
| let p = elem; | |
| let section = null; | |
| let id = null; | |
| for (let p = elem; | |
| section === null && p instanceof Element; | |
| p = p.parentNode) | |
| { | |
| let sid = p.getAttribute("id"); | |
| if (sid == null) | |
| continue; | |
| let cl = p.classList; | |
| for (let i = cl.length; --i >= 0; ) | |
| { | |
| if (section_names.indexOf(cl.item(i)) >= 0) | |
| { | |
| section = p; | |
| id = sid; | |
| break; | |
| } | |
| } | |
| } | |
| if (section && section !== elem) | |
| { | |
| // Send section id to sidebar so it can properly update. | |
| store.dispatch({ type: "section", hash: data.hash, section_hash: id } ); | |
| } | |
| } | |
| window.location.replace (data.hash); | |
| } | |
| else | |
| window.scroll (0, 0); | |
| } | |
| } | |
| return { | |
| on_load: on_load, | |
| on_message: on_message | |
| }; | |
| } /* init_iframe */ | |
| /*------------------------- | |
| | Common event handlers. | | |
| `------------------------*/ | |
| /** Handle click events. */ | |
| function | |
| on_click (event) | |
| { | |
| let in_sidebar = event.target.matches ("#slider *"); | |
| for (var target = event.target; target !== null; target = target.parentNode) | |
| { | |
| if (! (target instanceof Element)) | |
| continue; | |
| if (target.matches ("a")) | |
| { | |
| var href = link_href(target); | |
| if (href && maybe_pageref_url_p (href) | |
| && !external_manual_url_p (href)) | |
| { | |
| var linkid = href_hash (href) || config.TOP_ID; | |
| if (linkid === "index.SEC_Contents") | |
| linkid = config.CONTENTS_ID; | |
| store.dispatch (actions.set_current_url (linkid, null, | |
| in_sidebar ? "in-sidebar" : "in-page")); | |
| event.preventDefault (); | |
| event.stopPropagation (); | |
| break; | |
| } | |
| } | |
| if (target.matches (".sidebar-hider")) | |
| { | |
| let body = document.body; | |
| let show = body.getAttribute("show-sidebar"); | |
| show_sidebar (show==="no"); | |
| return; | |
| } | |
| } | |
| if (! in_sidebar) | |
| hide_sidebar_if_narrow (); | |
| } | |
| // Only valid when showing sidebar. | |
| function is_narrow_window () | |
| { | |
| return document.body.firstChild.offsetLeft == 0; | |
| } | |
| function show_sidebar (show) | |
| { | |
| store.dispatch({ type: "show-sidebar", show: show ? "yes" : "no" }); | |
| } | |
| function hide_sidebar_if_narrow () | |
| { | |
| store.dispatch({ type: "show-sidebar", show: "hide-if-narrow" }); | |
| } | |
| /** Handle unload events. */ | |
| function | |
| on_unload () | |
| { | |
| /* Cross origin requests are not supported in file protocol. */ | |
| if (window.location.protocol !== "file:") | |
| { | |
| var request = new XMLHttpRequest (); | |
| request.open ("GET", "(WINDOW-CLOSED)"); | |
| request.send (null); | |
| } | |
| } | |
| /** Handle Keyboard 'keydown' events. | |
| @arg {KeyboardEvent} event */ | |
| function | |
| on_keydown (event) | |
| { | |
| if (is_escape_key (event.key)) | |
| store.dispatch ({ type: "unfocus" }); | |
| else if (!event.altKey && !event.ctrlKey && !event.metaKey) | |
| { | |
| var val = on_keydown.dict[event.key]; | |
| if (val) | |
| { | |
| if (typeof val === "function") | |
| val (); | |
| else | |
| store.dispatch (val); | |
| event.preventDefault(); | |
| } | |
| } | |
| } | |
| /* Associate an Event 'key' property to an action or a thunk. */ | |
| on_keydown.dict = { | |
| i: actions.show_text_input ("index"), | |
| l: window.history.back.bind (window.history), | |
| m: actions.show_text_input ("menu"), | |
| n: actions.navigate ("next"), | |
| p: actions.navigate ("prev"), | |
| r: window.history.forward.bind (window.history), | |
| s: actions.show_text_input ("regexp-search"), | |
| t: actions.set_current_url_pointer ("*TOP*"), | |
| u: actions.navigate ("up"), | |
| "]": actions.navigate ("forward"), | |
| "[": actions.navigate ("backward"), | |
| "<": actions.set_current_url_pointer ("*TOP*"), | |
| ">": actions.set_current_url_pointer ("*END*"), | |
| "?": actions.show_help () | |
| }; | |
| /** Some standard methods used in this script might not be implemented by | |
| the current browser. If that is the case then augment the prototypes | |
| appropriately. */ | |
| function | |
| register_polyfills () | |
| { | |
| function | |
| includes (search, start) | |
| { | |
| start = start || 0; | |
| return ((start + search.length) <= this.length) | |
| && (this.indexOf (search, start) !== -1); | |
| } | |
| /* eslint-disable no-extend-native */ | |
| if (!Array.prototype.includes) | |
| Array.prototype.includes = includes; | |
| if (!String.prototype.includes) | |
| String.prototype.includes = includes; | |
| if (!String.prototype.endsWith) | |
| { | |
| String.prototype.endsWith = function endsWith (search, position) { | |
| var subject_string = this.toString (); | |
| if (typeof position !== "number" | |
| || !isFinite (position) | |
| || Math.floor (position) !== position | |
| || position > subject_string.length) | |
| position = subject_string.length; | |
| position -= search.length; | |
| var last_index = subject_string.lastIndexOf (search, position); | |
| return last_index !== -1 && last_index === position; | |
| }; | |
| } | |
| if (!Element.prototype.matches) | |
| { | |
| Element.prototype.matches = Element.prototype["matchesSelector"] | |
| || Element.prototype["mozMatchesSelector"] | |
| || Element.prototype["mozMatchesSelector"] | |
| || Element.prototype["msMatchesSelector"] | |
| || Element.prototype["webkitMatchesSelector"] | |
| || function element_matches (str) { | |
| var document = (this.document || this.ownerDocument); | |
| var matches = document.querySelectorAll (str); | |
| var i = matches.length; | |
| while ((i -= 1) >= 0 && matches.item (i) !== this); | |
| return i > -1; | |
| }; | |
| } | |
| if (typeof Object.assign != "function") | |
| { | |
| Object.assign = function assign (target) { | |
| if (undef_or_null (target)) | |
| throw new TypeError ("Cannot convert undefined or null to object"); | |
| var to = Object (target); | |
| for (var index = 1; index < arguments.length; index += 1) | |
| { | |
| var next_source = arguments[index]; | |
| if (undef_or_null (next_source)) | |
| continue; | |
| for (var key in next_source) | |
| { | |
| /* Avoid bugs when hasOwnProperty is shadowed. */ | |
| if (Object.prototype.hasOwnProperty.call (next_source, key)) | |
| to[key] = next_source[key]; | |
| } | |
| } | |
| return to; | |
| }; | |
| } | |
| (function (protos) { | |
| protos.forEach (function (proto) { | |
| if (!proto.hasOwnProperty ("remove")) | |
| { | |
| Object.defineProperty (proto, "remove", { | |
| configurable: true, | |
| enumerable: true, | |
| writable: true, | |
| value: function value () { | |
| this.parentNode.removeChild (this); | |
| } | |
| }); | |
| } | |
| }); | |
| } ([Element.prototype, CharacterData.prototype, DocumentType.prototype])); | |
| /* eslint-enable no-extend-native */ | |
| } | |
| /*-------------------. | |
| | Common utilities. | | |
| `-------------------*/ | |
| /** Check portably if KEY correspond to "Escape" key value. | |
| @arg {string} key */ | |
| function | |
| is_escape_key (key) | |
| { | |
| /* In Internet Explorer 9 and Firefox 36 and earlier, the Esc key | |
| returns "Esc" instead of "Escape". */ | |
| return key === "Escape" || key === "Esc"; | |
| } | |
| /** Check if OBJ is equal to 'undefined' or 'null'. */ | |
| function | |
| undef_or_null (obj) | |
| { | |
| return (obj === null || typeof obj === "undefined"); | |
| } | |
| /** Return a relative URL corresponding to HREF, which refers to an anchor | |
| of 'config.TOP_NAME'. HREF must be a USVString representing an | |
| absolute or relative URL. For example "foo/bar.html" will return | |
| "config.TOP_NAME#bar". | |
| @arg {string} href.*/ | |
| function | |
| with_sidebar_query (href) | |
| { | |
| if (basename (href) === config.TOP_NAME) | |
| return config.TOP_NAME; | |
| else | |
| { | |
| /* XXX: Do not use the URL API for IE portability. */ | |
| var url = with_sidebar_query.url; | |
| url.setAttribute ("href", href); | |
| var new_hash = "#" + basename (url.pathname, /[.]x?html/); | |
| /* XXX: 'new_hash !== url.hash' is a workaround to work with links | |
| produced by makeinfo which link to an anchor element in a page | |
| instead of directly to the page. */ | |
| if (url.hash && new_hash !== url.hash) | |
| new_hash += ("." + url.hash.slice (1)); | |
| return config.TOP_NAME + new_hash; | |
| } | |
| } | |
| /* Use the same DOM element for every function call. */ | |
| with_sidebar_query.url = document.createElement ("a"); | |
| /** 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. If ID is true then define an "id" | |
| attribute with a linkid value for relative links. | |
| @typedef {HTMLAnchorElement|HTMLAreaElement} Links | |
| @arg {Links[]|HTMLCollectionOf<Links>} links | |
| @arg {boolean} [id] | |
| @return void */ | |
| function | |
| fix_links (links, id) | |
| { | |
| for (var i = 0; i < links.length; i += 1) | |
| { | |
| var link = links[i]; | |
| var href = link_href(link); | |
| if (!href) | |
| continue; | |
| else if (external_manual_url_p (href)) | |
| link.setAttribute ("target", "_top"); | |
| else if (! maybe_pageref_url_p (href)) | |
| link.setAttribute ("target", "_blank"); | |
| else | |
| { | |
| var href$ = with_sidebar_query (href); | |
| if (href !== href$) | |
| { | |
| let protocol = location.protocol; | |
| if (protocol == "https:" || protocol == "http:") | |
| link._href = href$; | |
| else | |
| link.setAttribute ("href", href$); | |
| } | |
| if (id) | |
| { | |
| var linkid = (href$ === config.TOP_NAME) ? | |
| config.TOP_ID : href_hash (href$); | |
| link.setAttribute ("id", linkid); | |
| } | |
| } | |
| } | |
| } | |
| /** Convert HASH which is something that can be found 'Location.hash' to a | |
| "linkid" which can be handled in our model. | |
| @arg {string} hash | |
| @return {string} linkid */ | |
| function | |
| normalize_hash (hash) | |
| { | |
| var text = hash.slice (1); | |
| /* Some anchor elements are present in 'config.TOP_NAME' and we need to | |
| handle link to them specially (i.e. not try to find their corresponding | |
| iframe).*/ | |
| if (config.MAIN_ANCHORS.includes (text)) | |
| return config.TOP_ID + "." + text; | |
| else | |
| return text; | |
| } | |
| /** Return an object composed of the filename and the anchor of LINKID. | |
| LINKID can have the form "foobar.anchor" or just "foobar". | |
| @arg {string} linkid | |
| @return {{pageid: string, hash: string}} */ | |
| function | |
| linkid_split (linkid) | |
| { | |
| if (!linkid.includes (".")) | |
| return { pageid: linkid, hash: "" }; | |
| else | |
| { | |
| var ref = linkid.match (/^(.+)\.(.*)$/).slice (1); | |
| return { pageid: ref[0], hash: "#" + ref[1] }; | |
| } | |
| } | |
| /** Convert LINKID which has the form "foobar.anchor" or just "foobar", to | |
| an URL of the form `foobar${config.EXT}#anchor`. | |
| @arg {string} linkid */ | |
| function | |
| linkid_to_url (linkid) | |
| { | |
| if (linkid === config.TOP_ID) | |
| return config.TOP_NAME; | |
| else | |
| { | |
| var link = linkid_split (linkid); | |
| return link.pageid + config.EXT + link.hash; | |
| } | |
| } | |
| /** Check if 'URL' may be a cross-reference to another page in this manual. */ | |
| function | |
| maybe_pageref_url_p (url) | |
| { | |
| return url.match(config.LOCAL_HTML_PAGE_PATTERN); | |
| } | |
| /** Check if 'URL' is a link to another manual. For locally installed | |
| manuals only. */ | |
| function | |
| external_manual_url_p (url) | |
| { | |
| if (typeof url !== "string") | |
| throw new TypeError ("'" + url + "' is not a string"); | |
| return url.match(/^..\//); | |
| } | |
| /** Return PATHNAME with any leading directory components removed. If | |
| specified, also remove a trailing SUFFIX. */ | |
| function | |
| basename (pathname, suffix) | |
| { | |
| var res = pathname.replace (/.*[/]/, ""); | |
| if (!suffix) | |
| return res; | |
| else if (suffix instanceof RegExp) | |
| return res.replace (suffix, ""); | |
| else /* typeof SUFFIX === "string" */ | |
| return res.replace (new RegExp ("[.]" + suffix), ""); | |
| } | |
| /** Strip last component from PATHNAME and keep the trailing slash. For | |
| example if PATHNAME is "/foo/bar/baz.html" then return "/foo/bar/". | |
| @arg {string} pathname */ | |
| function | |
| dirname (pathname) | |
| { | |
| var res = pathname.match (/\/?.*\//); | |
| if (res) | |
| return res[0]; | |
| else | |
| throw new Error ("'location.pathname' is not recognized"); | |
| } | |
| /** Apply FUNC to each nodes in the NODE subtree. The walk follows a depth | |
| first algorithm. Only consider the nodes of type NODE_TYPE. */ | |
| function | |
| depth_first_walk (node, func, node_type) | |
| { | |
| if (!node_type || (node.nodeType === node_type)) | |
| func (node); | |
| for (var child = node.firstChild; child; child = child.nextSibling) | |
| depth_first_walk (child, func, node_type); | |
| } | |
| /** Return the "effective" href attribute of a link (a) element. */ | |
| function | |
| link_href (alink) | |
| { | |
| return alink._href || alink.getAttribute("href"); | |
| } | |
| /** Return the hash part of HREF without the '#' prefix. HREF must be a | |
| string. If there is no hash part in HREF then return the empty | |
| string. */ | |
| function | |
| href_hash (href) | |
| { | |
| if (typeof href !== "string") | |
| throw new TypeError (href + " is not a string"); | |
| if (href.includes ("#")) | |
| return href.replace (/.*#/, ""); | |
| else | |
| return ""; | |
| } | |
| /** Check if LINKID corresponds to a page containing index links. */ | |
| function | |
| linkid_contains_index (linkid) | |
| { | |
| return linkid.match (/^.*-index$/i) || linkid.match (/^Index$/); | |
| } | |
| /** 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...). */ | |
| function | |
| navigation_links (content) | |
| { | |
| var links = content.querySelectorAll ("a[accesskey][href]"); | |
| var res = {}; | |
| /* links have the form MAIN_FILE.html#FRAME-ID. For convenience | |
| only store FRAME-ID. */ | |
| for (var i = 0; i < links.length; i += 1) | |
| { | |
| var link = links[i]; | |
| var nav_id = navigation_links.dict[link.getAttribute ("accesskey")]; | |
| if (nav_id) | |
| { | |
| var href = basename (link_href (link)); | |
| if (href === config.TOP_NAME) | |
| res[nav_id] = config.TOP_ID; | |
| else | |
| res[nav_id] = href_hash (href); | |
| } | |
| else /* this link is part of local table of content. */ | |
| { | |
| res.TI_menu = res.TI_menu || {}; | |
| res.TI_menu[link.text] = href_hash (link_href (link)); | |
| } | |
| } | |
| return res; | |
| } | |
| navigation_links.dict = { n: "next", p: "prev", u: "up" }; | |
| function | |
| add_icons () | |
| { | |
| var div = document.createElement ("div"); | |
| div.setAttribute ("id", "icon-bar"); | |
| var span = document.createElement ("span"); | |
| span.innerHTML = "?"; | |
| // Set tool-tip (on hover) | |
| span.setAttribute ("title", "Help for keyboard shortcuts"); | |
| span.classList.add ("icon"); | |
| span.addEventListener ("click", function () { | |
| store.dispatch (actions.show_help ()); | |
| }, false); | |
| div.appendChild (span); | |
| document.body.insertBefore (div, document.body.firstChild); | |
| } | |
| /** Check if ELEM matches SEARCH | |
| @arg {Element} elem | |
| @arg {RegExp} rgxp | |
| @return {boolean} */ | |
| function | |
| search (elem, rgxp) | |
| { | |
| /** @type {Text} */ | |
| var text = null; | |
| /** @arg {Text} node */ | |
| function | |
| find (node) | |
| { | |
| if (rgxp.test (node.textContent)) | |
| { | |
| /* Ignore previous match. */ | |
| var prev = node.parentElement.matches ("span.highlight"); | |
| text = (prev) ? null : (text || node); | |
| } | |
| } | |
| depth_first_walk (elem, find, Node.TEXT_NODE); | |
| remove_highlight (elem); | |
| if (!text) | |
| return false; | |
| else | |
| { | |
| highlight_text (rgxp, text); | |
| return true; | |
| } | |
| } | |
| /** Find the pageid corresponding to forward direction. | |
| @arg {any} state | |
| @arg {string} linkid | |
| @return {string} the forward pageid */ | |
| function | |
| forward_pageid (state, linkid) | |
| { | |
| var data = state.loaded_nodes[linkid]; | |
| if (!data) | |
| throw new Error ("page not loaded: " + linkid); | |
| else if (!data.forward) | |
| return null; | |
| else | |
| { | |
| var cur = linkid_split (linkid); | |
| var fwd = linkid_split (data.forward); | |
| if (!fwd.hash && fwd.pageid !== cur.pageid) | |
| return fwd.pageid; | |
| else | |
| return forward_pageid (state, data.forward); | |
| } | |
| } | |
| /** Highlight text in NODE which match RGXP. | |
| @arg {RegExp} rgxp | |
| @arg {Text} node */ | |
| function | |
| highlight_text (rgxp, node) | |
| { | |
| /* Skip elements corresponding to highlighted words to avoid infinite | |
| recursion. */ | |
| if (node.parentElement.matches ("span.highlight")) | |
| return; | |
| var matches = rgxp.exec (node.textContent); | |
| if (matches) | |
| { | |
| /* Create an highlighted element containing first match. */ | |
| var span = document.createElement ("span"); | |
| span.appendChild (document.createTextNode (matches[0])); | |
| span.setAttribute ("id", "search-result"); | |
| span.classList.add ("highlight"); | |
| var right_node = node.splitText (matches.index); | |
| /* Remove first match from right node. */ | |
| right_node.textContent = right_node.textContent | |
| .slice (matches[0].length); | |
| node.parentElement.insertBefore (span, right_node); | |
| } | |
| } | |
| /** Remove every highlighted elements and inline their text content. | |
| @arg {Element} elem */ | |
| function | |
| remove_highlight (elem) | |
| { | |
| var spans = elem.getElementsByClassName ("highlight"); | |
| /* Replace spans with their inner text node. */ | |
| while (spans.length > 0) | |
| { | |
| var span = spans[0]; | |
| var parent = span.parentElement; | |
| parent.replaceChild (span.firstChild, span); | |
| } | |
| } | |
| /*--------------. | |
| | Tracing. | | |
| `--------------*/ | |
| function | |
| debug (msg) | |
| { | |
| if (config.DEBUG) { | |
| console.log (msg); | |
| } | |
| } | |
| /*--------------. | |
| | Entry point. | | |
| `--------------*/ | |
| /** Depending on the role of the document launching this script, different | |
| event handlers are registered. This script can be used in the context of: | |
| - the index page of the manual which manages the state of the application | |
| - the iframe which contains the lateral table of content | |
| - other iframes which contain other pages of the manual | |
| This is done to allow referencing the same script inside every HTML page. | |
| This has the benefits of reducing the number of HTTP requests required to | |
| fetch the Javascript code and simplifying the work of the Texinfo HTML | |
| converter. */ | |
| /* Display MSG bail out message portably. */ | |
| function | |
| error (msg) | |
| { | |
| /* XXX: This code needs to be highly portable. | |
| Check <https://quirksmode.org/dom/core/> for details. */ | |
| return function () { | |
| var div = document.createElement ("div"); | |
| div.setAttribute ("class", "error"); | |
| div.innerHTML = msg; | |
| var elem = document.body.firstChild; | |
| document.body.insertBefore (div, elem); | |
| window.setTimeout (function () { | |
| document.body.removeChild (div); | |
| }, config.WARNING_TIMEOUT); | |
| }; | |
| } | |
| /* Check if current browser supports the minimum requirements required for | |
| properly using this script, otherwise bails out. */ | |
| if (features && !(features.es5 | |
| && features.classlist | |
| && features.eventlistener | |
| && features.hidden | |
| && features.history | |
| && features.postmessage | |
| && features.queryselector)) | |
| { | |
| window.onload = error ("'info.js' is not compatible with this browser"); | |
| return; | |
| } | |
| register_polyfills (); | |
| /* Let the config provided by the user mask the default one. */ | |
| config = Object.assign (config, user_config); | |
| var inside_iframe = top !== window; | |
| var inside_top_page = window.location.pathname === config.TOP_NAME | |
| || window.location.pathname.endsWith ("/" + config.TOP_NAME) | |
| || window.location.pathname.endsWith ("/"); | |
| if (inside_top_page) | |
| { | |
| var initial_state = { | |
| /* Dictionary associating page ids to next, prev, up, forward, | |
| backward link ids. */ | |
| loaded_nodes: {}, | |
| /* Dictionary associating keyword to linkids. */ | |
| index: undefined, | |
| /* page id of the current page. */ | |
| current: config.TOP_ID, | |
| /* dictionary associating a page id to a boolean. */ | |
| ready: {}, | |
| /* Current mode for handling history. */ | |
| history: "replaceState", | |
| /* Define the name of current text input. */ | |
| text_input: null | |
| }; | |
| store = new Store (initial_state); | |
| var index = init_index_page (); | |
| var sidebar = init_sidebar (); | |
| window.addEventListener ("DOMContentLoaded", function () { | |
| index.on_load (); | |
| sidebar.on_load (); | |
| }, false); | |
| window.addEventListener ("message", index.on_message, false); | |
| window.addEventListener ("message", sidebar.on_message, false); | |
| window.onpopstate = index.on_popstate; | |
| } | |
| else if (inside_iframe) | |
| { | |
| store = new Remote_store (); | |
| var iframe = init_iframe (); | |
| window.addEventListener ("DOMContentLoaded", iframe.on_load, false); | |
| window.addEventListener ("message", iframe.on_message, false); | |
| } | |
| else | |
| { | |
| /* Jump to 'config.TOP_NAME' and adapt the selected iframe. */ | |
| window.location.replace (with_sidebar_query (window.location.href)); | |
| } | |
| /* Register common event handlers. */ | |
| window.addEventListener ("beforeunload", on_unload, false); | |
| window.addEventListener ("click", on_click, false); | |
| /* XXX: handle 'keydown' event instead of 'keypress' since Chromium | |
| doesn't handle the 'Escape' key properly. See | |
| https://bugs.chromium.org/p/chromium/issues/detail?id=9061. */ | |
| window.addEventListener ("keydown", on_keydown, false); | |
| } (window["Modernizr"], window["INFO_CONFIG"])); |