2012-11-30 18:33:32 +01:00
|
|
|
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;
|
|
|
|
};
|
|
|
|
|
2013-02-15 21:31:38 +01:00
|
|
|
var favicon_selector = 'link[rel="shortcut icon"]';
|
|
|
|
|
2012-12-17 22:55:10 +01:00
|
|
|
// 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 () {
|
2013-02-15 21:31:38 +01:00
|
|
|
$(favicon_selector).detach().appendTo('head');
|
|
|
|
};
|
|
|
|
|
|
|
|
exports.set_favicon = function (url) {
|
2013-02-25 21:59:35 +01:00
|
|
|
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($('<link>')
|
|
|
|
.attr('rel', 'shortcut icon')
|
|
|
|
.attr('href', url));
|
|
|
|
}
|
2012-12-17 22:55:10 +01:00
|
|
|
};
|
|
|
|
|
2013-07-12 18:30:53 +02:00
|
|
|
exports.make_loading_indicator = function (outer_container, opts) {
|
2013-07-30 05:11:50 +02:00
|
|
|
opts = opts || {};
|
2013-07-12 18:30:53 +02:00
|
|
|
var container = outer_container;
|
2013-01-15 21:41:16 +01:00
|
|
|
container.empty();
|
2013-07-12 18:30:53 +02:00
|
|
|
|
|
|
|
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 = $('<div id="' + container_id + '_box_container"></div>');
|
|
|
|
container.append(inner_container);
|
|
|
|
container = inner_container;
|
|
|
|
inner_container = $('<div id="' + container_id + '_box"></div>');
|
|
|
|
container.append(inner_container);
|
|
|
|
container = inner_container;
|
|
|
|
}
|
|
|
|
|
2013-01-16 19:50:18 +01:00
|
|
|
var spinner_elem = $('<div class="loading_indicator_spinner"></div>');
|
2013-01-15 21:41:16 +01:00
|
|
|
container.append(spinner_elem);
|
|
|
|
var text_width = 0;
|
|
|
|
|
2013-07-10 19:33:00 +02:00
|
|
|
if (opts.text !== undefined && opts.text !== '') {
|
2013-01-16 19:50:18 +01:00
|
|
|
var text_elem = $('<span class="loading_indicator_text"></span>');
|
2013-07-10 19:33:00 +02:00
|
|
|
text_elem.text(opts.text);
|
2013-01-15 21:41:16 +01:00
|
|
|
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
|
2013-01-24 23:01:15 +01:00
|
|
|
//
|
|
|
|
// 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.
|
2013-01-15 21:41:16 +01:00
|
|
|
container.css({width: 38 + text_width,
|
2013-07-12 18:30:53 +02:00
|
|
|
height: 38});
|
|
|
|
outer_container.css({display: 'block',
|
|
|
|
'white-space': 'nowrap'});
|
2013-01-15 20:26:49 +01:00
|
|
|
|
|
|
|
var spinner = new Spinner({
|
|
|
|
lines: 8,
|
|
|
|
length: 0,
|
|
|
|
width: 9,
|
|
|
|
radius: 9,
|
|
|
|
speed: 1.25,
|
2013-01-24 23:28:49 +01:00
|
|
|
shadow: false,
|
|
|
|
zIndex: 1000
|
2013-01-15 20:26:49 +01:00
|
|
|
}).spin(spinner_elem[0]);
|
2013-07-12 18:30:53 +02:00
|
|
|
outer_container.data("spinner_obj", spinner);
|
|
|
|
outer_container.data("destroying", false);
|
2013-01-24 21:28:07 +01:00
|
|
|
|
|
|
|
// 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%'});
|
2013-01-16 19:37:50 +01:00
|
|
|
};
|
|
|
|
|
2013-01-16 19:50:18 +01:00
|
|
|
exports.destroy_loading_indicator = function (container) {
|
2013-07-10 22:16:31 +02:00
|
|
|
if (container.data("destroying")) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
container.data("destroying", true);
|
|
|
|
|
2013-01-16 19:37:50 +01:00
|
|
|
var spinner = container.data("spinner_obj");
|
|
|
|
if (spinner !== undefined) {
|
|
|
|
spinner.stop();
|
|
|
|
}
|
|
|
|
container.removeData("spinner_obj");
|
|
|
|
container.empty();
|
2013-01-16 21:25:38 +01:00
|
|
|
container.css({width: 0, height: 0, display: 'none'});
|
2013-01-15 20:26:49 +01:00
|
|
|
};
|
|
|
|
|
2013-02-12 19:28:21 +01:00
|
|
|
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();
|
|
|
|
};
|
|
|
|
|
2013-02-20 00:49:21 +01:00
|
|
|
// 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;
|
2014-03-11 20:17:33 +01:00
|
|
|
if (less(array[middle], value, middle)) {
|
2013-02-20 00:49:21 +01:00
|
|
|
first = middle;
|
|
|
|
first++;
|
|
|
|
len = len - step - 1;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
len = step;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return first;
|
|
|
|
};
|
|
|
|
|
2013-02-26 23:09:15 +01:00
|
|
|
exports.same_stream_and_subject = function util_same_stream_and_subject(a, b) {
|
2013-04-08 17:58:29 +02:00
|
|
|
// Streams and subjects are case-insensitive.
|
2013-04-18 17:13:43 +02:00
|
|
|
return ((a.stream.toLowerCase() === b.stream.toLowerCase()) &&
|
2013-02-26 23:09:15 +01:00
|
|
|
(a.subject.toLowerCase() === b.subject.toLowerCase()));
|
|
|
|
};
|
|
|
|
|
2014-02-12 21:07:22 +01:00
|
|
|
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;
|
|
|
|
};
|
|
|
|
|
2013-02-26 23:09:15 +01:00
|
|
|
exports.same_recipient = function util_same_recipient(a, b) {
|
2013-08-01 17:47:48 +02:00
|
|
|
if ((a === undefined) || (b === undefined)) {
|
2013-02-26 23:09:15 +01:00
|
|
|
return false;
|
2013-08-01 17:47:48 +02:00
|
|
|
}
|
|
|
|
if (a.type !== b.type) {
|
2013-02-26 23:09:15 +01:00
|
|
|
return false;
|
2013-08-01 17:47:48 +02:00
|
|
|
}
|
2013-02-26 23:09:15 +01:00
|
|
|
|
|
|
|
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;
|
|
|
|
};
|
|
|
|
|
2013-07-25 22:08:16 +02:00
|
|
|
exports.recipient_key = function (message) {
|
|
|
|
if (message.type === 'stream') {
|
|
|
|
return message.stream.toLowerCase() + ">" + message.subject.toLowerCase();
|
|
|
|
} else {
|
|
|
|
return exports.normalize_recipients(message.reply_to);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2013-02-26 23:09:15 +01:00
|
|
|
exports.same_sender = function util_same_sender(a, b) {
|
|
|
|
return ((a !== undefined) && (b !== undefined) &&
|
|
|
|
(a.sender_email === b.sender_email));
|
|
|
|
};
|
|
|
|
|
2013-06-17 20:31:31 +02:00
|
|
|
exports.normalize_recipients = function (recipients) {
|
2013-07-26 20:27:06 +02:00
|
|
|
// Converts a string listing emails of message recipients
|
|
|
|
// into a canonical formatting: emails sorted ASCIIbetically
|
|
|
|
// with exactly one comma and no spaces between each.
|
2013-07-29 23:35:36 +02:00
|
|
|
recipients = _.map(recipients.split(','), $.trim);
|
2013-07-29 22:49:42 +02:00
|
|
|
recipients = _.filter(recipients, function (s) { return s.length > 0; });
|
2013-06-17 20:31:31 +02:00
|
|
|
recipients.sort();
|
|
|
|
return recipients.join(',');
|
|
|
|
};
|
|
|
|
|
2013-03-28 22:32:17 +01:00
|
|
|
// 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) {
|
2013-08-01 17:47:48 +02:00
|
|
|
if (!(e instanceof URIError)) {
|
2013-03-28 22:32:17 +01:00
|
|
|
throw e;
|
2013-08-01 17:47:48 +02:00
|
|
|
}
|
2013-03-28 22:32:17 +01:00
|
|
|
end--;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return '';
|
|
|
|
};
|
|
|
|
|
2013-05-03 19:16:50 +02:00
|
|
|
// 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));
|
|
|
|
};
|
|
|
|
}());
|
|
|
|
|
2013-07-05 17:43:56 +02:00
|
|
|
exports.escape_regexp = function (string) {
|
2013-07-01 23:51:03 +02:00
|
|
|
// 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");
|
|
|
|
};
|
|
|
|
|
2013-05-07 19:07:05 +02:00
|
|
|
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;
|
|
|
|
};
|
|
|
|
|
2013-07-11 23:42:44 +02:00
|
|
|
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;
|
|
|
|
};
|
|
|
|
|
2013-08-06 21:34:43 +02:00
|
|
|
/* 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;
|
|
|
|
}
|
|
|
|
};
|
2013-07-11 23:42:44 +02:00
|
|
|
|
2013-08-08 20:22:33 +02:00
|
|
|
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);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2013-09-03 22:11:55 +02:00
|
|
|
if (typeof $ !== 'undefined') {
|
|
|
|
$.fn.expectOne = function () {
|
|
|
|
if (blueslip && this.length !== 1) {
|
|
|
|
blueslip.error("Expected one element in jQuery set, " + this.length + " found");
|
|
|
|
}
|
|
|
|
return this;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2014-03-07 00:44:15 +01:00
|
|
|
exports.execute_early = function (func) {
|
|
|
|
if (page_params.test_suite) {
|
|
|
|
$(document).one('phantom_page_loaded', func);
|
|
|
|
} else {
|
|
|
|
$(func);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2012-11-30 18:33:32 +01:00
|
|
|
return exports;
|
|
|
|
}());
|
2013-08-06 19:06:45 +02:00
|
|
|
if (typeof module !== 'undefined') {
|
|
|
|
module.exports = util;
|
|
|
|
}
|