<commit_before>"use strict";

var moment = require('moment')

// !! Parameter note: All dates need to be in a form that moment can recognize (ie `moment(<date>)` gives a correct date object)

// Calculates a normalized list giving the value of the equity-variables for a person over the dates they've been working
// N - An object where each key is a date, and each value is the value of the Need value of the company changed to on that date.
// person - an object where the key is a date, and the value is an object describing a change in equity variables for that date.
    // The key <date> needs to be in a form that moment can recognize (ie `moment(<date>)` gives a correct date object)
    // The value is an object with the properties:
        // S - Salary equivalent changed to on that date.
        // W - Work-rate (percentage of full-time) changed to on that date.
        // D - A number of dollars invested on that date.
        // note - Adds a text note about the change.
// toDate - the date to calculate through (inclusive)
// returns a list of objects defining the equity-variables in various date ranges. Each object has the properties:
    // from - The date, as a unix timestamp, of the beginning of the range (inclusive)
    // to - The date, as a unix timestamp, of the end of the date range (exclusive)
    // S - Salary equivalent
    // W - Work-rate (percentage of full-time)
    // N - The company Need variable
    // D - Dollars invested on the 'from' date
exports.normalizePersonalEquity = function(N, person, toDate_) {
    var toDate = moment(toDate_)

    var normalizedPerson = personToArray(person, toDate)
    //console.log(JSON.stringify(normalizedPerson))
    var NArray = NToArray(N, toDate)

    var curIndex = 0
    for(var n=0; n<NArray.length; n++) {
        var NData = NArray[n]

        while(curIndex < normalizedPerson.length) {
            //console.log(NData.from.format('YYYY-MM-DD')+' - '+NData.to.format('YYYY-MM-DD')+'  '+normalizedPerson[curIndex].from.format('YYYY-MM-DD')+' - '+normalizedPerson[curIndex].to.format('YYYY-MM-DD'))
            // N range is before person range
            if( ! NData.to.isAfter(normalizedPerson[curIndex].from)) {
                break; // do nothing, continue on with the next N change

            // N range covers at least part of person range
            } else if(!NData.from.isAfter(normalizedPerson[curIndex].from)) {
                normalizedPerson[curIndex].N = NData.N

                // N range covers all of person range
                if(!NData.to.isBefore(normalizedPerson[curIndex].to)) {
                    curIndex++
                    continue; // there's more for this N to do

                // N range covers part of person range
                } else {
                    var newEntry = copy(normalizedPerson[curIndex])
                    newEntry.D = 0 // don't duplicate the investment counted
                    normalizedPerson.splice(curIndex+1,0,newEntry)
                    normalizedPerson[curIndex].to = normalizedPerson[curIndex+1].from = NData.to
                    curIndex++
                    break;
                }
            } else {
                throw new Error("unexpected: "+JSON.stringify(NData)+" - "+normalizedPerson[curIndex].from+' '+JSON.stringify(person))
            }
        }
    }

    for(var n=0; n<normalizedPerson.length; n++) {
        var dateInfo = normalizedPerson[n]

        dateInfo.from = dateInfo.from.format('YYYY-MM-DD')
        dateInfo.to = dateInfo.to.format('YYYY-MM-DD')
    }

    return normalizedPerson
}

// returns a object containing normalized total equity data in the same form as normalizePersonalEquity
    // note that W values are summed, and Salary equivalent is averaged (mean)
// normalizedPeople - A list of normalized personal equity objects (as returned from normalizePersonalEquity)
exports.total = function(normalizedPeople) {
    var result = []
    normalizedPeople.forEach(function(person, n) {
        if(n===0) result = person
        else result = mergePeople(result, person)
    })
    return result
}

// An algorithm that transforms a normalized equity object (as returned from normalizePersonalEquity) to use to ramp up the new member's
    // rate of earning equity over time.
// This algorithm starts at `startFraction` of base-equity and bumps up by `bump` every `milestoneDays` days until reaching 100%
// `startFraction` and `bump` are numbers between 0 and 1
exports.newMemberTransform = function(normalizedPerson, startFraction, bump, milestoneDays) {
    var currentFraction = startFraction
    var fractionIncreasePerMilestone = bump
    var nextMilestone = milestoneDays
    var totalManDays = 0
    for(var n=0; n<normalizedPerson.length; n++) {
        // totalManDays + manDays*fraction = nextMilestone
        var dateItem = normalizedPerson[n]

        var days = subtractMoments(moment(dateItem.to), moment(dateItem.from))
        var manDays = days * dateItem.W

        if(manDays + totalManDays > nextMilestone) {  // next milestone is hit in the middle of this item
            var daysTilNextMilestone = nextMilestone - totalManDays
            var newDate = moment(dateItem.from).add(daysTilNextMilestone, 'days')

            normalizedPerson.splice(n+1, 0, copy(dateItem))
            dateItem.to = normalizedPerson[n+1].from = newDate.format('YYYY-MM-DD')
            dateItem.S = Math.round(dateItem.S * currentFraction)
            if(dateItem.D !== undefined)
                normalizedPerson[n+1].D = 0

            currentFraction += fractionIncreasePerMilestone

            totalManDays = nextMilestone
            if(currentFraction >= 1) {
                break;
            } else {
                nextMilestone += milestoneDays
            }
        } else if(n+1 === normalizedPerson.length || manDays + totalManDays === nextMilestone) { // this item exactly hits the milestone (or is the last item)
            dateItem.S = Math.round(dateItem.S * currentFraction)
            currentFraction += fractionIncreasePerMilestone

            totalManDays = nextMilestone
            if(currentFraction >= 1) {
                break;
            } else {
                nextMilestone += milestoneDays
            }

        } else {      // next milestone isn't hit in this item
            dateItem.S = Math.round(dateItem.S * currentFraction)
            totalManDays+= manDays
        }
    }

    // continue building

    return normalizedPerson
}

// return the number of shares earned/created over a period of time
// normalizedEquity - A list of normalized personal equity objects (as returned from normalizePersonalEquity)
// from - the date which to start summing from (inclusive)
// to - the date which to sum to (exclusive)
exports.sumShares = function(normalizedEquity, _from, _to) {
    var from = moment(_from)
    var to = moment(_to)

    var totalShares = 0
    for(var n=0; n<normalizedEquity.length; n++) {
        var item = normalizedEquity[n]
        var itemTo = moment(item.to)
        var itemFrom = moment(item.from)

        if(!itemTo.isBefore(from) && !itemFrom.isAfter(to)) { // if its in range
            var includeInvestment = true
            if(itemFrom.isBefore(from)) {
                itemFrom = from
                includeInvestment = false
            } else if(itemTo.isAfter(to)) {
                itemTo = to
            }

            var days = subtractMoments(itemTo, itemFrom)
            totalShares += calculateShares(days, item, includeInvestment)
        }
    }

    return totalShares
}

// return the number of man-hours recorded over a period of time
// normalizedEquity - A list of normalized personal equity objects (as returned from normalizePersonalEquity)
// from - the date which to start summing from (inclusive)
// to - the date which to sum to (exclusive)
exports.sumManHours = function(normalizedEquity, _from, _to) {
    var from = moment(_from)
    var to = moment(_to)

    var totalManHours = 0
    for(var n=0; n<normalizedEquity.length; n++) {
        var item = normalizedEquity[n]
        var itemTo = moment(item.to)
        var itemFrom = moment(item.from)

        if(!itemTo.isBefore(from) && !itemFrom.isAfter(to)) { // if its in range
            var includeInvestment = true
            if(itemFrom.isBefore(from)) {
                itemFrom = from
                includeInvestment = false
            } else if(itemTo.isAfter(to)) {
                itemTo = to
            }

            var days = subtractMoments(itemTo, itemFrom)
            totalManHours += item.W*days
        }
    }

    return totalManHours
}

// returns the shares per dollar for a given value of N
exports.sharesPerDollar = function(N) {
    return N/.6
}

function calculateShares(days, data, includeDollars) {
    if(includeDollars === undefined) includeDollars = true

    var N = data.N
    var S = data.S
    var W = data.W
    if(includeDollars) var D = data.D
    else               var D = 0

    return N*(days*W*S/365 + D/0.6)
}

// merges two people into an average
function mergePeople(a,b) {
    var results = []

    if(moment(b[0].from).isBefore(moment(a[0].from))) {
        var curTime = moment(b[0].from)
    } else {
        var curTime = moment(a[0].from)
    }

    var bn = 0, an=0
    var aDollarsCounted = false, bDollarsCounted = false
    while(bn < b.length && an < a.length) {
        var aFrom = moment(a[an].from), aTo = moment(a[an].to)
        var bFrom = moment(b[bn].from), bTo = moment(b[bn].to)

        // b range is before a's range
        if( ! bTo.isAfter(aFrom)) {
            nonOverlappingItem(b[bn])
            bn++

        // a's range is before b's range
        } else if( ! aTo.isAfter(bFrom)) {
            nonOverlappingItem(a[an])
            an++

        // overlapping
        } else {

            // b starts before a
            if(bFrom.isBefore(aFrom) && curTime.isBefore(aFrom)) {
                overlappingItem(b[bn], aFrom)

            // a starts before b
            } else if(aFrom.isBefore(bFrom) && curTime.isBefore(bFrom)) {
                overlappingItem(a[an], bFrom)

            // start at same time
            } else {

                var D = 0
                if(!aDollarsCounted) {
                    aDollarsCounted = true
                    D += a[an].D
                }
                if(!bDollarsCounted) {
                    bDollarsCounted = true
                    D += b[bn].D
                }

                // b ends before a
                if(bTo.isBefore(aTo)) {
                    var overlapEnd = bTo
                    var increment = 'b'

                // a ends before b
                } else if(aTo.isBefore(bTo)) {
                    var overlapEnd = aTo
                    var increment = 'a'

                // end at same time
                } else {
                    var overlapEnd = aTo
                    var increment = 'both'
                }

                // combine parts that overlap
                var totalItem = {
                    from: curTime.format('YYYY-MM-DD'), to: overlapEnd.format('YYYY-MM-DD'),
                    N: a[an].N,               // should be same for both a and b, so this is arbitrary
                    S: (a[an].S+b[bn].S)/2,   // average
                    W: a[an].W+b[bn].W,   // total
                    D: D,
                }

                results.push(totalItem)
                curTime = overlapEnd

                if(increment === 'a' || increment === 'both') {
                    an++
                    aDollarsCounted = false
                }
                if(increment === 'b' || increment === 'both') {
                    bn++
                    bDollarsCounted = false
                }
            }
        }
    }

    return results

    function nonOverlappingItem(x) {
        var newItem = copy(x)
        newItem.from = curTime.format('YYYY-MM-DD')
        results.push(newItem)
        curTime = moment(x.to)
    }

    function overlappingItem(x, to) {
        var itemBefore = copy(x)
        itemBefore.D = 0 // D is added in elsewhere
        itemBefore.from = curTime.format('YYYY-MM-DD')
        itemBefore.to = to
        results.push(itemBefore)
        curTime = moment(x.to)
    }
}

