var util = (function () { var exports = {}; // From MDN: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/random exports.random_int = function random_int(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }; var favicon_selector = 'link[rel="shortcut icon"]'; // We need to reset the favicon after changing the // window.location.hash or Firefox will drop the favicon. See // https://bugzilla.mozilla.org/show_bug.cgi?id=519028 exports.reset_favicon = function () { $(favicon_selector).detach().appendTo('head'); }; exports.set_favicon = function (url) { if ($.browser.webkit) { // Works in Chrome 22 at least. // Doesn't work in Firefox 10. $(favicon_selector).attr('href', url); } else { // Delete and re-create the node. // May cause excessive work by the browser // in re-rendering the page (see #882). $(favicon_selector).remove(); $('head').append($('') .attr('rel', 'shortcut icon') .attr('href', url)); } }; exports.make_loading_indicator = function (outer_container, opts) { opts = opts || {}; var container = outer_container; container.empty(); if (opts.abs_positioned !== undefined && opts.abs_positioned) { // Create some additional containers to facilitate absolutely // positioned spinners. var container_id = container.attr('id'); var inner_container = $('
'); container.append(inner_container); container = inner_container; inner_container = $(''); container.append(inner_container); container = inner_container; } var spinner_elem = $(''); container.append(spinner_elem); var text_width = 0; if (opts.text !== undefined && opts.text !== '') { var text_elem = $(''); text_elem.text(opts.text); container.append(text_elem); // See note, below text_width = 20 + text_elem.width(); } // These width calculations are tied to the spinner width and // margins defined via CSS // // TODO: We set white-space to 'nowrap' because under some // unknown circumstances (it happens on Keegan's laptop) the text // width calculation, above, returns a result that's a few pixels // too small. The container's div will be slightly too small, // but that's probably OK for our purposes. container.css({width: 38 + text_width, height: 38}); outer_container.css({display: 'block', 'white-space': 'nowrap'}); var spinner = new Spinner({ lines: 8, length: 0, width: 9, radius: 9, speed: 1.25, shadow: false, zIndex: 1000 }).spin(spinner_elem[0]); outer_container.data("spinner_obj", spinner); outer_container.data("destroying", false); // Make the spinner appear in the center of its enclosing // element. spinner.el is a 0x0 div. The parts of the spinner // are arranged so that they're centered on the upper-left corner // of spinner.el. So, by setting spinner.el's position to // relative and top/left to 50%, the center of the spinner will // be located at the center of spinner_elem. $(spinner.el).css({left: '50%', top: '50%'}); }; exports.destroy_loading_indicator = function (container) { if (container.data("destroying")) { return; } container.data("destroying", true); var spinner = container.data("spinner_obj"); if (spinner !== undefined) { spinner.stop(); } container.removeData("spinner_obj"); container.empty(); container.css({width: 0, height: 0, display: 'none'}); }; exports.show_first_run_message = function () { $('#first_run_message').show(); }; exports.destroy_first_run_message = function () { // A no-op if the element no longer exists $('#first_run_message').remove(); }; // Like C++'s std::lower_bound. Returns the first index at which // `value` could be inserted without changing the ordering. Assumes // the array is sorted. // // `first` and `last` are indices and `less` is an optionally-specified // function that returns true if // array[i] < value // for some i and false otherwise. // // Usage: lower_bound(array, value, [less]) // lower_bound(array, first, last, value, [less]) exports.lower_bound = function (array, arg1, arg2, arg3, arg4) { var first, last, value, less; if (arg3 === undefined) { first = 0; last = array.length; value = arg1; less = arg2; } else { first = arg1; last = arg2; value = arg3; less = arg4; } if (less === undefined) { less = function (a, b) { return a < b; }; } var len = last - first; var middle; var step; var lower = 0; while (len > 0) { step = Math.floor(len / 2); middle = first + step; if (less(array[middle], value, middle)) { first = middle; first++; len = len - step - 1; } else { len = step; } } return first; }; exports.same_stream_and_subject = function util_same_stream_and_subject(a, b) { // Streams and subjects are case-insensitive. return ((a.stream.toLowerCase() === b.stream.toLowerCase()) && (a.subject.toLowerCase() === b.subject.toLowerCase())); }; exports.same_major_recipient = function (a, b) { // Same behavior as same_recipient, except that it returns true for messages // on different topics but the same stream. if ((a === undefined) || (b === undefined)) { return false; } if (a.type !== b.type) { return false; } switch (a.type) { case 'private': return a.reply_to === b.reply_to; case 'stream': return a.stream.toLowerCase() === b.stream.toLowerCase(); } // should never get here return false; }; exports.same_recipient = function util_same_recipient(a, b) { if ((a === undefined) || (b === undefined)) { return false; } if (a.type !== b.type) { return false; } switch (a.type) { case 'private': return a.reply_to === b.reply_to; case 'stream': return exports.same_stream_and_subject(a, b); } // should never get here return false; }; exports.recipient_key = function (message) { if (message.type === 'stream') { return message.stream.toLowerCase() + ">" + message.subject.toLowerCase(); } else { return exports.normalize_recipients(message.reply_to); } }; exports.same_sender = function util_same_sender(a, b) { return ((a !== undefined) && (b !== undefined) && (a.sender_email === b.sender_email)); }; exports.normalize_recipients = function (recipients) { // Converts a string listing emails of message recipients // into a canonical formatting: emails sorted ASCIIbetically // with exactly one comma and no spaces between each. recipients = _.map(recipients.split(','), $.trim); recipients = _.filter(recipients, function (s) { return s.length > 0; }); recipients.sort(); return recipients.join(','); }; // Avoid URI decode errors by removing characters from the end // one by one until the decode succeeds. This makes sense if // we are decoding input that the user is in the middle of // typing. exports.robust_uri_decode = function (str) { var end = str.length; while (end > 0) { try { return decodeURIComponent(str.substring(0, end)); } catch (e) { if (!(e instanceof URIError)) { throw e; } end--; } } return ''; }; // If we can, use a locale-aware sorter. However, if the browser // doesn't support the ECMAScript Internationalization API // Specification, do a dumb string comparison because // String.localeCompare is really slow. exports.strcmp = (function () { try { var collator = new Intl.Collator(); return collator.compare; } catch (e) { } return function util_strcmp (a, b) { return (a < b ? -1 : (a > b ? 1 : 0)); }; }()); exports.escape_regexp = function (string) { // code from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions // Modified to escape the ^ to appease jslint. :/ return string.replace(/([.*+?\^=!:${}()|\[\]\/\\])/g, "\\$1"); }; exports.array_compare = function util_array_compare(a, b) { if (a.length !== b.length) { return false; } var i; for (i = 0; i < a.length; ++i) { if (a[i] !== b[i]) { return false; } } return true; }; exports.xhr_error_message = function (message, xhr) { if (xhr.status.toString().charAt(0) === "4") { // Only display the error response for 4XX, where we've crafted // a nice response. message += ": " + $.parseJSON(xhr.responseText).msg; } return message; }; /* Represents a value that is expensive to compute and should be * computed on demand and then cached. The value can be forcefully * recalculated on the next call to get() by calling reset(). * * You must supply a option to the constructor called compute_value * which should be a function that computes the uncached value. */ var unassigned_value_sentinel = {}; exports.CachedValue = function (opts) { this._value = unassigned_value_sentinel; _.extend(this, opts); }; exports.CachedValue.prototype = { get: function CachedValue_get() { if (this._value === unassigned_value_sentinel) { this._value = this.compute_value(); } return this._value; }, reset: function CachedValue_reset() { this._value = unassigned_value_sentinel; } }; exports.enforce_arity = function util_enforce_arity(func) { return function () { if (func.length !== arguments.length) { throw new Error("Function '" + func.name + "' called with " + arguments.length + " arguments, but expected " + func.length); } return func.apply(this, arguments); }; }; if (typeof $ !== 'undefined') { $.fn.expectOne = function () { if (blueslip && this.length !== 1) { blueslip.error("Expected one element in jQuery set, " + this.length + " found"); } return this; }; } exports.execute_early = function (func) { if (page_params.test_suite) { $(document).one('phantom_page_loaded', func); } else { $(func); } }; return exports; }()); if (typeof module !== 'undefined') { module.exports = util; }