Commit 3400acbf authored by JC Brand's avatar JC Brand

Show MUC buttons in a dropdown menu

- Get rid of the ChatBoxHeading class
- Add support for showing standalone buttons in overlay viewmode
parent 2a7773dc
This diff is collapsed.
...@@ -41,7 +41,7 @@ ...@@ -41,7 +41,7 @@
color: #ffffff; color: #ffffff;
font-size: 100%; font-size: 100%;
margin: 0; margin: 0;
padding: 1rem;; padding: 0;
position: relative; position: relative;
&.chat-head-chatbox { &.chat-head-chatbox {
...@@ -54,22 +54,27 @@ ...@@ -54,22 +54,27 @@
.chat-head__desc { .chat-head__desc {
color: var(--chat-head-color-lighten-50-percent); color: var(--chat-head-color-lighten-50-percent);
font-size: 75%; font-size: var(--font-size-small);
font-size: 80%;
margin: 0; margin: 0;
overflow: hidden; overflow: hidden;
padding: 0; padding: 0.5rem 1rem 1rem 1rem;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
width: 100%;
} }
.chatbox-title { .chatbox-title {
padding: 0.75rem 1rem 0 1rem;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
width: 100%; width: 100%;
} }
.chatbox-title--no-desc {
padding: 0.75rem 1rem;
}
.chatbox-title--row { .chatbox-title--row {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
...@@ -137,7 +142,6 @@ ...@@ -137,7 +142,6 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
background-color: var(--chat-head-color);
box-shadow: 1px 3px 5px 3px rgba(0, 0, 0, 0.4); box-shadow: 1px 3px 5px 3px rgba(0, 0, 0, 0.4);
z-index: 2; z-index: 2;
overflow: hidden; overflow: hidden;
...@@ -175,7 +179,6 @@ ...@@ -175,7 +179,6 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
height: 100%;
background-color: var(--chat-head-color); background-color: var(--chat-head-color);
border-bottom-left-radius: var(--chatbox-border-radius); border-bottom-left-radius: var(--chatbox-border-radius);
border-bottom-right-radius: var(--chatbox-border-radius); border-bottom-right-radius: var(--chatbox-border-radius);
...@@ -426,8 +429,10 @@ ...@@ -426,8 +429,10 @@
#conversejs.converse-embedded, #conversejs.converse-embedded,
#conversejs.converse-overlayed { #conversejs.converse-overlayed {
.chat-head { .controlbox-head {
padding: 0.5em; padding: 0.5em;
}
.chat-head {
border-top-left-radius: var(--chatbox-border-radius); border-top-left-radius: var(--chatbox-border-radius);
border-top-right-radius: var(--chatbox-border-radius); border-top-right-radius: var(--chatbox-border-radius);
@media screen and (max-height: $mobile-landscape-height) { @media screen and (max-height: $mobile-landscape-height) {
...@@ -474,6 +479,22 @@ ...@@ -474,6 +479,22 @@
} }
} }
} }
.chat-body {
height: calc(100% - var(--overlayed-chat-head-height));
}
.chatbox-title {
padding: 0.5rem 0.75rem 0 0.75rem;
}
.chatbox-title--no-desc {
padding: 0.5rem 0.75rem;
}
converse-dropdown {
.btn--standalone {
padding: 0 0 0 0.5em;
}
}
} }
} }
...@@ -497,12 +518,6 @@ ...@@ -497,12 +518,6 @@
bottom: 0; bottom: 0;
} }
.chat-head {
.chat-head__desc {
font-size: 70%;
}
}
.chatbox { .chatbox {
margin: 0; margin: 0;
.box-flyout { .box-flyout {
...@@ -590,7 +605,6 @@ ...@@ -590,7 +605,6 @@
} }
.chatbox { .chatbox {
.box-flyout { .box-flyout {
background-color: var(--chat-head-color);
box-shadow: none; box-shadow: none;
height: var(--fullpage-chat-height); height: var(--fullpage-chat-height);
min-height: calc(var(--fullpage-chat-height) / 2); min-height: calc(var(--fullpage-chat-height) / 2);
...@@ -598,6 +612,7 @@ ...@@ -598,6 +612,7 @@
overflow: hidden; overflow: hidden;
} }
.chat-body { .chat-body {
height: calc(100% - var(--fullpage-chat-head-height));
background-color: var(--chat-head-color); background-color: var(--chat-head-color);
} }
.chat-title { .chat-title {
......
...@@ -42,15 +42,28 @@ ...@@ -42,15 +42,28 @@
border-bottom: var(--chatroom-head-border-bottom); border-bottom: var(--chatroom-head-border-bottom);
.chat-head__desc { .chat-head__desc {
color: var(--chatroom-head-description-color); color: var(--chatroom-head-color);
display: var(--chatroom-head-description-display); display: var(--chatroom-head-description-display);
font-size: 70%;
margin-top: 3px;
border-left: var(--chatroom-head-description-border-left);
padding-left: var(--chatroom-head-description-padding-left);
a { a {
color: var(--chatroom-head-description-link-color); color: var(--chatroom-head-description-link-color);
} }
&:hover {
button {
display: inline-block;
}
}
}
.chatbox-title {
.btn--transparent {
i {
color: var(--chatroom-head-color);
}
}
}
.chatbox-title__buttons {
background-color: var(--chatroom-head-bg-color);
} }
a, a:visited, a:hover, a:not([href]):not([tabindex]) { a, a:visited, a:hover, a:not([href]):not([tabindex]) {
...@@ -73,6 +86,7 @@ ...@@ -73,6 +86,7 @@
display: var(--heading-display); display: var(--heading-display);
font-weight: var(--chatroom-head-title-font-weight); font-weight: var(--chatroom-head-title-font-weight);
padding-right: var(--chatroom-head-title-padding-right); padding-right: var(--chatroom-head-title-padding-right);
margin: auto 0;
.chatroom-jid { .chatroom-jid {
font-size: var(--font-size-small); font-size: var(--font-size-small);
} }
...@@ -214,7 +228,6 @@ ...@@ -214,7 +228,6 @@
overflow-y: auto; overflow-y: auto;
flex-basis: 0; flex-basis: 0;
flex-grow: 1; flex-grow: 1;
border-bottom: var(--occupants-border-bottom);
} }
li { li {
cursor: default; cursor: default;
...@@ -467,10 +480,6 @@ ...@@ -467,10 +480,6 @@
.box-flyout { .box-flyout {
width: 100%; width: 100%;
.chat-head__desc {
font-size: 70%;
}
.chatroom-body { .chatroom-body {
.chat-area { .chat-area {
&.full { &.full {
......
...@@ -193,6 +193,20 @@ body.converse-fullscreen { ...@@ -193,6 +193,20 @@ body.converse-fullscreen {
} }
} }
.dropdown-item {
padding: 0.5rem 1rem;
.fa {
margin-right: 0.75rem;
}
&:active, &.selected {
color: white !important;
background-color: var(--list-item-open-color);
.fa {
color: white !important;
}
}
}
.popover { .popover {
position: fixed; position: fixed;
} }
...@@ -352,6 +366,10 @@ body.converse-fullscreen { ...@@ -352,6 +366,10 @@ body.converse-fullscreen {
.fa, .far, .fas { .fa, .far, .fas {
color: #fff; color: #fff;
margin-right: 0.5em; margin-right: 0.5em;
&.only-icon {
margin-right: 0;
}
} }
} }
...@@ -541,6 +559,11 @@ body.converse-fullscreen { ...@@ -541,6 +559,11 @@ body.converse-fullscreen {
} }
} }
.btn--transparent {
background: transparent;
border: none;
}
.btn-circle { .btn-circle {
width: 30px; width: 30px;
height: 30px; height: 30px;
......
...@@ -117,11 +117,8 @@ $mobile_portrait_length: 480px !default; ...@@ -117,11 +117,8 @@ $mobile_portrait_length: 480px !default;
--chatroom-head-button-color: var(--chatroom-head-bg-color); --chatroom-head-button-color: var(--chatroom-head-bg-color);
--chatroom-head-title-font-weight: normal; --chatroom-head-title-font-weight: normal;
--chatroom-head-title-padding-right: 0px; --chatroom-head-title-padding-right: 0px;
--chatroom-head-description-color: var(--chatroom-head-bg-color-lighten-25-percent);
--chatroom-head-description-link-color: white; --chatroom-head-description-link-color: white;
--chatroom-head-description-display: block; --chatroom-head-description-display: block;
--chatroom-head-description-border-left: 0px;
--chatroom-head-description-padding-left: 0px;
--chatroom-head-border-bottom: 0px; --chatroom-head-border-bottom: 0px;
--chatroom-width: 500px; --chatroom-width: 500px;
--chatroom-correcting-color: #fadfd7; // lighten($red, 30%) --chatroom-correcting-color: #fadfd7; // lighten($red, 30%)
......
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
const view = _converse.chatboxviews.get(jid); const view = _converse.chatboxviews.get(jid);
spyOn(view, 'renderBookmarkForm').and.callThrough(); spyOn(view, 'renderBookmarkForm').and.callThrough();
spyOn(view, 'closeForm').and.callThrough(); spyOn(view, 'closeForm').and.callThrough();
await u.waitUntil(() => !_.isNull(view.el.querySelector('.toggle-bookmark'))); await u.waitUntil(() => view.el.querySelector('.toggle-bookmark') !== null);
let toggle = view.el.querySelector('.toggle-bookmark'); let toggle = view.el.querySelector('.toggle-bookmark');
expect(toggle.title).toBe('Bookmark this groupchat'); expect(toggle.title).toBe('Bookmark this groupchat');
toggle.click(); toggle.click();
...@@ -216,8 +216,7 @@ ...@@ -216,8 +216,7 @@
); );
await _converse.api.rooms.open(`lounge@montague.lit`); await _converse.api.rooms.open(`lounge@montague.lit`);
const view = _converse.chatboxviews.get('lounge@montague.lit'); const view = _converse.chatboxviews.get('lounge@montague.lit');
let bookmark_icon = await u.waitUntil(() => view.el.querySelector('.toggle-bookmark')); expect(view.el.querySelector('.chatbox-title__text .fa-bookmark')).toBe(null);
expect(_.includes(bookmark_icon.classList, 'button-on')).toBeFalsy();
_converse.bookmarks.create({ _converse.bookmarks.create({
'jid': view.model.get('jid'), 'jid': view.model.get('jid'),
'autojoin': false, 'autojoin': false,
...@@ -225,11 +224,9 @@ ...@@ -225,11 +224,9 @@
'nick': ' some1' 'nick': ' some1'
}); });
view.model.set('bookmarked', true); view.model.set('bookmarked', true);
bookmark_icon = await u.waitUntil(() => view.el.querySelector('.toggle-bookmark')); expect(view.el.querySelector('.chatbox-title__text .fa-bookmark')).not.toBe(null);
expect(_.includes(bookmark_icon.classList, 'button-on')).toBeTruthy();
view.model.set('bookmarked', false); view.model.set('bookmarked', false);
bookmark_icon = await u.waitUntil(() => view.el.querySelector('.toggle-bookmark')); expect(view.el.querySelector('.chatbox-title__text .fa-bookmark')).toBe(null);
expect(_.includes(bookmark_icon.classList, 'button-on')).toBeFalsy();
done(); done();
})); }));
...@@ -256,14 +253,12 @@ ...@@ -256,14 +253,12 @@
expect(_converse.bookmarks.length).toBe(1); expect(_converse.bookmarks.length).toBe(1);
await u.waitUntil(() => _converse.chatboxes.length >= 1); await u.waitUntil(() => _converse.chatboxes.length >= 1);
expect(view.model.get('bookmarked')).toBeTruthy(); expect(view.model.get('bookmarked')).toBeTruthy();
let bookmark_icon = await u.waitUntil(() => view.el.querySelector('.toggle-bookmark')); expect(view.el.querySelector('.chatbox-title__text .fa-bookmark')).not.toBe(null);
expect(u.hasClass('button-on', bookmark_icon)).toBeTruthy();
spyOn(_converse.connection, 'getUniqueId').and.callThrough(); spyOn(_converse.connection, 'getUniqueId').and.callThrough();
const bookmark_icon = view.el.querySelector('.toggle-bookmark');
bookmark_icon.click(); bookmark_icon.click();
bookmark_icon = await u.waitUntil(() => view.el.querySelector('.toggle-bookmark'));
expect(view.toggleBookmark).toHaveBeenCalled(); expect(view.toggleBookmark).toHaveBeenCalled();
expect(u.hasClass('button-on', bookmark_icon)).toBeFalsy(); expect(view.el.querySelector('.chatbox-title__text .fa-bookmark')).toBe(null);
expect(_converse.bookmarks.length).toBe(0); expect(_converse.bookmarks.length).toBe(0);
// Check that an IQ stanza is sent out, containing no // Check that an IQ stanza is sent out, containing no
......
...@@ -364,9 +364,6 @@ ...@@ -364,9 +364,6 @@
expect(trimmedview.restore).toHaveBeenCalled(); expect(trimmedview.restore).toHaveBeenCalled();
expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMaximized', jasmine.any(Object)); expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMaximized', jasmine.any(Object));
const toggle_el = sizzle('.toggle-chatbox-button', chatview.el).pop();
expect(u.hasClass('fa-minus', toggle_el)).toBeTruthy();
expect(u.hasClass('fa-plus', toggle_el)).toBeFalsy();
expect(chatview.model.get('minimized')).toBeFalsy(); expect(chatview.model.get('minimized')).toBeFalsy();
done(); done();
})); }));
......
...@@ -149,6 +149,7 @@ ...@@ -149,6 +149,7 @@
const cbview = _converse.chatboxviews.get('controlbox'); const cbview = _converse.chatboxviews.get('controlbox');
await u.waitUntil(() => cbview.el.querySelectorAll(".open-headline").length); await u.waitUntil(() => cbview.el.querySelectorAll(".open-headline").length);
const hlview = _converse.chatboxviews.get('notify.example.com'); const hlview = _converse.chatboxviews.get('notify.example.com');
await u.isVisible(hlview.el);
const close_el = hlview.el.querySelector('.close-chatbox-button'); const close_el = hlview.el.querySelector('.close-chatbox-button');
close_el.click(); close_el.click();
await u.waitUntil(() => cbview.el.querySelectorAll(".open-headline").length === 0); await u.waitUntil(() => cbview.el.querySelectorAll(".open-headline").length === 0);
......
...@@ -1390,9 +1390,7 @@ ...@@ -1390,9 +1390,7 @@
}).up() }).up()
.c('status', {code: '110'}); .c('status', {code: '110'});
_converse.connection._dataRecv(test_utils.createRequest(presence)); _converse.connection._dataRecv(test_utils.createRequest(presence));
expect(u.isVisible(view.el.querySelector('.toggle-chatbox-button'))).toBeTruthy(); await u.waitUntil(() => view.el.querySelector('.configure-chatroom-button') !== null);
await u.waitUntil(() => !_.isNull(view.el.querySelector('.configure-chatroom-button')))
expect(u.isVisible(view.el.querySelector('.configure-chatroom-button'))).toBeTruthy();
view.el.querySelector('.configure-chatroom-button').click(); view.el.querySelector('.configure-chatroom-button').click();
/* Check that an IQ is sent out, asking for the /* Check that an IQ is sent out, asking for the
...@@ -1949,13 +1947,14 @@ ...@@ -1949,13 +1947,14 @@
// Members can't invite if the room isn't open // Members can't invite if the room isn't open
view.model.getOwnOccupant().set('affiliation', 'member'); view.model.getOwnOccupant().set('affiliation', 'member');
await u.waitUntil(() => view.el.querySelector('.open-invite-modal') === null); await u.waitUntil(() => view.el.querySelector('.open-invite-modal') === null);
view.model.features.set('open', 'true'); view.model.features.set('open', 'true');
await u.waitUntil(() => view.el.querySelector('.open-invite-modal')); await u.waitUntil(() => view.el.querySelector('.open-invite-modal'));
view.el.querySelector('.open-invite-modal').click(); view.el.querySelector('.open-invite-modal').click();
const modal = view.sidebar_view.muc_invite_modal; const modal = view.muc_invite_modal;
await u.waitUntil(() => u.isVisible(modal.el), 1000) await u.waitUntil(() => u.isVisible(modal.el), 1000)
expect(modal.el.querySelectorAll('#invitee_jids').length).toBe(1); expect(modal.el.querySelectorAll('#invitee_jids').length).toBe(1);
...@@ -2174,8 +2173,8 @@ ...@@ -2174,8 +2173,8 @@
_converse.connection._dataRecv(test_utils.createRequest(stanza)); _converse.connection._dataRecv(test_utils.createRequest(stanza));
const view = _converse.chatboxviews.get('jdev@conference.jabber.org'); const view = _converse.chatboxviews.get('jdev@conference.jabber.org');
await new Promise(resolve => view.model.once('change:subject', resolve)); await new Promise(resolve => view.model.once('change:subject', resolve));
expect(sizzle('.chat-event:last').pop().textContent.trim()).toBe('Topic set by ralphm');
expect(sizzle('.chat-topic:last').pop().textContent.trim()).toBe(text); expect(sizzle('.chat-event:last', view.el).pop().textContent.trim()).toBe('Topic set by ralphm');
expect(view.el.querySelector('.chat-head__desc').textContent.trim()).toBe(text); expect(view.el.querySelector('.chat-head__desc').textContent.trim()).toBe(text);
stanza = u.toStanza( stanza = u.toStanza(
...@@ -2185,7 +2184,6 @@ ...@@ -2185,7 +2184,6 @@
</message>`); </message>`);
_converse.connection._dataRecv(test_utils.createRequest(stanza)); _converse.connection._dataRecv(test_utils.createRequest(stanza));
await new Promise(resolve => view.once('messageInserted', resolve)); await new Promise(resolve => view.once('messageInserted', resolve));
expect(sizzle('.chat-topic', view.el).length).toBe(1);
expect(sizzle('.chat-msg__subject', view.el).length).toBe(1); expect(sizzle('.chat-msg__subject', view.el).length).toBe(1);
expect(sizzle('.chat-msg__subject', view.el).pop().textContent.trim()).toBe('This is a message subject'); expect(sizzle('.chat-msg__subject', view.el).pop().textContent.trim()).toBe('This is a message subject');
expect(sizzle('.chat-msg__text').length).toBe(1); expect(sizzle('.chat-msg__text').length).toBe(1);
...@@ -2199,7 +2197,7 @@ ...@@ -2199,7 +2197,7 @@
</message>`); </message>`);
_converse.connection._dataRecv(test_utils.createRequest(stanza)); _converse.connection._dataRecv(test_utils.createRequest(stanza));
await new Promise(resolve => view.model.once('change:subject', resolve)); await new Promise(resolve => view.model.once('change:subject', resolve));
expect(view.el.querySelector('.chat-head__desc').textContent.trim()).toBe(""); expect(view.el.querySelector('.chat-head__desc')).toBe(null);
expect(view.el.querySelector('.chat-info:last-child').textContent.trim()).toBe("Topic cleared by ralphm"); expect(view.el.querySelector('.chat-info:last-child').textContent.trim()).toBe("Topic cleared by ralphm");
done(); done();
})); }));
...@@ -2218,7 +2216,7 @@ ...@@ -2218,7 +2216,7 @@
'author': 'ralphm' 'author': 'ralphm'
}}); }});
expect(sizzle('.chat-event:last').pop().textContent.trim()).toBe('Topic set by ralphm'); expect(sizzle('.chat-event:last').pop().textContent.trim()).toBe('Topic set by ralphm');
expect(sizzle('.chat-topic:last').pop().textContent.trim()).toBe(subject); expect(view.el.querySelector('.chat-head__desc').textContent.trim()).toBe(subject);
done(); done();
})); }));
...@@ -2875,8 +2873,9 @@ ...@@ -2875,8 +2873,9 @@
spyOn(_converse.api, "trigger").and.callThrough(); spyOn(_converse.api, "trigger").and.callThrough();
spyOn(view.model, 'leave'); spyOn(view.model, 'leave');
view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true));
view.el.querySelector('.close-chatbox-button').click(); view.el.querySelector('.close-chatbox-button').click();
expect(view.close).toHaveBeenCalled(); await u.waitUntil(() => view.close.calls.count());
expect(view.model.leave).toHaveBeenCalled(); expect(view.model.leave).toHaveBeenCalled();
await u.waitUntil(() => _converse.api.trigger.calls.count()); await u.waitUntil(() => _converse.api.trigger.calls.count());
expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object)); expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object));
...@@ -4853,7 +4852,7 @@ ...@@ -4853,7 +4852,7 @@
await u.waitUntil(() => _converse.chatboxes.length > 1); await u.waitUntil(() => _converse.chatboxes.length > 1);
expect(sizzle('.chatroom', _converse.el).filter(u.isVisible).length).toBe(1); // There should now be an open chatroom expect(sizzle('.chatroom', _converse.el).filter(u.isVisible).length).toBe(1); // There should now be an open chatroom
var view = _converse.chatboxviews.get('inverness@chat.shakespeare.lit'); var view = _converse.chatboxviews.get('inverness@chat.shakespeare.lit');
expect(view.el.querySelector('.chat-head-chatroom').textContent.trim()).toBe("Macbeth's Castle"); expect(view.el.querySelector('.chatbox-title__text').textContent.trim()).toBe("Macbeth's Castle");
done(); done();
})); }));
......
...@@ -95,6 +95,7 @@ ...@@ -95,6 +95,7 @@
`</presence>`) `</presence>`)
await u.waitUntil(() => modal.el.getAttribute('aria-hidden') === "true"); await u.waitUntil(() => modal.el.getAttribute('aria-hidden') === "true");
await u.waitUntil(() => !u.isVisible(modal.el));
cbview.el.querySelector('.change-status').click() cbview.el.querySelector('.change-status').click()
await u.waitUntil(() => modal.el.getAttribute('aria-hidden') === "false", 1000); await u.waitUntil(() => modal.el.getAttribute('aria-hidden') === "false", 1000);
modal.el.querySelector('label[for="radio-busy"]').click(); // Change status to "dnd" modal.el.querySelector('label[for="radio-busy"]').click(); // Change status to "dnd"
......
...@@ -23,7 +23,6 @@ ...@@ -23,7 +23,6 @@
await u.waitUntil(() => _converse.chatboxes.length > 1); await u.waitUntil(() => _converse.chatboxes.length > 1);
const view = _converse.chatboxviews.get(contact_jid); const view = _converse.chatboxviews.get(contact_jid);
let show_modal_button = view.el.querySelector('.show-user-details-modal'); let show_modal_button = view.el.querySelector('.show-user-details-modal');
expect(u.isVisible(show_modal_button)).toBeTruthy();
show_modal_button.click(); show_modal_button.click();
const modal = view.user_details_modal; const modal = view.user_details_modal;
await u.waitUntil(() => u.isVisible(modal.el), 1000); await u.waitUntil(() => u.isVisible(modal.el), 1000);
...@@ -33,7 +32,7 @@ ...@@ -33,7 +32,7 @@
expect(u.isVisible(remove_contact_button)).toBeTruthy(); expect(u.isVisible(remove_contact_button)).toBeTruthy();
remove_contact_button.click(); remove_contact_button.click();
await u.waitUntil(() => modal.el.getAttribute('aria-hidden'), 1000); await u.waitUntil(() => modal.el.getAttribute('aria-hidden'), 1000);
await u.waitUntil(() => !u.isVisible(modal.el));
show_modal_button = view.el.querySelector('.show-user-details-modal'); show_modal_button = view.el.querySelector('.show-user-details-modal');
show_modal_button.click(); show_modal_button.click();
remove_contact_button = modal.el.querySelector('button.remove-contact'); remove_contact_button = modal.el.querySelector('button.remove-contact');
...@@ -51,7 +50,6 @@ ...@@ -51,7 +50,6 @@
await test_utils.openChatBoxFor(_converse, contact_jid) await test_utils.openChatBoxFor(_converse, contact_jid)
const view = _converse.chatboxviews.get(contact_jid); const view = _converse.chatboxviews.get(contact_jid);
let show_modal_button = view.el.querySelector('.show-user-details-modal'); let show_modal_button = view.el.querySelector('.show-user-details-modal');
expect(u.isVisible(show_modal_button)).toBeTruthy();
show_modal_button.click(); show_modal_button.click();
const modal = view.user_details_modal; const modal = view.user_details_modal;
await u.waitUntil(() => u.isVisible(modal.el), 2000); await u.waitUntil(() => u.isVisible(modal.el), 2000);
......
import { html } from 'lit-element';
import { CustomElement } from './element.js';
import { until } from 'lit-html/directives/until.js';
import DOMNavigator from "../dom-navigator";
import converse from "@converse/headless/converse-core";
const u = converse.env.utils;
export class Dropdown extends CustomElement {
static get properties () {
return {
'items': { type: Array }
}
}
render () {
return html`
<div class="dropleft">
<button type="button" class="btn btn--transparent btn--standalone" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-bars only-icon"></i>
</button>
<div class="dropdown-menu">
${ this.items.map(b => until(b, '')) }
</div>
</div>
`;
}
firstUpdated () {
this.menu = this.querySelector('.dropdown-menu');
this.dropdown = this.firstElementChild;
this.button = this.dropdown.querySelector('button');
this.dropdown.addEventListener('click', ev => this.toggleMenu(ev));
this.dropdown.addEventListener('keyup', ev => this.handleKeyUp(ev));
document.addEventListener('click', ev => !this.contains(ev.target) && this.hideMenu(ev));
this.initArrowNavigation();
}
initArrowNavigation () {
if (!this.navigator) {
const options = {
'selector': '.dropdown-item',
'onSelected': el => el.focus()
};
this.navigator = new DOMNavigator(this.menu, options);
}
}
enableArrowNavigation (ev) {
if (ev) {
ev.preventDefault();
ev.stopPropagation();
}
this.navigator.enable();
this.navigator.select(this.menu.firstElementChild);
}
hideMenu () {
u.removeClass('show', this.menu);
this.navigator.disable();
this.button.setAttribute('aria-expanded', false);
this.button.blur();
}
showMenu () {
u.addClass('show', this.menu);
this.button.setAttribute('aria-expanded', true);
}
toggleMenu () {
if (u.hasClass('show', this.menu)) {
this.hideMenu();
} else {
this.showMenu();
}
}
handleKeyUp (ev) {
if (ev.keyCode === converse.keycodes.ESCAPE) {
this.hideMenu();
} else if (ev.keyCode === converse.keycodes.DOWN_ARROW && !this.navigator.enabled) {
this.enableArrowNavigation(ev);
}
}
}
window.customElements.define('converse-dropdown', Dropdown);
import { LitElement } from 'lit-element';
export class CustomElement extends LitElement {
createRenderRoot () {
// Render without the shadow DOM
return this;
}
}
...@@ -7,12 +7,10 @@ ...@@ -7,12 +7,10 @@
import "@converse/headless/converse-muc"; import "@converse/headless/converse-muc";
import { Model } from 'skeletor.js/src/model.js'; import { Model } from 'skeletor.js/src/model.js';
import { View } from 'skeletor.js/src/view.js'; import { View } from 'skeletor.js/src/view.js';
import { html } from "lit-html";
import { __ } from '@converse/headless/i18n'; import { __ } from '@converse/headless/i18n';
import converse from "@converse/headless/converse-core"; import converse from "@converse/headless/converse-core";
import tpl_bookmarks_list from "templates/bookmarks_list.js" import tpl_bookmarks_list from "templates/bookmarks_list.js"
import tpl_muc_bookmark_form from "templates/muc_bookmark_form.js"; import tpl_muc_bookmark_form from "templates/muc_bookmark_form.js";
import tpl_chatroom_bookmark_toggle from "templates/chatroom_bookmark_toggle.html";
const { Strophe, _ } = converse.env; const { Strophe, _ } = converse.env;
const u = converse.env.utils; const u = converse.env.utils;
...@@ -37,21 +35,24 @@ converse.plugins.add('converse-bookmark-views', { ...@@ -37,21 +35,24 @@ converse.plugins.add('converse-bookmark-views', {
// plugin architecture they will replace existing methods on the // plugin architecture they will replace existing methods on the
// relevant objects or classes. // relevant objects or classes.
ChatRoomView: { ChatRoomView: {
events: {
'click .toggle-bookmark': 'toggleBookmark'
},
getHeadingButtons () { getHeadingButtons () {
const { _converse } = this.__super__; const { _converse } = this.__super__;
const buttons = this.__super__.getHeadingButtons.call(this); const buttons = this.__super__.getHeadingButtons.call(this);
if (_converse.allow_bookmarks) { if (_converse.allow_bookmarks) {
const supported = _converse.checkBookmarksSupport(); const supported = _converse.checkBookmarksSupport();
const info_toggle_bookmark = this.model.get('bookmarked') ? __('Unbookmark this groupchat') : __('Bookmark this groupchat');
const bookmarked = this.model.get('bookmarked'); const bookmarked = this.model.get('bookmarked');
const template = html`<a class="chatbox-btn toggle-bookmark fa fa-bookmark ${bookmarked ? 'button-on' : ''}" title="${info_toggle_bookmark}"></a>`; const data = {
'i18n_title': bookmarked ? __('Unbookmark this groupchat') : __('Bookmark this groupchat'),
'i18n_text': bookmarked ? __('Unbookmark') : __('Bookmark'),
'handler': ev => this.toggleBookmark(ev),
'a_class': 'toggle-bookmark',
'icon_class': 'fa-bookmark',
'name': 'bookmark'
}
const names = buttons.map(t => t.name); const names = buttons.map(t => t.name);
const idx = names.indexOf('configure'); const idx = names.indexOf('configure');
const template_promise = supported.then(s => s ? template : ''); const data_promise = supported.then(s => s ? data : '');
return idx > -1 ? [...buttons.slice(0, idx), template_promise, ...buttons.slice(idx)] : [template_promise, ...buttons]; return idx > -1 ? [...buttons.slice(0, idx), data_promise, ...buttons.slice(idx)] : [data_promise, ...buttons];
} }
return buttons; return buttons;
} }
...@@ -100,25 +101,6 @@ converse.plugins.add('converse-bookmark-views', { ...@@ -100,25 +101,6 @@ converse.plugins.add('converse-bookmark-views', {
}); });
const bookmarkableChatRoomView = { const bookmarkableChatRoomView = {
renderBookmarkToggle () {
const bookmark_button = tpl_chatroom_bookmark_toggle(
_.assignIn(this.model.toJSON(), {
'info_toggle_bookmark': this.model.get('bookmarked') ?
__('Unbookmark this groupchat') :
__('Bookmark this groupchat'),
'bookmarked': this.model.get('bookmarked')
}));
const buttons_row = this.el.querySelector('.chatbox-title__buttons')
const close_button = buttons_row.querySelector('.close-chatbox-button');
if (close_button) {
close_button.insertAdjacentHTML('afterend', bookmark_button);
} else {
buttons_row.insertAdjacentHTML('beforeEnd', bookmark_button);
}
},
/** /**
* Set whether the groupchat is bookmarked or not. * Set whether the groupchat is bookmarked or not.
* @private * @private
......
This diff is collapsed.
...@@ -138,7 +138,8 @@ converse.plugins.add('converse-headlines-view', { ...@@ -138,7 +138,8 @@ converse.plugins.add('converse-headlines-view', {
this.listenTo(this.model, 'destroy', this.hide); this.listenTo(this.model, 'destroy', this.hide);
this.listenTo(this.model, 'change:minimized', this.onMinimizedChanged); this.listenTo(this.model, 'change:minimized', this.onMinimizedChanged);
this.render().insertHeading() this.render();
this.renderHeading();
this.updateAfterMessagesFetched(); this.updateAfterMessagesFetched();
this.insertIntoDOM().hide(); this.insertIntoDOM().hide();
/** /**
......
...@@ -8,7 +8,6 @@ import { Model } from 'skeletor.js/src/model.js'; ...@@ -8,7 +8,6 @@ import { Model } from 'skeletor.js/src/model.js';
import { Overview } from "skeletor.js/src/overview"; import { Overview } from "skeletor.js/src/overview";
import { View } from "skeletor.js/src/view"; import { View } from "skeletor.js/src/view";
import { __ } from '@converse/headless/i18n'; import { __ } from '@converse/headless/i18n';
import { html } from "lit-html";
import converse from "@converse/headless/converse-core"; import converse from "@converse/headless/converse-core";
import tpl_chats_panel from "templates/chats_panel.html"; import tpl_chats_panel from "templates/chats_panel.html";
import tpl_toggle_chats from "templates/toggle_chats.html"; import tpl_toggle_chats from "templates/toggle_chats.html";
...@@ -74,10 +73,6 @@ converse.plugins.add('converse-minimize', { ...@@ -74,10 +73,6 @@ converse.plugins.add('converse-minimize', {
}, },
ChatBoxView: { ChatBoxView: {
events: {
'click .toggle-chatbox-button': 'minimize',
},
initialize () { initialize () {
this.listenTo(this.model, 'change:minimized', this.onMinimizedChanged) this.listenTo(this.model, 'change:minimized', this.onMinimizedChanged)
return this.__super__.initialize.apply(this, arguments); return this.__super__.initialize.apply(this, arguments);
...@@ -113,25 +108,27 @@ converse.plugins.add('converse-minimize', { ...@@ -113,25 +108,27 @@ converse.plugins.add('converse-minimize', {
if (!this.model.get('minimized')) { if (!this.model.get('minimized')) {
return this.__super__.setChatBoxWidth.call(this, width); return this.__super__.setChatBoxWidth.call(this, width);
} }
}
}, },
ChatBoxHeading: {
getHeadingButtons () { getHeadingButtons () {
const { _converse } = this.__super__;
const buttons = this.__super__.getHeadingButtons.call(this); const buttons = this.__super__.getHeadingButtons.call(this);
const info_minimize = __('Minimize this chat box'); const data = {
const template = html`<a class="chatbox-btn toggle-chatbox-button fa fa-minus" title="${info_minimize}"></a>`; 'a_class': 'toggle-chatbox-button',
'handler': ev => this.minimize(ev),
'i18n_text': __('Minimize'),
'i18n_title': __('Minimize this chat'),
'icon_class': "fa-minus",
'name': 'minimize',
'standalone': _converse.view_mode === 'overlayed'
}
const names = buttons.map(t => t.name); const names = buttons.map(t => t.name);
const idx = names.indexOf('close'); const idx = names.indexOf('close');
return idx > -1 ? [...buttons.slice(0, idx+1), template, ...buttons.slice(idx+1)] : [template, ...buttons]; return idx > -1 ? [...buttons.slice(0, idx), data, ...buttons.slice(idx)] : [data, ...buttons];
} }
}, },
ChatRoomView: { ChatRoomView: {
events: {
'click .toggle-chatbox-button': 'minimize',
},
initialize () { initialize () {
this.listenTo(this.model, 'change:minimized', this.onMinimizedChanged) this.listenTo(this.model, 'change:minimized', this.onMinimizedChanged)
const result = this.__super__.initialize.apply(this, arguments); const result = this.__super__.initialize.apply(this, arguments);
...@@ -142,12 +139,20 @@ converse.plugins.add('converse-minimize', { ...@@ -142,12 +139,20 @@ converse.plugins.add('converse-minimize', {
}, },
getHeadingButtons () { getHeadingButtons () {
const { _converse } = this.__super__;
const buttons = this.__super__.getHeadingButtons.call(this); const buttons = this.__super__.getHeadingButtons.call(this);
const info_minimize = __('Minimize this groupchat'); const data = {
const template = html`<a class="chatbox-btn toggle-chatbox-button fa fa-minus" title="${info_minimize}"></a>`; 'a_class': 'toggle-chatbox-button',
'handler': ev => this.minimize(ev),
'i18n_text': __('Minimize'),
'i18n_title': __('Minimize this groupchat'),
'icon_class': "fa-minus",
'name': 'minimize',
'standalone': _converse.view_mode === 'overlayed'
}
const names = buttons.map(t => t.name); const names = buttons.map(t => t.name);
const idx = names.indexOf('signout'); const idx = names.indexOf('signout');
return idx > -1 ? [...buttons.slice(0, idx+1), template, ...buttons.slice(idx+1)] : [template, ...buttons]; return idx > -1 ? [...buttons.slice(0, idx), data, ...buttons.slice(idx)] : [data, ...buttons];
} }
} }
}, },
......
This diff is collapsed.
...@@ -340,7 +340,6 @@ converse.plugins.add('converse-muc', { ...@@ -340,7 +340,6 @@ converse.plugins.add('converse-muc', {
// mention the user and `num_unread_general` to indicate // mention the user and `num_unread_general` to indicate
// generally unread messages (which *includes* mentions!). // generally unread messages (which *includes* mentions!).
'num_unread_general': 0, 'num_unread_general': 0,
'bookmarked': false, 'bookmarked': false,
'chat_state': undefined, 'chat_state': undefined,
'hidden': ['mobile', 'fullscreen'].includes(_converse.view_mode), 'hidden': ['mobile', 'fullscreen'].includes(_converse.view_mode),
...@@ -348,8 +347,8 @@ converse.plugins.add('converse-muc', { ...@@ -348,8 +347,8 @@ converse.plugins.add('converse-muc', {
'name': '', 'name': '',
'num_unread': 0, 'num_unread': 0,
'roomconfig': {}, 'roomconfig': {},
'time_sent': (new Date(0)).toISOString(),
'time_opened': this.get('time_opened') || (new Date()).getTime(), 'time_opened': this.get('time_opened') || (new Date()).getTime(),
'time_sent': (new Date(0)).toISOString(),
'type': _converse.CHATROOMS_TYPE 'type': _converse.CHATROOMS_TYPE
} }
}, },
...@@ -597,6 +596,13 @@ converse.plugins.add('converse-muc', { ...@@ -597,6 +596,13 @@ converse.plugins.add('converse-muc', {
return this; return this;
}, },
invitesAllowed () {
return _converse.allow_muc_invitations &&
(this.features.get('open') ||
this.getOwnAffiliation() === "owner"
);
},
getDisplayName () { getDisplayName () {
const name = this.get('name'); const name = this.get('name');
if (name) { if (name) {
...@@ -1510,7 +1516,10 @@ converse.plugins.add('converse-muc', { ...@@ -1510,7 +1516,10 @@ converse.plugins.add('converse-muc', {
// The subject is changed by sending a message of type "groupchat" to the <room@service>, // The subject is changed by sending a message of type "groupchat" to the <room@service>,
// where the <message/> MUST contain a <subject/> element that specifies the new subject but // where the <message/> MUST contain a <subject/> element that specifies the new subject but
// MUST NOT contain a <body/> element (or a <thread/> element). // MUST NOT contain a <body/> element (or a <thread/> element).
u.safeSave(this, {'subject': {'author': attrs.nick, 'text': attrs.subject || ''}}); u.safeSave(this, {
'subject': {'author': attrs.nick, 'text': attrs.subject || ''},
'hide_subject': attrs.subject ? false : this.get('hide_subject')
});
return true; return true;
} }
return false; return false;
......
...@@ -131,8 +131,7 @@ u.shouldCreateMessage = function (attrs) { ...@@ -131,8 +131,7 @@ u.shouldCreateMessage = function (attrs) {
u.shouldCreateGroupchatMessage = function (attrs) { u.shouldCreateGroupchatMessage = function (attrs) {
return attrs.nick && (u.shouldCreateMessage(attrs) || attrs.is_tombstone); return attrs.nick && (u.shouldCreateMessage(attrs) || attrs.is_tombstone);
}, }
u.isEmptyMessage = function (attrs) { u.isEmptyMessage = function (attrs) {
if (attrs instanceof Model) { if (attrs instanceof Model) {
......
<div class="flyout box-flyout"> <div class="flyout box-flyout">
<div class="chat-head chat-head-chatbox row no-gutters"></div>
<div class="chat-body"> <div class="chat-body">
<div class="chat-content {[ if (o.show_send_button) { ]}chat-content-sendbutton{[ } ]}" aria-live="polite"></div> <div class="chat-content {[ if (o.show_send_button) { ]}chat-content-sendbutton{[ } ]}" aria-live="polite"></div>
<div class="bottom-panel"> <div class="bottom-panel">
......
import { html } from "lit-html"; import { html } from "lit-html";
import { until } from 'lit-html/directives/until.js';
import { __ } from '@converse/headless/i18n'; import { __ } from '@converse/headless/i18n';
import { until } from 'lit-html/directives/until.js';
import avatar from "./avatar.js"; import avatar from "./avatar.js";
import converse from "@converse/headless/converse-core";
import xss from "xss/dist/xss";
const i18n_profile = __('The User\'s Profile Image'); const i18n_profile = __('The User\'s Profile Image');
...@@ -14,10 +12,12 @@ const avatar_data = { ...@@ -14,10 +12,12 @@ const avatar_data = {
'width': 40, 'width': 40,
} }
const tpl_standalone_btns = (o) => o.standalone_btns.reverse().map(b => until(b, ''));
export default (o) => { export default (o) => {
return html` return html`
<div class="chat-head chat-head-chatbox row no-gutters"> <div class="chatbox-title ${ o.status ? '' : "chatbox-title--no-desc"}">
<div class="chatbox-title">
<div class="chatbox-title--row"> <div class="chatbox-title--row">
${ (!o._converse.singleton) ? html`<div class="chatbox-navback"><i class="fa fa-arrow-left"></i></div>` : '' } ${ (!o._converse.singleton) ? html`<div class="chatbox-navback"><i class="fa fa-arrow-left"></i></div>` : '' }
${ (o.type !== o._converse.HEADLINES_TYPE) ? avatar(Object.assign({}, o, avatar_data)) : '' } ${ (o.type !== o._converse.HEADLINES_TYPE) ? avatar(Object.assign({}, o, avatar_data)) : '' }
...@@ -25,9 +25,11 @@ export default (o) => { ...@@ -25,9 +25,11 @@ export default (o) => {
${ o.url ? html`<a href="${o.url}" target="_blank" rel="noopener" class="user">${o.display_name}</a>` : o.display_name} ${ o.url ? html`<a href="${o.url}" target="_blank" rel="noopener" class="user">${o.display_name}</a>` : o.display_name}
</div> </div>
</div> </div>
<div class="chatbox-title__buttons row no-gutters">${ o.buttons.map(b => until(b, '')) }</div> <div class="chatbox-title__buttons row no-gutters">
${ o.dropdown_btns.length ? html`<converse-dropdown .items=${o.dropdown_btns}></converse-dropdown>` : '' }
${ o.standalone_btns.length ? tpl_standalone_btns(o) : '' }
</div> </div>
<p class="chat-head__desc">${ o.status }</p>
</div> </div>
${ o.status ? html`<p class="chat-head__desc">${ o.status }</p>` : '' }
`; `;
} }
import '../components/dropdown.js';
import { __ } from '@converse/headless/i18n';
import { html } from "lit-html"; import { html } from "lit-html";
import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'; import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
import { until } from 'lit-html/directives/until.js'; import { until } from 'lit-html/directives/until.js';
...@@ -5,18 +7,26 @@ import converse from "@converse/headless/converse-core"; ...@@ -5,18 +7,26 @@ import converse from "@converse/headless/converse-core";
import xss from "xss/dist/xss"; import xss from "xss/dist/xss";
const u = converse.env.utils; const u = converse.env.utils;
const i18n_hide_topic = __('Hide the groupchat topic');
const tpl_standalone_btns = (o) => o.standalone_btns.reverse().map(b => until(b, ''));
export default (o) => { export default (o) => {
const subject = o.subject ? u.addHyperlinks(xss.filterXSS(o.subject.text, {'whiteList': {}})) : ''; const subject = o.subject ? u.addHyperlinks(xss.filterXSS(o.subject.text, {'whiteList': {}})) : '';
const show_subject = (subject && !o.hide_subject);
return html` return html`
<div class="chatbox-title"> <div class="chatbox-title ${ show_subject ? '' : "chatbox-title--no-desc"}">
${ (!o._converse.singleton) ? html`<div class="chatbox-navback"><i class="fa fa-arrow-left"></i></div>` : '' } ${ (o._converse.standalone) ? html`<div class="chatbox-navback"><i class="fa fa-arrow-left"></i></div>` : '' }
<div class="chatbox-title__text" title="${ (o._converse.locked_muc_domain !== 'hidden') ? o.jid : '' }">${ o.title }</div> <div class="chatbox-title__text" title="${ (o._converse.locked_muc_domain !== 'hidden') ? o.jid : '' }">${ o.title }
${ (o.bookmarked) ? html`<i class="fa fa-bookmark"></i>` : '' }
</div>
<div class="chatbox-title__buttons row no-gutters"> <div class="chatbox-title__buttons row no-gutters">
${ o.buttons.map(b => until(b, '')) } ${ o.standalone_btns.length ? tpl_standalone_btns(o) : '' }
${ o.dropdown_btns.length ? html`<converse-dropdown .items=${o.dropdown_btns}></converse-dropdown>` : '' }
</div> </div>
</div> </div>
<p class="chat-head__desc">${unsafeHTML(subject)}</p> ${ show_subject ? html`<p class="chat-head__desc" title="${i18n_hide_topic}">${unsafeHTML(subject)}</p>` : '' }
`; `;
} }
...@@ -13,30 +13,10 @@ const PRETTY_CHAT_STATUS = { ...@@ -13,30 +13,10 @@ const PRETTY_CHAT_STATUS = {
'online': 'Online' 'online': 'Online'
}; };
const occupant_hint = (occupant) => __('Click to mention %1$s in your message.', occupant.get('nick')) const i18n_occupant_hint = (occupant) => __('Click to mention %1$s in your message.', occupant.get('nick'))
const i18n_invite = (o) => o._converse.view_mode === 'overlayed' ? __('Invite') : __('Invite someone');
const i18n_invite_hint = __('Invite someone to join this groupchat');
const i18n_participants = __('Participants'); const i18n_participants = __('Participants');
const invite_widget = (o) => {
if (o.invitesAllowed()) {
return html`
<a class="open-invite-modal"
title="${i18n_invite_hint}"
data-toggle="modal"
data-target="#muc-invite-modal"
@click=${o.showInviteModal}>
<i class="btn btn-primary btn-circle fa fa-user-plus"></i>
${i18n_invite(o)}
</a>`;
} else {
return '';
}
}
export default (o) => html` export default (o) => html`
<div class="occupants-header"> <div class="occupants-header">
<i class="hide-occupants fa fa-times"></i> <i class="hide-occupants fa fa-times"></i>
...@@ -51,10 +31,9 @@ export default (o) => html` ...@@ -51,10 +31,9 @@ export default (o) => html`
Object.assign({ Object.assign({
'jid': '', 'jid': '',
'hint_show': PRETTY_CHAT_STATUS[occupant.get('show')], 'hint_show': PRETTY_CHAT_STATUS[occupant.get('show')],
'hint_occupant': occupant_hint(occupant) 'hint_occupant': i18n_occupant_hint(occupant)
}, occupant.toJSON()) }, occupant.toJSON())
); );
}) } }) }
</ul> </ul>
${ invite_widget(o) }
`; `;
...@@ -60,7 +60,13 @@ const fingerprints = (o) => { ...@@ -60,7 +60,13 @@ const fingerprints = (o) => {
`; `;
} }
const remove_button = html`<button type="button" class="btn btn-danger remove-contact"><i class="far fa-trash-alt"> </i>${i18n_remove_contact}</button>`; const remove_button = (o) => {
return html`
<button type="button" @click="${o.removeContact}" class="btn btn-danger remove-contact">
<i class="far fa-trash-alt"></i>${i18n_remove_contact}
</button>
`;
}
export default (o) => html` export default (o) => html`
...@@ -84,7 +90,7 @@ export default (o) => html` ...@@ -84,7 +90,7 @@ export default (o) => html`
<div class="modal-footer"> <div class="modal-footer">
${modal_close_button} ${modal_close_button}
<button type="button" class="btn btn-info refresh-contact"><i class="fa fa-refresh"> </i>${i18n_refresh}</button> <button type="button" class="btn btn-info refresh-contact"><i class="fa fa-refresh"> </i>${i18n_refresh}</button>
${ (o.allow_contact_removal && o.is_roster_contact) ? remove_button : '' } ${ (o.allow_contact_removal && o.is_roster_contact) ? remove_button(o) : '' }
</div> </div>
</div> </div>
......
...@@ -275,6 +275,11 @@ u.hasClass = function (className, el) { ...@@ -275,6 +275,11 @@ u.hasClass = function (className, el) {
return (el instanceof Element) && el.classList.contains(className); return (el instanceof Element) && el.classList.contains(className);
}; };
u.toggleClass = function (className, el) {
u.hasClass(className, el) ? u.removeClass(className, el) : u.addClass(className, el);
}
/** /**
* Add a class to an element. * Add a class to an element.
* @method u#addClass * @method u#addClass
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment