123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558 |
- /*global define, escape, locales, Jed */
- (function (root, factory) {
- define([
- "jquery.noconflict",
- "sizzle",
- "jquery.browser",
- "lodash.noconflict",
- "locales",
- "moment_with_locales",
- "strophe",
- "tpl!field",
- "tpl!select_option",
- "tpl!form_select",
- "tpl!form_textarea",
- "tpl!form_checkbox",
- "tpl!form_username",
- "tpl!form_input",
- "tpl!form_captcha"
- ], factory);
- }(this, function (
- $, sizzle, dummy, _,
- locales,
- moment,
- Strophe,
- tpl_field,
- tpl_select_option,
- tpl_form_select,
- tpl_form_textarea,
- tpl_form_checkbox,
- tpl_form_username,
- tpl_form_input,
- tpl_form_captcha
- ) {
- "use strict";
- locales = locales || {};
- Strophe = Strophe.Strophe;
- var XFORM_TYPE_MAP = {
- 'text-private': 'password',
- 'text-single': 'text',
- 'fixed': 'label',
- 'boolean': 'checkbox',
- 'hidden': 'hidden',
- 'jid-multi': 'textarea',
- 'list-single': 'dropdown',
- 'list-multi': 'dropdown'
- };
- var afterAnimationEnd = function (el, callback) {
- el.classList.remove('visible');
- if (_.isFunction(callback)) {
- callback();
- }
- };
- var unescapeHTML = function (htmlEscapedText) {
- /* Helper method that replace HTML-escaped symbols with equivalent characters
- * (e.g. transform occurrences of '&' to '&')
- *
- * Parameters:
- * (String) htmlEscapedText: a String containing the HTML-escaped symbols.
- */
- var div = document.createElement('div');
- div.innerHTML = htmlEscapedText;
- return div.innerText;
- }
- var isImage = function (url) {
- var deferred = new $.Deferred();
- var img = new Image();
- var timer = window.setTimeout(function () {
- deferred.reject();
- img = null;
- }, 3000);
- img.onerror = img.onabort = function () {
- clearTimeout(timer);
- deferred.reject();
- };
- img.onload = function () {
- clearTimeout(timer);
- deferred.resolve(img);
- };
- img.src = url;
- return deferred.promise();
- };
- $.fn.hasScrollBar = function() {
- if (!$.contains(document, this.get(0))) {
- return false;
- }
- if(this.parent().height() < this.get(0).scrollHeight) {
- return true;
- }
- return false;
- };
- var throttledHTML = _.throttle(function (el, html) {
- el.innerHTML = html;
- }, 500);
- $.fn.addHyperlinks = function () {
- if (this.length > 0) {
- this.each(function (i, obj) {
- var prot, escaped_url;
- var x = obj.innerHTML;
- var list = x.match(/\b(https?:\/\/|www\.|https?:\/\/www\.)[^\s<]{2,200}\b/g );
- if (list) {
- for (i=0; i<list.length; i++) {
- prot = list[i].indexOf('http://') === 0 || list[i].indexOf('https://') === 0 ? '' : 'http://';
- escaped_url = encodeURI(decodeURI(list[i])).replace(/[!'()]/g, escape).replace(/\*/g, "%2A");
- x = x.replace(list[i], '<a target="_blank" rel="noopener" href="' + prot + escaped_url + '">'+ list[i] + '</a>' );
- }
- }
- obj.innerHTML = x;
- _.forEach(list, function (url) {
- isImage(unescapeHTML(url)).then(function (img) {
- img.className = 'chat-image';
- var a = obj.querySelector('a');
- if (!_.isNull(a)) {
- throttledHTML(a, img.outerHTML);
- }
- });
- });
- });
- }
- return this;
- };
- var utils = {
- // Translation machinery
- // ---------------------
- __: function (str) {
- if (!utils.isConverseLocale(this.locale) || this.locale === 'en') {
- return Jed.sprintf.apply(Jed, arguments);
- }
- if (typeof this.jed === "undefined") {
- this.jed = new Jed(window.JSON.parse(locales[this.locale]));
- }
- var t = this.jed.translate(str);
- if (arguments.length>1) {
- return t.fetch.apply(t, [].slice.call(arguments,1));
- } else {
- return t.fetch();
- }
- },
- ___: function (str) {
- /* XXX: This is part of a hack to get gettext to scan strings to be
- * translated. Strings we cannot send to the function above because
- * they require variable interpolation and we don't yet have the
- * variables at scan time.
- *
- * See actionInfoMessages in src/converse-muc.js
- */
- return str;
- },
- isLocaleAvailable: function (locale, available) {
- /* Check whether the locale or sub locale (e.g. en-US, en) is supported.
- *
- * Parameters:
- * (Function) available - returns a boolean indicating whether the locale is supported
- */
- if (available(locale)) {
- return locale;
- } else {
- var sublocale = locale.split("-")[0];
- if (sublocale !== locale && available(sublocale)) {
- return sublocale;
- }
- }
- },
- hideElement: function (el) {
- el.classList.add('hidden');
- },
- toggleElement: function (el) {
- if (_.includes(el.classList, 'hidden')) {
- // XXX: use fadeIn?
- el.classList.remove('hidden');
- } else {
- this.hideElement (el);
- }
- },
- fadeIn: function (el, callback) {
- if ($.fx.off) {
- el.classList.remove('hidden');
- if (_.isFunction(callback)) {
- callback();
- }
- return;
- }
- if (_.includes(el.classList, 'hidden')) {
- /* XXX: This doesn't appear to be working...
- el.addEventListener("webkitAnimationEnd", _.partial(afterAnimationEnd, el, callback), false);
- el.addEventListener("animationend", _.partial(afterAnimationEnd, el, callback), false);
- */
- setTimeout(_.partial(afterAnimationEnd, el, callback), 351);
- el.classList.add('visible');
- el.classList.remove('hidden');
- } else {
- afterAnimationEnd(el, callback);
- }
- },
- isSameBareJID: function (jid1, jid2) {
- return Strophe.getBareJidFromJid(jid1).toLowerCase() ===
- Strophe.getBareJidFromJid(jid2).toLowerCase();
- },
- isNewMessage: function (message) {
- /* Given a stanza, determine whether it's a new
- * message, i.e. not a MAM archived one.
- */
- if (message instanceof Element) {
- return !(sizzle('result[xmlns="'+Strophe.NS.MAM+'"]', message).length);
- } else {
- return !message.get('archive_id');
- }
- },
- isOTRMessage: function (message) {
- var body = message.querySelector('body'),
- text = (!_.isNull(body) ? body.textContent: undefined);
- return text && !!text.match(/^\?OTR/);
- },
- isHeadlineMessage: function (message) {
- var from_jid = message.getAttribute('from');
- if (message.getAttribute('type') === 'headline') {
- return true;
- }
- if (message.getAttribute('type') !== 'error' &&
- !_.isNil(from_jid) &&
- !_.includes(from_jid, '@')) {
- // Some servers (I'm looking at you Prosody) don't set the message
- // type to "headline" when sending server messages. For now we
- // check if an @ signal is included, and if not, we assume it's
- // a headline message.
- return true;
- }
- return false;
- },
- merge: function merge (first, second) {
- /* Merge the second object into the first one.
- */
- for (var k in second) {
- if (_.isObject(first[k])) {
- merge(first[k], second[k]);
- } else {
- first[k] = second[k];
- }
- }
- },
- applyUserSettings: function applyUserSettings (context, settings, user_settings) {
- /* Configuration settings might be nested objects. We only want to
- * add settings which are whitelisted.
- */
- for (var k in settings) {
- if (_.isUndefined(user_settings[k])) {
- continue;
- }
- if (_.isObject(settings[k]) && !_.isArray(settings[k])) {
- applyUserSettings(context[k], settings[k], user_settings[k]);
- } else {
- context[k] = user_settings[k];
- }
- }
- },
- refreshWebkit: function () {
- /* This works around a webkit bug. Refreshes the browser's viewport,
- * otherwise chatboxes are not moved along when one is closed.
- */
- if ($.browser.webkit && window.requestAnimationFrame) {
- window.requestAnimationFrame(function () {
- var conversejs = document.getElementById('conversejs');
- conversejs.style.display = 'none';
- var tmp = conversejs.offsetHeight; // jshint ignore:line
- conversejs.style.display = 'block';
- });
- }
- },
- webForm2xForm: function (field) {
- /* Takes an HTML DOM and turns it into an XForm field.
- *
- * Parameters:
- * (DOMElement) field - the field to convert
- */
- var $input = $(field), value;
- if ($input.is('[type=checkbox]')) {
- value = $input.is(':checked') && 1 || 0;
- } else if ($input.is('textarea')) {
- value = [];
- var lines = $input.val().split('\n');
- for( var vk=0; vk<lines.length; vk++) {
- var val = $.trim(lines[vk]);
- if (val === '')
- continue;
- value.push(val);
- }
- } else {
- value = $input.val();
- }
- return $(tpl_field({
- name: $input.attr('name'),
- value: value
- }))[0];
- },
- contains: function (attr, query) {
- return function (item) {
- if (typeof attr === 'object') {
- var value = false;
- _.forEach(attr, function (a) {
- value = value || _.includes(item.get(a).toLowerCase(), query.toLowerCase());
- });
- return value;
- } else if (typeof attr === 'string') {
- return _.includes(item.get(attr).toLowerCase(), query.toLowerCase());
- } else {
- throw new TypeError('contains: wrong attribute type. Must be string or array.');
- }
- };
- },
- xForm2webForm: function ($field, $stanza) {
- /* Takes a field in XMPP XForm (XEP-004: Data Forms) format
- * and turns it into a HTML DOM field.
- *
- * Parameters:
- * (XMLElement) field - the field to convert
- */
- // FIXME: take <required> into consideration
- var options = [], j, $options, $values, value, values;
- if ($field.attr('type') === 'list-single' || $field.attr('type') === 'list-multi') {
- values = [];
- $values = $field.children('value');
- for (j=0; j<$values.length; j++) {
- values.push($($values[j]).text());
- }
- $options = $field.children('option');
- for (j=0; j<$options.length; j++) {
- value = $($options[j]).find('value').text();
- options.push(tpl_select_option({
- value: value,
- label: $($options[j]).attr('label'),
- selected: _.startsWith(values, value),
- required: $field.find('required').length
- }));
- }
- return tpl_form_select({
- name: $field.attr('var'),
- label: $field.attr('label'),
- options: options.join(''),
- multiple: ($field.attr('type') === 'list-multi'),
- required: $field.find('required').length
- });
- } else if ($field.attr('type') === 'fixed') {
- return $('<p class="form-help">').text($field.find('value').text());
- } else if ($field.attr('type') === 'jid-multi') {
- return tpl_form_textarea({
- name: $field.attr('var'),
- label: $field.attr('label') || '',
- value: $field.find('value').text(),
- required: $field.find('required').length
- });
- } else if ($field.attr('type') === 'boolean') {
- return tpl_form_checkbox({
- name: $field.attr('var'),
- type: XFORM_TYPE_MAP[$field.attr('type')],
- label: $field.attr('label') || '',
- checked: $field.find('value').text() === "1" && 'checked="1"' || '',
- required: $field.find('required').length
- });
- } else if ($field.attr('type') && $field.attr('var') === 'username') {
- return tpl_form_username({
- domain: ' @'+this.domain,
- name: $field.attr('var'),
- type: XFORM_TYPE_MAP[$field.attr('type')],
- label: $field.attr('label') || '',
- value: $field.find('value').text(),
- required: $field.find('required').length
- });
- } else if ($field.attr('type')) {
- return tpl_form_input({
- name: $field.attr('var'),
- type: XFORM_TYPE_MAP[$field.attr('type')],
- label: $field.attr('label') || '',
- value: $field.find('value').text(),
- required: $field.find('required').length
- });
- } else {
- if ($field.attr('var') === 'ocr') { // Captcha
- return _.reduce(_.map($field.find('uri'),
- $.proxy(function (uri) {
- return tpl_form_captcha({
- label: this.$field.attr('label'),
- name: this.$field.attr('var'),
- data: this.$stanza.find('data[cid="'+uri.textContent.replace(/^cid:/, '')+'"]').text(),
- type: uri.getAttribute('type'),
- required: this.$field.find('required').length
- });
- }, {'$stanza': $stanza, '$field': $field})
- ),
- function (memo, num) { return memo + num; }, ''
- );
- }
- }
- }
- };
- utils.detectLocale = function (library_check) {
- /* Determine which locale is supported by the user's system as well
- * as by the relevant library (e.g. converse.js or moment.js).
- *
- * Parameters:
- * (Function) library_check - returns a boolean indicating whether
- * the locale is supported.
- */
- var locale, i;
- if (window.navigator.userLanguage) {
- locale = utils.isLocaleAvailable(window.navigator.userLanguage, library_check);
- }
- if (window.navigator.languages && !locale) {
- for (i=0; i<window.navigator.languages.length && !locale; i++) {
- locale = utils.isLocaleAvailable(window.navigator.languages[i], library_check);
- }
- }
- if (window.navigator.browserLanguage && !locale) {
- locale = utils.isLocaleAvailable(window.navigator.browserLanguage, library_check);
- }
- if (window.navigator.language && !locale) {
- locale = utils.isLocaleAvailable(window.navigator.language, library_check);
- }
- if (window.navigator.systemLanguage && !locale) {
- locale = utils.isLocaleAvailable(window.navigator.systemLanguage, library_check);
- }
- return locale || 'en';
- };
- utils.isConverseLocale = function (locale) {
- if (!_.isString(locale)) { return false; }
- return _.includes(_.keys(locales || {}), locale)
- };
- utils.isMomentLocale = function (locale) {
- if (!_.isString(locale)) { return false; }
- return moment.locale() !== moment.locale(locale);
- }
- utils.getLocale = function (preferred_locale, isSupportedByLibrary) {
- if (_.isString(preferred_locale)) {
- if (preferred_locale === 'en' || isSupportedByLibrary(preferred_locale)) {
- return preferred_locale;
- }
- try {
- var obj = window.JSON.parse(preferred_locale);
- return obj.locale_data.converse[""].lang;
- } catch (e) {
- console.log(e);
- }
- }
- return utils.detectLocale(isSupportedByLibrary) || 'en';
- };
- utils.isOfType = function (type, item) {
- return item.get('type') == type;
- }
- utils.isInstance = function (type, item) {
- return item instanceof type;
- };
- utils.getAttribute = function (key, item) {
- return item.get(key);
- };
- utils.contains.not = function (attr, query) {
- return function (item) {
- return !(utils.contains(attr, query)(item));
- };
- };
- utils.createElementsFromString = function (element, html) {
- // http://stackoverflow.com/questions/9334645/create-node-from-markup-string
- var frag = document.createDocumentFragment(),
- tmp = document.createElement('body'), child;
- tmp.innerHTML = html;
- // Append elements in a loop to a DocumentFragment, so that the browser does
- // not re-render the document for each node
- while (child = tmp.firstChild) { // eslint-disable-line no-cond-assign
- frag.appendChild(child);
- }
- element.appendChild(frag); // Now, append all elements at once
- frag = tmp = null;
- }
- utils.addEmoticons = function (_converse, emojione, text) {
- return emojione.shortnameToUnicode(text);
- }
- utils.marshallEmojis = function (emojione) {
- /* Return a dict of emojis with the categories as keys and
- * lists of emojis in that category as values.
- */
- if (_.isUndefined(this.emojis_by_category)) {
- var emojis = _.values(_.mapValues(emojione.emojioneList, function (value, key, o) {
- value._shortname = key;
- return value
- }));
- var tones = [':tone1:', ':tone2:', ':tone3:', ':tone4:', ':tone5:'];
- var categories = _.uniq(_.map(emojis, _.partial(_.get, _, 'category')));
- var emojis_by_category = {};
- _.forEach(categories, function (cat) {
- var list = _.sortBy(_.filter(emojis, ['category', cat]), ['uc_base']);
- list = _.filter(list, function (item) {
- return !_.includes(tones, item._shortname) &&
- !item._shortname.startsWith(':woman_') &&
- !item._shortname.startsWith(':man_');
- });
- if (cat === 'people') {
- var idx = _.findIndex(list, ['uc_base', '1f600']);
- list = _.union(_.slice(list, idx), _.slice(list, 0, idx+1));
- } else if (cat === 'activity') {
- list = _.union(_.slice(list, 27-1), _.slice(list, 0, 27));
- } else if (cat === 'objects') {
- list = _.union(_.slice(list, 24-1), _.slice(list, 0, 24));
- } else if (cat === 'travel') {
- list = _.union(_.slice(list, 17-1), _.slice(list, 0, 17));
- }
- emojis_by_category[cat] = list;
- });
- this.emojis_by_category = emojis_by_category;
- }
- return this.emojis_by_category;
- }
- utils.isPersistableModel = function (model) {
- return model.collection && model.collection.browserStorage;
- }
- utils.safeSave = function (model, attributes) {
- if (utils.isPersistableModel(model)) {
- model.save(attributes);
- } else {
- model.set(attributes);
- }
- }
- return utils;
- }));
|