core.js 31 KB

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