utils.js 20 KB

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