Replace typeahead with awesomplete.

Much smaller library.
No dependence on jQuery.

Updates #779
This commit is contained in:
JC Brand 2017-02-14 11:19:01 +01:00
parent 994c961d9c
commit d2227c8d44
10 changed files with 248 additions and 106 deletions

View File

@ -7,8 +7,7 @@
"bootstrap": "~3.2.0", "bootstrap": "~3.2.0",
"bourbon": "~4.2.6", "bourbon": "~4.2.6",
"crypto-js-evanvosberg": "https://github.com/evanvosberg/crypto-js.git#release-3.1.2-5", "crypto-js-evanvosberg": "https://github.com/evanvosberg/crypto-js.git#release-3.1.2-5",
"fontawesome": "~4.1.0", "fontawesome": "~4.1.0"
"typeahead.js": "https://raw.githubusercontent.com/jcbrand/typeahead.js/eedfb10505dd3a20123d1fafc07c1352d83f0ab3/dist/typeahead.jquery.js"
}, },
"dependencies": {}, "dependencies": {},
"exportsOverride": {}, "exportsOverride": {},

View File

@ -15,6 +15,7 @@ require.config({
baseUrl: '.', baseUrl: '.',
paths: { paths: {
"almond": "node_modules/almond/almond", "almond": "node_modules/almond/almond",
"awesomplete": "node_modules/awesomplete/awesomplete",
"backbone": "node_modules/backbone/backbone", "backbone": "node_modules/backbone/backbone",
"backbone.browserStorage": "node_modules/backbone.browserStorage/backbone.browserStorage", "backbone.browserStorage": "node_modules/backbone.browserStorage/backbone.browserStorage",
"backbone.overview": "node_modules/backbone.overview/backbone.overview", "backbone.overview": "node_modules/backbone.overview/backbone.overview",
@ -213,6 +214,7 @@ require.config({
// define module dependencies for modules not using define // define module dependencies for modules not using define
shim: { shim: {
'awesomplete': { exports: 'Awesomplete' },
'backbone': { deps: ['underscore'] }, 'backbone': { deps: ['underscore'] },
'bigint': { deps: ['crypto'] }, 'bigint': { deps: ['crypto'] },
'crypto.aes': { deps: ['crypto.cipher-core'] }, 'crypto.aes': { deps: ['crypto.cipher-core'] },

View File

@ -2384,38 +2384,6 @@
margin: -1px 0 0 -1px; margin: -1px 0 0 -1px;
width: 100%; width: 100%;
border: 1px solid #999; } border: 1px solid #999; }
#converse-embedded-chat .chatroom .room-invite .invited-contact.tt-input,
#conversejs .chatroom .room-invite .invited-contact.tt-input {
width: 100%;
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gkBCjE0uzKkOgAAAidJREFUKM+N0k+IEnEUB/D3cyscdagkWpHV0WGWREXm0AgOGkSJ07kh2UXYU5cOewm6Bp0KXG/tpSCv6hyEFQIhMEaE3yERYfwTOoqKGLQxDAbqYadLgu7J7/XxeY/3ez8EACDLsgljfMfj8ZxUKhXXYDAAnueBoqgyAMipVOovXAuSZdnUaDQeDofDs16vFyUIAjRNUwmCoG02G1AUdZ5IJN7GYrHfm3AvEAjcnUwmX0ajUdRqtV74fL6sruufKYoa6bp+fzabPUMI7ZfL5eImNHk8npNerxc1m80XHMe98fv9H3K5XDkSibxjWfb1arWaYoyPMMbCFqxUKi6CIODw8LDmdDq7oigaAACiKK5omv7KcdylpmlIkiTHFlRVFTRNUxVFqa/ROqIoGoqi5A3DgFartfU4Jp7ngSAI2uVyPZIk6dZmUZKk2w6H4xghBPF4HK7vWLbZbDCdTp+rqvpUkiS0RvV6/bTf7x8wDHMViURqm/AGAMgURZ232+1X1Wr102KxuEwmk3lZlo/7/f7BcrkkSZKs2e12tHXH/x/gHsY4jTE+0jQNGYYBCCFgGOaKJMkfjUaDZximGQ6HXzSbzZ+ZTMbY6oIxFgqFgqPT6YAgCMBxXM1ut6N0Op0fj8chi8XyjWXZ98Fg8DuCHZLNZh+USqWP8/n8idvt/hUKhV7u7QK9Xu8fmqanAJBQVXUfAGY7TQQAKBaLN8fjsdDtdh/run72Dzhf7XLe2UevAAAAAElFTkSuQmCC) no-repeat right 3px center; }
#converse-embedded-chat .chatroom .room-invite .invited-contact.tt-input:focus,
#conversejs .chatroom .room-invite .invited-contact.tt-input:focus {
border-color: #E76F51; }
#converse-embedded-chat .chatroom .room-invite .invited-contact.tt-hint,
#conversejs .chatroom .room-invite .invited-contact.tt-hint {
color: transparent;
background-color: white; }
#converse-embedded-chat .chatroom .room-invite .tt-dropdown-menu,
#conversejs .chatroom .room-invite .tt-dropdown-menu {
width: 96%;
max-height: 250px;
background: #E76F51;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
overflow-y: auto; }
#converse-embedded-chat .chatroom .room-invite .tt-dropdown-menu .tt-suggestion p,
#conversejs .chatroom .room-invite .tt-dropdown-menu .tt-suggestion p {
color: white;
cursor: pointer;
font-size: 11px;
text-overflow: ellipsis;
overflow-x: hidden; }
#converse-embedded-chat .chatroom .room-invite .tt-dropdown-menu .tt-suggestion p:hover,
#conversejs .chatroom .room-invite .tt-dropdown-menu .tt-suggestion p:hover {
background-color: #FF977C; }
#converse-embedded-chat .chatroom .room-invite .tt-dropdown-menu .tt-suggestion .tt-highlight,
#conversejs .chatroom .room-invite .tt-dropdown-menu .tt-suggestion .tt-highlight {
background-color: #D24E2B; }
#conversejs .chatbox.headlines .chat-head.chat-head-chatbox { #conversejs .chatbox.headlines .chat-head.chat-head-chatbox {
background-color: #2A9D8F; } background-color: #2A9D8F; }
@ -2482,4 +2450,97 @@
#conversejs #controlbox #chatrooms .bookmarks-list dl.rooms-list.bookmarks dd.available-chatroom .remove-bookmark { #conversejs #controlbox #chatrooms .bookmarks-list dl.rooms-list.bookmarks dd.available-chatroom .remove-bookmark {
float: right; } float: right; }
#converse-embedded-chat,
#conversejs {
/* Pointer */ }
#converse-embedded-chat [hidden],
#conversejs [hidden] {
display: none; }
#converse-embedded-chat .visually-hidden,
#conversejs .visually-hidden {
position: absolute;
clip: rect(0, 0, 0, 0); }
#converse-embedded-chat div.awesomplete,
#conversejs div.awesomplete {
display: inline-block;
position: relative; }
#converse-embedded-chat div.awesomplete > input,
#conversejs div.awesomplete > input {
display: block; }
#converse-embedded-chat div.awesomplete > ul,
#conversejs div.awesomplete > ul {
position: absolute;
left: 0;
right: 0;
z-index: 1;
min-width: 100%;
box-sizing: border-box;
list-style: none;
padding: 0;
border-radius: .3em;
margin: .2em 0 0;
background: rgba(255, 255, 255, 0.9);
background: linear-gradient(to bottom right, white, rgba(255, 255, 255, 0.8));
border: 1px solid rgba(0, 0, 0, 0.3);
box-shadow: 0.05em 0.2em 0.6em rgba(0, 0, 0, 0.2);
text-shadow: none; }
#converse-embedded-chat div.awesomplete > ul[hidden],
#converse-embedded-chat div.awesomplete > ul:empty,
#conversejs div.awesomplete > ul[hidden],
#conversejs div.awesomplete > ul:empty {
display: none; }
@supports (transform: scale(0)) {
#converse-embedded-chat div.awesomplete > ul,
#conversejs div.awesomplete > ul {
transition: 0.3s cubic-bezier(0.4, 0.2, 0.5, 1.4);
transform-origin: 1.43em -.43em; }
#converse-embedded-chat div.awesomplete > ul[hidden],
#converse-embedded-chat div.awesomplete > ul:empty,
#conversejs div.awesomplete > ul[hidden],
#conversejs div.awesomplete > ul:empty {
opacity: 0;
transform: scale(0);
display: block;
transition-timing-function: ease; } }
#converse-embedded-chat div.awesomplete > ul:before,
#conversejs div.awesomplete > ul:before {
content: "";
position: absolute;
top: -.43em;
left: 1em;
width: 0;
height: 0;
padding: .4em;
background: white;
border: inherit;
border-right: 0;
border-bottom: 0;
-webkit-transform: rotate(45deg);
transform: rotate(45deg); }
#converse-embedded-chat div.awesomplete > ul > li,
#conversejs div.awesomplete > ul > li {
text-overflow: ellipsis;
overflow-x: hidden;
position: relative;
padding: .2em .5em;
cursor: pointer; }
#converse-embedded-chat div.awesomplete > ul > li:hover,
#conversejs div.awesomplete > ul > li:hover {
background: #b8d3e0;
color: black; }
#converse-embedded-chat div.awesomplete > ul > li[aria-selected="true"],
#conversejs div.awesomplete > ul > li[aria-selected="true"] {
background: #3d6d8f;
color: white; }
#converse-embedded-chat div.awesomplete mark,
#conversejs div.awesomplete mark {
background: #eaff00; }
#converse-embedded-chat div.awesomplete li:hover mark,
#conversejs div.awesomplete li:hover mark {
background: #b5d100; }
#converse-embedded-chat div.awesomplete li[aria-selected="true"] mark,
#conversejs div.awesomplete li[aria-selected="true"] mark {
background: #3d6b00;
color: inherit; }
/*# sourceMappingURL=converse.css.map */ /*# sourceMappingURL=converse.css.map */

