Pārlūkot izejas kodu

Bugfix. Validation messages wasn't being shown

due to Awesomplete doing things to the DOM and then confusing Snabbdom.
JC Brand 7 gadi atpakaļ
vecāks
revīzija
57b4dec5a0
5 mainītis faili ar 374 papildinājumiem un 354 dzēšanām
  1. 345 321
      dist/converse.js
  2. 3 4
      package-lock.json
  3. 17 18
      src/converse-rosterview.js
  4. 3 5
      src/templates/add_contact_modal.html
  5. 6 6
      webpack.config.js

+ 345 - 321
dist/converse.js

@@ -2170,10 +2170,10 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
 
 
 /***/ }),
 /***/ }),
 
 
-/***/ "./node_modules/backbone.overview/dist/backbone.orderedlistview.js":
-/*!*************************************************************************!*\
-  !*** ./node_modules/backbone.overview/dist/backbone.orderedlistview.js ***!
-  \*************************************************************************/
+/***/ "./node_modules/backbone.overview/backbone.orderedlistview.js":
+/*!********************************************************************!*\
+  !*** ./node_modules/backbone.overview/backbone.orderedlistview.js ***!
+  \********************************************************************/
 /*! no static exports found */
 /*! no static exports found */
 /***/ (function(module, exports, __webpack_require__) {
 /***/ (function(module, exports, __webpack_require__) {
 
 
@@ -2188,101 +2188,99 @@ backbone.nativeview = __webpack_require__(/*! backbone.nativeview */ "./node_mod
  * Licensed under the Mozilla Public License (MPL) 
  * Licensed under the Mozilla Public License (MPL) 
  */
  */
 (function (root, factory) {
 (function (root, factory) {
-  if (true) {
-    !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! underscore */ "./src/underscore-shim.js"), __webpack_require__(/*! backbone */ "./node_modules/backbone/backbone.js"), __webpack_require__(/*! backbone.overview */ "./node_modules/backbone.overview/dist/backbone.overview.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory),
+    if (true) {
+        !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! underscore */ "./src/underscore-shim.js"), __webpack_require__(/*! backbone */ "./node_modules/backbone/backbone.js"), __webpack_require__(/*! backbone.overview */ "./node_modules/backbone.overview/backbone.overview.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory),
 				__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?
 				__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?
 				(__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__),
 				(__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__),
 				__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
 				__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
-  } else {}
-})(this, function (_, Backbone) {
-  "use strict";
+   } else {}
+}(this, function (_, Backbone) {
+    "use strict";
 
 
-  Backbone.OrderedListView = Backbone.Overview.extend({
-    /* An OrderedListView is a special type of Overview which adds some
-     * methods and conventions for rendering an ordered list of elements.
-     */
-    // The `listItems` attribute denotes the path (from this View) to the
-    // list of items.
-    listItems: 'model',
-    // The `sortEvent` attribute specifies the event which should cause the
-    // ordered list to be sorted.
-    sortEvent: 'change',
-    // The `listSelector` is the selector used to query for the DOM list
-    // element which contains the ordered items.
-    listSelector: '.ordered-items',
-    // The `itemView` is constructor which should be called to create a
-    // View for a new item.
-    ItemView: undefined,
-    // The `subviewIndex` is the attribute of the list element model which
-    // acts as the index of the subview in the overview.
-    // An overview is a "Collection" of views, and they can be retrieved
-    // via an index. By default this is the 'id' attribute, but it could be
-    // set to something else.
-    subviewIndex: 'id',
-    initialize: function initialize() {
-      this.sortEventually = _.debounce(this.sortAndPositionAllItems.bind(this), 500);
-      this.items = _.get(this, this.listItems);
-      this.items.on('add', this.sortAndPositionAllItems, this);
-      this.items.on('remove', this.removeView, this);
-
-      if (!_.isNil(this.sortEvent)) {
-        this.items.on(this.sortEvent, this.sortEventually, this);
-      }
-    },
-    createItemView: function createItemView(item) {
-      var item_view = this.get(item.get(this.subviewIndex));
-
-      if (!item_view) {
-        item_view = new this.ItemView({
-          model: item
-        });
-        this.add(item.get(this.subviewIndex), item_view);
-      } else {
-        item_view.model = item;
-        item_view.initialize();
-      }
+    Backbone.OrderedListView = Backbone.Overview.extend({
+        /* An OrderedListView is a special type of Overview which adds some
+         * methods and conventions for rendering an ordered list of elements.
+         */
 
 
-      item_view.render();
-      return item_view;
-    },
-    removeView: function removeView(item) {
-      this.remove(item.get(this.subviewIndex));
-    },
-    sortAndPositionAllItems: function sortAndPositionAllItems() {
-      var _this = this;
+        // The `listItems` attribute denotes the path (from this View) to the
+        // list of items.
+        listItems: 'model',
+        // The `sortEvent` attribute specifies the event which should cause the
+        // ordered list to be sorted.
+        sortEvent: 'change',
+        // The `listSelector` is the selector used to query for the DOM list
+        // element which contains the ordered items.
+        listSelector: '.ordered-items',
+        // The `itemView` is constructor which should be called to create a
+        // View for a new item.
+        ItemView: undefined,
+        // The `subviewIndex` is the attribute of the list element model which
+        // acts as the index of the subview in the overview.
+        // An overview is a "Collection" of views, and they can be retrieved
+        // via an index. By default this is the 'id' attribute, but it could be
+        // set to something else.
+        subviewIndex: 'id',
+
+        initialize () {
+            this.sortEventually = _.debounce(
+                this.sortAndPositionAllItems.bind(this), 500);
+
+            this.items = _.get(this, this.listItems);
+            this.items.on('add', this.sortAndPositionAllItems, this);
+            this.items.on('remove', this.removeView, this);
+            if (!_.isNil(this.sortEvent)) {
+                this.items.on(this.sortEvent, this.sortEventually, this);
+            }
+        },
 
 
-      if (!this.items.length) {
-        return;
-      }
+        createItemView (item) {
+            let item_view = this.get(item.get(this.subviewIndex));
+            if (!item_view) {
+                item_view = new this.ItemView({model: item});
+                this.add(item.get(this.subviewIndex), item_view);
+            } else {
+                item_view.model = item;
+                item_view.initialize();
+            }
+            item_view.render();
+            return item_view;
+        },
 
 
-      this.items.sort();
-      var list_el = this.el.querySelector(this.listSelector);
-      var div = document.createElement('div');
-      list_el.parentNode.replaceChild(div, list_el);
-      this.items.each(function (item) {
-        var view = _this.get(item.get(_this.subviewIndex));
+        removeView (item) {
+            this.remove(item.get(this.subviewIndex));
+        },
 
 
-        if (_.isUndefined(view)) {
-          view = _this.createItemView(item);
+        sortAndPositionAllItems () {
+            if (!this.items.length) {
+                return;
+            }
+            this.items.sort();
+
+            const list_el = this.el.querySelector(this.listSelector);
+            const div = document.createElement('div');
+            list_el.parentNode.replaceChild(div, list_el);
+            this.items.each((item) => {
+                let view = this.get(item.get(this.subviewIndex));
+                if (_.isUndefined(view)) {
+                    view = this.createItemView(item)
+                }
+                list_el.insertAdjacentElement('beforeend', view.el);
+            });
+            div.parentNode.replaceChild(list_el, div);
         }
         }
+    });
 
 
-        list_el.insertAdjacentElement('beforeend', view.el);
-      });
-      div.parentNode.replaceChild(list_el, div);
-    }
-  });
-  return Backbone.OrderedListView;
-});
+    return Backbone.OrderedListView;
+}));
 
 
-//# sourceMappingURL=backbone.orderedlistview.js.map
 
 
 
 
 /***/ }),
 /***/ }),
 
 
-/***/ "./node_modules/backbone.overview/dist/backbone.overview.js":
-/*!******************************************************************!*\
-  !*** ./node_modules/backbone.overview/dist/backbone.overview.js ***!
-  \******************************************************************/
+/***/ "./node_modules/backbone.overview/backbone.overview.js":
+/*!*************************************************************!*\
+  !*** ./node_modules/backbone.overview/backbone.overview.js ***!
+  \*************************************************************/
 /*! no static exports found */
 /*! no static exports found */
 /***/ (function(module, exports, __webpack_require__) {
 /***/ (function(module, exports, __webpack_require__) {
 
 
@@ -2297,168 +2295,171 @@ backbone.nativeview = __webpack_require__(/*! backbone.nativeview */ "./node_mod
  * Licensed under the Mozilla Public License (MPL) 
  * Licensed under the Mozilla Public License (MPL) 
  */
  */
 (function (root, factory) {
 (function (root, factory) {
-  if (true) {
-    !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! underscore */ "./src/underscore-shim.js"), __webpack_require__(/*! backbone */ "./node_modules/backbone/backbone.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory),
+    if (true) {
+        !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! underscore */ "./src/underscore-shim.js"), __webpack_require__(/*! backbone */ "./node_modules/backbone/backbone.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory),
 				__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?
 				__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?
 				(__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__),
 				(__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__),
 				__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
 				__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
-  } else {}
-})(this, function (_, Backbone) {
-  "use strict";
-
-  var View = _.isUndefined(Backbone.NativeView) ? Backbone.View : Backbone.NativeView;
+   } else {}
+}(this, function (_, Backbone) {
+    "use strict";
 
 
-  var Overview = Backbone.Overview = function (options) {
-    /* An Overview is a View that contains and keeps track of sub-views.
-     * Kind of like what a Collection is to a Model.
-     */
-    var that = this;
-    this.views = {};
-    this.keys = _.partial(_.keys, this.views);
-    this.getAll = _.partial(_.identity, this.views);
+    const View = _.isUndefined(Backbone.NativeView) ? Backbone.View : Backbone.NativeView;
 
 
-    this.get = function (id) {
-      return that.views[id];
-    };
+    const Overview = Backbone.Overview = function (options) {
+        /* An Overview is a View that contains and keeps track of sub-views.
+         * Kind of like what a Collection is to a Model.
+         */
+        var that = this;
+        this.views = {};
+        this.keys = _.partial(_.keys, this.views);
+        this.getAll = _.partial(_.identity, this.views);
 
 
-    this.xget = function (id) {
-      /* Exclusive get. Returns all instances except the given id. */
-      return _.filter(that.views, function (view, vid) {
-        return vid !== id;
-      });
-    };
+        this.get = function (id) {
+            return that.views[id];
+        };
 
 
-    this.add = function (id, view) {
-      that.views[id] = view;
-      return view;
-    };
+        this.xget = function (id) {
+            /* Exclusive get. Returns all instances except the given id. */
+            return _.filter(that.views, function (view, vid) {
+                return vid !== id;
+            });
+        };
 
 
-    this.remove = function (id) {
-      if (typeof id === "undefined") {
-        new View().remove.apply(that);
-      }
+        this.add = function (id, view) {
+            that.views[id] = view;
+            return view;
+        };
 
 
-      var view = that.views[id];
+        this.remove = function (id) {
+            if (typeof id === "undefined") {
+                new View().remove.apply(that);
+            }
+            var view = that.views[id];
+            if (view) {
+                delete that.views[id];
+                view.remove();
+                return view;
+            }
+        };
 
 
-      if (view) {
-        delete that.views[id];
-        view.remove();
-        return view;
-      }
+        this.removeAll = function () {
+            _.each(_.keys(that.views), that.remove);
+            return that;
+        };
+        View.apply(this, Array.prototype.slice.apply(arguments));
     };
     };
 
 
-    this.removeAll = function () {
-      _.each(_.keys(that.views), that.remove);
+    var methods = [
+        'all', 'any', 'chain', 'collect', 'contains', 'detect',
+        'difference', 'drop', 'each', 'every', 'filter', 'find',
+        'first', 'foldl', 'foldr', 'forEach', 'head', 'include',
+        'indexOf', 'initial', 'inject', 'invoke', 'isEmpty',
+        'last', 'lastIndexOf', 'map', 'max', 'min', 'reduce',
+        'reduceRight', 'reject', 'rest', 'sample', 'select',
+        'shuffle', 'size', 'some', 'sortBy', 'tail', 'take',
+        'toArray', 'without',
+    ];
+    // Mix in each Underscore method as a proxy to `Overview#view`.
+    _.each(methods, function(method) {
+        Overview.prototype[method] = function() {
+            var args = Array.prototype.slice.call(arguments);
+            args.unshift(this.views);
+            return _[method].apply(_, args);
+        };
+    });
+    _.extend(Overview.prototype, View.prototype);
+    Overview.extend = View.extend;
 
 
-      return that;
-    };
 
 
-    View.apply(this, Array.prototype.slice.apply(arguments));
-  };
+    Backbone.OrderedListView = Backbone.Overview.extend({
 
 
-  var methods = ['all', 'any', 'chain', 'collect', 'contains', 'detect', 'difference', 'drop', 'each', 'every', 'filter', 'find', 'first', 'foldl', 'foldr', 'forEach', 'head', 'include', 'indexOf', 'initial', 'inject', 'invoke', 'isEmpty', 'last', 'lastIndexOf', 'map', 'max', 'min', 'reduce', 'reduceRight', 'reject', 'rest', 'sample', 'select', 'shuffle', 'size', 'some', 'sortBy', 'tail', 'take', 'toArray', 'without']; // Mix in each Underscore method as a proxy to `Overview#view`.
+        // The `listItems` attribute denotes the path (from this View) to the
+        // list of items.
+        listItems: 'model',
+        // The `sortEvent` attribute specifies the event which should cause the
+        // ordered list to be sorted.
+        sortEvent: 'change',
+        // The `listSelector` is the selector used to query for the DOM list
+        // element which contains the ordered items.
+        listSelector: '.ordered-items',
+        // The `itemView` is constructor which should be called to create a
+        // View for a new item.
+        ItemView: undefined,
+
+        initialize () {
+            this.sortEventually = _.debounce(this.sortAndPositionAllItems.bind(this), 500);
+            this.items = _.get(this, this.listItems);
+            this.items.on('add', this.createItemView, this);
+            this.items.on('add', this.sortEventually, this);
+            this.items.on(this.sortEvent, this.sortEventually, this);
+        },
+
+        createItemView (item) {
+            let item_view = this.get(item.get('id'));
+            if (!item_view) {
+                item_view = new this.ItemView({model: item});
+                this.add(item.get('id'), item_view);
+            } else {
+                item_view.model = item;
+                item_view.initialize();
+            }
+            item_view.render();
+            return item_view;
+        },
 
 
-  _.each(methods, function (method) {
-    Overview.prototype[method] = function () {
-      var args = Array.prototype.slice.call(arguments);
-      args.unshift(this.views);
-      return _[method].apply(_, args);
-    };
-  });
 
 
-  _.extend(Overview.prototype, View.prototype);
-
-  Overview.extend = View.extend;
-  Backbone.OrderedListView = Backbone.Overview.extend({
-    // The `listItems` attribute denotes the path (from this View) to the
-    // list of items.
-    listItems: 'model',
-    // The `sortEvent` attribute specifies the event which should cause the
-    // ordered list to be sorted.
-    sortEvent: 'change',
-    // The `listSelector` is the selector used to query for the DOM list
-    // element which contains the ordered items.
-    listSelector: '.ordered-items',
-    // The `itemView` is constructor which should be called to create a
-    // View for a new item.
-    ItemView: undefined,
-    initialize: function initialize() {
-      this.sortEventually = _.debounce(this.sortAndPositionAllItems.bind(this), 500);
-      this.items = _.get(this, this.listItems);
-      this.items.on('add', this.createItemView, this);
-      this.items.on('add', this.sortEventually, this);
-      this.items.on(this.sortEvent, this.sortEventually, this);
-    },
-    createItemView: function createItemView(item) {
-      var item_view = this.get(item.get('id'));
-
-      if (!item_view) {
-        item_view = new this.ItemView({
-          model: item
-        });
-        this.add(item.get('id'), item_view);
-      } else {
-        item_view.model = item;
-        item_view.initialize();
-      }
+        sortAndPositionAllItems () {
+            this.items.sort();
+            this.items.each((item) => {
+                if (_.isUndefined(this.get(item.get('id')))) {
+                    this.createItemView(item)
+                }
+                this.positionItem(item, this.el.querySelector(this.listSelector));
+            });
+        },
 
 
-      item_view.render();
-      return item_view;
-    },
-    sortAndPositionAllItems: function sortAndPositionAllItems() {
-      var _this = this;
+        positionItem (item, list_el) {
+            /* Place the View's DOM element in the correct alphabetical
+             * position in the list.
+             *
+             * IMPORTANT: there's an important implicit assumption being
+             * made here. And that is that initially this method gets called
+             * for each item in the right positional order.
+             *
+             * In other words, it gets called for the 0th, then the
+             * 1st, then the 2nd, 3rd and so on.
+             *
+             * That's why we call it in the "success" handler after
+             * fetching the items, so that we know we have ALL of
+             * them and that they're sorted.
+             */
+            const view = this.get(item.get('id')),
+                index = this.items.indexOf(item);
 
 
-      this.items.sort();
-      this.items.each(function (item) {
-        if (_.isUndefined(_this.get(item.get('id')))) {
-          _this.createItemView(item);
+            if (index === 0) {
+                list_el.insertAdjacentElement('afterbegin', view.el);
+            } else if (index === (this.items.length-1)) {
+                list_el.insertAdjacentElement('beforeend', view.el);
+            } else {
+                const neighbour_el = list_el.querySelector('li:nth-child('+index+')');
+                neighbour_el.insertAdjacentElement('afterend', view.el);
+            }
+            return view;
         }
         }
+    });
 
 
-        _this.positionItem(item, _this.el.querySelector(_this.listSelector));
-      });
-    },
-    positionItem: function positionItem(item, list_el) {
-      /* Place the View's DOM element in the correct alphabetical
-       * position in the list.
-       *
-       * IMPORTANT: there's an important implicit assumption being
-       * made here. And that is that initially this method gets called
-       * for each item in the right positional order.
-       *
-       * In other words, it gets called for the 0th, then the
-       * 1st, then the 2nd, 3rd and so on.
-       *
-       * That's why we call it in the "success" handler after
-       * fetching the items, so that we know we have ALL of
-       * them and that they're sorted.
-       */
-      var view = this.get(item.get('id')),
-          index = this.items.indexOf(item);
-
-      if (index === 0) {
-        list_el.insertAdjacentElement('afterbegin', view.el);
-      } else if (index === this.items.length - 1) {
-        list_el.insertAdjacentElement('beforeend', view.el);
-      } else {
-        var neighbour_el = list_el.querySelector('li:nth-child(' + index + ')');
-        neighbour_el.insertAdjacentElement('afterend', view.el);
-      }
-
-      return view;
-    }
-  });
-  return Backbone.Overview;
-});
+    return Backbone.Overview;
+}));
 
 
-//# sourceMappingURL=backbone.overview.js.map
 
 
 
 
 /***/ }),
 /***/ }),
 
 
-/***/ "./node_modules/backbone.vdomview/dist/backbone.vdomview.js":
-/*!******************************************************************!*\
-  !*** ./node_modules/backbone.vdomview/dist/backbone.vdomview.js ***!
-  \******************************************************************/
+/***/ "./node_modules/backbone.vdomview/backbone.vdomview.js":
+/*!*************************************************************!*\
+  !*** ./node_modules/backbone.vdomview/backbone.vdomview.js ***!
+  \*************************************************************/
 /*! no static exports found */
 /*! no static exports found */
 /***/ (function(module, exports, __webpack_require__) {
 /***/ (function(module, exports, __webpack_require__) {
 
 
@@ -2466,90 +2467,123 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
 var backbone = (backbone || {});
 var backbone = (backbone || {});
 backbone.nativeview = __webpack_require__(/*! backbone.nativeview */ "./node_modules/backbone.nativeview/backbone.nativeview.js");
 backbone.nativeview = __webpack_require__(/*! backbone.nativeview */ "./node_modules/backbone.nativeview/backbone.nativeview.js");
 
 
-function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
-
 /*!
 /*!
  * Backbone.VDOMView
  * Backbone.VDOMView
  *
  *
  * MIT Licensed. Copyright (c) 2017, JC Brand <jc@opkode.com>
  * MIT Licensed. Copyright (c) 2017, JC Brand <jc@opkode.com>
  */
  */
 (function (root, factory) {
 (function (root, factory) {
-  if (true) {
-    !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! snabbdom */ "./node_modules/snabbdom/dist/snabbdom.js"), __webpack_require__(/*! snabbdom-attributes */ "./node_modules/snabbdom/dist/snabbdom-attributes.js"), __webpack_require__(/*! snabbdom-class */ "./node_modules/snabbdom/dist/snabbdom-class.js"), __webpack_require__(/*! snabbdom-dataset */ "./node_modules/snabbdom/dist/snabbdom-dataset.js"), __webpack_require__(/*! snabbdom-props */ "./node_modules/snabbdom/dist/snabbdom-props.js"), __webpack_require__(/*! snabbdom-style */ "./node_modules/snabbdom/dist/snabbdom-style.js"), __webpack_require__(/*! tovnode */ "./node_modules/snabbdom/dist/tovnode.js"), __webpack_require__(/*! underscore */ "./src/underscore-shim.js"), __webpack_require__(/*! backbone */ "./node_modules/backbone/backbone.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory),
+    if (true) {
+        !(__WEBPACK_AMD_DEFINE_ARRAY__ = [
+            __webpack_require__(/*! snabbdom */ "./node_modules/snabbdom/dist/snabbdom.js"),
+            __webpack_require__(/*! snabbdom-attributes */ "./node_modules/snabbdom/dist/snabbdom-attributes.js"),
+            __webpack_require__(/*! snabbdom-class */ "./node_modules/snabbdom/dist/snabbdom-class.js"),
+            __webpack_require__(/*! snabbdom-dataset */ "./node_modules/snabbdom/dist/snabbdom-dataset.js"),
+            __webpack_require__(/*! snabbdom-props */ "./node_modules/snabbdom/dist/snabbdom-props.js"),
+            __webpack_require__(/*! snabbdom-style */ "./node_modules/snabbdom/dist/snabbdom-style.js"),
+            __webpack_require__(/*! tovnode */ "./node_modules/snabbdom/dist/tovnode.js"),
+            __webpack_require__(/*! underscore */ "./src/underscore-shim.js"),
+            __webpack_require__(/*! backbone */ "./node_modules/backbone/backbone.js")
+        ], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory),
 				__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?
 				__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?
 				(__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__),
 				(__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__),
 				__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
 				__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
-  } else {}
-})(this, function (snabbdom, snabbdom_attributes, snabbdom_class, snabbdom_dataset, snabbdom_props, snabbdom_style, tovnode, _, Backbone) {
-  "use strict";
-
-  var domParser = new DOMParser();
-  var patch = snabbdom.init([snabbdom_attributes.default, snabbdom_class.default, snabbdom_dataset.default, snabbdom_props.default, snabbdom_style.default]);
-  var View = _.isUndefined(Backbone.NativeView) ? Backbone.View : Backbone.NativeView;
-
-  function parseHTMLToDOM(html_str) {
-    /* Parses a string with HTML and returns a DOM element.
-     *
-     * Forked from vdom_parser:
-     *      https://github.com/bitinn/vdom-parser
-     */
-    if (typeof html_str !== 'string') {
-      throw new Error('Invalid parameter type in parseHTMLToDOM');
-    }
+    } else {}
+}(this, function (
+        snabbdom,
+        snabbdom_attributes,
+        snabbdom_class,
+        snabbdom_dataset,
+        snabbdom_props,
+        snabbdom_style,
+        tovnode,
+        _,
+        Backbone) {
+    "use strict";
+
+    let domParser = new DOMParser();
+    const patch = snabbdom.init([
+        snabbdom_attributes.default,
+        snabbdom_class.default,
+        snabbdom_dataset.default,
+        snabbdom_props.default,
+        snabbdom_style.default
+    ]);
+
+    const View = _.isUndefined(Backbone.NativeView) ? Backbone.View : Backbone.NativeView;
+
+    function parseHTMLToDOM (html_str) {
+        /* Parses a string with HTML and returns a DOM element.
+         *
+         * Forked from vdom_parser:
+         *      https://github.com/bitinn/vdom-parser
+         */
+        if (typeof html_str !== 'string') {
+            throw new Error('Invalid parameter type in parseHTMLToDOM');
+        }
+        if ( !('DOMParser' in window) ) {
+            throw new Error(
+                'DOMParser is not available, '+
+                'so parsing string to DOM node is not possible.');
+        }
+        if (!html_str) {
+            return document.createTextNode('');
+        }
+        domParser = domParser || new DOMParser();
+        const doc = domParser.parseFromString(html_str, 'text/html');
 
 
-    if (!('DOMParser' in window)) {
-      throw new Error('DOMParser is not available, ' + 'so parsing string to DOM node is not possible.');
-    }
+        // most tags default to body
+        if (doc.body.firstChild) {
+            return doc.getElementsByTagName('body')[0].firstChild;
 
 
-    if (!html_str) {
-      return document.createTextNode('');
-    }
+        // some tags, like script and style, default to head
+        } else if (doc.head.firstChild && (doc.head.firstChild.tagName !== 'TITLE' || doc.title)) {
+            return doc.head.firstChild;
 
 
-    domParser = domParser || new DOMParser();
-    var doc = domParser.parseFromString(html_str, 'text/html'); // most tags default to body
+        // special case for html comment, cdata, doctype
+        } else if (doc.firstChild && doc.firstChild.tagName !== 'HTML') {
+            return doc.firstChild;
 
 
-    if (doc.body.firstChild) {
-      return doc.getElementsByTagName('body')[0].firstChild; // some tags, like script and style, default to head
-    } else if (doc.head.firstChild && (doc.head.firstChild.tagName !== 'TITLE' || doc.title)) {
-      return doc.head.firstChild; // special case for html comment, cdata, doctype
-    } else if (doc.firstChild && doc.firstChild.tagName !== 'HTML') {
-      return doc.firstChild; // other element, such as whitespace, or html/body/head tag, fallback to empty text node
-    } else {
-      return document.createTextNode('');
+        // other element, such as whitespace, or html/body/head tag, fallback to empty text node
+        } else {
+            return document.createTextNode('');
+        }
     }
     }
-  }
-
-  Backbone.VDOMView = View.extend({
-    updateEventListeners: function updateEventListeners(old_vnode, new_vnode) {
-      this.setElement(new_vnode.elm);
-    },
-    render: function render() {
-      if (_.isFunction(this.beforeRender)) {
-        this.beforeRender();
-      }
 
 
-      var new_vnode = tovnode.toVNode(parseHTMLToDOM(this.toHTML()));
-      new_vnode.data.hook = _.extend({
-        create: this.updateEventListeners.bind(this),
-        update: this.updateEventListeners.bind(this)
-      });
-      var el = this.vnode ? this.vnode.elm : this.el;
+    Backbone.VDOMView = View.extend({
 
 
-      if (el.outerHTML !== new_vnode.elm.outerHTML) {
-        this.vnode = patch(this.vnode || this.el, new_vnode);
-      }
+        updateEventListeners (old_vnode, new_vnode) {
+            this.setElement(new_vnode.elm);
+        },
 
 
-      if (_.isFunction(this.afterRender)) {
-        this.afterRender();
-      }
+        render () {
+            if (_.isFunction(this.beforeRender)) {
+                this.beforeRender();
+            }
+            let new_vnode;
+            if (!_.isNil(this.toHTML)) {
+                new_vnode = tovnode.toVNode(parseHTMLToDOM(this.toHTML()));
+            } else {
+                new_vnode = tovnode.toVNode(this.toDOM());
+            }
 
 
-      return this;
-    }
-  });
-  return Backbone.VDOMView;
-});
+            new_vnode.data.hook = _.extend({
+               create: this.updateEventListeners.bind(this),
+               update: this.updateEventListeners.bind(this)
+            });
+            const el = this.vnode ? this.vnode.elm : this.el;
+            if (el.outerHTML !== new_vnode.elm.outerHTML) {
+                this.vnode = patch(this.vnode || this.el, new_vnode);
+            }
+            if (_.isFunction(this.afterRender)) {
+                this.afterRender();
+            }
+            return this;
+        }
+    });
+    return Backbone.VDOMView;
+}));
 
 
-//# sourceMappingURL=backbone.vdomview.js.map
 
 
 
 
 /***/ }),
 /***/ }),
@@ -62031,7 +62065,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
 // Copyright (c) 2012-2018, the Converse.js developers
 // Copyright (c) 2012-2018, the Converse.js developers
 // Licensed under the Mozilla Public License (MPLv2)
 // Licensed under the Mozilla Public License (MPLv2)
 (function (root, factory) {
 (function (root, factory) {
-  !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! emojione */ "./node_modules/emojione/lib/js/emojione.js"), __webpack_require__(/*! filesize */ "./node_modules/filesize/lib/filesize.js"), __webpack_require__(/*! templates/chatboxes.html */ "./src/templates/chatboxes.html"), __webpack_require__(/*! backbone.overview */ "./node_modules/backbone.overview/dist/backbone.overview.js"), __webpack_require__(/*! utils/form */ "./src/utils/form.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory),
+  !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! emojione */ "./node_modules/emojione/lib/js/emojione.js"), __webpack_require__(/*! filesize */ "./node_modules/filesize/lib/filesize.js"), __webpack_require__(/*! templates/chatboxes.html */ "./src/templates/chatboxes.html"), __webpack_require__(/*! backbone.overview */ "./node_modules/backbone.overview/backbone.overview.js"), __webpack_require__(/*! utils/form */ "./src/utils/form.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory),
 				__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?
 				__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?
 				(__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__),
 				(__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__),
 				__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
 				__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
@@ -69054,7 +69088,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
 // Licensed under the Mozilla Public License (MPLv2)
 // Licensed under the Mozilla Public License (MPLv2)
 (function (root, factory) {
 (function (root, factory) {
   if (true) {
   if (true) {
-    !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! templates/alert_modal.html */ "./src/templates/alert_modal.html"), __webpack_require__(/*! bootstrap */ "./node_modules/bootstrap.native/dist/bootstrap-native-v4.js"), __webpack_require__(/*! backbone.vdomview */ "./node_modules/backbone.vdomview/dist/backbone.vdomview.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory),
+    !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! templates/alert_modal.html */ "./src/templates/alert_modal.html"), __webpack_require__(/*! bootstrap */ "./node_modules/bootstrap.native/dist/bootstrap-native-v4.js"), __webpack_require__(/*! backbone.vdomview */ "./node_modules/backbone.vdomview/backbone.vdomview.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory),
 				__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?
 				__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?
 				(__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__),
 				(__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__),
 				__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
 				__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
@@ -71123,7 +71157,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
 // Copyright (c) 2013-2018, the Converse.js developers
 // Copyright (c) 2013-2018, the Converse.js developers
 // Licensed under the Mozilla Public License (MPLv2)
 // Licensed under the Mozilla Public License (MPLv2)
 (function (root, factory) {
 (function (root, factory) {
-  !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! utils/form */ "./src/utils/form.js"), __webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! emojione */ "./node_modules/emojione/lib/js/emojione.js"), __webpack_require__(/*! converse-disco */ "./src/converse-disco.js"), __webpack_require__(/*! backbone.overview */ "./node_modules/backbone.overview/dist/backbone.overview.js"), __webpack_require__(/*! backbone.orderedlistview */ "./node_modules/backbone.overview/dist/backbone.orderedlistview.js"), __webpack_require__(/*! backbone.vdomview */ "./node_modules/backbone.vdomview/dist/backbone.vdomview.js"), __webpack_require__(/*! utils/muc */ "./src/utils/muc.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory),
+  !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! utils/form */ "./src/utils/form.js"), __webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! emojione */ "./node_modules/emojione/lib/js/emojione.js"), __webpack_require__(/*! converse-disco */ "./src/converse-disco.js"), __webpack_require__(/*! backbone.overview */ "./node_modules/backbone.overview/backbone.overview.js"), __webpack_require__(/*! backbone.orderedlistview */ "./node_modules/backbone.overview/backbone.orderedlistview.js"), __webpack_require__(/*! backbone.vdomview */ "./node_modules/backbone.vdomview/backbone.vdomview.js"), __webpack_require__(/*! utils/muc */ "./src/utils/muc.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory),
 				__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?
 				__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?
 				(__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__),
 				(__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__),
 				__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
 				__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
@@ -75611,20 +75645,26 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
             'label_xmpp_address': __('XMPP Address'),
             'label_xmpp_address': __('XMPP Address'),
             'label_nickname': label_nickname,
             'label_nickname': label_nickname,
             'contact_placeholder': __('name@example.org'),
             'contact_placeholder': __('name@example.org'),
-            'label_add': __('Add')
+            'label_add': __('Add'),
+            'error_message': __('Please enter a valid XMPP address')
           }));
           }));
         },
         },
 
 
         afterRender() {
         afterRender() {
           if (_converse.xhr_user_search_url && _.isString(_converse.xhr_user_search_url)) {
           if (_converse.xhr_user_search_url && _.isString(_converse.xhr_user_search_url)) {
-            this.initXHRAutoComplete();
+            this.initXHRAutoComplete(this.el);
           } else {
           } else {
-            this.initJIDAutoComplete();
+            this.initJIDAutoComplete(this.el);
           }
           }
-        },
 
 
-        initJIDAutoComplete() {
           const jid_input = this.el.querySelector('input[name="jid"]');
           const jid_input = this.el.querySelector('input[name="jid"]');
+          this.el.addEventListener('shown.bs.modal', () => {
+            jid_input.focus();
+          }, false);
+        },
+
+        initJIDAutoComplete(root) {
+          const jid_input = root.querySelector('input[name="jid"]');
 
 
           const list = _.uniq(_converse.roster.map(item => Strophe.getDomainFromJid(item.get('jid'))));
           const list = _.uniq(_converse.roster.map(item => Strophe.getDomainFromJid(item.get('jid'))));
 
 
@@ -75635,12 +75675,9 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
             },
             },
             'filter': Awesomplete.FILTER_STARTSWITH
             'filter': Awesomplete.FILTER_STARTSWITH
           });
           });
-          this.el.addEventListener('shown.bs.modal', () => {
-            jid_input.focus();
-          }, false);
         },
         },
 
 
