TripalUploader.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647
  1. /**
  2. * @file
  3. * TripalUploader Object
  4. *
  5. * To use the TripalUploader Object the following must be performed:
  6. *
  7. * 1) Add a Drupal form to your code that contains the following:
  8. * * A Drupal-style table with 4 or 8 columns. See the initialize
  9. * function in this class for a description of the columns.
  10. * * A button for submitting a file for upload.
  11. *
  12. * @code
  13. * $headers = array(
  14. * array('data' => 'Sequence File'),
  15. * array('data' => 'Size', 'width' => '10%'),
  16. * array('data' => 'Upload Progress', 'width' => '20%'),
  17. * array('data' => 'Action', 'width' => '10%')
  18. * );
  19. * $rows = array();
  20. * $table_vars = array(
  21. * 'header' => $headers,
  22. * 'rows' => $rows,
  23. * 'attributes' => array('id' => 'sequence-file-upload-table'),
  24. * 'sticky' => TRUE,
  25. * 'colgroups' => array(),
  26. * 'empty' => t('There are currently no files added.'),
  27. * );
  28. * $form['upload']['sequence_file'] = array(
  29. * '#markup' => theme('table', $table_vars)
  30. * );
  31. * $form['upload']['sequence_fid'] = array(
  32. * '#type' => 'hidden',
  33. * '#value' => 0,
  34. * '#attributes' => array('id' => 'sequence-fid')
  35. * );
  36. * $form['upload']['sequence_file_submit'] = array(
  37. * '#type' => 'submit',
  38. * '#value' => 'Upload Sequence File',
  39. * '#name' => 'sequence_file_submit',
  40. * // We don't want this button to submit as the file upload
  41. * // is handled by the JavaScript code.
  42. * '#attributes' => array('onclick' => 'return (false);')
  43. * );
  44. * @endcode
  45. *
  46. *
  47. * 2) Edit the theme/js/[module_name].js and in the "Drupal.behaviors.[module]"
  48. * section add a JQuery show function to the form that converts the table
  49. * created in the Drupal form to a TripalUploader table. The 'table_id' must be
  50. * the same as the 'id' attribute set for the table in the Drupal code above.
  51. * The 'submit_id' must be the id of the upload button added in the Drupal
  52. * code above. The 'category' for the files. This is the category that
  53. * will be saved in Tripal for the file. See the addUploadTable function
  54. * for additional options. Include a 'cardinality' setting to indicate
  55. * the number of allowed files per upload, and set the 'target_id' to the
  56. * name of the field that will contain the file ID (fid) after uploading.
  57. *
  58. * @code
  59. * // The TripalUploader object used for uploading of files using the
  60. * // HTML5 File API. Large files are uploaded as chunks and a progress
  61. * // bar is provided.
  62. * var uploader = new TripalUploader();
  63. *
  64. * $('#tripal-sequences-panel-form').show(function() {
  65. * uploader.addUploadTable('sequence_file', {
  66. * 'table_id' : '#sequence-file-upload-table',
  67. * 'submit_id': '#edit-sequence-file-submit',
  68. * 'category' : ['sequence_file'],
  69. * 'cardinality' : 1,
  70. * 'target_id' : 'sequence-fid',
  71. * });
  72. * });
  73. * @endcode
  74. *
  75. *
  76. * 3) Files are uploaded automatically to Tripal. Files are saved in the
  77. * Tripal user's directory. You can retreive information about the
  78. * file by querying for the file category for the current project.
  79. *
  80. * @code
  81. * $seq_files = TripalFeature::getFilesByTypes($user->uid, array('sequence_file'), $project_id);
  82. * @endcode
  83. *
  84. * 4) If the 'target_id' was used in array for step #2 above, then the
  85. * file ID can be retrieved in the hook_validate() and hook_submit() functions
  86. * via the $form_state['input'] array (not the $form_state['values'] array.
  87. */
  88. (function($) {
  89. "use strict";
  90. /**
  91. * The constructor function.
  92. */
  93. var TripalUploader = function() {
  94. // Holds the list of files and organizes them by category and then
  95. // by an index number.
  96. this.files = {};
  97. // The tables array will have the following keys:
  98. //
  99. // tname: the name of the HTML table containing the file.
  100. // category: the category within the table to which the file belongs.
  101. // index: the index of the file in the table.
  102. // url: The URL at the remote server where the file will uploaded.
  103. this.tables = {};
  104. /**
  105. * Adds a file to the TripalUploader object
  106. *
  107. * @param file
  108. * The HTML5 file object.
  109. * @param options
  110. * A set of key value pairs of the following
  111. * - tname: the name of the HTML table containing the file.
  112. * - category: the category within the table to which the file belongs.
  113. * - index: the index of the file in the table.
  114. * - url: The URL at the remote server where the file will uploaded.
  115. */
  116. this.addFile = function(file, options) {
  117. var tname = options['tname'];
  118. var category = options['category'];
  119. var i = options['i'];
  120. var url = options['url'];
  121. var self = this;
  122. // Make sure the file type is allowed. If there are no file types
  123. // then anything is allowed.
  124. if (this.tables[tname]['allowed_types'] && this.tables[tname]['allowed_types'].length > 0) {
  125. var allowed_types = this.tables[tname]['allowed_types'];
  126. var matches = file.name.match(/^.*\.(.+)$/);
  127. if (!matches) {
  128. alert('Please provide a file with a valid extension.');
  129. return null;
  130. }
  131. var type = matches[1];
  132. var j;
  133. var found = false;
  134. for (j = 0; j < allowed_types.length; j++) {
  135. if (allowed_types[j] == type) {
  136. found = true;
  137. }
  138. }
  139. if (!found) {
  140. alert('Please provide a file with a valid extension. The following are allowed: ' + allowed_types.join(','));
  141. return null;
  142. }
  143. }
  144. if (!(category in this.files)) {
  145. this.files[category] = {}
  146. }
  147. var options = {
  148. 'parent' : self,
  149. 'index' : i,
  150. 'url' : url,
  151. 'category' : category,
  152. 'tname' : tname,
  153. 'progress' : category + '-progress-' + i,
  154. 'links' : category + '-links-' + i,
  155. 'module' : this.tables[tname]['module']
  156. }
  157. var guf = new TripalUploadFile(file, options)
  158. this.files[category][i] = guf;
  159. return guf
  160. };
  161. /**
  162. *
  163. */
  164. this.removeFile = function(tname, category, i) {
  165. if (category in this.files) {
  166. if (i in this.files[category]) {
  167. delete this.files[category][i];
  168. }
  169. }
  170. this.setTarget(tname);
  171. }
  172. /**
  173. *
  174. */
  175. this.getMaxIndex = function(category) {
  176. var index = 0;
  177. if (category in this.files) {
  178. for (var i in this.files[category]) {
  179. if (i > index) {
  180. index = i;
  181. }
  182. }
  183. }
  184. return index;
  185. }
  186. /**
  187. *
  188. */
  189. this.getNumFiles = function(category) {
  190. var count = 0;
  191. if (category in this.files) {
  192. for (var i in this.files[category]) {
  193. count = count + 1;
  194. }
  195. }
  196. return count;
  197. }
  198. /**
  199. *
  200. */
  201. this.getCategoryFiles = function(category) {
  202. if (!(category in this.files)) {
  203. return [];
  204. }
  205. return this.files[category];
  206. };
  207. /**
  208. *
  209. */
  210. this.cancelFile = function(category, i) {
  211. if (category in this.files) {
  212. this.files[category][i].cancel();
  213. }
  214. };
  215. /**
  216. *
  217. */
  218. this.start = function(category) {
  219. if (category in this.files) {
  220. for (var i in this.files[category]) {
  221. this.files[category][i].start();
  222. }
  223. }
  224. };
  225. /**
  226. *
  227. */
  228. this.updateProgress = function(category) {
  229. if (category in this.files) {
  230. for (var i in this.files[category]) {
  231. this.files[category][i].updateStatus();
  232. }
  233. }
  234. };
  235. /**
  236. *
  237. */
  238. this.reset = function(category) {
  239. if (category in this.files) {
  240. for (i in this.files[category]) {
  241. this.files[category][i].cancel();
  242. }
  243. this.files[category] = [];
  244. }
  245. }
  246. /**
  247. *
  248. */
  249. this.getFileButton = function(tname, category, i) {
  250. var button_name = tname + '--' + category + '-upload-' + i;
  251. var element = '<input id="' + button_name + '" class="tripal-chunked-file-upload" type="file" ready="false">';
  252. return {
  253. 'name' : button_name,
  254. 'element' : element,
  255. }
  256. }
  257. /**
  258. *
  259. */
  260. this.parseButtonID = function(id) {
  261. // Get the category and index for this file.
  262. var tname = id.replace(/^(.+)--(.+)-upload-(.+)$/, '$1');
  263. var category = id.replace(/^(.+)--(.+)-upload-(.+)$/, '$2');
  264. var index = id.replace(/^(.+)--(.+)-upload-(.+)$/, '$3');
  265. return {
  266. 'tname' : tname,
  267. 'category' : category,
  268. 'index' : index
  269. };
  270. }
  271. /**
  272. * Initializes the loader for a given HTML table.
  273. *
  274. * The TripalUploader supports two types of tables, a table for
  275. * uploading paired data (e.g. RNA-seq) and single files. This function
  276. * replaces the body of an existing table as new files and updates
  277. * the table as files are uploaded.
  278. *
  279. * @param tname
  280. * The name of the table. For single files it is best to name the
  281. * table the same as the file category. For paired data it is best
  282. * to use a name that represents both categoires.
  283. * @param options
  284. * An associative array that contains the following keys:
  285. * table_id: The HTML id of the table. For single data, the table
  286. * must already have 4 columns with headers (file name,
  287. * size, progress and action). For paired data, the table
  288. * must already have 8 columns, which are the same as the
  289. * single table but with two sets.
  290. * category: An array. It must contain the list of categories that
  291. * this table manages. For paired data include two categories.
  292. * This is the category of the file when saved in Tripal.
  293. * submit_id: The HTML id of the submit button.
  294. * module: The name of the module managing the table.
  295. * cardinatily: (optional) The number of files allowed. Set to 0 for
  296. * unlimited. Defalt is 0.
  297. * target_id: (optional). The HTML id of the hidden field in the form
  298. * where the file ID will be written to this field. This only
  299. * works if cardinality is set to 1.
  300. * allowed_types: (optional). An array of allowed file extensions (e.g.
  301. * fasta, fastq, fna, gff3, etc.).
  302. */
  303. this.addUploadTable = function(tname, options) {
  304. var table_id = options['table_id'];
  305. var categories = options['category'];
  306. var submit_id = options['submit_id'];
  307. var target_id = options['target_id'];
  308. var cardinality = options['cardinality'];
  309. var module = options['module'];
  310. var allowed_types = options['allowed_types'];
  311. // Save the table ID for this category
  312. if (!(tname in this.tables)) {
  313. this.tables[tname] = {};
  314. }
  315. this.tables[tname]['table_id'] = table_id;
  316. this.tables[tname]['category'] = categories;
  317. this.tables[tname]['submit_id'] = submit_id;
  318. this.tables[tname]['target_id'] = target_id;
  319. this.tables[tname]['cardinality'] = cardinality;
  320. this.tables[tname]['module'] = module;
  321. this.tables[tname]['allowed_types'] = allowed_types;
  322. this.updateTable(categories[0]);
  323. this.enableSubmit(submit_id);
  324. }
  325. /**
  326. * Adds a click event to the submit button that starts the upload.
  327. */
  328. this.enableSubmit = function(submit_id) {
  329. var self = this;
  330. var categories = [];
  331. // Iterate through all of the tables that use this submit button
  332. // and collect all the categories. We want to update them all.
  333. for (var tname in this.tables) {
  334. if (this.tables[tname]['submit_id'] == submit_id){
  335. for (var i = 0; i < this.tables[tname]['category'].length; i++) {
  336. categories.push(this.tables[tname]['category'][i])
  337. }
  338. }
  339. }
  340. var func_name = ($.isFunction($.fn.live)) ? 'live' : 'on';
  341. $(submit_id)[func_name]('click', function() {
  342. for(var i = 0; i < categories.length; i++) {
  343. self.start(categories[i]);
  344. }
  345. });
  346. }
  347. /**
  348. * Updates the table for the given file category.
  349. */
  350. this.updateTable = function(category) {
  351. // Iterate through all of the tables that are managed by this object.
  352. for (var tname in this.tables) {
  353. // Iterate through all of the categories on each table.
  354. for (var i = 0; i < this.tables[tname]['category'].length; i++) {
  355. // If the category of the table matches then update it.
  356. if (this.tables[tname]['category'][i] == category) {
  357. // For single files:
  358. if (this.tables[tname]['category'].length == 1) {
  359. var cat = this.tables[tname]['category'][0];
  360. this.updateSingleTable(tname, cat);
  361. this.updateProgress(cat);
  362. return;
  363. }
  364. // For paired (e.g. RNA-seq) files:
  365. if (this.tables[tname]['category'].length == 2) {
  366. var categories = this.tables[tname]['category'];
  367. this.updatePairedTable(tname, categories);
  368. this.updateProgress(categories[0]);
  369. this.updateProgress(categories[1]);
  370. return;
  371. }
  372. }
  373. }
  374. }
  375. }
  376. /**
  377. * A table for non-paired single data.
  378. */
  379. this.updateSingleTable = function(tname, category) {
  380. var i = 0;
  381. var content = '';
  382. var files = this.getCategoryFiles(category);
  383. var max_index = this.getMaxIndex(category);
  384. var has_file = false;
  385. var table_id = this.tables[tname]['table_id'];
  386. var cardinality = this.tables[tname]['cardinality'];
  387. var target_id = this.tables[tname]['target_id'];
  388. var num_files = this.getNumFiles(category);
  389. var button = null;
  390. // Build the rows for the non paired samples.
  391. has_file = false;
  392. for (i = 0; i <= max_index; i++) {
  393. button = this.getFileButton(tname, category, i);
  394. var trclass = 'odd';
  395. if (i % 2 == 0) {
  396. trclass = 'even';
  397. }
  398. content += '<tr class="' + trclass + '">';
  399. if (i in files) {
  400. content += '<td>' + files[i].file.name + '</td>';
  401. content += '<td>' + files[i].getFileSize(true) + '</td>';
  402. content += '<td>' + files[i].getProgressBar() + '</td>';
  403. content += '<td>' + files[i].getLinks() + '</td>';
  404. content += '</tr>';
  405. has_file = true;
  406. }
  407. else {
  408. content += '<td colspan="4">' + button['element'] + '</td>';
  409. }
  410. content += '</tr>';
  411. }
  412. // Create an empty row with a file button.
  413. if (has_file) {
  414. // Only add a new row if we haven't reached our cardinality limit.
  415. if (!cardinality || cardinality == 0 || cardinality < num_files) {
  416. button = this.getFileButton(tname, category, i);
  417. content += '<tr><td colspan="4">' + button['element'] + '</td></tr>';
  418. }
  419. }
  420. // Add the body of the table to the table with the provided table_id.
  421. $(table_id + ' > tbody').html(content);
  422. if (button) {
  423. this.enableFileButton(button['name']);
  424. }
  425. }
  426. /**
  427. * Sets the table's target field with the file id.
  428. *
  429. * @param $file_id
  430. * The Tripal file_id
  431. * @param $tname
  432. * The name of the HTML table where the file is kept.
  433. * @param $category
  434. * The name of the category to which the file belongs.
  435. */
  436. this.setTarget = function(tname) {
  437. var categories = this.tables[tname]['category'];
  438. var num_categories = categories.length;
  439. var cardinality = this.tables[tname]['cardinality'];
  440. var target_id = this.tables[tname]['target_id'];
  441. if (target_id) {
  442. var fids = '';
  443. var c;
  444. // Iterate through the file categories.
  445. for (c = 0; c < num_categories; c++) {
  446. var files = this.getCategoryFiles(categories[c]);
  447. var num_files = this.getNumFiles(categories[c]);
  448. var i;
  449. // Deal with one category.
  450. if (num_categories == 1) {
  451. if (num_files > 0) {
  452. // Always set the first file_id.
  453. fids = files[0].file_id;
  454. }
  455. }
  456. // Deal with multiple categories.
  457. else {
  458. // When we have more than one category then we need to
  459. // separate the categories with a comma. So, this must happen
  460. // after every category except the first.
  461. if (c == 0) {
  462. if (num_files > 0) {
  463. fids = fids + files[0].file_id;
  464. }
  465. }
  466. else {
  467. fids = fids + ',';
  468. if (num_files > 0) {
  469. fids = fids + files[0].file_id;
  470. }
  471. }
  472. }
  473. // Iterate through any other files and add them with a '|' delemiter.
  474. for (i = 1; i < num_files; i++) {
  475. fids = fids + "|" + files[i].file_id;
  476. }
  477. $('#' + target_id).val(fids);
  478. }
  479. }
  480. }
  481. /**
  482. * A table for paired data (e.g. RNA-seq).
  483. */
  484. this.updatePairedTable = function(tname, categories) {
  485. var i = 0;
  486. var table_id = this.tables[tname]['table_id'];
  487. var cardinality = this.tables[tname]['cardinality'];
  488. var category1 = categories[0];
  489. var category2 = categories[1];
  490. var paired_content = '';
  491. var category1_files = this.getCategoryFiles(category1);
  492. var category2_files = this.getCategoryFiles(category2);
  493. var max_paired1 = this.getMaxIndex(category1);
  494. var max_paired2 = this.getMaxIndex(category2);
  495. var buttons = []
  496. var button1 = null;
  497. var button2 = null;
  498. // Build the rows for the paired sample files table.
  499. var has_file = false;
  500. for (i = 0; i <= Math.max(max_paired1, max_paired2); i++) {
  501. button1 = this.getFileButton(tname, category1, i);
  502. button2 = this.getFileButton(tname, category2, i);
  503. var trclass = 'odd';
  504. if (i % 2 == 0) {
  505. trclass = 'even';
  506. }
  507. paired_content += '<tr class="' + trclass + '">';
  508. if (i in category1_files) {
  509. paired_content += '<td>' + category1_files[i].getFileName() + '</td>';
  510. paired_content += '<td>' + category1_files[i].getFileSize(true) + '</td>';
  511. paired_content += '<td>' + category1_files[i].getProgressBar() + '</td>';
  512. paired_content += '<td>' + category1_files[i].getLinks() + '</td>';
  513. has_file = true;
  514. }
  515. else {
  516. paired_content += '<td colspan="4">' + button1['element'] + '</td>';
  517. buttons.push(button1);
  518. }
  519. if (i in category2_files) {
  520. paired_content += '<td>' + category2_files[i].getFileName() + '</td>';
  521. paired_content += '<td>' + category2_files[i].getFileSize(true) + '</td>';
  522. paired_content += '<td>' + category2_files[i].getProgressBar() + '</td>';
  523. paired_content += '<td nowrap>' + category2_files[i].getLinks() + '</td>';
  524. has_file = true;
  525. }
  526. else {
  527. paired_content += '<td colspan="4">' + button2['element'] + '</td>';
  528. buttons.push(button2);
  529. }
  530. paired_content += '</tr>';
  531. }
  532. // Create a new empty row of buttons if we have files.
  533. if (has_file) {
  534. // Only add a new row if we haven't reached our cardinality limit.
  535. if (!cardinality || cardinality == 0 || cardinality < max_paired1) {
  536. button1 = this.getFileButton(tname, category1, i);
  537. button2 = this.getFileButton(tname, category2, i);
  538. buttons.push(button1);
  539. buttons.push(button2);
  540. paired_content += '<tr class="odd"><td colspan="4">' + button1['element'] +
  541. '</td><td colspan="4">' + button2['element'] + '</td></tr>'
  542. }
  543. }
  544. $(table_id + ' > tbody').html(paired_content);
  545. for (i = 0; i < buttons.length; i++) {
  546. this.enableFileButton(buttons[i]['name']);
  547. }
  548. }
  549. /**
  550. * Adds a function to the change event for the file button that
  551. * causes a new file to be added to this object which it is clicked.
  552. * The button is added by the updateUploadTable
  553. */
  554. this.enableFileButton = function(button_name) {
  555. // If the button already exists then it's already setup so just
  556. // return.
  557. if($('#' + button_name).attr('ready') == 'true') {
  558. return;
  559. }
  560. // When the button provided by the TripalUploader class is clicked
  561. // then we need to add the files to the object. We must have this
  562. // function so that we can set the proper URL
  563. var self = this;
  564. var func_name = ($.isFunction($.fn.live)) ? 'live' : 'on';
  565. $('#' + button_name)[func_name]('change', function(e) {
  566. var id = this.id;
  567. // Get the HTML5 list of files to upload.
  568. var hfiles = e.target.files;
  569. // Let the TripalUploader object parse the button ID to give us
  570. // the proper category name and index.
  571. var button = self.parseButtonID(id);
  572. var tname = button['tname'];
  573. var category = button['category'];
  574. var index = button['index'];
  575. // Add the file(s) to the uploader object.
  576. for (var i = 0; i < hfiles.length; i++) {
  577. var f = hfiles[i];
  578. var options = {
  579. // Files are managed by tables.
  580. 'tname' : tname,
  581. // Files can be categorized to seprate them from other files.
  582. 'category': category,
  583. // The index is the numeric index of the file. Files are ordered
  584. // by their index. The file with an index of 0 is always ordered first.
  585. 'i': index,
  586. // The URL at the remote server where the file will uploaded.
  587. 'url' : baseurl + '/tripal/upload/' + category,
  588. };
  589. self.addFile(f, options);
  590. // We need to update the upload table and the progress. The
  591. // information for which table to update is in the self.tables
  592. // array.
  593. self.updateTable(category);
  594. }
  595. });
  596. $('#' + button_name).attr('ready', 'true');
  597. }
  598. };
  599. // Export the objects to the window for use in other JS files.
  600. window.TripalUploader = TripalUploader;
  601. })(jQuery);