<commit_before>/*global require:true*/
/*global module:true*/
(function(){
	"use strict";

	var fs = require( 'fs-extra' );
	var path = require( 'path' );
	var Handlebars = require( 'handlebars' );
	var SvgURIEncoder = require( './svg-uri-encoder' );
	var PngURIEncoder = require( './png-uri-encoder' );

	function DirectoryEncoder( input, output, options ){
		var files;
		if( typeof input === "string" && fs.lstatSync( input ).isDirectory()){
			files = fs.readdirSync( input ).map( function( file ){
				return path.join( input, file );
			});
		} else if( Array.isArray( input ) ){
			files = input;
		} else {
			throw new Error( "Input must be Array of files or String that is a directory" );
		}

		if( typeof output !== "string" ){
			throw new Error( "Output must be file name of desired encoded file" );
		}

		this.output = output;
		this.options = options || {};
		this.files = files;
		this.prefix = this.options.prefix || ".icon-";

		this.options.pngfolder = this.options.pngfolder || "";
		this.options.pngpath = this.options.pngpath || this.options.pngfolder;

		this.customselectors = this.options.customselectors || {};
		this.template = this._loadTemplate( this.options.template );
		this.templatePrepend = this.options.templatePrepend || "";
		this.templateAppend = this.options.templateAppend || "";
	}

	DirectoryEncoder.encoders = {
		svg: SvgURIEncoder,
		png: PngURIEncoder
	};

	DirectoryEncoder.prototype.encode = function() {
		var self = this, seen = {};

		// remove the file if it's there
		if( fs.existsSync(this.output) ) {
			fs.unlinkSync( this.output );
		}

		if( !fs.existsSync(path.dirname( this.output )) ){
			fs.mkdirpSync( path.dirname( this.output ) );
		}

		// add templatePrepend to the output file
		fs.appendFileSync( self.output, self.templatePrepend );

		// append each selector
		this.files.filter(function( filepath ){
			var file = path.basename( filepath ),
				extension = path.extname( file );
			return extension === ".svg" || extension === ".png";
		})
		.filter(function( filepath ){
			return fs.lstatSync( filepath ).isFile();
		})
		.forEach(function( filepath ) {
			var css, datauri, stats,
				file = path.basename( filepath ),
				extension = path.extname( file );

			self._checkName(seen, file.replace( extension, '' ));

			stats = self._stats( filepath );
			datauri = self._datauri( filepath );

			css = self._css( file.replace( extension, '' ), datauri, stats );

			fs.appendFileSync( self.output, css + "\n\n" );
		});

		// add templateAppend to the output file
		fs.appendFileSync( self.output, self.templateAppend );

	};
	DirectoryEncoder.prototype._css = function( name, datauri, stats ) {
		var self = this, width, height;

		if( stats ){
			width = stats.width;
			height = stats.height;
		}
		this.customselectors = this.customselectors || {};
		this.prefix = this.prefix || ".icon-";

		if( this.customselectors[ "*" ] ){
			this.customselectors[ name ] = this.customselectors[ name ] || [];
			var selectors = this.customselectors[ "*" ];
			selectors.forEach( function( el ){
				var s = name.replace( new RegExp( "(" + name + ")" ), el );
				if( self.customselectors[ name ].indexOf( s ) === -1 ) {
					self.customselectors[ name ].push( s );
				}
			});
		}

		var data = {
			prefix: this.prefix,
			name: name,
			datauri: datauri,
			width: width,
			height: height,
			customselectors: this.customselectors[ name ]
		}, css = "";

		if( this.template ){
			css = this.template( data );
		} else {
			for( var i in data.customselectors ){
				if( data.customselectors.hasOwnProperty( i ) ){
					css += data.customselectors[i] + ",\n";
				}
			}
			css += this.prefix + name +
				" { background-image: url('" +
				datauri +
				"'); background-repeat: no-repeat; }";
		}

		return css;
	};

	DirectoryEncoder.prototype._stats = function( file ){
		var encoder, extension = path.extname( file );

		if( typeof DirectoryEncoder.encoders[extension.replace(".", "")] === "undefined" ){
			throw new Error( "Encoder does not recognize file type: " + file );
		}

		encoder = new DirectoryEncoder.encoders[extension.replace(".", "")]( file );

		return encoder.stats();
	};

	DirectoryEncoder.prototype._datauri = function( file ) {
		var encoder, extension = path.extname( file );

		if( typeof DirectoryEncoder.encoders[extension.replace(".", "")] === "undefined" ){
			throw new Error( "Encoder does not recognize file type: " + file );
		}

		encoder = new DirectoryEncoder.encoders[extension.replace(".", "")]( file );

		// TODO passthrough of options is generally a code smell
		return encoder.encode( this.options );
	};

	DirectoryEncoder.prototype._checkName = function( seen, name ) {
		if( seen[name] ){
			throw new Error("Two files with the same name: `" + name + "` exist in the input directory");
		}

		seen[name] = true;
	};

	DirectoryEncoder.prototype._loadTemplate = function( templateFile ) {
		var tmpl;

		if( !templateFile ) { return false; }

		if( fs.existsSync( templateFile ) && fs.lstatSync( templateFile ).isFile() ){
			var source = fs.readFileSync( templateFile ).toString( 'utf-8' );
			tmpl = Handlebars.compile(source);
		} else {
			throw new Error( "Template file either doesn't exist or isn't a file" );
		}

		return tmpl;
	};

	module.exports = DirectoryEncoder;
}());
<commit_msg>add option for configuring handlebars with custom helpers<commit_after>/*global require:true*/
/*global module:true*/
(function(){
	"use strict";

	var fs = require( 'fs-extra' );
	var path = require( 'path' );
	var Handlebars = require( 'handlebars' );
	var SvgURIEncoder = require( './svg-uri-encoder' );
	var PngURIEncoder = require( './png-uri-encoder' );

	function DirectoryEncoder( input, output, options ){
		var files;
		if( typeof input === "string" && fs.lstatSync( input ).isDirectory()){
			files = fs.readdirSync( input ).map( function( file ){
				return path.join( input, file );
			});
		} else if( Array.isArray( input ) ){
			files = input;
		} else {
			throw new Error( "Input must be Array of files or String that is a directory" );
		}

		if( typeof output !== "string" ){
			throw new Error( "Output must be file name of desired encoded file" );
		}

		this.output = output;
		this.options = options || {};
		this.files = files;
		this.prefix = this.options.prefix || ".icon-";

		this.options.pngfolder = this.options.pngfolder || "";
		this.options.pngpath = this.options.pngpath || this.options.pngfolder;
		this.options.handlebarsCallback = this.options.handlebarsCallback || function () {};

		this.customselectors = this.options.customselectors || {};
		this.template = this._loadTemplate( this.options.template );
		this.templatePrepend = this.options.templatePrepend || "";
		this.templateAppend = this.options.templateAppend || "";
	}

	DirectoryEncoder.encoders = {
		svg: SvgURIEncoder,
		png: PngURIEncoder
	};

	DirectoryEncoder.prototype.encode = function() {
		var self = this, seen = {};

		// remove the file if it's there
		if( fs.existsSync(this.output) ) {
			fs.unlinkSync( this.output );
		}

		if( !fs.existsSync(path.dirname( this.output )) ){
			fs.mkdirpSync( path.dirname( this.output ) );
		}

		// add templatePrepend to the output file
		fs.appendFileSync( self.output, self.templatePrepend );

		// append each selector
		this.files.filter(function( filepath ){
			var file = path.basename( filepath ),
				extension = path.extname( file );
			return extension === ".svg" || extension === ".png";
		})
		.filter(function( filepath ){
			return fs.lstatSync( filepath ).isFile();
		})
		.forEach(function( filepath ) {
			var css, datauri, stats,
				file = path.basename( filepath ),
				extension = path.extname( file );

			self._checkName(seen, file.replace( extension, '' ));

			stats = self._stats( filepath );
			datauri = self._datauri( filepath );

			css = self._css( file.replace( extension, '' ), datauri, stats );

			fs.appendFileSync( self.output, css + "\n\n" );
		});

		// add templateAppend to the output file
		fs.appendFileSync( self.output, self.templateAppend );

	};
	DirectoryEncoder.prototype._css = function( name, datauri, stats ) {
		var self = this, width, height;

		if( stats ){
			width = stats.width;
			height = stats.height;
		}
		this.customselectors = this.customselectors || {};
		this.prefix = this.prefix || ".icon-";

		if( this.customselectors[ "*" ] ){
			this.customselectors[ name ] = this.customselectors[ name ] || [];
			var selectors = this.customselectors[ "*" ];
			selectors.forEach( function( el ){
				var s = name.replace( new RegExp( "(" + name + ")" ), el );
				if( self.customselectors[ name ].indexOf( s ) === -1 ) {
					self.customselectors[ name ].push( s );
				}
			});
		}

		var data = {
			prefix: this.prefix,
			name: name,
			datauri: datauri,
			width: width,
			height: height,
			customselectors: this.customselectors[ name ]
		}, css = "";

		if( this.template ){
			css = this.template( data );
		} else {
			for( var i in data.customselectors ){
				if( data.customselectors.hasOwnProperty( i ) ){
					css += data.customselectors[i] + ",\n";
				}
			}
			css += this.prefix + name +
				" { background-image: url('" +
				datauri +
				"'); background-repeat: no-repeat; }";
		}

		return css;
	};

	DirectoryEncoder.prototype._stats = function( file ){
		var encoder, extension = path.extname( file );

		if( typeof DirectoryEncoder.encoders[extension.replace(".", "")] === "undefined" ){
			throw new Error( "Encoder does not recognize file type: " + file );
		}

		encoder = new DirectoryEncoder.encoders[extension.replace(".", "")]( file );

		return encoder.stats();
	};

	DirectoryEncoder.prototype._datauri = function( file ) {
		var encoder, extension = path.extname( file );

		if( typeof DirectoryEncoder.encoders[extension.replace(".", "")] === "undefined" ){
			throw new Error( "Encoder does not recognize file type: " + file );
		}

		encoder = new DirectoryEncoder.encoders[extension.replace(".", "")]( file );

		// TODO passthrough of options is generally a code smell
		return encoder.encode( this.options );
	};

	DirectoryEncoder.prototype._checkName = function( seen, name ) {
		if( seen[name] ){
			throw new Error("Two files with the same name: `" + name + "` exist in the input directory");
		}

		seen[name] = true;
	};

	DirectoryEncoder.prototype._loadTemplate = function( templateFile ) {
		var tmpl;

		if( !templateFile ) { return false; }

		if( fs.existsSync( templateFile ) && fs.lstatSync( templateFile ).isFile() ){
			var source = fs.readFileSync( templateFile ).toString( 'utf-8' );
			this.options.handlebarsCallback(Handlebars);
			tmpl = Handlebars.compile(source);
		} else {
			throw new Error( "Template file either doesn't exist or isn't a file" );
		}

		return tmpl;
	};

	module.exports = DirectoryEncoder;
}());
<|endoftext|>


