<commit_before>class BaseGeocoder {

    constructor(options) {

        // initialize options
        this.options = {};

        // Altering using user-provided options
        for (const property in options) {
            if (options.hasOwnProperty(property)) {
                this.options[property] = options[property];
            }
        }

        // initialize parser configurations
        this.parser_configurations = {

            'US': {
                regex: /^(\d+)?(\D+)((suite|ste|#)?\s*\d+)?$/i,
                street_components: ['number', 'street', 'unit']
            },

            'MX': {
                regex: /^(\D+)(\d+|s\/n)?(\D+\d*)?$/i,
                street_components: ['street', 'number', 'unit']
            }

        }

    }

    // parse street address
    parse_street_address(street_address, country, address) {

        // initialize address
        address = address || {};

        // parse street address
        const street = street_address.split(',');

        // load configuration based on country
        const config = this.parser_configurations[country];

        const matches = street[0].match(config['regex']);
        if (matches) {
            console.log(matches);
            const street_components = config['street_components'];
            // matches[0] is the entire matched string
            for (let i = 1; i < matches.length; i++) {
                if (matches[i] !== undefined)
                    address[street_components[i - 1]] = matches[i].trim();
            }
        }

        return address;
    }

    reverse(location, callback, options) {
        throw new Error("Not implemented!");
    }

    geocode(location, callback, options) {
        throw new Error("Not implemented!");
    }

}

export class GeocoderMapBox extends BaseGeocoder {

    constructor(options) {

        // add query default parameters
        options['types'] = 'address';
        options['limit'] = 1;

        // call super
        super(options);

    }

    parse_response(response) {

        // retrieve features
        const features = response.features;
        if (features.length == 0)
            return null;

        // parse first feature
        const feature = features[0];
        if (feature['place_type'] == 'address') {

            let address = {
                street_address: "",
                number: "",
                street: "",
                unit: undefined,
                neighborhood: undefined,
                city: "",
                state: "",
                zipcode: undefined,
                country: "",
                location: undefined
            };

            // set location
            address['location'] = {
                'latitude': feature['center'][1],
                'longitude': feature['center'][0]
            };

            // parse context
            const context = feature['context'];
            for (let i = 0; i < context.length; i++) {
                const item = context[i];
                const id = item['id'];
                if (id.startsWith('neighborhood'))
                    address['neighborhood'] = item['text'];
                else if (id.startsWith('postcode'))
                    address['zipcode'] = item['text'];
                else if (id.startsWith('place'))
                    address['city'] = item['text'];
                else if (id.startsWith('region'))
                    address['state'] = item['short_code'].toUpperCase().substr(3);
                else if (id.startsWith('country'))
                    address['country'] = item['short_code'].toUpperCase();
            }

            // set street_address
            const street_address = feature['place_name'];
            address['street_address'] = street_address;

            // parse street address
            return this.parse_street_address(street_address,
                address['country'],
                address);

        }

        // log error
        console.log("Does not know how to parse feature of type '" + feature['place_type'] + "'");

        return null;
    }

    reverse(location, callback, options) {

        options = options || {};

        let property;
        let url = "https://api.mapbox.com/geocoding/v5/mapbox.places/";

        const parameters = this.options;

        // Start with class options
        for (property in this.options) {
            if (this.options.hasOwnProperty(property)) {
                parameters[property] = this.options[property];
            }
        }

        // Altering using user-provided options
        for (property in options) {
            if (options.hasOwnProperty(property)) {
                parameters[property] = options[property];
            }
        }

        // construct query
        url += location.lng + ',' + location.lat + ".json";

        // add parameters
        let prefix = '?';
        for (const option in parameters) {
            if (parameters.hasOwnProperty(option)) {

                // add options
                url += prefix
                    + encodeURIComponent(option)
                    + "="
                    + encodeURIComponent(parameters[option]);
                prefix = '&';

            }
        }

        console.log("geocode url = '" + url + "'");

        // query mapbox
        $.getJSON(url,
            (response) => {

                // parse results
                const address = this.parse_response(response);

                console.log(response);
                console.log(address);

                // callback
                if (callback)
                    callback(address, response);

            })
            .fail(
                (jqxhr, textStatus, error) => {

                    if (callback)
                        callback(null, {error: error});
                    else
                        console.log("Could not reverse geocode:" +
                            textStatus + "," + error + "\n");

                });

    }

    geocode(query, callback, options) {

        options = options || { autocomplete: 'true' };

        let property;
        let url = "https://api.mapbox.com/geocoding/v5/mapbox.places/";

        const parameters = this.options;

        // Start with class options
        for (property in this.options) {
            if (this.options.hasOwnProperty(property)) {
                parameters[property] = this.options[property];
            }
        }

        // Altering using user-provided options
        for (property in options) {
            if (options.hasOwnProperty(property)) {
                parameters[property] = options[property];
            }
        }

        // construct query
        url += encodeURIComponent(query) + ".json";

        // add parameters
        let prefix = '?';
        for (const option in parameters) {
            if (parameters.hasOwnProperty(option)) {

                // add options
                url += prefix
                    + encodeURIComponent(option)
                    + "="
                    + encodeURIComponent(parameters[option]);
                prefix = '&';

            }
        }

        console.log("geocode url = '" + url + "'");

        // query mapbox
        $.getJSON(url,
            (response) =>  {

                // parse results
                const address = this.parse_response(response);

                console.log(response);
                console.log(address);

                // callback
                if (callback)
                    callback(address, response);

            })
            .fail(
                (jqxhr, textStatus, error) => {

                    if (callback)
                        callback(null, {error: error});
                    else
                        console.log("Could not forward geocode:" +
                            textStatus + "," + error + "\n");

                });

    }

}

export class GeocoderGoogle extends BaseGeocoder {

}

/**
 * @return {null}
 */
export function GeocoderFactory(map_provider, options) {

    // default empty options
    options = options || {};

    // Retrieve MapBox access_token
    const provider = map_provider['provider'];
    if (provider === 'mapbox') {
        // add access token
        options['access_token'] = map_provider['access_token'];
        return new GeocoderMapBox(options);
    } else if (provider === 'google') {
        return new GeocoderGoogle(options);
    } else
        return null;

}

<commit_msg>Refactoring Geocoder to multiple providers.<commit_after>class BaseGeocoder {

    constructor(options) {

        // initialize options
        this.options = {};

        // Altering using user-provided options
        for (const property in options) {
            if (options.hasOwnProperty(property)) {
                this.options[property] = options[property];
            }
        }

        // initialize parser configurations
        this.parser_configurations = {

            'US': {
                regex: /^(\d+)?(\D+)((suite|ste|#)?\s*\d+)?$/i,
                street_components: ['number', 'street', 'unit']
            },

            'MX': {
                regex: /^(\D+)(\d+|s\/n)?(\D+\d*)?$/i,
                street_components: ['street', 'number', 'unit']
            }

        }

    }

    // parse street address
    parse_street_address(street_address, country, address) {

        // initialize address
        address = address || {};

        // parse street address
        const street = street_address.split(',');

        // load configuration based on country
        const config = this.parser_configurations[country];

        const matches = street[0].match(config['regex']);
        if (matches) {
            console.log(matches);
            const street_components = config['street_components'];
            // matches[0] is the entire matched string
            for (let i = 1; i < matches.length; i++) {
                if (matches[i] !== undefined)
                    address[street_components[i - 1]] = matches[i].trim();
            }
        }

        return address;
    }

    reverse(location, callback, options) {
        throw new Error("Not implemented!");
    }

    geocode(location, callback, options) {
        throw new Error("Not implemented!");
    }

}

export class GeocoderMapBox extends BaseGeocoder {

    constructor(options) {

        // add query default parameters
        options['types'] = 'address';
        options['limit'] = 1;

        // call super
        super(options);

    }

    parse_response(response) {

        // retrieve features
        const features = response.features;
        if (features.length == 0)
            return null;

        // parse first feature
        const feature = features[0];
        if (feature['place_type'] == 'address') {

            let address = {
                street_address: "",
                number: "",
                street: "",
                unit: undefined,
                neighborhood: undefined,
                city: "",
                state: "",
                zipcode: undefined,
                country: "",
                location: undefined
            };

            // set location
            address['location'] = {
                'latitude': feature['center'][1],
                'longitude': feature['center'][0]
            };

            // parse context
            const context = feature['context'];
            for (let i = 0; i < context.length; i++) {
                const item = context[i];
                const id = item['id'];
                if (id.startsWith('neighborhood'))
                    address['neighborhood'] = item['text'];
                else if (id.startsWith('postcode'))
                    address['zipcode'] = item['text'];
                else if (id.startsWith('place'))
                    address['city'] = item['text'];
                else if (id.startsWith('region'))
                    address['state'] = item['short_code'].toUpperCase().substr(3);
                else if (id.startsWith('country'))
                    address['country'] = item['short_code'].toUpperCase();
            }

            // set street_address
            const street_address = feature['place_name'];
            address['street_address'] = street_address;

            // parse street address
            return this.parse_street_address(street_address,
                address['country'],
                address);

        }

        // log error
        console.log("Does not know how to parse feature of type '" + feature['place_type'] + "'");

        return null;
    }

    reverse(location, callback, options) {

        options = options || {};

        let property;
        let url = "https://api.mapbox.com/geocoding/v5/mapbox.places/";

        const parameters = this.options;

        // Start with class options
        for (property in this.options) {
            if (this.options.hasOwnProperty(property)) {
                parameters[property] = this.options[property];
            }
        }

        // Altering using user-provided options
        for (property in options) {
            if (options.hasOwnProperty(property)) {
                parameters[property] = options[property];
            }
        }

        // construct query
        url += location.lng + ',' + location.lat + ".json";

        // add parameters
        let prefix = '?';
        for (const option in parameters) {
            if (parameters.hasOwnProperty(option)) {

                // add options
                url += prefix
                    + encodeURIComponent(option)
                    + "="
                    + encodeURIComponent(parameters[option]);
                prefix = '&';

            }
        }

        console.log("geocode url = '" + url + "'");

        // query mapbox
        $.getJSON(url,
            (response) => {

                // parse results
                const address = this.parse_response(response);

                console.log(response);
                console.log(address);

                // callback
                if (callback)
                    callback(address, response);

            })
            .fail(
                (jqxhr, textStatus, error) => {

                    if (callback)
                        callback(null, {error: error});
                    else
                        console.log("Could not reverse geocode:" +
                            textStatus + "," + error + "\n");

                });

    }

    geocode(query, callback, options) {

        options = options || { autocomplete: 'true' };

        let property;
        let url = "https://api.mapbox.com/geocoding/v5/mapbox.places/";

        const parameters = this.options;

        // Start with class options
        for (property in this.options) {
            if (this.options.hasOwnProperty(property)) {
                parameters[property] = this.options[property];
            }
        }

        // Altering using user-provided options
        for (property in options) {
            if (options.hasOwnProperty(property)) {
                parameters[property] = options[property];
            }
        }

        // construct query
        url += encodeURIComponent(query) + ".json";

        // add parameters
        let prefix = '?';
        for (const option in parameters) {
            if (parameters.hasOwnProperty(option)) {

                // add options
                url += prefix
                    + encodeURIComponent(option)
                    + "="
                    + encodeURIComponent(parameters[option]);
                prefix = '&';

            }
        }

        console.log("geocode url = '" + url + "'");

        // query mapbox
        $.getJSON(url,
            (response) =>  {

                // parse results
                const address = this.parse_response(response);

                console.log(response);
                console.log(address);

                // callback
                if (callback)
                    callback(address, response);

            })
            .fail(
                (jqxhr, textStatus, error) => {

                    if (callback)
                        callback(null, {error: error});
                    else
                        console.log("Could not forward geocode:" +
                            textStatus + "," + error + "\n");

                });

    }

}

export class GeocoderGoogle extends BaseGeocoder {

    constructor(options) {

        // add query default parameters
        options['types'] = 'address';
        options['limit'] = 1;

        // call super
        super(options);

    }

    parse_response(response) {

        // retrieve features
        const results = response.results;
        if (results.length == 0)
            return null;

        // parse first feature
        const feature = results[0];
        if ('address_components' in feature['types']) {

            let address = {
                street_address: "",
                number: "",
                street: "",
                unit: undefined,
                neighborhood: undefined,
                city: "",
                state: "",
                zipcode: undefined,
                country: "",
                location: undefined
            };

            // set location
            const location = feature['geometry']['location'];
            address['location'] = {
                'latitude': location['lat'],
                'longitude': location['lng']
            };

            // parse context
            const context = feature['address_components'];
            for (let i = 0; i < context.length; i++) {
                const item = context[i];
                const types = item['types'];
                if ('neighborhood' in types)
                    address['neighborhood'] = item['long_name'];
                else if ('street_number' in types)
                    address['number'] = item['short_name'];
                else if ('route' in types)
                    address['street_address'] = item['long_name'];
                else if ('locality' in types)
                    address['city'] = item['long_name'];
                else if ('admnistrative_area_level_1' in types)
                    address['state'] = item['short_name'];
                else if ('postal_code' in types)
                    address['zipcode'] = item['short_name'];
                else if ('country' in types)
                    address['country'] = item['short_name'].toUpperCase();
            }

            return address;

        }

        // log error
        console.log("Does not know how to parse feature of type '" + feature['place_type'] + "'");

        return null;
    }

    reverse(location, callback, options) {

        options = options || {};

        let property;
        let url = "https://maps.googleapis.com/maps/api/geocode/json";

        const parameters = this.options;

        // Start with class options
        for (property in this.options) {
            if (this.options.hasOwnProperty(property)) {
                parameters[property] = this.options[property];
            }
        }

        // Altering using user-provided options
        for (property in options) {
            if (options.hasOwnProperty(property)) {
                parameters[property] = options[property];
            }
        }

        // construct query
        parameters['latlgn'] = location.lat + ',' + location.lng;

        // add parameters
        let prefix = '?';
        for (const option in parameters) {
            if (parameters.hasOwnProperty(option)) {

                // add options
                url += prefix
                    + encodeURIComponent(option)
                    + "="
                    + encodeURIComponent(parameters[option]);
                prefix = '&';

            }
        }

        console.log("geocode url = '" + url + "'");

        // query mapbox
        $.getJSON(url,
            (response) => {

                // parse results
                const address = this.parse_response(response);

                console.log(response);
                console.log(address);

                // callback
                if (callback)
                    callback(address, response);

            })
            .fail(
                (jqxhr, textStatus, error) => {

                    if (callback)
                        callback(null, {error: error});
                    else
                        console.log("Could not reverse geocode:" +
                            textStatus + "," + error + "\n");

                });

    }

    geocode(query, callback, options) {

        options = options || { autocomplete: 'true' };

        let property;
        let url = "https://api.mapbox.com/geocoding/v5/mapbox.places/";

        const parameters = this.options;

        // Start with class options
        for (property in this.options) {
            if (this.options.hasOwnProperty(property)) {
                parameters[property] = this.options[property];
            }
        }

        // Altering using user-provided options
        for (property in options) {
            if (options.hasOwnProperty(property)) {
                parameters[property] = options[property];
            }
        }

        // construct query
        parameters['address'] = encodeURIComponent(query);

        // add parameters
        let prefix = '?';
        for (const option in parameters) {
            if (parameters.hasOwnProperty(option)) {

                // add options
                url += prefix
                    + encodeURIComponent(option)
                    + "="
                    + encodeURIComponent(parameters[option]);
                prefix = '&';

            }
        }

        console.log("geocode url = '" + url + "'");

        // query mapbox
        $.getJSON(url,
            (response) =>  {

                // parse results
                const address = this.parse_response(response);

                console.log(response);
                console.log(address);

                // callback
                if (callback)
                    callback(address, response);

            })
            .fail(
                (jqxhr, textStatus, error) => {

                    if (callback)
                        callback(null, {error: error});
                    else
                        console.log("Could not forward geocode:" +
                            textStatus + "," + error + "\n");

                });

    }

}

/**
 * @return {null}
 */
export function GeocoderFactory(map_provider, options) {

    // default empty options
    options = options || {};

    // Retrieve MapBox access_token
    const provider = map_provider['provider'];
    if (provider === 'mapbox') {
        // add access token
        options['access_token'] = map_provider['access_token'];
        return new GeocoderMapBox(options);
    } else if (provider === 'google') {
        // add access token
        options['key'] = map_provider['access_token'];
        return new GeocoderGoogle(options);
    } else
        return null;

}

<|endoftext|>


<commit_before>let is_open = false;
// the asset map is a map of all retrieved images and YouTube videos that are
// memoized instead of being looked up multiple times.
const asset_map = new Map();

function render_lightbox_list_images(preview_source) {
    if (!is_open) {
        const images = Array.prototype.slice.call($(".focused_table .message_inline_image img"));
        const $image_list = $("#lightbox_overlay .image-list").html("");

        images.forEach(function (img) {
            const src = img.getAttribute("src");
            const className = preview_source === src ? "image selected" : "image";

            const node = $("

", {
                class: className,
                "data-src": src,
            }).css({ backgroundImage: "url(" + src + ")"});

            $image_list.append(node);

            // We parse the data for each image to show in the list,
            // while we still have its original DOM element handy, so
            // that navigating within the list only needs the `src`
            // attribute used to construct the node object above.
            exports.parse_image_data(img);
        }, "");
    }
}

function display_image(payload, options) {
    render_lightbox_list_images(payload.preview);

    $(".player-container").hide();
    $(".image-actions, .image-description, .download, .lightbox-canvas-trigger").show();

    if (options.lightbox_canvas === true) {
        const canvas = document.createElement("canvas");
        canvas.setAttribute("data-src", payload.source);

        $("#lightbox_overlay .image-preview").html(canvas).show();
        const photo = new LightboxCanvas(canvas);
        photo.speed(2.3);
    } else {
        const img = new Image();
        img.src = payload.source;

        $("#lightbox_overlay .image-preview").html(img).show();
    }

    $(".image-description .title").text(payload.title || "N/A");
    $(".image-description .user").text(payload.user);

    $(".image-actions .open, .image-actions .download").attr("href", payload.source);
}

function display_video(payload) {
    render_lightbox_list_images(payload.preview);

    $("#lightbox_overlay .image-preview, .image-description, .download, .lightbox-canvas-trigger").hide();

    let source;
    if (payload.type === "youtube-video") {
        source = "https://www.youtube.com/embed/" + payload.source;
    } else if (payload.type === "vimeo-video") {
        source = "https://player.vimeo.com/video/" + payload.source;
    } else if (payload.type === "embed-video") {
        // Use data: to load the player in a unique origin for security.
        source = "data:text/html," + window.encodeURIComponent(
            "<!DOCTYPE html>" +
                payload.source
        );
    }

    const iframe = $("");
    iframe.attr("sandbox", "allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts");
    iframe.attr("src", source);
    iframe.attr("frameborder", 0);
    iframe.attr("allowfullscreen", true);

    $("#lightbox_overlay .player-container").html(iframe).show();
    $(".image-actions .open").attr("href", payload.url);
}

// the image param is optional, but required on the first preview of an image.
// this will likely be passed in every time but just ignored if the result is already
// stored in the `asset_map`.
exports.open = function (image, options) {
    if (!options) {
        options = {
            // default to showing standard images.
            lightbox_canvas: $(".lightbox-canvas-trigger").hasClass("enabled"),
        };
    }

    const $image = $(image);

    // if the asset_map already contains the metadata required to display the
    // asset, just recall that metadata.
    let $preview_src = $image.attr("src");
    let payload = asset_map.get($preview_src);
    if (payload === undefined) {
        if ($preview_src.endsWith("&size=full")) {
            // while fetching an image for canvas, `src` attribute supplies
            // full-sized image instead of thumbnail, so we have to replace
            // `size=full` with `size=thumbnail`.
            //
            // TODO: This is a hack to work around the fact that for
            // the lightbox canvas, the `src` is the data-fullsize-src
            // for the image, not the original thumbnail used to open
            // the lightbox.  A better fix will be to check a
            // `data-thumbnail-src` attribute that we add to the
            // canvas elements.
            $preview_src = $preview_src.replace(/.{4}$/, "thumbnail");
            payload = asset_map.get($preview_src);
        }
        if (payload === undefined) {
            payload = exports.parse_image_data($image);
        }
    }

    if (payload.type.match("-video")) {
        display_video(payload);
    } else if (payload.type === "image") {
        display_image(payload, options);
    }

    if (is_open) {
        return;
    }

    function lightbox_close_overlay() {
        $(".player-container iframe").remove();
        is_open = false;
        document.activeElement.blur();
    }

    overlays.open_overlay({
        name: 'lightbox',
        overlay: $("#lightbox_overlay"),
        on_close: lightbox_close_overlay,
    });

    popovers.hide_all();
    is_open = true;
};

exports.show_from_selected_message = function () {
    const $message_selected = $(".selected_message");
    let $message = $message_selected;
    let $image = $message.find(".message_inline_image img");
    let $prev_traverse = false;

    while ($image.length === 0) {
        if ($message.prev().length === 0) {
            $message = $message.parent().prev();
            if ($message.length === 0) {
                $prev_traverse = true;
                $message = $message_selected;
                break;
            } else {
                $message = $message.find(".last_message");
                continue;
            }
        }
        $message = $message.prev();
        $image = $message.find(".message_inline_image img");
    }

    if ($prev_traverse) {
        while ($image.length === 0) {
            if ($message.next().length === 0) {
                $message = $message.parent().next();
                if ($message.length === 0) {
                    break;
                } else {
                    $message = $message.children().first();
                    continue;
                }
            }
            $message = $message.next();
            $image = $message.find(".message_inline_image img");
        }
    }

    if ($image.length !== 0) {
        exports.open($image);
    }
};

// retrieve the metadata from the DOM and store into the asset_map.
exports.parse_image_data = function (image) {
    const $image = $(image);
    const $preview_src = $image.attr("src");

    if (asset_map.has($preview_src)) {
        // check if image's data is already present in asset_map.
        return;
    }

    // if wrapped in the .youtube-video class, it will be length = 1, and therefore
    // cast to true.
    const is_youtube_video = !!$image.closest(".youtube-video").length;
    const is_vimeo_video = !!$image.closest(".vimeo-video").length;
    const is_embed_video = !!$image.closest(".embed-video").length;

    // check if image is descendent of #preview_content
    const is_compose_preview_image = $image.closest("#preview_content").length === 1;

    const $parent = $image.parent();
    let $type;
    let $source;
    const $url = $parent.attr("href");
    if (is_youtube_video) {
        $type = "youtube-video";
        $source = $parent.attr("data-id");
    } else if (is_vimeo_video) {
        $type = "vimeo-video";
        $source = $parent.attr("data-id");
    } else if (is_embed_video) {
        $type = "embed-video";
        $source = $parent.attr("data-id");
    } else {
        $type = "image";
        // thumbor supplies the src as thumbnail, data-src-fullsize as full-sized.
        if ($image.attr("data-src-fullsize")) {
            $source = $image.attr("data-src-fullsize");
        } else {
            $source = $preview_src;
        }
    }
    let sender_full_name;
    if (is_compose_preview_image) {
        sender_full_name = people.my_full_name();
    } else {
        const $message = $parent.closest("[zid]");
        const zid = rows.id($message);
        const message = message_store.get(zid);
        if (message === undefined) {
            blueslip.error("Lightbox for unknown message " + zid);
        } else {
            sender_full_name = message.sender_full_name;
        }
    }
    const payload = {
        user: sender_full_name,
        title: $parent.attr("title"),
        type: $type,
        preview: $preview_src,
        source: $source,
        url: $url,
    };

    asset_map.set($preview_src, payload);
    return payload;
};

exports.prev = function () {
    $(".image-list .image.selected").prev().click();
};

exports.next = function () {
    $(".image-list .image.selected").next().click();
};

// this is a block of events that are required for the lightbox to work.
exports.initialize = function () {
    $("#main_div, #preview_content").on("click", ".message_inline_image a", function (e) {
        // prevent the link from opening in a new page.
        e.preventDefault();
        // prevent the message compose dialog from happening.
        e.stopPropagation();
        const $img = $(this).find("img");
        exports.open($img);
    });

    $("#lightbox_overlay .download").click(function () {
        this.blur();
    });

    $("#lightbox_overlay").on("click", ".image-list .image", function () {
        const $image_list = $(this).parent();
        const original_image = $(".message_row img[src='" + $(this).attr('data-src') + "']");

        exports.open(original_image);

        $(".image-list .image.selected").removeClass("selected");
        $(this).addClass("selected");

        const parentOffset = this.parentNode.clientWidth + this.parentNode.scrollLeft;
        // this is the left and right of the image compared to its parent.
        const coords = {
            left: this.offsetLeft,
            right: this.offsetLeft + this.clientWidth,
        };

        if (coords.right > parentOffset) {
            // add 2px margin
            $image_list.animate({
                scrollLeft: coords.right - this.parentNode.clientWidth + 2,
            }, 100);
        } else if (coords.left < this.parentNode.scrollLeft) {
            // subtract 2px margin
            $image_list.animate({ scrollLeft: coords.left - 2 }, 100);
        }
    });

    $("#lightbox_overlay").on("click", ".center .arrow", function () {
        const direction = $(this).attr("data-direction");

        if (direction === 'next') {
            exports.next();
        } else if (direction === 'prev') {
            exports.prev();
        }
    });

    $("#lightbox_overlay").on("click", ".lightbox-canvas-trigger", function () {
        let $img = $("#lightbox_overlay").find(".image-preview img");

        if ($img.length) {
            $(this).addClass("enabled");
            // the `lightbox.open` function will see the enabled class and
            // enable the `LightboxCanvas` class.
            exports.open($img);
        } else {
            $img = $($("#lightbox_overlay").find(".image-preview canvas")[0].image);

            $(this).removeClass("enabled");
            exports.open($img);
        }
    });

    $("#lightbox_overlay .image-preview").on("dblclick", "img, canvas", function (e) {
        $("#lightbox_overlay .lightbox-canvas-trigger").click();
        e.preventDefault();
    });

    $("#lightbox_overlay .player-container").on("click", function () {
        if ($(this).is(".player-container")) {
            overlays.close_active();
        }
    });

    $("#lightbox_overlay").on("click", ".image-info-wrapper, .center", function (e) {
        if ($(e.target).is(".image-info-wrapper, .center")) {
            overlays.close_overlay("lightbox");
        }
    });
};

window.lightbox = exports;
<commit_msg>lightbox: Remove redundant conversion of `image` to jQuery object.<commit_after>let is_open = false;
// the asset map is a map of all retrieved images and YouTube videos that are
// memoized instead of being looked up multiple times.
const asset_map = new Map();

function render_lightbox_list_images(preview_source) {
    if (!is_open) {
        const images = Array.prototype.slice.call($(".focused_table .message_inline_image img"));
        const $image_list = $("#lightbox_overlay .image-list").html("");

        images.forEach(function (img) {
            const src = img.getAttribute("src");
            const className = preview_source === src ? "image selected" : "image";

            const node = $("

", {
                class: className,
                "data-src": src,
            }).css({ backgroundImage: "url(" + src + ")"});

            $image_list.append(node);

            // We parse the data for each image to show in the list,
            // while we still have its original DOM element handy, so
            // that navigating within the list only needs the `src`
            // attribute used to construct the node object above.
            exports.parse_image_data(img);
        }, "");
    }
}

function display_image(payload, options) {
    render_lightbox_list_images(payload.preview);

    $(".player-container").hide();
    $(".image-actions, .image-description, .download, .lightbox-canvas-trigger").show();

    if (options.lightbox_canvas === true) {
        const canvas = document.createElement("canvas");
        canvas.setAttribute("data-src", payload.source);

        $("#lightbox_overlay .image-preview").html(canvas).show();
        const photo = new LightboxCanvas(canvas);
        photo.speed(2.3);
    } else {
        const img = new Image();
        img.src = payload.source;

        $("#lightbox_overlay .image-preview").html(img).show();
    }

    $(".image-description .title").text(payload.title || "N/A");
    $(".image-description .user").text(payload.user);

    $(".image-actions .open, .image-actions .download").attr("href", payload.source);
}

function display_video(payload) {
    render_lightbox_list_images(payload.preview);

    $("#lightbox_overlay .image-preview, .image-description, .download, .lightbox-canvas-trigger").hide();

    let source;
    if (payload.type === "youtube-video") {
        source = "https://www.youtube.com/embed/" + payload.source;
    } else if (payload.type === "vimeo-video") {
        source = "https://player.vimeo.com/video/" + payload.source;
    } else if (payload.type === "embed-video") {
        // Use data: to load the player in a unique origin for security.
        source = "data:text/html," + window.encodeURIComponent(
            "<!DOCTYPE html>" +
                payload.source
        );
    }

    const iframe = $("");
    iframe.attr("sandbox", "allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts");
    iframe.attr("src", source);
    iframe.attr("frameborder", 0);
    iframe.attr("allowfullscreen", true);

    $("#lightbox_overlay .player-container").html(iframe).show();
    $(".image-actions .open").attr("href", payload.url);
}

// the image param is optional, but required on the first preview of an image.
// this will likely be passed in every time but just ignored if the result is already
// stored in the `asset_map`.
exports.open = function ($image, options) {
    if (!options) {
        options = {
            // default to showing standard images.
            lightbox_canvas: $(".lightbox-canvas-trigger").hasClass("enabled"),
        };
    }

    // if the asset_map already contains the metadata required to display the
    // asset, just recall that metadata.
    let $preview_src = $image.attr("src");
    let payload = asset_map.get($preview_src);
    if (payload === undefined) {
        if ($preview_src.endsWith("&size=full")) {
            // while fetching an image for canvas, `src` attribute supplies
            // full-sized image instead of thumbnail, so we have to replace
            // `size=full` with `size=thumbnail`.
            //
            // TODO: This is a hack to work around the fact that for
            // the lightbox canvas, the `src` is the data-fullsize-src
            // for the image, not the original thumbnail used to open
            // the lightbox.  A better fix will be to check a
            // `data-thumbnail-src` attribute that we add to the
            // canvas elements.
            $preview_src = $preview_src.replace(/.{4}$/, "thumbnail");
            payload = asset_map.get($preview_src);
        }
        if (payload === undefined) {
            payload = exports.parse_image_data($image);
        }
    }

    if (payload.type.match("-video")) {
        display_video(payload);
    } else if (payload.type === "image") {
        display_image(payload, options);
    }

    if (is_open) {
        return;
    }

    function lightbox_close_overlay() {
        $(".player-container iframe").remove();
        is_open = false;
        document.activeElement.blur();
    }

    overlays.open_overlay({
        name: 'lightbox',
        overlay: $("#lightbox_overlay"),
        on_close: lightbox_close_overlay,
    });

    popovers.hide_all();
    is_open = true;
};

exports.show_from_selected_message = function () {
    const $message_selected = $(".selected_message");
    let $message = $message_selected;
    let $image = $message.find(".message_inline_image img");
    let $prev_traverse = false;

    while ($image.length === 0) {
        if ($message.prev().length === 0) {
            $message = $message.parent().prev();
            if ($message.length === 0) {
                $prev_traverse = true;
                $message = $message_selected;
                break;
            } else {
                $message = $message.find(".last_message");
                continue;
            }
        }
        $message = $message.prev();
        $image = $message.find(".message_inline_image img");
    }

    if ($prev_traverse) {
        while ($image.length === 0) {
            if ($message.next().length === 0) {
                $message = $message.parent().next();
                if ($message.length === 0) {
                    break;
                } else {
                    $message = $message.children().first();
                    continue;
                }
            }
            $message = $message.next();
            $image = $message.find(".message_inline_image img");
        }
    }

    if ($image.length !== 0) {
        exports.open($image);
    }
};

// retrieve the metadata from the DOM and store into the asset_map.
exports.parse_image_data = function (image) {
    const $image = $(image);
    const $preview_src = $image.attr("src");

    if (asset_map.has($preview_src)) {
        // check if image's data is already present in asset_map.
        return;
    }

    // if wrapped in the .youtube-video class, it will be length = 1, and therefore
    // cast to true.
    const is_youtube_video = !!$image.closest(".youtube-video").length;
    const is_vimeo_video = !!$image.closest(".vimeo-video").length;
    const is_embed_video = !!$image.closest(".embed-video").length;

    // check if image is descendent of #preview_content
    const is_compose_preview_image = $image.closest("#preview_content").length === 1;

    const $parent = $image.parent();
    let $type;
    let $source;
    const $url = $parent.attr("href");
    if (is_youtube_video) {
        $type = "youtube-video";
        $source = $parent.attr("data-id");
    } else if (is_vimeo_video) {
        $type = "vimeo-video";
        $source = $parent.attr("data-id");
    } else if (is_embed_video) {
        $type = "embed-video";
        $source = $parent.attr("data-id");
    } else {
        $type = "image";
        // thumbor supplies the src as thumbnail, data-src-fullsize as full-sized.
        if ($image.attr("data-src-fullsize")) {
            $source = $image.attr("data-src-fullsize");
        } else {
            $source = $preview_src;
        }
    }
    let sender_full_name;
    if (is_compose_preview_image) {
        sender_full_name = people.my_full_name();
    } else {
        const $message = $parent.closest("[zid]");
        const zid = rows.id($message);
        const message = message_store.get(zid);
        if (message === undefined) {
            blueslip.error("Lightbox for unknown message " + zid);
        } else {
            sender_full_name = message.sender_full_name;
        }
    }
    const payload = {
        user: sender_full_name,
        title: $parent.attr("title"),
        type: $type,
        preview: $preview_src,
        source: $source,
        url: $url,
    };

    asset_map.set($preview_src, payload);
    return payload;
};

exports.prev = function () {
    $(".image-list .image.selected").prev().click();
};

exports.next = function () {
    $(".image-list .image.selected").next().click();
};

// this is a block of events that are required for the lightbox to work.
exports.initialize = function () {
    $("#main_div, #preview_content").on("click", ".message_inline_image a", function (e) {
        // prevent the link from opening in a new page.
        e.preventDefault();
        // prevent the message compose dialog from happening.
        e.stopPropagation();
        const $img = $(this).find("img");
        exports.open($img);
    });

    $("#lightbox_overlay .download").click(function () {
        this.blur();
    });

    $("#lightbox_overlay").on("click", ".image-list .image", function () {
        const $image_list = $(this).parent();
        const original_image = $(".message_row img[src='" + $(this).attr('data-src') + "']");

        exports.open(original_image);

        $(".image-list .image.selected").removeClass("selected");
        $(this).addClass("selected");

        const parentOffset = this.parentNode.clientWidth + this.parentNode.scrollLeft;
        // this is the left and right of the image compared to its parent.
        const coords = {
            left: this.offsetLeft,
            right: this.offsetLeft + this.clientWidth,
        };

        if (coords.right > parentOffset) {
            // add 2px margin
            $image_list.animate({
                scrollLeft: coords.right - this.parentNode.clientWidth + 2,
            }, 100);
        } else if (coords.left < this.parentNode.scrollLeft) {
            // subtract 2px margin
            $image_list.animate({ scrollLeft: coords.left - 2 }, 100);
        }
    });

    $("#lightbox_overlay").on("click", ".center .arrow", function () {
        const direction = $(this).attr("data-direction");

        if (direction === 'next') {
            exports.next();
        } else if (direction === 'prev') {
            exports.prev();
        }
    });

    $("#lightbox_overlay").on("click", ".lightbox-canvas-trigger", function () {
        let $img = $("#lightbox_overlay").find(".image-preview img");

        if ($img.length) {
            $(this).addClass("enabled");
            // the `lightbox.open` function will see the enabled class and
            // enable the `LightboxCanvas` class.
            exports.open($img);
        } else {
            $img = $($("#lightbox_overlay").find(".image-preview canvas")[0].image);

            $(this).removeClass("enabled");
            exports.open($img);
        }
    });

    $("#lightbox_overlay .image-preview").on("dblclick", "img, canvas", function (e) {
        $("#lightbox_overlay .lightbox-canvas-trigger").click();
        e.preventDefault();
    });

    $("#lightbox_overlay .player-container").on("click", function () {
        if ($(this).is(".player-container")) {
            overlays.close_active();
        }
    });

    $("#lightbox_overlay").on("click", ".image-info-wrapper, .center", function (e) {
        if ($(e.target).is(".image-info-wrapper, .center")) {
            overlays.close_overlay("lightbox");
        }
    });
};

window.lightbox = exports;
<|endoftext|>


<commit_before>function toggleDetails(elem) {
    // Get the "details"-table corresponding to the clicked element
    var e = document.getElementById(elem.id + '_details');

    // Toggle the visibility
    if(e.style.display == 'block') {
        e.style.display = 'none';
    } else {
        e.style.display = 'block';
    }
}

function setEventHandlers() {
    // Define labels
    var name_label = 'Task name';
    var estimate_label = 'Estimated duration (e.g. 1d 5h 30m)';

    // Define colors
    var active_color = '#000000';
    var inactive_color = '#cbcbcb';

    // Set properties of the "name"-input field
    var name_field = document.getElementById('name_field');
    name_field.value = name_label;
    name_field.style.color = inactive_color;
    name_field.onfocus = function() { handleFocusEvent(name_field, name_label, active_color); };

    // Properties of the "duration"-input field
    var estimate_field = document.getElementById('estimate_field');
    estimate_field.value = estimate_label;
    estimate_field.style.color = inactive_color;
    estimate_field.onfocus = function() { handleFocusEvent(estimate_field, estimate_label, active_color); };
}

function handleFocusEvent(elem, label, color) {
    // Clear field if description is still shown
    if (elem.value == label) {
        elem.value = '';
    }

    // Change color
    elem.style.color = color;
}

function updateTimers() {
    var tasks = document.getElementsByClassName('task in_progress');
    if (tasks.length > 0) {
        for (var i = 0; i < tasks.length; i++) {
            var elem = tasks[i].getElementsByClassName('duration');
            var s = elem[0].innerHTML.split(':');
            var hours = parseInt(s[0], 10);
            var mins = parseInt(s[1]);
            var secs = parseInt(s[2]);

            secs += 1;
            if (secs >= 60) {
                secs -= 60;
                mins += 1;
            }

            if (mins >= 60) {
                mins -= 60;
                hours += 1;
            }

            var hours_str = hours.toString();
            var mins_str = '';
            var secs_str = '';

            if (mins < 10) {
                mins_str = '0';
            }
            mins_str += mins.toString();

            if (secs < 10) {
                secs_str = '0';
            }
            secs_str += secs.toString();

            elem[0].innerHTML = hours_str + ':' + mins_str + ':' + secs_str;
        }
    }
}

function listSetup() {
    // Set up event handlers for input field
    setEventHandlers();

    // Set callback for updating the timers
    setInterval(updateTimers, 1000);
}
<commit_msg>Update of documentation<commit_after>function toggleDetails(elem) {
    // Get the "details"-table corresponding to the clicked element
    var e = document.getElementById(elem.id + '_details');

    // Toggle the visibility
    if(e.style.display == 'block') {
        e.style.display = 'none';
    } else {
        e.style.display = 'block';
    }
}

function setEventHandlers() {
    // Define labels
    var name_label = 'Task name';
    var estimate_label = 'Estimated duration (e.g. 1d 5h 30m)';

    // Define colors
    var active_color = '#000000';
    var inactive_color = '#cbcbcb';

    // Set properties of the "name"-input field
    var name_field = document.getElementById('name_field');
    name_field.value = name_label;
    name_field.style.color = inactive_color;
    name_field.onfocus = function() { handleFocusEvent(name_field, name_label, active_color); };

    // Properties of the "duration"-input field
    var estimate_field = document.getElementById('estimate_field');
    estimate_field.value = estimate_label;
    estimate_field.style.color = inactive_color;
    estimate_field.onfocus = function() { handleFocusEvent(estimate_field, estimate_label, active_color); };
}

function handleFocusEvent(elem, label, color) {
    // Clear field if description is still shown
    if (elem.value == label) {
        elem.value = '';
    }

    // Change color
    elem.style.color = color;
}

function updateTimers() {
    // Get all tasks that are labeled in progress
    var tasks = document.getElementsByClassName('task in_progress');
    if (tasks.length > 0) {
        // Update timers for each task that is in progress
        for (var i = 0; i < tasks.length; i++) {
            // Get current time
            var elem = tasks[i].getElementsByClassName('duration');
            var s = elem[0].innerHTML.split(':');
            var hours = parseInt(s[0], 10);
            var mins = parseInt(s[1]);
            var secs = parseInt(s[2]);

            // Update seconds
            secs += 1;
            if (secs >= 60) {
                secs -= 60;
                mins += 1;
            }

            // Update hours
            if (mins >= 60) {
                mins -= 60;
                hours += 1;
            }

            // Convert hours to string representation
            var hours_str = hours.toString();

            // Convert minutes to string representation
            var mins_str = '';
            if (mins < 10) {
                mins_str = '0';
            }
            mins_str += mins.toString();

            // Convert seconds to string representation
            var secs_str = '';
            if (secs < 10) {
                secs_str = '0';
            }
            secs_str += secs.toString();

            // Update element
            elem[0].innerHTML = hours_str + ':' + mins_str + ':' + secs_str;
        }
    }
}

function listSetup() {
    // Set up event handlers for input field
    setEventHandlers();

    // Set callback for updating the timers
    setInterval(updateTimers, 1000);
}
<|endoftext|>


<commit_before>utilities.namespace("SymPy");
SymPy.DEFAULT_ANIMATION_DURATION = 500;
SymPy.SphinxShell = SymPy.Shell.$extend({
    evalModeTypes: ['eval', 'copy'],
    evalMode: 'eval',

    __init__: function(config) {
        this.$super(config);
        this.visible = true;
        this.queuedStatements = [];

        index = this.evalModeTypes.indexOf(config.record);
        this.evalMode = (index == -1) ? this.getCookie('sympy-evalMode', 'eval') : config.evalMode;
        this.banner = config.banner ? config.banner : '';
    },

    render: function(el) {
        this.$super(el);

        this.shellEl = $(el);

        var headerLink =
            $('SymPy Live Shell');
        var header = $("
").append(headerLink);
        this.shellEl.prepend(header);

        this.toggleShellEl = $('<button/>').
            html("Hide SymPy Live Shell").
            attr("id", "toggleShell").
            addClass('shown');
        this.toggleShellEl.prepend($('
'));

        this.toggleShellEl.appendTo(document.body);
        this.toggleShellEl.click($.proxy(function() {
            this.toggle();
        }, this));

        // Add a link to Python code that will evaluate it in SymPy Live
        this.processCodeBlocks();

        // Don't expand the list of tab completions (saves space)
        this.completer.expandCompletions = false;

        // Change Fullscreen to go to main website
        $("#fullscreen-button").html("Go to SymPy Live");

	    this.evalModeEl.change($.proxy(function(event) {
            this.updateSettings();
            this.focus();
        }, this));
    },

    renderToolbar: function(settings) {
        this.$super(settings);

        $('
Settings
').
            prependTo($("#settings")).
            click($.proxy(this.toggleSettings, this));
        $("#settings h3").prepend($('
'));

        // We don't need the "force desktop version" option since there is
        // no mobile version
        var checkbox = $('#settings input[type="checkbox"]');
        checkbox.prev().hide();
        checkbox.next().hide();
        checkbox.hide();

        this.toolbarEl.append(
            $('
'),
            $('Evaluation Mode:'),
            $('<select id="evalMode"/>').append(
                $('<option value="eval">Evaluate</option>'),
                $('<option value="copy">Copy</option>')
            ),
            $('
')
        );
        this.evalModeEl = $('#evalMode');

        var index = this.evalModeTypes.indexOf(this.evalMode);
        this.evalModeEl.children('option')[index].selected = true;

        // Make enter the default submission button
        $("#submit-behavior").val("enter");
    },

    done: function(response) {
        this.$super(response);
        if (this.queuedStatements.length !== 0) {
            this.dequeueStatement();
            this.evaluate();
        }
    },

    error: function(xhr, status, error) {
        this.$super(xhr, status, error);
        this.queuedStatements.length = 0;
    },

    dequeueStatement: function() {
        if (this.queuedStatements.length !== 0) {
            var statements = this.queuedStatements.shift();
            this.setValue(statements.join('\n'));
        }
    },

    processCodeBlocks: function() {
        $('.highlight-python').each($.proxy(function(index, el) {
            var el = $(el);

            // Add the toolbar
            var container = $("
").addClass('sympy-live-eval-toolbar');
            var evaluate = $("<button>Run in SymPy Live</button>").
                addClass('sympy-live-eval-button').
                appendTo(container);
            el.children('.highlight').prepend(container);

            var fillShell = $.proxy(function() {
                this.show();
                var code = el.find('pre').text();
                var lines = code.split(/\n/g);

                // We want to evaluate each block individually
                var codeBlocks = [];
                var currentBlock = [];
                for (var i = 0; i < lines.length; i++) {
                    if (lines[i].substring(0, 4) === ">>> ") {
                        codeBlocks.push(currentBlock);
                        currentBlock = [];

                        currentBlock.push(
                            lines[i].substring(4, lines[i].length));
                    }
                    else if (lines[i].substring(0, 4) === "... ") {
                        currentBlock.push(
                            lines[i].substring(4, lines[i].length));
                    }
                }
                if (currentBlock.length !== 0) {
                    codeBlocks.push(currentBlock);
                }
                codeBlocks = codeBlocks.slice(1);
                this.queuedStatements = codeBlocks;
            }, this);

            evaluate.click($.proxy(function() {
                fillShell();
                if (this.evalModeEl.val() === "eval") {
                    this.dequeueStatement();
                    this.evaluate();
                }
                else {
                    this.focus();
                }
            }, this));

            this.processIndividualCodeBlocks(el.find('pre'));
        }, this));
    },

    processIndividualCodeBlocks: function(codeEl) {
        // childNodes gives text nodes which we want for whitespace
        var childNodes = codeEl.get(0).childNodes;
        var currentLine = [];
        var lines = [];

        for (var i = 0; i < childNodes.length; i++) {
            var domNode = childNodes[i];
            if (domNode.nodeType === domNode.ELEMENT_NODE) {
                currentLine.push(domNode.cloneNode(true));

                if (currentLine.length === 1 &&
                    domNode.innerText.substr(0, 3) === "...") {
                    // First node on line and continuation, so continue from
                    // the previous line
                    currentLine = lines.pop();
                    currentLine.push(domNode.cloneNode(true));
                }
            }
            else if (domNode.nodeType === domNode.TEXT_NODE) {
                currentLine.push(domNode.cloneNode(true));
                if (domNode.data.substr(-1) === '\n') {
                    lines.push(currentLine);
                    currentLine = [];
                }
            }
        }
        codeEl.empty();
        for (var i = 0; i < lines.length; i++) {
            var line = $('
');
            var processingLine = lines[i];
            if (processingLine[0].innerText.substr(0, 4) === ">>> ") {
                line.addClass('live-statement');
            }
            line.append(processingLine);
            line.click($.proxy((function(line) {
                // Save the current line
                return function() {
                    var lines = line.text().split(/\n/g);
                    var strippedLines = [];
                    for (var i = 0; i < lines.length; i++) {
                        strippedLines.push(lines[i].slice(4));
                    }
                    this.setValue(strippedLines.join('\n').trim());
                    this.show();
                    if (this.evalModeEl.val() === "eval") {
                        this.evaluate();
                    }
                    else {
                        this.focus();
                    }
                }
            })(line), this));
            codeEl.append(line);
        }
    },

    hide: function(duration) {
        if (typeof duration === "undefined") {
            duration = SymPy.DEFAULT_ANIMATION_DURATION;
        }
        this.disablePrompt();

        this.shellDimensionsRestored = {
            width: this.shellEl.width(),
            height: this.shellEl.height(),
            opacity: 1
        };

        this.shellEl.animate({
            width: 0,
            height: 0,
            opacity: 0
        }, duration);
        this.visible = false;

        this.toggleShellEl.removeClass('shown').children('span').
            html("Show SymPy Live Shell");
    },

    show: function(duration) {
        if (this.visible) return;

        if (typeof duration === "undefined") {
            duration = SymPy.DEFAULT_ANIMATION_DURATION;
        }
        this.enablePrompt();
        $(this.shellEl).animate(
            this.shellDimensionsRestored,
            duration,
            function() {
                // Don't fix the height so that if the settings are
                // expanded, the shell will expand with them
                $(this).css('height', 'auto');
            });
        this.visible = true;
        this.toggleShellEl.addClass('shown').children('span').
            html("Hide SymPy Live Shell");
    },

    toggle: function(duration) {
        if (typeof duration === "undefined") {
            duration = SymPy.DEFAULT_ANIMATION_DURATION;
        }
        if (this.isVisible()) {
            this.hide(duration);
        }
        else {
            this.show(duration);
        }
    },

    toggleSettings: function(duration) {
        if (typeof duration === "undefined") {
            duration = SymPy.DEFAULT_ANIMATION_DURATION;
        }

        if ($("#settings .content").is(":visible")) {
            $("#settings .content").slideUp(duration);
            $("#settings h3").removeClass('shown');
        }
        else {
            $("#settings .content").slideDown(duration);
            $("#settings h3").addClass('shown');
        }
    },

    isVisible: function() {
        return this.visible;
    },

    fullscreen: function() {
        window.open("http://live.sympy.org");
    },

    updateSettings: function() {
        this.$super();
        this.setCookie('sympy-evalMode', this.evalModeEl.val());
    }
});

$(document).ready(function() {
    var path = SymPy.getBasePath('live-sphinx.js');

    $.get(path + '/sphinxbanner').done(function(data) {
        var shellEl = $('
').appendTo($(document.body));
        var settingsEl = $('

');
        settingsEl.appendTo(shellEl);  // Needed to render the shell

        var shell = new SymPy.SphinxShell({
            baseName: 'live-sphinx.js',
            banner: data
        });
        shell.render(shellEl);
        settingsEl.appendTo(shellEl); // Put it under the shell
        shell.toggleSettings();
        shell.hide(0);
    });
});
<commit_msg>Sphinx: fix bug where copy mode did not copy statement<commit_after>utilities.namespace("SymPy");
SymPy.DEFAULT_ANIMATION_DURATION = 500;
SymPy.SphinxShell = SymPy.Shell.$extend({
    evalModeTypes: ['eval', 'copy'],
    evalMode: 'eval',

    __init__: function(config) {
        this.$super(config);
        this.visible = true;
        this.queuedStatements = [];

        index = this.evalModeTypes.indexOf(config.record);
        this.evalMode = (index == -1) ? this.getCookie('sympy-evalMode', 'eval') : config.evalMode;
        this.banner = config.banner ? config.banner : '';
    },

    render: function(el) {
        this.$super(el);

        this.shellEl = $(el);

        var headerLink =
            $('SymPy Live Shell');
        var header = $("
").append(headerLink);
        this.shellEl.prepend(header);

        this.toggleShellEl = $('<button/>').
            html("Hide SymPy Live Shell").
            attr("id", "toggleShell").
            addClass('shown');
        this.toggleShellEl.prepend($('
'));

        this.toggleShellEl.appendTo(document.body);
        this.toggleShellEl.click($.proxy(function() {
            this.toggle();
        }, this));

        // Add a link to Python code that will evaluate it in SymPy Live
        this.processCodeBlocks();

        // Don't expand the list of tab completions (saves space)
        this.completer.expandCompletions = false;

        // Change Fullscreen to go to main website
        $("#fullscreen-button").html("Go to SymPy Live");

	    this.evalModeEl.change($.proxy(function(event) {
            this.updateSettings();
            this.focus();
        }, this));
    },

    renderToolbar: function(settings) {
        this.$super(settings);

        $('
Settings
').
            prependTo($("#settings")).
            click($.proxy(this.toggleSettings, this));
        $("#settings h3").prepend($('
'));

        // We don't need the "force desktop version" option since there is
        // no mobile version
        var checkbox = $('#settings input[type="checkbox"]');
        checkbox.prev().hide();
        checkbox.next().hide();
        checkbox.hide();

        this.toolbarEl.append(
            $('
'),
            $('Evaluation Mode:'),
            $('<select id="evalMode"/>').append(
                $('<option value="eval">Evaluate</option>'),
                $('<option value="copy">Copy</option>')
            ),
            $('
')
        );
        this.evalModeEl = $('#evalMode');

        var index = this.evalModeTypes.indexOf(this.evalMode);
        this.evalModeEl.children('option')[index].selected = true;

        // Make enter the default submission button
        $("#submit-behavior").val("enter");
    },

    done: function(response) {
        this.$super(response);
        if (this.queuedStatements.length !== 0) {
            this.dequeueStatement();
            this.evaluate();
        }
    },

    error: function(xhr, status, error) {
        this.$super(xhr, status, error);
        this.queuedStatements.length = 0;
    },

    dequeueStatement: function() {
        if (this.queuedStatements.length !== 0) {
            var statements = this.queuedStatements.shift();
            this.setValue(statements.join('\n'));
        }
    },

    processCodeBlocks: function() {
        $('.highlight-python').each($.proxy(function(index, el) {
            var el = $(el);

            // Add the toolbar
            var container = $("
").addClass('sympy-live-eval-toolbar');
            var evaluate = $("<button>Run in SymPy Live</button>").
                addClass('sympy-live-eval-button').
                appendTo(container);
            el.children('.highlight').prepend(container);

            var fillShell = $.proxy(function() {
                this.show();
                var code = el.find('pre').text();
                var lines = code.split(/\n/g);

                // We want to evaluate each block individually
                var codeBlocks = [];
                var currentBlock = [];
                for (var i = 0; i < lines.length; i++) {
                    if (lines[i].substring(0, 4) === ">>> ") {
                        codeBlocks.push(currentBlock);
                        currentBlock = [];

                        currentBlock.push(
                            lines[i].substring(4, lines[i].length));
                    }
                    else if (lines[i].substring(0, 4) === "... ") {
                        currentBlock.push(
                            lines[i].substring(4, lines[i].length));
                    }
                }
                if (currentBlock.length !== 0) {
                    codeBlocks.push(currentBlock);
                }
                codeBlocks = codeBlocks.slice(1);
                this.queuedStatements = codeBlocks;
            }, this);

            evaluate.click($.proxy(function() {
                fillShell();
                this.dequeueStatement();
                if (this.evalModeEl.val() === "eval") {
                    this.evaluate();
                }
                else {
                    this.focus();
                }
            }, this));

            this.processIndividualCodeBlocks(el.find('pre'));
        }, this));
    },

    processIndividualCodeBlocks: function(codeEl) {
        // childNodes gives text nodes which we want for whitespace
        var childNodes = codeEl.get(0).childNodes;
        var currentLine = [];
        var lines = [];

        for (var i = 0; i < childNodes.length; i++) {
            var domNode = childNodes[i];
            if (domNode.nodeType === domNode.ELEMENT_NODE) {
                currentLine.push(domNode.cloneNode(true));

                if (currentLine.length === 1 &&
                    domNode.innerText.substr(0, 3) === "...") {
                    // First node on line and continuation, so continue from
                    // the previous line
                    currentLine = lines.pop();
                    currentLine.push(domNode.cloneNode(true));
                }
            }
            else if (domNode.nodeType === domNode.TEXT_NODE) {
                currentLine.push(domNode.cloneNode(true));
                if (domNode.data.substr(-1) === '\n') {
                    lines.push(currentLine);
                    currentLine = [];
                }
            }
        }
        codeEl.empty();
        for (var i = 0; i < lines.length; i++) {
            var line = $('
');
            var processingLine = lines[i];
            if (processingLine[0].innerText.substr(0, 4) === ">>> ") {
                line.addClass('live-statement');
            }
            line.append(processingLine);
            line.click($.proxy((function(line) {
                // Save the current line
                return function() {
                    var lines = line.text().split(/\n/g);
                    var strippedLines = [];
                    for (var i = 0; i < lines.length; i++) {
                        strippedLines.push(lines[i].slice(4));
                    }
                    this.setValue(strippedLines.join('\n').trim());
                    this.show();
                    if (this.evalModeEl.val() === "eval") {
                        this.evaluate();
                    }
                    else {
                        this.focus();
                    }
                }
            })(line), this));
            codeEl.append(line);
        }
    },

    hide: function(duration) {
        if (typeof duration === "undefined") {
            duration = SymPy.DEFAULT_ANIMATION_DURATION;
        }
        this.disablePrompt();

        this.shellDimensionsRestored = {
            width: this.shellEl.width(),
            height: this.shellEl.height(),
            opacity: 1
        };

        this.shellEl.animate({
            width: 0,
            height: 0,
            opacity: 0
        }, duration);
        this.visible = false;

        this.toggleShellEl.removeClass('shown').children('span').
            html("Show SymPy Live Shell");
    },

    show: function(duration) {
        if (this.visible) return;

        if (typeof duration === "undefined") {
            duration = SymPy.DEFAULT_ANIMATION_DURATION;
        }
        this.enablePrompt();
        $(this.shellEl).animate(
            this.shellDimensionsRestored,
            duration,
            function() {
                // Don't fix the height so that if the settings are
                // expanded, the shell will expand with them
                $(this).css('height', 'auto');
            });
        this.visible = true;
        this.toggleShellEl.addClass('shown').children('span').
            html("Hide SymPy Live Shell");
    },

    toggle: function(duration) {
        if (typeof duration === "undefined") {
            duration = SymPy.DEFAULT_ANIMATION_DURATION;
        }
        if (this.isVisible()) {
            this.hide(duration);
        }
        else {
            this.show(duration);
        }
    },

    toggleSettings: function(duration) {
        if (typeof duration === "undefined") {
            duration = SymPy.DEFAULT_ANIMATION_DURATION;
        }

        if ($("#settings .content").is(":visible")) {
            $("#settings .content").slideUp(duration);
            $("#settings h3").removeClass('shown');
        }
        else {
            $("#settings .content").slideDown(duration);
            $("#settings h3").addClass('shown');
        }
    },

    isVisible: function() {
        return this.visible;
    },

    fullscreen: function() {
        window.open("http://live.sympy.org");
    },

    updateSettings: function() {
        this.$super();
        this.setCookie('sympy-evalMode', this.evalModeEl.val());
    }
});

$(document).ready(function() {
    var path = SymPy.getBasePath('live-sphinx.js');

    $.get(path + '/sphinxbanner').done(function(data) {
        var shellEl = $('
').appendTo($(document.body));
        var settingsEl = $('

');
        settingsEl.appendTo(shellEl);  // Needed to render the shell

        var shell = new SymPy.SphinxShell({
            baseName: 'live-sphinx.js',
            banner: data
        });
        shell.render(shellEl);
        settingsEl.appendTo(shellEl); // Put it under the shell
        shell.toggleSettings();
        shell.hide(0);
    });
});
<|endoftext|>


<commit_before>import PropTypes from 'prop-types';
import React from 'react';
import Styles from './Tabs.css';
import Variables from '../variables';

class Tabs extends React.Component {
  constructor(props) {
    super(props);
    this.onClick = this.onClick.bind(this);
    this.setInkbarStyles = this.setInkbarStyles.bind(this);
    this.makeTabs = this.makeTabs.bind(this);
    this.registerInkBar = this.registerInkBar.bind(this);
    this.registerTabBar = this.registerTabBar.bind(this);
    this.tabs = {};
    this.state = {
      inkBarLeft: 0,
      inkBarWidth: 0
    };
  }

  componentDidMount() {
    this.setInkbarStyles();
    this.timeout = setTimeout(() => (
      this.inkbar.style.transition = 'all 250ms ease'
    ), 0);
    window.addEventListener('resize', this.setInkbarStyles);
  }

  componentWillReceiveProps({index}) {
    if (this.props.index !== index) {
      this.setInkbarStyles(index);
    }
  }

  componentWillUnmount() {
    window.removeEventListener('resize', this.setInkbarStyles);
    clearTimeout(this.timeout);
  }

  onClick(e, index) {
    this.props.onChange(e, index);
  }

  setInkbarStyles(nextIndex) {
    const index = typeof nextIndex === 'number' ? nextIndex : this.props.index;
    const currentTab = this.tabs[index];
    if (currentTab) {
      const {width, left} = currentTab.getBoundingClientRect();
      const tabBarLeft = this.tabBar.getBoundingClientRect().left;
      const inkBarLeft = `${left - tabBarLeft}px`;
      const inkBarWidth = `${width}px`;
      this.setState({
        inkBarLeft,
        inkBarWidth
      });
    }
  }

  makeTabs() {
    const {children, textColor} = this.props;
    return React.Children.map(children, (tab, i) => {
      const other = {};
      return React.cloneElement(tab, {
        onClick: e => (this.onClick(e, i)),
        domRef: (c) => {
          this.tabs[i] = c;
        },
        style: {
          boxShadow: 'none',
          ...other
        },
        textColor
      });
    });
  }

  registerTabBar(c) {
    this.tabBar = c;
  }

  registerInkBar(c) {
    this.inkbar = c;
  }

  render() {
    const {barColor} = this.props;
    return (

          {this.makeTabs()}

    );
  }
}

Tabs.defaultProps = {
  barColor: Variables.$primary,
  children: null,
  index: 0,
  inkBarColor: Variables.$accent,
  onChange: () => {},
  textColor: '#fff'
};

Tabs.propTypes = {
  barColor: PropTypes.string,
  children: PropTypes.node,
  index: PropTypes.number,
  inkBarColor: PropTypes.string,
  onChange: PropTypes.func,
  textColor: PropTypes.string
};

export default Tabs;
<commit_msg>[Tabs] remove unnecessary wrapping
<commit_after>import PropTypes from 'prop-types';
import React from 'react';
import Styles from './Tabs.css';
import Variables from '../variables';

class Tabs extends React.Component {
  constructor(props) {
    super(props);
    this.onClick = this.onClick.bind(this);
    this.setInkbarStyles = this.setInkbarStyles.bind(this);
    this.makeTabs = this.makeTabs.bind(this);
    this.registerInkBar = this.registerInkBar.bind(this);
    this.registerTabBar = this.registerTabBar.bind(this);
    this.tabs = {};
    this.state = {
      inkBarLeft: 0,
      inkBarWidth: 0
    };
  }

  componentDidMount() {
    this.setInkbarStyles();
    this.timeout = setTimeout(() => (
      this.inkbar.style.transition = 'all 250ms ease'
    ), 0);
    window.addEventListener('resize', this.setInkbarStyles);
  }

  componentWillReceiveProps({index}) {
    if (this.props.index !== index) {
      this.setInkbarStyles(index);
    }
  }

  componentWillUnmount() {
    window.removeEventListener('resize', this.setInkbarStyles);
    clearTimeout(this.timeout);
  }

  onClick(e, index) {
    this.props.onChange(e, index);
  }

  setInkbarStyles(nextIndex) {
    const index = typeof nextIndex === 'number' ? nextIndex : this.props.index;
    const currentTab = this.tabs[index];
    if (currentTab) {
      const {width, left} = currentTab.getBoundingClientRect();
      const tabBarLeft = this.tabBar.getBoundingClientRect().left;
      const inkBarLeft = `${left - tabBarLeft}px`;
      const inkBarWidth = `${width}px`;
      this.setState({
        inkBarLeft,
        inkBarWidth
      });
    }
  }

  makeTabs() {
    const {children, textColor} = this.props;
    return React.Children.map(children, (tab, i) => {
      const other = {};
      return React.cloneElement(tab, {
        onClick: e => (this.onClick(e, i)),
        domRef: (c) => {
          this.tabs[i] = c;
        },
        style: {
          boxShadow: 'none',
          ...other
        },
        textColor
      });
    });
  }

  registerTabBar(c) {
    this.tabBar = c;
  }

  registerInkBar(c) {
    this.inkbar = c;
  }

  render() {
    const {barColor} = this.props;
    return (

        {this.makeTabs()}

    );
  }
}

Tabs.defaultProps = {
  barColor: Variables.$primary,
  children: null,
  index: 0,
  inkBarColor: Variables.$accent,
  onChange: () => {},
  textColor: '#fff'
};

Tabs.propTypes = {
  barColor: PropTypes.string,
  children: PropTypes.node,
  index: PropTypes.number,
  inkBarColor: PropTypes.string,
  onChange: PropTypes.func,
  textColor: PropTypes.string
};

export default Tabs;
<|endoftext|>


<commit_before>Hero = function(game, characterName, isLocal, initX, initY, textureName) {
    //Phaser.Sprite.call(this, game, initX, initY, 'hero');
    Phaser.Sprite.call(this, game, initX, initY, textureName);

    this.game = game;
    this.characterName = characterName;
    this.isLocal = isLocal;

    // If local player, listen for keys.
    if (this.isLocal) {
        //game.input.keyboard.onDownCallback(this.handleKeyDown.bind(this));
        this.moveDirectionKeys = {
            up: this.game.input.keyboard.addKey(Phaser.Keyboard.W),
            left: this.game.input.keyboard.addKey(Phaser.Keyboard.A),
            down: this.game.input.keyboard.addKey(Phaser.Keyboard.S),
            right: this.game.input.keyboard.addKey(Phaser.Keyboard.D)
        };
        this.actionDirectionKeys = this.game.input.keyboard.createCursorKeys();
        this.actionSwitchKey = this.game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR);
    }

    this.lastPos = {
        x: -1,
        y: -1
    }

    this.pos = {
        x: initX,
        y: initY
    }

    this.vel = {
        x: 0,
        y: 0
    }

    this.health = this.maxHealth;
    this.actionCooldown = 0;
    this.moveDelay = 0;
    this.currentAction = 1;

    this.animations.add('UP', [9, 10, 11]);
    this.animations.add('DOWN', [0,1,2]);
    this.animations.add('LEFT', [3,4,5]);
    this.animations.add('RIGHT', [6, 7, 8]);
    this.animations.play('DOWN', 5, true);

    // Init collision detection stuff
    game.add.existing(this);
    game.physics.enable(this, Phaser.Physics.ARCADE);

    this.body.collideWorldBounds = true;

    this.bringToTop();
};

Hero.prototype = Object.create(Phaser.Sprite.prototype);
Hero.prototype.constructor = Hero;

Hero.prototype.update = function() {
    if (this.moveDelay === 0) {
        var moveMessage = {
            type: 'FE_HERO_MOVE',
            content: {
                name: this.characterName,
                speed: 8,
                direction: ''
            }
        };

        if (this.moveDirectionKeys.left.isDown) {
            moveMessage.content.direction = 'LEFT'
            this.animations.play('LEFT', 5, true);
        } else if (this.moveDirectionKeys.right.isDown) {
            moveMessage.content.direction = 'RIGHT'
            this.animations.play('RIGHT', 5, true);
        } else if (this.moveDirectionKeys.up.isDown) {
            moveMessage.content.direction = 'UP'
            this.animations.play('UP', 5, true);
        } else if (this.moveDirectionKeys.down.isDown) {
            moveMessage.content.direction = 'DOWN'
            this.animations.play('DOWN', 5, true);
        }

        if (moveMessage.content.direction !== '') {
            //TODO this.messageCallback(moveMessage);
        }
    } else {
        this.moveDelay--;
    }

    if (this.actionSwitchKey.isDown) {
        if (this.currentAction === 1) {
            this.currentAction = 2;
        } else {
            this.currentAction = 1;
        }
    }

    if (this.actionCooldown === 0) {
        var actionMessage = {
            type: 'FE_HERO_ACTION',
            content: {
                name: this.characterName,
                action: this.currentAction,
                direction: ''
            }
        }

        if (this.actionDirectionKeys.left.isDown) {
            actionMessage.content.direction = 'LEFT'
        } else if (this.actionDirectionKeys.right.isDown) {
            actionMessage.content.direction = 'RIGHT'
        } else if (this.actionDirectionKeys.up.isDown) {
            actionMessage.content.direction = 'UP'
        } else if (this.actionDirectionKeys.down.isDown) {
            actionMessage.content.direction = 'DOWN'
        }

        if (actionMessage.content.direction !== '') {
            //TODO this.messageCallback(moveMessage);
        }
    }
}

Hero.prototype.update_smooth = function() {
    if (this.moveDelay === 0) {
        this.vel.x = 0;
        this.vel.y = 0;

        if (this.moveDirectionKeys.left.isDown) {
            this.vel.x = -8;
        } else if (this.moveDirectionKeys.right.isDown) {
            this.vel.x = 8;
        } else if (this.moveDirectionKeys.up.isDown) {
            this.vel.y = -8;
        } else if (this.moveDirectionKeys.down.isDown) {
            this.vel.y = 8;
        }

        this.pos.x += this.vel.x;
        this.pos.y += this.vel.y;

        this.x = this.pos.x;
        this.y = this.pos.y;

        // Check if there's been a change
        if (this.pos.x !== this.lastPos.x || this.pos.y !== this.lastPos.y) {
            /*this.messageCallback({
                type: 'FE_HERO_POSITION',
                content: {
                    name: this.characterName,
                    x: this.pos.x,
                    y: this.pos.y
                }
            })*/

            this.moveDelay = this.maxMoveDelay;

            this.lastPos.x = this.pos.x;
            this.lastPos.y = this.pos.y;
        }
    } else {
        this.moveDelay--;
    }
}<commit_msg>Actual JS line to send move messages to backend<commit_after>Hero = function(game, characterName, isLocal, initX, initY, textureName) {
    //Phaser.Sprite.call(this, game, initX, initY, 'hero');
    Phaser.Sprite.call(this, game, initX, initY, textureName);

    this.game = game;
    this.characterName = characterName;
    this.isLocal = isLocal;

    // If local player, listen for keys.
    if (this.isLocal) {
        //game.input.keyboard.onDownCallback(this.handleKeyDown.bind(this));
        this.moveDirectionKeys = {
            up: this.game.input.keyboard.addKey(Phaser.Keyboard.W),
            left: this.game.input.keyboard.addKey(Phaser.Keyboard.A),
            down: this.game.input.keyboard.addKey(Phaser.Keyboard.S),
            right: this.game.input.keyboard.addKey(Phaser.Keyboard.D)
        };
        this.actionDirectionKeys = this.game.input.keyboard.createCursorKeys();
        this.actionSwitchKey = this.game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR);
    }

    this.lastPos = {
        x: -1,
        y: -1
    }

    this.pos = {
        x: initX,
        y: initY
    }

    this.vel = {
        x: 0,
        y: 0
    }

    this.health = this.maxHealth;
    this.actionCooldown = 0;
    this.moveDelay = 0;
    this.currentAction = 1;

    this.animations.add('UP', [9, 10, 11]);
    this.animations.add('DOWN', [0,1,2]);
    this.animations.add('LEFT', [3,4,5]);
    this.animations.add('RIGHT', [6, 7, 8]);
    this.animations.play('DOWN', 5, true);

    // Init collision detection stuff
    game.add.existing(this);
    game.physics.enable(this, Phaser.Physics.ARCADE);

    this.body.collideWorldBounds = true;

    this.bringToTop();
};

Hero.prototype = Object.create(Phaser.Sprite.prototype);
Hero.prototype.constructor = Hero;

Hero.prototype.update = function() {
    if (this.moveDelay === 0) {
        var moveMessage = {
            type: 'FE_HERO_MOVE',
            content: {
                name: this.characterName,
                speed: 8,
                direction: ''
            }
        };

        if (this.moveDirectionKeys.left.isDown) {
            moveMessage.content.direction = 'LEFT'
            this.animations.play('LEFT', 5, true);
        } else if (this.moveDirectionKeys.right.isDown) {
            moveMessage.content.direction = 'RIGHT'
            this.animations.play('RIGHT', 5, true);
        } else if (this.moveDirectionKeys.up.isDown) {
            moveMessage.content.direction = 'UP'
            this.animations.play('UP', 5, true);
        } else if (this.moveDirectionKeys.down.isDown) {
            moveMessage.content.direction = 'DOWN'
            this.animations.play('DOWN', 5, true);
        }

        if (moveMessage.content.direction !== '') {
            socket.send(JSON.stringify(moveMessage));
        }
    } else {
        this.moveDelay--;
    }

    if (this.actionSwitchKey.isDown) {
        if (this.currentAction === 1) {
            this.currentAction = 2;
        } else {
            this.currentAction = 1;
        }
    }

    if (this.actionCooldown === 0) {
        var actionMessage = {
            type: 'FE_HERO_ACTION',
            content: {
                name: this.characterName,
                action: this.currentAction,
                direction: ''
            }
        }

        if (this.actionDirectionKeys.left.isDown) {
            actionMessage.content.direction = 'LEFT'
        } else if (this.actionDirectionKeys.right.isDown) {
            actionMessage.content.direction = 'RIGHT'
        } else if (this.actionDirectionKeys.up.isDown) {
            actionMessage.content.direction = 'UP'
        } else if (this.actionDirectionKeys.down.isDown) {
            actionMessage.content.direction = 'DOWN'
        }

        if (actionMessage.content.direction !== '') {
            //TODO this.messageCallback(moveMessage);
        }
    }
}

Hero.prototype.update_smooth = function() {
    if (this.moveDelay === 0) {
        this.vel.x = 0;
        this.vel.y = 0;

        if (this.moveDirectionKeys.left.isDown) {
            this.vel.x = -8;
        } else if (this.moveDirectionKeys.right.isDown) {
            this.vel.x = 8;
        } else if (this.moveDirectionKeys.up.isDown) {
            this.vel.y = -8;
        } else if (this.moveDirectionKeys.down.isDown) {
            this.vel.y = 8;
        }

        this.pos.x += this.vel.x;
        this.pos.y += this.vel.y;

        this.x = this.pos.x;
        this.y = this.pos.y;

        // Check if there's been a change
        if (this.pos.x !== this.lastPos.x || this.pos.y !== this.lastPos.y) {
            /*this.messageCallback({
                type: 'FE_HERO_POSITION',
                content: {
                    name: this.characterName,
                    x: this.pos.x,
                    y: this.pos.y
                }
            })*/

            this.moveDelay = this.maxMoveDelay;

            this.lastPos.x = this.pos.x;
            this.lastPos.y = this.pos.y;
        }
    } else {
        this.moveDelay--;
    }
}<|endoftext|>


<commit_before>var URL = "ws://" + location.hostname + ":" + location.port + "/socket";
var socket;

var docid;
var title;
var omnitoolbar;
var textbox;

var timeout_id = 0;
var last_text_hash = "";
var box_id = 0;

function get_keywords() {
    var keywords = [];
    textbox.find(".keyword").each(function() {
        if ($(this).text())
            keywords.push($(this).text());
    });
    return keywords;
}

function update_server() {
    keywords = get_keywords(textbox);
    var data = {"title": title.val(), "text": textbox.html(), "keywords": keywords};
    socket.send("UPDATE " + JSON.stringify(data));
    $("#tb-status").trigger("saved");
}

function gen_hash(s) {
    // http://stackoverflow.com/questions/7616461
    return s.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0);
}

function on_type() {
    var this_hash = gen_hash(title.val() + textbox.html());
    if (last_text_hash === "") {  // Don't update when we first receive input.
        last_text_hash = this_hash;
        return;
    }
    if (this_hash != last_text_hash) {
        last_text_hash = this_hash;
        window.clearTimeout(timeout_id);
        timeout_id = window.setTimeout(update_server, 500, textbox);
        $("#tb-status").trigger("saving");
    }
}

function process_update(payload) {
    box_id++;
    topic = payload["Keyword"];
    data = "";
    definition = payload["Definition"];
    if (definition !== undefined) {
        data += "
" + definition + "
";
    }
    data += "
";
    google = payload["GoogleArticles"];
    if (google !== undefined) {
        for (var i = 0; i < google["Links"].length; i++)
            data += '
' + google["Headline"][i] + ": " + google["Blurbs"][i] + "
";
    }
    youtube = payload["Youtube"];
    if (youtube !== undefined) {
        for(var i = 0; i < youtube["Title"].length; i++) {
            if(youtube["Title"][i] == "") break;
            data += '
' + youtube["Title"][i] + "
";
        }
    }
    hsw = payload["HSWArticles"];
    if (hsw !== undefined) {
        for (var i = 0; i < hsw["Links"].length; i++) {
            if (hsw["Links"][i] != null)
                data += '
' + hsw["Headline"][i] + ": " + hsw["Blurbs"][i] + "
";
        }
    }
    images = payload["Images"];
    nytimes = payload["NyTimesArticles"];
    if (nytimes !== undefined) {
        for (var i = 0; i < nytimes["Links"].length; i++) {
            if (nytimes["Links"][i] != null)
                data += '
' + nytimes["Headline"][i] + ": " + nytimes["Blurbs"][i] + "
";
        }
    }
    data += "
"
    box = '
';
    box += '
' + topic + ' ✖
';
    box += '
' + data + "

";
    omnitoolbar.append(box);
    $("#topic-box-" + box_id).css("display", "none").slideDown(300);
    $("#topic-box-remove-" + box_id).click(function(b_id) { return function() { $("#topic-box-" + b_id).slideUp(300, function() { $(this).remove(); }); } }(box_id));
}

$(document).ready(function() {
    $("#textbox").rte("/static/common.css");
    title = $("#title");
    omnitoolbar = $("#omnitoolbar");
    socket = new WebSocket(URL);

    socket.onopen = function(event) {
        socket.send("HELLO " + docid);
    };

    socket.onmessage = function(evt) {
        if (evt.data.indexOf("READY") == 0) {
            var payload = JSON.parse(evt.data.substring(6));
            title.val(payload.title);
            textbox.html(payload.text);
            $("#tb-status").trigger("saved");
        }
        else if (evt.data.indexOf("UPDATE") == 0) {
            var payload = JSON.parse(evt.data.substring(7));
            process_update(payload);
        }
        else if (evt.data == "GOODBYE") {
            socket.close();
        }
    };

    title.keyup(on_type());

    $("#tb-status").bind("saving", function() {
        $(this).removeClass("fa-exclamation-circle fa-check-circle").addClass("fa-spinner");
        $("#tb-status-text").text("Writing");
    });

    $("#tb-status").bind("saved", function() {
        $(this).removeClass("fa-exclamation-circle fa-spinner").addClass("fa-check-circle");
        $("#tb-status-text").text("Saved");
    });
});
<commit_msg>Fix title's keyup handler.<commit_after>var URL = "ws://" + location.hostname + ":" + location.port + "/socket";
var socket;

var docid;
var title;
var omnitoolbar;
var textbox;

var timeout_id = 0;
var last_text_hash = "";
var box_id = 0;

function get_keywords() {
    var keywords = [];
    textbox.find(".keyword").each(function() {
        if ($(this).text())
            keywords.push($(this).text());
    });
    return keywords;
}

function update_server() {
    keywords = get_keywords(textbox);
    var data = {"title": title.val(), "text": textbox.html(), "keywords": keywords};
    socket.send("UPDATE " + JSON.stringify(data));
    $("#tb-status").trigger("saved");
}

function gen_hash(s) {
    // http://stackoverflow.com/questions/7616461
    return s.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0);
}

function on_type() {
    var this_hash = gen_hash(title.val() + textbox.html());
    if (last_text_hash === "") {  // Don't update when we first receive input.
        last_text_hash = this_hash;
        return;
    }
    if (this_hash != last_text_hash) {
        last_text_hash = this_hash;
        window.clearTimeout(timeout_id);
        timeout_id = window.setTimeout(update_server, 500, textbox);
        $("#tb-status").trigger("saving");
    }
}

function process_update(payload) {
    box_id++;
    topic = payload["Keyword"];
    data = "";
    definition = payload["Definition"];
    if (definition !== undefined) {
        data += "
" + definition + "
";
    }
    data += "
";
    google = payload["GoogleArticles"];
    if (google !== undefined) {
        for (var i = 0; i < google["Links"].length; i++)
            data += '
' + google["Headline"][i] + ": " + google["Blurbs"][i] + "
";
    }
    youtube = payload["Youtube"];
    if (youtube !== undefined) {
        for(var i = 0; i < youtube["Title"].length; i++) {
            if(youtube["Title"][i] == "") break;
            data += '
' + youtube["Title"][i] + "
";
        }
    }
    hsw = payload["HSWArticles"];
    if (hsw !== undefined) {
        for (var i = 0; i < hsw["Links"].length; i++) {
            if (hsw["Links"][i] != null)
                data += '
' + hsw["Headline"][i] + ": " + hsw["Blurbs"][i] + "
";
        }
    }
    images = payload["Images"];
    nytimes = payload["NyTimesArticles"];
    if (nytimes !== undefined) {
        for (var i = 0; i < nytimes["Links"].length; i++) {
            if (nytimes["Links"][i] != null)
                data += '
' + nytimes["Headline"][i] + ": " + nytimes["Blurbs"][i] + "
";
        }
    }
    data += "
"
    box = '
';
    box += '
' + topic + ' ✖
';
    box += '
' + data + "

";
    omnitoolbar.append(box);
    $("#topic-box-" + box_id).css("display", "none").slideDown(300);
    $("#topic-box-remove-" + box_id).click(function(b_id) { return function() { $("#topic-box-" + b_id).slideUp(300, function() { $(this).remove(); }); } }(box_id));
}

$(document).ready(function() {
    $("#textbox").rte("/static/common.css");
    title = $("#title");
    omnitoolbar = $("#omnitoolbar");
    socket = new WebSocket(URL);

    socket.onopen = function(event) {
        socket.send("HELLO " + docid);
    };

    socket.onmessage = function(evt) {
        if (evt.data.indexOf("READY") == 0) {
            var payload = JSON.parse(evt.data.substring(6));
            title.val(payload.title);
            textbox.html(payload.text);
            $("#tb-status").trigger("saved");
        }
        else if (evt.data.indexOf("UPDATE") == 0) {
            var payload = JSON.parse(evt.data.substring(7));
            process_update(payload);
        }
        else if (evt.data == "GOODBYE") {
            socket.close();
        }
    };

    title.keyup(on_type);

    $("#tb-status").bind("saving", function() {
        $(this).removeClass("fa-exclamation-circle fa-check-circle").addClass("fa-spinner");
        $("#tb-status-text").text("Writing");
    });

    $("#tb-status").bind("saved", function() {
        $(this).removeClass("fa-exclamation-circle fa-spinner").addClass("fa-check-circle");
        $("#tb-status-text").text("Saved");
    });
});
<|endoftext|>