You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2735 lines
78 KiB

11 years ago
11 years ago
11 years ago
12 years ago
11 years ago
13 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
  1. /*
  2. * Copyright (c) 2014
  3. *
  4. * This file is licensed under the Affero General Public License version 3
  5. * or later.
  6. *
  7. * See the COPYING-README file.
  8. *
  9. */
  10. (function() {
  11. var TEMPLATE_ADDBUTTON = '<a href="#" class="button new"><img src="{{iconUrl}}" alt="{{addText}}"></img></a>';
  12. /**
  13. * @class OCA.Files.FileList
  14. * @classdesc
  15. *
  16. * The FileList class manages a file list view.
  17. * A file list view consists of a controls bar and
  18. * a file list table.
  19. *
  20. * @param $el container element with existing markup for the #controls
  21. * and a table
  22. * @param [options] map of options, see other parameters
  23. * @param [options.scrollContainer] scrollable container, defaults to $(window)
  24. * @param [options.dragOptions] drag options, disabled by default
  25. * @param [options.folderDropOptions] folder drop options, disabled by default
  26. * @param [options.detailsViewEnabled=true] whether to enable details view
  27. */
  28. var FileList = function($el, options) {
  29. this.initialize($el, options);
  30. };
  31. /**
  32. * @memberof OCA.Files
  33. */
  34. FileList.prototype = {
  35. SORT_INDICATOR_ASC_CLASS: 'icon-triangle-n',
  36. SORT_INDICATOR_DESC_CLASS: 'icon-triangle-s',
  37. id: 'files',
  38. appName: t('files', 'Files'),
  39. isEmpty: true,
  40. useUndo:true,
  41. /**
  42. * Top-level container with controls and file list
  43. */
  44. $el: null,
  45. /**
  46. * Files table
  47. */
  48. $table: null,
  49. /**
  50. * List of rows (table tbody)
  51. */
  52. $fileList: null,
  53. /**
  54. * @type OCA.Files.BreadCrumb
  55. */
  56. breadcrumb: null,
  57. /**
  58. * @type OCA.Files.FileSummary
  59. */
  60. fileSummary: null,
  61. /**
  62. * @type OCA.Files.DetailsView
  63. */
  64. _detailsView: null,
  65. /**
  66. * Whether the file list was initialized already.
  67. * @type boolean
  68. */
  69. initialized: false,
  70. /**
  71. * Number of files per page
  72. *
  73. * @return {int} page size
  74. */
  75. pageSize: function() {
  76. return Math.ceil(this.$container.height() / 50);
  77. },
  78. /**
  79. * Array of files in the current folder.
  80. * The entries are of file data.
  81. *
  82. * @type Array.<Object>
  83. */
  84. files: [],
  85. /**
  86. * File actions handler, defaults to OCA.Files.FileActions
  87. * @type OCA.Files.FileActions
  88. */
  89. fileActions: null,
  90. /**
  91. * Whether selection is allowed, checkboxes and selection overlay will
  92. * be rendered
  93. */
  94. _allowSelection: true,
  95. /**
  96. * Map of file id to file data
  97. * @type Object.<int, Object>
  98. */
  99. _selectedFiles: {},
  100. /**
  101. * Summary of selected files.
  102. * @type OCA.Files.FileSummary
  103. */
  104. _selectionSummary: null,
  105. /**
  106. * If not empty, only files containing this string will be shown
  107. * @type String
  108. */
  109. _filter: '',
  110. /**
  111. * Sort attribute
  112. * @type String
  113. */
  114. _sort: 'name',
  115. /**
  116. * Sort direction: 'asc' or 'desc'
  117. * @type String
  118. */
  119. _sortDirection: 'asc',
  120. /**
  121. * Sort comparator function for the current sort
  122. * @type Function
  123. */
  124. _sortComparator: null,
  125. /**
  126. * Whether to do a client side sort.
  127. * When false, clicking on a table header will call reload().
  128. * When true, clicking on a table header will simply resort the list.
  129. */
  130. _clientSideSort: false,
  131. /**
  132. * Current directory
  133. * @type String
  134. */
  135. _currentDirectory: null,
  136. _dragOptions: null,
  137. _folderDropOptions: null,
  138. /**
  139. * Initialize the file list and its components
  140. *
  141. * @param $el container element with existing markup for the #controls
  142. * and a table
  143. * @param options map of options, see other parameters
  144. * @param options.scrollContainer scrollable container, defaults to $(window)
  145. * @param options.dragOptions drag options, disabled by default
  146. * @param options.folderDropOptions folder drop options, disabled by default
  147. * @param options.scrollTo name of file to scroll to after the first load
  148. * @private
  149. */
  150. initialize: function($el, options) {
  151. var self = this;
  152. options = options || {};
  153. if (this.initialized) {
  154. return;
  155. }
  156. if (options.dragOptions) {
  157. this._dragOptions = options.dragOptions;
  158. }
  159. if (options.folderDropOptions) {
  160. this._folderDropOptions = options.folderDropOptions;
  161. }
  162. this.$el = $el;
  163. if (options.id) {
  164. this.id = options.id;
  165. }
  166. this.$container = options.scrollContainer || $(window);
  167. this.$table = $el.find('table:first');
  168. this.$fileList = $el.find('#fileList');
  169. if (_.isUndefined(options.detailsViewEnabled) || options.detailsViewEnabled) {
  170. this._detailsView = new OCA.Files.DetailsView();
  171. this._detailsView.$el.insertBefore(this.$el);
  172. this._detailsView.$el.addClass('disappear');
  173. }
  174. this._initFileActions(options.fileActions);
  175. if (this._detailsView) {
  176. this._detailsView.addDetailView(new OCA.Files.MainFileInfoDetailView({fileList: this, fileActions: this.fileActions}));
  177. }
  178. this.files = [];
  179. this._selectedFiles = {};
  180. this._selectionSummary = new OCA.Files.FileSummary();
  181. this.fileSummary = this._createSummary();
  182. this.setSort('name', 'asc');
  183. var breadcrumbOptions = {
  184. onClick: _.bind(this._onClickBreadCrumb, this),
  185. getCrumbUrl: function(part) {
  186. return self.linkTo(part.dir);
  187. }
  188. };
  189. // if dropping on folders is allowed, then also allow on breadcrumbs
  190. if (this._folderDropOptions) {
  191. breadcrumbOptions.onDrop = _.bind(this._onDropOnBreadCrumb, this);
  192. }
  193. this.breadcrumb = new OCA.Files.BreadCrumb(breadcrumbOptions);
  194. var $controls = this.$el.find('#controls');
  195. if ($controls.length > 0) {
  196. $controls.prepend(this.breadcrumb.$el);
  197. this.$table.addClass('has-controls');
  198. }
  199. this._renderNewButton();
  200. this.$el.find('thead th .columntitle').click(_.bind(this._onClickHeader, this));
  201. this._onResize = _.debounce(_.bind(this._onResize, this), 100);
  202. $('#app-content').on('appresized', this._onResize);
  203. $(window).resize(this._onResize);
  204. this.$el.on('show', this._onResize);
  205. this.updateSearch();
  206. this.$fileList.on('click','td.filename>a.name', _.bind(this._onClickFile, this));
  207. this.$fileList.on('change', 'td.filename>.selectCheckBox', _.bind(this._onClickFileCheckbox, this));
  208. this.$el.on('urlChanged', _.bind(this._onUrlChanged, this));
  209. this.$el.find('.select-all').click(_.bind(this._onClickSelectAll, this));
  210. this.$el.find('.download').click(_.bind(this._onClickDownloadSelected, this));
  211. this.$el.find('.delete-selected').click(_.bind(this._onClickDeleteSelected, this));
  212. this.$el.find('.selectedActions a').tooltip({placement:'top'});
  213. this.setupUploadEvents();
  214. this.$container.on('scroll', _.bind(this._onScroll, this));
  215. if (options.scrollTo) {
  216. this.$fileList.one('updated', function() {
  217. self.scrollTo(options.scrollTo);
  218. });
  219. }
  220. OC.Plugins.attach('OCA.Files.FileList', this);
  221. },
  222. /**
  223. * Destroy / uninitialize this instance.
  224. */
  225. destroy: function() {
  226. if (this._newFileMenu) {
  227. this._newFileMenu.remove();
  228. }
  229. if (this._newButton) {
  230. this._newButton.remove();
  231. }
  232. if (this._detailsView) {
  233. this._detailsView.remove();
  234. }
  235. // TODO: also unregister other event handlers
  236. this.fileActions.off('registerAction', this._onFileActionsUpdated);
  237. this.fileActions.off('setDefault', this._onFileActionsUpdated);
  238. OC.Plugins.detach('OCA.Files.FileList', this);
  239. $('#app-content').off('appresized', this._onResize);
  240. },
  241. /**
  242. * Initializes the file actions, set up listeners.
  243. *
  244. * @param {OCA.Files.FileActions} fileActions file actions
  245. */
  246. _initFileActions: function(fileActions) {
  247. var self = this;
  248. this.fileActions = fileActions;
  249. if (!this.fileActions) {
  250. this.fileActions = new OCA.Files.FileActions();
  251. this.fileActions.registerDefaultActions();
  252. }
  253. if (this._detailsView) {
  254. this.fileActions.registerAction({
  255. name: 'Details',
  256. displayName: t('files', 'Details'),
  257. mime: 'all',
  258. order: -50,
  259. icon: OC.imagePath('core', 'actions/details'),
  260. permissions: OC.PERMISSION_READ,
  261. actionHandler: function(fileName, context) {
  262. self._updateDetailsView(fileName);
  263. }
  264. });
  265. }
  266. this._onFileActionsUpdated = _.debounce(_.bind(this._onFileActionsUpdated, this), 100);
  267. this.fileActions.on('registerAction', this._onFileActionsUpdated);
  268. this.fileActions.on('setDefault', this._onFileActionsUpdated);
  269. },
  270. /**
  271. * Returns a unique model for the given file name.
  272. *
  273. * @param {string|object} fileName file name or jquery row
  274. * @return {OCA.Files.FileInfoModel} file info model
  275. */
  276. getModelForFile: function(fileName) {
  277. var self = this;
  278. var $tr;
  279. // jQuery object ?
  280. if (fileName.is) {
  281. $tr = fileName;
  282. fileName = $tr.attr('data-file');
  283. } else {
  284. $tr = this.findFileEl(fileName);
  285. }
  286. if (!$tr || !$tr.length) {
  287. return null;
  288. }
  289. // if requesting the selected model, return it
  290. if (this._currentFileModel && this._currentFileModel.get('name') === fileName) {
  291. return this._currentFileModel;
  292. }
  293. // TODO: note, this is a temporary model required for synchronising
  294. // state between different views.
  295. // In the future the FileList should work with Backbone.Collection
  296. // and contain existing models that can be used.
  297. // This method would in the future simply retrieve the matching model from the collection.
  298. var model = new OCA.Files.FileInfoModel(this.elementToFile($tr));
  299. if (!model.get('path')) {
  300. model.set('path', this.getCurrentDirectory(), {silent: true});
  301. }
  302. model.on('change', function(model) {
  303. // re-render row
  304. var highlightState = $tr.hasClass('highlighted');
  305. $tr = self.updateRow(
  306. $tr,
  307. _.extend({isPreviewAvailable: true}, model.toJSON()),
  308. {updateSummary: true, silent: false, animate: true}
  309. );
  310. $tr.toggleClass('highlighted', highlightState);
  311. });
  312. model.on('busy', function(model, state) {
  313. self.showFileBusyState($tr, state);
  314. });
  315. return model;
  316. },
  317. /**
  318. * Displays the details view for the given file and
  319. * selects the given tab
  320. *
  321. * @param {string} fileName file name for which to show details
  322. * @param {string} [tabId] optional tab id to select
  323. */
  324. showDetailsView: function(fileName, tabId) {
  325. this._updateDetailsView(fileName);
  326. if (tabId) {
  327. this._detailsView.selectTab(tabId);
  328. }
  329. OC.Apps.showAppSidebar(this._detailsView.$el);
  330. },
  331. /**
  332. * Update the details view to display the given file
  333. *
  334. * @param {string} fileName file name from the current list
  335. * @param {boolean} [show=true] whether to open the sidebar if it was closed
  336. */
  337. _updateDetailsView: function(fileName, show) {
  338. if (!this._detailsView) {
  339. return;
  340. }
  341. // show defaults to true
  342. show = _.isUndefined(show) || !!show;
  343. var oldFileInfo = this._detailsView.getFileInfo();
  344. if (oldFileInfo) {
  345. // TODO: use more efficient way, maybe track the highlight
  346. this.$fileList.children().filterAttr('data-id', '' + oldFileInfo.get('id')).removeClass('highlighted');
  347. oldFileInfo.off('change', this._onSelectedModelChanged, this);
  348. }
  349. if (!fileName) {
  350. this._detailsView.setFileInfo(null);
  351. if (this._currentFileModel) {
  352. this._currentFileModel.off();
  353. }
  354. this._currentFileModel = null;
  355. OC.Apps.hideAppSidebar(this._detailsView.$el);
  356. return;
  357. }
  358. if (show && this._detailsView.$el.hasClass('disappear')) {
  359. OC.Apps.showAppSidebar(this._detailsView.$el);
  360. }
  361. var $tr = this.findFileEl(fileName);
  362. var model = this.getModelForFile($tr);
  363. this._currentFileModel = model;
  364. $tr.addClass('highlighted');
  365. this._detailsView.setFileInfo(model);
  366. this._detailsView.$el.scrollTop(0);
  367. },
  368. /**
  369. * Event handler for when the window size changed
  370. */
  371. _onResize: function() {
  372. var containerWidth = this.$el.width();
  373. var actionsWidth = 0;
  374. $.each(this.$el.find('#controls .actions'), function(index, action) {
  375. actionsWidth += $(action).outerWidth();
  376. });
  377. // substract app navigation toggle when visible
  378. containerWidth -= $('#app-navigation-toggle').width();
  379. this.breadcrumb.setMaxWidth(containerWidth - actionsWidth - 10);
  380. this.$table.find('>thead').width($('#app-content').width() - OC.Util.getScrollBarWidth());
  381. this.updateSearch();
  382. },
  383. /**
  384. * Event handler for when the URL changed
  385. */
  386. _onUrlChanged: function(e) {
  387. if (e && e.dir) {
  388. this.changeDirectory(e.dir, false, true);
  389. }
  390. },
  391. /**
  392. * Selected/deselects the given file element and updated
  393. * the internal selection cache.
  394. *
  395. * @param {Object} $tr single file row element
  396. * @param {bool} state true to select, false to deselect
  397. */
  398. _selectFileEl: function($tr, state, showDetailsView) {
  399. var $checkbox = $tr.find('td.filename>.selectCheckBox');
  400. var oldData = !!this._selectedFiles[$tr.data('id')];
  401. var data;
  402. $checkbox.prop('checked', state);
  403. $tr.toggleClass('selected', state);
  404. // already selected ?
  405. if (state === oldData) {
  406. return;
  407. }
  408. data = this.elementToFile($tr);
  409. if (state) {
  410. this._selectedFiles[$tr.data('id')] = data;
  411. this._selectionSummary.add(data);
  412. }
  413. else {
  414. delete this._selectedFiles[$tr.data('id')];
  415. this._selectionSummary.remove(data);
  416. }
  417. if (this._detailsView && this._selectionSummary.getTotal() === 1 && !this._detailsView.$el.hasClass('disappear')) {
  418. this._updateDetailsView(_.values(this._selectedFiles)[0].name);
  419. }
  420. this.$el.find('.select-all').prop('checked', this._selectionSummary.getTotal() === this.files.length);
  421. },
  422. /**
  423. * Event handler for when clicking on files to select them
  424. */
  425. _onClickFile: function(event) {
  426. var $tr = $(event.target).closest('tr');
  427. if ($tr.hasClass('dragging')) {
  428. return;
  429. }
  430. if (this._allowSelection && (event.ctrlKey || event.shiftKey)) {
  431. event.preventDefault();
  432. if (event.shiftKey) {
  433. var $lastTr = $(this._lastChecked);
  434. var lastIndex = $lastTr.index();
  435. var currentIndex = $tr.index();
  436. var $rows = this.$fileList.children('tr');
  437. // last clicked checkbox below current one ?
  438. if (lastIndex > currentIndex) {
  439. var aux = lastIndex;
  440. lastIndex = currentIndex;
  441. currentIndex = aux;
  442. }
  443. // auto-select everything in-between
  444. for (var i = lastIndex + 1; i < currentIndex; i++) {
  445. this._selectFileEl($rows.eq(i), true);
  446. }
  447. }
  448. else {
  449. this._lastChecked = $tr;
  450. }
  451. var $checkbox = $tr.find('td.filename>.selectCheckBox');
  452. this._selectFileEl($tr, !$checkbox.prop('checked'));
  453. this.updateSelectionSummary();
  454. } else {
  455. // clicked directly on the name
  456. if (!this._detailsView || $(event.target).is('.nametext') || $(event.target).closest('.nametext').length) {
  457. var filename = $tr.attr('data-file');
  458. var renaming = $tr.data('renaming');
  459. if (!renaming) {
  460. this.fileActions.currentFile = $tr.find('td');
  461. var mime = this.fileActions.getCurrentMimeType();
  462. var type = this.fileActions.getCurrentType();
  463. var permissions = this.fileActions.getCurrentPermissions();
  464. var action = this.fileActions.getDefault(mime,type, permissions);
  465. if (action) {
  466. event.preventDefault();
  467. // also set on global object for legacy apps
  468. window.FileActions.currentFile = this.fileActions.currentFile;
  469. action(filename, {
  470. $file: $tr,
  471. fileList: this,
  472. fileActions: this.fileActions,
  473. dir: $tr.attr('data-path') || this.getCurrentDirectory()
  474. });
  475. }
  476. // deselect row
  477. $(event.target).closest('a').blur();
  478. }
  479. } else {
  480. this._updateDetailsView($tr.attr('data-file'));
  481. event.preventDefault();
  482. }
  483. }
  484. },
  485. /**
  486. * Event handler for when clicking on a file's checkbox
  487. */
  488. _onClickFileCheckbox: function(e) {
  489. var $tr = $(e.target).closest('tr');
  490. var state = !$tr.hasClass('selected');
  491. this._selectFileEl($tr, state);
  492. this._lastChecked = $tr;
  493. this.updateSelectionSummary();
  494. if (state) {
  495. this._updateDetailsView($tr.attr('data-file'));
  496. }
  497. },
  498. /**
  499. * Event handler for when selecting/deselecting all files
  500. */
  501. _onClickSelectAll: function(e) {
  502. var checked = $(e.target).prop('checked');
  503. this.$fileList.find('td.filename>.selectCheckBox').prop('checked', checked)
  504. .closest('tr').toggleClass('selected', checked);
  505. this._selectedFiles = {};
  506. this._selectionSummary.clear();
  507. if (checked) {
  508. for (var i = 0; i < this.files.length; i++) {
  509. var fileData = this.files[i];
  510. this._selectedFiles[fileData.id] = fileData;
  511. this._selectionSummary.add(fileData);
  512. }
  513. }
  514. this.updateSelectionSummary();
  515. },
  516. /**
  517. * Event handler for when clicking on "Download" for the selected files
  518. */
  519. _onClickDownloadSelected: function(event) {
  520. var files;
  521. var dir = this.getCurrentDirectory();
  522. if (this.isAllSelected()) {
  523. files = OC.basename(dir);
  524. dir = OC.dirname(dir) || '/';
  525. }
  526. else {
  527. files = _.pluck(this.getSelectedFiles(), 'name');
  528. }
  529. var downloadFileaction = $('#selectedActionsList').find('.download');
  530. // don't allow a second click on the download action
  531. if(downloadFileaction.hasClass('disabled')) {
  532. event.preventDefault();
  533. return;
  534. }
  535. var disableLoadingState = function(){
  536. OCA.Files.FileActions.updateFileActionSpinner(downloadFileaction, false);
  537. };
  538. OCA.Files.FileActions.updateFileActionSpinner(downloadFileaction, true);
  539. OCA.Files.Files.handleDownload(this.getDownloadUrl(files, dir), disableLoadingState);
  540. return false;
  541. },
  542. /**
  543. * Event handler for when clicking on "Delete" for the selected files
  544. */
  545. _onClickDeleteSelected: function(event) {
  546. var files = null;
  547. if (!this.isAllSelected()) {
  548. files = _.pluck(this.getSelectedFiles(), 'name');
  549. }
  550. this.do_delete(files);
  551. event.preventDefault();
  552. return false;
  553. },
  554. /**
  555. * Event handler when clicking on a table header
  556. */
  557. _onClickHeader: function(e) {
  558. if (this.$table.hasClass('multiselect')) {
  559. return;
  560. }
  561. var $target = $(e.target);
  562. var sort;
  563. if (!$target.is('a')) {
  564. $target = $target.closest('a');
  565. }
  566. sort = $target.attr('data-sort');
  567. if (sort) {
  568. if (this._sort === sort) {
  569. this.setSort(sort, (this._sortDirection === 'desc')?'asc':'desc', true);
  570. }
  571. else {
  572. if ( sort === 'name' ) { //default sorting of name is opposite to size and mtime
  573. this.setSort(sort, 'asc', true);
  574. }
  575. else {
  576. this.setSort(sort, 'desc', true);
  577. }
  578. }
  579. }
  580. },
  581. /**
  582. * Event handler when clicking on a bread crumb
  583. */
  584. _onClickBreadCrumb: function(e) {
  585. var $el = $(e.target).closest('.crumb'),
  586. $targetDir = $el.data('dir');
  587. if ($targetDir !== undefined && e.which === 1) {
  588. e.preventDefault();
  589. this.changeDirectory($targetDir);
  590. this.updateSearch();
  591. }
  592. },
  593. /**
  594. * Event handler for when scrolling the list container.
  595. * This appends/renders the next page of entries when reaching the bottom.
  596. */
  597. _onScroll: function(e) {
  598. if (this.$container.scrollTop() + this.$container.height() > this.$el.height() - 300) {
  599. this._nextPage(true);
  600. }
  601. },
  602. /**
  603. * Event handler when dropping on a breadcrumb
  604. */
  605. _onDropOnBreadCrumb: function( event, ui ) {
  606. var self = this;
  607. var $target = $(event.target);
  608. if (!$target.is('.crumb')) {
  609. $target = $target.closest('.crumb');
  610. }
  611. var targetPath = $(event.target).data('dir');
  612. var dir = this.getCurrentDirectory();
  613. while (dir.substr(0,1) === '/') {//remove extra leading /'s
  614. dir = dir.substr(1);
  615. }
  616. dir = '/' + dir;
  617. if (dir.substr(-1,1) !== '/') {
  618. dir = dir + '/';
  619. }
  620. // do nothing if dragged on current dir
  621. if (targetPath === dir || targetPath + '/' === dir) {
  622. return;
  623. }
  624. var files = this.getSelectedFiles();
  625. if (files.length === 0) {
  626. // single one selected without checkbox?
  627. files = _.map(ui.helper.find('tr'), function(el) {
  628. return self.elementToFile($(el));
  629. });
  630. }
  631. this.move(_.pluck(files, 'name'), targetPath);
  632. },
  633. /**
  634. * Sets a new page title
  635. */
  636. setPageTitle: function(title){
  637. if (title) {
  638. title += ' - ';
  639. } else {
  640. title = '';
  641. }
  642. title += this.appName;
  643. // Sets the page title with the " - ownCloud" suffix as in templates
  644. window.document.title = title + ' - ' + oc_defaults.title;
  645. return true;
  646. },
  647. /**
  648. * Returns the file info for the given file name from the internal collection.
  649. *
  650. * @param {string} fileName file name
  651. * @return {OCA.Files.FileInfo} file info or null if it was not found
  652. *
  653. * @since 8.2
  654. */
  655. findFile: function(fileName) {
  656. return _.find(this.files, function(aFile) {
  657. return (aFile.name === fileName);
  658. }) || null;
  659. },
  660. /**
  661. * Returns the tr element for a given file name, but only if it was already rendered.
  662. *
  663. * @param {string} fileName file name
  664. * @return {Object} jQuery object of the matching row
  665. */
  666. findFileEl: function(fileName){
  667. // use filterAttr to avoid escaping issues
  668. return this.$fileList.find('tr').filterAttr('data-file', fileName);
  669. },
  670. /**
  671. * Returns the file data from a given file element.
  672. * @param $el file tr element
  673. * @return file data
  674. */
  675. elementToFile: function($el){
  676. $el = $($el);
  677. var data = {
  678. id: parseInt($el.attr('data-id'), 10),
  679. name: $el.attr('data-file'),
  680. mimetype: $el.attr('data-mime'),
  681. mtime: parseInt($el.attr('data-mtime'), 10),
  682. type: $el.attr('data-type'),
  683. size: parseInt($el.attr('data-size'), 10),
  684. etag: $el.attr('data-etag'),
  685. permissions: parseInt($el.attr('data-permissions'), 10)
  686. };
  687. var icon = $el.attr('data-icon');
  688. if (icon) {
  689. data.icon = icon;
  690. }
  691. var mountType = $el.attr('data-mounttype');
  692. if (mountType) {
  693. data.mountType = mountType;
  694. }
  695. return data;
  696. },
  697. /**
  698. * Appends the next page of files into the table
  699. * @param animate true to animate the new elements
  700. * @return array of DOM elements of the newly added files
  701. */
  702. _nextPage: function(animate) {
  703. var index = this.$fileList.children().length,
  704. count = this.pageSize(),
  705. hidden,
  706. tr,
  707. fileData,
  708. newTrs = [],
  709. isAllSelected = this.isAllSelected();
  710. if (index >= this.files.length) {
  711. return false;
  712. }
  713. while (count > 0 && index < this.files.length) {
  714. fileData = this.files[index];
  715. if (this._filter) {
  716. hidden = fileData.name.toLowerCase().indexOf(this._filter.toLowerCase()) === -1;
  717. } else {
  718. hidden = false;
  719. }
  720. tr = this._renderRow(fileData, {updateSummary: false, silent: true, hidden: hidden});
  721. this.$fileList.append(tr);
  722. if (isAllSelected || this._selectedFiles[fileData.id]) {
  723. tr.addClass('selected');
  724. tr.find('.selectCheckBox').prop('checked', true);
  725. }
  726. if (animate) {
  727. tr.addClass('appear transparent');
  728. }
  729. newTrs.push(tr);
  730. index++;
  731. count--;
  732. }
  733. // trigger event for newly added rows
  734. if (newTrs.length > 0) {
  735. this.$fileList.trigger($.Event('fileActionsReady', {fileList: this, $files: newTrs}));
  736. }
  737. if (animate) {
  738. // defer, for animation
  739. window.setTimeout(function() {
  740. for (var i = 0; i < newTrs.length; i++ ) {
  741. newTrs[i].removeClass('transparent');
  742. }
  743. }, 0);
  744. }
  745. return newTrs;
  746. },
  747. /**
  748. * Event handler for when file actions were updated.
  749. * This will refresh the file actions on the list.
  750. */
  751. _onFileActionsUpdated: function() {
  752. var self = this;
  753. var $files = this.$fileList.find('tr');
  754. if (!$files.length) {
  755. return;
  756. }
  757. $files.each(function() {
  758. self.fileActions.display($(this).find('td.filename'), false, self);
  759. });
  760. this.$fileList.trigger($.Event('fileActionsReady', {fileList: this, $files: $files}));
  761. },
  762. /**
  763. * Sets the files to be displayed in the list.
  764. * This operation will re-render the list and update the summary.
  765. * @param filesArray array of file data (map)
  766. */
  767. setFiles: function(filesArray) {
  768. var self = this;
  769. // detach to make adding multiple rows faster
  770. this.files = filesArray;
  771. this.$fileList.empty();
  772. // clear "Select all" checkbox
  773. this.$el.find('.select-all').prop('checked', false);
  774. this.isEmpty = this.files.length === 0;
  775. this._nextPage();
  776. this.updateEmptyContent();
  777. this.fileSummary.calculate(filesArray);
  778. this._selectedFiles = {};
  779. this._selectionSummary.clear();
  780. this.updateSelectionSummary();
  781. $(window).scrollTop(0);
  782. this.$fileList.trigger(jQuery.Event('updated'));
  783. _.defer(function() {
  784. self.$el.closest('#app-content').trigger(jQuery.Event('apprendered'));
  785. });
  786. },
  787. /**
  788. * Creates a new table row element using the given file data.
  789. * @param {OCA.Files.FileInfo} fileData file info attributes
  790. * @param options map of attributes
  791. * @return new tr element (not appended to the table)
  792. */
  793. _createRow: function(fileData, options) {
  794. var td, simpleSize, basename, extension, sizeColor,
  795. icon = OC.MimeType.getIconUrl(fileData.mimetype),
  796. name = fileData.name,
  797. type = fileData.type || 'file',
  798. mtime = parseInt(fileData.mtime, 10),
  799. mime = fileData.mimetype,
  800. path = fileData.path,
  801. dataIcon = null,
  802. linkUrl;
  803. options = options || {};
  804. if (isNaN(mtime)) {
  805. mtime = new Date().getTime();
  806. }
  807. if (type === 'dir') {
  808. mime = mime || 'httpd/unix-directory';
  809. if (fileData.mountType && fileData.mountType.indexOf('external') === 0) {
  810. icon = OC.MimeType.getIconUrl('dir-external');
  811. dataIcon = icon;
  812. }
  813. }
  814. //containing tr
  815. var tr = $('<tr></tr>').attr({
  816. "data-id" : fileData.id,
  817. "data-type": type,
  818. "data-size": fileData.size,
  819. "data-file": name,
  820. "data-mime": mime,
  821. "data-mtime": mtime,
  822. "data-etag": fileData.etag,
  823. "data-permissions": fileData.permissions || this.getDirectoryPermissions()
  824. });
  825. if (dataIcon) {
  826. // icon override
  827. tr.attr('data-icon', dataIcon);
  828. }
  829. if (fileData.mountType) {
  830. tr.attr('data-mounttype', fileData.mountType);
  831. }
  832. if (!_.isUndefined(path)) {
  833. tr.attr('data-path', path);
  834. }
  835. else {
  836. path = this.getCurrentDirectory();
  837. }
  838. if (type === 'dir') {
  839. // use default folder icon
  840. icon = icon || OC.imagePath('core', 'filetypes/folder');
  841. }
  842. else {
  843. icon = icon || OC.imagePath('core', 'filetypes/file');
  844. }
  845. // filename td
  846. td = $('<td class="filename"></td>');
  847. // linkUrl
  848. if (type === 'dir') {
  849. linkUrl = this.linkTo(path + '/' + name);
  850. }
  851. else {
  852. linkUrl = this.getDownloadUrl(name, path);
  853. }
  854. if (this._allowSelection) {
  855. td.append(
  856. '<input id="select-' + this.id + '-' + fileData.id +
  857. '" type="checkbox" class="selectCheckBox checkbox"/><label for="select-' + this.id + '-' + fileData.id + '">' +
  858. '<div class="thumbnail" style="background-image:url(' + icon + '); background-size: 32px;"></div>' +
  859. '<span class="hidden-visually">' + t('files', 'Select') + '</span>' +
  860. '</label>'
  861. );
  862. } else {
  863. td.append('<div class="thumbnail" style="background-image:url(' + icon + '); background-size: 32px;"></div>');
  864. }
  865. var linkElem = $('<a></a>').attr({
  866. "class": "name",
  867. "href": linkUrl
  868. });
  869. // from here work on the display name
  870. name = fileData.displayName || name;
  871. // show hidden files (starting with a dot) completely in gray
  872. if(name.indexOf('.') === 0) {
  873. basename = '';
  874. extension = name;
  875. // split extension from filename for non dirs
  876. } else if (type !== 'dir' && name.indexOf('.') !== -1) {
  877. basename = name.substr(0, name.lastIndexOf('.'));
  878. extension = name.substr(name.lastIndexOf('.'));
  879. } else {
  880. basename = name;
  881. extension = false;
  882. }
  883. var nameSpan=$('<span></span>').addClass('nametext');
  884. var innernameSpan = $('<span></span>').addClass('innernametext').text(basename);
  885. nameSpan.append(innernameSpan);
  886. linkElem.append(nameSpan);
  887. if (extension) {
  888. nameSpan.append($('<span></span>').addClass('extension').text(extension));
  889. }
  890. if (fileData.extraData) {
  891. if (fileData.extraData.charAt(0) === '/') {
  892. fileData.extraData = fileData.extraData.substr(1);
  893. }
  894. nameSpan.addClass('extra-data').attr('title', fileData.extraData);
  895. nameSpan.tooltip({placement: 'right'});
  896. }
  897. // dirs can show the number of uploaded files
  898. if (type === 'dir') {
  899. linkElem.append($('<span></span>').attr({
  900. 'class': 'uploadtext',
  901. 'currentUploads': 0
  902. }));
  903. }
  904. td.append(linkElem);
  905. tr.append(td);
  906. // size column
  907. if (typeof(fileData.size) !== 'undefined' && fileData.size >= 0) {
  908. simpleSize = humanFileSize(parseInt(fileData.size, 10), true);
  909. sizeColor = Math.round(160-Math.pow((fileData.size/(1024*1024)),2));
  910. } else {
  911. simpleSize = t('files', 'Pending');
  912. }
  913. td = $('<td></td>').attr({
  914. "class": "filesize",
  915. "style": 'color:rgb(' + sizeColor + ',' + sizeColor + ',' + sizeColor + ')'
  916. }).text(simpleSize);
  917. tr.append(td);
  918. // date column (1000 milliseconds to seconds, 60 seconds, 60 minutes, 24 hours)
  919. // difference in days multiplied by 5 - brightest shade for files older than 32 days (160/5)
  920. var modifiedColor = Math.round(((new Date()).getTime() - mtime )/1000/60/60/24*5 );
  921. // ensure that the brightest color is still readable
  922. if (modifiedColor >= '160') {
  923. modifiedColor = 160;
  924. }
  925. var formatted;
  926. var text;
  927. if (mtime > 0) {
  928. formatted = OC.Util.formatDate(mtime);
  929. text = OC.Util.relativeModifiedDate(mtime);
  930. } else {
  931. formatted = t('files', 'Unable to determine date');
  932. text = '?';
  933. }
  934. td = $('<td></td>').attr({ "class": "date" });
  935. td.append($('<span></span>').attr({
  936. "class": "modified",
  937. "title": formatted,
  938. "style": 'color:rgb('+modifiedColor+','+modifiedColor+','+modifiedColor+')'
  939. }).text(text)
  940. .tooltip({placement: 'top'})
  941. );
  942. tr.find('.filesize').text(simpleSize);
  943. tr.append(td);
  944. return tr;
  945. },
  946. /**
  947. * Adds an entry to the files array and also into the DOM
  948. * in a sorted manner.
  949. *
  950. * @param {OCA.Files.FileInfo} fileData map of file attributes
  951. * @param {Object} [options] map of attributes
  952. * @param {boolean} [options.updateSummary] true to update the summary
  953. * after adding (default), false otherwise. Defaults to true.
  954. * @param {boolean} [options.silent] true to prevent firing events like "fileActionsReady",
  955. * defaults to false.
  956. * @param {boolean} [options.animate] true to animate the thumbnail image after load
  957. * defaults to true.
  958. * @return new tr element (not appended to the table)
  959. */
  960. add: function(fileData, options) {
  961. var index = -1;
  962. var $tr;
  963. var $rows;
  964. var $insertionPoint;
  965. options = _.extend({animate: true}, options || {});
  966. // there are three situations to cover:
  967. // 1) insertion point is visible on the current page
  968. // 2) insertion point is on a not visible page (visible after scrolling)
  969. // 3) insertion point is at the end of the list
  970. $rows = this.$fileList.children();
  971. index = this._findInsertionIndex(fileData);
  972. if (index > this.files.length) {
  973. index = this.files.length;
  974. }
  975. else {
  976. $insertionPoint = $rows.eq(index);
  977. }
  978. // is the insertion point visible ?
  979. if ($insertionPoint.length) {
  980. // only render if it will really be inserted
  981. $tr = this._renderRow(fileData, options);
  982. $insertionPoint.before($tr);
  983. }
  984. else {
  985. // if insertion point is after the last visible
  986. // entry, append
  987. if (index === $rows.length) {
  988. $tr = this._renderRow(fileData, options);
  989. this.$fileList.append($tr);
  990. }
  991. }
  992. this.isEmpty = false;
  993. this.files.splice(index, 0, fileData);
  994. if ($tr && options.animate) {
  995. $tr.addClass('appear transparent');
  996. window.setTimeout(function() {
  997. $tr.removeClass('transparent');
  998. });
  999. }
  1000. if (options.scrollTo) {
  1001. this.scrollTo(fileData.name);
  1002. }
  1003. // defaults to true if not defined
  1004. if (typeof(options.updateSummary) === 'undefined' || !!options.updateSummary) {
  1005. this.fileSummary.add(fileData, true);
  1006. this.updateEmptyContent();
  1007. }
  1008. return $tr;
  1009. },
  1010. /**
  1011. * Creates a new row element based on the given attributes
  1012. * and returns it.
  1013. *
  1014. * @param {OCA.Files.FileInfo} fileData map of file attributes
  1015. * @param {Object} [options] map of attributes
  1016. * @param {int} [options.index] index at which to insert the element
  1017. * @param {boolean} [options.updateSummary] true to update the summary
  1018. * after adding (default), false otherwise. Defaults to true.
  1019. * @param {boolean} [options.animate] true to animate the thumbnail image after load
  1020. * defaults to true.
  1021. * @return new tr element (not appended to the table)
  1022. */
  1023. _renderRow: function(fileData, options) {
  1024. options = options || {};
  1025. var type = fileData.type || 'file',
  1026. mime = fileData.mimetype,
  1027. path = fileData.path || this.getCurrentDirectory(),
  1028. permissions = parseInt(fileData.permissions, 10) || 0;
  1029. if (fileData.isShareMountPoint) {
  1030. permissions = permissions | OC.PERMISSION_UPDATE;
  1031. }
  1032. if (type === 'dir') {
  1033. mime = mime || 'httpd/unix-directory';
  1034. }
  1035. var tr = this._createRow(
  1036. fileData,
  1037. options
  1038. );
  1039. var filenameTd = tr.find('td.filename');
  1040. // TODO: move dragging to FileActions ?
  1041. // enable drag only for deletable files
  1042. if (this._dragOptions && permissions & OC.PERMISSION_DELETE) {
  1043. filenameTd.draggable(this._dragOptions);
  1044. }
  1045. // allow dropping on folders
  1046. if (this._folderDropOptions && fileData.type === 'dir') {
  1047. filenameTd.droppable(this._folderDropOptions);
  1048. }
  1049. if (options.hidden) {
  1050. tr.addClass('hidden');
  1051. }
  1052. // display actions
  1053. this.fileActions.display(filenameTd, !options.silent, this);
  1054. if (fileData.isPreviewAvailable && mime !== 'httpd/unix-directory') {
  1055. var iconDiv = filenameTd.find('.thumbnail');
  1056. // lazy load / newly inserted td ?
  1057. // the typeof check ensures that the default value of animate is true
  1058. if (typeof(options.animate) === 'undefined' || !!options.animate) {
  1059. this.lazyLoadPreview({
  1060. path: path + '/' + fileData.name,
  1061. mime: mime,
  1062. etag: fileData.etag,
  1063. callback: function(url) {
  1064. iconDiv.css('background-image', 'url("' + url + '")');
  1065. }
  1066. });
  1067. }
  1068. else {
  1069. // set the preview URL directly
  1070. var urlSpec = {
  1071. file: path + '/' + fileData.name,
  1072. c: fileData.etag
  1073. };
  1074. var previewUrl = this.generatePreviewUrl(urlSpec);
  1075. previewUrl = previewUrl.replace('(', '%28').replace(')', '%29');
  1076. iconDiv.css('background-image', 'url("' + previewUrl + '")');
  1077. }
  1078. }
  1079. return tr;
  1080. },
  1081. /**
  1082. * Returns the current directory
  1083. * @method getCurrentDirectory
  1084. * @return current directory
  1085. */
  1086. getCurrentDirectory: function(){
  1087. return this._currentDirectory || this.$el.find('#dir').val() || '/';
  1088. },
  1089. /**
  1090. * Returns the directory permissions
  1091. * @return permission value as integer
  1092. */
  1093. getDirectoryPermissions: function() {
  1094. return parseInt(this.$el.find('#permissions').val(), 10);
  1095. },
  1096. /**
  1097. * @brief Changes the current directory and reload the file list.
  1098. * @param targetDir target directory (non URL encoded)
  1099. * @param changeUrl false if the URL must not be changed (defaults to true)
  1100. * @param {boolean} force set to true to force changing directory
  1101. */
  1102. changeDirectory: function(targetDir, changeUrl, force) {
  1103. var self = this;
  1104. var currentDir = this.getCurrentDirectory();
  1105. targetDir = targetDir || '/';
  1106. if (!force && currentDir === targetDir) {
  1107. return;
  1108. }
  1109. this._setCurrentDir(targetDir, changeUrl);
  1110. this.reload().then(function(success){
  1111. if (!success) {
  1112. self.changeDirectory(currentDir, true);
  1113. }
  1114. });
  1115. },
  1116. linkTo: function(dir) {
  1117. return OC.linkTo('files', 'index.php')+"?dir="+ encodeURIComponent(dir).replace(/%2F/g, '/');
  1118. },
  1119. /**
  1120. * Sets the current directory name and updates the breadcrumb.
  1121. * @param targetDir directory to display
  1122. * @param changeUrl true to also update the URL, false otherwise (default)
  1123. */
  1124. _setCurrentDir: function(targetDir, changeUrl) {
  1125. targetDir = targetDir.replace(/\\/g, '/');
  1126. var previousDir = this.getCurrentDirectory(),
  1127. baseDir = OC.basename(targetDir);
  1128. if (baseDir !== '') {
  1129. this.setPageTitle(baseDir);
  1130. }
  1131. else {
  1132. this.setPageTitle();
  1133. }
  1134. this._currentDirectory = targetDir;
  1135. // legacy stuff
  1136. this.$el.find('#dir').val(targetDir);
  1137. if (changeUrl !== false) {
  1138. this.$el.trigger(jQuery.Event('changeDirectory', {
  1139. dir: targetDir,
  1140. previousDir: previousDir
  1141. }));
  1142. }
  1143. this.breadcrumb.setDirectory(this.getCurrentDirectory());
  1144. },
  1145. /**
  1146. * Sets the current sorting and refreshes the list
  1147. *
  1148. * @param sort sort attribute name
  1149. * @param direction sort direction, one of "asc" or "desc"
  1150. * @param update true to update the list, false otherwise (default)
  1151. */
  1152. setSort: function(sort, direction, update) {
  1153. var comparator = FileList.Comparators[sort] || FileList.Comparators.name;
  1154. this._sort = sort;
  1155. this._sortDirection = (direction === 'desc')?'desc':'asc';
  1156. this._sortComparator = comparator;
  1157. if (direction === 'desc') {
  1158. this._sortComparator = function(fileInfo1, fileInfo2) {
  1159. return -comparator(fileInfo1, fileInfo2);
  1160. };
  1161. }
  1162. this.$el.find('thead th .sort-indicator')
  1163. .removeClass(this.SORT_INDICATOR_ASC_CLASS)
  1164. .removeClass(this.SORT_INDICATOR_DESC_CLASS)
  1165. .toggleClass('hidden', true)
  1166. .addClass(this.SORT_INDICATOR_DESC_CLASS);
  1167. this.$el.find('thead th.column-' + sort + ' .sort-indicator')
  1168. .removeClass(this.SORT_INDICATOR_ASC_CLASS)
  1169. .removeClass(this.SORT_INDICATOR_DESC_CLASS)
  1170. .toggleClass('hidden', false)
  1171. .addClass(direction === 'desc' ? this.SORT_INDICATOR_DESC_CLASS : this.SORT_INDICATOR_ASC_CLASS);
  1172. if (update) {
  1173. if (this._clientSideSort) {
  1174. this.files.sort(this._sortComparator);
  1175. this.setFiles(this.files);
  1176. }
  1177. else {
  1178. this.reload();
  1179. }
  1180. }
  1181. },
  1182. /**
  1183. * Reloads the file list using ajax call
  1184. *
  1185. * @return ajax call object
  1186. */
  1187. reload: function() {
  1188. this._selectedFiles = {};
  1189. this._selectionSummary.clear();
  1190. if (this._currentFileModel) {
  1191. this._currentFileModel.off();
  1192. }
  1193. this._currentFileModel = null;
  1194. this.$el.find('.select-all').prop('checked', false);
  1195. this.showMask();
  1196. if (this._reloadCall) {
  1197. this._reloadCall.abort();
  1198. }
  1199. this._reloadCall = $.ajax({
  1200. url: this.getAjaxUrl('list'),
  1201. data: {
  1202. dir : this.getCurrentDirectory(),
  1203. sort: this._sort,
  1204. sortdirection: this._sortDirection
  1205. }
  1206. });
  1207. if (this._detailsView) {
  1208. // close sidebar
  1209. this._updateDetailsView(null);
  1210. }
  1211. var callBack = this.reloadCallback.bind(this);
  1212. return this._reloadCall.then(callBack, callBack);
  1213. },
  1214. reloadCallback: function(result) {
  1215. delete this._reloadCall;
  1216. this.hideMask();
  1217. if (!result || result.status === 'error') {
  1218. // if the error is not related to folder we're trying to load, reload the page to handle logout etc
  1219. if (result.data.error === 'authentication_error' ||
  1220. result.data.error === 'token_expired' ||
  1221. result.data.error === 'application_not_enabled'
  1222. ) {
  1223. OC.redirect(OC.generateUrl('apps/files'));
  1224. }
  1225. OC.Notification.showTemporary(result.data.message);
  1226. return false;
  1227. }
  1228. // Firewall Blocked request?
  1229. if (result.status === 403) {
  1230. // Go home
  1231. this.changeDirectory('/');
  1232. OC.Notification.showTemporary(t('files', 'This operation is forbidden'));
  1233. return false;
  1234. }
  1235. // Did share service die or something else fail?
  1236. if (result.status === 500) {
  1237. // Go home
  1238. this.changeDirectory('/');
  1239. OC.Notification.showTemporary(t('files', 'This directory is unavailable, please check the logs or contact the administrator'));
  1240. return false;
  1241. }
  1242. if (result.status === 404) {
  1243. // go back home
  1244. this.changeDirectory('/');
  1245. return false;
  1246. }
  1247. // aborted ?
  1248. if (result.status === 0){
  1249. return true;
  1250. }
  1251. // TODO: should rather return upload file size through
  1252. // the files list ajax call
  1253. this.updateStorageStatistics(true);
  1254. if (result.data.permissions) {
  1255. this.setDirectoryPermissions(result.data.permissions);
  1256. }
  1257. this.setFiles(result.data.files);
  1258. return true;
  1259. },
  1260. updateStorageStatistics: function(force) {
  1261. OCA.Files.Files.updateStorageStatistics(this.getCurrentDirectory(), force);
  1262. },
  1263. getAjaxUrl: function(action, params) {
  1264. return OCA.Files.Files.getAjaxUrl(action, params);
  1265. },
  1266. getDownloadUrl: function(files, dir) {
  1267. return OCA.Files.Files.getDownloadUrl(files, dir || this.getCurrentDirectory());
  1268. },
  1269. /**
  1270. * Generates a preview URL based on the URL space.
  1271. * @param urlSpec attributes for the URL
  1272. * @param {int} urlSpec.x width
  1273. * @param {int} urlSpec.y height
  1274. * @param {String} urlSpec.file path to the file
  1275. * @return preview URL
  1276. */
  1277. generatePreviewUrl: function(urlSpec) {
  1278. urlSpec = urlSpec || {};
  1279. if (!urlSpec.x) {
  1280. urlSpec.x = this.$table.data('preview-x') || 32;
  1281. }
  1282. if (!urlSpec.y) {
  1283. urlSpec.y = this.$table.data('preview-y') || 32;
  1284. }
  1285. urlSpec.x *= window.devicePixelRatio;
  1286. urlSpec.y *= window.devicePixelRatio;
  1287. urlSpec.x = Math.ceil(urlSpec.x);
  1288. urlSpec.y = Math.ceil(urlSpec.y);
  1289. urlSpec.forceIcon = 0;
  1290. return OC.generateUrl('/core/preview.png?') + $.param(urlSpec);
  1291. },
  1292. /**
  1293. * Lazy load a file's preview.
  1294. *
  1295. * @param path path of the file
  1296. * @param mime mime type
  1297. * @param callback callback function to call when the image was loaded
  1298. * @param etag file etag (for caching)
  1299. */
  1300. lazyLoadPreview : function(options) {
  1301. var self = this;
  1302. var path = options.path;
  1303. var mime = options.mime;
  1304. var ready = options.callback;
  1305. var etag = options.etag;
  1306. // get mime icon url
  1307. var iconURL = OC.MimeType.getIconUrl(mime);
  1308. var previewURL,
  1309. urlSpec = {};
  1310. ready(iconURL); // set mimeicon URL
  1311. urlSpec.file = OCA.Files.Files.fixPath(path);
  1312. if (options.x) {
  1313. urlSpec.x = options.x;
  1314. }
  1315. if (options.y) {
  1316. urlSpec.y = options.y;
  1317. }
  1318. if (options.a) {
  1319. urlSpec.a = options.a;
  1320. }
  1321. if (options.mode) {
  1322. urlSpec.mode = options.mode;
  1323. }
  1324. if (etag){
  1325. // use etag as cache buster
  1326. urlSpec.c = etag;
  1327. } else {
  1328. console.warn('OCA.Files.FileList.lazyLoadPreview(): missing etag argument');
  1329. }
  1330. previewURL = self.generatePreviewUrl(urlSpec);
  1331. previewURL = previewURL.replace('(', '%28');
  1332. previewURL = previewURL.replace(')', '%29');
  1333. // preload image to prevent delay
  1334. // this will make the browser cache the image
  1335. var img = new Image();
  1336. img.onload = function(){
  1337. // if loading the preview image failed (no preview for the mimetype) then img.width will < 5
  1338. if (img.width > 5) {
  1339. ready(previewURL, img);
  1340. } else if (options.error) {
  1341. options.error();
  1342. }
  1343. };
  1344. if (options.error) {
  1345. img.onerror = options.error;
  1346. }
  1347. img.src = previewURL;
  1348. },
  1349. setDirectoryPermissions: function(permissions) {
  1350. var isCreatable = (permissions & OC.PERMISSION_CREATE) !== 0;
  1351. this.$el.find('#permissions').val(permissions);
  1352. this.$el.find('.creatable').toggleClass('hidden', !isCreatable);
  1353. this.$el.find('.notCreatable').toggleClass('hidden', isCreatable);
  1354. },
  1355. /**
  1356. * Shows/hides action buttons
  1357. *
  1358. * @param show true for enabling, false for disabling
  1359. */
  1360. showActions: function(show){
  1361. this.$el.find('.actions,#file_action_panel').toggleClass('hidden', !show);
  1362. if (show){
  1363. // make sure to display according to permissions
  1364. var permissions = this.getDirectoryPermissions();
  1365. var isCreatable = (permissions & OC.PERMISSION_CREATE) !== 0;
  1366. this.$el.find('.creatable').toggleClass('hidden', !isCreatable);
  1367. this.$el.find('.notCreatable').toggleClass('hidden', isCreatable);
  1368. // remove old style breadcrumbs (some apps might create them)
  1369. this.$el.find('#controls .crumb').remove();
  1370. // refresh breadcrumbs in case it was replaced by an app
  1371. this.breadcrumb.render();
  1372. }
  1373. else{
  1374. this.$el.find('.creatable, .notCreatable').addClass('hidden');
  1375. }
  1376. },
  1377. /**
  1378. * Enables/disables viewer mode.
  1379. * In viewer mode, apps can embed themselves under the controls bar.
  1380. * In viewer mode, the actions of the file list will be hidden.
  1381. * @param show true for enabling, false for disabling
  1382. */
  1383. setViewerMode: function(show){
  1384. this.showActions(!show);
  1385. this.$el.find('#filestable').toggleClass('hidden', show);
  1386. this.$el.trigger(new $.Event('changeViewerMode', {viewerModeEnabled: show}));
  1387. },
  1388. /**
  1389. * Removes a file entry from the list
  1390. * @param name name of the file to remove
  1391. * @param {Object} [options] map of attributes
  1392. * @param {boolean} [options.updateSummary] true to update the summary
  1393. * after removing, false otherwise. Defaults to true.
  1394. * @return deleted element
  1395. */
  1396. remove: function(name, options){
  1397. options = options || {};
  1398. var fileEl = this.findFileEl(name);
  1399. var fileId = fileEl.data('id');
  1400. var index = fileEl.index();
  1401. if (!fileEl.length) {
  1402. return null;
  1403. }
  1404. if (this._selectedFiles[fileId]) {
  1405. // remove from selection first
  1406. this._selectFileEl(fileEl, false);
  1407. this.updateSelectionSummary();
  1408. }
  1409. if (this._dragOptions && (fileEl.data('permissions') & OC.PERMISSION_DELETE)) {
  1410. // file is only draggable when delete permissions are set
  1411. fileEl.find('td.filename').draggable('destroy');
  1412. }
  1413. this.files.splice(index, 1);
  1414. if (this._currentFileModel && this._currentFileModel.get('id') === fileId) {
  1415. // Note: in the future we should call destroy() directly on the model
  1416. // and the model will take care of the deletion.
  1417. // Here we only trigger the event to notify listeners that
  1418. // the file was removed.
  1419. this._currentFileModel.trigger('destroy');
  1420. this._updateDetailsView(null);
  1421. }
  1422. fileEl.remove();
  1423. // TODO: improve performance on batch update
  1424. this.isEmpty = !this.files.length;
  1425. if (typeof(options.updateSummary) === 'undefined' || !!options.updateSummary) {
  1426. this.updateEmptyContent();
  1427. this.fileSummary.remove({type: fileEl.attr('data-type'), size: fileEl.attr('data-size')}, true);
  1428. }
  1429. var lastIndex = this.$fileList.children().length;
  1430. // if there are less elements visible than one page
  1431. // but there are still pending elements in the array,
  1432. // then directly append the next page
  1433. if (lastIndex < this.files.length && lastIndex < this.pageSize()) {
  1434. this._nextPage(true);
  1435. }
  1436. return fileEl;
  1437. },
  1438. /**
  1439. * Finds the index of the row before which the given
  1440. * fileData should be inserted, considering the current
  1441. * sorting
  1442. *
  1443. * @param {OCA.Files.FileInfo} fileData file info
  1444. */
  1445. _findInsertionIndex: function(fileData) {
  1446. var index = 0;
  1447. while (index < this.files.length && this._sortComparator(fileData, this.files[index]) > 0) {
  1448. index++;
  1449. }
  1450. return index;
  1451. },
  1452. /**
  1453. * Moves a file to a given target folder.
  1454. *
  1455. * @param fileNames array of file names to move
  1456. * @param targetPath absolute target path
  1457. */
  1458. move: function(fileNames, targetPath) {
  1459. var self = this;
  1460. var dir = this.getCurrentDirectory();
  1461. var target = OC.basename(targetPath);
  1462. if (!_.isArray(fileNames)) {
  1463. fileNames = [fileNames];
  1464. }
  1465. _.each(fileNames, function(fileName) {
  1466. var $tr = self.findFileEl(fileName);
  1467. self.showFileBusyState($tr, true);
  1468. // TODO: improve performance by sending all file names in a single call
  1469. $.post(
  1470. OC.filePath('files', 'ajax', 'move.php'),
  1471. {
  1472. dir: dir,
  1473. file: fileName,
  1474. target: targetPath
  1475. },
  1476. function(result) {
  1477. if (result) {
  1478. if (result.status === 'success') {
  1479. // if still viewing the same directory
  1480. if (self.getCurrentDirectory() === dir) {
  1481. // recalculate folder size
  1482. var oldFile = self.findFileEl(target);
  1483. var newFile = self.findFileEl(fileName);
  1484. var oldSize = oldFile.data('size');
  1485. var newSize = oldSize + newFile.data('size');
  1486. oldFile.data('size', newSize);
  1487. oldFile.find('td.filesize').text(OC.Util.humanFileSize(newSize));
  1488. // TODO: also update entry in FileList.files
  1489. self.remove(fileName);
  1490. }
  1491. } else {
  1492. OC.Notification.hide();
  1493. if (result.status === 'error' && result.data.message) {
  1494. OC.Notification.showTemporary(result.data.message);
  1495. }
  1496. else {
  1497. OC.Notification.showTemporary(t('files', 'Error moving file.'));
  1498. }
  1499. }
  1500. } else {
  1501. OC.dialogs.alert(t('files', 'Error moving file'), t('files', 'Error'));
  1502. }
  1503. self.showFileBusyState($tr, false);
  1504. }
  1505. );
  1506. });
  1507. },
  1508. /**
  1509. * Updates the given row with the given file info
  1510. *
  1511. * @param {Object} $tr row element
  1512. * @param {OCA.Files.FileInfo} fileInfo file info
  1513. * @param {Object} options options
  1514. *
  1515. * @return {Object} new row element
  1516. */
  1517. updateRow: function($tr, fileInfo, options) {
  1518. this.files.splice($tr.index(), 1);
  1519. $tr.remove();
  1520. $tr = this.add(fileInfo, _.extend({updateSummary: false, silent: true}, options));
  1521. this.$fileList.trigger($.Event('fileActionsReady', {fileList: this, $files: $tr}));
  1522. return $tr;
  1523. },
  1524. /**
  1525. * Triggers file rename input field for the given file name.
  1526. * If the user enters a new name, the file will be renamed.
  1527. *
  1528. * @param oldname file name of the file to rename
  1529. */
  1530. rename: function(oldname) {
  1531. var self = this;
  1532. var tr, td, input, form;
  1533. tr = this.findFileEl(oldname);
  1534. var oldFileInfo = this.files[tr.index()];
  1535. tr.data('renaming',true);
  1536. td = tr.children('td.filename');
  1537. input = $('<input type="text" class="filename"/>').val(oldname);
  1538. form = $('<form></form>');
  1539. form.append(input);
  1540. td.children('a.name').hide();
  1541. td.append(form);
  1542. input.focus();
  1543. //preselect input
  1544. var len = input.val().lastIndexOf('.');
  1545. if ( len === -1 ||
  1546. tr.data('type') === 'dir' ) {
  1547. len = input.val().length;
  1548. }
  1549. input.selectRange(0, len);
  1550. var checkInput = function () {
  1551. var filename = input.val();
  1552. if (filename !== oldname) {
  1553. // Files.isFileNameValid(filename) throws an exception itself
  1554. OCA.Files.Files.isFileNameValid(filename);
  1555. if (self.inList(filename)) {
  1556. throw t('files', '{new_name} already exists', {new_name: filename});
  1557. }
  1558. }
  1559. return true;
  1560. };
  1561. function restore() {
  1562. input.tooltip('hide');
  1563. tr.data('renaming',false);
  1564. form.remove();
  1565. td.children('a.name').show();
  1566. }
  1567. form.submit(function(event) {
  1568. event.stopPropagation();
  1569. event.preventDefault();
  1570. if (input.hasClass('error')) {
  1571. return;
  1572. }
  1573. try {
  1574. var newName = input.val();
  1575. input.tooltip('hide');
  1576. form.remove();
  1577. if (newName !== oldname) {
  1578. checkInput();
  1579. // mark as loading (temp element)
  1580. self.showFileBusyState(tr, true);
  1581. tr.attr('data-file', newName);
  1582. var basename = newName;
  1583. if (newName.indexOf('.') > 0 && tr.data('type') !== 'dir') {
  1584. basename = newName.substr(0, newName.lastIndexOf('.'));
  1585. }
  1586. td.find('a.name span.nametext').text(basename);
  1587. td.children('a.name').show();
  1588. $.ajax({
  1589. url: OC.filePath('files','ajax','rename.php'),
  1590. data: {
  1591. dir : tr.attr('data-path') || self.getCurrentDirectory(),
  1592. newname: newName,
  1593. file: oldname
  1594. },
  1595. success: function(result) {
  1596. var fileInfo;
  1597. if (!result || result.status === 'error') {
  1598. OC.dialogs.alert(result.data.message, t('files', 'Could not rename file'));
  1599. fileInfo = oldFileInfo;
  1600. if (result.data.code === 'sourcenotfound') {
  1601. self.remove(result.data.newname, {updateSummary: true});
  1602. return;
  1603. }
  1604. }
  1605. else {
  1606. fileInfo = result.data;
  1607. }
  1608. // reinsert row
  1609. self.files.splice(tr.index(), 1);
  1610. tr.remove();
  1611. tr = self.add(fileInfo, {updateSummary: false, silent: true});
  1612. self.$fileList.trigger($.Event('fileActionsReady', {fileList: self, $files: $(tr)}));
  1613. self._updateDetailsView(fileInfo.name, false);
  1614. }
  1615. });
  1616. } else {
  1617. // add back the old file info when cancelled
  1618. self.files.splice(tr.index(), 1);
  1619. tr.remove();
  1620. tr = self.add(oldFileInfo, {updateSummary: false, silent: true});
  1621. self.$fileList.trigger($.Event('fileActionsReady', {fileList: self, $files: $(tr)}));
  1622. }
  1623. } catch (error) {
  1624. input.attr('title', error);
  1625. input.tooltip({placement: 'right', trigger: 'manual'});
  1626. input.tooltip('show');
  1627. input.addClass('error');
  1628. }
  1629. return false;
  1630. });
  1631. input.keyup(function(event) {
  1632. // verify filename on typing
  1633. try {
  1634. checkInput();
  1635. input.tooltip('hide');
  1636. input.removeClass('error');
  1637. } catch (error) {
  1638. input.attr('title', error);
  1639. input.tooltip({placement: 'right', trigger: 'manual'});
  1640. input.tooltip('show');
  1641. input.addClass('error');
  1642. }
  1643. if (event.keyCode === 27) {
  1644. restore();
  1645. }
  1646. });
  1647. input.click(function(event) {
  1648. event.stopPropagation();
  1649. event.preventDefault();
  1650. });
  1651. input.blur(function() {
  1652. form.trigger('submit');
  1653. });
  1654. },
  1655. /**
  1656. * Create an empty file inside the current directory.
  1657. *
  1658. * @param {string} name name of the file
  1659. *
  1660. * @return {Promise} promise that will be resolved after the
  1661. * file was created
  1662. *
  1663. * @since 8.2
  1664. */
  1665. createFile: function(name) {
  1666. var self = this;
  1667. var deferred = $.Deferred();
  1668. var promise = deferred.promise();
  1669. OCA.Files.Files.isFileNameValid(name);
  1670. name = this.getUniqueName(name);
  1671. if (this.lastAction) {
  1672. this.lastAction();
  1673. }
  1674. $.post(
  1675. OC.generateUrl('/apps/files/ajax/newfile.php'),
  1676. {
  1677. dir: this.getCurrentDirectory(),
  1678. filename: name
  1679. },
  1680. function(result) {
  1681. if (result.status === 'success') {
  1682. self.add(result.data, {animate: true, scrollTo: true});
  1683. deferred.resolve(result.status, result.data);
  1684. } else {
  1685. if (result.data && result.data.message) {
  1686. OC.Notification.showTemporary(result.data.message);
  1687. } else {
  1688. OC.Notification.showTemporary(t('core', 'Could not create file'));
  1689. }
  1690. deferred.reject(result.status, result.data);
  1691. }
  1692. }
  1693. );
  1694. return promise;
  1695. },
  1696. /**
  1697. * Create a directory inside the current directory.
  1698. *
  1699. * @param {string} name name of the directory
  1700. *
  1701. * @return {Promise} promise that will be resolved after the
  1702. * directory was created
  1703. *
  1704. * @since 8.2
  1705. */
  1706. createDirectory: function(name) {
  1707. var self = this;
  1708. var deferred = $.Deferred();
  1709. var promise = deferred.promise();
  1710. OCA.Files.Files.isFileNameValid(name);
  1711. name = this.getUniqueName(name);
  1712. if (this.lastAction) {
  1713. this.lastAction();
  1714. }
  1715. $.post(
  1716. OC.generateUrl('/apps/files/ajax/newfolder.php'),
  1717. {
  1718. dir: this.getCurrentDirectory(),
  1719. foldername: name
  1720. },
  1721. function(result) {
  1722. if (result.status === 'success') {
  1723. self.add(result.data, {animate: true, scrollTo: true});
  1724. deferred.resolve(result.status, result.data);
  1725. } else {
  1726. if (result.data && result.data.message) {
  1727. OC.Notification.showTemporary(result.data.message);
  1728. } else {
  1729. OC.Notification.showTemporary(t('core', 'Could not create folder'));
  1730. }
  1731. deferred.reject(result.status);
  1732. }
  1733. }
  1734. );
  1735. return promise;
  1736. },
  1737. /**
  1738. * Returns whether the given file name exists in the list
  1739. *
  1740. * @param {string} file file name
  1741. *
  1742. * @return {bool} true if the file exists in the list, false otherwise
  1743. */
  1744. inList:function(file) {
  1745. return this.findFile(file);
  1746. },
  1747. /**
  1748. * Shows busy state on a given file row or multiple
  1749. *
  1750. * @param {string|Array.<string>} files file name or array of file names
  1751. * @param {bool} [busy=true] busy state, true for busy, false to remove busy state
  1752. *
  1753. * @since 8.2
  1754. */
  1755. showFileBusyState: function(files, state) {
  1756. var self = this;
  1757. if (!_.isArray(files)) {
  1758. files = [files];
  1759. }
  1760. if (_.isUndefined(state)) {
  1761. state = true;
  1762. }
  1763. _.each(files, function($tr) {
  1764. // jquery element already ?
  1765. if (!$tr.is) {
  1766. $tr = self.findFileEl($tr);
  1767. }
  1768. var $thumbEl = $tr.find('.thumbnail');
  1769. $tr.toggleClass('busy', state);
  1770. if (state) {
  1771. $thumbEl.attr('data-oldimage', $thumbEl.css('background-image'));
  1772. $thumbEl.css('background-image', 'url('+ OC.imagePath('core', 'loading.gif') + ')');
  1773. } else {
  1774. $thumbEl.css('background-image', $thumbEl.attr('data-oldimage'));
  1775. $thumbEl.removeAttr('data-oldimage');
  1776. }
  1777. });
  1778. },
  1779. /**
  1780. * Delete the given files from the given dir
  1781. * @param files file names list (without path)
  1782. * @param dir directory in which to delete the files, defaults to the current
  1783. * directory
  1784. */
  1785. do_delete:function(files, dir) {
  1786. var self = this;
  1787. var params;
  1788. if (files && files.substr) {
  1789. files=[files];
  1790. }
  1791. if (files) {
  1792. this.showFileBusyState(files, true);
  1793. for (var i=0; i<files.length; i++) {
  1794. }
  1795. }
  1796. // Finish any existing actions
  1797. if (this.lastAction) {
  1798. this.lastAction();
  1799. }
  1800. params = {
  1801. dir: dir || this.getCurrentDirectory()
  1802. };
  1803. if (files) {
  1804. params.files = JSON.stringify(files);
  1805. }
  1806. else {
  1807. // no files passed, delete all in current dir
  1808. params.allfiles = true;
  1809. // show spinner for all files
  1810. this.showFileBusyState(this.$fileList.find('tr'), true);
  1811. }
  1812. $.post(OC.filePath('files', 'ajax', 'delete.php'),
  1813. params,
  1814. function(result) {
  1815. if (result.status === 'success') {
  1816. if (params.allfiles) {
  1817. self.setFiles([]);
  1818. }
  1819. else {
  1820. $.each(files,function(index,file) {
  1821. var fileEl = self.remove(file, {updateSummary: false});
  1822. // FIXME: not sure why we need this after the
  1823. // element isn't even in the DOM any more
  1824. fileEl.find('.selectCheckBox').prop('checked', false);
  1825. fileEl.removeClass('selected');
  1826. self.fileSummary.remove({type: fileEl.attr('data-type'), size: fileEl.attr('data-size')});
  1827. });
  1828. }
  1829. // TODO: this info should be returned by the ajax call!
  1830. self.updateEmptyContent();
  1831. self.fileSummary.update();
  1832. self.updateSelectionSummary();
  1833. self.updateStorageStatistics();
  1834. // in case there was a "storage full" permanent notification
  1835. OC.Notification.hide();
  1836. } else {
  1837. if (result.status === 'error' && result.data.message) {
  1838. OC.Notification.showTemporary(result.data.message);
  1839. }
  1840. else {
  1841. OC.Notification.showTemporary(t('files', 'Error deleting file.'));
  1842. }
  1843. if (params.allfiles) {
  1844. // reload the page as we don't know what files were deleted
  1845. // and which ones remain
  1846. self.reload();
  1847. }
  1848. else {
  1849. $.each(files,function(index,file) {
  1850. self.showFileBusyState(file, false);
  1851. });
  1852. }
  1853. }
  1854. });
  1855. },
  1856. /**
  1857. * Creates the file summary section
  1858. */
  1859. _createSummary: function() {
  1860. var $tr = $('<tr class="summary"></tr>');
  1861. this.$el.find('tfoot').append($tr);
  1862. return new OCA.Files.FileSummary($tr);
  1863. },
  1864. updateEmptyContent: function() {
  1865. var permissions = this.getDirectoryPermissions();
  1866. var isCreatable = (permissions & OC.PERMISSION_CREATE) !== 0;
  1867. this.$el.find('#emptycontent').toggleClass('hidden', !this.isEmpty);
  1868. this.$el.find('#emptycontent .uploadmessage').toggleClass('hidden', !isCreatable || !this.isEmpty);
  1869. this.$el.find('#filestable thead th').toggleClass('hidden', this.isEmpty);
  1870. },
  1871. /**
  1872. * Shows the loading mask.
  1873. *
  1874. * @see OCA.Files.FileList#hideMask
  1875. */
  1876. showMask: function() {
  1877. // in case one was shown before
  1878. var $mask = this.$el.find('.mask');
  1879. if ($mask.exists()) {
  1880. return;
  1881. }
  1882. this.$table.addClass('hidden');
  1883. this.$el.find('#emptycontent').addClass('hidden');
  1884. $mask = $('<div class="mask transparent"></div>');
  1885. $mask.css('background-image', 'url('+ OC.imagePath('core', 'loading.gif') + ')');
  1886. $mask.css('background-repeat', 'no-repeat');
  1887. this.$el.append($mask);
  1888. $mask.removeClass('transparent');
  1889. },
  1890. /**
  1891. * Hide the loading mask.
  1892. * @see OCA.Files.FileList#showMask
  1893. */
  1894. hideMask: function() {
  1895. this.$el.find('.mask').remove();
  1896. this.$table.removeClass('hidden');
  1897. },
  1898. scrollTo:function(file) {
  1899. if (!_.isArray(file)) {
  1900. file = [file];
  1901. }
  1902. this.highlightFiles(file, function($tr) {
  1903. $tr.addClass('searchresult');
  1904. $tr.one('hover', function() {
  1905. $tr.removeClass('searchresult');
  1906. });
  1907. });
  1908. },
  1909. /**
  1910. * @deprecated use setFilter(filter)
  1911. */
  1912. filter:function(query) {
  1913. this.setFilter('');
  1914. },
  1915. /**
  1916. * @deprecated use setFilter('')
  1917. */
  1918. unfilter:function() {
  1919. this.setFilter('');
  1920. },
  1921. /**
  1922. * hide files matching the given filter
  1923. * @param filter
  1924. */
  1925. setFilter:function(filter) {
  1926. this._filter = filter;
  1927. this.fileSummary.setFilter(filter, this.files);
  1928. if (!this.$el.find('.mask').exists()) {
  1929. this.hideIrrelevantUIWhenNoFilesMatch();
  1930. }
  1931. var that = this;
  1932. this.$fileList.find('tr').each(function(i,e) {
  1933. var $e = $(e);
  1934. if ($e.data('file').toString().toLowerCase().indexOf(filter.toLowerCase()) === -1) {
  1935. $e.addClass('hidden');
  1936. that.$container.trigger('scroll');
  1937. } else {
  1938. $e.removeClass('hidden');
  1939. }
  1940. });
  1941. },
  1942. hideIrrelevantUIWhenNoFilesMatch:function() {
  1943. if (this._filter && this.fileSummary.summary.totalDirs + this.fileSummary.summary.totalFiles === 0) {
  1944. this.$el.find('#filestable thead th').addClass('hidden');
  1945. this.$el.find('#emptycontent').addClass('hidden');
  1946. $('#searchresults').addClass('filter-empty');
  1947. if ( $('#searchresults').length === 0 || $('#searchresults').hasClass('hidden') ) {
  1948. this.$el.find('.nofilterresults').removeClass('hidden').
  1949. find('p').text(t('files', "No entries in this folder match '{filter}'", {filter:this._filter}, null, {'escape': false}));
  1950. }
  1951. } else {
  1952. $('#searchresults').removeClass('filter-empty');
  1953. this.$el.find('#filestable thead th').toggleClass('hidden', this.isEmpty);
  1954. if (!this.$el.find('.mask').exists()) {
  1955. this.$el.find('#emptycontent').toggleClass('hidden', !this.isEmpty);
  1956. }
  1957. this.$el.find('.nofilterresults').addClass('hidden');
  1958. }
  1959. },
  1960. /**
  1961. * get the current filter
  1962. * @param filter
  1963. */
  1964. getFilter:function(filter) {
  1965. return this._filter;
  1966. },
  1967. /**
  1968. * update the search object to use this filelist when filtering
  1969. */
  1970. updateSearch:function() {
  1971. if (OCA.Search.files) {
  1972. OCA.Search.files.setFileList(this);
  1973. }
  1974. if (OC.Search) {
  1975. OC.Search.clear();
  1976. }
  1977. },
  1978. /**
  1979. * Update UI based on the current selection
  1980. */
  1981. updateSelectionSummary: function() {
  1982. var summary = this._selectionSummary.summary;
  1983. var canDelete;
  1984. var selection;
  1985. if (summary.totalFiles === 0 && summary.totalDirs === 0) {
  1986. this.$el.find('#headerName a.name>span:first').text(t('files','Name'));
  1987. this.$el.find('#headerSize a>span:first').text(t('files','Size'));
  1988. this.$el.find('#modified a>span:first').text(t('files','Modified'));
  1989. this.$el.find('table').removeClass('multiselect');
  1990. this.$el.find('.selectedActions').addClass('hidden');
  1991. }
  1992. else {
  1993. canDelete = (this.getDirectoryPermissions() & OC.PERMISSION_DELETE) && this.isSelectedDeletable();
  1994. this.$el.find('.selectedActions').removeClass('hidden');
  1995. this.$el.find('#headerSize a>span:first').text(OC.Util.humanFileSize(summary.totalSize));
  1996. var directoryInfo = n('files', '%n folder', '%n folders', summary.totalDirs);
  1997. var fileInfo = n('files', '%n file', '%n files', summary.totalFiles);
  1998. if (summary.totalDirs > 0 && summary.totalFiles > 0) {
  1999. var selectionVars = {
  2000. dirs: directoryInfo,
  2001. files: fileInfo
  2002. };
  2003. selection = t('files', '{dirs} and {files}', selectionVars);
  2004. } else if (summary.totalDirs > 0) {
  2005. selection = directoryInfo;
  2006. } else {
  2007. selection = fileInfo;
  2008. }
  2009. this.$el.find('#headerName a.name>span:first').text(selection);
  2010. this.$el.find('#modified a>span:first').text('');
  2011. this.$el.find('table').addClass('multiselect');
  2012. this.$el.find('.delete-selected').toggleClass('hidden', !canDelete);
  2013. }
  2014. },
  2015. /**
  2016. * Check whether all selected files are deletable
  2017. */
  2018. isSelectedDeletable: function() {
  2019. return _.reduce(this.getSelectedFiles(), function(deletable, file) {
  2020. return deletable && (file.permissions & OC.PERMISSION_DELETE);
  2021. }, true);
  2022. },
  2023. /**
  2024. * Returns whether all files are selected
  2025. * @return true if all files are selected, false otherwise
  2026. */
  2027. isAllSelected: function() {
  2028. return this.$el.find('.select-all').prop('checked');
  2029. },
  2030. /**
  2031. * Returns the file info of the selected files
  2032. *
  2033. * @return array of file names
  2034. */
  2035. getSelectedFiles: function() {
  2036. return _.values(this._selectedFiles);
  2037. },
  2038. getUniqueName: function(name) {
  2039. if (this.findFileEl(name).exists()) {
  2040. var numMatch;
  2041. var parts=name.split('.');
  2042. var extension = "";
  2043. if (parts.length > 1) {
  2044. extension=parts.pop();
  2045. }
  2046. var base=parts.join('.');
  2047. numMatch=base.match(/\((\d+)\)/);
  2048. var num=2;
  2049. if (numMatch && numMatch.length>0) {
  2050. num=parseInt(numMatch[numMatch.length-1], 10)+1;
  2051. base=base.split('(');
  2052. base.pop();
  2053. base=$.trim(base.join('('));
  2054. }
  2055. name=base+' ('+num+')';
  2056. if (extension) {
  2057. name = name+'.'+extension;
  2058. }
  2059. // FIXME: ugly recursion
  2060. return this.getUniqueName(name);
  2061. }
  2062. return name;
  2063. },
  2064. /**
  2065. * Shows a "permission denied" notification
  2066. */
  2067. _showPermissionDeniedNotification: function() {
  2068. var message = t('core', 'You don’t have permission to upload or create files here');
  2069. OC.Notification.showTemporary(message);
  2070. },
  2071. /**
  2072. * Setup file upload events related to the file-upload plugin
  2073. */
  2074. setupUploadEvents: function() {
  2075. var self = this;
  2076. // handle upload events
  2077. var fileUploadStart = this.$el.find('#file_upload_start');
  2078. // detect the progress bar resize
  2079. fileUploadStart.on('resized', this._onResize);
  2080. fileUploadStart.on('fileuploaddrop', function(e, data) {
  2081. OC.Upload.log('filelist handle fileuploaddrop', e, data);
  2082. if (self.$el.hasClass('hidden')) {
  2083. // do not upload to invisible lists
  2084. return false;
  2085. }
  2086. var dropTarget = $(e.originalEvent.target);
  2087. // check if dropped inside this container and not another one
  2088. if (dropTarget.length
  2089. && !self.$el.is(dropTarget) // dropped on list directly
  2090. && !self.$el.has(dropTarget).length // dropped inside list
  2091. && !dropTarget.is(self.$container) // dropped on main container
  2092. ) {
  2093. return false;
  2094. }
  2095. // find the closest tr or crumb to use as target
  2096. dropTarget = dropTarget.closest('tr, .crumb');
  2097. // if dropping on tr or crumb, drag&drop upload to folder
  2098. if (dropTarget && (dropTarget.data('type') === 'dir' ||
  2099. dropTarget.hasClass('crumb'))) {
  2100. // remember as context
  2101. data.context = dropTarget;
  2102. // if permissions are specified, only allow if create permission is there
  2103. var permissions = dropTarget.data('permissions');
  2104. if (!_.isUndefined(permissions) && (permissions & OC.PERMISSION_CREATE) === 0) {
  2105. self._showPermissionDeniedNotification();
  2106. return false;
  2107. }
  2108. var dir = dropTarget.data('file');
  2109. // if from file list, need to prepend parent dir
  2110. if (dir) {
  2111. var parentDir = self.getCurrentDirectory();
  2112. if (parentDir[parentDir.length - 1] !== '/') {
  2113. parentDir += '/';
  2114. }
  2115. dir = parentDir + dir;
  2116. }
  2117. else{
  2118. // read full path from crumb
  2119. dir = dropTarget.data('dir') || '/';
  2120. }
  2121. // add target dir
  2122. data.targetDir = dir;
  2123. } else {
  2124. // we are dropping somewhere inside the file list, which will
  2125. // upload the file to the current directory
  2126. data.targetDir = self.getCurrentDirectory();
  2127. // cancel uploads to current dir if no permission
  2128. var isCreatable = (self.getDirectoryPermissions() & OC.PERMISSION_CREATE) !== 0;
  2129. if (!isCreatable) {
  2130. self._showPermissionDeniedNotification();
  2131. return false;
  2132. }
  2133. }
  2134. });
  2135. fileUploadStart.on('fileuploadadd', function(e, data) {
  2136. OC.Upload.log('filelist handle fileuploadadd', e, data);
  2137. //finish delete if we are uploading a deleted file
  2138. if (self.deleteFiles && self.deleteFiles.indexOf(data.files[0].name)!==-1) {
  2139. self.finishDelete(null, true); //delete file before continuing
  2140. }
  2141. // add ui visualization to existing folder
  2142. if (data.context && data.context.data('type') === 'dir') {
  2143. // add to existing folder
  2144. // update upload counter ui
  2145. var uploadText = data.context.find('.uploadtext');
  2146. var currentUploads = parseInt(uploadText.attr('currentUploads'), 10);
  2147. currentUploads += 1;
  2148. uploadText.attr('currentUploads', currentUploads);
  2149. var translatedText = n('files', 'Uploading %n file', 'Uploading %n files', currentUploads);
  2150. if (currentUploads === 1) {
  2151. var img = OC.imagePath('core', 'loading.gif');
  2152. data.context.find('.thumbnail').css('background-image', 'url(' + img + ')');
  2153. uploadText.text(translatedText);
  2154. uploadText.show();
  2155. } else {
  2156. uploadText.text(translatedText);
  2157. }
  2158. }
  2159. });
  2160. /*
  2161. * when file upload done successfully add row to filelist
  2162. * update counter when uploading to sub folder
  2163. */
  2164. fileUploadStart.on('fileuploaddone', function(e, data) {
  2165. OC.Upload.log('filelist handle fileuploaddone', e, data);
  2166. var response;
  2167. if (typeof data.result === 'string') {
  2168. response = data.result;
  2169. } else {
  2170. // fetch response from iframe
  2171. response = data.result[0].body.innerText;
  2172. }
  2173. var result=$.parseJSON(response);
  2174. if (typeof result[0] !== 'undefined' && result[0].status === 'success') {
  2175. var file = result[0];
  2176. var size = 0;
  2177. if (data.context && data.context.data('type') === 'dir') {
  2178. // update upload counter ui
  2179. var uploadText = data.context.find('.uploadtext');
  2180. var currentUploads = parseInt(uploadText.attr('currentUploads'), 10);
  2181. currentUploads -= 1;
  2182. uploadText.attr('currentUploads', currentUploads);
  2183. var translatedText = n('files', 'Uploading %n file', 'Uploading %n files', currentUploads);
  2184. if (currentUploads === 0) {
  2185. var img = OC.imagePath('core', 'filetypes/folder');
  2186. data.context.find('.thumbnail').css('background-image', 'url(' + img + ')');
  2187. uploadText.text(translatedText);
  2188. uploadText.hide();
  2189. } else {
  2190. uploadText.text(translatedText);
  2191. }
  2192. // update folder size
  2193. size = parseInt(data.context.data('size'), 10);
  2194. size += parseInt(file.size, 10);
  2195. data.context.attr('data-size', size);
  2196. data.context.find('td.filesize').text(humanFileSize(size));
  2197. } else {
  2198. // only append new file if uploaded into the current folder
  2199. if (file.directory !== self.getCurrentDirectory()) {
  2200. // Uploading folders actually uploads a list of files
  2201. // for which the target directory (file.directory) might lie deeper
  2202. // than the current directory
  2203. var fileDirectory = file.directory.replace('/','').replace(/\/$/, "");
  2204. var currentDirectory = self.getCurrentDirectory().replace('/','').replace(/\/$/, "") + '/';
  2205. if (currentDirectory !== '/') {
  2206. // abort if fileDirectory does not start with current one
  2207. if (fileDirectory.indexOf(currentDirectory) !== 0) {
  2208. return;
  2209. }
  2210. // remove the current directory part
  2211. fileDirectory = fileDirectory.substr(currentDirectory.length);
  2212. }
  2213. // only take the first section of the path
  2214. fileDirectory = fileDirectory.split('/');
  2215. var fd;
  2216. // if the first section exists / is a subdir
  2217. if (fileDirectory.length) {
  2218. fileDirectory = fileDirectory[0];
  2219. // See whether it is already in the list
  2220. fd = self.findFileEl(fileDirectory);
  2221. if (fd.length === 0) {
  2222. var dir = {
  2223. name: fileDirectory,
  2224. type: 'dir',
  2225. mimetype: 'httpd/unix-directory',
  2226. permissions: file.permissions,
  2227. size: 0,
  2228. id: file.parentId
  2229. };
  2230. fd = self.add(dir, {insert: true});
  2231. }
  2232. // update folder size
  2233. size = parseInt(fd.attr('data-size'), 10);
  2234. size += parseInt(file.size, 10);
  2235. fd.attr('data-size', size);
  2236. fd.find('td.filesize').text(OC.Util.humanFileSize(size));
  2237. }
  2238. return;
  2239. }
  2240. // add as stand-alone row to filelist
  2241. size = t('files', 'Pending');
  2242. if (data.files[0].size>=0) {
  2243. size=data.files[0].size;
  2244. }
  2245. //should the file exist in the list remove it
  2246. self.remove(file.name);
  2247. // create new file context
  2248. data.context = self.add(file, {animate: true});
  2249. }
  2250. }
  2251. });
  2252. fileUploadStart.on('fileuploadstop', function(e, data) {
  2253. OC.Upload.log('filelist handle fileuploadstop', e, data);
  2254. //if user pressed cancel hide upload chrome
  2255. if (data.errorThrown === 'abort') {
  2256. //cleanup uploading to a dir
  2257. var uploadText = $('tr .uploadtext');
  2258. var img = OC.imagePath('core', 'filetypes/folder');
  2259. uploadText.parents('td.filename').find('.thumbnail').css('background-image', 'url(' + img + ')');
  2260. uploadText.fadeOut();
  2261. uploadText.attr('currentUploads', 0);
  2262. }
  2263. self.updateStorageStatistics();
  2264. });
  2265. fileUploadStart.on('fileuploadfail', function(e, data) {
  2266. OC.Upload.log('filelist handle fileuploadfail', e, data);
  2267. //if user pressed cancel hide upload chrome
  2268. if (data.errorThrown === 'abort') {
  2269. //cleanup uploading to a dir
  2270. var uploadText = $('tr .uploadtext');
  2271. var img = OC.imagePath('core', 'filetypes/folder');
  2272. uploadText.parents('td.filename').find('.thumbnail').css('background-image', 'url(' + img + ')');
  2273. uploadText.fadeOut();
  2274. uploadText.attr('currentUploads', 0);
  2275. }
  2276. self.updateStorageStatistics();
  2277. });
  2278. },
  2279. /**
  2280. * Scroll to the last file of the given list
  2281. * Highlight the list of files
  2282. * @param files array of filenames,
  2283. * @param {Function} [highlightFunction] optional function
  2284. * to be called after the scrolling is finished
  2285. */
  2286. highlightFiles: function(files, highlightFunction) {
  2287. // Detection of the uploaded element
  2288. var filename = files[files.length - 1];
  2289. var $fileRow = this.findFileEl(filename);
  2290. while(!$fileRow.exists() && this._nextPage(false) !== false) { // Checking element existence
  2291. $fileRow = this.findFileEl(filename);
  2292. }
  2293. if (!$fileRow.exists()) { // Element not present in the file list
  2294. return;
  2295. }
  2296. var currentOffset = this.$container.scrollTop();
  2297. var additionalOffset = this.$el.find("#controls").height()+this.$el.find("#controls").offset().top;
  2298. // Animation
  2299. var _this = this;
  2300. var $scrollContainer = this.$container;
  2301. if ($scrollContainer[0] === window) {
  2302. // need to use "body" to animate scrolling
  2303. // when the scroll container is the window
  2304. $scrollContainer = $('body');
  2305. }
  2306. $scrollContainer.animate({
  2307. // Scrolling to the top of the new element
  2308. scrollTop: currentOffset + $fileRow.offset().top - $fileRow.height() * 2 - additionalOffset
  2309. }, {
  2310. duration: 500,
  2311. complete: function() {
  2312. // Highlighting function
  2313. var highlightRow = highlightFunction;
  2314. if (!highlightRow) {
  2315. highlightRow = function($fileRow) {
  2316. $fileRow.addClass("highlightUploaded");
  2317. setTimeout(function() {
  2318. $fileRow.removeClass("highlightUploaded");
  2319. }, 2500);
  2320. };
  2321. }
  2322. // Loop over uploaded files
  2323. for(var i=0; i<files.length; i++) {
  2324. var $fileRow = _this.findFileEl(files[i]);
  2325. if($fileRow.length !== 0) { // Checking element existence
  2326. highlightRow($fileRow);
  2327. }
  2328. }
  2329. }
  2330. });
  2331. },
  2332. _renderNewButton: function() {
  2333. // if an upload button (legacy) already exists or no actions container exist, skip
  2334. var $actionsContainer = this.$el.find('#controls .actions');
  2335. if (!$actionsContainer.length || this.$el.find('.button.upload').length) {
  2336. return;
  2337. }
  2338. if (!this._addButtonTemplate) {
  2339. this._addButtonTemplate = Handlebars.compile(TEMPLATE_ADDBUTTON);
  2340. }
  2341. var $newButton = $(this._addButtonTemplate({
  2342. addText: t('files', 'New'),
  2343. iconUrl: OC.imagePath('core', 'actions/add')
  2344. }));
  2345. $actionsContainer.prepend($newButton);
  2346. $newButton.tooltip({'placement': 'bottom'});
  2347. $newButton.click(_.bind(this._onClickNewButton, this));
  2348. this._newButton = $newButton;
  2349. },
  2350. _onClickNewButton: function(event) {
  2351. var $target = $(event.target);
  2352. if (!$target.hasClass('.button')) {
  2353. $target = $target.closest('.button');
  2354. }
  2355. this._newButton.tooltip('hide');
  2356. event.preventDefault();
  2357. if ($target.hasClass('disabled')) {
  2358. return false;
  2359. }
  2360. if (!this._newFileMenu) {
  2361. this._newFileMenu = new OCA.Files.NewFileMenu({
  2362. fileList: this
  2363. });
  2364. $('body').append(this._newFileMenu.$el);
  2365. }
  2366. this._newFileMenu.showAt($target);
  2367. return false;
  2368. },
  2369. /**
  2370. * Register a tab view to be added to all views
  2371. */
  2372. registerTabView: function(tabView) {
  2373. if (this._detailsView) {
  2374. this._detailsView.addTabView(tabView);
  2375. }
  2376. },
  2377. /**
  2378. * Register a detail view to be added to all views
  2379. */
  2380. registerDetailView: function(detailView) {
  2381. if (this._detailsView) {
  2382. this._detailsView.addDetailView(detailView);
  2383. }
  2384. }
  2385. };
  2386. /**
  2387. * Sort comparators.
  2388. * @namespace OCA.Files.FileList.Comparators
  2389. * @private
  2390. */
  2391. FileList.Comparators = {
  2392. /**
  2393. * Compares two file infos by name, making directories appear
  2394. * first.
  2395. *
  2396. * @param {OCA.Files.FileInfo} fileInfo1 file info
  2397. * @param {OCA.Files.FileInfo} fileInfo2 file info
  2398. * @return {int} -1 if the first file must appear before the second one,
  2399. * 0 if they are identify, 1 otherwise.
  2400. */
  2401. name: function(fileInfo1, fileInfo2) {
  2402. if (fileInfo1.type === 'dir' && fileInfo2.type !== 'dir') {
  2403. return -1;
  2404. }
  2405. if (fileInfo1.type !== 'dir' && fileInfo2.type === 'dir') {
  2406. return 1;
  2407. }
  2408. return OC.Util.naturalSortCompare(fileInfo1.name, fileInfo2.name);
  2409. },
  2410. /**
  2411. * Compares two file infos by size.
  2412. *
  2413. * @param {OCA.Files.FileInfo} fileInfo1 file info
  2414. * @param {OCA.Files.FileInfo} fileInfo2 file info
  2415. * @return {int} -1 if the first file must appear before the second one,
  2416. * 0 if they are identify, 1 otherwise.
  2417. */
  2418. size: function(fileInfo1, fileInfo2) {
  2419. return fileInfo1.size - fileInfo2.size;
  2420. },
  2421. /**
  2422. * Compares two file infos by timestamp.
  2423. *
  2424. * @param {OCA.Files.FileInfo} fileInfo1 file info
  2425. * @param {OCA.Files.FileInfo} fileInfo2 file info
  2426. * @return {int} -1 if the first file must appear before the second one,
  2427. * 0 if they are identify, 1 otherwise.
  2428. */
  2429. mtime: function(fileInfo1, fileInfo2) {
  2430. return fileInfo1.mtime - fileInfo2.mtime;
  2431. }
  2432. };
  2433. /**
  2434. * File info attributes.
  2435. *
  2436. * @todo make this a real class in the future
  2437. * @typedef {Object} OCA.Files.FileInfo
  2438. *
  2439. * @property {int} id file id
  2440. * @property {String} name file name
  2441. * @property {String} [path] file path, defaults to the list's current path
  2442. * @property {String} mimetype mime type
  2443. * @property {String} type "file" for files or "dir" for directories
  2444. * @property {int} permissions file permissions
  2445. * @property {int} mtime modification time in milliseconds
  2446. * @property {boolean} [isShareMountPoint] whether the file is a share mount
  2447. * point
  2448. * @property {boolean} [isPreviewAvailable] whether a preview is available
  2449. * for the given file type
  2450. * @property {String} [icon] path to the mime type icon
  2451. * @property {String} etag etag of the file
  2452. */
  2453. OCA.Files.FileList = FileList;
  2454. })();
  2455. $(document).ready(function() {
  2456. // FIXME: unused ?
  2457. OCA.Files.FileList.useUndo = (window.onbeforeunload)?true:false;
  2458. $(window).bind('beforeunload', function () {
  2459. if (OCA.Files.FileList.lastAction) {
  2460. OCA.Files.FileList.lastAction();
  2461. }
  2462. });
  2463. $(window).unload(function () {
  2464. $(window).trigger('beforeunload');
  2465. });
  2466. });