View File

@ -33,6 +33,7 @@
}, },
"devDependencies": { "devDependencies": {
"almond": "~0.3.1", "almond": "~0.3.1",
"awesomplete": "^1.1.1",
"backbone": "1.3.3", "backbone": "1.3.3",
"backbone.browserStorage": "0.0.3", "backbone.browserStorage": "0.0.3",
"backbone.overview": "0.0.3", "backbone.overview": "0.0.3",
@ -47,9 +48,9 @@
"grunt-json": "^0.2.0", "grunt-json": "^0.2.0",
"http-server": "^0.9.0", "http-server": "^0.9.0",
"install": "^0.8.4", "install": "^0.8.4",
"jasmine": "https://github.com/jcbrand/jasmine.git#439a7f805eeaec0cabe18a8ecf7e47da1a0afa33",
"jed": "0.5.4", "jed": "0.5.4",
"jquery": "2.2.3", "jquery": "2.2.3",
"sinon": "^1.17.3",
"jquery-easing": "0.0.1", "jquery-easing": "0.0.1",
"jquery.browser": ">=0.1.0", "jquery.browser": ">=0.1.0",
"jshint": "^2.9.4", "jshint": "^2.9.4",
@ -58,12 +59,12 @@
"moment": "~2.13.0", "moment": "~2.13.0",
"npm": "^4.1.1", "npm": "^4.1.1",
"otr": "0.2.16", "otr": "0.2.16",
"jasmine": "https://github.com/jcbrand/jasmine.git#439a7f805eeaec0cabe18a8ecf7e47da1a0afa33",
"phantom-jasmine": "0.1.8", "phantom-jasmine": "0.1.8",
"phantomjs": "~1.9.7-1", "phantomjs": "~1.9.7-1",
"pluggable.js": "https://github.com/jcbrand/pluggable.js.git#e5fc6a78dd568a120674ff7325da038d5ba9b334", "pluggable.js": "https://github.com/jcbrand/pluggable.js.git#e5fc6a78dd568a120674ff7325da038d5ba9b334",
"po2json": "^0.4.4", "po2json": "^0.4.4",
"requirejs": "2.3.2", "requirejs": "2.3.2",
"sinon": "^1.17.3",
"snyk": "^1.21.2", "snyk": "^1.21.2",
"strophe.js": "1.2.12", "strophe.js": "1.2.12",
"strophejs-plugins": "0.0.7", "strophejs-plugins": "0.0.7",

103
sass/_awesomplete.scss Normal file
View File

@ -0,0 +1,103 @@
#converse-embedded-chat,
#conversejs {
[hidden] { display: none; }
.visually-hidden {
position: absolute;
clip: rect(0, 0, 0, 0);
}
div.awesomplete {
display: inline-block;
position: relative;
}
div.awesomplete > input {
display: block;
}
div.awesomplete > ul {
position: absolute;
left: 0;
right: 0;
z-index: 1;
min-width: 100%;
box-sizing: border-box;
list-style: none;
padding: 0;
border-radius: .3em;
margin: .2em 0 0;
background: hsla(0,0%,100%,.9);
background: linear-gradient(to bottom right, white, hsla(0,0%,100%,.8));
border: 1px solid rgba(0,0,0,.3);
box-shadow: .05em .2em .6em rgba(0,0,0,.2);
text-shadow: none;
}
div.awesomplete > ul[hidden],
div.awesomplete > ul:empty {
display: none;
}
@supports (transform: scale(0)) {
div.awesomplete > ul {
transition: .3s cubic-bezier(.4,.2,.5,1.4);
transform-origin: 1.43em -.43em;
}
div.awesomplete > ul[hidden],
div.awesomplete > ul:empty {
opacity: 0;
transform: scale(0);
display: block;
transition-timing-function: ease;
}
}
/* Pointer */
div.awesomplete > ul:before {
content: "";
position: absolute;
top: -.43em;
left: 1em;
width: 0; height: 0;
padding: .4em;
background: white;
border: inherit;
border-right: 0;
border-bottom: 0;
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
}
div.awesomplete > ul > li {
text-overflow: ellipsis;
overflow-x: hidden;
position: relative;
padding: .2em .5em;
cursor: pointer;
}
div.awesomplete > ul > li:hover {
background: hsl(200, 40%, 80%);
color: black;
}
div.awesomplete > ul > li[aria-selected="true"] {
background: hsl(205, 40%, 40%);
color: white;
}
div.awesomplete mark {
background: hsl(65, 100%, 50%);
}
div.awesomplete li:hover mark {
background: hsl(68, 100%, 41%);
}
div.awesomplete li[aria-selected="true"] mark {
background: hsl(86, 100%, 21%);
color: inherit;
}
}

View File

@ -166,40 +166,6 @@
margin: -1px 0 0 -1px; margin: -1px 0 0 -1px;
width: 100%; width: 100%;
border: 1px solid #999; border: 1px solid #999;
&.tt-input {
width: 100%;
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gkBCjE0uzKkOgAAAidJREFUKM+N0k+IEnEUB/D3cyscdagkWpHV0WGWREXm0AgOGkSJ07kh2UXYU5cOewm6Bp0KXG/tpSCv6hyEFQIhMEaE3yERYfwTOoqKGLQxDAbqYadLgu7J7/XxeY/3ez8EACDLsgljfMfj8ZxUKhXXYDAAnueBoqgyAMipVOovXAuSZdnUaDQeDofDs16vFyUIAjRNUwmCoG02G1AUdZ5IJN7GYrHfm3AvEAjcnUwmX0ajUdRqtV74fL6sruufKYoa6bp+fzabPUMI7ZfL5eImNHk8npNerxc1m80XHMe98fv9H3K5XDkSibxjWfb1arWaYoyPMMbCFqxUKi6CIODw8LDmdDq7oigaAACiKK5omv7KcdylpmlIkiTHFlRVFTRNUxVFqa/ROqIoGoqi5A3DgFartfU4Jp7ngSAI2uVyPZIk6dZmUZKk2w6H4xghBPF4HK7vWLbZbDCdTp+rqvpUkiS0RvV6/bTf7x8wDHMViURqm/AGAMgURZ232+1X1Wr102KxuEwmk3lZlo/7/f7BcrkkSZKs2e12tHXH/x/gHsY4jTE+0jQNGYYBCCFgGOaKJMkfjUaDZximGQ6HXzSbzZ+ZTMbY6oIxFgqFgqPT6YAgCMBxXM1ut6N0Op0fj8chi8XyjWXZ98Fg8DuCHZLNZh+USqWP8/n8idvt/hUKhV7u7QK9Xu8fmqanAJBQVXUfAGY7TQQAKBaLN8fjsdDtdh/run72Dzhf7XLe2UevAAAAAElFTkSuQmCC ) no-repeat right 3px center;
&:focus {
border-color: $chatroom-head-color;
}
}
&.tt-hint {
color: transparent;
background-color: white;
}
}
.tt-dropdown-menu {
width: 96%;
max-height: 250px;
background: $chatroom-head-color;
border-bottom-right-radius: $chatbox-border-radius;
border-bottom-left-radius: $chatbox-border-radius;
overflow-y: auto;
.tt-suggestion {
p {
color: white;
cursor: pointer;
font-size: 11px;
text-overflow: ellipsis;
overflow-x: hidden;
&:hover {
background-color: $chatroom-color-light;
}
}
.tt-highlight {
background-color: $chatroom-color-dark;
}
}
} }
} }
} }

