utils.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  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. var a = obj.querySelector('a');
  112. if (!_.isNull(a)) {
  113. throttledHTML(a, img.outerHTML);
  114. }
  115. });
  116. });
  117. });
  118. }
  119. return this;
  120. };
  121. var utils = {
  122. // Translation machinery
  123. // ---------------------
  124. __: function (str) {
  125. if (!utils.isConverseLocale(this.locale) || this.locale === 'en') {
  126. return Jed.sprintf.apply(Jed, arguments);
  127. }
  128. if (typeof this.jed === "undefined") {
  129. this.jed = new Jed(window.JSON.parse(locales[this.locale]));
  130. }
  131. var t = this.jed.translate(str);
  132. if (arguments.length>1) {
  133. return t.fetch.apply(t, [].slice.call(arguments,1));
  134. } else {
  135. return t.fetch();
  136. }
  137. },
  138. ___: function (str) {
  139. /* XXX: This is part of a hack to get gettext to scan strings to be
  140. * translated. Strings we cannot send to the function above because
  141. * they require variable interpolation and we don't yet have the
  142. * variables at scan time.
  143. *
  144. * See actionInfoMessages in src/converse-muc.js
  145. */
  146. return str;
  147. },
  148. isLocaleAvailable: function (locale, available) {
  149. /* Check whether the locale or sub locale (e.g. en-US, en) is supported.
  150. *
  151. * Parameters:
  152. * (Function) available - returns a boolean indicating whether the locale is supported
  153. */
  154. if (available(locale)) {
  155. return locale;
  156. } else {
  157. var sublocale = locale.split("-")[0];
  158. if (sublocale !== locale && available(sublocale)) {
  159. return sublocale;
  160. }
  161. }
  162. },
  163. hideElement: function (el) {
  164. el.classList.add('hidden');
  165. },
  166. toggleElement: function (el) {
  167. if (_.includes(el.classList, 'hidden')) {
  168. // XXX: use fadeIn?
  169. el.classList.remove('hidden');
  170. } else {
  171. this.hideElement (el);
  172. }
  173. },
  174. fadeIn: function (el, callback) {
  175. if ($.fx.off) {
  176. el.classList.remove('hidden');
  177. if (_.isFunction(callback)) {
  178. callback();
  179. }
  180. return;
  181. }
  182. if (_.includes(el.classList, 'hidden')) {
  183. /* XXX: This doesn't appear to be working...
  184. el.addEventListener("webkitAnimationEnd", _.partial(afterAnimationEnd, el, callback), false);
  185. el.addEventListener("animationend", _.partial(afterAnimationEnd, el, callback), false);
  186. */
  187. setTimeout(_.partial(afterAnimationEnd, el, callback), 351);
  188. el.classList.add('visible');
  189. el.classList.remove('hidden');
  190. } else {
  191. afterAnimationEnd(el, callback);
  192. }
  193. },
  194. isSameBareJID: function (jid1, jid2) {
  195. return Strophe.getBareJidFromJid(jid1).toLowerCase() ===
  196. Strophe.getBareJidFromJid(jid2).toLowerCase();
  197. },
  198. isNewMessage: function (message) {
  199. /* Given a stanza, determine whether it's a new
  200. * message, i.e. not a MAM archived one.
  201. */
  202. if (message instanceof Element) {
  203. return !(sizzle('result[xmlns="'+Strophe.NS.MAM+'"]', message).length);
  204. } else {
  205. return !message.get('archive_id');
  206. }
  207. },
  208. isOTRMessage: function (message) {
  209. var body = message.querySelector('body'),
  210. text = (!_.isNull(body) ? body.textContent: undefined);
  211. return text && !!text.match(/^\?OTR/);
  212. },
  213. isHeadlineMessage: function (message) {
  214. var from_jid = message.getAttribute('from');
  215. if (message.getAttribute('type') === 'headline') {
  216. return true;
  217. }
  218. if (message.getAttribute('type') !== 'error' &&
  219. !_.isNil(from_jid) &&
  220. !_.includes(from_jid, '@')) {
  221. // Some servers (I'm looking at you Prosody) don't set the message
  222. // type to "headline" when sending server messages. For now we
  223. // check if an @ signal is included, and if not, we assume it's
  224. // a headline message.
  225. return true;
  226. }
  227. return false;
  228. },
  229. merge: function merge (first, second) {
  230. /* Merge the second object into the first one.
  231. */
  232. for (var k in second) {
  233. if (_.isObject(first[k])) {
  234. merge(first[k], second[k]);
  235. } else {
  236. first[k] = second[k];
  237. }
  238. }
  239. },
  240. applyUserSettings: function applyUserSettings (context, settings, user_settings) {
  241. /* Configuration settings might be nested objects. We only want to
  242. * add settings which are whitelisted.
  243. */
  244. for (var k in settings) {
  245. if (_.isUndefined(user_settings[k])) {
  246. continue;
  247. }
  248. if (_.isObject(settings[k]) && !_.isArray(settings[k])) {
  249. applyUserSettings(context[k], settings[k], user_settings[k]);
  250. } else {
  251. context[k] = user_settings[k];
  252. }
  253. }
  254. },
  255. refreshWebkit: function () {
  256. /* This works around a webkit bug. Refreshes the browser's viewport,
  257. * otherwise chatboxes are not moved along when one is closed.
  258. */
  259. if ($.browser.webkit && window.requestAnimationFrame) {
  260. window.requestAnimationFrame(function () {
  261. var conversejs = document.getElementById('conversejs');
  262. conversejs.style.display = 'none';
  263. var tmp = conversejs.offsetHeight; // jshint ignore:line
  264. conversejs.style.display = 'block';
  265. });
  266. }
  267. },
  268. webForm2xForm: function (field) {
  269. /* Takes an HTML DOM and turns it into an XForm field.
  270. *
  271. * Parameters:
  272. * (DOMElement) field - the field to convert
  273. */
  274. var $input = $(field), value;
  275. if ($input.is('[type=checkbox]')) {
  276. value = $input.is(':checked') && 1 || 0;
  277. } else if ($input.is('textarea')) {
  278. value = [];
  279. var lines = $input.val().split('\n');
  280. for( var vk=0; vk<lines.length; vk++) {
  281. var val = $.trim(lines[vk]);
  282. if (val === '')
  283. continue;
  284. value.push(val);
  285. }
  286. } else {
  287. value = $input.val();
  288. }
  289. return $(tpl_field({
  290. name: $input.attr('name'),
  291. value: value
  292. }))[0];
  293. },
  294. contains: function (attr, query) {
  295. return function (item) {
  296. if (typeof attr === 'object') {
  297. var value = false;
  298. _.forEach(attr, function (a) {
  299. value = value || _.includes(item.get(a).toLowerCase(), query.toLowerCase());
  300. });
  301. return value;
  302. } else if (typeof attr === 'string') {
  303. return _.includes(item.get(attr).toLowerCase(), query.toLowerCase());
  304. } else {
  305. throw new TypeError('contains: wrong attribute type. Must be string or array.');
  306. }
  307. };
  308. },
  309. xForm2webForm: function ($field, $stanza) {
  310. /* Takes a field in XMPP XForm (XEP-004: Data Forms) format
  311. * and turns it into a HTML DOM field.
  312. *
  313. * Parameters:
  314. * (XMLElement) field - the field to convert
  315. */
  316. // FIXME: take <required> into consideration
  317. var options = [], j, $options, $values, value, values;
  318. if ($field.attr('type') === 'list-single' || $field.attr('type') === 'list-multi') {
  319. values = [];
  320. $values = $field.children('value');
  321. for (j=0; j<$values.length; j++) {
  322. values.push($($values[j]).text());
  323. }
  324. $options = $field.children('option');
  325. for (j=0; j<$options.length; j++) {
  326. value = $($options[j]).find('value').text();
  327. options.push(tpl_select_option({
  328. value: value,
  329. label: $($options[j]).attr('label'),
  330. selected: _.startsWith(values, value),
  331. required: $field.find('required').length
  332. }));
  333. }
  334. return tpl_form_select({
  335. name: $field.attr('var'),
  336. label: $field.attr('label'),
  337. options: options.join(''),
  338. multiple: ($field.attr('type') === 'list-multi'),
  339. required: $field.find('required').length
  340. });
  341. } else if ($field.attr('type') === 'fixed') {
  342. return $('<p class="form-help">').text($field.find('value').text());
  343. } else if ($field.attr('type') === 'jid-multi') {
  344. return tpl_form_textarea({
  345. name: $field.attr('var'),
  346. label: $field.attr('label') || '',
  347. value: $field.find('value').text(),
  348. required: $field.find('required').length
  349. });
  350. } else if ($field.attr('type') === 'boolean') {
  351. return tpl_form_checkbox({
  352. name: $field.attr('var'),
  353. type: XFORM_TYPE_MAP[$field.attr('type')],
  354. label: $field.attr('label') || '',
  355. checked: $field.find('value').text() === "1" && 'checked="1"' || '',
  356. required: $field.find('required').length
  357. });
  358. } else if ($field.attr('type') && $field.attr('var') === 'username') {
  359. return tpl_form_username({
  360. domain: ' @'+this.domain,
  361. name: $field.attr('var'),
  362. type: XFORM_TYPE_MAP[$field.attr('type')],
  363. label: $field.attr('label') || '',
  364. value: $field.find('value').text(),
  365. required: $field.find('required').length
  366. });
  367. } else if ($field.attr('type')) {
  368. return tpl_form_input({
  369. name: $field.attr('var'),
  370. type: XFORM_TYPE_MAP[$field.attr('type')],
  371. label: $field.attr('label') || '',
  372. value: $field.find('value').text(),
  373. required: $field.find('required').length
  374. });
  375. } else {
  376. if ($field.attr('var') === 'ocr') { // Captcha
  377. return _.reduce(_.map($field.find('uri'),
  378. $.proxy(function (uri) {
  379. return tpl_form_captcha({
  380. label: this.$field.attr('label'),
  381. name: this.$field.attr('var'),
  382. data: this.$stanza.find('data[cid="'+uri.textContent.replace(/^cid:/, '')+'"]').text(),
  383. type: uri.getAttribute('type'),
  384. required: this.$field.find('required').length
  385. });
  386. }, {'$stanza': $stanza, '$field': $field})
  387. ),
  388. function (memo, num) { return memo + num; }, ''
  389. );
  390. }
  391. }
  392. }
  393. };
  394. utils.detectLocale = function (library_check) {
  395. /* Determine which locale is supported by the user's system as well
  396. * as by the relevant library (e.g. converse.js or moment.js).
  397. *
  398. * Parameters:
  399. * (Function) library_check - returns a boolean indicating whether
  400. * the locale is supported.
  401. */
  402. var locale, i;
  403. if (window.navigator.userLanguage) {
  404. locale = utils.isLocaleAvailable(window.navigator.userLanguage, library_check);
  405. }
  406. if (window.navigator.languages && !locale) {
  407. for (i=0; i<window.navigator.languages.length && !locale; i++) {
  408. locale = utils.isLocaleAvailable(window.navigator.languages[i], library_check);
  409. }
  410. }
  411. if (window.navigator.browserLanguage && !locale) {
  412. locale = utils.isLocaleAvailable(window.navigator.browserLanguage, library_check);
  413. }
  414. if (window.navigator.language && !locale) {
  415. locale = utils.isLocaleAvailable(window.navigator.language, library_check);
  416. }
  417. if (window.navigator.systemLanguage && !locale) {
  418. locale = utils.isLocaleAvailable(window.navigator.systemLanguage, library_check);
  419. }
  420. return locale || 'en';
  421. };
  422. utils.isConverseLocale = function (locale) {
  423. if (!_.isString(locale)) { return false; }
  424. return _.includes(_.keys(locales || {}), locale)
  425. };
  426. utils.isMomentLocale = function (locale) {
  427. if (!_.isString(locale)) { return false; }
  428. return moment.locale() !== moment.locale(locale);
  429. }
  430. utils.getLocale = function (preferred_locale, isSupportedByLibrary) {
  431. if (_.isString(preferred_locale)) {
  432. if (preferred_locale === 'en' || isSupportedByLibrary(preferred_locale)) {
  433. return preferred_locale;
  434. }
  435. try {
  436. var obj = window.JSON.parse(preferred_locale);
  437. return obj.locale_data.converse[""].lang;
  438. } catch (e) {
  439. console.log(e);
  440. }
  441. }
  442. return utils.detectLocale(isSupportedByLibrary) || 'en';
  443. };
  444. utils.isOfType = function (type, item) {
  445. return item.get('type') == type;
  446. }
  447. utils.isInstance = function (type, item) {
  448. return item instanceof type;
  449. };
  450. utils.getAttribute = function (key, item) {
  451. return item.get(key);
  452. };
  453. utils.contains.not = function (attr, query) {
  454. return function (item) {
  455. return !(utils.contains(attr, query)(item));
  456. };
  457. };
  458. utils.createElementsFromString = function (element, html) {
  459. // http://stackoverflow.com/questions/9334645/create-node-from-markup-string
  460. var frag = document.createDocumentFragment(),
  461. tmp = document.createElement('body'), child;
  462. tmp.innerHTML = html;
  463. // Append elements in a loop to a DocumentFragment, so that the browser does
  464. // not re-render the document for each node
  465. while (child = tmp.firstChild) { // eslint-disable-line no-cond-assign
  466. frag.appendChild(child);
  467. }
  468. element.appendChild(frag); // Now, append all elements at once
  469. frag = tmp = null;
  470. }
  471. utils.addEmoticons = function (_converse, emojione, text) {
  472. return emojione.shortnameToUnicode(text);
  473. }
  474. utils.marshallEmojis = function (emojione) {
  475. /* Return a dict of emojis with the categories as keys and
  476. * lists of emojis in that category as values.
  477. */
  478. if (_.isUndefined(this.emojis_by_category)) {
  479. var emojis = _.values(_.mapValues(emojione.emojioneList, function (value, key, o) {
  480. value._shortname = key;
  481. return value
  482. }));
  483. var tones = [':tone1:', ':tone2:', ':tone3:', ':tone4:', ':tone5:'];
  484. var categories = _.uniq(_.map(emojis, _.partial(_.get, _, 'category')));
  485. var emojis_by_category = {};
  486. _.forEach(categories, function (cat) {
  487. var list = _.sortBy(_.filter(emojis, ['category', cat]), ['uc_base']);
  488. list = _.filter(list, function (item) {
  489. return !_.includes(tones, item._shortname) &&
  490. !item._shortname.startsWith(':woman_') &&
  491. !item._shortname.startsWith(':man_');
  492. });
  493. if (cat === 'people') {
  494. var idx = _.findIndex(list, ['uc_base', '1f600']);
  495. list = _.union(_.slice(list, idx), _.slice(list, 0, idx+1));
  496. } else if (cat === 'activity') {
  497. list = _.union(_.slice(list, 27-1), _.slice(list, 0, 27));
  498. } else if (cat === 'objects') {
  499. list = _.union(_.slice(list, 24-1), _.slice(list, 0, 24));
  500. } else if (cat === 'travel') {
  501. list = _.union(_.slice(list, 17-1), _.slice(list, 0, 17));
  502. }
  503. emojis_by_category[cat] = list;
  504. });
  505. this.emojis_by_category = emojis_by_category;
  506. }
  507. return this.emojis_by_category;
  508. }
  509. utils.isPersistableModel = function (model) {
  510. return model.collection && model.collection.browserStorage;
  511. }
  512. utils.safeSave = function (model, attributes) {
  513. if (utils.isPersistableModel(model)) {
  514. model.save(attributes);
  515. } else {
  516. model.set(attributes);
  517. }
  518. }
  519. return utils;
  520. }));