<commit_before>"use strict";

// this is our web scraping library, which we need to get wait times and opening hours
var osmosis = require("osmosis");
// cookie library for reading geocookie for wait times
var cookie = require("cookie");

var Moment = require("moment-timezone");

var Park = require("../park");
var Settings = require("../settings");
var GeoLocation = require("../geoLocation");
var HTTP = require("../http");

var cacheKey = "disneytokyoapi_";

var s_parkID = Symbol();
var s_parkKind = Symbol();
var s_parkLocationMin = Symbol();
var s_parkLocationMax = Symbol();

// token history (maybe we can just calculate it instead of fetching it??)
// 25th 17:30: 2Gz2/J+AAhLVQsYnVWXzmFRR
// 25th 18:00: 2Gz2/J+AAhLVQsYnVWXzmFRR
// September 25th: 2Gz24oWAAhLVQsYnVWXzmFRS
// September 5th:  2GH25YWLFRfNG9QlVGL1kV9XCg==
// August 29th:    2GD2/J+AAhLVQsYnVWXzmVRe
// August 28th:    2GD2/J+AAhLVQsYnVWXzmVRe
// July 31st:      2GT24oWAAhLVQsYnVWXzllVW
// July 13th:      2WP25ZibFADIA403VmPyl1ZQDbc=
// May 31st:       2WX25YWLFRfNG9QlVGL1kVNUDQ==
// May 30th:       2WX2/J+AAhLVQsYnVWXzlFVX
// April 17th:     2mH24oWAAhLVQsYnVWXzlVdQ
// April 16th:     2mH24pGaEwHIA403VmPyl1ZTDbU=

/**
 * Implements the Tokyo Disneyland API framework.
 * @class
 * @extends Park
 */
class DisneyTokyoPark extends Park {
    /**
     * Create new DisneyTokyoPark Object.
     * This object should not be called directly, but rather extended for each of the individual Tokyo Disneyland parks
     * @param {Object} options
     * @param {String} options.park_id Tokyo Disneyland API park ID
     */
    constructor(options = {}) {
        options.name = options.name || "Tokyo Disneyland Park";

        // inherit from base class
        super(options);

        // assign park configurations
        if (!options.park_id) throw new Error("Missing park's API ID");
        this[s_parkID] = options.park_id;
        if (!options.park_kind) throw new Error("Missing park's kind ID");
        this[s_parkKind] = options.park_kind;
        // geoip range for generating valid cookie
        //  specify as two Location points
        if (!options.location_min || !options.location_min instanceof GeoLocation) throw new Error("Missing park's min location");
        if (!options.location_max || !options.location_max instanceof GeoLocation) throw new Error("Missing park's max location");
        this[s_parkLocationMin] = options.location_min;
        this[s_parkLocationMax] = options.location_max;
    }

    // override Fastpass Getter to declare support for Fastpass
    get FastPass() {
        return true;
    }

    /**
     * Refresh/Fetch new Wait Times for this Tokyo Disney Resort park
     * @returns {Promise}
     */
    FetchWaitTimes() {
        return new Promise(function(resolve, reject) {
            // get our access token
            this.GetAcccessToken().then(function(access_token) {
                // get ride data
                this.GetRideNames().then(function(rideData) {

                    // we need to loop through rides first, then apply them
                    //  the site returns zero data out-of-hours, so we need to manually inject known-rides when data is missing
                    var currentRideStatus = {};

                    // request wait times URL
                    osmosis.get(`http://info.tokyodisneyresort.jp/rt/s/realtime/${this[s_parkID]}_attraction.html`)
                        .config("headers", {
                            "Cookie": "tdrloc=" + encodeURIComponent(access_token)
                        })
                        .find(".schedule .midArw > a")
                        .set({
                            "link": "@href",
                            "waittime": ".waitTime",
                            // if FastPass tickets are still available
                            "fastpassAvailable": ".fp",
                            // if FastPass should be available, but they ran out of tickets
                            "fastpassExpired": ".fp-no",
                            // look for various closed notices
                            "closedNotice": ".run",
                        })
                        // append TDR_OSMOSIS to each log message from osmosis
                        .log(function() {
                            var logMessage = [].splice.call(arguments, 0);
                            logMessage.splice(0, 0, "TDR_OSMOSIS");
                            this.Log.apply(this, logMessage);
                        }.bind(this))
                        // callback for each ride on the page
                        .data(function(ride) {
                            // ignore if we didn't find a link
                            if (!ride.link) return;

                            // extract ride ID from it's link
                            var ride_id_match = /attraction\/detail\/str_id\:([a-z0-9_]+)/gi.exec(ride.link);
                            // skip if this isn't a ride
                            if (!ride_id_match) return;

                            // parse Int from string
                            var waitTime = parseInt(ride.waittime, 10);

                            if (ride.closedNotice) {
                                if (
                                    // ride down for refurb/off-season (closed all day)
                                    (ride.closedNotice.indexOf("運営・公演中止") >= 0) ||
                                    // ride unexpectedly closed
                                    (ride.closedNotice.indexOf("一時運営中止") >= 0) ||
                                    // "special type" closed, for non-standard attractions such as walking experiences or transport
                                    (ride.closedNotice.indexOf("案内終了") >= 0)
                                ) {
                                    waitTime = -1;
                                }
                            }
                            // push to our object for using in done() later
                            currentRideStatus[ride_id_match[1]] = {
                                // wait time
                                WaitTime: isNaN(waitTime) ? -1 : waitTime,
                                // FastPass status (based on presence of either of these HTML elements)
                                FastPass: (ride.fastpassAvailable || ride.fastpassExpired) ? true : false,
                            };
                        }.bind(this)).done(function() {
                            // now we're done parsing, check for any rides not on the website
                            for (var rideID in rideData) {
                                var rideObject = this.GetRideObject({
                                    id: rideID,
                                    name: rideData[rideID]
                                });

                                if (rideObject) {
                                    if (currentRideStatus[rideID]) {
                                        // update ride with new waittime data
                                        rideObject.WaitTime = currentRideStatus[rideID].WaitTime;
                                        rideObject.FastPass = currentRideStatus[rideID].FastPass;
                                    } else {
                                        // ride isn't on the website. Most like park is closed, so set wait time to -1 to mark as inactive
                                        rideObject.WaitTime = -1;
                                    }
                                }
                            }

                            resolve();
                        }.bind(this));
                }.bind(this), reject);
            }.bind(this), reject);
        }.bind(this));
    }

    /**
     * Get the Geo-Locked access token for accessing Tokyo Disneyland wait times
     * @returns {Promise<String>} tdrloc cookie needed for accessing wait time pages
     */
    GetAcccessToken() {
        return new Promise(function(resolve, reject) {
            // check the cache first
            Settings.Cache.get(cacheKey + "geocookie", function(err, access_token) {
                if (err || !access_token) {
                    // generate a new geo cookie for accessing Tokyo ride data
                    var randomGeoLocation = GeoLocation.RandomBetween(this[s_parkLocationMin], this[s_parkLocationMax]);

                    // request cookie for accessing wait times using a random location in the park
                    HTTP({
                        url: `http://info.tokyodisneyresort.jp/s/gps/${this[s_parkID]}_index.html`,
                        data: {
                            nextUrl: `http://info.tokyodisneyresort.jp/rt/s/realtime/${this[s_parkID]}_attraction.html`,
                            lat: randomGeoLocation.LatitudeRaw,
                            lng: randomGeoLocation.LongitudeRaw
                        },
                        headers: {
                            "Referer": `http://www.tokyodisneyresort.jp/en/attraction/lists/park:${this[s_parkID]}`,
                        },
                        // don't actually follow the redirect, we just want the cookie
                        follow_max: 0,
                        // we are actually only interested in the headers, so get the full response, not the body
                        returnFullResponse: true
                    }).then(function(resp) {
                        if (resp && resp.headers && resp.headers["set-cookie"] && resp.headers["set-cookie"].length) {
                            // hunt for the tdrloc cookie
                            var GPSCookie, GPSExpiresIn = 60 * 30;
                            for (var i = 0, cookie_string; cookie_string = resp.headers["set-cookie"][i++];) {
                                var cookie_data = cookie.parse(cookie_string);

                                // search for any tdrloc cookie
                                //  keep searching and keep the last set one
                                //  their server usually sets it twice, first deleting it, then setting the correct one
                                if (cookie_data && cookie_data.tdrloc) {
                                    GPSCookie = cookie_data.tdrloc;
                                    // parse cookie date to calculate expirey time in seconds
                                    GPSExpiresIn = Moment().diff(Moment(cookie_data.expires, "ddd, DD-MMM-YYYY HH:mm:ss z"), "seconds");

                                    // the cookie can actually be negative if the park is closed (weird, but OK)
                                    //  if this is so, keep the current one for 5 minutes and try again
                                    if (GPSExpiresIn < 0) {
                                        GPSExpiresIn = 60 * 5;
                                    }
                                }
                            }

                            // did we find the cookie?
                            if (GPSCookie) {
                                // save in the cache and return
                                resolve(GPSCookie);
                                Settings.Cache.set(cacheKey + "geocookie", GPSCookie, {
                                    ttl: GPSExpiresIn
                                });
                            } else {
                                return reject("Failed to find GPS Cookie from TDR website");
                            }
                        } else {
                            return reject("Missing GeoCookie from TDR response");
                        }
                    }.bind(this), reject);
                } else {
                    return resolve(access_token);
                }
            }.bind(this));
        }.bind(this));
    }

