<commit_before>/**
ROBERTO LONARDI - 2016
Api.js Async comunication Library.
API - Javascript Api calls module.

Required libraries for correct working:
- jQuery > 1.2.0
*/

/** Dependency constructor */
function api(){

    /** PRIVATE METHODS
    Private methods are available only from within the dependency.
    ========================================================== */

    /** TO MD5
    Parse the passed string in md5 and return the result. */
    function generateKey(request){
        var string = request.url + JSON.stringify(request.params);

        /** Check if md5 extension is loaded */
        if(md5){
            return md5(string);
        } else {
            return string;
        };

    }

    /** LOAD CACHE
    Load call from cache if present, otherwise return false. */
    function loadCache(request){
        var key = generateKey(request);

        return Api.cache[key];
    };

    /** SAVE CACHE
    Save the call response in memory cache. */
    function saveCache(request, response){
        var key = generateKey(request);

        if(Api.config.dev){ console.info(Api.config.info.name + ': Saving call in cache with key "' + key + '"'); }

        /** Save data in cache */
        Api.cache[key] = response;
    };

    /** GENERATE AJAX OBJECT
    Starting from the request, parse it and returns the
    correct ajax object request. */
    function generateAjaxObject(request){

        /** Generate arguments with fixed values */
        var data = {};

        data['url']    = request.url;
        data['method'] = request.method;

        if(request.accept){         data['accept']        = request.accept; }
        if(request.contentType){    data['contentType']   = request.contentType; }

        if(request.body){ data['data'] = request.body; };

        /** Parse headers */
        if(request.headers){
            data['headers'] = request.headers;
        };

        /** Parse settings */
        if(request.settings){
            $.extend(data, request.settings);
        };

        return data;
    };

    /** CALL
    Actual ajax call with passed parameters. */
    function call(request){

        /** Parse request to generate an ajax compatible object */
        var data = generateAjaxObject(request);

        /** Wehre the magic is done :) */
        var xhr = new Promise(function(resolve, reject){

            $.ajax(data).done(function(response, status, xhr){

                /** Resolve the promise */
                resolve(response, status, xhr);

            }).fail(function(response, status, xhr){

                /** Reject the promise */
                reject(response, status, xhr);

            });

        });

        return xhr;
    };

    /** PARSE REQUEST
    Parse the requests and return the call itself. */
    function parseRequest(request){

        /** If there are params left put them in query string */
        if(request.params){

            /** Add them to the url */
            request.url += Help.toQueryString(request.params);

            /** Delete the params */
            delete request.params;
        };

        /** If url has no http add api base url */
        if(request.url.indexOf('http') < 0){
            request.url = Api.config.paths.api + request.url;
        };

        /** If there is already cache for the call, return directly the cached response */
        if(Api.config.settings.cache.enabled && loadCache(request)){
            var cache = new Promise(function(resolve){

                /** Resolve the promise with the cached response */
                resolve(loadCache(request));

            });

            /** Return the actual call */
            var response = call(request);

            /** Save the response in cache */
            response.then(function(data){
                saveCache(request, data);
            });

            return cache;

        } else {

            /** Return the actual call */
            var response = call(request);

            /** Save the response in cache */
            response.then(function(data){
                saveCache(request, data);
            });

            return response;
        };
    }

    /** CHECK REQUESTS INTEGRITY
    Check if requests are in a correct format and if not return a boolean false. */
    function checkRequestsIntegrity(requests){

        if(!requests){ return false; }

        /** If request is not an array wrap it in one */
        if(!(requests instanceof Array)){
            requests = [requests];
        }

        /** For each request check for essential parameters */
        for(index in requests){
            var request = requests[index];

            if(!request.url){

                console.error(Api.config.info.name + ': No url passed, request aborted.');

                return false;
            }
        }

        return requests;
    };

    /** EXTENSIONS
    Initialize extensions of this dependency.
    (extensions are reachable for children of this dependency) */
    function initializeExtensions(){

        /** Get extensions */
        var exts = Api.config.ext;

        /** Initialize promises */
        var promises = [];

        /** If the are no extensions return a fake Promise */
        if(exts.length === 0){

            /** Fake promise */
            return Help.fakePromise(true);

        }

        /** For each extension create a promise */
        for(index in exts){
            var ext = exts[index];

            /** Create the promise */
            var xhr = new Promise(function(resolve, reject){
                $.getScript(ext, function(extension){
                    resolve(extension);
                }).fail(function(){
                    reject();
                });
            });

            promises.push(xhr);
        }

        return Promise.all(promises);
    };

    /** Dependency prototype */
    var Api = {

        /** CONFIG
        Dependency configuration. */
        config  : {

            /** Base paths settings */
            paths: {
                base    : '',
                api     : ''
            },

            dev: true,

            /** Extensions */
            ext: [
                'ext/helpers.js',
                'ext/md5.js'
                 ],

            /** Dependency general infos */
            info: {
                name: 'Api'
            },

            /** Methods */
            methods: {

                get     : 'GET',
                post    : 'POST',
                put     : 'PUT',
                delete  : 'DELETE'

            },

            /** Default configuration */
            settings: {

                /** Default Accept and contentType for AJAX calls */
                contentType : 'application/json',
                accept      : 'json',

                /** Caching settings */
                cache: {
                    enabled: true
                }

            }
        },

        /** CACHE
        Call cache for faster response. */
        cache   : {},

        /** BUILD
        Object constructors. */
        build   : {

            /** Request object constructor */
            Request: function(url, params, body){

                if(!url){
                    console.error(Api.config.info.name + ': At least url must be passed to create Request object.');
                    return false;
                }

                /** If url has no http add api base url */
                if(url.indexOf('http') < 0){
                    url = Api.config.paths.api + url;
                };

                /** Set params */
                if(params)  {

                    /** Check each parameter and populate the url if is a path parameter */
                    for(key in params){

                        /** Check if the key is in the URL as placeholder */
                        if(Help.checkPlaceholder(key, url)){
                            var placeholder = Help.placeholder(key);

                            /** If is present replace it */
                            url = url.replace(placeholder, params[key]);

                            /** Remove the param from params and leave only the query string ones */
                            delete params[key];
                        };
                    };

                    /** Assign remaining params */
                    this['params'] = params;

                };

                /** Set url, after path parameters have been added */
                this['url'] = url;

                /** Set body */
                if(body)    { this['body'] = body;      };

            }

        },

        /** PUBLIC METHODS
        Public methods can be called from outside the dependency.
        ===================================================== */

        /** INIT
        Dependency initialization. */
        init: function(config, callback){

            /** Extend configuration with the passed one */
            $.extend(true, Api.config, config);

            /** Initialize extensions */
            initializeExtensions().then(function(){

                /** If init callback is present call it at the init very end */
                if(callback){ callback(); }

            });

        },

        /** GET
        Get calls method. */
        get: function(requests, success, error){

            /** Check integrity */
            requests = checkRequestsIntegrity(requests);

            if(!requests){

                console.error(Api.config.info.name + ': No valid requests passes on GET method, aborted.');

                return false;
            }

            /** Initialize promises */
            var promises = [];

            /** Parse each request */
            for(index in requests){
                var request = requests[index];

                /** If the method is already present and different from "GET", the call is skipped */
                if(request.method && request.method !== Api.config.methods.get){

                    console.error(Api.config.info.name + ': Passed method different from GET, request skipped.', request);

                    continue;
                }

                /** Set the request method */
                request['method'] = Api.config.methods.get;

                /** Parse the requests then make the actual call */
                var xhr = parseRequest(request);

                /** Push xhr call in promises array */
                promises.push(xhr);
            }

            /** Parse the Promises to launch the callbacks */
            var callback = new Promise(function(resolve, reject){
                Promise.all(promises).then(function(response, status, xhr){

                    if(success){ success(response, status, xhr); }

                    /** Resolve the promise */
                    resolve(response, status, xhr);

                }).catch(function(response, status, xhr){

                    if(error){ error(response, status, xhr); }

                    /** Resolve the promise */
                    reject(response, status, xhr);

                });
            });

            return callback;
        },

        /** POST
        Post calls method. */
        post: function(requests, success, error){

            /** Check integrity */
            requests = checkRequestsIntegrity(requests);

            if(!requests){

                console.error(Api.config.info.name + ': No valid requests passes on POST method, aborted.');

                return false;
            }

            /** Initialize promises */
            var promises = [];

            /** Parse each request */
            for(index in requests){
                var request = requests[index];

                /** If the method is already present and different from "POST", the call is skipped */
                if(request.method && request.method !== Api.config.methods.post){

                    console.error(Api.config.info.name + ': Passed method different from POST, request skipped.', request);

                    continue;
                }

                /** Set the request method */
                request['method'] = Api.config.methods.post;

                /** Parse the requests then make the actual call */
                var xhr = parseRequest(request);

                /** Push xhr call in promises array */
                promises.push(xhr);

            }

            /** Return all the promises */
            return Promise.all(promises).then(function(response, status, xhr){
                if(success){ success(response, status, xhr); }
            }).catch(function(response, status, xhr){
                if(error){ error(response, status, xhr); }
            });

        },

        /** PUT
        Put calls method. */
        put: function(requests, success, error){

            /** Check integrity */
            requests = checkRequestsIntegrity(requests);

            if(!requests){

                console.error(Api.config.info.name + ': No valid requests passes on PUT method, aborted.');

                return false;
            }

            /** Initialize promises */
            var promises = [];

            /** Parse each request */
            for(index in requests){
                var request = requests[index];

                /** If the method is already present and different from "PUT", the call is skipped */
                if(request.method && request.method !== Api.config.methods.put){

                    console.error(Api.config.info.name + ': Passed method different from PUT, request skipped.', request);

                    continue;
                }

                /** Set the request method */
                request['method'] = Api.config.methods.put;

                /** Parse the requests then make the actual call */
                var xhr = parseRequest(request);

                /** Push xhr call in promises array */
                promises.push(xhr);

            }

            /** Return all the promises */
            return Promise.all(promises).then(function(response, status, xhr){
                if(success){ success(response, status, xhr); }
            }).catch(function(response, status, xhr){
                if(error){ error(response, status, xhr); }
            });

        },

        /** DELETE
        Delete calls method. */
        delete: function(requests, success, error){

            /** Check integrity */
            requests = checkRequestsIntegrity(requests);

            if(!requests){

                console.error(Api.config.info.name + ': No valid requests passes on DELETE method, aborted.');

                return false;
            }

            /** Initialize promises */
            var promises = [];

            /** Parse each request */
            for(index in requests){
                var request = requests[index];

                /** Ifa  method is already present and different from "DELETE", the call is skipped */
                if(request.method && request.method !== Api.config.methods.delete){

                    console.error(Api.config.info.name + ': Passed method different from DELETE, request skipped.', request);

                    continue;
                }

                /** Set the request method */
                request['method'] = Api.config.methods.delete;

                /** Parse the requests then make the actual call */
                var xhr = parseRequest(request);

                /** Push xhr call in promises array */
                promises.push(xhr);

            }

            /** Return all the promises */
            return Promise.all(promises).then(function(response, status, xhr){
                if(success){ success(response, status, xhr); }
            }).catch(function(response, status, xhr){
                if(error){ error(response, status, xhr); }
            });

        },

        /** CALL
        Calls with different methods. */
        call: function(requests, success, error){

            /** Check integrity */
            requests = checkRequestsIntegrity(requests);

            if(!requests){

                console.error(Api.config.info.name + ': No valid requests passes on CALL method, aborted.');

                return false;
            }

            /** Initialize promises */
            var promises = [];

            /** Parse each request */
            for(index in requests){
                var request = requests[index];

                /** If the method is not present, request is skipped */
                if(!request.method){

                    console.error(Api.config.info.name + ': No method found, request skipped.', request);

                    continue;
                }

                /** Parse the requests then make the actual call */
                var xhr = parseRequest(request);

                /** Push xhr call in promises array */
                promises.push(xhr);

            }

            /** Return all the promises */
            return Promise.all(promises).then(function(response, status, xhr){
                if(success){ success(response, status, xhr); }
            }).catch(function(response, status, xhr){
                if(error){ error(response, status, xhr); }
            });
        }

    };

    /** Return dependency prototype. */
    return Api;
}
<commit_msg>Minor performance fix<commit_after>/**
ROBERTO LONARDI - 2016
Api.js Async comunication Library.
API - Javascript Api calls module.

Required libraries for correct working:
- jQuery > 1.2.0
*/