-        initXHRAutoComplete() {
+        initXHRAutoComplete(root) {
           const name_input = this.el.querySelector('input[name="name"]');
           const name_input = this.el.querySelector('input[name="name"]');
           const jid_input = this.el.querySelector('input[name="jid"]');
           const jid_input = this.el.querySelector('input[name="jid"]');
           const awesomplete = new Awesomplete(name_input, {
           const awesomplete = new Awesomplete(name_input, {
@@ -75670,9 +75707,6 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
             jid_input.value = ev.text.value;
             jid_input.value = ev.text.value;
             name_input.value = ev.text.label;
             name_input.value = ev.text.label;
           });
           });
-          this.el.addEventListener('shown.bs.modal', () => {
-            name_input.focus();
-          }, false);
         },
         },
 
 
         addContactFromForm(ev) {
         addContactFromForm(ev) {
@@ -75680,14 +75714,16 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
           const data = new FormData(ev.target),
           const data = new FormData(ev.target),
                 jid = data.get('jid'),
                 jid = data.get('jid'),
                 name = data.get('name');
                 name = data.get('name');
-          ev.target.reset();
 
 
           if (!jid || _.compact(jid.split('@')).length < 2) {
           if (!jid || _.compact(jid.split('@')).length < 2) {
-            this.model.set({
-              'error_message': __('Please enter a valid XMPP address'),
-              'jid': jid
-            });
+            // XXX: we have to do this manually, instead of via
+            // toHTML because Awesomplete messes things up and
+            // confuses Snabbdom
+            u.addClass('is-invalid', this.el.querySelector('input[name="jid"]'));
+            u.addClass('d-block', this.el.querySelector('.invalid-feedback'));
           } else {
           } else {
+            ev.target.reset();
+
             _converse.roster.addAndSubscribe(jid, name);
             _converse.roster.addAndSubscribe(jid, name);
 
 
             this.model.clear();
             this.model.clear();
@@ -77317,27 +77353,15 @@ __p += '">\n                        <label class="clearfix" for="jid">' +
 __e(o.label_xmpp_address) +
 __e(o.label_xmpp_address) +
 ':</label>\n                        <input type="text" name="jid" required="required" value="' +
 ':</label>\n                        <input type="text" name="jid" required="required" value="' +
 __e(o.jid) +
 __e(o.jid) +
-'"\n                               class="form-control ';
- if (o.error_message) { ;
-__p += ' is-invalid ';
- } ;
-__p += '"\n                               placeholder="' +
+'"\n                               class="form-control"\n                               placeholder="' +
 __e(o.contact_placeholder) +
 __e(o.contact_placeholder) +
-'"/>\n                        ';
- if (o.error_message) { ;
-__p += '\n                            <div class="invalid-feedback">' +
+'"/>\n                        <div class="invalid-feedback">' +
 __e(o.error_message) +
 __e(o.error_message) +
-'</div>\n                        ';
- } ;
-__p += '\n                    </div>\n                    <div class="form-group">\n                        <label class="clearfix" for="name">' +
+'</div>\n                    </div>\n                    <div class="form-group">\n                        <label class="clearfix" for="name">' +
 __e(o.label_nickname) +
 __e(o.label_nickname) +
 ':</label>\n                        <input type="text" name="name" value="' +
 ':</label>\n                        <input type="text" name="name" value="' +
 __e(o.nickname) +
 __e(o.nickname) +
-'"\n                               class="form-control ';
- if (o.error_message) { ;
-__p += ' is-invalid ';
- } ;
-__p += '"\n                               placeholder="' +
+'"\n                               class="form-control"\n                               placeholder="' +
 __e(o.nickname_placeholder) +
 __e(o.nickname_placeholder) +
 '"/>\n                    </div>\n                </div>\n                <div class="modal-footer">\n                    <button type="submit" class="btn btn-primary">' +
 '"/>\n                    </div>\n                </div>\n                <div class="modal-footer">\n                    <button type="submit" class="btn btn-primary">' +
 __e(o.label_add) +
 __e(o.label_add) +

+ 3 - 4
package-lock.json

@@ -2855,12 +2855,11 @@
       }
       }
     },
     },
     "backbone.vdomview": {
     "backbone.vdomview": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/backbone.vdomview/-/backbone.vdomview-1.0.1.tgz",
-      "integrity": "sha512-KBrzWTa3Cyd/PVq4kxcnaF653z9W5zQkP4FYGdgDa2Du4TDbMwbjaGTJa302y34oX1iOvO/BmBrLgYM+RF3F8Q==",
+      "version": "github:jcbrand/backbone.vdomview#8222ff0c12462846fd3a5d915923e42da49c417f",
       "dev": true,
       "dev": true,
       "requires": {
       "requires": {
-        "backbone": "1.3.3"
+        "backbone": "1.3.3",
+        "snabbdom": "0.7.1"
       }
       }
     },
     },
     "balanced-match": {
     "balanced-match": {

+ 17 - 18
src/converse-rosterview.js

@@ -142,26 +142,31 @@
 
 
                 toHTML () {
                 toHTML () {
                     const label_nickname = _converse.xhr_user_search_url ? __('Contact name') : __('Optional nickname');
                     const label_nickname = _converse.xhr_user_search_url ? __('Contact name') : __('Optional nickname');
-                    return tpl_add_contact_modal(_.extend(this.model.toJSON(), {
+                    return  tpl_add_contact_modal(_.extend(this.model.toJSON(), {
                         '_converse': _converse,
                         '_converse': _converse,
                         'heading_new_contact': __('Add a Contact'),
                         'heading_new_contact': __('Add a Contact'),
                         'label_xmpp_address': __('XMPP Address'),
                         'label_xmpp_address': __('XMPP Address'),
                         'label_nickname': label_nickname,
                         'label_nickname': label_nickname,
                         'contact_placeholder': __('name@example.org'),
                         'contact_placeholder': __('name@example.org'),
                         'label_add': __('Add'),
                         'label_add': __('Add'),
+                        'error_message': __('Please enter a valid XMPP address')
                     }));
                     }));
                 },
                 },
 
 
                 afterRender () {
                 afterRender () {
                     if (_converse.xhr_user_search_url && _.isString(_converse.xhr_user_search_url)) {
                     if (_converse.xhr_user_search_url && _.isString(_converse.xhr_user_search_url)) {
-                        this.initXHRAutoComplete();
+                        this.initXHRAutoComplete(this.el);
                     } else {
                     } else {
-                        this.initJIDAutoComplete();
+                        this.initJIDAutoComplete(this.el);
                     }
                     }
+                    const jid_input = this.el.querySelector('input[name="jid"]');
+                    this.el.addEventListener('shown.bs.modal', () => {
+                        jid_input.focus();
+                    }, false);
                 },
                 },
 
 
-                initJIDAutoComplete () {
-                    const jid_input = this.el.querySelector('input[name="jid"]');
+                initJIDAutoComplete (root) {
+                    const jid_input = root.querySelector('input[name="jid"]');
                     const list = _.uniq(_converse.roster.map((item) => Strophe.getDomainFromJid(item.get('jid'))));
                     const list = _.uniq(_converse.roster.map((item) => Strophe.getDomainFromJid(item.get('jid'))));
                     new Awesomplete(jid_input, {
                     new Awesomplete(jid_input, {
                         'list': list,
                         'list': list,
@@ -170,12 +175,9 @@
                         },
                         },
                         'filter': Awesomplete.FILTER_STARTSWITH
                         'filter': Awesomplete.FILTER_STARTSWITH
                     });
                     });
-                    this.el.addEventListener('shown.bs.modal', () => {
-                        jid_input.focus();
-                    }, false);
                 },
                 },
 
 