    /**
     * Fetch English ride names from the API
     * @returns {Promise<Object>} `rideID` to English names
     */
    GetRideNames() {
        return new Promise(function(resolve, reject) {
            Settings.Cache.wrap(cacheKey + `${this[s_parkID]}_ridenames`, function(callback) {
                // fetch ride names
                HTTP({
                    url: `http://www.tokyodisneyresort.jp/api/v1/wapi_attractions/lists/sort_type:1/locale:1/park_kind:${this[s_parkKind]}/`,
                    forceJSON: true,
                    headers: {
                        "Referer": `http://www.tokyodisneyresort.jp/en/attraction/lists/park:${this[s_parkID]}`,
                    },
                }).then(function(body) {
                    if (!body || !body.entries || !body.entries.length) {
                        return callback("Failed to find entries in ride data response");
                    }

                    // populate data
                    var rideData = {};
                    for (var i = 0, ride; ride = body.entries[i++];) {
                        // use English if we can, fallback to yomi if we're missing an English name
                        rideData[ride.str_id] = ride.name || ride.name_yomi;
                    }

                    // return for caching
                    return callback(null, rideData);
                }.bind(this), callback);
            }.bind(this), {
                // cache for 12 hours
                ttl: 60 * 60 * 12
            }, function(err, rideData) {
                if (err) return reject(err);
                resolve(rideData);
            });
        }.bind(this));
    }
}

module.exports = DisneyTokyoPark;<commit_msg>Add operating hour fetching to Tokyo Disney Resort<commit_after>"use strict";

// this is our web scraping library, which we need to get wait times and opening hours
var osmosis = require("osmosis");
// cookie library for reading geocookie for wait times
var cookie = require("cookie");

var Moment = require("moment-timezone");

var Park = require("../park");
var Settings = require("../settings");
var GeoLocation = require("../geoLocation");
var HTTP = require("../http");

var cacheKey = "disneytokyoapi_";

var s_parkID = Symbol();
var s_parkKind = Symbol();
var s_parkLocationMin = Symbol();
var s_parkLocationMax = Symbol();

// token history (maybe we can just calculate it instead of fetching it??)
// 25th 17:30: 2Gz2/J+AAhLVQsYnVWXzmFRR
// 25th 18:00: 2Gz2/J+AAhLVQsYnVWXzmFRR
// September 25th: 2Gz24oWAAhLVQsYnVWXzmFRS
// September 5th:  2GH25YWLFRfNG9QlVGL1kV9XCg==
// August 29th:    2GD2/J+AAhLVQsYnVWXzmVRe
// August 28th:    2GD2/J+AAhLVQsYnVWXzmVRe
// July 31st:      2GT24oWAAhLVQsYnVWXzllVW
// July 13th:      2WP25ZibFADIA403VmPyl1ZQDbc=
// May 31st:       2WX25YWLFRfNG9QlVGL1kVNUDQ==
// May 30th:       2WX2/J+AAhLVQsYnVWXzlFVX
// April 17th:     2mH24oWAAhLVQsYnVWXzlVdQ
// April 16th:     2mH24pGaEwHIA403VmPyl1ZTDbU=

/**
 * Implements the Tokyo Disneyland API framework.
 * @class
 * @extends Park
 */
class DisneyTokyoPark extends Park {
    /**
     * Create new DisneyTokyoPark Object.
     * This object should not be called directly, but rather extended for each of the individual Tokyo Disneyland parks
     * @param {Object} options
     * @param {String} options.park_id Tokyo Disneyland API park ID
     */
    constructor(options = {}) {
        options.name = options.name || "Tokyo Disneyland Park";

        // inherit from base class
        super(options);

        // assign park configurations
        if (!options.park_id) throw new Error("Missing park's API ID");
        this[s_parkID] = options.park_id;
        if (!options.park_kind) throw new Error("Missing park's kind ID");
        this[s_parkKind] = options.park_kind;
        // geoip range for generating valid cookie
        //  specify as two Location points
        if (!options.location_min || !options.location_min instanceof GeoLocation) throw new Error("Missing park's min location");
        if (!options.location_max || !options.location_max instanceof GeoLocation) throw new Error("Missing park's max location");
        this[s_parkLocationMin] = options.location_min;
        this[s_parkLocationMax] = options.location_max;
    }

    // override Fastpass Getter to declare support for Fastpass
    get FastPass() {
        return true;
    }

    /**
     * Refresh/Fetch new Wait Times for this Tokyo Disney Resort park
     * @returns {Promise}
     */
    FetchWaitTimes() {
        return new Promise(function(resolve, reject) {
            // get our access token
            this.GetAcccessToken().then(function(access_token) {
                // get ride data
                this.GetRideNames().then(function(rideData) {

                    // we need to loop through rides first, then apply them
                    //  the site returns zero data out-of-hours, so we need to manually inject known-rides when data is missing
                    var currentRideStatus = {};

                    // request wait times URL
                    osmosis.get(`http://info.tokyodisneyresort.jp/rt/s/realtime/${this[s_parkID]}_attraction.html`)
                        .config("headers", {
                            "Cookie": "tdrloc=" + encodeURIComponent(access_token)
                        })
                        .find(".schedule .midArw > a")
                        .set({
                            "link": "@href",
                            "waittime": ".waitTime",
                            // if FastPass tickets are still available
                            "fastpassAvailable": ".fp",
                            // if FastPass should be available, but they ran out of tickets
                            "fastpassExpired": ".fp-no",
                            // look for various closed notices
                            "closedNotice": ".run",
                        })
                        // append TDR_OSMOSIS to each log message from osmosis
                        .log(function() {
                            var logMessage = [].splice.call(arguments, 0);
                            logMessage.splice(0, 0, "TDR_OSMOSIS");
                            this.Log.apply(this, logMessage);
                        }.bind(this))
                        // callback for each ride on the page
                        .data(function(ride) {
                            // ignore if we didn't find a link
                            if (!ride.link) return;

                            // extract ride ID from it's link
                            var ride_id_match = /attraction\/detail\/str_id\:([a-z0-9_]+)/gi.exec(ride.link);
                            // skip if this isn't a ride
                            if (!ride_id_match) return;

                            // parse Int from string
                            var waitTime = parseInt(ride.waittime, 10);

                            if (ride.closedNotice) {
                                if (
                                    // ride down for refurb/off-season (closed all day)
                                    (ride.closedNotice.indexOf("運営・公演中止") >= 0) ||
                                    // ride unexpectedly closed
                                    (ride.closedNotice.indexOf("一時運営中止") >= 0) ||
                                    // "special type" closed, for non-standard attractions such as walking experiences or transport
                                    (ride.closedNotice.indexOf("案内終了") >= 0)
                                ) {
                                    waitTime = -1;
                                }
                            }
                            // push to our object for using in done() later
                            currentRideStatus[ride_id_match[1]] = {
                                // wait time
                                WaitTime: isNaN(waitTime) ? -1 : waitTime,
                                // FastPass status (based on presence of either of these HTML elements)
                                FastPass: (ride.fastpassAvailable || ride.fastpassExpired) ? true : false,
                            };
                        }.bind(this)).done(function() {
                            // now we're done parsing, check for any rides not on the website
                            for (var rideID in rideData) {
                                var rideObject = this.GetRideObject({
                                    id: rideID,
                                    name: rideData[rideID]
                                });

                                if (rideObject) {
                                    if (currentRideStatus[rideID]) {
                                        // update ride with new waittime data
                                        rideObject.WaitTime = currentRideStatus[rideID].WaitTime;
                                        rideObject.FastPass = currentRideStatus[rideID].FastPass;
                                    } else {
                                        // ride isn't on the website. Most like park is closed, so set wait time to -1 to mark as inactive
                                        rideObject.WaitTime = -1;
                                    }
                                }
                            }

                            resolve();
                        }.bind(this));
                }.bind(this), reject);
            }.bind(this), reject);
        }.bind(this));
    }

    FetchOpeningTimes() {
        return new Promise(function(resolve, reject) {
            // fetch 3 months of schedule data
            var today = Moment().tz(this.Timezone);
            this.FetchOpeningTimesForMonth(today.format("YYYYMM")).then(function() {
                today.add(1, "month");
                this.FetchOpeningTimesForMonth(today.format("YYYYMM")).then(function() {
                    today.add(1, "month");
                    this.FetchOpeningTimesForMonth(today.format("YYYYMM")).then(function() {
                        resolve();
                    }.bind(this), reject);
                }.bind(this), reject);
            }.bind(this), reject);
        }.bind(this));
    }

    FetchOpeningTimesForMonth(month) {
        return new Promise(function(resolve, reject) {
            HTTP({
                url: `http://www.tokyodisneyresort.jp/api/v1/wapi_monthlycalendars/detail/ym:${month}/`,
                headers: {
                    "Referer": `http://www.tokyodisneyresort.jp/en/attraction/lists/park:${this[s_parkID]}`,
                    "X-Requested-With": "XMLHttpRequest"
                },
                forceJSON: true
            }).then(function(body) {
                if (!body || !body.entry) return reject("Failed to find data from TDR calendar");

                for (var date in body.entry) {
                    if (body.entry[date][this[s_parkID]]) {
                        var scheduleDate = Moment(date, "YYYY/MM/DD");
                        this.Schedule.SetDate({
                            date: scheduleDate,
                            openingTime: Moment.tz(scheduleDate.format("YYYY-MM-DD") + " " + body.entry[date][this[s_parkID]].open_time_1, "YYYY-MM-DD HH:mm", this.Timezone),
                            closingTime: Moment.tz(scheduleDate.format("YYYY-MM-DD") + " " + body.entry[date][this[s_parkID]].close_time_1, "YYYY-MM-DD HH:mm", this.Timezone),
                        });
                    }
                }

                resolve();
            }.bind(this), reject);
        }.bind(this));
    }