/** Dependency constructor */
function api(){

    /** PRIVATE METHODS
    Private methods are available only from within the dependency.
    ========================================================== */

    /** TO MD5
    Parse the passed string in md5 and return the result. */
    function generateKey(request){
        var string = request.url + JSON.stringify(request.params);

        /** Check if md5 extension is loaded */
        if(md5){
            return md5(string);
        } else {
            return string;
        };

    }

    /** LOAD CACHE
    Load call from cache if present, otherwise return false. */
    function loadCache(request){
        var key = generateKey(request);

        return Api.cache[key];
    };

    /** SAVE CACHE
    Save the call response in memory cache. */
    function saveCache(request, response){
        var key = generateKey(request);

        if(Api.config.dev){ console.info(Api.config.info.name + ': Saving call in cache with key "' + key + '"'); }

        /** Save data in cache */
        Api.cache[key] = response;
    };

    /** GENERATE AJAX OBJECT
    Starting from the request, parse it and returns the
    correct ajax object request. */
    function generateAjaxObject(request){

        /** Generate arguments with fixed values */
        var data = {};

        data['url']    = request.url;
        data['method'] = request.method;

        if(request.accept){         data['accept']        = request.accept; }
        if(request.contentType){    data['contentType']   = request.contentType; }

        if(request.body){ data['data'] = request.body; };

        /** Parse headers */
        if(request.headers){
            data['headers'] = request.headers;
        };

        /** Parse settings */
        if(request.settings){
            $.extend(data, request.settings);
        };

        return data;
    };

    /** CALL
    Actual ajax call with passed parameters. */
    function call(request){

        /** Parse request to generate an ajax compatible object */
        var data = generateAjaxObject(request);

        /** Wehre the magic is done :) */
        var xhr = new Promise(function(resolve, reject){

            $.ajax(data).done(function(response, status, xhr){

                /** Resolve the promise */
                resolve(response, status, xhr);

            }).fail(function(response, status, xhr){

                /** Reject the promise */
                reject(response, status, xhr);

            });

        });

        return xhr;
    };

    /** PARSE REQUEST
    Parse the requests and return the call itself. */
    function parseRequest(request){

        /** If there are params left put them in query string */
        if(request.params){

            /** Add them to the url */
            request.url += Help.toQueryString(request.params);

            /** Delete the params */
            delete request.params;
        };

        /** If url has no http add api base url */
        if(request.url.indexOf('http') < 0){
            request.url = Api.config.paths.api + request.url;
        };

        /** retrive cache if present */
        var cache = loadCache(request);

        /** If there is already cache for the call, return directly the cached response */
        if(Api.config.settings.cache.enabled && ){
            var cache = new Promise(function(resolve){

                /** Resolve the promise with the cached response */
                resolve(cache);

            });

            /** Return the actual call */
            var response = call(request);

            /** Save the response in cache */
            response.then(function(data){
                saveCache(request, data);
            });

            return cache;

        } else {

            /** Return the actual call */
            var response = call(request);

            /** Save the response in cache */
            response.then(function(data){
                saveCache(request, data);
            });

            return response;
        };
    }

    /** CHECK REQUESTS INTEGRITY
    Check if requests are in a correct format and if not return a boolean false. */
    function checkRequestsIntegrity(requests){

        if(!requests){ return false; }

        /** If request is not an array wrap it in one */
        if(!(requests instanceof Array)){
            requests = [requests];
        }

        /** For each request check for essential parameters */
        for(index in requests){
            var request = requests[index];

            if(!request.url){

                console.error(Api.config.info.name + ': No url passed, request aborted.');

                return false;
            }
        }

        return requests;
    };

    /** EXTENSIONS
    Initialize extensions of this dependency.
    (extensions are reachable for children of this dependency) */
    function initializeExtensions(){

        /** Get extensions */
        var exts = Api.config.ext;

        /** Initialize promises */
        var promises = [];

        /** If the are no extensions return a fake Promise */
        if(exts.length === 0){

            /** Fake promise */
            return Help.fakePromise(true);

        }

        /** For each extension create a promise */
        for(index in exts){
            var ext = exts[index];

            /** Create the promise */
            var xhr = new Promise(function(resolve, reject){
                $.getScript(ext, function(extension){
                    resolve(extension);
                }).fail(function(){
                    reject();
                });
            });

            promises.push(xhr);
        }

        return Promise.all(promises);
    };

    /** Dependency prototype */
    var Api = {

        /** CONFIG
        Dependency configuration. */
        config  : {

            /** Base paths settings */
            paths: {
                base    : '',
                api     : ''
            },

            dev: true,

            /** Extensions */
            ext: [
                'ext/helpers.js',
                'ext/md5.js'
                 ],

            /** Dependency general infos */
            info: {
                name: 'Api'
            },

            /** Methods */
            methods: {

                get     : 'GET',
                post    : 'POST',
                put     : 'PUT',
                delete  : 'DELETE'

            },

            /** Default configuration */
            settings: {

                /** Default Accept and contentType for AJAX calls */
                contentType : 'application/json',
                accept      : 'json',

                /** Caching settings */
                cache: {
                    enabled: true
                }

            }
        },

        /** CACHE
        Call cache for faster response. */
        cache   : {},

        /** BUILD
        Object constructors. */
        build   : {

            /** Request object constructor */
            Request: function(url, params, body){

                if(!url){
                    console.error(Api.config.info.name + ': At least url must be passed to create Request object.');
                    return false;
                }

                /** If url has no http add api base url */
                if(url.indexOf('http') < 0){
                    url = Api.config.paths.api + url;
                };

                /** Set params */
                if(params)  {

                    /** Check each parameter and populate the url if is a path parameter */
                    for(key in params){

                        /** Check if the key is in the URL as placeholder */
                        if(Help.checkPlaceholder(key, url)){
                            var placeholder = Help.placeholder(key);

                            /** If is present replace it */
                            url = url.replace(placeholder, params[key]);

                            /** Remove the param from params and leave only the query string ones */
                            delete params[key];
                        };
                    };

                    /** Assign remaining params */
                    this['params'] = params;

                };

                /** Set url, after path parameters have been added */
                this['url'] = url;

                /** Set body */
                if(body)    { this['body'] = body;      };

            }

        },

        /** PUBLIC METHODS
        Public methods can be called from outside the dependency.
        ===================================================== */

        /** INIT
        Dependency initialization. */
        init: function(config, callback){

            /** Extend configuration with the passed one */
            $.extend(true, Api.config, config);

            /** Initialize extensions */
            initializeExtensions().then(function(){

                /** If init callback is present call it at the init very end */
                if(callback){ callback(); }

            });

        },

        /** GET
        Get calls method. */
        get: function(requests, success, error){

            /** Check integrity */
            requests = checkRequestsIntegrity(requests);

            if(!requests){

                console.error(Api.config.info.name + ': No valid requests passes on GET method, aborted.');

                return false;
            }

            /** Initialize promises */
            var promises = [];

            /** Parse each request */
            for(index in requests){
                var request = requests[index];

                /** If the method is already present and different from "GET", the call is skipped */
                if(request.method && request.method !== Api.config.methods.get){

                    console.error(Api.config.info.name + ': Passed method different from GET, request skipped.', request);

                    continue;
                }

                /** Set the request method */
                request['method'] = Api.config.methods.get;

                /** Parse the requests then make the actual call */
                var xhr = parseRequest(request);

                /** Push xhr call in promises array */
                promises.push(xhr);
            }

            /** Parse the Promises to launch the callbacks */
            var callback = new Promise(function(resolve, reject){
                Promise.all(promises).then(function(response, status, xhr){

                    if(success){ success(response, status, xhr); }

                    /** Resolve the promise */
                    resolve(response, status, xhr);

                }).catch(function(response, status, xhr){

                    if(error){ error(response, status, xhr); }

                    /** Resolve the promise */
                    reject(response, status, xhr);

                });
            });

            return callback;
        },

        /** POST
        Post calls method. */
        post: function(requests, success, error){

            /** Check integrity */
            requests = checkRequestsIntegrity(requests);

            if(!requests){

                console.error(Api.config.info.name + ': No valid requests passes on POST method, aborted.');

                return false;
            }

            /** Initialize promises */
            var promises = [];

            /** Parse each request */
            for(index in requests){
                var request = requests[index];

                /** If the method is already present and different from "POST", the call is skipped */
                if(request.method && request.method !== Api.config.methods.post){

                    console.error(Api.config.info.name + ': Passed method different from POST, request skipped.', request);

                    continue;
                }

                /** Set the request method */
                request['method'] = Api.config.methods.post;

                /** Parse the requests then make the actual call */
                var xhr = parseRequest(request);

                /** Push xhr call in promises array */
                promises.push(xhr);

            }

            /** Return all the promises */
            return Promise.all(promises).then(function(response, status, xhr){
                if(success){ success(response, status, xhr); }
            }).catch(function(response, status, xhr){
                if(error){ error(response, status, xhr); }
            });

        },

        /** PUT
        Put calls method. */
        put: function(requests, success, error){

            /** Check integrity */
            requests = checkRequestsIntegrity(requests);

            if(!requests){

                console.error(Api.config.info.name + ': No valid requests passes on PUT method, aborted.');

                return false;
            }

            /** Initialize promises */
            var promises = [];

            /** Parse each request */
            for(index in requests){
                var request = requests[index];

                /** If the method is already present and different from "PUT", the call is skipped */
                if(request.method && request.method !== Api.config.methods.put){

                    console.error(Api.config.info.name + ': Passed method different from PUT, request skipped.', request);

                    continue;
                }

                /** Set the request method */
                request['method'] = Api.config.methods.put;

                /** Parse the requests then make the actual call */
                var xhr = parseRequest(request);

                /** Push xhr call in promises array */
                promises.push(xhr);

            }

            /** Return all the promises */
            return Promise.all(promises).then(function(response, status, xhr){
                if(success){ success(response, status, xhr); }
            }).catch(function(response, status, xhr){
                if(error){ error(response, status, xhr); }
            });

        },

        /** DELETE
        Delete calls method. */
        delete: function(requests, success, error){

            /** Check integrity */
            requests = checkRequestsIntegrity(requests);

            if(!requests){

                console.error(Api.config.info.name + ': No valid requests passes on DELETE method, aborted.');

                return false;
            }

            /** Initialize promises */
            var promises = [];

            /** Parse each request */
            for(index in requests){
                var request = requests[index];

                /** Ifa  method is already present and different from "DELETE", the call is skipped */
                if(request.method && request.method !== Api.config.methods.delete){

                    console.error(Api.config.info.name + ': Passed method different from DELETE, request skipped.', request);

                    continue;
                }

                /** Set the request method */
                request['method'] = Api.config.methods.delete;

                /** Parse the requests then make the actual call */
                var xhr = parseRequest(request);

                /** Push xhr call in promises array */
                promises.push(xhr);

            }

            /** Return all the promises */
            return Promise.all(promises).then(function(response, status, xhr){
                if(success){ success(response, status, xhr); }
            }).catch(function(response, status, xhr){
                if(error){ error(response, status, xhr); }
            });

        },

        /** CALL
        Calls with different methods. */
        call: function(requests, success, error){

            /** Check integrity */
            requests = checkRequestsIntegrity(requests);

            if(!requests){

                console.error(Api.config.info.name + ': No valid requests passes on CALL method, aborted.');

                return false;
            }

            /** Initialize promises */
            var promises = [];

            /** Parse each request */
            for(index in requests){
                var request = requests[index];

                /** If the method is not present, request is skipped */
                if(!request.method){

                    console.error(Api.config.info.name + ': No method found, request skipped.', request);

                    continue;
                }

                /** Parse the requests then make the actual call */
                var xhr = parseRequest(request);

                /** Push xhr call in promises array */
                promises.push(xhr);

            }

            /** Return all the promises */
            return Promise.all(promises).then(function(response, status, xhr){
                if(success){ success(response, status, xhr); }
            }).catch(function(response, status, xhr){
                if(error){ error(response, status, xhr); }
            });
        }

    };

    /** Return dependency prototype. */
    return Api;
}
<|endoftext|>


