utils.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. /*global define, escape, locales, Jed */
  2. (function (root, factory) {
  3. define([
  4. "jquery.noconflict",
  5. "sizzle",
  6. "jquery.browser",
  7. "lodash.noconflict",
  8. "locales",
  9. "moment_with_locales",
  10. "strophe",
  11. "tpl!field",
  12. "tpl!select_option",
  13. "tpl!form_select",
  14. "tpl!form_textarea",
  15. "tpl!form_checkbox",
  16. "tpl!form_username",
  17. "tpl!form_input",
  18. "tpl!form_captcha"
  19. ], factory);
  20. }(this, function (
  21. $, sizzle, dummy, _,
  22. locales,
  23. moment,
  24. Strophe,
  25. tpl_field,
  26. tpl_select_option,
  27. tpl_form_select,
  28. tpl_form_textarea,
  29. tpl_form_checkbox,
  30. tpl_form_username,
  31. tpl_form_input,
  32. tpl_form_captcha
  33. ) {
  34. "use strict";
  35. locales = locales || {};
  36. Strophe = Strophe.Strophe;
  37. var XFORM_TYPE_MAP = {
  38. 'text-private': 'password',
  39. 'text-single': 'text',
  40. 'fixed': 'label',
  41. 'boolean': 'checkbox',
  42. 'hidden': 'hidden',
  43. 'jid-multi': 'textarea',
  44. 'list-single': 'dropdown',
  45. 'list-multi': 'dropdown'
  46. };
  47. var afterAnimationEnd = function (el, callback) {
  48. el.classList.remove('visible');
  49. if (_.isFunction(callback)) {
  50. callback();
  51. }
  52. };
  53. var unescapeHTML = function (htmlEscapedText) {
  54. /* Helper method that replace HTML-escaped symbols with equivalent characters
  55. * (e.g. transform occurrences of '&' to '&')
  56. *
  57. * Parameters:
  58. * (String) htmlEscapedText: a String containing the HTML-escaped symbols.
  59. */
  60. var div = document.createElement('div');
  61. div.innerHTML = htmlEscapedText;
  62. return div.innerText;
  63. }
  64. var isImage = function (url) {
  65. var deferred = new $.Deferred();
  66. var img = new Image();
  67. var timer = window.setTimeout(function () {
  68. deferred.reject();
  69. img = null;
  70. }, 3000);
  71. img.onerror = img.onabort = function () {
  72. clearTimeout(timer);
  73. deferred.reject();
  74. };
  75. img.onload = function () {
  76. clearTimeout(timer);
  77. deferred.resolve(img);
  78. };
  79. img.src = url;
  80. return deferred.promise();
  81. };
  82. $.fn.hasScrollBar = function() {
  83. if (!$.contains(document, this.get(0))) {
  84. return false;
  85. }
  86. if(this.parent().height() < this.get(0).scrollHeight) {
  87. return true;
  88. }
  89. return false;
  90. };
  91. var throttledHTML = _.throttle(function (el, html) {
  92. el.innerHTML = html;
  93. }, 500);
  94. $.fn.addHyperlinks = function () {
  95. if (this.length > 0) {
  96. this.each(function (i, obj) {
  97. var prot, escaped_url;
  98. var x = obj.innerHTML;
  99. var list = x.match(/\b(https?:\/\/|www\.|https?:\/\/www\.)[^\s<]{2,200}\b/g );
  100. if (list) {
  101. for (i=0; i<list.length; i++) {
  102. prot = list[i].indexOf('http://') === 0 || list[i].indexOf('https://') === 0 ? '' : 'http://';
  103. escaped_url = encodeURI(decodeURI(list[i])).replace(/[!'()]/g, escape).replace(/\*/g, "%2A");
  104. x = x.replace(list[i], '<a target="_blank" rel="noopener" href="' + prot + escaped_url + '">'+ list[i] + '</a>' );
  105. }
  106. }
  107. obj.innerHTML = x;
  108. _.forEach(list, function (url) {
  109. isImage(unescapeHTML(url)).then(function (img) {
  110. img.className = 'chat-image';
  111. throttledHTML(obj.querySelector('a'), img.outerHTML);
  112. });
  113. });
  114. });
  115. }
  116. return this;
  117. };
  118. $.fn.addEmoticons = function (allowed) {
  119. if (allowed) {
  120. if (this.length > 0) {
  121. this.each(function (i, obj) {
  122. var text = $(obj).html();
  123. text = text.replace(/&gt;:\)/g, '<span class="emoticon icon-evil"></span>');
  124. text = text.replace(/:\)/g, '<span class="emoticon icon-smiley"></span>');
  125. text = text.replace(/:\-\)/g, '<span class="emoticon icon-smiley"></span>');
  126. text = text.replace(/;\)/g, '<span class="emoticon icon-wink"></span>');
  127. text = text.replace(/;\-\)/g, '<span class="emoticon icon-wink"></span>');
  128. text = text.replace(/:D/g, '<span class="emoticon icon-grin"></span>');
  129. text = text.replace(/:\-D/g, '<span class="emoticon icon-grin"></span>');
  130. text = text.replace(/:P/g, '<span class="emoticon icon-tongue"></span>');
  131. text = text.replace(/:\-P/g, '<span class="emoticon icon-tongue"></span>');
  132. text = text.replace(/:p/g, '<span class="emoticon icon-tongue"></span>');
  133. text = text.replace(/:\-p/g, '<span class="emoticon icon-tongue"></span>');
  134. text = text.replace(/8\)/g, '<span class="emoticon icon-cool"></span>');
  135. text = text.replace(/:S/g, '<span class="emoticon icon-confused"></span>');
  136. text = text.replace(/:\\/g, '<span class="emoticon icon-wondering"></span>');
  137. text = text.replace(/:\/ /g, '<span class="emoticon icon-wondering"></span>');
  138. text = text.replace(/&gt;:\(/g, '<span class="emoticon icon-angry"></span>');
  139. text = text.replace(/:\(/g, '<span class="emoticon icon-sad"></span>');
  140. text = text.replace(/:\-\(/g, '<span class="emoticon icon-sad"></span>');
  141. text = text.replace(/:O/g, '<span class="emoticon icon-shocked"></span>');
  142. text = text.replace(/:\-O/g, '<span class="emoticon icon-shocked"></span>');
  143. text = text.replace(/\=\-O/g, '<span class="emoticon icon-shocked"></span>');
  144. text = text.replace(/\(\^.\^\)b/g, '<span class="emoticon icon-thumbs-up"></span>');
  145. text = text.replace(/&lt;3/g, '<span class="emoticon icon-heart"></span>');
  146. $(obj).html(text);
  147. });
  148. }
  149. }
  150. return this;
  151. };
  152. var utils = {
  153. // Translation machinery
  154. // ---------------------
  155. __: function (str) {
  156. if (!utils.isConverseLocale(this.locale) || this.locale === 'en') {
  157. return Jed.sprintf.apply(Jed, arguments);
  158. }
  159. if (typeof this.jed === "undefined") {
  160. this.jed = new Jed(window.JSON.parse(locales[this.locale]));
  161. }
  162. var t = this.jed.translate(str);
  163. if (arguments.length>1) {
  164. return t.fetch.apply(t, [].slice.call(arguments,1));
  165. } else {
  166. return t.fetch();
  167. }
  168. },
  169. ___: function (str) {
  170. /* XXX: This is part of a hack to get gettext to scan strings to be
  171. * translated. Strings we cannot send to the function above because
  172. * they require variable interpolation and we don't yet have the
  173. * variables at scan time.
  174. *
  175. * See actionInfoMessages in src/converse-muc.js
  176. */
  177. return str;
  178. },
  179. isLocaleAvailable: function (locale, available) {
  180. /* Check whether the locale or sub locale (e.g. en-US, en) is supported.
  181. *
  182. * Parameters:
  183. * (Function) available - returns a boolean indicating whether the locale is supported
  184. */
  185. if (available(locale)) {
  186. return locale;
  187. } else {
  188. var sublocale = locale.split("-")[0];
  189. if (sublocale !== locale && available(sublocale)) {
  190. return sublocale;
  191. }
  192. }
  193. },
  194. fadeIn: function (el, callback) {
  195. if ($.fx.off) {
  196. el.classList.remove('hidden');
  197. if (_.isFunction(callback)) {
  198. callback();
  199. }
  200. return;
  201. }
  202. if (_.includes(el.classList, 'hidden')) {
  203. /* XXX: This doesn't appear to be working...
  204. el.addEventListener("webkitAnimationEnd", _.partial(afterAnimationEnd, el, callback), false);
  205. el.addEventListener("animationend", _.partial(afterAnimationEnd, el, callback), false);
  206. */
  207. setTimeout(_.partial(afterAnimationEnd, el, callback), 351);
  208. el.classList.add('visible');
  209. el.classList.remove('hidden');
  210. } else {
  211. afterAnimationEnd(el, callback);
  212. }
  213. },
  214. isSameBareJID: function (jid1, jid2) {
  215. return Strophe.getBareJidFromJid(jid1).toLowerCase() ===
  216. Strophe.getBareJidFromJid(jid2).toLowerCase();
  217. },
  218. isNewMessage: function (message) {
  219. /* Given a stanza, determine whether it's a new
  220. * message, i.e. not a MAM archived one.
  221. */
  222. if (message instanceof Element) {
  223. return !(sizzle('result[xmlns="'+Strophe.NS.MAM+'"]', message).length);
  224. } else {
  225. return !message.get('archive_id');
  226. }
  227. },
  228. isOTRMessage: function (message) {
  229. var body = message.querySelector('body'),
  230. text = (!_.isNull(body) ? body.textContent: undefined);
  231. return text && !!text.match(/^\?OTR/);
  232. },
  233. isHeadlineMessage: function (message) {
  234. var from_jid = message.getAttribute('from');
  235. if (message.getAttribute('type') === 'headline') {
  236. return true;
  237. }
  238. if (message.getAttribute('type') !== 'error' &&
  239. !_.isNil(from_jid) &&
  240. !_.includes(from_jid, '@')) {
  241. // Some servers (I'm looking at you Prosody) don't set the message
  242. // type to "headline" when sending server messages. For now we
  243. // check if an @ signal is included, and if not, we assume it's
  244. // a headline message.
  245. return true;
  246. }
  247. return false;
  248. },
  249. merge: function merge (first, second) {
  250. /* Merge the second object into the first one.
  251. */
  252. for (var k in second) {
  253. if (_.isObject(first[k])) {
  254. merge(first[k], second[k]);
  255. } else {
  256. first[k] = second[k];
  257. }
  258. }
  259. },
  260. applyUserSettings: function applyUserSettings (context, settings, user_settings) {
  261. /* Configuration settings might be nested objects. We only want to
  262. * add settings which are whitelisted.
  263. */
  264. for (var k in settings) {
  265. if (_.isUndefined(user_settings[k])) {
  266. continue;
  267. }
  268. if (_.isObject(settings[k]) && !_.isArray(settings[k])) {
  269. applyUserSettings(context[k], settings[k], user_settings[k]);
  270. } else {
  271. context[k] = user_settings[k];
  272. }
  273. }
  274. },
  275. refreshWebkit: function () {
  276. /* This works around a webkit bug. Refreshes the browser's viewport,
  277. * otherwise chatboxes are not moved along when one is closed.
  278. */
  279. if ($.browser.webkit && window.requestAnimationFrame) {
  280. window.requestAnimationFrame(function () {
  281. var conversejs = document.getElementById('conversejs');
  282. conversejs.style.display = 'none';
  283. var tmp = conversejs.offsetHeight; // jshint ignore:line
  284. conversejs.style.display = 'block';
  285. });
  286. }
  287. },
  288. webForm2xForm: function (field) {
  289. /* Takes an HTML DOM and turns it into an XForm field.
  290. *
  291. * Parameters:
  292. * (DOMElement) field - the field to convert
  293. */
  294. var $input = $(field), value;
  295. if ($input.is('[type=checkbox]')) {
  296. value = $input.is(':checked') && 1 || 0;
  297. } else if ($input.is('textarea')) {
  298. value = [];
  299. var lines = $input.val().split('\n');
  300. for( var vk=0; vk<lines.length; vk++) {
  301. var val = $.trim(lines[vk]);
  302. if (val === '')
  303. continue;
  304. value.push(val);
  305. }
  306. } else {
  307. value = $input.val();
  308. }
  309. return $(tpl_field({
  310. name: $input.attr('name'),
  311. value: value
  312. }))[0];
  313. },
  314. contains: function (attr, query) {
  315. return function (item) {
  316. if (typeof attr === 'object') {
  317. var value = false;
  318. _.forEach(attr, function (a) {
  319. value = value || _.includes(item.get(a).toLowerCase(), query.toLowerCase());
  320. });
  321. return value;
  322. } else if (typeof attr === 'string') {
  323. return _.includes(item.get(attr).toLowerCase(), query.toLowerCase());
  324. } else {
  325. throw new TypeError('contains: wrong attribute type. Must be string or array.');
  326. }
  327. };
  328. },
  329. xForm2webForm: function ($field, $stanza) {
  330. /* Takes a field in XMPP XForm (XEP-004: Data Forms) format
  331. * and turns it into a HTML DOM field.
  332. *
  333. * Parameters:
  334. * (XMLElement) field - the field to convert
  335. */
  336. // FIXME: take <required> into consideration
  337. var options = [], j, $options, $values, value, values;
  338. if ($field.attr('type') === 'list-single' || $field.attr('type') === 'list-multi') {
  339. values = [];
  340. $values = $field.children('value');
  341. for (j=0; j<$values.length; j++) {
  342. values.push($($values[j]).text());
  343. }
  344. $options = $field.children('option');
  345. for (j=0; j<$options.length; j++) {
  346. value = $($options[j]).find('value').text();
  347. options.push(tpl_select_option({
  348. value: value,
  349. label: $($options[j]).attr('label'),
  350. selected: _.startsWith(values, value),
  351. required: $field.find('required').length
  352. }));
  353. }
  354. return tpl_form_select({
  355. name: $field.attr('var'),
  356. label: $field.attr('label'),
  357. options: options.join(''),
  358. multiple: ($field.attr('type') === 'list-multi'),
  359. required: $field.find('required').length
  360. });
  361. } else if ($field.attr('type') === 'fixed') {
  362. return $('<p class="form-help">').text($field.find('value').text());
  363. } else if ($field.attr('type') === 'jid-multi') {
  364. return tpl_form_textarea({
  365. name: $field.attr('var'),
  366. label: $field.attr('label') || '',
  367. value: $field.find('value').text(),
  368. required: $field.find('required').length
  369. });
  370. } else if ($field.attr('type') === 'boolean') {
  371. return tpl_form_checkbox({
  372. name: $field.attr('var'),
  373. type: XFORM_TYPE_MAP[$field.attr('type')],
  374. label: $field.attr('label') || '',
  375. checked: $field.find('value').text() === "1" && 'checked="1"' || '',
  376. required: $field.find('required').length
  377. });
  378. } else if ($field.attr('type') && $field.attr('var') === 'username') {
  379. return tpl_form_username({
  380. domain: ' @'+this.domain,
  381. name: $field.attr('var'),
  382. type: XFORM_TYPE_MAP[$field.attr('type')],
  383. label: $field.attr('label') || '',
  384. value: $field.find('value').text(),
  385. required: $field.find('required').length
  386. });
  387. } else if ($field.attr('type')) {
  388. return tpl_form_input({
  389. name: $field.attr('var'),
  390. type: XFORM_TYPE_MAP[$field.attr('type')],
  391. label: $field.attr('label') || '',
  392. value: $field.find('value').text(),
  393. required: $field.find('required').length
  394. });
  395. } else {
  396. if ($field.attr('var') === 'ocr') { // Captcha
  397. return _.reduce(_.map($field.find('uri'),
  398. $.proxy(function (uri) {
  399. return tpl_form_captcha({
  400. label: this.$field.attr('label'),
  401. name: this.$field.attr('var'),
  402. data: this.$stanza.find('data[cid="'+uri.textContent.replace(/^cid:/, '')+'"]').text(),
  403. type: uri.getAttribute('type'),
  404. required: this.$field.find('required').length
  405. });
  406. }, {'$stanza': $stanza, '$field': $field})
  407. ),
  408. function (memo, num) { return memo + num; }, ''
  409. );
  410. }
  411. }
  412. }
  413. };
  414. utils.detectLocale = function (library_check) {
  415. /* Determine which locale is supported by the user's system as well
  416. * as by the relevant library (e.g. converse.js or moment.js).
  417. *
  418. * Parameters:
  419. * (Function) library_check - returns a boolean indicating whether
  420. * the locale is supported.
  421. */
  422. var locale, i;
  423. if (window.navigator.userLanguage) {
  424. locale = utils.isLocaleAvailable(window.navigator.userLanguage, library_check);
  425. }
  426. if (window.navigator.languages && !locale) {
  427. for (i=0; i<window.navigator.languages.length && !locale; i++) {
  428. locale = utils.isLocaleAvailable(window.navigator.languages[i], library_check);
  429. }
  430. }
  431. if (window.navigator.browserLanguage && !locale) {
  432. locale = utils.isLocaleAvailable(window.navigator.browserLanguage, library_check);
  433. }
  434. if (window.navigator.language && !locale) {
  435. locale = utils.isLocaleAvailable(window.navigator.language, library_check);
  436. }
  437. if (window.navigator.systemLanguage && !locale) {
  438. locale = utils.isLocaleAvailable(window.navigator.systemLanguage, library_check);
  439. }
  440. return locale || 'en';
  441. };
  442. utils.isConverseLocale = function (locale) {
  443. if (!_.isString(locale)) { return false; }
  444. return _.includes(_.keys(locales || {}), locale)
  445. };
  446. utils.isMomentLocale = function (locale) {
  447. if (!_.isString(locale)) { return false; }
  448. return moment.locale() !== moment.locale(locale);
  449. }
  450. utils.getLocale = function (preferred_locale, isSupportedByLibrary) {
  451. if (_.isString(preferred_locale)) {
  452. if (preferred_locale === 'en' || isSupportedByLibrary(preferred_locale)) {
  453. return preferred_locale;
  454. }
  455. try {
  456. var obj = window.JSON.parse(preferred_locale);
  457. return obj.locale_data.converse[""].lang;
  458. } catch (e) {
  459. console.log(e);
  460. }
  461. }
  462. return utils.detectLocale(isSupportedByLibrary) || 'en';
  463. };
  464. utils.isOfType = function (type, item) {
  465. return item.get('type') == type;
  466. }
  467. utils.isInstance = function (type, item) {
  468. return item instanceof type;
  469. };
  470. utils.getAttribute = function (key, item) {
  471. return item.get(key);
  472. };
  473. utils.contains.not = function (attr, query) {
  474. return function (item) {
  475. return !(utils.contains(attr, query)(item));
  476. };
  477. };
  478. utils.createElementsFromString = function (element, html) {
  479. // http://stackoverflow.com/questions/9334645/create-node-from-markup-string
  480. var frag = document.createDocumentFragment(),
  481. tmp = document.createElement('body'), child;
  482. tmp.innerHTML = html;
  483. // Append elements in a loop to a DocumentFragment, so that the browser does
  484. // not re-render the document for each node
  485. while (child = tmp.firstChild) { // eslint-disable-line no-cond-assign
  486. frag.appendChild(child);
  487. }
  488. element.appendChild(frag); // Now, append all elements at once
  489. frag = tmp = null;
  490. }
  491. utils.isPersistableModel = function (model) {
  492. return model.collection && model.collection.browserStorage;
  493. }
  494. utils.saveWithFallback = function (model, attrs) {
  495. if (utils.isPersistableModel(this)) {
  496. model.save(attrs);
  497. } else {
  498. model.set(attrs);
  499. }
  500. }
  501. return utils;
  502. }));