Commit b18cc6bc authored by JC Brand's avatar JC Brand

Move modals and their templates into `./modals/`

parent 34cba684
......@@ -3144,7 +3144,8 @@
"dependencies": {
"filesize": {
"version": "6.1.0",
"resolved": false
"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz",
"integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg=="
},
"fs-extra": {
"version": "8.1.0",
......@@ -3200,20 +3201,22 @@
},
"localforage": {
"version": "1.7.3",
"resolved": false,
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.7.3.tgz",
"integrity": "sha512-1TulyYfc4udS7ECSBT2vwJksWbkwwTX8BzeUIiq8Y07Riy7bDAAnxDaPU/tWyOVmQAcWJIEIFP9lPfBGqVoPgQ==",
"requires": {
"lie": "3.1.1"
}
},
"pluggable.js": {
"version": "2.0.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/pluggable.js/-/pluggable.js-2.0.1.tgz",
"integrity": "sha512-SBt6v6Tbp20Jf8hU0cpcc/+HBHGMY8/Q+yA6Ih0tBQE8tfdZ6U4PRG0iNvUUjLx/hVyOP53n0UfGBymlfaaXCg==",
"requires": {
"lodash": "^4.17.11"
}
},
"skeletor.js": {
"version": "0.0.1",
"version": "github:skeletorjs/skeletor#bf6d9c86f9fcf224fa9d9af5a25380b77aa4b561",
"from": "github:skeletorjs/skeletor#bf6d9c86f9fcf224fa9d9af5a25380b77aa4b561",
"requires": {
"lodash": "^4.17.14"
......@@ -3221,7 +3224,11 @@
},
"strophe.js": {
"version": "github:strophe/strophejs#c4a94e59877c06dc2395f4ccbd26f3fee67a4c9f",
"from": "strophe.js@github:strophe/strophejs#c4a94e59877c06dc2395f4ccbd26f3fee67a4c9f"
"from": "strophe.js@github:strophe/strophejs#c4a94e59877c06dc2395f4ccbd26f3fee67a4c9f",
"requires": {
"abab": "^2.0.3",
"xmldom": "^0.1.27"
}
},
"twemoji": {
"version": "12.1.5",
......
/*global mock */
/*global mock, converse */
const u = converse.env.utils;
......@@ -53,6 +53,7 @@ describe("The User Details Modal", function () {
let remove_contact_button = modal.el.querySelector('button.remove-contact');
expect(u.isVisible(remove_contact_button)).toBeTruthy();
remove_contact_button.click();
await u.waitUntil(() => u.isVisible(document.querySelector('.alert-danger')), 2000);
const header = document.querySelector('.alert-danger .modal-title');
......
......@@ -3,277 +3,133 @@
* @copyright The Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
import { View } from '@converse/skeletor/src/view.js';
import Alert from './modals/alert.js';
import BootstrapModal from './modals/base.js';
import Confirm from './modals/confirm.js';
import { Model } from '@converse/skeletor/src/model.js';
import { render } from 'lit-html';
import { __ } from './i18n';
import bootstrap from "bootstrap.native";
import { converse } from "@converse/headless/converse-core";
import log from "@converse/headless/log";
import tpl_alert_component from "templates/alert.js";
import tpl_alert_modal from "templates/alert_modal.js";
import tpl_prompt from "templates/prompt.js";
import { _converse, converse } from "@converse/headless/converse-core";
const { sizzle } = converse.env;
const u = converse.env.utils;
let _converse;
export const BootstrapModal = View.extend({
className: "modal",
events: {
'click .nav-item .nav-link': 'switchTab'
},
initialize () {
this.render()
this.el.setAttribute('tabindex', '-1');
this.el.setAttribute('role', 'dialog');
this.el.setAttribute('aria-hidden', 'true');
const label_id = this.el.querySelector('.modal-title').getAttribute('id');
label_id && this.el.setAttribute('aria-labelledby', label_id);
this.insertIntoDOM();
const Modal = bootstrap.Modal;
this.modal = new Modal(this.el, {
backdrop: true,
keyboard: true
});
this.el.addEventListener('hide.bs.modal', () => u.removeClass('selected', this.trigger_el), false);
},
insertIntoDOM () {
const container_el = _converse.chatboxviews.el.querySelector("#converse-modals");
container_el.insertAdjacentElement('beforeEnd', this.el);
},
converse.env.BootstrapModal = BootstrapModal; // expose to plugins
switchTab (ev) {
ev.stopPropagation();
ev.preventDefault();
sizzle('.nav-link.active', this.el).forEach(el => {
u.removeClass('active', this.el.querySelector(el.getAttribute('href')));
u.removeClass('active', el);
});
u.addClass('active', ev.target);
u.addClass('active', this.el.querySelector(ev.target.getAttribute('href')))
},
alert (message, type='primary') {
const body = this.el.querySelector('.modal-alert');
if (body === null) {
log.error("Could not find a .modal-alert element in the modal to show an alert message in!");
return;
let alert;
const modal_api = {
/**
* Show a confirm modal to the user.
* @method _converse.api.confirm
* @param { String } title - The header text for the confirmation dialog
* @param { (String[]|String) } messages - The text to show to the user
* @param { Array<Field> } fields - An object representing a fields presented to the user.
* @property { String } Field.label - The form label for the input field.
* @property { String } Field.name - The name for the input field.
* @property { String } [Field.challenge] - A challenge value that must be provided by the user.
* @property { String } [Field.placeholder] - The placeholder for the input field.
* @property { Boolean} [Field.required] - Whether the field is required or not
* @returns { Promise<Array|false> } A promise which resolves with an array of
* filled in fields or `false` if the confirm dialog was closed or canceled.
*/
async confirm (title, messages=[], fields=[]) {
if (typeof messages === 'string') {
messages = [messages];
}
// FIXME: Instead of adding the alert imperatively, we should
// find a way to let the modal rerender with an alert message
render(tpl_alert_component({'type': `alert-${type}`, 'message': message}), body);
const el = body.firstElementChild;
setTimeout(() => {
u.addClass('fade-out', el);
setTimeout(() => u.removeElement(el), 600);
}, 5000);
},
show (ev) {
if (ev) {
ev.preventDefault();
this.trigger_el = ev.target;
this.trigger_el.classList.add('selected');
const model = new Model({title, messages, fields, 'type': 'confirm'})
const confirm = new Confirm({model});
confirm.show();
let result;
try {
result = await confirm.confirmation;
} catch (e) {
result = false;
}
this.modal.show();
}
});
converse.env.BootstrapModal = BootstrapModal; // expose to plugins
export const Confirm = BootstrapModal.extend({
events: {
'submit .confirm': 'onConfimation'
},
initialize () {
this.confirmation = u.getResolveablePromise();
BootstrapModal.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'change', this.render)
this.el.addEventListener('closed.bs.modal', () => this.confirmation.reject(), false);
confirm.remove();
return result;
},
toHTML () {
return tpl_prompt(this.model.toJSON());
/**
* Show a prompt modal to the user.
* @method _converse.api.prompt
* @param { String } title - The header text for the prompt
* @param { (String[]|String) } messages - The prompt text to show to the user
* @param { String } placeholder - The placeholder text for the prompt input
* @returns { Promise<String|false> } A promise which resolves with the text provided by the
* user or `false` if the user canceled the prompt.
*/
async prompt (title, messages=[], placeholder='') {
if (typeof messages === 'string') {
messages = [messages];
}
const model = new Model({
title,
messages,
'fields': [{
'name': 'reason',
'placeholder': placeholder,
}],
'type': 'prompt'
})
const prompt = new Confirm({model});
prompt.show();
let result;
try {
result = (await prompt.confirmation).pop()?.value;
} catch (e) {
result = false;
}
prompt.remove();
return result;
},
afterRender () {
if (!this.close_handler_registered) {
this.el.addEventListener('closed.bs.modal', () => {
if (!this.confirmation.isResolved) {
this.confirmation.reject()
}
}, false);
this.close_handler_registered = true;
/**
* Show an alert modal to the user.
* @method _converse.api.alert
* @param { ('info'|'warn'|'error') } type - The type of alert.
* @param { String } title - The header text for the alert.
* @param { (String[]|String) } messages - The alert text to show to the user.
*/
alert (type, title, messages) {
if (typeof messages === 'string') {
messages = [messages];
}
let level;
if (type === 'error') {
level = 'alert-danger';
} else if (type === 'info') {
level = 'alert-info';
} else if (type === 'warn') {
level = 'alert-warning';
}
},
onConfimation (ev) {
ev.preventDefault();
const form_data = new FormData(ev.target);
const fields = (this.model.get('fields') || [])
.map(field => {
const value = form_data.get(field.name).trim();
field.value = value;
if (field.challenge) {
field.challenge_failed = (value !== field.challenge);
}
return field;
if (alert === undefined) {
const model = new Model({
'title': title,
'messages': messages,
'level': level,
'type': 'alert'
})
alert = new Alert({model});
} else {
alert.model.set({
'title': title,
'messages': messages,
'level': level
});
if (fields.filter(c => c.challenge_failed).length) {
this.model.set('fields', fields);
// Setting an array doesn't trigger a change event
this.model.trigger('change');
return;
}
this.confirmation.resolve(fields);
this.modal.hide();
alert.show();
}
});
export const Alert = BootstrapModal.extend({
initialize () {
BootstrapModal.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'change', this.render)
},
toHTML () {
return tpl_alert_modal(Object.assign({__}, this.model.toJSON()));
}
});
}
converse.plugins.add('converse-modal', {
initialize () {
_converse = this._converse
/************************ BEGIN Event Listeners ************************/
_converse.api.listen.on('disconnect', () => {
const container = document.querySelector("#converse-modals");
if (container) {
container.innerHTML = '';
}
});
/************************ BEGIN API ************************/
// We extend the default converse.js API to add methods specific to MUC chat rooms.
let alert;
Object.assign(_converse.api, {
/**
* Show a confirm modal to the user.
* @method _converse.api.confirm
* @param { String } title - The header text for the confirmation dialog
* @param { (String[]|String) } messages - The text to show to the user
* @param { Array<Field> } fields - An object representing a fields presented to the user.
* @property { String } Field.label - The form label for the input field.
* @property { String } Field.name - The name for the input field.
* @property { String } [Field.challenge] - A challenge value that must be provided by the user.
* @property { String } [Field.placeholder] - The placeholder for the input field.
* @property { Boolean} [Field.required] - Whether the field is required or not
* @returns { Promise<Array|false> } A promise which resolves with an array of
* filled in fields or `false` if the confirm dialog was closed or canceled.
*/
async confirm (title, messages=[], fields=[]) {
if (typeof messages === 'string') {
messages = [messages];
}
const model = new Model({title, messages, fields, 'type': 'confirm'})
const confirm = new Confirm({model});
confirm.show();
let result;
try {
result = await confirm.confirmation;
} catch (e) {
result = false;
}
confirm.remove();
return result;
},
/**
* Show a prompt modal to the user.
* @method _converse.api.prompt
* @param { String } title - The header text for the prompt
* @param { (String[]|String) } messages - The prompt text to show to the user
* @param { String } placeholder - The placeholder text for the prompt input
* @returns { Promise<String|false> } A promise which resolves with the text provided by the
* user or `false` if the user canceled the prompt.
*/
async prompt (title, messages=[], placeholder='') {
if (typeof messages === 'string') {
messages = [messages];
}
const model = new Model({
title,
messages,
'fields': [{
'name': 'reason',
'placeholder': placeholder,
}],
'type': 'prompt'
})
const prompt = new Confirm({model});
prompt.show();
let result;
try {
result = (await prompt.confirmation).pop()?.value;
} catch (e) {
result = false;
}
prompt.remove();
return result;
},
/**
* Show an alert modal to the user.
* @method _converse.api.alert
* @param { ('info'|'warn'|'error') } type - The type of alert.
* @param { String } title - The header text for the alert.
* @param { (String[]|String) } messages - The alert text to show to the user.
*/
alert (type, title, messages) {
if (typeof messages === 'string') {
messages = [messages];
}
let level;
if (type === 'error') {
level = 'alert-danger';
} else if (type === 'info') {
level = 'alert-info';
} else if (type === 'warn') {
level = 'alert-warning';
}
if (alert === undefined) {
const model = new Model({
'title': title,
'messages': messages,
'level': level,
'type': 'alert'
})
alert = new Alert({model});
} else {
alert.model.set({
'title': title,
'messages': messages,
'level': level
});
}
alert.show();
}
});
Object.assign(_converse.api, modal_api);
}
});
......@@ -3,22 +3,16 @@
* @copyright The Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
import "modals/profile.js";
import "modals/chat-status.js";
import "@converse/headless/converse-status";
import "@converse/headless/converse-vcard";
import "converse-modal";
import UserSettingsModal from "modals/user-settings";
import bootstrap from "bootstrap.native";
import log from "@converse/headless/log";
import sizzle from 'sizzle';
import tpl_chat_status_modal from "templates/chat_status_modal";
import tpl_profile from "templates/profile.js";
import tpl_profile_modal from "templates/profile_modal";
import { BootstrapModal } from "./converse-modal.js";
import { __ } from './i18n';
import { _converse, api, converse } from "@converse/headless/converse-core";
const u = converse.env.utils;
converse.plugins.add('converse-profile', {
......@@ -35,146 +29,6 @@ converse.plugins.add('converse-profile', {
});
_converse.ProfileModal = BootstrapModal.extend({
id: "user-profile-modal",
events: {
'submit .profile-form': 'onFormSubmitted'
},
initialize () {
this.listenTo(this.model, 'change', this.render);
BootstrapModal.prototype.initialize.apply(this, arguments);
/**
* Triggered when the _converse.ProfileModal has been created and initialized.
* @event _converse#profileModalInitialized
* @type { _converse.XMPPStatus }
* @example _converse.api.listen.on('profileModalInitialized', status => { ... });
*/
api.trigger('profileModalInitialized', this.model);
},
toHTML () {
return tpl_profile_modal(Object.assign(
this.model.toJSON(),
this.model.vcard.toJSON(),
this.getAvatarData(),
{ 'view': this }
));
},
getAvatarData () {
const image_type = this.model.vcard.get('image_type');
const image_data = this.model.vcard.get('image');
const image = "data:" + image_type + ";base64," + image_data;
return {
'height': 128,
'width': 128,
image,
};
},
afterRender () {
this.tabs = sizzle('.nav-item .nav-link', this.el).map(e => new bootstrap.Tab(e));
},
async setVCard (data) {
try {
await api.vcard.set(_converse.bare_jid, data);
} catch (err) {
log.fatal(err);
this.alert([
__("Sorry, an error happened while trying to save your profile data."),
__("You can check your browser's developer console for any error output.")
].join(" "));
return;
}
this.modal.hide();
},
onFormSubmitted (ev) {
ev.preventDefault();
const reader = new FileReader();
const form_data = new FormData(ev.target);
const image_file = form_data.get('image');
const data = {
'fn': form_data.get('fn'),
'nickname': form_data.get('nickname'),
'role': form_data.get('role'),
'email': form_data.get('email'),
'url': form_data.get('url'),
};
if (!image_file.size) {
Object.assign(data, {
'image': this.model.vcard.get('image'),
'image_type': this.model.vcard.get('image_type')
});
this.setVCard(data);
} else {
reader.onloadend = () => {
Object.assign(data, {
'image': btoa(reader.result),
'image_type': image_file.type
});
this.setVCard(data);
};
reader.readAsBinaryString(image_file);
}
}
});
_converse.ChatStatusModal = BootstrapModal.extend({
id: "modal-status-change",
events: {
"submit form#set-xmpp-status": "onFormSubmitted",
"click .clear-input": "clearStatusMessage"
},
toHTML () {
return tpl_chat_status_modal(
Object.assign(
this.model.toJSON(),
this.model.vcard.toJSON(), {
'label_away': __('Away'),
'label_busy': __('Busy'),
'label_cancel': __('Cancel'),
'label_close': __('Close'),
'label_custom_status': __('Custom status'),
'label_offline': __('Offline'),
'label_online': __('Online'),
'label_save': __('Save'),
'label_xa': __('Away for long'),
'modal_title': __('Change chat status'),
'placeholder_status_message': __('Personal status message')
}));
},
afterRender () {
this.el.addEventListener('shown.bs.modal', () => {
this.el.querySelector('input[name="status_message"]').focus();
}, false);
},
clearStatusMessage (ev) {
if (ev && ev.preventDefault) {
ev.preventDefault();
u.hideElement(this.el.querySelector('.clear-input'));
}
const roster_filter = this.el.querySelector('input[name="status_message"]');
roster_filter.value = '';
},
onFormSubmitted (ev) {
ev.preventDefault();
const data = new FormData(ev.target);
this.model.save({
'status_message': data.get('status_message'),
'status': data.get('chat_status')
});
this.modal.hide();
}
});
_converse.XMPPStatusView = _converse.ViewWithAvatar.extend({
tagName: "div",
events: {
......
......@@ -6,23 +6,21 @@
import "@converse/headless/converse-chatboxes";
import "@converse/headless/converse-roster";
import "converse-modal";
import "modals/add-contact.js";
import log from "@converse/headless/log";
import tpl_add_contact_modal from "templates/add_contact_modal.js";
import tpl_group_header from "templates/group_header.html";
import tpl_pending_contact from "templates/pending_contact.html";
import tpl_requesting_contact from "templates/requesting_contact.html";
import tpl_roster from "templates/roster.html";
import tpl_roster_filter from "templates/roster_filter.js";
import tpl_roster_item from "templates/roster_item.html";
import { BootstrapModal } from "./converse-modal.js";
import { Model } from '@converse/skeletor/src/model.js';
import { OrderedListView } from "@converse/skeletor/src/overview";
import { View } from '@converse/skeletor/src/view.js';
import { __ } from './i18n';
import { _converse, api, converse } from "@converse/headless/converse-core";
import { compact, debounce, has, without } from "lodash-es";
import { debounce, has, without } from "lodash-es";
const { Strophe } = converse.env;
const u = converse.env.utils;
......@@ -55,136 +53,6 @@ converse.plugins.add('converse-rosterview', {
};
_converse.AddContactModal = BootstrapModal.extend({
id: "add-contact-modal",
events: {
'submit form': 'addContactFromForm'
},
initialize () {
BootstrapModal.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'change', this.render);
},
toHTML () {
const label_nickname = api.settings.get('xhr_user_search_url') ? __('Contact name') : __('Optional nickname');
return tpl_add_contact_modal(Object.assign(this.model.toJSON(), { _converse, label_nickname }));
},
afterRender () {
if (typeof api.settings.get('xhr_user_search_url') === 'string') {
this.initXHRAutoComplete();
} else {
this.initJIDAutoComplete();
}
const jid_input = this.el.querySelector('input[name="jid"]');
this.el.addEventListener('shown.bs.modal', () => jid_input.focus(), false);
},
initJIDAutoComplete () {
if (!api.settings.get('autocomplete_add_contact')) {
return;
}
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': _converse.FILTER_STARTSWITH,
'list': [...new Set(_converse.roster.map(item => Strophe.getDomainFromJid(item.get('jid'))))]
});
},
initXHRAutoComplete () {
if (!api.settings.get('autocomplete_add_contact')) {
return this.initXHRFetch();
}
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 = () => {
if (xhr.responseText) {
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();
}
};
const input_el = this.el.querySelector('input[name="name"]');
input_el.addEventListener('input', debounce(() => {
xhr.open("GET", `${api.settings.get('xhr_user_search_url')}q=${encodeURIComponent(input_el.value)}`, true);
xhr.send()
} , 300));
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;
});
},
initXHRFetch () {
this.xhr = new window.XMLHttpRequest();
this.xhr.onload = () => {
if (this.xhr.responseText) {
const r = this.xhr.responseText;
const list = JSON.parse(r).map(i => ({'label': i.fullname || i.jid, 'value': i.jid}));
if (list.length !== 1) {
const el = this.el.querySelector('.invalid-feedback');
el.textContent = __('Sorry, could not find a contact with that name')
u.addClass('d-block', el);
return;
}
const jid = list[0].value;
if (this.validateSubmission(jid)) {
const form = this.el.querySelector('form');
const name = list[0].label;
this.afterSubmission(form, jid, name);
}
}
};
},
validateSubmission (jid) {
const el = this.el.querySelector('.invalid-feedback');
if (!jid || compact(jid.split('@')).length < 2) {
u.addClass('is-invalid', this.el.querySelector('input[name="jid"]'));
u.addClass('d-block', el);
return false;
} else if (_converse.roster.get(Strophe.getBareJidFromJid(jid))) {
el.textContent = __('This contact has already been added')
u.addClass('d-block', el);
return false;
}
u.removeClass('d-block', el);
return true;
},
afterSubmission (form, jid, name) {
_converse.roster.addAndSubscribe(jid, name);
this.model.clear();
this.modal.hide();
},
addContactFromForm (ev) {
ev.preventDefault();
const data = new FormData(ev.target),
jid = (data.get('jid') || '').trim();
if (!jid && typeof api.settings.get('xhr_user_search_url') === 'string') {
const input_el = this.el.querySelector('input[name="name"]');
this.xhr.open("GET", `${api.settings.get('xhr_user_search_url')}q=${encodeURIComponent(input_el.value)}`, true);
this.xhr.send()
return;
}
if (this.validateSubmission(jid)) {
this.afterSubmission(ev.target, jid, data.get('name'));
}
}
});
_converse.RosterFilter = Model.extend({
initialize () {
this.set({
......@@ -195,6 +63,7 @@ converse.plugins.add('converse-rosterview', {
},
});
_converse.RosterFilterView = View.extend({
tagName: 'span',
......@@ -980,4 +849,3 @@ converse.plugins.add('converse-rosterview', {
});
}
});
import BootstrapModal from "./base.js";
import tpl_add_contact_modal from "./templates/add-contact.js";
import { __ } from '../i18n';
import { _converse, api, converse } from "@converse/headless/converse-core";
import { compact, debounce } from "lodash-es";
const { Strophe } = converse.env;
const u = converse.env.utils;
const AddContactModal = BootstrapModal.extend({
id: "add-contact-modal",
events: {
'submit form': 'addContactFromForm'
},
initialize () {
BootstrapModal.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'change', this.render);
},
toHTML () {
const label_nickname = api.settings.get('xhr_user_search_url') ? __('Contact name') : __('Optional nickname');
return tpl_add_contact_modal(Object.assign(this.model.toJSON(), { _converse, label_nickname }));
},
afterRender () {
if (typeof api.settings.get('xhr_user_search_url') === 'string') {
this.initXHRAutoComplete();
} else {
this.initJIDAutoComplete();
}
const jid_input = this.el.querySelector('input[name="jid"]');
this.el.addEventListener('shown.bs.modal', () => jid_input.focus(), false);
},
initJIDAutoComplete () {
if (!api.settings.get('autocomplete_add_contact')) {
return;
}
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': _converse.FILTER_STARTSWITH,
'list': [...new Set(_converse.roster.map(item => Strophe.getDomainFromJid(item.get('jid'))))]
});
},
initXHRAutoComplete () {
if (!api.settings.get('autocomplete_add_contact')) {
return this.initXHRFetch();
}
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 = () => {
if (xhr.responseText) {
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();
}
};
const input_el = this.el.querySelector('input[name="name"]');
input_el.addEventListener('input', debounce(() => {
xhr.open("GET", `${api.settings.get('xhr_user_search_url')}q=${encodeURIComponent(input_el.value)}`, true);
xhr.send()
} , 300));
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;
});
},
initXHRFetch () {
this.xhr = new window.XMLHttpRequest();
this.xhr.onload = () => {
if (this.xhr.responseText) {
const r = this.xhr.responseText;
const list = JSON.parse(r).map(i => ({'label': i.fullname || i.jid, 'value': i.jid}));
if (list.length !== 1) {
const el = this.el.querySelector('.invalid-feedback');
el.textContent = __('Sorry, could not find a contact with that name')
u.addClass('d-block', el);
return;
}
const jid = list[0].value;
if (this.validateSubmission(jid)) {
const form = this.el.querySelector('form');
const name = list[0].label;
this.afterSubmission(form, jid, name);
}
}
};
},
validateSubmission (jid) {
const el = this.el.querySelector('.invalid-feedback');
if (!jid || compact(jid.split('@')).length < 2) {
u.addClass('is-invalid', this.el.querySelector('input[name="jid"]'));
u.addClass('d-block', el);
return false;
} else if (_converse.roster.get(Strophe.getBareJidFromJid(jid))) {
el.textContent = __('This contact has already been added')
u.addClass('d-block', el);
return false;
}
u.removeClass('d-block', el);
return true;
},
afterSubmission (form, jid, name) {
_converse.roster.addAndSubscribe(jid, name);
this.model.clear();
this.modal.hide();
},
addContactFromForm (ev) {
ev.preventDefault();
const data = new FormData(ev.target),
jid = (data.get('jid') || '').trim();
if (!jid && typeof api.settings.get('xhr_user_search_url') === 'string') {
const input_el = this.el.querySelector('input[name="name"]');
this.xhr.open("GET", `${api.settings.get('xhr_user_search_url')}q=${encodeURIComponent(input_el.value)}`, true);
this.xhr.send()
return;
}
if (this.validateSubmission(jid)) {
this.afterSubmission(ev.target, jid, data.get('name'));
}
}
});
_converse.AddContactModal = AddContactModal;
export default AddContactModal;
import tpl_add_chatroom_modal from "templates/add_chatroom_modal.js";
import { BootstrapModal } from "../converse-modal.js";
import tpl_add_muc from "./templates/add-muc.js";
import BootstrapModal from "./base.js";
import { Strophe } from 'strophe.js/src/strophe';
import { __ } from '../i18n';
import { _converse, api, converse } from "@converse/headless/converse-core";
......@@ -8,6 +8,7 @@ const u = converse.env.utils;
export default BootstrapModal.extend({
persistent: true,
id: 'add-chatroom-modal',
events: {
......@@ -28,7 +29,7 @@ export default BootstrapModal.extend({
const muc_domain = this.model.get('muc_domain') || api.settings.get('muc_domain');
placeholder = muc_domain ? `name@${muc_domain}` : __('name@conference.example.org');
}
return tpl_add_chatroom_modal(Object.assign(this.model.toJSON(), {
return tpl_add_muc(Object.assign(this.model.toJSON(), {
'_converse': _converse,
'label_room_address': api.settings.get('muc_domain') ? __('Groupchat name') : __('Groupchat address'),
'chatroom_placeholder': placeholder,
......
import BootstrapModal from "./base.js";
import tpl_alert_modal from "./templates/alert.js";
import { __ } from '../i18n';
const Alert = BootstrapModal.extend({
initialize () {
BootstrapModal.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'change', this.render)
},
toHTML () {
return tpl_alert_modal(Object.assign({__}, this.model.toJSON()));
}
});
export default Alert;
import bootstrap from "bootstrap.native";
import log from "@converse/headless/log";
import tpl_alert_component from "templates/alert.js";
import { View } from '@converse/skeletor/src/view.js';
import { _converse, converse } from "@converse/headless/converse-core";
import { render } from 'lit-html';
const { sizzle } = converse.env;
const u = converse.env.utils;
const BaseModal = View.extend({
className: "modal",
persistent: false, // Whether this modal should persist in the DOM once it's been closed
events: {
'click .nav-item .nav-link': 'switchTab'
},
initialize () {
this.render()
this.el.setAttribute('tabindex', '-1');
this.el.setAttribute('role', 'dialog');
this.el.setAttribute('aria-hidden', 'true');
const label_id = this.el.querySelector('.modal-title').getAttribute('id');
label_id && this.el.setAttribute('aria-labelledby', label_id);
this.insertIntoDOM();
const Modal = bootstrap.Modal;
this.modal = new Modal(this.el, {
backdrop: true,
keyboard: true
});
this.el.addEventListener('hide.bs.modal', () => this.onHide(), false);
},
onHide () {
u.removeClass('selected', this.trigger_el);
!this.persistent && this.remove();
},
insertIntoDOM () {
const container_el = _converse.chatboxviews.el.querySelector("#converse-modals");
container_el.insertAdjacentElement('beforeEnd', this.el);
},
switchTab (ev) {
ev.stopPropagation();
ev.preventDefault();
sizzle('.nav-link.active', this.el).forEach(el => {
u.removeClass('active', this.el.querySelector(el.getAttribute('href')));
u.removeClass('active', el);
});
u.addClass('active', ev.target);
u.addClass('active', this.el.querySelector(ev.target.getAttribute('href')))
},
alert (message, type='primary') {
const body = this.el.querySelector('.modal-alert');
if (body === null) {
log.error("Could not find a .modal-alert element in the modal to show an alert message in!");
return;
}
// FIXME: Instead of adding the alert imperatively, we should
// find a way to let the modal rerender with an alert message
render(tpl_alert_component({'type': `alert-${type}`, 'message': message}), body);
const el = body.firstElementChild;
setTimeout(() => {
u.addClass('fade-out', el);
setTimeout(() => u.removeElement(el), 600);
}, 5000);
},
show (ev) {
if (ev) {
ev.preventDefault();
this.trigger_el = ev.target;
this.trigger_el.classList.add('selected');
}
this.modal.show();
}
});
export default BaseModal;
import BootstrapModal from "./base.js";
import tpl_chat_status_modal from "./templates/chat-status.js";
import { __ } from '../i18n';
import { _converse, converse } from "@converse/headless/converse-core";
const u = converse.env.utils;
const ChatStatusModal = BootstrapModal.extend({
id: "modal-status-change",
events: {
"submit form#set-xmpp-status": "onFormSubmitted",
"click .clear-input": "clearStatusMessage"
},
toHTML () {
return tpl_chat_status_modal(
Object.assign(
this.model.toJSON(),
this.model.vcard.toJSON(), {
'label_away': __('Away'),
'label_busy': __('Busy'),
'label_cancel': __('Cancel'),
'label_close': __('Close'),
'label_custom_status': __('Custom status'),
'label_offline': __('Offline'),
'label_online': __('Online'),
'label_save': __('Save'),
'label_xa': __('Away for long'),
'modal_title': __('Change chat status'),
'placeholder_status_message': __('Personal status message')
}));
},
afterRender () {
this.el.addEventListener('shown.bs.modal', () => {
this.el.querySelector('input[name="status_message"]').focus();
}, false);
},
clearStatusMessage (ev) {
if (ev && ev.preventDefault) {
ev.preventDefault();
u.hideElement(this.el.querySelector('.clear-input'));
}
const roster_filter = this.el.querySelector('input[name="status_message"]');
roster_filter.value = '';
},
onFormSubmitted (ev) {
ev.preventDefault();
const data = new FormData(ev.target);
this.model.save({
'status_message': data.get('status_message'),
'status': data.get('chat_status')
});
this.modal.hide();
}
});
_converse.ChatStatusModal = ChatStatusModal;
export default ChatStatusModal;
import BootstrapModal from './base.js';
import tpl_prompt from "./templates/prompt.js";
import { converse } from "@converse/headless/converse-core";
const u = converse.env.utils;
const Confirm = BootstrapModal.extend({
events: {
'submit .confirm': 'onConfimation'
},
initialize () {
this.confirmation = u.getResolveablePromise();
BootstrapModal.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'change', this.render)
this.el.addEventListener('closed.bs.modal', () => this.confirmation.reject(), false);
},
toHTML () {
return tpl_prompt(this.model.toJSON());
},
afterRender () {
if (!this.close_handler_registered) {
this.el.addEventListener('closed.bs.modal', () => {
if (!this.confirmation.isResolved) {
this.confirmation.reject()
}
}, false);
this.close_handler_registered = true;
}
},
onConfimation (ev) {
ev.preventDefault();
const form_data = new FormData(ev.target);
const fields = (this.model.get('fields') || [])
.map(field => {
const value = form_data.get(field.name).trim();
field.value = value;
if (field.challenge) {
field.challenge_failed = (value !== field.challenge);
}
return field;
});
if (fields.filter(c => c.challenge_failed).length) {
this.model.set('fields', fields);
// Setting an array doesn't trigger a change event
this.model.trigger('change');
return;
}
this.confirmation.resolve(fields);
this.modal.hide();
}
});
export default Confirm;
import { BootstrapModal } from "../converse-modal.js";
import tpl_image_modal from "../templates/image_modal.js";
import BootstrapModal from "./base.js";
import tpl_image_modal from "./templates/image.js";
export default BootstrapModal.extend({
......
import { BootstrapModal } from "../converse-modal.js";
import tpl_message_versions_modal from "../templates/message_versions_modal.js";
import BootstrapModal from "./base.js";
import tpl_message_versions_modal from "./templates/message-versions.js";
export default BootstrapModal.extend({
// FIXME: this isn't globally unique
id: "message-versions-modal",
toHTML () {
return tpl_message_versions_modal(this.model.toJSON());
}
......
import BootstrapModal from "./base.js";
import log from "@converse/headless/log";
import sizzle from "sizzle";
import tpl_moderator_tools_modal from "../templates/moderator_tools_modal.js";
import tpl_moderator_tools_modal from "./templates/moderator-tools.js";
import { AFFILIATIONS, ROLES } from "@converse/headless/converse-muc.js";
import { BootstrapModal } from "../converse-modal.js";
import { __ } from '../i18n';
import { api, converse } from "@converse/headless/converse-core";
......@@ -12,7 +12,7 @@ let _converse;
export default BootstrapModal.extend({
id: "converse-modtools-modal",
persistent: true,
initialize (attrs) {
_converse = attrs._converse;
......
import { BootstrapModal } from "../converse-modal.js";
import BootstrapModal from "./base.js";
import { __ } from '../i18n';
import { api, converse } from "@converse/headless/converse-core";
import log from "@converse/headless/log";
......
import { BootstrapModal } from "../converse-modal.js";
import BootstrapModal from "./base.js";
import tpl_muc_details from "./templates/muc-details.js";
import { __ } from '../i18n';
import tpl_chatroom_details_modal from "../templates/chatroom_details_modal.js";
export default BootstrapModal.extend({
......@@ -15,7 +15,7 @@ export default BootstrapModal.extend({
},
toHTML () {
return tpl_chatroom_details_modal(Object.assign(
return tpl_muc_details(Object.assign(
this.model.toJSON(), {
'config': this.model.config.toJSON(),
'display_name': __('Groupchat info for %1$s', this.model.getDisplayName()),
......
import tpl_muc_invite_modal from "templates/muc_invite_modal.js";
import { BootstrapModal } from "../converse-modal.js";
import BootstrapModal from "./base.js";
import tpl_muc_invite_modal from "./templates/muc-invite.js";
import { _converse, converse } from "@converse/headless/converse-core";
const u = converse.env.utils;
......@@ -49,5 +49,3 @@ export default BootstrapModal.extend({
}
}
});
import BootstrapModal from "./base.js";
import log from "@converse/headless/log";
import sizzle from 'sizzle';
import st from "@converse/headless/utils/stanza";
import tpl_list_chatrooms_modal from "templates/list_chatrooms_modal.js";
import tpl_list_chatrooms_modal from "./templates/muc-list.js";
import tpl_room_description from "templates/room_description.html";
import tpl_spinner from "templates/spinner.js";
import { BootstrapModal } from "../converse-modal.js";
import { Strophe, $iq } from 'strophe.js/src/strophe';
import { __ } from '../i18n';
import { _converse, api, converse } from "@converse/headless/converse-core";
......@@ -83,6 +83,7 @@ function toggleRoomInfo (ev) {
export default BootstrapModal.extend({
id: "list-chatrooms-modal",
persistent: true,
initialize () {
this.items = [];
......
import BootstrapModal from "./base.js";
import tpl_occupant_modal from "./templates/occupant.js";
import { BootstrapModal } from "../converse-modal.js";
import { _converse, api } from "@converse/headless/converse-core";
const OccupantModal = BootstrapModal.extend({
id: "muc-occupant-modal",
initialize () {
BootstrapModal.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'change', this.render);
/**
* Triggered once the OccupantModal has been initialized
* @event _converse#userDetailsModalInitialized
* @type { _converse.ChatBox }
* @example _converse.api.listen.on('userDetailsModalInitialized', chatbox => { ... });
*/
* Triggered once the OccupantModal has been initialized
* @event _converse#userDetailsModalInitialized
* @type { _converse.ChatBox }
* @example _converse.api.listen.on('userDetailsModalInitialized', chatbox => { ... });
*/
api.trigger('occupantModalInitialized', this.model);
},
......
import BootstrapModal from "./base.js";
import bootstrap from "bootstrap.native";
import log from "@converse/headless/log";
import sizzle from 'sizzle';
import tpl_profile_modal from "./templates/profile.js";
import { __ } from '../i18n';
import { _converse, api } from "@converse/headless/converse-core";
const ProfileModal = BootstrapModal.extend({
id: "user-profile-modal",
events: {
'submit .profile-form': 'onFormSubmitted'
},
initialize () {
this.listenTo(this.model, 'change', this.render);
BootstrapModal.prototype.initialize.apply(this, arguments);
/**
* Triggered when the _converse.ProfileModal has been created and initialized.
* @event _converse#profileModalInitialized
* @type { _converse.XMPPStatus }
* @example _converse.api.listen.on('profileModalInitialized', status => { ... });
*/
api.trigger('profileModalInitialized', this.model);
},
toHTML () {
return tpl_profile_modal(Object.assign(
this.model.toJSON(),
this.model.vcard.toJSON(),
this.getAvatarData(),
{ 'view': this }
));
},
getAvatarData () {
const image_type = this.model.vcard.get('image_type');
const image_data = this.model.vcard.get('image');
const image = "data:" + image_type + ";base64," + image_data;
return {
'height': 128,
'width': 128,
image,
};
},
afterRender () {
this.tabs = sizzle('.nav-item .nav-link', this.el).map(e => new bootstrap.Tab(e));
},
async setVCard (data) {
try {
await api.vcard.set(_converse.bare_jid, data);
} catch (err) {
log.fatal(err);
this.alert([
__("Sorry, an error happened while trying to save your profile data."),
__("You can check your browser's developer console for any error output.")
].join(" "));
return;
}
this.modal.hide();
},
onFormSubmitted (ev) {
ev.preventDefault();
const reader = new FileReader();
const form_data = new FormData(ev.target);
const image_file = form_data.get('image');
const data = {
'fn': form_data.get('fn'),
'nickname': form_data.get('nickname'),
'role': form_data.get('role'),
'email': form_data.get('email'),
'url': form_data.get('url'),
};
if (!image_file.size) {
Object.assign(data, {
'image': this.model.vcard.get('image'),
'image_type': this.model.vcard.get('image_type')
});
this.setVCard(data);
} else {
reader.onloadend = () => {
Object.assign(data, {
'image': btoa(reader.result),
'image_type': image_file.type
});
this.setVCard(data);
};
reader.readAsBinaryString(image_file);
}
}
});
_converse.ProfileModal = ProfileModal;
export default ProfileModal;
import { html } from "lit-html";
import { __ } from '../i18n';
import { modal_header_close_button } from "./buttons"
import { __ } from '../../i18n';
import { modal_header_close_button } from "./buttons.js"
export default (o) => {
......
import xss from "xss/dist/xss";
import { __ } from '../i18n';
import { __ } from '../../i18n';
import { html } from "lit-html";
import { modal_header_close_button } from "./buttons"
import { unsafeHTML } from "lit-html/directives/unsafe-html.js";
......
import { html } from "lit-html";
import { modal_header_close_button } from "./buttons"
import { modal_header_close_button } from "./buttons.js"
export default (o) => html`
......
import { __ } from '../i18n';
import { __ } from '../../i18n';
import { html } from "lit-html";
export const modal_close_button = html`<button type="button" class="btn btn-secondary" data-dismiss="modal">${__('Close')}</button>`;
export const modal_header_close_button = html`<button type="button" class="close" data-dismiss="modal" aria-label="${__('Close')}"><span aria-hidden="true">×</span></button>`;
import { html } from "lit-html";
import { modal_header_close_button } from "./buttons"
import { modal_header_close_button } from "./buttons.js"
export default (o) => html`
......
import { html } from "lit-html";
import { __ } from '../i18n';
import { modal_close_button, modal_header_close_button } from "./buttons"
import { __ } from '../../i18n';
import { modal_close_button, modal_header_close_button } from "./buttons.js"
export default (o) => {
......
import { html } from "lit-html";
import { __ } from '../i18n';
import dayjs from 'dayjs';
import { modal_close_button, modal_header_close_button } from "./buttons"
import { __ } from '../../i18n';
import { html } from "lit-html";
import { modal_close_button, modal_header_close_button } from "./buttons.js"
export default (o) => html`
......
import { html } from "lit-html";
import { __ } from '../i18n';
import spinner from "./spinner.js";
import { modal_header_close_button } from "./buttons"
import { __ } from '../../i18n';
import spinner from "../../templates/spinner.js";
import { modal_header_close_button } from "./buttons.js"
function getRoleHelpText (role) {
......
import { __ } from '../i18n';
import { __ } from '../../i18n';
import { html } from "lit-html";
import { modal_close_button, modal_header_close_button } from "./buttons"
import { modal_close_button, modal_header_close_button } from "./buttons.js"
import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
import xss from "xss/dist/xss";
......
import { html } from "lit-html";
import { __ } from '../i18n';
import { modal_header_close_button } from "./buttons"
import { __ } from '../../i18n';
import { modal_header_close_button } from "./buttons.js"
export default (o) => {
......
import { __ } from '../i18n';
import { __ } from '../../i18n';
import { html } from "lit-html";
import { repeat } from 'lit-html/directives/repeat.js';
import { modal_close_button, modal_header_close_button } from "./buttons"
import spinner from "./spinner.js";
import { modal_close_button, modal_header_close_button } from "./buttons.js"
import spinner from "../../templates/spinner.js";
const form = (o) => {
......
import { html } from "lit-html";
import { modal_close_button, modal_header_close_button } from "../../templates/buttons"
import { modal_close_button, modal_header_close_button } from "./buttons.js"
import { renderAvatar } from '../../templates/directives/avatar';
......
import "../components/image_picker.js";
import spinner from "./spinner.js";
import { __ } from '../i18n';
import "../../components/image_picker.js";
import spinner from "../../templates/spinner.js";
import { __ } from '../../i18n';
import { _converse, converse } from "@converse/headless/converse-core";
import { html } from "lit-html";
import { modal_header_close_button } from "./buttons";
import { modal_header_close_button } from "./buttons.js";
const u = converse.env.utils;
......
import { html } from "lit-html";
import { __ } from '../i18n';
import { __ } from '../../i18n';
const tpl_field = (f) => html`
......
import { __ } from '../i18n';
import { __ } from '../../i18n';
import { html } from "lit-html";
import avatar from "./avatar.js";
import { modal_close_button, modal_header_close_button } from "./buttons"
import avatar from "../../templates/avatar.js";
import { modal_close_button, modal_header_close_button } from "./buttons.js"
const device_fingerprint = (o) => {
......
import '../components/adhoc-commands.js';
import '../../components/adhoc-commands.js';
import xss from "xss/dist/xss";
import { __ } from '../i18n';
import { __ } from '../../i18n';
import { api } from "@converse/headless/converse-core";
import { html } from "lit-html";
import { modal_header_close_button } from "./buttons"
import { modal_header_close_button } from "./buttons.js"
import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
......
import BootstrapModal from "./base.js";
import log from "@converse/headless/log";
import tpl_user_details_modal from "../templates/user_details_modal.js";
import { BootstrapModal } from "../converse-modal.js";
import tpl_user_details_modal from "./templates/user-details.js";
import { __ } from '../i18n';
import { _converse, api, converse } from "@converse/headless/converse-core";
......@@ -8,7 +8,7 @@ const u = converse.env.utils;
const UserDetailsModal = BootstrapModal.extend({
id: "user-details-modal",
persistent: true,
events: {
'click button.refresh-contact': 'refreshContact',
......
import { BootstrapModal } from "../converse-modal.js";
import tpl_user_settings_modal from "templates/user_settings_modal";
import BootstrapModal from "./base.js";
import tpl_user_settings_modal from "./templates/user-settings.js";
let _converse;
......@@ -21,4 +21,3 @@ export default BootstrapModal.extend({
);
}
});
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