Commit eabdc739 authored by Steffen van Bergerem's avatar Steffen van Bergerem

Port aspect membership dropdown and hovercards

parent f217a5bc
......@@ -99,7 +99,7 @@ var app = {
setupGlobalViews: function() {
app.hovercard = new app.views.Hovercard();
app.aspectMemberships = new app.views.AspectMembership();
app.aspectMembershipsBlueprint = new app.views.AspectMembershipBlueprint();
app.sidebar = new app.views.Sidebar();
},
......
/**
* this view lets the user (de-)select aspect memberships in the context
* of another users profile or the contact page.
*
* updates to the list of aspects are immediately propagated to the server, and
* the results are dislpayed as flash messages.
*/
app.views.AspectMembershipBlueprint = Backbone.View.extend({
initialize: function() {
// attach event handler, removing any previous instances
var selector = '.dropdown.aspect_membership .dropdown_list > li';
$('body')
.off('click', selector)
.on('click', selector, _.bind(this._clickHandler, this));
this.list_item = null;
this.dropdown = null;
},
// decide what to do when clicked
// -> addMembership
// -> removeMembership
_clickHandler: function(evt) {
this.list_item = $(evt.target);
this.dropdown = this.list_item.parent();
this.list_item.addClass('loading');
if( this.list_item.is('.selected') ) {
var membership_id = this.list_item.data('membership_id');
this.removeMembership(membership_id);
} else {
var aspect_id = this.list_item.data('aspect_id');
var person_id = this.dropdown.data('person_id');
this.addMembership(person_id, aspect_id);
}
return false; // stop the event
},
// return the (short) name of the person associated with the current dropdown
_name: function() {
return this.dropdown.data('person-short-name');
},
// create a membership for the given person in the given aspect
addMembership: function(person_id, aspect_id) {
var aspect_membership = new app.models.AspectMembership({
'person_id': person_id,
'aspect_id': aspect_id
});
aspect_membership.on('sync', this._successSaveCb, this);
aspect_membership.on('error', function() {
this._displayError('aspect_dropdown.error');
}, this);
aspect_membership.save();
},
_successSaveCb: function(aspect_membership) {
var aspect_id = aspect_membership.get('aspect_id');
var membership_id = aspect_membership.get('id');
var li = this.dropdown.find('li[data-aspect_id="'+aspect_id+'"]');
// the user didn't have this person in any aspects before, congratulate them
// on their newly found friendship ;)
if( this.dropdown.find('li.selected').length == 0 ) {
var msg = Diaspora.I18n.t('aspect_dropdown.started_sharing_with', { 'name': this._name() });
Diaspora.page.flashMessages.render({ 'success':true, 'notice':msg });
}
li.attr('data-membership_id', membership_id) // just to be sure...
.data('membership_id', membership_id)
.addClass('selected');
this.updateSummary();
this._done();
},
// show an error flash msg
_displayError: function(msg_id) {
this._done();
this.dropdown.removeClass('active'); // close the dropdown
var msg = Diaspora.I18n.t(msg_id, { 'name': this._name() });
Diaspora.page.flashMessages.render({ 'success':false, 'notice':msg });
},
// remove the membership with the given id
removeMembership: function(membership_id) {
var aspect_membership = new app.models.AspectMembership({
'id': membership_id
});
aspect_membership.on('sync', this._successDestroyCb, this);
aspect_membership.on('error', function() {
this._displayError('aspect_dropdown.error_remove');
}, this);
aspect_membership.destroy();
},
_successDestroyCb: function(aspect_membership) {
var membership_id = aspect_membership.get('id');
var li = this.dropdown.find('li[data-membership_id="'+membership_id+'"]');
li.removeAttr('data-membership_id')
.removeData('membership_id')
.removeClass('selected');
// we just removed the last aspect, inform the user with a flash message
// that he is no longer sharing with that person
if( this.dropdown.find('li.selected').length == 0 ) {
var msg = Diaspora.I18n.t('aspect_dropdown.stopped_sharing_with', { 'name': this._name() });
Diaspora.page.flashMessages.render({ 'success':true, 'notice':msg });
}
this.updateSummary();
this._done();
},
// cleanup tasks after aspect selection
_done: function() {
if( this.list_item ) {
this.list_item.removeClass('loading');
}
},
// refresh the button text to reflect the current aspect selection status
updateSummary: function() {
var btn = this.dropdown.parents('div.aspect_membership').find('.button.toggle');
var aspects_cnt = this.dropdown.find('li.selected').length;
var txt;
if( aspects_cnt == 0 ) {
btn.removeClass('in_aspects');
txt = Diaspora.I18n.t('aspect_dropdown.toggle.zero');
} else {
btn.addClass('in_aspects');
txt = this._pluralSummaryTxt(aspects_cnt);
}
btn.text(txt + '');
},
_pluralSummaryTxt: function(cnt) {
var all_aspects_cnt = this.dropdown.find('li').length;
if( cnt == 1 ) {
return this.dropdown.find('li.selected').first().text();
}
if( cnt == all_aspects_cnt ) {
return Diaspora.I18n.t('aspect_dropdown.all_aspects');
}
return Diaspora.I18n.t('aspect_dropdown.toggle', { 'count':cnt.toString() });
}
});
//= require ./aspects_dropdown_view
/**
* this view lets the user (de-)select aspect memberships in the context
* of another users profile or the contact page.
......@@ -5,15 +7,13 @@
* updates to the list of aspects are immediately propagated to the server, and
* the results are dislpayed as flash messages.
*/
app.views.AspectMembership = Backbone.View.extend({
app.views.AspectMembership = app.views.AspectsDropdown.extend({
initialize: function() {
// attach event handler, removing any previous instances
var selector = '.dropdown.aspect_membership .dropdown_list > li';
$('body')
.off('click', selector)
.on('click', selector, _.bind(this._clickHandler, this));
events: {
"click ul.aspect_membership.dropdown-menu > li.aspect_selector": "_clickHandler"
},
initialize: function() {
this.list_item = null;
this.dropdown = null;
},
......@@ -22,7 +22,7 @@ app.views.AspectMembership = Backbone.View.extend({
// -> addMembership
// -> removeMembership
_clickHandler: function(evt) {
this.list_item = $(evt.target);
this.list_item = $(evt.target).closest('li.aspect_selector');
this.dropdown = this.list_item.parent();
this.list_item.addClass('loading');
......@@ -72,17 +72,16 @@ app.views.AspectMembership = Backbone.View.extend({
}
li.attr('data-membership_id', membership_id) // just to be sure...
.data('membership_id', membership_id)
.addClass('selected');
.data('membership_id', membership_id);
this.updateSummary();
this.updateSummary(li);
this._done();
},
// show an error flash msg
_displayError: function(msg_id) {
this._done();
this.dropdown.removeClass('active'); // close the dropdown
this.dropdown.closest('.aspect_membership_dropdown').removeClass('open'); // close the dropdown
var msg = Diaspora.I18n.t(msg_id, { 'name': this._name() });
Diaspora.page.flashMessages.render({ 'success':false, 'notice':msg });
......@@ -107,8 +106,8 @@ app.views.AspectMembership = Backbone.View.extend({
var li = this.dropdown.find('li[data-membership_id="'+membership_id+'"]');
li.removeAttr('data-membership_id')
.removeData('membership_id')
.removeClass('selected');
.removeData('membership_id');
this.updateSummary(li);
// we just removed the last aspect, inform the user with a flash message
// that he is no longer sharing with that person
......@@ -117,7 +116,6 @@ app.views.AspectMembership = Backbone.View.extend({
Diaspora.page.flashMessages.render({ 'success':true, 'notice':msg });
}
this.updateSummary();
this._done();
},
......@@ -129,33 +127,8 @@ app.views.AspectMembership = Backbone.View.extend({
},
// refresh the button text to reflect the current aspect selection status
updateSummary: function() {
var btn = this.dropdown.parents('div.aspect_membership').find('.button.toggle');
var aspects_cnt = this.dropdown.find('li.selected').length;
var txt;
if( aspects_cnt == 0 ) {
btn.removeClass('in_aspects');
txt = Diaspora.I18n.t('aspect_dropdown.toggle.zero');
} else {
btn.addClass('in_aspects');
txt = this._pluralSummaryTxt(aspects_cnt);
}
btn.text(txt + '');
updateSummary: function(target) {
this._toggleCheckbox(target);
this._updateButton('green');
},
_pluralSummaryTxt: function(cnt) {
var all_aspects_cnt = this.dropdown.find('li').length;
if( cnt == 1 ) {
return this.dropdown.find('li.selected').first().text();
}
if( cnt == all_aspects_cnt ) {
return Diaspora.I18n.t('aspect_dropdown.all_aspects');
}
return Diaspora.I18n.t('aspect_dropdown.toggle', { 'count':cnt.toString() });
}
});
......@@ -103,10 +103,14 @@ app.views.Hovercard = Backbone.View.extend({
// set aspect dropdown
var href = this.href();
href += "/aspect_membership_button"
href += "/aspect_membership_button";
if(gon.bootstrap == true){
href += "?bootstrap=true";
}
$.get(href, function(response) {
self.dropdown_container.html(response);
});
var aspect_membership = new app.views.AspectMembership({el: self.dropdown_container});
},
_positionHovercard: function() {
......
// require ../aspects_dropdown_view
//= require ../aspects_dropdown_view
/*
* Aspects view for the publisher.
......
......@@ -18,7 +18,10 @@
.icon-refresh { display: inline-block;}
.icon-ok { display: none;}
}
a { cursor: pointer; }
a {
cursor: pointer;
padding-left: 10px;
}
}
}
......
......@@ -5,8 +5,25 @@
border: 1px solid darken($button-border-color,20%);
&:hover {
@include button-gradient-hover($creation-blue);
background: $creation-blue;
border: 1px solid darken($button-border-color,35%);
}
}
.btn-group.open > .btn.creation {
background: $creation-blue;
}
.btn.green {
$button-border-color: #aaa;
@include button-gradient($green);
color: $grey;
border: 1px solid darken($button-border-color,20%);
&:hover {
background: $green;
border: 1px solid darken($button-border-color,35%);
}
}
.btn-group.open > .btn.green {
background: $green;
}
......@@ -49,22 +49,25 @@
};
h4 {
margin-top: 0px;
margin-bottom: 0px;
padding-bottom: 0px;
}
a {
color: $blue;
font-weight: bold !important;
font-size: 16px;
a {
color: $blue;
font-weight: bold !important;
}
}
p {
color: $text-grey;
padding-top: 0px;
margin-top: 0px;
margin-bottom: 10px;
margin-bottom: 5px;
}
.btn-group.aspect_membership_dropdown { margin: 0 !important; }
.hovercard_footer {
position: absolute;
bottom: 0;
......
......@@ -160,7 +160,8 @@ class PeopleController < ApplicationController
return render :text => I18n.t('people.person.thats_you') if @person == current_user.person
@contact = current_user.contact_for(@person) || Contact.new
render :partial => 'aspect_membership_dropdown', :locals => {:contact => @contact, :person => @person, :hang => 'left'}
bootstrap = params[:bootstrap] || false
render :partial => 'aspect_membership_dropdown', :locals => {:contact => @contact, :person => @person, :hang => 'left', :bootstrap => bootstrap}
end
private
......
......@@ -3,7 +3,7 @@
# the COPYRIGHT file.
module AspectGlobalHelper
def aspect_membership_dropdown(contact, person, hang, aspect=nil)
def aspect_membership_dropdown(contact, person, hang, aspect=nil, force_bootstrap=false)
aspect_membership_ids = {}
selected_aspects = all_aspects.select{|aspect| contact.in_aspect?(aspect)}
......@@ -12,12 +12,21 @@ module AspectGlobalHelper
aspect_membership_ids[a.id] = record.id
end
render "shared/aspect_dropdown",
:selected_aspects => selected_aspects,
:aspect_membership_ids => aspect_membership_ids,
:person => person,
:hang => hang,
:dropdown_class => "aspect_membership"
if bootstrap? || force_bootstrap
render "aspect_memberships/aspect_membership_dropdown",
:selected_aspects => selected_aspects,
:aspect_membership_ids => aspect_membership_ids,
:person => person,
:hang => hang,
:dropdown_class => "aspect_membership"
else
render "aspect_memberships/aspect_membership_dropdown_blueprint",
:selected_aspects => selected_aspects,
:aspect_membership_ids => aspect_membership_ids,
:person => person,
:hang => hang,
:dropdown_class => "aspect_membership"
end
end
def aspect_dropdown_list_item(aspect, am_id=nil)
......
......@@ -39,7 +39,8 @@ module PeopleHelper
if opts[:to] == :photos
link_to person_image_tag(person, opts[:size]), person_photos_path(person)
else
"<a #{person_href(person)} class='#{opts[:class]}' #{ ("target=" + opts[:target]) if opts[:target]}>
remote_or_hovercard_link = Rails.application.routes.url_helpers.person_path(person).html_safe
"<a href='#{remote_or_hovercard_link}' class='#{opts[:class]}' #{ ("target=" + opts[:target]) if opts[:target]}>
#{person_image_tag(person, opts[:size])}
</a>".html_safe
end
......
.btn-group.aspect_dropdown.aspect_membership_dropdown
%button.btn.btn-small.dropdown-toggle{:class => selected_aspects.size>0 ? "green" : "btn-default", "data-toggle" => "dropdown"}
%span.text
- if selected_aspects.size == all_aspects.size
= t('all_aspects')
- elsif selected_aspects.size == 1
= selected_aspects.first.name
- else
= t('shared.aspect_dropdown.toggle', :count => selected_aspects.size)
%span.caret
%ul.dropdown-menu{:class => ["pull-#{hang}", defined?(dropdown_class) && dropdown_class], :unSelectable => 'on', 'data-person_id' => (person.id if defined?(person) && person), 'data-service_uid' => (service_uid if defined?(service_uid)), 'data-person-short-name' => (person.first_name if defined?(person) && person)}
- for aspect in all_aspects
%li.aspect_selector{ :class => ('selected' if aspect_membership_ids[aspect.id].present?), 'data-aspect_id' => aspect.id, 'data-membership_id' => aspect_membership_ids[aspect.id] }
%a
%span.status_indicator
%i.icon-ok
%i.icon-refresh
%span.text
= aspect.name
- if (dropdown_may_create_new_aspect && defined?(person) && person)
%li.divider
%li.newItem
.add_aspect
= link_to t('contacts.index.add_a_new_aspect'), new_aspect_path(:person_id => person.id, :remote => true), :rel => 'facebox'
-# Copyright (c) 2010-2011, Diaspora Inc. This file is
-# licensed under the Affero General Public License version 3 or later. See
-# the COPYRIGHT file.
.dropdown{:class => ["hang_#{hang}", defined?(dropdown_class) && dropdown_class]}
.button.toggle{:class => ("in_aspects" if selected_aspects.size > 0)}
- if selected_aspects.size == all_aspects.size
......@@ -9,7 +5,7 @@
- elsif selected_aspects.size == 1
= selected_aspects.first.name
- else
= t('.toggle', :count => selected_aspects.size)
= t('shared.aspect_dropdown.toggle', :count => selected_aspects.size)
&#9660;
.wrapper
......
= aspect_membership_dropdown(@contact, @person, 'left')
= aspect_membership_dropdown(@contact, @person, 'left', nil, bootstrap)
......@@ -20,4 +20,20 @@ describe PeopleController do
save_fixture(html_for("body"), "pending_external_people_search")
end
end
describe '#aspect_membership_dropdown' do
before do
sign_in :user, bob
end
it "generates a jasmine fixture using Blueprint", :fixture => true do
get :aspect_membership_dropdown, :person_id => alice.person.guid
save_fixture(html_for("body"), "aspect_membership_dropdown_blueprint")
end
it "generates a jasmine fixture using Bootstrap", :fixture => true do
get :aspect_membership_dropdown, :person_id => alice.person.guid, :bootstrap => true
save_fixture(html_for("body"), "aspect_membership_dropdown_bootstrap")
end
end
end
describe("app.views.AspectMembershipBlueprint", function(){
beforeEach(function() {
spec.loadFixture("aspect_membership_dropdown_blueprint");
this.view = new app.views.AspectMembershipBlueprint();
this.person_id = $('.dropdown_list').data('person_id');
});
it('attaches to the aspect selector', function(){
spyOn($.fn, 'on');
view = new app.views.AspectMembership();
expect($.fn.on).toHaveBeenCalled();
});
context('adding to aspects', function() {
beforeEach(function() {
this.newAspect = $('li:not(.selected)');
this.newAspectId = this.newAspect.data('aspect_id');
});
it('calls "addMembership"', function() {
spyOn(this.view, "addMembership");
this.newAspect.trigger('click');
expect(this.view.addMembership).toHaveBeenCalledWith(this.person_id, this.newAspectId);
});
it('tries to create a new AspectMembership', function() {
spyOn(app.models.AspectMembership.prototype, "save");
this.view.addMembership(1, 2);
expect(app.models.AspectMembership.prototype.save).toHaveBeenCalled();
});
it('displays an error when it fails', function() {
spyOn(this.view, "_displayError");
spyOn(app.models.AspectMembership.prototype, "save").andCallFake(function() {
this.trigger('error');
});
this.view.addMembership(1, 2);
expect(this.view._displayError).toHaveBeenCalledWith('aspect_dropdown.error');
});
});
context('removing from aspects', function(){
beforeEach(function() {
this.oldAspect = $('li.selected');
this.oldMembershipId = this.oldAspect.data('membership_id');
});
it('calls "removeMembership"', function(){
spyOn(this.view, "removeMembership");
this.oldAspect.trigger('click');
expect(this.view.removeMembership).toHaveBeenCalledWith(this.oldMembershipId);
});
it('tries to destroy an AspectMembership', function() {
spyOn(app.models.AspectMembership.prototype, "destroy");
this.view.removeMembership(1);
expect(app.models.AspectMembership.prototype.destroy).toHaveBeenCalled();
});
it('displays an error when it fails', function() {
spyOn(this.view, "_displayError");
spyOn(app.models.AspectMembership.prototype, "destroy").andCallFake(function() {
</