<commit_before>/**
 * Module dependencies.
 */
var mongoose = require('mongoose'),
    async = require('async'),
    Quote = mongoose.model('Quote'),
    Vendor = mongoose.model('Vendor'),
    emailer = require('./../emails/init'),
    _ = require('underscore'),
    natural = require('natural'),
    nounInflector = new natural.NounInflector(),
    numeral = require('numeral'),
    currency = require('./../helpers/currency');

/**
 * Find quote by id
 */
exports.quote = function(req, res, next, id) {
    Quote.load(id, function(err, quote) {
        if (err) return next(err);
        if (!quote) return res.failure('Failed to load quote ' + id, 404);
        req.quote = quote;
        next();
    });
};

/**
* ----------------------------------------
* Middleware called when accessing public quoter api
* validates that required fields are present, then calls next
*
* @note we use this because it allows us to send a more specific error message
*       then what mongoose validation would normally throw.
*       It also allows us to process, trim, etc.
*
* ----------------------------------------
*
*/
exports.validateQuoteRequest = function(req, res, next) {

    // support instances of updating existing quote, PUT requests, where
    // req.quote will be set instead of req.body
    req.body.totalCost = req.body.totalCost || req.quote.totalCost;
    req.body.description = req.body.description || req.quote.description;

    // message user for required totalCost
    if(!req.body.totalCost) {
        return res.failure("totalCost is required", 400);
    }

    // message user for required totalCost
    if(!req.body.description) {
        return res.failure("description is required", 400);
    }

    // format total cost toCents, supporting error callback if conversion fails
    currency.toCents(req.body.totalCost, function(err, result) {
        if(err) return res.failure(err, 400);
        req.body.totalCost = result;
        next();
    });

};

/**
* ----------------------------------------
* Applies rate to cost
* ----------------------------------------
*/
var applyRateToCost = function(totalCost, rate) {

    // adjust rate for the fact we are doing all calculations in cents
    rate = rate / 100;

    var payment = rate * totalCost;

    return payment;

};

/**
* ----------------------------------------
* Formats and rounds currecny
* ----------------------------------------
*/
var formatPayment = function(payment) {

    return numeral(payment).format('$0,0.00');

};

/**
* ----------------------------------------
* Handle no programs found for this quote
* ----------------------------------------
*/
var handleNoPrograms = function(req, res, next) {

    var response = {};

    response.message = "We could't generate a quote for you based on this total cost. Please contact us to arrange special financing.";

    // @todo confirm with Brian that we can show contactPerson info, and not vendor phone
    // which is what current system does.
    response.contact = req.vendor.contactPerson;

    res.failure(response, 200);

};

/**
 * ----------------------------------------
 * Create a quote
 * ----------------------------------------
 */
exports.createOrUpdate = function(req, res) {

    // shorthand for our request totalcost
    // @note we have alreay validated it exists
    // @todo convert if decimal places and strip commas
    var totalCost = req.body.totalCost;
    var programs = req.vendor.toObject().programs;

    /**
    * ----------------------------------------
    * Main logic to iterate though programs and build a quote for the end user
    *
    * @note this logic creates one array of buyout options, even if a vendor has multiple programs
    *       each option is saved with the program name as reference... so we can group them
    *       by program before returning them to user. We do this because it makes it very easy to
    *       check if any options were returned, !filteredPrograms.length
    *
    * ----------------------------------------
    */

    // this will hold out final filtered programs based on the cost range
    //
    var filteredPrograms = [];

    // vendors can have multiple programs, so first iterate through these
    //
    _.each(programs, function(program, $programIdx) {

        // next, lets iterate though the buyout options
        //
        _.each(program.rateSheet.buyoutOptions, function(buyOutOption, $buyOutIdx) {

            // save our match if we find one
            var matchedOption = null;

            // each buyout option will have an array of costs, each with a min and max value
            // iterate though each of these, looking for a a cost range that our totalCost falls between
            //
            _.each(buyOutOption.costs, function(cost, $costIdx) {

                // if the totalCost is within the min and max for this cost range,
                // we want to save this cost as "THE" cost for this buyout option.
                // in other words, we want to replace the array of perhaps 5 buyout options
                // with just this one
                //

                // @note since we only storing cost ranges to dolar value accurary,
                //       we need to account for the true range of this dollar value by
                //       adding 99 cents, so that 1000 - 5000 is actually 1000 - 5000.99
                //       and this way a quote for 5000.80 doesn't get ignored
                cost.max += 99;

                if (cost && totalCost >= cost.min && totalCost <= cost.max) {

                    // replace the buyoutOption costs with THE cost
                    buyOutOption.costs = buyOutOption.costs[$costIdx];

                    // do some additional processing of the object
                    // saving program info onto the buyout option
                    // which allows us to store one level option
                    buyOutOption.programName = program.name;
                    buyOutOption.termPeriod = program.rateSheet.termPeriod;

                    // remove IDs and such since we don't need them on the front end
                    delete buyOutOption._id;
                    delete buyOutOption.costs._id;

                    matchedOption = buyOutOption;

                    // no longer a need to iterate though
                    return;

                }  else {
                    //console.log('%s is not within %s and %s', totalCost, cost.min, cost.max);
                }

            });

            // push to our filtered array. This gives us one array where all programs are mixed
            // together, but we can always group them later by programName
            if(matchedOption) filteredPrograms.push(matchedOption);

        });

    });

    /**
    * ----------------------------------------
    * Process filtered programs, for each one factoring the rate * totalCost
    * and assembling other bits and pieces as needed for return
    * ----------------------------------------
    *
    */

    // our returned quote
    var returnQuote = [];

    // iterate through our rates
    // we might have multiple rates for multiple programs.
    // we'll do some calculations to turn a rate into a payment
    _.each(filteredPrograms, function(program) {

       // create empty return array
       var termAndRates = [];

       // pluralize our term period
       program.termPeriod = nounInflector.pluralize(program.termPeriod);

       // iterate thorugh terms, check if a rate for this term exists and is not 0
       // in some cases rates will be 0 if its not a supported term.
       // @todo test that Marlin's formula works when multiplying by cents, not just dollars
       _.each(program.terms, function(term, key) {

           // if there is no payment info, ie there is no rate,
           // don't include it. In some cases the rate will be '0' if not being used.
           if(program.costs.rates[key] === 0 || program.costs.rates[key] === null || typeof program.costs.rates[key] !== 'number') {
                return;
           }

           var payment = applyRateToCost(totalCost, program.costs.rates[key]);

           termAndRates.push({

               // term + plurized version of term length
               term: term + ' ' + program.termPeriod,

               // rate for testing @todo remove for production
               rate: program.costs.rates[key],

               // payment
               payment: payment,
               paymentDisplay: formatPayment(payment),

               // totalCost thus far is in cents, lets convert back
               totalCost: totalCost / 100,
               totalCostDisplay: formatPayment(totalCost / 100)
           });

       });

       // delete stuff and push
       program.rates = termAndRates;
       delete program.costs;
       delete program.terms;

       returnQuote.push(program);

    });

    /**
    * ----------------------------------------
    * Group by program name
    * ----------------------------------------
    *
    */
    returnQuote = _.groupBy(returnQuote, function(item) {
        return item.programName;
    });

    /**
    * ----------------------------------------
    * Handle case where Quote is not within range
    * @todo capture these leads by sending email to sales and vendor rep
    * ----------------------------------------
    *
    */
    if(filteredPrograms.length === 0) {
        return handleNoPrograms(req, res);
    }

    /**
    * ----------------------------------------
    * Save the quote to the database
    * ----------------------------------------
    *
    *
    */

    // try to get quote from request
    var quote = req.quote || null;

    // update or create a quote
    if(quote) {
        quote = _.extend(quote, req.body);
    } else {
        quote = new Quote(req.body);
    }

    // add payments data to it
    // payments is {} in database, so we can efficiently store entire object here
    quote.payments = returnQuote;

    // adjust totalCost so it's back to dollars
    quote.totalCost = quote.totalCost / 100;
    quote.totalCostDisplay = formatPayment(quote.totalCost);

    // finally, save the quote
    quote.save(function(err) {

        // handle error case
        if (err) return res.failure(err);

        // delete some things we dont want to be public
        // @todo later we should all more robust access controll, since the "get" method needs this also
        //delete quote.vendorId;
        //delete quote.salesRep;
        //delete quote.vendorRep;

        // send result to user
        res.ok(quote);

        // get the quote we just saved and trigger emails to end user
        // @note this is async so it will not delay response to user
        Quote
            .findOne({_id : quote._id})
            .populate('vendorId vendorRep salesRep')
            .exec(function(err, result) {

                if(err) return;

                emailer.newQuoteEndUser(req, result);
                emailer.newQuoteSalesRep(req, result);
                emailer.newQuoteVendorRep(req, result);

        });

    });

};

/**
 * Update a quote
 */
exports.update = function(req, res) {
    var quote = req.quote;

    quote = _.extend(quote, req.body);

    quote.save(function(err) {
        res.ok(quote);
    });
};

/**
 * Delete an quote
 */
exports.destroy = function(req, res) {
    var quote = req.quote;

    quote.remove(function(err) {
        if (err) {
            res.failure(err);
        } else {
            res.ok(quote);
        }
    });
};

/**
 * Show an quote
 */
exports.show = function(req, res) {
    res.ok(req.quote);
};

/**
 * List of Quotes
 */
exports.all = function(req, res) {

    var where = {};

    // limit quotes to sales rep only.
    if (req.userHasRole('salesRep')) {
        where = {
            salesRep: req.user._id
        };
    } else if (req.userHasRole('vendorRep')) {
        where = {
            vendorRep: req.user._id
        };
    }

    Quote
        .find(where)
        .sort('-status -created')
    //.populate('vendorId salesRep')
    .exec(function(err, quotes) {
        if (err) {
            res.failure(err);
        } else {
            res.ok(quotes);
        }
    });

};

/**
 * Get quotes for a sales rep.
 *
 * @note This can be used to limit quotes when a user is logged in, or
 *       it can be used for a resource/:id/children instance (if we modify the way we get user id)
 *
 */
exports.getAllForSalesRep = function(req, res) {

    // First get all vendors for this sales rep.
    Vendor
        .where('salesRep')
        .equals(req.user._id)
        .find()
        .select('_id')
        .exec(function(err, vendors) {
            if (err) {
                res.failure(err);
            } else {

                // extract the vendor ids from the results
                // this will be all vendors the user is associated with NOW!
                // @note we don't store user ids with the quotes... because if at any point the vendor gets
                // a new sales rep, things would be out of sync.
                var vendorIds = [];
                _.each(vendors, function(item) {
                    vendorIds.push(item._id);
                });
                getQuotes(vendorIds);

            }
        });

    var getQuotes = function(vendorIds) {

        Quote
            .find()
            .where('vendorId')
            .in(vendorIds)
            .sort('-status -created')
            .exec(function(err, quotes) {
                if (err) {
                    res.failure(err);
                } else {
                    res.ok(quotes, 'Getting quotes for salesRep ' + req.user.fullName);
                }
            });
    };
};
<commit_msg>More robust req checking on quoter tool<commit_after>/**
 * Module dependencies.
 */
