Commit 75ae76ad authored by JC Brand's avatar JC Brand

Use `_converse.AutoComplete` in "Add Contact" modal

parent f0848c28
......@@ -12198,14 +12198,14 @@ body.converse-fullscreen {
display: none; }
#conversejs .suggestion-box .suggestion-box__results--above:after,
#conversejs .awesomplete .suggestion-box__results--above:after {
z-index: 1;
z-index: -1;
content: "";
position: absolute;
bottom: -.43em;
bottom: -0.43em;
left: 1em;
width: 0;
height: 0;
padding: .4em;
padding: 0.4em;
background: white;
border: inherit;
border-left: 0;
......@@ -12252,7 +12252,7 @@ body.converse-fullscreen {
bottom: 4.5em; }
#conversejs.converse-overlayed .suggestion-box__results--above {
bottom: 5.5em; }
bottom: 3.5em; }
#conversejs.converse-embedded {
-webkit-box-sizing: border-box;
......
This diff is collapsed.
......@@ -71,13 +71,14 @@
display: none;
}
&:after {
z-index: 1;
z-index: -1;
content: "";
position: absolute;
bottom: -.43em;
bottom: -0.43em;
left: 1em;
width: 0; height: 0;
padding: .4em;
width: 0;
height: 0;
padding: 0.4em;
background: white;
border: inherit;
border-left: 0;
......@@ -140,6 +141,6 @@
#conversejs.converse-overlayed {
.suggestion-box__results--above {
bottom: 5.5em;
bottom: 3.5em;
}
}
......@@ -207,7 +207,7 @@
input_jid.value = 'someone@';
const evt = new Event('input');
input_jid.dispatchEvent(evt);
expect(modal.el.querySelector('.awesomplete li').textContent).toBe('someone@localhost');
expect(modal.el.querySelector('.suggestion-box li').textContent).toBe('someone@localhost');
input_jid.value = 'someone@localhost';
input_name.value = 'Someone';
modal.el.querySelector('button[type="submit"]').click();
......@@ -246,30 +246,28 @@
cbview.el.querySelector('.add-contact').click()
const modal = _converse.rosterview.add_contact_modal;
await test_utils.waitUntil(() => u.isVisible(modal.el), 1000);
// We only have autocomplete for the name input
expect(modal.jid_auto_complete).toBe(undefined);
expect(modal.name_auto_complete instanceof _converse.AutoComplete).toBe(true);
const input_el = modal.el.querySelector('input[name="name"]');
input_el.value = 'marty';
let evt = new Event('input');
input_el.dispatchEvent(evt);
await test_utils.waitUntil(() => modal.el.querySelector('.awesomplete li'), 1000);
input_el.dispatchEvent(new Event('input'));
await test_utils.waitUntil(() => modal.el.querySelector('.suggestion-box li'), 1000);
const sendIQ = _converse.connection.sendIQ;
let sent_stanza, IQ_id;
spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
sent_stanza = iq;
IQ_id = sendIQ.bind(this)(iq, callback, errback);
});
expect(modal.el.querySelectorAll('.awesomplete li').length).toBe(1);
const suggestion = modal.el.querySelector('.awesomplete li');
expect(modal.el.querySelectorAll('.suggestion-box li').length).toBe(1);
const suggestion = modal.el.querySelector('.suggestion-box li');
expect(suggestion.textContent).toBe('Marty McFly');
// Can't trigger "mousedown" event so trigger the Awesomplete
// custom event which would have been triggered upon mousedown.
evt = document.createEvent("HTMLEvents");
evt.initEvent('awesomplete-selectcomplete', true, true );
evt.text = {
'label': 'Marty McFly',
'value': 'marty@mcfly.net'
}
modal.el.dispatchEvent(evt);
// Mock selection
modal.name_auto_complete.select(suggestion);
expect(input_el.value).toBe('Marty McFly');
expect(modal.el.querySelector('input[name="jid"]').value).toBe('marty@mcfly.net');
modal.el.querySelector('button[type="submit"]').click();
......
......@@ -53,6 +53,32 @@ converse.plugins.add("converse-autocomplete", {
};
class Suggestion extends String {
constructor (data) {
super();
const o = Array.isArray(data)
? { label: data[0], value: data[1] }
: typeof data === "object" && "label" in data && "value" in data ? data : { label: data, value: data };
this.label = o.label || o.value;
this.value = o.value;
}
get lenth () {
return this.label.length;
}
toString () {
return "" + this.label;
}
valueOf () {
return this.toString();
}
}
class AutoComplete {
constructor (el, config={}) {
......@@ -76,7 +102,7 @@ converse.plugins.add("converse-autocomplete", {
'include_triggers': [], // Array of trigger keys which should be included in the returned value
'min_chars': 2,
'max_items': 10,
'auto_evaluate': true,
'auto_evaluate': true, // Should evaluation happen automatically without any particular key as trigger?
'auto_first': false, // Should the first element be automatically selected?
'data': _.identity,
'filter': _converse.FILTER_CONTAINS,
......@@ -129,7 +155,7 @@ converse.plugins.add("converse-autocomplete", {
list = helpers.getElement(list);
if (list && list.children) {
const items = [];
slice.apply(list.children).forEach(function (el) {
Array.prototype.slice.apply(list.children).forEach(function (el) {
if (!el.disabled) {
const text = el.textContent.trim(),
value = el.value || text,
......@@ -230,7 +256,7 @@ converse.plugins.add("converse-autocomplete", {
}
}
select (selected, origin) {
select (selected) {
if (selected) {
this.index = u.siblingIndex(selected);
} else {
......@@ -305,11 +331,11 @@ converse.plugins.add("converse-autocomplete", {
}
evaluate (ev) {
const arrow_pressed = (
const selecting = this.selected && ev && (
ev.keyCode === _converse.keycodes.UP_ARROW ||
ev.keyCode === _converse.keycodes.DOWN_ARROW
);
if (!this.auto_completing || (this.selected && arrow_pressed)) {
if (!this.auto_evaluate && !this.auto_completing || selecting) {
return;
}
......@@ -339,7 +365,7 @@ converse.plugins.add("converse-autocomplete", {
this.suggestions = this.suggestions.sort(this.sort);
}
this.suggestions = this.suggestions.slice(0, this.max_items);
this.suggestions.forEach((text) => this.ul.appendChild(this.item(text, value)));
this.suggestions.forEach(text => this.ul.appendChild(this.item(text, value)));
if (this.ul.children.length === 0) {
this.close({'reason': 'nomatches'});
......@@ -357,28 +383,6 @@ converse.plugins.add("converse-autocomplete", {
_.extend(AutoComplete.prototype, Backbone.Events);
// Private functions
function Suggestion(data) {
const o = Array.isArray(data)
? { label: data[0], value: data[1] }
: typeof data === "object" && "label" in data && "value" in data ? data : { label: data, value: data };
this.label = o.label || o.value;
this.value = o.value;
}
Object.defineProperty(Suggestion.prototype = Object.create(String.prototype), "length", {
get: function() { return this.label.length; }
});
Suggestion.prototype.toString = Suggestion.prototype.valueOf = function () {
return "" + this.label;
};
// Helpers
var slice = Array.prototype.slice;
const helpers = {
getElement (expr, el) {
......
// Converse.js (A browser based XMPP chat client)
// https://conversejs.org
//
// Copyright (c) 2012-2017, Jan-Carel Brand <jc@opkode.com>
// Copyright (c) 2019, Jan-Carel Brand <jc@opkode.com>
// Licensed under the Mozilla Public License (MPLv2)
import "converse-chatview";
......
......@@ -7,7 +7,6 @@
import "@converse/headless/converse-roster";
import "@converse/headless/converse-chatboxes";
import "converse-modal";
import Awesomplete from "awesomplete";
import _FormData from "formdata-polyfill";
import converse from "@converse/headless/converse-core";
import tpl_add_contact_modal from "templates/add_contact_modal.html";
......@@ -119,7 +118,7 @@ converse.plugins.add('converse-rosterview', {
toHTML () {
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,
'heading_new_contact': __('Add a Contact'),
'label_xmpp_address': __('XMPP Address'),
......@@ -132,47 +131,47 @@ converse.plugins.add('converse-rosterview', {
afterRender () {
if (_converse.xhr_user_search_url && _.isString(_converse.xhr_user_search_url)) {
this.initXHRAutoComplete(this.el);
this.el.addEventListener('awesomplete-selectcomplete', ev => {
this.initXHRAutoComplete();
this.name_auto_complete.on('suggestion-box-selectcomplete', ev => {
this.el.querySelector('input[name="name"]').value = ev.text.label;
this.el.querySelector('input[name="jid"]').value = ev.text.value;
});
} else {
this.initJIDAutoComplete(this.el);
this.initJIDAutoComplete();
}
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'))));
new Awesomplete(jid_input, {
'list': list,
initJIDAutoComplete () {
const el = this.el.querySelector('.suggestion-box__jid').parentElement;
this.jid_auto_complete = new _converse.AutoComplete(el, {
'data': (text, input) => `${input.slice(0, input.indexOf("@"))}@${text}`,
'filter': Awesomplete.FILTER_STARTSWITH
'filter': _converse.FILTER_STARTSWITH,
'list': _.uniq(_converse.roster.map(item => Strophe.getDomainFromJid(item.get('jid'))))
});
},
initXHRAutoComplete (root) {
const name_input = this.el.querySelector('input[name="name"]');
const jid_input = this.el.querySelector('input[name="jid"]');
const awesomplete = new Awesomplete(name_input, {
'minChars': 1,
initXHRAutoComplete () {
const el = this.el.querySelector('.suggestion-box__name').parentElement;
this.name_auto_complete = new _converse.AutoComplete(el, {
'auto_evaluate': false,
'filter': _converse.FILTER_STARTSWITH,
'list': []
});
const xhr = new window.XMLHttpRequest();
// `open` must be called after `onload` for mock/testing purposes.
xhr.onload = function () {
xhr.onload = () => {
if (xhr.responseText) {
awesomplete.list = JSON.parse(xhr.responseText).map((i) => { //eslint-disable-line arrow-body-style
return {'label': i.fullname || i.jid, 'value': i.jid};
});
awesomplete.evaluate();
const r = xhr.responseText;
this.name_auto_complete.list = JSON.parse(r).map(i => ({'label': i.fullname || i.jid, 'value': i.jid}));
this.name_auto_complete.auto_completing = true;
this.name_auto_complete.evaluate();
}
};
name_input.addEventListener('input', _.debounce(() => {
xhr.open("GET", `${_converse.xhr_user_search_url}q=${name_input.value}`, true);
const input_el = this.el.querySelector('input[name="name"]');
input_el.addEventListener('input', _.debounce(() => {
xhr.open("GET", `${_converse.xhr_user_search_url}q=${input_el.value}`, true);
xhr.send()
} , 300));
},
......@@ -183,9 +182,10 @@ converse.plugins.add('converse-rosterview', {
jid = data.get('jid'),
name = data.get('name');
if (!jid || _.compact(jid.split('@')).length < 2) {
// XXX: we have to do this manually, instead of via
// XXX: we used to have to do this manually, instead of via
// toHTML because Awesomplete messes things up and
// confuses Snabbdom
// We now use _converse.AutoComplete, can this be removed?
u.addClass('is-invalid', this.el.querySelector('input[name="jid"]'));
u.addClass('d-block', this.el.querySelector('.invalid-feedback'));
} else {
......
......@@ -10,16 +10,26 @@
<div class="modal-body">
<div class="form-group {[ if (o._converse.xhr_user_search_url) { ]} hidden {[ } ]}">
<label class="clearfix" for="jid">{{{o.label_xmpp_address}}}:</label>
<input type="text" name="jid" required="required" value="{{{o.jid}}}"
class="form-control"
placeholder="{{{o.contact_placeholder}}}"/>
<div class="invalid-feedback">{{{o.error_message}}}</div>
<div class="suggestion-box suggestion-box__jid">
<ul class="suggestion-box__results suggestion-box__results--above" hidden=""></ul>
<input type="text" name="jid" required="required" value="{{{o.jid}}}"
class="form-control suggestion-box__input"
placeholder="{{{o.contact_placeholder}}}"/>
<div class="invalid-feedback">{{{o.error_message}}}</div>
<span class="suggestion-box__additions visually-hidden" role="status" aria-live="assertive" aria-relevant="additions"></span>
</div>
</div>
<div class="form-group">
<label class="clearfix" for="name">{{{o.label_nickname}}}:</label>
<input type="text" name="name" value="{{{o.nickname}}}"
class="form-control"
placeholder="{{{o.nickname_placeholder}}}"/>
<div class="suggestion-box suggestion-box__name">
<ul class="suggestion-box__results suggestion-box__results--above" hidden=""></ul>
<input type="text" name="name" value="{{{o.nickname}}}"
class="form-control suggestion-box__input"
placeholder="{{{o.nickname_placeholder}}}"/>
<div class="invalid-feedback">{{{o.error_message}}}</div>
<span class="suggestion-box__additions visually-hidden" role="status" aria-live="assertive" aria-relevant="additions"></span>
</div>
</div>
<button type="submit" class="btn btn-primary">{{{o.label_add}}}</button>
</div>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment