瀏覽代碼

Add support for separe group objects and collection

JC Brand 11 年之前
父節點
當前提交
ca260f7b4f
共有 2 個文件被更改,包括 147 次插入81 次删除
  1. 146 78
      converse.js
  2. 1 3
      spec/controlbox.js

+ 146 - 78
converse.js

@@ -140,6 +140,9 @@
             (typeof DSA !== "undefined")
         );
 
+        var OPENED = 'opened';
+        var CLOSED = 'closed';
+
         // Default configuration values
         // ----------------------------
         this.allow_contact_requests = true;
@@ -281,9 +284,18 @@
             'away': __('This contact is away')
         };
         var DESC_GROUP_TOGGLE = __('Click to hide these contacts');
+
         var HEADER_CURRENT_CONTACTS =  __('My contacts');
+        var HEADER_PENDING_CONTACTS = __('Pending contacts');
+        var HEADER_REQUESTING_CONTACTS = __('Contact requests');
         var HEADER_UNGROUPED = __('Ungrouped');
 
+        var HEADER_WEIGHTS = {};
+        HEADER_WEIGHTS[HEADER_CURRENT_CONTACTS]    = 0;
+        HEADER_WEIGHTS[HEADER_UNGROUPED]           = 1;
+        HEADER_WEIGHTS[HEADER_REQUESTING_CONTACTS] = 2;
+        HEADER_WEIGHTS[HEADER_PENDING_CONTACTS]    = 3;
+
         // Module-level variables
         // ----------------------
         this.callback = callback || function () {};
@@ -3251,13 +3263,100 @@
             }
         });
 