<commit_before>import _ from "lodash"

// TODO: Replace searching algorithm to
//  fragments->method pattern. (Now is `method->fragments` implement).
// For simplify and reduce searching times.
export default class RouteTree {
    /**
     * @class RouteTree
     * @constructor
     * @param {Object?} routeTree initial route tree
     */
    constructor(routeTree) {
        this.clear();
        routeTree && (this.tree = routeTree);
    }

    /**
     * Clear holding route tree
     * @method clear
     */
    clear() {
        this.tree = Object.create(null);
    }

    /**
     * @method mergeFlattenTree
     * @param {Array<Array<string, Array<string>, Object>} routeList
     *  [0] -> httpMethod
     *  [1] -> url fragments pattern
     *  [2] -> Controller information
     */
    mergeFlattenTree(routeList) {
        const routeTree = Object.create(null);

        _.each(routeList, ([httpMethod, urlFragments, handlerInfo], idx, list) => {
            var targetNode;
            const validUrlFragments = urlFragments.filter(f => f !== "/" && f !== "" & !! f);

            // Build route tree
            const pattern = [];
            validUrlFragments.reduce((parentNode, fragment) => {
                pattern.push(fragment);
                parentNode[fragment] = parentNode[fragment] || Object.create(null);

                targetNode = parentNode[fragment];
                targetNode._isParam = fragment[1] === ":";
                targetNode._pattern = pattern.join("");

                return targetNode;
            }, routeTree);

            targetNode._handlers = targetNode._handlers || Object.create(null);
            targetNode._handlers[httpMethod] = handlerInfo;
        });

        this.merge(routeTree);
    }

    /**
     * @method merge
     * @param {Array<Object>}
     */
    merge(...trees) {
        // TODO: Implement merge with conflict checking
        return _.merge(this.tree, ...trees);
    }

    /**
     * @method findMatchController
     * @param {String} httpMethod
     * @param {String} url likes "/path/to/route"
     * @return {Object}
     *  controller : Path to controller file
     *  method : Calling method name
     *  url : matched url pattern
     */
    findMatchController(httpMethod, url, routeTree = this.tree) {
        httpMethod = httpMethod.toLowerCase();

        const urlFragments = url.split("/")
            .slice(1)  // Remove predixed "/" in "/route/url"
            .map(fragment => `/${fragment}`);

        const candidateNodes = urlFragments.reduce((candidateNodes, currentFragment) => {
            currentFragment = currentFragment === "/" ? "/index" : currentFragment;
            return this._findMatchNodes(candidateNodes, currentFragment);
        }, [this.tree]);

        const matchedNode = candidateNodes[0];
        const handlers = matchedNode != null ? matchedNode._handlers : null;

        if (candidateNodes.length > 1) {
            console.warn('Conflicted route detected.', candidateNodes.map(node => node._handlers));
            return;
        }
        if (! handlers) return;
        if (! matchedNode || (! handlers[httpMethod] && ! handlers.all)) return;

        const matchedNodeInfo = (handlers[httpMethod] || handlers.all);

        return {
            controller : matchedNodeInfo.controller,
            method : matchedNodeInfo.method,
            pattern : matchedNode._pattern
        };
    }

    /**
     * @private
     * @param {Array<Object>} nodes
     * @param {String} fragment a URL fragment likes "/fragment"
     */
    _findMatchNodes(nodes, fragment) {
        return _.reduce(nodes, (matches, node) => {
            const matched = _.select(node, (child, key) => {
                return key === fragment || child._isParam;
            });
            matches.push(...matched);
            return matches;
        }, []);
    }
}
<commit_msg>RouteTree now can resolve `/index` without `/` suffix in URL.<commit_after>import _ from "lodash"

// TODO: Replace searching algorithm to
//  fragments->method pattern. (Now is `method->fragments` implement).
// For simplify and reduce searching times.
export default class RouteTree {
    /**
     * @class RouteTree
     * @constructor
     * @param {Object?} routeTree initial route tree
     */
    constructor(routeTree) {
        this.clear();
        routeTree && (this.tree = routeTree);
    }

    /**
     * Clear holding route tree
     * @method clear
     */
    clear() {
        this.tree = Object.create(null);
    }