var mongoose = require('mongoose'),
    async = require('async'),
    Quote = mongoose.model('Quote'),
    Vendor = mongoose.model('Vendor'),
    emailer = require('./../emails/init'),
    _ = require('underscore'),
    natural = require('natural'),
    nounInflector = new natural.NounInflector(),
    numeral = require('numeral'),
    currency = require('./../helpers/currency');

/**
 * Find quote by id
 */
exports.quote = function(req, res, next, id) {
    Quote.load(id, function(err, quote) {
        if (err) return next(err);
        if (!quote) return res.failure('Failed to load quote ' + id, 404);
        req.quote = quote;
        next();
    });
};

/**
* ----------------------------------------
* Middleware called when accessing public quoter api
* validates that required fields are present, then calls next
*
* @note we use this because it allows us to send a more specific error message
*       then what mongoose validation would normally throw.
*       It also allows us to process, trim, etc.
*
* ----------------------------------------
*
*/
exports.validateQuoteRequest = function(req, res, next) {

    // support instances of updating existing quote, PUT requests, where
    // req.quote will be set instead of req.body
    req.body.totalCost = req.body.totalCost || req.quote && req.quote.totalCost || null;
    req.body.description = req.body.description || req.quote && req.quote.description || null;

    // message user for required totalCost
    if(!req.body.totalCost) {
        return res.failure("totalCost is required", 400);
    }

    // message user for required totalCost
    if(!req.body.description) {
        return res.failure("description is required", 400);
    }

    // format total cost toCents, supporting error callback if conversion fails
    currency.toCents(req.body.totalCost, function(err, result) {
        if(err) return res.failure(err, 400);
        req.body.totalCost = result;
        next();
    });

};

/**
* ----------------------------------------
* Applies rate to cost
* ----------------------------------------
*/
var applyRateToCost = function(totalCost, rate) {

    // adjust rate for the fact we are doing all calculations in cents
    rate = rate / 100;

    var payment = rate * totalCost;

    return payment;

};

/**
* ----------------------------------------
* Formats and rounds currecny
* ----------------------------------------
*/
var formatPayment = function(payment) {

    return numeral(payment).format('$0,0.00');

};

/**
* ----------------------------------------
* Handle no programs found for this quote
* ----------------------------------------
*/
var handleNoPrograms = function(req, res, next) {

    var response = {};

    response.message = "We could't generate a quote for you based on this total cost. Please contact us to arrange special financing.";

    // @todo confirm with Brian that we can show contactPerson info, and not vendor phone
    // which is what current system does.
    response.contact = req.vendor.contactPerson;

    res.failure(response, 200);

};

/**
 * ----------------------------------------
 * Create a quote
 * ----------------------------------------
 */
exports.createOrUpdate = function(req, res) {

    // shorthand for our request totalcost
    // @note we have alreay validated it exists
    // @todo convert if decimal places and strip commas
    var totalCost = req.body.totalCost;
    var programs = req.vendor.toObject().programs;

    /**
    * ----------------------------------------
    * Main logic to iterate though programs and build a quote for the end user
    *
    * @note this logic creates one array of buyout options, even if a vendor has multiple programs
    *       each option is saved with the program name as reference... so we can group them
    *       by program before returning them to user. We do this because it makes it very easy to
    *       check if any options were returned, !filteredPrograms.length
    *
    * ----------------------------------------
    */

    // this will hold out final filtered programs based on the cost range
    //
    var filteredPrograms = [];

    // vendors can have multiple programs, so first iterate through these
    //
    _.each(programs, function(program, $programIdx) {

        // next, lets iterate though the buyout options
        //
        _.each(program.rateSheet.buyoutOptions, function(buyOutOption, $buyOutIdx) {

            // save our match if we find one
            var matchedOption = null;

            // each buyout option will have an array of costs, each with a min and max value
            // iterate though each of these, looking for a a cost range that our totalCost falls between
            //
            _.each(buyOutOption.costs, function(cost, $costIdx) {

                // if the totalCost is within the min and max for this cost range,
                // we want to save this cost as "THE" cost for this buyout option.
                // in other words, we want to replace the array of perhaps 5 buyout options
                // with just this one
                //

                // @note since we only storing cost ranges to dolar value accurary,
                //       we need to account for the true range of this dollar value by
                //       adding 99 cents, so that 1000 - 5000 is actually 1000 - 5000.99
                //       and this way a quote for 5000.80 doesn't get ignored
                cost.max += 99;

                if (cost && totalCost >= cost.min && totalCost <= cost.max) {

                    // replace the buyoutOption costs with THE cost
                    buyOutOption.costs = buyOutOption.costs[$costIdx];

                    // do some additional processing of the object
                    // saving program info onto the buyout option
                    // which allows us to store one level option
                    buyOutOption.programName = program.name;
                    buyOutOption.termPeriod = program.rateSheet.termPeriod;

                    // remove IDs and such since we don't need them on the front end
                    delete buyOutOption._id;
                    delete buyOutOption.costs._id;

                    matchedOption = buyOutOption;

                    // no longer a need to iterate though
                    return;

                }  else {
                    //console.log('%s is not within %s and %s', totalCost, cost.min, cost.max);
                }

            });

            // push to our filtered array. This gives us one array where all programs are mixed
            // together, but we can always group them later by programName
            if(matchedOption) filteredPrograms.push(matchedOption);

        });

    });

    /**
    * ----------------------------------------
    * Process filtered programs, for each one factoring the rate * totalCost
    * and assembling other bits and pieces as needed for return
    * ----------------------------------------
    *
    */

    // our returned quote
    var returnQuote = [];

    // iterate through our rates
    // we might have multiple rates for multiple programs.
    // we'll do some calculations to turn a rate into a payment
    _.each(filteredPrograms, function(program) {

       // create empty return array
       var termAndRates = [];

       // pluralize our term period
       program.termPeriod = nounInflector.pluralize(program.termPeriod);

       // iterate thorugh terms, check if a rate for this term exists and is not 0
       // in some cases rates will be 0 if its not a supported term.
       // @todo test that Marlin's formula works when multiplying by cents, not just dollars
       _.each(program.terms, function(term, key) {

           // if there is no payment info, ie there is no rate,
           // don't include it. In some cases the rate will be '0' if not being used.
           if(program.costs.rates[key] === 0 || program.costs.rates[key] === null || typeof program.costs.rates[key] !== 'number') {
                return;
           }

           var payment = applyRateToCost(totalCost, program.costs.rates[key]);

           termAndRates.push({

               // term + plurized version of term length
               term: term + ' ' + program.termPeriod,

               // rate for testing @todo remove for production
               rate: program.costs.rates[key],

               // payment
               payment: payment,
               paymentDisplay: formatPayment(payment),

               // totalCost thus far is in cents, lets convert back
               totalCost: totalCost / 100,
               totalCostDisplay: formatPayment(totalCost / 100)
           });

       });

       // delete stuff and push
       program.rates = termAndRates;
       delete program.costs;
       delete program.terms;

       returnQuote.push(program);

    });

    /**
    * ----------------------------------------
    * Group by program name
    * ----------------------------------------
    *
    */
    returnQuote = _.groupBy(returnQuote, function(item) {
        return item.programName;
    });

    /**
    * ----------------------------------------
    * Handle case where Quote is not within range
    * @todo capture these leads by sending email to sales and vendor rep
    * ----------------------------------------
    *
    */
    if(filteredPrograms.length === 0) {
        return handleNoPrograms(req, res);
    }

    /**
    * ----------------------------------------
    * Save the quote to the database
    * ----------------------------------------
    *
    *
    */

    // try to get quote from request
    var quote = req.quote || null;

    // update or create a quote
    if(quote) {
        quote = _.extend(quote, req.body);
    } else {
        quote = new Quote(req.body);
    }

    // add payments data to it
    // payments is {} in database, so we can efficiently store entire object here
    quote.payments = returnQuote;

    // adjust totalCost so it's back to dollars
    quote.totalCost = quote.totalCost / 100;
    quote.totalCostDisplay = formatPayment(quote.totalCost);

    // finally, save the quote
    quote.save(function(err) {

        // handle error case
        if (err) return res.failure(err);

        // delete some things we dont want to be public
        // @todo later we should all more robust access controll, since the "get" method needs this also
        //delete quote.vendorId;
        //delete quote.salesRep;
        //delete quote.vendorRep;

        // send result to user
        res.ok(quote);

        // get the quote we just saved and trigger emails to end user
        // @note this is async so it will not delay response to user
        Quote
            .findOne({_id : quote._id})
            .populate('vendorId vendorRep salesRep')
            .exec(function(err, result) {

                if(err) return;

                emailer.newQuoteEndUser(req, result);
                emailer.newQuoteSalesRep(req, result);
                emailer.newQuoteVendorRep(req, result);

        });

    });

};

/**
 * Update a quote
 */
exports.update = function(req, res) {
    var quote = req.quote;

    quote = _.extend(quote, req.body);

    quote.save(function(err) {
        res.ok(quote);
    });
};

/**
 * Delete an quote
 */
exports.destroy = function(req, res) {
    var quote = req.quote;

    quote.remove(function(err) {
        if (err) {
            res.failure(err);
        } else {
            res.ok(quote);
        }
    });
};

/**
 * Show an quote
 */
exports.show = function(req, res) {
    res.ok(req.quote);
};

/**
 * List of Quotes
 */
exports.all = function(req, res) {

    var where = {};

    // limit quotes to sales rep only.
    if (req.userHasRole('salesRep')) {
        where = {
            salesRep: req.user._id
        };
    } else if (req.userHasRole('vendorRep')) {
        where = {
            vendorRep: req.user._id
        };
    }

    Quote
        .find(where)
        .sort('-status -created')
    //.populate('vendorId salesRep')
    .exec(function(err, quotes) {
        if (err) {
            res.failure(err);
        } else {
            res.ok(quotes);
        }
    });

};

/**
 * Get quotes for a sales rep.
 *
 * @note This can be used to limit quotes when a user is logged in, or
 *       it can be used for a resource/:id/children instance (if we modify the way we get user id)
 *
 */
exports.getAllForSalesRep = function(req, res) {

    // First get all vendors for this sales rep.
    Vendor
        .where('salesRep')
        .equals(req.user._id)
        .find()
        .select('_id')
        .exec(function(err, vendors) {
            if (err) {
                res.failure(err);
            } else {

                // extract the vendor ids from the results
                // this will be all vendors the user is associated with NOW!
                // @note we don't store user ids with the quotes... because if at any point the vendor gets
                // a new sales rep, things would be out of sync.
                var vendorIds = [];
                _.each(vendors, function(item) {
                    vendorIds.push(item._id);
                });
                getQuotes(vendorIds);

            }
        });

    var getQuotes = function(vendorIds) {

        Quote
            .find()
            .where('vendorId')
            .in(vendorIds)
            .sort('-status -created')
            .exec(function(err, quotes) {
                if (err) {
                    res.failure(err);
                } else {
                    res.ok(quotes, 'Getting quotes for salesRep ' + req.user.fullName);
                }
            });
    };
};
<|endoftext|>


