textcell.js
580 lines
| 17.1 KiB
| application/javascript
|
JavascriptLexer
|
r4609 | //---------------------------------------------------------------------------- | ||
|
r8565 | // Copyright (C) 2008-2012 The IPython Development Team | ||
|
r4609 | // | ||
// Distributed under the terms of the BSD License. The full license is in | ||||
// the file COPYING, distributed as part of this software. | ||||
//---------------------------------------------------------------------------- | ||||
|
r4349 | |||
//============================================================================ | ||||
|
r4508 | // TextCell | ||
|
r4349 | //============================================================================ | ||
|
r10165 | |||
|
r8709 | /** | ||
A module that allow to create different type of Text Cell | ||||
|
r8739 | @module IPython | ||
@namespace IPython | ||||
|
r8709 | */ | ||
|
r4352 | var IPython = (function (IPython) { | ||
|
r11526 | "use strict"; | ||
|
r4349 | |||
|
r4508 | // TextCell base class | ||
|
r8168 | var key = IPython.utils.keycodes; | ||
|
r4508 | |||
|
r8709 | /** | ||
|
r8711 | * Construct a new TextCell, codemirror mode is by default 'htmlmixed', and cell type is 'text' | ||
* cell start as not redered. | ||||
* | ||||
|
r8709 | * @class TextCell | ||
|
r8711 | * @constructor TextCell | ||
|
r11288 | * @extend IPython.Cell | ||
|
r9537 | * @param {object|undefined} [options] | ||
* @param [options.cm_config] {object} config to pass to CodeMirror, will extend/overwrite default config | ||||
|
r10165 | * @param [options.placeholder] {string} default string to use when souce in empty for rendering (only use in some TextCell subclass) | ||
|
r8709 | */ | ||
|
r9537 | var TextCell = function (options) { | ||
|
r10165 | // 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. | ||||
|
r9537 | |||
|
r10165 | // we cannot put this as a class key as it has handle to "this". | ||
|
r9537 | var cm_overwrite_options = { | ||
onKeyEvent: $.proxy(this.handle_codemirror_keyevent,this) | ||||
}; | ||||
|
r10165 | options = this.mergeopt(TextCell,options,{cm_config:cm_overwrite_options}); | ||
|
r9537 | |||
|
r13668 | this.cell_type = this.cell_type || 'text'; | ||
|
r9537 | |||
|
r13668 | IPython.Cell.apply(this, [options]); | ||
|
r9537 | |||
|
r4352 | this.rendered = false; | ||
}; | ||||
|
r10165 | TextCell.prototype = new IPython.Cell(); | ||
TextCell.options_default = { | ||||
cm_config : { | ||||
extraKeys: {"Tab": "indentMore","Shift-Tab" : "indentLess"}, | ||||
mode: 'htmlmixed', | ||||
|
r9537 | lineWrapping : true, | ||
} | ||||
|
r10165 | }; | ||
|
r9537 | |||
|
r4352 | |||
|
r8711 | /** | ||
* Create the DOM element of the TextCell | ||||
|
r8709 | * @method create_element | ||
* @private | ||||
*/ | ||||
|
r4508 | TextCell.prototype.create_element = function () { | ||
|
r9073 | IPython.Cell.prototype.create_element.apply(this, arguments); | ||
|
r13776 | |||
|
r10217 | var cell = $("<div>").addClass('cell text_cell border-box-sizing'); | ||
|
r4629 | cell.attr('tabindex','2'); | ||
|
r9144 | |||
|
r13776 | var prompt = $('<div/>').addClass('prompt input_prompt'); | ||
cell.append(prompt); | ||||
var inner_cell = $('<div/>').addClass('inner_cell'); | ||||
|
r9142 | this.celltoolbar = new IPython.CellToolbar(this); | ||
|
r13776 | inner_cell.append(this.celltoolbar.element); | ||
|
r5946 | var input_area = $('<div/>').addClass('text_cell_input border-box-sizing'); | ||
|
r9537 | this.code_mirror = CodeMirror(input_area.get(0), this.cm_config); | ||
|
r4499 | // The tabindex=-1 makes this div focusable. | ||
|
r5946 | var render_area = $('<div/>').addClass('text_cell_render border-box-sizing'). | ||
|
r8661 | addClass('rendered_html').attr('tabindex','-1'); | ||
|
r13776 | inner_cell.append(input_area).append(render_area); | ||
cell.append(inner_cell); | ||||
|
r4352 | this.element = cell; | ||
}; | ||||
|
r8711 | /** | ||
* Bind the DOM evet to cell actions | ||||
|
r8709 | * Need to be called after TextCell.create_element | ||
* @private | ||||
* @method bind_event | ||||
*/ | ||||
|
r4508 | TextCell.prototype.bind_events = function () { | ||
|
r4352 | IPython.Cell.prototype.bind_events.apply(this); | ||
var that = this; | ||||
this.element.keydown(function (event) { | ||||
|
r6050 | if (event.which === 13 && !event.shiftKey) { | ||
|
r4352 | if (that.rendered) { | ||
that.edit(); | ||||
|
r5942 | return false; | ||
|
r5946 | }; | ||
}; | ||||
}); | ||||
this.element.dblclick(function () { | ||||
that.edit(); | ||||
|
r4352 | }); | ||
}; | ||||
|
r4349 | |||
|
r8711 | /** | ||
* This method gets called in CodeMirror's onKeyDown/onKeyPress | ||||
|
r8709 | * handlers and is used to provide custom key handling. | ||
* | ||||
|
r8711 | * Subclass should override this method to have custom handeling | ||
* | ||||
|
r8709 | * @method handle_codemirror_keyevent | ||
|
r8711 | * @param {CodeMirror} editor - The codemirror instance bound to the cell | ||
|
r8709 | * @param {event} event - | ||
|
r8711 | * @return {Boolean} `true` if CodeMirror should ignore the event, `false` Otherwise | ||
|
r8709 | */ | ||
|
r5942 | TextCell.prototype.handle_codemirror_keyevent = function (editor, event) { | ||
|
r8709 | |||
|
r5942 | if (event.keyCode === 13 && (event.shiftKey || event.ctrlKey)) { | ||
// Always ignore shift-enter in CodeMirror as we handle it. | ||||
return true; | ||||
} | ||||
return false; | ||||
}; | ||||
|
r8709 | /** | ||
|
r8711 | * Select the current cell and trigger 'focus' | ||
|
r8709 | * @method select | ||
*/ | ||||
|
r4508 | TextCell.prototype.select = function () { | ||
|
r4352 | IPython.Cell.prototype.select.apply(this); | ||
|
r4508 | var output = this.element.find("div.text_cell_render"); | ||
|
r4352 | output.trigger('focus'); | ||
}; | ||||
|
r4349 | |||
|
r8711 | /** | ||
* unselect the current cell and `render` it | ||||
|
r8709 | * @method unselect | ||
*/ | ||||
|
r5833 | TextCell.prototype.unselect = function() { | ||
// render on selection of another cell | ||||
this.render(); | ||||
IPython.Cell.prototype.unselect.apply(this); | ||||
}; | ||||
|
r8711 | /** | ||
* | ||||
* put the current cell in edition mode | ||||
|
r8709 | * @method edit | ||
*/ | ||||
|
r4508 | TextCell.prototype.edit = function () { | ||
|
r4352 | if (this.rendered === true) { | ||
|
r4508 | var text_cell = this.element; | ||
|
r8709 | var output = text_cell.find("div.text_cell_render"); | ||
|
r4352 | output.hide(); | ||
|
r4508 | text_cell.find('div.text_cell_input').show(); | ||
|
r5946 | this.code_mirror.refresh(); | ||
|
r4499 | this.code_mirror.focus(); | ||
|
r5971 | // We used to need an additional refresh() after the focus, but | ||
// it appears that this has been fixed in CM. This bug would show | ||||
// up on FF when a newly loaded markdown cell was edited. | ||||
|
r4352 | this.rendered = false; | ||
|
r5943 | if (this.get_text() === this.placeholder) { | ||
this.set_text(''); | ||||
|
r5946 | this.refresh(); | ||
|
r5479 | } | ||
} | ||||
|
r4349 | }; | ||
|
r8711 | /** | ||
* Empty, Subclasses must define render. | ||||
|
r8709 | * @method render | ||
*/ | ||||
|
r4508 | TextCell.prototype.render = function () {}; | ||
|
r4349 | |||
|
r8709 | /** | ||
* setter: {{#crossLink "TextCell/set_text"}}{{/crossLink}} | ||||
* @method get_text | ||||
* @retrun {string} CodeMirror current text value | ||||
*/ | ||||
|
r5943 | TextCell.prototype.get_text = function() { | ||
|
r4499 | return this.code_mirror.getValue(); | ||
|
r4352 | }; | ||
|
r4349 | |||
|
r8709 | /** | ||
* @param {string} text - Codemiror text value | ||||
* @see TextCell#get_text | ||||
* @method set_text | ||||
* */ | ||||
|
r5943 | TextCell.prototype.set_text = function(text) { | ||
|
r4499 | this.code_mirror.setValue(text); | ||
this.code_mirror.refresh(); | ||||
|
r4352 | }; | ||
|
r4349 | |||
|
r8709 | /** | ||
* setter :{{#crossLink "TextCell/set_rendered"}}{{/crossLink}} | ||||
* @method get_rendered | ||||
* @return {html} html of rendered element | ||||
* */ | ||||
|
r4513 | TextCell.prototype.get_rendered = function() { | ||
|
r4508 | return this.element.find('div.text_cell_render').html(); | ||
}; | ||||
|
r8709 | /** | ||
* @method set_rendered | ||||
*/ | ||||
|
r4508 | TextCell.prototype.set_rendered = function(text) { | ||
this.element.find('div.text_cell_render').html(text); | ||||
|
r4352 | }; | ||
|
r4349 | |||
|
r8709 | /** | ||
* not deprecated, but implementation wrong | ||||
* @method at_top | ||||
* @deprecated | ||||
* @return {Boolean} true is cell rendered, false otherwise | ||||
* I doubt this is what it is supposed to do | ||||
* this implementation is completly false | ||||
*/ | ||||
|
r4508 | TextCell.prototype.at_top = function () { | ||
|
r4352 | if (this.rendered) { | ||
return true; | ||||
} else { | ||||
return false; | ||||
} | ||||
}; | ||||
|
r8709 | /** | ||
* not deprecated, but implementation wrong | ||||
* @method at_bottom | ||||
* @deprecated | ||||
* @return {Boolean} true is cell rendered, false otherwise | ||||
* I doubt this is what it is supposed to do | ||||
* this implementation is completly false | ||||
* */ | ||||
|
r4508 | TextCell.prototype.at_bottom = function () { | ||
|
r4352 | if (this.rendered) { | ||
return true; | ||||
} else { | ||||
return false; | ||||
} | ||||
}; | ||||
|
r4349 | |||
|
r8711 | /** | ||
* Create Text cell from JSON | ||||
|
r8709 | * @param {json} data - JSON serialized text-cell | ||
|
r8711 | * @method fromJSON | ||
|
r8709 | */ | ||
|
r4508 | TextCell.prototype.fromJSON = function (data) { | ||
|
r7523 | IPython.Cell.prototype.fromJSON.apply(this, arguments); | ||
|
r4508 | if (data.cell_type === this.cell_type) { | ||
|
r4499 | if (data.source !== undefined) { | ||
|
r5943 | this.set_text(data.source); | ||
|
r7587 | // make this value the starting point, so that we can only undo | ||
// to this state, instead of a blank cell | ||||
this.code_mirror.clearHistory(); | ||||
|
r4513 | this.set_rendered(data.rendered || ''); | ||
this.rendered = false; | ||||
this.render(); | ||||
|
r5479 | } | ||
} | ||||
|
r4513 | }; | ||
|
r4349 | |||
|
r8711 | /** Generate JSON from cell | ||
* @return {object} cell data serialised to json | ||||
*/ | ||||
|
r4508 | TextCell.prototype.toJSON = function () { | ||
|
r7523 | var data = IPython.Cell.prototype.toJSON.apply(this); | ||
|
r5943 | data.source = this.get_text(); | ||
|
r13668 | if (data.source == this.placeholder) { | ||
data.source = ""; | ||||
} | ||||
|
r4484 | return data; | ||
|
r4349 | }; | ||
|
r4508 | |||
|
r8709 | /** | ||
|
r8711 | * @class MarkdownCell | ||
* @constructor MarkdownCell | ||||
|
r11288 | * @extends IPython.HTMLCell | ||
|
r8709 | */ | ||
|
r10165 | var MarkdownCell = function (options) { | ||
|
r13668 | options = this.mergeopt(MarkdownCell, options); | ||
|
r10165 | |||
|
r4508 | this.cell_type = 'markdown'; | ||
|
r13668 | TextCell.apply(this, [options]); | ||
|
r4508 | }; | ||
|
r10165 | MarkdownCell.options_default = { | ||
cm_config: { | ||||
|
r10539 | mode: 'gfm' | ||
|
r10165 | }, | ||
placeholder: "Type *Markdown* and LaTeX: $\\alpha^2$" | ||||
} | ||||
|
r4508 | |||
MarkdownCell.prototype = new TextCell(); | ||||
|
r8711 | /** | ||
* @method render | ||||
*/ | ||||
|
r4508 | MarkdownCell.prototype.render = function () { | ||
if (this.rendered === false) { | ||||
|
r5943 | var text = this.get_text(); | ||
|
r11843 | var math = null; | ||
|
r5479 | if (text === "") { text = this.placeholder; } | ||
|
r11843 | var text_and_math = IPython.mathjaxutils.remove_math(text); | ||
text = text_and_math[0]; | ||||
math = text_and_math[1]; | ||||
|
r10536 | var html = marked.parser(marked.lexer(text)); | ||
|
r11524 | html = $(IPython.mathjaxutils.replace_math(html, math)); | ||
|
r11211 | // links in markdown cells should open in new tabs | ||
|
r11805 | html.find("a[href]").not('[href^="#"]').attr("target", "_blank"); | ||
|
r8662 | try { | ||
this.set_rendered(html); | ||||
} catch (e) { | ||||
console.log("Error running Javascript in Markdown:"); | ||||
console.log(e); | ||||
this.set_rendered($("<div/>").addClass("js-error").html( | ||||
"Error rendering Markdown!<br/>" + e.toString()) | ||||
); | ||||
|
r8052 | } | ||
|
r8662 | this.element.find('div.text_cell_input').hide(); | ||
this.element.find("div.text_cell_render").show(); | ||||
|
r13668 | this.typeset(); | ||
|
r4508 | this.rendered = true; | ||
|
r5479 | } | ||
|
r4508 | }; | ||
|
r6248 | // RawCell | ||
|
r4508 | |||
|
r8711 | /** | ||
* @class RawCell | ||||
* @constructor RawCell | ||||
|
r11288 | * @extends IPython.TextCell | ||
|
r8709 | */ | ||
|
r10165 | var RawCell = function (options) { | ||
|
r13668 | options = this.mergeopt(RawCell, options); | ||
|
r6248 | this.cell_type = 'raw'; | ||
|
r13668 | TextCell.apply(this, [options]); | ||
|
r8202 | |||
|
r13668 | var that = this; | ||
|
r8202 | this.element.focusout( | ||
function() { that.auto_highlight(); } | ||||
|
r13676 | ); | ||
|
r4508 | }; | ||
|
r10165 | RawCell.options_default = { | ||
|
r13769 | placeholder : "Write raw LaTeX or other formats here, for use with nbconvert.\n" + | ||
|
r13668 | "It will not be rendered in the notebook.\n" + | ||
"When passing through nbconvert, a Raw Cell's content is added to the output unmodified." | ||||
|
r10165 | }; | ||
|
r4508 | |||
|
r6248 | RawCell.prototype = new TextCell(); | ||
|
r4508 | |||
|
r8711 | /** | ||
* Trigger autodetection of highlight scheme for current cell | ||||
* @method auto_highlight | ||||
*/ | ||||
|
r8202 | RawCell.prototype.auto_highlight = function () { | ||
this._auto_highlight(IPython.config.raw_cell_highlight); | ||||
}; | ||||
|
r4508 | |||
|
r8711 | /** @method render **/ | ||
|
r6248 | RawCell.prototype.render = function () { | ||
|
r6017 | this.rendered = true; | ||
|
r13668 | var text = this.get_text(); | ||
if (text === "") { text = this.placeholder; } | ||||
console.log('rendering', text); | ||||
this.set_text(text); | ||||
|
r6017 | }; | ||
|
r8711 | /** @method handle_codemirror_keyevent **/ | ||
|
r8168 | RawCell.prototype.handle_codemirror_keyevent = function (editor, event) { | ||
var that = this; | ||||
if (event.which === key.UPARROW && event.type === 'keydown') { | ||||
// If we are not at the top, let CM handle the up arrow and | ||||
// prevent the global keydown handler from handling it. | ||||
if (!that.at_top()) { | ||||
event.stop(); | ||||
return false; | ||||
} else { | ||||
return true; | ||||
}; | ||||
} else if (event.which === key.DOWNARROW && event.type === 'keydown') { | ||||
// If we are not at the bottom, let CM handle the down arrow and | ||||
// prevent the global keydown handler from handling it. | ||||
if (!that.at_bottom()) { | ||||
event.stop(); | ||||
return false; | ||||
} else { | ||||
return true; | ||||
}; | ||||
}; | ||||
return false; | ||||
}; | ||||
|
r8711 | /** @method select **/ | ||
|
r6248 | RawCell.prototype.select = function () { | ||
|
r6017 | IPython.Cell.prototype.select.apply(this); | ||
|
r13668 | this.edit(); | ||
|
r6017 | }; | ||
|
r8711 | /** @method at_top **/ | ||
|
r6248 | RawCell.prototype.at_top = function () { | ||
|
r6017 | var cursor = this.code_mirror.getCursor(); | ||
|
r8168 | if (cursor.line === 0 && cursor.ch === 0) { | ||
|
r6017 | return true; | ||
} else { | ||||
return false; | ||||
|
r5479 | } | ||
|
r4508 | }; | ||
|
r8711 | /** @method at_bottom **/ | ||
|
r6248 | RawCell.prototype.at_bottom = function () { | ||
|
r6017 | var cursor = this.code_mirror.getCursor(); | ||
|
r8168 | if (cursor.line === (this.code_mirror.lineCount()-1) && cursor.ch === this.code_mirror.getLine(cursor.line).length) { | ||
|
r6017 | return true; | ||
} else { | ||||
return false; | ||||
} | ||||
|
r4508 | }; | ||
|
r8714 | /** | ||
|
r8711 | * @class HeadingCell | ||
|
r11288 | * @extends IPython.TextCell | ||
|
r8711 | */ | ||
/** | ||||
* @constructor HeadingCell | ||||
|
r11288 | * @extends IPython.TextCell | ||
|
r8709 | */ | ||
|
r10165 | var HeadingCell = function (options) { | ||
|
r13668 | options = this.mergeopt(HeadingCell, options); | ||
|
r10165 | |||
|
r13668 | this.level = 1; | ||
this.cell_type = 'heading'; | ||||
|
r10165 | TextCell.apply(this, [options]); | ||
|
r8711 | /** | ||
* heading level of the cell, use getter and setter to access | ||||
* @property level | ||||
*/ | ||||
|
r6018 | }; | ||
|
r10165 | HeadingCell.options_default = { | ||
placeholder: "Type Heading Here" | ||||
}; | ||||
|
r6018 | |||
HeadingCell.prototype = new TextCell(); | ||||
|
r8711 | /** @method fromJSON */ | ||
|
r6156 | HeadingCell.prototype.fromJSON = function (data) { | ||
if (data.level != undefined){ | ||||
this.level = data.level; | ||||
} | ||||
|
r10165 | TextCell.prototype.fromJSON.apply(this, arguments); | ||
|
r6156 | }; | ||
|
r8711 | /** @method toJSON */ | ||
|
r6156 | HeadingCell.prototype.toJSON = function () { | ||
|
r10165 | var data = TextCell.prototype.toJSON.apply(this); | ||
|
r6156 | data.level = this.get_level(); | ||
return data; | ||||
}; | ||||
|
r12510 | /** | ||
* can the cell be split into two cells | ||||
* @method is_splittable | ||||
**/ | ||||
HeadingCell.prototype.is_splittable = function () { | ||||
return false; | ||||
}; | ||||
/** | ||||
* can the cell be merged with other cells | ||||
* @method is_mergeable | ||||
**/ | ||||
HeadingCell.prototype.is_mergeable = function () { | ||||
return false; | ||||
}; | ||||
|
r6156 | |||
|
r8711 | /** | ||
* Change heading level of cell, and re-render | ||||
* @method set_level | ||||
*/ | ||||
|
r6019 | HeadingCell.prototype.set_level = function (level) { | ||
this.level = level; | ||||
if (this.rendered) { | ||||
this.rendered = false; | ||||
this.render(); | ||||
}; | ||||
}; | ||||
|
r8709 | /** The depth of header cell, based on html (h1 to h6) | ||
|
r8711 | * @method get_level | ||
|
r8709 | * @return {integer} level - for 1 to 6 | ||
*/ | ||||
|
r6019 | HeadingCell.prototype.get_level = function () { | ||
return this.level; | ||||
}; | ||||
|
r11288 | HeadingCell.prototype.set_rendered = function (html) { | ||
this.element.find("div.text_cell_render").html(html); | ||||
|
r6019 | }; | ||
|
r6018 | |||
HeadingCell.prototype.get_rendered = function () { | ||||
var r = this.element.find("div.text_cell_render"); | ||||
return r.children().first().html(); | ||||
|
r6019 | }; | ||
|
r6018 | |||
HeadingCell.prototype.render = function () { | ||||
if (this.rendered === false) { | ||||
var text = this.get_text(); | ||||
|
r11843 | var math = null; | ||
|
r11290 | // Markdown headings must be a single line | ||
text = text.replace(/\n/g, ' '); | ||||
|
r6018 | if (text === "") { text = this.placeholder; } | ||
|
r11288 | text = Array(this.level + 1).join("#") + " " + text; | ||
|
r11526 | var text_and_math = IPython.mathjaxutils.remove_math(text); | ||
|
r11843 | text = text_and_math[0]; | ||
math = text_and_math[1]; | ||||
|
r11288 | var html = marked.parser(marked.lexer(text)); | ||
|
r11526 | var h = $(IPython.mathjaxutils.replace_math(html, math)); | ||
|
r11291 | // add id and linkback anchor | ||
var hash = h.text().replace(/ /g, '-'); | ||||
h.attr('id', hash); | ||||
h.append( | ||||
|
r11288 | $('<a/>') | ||
.addClass('anchor-link') | ||||
.attr('href', '#' + hash) | ||||
.text('ΒΆ') | ||||
); | ||||
this.set_rendered(h); | ||||
|
r6018 | this.typeset(); | ||
this.element.find('div.text_cell_input').hide(); | ||||
this.element.find("div.text_cell_render").show(); | ||||
this.rendered = true; | ||||
|
r6019 | }; | ||
|
r6018 | }; | ||
|
r4508 | IPython.TextCell = TextCell; | ||
IPython.MarkdownCell = MarkdownCell; | ||||
|
r6248 | IPython.RawCell = RawCell; | ||
|
r6019 | IPython.HeadingCell = HeadingCell; | ||
|
r4508 | |||
|
r4349 | |||
|
r4352 | return IPython; | ||
|
r4349 | |||
|
r4352 | }(IPython)); | ||
|
r4349 | |||