    /**
     * @method mergeFlattenTree
     * @param {Array<Array<string, Array<string>, Object>} routeList
     *  [0] -> httpMethod
     *  [1] -> url fragments pattern
     *  [2] -> Controller information
     */
    mergeFlattenTree(routeList) {
        const routeTree = Object.create(null);

        _.each(routeList, ([httpMethod, urlFragments, handlerInfo], idx, list) => {
            var targetNode;
            const validUrlFragments = urlFragments.filter(f => f !== "/" && f !== "" & !! f);

            // Build route tree
            const pattern = [];
            validUrlFragments.reduce((parentNode, fragment) => {
                pattern.push(fragment);
                parentNode[fragment] = parentNode[fragment] || Object.create(null);

                targetNode = parentNode[fragment];
                targetNode._isParam = fragment[1] === ":";
                targetNode._pattern = pattern.join("");

                return targetNode;
            }, routeTree);

            targetNode._handlers = targetNode._handlers || Object.create(null);
            targetNode._handlers[httpMethod] = handlerInfo;
        });

        this.merge(routeTree);
    }

    /**
     * @method merge
     * @param {Array<Object>}
     */
    merge(...trees) {
        // TODO: Implement merge with conflict checking
        return _.merge(this.tree, ...trees);
    }

    /**
     * @method findMatchController
     * @param {String} httpMethod
     * @param {String} url likes "/path/to/route"
     * @return {Object}
     *  controller : Path to controller file
     *  method : Calling method name
     *  url : matched url pattern
     */
    findMatchController(httpMethod, url, routeTree = this.tree) {
        httpMethod = httpMethod.toLowerCase();

        const urlFragments = url.split("/")
            .slice(1)  // Remove predixed "/" in "/route/url"
            .map(fragment => `/${fragment}`);

        if (urlFragments.length === 0) {
            urlFragments.push("/");
        }

        const candidateNodes = urlFragments.reduce((candidateNodes, currentFragment) => {
            const fragment = currentFragment === "/" ? "/index" : currentFragment;
            return this._findMatchNodes(candidateNodes, fragment);
        }, [this.tree]);

        const matchedNode = candidateNodes[0];
        const handlers = matchedNode != null ? matchedNode._handlers : null;

        if (candidateNodes.length > 1) {
            console.warn("Can't resolve route.", candidateNodes.map(node => node._handlers));
            return;
        }

        if (! handlers) return;
        if (! matchedNode || (! handlers[httpMethod] && ! handlers.all)) return;

        const matchedNodeInfo = (handlers[httpMethod] || handlers.all);

        return {
            controller : matchedNodeInfo.controller,
            method : matchedNodeInfo.method,
            pattern : matchedNode._pattern
        };
    }

    /**
     * @private
     * @param {Array<Object>} nodes
     * @param {String} fragment a URL fragment likes "/fragment"
     */
    _findMatchNodes(nodes, fragment) {
        const firstMatches = _.reduce(nodes, (matches, node) => {
            _.each(node, (child, key) => {
                if (key === fragment) {
                    matches.statics.push(child);
                }
                else if (child._isParam) {
                    matches.dynamics.push(child);
                }
            });

            return matches;
        }, {dynamics : [], statics: []});

        if (firstMatches.statics.length === 1) {
            if (! firstMatches.statics[0]._handlers && firstMatches.statics[0]["/index"]) {
                return [firstMatches.statics[0]["/index"]];
            }

            return firstMatches.statics;
        }
        else if (firstMatches.dynamics.length === 1){
            return firstMatches.dynamics;
        }

        return [...firstMatches.statics, ...firstMatches.dynamics];
    }
}
<|endoftext|>


<commit_before>// @inheritedComponent Modal

import React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import warning from 'warning';
import contains from 'dom-helpers/query/contains';
import ownerDocument from 'dom-helpers/ownerDocument';
import debounce from 'lodash/debounce';
import EventListener from 'react-event-listener';
import withStyles from '../styles/withStyles';
import Modal from '../Modal';
import Grow from '../transitions/Grow';
import Paper from '../Paper';

function getOffsetTop(rect, vertical) {
  let offset = 0;

  if (typeof vertical === 'number') {
    offset = vertical;
  } else if (vertical === 'center') {
    offset = rect.height / 2;
  } else if (vertical === 'bottom') {
    offset = rect.height;
  }

  return offset;
}

function getOffsetLeft(rect, horizontal) {
  let offset = 0;

  if (typeof horizontal === 'number') {
    offset = horizontal;
  } else if (horizontal === 'center') {
    offset = rect.width / 2;
  } else if (horizontal === 'right') {
    offset = rect.width;
  }

  return offset;
}

function getTransformOriginValue(transformOrigin) {
  return [transformOrigin.horizontal, transformOrigin.vertical]
    .map(n => {
      return typeof n === 'number' ? `${n}px` : n;
    })
    .join(' ');
}

// Sum the scrollTop between two elements.
function getScrollParent(parent, child) {
  let element = child;
  let scrollTop = 0;

  while (element && element !== parent) {
    element = element.parentNode;
    scrollTop += element.scrollTop;
  }
  return scrollTop;
}

export const styles = {
  paper: {
    position: 'absolute',
    overflowY: 'auto',
    overflowX: 'hidden',
    // So we see the popover when it's empty.
    // It's most likely on issue on userland.
    minWidth: 16,
    minHeight: 16,
    maxWidth: 'calc(100vw - 32px)',
    maxHeight: 'calc(100vh - 32px)',
    '&:focus': {
      outline: 'none',
    },
  },
};

class Popover extends React.Component {
  componentDidMount() {
    if (this.props.action) {
      this.props.action({
        updatePosition: this.handleResize,
      });
    }
  }

  componentWillUnmount = () => {
    this.handleResize.cancel();
  };

  setPositioningStyles = element => {
    if (element && element.style) {
      const positioning = this.getPositioningStyle(element);
      element.style.top = positioning.top;
      element.style.left = positioning.left;
      element.style.transformOrigin = positioning.transformOrigin;
    }
  };

  getPositioningStyle = element => {
    const { marginThreshold } = this.props;

    // Check if the parent has requested anchoring on an inner content node
    const contentAnchorOffset = this.getContentAnchorOffset(element);
    // Get the offset of of the anchoring element
    const anchorOffset = this.getAnchorOffset(contentAnchorOffset);

    const elemRect = {
      width: element.clientWidth,
      height: element.clientHeight,
    };
    // Get the transform origin point on the element itself
    const transformOrigin = this.getTransformOrigin(elemRect, contentAnchorOffset);

    // Calculate element positioning
    let top = anchorOffset.top - transformOrigin.vertical;
    let left = anchorOffset.left - transformOrigin.horizontal;
    const bottom = top + elemRect.height;
    const right = left + elemRect.width;

    // Window thresholds taking required margin into account
    const heightThreshold = window.innerHeight - marginThreshold;
    const widthThreshold = window.innerWidth - marginThreshold;

    // Check if the vertical axis needs shifting
    if (top < marginThreshold) {
      const diff = top - marginThreshold;
      top -= diff;
      transformOrigin.vertical += diff;
    } else if (bottom > heightThreshold) {
      const diff = bottom - heightThreshold;
      top -= diff;
      transformOrigin.vertical += diff;
    }

    warning(
      elemRect.height < heightThreshold || !elemRect.height || !heightThreshold,
      [
        'Material-UI: the popover component is too tall.',
        `Some part of it can not be seen on the screen (${elemRect.height - heightThreshold}px).`,
        'Please consider adding a `max-height` to improve the user-experience.',
      ].join('\n'),
    );

    // Check if the horizontal axis needs shifting
    if (left < marginThreshold) {
      const diff = left - marginThreshold;
      left -= diff;
      transformOrigin.horizontal += diff;
    } else if (right > widthThreshold) {
      const diff = right - widthThreshold;
      left -= diff;
      transformOrigin.horizontal += diff;
    }

    return {
      top: `${top}px`,
      left: `${left}px`,
      transformOrigin: getTransformOriginValue(transformOrigin),
    };
  };

  // Returns the top/left offset of the position
  // to attach to on the anchor element (or body if none is provided)
  getAnchorOffset(contentAnchorOffset) {
    const { anchorEl, anchorOrigin, anchorReference, anchorPosition } = this.props;

    if (anchorReference === 'anchorPosition') {
      return anchorPosition;
    }

    const anchorElement = anchorEl || document.body;
    const anchorRect = anchorElement.getBoundingClientRect();
    const anchorVertical = contentAnchorOffset === 0 ? anchorOrigin.vertical : 'center';

    return {
      top: anchorRect.top + this.handleGetOffsetTop(anchorRect, anchorVertical),
      left: anchorRect.left + this.handleGetOffsetLeft(anchorRect, anchorOrigin.horizontal),
    };
  }

  // Returns the vertical offset of inner content to anchor the transform on if provided
  getContentAnchorOffset(element) {
    const { getContentAnchorEl, anchorReference } = this.props;
    let contentAnchorOffset = 0;

    if (getContentAnchorEl && anchorReference === 'anchorEl') {
      const contentAnchorEl = getContentAnchorEl(element);

      if (contentAnchorEl && contains(element, contentAnchorEl)) {
        const scrollTop = getScrollParent(element, contentAnchorEl);
        contentAnchorOffset =
          contentAnchorEl.offsetTop + contentAnchorEl.clientHeight / 2 - scrollTop || 0;
      }

      // != the default value
      warning(
        this.props.anchorOrigin.vertical === 'top',
        [
          'Material-UI: you can not change the default `anchorOrigin.vertical` value ',
          'when also providing the `getContentAnchorEl` property to the popover component.',
          'Only use one of the two properties.',
          'Set `getContentAnchorEl` to null or left `anchorOrigin.vertical` unchanged.',
        ].join('\n'),
      );
    }

    return contentAnchorOffset;
  }