-                initXHRAutoComplete () {
+                initXHRAutoComplete (root) {
                     const name_input = this.el.querySelector('input[name="name"]');
                     const name_input = this.el.querySelector('input[name="name"]');
                     const jid_input = this.el.querySelector('input[name="jid"]');
                     const jid_input = this.el.querySelector('input[name="jid"]');
                     const awesomplete = new Awesomplete(name_input, {
                     const awesomplete = new Awesomplete(name_input, {
@@ -200,9 +202,6 @@
                         jid_input.value = ev.text.value;
                         jid_input.value = ev.text.value;
                         name_input.value = ev.text.label;
                         name_input.value = ev.text.label;
                     });
                     });
-                    this.el.addEventListener('shown.bs.modal', () => {
-                        name_input.focus();
-                    }, false);
                 },
                 },
 
 
                 addContactFromForm (ev) {
                 addContactFromForm (ev) {
@@ -210,14 +209,14 @@
                     const data = new FormData(ev.target),
                     const data = new FormData(ev.target),
                           jid = data.get('jid'),
                           jid = data.get('jid'),
                           name = data.get('name');
                           name = data.get('name');
-                    ev.target.reset();
-
                     if (!jid || _.compact(jid.split('@')).length < 2) {
                     if (!jid || _.compact(jid.split('@')).length < 2) {
-                        this.model.set({
-                            'error_message': __('Please enter a valid XMPP address'),
-                            'jid': jid
-                        })
+                        // XXX: we have to do this manually, instead of via
+                        // toHTML because Awesomplete messes things up and
+                        // confuses Snabbdom
+                        u.addClass('is-invalid', this.el.querySelector('input[name="jid"]'));
+                        u.addClass('d-block', this.el.querySelector('.invalid-feedback'));
                     } else {
                     } else {
+                        ev.target.reset();
                         _converse.roster.addAndSubscribe(jid, name);
                         _converse.roster.addAndSubscribe(jid, name);
                         this.model.clear();
                         this.model.clear();
                         this.modal.hide();
                         this.modal.hide();

+ 3 - 5
src/templates/add_contact_modal.html

@@ -11,16 +11,14 @@
                     <div class="form-group {[ if (o._converse.xhr_user_search_url) { ]} hidden {[ } ]}">
                     <div class="form-group {[ if (o._converse.xhr_user_search_url) { ]} hidden {[ } ]}">
                         <label class="clearfix" for="jid">{{{o.label_xmpp_address}}}:</label>
                         <label class="clearfix" for="jid">{{{o.label_xmpp_address}}}:</label>
                         <input type="text" name="jid" required="required" value="{{{o.jid}}}"
                         <input type="text" name="jid" required="required" value="{{{o.jid}}}"
-                               class="form-control {[ if (o.error_message) { ]} is-invalid {[ } ]}"
+                               class="form-control"
                                placeholder="{{{o.contact_placeholder}}}"/>
                                placeholder="{{{o.contact_placeholder}}}"/>
-                        {[ if (o.error_message) { ]}
-                            <div class="invalid-feedback">{{{o.error_message}}}</div>
-                        {[ } ]}
+                        <div class="invalid-feedback">{{{o.error_message}}}</div>
                     </div>
                     </div>
                     <div class="form-group">
                     <div class="form-group">
                         <label class="clearfix" for="name">{{{o.label_nickname}}}:</label>
                         <label class="clearfix" for="name">{{{o.label_nickname}}}:</label>
                         <input type="text" name="name" value="{{{o.nickname}}}"
                         <input type="text" name="name" value="{{{o.nickname}}}"
-                               class="form-control {[ if (o.error_message) { ]} is-invalid {[ } ]}"
+                               class="form-control"
                                placeholder="{{{o.nickname_placeholder}}}"/>
                                placeholder="{{{o.nickname_placeholder}}}"/>
                     </div>
                     </div>
                 </div>
                 </div>

+ 6 - 6
webpack.config.js

@@ -16,15 +16,15 @@ const config = {
     module: {
     module: {
         rules: [
         rules: [
         {
         {
-            test: path.resolve(__dirname, "node_modules/backbone.overview/dist/backbone.orderedlistview"),
+            test: path.resolve(__dirname, "node_modules/backbone.overview/backbone.orderedlistview"),
             use: 'imports-loader?backbone.nativeview'
             use: 'imports-loader?backbone.nativeview'
         },
         },
         {
         {
-            test: path.resolve(__dirname, "node_modules/backbone.overview/dist/backbone.overview"),
+            test: path.resolve(__dirname, "node_modules/backbone.overview/backbone.overview"),
             use: 'imports-loader?backbone.nativeview'
             use: 'imports-loader?backbone.nativeview'
         },
         },
         {
         {
-            test: path.resolve(__dirname, "node_modules/backbone.vdomview/dist/backbone.vdomview"),
+            test: path.resolve(__dirname, "node_modules/backbone.vdomview/backbone.vdomview"),
             use: 'imports-loader?backbone.nativeview'
             use: 'imports-loader?backbone.nativeview'
         },
         },
         {
         {
@@ -83,9 +83,9 @@ const config = {
             "backbone":                 path.resolve(__dirname, "node_modules/backbone/backbone"),
             "backbone":                 path.resolve(__dirname, "node_modules/backbone/backbone"),
             "backbone.browserStorage":  path.resolve(__dirname, "node_modules/backbone.browserStorage/backbone.browserStorage"),
             "backbone.browserStorage":  path.resolve(__dirname, "node_modules/backbone.browserStorage/backbone.browserStorage"),
             "backbone.nativeview":      path.resolve(__dirname, "node_modules/backbone.nativeview/backbone.nativeview"),
             "backbone.nativeview":      path.resolve(__dirname, "node_modules/backbone.nativeview/backbone.nativeview"),
-            "backbone.orderedlistview": path.resolve(__dirname, "node_modules/backbone.overview/dist/backbone.orderedlistview"),
-            "backbone.overview":        path.resolve(__dirname, "node_modules/backbone.overview/dist/backbone.overview"),
-            "backbone.vdomview":        path.resolve(__dirname, "node_modules/backbone.vdomview/dist/backbone.vdomview"),
+            "backbone.orderedlistview": path.resolve(__dirname, "node_modules/backbone.overview/backbone.orderedlistview"),
+            "backbone.overview":        path.resolve(__dirname, "node_modules/backbone.overview/backbone.overview"),
+            "backbone.vdomview":        path.resolve(__dirname, "node_modules/backbone.vdomview/backbone.vdomview"),
             "bootstrap":                path.resolve(__dirname, "node_modules/bootstrap.native/dist/bootstrap-native-v4"),
             "bootstrap":                path.resolve(__dirname, "node_modules/bootstrap.native/dist/bootstrap-native-v4"),
             "crypto":                   path.resolve(__dirname, "node_modules/otr/build/dep/crypto"),
             "crypto":                   path.resolve(__dirname, "node_modules/otr/build/dep/crypto"),
             "emojione":                 path.resolve(__dirname, "node_modules/emojione/lib/js/emojione"),
             "emojione":                 path.resolve(__dirname, "node_modules/emojione/lib/js/emojione"),