function NToArray(N, endTime) {
    var result = []
    for(var date in N) {
        result.push({from: moment(date), N: N[date]})
    }

    result = result.sort(dateSortFn)

    for(var n=0; n<result.length; n++) {
        if(n+1<result.length) {
            result[n].to = result[n+1].from  // note: to is non-inclusive
        } else {
            result[n].to = endTime
        }
    }

    return result
}

// turns a persons changes into an array of changes sorted by date
function personToArray(person, endTime) {
    var result = []
    var curValues = {S: 0, W: 0}
    for(var date in person) {
        var changes = person[date]
        result.push({from: moment(date), S: newValue(changes, 'S'), W: newValue(changes, 'W'), D: newValue(changes, 'D')})
    }

    result.sort(dateSortFn)

    for(var n=0; n<result.length; n++) {
        if(n+1<result.length) {
            result[n].to = result[n+1].from  // note: to is non-inclusive
        } else {
            result[n].to = endTime
        }
    }

    return result

    function newValue(changes, property) {
        if(property === 'D')
            return changes.D || 0
        else {
            if(changes[property] !== undefined) {
                var newValue = changes[property]
            } else {
                var newValue = curValues[property]
            }

            curValues[property] = newValue
            return newValue
        }
    }
}

function dateSortFn(a,b) {
    return a.from.unix() - b.from.unix()
}

// shallow copies an object
function copy(y) {
    var result = {}
    for(var x in y) {
        result[x] = y[x]
    }
    return result
}

// returns a-b in days
function subtractMoments(a,b) {
    return a.diff(b, 'days')
    //return moment.duration(a.unix() - b.unix(), 'seconds').as('days')
}
<commit_msg>another minor fix<commit_after>"use strict";

var moment = require('moment')

// !! Parameter note: All dates need to be in a form that moment can recognize (ie `moment(<date>)` gives a correct date object)

// Calculates a normalized list giving the value of the equity-variables for a person over the dates they've been working
// N - An object where each key is a date, and each value is the value of the Need value of the company changed to on that date.
// person - an object where the key is a date, and the value is an object describing a change in equity variables for that date.
    // The key <date> needs to be in a form that moment can recognize (ie `moment(<date>)` gives a correct date object)
    // The value is an object with the properties:
        // S - Salary equivalent changed to on that date.
        // W - Work-rate (percentage of full-time) changed to on that date.
        // D - A number of dollars invested on that date.
        // note - Adds a text note about the change.
// toDate - the date to calculate through (inclusive)
// returns a list of objects defining the equity-variables in various date ranges. Each object has the properties:
    // from - The date, as a unix timestamp, of the beginning of the range (inclusive)
    // to - The date, as a unix timestamp, of the end of the date range (exclusive)
    // S - Salary equivalent
    // W - Work-rate (percentage of full-time)
    // N - The company Need variable
    // D - Dollars invested on the 'from' date
exports.normalizePersonalEquity = function(N, person, toDate_) {
    var toDate = moment(toDate_)

    var normalizedPerson = personToArray(person, toDate)
    //console.log(JSON.stringify(normalizedPerson))
    var NArray = NToArray(N, toDate)

    var curIndex = 0
    for(var n=0; n<NArray.length; n++) {
        var NData = NArray[n]

        while(curIndex < normalizedPerson.length) {
            //console.log(NData.from.format('YYYY-MM-DD')+' - '+NData.to.format('YYYY-MM-DD')+'  '+normalizedPerson[curIndex].from.format('YYYY-MM-DD')+' - '+normalizedPerson[curIndex].to.format('YYYY-MM-DD'))
            // N range is before person range
            if( ! NData.to.isAfter(normalizedPerson[curIndex].from)) {
                break; // do nothing, continue on with the next N change

            // N range covers at least part of person range
            } else if(!NData.from.isAfter(normalizedPerson[curIndex].from)) {
                normalizedPerson[curIndex].N = NData.N

                // N range covers all of person range
                if(!NData.to.isBefore(normalizedPerson[curIndex].to)) {
                    curIndex++
                    continue; // there's more for this N to do

                // N range covers part of person range
                } else {
                    var newEntry = copy(normalizedPerson[curIndex])
                    newEntry.D = 0 // don't duplicate the investment counted
                    normalizedPerson.splice(curIndex+1,0,newEntry)
                    normalizedPerson[curIndex].to = normalizedPerson[curIndex+1].from = NData.to
                    curIndex++
                    break;
                }
            } else {
                throw new Error("unexpected: "+JSON.stringify(NData)+" - "+normalizedPerson[curIndex].from+' '+JSON.stringify(person))
            }
        }
    }

    for(var n=0; n<normalizedPerson.length; n++) {
        var dateInfo = normalizedPerson[n]

        dateInfo.from = dateInfo.from.format('YYYY-MM-DD')
        dateInfo.to = dateInfo.to.format('YYYY-MM-DD')
    }

    return normalizedPerson
}

// returns a object containing normalized total equity data in the same form as normalizePersonalEquity
    // note that W values are summed, and Salary equivalent is averaged (mean)
// normalizedPeople - A list of normalized personal equity objects (as returned from normalizePersonalEquity)
exports.total = function(normalizedPeople) {
    var result = []
    normalizedPeople.forEach(function(person, n) {
        if(n===0) result = person
        else result = mergePeople(result, person)
    })
    return result
}

// An algorithm that transforms a normalized equity object (as returned from normalizePersonalEquity) to use to ramp up the new member's
    // rate of earning equity over time.
// This algorithm starts at `startFraction` of base-equity and bumps up by `bump` every `milestoneDays` days until reaching 100%
// `startFraction` and `bump` are numbers between 0 and 1
exports.newMemberTransform = function(normalizedPerson, startFraction, bump, milestoneDays) {
    var currentFraction = startFraction
    var fractionIncreasePerMilestone = bump
    var nextMilestone = milestoneDays
    var totalManDays = 0
    for(var n=0; n<normalizedPerson.length; n++) {
        // totalManDays + manDays*fraction = nextMilestone
        var dateItem = normalizedPerson[n]

        var days = subtractMoments(moment(dateItem.to), moment(dateItem.from))
        var manDays = days * dateItem.W

        if(manDays + totalManDays > nextMilestone) {  // next milestone is hit in the middle of this item
            var daysTilNextMilestone = nextMilestone - totalManDays
            var newDate = moment(dateItem.from).add(daysTilNextMilestone, 'days')

            normalizedPerson.splice(n+1, 0, copy(dateItem))
            dateItem.to = normalizedPerson[n+1].from = newDate.format('YYYY-MM-DD')
            dateItem.S = Math.round(dateItem.S * currentFraction)
            if(dateItem.D !== undefined)
                normalizedPerson[n+1].D = 0

            currentFraction += fractionIncreasePerMilestone

            totalManDays = nextMilestone
            if(currentFraction >= 1) {
                break;
            } else {
                nextMilestone += milestoneDays
            }
        } else if(n+1 === normalizedPerson.length || manDays + totalManDays === nextMilestone) { // this item exactly hits the milestone (or is the last item)
            dateItem.S = Math.round(dateItem.S * currentFraction)
            currentFraction += fractionIncreasePerMilestone

            totalManDays = nextMilestone
            if(currentFraction >= 1) {
                break;
            } else {
                nextMilestone += milestoneDays
            }

        } else {      // next milestone isn't hit in this item
            dateItem.S = Math.round(dateItem.S * currentFraction)
            totalManDays+= manDays
        }
    }

    // continue building

    return normalizedPerson
}

// return the number of shares earned/created over a period of time
// normalizedEquity - A list of normalized personal equity objects (as returned from normalizePersonalEquity)
// from - the date which to start summing from (inclusive)
// to - the date which to sum to (exclusive)
exports.sumShares = function(normalizedEquity, _from, _to) {
    var from = moment(_from)
    var to = moment(_to)

    var totalShares = 0
    for(var n=0; n<normalizedEquity.length; n++) {
        var item = normalizedEquity[n]
        var itemTo = moment(item.to)
        var itemFrom = moment(item.from)

        if(!itemTo.isBefore(from) && !itemFrom.isAfter(to)) { // if its in range
            var includeInvestment = true
            if(itemFrom.isBefore(from)) {
                itemFrom = from
                includeInvestment = false
            } else if(itemTo.isAfter(to)) {
                itemTo = to
            }

            var days = subtractMoments(itemTo, itemFrom)
            totalShares += calculateShares(days, item, includeInvestment)
        }
    }

    return totalShares
}