  // Return the base transform origin using the element
  // and taking the content anchor offset into account if in use
  getTransformOrigin(elemRect, contentAnchorOffset = 0) {
    const { transformOrigin } = this.props;
    return {
      vertical: this.handleGetOffsetTop(elemRect, transformOrigin.vertical) + contentAnchorOffset,
      horizontal: this.handleGetOffsetLeft(elemRect, transformOrigin.horizontal),
    };
  }

  transitionEl = undefined;

  handleGetOffsetTop = getOffsetTop;

  handleGetOffsetLeft = getOffsetLeft;

  handleEnter = element => {
    if (this.props.onEnter) {
      this.props.onEnter(element);
    }

    this.setPositioningStyles(element);
  };

  handleResize = debounce(() => {
    const element = ReactDOM.findDOMNode(this.transitionEl);
    this.setPositioningStyles(element);
  }, 166);

  render() {
    const {
      anchorEl,
      anchorOrigin,
      anchorPosition,
      anchorReference,
      children,
      classes,
      container: containerProp,
      elevation,
      getContentAnchorEl,
      marginThreshold,
      onEnter,
      onEntered,
      onEntering,
      onExit,
      onExited,
      onExiting,
      open,
      PaperProps,
      role,
      transformOrigin,
      transition: TransitionProp,
      transitionDuration,
      action,
      ...other
    } = this.props;

    // If the container prop is provided, use that
    // If the anchorEl prop is provided, use its parent body element as the container
    // If neither are provided let the Modal take care of choosing the container
    const container = containerProp || (anchorEl ? ownerDocument(anchorEl).body : undefined);

    const transitionProps = {};
    // The provided transition might not support the auto timeout value.
    if (TransitionProp === Grow) {
      transitionProps.timeout = transitionDuration;
    }

    return (
      <Modal container={container} open={open} BackdropProps={{ invisible: true }} {...other}>
        <TransitionProp
          appear
          in={open}
          onEnter={this.handleEnter}
          onEntered={onEntered}
          onEntering={onEntering}
          onExit={onExit}
          onExited={onExited}
          onExiting={onExiting}
          role={role}
          ref={node => {
            this.transitionEl = node;
          }}
          {...transitionProps}
        >
          <Paper
            className={classes.paper}
            data-mui-test="Popover"
            elevation={elevation}
            {...PaperProps}
          >
            <EventListener target="window" onResize={this.handleResize} />
            {children}
          </Paper>
        </TransitionProp>
      </Modal>
    );
  }
}

Popover.propTypes = {
  /**
   * This is callback property. It's called by the component on mount.
   * This is useful when you want to trigger an action programmatically.
   * It currently only supports updatePosition() action.
   *
   * @param {object} actions This object contains all posible actions
   * that can be triggered programmatically.
   */
  action: PropTypes.func,
  /**
   * This is the DOM element that may be used
   * to set the position of the popover.
   */
  anchorEl: PropTypes.object,
  /**
   * This is the point on the anchor where the popover's
   * `anchorEl` will attach to. This is not used when the
   * anchorReference is 'anchorPosition'.
   *
   * Options:
   * vertical: [top, center, bottom];
   * horizontal: [left, center, right].
   */
  anchorOrigin: PropTypes.shape({
    horizontal: PropTypes.oneOfType([
      PropTypes.number,
      PropTypes.oneOf(['left', 'center', 'right']),
    ]),
    vertical: PropTypes.oneOfType([PropTypes.number, PropTypes.oneOf(['top', 'center', 'bottom'])]),
  }),
  /**
   * This is the position that may be used
   * to set the position of the popover.
   * The coordinates are relative to
   * the application's client area.
   */
  anchorPosition: PropTypes.shape({
    top: PropTypes.number,
    left: PropTypes.number,
  }),
  /*
   * This determines which anchor prop to refer to to set
   * the position of the popover.
   */
  anchorReference: PropTypes.oneOf(['anchorEl', 'anchorPosition']),
  /**
   * The content of the component.
   */
  children: PropTypes.node,
  /**
   * Useful to extend the style applied to components.
   */
  classes: PropTypes.object.isRequired,
  /**
   * A node, component instance, or function that returns either.
   * The `container` will passed to the Modal component.
   * By default, it's using the body of the anchorEl's top-level document object,
   * so it's simply `document.body` most of the time.
   */
  container: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
  /**
   * The elevation of the popover.
   */
  elevation: PropTypes.number,
  /**
   * This function is called in order to retrieve the content anchor element.
   * It's the opposite of the `anchorEl` property.
   * The content anchor element should be an element inside the popover.
   * It's used to correctly scroll and set the position of the popover.
   * The positioning strategy tries to make the content anchor element just above the
   * anchor element.
   */
  getContentAnchorEl: PropTypes.func,
  /**
   * Specifies how close to the edge of the window the popover can appear.
   */
  marginThreshold: PropTypes.number,
  /**
   * Callback fired when the component requests to be closed.
   *
   * @param {object} event The event source of the callback.
   */
  onClose: PropTypes.func,
  /**
   * Callback fired before the component is entering.
   */
  onEnter: PropTypes.func,
  /**
   * Callback fired when the component has entered.
   */
  onEntered: PropTypes.func,
  /**
   * Callback fired when the component is entering.
   */
  onEntering: PropTypes.func,
  /**
   * Callback fired before the component is exiting.
   */
  onExit: PropTypes.func,
  /**
   * Callback fired when the component has exited.
   */
  onExited: PropTypes.func,
  /**
   * Callback fired when the component is exiting.
   */
  onExiting: PropTypes.func,
  /**
   * If `true`, the popover is visible.
   */
  open: PropTypes.bool.isRequired,
  /**
   * Properties applied to the `Paper` element.
   */
  PaperProps: PropTypes.object,
  /**
   * @ignore
   */
  role: PropTypes.string,
  /**
   * This is the point on the popover which
   * will attach to the anchor's origin.
   *
   * Options:
   * vertical: [top, center, bottom, x(px)];
   * horizontal: [left, center, right, x(px)].
   */
  transformOrigin: PropTypes.shape({
    horizontal: PropTypes.oneOfType([
      PropTypes.number,
      PropTypes.oneOf(['left', 'center', 'right']),
    ]),
    vertical: PropTypes.oneOfType([PropTypes.number, PropTypes.oneOf(['top', 'center', 'bottom'])]),
  }),
  /**
   * Transition component.
   */
  transition: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
  /**
   * Set to 'auto' to automatically calculate transition time based on height.
   */
  transitionDuration: PropTypes.oneOfType([
    PropTypes.number,
    PropTypes.shape({ enter: PropTypes.number, exit: PropTypes.number }),
    PropTypes.oneOf(['auto']),
  ]),
};

Popover.defaultProps = {
  anchorReference: 'anchorEl',
  anchorOrigin: {
    vertical: 'top',
    horizontal: 'left',
  },
  elevation: 8,
  marginThreshold: 16,
  transformOrigin: {
    vertical: 'top',
    horizontal: 'left',
  },
  transition: Grow,
  transitionDuration: 'auto',
};

export default withStyles(styles, { name: 'MuiPopover' })(Popover);
<commit_msg>[Popover] Respect anchorEl's parent window when calculating position (#10048)<commit_after>// @inheritedComponent Modal

import React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import warning from 'warning';
import contains from 'dom-helpers/query/contains';
import ownerDocument from 'dom-helpers/ownerDocument';
import ownerWindow from 'dom-helpers/ownerWindow';
import debounce from 'lodash/debounce';
import EventListener from 'react-event-listener';
import withStyles from '../styles/withStyles';
import Modal from '../Modal';
import Grow from '../transitions/Grow';
import Paper from '../Paper';

function getOffsetTop(rect, vertical) {
  let offset = 0;

  if (typeof vertical === 'number') {
    offset = vertical;
  } else if (vertical === 'center') {
    offset = rect.height / 2;
  } else if (vertical === 'bottom') {
    offset = rect.height;
  }

  return offset;
}

function getOffsetLeft(rect, horizontal) {
  let offset = 0;

  if (typeof horizontal === 'number') {
    offset = horizontal;
  } else if (horizontal === 'center') {
    offset = rect.width / 2;
  } else if (horizontal === 'right') {
    offset = rect.width;
  }

  return offset;
}

function getTransformOriginValue(transformOrigin) {
  return [transformOrigin.horizontal, transformOrigin.vertical]
    .map(n => {
      return typeof n === 'number' ? `${n}px` : n;
    })
    .join(' ');
}

// Sum the scrollTop between two elements.
function getScrollParent(parent, child) {
  let element = child;
  let scrollTop = 0;

  while (element && element !== parent) {
    element = element.parentNode;
    scrollTop += element.scrollTop;
  }
  return scrollTop;
}

export const styles = {
  paper: {
    position: 'absolute',
    overflowY: 'auto',
    overflowX: 'hidden',
    // So we see the popover when it's empty.
    // It's most likely on issue on userland.
    minWidth: 16,
    minHeight: 16,
    maxWidth: 'calc(100vw - 32px)',
    maxHeight: 'calc(100vh - 32px)',
    '&:focus': {
      outline: 'none',
    },
  },
};

class Popover extends React.Component {
  componentDidMount() {
    if (this.props.action) {
      this.props.action({
        updatePosition: this.handleResize,
      });
    }
  }

  componentWillUnmount = () => {
    this.handleResize.cancel();
  };

  setPositioningStyles = element => {
    if (element && element.style) {
      const positioning = this.getPositioningStyle(element);
      element.style.top = positioning.top;
      element.style.left = positioning.left;
      element.style.transformOrigin = positioning.transformOrigin;
    }
  };

