core.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779
  1. // Converse.js (A browser based XMPP chat client)
  2. // http://conversejs.org
  3. //
  4. // This is the utilities module.
  5. //
  6. // Copyright (c) 2012-2017, Jan-Carel Brand <jc@opkode.com>
  7. // Licensed under the Mozilla Public License (MPLv2)
  8. //
  9. /*global define, escape, window */
  10. (function (root, factory) {
  11. define([
  12. "sizzle",
  13. "es6-promise",
  14. "lodash.noconflict",
  15. "strophe",
  16. "tpl!audio",
  17. "tpl!file",
  18. "tpl!image",
  19. "tpl!video"
  20. ], factory);
  21. }(this, function (
  22. sizzle,
  23. Promise,
  24. _,
  25. Strophe,
  26. tpl_audio,
  27. tpl_file,
  28. tpl_image,
  29. tpl_video
  30. ) {
  31. "use strict";
  32. const b64_sha1 = Strophe.SHA1.b64_sha1;
  33. Strophe = Strophe.Strophe;
  34. const URL_REGEX = /\b(https?:\/\/|www\.|https?:\/\/www\.)[^\s<>]{2,200}\b\/?/g;
  35. const logger = _.assign({
  36. 'debug': _.get(console, 'log') ? console.log.bind(console) : _.noop,
  37. 'error': _.get(console, 'log') ? console.log.bind(console) : _.noop,
  38. 'info': _.get(console, 'log') ? console.log.bind(console) : _.noop,
  39. 'warn': _.get(console, 'log') ? console.log.bind(console) : _.noop
  40. }, console);
  41. var unescapeHTML = function (htmlEscapedText) {
  42. /* Helper method that replace HTML-escaped symbols with equivalent characters
  43. * (e.g. transform occurrences of '&amp;' to '&')
  44. *
  45. * Parameters:
  46. * (String) htmlEscapedText: a String containing the HTML-escaped symbols.
  47. */
  48. var div = document.createElement('div');
  49. div.innerHTML = htmlEscapedText;
  50. return div.innerText;
  51. };
  52. var isImage = function (url) {
  53. return new Promise((resolve, reject) => {
  54. var img = new Image();
  55. var timer = window.setTimeout(function () {
  56. reject(new Error("Could not determine whether it's an image"));
  57. img = null;
  58. }, 3000);
  59. img.onerror = img.onabort = function () {
  60. clearTimeout(timer);
  61. reject(new Error("Could not determine whether it's an image"));
  62. };
  63. img.onload = function () {
  64. clearTimeout(timer);
  65. resolve(img);
  66. };
  67. img.src = url;
  68. });
  69. };
  70. function slideOutWrapup (el) {
  71. /* Wrapup function for slideOut. */
  72. el.removeAttribute('data-slider-marker');
  73. el.classList.remove('collapsed');
  74. el.style.overflow = "";
  75. el.style.height = "";
  76. }
  77. var u = {};
  78. u.getNextElement = function (el, selector='*') {
  79. let next_el = el.nextElementSibling;
  80. while (!_.isNull(next_el) && !sizzle.matchesSelector(next_el, selector)) {
  81. next_el = next_el.nextElementSibling;
  82. }
  83. return next_el;
  84. }
  85. u.getPreviousElement = function (el, selector='*') {
  86. let prev_el = el.previousSibling;
  87. while (!_.isNull(prev_el) && !sizzle.matchesSelector(prev_el, selector)) {
  88. prev_el = prev_el.previousSibling
  89. }
  90. return prev_el;
  91. }
  92. u.getFirstChildElement = function (el, selector='*') {
  93. let first_el = el.firstElementChild;
  94. while (!_.isNull(first_el) && !sizzle.matchesSelector(first_el, selector)) {
  95. first_el = first_el.nextSibling
  96. }
  97. return first_el;
  98. }
  99. u.getLastChildElement = function (el, selector='*') {
  100. let last_el = el.lastElementChild;
  101. while (!_.isNull(last_el) && !sizzle.matchesSelector(last_el, selector)) {
  102. last_el = last_el.previousSibling
  103. }
  104. return last_el;
  105. }
  106. u.calculateElementHeight = function (el) {
  107. /* Return the height of the passed in DOM element,
  108. * based on the heights of its children.
  109. */
  110. return _.reduce(
  111. el.children,
  112. (result, child) => result + child.offsetHeight, 0
  113. );
  114. }
  115. u.addClass = function (className, el) {
  116. if (el instanceof Element) {
  117. el.classList.add(className);
  118. }
  119. }
  120. u.removeClass = function (className, el) {
  121. if (el instanceof Element) {
  122. el.classList.remove(className);
  123. }
  124. return el;
  125. }
  126. u.removeElement = function (el) {
  127. if (!_.isNil(el) && !_.isNil(el.parentNode)) {
  128. el.parentNode.removeChild(el);
  129. }
  130. }
  131. u.showElement = _.flow(
  132. _.partial(u.removeClass, 'collapsed'),
  133. _.partial(u.removeClass, 'hidden')
  134. )
  135. u.hideElement = function (el) {
  136. if (!_.isNil(el)) {
  137. el.classList.add('hidden');
  138. }
  139. return el;
  140. }
  141. u.ancestor = function (el, selector) {
  142. let parent = el;
  143. while (!_.isNil(parent) && !sizzle.matchesSelector(parent, selector)) {
  144. parent = parent.parentElement;
  145. }
  146. return parent;
  147. }
  148. u.nextUntil = function (el, selector, include_self=false) {
  149. /* Return the element's siblings until one matches the selector. */
  150. const matches = [];
  151. let sibling_el = el.nextElementSibling;
  152. while (!_.isNil(sibling_el) && !sibling_el.matches(selector)) {
  153. matches.push(sibling_el);
  154. sibling_el = sibling_el.nextElementSibling;
  155. }
  156. return matches;
  157. }
  158. u.addHyperlinks = function (text) {
  159. const list = text.match(URL_REGEX) || [];
  160. var links = [];
  161. _.each(list, (match) => {
  162. const prot = match.indexOf('http://') === 0 || match.indexOf('https://') === 0 ? '' : 'http://';
  163. const url = prot + encodeURI(decodeURI(match)).replace(/[!'()]/g, escape).replace(/\*/g, "%2A");
  164. const a = '<a target="_blank" rel="noopener" href="' + url + '">'+ _.escape(match) + '</a>';
  165. // We first insert a hash of the code that will be inserted, and
  166. // then later replace that with the code itself. That way we avoid
  167. // issues when some matches are substrings of others.
  168. links.push(a);
  169. text = text.replace(match, b64_sha1(a));
  170. });
  171. while (links.length) {
  172. const a = links.pop();
  173. text = text.replace(b64_sha1(a), a);
  174. }
  175. return text;
  176. };
  177. u.renderImageURLs = function (obj) {
  178. /* Returns a Promise which resolves once all images have been loaded.
  179. */
  180. const list = obj.textContent.match(URL_REGEX) || [];
  181. return Promise.all(
  182. _.map(list, (url) =>
  183. new Promise((resolve, reject) =>
  184. isImage(url).then(function (img) {
  185. // XXX: need to create a new image, otherwise the event
  186. // listener doesn't fire
  187. const i = new Image();
  188. i.className = 'chat-image';
  189. i.src = img.src;
  190. i.addEventListener('load', resolve);
  191. // We also resolve for non-images, otherwise the
  192. // Promise.all resolves prematurely.
  193. i.addEventListener('error', resolve);
  194. var anchors = sizzle(`a[href="${url}"]`, obj);
  195. _.each(anchors, (a) => {
  196. a.replaceChild(i, a.firstChild);
  197. });
  198. }).catch(resolve)
  199. )
  200. ))
  201. };
  202. u.renderFileURL = function (_converse, url) {
  203. if (url.endsWith('mp3') || url.endsWith('mp4') ||
  204. url.endsWith('jpg') || url.endsWith('jpeg') ||
  205. url.endsWith('png') || url.endsWith('gif') ||
  206. url.endsWith('svg')) {
  207. return url;
  208. }
  209. const name = url.split('/').pop(),
  210. { __ } = _converse;
  211. return tpl_file({
  212. 'url': url,
  213. 'label_download': __('Download file: "%1$s', name)
  214. })
  215. };
  216. u.renderImageURL = function (_converse, url) {
  217. const { __ } = _converse;
  218. if (url.endsWith('jpg') || url.endsWith('jpeg') || url.endsWith('png') ||
  219. url.endsWith('gif') || url.endsWith('svg')) {
  220. return tpl_image({
  221. 'url': url,
  222. 'label_download': __('Download image file')
  223. })
  224. }
  225. return url;
  226. };
  227. u.renderMovieURL = function (_converse, url) {
  228. const { __ } = _converse;
  229. if (url.endsWith('mp4')) {
  230. return tpl_video({
  231. 'url': url,
  232. 'label_download': __('Download video file')
  233. })
  234. }
  235. return url;
  236. };
  237. u.renderAudioURL = function (_converse, url) {
  238. const { __ } = _converse;
  239. if (url.endsWith('mp3')) {
  240. return tpl_audio({
  241. 'url': url,
  242. 'label_download': __('Download audio file')
  243. })
  244. }
  245. return url;
  246. };
  247. u.slideInAllElements = function (elements, duration=300) {
  248. return Promise.all(
  249. _.map(
  250. elements,
  251. _.partial(u.slideIn, _, duration)
  252. ));
  253. };
  254. u.slideToggleElement = function (el, duration) {
  255. if (_.includes(el.classList, 'collapsed') ||
  256. _.includes(el.classList, 'hidden')) {
  257. return u.slideOut(el, duration);
  258. } else {
  259. return u.slideIn(el, duration);
  260. }
  261. };
  262. u.hasClass = function (className, el) {
  263. return _.includes(el.classList, className);
  264. };
  265. u.slideOut = function (el, duration=200) {
  266. /* Shows/expands an element by sliding it out of itself
  267. *
  268. * Parameters:
  269. * (HTMLElement) el - The HTML string
  270. * (Number) duration - The duration amount in milliseconds
  271. */
  272. return new Promise((resolve, reject) => {
  273. if (_.isNil(el)) {
  274. const err = "Undefined or null element passed into slideOut"
  275. logger.warn(err);
  276. reject(new Error(err));
  277. return;
  278. }
  279. const marker = el.getAttribute('data-slider-marker');
  280. if (marker) {
  281. el.removeAttribute('data-slider-marker');
  282. window.cancelAnimationFrame(marker);
  283. }
  284. const end_height = u.calculateElementHeight(el);
  285. if (window.converse_disable_effects) { // Effects are disabled (for tests)
  286. el.style.height = end_height + 'px';
  287. slideOutWrapup(el);
  288. resolve();
  289. return;
  290. }
  291. if (!u.hasClass('collapsed', el) && !u.hasClass('hidden', el)) {
  292. resolve();
  293. return;
  294. }
  295. const steps = duration/17; // We assume 17ms per animation which is ~60FPS
  296. let height = 0;
  297. function draw () {
  298. height += end_height/steps;
  299. if (height < end_height) {
  300. el.style.height = height + 'px';
  301. el.setAttribute(
  302. 'data-slider-marker',
  303. window.requestAnimationFrame(draw)
  304. );
  305. } else {
  306. // We recalculate the height to work around an apparent
  307. // browser bug where browsers don't know the correct
  308. // offsetHeight beforehand.
  309. el.removeAttribute('data-slider-marker');
  310. el.style.height = u.calculateElementHeight(el) + 'px';
  311. el.style.overflow = "";
  312. el.style.height = "";
  313. resolve();
  314. }
  315. }
  316. el.style.height = '0';
  317. el.style.overflow = 'hidden';
  318. el.classList.remove('hidden');
  319. el.classList.remove('collapsed');
  320. el.setAttribute(
  321. 'data-slider-marker',
  322. window.requestAnimationFrame(draw)
  323. );
  324. });
  325. };
  326. u.slideIn = function (el, duration=200) {
  327. /* Hides/collapses an element by sliding it into itself. */
  328. return new Promise((resolve, reject) => {
  329. if (_.isNil(el)) {
  330. const err = "Undefined or null element passed into slideIn";
  331. logger.warn(err);
  332. return reject(new Error(err));
  333. } else if (_.includes(el.classList, 'collapsed')) {
  334. return resolve(el);
  335. } else if (window.converse_disable_effects) { // Effects are disabled (for tests)
  336. el.classList.add('collapsed');
  337. el.style.height = "";
  338. return resolve(el);
  339. }
  340. const marker = el.getAttribute('data-slider-marker');
  341. if (marker) {
  342. el.removeAttribute('data-slider-marker');
  343. window.cancelAnimationFrame(marker);
  344. }
  345. const original_height = el.offsetHeight,
  346. steps = duration/17; // We assume 17ms per animation which is ~60FPS
  347. let height = original_height;
  348. el.style.overflow = 'hidden';
  349. function draw () {
  350. height -= original_height/steps;
  351. if (height > 0) {
  352. el.style.height = height + 'px';
  353. el.setAttribute(
  354. 'data-slider-marker',
  355. window.requestAnimationFrame(draw)
  356. );
  357. } else {
  358. el.removeAttribute('data-slider-marker');
  359. el.classList.add('collapsed');
  360. el.style.height = "";
  361. resolve(el);
  362. }
  363. }
  364. el.setAttribute(
  365. 'data-slider-marker',
  366. window.requestAnimationFrame(draw)
  367. );
  368. });
  369. };
  370. function afterAnimationEnds (el, callback) {
  371. el.classList.remove('visible');
  372. if (_.isFunction(callback)) {
  373. callback();
  374. }
  375. }
  376. u.fadeIn = function (el, callback) {
  377. if (_.isNil(el)) {
  378. logger.warn("Undefined or null element passed into fadeIn");
  379. }
  380. if (window.converse_disable_effects) {
  381. el.classList.remove('hidden');
  382. return afterAnimationEnds(el, callback);
  383. }
  384. if (_.includes(el.classList, 'hidden')) {
  385. el.classList.add('visible');
  386. el.classList.remove('hidden');
  387. el.addEventListener("webkitAnimationEnd", _.partial(afterAnimationEnds, el, callback));
  388. el.addEventListener("animationend", _.partial(afterAnimationEnds, el, callback));
  389. el.addEventListener("oanimationend", _.partial(afterAnimationEnds, el, callback));
  390. } else {
  391. afterAnimationEnds(el, callback);
  392. }
  393. };
  394. u.isValidJID = function (jid) {
  395. return _.compact(jid.split('@')).length === 2 && !jid.startsWith('@') && !jid.endsWith('@');
  396. };
  397. u.isValidMUCJID = function (jid) {
  398. return !jid.startsWith('@') && !jid.endsWith('@');
  399. };
  400. u.isSameBareJID = function (jid1, jid2) {
  401. return Strophe.getBareJidFromJid(jid1).toLowerCase() ===
  402. Strophe.getBareJidFromJid(jid2).toLowerCase();
  403. };
  404. u.getMostRecentMessage = function (model) {
  405. const messages = model.messages.filter('message');
  406. return messages[messages.length-1];
  407. }
  408. u.isNewMessage = function (message) {
  409. /* Given a stanza, determine whether it's a new
  410. * message, i.e. not a MAM archived one.
  411. */
  412. if (message instanceof Element) {
  413. return !sizzle('result[xmlns="'+Strophe.NS.MAM+'"]', message).length &&
  414. !sizzle('delay[xmlns="'+Strophe.NS.DELAY+'"]', message).length;
  415. } else {
  416. return !message.get('delayed');
  417. }
  418. };
  419. u.isOTRMessage = function (message) {
  420. var body = message.querySelector('body'),
  421. text = (!_.isNull(body) ? body.textContent: undefined);
  422. return text && !!text.match(/^\?OTR/);
  423. };
  424. u.isHeadlineMessage = function (_converse, message) {
  425. var from_jid = message.getAttribute('from');
  426. if (message.getAttribute('type') === 'headline') {
  427. return true;
  428. }
  429. const chatbox = _converse.chatboxes.get(Strophe.getBareJidFromJid(from_jid));
  430. if (chatbox && chatbox.get('type') === 'chatroom') {
  431. return false;
  432. }
  433. if (message.getAttribute('type') !== 'error' &&
  434. !_.isNil(from_jid) &&
  435. !_.includes(from_jid, '@')) {
  436. // Some servers (I'm looking at you Prosody) don't set the message
  437. // type to "headline" when sending server messages. For now we
  438. // check if an @ signal is included, and if not, we assume it's
  439. // a headline message.
  440. return true;
  441. }
  442. return false;
  443. };
  444. u.merge = function merge (first, second) {
  445. /* Merge the second object into the first one.
  446. */
  447. for (var k in second) {
  448. if (_.isObject(first[k])) {
  449. merge(first[k], second[k]);
  450. } else {
  451. first[k] = second[k];
  452. }
  453. }
  454. };
  455. u.applyUserSettings = function applyUserSettings (context, settings, user_settings) {
  456. /* Configuration settings might be nested objects. We only want to
  457. * add settings which are whitelisted.
  458. */
  459. for (var k in settings) {
  460. if (_.isUndefined(user_settings[k])) {
  461. continue;
  462. }
  463. if (_.isObject(settings[k]) && !_.isArray(settings[k])) {
  464. applyUserSettings(context[k], settings[k], user_settings[k]);
  465. } else {
  466. context[k] = user_settings[k];
  467. }
  468. }
  469. };
  470. u.stringToNode = function (s) {
  471. /* Converts an HTML string into a DOM Node.
  472. * Expects that the HTML string has only one top-level element,
  473. * i.e. not multiple ones.
  474. *
  475. * Parameters:
  476. * (String) s - The HTML string
  477. */
  478. var div = document.createElement('div');
  479. div.innerHTML = s;
  480. return div.firstChild;
  481. };
  482. u.getOuterWidth = function (el, include_margin=false) {
  483. var width = el.offsetWidth;
  484. if (!include_margin) {
  485. return width;
  486. }
  487. var style = window.getComputedStyle(el);
  488. width += parseInt(style.marginLeft, 10) + parseInt(style.marginRight, 10);
  489. return width;
  490. };
  491. u.stringToElement = function (s) {
  492. /* Converts an HTML string into a DOM element.
  493. * Expects that the HTML string has only one top-level element,
  494. * i.e. not multiple ones.
  495. *
  496. * Parameters:
  497. * (String) s - The HTML string
  498. */
  499. var div = document.createElement('div');
  500. div.innerHTML = s;
  501. return div.firstElementChild;
  502. };
  503. u.matchesSelector = function (el, selector) {
  504. /* Checks whether the DOM element matches the given selector.
  505. *
  506. * Parameters:
  507. * (DOMElement) el - The DOM element
  508. * (String) selector - The selector
  509. */
  510. return (
  511. el.matches ||
  512. el.matchesSelector ||
  513. el.msMatchesSelector ||
  514. el.mozMatchesSelector ||
  515. el.webkitMatchesSelector ||
  516. el.oMatchesSelector
  517. ).call(el, selector);
  518. };
  519. u.queryChildren = function (el, selector) {
  520. /* Returns a list of children of the DOM element that match the
  521. * selector.
  522. *
  523. * Parameters:
  524. * (DOMElement) el - the DOM element
  525. * (String) selector - the selector they should be matched
  526. * against.
  527. */
  528. return _.filter(el.children, _.partial(u.matchesSelector, _, selector));
  529. };
  530. u.contains = function (attr, query) {
  531. return function (item) {
  532. if (typeof attr === 'object') {
  533. var value = false;
  534. _.forEach(attr, function (a) {
  535. value = value || _.includes(item.get(a).toLowerCase(), query.toLowerCase());
  536. });
  537. return value;
  538. } else if (typeof attr === 'string') {
  539. return _.includes(item.get(attr).toLowerCase(), query.toLowerCase());
  540. } else {
  541. throw new TypeError('contains: wrong attribute type. Must be string or array.');
  542. }
  543. };
  544. };
  545. u.isOfType = function (type, item) {
  546. return item.get('type') == type;
  547. };
  548. u.isInstance = function (type, item) {
  549. return item instanceof type;
  550. };
  551. u.getAttribute = function (key, item) {
  552. return item.get(key);
  553. };
  554. u.contains.not = function (attr, query) {
  555. return function (item) {
  556. return !(u.contains(attr, query)(item));
  557. };
  558. };
  559. u.createFragmentFromText = function (markup) {
  560. /* Returns a DocumentFragment containing DOM nodes based on the
  561. * passed-in markup text.
  562. */
  563. // http://stackoverflow.com/questions/9334645/create-node-from-markup-string
  564. var frag = document.createDocumentFragment(),
  565. tmp = document.createElement('body'), child;
  566. tmp.innerHTML = markup;
  567. // Append elements in a loop to a DocumentFragment, so that the
  568. // browser does not re-render the document for each node.
  569. while (child = tmp.firstChild) { // eslint-disable-line no-cond-assign
  570. frag.appendChild(child);
  571. }
  572. return frag
  573. };
  574. u.addEmoji = function (_converse, emojione, text) {
  575. if (_converse.use_emojione) {
  576. return emojione.toImage(text);
  577. } else {
  578. return emojione.shortnameToUnicode(text);
  579. }
  580. }
  581. u.getEmojisByCategory = function (_converse, emojione) {
  582. /* Return a dict of emojis with the categories as keys and
  583. * lists of emojis in that category as values.
  584. */
  585. if (_.isUndefined(_converse.emojis_by_category)) {
  586. const emojis = _.values(_.mapValues(emojione.emojioneList, function (value, key, o) {
  587. value._shortname = key;
  588. return value
  589. }));
  590. const tones = [':tone1:', ':tone2:', ':tone3:', ':tone4:', ':tone5:'];
  591. const excluded = [':kiss_ww:', ':kiss_mm:', ':kiss_woman_man:'];
  592. const excluded_substrings = [
  593. ':woman', ':man', ':women_', ':men_', '_man_', '_woman_', '_woman:', '_man:'
  594. ];
  595. const excluded_categories = ['modifier', 'regional'];
  596. const categories = _.difference(
  597. _.uniq(_.map(emojis, _.partial(_.get, _, 'category'))),
  598. excluded_categories
  599. );
  600. const emojis_by_category = {};
  601. _.forEach(categories, (cat) => {
  602. let list = _.sortBy(_.filter(emojis, ['category', cat]), ['uc_base']);
  603. list = _.filter(
  604. list,
  605. (item) => !_.includes(_.concat(tones, excluded), item._shortname) &&
  606. !_.some(excluded_substrings, _.partial(_.includes, item._shortname))
  607. );
  608. if (cat === 'people') {
  609. const idx = _.findIndex(list, ['uc_base', '1f600']);
  610. list = _.union(_.slice(list, idx), _.slice(list, 0, idx+1));
  611. } else if (cat === 'activity') {
  612. list = _.union(_.slice(list, 27-1), _.slice(list, 0, 27));
  613. } else if (cat === 'objects') {
  614. list = _.union(_.slice(list, 24-1), _.slice(list, 0, 24));
  615. } else if (cat === 'travel') {
  616. list = _.union(_.slice(list, 17-1), _.slice(list, 0, 17));
  617. } else if (cat === 'symbols') {
  618. list = _.union(_.slice(list, 60-1), _.slice(list, 0, 60));
  619. }
  620. emojis_by_category[cat] = list;
  621. });
  622. _converse.emojis_by_category = emojis_by_category;
  623. }
  624. return _converse.emojis_by_category;
  625. };
  626. u.getTonedEmojis = function (_converse) {
  627. _converse.toned_emojis = _.uniq(
  628. _.map(
  629. _.filter(
  630. u.getEmojisByCategory(_converse).people,
  631. (person) => _.includes(person._shortname, '_tone')
  632. ),
  633. (person) => person._shortname.replace(/_tone[1-5]/, '')
  634. ));
  635. return _converse.toned_emojis;
  636. };
  637. u.isPersistableModel = function (model) {
  638. return model.collection && model.collection.browserStorage;
  639. };
  640. u.getResolveablePromise = function () {
  641. /* Returns a promise object on which `resolve` or `reject` can be
  642. * called.
  643. */
  644. const wrapper = {};
  645. const promise = new Promise((resolve, reject) => {
  646. wrapper.resolve = resolve;
  647. wrapper.reject = reject;
  648. })
  649. _.assign(promise, wrapper);
  650. return promise;
  651. };
  652. u.interpolate = function (string, o) {
  653. return string.replace(/{{{([^{}]*)}}}/g,
  654. (a, b) => {
  655. var r = o[b];
  656. return typeof r === 'string' || typeof r === 'number' ? r : a;
  657. });
  658. };
  659. u.onMultipleEvents = function (events=[], callback) {
  660. /* Call the callback once all the events have been triggered
  661. *
  662. * Parameters:
  663. * (Array) events: An array of objects, with keys `object` and
  664. * `event`, representing the event name and the object it's
  665. * triggered upon.
  666. * (Function) callback: The function to call once all events have
  667. * been triggered.
  668. */
  669. let triggered = [];
  670. function handler (result) {
  671. triggered.push(result)
  672. if (events.length === triggered.length) {
  673. callback(triggered);
  674. triggered = [];
  675. }
  676. }
  677. _.each(events, (map) => map.object.on(map.event, handler));
  678. };
  679. u.safeSave = function (model, attributes) {
  680. if (u.isPersistableModel(model)) {
  681. model.save(attributes);
  682. } else {
  683. model.set(attributes);
  684. }
  685. }
  686. u.isVisible = function (el) {
  687. if (u.hasClass('hidden', el)) {
  688. return false;
  689. }
  690. // XXX: Taken from jQuery's "visible" implementation
  691. return el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0;
  692. };
  693. u.triggerEvent = function (el, name, type="Event", bubbles=true, cancelable=true) {
  694. const evt = document.createEvent(type);
  695. evt.initEvent(name, bubbles, cancelable);
  696. el.dispatchEvent(evt);
  697. };
  698. u.geoUriToHttp = function(text, geouri_replacement) {
  699. const regex = /geo:([\-0-9.]+),([\-0-9.]+)(?:,([\-0-9.]+))?(?:\?(.*))?/g;
  700. return text.replace(regex, geouri_replacement);
  701. };
  702. u.httpToGeoUri = function(text, _converse) {
  703. const replacement = 'geo:$1,$2';
  704. return text.replace(_converse.geouri_regex, replacement);
  705. };
  706. return u;
  707. }));