// return the number of man-hours recorded over a period of time
// normalizedEquity - A list of normalized personal equity objects (as returned from normalizePersonalEquity)
// from - the date which to start summing from (inclusive)
// to - the date which to sum to (exclusive)
exports.sumManHours = function(normalizedEquity, _from, _to) {
    var from = moment(_from)
    var to = moment(_to)

    var totalManHours = 0
    for(var n=0; n<normalizedEquity.length; n++) {
        var item = normalizedEquity[n]
        var itemTo = moment(item.to)
        var itemFrom = moment(item.from)

        if(!itemTo.isBefore(from) && !itemFrom.isAfter(to)) { // if its in range
            var includeInvestment = true
            if(itemFrom.isBefore(from)) {
                itemFrom = from
                includeInvestment = false
            } else if(itemTo.isAfter(to)) {
                itemTo = to
            }

            var days = subtractMoments(itemTo, itemFrom)
            totalManHours += item.W*days
        }
    }

    return totalManHours
}

// returns the shares per dollar for a given value of N
exports.sharesPerDollar = function(N) {
    return N/.6
}

function calculateShares(days, data, includeDollars) {
    if(includeDollars === undefined) includeDollars = true

    var N = data.N
    var S = data.S
    var W = data.W
    if(includeDollars) var D = data.D
    else               var D = 0

    return N*(days*W*S/365 + D/0.6)
}

// merges two people into an average
function mergePeople(a,b) {
    var results = []

    if(moment(b[0].from).isBefore(moment(a[0].from))) {
        var curTime = moment(b[0].from)
    } else {
        var curTime = moment(a[0].from)
    }

    var bn = 0, an=0
    var aDollarsCounted = false, bDollarsCounted = false
    while(bn < b.length && an < a.length) {
        var aFrom = moment(a[an].from), aTo = moment(a[an].to)
        var bFrom = moment(b[bn].from), bTo = moment(b[bn].to)

        // b range is before a's range
        if( ! bTo.isAfter(aFrom)) {
            nonOverlappingItem(b[bn])
            bn++

        // a's range is before b's range
        } else if( ! aTo.isAfter(bFrom)) {
            nonOverlappingItem(a[an])
            an++

        // overlapping
        } else {

            // b starts before a
            if(bFrom.isBefore(aFrom) && curTime.isBefore(aFrom)) {
                overlappingItem(b[bn], a[an].from)

            // a starts before b
            } else if(aFrom.isBefore(bFrom) && curTime.isBefore(bFrom)) {
                overlappingItem(a[an], b[bn].from)

            // start at same time
            } else {

                var D = 0
                if(!aDollarsCounted) {
                    aDollarsCounted = true
                    D += a[an].D
                }
                if(!bDollarsCounted) {
                    bDollarsCounted = true
                    D += b[bn].D
                }

                // b ends before a
                if(bTo.isBefore(aTo)) {
                    var overlapEnd = bTo
                    var increment = 'b'

                // a ends before b
                } else if(aTo.isBefore(bTo)) {
                    var overlapEnd = aTo
                    var increment = 'a'

                // end at same time
                } else {
                    var overlapEnd = aTo
                    var increment = 'both'
                }

                // combine parts that overlap
                var totalItem = {
                    from: curTime.format('YYYY-MM-DD'), to: overlapEnd.format('YYYY-MM-DD'),
                    N: a[an].N,               // should be same for both a and b, so this is arbitrary
                    S: (a[an].S+b[bn].S)/2,   // average
                    W: a[an].W+b[bn].W,   // total
                    D: D,
                }

                results.push(totalItem)
                curTime = overlapEnd

                if(increment === 'a' || increment === 'both') {
                    an++
                    aDollarsCounted = false
                }
                if(increment === 'b' || increment === 'both') {
                    bn++
                    bDollarsCounted = false
                }
            }
        }
    }

    return results

    function nonOverlappingItem(x) {
        var newItem = copy(x)
        newItem.from = curTime.format('YYYY-MM-DD')
        results.push(newItem)
        curTime = moment(x.to)
    }

    function overlappingItem(x, to) {
        var itemBefore = copy(x)
        itemBefore.D = 0 // D is added in elsewhere
        itemBefore.from = curTime.format('YYYY-MM-DD')
        itemBefore.to = to
        results.push(itemBefore)
        curTime = moment(x.to)
    }
}

function NToArray(N, endTime) {
    var result = []
    for(var date in N) {
        result.push({from: moment(date), N: N[date]})
    }

    result = result.sort(dateSortFn)

    for(var n=0; n<result.length; n++) {
        if(n+1<result.length) {
            result[n].to = result[n+1].from  // note: to is non-inclusive
        } else {
            result[n].to = endTime
        }
    }

    return result
}

// turns a persons changes into an array of changes sorted by date
function personToArray(person, endTime) {
    var result = []
    var curValues = {S: 0, W: 0}
    for(var date in person) {
        var changes = person[date]
        result.push({from: moment(date), S: newValue(changes, 'S'), W: newValue(changes, 'W'), D: newValue(changes, 'D')})
    }

    result.sort(dateSortFn)

    for(var n=0; n<result.length; n++) {
        if(n+1<result.length) {
            result[n].to = result[n+1].from  // note: to is non-inclusive
        } else {
            result[n].to = endTime
        }
    }

    return result

    function newValue(changes, property) {
        if(property === 'D')
            return changes.D || 0
        else {
            if(changes[property] !== undefined) {
                var newValue = changes[property]
            } else {
                var newValue = curValues[property]
            }

            curValues[property] = newValue
            return newValue
        }
    }
}

function dateSortFn(a,b) {
    return a.from.unix() - b.from.unix()
}

// shallow copies an object
function copy(y) {
    var result = {}
    for(var x in y) {
        result[x] = y[x]
    }
    return result
}

// returns a-b in days
function subtractMoments(a,b) {
    return a.diff(b, 'days')
    //return moment.duration(a.unix() - b.unix(), 'seconds').as('days')
}
<|endoftext|>


<commit_before>/**
 * node-appc - Appcelerator Common Library for Node.js
 * Copyright (c) 2009-2013 by Appcelerator, Inc. All Rights Reserved.
 * Licensed under the terms of the Apache Public License
 * Please see the LICENSE included with this distribution for details.
 */

var appc = require('../index'),
	http = require('http'),
	temp = require('temp');

describe('analytics', function () {
	it('namespace exists', function () {
		appc.should.have.property('analytics');
		appc.analytics.should.be.a('object');
	});

	describe('#addEvent()', function () {
		it('should add an analytics event to queue', function () {
			var length = appc.analytics.events.length;
			appc.analytics.addEvent('dummy unit test event', { dummy: 'data' }, 'unit.test');
			(appc.analytics.events.length).should.equal(length + 1);
			appc.analytics.events = [];
		});
	});

	describe('#send()', function () {
		it('should fail to send because missing arguments', function (done) {
			this.timeout(10000);
			this.slow(5000);

			appc.analytics.events = [];

			var server = http.createServer(function (req, res) {
					res.writeHead(200, {'Content-Type': 'text/plain'});
					res.end('Hello World\n');
					cleanup(new Error('analytics sent despite missing arguments'));
				});

			server.listen(1337, '127.0.0.1');

			var childRunning = true,
				successTimer = setTimeout(function () {
					cleanup();
				}, 5000),
				child = appc.analytics.send({
					analyticsUrl: 'http://127.0.0.1:1337'
				});

			function cleanup(err) {
				clearTimeout(successTimer);
				if (childRunning) {
					childRunning = false;
					child.kill();
				}
				server && server.close(function () {
					server = null;
					done(err);
				});
			}

			// check if the child exited abnormally
			child.on('exit', function (code) {
				childRunning = false;
				code && cleanup();
			});
		});

		it('should post ti.start event', function (done) {
			this.timeout(10000);
			this.slow(5000);

			appc.analytics.events = [];

			var tempDir = temp.mkdirSync(),
				server = http.createServer(function (req, res) {
console.log('got connection!');
					if (req.method != 'POST') return cleanup(new Error('expected POST, got ' + req.method));

					var body = '';
					req.on('data', function (chunk) {
						body += chunk.toString();
					});

					req.on('end', function () {
						// verify the body is good
						var b = {};
						body.split(/&(?!amp;)/).map(function (i) {
							return decodeURIComponent(i);
						}).forEach(function (entry) {
							var p = entry.indexOf('=');
							b[entry.substring(0, p)] = entry.substring(p + 1);
						});

						if (!b.type) return cleanup(new Error('analytics event missing type'));
						if (b.type != 'ti.start') return cleanup(new Error('analytics event wrong type; got ' + b.type + ', expected ti.start'));

						res.writeHead(204);
						res.end();
						setTimeout(cleanup, 100);
					});
				});

			server.listen(1337, '127.0.0.1');

			var childRunning = true,
				successTimer = setTimeout(function () {
					cleanup(new Error('analytics timed out'));
				}, 8000),
				child = appc.analytics.send({
					analyticsUrl: 'http://127.0.0.1:1337',
					appId: 'com.appcelerator.node-appc.unit-tests.test-analytics',
					appName: 'Analytics Unit Test',
					appGuid: '12345678_1234_1234_123456789012',
					directory: tempDir,
					version: '1.0.0'
				});

			function cleanup(err) {
				clearTimeout(successTimer);
				if (childRunning) {
					childRunning = false;
					child.kill();
				}
				server && server.close(function () {
					server = null;
					done(err);
				});
			}
		});

		// TODO: test sending multiple events
		// TODO: simulate send while logged out
		// TODO: simulate send while logged in
	});
});<commit_msg>More analytics unit test debugging.<commit_after>/**
 * node-appc - Appcelerator Common Library for Node.js
 * Copyright (c) 2009-2013 by Appcelerator, Inc. All Rights Reserved.
 * Licensed under the terms of the Apache Public License
 * Please see the LICENSE included with this distribution for details.
 */