  getPositioningStyle = element => {
    const { anchorEl, marginThreshold } = this.props;

    // Check if the parent has requested anchoring on an inner content node
    const contentAnchorOffset = this.getContentAnchorOffset(element);
    // Get the offset of of the anchoring element
    const anchorOffset = this.getAnchorOffset(contentAnchorOffset);

    const elemRect = {
      width: element.clientWidth,
      height: element.clientHeight,
    };
    // Get the transform origin point on the element itself
    const transformOrigin = this.getTransformOrigin(elemRect, contentAnchorOffset);

    // Calculate element positioning
    let top = anchorOffset.top - transformOrigin.vertical;
    let left = anchorOffset.left - transformOrigin.horizontal;
    const bottom = top + elemRect.height;
    const right = left + elemRect.width;

    // Use the parent window of the anchorEl if provided
    const containerWindow = ownerWindow(anchorEl);

    // Window thresholds taking required margin into account
    const heightThreshold = containerWindow.innerHeight - marginThreshold;
    const widthThreshold = containerWindow.innerWidth - marginThreshold;

    // Check if the vertical axis needs shifting
    if (top < marginThreshold) {
      const diff = top - marginThreshold;
      top -= diff;
      transformOrigin.vertical += diff;
    } else if (bottom > heightThreshold) {
      const diff = bottom - heightThreshold;
      top -= diff;
      transformOrigin.vertical += diff;
    }

    warning(
      elemRect.height < heightThreshold || !elemRect.height || !heightThreshold,
      [
        'Material-UI: the popover component is too tall.',
        `Some part of it can not be seen on the screen (${elemRect.height - heightThreshold}px).`,
        'Please consider adding a `max-height` to improve the user-experience.',
      ].join('\n'),
    );

    // Check if the horizontal axis needs shifting
    if (left < marginThreshold) {
      const diff = left - marginThreshold;
      left -= diff;
      transformOrigin.horizontal += diff;
    } else if (right > widthThreshold) {
      const diff = right - widthThreshold;
      left -= diff;
      transformOrigin.horizontal += diff;
    }

    return {
      top: `${top}px`,
      left: `${left}px`,
      transformOrigin: getTransformOriginValue(transformOrigin),
    };
  };

  // Returns the top/left offset of the position
  // to attach to on the anchor element (or body if none is provided)
  getAnchorOffset(contentAnchorOffset) {
    const { anchorEl, anchorOrigin, anchorReference, anchorPosition } = this.props;

    if (anchorReference === 'anchorPosition') {
      return anchorPosition;
    }

    const anchorElement = anchorEl || document.body;
    const anchorRect = anchorElement.getBoundingClientRect();
    const anchorVertical = contentAnchorOffset === 0 ? anchorOrigin.vertical : 'center';

    return {
      top: anchorRect.top + this.handleGetOffsetTop(anchorRect, anchorVertical),
      left: anchorRect.left + this.handleGetOffsetLeft(anchorRect, anchorOrigin.horizontal),
    };
  }

  // Returns the vertical offset of inner content to anchor the transform on if provided
  getContentAnchorOffset(element) {
    const { getContentAnchorEl, anchorReference } = this.props;
    let contentAnchorOffset = 0;

    if (getContentAnchorEl && anchorReference === 'anchorEl') {
      const contentAnchorEl = getContentAnchorEl(element);

      if (contentAnchorEl && contains(element, contentAnchorEl)) {
        const scrollTop = getScrollParent(element, contentAnchorEl);
        contentAnchorOffset =
          contentAnchorEl.offsetTop + contentAnchorEl.clientHeight / 2 - scrollTop || 0;
      }

      // != the default value
      warning(
        this.props.anchorOrigin.vertical === 'top',
        [
          'Material-UI: you can not change the default `anchorOrigin.vertical` value ',
          'when also providing the `getContentAnchorEl` property to the popover component.',
          'Only use one of the two properties.',
          'Set `getContentAnchorEl` to null or left `anchorOrigin.vertical` unchanged.',
        ].join('\n'),
      );
    }

    return contentAnchorOffset;
  }

  // Return the base transform origin using the element
  // and taking the content anchor offset into account if in use
  getTransformOrigin(elemRect, contentAnchorOffset = 0) {
    const { transformOrigin } = this.props;
    return {
      vertical: this.handleGetOffsetTop(elemRect, transformOrigin.vertical) + contentAnchorOffset,
      horizontal: this.handleGetOffsetLeft(elemRect, transformOrigin.horizontal),
    };
  }

  transitionEl = undefined;

  handleGetOffsetTop = getOffsetTop;

  handleGetOffsetLeft = getOffsetLeft;

  handleEnter = element => {
    if (this.props.onEnter) {
      this.props.onEnter(element);
    }

    this.setPositioningStyles(element);
  };

  handleResize = debounce(() => {
    const element = ReactDOM.findDOMNode(this.transitionEl);
    this.setPositioningStyles(element);
  }, 166);

  render() {
    const {
      anchorEl,
      anchorOrigin,
      anchorPosition,
      anchorReference,
      children,
      classes,
      container: containerProp,
      elevation,
      getContentAnchorEl,
      marginThreshold,
      onEnter,
      onEntered,
      onEntering,
      onExit,
      onExited,
      onExiting,
      open,
      PaperProps,
      role,
      transformOrigin,
      transition: TransitionProp,
      transitionDuration,
      action,
      ...other
    } = this.props;

    // If the container prop is provided, use that
    // If the anchorEl prop is provided, use its parent body element as the container
    // If neither are provided let the Modal take care of choosing the container
    const container = containerProp || (anchorEl ? ownerDocument(anchorEl).body : undefined);

    const transitionProps = {};
    // The provided transition might not support the auto timeout value.
    if (TransitionProp === Grow) {
      transitionProps.timeout = transitionDuration;
    }

    return (
      <Modal container={container} open={open} BackdropProps={{ invisible: true }} {...other}>
        <TransitionProp
          appear
          in={open}
          onEnter={this.handleEnter}
          onEntered={onEntered}
          onEntering={onEntering}
          onExit={onExit}
          onExited={onExited}
          onExiting={onExiting}
          role={role}
          ref={node => {
            this.transitionEl = node;
          }}
          {...transitionProps}
        >
          <Paper
            className={classes.paper}
            data-mui-test="Popover"
            elevation={elevation}
            {...PaperProps}
          >
            <EventListener target="window" onResize={this.handleResize} />
            {children}
          </Paper>
        </TransitionProp>
      </Modal>
    );
  }
}

Popover.propTypes = {
  /**
   * This is callback property. It's called by the component on mount.
   * This is useful when you want to trigger an action programmatically.
   * It currently only supports updatePosition() action.
   *
   * @param {object} actions This object contains all posible actions
   * that can be triggered programmatically.
   */
  action: PropTypes.func,
  /**
   * This is the DOM element that may be used
   * to set the position of the popover.
   */
  anchorEl: PropTypes.object,
  /**
   * This is the point on the anchor where the popover's
   * `anchorEl` will attach to. This is not used when the
   * anchorReference is 'anchorPosition'.
   *
   * Options:
   * vertical: [top, center, bottom];
   * horizontal: [left, center, right].
   */
  anchorOrigin: PropTypes.shape({
    horizontal: PropTypes.oneOfType([
      PropTypes.number,
      PropTypes.oneOf(['left', 'center', 'right']),
    ]),
    vertical: PropTypes.oneOfType([PropTypes.number, PropTypes.oneOf(['top', 'center', 'bottom'])]),
  }),
  /**
   * This is the position that may be used
   * to set the position of the popover.
   * The coordinates are relative to
   * the application's client area.
   */
  anchorPosition: PropTypes.shape({
    top: PropTypes.number,
    left: PropTypes.number,
  }),
  /*
   * This determines which anchor prop to refer to to set
   * the position of the popover.
   */
  anchorReference: PropTypes.oneOf(['anchorEl', 'anchorPosition']),
  /**
   * The content of the component.
   */
  children: PropTypes.node,
  /**
   * Useful to extend the style applied to components.
   */
  classes: PropTypes.object.isRequired,
  /**
   * A node, component instance, or function that returns either.
   * The `container` will passed to the Modal component.
   * By default, it's using the body of the anchorEl's top-level document object,
   * so it's simply `document.body` most of the time.
   */
  container: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
  /**
   * The elevation of the popover.
   */
  elevation: PropTypes.number,
  /**
   * This function is called in order to retrieve the content anchor element.
   * It's the opposite of the `anchorEl` property.
   * The content anchor element should be an element inside the popover.
   * It's used to correctly scroll and set the position of the popover.
   * The positioning strategy tries to make the content anchor element just above the
   * anchor element.
   */
  getContentAnchorEl: PropTypes.func,
  /**
   * Specifies how close to the edge of the window the popover can appear.
   */
  marginThreshold: PropTypes.number,
  /**
   * Callback fired when the component requests to be closed.
   *
   * @param {object} event The event source of the callback.
   */
  onClose: PropTypes.func,
  /**
   * Callback fired before the component is entering.
   */
  onEnter: PropTypes.func,
  /**
   * Callback fired when the component has entered.
   */
  onEntered: PropTypes.func,
  /**
   * Callback fired when the component is entering.
   */
  onEntering: PropTypes.func,
  /**
   * Callback fired before the component is exiting.
   */
  onExit: PropTypes.func,
  /**
   * Callback fired when the component has exited.
   */
  onExited: PropTypes.func,
  /**
   * Callback fired when the component is exiting.
   */
  onExiting: PropTypes.func,
  /**
   * If `true`, the popover is visible.
   */
  open: PropTypes.bool.isRequired,
  /**
   * Properties applied to the `Paper` element.
   */
  PaperProps: PropTypes.object,
  /**
   * @ignore
   */
  role: PropTypes.string,
  /**
   * This is the point on the popover which
   * will attach to the anchor's origin.
   *
   * Options:
   * vertical: [top, center, bottom, x(px)];
   * horizontal: [left, center, right, x(px)].
   */
  transformOrigin: PropTypes.shape({
    horizontal: PropTypes.oneOfType([
      PropTypes.number,
      PropTypes.oneOf(['left', 'center', 'right']),
    ]),
    vertical: PropTypes.oneOfType([PropTypes.number, PropTypes.oneOf(['top', 'center', 'bottom'])]),
  }),
  /**
   * Transition component.
   */
  transition: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
  /**
   * Set to 'auto' to automatically calculate transition time based on height.
   */
  transitionDuration: PropTypes.oneOfType([
    PropTypes.number,
    PropTypes.shape({ enter: PropTypes.number, exit: PropTypes.number }),
    PropTypes.oneOf(['auto']),
  ]),
};

