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

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