var appc = require('../index'),
	http = require('http'),
	temp = require('temp');

describe('analytics', function () {
	it('namespace exists', function () {
		appc.should.have.property('analytics');
		appc.analytics.should.be.a('object');
	});

	describe('#addEvent()', function () {
		it('should add an analytics event to queue', function () {
			var length = appc.analytics.events.length;
			appc.analytics.addEvent('dummy unit test event', { dummy: 'data' }, 'unit.test');
			(appc.analytics.events.length).should.equal(length + 1);
			appc.analytics.events = [];
		});
	});

	describe('#send()', function () {
		it('should fail to send because missing arguments', function (done) {
			this.timeout(10000);
			this.slow(8000);

			appc.analytics.events = [];

			var server = http.createServer(function (req, res) {
					res.writeHead(200, {'Content-Type': 'text/plain'});
					res.end('Hello World\n');
					cleanup(new Error('analytics sent despite missing arguments'));
				});

			server.listen(1337, '127.0.0.1');

			var childRunning = true,
				successTimer = setTimeout(function () {
					cleanup();
				}, 5000),
				child = appc.analytics.send({
					analyticsUrl: 'http://127.0.0.1:1337'
				});

			function cleanup(err) {
				clearTimeout(successTimer);
				if (childRunning) {
					childRunning = false;
					child.kill();
				}
				server && server.close(function () {
					server = null;
					done(err);
				});
			}

			// check if the child exited abnormally
			child.on('exit', function (code) {
				childRunning = false;
				code && cleanup();
			});
		});

		it('should post ti.start event', function (done) {
			this.timeout(10000);
			this.slow(5000);

			appc.analytics.events = [];

			var finished = false,
				tempDir = temp.mkdirSync(),
				server = http.createServer(function (req, res) {
console.error('got connection!');
					if (req.method != 'POST') return cleanup(new Error('expected POST, got ' + req.method));

					var body = '';
					req.on('data', function (chunk) {
						body += chunk.toString();
					});

					req.on('end', function () {
						// verify the body is good
						var b = {};
						body.split(/&(?!amp;)/).map(function (i) {
							return decodeURIComponent(i);
						}).forEach(function (entry) {
							var p = entry.indexOf('=');
							b[entry.substring(0, p)] = entry.substring(p + 1);
						});

						if (!b.type) return cleanup(new Error('analytics event missing type'));
						if (b.type != 'ti.start') return cleanup(new Error('analytics event wrong type; got ' + b.type + ', expected ti.start'));

						res.writeHead(204);
						res.end();
						setTimeout(cleanup, 100);
					});
				});

			server.listen(1337, '127.0.0.1');

			var childRunning = true,
				successTimer = setTimeout(function () {
					cleanup(new Error('analytics timed out'));
				}, 8000),
				child = appc.analytics.send({
					analyticsUrl: 'http://127.0.0.1:1337',
					appId: 'com.appcelerator.node-appc.unit-tests.test-analytics',
					appName: 'Analytics Unit Test',
					appGuid: '12345678_1234_1234_123456789012',
					directory: tempDir,
					version: '1.0.0'
				});

			function cleanup(err) {
				if (finished) return;
				finished = true;
				clearTimeout(successTimer);
				if (childRunning) {
					childRunning = false;
					child.kill();
				}
				server && server.close(function () {
					server = null;
					done(err);
				});
			}

			// check if the child exited
			child.on('exit', function (code) {
				childRunning = false;
				if (code) {
					cleanup(new Error('analytics send process exited with ' + code));
				} else if (!finished) {
					cleanup(new Error('analytics sent, but server never received the request'));
				}
			});
		});

		// TODO: test sending multiple events
		// TODO: simulate send while logged out
		// TODO: simulate send while logged in
	});
});<|endoftext|>


<commit_before>$(function() {
  $('#content.search').tabs();
});
<commit_msg>Only show search tabs when the correct class is applied<commit_after>$(function() {
  $('#content.search.js-tabs-content').tabs();
});
<|endoftext|>


<commit_before>// Copyright (c) 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

export default class CoverageListView extends UI.VBox {
  /**
   * @param {function(!Coverage.URLCoverageInfo):boolean} filterCallback
   */
  constructor(filterCallback) {
    super(true);
    /** @type {!Map<!Coverage.URLCoverageInfo, !Coverage.CoverageListView.GridNode>} */
    this._nodeForCoverageInfo = new Map();
    this._filterCallback = filterCallback;
    /** @type {?RegExp} */
    this._highlightRegExp = null;
    this.registerRequiredCSS('coverage/coverageListView.css');
    const columns = [
      {id: 'url', title: Common.UIString('URL'), width: '250px', fixedWidth: false, sortable: true},
      {id: 'type', title: Common.UIString('Type'), width: '45px', fixedWidth: true, sortable: true}, {
        id: 'size',
        title: Common.UIString('Total Bytes'),
        width: '60px',
        fixedWidth: true,
        sortable: true,
        align: DataGrid.DataGrid.Align.Right
      },
      {
        id: 'unusedSize',
        title: Common.UIString('Unused Bytes'),
        width: '100px',
        fixedWidth: true,
        sortable: true,
        align: DataGrid.DataGrid.Align.Right,
        sort: DataGrid.DataGrid.Order.Descending
      },
      {id: 'bars', title: ls`Usage Visualization`, width: '250px', fixedWidth: false, sortable: true}
    ];
    this._dataGrid = new DataGrid.SortableDataGrid({displayName: ls`Code Coverage`, columns});
    this._dataGrid.setResizeMethod(DataGrid.DataGrid.ResizeMethod.Last);
    this._dataGrid.element.classList.add('flex-auto');
    this._dataGrid.element.addEventListener('keydown', this._onKeyDown.bind(this), false);
    this._dataGrid.addEventListener(DataGrid.DataGrid.Events.OpenedNode, this._onOpenedNode, this);
    this._dataGrid.addEventListener(DataGrid.DataGrid.Events.SortingChanged, this._sortingChanged, this);

    const dataGridWidget = this._dataGrid.asWidget();
    dataGridWidget.show(this.contentElement);
  }

  /**
   * @param {!Array<!Coverage.URLCoverageInfo>} coverageInfo
   */
  update(coverageInfo) {
    let hadUpdates = false;
    const maxSize = coverageInfo.reduce((acc, entry) => Math.max(acc, entry.size()), 0);
    const rootNode = this._dataGrid.rootNode();
    for (const entry of coverageInfo) {
      let node = this._nodeForCoverageInfo.get(entry);
      if (node) {
        if (this._filterCallback(node._coverageInfo)) {
          hadUpdates = node._refreshIfNeeded(maxSize) || hadUpdates;
        }
        continue;
      }
      node = new GridNode(entry, maxSize);
      this._nodeForCoverageInfo.set(entry, node);
      if (this._filterCallback(node._coverageInfo)) {
        rootNode.appendChild(node);
        hadUpdates = true;
      }
    }
    if (hadUpdates) {
      this._sortingChanged();
    }
  }

  reset() {
    this._nodeForCoverageInfo.clear();
    this._dataGrid.rootNode().removeChildren();
  }

  /**
   * @param {?RegExp} highlightRegExp
   */
  updateFilterAndHighlight(highlightRegExp) {
    this._highlightRegExp = highlightRegExp;
    let hadTreeUpdates = false;
    for (const node of this._nodeForCoverageInfo.values()) {
      const shouldBeVisible = this._filterCallback(node._coverageInfo);
      const isVisible = !!node.parent;
      if (shouldBeVisible) {
        node._setHighlight(this._highlightRegExp);
      }
      if (shouldBeVisible === isVisible) {
        continue;
      }
      hadTreeUpdates = true;
      if (!shouldBeVisible) {
        node.remove();
      } else {
        this._dataGrid.rootNode().appendChild(node);
      }
    }
    if (hadTreeUpdates) {
      this._sortingChanged();
    }
  }

  selectByUrl(url) {
    for (const [info, node] of this._nodeForCoverageInfo.entries()) {
      if (info.url() === url) {
        node.revealAndSelect();
        break;
      }
    }
  }

  _onOpenedNode() {
    this._revealSourceForSelectedNode();
  }

  /**
   * @param {!Event} event
   */
  _onKeyDown(event) {
    if (!isEnterKey(event)) {
      return;
    }
    event.consume(true);
    this._revealSourceForSelectedNode();
  }

  async _revealSourceForSelectedNode() {
    const node = this._dataGrid.selectedNode;
    if (!node) {
      return;
    }
    const coverageInfo = /** @type {!Coverage.CoverageListView.GridNode} */ (node)._coverageInfo;
    let sourceCode = Workspace.workspace.uiSourceCodeForURL(coverageInfo.url());
    if (!sourceCode) {
      return;
    }

    const formatData = await Formatter.sourceFormatter.format(sourceCode);
    sourceCode = formatData.formattedSourceCode;

    if (this._dataGrid.selectedNode !== node) {
      return;
    }
    Common.Revealer.reveal(sourceCode);
  }