    /**
     * Get the Geo-Locked access token for accessing Tokyo Disneyland wait times
     * @returns {Promise<String>} tdrloc cookie needed for accessing wait time pages
     */
    GetAcccessToken() {
        return new Promise(function(resolve, reject) {
            // check the cache first
            Settings.Cache.get(cacheKey + "geocookie", function(err, access_token) {
                if (err || !access_token) {
                    // generate a new geo cookie for accessing Tokyo ride data
                    var randomGeoLocation = GeoLocation.RandomBetween(this[s_parkLocationMin], this[s_parkLocationMax]);

                    // request cookie for accessing wait times using a random location in the park
                    HTTP({
                        url: `http://info.tokyodisneyresort.jp/s/gps/${this[s_parkID]}_index.html`,
                        data: {
                            nextUrl: `http://info.tokyodisneyresort.jp/rt/s/realtime/${this[s_parkID]}_attraction.html`,
                            lat: randomGeoLocation.LatitudeRaw,
                            lng: randomGeoLocation.LongitudeRaw
                        },
                        headers: {
                            "Referer": `http://www.tokyodisneyresort.jp/en/attraction/lists/park:${this[s_parkID]}`,
                        },
                        // don't actually follow the redirect, we just want the cookie
                        follow_max: 0,
                        // we are actually only interested in the headers, so get the full response, not the body
                        returnFullResponse: true
                    }).then(function(resp) {
                        if (resp && resp.headers && resp.headers["set-cookie"] && resp.headers["set-cookie"].length) {
                            // hunt for the tdrloc cookie
                            var GPSCookie, GPSExpiresIn = 60 * 30;
                            for (var i = 0, cookie_string; cookie_string = resp.headers["set-cookie"][i++];) {
                                var cookie_data = cookie.parse(cookie_string);

                                // search for any tdrloc cookie
                                //  keep searching and keep the last set one
                                //  their server usually sets it twice, first deleting it, then setting the correct one
                                if (cookie_data && cookie_data.tdrloc) {
                                    GPSCookie = cookie_data.tdrloc;
                                    // parse cookie date to calculate expirey time in seconds
                                    GPSExpiresIn = Moment().diff(Moment(cookie_data.expires, "ddd, DD-MMM-YYYY HH:mm:ss z"), "seconds");

                                    // the cookie can actually be negative if the park is closed (weird, but OK)
                                    //  if this is so, keep the current one for 5 minutes and try again
                                    if (GPSExpiresIn < 0) {
                                        GPSExpiresIn = 60 * 5;
                                    }
                                }
                            }

                            // did we find the cookie?
                            if (GPSCookie) {
                                // save in the cache and return
                                resolve(GPSCookie);
                                Settings.Cache.set(cacheKey + "geocookie", GPSCookie, {
                                    ttl: GPSExpiresIn
                                });
                            } else {
                                return reject("Failed to find GPS Cookie from TDR website");
                            }
                        } else {
                            return reject("Missing GeoCookie from TDR response");
                        }
                    }.bind(this), reject);
                } else {
                    return resolve(access_token);
                }
            }.bind(this));
        }.bind(this));
    }

    /**
     * Fetch English ride names from the API
     * @returns {Promise<Object>} `rideID` to English names
     */
    GetRideNames() {
        return new Promise(function(resolve, reject) {
            Settings.Cache.wrap(cacheKey + `${this[s_parkID]}_ridenames`, function(callback) {
                // fetch ride names
                HTTP({
                    url: `http://www.tokyodisneyresort.jp/api/v1/wapi_attractions/lists/sort_type:1/locale:1/park_kind:${this[s_parkKind]}/`,
                    forceJSON: true,
                    headers: {
                        "Referer": `http://www.tokyodisneyresort.jp/en/attraction/lists/park:${this[s_parkID]}`,
                    },
                }).then(function(body) {
                    if (!body || !body.entries || !body.entries.length) {
                        return callback("Failed to find entries in ride data response");
                    }

                    // populate data
                    var rideData = {};
                    for (var i = 0, ride; ride = body.entries[i++];) {
                        // use English if we can, fallback to yomi if we're missing an English name
                        rideData[ride.str_id] = ride.name || ride.name_yomi;
                    }

                    // return for caching
                    return callback(null, rideData);
                }.bind(this), callback);
            }.bind(this), {
                // cache for 12 hours
                ttl: 60 * 60 * 12
            }, function(err, rideData) {
                if (err) return reject(err);
                resolve(rideData);
            });
        }.bind(this));
    }
}

module.exports = DisneyTokyoPark;<|endoftext|>


<commit_before>const hasOwn = Object.prototype.hasOwnProperty

