diff --git a/src/converse-message-view.js b/src/converse-message-view.js
index 2371443c3..9bdf2b0f6 100644
--- a/src/converse-message-view.js
+++ b/src/converse-message-view.js
@@ -6,6 +6,7 @@
(function (root, factory) {
define([
+ "./utils/html",
"utils/emoji",
"@converse/headless/converse-core",
"xss",
@@ -17,6 +18,7 @@
"templates/message_versions_modal.html",
], factory);
}(this, function (
+ html,
u,
converse,
xss,
diff --git a/src/converse-register.js b/src/converse-register.js
index d9339576f..3de209ab9 100644
--- a/src/converse-register.js
+++ b/src/converse-register.js
@@ -12,12 +12,12 @@
(function (root, factory) {
define(["utils/form",
"@converse/headless/converse-core",
- "@converse/headless/templates/form_username.html",
+ "templates/form_username.html",
"templates/register_link.html",
"templates/register_panel.html",
"templates/registration_form.html",
"templates/registration_request.html",
- "@converse/headless/templates/form_input.html",
+ "templates/form_input.html",
"templates/spinner.html",
"converse-controlbox"
], factory);
diff --git a/src/headless/utils/core.js b/src/headless/utils/core.js
index 13e956825..e08ad47c3 100644
--- a/src/headless/utils/core.js
+++ b/src/headless/utils/core.js
@@ -12,15 +12,9 @@
define([
"sizzle",
"es6-promise/dist/es6-promise.auto",
- "fast-text-encoding/text",
"../lodash.noconflict",
"backbone",
"strophe.js",
- "urijs",
- "../templates/audio.html",
- "../templates/file.html",
- "../templates/image.html",
- "../templates/video.html"
], factory);
} else {
// Used by the mockups
@@ -49,57 +43,14 @@
}(this, function (
sizzle,
Promise,
- FastTextEncoding,
_,
Backbone,
- Strophe,
- URI,
- tpl_audio,
- tpl_file,
- tpl_image,
- tpl_video
+ Strophe
) {
"use strict";
Strophe = Strophe.Strophe;
- const URL_REGEX = /\b(https?:\/\/|www\.|https?:\/\/www\.)[^\s<>]{2,200}\b\/?/g;
-
- const logger = _.assign({
- 'debug': _.get(console, 'log') ? console.log.bind(console) : _.noop,
- 'error': _.get(console, 'log') ? console.log.bind(console) : _.noop,
- 'info': _.get(console, 'log') ? console.log.bind(console) : _.noop,
- 'warn': _.get(console, 'log') ? console.log.bind(console) : _.noop
- }, console);
-
- const isImage = function (url) {
- return new Promise((resolve, reject) => {
- var img = new Image();
- var timer = window.setTimeout(function () {
- reject(new Error("Could not determine whether it's an image"));
- img = null;
- }, 3000);
- img.onerror = img.onabort = function () {
- clearTimeout(timer);
- reject(new Error("Could not determine whether it's an image"));
- };
- img.onload = function () {
- clearTimeout(timer);
- resolve(img);
- };
- img.src = url;
- });
- };
-
- function slideOutWrapup (el) {
- /* Wrapup function for slideOut. */
- el.removeAttribute('data-slider-marker');
- el.classList.remove('collapsed');
- el.style.overflow = "";
- el.style.height = "";
- }
-
-
- var u = {};
+ const u = {};
u.getLongestSubstring = function (string, candidates) {
function reducer (accumulator, current_value) {
@@ -116,118 +67,6 @@
return candidates.reduce(reducer, '');
}
- u.getNextElement = function (el, selector='*') {
- let next_el = el.nextElementSibling;
- while (!_.isNull(next_el) && !sizzle.matchesSelector(next_el, selector)) {
- next_el = next_el.nextElementSibling;
- }
- return next_el;
- }
-
- u.getPreviousElement = function (el, selector='*') {
- let prev_el = el.previousSibling;
- while (!_.isNull(prev_el) && !sizzle.matchesSelector(prev_el, selector)) {
- prev_el = prev_el.previousSibling
- }
- return prev_el;
- }
-
- u.getFirstChildElement = function (el, selector='*') {
- let first_el = el.firstElementChild;
- while (!_.isNull(first_el) && !sizzle.matchesSelector(first_el, selector)) {
- first_el = first_el.nextSibling
- }
- return first_el;
- }
-
- u.getLastChildElement = function (el, selector='*') {
- let last_el = el.lastElementChild;
- while (!_.isNull(last_el) && !sizzle.matchesSelector(last_el, selector)) {
- last_el = last_el.previousSibling
- }
- return last_el;
- }
-
- u.calculateElementHeight = function (el) {
- /* Return the height of the passed in DOM element,
- * based on the heights of its children.
- */
- return _.reduce(
- el.children,
- (result, child) => result + child.offsetHeight, 0
- );
- }
-
- u.addClass = function (className, el) {
- if (el instanceof Element) {
- el.classList.add(className);
- }
- }
-
- u.removeClass = function (className, el) {
- if (el instanceof Element) {
- el.classList.remove(className);
- }
- return el;
- }
-
- u.removeElement = function (el) {
- if (!_.isNil(el) && !_.isNil(el.parentNode)) {
- el.parentNode.removeChild(el);
- }
- }
-
- u.showElement = _.flow(
- _.partial(u.removeClass, 'collapsed'),
- _.partial(u.removeClass, 'hidden')
- )
-
- u.hideElement = function (el) {
- if (!_.isNil(el)) {
- el.classList.add('hidden');
- }
- return el;
- }
-
- u.ancestor = function (el, selector) {
- let parent = el;
- while (!_.isNil(parent) && !sizzle.matchesSelector(parent, selector)) {
- parent = parent.parentElement;
- }
- return parent;
- }
-
- u.nextUntil = function (el, selector, include_self=false) {
- /* Return the element's siblings until one matches the selector. */
- const matches = [];
- let sibling_el = el.nextElementSibling;
- while (!_.isNil(sibling_el) && !sibling_el.matches(selector)) {
- matches.push(sibling_el);
- sibling_el = sibling_el.nextElementSibling;
- }
- return matches;
- }
-
- u.unescapeHTML = function (string) {
- /* Helper method that replace HTML-escaped symbols with equivalent characters
- * (e.g. transform occurrences of '&' to '&')
- *
- * Parameters:
- * (String) string: a String containing the HTML-escaped symbols.
- */
- var div = document.createElement('div');
- div.innerHTML = string;
- return div.innerText;
- };
-
- u.escapeHTML = function (string) {
- return string
- .replace(/&/g, "&")
- .replace(//g, ">")
- .replace(/"/g, """);
- };
-
u.prefixMentions = function (message) {
/* Given a message object, return its text with @ chars
* inserted before the mentioned nicknames.
@@ -241,328 +80,6 @@
return text;
};
- u.addMentionsMarkup = function (text, references, chatbox) {
- if (chatbox.get('message_type') !== 'groupchat') {
- return text;
- }
- const nick = chatbox.get('nick');
- references
- .sort((a, b) => b.begin - a.begin)
- .forEach(ref => {
- const mention = text.slice(ref.begin, ref.end)
- chatbox;
- if (mention === nick) {
- text = text.slice(0, ref.begin) + `${mention}` + text.slice(ref.end);
- } else {
- text = text.slice(0, ref.begin) + `${mention}` + text.slice(ref.end);
- }
- });
- return text;
- };
-
- u.addHyperlinks = function (text) {
- return URI.withinString(text, url => {
- const uri = new URI(url);
- url = uri.normalize()._string;
- const pretty_url = uri._parts.urn ? url : uri.readable();
- if (!uri._parts.protocol && !url.startsWith('http://') && !url.startsWith('https://')) {
- url = 'http://' + url;
- }
- if (uri._parts.protocol === 'xmpp' && uri._parts.query === 'join') {
- return `${u.escapeHTML(pretty_url)}`;
- }
- return `${u.escapeHTML(pretty_url)}`;
- }, {
- 'start': /\b(?:([a-z][a-z0-9.+-]*:\/\/)|xmpp:|mailto:|www\.)/gi
- });
- };
-
- u.renderNewLines = function (text) {
- return text.replace(/\n\n+/g, '
').replace(/\n/g, '
');
- };
-
- u.renderImageURLs = function (_converse, el) {
- /* Returns a Promise which resolves once all images have been loaded.
- */
- if (!_converse.show_images_inline) {
- return Promise.resolve();
- }
- const { __ } = _converse;
- const list = el.textContent.match(URL_REGEX) || [];
- return Promise.all(
- _.map(list, url =>
- new Promise((resolve, reject) => {
- if (u.isImageURL(url)) {
- return isImage(url).then(img => {
- const i = new Image();
- i.src = img.src;
- i.addEventListener('load', resolve);
- // We also resolve for non-images, otherwise the
- // Promise.all resolves prematurely.
- i.addEventListener('error', resolve);
-
- const { __ } = _converse;
- _.each(sizzle(`a[href="${url}"]`, el), (a) => {
- a.outerHTML= tpl_image({
- 'url': url,
- 'label_download': __('Download')
- })
- });
- }).catch(resolve)
- } else {
- return resolve();
- }
- })
- )
- )
- };
-
- u.renderFileURL = function (_converse, url) {
- const uri = new URI(url);
- if (u.isImageURL(uri) || u.isVideoURL(uri) || u.isAudioURL(uri)) {
- return url;
- }
- const { __ } = _converse,
- filename = uri.filename();
- return tpl_file({
- 'url': url,
- 'label_download': __('Download file "%1$s"', decodeURI(filename))
- })
- };
-
- u.isAudioURL = function (url) {
- if (!(url instanceof URI)) {
- url = new URI(url);
- }
- const filename = url.filename().toLowerCase();
- if (!_.includes(["https", "http"], url.protocol().toLowerCase())) {
- return false;
- }
- return filename.endsWith('.ogg') || filename.endsWith('.mp3') || filename.endsWith('.m4a');
- }
-
- u.isVideoURL = function (url) {
- if (!(url instanceof URI)) {
- url = new URI(url);
- }
- const filename = url.filename().toLowerCase();
- if (!_.includes(["https", "http"], url.protocol().toLowerCase())) {
- return false;
- }
- return filename.endsWith('.mp4') || filename.endsWith('.webm');
- }
-
- u.isImageURL = function (url) {
- if (!(url instanceof URI)) {
- url = new URI(url);
- }
- const filename = url.filename().toLowerCase();
- if (!_.includes(["https", "http"], url.protocol().toLowerCase())) {
- return false;
- }
- return filename.endsWith('.jpg') || filename.endsWith('.jpeg') ||
- filename.endsWith('.png') || filename.endsWith('.gif') ||
- filename.endsWith('.bmp') || filename.endsWith('.tiff') ||
- filename.endsWith('.svg');
- };
-
- u.renderImageURL = function (_converse, url) {
- if (!_converse.show_images_inline) {
- return u.addHyperlinks(url);
- }
- const uri = new URI(url);
- if (u.isImageURL(uri)) {
- const { __ } = _converse;
- return tpl_image({
- 'url': url,
- 'label_download': __('Download image "%1$s"', decodeURI(uri.filename()))
- })
- }
- return url;
- };
-
- u.renderMovieURL = function (_converse, url) {
- const uri = new URI(url);
- if (u.isVideoURL(uri)) {
- const { __ } = _converse;
- return tpl_video({
- 'url': url,
- 'label_download': __('Download video file "%1$s"', decodeURI(uri.filename()))
- })
- }
- return url;
- };
-
- u.renderAudioURL = function (_converse, url) {
- const uri = new URI(url);
- if (u.isAudioURL(uri)) {
- const { __ } = _converse;
- return tpl_audio({
- 'url': url,
- 'label_download': __('Download audio file "%1$s"', decodeURI(uri.filename()))
- })
- }
- return url;
- };
-
- u.slideInAllElements = function (elements, duration=300) {
- return Promise.all(
- _.map(
- elements,
- _.partial(u.slideIn, _, duration)
- ));
- };
-
- u.slideToggleElement = function (el, duration) {
- if (_.includes(el.classList, 'collapsed') ||
- _.includes(el.classList, 'hidden')) {
- return u.slideOut(el, duration);
- } else {
- return u.slideIn(el, duration);
- }
- };
-
- u.hasClass = function (className, el) {
- return _.includes(el.classList, className);
- };
-
- u.slideOut = function (el, duration=200) {
- /* Shows/expands an element by sliding it out of itself
- *
- * Parameters:
- * (HTMLElement) el - The HTML string
- * (Number) duration - The duration amount in milliseconds
- */
- return new Promise((resolve, reject) => {
- if (_.isNil(el)) {
- const err = "Undefined or null element passed into slideOut"
- logger.warn(err);
- reject(new Error(err));
- return;
- }
- const marker = el.getAttribute('data-slider-marker');
- if (marker) {
- el.removeAttribute('data-slider-marker');
- window.cancelAnimationFrame(marker);
- }
- const end_height = u.calculateElementHeight(el);
- if (window.converse_disable_effects) { // Effects are disabled (for tests)
- el.style.height = end_height + 'px';
- slideOutWrapup(el);
- resolve();
- return;
- }
- if (!u.hasClass('collapsed', el) && !u.hasClass('hidden', el)) {
- resolve();
- return;
- }
-
- const steps = duration/17; // We assume 17ms per animation which is ~60FPS
- let height = 0;
-
- function draw () {
- height += end_height/steps;
- if (height < end_height) {
- el.style.height = height + 'px';
- el.setAttribute(
- 'data-slider-marker',
- window.requestAnimationFrame(draw)
- );
- } else {
- // We recalculate the height to work around an apparent
- // browser bug where browsers don't know the correct
- // offsetHeight beforehand.
- el.removeAttribute('data-slider-marker');
- el.style.height = u.calculateElementHeight(el) + 'px';
- el.style.overflow = "";
- el.style.height = "";
- resolve();
- }
- }
- el.style.height = '0';
- el.style.overflow = 'hidden';
- el.classList.remove('hidden');
- el.classList.remove('collapsed');
- el.setAttribute(
- 'data-slider-marker',
- window.requestAnimationFrame(draw)
- );
- });
- };
-
- u.slideIn = function (el, duration=200) {
- /* Hides/collapses an element by sliding it into itself. */
- return new Promise((resolve, reject) => {
- if (_.isNil(el)) {
- const err = "Undefined or null element passed into slideIn";
- logger.warn(err);
- return reject(new Error(err));
- } else if (_.includes(el.classList, 'collapsed')) {
- return resolve(el);
- } else if (window.converse_disable_effects) { // Effects are disabled (for tests)
- el.classList.add('collapsed');
- el.style.height = "";
- return resolve(el);
- }
- const marker = el.getAttribute('data-slider-marker');
- if (marker) {
- el.removeAttribute('data-slider-marker');
- window.cancelAnimationFrame(marker);
- }
- const original_height = el.offsetHeight,
- steps = duration/17; // We assume 17ms per animation which is ~60FPS
- let height = original_height;
-
- el.style.overflow = 'hidden';
-
- function draw () {
- height -= original_height/steps;
- if (height > 0) {
- el.style.height = height + 'px';
- el.setAttribute(
- 'data-slider-marker',
- window.requestAnimationFrame(draw)
- );
- } else {
- el.removeAttribute('data-slider-marker');
- el.classList.add('collapsed');
- el.style.height = "";
- resolve(el);
- }
- }
- el.setAttribute(
- 'data-slider-marker',
- window.requestAnimationFrame(draw)
- );
- });
- };
-
- function afterAnimationEnds (el, callback) {
- el.classList.remove('visible');
- if (_.isFunction(callback)) {
- callback();
- }
- }
-
- u.fadeIn = function (el, callback) {
- if (_.isNil(el)) {
- logger.warn("Undefined or null element passed into fadeIn");
- }
- if (window.converse_disable_effects) {
- el.classList.remove('hidden');
- return afterAnimationEnds(el, callback);
- }
- if (_.includes(el.classList, 'hidden')) {
- el.classList.add('visible');
- el.classList.remove('hidden');
- el.addEventListener("webkitAnimationEnd", _.partial(afterAnimationEnds, el, callback));
- el.addEventListener("animationend", _.partial(afterAnimationEnds, el, callback));
- el.addEventListener("oanimationend", _.partial(afterAnimationEnds, el, callback));
- } else {
- afterAnimationEnds(el, callback);
- }
- };
-
u.isValidJID = function (jid) {
return _.compact(jid.split('@')).length === 2 && !jid.startsWith('@') && !jid.endsWith('@');
};
diff --git a/src/headless/utils/form.js b/src/headless/utils/form.js
index 42acc3494..fcd6e43dc 100644
--- a/src/headless/utils/form.js
+++ b/src/headless/utils/form.js
@@ -6,49 +6,16 @@
// Copyright (c) 2013-2018, Jan-Carel Brand
// Licensed under the Mozilla Public License (MPLv2)
//
-/*global define, escape, Jed */
+/*global define */
(function (root, factory) {
define([
- "sizzle",
"../lodash.noconflict",
"./core",
- "../templates/field.html",
- "../templates/select_option.html",
- "../templates/form_select.html",
- "../templates/form_textarea.html",
- "../templates/form_checkbox.html",
- "../templates/form_username.html",
- "../templates/form_input.html",
- "../templates/form_captcha.html",
- "../templates/form_url.html",
+ "../templates/field.html"
], factory);
-}(this, function (
- sizzle,
- _,
- u,
- tpl_field,
- tpl_select_option,
- tpl_form_select,
- tpl_form_textarea,
- tpl_form_checkbox,
- tpl_form_username,
- tpl_form_input,
- tpl_form_captcha,
- tpl_form_url
- ) {
+}(this, function (_, u, tpl_field) {
"use strict";
- var XFORM_TYPE_MAP = {
- 'text-private': 'password',
- 'text-single': 'text',
- 'fixed': 'label',
- 'boolean': 'checkbox',
- 'hidden': 'hidden',
- 'jid-multi': 'textarea',
- 'list-single': 'dropdown',
- 'list-multi': 'dropdown'
- };
-
u.webForm2xForm = function (field) {
/* Takes an HTML DOM and turns it into an XForm field.
*
@@ -72,101 +39,5 @@
})
);
};
-
- u.xForm2webForm = function (field, stanza, domain) {
- /* Takes a field in XMPP XForm (XEP-004: Data Forms) format
- * and turns it into an HTML field.
- *
- * Returns either text or a DOM element (which is not ideal, but fine
- * for now).
- *
- * Parameters:
- * (XMLElement) field - the field to convert
- */
- if (field.getAttribute('type')) {
- if (field.getAttribute('type') === 'list-single' ||
- field.getAttribute('type') === 'list-multi') {
-
- const values = _.map(
- u.queryChildren(field, 'value'),
- _.partial(_.get, _, 'textContent')
- );
- const options = _.map(
- u.queryChildren(field, 'option'),
- function (option) {
- const value = _.get(option.querySelector('value'), 'textContent');
- return tpl_select_option({
- 'value': value,
- 'label': option.getAttribute('label'),
- 'selected': _.includes(values, value),
- 'required': !_.isNil(field.querySelector('required'))
- })
- }
- );
- return tpl_form_select({
- 'id': u.getUniqueId(),
- 'name': field.getAttribute('var'),
- 'label': field.getAttribute('label'),
- 'options': options.join(''),
- 'multiple': (field.getAttribute('type') === 'list-multi'),
- 'required': !_.isNil(field.querySelector('required'))
- });
- } else if (field.getAttribute('type') === 'fixed') {
- const text = _.get(field.querySelector('value'), 'textContent');
- return ''+text+'
';
- } else if (field.getAttribute('type') === 'jid-multi') {
- return tpl_form_textarea({
- 'name': field.getAttribute('var'),
- 'label': field.getAttribute('label') || '',
- 'value': _.get(field.querySelector('value'), 'textContent'),
- 'required': !_.isNil(field.querySelector('required'))
- });
- } else if (field.getAttribute('type') === 'boolean') {
- return tpl_form_checkbox({
- 'id': u.getUniqueId(),
- 'name': field.getAttribute('var'),
- 'label': field.getAttribute('label') || '',
- 'checked': _.get(field.querySelector('value'), 'textContent') === "1" && 'checked="1"' || '',
- 'required': !_.isNil(field.querySelector('required'))
- });
- } else if (field.getAttribute('var') === 'url') {
- return tpl_form_url({
- 'label': field.getAttribute('label') || '',
- 'value': _.get(field.querySelector('value'), 'textContent')
- });
- } else if (field.getAttribute('var') === 'username') {
- return tpl_form_username({
- 'domain': ' @'+domain,
- 'name': field.getAttribute('var'),
- 'type': XFORM_TYPE_MAP[field.getAttribute('type')],
- 'label': field.getAttribute('label') || '',
- 'value': _.get(field.querySelector('value'), 'textContent'),
- 'required': !_.isNil(field.querySelector('required'))
- });
- } else {
- return tpl_form_input({
- 'id': u.getUniqueId(),
- 'label': field.getAttribute('label') || '',
- 'name': field.getAttribute('var'),
- 'placeholder': null,
- 'required': !_.isNil(field.querySelector('required')),
- 'type': XFORM_TYPE_MAP[field.getAttribute('type')],
- 'value': _.get(field.querySelector('value'), 'textContent')
- });
- }
- } else {
- if (field.getAttribute('var') === 'ocr') { // Captcha
- const uri = field.querySelector('uri');
- const el = sizzle('data[cid="'+uri.textContent.replace(/^cid:/, '')+'"]', stanza)[0];
- return tpl_form_captcha({
- 'label': field.getAttribute('label'),
- 'name': field.getAttribute('var'),
- 'data': _.get(el, 'textContent'),
- 'type': uri.getAttribute('type'),
- 'required': !_.isNil(field.querySelector('required'))
- });
- }
- }
- }
return u;
}));
diff --git a/src/headless/templates/audio.html b/src/templates/audio.html
similarity index 100%
rename from src/headless/templates/audio.html
rename to src/templates/audio.html
diff --git a/src/headless/templates/file.html b/src/templates/file.html
similarity index 100%
rename from src/headless/templates/file.html
rename to src/templates/file.html
diff --git a/src/headless/templates/form_captcha.html b/src/templates/form_captcha.html
similarity index 100%
rename from src/headless/templates/form_captcha.html
rename to src/templates/form_captcha.html
diff --git a/src/headless/templates/form_checkbox.html b/src/templates/form_checkbox.html
similarity index 100%
rename from src/headless/templates/form_checkbox.html
rename to src/templates/form_checkbox.html
diff --git a/src/headless/templates/form_input.html b/src/templates/form_input.html
similarity index 100%
rename from src/headless/templates/form_input.html
rename to src/templates/form_input.html
diff --git a/src/headless/templates/form_select.html b/src/templates/form_select.html
similarity index 100%
rename from src/headless/templates/form_select.html
rename to src/templates/form_select.html
diff --git a/src/headless/templates/form_textarea.html b/src/templates/form_textarea.html
similarity index 100%
rename from src/headless/templates/form_textarea.html
rename to src/templates/form_textarea.html
diff --git a/src/headless/templates/form_url.html b/src/templates/form_url.html
similarity index 100%
rename from src/headless/templates/form_url.html
rename to src/templates/form_url.html
diff --git a/src/headless/templates/form_username.html b/src/templates/form_username.html
similarity index 100%
rename from src/headless/templates/form_username.html
rename to src/templates/form_username.html
diff --git a/src/headless/templates/image.html b/src/templates/image.html
similarity index 100%
rename from src/headless/templates/image.html
rename to src/templates/image.html
diff --git a/src/headless/templates/select_option.html b/src/templates/select_option.html
similarity index 100%
rename from src/headless/templates/select_option.html
rename to src/templates/select_option.html
diff --git a/src/headless/templates/video.html b/src/templates/video.html
similarity index 100%
rename from src/headless/templates/video.html
rename to src/templates/video.html
diff --git a/src/utils/html.js b/src/utils/html.js
new file mode 100644
index 000000000..27f42b7eb
--- /dev/null
+++ b/src/utils/html.js
@@ -0,0 +1,643 @@
+// Converse.js (A browser based XMPP chat client)
+// http://conversejs.org
+//
+// This is a form utilities module.
+//
+// Copyright (c) 2013-2018, Jan-Carel Brand
+// Licensed under the Mozilla Public License (MPLv2)
+//
+/*global define */
+(function (root, factory) {
+ define([
+ "sizzle",
+ "../headless/lodash.noconflict",
+ "../headless/utils/core",
+ "urijs",
+ "../templates/audio.html",
+ "../headless/templates/field.html",
+ "../templates/file.html",
+ "../templates/form_captcha.html",
+ "../templates/form_checkbox.html",
+ "../templates/form_input.html",
+ "../templates/form_select.html",
+ "../templates/form_textarea.html",
+ "../templates/form_url.html",
+ "../templates/form_username.html",
+ "../templates/image.html",
+ "../templates/select_option.html",
+ "../templates/video.html"
+ ], factory);
+}(this, function (
+ sizzle,
+ _,
+ u,
+ URI,
+ tpl_audio,
+ tpl_field,
+ tpl_file,
+ tpl_form_captcha,
+ tpl_form_checkbox,
+ tpl_form_input,
+ tpl_form_select,
+ tpl_form_textarea,
+ tpl_form_url,
+ tpl_form_username,
+ tpl_image,
+ tpl_select_option,
+ tpl_video
+ ) {
+ "use strict";
+
+ const URL_REGEX = /\b(https?:\/\/|www\.|https?:\/\/www\.)[^\s<>]{2,200}\b\/?/g;
+
+ const logger = _.assign({
+ 'debug': _.get(console, 'log') ? console.log.bind(console) : _.noop,
+ 'error': _.get(console, 'log') ? console.log.bind(console) : _.noop,
+ 'info': _.get(console, 'log') ? console.log.bind(console) : _.noop,
+ 'warn': _.get(console, 'log') ? console.log.bind(console) : _.noop
+ }, console);
+
+ const XFORM_TYPE_MAP = {
+ 'text-private': 'password',
+ 'text-single': 'text',
+ 'fixed': 'label',
+ 'boolean': 'checkbox',
+ 'hidden': 'hidden',
+ 'jid-multi': 'textarea',
+ 'list-single': 'dropdown',
+ 'list-multi': 'dropdown'
+ };
+
+ function slideOutWrapup (el) {
+ /* Wrapup function for slideOut. */
+ el.removeAttribute('data-slider-marker');
+ el.classList.remove('collapsed');
+ el.style.overflow = "";
+ el.style.height = "";
+ }
+
+
+ const isImage = function (url) {
+ return new Promise((resolve, reject) => {
+ var img = new Image();
+ var timer = window.setTimeout(function () {
+ reject(new Error("Could not determine whether it's an image"));
+ img = null;
+ }, 3000);
+ img.onerror = img.onabort = function () {
+ clearTimeout(timer);
+ reject(new Error("Could not determine whether it's an image"));
+ };
+ img.onload = function () {
+ clearTimeout(timer);
+ resolve(img);
+ };
+ img.src = url;
+ });
+ };
+
+
+ u.isAudioURL = function (url) {
+ if (!(url instanceof URI)) {
+ url = new URI(url);
+ }
+ const filename = url.filename().toLowerCase();
+ if (!_.includes(["https", "http"], url.protocol().toLowerCase())) {
+ return false;
+ }
+ return filename.endsWith('.ogg') || filename.endsWith('.mp3') || filename.endsWith('.m4a');
+ }
+
+
+ u.isImageURL = function (url) {
+ if (!(url instanceof URI)) {
+ url = new URI(url);
+ }
+ const filename = url.filename().toLowerCase();
+ if (!_.includes(["https", "http"], url.protocol().toLowerCase())) {
+ return false;
+ }
+ return filename.endsWith('.jpg') || filename.endsWith('.jpeg') ||
+ filename.endsWith('.png') || filename.endsWith('.gif') ||
+ filename.endsWith('.bmp') || filename.endsWith('.tiff') ||
+ filename.endsWith('.svg');
+ };
+
+
+ u.isVideoURL = function (url) {
+ if (!(url instanceof URI)) {
+ url = new URI(url);
+ }
+ const filename = url.filename().toLowerCase();
+ if (!_.includes(["https", "http"], url.protocol().toLowerCase())) {
+ return false;
+ }
+ return filename.endsWith('.mp4') || filename.endsWith('.webm');
+ }
+
+
+ u.renderAudioURL = function (_converse, url) {
+ const uri = new URI(url);
+ if (u.isAudioURL(uri)) {
+ const { __ } = _converse;
+ return tpl_audio({
+ 'url': url,
+ 'label_download': __('Download audio file "%1$s"', decodeURI(uri.filename()))
+ })
+ }
+ return url;
+ };
+
+
+ u.renderFileURL = function (_converse, url) {
+ const uri = new URI(url);
+ if (u.isImageURL(uri) || u.isVideoURL(uri) || u.isAudioURL(uri)) {
+ return url;
+ }
+ const { __ } = _converse,
+ filename = uri.filename();
+ return tpl_file({
+ 'url': url,
+ 'label_download': __('Download file "%1$s"', decodeURI(filename))
+ })
+ };
+
+
+ u.renderImageURL = function (_converse, url) {
+ if (!_converse.show_images_inline) {
+ return u.addHyperlinks(url);
+ }
+ const uri = new URI(url);
+ if (u.isImageURL(uri)) {
+ const { __ } = _converse;
+ return tpl_image({
+ 'url': url,
+ 'label_download': __('Download image "%1$s"', decodeURI(uri.filename()))
+ })
+ }
+ return url;
+ };
+
+
+ u.renderImageURLs = function (_converse, el) {
+ /* Returns a Promise which resolves once all images have been loaded.
+ */
+ if (!_converse.show_images_inline) {
+ return Promise.resolve();
+ }
+ const { __ } = _converse;
+ const list = el.textContent.match(URL_REGEX) || [];
+ return Promise.all(
+ _.map(list, url =>
+ new Promise((resolve, reject) => {
+ if (u.isImageURL(url)) {
+ return isImage(url).then(img => {
+ const i = new Image();
+ i.src = img.src;
+ i.addEventListener('load', resolve);
+ // We also resolve for non-images, otherwise the
+ // Promise.all resolves prematurely.
+ i.addEventListener('error', resolve);
+
+ const { __ } = _converse;
+ _.each(sizzle(`a[href="${url}"]`, el), (a) => {
+ a.outerHTML= tpl_image({
+ 'url': url,
+ 'label_download': __('Download')
+ })
+ });
+ }).catch(resolve)
+ } else {
+ return resolve();
+ }
+ })
+ )
+ )
+ };
+
+
+ u.renderMovieURL = function (_converse, url) {
+ const uri = new URI(url);
+ if (u.isVideoURL(uri)) {
+ const { __ } = _converse;
+ return tpl_video({
+ 'url': url,
+ 'label_download': __('Download video file "%1$s"', decodeURI(uri.filename()))
+ })
+ }
+ return url;
+ };
+
+
+ u.renderNewLines = function (text) {
+ return text.replace(/\n\n+/g, '
').replace(/\n/g, '
');
+ };
+
+ u.calculateElementHeight = function (el) {
+ /* Return the height of the passed in DOM element,
+ * based on the heights of its children.
+ */
+ return _.reduce(
+ el.children,
+ (result, child) => result + child.offsetHeight, 0
+ );
+ }
+
+ u.getNextElement = function (el, selector='*') {
+ let next_el = el.nextElementSibling;
+ while (!_.isNull(next_el) && !sizzle.matchesSelector(next_el, selector)) {
+ next_el = next_el.nextElementSibling;
+ }
+ return next_el;
+ }
+
+ u.getPreviousElement = function (el, selector='*') {
+ let prev_el = el.previousSibling;
+ while (!_.isNull(prev_el) && !sizzle.matchesSelector(prev_el, selector)) {
+ prev_el = prev_el.previousSibling
+ }
+ return prev_el;
+ }
+
+ u.getFirstChildElement = function (el, selector='*') {
+ let first_el = el.firstElementChild;
+ while (!_.isNull(first_el) && !sizzle.matchesSelector(first_el, selector)) {
+ first_el = first_el.nextSibling
+ }
+ return first_el;
+ }
+
+ u.getLastChildElement = function (el, selector='*') {
+ let last_el = el.lastElementChild;
+ while (!_.isNull(last_el) && !sizzle.matchesSelector(last_el, selector)) {
+ last_el = last_el.previousSibling
+ }
+ return last_el;
+ }
+
+ u.hasClass = function (className, el) {
+ return _.includes(el.classList, className);
+ };
+
+ u.addClass = function (className, el) {
+ if (el instanceof Element) {
+ el.classList.add(className);
+ }
+ }
+
+ u.removeClass = function (className, el) {
+ if (el instanceof Element) {
+ el.classList.remove(className);
+ }
+ return el;
+ }
+
+ u.removeElement = function (el) {
+ if (!_.isNil(el) && !_.isNil(el.parentNode)) {
+ el.parentNode.removeChild(el);
+ }
+ }
+
+ u.showElement = _.flow(
+ _.partial(u.removeClass, 'collapsed'),
+ _.partial(u.removeClass, 'hidden')
+ )
+
+ u.hideElement = function (el) {
+ if (!_.isNil(el)) {
+ el.classList.add('hidden');
+ }
+ return el;
+ }
+
+ u.ancestor = function (el, selector) {
+ let parent = el;
+ while (!_.isNil(parent) && !sizzle.matchesSelector(parent, selector)) {
+ parent = parent.parentElement;
+ }
+ return parent;
+ }
+
+ u.nextUntil = function (el, selector, include_self=false) {
+ /* Return the element's siblings until one matches the selector. */
+ const matches = [];
+ let sibling_el = el.nextElementSibling;
+ while (!_.isNil(sibling_el) && !sibling_el.matches(selector)) {
+ matches.push(sibling_el);
+ sibling_el = sibling_el.nextElementSibling;
+ }
+ return matches;
+ }
+
+ u.unescapeHTML = function (string) {
+ /* Helper method that replace HTML-escaped symbols with equivalent characters
+ * (e.g. transform occurrences of '&' to '&')
+ *
+ * Parameters:
+ * (String) string: a String containing the HTML-escaped symbols.
+ */
+ var div = document.createElement('div');
+ div.innerHTML = string;
+ return div.innerText;
+ };
+
+ u.escapeHTML = function (string) {
+ return string
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """);
+ };
+
+
+ u.addMentionsMarkup = function (text, references, chatbox) {
+ if (chatbox.get('message_type') !== 'groupchat') {
+ return text;
+ }
+ const nick = chatbox.get('nick');
+ references
+ .sort((a, b) => b.begin - a.begin)
+ .forEach(ref => {
+ const mention = text.slice(ref.begin, ref.end)
+ chatbox;
+ if (mention === nick) {
+ text = text.slice(0, ref.begin) + `${mention}` + text.slice(ref.end);
+ } else {
+ text = text.slice(0, ref.begin) + `${mention}` + text.slice(ref.end);
+ }
+ });
+ return text;
+ };
+
+
+ u.addHyperlinks = function (text) {
+ return URI.withinString(text, url => {
+ const uri = new URI(url);
+ url = uri.normalize()._string;
+ const pretty_url = uri._parts.urn ? url : uri.readable();
+ if (!uri._parts.protocol && !url.startsWith('http://') && !url.startsWith('https://')) {
+ url = 'http://' + url;
+ }
+ if (uri._parts.protocol === 'xmpp' && uri._parts.query === 'join') {
+ return `${u.escapeHTML(pretty_url)}`;
+ }
+ return `${u.escapeHTML(pretty_url)}`;
+ }, {
+ 'start': /\b(?:([a-z][a-z0-9.+-]*:\/\/)|xmpp:|mailto:|www\.)/gi
+ });
+ };
+
+
+ u.slideInAllElements = function (elements, duration=300) {
+ return Promise.all(
+ _.map(
+ elements,
+ _.partial(u.slideIn, _, duration)
+ ));
+ };
+
+ u.slideToggleElement = function (el, duration) {
+ if (_.includes(el.classList, 'collapsed') ||
+ _.includes(el.classList, 'hidden')) {
+ return u.slideOut(el, duration);
+ } else {
+ return u.slideIn(el, duration);
+ }
+ };
+
+
+ u.slideOut = function (el, duration=200) {
+ /* Shows/expands an element by sliding it out of itself
+ *
+ * Parameters:
+ * (HTMLElement) el - The HTML string
+ * (Number) duration - The duration amount in milliseconds
+ */
+ return new Promise((resolve, reject) => {
+ if (_.isNil(el)) {
+ const err = "Undefined or null element passed into slideOut"
+ logger.warn(err);
+ reject(new Error(err));
+ return;
+ }
+ const marker = el.getAttribute('data-slider-marker');
+ if (marker) {
+ el.removeAttribute('data-slider-marker');
+ window.cancelAnimationFrame(marker);
+ }
+ const end_height = u.calculateElementHeight(el);
+ if (window.converse_disable_effects) { // Effects are disabled (for tests)
+ el.style.height = end_height + 'px';
+ slideOutWrapup(el);
+ resolve();
+ return;
+ }
+ if (!u.hasClass('collapsed', el) && !u.hasClass('hidden', el)) {
+ resolve();
+ return;
+ }
+
+ const steps = duration/17; // We assume 17ms per animation which is ~60FPS
+ let height = 0;
+
+ function draw () {
+ height += end_height/steps;
+ if (height < end_height) {
+ el.style.height = height + 'px';
+ el.setAttribute(
+ 'data-slider-marker',
+ window.requestAnimationFrame(draw)
+ );
+ } else {
+ // We recalculate the height to work around an apparent
+ // browser bug where browsers don't know the correct
+ // offsetHeight beforehand.
+ el.removeAttribute('data-slider-marker');
+ el.style.height = u.calculateElementHeight(el) + 'px';
+ el.style.overflow = "";
+ el.style.height = "";
+ resolve();
+ }
+ }
+ el.style.height = '0';
+ el.style.overflow = 'hidden';
+ el.classList.remove('hidden');
+ el.classList.remove('collapsed');
+ el.setAttribute(
+ 'data-slider-marker',
+ window.requestAnimationFrame(draw)
+ );
+ });
+ };
+
+ u.slideIn = function (el, duration=200) {
+ /* Hides/collapses an element by sliding it into itself. */
+ return new Promise((resolve, reject) => {
+ if (_.isNil(el)) {
+ const err = "Undefined or null element passed into slideIn";
+ logger.warn(err);
+ return reject(new Error(err));
+ } else if (_.includes(el.classList, 'collapsed')) {
+ return resolve(el);
+ } else if (window.converse_disable_effects) { // Effects are disabled (for tests)
+ el.classList.add('collapsed');
+ el.style.height = "";
+ return resolve(el);
+ }
+ const marker = el.getAttribute('data-slider-marker');
+ if (marker) {
+ el.removeAttribute('data-slider-marker');
+ window.cancelAnimationFrame(marker);
+ }
+ const original_height = el.offsetHeight,
+ steps = duration/17; // We assume 17ms per animation which is ~60FPS
+ let height = original_height;
+
+ el.style.overflow = 'hidden';
+
+ function draw () {
+ height -= original_height/steps;
+ if (height > 0) {
+ el.style.height = height + 'px';
+ el.setAttribute(
+ 'data-slider-marker',
+ window.requestAnimationFrame(draw)
+ );
+ } else {
+ el.removeAttribute('data-slider-marker');
+ el.classList.add('collapsed');
+ el.style.height = "";
+ resolve(el);
+ }
+ }
+ el.setAttribute(
+ 'data-slider-marker',
+ window.requestAnimationFrame(draw)
+ );
+ });
+ };
+
+ function afterAnimationEnds (el, callback) {
+ el.classList.remove('visible');
+ if (_.isFunction(callback)) {
+ callback();
+ }
+ }
+
+ u.fadeIn = function (el, callback) {
+ if (_.isNil(el)) {
+ logger.warn("Undefined or null element passed into fadeIn");
+ }
+ if (window.converse_disable_effects) {
+ el.classList.remove('hidden');
+ return afterAnimationEnds(el, callback);
+ }
+ if (_.includes(el.classList, 'hidden')) {
+ el.classList.add('visible');
+ el.classList.remove('hidden');
+ el.addEventListener("webkitAnimationEnd", _.partial(afterAnimationEnds, el, callback));
+ el.addEventListener("animationend", _.partial(afterAnimationEnds, el, callback));
+ el.addEventListener("oanimationend", _.partial(afterAnimationEnds, el, callback));
+ } else {
+ afterAnimationEnds(el, callback);
+ }
+ };
+
+
+ u.xForm2webForm = function (field, stanza, domain) {
+ /* Takes a field in XMPP XForm (XEP-004: Data Forms) format
+ * and turns it into an HTML field.
+ *
+ * Returns either text or a DOM element (which is not ideal, but fine
+ * for now).
+ *
+ * Parameters:
+ * (XMLElement) field - the field to convert
+ */
+ if (field.getAttribute('type')) {
+ if (field.getAttribute('type') === 'list-single' ||
+ field.getAttribute('type') === 'list-multi') {
+
+ const values = _.map(
+ u.queryChildren(field, 'value'),
+ _.partial(_.get, _, 'textContent')
+ );
+ const options = _.map(
+ u.queryChildren(field, 'option'),
+ function (option) {
+ const value = _.get(option.querySelector('value'), 'textContent');
+ return tpl_select_option({
+ 'value': value,
+ 'label': option.getAttribute('label'),
+ 'selected': _.includes(values, value),
+ 'required': !_.isNil(field.querySelector('required'))
+ })
+ }
+ );
+ return tpl_form_select({
+ 'id': u.getUniqueId(),
+ 'name': field.getAttribute('var'),
+ 'label': field.getAttribute('label'),
+ 'options': options.join(''),
+ 'multiple': (field.getAttribute('type') === 'list-multi'),
+ 'required': !_.isNil(field.querySelector('required'))
+ });
+ } else if (field.getAttribute('type') === 'fixed') {
+ const text = _.get(field.querySelector('value'), 'textContent');
+ return ''+text+'
';
+ } else if (field.getAttribute('type') === 'jid-multi') {
+ return tpl_form_textarea({
+ 'name': field.getAttribute('var'),
+ 'label': field.getAttribute('label') || '',
+ 'value': _.get(field.querySelector('value'), 'textContent'),
+ 'required': !_.isNil(field.querySelector('required'))
+ });
+ } else if (field.getAttribute('type') === 'boolean') {
+ return tpl_form_checkbox({
+ 'id': u.getUniqueId(),
+ 'name': field.getAttribute('var'),
+ 'label': field.getAttribute('label') || '',
+ 'checked': _.get(field.querySelector('value'), 'textContent') === "1" && 'checked="1"' || '',
+ 'required': !_.isNil(field.querySelector('required'))
+ });
+ } else if (field.getAttribute('var') === 'url') {
+ return tpl_form_url({
+ 'label': field.getAttribute('label') || '',
+ 'value': _.get(field.querySelector('value'), 'textContent')
+ });
+ } else if (field.getAttribute('var') === 'username') {
+ return tpl_form_username({
+ 'domain': ' @'+domain,
+ 'name': field.getAttribute('var'),
+ 'type': XFORM_TYPE_MAP[field.getAttribute('type')],
+ 'label': field.getAttribute('label') || '',
+ 'value': _.get(field.querySelector('value'), 'textContent'),
+ 'required': !_.isNil(field.querySelector('required'))
+ });
+ } else {
+ return tpl_form_input({
+ 'id': u.getUniqueId(),
+ 'label': field.getAttribute('label') || '',
+ 'name': field.getAttribute('var'),
+ 'placeholder': null,
+ 'required': !_.isNil(field.querySelector('required')),
+ 'type': XFORM_TYPE_MAP[field.getAttribute('type')],
+ 'value': _.get(field.querySelector('value'), 'textContent')
+ });
+ }
+ } else {
+ if (field.getAttribute('var') === 'ocr') { // Captcha
+ const uri = field.querySelector('uri');
+ const el = sizzle('data[cid="'+uri.textContent.replace(/^cid:/, '')+'"]', stanza)[0];
+ return tpl_form_captcha({
+ 'label': field.getAttribute('label'),
+ 'name': field.getAttribute('var'),
+ 'data': _.get(el, 'textContent'),
+ 'type': uri.getAttribute('type'),
+ 'required': !_.isNil(field.querySelector('required'))
+ });
+ }
+ }
+ }
+ return u;
+}));