  _sortingChanged() {
    const columnId = this._dataGrid.sortColumnId();
    if (!columnId) {
      return;
    }
    let sortFunction;
    switch (columnId) {
      case 'url':
        sortFunction = compareURL;
        break;
      case 'type':
        sortFunction = compareType;
        break;
      case 'size':
        sortFunction = compareNumericField.bind(null, 'size');
        break;
      case 'bars':
      case 'unusedSize':
        sortFunction = compareNumericField.bind(null, 'unusedSize');
        break;
      default:
        console.assert(false, 'Unknown sort field: ' + columnId);
        return;
    }

    this._dataGrid.sortNodes(sortFunction, !this._dataGrid.isSortOrderAscending());

    /**
     * @param {!DataGrid.DataGridNode} a
     * @param {!DataGrid.DataGridNode} b
     * @return {number}
     */
    function compareURL(a, b) {
      const nodeA = /** @type {!Coverage.CoverageListView.GridNode} */ (a);
      const nodeB = /** @type {!Coverage.CoverageListView.GridNode} */ (b);

      return nodeA._url.localeCompare(nodeB._url);
    }

    /**
     * @param {string} fieldName
     * @param {!DataGrid.DataGridNode} a
     * @param {!DataGrid.DataGridNode} b
     * @return {number}
     */
    function compareNumericField(fieldName, a, b) {
      const nodeA = /** @type {!Coverage.CoverageListView.GridNode} */ (a);
      const nodeB = /** @type {!Coverage.CoverageListView.GridNode} */ (b);

      return nodeA._coverageInfo[fieldName]() - nodeB._coverageInfo[fieldName]() || compareURL(a, b);
    }

    /**
     * @param {!DataGrid.DataGridNode} a
     * @param {!DataGrid.DataGridNode} b
     * @return {number}
     */
    function compareType(a, b) {
      const nodeA = /** @type {!Coverage.CoverageListView.GridNode} */ (a);
      const nodeB = /** @type {!Coverage.CoverageListView.GridNode} */ (b);
      const typeA = Coverage.CoverageListView._typeToString(nodeA._coverageInfo.type());
      const typeB = Coverage.CoverageListView._typeToString(nodeB._coverageInfo.type());
      return typeA.localeCompare(typeB) || compareURL(a, b);
    }
  }

  /**
   * @param {!Coverage.CoverageType} type
   */
  static _typeToString(type) {
    const types = [];
    if (type & Coverage.CoverageType.CSS) {
      types.push(Common.UIString('CSS'));
    }
    if (type & Coverage.CoverageType.JavaScriptPerFunction) {
      types.push(Common.UIString('JS (per function)'));
    } else if (type & Coverage.CoverageType.JavaScript) {
      types.push(Common.UIString('JS (per block)'));
    }
    return types.join('+');
  }
}

export class GridNode extends DataGrid.SortableDataGridNode {
  /**
   * @param {!Coverage.URLCoverageInfo} coverageInfo
   * @param {number} maxSize
   */
  constructor(coverageInfo, maxSize) {
    super();
    this._coverageInfo = coverageInfo;
    /** @type {number|undefined} */
    this._lastUsedSize;
    this._url = coverageInfo.url();
    this._maxSize = maxSize;
    this._highlightDOMChanges = [];
    /** @type {?RegExp} */
    this._highlightRegExp = null;
  }

  /**
   * @param {?RegExp} highlightRegExp
   */
  _setHighlight(highlightRegExp) {
    if (this._highlightRegExp === highlightRegExp) {
      return;
    }
    this._highlightRegExp = highlightRegExp;
    this.refresh();
  }

  /**
   * @param {number} maxSize
   * @return {boolean}
   */
  _refreshIfNeeded(maxSize) {
    if (this._lastUsedSize === this._coverageInfo.usedSize() && maxSize === this._maxSize) {
      return false;
    }
    this._lastUsedSize = this._coverageInfo.usedSize();
    this._maxSize = maxSize;
    this.refresh();
    return true;
  }

  /**
   * @override
   * @param {string} columnId
   * @return {!Element}
   */
  createCell(columnId) {
    const cell = this.createTD(columnId);
    switch (columnId) {
      case 'url':
        cell.title = this._url;
        const outer = cell.createChild('div', 'url-outer');
        const prefix = outer.createChild('div', 'url-prefix');
        const suffix = outer.createChild('div', 'url-suffix');
        const splitURL = /^(.*)(\/[^/]*)$/.exec(this._url);
        prefix.textContent = splitURL ? splitURL[1] : this._url;
        suffix.textContent = splitURL ? splitURL[2] : '';
        if (this._highlightRegExp) {
          this._highlight(outer, this._url);
        }
        break;
      case 'type':
        cell.textContent = Coverage.CoverageListView._typeToString(this._coverageInfo.type());
        if (this._coverageInfo.type() & Coverage.CoverageType.JavaScriptPerFunction) {
          cell.title = ls
          `JS coverage with per function granularity: Once a function was executed, the whole function is marked as covered.`;
        } else if (this._coverageInfo.type() & Coverage.CoverageType.JavaScript) {
          cell.title = ls
          `JS coverage with per block granularity: Once a block of JavaScript was executed, that block is marked as covered.`;
        }
        break;
      case 'size':
        const sizeSpan = cell.createChild('span');
        sizeSpan.textContent = Number.withThousandsSeparator(this._coverageInfo.size() || 0);
        const sizeAccessibleName =
            (this._coverageInfo.size() === 1) ? ls`1 byte` : ls`${this._coverageInfo.size() || 0} bytes`;
        this.setCellAccessibleName(sizeAccessibleName, cell, columnId);
        break;
      case 'unusedSize':
        const unusedSize = this._coverageInfo.unusedSize() || 0;
        const unusedSizeSpan = cell.createChild('span');
        const unusedPercentsSpan = cell.createChild('span', 'percent-value');
        unusedSizeSpan.textContent = Number.withThousandsSeparator(unusedSize);
        const unusedPercentFormatted = ls`${this._percentageString(this._coverageInfo.unusedPercentage())} %`;
        unusedPercentsSpan.textContent = unusedPercentFormatted;
        const unusedAccessibleName = (unusedSize === 1) ? ls`1 byte, ${unusedPercentFormatted}` :
                                                          ls`${unusedSize} bytes, ${unusedPercentFormatted}`;
        this.setCellAccessibleName(unusedAccessibleName, cell, columnId);
        break;
      case 'bars':
        const barContainer = cell.createChild('div', 'bar-container');
        const unusedPercent = this._percentageString(this._coverageInfo.unusedPercentage());
        const usedPercent = this._percentageString(this._coverageInfo.usedPercentage());
        if (this._coverageInfo.unusedSize() > 0) {
          const unusedSizeBar = barContainer.createChild('div', 'bar bar-unused-size');
          unusedSizeBar.style.width = ((this._coverageInfo.unusedSize() / this._maxSize) * 100 || 0) + '%';
          if (this._coverageInfo.type() & Coverage.CoverageType.JavaScriptPerFunction) {
            unusedSizeBar.title = ls`${this._coverageInfo.unusedSize()} bytes (${
                unusedPercent} %) belong to functions that have not (yet) been executed.`;
          } else if (this._coverageInfo.type() & Coverage.CoverageType.JavaScript) {
            unusedSizeBar.title = ls`${this._coverageInfo.unusedSize()} bytes (${
                unusedPercent} %) belong to blocks of JavaScript that have not (yet) been executed.`;
          }
        }
        if (this._coverageInfo.usedSize() > 0) {
          const usedSizeBar = barContainer.createChild('div', 'bar bar-used-size');
          usedSizeBar.style.width = ((this._coverageInfo.usedSize() / this._maxSize) * 100 || 0) + '%';
          if (this._coverageInfo.type() & Coverage.CoverageType.JavaScriptPerFunction) {
            usedSizeBar.title = ls`${this._coverageInfo.usedSize()} bytes (${
                usedPercent} %) belong to functions that have executed at least once.`;
          } else if (this._coverageInfo.type() & Coverage.CoverageType.JavaScript) {
            usedSizeBar.title = ls`${this._coverageInfo.usedSize()} bytes (${
                usedPercent} %) belong to blocks of JavaScript that have executed at least once.`;
          }
        }
        this.setCellAccessibleName(
            ls`${unusedPercent} % of file unused, ${usedPercent} % of file used`, cell, columnId);
    }
    return cell;
  }

  /**
   * @param {number} value
   * @return {string}
   */
  _percentageString(value) {
    return value.toFixed(1);
  }

  /**
   * @param {!Element} element
   * @param {string} textContent
   */
  _highlight(element, textContent) {
    const matches = this._highlightRegExp.exec(textContent);
    if (!matches || !matches.length) {
      return;
    }
    const range = new TextUtils.SourceRange(matches.index, matches[0].length);
    UI.highlightRangesWithStyleClass(element, [range], 'filter-highlight');
  }
}

/* Legacy exported object */
self.Coverage = self.Coverage || {};

/* Legacy exported object */
Coverage = Coverage || {};

/**
 * @constructor
 */
Coverage.CoverageListView = CoverageListView;

/**
 * @constructor
 */
Coverage.CoverageListView.GridNode = GridNode;
<commit_msg>Coverage: Update URL cell in datagrid screen reader label to full url<commit_after>// Copyright (c) 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

export default class CoverageListView extends UI.VBox {
  /**
   * @param {function(!Coverage.URLCoverageInfo):boolean} filterCallback
   */
  constructor(filterCallback) {
    super(true);
    /** @type {!Map<!Coverage.URLCoverageInfo, !Coverage.CoverageListView.GridNode>} */
    this._nodeForCoverageInfo = new Map();
    this._filterCallback = filterCallback;
    /** @type {?RegExp} */
    this._highlightRegExp = null;
    this.registerRequiredCSS('coverage/coverageListView.css');
    const columns = [
      {id: 'url', title: Common.UIString('URL'), width: '250px', fixedWidth: false, sortable: true},
      {id: 'type', title: Common.UIString('Type'), width: '45px', fixedWidth: true, sortable: true}, {
        id: 'size',
        title: Common.UIString('Total Bytes'),
        width: '60px',
        fixedWidth: true,
        sortable: true,
        align: DataGrid.DataGrid.Align.Right
      },
      {
        id: 'unusedSize',
        title: Common.UIString('Unused Bytes'),
        width: '100px',
        fixedWidth: true,
        sortable: true,
        align: DataGrid.DataGrid.Align.Right,
        sort: DataGrid.DataGrid.Order.Descending
      },
      {id: 'bars', title: ls`Usage Visualization`, width: '250px', fixedWidth: false, sortable: true}
    ];
    this._dataGrid = new DataGrid.SortableDataGrid({displayName: ls`Code Coverage`, columns});
    this._dataGrid.setResizeMethod(DataGrid.DataGrid.ResizeMethod.Last);
    this._dataGrid.element.classList.add('flex-auto');
    this._dataGrid.element.addEventListener('keydown', this._onKeyDown.bind(this), false);
    this._dataGrid.addEventListener(DataGrid.DataGrid.Events.OpenedNode, this._onOpenedNode, this);
    this._dataGrid.addEventListener(DataGrid.DataGrid.Events.SortingChanged, this._sortingChanged, this);

    const dataGridWidget = this._dataGrid.asWidget();
    dataGridWidget.show(this.contentElement);
  }

