mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-03 21:43:21 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			353 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			353 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
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($('<link>')
 | 
						|
            .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 = $('<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;
 | 
						|
    }
 | 
						|
 | 
						|
    var spinner_elem = $('<div class="loading_indicator_spinner"></div>');
 | 
						|
    container.append(spinner_elem);
 | 
						|
    var text_width = 0;
 | 
						|
 | 
						|
    if (opts.text !== undefined && opts.text !== '') {
 | 
						|
        var text_elem = $('<span class="loading_indicator_text"></span>');
 | 
						|
        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)) {
 | 
						|
            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;
 | 
						|
    };
 | 
						|
}
 | 
						|
 | 
						|
return exports;
 | 
						|
}());
 | 
						|
if (typeof module !== 'undefined') {
 | 
						|
    module.exports = util;
 | 
						|
}
 |