export default function shallowEqual(a, b) {
  if (a === b) return true

  let countA = 0
  let countB = 0

  for (let key in a) {
    if (hasOwn.call(a, key) && a[key] !== b[key]) return false
    countA++
  }

  for (let key in b) {
    if (hasOwn.call(b, key)) countB++
  }

  return countA === countB
}
<commit_msg>Use shallowEqual from fbjs. (#591)<commit_after>const hasOwn = Object.prototype.hasOwnProperty

function is(x, y) {
  if (x === y) {
    return x !== 0 || y !== 0 || 1 / x === 1 / y
  } else {
    return x !== x && y !== y
  }
}

export default function shallowEqual(objA, objB) {
  if (is(objA, objB)) return true

  if (typeof objA !== 'object' || objA === null ||
      typeof objB !== 'object' || objB === null) {
    return false
  }

  const keysA = Object.keys(objA)
  const keysB = Object.keys(objB)

  if (keysA.length !== keysB.length) return false

  for (let i = 0; i < keysA.length; i++) {
    if (!hasOwn.call(objB, keysA[i]) ||
        !is(objA[keysA[i]], objB[keysA[i]])) {
      return false
    }
  }

  return true
}
<|endoftext|>


<commit_before>/**
 *
 * @copyright Copyright (c) 2019, Daniel Calviño Sánchez (danxuliu@gmail.com)
 *
 * @license GNU AGPL version 3 or any later version
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

import './shims/MediaStream'
import './shims/MediaStreamTrack'
import Axios from '@nextcloud/axios'
import CancelableRequest from '../cancelableRequest'
import Signaling from '../signaling'
import initWebRtc from './webrtc'
import CallAnalyzer from './analyzers/CallAnalyzer'
import CallParticipantCollection from './models/CallParticipantCollection'
import LocalCallParticipantModel from './models/LocalCallParticipantModel'
import LocalMediaModel from './models/LocalMediaModel'
import MediaDevicesManager from './MediaDevicesManager'
import SentVideoQualityThrottler from './SentVideoQualityThrottler'
import { PARTICIPANT } from '../../constants'
import { fetchSignalingSettings } from '../../services/signalingService'

let webRtc = null
const callParticipantCollection = new CallParticipantCollection()
const localCallParticipantModel = new LocalCallParticipantModel()
const localMediaModel = new LocalMediaModel()
const mediaDevicesManager = new MediaDevicesManager()
let callAnalyzer = null
let sentVideoQualityThrottler = null

let cancelFetchSignalingSettings = null
let signaling = null
let tokensInSignaling = {}

async function getSignalingSettings(token) {
	// If getSignalingSettings is called again while a previous one was still
	// being executed the previous one is cancelled.
	if (cancelFetchSignalingSettings) {
		cancelFetchSignalingSettings('canceled')
		cancelFetchSignalingSettings = null
	}

	const { request, cancel } = CancelableRequest(fetchSignalingSettings)
	cancelFetchSignalingSettings = cancel

	let settings = null
	try {
		const response = await request({ token })
		settings = response.data.ocs.data

		settings.token = token

		cancelFetchSignalingSettings = null
	} catch (exception) {
		if (Axios.isCancel(exception)) {
			console.debug('Getting the signaling settings for ' + token + ' was cancelled by a newer getSignalingSettings')
		} else {
			console.warn('Failed to get the signaling settings for ' + token)
		}
	}

	return settings
}

async function connectSignaling(token) {
	const settings = await getSignalingSettings(token)
	if (!settings) {
		return
	}

	if (signaling && signaling.settings.server !== settings.server) {
		if (webRtc) {
			webRtc.disconnect()
			webRtc = null
		}
		signaling.disconnect()
		signaling = null

		tokensInSignaling = {}
	}

	if (!signaling) {
		signaling = Signaling.createConnection(settings)
	}

	tokensInSignaling[token] = true
}

let pendingJoinCallToken = null
let startedCall = null
let failedToStartCall = null

function startCall(signaling, configuration) {
	let flags = PARTICIPANT.CALL_FLAG.IN_CALL
	if (configuration) {
		if (configuration.audio) {
			flags |= PARTICIPANT.CALL_FLAG.WITH_AUDIO
		}
		if (configuration.video && signaling.getSendVideoIfAvailable()) {
			flags |= PARTICIPANT.CALL_FLAG.WITH_VIDEO
		}
	}

	signaling.joinCall(pendingJoinCallToken, flags).then(() => {
		startedCall(flags)
	}).catch(error => {
		failedToStartCall(error)
	})
}

function setupWebRtc() {
	if (webRtc) {
		return
	}

	const _signaling = signaling

	webRtc = initWebRtc(_signaling, callParticipantCollection, localCallParticipantModel)
	localCallParticipantModel.setWebRtc(webRtc)
	localMediaModel.setWebRtc(webRtc)

	webRtc.on('localMediaStarted', (configuration) => {
		startCall(_signaling, configuration)
	})
	webRtc.on('localMediaError', () => {
		startCall(_signaling, null)
	})
}

/**
 * Join the given conversation on the respective signaling server with the given sessionId
 *
 * @param {string} token Conversation to join
 * @param {string} sessionId Session id to join with
 * @returns {Promise<void>}
 */
async function signalingJoinConversation(token, sessionId) {
	await connectSignaling(token)
	if (tokensInSignaling[token]) {
		await signaling.joinRoom(token, sessionId)
	}
}

/**
 * Join the call of the given conversation
 *
 * @param {string} token Conversation to join the call
 * @param {int} flags Bitwise combination of PARTICIPANT.CALL_FLAG
 * @returns {Promise<void>} Resolved with the actual flags based on the
 *          available media
 */
async function signalingJoinCall(token, flags) {
	if (tokensInSignaling[token]) {
		pendingJoinCallToken = token

		setupWebRtc()

		sentVideoQualityThrottler = new SentVideoQualityThrottler(localMediaModel, callParticipantCollection)

		if (signaling.hasFeature('mcu')) {
			callAnalyzer = new CallAnalyzer(localMediaModel, localCallParticipantModel, callParticipantCollection)
		} else {
			callAnalyzer = new CallAnalyzer(localMediaModel, null, callParticipantCollection)
		}

		return new Promise((resolve, reject) => {
			startedCall = resolve
			failedToStartCall = reject

			webRtc.startMedia(token, flags)
		})
	}
}

/**
 * Leave the call of the given conversation
 *
 * @param {string} token Conversation to leave the call
 * @returns {Promise<void>}
 */
async function signalingLeaveCall(token) {
	sentVideoQualityThrottler.destroy()
	sentVideoQualityThrottler = null

	callAnalyzer.destroy()
	callAnalyzer = null

	if (tokensInSignaling[token]) {
		await signaling.leaveCall(token)
	}
}

/**
 * Leave the given conversation on the respective signaling server
 *
 * @param {string} token Conversation to leave
 * @returns {Promise<void>}
 */
async function signalingLeaveConversation(token) {
	if (tokensInSignaling[token]) {
		await signaling.leaveRoom(token)
	}
}

/**
 * Immediately kill the signaling connection synchronously
 * This should be called only in the unload handler
 */
function signalingKill() {
	if (signaling) {
		signaling.disconnect()
	}
}

export {
	callParticipantCollection,
	localCallParticipantModel,
	localMediaModel,

	mediaDevicesManager,

	callAnalyzer,

	signalingJoinConversation,
	signalingJoinCall,
	signalingLeaveCall,
	signalingLeaveConversation,
	signalingKill,
}
<commit_msg>Listen to local media events only once when joining a call<commit_after>/**
 *
 * @copyright Copyright (c) 2019, Daniel Calviño Sánchez (danxuliu@gmail.com)
 *
 * @license GNU AGPL version 3 or any later version
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

import './shims/MediaStream'
import './shims/MediaStreamTrack'
import Axios from '@nextcloud/axios'
import CancelableRequest from '../cancelableRequest'
import Signaling from '../signaling'
import initWebRtc from './webrtc'
import CallAnalyzer from './analyzers/CallAnalyzer'
import CallParticipantCollection from './models/CallParticipantCollection'
import LocalCallParticipantModel from './models/LocalCallParticipantModel'
import LocalMediaModel from './models/LocalMediaModel'
import MediaDevicesManager from './MediaDevicesManager'
import SentVideoQualityThrottler from './SentVideoQualityThrottler'
import { PARTICIPANT } from '../../constants'
import { fetchSignalingSettings } from '../../services/signalingService'

let webRtc = null
const callParticipantCollection = new CallParticipantCollection()
const localCallParticipantModel = new LocalCallParticipantModel()
const localMediaModel = new LocalMediaModel()
const mediaDevicesManager = new MediaDevicesManager()
let callAnalyzer = null
let sentVideoQualityThrottler = null

let cancelFetchSignalingSettings = null
let signaling = null
let tokensInSignaling = {}

async function getSignalingSettings(token) {
	// If getSignalingSettings is called again while a previous one was still
	// being executed the previous one is cancelled.
	if (cancelFetchSignalingSettings) {
		cancelFetchSignalingSettings('canceled')
		cancelFetchSignalingSettings = null
	}

	const { request, cancel } = CancelableRequest(fetchSignalingSettings)
	cancelFetchSignalingSettings = cancel

	let settings = null
	try {
		const response = await request({ token })
		settings = response.data.ocs.data

		settings.token = token

		cancelFetchSignalingSettings = null
	} catch (exception) {
		if (Axios.isCancel(exception)) {
			console.debug('Getting the signaling settings for ' + token + ' was cancelled by a newer getSignalingSettings')
		} else {
			console.warn('Failed to get the signaling settings for ' + token)
		}
	}

	return settings
}

async function connectSignaling(token) {
	const settings = await getSignalingSettings(token)
	if (!settings) {
		return
	}

	if (signaling && signaling.settings.server !== settings.server) {
		if (webRtc) {
			webRtc.disconnect()
			webRtc = null
		}
		signaling.disconnect()
		signaling = null

		tokensInSignaling = {}
	}

	if (!signaling) {
		signaling = Signaling.createConnection(settings)
	}

	tokensInSignaling[token] = true
}

let pendingJoinCallToken = null
let startedCall = null
let failedToStartCall = null

function startCall(signaling, configuration) {
	let flags = PARTICIPANT.CALL_FLAG.IN_CALL
	if (configuration) {
		if (configuration.audio) {
			flags |= PARTICIPANT.CALL_FLAG.WITH_AUDIO
		}
		if (configuration.video && signaling.getSendVideoIfAvailable()) {
			flags |= PARTICIPANT.CALL_FLAG.WITH_VIDEO
		}
	}

	signaling.joinCall(pendingJoinCallToken, flags).then(() => {
		startedCall(flags)
	}).catch(error => {
		failedToStartCall(error)
	})
}

function setupWebRtc() {
	if (webRtc) {
		return
	}

	webRtc = initWebRtc(signaling, callParticipantCollection, localCallParticipantModel)
	localCallParticipantModel.setWebRtc(webRtc)
	localMediaModel.setWebRtc(webRtc)
}

/**
 * Join the given conversation on the respective signaling server with the given sessionId
 *
 * @param {string} token Conversation to join
 * @param {string} sessionId Session id to join with
 * @returns {Promise<void>}
 */
async function signalingJoinConversation(token, sessionId) {
	await connectSignaling(token)
	if (tokensInSignaling[token]) {
		await signaling.joinRoom(token, sessionId)
	}
}

/**
 * Join the call of the given conversation
 *
 * @param {string} token Conversation to join the call
 * @param {int} flags Bitwise combination of PARTICIPANT.CALL_FLAG
 * @returns {Promise<void>} Resolved with the actual flags based on the
 *          available media
 */
async function signalingJoinCall(token, flags) {
	if (tokensInSignaling[token]) {
		pendingJoinCallToken = token

		setupWebRtc()

		sentVideoQualityThrottler = new SentVideoQualityThrottler(localMediaModel, callParticipantCollection)

		if (signaling.hasFeature('mcu')) {
			callAnalyzer = new CallAnalyzer(localMediaModel, localCallParticipantModel, callParticipantCollection)
		} else {
			callAnalyzer = new CallAnalyzer(localMediaModel, null, callParticipantCollection)
		}

		const _signaling = signaling

		return new Promise((resolve, reject) => {
			startedCall = resolve
			failedToStartCall = reject

			const startCallOnceLocalMediaStarted = (configuration) => {
				webRtc.off('localMediaStarted', startCallOnceLocalMediaStarted)
				webRtc.off('localMediaError', startCallOnceLocalMediaError)

				startCall(_signaling, configuration)
			}
			const startCallOnceLocalMediaError = () => {
				webRtc.off('localMediaStarted', startCallOnceLocalMediaStarted)
				webRtc.off('localMediaError', startCallOnceLocalMediaError)

				startCall(_signaling, null)
			}

			// ".once" can not be used, as both handlers need to be removed when
			// just one of them is executed.
			webRtc.on('localMediaStarted', startCallOnceLocalMediaStarted)
			webRtc.on('localMediaError', startCallOnceLocalMediaError)

			webRtc.startMedia(token, flags)
		})
	}
}

/**
 * Leave the call of the given conversation
 *
 * @param {string} token Conversation to leave the call
 * @returns {Promise<void>}
 */
async function signalingLeaveCall(token) {
	sentVideoQualityThrottler.destroy()
	sentVideoQualityThrottler = null

	callAnalyzer.destroy()
	callAnalyzer = null

	if (tokensInSignaling[token]) {
		await signaling.leaveCall(token)
	}
}

/**
 * Leave the given conversation on the respective signaling server
 *
 * @param {string} token Conversation to leave
 * @returns {Promise<void>}
 */
async function signalingLeaveConversation(token) {
	if (tokensInSignaling[token]) {
		await signaling.leaveRoom(token)
	}
}

/**
 * Immediately kill the signaling connection synchronously
 * This should be called only in the unload handler
 */
function signalingKill() {
	if (signaling) {
		signaling.disconnect()
	}
}

export {
	callParticipantCollection,
	localCallParticipantModel,
	localMediaModel,

	mediaDevicesManager,

	callAnalyzer,

	signalingJoinConversation,
	signalingJoinCall,
	signalingLeaveCall,
	signalingLeaveConversation,
	signalingKill,
}
<|endoftext|>


<commit_before>'use strict';

var React = require('react');

var _ = require('lodash');

var MessageActionCreators = require('../../actions/MessageActionCreators');
var VisibilityStore = require('../../stores/VisibilityStore');

var VisibilitySensor = require('react-visibility-sensor');
var MessageItem = require('../common/MessageItem.react');

var _delayed = [];
var debouncedOnVisibleChange = _.debounce(function(isVisible) {
  if (isVisible) {
    if (VisibilityStore.isVisible) {
      MessageActionCreators.setMessageShown(this.props.peer, this.props.message)
    } else {
      _delayed.push({peer: this.props.peer, message: this.props.message})
    }
  }
}, 30, {maxWait: 100});

var ReadableMessage = React.createClass({
  propTypes: {
    peer: React.PropTypes.object.isRequired,
    message: React.PropTypes.object.isRequired
  },

  render: function() {
    return(
      <VisibilitySensor onChange={this._onVisibilityChange}>
        <MessageItem message={this.props.message}/>
      </VisibilitySensor>
    )
  },

  _onVisibilityChange: debouncedOnVisibleChange
});

var MessagesSection = React.createClass({
  propTypes: {
    messages: React.PropTypes.array.isRequired,
    peer: React.PropTypes.object.isRequired
  },

  componentDidMount: function() {
    VisibilityStore.addChangeListener(this._onAppVisibilityChange);
  },

  componentWillUnmount: function() {
    VisibilityStore.removeChangeListener(this._onAppVisibilityChange);
  },

  render: function() {
    var messages = _.map(this.props.messages, this._getMessagesListItem);

    return(

        {messages}

    )
  },

  _getMessagesListItem: function (message, index) {
    return (
      <ReadableMessage key={message.sortKey} peer={this.props.peer} message={message}/>
    );
  },

  _onAppVisibilityChange: function() {
    if (VisibilityStore.isVisible) {
      _delayed.forEach(function(p) {
        MessageActionCreators.setMessageShown(p.peer, p.message)
      });

      _delayed = [];
    }
  }
});

module.exports = MessagesSection;
<commit_msg>fix(web): fixed visibility problems<commit_after>'use strict';

var React = require('react');

var _ = require('lodash');

var MessageActionCreators = require('../../actions/MessageActionCreators');
var VisibilityStore = require('../../stores/VisibilityStore');

var VisibilitySensor = require('react-visibility-sensor');
var MessageItem = require('../common/MessageItem.react');

var _delayed = [];

var flushDelayed = function() {
  _.forEach(_delayed, function(p) {
    MessageActionCreators.setMessageShown(p.peer, p.message)
  });

  _delayed = [];
};

var flushDelayedDebounced = _.debounce(flushDelayed, 30, 100);

var ReadableMessage = React.createClass({
  propTypes: {
    peer: React.PropTypes.object.isRequired,
    message: React.PropTypes.object.isRequired
  },

  render: function() {
    return(
      <VisibilitySensor onChange={this._onVisibilityChange}>
        <MessageItem message={this.props.message}/>
      </VisibilitySensor>
    )
  },

  _onVisibilityChange: function(isVisible) {
    if (isVisible) {
      _delayed.push({peer: this.props.peer, message: this.props.message});

      if (VisibilityStore.isVisible) {
        flushDelayedDebounced();
      }
    }
  }
});

var MessagesSection = React.createClass({
  propTypes: {
    messages: React.PropTypes.array.isRequired,
    peer: React.PropTypes.object.isRequired
  },

  componentDidMount: function() {
    VisibilityStore.addChangeListener(this._onAppVisibilityChange);
  },

  componentWillUnmount: function() {
    VisibilityStore.removeChangeListener(this._onAppVisibilityChange);
  },

  render: function() {
    var messages = _.map(this.props.messages, this._getMessagesListItem);

    return(

        {messages}

    )
  },

  _getMessagesListItem: function (message, index) {
    return (
      <ReadableMessage key={message.sortKey} peer={this.props.peer} message={message}/>
    );
  },

  _onAppVisibilityChange: function() {
    if (VisibilityStore.isVisible) {
      flushDelayed()
    }
  }
});

module.exports = MessagesSection;
<|endoftext|>


<commit_before>import 'file-loader?name=vendor/draggabilly.js!draggabilly/dist/draggabilly.pkgd'
const $ = jQuery

// admin
if ($('body.wp-admin').length) {
  require('./admin')
} else if ($('html.logged-in').length) {
  // front-end logged in
  require('./auth')
}
<commit_msg>fix(AdminComponentPreview/script.js): draggabilly.pkgd.min instead of draggabilly.pkgd<commit_after>import 'file-loader?name=vendor/draggabilly.js!draggabilly/dist/draggabilly.pkgd.min'
const $ = jQuery

// admin
if ($('body.wp-admin').length) {
  require('./admin')
} else if ($('html.logged-in').length) {
  // front-end logged in
  require('./auth')
}
<|endoftext|>


<commit_before>import React from 'react/addons';
import './Me.scss';

import {FontIcon} from 'material-ui';

//todo: how to get ThemeManager in child material-ui master component?
export default class Me extends React.Component{
  render(){
    let themeBackColor = '#00bcd4';
    return (

          Hi everyone, I{"'"}m David Guan.

          Coder, blog writer, books lover.

          <FontIcon style={{fontSize: '6em', color: themeBackColor}}
            className='fa fa-graduation-cap icon left'
            hoverColor='#E74748'/>

            My
            first resume,
            finished on Jun 2, 2015.

            It summarizes my campus life :)

            Working at Newegg.com after Jun 13, 2015.

            Based on ASP.Net

          <FontIcon style={{fontSize: '6em', color: themeBackColor}}
            className='fa fa-laptop icon right'
            hoverColor='#E74748'/>

          <FontIcon style={{fontSize: '6em', color: themeBackColor}}
            className='fa fa-book icon left'
            hoverColor='#E74748'/>

            Every day after work,

            I begin study and practise new techs.

            Never stop improving my skills and efficiency.

            Github profile

            Build project on github repo everyday.

            Enjoy Making contribution to open source.

          <FontIcon style={{fontSize: '6em', color: themeBackColor}}
            className='fa fa-github icon right'
            hoverColor='#E74748'/>

          <FontIcon style={{fontSize: '6em', color: themeBackColor}}
            className='fa fa-file-code-o icon left'
            hoverColor='#E74748'/>

            Enjoy writing Blog,

            but this Blog is in Chinese.

            So I start build this site, with react & node.js.

            I will try my best to do everything!

          <FontIcon style={{fontSize: '6em', color: themeBackColor}}
            className='fa fa-heart-o icon right'
            hoverColor='#E74748'/>

    );
  }
}
<commit_msg>Update Me.<commit_after>import React from 'react/addons';
import './Me.scss';

