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",
"bourbon": "~4.2.6",
"crypto-js-evanvosberg": "https://github.com/evanvosberg/crypto-js.git#release-3.1.2-5",
"fontawesome": "~4.1.0",
"typeahead.js": "https://raw.githubusercontent.com/jcbrand/typeahead.js/eedfb10505dd3a20123d1fafc07c1352d83f0ab3/dist/typeahead.jquery.js"
"fontawesome": "~4.1.0"
},
"dependencies": {},
"exportsOverride": {},

View File

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

View File

@ -2384,38 +2384,6 @@
margin: -1px 0 0 -1px;
width: 100%;
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 {
background-color: #2A9D8F; }
@ -2482,4 +2450,97 @@
#conversejs #controlbox #chatrooms .bookmarks-list dl.rooms-list.bookmarks dd.available-chatroom .remove-bookmark {
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 */

View File

@ -33,6 +33,7 @@
},
"devDependencies": {
"almond": "~0.3.1",
"awesomplete": "^1.1.1",
"backbone": "1.3.3",
"backbone.browserStorage": "0.0.3",
"backbone.overview": "0.0.3",
@ -47,9 +48,9 @@
"grunt-json": "^0.2.0",
"http-server": "^0.9.0",
"install": "^0.8.4",
"jasmine": "https://github.com/jcbrand/jasmine.git#439a7f805eeaec0cabe18a8ecf7e47da1a0afa33",
"jed": "0.5.4",
"jquery": "2.2.3",
"sinon": "^1.17.3",
"jquery-easing": "0.0.1",
"jquery.browser": ">=0.1.0",
"jshint": "^2.9.4",
@ -58,12 +59,12 @@
"moment": "~2.13.0",
"npm": "^4.1.1",
"otr": "0.2.16",
"jasmine": "https://github.com/jcbrand/jasmine.git#439a7f805eeaec0cabe18a8ecf7e47da1a0afa33",
"phantom-jasmine": "0.1.8",
"phantomjs": "~1.9.7-1",
"pluggable.js": "https://github.com/jcbrand/pluggable.js.git#e5fc6a78dd568a120674ff7325da038d5ba9b334",
"po2json": "^0.4.4",
"requirejs": "2.3.2",
"sinon": "^1.17.3",
"snyk": "^1.21.2",
"strophe.js": "1.2.12",
"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;
width: 100%;
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 "minimized_chats";
@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) {
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(window, 'prompt').andCallFake(function () {
return null;
return "Please join!";
});
var $input;
var view = _converse.chatboxviews.get('lounge@localhost');
spyOn(view, 'directInvite').andCallThrough();
var $input;
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.tt-input');
var $hint = view.$el.find('input.invited-contact.tt-hint');
$input = view.$el.find('input.invited-contact');
runs (function () {
expect($input.length).toBe(1);
expect($input.attr('placeholder')).toBe('Invite');
$input.val("Felix");
$input.trigger('input');
$input[0].dispatchEvent(new Event('input'));
});
waits(350); // Needed, due to debounce
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($hint.val()).toBe('Felix Amsel');
var $sugg = view.$el.find('[data-jid="felix.amsel@localhost"]');
expect($sugg.length).toBe(1);
$sugg.trigger('click');
expect($hint[0].textContent).toBe('Felix Amsel');
expect($hint.length).toBe(1);
var evt = new Event('mousedown', {'bubbles': true});
evt.button = 0; // For some reason awesomplete wants this
$hint[0].dispatchEvent(evt);
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 */
(function (root, factory) {
define([
"jquery",
define(["jquery",
"lodash",
"moment_with_locales",
"strophe",

View File

@ -26,7 +26,7 @@
"tpl!room_description",
"tpl!room_item",
"tpl!room_panel",
"typeahead",
"awesomplete",
"converse-chatview"
], factory);
}(this, function (
@ -44,7 +44,8 @@
tpl_occupant,
tpl_room_description,
tpl_room_item,
tpl_room_panel
tpl_room_panel,
Awesomplete
) {
"use strict";
var ROOMS_PANEL_ID = 'chatrooms';
@ -1936,7 +1937,7 @@
})
);
if (_converse.allow_muc_invitations) {
return this.initInviteWidget();
_converse.api.waitUntil('rosterContactsFetched').then(this.initInviteWidget.bind(this));
}
return this;
},
@ -2037,33 +2038,24 @@
},
initInviteWidget: function () {
var $el = this.$('input.invited-contact');
$el.typeahead({
minLength: 1,
highlight: true
}, {
name: 'contacts-dataset',
source: function (q, cb) {
cb(_.map(
_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>')
}
var el = this.el.querySelector('input.invited-contact');
var list = _converse.roster.map(function (item) {
var label = item.get('fullname') || item.get('jid');
return {'label': label, 'value':item.get('jid')};
});
var awesomplete = new Awesomplete(el, {
'minChars': 1,
'list': list
});
$el.on('typeahead:selected', function (ev, suggestion, dname) {
el.addEventListener('awesomplete-selectcomplete', function (suggestion) {
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.")
);
if (reason !== null) {
this.chatroomview.directInvite(suggestion.jid, reason);
this.chatroomview.directInvite(suggestion.text.value, reason);
}
$(ev.target).typeahead('val', '');
el.value = '';
}.bind(this));
return this;
}