Merge pull request #1068 from jfbercher/toc2scroll

[toc2] Scrolling: add a mark to currently displayed section in TOC window (including @jcb91 toc2 amd module)
This commit is contained in:
Jean-François Bercher
2017-09-12 22:06:20 +02:00
committed by GitHub
6 changed files with 234 additions and 158 deletions
@@ -4,12 +4,15 @@
The toc2 extension enables to collect all running headers and display them in a floating window, as a sidebar or with a navigation menu. The extension is also draggable, resizable, collapsable, dockable and features automatic numerotation with unique links ids, and an optional toc cell. Sections of currently selected/edited or running cells are highlighted in the toc. Some minor diplay tweaks are also available (moving header tile/menus, widening cells); Finally, the toc can preserved when exporting to html.
#### First demo:
#### First demo: Floating toc window and SideBar, toc auto-update, section numbering
![](demo.gif)
#### Second demo:
##### Second demo: Save as html with toc / Navigation menu
![](demo2.gif)
### Third demo: Notebook scrolling and Collapsing sections
![](https://user-images.githubusercontent.com/7596356/29540207-a3d892fe-86cd-11e7-8476-54c79d9f8d7c.gif)
The table of contents is automatically updated when modifications occur in the notebook. The toc window can be moved and resized. It can be docked as a sidebar or dragged from the sidebar into a floating window. The table of contents can be collapsed or the window can be completely hidden. The navigation menu can be enabled/disabled via the nbextensions configuration utility. It can also be resized. The position, dimensions, and states (that is 'collapsed' and 'hidden' states) are remembered (actually stored in the notebook's metadata) and restored on the next session.
There is a configurable option to skip h1 headers from the ToC, to allow their use as a notebook
@@ -40,6 +43,7 @@ The initial configuration can be given using the IPython-contrib nbextensions fa
- Widening the display area to fit the browser window (may be useful with sidebar option; default: true)
- The numbering of headers (true by default)
- Moving header title and menus on the left (default: true)
- Marking toc item of first header displayed on viewport when scrolling the notebook (default: true)
- Skipping h1 headers, useful if you want to use h1 as unnumbered notebook title (default: false)
- Customization of highlighting the title of currently selected/running sections.
- Customization of background, fonts, border and highlighting colors in the toc window and navigation menus (as third demo).
@@ -47,7 +51,7 @@ The initial configuration can be given using the IPython-contrib nbextensions fa
The differents states and position of the floating window have reasonable defaults and can be modfied per notebook).
#### Third demo with dark theme
#### Demo with dark theme
![](demo_dark.png)
## Export
@@ -118,3 +122,5 @@ This option requires the IPython kernel and is not present with other kernels.
- @jfbercher, @louisabraham, @jcb91 July 2017. Add support for skipping h1
headings, enabling their use as unnumbered notebook titles
- @jcb91 with minor contributions by @jfbercher. August 2017. Make toc entries collapsible #1031 with optional synchronization with `collapsible_headings` + some small other tweaks.
- @jcb91 August 2017. Use amd structure for toc2.js
- @jfbercher August 2017. Add a mark to the currently displayed section in the table of contents window as user scrolls the notebook, cf #944.
@@ -1,18 +1,21 @@
/*extracted from https://gist.github.com/magican/5574556*/
/*background color for links when you mouse over it
This style is used on export, but can be overwritted in the livenotebook
by selecting a color in the nbextension-configurator*/
/*
Most colors definded here will be used on export, but can be overwritted in
the livenotebook by modyfying notebook.json or selecting a color in the
nbextension-configurator
*/
/*background color for links when you mouse over it */
#toc-wrapper li > span:hover {
background-color: #DAA520;
}
#toc-level0 a {
color: #333333; /* default - color also loaded from notebook config */
color: #333333; /* default - alterable via nbextension-configurator */
text-decoration: none;
}
#navigate_menu li > span:hover {background-color: #f1f1f1}
@@ -36,7 +39,7 @@ padding-left: 20px;
#navigate_menu a {
list-style-type: none;
/*color: #333333; */ /* now specified in js */
color: #333333; /* default - alterable via nbextension-configurator */
text-decoration: none;
}
@@ -55,7 +58,7 @@ padding-left: 20px;
padding: 0px;
overflow-y: auto;
font-weight: normal;
/*color: #333333;*/ /* now specified in js */
color: #333333; /* default - alterable via nbextension-configurator */
white-space: nowrap;
overflow-x: auto;
}
@@ -92,7 +95,7 @@ padding-left: 20px;
border: thin solid rgba(0, 0, 0, 0.38);
border-radius: 5px;
padding:10px;
/*background-color: #fff;*/ /* now specified in js */
background-color: #fff; /* default - alterable via nbextension-configurator */
opacity: .8;
z-index: 100;
overflow: hidden;
@@ -105,9 +108,9 @@ padding-left: 20px;
position: fixed !important;
width: 212px;
max-width: 28%;
/*background-color: #fff;*/ /* now specified in js */
background-color: #fff; /* default - alterable via nbextension-configurator */
border-style: solid;
/*border-color: #eeeeee;*/ /* now specified in js */
border-color: #eeeeee; /* default - alterable via nbextension-configurator */
opacity: .99;
overflow: hidden;
}
@@ -145,7 +148,10 @@ padding-left: 20px;
font-family: monospace;
}
/* on scroll style */
.highlight_on_scroll {
border-left: solid 4px blue;
}
/* don't waste so much screen space... */
/*
@@ -170,7 +176,7 @@ padding-left: 20px;
#toc-wrapper .toc-item-num {
font-style: normal;
font-family: Georgia, Times New Roman, Times, serif;
/*color: black;*/ /* now specified in js */
color: black; /* default - alterable via nbextension-configurator */
}
/*
@@ -7,7 +7,7 @@ define([
'jquery',
'base/js/namespace',
'notebook/js/codecell',
'./toc2'
'nbextensions/toc2/toc2'
], function(
require,
$,
@@ -25,7 +25,8 @@ define([
// ...........Parameters configuration......................
// default values for system-wide configurable parameters
var cfg={'threshold':4,
'navigate_menu':true,
'navigate_menu':true,
'markTocItemOnScroll': true,
'moveMenuLeft': true,
'widenNotebook': false,
'colors': {
@@ -36,6 +37,7 @@ define([
'sidebar_border': '#EEEEEE',
'navigate_text': '#333333',
'navigate_num': '#000000',
'on_scroll': '#2447f0',
},
collapse_to_match_collapsible_headings: false,
}
@@ -58,7 +60,7 @@ define([
st.rendering_toc_cell = false;
st.oldTocHeight = undefined
st.cell_toc = undefined;
st.toc_index=0;
st.toc_index = 0;
var read_config = function (cfg, callback) {
IPython.notebook.config.loaded.then(function () {
@@ -114,144 +116,151 @@ define([
//***********************************************************************
// ----------------------------------------------------------------------
function toggleToc() {
toggle_toc(cfg,st)
}
var toc_button = function () {
if (!IPython.toolbar) {
$([IPython.events]).on("app_initialized.NotebookApp", toc_button);
return;
function toggleToc() {
toggle_toc(cfg, st)
}
if ($("#toc_button").length === 0) {
IPython.toolbar.add_buttons_group([
{
'label' : 'Table of Contents',
'icon' : 'fa-list',
'callback': toggleToc,
'id' : 'toc_button'
var toc_button = function() {
if (!IPython.toolbar) {
$([IPython.events]).on("app_initialized.NotebookApp", toc_button);
return;
}
if ($("#toc_button").length === 0) {
IPython.toolbar.add_buttons_group([{
'label': 'Table of Contents',
'icon': 'fa-list',
'callback': toggleToc,
'id': 'toc_button'
}]);
}
]);
}
};
var load_css = function () {
var link = document.createElement("link");
link.type = "text/css";
link.rel = "stylesheet";
link.href = require.toUrl("./main.css");
document.getElementsByTagName("head")[0].appendChild(link);
};
function create_additional_css() {
var sheet = document.createElement('style')
sheet.innerHTML = "#toc-level0 li > span:hover { background-color: " + cfg.colors.hover_highlight + " }\n" +
".toc-item-highlight-select {background-color: " + cfg.colors.selected_highlight + "}\n" +
".toc-item-highlight-execute {background-color: " + cfg.colors.running_highlight + "}\n" +
".toc-item-highlight-execute.toc-item-highlight-select {background-color: " + cfg.colors.selected_highlight + "}"
if (cfg.moveMenuLeft){
sheet.innerHTML += "div#menubar-container, div#header-container {\n"+
"width: auto;\n"+
"padding-left: 20px; }"
}
// Using custom colors
sheet.innerHTML += ".float-wrapper, .sidebar-wrapper { background-color: " + cfg.colors.wrapper_background + "}";
sheet.innerHTML += "#toc-level0 a, #navigate_menu a, .toc { color: " + cfg.colors.navigate_text + "}";
sheet.innerHTML += "#toc-wrapper .toc-item-num { color: " + cfg.colors.navigate_num + "}";
sheet.innerHTML += ".sidebar-wrapper { border-color: " + cfg.colors.sidebar_border + "}";
document.body.appendChild(sheet);
}
var CodeCell = codecell.CodeCell;
function patch_CodeCell_get_callbacks() {
var previous_get_callbacks = CodeCell.prototype.get_callbacks;
CodeCell.prototype.get_callbacks = function() {
var that = this;
var callbacks = previous_get_callbacks.apply(this, arguments);
var prev_reply_callback = callbacks.shell.reply;
callbacks.shell.reply = function(msg) {
if (msg.msg_type === 'execute_reply') {
setTimeout(function(){
$('.toc .toc-item-highlight-execute').removeClass('toc-item-highlight-execute');
rehighlight_running_cells() // re-highlight running cells
}, 100);
var c = IPython.notebook.get_selected_cell();
highlight_toc_item({ type: 'selected' }, { cell: c })
}
return prev_reply_callback(msg);
};
return callbacks;
};
}
var load_css = function() {
var link = document.createElement("link");
link.type = "text/css";
link.rel = "stylesheet";
link.href = require.toUrl("./main.css");
document.getElementsByTagName("head")[0].appendChild(link);
};
function excute_codecell_callback(evt, data) {
var cell = data.cell;
highlight_toc_item(evt, data);
}
function rehighlight_running_cells() {
$.each($('.running'), // re-highlight running cells
function(idx, elt) {
highlight_toc_item({ type: "execute" }, $(elt).data())
}
)
}
var toc_init = function() {
// read configuration, then call toc
cfg = read_config(cfg, function() { table_of_contents(cfg, st); }); // called after config is stable
// event: render toc for each markdown cell modification
$([IPython.events]).on("rendered.MarkdownCell",
function(evt, data) {
table_of_contents(cfg, st); // recompute the toc
rehighlight_running_cells() // re-highlight running cells
highlight_toc_item(evt, data); // and of course the one currently rendered
});
// event: on cell selection, highlight the corresponding item
$([IPython.events]).on('select.Cell', highlight_toc_item)
// event: if kernel_ready (kernel change/restart): add/remove a menu item
$([IPython.events]).on("kernel_ready.Kernel", function() {
addSaveAsWithToc();
})
// add a save as HTML with toc included
addSaveAsWithToc();
//
// Highlight cell on execution
patch_CodeCell_get_callbacks()
$([Jupyter.events]).on('execute.CodeCell', excute_codecell_callback);
}
function create_additional_css() {
var sheet = document.createElement('style')
sheet.innerHTML = "#toc-level0 li > span:hover { background-color: " + cfg.colors.hover_highlight + " }\n" +
".toc-item-highlight-select {background-color: " + cfg.colors.selected_highlight + "}\n" +
".toc-item-highlight-execute {background-color: " + cfg.colors.running_highlight + "}\n" +
".toc-item-highlight-execute.toc-item-highlight-select {background-color: " + cfg.colors.selected_highlight + "}"
if (cfg.moveMenuLeft) {
sheet.innerHTML += "div#menubar-container, div#header-container {\n" +
"width: auto;\n" +
"padding-left: 20px; }"
}
// Using custom colors
sheet.innerHTML += ".float-wrapper, .sidebar-wrapper { background-color: " + cfg.colors.wrapper_background + "}";
sheet.innerHTML += "#toc-level0 a, #navigate_menu a, .toc { color: " + cfg.colors.navigate_text + "}";
sheet.innerHTML += "#toc-wrapper .toc-item-num { color: " + cfg.colors.navigate_num + "}";
sheet.innerHTML += ".sidebar-wrapper { border-color: " + cfg.colors.sidebar_border + "}";
sheet.innerHTML += ".highlight_on_scroll { border-left: solid 4px " + cfg.colors.on_scroll + '}';
document.body.appendChild(sheet);
}
var load_ipython_extension = function() {
load_css(); //console.log("Loading css")
toc_button(); //console.log("Adding toc_button")
var CodeCell = codecell.CodeCell;
// Wait for the notebook to be fully loaded
if (Jupyter.notebook !== undefined && Jupyter.notebook._fully_loaded) {
// this tests if the notebook is fully loaded
console.log("[toc2] Notebook fully loaded -- toc2 initialized ")
toc_init();
} else {
console.log("[toc2] Waiting for notebook availability")
$([Jupyter.events]).on("notebook_loaded.Notebook", function() {
console.log("[toc2] toc2 initialized (via notebook_loaded)")
toc_init();
})
}
function patch_CodeCell_get_callbacks() {
};
var previous_get_callbacks = CodeCell.prototype.get_callbacks;
CodeCell.prototype.get_callbacks = function() {
var that = this;
var callbacks = previous_get_callbacks.apply(this, arguments);
var prev_reply_callback = callbacks.shell.reply;
callbacks.shell.reply = function(msg) {
if (msg.msg_type === 'execute_reply') {
setTimeout(function() {
$('.toc .toc-item-highlight-execute').removeClass('toc-item-highlight-execute');
rehighlight_running_cells() // re-highlight running cells
}, 100);
var c = IPython.notebook.get_selected_cell();
highlight_toc_item({
type: 'selected'
}, {
cell: c
})
}
return prev_reply_callback(msg);
};
return callbacks;
};
}
return {
load_ipython_extension : load_ipython_extension,
toggle_toc : toggle_toc,
table_of_contents : table_of_contents
};
function excute_codecell_callback(evt, data) {
var cell = data.cell;
highlight_toc_item(evt, data);
}
function rehighlight_running_cells() {
$.each($('.running'), // re-highlight running cells
function(idx, elt) {
highlight_toc_item({
type: "execute"
}, $(elt).data())
}
)
}
var toc_init = function() {
// read configuration, then call toc
cfg = read_config(cfg, function() {
table_of_contents(cfg, st);
}); // called after config is stable
// event: render toc for each markdown cell modification
$([IPython.events]).on("rendered.MarkdownCell",
function(evt, data) {
table_of_contents(cfg, st); // recompute the toc
rehighlight_running_cells() // re-highlight running cells
highlight_toc_item(evt, data); // and of course the one currently rendered
});
// event: on cell selection, highlight the corresponding item
$([IPython.events]).on('select.Cell', highlight_toc_item)
// event: if kernel_ready (kernel change/restart): add/remove a menu item
$([IPython.events]).on("kernel_ready.Kernel", function() {
addSaveAsWithToc();
})
// add a save as HTML with toc included
addSaveAsWithToc();
//
// Highlight cell on execution
patch_CodeCell_get_callbacks()
$([Jupyter.events]).on('execute.CodeCell', excute_codecell_callback);
}
var load_ipython_extension = function() {
load_css(); //console.log("Loading css")
toc_button(); //console.log("Adding toc_button")
// Wait for the notebook to be fully loaded
if (Jupyter.notebook !== undefined && Jupyter.notebook._fully_loaded) {
// this tests if the notebook is fully loaded
console.log("[toc2] Notebook fully loaded -- toc2 initialized ")
toc_init();
} else {
console.log("[toc2] Waiting for notebook availability")
$([Jupyter.events]).on("notebook_loaded.Notebook", function() {
console.log("[toc2] toc2 initialized (via notebook_loaded)")
toc_init();
})
}
};
return {
load_ipython_extension: load_ipython_extension,
toggle_toc: toggle_toc,
table_of_contents: table_of_contents
};
});
@@ -9,6 +9,8 @@
var IPython;
var events;
var liveNotebook = false;
var all_headers= $("#notebook").find(":header");
try {
// this will work in a live notebook because nbextensions & custom.js
// are loaded by/after notebook.js, which requires base/js/namespace
@@ -17,8 +19,8 @@
liveNotebook = true;
}
catch (err) {
// log the error, just in case we *are* in a live notebook
console.log('[toc2] working in non-live notebook:', err);
// We *are* theoretically in a non-live notebook
console.log('[toc2] working in non-live notebook'); //, err);
// in non-live notebook, there's no event structure, so we make our own
if (window.events === undefined) {
var Events = function () {};
@@ -162,7 +164,7 @@ function highlight_toc_item(evt, data) {
$('#notebook-container').css('margin-left', 30);
$('#notebook-container').css('width', $('#notebook').width() - 30);
} else { // original width
$("#notebook-container").css({'width':''})
$("#notebook-container").css({'width':'', 'margin-left':''})
}
}
} else {
@@ -170,7 +172,7 @@ function highlight_toc_item(evt, data) {
$('#notebook-container').css('margin-left', 30);
$('#notebook-container').css('width', $('#notebook').width() - 30);
} else { // original width
$("#notebook-container").css({'width':''})
$("#notebook-container").css({'width':'', 'margin-left':''})
}
}
}
@@ -367,6 +369,15 @@ function highlight_toc_item(evt, data) {
}
})
$("body").append(toc_wrapper);
// On header/menu/toolbar resize, resize the toc itself
// (if displayed as a sidebar)
if (liveNotebook) {
$([Jupyter.events]).on("resize-header.Page toggle-all-headers", function() {setSideBarHeight(cfg, st);});
}
// restore toc position at load
if(liveNotebook){
@@ -437,7 +448,46 @@ function highlight_toc_item(evt, data) {
}
}
//------------------------------------------------------------------
//----------------------------------------------------------------------------
// on scroll - mark the toc item corresponding to the first header visible in
// the viewport with 'highlight_on_scroll' class
// some elements from https://stackoverflow.com/questions/20791374/jquery-check-if-element-is-visible-in-viewport
function highlightTocItemOnScroll(cfg,st){
if(cfg.markTocItemOnScroll) {
var scrolling_elt = liveNotebook ? '#site' : window
$(scrolling_elt).scroll(function() {
var headerVisibleHeight = $('#header').is(':visible') ? $('#header').height() : 0
var headerHeight = liveNotebook ? headerVisibleHeight : 0
var bottom_of_screen = $(window).scrollTop() + $(scrolling_elt).height() + headerHeight;
var top_of_screen = $(window).scrollTop() + headerHeight;
//loop over all headers
all_headers.each(function (i, h) {
var top_of_element = $(h).offset().top;
// var bottom_of_element = $(h).offset().top + $(h).outerHeight();
if((bottom_of_screen > top_of_element) && (top_of_screen < top_of_element)){
// The element is visible
var trg_id = $(h).attr('data-toc-modified-id')
if (trg_id !== undefined) {
var highlighted_item = $('#toc a').filter(function (idx, elt) {
return $(elt).attr('data-toc-modified-id') === trg_id;
});
$('#toc .highlight_on_scroll').removeClass('highlight_on_scroll')
highlighted_item.parent().addClass('highlight_on_scroll')
}
return false;
}
else {
// The element is not visible
// If the current header is already below the viewport then break
if (bottom_of_screen < top_of_element) return false
else return
}
})
});
}
}
//----------------------------------------------------------------------------
// TOC CELL -- if cfg.toc_cell=true, add and update a toc cell in the notebook.
// This cell, initially at the very beginning, can be moved.
// Its contents are automatically updated.
@@ -531,8 +581,9 @@ var table_of_contents = function (cfg,st) {
var toc_wrapper = $("#toc-wrapper");
// var toc_index=0;
if (toc_wrapper.length === 0) {
create_toc_div(cfg,st);
if (toc_wrapper.length === 0) { // toc window doesn't exist at all
create_toc_div(cfg,st); // create it
highlightTocItemOnScroll(cfg, st); // initialize highlighting on scroll
}
var segments = [];
var ul = $("<ul/>").addClass("toc-item").attr('id','toc-level0');
@@ -553,7 +604,7 @@ var table_of_contents = function (cfg,st) {
var cell_toc_text = " # Table of Contents\n";
var depth = 1; //var depth = ol_depth(ol);
var li= ul;//yes, initialize li with ul!
var all_headers= $("#notebook").find(":header");
all_headers= $("#notebook").find(":header"); // update all_headers
var min_lvl = 1 + Number(Boolean(cfg.skip_h1_title)), lbl_ary = [];
for(; min_lvl <= 6; min_lvl++){ if(all_headers.is('h'+min_lvl)){break;} }
for(var i= min_lvl; i <= 6; i++){ lbl_ary[i - min_lvl]= 0; }
@@ -36,6 +36,10 @@ Parameters:
description: Display Table of Contents as a sidebar (otherwise as a floating window)
input_type: checkbox
default: true
- name: toc2.markTocItemOnScroll
description: Mark toc item of header in viewport when scrolling
input_type: checkbox
default: true
- name: toc2.widenNotebook
description: Widen the display area to fit the browser window (may be useful with sidebar option)
input_type: checkbox
@@ -61,6 +65,10 @@ Parameters:
input_type: color
description: Hover color in toc
default: "#DAA520"
- name: toc2.colors.on_scroll
input_type: color
description: Color of highlight mark on scrolling
default: '#2447f0'
- name: toc2.colors.selected_highlight
input_type: color
description: Color of sections with selected elements
@@ -33,17 +33,13 @@ $( document ).ready(function(){
'toc_cell': false, // useless here
'toc_window_display': true, // display the toc window
"toc_section_display": "block", // display toc contents in the window
'markTocItemOnScroll': {{ 'true' if nb.get('metadata', {}).get('toc', {}).get('markTocItemOnScroll', False) else 'false' }},
'sideBar':{{ 'true' if nb.get('metadata', {}).get('toc', {}).get('sideBar', False) else 'false' }}, // sidebar or floating window
'navigate_menu':false // navigation menu (only in liveNotebook -- do not change)
}
var st={}; // some variables used in the script
st.rendering_toc_cell = false;
st.config_loaded = false;
st.extension_initialized=false;
st.nbcontainer_marginleft = $('#notebook-container').css('margin-left')
st.nbcontainer_marginright = $('#notebook-container').css('margin-right')
st.nbcontainer_width = $('#notebook-container').css('width')
st.oldTocHeight = undefined
st.cell_toc = undefined;
st.toc_index=0;