import {FontIcon} from 'material-ui';

//todo: how to get ThemeManager in child material-ui master component?
export default class Me extends React.Component{
  render(){
    let themeBackColor = '#00bcd4';
    return (

          Hi everyone, I{"'"}m David Guan.

          Coder, blog writer, books lover.

          <FontIcon style={{fontSize: '6em', color: themeBackColor}}
            className='fa fa-graduation-cap icon left'
            hoverColor='#E74748'/>

            My
            first resume,
            finished on Jun 2, 2015.

            It summarizes my campus life :)

            Working at Newegg.com after Jun 13, 2015.

            Based on ASP.Net

          <FontIcon style={{fontSize: '6em', color: themeBackColor}}
            className='fa fa-laptop icon right'
            hoverColor='#E74748'/>

          <FontIcon style={{fontSize: '6em', color: themeBackColor}}
            className='fa fa-book icon left'
            hoverColor='#E74748'/>

            Every day after work,

            I begin study and practise new techs.

            Never stop improving my craft.

            Github profile

            Build project on github repo everyday.

            Enjoy Making contribution to open source.

          <FontIcon style={{fontSize: '6em', color: themeBackColor}}
            className='fa fa-github icon right'
            hoverColor='#E74748'/>

          <FontIcon style={{fontSize: '6em', color: themeBackColor}}
            className='fa fa-file-code-o icon left'
            hoverColor='#E74748'/>

            Enjoy writing Blog,

            but this Blog is in Chinese.

            So I start build this site, with react & node.js.

            I will try my best to do everything!

          <FontIcon style={{fontSize: '6em', color: themeBackColor}}
            className='fa fa-heart-o icon right'
            hoverColor='#E74748'/>

          <FontIcon style={{fontSize: '6em', color: themeBackColor}}
            className='fa fa-group icon left'
            hoverColor='#E74748'/>

            Start working at Wiredcraft

            from Nov 4, 2015.

            I love there.

    );
  }
}
<|endoftext|>


