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.
446 lines
12 KiB
446 lines
12 KiB
/* |
|
* Copyright (c) 2015 |
|
* |
|
* This file is licensed under the Affero General Public License version 3 |
|
* or later. |
|
* |
|
* See the COPYING-README file. |
|
* |
|
*/ |
|
|
|
/* global Handlebars */ |
|
|
|
(function(OC) { |
|
|
|
/** |
|
* @class OC.SystemTags.SystemTagsInputField |
|
* @classdesc |
|
* |
|
* Displays a file's system tags |
|
* |
|
*/ |
|
var SystemTagsInputField = OC.Backbone.View.extend( |
|
/** @lends OC.SystemTags.SystemTagsInputField.prototype */ { |
|
|
|
_rendered: false, |
|
|
|
_newTag: null, |
|
|
|
_lastUsedTags: [], |
|
|
|
className: 'systemTagsInputFieldContainer', |
|
|
|
template: function(data) { |
|
return '<input class="systemTagsInputField" type="hidden" name="tags" value=""/>'; |
|
}, |
|
|
|
/** |
|
* Creates a new SystemTagsInputField |
|
* |
|
* @param {Object} [options] |
|
* @param {string} [options.objectType=files] object type for which tags are assigned to |
|
* @param {bool} [options.multiple=false] whether to allow selecting multiple tags |
|
* @param {bool} [options.allowActions=true] whether tags can be renamed/delete within the dropdown |
|
* @param {bool} [options.allowCreate=true] whether new tags can be created |
|
* @param {bool} [options.isAdmin=true] whether the user is an administrator |
|
* @param {Function} options.initSelection function to convert selection to data |
|
*/ |
|
initialize: function(options) { |
|
options = options || {}; |
|
|
|
this._multiple = !!options.multiple; |
|
this._allowActions = _.isUndefined(options.allowActions) || !!options.allowActions; |
|
this._allowCreate = _.isUndefined(options.allowCreate) || !!options.allowCreate; |
|
this._isAdmin = !!options.isAdmin; |
|
|
|
if (_.isFunction(options.initSelection)) { |
|
this._initSelection = options.initSelection; |
|
} |
|
|
|
this.collection = options.collection || OC.SystemTags.collection; |
|
|
|
var self = this; |
|
this.collection.on('change:name remove', function() { |
|
// refresh selection |
|
_.defer(self._refreshSelection); |
|
}); |
|
|
|
_.defer(_.bind(this._getLastUsedTags, this)); |
|
|
|
_.bindAll( |
|
this, |
|
'_refreshSelection', |
|
'_onClickRenameTag', |
|
'_onClickDeleteTag', |
|
'_onSelectTag', |
|
'_onDeselectTag', |
|
'_onSubmitRenameTag' |
|
); |
|
}, |
|
|
|
_getLastUsedTags: function() { |
|
var self = this; |
|
$.ajax({ |
|
type: 'GET', |
|
url: OC.generateUrl('/apps/systemtags/lastused'), |
|
success: function (response) { |
|
self._lastUsedTags = response; |
|
} |
|
}); |
|
}, |
|
|
|
/** |
|
* Refreshes the selection, triggering a call to |
|
* select2's initSelection |
|
*/ |
|
_refreshSelection: function() { |
|
this.$tagsField.select2('val', this.$tagsField.val()); |
|
}, |
|
|
|
/** |
|
* Event handler whenever the user clicked the "rename" action. |
|
* This will display the rename field. |
|
*/ |
|
_onClickRenameTag: function(ev) { |
|
var $item = $(ev.target).closest('.systemtags-item'); |
|
var tagId = $item.attr('data-id'); |
|
var tagModel = this.collection.get(tagId); |
|
|
|
var oldName = tagModel.get('name'); |
|
var $renameForm = $(OC.SystemTags.Templates['result_form']({ |
|
cid: this.cid, |
|
name: oldName, |
|
deleteTooltip: t('core', 'Delete'), |
|
renameLabel: t('core', 'Rename'), |
|
isAdmin: this._isAdmin |
|
})); |
|
$item.find('.label').after($renameForm); |
|
$item.find('.label, .systemtags-actions').addClass('hidden'); |
|
$item.closest('.select2-result').addClass('has-form'); |
|
|
|
$renameForm.find('[title]').tooltip({ |
|
placement: 'bottom', |
|
container: 'body' |
|
}); |
|
$renameForm.find('input').focus().selectRange(0, oldName.length); |
|
return false; |
|
}, |
|
|
|
/** |
|
* Event handler whenever the rename form has been submitted after |
|
* the user entered a new tag name. |
|
* This will submit the change to the server. |
|
* |
|
* @param {Object} ev event |
|
*/ |
|
_onSubmitRenameTag: function(ev) { |
|
ev.preventDefault(); |
|
var $form = $(ev.target); |
|
var $item = $form.closest('.systemtags-item'); |
|
var tagId = $item.attr('data-id'); |
|
var tagModel = this.collection.get(tagId); |
|
var newName = $(ev.target).find('input').val().trim(); |
|
if (newName && newName !== tagModel.get('name')) { |
|
tagModel.save({'name': newName}); |
|
// TODO: spinner, and only change text after finished saving |
|
$item.find('.label').text(newName); |
|
} |
|
$item.find('.label, .systemtags-actions').removeClass('hidden'); |
|
$form.remove(); |
|
$item.closest('.select2-result').removeClass('has-form'); |
|
}, |
|
|
|
/** |
|
* Event handler whenever a tag must be deleted |
|
* |
|
* @param {Object} ev event |
|
*/ |
|
_onClickDeleteTag: function(ev) { |
|
var $item = $(ev.target).closest('.systemtags-item'); |
|
var tagId = $item.attr('data-id'); |
|
this.collection.get(tagId).destroy(); |
|
$(ev.target).tooltip('hide'); |
|
$item.closest('.select2-result').remove(); |
|
// TODO: spinner |
|
return false; |
|
}, |
|
|
|
_addToSelect2Selection: function(selection) { |
|
var data = this.$tagsField.select2('data'); |
|
data.push(selection); |
|
this.$tagsField.select2('data', data); |
|
}, |
|
|
|
/** |
|
* Event handler whenever a tag is selected. |
|
* Also called whenever tag creation is requested through the dummy tag object. |
|
* |
|
* @param {Object} e event |
|
*/ |
|
_onSelectTag: function(e) { |
|
var self = this; |
|
var tag; |
|
if (e.object && e.object.isNew) { |
|
// newly created tag, check if existing |
|
// create a new tag |
|
tag = this.collection.create({ |
|
name: e.object.name.trim(), |
|
userVisible: true, |
|
userAssignable: true, |
|
canAssign: true |
|
}, { |
|
success: function(model) { |
|
self._addToSelect2Selection(model.toJSON()); |
|
self._lastUsedTags.unshift(model.id); |
|
self.trigger('select', model); |
|
}, |
|
error: function(model, xhr) { |
|
if (xhr.status === 409) { |
|
// re-fetch collection to get the missing tag |
|
self.collection.reset(); |
|
self.collection.fetch({ |
|
success: function(collection) { |
|
// find the tag in the collection |
|
var model = collection.where({ |
|
name: e.object.name.trim(), |
|
userVisible: true, |
|
userAssignable: true |
|
}); |
|
if (model.length) { |
|
model = model[0]; |
|
// the tag already exists or was already assigned, |
|
// add it to the list anyway |
|
self._addToSelect2Selection(model.toJSON()); |
|
self.trigger('select', model); |
|
} |
|
} |
|
}); |
|
} |
|
} |
|
}); |
|
this.$tagsField.select2('close'); |
|
e.preventDefault(); |
|
return false; |
|
} else { |
|
tag = this.collection.get(e.object.id); |
|
this._lastUsedTags.unshift(tag.id); |
|
} |
|
this._newTag = null; |
|
this.trigger('select', tag); |
|
}, |
|
|
|
/** |
|
* Event handler whenever a tag gets deselected. |
|
* |
|
* @param {Object} e event |
|
*/ |
|
_onDeselectTag: function(e) { |
|
this.trigger('deselect', e.choice.id); |
|
}, |
|
|
|
/** |
|
* Autocomplete function for dropdown results |
|
* |
|
* @param {Object} query select2 query object |
|
*/ |
|
_queryTagsAutocomplete: function(query) { |
|
var self = this; |
|
this.collection.fetch({ |
|
success: function(collection) { |
|
var tagModels = collection.filterByName(query.term.trim()); |
|
if (!self._isAdmin) { |
|
tagModels = _.filter(tagModels, function(tagModel) { |
|
return tagModel.get('canAssign'); |
|
}); |
|
} |
|
query.callback({ |
|
results: _.invoke(tagModels, 'toJSON') |
|
}); |
|
} |
|
}); |
|
}, |
|
|
|
_preventDefault: function(e) { |
|
e.stopPropagation(); |
|
}, |
|
|
|
/** |
|
* Formats a single dropdown result |
|
* |
|
* @param {Object} data data to format |
|
* @return {string} HTML markup |
|
*/ |
|
_formatDropDownResult: function(data) { |
|
return OC.SystemTags.Templates['result'](_.extend({ |
|
renameTooltip: t('core', 'Rename'), |
|
allowActions: this._allowActions, |
|
tagMarkup: this._isAdmin ? OC.SystemTags.getDescriptiveTag(data)[0].innerHTML : null, |
|
isAdmin: this._isAdmin |
|
}, data)); |
|
}, |
|
|
|
/** |
|
* Formats a single selection item |
|
* |
|
* @param {Object} data data to format |
|
* @return {string} HTML markup |
|
*/ |
|
_formatSelection: function(data) { |
|
return OC.SystemTags.Templates['selection'](_.extend({ |
|
tagMarkup: this._isAdmin ? OC.SystemTags.getDescriptiveTag(data)[0].innerHTML : null, |
|
isAdmin: this._isAdmin |
|
}, data)); |
|
}, |
|
|
|
/** |
|
* Create new dummy choice for select2 when the user |
|
* types an arbitrary string |
|
* |
|
* @param {string} term entered term |
|
* @return {Object} dummy tag |
|
*/ |
|
_createSearchChoice: function(term) { |
|
term = term.trim(); |
|
if (this.collection.filter(function(entry) { |
|
return entry.get('name') === term; |
|
}).length) { |
|
return; |
|
} |
|
if (!this._newTag) { |
|
this._newTag = { |
|
id: -1, |
|
name: term, |
|
userAssignable: true, |
|
userVisible: true, |
|
canAssign: true, |
|
isNew: true |
|
}; |
|
} else { |
|
this._newTag.name = term; |
|
} |
|
|
|
return this._newTag; |
|
}, |
|
|
|
_initSelection: function(element, callback) { |
|
var self = this; |
|
var ids = $(element).val().split(','); |
|
|
|
function modelToSelection(model) { |
|
var data = model.toJSON(); |
|
if (!self._isAdmin && !data.canAssign) { |
|
// lock static tags for non-admins |
|
data.locked = true; |
|
} |
|
return data; |
|
} |
|
|
|
function findSelectedObjects(ids) { |
|
var selectedModels = self.collection.filter(function(model) { |
|
return ids.indexOf(model.id) >= 0 && (self._isAdmin || model.get('userVisible')); |
|
}); |
|
return _.map(selectedModels, modelToSelection); |
|
} |
|
|
|
this.collection.fetch({ |
|
success: function() { |
|
callback(findSelectedObjects(ids)); |
|
} |
|
}); |
|
}, |
|
|
|
/** |
|
* Renders this details view |
|
*/ |
|
render: function() { |
|
var self = this; |
|
this.$el.html(this.template()); |
|
|
|
this.$el.find('[title]').tooltip({placement: 'bottom'}); |
|
this.$tagsField = this.$el.find('[name=tags]'); |
|
this.$tagsField.select2({ |
|
placeholder: t('core', 'Collaborative tags'), |
|
containerCssClass: 'systemtags-select2-container', |
|
dropdownCssClass: 'systemtags-select2-dropdown', |
|
closeOnSelect: false, |
|
allowClear: false, |
|
multiple: this._multiple, |
|
toggleSelect: this._multiple, |
|
query: _.bind(this._queryTagsAutocomplete, this), |
|
id: function(tag) { |
|
return tag.id; |
|
}, |
|
initSelection: _.bind(this._initSelection, this), |
|
formatResult: _.bind(this._formatDropDownResult, this), |
|
formatSelection: _.bind(this._formatSelection, this), |
|
createSearchChoice: this._allowCreate ? _.bind(this._createSearchChoice, this) : undefined, |
|
sortResults: function(results) { |
|
var selectedItems = _.pluck(self.$tagsField.select2('data'), 'id'); |
|
results.sort(function(a, b) { |
|
var aSelected = selectedItems.indexOf(a.id) >= 0; |
|
var bSelected = selectedItems.indexOf(b.id) >= 0; |
|
if (aSelected === bSelected) { |
|
var aLastUsed = self._lastUsedTags.indexOf(a.id); |
|
var bLastUsed = self._lastUsedTags.indexOf(b.id); |
|
|
|
if (aLastUsed !== bLastUsed) { |
|
if (bLastUsed === -1) { |
|
return -1; |
|
} |
|
if (aLastUsed === -1) { |
|
return 1; |
|
} |
|
return aLastUsed < bLastUsed ? -1 : 1; |
|
} |
|
|
|
// Both not found |
|
return OC.Util.naturalSortCompare(a.name, b.name); |
|
} |
|
if (aSelected && !bSelected) { |
|
return -1; |
|
} |
|
return 1; |
|
}); |
|
return results; |
|
}, |
|
formatNoMatches: function() { |
|
return t('core', 'No tags found'); |
|
} |
|
}) |
|
.on('select2-selecting', this._onSelectTag) |
|
.on('select2-removing', this._onDeselectTag); |
|
|
|
var $dropDown = this.$tagsField.select2('dropdown'); |
|
// register events for inside the dropdown |
|
$dropDown.on('mouseup', '.rename', this._onClickRenameTag); |
|
$dropDown.on('mouseup', '.delete', this._onClickDeleteTag); |
|
$dropDown.on('mouseup', '.select2-result-selectable.has-form', this._preventDefault); |
|
$dropDown.on('submit', '.systemtags-rename-form', this._onSubmitRenameTag); |
|
|
|
this.delegateEvents(); |
|
}, |
|
|
|
remove: function() { |
|
if (this.$tagsField) { |
|
this.$tagsField.select2('destroy'); |
|
} |
|
}, |
|
|
|
getValues: function() { |
|
this.$tagsField.select2('val'); |
|
}, |
|
|
|
setValues: function(values) { |
|
this.$tagsField.select2('val', values); |
|
}, |
|
|
|
setData: function(data) { |
|
this.$tagsField.select2('data', data); |
|
} |
|
}); |
|
|
|
OC.SystemTags = OC.SystemTags || {}; |
|
OC.SystemTags.SystemTagsInputField = SystemTagsInputField; |
|
|
|
})(OC); |
|
|
|
|