  /**
   * @param {!Array<!Coverage.URLCoverageInfo>} coverageInfo
   */
  update(coverageInfo) {
    let hadUpdates = false;
    const maxSize = coverageInfo.reduce((acc, entry) => Math.max(acc, entry.size()), 0);
    const rootNode = this._dataGrid.rootNode();
    for (const entry of coverageInfo) {
      let node = this._nodeForCoverageInfo.get(entry);
      if (node) {
        if (this._filterCallback(node._coverageInfo)) {
          hadUpdates = node._refreshIfNeeded(maxSize) || hadUpdates;
        }
        continue;
      }
      node = new GridNode(entry, maxSize);
      this._nodeForCoverageInfo.set(entry, node);
      if (this._filterCallback(node._coverageInfo)) {
        rootNode.appendChild(node);
        hadUpdates = true;
      }
    }
    if (hadUpdates) {
      this._sortingChanged();
    }
  }

  reset() {
    this._nodeForCoverageInfo.clear();
    this._dataGrid.rootNode().removeChildren();
  }

  /**
   * @param {?RegExp} highlightRegExp
   */
  updateFilterAndHighlight(highlightRegExp) {
    this._highlightRegExp = highlightRegExp;
    let hadTreeUpdates = false;
    for (const node of this._nodeForCoverageInfo.values()) {
      const shouldBeVisible = this._filterCallback(node._coverageInfo);
      const isVisible = !!node.parent;
      if (shouldBeVisible) {
        node._setHighlight(this._highlightRegExp);
      }
      if (shouldBeVisible === isVisible) {
        continue;
      }
      hadTreeUpdates = true;
      if (!shouldBeVisible) {
        node.remove();
      } else {
        this._dataGrid.rootNode().appendChild(node);
      }
    }
    if (hadTreeUpdates) {
      this._sortingChanged();
    }
  }

  selectByUrl(url) {
    for (const [info, node] of this._nodeForCoverageInfo.entries()) {
      if (info.url() === url) {
        node.revealAndSelect();
        break;
      }
    }
  }

  _onOpenedNode() {
    this._revealSourceForSelectedNode();
  }

  /**
   * @param {!Event} event
   */
  _onKeyDown(event) {
    if (!isEnterKey(event)) {
      return;
    }
    event.consume(true);
    this._revealSourceForSelectedNode();
  }

  async _revealSourceForSelectedNode() {
    const node = this._dataGrid.selectedNode;
    if (!node) {
      return;
    }
    const coverageInfo = /** @type {!Coverage.CoverageListView.GridNode} */ (node)._coverageInfo;
    let sourceCode = Workspace.workspace.uiSourceCodeForURL(coverageInfo.url());
    if (!sourceCode) {
      return;
    }

    const formatData = await Formatter.sourceFormatter.format(sourceCode);
    sourceCode = formatData.formattedSourceCode;

    if (this._dataGrid.selectedNode !== node) {
      return;
    }
    Common.Revealer.reveal(sourceCode);
  }

  _sortingChanged() {
    const columnId = this._dataGrid.sortColumnId();
    if (!columnId) {
      return;
    }
    let sortFunction;
    switch (columnId) {
      case 'url':
        sortFunction = compareURL;
        break;
      case 'type':
        sortFunction = compareType;
        break;
      case 'size':
        sortFunction = compareNumericField.bind(null, 'size');
        break;
      case 'bars':
      case 'unusedSize':
        sortFunction = compareNumericField.bind(null, 'unusedSize');
        break;
      default:
        console.assert(false, 'Unknown sort field: ' + columnId);
        return;
    }

    this._dataGrid.sortNodes(sortFunction, !this._dataGrid.isSortOrderAscending());

    /**
     * @param {!DataGrid.DataGridNode} a
     * @param {!DataGrid.DataGridNode} b
     * @return {number}
     */
    function compareURL(a, b) {
      const nodeA = /** @type {!Coverage.CoverageListView.GridNode} */ (a);
      const nodeB = /** @type {!Coverage.CoverageListView.GridNode} */ (b);

      return nodeA._url.localeCompare(nodeB._url);
    }

    /**
     * @param {string} fieldName
     * @param {!DataGrid.DataGridNode} a
     * @param {!DataGrid.DataGridNode} b
     * @return {number}
     */
    function compareNumericField(fieldName, a, b) {
      const nodeA = /** @type {!Coverage.CoverageListView.GridNode} */ (a);
      const nodeB = /** @type {!Coverage.CoverageListView.GridNode} */ (b);

      return nodeA._coverageInfo[fieldName]() - nodeB._coverageInfo[fieldName]() || compareURL(a, b);
    }

    /**
     * @param {!DataGrid.DataGridNode} a
     * @param {!DataGrid.DataGridNode} b
     * @return {number}
     */
    function compareType(a, b) {
      const nodeA = /** @type {!Coverage.CoverageListView.GridNode} */ (a);
      const nodeB = /** @type {!Coverage.CoverageListView.GridNode} */ (b);
      const typeA = Coverage.CoverageListView._typeToString(nodeA._coverageInfo.type());
      const typeB = Coverage.CoverageListView._typeToString(nodeB._coverageInfo.type());
      return typeA.localeCompare(typeB) || compareURL(a, b);
    }
  }

  /**
   * @param {!Coverage.CoverageType} type
   */
  static _typeToString(type) {
    const types = [];
    if (type & Coverage.CoverageType.CSS) {
      types.push(Common.UIString('CSS'));
    }
    if (type & Coverage.CoverageType.JavaScriptPerFunction) {
      types.push(Common.UIString('JS (per function)'));
    } else if (type & Coverage.CoverageType.JavaScript) {
      types.push(Common.UIString('JS (per block)'));
    }
    return types.join('+');
  }
}

export class GridNode extends DataGrid.SortableDataGridNode {
  /**
   * @param {!Coverage.URLCoverageInfo} coverageInfo
   * @param {number} maxSize
   */
  constructor(coverageInfo, maxSize) {
    super();
    this._coverageInfo = coverageInfo;
    /** @type {number|undefined} */
    this._lastUsedSize;
    this._url = coverageInfo.url();
    this._maxSize = maxSize;
    this._highlightDOMChanges = [];
    /** @type {?RegExp} */
    this._highlightRegExp = null;
  }

  /**
   * @param {?RegExp} highlightRegExp
   */
  _setHighlight(highlightRegExp) {
    if (this._highlightRegExp === highlightRegExp) {
      return;
    }
    this._highlightRegExp = highlightRegExp;
    this.refresh();
  }

  /**
   * @param {number} maxSize
   * @return {boolean}
   */
  _refreshIfNeeded(maxSize) {
    if (this._lastUsedSize === this._coverageInfo.usedSize() && maxSize === this._maxSize) {
      return false;
    }
    this._lastUsedSize = this._coverageInfo.usedSize();
    this._maxSize = maxSize;
    this.refresh();
    return true;
  }