View File

@ -18,3 +18,4 @@
@import "headline"; @import "headline";
@import "minimized_chats"; @import "minimized_chats";
@import "bookmarks"; @import "bookmarks";
@import "awesomplete"

View File

@ -646,30 +646,48 @@
it("allows the user to invite their roster contacts to enter the chat room", mock.initConverse(function (_converse) { it("allows the user to invite their roster contacts to enter the chat room", mock.initConverse(function (_converse) {
test_utils.openChatRoom(_converse, 'lounge', 'localhost', 'dummy'); test_utils.openChatRoom(_converse, 'lounge', 'localhost', 'dummy');
test_utils.createContacts(_converse, 'current'); // We need roster contacts, so that we have someone to invite
// Since we don't actually fetch roster contacts, we need to
// cheat here and emit the event.
_converse.emit('rosterContactsFetched');
spyOn(_converse, 'emit'); spyOn(_converse, 'emit');
spyOn(window, 'prompt').andCallFake(function () { spyOn(window, 'prompt').andCallFake(function () {
return null; return "Please join!";
}); });
var $input;
var view = _converse.chatboxviews.get('lounge@localhost'); var view = _converse.chatboxviews.get('lounge@localhost');
spyOn(view, 'directInvite').andCallThrough();
var $input;
view.$el.find('.chat-area').remove(); view.$el.find('.chat-area').remove();
test_utils.createContacts(_converse, 'current'); // We need roster contacts, so that we have someone to invite $input = view.$el.find('input.invited-contact');
$input = view.$el.find('input.invited-contact.tt-input');
var $hint = view.$el.find('input.invited-contact.tt-hint');
runs (function () { runs (function () {
expect($input.length).toBe(1); expect($input.length).toBe(1);
expect($input.attr('placeholder')).toBe('Invite'); expect($input.attr('placeholder')).toBe('Invite');
$input.val("Felix"); $input.val("Felix");
$input.trigger('input'); $input[0].dispatchEvent(new Event('input'));
}); });
waits(350); // Needed, due to debounce waits(350); // Needed, due to debounce
runs (function () { runs (function () {
var sent_stanza;
spyOn(_converse.connection, 'send').andCallFake(function (stanza) {
sent_stanza = stanza;
});
var $hint = $input.siblings('ul').children('li');
expect($input.val()).toBe('Felix'); expect($input.val()).toBe('Felix');
expect($hint.val()).toBe('Felix Amsel'); expect($hint[0].textContent).toBe('Felix Amsel');
var $sugg = view.$el.find('[data-jid="felix.amsel@localhost"]'); expect($hint.length).toBe(1);
expect($sugg.length).toBe(1); var evt = new Event('mousedown', {'bubbles': true});
$sugg.trigger('click'); evt.button = 0; // For some reason awesomplete wants this
$hint[0].dispatchEvent(evt);
expect(window.prompt).toHaveBeenCalled(); expect(window.prompt).toHaveBeenCalled();
expect(view.directInvite).toHaveBeenCalled();
expect(sent_stanza.toLocaleString()).toBe(
"<message from='dummy@localhost/resource' to='felix.amsel@localhost' id='" +
sent_stanza.nodeTree.getAttribute('id') +
"' xmlns='jabber:client'>"+
"<x xmlns='jabber:x:conference' jid='lounge@localhost' reason='Please join!'/>"+
"</message>"
);
}); });
})); }));

