##// END OF EJS Templates
Merge pull request #7578 from Carreau/moreerrors...
Min RK -
r20187:f8315efc merge
parent child Browse files
Show More
@@ -1,196 +1,200 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 define([
4 define([
5 'jquery',
5 'jquery',
6 'base/js/utils',
6 'base/js/utils',
7 'codemirror/lib/codemirror',
7 'codemirror/lib/codemirror',
8 'codemirror/mode/meta',
8 'codemirror/mode/meta',
9 'codemirror/addon/comment/comment',
9 'codemirror/addon/comment/comment',
10 'codemirror/addon/dialog/dialog',
10 'codemirror/addon/dialog/dialog',
11 'codemirror/addon/edit/closebrackets',
11 'codemirror/addon/edit/closebrackets',
12 'codemirror/addon/edit/matchbrackets',
12 'codemirror/addon/edit/matchbrackets',
13 'codemirror/addon/search/searchcursor',
13 'codemirror/addon/search/searchcursor',
14 'codemirror/addon/search/search',
14 'codemirror/addon/search/search',
15 'codemirror/keymap/emacs',
15 'codemirror/keymap/emacs',
16 'codemirror/keymap/sublime',
16 'codemirror/keymap/sublime',
17 'codemirror/keymap/vim',
17 'codemirror/keymap/vim',
18 ],
18 ],
19 function($,
19 function($,
20 utils,
20 utils,
21 CodeMirror
21 CodeMirror
22 ) {
22 ) {
23 "use strict";
23 "use strict";
24
24
25 var Editor = function(selector, options) {
25 var Editor = function(selector, options) {
26 var that = this;
26 var that = this;
27 this.selector = selector;
27 this.selector = selector;
28 this.contents = options.contents;
28 this.contents = options.contents;
29 this.events = options.events;
29 this.events = options.events;
30 this.base_url = options.base_url;
30 this.base_url = options.base_url;
31 this.file_path = options.file_path;
31 this.file_path = options.file_path;
32 this.config = options.config;
32 this.config = options.config;
33 this.codemirror = new CodeMirror($(this.selector)[0]);
33 this.codemirror = new CodeMirror($(this.selector)[0]);
34 this.generation = -1;
34 this.generation = -1;
35
35
36 // It appears we have to set commands on the CodeMirror class, not the
36 // It appears we have to set commands on the CodeMirror class, not the
37 // instance. I'd like to be wrong, but since there should only be one CM
37 // instance. I'd like to be wrong, but since there should only be one CM
38 // instance on the page, this is good enough for now.
38 // instance on the page, this is good enough for now.
39 CodeMirror.commands.save = $.proxy(this.save, this);
39 CodeMirror.commands.save = $.proxy(this.save, this);
40
40
41 this.save_enabled = false;
41 this.save_enabled = false;
42
42
43 this.config.loaded.then(function () {
43 this.config.loaded.then(function () {
44 // load codemirror config
44 // load codemirror config
45 var cfg = that.config.data.Editor || {};
45 var cfg = that.config.data.Editor || {};
46 var cmopts = $.extend(true, {}, // true = recursive copy
46 var cmopts = $.extend(true, {}, // true = recursive copy
47 Editor.default_codemirror_options,
47 Editor.default_codemirror_options,
48 cfg.codemirror_options || {}
48 cfg.codemirror_options || {}
49 );
49 );
50 that._set_codemirror_options(cmopts);
50 that._set_codemirror_options(cmopts);
51 that.events.trigger('config_changed.Editor', {config: that.config});
51 that.events.trigger('config_changed.Editor', {config: that.config});
52 });
52 });
53 };
53 };
54
54
55 // default CodeMirror options
55 // default CodeMirror options
56 Editor.default_codemirror_options = {
56 Editor.default_codemirror_options = {
57 extraKeys: {
57 extraKeys: {
58 "Tab" : "indentMore",
58 "Tab" : "indentMore",
59 },
59 },
60 indentUnit: 4,
60 indentUnit: 4,
61 theme: "ipython",
61 theme: "ipython",
62 lineNumbers: true,
62 lineNumbers: true,
63 lineWrapping: true,
63 lineWrapping: true,
64 };
64 };
65
65
66 Editor.prototype.load = function() {
66 Editor.prototype.load = function() {
67 /** load the file */
67 /** load the file */
68 var that = this;
68 var that = this;
69 var cm = this.codemirror;
69 var cm = this.codemirror;
70 return this.contents.get(this.file_path, {type: 'file', format: 'text'})
70 return this.contents.get(this.file_path, {type: 'file', format: 'text'})
71 .then(function(model) {
71 .then(function(model) {
72 cm.setValue(model.content);
72 cm.setValue(model.content);
73
73
74 // Setting the file's initial value creates a history entry,
74 // Setting the file's initial value creates a history entry,
75 // which we don't want.
75 // which we don't want.
76 cm.clearHistory();
76 cm.clearHistory();
77 that._set_mode_for_model(model);
77 that._set_mode_for_model(model);
78 that.save_enabled = true;
78 that.save_enabled = true;
79 that.generation = cm.changeGeneration();
79 that.generation = cm.changeGeneration();
80 that.events.trigger("file_loaded.Editor", model);
80 that.events.trigger("file_loaded.Editor", model);
81 },
81 }).catch(
82 function(error) {
82 function(error) {
83 that.events.trigger("file_load_failed.Editor", error);
83 that.events.trigger("file_load_failed.Editor", error);
84 if (error.xhr.responseJSON.reason === 'bad format') {
84 if (((error.xhr||{}).responseJSON||{}).reason === 'bad format') {
85 window.location = utils.url_path_join(
85 window.location = utils.url_path_join(
86 that.base_url,
86 that.base_url,
87 'files',
87 'files',
88 that.file_path
88 that.file_path
89 );
89 );
90 } else {
91 console.warn('Error while loading: the error was:')
92 console.warn(error)
90 }
93 }
91 cm.setValue("Error! " + error.message +
94 cm.setValue("Error! " + error.message +
92 "\nSaving disabled.");
95 "\nSaving disabled.\nSee Console for more details.");
96 cm.setOption('readOnly','nocursor')
93 that.save_enabled = false;
97 that.save_enabled = false;
94 }
98 }
95 );
99 );
96 };
100 };
97
101
98 Editor.prototype._set_mode_for_model = function (model) {
102 Editor.prototype._set_mode_for_model = function (model) {
99 /** Set the CodeMirror mode based on the file model */
103 /** Set the CodeMirror mode based on the file model */
100
104
101 // Find and load the highlighting mode,
105 // Find and load the highlighting mode,
102 // first by mime-type, then by file extension
106 // first by mime-type, then by file extension
103
107
104 var modeinfo;
108 var modeinfo;
105 // mimetype is unset on file rename
109 // mimetype is unset on file rename
106 if (model.mimetype) {
110 if (model.mimetype) {
107 modeinfo = CodeMirror.findModeByMIME(model.mimetype);
111 modeinfo = CodeMirror.findModeByMIME(model.mimetype);
108 }
112 }
109 if (!modeinfo || modeinfo.mode === "null") {
113 if (!modeinfo || modeinfo.mode === "null") {
110 // find by mime failed, use find by ext
114 // find by mime failed, use find by ext
111 var ext_idx = model.name.lastIndexOf('.');
115 var ext_idx = model.name.lastIndexOf('.');
112
116
113 if (ext_idx > 0) {
117 if (ext_idx > 0) {
114 // CodeMirror.findModeByExtension wants extension without '.'
118 // CodeMirror.findModeByExtension wants extension without '.'
115 modeinfo = CodeMirror.findModeByExtension(model.name.slice(ext_idx + 1));
119 modeinfo = CodeMirror.findModeByExtension(model.name.slice(ext_idx + 1));
116 }
120 }
117 }
121 }
118 if (modeinfo) {
122 if (modeinfo) {
119 this.set_codemirror_mode(modeinfo);
123 this.set_codemirror_mode(modeinfo);
120 }
124 }
121 };
125 };
122
126
123 Editor.prototype.set_codemirror_mode = function (modeinfo) {
127 Editor.prototype.set_codemirror_mode = function (modeinfo) {
124 /** set the codemirror mode from a modeinfo struct */
128 /** set the codemirror mode from a modeinfo struct */
125 var that = this;
129 var that = this;
126 utils.requireCodeMirrorMode(modeinfo, function () {
130 utils.requireCodeMirrorMode(modeinfo, function () {
127 that.codemirror.setOption('mode', modeinfo.mode);
131 that.codemirror.setOption('mode', modeinfo.mode);
128 that.events.trigger("mode_changed.Editor", modeinfo);
132 that.events.trigger("mode_changed.Editor", modeinfo);
129 });
133 });
130 };
134 };
131
135
132 Editor.prototype.get_filename = function () {
136 Editor.prototype.get_filename = function () {
133 return utils.url_path_split(this.file_path)[1];
137 return utils.url_path_split(this.file_path)[1];
134 };
138 };
135
139
136 Editor.prototype.rename = function (new_name) {
140 Editor.prototype.rename = function (new_name) {
137 /** rename the file */
141 /** rename the file */
138 var that = this;
142 var that = this;
139 var parent = utils.url_path_split(this.file_path)[0];
143 var parent = utils.url_path_split(this.file_path)[0];
140 var new_path = utils.url_path_join(parent, new_name);
144 var new_path = utils.url_path_join(parent, new_name);
141 return this.contents.rename(this.file_path, new_path).then(
145 return this.contents.rename(this.file_path, new_path).then(
142 function (model) {
146 function (model) {
143 that.file_path = model.path;
147 that.file_path = model.path;
144 that.events.trigger('file_renamed.Editor', model);
148 that.events.trigger('file_renamed.Editor', model);
145 that._set_mode_for_model(model);
149 that._set_mode_for_model(model);
146 }
150 }
147 );
151 );
148 };
152 };
149
153
150 Editor.prototype.save = function () {
154 Editor.prototype.save = function () {
151 /** save the file */
155 /** save the file */
152 if (!this.save_enabled) {
156 if (!this.save_enabled) {
153 console.log("Not saving, save disabled");
157 console.log("Not saving, save disabled");
154 return;
158 return;
155 }
159 }
156 var model = {
160 var model = {
157 path: this.file_path,
161 path: this.file_path,
158 type: 'file',
162 type: 'file',
159 format: 'text',
163 format: 'text',
160 content: this.codemirror.getValue(),
164 content: this.codemirror.getValue(),
161 };
165 };
162 var that = this;
166 var that = this;
163 // record change generation for isClean
167 // record change generation for isClean
164 this.generation = this.codemirror.changeGeneration();
168 this.generation = this.codemirror.changeGeneration();
165 that.events.trigger("file_saving.Editor");
169 that.events.trigger("file_saving.Editor");
166 return this.contents.save(this.file_path, model).then(function(data) {
170 return this.contents.save(this.file_path, model).then(function(data) {
167 that.events.trigger("file_saved.Editor", data);
171 that.events.trigger("file_saved.Editor", data);
168 });
172 });
169 };
173 };
170
174
171 Editor.prototype._set_codemirror_options = function (options) {
175 Editor.prototype._set_codemirror_options = function (options) {
172 // update codemirror options from a dict
176 // update codemirror options from a dict
173 var codemirror = this.codemirror;
177 var codemirror = this.codemirror;
174 $.map(options, function (value, opt) {
178 $.map(options, function (value, opt) {
175 if (value === null) {
179 if (value === null) {
176 value = CodeMirror.defaults[opt];
180 value = CodeMirror.defaults[opt];
177 }
181 }
178 codemirror.setOption(opt, value);
182 codemirror.setOption(opt, value);
179 });
183 });
180 };
184 };
181
185
182 Editor.prototype.update_codemirror_options = function (options) {
186 Editor.prototype.update_codemirror_options = function (options) {
183 /** update codemirror options locally and save changes in config */
187 /** update codemirror options locally and save changes in config */
184 var that = this;
188 var that = this;
185 this._set_codemirror_options(options);
189 this._set_codemirror_options(options);
186 return this.config.update({
190 return this.config.update({
187 Editor: {
191 Editor: {
188 codemirror_options: options
192 codemirror_options: options
189 }
193 }
190 }).then(
194 }).then(
191 that.events.trigger('config_changed.Editor', {config: that.config})
195 that.events.trigger('config_changed.Editor', {config: that.config})
192 );
196 );
193 };
197 };
194
198
195 return {Editor: Editor};
199 return {Editor: Editor};
196 });
200 });
@@ -1,2438 +1,2440 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 /**
4 /**
5 * @module notebook
5 * @module notebook
6 */
6 */
7 define([
7 define([
8 'base/js/namespace',
8 'base/js/namespace',
9 'jquery',
9 'jquery',
10 'base/js/utils',
10 'base/js/utils',
11 'base/js/dialog',
11 'base/js/dialog',
12 'notebook/js/cell',
12 'notebook/js/cell',
13 'notebook/js/textcell',
13 'notebook/js/textcell',
14 'notebook/js/codecell',
14 'notebook/js/codecell',
15 'services/config',
15 'services/config',
16 'services/sessions/session',
16 'services/sessions/session',
17 'notebook/js/celltoolbar',
17 'notebook/js/celltoolbar',
18 'components/marked/lib/marked',
18 'components/marked/lib/marked',
19 'codemirror/lib/codemirror',
19 'codemirror/lib/codemirror',
20 'codemirror/addon/runmode/runmode',
20 'codemirror/addon/runmode/runmode',
21 'notebook/js/mathjaxutils',
21 'notebook/js/mathjaxutils',
22 'base/js/keyboard',
22 'base/js/keyboard',
23 'notebook/js/tooltip',
23 'notebook/js/tooltip',
24 'notebook/js/celltoolbarpresets/default',
24 'notebook/js/celltoolbarpresets/default',
25 'notebook/js/celltoolbarpresets/rawcell',
25 'notebook/js/celltoolbarpresets/rawcell',
26 'notebook/js/celltoolbarpresets/slideshow',
26 'notebook/js/celltoolbarpresets/slideshow',
27 'notebook/js/scrollmanager'
27 'notebook/js/scrollmanager'
28 ], function (
28 ], function (
29 IPython,
29 IPython,
30 $,
30 $,
31 utils,
31 utils,
32 dialog,
32 dialog,
33 cellmod,
33 cellmod,
34 textcell,
34 textcell,
35 codecell,
35 codecell,
36 configmod,
36 configmod,
37 session,
37 session,
38 celltoolbar,
38 celltoolbar,
39 marked,
39 marked,
40 CodeMirror,
40 CodeMirror,
41 runMode,
41 runMode,
42 mathjaxutils,
42 mathjaxutils,
43 keyboard,
43 keyboard,
44 tooltip,
44 tooltip,
45 default_celltoolbar,
45 default_celltoolbar,
46 rawcell_celltoolbar,
46 rawcell_celltoolbar,
47 slideshow_celltoolbar,
47 slideshow_celltoolbar,
48 scrollmanager
48 scrollmanager
49 ) {
49 ) {
50 "use strict";
50 "use strict";
51
51
52 /**
52 /**
53 * Contains and manages cells.
53 * Contains and manages cells.
54 *
54 *
55 * @class Notebook
55 * @class Notebook
56 * @param {string} selector
56 * @param {string} selector
57 * @param {object} options - Dictionary of keyword arguments.
57 * @param {object} options - Dictionary of keyword arguments.
58 * @param {jQuery} options.events - selector of Events
58 * @param {jQuery} options.events - selector of Events
59 * @param {KeyboardManager} options.keyboard_manager
59 * @param {KeyboardManager} options.keyboard_manager
60 * @param {Contents} options.contents
60 * @param {Contents} options.contents
61 * @param {SaveWidget} options.save_widget
61 * @param {SaveWidget} options.save_widget
62 * @param {object} options.config
62 * @param {object} options.config
63 * @param {string} options.base_url
63 * @param {string} options.base_url
64 * @param {string} options.notebook_path
64 * @param {string} options.notebook_path
65 * @param {string} options.notebook_name
65 * @param {string} options.notebook_name
66 */
66 */
67 var Notebook = function (selector, options) {
67 var Notebook = function (selector, options) {
68 this.config = options.config;
68 this.config = options.config;
69 this.class_config = new configmod.ConfigWithDefaults(this.config,
69 this.class_config = new configmod.ConfigWithDefaults(this.config,
70 Notebook.options_default, 'Notebook');
70 Notebook.options_default, 'Notebook');
71 this.base_url = options.base_url;
71 this.base_url = options.base_url;
72 this.notebook_path = options.notebook_path;
72 this.notebook_path = options.notebook_path;
73 this.notebook_name = options.notebook_name;
73 this.notebook_name = options.notebook_name;
74 this.events = options.events;
74 this.events = options.events;
75 this.keyboard_manager = options.keyboard_manager;
75 this.keyboard_manager = options.keyboard_manager;
76 this.contents = options.contents;
76 this.contents = options.contents;
77 this.save_widget = options.save_widget;
77 this.save_widget = options.save_widget;
78 this.tooltip = new tooltip.Tooltip(this.events);
78 this.tooltip = new tooltip.Tooltip(this.events);
79 this.ws_url = options.ws_url;
79 this.ws_url = options.ws_url;
80 this._session_starting = false;
80 this._session_starting = false;
81
81
82 // Create default scroll manager.
82 // Create default scroll manager.
83 this.scroll_manager = new scrollmanager.ScrollManager(this);
83 this.scroll_manager = new scrollmanager.ScrollManager(this);
84
84
85 // TODO: This code smells (and the other `= this` line a couple lines down)
85 // TODO: This code smells (and the other `= this` line a couple lines down)
86 // We need a better way to deal with circular instance references.
86 // We need a better way to deal with circular instance references.
87 this.keyboard_manager.notebook = this;
87 this.keyboard_manager.notebook = this;
88 this.save_widget.notebook = this;
88 this.save_widget.notebook = this;
89
89
90 mathjaxutils.init();
90 mathjaxutils.init();
91
91
92 if (marked) {
92 if (marked) {
93 marked.setOptions({
93 marked.setOptions({
94 gfm : true,
94 gfm : true,
95 tables: true,
95 tables: true,
96 // FIXME: probably want central config for CodeMirror theme when we have js config
96 // FIXME: probably want central config for CodeMirror theme when we have js config
97 langPrefix: "cm-s-ipython language-",
97 langPrefix: "cm-s-ipython language-",
98 highlight: function(code, lang, callback) {
98 highlight: function(code, lang, callback) {
99 if (!lang) {
99 if (!lang) {
100 // no language, no highlight
100 // no language, no highlight
101 if (callback) {
101 if (callback) {
102 callback(null, code);
102 callback(null, code);
103 return;
103 return;
104 } else {
104 } else {
105 return code;
105 return code;
106 }
106 }
107 }
107 }
108 utils.requireCodeMirrorMode(lang, function (spec) {
108 utils.requireCodeMirrorMode(lang, function (spec) {
109 var el = document.createElement("div");
109 var el = document.createElement("div");
110 var mode = CodeMirror.getMode({}, spec);
110 var mode = CodeMirror.getMode({}, spec);
111 if (!mode) {
111 if (!mode) {
112 console.log("No CodeMirror mode: " + lang);
112 console.log("No CodeMirror mode: " + lang);
113 callback(null, code);
113 callback(null, code);
114 return;
114 return;
115 }
115 }
116 try {
116 try {
117 CodeMirror.runMode(code, spec, el);
117 CodeMirror.runMode(code, spec, el);
118 callback(null, el.innerHTML);
118 callback(null, el.innerHTML);
119 } catch (err) {
119 } catch (err) {
120 console.log("Failed to highlight " + lang + " code", err);
120 console.log("Failed to highlight " + lang + " code", err);
121 callback(err, code);
121 callback(err, code);
122 }
122 }
123 }, function (err) {
123 }, function (err) {
124 console.log("No CodeMirror mode: " + lang);
124 console.log("No CodeMirror mode: " + lang);
125 callback(err, code);
125 callback(err, code);
126 });
126 });
127 }
127 }
128 });
128 });
129 }
129 }
130
130
131 this.element = $(selector);
131 this.element = $(selector);
132 this.element.scroll();
132 this.element.scroll();
133 this.element.data("notebook", this);
133 this.element.data("notebook", this);
134 this.next_prompt_number = 1;
134 this.next_prompt_number = 1;
135 this.session = null;
135 this.session = null;
136 this.kernel = null;
136 this.kernel = null;
137 this.clipboard = null;
137 this.clipboard = null;
138 this.undelete_backup = null;
138 this.undelete_backup = null;
139 this.undelete_index = null;
139 this.undelete_index = null;
140 this.undelete_below = false;
140 this.undelete_below = false;
141 this.paste_enabled = false;
141 this.paste_enabled = false;
142 this.writable = false;
142 this.writable = false;
143 // It is important to start out in command mode to match the intial mode
143 // It is important to start out in command mode to match the intial mode
144 // of the KeyboardManager.
144 // of the KeyboardManager.
145 this.mode = 'command';
145 this.mode = 'command';
146 this.set_dirty(false);
146 this.set_dirty(false);
147 this.metadata = {};
147 this.metadata = {};
148 this._checkpoint_after_save = false;
148 this._checkpoint_after_save = false;
149 this.last_checkpoint = null;
149 this.last_checkpoint = null;
150 this.checkpoints = [];
150 this.checkpoints = [];
151 this.autosave_interval = 0;
151 this.autosave_interval = 0;
152 this.autosave_timer = null;
152 this.autosave_timer = null;
153 // autosave *at most* every two minutes
153 // autosave *at most* every two minutes
154 this.minimum_autosave_interval = 120000;
154 this.minimum_autosave_interval = 120000;
155 this.notebook_name_blacklist_re = /[\/\\:]/;
155 this.notebook_name_blacklist_re = /[\/\\:]/;
156 this.nbformat = 4; // Increment this when changing the nbformat
156 this.nbformat = 4; // Increment this when changing the nbformat
157 this.nbformat_minor = this.current_nbformat_minor = 0; // Increment this when changing the nbformat
157 this.nbformat_minor = this.current_nbformat_minor = 0; // Increment this when changing the nbformat
158 this.codemirror_mode = 'ipython';
158 this.codemirror_mode = 'ipython';
159 this.create_elements();
159 this.create_elements();
160 this.bind_events();
160 this.bind_events();
161 this.kernel_selector = null;
161 this.kernel_selector = null;
162 this.dirty = null;
162 this.dirty = null;
163 this.trusted = null;
163 this.trusted = null;
164 this._fully_loaded = false;
164 this._fully_loaded = false;
165
165
166 // Trigger cell toolbar registration.
166 // Trigger cell toolbar registration.
167 default_celltoolbar.register(this);
167 default_celltoolbar.register(this);
168 rawcell_celltoolbar.register(this);
168 rawcell_celltoolbar.register(this);
169 slideshow_celltoolbar.register(this);
169 slideshow_celltoolbar.register(this);
170
170
171 // prevent assign to miss-typed properties.
171 // prevent assign to miss-typed properties.
172 Object.seal(this);
172 Object.seal(this);
173 };
173 };
174
174
175 Notebook.options_default = {
175 Notebook.options_default = {
176 // can be any cell type, or the special values of
176 // can be any cell type, or the special values of
177 // 'above', 'below', or 'selected' to get the value from another cell.
177 // 'above', 'below', or 'selected' to get the value from another cell.
178 default_cell_type: 'code'
178 default_cell_type: 'code'
179 };
179 };
180
180
181 /**
181 /**
182 * Create an HTML and CSS representation of the notebook.
182 * Create an HTML and CSS representation of the notebook.
183 */
183 */
184 Notebook.prototype.create_elements = function () {
184 Notebook.prototype.create_elements = function () {
185 var that = this;
185 var that = this;
186 this.element.attr('tabindex','-1');
186 this.element.attr('tabindex','-1');
187 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
187 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
188 // We add this end_space div to the end of the notebook div to:
188 // We add this end_space div to the end of the notebook div to:
189 // i) provide a margin between the last cell and the end of the notebook
189 // i) provide a margin between the last cell and the end of the notebook
190 // ii) to prevent the div from scrolling up when the last cell is being
190 // ii) to prevent the div from scrolling up when the last cell is being
191 // edited, but is too low on the page, which browsers will do automatically.
191 // edited, but is too low on the page, which browsers will do automatically.
192 var end_space = $('<div/>').addClass('end_space');
192 var end_space = $('<div/>').addClass('end_space');
193 end_space.dblclick(function (e) {
193 end_space.dblclick(function (e) {
194 var ncells = that.ncells();
194 var ncells = that.ncells();
195 that.insert_cell_below('code',ncells-1);
195 that.insert_cell_below('code',ncells-1);
196 });
196 });
197 this.element.append(this.container);
197 this.element.append(this.container);
198 this.container.after(end_space);
198 this.container.after(end_space);
199 };
199 };
200
200
201 /**
201 /**
202 * Bind JavaScript events: key presses and custom IPython events.
202 * Bind JavaScript events: key presses and custom IPython events.
203 */
203 */
204 Notebook.prototype.bind_events = function () {
204 Notebook.prototype.bind_events = function () {
205 var that = this;
205 var that = this;
206
206
207 this.events.on('set_next_input.Notebook', function (event, data) {
207 this.events.on('set_next_input.Notebook', function (event, data) {
208 if (data.replace) {
208 if (data.replace) {
209 data.cell.set_text(data.text);
209 data.cell.set_text(data.text);
210 data.cell.clear_output();
210 data.cell.clear_output();
211 } else {
211 } else {
212 var index = that.find_cell_index(data.cell);
212 var index = that.find_cell_index(data.cell);
213 var new_cell = that.insert_cell_below('code',index);
213 var new_cell = that.insert_cell_below('code',index);
214 new_cell.set_text(data.text);
214 new_cell.set_text(data.text);
215 }
215 }
216 that.dirty = true;
216 that.dirty = true;
217 });
217 });
218
218
219 this.events.on('unrecognized_cell.Cell', function () {
219 this.events.on('unrecognized_cell.Cell', function () {
220 that.warn_nbformat_minor();
220 that.warn_nbformat_minor();
221 });
221 });
222
222
223 this.events.on('unrecognized_output.OutputArea', function () {
223 this.events.on('unrecognized_output.OutputArea', function () {
224 that.warn_nbformat_minor();
224 that.warn_nbformat_minor();
225 });
225 });
226
226
227 this.events.on('set_dirty.Notebook', function (event, data) {
227 this.events.on('set_dirty.Notebook', function (event, data) {
228 that.dirty = data.value;
228 that.dirty = data.value;
229 });
229 });
230
230
231 this.events.on('trust_changed.Notebook', function (event, trusted) {
231 this.events.on('trust_changed.Notebook', function (event, trusted) {
232 that.trusted = trusted;
232 that.trusted = trusted;
233 });
233 });
234
234
235 this.events.on('select.Cell', function (event, data) {
235 this.events.on('select.Cell', function (event, data) {
236 var index = that.find_cell_index(data.cell);
236 var index = that.find_cell_index(data.cell);
237 that.select(index);
237 that.select(index);
238 });
238 });
239
239
240 this.events.on('edit_mode.Cell', function (event, data) {
240 this.events.on('edit_mode.Cell', function (event, data) {
241 that.handle_edit_mode(data.cell);
241 that.handle_edit_mode(data.cell);
242 });
242 });
243
243
244 this.events.on('command_mode.Cell', function (event, data) {
244 this.events.on('command_mode.Cell', function (event, data) {
245 that.handle_command_mode(data.cell);
245 that.handle_command_mode(data.cell);
246 });
246 });
247
247
248 this.events.on('spec_changed.Kernel', function(event, data) {
248 this.events.on('spec_changed.Kernel', function(event, data) {
249 that.metadata.kernelspec =
249 that.metadata.kernelspec =
250 {name: data.name, display_name: data.spec.display_name};
250 {name: data.name, display_name: data.spec.display_name};
251 // start session if the current session isn't already correct
251 // start session if the current session isn't already correct
252 if (!(this.session && this.session.kernel && this.session.kernel.name === data.name)) {
252 if (!(this.session && this.session.kernel && this.session.kernel.name === data.name)) {
253 that.start_session(data.name);
253 that.start_session(data.name);
254 }
254 }
255 });
255 });
256
256
257 this.events.on('kernel_ready.Kernel', function(event, data) {
257 this.events.on('kernel_ready.Kernel', function(event, data) {
258 var kinfo = data.kernel.info_reply;
258 var kinfo = data.kernel.info_reply;
259 if (!kinfo.language_info) {
259 if (!kinfo.language_info) {
260 delete that.metadata.language_info;
260 delete that.metadata.language_info;
261 return;
261 return;
262 }
262 }
263 var langinfo = kinfo.language_info;
263 var langinfo = kinfo.language_info;
264 that.metadata.language_info = langinfo;
264 that.metadata.language_info = langinfo;
265 // Mode 'null' should be plain, unhighlighted text.
265 // Mode 'null' should be plain, unhighlighted text.
266 var cm_mode = langinfo.codemirror_mode || langinfo.name || 'null';
266 var cm_mode = langinfo.codemirror_mode || langinfo.name || 'null';
267 that.set_codemirror_mode(cm_mode);
267 that.set_codemirror_mode(cm_mode);
268 });
268 });
269
269
270 var collapse_time = function (time) {
270 var collapse_time = function (time) {
271 var app_height = $('#ipython-main-app').height(); // content height
271 var app_height = $('#ipython-main-app').height(); // content height
272 var splitter_height = $('div#pager_splitter').outerHeight(true);
272 var splitter_height = $('div#pager_splitter').outerHeight(true);
273 var new_height = app_height - splitter_height;
273 var new_height = app_height - splitter_height;
274 that.element.animate({height : new_height + 'px'}, time);
274 that.element.animate({height : new_height + 'px'}, time);
275 };
275 };
276
276
277 this.element.bind('collapse_pager', function (event, extrap) {
277 this.element.bind('collapse_pager', function (event, extrap) {
278 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
278 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
279 collapse_time(time);
279 collapse_time(time);
280 });
280 });
281
281
282 var expand_time = function (time) {
282 var expand_time = function (time) {
283 var app_height = $('#ipython-main-app').height(); // content height
283 var app_height = $('#ipython-main-app').height(); // content height
284 var splitter_height = $('div#pager_splitter').outerHeight(true);
284 var splitter_height = $('div#pager_splitter').outerHeight(true);
285 var pager_height = $('div#pager').outerHeight(true);
285 var pager_height = $('div#pager').outerHeight(true);
286 var new_height = app_height - pager_height - splitter_height;
286 var new_height = app_height - pager_height - splitter_height;
287 that.element.animate({height : new_height + 'px'}, time);
287 that.element.animate({height : new_height + 'px'}, time);
288 };
288 };
289
289
290 this.element.bind('expand_pager', function (event, extrap) {
290 this.element.bind('expand_pager', function (event, extrap) {
291 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
291 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
292 expand_time(time);
292 expand_time(time);
293 });
293 });
294
294
295 // Firefox 22 broke $(window).on("beforeunload")
295 // Firefox 22 broke $(window).on("beforeunload")
296 // I'm not sure why or how.
296 // I'm not sure why or how.
297 window.onbeforeunload = function (e) {
297 window.onbeforeunload = function (e) {
298 // TODO: Make killing the kernel configurable.
298 // TODO: Make killing the kernel configurable.
299 var kill_kernel = false;
299 var kill_kernel = false;
300 if (kill_kernel) {
300 if (kill_kernel) {
301 that.session.delete();
301 that.session.delete();
302 }
302 }
303 // if we are autosaving, trigger an autosave on nav-away.
303 // if we are autosaving, trigger an autosave on nav-away.
304 // still warn, because if we don't the autosave may fail.
304 // still warn, because if we don't the autosave may fail.
305 if (that.dirty) {
305 if (that.dirty) {
306 if ( that.autosave_interval ) {
306 if ( that.autosave_interval ) {
307 // schedule autosave in a timeout
307 // schedule autosave in a timeout
308 // this gives you a chance to forcefully discard changes
308 // this gives you a chance to forcefully discard changes
309 // by reloading the page if you *really* want to.
309 // by reloading the page if you *really* want to.
310 // the timer doesn't start until you *dismiss* the dialog.
310 // the timer doesn't start until you *dismiss* the dialog.
311 setTimeout(function () {
311 setTimeout(function () {
312 if (that.dirty) {
312 if (that.dirty) {
313 that.save_notebook();
313 that.save_notebook();
314 }
314 }
315 }, 1000);
315 }, 1000);
316 return "Autosave in progress, latest changes may be lost.";
316 return "Autosave in progress, latest changes may be lost.";
317 } else {
317 } else {
318 return "Unsaved changes will be lost.";
318 return "Unsaved changes will be lost.";
319 }
319 }
320 }
320 }
321 // Null is the *only* return value that will make the browser not
321 // Null is the *only* return value that will make the browser not
322 // pop up the "don't leave" dialog.
322 // pop up the "don't leave" dialog.
323 return null;
323 return null;
324 };
324 };
325 };
325 };
326
326
327 /**
327 /**
328 * Trigger a warning dialog about missing functionality from newer minor versions
328 * Trigger a warning dialog about missing functionality from newer minor versions
329 */
329 */
330 Notebook.prototype.warn_nbformat_minor = function (event) {
330 Notebook.prototype.warn_nbformat_minor = function (event) {
331 var v = 'v' + this.nbformat + '.';
331 var v = 'v' + this.nbformat + '.';
332 var orig_vs = v + this.nbformat_minor;
332 var orig_vs = v + this.nbformat_minor;
333 var this_vs = v + this.current_nbformat_minor;
333 var this_vs = v + this.current_nbformat_minor;
334 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
334 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
335 this_vs + ". You can still work with this notebook, but cell and output types " +
335 this_vs + ". You can still work with this notebook, but cell and output types " +
336 "introduced in later notebook versions will not be available.";
336 "introduced in later notebook versions will not be available.";
337
337
338 dialog.modal({
338 dialog.modal({
339 notebook: this,
339 notebook: this,
340 keyboard_manager: this.keyboard_manager,
340 keyboard_manager: this.keyboard_manager,
341 title : "Newer Notebook",
341 title : "Newer Notebook",
342 body : msg,
342 body : msg,
343 buttons : {
343 buttons : {
344 OK : {
344 OK : {
345 "class" : "btn-danger"
345 "class" : "btn-danger"
346 }
346 }
347 }
347 }
348 });
348 });
349 };
349 };
350
350
351 /**
351 /**
352 * Set the dirty flag, and trigger the set_dirty.Notebook event
352 * Set the dirty flag, and trigger the set_dirty.Notebook event
353 */
353 */
354 Notebook.prototype.set_dirty = function (value) {
354 Notebook.prototype.set_dirty = function (value) {
355 if (value === undefined) {
355 if (value === undefined) {
356 value = true;
356 value = true;
357 }
357 }
358 if (this.dirty === value) {
358 if (this.dirty === value) {
359 return;
359 return;
360 }
360 }
361 this.events.trigger('set_dirty.Notebook', {value: value});
361 this.events.trigger('set_dirty.Notebook', {value: value});
362 };
362 };
363
363
364 /**
364 /**
365 * Scroll the top of the page to a given cell.
365 * Scroll the top of the page to a given cell.
366 *
366 *
367 * @param {integer} index - An index of the cell to view
367 * @param {integer} index - An index of the cell to view
368 * @param {integer} time - Animation time in milliseconds
368 * @param {integer} time - Animation time in milliseconds
369 * @return {integer} Pixel offset from the top of the container
369 * @return {integer} Pixel offset from the top of the container
370 */
370 */
371 Notebook.prototype.scroll_to_cell = function (index, time) {
371 Notebook.prototype.scroll_to_cell = function (index, time) {
372 var cells = this.get_cells();
372 var cells = this.get_cells();
373 time = time || 0;
373 time = time || 0;
374 index = Math.min(cells.length-1,index);
374 index = Math.min(cells.length-1,index);
375 index = Math.max(0 ,index);
375 index = Math.max(0 ,index);
376 var scroll_value = cells[index].element.position().top-cells[0].element.position().top ;
376 var scroll_value = cells[index].element.position().top-cells[0].element.position().top ;
377 this.scroll_manager.element.animate({scrollTop:scroll_value}, time);
377 this.scroll_manager.element.animate({scrollTop:scroll_value}, time);
378 return scroll_value;
378 return scroll_value;
379 };
379 };
380
380
381 /**
381 /**
382 * Scroll to the bottom of the page.
382 * Scroll to the bottom of the page.
383 */
383 */
384 Notebook.prototype.scroll_to_bottom = function () {
384 Notebook.prototype.scroll_to_bottom = function () {
385 this.scroll_manager.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
385 this.scroll_manager.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
386 };
386 };
387
387
388 /**
388 /**
389 * Scroll to the top of the page.
389 * Scroll to the top of the page.
390 */
390 */
391 Notebook.prototype.scroll_to_top = function () {
391 Notebook.prototype.scroll_to_top = function () {
392 this.scroll_manager.element.animate({scrollTop:0}, 0);
392 this.scroll_manager.element.animate({scrollTop:0}, 0);
393 };
393 };
394
394
395 // Edit Notebook metadata
395 // Edit Notebook metadata
396
396
397 /**
397 /**
398 * Display a dialog that allows the user to edit the Notebook's metadata.
398 * Display a dialog that allows the user to edit the Notebook's metadata.
399 */
399 */
400 Notebook.prototype.edit_metadata = function () {
400 Notebook.prototype.edit_metadata = function () {
401 var that = this;
401 var that = this;
402 dialog.edit_metadata({
402 dialog.edit_metadata({
403 md: this.metadata,
403 md: this.metadata,
404 callback: function (md) {
404 callback: function (md) {
405 that.metadata = md;
405 that.metadata = md;
406 },
406 },
407 name: 'Notebook',
407 name: 'Notebook',
408 notebook: this,
408 notebook: this,
409 keyboard_manager: this.keyboard_manager});
409 keyboard_manager: this.keyboard_manager});
410 };
410 };
411
411
412 // Cell indexing, retrieval, etc.
412 // Cell indexing, retrieval, etc.
413
413
414 /**
414 /**
415 * Get all cell elements in the notebook.
415 * Get all cell elements in the notebook.
416 *
416 *
417 * @return {jQuery} A selector of all cell elements
417 * @return {jQuery} A selector of all cell elements
418 */
418 */
419 Notebook.prototype.get_cell_elements = function () {
419 Notebook.prototype.get_cell_elements = function () {
420 return this.container.find(".cell").not('.cell .cell');
420 return this.container.find(".cell").not('.cell .cell');
421 };
421 };
422
422
423 /**
423 /**
424 * Get a particular cell element.
424 * Get a particular cell element.
425 *
425 *
426 * @param {integer} index An index of a cell to select
426 * @param {integer} index An index of a cell to select
427 * @return {jQuery} A selector of the given cell.
427 * @return {jQuery} A selector of the given cell.
428 */
428 */
429 Notebook.prototype.get_cell_element = function (index) {
429 Notebook.prototype.get_cell_element = function (index) {
430 var result = null;
430 var result = null;
431 var e = this.get_cell_elements().eq(index);
431 var e = this.get_cell_elements().eq(index);
432 if (e.length !== 0) {
432 if (e.length !== 0) {
433 result = e;
433 result = e;
434 }
434 }
435 return result;
435 return result;
436 };
436 };
437
437
438 /**
438 /**
439 * Try to get a particular cell by msg_id.
439 * Try to get a particular cell by msg_id.
440 *
440 *
441 * @param {string} msg_id A message UUID
441 * @param {string} msg_id A message UUID
442 * @return {Cell} Cell or null if no cell was found.
442 * @return {Cell} Cell or null if no cell was found.
443 */
443 */
444 Notebook.prototype.get_msg_cell = function (msg_id) {
444 Notebook.prototype.get_msg_cell = function (msg_id) {
445 return codecell.CodeCell.msg_cells[msg_id] || null;
445 return codecell.CodeCell.msg_cells[msg_id] || null;
446 };
446 };
447
447
448 /**
448 /**
449 * Count the cells in this notebook.
449 * Count the cells in this notebook.
450 *
450 *
451 * @return {integer} The number of cells in this notebook
451 * @return {integer} The number of cells in this notebook
452 */
452 */
453 Notebook.prototype.ncells = function () {
453 Notebook.prototype.ncells = function () {
454 return this.get_cell_elements().length;
454 return this.get_cell_elements().length;
455 };
455 };
456
456
457 /**
457 /**
458 * Get all Cell objects in this notebook.
458 * Get all Cell objects in this notebook.
459 *
459 *
460 * @return {Array} This notebook's Cell objects
460 * @return {Array} This notebook's Cell objects
461 */
461 */
462 Notebook.prototype.get_cells = function () {
462 Notebook.prototype.get_cells = function () {
463 // TODO: we are often calling cells as cells()[i], which we should optimize
463 // TODO: we are often calling cells as cells()[i], which we should optimize
464 // to cells(i) or a new method.
464 // to cells(i) or a new method.
465 return this.get_cell_elements().toArray().map(function (e) {
465 return this.get_cell_elements().toArray().map(function (e) {
466 return $(e).data("cell");
466 return $(e).data("cell");
467 });
467 });
468 };
468 };
469
469
470 /**
470 /**
471 * Get a Cell objects from this notebook.
471 * Get a Cell objects from this notebook.
472 *
472 *
473 * @param {integer} index - An index of a cell to retrieve
473 * @param {integer} index - An index of a cell to retrieve
474 * @return {Cell} Cell or null if no cell was found.
474 * @return {Cell} Cell or null if no cell was found.
475 */
475 */
476 Notebook.prototype.get_cell = function (index) {
476 Notebook.prototype.get_cell = function (index) {
477 var result = null;
477 var result = null;
478 var ce = this.get_cell_element(index);
478 var ce = this.get_cell_element(index);
479 if (ce !== null) {
479 if (ce !== null) {
480 result = ce.data('cell');
480 result = ce.data('cell');
481 }
481 }
482 return result;
482 return result;
483 };
483 };
484
484
485 /**
485 /**
486 * Get the cell below a given cell.
486 * Get the cell below a given cell.
487 *
487 *
488 * @param {Cell} cell
488 * @param {Cell} cell
489 * @return {Cell} the next cell or null if no cell was found.
489 * @return {Cell} the next cell or null if no cell was found.
490 */
490 */
491 Notebook.prototype.get_next_cell = function (cell) {
491 Notebook.prototype.get_next_cell = function (cell) {
492 var result = null;
492 var result = null;
493 var index = this.find_cell_index(cell);
493 var index = this.find_cell_index(cell);
494 if (this.is_valid_cell_index(index+1)) {
494 if (this.is_valid_cell_index(index+1)) {
495 result = this.get_cell(index+1);
495 result = this.get_cell(index+1);
496 }
496 }
497 return result;
497 return result;
498 };
498 };
499
499
500 /**
500 /**
501 * Get the cell above a given cell.
501 * Get the cell above a given cell.
502 *
502 *
503 * @param {Cell} cell
503 * @param {Cell} cell
504 * @return {Cell} The previous cell or null if no cell was found.
504 * @return {Cell} The previous cell or null if no cell was found.
505 */
505 */
506 Notebook.prototype.get_prev_cell = function (cell) {
506 Notebook.prototype.get_prev_cell = function (cell) {
507 var result = null;
507 var result = null;
508 var index = this.find_cell_index(cell);
508 var index = this.find_cell_index(cell);
509 if (index !== null && index > 0) {
509 if (index !== null && index > 0) {
510 result = this.get_cell(index-1);
510 result = this.get_cell(index-1);
511 }
511 }
512 return result;
512 return result;
513 };
513 };
514
514
515 /**
515 /**
516 * Get the numeric index of a given cell.
516 * Get the numeric index of a given cell.
517 *
517 *
518 * @param {Cell} cell
518 * @param {Cell} cell
519 * @return {integer} The cell's numeric index or null if no cell was found.
519 * @return {integer} The cell's numeric index or null if no cell was found.
520 */
520 */
521 Notebook.prototype.find_cell_index = function (cell) {
521 Notebook.prototype.find_cell_index = function (cell) {
522 var result = null;
522 var result = null;
523 this.get_cell_elements().filter(function (index) {
523 this.get_cell_elements().filter(function (index) {
524 if ($(this).data("cell") === cell) {
524 if ($(this).data("cell") === cell) {
525 result = index;
525 result = index;
526 }
526 }
527 });
527 });
528 return result;
528 return result;
529 };
529 };
530
530
531 /**
531 /**
532 * Return given index if defined, or the selected index if not.
532 * Return given index if defined, or the selected index if not.
533 *
533 *
534 * @param {integer} [index] - A cell's index
534 * @param {integer} [index] - A cell's index
535 * @return {integer} cell index
535 * @return {integer} cell index
536 */
536 */
537 Notebook.prototype.index_or_selected = function (index) {
537 Notebook.prototype.index_or_selected = function (index) {
538 var i;
538 var i;
539 if (index === undefined || index === null) {
539 if (index === undefined || index === null) {
540 i = this.get_selected_index();
540 i = this.get_selected_index();
541 if (i === null) {
541 if (i === null) {
542 i = 0;
542 i = 0;
543 }
543 }
544 } else {
544 } else {
545 i = index;
545 i = index;
546 }
546 }
547 return i;
547 return i;
548 };
548 };
549
549
550 /**
550 /**
551 * Get the currently selected cell.
551 * Get the currently selected cell.
552 *
552 *
553 * @return {Cell} The selected cell
553 * @return {Cell} The selected cell
554 */
554 */
555 Notebook.prototype.get_selected_cell = function () {
555 Notebook.prototype.get_selected_cell = function () {
556 var index = this.get_selected_index();
556 var index = this.get_selected_index();
557 return this.get_cell(index);
557 return this.get_cell(index);
558 };
558 };
559
559
560 /**
560 /**
561 * Check whether a cell index is valid.
561 * Check whether a cell index is valid.
562 *
562 *
563 * @param {integer} index - A cell index
563 * @param {integer} index - A cell index
564 * @return True if the index is valid, false otherwise
564 * @return True if the index is valid, false otherwise
565 */
565 */
566 Notebook.prototype.is_valid_cell_index = function (index) {
566 Notebook.prototype.is_valid_cell_index = function (index) {
567 if (index !== null && index >= 0 && index < this.ncells()) {
567 if (index !== null && index >= 0 && index < this.ncells()) {
568 return true;
568 return true;
569 } else {
569 } else {
570 return false;
570 return false;
571 }
571 }
572 };
572 };
573
573
574 /**
574 /**
575 * Get the index of the currently selected cell.
575 * Get the index of the currently selected cell.
576 *
576 *
577 * @return {integer} The selected cell's numeric index
577 * @return {integer} The selected cell's numeric index
578 */
578 */
579 Notebook.prototype.get_selected_index = function () {
579 Notebook.prototype.get_selected_index = function () {
580 var result = null;
580 var result = null;
581 this.get_cell_elements().filter(function (index) {
581 this.get_cell_elements().filter(function (index) {
582 if ($(this).data("cell").selected === true) {
582 if ($(this).data("cell").selected === true) {
583 result = index;
583 result = index;
584 }
584 }
585 });
585 });
586 return result;
586 return result;
587 };
587 };
588
588
589
589
590 // Cell selection.
590 // Cell selection.
591
591
592 /**
592 /**
593 * Programmatically select a cell.
593 * Programmatically select a cell.
594 *
594 *
595 * @param {integer} index - A cell's index
595 * @param {integer} index - A cell's index
596 * @return {Notebook} This notebook
596 * @return {Notebook} This notebook
597 */
597 */
598 Notebook.prototype.select = function (index) {
598 Notebook.prototype.select = function (index) {
599 if (this.is_valid_cell_index(index)) {
599 if (this.is_valid_cell_index(index)) {
600 var sindex = this.get_selected_index();
600 var sindex = this.get_selected_index();
601 if (sindex !== null && index !== sindex) {
601 if (sindex !== null && index !== sindex) {
602 // If we are about to select a different cell, make sure we are
602 // If we are about to select a different cell, make sure we are
603 // first in command mode.
603 // first in command mode.
604 if (this.mode !== 'command') {
604 if (this.mode !== 'command') {
605 this.command_mode();
605 this.command_mode();
606 }
606 }
607 this.get_cell(sindex).unselect();
607 this.get_cell(sindex).unselect();
608 }
608 }
609 var cell = this.get_cell(index);
609 var cell = this.get_cell(index);
610 cell.select();
610 cell.select();
611 if (cell.cell_type === 'heading') {
611 if (cell.cell_type === 'heading') {
612 this.events.trigger('selected_cell_type_changed.Notebook',
612 this.events.trigger('selected_cell_type_changed.Notebook',
613 {'cell_type':cell.cell_type,level:cell.level}
613 {'cell_type':cell.cell_type,level:cell.level}
614 );
614 );
615 } else {
615 } else {
616 this.events.trigger('selected_cell_type_changed.Notebook',
616 this.events.trigger('selected_cell_type_changed.Notebook',
617 {'cell_type':cell.cell_type}
617 {'cell_type':cell.cell_type}
618 );
618 );
619 }
619 }
620 }
620 }
621 return this;
621 return this;
622 };
622 };
623
623
624 /**
624 /**
625 * Programmatically select the next cell.
625 * Programmatically select the next cell.
626 *
626 *
627 * @return {Notebook} This notebook
627 * @return {Notebook} This notebook
628 */
628 */
629 Notebook.prototype.select_next = function () {
629 Notebook.prototype.select_next = function () {
630 var index = this.get_selected_index();
630 var index = this.get_selected_index();
631 this.select(index+1);
631 this.select(index+1);
632 return this;
632 return this;
633 };
633 };
634
634
635 /**
635 /**
636 * Programmatically select the previous cell.
636 * Programmatically select the previous cell.
637 *
637 *
638 * @return {Notebook} This notebook
638 * @return {Notebook} This notebook
639 */
639 */
640 Notebook.prototype.select_prev = function () {
640 Notebook.prototype.select_prev = function () {
641 var index = this.get_selected_index();
641 var index = this.get_selected_index();
642 this.select(index-1);
642 this.select(index-1);
643 return this;
643 return this;
644 };
644 };
645
645
646
646
647 // Edit/Command mode
647 // Edit/Command mode
648
648
649 /**
649 /**
650 * Gets the index of the cell that is in edit mode.
650 * Gets the index of the cell that is in edit mode.
651 *
651 *
652 * @return {integer} index
652 * @return {integer} index
653 */
653 */
654 Notebook.prototype.get_edit_index = function () {
654 Notebook.prototype.get_edit_index = function () {
655 var result = null;
655 var result = null;
656 this.get_cell_elements().filter(function (index) {
656 this.get_cell_elements().filter(function (index) {
657 if ($(this).data("cell").mode === 'edit') {
657 if ($(this).data("cell").mode === 'edit') {
658 result = index;
658 result = index;
659 }
659 }
660 });
660 });
661 return result;
661 return result;
662 };
662 };
663
663
664 /**
664 /**
665 * Handle when a a cell blurs and the notebook should enter command mode.
665 * Handle when a a cell blurs and the notebook should enter command mode.
666 *
666 *
667 * @param {Cell} [cell] - Cell to enter command mode on.
667 * @param {Cell} [cell] - Cell to enter command mode on.
668 */
668 */
669 Notebook.prototype.handle_command_mode = function (cell) {
669 Notebook.prototype.handle_command_mode = function (cell) {
670 if (this.mode !== 'command') {
670 if (this.mode !== 'command') {
671 cell.command_mode();
671 cell.command_mode();
672 this.mode = 'command';
672 this.mode = 'command';
673 this.events.trigger('command_mode.Notebook');
673 this.events.trigger('command_mode.Notebook');
674 this.keyboard_manager.command_mode();
674 this.keyboard_manager.command_mode();
675 }
675 }
676 };
676 };
677
677
678 /**
678 /**
679 * Make the notebook enter command mode.
679 * Make the notebook enter command mode.
680 */
680 */
681 Notebook.prototype.command_mode = function () {
681 Notebook.prototype.command_mode = function () {
682 var cell = this.get_cell(this.get_edit_index());
682 var cell = this.get_cell(this.get_edit_index());
683 if (cell && this.mode !== 'command') {
683 if (cell && this.mode !== 'command') {
684 // We don't call cell.command_mode, but rather call cell.focus_cell()
684 // We don't call cell.command_mode, but rather call cell.focus_cell()
685 // which will blur and CM editor and trigger the call to
685 // which will blur and CM editor and trigger the call to
686 // handle_command_mode.
686 // handle_command_mode.
687 cell.focus_cell();
687 cell.focus_cell();
688 }
688 }
689 };
689 };
690
690
691 /**
691 /**
692 * Handle when a cell fires it's edit_mode event.
692 * Handle when a cell fires it's edit_mode event.
693 *
693 *
694 * @param {Cell} [cell] Cell to enter edit mode on.
694 * @param {Cell} [cell] Cell to enter edit mode on.
695 */
695 */
696 Notebook.prototype.handle_edit_mode = function (cell) {
696 Notebook.prototype.handle_edit_mode = function (cell) {
697 if (cell && this.mode !== 'edit') {
697 if (cell && this.mode !== 'edit') {
698 cell.edit_mode();
698 cell.edit_mode();
699 this.mode = 'edit';
699 this.mode = 'edit';
700 this.events.trigger('edit_mode.Notebook');
700 this.events.trigger('edit_mode.Notebook');
701 this.keyboard_manager.edit_mode();
701 this.keyboard_manager.edit_mode();
702 }
702 }
703 };
703 };
704
704
705 /**
705 /**
706 * Make a cell enter edit mode.
706 * Make a cell enter edit mode.
707 */
707 */
708 Notebook.prototype.edit_mode = function () {
708 Notebook.prototype.edit_mode = function () {
709 var cell = this.get_selected_cell();
709 var cell = this.get_selected_cell();
710 if (cell && this.mode !== 'edit') {
710 if (cell && this.mode !== 'edit') {
711 cell.unrender();
711 cell.unrender();
712 cell.focus_editor();
712 cell.focus_editor();
713 }
713 }
714 };
714 };
715
715
716 /**
716 /**
717 * Focus the currently selected cell.
717 * Focus the currently selected cell.
718 */
718 */
719 Notebook.prototype.focus_cell = function () {
719 Notebook.prototype.focus_cell = function () {
720 var cell = this.get_selected_cell();
720 var cell = this.get_selected_cell();
721 if (cell === null) {return;} // No cell is selected
721 if (cell === null) {return;} // No cell is selected
722 cell.focus_cell();
722 cell.focus_cell();
723 };
723 };
724
724
725 // Cell movement
725 // Cell movement
726
726
727 /**
727 /**
728 * Move given (or selected) cell up and select it.
728 * Move given (or selected) cell up and select it.
729 *
729 *
730 * @param {integer} [index] - cell index
730 * @param {integer} [index] - cell index
731 * @return {Notebook} This notebook
731 * @return {Notebook} This notebook
732 */
732 */
733 Notebook.prototype.move_cell_up = function (index) {
733 Notebook.prototype.move_cell_up = function (index) {
734 var i = this.index_or_selected(index);
734 var i = this.index_or_selected(index);
735 if (this.is_valid_cell_index(i) && i > 0) {
735 if (this.is_valid_cell_index(i) && i > 0) {
736 var pivot = this.get_cell_element(i-1);
736 var pivot = this.get_cell_element(i-1);
737 var tomove = this.get_cell_element(i);
737 var tomove = this.get_cell_element(i);
738 if (pivot !== null && tomove !== null) {
738 if (pivot !== null && tomove !== null) {
739 tomove.detach();
739 tomove.detach();
740 pivot.before(tomove);
740 pivot.before(tomove);
741 this.select(i-1);
741 this.select(i-1);
742 var cell = this.get_selected_cell();
742 var cell = this.get_selected_cell();
743 cell.focus_cell();
743 cell.focus_cell();
744 }
744 }
745 this.set_dirty(true);
745 this.set_dirty(true);
746 }
746 }
747 return this;
747 return this;
748 };
748 };
749
749
750
750
751 /**
751 /**
752 * Move given (or selected) cell down and select it.
752 * Move given (or selected) cell down and select it.
753 *
753 *
754 * @param {integer} [index] - cell index
754 * @param {integer} [index] - cell index
755 * @return {Notebook} This notebook
755 * @return {Notebook} This notebook
756 */
756 */
757 Notebook.prototype.move_cell_down = function (index) {
757 Notebook.prototype.move_cell_down = function (index) {
758 var i = this.index_or_selected(index);
758 var i = this.index_or_selected(index);
759 if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
759 if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
760 var pivot = this.get_cell_element(i+1);
760 var pivot = this.get_cell_element(i+1);
761 var tomove = this.get_cell_element(i);
761 var tomove = this.get_cell_element(i);
762 if (pivot !== null && tomove !== null) {
762 if (pivot !== null && tomove !== null) {
763 tomove.detach();
763 tomove.detach();
764 pivot.after(tomove);
764 pivot.after(tomove);
765 this.select(i+1);
765 this.select(i+1);
766 var cell = this.get_selected_cell();
766 var cell = this.get_selected_cell();
767 cell.focus_cell();
767 cell.focus_cell();
768 }
768 }
769 }
769 }
770 this.set_dirty();
770 this.set_dirty();
771 return this;
771 return this;
772 };
772 };
773
773
774
774
775 // Insertion, deletion.
775 // Insertion, deletion.
776
776
777 /**
777 /**
778 * Delete a cell from the notebook without any precautions
778 * Delete a cell from the notebook without any precautions
779 * Needed to reload checkpoints and other things like that.
779 * Needed to reload checkpoints and other things like that.
780 *
780 *
781 * @param {integer} [index] - cell's numeric index
781 * @param {integer} [index] - cell's numeric index
782 * @return {Notebook} This notebook
782 * @return {Notebook} This notebook
783 */
783 */
784 Notebook.prototype._unsafe_delete_cell = function (index) {
784 Notebook.prototype._unsafe_delete_cell = function (index) {
785 var i = this.index_or_selected(index);
785 var i = this.index_or_selected(index);
786 var cell = this.get_cell(i);
786 var cell = this.get_cell(i);
787
787
788 $('#undelete_cell').addClass('disabled');
788 $('#undelete_cell').addClass('disabled');
789 if (this.is_valid_cell_index(i)) {
789 if (this.is_valid_cell_index(i)) {
790 var old_ncells = this.ncells();
790 var old_ncells = this.ncells();
791 var ce = this.get_cell_element(i);
791 var ce = this.get_cell_element(i);
792 ce.remove();
792 ce.remove();
793 this.set_dirty(true);
793 this.set_dirty(true);
794 }
794 }
795 return this;
795 return this;
796 };
796 };
797
797
798 /**
798 /**
799 * Delete a cell from the notebook.
799 * Delete a cell from the notebook.
800 *
800 *
801 * @param {integer} [index] - cell's numeric index
801 * @param {integer} [index] - cell's numeric index
802 * @return {Notebook} This notebook
802 * @return {Notebook} This notebook
803 */
803 */
804 Notebook.prototype.delete_cell = function (index) {
804 Notebook.prototype.delete_cell = function (index) {
805 var i = this.index_or_selected(index);
805 var i = this.index_or_selected(index);
806 var cell = this.get_cell(i);
806 var cell = this.get_cell(i);
807 if (!cell.is_deletable()) {
807 if (!cell.is_deletable()) {
808 return this;
808 return this;
809 }
809 }
810
810
811 this.undelete_backup = cell.toJSON();
811 this.undelete_backup = cell.toJSON();
812 $('#undelete_cell').removeClass('disabled');
812 $('#undelete_cell').removeClass('disabled');
813 if (this.is_valid_cell_index(i)) {
813 if (this.is_valid_cell_index(i)) {
814 var old_ncells = this.ncells();
814 var old_ncells = this.ncells();
815 var ce = this.get_cell_element(i);
815 var ce = this.get_cell_element(i);
816 ce.remove();
816 ce.remove();
817 if (i === 0) {
817 if (i === 0) {
818 // Always make sure we have at least one cell.
818 // Always make sure we have at least one cell.
819 if (old_ncells === 1) {
819 if (old_ncells === 1) {
820 this.insert_cell_below('code');
820 this.insert_cell_below('code');
821 }
821 }
822 this.select(0);
822 this.select(0);
823 this.undelete_index = 0;
823 this.undelete_index = 0;
824 this.undelete_below = false;
824 this.undelete_below = false;
825 } else if (i === old_ncells-1 && i !== 0) {
825 } else if (i === old_ncells-1 && i !== 0) {
826 this.select(i-1);
826 this.select(i-1);
827 this.undelete_index = i - 1;
827 this.undelete_index = i - 1;
828 this.undelete_below = true;
828 this.undelete_below = true;
829 } else {
829 } else {
830 this.select(i);
830 this.select(i);
831 this.undelete_index = i;
831 this.undelete_index = i;
832 this.undelete_below = false;
832 this.undelete_below = false;
833 }
833 }
834 this.events.trigger('delete.Cell', {'cell': cell, 'index': i});
834 this.events.trigger('delete.Cell', {'cell': cell, 'index': i});
835 this.set_dirty(true);
835 this.set_dirty(true);
836 }
836 }
837 return this;
837 return this;
838 };
838 };
839
839
840 /**
840 /**
841 * Restore the most recently deleted cell.
841 * Restore the most recently deleted cell.
842 */
842 */
843 Notebook.prototype.undelete_cell = function() {
843 Notebook.prototype.undelete_cell = function() {
844 if (this.undelete_backup !== null && this.undelete_index !== null) {
844 if (this.undelete_backup !== null && this.undelete_index !== null) {
845 var current_index = this.get_selected_index();
845 var current_index = this.get_selected_index();
846 if (this.undelete_index < current_index) {
846 if (this.undelete_index < current_index) {
847 current_index = current_index + 1;
847 current_index = current_index + 1;
848 }
848 }
849 if (this.undelete_index >= this.ncells()) {
849 if (this.undelete_index >= this.ncells()) {
850 this.select(this.ncells() - 1);
850 this.select(this.ncells() - 1);
851 }
851 }
852 else {
852 else {
853 this.select(this.undelete_index);
853 this.select(this.undelete_index);
854 }
854 }
855 var cell_data = this.undelete_backup;
855 var cell_data = this.undelete_backup;
856 var new_cell = null;
856 var new_cell = null;
857 if (this.undelete_below) {
857 if (this.undelete_below) {
858 new_cell = this.insert_cell_below(cell_data.cell_type);
858 new_cell = this.insert_cell_below(cell_data.cell_type);
859 } else {
859 } else {
860 new_cell = this.insert_cell_above(cell_data.cell_type);
860 new_cell = this.insert_cell_above(cell_data.cell_type);
861 }
861 }
862 new_cell.fromJSON(cell_data);
862 new_cell.fromJSON(cell_data);
863 if (this.undelete_below) {
863 if (this.undelete_below) {
864 this.select(current_index+1);
864 this.select(current_index+1);
865 } else {
865 } else {
866 this.select(current_index);
866 this.select(current_index);
867 }
867 }
868 this.undelete_backup = null;
868 this.undelete_backup = null;
869 this.undelete_index = null;
869 this.undelete_index = null;
870 }
870 }
871 $('#undelete_cell').addClass('disabled');
871 $('#undelete_cell').addClass('disabled');
872 };
872 };
873
873
874 /**
874 /**
875 * Insert a cell so that after insertion the cell is at given index.
875 * Insert a cell so that after insertion the cell is at given index.
876 *
876 *
877 * If cell type is not provided, it will default to the type of the
877 * If cell type is not provided, it will default to the type of the
878 * currently active cell.
878 * currently active cell.
879 *
879 *
880 * Similar to insert_above, but index parameter is mandatory.
880 * Similar to insert_above, but index parameter is mandatory.
881 *
881 *
882 * Index will be brought back into the accessible range [0,n].
882 * Index will be brought back into the accessible range [0,n].
883 *
883 *
884 * @param {string} [type] - in ['code','markdown', 'raw'], defaults to 'code'
884 * @param {string} [type] - in ['code','markdown', 'raw'], defaults to 'code'
885 * @param {integer} [index] - a valid index where to insert cell
885 * @param {integer} [index] - a valid index where to insert cell
886 * @return {Cell|null} created cell or null
886 * @return {Cell|null} created cell or null
887 */
887 */
888 Notebook.prototype.insert_cell_at_index = function(type, index){
888 Notebook.prototype.insert_cell_at_index = function(type, index){
889
889
890 var ncells = this.ncells();
890 var ncells = this.ncells();
891 index = Math.min(index, ncells);
891 index = Math.min(index, ncells);
892 index = Math.max(index, 0);
892 index = Math.max(index, 0);
893 var cell = null;
893 var cell = null;
894 type = type || this.class_config.get_sync('default_cell_type');
894 type = type || this.class_config.get_sync('default_cell_type');
895 if (type === 'above') {
895 if (type === 'above') {
896 if (index > 0) {
896 if (index > 0) {
897 type = this.get_cell(index-1).cell_type;
897 type = this.get_cell(index-1).cell_type;
898 } else {
898 } else {
899 type = 'code';
899 type = 'code';
900 }
900 }
901 } else if (type === 'below') {
901 } else if (type === 'below') {
902 if (index < ncells) {
902 if (index < ncells) {
903 type = this.get_cell(index).cell_type;
903 type = this.get_cell(index).cell_type;
904 } else {
904 } else {
905 type = 'code';
905 type = 'code';
906 }
906 }
907 } else if (type === 'selected') {
907 } else if (type === 'selected') {
908 type = this.get_selected_cell().cell_type;
908 type = this.get_selected_cell().cell_type;
909 }
909 }
910
910
911 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
911 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
912 var cell_options = {
912 var cell_options = {
913 events: this.events,
913 events: this.events,
914 config: this.config,
914 config: this.config,
915 keyboard_manager: this.keyboard_manager,
915 keyboard_manager: this.keyboard_manager,
916 notebook: this,
916 notebook: this,
917 tooltip: this.tooltip
917 tooltip: this.tooltip
918 };
918 };
919 switch(type) {
919 switch(type) {
920 case 'code':
920 case 'code':
921 cell = new codecell.CodeCell(this.kernel, cell_options);
921 cell = new codecell.CodeCell(this.kernel, cell_options);
922 cell.set_input_prompt();
922 cell.set_input_prompt();
923 break;
923 break;
924 case 'markdown':
924 case 'markdown':
925 cell = new textcell.MarkdownCell(cell_options);
925 cell = new textcell.MarkdownCell(cell_options);
926 break;
926 break;
927 case 'raw':
927 case 'raw':
928 cell = new textcell.RawCell(cell_options);
928 cell = new textcell.RawCell(cell_options);
929 break;
929 break;
930 default:
930 default:
931 console.log("Unrecognized cell type: ", type, cellmod);
931 console.log("Unrecognized cell type: ", type, cellmod);
932 cell = new cellmod.UnrecognizedCell(cell_options);
932 cell = new cellmod.UnrecognizedCell(cell_options);
933 }
933 }
934
934
935 if(this._insert_element_at_index(cell.element,index)) {
935 if(this._insert_element_at_index(cell.element,index)) {
936 cell.render();
936 cell.render();
937 this.events.trigger('create.Cell', {'cell': cell, 'index': index});
937 this.events.trigger('create.Cell', {'cell': cell, 'index': index});
938 cell.refresh();
938 cell.refresh();
939 // We used to select the cell after we refresh it, but there
939 // We used to select the cell after we refresh it, but there
940 // are now cases were this method is called where select is
940 // are now cases were this method is called where select is
941 // not appropriate. The selection logic should be handled by the
941 // not appropriate. The selection logic should be handled by the
942 // caller of the the top level insert_cell methods.
942 // caller of the the top level insert_cell methods.
943 this.set_dirty(true);
943 this.set_dirty(true);
944 }
944 }
945 }
945 }
946 return cell;
946 return cell;
947
947
948 };
948 };
949
949
950 /**
950 /**
951 * Insert an element at given cell index.
951 * Insert an element at given cell index.
952 *
952 *
953 * @param {HTMLElement} element - a cell element
953 * @param {HTMLElement} element - a cell element
954 * @param {integer} [index] - a valid index where to inser cell
954 * @param {integer} [index] - a valid index where to inser cell
955 * @returns {boolean} success
955 * @returns {boolean} success
956 */
956 */
957 Notebook.prototype._insert_element_at_index = function(element, index){
957 Notebook.prototype._insert_element_at_index = function(element, index){
958 if (element === undefined){
958 if (element === undefined){
959 return false;
959 return false;
960 }
960 }
961
961
962 var ncells = this.ncells();
962 var ncells = this.ncells();
963
963
964 if (ncells === 0) {
964 if (ncells === 0) {
965 // special case append if empty
965 // special case append if empty
966 this.container.append(element);
966 this.container.append(element);
967 } else if ( ncells === index ) {
967 } else if ( ncells === index ) {
968 // special case append it the end, but not empty
968 // special case append it the end, but not empty
969 this.get_cell_element(index-1).after(element);
969 this.get_cell_element(index-1).after(element);
970 } else if (this.is_valid_cell_index(index)) {
970 } else if (this.is_valid_cell_index(index)) {
971 // otherwise always somewhere to append to
971 // otherwise always somewhere to append to
972 this.get_cell_element(index).before(element);
972 this.get_cell_element(index).before(element);
973 } else {
973 } else {
974 return false;
974 return false;
975 }
975 }
976
976
977 if (this.undelete_index !== null && index <= this.undelete_index) {
977 if (this.undelete_index !== null && index <= this.undelete_index) {
978 this.undelete_index = this.undelete_index + 1;
978 this.undelete_index = this.undelete_index + 1;
979 this.set_dirty(true);
979 this.set_dirty(true);
980 }
980 }
981 return true;
981 return true;
982 };
982 };
983
983
984 /**
984 /**
985 * Insert a cell of given type above given index, or at top
985 * Insert a cell of given type above given index, or at top
986 * of notebook if index smaller than 0.
986 * of notebook if index smaller than 0.
987 *
987 *
988 * @param {string} [type] - cell type
988 * @param {string} [type] - cell type
989 * @param {integer} [index] - defaults to the currently selected cell
989 * @param {integer} [index] - defaults to the currently selected cell
990 * @return {Cell|null} handle to created cell or null
990 * @return {Cell|null} handle to created cell or null
991 */
991 */
992 Notebook.prototype.insert_cell_above = function (type, index) {
992 Notebook.prototype.insert_cell_above = function (type, index) {
993 index = this.index_or_selected(index);
993 index = this.index_or_selected(index);
994 return this.insert_cell_at_index(type, index);
994 return this.insert_cell_at_index(type, index);
995 };
995 };
996
996
997 /**
997 /**
998 * Insert a cell of given type below given index, or at bottom
998 * Insert a cell of given type below given index, or at bottom
999 * of notebook if index greater than number of cells
999 * of notebook if index greater than number of cells
1000 *
1000 *
1001 * @param {string} [type] - cell type
1001 * @param {string} [type] - cell type
1002 * @param {integer} [index] - defaults to the currently selected cell
1002 * @param {integer} [index] - defaults to the currently selected cell
1003 * @return {Cell|null} handle to created cell or null
1003 * @return {Cell|null} handle to created cell or null
1004 */
1004 */
1005 Notebook.prototype.insert_cell_below = function (type, index) {
1005 Notebook.prototype.insert_cell_below = function (type, index) {
1006 index = this.index_or_selected(index);
1006 index = this.index_or_selected(index);
1007 return this.insert_cell_at_index(type, index+1);
1007 return this.insert_cell_at_index(type, index+1);
1008 };
1008 };
1009
1009
1010
1010
1011 /**
1011 /**
1012 * Insert cell at end of notebook
1012 * Insert cell at end of notebook
1013 *
1013 *
1014 * @param {string} type - cell type
1014 * @param {string} type - cell type
1015 * @return {Cell|null} handle to created cell or null
1015 * @return {Cell|null} handle to created cell or null
1016 */
1016 */
1017 Notebook.prototype.insert_cell_at_bottom = function (type){
1017 Notebook.prototype.insert_cell_at_bottom = function (type){
1018 var len = this.ncells();
1018 var len = this.ncells();
1019 return this.insert_cell_below(type,len-1);
1019 return this.insert_cell_below(type,len-1);
1020 };
1020 };
1021
1021
1022 /**
1022 /**
1023 * Turn a cell into a code cell.
1023 * Turn a cell into a code cell.
1024 *
1024 *
1025 * @param {integer} [index] - cell index
1025 * @param {integer} [index] - cell index
1026 */
1026 */
1027 Notebook.prototype.to_code = function (index) {
1027 Notebook.prototype.to_code = function (index) {
1028 var i = this.index_or_selected(index);
1028 var i = this.index_or_selected(index);
1029 if (this.is_valid_cell_index(i)) {
1029 if (this.is_valid_cell_index(i)) {
1030 var source_cell = this.get_cell(i);
1030 var source_cell = this.get_cell(i);
1031 if (!(source_cell instanceof codecell.CodeCell)) {
1031 if (!(source_cell instanceof codecell.CodeCell)) {
1032 var target_cell = this.insert_cell_below('code',i);
1032 var target_cell = this.insert_cell_below('code',i);
1033 var text = source_cell.get_text();
1033 var text = source_cell.get_text();
1034 if (text === source_cell.placeholder) {
1034 if (text === source_cell.placeholder) {
1035 text = '';
1035 text = '';
1036 }
1036 }
1037 //metadata
1037 //metadata
1038 target_cell.metadata = source_cell.metadata;
1038 target_cell.metadata = source_cell.metadata;
1039
1039
1040 target_cell.set_text(text);
1040 target_cell.set_text(text);
1041 // make this value the starting point, so that we can only undo
1041 // make this value the starting point, so that we can only undo
1042 // to this state, instead of a blank cell
1042 // to this state, instead of a blank cell
1043 target_cell.code_mirror.clearHistory();
1043 target_cell.code_mirror.clearHistory();
1044 source_cell.element.remove();
1044 source_cell.element.remove();
1045 this.select(i);
1045 this.select(i);
1046 var cursor = source_cell.code_mirror.getCursor();
1046 var cursor = source_cell.code_mirror.getCursor();
1047 target_cell.code_mirror.setCursor(cursor);
1047 target_cell.code_mirror.setCursor(cursor);
1048 this.set_dirty(true);
1048 this.set_dirty(true);
1049 }
1049 }
1050 }
1050 }
1051 };
1051 };
1052
1052
1053 /**
1053 /**
1054 * Turn a cell into a Markdown cell.
1054 * Turn a cell into a Markdown cell.
1055 *
1055 *
1056 * @param {integer} [index] - cell index
1056 * @param {integer} [index] - cell index
1057 */
1057 */
1058 Notebook.prototype.to_markdown = function (index) {
1058 Notebook.prototype.to_markdown = function (index) {
1059 var i = this.index_or_selected(index);
1059 var i = this.index_or_selected(index);
1060 if (this.is_valid_cell_index(i)) {
1060 if (this.is_valid_cell_index(i)) {
1061 var source_cell = this.get_cell(i);
1061 var source_cell = this.get_cell(i);
1062
1062
1063 if (!(source_cell instanceof textcell.MarkdownCell)) {
1063 if (!(source_cell instanceof textcell.MarkdownCell)) {
1064 var target_cell = this.insert_cell_below('markdown',i);
1064 var target_cell = this.insert_cell_below('markdown',i);
1065 var text = source_cell.get_text();
1065 var text = source_cell.get_text();
1066
1066
1067 if (text === source_cell.placeholder) {
1067 if (text === source_cell.placeholder) {
1068 text = '';
1068 text = '';
1069 }
1069 }
1070 // metadata
1070 // metadata
1071 target_cell.metadata = source_cell.metadata;
1071 target_cell.metadata = source_cell.metadata;
1072 // We must show the editor before setting its contents
1072 // We must show the editor before setting its contents
1073 target_cell.unrender();
1073 target_cell.unrender();
1074 target_cell.set_text(text);
1074 target_cell.set_text(text);
1075 // make this value the starting point, so that we can only undo
1075 // make this value the starting point, so that we can only undo
1076 // to this state, instead of a blank cell
1076 // to this state, instead of a blank cell
1077 target_cell.code_mirror.clearHistory();
1077 target_cell.code_mirror.clearHistory();
1078 source_cell.element.remove();
1078 source_cell.element.remove();
1079 this.select(i);
1079 this.select(i);
1080 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1080 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1081 target_cell.render();
1081 target_cell.render();
1082 }
1082 }
1083 var cursor = source_cell.code_mirror.getCursor();
1083 var cursor = source_cell.code_mirror.getCursor();
1084 target_cell.code_mirror.setCursor(cursor);
1084 target_cell.code_mirror.setCursor(cursor);
1085 this.set_dirty(true);
1085 this.set_dirty(true);
1086 }
1086 }
1087 }
1087 }
1088 };
1088 };
1089
1089
1090 /**
1090 /**
1091 * Turn a cell into a raw text cell.
1091 * Turn a cell into a raw text cell.
1092 *
1092 *
1093 * @param {integer} [index] - cell index
1093 * @param {integer} [index] - cell index
1094 */
1094 */
1095 Notebook.prototype.to_raw = function (index) {
1095 Notebook.prototype.to_raw = function (index) {
1096 var i = this.index_or_selected(index);
1096 var i = this.index_or_selected(index);
1097 if (this.is_valid_cell_index(i)) {
1097 if (this.is_valid_cell_index(i)) {
1098 var target_cell = null;
1098 var target_cell = null;
1099 var source_cell = this.get_cell(i);
1099 var source_cell = this.get_cell(i);
1100
1100
1101 if (!(source_cell instanceof textcell.RawCell)) {
1101 if (!(source_cell instanceof textcell.RawCell)) {
1102 target_cell = this.insert_cell_below('raw',i);
1102 target_cell = this.insert_cell_below('raw',i);
1103 var text = source_cell.get_text();
1103 var text = source_cell.get_text();
1104 if (text === source_cell.placeholder) {
1104 if (text === source_cell.placeholder) {
1105 text = '';
1105 text = '';
1106 }
1106 }
1107 //metadata
1107 //metadata
1108 target_cell.metadata = source_cell.metadata;
1108 target_cell.metadata = source_cell.metadata;
1109 // We must show the editor before setting its contents
1109 // We must show the editor before setting its contents
1110 target_cell.unrender();
1110 target_cell.unrender();
1111 target_cell.set_text(text);
1111 target_cell.set_text(text);
1112 // make this value the starting point, so that we can only undo
1112 // make this value the starting point, so that we can only undo
1113 // to this state, instead of a blank cell
1113 // to this state, instead of a blank cell
1114 target_cell.code_mirror.clearHistory();
1114 target_cell.code_mirror.clearHistory();
1115 source_cell.element.remove();
1115 source_cell.element.remove();
1116 this.select(i);
1116 this.select(i);
1117 var cursor = source_cell.code_mirror.getCursor();
1117 var cursor = source_cell.code_mirror.getCursor();
1118 target_cell.code_mirror.setCursor(cursor);
1118 target_cell.code_mirror.setCursor(cursor);
1119 this.set_dirty(true);
1119 this.set_dirty(true);
1120 }
1120 }
1121 }
1121 }
1122 };
1122 };
1123
1123
1124 /**
1124 /**
1125 * Warn about heading cell support removal.
1125 * Warn about heading cell support removal.
1126 */
1126 */
1127 Notebook.prototype._warn_heading = function () {
1127 Notebook.prototype._warn_heading = function () {
1128 dialog.modal({
1128 dialog.modal({
1129 notebook: this,
1129 notebook: this,
1130 keyboard_manager: this.keyboard_manager,
1130 keyboard_manager: this.keyboard_manager,
1131 title : "Use markdown headings",
1131 title : "Use markdown headings",
1132 body : $("<p/>").text(
1132 body : $("<p/>").text(
1133 'IPython no longer uses special heading cells. ' +
1133 'IPython no longer uses special heading cells. ' +
1134 'Instead, write your headings in Markdown cells using # characters:'
1134 'Instead, write your headings in Markdown cells using # characters:'
1135 ).append($('<pre/>').text(
1135 ).append($('<pre/>').text(
1136 '## This is a level 2 heading'
1136 '## This is a level 2 heading'
1137 )),
1137 )),
1138 buttons : {
1138 buttons : {
1139 "OK" : {}
1139 "OK" : {}
1140 }
1140 }
1141 });
1141 });
1142 };
1142 };
1143
1143
1144 /**
1144 /**
1145 * Turn a cell into a heading containing markdown cell.
1145 * Turn a cell into a heading containing markdown cell.
1146 *
1146 *
1147 * @param {integer} [index] - cell index
1147 * @param {integer} [index] - cell index
1148 * @param {integer} [level] - heading level (e.g., 1 for h1)
1148 * @param {integer} [level] - heading level (e.g., 1 for h1)
1149 */
1149 */
1150 Notebook.prototype.to_heading = function (index, level) {
1150 Notebook.prototype.to_heading = function (index, level) {
1151 this.to_markdown(index);
1151 this.to_markdown(index);
1152 level = level || 1;
1152 level = level || 1;
1153 var i = this.index_or_selected(index);
1153 var i = this.index_or_selected(index);
1154 if (this.is_valid_cell_index(i)) {
1154 if (this.is_valid_cell_index(i)) {
1155 var cell = this.get_cell(i);
1155 var cell = this.get_cell(i);
1156 cell.set_heading_level(level);
1156 cell.set_heading_level(level);
1157 this.set_dirty(true);
1157 this.set_dirty(true);
1158 }
1158 }
1159 };
1159 };
1160
1160
1161
1161
1162 // Cut/Copy/Paste
1162 // Cut/Copy/Paste
1163
1163
1164 /**
1164 /**
1165 * Enable the UI elements for pasting cells.
1165 * Enable the UI elements for pasting cells.
1166 */
1166 */
1167 Notebook.prototype.enable_paste = function () {
1167 Notebook.prototype.enable_paste = function () {
1168 var that = this;
1168 var that = this;
1169 if (!this.paste_enabled) {
1169 if (!this.paste_enabled) {
1170 $('#paste_cell_replace').removeClass('disabled')
1170 $('#paste_cell_replace').removeClass('disabled')
1171 .on('click', function () {that.paste_cell_replace();});
1171 .on('click', function () {that.paste_cell_replace();});
1172 $('#paste_cell_above').removeClass('disabled')
1172 $('#paste_cell_above').removeClass('disabled')
1173 .on('click', function () {that.paste_cell_above();});
1173 .on('click', function () {that.paste_cell_above();});
1174 $('#paste_cell_below').removeClass('disabled')
1174 $('#paste_cell_below').removeClass('disabled')
1175 .on('click', function () {that.paste_cell_below();});
1175 .on('click', function () {that.paste_cell_below();});
1176 this.paste_enabled = true;
1176 this.paste_enabled = true;
1177 }
1177 }
1178 };
1178 };
1179
1179
1180 /**
1180 /**
1181 * Disable the UI elements for pasting cells.
1181 * Disable the UI elements for pasting cells.
1182 */
1182 */
1183 Notebook.prototype.disable_paste = function () {
1183 Notebook.prototype.disable_paste = function () {
1184 if (this.paste_enabled) {
1184 if (this.paste_enabled) {
1185 $('#paste_cell_replace').addClass('disabled').off('click');
1185 $('#paste_cell_replace').addClass('disabled').off('click');
1186 $('#paste_cell_above').addClass('disabled').off('click');
1186 $('#paste_cell_above').addClass('disabled').off('click');
1187 $('#paste_cell_below').addClass('disabled').off('click');
1187 $('#paste_cell_below').addClass('disabled').off('click');
1188 this.paste_enabled = false;
1188 this.paste_enabled = false;
1189 }
1189 }
1190 };
1190 };
1191
1191
1192 /**
1192 /**
1193 * Cut a cell.
1193 * Cut a cell.
1194 */
1194 */
1195 Notebook.prototype.cut_cell = function () {
1195 Notebook.prototype.cut_cell = function () {
1196 this.copy_cell();
1196 this.copy_cell();
1197 this.delete_cell();
1197 this.delete_cell();
1198 };
1198 };
1199
1199
1200 /**
1200 /**
1201 * Copy a cell.
1201 * Copy a cell.
1202 */
1202 */
1203 Notebook.prototype.copy_cell = function () {
1203 Notebook.prototype.copy_cell = function () {
1204 var cell = this.get_selected_cell();
1204 var cell = this.get_selected_cell();
1205 this.clipboard = cell.toJSON();
1205 this.clipboard = cell.toJSON();
1206 // remove undeletable status from the copied cell
1206 // remove undeletable status from the copied cell
1207 if (this.clipboard.metadata.deletable !== undefined) {
1207 if (this.clipboard.metadata.deletable !== undefined) {
1208 delete this.clipboard.metadata.deletable;
1208 delete this.clipboard.metadata.deletable;
1209 }
1209 }
1210 this.enable_paste();
1210 this.enable_paste();
1211 };
1211 };
1212
1212
1213 /**
1213 /**
1214 * Replace the selected cell with the cell in the clipboard.
1214 * Replace the selected cell with the cell in the clipboard.
1215 */
1215 */
1216 Notebook.prototype.paste_cell_replace = function () {
1216 Notebook.prototype.paste_cell_replace = function () {
1217 if (this.clipboard !== null && this.paste_enabled) {
1217 if (this.clipboard !== null && this.paste_enabled) {
1218 var cell_data = this.clipboard;
1218 var cell_data = this.clipboard;
1219 var new_cell = this.insert_cell_above(cell_data.cell_type);
1219 var new_cell = this.insert_cell_above(cell_data.cell_type);
1220 new_cell.fromJSON(cell_data);
1220 new_cell.fromJSON(cell_data);
1221 var old_cell = this.get_next_cell(new_cell);
1221 var old_cell = this.get_next_cell(new_cell);
1222 this.delete_cell(this.find_cell_index(old_cell));
1222 this.delete_cell(this.find_cell_index(old_cell));
1223 this.select(this.find_cell_index(new_cell));
1223 this.select(this.find_cell_index(new_cell));
1224 }
1224 }
1225 };
1225 };
1226
1226
1227 /**
1227 /**
1228 * Paste a cell from the clipboard above the selected cell.
1228 * Paste a cell from the clipboard above the selected cell.
1229 */
1229 */
1230 Notebook.prototype.paste_cell_above = function () {
1230 Notebook.prototype.paste_cell_above = function () {
1231 if (this.clipboard !== null && this.paste_enabled) {
1231 if (this.clipboard !== null && this.paste_enabled) {
1232 var cell_data = this.clipboard;
1232 var cell_data = this.clipboard;
1233 var new_cell = this.insert_cell_above(cell_data.cell_type);
1233 var new_cell = this.insert_cell_above(cell_data.cell_type);
1234 new_cell.fromJSON(cell_data);
1234 new_cell.fromJSON(cell_data);
1235 new_cell.focus_cell();
1235 new_cell.focus_cell();
1236 }
1236 }
1237 };
1237 };
1238
1238
1239 /**
1239 /**
1240 * Paste a cell from the clipboard below the selected cell.
1240 * Paste a cell from the clipboard below the selected cell.
1241 */
1241 */
1242 Notebook.prototype.paste_cell_below = function () {
1242 Notebook.prototype.paste_cell_below = function () {
1243 if (this.clipboard !== null && this.paste_enabled) {
1243 if (this.clipboard !== null && this.paste_enabled) {
1244 var cell_data = this.clipboard;
1244 var cell_data = this.clipboard;
1245 var new_cell = this.insert_cell_below(cell_data.cell_type);
1245 var new_cell = this.insert_cell_below(cell_data.cell_type);
1246 new_cell.fromJSON(cell_data);
1246 new_cell.fromJSON(cell_data);
1247 new_cell.focus_cell();
1247 new_cell.focus_cell();
1248 }
1248 }
1249 };
1249 };
1250
1250
1251 // Split/merge
1251 // Split/merge
1252
1252
1253 /**
1253 /**
1254 * Split the selected cell into two cells.
1254 * Split the selected cell into two cells.
1255 */
1255 */
1256 Notebook.prototype.split_cell = function () {
1256 Notebook.prototype.split_cell = function () {
1257 var cell = this.get_selected_cell();
1257 var cell = this.get_selected_cell();
1258 if (cell.is_splittable()) {
1258 if (cell.is_splittable()) {
1259 var texta = cell.get_pre_cursor();
1259 var texta = cell.get_pre_cursor();
1260 var textb = cell.get_post_cursor();
1260 var textb = cell.get_post_cursor();
1261 cell.set_text(textb);
1261 cell.set_text(textb);
1262 var new_cell = this.insert_cell_above(cell.cell_type);
1262 var new_cell = this.insert_cell_above(cell.cell_type);
1263 // Unrender the new cell so we can call set_text.
1263 // Unrender the new cell so we can call set_text.
1264 new_cell.unrender();
1264 new_cell.unrender();
1265 new_cell.set_text(texta);
1265 new_cell.set_text(texta);
1266 }
1266 }
1267 };
1267 };
1268
1268
1269 /**
1269 /**
1270 * Merge the selected cell into the cell above it.
1270 * Merge the selected cell into the cell above it.
1271 */
1271 */
1272 Notebook.prototype.merge_cell_above = function () {
1272 Notebook.prototype.merge_cell_above = function () {
1273 var index = this.get_selected_index();
1273 var index = this.get_selected_index();
1274 var cell = this.get_cell(index);
1274 var cell = this.get_cell(index);
1275 var render = cell.rendered;
1275 var render = cell.rendered;
1276 if (!cell.is_mergeable()) {
1276 if (!cell.is_mergeable()) {
1277 return;
1277 return;
1278 }
1278 }
1279 if (index > 0) {
1279 if (index > 0) {
1280 var upper_cell = this.get_cell(index-1);
1280 var upper_cell = this.get_cell(index-1);
1281 if (!upper_cell.is_mergeable()) {
1281 if (!upper_cell.is_mergeable()) {
1282 return;
1282 return;
1283 }
1283 }
1284 var upper_text = upper_cell.get_text();
1284 var upper_text = upper_cell.get_text();
1285 var text = cell.get_text();
1285 var text = cell.get_text();
1286 if (cell instanceof codecell.CodeCell) {
1286 if (cell instanceof codecell.CodeCell) {
1287 cell.set_text(upper_text+'\n'+text);
1287 cell.set_text(upper_text+'\n'+text);
1288 } else {
1288 } else {
1289 cell.unrender(); // Must unrender before we set_text.
1289 cell.unrender(); // Must unrender before we set_text.
1290 cell.set_text(upper_text+'\n\n'+text);
1290 cell.set_text(upper_text+'\n\n'+text);
1291 if (render) {
1291 if (render) {
1292 // The rendered state of the final cell should match
1292 // The rendered state of the final cell should match
1293 // that of the original selected cell;
1293 // that of the original selected cell;
1294 cell.render();
1294 cell.render();
1295 }
1295 }
1296 }
1296 }
1297 this.delete_cell(index-1);
1297 this.delete_cell(index-1);
1298 this.select(this.find_cell_index(cell));
1298 this.select(this.find_cell_index(cell));
1299 }
1299 }
1300 };
1300 };
1301
1301
1302 /**
1302 /**
1303 * Merge the selected cell into the cell below it.
1303 * Merge the selected cell into the cell below it.
1304 */
1304 */
1305 Notebook.prototype.merge_cell_below = function () {
1305 Notebook.prototype.merge_cell_below = function () {
1306 var index = this.get_selected_index();
1306 var index = this.get_selected_index();
1307 var cell = this.get_cell(index);
1307 var cell = this.get_cell(index);
1308 var render = cell.rendered;
1308 var render = cell.rendered;
1309 if (!cell.is_mergeable()) {
1309 if (!cell.is_mergeable()) {
1310 return;
1310 return;
1311 }
1311 }
1312 if (index < this.ncells()-1) {
1312 if (index < this.ncells()-1) {
1313 var lower_cell = this.get_cell(index+1);
1313 var lower_cell = this.get_cell(index+1);
1314 if (!lower_cell.is_mergeable()) {
1314 if (!lower_cell.is_mergeable()) {
1315 return;
1315 return;
1316 }
1316 }
1317 var lower_text = lower_cell.get_text();
1317 var lower_text = lower_cell.get_text();
1318 var text = cell.get_text();
1318 var text = cell.get_text();
1319 if (cell instanceof codecell.CodeCell) {
1319 if (cell instanceof codecell.CodeCell) {
1320 cell.set_text(text+'\n'+lower_text);
1320 cell.set_text(text+'\n'+lower_text);
1321 } else {
1321 } else {
1322 cell.unrender(); // Must unrender before we set_text.
1322 cell.unrender(); // Must unrender before we set_text.
1323 cell.set_text(text+'\n\n'+lower_text);
1323 cell.set_text(text+'\n\n'+lower_text);
1324 if (render) {
1324 if (render) {
1325 // The rendered state of the final cell should match
1325 // The rendered state of the final cell should match
1326 // that of the original selected cell;
1326 // that of the original selected cell;
1327 cell.render();
1327 cell.render();
1328 }
1328 }
1329 }
1329 }
1330 this.delete_cell(index+1);
1330 this.delete_cell(index+1);
1331 this.select(this.find_cell_index(cell));
1331 this.select(this.find_cell_index(cell));
1332 }
1332 }
1333 };
1333 };
1334
1334
1335
1335
1336 // Cell collapsing and output clearing
1336 // Cell collapsing and output clearing
1337
1337
1338 /**
1338 /**
1339 * Hide a cell's output.
1339 * Hide a cell's output.
1340 *
1340 *
1341 * @param {integer} index - cell index
1341 * @param {integer} index - cell index
1342 */
1342 */
1343 Notebook.prototype.collapse_output = function (index) {
1343 Notebook.prototype.collapse_output = function (index) {
1344 var i = this.index_or_selected(index);
1344 var i = this.index_or_selected(index);
1345 var cell = this.get_cell(i);
1345 var cell = this.get_cell(i);
1346 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1346 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1347 cell.collapse_output();
1347 cell.collapse_output();
1348 this.set_dirty(true);
1348 this.set_dirty(true);
1349 }
1349 }
1350 };
1350 };
1351
1351
1352 /**
1352 /**
1353 * Hide each code cell's output area.
1353 * Hide each code cell's output area.
1354 */
1354 */
1355 Notebook.prototype.collapse_all_output = function () {
1355 Notebook.prototype.collapse_all_output = function () {
1356 this.get_cells().map(function (cell, i) {
1356 this.get_cells().map(function (cell, i) {
1357 if (cell instanceof codecell.CodeCell) {
1357 if (cell instanceof codecell.CodeCell) {
1358 cell.collapse_output();
1358 cell.collapse_output();
1359 }
1359 }
1360 });
1360 });
1361 // this should not be set if the `collapse` key is removed from nbformat
1361 // this should not be set if the `collapse` key is removed from nbformat
1362 this.set_dirty(true);
1362 this.set_dirty(true);
1363 };
1363 };
1364
1364
1365 /**
1365 /**
1366 * Show a cell's output.
1366 * Show a cell's output.
1367 *
1367 *
1368 * @param {integer} index - cell index
1368 * @param {integer} index - cell index
1369 */
1369 */
1370 Notebook.prototype.expand_output = function (index) {
1370 Notebook.prototype.expand_output = function (index) {
1371 var i = this.index_or_selected(index);
1371 var i = this.index_or_selected(index);
1372 var cell = this.get_cell(i);
1372 var cell = this.get_cell(i);
1373 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1373 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1374 cell.expand_output();
1374 cell.expand_output();
1375 this.set_dirty(true);
1375 this.set_dirty(true);
1376 }
1376 }
1377 };
1377 };
1378
1378
1379 /**
1379 /**
1380 * Expand each code cell's output area, and remove scrollbars.
1380 * Expand each code cell's output area, and remove scrollbars.
1381 */
1381 */
1382 Notebook.prototype.expand_all_output = function () {
1382 Notebook.prototype.expand_all_output = function () {
1383 this.get_cells().map(function (cell, i) {
1383 this.get_cells().map(function (cell, i) {
1384 if (cell instanceof codecell.CodeCell) {
1384 if (cell instanceof codecell.CodeCell) {
1385 cell.expand_output();
1385 cell.expand_output();
1386 }
1386 }
1387 });
1387 });
1388 // this should not be set if the `collapse` key is removed from nbformat
1388 // this should not be set if the `collapse` key is removed from nbformat
1389 this.set_dirty(true);
1389 this.set_dirty(true);
1390 };
1390 };
1391
1391
1392 /**
1392 /**
1393 * Clear the selected CodeCell's output area.
1393 * Clear the selected CodeCell's output area.
1394 *
1394 *
1395 * @param {integer} index - cell index
1395 * @param {integer} index - cell index
1396 */
1396 */
1397 Notebook.prototype.clear_output = function (index) {
1397 Notebook.prototype.clear_output = function (index) {
1398 var i = this.index_or_selected(index);
1398 var i = this.index_or_selected(index);
1399 var cell = this.get_cell(i);
1399 var cell = this.get_cell(i);
1400 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1400 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1401 cell.clear_output();
1401 cell.clear_output();
1402 this.set_dirty(true);
1402 this.set_dirty(true);
1403 }
1403 }
1404 };
1404 };
1405
1405
1406 /**
1406 /**
1407 * Clear each code cell's output area.
1407 * Clear each code cell's output area.
1408 */
1408 */
1409 Notebook.prototype.clear_all_output = function () {
1409 Notebook.prototype.clear_all_output = function () {
1410 this.get_cells().map(function (cell, i) {
1410 this.get_cells().map(function (cell, i) {
1411 if (cell instanceof codecell.CodeCell) {
1411 if (cell instanceof codecell.CodeCell) {
1412 cell.clear_output();
1412 cell.clear_output();
1413 }
1413 }
1414 });
1414 });
1415 this.set_dirty(true);
1415 this.set_dirty(true);
1416 };
1416 };
1417
1417
1418 /**
1418 /**
1419 * Scroll the selected CodeCell's output area.
1419 * Scroll the selected CodeCell's output area.
1420 *
1420 *
1421 * @param {integer} index - cell index
1421 * @param {integer} index - cell index
1422 */
1422 */
1423 Notebook.prototype.scroll_output = function (index) {
1423 Notebook.prototype.scroll_output = function (index) {
1424 var i = this.index_or_selected(index);
1424 var i = this.index_or_selected(index);
1425 var cell = this.get_cell(i);
1425 var cell = this.get_cell(i);
1426 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1426 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1427 cell.scroll_output();
1427 cell.scroll_output();
1428 this.set_dirty(true);
1428 this.set_dirty(true);
1429 }
1429 }
1430 };
1430 };
1431
1431
1432 /**
1432 /**
1433 * Expand each code cell's output area and add a scrollbar for long output.
1433 * Expand each code cell's output area and add a scrollbar for long output.
1434 */
1434 */
1435 Notebook.prototype.scroll_all_output = function () {
1435 Notebook.prototype.scroll_all_output = function () {
1436 this.get_cells().map(function (cell, i) {
1436 this.get_cells().map(function (cell, i) {
1437 if (cell instanceof codecell.CodeCell) {
1437 if (cell instanceof codecell.CodeCell) {
1438 cell.scroll_output();
1438 cell.scroll_output();
1439 }
1439 }
1440 });
1440 });
1441 // this should not be set if the `collapse` key is removed from nbformat
1441 // this should not be set if the `collapse` key is removed from nbformat
1442 this.set_dirty(true);
1442 this.set_dirty(true);
1443 };
1443 };
1444
1444
1445 /**
1445 /**
1446 * Toggle whether a cell's output is collapsed or expanded.
1446 * Toggle whether a cell's output is collapsed or expanded.
1447 *
1447 *
1448 * @param {integer} index - cell index
1448 * @param {integer} index - cell index
1449 */
1449 */
1450 Notebook.prototype.toggle_output = function (index) {
1450 Notebook.prototype.toggle_output = function (index) {
1451 var i = this.index_or_selected(index);
1451 var i = this.index_or_selected(index);
1452 var cell = this.get_cell(i);
1452 var cell = this.get_cell(i);
1453 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1453 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1454 cell.toggle_output();
1454 cell.toggle_output();
1455 this.set_dirty(true);
1455 this.set_dirty(true);
1456 }
1456 }
1457 };
1457 };
1458
1458
1459 /**
1459 /**
1460 * Toggle the output of all cells.
1460 * Toggle the output of all cells.
1461 */
1461 */
1462 Notebook.prototype.toggle_all_output = function () {
1462 Notebook.prototype.toggle_all_output = function () {
1463 this.get_cells().map(function (cell, i) {
1463 this.get_cells().map(function (cell, i) {
1464 if (cell instanceof codecell.CodeCell) {
1464 if (cell instanceof codecell.CodeCell) {
1465 cell.toggle_output();
1465 cell.toggle_output();
1466 }
1466 }
1467 });
1467 });
1468 // this should not be set if the `collapse` key is removed from nbformat
1468 // this should not be set if the `collapse` key is removed from nbformat
1469 this.set_dirty(true);
1469 this.set_dirty(true);
1470 };
1470 };
1471
1471
1472 /**
1472 /**
1473 * Toggle a scrollbar for long cell outputs.
1473 * Toggle a scrollbar for long cell outputs.
1474 *
1474 *
1475 * @param {integer} index - cell index
1475 * @param {integer} index - cell index
1476 */
1476 */
1477 Notebook.prototype.toggle_output_scroll = function (index) {
1477 Notebook.prototype.toggle_output_scroll = function (index) {
1478 var i = this.index_or_selected(index);
1478 var i = this.index_or_selected(index);
1479 var cell = this.get_cell(i);
1479 var cell = this.get_cell(i);
1480 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1480 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1481 cell.toggle_output_scroll();
1481 cell.toggle_output_scroll();
1482 this.set_dirty(true);
1482 this.set_dirty(true);
1483 }
1483 }
1484 };
1484 };
1485
1485
1486 /**
1486 /**
1487 * Toggle the scrolling of long output on all cells.
1487 * Toggle the scrolling of long output on all cells.
1488 */
1488 */
1489 Notebook.prototype.toggle_all_output_scroll = function () {
1489 Notebook.prototype.toggle_all_output_scroll = function () {
1490 this.get_cells().map(function (cell, i) {
1490 this.get_cells().map(function (cell, i) {
1491 if (cell instanceof codecell.CodeCell) {
1491 if (cell instanceof codecell.CodeCell) {
1492 cell.toggle_output_scroll();
1492 cell.toggle_output_scroll();
1493 }
1493 }
1494 });
1494 });
1495 // this should not be set if the `collapse` key is removed from nbformat
1495 // this should not be set if the `collapse` key is removed from nbformat
1496 this.set_dirty(true);
1496 this.set_dirty(true);
1497 };
1497 };
1498
1498
1499 // Other cell functions: line numbers, ...
1499 // Other cell functions: line numbers, ...
1500
1500
1501 /**
1501 /**
1502 * Toggle line numbers in the selected cell's input area.
1502 * Toggle line numbers in the selected cell's input area.
1503 */
1503 */
1504 Notebook.prototype.cell_toggle_line_numbers = function() {
1504 Notebook.prototype.cell_toggle_line_numbers = function() {
1505 this.get_selected_cell().toggle_line_numbers();
1505 this.get_selected_cell().toggle_line_numbers();
1506 };
1506 };
1507
1507
1508 /**
1508 /**
1509 * Set the codemirror mode for all code cells, including the default for
1509 * Set the codemirror mode for all code cells, including the default for
1510 * new code cells.
1510 * new code cells.
1511 */
1511 */
1512 Notebook.prototype.set_codemirror_mode = function(newmode){
1512 Notebook.prototype.set_codemirror_mode = function(newmode){
1513 if (newmode === this.codemirror_mode) {
1513 if (newmode === this.codemirror_mode) {
1514 return;
1514 return;
1515 }
1515 }
1516 this.codemirror_mode = newmode;
1516 this.codemirror_mode = newmode;
1517 codecell.CodeCell.options_default.cm_config.mode = newmode;
1517 codecell.CodeCell.options_default.cm_config.mode = newmode;
1518
1518
1519 var that = this;
1519 var that = this;
1520 utils.requireCodeMirrorMode(newmode, function (spec) {
1520 utils.requireCodeMirrorMode(newmode, function (spec) {
1521 that.get_cells().map(function(cell, i) {
1521 that.get_cells().map(function(cell, i) {
1522 if (cell.cell_type === 'code'){
1522 if (cell.cell_type === 'code'){
1523 cell.code_mirror.setOption('mode', spec);
1523 cell.code_mirror.setOption('mode', spec);
1524 // This is currently redundant, because cm_config ends up as
1524 // This is currently redundant, because cm_config ends up as
1525 // codemirror's own .options object, but I don't want to
1525 // codemirror's own .options object, but I don't want to
1526 // rely on that.
1526 // rely on that.
1527 cell.cm_config.mode = spec;
1527 cell.cm_config.mode = spec;
1528 }
1528 }
1529 });
1529 });
1530 });
1530 });
1531 };
1531 };
1532
1532
1533 // Session related things
1533 // Session related things
1534
1534
1535 /**
1535 /**
1536 * Start a new session and set it on each code cell.
1536 * Start a new session and set it on each code cell.
1537 */
1537 */
1538 Notebook.prototype.start_session = function (kernel_name) {
1538 Notebook.prototype.start_session = function (kernel_name) {
1539 if (this._session_starting) {
1539 if (this._session_starting) {
1540 throw new session.SessionAlreadyStarting();
1540 throw new session.SessionAlreadyStarting();
1541 }
1541 }
1542 this._session_starting = true;
1542 this._session_starting = true;
1543
1543
1544 var options = {
1544 var options = {
1545 base_url: this.base_url,
1545 base_url: this.base_url,
1546 ws_url: this.ws_url,
1546 ws_url: this.ws_url,
1547 notebook_path: this.notebook_path,
1547 notebook_path: this.notebook_path,
1548 notebook_name: this.notebook_name,
1548 notebook_name: this.notebook_name,
1549 kernel_name: kernel_name,
1549 kernel_name: kernel_name,
1550 notebook: this
1550 notebook: this
1551 };
1551 };
1552
1552
1553 var success = $.proxy(this._session_started, this);
1553 var success = $.proxy(this._session_started, this);
1554 var failure = $.proxy(this._session_start_failed, this);
1554 var failure = $.proxy(this._session_start_failed, this);
1555
1555
1556 if (this.session !== null) {
1556 if (this.session !== null) {
1557 this.session.restart(options, success, failure);
1557 this.session.restart(options, success, failure);
1558 } else {
1558 } else {
1559 this.session = new session.Session(options);
1559 this.session = new session.Session(options);
1560 this.session.start(success, failure);
1560 this.session.start(success, failure);
1561 }
1561 }
1562 };
1562 };
1563
1563
1564
1564
1565 /**
1565 /**
1566 * Once a session is started, link the code cells to the kernel and pass the
1566 * Once a session is started, link the code cells to the kernel and pass the
1567 * comm manager to the widget manager.
1567 * comm manager to the widget manager.
1568 */
1568 */
1569 Notebook.prototype._session_started = function (){
1569 Notebook.prototype._session_started = function (){
1570 this._session_starting = false;
1570 this._session_starting = false;
1571 this.kernel = this.session.kernel;
1571 this.kernel = this.session.kernel;
1572 var ncells = this.ncells();
1572 var ncells = this.ncells();
1573 for (var i=0; i<ncells; i++) {
1573 for (var i=0; i<ncells; i++) {
1574 var cell = this.get_cell(i);
1574 var cell = this.get_cell(i);
1575 if (cell instanceof codecell.CodeCell) {
1575 if (cell instanceof codecell.CodeCell) {
1576 cell.set_kernel(this.session.kernel);
1576 cell.set_kernel(this.session.kernel);
1577 }
1577 }
1578 }
1578 }
1579 };
1579 };
1580
1580
1581 /**
1581 /**
1582 * Called when the session fails to start.
1582 * Called when the session fails to start.
1583 */
1583 */
1584 Notebook.prototype._session_start_failed = function(jqxhr, status, error){
1584 Notebook.prototype._session_start_failed = function(jqxhr, status, error){
1585 this._session_starting = false;
1585 this._session_starting = false;
1586 utils.log_ajax_error(jqxhr, status, error);
1586 utils.log_ajax_error(jqxhr, status, error);
1587 };
1587 };
1588
1588
1589 /**
1589 /**
1590 * Prompt the user to restart the IPython kernel.
1590 * Prompt the user to restart the IPython kernel.
1591 */
1591 */
1592 Notebook.prototype.restart_kernel = function () {
1592 Notebook.prototype.restart_kernel = function () {
1593 var that = this;
1593 var that = this;
1594 dialog.modal({
1594 dialog.modal({
1595 notebook: this,
1595 notebook: this,
1596 keyboard_manager: this.keyboard_manager,
1596 keyboard_manager: this.keyboard_manager,
1597 title : "Restart kernel or continue running?",
1597 title : "Restart kernel or continue running?",
1598 body : $("<p/>").text(
1598 body : $("<p/>").text(
1599 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1599 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1600 ),
1600 ),
1601 buttons : {
1601 buttons : {
1602 "Continue running" : {},
1602 "Continue running" : {},
1603 "Restart" : {
1603 "Restart" : {
1604 "class" : "btn-danger",
1604 "class" : "btn-danger",
1605 "click" : function() {
1605 "click" : function() {
1606 that.kernel.restart();
1606 that.kernel.restart();
1607 }
1607 }
1608 }
1608 }
1609 }
1609 }
1610 });
1610 });
1611 };
1611 };
1612
1612
1613 /**
1613 /**
1614 * Execute or render cell outputs and go into command mode.
1614 * Execute or render cell outputs and go into command mode.
1615 */
1615 */
1616 Notebook.prototype.execute_cell = function () {
1616 Notebook.prototype.execute_cell = function () {
1617 // mode = shift, ctrl, alt
1617 // mode = shift, ctrl, alt
1618 var cell = this.get_selected_cell();
1618 var cell = this.get_selected_cell();
1619
1619
1620 cell.execute();
1620 cell.execute();
1621 this.command_mode();
1621 this.command_mode();
1622 this.set_dirty(true);
1622 this.set_dirty(true);
1623 };
1623 };
1624
1624
1625 /**
1625 /**
1626 * Execute or render cell outputs and insert a new cell below.
1626 * Execute or render cell outputs and insert a new cell below.
1627 */
1627 */
1628 Notebook.prototype.execute_cell_and_insert_below = function () {
1628 Notebook.prototype.execute_cell_and_insert_below = function () {
1629 var cell = this.get_selected_cell();
1629 var cell = this.get_selected_cell();
1630 var cell_index = this.find_cell_index(cell);
1630 var cell_index = this.find_cell_index(cell);
1631
1631
1632 cell.execute();
1632 cell.execute();
1633
1633
1634 // If we are at the end always insert a new cell and return
1634 // If we are at the end always insert a new cell and return
1635 if (cell_index === (this.ncells()-1)) {
1635 if (cell_index === (this.ncells()-1)) {
1636 this.command_mode();
1636 this.command_mode();
1637 this.insert_cell_below();
1637 this.insert_cell_below();
1638 this.select(cell_index+1);
1638 this.select(cell_index+1);
1639 this.edit_mode();
1639 this.edit_mode();
1640 this.scroll_to_bottom();
1640 this.scroll_to_bottom();
1641 this.set_dirty(true);
1641 this.set_dirty(true);
1642 return;
1642 return;
1643 }
1643 }
1644
1644
1645 this.command_mode();
1645 this.command_mode();
1646 this.insert_cell_below();
1646 this.insert_cell_below();
1647 this.select(cell_index+1);
1647 this.select(cell_index+1);
1648 this.edit_mode();
1648 this.edit_mode();
1649 this.set_dirty(true);
1649 this.set_dirty(true);
1650 };
1650 };
1651
1651
1652 /**
1652 /**
1653 * Execute or render cell outputs and select the next cell.
1653 * Execute or render cell outputs and select the next cell.
1654 */
1654 */
1655 Notebook.prototype.execute_cell_and_select_below = function () {
1655 Notebook.prototype.execute_cell_and_select_below = function () {
1656
1656
1657 var cell = this.get_selected_cell();
1657 var cell = this.get_selected_cell();
1658 var cell_index = this.find_cell_index(cell);
1658 var cell_index = this.find_cell_index(cell);
1659
1659
1660 cell.execute();
1660 cell.execute();
1661
1661
1662 // If we are at the end always insert a new cell and return
1662 // If we are at the end always insert a new cell and return
1663 if (cell_index === (this.ncells()-1)) {
1663 if (cell_index === (this.ncells()-1)) {
1664 this.command_mode();
1664 this.command_mode();
1665 this.insert_cell_below();
1665 this.insert_cell_below();
1666 this.select(cell_index+1);
1666 this.select(cell_index+1);
1667 this.edit_mode();
1667 this.edit_mode();
1668 this.scroll_to_bottom();
1668 this.scroll_to_bottom();
1669 this.set_dirty(true);
1669 this.set_dirty(true);
1670 return;
1670 return;
1671 }
1671 }
1672
1672
1673 this.command_mode();
1673 this.command_mode();
1674 this.select(cell_index+1);
1674 this.select(cell_index+1);
1675 this.focus_cell();
1675 this.focus_cell();
1676 this.set_dirty(true);
1676 this.set_dirty(true);
1677 };
1677 };
1678
1678
1679 /**
1679 /**
1680 * Execute all cells below the selected cell.
1680 * Execute all cells below the selected cell.
1681 */
1681 */
1682 Notebook.prototype.execute_cells_below = function () {
1682 Notebook.prototype.execute_cells_below = function () {
1683 this.execute_cell_range(this.get_selected_index(), this.ncells());
1683 this.execute_cell_range(this.get_selected_index(), this.ncells());
1684 this.scroll_to_bottom();
1684 this.scroll_to_bottom();
1685 };
1685 };
1686
1686
1687 /**
1687 /**
1688 * Execute all cells above the selected cell.
1688 * Execute all cells above the selected cell.
1689 */
1689 */
1690 Notebook.prototype.execute_cells_above = function () {
1690 Notebook.prototype.execute_cells_above = function () {
1691 this.execute_cell_range(0, this.get_selected_index());
1691 this.execute_cell_range(0, this.get_selected_index());
1692 };
1692 };
1693
1693
1694 /**
1694 /**
1695 * Execute all cells.
1695 * Execute all cells.
1696 */
1696 */
1697 Notebook.prototype.execute_all_cells = function () {
1697 Notebook.prototype.execute_all_cells = function () {
1698 this.execute_cell_range(0, this.ncells());
1698 this.execute_cell_range(0, this.ncells());
1699 this.scroll_to_bottom();
1699 this.scroll_to_bottom();
1700 };
1700 };
1701
1701
1702 /**
1702 /**
1703 * Execute a contiguous range of cells.
1703 * Execute a contiguous range of cells.
1704 *
1704 *
1705 * @param {integer} start - index of the first cell to execute (inclusive)
1705 * @param {integer} start - index of the first cell to execute (inclusive)
1706 * @param {integer} end - index of the last cell to execute (exclusive)
1706 * @param {integer} end - index of the last cell to execute (exclusive)
1707 */
1707 */
1708 Notebook.prototype.execute_cell_range = function (start, end) {
1708 Notebook.prototype.execute_cell_range = function (start, end) {
1709 this.command_mode();
1709 this.command_mode();
1710 for (var i=start; i<end; i++) {
1710 for (var i=start; i<end; i++) {
1711 this.select(i);
1711 this.select(i);
1712 this.execute_cell();
1712 this.execute_cell();
1713 }
1713 }
1714 };
1714 };
1715
1715
1716 // Persistance and loading
1716 // Persistance and loading
1717
1717
1718 /**
1718 /**
1719 * Getter method for this notebook's name.
1719 * Getter method for this notebook's name.
1720 *
1720 *
1721 * @return {string} This notebook's name (excluding file extension)
1721 * @return {string} This notebook's name (excluding file extension)
1722 */
1722 */
1723 Notebook.prototype.get_notebook_name = function () {
1723 Notebook.prototype.get_notebook_name = function () {
1724 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1724 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1725 return nbname;
1725 return nbname;
1726 };
1726 };
1727
1727
1728 /**
1728 /**
1729 * Setter method for this notebook's name.
1729 * Setter method for this notebook's name.
1730 *
1730 *
1731 * @param {string} name
1731 * @param {string} name
1732 */
1732 */
1733 Notebook.prototype.set_notebook_name = function (name) {
1733 Notebook.prototype.set_notebook_name = function (name) {
1734 var parent = utils.url_path_split(this.notebook_path)[0];
1734 var parent = utils.url_path_split(this.notebook_path)[0];
1735 this.notebook_name = name;
1735 this.notebook_name = name;
1736 this.notebook_path = utils.url_path_join(parent, name);
1736 this.notebook_path = utils.url_path_join(parent, name);
1737 };
1737 };
1738
1738
1739 /**
1739 /**
1740 * Check that a notebook's name is valid.
1740 * Check that a notebook's name is valid.
1741 *
1741 *
1742 * @param {string} nbname - A name for this notebook
1742 * @param {string} nbname - A name for this notebook
1743 * @return {boolean} True if the name is valid, false if invalid
1743 * @return {boolean} True if the name is valid, false if invalid
1744 */
1744 */
1745 Notebook.prototype.test_notebook_name = function (nbname) {
1745 Notebook.prototype.test_notebook_name = function (nbname) {
1746 nbname = nbname || '';
1746 nbname = nbname || '';
1747 if (nbname.length>0 && !this.notebook_name_blacklist_re.test(nbname)) {
1747 if (nbname.length>0 && !this.notebook_name_blacklist_re.test(nbname)) {
1748 return true;
1748 return true;
1749 } else {
1749 } else {
1750 return false;
1750 return false;
1751 }
1751 }
1752 };
1752 };
1753
1753
1754 /**
1754 /**
1755 * Load a notebook from JSON (.ipynb).
1755 * Load a notebook from JSON (.ipynb).
1756 *
1756 *
1757 * @param {object} data - JSON representation of a notebook
1757 * @param {object} data - JSON representation of a notebook
1758 */
1758 */
1759 Notebook.prototype.fromJSON = function (data) {
1759 Notebook.prototype.fromJSON = function (data) {
1760
1760
1761 var content = data.content;
1761 var content = data.content;
1762 var ncells = this.ncells();
1762 var ncells = this.ncells();
1763 var i;
1763 var i;
1764 for (i=0; i<ncells; i++) {
1764 for (i=0; i<ncells; i++) {
1765 // Always delete cell 0 as they get renumbered as they are deleted.
1765 // Always delete cell 0 as they get renumbered as they are deleted.
1766 this._unsafe_delete_cell(0);
1766 this._unsafe_delete_cell(0);
1767 }
1767 }
1768 // Save the metadata and name.
1768 // Save the metadata and name.
1769 this.metadata = content.metadata;
1769 this.metadata = content.metadata;
1770 this.notebook_name = data.name;
1770 this.notebook_name = data.name;
1771 this.notebook_path = data.path;
1771 this.notebook_path = data.path;
1772 var trusted = true;
1772 var trusted = true;
1773
1773
1774 // Set the codemirror mode from language_info metadata
1774 // Set the codemirror mode from language_info metadata
1775 if (this.metadata.language_info !== undefined) {
1775 if (this.metadata.language_info !== undefined) {
1776 var langinfo = this.metadata.language_info;
1776 var langinfo = this.metadata.language_info;
1777 // Mode 'null' should be plain, unhighlighted text.
1777 // Mode 'null' should be plain, unhighlighted text.
1778 var cm_mode = langinfo.codemirror_mode || langinfo.name || 'null';
1778 var cm_mode = langinfo.codemirror_mode || langinfo.name || 'null';
1779 this.set_codemirror_mode(cm_mode);
1779 this.set_codemirror_mode(cm_mode);
1780 }
1780 }
1781
1781
1782 var new_cells = content.cells;
1782 var new_cells = content.cells;
1783 ncells = new_cells.length;
1783 ncells = new_cells.length;
1784 var cell_data = null;
1784 var cell_data = null;
1785 var new_cell = null;
1785 var new_cell = null;
1786 for (i=0; i<ncells; i++) {
1786 for (i=0; i<ncells; i++) {
1787 cell_data = new_cells[i];
1787 cell_data = new_cells[i];
1788 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1788 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1789 new_cell.fromJSON(cell_data);
1789 new_cell.fromJSON(cell_data);
1790 if (new_cell.cell_type === 'code' && !new_cell.output_area.trusted) {
1790 if (new_cell.cell_type === 'code' && !new_cell.output_area.trusted) {
1791 trusted = false;
1791 trusted = false;
1792 }
1792 }
1793 }
1793 }
1794 if (trusted !== this.trusted) {
1794 if (trusted !== this.trusted) {
1795 this.trusted = trusted;
1795 this.trusted = trusted;
1796 this.events.trigger("trust_changed.Notebook", trusted);
1796 this.events.trigger("trust_changed.Notebook", trusted);
1797 }
1797 }
1798 };
1798 };
1799
1799
1800 /**
1800 /**
1801 * Dump this notebook into a JSON-friendly object.
1801 * Dump this notebook into a JSON-friendly object.
1802 *
1802 *
1803 * @return {object} A JSON-friendly representation of this notebook.
1803 * @return {object} A JSON-friendly representation of this notebook.
1804 */
1804 */
1805 Notebook.prototype.toJSON = function () {
1805 Notebook.prototype.toJSON = function () {
1806 // remove the conversion indicator, which only belongs in-memory
1806 // remove the conversion indicator, which only belongs in-memory
1807 delete this.metadata.orig_nbformat;
1807 delete this.metadata.orig_nbformat;
1808 delete this.metadata.orig_nbformat_minor;
1808 delete this.metadata.orig_nbformat_minor;
1809
1809
1810 var cells = this.get_cells();
1810 var cells = this.get_cells();
1811 var ncells = cells.length;
1811 var ncells = cells.length;
1812 var cell_array = new Array(ncells);
1812 var cell_array = new Array(ncells);
1813 var trusted = true;
1813 var trusted = true;
1814 for (var i=0; i<ncells; i++) {
1814 for (var i=0; i<ncells; i++) {
1815 var cell = cells[i];
1815 var cell = cells[i];
1816 if (cell.cell_type === 'code' && !cell.output_area.trusted) {
1816 if (cell.cell_type === 'code' && !cell.output_area.trusted) {
1817 trusted = false;
1817 trusted = false;
1818 }
1818 }
1819 cell_array[i] = cell.toJSON();
1819 cell_array[i] = cell.toJSON();
1820 }
1820 }
1821 var data = {
1821 var data = {
1822 cells: cell_array,
1822 cells: cell_array,
1823 metadata: this.metadata,
1823 metadata: this.metadata,
1824 nbformat: this.nbformat,
1824 nbformat: this.nbformat,
1825 nbformat_minor: this.nbformat_minor
1825 nbformat_minor: this.nbformat_minor
1826 };
1826 };
1827 if (trusted !== this.trusted) {
1827 if (trusted !== this.trusted) {
1828 this.trusted = trusted;
1828 this.trusted = trusted;
1829 this.events.trigger("trust_changed.Notebook", trusted);
1829 this.events.trigger("trust_changed.Notebook", trusted);
1830 }
1830 }
1831 return data;
1831 return data;
1832 };
1832 };
1833
1833
1834 /**
1834 /**
1835 * Start an autosave timer which periodically saves the notebook.
1835 * Start an autosave timer which periodically saves the notebook.
1836 *
1836 *
1837 * @param {integer} interval - the autosave interval in milliseconds
1837 * @param {integer} interval - the autosave interval in milliseconds
1838 */
1838 */
1839 Notebook.prototype.set_autosave_interval = function (interval) {
1839 Notebook.prototype.set_autosave_interval = function (interval) {
1840 var that = this;
1840 var that = this;
1841 // clear previous interval, so we don't get simultaneous timers
1841 // clear previous interval, so we don't get simultaneous timers
1842 if (this.autosave_timer) {
1842 if (this.autosave_timer) {
1843 clearInterval(this.autosave_timer);
1843 clearInterval(this.autosave_timer);
1844 }
1844 }
1845 if (!this.writable) {
1845 if (!this.writable) {
1846 // disable autosave if not writable
1846 // disable autosave if not writable
1847 interval = 0;
1847 interval = 0;
1848 }
1848 }
1849
1849
1850 this.autosave_interval = this.minimum_autosave_interval = interval;
1850 this.autosave_interval = this.minimum_autosave_interval = interval;
1851 if (interval) {
1851 if (interval) {
1852 this.autosave_timer = setInterval(function() {
1852 this.autosave_timer = setInterval(function() {
1853 if (that.dirty) {
1853 if (that.dirty) {
1854 that.save_notebook();
1854 that.save_notebook();
1855 }
1855 }
1856 }, interval);
1856 }, interval);
1857 this.events.trigger("autosave_enabled.Notebook", interval);
1857 this.events.trigger("autosave_enabled.Notebook", interval);
1858 } else {
1858 } else {
1859 this.autosave_timer = null;
1859 this.autosave_timer = null;
1860 this.events.trigger("autosave_disabled.Notebook");
1860 this.events.trigger("autosave_disabled.Notebook");
1861 }
1861 }
1862 };
1862 };
1863
1863
1864 /**
1864 /**
1865 * Save this notebook on the server. This becomes a notebook instance's
1865 * Save this notebook on the server. This becomes a notebook instance's
1866 * .save_notebook method *after* the entire notebook has been loaded.
1866 * .save_notebook method *after* the entire notebook has been loaded.
1867 */
1867 */
1868 Notebook.prototype.save_notebook = function () {
1868 Notebook.prototype.save_notebook = function () {
1869 if (!this._fully_loaded) {
1869 if (!this._fully_loaded) {
1870 this.events.trigger('notebook_save_failed.Notebook',
1870 this.events.trigger('notebook_save_failed.Notebook',
1871 new Error("Load failed, save is disabled")
1871 new Error("Load failed, save is disabled")
1872 );
1872 );
1873 return;
1873 return;
1874 } else if (!this.writable) {
1874 } else if (!this.writable) {
1875 this.events.trigger('notebook_save_failed.Notebook',
1875 this.events.trigger('notebook_save_failed.Notebook',
1876 new Error("Notebook is read-only")
1876 new Error("Notebook is read-only")
1877 );
1877 );
1878 return;
1878 return;
1879 }
1879 }
1880
1880
1881 // Trigger an event before save, which allows listeners to modify
1881 // Trigger an event before save, which allows listeners to modify
1882 // the notebook as needed.
1882 // the notebook as needed.
1883 this.events.trigger('before_save.Notebook');
1883 this.events.trigger('before_save.Notebook');
1884
1884
1885 // Create a JSON model to be sent to the server.
1885 // Create a JSON model to be sent to the server.
1886 var model = {
1886 var model = {
1887 type : "notebook",
1887 type : "notebook",
1888 content : this.toJSON()
1888 content : this.toJSON()
1889 };
1889 };
1890 // time the ajax call for autosave tuning purposes.
1890 // time the ajax call for autosave tuning purposes.
1891 var start = new Date().getTime();
1891 var start = new Date().getTime();
1892
1892
1893 var that = this;
1893 var that = this;
1894 return this.contents.save(this.notebook_path, model).then(
1894 return this.contents.save(this.notebook_path, model).then(
1895 $.proxy(this.save_notebook_success, this, start),
1895 $.proxy(this.save_notebook_success, this, start),
1896 function (error) {
1896 function (error) {
1897 that.events.trigger('notebook_save_failed.Notebook', error);
1897 that.events.trigger('notebook_save_failed.Notebook', error);
1898 }
1898 }
1899 );
1899 );
1900 };
1900 };
1901
1901
1902 /**
1902 /**
1903 * Success callback for saving a notebook.
1903 * Success callback for saving a notebook.
1904 *
1904 *
1905 * @param {integer} start - Time when the save request start
1905 * @param {integer} start - Time when the save request start
1906 * @param {object} data - JSON representation of a notebook
1906 * @param {object} data - JSON representation of a notebook
1907 */
1907 */
1908 Notebook.prototype.save_notebook_success = function (start, data) {
1908 Notebook.prototype.save_notebook_success = function (start, data) {
1909 this.set_dirty(false);
1909 this.set_dirty(false);
1910 if (data.message) {
1910 if (data.message) {
1911 // save succeeded, but validation failed.
1911 // save succeeded, but validation failed.
1912 var body = $("<div>");
1912 var body = $("<div>");
1913 var title = "Notebook validation failed";
1913 var title = "Notebook validation failed";
1914
1914
1915 body.append($("<p>").text(
1915 body.append($("<p>").text(
1916 "The save operation succeeded," +
1916 "The save operation succeeded," +
1917 " but the notebook does not appear to be valid." +
1917 " but the notebook does not appear to be valid." +
1918 " The validation error was:"
1918 " The validation error was:"
1919 )).append($("<div>").addClass("validation-error").append(
1919 )).append($("<div>").addClass("validation-error").append(
1920 $("<pre>").text(data.message)
1920 $("<pre>").text(data.message)
1921 ));
1921 ));
1922 dialog.modal({
1922 dialog.modal({
1923 notebook: this,
1923 notebook: this,
1924 keyboard_manager: this.keyboard_manager,
1924 keyboard_manager: this.keyboard_manager,
1925 title: title,
1925 title: title,
1926 body: body,
1926 body: body,
1927 buttons : {
1927 buttons : {
1928 OK : {
1928 OK : {
1929 "class" : "btn-primary"
1929 "class" : "btn-primary"
1930 }
1930 }
1931 }
1931 }
1932 });
1932 });
1933 }
1933 }
1934 this.events.trigger('notebook_saved.Notebook');
1934 this.events.trigger('notebook_saved.Notebook');
1935 this._update_autosave_interval(start);
1935 this._update_autosave_interval(start);
1936 if (this._checkpoint_after_save) {
1936 if (this._checkpoint_after_save) {
1937 this.create_checkpoint();
1937 this.create_checkpoint();
1938 this._checkpoint_after_save = false;
1938 this._checkpoint_after_save = false;
1939 }
1939 }
1940 };
1940 };
1941
1941
1942 /**
1942 /**
1943 * Update the autosave interval based on the duration of the last save.
1943 * Update the autosave interval based on the duration of the last save.
1944 *
1944 *
1945 * @param {integer} timestamp - when the save request started
1945 * @param {integer} timestamp - when the save request started
1946 */
1946 */
1947 Notebook.prototype._update_autosave_interval = function (start) {
1947 Notebook.prototype._update_autosave_interval = function (start) {
1948 var duration = (new Date().getTime() - start);
1948 var duration = (new Date().getTime() - start);
1949 if (this.autosave_interval) {
1949 if (this.autosave_interval) {
1950 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
1950 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
1951 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
1951 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
1952 // round to 10 seconds, otherwise we will be setting a new interval too often
1952 // round to 10 seconds, otherwise we will be setting a new interval too often
1953 interval = 10000 * Math.round(interval / 10000);
1953 interval = 10000 * Math.round(interval / 10000);
1954 // set new interval, if it's changed
1954 // set new interval, if it's changed
1955 if (interval !== this.autosave_interval) {
1955 if (interval !== this.autosave_interval) {
1956 this.set_autosave_interval(interval);
1956 this.set_autosave_interval(interval);
1957 }
1957 }
1958 }
1958 }
1959 };
1959 };
1960
1960
1961 /**
1961 /**
1962 * Explicitly trust the output of this notebook.
1962 * Explicitly trust the output of this notebook.
1963 */
1963 */
1964 Notebook.prototype.trust_notebook = function () {
1964 Notebook.prototype.trust_notebook = function () {
1965 var body = $("<div>").append($("<p>")
1965 var body = $("<div>").append($("<p>")
1966 .text("A trusted IPython notebook may execute hidden malicious code ")
1966 .text("A trusted IPython notebook may execute hidden malicious code ")
1967 .append($("<strong>")
1967 .append($("<strong>")
1968 .append(
1968 .append(
1969 $("<em>").text("when you open it")
1969 $("<em>").text("when you open it")
1970 )
1970 )
1971 ).append(".").append(
1971 ).append(".").append(
1972 " Selecting trust will immediately reload this notebook in a trusted state."
1972 " Selecting trust will immediately reload this notebook in a trusted state."
1973 ).append(
1973 ).append(
1974 " For more information, see the "
1974 " For more information, see the "
1975 ).append($("<a>").attr("href", "http://ipython.org/ipython-doc/2/notebook/security.html")
1975 ).append($("<a>").attr("href", "http://ipython.org/ipython-doc/2/notebook/security.html")
1976 .text("IPython security documentation")
1976 .text("IPython security documentation")
1977 ).append(".")
1977 ).append(".")
1978 );
1978 );
1979
1979
1980 var nb = this;
1980 var nb = this;
1981 dialog.modal({
1981 dialog.modal({
1982 notebook: this,
1982 notebook: this,
1983 keyboard_manager: this.keyboard_manager,
1983 keyboard_manager: this.keyboard_manager,
1984 title: "Trust this notebook?",
1984 title: "Trust this notebook?",
1985 body: body,
1985 body: body,
1986
1986
1987 buttons: {
1987 buttons: {
1988 Cancel : {},
1988 Cancel : {},
1989 Trust : {
1989 Trust : {
1990 class : "btn-danger",
1990 class : "btn-danger",
1991 click : function () {
1991 click : function () {
1992 var cells = nb.get_cells();
1992 var cells = nb.get_cells();
1993 for (var i = 0; i < cells.length; i++) {
1993 for (var i = 0; i < cells.length; i++) {
1994 var cell = cells[i];
1994 var cell = cells[i];
1995 if (cell.cell_type === 'code') {
1995 if (cell.cell_type === 'code') {
1996 cell.output_area.trusted = true;
1996 cell.output_area.trusted = true;
1997 }
1997 }
1998 }
1998 }
1999 nb.events.on('notebook_saved.Notebook', function () {
1999 nb.events.on('notebook_saved.Notebook', function () {
2000 window.location.reload();
2000 window.location.reload();
2001 });
2001 });
2002 nb.save_notebook();
2002 nb.save_notebook();
2003 }
2003 }
2004 }
2004 }
2005 }
2005 }
2006 });
2006 });
2007 };
2007 };
2008
2008
2009 /**
2009 /**
2010 * Make a copy of the current notebook.
2010 * Make a copy of the current notebook.
2011 */
2011 */
2012 Notebook.prototype.copy_notebook = function () {
2012 Notebook.prototype.copy_notebook = function () {
2013 var that = this;
2013 var that = this;
2014 var base_url = this.base_url;
2014 var base_url = this.base_url;
2015 var w = window.open();
2015 var w = window.open();
2016 var parent = utils.url_path_split(this.notebook_path)[0];
2016 var parent = utils.url_path_split(this.notebook_path)[0];
2017 this.contents.copy(this.notebook_path, parent).then(
2017 this.contents.copy(this.notebook_path, parent).then(
2018 function (data) {
2018 function (data) {
2019 w.location = utils.url_join_encode(
2019 w.location = utils.url_join_encode(
2020 base_url, 'notebooks', data.path
2020 base_url, 'notebooks', data.path
2021 );
2021 );
2022 },
2022 },
2023 function(error) {
2023 function(error) {
2024 w.close();
2024 w.close();
2025 that.events.trigger('notebook_copy_failed', error);
2025 that.events.trigger('notebook_copy_failed', error);
2026 }
2026 }
2027 );
2027 );
2028 };
2028 };
2029
2029
2030 /**
2030 /**
2031 * Ensure a filename has the right extension
2031 * Ensure a filename has the right extension
2032 * Returns the filename with the appropriate extension, appending if necessary.
2032 * Returns the filename with the appropriate extension, appending if necessary.
2033 */
2033 */
2034 Notebook.prototype.ensure_extension = function (name) {
2034 Notebook.prototype.ensure_extension = function (name) {
2035 if (!name.match(/\.ipynb$/)) {
2035 if (!name.match(/\.ipynb$/)) {
2036 name = name + ".ipynb";
2036 name = name + ".ipynb";
2037 }
2037 }
2038 return name;
2038 return name;
2039 };
2039 };
2040
2040
2041 /**
2041 /**
2042 * Rename the notebook.
2042 * Rename the notebook.
2043 * @param {string} new_name
2043 * @param {string} new_name
2044 * @return {Promise} promise that resolves when the notebook is renamed.
2044 * @return {Promise} promise that resolves when the notebook is renamed.
2045 */
2045 */
2046 Notebook.prototype.rename = function (new_name) {
2046 Notebook.prototype.rename = function (new_name) {
2047 new_name = this.ensure_extension(new_name);
2047 new_name = this.ensure_extension(new_name);
2048
2048
2049 var that = this;
2049 var that = this;
2050 var parent = utils.url_path_split(this.notebook_path)[0];
2050 var parent = utils.url_path_split(this.notebook_path)[0];
2051 var new_path = utils.url_path_join(parent, new_name);
2051 var new_path = utils.url_path_join(parent, new_name);
2052 return this.contents.rename(this.notebook_path, new_path).then(
2052 return this.contents.rename(this.notebook_path, new_path).then(
2053 function (json) {
2053 function (json) {
2054 that.notebook_name = json.name;
2054 that.notebook_name = json.name;
2055 that.notebook_path = json.path;
2055 that.notebook_path = json.path;
2056 that.session.rename_notebook(json.path);
2056 that.session.rename_notebook(json.path);
2057 that.events.trigger('notebook_renamed.Notebook', json);
2057 that.events.trigger('notebook_renamed.Notebook', json);
2058 }
2058 }
2059 );
2059 );
2060 };
2060 };
2061
2061
2062 /**
2062 /**
2063 * Delete this notebook
2063 * Delete this notebook
2064 */
2064 */
2065 Notebook.prototype.delete = function () {
2065 Notebook.prototype.delete = function () {
2066 this.contents.delete(this.notebook_path);
2066 this.contents.delete(this.notebook_path);
2067 };
2067 };
2068
2068
2069 /**
2069 /**
2070 * Request a notebook's data from the server.
2070 * Request a notebook's data from the server.
2071 *
2071 *
2072 * @param {string} notebook_path - A notebook to load
2072 * @param {string} notebook_path - A notebook to load
2073 */
2073 */
2074 Notebook.prototype.load_notebook = function (notebook_path) {
2074 Notebook.prototype.load_notebook = function (notebook_path) {
2075 var that = this;
2075 var that = this;
2076 this.notebook_path = notebook_path;
2076 this.notebook_path = notebook_path;
2077 this.notebook_name = utils.url_path_split(this.notebook_path)[1];
2077 this.notebook_name = utils.url_path_split(this.notebook_path)[1];
2078 this.events.trigger('notebook_loading.Notebook');
2078 this.events.trigger('notebook_loading.Notebook');
2079 this.contents.get(notebook_path, {type: 'notebook'}).then(
2079 this.contents.get(notebook_path, {type: 'notebook'}).then(
2080 $.proxy(this.load_notebook_success, this),
2080 $.proxy(this.load_notebook_success, this),
2081 $.proxy(this.load_notebook_error, this)
2081 $.proxy(this.load_notebook_error, this)
2082 );
2082 );
2083 };
2083 };
2084
2084
2085 /**
2085 /**
2086 * Success callback for loading a notebook from the server.
2086 * Success callback for loading a notebook from the server.
2087 *
2087 *
2088 * Load notebook data from the JSON response.
2088 * Load notebook data from the JSON response.
2089 *
2089 *
2090 * @param {object} data JSON representation of a notebook
2090 * @param {object} data JSON representation of a notebook
2091 */
2091 */
2092 Notebook.prototype.load_notebook_success = function (data) {
2092 Notebook.prototype.load_notebook_success = function (data) {
2093 var failed, msg;
2093 var failed, msg;
2094 try {
2094 try {
2095 this.fromJSON(data);
2095 this.fromJSON(data);
2096 } catch (e) {
2096 } catch (e) {
2097 failed = e;
2097 failed = e;
2098 console.log("Notebook failed to load from JSON:", e);
2098 console.log("Notebook failed to load from JSON:", e);
2099 }
2099 }
2100 if (failed || data.message) {
2100 if (failed || data.message) {
2101 // *either* fromJSON failed or validation failed
2101 // *either* fromJSON failed or validation failed
2102 var body = $("<div>");
2102 var body = $("<div>");
2103 var title;
2103 var title;
2104 if (failed) {
2104 if (failed) {
2105 title = "Notebook failed to load";
2105 title = "Notebook failed to load";
2106 body.append($("<p>").text(
2106 body.append($("<p>").text(
2107 "The error was: "
2107 "The error was: "
2108 )).append($("<div>").addClass("js-error").text(
2108 )).append($("<div>").addClass("js-error").text(
2109 failed.toString()
2109 failed.toString()
2110 )).append($("<p>").text(
2110 )).append($("<p>").text(
2111 "See the error console for details."
2111 "See the error console for details."
2112 ));
2112 ));
2113 } else {
2113 } else {
2114 title = "Notebook validation failed";
2114 title = "Notebook validation failed";
2115 }
2115 }
2116
2116
2117 if (data.message) {
2117 if (data.message) {
2118 if (failed) {
2118 if (failed) {
2119 msg = "The notebook also failed validation:";
2119 msg = "The notebook also failed validation:";
2120 } else {
2120 } else {
2121 msg = "An invalid notebook may not function properly." +
2121 msg = "An invalid notebook may not function properly." +
2122 " The validation error was:";
2122 " The validation error was:";
2123 }
2123 }
2124 body.append($("<p>").text(
2124 body.append($("<p>").text(
2125 msg
2125 msg
2126 )).append($("<div>").addClass("validation-error").append(
2126 )).append($("<div>").addClass("validation-error").append(
2127 $("<pre>").text(data.message)
2127 $("<pre>").text(data.message)
2128 ));
2128 ));
2129 }
2129 }
2130
2130
2131 dialog.modal({
2131 dialog.modal({
2132 notebook: this,
2132 notebook: this,
2133 keyboard_manager: this.keyboard_manager,
2133 keyboard_manager: this.keyboard_manager,
2134 title: title,
2134 title: title,
2135 body: body,
2135 body: body,
2136 buttons : {
2136 buttons : {
2137 OK : {
2137 OK : {
2138 "class" : "btn-primary"
2138 "class" : "btn-primary"
2139 }
2139 }
2140 }
2140 }
2141 });
2141 });
2142 }
2142 }
2143 if (this.ncells() === 0) {
2143 if (this.ncells() === 0) {
2144 this.insert_cell_below('code');
2144 this.insert_cell_below('code');
2145 this.edit_mode(0);
2145 this.edit_mode(0);
2146 } else {
2146 } else {
2147 this.select(0);
2147 this.select(0);
2148 this.handle_command_mode(this.get_cell(0));
2148 this.handle_command_mode(this.get_cell(0));
2149 }
2149 }
2150 this.set_dirty(false);
2150 this.set_dirty(false);
2151 this.scroll_to_top();
2151 this.scroll_to_top();
2152 this.writable = data.writable || false;
2152 this.writable = data.writable || false;
2153 var nbmodel = data.content;
2153 var nbmodel = data.content;
2154 var orig_nbformat = nbmodel.metadata.orig_nbformat;
2154 var orig_nbformat = nbmodel.metadata.orig_nbformat;
2155 var orig_nbformat_minor = nbmodel.metadata.orig_nbformat_minor;
2155 var orig_nbformat_minor = nbmodel.metadata.orig_nbformat_minor;
2156 if (orig_nbformat !== undefined && nbmodel.nbformat !== orig_nbformat) {
2156 if (orig_nbformat !== undefined && nbmodel.nbformat !== orig_nbformat) {
2157 var src;
2157 var src;
2158 if (nbmodel.nbformat > orig_nbformat) {
2158 if (nbmodel.nbformat > orig_nbformat) {
2159 src = " an older notebook format ";
2159 src = " an older notebook format ";
2160 } else {
2160 } else {
2161 src = " a newer notebook format ";
2161 src = " a newer notebook format ";
2162 }
2162 }
2163
2163
2164 msg = "This notebook has been converted from" + src +
2164 msg = "This notebook has been converted from" + src +
2165 "(v"+orig_nbformat+") to the current notebook " +
2165 "(v"+orig_nbformat+") to the current notebook " +
2166 "format (v"+nbmodel.nbformat+"). The next time you save this notebook, the " +
2166 "format (v"+nbmodel.nbformat+"). The next time you save this notebook, the " +
2167 "current notebook format will be used.";
2167 "current notebook format will be used.";
2168
2168
2169 if (nbmodel.nbformat > orig_nbformat) {
2169 if (nbmodel.nbformat > orig_nbformat) {
2170 msg += " Older versions of IPython may not be able to read the new format.";
2170 msg += " Older versions of IPython may not be able to read the new format.";
2171 } else {
2171 } else {
2172 msg += " Some features of the original notebook may not be available.";
2172 msg += " Some features of the original notebook may not be available.";
2173 }
2173 }
2174 msg += " To preserve the original version, close the " +
2174 msg += " To preserve the original version, close the " +
2175 "notebook without saving it.";
2175 "notebook without saving it.";
2176 dialog.modal({
2176 dialog.modal({
2177 notebook: this,
2177 notebook: this,
2178 keyboard_manager: this.keyboard_manager,
2178 keyboard_manager: this.keyboard_manager,
2179 title : "Notebook converted",
2179 title : "Notebook converted",
2180 body : msg,
2180 body : msg,
2181 buttons : {
2181 buttons : {
2182 OK : {
2182 OK : {
2183 class : "btn-primary"
2183 class : "btn-primary"
2184 }
2184 }
2185 }
2185 }
2186 });
2186 });
2187 } else if (this.nbformat_minor < nbmodel.nbformat_minor) {
2187 } else if (this.nbformat_minor < nbmodel.nbformat_minor) {
2188 this.nbformat_minor = nbmodel.nbformat_minor;
2188 this.nbformat_minor = nbmodel.nbformat_minor;
2189 }
2189 }
2190
2190
2191 if (this.session === null) {
2191 if (this.session === null) {
2192 var kernel_name;
2192 var kernel_name;
2193 if (this.metadata.kernelspec) {
2193 if (this.metadata.kernelspec) {
2194 var kernelspec = this.metadata.kernelspec || {};
2194 var kernelspec = this.metadata.kernelspec || {};
2195 kernel_name = kernelspec.name;
2195 kernel_name = kernelspec.name;
2196 } else {
2196 } else {
2197 kernel_name = utils.get_url_param('kernel_name');
2197 kernel_name = utils.get_url_param('kernel_name');
2198 }
2198 }
2199 if (kernel_name) {
2199 if (kernel_name) {
2200 // setting kernel_name here triggers start_session
2200 // setting kernel_name here triggers start_session
2201 this.kernel_selector.set_kernel(kernel_name);
2201 this.kernel_selector.set_kernel(kernel_name);
2202 } else {
2202 } else {
2203 // start a new session with the server's default kernel
2203 // start a new session with the server's default kernel
2204 // spec_changed events will fire after kernel is loaded
2204 // spec_changed events will fire after kernel is loaded
2205 this.start_session();
2205 this.start_session();
2206 }
2206 }
2207 }
2207 }
2208 // load our checkpoint list
2208 // load our checkpoint list
2209 this.list_checkpoints();
2209 this.list_checkpoints();
2210
2210
2211 // load toolbar state
2211 // load toolbar state
2212 if (this.metadata.celltoolbar) {
2212 if (this.metadata.celltoolbar) {
2213 celltoolbar.CellToolbar.global_show();
2213 celltoolbar.CellToolbar.global_show();
2214 celltoolbar.CellToolbar.activate_preset(this.metadata.celltoolbar);
2214 celltoolbar.CellToolbar.activate_preset(this.metadata.celltoolbar);
2215 } else {
2215 } else {
2216 celltoolbar.CellToolbar.global_hide();
2216 celltoolbar.CellToolbar.global_hide();
2217 }
2217 }
2218
2218
2219 if (!this.writable) {
2219 if (!this.writable) {
2220 this.set_autosave_interval(0);
2220 this.set_autosave_interval(0);
2221 this.events.trigger('notebook_read_only.Notebook');
2221 this.events.trigger('notebook_read_only.Notebook');
2222 }
2222 }
2223
2223
2224 // now that we're fully loaded, it is safe to restore save functionality
2224 // now that we're fully loaded, it is safe to restore save functionality
2225 this._fully_loaded = true;
2225 this._fully_loaded = true;
2226 this.events.trigger('notebook_loaded.Notebook');
2226 this.events.trigger('notebook_loaded.Notebook');
2227 };
2227 };
2228
2228
2229 Notebook.prototype.set_kernelselector = function(k_selector){
2229 Notebook.prototype.set_kernelselector = function(k_selector){
2230 this.kernel_selector = k_selector;
2230 this.kernel_selector = k_selector;
2231 };
2231 };
2232
2232
2233 /**
2233 /**
2234 * Failure callback for loading a notebook from the server.
2234 * Failure callback for loading a notebook from the server.
2235 *
2235 *
2236 * @param {Error} error
2236 * @param {Error} error
2237 */
2237 */
2238 Notebook.prototype.load_notebook_error = function (error) {
2238 Notebook.prototype.load_notebook_error = function (error) {
2239 this.events.trigger('notebook_load_failed.Notebook', error);
2239 this.events.trigger('notebook_load_failed.Notebook', error);
2240 var msg;
2240 var msg;
2241 if (error.name === utils.XHR_ERROR && error.xhr.status === 500) {
2241 if (error.name === utils.XHR_ERROR && error.xhr.status === 500) {
2242 utils.log_ajax_error(error.xhr, error.xhr_status, error.xhr_error);
2242 utils.log_ajax_error(error.xhr, error.xhr_status, error.xhr_error);
2243 msg = "An unknown error occurred while loading this notebook. " +
2243 msg = "An unknown error occurred while loading this notebook. " +
2244 "This version can load notebook formats " +
2244 "This version can load notebook formats " +
2245 "v" + this.nbformat + " or earlier. See the server log for details.";
2245 "v" + this.nbformat + " or earlier. See the server log for details.";
2246 } else {
2246 } else {
2247 msg = error.message;
2247 msg = error.message;
2248 console.warn('Error stack trace while loading notebook was:');
2249 console.warn(error.stack);
2248 }
2250 }
2249 dialog.modal({
2251 dialog.modal({
2250 notebook: this,
2252 notebook: this,
2251 keyboard_manager: this.keyboard_manager,
2253 keyboard_manager: this.keyboard_manager,
2252 title: "Error loading notebook",
2254 title: "Error loading notebook",
2253 body : msg,
2255 body : msg,
2254 buttons : {
2256 buttons : {
2255 "OK": {}
2257 "OK": {}
2256 }
2258 }
2257 });
2259 });
2258 };
2260 };
2259
2261
2260 /********************* checkpoint-related ********************/
2262 /********************* checkpoint-related ********************/
2261
2263
2262 /**
2264 /**
2263 * Save the notebook then immediately create a checkpoint.
2265 * Save the notebook then immediately create a checkpoint.
2264 */
2266 */
2265 Notebook.prototype.save_checkpoint = function () {
2267 Notebook.prototype.save_checkpoint = function () {
2266 this._checkpoint_after_save = true;
2268 this._checkpoint_after_save = true;
2267 this.save_notebook();
2269 this.save_notebook();
2268 };
2270 };
2269
2271
2270 /**
2272 /**
2271 * Add a checkpoint for this notebook.
2273 * Add a checkpoint for this notebook.
2272 */
2274 */
2273 Notebook.prototype.add_checkpoint = function (checkpoint) {
2275 Notebook.prototype.add_checkpoint = function (checkpoint) {
2274 var found = false;
2276 var found = false;
2275 for (var i = 0; i < this.checkpoints.length; i++) {
2277 for (var i = 0; i < this.checkpoints.length; i++) {
2276 var existing = this.checkpoints[i];
2278 var existing = this.checkpoints[i];
2277 if (existing.id === checkpoint.id) {
2279 if (existing.id === checkpoint.id) {
2278 found = true;
2280 found = true;
2279 this.checkpoints[i] = checkpoint;
2281 this.checkpoints[i] = checkpoint;
2280 break;
2282 break;
2281 }
2283 }
2282 }
2284 }
2283 if (!found) {
2285 if (!found) {
2284 this.checkpoints.push(checkpoint);
2286 this.checkpoints.push(checkpoint);
2285 }
2287 }
2286 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
2288 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
2287 };
2289 };
2288
2290
2289 /**
2291 /**
2290 * List checkpoints for this notebook.
2292 * List checkpoints for this notebook.
2291 */
2293 */
2292 Notebook.prototype.list_checkpoints = function () {
2294 Notebook.prototype.list_checkpoints = function () {
2293 var that = this;
2295 var that = this;
2294 this.contents.list_checkpoints(this.notebook_path).then(
2296 this.contents.list_checkpoints(this.notebook_path).then(
2295 $.proxy(this.list_checkpoints_success, this),
2297 $.proxy(this.list_checkpoints_success, this),
2296 function(error) {
2298 function(error) {
2297 that.events.trigger('list_checkpoints_failed.Notebook', error);
2299 that.events.trigger('list_checkpoints_failed.Notebook', error);
2298 }
2300 }
2299 );
2301 );
2300 };
2302 };
2301
2303
2302 /**
2304 /**
2303 * Success callback for listing checkpoints.
2305 * Success callback for listing checkpoints.
2304 *
2306 *
2305 * @param {object} data - JSON representation of a checkpoint
2307 * @param {object} data - JSON representation of a checkpoint
2306 */
2308 */
2307 Notebook.prototype.list_checkpoints_success = function (data) {
2309 Notebook.prototype.list_checkpoints_success = function (data) {
2308 this.checkpoints = data;
2310 this.checkpoints = data;
2309 if (data.length) {
2311 if (data.length) {
2310 this.last_checkpoint = data[data.length - 1];
2312 this.last_checkpoint = data[data.length - 1];
2311 } else {
2313 } else {
2312 this.last_checkpoint = null;
2314 this.last_checkpoint = null;
2313 }
2315 }
2314 this.events.trigger('checkpoints_listed.Notebook', [data]);
2316 this.events.trigger('checkpoints_listed.Notebook', [data]);
2315 };
2317 };
2316
2318
2317 /**
2319 /**
2318 * Create a checkpoint of this notebook on the server from the most recent save.
2320 * Create a checkpoint of this notebook on the server from the most recent save.
2319 */
2321 */
2320 Notebook.prototype.create_checkpoint = function () {
2322 Notebook.prototype.create_checkpoint = function () {
2321 var that = this;
2323 var that = this;
2322 this.contents.create_checkpoint(this.notebook_path).then(
2324 this.contents.create_checkpoint(this.notebook_path).then(
2323 $.proxy(this.create_checkpoint_success, this),
2325 $.proxy(this.create_checkpoint_success, this),
2324 function (error) {
2326 function (error) {
2325 that.events.trigger('checkpoint_failed.Notebook', error);
2327 that.events.trigger('checkpoint_failed.Notebook', error);
2326 }
2328 }
2327 );
2329 );
2328 };
2330 };
2329
2331
2330 /**
2332 /**
2331 * Success callback for creating a checkpoint.
2333 * Success callback for creating a checkpoint.
2332 *
2334 *
2333 * @param {object} data - JSON representation of a checkpoint
2335 * @param {object} data - JSON representation of a checkpoint
2334 */
2336 */
2335 Notebook.prototype.create_checkpoint_success = function (data) {
2337 Notebook.prototype.create_checkpoint_success = function (data) {
2336 this.add_checkpoint(data);
2338 this.add_checkpoint(data);
2337 this.events.trigger('checkpoint_created.Notebook', data);
2339 this.events.trigger('checkpoint_created.Notebook', data);
2338 };
2340 };
2339
2341
2340 /**
2342 /**
2341 * Display the restore checkpoint dialog
2343 * Display the restore checkpoint dialog
2342 * @param {string} checkpoint ID
2344 * @param {string} checkpoint ID
2343 */
2345 */
2344 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2346 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2345 var that = this;
2347 var that = this;
2346 checkpoint = checkpoint || this.last_checkpoint;
2348 checkpoint = checkpoint || this.last_checkpoint;
2347 if ( ! checkpoint ) {
2349 if ( ! checkpoint ) {
2348 console.log("restore dialog, but no checkpoint to restore to!");
2350 console.log("restore dialog, but no checkpoint to restore to!");
2349 return;
2351 return;
2350 }
2352 }
2351 var body = $('<div/>').append(
2353 var body = $('<div/>').append(
2352 $('<p/>').addClass("p-space").text(
2354 $('<p/>').addClass("p-space").text(
2353 "Are you sure you want to revert the notebook to " +
2355 "Are you sure you want to revert the notebook to " +
2354 "the latest checkpoint?"
2356 "the latest checkpoint?"
2355 ).append(
2357 ).append(
2356 $("<strong/>").text(
2358 $("<strong/>").text(
2357 " This cannot be undone."
2359 " This cannot be undone."
2358 )
2360 )
2359 )
2361 )
2360 ).append(
2362 ).append(
2361 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2363 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2362 ).append(
2364 ).append(
2363 $('<p/>').addClass("p-space").text(
2365 $('<p/>').addClass("p-space").text(
2364 Date(checkpoint.last_modified)
2366 Date(checkpoint.last_modified)
2365 ).css("text-align", "center")
2367 ).css("text-align", "center")
2366 );
2368 );
2367
2369
2368 dialog.modal({
2370 dialog.modal({
2369 notebook: this,
2371 notebook: this,
2370 keyboard_manager: this.keyboard_manager,
2372 keyboard_manager: this.keyboard_manager,
2371 title : "Revert notebook to checkpoint",
2373 title : "Revert notebook to checkpoint",
2372 body : body,
2374 body : body,
2373 buttons : {
2375 buttons : {
2374 Revert : {
2376 Revert : {
2375 class : "btn-danger",
2377 class : "btn-danger",
2376 click : function () {
2378 click : function () {
2377 that.restore_checkpoint(checkpoint.id);
2379 that.restore_checkpoint(checkpoint.id);
2378 }
2380 }
2379 },
2381 },
2380 Cancel : {}
2382 Cancel : {}
2381 }
2383 }
2382 });
2384 });
2383 };
2385 };
2384
2386
2385 /**
2387 /**
2386 * Restore the notebook to a checkpoint state.
2388 * Restore the notebook to a checkpoint state.
2387 *
2389 *
2388 * @param {string} checkpoint ID
2390 * @param {string} checkpoint ID
2389 */
2391 */
2390 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2392 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2391 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2393 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2392 var that = this;
2394 var that = this;
2393 this.contents.restore_checkpoint(this.notebook_path, checkpoint).then(
2395 this.contents.restore_checkpoint(this.notebook_path, checkpoint).then(
2394 $.proxy(this.restore_checkpoint_success, this),
2396 $.proxy(this.restore_checkpoint_success, this),
2395 function (error) {
2397 function (error) {
2396 that.events.trigger('checkpoint_restore_failed.Notebook', error);
2398 that.events.trigger('checkpoint_restore_failed.Notebook', error);
2397 }
2399 }
2398 );
2400 );
2399 };
2401 };
2400
2402
2401 /**
2403 /**
2402 * Success callback for restoring a notebook to a checkpoint.
2404 * Success callback for restoring a notebook to a checkpoint.
2403 */
2405 */
2404 Notebook.prototype.restore_checkpoint_success = function () {
2406 Notebook.prototype.restore_checkpoint_success = function () {
2405 this.events.trigger('checkpoint_restored.Notebook');
2407 this.events.trigger('checkpoint_restored.Notebook');
2406 this.load_notebook(this.notebook_path);
2408 this.load_notebook(this.notebook_path);
2407 };
2409 };
2408
2410
2409 /**
2411 /**
2410 * Delete a notebook checkpoint.
2412 * Delete a notebook checkpoint.
2411 *
2413 *
2412 * @param {string} checkpoint ID
2414 * @param {string} checkpoint ID
2413 */
2415 */
2414 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2416 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2415 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2417 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2416 var that = this;
2418 var that = this;
2417 this.contents.delete_checkpoint(this.notebook_path, checkpoint).then(
2419 this.contents.delete_checkpoint(this.notebook_path, checkpoint).then(
2418 $.proxy(this.delete_checkpoint_success, this),
2420 $.proxy(this.delete_checkpoint_success, this),
2419 function (error) {
2421 function (error) {
2420 that.events.trigger('checkpoint_delete_failed.Notebook', error);
2422 that.events.trigger('checkpoint_delete_failed.Notebook', error);
2421 }
2423 }
2422 );
2424 );
2423 };
2425 };
2424
2426
2425 /**
2427 /**
2426 * Success callback for deleting a notebook checkpoint.
2428 * Success callback for deleting a notebook checkpoint.
2427 */
2429 */
2428 Notebook.prototype.delete_checkpoint_success = function () {
2430 Notebook.prototype.delete_checkpoint_success = function () {
2429 this.events.trigger('checkpoint_deleted.Notebook');
2431 this.events.trigger('checkpoint_deleted.Notebook');
2430 this.load_notebook(this.notebook_path);
2432 this.load_notebook(this.notebook_path);
2431 };
2433 };
2432
2434
2433
2435
2434 // For backwards compatability.
2436 // For backwards compatability.
2435 IPython.Notebook = Notebook;
2437 IPython.Notebook = Notebook;
2436
2438
2437 return {'Notebook': Notebook};
2439 return {'Notebook': Notebook};
2438 });
2440 });
General Comments 0
You need to be logged in to leave comments. Login now