  /**
   * @override
   * @param {string} columnId
   * @return {!Element}
   */
  createCell(columnId) {
    const cell = this.createTD(columnId);
    switch (columnId) {
      case 'url':
        cell.title = this._url;
        const outer = cell.createChild('div', 'url-outer');
        const prefix = outer.createChild('div', 'url-prefix');
        const suffix = outer.createChild('div', 'url-suffix');
        const splitURL = /^(.*)(\/[^/]*)$/.exec(this._url);
        prefix.textContent = splitURL ? splitURL[1] : this._url;
        suffix.textContent = splitURL ? splitURL[2] : '';
        if (this._highlightRegExp) {
          this._highlight(outer, this._url);
        }
        this.setCellAccessibleName(this._url, cell, columnId);
        break;
      case 'type':
        cell.textContent = Coverage.CoverageListView._typeToString(this._coverageInfo.type());
        if (this._coverageInfo.type() & Coverage.CoverageType.JavaScriptPerFunction) {
          cell.title = ls
          `JS coverage with per function granularity: Once a function was executed, the whole function is marked as covered.`;
        } else if (this._coverageInfo.type() & Coverage.CoverageType.JavaScript) {
          cell.title = ls
          `JS coverage with per block granularity: Once a block of JavaScript was executed, that block is marked as covered.`;
        }
        break;
      case 'size':
        const sizeSpan = cell.createChild('span');
        sizeSpan.textContent = Number.withThousandsSeparator(this._coverageInfo.size() || 0);
        const sizeAccessibleName =
            (this._coverageInfo.size() === 1) ? ls`1 byte` : ls`${this._coverageInfo.size() || 0} bytes`;
        this.setCellAccessibleName(sizeAccessibleName, cell, columnId);
        break;
      case 'unusedSize':
        const unusedSize = this._coverageInfo.unusedSize() || 0;
        const unusedSizeSpan = cell.createChild('span');
        const unusedPercentsSpan = cell.createChild('span', 'percent-value');
        unusedSizeSpan.textContent = Number.withThousandsSeparator(unusedSize);
        const unusedPercentFormatted = ls`${this._percentageString(this._coverageInfo.unusedPercentage())} %`;
        unusedPercentsSpan.textContent = unusedPercentFormatted;
        const unusedAccessibleName = (unusedSize === 1) ? ls`1 byte, ${unusedPercentFormatted}` :
                                                          ls`${unusedSize} bytes, ${unusedPercentFormatted}`;
        this.setCellAccessibleName(unusedAccessibleName, cell, columnId);
        break;
      case 'bars':
        const barContainer = cell.createChild('div', 'bar-container');
        const unusedPercent = this._percentageString(this._coverageInfo.unusedPercentage());
        const usedPercent = this._percentageString(this._coverageInfo.usedPercentage());
        if (this._coverageInfo.unusedSize() > 0) {
          const unusedSizeBar = barContainer.createChild('div', 'bar bar-unused-size');
          unusedSizeBar.style.width = ((this._coverageInfo.unusedSize() / this._maxSize) * 100 || 0) + '%';
          if (this._coverageInfo.type() & Coverage.CoverageType.JavaScriptPerFunction) {
            unusedSizeBar.title = ls`${this._coverageInfo.unusedSize()} bytes (${
                unusedPercent} %) belong to functions that have not (yet) been executed.`;
          } else if (this._coverageInfo.type() & Coverage.CoverageType.JavaScript) {
            unusedSizeBar.title = ls`${this._coverageInfo.unusedSize()} bytes (${
                unusedPercent} %) belong to blocks of JavaScript that have not (yet) been executed.`;
          }
        }
        if (this._coverageInfo.usedSize() > 0) {
          const usedSizeBar = barContainer.createChild('div', 'bar bar-used-size');
          usedSizeBar.style.width = ((this._coverageInfo.usedSize() / this._maxSize) * 100 || 0) + '%';
          if (this._coverageInfo.type() & Coverage.CoverageType.JavaScriptPerFunction) {
            usedSizeBar.title = ls`${this._coverageInfo.usedSize()} bytes (${
                usedPercent} %) belong to functions that have executed at least once.`;
          } else if (this._coverageInfo.type() & Coverage.CoverageType.JavaScript) {
            usedSizeBar.title = ls`${this._coverageInfo.usedSize()} bytes (${
                usedPercent} %) belong to blocks of JavaScript that have executed at least once.`;
          }
        }
        this.setCellAccessibleName(
            ls`${unusedPercent} % of file unused, ${usedPercent} % of file used`, cell, columnId);
    }
    return cell;
  }

  /**
   * @param {number} value
   * @return {string}
   */
  _percentageString(value) {
    return value.toFixed(1);
  }

  /**
   * @param {!Element} element
   * @param {string} textContent
   */
  _highlight(element, textContent) {
    const matches = this._highlightRegExp.exec(textContent);
    if (!matches || !matches.length) {
      return;
    }
    const range = new TextUtils.SourceRange(matches.index, matches[0].length);
    UI.highlightRangesWithStyleClass(element, [range], 'filter-highlight');
  }
}

/* Legacy exported object */
self.Coverage = self.Coverage || {};

/* Legacy exported object */
Coverage = Coverage || {};

/**
 * @constructor
 */
Coverage.CoverageListView = CoverageListView;

/**
 * @constructor
 */
Coverage.CoverageListView.GridNode = GridNode;
<|endoftext|>


<commit_before>// Fetch JSON resources using a polyfill for the forthcoming
// window.fetch specification.
//
// Automatically takes care of sending cookies and request headers.
require('es6-promise').polyfill();
require('isomorphic-fetch');

// Default options for GET requests.
var fetchOptions = {
  method: 'GET',
  headers: {
    'Accept': 'application/json'
  },
  credentials: 'include'
}

// Throws a catchable error if the HTTP status code
// is in the error range.
function checkStatus(response) {
  if (response.status >= 200 && response.status < 300) {
    return response;
  } else {
    var error = new Error(response.statusText)
    error.response = response
    throw error;
  }
}

// Fetches a JSON resource via GET.
export function sendGet(uri) {
  return fetch(uri, fetchOptions).then(checkStatus);
}

// Sends a POST with a JSON body.
export function sendPost(uri, data) {
  return fetch(uri, Object.assign({}, fetchOptions, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(data)
  })).then(checkStatus);
}

// Sends a PUT with a JSON body.
export function sendPut(uri, data) {
  return fetch(uri, Object.assign({}, fetchOptions, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(data)
  })).then(checkStatus);
}

// Sends a PATCH with a JSON body.
export function sendPatch(uri, data) {
  return fetch(uri, Object.assign({}, fetchOptions, {
    method: 'PATCH',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(data)
  })).then(checkStatus);
}

// Sends a DELETE request.
export function sendDelete(uri) {
  return fetch(uri, Object.assign({}, fetchOptions, {
    method: 'DELETE'
  })).then(checkStatus);
}
<commit_msg>Refactor internal object assign to use helper function<commit_after>// Fetch JSON resources using a polyfill for the forthcoming
// window.fetch specification.
//
// Automatically takes care of sending cookies and request headers.
require('es6-promise').polyfill();
require('isomorphic-fetch');

// Default options for GET requests.
const fetchOptions = {
  method: 'GET',
  headers: {
    'Accept': 'application/json'
  },
  credentials: 'include'
}

function requestOptions(customOptions) {
  return Object.assign({}, fetchOptions, customOptions);
}

// Throws a catchable error if the HTTP status code
// is in the error range.
function checkStatus(response) {
  if (response.status >= 200 && response.status < 300) {
    return response;
  } else {
    var error = new Error(response.statusText)
    error.response = response
    throw error;
  }
}

// Fetches a JSON resource via GET.
export function sendGet(uri) {
  return fetch(uri, fetchOptions).then(checkStatus);
}

// Sends a POST with a JSON body.
export function sendPost(uri, data) {
  return fetch(uri, requestOptions({
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(data)
  })).then(checkStatus);
}

// Sends a PUT with a JSON body.
export function sendPut(uri, data) {
  return fetch(uri, requestOptions({
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(data)
  })).then(checkStatus);
}

// Sends a PATCH with a JSON body.
export function sendPatch(uri, data) {
  return fetch(uri, requestOptions({
    method: 'PATCH',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(data)
  })).then(checkStatus);
}

// Sends a DELETE request.
export function sendDelete(uri) {
  return fetch(uri, requestOptions({
    method: 'DELETE'
  })).then(checkStatus);
}
<|endoftext|>


