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
* @extends Backbone.View
*/
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;
});