##// END OF EJS Templates
Some JS test fixes
Jonathan Frederic -
Show More
@@ -1,158 +1,161
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 ], function(IPython, $) {
8 8 "use strict";
9 9
10 var modal = function (options, keyboard_manager, notebook) {
10 var modal = function (options) {
11
11 12 var modal = $("<div/>")
12 13 .addClass("modal")
13 14 .addClass("fade")
14 15 .attr("role", "dialog");
15 16 var dialog = $("<div/>")
16 17 .addClass("modal-dialog")
17 18 .appendTo(modal);
18 19 var dialog_content = $("<div/>")
19 20 .addClass("modal-content")
20 21 .appendTo(dialog);
21 22 dialog_content.append(
22 23 $("<div/>")
23 24 .addClass("modal-header")
24 25 .append($("<button>")
25 26 .attr("type", "button")
26 27 .addClass("close")
27 28 .attr("data-dismiss", "modal")
28 29 .attr("aria-hidden", "true")
29 30 .html("&times;")
30 31 ).append(
31 32 $("<h4/>")
32 33 .addClass('modal-title')
33 34 .text(options.title || "")
34 35 )
35 36 ).append(
36 37 $("<div/>").addClass("modal-body").append(
37 38 options.body || $("<p/>")
38 39 )
39 40 );
40 41
41 42 var footer = $("<div/>").addClass("modal-footer");
42 43
43 44 for (var label in options.buttons) {
44 45 var btn_opts = options.buttons[label];
45 46 var button = $("<button/>")
46 47 .addClass("btn btn-default btn-sm")
47 48 .attr("data-dismiss", "modal")
48 49 .text(label);
49 50 if (btn_opts.click) {
50 51 button.click($.proxy(btn_opts.click, dialog_content));
51 52 }
52 53 if (btn_opts.class) {
53 54 button.addClass(btn_opts.class);
54 55 }
55 56 footer.append(button);
56 57 }
57 58 dialog_content.append(footer);
58 59 // hook up on-open event
59 60 modal.on("shown.bs.modal", function() {
60 61 setTimeout(function() {
61 62 footer.find("button").last().focus();
62 63 if (options.open) {
63 64 $.proxy(options.open, modal)();
64 65 }
65 66 }, 0);
66 67 });
67 68
68 69 // destroy modal on hide, unless explicitly asked not to
69 70 if (options.destroy === undefined || options.destroy) {
70 71 modal.on("hidden.bs.modal", function () {
71 72 modal.remove();
72 73 });
73 74 }
74 75 modal.on("hidden.bs.modal", function () {
75 if (notebook) {
76 var cell = notebook.get_selected_cell();
76 if (options.notebook) {
77 var cell = options.notebook.get_selected_cell();
77 78 if (cell) cell.select();
78 keyboard_manager.enable();
79 keyboard_manager.command_mode();
79 if (options.keyboard_manager) {
80 options.keyboard_manager.enable();
81 options.keyboard_manager.command_mode();
82 }
80 83 }
81 84 });
82 85
83 if (keyboard_manager) {
84 keyboard_manager.disable();
86 if (options.keyboard_manager) {
87 options.keyboard_manager.disable();
85 88 }
86 89
87 90 return modal.modal(options);
88 91 };
89 92
90 93 var edit_metadata = function (md, callback, name, keyboard_manager, notebook) {
91 94 name = name || "Cell";
92 95 var error_div = $('<div/>').css('color', 'red');
93 96 var message =
94 97 "Manually edit the JSON below to manipulate the metadata for this " + name + "." +
95 98 " We recommend putting custom metadata attributes in an appropriately named sub-structure," +
96 99 " so they don't conflict with those of others.";
97 100
98 101 var textarea = $('<textarea/>')
99 102 .attr('rows', '13')
100 103 .attr('cols', '80')
101 104 .attr('name', 'metadata')
102 105 .text(JSON.stringify(md || {}, null, 2));
103 106
104 107 var dialogform = $('<div/>').attr('title', 'Edit the metadata')
105 108 .append(
106 109 $('<form/>').append(
107 110 $('<fieldset/>').append(
108 111 $('<label/>')
109 112 .attr('for','metadata')
110 113 .text(message)
111 114 )
112 115 .append(error_div)
113 116 .append($('<br/>'))
114 117 .append(textarea)
115 118 )
116 119 );
117 120 var editor = CodeMirror.fromTextArea(textarea[0], {
118 121 lineNumbers: true,
119 122 matchBrackets: true,
120 123 indentUnit: 2,
121 124 autoIndent: true,
122 125 mode: 'application/json',
123 126 });
124 127 var modal = modal({
125 128 title: "Edit " + name + " Metadata",
126 129 body: dialogform,
127 130 buttons: {
128 131 OK: { class : "btn-primary",
129 132 click: function() {
130 133 // validate json and set it
131 134 var new_md;
132 135 try {
133 136 new_md = JSON.parse(editor.getValue());
134 137 } catch(e) {
135 138 console.log(e);
136 139 error_div.text('WARNING: Could not save invalid JSON.');
137 140 return false;
138 141 }
139 142 callback(new_md);
140 143 }
141 144 },
142 145 Cancel: {}
143 146 }
144 147 }, keyboard_manager, notebook);
145 148
146 149 modal.on('shown.bs.modal', function(){ editor.refresh(); });
147 150 };
148 151
149 152 var dialog = {
150 153 modal : modal,
151 154 edit_metadata : edit_metadata,
152 155 };
153 156
154 157 // Backwards compatability.
155 158 IPython.Dialog = dialog;
156 159
157 160 return dialog;
158 161 });
@@ -1,117 +1,121
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'components/google-caja/html-css-sanitizer-minified',
8 8 ], function(IPython, $) {
9 9 "use strict";
10 10
11 11 var noop = function (x) { return x; };
12 12
13 13 var caja;
14 14 if (window && window.html) {
15 15 caja = window.html;
16 16 caja.html4 = window.html4;
17 17 caja.sanitizeStylesheet = window.sanitizeStylesheet;
18 18 }
19 19
20 20 var sanitizeAttribs = function (tagName, attribs, opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger) {
21 21 // add trusting data-attributes to the default sanitizeAttribs from caja
22 22 // this function is mostly copied from the caja source
23 23 var ATTRIBS = caja.html4.ATTRIBS;
24 24 for (var i = 0; i < attribs.length; i += 2) {
25 25 var attribName = attribs[i];
26 26 if (attribName.substr(0,5) == 'data-') {
27 27 var attribKey = '*::' + attribName;
28 28 if (!ATTRIBS.hasOwnProperty(attribKey)) {
29 29 ATTRIBS[attribKey] = 0;
30 30 }
31 31 }
32 32 }
33 33 return caja.sanitizeAttribs(tagName, attribs, opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger);
34 34 };
35 35
36 36 var sanitize_css = function (css, tagPolicy) {
37 37 // sanitize CSS
38 38 // like sanitize_html, but for CSS
39 39 // called by sanitize_stylesheets
40 40 return caja.sanitizeStylesheet(
41 41 window.location.pathname,
42 42 css,
43 43 {
44 44 containerClass: null,
45 45 idSuffix: '',
46 46 tagPolicy: tagPolicy,
47 47 virtualizeAttrName: noop
48 48 },
49 49 noop
50 50 );
51 51 };
52 52
53 53 var sanitize_stylesheets = function (html, tagPolicy) {
54 54 // sanitize just the css in style tags in a block of html
55 55 // called by sanitize_html, if allow_css is true
56 56 var h = $("<div/>").append(html);
57 57 var style_tags = h.find("style");
58 58 if (!style_tags.length) {
59 59 // no style tags to sanitize
60 60 return html;
61 61 }
62 62 style_tags.each(function(i, style) {
63 63 style.innerHTML = sanitize_css(style.innerHTML, tagPolicy);
64 64 });
65 65 return h.html();
66 66 };
67 67
68 68 var sanitize_html = function (html, allow_css) {
69 69 // sanitize HTML
70 70 // if allow_css is true (default: false), CSS is sanitized as well.
71 71 // otherwise, CSS elements and attributes are simply removed.
72 72 var html4 = caja.html4;
73 73
74 74 if (allow_css) {
75 75 // allow sanitization of style tags,
76 76 // not just scrubbing
77 77 html4.ELEMENTS.style &= ~html4.eflags.UNSAFE;
78 78 html4.ATTRIBS.style = html4.atype.STYLE;
79 79 } else {
80 80 // scrub all CSS
81 81 html4.ELEMENTS.style |= html4.eflags.UNSAFE;
82 82 html4.ATTRIBS.style = html4.atype.SCRIPT;
83 83 }
84 84
85 85 var record_messages = function (msg, opts) {
86 86 console.log("HTML Sanitizer", msg, opts);
87 87 };
88 88
89 89 var policy = function (tagName, attribs) {
90 90 if (!(html4.ELEMENTS[tagName] & html4.eflags.UNSAFE)) {
91 91 return {
92 92 'attribs': sanitizeAttribs(tagName, attribs,
93 93 noop, noop, record_messages)
94 94 };
95 95 } else {
96 96 record_messages(tagName + " removed", {
97 97 change: "removed",
98 98 tagName: tagName
99 99 });
100 100 }
101 101 };
102 102
103 103 var sanitized = caja.sanitizeWithPolicy(html, policy);
104 104
105 105 if (allow_css) {
106 106 // sanitize style tags as stylesheets
107 107 sanitized = sanitize_stylesheets(result.sanitized, policy);
108 108 }
109 109
110 110 return sanitized;
111 111 };
112 112
113 return {
113 var security = {
114 114 caja: caja,
115 115 sanitize_html: sanitize_html
116 116 };
117
118 IPython.security = security;
119
120 return security;
117 121 });
@@ -1,132 +1,136
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 var ipython = ipython || {};
5 4 require([
6 5 'base/js/namespace',
7 6 'jquery',
8 7 'notebook/js/notebook',
9 8 'base/js/utils',
9 'base/js/keyboard',
10 10 'base/js/page',
11 11 'notebook/js/layoutmanager',
12 12 'base/js/events',
13 13 'auth/js/loginwidget',
14 14 'notebook/js/maintoolbar',
15 15 'notebook/js/pager',
16 16 'notebook/js/quickhelp',
17 17 'notebook/js/menubar',
18 18 'notebook/js/notificationarea',
19 19 'notebook/js/savewidget',
20 20 'notebook/js/keyboardmanager',
21 21 'notebook/js/config',
22 22 ], function(
23 23 IPython,
24 24 $,
25 25 notebook,
26 26 utils,
27 keyboard,
27 28 page,
28 29 layoutmanager,
29 30 events,
30 31 loginwidget,
31 32 maintoolbar,
32 33 pager,
33 34 quickhelp,
34 35 menubar,
35 36 notificationarea,
36 37 savewidget,
37 38 keyboardmanager,
38 39 config
39 40 ) {
40 41 "use strict";
41 42
42 43 $('#ipython-main-app').addClass('border-box-sizing');
43 44 $('div#notebook_panel').addClass('border-box-sizing');
44 45
45 46 var common_options = {
46 47 base_url : utils.get_body_data("baseUrl"),
47 48 notebook_path : utils.get_body_data("notebookPath"),
48 49 notebook_name : utils.get_body_data('notebookName')
49 50 };
50 51
51 52 var user_config = $.extend({}, config.default_config);
52 53 var page = new page.Page();
53 54 var layout_manager = new layoutmanager.LayoutManager();
54 55 var events = $([new events.Events()]);
55 56 var pager = new pager.Pager('div#pager', 'div#pager_splitter', {
56 57 layout_manager: layout_manager,
57 58 events: events});
58 59 var keyboard_manager = new keyboardmanager.KeyboardManager({
59 60 pager: pager,
60 61 events: events});
61 62 var save_widget = new savewidget.SaveWidget('span#save_widget', events);
62 63 var notebook = new notebook.Notebook('div#notebook', $.extend({
63 64 events: events,
64 65 keyboard_manager: keyboard_manager,
65 66 save_widget: save_widget,
66 67 config: user_config},
67 68 common_options));
68 69 var login_widget = new loginwidget.LoginWidget('span#login_widget', common_options);
69 70 var toolbar = new maintoolbar.MainToolBar('#maintoolbar-container', {
70 71 notebook: notebook,
71 72 events: events});
72 73 var quick_help = new quickhelp.QuickHelp({
73 74 keyboard_manager: keyboard_manager,
74 events: events});
75 events: events,
76 notebook: notebook});
75 77 var menubar = new menubar.MenuBar('#menubar', $.extend({
76 78 notebook: notebook,
77 79 layout_manager: layout_manager,
78 80 events: events,
79 81 save_widget: save_widget,
80 82 quick_help: quick_help},
81 83 common_options));
82 84 var notification_area = new notificationarea.NotificationArea(
83 85 '#notification_area', {
84 86 events: events,
85 87 save_widget: save_widget,
86 notebook: notebook});
88 notebook: notebook,
89 keyboard_manager: keyboard_manager});
87 90 notification_area.init_notification_widgets();
88 91
89 92 $('body').append('<div id="fonttest"><pre><span id="test1">x</span>'+
90 93 '<span id="test2" style="font-weight: bold;">x</span>'+
91 94 '<span id="test3" style="font-style: italic;">x</span></pre></div>');
92 95 var nh = $('#test1').innerHeight();
93 96 var bh = $('#test2').innerHeight();
94 97 var ih = $('#test3').innerHeight();
95 98 if(nh != bh || nh != ih) {
96 99 $('head').append('<style>.CodeMirror span { vertical-align: bottom; }</style>');
97 100 }
98 101 $('#fonttest').remove();
99 102
100 103 page.show();
101 104
102 105 layout_manager.do_resize();
103 106 var first_load = function () {
104 107 layout_manager.do_resize();
105 108 var hash = document.location.hash;
106 109 if (hash) {
107 110 document.location.hash = '';
108 111 document.location.hash = hash;
109 112 }
110 113 notebook.set_autosave_interval(notebook.minimum_autosave_interval);
111 114 // only do this once
112 115 events.off('notebook_loaded.Notebook', first_load);
113 116 };
114 117
115 118 events.on('notebook_loaded.Notebook', first_load);
116 119 events.trigger('app_initialized.NotebookApp');
117 120 notebook.load_notebook(common_options.notebook_name, common_options.notebook_path);
118 121
119 ipython.page = page;
120 ipython.layout_manager = layout_manager;
121 ipython.notebook = notebook;
122 ipython.pager = pager;
123 ipython.quick_help = quick_help;
124 ipython.login_widget = login_widget;
125 ipython.menubar = menubar;
126 ipython.toolbar = toolbar;
127 ipython.notification_area = notification_area;
128 ipython.events = events;
129 ipython.keyboard_manager = keyboard_manager;
130 ipython.save_widget = save_widget;
131 ipython.config = user_config;
122 IPython.page = page;
123 IPython.layout_manager = layout_manager;
124 IPython.notebook = notebook;
125 IPython.pager = pager;
126 IPython.quick_help = quick_help;
127 IPython.login_widget = login_widget;
128 IPython.menubar = menubar;
129 IPython.toolbar = toolbar;
130 IPython.notification_area = notification_area;
131 IPython.events = events;
132 IPython.keyboard_manager = keyboard_manager;
133 IPython.save_widget = save_widget;
134 IPython.config = user_config;
135 IPython.keyboard = keyboard;
132 136 });
@@ -1,2496 +1,2514
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'base/js/utils',
8 8 'base/js/dialog',
9 9 'notebook/js/textcell',
10 10 'notebook/js/codecell',
11 11 'services/sessions/js/session',
12 12 'notebook/js/celltoolbar',
13 13 'components/marked/lib/marked',
14 14 'notebook/js/mathjaxutils',
15 15 'base/js/keyboard',
16 16 'components/jquery-ui/ui/minified/jquery-ui.min',
17 17 'components/bootstrap/js/bootstrap.min',
18 18 ], function (
19 19 IPython,
20 20 $,
21 21 utils,
22 22 dialog,
23 23 textcell,
24 24 codecell,
25 25 session,
26 26 celltoolbar,
27 27 marked,
28 28 mathjaxutils,
29 29 keyboard
30 30 ) {
31 31
32 32 var Notebook = function (selector, options) {
33 33 // Constructor
34 34 //
35 35 // A notebook contains and manages cells.
36 36 //
37 37 // Parameters:
38 38 // selector: string
39 39 // options: dictionary
40 40 // Dictionary of keyword arguments.
41 41 // events: $(Events) instance
42 42 // keyboard_manager: KeyboardManager instance
43 43 // save_widget: SaveWidget instance
44 44 // config: dictionary
45 45 // base_url : string
46 46 // notebook_path : string
47 47 // notebook_name : string
48 48 this.config = options.config || {};
49 49 this.base_url = options.base_url;
50 50 this.notebook_path = options.notebook_path;
51 51 this.notebook_name = options.notebook_name;
52 52 this.events = options.events;
53 53 this.keyboard_manager = options.keyboard_manager;
54 54 this.save_widget = options.save_widget;
55 55 // TODO: This code smells (and the other `= this` line a couple lines down)
56 56 // We need a better way to deal with circular instance references.
57 57 this.keyboard_manager.notebook = this;
58 58 this.save_widget.notebook = this;
59 59
60 60 mathjaxutils.init();
61 61
62 62 if (marked) {
63 63 marked.setOptions({
64 64 gfm : true,
65 65 tables: true,
66 66 langPrefix: "language-",
67 67 highlight: function(code, lang) {
68 68 if (!lang) {
69 69 // no language, no highlight
70 70 return code;
71 71 }
72 72 var highlighted;
73 73 try {
74 74 highlighted = hljs.highlight(lang, code, false);
75 75 } catch(err) {
76 76 highlighted = hljs.highlightAuto(code);
77 77 }
78 78 return highlighted.value;
79 79 }
80 80 });
81 81 }
82 82
83 83 // Backwards compatability.
84 84 IPython.keyboard_manager = this.keyboard_manager;
85 85 IPython.save_widget = this.save_widget;
86 86 IPython.keyboard = this.keyboard;
87 87
88 88 this.element = $(selector);
89 89 this.element.scroll();
90 90 this.element.data("notebook", this);
91 91 this.next_prompt_number = 1;
92 92 this.session = null;
93 93 this.kernel = null;
94 94 this.clipboard = null;
95 95 this.undelete_backup = null;
96 96 this.undelete_index = null;
97 97 this.undelete_below = false;
98 98 this.paste_enabled = false;
99 99 // It is important to start out in command mode to match the intial mode
100 100 // of the KeyboardManager.
101 101 this.mode = 'command';
102 102 this.set_dirty(false);
103 103 this.metadata = {};
104 104 this._checkpoint_after_save = false;
105 105 this.last_checkpoint = null;
106 106 this.checkpoints = [];
107 107 this.autosave_interval = 0;
108 108 this.autosave_timer = null;
109 109 // autosave *at most* every two minutes
110 110 this.minimum_autosave_interval = 120000;
111 111 // single worksheet for now
112 112 this.worksheet_metadata = {};
113 113 this.notebook_name_blacklist_re = /[\/\\:]/;
114 114 this.nbformat = 3; // Increment this when changing the nbformat
115 115 this.nbformat_minor = 0; // Increment this when changing the nbformat
116 116 this.style();
117 117 this.create_elements();
118 118 this.bind_events();
119 119 this.save_notebook = function() { // don't allow save until notebook_loaded
120 120 this.save_notebook_error(null, null, "Load failed, save is disabled");
121 121 };
122 122 };
123 123
124 124 /**
125 125 * Tweak the notebook's CSS style.
126 126 *
127 127 * @method style
128 128 */
129 129 Notebook.prototype.style = function () {
130 130 $('div#notebook').addClass('border-box-sizing');
131 131 };
132 132
133 133 /**
134 134 * Create an HTML and CSS representation of the notebook.
135 135 *
136 136 * @method create_elements
137 137 */
138 138 Notebook.prototype.create_elements = function () {
139 139 var that = this;
140 140 this.element.attr('tabindex','-1');
141 141 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
142 142 // We add this end_space div to the end of the notebook div to:
143 143 // i) provide a margin between the last cell and the end of the notebook
144 144 // ii) to prevent the div from scrolling up when the last cell is being
145 145 // edited, but is too low on the page, which browsers will do automatically.
146 146 var end_space = $('<div/>').addClass('end_space');
147 147 end_space.dblclick(function (e) {
148 148 var ncells = that.ncells();
149 149 that.insert_cell_below('code',ncells-1);
150 150 });
151 151 this.element.append(this.container);
152 152 this.container.append(end_space);
153 153 };
154 154
155 155 /**
156 156 * Bind JavaScript events: key presses and custom IPython events.
157 157 *
158 158 * @method bind_events
159 159 */
160 160 Notebook.prototype.bind_events = function () {
161 161 var that = this;
162 162
163 163 this.events.on('set_next_input.Notebook', function (event, data) {
164 164 var index = that.find_cell_index(data.cell);
165 165 var new_cell = that.insert_cell_below('code',index);
166 166 new_cell.set_text(data.text);
167 167 that.dirty = true;
168 168 });
169 169
170 170 this.events.on('set_dirty.Notebook', function (event, data) {
171 171 that.dirty = data.value;
172 172 });
173 173
174 174 this.events.on('trust_changed.Notebook', function (event, data) {
175 175 that.trusted = data.value;
176 176 });
177 177
178 178 this.events.on('select.Cell', function (event, data) {
179 179 var index = that.find_cell_index(data.cell);
180 180 that.select(index);
181 181 });
182 182
183 183 this.events.on('edit_mode.Cell', function (event, data) {
184 184 that.handle_edit_mode(data.cell);
185 185 });
186 186
187 187 this.events.on('command_mode.Cell', function (event, data) {
188 188 that.handle_command_mode(data.cell);
189 189 });
190 190
191 191 this.events.on('status_autorestarting.Kernel', function () {
192 192 dialog.modal({
193 notebook: that,
194 keyboard_manager: that.keyboard_manager,
193 195 title: "Kernel Restarting",
194 196 body: "The kernel appears to have died. It will restart automatically.",
195 197 buttons: {
196 198 OK : {
197 199 class : "btn-primary"
198 200 }
199 201 }
200 202 });
201 203 });
202 204
203 205 var collapse_time = function (time) {
204 206 var app_height = $('#ipython-main-app').height(); // content height
205 207 var splitter_height = $('div#pager_splitter').outerHeight(true);
206 208 var new_height = app_height - splitter_height;
207 209 that.element.animate({height : new_height + 'px'}, time);
208 210 };
209 211
210 212 this.element.bind('collapse_pager', function (event, extrap) {
211 213 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
212 214 collapse_time(time);
213 215 });
214 216
215 217 var expand_time = function (time) {
216 218 var app_height = $('#ipython-main-app').height(); // content height
217 219 var splitter_height = $('div#pager_splitter').outerHeight(true);
218 220 var pager_height = $('div#pager').outerHeight(true);
219 221 var new_height = app_height - pager_height - splitter_height;
220 222 that.element.animate({height : new_height + 'px'}, time);
221 223 };
222 224
223 225 this.element.bind('expand_pager', function (event, extrap) {
224 226 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
225 227 expand_time(time);
226 228 });
227 229
228 230 // Firefox 22 broke $(window).on("beforeunload")
229 231 // I'm not sure why or how.
230 232 window.onbeforeunload = function (e) {
231 233 // TODO: Make killing the kernel configurable.
232 234 var kill_kernel = false;
233 235 if (kill_kernel) {
234 236 that.session.kill_kernel();
235 237 }
236 238 // if we are autosaving, trigger an autosave on nav-away.
237 239 // still warn, because if we don't the autosave may fail.
238 240 if (that.dirty) {
239 241 if ( that.autosave_interval ) {
240 242 // schedule autosave in a timeout
241 243 // this gives you a chance to forcefully discard changes
242 244 // by reloading the page if you *really* want to.
243 245 // the timer doesn't start until you *dismiss* the dialog.
244 246 setTimeout(function () {
245 247 if (that.dirty) {
246 248 that.save_notebook();
247 249 }
248 250 }, 1000);
249 251 return "Autosave in progress, latest changes may be lost.";
250 252 } else {
251 253 return "Unsaved changes will be lost.";
252 254 }
253 255 }
254 256 // Null is the *only* return value that will make the browser not
255 257 // pop up the "don't leave" dialog.
256 258 return null;
257 259 };
258 260 };
259 261
260 262 /**
261 263 * Set the dirty flag, and trigger the set_dirty.Notebook event
262 264 *
263 265 * @method set_dirty
264 266 */
265 267 Notebook.prototype.set_dirty = function (value) {
266 268 if (value === undefined) {
267 269 value = true;
268 270 }
269 271 if (this.dirty == value) {
270 272 return;
271 273 }
272 274 this.events.trigger('set_dirty.Notebook', {value: value});
273 275 };
274 276
275 277 /**
276 278 * Scroll the top of the page to a given cell.
277 279 *
278 280 * @method scroll_to_cell
279 281 * @param {Number} cell_number An index of the cell to view
280 282 * @param {Number} time Animation time in milliseconds
281 283 * @return {Number} Pixel offset from the top of the container
282 284 */
283 285 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
284 286 var cells = this.get_cells();
285 287 time = time || 0;
286 288 cell_number = Math.min(cells.length-1,cell_number);
287 289 cell_number = Math.max(0 ,cell_number);
288 290 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
289 291 this.element.animate({scrollTop:scroll_value}, time);
290 292 return scroll_value;
291 293 };
292 294
293 295 /**
294 296 * Scroll to the bottom of the page.
295 297 *
296 298 * @method scroll_to_bottom
297 299 */
298 300 Notebook.prototype.scroll_to_bottom = function () {
299 301 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
300 302 };
301 303
302 304 /**
303 305 * Scroll to the top of the page.
304 306 *
305 307 * @method scroll_to_top
306 308 */
307 309 Notebook.prototype.scroll_to_top = function () {
308 310 this.element.animate({scrollTop:0}, 0);
309 311 };
310 312
311 313 // Edit Notebook metadata
312 314
313 315 Notebook.prototype.edit_metadata = function () {
314 316 var that = this;
315 317 dialog.edit_metadata(this.metadata, function (md) {
316 318 that.metadata = md;
317 319 }, 'Notebook');
318 320 };
319 321
320 322 // Cell indexing, retrieval, etc.
321 323
322 324 /**
323 325 * Get all cell elements in the notebook.
324 326 *
325 327 * @method get_cell_elements
326 328 * @return {jQuery} A selector of all cell elements
327 329 */
328 330 Notebook.prototype.get_cell_elements = function () {
329 331 return this.container.children("div.cell");
330 332 };
331 333
332 334 /**
333 335 * Get a particular cell element.
334 336 *
335 337 * @method get_cell_element
336 338 * @param {Number} index An index of a cell to select
337 339 * @return {jQuery} A selector of the given cell.
338 340 */
339 341 Notebook.prototype.get_cell_element = function (index) {
340 342 var result = null;
341 343 var e = this.get_cell_elements().eq(index);
342 344 if (e.length !== 0) {
343 345 result = e;
344 346 }
345 347 return result;
346 348 };
347 349
348 350 /**
349 351 * Try to get a particular cell by msg_id.
350 352 *
351 353 * @method get_msg_cell
352 354 * @param {String} msg_id A message UUID
353 355 * @return {Cell} Cell or null if no cell was found.
354 356 */
355 357 Notebook.prototype.get_msg_cell = function (msg_id) {
356 358 return codecell.CodeCell.msg_cells[msg_id] || null;
357 359 };
358 360
359 361 /**
360 362 * Count the cells in this notebook.
361 363 *
362 364 * @method ncells
363 365 * @return {Number} The number of cells in this notebook
364 366 */
365 367 Notebook.prototype.ncells = function () {
366 368 return this.get_cell_elements().length;
367 369 };
368 370
369 371 /**
370 372 * Get all Cell objects in this notebook.
371 373 *
372 374 * @method get_cells
373 375 * @return {Array} This notebook's Cell objects
374 376 */
375 377 // TODO: we are often calling cells as cells()[i], which we should optimize
376 378 // to cells(i) or a new method.
377 379 Notebook.prototype.get_cells = function () {
378 380 return this.get_cell_elements().toArray().map(function (e) {
379 381 return $(e).data("cell");
380 382 });
381 383 };
382 384
383 385 /**
384 386 * Get a Cell object from this notebook.
385 387 *
386 388 * @method get_cell
387 389 * @param {Number} index An index of a cell to retrieve
388 390 * @return {Cell} A particular cell
389 391 */
390 392 Notebook.prototype.get_cell = function (index) {
391 393 var result = null;
392 394 var ce = this.get_cell_element(index);
393 395 if (ce !== null) {
394 396 result = ce.data('cell');
395 397 }
396 398 return result;
397 399 };
398 400
399 401 /**
400 402 * Get the cell below a given cell.
401 403 *
402 404 * @method get_next_cell
403 405 * @param {Cell} cell The provided cell
404 406 * @return {Cell} The next cell
405 407 */
406 408 Notebook.prototype.get_next_cell = function (cell) {
407 409 var result = null;
408 410 var index = this.find_cell_index(cell);
409 411 if (this.is_valid_cell_index(index+1)) {
410 412 result = this.get_cell(index+1);
411 413 }
412 414 return result;
413 415 };
414 416
415 417 /**
416 418 * Get the cell above a given cell.
417 419 *
418 420 * @method get_prev_cell
419 421 * @param {Cell} cell The provided cell
420 422 * @return {Cell} The previous cell
421 423 */
422 424 Notebook.prototype.get_prev_cell = function (cell) {
423 425 // TODO: off-by-one
424 426 // nb.get_prev_cell(nb.get_cell(1)) is null
425 427 var result = null;
426 428 var index = this.find_cell_index(cell);
427 429 if (index !== null && index > 1) {
428 430 result = this.get_cell(index-1);
429 431 }
430 432 return result;
431 433 };
432 434
433 435 /**
434 436 * Get the numeric index of a given cell.
435 437 *
436 438 * @method find_cell_index
437 439 * @param {Cell} cell The provided cell
438 440 * @return {Number} The cell's numeric index
439 441 */
440 442 Notebook.prototype.find_cell_index = function (cell) {
441 443 var result = null;
442 444 this.get_cell_elements().filter(function (index) {
443 445 if ($(this).data("cell") === cell) {
444 446 result = index;
445 447 }
446 448 });
447 449 return result;
448 450 };
449 451
450 452 /**
451 453 * Get a given index , or the selected index if none is provided.
452 454 *
453 455 * @method index_or_selected
454 456 * @param {Number} index A cell's index
455 457 * @return {Number} The given index, or selected index if none is provided.
456 458 */
457 459 Notebook.prototype.index_or_selected = function (index) {
458 460 var i;
459 461 if (index === undefined || index === null) {
460 462 i = this.get_selected_index();
461 463 if (i === null) {
462 464 i = 0;
463 465 }
464 466 } else {
465 467 i = index;
466 468 }
467 469 return i;
468 470 };
469 471
470 472 /**
471 473 * Get the currently selected cell.
472 474 * @method get_selected_cell
473 475 * @return {Cell} The selected cell
474 476 */
475 477 Notebook.prototype.get_selected_cell = function () {
476 478 var index = this.get_selected_index();
477 479 return this.get_cell(index);
478 480 };
479 481
480 482 /**
481 483 * Check whether a cell index is valid.
482 484 *
483 485 * @method is_valid_cell_index
484 486 * @param {Number} index A cell index
485 487 * @return True if the index is valid, false otherwise
486 488 */
487 489 Notebook.prototype.is_valid_cell_index = function (index) {
488 490 if (index !== null && index >= 0 && index < this.ncells()) {
489 491 return true;
490 492 } else {
491 493 return false;
492 494 }
493 495 };
494 496
495 497 /**
496 498 * Get the index of the currently selected cell.
497 499
498 500 * @method get_selected_index
499 501 * @return {Number} The selected cell's numeric index
500 502 */
501 503 Notebook.prototype.get_selected_index = function () {
502 504 var result = null;
503 505 this.get_cell_elements().filter(function (index) {
504 506 if ($(this).data("cell").selected === true) {
505 507 result = index;
506 508 }
507 509 });
508 510 return result;
509 511 };
510 512
511 513
512 514 // Cell selection.
513 515
514 516 /**
515 517 * Programmatically select a cell.
516 518 *
517 519 * @method select
518 520 * @param {Number} index A cell's index
519 521 * @return {Notebook} This notebook
520 522 */
521 523 Notebook.prototype.select = function (index) {
522 524 if (this.is_valid_cell_index(index)) {
523 525 var sindex = this.get_selected_index();
524 526 if (sindex !== null && index !== sindex) {
525 527 // If we are about to select a different cell, make sure we are
526 528 // first in command mode.
527 529 if (this.mode !== 'command') {
528 530 this.command_mode();
529 531 }
530 532 this.get_cell(sindex).unselect();
531 533 }
532 534 var cell = this.get_cell(index);
533 535 cell.select();
534 536 if (cell.cell_type === 'heading') {
535 537 this.events.trigger('selected_cell_type_changed.Notebook',
536 538 {'cell_type':cell.cell_type,level:cell.level}
537 539 );
538 540 } else {
539 541 this.events.trigger('selected_cell_type_changed.Notebook',
540 542 {'cell_type':cell.cell_type}
541 543 );
542 544 }
543 545 }
544 546 return this;
545 547 };
546 548
547 549 /**
548 550 * Programmatically select the next cell.
549 551 *
550 552 * @method select_next
551 553 * @return {Notebook} This notebook
552 554 */
553 555 Notebook.prototype.select_next = function () {
554 556 var index = this.get_selected_index();
555 557 this.select(index+1);
556 558 return this;
557 559 };
558 560
559 561 /**
560 562 * Programmatically select the previous cell.
561 563 *
562 564 * @method select_prev
563 565 * @return {Notebook} This notebook
564 566 */
565 567 Notebook.prototype.select_prev = function () {
566 568 var index = this.get_selected_index();
567 569 this.select(index-1);
568 570 return this;
569 571 };
570 572
571 573
572 574 // Edit/Command mode
573 575
574 576 /**
575 577 * Gets the index of the cell that is in edit mode.
576 578 *
577 579 * @method get_edit_index
578 580 *
579 581 * @return index {int}
580 582 **/
581 583 Notebook.prototype.get_edit_index = function () {
582 584 var result = null;
583 585 this.get_cell_elements().filter(function (index) {
584 586 if ($(this).data("cell").mode === 'edit') {
585 587 result = index;
586 588 }
587 589 });
588 590 return result;
589 591 };
590 592
591 593 /**
592 594 * Handle when a a cell blurs and the notebook should enter command mode.
593 595 *
594 596 * @method handle_command_mode
595 597 * @param [cell] {Cell} Cell to enter command mode on.
596 598 **/
597 599 Notebook.prototype.handle_command_mode = function (cell) {
598 600 if (this.mode !== 'command') {
599 601 cell.command_mode();
600 602 this.mode = 'command';
601 603 this.events.trigger('command_mode.Notebook');
602 604 this.keyboard_manager.command_mode();
603 605 }
604 606 };
605 607
606 608 /**
607 609 * Make the notebook enter command mode.
608 610 *
609 611 * @method command_mode
610 612 **/
611 613 Notebook.prototype.command_mode = function () {
612 614 var cell = this.get_cell(this.get_edit_index());
613 615 if (cell && this.mode !== 'command') {
614 616 // We don't call cell.command_mode, but rather call cell.focus_cell()
615 617 // which will blur and CM editor and trigger the call to
616 618 // handle_command_mode.
617 619 cell.focus_cell();
618 620 }
619 621 };
620 622
621 623 /**
622 624 * Handle when a cell fires it's edit_mode event.
623 625 *
624 626 * @method handle_edit_mode
625 627 * @param [cell] {Cell} Cell to enter edit mode on.
626 628 **/
627 629 Notebook.prototype.handle_edit_mode = function (cell) {
628 630 if (cell && this.mode !== 'edit') {
629 631 cell.edit_mode();
630 632 this.mode = 'edit';
631 633 this.events.trigger('edit_mode.Notebook');
632 634 this.keyboard_manager.edit_mode();
633 635 }
634 636 };
635 637
636 638 /**
637 639 * Make a cell enter edit mode.
638 640 *
639 641 * @method edit_mode
640 642 **/
641 643 Notebook.prototype.edit_mode = function () {
642 644 var cell = this.get_selected_cell();
643 645 if (cell && this.mode !== 'edit') {
644 646 cell.unrender();
645 647 cell.focus_editor();
646 648 }
647 649 };
648 650
649 651 /**
650 652 * Focus the currently selected cell.
651 653 *
652 654 * @method focus_cell
653 655 **/
654 656 Notebook.prototype.focus_cell = function () {
655 657 var cell = this.get_selected_cell();
656 658 if (cell === null) {return;} // No cell is selected
657 659 cell.focus_cell();
658 660 };
659 661
660 662 // Cell movement
661 663
662 664 /**
663 665 * Move given (or selected) cell up and select it.
664 666 *
665 667 * @method move_cell_up
666 668 * @param [index] {integer} cell index
667 669 * @return {Notebook} This notebook
668 670 **/
669 671 Notebook.prototype.move_cell_up = function (index) {
670 672 var i = this.index_or_selected(index);
671 673 if (this.is_valid_cell_index(i) && i > 0) {
672 674 var pivot = this.get_cell_element(i-1);
673 675 var tomove = this.get_cell_element(i);
674 676 if (pivot !== null && tomove !== null) {
675 677 tomove.detach();
676 678 pivot.before(tomove);
677 679 this.select(i-1);
678 680 var cell = this.get_selected_cell();
679 681 cell.focus_cell();
680 682 }
681 683 this.set_dirty(true);
682 684 }
683 685 return this;
684 686 };
685 687
686 688
687 689 /**
688 690 * Move given (or selected) cell down and select it
689 691 *
690 692 * @method move_cell_down
691 693 * @param [index] {integer} cell index
692 694 * @return {Notebook} This notebook
693 695 **/
694 696 Notebook.prototype.move_cell_down = function (index) {
695 697 var i = this.index_or_selected(index);
696 698 if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
697 699 var pivot = this.get_cell_element(i+1);
698 700 var tomove = this.get_cell_element(i);
699 701 if (pivot !== null && tomove !== null) {
700 702 tomove.detach();
701 703 pivot.after(tomove);
702 704 this.select(i+1);
703 705 var cell = this.get_selected_cell();
704 706 cell.focus_cell();
705 707 }
706 708 }
707 709 this.set_dirty();
708 710 return this;
709 711 };
710 712
711 713
712 714 // Insertion, deletion.
713 715
714 716 /**
715 717 * Delete a cell from the notebook.
716 718 *
717 719 * @method delete_cell
718 720 * @param [index] A cell's numeric index
719 721 * @return {Notebook} This notebook
720 722 */
721 723 Notebook.prototype.delete_cell = function (index) {
722 724 var i = this.index_or_selected(index);
723 725 var cell = this.get_selected_cell();
724 726 this.undelete_backup = cell.toJSON();
725 727 $('#undelete_cell').removeClass('disabled');
726 728 if (this.is_valid_cell_index(i)) {
727 729 var old_ncells = this.ncells();
728 730 var ce = this.get_cell_element(i);
729 731 ce.remove();
730 732 if (i === 0) {
731 733 // Always make sure we have at least one cell.
732 734 if (old_ncells === 1) {
733 735 this.insert_cell_below('code');
734 736 }
735 737 this.select(0);
736 738 this.undelete_index = 0;
737 739 this.undelete_below = false;
738 740 } else if (i === old_ncells-1 && i !== 0) {
739 741 this.select(i-1);
740 742 this.undelete_index = i - 1;
741 743 this.undelete_below = true;
742 744 } else {
743 745 this.select(i);
744 746 this.undelete_index = i;
745 747 this.undelete_below = false;
746 748 }
747 749 this.events.trigger('delete.Cell', {'cell': cell, 'index': i});
748 750 this.set_dirty(true);
749 751 }
750 752 return this;
751 753 };
752 754
753 755 /**
754 756 * Restore the most recently deleted cell.
755 757 *
756 758 * @method undelete
757 759 */
758 760 Notebook.prototype.undelete_cell = function() {
759 761 if (this.undelete_backup !== null && this.undelete_index !== null) {
760 762 var current_index = this.get_selected_index();
761 763 if (this.undelete_index < current_index) {
762 764 current_index = current_index + 1;
763 765 }
764 766 if (this.undelete_index >= this.ncells()) {
765 767 this.select(this.ncells() - 1);
766 768 }
767 769 else {
768 770 this.select(this.undelete_index);
769 771 }
770 772 var cell_data = this.undelete_backup;
771 773 var new_cell = null;
772 774 if (this.undelete_below) {
773 775 new_cell = this.insert_cell_below(cell_data.cell_type);
774 776 } else {
775 777 new_cell = this.insert_cell_above(cell_data.cell_type);
776 778 }
777 779 new_cell.fromJSON(cell_data);
778 780 if (this.undelete_below) {
779 781 this.select(current_index+1);
780 782 } else {
781 783 this.select(current_index);
782 784 }
783 785 this.undelete_backup = null;
784 786 this.undelete_index = null;
785 787 }
786 788 $('#undelete_cell').addClass('disabled');
787 789 };
788 790
789 791 /**
790 792 * Insert a cell so that after insertion the cell is at given index.
791 793 *
792 794 * If cell type is not provided, it will default to the type of the
793 795 * currently active cell.
794 796 *
795 797 * Similar to insert_above, but index parameter is mandatory
796 798 *
797 799 * Index will be brought back into the accessible range [0,n]
798 800 *
799 801 * @method insert_cell_at_index
800 802 * @param [type] {string} in ['code','markdown','heading'], defaults to 'code'
801 803 * @param [index] {int} a valid index where to insert cell
802 804 *
803 805 * @return cell {cell|null} created cell or null
804 806 **/
805 807 Notebook.prototype.insert_cell_at_index = function(type, index){
806 808
807 809 var ncells = this.ncells();
808 810 index = Math.min(index,ncells);
809 811 index = Math.max(index,0);
810 812 var cell = null;
811 813 type = type || this.get_selected_cell().cell_type;
812 814
813 815 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
814 816 var cell_options = {
815 817 events: this.events,
816 818 config: this.config,
817 819 keyboard_manager: this.keyboard_manager,
818 820 notebook: this
819 821 };
820 822 if (type === 'code') {
821 823 cell = new codecell.CodeCell(this.kernel, cell_options);
822 824 cell.set_input_prompt();
823 825 } else if (type === 'markdown') {
824 826 cell = new textcell.MarkdownCell(cell_options);
825 827 } else if (type === 'raw') {
826 828 cell = new textcell.RawCell(cell_options);
827 829 } else if (type === 'heading') {
828 830 cell = new textcell.HeadingCell(cell_options);
829 831 }
830 832
831 833 if(this._insert_element_at_index(cell.element,index)) {
832 834 cell.render();
833 835 this.events.trigger('create.Cell', {'cell': cell, 'index': index});
834 836 cell.refresh();
835 837 // We used to select the cell after we refresh it, but there
836 838 // are now cases were this method is called where select is
837 839 // not appropriate. The selection logic should be handled by the
838 840 // caller of the the top level insert_cell methods.
839 841 this.set_dirty(true);
840 842 }
841 843 }
842 844 return cell;
843 845
844 846 };
845 847
846 848 /**
847 849 * Insert an element at given cell index.
848 850 *
849 851 * @method _insert_element_at_index
850 852 * @param element {dom element} a cell element
851 853 * @param [index] {int} a valid index where to inser cell
852 854 * @private
853 855 *
854 856 * return true if everything whent fine.
855 857 **/
856 858 Notebook.prototype._insert_element_at_index = function(element, index){
857 859 if (element === undefined){
858 860 return false;
859 861 }
860 862
861 863 var ncells = this.ncells();
862 864
863 865 if (ncells === 0) {
864 866 // special case append if empty
865 867 this.element.find('div.end_space').before(element);
866 868 } else if ( ncells === index ) {
867 869 // special case append it the end, but not empty
868 870 this.get_cell_element(index-1).after(element);
869 871 } else if (this.is_valid_cell_index(index)) {
870 872 // otherwise always somewhere to append to
871 873 this.get_cell_element(index).before(element);
872 874 } else {
873 875 return false;
874 876 }
875 877
876 878 if (this.undelete_index !== null && index <= this.undelete_index) {
877 879 this.undelete_index = this.undelete_index + 1;
878 880 this.set_dirty(true);
879 881 }
880 882 return true;
881 883 };
882 884
883 885 /**
884 886 * Insert a cell of given type above given index, or at top
885 887 * of notebook if index smaller than 0.
886 888 *
887 889 * default index value is the one of currently selected cell
888 890 *
889 891 * @method insert_cell_above
890 892 * @param [type] {string} cell type
891 893 * @param [index] {integer}
892 894 *
893 895 * @return handle to created cell or null
894 896 **/
895 897 Notebook.prototype.insert_cell_above = function (type, index) {
896 898 index = this.index_or_selected(index);
897 899 return this.insert_cell_at_index(type, index);
898 900 };
899 901
900 902 /**
901 903 * Insert a cell of given type below given index, or at bottom
902 904 * of notebook if index greater than number of cells
903 905 *
904 906 * default index value is the one of currently selected cell
905 907 *
906 908 * @method insert_cell_below
907 909 * @param [type] {string} cell type
908 910 * @param [index] {integer}
909 911 *
910 912 * @return handle to created cell or null
911 913 *
912 914 **/
913 915 Notebook.prototype.insert_cell_below = function (type, index) {
914 916 index = this.index_or_selected(index);
915 917 return this.insert_cell_at_index(type, index+1);
916 918 };
917 919
918 920
919 921 /**
920 922 * Insert cell at end of notebook
921 923 *
922 924 * @method insert_cell_at_bottom
923 925 * @param {String} type cell type
924 926 *
925 927 * @return the added cell; or null
926 928 **/
927 929 Notebook.prototype.insert_cell_at_bottom = function (type){
928 930 var len = this.ncells();
929 931 return this.insert_cell_below(type,len-1);
930 932 };
931 933
932 934 /**
933 935 * Turn a cell into a code cell.
934 936 *
935 937 * @method to_code
936 938 * @param {Number} [index] A cell's index
937 939 */
938 940 Notebook.prototype.to_code = function (index) {
939 941 var i = this.index_or_selected(index);
940 942 if (this.is_valid_cell_index(i)) {
941 943 var source_element = this.get_cell_element(i);
942 944 var source_cell = source_element.data("cell");
943 945 if (!(source_cell instanceof codecell.CodeCell)) {
944 946 var target_cell = this.insert_cell_below('code',i);
945 947 var text = source_cell.get_text();
946 948 if (text === source_cell.placeholder) {
947 949 text = '';
948 950 }
949 951 target_cell.set_text(text);
950 952 // make this value the starting point, so that we can only undo
951 953 // to this state, instead of a blank cell
952 954 target_cell.code_mirror.clearHistory();
953 955 source_element.remove();
954 956 this.select(i);
955 957 var cursor = source_cell.code_mirror.getCursor();
956 958 target_cell.code_mirror.setCursor(cursor);
957 959 this.set_dirty(true);
958 960 }
959 961 }
960 962 };
961 963
962 964 /**
963 965 * Turn a cell into a Markdown cell.
964 966 *
965 967 * @method to_markdown
966 968 * @param {Number} [index] A cell's index
967 969 */
968 970 Notebook.prototype.to_markdown = function (index) {
969 971 var i = this.index_or_selected(index);
970 972 if (this.is_valid_cell_index(i)) {
971 973 var source_element = this.get_cell_element(i);
972 974 var source_cell = source_element.data("cell");
973 975 if (!(source_cell instanceof textcell.MarkdownCell)) {
974 976 var target_cell = this.insert_cell_below('markdown',i);
975 977 var text = source_cell.get_text();
976 978 if (text === source_cell.placeholder) {
977 979 text = '';
978 980 }
979 981 // We must show the editor before setting its contents
980 982 target_cell.unrender();
981 983 target_cell.set_text(text);
982 984 // make this value the starting point, so that we can only undo
983 985 // to this state, instead of a blank cell
984 986 target_cell.code_mirror.clearHistory();
985 987 source_element.remove();
986 988 this.select(i);
987 989 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
988 990 target_cell.render();
989 991 }
990 992 var cursor = source_cell.code_mirror.getCursor();
991 993 target_cell.code_mirror.setCursor(cursor);
992 994 this.set_dirty(true);
993 995 }
994 996 }
995 997 };
996 998
997 999 /**
998 1000 * Turn a cell into a raw text cell.
999 1001 *
1000 1002 * @method to_raw
1001 1003 * @param {Number} [index] A cell's index
1002 1004 */
1003 1005 Notebook.prototype.to_raw = function (index) {
1004 1006 var i = this.index_or_selected(index);
1005 1007 if (this.is_valid_cell_index(i)) {
1006 1008 var source_element = this.get_cell_element(i);
1007 1009 var source_cell = source_element.data("cell");
1008 1010 var target_cell = null;
1009 1011 if (!(source_cell instanceof textcell.RawCell)) {
1010 1012 target_cell = this.insert_cell_below('raw',i);
1011 1013 var text = source_cell.get_text();
1012 1014 if (text === source_cell.placeholder) {
1013 1015 text = '';
1014 1016 }
1015 1017 // We must show the editor before setting its contents
1016 1018 target_cell.unrender();
1017 1019 target_cell.set_text(text);
1018 1020 // make this value the starting point, so that we can only undo
1019 1021 // to this state, instead of a blank cell
1020 1022 target_cell.code_mirror.clearHistory();
1021 1023 source_element.remove();
1022 1024 this.select(i);
1023 1025 var cursor = source_cell.code_mirror.getCursor();
1024 1026 target_cell.code_mirror.setCursor(cursor);
1025 1027 this.set_dirty(true);
1026 1028 }
1027 1029 }
1028 1030 };
1029 1031
1030 1032 /**
1031 1033 * Turn a cell into a heading cell.
1032 1034 *
1033 1035 * @method to_heading
1034 1036 * @param {Number} [index] A cell's index
1035 1037 * @param {Number} [level] A heading level (e.g., 1 becomes &lt;h1&gt;)
1036 1038 */
1037 1039 Notebook.prototype.to_heading = function (index, level) {
1038 1040 level = level || 1;
1039 1041 var i = this.index_or_selected(index);
1040 1042 if (this.is_valid_cell_index(i)) {
1041 1043 var source_element = this.get_cell_element(i);
1042 1044 var source_cell = source_element.data("cell");
1043 1045 var target_cell = null;
1044 1046 if (source_cell instanceof textcell.HeadingCell) {
1045 1047 source_cell.set_level(level);
1046 1048 } else {
1047 1049 target_cell = this.insert_cell_below('heading',i);
1048 1050 var text = source_cell.get_text();
1049 1051 if (text === source_cell.placeholder) {
1050 1052 text = '';
1051 1053 }
1052 1054 // We must show the editor before setting its contents
1053 1055 target_cell.set_level(level);
1054 1056 target_cell.unrender();
1055 1057 target_cell.set_text(text);
1056 1058 // make this value the starting point, so that we can only undo
1057 1059 // to this state, instead of a blank cell
1058 1060 target_cell.code_mirror.clearHistory();
1059 1061 source_element.remove();
1060 1062 this.select(i);
1061 1063 var cursor = source_cell.code_mirror.getCursor();
1062 1064 target_cell.code_mirror.setCursor(cursor);
1063 1065 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1064 1066 target_cell.render();
1065 1067 }
1066 1068 }
1067 1069 this.set_dirty(true);
1068 1070 this.events.trigger('selected_cell_type_changed.Notebook',
1069 1071 {'cell_type':'heading',level:level}
1070 1072 );
1071 1073 }
1072 1074 };
1073 1075
1074 1076
1075 1077 // Cut/Copy/Paste
1076 1078
1077 1079 /**
1078 1080 * Enable UI elements for pasting cells.
1079 1081 *
1080 1082 * @method enable_paste
1081 1083 */
1082 1084 Notebook.prototype.enable_paste = function () {
1083 1085 var that = this;
1084 1086 if (!this.paste_enabled) {
1085 1087 $('#paste_cell_replace').removeClass('disabled')
1086 1088 .on('click', function () {that.paste_cell_replace();});
1087 1089 $('#paste_cell_above').removeClass('disabled')
1088 1090 .on('click', function () {that.paste_cell_above();});
1089 1091 $('#paste_cell_below').removeClass('disabled')
1090 1092 .on('click', function () {that.paste_cell_below();});
1091 1093 this.paste_enabled = true;
1092 1094 }
1093 1095 };
1094 1096
1095 1097 /**
1096 1098 * Disable UI elements for pasting cells.
1097 1099 *
1098 1100 * @method disable_paste
1099 1101 */
1100 1102 Notebook.prototype.disable_paste = function () {
1101 1103 if (this.paste_enabled) {
1102 1104 $('#paste_cell_replace').addClass('disabled').off('click');
1103 1105 $('#paste_cell_above').addClass('disabled').off('click');
1104 1106 $('#paste_cell_below').addClass('disabled').off('click');
1105 1107 this.paste_enabled = false;
1106 1108 }
1107 1109 };
1108 1110
1109 1111 /**
1110 1112 * Cut a cell.
1111 1113 *
1112 1114 * @method cut_cell
1113 1115 */
1114 1116 Notebook.prototype.cut_cell = function () {
1115 1117 this.copy_cell();
1116 1118 this.delete_cell();
1117 1119 };
1118 1120
1119 1121 /**
1120 1122 * Copy a cell.
1121 1123 *
1122 1124 * @method copy_cell
1123 1125 */
1124 1126 Notebook.prototype.copy_cell = function () {
1125 1127 var cell = this.get_selected_cell();
1126 1128 this.clipboard = cell.toJSON();
1127 1129 this.enable_paste();
1128 1130 };
1129 1131
1130 1132 /**
1131 1133 * Replace the selected cell with a cell in the clipboard.
1132 1134 *
1133 1135 * @method paste_cell_replace
1134 1136 */
1135 1137 Notebook.prototype.paste_cell_replace = function () {
1136 1138 if (this.clipboard !== null && this.paste_enabled) {
1137 1139 var cell_data = this.clipboard;
1138 1140 var new_cell = this.insert_cell_above(cell_data.cell_type);
1139 1141 new_cell.fromJSON(cell_data);
1140 1142 var old_cell = this.get_next_cell(new_cell);
1141 1143 this.delete_cell(this.find_cell_index(old_cell));
1142 1144 this.select(this.find_cell_index(new_cell));
1143 1145 }
1144 1146 };
1145 1147
1146 1148 /**
1147 1149 * Paste a cell from the clipboard above the selected cell.
1148 1150 *
1149 1151 * @method paste_cell_above
1150 1152 */
1151 1153 Notebook.prototype.paste_cell_above = function () {
1152 1154 if (this.clipboard !== null && this.paste_enabled) {
1153 1155 var cell_data = this.clipboard;
1154 1156 var new_cell = this.insert_cell_above(cell_data.cell_type);
1155 1157 new_cell.fromJSON(cell_data);
1156 1158 new_cell.focus_cell();
1157 1159 }
1158 1160 };
1159 1161
1160 1162 /**
1161 1163 * Paste a cell from the clipboard below the selected cell.
1162 1164 *
1163 1165 * @method paste_cell_below
1164 1166 */
1165 1167 Notebook.prototype.paste_cell_below = function () {
1166 1168 if (this.clipboard !== null && this.paste_enabled) {
1167 1169 var cell_data = this.clipboard;
1168 1170 var new_cell = this.insert_cell_below(cell_data.cell_type);
1169 1171 new_cell.fromJSON(cell_data);
1170 1172 new_cell.focus_cell();
1171 1173 }
1172 1174 };
1173 1175
1174 1176 // Split/merge
1175 1177
1176 1178 /**
1177 1179 * Split the selected cell into two, at the cursor.
1178 1180 *
1179 1181 * @method split_cell
1180 1182 */
1181 1183 Notebook.prototype.split_cell = function () {
1182 1184 var mdc = textcell.MarkdownCell;
1183 1185 var rc = textcell.RawCell;
1184 1186 var cell = this.get_selected_cell();
1185 1187 if (cell.is_splittable()) {
1186 1188 var texta = cell.get_pre_cursor();
1187 1189 var textb = cell.get_post_cursor();
1188 1190 if (cell instanceof codecell.CodeCell) {
1189 1191 // In this case the operations keep the notebook in its existing mode
1190 1192 // so we don't need to do any post-op mode changes.
1191 1193 cell.set_text(textb);
1192 1194 var new_cell = this.insert_cell_above('code');
1193 1195 new_cell.set_text(texta);
1194 1196 } else if ((cell instanceof mdc && !cell.rendered) || (cell instanceof rc)) {
1195 1197 // We know cell is !rendered so we can use set_text.
1196 1198 cell.set_text(textb);
1197 1199 var new_cell = this.insert_cell_above(cell.cell_type);
1198 1200 // Unrender the new cell so we can call set_text.
1199 1201 new_cell.unrender();
1200 1202 new_cell.set_text(texta);
1201 1203 }
1202 1204 }
1203 1205 };
1204 1206
1205 1207 /**
1206 1208 * Combine the selected cell into the cell above it.
1207 1209 *
1208 1210 * @method merge_cell_above
1209 1211 */
1210 1212 Notebook.prototype.merge_cell_above = function () {
1211 1213 var mdc = textcell.MarkdownCell;
1212 1214 var rc = textcell.RawCell;
1213 1215 var index = this.get_selected_index();
1214 1216 var cell = this.get_cell(index);
1215 1217 var render = cell.rendered;
1216 1218 if (!cell.is_mergeable()) {
1217 1219 return;
1218 1220 }
1219 1221 if (index > 0) {
1220 1222 var upper_cell = this.get_cell(index-1);
1221 1223 if (!upper_cell.is_mergeable()) {
1222 1224 return;
1223 1225 }
1224 1226 var upper_text = upper_cell.get_text();
1225 1227 var text = cell.get_text();
1226 1228 if (cell instanceof codecell.CodeCell) {
1227 1229 cell.set_text(upper_text+'\n'+text);
1228 1230 } else if ((cell instanceof mdc) || (cell instanceof rc)) {
1229 1231 cell.unrender(); // Must unrender before we set_text.
1230 1232 cell.set_text(upper_text+'\n\n'+text);
1231 1233 if (render) {
1232 1234 // The rendered state of the final cell should match
1233 1235 // that of the original selected cell;
1234 1236 cell.render();
1235 1237 }
1236 1238 }
1237 1239 this.delete_cell(index-1);
1238 1240 this.select(this.find_cell_index(cell));
1239 1241 }
1240 1242 };
1241 1243
1242 1244 /**
1243 1245 * Combine the selected cell into the cell below it.
1244 1246 *
1245 1247 * @method merge_cell_below
1246 1248 */
1247 1249 Notebook.prototype.merge_cell_below = function () {
1248 1250 var mdc = textcell.MarkdownCell;
1249 1251 var rc = textcell.RawCell;
1250 1252 var index = this.get_selected_index();
1251 1253 var cell = this.get_cell(index);
1252 1254 var render = cell.rendered;
1253 1255 if (!cell.is_mergeable()) {
1254 1256 return;
1255 1257 }
1256 1258 if (index < this.ncells()-1) {
1257 1259 var lower_cell = this.get_cell(index+1);
1258 1260 if (!lower_cell.is_mergeable()) {
1259 1261 return;
1260 1262 }
1261 1263 var lower_text = lower_cell.get_text();
1262 1264 var text = cell.get_text();
1263 1265 if (cell instanceof codecell.CodeCell) {
1264 1266 cell.set_text(text+'\n'+lower_text);
1265 1267 } else if ((cell instanceof mdc) || (cell instanceof rc)) {
1266 1268 cell.unrender(); // Must unrender before we set_text.
1267 1269 cell.set_text(text+'\n\n'+lower_text);
1268 1270 if (render) {
1269 1271 // The rendered state of the final cell should match
1270 1272 // that of the original selected cell;
1271 1273 cell.render();
1272 1274 }
1273 1275 }
1274 1276 this.delete_cell(index+1);
1275 1277 this.select(this.find_cell_index(cell));
1276 1278 }
1277 1279 };
1278 1280
1279 1281
1280 1282 // Cell collapsing and output clearing
1281 1283
1282 1284 /**
1283 1285 * Hide a cell's output.
1284 1286 *
1285 1287 * @method collapse_output
1286 1288 * @param {Number} index A cell's numeric index
1287 1289 */
1288 1290 Notebook.prototype.collapse_output = function (index) {
1289 1291 var i = this.index_or_selected(index);
1290 1292 var cell = this.get_cell(i);
1291 1293 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1292 1294 cell.collapse_output();
1293 1295 this.set_dirty(true);
1294 1296 }
1295 1297 };
1296 1298
1297 1299 /**
1298 1300 * Hide each code cell's output area.
1299 1301 *
1300 1302 * @method collapse_all_output
1301 1303 */
1302 1304 Notebook.prototype.collapse_all_output = function () {
1303 1305 $.map(this.get_cells(), function (cell, i) {
1304 1306 if (cell instanceof codecell.CodeCell) {
1305 1307 cell.collapse_output();
1306 1308 }
1307 1309 });
1308 1310 // this should not be set if the `collapse` key is removed from nbformat
1309 1311 this.set_dirty(true);
1310 1312 };
1311 1313
1312 1314 /**
1313 1315 * Show a cell's output.
1314 1316 *
1315 1317 * @method expand_output
1316 1318 * @param {Number} index A cell's numeric index
1317 1319 */
1318 1320 Notebook.prototype.expand_output = function (index) {
1319 1321 var i = this.index_or_selected(index);
1320 1322 var cell = this.get_cell(i);
1321 1323 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1322 1324 cell.expand_output();
1323 1325 this.set_dirty(true);
1324 1326 }
1325 1327 };
1326 1328
1327 1329 /**
1328 1330 * Expand each code cell's output area, and remove scrollbars.
1329 1331 *
1330 1332 * @method expand_all_output
1331 1333 */
1332 1334 Notebook.prototype.expand_all_output = function () {
1333 1335 $.map(this.get_cells(), function (cell, i) {
1334 1336 if (cell instanceof codecell.CodeCell) {
1335 1337 cell.expand_output();
1336 1338 }
1337 1339 });
1338 1340 // this should not be set if the `collapse` key is removed from nbformat
1339 1341 this.set_dirty(true);
1340 1342 };
1341 1343
1342 1344 /**
1343 1345 * Clear the selected CodeCell's output area.
1344 1346 *
1345 1347 * @method clear_output
1346 1348 * @param {Number} index A cell's numeric index
1347 1349 */
1348 1350 Notebook.prototype.clear_output = function (index) {
1349 1351 var i = this.index_or_selected(index);
1350 1352 var cell = this.get_cell(i);
1351 1353 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1352 1354 cell.clear_output();
1353 1355 this.set_dirty(true);
1354 1356 }
1355 1357 };
1356 1358
1357 1359 /**
1358 1360 * Clear each code cell's output area.
1359 1361 *
1360 1362 * @method clear_all_output
1361 1363 */
1362 1364 Notebook.prototype.clear_all_output = function () {
1363 1365 $.map(this.get_cells(), function (cell, i) {
1364 1366 if (cell instanceof codecell.CodeCell) {
1365 1367 cell.clear_output();
1366 1368 }
1367 1369 });
1368 1370 this.set_dirty(true);
1369 1371 };
1370 1372
1371 1373 /**
1372 1374 * Scroll the selected CodeCell's output area.
1373 1375 *
1374 1376 * @method scroll_output
1375 1377 * @param {Number} index A cell's numeric index
1376 1378 */
1377 1379 Notebook.prototype.scroll_output = function (index) {
1378 1380 var i = this.index_or_selected(index);
1379 1381 var cell = this.get_cell(i);
1380 1382 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1381 1383 cell.scroll_output();
1382 1384 this.set_dirty(true);
1383 1385 }
1384 1386 };
1385 1387
1386 1388 /**
1387 1389 * Expand each code cell's output area, and add a scrollbar for long output.
1388 1390 *
1389 1391 * @method scroll_all_output
1390 1392 */
1391 1393 Notebook.prototype.scroll_all_output = function () {
1392 1394 $.map(this.get_cells(), function (cell, i) {
1393 1395 if (cell instanceof codecell.CodeCell) {
1394 1396 cell.scroll_output();
1395 1397 }
1396 1398 });
1397 1399 // this should not be set if the `collapse` key is removed from nbformat
1398 1400 this.set_dirty(true);
1399 1401 };
1400 1402
1401 1403 /** Toggle whether a cell's output is collapsed or expanded.
1402 1404 *
1403 1405 * @method toggle_output
1404 1406 * @param {Number} index A cell's numeric index
1405 1407 */
1406 1408 Notebook.prototype.toggle_output = function (index) {
1407 1409 var i = this.index_or_selected(index);
1408 1410 var cell = this.get_cell(i);
1409 1411 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1410 1412 cell.toggle_output();
1411 1413 this.set_dirty(true);
1412 1414 }
1413 1415 };
1414 1416
1415 1417 /**
1416 1418 * Hide/show the output of all cells.
1417 1419 *
1418 1420 * @method toggle_all_output
1419 1421 */
1420 1422 Notebook.prototype.toggle_all_output = function () {
1421 1423 $.map(this.get_cells(), function (cell, i) {
1422 1424 if (cell instanceof codecell.CodeCell) {
1423 1425 cell.toggle_output();
1424 1426 }
1425 1427 });
1426 1428 // this should not be set if the `collapse` key is removed from nbformat
1427 1429 this.set_dirty(true);
1428 1430 };
1429 1431
1430 1432 /**
1431 1433 * Toggle a scrollbar for long cell outputs.
1432 1434 *
1433 1435 * @method toggle_output_scroll
1434 1436 * @param {Number} index A cell's numeric index
1435 1437 */
1436 1438 Notebook.prototype.toggle_output_scroll = function (index) {
1437 1439 var i = this.index_or_selected(index);
1438 1440 var cell = this.get_cell(i);
1439 1441 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1440 1442 cell.toggle_output_scroll();
1441 1443 this.set_dirty(true);
1442 1444 }
1443 1445 };
1444 1446
1445 1447 /**
1446 1448 * Toggle the scrolling of long output on all cells.
1447 1449 *
1448 1450 * @method toggle_all_output_scrolling
1449 1451 */
1450 1452 Notebook.prototype.toggle_all_output_scroll = function () {
1451 1453 $.map(this.get_cells(), function (cell, i) {
1452 1454 if (cell instanceof codecell.CodeCell) {
1453 1455 cell.toggle_output_scroll();
1454 1456 }
1455 1457 });
1456 1458 // this should not be set if the `collapse` key is removed from nbformat
1457 1459 this.set_dirty(true);
1458 1460 };
1459 1461
1460 1462 // Other cell functions: line numbers, ...
1461 1463
1462 1464 /**
1463 1465 * Toggle line numbers in the selected cell's input area.
1464 1466 *
1465 1467 * @method cell_toggle_line_numbers
1466 1468 */
1467 1469 Notebook.prototype.cell_toggle_line_numbers = function() {
1468 1470 this.get_selected_cell().toggle_line_numbers();
1469 1471 };
1470 1472
1471 1473 // Session related things
1472 1474
1473 1475 /**
1474 1476 * Start a new session and set it on each code cell.
1475 1477 *
1476 1478 * @method start_session
1477 1479 */
1478 1480 Notebook.prototype.start_session = function () {
1479 this.session = new session.Session(this, {
1480 base_url: base_url,
1481 notebook_path: notebook_path,
1482 notebook_name: notebook_name,
1481 this.session = new session.Session({
1482 base_url: this.base_url,
1483 notebook_path: this.notebook_path,
1484 notebook_name: this.notebook_name,
1483 1485 notebook: this});
1484 1486 this.session.start($.proxy(this._session_started, this));
1485 1487 };
1486 1488
1487 1489
1488 1490 /**
1489 1491 * Once a session is started, link the code cells to the kernel and pass the
1490 1492 * comm manager to the widget manager
1491 1493 *
1492 1494 */
1493 1495 Notebook.prototype._session_started = function(){
1494 1496 this.kernel = this.session.kernel;
1495 1497 var ncells = this.ncells();
1496 1498 for (var i=0; i<ncells; i++) {
1497 1499 var cell = this.get_cell(i);
1498 1500 if (cell instanceof codecell.CodeCell) {
1499 1501 cell.set_kernel(this.session.kernel);
1500 1502 }
1501 1503 }
1502 1504 };
1503 1505
1504 1506 /**
1505 1507 * Prompt the user to restart the IPython kernel.
1506 1508 *
1507 1509 * @method restart_kernel
1508 1510 */
1509 1511 Notebook.prototype.restart_kernel = function () {
1510 1512 var that = this;
1511 1513 dialog.modal({
1514 notebook: this,
1515 keyboard_manager: this.keyboard_manager,
1512 1516 title : "Restart kernel or continue running?",
1513 1517 body : $("<p/>").text(
1514 1518 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1515 1519 ),
1516 1520 buttons : {
1517 1521 "Continue running" : {},
1518 1522 "Restart" : {
1519 1523 "class" : "btn-danger",
1520 1524 "click" : function() {
1521 1525 that.session.restart_kernel();
1522 1526 }
1523 1527 }
1524 1528 }
1525 1529 });
1526 1530 };
1527 1531
1528 1532 /**
1529 1533 * Execute or render cell outputs and go into command mode.
1530 1534 *
1531 1535 * @method execute_cell
1532 1536 */
1533 1537 Notebook.prototype.execute_cell = function () {
1534 1538 // mode = shift, ctrl, alt
1535 1539 var cell = this.get_selected_cell();
1536 1540 var cell_index = this.find_cell_index(cell);
1537 1541
1538 1542 cell.execute();
1539 1543 this.command_mode();
1540 1544 this.set_dirty(true);
1541 1545 };
1542 1546
1543 1547 /**
1544 1548 * Execute or render cell outputs and insert a new cell below.
1545 1549 *
1546 1550 * @method execute_cell_and_insert_below
1547 1551 */
1548 1552 Notebook.prototype.execute_cell_and_insert_below = function () {
1549 1553 var cell = this.get_selected_cell();
1550 1554 var cell_index = this.find_cell_index(cell);
1551 1555
1552 1556 cell.execute();
1553 1557
1554 1558 // If we are at the end always insert a new cell and return
1555 1559 if (cell_index === (this.ncells()-1)) {
1556 1560 this.command_mode();
1557 1561 this.insert_cell_below();
1558 1562 this.select(cell_index+1);
1559 1563 this.edit_mode();
1560 1564 this.scroll_to_bottom();
1561 1565 this.set_dirty(true);
1562 1566 return;
1563 1567 }
1564 1568
1565 1569 this.command_mode();
1566 1570 this.insert_cell_below();
1567 1571 this.select(cell_index+1);
1568 1572 this.edit_mode();
1569 1573 this.set_dirty(true);
1570 1574 };
1571 1575
1572 1576 /**
1573 1577 * Execute or render cell outputs and select the next cell.
1574 1578 *
1575 1579 * @method execute_cell_and_select_below
1576 1580 */
1577 1581 Notebook.prototype.execute_cell_and_select_below = function () {
1578 1582
1579 1583 var cell = this.get_selected_cell();
1580 1584 var cell_index = this.find_cell_index(cell);
1581 1585
1582 1586 cell.execute();
1583 1587
1584 1588 // If we are at the end always insert a new cell and return
1585 1589 if (cell_index === (this.ncells()-1)) {
1586 1590 this.command_mode();
1587 1591 this.insert_cell_below();
1588 1592 this.select(cell_index+1);
1589 1593 this.edit_mode();
1590 1594 this.scroll_to_bottom();
1591 1595 this.set_dirty(true);
1592 1596 return;
1593 1597 }
1594 1598
1595 1599 this.command_mode();
1596 1600 this.select(cell_index+1);
1597 1601 this.focus_cell();
1598 1602 this.set_dirty(true);
1599 1603 };
1600 1604
1601 1605 /**
1602 1606 * Execute all cells below the selected cell.
1603 1607 *
1604 1608 * @method execute_cells_below
1605 1609 */
1606 1610 Notebook.prototype.execute_cells_below = function () {
1607 1611 this.execute_cell_range(this.get_selected_index(), this.ncells());
1608 1612 this.scroll_to_bottom();
1609 1613 };
1610 1614
1611 1615 /**
1612 1616 * Execute all cells above the selected cell.
1613 1617 *
1614 1618 * @method execute_cells_above
1615 1619 */
1616 1620 Notebook.prototype.execute_cells_above = function () {
1617 1621 this.execute_cell_range(0, this.get_selected_index());
1618 1622 };
1619 1623
1620 1624 /**
1621 1625 * Execute all cells.
1622 1626 *
1623 1627 * @method execute_all_cells
1624 1628 */
1625 1629 Notebook.prototype.execute_all_cells = function () {
1626 1630 this.execute_cell_range(0, this.ncells());
1627 1631 this.scroll_to_bottom();
1628 1632 };
1629 1633
1630 1634 /**
1631 1635 * Execute a contiguous range of cells.
1632 1636 *
1633 1637 * @method execute_cell_range
1634 1638 * @param {Number} start Index of the first cell to execute (inclusive)
1635 1639 * @param {Number} end Index of the last cell to execute (exclusive)
1636 1640 */
1637 1641 Notebook.prototype.execute_cell_range = function (start, end) {
1638 1642 this.command_mode();
1639 1643 for (var i=start; i<end; i++) {
1640 1644 this.select(i);
1641 1645 this.execute_cell();
1642 1646 }
1643 1647 };
1644 1648
1645 1649 // Persistance and loading
1646 1650
1647 1651 /**
1648 1652 * Getter method for this notebook's name.
1649 1653 *
1650 1654 * @method get_notebook_name
1651 1655 * @return {String} This notebook's name (excluding file extension)
1652 1656 */
1653 1657 Notebook.prototype.get_notebook_name = function () {
1654 1658 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1655 1659 return nbname;
1656 1660 };
1657 1661
1658 1662 /**
1659 1663 * Setter method for this notebook's name.
1660 1664 *
1661 1665 * @method set_notebook_name
1662 1666 * @param {String} name A new name for this notebook
1663 1667 */
1664 1668 Notebook.prototype.set_notebook_name = function (name) {
1665 1669 this.notebook_name = name;
1666 1670 };
1667 1671
1668 1672 /**
1669 1673 * Check that a notebook's name is valid.
1670 1674 *
1671 1675 * @method test_notebook_name
1672 1676 * @param {String} nbname A name for this notebook
1673 1677 * @return {Boolean} True if the name is valid, false if invalid
1674 1678 */
1675 1679 Notebook.prototype.test_notebook_name = function (nbname) {
1676 1680 nbname = nbname || '';
1677 1681 if (nbname.length>0 && !this.notebook_name_blacklist_re.test(nbname)) {
1678 1682 return true;
1679 1683 } else {
1680 1684 return false;
1681 1685 }
1682 1686 };
1683 1687
1684 1688 /**
1685 1689 * Load a notebook from JSON (.ipynb).
1686 1690 *
1687 1691 * This currently handles one worksheet: others are deleted.
1688 1692 *
1689 1693 * @method fromJSON
1690 1694 * @param {Object} data JSON representation of a notebook
1691 1695 */
1692 1696 Notebook.prototype.fromJSON = function (data) {
1693 1697 var content = data.content;
1694 1698 var ncells = this.ncells();
1695 1699 var i;
1696 1700 for (i=0; i<ncells; i++) {
1697 1701 // Always delete cell 0 as they get renumbered as they are deleted.
1698 1702 this.delete_cell(0);
1699 1703 }
1700 1704 // Save the metadata and name.
1701 1705 this.metadata = content.metadata;
1702 1706 this.notebook_name = data.name;
1703 1707 var trusted = true;
1704 1708 // Only handle 1 worksheet for now.
1705 1709 var worksheet = content.worksheets[0];
1706 1710 if (worksheet !== undefined) {
1707 1711 if (worksheet.metadata) {
1708 1712 this.worksheet_metadata = worksheet.metadata;
1709 1713 }
1710 1714 var new_cells = worksheet.cells;
1711 1715 ncells = new_cells.length;
1712 1716 var cell_data = null;
1713 1717 var new_cell = null;
1714 1718 for (i=0; i<ncells; i++) {
1715 1719 cell_data = new_cells[i];
1716 1720 // VERSIONHACK: plaintext -> raw
1717 1721 // handle never-released plaintext name for raw cells
1718 1722 if (cell_data.cell_type === 'plaintext'){
1719 1723 cell_data.cell_type = 'raw';
1720 1724 }
1721 1725
1722 1726 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1723 1727 new_cell.fromJSON(cell_data);
1724 1728 if (new_cell.cell_type == 'code' && !new_cell.output_area.trusted) {
1725 1729 trusted = false;
1726 1730 }
1727 1731 }
1728 1732 }
1729 1733 if (trusted != this.trusted) {
1730 1734 this.trusted = trusted;
1731 1735 this.events.trigger("trust_changed.Notebook", trusted);
1732 1736 }
1733 1737 if (content.worksheets.length > 1) {
1734 1738 dialog.modal({
1739 notebook: this,
1740 keyboard_manager: this.keyboard_manager,
1735 1741 title : "Multiple worksheets",
1736 1742 body : "This notebook has " + data.worksheets.length + " worksheets, " +
1737 1743 "but this version of IPython can only handle the first. " +
1738 1744 "If you save this notebook, worksheets after the first will be lost.",
1739 1745 buttons : {
1740 1746 OK : {
1741 1747 class : "btn-danger"
1742 1748 }
1743 1749 }
1744 1750 });
1745 1751 }
1746 1752 };
1747 1753
1748 1754 /**
1749 1755 * Dump this notebook into a JSON-friendly object.
1750 1756 *
1751 1757 * @method toJSON
1752 1758 * @return {Object} A JSON-friendly representation of this notebook.
1753 1759 */
1754 1760 Notebook.prototype.toJSON = function () {
1755 1761 var cells = this.get_cells();
1756 1762 var ncells = cells.length;
1757 1763 var cell_array = new Array(ncells);
1758 1764 var trusted = true;
1759 1765 for (var i=0; i<ncells; i++) {
1760 1766 var cell = cells[i];
1761 1767 if (cell.cell_type == 'code' && !cell.output_area.trusted) {
1762 1768 trusted = false;
1763 1769 }
1764 1770 cell_array[i] = cell.toJSON();
1765 1771 }
1766 1772 var data = {
1767 1773 // Only handle 1 worksheet for now.
1768 1774 worksheets : [{
1769 1775 cells: cell_array,
1770 1776 metadata: this.worksheet_metadata
1771 1777 }],
1772 1778 metadata : this.metadata
1773 1779 };
1774 1780 if (trusted != this.trusted) {
1775 1781 this.trusted = trusted;
1776 1782 this.events.trigger("trust_changed.Notebook", trusted);
1777 1783 }
1778 1784 return data;
1779 1785 };
1780 1786
1781 1787 /**
1782 1788 * Start an autosave timer, for periodically saving the notebook.
1783 1789 *
1784 1790 * @method set_autosave_interval
1785 1791 * @param {Integer} interval the autosave interval in milliseconds
1786 1792 */
1787 1793 Notebook.prototype.set_autosave_interval = function (interval) {
1788 1794 var that = this;
1789 1795 // clear previous interval, so we don't get simultaneous timers
1790 1796 if (this.autosave_timer) {
1791 1797 clearInterval(this.autosave_timer);
1792 1798 }
1793 1799
1794 1800 this.autosave_interval = this.minimum_autosave_interval = interval;
1795 1801 if (interval) {
1796 1802 this.autosave_timer = setInterval(function() {
1797 1803 if (that.dirty) {
1798 1804 that.save_notebook();
1799 1805 }
1800 1806 }, interval);
1801 1807 this.events.trigger("autosave_enabled.Notebook", interval);
1802 1808 } else {
1803 1809 this.autosave_timer = null;
1804 1810 this.events.trigger("autosave_disabled.Notebook");
1805 1811 }
1806 1812 };
1807 1813
1808 1814 /**
1809 1815 * Save this notebook on the server. This becomes a notebook instance's
1810 1816 * .save_notebook method *after* the entire notebook has been loaded.
1811 1817 *
1812 1818 * @method save_notebook
1813 1819 */
1814 1820 Notebook.prototype.save_notebook = function (extra_settings) {
1815 1821 // Create a JSON model to be sent to the server.
1816 1822 var model = {};
1817 1823 model.name = this.notebook_name;
1818 1824 model.path = this.notebook_path;
1819 1825 model.content = this.toJSON();
1820 1826 model.content.nbformat = this.nbformat;
1821 1827 model.content.nbformat_minor = this.nbformat_minor;
1822 1828 // time the ajax call for autosave tuning purposes.
1823 1829 var start = new Date().getTime();
1824 1830 // We do the call with settings so we can set cache to false.
1825 1831 var settings = {
1826 1832 processData : false,
1827 1833 cache : false,
1828 1834 type : "PUT",
1829 1835 data : JSON.stringify(model),
1830 1836 headers : {'Content-Type': 'application/json'},
1831 1837 success : $.proxy(this.save_notebook_success, this, start),
1832 1838 error : $.proxy(this.save_notebook_error, this)
1833 1839 };
1834 1840 if (extra_settings) {
1835 1841 for (var key in extra_settings) {
1836 1842 settings[key] = extra_settings[key];
1837 1843 }
1838 1844 }
1839 1845 this.events.trigger('notebook_saving.Notebook');
1840 1846 var url = utils.url_join_encode(
1841 1847 this.base_url,
1842 1848 'api/notebooks',
1843 1849 this.notebook_path,
1844 1850 this.notebook_name
1845 1851 );
1846 1852 $.ajax(url, settings);
1847 1853 };
1848 1854
1849 1855 /**
1850 1856 * Success callback for saving a notebook.
1851 1857 *
1852 1858 * @method save_notebook_success
1853 1859 * @param {Integer} start the time when the save request started
1854 1860 * @param {Object} data JSON representation of a notebook
1855 1861 * @param {String} status Description of response status
1856 1862 * @param {jqXHR} xhr jQuery Ajax object
1857 1863 */
1858 1864 Notebook.prototype.save_notebook_success = function (start, data, status, xhr) {
1859 1865 this.set_dirty(false);
1860 1866 this.events.trigger('notebook_saved.Notebook');
1861 1867 this._update_autosave_interval(start);
1862 1868 if (this._checkpoint_after_save) {
1863 1869 this.create_checkpoint();
1864 1870 this._checkpoint_after_save = false;
1865 1871 }
1866 1872 };
1867 1873
1868 1874 /**
1869 1875 * update the autosave interval based on how long the last save took
1870 1876 *
1871 1877 * @method _update_autosave_interval
1872 1878 * @param {Integer} timestamp when the save request started
1873 1879 */
1874 1880 Notebook.prototype._update_autosave_interval = function (start) {
1875 1881 var duration = (new Date().getTime() - start);
1876 1882 if (this.autosave_interval) {
1877 1883 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
1878 1884 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
1879 1885 // round to 10 seconds, otherwise we will be setting a new interval too often
1880 1886 interval = 10000 * Math.round(interval / 10000);
1881 1887 // set new interval, if it's changed
1882 1888 if (interval != this.autosave_interval) {
1883 1889 this.set_autosave_interval(interval);
1884 1890 }
1885 1891 }
1886 1892 };
1887 1893
1888 1894 /**
1889 1895 * Failure callback for saving a notebook.
1890 1896 *
1891 1897 * @method save_notebook_error
1892 1898 * @param {jqXHR} xhr jQuery Ajax object
1893 1899 * @param {String} status Description of response status
1894 1900 * @param {String} error HTTP error message
1895 1901 */
1896 1902 Notebook.prototype.save_notebook_error = function (xhr, status, error) {
1897 1903 this.events.trigger('notebook_save_failed.Notebook', [xhr, status, error]);
1898 1904 };
1899 1905
1900 1906 /**
1901 1907 * Explicitly trust the output of this notebook.
1902 1908 *
1903 1909 * @method trust_notebook
1904 1910 */
1905 1911 Notebook.prototype.trust_notebook = function (extra_settings) {
1906 1912 var body = $("<div>").append($("<p>")
1907 1913 .text("A trusted IPython notebook may execute hidden malicious code ")
1908 1914 .append($("<strong>")
1909 1915 .append(
1910 1916 $("<em>").text("when you open it")
1911 1917 )
1912 1918 ).append(".").append(
1913 1919 " Selecting trust will immediately reload this notebook in a trusted state."
1914 1920 ).append(
1915 1921 " For more information, see the "
1916 1922 ).append($("<a>").attr("href", "http://ipython.org/ipython-doc/2/notebook/security.html")
1917 1923 .text("IPython security documentation")
1918 1924 ).append(".")
1919 1925 );
1920 1926
1921 1927 var nb = this;
1922 1928 dialog.modal({
1929 notebook: this,
1930 keyboard_manager: this.keyboard_manager,
1923 1931 title: "Trust this notebook?",
1924 1932 body: body,
1925 1933
1926 1934 buttons: {
1927 1935 Cancel : {},
1928 1936 Trust : {
1929 1937 class : "btn-danger",
1930 1938 click : function () {
1931 1939 var cells = nb.get_cells();
1932 1940 for (var i = 0; i < cells.length; i++) {
1933 1941 var cell = cells[i];
1934 1942 if (cell.cell_type == 'code') {
1935 1943 cell.output_area.trusted = true;
1936 1944 }
1937 1945 }
1938 1946 this.events.on('notebook_saved.Notebook', function () {
1939 1947 window.location.reload();
1940 1948 });
1941 1949 nb.save_notebook();
1942 1950 }
1943 1951 }
1944 1952 }
1945 1953 });
1946 1954 };
1947 1955
1948 1956 Notebook.prototype.new_notebook = function(){
1949 1957 var path = this.notebook_path;
1950 1958 var base_url = this.base_url;
1951 1959 var settings = {
1952 1960 processData : false,
1953 1961 cache : false,
1954 1962 type : "POST",
1955 1963 dataType : "json",
1956 1964 async : false,
1957 1965 success : function (data, status, xhr){
1958 1966 var notebook_name = data.name;
1959 1967 window.open(
1960 1968 utils.url_join_encode(
1961 1969 base_url,
1962 1970 'notebooks',
1963 1971 path,
1964 1972 notebook_name
1965 1973 ),
1966 1974 '_blank'
1967 1975 );
1968 1976 },
1969 1977 error : utils.log_ajax_error,
1970 1978 };
1971 1979 var url = utils.url_join_encode(
1972 1980 base_url,
1973 1981 'api/notebooks',
1974 1982 path
1975 1983 );
1976 1984 $.ajax(url,settings);
1977 1985 };
1978 1986
1979 1987
1980 1988 Notebook.prototype.copy_notebook = function(){
1981 1989 var path = this.notebook_path;
1982 1990 var base_url = this.base_url;
1983 1991 var settings = {
1984 1992 processData : false,
1985 1993 cache : false,
1986 1994 type : "POST",
1987 1995 dataType : "json",
1988 1996 data : JSON.stringify({copy_from : this.notebook_name}),
1989 1997 async : false,
1990 1998 success : function (data, status, xhr) {
1991 1999 window.open(utils.url_join_encode(
1992 2000 base_url,
1993 2001 'notebooks',
1994 2002 data.path,
1995 2003 data.name
1996 2004 ), '_blank');
1997 2005 },
1998 2006 error : utils.log_ajax_error,
1999 2007 };
2000 2008 var url = utils.url_join_encode(
2001 2009 base_url,
2002 2010 'api/notebooks',
2003 2011 path
2004 2012 );
2005 2013 $.ajax(url,settings);
2006 2014 };
2007 2015
2008 2016 Notebook.prototype.rename = function (nbname) {
2009 2017 var that = this;
2010 2018 if (!nbname.match(/\.ipynb$/)) {
2011 2019 nbname = nbname + ".ipynb";
2012 2020 }
2013 2021 var data = {name: nbname};
2014 2022 var settings = {
2015 2023 processData : false,
2016 2024 cache : false,
2017 2025 type : "PATCH",
2018 2026 data : JSON.stringify(data),
2019 2027 dataType: "json",
2020 2028 headers : {'Content-Type': 'application/json'},
2021 2029 success : $.proxy(that.rename_success, this),
2022 2030 error : $.proxy(that.rename_error, this)
2023 2031 };
2024 2032 this.events.trigger('rename_notebook.Notebook', data);
2025 2033 var url = utils.url_join_encode(
2026 2034 this.base_url,
2027 2035 'api/notebooks',
2028 2036 this.notebook_path,
2029 2037 this.notebook_name
2030 2038 );
2031 2039 $.ajax(url, settings);
2032 2040 };
2033 2041
2034 2042 Notebook.prototype.delete = function () {
2035 2043 var that = this;
2036 2044 var settings = {
2037 2045 processData : false,
2038 2046 cache : false,
2039 2047 type : "DELETE",
2040 2048 dataType: "json",
2041 2049 error : utils.log_ajax_error,
2042 2050 };
2043 2051 var url = utils.url_join_encode(
2044 2052 this.base_url,
2045 2053 'api/notebooks',
2046 2054 this.notebook_path,
2047 2055 this.notebook_name
2048 2056 );
2049 2057 $.ajax(url, settings);
2050 2058 };
2051 2059
2052 2060
2053 2061 Notebook.prototype.rename_success = function (json, status, xhr) {
2054 2062 var name = this.notebook_name = json.name;
2055 2063 var path = json.path;
2056 2064 this.session.rename_notebook(name, path);
2057 2065 this.events.trigger('notebook_renamed.Notebook', json);
2058 2066 };
2059 2067
2060 2068 Notebook.prototype.rename_error = function (xhr, status, error) {
2061 2069 var that = this;
2062 2070 var dialog_body = $('<div/>').append(
2063 2071 $("<p/>").addClass("rename-message")
2064 2072 .text('This notebook name already exists.')
2065 2073 );
2066 2074 this.events.trigger('notebook_rename_failed.Notebook', [xhr, status, error]);
2067 2075 dialog.modal({
2076 notebook: this,
2077 keyboard_manager: this.keyboard_manager,
2068 2078 title: "Notebook Rename Error!",
2069 2079 body: dialog_body,
2070 2080 buttons : {
2071 2081 "Cancel": {},
2072 2082 "OK": {
2073 2083 class: "btn-primary",
2074 2084 click: function () {
2075 2085 this.save_widget.rename_notebook();
2076 2086 }}
2077 2087 },
2078 2088 open : function (event, ui) {
2079 2089 var that = $(this);
2080 2090 // Upon ENTER, click the OK button.
2081 2091 that.find('input[type="text"]').keydown(function (event, ui) {
2082 2092 if (event.which === this.keyboard.keycodes.enter) {
2083 2093 that.find('.btn-primary').first().click();
2084 2094 }
2085 2095 });
2086 2096 that.find('input[type="text"]').focus();
2087 2097 }
2088 2098 });
2089 2099 };
2090 2100
2091 2101 /**
2092 2102 * Request a notebook's data from the server.
2093 2103 *
2094 2104 * @method load_notebook
2095 2105 * @param {String} notebook_name and path A notebook to load
2096 2106 */
2097 2107 Notebook.prototype.load_notebook = function (notebook_name, notebook_path) {
2098 2108 var that = this;
2099 2109 this.notebook_name = notebook_name;
2100 2110 this.notebook_path = notebook_path;
2101 2111 // We do the call with settings so we can set cache to false.
2102 2112 var settings = {
2103 2113 processData : false,
2104 2114 cache : false,
2105 2115 type : "GET",
2106 2116 dataType : "json",
2107 2117 success : $.proxy(this.load_notebook_success,this),
2108 2118 error : $.proxy(this.load_notebook_error,this),
2109 2119 };
2110 2120 this.events.trigger('notebook_loading.Notebook');
2111 2121 var url = utils.url_join_encode(
2112 2122 this.base_url,
2113 2123 'api/notebooks',
2114 2124 this.notebook_path,
2115 2125 this.notebook_name
2116 2126 );
2117 2127 $.ajax(url, settings);
2118 2128 };
2119 2129
2120 2130 /**
2121 2131 * Success callback for loading a notebook from the server.
2122 2132 *
2123 2133 * Load notebook data from the JSON response.
2124 2134 *
2125 2135 * @method load_notebook_success
2126 2136 * @param {Object} data JSON representation of a notebook
2127 2137 * @param {String} status Description of response status
2128 2138 * @param {jqXHR} xhr jQuery Ajax object
2129 2139 */
2130 2140 Notebook.prototype.load_notebook_success = function (data, status, xhr) {
2131 2141 this.fromJSON(data);
2132 2142 if (this.ncells() === 0) {
2133 2143 this.insert_cell_below('code');
2134 2144 this.edit_mode(0);
2135 2145 } else {
2136 2146 this.select(0);
2137 2147 this.handle_command_mode(this.get_cell(0));
2138 2148 }
2139 2149 this.set_dirty(false);
2140 2150 this.scroll_to_top();
2141 2151 if (data.orig_nbformat !== undefined && data.nbformat !== data.orig_nbformat) {
2142 2152 var msg = "This notebook has been converted from an older " +
2143 2153 "notebook format (v"+data.orig_nbformat+") to the current notebook " +
2144 2154 "format (v"+data.nbformat+"). The next time you save this notebook, the " +
2145 2155 "newer notebook format will be used and older versions of IPython " +
2146 2156 "may not be able to read it. To keep the older version, close the " +
2147 2157 "notebook without saving it.";
2148 2158 dialog.modal({
2159 notebook: this,
2160 keyboard_manager: this.keyboard_manager,
2149 2161 title : "Notebook converted",
2150 2162 body : msg,
2151 2163 buttons : {
2152 2164 OK : {
2153 2165 class : "btn-primary"
2154 2166 }
2155 2167 }
2156 2168 });
2157 2169 } else if (data.orig_nbformat_minor !== undefined && data.nbformat_minor !== data.orig_nbformat_minor) {
2158 2170 var that = this;
2159 2171 var orig_vs = 'v' + data.nbformat + '.' + data.orig_nbformat_minor;
2160 2172 var this_vs = 'v' + data.nbformat + '.' + this.nbformat_minor;
2161 2173 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
2162 2174 this_vs + ". You can still work with this notebook, but some features " +
2163 2175 "introduced in later notebook versions may not be available.";
2164 2176
2165 2177 dialog.modal({
2178 notebook: this,
2179 keyboard_manager: this.keyboard_manager,
2166 2180 title : "Newer Notebook",
2167 2181 body : msg,
2168 2182 buttons : {
2169 2183 OK : {
2170 2184 class : "btn-danger"
2171 2185 }
2172 2186 }
2173 2187 });
2174 2188
2175 2189 }
2176 2190
2177 2191 // Create the session after the notebook is completely loaded to prevent
2178 2192 // code execution upon loading, which is a security risk.
2179 2193 if (this.session === null) {
2180 2194 this.start_session();
2181 2195 }
2182 2196 // load our checkpoint list
2183 2197 this.list_checkpoints();
2184 2198
2185 2199 // load toolbar state
2186 2200 if (this.metadata.celltoolbar) {
2187 2201 celltoolbar.CellToolbar.global_show();
2188 2202 celltoolbar.CellToolbar.activate_preset(this.metadata.celltoolbar);
2189 2203 } else {
2190 2204 celltoolbar.CellToolbar.global_hide();
2191 2205 }
2192 2206
2193 2207 // now that we're fully loaded, it is safe to restore save functionality
2194 2208 delete(this.save_notebook);
2195 2209 this.events.trigger('notebook_loaded.Notebook');
2196 2210 };
2197 2211
2198 2212 /**
2199 2213 * Failure callback for loading a notebook from the server.
2200 2214 *
2201 2215 * @method load_notebook_error
2202 2216 * @param {jqXHR} xhr jQuery Ajax object
2203 2217 * @param {String} status Description of response status
2204 2218 * @param {String} error HTTP error message
2205 2219 */
2206 2220 Notebook.prototype.load_notebook_error = function (xhr, status, error) {
2207 2221 this.events.trigger('notebook_load_failed.Notebook', [xhr, status, error]);
2208 2222 var msg;
2209 2223 if (xhr.status === 400) {
2210 2224 msg = error;
2211 2225 } else if (xhr.status === 500) {
2212 2226 msg = "An unknown error occurred while loading this notebook. " +
2213 2227 "This version can load notebook formats " +
2214 2228 "v" + this.nbformat + " or earlier.";
2215 2229 }
2216 2230 dialog.modal({
2231 notebook: this,
2232 keyboard_manager: this.keyboard_manager,
2217 2233 title: "Error loading notebook",
2218 2234 body : msg,
2219 2235 buttons : {
2220 2236 "OK": {}
2221 2237 }
2222 2238 });
2223 2239 };
2224 2240
2225 2241 /********************* checkpoint-related *********************/
2226 2242
2227 2243 /**
2228 2244 * Save the notebook then immediately create a checkpoint.
2229 2245 *
2230 2246 * @method save_checkpoint
2231 2247 */
2232 2248 Notebook.prototype.save_checkpoint = function () {
2233 2249 this._checkpoint_after_save = true;
2234 2250 this.save_notebook();
2235 2251 };
2236 2252
2237 2253 /**
2238 2254 * Add a checkpoint for this notebook.
2239 2255 * for use as a callback from checkpoint creation.
2240 2256 *
2241 2257 * @method add_checkpoint
2242 2258 */
2243 2259 Notebook.prototype.add_checkpoint = function (checkpoint) {
2244 2260 var found = false;
2245 2261 for (var i = 0; i < this.checkpoints.length; i++) {
2246 2262 var existing = this.checkpoints[i];
2247 2263 if (existing.id == checkpoint.id) {
2248 2264 found = true;
2249 2265 this.checkpoints[i] = checkpoint;
2250 2266 break;
2251 2267 }
2252 2268 }
2253 2269 if (!found) {
2254 2270 this.checkpoints.push(checkpoint);
2255 2271 }
2256 2272 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
2257 2273 };
2258 2274
2259 2275 /**
2260 2276 * List checkpoints for this notebook.
2261 2277 *
2262 2278 * @method list_checkpoints
2263 2279 */
2264 2280 Notebook.prototype.list_checkpoints = function () {
2265 2281 var url = utils.url_join_encode(
2266 2282 this.base_url,
2267 2283 'api/notebooks',
2268 2284 this.notebook_path,
2269 2285 this.notebook_name,
2270 2286 'checkpoints'
2271 2287 );
2272 2288 $.get(url).done(
2273 2289 $.proxy(this.list_checkpoints_success, this)
2274 2290 ).fail(
2275 2291 $.proxy(this.list_checkpoints_error, this)
2276 2292 );
2277 2293 };
2278 2294
2279 2295 /**
2280 2296 * Success callback for listing checkpoints.
2281 2297 *
2282 2298 * @method list_checkpoint_success
2283 2299 * @param {Object} data JSON representation of a checkpoint
2284 2300 * @param {String} status Description of response status
2285 2301 * @param {jqXHR} xhr jQuery Ajax object
2286 2302 */
2287 2303 Notebook.prototype.list_checkpoints_success = function (data, status, xhr) {
2288 2304 data = $.parseJSON(data);
2289 2305 this.checkpoints = data;
2290 2306 if (data.length) {
2291 2307 this.last_checkpoint = data[data.length - 1];
2292 2308 } else {
2293 2309 this.last_checkpoint = null;
2294 2310 }
2295 2311 this.events.trigger('checkpoints_listed.Notebook', [data]);
2296 2312 };
2297 2313
2298 2314 /**
2299 2315 * Failure callback for listing a checkpoint.
2300 2316 *
2301 2317 * @method list_checkpoint_error
2302 2318 * @param {jqXHR} xhr jQuery Ajax object
2303 2319 * @param {String} status Description of response status
2304 2320 * @param {String} error_msg HTTP error message
2305 2321 */
2306 2322 Notebook.prototype.list_checkpoints_error = function (xhr, status, error_msg) {
2307 2323 this.events.trigger('list_checkpoints_failed.Notebook');
2308 2324 };
2309 2325
2310 2326 /**
2311 2327 * Create a checkpoint of this notebook on the server from the most recent save.
2312 2328 *
2313 2329 * @method create_checkpoint
2314 2330 */
2315 2331 Notebook.prototype.create_checkpoint = function () {
2316 2332 var url = utils.url_join_encode(
2317 2333 this.base_url,
2318 2334 'api/notebooks',
2319 2335 this.notebook_path,
2320 2336 this.notebook_name,
2321 2337 'checkpoints'
2322 2338 );
2323 2339 $.post(url).done(
2324 2340 $.proxy(this.create_checkpoint_success, this)
2325 2341 ).fail(
2326 2342 $.proxy(this.create_checkpoint_error, this)
2327 2343 );
2328 2344 };
2329 2345
2330 2346 /**
2331 2347 * Success callback for creating a checkpoint.
2332 2348 *
2333 2349 * @method create_checkpoint_success
2334 2350 * @param {Object} data JSON representation of a checkpoint
2335 2351 * @param {String} status Description of response status
2336 2352 * @param {jqXHR} xhr jQuery Ajax object
2337 2353 */
2338 2354 Notebook.prototype.create_checkpoint_success = function (data, status, xhr) {
2339 2355 data = $.parseJSON(data);
2340 2356 this.add_checkpoint(data);
2341 2357 this.events.trigger('checkpoint_created.Notebook', data);
2342 2358 };
2343 2359
2344 2360 /**
2345 2361 * Failure callback for creating a checkpoint.
2346 2362 *
2347 2363 * @method create_checkpoint_error
2348 2364 * @param {jqXHR} xhr jQuery Ajax object
2349 2365 * @param {String} status Description of response status
2350 2366 * @param {String} error_msg HTTP error message
2351 2367 */
2352 2368 Notebook.prototype.create_checkpoint_error = function (xhr, status, error_msg) {
2353 2369 this.events.trigger('checkpoint_failed.Notebook');
2354 2370 };
2355 2371
2356 2372 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2357 2373 var that = this;
2358 2374 checkpoint = checkpoint || this.last_checkpoint;
2359 2375 if ( ! checkpoint ) {
2360 2376 console.log("restore dialog, but no checkpoint to restore to!");
2361 2377 return;
2362 2378 }
2363 2379 var body = $('<div/>').append(
2364 2380 $('<p/>').addClass("p-space").text(
2365 2381 "Are you sure you want to revert the notebook to " +
2366 2382 "the latest checkpoint?"
2367 2383 ).append(
2368 2384 $("<strong/>").text(
2369 2385 " This cannot be undone."
2370 2386 )
2371 2387 )
2372 2388 ).append(
2373 2389 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2374 2390 ).append(
2375 2391 $('<p/>').addClass("p-space").text(
2376 2392 Date(checkpoint.last_modified)
2377 2393 ).css("text-align", "center")
2378 2394 );
2379 2395
2380 2396 dialog.modal({
2397 notebook: this,
2398 keyboard_manager: this.keyboard_manager,
2381 2399 title : "Revert notebook to checkpoint",
2382 2400 body : body,
2383 2401 buttons : {
2384 2402 Revert : {
2385 2403 class : "btn-danger",
2386 2404 click : function () {
2387 2405 that.restore_checkpoint(checkpoint.id);
2388 2406 }
2389 2407 },
2390 2408 Cancel : {}
2391 2409 }
2392 2410 });
2393 2411 };
2394 2412
2395 2413 /**
2396 2414 * Restore the notebook to a checkpoint state.
2397 2415 *
2398 2416 * @method restore_checkpoint
2399 2417 * @param {String} checkpoint ID
2400 2418 */
2401 2419 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2402 2420 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2403 2421 var url = utils.url_join_encode(
2404 2422 this.base_url,
2405 2423 'api/notebooks',
2406 2424 this.notebook_path,
2407 2425 this.notebook_name,
2408 2426 'checkpoints',
2409 2427 checkpoint
2410 2428 );
2411 2429 $.post(url).done(
2412 2430 $.proxy(this.restore_checkpoint_success, this)
2413 2431 ).fail(
2414 2432 $.proxy(this.restore_checkpoint_error, this)
2415 2433 );
2416 2434 };
2417 2435
2418 2436 /**
2419 2437 * Success callback for restoring a notebook to a checkpoint.
2420 2438 *
2421 2439 * @method restore_checkpoint_success
2422 2440 * @param {Object} data (ignored, should be empty)
2423 2441 * @param {String} status Description of response status
2424 2442 * @param {jqXHR} xhr jQuery Ajax object
2425 2443 */
2426 2444 Notebook.prototype.restore_checkpoint_success = function (data, status, xhr) {
2427 2445 this.events.trigger('checkpoint_restored.Notebook');
2428 2446 this.load_notebook(this.notebook_name, this.notebook_path);
2429 2447 };
2430 2448
2431 2449 /**
2432 2450 * Failure callback for restoring a notebook to a checkpoint.
2433 2451 *
2434 2452 * @method restore_checkpoint_error
2435 2453 * @param {jqXHR} xhr jQuery Ajax object
2436 2454 * @param {String} status Description of response status
2437 2455 * @param {String} error_msg HTTP error message
2438 2456 */
2439 2457 Notebook.prototype.restore_checkpoint_error = function (xhr, status, error_msg) {
2440 2458 this.events.trigger('checkpoint_restore_failed.Notebook');
2441 2459 };
2442 2460
2443 2461 /**
2444 2462 * Delete a notebook checkpoint.
2445 2463 *
2446 2464 * @method delete_checkpoint
2447 2465 * @param {String} checkpoint ID
2448 2466 */
2449 2467 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2450 2468 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2451 2469 var url = utils.url_join_encode(
2452 2470 this.base_url,
2453 2471 'api/notebooks',
2454 2472 this.notebook_path,
2455 2473 this.notebook_name,
2456 2474 'checkpoints',
2457 2475 checkpoint
2458 2476 );
2459 2477 $.ajax(url, {
2460 2478 type: 'DELETE',
2461 2479 success: $.proxy(this.delete_checkpoint_success, this),
2462 2480 error: $.proxy(this.delete_checkpoint_error, this)
2463 2481 });
2464 2482 };
2465 2483
2466 2484 /**
2467 2485 * Success callback for deleting a notebook checkpoint
2468 2486 *
2469 2487 * @method delete_checkpoint_success
2470 2488 * @param {Object} data (ignored, should be empty)
2471 2489 * @param {String} status Description of response status
2472 2490 * @param {jqXHR} xhr jQuery Ajax object
2473 2491 */
2474 2492 Notebook.prototype.delete_checkpoint_success = function (data, status, xhr) {
2475 2493 this.events.trigger('checkpoint_deleted.Notebook', data);
2476 2494 this.load_notebook(this.notebook_name, this.notebook_path);
2477 2495 };
2478 2496
2479 2497 /**
2480 2498 * Failure callback for deleting a notebook checkpoint.
2481 2499 *
2482 2500 * @method delete_checkpoint_error
2483 2501 * @param {jqXHR} xhr jQuery Ajax object
2484 2502 * @param {String} status Description of response status
2485 2503 * @param {String} error_msg HTTP error message
2486 2504 */
2487 2505 Notebook.prototype.delete_checkpoint_error = function (xhr, status, error_msg) {
2488 2506 this.events.trigger('checkpoint_delete_failed.Notebook');
2489 2507 };
2490 2508
2491 2509
2492 2510 // For backwards compatability.
2493 2511 IPython.Notebook = Notebook;
2494 2512
2495 2513 return {'Notebook': Notebook};
2496 2514 });
@@ -1,242 +1,247
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'base/js/utils',
8 8 'base/js/dialog',
9 9 'notebook/js/notificationwidget',
10 10 ], function(IPython, $, utils, dialog, notificationwidget) {
11 11 "use strict";
12 12
13 13 var NotificationArea = function (selector, options) {
14 14 // Constructor
15 15 //
16 16 // Parameters:
17 17 // selector: string
18 18 // options: dictionary
19 19 // Dictionary of keyword arguments.
20 20 // notebook: Notebook instance
21 21 // events: $(Events) instance
22 22 // save_widget: SaveWidget instance
23 23 this.selector = selector;
24 24 this.events = options.events;
25 25 this.save_widget = options.save_widget;
26 26 this.notebook = options.notebook;
27 this.keyboard_manager = options.keyboard_manager;
27 28 if (this.selector !== undefined) {
28 29 this.element = $(selector);
29 30 }
30 31 this.widget_dict = {};
31 32 };
32 33
33 34 NotificationArea.prototype.temp_message = function (msg, timeout, css_class) {
34 35 var uuid = utils.uuid();
35 36 if( css_class == 'danger') {css_class = 'ui-state-error';}
36 37 if( css_class == 'warning') {css_class = 'ui-state-highlight';}
37 38 var tdiv = $('<div>')
38 39 .attr('id',uuid)
39 40 .addClass('notification_widget ui-widget ui-widget-content ui-corner-all')
40 41 .addClass('border-box-sizing')
41 42 .addClass(css_class)
42 43 .hide()
43 44 .text(msg);
44 45
45 46 $(this.selector).append(tdiv);
46 47 var tmout = Math.max(1500,(timeout||1500));
47 48 tdiv.fadeIn(100);
48 49
49 50 setTimeout(function () {
50 51 tdiv.fadeOut(100, function () {tdiv.remove();});
51 52 }, tmout);
52 53 };
53 54
54 55 NotificationArea.prototype.widget = function(name) {
55 56 if(this.widget_dict[name] === undefined) {
56 57 return this.new_notification_widget(name);
57 58 }
58 59 return this.get_widget(name);
59 60 };
60 61
61 62 NotificationArea.prototype.get_widget = function(name) {
62 63 if(this.widget_dict[name] === undefined) {
63 64 throw('no widgets with this name');
64 65 }
65 66 return this.widget_dict[name];
66 67 };
67 68
68 69 NotificationArea.prototype.new_notification_widget = function(name) {
69 70 if(this.widget_dict[name] !== undefined) {
70 71 throw('widget with that name already exists ! ');
71 72 }
72 73 var div = $('<div/>').attr('id','notification_'+name);
73 74 $(this.selector).append(div);
74 75 this.widget_dict[name] = new notificationwidget.NotificationWidget('#notification_'+name);
75 76 return this.widget_dict[name];
76 77 };
77 78
78 79 NotificationArea.prototype.init_notification_widgets = function() {
79 80 var that = this;
80 81 var knw = this.new_notification_widget('kernel');
81 82 var $kernel_ind_icon = $("#kernel_indicator_icon");
82 83 var $modal_ind_icon = $("#modal_indicator_icon");
83 84
84 85 // Command/Edit mode
85 86 this.events.on('edit_mode.Notebook',function () {
86 87 that.save_widget.update_document_title();
87 88 $modal_ind_icon.attr('class','edit_mode_icon').attr('title','Edit Mode');
88 89 });
89 90
90 91 this.events.on('command_mode.Notebook',function () {
91 92 that.save_widget.update_document_title();
92 93 $modal_ind_icon.attr('class','command_mode_icon').attr('title','Command Mode');
93 94 });
94 95
95 96 // Implicitly start off in Command mode, switching to Edit mode will trigger event
96 97 $modal_ind_icon.attr('class','command-mode_icon').attr('title','Command Mode');
97 98
98 99 // Kernel events
99 100 this.events.on('status_idle.Kernel',function () {
100 101 that.save_widget.update_document_title();
101 102 $kernel_ind_icon.attr('class','kernel_idle_icon').attr('title','Kernel Idle');
102 103 });
103 104
104 105 this.events.on('status_busy.Kernel',function () {
105 106 window.document.title='(Busy) '+window.document.title;
106 107 $kernel_ind_icon.attr('class','kernel_busy_icon').attr('title','Kernel Busy');
107 108 });
108 109
109 110 this.events.on('status_restarting.Kernel',function () {
110 111 that.save_widget.update_document_title();
111 112 knw.set_message("Restarting kernel", 2000);
112 113 });
113 114
114 115 this.events.on('status_interrupting.Kernel',function () {
115 116 knw.set_message("Interrupting kernel", 2000);
116 117 });
117 118
118 119 // Start the kernel indicator in the busy state, and send a kernel_info request.
119 120 // When the kernel_info reply arrives, the kernel is idle.
120 121 $kernel_ind_icon.attr('class','kernel_busy_icon').attr('title','Kernel Busy');
121 122
122 123 this.events.on('status_started.Kernel', function (evt, data) {
123 124 data.kernel.kernel_info(function () {
124 125 that.events.trigger('status_idle.Kernel');
125 126 });
126 127 });
127 128
128 129 this.events.on('status_dead.Kernel',function () {
129 130 var msg = 'The kernel has died, and the automatic restart has failed.' +
130 131 ' It is possible the kernel cannot be restarted.' +
131 132 ' If you are not able to restart the kernel, you will still be able to save' +
132 133 ' the notebook, but running code will no longer work until the notebook' +
133 134 ' is reopened.';
134 135
135 136 dialog.modal({
136 137 title: "Dead kernel",
137 138 body : msg,
139 keyboard_manager: that.keyboard_manager,
140 notebook: that.notebook,
138 141 buttons : {
139 142 "Manual Restart": {
140 143 class: "btn-danger",
141 144 click: function () {
142 145 that.events.trigger('status_restarting.Kernel');
143 146 that.notebook.start_kernel();
144 147 }
145 148 },
146 149 "Don't restart": {}
147 150 }
148 151 });
149 152 });
150 153
151 154 this.events.on('websocket_closed.Kernel', function (event, data) {
152 155 var kernel = data.kernel;
153 156 var ws_url = data.ws_url;
154 157 var early = data.early;
155 158 var msg;
156 159 if (!early) {
157 160 knw.set_message('Reconnecting WebSockets', 1000);
158 161 setTimeout(function () {
159 162 kernel.start_channels();
160 163 }, 5000);
161 164 return;
162 165 }
163 166 console.log('WebSocket connection failed: ', ws_url);
164 167 msg = "A WebSocket connection could not be established." +
165 168 " You will NOT be able to run code. Check your" +
166 169 " network connection or notebook server configuration.";
167 170 dialog.modal({
168 171 title: "WebSocket connection failed",
169 172 body: msg,
173 keyboard_manager: that.keyboard_manager,
174 notebook: that.notebook,
170 175 buttons : {
171 176 "OK": {},
172 177 "Reconnect": {
173 178 click: function () {
174 179 knw.set_message('Reconnecting WebSockets', 1000);
175 180 setTimeout(function () {
176 181 kernel.start_channels();
177 182 }, 5000);
178 183 }
179 184 }
180 185 }
181 186 });
182 187 });
183 188
184 189
185 190 var nnw = this.new_notification_widget('notebook');
186 191
187 192 // Notebook events
188 193 this.events.on('notebook_loading.Notebook', function () {
189 194 nnw.set_message("Loading notebook",500);
190 195 });
191 196 this.events.on('notebook_loaded.Notebook', function () {
192 197 nnw.set_message("Notebook loaded",500);
193 198 });
194 199 this.events.on('notebook_saving.Notebook', function () {
195 200 nnw.set_message("Saving notebook",500);
196 201 });
197 202 this.events.on('notebook_saved.Notebook', function () {
198 203 nnw.set_message("Notebook saved",2000);
199 204 });
200 205 this.events.on('notebook_save_failed.Notebook', function (evt, xhr, status, data) {
201 206 nnw.set_message(data || "Notebook save failed");
202 207 });
203 208
204 209 // Checkpoint events
205 210 this.events.on('checkpoint_created.Notebook', function (evt, data) {
206 211 var msg = "Checkpoint created";
207 212 if (data.last_modified) {
208 213 var d = new Date(data.last_modified);
209 214 msg = msg + ": " + d.format("HH:MM:ss");
210 215 }
211 216 nnw.set_message(msg, 2000);
212 217 });
213 218 this.events.on('checkpoint_failed.Notebook', function () {
214 219 nnw.set_message("Checkpoint failed");
215 220 });
216 221 this.events.on('checkpoint_deleted.Notebook', function () {
217 222 nnw.set_message("Checkpoint deleted", 500);
218 223 });
219 224 this.events.on('checkpoint_delete_failed.Notebook', function () {
220 225 nnw.set_message("Checkpoint delete failed");
221 226 });
222 227 this.events.on('checkpoint_restoring.Notebook', function () {
223 228 nnw.set_message("Restoring to checkpoint...", 500);
224 229 });
225 230 this.events.on('checkpoint_restore_failed.Notebook', function () {
226 231 nnw.set_message("Checkpoint restore failed");
227 232 });
228 233
229 234 // Autosave events
230 235 this.events.on('autosave_disabled.Notebook', function () {
231 236 nnw.set_message("Autosave disabled", 2000);
232 237 });
233 238 this.events.on('autosave_enabled.Notebook', function (evt, interval) {
234 239 nnw.set_message("Saving every " + interval / 1000 + "s", 1000);
235 240 });
236 241
237 242 };
238 243
239 244 IPython.NotificationArea = NotificationArea;
240 245
241 246 return {'NotificationArea': NotificationArea};
242 247 });
@@ -1,180 +1,184
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'base/js/utils',
8 8 'base/js/dialog',
9 9 ], function(IPython, $, utils, dialog) {
10 10 "use strict";
11 11 var platform = utils.platform;
12 12
13 13 var QuickHelp = function (options) {
14 14 // Constructor
15 15 //
16 16 // Parameters:
17 17 // options: dictionary
18 18 // Dictionary of keyword arguments.
19 19 // events: $(Events) instance
20 20 // keyboard_manager: KeyboardManager instance
21 // notebook: Notebook instance
21 22 this.keyboard_manager = options.keyboard_manager;
23 this.notebook = options.notebook;
22 24 this.keyboard_manager.quick_help = this;
23 25 this.events = options.events;
24 26 };
25 27
26 28 var cmd_ctrl = 'Ctrl-';
27 29 var platform_specific;
28 30
29 31 if (platform === 'MacOS') {
30 32 // Mac OS X specific
31 33 cmd_ctrl = 'Cmd-';
32 34 platform_specific = [
33 35 { shortcut: "Cmd-Up", help:"go to cell start" },
34 36 { shortcut: "Cmd-Down", help:"go to cell end" },
35 37 { shortcut: "Opt-Left", help:"go one word left" },
36 38 { shortcut: "Opt-Right", help:"go one word right" },
37 39 { shortcut: "Opt-Backspace", help:"del word before" },
38 40 { shortcut: "Opt-Delete", help:"del word after" },
39 41 ];
40 42 } else {
41 43 // PC specific
42 44 platform_specific = [
43 45 { shortcut: "Ctrl-Home", help:"go to cell start" },
44 46 { shortcut: "Ctrl-Up", help:"go to cell start" },
45 47 { shortcut: "Ctrl-End", help:"go to cell end" },
46 48 { shortcut: "Ctrl-Down", help:"go to cell end" },
47 49 { shortcut: "Ctrl-Left", help:"go one word left" },
48 50 { shortcut: "Ctrl-Right", help:"go one word right" },
49 51 { shortcut: "Ctrl-Backspace", help:"del word before" },
50 52 { shortcut: "Ctrl-Delete", help:"del word after" },
51 53 ];
52 54 }
53 55
54 56 var cm_shortcuts = [
55 57 { shortcut:"Tab", help:"code completion or indent" },
56 58 { shortcut:"Shift-Tab", help:"tooltip" },
57 59 { shortcut: cmd_ctrl + "]", help:"indent" },
58 60 { shortcut: cmd_ctrl + "[", help:"dedent" },
59 61 { shortcut: cmd_ctrl + "a", help:"select all" },
60 62 { shortcut: cmd_ctrl + "z", help:"undo" },
61 63 { shortcut: cmd_ctrl + "Shift-z", help:"redo" },
62 64 { shortcut: cmd_ctrl + "y", help:"redo" },
63 65 ].concat( platform_specific );
64 66
65 67
66 68
67 69
68 70
69 71
70 72 QuickHelp.prototype.show_keyboard_shortcuts = function () {
71 73 // toggles display of keyboard shortcut dialog
72 74 var that = this;
73 75 if ( this.force_rebuild ) {
74 76 this.shortcut_dialog.remove();
75 77 delete(this.shortcut_dialog);
76 78 this.force_rebuild = false;
77 79 }
78 80 if ( this.shortcut_dialog ){
79 81 // if dialog is already shown, close it
80 82 $(this.shortcut_dialog).modal("toggle");
81 83 return;
82 84 }
83 85 var command_shortcuts = this.keyboard_manager.command_shortcuts.help();
84 86 var edit_shortcuts = this.keyboard_manager.edit_shortcuts.help();
85 87 var help, shortcut;
86 88 var i, half, n;
87 89 var element = $('<div/>');
88 90
89 91 // The documentation
90 92 var doc = $('<div/>').addClass('alert alert-warning');
91 93 doc.append(
92 94 $('<button/>').addClass('close').attr('data-dismiss','alert').html('&times;')
93 95 ).append(
94 96 'The IPython Notebook has two different keyboard input modes. <b>Edit mode</b> '+
95 97 'allows you to type code/text into a cell and is indicated by a green cell '+
96 98 'border. <b>Command mode</b> binds the keyboard to notebook level actions '+
97 99 'and is indicated by a grey cell border.'
98 100 );
99 101 element.append(doc);
100 102
101 103 // Command mode
102 104 var cmd_div = this.build_command_help();
103 105 element.append(cmd_div);
104 106
105 107 // Edit mode
106 108 var edit_div = this.build_edit_help(cm_shortcuts);
107 109 element.append(edit_div);
108 110
109 111 this.shortcut_dialog = dialog.modal({
110 112 title : "Keyboard shortcuts",
111 113 body : element,
112 114 destroy : false,
113 115 buttons : {
114 116 Close : {}
115 }
117 },
118 notebook: this.notebook,
119 keyboard_manager: this.keyboard_manager,
116 120 });
117 121 this.shortcut_dialog.addClass("modal_stretch");
118 122
119 123 this.events.on('rebuild.QuickHelp', function() { that.force_rebuild = true;});
120 124 };
121 125
122 126 QuickHelp.prototype.build_command_help = function () {
123 127 var command_shortcuts = this.keyboard_manager.command_shortcuts.help();
124 128 return build_div('<h4>Command Mode (press <code>Esc</code> to enable)</h4>', command_shortcuts);
125 129 };
126 130
127 131 var special_case = { pageup: "PageUp", pagedown: "Page Down", 'minus': '-' };
128 132 var prettify = function (s) {
129 133 s = s.replace(/-$/, 'minus'); // catch shortcuts using '-' key
130 134 var keys = s.split('-');
131 135 var k, i;
132 136 for (i=0; i < keys.length; i++) {
133 137 k = keys[i];
134 138 if ( k.length == 1 ) {
135 139 keys[i] = "<code><strong>" + k + "</strong></code>";
136 140 continue; // leave individual keys lower-cased
137 141 }
138 142 keys[i] = ( special_case[k] ? special_case[k] : k.charAt(0).toUpperCase() + k.slice(1) );
139 143 keys[i] = "<code><strong>" + keys[i] + "</strong></code>";
140 144 }
141 145 return keys.join('-');
142 146
143 147
144 148 };
145 149
146 150 QuickHelp.prototype.build_edit_help = function (cm_shortcuts) {
147 151 var edit_shortcuts = this.keyboard_manager.edit_shortcuts.help();
148 152 jQuery.merge(cm_shortcuts, edit_shortcuts);
149 153 return build_div('<h4>Edit Mode (press <code>Enter</code> to enable)</h4>', cm_shortcuts);
150 154 };
151 155
152 156 var build_one = function (s) {
153 157 var help = s.help;
154 158 var shortcut = prettify(s.shortcut);
155 159 return $('<div>').addClass('quickhelp').
156 160 append($('<span/>').addClass('shortcut_key').append($(shortcut))).
157 161 append($('<span/>').addClass('shortcut_descr').text(' : ' + help));
158 162
159 163 };
160 164
161 165 var build_div = function (title, shortcuts) {
162 166 var i, half, n;
163 167 var div = $('<div/>').append($(title));
164 168 var sub_div = $('<div/>').addClass('hbox');
165 169 var col1 = $('<div/>').addClass('box-flex1');
166 170 var col2 = $('<div/>').addClass('box-flex1');
167 171 n = shortcuts.length;
168 172 half = ~~(n/2); // Truncate :)
169 173 for (i=0; i<half; i++) { col1.append( build_one(shortcuts[i]) ); }
170 174 for (i=half; i<n; i++) { col2.append( build_one(shortcuts[i]) ); }
171 175 sub_div.append(col1).append(col2);
172 176 div.append(sub_div);
173 177 return div;
174 178 };
175 179
176 180 // Backwards compatability.
177 181 IPython.QuickHelp = QuickHelp;
178 182
179 183 return {'QuickHelp': QuickHelp};
180 184 });
@@ -1,461 +1,461
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'notebook/js/cell',
8 8 'base/js/security',
9 9 'notebook/js/mathjaxutils',
10 10 'notebook/js/celltoolbar',
11 11 'components/marked/lib/marked',
12 12 ], function(IPython, $, cell, security, mathjaxutils, celltoolbar, marked) {
13 13 "use strict";
14 14 var Cell = cell.Cell;
15 15
16 16 var TextCell = function (options) {
17 17 // Constructor
18 18 //
19 19 // Construct a new TextCell, codemirror mode is by default 'htmlmixed',
20 20 // and cell type is 'text' cell start as not redered.
21 21 //
22 22 // Parameters:
23 23 // options: dictionary
24 24 // Dictionary of keyword arguments.
25 25 // events: $(Events) instance
26 26 // config: dictionary
27 27 // keyboard_manager: KeyboardManager instance
28 28 // notebook: Notebook instance
29 29 options = options || {};
30 30
31 31 // in all TextCell/Cell subclasses
32 32 // do not assign most of members here, just pass it down
33 33 // in the options dict potentially overwriting what you wish.
34 34 // they will be assigned in the base class.
35 35 this.notebook = options.notebook;
36 36 this.events = options.events;
37 37 this.config = options.config;
38 38
39 39 // we cannot put this as a class key as it has handle to "this".
40 40 var cm_overwrite_options = {
41 41 onKeyEvent: $.proxy(this.handle_keyevent,this)
42 42 };
43 43 var config = this.mergeopt(TextCell, this.config, {cm_config:cm_overwrite_options});
44 44 Cell.apply(this, [{
45 45 config: config,
46 46 keyboard_manager: options.keyboard_manager,
47 events: events}]);
47 events: this.events}]);
48 48
49 49 this.cell_type = this.cell_type || 'text';
50 50 mathjaxutils = mathjaxutils;
51 51 this.rendered = false;
52 52 };
53 53
54 54 TextCell.prototype = new Cell();
55 55
56 56 TextCell.options_default = {
57 57 cm_config : {
58 58 extraKeys: {"Tab": "indentMore","Shift-Tab" : "indentLess"},
59 59 mode: 'htmlmixed',
60 60 lineWrapping : true,
61 61 }
62 62 };
63 63
64 64
65 65 /**
66 66 * Create the DOM element of the TextCell
67 67 * @method create_element
68 68 * @private
69 69 */
70 70 TextCell.prototype.create_element = function () {
71 71 Cell.prototype.create_element.apply(this, arguments);
72 72
73 73 var cell = $("<div>").addClass('cell text_cell border-box-sizing');
74 74 cell.attr('tabindex','2');
75 75
76 76 var prompt = $('<div/>').addClass('prompt input_prompt');
77 77 cell.append(prompt);
78 78 var inner_cell = $('<div/>').addClass('inner_cell');
79 79 this.celltoolbar = new celltoolbar.CellToolbar(this, this.events, this.notebook);
80 80 inner_cell.append(this.celltoolbar.element);
81 81 var input_area = $('<div/>').addClass('input_area');
82 82 this.code_mirror = new CodeMirror(input_area.get(0), this.cm_config);
83 83 // The tabindex=-1 makes this div focusable.
84 84 var render_area = $('<div/>').addClass('text_cell_render border-box-sizing').
85 85 addClass('rendered_html').attr('tabindex','-1');
86 86 inner_cell.append(input_area).append(render_area);
87 87 cell.append(inner_cell);
88 88 this.element = cell;
89 89 };
90 90
91 91
92 92 /**
93 93 * Bind the DOM evet to cell actions
94 94 * Need to be called after TextCell.create_element
95 95 * @private
96 96 * @method bind_event
97 97 */
98 98 TextCell.prototype.bind_events = function () {
99 99 Cell.prototype.bind_events.apply(this);
100 100 var that = this;
101 101
102 102 this.element.dblclick(function () {
103 103 if (that.selected === false) {
104 104 this.events.trigger('select.Cell', {'cell':that});
105 105 }
106 106 var cont = that.unrender();
107 107 if (cont) {
108 108 that.focus_editor();
109 109 }
110 110 });
111 111 };
112 112
113 113 // Cell level actions
114 114
115 115 TextCell.prototype.select = function () {
116 116 var cont = Cell.prototype.select.apply(this);
117 117 if (cont) {
118 118 if (this.mode === 'edit') {
119 119 this.code_mirror.refresh();
120 120 }
121 121 }
122 122 return cont;
123 123 };
124 124
125 125 TextCell.prototype.unrender = function () {
126 126 if (this.read_only) return;
127 127 var cont = Cell.prototype.unrender.apply(this);
128 128 if (cont) {
129 129 var text_cell = this.element;
130 130 var output = text_cell.find("div.text_cell_render");
131 131 output.hide();
132 132 text_cell.find('div.input_area').show();
133 133 if (this.get_text() === this.placeholder) {
134 134 this.set_text('');
135 135 }
136 136 this.refresh();
137 137 }
138 138 if (this.celltoolbar.ui_controls_list.length) {
139 139 this.celltoolbar.show();
140 140 }
141 141 return cont;
142 142 };
143 143
144 144 TextCell.prototype.execute = function () {
145 145 this.render();
146 146 };
147 147
148 148 /**
149 149 * setter: {{#crossLink "TextCell/set_text"}}{{/crossLink}}
150 150 * @method get_text
151 151 * @retrun {string} CodeMirror current text value
152 152 */
153 153 TextCell.prototype.get_text = function() {
154 154 return this.code_mirror.getValue();
155 155 };
156 156
157 157 /**
158 158 * @param {string} text - Codemiror text value
159 159 * @see TextCell#get_text
160 160 * @method set_text
161 161 * */
162 162 TextCell.prototype.set_text = function(text) {
163 163 this.code_mirror.setValue(text);
164 164 this.code_mirror.refresh();
165 165 };
166 166
167 167 /**
168 168 * setter :{{#crossLink "TextCell/set_rendered"}}{{/crossLink}}
169 169 * @method get_rendered
170 170 * */
171 171 TextCell.prototype.get_rendered = function() {
172 172 return this.element.find('div.text_cell_render').html();
173 173 };
174 174
175 175 /**
176 176 * @method set_rendered
177 177 */
178 178 TextCell.prototype.set_rendered = function(text) {
179 179 this.element.find('div.text_cell_render').html(text);
180 180 this.celltoolbar.hide();
181 181 };
182 182
183 183
184 184 /**
185 185 * Create Text cell from JSON
186 186 * @param {json} data - JSON serialized text-cell
187 187 * @method fromJSON
188 188 */
189 189 TextCell.prototype.fromJSON = function (data) {
190 190 Cell.prototype.fromJSON.apply(this, arguments);
191 191 if (data.cell_type === this.cell_type) {
192 192 if (data.source !== undefined) {
193 193 this.set_text(data.source);
194 194 // make this value the starting point, so that we can only undo
195 195 // to this state, instead of a blank cell
196 196 this.code_mirror.clearHistory();
197 197 // TODO: This HTML needs to be treated as potentially dangerous
198 198 // user input and should be handled before set_rendered.
199 199 this.set_rendered(data.rendered || '');
200 200 this.rendered = false;
201 201 this.render();
202 202 }
203 203 }
204 204 };
205 205
206 206 /** Generate JSON from cell
207 207 * @return {object} cell data serialised to json
208 208 */
209 209 TextCell.prototype.toJSON = function () {
210 210 var data = Cell.prototype.toJSON.apply(this);
211 211 data.source = this.get_text();
212 212 if (data.source == this.placeholder) {
213 213 data.source = "";
214 214 }
215 215 return data;
216 216 };
217 217
218 218
219 219 var MarkdownCell = function (options) {
220 220 // Constructor
221 221 //
222 222 // Parameters:
223 223 // options: dictionary
224 224 // Dictionary of keyword arguments.
225 225 // events: $(Events) instance
226 226 // config: dictionary
227 227 // keyboard_manager: KeyboardManager instance
228 228 // notebook: Notebook instance
229 229 options = options || {};
230 230 var config = this.mergeopt(MarkdownCell, options.config);
231 231 TextCell.apply(this, [$.extend({}, options, {config: config})]);
232 232
233 233 this.cell_type = 'markdown';
234 234 };
235 235
236 236 MarkdownCell.options_default = {
237 237 cm_config: {
238 238 mode: 'ipythongfm'
239 239 },
240 240 placeholder: "Type *Markdown* and LaTeX: $\\alpha^2$"
241 241 };
242 242
243 243 MarkdownCell.prototype = new TextCell();
244 244
245 245 /**
246 246 * @method render
247 247 */
248 248 MarkdownCell.prototype.render = function () {
249 249 var cont = TextCell.prototype.render.apply(this);
250 250 if (cont) {
251 251 var text = this.get_text();
252 252 var math = null;
253 253 if (text === "") { text = this.placeholder; }
254 254 var text_and_math = mathjaxutils.remove_math(text);
255 255 text = text_and_math[0];
256 256 math = text_and_math[1];
257 257 var html = marked.parser(marked.lexer(text));
258 258 html = mathjaxutils.replace_math(html, math);
259 259 html = security.sanitize_html(html);
260 260 html = $($.parseHTML(html));
261 261 // links in markdown cells should open in new tabs
262 262 html.find("a[href]").not('[href^="#"]').attr("target", "_blank");
263 263 this.set_rendered(html);
264 264 this.element.find('div.input_area').hide();
265 265 this.element.find("div.text_cell_render").show();
266 266 this.typeset();
267 267 }
268 268 return cont;
269 269 };
270 270
271 271
272 272 var RawCell = function (options) {
273 273 // Constructor
274 274 //
275 275 // Parameters:
276 276 // options: dictionary
277 277 // Dictionary of keyword arguments.
278 278 // events: $(Events) instance
279 279 // config: dictionary
280 280 // keyboard_manager: KeyboardManager instance
281 281 // notebook: Notebook instance
282 282 options = options || {};
283 283 var config = this.mergeopt(RawCell, options.config);
284 284 TextCell.apply(this, [$.extend({}, options, {config: config})]);
285 285
286 286 // RawCell should always hide its rendered div
287 287 this.element.find('div.text_cell_render').hide();
288 288 this.cell_type = 'raw';
289 289 };
290 290
291 291 RawCell.options_default = {
292 292 placeholder : "Write raw LaTeX or other formats here, for use with nbconvert. " +
293 293 "It will not be rendered in the notebook. " +
294 294 "When passing through nbconvert, a Raw Cell's content is added to the output unmodified."
295 295 };
296 296
297 297 RawCell.prototype = new TextCell();
298 298
299 299 /** @method bind_events **/
300 300 RawCell.prototype.bind_events = function () {
301 301 TextCell.prototype.bind_events.apply(this);
302 302 var that = this;
303 303 this.element.focusout(function() {
304 304 that.auto_highlight();
305 305 that.render();
306 306 });
307 307
308 308 this.code_mirror.on('focus', function() { that.unrender(); });
309 309 };
310 310
311 311 /**
312 312 * Trigger autodetection of highlight scheme for current cell
313 313 * @method auto_highlight
314 314 */
315 315 RawCell.prototype.auto_highlight = function () {
316 316 this._auto_highlight(this.config.raw_cell_highlight);
317 317 };
318 318
319 319 /** @method render **/
320 320 RawCell.prototype.render = function () {
321 321 var cont = TextCell.prototype.render.apply(this);
322 322 if (cont){
323 323 var text = this.get_text();
324 324 if (text === "") { text = this.placeholder; }
325 325 this.set_text(text);
326 326 this.element.removeClass('rendered');
327 327 }
328 328 return cont;
329 329 };
330 330
331 331
332 332 var HeadingCell = function (options) {
333 333 // Constructor
334 334 //
335 335 // Parameters:
336 336 // options: dictionary
337 337 // Dictionary of keyword arguments.
338 338 // events: $(Events) instance
339 339 // config: dictionary
340 340 // keyboard_manager: KeyboardManager instance
341 341 // notebook: Notebook instance
342 342 options = options || {};
343 343 var config = this.mergeopt(HeadingCell, options.config);
344 344 TextCell.apply(this, [$.extend({}, options, {config: config})]);
345 345
346 346 this.level = 1;
347 347 this.cell_type = 'heading';
348 348 };
349 349
350 350 HeadingCell.options_default = {
351 351 placeholder: "Type Heading Here"
352 352 };
353 353
354 354 HeadingCell.prototype = new TextCell();
355 355
356 356 /** @method fromJSON */
357 357 HeadingCell.prototype.fromJSON = function (data) {
358 358 if (data.level !== undefined){
359 359 this.level = data.level;
360 360 }
361 361 TextCell.prototype.fromJSON.apply(this, arguments);
362 362 };
363 363
364 364
365 365 /** @method toJSON */
366 366 HeadingCell.prototype.toJSON = function () {
367 367 var data = TextCell.prototype.toJSON.apply(this);
368 368 data.level = this.get_level();
369 369 return data;
370 370 };
371 371
372 372 /**
373 373 * can the cell be split into two cells
374 374 * @method is_splittable
375 375 **/
376 376 HeadingCell.prototype.is_splittable = function () {
377 377 return false;
378 378 };
379 379
380 380
381 381 /**
382 382 * can the cell be merged with other cells
383 383 * @method is_mergeable
384 384 **/
385 385 HeadingCell.prototype.is_mergeable = function () {
386 386 return false;
387 387 };
388 388
389 389 /**
390 390 * Change heading level of cell, and re-render
391 391 * @method set_level
392 392 */
393 393 HeadingCell.prototype.set_level = function (level) {
394 394 this.level = level;
395 395 if (this.rendered) {
396 396 this.rendered = false;
397 397 this.render();
398 398 }
399 399 };
400 400
401 401 /** The depth of header cell, based on html (h1 to h6)
402 402 * @method get_level
403 403 * @return {integer} level - for 1 to 6
404 404 */
405 405 HeadingCell.prototype.get_level = function () {
406 406 return this.level;
407 407 };
408 408
409 409
410 410 HeadingCell.prototype.get_rendered = function () {
411 411 var r = this.element.find("div.text_cell_render");
412 412 return r.children().first().html();
413 413 };
414 414
415 415 HeadingCell.prototype.render = function () {
416 416 var cont = TextCell.prototype.render.apply(this);
417 417 if (cont) {
418 418 var text = this.get_text();
419 419 var math = null;
420 420 // Markdown headings must be a single line
421 421 text = text.replace(/\n/g, ' ');
422 422 if (text === "") { text = this.placeholder; }
423 423 text = new Array(this.level + 1).join("#") + " " + text;
424 424 var text_and_math = mathjaxutils.remove_math(text);
425 425 text = text_and_math[0];
426 426 math = text_and_math[1];
427 427 var html = marked.parser(marked.lexer(text));
428 428 html = mathjaxutils.replace_math(html, math);
429 429 html = security.sanitize_html(html);
430 430 var h = $($.parseHTML(html));
431 431 // add id and linkback anchor
432 432 var hash = h.text().replace(/ /g, '-');
433 433 h.attr('id', hash);
434 434 h.append(
435 435 $('<a/>')
436 436 .addClass('anchor-link')
437 437 .attr('href', '#' + hash)
438 438 .text('¶')
439 439 );
440 440 this.set_rendered(h);
441 441 this.element.find('div.input_area').hide();
442 442 this.element.find("div.text_cell_render").show();
443 443 this.typeset();
444 444 }
445 445 return cont;
446 446 };
447 447
448 448 // Backwards compatability.
449 449 IPython.TextCell = TextCell;
450 450 IPython.MarkdownCell = MarkdownCell;
451 451 IPython.RawCell = RawCell;
452 452 IPython.HeadingCell = HeadingCell;
453 453
454 454 var Cells = {
455 455 'TextCell': TextCell,
456 456 'MarkdownCell': MarkdownCell,
457 457 'RawCell': RawCell,
458 458 'HeadingCell': HeadingCell,
459 459 };
460 460 return Cells;
461 461 });
@@ -1,173 +1,173
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 'components/bootstrap-tour/build/js/bootstrap-tour.min',
7 'bootstraptour',
8 8 ], function(IPython, $, Tour) {
9 9 "use strict";
10 10
11 11 var tour_style = "<div class='popover tour'>\n" +
12 12 "<div class='arrow'></div>\n" +
13 13 "<div style='position:absolute; top:7px; right:7px'>\n" +
14 14 "<button class='btn btn-default btn-sm icon-remove' data-role='end'></button>\n" +
15 15 "</div><h3 class='popover-title'></h3>\n" +
16 16 "<div class='popover-content'></div>\n" +
17 17 "<div class='popover-navigation'>\n" +
18 18 "<button class='btn btn-default icon-step-backward' data-role='prev'></button>\n" +
19 19 "<button class='btn btn-default icon-step-forward pull-right' data-role='next'></button>\n" +
20 20 "<button id='tour-pause' class='btn btn-sm btn-default icon-pause' data-resume-text='' data-pause-text='' data-role='pause-resume'></button>\n" +
21 21 "</div>\n" +
22 22 "</div>";
23 23
24 24 var NotebookTour = function (notebook, events) {
25 25 var that = this;
26 26 this.notebook = notebook;
27 27 this.step_duration = 0;
28 28 this.events = events;
29 29 this.tour_steps = [
30 30 {
31 31 title: "Welcome to the Notebook Tour",
32 32 placement: 'bottom',
33 33 orphan: true,
34 34 content: "You can use the left and right arrow keys to go backwards and forwards.",
35 35 }, {
36 36 element: "#notebook_name",
37 37 title: "Filename",
38 38 placement: 'bottom',
39 39 content: "Click here to change the filename for this notebook."
40 40 }, {
41 41 element: $("#menus").parent(),
42 42 placement: 'bottom',
43 43 backdrop: true,
44 44 title: "Notebook Menubar",
45 45 content: "The menubar has menus for actions on the notebook, its cells, and the kernel it communicates with."
46 46 }, {
47 47 element: "#maintoolbar",
48 48 placement: 'bottom',
49 49 backdrop: true,
50 50 title: "Notebook Toolbar",
51 51 content: "The toolbar has buttons for the most common actions. Hover your mouse over each button for more information."
52 52 }, {
53 53 element: "#modal_indicator",
54 54 title: "Mode Indicator",
55 55 placement: 'bottom',
56 56 content: "The Notebook has two modes: Edit Mode and Command Mode. In this area, an indicator can appear to tell you which mode you are in.",
57 57 onShow: function(tour) { that.command_icon_hack(); }
58 58 }, {
59 59 element: "#modal_indicator",
60 60 title: "Command Mode",
61 61 placement: 'bottom',
62 62 onShow: function(tour) { notebook.command_mode(); that.command_icon_hack(); },
63 63 onNext: function(tour) { that.edit_mode(); },
64 64 content: "Right now you are in Command Mode, and many keyboard shortcuts are available. In this mode, no icon is displayed in the indicator area."
65 65 }, {
66 66 element: "#modal_indicator",
67 67 title: "Edit Mode",
68 68 placement: 'bottom',
69 69 onShow: function(tour) { that.edit_mode(); },
70 70 content: "Pressing <code>Enter</code> or clicking in the input text area of the cell switches to Edit Mode."
71 71 }, {
72 72 element: '.selected',
73 73 title: "Edit Mode",
74 74 placement: 'bottom',
75 75 onShow: function(tour) { that.edit_mode(); },
76 76 content: "Notice that the border around the currently active cell changed color. Typing will insert text into the currently active cell."
77 77 }, {
78 78 element: '.selected',
79 79 title: "Back to Command Mode",
80 80 placement: 'bottom',
81 81 onShow: function(tour) { notebook.command_mode(); },
82 82 onHide: function(tour) { $('#help_menu').parent().children('a').click(); },
83 83 content: "Pressing <code>Esc</code> or clicking outside of the input text area takes you back to Command Mode."
84 84 }, {
85 85 element: '#keyboard_shortcuts',
86 86 title: "Keyboard Shortcuts",
87 87 placement: 'bottom',
88 88 onHide: function(tour) { $('#help_menu').parent().children('a').click(); },
89 89 content: "You can click here to get a list of all of the keyboard shortcuts."
90 90 }, {
91 91 element: "#kernel_indicator",
92 92 title: "Kernel Indicator",
93 93 placement: 'bottom',
94 94 onShow: function(tour) { events.trigger('status_idle.Kernel');},
95 95 content: "This is the Kernel indicator. It looks like this when the Kernel is idle.",
96 96 }, {
97 97 element: "#kernel_indicator",
98 98 title: "Kernel Indicator",
99 99 placement: 'bottom',
100 100 onShow: function(tour) { events.trigger('status_busy.Kernel'); },
101 101 content: "The Kernel indicator looks like this when the Kernel is busy.",
102 102 }, {
103 103 element: ".icon-stop",
104 104 placement: 'bottom',
105 105 title: "Interrupting the Kernel",
106 106 onHide: function(tour) { events.trigger('status_idle.Kernel'); },
107 107 content: "To cancel a computation in progress, you can click here."
108 108 }, {
109 109 element: "#notification_kernel",
110 110 placement: 'bottom',
111 111 onShow: function(tour) { $('.icon-stop').click(); },
112 112 title: "Notification Area",
113 113 content: "Messages in response to user actions (Save, Interrupt, etc) appear here."
114 114 }, {
115 115 title: "Fin.",
116 116 placement: 'bottom',
117 117 orphan: true,
118 118 content: "This concludes the IPython Notebook User Interface tour.Tour. Happy hacking!",
119 119 }
120 120 ];
121 121
122 122 this.tour = new Tour({
123 123 //orphan: true,
124 124 storage: false, // start tour from beginning every time
125 125 //element: $("#ipython_notebook"),
126 126 debug: true,
127 127 reflex: true, // click on element to continue tour
128 128 //backdrop: true, // show dark behind popover
129 129 animation: false,
130 130 duration: this.step_duration,
131 131 onStart: function() { console.log('tour started'); },
132 132 // TODO: remove the onPause/onResume logic once pi's patch has been
133 133 // merged upstream to make this work via data-resume-class and
134 134 // data-resume-text attributes.
135 135 onPause: this.toggle_pause_play,
136 136 onResume: this.toggle_pause_play,
137 137 steps: this.tour_steps,
138 138 template: tour_style,
139 139 orphan: true
140 140 });
141 141
142 142 };
143 143
144 144 NotebookTour.prototype.start = function () {
145 145 console.log("let's start the tour");
146 146 this.tour.init();
147 147 this.tour.start();
148 148 if (this.tour.ended())
149 149 {
150 150 this.tour.restart();
151 151 }
152 152 };
153 153
154 154 NotebookTour.prototype.command_icon_hack = function() {
155 155 $('#modal_indicator').css('min-height', 20);
156 156 };
157 157
158 158 NotebookTour.prototype.toggle_pause_play = function () {
159 159 $('#tour-pause').toggleClass('icon-pause icon-play');
160 160 };
161 161
162 162 NotebookTour.prototype.edit_mode = function() {
163 163 this.notebook.focus_cell();
164 164 this.notebook.edit_mode();
165 165 };
166 166
167 167 // For backwards compatability.
168 168 IPython.NotebookTour = NotebookTour;
169 169
170 170 return {'Tour': NotebookTour};
171 171
172 172 });
173 173
@@ -1,116 +1,116
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'base/js/utils',
8 8 'services/kernels/js/kernel',
9 9 ], function(IPython, $, utils, kernel) {
10 10 "use strict";
11 11
12 12 var Session = function(options){
13 13 this.kernel = null;
14 14 this.id = null;
15 15 this.notebook = options.notebook;
16 this.name = notebook.notebook_name;
17 this.path = notebook.notebook_path;
18 this.base_url = notebook.base_url;
16 this.name = options.notebook_name;
17 this.path = options.notebook_path;
18 this.base_url = options.base_url;
19 19 };
20 20
21 21 Session.prototype.start = function(callback) {
22 22 var that = this;
23 23 var model = {
24 24 notebook : {
25 25 name : this.name,
26 26 path : this.path
27 27 }
28 28 };
29 29 var settings = {
30 30 processData : false,
31 31 cache : false,
32 32 type : "POST",
33 33 data: JSON.stringify(model),
34 34 dataType : "json",
35 35 success : function (data, status, xhr) {
36 36 that._handle_start_success(data);
37 37 if (callback) {
38 38 callback(data, status, xhr);
39 39 }
40 40 },
41 41 error : utils.log_ajax_error,
42 42 };
43 43 var url = utils.url_join_encode(this.base_url, 'api/sessions');
44 44 $.ajax(url, settings);
45 45 };
46 46
47 47 Session.prototype.rename_notebook = function (name, path) {
48 48 this.name = name;
49 49 this.path = path;
50 50 var model = {
51 51 notebook : {
52 52 name : this.name,
53 53 path : this.path
54 54 }
55 55 };
56 56 var settings = {
57 57 processData : false,
58 58 cache : false,
59 59 type : "PATCH",
60 60 data: JSON.stringify(model),
61 61 dataType : "json",
62 62 error : utils.log_ajax_error,
63 63 };
64 64 var url = utils.url_join_encode(this.base_url, 'api/sessions', this.id);
65 65 $.ajax(url, settings);
66 66 };
67 67
68 68 Session.prototype.delete = function() {
69 69 var settings = {
70 70 processData : false,
71 71 cache : false,
72 72 type : "DELETE",
73 73 dataType : "json",
74 74 error : utils.log_ajax_error,
75 75 };
76 76 this.kernel.running = false;
77 77 var url = utils.url_join_encode(this.base_url, 'api/sessions', this.id);
78 78 $.ajax(url, settings);
79 79 };
80 80
81 81 // Kernel related things
82 82 /**
83 83 * Create the Kernel object associated with this Session.
84 84 *
85 85 * @method _handle_start_success
86 86 */
87 87 Session.prototype._handle_start_success = function (data, status, xhr) {
88 88 this.id = data.id;
89 89 var kernel_service_url = utils.url_path_join(this.base_url, "api/kernels");
90 90 this.kernel = new kernel.Kernel(kernel_service_url, this.notebook);
91 91 this.kernel._kernel_started(data.kernel);
92 92 };
93 93
94 94 /**
95 95 * Prompt the user to restart the IPython kernel.
96 96 *
97 97 * @method restart_kernel
98 98 */
99 99 Session.prototype.restart_kernel = function () {
100 100 this.kernel.restart();
101 101 };
102 102
103 103 Session.prototype.interrupt_kernel = function() {
104 104 this.kernel.interrupt();
105 105 };
106 106
107 107
108 108 Session.prototype.kill_kernel = function() {
109 109 this.kernel.kill();
110 110 };
111 111
112 112 // For backwards compatability.
113 113 IPython.Session = Session;
114 114
115 115 return {'Session': Session};
116 116 });
@@ -1,87 +1,91
1 1 <!DOCTYPE HTML>
2 2 <html>
3 3
4 4 <head>
5 5 <meta charset="utf-8">
6 6
7 7 <title>{% block title %}IPython Notebook{% endblock %}</title>
8 8 <link rel="shortcut icon" type="image/x-icon" href="{{static_url("base/images/favicon.ico") }}">
9 9 <meta http-equiv="X-UA-Compatible" content="chrome=1">
10 10 <link rel="stylesheet" href="{{static_url("components/jquery-ui/themes/smoothness/jquery-ui.min.css") }}" type="text/css" />
11 11 <meta name="viewport" content="width=device-width, initial-scale=1.0">
12 12
13 13 {% block stylesheet %}
14 14 <link rel="stylesheet" href="{{ static_url("style/style.min.css") }}" type="text/css"/>
15 15 {% endblock %}
16 16 <link rel="stylesheet" href="{{ static_url("custom/custom.css") }}" type="text/css" />
17 17 <script src="{{static_url("components/requirejs/require.js") }}" type="text/javascript" charset="utf-8"></script>
18 18 <script>
19 19 require.config({
20 20 baseUrl: '{{static_url("", include_version=False)}}',
21 21 paths: {
22 22 nbextensions : '{{ base_url }}nbextensions',
23 23 underscore : 'components/underscore/underscore-min',
24 24 backbone : 'components/backbone/backbone-min',
25 25 jquery: 'components/jquery/jquery.min',
26 bootstraptour: 'components/bootstrap-tour/build/js/bootstrap-tour.min',
26 27 },
27 28 shim: {
28 29 underscore: {
29 30 exports: '_'
30 31 },
31 32 backbone: {
32 33 deps: ["underscore", "jquery"],
33 34 exports: "Backbone"
35 },
36 bootstraptour: {
37 exports: "Tour"
34 38 }
35 39 }
36 40 });
37 41 </script>
38 42
39 43 {% block meta %}
40 44 {% endblock %}
41 45
42 46 </head>
43 47
44 48 <body {% block params %}{% endblock %}>
45 49
46 50 <noscript>
47 51 <div id='noscript'>
48 52 IPython Notebook requires JavaScript.<br>
49 53 Please enable it to proceed.
50 54 </div>
51 55 </noscript>
52 56
53 57 <div id="header" class="navbar navbar-static-top">
54 58 <div class="container">
55 59 <div id="ipython_notebook" class="nav navbar-brand pull-left"><a href="{{base_url}}tree/{{notebook_path}}" alt='dashboard'><img src='{{static_url("base/images/ipynblogo.png") }}' alt='IPython Notebook'/></a></div>
56 60
57 61 {% block login_widget %}
58 62
59 63 <span id="login_widget">
60 64 {% if logged_in %}
61 65 <button id="logout">Logout</button>
62 66 {% elif login_available and not logged_in %}
63 67 <button id="login">Login</button>
64 68 {% endif %}
65 69 </span>
66 70
67 71 {% endblock %}
68 72
69 73 {% block header %}
70 74 {% endblock %}
71 75 </div>
72 76 </div>
73 77
74 78 <div id="site">
75 79 {% block site %}
76 80 {% endblock %}
77 81 </div>
78 82
79 83 {% block script %}
80 84 <script src="{{static_url("base/js/pagemain.js") }}" type="text/javascript" charset="utf-8"></script>
81 85 {% endblock %}
82 86
83 87 <script src="{{static_url("custom/custom.js") }}" type="text/javascript" charset="utf-8"></script>
84 88
85 89 </body>
86 90
87 91 </html>
@@ -1,536 +1,549
1 1 //
2 2 // Utility functions for the HTML notebook's CasperJS tests.
3 3 //
4 4 casper.get_notebook_server = function () {
5 5 // Get the URL of a notebook server on which to run tests.
6 6 port = casper.cli.get("port");
7 7 port = (typeof port === 'undefined') ? '8888' : port;
8 8 return 'http://127.0.0.1:' + port;
9 9 };
10 10
11 11 casper.open_new_notebook = function () {
12 12 // Create and open a new notebook.
13 13 var baseUrl = this.get_notebook_server();
14 14 this.start(baseUrl);
15 this.waitFor(this.page_loaded);
15 16 this.thenClick('button#new_notebook');
16 17 this.waitForPopup('');
17 18
18 19 this.withPopup('', function () {this.waitForSelector('.CodeMirror-code');});
19 20 this.then(function () {
20 21 this.open(this.popups[0].url);
21 22 });
23 this.waitFor(this.page_loaded);
22 24
23 25 // Make sure the kernel has started
24 this.waitFor( this.kernel_running );
26 this.waitFor(this.kernel_running);
25 27 // track the IPython busy/idle state
26 28 this.thenEvaluate(function () {
29 IPython._status = 'idle';
27 30 $([IPython.events]).on('status_idle.Kernel',function () {
28 31 IPython._status = 'idle';
29 32 });
30 33 $([IPython.events]).on('status_busy.Kernel',function () {
31 34 IPython._status = 'busy';
32 35 });
33 36 });
34 37
35 38 // Because of the asynchronous nature of SlimerJS (Gecko), we need to make
36 39 // sure the notebook has actually been loaded into the IPython namespace
37 40 // before running any tests.
38 41 this.waitFor(function() {
39 42 return this.evaluate(function () {
40 43 return IPython.notebook;
41 44 });
42 45 });
43 46 };
44 47
45 casper.kernel_running = function kernel_running() {
48 casper.page_loaded = function() {
46 49 // Return whether or not the kernel is running.
47 return this.evaluate(function kernel_running() {
50 return this.evaluate(function() {
51 return IPython !== undefined &&
52 IPython.page !== undefined &&
53 IPython.events !== undefined;
54 });
55 };
56
57 casper.kernel_running = function() {
58 // Return whether or not the kernel is running.
59 return this.evaluate(function() {
48 60 return IPython.notebook.kernel.running;
49 61 });
50 62 };
51 63
52 64 casper.shutdown_current_kernel = function () {
53 65 // Shut down the current notebook's kernel.
54 66 this.thenEvaluate(function() {
55 67 IPython.notebook.kernel.kill();
56 68 });
57 69 // We close the page right after this so we need to give it time to complete.
58 70 this.wait(1000);
59 71 };
60 72
61 73 casper.delete_current_notebook = function () {
62 74 // Delete created notebook.
63 75
64 76 // For some unknown reason, this doesn't work?!?
65 77 this.thenEvaluate(function() {
66 78 IPython.notebook.delete();
67 79 });
68 80 };
69 81
70 82 casper.wait_for_busy = function () {
71 83 // Waits for the notebook to enter a busy state.
72 84 this.waitFor(function () {
73 85 return this.evaluate(function () {
74 86 return IPython._status == 'busy';
75 87 });
76 88 });
77 89 };
78 90
79 91 casper.wait_for_idle = function () {
80 92 // Waits for the notebook to idle.
81 93 this.waitFor(function () {
82 94 return this.evaluate(function () {
83 95 return IPython._status == 'idle';
84 96 });
85 97 });
86 98 };
87 99
88 100 casper.wait_for_output = function (cell_num, out_num) {
89 101 // wait for the nth output in a given cell
90 102 this.wait_for_idle();
91 103 out_num = out_num || 0;
92 104 this.then(function() {
93 105 this.waitFor(function (c, o) {
94 106 return this.evaluate(function get_output(c, o) {
95 107 var cell = IPython.notebook.get_cell(c);
96 108 return cell.output_area.outputs.length > o;
97 109 },
98 110 // pass parameter from the test suite js to the browser code js
99 111 {c : cell_num, o : out_num});
100 112 });
101 113 },
102 114 function then() { },
103 115 function timeout() {
104 116 this.echo("wait_for_output timed out!");
105 117 });
106 118 };
107 119
108 120 casper.wait_for_widget = function (widget_info) {
109 121 // wait for a widget msg que to reach 0
110 122 //
111 123 // Parameters
112 124 // ----------
113 125 // widget_info : object
114 126 // Object which contains info related to the widget. The model_id property
115 127 // is used to identify the widget.
116 128 this.waitFor(function () {
117 129 var pending = this.evaluate(function (m) {
118 130 return IPython.notebook.kernel.widget_manager.get_model(m).pending_msgs;
119 131 }, {m: widget_info.model_id});
120 132
121 133 if (pending === 0) {
122 134 return true;
123 135 } else {
124 136 return false;
125 137 }
126 138 });
127 139 };
128 140
129 141 casper.get_output_cell = function (cell_num, out_num) {
130 142 // return an output of a given cell
131 143 out_num = out_num || 0;
132 144 var result = casper.evaluate(function (c, o) {
133 145 var cell = IPython.notebook.get_cell(c);
134 146 return cell.output_area.outputs[o];
135 147 },
136 148 {c : cell_num, o : out_num});
137 149 if (!result) {
138 150 var num_outputs = casper.evaluate(function (c) {
139 151 var cell = IPython.notebook.get_cell(c);
140 152 return cell.output_area.outputs.length;
141 153 },
142 154 {c : cell_num});
143 155 this.test.assertTrue(false,
144 156 "Cell " + cell_num + " has no output #" + out_num + " (" + num_outputs + " total)"
145 157 );
146 158 } else {
147 159 return result;
148 160 }
149 161 };
150 162
151 163 casper.get_cells_length = function () {
152 164 // return the number of cells in the notebook
153 165 var result = casper.evaluate(function () {
154 166 return IPython.notebook.get_cells().length;
155 167 });
156 168 return result;
157 169 };
158 170
159 171 casper.set_cell_text = function(index, text){
160 172 // Set the text content of a cell.
161 173 this.evaluate(function (index, text) {
162 174 var cell = IPython.notebook.get_cell(index);
163 175 cell.set_text(text);
164 176 }, index, text);
165 177 };
166 178
167 179 casper.get_cell_text = function(index){
168 180 // Get the text content of a cell.
169 181 return this.evaluate(function (index) {
170 182 var cell = IPython.notebook.get_cell(index);
171 183 return cell.get_text();
172 184 }, index);
173 185 };
174 186
175 187 casper.insert_cell_at_bottom = function(cell_type){
176 188 // Inserts a cell at the bottom of the notebook
177 189 // Returns the new cell's index.
178 190 return this.evaluate(function (cell_type) {
179 191 var cell = IPython.notebook.insert_cell_at_bottom(cell_type);
180 192 return IPython.notebook.find_cell_index(cell);
181 193 }, cell_type);
182 194 };
183 195
184 196 casper.append_cell = function(text, cell_type) {
185 197 // Insert a cell at the bottom of the notebook and set the cells text.
186 198 // Returns the new cell's index.
187 199 var index = this.insert_cell_at_bottom(cell_type);
188 200 if (text !== undefined) {
189 201 this.set_cell_text(index, text);
190 202 }
191 203 return index;
192 204 };
193 205
194 206 casper.execute_cell = function(index){
195 207 // Asynchronously executes a cell by index.
196 208 // Returns the cell's index.
197 209 var that = this;
198 210 this.then(function(){
199 211 that.evaluate(function (index) {
200 212 var cell = IPython.notebook.get_cell(index);
201 213 cell.execute();
202 214 }, index);
203 215 });
204 216 return index;
205 217 };
206 218
207 219 casper.execute_cell_then = function(index, then_callback) {
208 220 // Synchronously executes a cell by index.
209 221 // Optionally accepts a then_callback parameter. then_callback will get called
210 222 // when the cell has finished executing.
211 223 // Returns the cell's index.
212 224 var return_val = this.execute_cell(index);
213 225
214 226 this.wait_for_idle();
215 227
216 228 var that = this;
217 229 this.then(function(){
218 230 if (then_callback!==undefined) {
219 231 then_callback.apply(that, [index]);
220 232 }
221 233 });
222 234
223 235 return return_val;
224 236 };
225 237
226 238 casper.cell_element_exists = function(index, selector){
227 239 // Utility function that allows us to easily check if an element exists
228 240 // within a cell. Uses JQuery selector to look for the element.
229 241 return casper.evaluate(function (index, selector) {
230 242 var $cell = IPython.notebook.get_cell(index).element;
231 243 return $cell.find(selector).length > 0;
232 244 }, index, selector);
233 245 };
234 246
235 247 casper.cell_element_function = function(index, selector, function_name, function_args){
236 248 // Utility function that allows us to execute a jQuery function on an
237 249 // element within a cell.
238 250 return casper.evaluate(function (index, selector, function_name, function_args) {
239 251 var $cell = IPython.notebook.get_cell(index).element;
240 252 var $el = $cell.find(selector);
241 253 return $el[function_name].apply($el, function_args);
242 254 }, index, selector, function_name, function_args);
243 255 };
244 256
245 257 casper.validate_notebook_state = function(message, mode, cell_index) {
246 258 // Validate the entire dual mode state of the notebook. Make sure no more than
247 259 // one cell is selected, focused, in edit mode, etc...
248 260
249 261 // General tests.
250 262 this.test.assertEquals(this.get_keyboard_mode(), this.get_notebook_mode(),
251 263 message + '; keyboard and notebook modes match');
252 264 // Is the selected cell the only cell that is selected?
253 265 if (cell_index!==undefined) {
254 266 this.test.assert(this.is_only_cell_selected(cell_index),
255 267 message + '; cell ' + cell_index + ' is the only cell selected');
256 268 }
257 269
258 270 // Mode specific tests.
259 271 if (mode==='command') {
260 272 // Are the notebook and keyboard manager in command mode?
261 273 this.test.assertEquals(this.get_keyboard_mode(), 'command',
262 274 message + '; in command mode');
263 275 // Make sure there isn't a single cell in edit mode.
264 276 this.test.assert(this.is_only_cell_edit(null),
265 277 message + '; all cells in command mode');
266 278 this.test.assert(this.is_cell_editor_focused(null),
267 279 message + '; no cell editors are focused while in command mode');
268 280
269 281 } else if (mode==='edit') {
270 282 // Are the notebook and keyboard manager in edit mode?
271 283 this.test.assertEquals(this.get_keyboard_mode(), 'edit',
272 284 message + '; in edit mode');
273 285 if (cell_index!==undefined) {
274 286 // Is the specified cell the only cell in edit mode?
275 287 this.test.assert(this.is_only_cell_edit(cell_index),
276 288 message + '; cell ' + cell_index + ' is the only cell in edit mode');
277 289 // Is the specified cell the only cell with a focused code mirror?
278 290 this.test.assert(this.is_cell_editor_focused(cell_index),
279 291 message + '; cell ' + cell_index + '\'s editor is appropriately focused');
280 292 }
281 293
282 294 } else {
283 295 this.test.assert(false, message + '; ' + mode + ' is an unknown mode');
284 296 }
285 297 };
286 298
287 299 casper.select_cell = function(index) {
288 300 // Select a cell in the notebook.
289 301 this.evaluate(function (i) {
290 302 IPython.notebook.select(i);
291 303 }, {i: index});
292 304 };
293 305
294 306 casper.click_cell_editor = function(index) {
295 307 // Emulate a click on a cell's editor.
296 308
297 309 // Code Mirror does not play nicely with emulated brower events.
298 310 // Instead of trying to emulate a click, here we run code similar to
299 311 // the code used in Code Mirror that handles the mousedown event on a
300 312 // region of codemirror that the user can focus.
301 313 this.evaluate(function (i) {
302 314 var cm = IPython.notebook.get_cell(i).code_mirror;
303 315 if (cm.options.readOnly != "nocursor" && (document.activeElement != cm.display.input))
304 316 cm.display.input.focus();
305 317 }, {i: index});
306 318 };
307 319
308 320 casper.set_cell_editor_cursor = function(index, line_index, char_index) {
309 321 // Set the Code Mirror instance cursor's location.
310 322 this.evaluate(function (i, l, c) {
311 323 IPython.notebook.get_cell(i).code_mirror.setCursor(l, c);
312 324 }, {i: index, l: line_index, c: char_index});
313 325 };
314 326
315 327 casper.focus_notebook = function() {
316 328 // Focus the notebook div.
317 329 this.evaluate(function (){
318 330 $('#notebook').focus();
319 331 }, {});
320 332 };
321 333
322 334 casper.trigger_keydown = function() {
323 335 // Emulate a keydown in the notebook.
324 336 for (var i = 0; i < arguments.length; i++) {
325 337 this.evaluate(function (k) {
326 338 var element = $(document);
327 339 var event = IPython.keyboard.shortcut_to_event(k, 'keydown');
328 340 element.trigger(event);
329 341 }, {k: arguments[i]});
330 342 }
331 343 };
332 344
333 345 casper.get_keyboard_mode = function() {
334 346 // Get the mode of the keyboard manager.
335 347 return this.evaluate(function() {
336 348 return IPython.keyboard_manager.mode;
337 349 }, {});
338 350 };
339 351
340 352 casper.get_notebook_mode = function() {
341 353 // Get the mode of the notebook.
342 354 return this.evaluate(function() {
343 355 return IPython.notebook.mode;
344 356 }, {});
345 357 };
346 358
347 359 casper.get_cell = function(index) {
348 360 // Get a single cell.
349 361 //
350 362 // Note: Handles to DOM elements stored in the cell will be useless once in
351 363 // CasperJS context.
352 364 return this.evaluate(function(i) {
353 365 var cell = IPython.notebook.get_cell(i);
354 366 if (cell) {
355 367 return cell;
356 368 }
357 369 return null;
358 370 }, {i : index});
359 371 };
360 372
361 373 casper.is_cell_editor_focused = function(index) {
362 374 // Make sure a cell's editor is the only editor focused on the page.
363 375 return this.evaluate(function(i) {
364 376 var focused_textarea = $('#notebook .CodeMirror-focused textarea');
365 377 if (focused_textarea.length > 1) { throw 'More than one Code Mirror editor is focused at once!'; }
366 378 if (i === null) {
367 379 return focused_textarea.length === 0;
368 380 } else {
369 381 var cell = IPython.notebook.get_cell(i);
370 382 if (cell) {
371 383 return cell.code_mirror.getInputField() == focused_textarea[0];
372 384 }
373 385 }
374 386 return false;
375 387 }, {i : index});
376 388 };
377 389
378 390 casper.is_only_cell_selected = function(index) {
379 391 // Check if a cell is the only cell selected.
380 392 // Pass null as the index to check if no cells are selected.
381 393 return this.is_only_cell_on(index, 'selected', 'unselected');
382 394 };
383 395
384 396 casper.is_only_cell_edit = function(index) {
385 397 // Check if a cell is the only cell in edit mode.
386 398 // Pass null as the index to check if all of the cells are in command mode.
387 399 return this.is_only_cell_on(index, 'edit_mode', 'command_mode');
388 400 };
389 401
390 402 casper.is_only_cell_on = function(i, on_class, off_class) {
391 403 // Check if a cell is the only cell with the `on_class` DOM class applied to it.
392 404 // All of the other cells are checked for the `off_class` DOM class.
393 405 // Pass null as the index to check if all of the cells have the `off_class`.
394 406 var cells_length = this.get_cells_length();
395 407 for (var j = 0; j < cells_length; j++) {
396 408 if (j === i) {
397 409 if (this.cell_has_class(j, off_class) || !this.cell_has_class(j, on_class)) {
398 410 return false;
399 411 }
400 412 } else {
401 413 if (!this.cell_has_class(j, off_class) || this.cell_has_class(j, on_class)) {
402 414 return false;
403 415 }
404 416 }
405 417 }
406 418 return true;
407 419 };
408 420
409 421 casper.cell_has_class = function(index, classes) {
410 422 // Check if a cell has a class.
411 423 return this.evaluate(function(i, c) {
412 424 var cell = IPython.notebook.get_cell(i);
413 425 if (cell) {
414 426 return cell.element.hasClass(c);
415 427 }
416 428 return false;
417 429 }, {i : index, c: classes});
418 430 };
419 431
420 432 casper.is_cell_rendered = function (index) {
421 433 return this.evaluate(function(i) {
422 434 return !!IPython.notebook.get_cell(i).rendered;
423 435 }, {i:index});
424 436 };
425 437
426 438 casper.assert_colors_equal = function (hex_color, local_color, msg) {
427 439 // Tests to see if two colors are equal.
428 440 //
429 441 // Parameters
430 442 // hex_color: string
431 443 // Hexadecimal color code, with or without preceeding hash character.
432 444 // local_color: string
433 445 // Local color representation. Can either be hexadecimal (default for
434 446 // phantom) or rgb (default for slimer).
435 447
436 448 // Remove parentheses, hashes, semi-colons, and space characters.
437 449 hex_color = hex_color.replace(/[\(\); #]/, '');
438 450 local_color = local_color.replace(/[\(\); #]/, '');
439 451
440 452 // If the local color is rgb, clean it up and replace
441 453 if (local_color.substr(0,3).toLowerCase() == 'rgb') {
442 454 components = local_color.substr(3).split(',');
443 455 local_color = '';
444 456 for (var i = 0; i < components.length; i++) {
445 457 var part = parseInt(components[i]).toString(16);
446 458 while (part.length < 2) part = '0' + part;
447 459 local_color += part;
448 460 }
449 461 }
450 462
451 463 this.test.assertEquals(hex_color.toUpperCase(), local_color.toUpperCase(), msg);
452 464 };
453 465
454 466 casper.notebook_test = function(test) {
455 467 // Wrap a notebook test to reduce boilerplate.
456 468 this.open_new_notebook();
457 469
458 470 // Echo whether or not we are running this test using SlimerJS
459 471 if (this.evaluate(function(){
460 472 return typeof InstallTrigger !== 'undefined'; // Firefox 1.0+
461 473 })) {
462 474 console.log('This test is running in SlimerJS.');
463 475 this.slimerjs = true;
464 476 }
465 477
466 478 // Make sure to remove the onbeforeunload callback. This callback is
467 479 // responsible for the "Are you sure you want to quit?" type messages.
468 480 // PhantomJS ignores these prompts, SlimerJS does not which causes hangs.
469 481 this.then(function(){
470 482 this.evaluate(function(){
471 483 window.onbeforeunload = function(){};
472 484 });
473 485 });
474 486
475 487 this.then(test);
476 488
477 489 // Kill the kernel and delete the notebook.
478 490 this.shutdown_current_kernel();
479 491 // This is still broken but shouldn't be a problem for now.
480 492 // this.delete_current_notebook();
481 493
482 494 // This is required to clean up the page we just finished with. If we don't call this
483 495 // casperjs will leak file descriptors of all the open WebSockets in that page. We
484 496 // have to set this.page=null so that next time casper.start runs, it will create a
485 497 // new page from scratch.
486 498 this.then(function () {
487 499 this.page.close();
488 500 this.page = null;
489 501 });
490 502
491 503 // Run the browser automation.
492 504 this.run(function() {
493 505 this.test.done();
494 506 });
495 507 };
496 508
497 509 casper.wait_for_dashboard = function () {
498 510 // Wait for the dashboard list to load.
499 511 casper.waitForSelector('.list_item');
500 512 };
501 513
502 514 casper.open_dashboard = function () {
503 515 // Start casper by opening the dashboard page.
504 516 var baseUrl = this.get_notebook_server();
505 517 this.start(baseUrl);
518 this.waitFor(this.page_loaded);
506 519 this.wait_for_dashboard();
507 520 };
508 521
509 522 casper.dashboard_test = function (test) {
510 523 // Open the dashboard page and run a test.
511 524 this.open_dashboard();
512 525 this.then(test);
513 526
514 527 this.then(function () {
515 528 this.page.close();
516 529 this.page = null;
517 530 });
518 531
519 532 // Run the browser automation.
520 533 this.run(function() {
521 534 this.test.done();
522 535 });
523 536 };
524 537
525 538 casper.options.waitTimeout=10000;
526 539 casper.on('waitFor.timeout', function onWaitForTimeout(timeout) {
527 540 this.echo("Timeout for " + casper.get_notebook_server());
528 541 this.echo("Is the notebook server running?");
529 542 });
530 543
531 544 casper.print_log = function () {
532 545 // Pass `console.log` calls from page JS to casper.
533 546 this.on('remote.message', function(msg) {
534 547 this.echo('Remote message caught: ' + msg);
535 548 });
536 549 };
General Comments 0
You need to be logged in to leave comments. Login now