<commit_before>/*
 * Copyright [1999-2015] Wellcome Trust Sanger Institute and the EMBL-European Bioinformatics Institute
 * Copyright [2016-2018] EMBL-European Bioinformatics Institute
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

Ensembl.Panel.ModalContent = Ensembl.Panel.LocalContext.extend({
  constructor: function () {
    this.base.apply(this, arguments);

    Ensembl.EventManager.register('modalFormSubmit', this, this.formSubmit);
  },

  init: function () {
    var panel = this;

    this.activeLink = false;

    this.base();

    this.elLk.content = $('.modal_wrapper', this.el);

    $('a', this.elLk.links).on('click', function () {
      if (!$(this).hasClass('disabled')) {
        var link = $(this).parent();

        if (!link.hasClass('active')) {
          panel.elLk.links.removeClass('active');
          panel.getContent(link.addClass('active'), this.href);
        }

        link = null;
      }

      return false;
    });

    this.elLk.links.on('click', function (e) {
      e.stopPropagation();
      return $(this).children('a').trigger('click');
    });

    this.elLk.content.on('click', 'a.delete_bookmark', function () {
      Ensembl.EventManager.trigger('deleteBookmark', this.href.match(/id=(\d+)\b/)[1]);
    }).on('click', 'form div.select_all input', function () {
      $(this).parents('fieldset').find('input[type=checkbox]').prop('checked', this.checked);
    }).on('click', 'form.wizard input.back', function () {
      $(this).parents('form.wizard').append('<input type="hidden" name="wizard_back" value="1" />').submit();
    });

    this.initialize();
    this.initFromHash();
  },

  initialize: function () {
    this.addSubPanel();
    this.setSelectAll();

    this.elLk.dataTable = $('table.data_table', this.el);

    if (this.elLk.dataTable.length) {
      if (!this.dataTableInit) {
        $.extend(this, Ensembl.DataTable);
      }

      this.dataTableInit();
      this.el.togglewrap();
    }

    Ensembl.EventManager.trigger('validateForms', this.el);

    this.el.find('._ht').helptip();
    this.el.find('._stt').selectToToggle({}, this.el);
    this.el.find('._sdd').speciesDropdown();
  },

  initFromHash: function() {
    if (this.params.hash) {
      this.elLk.links.filter(':has(a.' + this.params.hash + ')').trigger('click');
    }
  },

  getContent: function (link, url) {
    this.elLk.content.html('

Loading Content

');

    var params = Ensembl.prepareRequestParams(url);

    $.ajax({
      url: params.requestURL,
      type: params.requestType,
      data: params.requestData,
      dataType: 'json',
      context: this,
      success: function (json) {
        if (json.redirectURL && json.redirectType === 'modal') {
          return this.getContent(link, json.redirectURL);
        } else if (json.redirectType === 'page') {
          return Ensembl.redirect(json.redirectURL);
        } else if (json.redirectType === 'download') {
          Ensembl.EventManager.trigger('modalClose');
          window.location.href = json.redirectURL;
          return;
        }

        // Avoid race conditions if the user has clicked another nav link while waiting for content to load
        if (typeof link === 'undefined' || link.hasClass('active')) {
          this.updateContent(json);
        }
      },
      error: function (e) {
        if (e.status !== 0) {
          this.displayErrorMessage(e.responseText);
        }
      }
    });
  },

  formSubmit: function (form, data) {
    data = data || form.serialize();

    $.ajax({
      url: form.attr('action'),
      type: form.attr('method'),
      data: data,
      dataType: 'json',
      context: this,
      form: form,
      success: function (json) {
        if (json.redirectURL && json.redirectType === 'modal') {
          return json.modalTab ? Ensembl.EventManager.trigger('modalOpen', { href: json.redirectURL, rel: json.modalTab }) : this.getContent(undefined, json.redirectURL);
        }

        if (json.redirectType === 'download') {
          Ensembl.EventManager.trigger('modalClose');
          window.location.href = json.redirectURL; // not triggering reloadPage here as reloadPage will call destructor on existing panels
        } else if (json.success === true || json.redirectType === 'page') {
          Ensembl.EventManager.trigger('reloadPage', false, json.redirectType === 'page' ? json.redirectURL : false);
        } else if (this.el.is(':visible')) {
          this.updateContent(json);
        }
      },
      error: function (e) {
        if (e.status !== 0) {
          this.displayErrorMessage(e.responseText);
        }
      }
    });

    this.elLk.content.html('

Loading Content

');

    return false;
  },

  updateContent: function (json) {
    var panel = this;

    if (json.wrapper) {
      this.elLk.content.wrapInner(json.wrapper);
    }

    this.elLk.content.html(json.content);

    if ($('.modal_reload', this.el).length) {
      Ensembl.EventManager.trigger('queuePageReload', '', false, false, $('.modal_reload', this.el).attr('href'));
    }

    if ($('.export_buttons_preview', this.el).length) {
      $('.export_buttons_preview', this.el).on('click', function() {
        var action = $('input[name="' + this.name + '"]:hidden', panel.el);
        if (this.name === 'uncompressed') {
          window.location = $(action).val();
          Ensembl.EventManager.trigger('modalClose');
          return;
        }
        else if (this.name === 'gz') {
          $('form#export', this.el).attr('action', $(action).val())
            .find('input[name="compression"]').val('gz');
        }
        $('form#export', this.el).submit();
      });
    }
    this.initialize();
  },

  addSubPanel: function () {
    var panel  = this;
    var params = [];

    $('.ajax', this.elLk.content).each(function () {
      params.push([ $(this).parents('.js_panel')[0].id, 'Content' ]);
    });

    $('.js_panel', this.elLk.content).each(function () {
      var panelType = $('input.subpanel_type', this).val();

      if (panelType && !(panel instanceof Ensembl.Panel[panelType])) {
        params.push([ this.id, panelType ]);
      }
    });

    for (var i in params) {
      Ensembl.EventManager.trigger('destroyPanel', params[i][0], 'empty');
      Ensembl.EventManager.trigger('createPanel',  params[i][0], params[i][1]);
    }
  },

  setSelectAll: function () {
    $('form div.select_all input', this.elLk.content).prop('checked', function () {
      return $(this).parents('fieldset').find('input[type=checkbox]:not(:checked)').length - 1 <= 0; // -1 for the select_all checkbox itself
    });
    this.elLk.content.find('input._selectall').on('change', function() {
      $(this).parents('div._selectall').find('input[type=checkbox]').prop('checked', this.checked);
    });
  },

  displayErrorMessage: function (message) {
    this.elLk.content.html('

Ajax error

Sorry, the page request failed to load.

').find('pre').text(message || '');
  }
});
<commit_msg>Revert "Remove iframe param - ENSWEB-5575"<commit_after>/*
 * Copyright [1999-2015] Wellcome Trust Sanger Institute and the EMBL-European Bioinformatics Institute
 * Copyright [2016-2018] EMBL-European Bioinformatics Institute
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

Ensembl.Panel.ModalContent = Ensembl.Panel.LocalContext.extend({
  constructor: function () {
    this.base.apply(this, arguments);

    Ensembl.EventManager.register('modalFormSubmit', this, this.formSubmit);
  },

  init: function () {
    var panel = this;

    this.activeLink = false;

    this.base();

    this.elLk.content = $('.modal_wrapper', this.el);

    $('a', this.elLk.links).on('click', function () {
      if (!$(this).hasClass('disabled')) {
        var link = $(this).parent();

        if (!link.hasClass('active')) {
          panel.elLk.links.removeClass('active');
          panel.getContent(link.addClass('active'), this.href);
        }

        link = null;
      }

      return false;
    });

    this.elLk.links.on('click', function (e) {
      e.stopPropagation();
      return $(this).children('a').trigger('click');
    });

    this.elLk.content.on('click', 'a.delete_bookmark', function () {
      Ensembl.EventManager.trigger('deleteBookmark', this.href.match(/id=(\d+)\b/)[1]);
    }).on('click', 'form div.select_all input', function () {
      $(this).parents('fieldset').find('input[type=checkbox]').prop('checked', this.checked);
    }).on('click', 'form.wizard input.back', function () {
      $(this).parents('form.wizard').append('<input type="hidden" name="wizard_back" value="1" />').submit();
    });

    this.initialize();
    this.initFromHash();
  },

  initialize: function () {
    this.addSubPanel();
    this.setSelectAll();

    this.elLk.dataTable = $('table.data_table', this.el);

    if (this.elLk.dataTable.length) {
      if (!this.dataTableInit) {
        $.extend(this, Ensembl.DataTable);
      }

      this.dataTableInit();
      this.el.togglewrap();
    }

    Ensembl.EventManager.trigger('validateForms', this.el);

    this.el.find('._ht').helptip();
    this.el.find('._stt').selectToToggle({}, this.el);
    this.el.find('._sdd').speciesDropdown();
  },

  initFromHash: function() {
    if (this.params.hash) {
      this.elLk.links.filter(':has(a.' + this.params.hash + ')').trigger('click');
    }
  },

  getContent: function (link, url) {
    this.elLk.content.html('

Loading Content

');

    var params = Ensembl.prepareRequestParams(url);

    $.ajax({
      url: params.requestURL,
      type: params.requestType,
      data: params.requestData,
      dataType: 'json',
      context: this,
      success: function (json) {
        if (json.redirectURL && json.redirectType === 'modal') {
          return this.getContent(link, json.redirectURL);
        } else if (json.redirectType === 'page') {
          return Ensembl.redirect(json.redirectURL);
        } else if (json.redirectType === 'download') {
          Ensembl.EventManager.trigger('modalClose');
          window.location.href = json.redirectURL;
          return;
        }

        // Avoid race conditions if the user has clicked another nav link while waiting for content to load
        if (typeof link === 'undefined' || link.hasClass('active')) {
          this.updateContent(json);
        }
      },
      error: function (e) {
        if (e.status !== 0) {
          this.displayErrorMessage(e.responseText);
        }
      }
    });
  },

  formSubmit: function (form, data) {
    data = data || form.serialize();

    $.ajax({
      url: form.attr('action'),
      type: form.attr('method'),
      data: data,
      dataType: 'json',
      context: this,
      iframe: !!form.find('input[type=file]').length,
      form: form,
      success: function (json) {
        if (json.redirectURL && json.redirectType === 'modal') {
          return json.modalTab ? Ensembl.EventManager.trigger('modalOpen', { href: json.redirectURL, rel: json.modalTab }) : this.getContent(undefined, json.redirectURL);
        }

        if (json.redirectType === 'download') {
          Ensembl.EventManager.trigger('modalClose');
          window.location.href = json.redirectURL; // not triggering reloadPage here as reloadPage will call destructor on existing panels
        } else if (json.success === true || json.redirectType === 'page') {
          Ensembl.EventManager.trigger('reloadPage', false, json.redirectType === 'page' ? json.redirectURL : false);
        } else if (this.el.is(':visible')) {
          this.updateContent(json);
        }
      },
      error: function (e) {
        if (e.status !== 0) {
          this.displayErrorMessage(e.responseText);
        }
      }
    });

    this.elLk.content.html('

Loading Content

');

    return false;
  },

  updateContent: function (json) {
    var panel = this;

    if (json.wrapper) {
      this.elLk.content.wrapInner(json.wrapper);
    }

    this.elLk.content.html(json.content);

    if ($('.modal_reload', this.el).length) {
      Ensembl.EventManager.trigger('queuePageReload', '', false, false, $('.modal_reload', this.el).attr('href'));
    }

    if ($('.export_buttons_preview', this.el).length) {
      $('.export_buttons_preview', this.el).on('click', function() {
        var action = $('input[name="' + this.name + '"]:hidden', panel.el);
        if (this.name === 'uncompressed') {
          window.location = $(action).val();
          Ensembl.EventManager.trigger('modalClose');
          return;
        }
        else if (this.name === 'gz') {
          $('form#export', this.el).attr('action', $(action).val())
            .find('input[name="compression"]').val('gz');
        }
        $('form#export', this.el).submit();
      });
    }
    this.initialize();
  },

  addSubPanel: function () {
    var panel  = this;
    var params = [];

    $('.ajax', this.elLk.content).each(function () {
      params.push([ $(this).parents('.js_panel')[0].id, 'Content' ]);
    });

    $('.js_panel', this.elLk.content).each(function () {
      var panelType = $('input.subpanel_type', this).val();

      if (panelType && !(panel instanceof Ensembl.Panel[panelType])) {
        params.push([ this.id, panelType ]);
      }
    });

    for (var i in params) {
      Ensembl.EventManager.trigger('destroyPanel', params[i][0], 'empty');
      Ensembl.EventManager.trigger('createPanel',  params[i][0], params[i][1]);
    }
  },

  setSelectAll: function () {
    $('form div.select_all input', this.elLk.content).prop('checked', function () {
      return $(this).parents('fieldset').find('input[type=checkbox]:not(:checked)').length - 1 <= 0; // -1 for the select_all checkbox itself
    });
    this.elLk.content.find('input._selectall').on('change', function() {
      $(this).parents('div._selectall').find('input[type=checkbox]').prop('checked', this.checked);
    });
  },

  displayErrorMessage: function (message) {
    this.elLk.content.html('

Ajax error

Sorry, the page request failed to load.

').find('pre').text(message || '');
  }
});
<|endoftext|>