<commit_before>// The autocomplete class constructor. Basically it takes a takes a text input
// element as argument, and sets attributes and methods for this instance.
function Autocomplete(el) {
    // The text input element that should have the suggestion box.
    this.el = el;
    // Disable browser's autocomplete on that element.
    this.el.attr('autocomplete', 'off');
    // Sets the initial value to an empty string.
    this.value = '';
    // Current XMLHttpRequest that is kept so that when another request is
    // started, a unfinished request is aborted. This avoids having several
    // ajax requests at the time.
    this.xhr = false;
    // Url of the autocomplete view, that should parse the queryVariable and
    // return a rendered autocomplete box.
    this.url = false;
    // Time to wait after a key was pressed in the text input before firing an
    // ajax request.
    this.timeout = 100;
    // The id of this autocomplete instance. It should be unique as it is used
    // as key by the plugin registry of Autocomplete instances.
    this.id = false;
    // Fire the autocomplete after that number of characters is in the
    // autocomplete.
    this.minCharacters = 2;
    // Text input default text, used as a placeholder.
    this.defaultValue = 'type your search here';
    // Class of the currently hovered element.
    this.activeClass = 'active';
    // A selector that matches all options of the autocomplete.
    this.iterablesSelector = 'li:has(a)';
    // Name of the variable to pass to the Autocomplete url. For example, if
    // the text input contains 'abc' then it will fetch the autocomplete box
    // from this url
    // this.url + '?' + this.queryVariable + '=abc'.
    this.queryVariable = 'q';
    // Milliseconds after which the script should check if the autocomplete box
    // should be hidden
    this.blurTimeout = 500;
    // Where to append the autocomplete suggestion box, note that it's placed
    // absolutely.
    this.appendTo = $('body');
    // Extra classes to add to the autocomplete box container.
    this.outerContainerClasses = '';
    // Extra data to pass to the autocomplete url.
    this.data = {};

    // Called after an Autocomplete was instanciated *and* overridden.
    this.initialize = function() {
        var autocomplete = this;

        this.el.val(this.defaultValue);
        this.el.live('focus', function() {
            if ($(this).val() == autocomplete.defaultValue) {
                $(this).val('');
            }
        });
        this.el.live('blur', function() {
            if ($(this).val() == '') {
                $(this).val(autocomplete.defaultValue);
            }
        });

        $('.yourlabs_autocomplete.inner_container.id_'+this.id+' ' + this.iterablesSelector).live({
            mouseenter: function(e) {
                $('.yourlabs_autocomplete.inner_container.id_'+autocomplete.id+' ' + autocomplete.iterablesSelector + '.' + autocomplete.activeClass).each(function() {
                    autocomplete.el.trigger('deactivateOption', [autocomplete, $(this)]);
                });
                autocomplete.el.trigger('activateOption', [autocomplete, $(this)]);
            },
            mouseleave: function(e) {
                autocomplete.el.trigger('deactivateOption', [autocomplete, $(this)]);
            },
            click: function(e) {
                e.preventDefault();
                e.stopPropagation();
                autocomplete.el.trigger('selectOption', [$(this)]);
            },
        });

        this.el.keyup(function(e) { autocomplete.refresh(); });

        $('

').appendTo(this.appendTo);
        this.innerContainer = $('.yourlabs_autocomplete.inner_container.id_'+this.id);
        this.outerContainer = $('.yourlabs_autocomplete.outer_container.id_'+this.id);

        if (window.opera) {
            this.el.keypress(function(e) { autocomplete.onKeyPress(e); });
        } else {
            this.el.keydown(function(e) { autocomplete.onKeyPress(e); });
        }
        this.el.blur(function(e) {
            window.setTimeout(function() {
                autocomplete.hide();
            }, autocomplete.blurTimeout);
        });
        this.el.click(function(e) {
            if ($(this).val().length >= autocomplete.minCharacters)
                autocomplete.show();
        });
    }

    this.onKeyPress = function(e) {
        var option;

        switch (e.keyCode) {
            case 27: //KEY_ESC:
                this.el.val();
                this.hide();
                break;
            case 9: //KEY_TAB:
                break;
            case 13: //KEY_RETURN:
                option = this.innerContainer.find(this.iterablesSelector + '.' + this.activeClass);
                if (option) {
                    e.preventDefault();
                    e.stopPropagation();
                    this.el.trigger('selectOption', [option]);
                    this.hide();
                }
                if(e.keyCode === 9){ return; }
                break;
            case 38: //KEY_UP:
                this.move('up');
                break;
            case 40: //KEY_DOWN:
                this.move('down');
                break;
            default:
                return;
        }
        e.stopImmediatePropagation();
        e.preventDefault();
    }

    this.show = function(html) {
        this.fixPosition();
        if ($.trim(this.innerContainer.html()).length == 0 && !this.xhr) {
            this.fetchAutocomplete();
            return;
        }

        if (html) {
            this.innerContainer.html(html);
        }
        if (!this.innerContainer.is(':visible')) {
            this.outerContainer.show();
            this.innerContainer.show();
        }
    }

    this.hide = function() {
        this.outerContainer.hide();
        this.innerContainer.hide();
    }

    this.move = function(way) {
        var current, target, first, last;
        current = this.innerContainer.find(this.iterablesSelector + '.' + this.activeClass);
        first = this.innerContainer.find(this.iterablesSelector + ':first');
        last = this.innerContainer.find(this.iterablesSelector + ':last');

        this.show();

        if (current.length) {
            if (way == 'up') {
                target = current.prevAll(this.iterablesSelector + ':first');
                if (!target.length) {
                    target = last;
                }
            } else {
                target = current.nextAll(this.iterablesSelector + ':first');
                if (!target.length) {
                    target = first;
                }
            }
            this.el.trigger('deactivateOption', [this, current]);
        } else {
            if (way == 'up') {
                target = last;
            } else {
                target = first;
            }
        }
        this.el.trigger('activateOption', [this, target]);
    }

    this.fixPosition = function() {
        var css = {
            'top': Math.floor(this.el.offset()['top']),
            'left': Math.floor(this.el.offset()['left']),
            'position': 'absolute',
        }
        css['top'] += Math.floor(this.el.innerHeight());

        this.outerContainer.css(css);
    }

    this.refresh = function() {
        var newValue;
        newValue = this.el.val();
        if (newValue == this.defaultValue) {
            return false;
        }
        if (newValue.length < this.minCharacters) {
            return false;
        }
        if (newValue == this.value) {
            return false;
        }
        this.value = newValue;
        this.fetchAutocomplete();
    }

    this.fetchAutocomplete = function() {
        var autocomplete, data;

        if (this.xhr) {
            this.xhr.abort();
        }

        autocomplete = this;
        data = this.data;
        data[this.queryVariable] = this.value;
        this.xhr = $.ajax(this.url, {
            'data': data,
            'complete': function(jqXHR, textStatus) {
                autocomplete.fixPosition();
                autocomplete.show(jqXHR.responseText);
                autocomplete.xhr = false;
            },
        });
    }
}

$.fn.yourlabs_autocomplete = function(overrides) {
    var id;
    overrides = overrides ? overrides : {};
    id = overrides.id || this.attr('id');

    if (!(id && this)) {
        alert('failure: the element needs an id attribute, or an id option must be passed');
        return false;
    }

    if ($.fn.yourlabs_autocomplete.registry == undefined) {
        $.fn.yourlabs_autocomplete.registry = {};
    }

    if ($.fn.yourlabs_autocomplete.registry[id] == undefined) {
        $.fn.yourlabs_autocomplete.registry[id] = new Autocomplete(this);
        $.fn.yourlabs_autocomplete.registry[id] = $.extend($.fn.yourlabs_autocomplete.registry[id], overrides);
        $.fn.yourlabs_autocomplete.registry[id].initialize();
    }

    return $.fn.yourlabs_autocomplete.registry[id];
};

$(document).ready(function() {
    $(document).bind('activateOption', function(e, autocomplete, option) {
        option.addClass(autocomplete.activeClass);
    });
    $(document).bind('deactivateOption', function(e, autocomplete, option) {
        option.removeClass(autocomplete.activeClass);
    });
});
<commit_msg>Cleaned autocomplete.js, many BC breaks<commit_after>/*
 * Here is the list of the major difference with other autocomplete scripts:
 *
 * - don't do anything but fire a signal when a suggestion is selected: it's
 *   left as an exercise to the developer to implement whatever he wants when
 *   that happens
 * - don't generate the autocomplete HTML, it should be generated by the server

 * Let's set the vocabulary used in this script, so that we speak the
 * same language:
 *
 * - The text input element is "input"
 * - The default text in the input is "placeholder"
 * - The box that contains a list of choices is "autocomplete"
 * - Each result in the "autocomplete" is a "suggestion"
 *
 * Here is a fantastic schema in ASCII art:
 *
 * +---------------------+ <----- Input
 * | Your city name ? <---------- Placeholder
 * +---------------------+
 * | Paris, France       | <----- Autocomplete
 * | Paris, TX, USA      |
 * | Paris, TN, USA      |
 * | Paris, KY, USA <------------ Suggestion
 * | Paris, IL, USA      |
 * +---------------------+
 *
 * This script defines three signals:
 *
 * - hilightSuggestion: when a suggestion is hilight, or that the user
 *   navigates into a suggestion with the keyboard,
 * - dehilightSuggestion: when a suggestion was hilighed, and that the user
 *   navigates into another suggestion with the keyboard or mouse,
 * - selectSuggestion: when the user clicks on a suggestion, or that he pressed
 *   enter on a hilighted suggestion.
 *
 * They all work the same, here's a trivial example:
 *
 *     $('#your_autocomplete').bind(
 *         'selectSuggestion',
 *         function(e, suggestion, autocomplete) {
 *             alert('You selected: ' + suggestion.html());
 *         }
 *     );
 *
 * Note that 'e' is the variable containing the event object.
 *
 * Also, note that this script is composed of two main parts:
 *
 * - The Autocomplete class that handles all interaction, defined as
 *   `Autocomplete`,
 * - The jQuery plugin that manages Autocomplete instance, defined as
 *   `$.fn.yourlabs_autocomplete`
 */

/*
 * The autocomplete class constructor:
 *
 * - takes a takes a text input element as argument,
 * - sets attributes and methods for this instance.
 *
 * The reason you want to learn about all this script is that you will then be
 * able to override any variable or function in it on a case-per-case basis.
 * However, overriding is the job of the jQuery plugin so the procedure is
 * described there.
 */
function Autocomplete(input) {
    /*
     * Each autocomplete instance should have a unique identifier, so that the
     * jQuery plugin can keep a registry of instances, but also to compose some
     * unique attributes for the generated autocomplete.
     *
     * By default, assume that the input has a unique id attribute that can be
     * re-used. Otherwise, the jQuery plugin will yell.
     */
    this.id = input.attr('id');

    /*
     * The text input element that should have an autocomplete. It should have
     * a unique id attribute, or you'd have to override this.id as described
     * above.
     */
    this.input = input;

    // The value of the input. It is kept as an attribute for optimisation
    // purposes.
    this.value = '';

    // When the input is empty, show this text.
    this.placeholder = 'type your search here';

    /*
     * It is possible to wait until a certain number of characters have been
     * typed in the input before making a request to the server, to limit the
     * number of requests.
     *
     * However, you may want the autocomplete to behave like a select. If you
     * want that a simple click shows the autocomplete, set this to 0.
     */
    this.minimum_characters = 2;

    /*
     * The autocomplete should be above any other element in the page. If your
     * autocomplete is hidden by another element, overriding this attribute
     * could fix it.
     */
    this.autocomplete_z_index = 999999;

    /*
     * The autocomplete HTML. Because of CSS limitations, it is wrapped in 3
     * divs, the outermost being the 'outer container' and the innermost the
     * 'inner container'.
     *
     * The server-side generate autocomplete HTML will be set in the inner
     * container.
     */
    this.autocomplete_container_html = [
        '
',
        '
'
                '
'
                '
'
            '
'
        '
'
    ].join(' ')

    /*
     * The autocomplete is placed in position:absolute. Thus, it doesn't really
     * matter where this script appends the autocomplete container.
     */
    this.append_autocomplete_to = $('body');

    /*
     * In a perfect world, we would hide the autocomplete when the input looses
     * focus (on blur). But in reality, if the user clicks on a suggestion, the
     * input looses focus, and that would hide the autocomplete, *before* we
     * can intercept the click on the suggestion.
     *
     * When the input looses focus, wait for this number of milliseconds before
     * hiding the autocomplete.
     */
    this.hide_after = 500;

    /*
     * The server should have a URL that takes the input value, and responds
     * with the list of suggestions as HTML. In most cases, an absolute URL is
     * better.
     */
    this.url = false;

    /*
     * As the server responds with plain HTML, we need a selector to find the
     * choices that it contains.
     *
     * For example, if the URL returns an HTML body where every result is in a
     * div of class "suggestion", then this should be set to '.suggestion'.
     */
    this.suggestion_selector = '.suggestion';

    /*
     * When the user hovers a suggestion, it is nice to hilight it, for
     * example by changing it's background color. That's the job of CSS code.
     *
     * However, the CSS can not depend on the :hover because the user can
     * hilight suggestions with the keyboard by pressing the up and down
     * keys.
     *
     * To counter that problem, we specify a particular class that will be set
     * on a suggestion when it's 'hilightd', and unset when it's
     * 'dehilighted'.
     */
    this.suggestion_hilight_class = 'active';

    /*
     * The value of the input is passed to the server via a GET variable. This
     * is the name of the variable.
     */
    this.query_variable = 'q';

    /*
     * This dict will also be passed to the server as GET variables.
     *
     * If this autocomplete depends on another user defined value, then the
     * other user defined value should be set in this dict.
     *
     * Consider a country select and a city autocomplete. The city autocomplete
     * should only fetch city suggestions that are in the selected country. To
     * achieve this, update the data with the value of the country select:
     *
     *     $('select[name=country]').change(function() {
     *         $('city[name=country]').yourlabs_autocomplete().data = {
     *             country: $(this).val(),
     *         }
     *     });
     */
    this.data = {};

    /*
     * To avoid several requests to be pending at the same time, the current
     * request is aborted before a new one is sent.
     *
     * This attribute will hold the current XMLHttpRequest.
     */
    this.xhr = false;

    /*
     * Rather than directly setting up the autocomplete (DOM events etc ...) in
     * the constructor, setup is done in this method. This allows to:
     *
     * - instanciate an Autocomplete,
     * - override attribute/methods of the instance,
     * - and *then* setup the instance.
     */
    this.initialize = function() {
        // Selector for suggestions of this autocomplete.
        this.autocomplete_suggestion_selector = [
            '.yourlabs_autocomplete.inner_container.id_' + this.id,
            this.suggestion_selector,
        ].join(' ')

        // Selector for hilighted suggestions of this autocomplete.
        this.hilighted_suggestion_selector = [
            this.autocomplete_suggestion_selector,
            this.suggestion_hilight_class,
        ].join(' ')

        // 'this' is going to be out of scope some times, so we reference it in
        // a local variable.
        var autocomplete = this;

        // Append the container HTML somewhere so that it exists in the DOM.
        $(this.autocomplete_container_html).appendTo(
            this.append_autocomplete_to);

        // Cache the references to the container elements for performance.
        this.inner_container = $('.yourlabs_autocomplete.inner_container.id_'+this.id);
        this.outer_container = $('.yourlabs_autocomplete.outer_container.id_'+this.id);

        // Set the initial input value using the placeholder string.
        this.input.val(this.placeholder);

        this.input.live({
            // Empty the input if it contains the placeholder string so that
            // the user can type his stuff.
            focus: function() {
                if ($(this).val() == autocomplete.placeholder) {
                    $(this).val('');
                }
            },
            blur: function() {
                // Put the placeholder back in the input when the user leaves the
                // input empty.
                if ($(this).val() == '') {
                    $(this).val(autocomplete.placeholder);
                }

                // And hide the autocomplete after a short while.
                window.setTimeout(function() { autocomplete.hide(); },
                    autocomplete.hide_after);
            },
            click: function() {
                // Show the autocomplete when the user clicks on the input,
                // assuming it contains enought characters.
                if ($(this).val().length >= autocomplete.minimum_characters)
                    autocomplete.show();
            }
        });

        /*
         * Bind mouse events to fire signals. Because the same signals will be
         * sent if the user uses keyboard to work with the autocomplete.
         */
        $(this.autocomplete_suggestion_selector).live({
            // When the mouse enters a suggestion ...
            mouseenter: function(e) {
                // ... the first thing we want is to send the dehilight signal
                // for any hilighted suggestion ...
                $(autocomplete.hilighted_suggestion_selector).each(function() {
                    autocomplete.input.trigger('dehilightSuggestion',
                        [$(this), autocomplete]);
                });
                // ... and then sent the hilight signal for the suggestion.
                autocomplete.input.trigger('hilightSuggestion',
                    [$(this), autocomplete]);
            },
            mouseleave: function(e) {
                // Send dehilightSuggestion when the mouse leaves a suggestion.
                autocomplete.input.trigger('dehilightSuggestion',
                    [$(this), autocomplete]);
            },
            click: function(e) {
                // Send selectSuggestion when the user clicks on a suggestion.
                e.preventDefault();
                e.stopPropagation();
                autocomplete.input.trigger('selectSuggestion',
                    [$(this), autocomplete]);
            },
        });

        // Bind keyup in the input to call this.refresh()
        this.input.keyup(function(e) { autocomplete.refresh(); });

        // Bind keyboard events to call this.keypress(), which handles keyboard
        // navigation.
        if (window.opera) {
            this.input.keypress(function(e) { autocomplete.keypress(e); });
        } else {
            this.input.keydown(function(e) { autocomplete.keypress(e); });
        }
    }

    // This function is in charge of keyboard usage.
    this.keypress = function(e) {
        var suggestion;

        switch (e.keyCode) {
            // KEY_ESC pressed hide the autocomplete.
            case 27:
                this.hide();
                break;
            // KEY_RETURN or KEY_TAB pressed, trigger selectSuggestion if a
            // suggestion is hilighted.
            case 9:
            case 13:
                suggestion = $(this.hilighted_suggestion_selector);

                if (suggestion.length) {
                    e.preventDefault();
                    e.stopPropagation();
                    this.input.trigger('selectSuggestion',
                        [suggestion, this]);
                    this.hide();
                }
                break;
            // On KEY_UP, call move()
            case 38:
                this.move('up');
                break;
            // On KEY_DOWN, call move()
            case 40: //KEY_DOWN:
                this.move('down');
                break;
            // Ignore other keypresses.
            default:
                return;
        }

        // We handled our cases, prevent the browser from doing anything
        // unexpected.
        e.stopImmediatePropagation();
        e.preventDefault();
    }

    // This function is in charge of ensuring that a relevant autocomplete is
    // shown.
    this.show = function(html) {
        // First recalculate the absolute position since the autocomplete may
        // have changed position.
        this.fix_position();

        // If the inner container is empty and there is no current pending
        // request, rely on fetch_autocomplete(), which should show the
        // autocomplete as soon as it's done fetching.
        if ($.trim(this.inner_container.html()).length == 0 && !this.xhr) {
            this.fetch_autocomplete();
            return;
        }

        // And actually, fetch_autocomplete() will call show() with the response
        // body as argument.
        if (html) {
            this.inner_container.html(html);
        }

        // Show the inner and outer container only if necessary.
        if (!this.inner_container.is(':visible')) {
            this.outer_container.show();
            this.inner_container.show();
        }
    }

    // This function is in charge of the opposite.
    this.hide = function() {
        this.outer_container.hide();
        this.inner_container.hide();
    }

    // This function is in charge of hilighting the right result from keyboard
    // navigation.
    this.move = function(way) {
        // The current suggestion if any.
        var current = $(this.hilighted_suggestion_selector);
        // The first and last suggestions. If the user presses down on the last
        // suggestion, then the first one will be hilighted.
        var first = $(this.autocomplete_suggestion_selector + ':first');
        var last = $(this.autocomplete_suggestion_selector + ':last');

        // The suggestion that should be hilighted after the move.
        var target;

        // The autocomplete must be shown so that the user sees what suggestion
        // he is hilighting.
        this.show();

        // If a suggestion is currently hilighted:
        if (current.length) {
            if (way == 'up') {
                // The target suggestion becomes the first previous suggestion.
                target = current.prevAll(this.suggestion_selector + ':first');

                // If none, then the last suggestion becomes the target.
                if (!target.length) target = last;
            } else {
                // The target suggestion becomes the first **next** suggestion.
                target = current.nextAll(this.suggestion_selector + ':first');

                // If none, then the first suggestion becomes the target.
                if (!target.length) target = first;
            }

            // Trigger dehilightSuggestion on the currently hilighted suggestion.
            this.input.trigger('dehilightSuggestion',
                [current, autocomplete]);
        } else {
            target = way == 'up' ? last : first;
        }

        // Trigger hilightSuggestion on the target suggestion.
        this.input.trigger('hilightSuggestion',
            [target, autocomplete]);
    }

    // Calculate and set the outer container's absolute positionning.
    this.fix_position = function() {
        var css = {
            'top': Math.floor(this.input.offset()['top']),
            'left': Math.floor(this.input.offset()['left']),
            'position': 'absolute',
        }

        css['top'] += Math.floor(this.input.innerHeight());

        this.outer_container.css(css);
    }

    // Proxy fetch_autocomplete(), with some sanity checks.
    this.refresh = function() {
        var newValue = this.input.val();

        // If the input contains the placehold then abort.
        if (newValue == this.placeholder) return false;

        // If the input doesn't contain enought characters then abort.
        if (newValue.length < this.minimum_characters) return false;

        // If the input hasn't changed value then abort.
        if (newValue == this.value) return false;

        // Set the new current value.
        this.value = newValue;

        // All clear, continue on refreshing the autocomplete.
        this.fetch_autocomplete();
    }

    // Manage requests to this.url.
    this.fetch_autocomplete = function() {
        // Abort any current request.
        if (this.xhr) this.xhr.abort();

        // Again we need this from another scope.
        var autocomplete = this;

        // Add the current value to the data dict.
        this.data[this.query_variable] = this.value;

        // Make an asynchronous GET request to this.url.
        this.xhr = $.ajax(this.url, {
            data: data,
            complete: function(jqXHR, textStatus) {
                // Update and show the autocomplete.
                autocomplete.show(jqXHR.responseText);
                // Clear the current request keeper.
                autocomplete.xhr = false;
            },
        });
    }
}

/*
 * The jQuery plugin that manages Autocomplete instances across the various
 * inputs. It is named 'yourlabs_autocomplete' rather than just 'autocomplete'
 * to live happily with other plugins that may define an autocomplete() jQuery
 * plugin.
 *
 * It takes an array as argument, the array may contain any attribute or
 * function that should override the Autocomplete builtin. For example:
 *
 *     $('input#your_autocomplete').yourlabs_autocomplete({
 *         url: '/some/url/',
 *         hide: function() {
 *             this.outer_container
 *         },
 *     })
 *
 * Also, it implements a simple identity map, which means that:
 *
 *     // First call for an input instanciates the Autocomplete instance
 *     $('input#your_autocomplete').yourlabs_autocomplete({
 *         url: '/some/url/',
 *     });
 *
 *     // Other calls return the previously created Autocomplete instance
 *     $('input#your_autocomplete').yourlabs_autocomplete().data = {
 *         new_data: $('#foo').val(),
 *     }
 *
 */
$.fn.yourlabs_autocomplete = function(overrides) {
    var overrides = overrides ? overrides : {};

    if (overrides.url == undefined) {
        alert('Autocomplete needs a url !');
        return;
    }

    var id = overrides.id || this.attr('id');

    if (id == undefined) {
        alert('Autocomplete must have an id !');
        return;
    }

    // Disable the browser's autocomplete features on that input.
    this.attr('autocomplete', 'off');

    if ($.fn.yourlabs_autocomplete.registry == undefined) {
        // Instanciate a registry for {id: Autocomplete instance}
        $.fn.yourlabs_autocomplete.registry = {};
    }

    // If no Autocomplete instance is defined for this id, make one.
    if ($.fn.yourlabs_autocomplete.registry[id] == undefined) {
        // Instanciate Autocomplete.
        $.fn.yourlabs_autocomplete.registry[id] = new Autocomplete(this);

        // Extend the instance with overrides.
        $.fn.yourlabs_autocomplete.registry[id] = $.extend(
            $.fn.yourlabs_autocomplete.registry[id], overrides);

        // All set, call initialize().
        $.fn.yourlabs_autocomplete.registry[id].initialize();
    }

    // Return the Autocomplete instance for this id from the registry.
    return $.fn.yourlabs_autocomplete.registry[id];
};

// Serves as both an example to set a signal, and to set or unset the hilight class.
$(document).ready(function() {
    $(document).bind('hilightSuggestion', function(e, suggestion, autocomplete) {
        suggestion.addClass(autocomplete.suggestion_hilight_class);
    });
    $(document).bind('dehilightSuggestion', function(e, suggestion, autocomplete) {
        suggestion.removeClass(autocomplete.suggestion_hilight_class);
    });
});

<|endoftext|>