Popover.defaultProps = {
  anchorReference: 'anchorEl',
  anchorOrigin: {
    vertical: 'top',
    horizontal: 'left',
  },
  elevation: 8,
  marginThreshold: 16,
  transformOrigin: {
    vertical: 'top',
    horizontal: 'left',
  },
  transition: Grow,
  transitionDuration: 'auto',
};

export default withStyles(styles, { name: 'MuiPopover' })(Popover);
<|endoftext|>


<commit_before>'use strict';

var assert = require("assert");
var fs = require('fs');
var path = require('path');
var touch = require('touch');
var middleware = require('../index.js');

//Helper Functions
function getManifest(server, callback) {
  var buf = [];
  var ccHeader = null;
  var ctHeader = null;

  var mockResponse = {
    end: function(s) {
      if (typeof s === 'string') {
        buf.push(s);
      }
      try {
        assert.equal(ccHeader, 'no-cache', 'Cache-Control header should be no-cache');
        assert.equal(ctHeader, 'text/cache-manifest', 'Content-Type header should be text/cache-manifest');
        var manifest = parseManifest(buf.join(''));
        callback(null, manifest);
      } catch(err) {
        callback(err);
      }
    },
    write: function(s) {
      if (typeof s === 'string') {
        buf.push(s);
      }
    },
    set: function(name, value) {
      if (name === 'Cache-Control') {
        ccHeader = value;
      } else if (name === 'Content-Type') {
        ctHeader = value;
      }
    }
  };

  server(null, mockResponse);
}

function parseManifest(body) {
  var manifest = { CACHE: [], COMMENTS: [] };
  var lines = body.split('\n');
  assert.equal(lines[0], 'CACHE MANIFEST', 'First line should be CACHE MANIFEST');
  var section = 'CACHE';
  for (var i = 1; i < lines.length; i++) {
    var line = lines[i];
    assert.equal(line.trim(), line, 'No extra whitespace expected in cache manifest');
    if (line.length === 0) {
      //Blank line
      section = null;
    } else if (line.startsWith('#')) {
      //Comment
      manifest['COMMENTS'].push(line.substr(1));
    } else if (section === null) {
      //New section header
      assert(line.endsWith(':'), 'Cache section lines should end with :');
      section = line.substr(0, line.length - 1);
      if (section !== 'CACHE') {
        assert(!(section in manifest), 'Multiple copies of section header ' + section + ' found in manifest');
        manifest[section] = [];
      }
    } else {
      //Inside of an existing section
      manifest[section].push(line);
    }
  }

  assert.deepEqual(manifest['CACHE'].slice().sort(), manifest['CACHE'], 'Cache entries should be soted');

  return manifest;
}

function addFile(path) {

}

function touchFile(path) {

}

function delFile(path) {

}

function addDir(path) {

}

function rmDir(path) {

}

function assertInManifest(server, line) {

}

function assertManifestChanged(server) {

}

function assertManifestNotChanged(server) {

}

var CONFIG = [{
    file: 'test_files/some_files',
    url: 'some'
  }, {
    file: 'test_files/more_files',
    url: 'test_files/more_files'
  }, {
    file: 'test_files/hello.txt',
    url: 'hello.txt'
  }
];

var INITIAL_FILES = [
  'test_files/hello.txt',
  'test_files/some_files/a.txt',
  'test_files/some_files/b.txt',
  'test_files/some_files/nested/x.txt',
  'test_files/some_files/nested/y.txt',
  'test_files/more_files/1.txt',
  'test_files/more_files/2.txt'
];

var INITIAL_URLS = [
  '/hello.txt',
  '/some/a.txt',
  '/some/b.txt',
  '/some/nested/x.txt',
  '/some/nested/y.txt',
  '/test_files/more_files/1.txt',
  '/test_files/more_files/2.txt'
];

//Tests
describe('Check filesystem', function() {
  it('Should contain expected files', function(done) {
    var count = 0;
    for(var i = 0; i < INITIAL_FILES.length; i++) {
      var path = INITIAL_FILES[i];
      fs.stat(path, function(err, stat) {
        count++;
        try {
          assert.equal(err, null, 'Error getting fs stat for ' + path);
          assert(stat.isFile(), 'Missing initial file ' + path);
          if (count === INITIAL_FILES.length) {
            done();
          }
        } catch(err) {
          done(err);
        }
      });
    }
  });
});

describe.skip('Initialization', function() {
  it('should initialize properly', function (done) {
    middleware.generator(CONFIG, { readyCallback: function(server) {
      server.stop();
      done();
    }});
  });

  it('Should contain expected elements', function (done) {
    middleware.generator(CONFIG, { readyCallback: function(server) {
      getManifest(server, function(err, manifest) {
        try {
          if (err) { throw err; }
          assert.deepEqual(manifest['NETWORK'], ['*'], 'Network section doesn\'t hold expected value'); //TODO: pull this from opts
          assert.deepEqual(manifest['CACHE'], INITIAL_URLS, 'Cache section doesn\'t hold expected value(s)');
          done();
        } catch (err) {
          done(err);
        } finally {
          server.stop();
        }
      });
    }});
  });

  it('Should contain expected elements when configured with absolute paths', function (done) {
    var absolutePaths = [];
    for (var i = 0; i < CONFIG.length; i++) {
      absolutePaths.push({
        file: path.resolve(process.cwd(), CONFIG[i]['file']),
        url: CONFIG[i]['url']
      });
    }

    middleware.generator(absolutePaths, { readyCallback: function(server) {
      getManifest(server, function(err, manifest) {
        try {
          if (err) { throw err; }
          assert.deepEqual(manifest['NETWORK'], ['*'], 'Network section doesn\'t hold expected value'); //TODO: pull this from opts
          assert.deepEqual(manifest['CACHE'], INITIAL_URLS, 'Cache section doesn\'t hold expected value(s)');
          done();
        } catch (err) {
          done(err);
        } finally {
          server.stop();
        }
      });
    }});
  });
});

describe('Observe Changes', function() {
  var newFile = 'test_files/some_files/new_dir/1.txt';
  var newUrl = '/some/new_dir/1.txt';

  var updateCallback = null;
  function updateListener() {
    console.log('it came back!');
    if (typeof updateCallback === 'function') {
      updateCallback();
      updateCallback = null;
    }
  }

  function waitForUpdate(callback, timeout) {
    var outtaTime = false;
    if (typeof timeout !== 'number') {
      timeout = 500;
    }
    var timeoutId = setTimeout(function() {
      outtaTime = true;
      updateCallback = null;
      callback(new Error('Timeout waiting for update'));
    }, timeout);

    updateCallback = function() {
      if (! outtaTime) {
        clearTimeout(timeoutId);
        callback(null);
      }
    };
  }

  it('Should observe modifications to watched files', function(done) {
    middleware.generator(CONFIG, { catchupDelay: 0, updateListener: updateListener, readyCallback: function(server) {
      touch('test_files/hello.txt');
      waitForUpdate(function(err) {
        server.stop();
        if (err) { done(err); }
        else { done(); }
      });
    }});
  });

  it('Should observe modifications to files in watched directories', function(done) {
    middleware.generator(CONFIG, { catchupDelay: 0, updateListener: updateListener, readyCallback: function(server) {
      touch('test_files/some_files/a.txt');
      waitForUpdate(function(err) {
        server.stop();
        if (err) { done(err); }
        else { done(); }
      });
    }});
  });

  it('Should observe modifications to files in subdirectories', function(done) {
    middleware.generator(CONFIG, { catchupDelay: 0, updateListener: updateListener, readyCallback: function(server) {
      touch('test_files/some_files/nested/x.txt');
      waitForUpdate(function(err) {
        server.stop();
        if (err) { done(err); }
        else { done(); }
      });
    }});
  });

  //TODO: So many callbacks! Convert this to promises or something
  it('Should observe file creations and modifications to those new files', function(done) {
    //TODO: "before" hooks to make sure the temp files are gone and the initial files are there
    middleware.generator(CONFIG, { catchupDelay: 0, updateListener: updateListener, readyCallback: function(server) {
      function cleanup(err) {
        console.log('bailing out');
        console.log(err);
        server.stop();
        done(err);
      }

      try {
        fs.mkdirSync(path.dirname(newFile));
        fs.writeFileSync(newFile, 'TEXT');
        waitForUpdate(function() {
          getManifest(server, function(err, manifest) {
            if (err) { return cleanup(err); }
            try {
              assert(manifest['CACHE'].indexOf(newUrl) !== -1, 'Newly created file should be in manifest');

              touch.sync(newFile);
              //TODO: the updateCallback should return the pre-parsed manifest object! (and the ready callback as well)
              waitForUpdate(function() {
                fs.unlinkSync(newFile);
                //Deleting a directory that is being watched in Windows crashes watchr!
                //fs.rmdirSync(path.dirname(newFile));
                waitForUpdate(function() {
                  getManifest(server, function(err, manifest) {
                    if (err) { return cleanup(err); }
                    try {
                      assert(manifest['CACHE'].indexOf(newUrl) === -1, 'Deleted file shouldn\'t be in manifest');
                      server.stop();
                      done();
                    } catch (err) {
                      cleanup(err);
                    }
                  });
                });
              });
            } catch (err) {
              cleanup(err);
            }
          });
        });
      } catch (err) {
        console.log('top level error');
        cleanup(err);
      }
    }});
  });

  it('Should observe creations and deletions in newly nested directories');
});
<commit_msg>Fix error with incorrect access to variable outside of callback<commit_after>'use strict';

