notebooklist.js 62 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. define([
  4. 'jquery',
  5. 'base/js/namespace',
  6. 'base/js/utils',
  7. 'base/js/i18n',
  8. 'base/js/dialog',
  9. 'base/js/events',
  10. 'base/js/keyboard',
  11. 'moment',
  12. 'bidi/bidi'
  13. ], function($, IPython, utils, i18n, dialog, events, keyboard, moment, bidi) {
  14. "use strict";
  15. var extension = function(path){
  16. /**
  17. * return the last pat after the dot in a filepath
  18. * or the filepath itself if no dots present.
  19. * Empty string if the filepath ends with a dot.
  20. **/
  21. var parts = path.split('.');
  22. return parts[parts.length-1];
  23. };
  24. var item_in = function(item, list) {
  25. // Normalize list and item to lowercase
  26. var normalized_list = list.map(function(_item) {
  27. return _item.toLowerCase();
  28. });
  29. return normalized_list.indexOf(item.toLowerCase()) !== -1;
  30. };
  31. var includes_extension = function(filepath, extensionslist) {
  32. return item_in(extension(filepath), extensionslist);
  33. };
  34. function name_sorter(ascending) {
  35. return (function(a, b) {
  36. if (type_order[a['type']] < type_order[b['type']]) {
  37. return -1;
  38. }
  39. if (type_order[a['type']] > type_order[b['type']]) {
  40. return 1;
  41. }
  42. if (a['name'].toLowerCase() < b['name'].toLowerCase()) {
  43. return (ascending) ? -1 : 1;
  44. }
  45. if (a['name'].toLowerCase() > b['name'].toLowerCase()) {
  46. return (ascending) ? 1 : -1;
  47. }
  48. return 0;
  49. });
  50. }
  51. function modified_sorter(ascending) {
  52. var order = ascending ? 1 : 0;
  53. return (function(a, b) {
  54. return utils.datetime_sort_helper(a.last_modified, b.last_modified,
  55. order)
  56. });
  57. }
  58. function size_sorter(ascending) {
  59. var order = ascending ? 1 : 0;
  60. // directories have file size of undefined
  61. return (function(a, b) {
  62. if (a.size === undefined) {
  63. return (ascending) ? -1 : 1;
  64. }
  65. if (b.size === undefined) {
  66. return (ascending) ? 1 : -1;
  67. }
  68. if (a.size > b.size) {
  69. return (ascending) ? -1 : 1;
  70. }
  71. if (b.size > a.size) {
  72. return (ascending) ? 1 : -1;
  73. }
  74. return 0;
  75. });
  76. }
  77. var sort_functions = {
  78. 'sort-name': name_sorter,
  79. 'last-modified': modified_sorter,
  80. 'file-size': size_sorter
  81. };
  82. var NotebookList = function (selector, options) {
  83. /**
  84. * Constructor
  85. *
  86. * Parameters:
  87. * selector: string
  88. * options: dictionary
  89. * Dictionary of keyword arguments.
  90. * session_list: SessionList instance
  91. * element_name: string
  92. * base_url: string
  93. * notebook_path: string
  94. * contents: Contents instance
  95. */
  96. var that = this;
  97. this.session_list = options.session_list;
  98. this.events = this.session_list.events;
  99. // allow code re-use by just changing element_name in kernellist.js
  100. this.element_name = options.element_name || 'notebook';
  101. this.selector = selector;
  102. if (this.selector !== undefined) {
  103. this.element = $(selector);
  104. this.style();
  105. this.bind_events();
  106. }
  107. this.notebooks_list = [];
  108. this.sessions = {};
  109. this.base_url = options.base_url || utils.get_body_data("baseUrl");
  110. this.notebook_path = options.notebook_path || utils.get_body_data("notebookPath");
  111. this.initial_notebook_path = this.notebook_path;
  112. this.contents = options.contents;
  113. if (this.session_list && this.session_list.events) {
  114. this.session_list.events.on('sessions_loaded.Dashboard',
  115. function(e, d) { that.sessions_loaded(d); });
  116. }
  117. this.selected = [];
  118. this.sort_function = name_sorter(1);
  119. // 0 => descending, 1 => ascending
  120. this.sort_id = 'sort-name';
  121. this.sort_direction = 1;
  122. this._max_upload_size_mb = 25;
  123. this.EDIT_MIMETYPES = [
  124. 'application/javascript',
  125. 'application/x-sh',
  126. 'application/vnd.groove-tool-template'
  127. ];
  128. };
  129. NotebookList.prototype.style = function () {
  130. var prefix = '#' + this.element_name;
  131. $(prefix + '_toolbar').addClass('list_toolbar');
  132. $(prefix + '_list_info').addClass('toolbar_info');
  133. $(prefix + '_buttons').addClass('toolbar_buttons');
  134. $(prefix + '_list_header').addClass('list_header');
  135. this.element.addClass("list_container");
  136. };
  137. NotebookList.prototype.bind_events = function () {
  138. var that = this;
  139. $('#refresh_' + this.element_name + '_list').click(function () {
  140. that.load_sessions();
  141. });
  142. this.element.bind('dragover', function () {
  143. return false;
  144. });
  145. this.element.bind('drop', function(event){
  146. that.handleFilesUpload(event,'drop');
  147. return false;
  148. });
  149. // Bind events for singleton controls.
  150. if (!NotebookList._bound_singletons) {
  151. NotebookList._bound_singletons = true;
  152. $('#new-file').click(function(e) {
  153. var w = window.open('', IPython._target);
  154. that.contents.new_untitled(that.notebook_path || '', {type: 'file', ext: '.txt'}).then(function(data) {
  155. w.location = utils.url_path_join(
  156. that.base_url, 'edit',
  157. utils.encode_uri_components(data.path)
  158. );
  159. }).catch(function (e) {
  160. w.close();
  161. dialog.modal({
  162. title: i18n.msg._('Creating File Failed'),
  163. body: $('<div/>')
  164. .text(i18n.msg._("An error occurred while creating a new file."))
  165. .append($('<div/>')
  166. .addClass('alert alert-danger')
  167. .text(e.message || e)),
  168. buttons: {
  169. OK: {'class': 'btn-primary'}
  170. }
  171. });
  172. console.warn('Error during New file creation', e);
  173. });
  174. that.load_sessions();
  175. e.preventDefault();
  176. });
  177. $('#new-folder').click(function(e) {
  178. that.contents.new_untitled(that.notebook_path || '', {type: 'directory'})
  179. .then(function(){
  180. that.load_list();
  181. }).catch(function (e) {
  182. dialog.modal({
  183. title: i18n.msg._('Creating Folder Failed'),
  184. body: $('<div/>')
  185. .text(i18n.msg._("An error occurred while creating a new folder."))
  186. .append($('<div/>')
  187. .addClass('alert alert-danger')
  188. .text(e.message || e)),
  189. buttons: {
  190. OK: {'class': 'btn-primary'}
  191. }
  192. });
  193. console.warn('Error during New directory creation', e);
  194. });
  195. that.load_sessions();
  196. e.preventDefault();
  197. });
  198. // Bind events for action buttons.
  199. $('.rename-button').click($.proxy(this.rename_selected, this));
  200. $('.move-button').click($.proxy(this.move_selected, this));
  201. $('.download-button').click($.proxy(this.download_selected, this));
  202. $('.shutdown-button').click($.proxy(this.shutdown_selected, this));
  203. $('.duplicate-button').click($.proxy(this.duplicate_selected, this));
  204. $('.view-button').click($.proxy(this.view_selected, this));
  205. $('.edit-button').click($.proxy(this.edit_selected, this));
  206. $('.delete-button').click($.proxy(this.delete_selected, this));
  207. // Bind events for selection menu buttons.
  208. $('#selector-menu').click(function (event) {
  209. that.select($(event.target).attr('id'));
  210. });
  211. var select_all = $('#select-all');
  212. select_all.change(function () {
  213. if (!select_all.prop('checked') || select_all.data('indeterminate')) {
  214. that.select('select-none');
  215. } else {
  216. that.select('select-all');
  217. }
  218. });
  219. $('#button-select-all').click(function (e) {
  220. // toggle checkbox if the click doesn't come from the checkbox already
  221. if (!$(e.target).is('input[type=checkbox]')) {
  222. if (select_all.prop('checked') || select_all.data('indeterminate')) {
  223. that.select('select-none');
  224. } else {
  225. that.select('select-all');
  226. }
  227. }
  228. });
  229. $('.sort-action').click(function(e) {
  230. var sort_on = e.target.id;
  231. // Clear sort indications in UI
  232. $(".sort-action i").removeClass("fa-arrow-up").removeClass("fa-arrow-down");
  233. if ((that.sort_id === sort_on) && (that.sort_direction === 1)) {
  234. that.sort_list(sort_on, 0);
  235. $("#" + sort_on + " i").addClass("fa-arrow-up");
  236. that.sort_direction = 0;
  237. } else {
  238. that.sort_list(sort_on, 1);
  239. $("#" + sort_on + " i").addClass("fa-arrow-down");
  240. that.sort_direction = 1;
  241. }
  242. that.sort_id = sort_on;
  243. });
  244. }
  245. };
  246. NotebookList.prototype.sort_list = function(id, order) {
  247. if (sort_functions.hasOwnProperty(id)) {
  248. this.sort_function = sort_functions[id](order);
  249. this.draw_notebook_list(this.model_list, this.error_msg);
  250. } else {
  251. console.error("No such sort id: '" + id + "'")
  252. }
  253. };
  254. NotebookList.prototype.handleFilesUpload = function(event, dropOrForm) {
  255. var that = this;
  256. var files;
  257. if(dropOrForm === 'drop'){
  258. files = event.originalEvent.dataTransfer.files;
  259. } else {
  260. files = event.originalEvent.target.files;
  261. }
  262. var reader_onload = function (event) {
  263. var item = $(event.target).data('item');
  264. that.add_file_data(event.target.result, item);
  265. that.add_upload_button(item);
  266. };
  267. var reader_onerror = function (event) {
  268. var item = $(event.target).data('item');
  269. var name = item.data('name');
  270. item.remove();
  271. dialog.modal({
  272. title : i18n.msg._('Failed to read file'),
  273. body : i18n.msg.sprintf(i18n.msg._("Failed to read file %s"),name),
  274. buttons : {'OK' : { 'class' : 'btn-primary' }}
  275. });
  276. };
  277. Array.from(files).forEach(function(f) {
  278. var name_and_ext = utils.splitext(f.name);
  279. var file_ext = name_and_ext[1];
  280. if (f.size > that._max_upload_size_mb * 1024 * 1024) {
  281. var body_msg = i18n.msg.sprintf(i18n.msg._("The file size is %d MB. Do you still want to upload it?"),
  282. Math.round(f.size / (1024 * 1024)));
  283. dialog.modal({
  284. title : i18n.msg._('Large file size warning'),
  285. body : body_msg,
  286. buttons : {
  287. Cancel: {},
  288. Ok: {
  289. class: "btn-primary",
  290. click: function() {
  291. that.add_large_file_upload_button(f);
  292. }
  293. }
  294. }
  295. });
  296. }
  297. else{
  298. var reader = new FileReader();
  299. if (file_ext === '.ipynb') {
  300. reader.readAsText(f);
  301. } else {
  302. // read non-notebook files as binary
  303. reader.readAsArrayBuffer(f);
  304. }
  305. var item = that.new_item(0, true);
  306. item.addClass('new-file');
  307. that.add_name_input(f.name, item, file_ext === '.ipynb' ? 'notebook' : 'file');
  308. // Store the list item in the reader so we can use it later
  309. // to know which item it belongs to.
  310. $(reader).data('item', item);
  311. reader.onload = reader_onload;
  312. reader.onerror = reader_onerror;
  313. }
  314. });
  315. // Clear fileinput value. This is required to
  316. // reset the form. Otherwise, if you upload a file, delete it and try to
  317. // upload it again, the changed event won't fire.
  318. var form = $('input.fileinput');
  319. form.val('');
  320. return false;
  321. };
  322. NotebookList.prototype.clear_list = function (remove_uploads) {
  323. /**
  324. * Clears the navigation tree.
  325. *
  326. * Parameters
  327. * remove_uploads: bool=False
  328. * Should upload prompts also be removed from the tree.
  329. */
  330. if (remove_uploads) {
  331. this.element.children('.list_item').remove();
  332. } else {
  333. this.element.children('.list_item:not(.new-file)').remove();
  334. }
  335. };
  336. NotebookList.prototype.load_sessions = function(){
  337. this.session_list.load_sessions();
  338. };
  339. NotebookList.prototype.sessions_loaded = function(data){
  340. this.sessions = data;
  341. this.load_list();
  342. };
  343. NotebookList.prototype.load_list = function () {
  344. var that = this;
  345. // Add an event handler browser back and forward events
  346. window.onpopstate = function(e) {
  347. var path = (window.history.state && window.history.state.path) ?
  348. window.history.state.path : that.initial_notebook_path;
  349. that.update_location(path);
  350. };
  351. var breadcrumb = $('.breadcrumb');
  352. breadcrumb.empty();
  353. var list_item = $('<li/>');
  354. var root_url = utils.url_path_join(that.base_url, '/tree');
  355. var root = $('<li/>').append(
  356. $("<a/>")
  357. .attr('href', root_url)
  358. .append(
  359. $("<i/>")
  360. .addClass('fa fa-folder')
  361. )
  362. .click(function(e) {
  363. // Allow the default browser action when the user holds a modifier (e.g., Ctrl-Click)
  364. if(e.altKey || e.metaKey || e.shiftKey) {
  365. return true;
  366. }
  367. var path = '';
  368. window.history.pushState(
  369. {path: path},
  370. 'Home',
  371. utils.url_path_join(that.base_url, 'tree')
  372. );
  373. that.update_location(path);
  374. return false;
  375. })
  376. );
  377. breadcrumb.append(root);
  378. var path_parts = [];
  379. this.notebook_path.split('/').forEach(function(path_part) {
  380. path_parts.push(path_part);
  381. var path = path_parts.join('/');
  382. var url = utils.url_path_join(
  383. that.base_url,
  384. '/tree',
  385. utils.encode_uri_components(path)
  386. );
  387. var crumb = $('<li/>').append(
  388. $('<a/>')
  389. .attr('href', url)
  390. .text(path_part)
  391. .click(function(e) {
  392. // Allow the default browser action when the user holds a modifier (e.g., Ctrl-Click)
  393. if(e.altKey || e.metaKey || e.shiftKey) {
  394. return true;
  395. }
  396. window.history.pushState(
  397. {path: path},
  398. path,
  399. url
  400. );
  401. that.update_location(path);
  402. return false;
  403. })
  404. );
  405. breadcrumb.append(crumb);
  406. });
  407. this.contents.list_contents(that.notebook_path).then(
  408. $.proxy(this.draw_notebook_list, this),
  409. function(error) {
  410. that.draw_notebook_list({content: []}, i18n.msg._("Server error: ") + error.message);
  411. }
  412. );
  413. };
  414. NotebookList.prototype.update_location = function (path) {
  415. this.notebook_path = path;
  416. $('body').attr('data-notebook-path', path);
  417. // Update the file tree list without reloading the page
  418. this.load_list();
  419. // Update the page title so the browser tab reflects it
  420. // Match how the title appears with a trailing slash or
  421. // "Home" if the page loads from the server.
  422. $('title').text(path ? path+'/' : i18n.msg._("Home"));
  423. };
  424. /**
  425. * Draw the list of notebooks
  426. * @method draw_notebook_list
  427. * @param {Array} list An array of dictionaries representing files or
  428. * directories.
  429. * @param {String} error_msg An error message
  430. */
  431. var type_order = {'directory':0,'notebook':1,'file':2};
  432. NotebookList.prototype.draw_notebook_list = function (list, error_msg) {
  433. // Remember what was selected before the refresh.
  434. var selected_before = this.selected;
  435. // Store the data to be redrawn by sorting
  436. this.model_list = list;
  437. this.error_msg = error_msg;
  438. list.content.sort(this.sort_function);
  439. var message = error_msg || i18n.msg._('The notebook list is empty.');
  440. var item = null;
  441. var model = null;
  442. var len = list.content.length;
  443. this.clear_list();
  444. var n_uploads = this.element.children('.list_item').length;
  445. if (len === 0) {
  446. item = this.new_item(0);
  447. var span12 = item.children().first();
  448. span12.empty();
  449. span12.append($('<div style="margin:auto;text-align:center;color:grey"/>').text(message));
  450. }
  451. var path = this.notebook_path;
  452. var offset = n_uploads;
  453. if (path !== '') {
  454. item = this.new_item(offset, false);
  455. model = {
  456. type: 'directory',
  457. name: '..',
  458. path: utils.url_path_split(path)[0]
  459. };
  460. this.add_link(model, item);
  461. offset += 1;
  462. }
  463. for (var i=0; i<len; i++) {
  464. model = list.content[i];
  465. item = this.new_item(i+offset, true);
  466. try {
  467. this.add_link(model, item);
  468. } catch(err) {
  469. console.log('Error adding link: ' + err);
  470. }
  471. }
  472. // Trigger an event when we've finished drawing the notebook list.
  473. events.trigger('draw_notebook_list.NotebookList');
  474. // Reselect the items that were selected before. Notify listeners
  475. // that the selected items may have changed. O(n^2) operation.
  476. selected_before.forEach(function(item) {
  477. var list_items = $('.list_item');
  478. for (var i=0; i<list_items.length; i++) {
  479. var $list_item = $(list_items[i]);
  480. if ($list_item.data('path') === item.path) {
  481. $list_item.find('input[type=checkbox]').prop('checked', true);
  482. break;
  483. }
  484. }
  485. });
  486. this._selection_changed();
  487. };
  488. /**
  489. * Creates a new item.
  490. * @param {integer} index
  491. * @param {boolean} [selectable] - tristate, undefined: don't draw checkbox,
  492. * false: don't draw checkbox but pad
  493. * where it should be, true: draw checkbox.
  494. * @return {JQuery} row
  495. */
  496. NotebookList.prototype.new_item = function (index, selectable) {
  497. var row = $('<div/>')
  498. .addClass("list_item")
  499. .addClass("row");
  500. var item = $("<div/>")
  501. .addClass("col-md-12")
  502. .appendTo(row);
  503. var checkbox;
  504. if (selectable !== undefined) {
  505. checkbox = $('<input/>')
  506. .attr('type', 'checkbox')
  507. .attr('title', i18n.msg._('Click here to rename, delete, etc.'))
  508. .appendTo(item);
  509. }
  510. $('<i/>')
  511. .addClass('item_icon')
  512. .appendTo(item);
  513. var link = $("<a/>")
  514. .addClass("item_link")
  515. .appendTo(item);
  516. $("<span/>")
  517. .addClass("item_name")
  518. .appendTo(link);
  519. $("<span/>")
  520. .addClass("file_size")
  521. .addClass("pull-right")
  522. .appendTo(item);
  523. $("<span/>")
  524. .addClass("item_modified")
  525. .addClass("pull-right")
  526. .appendTo(item);
  527. if (selectable === false) {
  528. checkbox.css('visibility', 'hidden');
  529. } else if (selectable === true) {
  530. var that = this;
  531. row.click(function(e) {
  532. // toggle checkbox only if the click doesn't come from the checkbox or the link
  533. if (!$(e.target).is('span[class=item_name]') && !$(e.target).is('input[type=checkbox]')) {
  534. checkbox.prop('checked', !checkbox.prop('checked'));
  535. }
  536. that._selection_changed();
  537. });
  538. }
  539. var buttons = $('<div/>')
  540. .addClass("item_buttons pull-right")
  541. .appendTo(item);
  542. $('<div/>')
  543. .addClass('running-indicator')
  544. .text(i18n.msg._('Running'))
  545. .css('visibility', 'hidden')
  546. .appendTo(buttons);
  547. if (index === -1) {
  548. this.element.append(row);
  549. } else {
  550. this.element.children().eq(index).after(row);
  551. }
  552. return row;
  553. };
  554. NotebookList.icons = {
  555. directory: 'folder_icon',
  556. notebook: 'notebook_icon',
  557. file: 'file_icon'
  558. };
  559. NotebookList.uri_prefixes = {
  560. directory: 'tree',
  561. notebook: 'notebooks',
  562. file: 'edit'
  563. };
  564. /**
  565. * Select all items in the tree of specified type.
  566. * selection_type : string among "select-all", "select-folders", "select-notebooks", "select-running-notebooks", "select-files"
  567. * any other string (like "select-none") deselects all items
  568. */
  569. NotebookList.prototype.select = function(selection_type) {
  570. var that = this;
  571. $('.list_item').each(function(index, item) {
  572. var item_type = $(item).data('type');
  573. var state = false;
  574. state = state || (selection_type === "select-all");
  575. state = state || (selection_type === "select-folders" && item_type === 'directory');
  576. state = state || (selection_type === "select-notebooks" && item_type === 'notebook');
  577. state = state || (selection_type === "select-running-notebooks" && item_type === 'notebook' && that.sessions[$(item).data('path')] !== undefined);
  578. state = state || (selection_type === "select-files" && item_type === 'file');
  579. $(item).find('input[type=checkbox]').prop('checked', state);
  580. });
  581. this._selection_changed();
  582. };
  583. NotebookList.prototype._is_notebook = function(model) {
  584. var ipynb_extensions = ['ipynb'];
  585. return includes_extension(model.path, ipynb_extensions);
  586. };
  587. NotebookList.prototype._is_editable = function(model) {
  588. // Allow any file to be "edited"
  589. // Non-text files will display the following error:
  590. // Error: [FILE] is not UTF-8 encoded
  591. // Saving is disabled.
  592. // See Console for more details.
  593. return true;
  594. };
  595. NotebookList.prototype._is_viewable = function(model) {
  596. var html_types = ['htm', 'html', 'xhtml', 'xml', 'mht', 'mhtml'];
  597. var media_extension = ['3gp', 'avi', 'mov', 'mp4', 'm4v', 'm4a', 'mp3', 'mkv', 'ogv', 'ogm', 'ogg', 'oga', 'webm', 'wav'];
  598. var image_type = ['bmp', 'gif', 'jpg', 'jpeg', 'png', 'webp'];
  599. var other_type = ['ico'];
  600. var viewable_extensions = [].concat(html_types, media_extension, image_type, other_type);
  601. return model.mimetype === 'text/html'
  602. || includes_extension(model.path, viewable_extensions);
  603. };
  604. // Files like PDF that should be opened using `/files` prefix
  605. NotebookList.prototype._is_pdflike = function(model) {
  606. var pdflike_extensions = ['pdf'];
  607. return includes_extension(model.path, pdflike_extensions);
  608. };
  609. /**
  610. * Handles when any row selector checkbox is toggled.
  611. */
  612. NotebookList.prototype._selection_changed = function() {
  613. // Use a JQuery selector to find each row with a checked checkbox. If
  614. // we decide to add more checkboxes in the future, this code will need
  615. // to be changed to distinguish which checkbox is the row selector.
  616. var that = this;
  617. var selected = [];
  618. var has_running_notebook = false;
  619. var has_directory = false;
  620. var has_file = false;
  621. var checked = 0;
  622. $('.list_item :checked').each(function(index, item) {
  623. var parent = $(item).parent().parent();
  624. // If the item doesn't have an upload button, isn't the
  625. // breadcrumbs and isn't the parent folder '..', then it can be selected.
  626. // Breadcrumbs path == ''.
  627. if (parent.find('.upload_button').length === 0 && parent.data('path') !== '' && parent.data('path') !== utils.url_path_split(that.notebook_path)[0]) {
  628. checked++;
  629. selected.push({
  630. name: parent.data('name'),
  631. path: parent.data('path'),
  632. type: parent.data('type')
  633. });
  634. // Set flags according to what is selected. Flags are later
  635. // used to decide which action buttons are visible.
  636. has_running_notebook = has_running_notebook ||
  637. (parent.data('type') === 'notebook' && that.sessions[parent.data('path')] !== undefined);
  638. has_file = has_file || (parent.data('type') === 'file');
  639. has_directory = has_directory || (parent.data('type') === 'directory');
  640. }
  641. });
  642. this.selected = selected;
  643. // Rename is only visible when one item is selected, and it is not a running notebook
  644. if (selected.length === 1 && !has_running_notebook) {
  645. $('.rename-button').css('display', 'inline-block');
  646. } else {
  647. $('.rename-button').css('display', 'none');
  648. }
  649. // Move is visible if at least one item is selected, and none of them
  650. // are a running notebook.
  651. if (selected.length > 0 && !has_running_notebook) {
  652. $('.move-button').css('display', 'inline-block');
  653. } else {
  654. $('.move-button').css('display', 'none');
  655. }
  656. // Download is only visible when one item is selected, and it is not a
  657. // running notebook or a directory
  658. // TODO(nhdaly): Add support for download multiple items at once.
  659. if (selected.length === 1 && !has_running_notebook && !has_directory) {
  660. $('.download-button').css('display', 'inline-block');
  661. } else {
  662. $('.download-button').css('display', 'none');
  663. }
  664. // Shutdown is only visible when one or more notebooks running notebooks
  665. // are selected and no non-notebook items are selected.
  666. if (has_running_notebook && !(has_file || has_directory)) {
  667. $('.shutdown-button').css('display', 'inline-block');
  668. } else {
  669. $('.shutdown-button').css('display', 'none');
  670. }
  671. // Duplicate isn't visible when a directory is selected.
  672. if (selected.length > 0 && !has_directory) {
  673. $('.duplicate-button').css('display', 'inline-block');
  674. } else {
  675. $('.duplicate-button').css('display', 'none');
  676. }
  677. // Delete is visible if one or more items are selected.
  678. if (selected.length > 0) {
  679. $('.delete-button').css('display', 'inline-block');
  680. } else {
  681. $('.delete-button').css('display', 'none');
  682. }
  683. // View is visible in the following case:
  684. //
  685. // - the item is editable
  686. // - it is not a notebook
  687. //
  688. // If it's not editable or unknown, the default action should be view
  689. // already so no need to show the button.
  690. // That should include things like, html, py, txt, json....
  691. if (selected.length >= 1 && !has_directory) {
  692. $('.view-button').css('display', 'inline-block');
  693. } else {
  694. $('.view-button').css('display', 'none');
  695. }
  696. // Edit is visible when an item is unknown, that is to say:
  697. // - not in the editable list
  698. // - not in the known non-editable list.
  699. // - not a notebook.
  700. // Indeed if it's editable the default action is already to edit.
  701. // And non editable files should not show edit button.
  702. // for unknown we'll assume users know what they are doing.
  703. if (selected.length >= 1 && !has_directory && selected.every(function(el) {
  704. return that._is_editable(el);
  705. })) {
  706. $('.edit-button').css('display', 'inline-block');
  707. } else {
  708. $('.edit-button').css('display', 'none');
  709. }
  710. // If all of the items are selected, show the selector as checked. If
  711. // some of the items are selected, show it as checked. Otherwise,
  712. // uncheck it.
  713. var total = 0;
  714. $('.list_item input[type=checkbox]').each(function(index, item) {
  715. var parent = $(item).parent().parent();
  716. // If the item doesn't have an upload button and it's not the
  717. // breadcrumbs, it can be selected. Breadcrumbs path == ''.
  718. if (parent.find('.upload_button').length === 0 && parent.data('path') !== '' && parent.data('path') !== utils.url_path_split(that.notebook_path)[0]) {
  719. total++;
  720. }
  721. });
  722. var select_all = $("#select-all");
  723. if (checked === 0) {
  724. select_all.prop('checked', false);
  725. select_all.prop('indeterminate', false);
  726. select_all.data('indeterminate', false);
  727. } else if (checked === total) {
  728. select_all.prop('checked', true);
  729. select_all.prop('indeterminate', false);
  730. select_all.data('indeterminate', false);
  731. } else {
  732. select_all.prop('checked', false);
  733. select_all.prop('indeterminate', true);
  734. select_all.data('indeterminate', true);
  735. }
  736. // Update total counter
  737. checked = bidi.applyBidi(checked);
  738. $('#counter-select-all').html(checked===0 ? '&nbsp;' : checked);
  739. // If at aleast on item is selected, hide the selection instructions.
  740. if (checked > 0) {
  741. $('.dynamic-instructions').hide();
  742. } else {
  743. $('.dynamic-instructions').show();
  744. }
  745. };
  746. NotebookList.prototype.add_link = function (model, item) {
  747. var that = this;
  748. var running = (model.type === 'notebook' && this.sessions[model.path] !== undefined);
  749. item.data('name',model.name);
  750. item.data('path', model.path);
  751. item.data('modified', model.last_modified);
  752. item.data('type', model.type);
  753. item.find(".item_name").text(bidi.applyBidi(model.name));
  754. var icon = NotebookList.icons[model.type];
  755. if (running) {
  756. icon = 'running_' + icon;
  757. }
  758. var uri_prefix = NotebookList.uri_prefixes[model.type];
  759. if (model.type === 'file' && this._is_viewable(model))
  760. {
  761. uri_prefix = 'view';
  762. }
  763. if (model.type === 'file' && this._is_pdflike(model))
  764. {
  765. uri_prefix = 'files';
  766. }
  767. if (model.type === 'file' && this._is_notebook(model))
  768. {
  769. uri_prefix = 'notebooks';
  770. }
  771. item.find(".item_icon").addClass(icon).addClass('icon-fixed-width');
  772. var link = item.find("a.item_link")
  773. .attr('href',
  774. utils.url_path_join(
  775. this.base_url,
  776. uri_prefix,
  777. utils.encode_uri_components(model.path)
  778. )
  779. );
  780. item.find(".item_buttons .running-indicator").css('visibility', running ? '' : 'hidden');
  781. // directory nav doesn't open new tabs
  782. // files, notebooks do
  783. if (model.type !== "directory") {
  784. link.attr('target', IPython._target);
  785. } else {
  786. // Replace with a click handler that will use the History API to
  787. // push a new route without reloading the page if the click is
  788. // not modified (e.g., Ctrl-Click)
  789. link.click(function (e) {
  790. if(e.altKey || e.metaKey || e.shiftKey) {
  791. return true;
  792. }
  793. window.history.pushState({
  794. path: model.path
  795. }, model.path, utils.url_path_join(
  796. that.base_url,
  797. 'tree',
  798. utils.encode_uri_components(model.path)
  799. ));
  800. that.update_location(model.path);
  801. return false;
  802. });
  803. }
  804. // Add in the date that the file was last modified
  805. item.find(".item_modified").text(utils.format_datetime(model.last_modified));
  806. item.find(".item_modified").attr("title", moment(model.last_modified).format("YYYY-MM-DD HH:mm"));
  807. var filesize = utils.format_filesize(model.size);
  808. item.find(".file_size").text(filesize || '\xA0');
  809. };
  810. NotebookList.prototype.add_name_input = function (name, item, icon_type) {
  811. item.data('name', name);
  812. item.find(".item_icon").addClass(NotebookList.icons[icon_type]).addClass('icon-fixed-width');
  813. item.find(".item_name").empty().append(
  814. $('<input/>')
  815. .addClass("filename_input")
  816. .attr('value', name)
  817. .attr('size', '30')
  818. .attr('type', 'text')
  819. .keyup(function(event){
  820. if(event.keyCode === 13){item.find('.upload_button').click();}
  821. else if(event.keyCode === 27){item.remove();}
  822. })
  823. );
  824. };
  825. NotebookList.prototype.add_file_data = function (data, item) {
  826. item.data('filedata', data);
  827. };
  828. NotebookList.prototype.shutdown_selected = function() {
  829. var that = this;
  830. this.selected.forEach(function(item) {
  831. if (item.type === 'notebook') {
  832. that.shutdown_notebook(item.path);
  833. }
  834. });
  835. // Deselect items after successful shutdown.
  836. that.select('select-none');
  837. };
  838. NotebookList.prototype.shutdown_notebook = function(path) {
  839. var that = this;
  840. var settings = {
  841. processData : false,
  842. cache : false,
  843. type : "DELETE",
  844. dataType : "json",
  845. success : function () {
  846. that.load_sessions();
  847. },
  848. error : utils.log_ajax_error
  849. };
  850. var session = this.sessions[path];
  851. if (session) {
  852. var url = utils.url_path_join(
  853. this.base_url,
  854. 'api/sessions',
  855. encodeURIComponent(session.id)
  856. );
  857. utils.ajax(url, settings);
  858. }
  859. };
  860. NotebookList.prototype.rename_selected = function() {
  861. if (this.selected.length !== 1){
  862. return;
  863. }
  864. var that = this;
  865. var item_path = this.selected[0].path;
  866. var item_name = this.selected[0].name;
  867. var item_type = this.selected[0].type;
  868. var input = $('<input/>').attr('type','text').attr('size','25').addClass('form-control')
  869. .val(item_name);
  870. var rename_msg = function (type) {
  871. switch(type) {
  872. case 'file': return i18n.msg._("Enter a new file name:");
  873. case 'directory': return i18n.msg._("Enter a new directory name:");
  874. case 'notebook': return i18n.msg._("Enter a new notebook name:");
  875. default: return i18n.msg._("Enter a new name:");
  876. }
  877. };
  878. var rename_title = function (type) {
  879. switch(type) {
  880. case 'file': return i18n.msg._("Rename file");
  881. case 'directory': return i18n.msg._("Rename directory");
  882. case 'notebook': return i18n.msg._("Rename notebook");
  883. default: return i18n.msg._("Rename");
  884. }
  885. };
  886. var dialog_body = $('<div/>').append(
  887. $("<p/>").addClass("rename-message")
  888. .text(rename_msg(item_type))
  889. ).append(
  890. $("<br/>")
  891. ).append(input);
  892. // This statement is used simply so that message extraction
  893. // will pick up the strings. The actual setting of the text
  894. // for the button is in dialog.js.
  895. var button_labels = [ i18n.msg._("Cancel"), i18n.msg._("Rename"), i18n.msg._("OK"), i18n.msg._("Move")];
  896. var d = dialog.modal({
  897. title : rename_title(item_type),
  898. body : dialog_body,
  899. default_button: "Cancel",
  900. buttons : {
  901. Cancel: {},
  902. Rename : {
  903. class: "btn-primary",
  904. click: function() {
  905. that.contents.rename(item_path, utils.url_path_join(that.notebook_path, input.val())).then(function() {
  906. that.load_list();
  907. // Deselect items after successful rename.
  908. that.select('select-none');
  909. }).catch(function(e) {
  910. var template = i18n.msg._("An error occurred while renaming \"%1$s\" to \"%2$s\".");
  911. var failmsg = i18n.msg.sprintf(template,item_name,input.val());
  912. dialog.modal({
  913. title: i18n.msg._("Rename Failed"),
  914. body: $('<div/>')
  915. .text(failmsg)
  916. .append($('<div/>')
  917. .addClass('alert alert-danger')
  918. .text(e.message || e)),
  919. buttons: {
  920. OK: {'class': 'btn-primary'}
  921. }
  922. });
  923. console.warn('Error during renaming :', e);
  924. });
  925. }
  926. }
  927. },
  928. open : function () {
  929. // Upon ENTER, click the OK button.
  930. input.keydown(function (event) {
  931. if (event.which === keyboard.keycodes.enter) {
  932. d.find('.btn-primary').first().click();
  933. return false;
  934. }
  935. });
  936. input.focus();
  937. // Highlight the filename (up to the filetype suffix) in the input field.
  938. if (input.val().indexOf(".") > 0) {
  939. input[0].setSelectionRange(0,input.val().indexOf("."));
  940. } else {
  941. input.select();
  942. }
  943. }
  944. });
  945. };
  946. NotebookList.prototype.move_selected = function() {
  947. var that = this;
  948. var selected = that.selected.slice(); // Don't let that.selected change out from under us
  949. var num_items = selected.length;
  950. // Can move one or more selected items.
  951. if (!(num_items >= 1)) {
  952. return;
  953. }
  954. // Open a dialog to enter the new path, with current path as default.
  955. var input = $('<input/>').attr('type','text').attr('size','25').addClass('form-control')
  956. .val(utils.url_path_join('/', that.notebook_path));
  957. var dialog_body = $('<div/>').append(
  958. $("<p/>").addClass("rename-message")
  959. .text(i18n.msg.sprintf(i18n.msg.ngettext("Enter a new destination directory path for this item:",
  960. "Enter a new destination directory path for these %d items:", num_items),num_items))
  961. ).append(
  962. $("<br/>")
  963. ).append(
  964. $("<div/>").append(
  965. // $("<i/>").addClass("fa fa-folder").addClass("server-root")
  966. $("<span/>").text(utils.get_body_data("serverRoot")).addClass("server-root")
  967. ).append(
  968. input.addClass("path-input")
  969. ).addClass("move-path")
  970. );
  971. var d = dialog.modal({
  972. title : i18n.msg.sprintf(i18n.msg.ngettext("Move an Item","Move %d Items",num_items),num_items),
  973. body : dialog_body,
  974. default_button: "Cancel",
  975. buttons : {
  976. Cancel : {},
  977. Move : {
  978. class: "btn-primary",
  979. click: function() {
  980. // Move all the items.
  981. selected.forEach(function(item) {
  982. var item_path = item.path;
  983. var item_name = item.name;
  984. // Construct the new path using the user input and the item's name.
  985. var new_path = utils.url_path_join(input.val(), item_name);
  986. that.contents.rename(item_path, new_path).then(function() {
  987. // After each move finishes, reload the list.
  988. that.load_list();
  989. }).catch(function(e) {
  990. // If any of the moves fails, show this dialog for that move.
  991. var failmsg = i18n.msg._("An error occurred while moving \"%1$s\" from \"%2$s\" to \"%3$s\".");
  992. dialog.modal({
  993. title: i18n.msg._("Move Failed"),
  994. body: $('<div/>')
  995. .text(i18n.msg.sprintf(failmsg,item_name,item_path,new_path))
  996. .append($('<div/>')
  997. .addClass('alert alert-danger')
  998. .text(e.message || e)),
  999. buttons: {
  1000. OK: {'class': 'btn-primary'}
  1001. }
  1002. });
  1003. console.warn('Error during moving :', e);
  1004. });
  1005. }); // End of forEach.
  1006. }
  1007. }
  1008. },
  1009. // TODO: Consider adding fancier UI per Issue #941.
  1010. open : function () {
  1011. // Upon ENTER, click the OK button.
  1012. input.keydown(function (event) {
  1013. if (event.which === keyboard.keycodes.enter) {
  1014. d.find('.btn-primary').first().click();
  1015. return false;
  1016. }
  1017. });
  1018. // Put the cursor at the end of the input.
  1019. input.focus();
  1020. }
  1021. });
  1022. };
  1023. NotebookList.prototype.download_selected = function() {
  1024. var that = this;
  1025. // TODO(nhdaly): Support download multiple items at once.
  1026. if (that.selected.length !== 1){
  1027. return;
  1028. }
  1029. var item_path = that.selected[0].path;
  1030. window.open(utils.url_path_join(that.base_url, 'files', utils.encode_uri_components(item_path)) + '?download=1', IPython._target);
  1031. };
  1032. NotebookList.prototype.delete_selected = function() {
  1033. var selected = this.selected.slice(); // Don't let that.selected change out from under us
  1034. var template = i18n.msg.ngettext("Are you sure you want to permanently delete: \"%s\"?",
  1035. "Are you sure you want to permanently delete the %d files or folders selected?",
  1036. selected.length);
  1037. var delete_msg;
  1038. if (selected.length === 1) {
  1039. delete_msg = i18n.msg.sprintf(template, selected[0].name);
  1040. } else {
  1041. delete_msg = i18n.msg.sprintf(template, selected.length);
  1042. }
  1043. var that = this;
  1044. dialog.modal({
  1045. title : i18n.msg._("Delete"),
  1046. body : delete_msg,
  1047. default_button: "Cancel",
  1048. buttons : {
  1049. Cancel: {},
  1050. Delete : {
  1051. class: "btn-danger",
  1052. click: function() {
  1053. // Shutdown any/all selected notebooks before deleting
  1054. // the files.
  1055. that.shutdown_selected();
  1056. // Delete selected.
  1057. selected.forEach(function(item) {
  1058. that.contents.delete(item.path).then(function() {
  1059. that.notebook_deleted(item.path);
  1060. }).catch(function(e) {
  1061. var failmsg = i18n.msg._("An error occurred while deleting \"%s\".");
  1062. dialog.modal({
  1063. title: i18n.msg._("Delete Failed"),
  1064. body: $('<div/>')
  1065. .text(i18n.msg.sprintf(failmsg, item.path))
  1066. .append($('<div/>')
  1067. .addClass('alert alert-danger')
  1068. .text(e.message || e)),
  1069. buttons: {
  1070. OK: {'class': 'btn-primary'}
  1071. }
  1072. });
  1073. console.warn('Error during content deletion:', e);
  1074. });
  1075. });
  1076. }
  1077. }
  1078. }
  1079. });
  1080. };
  1081. NotebookList.prototype.view_selected = function() {
  1082. var that = this;
  1083. that.selected.forEach(function(item) {
  1084. var item_path = utils.encode_uri_components(item.path);
  1085. var item_type = that._is_notebook(item) ? 'notebooks' : that._is_viewable(item) ? 'view' : 'files';
  1086. window.open(utils.url_path_join(that.base_url, item_type, item_path), IPython._target);
  1087. });
  1088. };
  1089. NotebookList.prototype.edit_selected = function() {
  1090. var that = this;
  1091. that.selected.forEach(function(item) {
  1092. var item_path = utils.encode_uri_components(item.path);
  1093. window.open(utils.url_path_join(that.base_url, 'edit', item_path), IPython._target);
  1094. });
  1095. };
  1096. NotebookList.prototype.duplicate_selected = function() {
  1097. var selected = this.selected.slice(); // Don't let that.selected change out from under us
  1098. var template = i18n.msg.ngettext("Are you sure you want to duplicate: \"%s\"?",
  1099. "Are you sure you want to duplicate the %d files selected?",selected.length);
  1100. var dup_msg;
  1101. if (selected.length === 1) {
  1102. dup_msg = i18n.msg.sprintf(template,selected[0].name);
  1103. } else {
  1104. dup_msg = i18n.msg.sprintf(template,selected.length);
  1105. }
  1106. var that = this;
  1107. dialog.modal({
  1108. title : i18n.msg._("Duplicate"),
  1109. body : dup_msg,
  1110. default_button: "Cancel",
  1111. buttons : {
  1112. Cancel: {},
  1113. Duplicate : {
  1114. class: "btn-primary",
  1115. click: function() {
  1116. selected.forEach(function(item) {
  1117. that.contents.copy(item.path, that.notebook_path).then(function () {
  1118. that.load_list();
  1119. // Deselect items after successful duplication.
  1120. that.select('select-none');
  1121. }).catch(function(e) {
  1122. var failmsg = i18n.msg._("An error occurred while duplicating \"%s\".");
  1123. dialog.modal({
  1124. title: i18n.msg._("Duplicate Failed"),
  1125. body: $('<div/>')
  1126. .text(i18n.msg.sprintf(failmsg,item.path))
  1127. .append($('<div/>')
  1128. .addClass('alert alert-danger')
  1129. .text(e.message || e)),
  1130. buttons: {
  1131. OK: {'class': 'btn-primary'}
  1132. }
  1133. });
  1134. console.warn('Error during content duplication', e);
  1135. });
  1136. });
  1137. }
  1138. }
  1139. }
  1140. });
  1141. };
  1142. NotebookList.prototype.notebook_deleted = function(path) {
  1143. /**
  1144. * Remove the deleted notebook.
  1145. */
  1146. var that = this;
  1147. $(".list_item").each(function() {
  1148. var element = $(this);
  1149. if (element.data("path") === path) {
  1150. element.remove();
  1151. events.trigger('notebook_deleted.NotebookList');
  1152. that._selection_changed();
  1153. }
  1154. });
  1155. };
  1156. // Add a new class for large file upload
  1157. NotebookList.prototype.add_large_file_upload_button = function (file) {
  1158. var that = this;
  1159. var item = that.new_item(0, true);
  1160. var stop_signal = false;
  1161. item.addClass('new-file');
  1162. that.add_name_input(file.name, item, 'file');
  1163. var cancel_button = $('<button/>').text("Cancel")
  1164. .addClass("btn btn-default btn-xs")
  1165. .click(function (e) {
  1166. item.remove();
  1167. stop_signal = true;
  1168. return false;
  1169. });
  1170. var upload_button = $('<button/>').text("Upload")
  1171. .addClass('btn btn-primary btn-xs upload_button')
  1172. .click(function (e) {
  1173. var filename = item.find('.item_name > input').val();
  1174. var path = utils.url_path_join(that.notebook_path, filename);
  1175. var format = 'text';
  1176. if (filename.length === 0 || filename[0] === '.') {
  1177. dialog.modal({
  1178. title : 'Invalid file name',
  1179. body : "File names must be at least one character and not start with a dot",
  1180. buttons : {'OK' : { 'class' : 'btn-primary' }}
  1181. });
  1182. return false;
  1183. }
  1184. var check_exist = function () {
  1185. var exists = false;
  1186. $.each(that.element.find('.list_item:not(.new-file)'), function(k,v){
  1187. if ($(v).data('name') === filename) { exists = true; return false; }
  1188. });
  1189. return exists
  1190. };
  1191. var exists = check_exist();
  1192. var add_uploading_button = function (f, item) {
  1193. // change buttons, add a progress bar
  1194. var uploading_button = item.find('.upload_button').text("Uploading");
  1195. uploading_button.off('click'); // Prevent double upload
  1196. var progress_bar = $('<span/>')
  1197. .addClass('progress-bar')
  1198. .css('top', '0')
  1199. .css('left', '0')
  1200. .css('width', '0')
  1201. .css('height', '3px')
  1202. .css('border-radius', '0 0 0 0')
  1203. .css('display', 'inline-block')
  1204. .css('position', 'absolute');
  1205. var parse_large_file = function (f, item) {
  1206. // codes inspired by https://stackoverflow.com/a/28318964
  1207. var chunk_size = 1024 * 1024;
  1208. var offset = 0;
  1209. var chunk = 0;
  1210. var chunk_reader = null;
  1211. var large_reader_onload = function (event) {
  1212. if (stop_signal === true) {
  1213. return;
  1214. }
  1215. if (event.target.error == null) {
  1216. offset += chunk_size;
  1217. if (offset >= f.size) {
  1218. chunk = -1;
  1219. } else {
  1220. chunk += 1;
  1221. }
  1222. // callback for handling reading: reader_onload in add_upload_button
  1223. var item = $(event.target).data('item');
  1224. that.add_file_data(event.target.result, item);
  1225. upload_file(item, chunk); // Do the upload
  1226. } else {
  1227. console.log("Read error: " + event.target.error);
  1228. }
  1229. };
  1230. var on_error = function (event) {
  1231. var item = $(event.target).data('item');
  1232. var name = item.data('name');
  1233. item.remove();
  1234. var _exists = check_exist();
  1235. if (_exists) {
  1236. that.contents.delete(path);
  1237. }
  1238. dialog.modal({
  1239. title : 'Failed to read file',
  1240. body : "Failed to read file '" + name + "'",
  1241. buttons : {'OK' : { 'class' : 'btn-primary' }}
  1242. });
  1243. };
  1244. chunk_reader = function (_offset, _f) {
  1245. var reader = new FileReader();
  1246. var blob = _f.slice(_offset, chunk_size + _offset);
  1247. // Load everything as ArrayBuffer
  1248. reader.readAsArrayBuffer(blob);
  1249. // Store the list item in the reader so we can use it later
  1250. // to know which item it belongs to.
  1251. $(reader).data('item', item);
  1252. reader.onload = large_reader_onload;
  1253. reader.onerror = on_error;
  1254. };
  1255. // These codes to upload file in original class
  1256. var upload_file = function(item, chunk) {
  1257. var filedata = item.data('filedata');
  1258. if (filedata instanceof ArrayBuffer) {
  1259. // base64-encode binary file data
  1260. var bytes = '';
  1261. var buf = new Uint8Array(filedata);
  1262. var nbytes = buf.byteLength;
  1263. for (var i=0; i<nbytes; i++) {
  1264. bytes += String.fromCharCode(buf[i]);
  1265. }
  1266. filedata = btoa(bytes);
  1267. format = 'base64';
  1268. }
  1269. var model = { name: filename, path: path };
  1270. var name_and_ext = utils.splitext(filename);
  1271. var file_ext = name_and_ext[1];
  1272. var content_type;
  1273. // Treat everything as generic file
  1274. model.type = 'file';
  1275. model.format = format;
  1276. content_type = 'application/octet-stream';
  1277. model.chunk = chunk;
  1278. model.content = filedata;
  1279. var on_success = function () {
  1280. if (offset < f.size) {
  1281. // of to the next chunk
  1282. chunk_reader(offset, f);
  1283. // change progress bar and progress button
  1284. var progress = offset / f.size * 100;
  1285. progress = progress > 100 ? 100 : progress;
  1286. uploading_button.text(progress.toFixed(0)+'%');
  1287. progress_bar.css('width', progress+'%')
  1288. .attr('aria-valuenow', progress.toString());
  1289. } else {
  1290. item.removeClass('new-file');
  1291. that.add_link(model, item);
  1292. that.session_list.load_sessions();
  1293. }
  1294. };
  1295. that.contents.save(path, model).then(on_success, on_error);
  1296. };
  1297. // now let's start the read with the first block
  1298. chunk_reader(offset, f);
  1299. };
  1300. item.find('.item_buttons')
  1301. .append(progress_bar);
  1302. parse_large_file(f, item);
  1303. };
  1304. if (exists) {
  1305. dialog.modal({
  1306. title : "Replace file",
  1307. body : 'There is already a file named ' + filename + ', do you want to replace it?',
  1308. default_button: "Cancel",
  1309. buttons : {
  1310. Overwrite : {
  1311. class: "btn-danger",
  1312. click: function () {
  1313. add_uploading_button(file, item);
  1314. }
  1315. },
  1316. Cancel : {
  1317. click: function() { item.remove(); }
  1318. }
  1319. }
  1320. });
  1321. } else {
  1322. add_uploading_button(file, item);
  1323. }
  1324. return false;
  1325. });
  1326. item.find(".item_buttons").empty()
  1327. .append(upload_button)
  1328. .append(cancel_button);
  1329. };
  1330. NotebookList.prototype.add_upload_button = function (item) {
  1331. var that = this;
  1332. var upload_button = $('<button/>').text(i18n.msg._("Upload"))
  1333. .addClass('btn btn-primary btn-xs upload_button')
  1334. .click(function (e) {
  1335. var filename = item.find('.item_name > input').val();
  1336. var path = utils.url_path_join(that.notebook_path, filename);
  1337. var filedata = item.data('filedata');
  1338. var format = 'text';
  1339. if (filename.length === 0 || filename[0] === '.') {
  1340. dialog.modal({
  1341. title : i18n.msg._('Invalid file name'),
  1342. body : i18n.msg._("File names must be at least one character and not start with a period"),
  1343. buttons : {'OK' : { 'class' : 'btn-primary' }}
  1344. });
  1345. return false;
  1346. }
  1347. if (filedata instanceof ArrayBuffer) {
  1348. // base64-encode binary file data
  1349. var bytes = '';
  1350. var buf = new Uint8Array(filedata);
  1351. var nbytes = buf.byteLength;
  1352. for (var i=0; i<nbytes; i++) {
  1353. bytes += String.fromCharCode(buf[i]);
  1354. }
  1355. filedata = btoa(bytes);
  1356. format = 'base64';
  1357. }
  1358. var model = { name: filename, path: path };
  1359. var name_and_ext = utils.splitext(filename);
  1360. var file_ext = name_and_ext[1];
  1361. var content_type;
  1362. if (file_ext === '.ipynb') {
  1363. model.type = 'notebook';
  1364. model.format = 'json';
  1365. try {
  1366. model.content = JSON.parse(filedata);
  1367. } catch (e) {
  1368. var failbody = i18n.msg._("The error was: %s");
  1369. dialog.modal({
  1370. title : i18n.msg._('Cannot upload invalid Notebook'),
  1371. body : i18n.msg.sprintf(failbody,e),
  1372. buttons : {'OK' : {
  1373. 'class' : 'btn-primary',
  1374. click: function () {
  1375. item.remove();
  1376. }
  1377. }}
  1378. });
  1379. console.warn('Error during notebook uploading', e);
  1380. return false;
  1381. }
  1382. content_type = 'application/json';
  1383. } else {
  1384. model.type = 'file';
  1385. model.format = format;
  1386. model.content = filedata;
  1387. content_type = 'application/octet-stream';
  1388. }
  1389. var on_success = function () {
  1390. item.removeClass('new-file');
  1391. that.add_link(model, item);
  1392. that.session_list.load_sessions();
  1393. };
  1394. var exists = false;
  1395. $.each(that.element.find('.list_item:not(.new-file)'), function(k,v){
  1396. if ($(v).data('name') === filename) { exists = true; return false; }
  1397. });
  1398. if (exists) {
  1399. var body = i18n.msg._("There is already a file named \"%s\". Do you want to replace it?");
  1400. dialog.modal({
  1401. title : i18n.msg._("Replace file"),
  1402. body : i18n.msg.sprintf(body,filename),
  1403. default_button: "Cancel",
  1404. buttons : {
  1405. Cancel : {
  1406. click: function() { item.remove(); }
  1407. },
  1408. Overwrite : {
  1409. class: "btn-danger",
  1410. click: function () {
  1411. that.contents.save(path, model).then(on_success);
  1412. }
  1413. }
  1414. }
  1415. });
  1416. } else {
  1417. that.contents.save(path, model).then(on_success);
  1418. }
  1419. return false;
  1420. });
  1421. var cancel_button = $('<button/>').text(i18n.msg._("Cancel"))
  1422. .addClass("btn btn-default btn-xs")
  1423. .click(function (e) {
  1424. item.remove();
  1425. return false;
  1426. });
  1427. item.find(".item_buttons").empty()
  1428. .append(upload_button)
  1429. .append(cancel_button);
  1430. };
  1431. return {'NotebookList': NotebookList};
  1432. });