<commit_before>"use strict";

dopple.Token = dopple.Class.extend
({
	print: function()
	{
		var tokenEnum = dopple.TokenEnum;
		switch(this.type)
		{
			case tokenEnum.EOF: return "(EOF)";
			case tokenEnum.SYMBOL: return "(SYMBOL " + this.str + ")";
			case tokenEnum.BINOP: return "(BINOP \"" + this.str + "\")";
			case tokenEnum.NUMBER: return "(NUMBER " + this.value + ")";
			case tokenEnum.BOOL: return "(BOOL " + this.value + ")";
			case tokenEnum.NAME: return "(NAME " + this.str + ")";
			case tokenEnum.STRING: return "(STRING \"" + this.str + "\")";
			case tokenEnum.VAR: return "(VAR)";
			case tokenEnum.RETURN: return "(RETURN)";
			case tokenEnum.FUNCTION: return "(FUNCTION)";
			case tokenEnum.COMMENT: return "(COMMENT)";
		}

		return "(Unknown)";
	},

	//
	type: 0,
	str: "",
	value: 0
});

dopple.Tokenizer = dopple.Class.extend
({
	init: function(buffer)  {
		this.customKeyword = {};
	},

	setBuffer: function(buffer)
	{
		this.buffer = buffer;
		this.bufferLength = buffer.length;
		this.cursor = 0;
		this.currChar = "";
	},

	nextToken: function()
	{
		this.token.type = 0;
		this.token.str = "";
		this.token.value = 0;

		this.nextChar();

		// Skip spaces
		while(isSpace(this.currChar)) {
			this.nextChar();
		}

		// NAME
		if(isAlpha(this.currChar))
		{
			this.token.str += this.currChar;
			this.nextChar();
			while(isAlphaNum(this.currChar)) {
				this.token.str += this.currChar;
				this.nextChar();
			}
			this.cursor--;

			if(this.token.str === "var") {
				this.token.type = this.tokenEnum.VAR;
			}
			else if(this.token.str === "if") {
				this.token.type = this.tokenEnum.IF;
			}
			else if(this.token.str === "return") {
				this.token.type = this.tokenEnum.RETURN;
			}
			else if(this.token.str === "function") {
				this.token.type = this.tokenEnum.FUNCTION;
			}
			else if(this.token.str === "true") {
				this.token.type = this.tokenEnum.BOOL;
				this.token.value = 1;
			}
			else if(this.token.str === "false") {
				this.token.type = this.tokenEnum.BOOL;
			}
			else if(this.token.str === "NaN") {
				this.token.type = this.tokenEnum.NUMBER;
				this.token.value = NaN;
			}
			else if(this.customKeyword[this.token.str]) {
				this.token.type = this.customKeyword[this.token.str];
			}
			else {
				this.token.type = this.tokenEnum.NAME;
			}

			return this.token;
		}

		// Number
		if(isDigit(this.currChar))
		{
			this.token.str += this.currChar;

			this.nextChar();
			while(isDigit(this.currChar)) {
				this.token.str += this.currChar;
				this.nextChar();
			}
			this.cursor--;

			// Only a symbol:
			if(this.token.str === ".") {
				this.token.type = this.tokenEnum.SYMBOL;
				this.token.value = this.token.str;
				return this.token;
			}

			this.token.type = this.tokenEnum.NUMBER;
			this.token.value = parseFloat(this.token.str);
			return this.token;
		}

		// BinOp
		if(isBinOp(this.currChar))
		{
			this.token.str = this.currChar;

			if(this.currChar !== "=")
			{
				this.nextChar();
				if(this.currChar === "=") {
					this.token.str += "=";
					this.token.type = this.tokenEnum.BINOP_ASSIGN;
					return this.token;
				}
				else {
					this.cursor--;
				}
			}

			if(this.currChar === "=")
			{
				this.nextChar();
				while(isBinOp(this.currChar)) {
					this.token.str += this.currChar;
					this.nextChar();
				}

				this.cursor--;
				this.token.type = this.tokenEnum.ASSIGN;
			}
			else if(this.currChar === "-")
			{
				this.nextChar();
				if(isDigit(this.currChar))
				{
					while(isDigit(this.currChar)) {
						this.token.str += this.currChar;
						this.nextChar();
					}
					this.token.value = parseFloat(this.token.str);
					this.token.type = this.tokenEnum.NUMBER;
					this.cursor--;
					return this.token;
				}
				else if(this.currChar === "-") {
					this.token.str += "-";
					this.token.type = this.tokenEnum.UNARY;
				}
				else {
					this.cursor--;
					this.token.type = this.tokenEnum.BINOP;
				}
			}
			else if(this.currChar === "+")
			{
				this.nextChar();
				if(this.currChar === "+") {
					this.token.str += "+";
					this.token.type = this.tokenEnum.UNARY;
				}
				else {
					this.cursor--;
					this.token.type = this.tokenEnum.BINOP;
				}
			}
			else if(this.currChar === "/")
			{
				this.nextChar();
				if(this.currChar === "/") {
					this.skipUntilNewline();
					this.token.type = this.tokenEnum.COMMENT;
					return this.token;
				}
				else if(this.currChar === "*") {
					this.skipUntil("/");
					this.token.type = this.tokenEnum.COMMENT;
					return this.token;
				}

				this.cursor--;
				this.token.type = this.tokenEnum.BINOP;
			}

			return this.token;
		}

		// NAME
		if(this.currChar === "\"" || this.currChar === "'")
		{
			var endChar = this.currChar;

			this.nextChar();
			while(this.currChar !== endChar)
			{
				if(this.currChar === "\0") {
					throw dopple.throw(dopple.Error.UNEXPECTED_EOI);
				}

				this.token.str += this.currChar;
				this.nextChar();
			}

			this.token.type = this.tokenEnum.STRING;
			return this.token;
		}

		// EOF
		if(this.currChar === "\0") {
			this.token.type = this.tokenEnum.EOF;
			return this.token;
		}

		this.token.type = this.tokenEnum.SYMBOL;
		this.token.str = this.currChar;
		return this.token;
	},

	peek: function()
	{
		var tmpCursor = this.cursor;
		var token = this.nextToken();
		this.peekCursor = this.cursor;
		this.cursor = tmpCursor;
		return token;
	},

	eat: function() {
		this.cursor = this.peekCursor;
	},

	nextChar: function(inc)
	{
		if(this.cursor >= this.bufferLength) {
			this.currChar = "\0";
		}
		else {
			this.currChar = this.buffer.charAt(this.cursor);
		}

		this.cursor++;
	},

	readUntil: function(symbol)
	{
		var output = "";

		this.nextChar();
		while(this.currChar !== symbol && this.currChar !== "\0")
		{
			if(this.currChar === "\\") {
				output += "\\";
				this.nextChar();
			}

			output += this.currChar;
			this.nextChar();
		}

		return output;
	},

	skipUntil: function(symbol)
	{
		this.nextChar();
		while(this.currChar !== symbol && this.currChar !== "\0") {
			this.nextChar();
		}
	},

	skipUntilNewline: function()
	{
		this.nextChar();
		while(this.currChar !== "\r" && this.currChar !== "\n" && this.currChar !== "\0") {
			this.nextChar();
		}
	},

	//
	buffer: "",
	bufferLength: 0,
	cursor: 0, peekCursor: 0,
	currChar: "",

	customKeyword: null,

	token: new dopple.Token(),
	tokenEnum: dopple.TokenEnum
});<commit_msg>Tokenizer fixes.<commit_after>"use strict";

dopple.Token = dopple.Class.extend
({
	print: function()
	{
		var tokenEnum = dopple.TokenEnum;
		switch(this.type)
		{
			case tokenEnum.EOF: return "(EOF)";
			case tokenEnum.SYMBOL: return "(SYMBOL " + this.str + ")";
			case tokenEnum.BINOP: return "(BINOP \"" + this.str + "\")";
			case tokenEnum.NUMBER: return "(NUMBER " + this.value + ")";
			case tokenEnum.BOOL: return "(BOOL " + this.value + ")";
			case tokenEnum.NAME: return "(NAME " + this.str + ")";
			case tokenEnum.STRING: return "(STRING \"" + this.str + "\")";
			case tokenEnum.VAR: return "(VAR)";
			case tokenEnum.RETURN: return "(RETURN)";
			case tokenEnum.FUNCTION: return "(FUNCTION)";
			case tokenEnum.COMMENT: return "(COMMENT)";
		}

		return "(Unknown)";
	},

	//
	type: 0,
	str: "",
	value: 0
});