View File

@ -6,8 +6,7 @@
// //
/*global define */ /*global define */
(function (root, factory) { (function (root, factory) {
define([ define(["jquery",
"jquery",
"lodash", "lodash",
"moment_with_locales", "moment_with_locales",
"strophe", "strophe",

View File

@ -26,7 +26,7 @@
"tpl!room_description", "tpl!room_description",
"tpl!room_item", "tpl!room_item",
"tpl!room_panel", "tpl!room_panel",
"typeahead", "awesomplete",
"converse-chatview" "converse-chatview"
], factory); ], factory);
}(this, function ( }(this, function (
@ -44,7 +44,8 @@
tpl_occupant, tpl_occupant,
tpl_room_description, tpl_room_description,
tpl_room_item, tpl_room_item,
tpl_room_panel tpl_room_panel,
Awesomplete
) { ) {
"use strict"; "use strict";
var ROOMS_PANEL_ID = 'chatrooms'; var ROOMS_PANEL_ID = 'chatrooms';
@ -1936,7 +1937,7 @@
}) })
); );
if (_converse.allow_muc_invitations) { if (_converse.allow_muc_invitations) {
return this.initInviteWidget(); _converse.api.waitUntil('rosterContactsFetched').then(this.initInviteWidget.bind(this));
} }
return this; return this;
}, },
@ -2037,33 +2038,24 @@
}, },
initInviteWidget: function () { initInviteWidget: function () {
var $el = this.$('input.invited-contact'); var el = this.el.querySelector('input.invited-contact');
$el.typeahead({ var list = _converse.roster.map(function (item) {
minLength: 1, var label = item.get('fullname') || item.get('jid');
highlight: true return {'label': label, 'value':item.get('jid')};
}, { });
name: 'contacts-dataset', var awesomplete = new Awesomplete(el, {
source: function (q, cb) { 'minChars': 1,
cb(_.map( 'list': list
_converse.roster.filter(utils.contains(['fullname', 'jid'], q)),
function (n) {
return {value: n.get('fullname'), jid: n.get('jid')};
}
));
},
templates: {
suggestion: _.template('<p data-jid="{{jid}}">{{value}}</p>')
}
}); });
$el.on('typeahead:selected', function (ev, suggestion, dname) { el.addEventListener('awesomplete-selectcomplete', function (suggestion) {
var reason = prompt( var reason = prompt(
__(___('You are about to invite %1$s to the chat room "%2$s". '), suggestion.value, this.model.get('id')) + __(___('You are about to invite %1$s to the chat room "%2$s". '), suggestion.text.label, this.model.get('id')) +
__("You may optionally include a message, explaining the reason for the invitation.") __("You may optionally include a message, explaining the reason for the invitation.")
); );
if (reason !== null) { if (reason !== null) {
this.chatroomview.directInvite(suggestion.jid, reason); this.chatroomview.directInvite(suggestion.text.value, reason);
} }
$(ev.target).typeahead('val', ''); el.value = '';
}.bind(this)); }.bind(this));
return this; return this;
} }