-        this.RosterView = Backbone.View.extend({
-            tagName: 'dl',
-            id: 'converse-roster',
+        this.RosterGroup = Backbone.Model.extend({
+            initialize: function (attributes, options) {
+                this.set(_.extend({
+                    description: DESC_GROUP_TOGGLE,
+                    toggle_state: OPENED
+                }, attributes))
+            }
+        });
+
+        this.RosterGroupView = Backbone.View.extend({
             events: {
                 "click a.group-toggle": "toggleGroup"
             },
 
+            render: function () {
+                this.$el.replace(
+                    $(converse.templates.group_header({
+                        label_group: this.model.get('name'),
+                        desc_group_toggle: this.model.get('description'),
+                        toggle_state: this.model.get('state') 
+                    }))
+                );
+                return this;
+            },
+
+            _ensureElement: function() {
+                if (!this.el) {
+                    var $el = $(converse.templates.group_header({
+                        label_group: this.model.get('name'),
+                        desc_group_toggle: this.model.get('description'),
+                        toggle_state: this.model.get('state') 
+                    }));
+                    this.setElement($el, false);
+                } else {
+                    this.setElement(_.result(this, 'el'), false);
+                }
+            },
+
+            toggleGroup: function (ev) {
+                if (ev && ev.preventDefault) { ev.preventDefault(); }
+                var $el = $(ev.target);
+                $el.parent().nextUntil('dt').slideToggle();
+                if ($el.hasClass("icon-opened")) {
+                    $el.removeClass("icon-opened").addClass("icon-closed");
+                } else {
+                    $el.removeClass("icon-closed").addClass("icon-opened");
+                }
+            }
+        });
+
+        this.RosterGroups = Backbone.Collection.extend({
+            model: converse.RosterGroup,
+            comparator: function (a, b) {
+                /* Groups are sorted alphabetically, ignoring case.
+                 * However, Ungrouped, Requesting Contacts and Pending Contacts
+                 * appear last and in that order. */
+                a = a.get('name');
+                b = b.get('name');
+                var special_groups = _.keys(HEADER_WEIGHTS);
+                var a_is_special = _.contains(special_groups, a);
+                var b_is_special = _.contains(special_groups, b);
+                if (!a_is_special && !b_is_special ) {
+                    return a.toLowerCase() < b.toLowerCase() ? -1 : (a.toLowerCase() > b.toLowerCase() ? 1 : 0);
+                } else if (a_is_special && b_is_special) {
+                    return HEADER_WEIGHTS[a] < HEADER_WEIGHTS[b] ? -1 : (HEADER_WEIGHTS[a] > HEADER_WEIGHTS[b] ? 1 : 0);
+                } else if (!a_is_special && b_is_special) {
+                    return (b === HEADER_CURRENT_CONTACTS) ? 1 : -1;
+                } else if (a_is_special && !b_is_special) {
+                    return (a === HEADER_CURRENT_CONTACTS) ? -1 : 1;
+                }
+            },
+
+            initialize: function () {
+                this.browserStorage = new Backbone.BrowserStorage[converse.storage](
+                    b64_sha1('converse.roster.groups'+converse.bare_jid));
+            }
+        });
+
+        this.GroupViews = Backbone.Overview.extend({
+
+            initialize: function () {
+                this.model.on("add", this.onAdd, this);
+            },
+
+            onGroupAdd: function (group) {
+                this.add(group.get('name'), group);
+            }
+
+        });
+
+        this.RosterView = Backbone.View.extend({
+            tagName: 'dl',
+            id: 'converse-roster',
+
             initialize: function () {
                 /* If initialize ever gets called again, event listeners will
                  * be registered twice. So we turn them off first.
@@ -3270,31 +3369,16 @@
                 this.model.on("destroy", this.update, this);
                 this.model.on("reset", this.reset, this);
                 this.render();
-                this.model.fetch({add: true}); // Get the cached roster items from localstorage
+
+                this.groupviews = new converse.GroupViews({
+                    model: new converse.RosterGroups()
+                });
+                this.groupviews.model.fetch({add: true});
+                this.model.fetch({add: true});
             },
 
             render: function () {
-                var toggle_state = 'opened', // TODO: remember state...
-                    roster_markup = converse.templates.group_header({
-                        label_group: converse.roster_groups ? HEADER_UNGROUPED : HEADER_CURRENT_CONTACTS,
-                        desc_group_toggle: DESC_GROUP_TOGGLE,
-                        toggle_state: toggle_state
-                    });
-
-                if (converse.allow_contact_requests) {
-                    roster_markup += converse.templates.requesting_contacts({
-                        label_contact_requests: __('Contact requests'),
-                        desc_group_toggle: DESC_GROUP_TOGGLE,
-                        toggle_state: toggle_state
-                    }) +
-                    converse.templates.pending_contacts({
-                        label_pending_contacts: __('Pending contacts'),
-                        desc_group_toggle: DESC_GROUP_TOGGLE,
-                        toggle_state: toggle_state
-                    });
-                }
                 this.$fragment = $('<span>');
-                this.$fragment.append($(roster_markup));
                 return this;
             },
 
@@ -3310,7 +3394,7 @@
 
             insertRosterFragment: function () {
                 if (this.$fragment) {
-                    this.$el.html(this.$fragment)
+                    this.$el.html(this.$fragment.contents())
                     delete this.$fragment;
                 }
                 return this;
@@ -3365,54 +3449,49 @@
                 }
             },
 
-            getGroup: function (name) {
-                var groups, $groups, group_lower, index,
-                    $el = this.getRoster();
-                var $group = $el.find('.roster-group[data-group="'+name+'"]');
-                if ($group.length > 0) {
-                    return $group;
-                }
-                $groups = $el.find('.roster-group');
-                $group = $(converse.templates.group_header({
-                        label_group: name,
-                        desc_group_toggle: DESC_GROUP_TOGGLE,
-                        toggle_state: 'opened' // TODO: remember state...
-                    }));
-                if ($groups.length) {
-                    group_lower = name.toLowerCase();
-                    groups = $.map($groups, function(o) { return o.dataset.group.toLowerCase(); })
-                    groups.push(group_lower);
-                    index = groups.sort().indexOf(group_lower);
-                    if (index == 0) {
-                        $($groups.first()).before($group);
-                    } else if (index == groups.length) {
-                        $($groups.last()).after($group);
-                    } else {
-                        $($groups.eq(index)).before($group);
-                    }
+            positionGroup: function (view) {
+                /* Place the group's DOM element in the correct alphabetical
+                 * position amongst the other groups in the roster.
+                 */
+                var groups = this.groupviews.model;
+                var index = groups.indexOf(view.model);
+                if (index == 0) {
+                    this.getRoster().prepend(view.$el);
+                } else if (index == (groups.length-1)) {
+                    this.getRoster().find('.roster-group').last().siblings('dd').last().after(view.$el);
                 } else {
-                    // This shouldn't actually happen, since
-                    // there's always the Ungrouped.
-                    this.getRoster().append($group);
+                    $(this.getRoster().find('.roster-group').eq(index)).before(view.$el);
                 }
-                return $group;
+                return view;
+            },
+
+            getGroup: function (name) {
+                /* Returns the group view for a group specified by name.
+                 * Creates the view if it doesn't exist.
+                 */
+                var view =  this.groupviews.get(name);
+                if (view) {
+                    return view;
+                }
+                view = new converse.RosterGroupView({
+                    model: this.groupviews.model.create({name: name})
+                });
+                this.groupviews.add(name, view);
+                return this.positionGroup(view)
             },
 
             addCurrentContact: function (item) {
+                var groups;
                 if (converse.roster_groups) {
-                    if (item.get('groups').length === 0) {
-                        this.getRoster().find('.roster-group[data-group="'+HEADER_UNGROUPED+'"]')
-                            .after((new converse.RosterContactView({model: item})).render().el);
-                    } else {
-                        _.each(item.get('groups'), $.proxy(function (group) {
-                            // We need a separate view per group
-                            this.getGroup(group).after((new converse.RosterContactView({model: item})).render().el);
-                        },this));
-                    }
+                    groups = item.get('groups') || [HEADER_UNGROUPED];
                 } else {
-                    this.getRoster().find('.roster-group[data-group="'+HEADER_CURRENT_CONTACTS+'"]')
-                        .after((new converse.RosterContactView({model: item})).render().el);
+                    groups = [HEADER_CURRENT_CONTACTS];
                 }
+                _.each(groups, $.proxy(function (group) {
+                    // We need a separate view per group
+                    this.getGroup(group).$el
+                        .after((new converse.RosterContactView({model: item})).render().el);
+                },this));
             },
 
             addRosterContact: function (item) {
@@ -3422,9 +3501,9 @@
                 } else {
                     view = (new converse.RosterContactView({model: item})).render();
                     if ((item.get('ask') === 'subscribe') || (item.get('subscription') === 'from')) {
-                        this.getRoster().find('#pending-xmpp-contacts').after(view.el);
+                        this.getGroup(HEADER_PENDING_CONTACTS).$el.after(view.el);
                     } else if (item.get('requesting') === true) {
-                        this.getRoster().find('#xmpp-contact-requests').after(view.el);
+                        this.getGroup(HEADER_REQUESTING_CONTACTS).$el.after(view.el);
                     }
                 }
                 return this;
@@ -3474,17 +3553,6 @@
                 return this;
             },
 
-            toggleGroup: function (ev) {
-                if (ev && ev.preventDefault) { ev.preventDefault(); }
-                var $el = $(ev.target);
-                $el.parent().nextUntil('dt').slideToggle();
-                if ($el.hasClass("icon-opened")) {
-                    $el.removeClass("icon-opened").addClass("icon-closed");
-                } else {
-                    $el.removeClass("icon-closed").addClass("icon-opened");
-                }
-            },
-
             sortFunction: function (a, b) {
                 var a_status = a.s[0],
                     a_name =a.s[1],

+ 1 - 3
spec/controlbox.js

@@ -11,18 +11,15 @@
     var checkHeaderToggling = function ($header) {
         var $toggle = $header.find('a.group-toggle');
         expect($header.css('display')).toEqual('block');
-        spyOn(this.rosterview, 'toggleGroup').andCallThrough();
         expect($header.nextUntil('dt', 'dd').length === $header.nextUntil('dt', 'dd:visible').length).toBeTruthy();
         this.rosterview.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
         expect($toggle.hasClass('icon-closed')).toBeFalsy();
         expect($toggle.hasClass('icon-opened')).toBeTruthy();
         $toggle.click();
-        expect(this.rosterview.toggleGroup).toHaveBeenCalled();
         expect($toggle.hasClass('icon-closed')).toBeTruthy();
         expect($toggle.hasClass('icon-opened')).toBeFalsy();
         expect($header.nextUntil('dt', 'dd').length === $header.nextUntil('dt', 'dd:hidden').length).toBeTruthy();
         $toggle.click();
-        expect(this.rosterview.toggleGroup).toHaveBeenCalled();
         expect($toggle.hasClass('icon-closed')).toBeFalsy();
         expect($toggle.hasClass('icon-opened')).toBeTruthy();
         expect($header.nextUntil('dt', 'dd').length === $header.nextUntil('dt', 'dd:visible').length).toBeTruthy();
@@ -172,6 +169,7 @@
                     "colleagues",
                     "Family",
                     "friends & acquaintences",
+                    "ænemies",
                     "Ungrouped",
                     "Contact requests",
                     "Pending contacts"