##// END OF EJS Templates
Reverse hscrollbar min-height hack on OS X...
Reverse hscrollbar min-height hack on OS X OS X has optional behavior to only draw scrollbars during scroll, which causes problems for CodeMirror's scrollbars. CodeMirror's solution is to set a minimum size for their scrollbars, which is always present. The trade is that the container overlays most of the last line, swallowing click events when there is scrolling to do, even when no scrollbar is visible. This reverses the trade, recovering the click events at the expense of never showing the horizontal scrollbar on OS X when this option is enabled.

File last commit:

r20298:2907e856
r20298:2907e856
Show More
cell.js
692 lines | 21.1 KiB | application/javascript | JavascriptLexer
// Copyright (c) IPython Development Team.
// Distributed under the terms of the Modified BSD License.
/**
*
*
* @module cell
* @namespace cell
* @class Cell
*/
define([
'base/js/namespace',
'jquery',
'base/js/utils',
'codemirror/lib/codemirror',
'codemirror/addon/edit/matchbrackets',
'codemirror/addon/edit/closebrackets',
'codemirror/addon/comment/comment'
], function(IPython, $, utils, CodeMirror, cm_match, cm_closeb, cm_comment) {
// TODO: remove IPython dependency here
"use strict";
var overlayHack = CodeMirror.scrollbarModel.native.prototype.overlayHack;
CodeMirror.scrollbarModel.native.prototype.overlayHack = function () {
overlayHack.apply(this, arguments);
// Reverse `min-height: 18px` scrollbar hack on OS X
// which causes a dead area, making it impossible to click on the last line
// when there is horizontal scrolling to do and the "show scrollbar only when scrolling" behavior
// is enabled.
// This, in turn, has the undesirable behavior of never showing the horizontal scrollbar,
// even when it should, which is less problematic, at least.
if (/Mac/.test(navigator.platform)) {
this.horiz.style.minHeight = "";
}
};
var Cell = function (options) {
/* Constructor
*
* The Base `Cell` class from which to inherit.
* @constructor
* @param:
* options: dictionary
* Dictionary of keyword arguments.
* events: $(Events) instance
* config: dictionary
* keyboard_manager: KeyboardManager instance
*/
options = options || {};
this.keyboard_manager = options.keyboard_manager;
this.events = options.events;
var config = utils.mergeopt(Cell, options.config);
// superclass default overwrite our default
this.placeholder = config.placeholder || '';
this.read_only = config.cm_config.readOnly;
this.selected = false;
this.rendered = false;
this.mode = 'command';
// Metadata property
var that = this;
this._metadata = {};
Object.defineProperty(this, 'metadata', {
get: function() { return that._metadata; },
set: function(value) {
that._metadata = value;
if (that.celltoolbar) {
that.celltoolbar.rebuild();
}
}
});
// load this from metadata later ?
this.user_highlight = 'auto';
this.cm_config = config.cm_config;
this.cell_id = utils.uuid();
this._options = config;
// For JS VM engines optimization, attributes should be all set (even
// to null) in the constructor, and if possible, if different subclass
// have new attributes with same name, they should be created in the
// same order. Easiest is to create and set to null in parent class.
this.element = null;
this.cell_type = this.cell_type || null;
this.code_mirror = null;
this.create_element();
if (this.element !== null) {
this.element.data("cell", this);
this.bind_events();
this.init_classes();
}
};
Cell.options_default = {
cm_config : {
indentUnit : 4,
readOnly: false,
theme: "default",
extraKeys: {
"Cmd-Right":"goLineRight",
"End":"goLineRight",
"Cmd-Left":"goLineLeft"
}
}
};
// FIXME: Workaround CM Bug #332 (Safari segfault on drag)
// by disabling drag/drop altogether on Safari
// https://github.com/codemirror/CodeMirror/issues/332
if (utils.browser[0] == "Safari") {
Cell.options_default.cm_config.dragDrop = false;
}
/**
* Empty. Subclasses must implement create_element.
* This should contain all the code to create the DOM element in notebook
* and will be called by Base Class constructor.
* @method create_element
*/
Cell.prototype.create_element = function () {
};
Cell.prototype.init_classes = function () {
/**
* Call after this.element exists to initialize the css classes
* related to selected, rendered and mode.
*/
if (this.selected) {
this.element.addClass('selected');
} else {
this.element.addClass('unselected');
}
if (this.rendered) {
this.element.addClass('rendered');
} else {
this.element.addClass('unrendered');
}
};
/**
* Subclasses can implement override bind_events.
* Be carefull to call the parent method when overwriting as it fires event.
* this will be triggerd after create_element in constructor.
* @method bind_events
*/
Cell.prototype.bind_events = function () {
var that = this;
// We trigger events so that Cell doesn't have to depend on Notebook.
that.element.click(function (event) {
if (!that.selected) {
that.events.trigger('select.Cell', {'cell':that});
}
});
that.element.focusin(function (event) {
if (!that.selected) {
that.events.trigger('select.Cell', {'cell':that});
}
});
if (this.code_mirror) {
this.code_mirror.on("change", function(cm, change) {
that.events.trigger("set_dirty.Notebook", {value: true});
});
}
if (this.code_mirror) {
this.code_mirror.on('focus', function(cm, change) {
that.events.trigger('edit_mode.Cell', {cell: that});
});
}
if (this.code_mirror) {
this.code_mirror.on('blur', function(cm, change) {
that.events.trigger('command_mode.Cell', {cell: that});
});
}
this.element.dblclick(function () {
if (that.selected === false) {
this.events.trigger('select.Cell', {'cell':that});
}
var cont = that.unrender();
if (cont) {
that.focus_editor();
}
});
};
/**
* This method gets called in CodeMirror's onKeyDown/onKeyPress
* handlers and is used to provide custom key handling.
*
* To have custom handling, subclasses should override this method, but still call it
* in order to process the Edit mode keyboard shortcuts.
*
* @method handle_codemirror_keyevent
* @param {CodeMirror} editor - The codemirror instance bound to the cell
* @param {event} event - key press event which either should or should not be handled by CodeMirror
* @return {Boolean} `true` if CodeMirror should ignore the event, `false` Otherwise
*/
Cell.prototype.handle_codemirror_keyevent = function (editor, event) {
var shortcuts = this.keyboard_manager.edit_shortcuts;
var cur = editor.getCursor();
if((cur.line !== 0 || cur.ch !==0) && event.keyCode === 38){
event._ipkmIgnore = true;
}
var nLastLine = editor.lastLine();
if ((event.keyCode === 40) &&
((cur.line !== nLastLine) ||
(cur.ch !== editor.getLineHandle(nLastLine).text.length))
) {
event._ipkmIgnore = true;
}
// if this is an edit_shortcuts shortcut, the global keyboard/shortcut
// manager will handle it
if (shortcuts.handles(event)) {
return true;
}
return false;
};
/**
* Triger typsetting of math by mathjax on current cell element
* @method typeset
*/
Cell.prototype.typeset = function () {
utils.typeset(this.element);
};
/**
* handle cell level logic when a cell is selected
* @method select
* @return is the action being taken
*/
Cell.prototype.select = function () {
if (!this.selected) {
this.element.addClass('selected');
this.element.removeClass('unselected');
this.selected = true;
return true;
} else {
return false;
}
};
/**
* handle cell level logic when a cell is unselected
* @method unselect
* @return is the action being taken
*/
Cell.prototype.unselect = function () {
if (this.selected) {
this.element.addClass('unselected');
this.element.removeClass('selected');
this.selected = false;
return true;
} else {
return false;
}
};
/**
* should be overritten by subclass
* @method execute
*/
Cell.prototype.execute = function () {
return;
};
/**
* handle cell level logic when a cell is rendered
* @method render
* @return is the action being taken
*/
Cell.prototype.render = function () {
if (!this.rendered) {
this.element.addClass('rendered');
this.element.removeClass('unrendered');
this.rendered = true;
return true;
} else {
return false;
}
};
/**
* handle cell level logic when a cell is unrendered
* @method unrender
* @return is the action being taken
*/
Cell.prototype.unrender = function () {
if (this.rendered) {
this.element.addClass('unrendered');
this.element.removeClass('rendered');
this.rendered = false;
return true;
} else {
return false;
}
};
/**
* Delegates keyboard shortcut handling to either IPython keyboard
* manager when in command mode, or CodeMirror when in edit mode
*
* @method handle_keyevent
* @param {CodeMirror} editor - The codemirror instance bound to the cell
* @param {event} - key event to be handled
* @return {Boolean} `true` if CodeMirror should ignore the event, `false` Otherwise
*/
Cell.prototype.handle_keyevent = function (editor, event) {
if (this.mode === 'command') {
return true;
} else if (this.mode === 'edit') {
return this.handle_codemirror_keyevent(editor, event);
}
};
/**
* @method at_top
* @return {Boolean}
*/
Cell.prototype.at_top = function () {
var cm = this.code_mirror;
var cursor = cm.getCursor();
if (cursor.line === 0 && cursor.ch === 0) {
return true;
}
return false;
};
/**
* @method at_bottom
* @return {Boolean}
* */
Cell.prototype.at_bottom = function () {
var cm = this.code_mirror;
var cursor = cm.getCursor();
if (cursor.line === (cm.lineCount()-1) && cursor.ch === cm.getLine(cursor.line).length) {
return true;
}
return false;
};
/**
* enter the command mode for the cell
* @method command_mode
* @return is the action being taken
*/
Cell.prototype.command_mode = function () {
if (this.mode !== 'command') {
this.mode = 'command';
return true;
} else {
return false;
}
};
/**
* enter the edit mode for the cell
* @method command_mode
* @return is the action being taken
*/
Cell.prototype.edit_mode = function () {
if (this.mode !== 'edit') {
this.mode = 'edit';
return true;
} else {
return false;
}
};
/**
* Focus the cell in the DOM sense
* @method focus_cell
*/
Cell.prototype.focus_cell = function () {
this.element.focus();
};
/**
* Focus the editor area so a user can type
*
* NOTE: If codemirror is focused via a mouse click event, you don't want to
* call this because it will cause a page jump.
* @method focus_editor
*/
Cell.prototype.focus_editor = function () {
this.refresh();
this.code_mirror.focus();
};
/**
* Refresh codemirror instance
* @method refresh
*/
Cell.prototype.refresh = function () {
if (this.code_mirror) {
this.code_mirror.refresh();
}
};
/**
* should be overritten by subclass
* @method get_text
*/
Cell.prototype.get_text = function () {
};
/**
* should be overritten by subclass
* @method set_text
* @param {string} text
*/
Cell.prototype.set_text = function (text) {
};
/**
* should be overritten by subclass
* serialise cell to json.
* @method toJSON
**/
Cell.prototype.toJSON = function () {
var data = {};
// deepcopy the metadata so copied cells don't share the same object
data.metadata = JSON.parse(JSON.stringify(this.metadata));
data.cell_type = this.cell_type;
return data;
};
/**
* should be overritten by subclass
* @method fromJSON
**/
Cell.prototype.fromJSON = function (data) {
if (data.metadata !== undefined) {
this.metadata = data.metadata;
}
};
/**
* can the cell be split into two cells (false if not deletable)
* @method is_splittable
**/
Cell.prototype.is_splittable = function () {
return this.is_deletable();
};
/**
* can the cell be merged with other cells (false if not deletable)
* @method is_mergeable
**/
Cell.prototype.is_mergeable = function () {
return this.is_deletable();
};
/**
* is the cell deletable? only false (undeletable) if
* metadata.deletable is explicitly false -- everything else
* counts as true
*
* @method is_deletable
**/
Cell.prototype.is_deletable = function () {
if (this.metadata.deletable === false) {
return false;
}
return true;
};
/**
* @return {String} - the text before the cursor
* @method get_pre_cursor
**/
Cell.prototype.get_pre_cursor = function () {
var cursor = this.code_mirror.getCursor();
var text = this.code_mirror.getRange({line:0, ch:0}, cursor);
text = text.replace(/^\n+/, '').replace(/\n+$/, '');
return text;
};
/**
* @return {String} - the text after the cursor
* @method get_post_cursor
**/
Cell.prototype.get_post_cursor = function () {
var cursor = this.code_mirror.getCursor();
var last_line_num = this.code_mirror.lineCount()-1;
var last_line_len = this.code_mirror.getLine(last_line_num).length;
var end = {line:last_line_num, ch:last_line_len};
var text = this.code_mirror.getRange(cursor, end);
text = text.replace(/^\n+/, '').replace(/\n+$/, '');
return text;
};
/**
* Show/Hide CodeMirror LineNumber
* @method show_line_numbers
*
* @param value {Bool} show (true), or hide (false) the line number in CodeMirror
**/
Cell.prototype.show_line_numbers = function (value) {
this.code_mirror.setOption('lineNumbers', value);
this.code_mirror.refresh();
};
/**
* Toggle CodeMirror LineNumber
* @method toggle_line_numbers
**/
Cell.prototype.toggle_line_numbers = function () {
var val = this.code_mirror.getOption('lineNumbers');
this.show_line_numbers(!val);
};
/**
* Force codemirror highlight mode
* @method force_highlight
* @param {object} - CodeMirror mode
**/
Cell.prototype.force_highlight = function(mode) {
this.user_highlight = mode;
this.auto_highlight();
};
/**
* Trigger autodetection of highlight scheme for current cell
* @method auto_highlight
*/
Cell.prototype.auto_highlight = function () {
this._auto_highlight(this.class_config.get_sync('highlight_modes'));
};
/**
* Try to autodetect cell highlight mode, or use selected mode
* @methods _auto_highlight
* @private
* @param {String|object|undefined} - CodeMirror mode | 'auto'
**/
Cell.prototype._auto_highlight = function (modes) {
/**
*Here we handle manually selected modes
*/
var that = this;
var mode;
if( this.user_highlight !== undefined && this.user_highlight != 'auto' )
{
mode = this.user_highlight;
CodeMirror.autoLoadMode(this.code_mirror, mode);
this.code_mirror.setOption('mode', mode);
return;
}
var current_mode = this.code_mirror.getOption('mode', mode);
var first_line = this.code_mirror.getLine(0);
// loop on every pairs
for(mode in modes) {
var regs = modes[mode].reg;
// only one key every time but regexp can't be keys...
for(var i=0; i<regs.length; i++) {
// here we handle non magic_modes
if(first_line.match(regs[i]) !== null) {
if(current_mode == mode){
return;
}
if (mode.search('magic_') !== 0) {
utils.requireCodeMirrorMode(mode, function (spec) {
that.code_mirror.setOption('mode', spec);
});
return;
}
var open = modes[mode].open || "%%";
var close = modes[mode].close || "%%end";
var magic_mode = mode;
mode = magic_mode.substr(6);
if(current_mode == magic_mode){
return;
}
utils.requireCodeMirrorMode(mode, function (spec) {
// create on the fly a mode that switch between
// plain/text and something else, otherwise `%%` is
// source of some highlight issues.
CodeMirror.defineMode(magic_mode, function(config) {
return CodeMirror.multiplexingMode(
CodeMirror.getMode(config, 'text/plain'),
// always set something on close
{open: open, close: close,
mode: CodeMirror.getMode(config, spec),
delimStyle: "delimit"
}
);
});
that.code_mirror.setOption('mode', magic_mode);
});
return;
}
}
}
// fallback on default
var default_mode;
try {
default_mode = this._options.cm_config.mode;
} catch(e) {
default_mode = 'text/plain';
}
if( current_mode === default_mode){
return;
}
this.code_mirror.setOption('mode', default_mode);
};
var UnrecognizedCell = function (options) {
/** Constructor for unrecognized cells */
Cell.apply(this, arguments);
this.cell_type = 'unrecognized';
this.celltoolbar = null;
this.data = {};
Object.seal(this);
};
UnrecognizedCell.prototype = Object.create(Cell.prototype);
// cannot merge or split unrecognized cells
UnrecognizedCell.prototype.is_mergeable = function () {
return false;
};
UnrecognizedCell.prototype.is_splittable = function () {
return false;
};
UnrecognizedCell.prototype.toJSON = function () {
/**
* deepcopy the metadata so copied cells don't share the same object
*/
return JSON.parse(JSON.stringify(this.data));
};
UnrecognizedCell.prototype.fromJSON = function (data) {
this.data = data;
if (data.metadata !== undefined) {
this.metadata = data.metadata;
} else {
data.metadata = this.metadata;
}
this.element.find('.inner_cell').find("a").text("Unrecognized cell type: " + data.cell_type);
};
UnrecognizedCell.prototype.create_element = function () {
Cell.prototype.create_element.apply(this, arguments);
var cell = this.element = $("<div>").addClass('cell unrecognized_cell');
cell.attr('tabindex','2');
var prompt = $('<div/>').addClass('prompt input_prompt');
cell.append(prompt);
var inner_cell = $('<div/>').addClass('inner_cell');
inner_cell.append(
$("<a>")
.attr("href", "#")
.text("Unrecognized cell type")
);
cell.append(inner_cell);
this.element = cell;
};
UnrecognizedCell.prototype.bind_events = function () {
Cell.prototype.bind_events.apply(this, arguments);
var cell = this;
this.element.find('.inner_cell').find("a").click(function () {
cell.events.trigger('unrecognized_cell.Cell', {cell: cell});
});
};
// Backwards compatibility.
IPython.Cell = Cell;
return {
Cell: Cell,
UnrecognizedCell: UnrecognizedCell
};
});