core.js 32 KB

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