dialog.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. define(['jquery',
  4. 'codemirror/lib/codemirror',
  5. 'bootstrap',
  6. 'base/js/i18n'],
  7. function($, CodeMirror, bs, i18n) {
  8. "use strict";
  9. /**
  10. * A wrapper around bootstrap modal for easier use
  11. * Pass it an option dictionary with the following properties:
  12. *
  13. * - body : <string> or <DOM node>, main content of the dialog
  14. * if pass a <string> it will be wrapped in a p tag and
  15. * html element escaped, unless you specify sanitize=false
  16. * option.
  17. * - title : Dialog title, default to empty string.
  18. * - buttons : dict of btn_options who keys are button label.
  19. * see btn_options below for description
  20. * - open : callback to trigger on dialog open.
  21. * - destroy:
  22. * - notebook : notebook instance
  23. * - keyboard_manager: keyboard manager instance.
  24. *
  25. * Unlike bootstrap modals, the backdrop options is set by default
  26. * to 'static'.
  27. *
  28. * The rest of the options are passed as is to bootstrap modals.
  29. *
  30. * btn_options: dict with the following property:
  31. *
  32. * - click : callback to trigger on click
  33. * - class : css classes to add to button.
  34. *
  35. *
  36. *
  37. **/
  38. var modal = function (options) {
  39. var modal = $("<div/>")
  40. .addClass("modal")
  41. .addClass("fade")
  42. .attr("role", "dialog");
  43. var dialog = $("<div/>")
  44. .addClass("modal-dialog")
  45. .appendTo(modal);
  46. var dialog_content = $("<div/>")
  47. .addClass("modal-content")
  48. .appendTo(dialog);
  49. if(typeof(options.body) === 'string' && options.sanitize !== false){
  50. options.body = $("<p/>").text(options.body);
  51. }
  52. dialog_content.append(
  53. $("<div/>")
  54. .addClass("modal-header")
  55. .mousedown(function() {
  56. $(".modal").draggable({handle: '.modal-header'});
  57. })
  58. .append($("<button>")
  59. .attr("type", "button")
  60. .addClass("close")
  61. .attr("data-dismiss", "modal")
  62. .attr("aria-hidden", "true")
  63. .html("&times;")
  64. ).append(
  65. $("<h4/>")
  66. .addClass('modal-title')
  67. .text(options.title || "")
  68. )
  69. ).append(
  70. $("<div/>")
  71. .addClass("modal-body")
  72. .append(
  73. options.body || $("<p/>")
  74. )
  75. );
  76. var footer = $("<div/>").addClass("modal-footer");
  77. var default_button;
  78. for (var label in options.buttons) {
  79. var btn_opts = options.buttons[label];
  80. var button = $("<button/>")
  81. .addClass("btn btn-default btn-sm")
  82. .attr("data-dismiss", "modal")
  83. .text(i18n.msg.translate(label).fetch());
  84. if (btn_opts.id) {
  85. button.attr('id', btn_opts.id);
  86. }
  87. if (btn_opts.click) {
  88. button.click($.proxy(btn_opts.click, dialog_content));
  89. }
  90. if (btn_opts.class) {
  91. button.addClass(btn_opts.class);
  92. }
  93. footer.append(button);
  94. if (options.default_button && label === options.default_button) {
  95. default_button = button;
  96. }
  97. }
  98. if (!options.default_button) {
  99. default_button = footer.find("button").last();
  100. }
  101. dialog_content.append(footer);
  102. // hook up on-open event
  103. modal.on("shown.bs.modal", function () {
  104. setTimeout(function () {
  105. default_button.focus();
  106. if (options.open) {
  107. $.proxy(options.open, modal)();
  108. }
  109. }, 0);
  110. });
  111. // destroy modal on hide, unless explicitly asked not to
  112. if (options.destroy === undefined || options.destroy) {
  113. modal.on("hidden.bs.modal", function () {
  114. modal.remove();
  115. });
  116. }
  117. modal.on("hidden.bs.modal", function () {
  118. if (options.notebook) {
  119. var cell = options.notebook.get_selected_cell();
  120. if (cell) cell.select();
  121. }
  122. if (options.keyboard_manager) {
  123. options.keyboard_manager.enable();
  124. options.keyboard_manager.command_mode();
  125. }
  126. });
  127. if (options.keyboard_manager) {
  128. options.keyboard_manager.disable();
  129. }
  130. if(options.backdrop === undefined){
  131. options.backdrop = 'static';
  132. }
  133. return modal.modal(options);
  134. };
  135. var kernel_modal = function (options) {
  136. /**
  137. * only one kernel dialog should be open at a time -- but
  138. * other modal dialogs can still be open
  139. */
  140. $('.kernel-modal').modal('hide');
  141. var dialog = modal(options);
  142. dialog.addClass('kernel-modal');
  143. return dialog;
  144. };
  145. var edit_metadata = function (options) {
  146. options.name = options.name || "Cell";
  147. var error_div = $('<div/>').css('color', 'red');
  148. var message_cell =
  149. i18n.msg._("Manually edit the JSON below to manipulate the metadata for this cell.");
  150. var message_notebook =
  151. i18n.msg._("Manually edit the JSON below to manipulate the metadata for this notebook.");
  152. var message_end =
  153. i18n.msg._(" We recommend putting custom metadata attributes in an appropriately named substructure," +
  154. " so they don't conflict with those of others.");
  155. var message;
  156. if (options.name === 'Notebook') {
  157. message = message_notebook + message_end;
  158. } else {
  159. message = message_cell + message_end;
  160. }
  161. var textarea = $('<textarea/>')
  162. .attr('rows', '13')
  163. .attr('cols', '80')
  164. .attr('name', 'metadata')
  165. .text(JSON.stringify(options.md || {}, null, 2));
  166. var dialogform = $('<div/>').attr('title', i18n.msg._('Edit the metadata'))
  167. .append(
  168. $('<form/>').append(
  169. $('<fieldset/>').append(
  170. $('<label/>')
  171. .attr('for','metadata')
  172. .text(message)
  173. )
  174. .append(error_div)
  175. .append($('<br/>'))
  176. .append(textarea)
  177. )
  178. );
  179. var editor = CodeMirror.fromTextArea(textarea[0], {
  180. lineNumbers: true,
  181. matchBrackets: true,
  182. indentUnit: 2,
  183. autoIndent: true,
  184. mode: 'application/json',
  185. });
  186. var title_msg;
  187. if (options.name === "Notebook") {
  188. title_msg = i18n.msg._("Edit Notebook Metadata");
  189. } else {
  190. title_msg = i18n.msg._("Edit Cell Metadata");
  191. }
  192. // This statement is used simply so that message extraction
  193. // will pick up the strings.
  194. var button_labels = [ i18n.msg._("Cancel"), i18n.msg._("Edit"), i18n.msg._("OK"), i18n.msg._("Apply")];
  195. var modal_obj = modal({
  196. title: title_msg,
  197. body: dialogform,
  198. default_button: "Cancel",
  199. buttons: {
  200. Cancel: {},
  201. Edit: { class : "btn-primary",
  202. click: function() {
  203. /**
  204. * validate json and set it
  205. */
  206. var new_md;
  207. try {
  208. new_md = JSON.parse(editor.getValue());
  209. } catch(e) {
  210. console.log(e);
  211. error_div.text(i18n.msg._('WARNING: Could not save invalid JSON.'));
  212. return false;
  213. }
  214. options.callback(new_md);
  215. }
  216. }
  217. },
  218. notebook: options.notebook,
  219. keyboard_manager: options.keyboard_manager,
  220. });
  221. modal_obj.on('shown.bs.modal', function(){ editor.refresh(); });
  222. };
  223. var edit_attachments = function (options) {
  224. // This shows the Edit Attachments dialog. This dialog allows the
  225. // user to delete attachments. We show a list of attachments to
  226. // the user and he can mark some of them for deletion. The deletion
  227. // is applied when the 'Apply' button of this dialog is pressed.
  228. var message;
  229. var attachments_list;
  230. if (Object.keys(options.attachments).length == 0) {
  231. message = i18n.msg._("There are no attachments for this cell.");
  232. attachments_list = $('<div>');
  233. } else {
  234. message = i18n.msg._("Current cell attachments");
  235. attachments_list = $('<div>')
  236. .addClass('list_container')
  237. .append(
  238. $('<div>')
  239. .addClass('row list_header')
  240. .append(
  241. $('<div>')
  242. .text(i18n.msg._('Attachments'))
  243. )
  244. );
  245. // This is a set containing keys of attachments to be deleted when
  246. // the Apply button is clicked
  247. var to_delete = {};
  248. var refresh_attachments_list = function() {
  249. $(attachments_list).find('.row').remove();
  250. for (var key in options.attachments) {
  251. var mime = Object.keys(options.attachments[key])[0];
  252. var deleted = key in to_delete;
  253. // This ensures the current value of key is captured since
  254. // javascript only has function scope
  255. var btn;
  256. // Trash/restore button
  257. (function(){
  258. var _key = key;
  259. btn = $('<button>')
  260. .addClass('btn btn-default btn-xs')
  261. .css('display', 'inline-block');
  262. if (deleted) {
  263. btn.attr('title', i18n.msg._('Restore'))
  264. .append(
  265. $('<i>')
  266. .addClass('fa fa-plus')
  267. );
  268. btn.click(function() {
  269. delete to_delete[_key];
  270. refresh_attachments_list();
  271. });
  272. } else {
  273. btn.attr('title', i18n.msg._('Delete'))
  274. .addClass('btn-danger')
  275. .append(
  276. $('<i>')
  277. .addClass('fa fa-trash')
  278. );
  279. btn.click(function() {
  280. to_delete[_key] = true;
  281. refresh_attachments_list();
  282. });
  283. }
  284. return btn;
  285. })();
  286. var row = $('<div>')
  287. .addClass('col-md-12 att_row')
  288. .append(
  289. $('<div>')
  290. .addClass('row')
  291. .append(
  292. $('<div>')
  293. .addClass('att-name col-xs-4')
  294. .text(key)
  295. )
  296. .append(
  297. $('<div>')
  298. .addClass('col-xs-4 text-muted')
  299. .text(mime)
  300. )
  301. .append(
  302. $('<div>')
  303. .addClass('item-buttons pull-right')
  304. .append(btn)
  305. )
  306. );
  307. if (deleted) {
  308. row.find('.att-name')
  309. .css('text-decoration', 'line-through');
  310. }
  311. attachments_list.append($('<div>')
  312. .addClass('list_item row')
  313. .append(row)
  314. );
  315. }
  316. };
  317. refresh_attachments_list();
  318. }
  319. var dialogform = $('<div/>')
  320. .attr('title', i18n.msg._('Edit attachments'))
  321. .append(message)
  322. .append('<br />')
  323. .append(attachments_list);
  324. var title_msg;
  325. if ( options.name === "Notebook" ) {
  326. title_msg = i18n.msg._("Edit Notebook Attachments");
  327. } else {
  328. title_msg = i18n.msg._("Edit Cell Attachments");
  329. }
  330. var modal_obj = modal({
  331. title: title_msg,
  332. body: dialogform,
  333. buttons: {
  334. Apply: { class : "btn-primary",
  335. click: function() {
  336. for (var key in to_delete) {
  337. delete options.attachments[key];
  338. }
  339. options.callback(options.attachments);
  340. }
  341. },
  342. Cancel: {}
  343. },
  344. notebook: options.notebook,
  345. keyboard_manager: options.keyboard_manager,
  346. });
  347. };
  348. var insert_image = function (options) {
  349. var message =
  350. i18n.msg._("Select a file to insert.");
  351. var file_input = $('<input/>')
  352. .attr('type', 'file')
  353. .attr('accept', 'image/*')
  354. .attr('name', 'file')
  355. .on('change', function(file) {
  356. var $btn = $(modal_obj).find('#btn_ok');
  357. if (this.files.length > 0) {
  358. $btn.removeClass('disabled');
  359. } else {
  360. $btn.addClass('disabled');
  361. }
  362. });
  363. var dialogform = $('<div/>').attr('title', i18n.msg._('Edit attachments'))
  364. .append(
  365. $('<form id="insert-image-form" />').append(
  366. $('<fieldset/>').append(
  367. $('<label/>')
  368. .attr('for','file')
  369. .text(message)
  370. )
  371. .append($('<br/>'))
  372. .append(file_input)
  373. )
  374. );
  375. var modal_obj = modal({
  376. title: i18n.msg._("Select a file"),
  377. body: dialogform,
  378. buttons: {
  379. OK: {
  380. id : 'btn_ok',
  381. class : "btn-primary disabled",
  382. click: function() {
  383. options.callback(file_input[0].files[0]);
  384. }
  385. },
  386. Cancel: {}
  387. },
  388. notebook: options.notebook,
  389. keyboard_manager: options.keyboard_manager,
  390. });
  391. };
  392. var dialog = {
  393. modal : modal,
  394. kernel_modal : kernel_modal,
  395. edit_metadata : edit_metadata,
  396. edit_attachments : edit_attachments,
  397. insert_image : insert_image
  398. };
  399. return dialog;
  400. });