瀏覽代碼

Replace typeahead with awesomplete.

Much smaller library.
No dependence on jQuery.

Updates #779
JC Brand 8 年之前
父節點
當前提交
d2227c8d44
共有 10 個文件被更改,包括 248 次插入106 次删除
  1. 1 2
      bower.json
  2. 2 0
      config.js
  3. 93 32
      css/converse.css
  4. 3 2
      package.json
  5. 103 0
      sass/_awesomplete.scss
  6. 0 34
      sass/_chatrooms.scss
  7. 1 0
      sass/converse.scss
  8. 28 10
      spec/chatroom.js
  9. 1 2
      src/converse-api.js
  10. 16 24
      src/converse-muc.js

+ 1 - 2
bower.json

@@ -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": {},

+ 2 - 0
config.js

@@ -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'] },

+ 93 - 32
css/converse.css

@@ -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 */

+ 3 - 2
package.json

@@ -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 - 0
sass/_awesomplete.scss

@@ -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;
+    }
+}

+ 0 - 34
sass/_chatrooms.scss

@@ -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;
-                    }
-                }
             }
         }
     }

+ 1 - 0
sass/converse.scss

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

+ 28 - 10
spec/chatroom.js

@@ -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>"
+                    );
                 });
             }));
 

+ 1 - 2
src/converse-api.js

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

+ 16 - 24
src/converse-muc.js

@@ -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;
                 }