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