/*global define */
define(['jquery', 'underscore', 'backbone', 'collections/UserGroup', 'models/UserModel', 'views/PagerView'],
function($, _, Backbone, UserGroup, UserModel, PagerView) {
'use strict';
/**
* @class GroupListView
* @classdesc Displays a list of UserModels of a UserGroup collection and allows owners to add/remove members from the group
* @classcategory Views
* @screenshot views/GroupListView.png
*/
var GroupListView = Backbone.View.extend(
/** @lends GroupListView.prototype */{
type: "GroupListView",
tagName: "ul",
className: "list-group member-list",
memberEls: [],
/*
* New instances of this view should pass a UserGroup collection in the options object
*/
initialize: function(options){
if(typeof options == "undefined")
var options = {};
this.collection = options.collection || new UserGroup();
this.groupId = this.collection.groupId || null;
this.collapsable = (typeof options.collapsable == "undefined")? true : options.collapsable;
this.showGroupName = (typeof options.showGroupName == "undefined")? true : options.showGroupName;
this.maxItems = options.maxItems || 2;
},
events: {
"click .toggle" : "toggleMemberList",
"click .add-member .submit" : "addToCollection",
"click .remove-member" : "removeFromCollection",
"click .add-owner" : "addOwnerToCollection",
"click .remove-owner" : "removeOwnership",
"keypress input" : "checkForReturn"
},
/*
* The overall list layout is created, with a header and list of members.
* This view listens to the UserGroup collection and updates the member list as
* members are added and removed.
*/
render: function(){
var group = this.collection,
view = this;
//Empty first
this.$el.empty();
//Create the header/first-level of the list
var listItem = $(document.createElement("li")).addClass("list-group-header list-group-item group"),
//icon = $(document.createElement("i")).addClass("icon icon-caret-down tooltip-this group"),
numMembers = $(document.createElement("span")).addClass("num-members").text(group.length),
numMembersLabel = $(document.createElement("span")).text(" members"),
numMembersContainer = $(document.createElement("span")).append(numMembers, numMembersLabel);
if(this.showGroupName){
if(!this.collection.pending){
var link = $(document.createElement("a")).attr("href", MetacatUI.root + "/profile/" + group.groupId).attr("data-subject", group.groupId),
groupName = $(document.createElement("span")).text(group.name);
}
else{
var link = $(document.createElement("a")).attr("href", "#"),
groupName = $(document.createElement("span")).text("");
}
//Put it all together
$(listItem).append($(link).prepend(/*icon, */groupName));
numMembersContainer.prepend(" (").append(")");
}
//Add the member count
this.$el.append(listItem.append(numMembersContainer));
//Save some elements for later use
this.$header = $(listItem);
this.$numMembers = $(numMembers);
this.$groupName = $(groupName);
//Create a list of member names
var view = this;
group.forEach(function(member){
view.addMember(member);
});
//Create a pager for this list if there are many group members
if(group.length > 4){
this.pager = new PagerView({
pages: this.$(".member"),
itemsPerPage: 4,
classes: "list-group-item"
});
this.$el.append(this.pager.render().el);
}
//Add some group controls for the owners
if(group.isOwner(MetacatUI.appUserModel))
this.addControls();
this.listenTo(group, "add", this.addMember);
this.listenTo(group, "remove", this.removeMember);
this.listenTo(group, "change:isOwnerOf", this.addControls);
return this;
},
//-------- Adding members to the group --------//
/*
* The specified UserModel is added to the UI, and if the current user is an owner of the group,
* the owner controls are displayed
*/
addMember: function(member){
var username = member.get("username"),
name = member.get("fullName") || member.get("usernameReadable") || member.get("username");
//If this is the currently-logged-in user, display "Me"
if(username == MetacatUI.appUserModel.get("username"))
name = name + " (Me)";
//Create a list item for this member
var memberListItem = $(document.createElement("li")).addClass("list-group-item member").attr("data-username", username),
memberNameContainer = $(document.createElement("div")).addClass("name-container"),
memberIcon = $(document.createElement("i")).addClass("icon icon-user icon-on-right"),
memberLink = $(document.createElement("a")).attr("href", MetacatUI.root + "/profile/" + username).attr("data-username", username).prepend(memberIcon, name),
memberName = $(document.createElement("span")).addClass("details ellipsis").attr("data-username", username).text(member.get("usernameReadable"));
memberIcon.tooltip({
placement: "top",
trigger: "hover",
title: "Group member"
});
//Put all the elements together
var memberEl = $(memberListItem).append($(memberNameContainer).append(memberLink, memberName));
//Store this element in the view
this.memberEls[member.cid] = memberEl;
//Append after the last member listed
if(this.$(".member").length)
this.$(".member").last().after(memberEl);
//If no members are listed yet, append to the main el
else
this.$el.append(memberEl);
//Add an owner icon for owners of the group or to assign owners to the group
if(this.collection.isOwner(member) || this.collection.isOwner(MetacatUI.appUserModel)){
var ownerIcon = this.getOwnerEl(member);
memberLink.before(ownerIcon);
}
//If the current user is an owner of this group, then display a 'remove member' button - but not for themselves
if(this.collection.isOwner(MetacatUI.appUserModel) && (username.toLowerCase() != MetacatUI.appUserModel.get("username").toLowerCase())){
//Add a remove icon for each member
var removeIcon = $(document.createElement("i")).addClass("icon icon-remove icon-negative remove-member"),
clearfix = $(document.createElement("div")).addClass('clear'),
memberControls = $(document.createElement("div")).addClass("member-controls").append(removeIcon);
removeIcon.tooltip({
trigger : "hover",
placement : "top",
title : "Remove this person from the group"
});
memberNameContainer.addClass("has-member-controls").after(memberControls, clearfix);
}
//Update the header
this.updateHeader();
//Collapse members of this group is necessary
if(this.$el.is(".collapsed"))
this.collapseMember(memberEl);
if(this.pager)
this.pager.update(this.$(".member"));
},
/*
* When the user inputs a username, a UserModel is created and added to the collection.
* The collection is saved to the server. Failed and successful member additions are
* handled and displayed to the user
*/
addToCollection: function(e){
if(e) e.preventDefault();
//Get form values
var username = this.$addMember.find("input[name='username']").val().trim();
var fullName = this.$addMember.find("input[name='fullName']").val().trim();
//Reset the form
this.$addMember.find("input[name='username']").val("");
this.$addMember.find("input[name='fullName']").val("");
if(!username){
this.addMemberNotification({
msg: "You must enter a person's username. Try searching by name or email address.",
status: "error"
});
return;
}
//Is this user already in the collection?
if(this.collection.findWhere({username: username})){
this.addMemberNotification({
msg: fullName + " is already in this group",
status: "error"
});
return;
}
//Don't auto-collapse the list since the user is interacting with the controls right now
this.preventToggle = true;
//Create User Model
var user = new UserModel({
username: username,
fullName: fullName
});
//Add this user to the collection
this.collection.add(user);
//If this is a pending group (in the middle of creation), then don't save it to the server
if(this.collection.pending) return;
//Save this user in the group
var view = this;
var success = function(response){
view.addMemberNotification({
msg: fullName + " added",
status: "success"
});
}
var error = function(response){
if(!fullName) fullName = "that person";
view.addMemberNotification({
msg: "Something went wrong and " + fullName + " could not be added. " +
"Hint: That user may not exist.",
status: "error"
});
//Remove this user from the collection and other storage
view.memberEls[user.cid] = null;
view.collection.remove(user);
}
//Save
this.collection.save(success, error);
},
//-------- Removing members from the group ------//
/*
* When the user clicks on the remove icon, the member is removed from the collection
* and the updated collection is saved to the server
*/
removeFromCollection: function(e){
e.preventDefault();
var username = $(e.target).parents(".member").attr("data-username");
if(!username) return;
if(username.toLowerCase() == MetacatUI.appUserModel.get("username").toLowerCase()){
this.addMemberNotification({
status: "error",
msg: "You can't remove yourself from a group."
});
return;
}
else if(this.collection.length == 1){
this.addMemberNotification({
status: "error",
msg: "You must have at least one member in a group."
});
return;
}
//Remove the member from the collection
var member = this.collection.findWhere({username: username});
this.collection.remove(member);
//Update the header
this.updateHeader();
//Only save the group to the server if its not a pending group
if(!this.collection.pending)
this.collection.save();
},
/*
* Removes the specified member from the UI
*/
removeMember: function(member){
//Get DOM element for this user
var memberEl = this.memberEls[member.cid];
if((typeof memberEl === "undefined") || !memberEl)
memberEl = this.$("li[data-username='" + member.get("username") + "']");
//Remove from page
memberEl.remove();
//Remove this member el from the view storage
this.memberEls[member.cid] = null;
if(this.pager)
this.pager.update(this.$(".member"));
},
//-------------- Displaying UI elements for owners --------------//
/*
* When a user clicks on the add-owner element, this view will add the user as an owner of the
* group and will update the collection. The collection is saved to the server.
*/
addOwnerToCollection: function(e){
if(!e) return;
e.preventDefault();
var view = this;
//Get this member
var username = $(e.target).parents(".member").attr("data-username");
if(!username) return;
var member = this.collection.findWhere({username: username});
//Update ownership
member.get("isOwnerOf").push(this.collection.groupId);
member.trigger("change:isOwnerOf");
//Save
var success = function(){ view.refreshOwner(member); }
this.collection.save(success);
},
/*
* When the user clicks on the remove ownership icon for an owner, the rightsHolder is removed
* from the group and the updated group is saved to the server.
*/
removeOwnership: function(e){
if(!e) return;
e.preventDefault();
var view = this;
//Get this member
var username = $(e.target).parents(".member").attr("data-username");
if(!username) return;
var member = this.collection.findWhere({username: username});
//Make sure we have at least one owner in this group left
var newOwners = _.without( this.collection.getOwners(), member);
if(newOwners.length < 1){
MetacatUI.appView.showAlert("Groups need to have at least one owner.", "aler-error", this.$el, true);
return;
}
//Update the model
var newOwnership = _.without(member.get("isOwnerOf"), view.collection.groupId);
member.set("isOwnerOf", newOwnership);
member.trigger("change:isOwnerOf");
//Save
var success = function(){ view.refreshOwner(member); }
this.collection.save(success);
},
refreshOwner: function(user){
//Get the member element on the page
var memberEl = this.memberEls[user.cid];
if((typeof memberEl === "undefined") || !memberEl)
memberEl = this.$(".member[data-username='" + user.get("username") + "'");
//Replace the owner element with the new one
$(memberEl).find(".owner").tooltip("destroy").replaceWith(this.getOwnerEl(user));
},
getOwnerEl: function(member){
var ownerIcon = $(document.createElement("i")).addClass("icon owner pointer");
if(this.collection.isOwner(member)){
ownerIcon.addClass("icon-star is-owner remove-owner").tooltip({
placement: "top",
trigger: "hover",
title: "Group owner",
delay: {
show: 500
}
});
}
else{
ownerIcon.addClass("icon-star-empty add-owner").tooltip({
placement: "top",
trigger: "hover",
title: "Add this person as a co-owner of the group",
delay: {
show: 500
}
});
}
return ownerIcon;
},
addControls: function(){
if(!MetacatUI.appUserModel.get("loggedIn") || (!this.collection.isOwner(MetacatUI.appUserModel)) || this.$addMember) return;
//Add a form for adding a new member
var addMemberInput = $(document.createElement("input"))
.attr("type", "text")
.attr("name", "username")
.attr("placeholder", "Username or Name (cn=me, o=my org...)")
.attr("data-submit-callback", "addToCollection")
.addClass("input-xlarge account-autocomplete submit-enter"),
addMemberName = $(document.createElement("input"))
.attr("type", "hidden")
.attr("name", "fullName")
.attr("disabled", "disabled"),
addMemberIcon = $(document.createElement("i")).addClass("icon icon-plus"),
addMemberSubmit = $(document.createElement("button")).addClass("btn submit").append(addMemberIcon),
addMemberLabel = $(document.createElement("label")).text("Add Member - Search by username, email, or name OR enter a full username below."),
addMemberMsg = $(document.createElement("div")).addClass("notification")
.append($(document.createElement("i")).addClass("icon"),
$(document.createElement("span")).addClass("msg")),
addMemberForm = $(document.createElement("form")).append(addMemberLabel, addMemberInput, addMemberName, addMemberSubmit, addMemberMsg),
addMemberListItem = $(document.createElement("li")).addClass("list-group-item add-member input-append").append(addMemberForm);
this.$addMember = $(addMemberForm);
this.$addMemberMsg = $(addMemberMsg);
this.$el.append(addMemberListItem);
this.setUpAutocomplete();
},
/*
* Display a notification in the "add member" form space
* Pass an options object with a msg (message string) and status ('success' or 'error')
*/
addMemberNotification: function(options){
if(!options.status) options.status = "success";
if(!options.msg) return;
if(options.status == "success"){
this.$addMemberMsg.addClass("success").removeClass("error")
.children(".icon").addClass("icon-ok").removeClass("icon-remove");
this.$addMemberMsg.children(".msg").text(options.msg);
}
else{
this.$addMemberMsg.removeClass("success").addClass("error")
.children(".icon").removeClass("icon-ok").addClass("icon-remove");
this.$addMemberMsg.children(".msg").text(options.msg);
}
this.$addMemberMsg.show().delay(3000).fadeOut();
},
/*
* Update the header of this group list, which includes the number of members and the group name
*/
updateHeader: function(){
if(this.$numMembers)
this.$numMembers.text(this.collection.length);
if(this.$groupName)
this.$groupName.text(this.collection.name);
},
//----------- Form utilities -------------//
setUpAutocomplete: function() {
var input = this.$(".account-autocomplete");
if(!input || !input.length) return;
var view = this;
// look up registered identities
$(input).hoverAutocomplete({
source: function (request, response) {
var term = $.ui.autocomplete.escapeRegex(request.term);
var list = [];
//Ids/Usernames that we want to ignore in the autocompelte
var ignoreIds = view.collection.pluck("username");
_.each(ignoreIds, function(id){
ignoreIds.push(id.toLowerCase());
});
ignoreIds.push(MetacatUI.appUserModel.get("username").toLowerCase());
var url = MetacatUI.appModel.get("accountsUrl") + "?query=" + encodeURIComponent(term);
var requestSettings = {
url: url,
type: "GET",
success: function(data, textStatus, xhr) {
_.each($(data).find("person"), function(person, i){
var item = {};
item.value = $(person).find("subject").text();
//Ignore certain values
if(_.contains(ignoreIds, item.value.toLowerCase())) return;
item.fullName = $(person).find("fullName").text() || ($(person).find("givenName").text() + " " + $(person).find("familyName").text());
item.label = item.fullName;
//item.label = "<h3>"+item.fullName+"</h3>Google!";
list.push(item);
});
response(list);
}
}
$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
//Send an ORCID search when the search string gets long enough
if(request.term.length > 3)
MetacatUI.appLookupModel.orcidSearch(request, response, false, ignoreIds);
},
select: function(e, ui) {
e.preventDefault();
// set the text field
$(e.target).val(ui.item.value);
$(e.target).parents("form").find("input[name='fullName']").val(ui.item.fullName);
},
position: {
my: "left top",
at: "left bottom",
collision: "none"
}
});
},
checkForReturn: function(e){
if(e.keyCode != 13) return;
if($.contains(e.target, this.$addMember.find("input[name='fullName']"))){
e.preventDefault();
return;
}
else if($(e.target).is(".submit-enter")){
e.preventDefault();
var callback = $(e.target).attr("data-submit-callback");
this[callback]();
return;
}
},
//---------- Collapsing/Expanding the member list --------//
collapseMember: function(memberEl){
if(this.preventToggle || !this.collapsable) return;
$(memberEl).slideUp();
},
collapseMemberList: function(e){
if((this.preventToggle && !e) || !this.collapsable) return;
this.$(".member, .add-member").slideUp().addClass("collapsed");
this.$(".icon.group").addClass("icon-caret-right").removeClass("icon-caret-down");
},
toggleMemberList: function(e){
if(e) e.preventDefault();
else if(this.preventToggle || !this.collapsable) return;
this.$(".member, .add-member").slideToggle().toggleClass("collapsed");
this.$(".icon.group").toggleClass("icon-caret-right icon-caret-down");
},
// ------- When this view is closed --------//
onClose: function(){
this.remove();
}
});
return GroupListView;
});