var assert = require("assert");
var fs = require('fs');
var path = require('path');
var touch = require('touch');
var middleware = require('../index.js');

//Helper Functions
function getManifest(server, callback) {
  var buf = [];
  var ccHeader = null;
  var ctHeader = null;

  var mockResponse = {
    end: function(s) {
      if (typeof s === 'string') {
        buf.push(s);
      }
      try {
        assert.equal(ccHeader, 'no-cache', 'Cache-Control header should be no-cache');
        assert.equal(ctHeader, 'text/cache-manifest', 'Content-Type header should be text/cache-manifest');
        var manifest = parseManifest(buf.join(''));
        callback(null, manifest);
      } catch(err) {
        callback(err);
      }
    },
    write: function(s) {
      if (typeof s === 'string') {
        buf.push(s);
      }
    },
    set: function(name, value) {
      if (name === 'Cache-Control') {
        ccHeader = value;
      } else if (name === 'Content-Type') {
        ctHeader = value;
      }
    }
  };

  server(null, mockResponse);
}

function parseManifest(body) {
  var manifest = { CACHE: [], COMMENTS: [] };
  var lines = body.split('\n');
  assert.equal(lines[0], 'CACHE MANIFEST', 'First line should be CACHE MANIFEST');
  var section = 'CACHE';
  for (var i = 1; i < lines.length; i++) {
    var line = lines[i];
    assert.equal(line.trim(), line, 'No extra whitespace expected in cache manifest');
    if (line.length === 0) {
      //Blank line
      section = null;
    } else if (line.startsWith('#')) {
      //Comment
      manifest['COMMENTS'].push(line.substr(1));
    } else if (section === null) {
      //New section header
      assert(line.endsWith(':'), 'Cache section lines should end with :');
      section = line.substr(0, line.length - 1);
      if (section !== 'CACHE') {
        assert(!(section in manifest), 'Multiple copies of section header ' + section + ' found in manifest');
        manifest[section] = [];
      }
    } else {
      //Inside of an existing section
      manifest[section].push(line);
    }
  }

  assert.deepEqual(manifest['CACHE'].slice().sort(), manifest['CACHE'], 'Cache entries should be soted');

  return manifest;
}

function addFile(path) {

}

function touchFile(path) {

}

function delFile(path) {

}

function addDir(path) {

}

function rmDir(path) {

}

function assertInManifest(server, line) {

}

function assertManifestChanged(server) {

}

function assertManifestNotChanged(server) {

}

var CONFIG = [{
    file: 'test_files/some_files',
    url: 'some'
  }, {
    file: 'test_files/more_files',
    url: 'test_files/more_files'
  }, {
    file: 'test_files/hello.txt',
    url: 'hello.txt'
  }
];

var INITIAL_FILES = [
  'test_files/hello.txt',
  'test_files/some_files/a.txt',
  'test_files/some_files/b.txt',
  'test_files/some_files/nested/x.txt',
  'test_files/some_files/nested/y.txt',
  'test_files/more_files/1.txt',
  'test_files/more_files/2.txt'
];

var INITIAL_URLS = [
  '/hello.txt',
  '/some/a.txt',
  '/some/b.txt',
  '/some/nested/x.txt',
  '/some/nested/y.txt',
  '/test_files/more_files/1.txt',
  '/test_files/more_files/2.txt'
];

//Tests
describe('Check filesystem', function() {
  it('Should contain expected files', function(done) {
    var count = 0;
    for(var i = 0; i < INITIAL_FILES.length; i++) {
      var filePath = INITIAL_FILES[i];
      fs.stat(filePath, function(err, stat) {
        count++;
        try {
          assert.equal(err, null, 'Error getting fs stat');
          assert(stat.isFile(), 'Missing initial file');
          if (count === INITIAL_FILES.length) {
            done();
          }
        } catch(err) {
          done(err);
        }
      });
    }
  });
});

describe.skip('Initialization', function() {
  it('should initialize properly', function (done) {
    middleware.generator(CONFIG, { readyCallback: function(server) {
      server.stop();
      done();
    }});
  });

  it('Should contain expected elements', function (done) {
    middleware.generator(CONFIG, { readyCallback: function(server) {
      getManifest(server, function(err, manifest) {
        try {
          if (err) { throw err; }
          assert.deepEqual(manifest['NETWORK'], ['*'], 'Network section doesn\'t hold expected value'); //TODO: pull this from opts
          assert.deepEqual(manifest['CACHE'], INITIAL_URLS, 'Cache section doesn\'t hold expected value(s)');
          done();
        } catch (err) {
          done(err);
        } finally {
          server.stop();
        }
      });
    }});
  });

  it('Should contain expected elements when configured with absolute paths', function (done) {
    var absolutePaths = [];
    for (var i = 0; i < CONFIG.length; i++) {
      absolutePaths.push({
        file: path.resolve(process.cwd(), CONFIG[i]['file']),
        url: CONFIG[i]['url']
      });
    }

    middleware.generator(absolutePaths, { readyCallback: function(server) {
      getManifest(server, function(err, manifest) {
        try {
          if (err) { throw err; }
          assert.deepEqual(manifest['NETWORK'], ['*'], 'Network section doesn\'t hold expected value'); //TODO: pull this from opts
          assert.deepEqual(manifest['CACHE'], INITIAL_URLS, 'Cache section doesn\'t hold expected value(s)');
          done();
        } catch (err) {
          done(err);
        } finally {
          server.stop();
        }
      });
    }});
  });
});

describe('Observe Changes', function() {
  var newFile = 'test_files/some_files/new_dir/1.txt';
  var newUrl = '/some/new_dir/1.txt';

  var updateCallback = null;
  function updateListener() {
    console.log('it came back!');
    if (typeof updateCallback === 'function') {
      updateCallback();
      updateCallback = null;
    }
  }

  function waitForUpdate(callback, timeout) {
    var outtaTime = false;
    if (typeof timeout !== 'number') {
      timeout = 500;
    }
    var timeoutId = setTimeout(function() {
      outtaTime = true;
      updateCallback = null;
      callback(new Error('Timeout waiting for update'));
    }, timeout);

    updateCallback = function() {
      if (! outtaTime) {
        clearTimeout(timeoutId);
        callback(null);
      }
    };
  }

  it('Should observe modifications to watched files', function(done) {
    middleware.generator(CONFIG, { catchupDelay: 0, updateListener: updateListener, readyCallback: function(server) {
      touch('test_files/hello.txt');
      waitForUpdate(function(err) {
        server.stop();
        if (err) { done(err); }
        else { done(); }
      });
    }});
  });

  it('Should observe modifications to files in watched directories', function(done) {
    middleware.generator(CONFIG, { catchupDelay: 0, updateListener: updateListener, readyCallback: function(server) {
      touch('test_files/some_files/a.txt');
      waitForUpdate(function(err) {
        server.stop();
        if (err) { done(err); }
        else { done(); }
      });
    }});
  });

  it('Should observe modifications to files in subdirectories', function(done) {
    middleware.generator(CONFIG, { catchupDelay: 0, updateListener: updateListener, readyCallback: function(server) {
      touch('test_files/some_files/nested/x.txt');
      waitForUpdate(function(err) {
        server.stop();
        if (err) { done(err); }
        else { done(); }
      });
    }});
  });

  //TODO: So many callbacks! Convert this to promises or something
  it('Should observe file creations and modifications to those new files', function(done) {
    //TODO: "before" hooks to make sure the temp files are gone and the initial files are there
    middleware.generator(CONFIG, { catchupDelay: 0, updateListener: updateListener, readyCallback: function(server) {
      function cleanup(err) {
        console.log('bailing out');
        console.log(err);
        server.stop();
        done(err);
      }

      try {
        fs.mkdirSync(path.dirname(newFile));
        fs.writeFileSync(newFile, 'TEXT');
        waitForUpdate(function() {
          getManifest(server, function(err, manifest) {
            if (err) { return cleanup(err); }
            try {
              assert(manifest['CACHE'].indexOf(newUrl) !== -1, 'Newly created file should be in manifest');

              touch.sync(newFile);
              //TODO: the updateCallback should return the pre-parsed manifest object! (and the ready callback as well)
              waitForUpdate(function() {
                fs.unlinkSync(newFile);
                //Deleting a directory that is being watched in Windows crashes watchr!
                //fs.rmdirSync(path.dirname(newFile));
                waitForUpdate(function() {
                  getManifest(server, function(err, manifest) {
                    if (err) { return cleanup(err); }
                    try {
                      assert(manifest['CACHE'].indexOf(newUrl) === -1, 'Deleted file shouldn\'t be in manifest');
                      server.stop();
                      done();
                    } catch (err) {
                      cleanup(err);
                    }
                  });
                });
              });
            } catch (err) {
              cleanup(err);
            }
          });
        });
      } catch (err) {
        console.log('top level error');
        cleanup(err);
      }
    }});
  });

  it('Should observe creations and deletions in newly nested directories');
});
<|endoftext|>