2017-08-15 21:23:30 +02:00
|
|
|
// Converse.js (A browser based XMPP chat client)
|
|
|
|
// http://conversejs.org
|
|
|
|
//
|
|
|
|
// This is the utilities module.
|
|
|
|
//
|
|
|
|
// Copyright (c) 2012-2017, Jan-Carel Brand <jc@opkode.com>
|
|
|
|
// Licensed under the Mozilla Public License (MPLv2)
|
|
|
|
//
|
2017-09-26 18:27:41 +02:00
|
|
|
/*global define, escape, window */
|
2015-01-16 22:07:27 +01:00
|
|
|
(function (root, factory) {
|
2018-04-26 19:10:06 +02:00
|
|
|
if (typeof define === 'function' && define.amd) {
|
|
|
|
define([
|
|
|
|
"sizzle",
|
|
|
|
"es6-promise",
|
|
|
|
"lodash.noconflict",
|
2018-05-07 12:57:05 +02:00
|
|
|
"backbone",
|
2018-04-26 19:10:06 +02:00
|
|
|
"strophe",
|
2018-04-27 13:47:44 +02:00
|
|
|
"uri",
|
2018-05-24 21:09:33 +02:00
|
|
|
"templates/audio.html",
|
|
|
|
"templates/file.html",
|
|
|
|
"templates/image.html",
|
|
|
|
"templates/video.html"
|
2018-04-26 19:10:06 +02:00
|
|
|
], factory);
|
|
|
|
} else {
|
|
|
|
// Used by the mockups
|
|
|
|
const Strophe = {
|
|
|
|
'Strophe': root.Strophe,
|
|
|
|
'$build': root.$build,
|
|
|
|
'$iq': root.$iq,
|
|
|
|
'$msg': root.$msg,
|
|
|
|
'$pres': root.$pres,
|
|
|
|
'SHA1': root.SHA1,
|
|
|
|
'MD5': root.MD5,
|
|
|
|
'b64_hmac_sha1': root.b64_hmac_sha1,
|
|
|
|
'b64_sha1': root.b64_sha1,
|
|
|
|
'str_hmac_sha1': root.str_hmac_sha1,
|
|
|
|
'str_sha1': root.str_sha1
|
|
|
|
};
|
|
|
|
root.converse_utils = factory(
|
|
|
|
root.sizzle,
|
|
|
|
root.Promise,
|
|
|
|
root._,
|
2018-05-08 17:48:37 +02:00
|
|
|
root.Backbone,
|
2018-04-26 19:10:06 +02:00
|
|
|
Strophe
|
|
|
|
);
|
|
|
|
}
|
2016-09-23 10:54:55 +02:00
|
|
|
}(this, function (
|
2017-08-15 16:36:43 +02:00
|
|
|
sizzle,
|
2017-07-10 21:14:48 +02:00
|
|
|
Promise,
|
2017-08-15 16:36:43 +02:00
|
|
|
_,
|
2018-05-07 12:57:05 +02:00
|
|
|
Backbone,
|
2018-04-18 16:55:26 +02:00
|
|
|
Strophe,
|
2018-04-27 13:47:44 +02:00
|
|
|
URI,
|
2018-04-18 16:55:26 +02:00
|
|
|
tpl_audio,
|
|
|
|
tpl_file,
|
|
|
|
tpl_image,
|
|
|
|
tpl_video
|
2016-09-23 10:54:55 +02:00
|
|
|
) {
|
2014-11-16 12:47:30 +01:00
|
|
|
"use strict";
|
2017-06-14 18:41:45 +02:00
|
|
|
Strophe = Strophe.Strophe;
|
2014-11-16 12:47:30 +01:00
|
|
|
|
2018-01-29 15:35:36 +01:00
|
|
|
const URL_REGEX = /\b(https?:\/\/|www\.|https?:\/\/www\.)[^\s<>]{2,200}\b\/?/g;
|
2017-07-16 01:24:33 +02:00
|
|
|
|
2017-08-21 12:52:18 +02:00
|
|
|
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
|
2017-08-21 11:27:47 +02:00
|
|
|
}, console);
|
|
|
|
|
2016-05-28 13:13:49 +02:00
|
|
|
var isImage = function (url) {
|
2017-07-11 17:21:13 +02:00
|
|
|
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;
|
|
|
|
});
|
2016-05-28 13:13:49 +02:00
|
|
|
};
|
|
|
|
|
2017-08-23 11:54:19 +02:00
|
|
|
function slideOutWrapup (el) {
|
|
|
|
/* Wrapup function for slideOut. */
|
|
|
|
el.removeAttribute('data-slider-marker');
|
|
|
|
el.classList.remove('collapsed');
|
|
|
|
el.style.overflow = "";
|
|
|
|
el.style.height = "";
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
var u = {};
|
2017-07-15 11:03:22 +02:00
|
|
|
|
2018-08-15 17:22:24 +02:00
|
|
|
u.getLongestSubstring = function (string, candidates) {
|
|
|
|
function reducer (accumulator, current_value) {
|
|
|
|
if (string.startsWith(current_value)) {
|
|
|
|
if (current_value.length > accumulator.length) {
|
|
|
|
return current_value;
|
|
|
|
} else {
|
|
|
|
return accumulator;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return accumulator;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return candidates.reduce(reducer, '');
|
|
|
|
}
|
|
|
|
|
2018-01-16 14:55:25 +01:00
|
|
|
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
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-01-03 17:49:08 +01:00
|
|
|
u.addClass = function (className, el) {
|
2018-01-03 14:58:05 +01:00
|
|
|
if (el instanceof Element) {
|
|
|
|
el.classList.add(className);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-03 17:49:08 +01:00
|
|
|
u.removeClass = function (className, el) {
|
2018-01-03 14:58:05 +01:00
|
|
|
if (el instanceof Element) {
|
2018-01-03 17:49:08 +01:00
|
|
|
el.classList.remove(className);
|
2017-12-25 12:58:30 +01:00
|
|
|
}
|
|
|
|
return el;
|
|
|
|
}
|
|
|
|
|
2017-12-06 14:59:01 +01:00
|
|
|
u.removeElement = function (el) {
|
|
|
|
if (!_.isNil(el) && !_.isNil(el.parentNode)) {
|
|
|
|
el.parentNode.removeChild(el);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-25 12:58:30 +01:00
|
|
|
u.showElement = _.flow(
|
|
|
|
_.partial(u.removeClass, 'collapsed'),
|
|
|
|
_.partial(u.removeClass, 'hidden')
|
|
|
|
)
|
2017-12-17 15:57:07 +01:00
|
|
|
|
2017-12-06 14:59:01 +01:00
|
|
|
u.hideElement = function (el) {
|
|
|
|
if (!_.isNil(el)) {
|
|
|
|
el.classList.add('hidden');
|
|
|
|
}
|
2017-12-25 12:58:30 +01:00
|
|
|
return el;
|
2017-12-06 14:59:01 +01:00
|
|
|
}
|
|
|
|
|
2018-02-19 20:54:58 +01:00
|
|
|
u.ancestor = function (el, selector) {
|
|
|
|
let parent = el;
|
|
|
|
while (!_.isNil(parent) && !sizzle.matchesSelector(parent, selector)) {
|
|
|
|
parent = parent.parentElement;
|
|
|
|
}
|
|
|
|
return parent;
|
|
|
|
}
|
|
|
|
|
2017-12-17 15:57:07 +01:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2018-04-27 17:59:41 +02:00
|
|
|
u.unescapeHTML = function (htmlEscapedText) {
|
|
|
|
/* Helper method that replace HTML-escaped symbols with equivalent characters
|
|
|
|
* (e.g. transform occurrences of '&' to '&')
|
|
|
|
*
|
|
|
|
* Parameters:
|
|
|
|
* (String) htmlEscapedText: a String containing the HTML-escaped symbols.
|
|
|
|
*/
|
|
|
|
var div = document.createElement('div');
|
|
|
|
div.innerHTML = htmlEscapedText;
|
|
|
|
return div.innerText;
|
|
|
|
};
|
|
|
|
|
2018-04-27 16:58:40 +02:00
|
|
|
u.escapeHTML = function (string) {
|
|
|
|
return string
|
|
|
|
.replace(/&/g, "&")
|
|
|
|
.replace(/</g, "<")
|
|
|
|
.replace(/>/g, ">")
|
|
|
|
.replace(/"/g, """);
|
|
|
|
};
|
|
|
|
|
2018-07-20 15:54:59 +02:00
|
|
|
u.escapeURL = function (url) {
|
|
|
|
return encodeURI(decodeURI(url)).replace(/[!'()]/g, escape).replace(/\*/g, "%2A");
|
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.addHyperlinks = function (text) {
|
2018-04-27 16:58:40 +02:00
|
|
|
return URI.withinString(text, function (url) {
|
|
|
|
var uri = new URI(url);
|
|
|
|
uri.normalize();
|
2018-04-27 17:59:41 +02:00
|
|
|
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
|
|
url = 'http://' + url;
|
|
|
|
}
|
2018-07-20 15:54:59 +02:00
|
|
|
url = u.escapeHTML(u.escapeURL(url));
|
|
|
|
return `<a target="_blank" rel="noopener" href="${url}">${u.escapeHTML(uri.readable())}</a>`;
|
2017-07-15 17:33:25 +02:00
|
|
|
});
|
2017-07-15 20:13:44 +02:00
|
|
|
};
|
|
|
|
|
2018-06-30 15:07:30 +02:00
|
|
|
u.renderNewLines = function (text) {
|
2018-06-30 16:15:27 +02:00
|
|
|
return text.replace(/\n\n+/g, '<br><br>').replace(/\n/g, '<br/>');
|
2018-06-30 15:07:30 +02:00
|
|
|
};
|
|
|
|
|
2018-04-27 12:06:56 +02:00
|
|
|
u.renderImageURLs = function (_converse, obj) {
|
2018-01-17 19:03:47 +01:00
|
|
|
/* Returns a Promise which resolves once all images have been loaded.
|
|
|
|
*/
|
2018-04-27 12:06:56 +02:00
|
|
|
const { __ } = _converse;
|
2017-07-16 01:24:33 +02:00
|
|
|
const list = obj.textContent.match(URL_REGEX) || [];
|
2018-01-17 19:03:47 +01:00
|
|
|
return Promise.all(
|
|
|
|
_.map(list, (url) =>
|
|
|
|
new Promise((resolve, reject) =>
|
|
|
|
isImage(url).then(function (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);
|
2018-04-27 12:06:56 +02:00
|
|
|
|
|
|
|
_.each(sizzle(`a[href="${url}"]`, obj), (a) => {
|
2018-04-27 17:59:41 +02:00
|
|
|
a.outerHTML= tpl_image({
|
2018-04-27 12:06:56 +02:00
|
|
|
'url': url,
|
2018-04-27 17:59:41 +02:00
|
|
|
'label_download': __('Download')
|
2018-04-27 12:06:56 +02:00
|
|
|
})
|
2018-01-17 19:03:47 +01:00
|
|
|
});
|
|
|
|
}).catch(resolve)
|
|
|
|
)
|
2018-04-27 12:06:56 +02:00
|
|
|
)
|
|
|
|
)
|
2017-07-15 17:33:25 +02:00
|
|
|
};
|
|
|
|
|
2018-04-18 16:55:26 +02:00
|
|
|
u.renderFileURL = function (_converse, url) {
|
2018-07-20 15:54:59 +02:00
|
|
|
const uri = new URI(url),
|
|
|
|
{ __ } = _converse,
|
2018-04-27 13:47:44 +02:00
|
|
|
filename = uri.filename();
|
|
|
|
if (!_.includes(["https", "http"], uri.protocol()) ||
|
|
|
|
filename.endsWith('mp3') || filename.endsWith('mp4') ||
|
|
|
|
filename.endsWith('jpg') || filename.endsWith('jpeg') ||
|
|
|
|
filename.endsWith('png') || filename.endsWith('gif') ||
|
|
|
|
filename.endsWith('svg')) {
|
2018-04-18 16:55:26 +02:00
|
|
|
|
|
|
|
return url;
|
2018-04-17 15:17:39 +02:00
|
|
|
}
|
2018-04-18 16:55:26 +02:00
|
|
|
return tpl_file({
|
|
|
|
'url': url,
|
2018-05-06 12:05:21 +02:00
|
|
|
'label_download': __('Download "%1$s"', filename)
|
2018-04-18 16:55:26 +02:00
|
|
|
})
|
2018-04-03 11:20:57 +02:00
|
|
|
};
|
|
|
|
|
2018-04-18 16:55:26 +02:00
|
|
|
u.renderImageURL = function (_converse, url) {
|
|
|
|
const { __ } = _converse;
|
|
|
|
if (url.endsWith('jpg') || url.endsWith('jpeg') || url.endsWith('png') ||
|
|
|
|
url.endsWith('gif') || url.endsWith('svg')) {
|
|
|
|
|
|
|
|
return tpl_image({
|
|
|
|
'url': url,
|
2018-04-27 17:59:41 +02:00
|
|
|
'label_download': __('Download')
|
2018-04-18 16:55:26 +02:00
|
|
|
})
|
2018-04-17 15:17:39 +02:00
|
|
|
}
|
2018-04-18 16:55:26 +02:00
|
|
|
return url;
|
|
|
|
};
|
|
|
|
|
|
|
|
u.renderMovieURL = function (_converse, url) {
|
|
|
|
const { __ } = _converse;
|
|
|
|
if (url.endsWith('mp4')) {
|
|
|
|
return tpl_video({
|
|
|
|
'url': url,
|
|
|
|
'label_download': __('Download video file')
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return url;
|
|
|
|
};
|
|
|
|
|
|
|
|
u.renderAudioURL = function (_converse, url) {
|
|
|
|
const { __ } = _converse;
|
|
|
|
if (url.endsWith('mp3')) {
|
|
|
|
return tpl_audio({
|
|
|
|
'url': url,
|
|
|
|
'label_download': __('Download audio file')
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return url;
|
2018-04-03 11:20:57 +02:00
|
|
|
};
|
|
|
|
|
2018-01-03 20:02:05 +01:00
|
|
|
u.slideInAllElements = function (elements, duration=300) {
|
2017-07-15 11:03:22 +02:00
|
|
|
return Promise.all(
|
|
|
|
_.map(
|
|
|
|
elements,
|
2017-12-17 15:57:07 +01:00
|
|
|
_.partial(u.slideIn, _, duration)
|
2017-07-15 11:03:22 +02:00
|
|
|
));
|
|
|
|
};
|
2017-06-16 11:31:57 +02:00
|
|
|
|
2018-01-03 14:58:05 +01:00
|
|
|
u.slideToggleElement = function (el, duration) {
|
|
|
|
if (_.includes(el.classList, 'collapsed') ||
|
|
|
|
_.includes(el.classList, 'hidden')) {
|
|
|
|
return u.slideOut(el, duration);
|
2017-07-15 11:03:22 +02:00
|
|
|
} else {
|
2018-01-03 14:58:05 +01:00
|
|
|
return u.slideIn(el, duration);
|
2017-07-15 11:03:22 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2018-01-03 20:02:05 +01:00
|
|
|
u.hasClass = function (className, el) {
|
2017-12-17 15:57:07 +01:00
|
|
|
return _.includes(el.classList, className);
|
|
|
|
};
|
|
|
|
|
2017-12-22 21:49:10 +01:00
|
|
|
u.slideOut = function (el, duration=200) {
|
2017-08-23 11:54:19 +02:00
|
|
|
/* Shows/expands an element by sliding it out of itself
|
|
|
|
*
|
|
|
|
* Parameters:
|
|
|
|
* (HTMLElement) el - The HTML string
|
|
|
|
* (Number) duration - The duration amount in milliseconds
|
|
|
|
*/
|
2017-07-15 11:03:22 +02:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
if (_.isNil(el)) {
|
|
|
|
const err = "Undefined or null element passed into slideOut"
|
2017-08-21 11:27:47 +02:00
|
|
|
logger.warn(err);
|
2017-07-15 11:03:22 +02:00
|
|
|
reject(new Error(err));
|
2017-07-15 15:15:37 +02:00
|
|
|
return;
|
2017-06-16 11:31:57 +02:00
|
|
|
}
|
2017-12-19 14:16:19 +01:00
|
|
|
const marker = el.getAttribute('data-slider-marker');
|
|
|
|
if (marker) {
|
2017-07-15 15:15:37 +02:00
|
|
|
el.removeAttribute('data-slider-marker');
|
2017-12-19 14:16:19 +01:00
|
|
|
window.cancelAnimationFrame(marker);
|
2017-07-15 11:03:22 +02:00
|
|
|
}
|
2018-01-16 14:55:25 +01:00
|
|
|
const end_height = u.calculateElementHeight(el);
|
2017-08-15 21:23:30 +02:00
|
|
|
if (window.converse_disable_effects) { // Effects are disabled (for tests)
|
2017-07-15 15:15:37 +02:00
|
|
|
el.style.height = end_height + 'px';
|
2017-08-23 11:54:19 +02:00
|
|
|
slideOutWrapup(el);
|
2017-07-15 15:15:37 +02:00
|
|
|
resolve();
|
|
|
|
return;
|
|
|
|
}
|
2018-01-03 20:02:05 +01:00
|
|
|
if (!u.hasClass('collapsed', el) && !u.hasClass('hidden', el)) {
|
2017-12-17 15:57:07 +01:00
|
|
|
resolve();
|
|
|
|
return;
|
|
|
|
}
|
2017-07-15 15:15:37 +02:00
|
|
|
|
2017-12-19 14:16:19 +01:00
|
|
|
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)
|
|
|
|
);
|
2017-07-15 11:03:22 +02:00
|
|
|
} else {
|
2017-07-17 18:07:35 +02:00
|
|
|
// We recalculate the height to work around an apparent
|
|
|
|
// browser bug where browsers don't know the correct
|
|
|
|
// offsetHeight beforehand.
|
2017-12-19 14:16:19 +01:00
|
|
|
el.removeAttribute('data-slider-marker');
|
2018-01-16 14:55:25 +01:00
|
|
|
el.style.height = u.calculateElementHeight(el) + 'px';
|
2017-12-17 15:57:07 +01:00
|
|
|
el.style.overflow = "";
|
|
|
|
el.style.height = "";
|
2017-07-15 11:03:22 +02:00
|
|
|
resolve();
|
2017-07-14 10:01:00 +02:00
|
|
|
}
|
2017-12-19 14:16:19 +01:00
|
|
|
}
|
2017-12-17 15:57:07 +01:00
|
|
|
el.style.height = '0';
|
|
|
|
el.style.overflow = 'hidden';
|
|
|
|
el.classList.remove('hidden');
|
|
|
|
el.classList.remove('collapsed');
|
2017-12-19 14:16:19 +01:00
|
|
|
el.setAttribute(
|
|
|
|
'data-slider-marker',
|
|
|
|
window.requestAnimationFrame(draw)
|
|
|
|
);
|
2017-07-15 11:03:22 +02:00
|
|
|
});
|
|
|
|
};
|
2017-07-14 10:01:00 +02:00
|
|
|
|
2017-12-22 21:49:10 +01:00
|
|
|
u.slideIn = function (el, duration=200) {
|
2017-07-15 11:03:22 +02:00
|
|
|
/* Hides/collapses an element by sliding it into itself. */
|
|
|
|
return new Promise((resolve, reject) => {
|
2017-07-14 10:01:00 +02:00
|
|
|
if (_.isNil(el)) {
|
2017-07-15 11:03:22 +02:00
|
|
|
const err = "Undefined or null element passed into slideIn";
|
2017-08-21 11:27:47 +02:00
|
|
|
logger.warn(err);
|
2017-07-15 15:15:37 +02:00
|
|
|
return reject(new Error(err));
|
2017-07-17 21:53:33 +02:00
|
|
|
} else if (_.includes(el.classList, 'collapsed')) {
|
2017-12-23 21:28:46 +01:00
|
|
|
return resolve(el);
|
2017-08-15 21:23:30 +02:00
|
|
|
} else if (window.converse_disable_effects) { // Effects are disabled (for tests)
|
2017-07-19 09:10:17 +02:00
|
|
|
el.classList.add('collapsed');
|
|
|
|
el.style.height = "";
|
2017-12-23 21:28:46 +01:00
|
|
|
return resolve(el);
|
2016-11-22 17:42:58 +01:00
|
|
|
}
|
2017-12-19 14:16:19 +01:00
|
|
|
const marker = el.getAttribute('data-slider-marker');
|
|
|
|
if (marker) {
|
2017-07-15 15:15:37 +02:00
|
|
|
el.removeAttribute('data-slider-marker');
|
2017-12-19 14:16:19 +01:00
|
|
|
window.cancelAnimationFrame(marker);
|
2017-02-02 16:07:00 +01:00
|
|
|
}
|
2017-12-19 14:16:19 +01:00
|
|
|
const original_height = el.offsetHeight,
|
|
|
|
steps = duration/17; // We assume 17ms per animation which is ~60FPS
|
|
|
|
let height = original_height;
|
2017-07-15 11:03:22 +02:00
|
|
|
|
|
|
|
el.style.overflow = 'hidden';
|
|
|
|
|
2017-12-19 14:16:19 +01:00
|
|
|
function draw () {
|
|
|
|
height -= original_height/steps;
|
|
|
|
if (height > 0) {
|
|
|
|
el.style.height = height + 'px';
|
|
|
|
el.setAttribute(
|
|
|
|
'data-slider-marker',
|
|
|
|
window.requestAnimationFrame(draw)
|
|
|
|
);
|
2017-07-15 11:03:22 +02:00
|
|
|
} else {
|
2017-07-19 09:10:17 +02:00
|
|
|
el.removeAttribute('data-slider-marker');
|
2017-07-17 21:53:33 +02:00
|
|
|
el.classList.add('collapsed');
|
|
|
|
el.style.height = "";
|
2017-12-23 21:28:46 +01:00
|
|
|
resolve(el);
|
2017-07-15 11:03:22 +02:00
|
|
|
}
|
2017-12-19 14:16:19 +01:00
|
|
|
}
|
|
|
|
el.setAttribute(
|
|
|
|
'data-slider-marker',
|
|
|
|
window.requestAnimationFrame(draw)
|
|
|
|
);
|
2017-07-15 11:03:22 +02:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2017-12-19 21:26:09 +01:00
|
|
|
function afterAnimationEnds (el, callback) {
|
|
|
|
el.classList.remove('visible');
|
|
|
|
if (_.isFunction(callback)) {
|
|
|
|
callback();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.fadeIn = function (el, callback) {
|
2017-07-15 11:03:22 +02:00
|
|
|
if (_.isNil(el)) {
|
2017-08-21 11:27:47 +02:00
|
|
|
logger.warn("Undefined or null element passed into fadeIn");
|
2017-07-15 11:03:22 +02:00
|
|
|
}
|
2017-12-19 21:26:09 +01:00
|
|
|
if (window.converse_disable_effects) {
|
2017-07-15 11:03:22 +02:00
|
|
|
el.classList.remove('hidden');
|
2017-12-19 21:26:09 +01:00
|
|
|
return afterAnimationEnds(el, callback);
|
2017-07-15 11:03:22 +02:00
|
|
|
}
|
|
|
|
if (_.includes(el.classList, 'hidden')) {
|
|
|
|
el.classList.add('visible');
|
|
|
|
el.classList.remove('hidden');
|
2017-12-19 21:26:09 +01:00
|
|
|
el.addEventListener("webkitAnimationEnd", _.partial(afterAnimationEnds, el, callback));
|
|
|
|
el.addEventListener("animationend", _.partial(afterAnimationEnds, el, callback));
|
|
|
|
el.addEventListener("oanimationend", _.partial(afterAnimationEnds, el, callback));
|
2017-07-15 11:03:22 +02:00
|
|
|
} else {
|
2017-12-19 21:26:09 +01:00
|
|
|
afterAnimationEnds(el, callback);
|
2017-07-15 11:03:22 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-09-29 00:07:16 +02:00
|
|
|
u.isValidJID = function (jid) {
|
2018-02-07 13:29:54 +01:00
|
|
|
return _.compact(jid.split('@')).length === 2 && !jid.startsWith('@') && !jid.endsWith('@');
|
2017-09-29 00:07:16 +02:00
|
|
|
};
|
|
|
|
|
2018-01-29 14:51:49 +01:00
|
|
|
u.isValidMUCJID = function (jid) {
|
|
|
|
return !jid.startsWith('@') && !jid.endsWith('@');
|
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.isSameBareJID = function (jid1, jid2) {
|
2017-07-15 11:03:22 +02:00
|
|
|
return Strophe.getBareJidFromJid(jid1).toLowerCase() ===
|
|
|
|
Strophe.getBareJidFromJid(jid2).toLowerCase();
|
|
|
|
};
|
|
|
|
|
2017-12-02 14:12:17 +01:00
|
|
|
u.getMostRecentMessage = function (model) {
|
|
|
|
const messages = model.messages.filter('message');
|
|
|
|
return messages[messages.length-1];
|
|
|
|
}
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.isNewMessage = function (message) {
|
2017-07-15 11:03:22 +02:00
|
|
|
/* Given a stanza, determine whether it's a new
|
|
|
|
* message, i.e. not a MAM archived one.
|
|
|
|
*/
|
|
|
|
if (message instanceof Element) {
|
2018-06-07 12:45:53 +02:00
|
|
|
return !(
|
|
|
|
sizzle(`result[xmlns="${Strophe.NS.MAM}"]`, message).length &&
|
|
|
|
sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, message).length
|
|
|
|
);
|
2017-07-15 11:03:22 +02:00
|
|
|
} else {
|
2018-06-07 12:45:53 +02:00
|
|
|
return !(message.get('is_delayed') && message.get('is_archived'));
|
2017-07-15 11:03:22 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2018-05-07 12:57:05 +02:00
|
|
|
u.isOnlyChatStateNotification = function (attrs) {
|
|
|
|
if (attrs instanceof Backbone.Model) {
|
|
|
|
attrs = attrs.attributes;
|
|
|
|
}
|
|
|
|
return attrs['chat_state'] &&
|
|
|
|
!attrs['oob_url'] &&
|
|
|
|
!attrs['file'] &&
|
|
|
|
!attrs['message'];
|
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.isOTRMessage = function (message) {
|
2017-07-15 11:03:22 +02:00
|
|
|
var body = message.querySelector('body'),
|
|
|
|
text = (!_.isNull(body) ? body.textContent: undefined);
|
|
|
|
return text && !!text.match(/^\?OTR/);
|
|
|
|
};
|
|
|
|
|
2018-01-29 15:00:11 +01:00
|
|
|
u.isHeadlineMessage = function (_converse, message) {
|
2017-07-15 11:03:22 +02:00
|
|
|
var from_jid = message.getAttribute('from');
|
|
|
|
if (message.getAttribute('type') === 'headline') {
|
|
|
|
return true;
|
|
|
|
}
|
2018-01-29 15:00:11 +01:00
|
|
|
const chatbox = _converse.chatboxes.get(Strophe.getBareJidFromJid(from_jid));
|
|
|
|
if (chatbox && chatbox.get('type') === 'chatroom') {
|
|
|
|
return false;
|
|
|
|
}
|
2017-07-15 11:03:22 +02:00
|
|
|
if (message.getAttribute('type') !== 'error' &&
|
|
|
|
!_.isNil(from_jid) &&
|
|
|
|
!_.includes(from_jid, '@')) {
|
|
|
|
// Some servers (I'm looking at you Prosody) don't set the message
|
|
|
|
// type to "headline" when sending server messages. For now we
|
|
|
|
// check if an @ signal is included, and if not, we assume it's
|
|
|
|
// a headline message.
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.merge = function merge (first, second) {
|
2017-07-15 11:03:22 +02:00
|
|
|
/* Merge the second object into the first one.
|
|
|
|
*/
|
|
|
|
for (var k in second) {
|
|
|
|
if (_.isObject(first[k])) {
|
|
|
|
merge(first[k], second[k]);
|
|
|
|
} else {
|
|
|
|
first[k] = second[k];
|
2017-02-25 09:33:34 +01:00
|
|
|
}
|
2017-07-15 11:03:22 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.applyUserSettings = function applyUserSettings (context, settings, user_settings) {
|
2017-07-15 11:03:22 +02:00
|
|
|
/* Configuration settings might be nested objects. We only want to
|
|
|
|
* add settings which are whitelisted.
|
|
|
|
*/
|
|
|
|
for (var k in settings) {
|
|
|
|
if (_.isUndefined(user_settings[k])) {
|
|
|
|
continue;
|
2016-03-28 12:49:52 +02:00
|
|
|
}
|
2017-07-15 11:03:22 +02:00
|
|
|
if (_.isObject(settings[k]) && !_.isArray(settings[k])) {
|
|
|
|
applyUserSettings(context[k], settings[k], user_settings[k]);
|
|
|
|
} else {
|
|
|
|
context[k] = user_settings[k];
|
2016-10-27 13:30:58 +02:00
|
|
|
}
|
2017-07-15 11:03:22 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2018-01-02 21:44:46 +01:00
|
|
|
u.stringToNode = function (s) {
|
|
|
|
/* Converts an HTML string into a DOM Node.
|
|
|
|
* Expects that the HTML string has only one top-level element,
|
|
|
|
* i.e. not multiple ones.
|
|
|
|
*
|
|
|
|
* Parameters:
|
|
|
|
* (String) s - The HTML string
|
|
|
|
*/
|
|
|
|
var div = document.createElement('div');
|
|
|
|
div.innerHTML = s;
|
2018-06-04 10:26:44 +02:00
|
|
|
return div.firstElementChild;
|
2018-01-02 21:44:46 +01:00
|
|
|
};
|
|
|
|
|
2018-01-03 14:58:05 +01:00
|
|
|
u.getOuterWidth = function (el, include_margin=false) {
|
|
|
|
var width = el.offsetWidth;
|
|
|
|
if (!include_margin) {
|
|
|
|
return width;
|
|
|
|
}
|
|
|
|
var style = window.getComputedStyle(el);
|
|
|
|
width += parseInt(style.marginLeft, 10) + parseInt(style.marginRight, 10);
|
|
|
|
return width;
|
|
|
|
};
|
|
|
|
|
2018-01-02 21:44:46 +01:00
|
|
|
u.stringToElement = function (s) {
|
2017-08-15 21:23:30 +02:00
|
|
|
/* Converts an HTML string into a DOM element.
|
2018-01-02 21:44:46 +01:00
|
|
|
* Expects that the HTML string has only one top-level element,
|
|
|
|
* i.e. not multiple ones.
|
2017-08-15 21:23:30 +02:00
|
|
|
*
|
|
|
|
* Parameters:
|
|
|
|
* (String) s - The HTML string
|
|
|
|
*/
|
|
|
|
var div = document.createElement('div');
|
|
|
|
div.innerHTML = s;
|
2018-01-02 21:44:46 +01:00
|
|
|
return div.firstElementChild;
|
2017-08-15 21:23:30 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
u.matchesSelector = function (el, selector) {
|
|
|
|
/* Checks whether the DOM element matches the given selector.
|
|
|
|
*
|
|
|
|
* Parameters:
|
|
|
|
* (DOMElement) el - The DOM element
|
|
|
|
* (String) selector - The selector
|
|
|
|
*/
|
|
|
|
return (
|
|
|
|
el.matches ||
|
|
|
|
el.matchesSelector ||
|
|
|
|
el.msMatchesSelector ||
|
|
|
|
el.mozMatchesSelector ||
|
|
|
|
el.webkitMatchesSelector ||
|
|
|
|
el.oMatchesSelector
|
|
|
|
).call(el, selector);
|
|
|
|
};
|
|
|
|
|
|
|
|
u.queryChildren = function (el, selector) {
|
|
|
|
/* Returns a list of children of the DOM element that match the
|
|
|
|
* selector.
|
|
|
|
*
|
|
|
|
* Parameters:
|
|
|
|
* (DOMElement) el - the DOM element
|
|
|
|
* (String) selector - the selector they should be matched
|
|
|
|
* against.
|
|
|
|
*/
|
2018-05-13 12:39:16 +02:00
|
|
|
return _.filter(el.childNodes, _.partial(u.matchesSelector, _, selector));
|
2017-08-15 21:23:30 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
u.contains = function (attr, query) {
|
2017-07-15 11:03:22 +02:00
|
|
|
return function (item) {
|
|
|
|
if (typeof attr === 'object') {
|
|
|
|
var value = false;
|
|
|
|
_.forEach(attr, function (a) {
|
|
|
|
value = value || _.includes(item.get(a).toLowerCase(), query.toLowerCase());
|
|
|
|
});
|
|
|
|
return value;
|
|
|
|
} else if (typeof attr === 'string') {
|
|
|
|
return _.includes(item.get(attr).toLowerCase(), query.toLowerCase());
|
2014-11-17 09:44:42 +01:00
|
|
|
} else {
|
2017-07-15 11:03:22 +02:00
|
|
|
throw new TypeError('contains: wrong attribute type. Must be string or array.');
|
2014-11-17 09:44:42 +01:00
|
|
|
}
|
2017-07-15 11:03:22 +02:00
|
|
|
};
|
|
|
|
};
|
2014-11-24 20:35:00 +01:00
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.isOfType = function (type, item) {
|
2017-06-09 23:43:58 +02:00
|
|
|
return item.get('type') == type;
|
2017-06-12 20:30:58 +02:00
|
|
|
};
|
2017-06-09 23:43:58 +02:00
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.isInstance = function (type, item) {
|
2017-06-07 00:55:25 +02:00
|
|
|
return item instanceof type;
|
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.getAttribute = function (key, item) {
|
2017-06-07 00:55:25 +02:00
|
|
|
return item.get(key);
|
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.contains.not = function (attr, query) {
|
2016-02-19 10:36:01 +01:00
|
|
|
return function (item) {
|
2017-08-15 21:23:30 +02:00
|
|
|
return !(u.contains(attr, query)(item));
|
2016-02-19 10:36:01 +01:00
|
|
|
};
|
|
|
|
};
|
2017-03-31 03:54:42 +02:00
|
|
|
|
2018-05-23 04:22:47 +02:00
|
|
|
u.rootContains = function (root, el) {
|
|
|
|
// The document element does not have the contains method in IE.
|
|
|
|
if (root === document && !root.contains) {
|
|
|
|
return document.head.contains(el) || document.body.contains(el);
|
|
|
|
}
|
|
|
|
return root.contains ? root.contains(el) : window.HTMLElement.prototype.contains.call(root, el);
|
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.createFragmentFromText = function (markup) {
|
2017-07-15 16:00:18 +02:00
|
|
|
/* Returns a DocumentFragment containing DOM nodes based on the
|
|
|
|
* passed-in markup text.
|
|
|
|
*/
|
2017-03-31 03:54:42 +02:00
|
|
|
// http://stackoverflow.com/questions/9334645/create-node-from-markup-string
|
|
|
|
var frag = document.createDocumentFragment(),
|
|
|
|
tmp = document.createElement('body'), child;
|
2017-07-15 16:00:18 +02:00
|
|
|
tmp.innerHTML = markup;
|
|
|
|
// Append elements in a loop to a DocumentFragment, so that the
|
|
|
|
// browser does not re-render the document for each node.
|
2017-03-31 03:54:42 +02:00
|
|
|
while (child = tmp.firstChild) { // eslint-disable-line no-cond-assign
|
|
|
|
frag.appendChild(child);
|
|
|
|
}
|
2017-07-15 16:00:18 +02:00
|
|
|
return frag
|
2017-06-12 20:30:58 +02:00
|
|
|
};
|
2017-03-31 03:54:42 +02:00
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.addEmoji = function (_converse, emojione, text) {
|
2017-07-15 19:17:15 +02:00
|
|
|
if (_converse.use_emojione) {
|
|
|
|
return emojione.toImage(text);
|
|
|
|
} else {
|
|
|
|
return emojione.shortnameToUnicode(text);
|
|
|
|
}
|
2017-06-17 00:49:25 +02:00
|
|
|
}
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.getEmojisByCategory = function (_converse, emojione) {
|
2017-06-16 11:31:57 +02:00
|
|
|
/* Return a dict of emojis with the categories as keys and
|
|
|
|
* lists of emojis in that category as values.
|
|
|
|
*/
|
2017-07-16 12:14:56 +02:00
|
|
|
if (_.isUndefined(_converse.emojis_by_category)) {
|
2017-07-15 07:58:30 +02:00
|
|
|
const emojis = _.values(_.mapValues(emojione.emojioneList, function (value, key, o) {
|
2017-06-16 11:31:57 +02:00
|
|
|
value._shortname = key;
|
|
|
|
return value
|
|
|
|
}));
|
2017-07-15 07:58:30 +02:00
|
|
|
const tones = [':tone1:', ':tone2:', ':tone3:', ':tone4:', ':tone5:'];
|
|
|
|
const excluded = [':kiss_ww:', ':kiss_mm:', ':kiss_woman_man:'];
|
|
|
|
const excluded_substrings = [
|
|
|
|
':woman', ':man', ':women_', ':men_', '_man_', '_woman_', '_woman:', '_man:'
|
|
|
|
];
|
2017-07-15 11:51:15 +02:00
|
|
|
const excluded_categories = ['modifier', 'regional'];
|
2017-07-15 07:58:30 +02:00
|
|
|
const categories = _.difference(
|
|
|
|
_.uniq(_.map(emojis, _.partial(_.get, _, 'category'))),
|
|
|
|
excluded_categories
|
|
|
|
);
|
|
|
|
const emojis_by_category = {};
|
|
|
|
_.forEach(categories, (cat) => {
|
|
|
|
let list = _.sortBy(_.filter(emojis, ['category', cat]), ['uc_base']);
|
|
|
|
list = _.filter(
|
|
|
|
list,
|
|
|
|
(item) => !_.includes(_.concat(tones, excluded), item._shortname) &&
|
|
|
|
!_.some(excluded_substrings, _.partial(_.includes, item._shortname))
|
|
|
|
);
|
2017-06-16 11:31:57 +02:00
|
|
|
if (cat === 'people') {
|
2017-07-15 07:58:30 +02:00
|
|
|
const idx = _.findIndex(list, ['uc_base', '1f600']);
|
2017-06-16 11:31:57 +02:00
|
|
|
list = _.union(_.slice(list, idx), _.slice(list, 0, idx+1));
|
|
|
|
} else if (cat === 'activity') {
|
|
|
|
list = _.union(_.slice(list, 27-1), _.slice(list, 0, 27));
|
|
|
|
} else if (cat === 'objects') {
|
|
|
|
list = _.union(_.slice(list, 24-1), _.slice(list, 0, 24));
|
|
|
|
} else if (cat === 'travel') {
|
|
|
|
list = _.union(_.slice(list, 17-1), _.slice(list, 0, 17));
|
2017-07-15 11:51:15 +02:00
|
|
|
} else if (cat === 'symbols') {
|
|
|
|
list = _.union(_.slice(list, 60-1), _.slice(list, 0, 60));
|
2017-06-16 11:31:57 +02:00
|
|
|
}
|
|
|
|
emojis_by_category[cat] = list;
|
|
|
|
});
|
2017-07-16 12:14:56 +02:00
|
|
|
_converse.emojis_by_category = emojis_by_category;
|
2017-06-16 11:31:57 +02:00
|
|
|
}
|
2017-07-16 12:14:56 +02:00
|
|
|
return _converse.emojis_by_category;
|
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.getTonedEmojis = function (_converse) {
|
2017-07-16 12:14:56 +02:00
|
|
|
_converse.toned_emojis = _.uniq(
|
|
|
|
_.map(
|
|
|
|
_.filter(
|
2017-08-15 21:23:30 +02:00
|
|
|
u.getEmojisByCategory(_converse).people,
|
2017-07-16 12:14:56 +02:00
|
|
|
(person) => _.includes(person._shortname, '_tone')
|
|
|
|
),
|
|
|
|
(person) => person._shortname.replace(/_tone[1-5]/, '')
|
|
|
|
));
|
|
|
|
return _converse.toned_emojis;
|
|
|
|
};
|
2017-06-24 11:00:44 +02:00
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.isPersistableModel = function (model) {
|
2017-06-19 11:08:57 +02:00
|
|
|
return model.collection && model.collection.browserStorage;
|
2017-07-10 21:14:48 +02:00
|
|
|
};
|
|
|
|
|
2017-11-10 15:52:50 +01:00
|
|
|
u.getResolveablePromise = function () {
|
|
|
|
/* Returns a promise object on which `resolve` or `reject` can be
|
|
|
|
* called.
|
|
|
|
*/
|
2017-07-10 21:14:48 +02:00
|
|
|
const wrapper = {};
|
2017-11-10 15:52:50 +01:00
|
|
|
const promise = new Promise((resolve, reject) => {
|
2017-07-10 21:14:48 +02:00
|
|
|
wrapper.resolve = resolve;
|
|
|
|
wrapper.reject = reject;
|
|
|
|
})
|
2017-11-10 15:52:50 +01:00
|
|
|
_.assign(promise, wrapper);
|
|
|
|
return promise;
|
2017-07-10 21:14:48 +02:00
|
|
|
};
|
2017-06-19 11:08:57 +02:00
|
|
|
|
2018-02-19 10:35:58 +01:00
|
|
|
u.interpolate = function (string, o) {
|
|
|
|
return string.replace(/{{{([^{}]*)}}}/g,
|
|
|
|
(a, b) => {
|
|
|
|
var r = o[b];
|
|
|
|
return typeof r === 'string' || typeof r === 'number' ? r : a;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2018-02-22 12:46:10 +01:00
|
|
|
u.onMultipleEvents = function (events=[], callback) {
|
|
|
|
/* Call the callback once all the events have been triggered
|
|
|
|
*
|
|
|
|
* Parameters:
|
|
|
|
* (Array) events: An array of objects, with keys `object` and
|
|
|
|
* `event`, representing the event name and the object it's
|
|
|
|
* triggered upon.
|
|
|
|
* (Function) callback: The function to call once all events have
|
|
|
|
* been triggered.
|
|
|
|
*/
|
|
|
|
let triggered = [];
|
|
|
|
|
|
|
|
function handler (result) {
|
|
|
|
triggered.push(result)
|
|
|
|
if (events.length === triggered.length) {
|
|
|
|
callback(triggered);
|
|
|
|
triggered = [];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_.each(events, (map) => map.object.on(map.event, handler));
|
|
|
|
};
|
|
|
|
|
2017-08-15 21:23:30 +02:00
|
|
|
u.safeSave = function (model, attributes) {
|
|
|
|
if (u.isPersistableModel(model)) {
|
2017-06-23 20:16:16 +02:00
|
|
|
model.save(attributes);
|
2017-06-19 11:08:57 +02:00
|
|
|
} else {
|
2017-06-23 20:16:16 +02:00
|
|
|
model.set(attributes);
|
2017-06-19 11:08:57 +02:00
|
|
|
}
|
2018-08-10 14:09:21 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
u.siblingIndex = function (el) {
|
|
|
|
/* eslint-disable no-cond-assign */
|
|
|
|
for (var i = 0; el = el.previousElementSibling; i++);
|
|
|
|
return i;
|
|
|
|
};
|
|
|
|
|
|
|
|
u.getCurrentWord = function (input) {
|
|
|
|
const cursor = input.selectionEnd || undefined;
|
|
|
|
return _.last(input.value.slice(0, cursor).split(' '));
|
|
|
|
};
|
2017-11-10 22:50:39 +01:00
|
|
|
|
2018-08-14 15:15:04 +02:00
|
|
|
u.replaceCurrentWord = function (input, new_value) {
|
|
|
|
const cursor = input.selectionEnd || undefined,
|
|
|
|
current_word = _.last(input.value.slice(0, cursor).split(' ')),
|
|
|
|
value = input.value;
|
2018-08-14 20:15:38 +02:00
|
|
|
input.value = value.slice(0, cursor - current_word.length) + `${new_value} ` + value.slice(cursor);
|
|
|
|
input.selectionEnd = cursor - current_word.length + new_value.length + 1;
|
2018-08-14 15:15:04 +02:00
|
|
|
};
|
|
|
|
|
2017-11-10 22:50:39 +01:00
|
|
|
u.isVisible = function (el) {
|
2018-02-19 10:35:42 +01:00
|
|
|
if (u.hasClass('hidden', el)) {
|
|
|
|
return false;
|
|
|
|
}
|
2017-11-10 22:50:39 +01:00
|
|
|
// XXX: Taken from jQuery's "visible" implementation
|
|
|
|
return el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0;
|
|
|
|
};
|
|
|
|
|
2018-01-04 17:12:09 +01:00
|
|
|
u.triggerEvent = function (el, name, type="Event", bubbles=true, cancelable=true) {
|
|
|
|
const evt = document.createEvent(type);
|
|
|
|
evt.initEvent(name, bubbles, cancelable);
|
|
|
|
el.dispatchEvent(evt);
|
|
|
|
};
|
2018-03-31 18:29:01 +02:00
|
|
|
|
2018-04-17 15:17:39 +02:00
|
|
|
u.geoUriToHttp = function(text, geouri_replacement) {
|
2018-03-31 18:29:01 +02:00
|
|
|
const regex = /geo:([\-0-9.]+),([\-0-9.]+)(?:,([\-0-9.]+))?(?:\?(.*))?/g;
|
2018-04-17 15:17:39 +02:00
|
|
|
return text.replace(regex, geouri_replacement);
|
2018-03-31 18:29:01 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
u.httpToGeoUri = function(text, _converse) {
|
|
|
|
const replacement = 'geo:$1,$2';
|
|
|
|
return text.replace(_converse.geouri_regex, replacement);
|
|
|
|
};
|
2018-05-12 16:45:40 +02:00
|
|
|
|
|
|
|
u.getSelectValues = function(select) {
|
|
|
|
var result = [];
|
|
|
|
var options = select && select.options;
|
|
|
|
var opt;
|
|
|
|
|
|
|
|
for (var i=0, iLen=options.length; i<iLen; i++) {
|
|
|
|
opt = options[i];
|
|
|
|
|
|
|
|
if (opt.selected) {
|
|
|
|
result.push(opt.value || opt.text);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
};
|
|
|
|
|
2018-08-07 09:45:55 +02:00
|
|
|
u.putCurserAtEnd = function (textarea) {
|
|
|
|
if (textarea !== document.activeElement) {
|
|
|
|
textarea.focus();
|
|
|
|
}
|
|
|
|
// Double the length because Opera is inconsistent about whether a carriage return is one character or two.
|
|
|
|
const len = textarea.value.length * 2;
|
|
|
|
// Timeout seems to be required for Blink
|
|
|
|
setTimeout(() => textarea.setSelectionRange(len, len), 1);
|
|
|
|
// Scroll to the bottom, in case we're in a tall textarea
|
|
|
|
// (Necessary for Firefox and Chrome)
|
|
|
|
this.scrollTop = 999999;
|
|
|
|
};
|
|
|
|
|
2018-07-07 22:44:07 +02:00
|
|
|
u.getUniqueId = function () {
|
|
|
|
return 'xxxxxxxx-xxxx'.replace(/[x]/g, function(c) {
|
|
|
|
var r = Math.random() * 16 | 0,
|
|
|
|
v = c === 'x' ? r : r & 0x3 | 0x8;
|
|
|
|
return v.toString(16);
|
|
|
|
});
|
|
|
|
};
|
2017-08-15 21:23:30 +02:00
|
|
|
return u;
|
2015-01-16 22:07:27 +01:00
|
|
|
}));
|