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