fileactions.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  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. /**
  12. * Construct a new FileActions instance
  13. * @constructs FileActions
  14. * @memberof OCA.Files
  15. */
  16. var FileActions = function() {
  17. this.initialize();
  18. };
  19. FileActions.prototype = {
  20. /** @lends FileActions.prototype */
  21. actions: {},
  22. defaults: {},
  23. icons: {},
  24. currentFile: null,
  25. /**
  26. * Dummy jquery element, for events
  27. */
  28. $el: null,
  29. /**
  30. * List of handlers to be notified whenever a register() or
  31. * setDefault() was called.
  32. *
  33. * @member {Function[]}
  34. */
  35. _updateListeners: {},
  36. /**
  37. * @private
  38. */
  39. initialize: function() {
  40. this.clear();
  41. // abusing jquery for events until we get a real event lib
  42. this.$el = $('<div class="dummy-fileactions hidden"></div>');
  43. $('body').append(this.$el);
  44. },
  45. /**
  46. * Adds an event handler
  47. *
  48. * @param {String} eventName event name
  49. * @param {Function} callback
  50. */
  51. on: function(eventName, callback) {
  52. this.$el.on(eventName, callback);
  53. },
  54. /**
  55. * Removes an event handler
  56. *
  57. * @param {String} eventName event name
  58. * @param Function callback
  59. */
  60. off: function(eventName, callback) {
  61. this.$el.off(eventName, callback);
  62. },
  63. /**
  64. * Notifies the event handlers
  65. *
  66. * @param {String} eventName event name
  67. * @param {Object} data data
  68. */
  69. _notifyUpdateListeners: function(eventName, data) {
  70. this.$el.trigger(new $.Event(eventName, data));
  71. },
  72. /**
  73. * Merges the actions from the given fileActions into
  74. * this instance.
  75. *
  76. * @param {OCA.Files.FileActions} fileActions instance of OCA.Files.FileActions
  77. */
  78. merge: function(fileActions) {
  79. var self = this;
  80. // merge first level to avoid unintended overwriting
  81. _.each(fileActions.actions, function(sourceMimeData, mime) {
  82. var targetMimeData = self.actions[mime];
  83. if (!targetMimeData) {
  84. targetMimeData = {};
  85. }
  86. self.actions[mime] = _.extend(targetMimeData, sourceMimeData);
  87. });
  88. this.defaults = _.extend(this.defaults, fileActions.defaults);
  89. this.icons = _.extend(this.icons, fileActions.icons);
  90. },
  91. /**
  92. * @deprecated use #registerAction() instead
  93. */
  94. register: function(mime, name, permissions, icon, action, displayName) {
  95. return this.registerAction({
  96. name: name,
  97. mime: mime,
  98. permissions: permissions,
  99. icon: icon,
  100. actionHandler: action,
  101. displayName: displayName || name
  102. });
  103. },
  104. /**
  105. * Register action
  106. *
  107. * @param {OCA.Files.FileAction} action object
  108. */
  109. registerAction: function (action) {
  110. var mime = action.mime;
  111. var name = action.name;
  112. var actionSpec = {
  113. action: action.actionHandler,
  114. name: name,
  115. displayName: action.displayName,
  116. mime: mime,
  117. icon: action.icon,
  118. permissions: action.permissions
  119. };
  120. if (_.isUndefined(action.displayName)) {
  121. actionSpec.displayName = t('files', name);
  122. }
  123. if (_.isFunction(action.render)) {
  124. actionSpec.render = action.render;
  125. } else {
  126. actionSpec.render = _.bind(this._defaultRenderAction, this);
  127. }
  128. if (!this.actions[mime]) {
  129. this.actions[mime] = {};
  130. }
  131. this.actions[mime][name] = actionSpec;
  132. this.icons[name] = action.icon;
  133. this._notifyUpdateListeners('registerAction', {action: action});
  134. },
  135. /**
  136. * Clears all registered file actions.
  137. */
  138. clear: function() {
  139. this.actions = {};
  140. this.defaults = {};
  141. this.icons = {};
  142. this.currentFile = null;
  143. this._updateListeners = [];
  144. },
  145. /**
  146. * Sets the default action for a given mime type.
  147. *
  148. * @param {String} mime mime type
  149. * @param {String} name action name
  150. */
  151. setDefault: function (mime, name) {
  152. this.defaults[mime] = name;
  153. this._notifyUpdateListeners('setDefault', {defaultAction: {mime: mime, name: name}});
  154. },
  155. get: function (mime, type, permissions) {
  156. var actions = this.getActions(mime, type, permissions);
  157. var filteredActions = {};
  158. $.each(actions, function (name, action) {
  159. filteredActions[name] = action.action;
  160. });
  161. return filteredActions;
  162. },
  163. getActions: function (mime, type, permissions) {
  164. var actions = {};
  165. if (this.actions.all) {
  166. actions = $.extend(actions, this.actions.all);
  167. }
  168. if (type) {//type is 'dir' or 'file'
  169. if (this.actions[type]) {
  170. actions = $.extend(actions, this.actions[type]);
  171. }
  172. }
  173. if (mime) {
  174. var mimePart = mime.substr(0, mime.indexOf('/'));
  175. if (this.actions[mimePart]) {
  176. actions = $.extend(actions, this.actions[mimePart]);
  177. }
  178. if (this.actions[mime]) {
  179. actions = $.extend(actions, this.actions[mime]);
  180. }
  181. }
  182. var filteredActions = {};
  183. $.each(actions, function (name, action) {
  184. if (action.permissions & permissions) {
  185. filteredActions[name] = action;
  186. }
  187. });
  188. return filteredActions;
  189. },
  190. getDefault: function (mime, type, permissions) {
  191. var mimePart;
  192. if (mime) {
  193. mimePart = mime.substr(0, mime.indexOf('/'));
  194. }
  195. var name = false;
  196. if (mime && this.defaults[mime]) {
  197. name = this.defaults[mime];
  198. } else if (mime && this.defaults[mimePart]) {
  199. name = this.defaults[mimePart];
  200. } else if (type && this.defaults[type]) {
  201. name = this.defaults[type];
  202. } else {
  203. name = this.defaults.all;
  204. }
  205. var actions = this.get(mime, type, permissions);
  206. return actions[name];
  207. },
  208. /**
  209. * Default function to render actions
  210. *
  211. * @param {OCA.Files.FileAction} actionSpec file action spec
  212. * @param {boolean} isDefault true if the action is a default one,
  213. * false otherwise
  214. * @param {OCA.Files.FileActionContext} context action context
  215. */
  216. _defaultRenderAction: function(actionSpec, isDefault, context) {
  217. var name = actionSpec.name;
  218. if (name === 'Download' || !isDefault) {
  219. var $actionLink = this._makeActionLink(actionSpec, context);
  220. context.$file.find('a.name>span.fileactions').append($actionLink);
  221. return $actionLink;
  222. }
  223. },
  224. /**
  225. * Renders the action link element
  226. *
  227. * @param {OCA.Files.FileAction} actionSpec action object
  228. * @param {OCA.Files.FileActionContext} context action context
  229. */
  230. _makeActionLink: function(actionSpec, context) {
  231. var img = actionSpec.icon;
  232. if (img && img.call) {
  233. img = img(context.$file.attr('data-file'));
  234. }
  235. var html = '<a href="#">';
  236. if (img) {
  237. html += '<img class="svg" alt="" src="' + img + '" />';
  238. }
  239. if (actionSpec.displayName) {
  240. html += '<span> ' + actionSpec.displayName + '</span>';
  241. }
  242. html += '</a>';
  243. return $(html);
  244. },
  245. /**
  246. * Custom renderer for the "Rename" action.
  247. * Displays the rename action as an icon behind the file name.
  248. *
  249. * @param {OCA.Files.FileAction} actionSpec file action to render
  250. * @param {boolean} isDefault true if the action is a default action,
  251. * false otherwise
  252. * @param {OCAFiles.FileActionContext} context rendering context
  253. */
  254. _renderRenameAction: function(actionSpec, isDefault, context) {
  255. var $actionEl = this._makeActionLink(actionSpec, context);
  256. var $container = context.$file.find('a.name span.nametext');
  257. $actionEl.find('img').attr('alt', t('files', 'Rename'));
  258. $container.find('.action-rename').remove();
  259. $container.append($actionEl);
  260. return $actionEl;
  261. },
  262. /**
  263. * Custom renderer for the "Delete" action.
  264. * Displays the "Delete" action as a trash icon at the end of
  265. * the table row.
  266. *
  267. * @param {OCA.Files.FileAction} actionSpec file action to render
  268. * @param {boolean} isDefault true if the action is a default action,
  269. * false otherwise
  270. * @param {OCAFiles.FileActionContext} context rendering context
  271. */
  272. _renderDeleteAction: function(actionSpec, isDefault, context) {
  273. var mountType = context.$file.attr('data-mounttype');
  274. var deleteTitle = t('files', 'Delete');
  275. if (mountType === 'external-root') {
  276. deleteTitle = t('files', 'Disconnect storage');
  277. } else if (mountType === 'shared-root') {
  278. deleteTitle = t('files', 'Unshare');
  279. }
  280. var $actionLink = $('<a href="#" original-title="' +
  281. escapeHTML(deleteTitle) +
  282. '" class="action delete icon-delete">' +
  283. '<span class="hidden-visually">' + escapeHTML(deleteTitle) + '</span>' +
  284. '</a>'
  285. );
  286. var $container = context.$file.find('td:last');
  287. $container.find('.delete').remove();
  288. $container.append($actionLink);
  289. return $actionLink;
  290. },
  291. /**
  292. * Renders the action element by calling actionSpec.render() and
  293. * registers the click event to process the action.
  294. *
  295. * @param {OCA.Files.FileAction} actionSpec file action to render
  296. * @param {boolean} isDefault true if the action is a default action,
  297. * false otherwise
  298. * @param {OCAFiles.FileActionContext} context rendering context
  299. */
  300. _renderAction: function(actionSpec, isDefault, context) {
  301. var $actionEl = actionSpec.render(actionSpec, isDefault, context);
  302. if (!$actionEl || !$actionEl.length) {
  303. return;
  304. }
  305. $actionEl.addClass('action action-' + actionSpec.name.toLowerCase());
  306. $actionEl.attr('data-action', actionSpec.name);
  307. $actionEl.on(
  308. 'click', {
  309. a: null
  310. },
  311. function(event) {
  312. var $file = $(event.target).closest('tr');
  313. var currentFile = $file.find('td.filename');
  314. var fileName = $file.attr('data-file');
  315. event.stopPropagation();
  316. event.preventDefault();
  317. context.fileActions.currentFile = currentFile;
  318. // also set on global object for legacy apps
  319. window.FileActions.currentFile = currentFile;
  320. actionSpec.action(
  321. fileName,
  322. _.extend(context, {
  323. dir: $file.attr('data-path') || context.fileList.getCurrentDirectory()
  324. })
  325. );
  326. }
  327. );
  328. return $actionEl;
  329. },
  330. /**
  331. * Display file actions for the given element
  332. * @param parent "td" element of the file for which to display actions
  333. * @param triggerEvent if true, triggers the fileActionsReady on the file
  334. * list afterwards (false by default)
  335. * @param fileList OCA.Files.FileList instance on which the action is
  336. * done, defaults to OCA.Files.App.fileList
  337. */
  338. display: function (parent, triggerEvent, fileList) {
  339. if (!fileList) {
  340. console.warn('FileActions.display() MUST be called with a OCA.Files.FileList instance');
  341. return;
  342. }
  343. this.currentFile = parent;
  344. var self = this;
  345. var $tr = parent.closest('tr');
  346. var actions = this.getActions(
  347. this.getCurrentMimeType(),
  348. this.getCurrentType(),
  349. this.getCurrentPermissions()
  350. );
  351. var nameLinks;
  352. if ($tr.data('renaming')) {
  353. return;
  354. }
  355. // recreate fileactions container
  356. nameLinks = parent.children('a.name');
  357. nameLinks.find('.fileactions, .nametext .action').remove();
  358. nameLinks.append('<span class="fileactions" />');
  359. var defaultAction = this.getDefault(
  360. this.getCurrentMimeType(),
  361. this.getCurrentType(),
  362. this.getCurrentPermissions()
  363. );
  364. $.each(actions, function (name, actionSpec) {
  365. if (name !== 'Share') {
  366. self._renderAction(
  367. actionSpec,
  368. actionSpec.action === defaultAction, {
  369. $file: $tr,
  370. fileActions: this,
  371. fileList : fileList
  372. }
  373. );
  374. }
  375. });
  376. // added here to make sure it's always the last action
  377. var shareActionSpec = actions.Share;
  378. if (shareActionSpec){
  379. this._renderAction(
  380. shareActionSpec,
  381. shareActionSpec.action === defaultAction, {
  382. $file: $tr,
  383. fileActions: this,
  384. fileList: fileList
  385. }
  386. );
  387. }
  388. if (triggerEvent){
  389. fileList.$fileList.trigger(jQuery.Event("fileActionsReady", {fileList: fileList, $files: $tr}));
  390. }
  391. },
  392. getCurrentFile: function () {
  393. return this.currentFile.parent().attr('data-file');
  394. },
  395. getCurrentMimeType: function () {
  396. return this.currentFile.parent().attr('data-mime');
  397. },
  398. getCurrentType: function () {
  399. return this.currentFile.parent().attr('data-type');
  400. },
  401. getCurrentPermissions: function () {
  402. return this.currentFile.parent().data('permissions');
  403. },
  404. /**
  405. * Register the actions that are used by default for the files app.
  406. */
  407. registerDefaultActions: function() {
  408. this.registerAction({
  409. name: 'Delete',
  410. displayName: '',
  411. mime: 'all',
  412. permissions: OC.PERMISSION_DELETE,
  413. icon: function() {
  414. return OC.imagePath('core', 'actions/delete');
  415. },
  416. render: _.bind(this._renderDeleteAction, this),
  417. actionHandler: function(fileName, context) {
  418. context.fileList.do_delete(fileName, context.dir);
  419. $('.tipsy').remove();
  420. }
  421. });
  422. // t('files', 'Rename')
  423. this.registerAction({
  424. name: 'Rename',
  425. displayName: '',
  426. mime: 'all',
  427. permissions: OC.PERMISSION_UPDATE,
  428. icon: function() {
  429. return OC.imagePath('core', 'actions/rename');
  430. },
  431. render: _.bind(this._renderRenameAction, this),
  432. actionHandler: function (filename, context) {
  433. context.fileList.rename(filename);
  434. }
  435. });
  436. this.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) {
  437. var dir = context.$file.attr('data-path') || context.fileList.getCurrentDirectory();
  438. if (dir !== '/') {
  439. dir = dir + '/';
  440. }
  441. context.fileList.changeDirectory(dir + filename);
  442. });
  443. this.setDefault('dir', 'Open');
  444. this.register('all', 'Download', OC.PERMISSION_READ, function () {
  445. return OC.imagePath('core', 'actions/download');
  446. }, function (filename, context) {
  447. var dir = context.dir || context.fileList.getCurrentDirectory();
  448. var url = context.fileList.getDownloadUrl(filename, dir);
  449. if (url) {
  450. OC.redirect(url);
  451. }
  452. }, t('files', 'Download'));
  453. }
  454. };
  455. OCA.Files.FileActions = FileActions;
  456. /**
  457. * File action attributes.
  458. *
  459. * @todo make this a real class in the future
  460. * @typedef {Object} OCA.Files.FileAction
  461. *
  462. * @property {String} name identifier of the action
  463. * @property {String} displayName display name of the action, defaults
  464. * to the name given in name property
  465. * @property {String} mime mime type
  466. * @property {int} permissions permissions
  467. * @property {(Function|String)} icon icon path to the icon or function
  468. * that returns it
  469. * @property {OCA.Files.FileActions~renderActionFunction} [render] optional rendering function
  470. * @property {OCA.Files.FileActions~actionHandler} actionHandler action handler function
  471. */
  472. /**
  473. * File action context attributes.
  474. *
  475. * @typedef {Object} OCA.Files.FileActionContext
  476. *
  477. * @property {Object} $file jQuery file row element
  478. * @property {OCA.Files.FileActions} fileActions file actions object
  479. * @property {OCA.Files.FileList} fileList file list object
  480. */
  481. /**
  482. * Render function for actions.
  483. * The function must render a link element somewhere in the DOM
  484. * and return it. The function should NOT register the event handler
  485. * as this will be done after the link was returned.
  486. *
  487. * @callback OCA.Files.FileActions~renderActionFunction
  488. * @param {OCA.Files.FileAction} actionSpec action definition
  489. * @param {Object} $row row container
  490. * @param {boolean} isDefault true if the action is the default one,
  491. * false otherwise
  492. * @return {Object} jQuery link object
  493. */
  494. /**
  495. * Action handler function for file actions
  496. *
  497. * @callback OCA.Files.FileActions~actionHandler
  498. * @param {String} fileName name of the clicked file
  499. * @param context context
  500. * @param {String} context.dir directory of the file
  501. * @param context.$file jQuery element of the file
  502. * @param {OCA.Files.FileList} context.fileList the FileList instance on which the action occurred
  503. * @param {OCA.Files.FileActions} context.fileActions the FileActions instance on which the action occurred
  504. */
  505. // global file actions to be used by all lists
  506. OCA.Files.fileActions = new OCA.Files.FileActions();
  507. OCA.Files.legacyFileActions = new OCA.Files.FileActions();
  508. // for backward compatibility
  509. //
  510. // legacy apps are expecting a stateful global FileActions object to register
  511. // their actions on. Since legacy apps are very likely to break with other
  512. // FileList views than the main one ("All files"), actions registered
  513. // through window.FileActions will be limited to the main file list.
  514. // @deprecated use OCA.Files.FileActions instead
  515. window.FileActions = OCA.Files.legacyFileActions;
  516. window.FileActions.register = function (mime, name, permissions, icon, action, displayName) {
  517. console.warn('FileActions.register() is deprecated, please use OCA.Files.fileActions.register() instead', arguments);
  518. OCA.Files.FileActions.prototype.register.call(
  519. window.FileActions, mime, name, permissions, icon, action, displayName
  520. );
  521. };
  522. window.FileActions.display = function (parent, triggerEvent, fileList) {
  523. fileList = fileList || OCA.Files.App.fileList;
  524. console.warn('FileActions.display() is deprecated, please use OCA.Files.fileActions.register() which automatically redisplays actions', mime, name);
  525. OCA.Files.FileActions.prototype.display.call(window.FileActions, parent, triggerEvent, fileList);
  526. };
  527. })();