2015-11-30 17:40:17 +03:00
|
|
|
/*
|
|
|
|
* 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) {
|
|
|
|
var TEMPLATE =
|
|
|
|
'<input class="systemTagsInputField" type="hidden" name="tags" value=""/>';
|
|
|
|
|
|
|
|
var RESULT_TEMPLATE =
|
|
|
|
'<span class="systemtags-item{{#if isNew}} new-item{{/if}}" data-id="{{id}}">' +
|
|
|
|
' <span class="checkmark icon icon-checkmark"></span>' +
|
2016-01-21 17:23:49 +03:00
|
|
|
'{{#if isAdmin}}' +
|
|
|
|
' <span class="label">{{{tagMarkup}}}</span>' +
|
|
|
|
'{{else}}' +
|
2015-11-30 17:40:17 +03:00
|
|
|
' <span class="label">{{name}}</span>' +
|
2016-01-21 17:23:49 +03:00
|
|
|
'{{/if}}' +
|
2015-11-30 17:40:17 +03:00
|
|
|
'{{#allowActions}}' +
|
|
|
|
' <span class="systemtags-actions">' +
|
|
|
|
' <a href="#" class="rename icon icon-rename" title="{{renameTooltip}}"></a>' +
|
|
|
|
' </span>' +
|
|
|
|
'{{/allowActions}}' +
|
|
|
|
'</span>';
|
|
|
|
|
2016-01-21 17:23:49 +03:00
|
|
|
var SELECTION_TEMPLATE =
|
|
|
|
'{{#if isAdmin}}' +
|
|
|
|
' <span class="label">{{{tagMarkup}}}</span>' +
|
|
|
|
'{{else}}' +
|
|
|
|
' <span class="label">{{name}}</span>' +
|
2016-01-27 17:49:11 +03:00
|
|
|
'{{/if}}';
|
2016-01-21 17:23:49 +03:00
|
|
|
|
2015-11-30 17:40:17 +03:00
|
|
|
var RENAME_FORM_TEMPLATE =
|
|
|
|
'<form class="systemtags-rename-form">' +
|
|
|
|
' <label class="hidden-visually" for="{{cid}}-rename-input">{{renameLabel}}</label>' +
|
|
|
|
' <input id="{{cid}}-rename-input" type="text" value="{{name}}">' +
|
|
|
|
' <a href="#" class="delete icon icon-delete" title="{{deleteTooltip}}"></a>' +
|
|
|
|
'</form>';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @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,
|
|
|
|
|
|
|
|
className: 'systemTagsInputFieldContainer',
|
|
|
|
|
|
|
|
template: function(data) {
|
|
|
|
if (!this._template) {
|
|
|
|
this._template = Handlebars.compile(TEMPLATE);
|
|
|
|
}
|
|
|
|
return this._template(data);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
2016-01-21 17:23:49 +03:00
|
|
|
* @param {bool} [options.isAdmin=true] whether the user is an administrator
|
2015-11-30 17:40:17 +03:00
|
|
|
* @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;
|
2016-01-21 17:23:49 +03:00
|
|
|
this._isAdmin = !!options.isAdmin;
|
2015-11-30 17:40:17 +03:00
|
|
|
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
|
|
|
|
_.bindAll(
|
|
|
|
this,
|
|
|
|
'_refreshSelection',
|
|
|
|
'_onClickRenameTag',
|
|
|
|
'_onClickDeleteTag',
|
|
|
|
'_onSelectTag',
|
|
|
|
'_onDeselectTag',
|
|
|
|
'_onSubmitRenameTag'
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
if (!this._renameFormTemplate) {
|
|
|
|
this._renameFormTemplate = Handlebars.compile(RENAME_FORM_TEMPLATE);
|
|
|
|
}
|
|
|
|
|
|
|
|
var oldName = tagModel.get('name');
|
|
|
|
var $renameForm = $(this._renameFormTemplate({
|
|
|
|
cid: this.cid,
|
|
|
|
name: oldName,
|
|
|
|
deleteTooltip: t('core', 'Delete'),
|
2016-01-28 16:23:56 +03:00
|
|
|
renameLabel: t('core', 'Rename')
|
2015-11-30 17:40:17 +03:00
|
|
|
}));
|
|
|
|
$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);
|
2016-01-28 16:23:56 +03:00
|
|
|
var newName = $(ev.target).find('input').val().trim();
|
2015-11-30 17:40:17 +03:00
|
|
|
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();
|
|
|
|
$item.closest('.select2-result').remove();
|
|
|
|
// TODO: spinner
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
|
2016-01-27 17:09:59 +03:00
|
|
|
_addToSelect2Selection: function(selection) {
|
|
|
|
var data = this.$tagsField.select2('data');
|
|
|
|
data.push(selection);
|
|
|
|
this.$tagsField.select2('data', data);
|
|
|
|
},
|
|
|
|
|
2015-11-30 17:40:17 +03:00
|
|
|
/**
|
|
|
|
* 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({
|
2016-01-28 16:23:56 +03:00
|
|
|
name: e.object.name.trim(),
|
2015-11-30 17:40:17 +03:00
|
|
|
userVisible: true,
|
|
|
|
userAssignable: true
|
|
|
|
}, {
|
|
|
|
success: function(model) {
|
2016-01-27 17:09:59 +03:00
|
|
|
self._addToSelect2Selection(model.toJSON());
|
2015-11-30 17:40:17 +03:00
|
|
|
self.trigger('select', model);
|
2016-01-27 17:09:59 +03:00
|
|
|
},
|
|
|
|
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
|
2016-01-28 16:23:56 +03:00
|
|
|
var model = collection.where({name: e.object.name.trim(), userVisible: true, userAssignable: true});
|
2016-01-27 17:09:59 +03:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2015-11-30 17:40:17 +03:00
|
|
|
}
|
|
|
|
});
|
|
|
|
this.$tagsField.select2('close');
|
|
|
|
e.preventDefault();
|
|
|
|
return false;
|
|
|
|
} else {
|
|
|
|
tag = this.collection.get(e.object.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({
|
2016-01-21 17:23:49 +03:00
|
|
|
success: function(collection) {
|
2016-01-28 16:23:56 +03:00
|
|
|
var tagModels = collection.filterByName(query.term.trim());
|
2016-01-21 17:23:49 +03:00
|
|
|
if (!self._isAdmin) {
|
|
|
|
tagModels = _.filter(tagModels, function(tagModel) {
|
|
|
|
return tagModel.get('userAssignable');
|
|
|
|
});
|
|
|
|
}
|
2015-11-30 17:40:17 +03:00
|
|
|
query.callback({
|
2016-01-21 17:23:49 +03:00
|
|
|
results: _.invoke(tagModels, 'toJSON')
|
2015-11-30 17:40:17 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
_preventDefault: function(e) {
|
|
|
|
e.stopPropagation();
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Formats a single dropdown result
|
|
|
|
*
|
|
|
|
* @param {Object} data data to format
|
|
|
|
* @return {string} HTML markup
|
|
|
|
*/
|
|
|
|
_formatDropDownResult: function(data) {
|
|
|
|
if (!this._resultTemplate) {
|
|
|
|
this._resultTemplate = Handlebars.compile(RESULT_TEMPLATE);
|
|
|
|
}
|
|
|
|
return this._resultTemplate(_.extend({
|
|
|
|
renameTooltip: t('core', 'Rename'),
|
2016-01-21 17:23:49 +03:00
|
|
|
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) {
|
|
|
|
if (!this._selectionTemplate) {
|
|
|
|
this._selectionTemplate = Handlebars.compile(SELECTION_TEMPLATE);
|
|
|
|
}
|
|
|
|
return this._selectionTemplate(_.extend({
|
|
|
|
tagMarkup: this._isAdmin ? OC.SystemTags.getDescriptiveTag(data)[0].innerHTML : null,
|
|
|
|
isAdmin: this._isAdmin
|
2015-11-30 17:40:17 +03:00
|
|
|
}, 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) {
|
2016-01-28 16:23:56 +03:00
|
|
|
term = term.trim();
|
2015-11-30 17:40:17 +03:00
|
|
|
if (this.collection.filterByName(term).length) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!this._newTag) {
|
|
|
|
this._newTag = {
|
|
|
|
id: -1,
|
|
|
|
name: term,
|
2016-01-21 17:23:49 +03:00
|
|
|
userAssignable: true,
|
|
|
|
userVisible: true,
|
2015-11-30 17:40:17 +03:00
|
|
|
isNew: true
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
this._newTag.name = term;
|
|
|
|
}
|
|
|
|
|
|
|
|
return this._newTag;
|
|
|
|
},
|
|
|
|
|
|
|
|
_initSelection: function(element, callback) {
|
|
|
|
var self = this;
|
|
|
|
var ids = $(element).val().split(',');
|
|
|
|
|
2016-01-21 17:23:49 +03:00
|
|
|
function modelToSelection(model) {
|
|
|
|
var data = model.toJSON();
|
|
|
|
if (!self._isAdmin && !data.userAssignable) {
|
|
|
|
// lock static tags for non-admins
|
|
|
|
data.locked = true;
|
|
|
|
}
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
2015-11-30 17:40:17 +03:00
|
|
|
function findSelectedObjects(ids) {
|
|
|
|
var selectedModels = self.collection.filter(function(model) {
|
2016-01-21 17:23:49 +03:00
|
|
|
return ids.indexOf(model.id) >= 0 && (self._isAdmin || model.get('userVisible'));
|
2015-11-30 17:40:17 +03:00
|
|
|
});
|
2016-01-21 17:23:49 +03:00
|
|
|
return _.map(selectedModels, modelToSelection);
|
2015-11-30 17:40:17 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
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({
|
2016-02-24 10:34:07 +03:00
|
|
|
placeholder: t('core', 'Collaborative tags'),
|
2015-11-30 17:40:17 +03:00
|
|
|
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),
|
2016-01-21 17:23:49 +03:00
|
|
|
formatSelection: _.bind(this._formatSelection, this),
|
2015-11-30 17:40:17 +03:00
|
|
|
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) {
|
|
|
|
return OC.Util.naturalSortCompare(a.name, b.name);
|
|
|
|
}
|
|
|
|
if (aSelected && !bSelected) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
return 1;
|
|
|
|
});
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.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');
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2016-02-08 13:43:42 +03:00
|
|
|
getValues: function() {
|
|
|
|
this.$tagsField.select2('val');
|
|
|
|
},
|
|
|
|
|
2015-11-30 17:40:17 +03:00
|
|
|
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);
|
|
|
|
|