##// END OF EJS Templates
Fix #4777 and #7887...
Fix #4777 and #7887 The function in charge of actually converting cursor offset to CodeMirror line number and character number was actually crashing when the cursor was at the last character (loop until undefined, then access length of variable, which is undefined). This was hiding a bug in which when you would completer to a single completion pressing tab after as-you-type filtering, the completion would be completed twice. The logic that was supposed to detect whether or not all completions had a common prefix was actually faulty as the common prefix used to be a string but was then changed to an object. Hence the logic to check whether or not there was actually a common prefix was always true, even for empty string, leading to the deletion of the line (replace by '') in some cases.

File last commit:

r20118:a58bc8bc
r20538:ae7f6d6a
Show More
textcell.js
375 lines | 11.7 KiB | application/javascript | JavascriptLexer
// Copyright (c) IPython Development Team.
// Distributed under the terms of the Modified BSD License.
define([
'base/js/namespace',
'base/js/utils',
'jquery',
'notebook/js/cell',
'base/js/security',
'services/config',
'notebook/js/mathjaxutils',
'notebook/js/celltoolbar',
'components/marked/lib/marked',
'codemirror/lib/codemirror',
'codemirror/mode/gfm/gfm',
'notebook/js/codemirror-ipythongfm'
], function(IPython,
utils,
$,
cell,
security,
configmod,
mathjaxutils,
celltoolbar,
marked,
CodeMirror,
gfm,
ipgfm
) {
"use strict";
var Cell = cell.Cell;
var TextCell = function (options) {
/**
* Constructor
*
* Construct a new TextCell, codemirror mode is by default 'htmlmixed',
* and cell type is 'text' cell start as not redered.
*
* Parameters:
* options: dictionary
* Dictionary of keyword arguments.
* events: $(Events) instance
* config: dictionary
* keyboard_manager: KeyboardManager instance
* notebook: Notebook instance
*/
options = options || {};
// in all TextCell/Cell subclasses
// do not assign most of members here, just pass it down
// in the options dict potentially overwriting what you wish.
// they will be assigned in the base class.
this.notebook = options.notebook;
this.events = options.events;
this.config = options.config;
// we cannot put this as a class key as it has handle to "this".
var config = utils.mergeopt(TextCell, this.config);
Cell.apply(this, [{
config: config,
keyboard_manager: options.keyboard_manager,
events: this.events}]);
this.cell_type = this.cell_type || 'text';
mathjaxutils = mathjaxutils;
this.rendered = false;
};
TextCell.prototype = Object.create(Cell.prototype);
TextCell.options_default = {
cm_config : {
extraKeys: {"Tab": "indentMore","Shift-Tab" : "indentLess"},
mode: 'htmlmixed',
lineWrapping : true,
}
};
/**
* Create the DOM element of the TextCell
* @method create_element
* @private
*/
TextCell.prototype.create_element = function () {
Cell.prototype.create_element.apply(this, arguments);
var that = this;
var cell = $("<div>").addClass('cell text_cell');
cell.attr('tabindex','2');
var prompt = $('<div/>').addClass('prompt input_prompt');
cell.append(prompt);
var inner_cell = $('<div/>').addClass('inner_cell');
this.celltoolbar = new celltoolbar.CellToolbar({
cell: this,
notebook: this.notebook});
inner_cell.append(this.celltoolbar.element);
var input_area = $('<div/>').addClass('input_area');
this.code_mirror = new CodeMirror(input_area.get(0), this.cm_config);
// In case of bugs that put the keyboard manager into an inconsistent state,
// ensure KM is enabled when CodeMirror is focused:
this.code_mirror.on('focus', function () {
if (that.keyboard_manager) {
that.keyboard_manager.enable();
}
});
this.code_mirror.on('keydown', $.proxy(this.handle_keyevent,this))
// The tabindex=-1 makes this div focusable.
var render_area = $('<div/>').addClass('text_cell_render rendered_html')
.attr('tabindex','-1');
inner_cell.append(input_area).append(render_area);
cell.append(inner_cell);
this.element = cell;
};
// Cell level actions
TextCell.prototype.select = function () {
var cont = Cell.prototype.select.apply(this);
if (cont) {
if (this.mode === 'edit') {
this.code_mirror.refresh();
}
}
return cont;
};
TextCell.prototype.unrender = function () {
if (this.read_only) return;
var cont = Cell.prototype.unrender.apply(this);
if (cont) {
var text_cell = this.element;
if (this.get_text() === this.placeholder) {
this.set_text('');
}
this.refresh();
}
return cont;
};
TextCell.prototype.execute = function () {
this.render();
};
/**
* setter: {{#crossLink "TextCell/set_text"}}{{/crossLink}}
* @method get_text
* @retrun {string} CodeMirror current text value
*/
TextCell.prototype.get_text = function() {
return this.code_mirror.getValue();
};
/**
* @param {string} text - Codemiror text value
* @see TextCell#get_text
* @method set_text
* */
TextCell.prototype.set_text = function(text) {
this.code_mirror.setValue(text);
this.unrender();
this.code_mirror.refresh();
};
/**
* setter :{{#crossLink "TextCell/set_rendered"}}{{/crossLink}}
* @method get_rendered
* */
TextCell.prototype.get_rendered = function() {
return this.element.find('div.text_cell_render').html();
};
/**
* @method set_rendered
*/
TextCell.prototype.set_rendered = function(text) {
this.element.find('div.text_cell_render').html(text);
};
/**
* Create Text cell from JSON
* @param {json} data - JSON serialized text-cell
* @method fromJSON
*/
TextCell.prototype.fromJSON = function (data) {
Cell.prototype.fromJSON.apply(this, arguments);
if (data.cell_type === this.cell_type) {
if (data.source !== undefined) {
this.set_text(data.source);
// make this value the starting point, so that we can only undo
// to this state, instead of a blank cell
this.code_mirror.clearHistory();
// TODO: This HTML needs to be treated as potentially dangerous
// user input and should be handled before set_rendered.
this.set_rendered(data.rendered || '');
this.rendered = false;
this.render();
}
}
};
/** Generate JSON from cell
* @return {object} cell data serialised to json
*/
TextCell.prototype.toJSON = function () {
var data = Cell.prototype.toJSON.apply(this);
data.source = this.get_text();
if (data.source == this.placeholder) {
data.source = "";
}
return data;
};
var MarkdownCell = function (options) {
/**
* Constructor
*
* Parameters:
* options: dictionary
* Dictionary of keyword arguments.
* events: $(Events) instance
* config: ConfigSection instance
* keyboard_manager: KeyboardManager instance
* notebook: Notebook instance
*/
options = options || {};
var config = utils.mergeopt(MarkdownCell, {});
TextCell.apply(this, [$.extend({}, options, {config: config})]);
this.class_config = new configmod.ConfigWithDefaults(options.config,
{}, 'MarkdownCell');
this.cell_type = 'markdown';
};
MarkdownCell.options_default = {
cm_config: {
mode: 'ipythongfm'
},
placeholder: "Type *Markdown* and LaTeX: $\\alpha^2$"
};
MarkdownCell.prototype = Object.create(TextCell.prototype);
MarkdownCell.prototype.set_heading_level = function (level) {
/**
* make a markdown cell a heading
*/
level = level || 1;
var source = this.get_text();
source = source.replace(/^(#*)\s?/,
new Array(level + 1).join('#') + ' ');
this.set_text(source);
this.refresh();
if (this.rendered) {
this.render();
}
};
/**
* @method render
*/
MarkdownCell.prototype.render = function () {
var cont = TextCell.prototype.render.apply(this);
if (cont) {
var that = this;
var text = this.get_text();
var math = null;
if (text === "") { text = this.placeholder; }
var text_and_math = mathjaxutils.remove_math(text);
text = text_and_math[0];
math = text_and_math[1];
marked(text, function (err, html) {
html = mathjaxutils.replace_math(html, math);
html = security.sanitize_html(html);
html = $($.parseHTML(html));
// add anchors to headings
html.find(":header").addBack(":header").each(function (i, h) {
h = $(h);
var hash = h.text().replace(/ /g, '-');
h.attr('id', hash);
h.append(
$('<a/>')
.addClass('anchor-link')
.attr('href', '#' + hash)
.text('ΒΆ')
);
});
// links in markdown cells should open in new tabs
html.find("a[href]").not('[href^="#"]').attr("target", "_blank");
that.set_rendered(html);
that.typeset();
that.events.trigger("rendered.MarkdownCell", {cell: that});
});
}
return cont;
};
var RawCell = function (options) {
/**
* Constructor
*
* Parameters:
* options: dictionary
* Dictionary of keyword arguments.
* events: $(Events) instance
* config: ConfigSection instance
* keyboard_manager: KeyboardManager instance
* notebook: Notebook instance
*/
options = options || {};
var config = utils.mergeopt(RawCell, {});
TextCell.apply(this, [$.extend({}, options, {config: config})]);
this.class_config = new configmod.ConfigWithDefaults(options.config,
RawCell.config_defaults, 'RawCell');
this.cell_type = 'raw';
};
RawCell.options_default = {
placeholder : "Write raw LaTeX or other formats here, for use with nbconvert. " +
"It will not be rendered in the notebook. " +
"When passing through nbconvert, a Raw Cell's content is added to the output unmodified."
};
RawCell.config_defaults = {
highlight_modes : {
'diff' :{'reg':[/^diff/]}
},
};
RawCell.prototype = Object.create(TextCell.prototype);
/** @method bind_events **/
RawCell.prototype.bind_events = function () {
TextCell.prototype.bind_events.apply(this);
var that = this;
this.element.focusout(function() {
that.auto_highlight();
that.render();
});
this.code_mirror.on('focus', function() { that.unrender(); });
};
/** @method render **/
RawCell.prototype.render = function () {
var cont = TextCell.prototype.render.apply(this);
if (cont){
var text = this.get_text();
if (text === "") { text = this.placeholder; }
this.set_text(text);
this.element.removeClass('rendered');
this.auto_highlight();
}
return cont;
};
// Backwards compatability.
IPython.TextCell = TextCell;
IPython.MarkdownCell = MarkdownCell;
IPython.RawCell = RawCell;
var textcell = {
TextCell: TextCell,
MarkdownCell: MarkdownCell,
RawCell: RawCell
};
return textcell;
});