Merge pull request #621 from julienr/inline_images

Implement markdown cell attachments. Allow drag’n’drop of images into…
This commit is contained in:
Min RK 2016-03-08 11:33:56 +01:00
commit 233f04428d
12 changed files with 762 additions and 6 deletions

View File

@ -80,6 +80,9 @@ define(function(require) {
.addClass("btn btn-default btn-sm")
.attr("data-dismiss", "modal")
.text(label);
if (btn_opts.id) {
button.attr('id', btn_opts.id);
}
if (btn_opts.click) {
button.click($.proxy(btn_opts.click, dialog_content));
}
@ -208,10 +211,184 @@ define(function(require) {
modal_obj.on('shown.bs.modal', function(){ editor.refresh(); });
};
var edit_attachments = function (options) {
// This shows the Edit Attachments dialog. This dialog allows the
// user to delete attachments. We show a list of attachments to
// the user and he can mark some of them for deletion. The deletion
// is applied when the 'Apply' button of this dialog is pressed.
var message;
var attachments_list;
if (Object.keys(options.attachments).length == 0) {
message = "There are no attachments for this cell.";
attachments_list = $('<div>');
} else {
message = "Current cell attachments";
attachments_list = $('<div>')
.addClass('list_container')
.append(
$('<div>')
.addClass('row list_header')
.append(
$('<div>')
.text('Attachments')
)
);
// This is a set containing keys of attachments to be deleted when
// the Apply button is clicked
var to_delete = {};
var refresh_attachments_list = function() {
$(attachments_list).find('.row').remove();
for (var key in options.attachments) {
var mime = Object.keys(options.attachments[key])[0];
var deleted = key in to_delete;
// This ensures the current value of key is captured since
// javascript only has function scope
var btn;
// Trash/restore button
(function(){
var _key = key;
btn = $('<button>')
.addClass('btn btn-default btn-xs')
.css('display', 'inline-block');
if (deleted) {
btn.attr('title', 'Restore')
.append(
$('<i>')
.addClass('fa fa-plus')
);
btn.click(function() {
delete to_delete[_key];
refresh_attachments_list();
});
} else {
btn.attr('title', 'Delete')
.addClass('btn-danger')
.append(
$('<i>')
.addClass('fa fa-trash')
);
btn.click(function() {
to_delete[_key] = true;
refresh_attachments_list();
});
}
return btn;
})();
var row = $('<div>')
.addClass('col-md-12 att_row')
.append(
$('<div>')
.addClass('row')
.append(
$('<div>')
.addClass('att-name col-xs-4')
.text(key)
)
.append(
$('<div>')
.addClass('col-xs-4 text-muted')
.text(mime)
)
.append(
$('<div>')
.addClass('item-buttons pull-right')
.append(btn)
)
);
if (deleted) {
row.find('.att-name')
.css('text-decoration', 'line-through');
}
attachments_list.append($('<div>')
.addClass('list_item row')
.append(row)
);
}
};
refresh_attachments_list();
}
var dialogform = $('<div/>')
.attr('title', 'Edit attachments')
.append(message)
.append('<br />')
.append(attachments_list)
var modal_obj = modal({
title: "Edit " + options.name + " Attachments",
body: dialogform,
buttons: {
Apply: { class : "btn-primary",
click: function() {
for (var key in to_delete) {
delete options.attachments[key];
}
options.callback(options.attachments);
}
},
Cancel: {}
},
notebook: options.notebook,
keyboard_manager: options.keyboard_manager,
});
};
var insert_image = function (options) {
var message =
"Select a file to insert.";
var file_input = $('<input/>')
.attr('type', 'file')
.attr('accept', 'image/*')
.attr('name', 'file')
.on('change', function(file) {
var $btn = $(modal_obj).find('#btn_ok');
if (this.files.length > 0) {
$btn.removeClass('disabled');
} else {
$btn.addClass('disabled');
}
});
var dialogform = $('<div/>').attr('title', 'Edit attachments')
.append(
$('<form id="insert-image-form" />').append(
$('<fieldset/>').append(
$('<label/>')
.attr('for','file')
.text(message)
)
.append($('<br/>'))
.append(file_input)
)
);
var modal_obj = modal({
title: "Pick a file",
body: dialogform,
buttons: {
OK: {
id : 'btn_ok',
class : "btn-primary disabled",
click: function() {
options.callback(file_input[0].files[0]);
}
},
Cancel: {}
},
notebook: options.notebook,
keyboard_manager: options.keyboard_manager,
});
};
var dialog = {
modal : modal,
kernel_modal : kernel_modal,
edit_metadata : edit_metadata,
edit_attachments : edit_attachments,
insert_image : insert_image
};
return dialog;

View File

@ -30,6 +30,12 @@ define([
}
}
}
// Caja doesn't allow data uri for img::src, see
// https://github.com/google/caja/issues/1558
// This is not a security issue for browser post ie6 though, so we
// disable the check
// https://www.owasp.org/index.php/Script_in_IMG_tags
ATTRIBS['img::src'] = 0;
return caja.sanitizeAttribs(tagName, attribs, opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger);
};

View File

@ -789,6 +789,27 @@ define([
});
};
var parse_b64_data_uri = function(uri) {
/**
* Parses a base64 encoded data-uri to extract mimetype and the
* base64 string.
*
* For example, given 'data:image/png;base64,iVBORw', it will return
* ["image/png", "iVBORw"]
*
* Parameters
*/
// For performance reasons, the non-greedy ? qualifiers are crucial so
// that the matcher stops early on big blobs. Without them, it will try
// to match the whole blob which can take ages
var regex = /^data:(.+?\/.+?);base64,/;
var matches = uri.match(regex);
var mime = matches[1];
// matches[0] contains the whole data-uri prefix
var b64_data = uri.slice(matches[0].length);
return [mime, b64_data];
};
var time = {};
time.milliseconds = {};
time.milliseconds.s = 1000;
@ -877,6 +898,7 @@ define([
resolve_promises_dict: resolve_promises_dict,
reject: reject,
typeset: typeset,
parse_b64_data_uri: parse_b64_data_uri,
time: time,
format_datetime: format_datetime,
datetime_sort_helper: datetime_sort_helper,

View File

@ -153,6 +153,34 @@ define(function(require){
env.notebook.command_mode();
}
},
'insert-image': {
help : 'insert image',
help_index : 'dz',
handler : function (env) {
env.notebook.insert_image();
}
},
'cut-cell-attachments': {
help : 'cut cell attachments',
help_index : 'dza',
handler: function (env) {
env.notebook.cut_cell_attachments();
}
},
'copy-cell-attachments': {
help : 'copy cell attachments',
help_index: 'dzb',
handler: function (env) {
env.notebook.copy_cell_attachments();
}
},
'paste-cell-attachments': {
help : 'paste cell attachments',
help_index: 'dzc',
handler: function (env) {
env.notebook.paste_cell_attachments();
}
},
'split-cell-at-cursor': {
help : 'split cell',
help_index : 'ea',

View File

@ -70,6 +70,7 @@ define([
}
});
// backward compat.
Object.defineProperty(this, 'cm_config', {
get: function() {
@ -99,6 +100,12 @@ define([
this.cell_type = this.cell_type || null;
this.code_mirror = null;
// The nbformat only specifies attachments for textcell, but to avoid
// data loss when switching between cell types in the UI, all cells
// have an attachments property here. It is only saved to disk
// for textcell though (in toJSON)
this.attachments = {};
this.create_element();
if (this.element !== null) {
this.element.data("cell", this);
@ -272,6 +279,9 @@ define([
this.element.addClass('selected');
this.element.removeClass('unselected');
this.selected = true;
// disable 'insert image' menu item (specific cell types will enable
// it in their override select())
this.notebook.set_insert_image_enabled(false);
return true;
} else {
return false;
@ -340,6 +350,16 @@ define([
}
};
/**
* Garbage collects unused attachments in this cell
* @method remove_unused_attachments
*/
Cell.prototype.remove_unused_attachments = function () {
// Cell subclasses which support attachments should override this
// and keep them when needed
this.attachments = {};
};
/**
* Delegates keyboard shortcut handling to either Jupyter keyboard
* manager when in command mode, or CodeMirror when in edit mode
@ -736,6 +756,11 @@ define([
this.element = cell;
};
UnrecognizedCell.prototype.remove_unused_attachments = function () {
// Do nothing to avoid removing attachments from a possible future
// attachment-supporting cell type
};
UnrecognizedCell.prototype.bind_events = function () {
Cell.prototype.bind_events.apply(this, arguments);
var cell = this;

View File

@ -0,0 +1,50 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
define([
'notebook/js/celltoolbar',
'base/js/dialog',
], function(celltoolbar, dialog) {
"use strict";
var CellToolbar = celltoolbar.CellToolbar;
var edit_attachments_dialog = function(cell) {
dialog.edit_attachments({
attachments: cell.attachments,
callback: function(attachments) {
cell.attachments = attachments;
// Force cell refresh
cell.unrender();
cell.render();
},
name: 'cell',
notebook: cell.notebook,
keyboard_manager: cell.keyboard_manager
});
};
var add_dialog_button = function(div, cell) {
var button_container = $(div);
var button = $('<button />')
.addClass('btn btn-default btn-xs')
.text('Edit Attachments')
.click( function() {
edit_attachments_dialog(cell);
return false;
});
button_container.append(button);
};
var register = function(notebook) {
CellToolbar.register_callback('attachments.edit', add_dialog_button);
var attachments_preset = [];
attachments_preset.push('attachments.edit');
CellToolbar.register_preset('Attachments', attachments_preset, notebook);
console.log('Attachments editing toolbar loaded.');
};
return {'register' : register}
});

View File

@ -228,6 +228,10 @@ define([
'#toggle_all_output': 'toggle-all-cells-output-collapsed',
'#toggle_all_output_scroll': 'toggle-all-cells-output-scrolled',
'#clear_all_output': 'clear-all-cells-output',
'#cut_cell_attachments': 'cut-cell-attachments',
'#copy_cell_attachments': 'copy-cell-attachments',
'#paste_cell_attachments': 'paste-cell-attachments',
'#insert_image': 'insert-image',
};
for(var idx in id_actions_dict){

View File

@ -26,6 +26,7 @@ define(function (require) {
var default_celltoolbar = require('notebook/js/celltoolbarpresets/default');
var rawcell_celltoolbar = require('notebook/js/celltoolbarpresets/rawcell');
var slideshow_celltoolbar = require('notebook/js/celltoolbarpresets/slideshow');
var attachments_celltoolbar = require('notebook/js/celltoolbarpresets/attachments');
var scrollmanager = require('notebook/js/scrollmanager');
var commandpalette = require('notebook/js/commandpalette');
@ -125,8 +126,10 @@ define(function (require) {
this.kernel = null;
this.kernel_busy = false;
this.clipboard = null;
this.clipboard_attachments = null;
this.undelete_backup_stack = [];
this.paste_enabled = false;
this.paste_attachments_enabled = false;
this.writable = false;
// It is important to start out in command mode to match the intial mode
// of the KeyboardManager.
@ -142,7 +145,7 @@ define(function (require) {
this.minimum_autosave_interval = 120000;
this.notebook_name_blacklist_re = /[\/\\:]/;
this.nbformat = 4; // Increment this when changing the nbformat
this.nbformat_minor = this.current_nbformat_minor = 0; // Increment this when changing the nbformat
this.nbformat_minor = this.current_nbformat_minor = 1; // Increment this when changing the nbformat
this.codemirror_mode = 'text';
this.create_elements();
this.bind_events();
@ -155,6 +158,7 @@ define(function (require) {
default_celltoolbar.register(this);
rawcell_celltoolbar.register(this);
slideshow_celltoolbar.register(this);
attachments_celltoolbar.register(this);
// prevent assign to miss-typed properties.
Object.seal(this);
@ -1296,6 +1300,9 @@ define(function (require) {
}
//metadata
target_cell.metadata = source_cell.metadata;
// attachments (we transfer them so they aren't lost if the
// cell is turned back into markdown)
target_cell.attachments = source_cell.attachments;
target_cell.set_text(text);
// make this value the starting point, so that we can only undo
@ -1344,6 +1351,8 @@ define(function (require) {
}
// metadata
target_cell.metadata = source_cell.metadata;
target_cell.attachments = source_cell.attachments;
// We must show the editor before setting its contents
target_cell.unrender();
target_cell.set_text(text);
@ -1396,6 +1405,10 @@ define(function (require) {
}
//metadata
target_cell.metadata = source_cell.metadata;
// attachments (we transfer them so they aren't lost if the
// cell is turned back into markdown)
target_cell.attachments = source_cell.attachments;
// We must show the editor before setting its contents
target_cell.unrender();
target_cell.set_text(text);
@ -1662,6 +1675,112 @@ define(function (require) {
this.merge_cells([index, index+1], false);
};
// Attachments handling
/**
* Shows a dialog letting the user pick an image from her computer and
* insert it into the edited markdown cell
*/
Notebook.prototype.insert_image = function () {
var that = this;
var cell = this.get_selected_cell();
// The following should not happen as the menu item is greyed out
// when those conditions are not fullfilled (see MarkdownCell
// unselect/select/unrender handlers)
if (cell.cell_type != 'markdown') {
console.log('Error: insert_image called on non-markdown cell');
return;
}
if (cell.rendered) {
console.log('Error: insert_image called on rendered cell');
return;
}
dialog.insert_image({
callback: function(file) {
cell.edit_mode();
cell.insert_inline_image_from_blob(file);
},
notebook: this,
keyboard_manager: this.keyboard_manager
});
};
/**
* Cut the attachments of a cell
*/
Notebook.prototype.cut_cell_attachments = function() {
var cell = this.get_selected_cell();
if (cell.attachments !== undefined) {
this.clipboard_attachments = cell.attachments;
this.enable_attachments_paste();
delete cell.attachments;
cell.unrender();
cell.render();
}
};
/**
* Copy the attachments of a cell
*/
Notebook.prototype.copy_cell_attachments = function() {
var cell = this.get_selected_cell();
if (cell.attachments !== undefined) {
// Do a deep copy of attachments to avoid subsequent modification
// to the cell to modify the clipboard
this.clipboard_attachments = $.extend(true, {}, cell.attachments);
this.enable_attachments_paste();
}
};
/**
* Paste the attachments in the clipboard into the currently selected
* cell
*/
Notebook.prototype.paste_cell_attachments = function() {
if (this.clipboard_attachments !== null &&
this.paste_attachments_enabled) {
var cell = this.get_selected_cell();
if (cell.attachments === undefined) {
cell.attachments = {};
}
// Do a deep copy so we can paste multiple times
$.extend(true, cell.attachments, this.clipboard_attachments);
cell.unrender();
cell.render();
}
};
/**
* Disable the "Paste Cell Attachments" menu item
*/
Notebook.prototype.disable_attachments_paste = function () {
if (this.paste_attachments_enabled) {
$('#paste_cell_attachments').addClass('disabled');
this.paste_attachments_enabled = false;
}
};
/**
* Enable the "Paste Cell Attachments" menu item
*/
Notebook.prototype.enable_attachments_paste = function () {
var that = this;
if (!this.paste_attachments_enabled) {
$('#paste_cell_attachments').removeClass('disabled');
this.paste_attachments_enabled = true;
}
};
/**
* Enable/disable the "Insert image" menu item
*/
Notebook.prototype.set_insert_image_enabled = function(enabled) {
if (enabled) {
$('#insert_image').removeClass('disabled');
} else {
$('#insert_image').addClass('disabled');
}
};
// Cell collapsing and output clearing
@ -2282,6 +2401,17 @@ define(function (require) {
}
};
/**
* Garbage collects unused attachments in all the cells
*/
Notebook.prototype.remove_unused_attachments = function() {
var cells = this.get_cells();
for (var i = 0; i < cells.length; i++) {
var cell = cells[i];
cell.remove_unused_attachments();
}
}
/**
* Load a notebook from JSON (.ipynb).
*
@ -2395,11 +2525,17 @@ define(function (require) {
/**
* Save this notebook on the server. This becomes a notebook instance's
* .save_notebook method *after* the entire notebook has been loaded.
*
* manual_save will be true if the save was manually trigered by the user
*/
Notebook.prototype.save_notebook = function (check_last_modified) {
Notebook.prototype.save_notebook = function (check_last_modified,
manual_save) {
if (check_last_modified === undefined) {
check_last_modified = true;
}
if (manual_save === undefined) {
manual_save = false;
}
var error;
if (!this._fully_loaded) {
@ -2416,6 +2552,13 @@ define(function (require) {
// the notebook as needed.
this.events.trigger('before_save.Notebook');
// Garbage collect unused attachments. Only do this for manual save
// to avoid removing unused attachments while the user is editing if
// an autosave gets triggered in the midle of an edit
if (manual_save) {
this.remove_unused_attachments();
}
// Create a JSON model to be sent to the server.
var model = {
type : "notebook",
@ -2599,7 +2742,7 @@ define(function (require) {
var parent = utils.url_path_split(this.notebook_path)[0];
var p;
if (this.dirty) {
p = this.save_notebook();
p = this.save_notebook(true, true);
} else {
p = Promise.resolve();
}
@ -2869,7 +3012,7 @@ define(function (require) {
*/
Notebook.prototype.save_checkpoint = function () {
this._checkpoint_after_save = true;
this.save_notebook();
this.save_notebook(true, true);
};
/**

View File

@ -51,6 +51,7 @@ define([
this.notebook = options.notebook;
this.events = options.events;
this.config = options.config;
this.notebook = options.notebook;
// we cannot put this as a class key as it has handle to "this".
var config = utils.mergeopt(TextCell, this.config);
@ -114,6 +115,52 @@ define([
// Cell level actions
TextCell.prototype.add_attachment = function (key, mime_type, b64_data) {
/**
* Add a new attachment to this cell
*/
this.attachments[key] = {};
this.attachments[key][mime_type] = [b64_data];
};
TextCell.prototype.remove_unused_attachments = function () {
// The general idea is to render the text, find attachment like when
// we substitute them in render() and mark used attachments by adding
// a temporary .used property to them.
if (Object.keys(this.attachments).length > 0) {
var that = this;
// To find unused attachments, rendering to HTML is easier than
// searching in the markdown source for the multiple ways you can
// reference an image in markdown (using []() or a HTML <img> tag)
var text = this.get_text();
marked(text, function (err, html) {
html = security.sanitize_html(html);
html = $($.parseHTML(html));
html.find('img[src^="attachment:"]').each(function (i, h) {
h = $(h);
var key = h.attr('src').replace(/^attachment:/, '');
if (key in that.attachments) {
that.attachments[key].used = true;
}
// This is to avoid having the browser do a GET request
// on the invalid attachment: URL
h.attr('src', '');
});
});
for (var key in this.attachments) {
if (this.attachments[key].used === undefined) {
console.log('Dropping unused attachment ' + key);
delete this.attachments[key];
} else {
// Remove temporary property
delete this.attachments[key].used;
}
}
}
}
TextCell.prototype.select = function () {
var cont = Cell.prototype.select.apply(this, arguments);
if (cont) {
@ -183,7 +230,12 @@ define([
*/
TextCell.prototype.fromJSON = function (data) {
Cell.prototype.fromJSON.apply(this, arguments);
console.log('data cell_type : ' + data.cell_type + ' this.cell_type : ' + this.cell_type);
if (data.cell_type === this.cell_type) {
if (data.attachments !== undefined) {
this.attachments = data.attachments;
}
if (data.source !== undefined) {
this.set_text(data.source);
// make this value the starting point, so that we can only undo
@ -207,6 +259,11 @@ define([
if (data.source == this.placeholder) {
data.source = "";
}
// Deepcopy the attachments so copied cells don't share the same
// objects
if (Object.keys(this.attachments).length > 0) {
data.attachments = JSON.parse(JSON.stringify(this.attachments));
}
return data;
};
@ -256,6 +313,57 @@ define([
}
};
MarkdownCell.prototype.select = function () {
var cont = TextCell.prototype.select.apply(this);
if (cont) {
this.notebook.set_insert_image_enabled(!this.rendered);
}
};
MarkdownCell.prototype.unrender = function () {
var cont = TextCell.prototype.unrender.apply(this);
this.notebook.set_insert_image_enabled(true);
};
MarkdownCell.prototype.insert_inline_image_from_blob = function(blob) {
/**
* Insert markup for an inline image at the current cursor position.
* This works as follow :
* - We insert the base64-encoded blob data into the cell attachments
* dictionary, keyed by the filename.
* - We insert an img tag with a 'attachment:key' src that refers to
* the attachments entry.
*
* Parameters:
* file: Blob
* The JS Blob object (e.g. from the DataTransferItem)
*/
var that = this;
var pos = this.code_mirror.getCursor();
var reader = new FileReader;
// We can get either a named file (drag'n'drop) or a blob (copy/paste)
// We generate names for blobs
var key;
if (blob.name !== undefined) {
key = blob.name;
} else {
key = '_auto_' + Object.keys(that.attachments).length;
}
reader.onloadend = function() {
var d = utils.parse_b64_data_uri(reader.result);
if (blob.type != d[0]) {
// TODO(julienr): Not sure what we should do in this case
console.log('File type (' + blob.type + ') != data-uri ' +
'type (' + d[0] + ')');
}
that.add_attachment(key, blob.type, d[1]);
var img_md = '![attachment:' + key + '](attachment:' + key + ')';
that.code_mirror.replaceRange(img_md, pos);
}
reader.readAsDataURL(blob);
};
/**
* @method render
*/
@ -290,6 +398,20 @@ define([
});
// links in markdown cells should open in new tabs
html.find("a[href]").not('[href^="#"]').attr("target", "_blank");
// replace attachment:<key> by the corresponding entry
// in the cell's attachments
html.find('img[src^="attachment:"]').each(function (i, h) {
h = $(h);
var key = h.attr('src').replace(/^attachment:/, '');
if (key in that.attachments) {
var att = that.attachments[key];
var mime = Object.keys(att)[0];
h.attr('src', 'data:' + mime + ';base64,' + att[mime][0]);
} else {
h.attr('src', '');
}
});
that.set_rendered(html);
that.typeset();
that.events.trigger("rendered.MarkdownCell", {cell: that});
@ -309,8 +431,58 @@ define([
that.focus_editor();
}
});
var attachment_regex = /^image\/.*$/;
// Event handlers to allow users to insert image using either
// drag'n'drop or copy/paste
var div = that.code_mirror.getWrapperElement();
$(div).on('paste', function(evt) {
var data = evt.originalEvent.clipboardData;
var items = data.items;
if (data.items !== undefined) {
for (var i = 0; i < items.length; ++i) {
var item = items[i];
if (item.kind == 'file' && attachment_regex.test(item.type)) {
// TODO(julienr): This does not stop code_mirror from pasting
// the filename.
evt.stopPropagation();
evt.preventDefault();
that.insert_inline_image_from_blob(item.getAsFile());
}
}
}
});
// Allow drop event if the dragged file can be used as an attachment
this.code_mirror.on("dragstart", function(cm, evt) {
var files = evt.dataTransfer.files;
for (var i = 0; i < files.length; ++i) {
var file = files[i];
if (attachment_regex.test(file.type)) {
return false;
}
}
return true;
});
this.code_mirror.on("drop", function(cm, evt) {
var files = evt.dataTransfer.files;
for (var i = 0; i < files.length; ++i) {
var file = files[i];
if (attachment_regex.test(file.type)) {
// Prevent the default code_mirror 'drop' event handler
// (which inserts the file content) if this is a
// recognized media file
evt.stopPropagation();
evt.preventDefault();
that.insert_inline_image_from_blob(file);
}
}
});
};
var RawCell = function (options) {
/**
* Constructor

View File

@ -143,6 +143,12 @@ data-notebook-path="{{notebook_path | urlencode}}"
<li id="edit_nb_metadata"><a href="#">Edit Notebook Metadata</a></li>
<li class="divider"></li>
<li id="find_and_replace"><a href="#"> Find and Replace </a></li>
<li class="divider"></li>
<li id="cut_cell_attachments"><a href="#">Cut Cell Attachments</a></li>
<li id="copy_cell_attachments"><a href="#">Copy Cell Attachments</a></li>
<li id="paste_cell_attachments" class="disabled"><a href="#">Paste Cell Attachments</a></li>
<li class="divider"></li>
<li id="insert_image" class="disabled"><a href="#"> Insert Image </a></li>
</ul>
</li>
<li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">View</a>

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 B

View File

@ -0,0 +1,123 @@
//
// Test cell attachments
//
var fs = require('fs');
casper.notebook_test(function () {
// -- Test the Edit->Insert Image menu to insert new attachments
"use strict";
casper.test.info("Testing attachments insertion through the menuitem");
this.viewport(1024, 768);
// Click on menuitem
var selector = '#insert_image > a';
this.waitForSelector(selector);
this.thenEvaluate(function(sel) {
IPython.notebook.to_markdown();
var cell = IPython.notebook.get_selected_cell();
cell.set_text("");
cell.unrender();
$(sel).click();
}, selector);
// Wait for the dialog to be shown
this.waitUntilVisible(".modal-body");
this.wait(200);
// Select the image file to insert
// For some reason, this doesn't seem to work in a reliable way in
// phantomjs. So we manually set the input's files attribute
//this.page.uploadFile('.modal-body input[name=file]', 'test.png')
this.then(function() {
var fname = 'notebook/tests/_testdata/black_square_22.png';
if (!fs.exists(fname)) {
this.test.fail(
" does not exist, are you running the tests " +
"from the root directory ? "
);
}
this.fill('form#insert-image-form', {'file': fname});
});
// Validate and render the markdown cell
this.thenClick('#btn_ok');
this.thenEvaluate(function() {
IPython.notebook.get_cell(0).render();
});
this.wait(300);
// Check that an <img> tag has been inserted and that it contains the
// image
this.then(function() {
var img = this.evaluate(function() {
var cell = IPython.notebook.get_cell(0);
var img = $("div.text_cell_render").find("img");
return {
src: img.attr("src"),
width: img.width(),
height: img.height(),
};
});
this.test.assertType(img, "object", "Image('image/png')");
this.test.assertEquals(img.src.split(',')[0],
"data:image/png;base64",
"Image data-uri prefix");
this.test.assertEquals(img.width, 2, "width == 2");
this.test.assertEquals(img.height, 2, "height == 2");
});
//this.then(function() {
//this.capture('test.png');
//});
// -- Use the Edit->Copy/Paste Cell Attachments menu items
selector = '#copy_cell_attachments > a';
this.waitForSelector(selector);
this.thenClick(selector);
// append a new cell
this.append_cell('', 'markdown');
this.thenEvaluate(function() {
IPython.notebook.select_next();
});
// and paste the attachments into it
selector = '#paste_cell_attachments > a';
this.waitForSelector(selector);
this.thenClick(selector);
// check that the new cell has attachments
this.then(function() {
var cell = this.evaluate(function() {
return IPython.notebook.get_selected_cell();
});
var orig_cell = this.evaluate(function() {
return IPython.notebook.get_cell(0);
});
var clip = this.evaluate(function() { return IPython.notebook.clipboard_attachments; });
// Check that the two cells have the same attachments
this.test.assertEquals(cell.attachments, orig_cell.attachments,
"both cells have the attachments");
});
// -- Save the notebook. This should cause garbage collection for the
// second cell (since we just pasted the attachments but there is no
// markdown referencing them)
this.thenEvaluate(function() {
IPython.notebook.save_checkpoint();
});
this.then(function() {
var cell0 = this.evaluate(function() {
return IPython.notebook.get_cell(0);
});
var cell1 = this.evaluate(function() {
return IPython.notebook.get_cell(1);
});
this.test.assert('black_square_22.png' in cell0.attachments,
'cell0 has kept its attachments');
this.test.assertEquals(Object.keys(cell1.attachments).length, 0,
'cell1 attachments have been garbage collected');
});
});