/* Part of SWISH Author: Jan Wielemaker E-mail: J.Wielemaker@cs.vu.nl WWW: http://www.swi-prolog.org Copyright (C): 2015-2019, VU University Amsterdam CWI Amsterdam All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * @fileOverview * Manage the cell structure of a notebook modelled after IPython * NoteBook. The nodebook consists of a toolbar with a series of * buttons and manages a list of cells. The file defines two plugins * `notebook`, implementing the overall notebook and `nbCell`, * implementing a single cell. * * @version 0.2.0 * @author Jan Wielemaker, J.Wielemaker@vu.nl */ define(["jquery", "config", "tabbed", "form", "preferences", "modal", "prolog", "links", "utils", "cm/lib/codemirror", "editor", "laconic", "runner", "storage", "sha1", "printThis" ], function($, config, tabbed, form, preferences, modal, prolog, links, utils, CodeMirror) { var cellTypes = { "program": { label: "Program", prefix: "p" }, "query": { label: "Query", prefix: "q" }, "markdown": { label: "Markdown", prefix: "md" }, "html": { label: "HTML", prefix: "htm" } }; /* Support ```eval using Prolog mode */ CodeMirror.modes.eval = CodeMirror.modes.prolog; (function($) { var pluginName = 'notebook'; var clipboard = null; /** @lends $.fn.notebook */ var methods = { /** * Initialize a Prolog Notebook. * @param {Object} options * @param {String} [options.value] provides the initial content * @param {Boolean} [options.fullscreen] open notebook in fullscreen * mode. */ _init: function(options) { options = options || {}; return this.each(function() { var elem = $(this); var storage = {}; /* storage info */ var data = {}; /* private data */ var toolbar, content; elem.addClass("notebook"); elem.addClass("swish-event-receiver"); function notebookMenu() { var icon = $.el.span({ class: "glyphicon glyphicon-menu-hamburger" }); var menu = form.widgets.dropdownButton( icon, { divClass: "notebook-menu btn-transparent", ulClass: "pull-right", client: elem, actions: { "Delete cell": function() { this.notebook('delete'); }, "Copy cell": function() { this.notebook('copy'); }, "Paste cell": function() { this.notebook('paste'); }, "Move cell up": function() { this.notebook('up'); }, "Move cell down": function() { this.notebook('down'); }, "Insert cell": function() { this.notebook('insertBelow'); }, "--": "Overall options", "Clear all": function() { this.notebook('clear_all'); }, "Play": function() { this.notebook('run_all'); }, "Settings": function() { this.notebook('settings'); }, "---": "Notebook actions", "Exit fullscreen": function() { this.notebook('fullscreen', false) } } }); return menu; } elem.append(toolbar = $.el.div({ class: "nb-toolbar" }, glyphButton("trash", "delete", "Delete cell", "warning"), sep(), glyphButton("copy", "copy", "Copy cell", "default"), glyphButton("paste", "paste", "Paste cell below", "default"), sep(), glyphButton("chevron-up", "up", "Move cell up", "default"), glyphButton("chevron-down", "down", "Move cell down", "default"), sep(), glyphButton("plus", "insertBelow", "Insert cell below", "primary"), sep(), glyphButton("erase", "clear_all", "Clear all query output", "warning"), glyphButton("play", "run_all", "Run all queries", "primary"), glyphButton("wrench", "settings", "Settings", "default"), glyphButton("fullscreen", "fullscreen", "Full screen", "default") )); elem.append(notebookMenu()); elem.append($.el.div({ class: "nb-view", tabIndex: "-1" }, content = $.el.div({ class: "nb-content" }), $.el.div({ class: "nb-bottom" }))); $(toolbar).on("click", "a.btn", function(ev) { var action = $(ev.target).closest("a").data("action"); elem.notebook(action); ev.preventDefault(); return false; }); $(content).on("click", ".nb-cell-buttons a.btn", function(ev) { var a = $(ev.target).closest("a"); var cell = a.closest(".nb-cell"); var action = a.data("action"); cell.nbCell(action); ev.preventDefault(); return false; }); $(content).on("mouseenter mouseleave", ".nb-menu-sense", function(ev) { var mdiv = $(ev.target).closest(".nb-menu"); if (ev.type == "mouseenter") { var select = cell_type_select_div(); mdiv.find(".nb-menu-line").css("background-color", "#333"); function removeSelector() { select.remove(); mdiv.find(".nb-menu-line").css("background-color", "#fff"); data.menu_state = "idle"; } select.hide(); mdiv.append(select); data.menu_state = "waiting"; setTimeout(function() { if (mdiv.find(":hover").length > 0) { data.menu_state = "showing"; select.on("mouseleave", removeSelector); select.fadeIn(400); } else { removeSelector(); } }, 250); } else { if (data.menu_state != "showing") mdiv.find(".nb-menu-line").css("background-color", "#fff"); } }); $(content).on("click", ".nb-menu .btn", function(ev) { ev.preventDefault(); var type = $(ev.target).data('type'); var mdiv = $(ev.target).closest(".nb-menu"); var nb = mdiv.closest(".notebook"); var cell = $.el.div({ class: "nb-cell" }); if (mdiv.parent().hasClass("nb-placeholder")) { nb.find(".nb-content").empty().append(cell); } else { mdiv.find(".nb-type-select").remove(); mdiv.after(cell); } $(cell).nbCell({ type: type }); nb.notebook('organize'); nb.notebook('active', $(cell), true); return false; }); elem.focusin(function(ev) { var cell = $(ev.target).closest(".nb-cell"); if (cell.length > 0) { elem.notebook('active', cell); } else if ($(ev.target).closest(".nb-view").length > 0) { elem.find(".nb-content").children(".nb-cell.active") .nbCell('active', false); } }); elem.focusout(function(ev) { if ($(ev.target).closest(".notebook")[0] != elem[0]) { elem.find(".nb-content").children(".nb-cell.active") .nbCell('active', false); } }); /* Activate the active source or first source. If the active * cell is a query, we could activate the source of the query? */ elem.on("activate-tab", function(ev) { if (ev.target == elem[0]) { var eds = elem.find(".nb-content") .children(".nb-cell.program"); var aeds = eds.filter(".active"); var nc = aeds[0] || eds[0]; if (nc) { $(nc).find(".prolog-editor").prologEditor('makeCurrent'); } ev.stopPropagation(); } }); /* monitor output on runners */ elem.on("scroll-to-bottom", function(ev, arg) { if (arg != true) { $(ev.target).closest(".nb-cell").nbCell('ensure_in_view', 'bottom'); } }); elem.data(pluginName, data); /* store with element */ /* restore content */ var content = elem.find(".notebook-data"); if (options.value) { elem.notebook('value', options.value); } else if (content.length > 0) { function copyData(name) { var value = content.data(name); if (value) { storage[name] = value; } } copyData("file"); copyData("url"); copyData("title"); copyData("meta"); copyData("st_type"); copyData("chats"); var docid = elem.storage('docid', undefined, storage); var fullscreen = preferences.getDocVal( docid, 'fullscreen', config.swish.notebook && config.swish.notebook.fullscreen); elem.notebook('value', content.text(), { fullscreen: fullscreen }); content.remove(); } else { elem.notebook('placeHolder'); } elem.notebook('setupStorage', storage); elem.on("data-is-clean", function(ev, clean) { if ($(ev.target).hasClass("prolog-editor")) { elem.notebook('checkModified'); ev.stopPropagation(); return false; } }); elem.on("fullscreen", function(ev, val) { preferences.setDocVal(docid, 'fullscreen', val); }); elem.on("print", function(ev) { if ($(ev.target).hasClass("notebook") && $(ev.target).is(":visible")) elem.notebook('print'); ev.stopPropagation(); }); }); /* end .each() */ }, /******************************* * BUTTON ACTIONS * *******************************/ delete: function(cell) { cell = cell || currentCell(this); if (cell) { this.notebook('active', cell.nbCell('next') || cell.nbCell('prev')); cell.nbCell('close'); this.notebook('updatePlaceHolder'); } this.notebook('checkModified'); return this; }, copy: function(cell) { cell = cell || currentCell(this); if (cell) { var dom = $.el.div({ class: "notebook" }); $(dom).append($(cell).nbCell('saveDOM')); $(dom).find(".nb-cell").removeAttr("name"); clipboard = stringifyNotebookDOM(dom); } }, paste: function(text) { var nb = this; text = text || clipboard; if (text) { var dom = $.el.div(); $(dom).html(text); var cells = $(dom).find(".nb-cell"); if (cells.length > 0) { $(dom).find(".nb-cell").each(function() { nb.notebook('insert', { where: "below", restore: $(this) }); }); return this; } else { modal.alert("Not a SWISH notebook"); } } else { modal.alert("Clipboard is empty"); } }, up: function(cell) { cell = cell || currentCell(this); if (cell) { cell.insertBefore(cell.nbCell('prev')); this.notebook('checkModified'); } return this; }, down: function(cell) { cell = cell || currentCell(this); if (cell) { cell.insertAfter(cell.nbCell('next')); this.notebook('checkModified'); } return this; }, insertAbove: function() { return this.notebook('insert', { where: "above" }); }, insertBelow: function() { if (this.notebook('insert', { where: "below", if_visible: true }) == false) { modal.alert("
New cell would appear outside the visible area of the " + "notebook." + "
Please select the cell below which you want the " +
"new cell to appear or scroll to the bottom of the " +
"notebook.");
}
return this;
},
getSettings: function() {
var settings = {
open_fullscreen: this.hasClass('open-fullscreen'),
hide_navbar: this.hasClass('hide-navbar')
};
return settings;
},
settings: function() {
var that = this;
var current = this[pluginName]('getSettings');
function notebookSettingsBody() {
this.append($.el.form({
class: "form-horizontal"
},
form.fields.checkboxes(
[{
name: "open_fullscreen",
label: "open in fullscreen mode",
value: current.open_fullscreen,
title: "Open in fullscreen mode"
}], {
col: 3,
label: "Initial view"
}),
form.fields.checkboxes(
[{
name: "hide_navbar",
label: "hide navigation bar",
value: current.hide_navbar,
title: "Hide navigation bar"
}], {
col: 3,
label: "Full screen options"
}),
form.fields.buttons({
label: "Apply",
offset: 3,
action: function(ev, values) {
function update(field, cls) {
if (values[field] != current[field]) {
if (values[field])
that.addClass(cls);
else
that.removeClass(cls);
}
}
update("hide_navbar", "hide-navbar");
update("open_fullscreen", "open-fullscreen");
that.notebook('checkModified');
}
})));
}
form.showDialog({
title: "Set options for notebook",
body: notebookSettingsBody
});
},
run: function(cell) {
cell = cell || currentCell(this);
if (cell)
cell.nbCell("run");
},
/**
* Set the notebook in fullscreen mode.
* @arg {Boolean} [val] if `true` or the notebook has the class
* `fullscreen`, go to fullscreen mode.
* @arg {Boolean} [hide_navbar] if `val = true` and this parameter
* is true, also hide the SWISH navigation bar.
*/
fullscreen: function(val, hide_navbar) {
if (val == undefined) /* default: toggle */
val = !this.hasClass("fullscreen");
if (hide_navbar == undefined)
hide_navbar = this.hasClass("hide-navbar");
if (val) {
var chat_container = this.closest(".chat-container");
var node = chat_container.length == 1 ? chat_container : this;
$("body.swish").swish('fullscreen', node, this, hide_navbar);
} else {
$("body.swish").swish('exitFullscreen');
}
return this;
},
cellType: function(cell, type) {
cell = cell || currentCell(this);
if (cell)
cell.nbCell('type', type);
},
/*******************************
* SELECTION *
*******************************/
getSelection: function() {
return this.notebook('assignCellNames')
.find(".prolog-editor")
.prologEditor('getSelection');
},
restoreSelection: function(sel) {
return this.notebook('assignCellNames')
.find(".prolog-editor")
.prologEditor('restoreSelection', sel);
},
/*******************************
* CLEAN/DIRTY *
*******************************/
checkModified: function() {
return this.each(function() {
var nb = $(this);
var store = nb.data("storage");
var clean = store.cleanGeneration == nb.notebook('changeGen');
nb.notebook('markClean', clean);
nb.notebook('organize');
});
},
/**
* Called if the notebook changes from clean to dirty or visa versa.
* This triggers `data-is-clean`, which is trapped by the tab to
* indicate the changed state of the editor.
*/
markClean: function(clean) {
return this.each(function() {
var nb = $(this);
var data = nb.data(pluginName);
if (data.clean_signalled != clean) {
data.clean_signalled = clean;
nb.trigger("data-is-clean", clean);
}
if (clean) {
nb.find(".prolog-editor").prologEditor('setIsClean');
}
});
},
/*******************************
* PRINT *
*******************************/
/**
* Print the content of the notebook through printThis(). Requires
* additional media rules to improve the styling.
*/
print: function() {
var elem = $(this);
elem.find(".nb-content").printThis({
// debug: true,
printDelay: 2000
});
return this;
},
/*******************************
* CELL MANAGEMENT *
*******************************/
/**
* @param {jQuery} cell is the cell that must be activated
* @param {Boolean} [focus] if `true`, give the cell the focus
*/
active: function(cell, focus) {
if (cell) {
var current = this.find(".nb-content .nb-cell.active");
function removeNotForQuery(elem) {
elem.find(".nb-content .nb-cell.not-for-query")
.removeClass("not-for-query");
}
if (cell.length == 1) {
if (!(current.length == 1 && cell[0] == current[0])) {
removeNotForQuery(this);
current.nbCell('active', false);
cell.nbCell('active', true);
if (focus) {
var editors = cell.find(".prolog-editor");
if (editors.length > 0)
editors.prologEditor('focus');
else
cell.focus();
}
}
} else {
removeNotForQuery(this);
current.nbCell('active', false);
}
}
},
/**
* Insert a new cell
* @param {Object} [options]
* @param {String} [options.where] defines where the cell is
* inserted relative to the cell with the current focus.
* @param {jQuery} [options.restore] If provided, it must contains
* a save/restore node that will be used to fill the new cell.
* @param {Bool} [options.if_visible] If `true`, only insert is
* the insertion point is visible.
*/
insert: function(options) {
options = options || {};
var relto = currentCell(this);
var cell = options.cell || $.el.div({
class: "nb-cell"
});
var view = this.find(".nb-view")
var viewrect;
if (options.if_visible) {
if (view.find(".nb-content > div.nb-cell").length > 0)
viewrect = view[0].getBoundingClientRect();
}
if (relto) {
if (options.where == 'above') {
if (viewrect) {
var seltop = relto[0].getBoundingClientRect().top;
if (seltop < viewrect.top)
return false;
}
$(cell).insertBefore(relto);
} else {
if (viewrect) {
var selbottom = relto[0].getBoundingClientRect().bottom;
if (selbottom > viewrect.bottom - 20)
return false;
}
$(cell).insertAfter(relto);
}
} else {
var content = this.find(".nb-content");
if (viewrect) {
var cbottom = content[0].getBoundingClientRect().bottom;
if (cbottom > viewrect.bottom - 20)
return false;
}
content.append(cell);
}
if (!options.cell) {
$(cell).nbCell(options.restore);
}
$(cell).nbCell('assignName');
this.notebook('updatePlaceHolder');
this.notebook('active', $(cell));
this.notebook('checkModified');
return this;
},
/**
* Organize the notebook. This maintains the section hierarchy
* and places a hover menu to insert a new cell
*/
organize: function() {
var notebook = this;
var content = this.find(".nb-content");
var cells = content.children(".nb-cell");
// ensure there is a menu before and after each cell
cells.each(function() {
var cell = $(this);
if (!cell.prev().hasClass("nb-menu"))
cell.before(notebook_menu());
if (!cell.next().hasClass("nb-menu"))
cell.after(notebook_menu());
});
// remove duplicate menus
content.children(".nb-menu").each(function() {
var menu = $(this);
if (menu.next().hasClass("nb-menu"))
menu.remove();
});
},
/*******************************
* SAVE/RESTORE *
*******************************/
/**
* Setup connection to the storage manager.
*/
setupStorage: function(storage) {
var notebook = this;
storage = $.extend(storage, {
getValue: function() {
return notebook.notebook('value');
},
setValue: function(source) {
return notebook.notebook('setSource', source);
},
changeGen: function() {
return notebook.notebook('changeGen');
},
isClean: function(gen) {
var cgen = notebook.notebook('changeGen');
return gen == cgen;
},
markClean: function(clean) {
notebook.notebook('markClean', clean);
},
cleanGeneration: this.notebook('changeGen'),
cleanData: this.notebook('value'),
cleanCheckpoint: "load",
typeName: "notebook"
});
return this.storage(storage);
},
/**
* Set the source
*/
setSource: function(source) {
if (typeof(source) == "string")
source = {
data: source
};
this.notebook('value', source.data);
},
/**
* Set or get the state of this notebook as a string.
* @param {Object} options
* @param {Boolean} [options.skipEmpty=false] if `true`, do not save
* empty cells.
* @param {Boolean} [options.fullscreen] if `true', go fullscreen.
* Default is `true` if the toplevel `div.notebook` has a class
* `fullscreen`.
* @param [String] val is an HTML string that represents
* the notebook state.
*/
value: function(val, options) {
options = options || {};
if (val == undefined) {
var classes = this[pluginName]('getClasses');
classes.unshift("notebook");
var dom = $.el.div({
class: classes.join(" ")
});
this.notebook('assignCellNames', false);
this.find(".nb-cell").each(function() {
cell = $(this);
if (!(options.skipEmpty && cell.nbCell('isEmpty')))
$(dom).append(cell.nbCell('saveDOM'));
});
var str = stringifyNotebookDOM(dom);
// debugger;
return str;
} else {
var notebook = this;
var content = this.find(".nb-content");
var dom = $.el.div();
var isnew = content.children(".nb-cell").length == 0;
content.html("");
dom.innerHTML = val; /* do not execute scripts */
var outer_div = $(dom).find("div.notebook");
this.removeClass("fullscreen hide-navbar");
if (outer_div.hasClass("open-fullscreen")) {
options.fullscreen = true;
this.addClass("open-fullscreen");
} else if (outer_div.hasClass("fullscreen")) {
options.fullscreen = true;
this.removeClass("fullscreen");
}
if (outer_div.hasClass("hide-navbar")) {
options.hide_navbar = true;
this.addClass("hide-navbar");
}
if (isnew && options.fullscreen) {
this.notebook('fullscreen', true, options.hide_navbar);
}
var thenbcells = $(dom).find(".nb-cell");
if (isnew) {
//debugger;
} else {
//debugger;
}
thenbcells.each(function() {
var cell = $.el.div({
class: "nb-cell"
});
content.append(cell);
$(cell).nbCell($(this));
});
var thisnbcell = this.find(".nb-cell");
thisnbcell.nbCell('onload');
if (isnew) {
//debugger;
} else {
//debugger;
}
this.notebook('run_all', 'onload');
this.notebook('updatePlaceHolder');
this.notebook('assignCellNames', false);
this.notebook('organize');
}
},
/**
* @return {Array} of class names that are preserved.
*/
getClasses: function() {
var found = this.attr("class").split(" ");
var classes = [];
var allowed = ["open-fullscreen", "hide-navbar"];
for (var i = 0; i < found.length; i++) {
if (allowed.indexOf(found[i]) >= 0)
classes.push(found[i]);
}
return classes.sort();
},
/**
* Compute a state fingerprint for the entire notebook
* @return {String} SHA1 fingerprint
*/
changeGen: function() {
var list = this[pluginName]('getClasses');
this.find(".nb-cell").each(function() {
var cg = $(this).nbCell('changeGen');
list.push(cg);
});
return sha1(list.join());
},
/**
* Assign names to all cells. This is normally done as the
* notebook is created, but needs to be done for old notebooks
* if functions are used that require named cells. Calling this
* method has no effect if all cells already have a name.
*/
assignCellNames: function(check) {
this.find(".nb-cell").nbCell('assignName');
if (check != false)
this.notebook('checkModified');
return this;
},
/*******************************
* HELP *
*******************************/
updatePlaceHolder: function() {
if (this.find(".nb-content").find(".nb-cell").length == 0)
this.notebook('placeHolder');
else
this.find(".nb-placeholder").remove();
},
placeHolder: function() {
var menu = notebook_menu();
var select = cell_type_select_div();
var placeholder = $.el.div({
class: "nb-placeholder"
});
var a;
$(menu).append(select);
placeholder.append(
menu,
$.el.div({
class: "nb-help"
},
"New here? See the notebook ",
a = $.el.a("help page"),
"."));
$(a).on("click", function() {
$(".swish-event-receiver").trigger("help", {
file: "notebook.html"
});
});
this.find(".nb-content").append(placeholder);
return this;
},
/**
* Run the notebook
*/
run_all: function(why) {
var queries = [];
why = why || 'all';
this.notebook('clear_all');
this.find(".nb-cell.query").each(function() {
if (why == 'all' || $(this).data('run') == why)
queries.push(this);
});
function cont(pengine) {
switch (pengine.state) {
case 'error':
case 'aborted':
return false;
}
return true;
}
if (queries.length > 0) {
queries.current = 0;
var complete = function(pengine) {
if (cont(pengine) &&
++queries.current < queries.length) {
$(queries[queries.current]).nbCell('run', {
complete: complete
})
}
};
$(queries[0]).nbCell('run', {
complete: complete
});
}
},
/**
* Erase all query output, killing possibly running queries
*/
clear_all: function() {
this.find(".prolog-runner").prologRunner('close');
}
}; // methods
//