Move unnecessary templates in headless package to main package

This commit is contained in:
JC Brand 2018-10-22 13:59:06 +02:00
parent 18024b8bd2
commit 7590a030b0
17 changed files with 652 additions and 619 deletions

View File

@ -6,6 +6,7 @@
(function (root, factory) { (function (root, factory) {
define([ define([
"./utils/html",
"utils/emoji", "utils/emoji",
"@converse/headless/converse-core", "@converse/headless/converse-core",
"xss", "xss",
@ -17,6 +18,7 @@
"templates/message_versions_modal.html", "templates/message_versions_modal.html",
], factory); ], factory);
}(this, function ( }(this, function (
html,
u, u,
converse, converse,
xss, xss,

View File

@ -12,12 +12,12 @@
(function (root, factory) { (function (root, factory) {
define(["utils/form", define(["utils/form",
"@converse/headless/converse-core", "@converse/headless/converse-core",
"@converse/headless/templates/form_username.html", "templates/form_username.html",
"templates/register_link.html", "templates/register_link.html",
"templates/register_panel.html", "templates/register_panel.html",
"templates/registration_form.html", "templates/registration_form.html",
"templates/registration_request.html", "templates/registration_request.html",
"@converse/headless/templates/form_input.html", "templates/form_input.html",
"templates/spinner.html", "templates/spinner.html",
"converse-controlbox" "converse-controlbox"
], factory); ], factory);

View File

@ -12,15 +12,9 @@
define([ define([
"sizzle", "sizzle",
"es6-promise/dist/es6-promise.auto", "es6-promise/dist/es6-promise.auto",
"fast-text-encoding/text",
"../lodash.noconflict", "../lodash.noconflict",
"backbone", "backbone",
"strophe.js", "strophe.js",
"urijs",
"../templates/audio.html",
"../templates/file.html",
"../templates/image.html",
"../templates/video.html"
], factory); ], factory);
} else { } else {
// Used by the mockups // Used by the mockups
@ -49,57 +43,14 @@
}(this, function ( }(this, function (
sizzle, sizzle,
Promise, Promise,
FastTextEncoding,
_, _,
Backbone, Backbone,
Strophe, Strophe
URI,
tpl_audio,
tpl_file,
tpl_image,
tpl_video
) { ) {
"use strict"; "use strict";
Strophe = Strophe.Strophe; Strophe = Strophe.Strophe;
const URL_REGEX = /\b(https?:\/\/|www\.|https?:\/\/www\.)[^\s<>]{2,200}\b\/?/g; const u = {};
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 = {};
u.getLongestSubstring = function (string, candidates) { u.getLongestSubstring = function (string, candidates) {
function reducer (accumulator, current_value) { function reducer (accumulator, current_value) {
@ -116,118 +67,6 @@
return candidates.reduce(reducer, ''); 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 '&amp;' 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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
};
u.prefixMentions = function (message) { u.prefixMentions = function (message) {
/* Given a message object, return its text with @ chars /* Given a message object, return its text with @ chars
* inserted before the mentioned nicknames. * inserted before the mentioned nicknames.
@ -241,328 +80,6 @@
return text; 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) + `<span class="mention mention--self badge badge-info">${mention}</span>` + text.slice(ref.end);
} else {
text = text.slice(0, ref.begin) + `<span class="mention">${mention}</span>` + 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 `<a target="_blank" rel="noopener" class="open-chatroom" href="${url}">${u.escapeHTML(pretty_url)}</a>`;
}
return `<a target="_blank" rel="noopener" href="${url}">${u.escapeHTML(pretty_url)}</a>`;
}, {
'start': /\b(?:([a-z][a-z0-9.+-]*:\/\/)|xmpp:|mailto:|www\.)/gi
});
};
u.renderNewLines = function (text) {
return text.replace(/\n\n+/g, '<br/><br/>').replace(/\n/g, '<br/>');
};
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) { u.isValidJID = function (jid) {
return _.compact(jid.split('@')).length === 2 && !jid.startsWith('@') && !jid.endsWith('@'); return _.compact(jid.split('@')).length === 2 && !jid.startsWith('@') && !jid.endsWith('@');
}; };

View File

@ -6,49 +6,16 @@
// Copyright (c) 2013-2018, Jan-Carel Brand <jc@opkode.com> // Copyright (c) 2013-2018, Jan-Carel Brand <jc@opkode.com>
// Licensed under the Mozilla Public License (MPLv2) // Licensed under the Mozilla Public License (MPLv2)
// //
/*global define, escape, Jed */ /*global define */
(function (root, factory) { (function (root, factory) {
define([ define([
"sizzle",
"../lodash.noconflict", "../lodash.noconflict",
"./core", "./core",
"../templates/field.html", "../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",
], factory); ], factory);
}(this, function ( }(this, function (_, u, tpl_field) {
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
) {
"use strict"; "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) { u.webForm2xForm = function (field) {
/* Takes an HTML DOM and turns it into an XForm 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 '<p class="form-help">'+text+'</p>';
} 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; return u;
})); }));

643
src/utils/html.js Normal file
View File

@ -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 <jc@opkode.com>
// 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, '<br/><br/>').replace(/\n/g, '<br/>');
};
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 '&amp;' 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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
};
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) + `<span class="mention mention--self badge badge-info">${mention}</span>` + text.slice(ref.end);
} else {
text = text.slice(0, ref.begin) + `<span class="mention">${mention}</span>` + 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 `<a target="_blank" rel="noopener" class="open-chatroom" href="${url}">${u.escapeHTML(pretty_url)}</a>`;
}
return `<a target="_blank" rel="noopener" href="${url}">${u.escapeHTML(pretty_url)}</a>`;
}, {
'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 '<p class="form-help">'+text+'</p>';
} 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;
}));