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.

systemtagsinputfield.js 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. /*
  2. * Copyright (c) 2015
  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. /* global Handlebars */
  11. (function(OC) {
  12. /**
  13. * @class OC.SystemTags.SystemTagsInputField
  14. * @classdesc
  15. *
  16. * Displays a file's system tags
  17. *
  18. */
  19. var SystemTagsInputField = OC.Backbone.View.extend(
  20. /** @lends OC.SystemTags.SystemTagsInputField.prototype */ {
  21. _rendered: false,
  22. _newTag: null,
  23. _lastUsedTags: [],
  24. className: 'systemTagsInputFieldContainer',
  25. template: function(data) {
  26. return '<input class="systemTagsInputField" type="hidden" name="tags" value=""/>';
  27. },
  28. /**
  29. * Creates a new SystemTagsInputField
  30. *
  31. * @param {Object} [options]
  32. * @param {string} [options.objectType=files] object type for which tags are assigned to
  33. * @param {bool} [options.multiple=false] whether to allow selecting multiple tags
  34. * @param {bool} [options.allowActions=true] whether tags can be renamed/delete within the dropdown
  35. * @param {bool} [options.allowCreate=true] whether new tags can be created
  36. * @param {bool} [options.isAdmin=true] whether the user is an administrator
  37. * @param {Function} options.initSelection function to convert selection to data
  38. */
  39. initialize: function(options) {
  40. options = options || {};
  41. this._multiple = !!options.multiple;
  42. this._allowActions = _.isUndefined(options.allowActions) || !!options.allowActions;
  43. this._allowCreate = _.isUndefined(options.allowCreate) || !!options.allowCreate;
  44. this._isAdmin = !!options.isAdmin;
  45. if (_.isFunction(options.initSelection)) {
  46. this._initSelection = options.initSelection;
  47. }
  48. this.collection = options.collection || OC.SystemTags.collection;
  49. var self = this;
  50. this.collection.on('change:name remove', function() {
  51. // refresh selection
  52. _.defer(self._refreshSelection);
  53. });
  54. _.defer(_.bind(this._getLastUsedTags, this));
  55. _.bindAll(
  56. this,
  57. '_refreshSelection',
  58. '_onClickRenameTag',
  59. '_onClickDeleteTag',
  60. '_onSelectTag',
  61. '_onDeselectTag',
  62. '_onSubmitRenameTag'
  63. );
  64. },
  65. _getLastUsedTags: function() {
  66. var self = this;
  67. $.ajax({
  68. type: 'GET',
  69. url: OC.generateUrl('/apps/systemtags/lastused'),
  70. success: function (response) {
  71. self._lastUsedTags = response;
  72. }
  73. });
  74. },
  75. /**
  76. * Refreshes the selection, triggering a call to
  77. * select2's initSelection
  78. */
  79. _refreshSelection: function() {
  80. this.$tagsField.select2('val', this.$tagsField.val());
  81. },
  82. /**
  83. * Event handler whenever the user clicked the "rename" action.
  84. * This will display the rename field.
  85. */
  86. _onClickRenameTag: function(ev) {
  87. var $item = $(ev.target).closest('.systemtags-item');
  88. var tagId = $item.attr('data-id');
  89. var tagModel = this.collection.get(tagId);
  90. var oldName = tagModel.get('name');
  91. var $renameForm = $(OC.SystemTags.Templates['result_form']({
  92. cid: this.cid,
  93. name: oldName,
  94. deleteTooltip: t('core', 'Delete'),
  95. renameLabel: t('core', 'Rename'),
  96. isAdmin: this._isAdmin
  97. }));
  98. $item.find('.label').after($renameForm);
  99. $item.find('.label, .systemtags-actions').addClass('hidden');
  100. $item.closest('.select2-result').addClass('has-form');
  101. $renameForm.find('[title]').tooltip({
  102. placement: 'bottom',
  103. container: 'body'
  104. });
  105. $renameForm.find('input').focus().selectRange(0, oldName.length);
  106. return false;
  107. },
  108. /**
  109. * Event handler whenever the rename form has been submitted after
  110. * the user entered a new tag name.
  111. * This will submit the change to the server.
  112. *
  113. * @param {Object} ev event
  114. */
  115. _onSubmitRenameTag: function(ev) {
  116. ev.preventDefault();
  117. var $form = $(ev.target);
  118. var $item = $form.closest('.systemtags-item');
  119. var tagId = $item.attr('data-id');
  120. var tagModel = this.collection.get(tagId);
  121. var newName = $(ev.target).find('input').val().trim();
  122. if (newName && newName !== tagModel.get('name')) {
  123. tagModel.save({'name': newName});
  124. // TODO: spinner, and only change text after finished saving
  125. $item.find('.label').text(newName);
  126. }
  127. $item.find('.label, .systemtags-actions').removeClass('hidden');
  128. $form.remove();
  129. $item.closest('.select2-result').removeClass('has-form');
  130. },
  131. /**
  132. * Event handler whenever a tag must be deleted
  133. *
  134. * @param {Object} ev event
  135. */
  136. _onClickDeleteTag: function(ev) {
  137. var $item = $(ev.target).closest('.systemtags-item');
  138. var tagId = $item.attr('data-id');
  139. this.collection.get(tagId).destroy();
  140. $(ev.target).tooltip('hide');
  141. $item.closest('.select2-result').remove();
  142. // TODO: spinner
  143. return false;
  144. },
  145. _addToSelect2Selection: function(selection) {
  146. var data = this.$tagsField.select2('data');
  147. data.push(selection);
  148. this.$tagsField.select2('data', data);
  149. },
  150. /**
  151. * Event handler whenever a tag is selected.
  152. * Also called whenever tag creation is requested through the dummy tag object.
  153. *
  154. * @param {Object} e event
  155. */
  156. _onSelectTag: function(e) {
  157. var self = this;
  158. var tag;
  159. if (e.object && e.object.isNew) {
  160. // newly created tag, check if existing
  161. // create a new tag
  162. tag = this.collection.create({
  163. name: e.object.name.trim(),
  164. userVisible: true,
  165. userAssignable: true,
  166. canAssign: true
  167. }, {
  168. success: function(model) {
  169. self._addToSelect2Selection(model.toJSON());
  170. self._lastUsedTags.unshift(model.id);
  171. self.trigger('select', model);
  172. },
  173. error: function(model, xhr) {
  174. if (xhr.status === 409) {
  175. // re-fetch collection to get the missing tag
  176. self.collection.reset();
  177. self.collection.fetch({
  178. success: function(collection) {
  179. // find the tag in the collection
  180. var model = collection.where({
  181. name: e.object.name.trim(),
  182. userVisible: true,
  183. userAssignable: true
  184. });
  185. if (model.length) {
  186. model = model[0];
  187. // the tag already exists or was already assigned,
  188. // add it to the list anyway
  189. self._addToSelect2Selection(model.toJSON());
  190. self.trigger('select', model);
  191. }
  192. }
  193. });
  194. }
  195. }
  196. });
  197. this.$tagsField.select2('close');
  198. e.preventDefault();
  199. return false;
  200. } else {
  201. tag = this.collection.get(e.object.id);
  202. this._lastUsedTags.unshift(tag.id);
  203. }
  204. this._newTag = null;
  205. this.trigger('select', tag);
  206. },
  207. /**
  208. * Event handler whenever a tag gets deselected.
  209. *
  210. * @param {Object} e event
  211. */
  212. _onDeselectTag: function(e) {
  213. this.trigger('deselect', e.choice.id);
  214. },
  215. /**
  216. * Autocomplete function for dropdown results
  217. *
  218. * @param {Object} query select2 query object
  219. */
  220. _queryTagsAutocomplete: function(query) {
  221. var self = this;
  222. this.collection.fetch({
  223. success: function(collection) {
  224. var tagModels = collection.filterByName(query.term.trim());
  225. if (!self._isAdmin) {
  226. tagModels = _.filter(tagModels, function(tagModel) {
  227. return tagModel.get('canAssign');
  228. });
  229. }
  230. query.callback({
  231. results: _.invoke(tagModels, 'toJSON')
  232. });
  233. }
  234. });
  235. },
  236. _preventDefault: function(e) {
  237. e.stopPropagation();
  238. },
  239. /**
  240. * Formats a single dropdown result
  241. *
  242. * @param {Object} data data to format
  243. * @return {string} HTML markup
  244. */
  245. _formatDropDownResult: function(data) {
  246. return OC.SystemTags.Templates['result'](_.extend({
  247. renameTooltip: t('core', 'Rename'),
  248. allowActions: this._allowActions,
  249. tagMarkup: this._isAdmin ? OC.SystemTags.getDescriptiveTag(data)[0].innerHTML : null,
  250. isAdmin: this._isAdmin
  251. }, data));
  252. },
  253. /**
  254. * Formats a single selection item
  255. *
  256. * @param {Object} data data to format
  257. * @return {string} HTML markup
  258. */
  259. _formatSelection: function(data) {
  260. return OC.SystemTags.Templates['selection'](_.extend({
  261. tagMarkup: this._isAdmin ? OC.SystemTags.getDescriptiveTag(data)[0].innerHTML : null,
  262. isAdmin: this._isAdmin
  263. }, data));
  264. },
  265. /**
  266. * Create new dummy choice for select2 when the user
  267. * types an arbitrary string
  268. *
  269. * @param {string} term entered term
  270. * @return {Object} dummy tag
  271. */
  272. _createSearchChoice: function(term) {
  273. term = term.trim();
  274. if (this.collection.filter(function(entry) {
  275. return entry.get('name') === term;
  276. }).length) {
  277. return;
  278. }
  279. if (!this._newTag) {
  280. this._newTag = {
  281. id: -1,
  282. name: term,
  283. userAssignable: true,
  284. userVisible: true,
  285. canAssign: true,
  286. isNew: true
  287. };
  288. } else {
  289. this._newTag.name = term;
  290. }
  291. return this._newTag;
  292. },
  293. _initSelection: function(element, callback) {
  294. var self = this;
  295. var ids = $(element).val().split(',');
  296. function modelToSelection(model) {
  297. var data = model.toJSON();
  298. if (!self._isAdmin && !data.canAssign) {
  299. // lock static tags for non-admins
  300. data.locked = true;
  301. }
  302. return data;
  303. }
  304. function findSelectedObjects(ids) {
  305. var selectedModels = self.collection.filter(function(model) {
  306. return ids.indexOf(model.id) >= 0 && (self._isAdmin || model.get('userVisible'));
  307. });
  308. return _.map(selectedModels, modelToSelection);
  309. }
  310. this.collection.fetch({
  311. success: function() {
  312. callback(findSelectedObjects(ids));
  313. }
  314. });
  315. },
  316. /**
  317. * Renders this details view
  318. */
  319. render: function() {
  320. var self = this;
  321. this.$el.html(this.template());
  322. this.$el.find('[title]').tooltip({placement: 'bottom'});
  323. this.$tagsField = this.$el.find('[name=tags]');
  324. this.$tagsField.select2({
  325. placeholder: t('core', 'Collaborative tags'),
  326. containerCssClass: 'systemtags-select2-container',
  327. dropdownCssClass: 'systemtags-select2-dropdown',
  328. closeOnSelect: false,
  329. allowClear: false,
  330. multiple: this._multiple,
  331. toggleSelect: this._multiple,
  332. query: _.bind(this._queryTagsAutocomplete, this),
  333. id: function(tag) {
  334. return tag.id;
  335. },
  336. initSelection: _.bind(this._initSelection, this),
  337. formatResult: _.bind(this._formatDropDownResult, this),
  338. formatSelection: _.bind(this._formatSelection, this),
  339. createSearchChoice: this._allowCreate ? _.bind(this._createSearchChoice, this) : undefined,
  340. sortResults: function(results) {
  341. var selectedItems = _.pluck(self.$tagsField.select2('data'), 'id');
  342. results.sort(function(a, b) {
  343. var aSelected = selectedItems.indexOf(a.id) >= 0;
  344. var bSelected = selectedItems.indexOf(b.id) >= 0;
  345. if (aSelected === bSelected) {
  346. var aLastUsed = self._lastUsedTags.indexOf(a.id);
  347. var bLastUsed = self._lastUsedTags.indexOf(b.id);
  348. if (aLastUsed !== bLastUsed) {
  349. if (bLastUsed === -1) {
  350. return -1;
  351. }
  352. if (aLastUsed === -1) {
  353. return 1;
  354. }
  355. return aLastUsed < bLastUsed ? -1 : 1;
  356. }
  357. // Both not found
  358. return OC.Util.naturalSortCompare(a.name, b.name);
  359. }
  360. if (aSelected && !bSelected) {
  361. return -1;
  362. }
  363. return 1;
  364. });
  365. return results;
  366. },
  367. formatNoMatches: function() {
  368. return t('core', 'No tags found');
  369. }
  370. })
  371. .on('select2-selecting', this._onSelectTag)
  372. .on('select2-removing', this._onDeselectTag);
  373. var $dropDown = this.$tagsField.select2('dropdown');
  374. // register events for inside the dropdown
  375. $dropDown.on('mouseup', '.rename', this._onClickRenameTag);
  376. $dropDown.on('mouseup', '.delete', this._onClickDeleteTag);
  377. $dropDown.on('mouseup', '.select2-result-selectable.has-form', this._preventDefault);
  378. $dropDown.on('submit', '.systemtags-rename-form', this._onSubmitRenameTag);
  379. this.delegateEvents();
  380. },
  381. remove: function() {
  382. if (this.$tagsField) {
  383. this.$tagsField.select2('destroy');
  384. }
  385. },
  386. getValues: function() {
  387. this.$tagsField.select2('val');
  388. },
  389. setValues: function(values) {
  390. this.$tagsField.select2('val', values);
  391. },
  392. setData: function(data) {
  393. this.$tagsField.select2('data', data);
  394. }
  395. });
  396. OC.SystemTags = OC.SystemTags || {};
  397. OC.SystemTags.SystemTagsInputField = SystemTagsInputField;
  398. })(OC);