dopple.Tokenizer = dopple.Class.extend
({
	init: function(buffer)  {
		this.customKeyword = {};
	},

	setBuffer: function(buffer)
	{
		this.buffer = buffer;
		this.bufferLength = buffer.length;
		this.cursor = 0;
		this.currChar = "";
	},

	nextToken: function()
	{
		this.token.type = 0;
		this.token.str = "";
		this.token.value = 0;

		this.nextChar();

		// Skip spaces
		while(isSpace(this.currChar)) {
			this.nextChar();
		}

		// NAME
		if(isAlpha(this.currChar))
		{
			this.token.str += this.currChar;
			this.nextChar();
			while(isAlphaNum(this.currChar)) {
				this.token.str += this.currChar;
				this.nextChar();
			}
			this.cursor--;

			if(this.token.str === "var") {
				this.token.type = this.tokenEnum.VAR;
			}
			else if(this.token.str === "if") {
				this.token.type = this.tokenEnum.IF;
			}
			else if(this.token.str === "return") {
				this.token.type = this.tokenEnum.RETURN;
			}
			else if(this.token.str === "function") {
				this.token.type = this.tokenEnum.FUNCTION;
			}
			else if(this.token.str === "true") {
				this.token.type = this.tokenEnum.BOOL;
				this.token.value = 1;
			}
			else if(this.token.str === "false") {
				this.token.type = this.tokenEnum.BOOL;
			}
			else if(this.token.str === "NaN") {
				this.token.type = this.tokenEnum.NUMBER;
				this.token.value = NaN;
			}
			else if(this.customKeyword[this.token.str]) {
				this.token.type = this.customKeyword[this.token.str];
			}
			else {
				this.token.type = this.tokenEnum.NAME;
			}

			return this.token;
		}

		// Number
		if(isDigit(this.currChar))
		{
			this.token.str += this.currChar;

			this.nextChar();
			while(isDigit(this.currChar)) {
				this.token.str += this.currChar;
				this.nextChar();
			}
			this.cursor--;

			// Only a symbol:
			if(this.token.str === ".") {
				this.token.type = this.tokenEnum.SYMBOL;
				this.token.value = this.token.str;
				return this.token;
			}

			this.token.type = this.tokenEnum.NUMBER;
			this.token.value = parseFloat(this.token.str);
			return this.token;
		}

		// BinOp
		if(isBinOp(this.currChar))
		{
			this.token.str = this.currChar;

			if(this.currChar !== "=")
			{
				this.nextChar();
				if(this.currChar === "=") {
					this.token.str += "=";
					this.token.type = this.tokenEnum.BINOP_ASSIGN;
					return this.token;
				}
				else {
					this.prevChar();
				}
			}

			if(this.currChar === "=")
			{
				this.nextChar();
				while(isBinOp(this.currChar)) {
					this.token.str += this.currChar;
					this.nextChar();
				}

				this.cursor--;
				this.token.type = this.tokenEnum.ASSIGN;
			}
			else if(this.currChar === "-")
			{
				this.nextChar();
				if(isDigit(this.currChar))
				{
					while(isDigit(this.currChar)) {
						this.token.str += this.currChar;
						this.nextChar();
					}
					this.token.value = parseFloat(this.token.str);
					this.token.type = this.tokenEnum.NUMBER;
					this.cursor--;
					return this.token;
				}
				else if(this.currChar === "-") {
					this.token.str += "-";
					this.token.type = this.tokenEnum.UNARY;
				}
				else {
					this.cursor--;
					this.token.type = this.tokenEnum.BINOP;
				}
			}
			else if(this.currChar === "+")
			{
				this.nextChar();
				if(this.currChar === "+") {
					this.token.str += "+";
					this.token.type = this.tokenEnum.UNARY;
				}
				else {
					this.cursor--;
					this.token.type = this.tokenEnum.BINOP;
				}
			}
			else if(this.currChar === "/")
			{
				this.nextChar();
				if(this.currChar === "/") {
					this.skipUntilNewline();
					this.token.type = this.tokenEnum.COMMENT;
					return this.token;
				}
				else if(this.currChar === "*") {
					this.skipUntil("/");
					this.token.type = this.tokenEnum.COMMENT;
					return this.token;
				}

				this.cursor--;
				this.token.type = this.tokenEnum.BINOP;
			}
			else {
				this.token.type = this.tokenEnum.BINOP;
			}

			return this.token;
		}

		// NAME
		if(this.currChar === "\"" || this.currChar === "'")
		{
			var endChar = this.currChar;

			this.nextChar();
			while(this.currChar !== endChar)
			{
				if(this.currChar === "\0") {
					throw dopple.throw(dopple.Error.UNEXPECTED_EOI);
				}

				this.token.str += this.currChar;
				this.nextChar();
			}

			this.token.type = this.tokenEnum.STRING;
			return this.token;
		}

		// EOF
		if(this.currChar === "\0") {
			this.token.type = this.tokenEnum.EOF;
			return this.token;
		}

		this.token.type = this.tokenEnum.SYMBOL;
		this.token.str = this.currChar;
		return this.token;
	},

	peek: function()
	{
		var tmpCursor = this.cursor;
		var token = this.nextToken();
		this.peekCursor = this.cursor;
		this.cursor = tmpCursor;
		return token;
	},

	eat: function() {
		this.cursor = this.peekCursor;
	},

	nextChar: function()
	{
		if(this.cursor >= this.bufferLength) {
			this.currChar = "\0";
		}
		else {
			this.currChar = this.buffer.charAt(this.cursor);
		}

		this.cursor++;
	},

	prevChar: function() {
		this.cursor--;
		this.currChar = this.buffer.charAt(this.cursor - 1);
	},

	readUntil: function(symbol)
	{
		var output = "";

		this.nextChar();
		while(this.currChar !== symbol && this.currChar !== "\0")
		{
			if(this.currChar === "\\") {
				output += "\\";
				this.nextChar();
			}

			output += this.currChar;
			this.nextChar();
		}

		return output;
	},

	skipUntil: function(symbol)
	{
		this.nextChar();
		while(this.currChar !== symbol && this.currChar !== "\0") {
			this.nextChar();
		}
	},

	skipUntilNewline: function()
	{
		this.nextChar();
		while(this.currChar !== "\r" && this.currChar !== "\n" && this.currChar !== "\0") {
			this.nextChar();
		}
	},

	//
	buffer: "",
	bufferLength: 0,
	cursor: 0, peekCursor: 0,
	currChar: "",

	customKeyword: null,

	token: new dopple.Token(),
	tokenEnum: dopple.TokenEnum
});<|endoftext|>


<commit_before>'use strict';

// Declare app level module which depends on views, and components
var app = angular.module('facilitation', [
    'ngRoute',
    'ngCookies',
    'ngResource',
    'ngSanitize',
    'ngRoute',
    'ui.bootstrap',
    'http-auth-interceptor',
    'angularNotify',
    'socketio.service',
    'facilitation.qrcode'
]);

app.config(function($routeProvider, $locationProvider) {
  $routeProvider
      .when("/", {
          templateUrl: "components/home/home.html"
      })
      .when("/importer", {
          templateUrl: "components/importer/importer.html",
          controller:"importationCtrl"
      })
      .when("/catalogue", {
          templateUrl: "components/catalogue/catalogue.html",
          controller: "catalogueCtrl"
      })
      .when("/feedback/:instanceId", {
          templateUrl: "components/feedback/feedback.html"
      })
      .when("/catalogue/:catalogueId", {
          templateUrl: "components/workshop/workshop.html",
          controller: "workshopCtrl"
      })
      .when("/favoriteWorkshops", {
          templateUrl: "components/favorite_workshops/favorite/favorite.html",
          controller: "favoriteCtrl"
      })
      .when("/instances", {
          templateUrl: "components/favorite_workshops/instances/instances.html",
          controller: "menuCtrl"
      })
      .when("/instances/:idInstance", {
          templateUrl: "components/workshop_instance/workshop-instance.html",
          controller: "instanceCtrl"
      })
      .when("/instances/:idInstance/qrcode", {
          templateUrl: "components/qrcode/qrcode.html",
          controller: "qrcodeCtrl"
      });
    // $locationProvider.html5Mode(true);
});

app.run(function ($rootScope, $location, Auth) {

    //watching the value of the currentUser variable.
    $rootScope.$watch('currentUser', function(currentUser) {
        // if no currentUser and on a page that requires authorization then try to update it
        // will trigger 401s if user does not have a valid session
        if (!currentUser && (['/'].indexOf($location.path()) == -1 )) {
            Auth.currentUser();
        }
    });

    // On catching 401 errors, redirect to the login page.
    $rootScope.$on('event:auth-loginRequired', function() {
        $location.path('/blabalabl');
        return false;
    });
});
<commit_msg>bug "blablabl" fixed<commit_after>'use strict';

// Declare app level module which depends on views, and components
var app = angular.module('facilitation', [
    'ngRoute',
    'ngCookies',
    'ngResource',
    'ngSanitize',
    'ngRoute',
    'ui.bootstrap',
    'http-auth-interceptor',
    'angularNotify',
    'socketio.service',
    'facilitation.qrcode'
]);

app.config(function($routeProvider, $locationProvider) {
  $routeProvider
      .when("/", {
          templateUrl: "components/home/home.html"
      })
      .when("/importer", {
          templateUrl: "components/importer/importer.html",
          controller:"importationCtrl"
      })
      .when("/catalogue", {
          templateUrl: "components/catalogue/catalogue.html",
          controller: "catalogueCtrl"
      })
      .when("/feedback/:instanceId", {
          templateUrl: "components/feedback/feedback.html"
      })
      .when("/catalogue/:catalogueId", {
          templateUrl: "components/workshop/workshop.html",
          controller: "workshopCtrl"
      })
      .when("/favoriteWorkshops", {
          templateUrl: "components/favorite_workshops/favorite/favorite.html",
          controller: "favoriteCtrl"
      })
      .when("/instances", {
          templateUrl: "components/favorite_workshops/instances/instances.html",
          controller: "menuCtrl"
      })
      .when("/instances/:idInstance", {
          templateUrl: "components/workshop_instance/workshop-instance.html",
          controller: "instanceCtrl"
      })
      .when("/instances/:idInstance/qrcode", {
          templateUrl: "components/qrcode/qrcode.html",
          controller: "qrcodeCtrl"
      });
    // $locationProvider.html5Mode(true);
});

app.run(function ($rootScope, $location, Auth) {

    //watching the value of the currentUser variable.
    $rootScope.$watch('currentUser', function(currentUser) {
        // if no currentUser and on a page that requires authorization then try to update it
        // will trigger 401s if user does not have a valid session
        if (!currentUser && (['/'].indexOf($location.path()) == -1 )) {
            Auth.currentUser();
        }
    });

    // On catching 401 errors, redirect to the login page.
    $rootScope.$on('event:auth-loginRequired', function() {
        $location.path('/');
        return false;
    });
});
<|endoftext|>


<commit_before>/**
* ownCloud
*
* @author Vincent Petry
* @copyright 2014 Vincent Petry <pvince81@owncloud.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library.  If not, see <http://www.gnu.org/licenses/>.
*
*/

