##// END OF EJS Templates
Merge pull request #6857 from jdfreder/celltoolbar_metadata_fix...
Thomas Kluyver -
r18858:9932fd3f merge
parent child Browse files
Show More
@@ -1,588 +1,597 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 /**
5 5 *
6 6 *
7 7 * @module cell
8 8 * @namespace cell
9 9 * @class Cell
10 10 */
11 11
12 12
13 13 define([
14 14 'base/js/namespace',
15 15 'jquery',
16 16 'base/js/utils',
17 17 'codemirror/lib/codemirror',
18 18 'codemirror/addon/edit/matchbrackets',
19 19 'codemirror/addon/edit/closebrackets',
20 20 'codemirror/addon/comment/comment'
21 21 ], function(IPython, $, utils, CodeMirror, cm_match, cm_closeb, cm_comment) {
22 22 // TODO: remove IPython dependency here
23 23 "use strict";
24 24
25 25 var Cell = function (options) {
26 26 /* Constructor
27 27 *
28 28 * The Base `Cell` class from which to inherit.
29 29 * @constructor
30 30 * @param:
31 31 * options: dictionary
32 32 * Dictionary of keyword arguments.
33 33 * events: $(Events) instance
34 34 * config: dictionary
35 35 * keyboard_manager: KeyboardManager instance
36 36 */
37 37 options = options || {};
38 38 this.keyboard_manager = options.keyboard_manager;
39 39 this.events = options.events;
40 40 var config = utils.mergeopt(Cell, options.config);
41 41 // superclass default overwrite our default
42 42
43 43 this.placeholder = config.placeholder || '';
44 44 this.read_only = config.cm_config.readOnly;
45 45 this.selected = false;
46 46 this.rendered = false;
47 47 this.mode = 'command';
48 this.metadata = {};
48
49 // Metadata property
50 var that = this;
51 this._metadata = {};
52 Object.defineProperty(this, 'metadata', {
53 get: function() { return that._metadata; },
54 set: function(value) {
55 that._metadata = value;
56 that.celltoolbar.rebuild();
57 }
58 });
59
49 60 // load this from metadata later ?
50 61 this.user_highlight = 'auto';
51 62 this.cm_config = config.cm_config;
52 63 this.cell_id = utils.uuid();
53 64 this._options = config;
54 65
55 66 // For JS VM engines optimization, attributes should be all set (even
56 67 // to null) in the constructor, and if possible, if different subclass
57 68 // have new attributes with same name, they should be created in the
58 69 // same order. Easiest is to create and set to null in parent class.
59 70
60 71 this.element = null;
61 72 this.cell_type = this.cell_type || null;
62 73 this.code_mirror = null;
63 74
64 75 this.create_element();
65 76 if (this.element !== null) {
66 77 this.element.data("cell", this);
67 78 this.bind_events();
68 79 this.init_classes();
69 80 }
70 81 };
71 82
72 83 Cell.options_default = {
73 84 cm_config : {
74 85 indentUnit : 4,
75 86 readOnly: false,
76 87 theme: "default",
77 88 extraKeys: {
78 89 "Cmd-Right":"goLineRight",
79 90 "End":"goLineRight",
80 91 "Cmd-Left":"goLineLeft"
81 92 }
82 93 }
83 94 };
84 95
85 96 // FIXME: Workaround CM Bug #332 (Safari segfault on drag)
86 97 // by disabling drag/drop altogether on Safari
87 98 // https://github.com/codemirror/CodeMirror/issues/332
88 99 if (utils.browser[0] == "Safari") {
89 100 Cell.options_default.cm_config.dragDrop = false;
90 101 }
91 102
92 103 /**
93 104 * Empty. Subclasses must implement create_element.
94 105 * This should contain all the code to create the DOM element in notebook
95 106 * and will be called by Base Class constructor.
96 107 * @method create_element
97 108 */
98 109 Cell.prototype.create_element = function () {
99 110 };
100 111
101 112 Cell.prototype.init_classes = function () {
102 113 // Call after this.element exists to initialize the css classes
103 114 // related to selected, rendered and mode.
104 115 if (this.selected) {
105 116 this.element.addClass('selected');
106 117 } else {
107 118 this.element.addClass('unselected');
108 119 }
109 120 if (this.rendered) {
110 121 this.element.addClass('rendered');
111 122 } else {
112 123 this.element.addClass('unrendered');
113 124 }
114 125 if (this.mode === 'edit') {
115 126 this.element.addClass('edit_mode');
116 127 } else {
117 128 this.element.addClass('command_mode');
118 129 }
119 130 };
120 131
121 132 /**
122 133 * Subclasses can implement override bind_events.
123 134 * Be carefull to call the parent method when overwriting as it fires event.
124 135 * this will be triggerd after create_element in constructor.
125 136 * @method bind_events
126 137 */
127 138 Cell.prototype.bind_events = function () {
128 139 var that = this;
129 140 // We trigger events so that Cell doesn't have to depend on Notebook.
130 141 that.element.click(function (event) {
131 142 if (!that.selected) {
132 143 that.events.trigger('select.Cell', {'cell':that});
133 144 }
134 145 });
135 146 that.element.focusin(function (event) {
136 147 if (!that.selected) {
137 148 that.events.trigger('select.Cell', {'cell':that});
138 149 }
139 150 });
140 151 if (this.code_mirror) {
141 152 this.code_mirror.on("change", function(cm, change) {
142 153 that.events.trigger("set_dirty.Notebook", {value: true});
143 154 });
144 155 }
145 156 if (this.code_mirror) {
146 157 this.code_mirror.on('focus', function(cm, change) {
147 158 that.events.trigger('edit_mode.Cell', {cell: that});
148 159 });
149 160 }
150 161 if (this.code_mirror) {
151 162 this.code_mirror.on('blur', function(cm, change) {
152 163 that.events.trigger('command_mode.Cell', {cell: that});
153 164 });
154 165 }
155 166
156 167 this.element.dblclick(function () {
157 168 if (that.selected === false) {
158 169 this.events.trigger('select.Cell', {'cell':that});
159 170 }
160 171 var cont = that.unrender();
161 172 if (cont) {
162 173 that.focus_editor();
163 174 }
164 175 });
165 176 };
166 177
167 178 /**
168 179 * This method gets called in CodeMirror's onKeyDown/onKeyPress
169 180 * handlers and is used to provide custom key handling.
170 181 *
171 182 * To have custom handling, subclasses should override this method, but still call it
172 183 * in order to process the Edit mode keyboard shortcuts.
173 184 *
174 185 * @method handle_codemirror_keyevent
175 186 * @param {CodeMirror} editor - The codemirror instance bound to the cell
176 187 * @param {event} event - key press event which either should or should not be handled by CodeMirror
177 188 * @return {Boolean} `true` if CodeMirror should ignore the event, `false` Otherwise
178 189 */
179 190 Cell.prototype.handle_codemirror_keyevent = function (editor, event) {
180 191 var shortcuts = this.keyboard_manager.edit_shortcuts;
181 192
182 193 var cur = editor.getCursor();
183 194 if((cur.line !== 0 || cur.ch !==0) && event.keyCode === 38){
184 195 event._ipkmIgnore = true;
185 196 }
186 197 var nLastLine = editor.lastLine()
187 198 if( ( event.keyCode === 40)
188 199 && (( cur.line !== nLastLine)
189 200 || ( cur.ch !== editor.getLineHandle(nLastLine).text.length))
190 201 ){
191 202 event._ipkmIgnore = true;
192 203 }
193 204 // if this is an edit_shortcuts shortcut, the global keyboard/shortcut
194 205 // manager will handle it
195 206 if (shortcuts.handles(event)) {
196 207 return true;
197 208 }
198 209
199 210 return false;
200 211 };
201 212
202 213
203 214 /**
204 215 * Triger typsetting of math by mathjax on current cell element
205 216 * @method typeset
206 217 */
207 218 Cell.prototype.typeset = function () {
208 219 if (window.MathJax) {
209 220 var cell_math = this.element.get(0);
210 221 MathJax.Hub.Queue(["Typeset", MathJax.Hub, cell_math]);
211 222 }
212 223 };
213 224
214 225 /**
215 226 * handle cell level logic when a cell is selected
216 227 * @method select
217 228 * @return is the action being taken
218 229 */
219 230 Cell.prototype.select = function () {
220 231 if (!this.selected) {
221 232 this.element.addClass('selected');
222 233 this.element.removeClass('unselected');
223 234 this.selected = true;
224 235 return true;
225 236 } else {
226 237 return false;
227 238 }
228 239 };
229 240
230 241 /**
231 242 * handle cell level logic when a cell is unselected
232 243 * @method unselect
233 244 * @return is the action being taken
234 245 */
235 246 Cell.prototype.unselect = function () {
236 247 if (this.selected) {
237 248 this.element.addClass('unselected');
238 249 this.element.removeClass('selected');
239 250 this.selected = false;
240 251 return true;
241 252 } else {
242 253 return false;
243 254 }
244 255 };
245 256
246 257 /**
247 258 * handle cell level logic when a cell is rendered
248 259 * @method render
249 260 * @return is the action being taken
250 261 */
251 262 Cell.prototype.render = function () {
252 263 if (!this.rendered) {
253 264 this.element.addClass('rendered');
254 265 this.element.removeClass('unrendered');
255 266 this.rendered = true;
256 267 return true;
257 268 } else {
258 269 return false;
259 270 }
260 271 };
261 272
262 273 /**
263 274 * handle cell level logic when a cell is unrendered
264 275 * @method unrender
265 276 * @return is the action being taken
266 277 */
267 278 Cell.prototype.unrender = function () {
268 279 if (this.rendered) {
269 280 this.element.addClass('unrendered');
270 281 this.element.removeClass('rendered');
271 282 this.rendered = false;
272 283 return true;
273 284 } else {
274 285 return false;
275 286 }
276 287 };
277 288
278 289 /**
279 290 * Delegates keyboard shortcut handling to either IPython keyboard
280 291 * manager when in command mode, or CodeMirror when in edit mode
281 292 *
282 293 * @method handle_keyevent
283 294 * @param {CodeMirror} editor - The codemirror instance bound to the cell
284 295 * @param {event} - key event to be handled
285 296 * @return {Boolean} `true` if CodeMirror should ignore the event, `false` Otherwise
286 297 */
287 298 Cell.prototype.handle_keyevent = function (editor, event) {
288 299 if (this.mode === 'command') {
289 300 return true;
290 301 } else if (this.mode === 'edit') {
291 302 return this.handle_codemirror_keyevent(editor, event);
292 303 }
293 304 };
294 305
295 306 /**
296 307 * @method at_top
297 308 * @return {Boolean}
298 309 */
299 310 Cell.prototype.at_top = function () {
300 311 var cm = this.code_mirror;
301 312 var cursor = cm.getCursor();
302 313 if (cursor.line === 0 && cursor.ch === 0) {
303 314 return true;
304 315 }
305 316 return false;
306 317 };
307 318
308 319 /**
309 320 * @method at_bottom
310 321 * @return {Boolean}
311 322 * */
312 323 Cell.prototype.at_bottom = function () {
313 324 var cm = this.code_mirror;
314 325 var cursor = cm.getCursor();
315 326 if (cursor.line === (cm.lineCount()-1) && cursor.ch === cm.getLine(cursor.line).length) {
316 327 return true;
317 328 }
318 329 return false;
319 330 };
320 331
321 332 /**
322 333 * enter the command mode for the cell
323 334 * @method command_mode
324 335 * @return is the action being taken
325 336 */
326 337 Cell.prototype.command_mode = function () {
327 338 if (this.mode !== 'command') {
328 339 this.element.addClass('command_mode');
329 340 this.element.removeClass('edit_mode');
330 341 this.mode = 'command';
331 342 return true;
332 343 } else {
333 344 return false;
334 345 }
335 346 };
336 347
337 348 /**
338 349 * enter the edit mode for the cell
339 350 * @method command_mode
340 351 * @return is the action being taken
341 352 */
342 353 Cell.prototype.edit_mode = function () {
343 354 if (this.mode !== 'edit') {
344 355 this.element.addClass('edit_mode');
345 356 this.element.removeClass('command_mode');
346 357 this.mode = 'edit';
347 358 return true;
348 359 } else {
349 360 return false;
350 361 }
351 362 };
352 363
353 364 /**
354 365 * Focus the cell in the DOM sense
355 366 * @method focus_cell
356 367 */
357 368 Cell.prototype.focus_cell = function () {
358 369 this.element.focus();
359 370 };
360 371
361 372 /**
362 373 * Focus the editor area so a user can type
363 374 *
364 375 * NOTE: If codemirror is focused via a mouse click event, you don't want to
365 376 * call this because it will cause a page jump.
366 377 * @method focus_editor
367 378 */
368 379 Cell.prototype.focus_editor = function () {
369 380 this.refresh();
370 381 this.code_mirror.focus();
371 382 };
372 383
373 384 /**
374 385 * Refresh codemirror instance
375 386 * @method refresh
376 387 */
377 388 Cell.prototype.refresh = function () {
378 389 this.code_mirror.refresh();
379 390 };
380 391
381 392 /**
382 393 * should be overritten by subclass
383 394 * @method get_text
384 395 */
385 396 Cell.prototype.get_text = function () {
386 397 };
387 398
388 399 /**
389 400 * should be overritten by subclass
390 401 * @method set_text
391 402 * @param {string} text
392 403 */
393 404 Cell.prototype.set_text = function (text) {
394 405 };
395 406
396 407 /**
397 408 * should be overritten by subclass
398 409 * serialise cell to json.
399 410 * @method toJSON
400 411 **/
401 412 Cell.prototype.toJSON = function () {
402 413 var data = {};
403 414 // deepcopy the metadata so copied cells don't share the same object
404 415 data.metadata = JSON.parse(JSON.stringify(this.metadata));
405 416 data.cell_type = this.cell_type;
406 417 return data;
407 418 };
408 419
409
410 420 /**
411 421 * should be overritten by subclass
412 422 * @method fromJSON
413 423 **/
414 424 Cell.prototype.fromJSON = function (data) {
415 425 if (data.metadata !== undefined) {
416 426 this.metadata = data.metadata;
417 427 }
418 this.celltoolbar.rebuild();
419 428 };
420 429
421 430
422 431 /**
423 432 * can the cell be split into two cells (false if not deletable)
424 433 * @method is_splittable
425 434 **/
426 435 Cell.prototype.is_splittable = function () {
427 436 return this.is_deletable();
428 437 };
429 438
430 439
431 440 /**
432 441 * can the cell be merged with other cells (false if not deletable)
433 442 * @method is_mergeable
434 443 **/
435 444 Cell.prototype.is_mergeable = function () {
436 445 return this.is_deletable();
437 446 };
438 447
439 448 /**
440 449 * is the cell deletable? only false (undeletable) if
441 450 * metadata.deletable is explicitly false -- everything else
442 451 * counts as true
443 452 *
444 453 * @method is_deletable
445 454 **/
446 455 Cell.prototype.is_deletable = function () {
447 456 if (this.metadata.deletable === false) {
448 457 return false;
449 458 }
450 459 return true;
451 460 };
452 461
453 462 /**
454 463 * @return {String} - the text before the cursor
455 464 * @method get_pre_cursor
456 465 **/
457 466 Cell.prototype.get_pre_cursor = function () {
458 467 var cursor = this.code_mirror.getCursor();
459 468 var text = this.code_mirror.getRange({line:0, ch:0}, cursor);
460 469 text = text.replace(/^\n+/, '').replace(/\n+$/, '');
461 470 return text;
462 471 };
463 472
464 473
465 474 /**
466 475 * @return {String} - the text after the cursor
467 476 * @method get_post_cursor
468 477 **/
469 478 Cell.prototype.get_post_cursor = function () {
470 479 var cursor = this.code_mirror.getCursor();
471 480 var last_line_num = this.code_mirror.lineCount()-1;
472 481 var last_line_len = this.code_mirror.getLine(last_line_num).length;
473 482 var end = {line:last_line_num, ch:last_line_len};
474 483 var text = this.code_mirror.getRange(cursor, end);
475 484 text = text.replace(/^\n+/, '').replace(/\n+$/, '');
476 485 return text;
477 486 };
478 487
479 488 /**
480 489 * Show/Hide CodeMirror LineNumber
481 490 * @method show_line_numbers
482 491 *
483 492 * @param value {Bool} show (true), or hide (false) the line number in CodeMirror
484 493 **/
485 494 Cell.prototype.show_line_numbers = function (value) {
486 495 this.code_mirror.setOption('lineNumbers', value);
487 496 this.code_mirror.refresh();
488 497 };
489 498
490 499 /**
491 500 * Toggle CodeMirror LineNumber
492 501 * @method toggle_line_numbers
493 502 **/
494 503 Cell.prototype.toggle_line_numbers = function () {
495 504 var val = this.code_mirror.getOption('lineNumbers');
496 505 this.show_line_numbers(!val);
497 506 };
498 507
499 508 /**
500 509 * Force codemirror highlight mode
501 510 * @method force_highlight
502 511 * @param {object} - CodeMirror mode
503 512 **/
504 513 Cell.prototype.force_highlight = function(mode) {
505 514 this.user_highlight = mode;
506 515 this.auto_highlight();
507 516 };
508 517
509 518 /**
510 519 * Try to autodetect cell highlight mode, or use selected mode
511 520 * @methods _auto_highlight
512 521 * @private
513 522 * @param {String|object|undefined} - CodeMirror mode | 'auto'
514 523 **/
515 524 Cell.prototype._auto_highlight = function (modes) {
516 525 //Here we handle manually selected modes
517 526 var that = this;
518 527 var mode;
519 528 if( this.user_highlight !== undefined && this.user_highlight != 'auto' )
520 529 {
521 530 mode = this.user_highlight;
522 531 CodeMirror.autoLoadMode(this.code_mirror, mode);
523 532 this.code_mirror.setOption('mode', mode);
524 533 return;
525 534 }
526 535 var current_mode = this.code_mirror.getOption('mode', mode);
527 536 var first_line = this.code_mirror.getLine(0);
528 537 // loop on every pairs
529 538 for(mode in modes) {
530 539 var regs = modes[mode].reg;
531 540 // only one key every time but regexp can't be keys...
532 541 for(var i=0; i<regs.length; i++) {
533 542 // here we handle non magic_modes
534 543 if(first_line.match(regs[i]) !== null) {
535 544 if(current_mode == mode){
536 545 return;
537 546 }
538 547 if (mode.search('magic_') !== 0) {
539 548 utils.requireCodeMirrorMode(mode, function () {
540 549 that.code_mirror.setOption('mode', mode);
541 550 });
542 551 return;
543 552 }
544 553 var open = modes[mode].open || "%%";
545 554 var close = modes[mode].close || "%%end";
546 555 var magic_mode = mode;
547 556 mode = magic_mode.substr(6);
548 557 if(current_mode == magic_mode){
549 558 return;
550 559 }
551 560 utils.requireCodeMirrorMode(mode, function () {
552 561 // create on the fly a mode that switch between
553 562 // plain/text and something else, otherwise `%%` is
554 563 // source of some highlight issues.
555 564 CodeMirror.defineMode(magic_mode, function(config) {
556 565 return CodeMirror.multiplexingMode(
557 566 CodeMirror.getMode(config, 'text/plain'),
558 567 // always set something on close
559 568 {open: open, close: close,
560 569 mode: CodeMirror.getMode(config, mode),
561 570 delimStyle: "delimit"
562 571 }
563 572 );
564 573 });
565 574 that.code_mirror.setOption('mode', magic_mode);
566 575 });
567 576 return;
568 577 }
569 578 }
570 579 }
571 580 // fallback on default
572 581 var default_mode;
573 582 try {
574 583 default_mode = this._options.cm_config.mode;
575 584 } catch(e) {
576 585 default_mode = 'text/plain';
577 586 }
578 587 if( current_mode === default_mode){
579 588 return;
580 589 }
581 590 this.code_mirror.setOption('mode', default_mode);
582 591 };
583 592
584 593 // Backwards compatibility.
585 594 IPython.Cell = Cell;
586 595
587 596 return {'Cell': Cell};
588 597 });
General Comments 0
You need to be logged in to leave comments. Login now