From 6c6ef1f1f9e43b5b75a30e84a53b5f59c1cac2ad Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sun, 17 Dec 2017 15:03:28 +0000 Subject: [PATCH] Refactoring of the roster view. * Removed the dependency on jQuery * Contacts are now shown inside a group element, simplifying the code --- .eslintrc.json | 2 +- css/converse.css | 175 +++++++++-------- css/inverse.css | 175 +++++++++-------- sass/_core.scss | 5 +- sass/_roster.scss | 206 ++++++++++---------- spec/chatbox.js | 42 ++-- spec/chatroom.js | 7 +- spec/controlbox.js | 300 ++++++++++++++++------------- spec/protocol.js | 14 +- src/converse-core.js | 4 +- src/converse-rosterview.js | 332 +++++++++++++++----------------- src/templates/group_header.html | 1 + src/templates/roster.html | 2 +- 13 files changed, 638 insertions(+), 627 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 53765ff08..34585d5b3 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -18,7 +18,7 @@ "lodash/prefer-lodash-method": [2, { "ignoreMethods": [ "find", "endsWith", "startsWith", "filter", "reduce", - "map", "replace", "toLower", "split", "trim", "forEach", "toUpperCase" + "map", "replace", "toLower", "split", "trim", "forEach", "toUpperCase", "includes" ] }], "lodash/prefer-startswith": "off", diff --git a/css/converse.css b/css/converse.css index 89ce7b9eb..be403d7d7 100644 --- a/css/converse.css +++ b/css/converse.css @@ -1203,8 +1203,9 @@ display: none; } #converse-embedded-chat .collapsed, #conversejs .collapsed { - height: 0; - overflow: hidden; } + height: 0 !important; + overflow: hidden !important; + padding: 0 !important; } #converse-embedded-chat .locked, #conversejs .locked { padding-right: 22px; } @@ -2374,103 +2375,101 @@ margin: 0; height: 100%; overflow-x: hidden; - overflow-y: auto; - display: none; } - #conversejs #converse-roster .roster-contacts dt.roster-group { + overflow-y: auto; } + #conversejs #converse-roster .roster-contacts .roster-group { border: none; color: #777; - display: none; font-weight: normal; - margin: 1em 0 0.5em 0; text-shadow: 0 1px 0 #FAFAFA; } - #conversejs #converse-roster .roster-contacts dt.roster-group .group-toggle { + #conversejs #converse-roster .roster-contacts .roster-group .group-toggle { color: #777; display: block; - width: 100%; } - #conversejs #converse-roster .roster-contacts dt.roster-group .group-toggle:hover { + width: 100%; + margin: 0.5em 0; } + #conversejs #converse-roster .roster-contacts .roster-group .group-toggle:hover { color: #585B51; } - #conversejs #converse-roster .roster-contacts dd { - border: none; - clear: both; - color: #777; - display: block; - height: 24px; - overflow-y: hidden; - text-shadow: 0 1px 0 #FAFAFA; - line-height: 14px; - width: 100%; - height: 30px; - margin: 0; - padding: 0.5em 0 0 0; } - #conversejs #converse-roster .roster-contacts dd a:hover { - color: #206485; } - #conversejs #converse-roster .roster-contacts dd .open-chat { - margin: auto; - padding: 0; - width: 85%; } - #conversejs #converse-roster .roster-contacts dd .open-chat.unread-msgs { - font-weight: bold; } - #conversejs #converse-roster .roster-contacts dd .open-chat.unread-msgs .contact-name { - width: 70%; } - #conversejs #converse-roster .roster-contacts dd .open-chat .msgs-indicator { - background-color: #3AA569; - opacity: 1; - border-radius: 10%; - padding: 0 0.2em; - font-size: 12px; } - #conversejs #converse-roster .roster-contacts dd .open-chat .contact-name { + #conversejs #converse-roster .roster-contacts .roster-group li { + border: none; + clear: both; + color: #777; + display: block; + height: 24px; + overflow-y: hidden; + text-shadow: 0 1px 0 #FAFAFA; + line-height: 14px; + width: 100%; + height: 30px; + margin: 0; + padding: 0.5em 0 0 0; } + #conversejs #converse-roster .roster-contacts .roster-group li a:hover { + color: #206485; } + #conversejs #converse-roster .roster-contacts .roster-group li .open-chat { + margin: auto; + padding: 0; + width: 85%; } + #conversejs #converse-roster .roster-contacts .roster-group li .open-chat.unread-msgs { + font-weight: bold; } + #conversejs #converse-roster .roster-contacts .roster-group li .open-chat.unread-msgs .contact-name { + width: 70%; } + #conversejs #converse-roster .roster-contacts .roster-group li .open-chat .msgs-indicator { + background-color: #3AA569; + opacity: 1; + border-radius: 10%; + padding: 0 0.2em; + font-size: 12px; } + #conversejs #converse-roster .roster-contacts .roster-group li .open-chat .contact-name { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + padding: 0; + margin: 0; + max-width: 80%; + float: none; + height: 60px; } + #conversejs #converse-roster .roster-contacts .roster-group li .open-chat .contact-name.unread-msgs { + max-width: 60%; } + #conversejs #converse-roster .roster-contacts .roster-group li .open-chat .avatar { + float: left; + display: inline-block; + height: 60px; } + #conversejs #converse-roster .roster-contacts .roster-group li.requesting-xmpp-contact .request-actions { + padding: 0 0 0 0.3em; + float: right; } + #conversejs #converse-roster .roster-contacts .roster-group li.requesting-xmpp-contact .open-chat { + max-width: 70%; } + #conversejs #converse-roster .roster-contacts .roster-group li.requesting-xmpp-contact .open-chat .req-contact-name { + width: 100%; } + #conversejs #converse-roster .roster-contacts .roster-group li.requesting-xmpp-contact .req-contact-name { + line-height: 16px; + width: 69%; + padding: 0; } + #conversejs #converse-roster .roster-contacts .roster-group li.current-xmpp-contact span { + font-size: 14px; + float: left; + margin-right: 0.5em; } + #conversejs #converse-roster .roster-contacts .roster-group li.odd { + background-color: #DCEAC5; + /* Make this difference */ } + #conversejs #converse-roster .roster-contacts .roster-group li a, #conversejs #converse-roster .roster-contacts .roster-group li span { + display: inline-block; overflow: hidden; white-space: nowrap; - text-overflow: ellipsis; + text-overflow: ellipsis; } + #conversejs #converse-roster .roster-contacts .roster-group li span { padding: 0; + height: 100%; } + #conversejs #converse-roster .roster-contacts .roster-group li a.decline-xmpp-request { + margin-left: 5px; } + #conversejs #converse-roster .roster-contacts .roster-group li a.remove-xmpp-contact { + font-size: 10px; + float: right; margin: 0; - max-width: 80%; - float: none; - height: 60px; } - #conversejs #converse-roster .roster-contacts dd .open-chat .contact-name.unread-msgs { - max-width: 60%; } - #conversejs #converse-roster .roster-contacts dd .open-chat .avatar { - float: left; - display: inline-block; - height: 60px; } - #conversejs #converse-roster .roster-contacts dd.requesting-xmpp-contact .request-actions { - padding: 0 0 0 0.3em; - float: right; } - #conversejs #converse-roster .roster-contacts dd.requesting-xmpp-contact .open-chat { - max-width: 70%; } - #conversejs #converse-roster .roster-contacts dd.requesting-xmpp-contact .open-chat .req-contact-name { - width: 100%; } - #conversejs #converse-roster .roster-contacts dd.requesting-xmpp-contact .req-contact-name { - line-height: 16px; - width: 69%; - padding: 0; } - #conversejs #converse-roster .roster-contacts dd.current-xmpp-contact span { - font-size: 14px; - float: left; - margin-right: 0.5em; } - #conversejs #converse-roster .roster-contacts dd.odd { - background-color: #DCEAC5; - /* Make this difference */ } - #conversejs #converse-roster .roster-contacts dd a, #conversejs #converse-roster .roster-contacts dd span { - display: inline-block; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; } - #conversejs #converse-roster .roster-contacts dd span { - padding: 0; - height: 100%; } - #conversejs #converse-roster .roster-contacts dd a.decline-xmpp-request { - margin-left: 5px; } - #conversejs #converse-roster .roster-contacts dd a.remove-xmpp-contact { - font-size: 10px; - float: right; - margin: 0; - padding: 0; - color: #A8ABA1; } - #conversejs #converse-roster .roster-contacts dd a.remove-xmpp-contact:before { - font-size: 14px; } - #conversejs #converse-roster .roster-contacts dd a.remove-xmpp-contact:hover { - color: #818479; } + padding: 0; + color: #A8ABA1; } + #conversejs #converse-roster .roster-contacts .roster-group li a.remove-xmpp-contact:before { + font-size: 14px; } + #conversejs #converse-roster .roster-contacts .roster-group li a.remove-xmpp-contact:hover { + color: #818479; } #conversejs #converse-roster span.pending-contact-name { line-height: 16px; width: 100%; } diff --git a/css/inverse.css b/css/inverse.css index c7133ec50..c776c15b8 100644 --- a/css/inverse.css +++ b/css/inverse.css @@ -1203,8 +1203,9 @@ display: none; } #converse-embedded-chat .collapsed, #conversejs .collapsed { - height: 0; - overflow: hidden; } + height: 0 !important; + overflow: hidden !important; + padding: 0 !important; } #converse-embedded-chat .locked, #conversejs .locked { padding-right: 22px; } @@ -2533,103 +2534,101 @@ body { margin: 0; height: 100%; overflow-x: hidden; - overflow-y: auto; - display: none; } - #conversejs #converse-roster .roster-contacts dt.roster-group { + overflow-y: auto; } + #conversejs #converse-roster .roster-contacts .roster-group { border: none; color: #777; - display: none; font-weight: normal; - margin: 1em 0 0.5em 0; text-shadow: 0 1px 0 #FAFAFA; } - #conversejs #converse-roster .roster-contacts dt.roster-group .group-toggle { + #conversejs #converse-roster .roster-contacts .roster-group .group-toggle { color: #777; display: block; - width: 100%; } - #conversejs #converse-roster .roster-contacts dt.roster-group .group-toggle:hover { + width: 100%; + margin: 0.5em 0; } + #conversejs #converse-roster .roster-contacts .roster-group .group-toggle:hover { color: #585B51; } - #conversejs #converse-roster .roster-contacts dd { - border: none; - clear: both; - color: #777; - display: block; - height: 24px; - overflow-y: hidden; - text-shadow: 0 1px 0 #FAFAFA; - line-height: 16px; - width: 100%; - height: 30px; - margin: 0; - padding: 0.5em 0 0 0; } - #conversejs #converse-roster .roster-contacts dd a:hover { - color: #206485; } - #conversejs #converse-roster .roster-contacts dd .open-chat { - margin: auto; - padding: 0; - width: 85%; } - #conversejs #converse-roster .roster-contacts dd .open-chat.unread-msgs { - font-weight: bold; } - #conversejs #converse-roster .roster-contacts dd .open-chat.unread-msgs .contact-name { - width: 70%; } - #conversejs #converse-roster .roster-contacts dd .open-chat .msgs-indicator { - background-color: #3AA569; - opacity: 1; - border-radius: 10%; - padding: 0 0.2em; - font-size: 14px; } - #conversejs #converse-roster .roster-contacts dd .open-chat .contact-name { + #conversejs #converse-roster .roster-contacts .roster-group li { + border: none; + clear: both; + color: #777; + display: block; + height: 24px; + overflow-y: hidden; + text-shadow: 0 1px 0 #FAFAFA; + line-height: 16px; + width: 100%; + height: 30px; + margin: 0; + padding: 0.5em 0 0 0; } + #conversejs #converse-roster .roster-contacts .roster-group li a:hover { + color: #206485; } + #conversejs #converse-roster .roster-contacts .roster-group li .open-chat { + margin: auto; + padding: 0; + width: 85%; } + #conversejs #converse-roster .roster-contacts .roster-group li .open-chat.unread-msgs { + font-weight: bold; } + #conversejs #converse-roster .roster-contacts .roster-group li .open-chat.unread-msgs .contact-name { + width: 70%; } + #conversejs #converse-roster .roster-contacts .roster-group li .open-chat .msgs-indicator { + background-color: #3AA569; + opacity: 1; + border-radius: 10%; + padding: 0 0.2em; + font-size: 14px; } + #conversejs #converse-roster .roster-contacts .roster-group li .open-chat .contact-name { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + padding: 0; + margin: 0; + max-width: 80%; + float: none; + height: 30px; } + #conversejs #converse-roster .roster-contacts .roster-group li .open-chat .contact-name.unread-msgs { + max-width: 60%; } + #conversejs #converse-roster .roster-contacts .roster-group li .open-chat .avatar { + float: left; + display: inline-block; + height: 30px; } + #conversejs #converse-roster .roster-contacts .roster-group li.requesting-xmpp-contact .request-actions { + padding: 0 0 0 0.3em; + float: right; } + #conversejs #converse-roster .roster-contacts .roster-group li.requesting-xmpp-contact .open-chat { + max-width: 70%; } + #conversejs #converse-roster .roster-contacts .roster-group li.requesting-xmpp-contact .open-chat .req-contact-name { + width: 100%; } + #conversejs #converse-roster .roster-contacts .roster-group li.requesting-xmpp-contact .req-contact-name { + line-height: 22px; + width: 69%; + padding: 0; } + #conversejs #converse-roster .roster-contacts .roster-group li.current-xmpp-contact span { + font-size: 16px; + float: left; + margin-right: 0.5em; } + #conversejs #converse-roster .roster-contacts .roster-group li.odd { + background-color: #DCEAC5; + /* Make this difference */ } + #conversejs #converse-roster .roster-contacts .roster-group li a, #conversejs #converse-roster .roster-contacts .roster-group li span { + display: inline-block; overflow: hidden; white-space: nowrap; - text-overflow: ellipsis; + text-overflow: ellipsis; } + #conversejs #converse-roster .roster-contacts .roster-group li span { padding: 0; + height: 100%; } + #conversejs #converse-roster .roster-contacts .roster-group li a.decline-xmpp-request { + margin-left: 5px; } + #conversejs #converse-roster .roster-contacts .roster-group li a.remove-xmpp-contact { + font-size: 10px; + float: right; margin: 0; - max-width: 80%; - float: none; - height: 30px; } - #conversejs #converse-roster .roster-contacts dd .open-chat .contact-name.unread-msgs { - max-width: 60%; } - #conversejs #converse-roster .roster-contacts dd .open-chat .avatar { - float: left; - display: inline-block; - height: 30px; } - #conversejs #converse-roster .roster-contacts dd.requesting-xmpp-contact .request-actions { - padding: 0 0 0 0.3em; - float: right; } - #conversejs #converse-roster .roster-contacts dd.requesting-xmpp-contact .open-chat { - max-width: 70%; } - #conversejs #converse-roster .roster-contacts dd.requesting-xmpp-contact .open-chat .req-contact-name { - width: 100%; } - #conversejs #converse-roster .roster-contacts dd.requesting-xmpp-contact .req-contact-name { - line-height: 22px; - width: 69%; - padding: 0; } - #conversejs #converse-roster .roster-contacts dd.current-xmpp-contact span { - font-size: 16px; - float: left; - margin-right: 0.5em; } - #conversejs #converse-roster .roster-contacts dd.odd { - background-color: #DCEAC5; - /* Make this difference */ } - #conversejs #converse-roster .roster-contacts dd a, #conversejs #converse-roster .roster-contacts dd span { - display: inline-block; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; } - #conversejs #converse-roster .roster-contacts dd span { - padding: 0; - height: 100%; } - #conversejs #converse-roster .roster-contacts dd a.decline-xmpp-request { - margin-left: 5px; } - #conversejs #converse-roster .roster-contacts dd a.remove-xmpp-contact { - font-size: 10px; - float: right; - margin: 0; - padding: 0; - color: #A8ABA1; } - #conversejs #converse-roster .roster-contacts dd a.remove-xmpp-contact:before { - font-size: 16px; } - #conversejs #converse-roster .roster-contacts dd a.remove-xmpp-contact:hover { - color: #818479; } + padding: 0; + color: #A8ABA1; } + #conversejs #converse-roster .roster-contacts .roster-group li a.remove-xmpp-contact:before { + font-size: 16px; } + #conversejs #converse-roster .roster-contacts .roster-group li a.remove-xmpp-contact:hover { + color: #818479; } #conversejs #converse-roster span.pending-contact-name { line-height: 22px; width: 100%; } diff --git a/sass/_core.scss b/sass/_core.scss index d1992bf0e..d80866254 100644 --- a/sass/_core.scss +++ b/sass/_core.scss @@ -86,8 +86,9 @@ display: none; } .collapsed { - height: 0; - overflow: hidden; + height: 0 !important; + overflow: hidden !important; + padding: 0 !important; } .locked { diff --git a/sass/_roster.scss b/sass/_roster.scss index 7bea01f99..65299a552 100644 --- a/sass/_roster.scss +++ b/sass/_roster.scss @@ -83,13 +83,10 @@ height: 100%; overflow-x: hidden; overflow-y: auto; - display: none; - dt.roster-group { + .roster-group { border: none; color: $text-color; - display: none; font-weight: normal; - margin: 1em 0 0.5em 0; text-shadow: 0 1px 0 $text-shadow-color; .group-toggle { &:hover { @@ -98,116 +95,117 @@ color: $text-color; display: block; width: 100%; + margin: 0.5em 0;; } - } - dd { - border: none; - clear: both; - color: $text-color; - display: block; - height: 24px; - overflow-y: hidden; - text-shadow: 0 1px 0 $text-shadow-color; - line-height: $font-size; - width: 100%; - height: 30px; - margin: 0; - padding: 0.5em 0 0 0; + li { + border: none; + clear: both; + color: $text-color; + display: block; + height: 24px; + overflow-y: hidden; + text-shadow: 0 1px 0 $text-shadow-color; + line-height: $font-size; + width: 100%; + height: 30px; + margin: 0; + padding: 0.5em 0 0 0; - a:hover { - color: $dark-link-color; - } - - .open-chat { - margin: auto; - padding: 0; - width: 85%; - &.unread-msgs { - font-weight: bold; - .contact-name { - width: 70%; - } + a:hover { + color: $dark-link-color; } - .msgs-indicator { - background-color: $chat-head-color; - opacity: 1; - border-radius: 10%; - padding: 0 0.2em; - font-size: $font-size-small; - } + .open-chat { + margin: auto; + padding: 0; + width: 85%; + &.unread-msgs { + font-weight: bold; + .contact-name { + width: 70%; + } + } - .contact-name { + .msgs-indicator { + background-color: $chat-head-color; + opacity: 1; + border-radius: 10%; + padding: 0 0.2em; + font-size: $font-size-small; + } + + .contact-name { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + padding: 0; + margin: 0; + max-width: 80%; + float: none; + height: $roster-item-height; + &.unread-msgs { + max-width: 60%; + } + } + + .avatar { + float: left; + display: inline-block; + height: $roster-item-height; + } + } + &.requesting-xmpp-contact { + .request-actions { + padding: 0 0 0 0.3em; + float: right; + } + .open-chat { + max-width: 70%; + .req-contact-name { + width: 100%; + } + } + .req-contact-name { + line-height: $line-height; + width: 69%; + padding: 0; + } + } + &.current-xmpp-contact span { + font-size: $font-size; + float: left; + margin-right: 0.5em; + } + &.odd { + background-color: #DCEAC5; + /* Make this difference */ + } + a, span { + display: inline-block; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; + } + span { padding: 0; - margin: 0; - max-width: 80%; - float: none; - height: $roster-item-height; - &.unread-msgs { - max-width: 60%; + height: 100%; + } + a { + &.decline-xmpp-request { + margin-left: 5px; } - } - - .avatar { - float: left; - display: inline-block; - height: $roster-item-height; - } - } - &.requesting-xmpp-contact { - .request-actions { - padding: 0 0 0 0.3em; - float: right; - } - .open-chat { - max-width: 70%; - .req-contact-name { - width: 100%; - } - } - .req-contact-name { - line-height: $line-height; - width: 69%; - padding: 0; - } - } - &.current-xmpp-contact span { - font-size: $font-size; - float: left; - margin-right: 0.5em; - } - &.odd { - background-color: #DCEAC5; - /* Make this difference */ - } - a, span { - display: inline-block; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - span { - padding: 0; - height: 100%; - } - a { - &.decline-xmpp-request { - margin-left: 5px; - } - &.remove-xmpp-contact { - font-size: $font-size-tiny; - float: right; - margin: 0; - padding: 0; - color: $subdued-color; - &:before { - font-size: $font-size; - } - &:hover { - color: $gray-color; + &.remove-xmpp-contact { + font-size: $font-size-tiny; + float: right; + margin: 0; + padding: 0; + color: $subdued-color; + &:before { + font-size: $font-size; + } + &:hover { + color: $gray-color; + } } } } diff --git a/spec/chatbox.js b/spec/chatbox.js index 26f8d91e4..dd8f6ff26 100644 --- a/spec/chatbox.js +++ b/spec/chatbox.js @@ -110,7 +110,7 @@ _converse.rosterview.update(); // XXX: Hack to make sure $roster element is attaced. test_utils.waitUntil(function () { - return _converse.rosterview.$el.find('dt').length; + return _converse.rosterview.$el.find('.roster-group').length; }, 300) .then(function () { // Test that they can be maximized again @@ -243,7 +243,7 @@ test_utils.openControlBox(); test_utils.openContactsPanel(_converse); test_utils.waitUntil(function () { - return _converse.rosterview.$el.find('dt').length; + return _converse.rosterview.$el.find('.roster-group').length; }, 300) .then(function () { var chatbox = test_utils.openChatBoxes(_converse, 1)[0], @@ -281,7 +281,7 @@ test_utils.openControlBox(); test_utils.openContactsPanel(_converse); test_utils.waitUntil(function () { - return _converse.rosterview.$el.find('dt').length; + return _converse.rosterview.$el.find('.roster-group').length; }, 300) .then(function () { var chatbox = test_utils.openChatBoxes(_converse, 1)[0], @@ -328,7 +328,7 @@ test_utils.openControlBox(); test_utils.openContactsPanel(_converse); test_utils.waitUntil(function () { - return _converse.rosterview.$el.find('dt').length; + return _converse.rosterview.$el.find('.roster-group').length; }, 300) .then(function () { spyOn(_converse, 'emit'); @@ -443,7 +443,7 @@ test_utils.openContactsPanel(_converse); test_utils.waitUntil(function () { - return _converse.rosterview.$el.find('dt').length; + return _converse.rosterview.$el.find('.roster-group').length; }, 300) .then(function () { // TODO: More tests can be added here... @@ -546,7 +546,7 @@ test_utils.openControlBox(); test_utils.openContactsPanel(_converse); test_utils.waitUntil(function () { - return _converse.rosterview.$el.find('dt').length; + return _converse.rosterview.$el.find('.roster-group').length; }, 300) .then(function () { spyOn(_converse, 'emit'); @@ -885,7 +885,7 @@ test_utils.openContactsPanel(_converse); test_utils.waitUntil(function () { - return _converse.rosterview.$el.find('dt').length; + return _converse.rosterview.$el.find('.roster-group').length; }, 300) .then(function () { // Send a message from a different resource @@ -1130,7 +1130,7 @@ test_utils.openControlBox(); test_utils.openContactsPanel(_converse); test_utils.waitUntil(function () { - return _converse.rosterview.$el.find('dt').length; + return _converse.rosterview.$el.find('.roster-group').length; }, 300) .then(function () { var contact_name = mock.cur_names[0]; @@ -1191,7 +1191,7 @@ test_utils.openControlBox(); test_utils.openContactsPanel(_converse); test_utils.waitUntil(function () { - return _converse.rosterview.$el.find('dt').length; + return _converse.rosterview.$el.find('.roster-group').length; }, 300) .then(function () { spyOn(_converse, 'emit'); @@ -1503,7 +1503,7 @@ test_utils.openControlBox(); test_utils.openContactsPanel(_converse); test_utils.waitUntil(function () { - return _converse.rosterview.$el.find('dt').length; + return _converse.rosterview.$el.find('.roster-group').length; }, 300).then(function () { spyOn(_converse.connection, 'send'); var contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost'; @@ -1531,7 +1531,7 @@ var contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost'; test_utils.waitUntil(function () { - return _converse.rosterview.$el.find('dt').length; + return _converse.rosterview.$el.find('.roster-group').length; }, 500).then(function () { test_utils.openChatBoxFor(_converse, contact_jid); var view = _converse.chatboxviews.get(contact_jid); @@ -1570,7 +1570,7 @@ test_utils.openControlBox(); test_utils.openContactsPanel(_converse); test_utils.waitUntil(function () { - return _converse.rosterview.$el.find('dt').length; + return _converse.rosterview.$el.find('.roster-group').length; }, 300) .then(function () { var contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost'; @@ -1694,7 +1694,7 @@ test_utils.openControlBox(); test_utils.openContactsPanel(_converse); test_utils.waitUntil(function () { - return _converse.rosterview.$el.find('dt').length; + return _converse.rosterview.$el.find('.roster-group').length; }, 300) .then(function () { _converse.TIMEOUTS.PAUSED = 200; // Make the timeout shorter so that we can test @@ -1757,7 +1757,7 @@ test_utils.openControlBox(); test_utils.openContactsPanel(_converse); test_utils.waitUntil(function () { - return _converse.rosterview.$el.find('dt').length; + return _converse.rosterview.$el.find('.roster-group').length; }, 300) .then(function () { // TODO: only show paused state if the previous state was composing @@ -1842,7 +1842,7 @@ test_utils.openControlBox(); test_utils.openContactsPanel(_converse); test_utils.waitUntil(function () { - return _converse.rosterview.$el.find('dt').length; + return _converse.rosterview.$el.find('.roster-group').length; }, 500).then(function () { // Make the timeouts shorter so that we can test _converse.TIMEOUTS.PAUSED = 200; @@ -1932,7 +1932,7 @@ test_utils.openControlBox(); test_utils.openContactsPanel(_converse); test_utils.waitUntil(function () { - return _converse.rosterview.$el.find('dt').length; + return _converse.rosterview.$el.find('.roster-group').length; }, 300).then(function () { var contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost'; test_utils.openChatBoxFor(_converse, contact_jid); @@ -2327,7 +2327,7 @@ test_utils.createContacts(_converse, 'current'); test_utils.openContactsPanel(_converse); test_utils.waitUntil(function () { - return _converse.rosterview.$el.find('dt').length; + return _converse.rosterview.$el.find('.roster-group').length; }, 500) .then(function () { var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost'; @@ -2360,7 +2360,7 @@ test_utils.createContacts(_converse, 'current'); test_utils.openContactsPanel(_converse); test_utils.waitUntil(function () { - return _converse.rosterview.$el.find('dt').length; + return _converse.rosterview.$el.find('.roster-group').length; }, 500) .then(function () { var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost'; @@ -2394,7 +2394,7 @@ test_utils.createContacts(_converse, 'current'); test_utils.openContactsPanel(_converse); test_utils.waitUntil(function () { - return _converse.rosterview.$el.find('dt').length; + return _converse.rosterview.$el.find('.roster-group').length; }, 500) .then(function () { var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost'; @@ -2429,7 +2429,7 @@ test_utils.createContacts(_converse, 'current'); test_utils.openContactsPanel(_converse); test_utils.waitUntil(function () { - return _converse.rosterview.$el.find('dt').length; + return _converse.rosterview.$el.find('.roster-group').length; }, 500) .then(function () { var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost'; @@ -2462,7 +2462,7 @@ test_utils.createContacts(_converse, 'current'); test_utils.openContactsPanel(_converse); test_utils.waitUntil(function () { - return _converse.rosterview.$el.find('dt').length; + return _converse.rosterview.$el.find('.roster-group').length; }, 500) .then(function () { var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost'; diff --git a/spec/chatroom.js b/spec/chatroom.js index 15cb2d9ba..0d2feb7e4 100644 --- a/spec/chatroom.js +++ b/spec/chatroom.js @@ -72,7 +72,7 @@ test_utils.createContacts(_converse, 'current'); test_utils.waitUntil(function () { - return _converse.rosterview.$el.find('dt').length; + return _converse.rosterview.$el.find('.roster-group .group-toggle').length; }, 300) .then(function () { test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'dummy').then(function () { @@ -129,9 +129,10 @@ return deferred.promise(); }); + test_utils.openControlBox(); test_utils.createContacts(_converse, 'current'); test_utils.waitUntil(function () { - return _converse.rosterview.$el.find('dt').length; + return _converse.rosterview.$el.find('.roster-group .group-toggle').length; }, 300).then(function () { var jid = 'lounge@localhost'; var room = _converse.api.rooms.open(jid); @@ -256,7 +257,7 @@ ' '+ ' ')[0])); - test_utils.waitUntil(function () { + return test_utils.waitUntil(function () { return sent_IQ.toLocaleString() !== ""; diff --git a/spec/controlbox.js b/spec/controlbox.js index 0463c73bd..ef4aeceeb 100644 --- a/spec/controlbox.js +++ b/spec/controlbox.js @@ -5,21 +5,29 @@ var $pres = converse.env.$pres; var $msg = converse.env.$msg; var $iq = converse.env.$iq; + var u = converse.env.utils; - var checkHeaderToggling = function ($header) { - var $toggle = $header.find('a.group-toggle'); - expect($header.css('display')).toEqual('block'); - expect($header.nextUntil('dt', 'dd').length === $header.nextUntil('dt', 'dd:visible').length).toBeTruthy(); + var checkHeaderToggling = function ($group) { + var $toggle = $group.find('a.group-toggle'); + expect(u.isVisible($group[0])).toBeTruthy(); + expect($group.find('ul.collapsed').length).toBe(0); expect($toggle.hasClass('icon-closed')).toBeFalsy(); expect($toggle.hasClass('icon-opened')).toBeTruthy(); $toggle.click(); - 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($toggle.hasClass('icon-closed')).toBeFalsy(); - expect($toggle.hasClass('icon-opened')).toBeTruthy(); - expect($header.nextUntil('dt', 'dd').length === $header.nextUntil('dt', 'dd:visible').length).toBeTruthy(); + + return test_utils.waitUntil(function () { + return $group.find('ul.collapsed').length === 1; + }, 500).then(function () { + expect($toggle.hasClass('icon-closed')).toBeTruthy(); + expect($toggle.hasClass('icon-opened')).toBeFalsy(); + $toggle.click(); + return test_utils.waitUntil(function () { + return $group.find('li').length === $group.find('li:visible').length + }, 500); + }).then(function () { + expect($toggle.hasClass('icon-closed')).toBeFalsy(); + expect($toggle.hasClass('icon-opened')).toBeTruthy(); + }); }; describe("The Control Box", function () { @@ -149,7 +157,7 @@ }; return test_utils.waitUntil(function () { - if (_converse.rosterview.$roster.hasScrollBar()) { + if ($(_converse.rosterview.roster_el).hasScrollBar()) { return $filter.is(':visible'); } else { return !$filter.is(':visible'); @@ -165,54 +173,64 @@ null, ['rosterGroupsFetched'], {}, function (done, _converse) { - var $filter; - var $roster; _converse.roster_groups = true; test_utils.openControlBox(); test_utils.createGroupedContacts(_converse); - $filter = _converse.rosterview.$('.roster-filter'); - $roster = _converse.rosterview.$roster; + var $filter = _converse.rosterview.$('.roster-filter'); + var $roster = $(_converse.rosterview.roster_el); _converse.rosterview.filter_view.delegateEvents(); var promise = test_utils.waitUntil(function () { - return $roster.find('dd:visible').length === 15; - }, 500) - .then(function (contacts) { - expect($roster.find('dt:visible').length).toBe(5); + return $roster.find('li:visible').length === 15; + }, 500).then(function (contacts) { + expect($roster.find('ul.roster-group-contacts:visible').length).toBe(5); $filter.val("candice"); $filter.trigger('keydown'); return test_utils.waitUntil(function () { - return $roster.find('dd:visible').length === 1; + return $roster.find('li:visible').length === 1; }, 500); }).then(function (contacts) { - expect($roster.find('dd:visible').eq(0).text().trim()).toBe('Candice van der Knijff'); - expect($roster.find('dt:visible').length).toBe(1); - expect(_.trim($roster.find('dt:visible').eq(0).text())).toBe('colleagues'); + // Only one roster contact is now visible + expect($roster.find('li:visible').length).toBe(1); + expect($roster.find('li:visible').eq(0).text().trim()).toBe('Candice van der Knijff'); + // Only one foster group is still visible + expect($roster.find('.roster-group:visible').length).toBe(1); + expect(_.trim($roster.find('.roster-group:visible a.group-toggle').eq(0).text())).toBe('colleagues'); + $filter = _converse.rosterview.$('.roster-filter'); $filter.val("an"); $filter.trigger('keydown'); return test_utils.waitUntil(function () { - return $roster.find('dd:visible').length === 5; + return $roster.find('li:visible').length === 5; }, 500) }).then(function (contacts) { - expect($roster.find('dt:visible').length).toBe(4); + // Five roster contact is now visible + expect($roster.find('li:visible').length).toBe(5); + // Four groups are still visible + var $groups = $roster.find('.roster-group:visible a.group-toggle'); + expect($groups.length).toBe(4); + expect(_.trim($groups.eq(0).text())).toBe('colleagues'); + expect(_.trim($groups.eq(1).text())).toBe('Family'); + expect(_.trim($groups.eq(2).text())).toBe('friends & acquaintences'); + expect(_.trim($groups.eq(3).text())).toBe('ænemies'); + $filter = _converse.rosterview.$('.roster-filter'); $filter.val("xxx"); $filter.trigger('keydown'); return test_utils.waitUntil(function () { - return $roster.find('dd:visible').length === 0; + return $roster.find('li:visible').length === 0; }, 500) }).then(function () { - expect($roster.find('dt:visible').length).toBe(0); + expect($roster.find('ul.roster-group-contacts:visible a.group-toggle').length).toBe(0); $filter = _converse.rosterview.$('.roster-filter'); $filter.val(""); // Check that contacts are shown again, when the filter string is cleared. $filter.trigger('keydown'); return test_utils.waitUntil(function () { - return $roster.find('dd:visible').length === 15; + return $roster.find('li:visible').length === 15; }, 500) }).then(function () { - expect($roster.find('dt:visible').length).toBe(5); + expect($roster.find('ul.roster-group-contacts:visible').length).toBe(5); _converse.roster_groups = false; done(); }); @@ -224,46 +242,49 @@ function (done, _converse) { var $filter; - var $roster; var $type; _converse.roster_groups = true; test_utils.openControlBox(); test_utils.createGroupedContacts(_converse); _converse.rosterview.filter_view.delegateEvents(); $filter = _converse.rosterview.$('.roster-filter'); - $roster = _converse.rosterview.$roster; + var $roster = $(_converse.rosterview.roster_el); $type = _converse.rosterview.$('.filter-type'); $type.val('groups'); - var promise = test_utils.waitUntil(function () { - return $roster.find('dd:visible').length === 15; - }, 500); - promise.then(function () { - expect($roster.find('dt:visible').length).toBe(5); + test_utils.waitUntil(function () { + return $roster.find('li:visible').length === 15; + }, 500).then(function () { + expect($roster.find('div.roster-group:visible a.group-toggle').length).toBe(5); + $filter.val("colleagues"); $filter.trigger('keydown'); return test_utils.waitUntil(function () { - return $roster.find('dt:visible').length === 1; + return $roster.find('div.roster-group:not(.collapsed) a.group-toggle').length === 1; }, 500); }).then(function () { - expect(_.trim($roster.find('dt:visible').eq(0).text())).toBe('colleagues'); - expect($roster.find('dd:visible').length).toBe(3); + expect(_.trim($roster.find('div.roster-group:not(.collapsed) a').eq(0).text())).toBe('colleagues'); + expect($roster.find('div.roster-group:not(.collapsed) li:visible').length).toBe(3); + // Check that all contacts under the group are shown - expect($roster.find('dt:visible').nextUntil('dt', 'dd:hidden').length).toBe(0); + expect($roster.find('div.roster-group:not(.collapsed) li:hidden').length).toBe(0); + $filter = _converse.rosterview.$('.roster-filter'); $filter.val("xxx").trigger('keydown'); return test_utils.waitUntil(function () { - return $roster.find('dd:visible').length === 0; + return $roster.find('div.roster-group.collapsed a.group-toggle').length === 5; }, 700); }).then(function () { - expect($roster.find('dt:visible').length).toBe(0); + expect($roster.find('div.roster-group:not(.collapsed) a').length).toBe(0); + $filter = _converse.rosterview.$('.roster-filter'); $filter.val(""); // Check that groups are shown again, when the filter string is cleared. $filter.trigger('keydown'); return test_utils.waitUntil(function () { - return $roster.find('dd:visible').length === 15; + return $roster.find('div.roster-group.collapsed a.group-toggle').length === 0; }, 500); }).then(function () { - expect($roster.find('dt:visible').length).toBe(5); + expect($roster.find('div.roster-group:not(collapsed)').length).toBe(5); + expect($roster.find('div.roster-group:not(collapsed) li').length).toBe(15); done(); }); })); @@ -302,7 +323,6 @@ function (done, _converse) { var $filter; - var $roster; _converse.roster_groups = true; test_utils.createGroupedContacts(_converse); var jid = mock.cur_names[3].replace(/ /g,'.').toLowerCase() + '@localhost'; @@ -313,21 +333,20 @@ var $type = _converse.rosterview.$('.filter-type'); $type.val('state').trigger('change'); $filter = _converse.rosterview.$('.state-type'); - $roster = _converse.rosterview.$roster; + var $roster = $(_converse.rosterview.roster_el); test_utils.waitUntil(function () { - return $roster.find('dd:visible').length === 15; - }, 500) - .then(function () { - expect($roster.find('dt:visible').length).toBe(5); + return $roster.find('li:visible').length === 15; + }, 500).then(function () { + expect($roster.find('ul.roster-group-contacts:visible').length).toBe(5); $filter.val("online"); $filter.trigger('change'); return test_utils.waitUntil(function () { - return $roster.find('dd:visible').length === 1; + return $roster.find('li:visible').length === 1; }, 500) }).then(function () { - expect($roster.find('dd:visible').eq(0).text().trim()).toBe('Rinse Sommer'); - expect($roster.find('dt:visible').length).toBe(1); + expect($roster.find('li:visible').eq(0).text().trim()).toBe('Rinse Sommer'); + expect($roster.find('ul.roster-group-contacts:visible').length).toBe(1); var $type = _converse.rosterview.$('.filter-type'); $type.val('contacts').trigger('change'); done(); @@ -346,16 +365,19 @@ spyOn(_converse, 'emit'); spyOn(_converse.rosterview, 'update').and.callThrough(); _converse.rosterview.render(); + test_utils.openControlBox(); test_utils.createContacts(_converse, 'pending'); test_utils.createContacts(_converse, 'requesting'); test_utils.createGroupedContacts(_converse); // Check that the groups appear alphabetically and that // requesting and pending contacts are last. test_utils.waitUntil(function () { - return _converse.rosterview.$el.find('dt').length; - }, 500) - .then(function () { - var group_titles = $.map(_converse.rosterview.$el.find('dt'), function (o) { return $(o).text().trim(); }); + return _converse.rosterview.$el.find('.roster-group:visible a.group-toggle').length; + }, 500).then(function () { + var group_titles = $.map( + _converse.rosterview.$el.find('.roster-group:visible a.group-toggle'), + function (o) { return $(o).text().trim(); } + ); expect(group_titles).toEqual([ "Contact requests", "colleagues", @@ -367,7 +389,7 @@ ]); // Check that usernames appear alphabetically per group _.each(_.keys(mock.groups), function (name) { - var $contacts = _converse.rosterview.$('dt.roster-group[data-group="'+name+'"]').nextUntil('dt', 'dd'); + var $contacts = _converse.rosterview.$('.roster-group[data-group="'+name+'"] ul'); var names = $.map($contacts, function (o) { return $(o).text().trim(); }); expect(names).toEqual(_.clone(names).sort()); }); @@ -384,6 +406,7 @@ var groups = ['colleagues', 'friends']; spyOn(_converse, 'emit'); spyOn(_converse.rosterview, 'update').and.callThrough(); + test_utils.openControlBox(); _converse.rosterview.render(); for (var i=0; i { this.fetch({ - add: true, + 'add': true, + 'silent': true, success (collection) { if (collection.length === 0) { _converse.send_initial_presence = true; @@ -1429,6 +1430,7 @@ this.RosterGroup = Backbone.Model.extend({ + initialize (attributes) { this.set(_.assignIn({ description: __('Click to hide these contacts'), diff --git a/src/converse-rosterview.js b/src/converse-rosterview.js index 197b3f919..ff5a96700 100644 --- a/src/converse-rosterview.js +++ b/src/converse-rosterview.js @@ -7,8 +7,7 @@ /*global define */ (function (root, factory) { - define(["jquery.noconflict", - "converse-core", + define(["converse-core", "tpl!group_header", "tpl!pending_contact", "tpl!requesting_contact", @@ -18,7 +17,6 @@ "converse-chatboxes" ], factory); }(this, function ( - $, converse, tpl_group_header, tpl_pending_contact, @@ -27,7 +25,8 @@ tpl_roster_filter, tpl_roster_item) { "use strict"; - const { Backbone, utils, Strophe, $iq, b64_sha1, sizzle, _ } = converse.env; + const { Backbone, Strophe, $iq, b64_sha1, sizzle, _ } = converse.env; + const u = converse.env.utils; converse.plugins.add('converse-rosterview', { @@ -246,14 +245,14 @@ }, show () { - if (utils.isVisible(this.el)) { return this; } + if (u.isVisible(this.el)) { return this; } this.el.classList.add('fade-in'); this.el.classList.remove('hidden'); return this; }, hide () { - if (!utils.isVisible(this.el)) { return this; } + if (!u.isVisible(this.el)) { return this; } this.model.save({ 'filter_text': '', 'chat_state': '' @@ -278,22 +277,24 @@ id: 'converse-roster', initialize () { - _converse.roster.on("add", this.onContactAdd, this); + _converse.roster.on("add", this.onContactAdded, this); _converse.roster.on('change', this.onContactChange, this); _converse.roster.on("destroy", this.update, this); _converse.roster.on("remove", this.update, this); - this.model.on("add", this.onGroupAdd, this); + this.model.on("add", this.onGroupAdded, this); this.model.on("reset", this.reset, this); _converse.on('rosterGroupsFetched', this.positionFetchedGroups, this); - _converse.on('rosterContactsFetched', this.update, this); + _converse.on('rosterContactsFetched', () => { + _converse.roster.each(this.onContactAdded.bind(this)); + this.update(); + }); this.createRosterFilter(); }, render () { - this.renderRoster(); this.el.innerHTML = ""; this.el.appendChild(this.filter_view.render().el); - + this.renderRoster(); if (!_converse.allow_contact_requests) { // XXX: if we ever support live editing of config then // we'll need to be able to remove this class on the fly. @@ -303,8 +304,10 @@ }, renderRoster () { - this.$roster = $(tpl_roster()); - this.roster = this.$roster[0]; + const div = document.createElement('div'); + div.insertAdjacentHTML('beforeend', tpl_roster()); + this.roster_el = div.firstChild; + this.el.insertAdjacentElement('beforeend', this.roster_el); }, createRosterFilter () { @@ -334,14 +337,14 @@ }, 100), update: _.debounce(function () { - if (_.isNull(this.roster.parentElement)) { - this.$el.append(this.$roster.show()); + if (!u.isVisible(this.roster_el)) { + u.showElement(this.roster_el); } return this.showHideFilter(); }, _converse.animate ? 100 : 0), showHideFilter () { - if (!utils.isVisible(this.el)) { + if (!u.isVisible(this.el)) { return; } this.filter_view.showOrHide(); @@ -361,9 +364,9 @@ if (type === 'groups') { _.each(this.getAll(), function (view, idx) { if (!_.includes(view.model.get('name').toLowerCase(), query.toLowerCase())) { - view.hide(); + u.slideIn(view.el); } else if (view.model.contacts.length > 0) { - view.show(); + u.slideOut(view.el); } }); } else { @@ -376,18 +379,17 @@ reset () { _converse.roster.reset(); this.removeAll(); - this.renderRoster(); this.render().update(); return this; }, - onGroupAdd (group) { + onGroupAdded (group) { const view = new _converse.RosterGroupView({model: group}); - this.add(group.get('name'), view.render()); - this.positionGroup(view); + this.add(group.get('name'), view); + this.positionGroup(group); }, - onContactAdd (contact) { + onContactAdded (contact) { this.addRosterContact(contact).update(); this.updateFilter(); }, @@ -435,47 +437,28 @@ * positioned aren't already in inserted into the * roster DOM element. */ - const that = this; this.model.sort(); - this.model.each(function (group, idx) { - let view = that.get(group.get('name')); - if (!view) { - view = new _converse.RosterGroupView({model: group}); - that.add(group.get('name'), view.render()); - } - if (idx === 0) { - that.$roster.append(view.$el); - } else { - that.appendGroup(view); - } - }); + this.model.each(this.onGroupAdded.bind(this)); }, - positionGroup (view) { + positionGroup (group) { /* Place the group's DOM element in the correct alphabetical * position amongst the other groups in the roster. + * + * NOTE: relies on the assumption that it will be called in + * the right order of appearance of groups. */ - const $groups = this.$roster.find('.roster-group'), - index = $groups.length ? this.model.indexOf(view.model) : 0; + const view = this.get(group.get('name')); + view.render(); + const list = this.roster_el, + index = this.model.indexOf(view.model); if (index === 0) { - this.$roster.prepend(view.$el); + list.insertAdjacentElement('afterbegin', view.el); } else if (index === (this.model.length-1)) { - this.appendGroup(view); + list.insertAdjacentElement('beforeend', view.el); } else { - $($groups.eq(index)).before(view.$el); - } - return this; - }, - - appendGroup (view) { - /* Add the group at the bottom of the roster - */ - const $last = this.$roster.find('.roster-group').last(); - const $siblings = $last.siblings('dd'); - if ($siblings.length > 0) { - $siblings.last().after(view.$el); - } else { - $last.after(view.$el); + const neighbour_el = list.querySelector('div:nth-child('+index+')'); + neighbour_el.insertAdjacentElement('afterend', view.el); } return this; }, @@ -524,7 +507,8 @@ _converse.RosterContactView = Backbone.View.extend({ - tagName: 'dd', + tagName: 'li', + className: 'hidden', events: { "click .accept-xmpp-request": "acceptRequest", @@ -543,7 +527,7 @@ render () { const that = this; if (!this.mayBeShown()) { - this.$el.hide(); + u.hideElement(this.el); return this; } const item = this.model, @@ -564,7 +548,8 @@ that.el.classList.remove(cls); } }); - this.$el.addClass(chat_status).data('status', chat_status); + this.el.classList.add(chat_status); + this.el.setAttribute('data-status', chat_status); if ((ask === 'subscribe') || (subscription === 'from')) { /* ask === 'subscribe' @@ -579,21 +564,21 @@ * So in both cases the user is a "pending" contact. */ this.el.classList.add('pending-xmpp-contact'); - this.$el.html(tpl_pending_contact( + this.el.innerHTML = tpl_pending_contact( _.extend(item.toJSON(), { 'desc_remove': __('Click to remove %1$s as a contact', item.get('fullname')), 'allow_chat_pending_contacts': _converse.allow_chat_pending_contacts }) - )); + ); } else if (requesting === true) { this.el.classList.add('requesting-xmpp-contact'); - this.$el.html(tpl_requesting_contact( + this.el.innerHTML = tpl_requesting_contact( _.extend(item.toJSON(), { 'desc_accept': __("Click to accept the contact request from %1$s", item.get('fullname')), 'desc_decline': __("Click to decline the contact request from %1$s", item.get('fullname')), 'allow_chat_pending_contacts': _converse.allow_chat_pending_contacts }) - )); + ); } else if (subscription === 'both' || subscription === 'to') { this.el.classList.add('current-xmpp-contact'); this.el.classList.remove(_.without(['both', 'to'], subscription)[0]); @@ -604,44 +589,25 @@ }, renderRosterItem (item) { - const chat_status = item.get('chat_status'); - this.$el.html(tpl_roster_item( + this.el.innerHTML = tpl_roster_item( _.extend(item.toJSON(), { - 'desc_status': STATUSES[chat_status||'offline'], + 'desc_status': STATUSES[item.get('chat_status')||'offline'], 'desc_chat': __('Click to chat with this contact'), 'desc_remove': __('Click to remove %1$s as a contact', item.get('fullname')), 'title_fullname': __('Name'), 'allow_contact_removal': _converse.allow_contact_removal, 'num_unread': item.get('num_unread') || 0 }) - )); + ); return this; }, - isGroupCollapsed () { - /* Check whether the group in which this contact appears is - * collapsed. - */ - // XXX: this sucks and is fragile. - // It's because I tried to do the "right thing" - // and use definition lists to represent roster groups. - // If roster group items were inside the group elements, we - // would simplify things by not having to check whether the - // group is collapsed or not. - const name = this.$el.prevAll('dt:first').data('group'); - const group = _.head(_converse.rosterview.model.where({'name': name.toString()})); - if (group.get('state') === _converse.CLOSED) { - return true; - } - return false; - }, - mayBeShown () { /* Return a boolean indicating whether this contact should * generally be visible in the roster. * * It doesn't check for the more specific case of whether - * the group it's in is collapsed (see isGroupCollapsed). + * the group it's in is collapsed. */ const chatStatus = this.model.get('chat_status'); if ((_converse.show_only_online_users && chatStatus !== 'online') || @@ -705,19 +671,17 @@ _converse.RosterGroupView = Backbone.Overview.extend({ - tagName: 'dt', + tagName: 'div', className: 'roster-group', events: { "click a.group-toggle": "toggle" }, initialize () { - this.model.contacts.on("add", this.addContact, this); + this.model.contacts.on("add", this.onContactAdded, this); this.model.contacts.on("change:subscription", this.onContactSubscriptionChange, this); this.model.contacts.on("change:requesting", this.onContactRequestChange, this); this.model.contacts.on("change:chat_status", function (contact) { - // This might be optimized by instead of first sorting, - // finding the correct position in positionContact this.model.contacts.sort(); this.positionContact(contact).render(); }, this); @@ -728,25 +692,27 @@ render () { this.el.setAttribute('data-group', this.model.get('name')); - const html = tpl_group_header({ - label_group: this.model.get('name'), - desc_group_toggle: this.model.get('description'), - toggle_state: this.model.get('state') + this.el.innerHTML = tpl_group_header({ + 'label_group': this.model.get('name'), + 'desc_group_toggle': this.model.get('description'), + 'toggle_state': this.model.get('state'), + '_converse': _converse }); - this.el.innerHTML = html; + this.contacts_el = this.el.querySelector('.roster-group-contacts'); return this; }, - addContact (contact) { - let view = new _converse.RosterContactView({model: contact}); - this.add(contact.get('id'), view); - view = this.positionContact(contact).render(); - if (view.mayBeShown()) { + onContactAdded (contact) { + let contact_view = new _converse.RosterContactView({model: contact}); + this.add(contact.get('id'), contact_view); + contact_view = this.positionContact(contact).render(); + if (contact_view.mayBeShown()) { if (this.model.get('state') === _converse.CLOSED) { - if (view.$el[0].style.display !== "none") { view.$el.hide(); } - if (!this.$el.is(':visible')) { this.$el.show(); } + u.hideElement(contact_view.el); + u.showElement(this.el); } else { - if (this.$el[0].style.display !== "block") { this.show(); } + u.showElement(contact_view.el); + u.showElement(this.el); } } }, @@ -756,30 +722,89 @@ * position amongst the other contacts in this group. */ const view = this.get(contact.get('id')); + view.render(); + const list = this.contacts_el; const index = this.model.contacts.indexOf(contact); - view.$el.detach(); if (index === 0) { - this.$el.after(view.$el); + list.insertAdjacentElement('afterbegin', view.el); } else if (index === (this.model.contacts.length-1)) { - this.$el.nextUntil('dt').last().after(view.$el); + list.insertAdjacentElement('beforeend', view.el); } else { - this.$el.nextUntil('dt').eq(index).before(view.$el); + const neighbour_el = list.querySelector('li:nth-child('+index+')'); + neighbour_el.insertAdjacentElement('afterend', view.el); } return view; }, show () { - this.$el.show(); - _.each(this.getAll(), function (view) { - if (view.mayBeShown() && !view.isGroupCollapsed()) { - view.$el.show(); + u.showElement(this.el); + _.each(this.getAll(), (contact_view) => { + if (contact_view.mayBeShown() && this.model.get('state') === _converse.OPENED) { + u.showElement(contact_view.el); } }); return this; }, - hide () { - this.$el.nextUntil('dt').addBack().hide(); + collapse () { + return u.slideIn(this.contacts_el); + }, + + filterOutContacts (contacts=[]) { + /* Given a list of contacts, make sure they're filtered out + * (aka hidden) and that all other contacts are visible. + * + * If all contacts are hidden, then also hide the group + * title. + */ + let shown = 0; + const all_contact_views = this.getAll(); + _.each(this.model.contacts.models, (contact) => { + const contact_view = this.get(contact.get('id')); + if (_.includes(contacts, contact)) { + u.hideElement(contact_view.el); + } else if (contact_view.mayBeShown()) { + u.showElement(contact_view.el); + shown += 1; + } + }); + if (shown) { + u.showElement(this.el); + } else { + u.hideElement(this.el); + } + }, + + getFilterMatches (q, type) { + /* Given the filter query "q" and the filter type "type", + * return a list of contacts that need to be filtered out. + */ + if (q.length === 0) { + return []; + } + let matches; + q = q.toLowerCase(); + if (type === 'state') { + if (this.model.get('name') === HEADER_REQUESTING_CONTACTS) { + // When filtering by chat state, we still want to + // show requesting contacts, even though they don't + // have the state in question. + matches = this.model.contacts.filter( + (contact) => u.contains.not('chat_status', q)(contact) && !contact.get('requesting') + ); + } else if (q === 'unread_messages') { + matches = this.model.contacts.filter({'num_unread': 0}); + } else { + matches = this.model.contacts.filter( + u.contains.not('chat_status', q) + ); + } + } else { + matches = this.model.contacts.filter( + u.contains.not('fullname', q) + ); + } + return matches; }, filter (q, type) { @@ -788,80 +813,27 @@ * If all contacts are filtered out (i.e. hidden), then the * group must be filtered out as well. */ - let matches; - if (q.length === 0) { - if (this.model.get('state') === _converse.OPENED) { - this.model.contacts.each( - (item) => { - const view = this.get(item.get('id')); - if (view.mayBeShown() && !view.isGroupCollapsed()) { - view.$el.show(); - } - } - ); - } - this.showIfNecessary(); - } else { - q = q.toLowerCase(); - if (type === 'state') { - if (this.model.get('name') === HEADER_REQUESTING_CONTACTS) { - // When filtering by chat state, we still want to - // show requesting contacts, even though they don't - // have the state in question. - matches = this.model.contacts.filter( - (contact) => utils.contains.not('chat_status', q)(contact) && !contact.get('requesting') - ); - } else if (q === 'unread_messages') { - matches = this.model.contacts.filter({'num_unread': 0}); - } else { - matches = this.model.contacts.filter( - utils.contains.not('chat_status', q) - ); - } - } else { - matches = this.model.contacts.filter( - utils.contains.not('fullname', q) - ); - } - if (matches.length === this.model.contacts.length) { - // hide the whole group - this.hide(); - } else { - _.each(matches, (item) => { - this.get(item.get('id')).$el.hide(); - }); - if (this.model.get('state') === _converse.OPENED) { - _.each(this.model.contacts.reject( - utils.contains.not('fullname', q)), - (item) => { - this.get(item.get('id')).$el.show(); - }); - } - this.showIfNecessary(); - } - } - }, - - showIfNecessary () { - if (!this.$el.is(':visible') && this.model.contacts.length > 0) { - this.$el.show(); - } + this.filterOutContacts(this.getFilterMatches(q, type)); }, toggle (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } - const $el = $(ev.target); - if ($el.hasClass("icon-opened")) { - this.$el.nextUntil('dt').slideUp(); + if (_.includes(ev.target.classList, "icon-opened")) { this.model.save({state: _converse.CLOSED}); - $el.removeClass("icon-opened").addClass("icon-closed"); + this.collapse().then(() => { + ev.target.classList.remove("icon-opened"); + ev.target.classList.add("icon-closed"); + }); } else { - $el.removeClass("icon-closed").addClass("icon-opened"); + ev.target.classList.remove("icon-closed"); + ev.target.classList.add("icon-opened"); this.model.save({state: _converse.OPENED}); this.filter( - _converse.rosterview.$('.roster-filter').val() || '', - _converse.rosterview.$('.filter-type').val() + _converse.rosterview.el.querySelector('.roster-filter').value, + _converse.rosterview.el.querySelector('.filter-type').value ); + u.showElement(this.el); + u.slideOut(this.contacts_el); } }, @@ -872,7 +844,7 @@ if (in_this_group && !in_this_overview) { this.model.contacts.remove(cid); } else if (!in_this_group && in_this_overview) { - this.addContact(contact); + this.onContactAdded(contact); } }, @@ -899,7 +871,7 @@ onRemove (contact) { this.remove(contact.get('id')); if (this.model.contacts.length === 0) { - this.$el.hide(); + u.hideElement(this.el); } } }); @@ -931,7 +903,7 @@ return; // The message has no text } if (chatbox.get('type') !== 'chatroom' && - utils.isNewMessage(data.stanza) && + u.isNewMessage(data.stanza) && chatbox.newMessageWillBeHidden()) { const contact = _.head(_converse.roster.where({'jid': chatbox.get('jid')})); diff --git a/src/templates/group_header.html b/src/templates/group_header.html index 93c8570a5..31993655d 100644 --- a/src/templates/group_header.html +++ b/src/templates/group_header.html @@ -1 +1,2 @@ {{{o.label_group}}} + diff --git a/src/templates/roster.html b/src/templates/roster.html index 1318e6daa..058e7d896 100644 --- a/src/templates/roster.html +++ b/src/templates/roster.html @@ -1 +1 @@ -
+