(function() {
	/**
	 * @class BreadCrumb
	 * @memberof OCA.Files
	 * @classdesc Breadcrumbs that represent the current path.
	 *
	 * @param {Object} [options] options
	 * @param {Function} [options.onClick] click event handler
	 * @param {Function} [options.onDrop] drop event handler
	 * @param {Function} [options.getCrumbUrl] callback that returns
	 * the URL of a given breadcrumb
	 */
	var BreadCrumb = function(options){
		this.$el = $('

');
		this.$menu = $('

');

		this.crumbSelector = '.crumb:not(.hidden):not(.crumbhome):not(.crumbmenu)';
		options = options || {};
		if (options.onClick) {
			this.onClick = options.onClick;
		}
		if (options.onDrop) {
			this.onDrop = options.onDrop;
			this.onOver = options.onOver;
			this.onOut = options.onOut;
		}
		if (options.getCrumbUrl) {
			this.getCrumbUrl = options.getCrumbUrl;
		}
		this._detailViews = [];
	};

	/**
	 * @memberof OCA.Files
	 */
	BreadCrumb.prototype = {
		$el: null,
		dir: null,
		dirInfo: null,

		/**
		 * Total width of all breadcrumbs
		 * @type int
		 * @private
		 */
		totalWidth: 0,
		breadcrumbs: [],
		onClick: null,
		onDrop: null,
		onOver: null,
		onOut: null,

		/**
		 * Sets the directory to be displayed as breadcrumb.
		 * This will re-render the breadcrumb.
		 * @param dir path to be displayed as breadcrumb
		 */
		setDirectory: function(dir) {
			dir = dir.replace(/\\/g, '/');
			dir = dir || '/';
			if (dir !== this.dir) {
				this.dir = dir;
				this.render();
			}
		},

		setDirectoryInfo: function(dirInfo) {
			if (dirInfo !== this.dirInfo) {
				this.dirInfo = dirInfo;
				this.render();
			}
		},

		/**
		 * @param {Backbone.View} detailView
		 */
		addDetailView: function(detailView) {
			this._detailViews.push(detailView);
		},

		/**
		 * Returns the full URL to the given directory
		 *
		 * @param {Object.<String, String>} part crumb data as map
		 * @param {int} index crumb index
		 * @return full URL
		 */
		getCrumbUrl: function(part, index) {
			return '#';
		},

		/**
		 * Renders the breadcrumb elements
		 */
		render: function() {
			// Hide menu on render
			OC.hideMenus();

			var parts = this._makeCrumbs(this.dir || '/');
			var $crumb;
			var $menuItem;
			this.$el.empty();
			this.breadcrumbs = [];

			for (var i = 0; i < parts.length; i++) {
				var part = parts[i];
				var $image;
				var $link = $('');
				$crumb = $('

');
				if(part.dir) {
					$link.attr('href', this.getCrumbUrl(part, i));
				}
				if(part.name) {
					$link.text(part.name);
				}
				$link.addClass(part.linkclass);
				$crumb.append($link);
				$crumb.data('dir', part.dir);
				// Ignore menu button
				$crumb.data('crumb-id', i - 1);
				$crumb.addClass(part.class);

				if (part.img) {
					$image = $('');
					$image.attr('src', part.img);
					$image.attr('alt', part.alt);
					$link.append($image);
				}
				this.breadcrumbs.push($crumb);
				this.$el.append($crumb);
				// Only add feedback if not menu
				if (this.onClick && i !== 0) {
					$link.on('click', this.onClick);
				}
			}

			// Menu creation
			this._createMenu();
			for (var j = 0; j < parts.length; j++) {
				var menuPart = parts[j];
				if(menuPart.dir) {
					$menuItem = $('

');
					$menuItem.data('dir', menuPart.dir);
					$menuItem.find('a').attr('href', this.getCrumbUrl(part, j));
					$menuItem.find('span:eq(1)').text(menuPart.name);
					this.$menu.children('ul').append($menuItem);
					if (this.onClick) {
						$menuItem.on('click', this.onClick);
					}
				}
			}
			_.each(this._detailViews, function(view) {
				view.render({
					dirInfo: this.dirInfo
				});
				$crumb.append(view.$el);
			}, this);

			// in case svg is not supported by the browser we need to execute the fallback mechanism
			if (!OC.Util.hasSVGSupport()) {
				OC.Util.replaceSVG(this.$el);
			}

			// setup drag and drop
			if (this.onDrop) {
				this.$el.find('.crumb:not(:last-child):not(.crumbmenu), .crumblist:not(:last-child)').droppable({
					drop: this.onDrop,
					over: this.onOver,
					out: this.onOut,
					tolerance: 'pointer',
					hoverClass: 'canDrop',
					greedy: true
				});
			}
			// Menu is destroyed on every change, we need to init it
			OC.registerMenu($('.crumbmenu'), $('.crumbmenu > .popovermenu'));
			this._resize();
		},

		/**
		 * Makes a breadcrumb structure based on the given path
		 *
		 * @param {String} dir path to split into a breadcrumb structure
		 * @return {Object.<String, String>} map of {dir: path, name: displayName}
		 */
		_makeCrumbs: function(dir) {
			var crumbs = [];
			var pathToHere = '';
			// trim leading and trailing slashes
			dir = dir.replace(/^\/+|\/+$/g, '');
			var parts = dir.split('/');
			if (dir === '') {
				parts = [];
			}
			// menu part
			crumbs.push({
				class: 'crumbmenu hidden',
				linkclass: 'icon-more'
			});
			// root part
			crumbs.push({
				name: t('core', 'Home'),
				dir: '/',
				class: 'crumbhome',
				linkclass: 'icon-home'
			});
			for (var i = 0; i < parts.length; i++) {
				var part = parts[i];
				pathToHere = pathToHere + '/' + part;
				crumbs.push({
					dir: pathToHere,
					name: part
				});
			}
			return crumbs;
		},

		/**
		 * Show/hide breadcrumbs to fit the given width
		 * Mostly used by tests
		 *
		 * @param {int} availableWidth available width
		 */
		setMaxWidth: function (availableWidth) {
			if (this.availableWidth !== availableWidth) {
				this.availableWidth = availableWidth;
				this._resize();
			}
		},

		/**
		 * Calculate real width based on individual crumbs
		 * More accurate and works with tests
		 *
		 * @param {boolean} ignoreHidden ignore hidden crumbs
		 */
		getTotalWidth: function(ignoreHidden) {
			var totalWidth = 0;
			for (var i = 0; i < this.breadcrumbs.length; i++ ) {
				var $crumb = $(this.breadcrumbs[i]);
				if(!$crumb.hasClass('hidden') || ignoreHidden === true) {
					totalWidth += $crumb.outerWidth();
				}
			}
			return totalWidth;
		},

 		/**
 		 * Hide the middle crumb
 		 */
 		_hideCrumb: function() {
			var length = this.$el.find(this.crumbSelector).length;
			// Get the middle one floored down
			var elmt = Math.floor(length / 2 - 0.5);
			this.$el.find(this.crumbSelector+':eq('+elmt+')').addClass('hidden');
 		},

 		/**
 		 * Get the crumb to show
 		 */
 		_getCrumbElement: function() {
			var hidden = this.$el.find('.crumb.hidden').length;
			var shown = this.$el.find(this.crumbSelector).length;
			// Get the outer one with priority to the highest
			var elmt = (1 - shown % 2) * (hidden - 1);
			return this.$el.find('.crumb.hidden:eq('+elmt+')');
		},

 		/**
 		 * Show the middle crumb
 		 */
 		_showCrumb: function() {
			if(this.$el.find('.crumb.hidden').length === 1) {
				this.$el.find('.crumb.hidden').removeClass('hidden');
			}
			this._getCrumbElement().removeClass('hidden');
 		},

		/**
		 * Create and append the popovermenu
		 */
		_createMenu: function() {
			this.$el.find('.crumbmenu').append(this.$menu);
			this.$menu.children('ul').empty();
		},

		/**
		 * Update the popovermenu
		 */
		_updateMenu: function() {
			var menuItems = this.$el.find('.crumb.hidden');
			// Hide the crumb menu if no elements
			this.$el.find('.crumbmenu').toggleClass('hidden', menuItems.length === 0);

			this.$menu.find('li').addClass('in-breadcrumb');
			for (var i = 0; i < menuItems.length; i++) {
				var crumbId = $(menuItems[i]).data('crumb-id');
				this.$menu.find('li:eq('+crumbId+')').removeClass('in-breadcrumb');
			}
		},

		_resize: function() {

			if (this.breadcrumbs.length <= 2) {
				// home & menu
				return;
			}

			// Used for testing since this.$el.parent fails
			if (!this.availableWidth) {
				this.usedWidth = this.$el.parent().width() - this.$el.next('.actions').width();
			} else {
				this.usedWidth = this.availableWidth;
			}

			// If container is smaller than content
			// AND if there are crumbs left to hide
			while (this.getTotalWidth() > this.usedWidth
			&& this.$el.find(this.crumbSelector).length > 0) {
				this._hideCrumb();
			}
			// If container is bigger than content + element to be shown
			// AND if there is at least one hidden crumb
			while (this.$el.find('.crumb.hidden').length > 0
				&& this.getTotalWidth() + this._getCrumbElement().width() < this.usedWidth) {
				this._showCrumb();
			}

			this._updateMenu();
		}
	};

	OCA.Files.BreadCrumb = BreadCrumb;
})();
<commit_msg>Fix menu declaration and width calculation<commit_after>/**
* ownCloud
*
* @author Vincent Petry
* @copyright 2014 Vincent Petry <pvince81@owncloud.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library.  If not, see <http://www.gnu.org/licenses/>.
*
*/

(function() {
	/**
	 * @class BreadCrumb
	 * @memberof OCA.Files
	 * @classdesc Breadcrumbs that represent the current path.
	 *
	 * @param {Object} [options] options
	 * @param {Function} [options.onClick] click event handler
	 * @param {Function} [options.onDrop] drop event handler
	 * @param {Function} [options.getCrumbUrl] callback that returns
	 * the URL of a given breadcrumb
	 */
	var BreadCrumb = function(options){
		this.$el = $('

');
		this.$menu = $('

');

		this.crumbSelector = '.crumb:not(.hidden):not(.crumbhome):not(.crumbmenu)';
		options = options || {};
		if (options.onClick) {
			this.onClick = options.onClick;
		}
		if (options.onDrop) {
			this.onDrop = options.onDrop;
			this.onOver = options.onOver;
			this.onOut = options.onOut;
		}
		if (options.getCrumbUrl) {
			this.getCrumbUrl = options.getCrumbUrl;
		}
		this._detailViews = [];
	};

	/**
	 * @memberof OCA.Files
	 */
	BreadCrumb.prototype = {
		$el: null,
		dir: null,
		dirInfo: null,

		/**
		 * Total width of all breadcrumbs
		 * @type int
		 * @private
		 */
		totalWidth: 0,
		breadcrumbs: [],
		onClick: null,
		onDrop: null,
		onOver: null,
		onOut: null,

		/**
		 * Sets the directory to be displayed as breadcrumb.
		 * This will re-render the breadcrumb.
		 * @param dir path to be displayed as breadcrumb
		 */
		setDirectory: function(dir) {
			dir = dir.replace(/\\/g, '/');
			dir = dir || '/';
			if (dir !== this.dir) {
				this.dir = dir;
				this.render();
			}
		},

		setDirectoryInfo: function(dirInfo) {
			if (dirInfo !== this.dirInfo) {
				this.dirInfo = dirInfo;
				this.render();
			}
		},

		/**
		 * @param {Backbone.View} detailView
		 */
		addDetailView: function(detailView) {
			this._detailViews.push(detailView);
		},

		/**
		 * Returns the full URL to the given directory
		 *
		 * @param {Object.<String, String>} part crumb data as map
		 * @param {int} index crumb index
		 * @return full URL
		 */
		getCrumbUrl: function(part, index) {
			return '#';
		},

		/**
		 * Renders the breadcrumb elements
		 */
		render: function() {
			// Menu is destroyed on every change, we need to init it
			OC.unregisterMenu($('.crumbmenu'), $('.crumbmenu > .popovermenu'));

			var parts = this._makeCrumbs(this.dir || '/');
			var $crumb;
			var $menuItem;
			this.$el.empty();
			this.breadcrumbs = [];

			for (var i = 0; i < parts.length; i++) {
				var part = parts[i];
				var $image;
				var $link = $('');
				$crumb = $('

');
				if(part.dir) {
					$link.attr('href', this.getCrumbUrl(part, i));
				}
				if(part.name) {
					$link.text(part.name);
				}
				$link.addClass(part.linkclass);
				$crumb.append($link);
				$crumb.data('dir', part.dir);
				// Ignore menu button
				$crumb.data('crumb-id', i - 1);
				$crumb.addClass(part.class);

				if (part.img) {
					$image = $('');
					$image.attr('src', part.img);
					$image.attr('alt', part.alt);
					$link.append($image);
				}
				this.breadcrumbs.push($crumb);
				this.$el.append($crumb);
				// Only add feedback if not menu
				if (this.onClick && i !== 0) {
					$link.on('click', this.onClick);
				}
			}

			// Menu creation
			this._createMenu();
			for (var j = 0; j < parts.length; j++) {
				var menuPart = parts[j];
				if(menuPart.dir) {
					$menuItem = $('

');
					$menuItem.data('dir', menuPart.dir);
					$menuItem.find('a').attr('href', this.getCrumbUrl(part, j));
					$menuItem.find('span:eq(1)').text(menuPart.name);
					this.$menu.children('ul').append($menuItem);
					if (this.onClick) {
						$menuItem.on('click', this.onClick);
					}
				}
			}
			_.each(this._detailViews, function(view) {
				view.render({
					dirInfo: this.dirInfo
				});
				$crumb.append(view.$el);
			}, this);

			// in case svg is not supported by the browser we need to execute the fallback mechanism
			if (!OC.Util.hasSVGSupport()) {
				OC.Util.replaceSVG(this.$el);
			}

			// setup drag and drop
			if (this.onDrop) {
				this.$el.find('.crumb:not(:last-child):not(.crumbmenu), .crumblist:not(:last-child)').droppable({
					drop: this.onDrop,
					over: this.onOver,
					out: this.onOut,
					tolerance: 'pointer',
					hoverClass: 'canDrop',
					greedy: true
				});
			}

			// Menu is destroyed on every change, we need to init it
			OC.registerMenu($('.crumbmenu'), $('.crumbmenu > .popovermenu'));

			this._resize();
		},

		/**
		 * Makes a breadcrumb structure based on the given path
		 *
		 * @param {String} dir path to split into a breadcrumb structure
		 * @return {Object.<String, String>} map of {dir: path, name: displayName}
		 */
		_makeCrumbs: function(dir) {
			var crumbs = [];
			var pathToHere = '';
			// trim leading and trailing slashes
			dir = dir.replace(/^\/+|\/+$/g, '');
			var parts = dir.split('/');
			if (dir === '') {
				parts = [];
			}
			// menu part
			crumbs.push({
				class: 'crumbmenu hidden',
				linkclass: 'icon-more'
			});
			// root part
			crumbs.push({
				name: t('core', 'Home'),
				dir: '/',
				class: 'crumbhome',
				linkclass: 'icon-home'
			});
			for (var i = 0; i < parts.length; i++) {
				var part = parts[i];
				pathToHere = pathToHere + '/' + part;
				crumbs.push({
					dir: pathToHere,
					name: part
				});
			}
			return crumbs;
		},

		/**
		 * Show/hide breadcrumbs to fit the given width
		 * Mostly used by tests
		 *
		 * @param {int} availableWidth available width
		 */
		setMaxWidth: function (availableWidth) {
			if (this.availableWidth !== availableWidth) {
				this.availableWidth = availableWidth;
				this._resize();
			}
		},

		/**
		 * Calculate real width based on individual crumbs
		 * More accurate and works with tests
		 *
		 * @param {boolean} ignoreHidden ignore hidden crumbs
		 */
		getTotalWidth: function(ignoreHidden) {
			var totalWidth = 0;
			for (var i = 0; i < this.breadcrumbs.length; i++ ) {
				var $crumb = $(this.breadcrumbs[i]);
				if(!$crumb.hasClass('hidden') || ignoreHidden === true) {
					totalWidth += $crumb.outerWidth();
				}
			}
			return totalWidth;
		},

 		/**
 		 * Hide the middle crumb
 		 */
 		_hideCrumb: function() {
			var length = this.$el.find(this.crumbSelector).length;
			// Get the middle one floored down
			var elmt = Math.floor(length / 2 - 0.5);
			this.$el.find(this.crumbSelector+':eq('+elmt+')').addClass('hidden');
 		},

 		/**
 		 * Get the crumb to show
 		 */
 		_getCrumbElement: function() {
			var hidden = this.$el.find('.crumb.hidden').length;
			var shown = this.$el.find(this.crumbSelector).length;
			// Get the outer one with priority to the highest
			var elmt = (1 - shown % 2) * (hidden - 1);
			return this.$el.find('.crumb.hidden:eq('+elmt+')');
		},

 		/**
 		 * Show the middle crumb
 		 */
 		_showCrumb: function() {
			if(this.$el.find('.crumb.hidden').length === 1) {
				this.$el.find('.crumb.hidden').removeClass('hidden');
			}
			this._getCrumbElement().removeClass('hidden');
 		},

		/**
		 * Create and append the popovermenu
		 */
		_createMenu: function() {
			this.$el.find('.crumbmenu').append(this.$menu);
			this.$menu.children('ul').empty();
		},

		/**
		 * Update the popovermenu
		 */
		_updateMenu: function() {
			var menuItems = this.$el.find('.crumb.hidden');
			// Hide the crumb menu if no elements
			this.$el.find('.crumbmenu').toggleClass('hidden', menuItems.length === 0);

			this.$menu.find('li').addClass('in-breadcrumb');
			for (var i = 0; i < menuItems.length; i++) {
				var crumbId = $(menuItems[i]).data('crumb-id');
				this.$menu.find('li:eq('+crumbId+')').removeClass('in-breadcrumb');
			}
		},

		_resize: function() {

			if (this.breadcrumbs.length <= 2) {
				// home & menu
				return;
			}

			// Used for testing since this.$el.parent fails
			if (!this.availableWidth) {
				this.usedWidth = this.$el.parent().width() - (this.$el.parent().find('.button').length + 1) * 44;
			} else {
				this.usedWidth = this.availableWidth;
			}

			// If container is smaller than content
			// AND if there are crumbs left to hide
			while (this.getTotalWidth() > this.usedWidth
				&& this.$el.find(this.crumbSelector).length > 0) {
				this._hideCrumb();
			}
			// If container is bigger than content + element to be shown
			// AND if there is at least one hidden crumb
			while (this.$el.find('.crumb.hidden').length > 0
				&& this.getTotalWidth() + this._getCrumbElement().width() < this.usedWidth) {
				this._showCrumb();
			}

			this._updateMenu();
		}
	};

	OCA.Files.BreadCrumb = BreadCrumb;
})();
<|endoftext|>


<commit_before>import d3 from 'd3';
import fc from 'd3fc';
import util from '../util/util';
import event from '../event';
import zoomBehavior from '../behavior/zoom';

export default function() {
    var navHeight = 100; // Also maintain in variables.less
    var bottomMargin = 40; // Also maintain in variables.less
    var navChartHeight = navHeight - bottomMargin;
    var backgroundStrokeWidth = 2; // Also maintain in variables.less
    // Stroke is half inside half outside, so stroke/2 per border
    var borderWidth = backgroundStrokeWidth / 2;
    // should have been 2 * borderWidth, but for unknown reason it is incorrect in practice.
    var extentHeight = navChartHeight - borderWidth;
    var barHeight = extentHeight;
    var handleCircleCenter = borderWidth + barHeight / 2;
    var handleBarWidth = 2;

    var dispatch = d3.dispatch(event.viewChange);

    var navChart = fc.chart.cartesian(fc.scale.dateTime(), d3.scale.linear())
      .yTicks(0)
      .margin({
          bottom: bottomMargin      // Variable also in navigator.less - should be used once ported to flex
      });

    var viewScale = fc.scale.dateTime();

    var area = fc.series.area()
      .yValue(function(d) { return d.close; });
    var line = fc.series.line()
      .yValue(function(d) { return d.close; });
    var brush = d3.svg.brush();
    var navMulti = fc.series.multi()
      .series([area, line, brush])
      .decorate(function(selection) {
          var enter = selection.enter();

          selection.select('.extent')
            .attr('height', extentHeight)
            .attr('y', backgroundStrokeWidth / 2);

          // overload d3 styling for the brush handles
          // as Firefox does not react properly to setting these through less file.
          enter.selectAll('.resize.w>rect, .resize.e>rect')
            .attr('width', handleBarWidth)
            .attr('x', -handleBarWidth / 2);
          selection.selectAll('.resize.w>rect, .resize.e>rect')
            .attr('height', barHeight)
            .attr('y', borderWidth);
          enter.select('.extent')
            .attr('mask', 'url("#brush-mask")')
            .attr('fill', 'url("#brush-gradient")');

          // Adds the handles to the brush sides
          var handles = enter.selectAll('.e, .w');
          handles.append('circle')
            .attr('cy', handleCircleCenter)
            .attr('r', 7)
            .attr('class', 'outer-handle');
          handles.append('circle')
            .attr('cy', handleCircleCenter)
            .attr('r', 4)
            .attr('class', 'inner-handle');
      })
      .mapping(function(series) {
          if (series === brush) {
              brush.extent([
                  [viewScale.domain()[0], navChart.yDomain()[0]],
                  [viewScale.domain()[1], navChart.yDomain()[1]]
              ]);
          } else {
              // This stops the brush data being overwritten by the point data
              return this.data;
          }
      });

    var maskXScale = fc.scale.dateTime();
    var maskYScale = d3.scale.linear();

    var brushMask = fc.series.area()
      .yValue(function(d) { return d.close; })
      .xScale(maskXScale)
      .yScale(maskYScale);

    var layoutWidth;

    function setHide(selection, brushHide) {
        selection.select('.plot-area')
          .selectAll('.e, .w')
          .classed('hidden', brushHide);
    }

    function xEmpty(navBrush) {
        return ((navBrush.extent()[0][0] - navBrush.extent()[1][0]) === 0);
    }

    function createDefs(selection, data) {
        var defsEnter = selection.selectAll('defs')
          .data([0])
          .enter()
          .append('defs');

        defsEnter.html('<linearGradient id="brush-gradient" x1="0" x2="0" y1="0" y2="1"> \
              <stop offset="0%" class="brush-gradient-top" /> \
              <stop offset="100%" class="brush-gradient-bottom" /> \
          </linearGradient> \
          <mask id="brush-mask"> \
              <rect class="mask-background"></rect> \
          </mask>');

        selection.select('.mask-background').attr({
            width: layoutWidth,
            height: navChartHeight
        });

        maskXScale.domain(fc.util.extent().fields('date')(data));
        maskYScale.domain(fc.util.extent().fields(['low', 'high'])(data));

        selection.select('mask')
            .datum(data)
            .call(brushMask);
    }

    function nav(selection) {
        var model = selection.datum();

        createDefs(selection, model.data);

        viewScale.domain(model.viewDomain);

        var filteredData = util.domain.filterDataInDateRange(
          fc.util.extent().fields('date')(model.data),
          model.data);
        var yExtent = fc.util.extent()
          .fields(['low', 'high'])(filteredData);

        var brushHide = false;

        navChart.xDomain(fc.util.extent().fields('date')(model.data))
          .yDomain(yExtent);

        brush.on('brush', function() {
            var brushExtentIsEmpty = xEmpty(brush);

            // Hide the bar if the extent is empty
            setHide(selection, brushExtentIsEmpty);
            if (!brushExtentIsEmpty) {
                dispatch[event.viewChange]([brush.extent()[0][0], brush.extent()[1][0]]);
            }
        })
            .on('brushend', function() {
                var brushExtentIsEmpty = xEmpty(brush);
                setHide(selection, false);
                if (brushExtentIsEmpty) {
                    dispatch[event.viewChange](util.domain.centerOnDate(viewScale.domain(),
                        model.data, brush.extent()[0][0]));
                }
            });

        navChart.plotArea(navMulti);
        selection.call(navChart);

        // Allow to zoom using mouse, but disable panning
        var zoom = zoomBehavior(layoutWidth)
          .scale(viewScale)
          .trackingLatest(model.trackingLatest)
          .allowPan(false)
          .on('zoom', function(domain) {
              dispatch[event.viewChange](domain);
          });

        selection.select('.plot-area')
          .call(zoom);
    }

    d3.rebind(nav, dispatch, 'on');

    nav.dimensionChanged = function(container) {
        layoutWidth = util.width(container.node());
        viewScale.range([0, layoutWidth]);
        maskXScale.range([0, layoutWidth]);
        maskYScale.range([navChartHeight, 0]);
    };

    return nav;
}
<commit_msg>Padded nav y extent.<commit_after>import d3 from 'd3';
import fc from 'd3fc';
import util from '../util/util';
import event from '../event';
import zoomBehavior from '../behavior/zoom';

export default function() {
    var navHeight = 100; // Also maintain in variables.less
    var bottomMargin = 40; // Also maintain in variables.less
    var navChartHeight = navHeight - bottomMargin;
    var backgroundStrokeWidth = 2; // Also maintain in variables.less
    // Stroke is half inside half outside, so stroke/2 per border
    var borderWidth = backgroundStrokeWidth / 2;
    // should have been 2 * borderWidth, but for unknown reason it is incorrect in practice.
    var extentHeight = navChartHeight - borderWidth;
    var barHeight = extentHeight;
    var handleCircleCenter = borderWidth + barHeight / 2;
    var handleBarWidth = 2;
    var yExtentPadding = 0.2;

    var dispatch = d3.dispatch(event.viewChange);

    var navChart = fc.chart.cartesian(fc.scale.dateTime(), d3.scale.linear())
      .yTicks(0)
      .margin({
          bottom: bottomMargin      // Variable also in navigator.less - should be used once ported to flex
      });

    var viewScale = fc.scale.dateTime();

    var area = fc.series.area()
      .yValue(function(d) { return d.close; });
    var line = fc.series.line()
      .yValue(function(d) { return d.close; });
    var brush = d3.svg.brush();
    var navMulti = fc.series.multi()
      .series([area, line, brush])
      .decorate(function(selection) {
          var enter = selection.enter();

          selection.select('.extent')
            .attr('height', extentHeight)
            .attr('y', backgroundStrokeWidth / 2);

          // overload d3 styling for the brush handles
          // as Firefox does not react properly to setting these through less file.
          enter.selectAll('.resize.w>rect, .resize.e>rect')
            .attr('width', handleBarWidth)
            .attr('x', -handleBarWidth / 2);
          selection.selectAll('.resize.w>rect, .resize.e>rect')
            .attr('height', barHeight)
            .attr('y', borderWidth);
          enter.select('.extent')
            .attr('mask', 'url("#brush-mask")')
            .attr('fill', 'url("#brush-gradient")');

          // Adds the handles to the brush sides
          var handles = enter.selectAll('.e, .w');
          handles.append('circle')
            .attr('cy', handleCircleCenter)
            .attr('r', 7)
            .attr('class', 'outer-handle');
          handles.append('circle')
            .attr('cy', handleCircleCenter)
            .attr('r', 4)
            .attr('class', 'inner-handle');
      })
      .mapping(function(series) {
          if (series === brush) {
              brush.extent([
                  [viewScale.domain()[0], navChart.yDomain()[0]],
                  [viewScale.domain()[1], navChart.yDomain()[1]]
              ]);
          } else {
              // This stops the brush data being overwritten by the point data
              return this.data;
          }
      });

    var maskXScale = fc.scale.dateTime();
    var maskYScale = d3.scale.linear();

    var brushMask = fc.series.area()
      .yValue(function(d) { return d.close; })
      .xScale(maskXScale)
      .yScale(maskYScale);

    var layoutWidth;

    function setHide(selection, brushHide) {
        selection.select('.plot-area')
          .selectAll('.e, .w')
          .classed('hidden', brushHide);
    }

    function xEmpty(navBrush) {
        return ((navBrush.extent()[0][0] - navBrush.extent()[1][0]) === 0);
    }

    function createDefs(selection, data) {
        var defsEnter = selection.selectAll('defs')
          .data([0])
          .enter()
          .append('defs');

        defsEnter.html('<linearGradient id="brush-gradient" x1="0" x2="0" y1="0" y2="1"> \
              <stop offset="0%" class="brush-gradient-top" /> \
              <stop offset="100%" class="brush-gradient-bottom" /> \
          </linearGradient> \
          <mask id="brush-mask"> \
              <rect class="mask-background"></rect> \
          </mask>');

        selection.select('.mask-background').attr({
            width: layoutWidth,
            height: navChartHeight
        });

        maskXScale.domain(fc.util.extent().fields('date')(data));
        maskYScale.domain(fc.util.extent().fields(['low', 'high']).pad(yExtentPadding)(data));

        selection.select('mask')
            .datum(data)
            .call(brushMask);
    }

    function nav(selection) {
        var model = selection.datum();

        createDefs(selection, model.data);

        viewScale.domain(model.viewDomain);

        var filteredData = util.domain.filterDataInDateRange(
          fc.util.extent().fields('date')(model.data),
          model.data);
        var yExtent = fc.util.extent()
          .fields(['low', 'high']).pad(yExtentPadding)(filteredData);

        var brushHide = false;

        navChart.xDomain(fc.util.extent().fields('date')(model.data))
          .yDomain(yExtent);

        brush.on('brush', function() {
            var brushExtentIsEmpty = xEmpty(brush);

            // Hide the bar if the extent is empty
            setHide(selection, brushExtentIsEmpty);
            if (!brushExtentIsEmpty) {
                dispatch[event.viewChange]([brush.extent()[0][0], brush.extent()[1][0]]);
            }
        })
            .on('brushend', function() {
                var brushExtentIsEmpty = xEmpty(brush);
                setHide(selection, false);
                if (brushExtentIsEmpty) {
                    dispatch[event.viewChange](util.domain.centerOnDate(viewScale.domain(),
                        model.data, brush.extent()[0][0]));
                }
            });

        navChart.plotArea(navMulti);
        selection.call(navChart);

        // Allow to zoom using mouse, but disable panning
        var zoom = zoomBehavior(layoutWidth)
          .scale(viewScale)
          .trackingLatest(model.trackingLatest)
          .allowPan(false)
          .on('zoom', function(domain) {
              dispatch[event.viewChange](domain);
          });

        selection.select('.plot-area')
          .call(zoom);
    }

    d3.rebind(nav, dispatch, 'on');

    nav.dimensionChanged = function(container) {
        layoutWidth = util.width(container.node());
        viewScale.range([0, layoutWidth]);
        maskXScale.range([0, layoutWidth]);
        maskYScale.range([navChartHeight, 0]);
    };

    return nav;
}
<|endoftext|>


<commit_before>/*global zeit,Backbone,window,document*/
(function() {
    "use strict";

    var $ = window.jQuery;
    var _ = window.Underscore;

    zeit.cms.declare_namespace('zeit.content.image');
    zeit.cms.declare_namespace('zeit.content.image.browser');

    /* MODELS */

    zeit.content.image.Variant = Backbone.Model.extend({
        urlRoot: window.context_url + '/variants',

        make_url: function() {
            var self = this,
                url = self.get('url');
            return url + '?nocache=' + new Date().getTime();
        }
    });

    zeit.content.image.VariantList = Backbone.Collection.extend({

        model: zeit.content.image.Variant,
        url: window.context_url + '/variants'

    });

    zeit.content.image.VARIANTS = new zeit.content.image.VariantList();

    /* VIEWS */

    var AbstractVariant = Backbone.View.extend({

        render: function() {
            var self = this;
            self.image = self.create_image();

            // Replace default DIV element with img as rewire events.
            self.$el.replaceWith(self.image);
            self.setElement(self.image);

            return self;
        },

        create_image: function() {
            var self = this,
                image = $('<img/>');

            // Event handler so Jasmine tests can wait for the image to load.
            image.on('load', function() {
                self.trigger('render');
            });

            // Set src attribute after event registration
            image.attr('src', self.model.make_url());

            return image;
        }
    });

    zeit.content.image.browser.PreviewVariant = AbstractVariant.extend({

        create_image: function() {
            var self = this,
                image = AbstractVariant.prototype.create_image.apply(this);

            image.addClass('preview');

            // Set max-width defined in config as width to display smaller
            // sizes of a variant as big as allowed.
            if (self.model.has('max_size')) {
                var size = self.model.get('max_size').split('x');
                image.width(size[0]);
            }

            // Notify world that image was clicked to make the model active.
            image.on('click', function() {
                self.model.trigger('switch-focus', self.model, self);
            });

            return image;
        },

        update_image: function() {
            this.$el.attr('src', this.model.make_url());
        }
    });

    zeit.content.image.browser.EditableVariant = AbstractVariant.extend({

        create_image: function() {
            var self = this,
                image = AbstractVariant.prototype.create_image.apply(this);

            image.addClass('editor');

            image.cropper({
                aspectRatio: self.model.get('ratio'),
                zoomable: false,
                autoCrop: false,
                rotatable: false,
                movable: false,
                doubleClickToggle: false
            });

            return image;
        }
    });

    zeit.content.image.browser.VariantList = Backbone.View.extend({

        el: '#variant-preview',

        initialize: function() {
            this.listenTo(zeit.content.image.VARIANTS, 'reset', this.reset);
            this.listenTo(zeit.content.image.VARIANTS, 'reload', this.reload);
            this.model_views = [];
        },

        reset: function() {
            // Render all models. Used for initial load.
            var self = this;
            self.$el.empty();
            zeit.content.image.VARIANTS.each(function(variant) {
                var view = new zeit.content.image.browser.PreviewVariant(
                    {model: variant}
                );
                self.model_views.push(view);
                self.$el.append(view.render().el);
            });
        },

        reload: function() {
            // Only update src attribute of images.
            var self = this;
            $(self.model_views).each(function(index, view) {
                view.update_image();
            });
        }

    });

    zeit.content.image.browser.VariantEditor = Backbone.View.extend({

        el: '#variant-inner',

        events: {
            "dragstop .focuspoint": "save"
        },

        initialize: function() {
            var self = this;
            self.default_model = new zeit.content.image.Variant(
                {id: 'default'}
            );
            self.current_model = self.default_model;
            self.model_view = new zeit.content.image.browser.EditableVariant(
                {model: self.current_model}
            );
            self.model_view.img_css_class = 'editor';

            self.model_view.on('render', function() {
                self.trigger('render');
            });

            self.listenTo(zeit.content.image.VARIANTS, 'switch-focus', self.switch_focus);

            $('#reset').on('click', function() {
                self.switch_focus(
                    self.default_model,
                    new zeit.content.image.browser.EditableVariant(
                        {model: self.default_model}
                    )
                );
            });
        },

        prepare: function () {
            var self = this;
            self.default_model.fetch().done(function() {
                self.render();
            });
        },

        render: function() {
            var self = this;
            self.$el.append(self.model_view.render().el);
            self.cropper_info = self.$('img');
            self.cropper_info.on('built.cropper', function() {
                var image = self.$('.cropper-canvas > img').get(0);
                self.width = image.naturalWidth;
                self.height = image.naturalHeight;
                self.ratio = self.width / self.height;
                self.update();
            });
            self.cropper_info.on('dragend.cropper', function() {
                self.save();
            });
        },

        save: function() {
            var self = this,
                data = self.cropper_info.cropper('getData'),
                zoom,
                focus_x,
                focus_y;

            zoom = Math.max(
                data.width / self.width,
                data.height / self.height
            );

            // guard against rounding issues
            if (zoom > 1) {
                zoom = 1;
            }

            // rectangle uses full width, i.e. it does not matter where the
            // focus point is; use 1/2 as default since calculation would fail
            if (self.width - data.width <= 0) {
                focus_x = 1 / 2;
            } else {
                focus_x = data.x / (self.width - data.width);
            }

            if (focus_x > 1) {
                focus_x = 1;
            }

            // rectangle uses full height, i.e. it does not matter where the
            // focus point is; use 1/3 as default since calculation would fail
            if (self.height - data.height <= 0) {
                focus_y = 1 / 3;
            } else {
                focus_y = data.y / (self.height - data.height);
            }

            if (focus_y > 1) {
                focus_y = 1;
            }

            self.current_model.save(
                {"focus_x": focus_x, "focus_y": focus_y, "zoom": zoom}
            ).done(function() {
                self.update();
                zeit.content.image.VARIANTS.trigger('reload');
                self.notify_status("saved");
            });
        },

        update: function() {
            var self = this,
                ratio = self.current_model.get('ratio'),
                zoom = self.current_model.get('zoom'),
                focus_x = self.current_model.get('focus_x'),
                focus_y = self.current_model.get('focus_y'),
                width = self.width,
                height = self.height;

            // adjust width / height according to their ratio,
            // e.g. square has ratio 1, thus width = height
            if (ratio > self.ratio) {
                height = width / ratio;
            } else {
                width = height * ratio;
            }

            // incorporate zoom to have final width / height of rectangle
            height = height * zoom;
            width = width * zoom;

            self.cropper_info.cropper('crop');

            self.cropper_info.cropper(
                'setAspectRatio',
                self.current_model.get('ratio')
            );

            self.cropper_info.cropper('setData', {
                x: focus_x * (self.width - width),
                y: focus_y * (self.height - height),
                width: width,
                height: height,
                rotate: 0
            });
        },

        switch_focus: function(model, view) {
            var self = this;
            self.model_view.$el.removeClass('active');
            self.model_view = view;
            self.model_view.$el.addClass('active');
            self.current_model = model;
            self.current_model.fetch().done(function() {
                self.update();
                self.notify_status("switched");
            });
        },

        notify_status: function(status) {
            // Used for Selenium tests
            var self = this;
            self.$el.addClass(status);
            window.setTimeout(function () {
                self.$el.removeClass(status);
            }, 2000);
        }
    });

    $(document).ready(function() {
        if (!$('#variant-content').length) {
            return;
        }

        new zeit.content.image.browser.VariantList();
        zeit.content.image.VARIANTS.fetch({reset: true}).done(function() {
            var view = new zeit.content.image.browser.VariantEditor();
            view.prepare();
        });

    });

})();
<commit_msg>Escape Model information that is inserted into the DOM.<commit_after>/*global zeit,Backbone,window,document*/
(function() {
    "use strict";

    var $ = window.jQuery;
    var _ = window.Underscore;

    zeit.cms.declare_namespace('zeit.content.image');
    zeit.cms.declare_namespace('zeit.content.image.browser');

    /* MODELS */

    zeit.content.image.Variant = Backbone.Model.extend({
        urlRoot: window.context_url + '/variants',

        make_url: function() {
            var self = this,
                url = self.escape('url');
            return url + '?nocache=' + new Date().getTime();
        }
    });

    zeit.content.image.VariantList = Backbone.Collection.extend({

        model: zeit.content.image.Variant,
        url: window.context_url + '/variants'

    });

    zeit.content.image.VARIANTS = new zeit.content.image.VariantList();

    /* VIEWS */

    var AbstractVariant = Backbone.View.extend({

        render: function() {
            var self = this;
            self.image = self.create_image();

            // Replace default DIV element with img as rewire events.
            self.$el.replaceWith(self.image);
            self.setElement(self.image);

            return self;
        },

        create_image: function() {
            var self = this,
                image = $('<img/>');

            // Event handler so Jasmine tests can wait for the image to load.
            image.on('load', function() {
                self.trigger('render');
            });

            // Set src attribute after event registration
            image.attr('src', self.model.make_url());

            return image;
        }
    });

    zeit.content.image.browser.PreviewVariant = AbstractVariant.extend({

        create_image: function() {
            var self = this,
                image = AbstractVariant.prototype.create_image.apply(this);

            image.addClass('preview');

            // Set max-width defined in config as width to display smaller
            // sizes of a variant as big as allowed.
            if (self.model.has('max_size')) {
                var size = self.model.escape('max_size').split('x');
                image.width(size[0]);
            }

            // Notify world that image was clicked to make the model active.
            image.on('click', function() {
                self.model.trigger('switch-focus', self.model, self);
            });

            return image;
        },

        update_image: function() {
            this.$el.attr('src', this.model.make_url());
        }
    });

    zeit.content.image.browser.EditableVariant = AbstractVariant.extend({

        create_image: function() {
            var self = this,
                image = AbstractVariant.prototype.create_image.apply(this);

            image.addClass('editor');

            image.cropper({
                aspectRatio: self.model.get('ratio'),
                zoomable: false,
                autoCrop: false,
                rotatable: false,
                movable: false,
                doubleClickToggle: false
            });

            return image;
        }
    });

    zeit.content.image.browser.VariantList = Backbone.View.extend({

        el: '#variant-preview',

        initialize: function() {
            this.listenTo(zeit.content.image.VARIANTS, 'reset', this.reset);
            this.listenTo(zeit.content.image.VARIANTS, 'reload', this.reload);
            this.model_views = [];
        },

        reset: function() {
            // Render all models. Used for initial load.
            var self = this;
            self.$el.empty();
            zeit.content.image.VARIANTS.each(function(variant) {
                var view = new zeit.content.image.browser.PreviewVariant(
                    {model: variant}
                );
                self.model_views.push(view);
                self.$el.append(view.render().el);
            });
        },

        reload: function() {
            // Only update src attribute of images.
            var self = this;
            $(self.model_views).each(function(index, view) {
                view.update_image();
            });
        }

    });

    zeit.content.image.browser.VariantEditor = Backbone.View.extend({

        el: '#variant-inner',

        events: {
            "dragstop .focuspoint": "save"
        },

        initialize: function() {
            var self = this;
            self.default_model = new zeit.content.image.Variant(
                {id: 'default'}
            );
            self.current_model = self.default_model;
            self.model_view = new zeit.content.image.browser.EditableVariant(
                {model: self.current_model}
            );
            self.model_view.img_css_class = 'editor';

            self.model_view.on('render', function() {
                self.trigger('render');
            });

            self.listenTo(zeit.content.image.VARIANTS, 'switch-focus', self.switch_focus);

            $('#reset').on('click', function() {
                self.switch_focus(
                    self.default_model,
                    new zeit.content.image.browser.EditableVariant(
                        {model: self.default_model}
                    )
                );
            });
        },

        prepare: function () {
            var self = this;
            self.default_model.fetch().done(function() {
                self.render();
            });
        },

        render: function() {
            var self = this;
            self.$el.append(self.model_view.render().el);
            self.cropper_info = self.$('img');
            self.cropper_info.on('built.cropper', function() {
                var image = self.$('.cropper-canvas > img').get(0);
                self.width = image.naturalWidth;
                self.height = image.naturalHeight;
                self.ratio = self.width / self.height;
                self.update();
            });
            self.cropper_info.on('dragend.cropper', function() {
                self.save();
            });
        },

        save: function() {
            var self = this,
                data = self.cropper_info.cropper('getData'),
                zoom,
                focus_x,
                focus_y;

            zoom = Math.max(
                data.width / self.width,
                data.height / self.height
            );

            // guard against rounding issues
            if (zoom > 1) {
                zoom = 1;
            }

            // rectangle uses full width, i.e. it does not matter where the
            // focus point is; use 1/2 as default since calculation would fail
            if (self.width - data.width <= 0) {
                focus_x = 1 / 2;
            } else {
                focus_x = data.x / (self.width - data.width);
            }

            if (focus_x > 1) {
                focus_x = 1;
            }

            // rectangle uses full height, i.e. it does not matter where the
            // focus point is; use 1/3 as default since calculation would fail
            if (self.height - data.height <= 0) {
                focus_y = 1 / 3;
            } else {
                focus_y = data.y / (self.height - data.height);
            }

            if (focus_y > 1) {
                focus_y = 1;
            }

            self.current_model.save(
                {"focus_x": focus_x, "focus_y": focus_y, "zoom": zoom}
            ).done(function() {
                self.update();
                zeit.content.image.VARIANTS.trigger('reload');
                self.notify_status("saved");
            });
        },

        update: function() {
            var self = this,
                ratio = self.current_model.get('ratio'),
                zoom = self.current_model.get('zoom'),
                focus_x = self.current_model.get('focus_x'),
                focus_y = self.current_model.get('focus_y'),
                width = self.width,
                height = self.height;

            // adjust width / height according to their ratio,
            // e.g. square has ratio 1, thus width = height
            if (ratio > self.ratio) {
                height = width / ratio;
            } else {
                width = height * ratio;
            }

            // incorporate zoom to have final width / height of rectangle
            height = height * zoom;
            width = width * zoom;

            self.cropper_info.cropper('crop');

            self.cropper_info.cropper(
                'setAspectRatio',
                self.current_model.get('ratio')
            );

            self.cropper_info.cropper('setData', {
                x: focus_x * (self.width - width),
                y: focus_y * (self.height - height),
                width: width,
                height: height,
                rotate: 0
            });
        },

        switch_focus: function(model, view) {
            var self = this;
            self.model_view.$el.removeClass('active');
            self.model_view = view;
            self.model_view.$el.addClass('active');
            self.current_model = model;
            self.current_model.fetch().done(function() {
                self.update();
                self.notify_status("switched");
            });
        },

        notify_status: function(status) {
            // Used for Selenium tests
            var self = this;
            self.$el.addClass(status);
            window.setTimeout(function () {
                self.$el.removeClass(status);
            }, 2000);
        }
    });

    $(document).ready(function() {
        if (!$('#variant-content').length) {
            return;
        }

        new zeit.content.image.browser.VariantList();
        zeit.content.image.VARIANTS.fetch({reset: true}).done(function() {
            var view = new zeit.content.image.browser.VariantEditor();
            view.prepare();
        });

    });

})();
<|endoftext|>