<commit_before>var _ = require('underscore'),
    urlTools = require('../util/urlTools'),
    cssom = require('cssom-papandreou'),
    uglifyJs = require('uglify-js'),
    AssetGraph = require('../AssetGraph'),
    bundleStrategyByName = {};

function getDiscriminatorForRelation(relation) {
    var discriminatorFragments = [];
    if (relation.conditionalComments) {
        Array.prototype.push.apply(discriminatorFragments, _.pluck(relation.conditionalComments, 'nodeValue'));
    }
    if (relation.type === 'HtmlStyle') {
        discriminatorFragments.push(relation.node.getAttribute('media') || 'all');
    }
    return discriminatorFragments.join(':');
}

// Internal helper function. Reuses the parse trees of existing assets, so be careful!
function makeBundle(assetGraph, assetsToBundle) {
    if (assetsToBundle.length === 0) {
        throw new Error('makeBundle: Bundle must contain at least one asset');
    } else if (assetsToBundle.length === 1) {
        // Shortcut
        return [assetsToBundle[0]];
    }

    var type = assetsToBundle[0].type,
        incomingType = {Css: 'HtmlStyle', JavaScript: 'HtmlScript'}[type],
        parseTrees = _.pluck(assetsToBundle, 'parseTree'),
        constructorOptions = {};

    if (type === 'JavaScript') {
        var topLevelStatements = [];

        constructorOptions.parseTree = new uglifyJs.AST_Toplevel({body: []});
        assetsToBundle.forEach(function (asset) {
            assetGraph.findRelations({from: asset, type: 'JavaScriptInclude'}, true).forEach(function (relation) {
                if (relation.parentNode === asset.parseTree.body) {
                    // This INCLUDE relation is a top level statement, update its .parentNode property to point at
                    // the top level statements array of the bundle:
                    relation.parentNode = constructorOptions.parseTree;
                }
            });
            Array.prototype.push.apply(constructorOptions.parseTree.body, asset.parseTree.body);
        });
    } else {
        // type === 'Css'
        constructorOptions.parseTree = new cssom.CSSStyleSheet();
        // Make sure that all @import rules go at the top of the bundle:
        var importRules = [];
        assetsToBundle.forEach(function (asset) {
            var cssRules = asset.parseTree.cssRules;
            asset.parseTree.parentStyleSheet = constructorOptions.parseTree;
            for (var i = 0 ; i < cssRules.length ; i += 1) {
                if (cssRules[i].type === cssom.CSSRule.IMPORT_RULE) {
                    importRules.push(cssRules[i]);
                    cssRules.splice(i, 1);
                    i -= 1;
                }
            }
            Array.prototype.push.apply(constructorOptions.parseTree.cssRules, cssRules);
        });
        if (importRules.length > 0) {
            Array.prototype.unshift.apply(constructorOptions.parseTree.cssRules, importRules);
        }
    }

    var bundleAsset = new AssetGraph[type](constructorOptions);

    bundleAsset.url = urlTools.resolveUrl(assetGraph.root, bundleAsset.id + bundleAsset.extension);
    bundleAsset._outgoingRelations = assetGraph.findRelations({from: assetsToBundle});
    bundleAsset._outgoingRelations.forEach(function (outgoingRelation) {
        outgoingRelation.remove();
        outgoingRelation.from = bundleAsset;
    });

    var seenReferringAssets = {},
        incomingRelations = assetGraph.findRelations({type: incomingType, to: assetsToBundle}),
        // Point at the bundled asset with a root-relative href if at least one of the relations
        // being bundled have a more specific hrefType than 'relative':
        bundleRelationHrefType = incomingRelations.some(function (incomingRelation) {
            return incomingRelation.hrefType !== 'relative';
        }) ? 'rootRelative' : 'relative';

    incomingRelations.forEach(function (incomingRelation) {
        if (!(incomingRelation.from.id in seenReferringAssets)) {
            var bundleRelation = new AssetGraph[incomingType]({
                hrefType: bundleRelationHrefType,
                to: bundleAsset
            });
            bundleRelation.attach(incomingRelation.from, 'before', incomingRelation);
            if (incomingType === 'HtmlStyle') {
                var media = incomingRelation.node.getAttribute('media');
                if (media && media !== 'all') {
                    bundleRelation.node.setAttribute('media', media);
                    bundleRelation.from.markDirty();
                }
            } else if (incomingType === 'HtmlScript') {
                for (var i = 0 ; i < incomingRelations.length ; i += 1) {
                    var otherIncomingRelation = incomingRelations[i];
                    if (otherIncomingRelation.assetGraph && otherIncomingRelation.node.hasAttribute('data-main')) {
                        var existingHtmlRequireJsMainRelations = assetGraph.findRelations({
                            from: otherIncomingRelation.from,
                            type: 'HtmlRequireJsMain'
                        }).filter(function (relation) {
                            return relation.node === otherIncomingRelation.node;
                        });
                        if (existingHtmlRequireJsMainRelations.length === 1) {
                            var existingHtmlRequireJsMainRelation = existingHtmlRequireJsMainRelations[0];
                            bundleRelation.node.setAttribute('data-main', otherIncomingRelation.node.getAttribute('data-main'));
                            existingHtmlRequireJsMainRelation.node = bundleRelation.node;
                            break;
                        } else {
                            throw new Error("transforms.bundleRelations: Unexpected number of existing HtmlRequireJsMain relations found");
                        }
                    }
                }
            }
            seenReferringAssets[incomingRelation.from.id] = true;
        }
        incomingRelation.detach();
    });

    assetGraph.addAsset(bundleAsset);

    assetsToBundle.forEach(function (asset) {
        if (assetGraph.findRelations({to: asset}).length === 0) {
            assetGraph.removeAsset(asset);
        }
    });
    return bundleAsset;
}

// Quick and dirty bundling strategy that gets you down to one <script> and one <link rel='stylesheet'>
// per document, but doesn't do any cross-page optimization.
bundleStrategyByName.oneBundlePerIncludingAsset = function (assetGraph, queryObj) {
    var assetsToBundleById = {},
        seenIncludingAssets = {},
        bundleAssets = [],
        relationsByIncludingAsset = {};

    assetGraph.findRelations(queryObj).forEach(function (relation) {
        assetsToBundleById[relation.to.id] = relation.to; // Means not in a bundle yet
        (relationsByIncludingAsset[relation.from.id] = relationsByIncludingAsset[relation.from.id] || []).push(relation);
    });

    Object.keys(relationsByIncludingAsset).forEach(function (includingAssetId) {
        var includingAsset = assetGraph.idIndex[includingAssetId],
            relationsToBundle = relationsByIncludingAsset[includingAssetId];

        _.unique(_.pluck(relationsToBundle, 'type')).forEach(function (relationType) {
            var currentBundle = [],
                bundleDiscriminator,
                relationsOfTypeToBundle = relationsToBundle.filter(function (relation) {
                    return relation.type === relationType;
                });

            function flushBundle() {
                if (currentBundle.length > 0) {
                    bundleAssets.push(makeBundle(assetGraph, currentBundle)); // FIXME
                    currentBundle = [];
                }
            }
            assetGraph.findRelations(assetGraph.constructor.query.or({type: relationType}, {type: 'HtmlConditionalComment'})).forEach(function (outgoingRelation) {
                if (outgoingRelation.type === 'HtmlConditionalComment') {
                    if (assetGraph.findRelations({from: outgoingRelation.to, type: relationType}, true).length > 0) {
                        flushBundle();
                    }
                } else if (relationsOfTypeToBundle.indexOf(outgoingRelation) !== -1) {
                    // Make sure that we don't bundle HtmlStyles with different media attributes together etc.:
                    var discriminator = getDiscriminatorForRelation(outgoingRelation);
                    if (bundleDiscriminator && discriminator !== bundleDiscriminator) {
                        flushBundle();
                    }
                    bundleDiscriminator = discriminator;
                    if (assetGraph.findRelations({to: outgoingRelation.to}).length > 1) {
                        currentBundle.push(outgoingRelation.to.clone(outgoingRelation));
                    } else {
                        currentBundle.push(outgoingRelation.to);
                    }
                } else {
                    flushBundle();
                }
            });
            flushBundle();
        });
    });
    return bundleAssets;
};

// Cross-page optimizing bundling strategy that never puts the same chunk in multiple bundles, but still tries
// to create as few bundles as possible. Also preserves inclusion order.
// FIXME: This bundling strategy is still quite buggy, please don't use it yet.
bundleStrategyByName.sharedBundles = function (assetGraph, queryObj) {
    var assetIndex = {},
        seenIncludingAssets = {},
        bundles = [],
        relationsByIncludingAsset = {};

    assetGraph.findRelations(queryObj).forEach(function (relation) {
        assetIndex[relation.to.id] = null; // Means not in a bundle yet
        seenIncludingAssets[relation.from.id] = relation.from;
        (relationsByIncludingAsset[relation.from.id] = relationsByIncludingAsset[relation.from.id] || []).push(relation);
    });

    function splitBundle(bundle, index) {
        var newBundle = bundle.splice(index);
        newBundle.forEach(function (asset) {
            assetIndex[asset.id] = newBundle;
        });
        if (newBundle.length > 0) {
            bundles.push(newBundle);
        }
        return newBundle;
    }

    _.values(seenIncludingAssets).forEach(function (includingAsset) {
        var relationsToBundle = relationsByIncludingAsset[includingAsset.id];

        _.unique(_.pluck(relationsToBundle, 'type')).forEach(function (relationType) {
            var outgoingRelations = assetGraph.findRelations({from: includingAsset, type: [relationType, 'HtmlConditionalComment']}, true), // includeUnresolved
                previousBundle,
                bundleDiscriminator,
                canAppendToPreviousBundle = false,
                previousBundleIndex;

            outgoingRelations.forEach(function (outgoingRelation) {
                if (outgoingRelation.type === 'HtmlConditionalComment') {
                    if (assetGraph.findRelations({from: outgoingRelation.to, type: relationType}).length > 0) {
                        canAppendToPreviousBundle = false;
                    }
                    return;
                }

                // Make sure that we don't bundle HtmlStyles with different media attributes together etc.:
                var discriminator = getDiscriminatorForRelation(outgoingRelation);
                if (bundleDiscriminator && discriminator !== bundleDiscriminator) {
                    canAppendToPreviousBundle = false;
                }
                bundleDiscriminator = discriminator;

                var existingBundle = assetIndex[outgoingRelation.to.id];
                if (existingBundle === null) {
                    // Not bundled yet, append to previousBundle if possible, else create a new one
                    if (canAppendToPreviousBundle) {
                        previousBundle.push(outgoingRelation.to);
                        previousBundleIndex = previousBundle.length - 1;
                    } else {
                        if (previousBundle && previousBundleIndex !== previousBundle.length - 1) {
                            splitBundle(previousBundle, previousBundleIndex + 1);
                        }
                        previousBundle = [outgoingRelation.to];
                        previousBundleIndex = 0;
                        bundles.push(previousBundle);
                        canAppendToPreviousBundle = true;
                    }
                    assetIndex[outgoingRelation.to.id] = previousBundle;
                } else if (existingBundle) {
                    // Already in another bundle
                    canAppendToPreviousBundle = false;
                    var indexInExistingBundle = existingBundle.indexOf(outgoingRelation.to);
                    if (previousBundle && existingBundle === previousBundle) {
                        if (indexInExistingBundle === previousBundleIndex + 1) {
                            previousBundleIndex = indexInExistingBundle;
                        } else {
                            splitBundle(previousBundle, indexInExistingBundle + 1);
                            existingBundle = assetIndex[outgoingRelation.to.id];
                            indexInExistingBundle = existingBundle.indexOf(outgoingRelation.to);
                            if (indexInExistingBundle !== 0) {
                                existingBundle = splitBundle(existingBundle, indexInExistingBundle);
                            }
                            previousBundle = existingBundle;
                            previousBundleIndex = 0;
                        }
                    } else {
                        if (previousBundle && previousBundleIndex !== (previousBundle.length - 1)) {
                            splitBundle(previousBundle, previousBundleIndex + 1);
                        }
                        if (indexInExistingBundle !== 0) {
                            existingBundle = splitBundle(existingBundle, indexInExistingBundle);
                        }
                        previousBundle = existingBundle;
                        previousBundleIndex = 0;
                    }
                } else {
                    // The relation doesn't point at an asset matched by queryObj
                    previousBundle = null;
                    canAppendToPreviousBundle = false;
                }
            });
            // No more outgoing relations for this asset, make sure that the asset that was bundled
            // last is at the last position in its bundle:
            if (previousBundle && previousBundleIndex !== previousBundle.length - 1) {
                splitBundle(previousBundle, previousBundleIndex + 1);
            }
        });
    });

    return bundles.map(function (bundle) {
        return makeBundle(assetGraph, bundle);
    });
};

module.exports = function (queryObj, bundleStrategyName) {
    if (!bundleStrategyName) {
        bundleStrategyName = 'oneBundlePerIncludingAsset';
    } else if (!(bundleStrategyName in bundleStrategyByName)) {
        throw new Error("transforms.bundleRelations: Unknown bundle strategy: " + bundleStrategyName);
    }

    return function bundleRelations(assetGraph) {
        var bundleAssets = bundleStrategyByName[bundleStrategyName](assetGraph, queryObj);
        assetGraph.recomputeBaseAssets();
        bundleAssets.forEach(function (bundleAsset) {
            assetGraph.findRelations({to: bundleAsset}).forEach(function (incomingRelation) {
                incomingRelation.refreshHref();
            });
            assetGraph.findRelations({from: bundleAsset}).forEach(function (outgoingRelation) {
                outgoingRelation.refreshHref();
            });
        });
    };
};
<commit_msg>Fixed JavaScriptInclude + bundleRelations.<commit_after>var _ = require('underscore'),
    urlTools = require('../util/urlTools'),
    cssom = require('cssom-papandreou'),
    uglifyJs = require('uglify-js'),
    AssetGraph = require('../AssetGraph'),
    bundleStrategyByName = {};

function getDiscriminatorForRelation(relation) {
    var discriminatorFragments = [];
    if (relation.conditionalComments) {
        Array.prototype.push.apply(discriminatorFragments, _.pluck(relation.conditionalComments, 'nodeValue'));
    }
    if (relation.type === 'HtmlStyle') {
        discriminatorFragments.push(relation.node.getAttribute('media') || 'all');
    }
    return discriminatorFragments.join(':');
}

// Internal helper function. Reuses the parse trees of existing assets, so be careful!
function makeBundle(assetGraph, assetsToBundle) {
    if (assetsToBundle.length === 0) {
        throw new Error('makeBundle: Bundle must contain at least one asset');
    } else if (assetsToBundle.length === 1) {
        // Shortcut
        return [assetsToBundle[0]];
    }

    var type = assetsToBundle[0].type,
        incomingType = {Css: 'HtmlStyle', JavaScript: 'HtmlScript'}[type],
        parseTrees = _.pluck(assetsToBundle, 'parseTree'),
        constructorOptions = {};

    if (type === 'JavaScript') {
        var topLevelStatements = [];

        constructorOptions.parseTree = new uglifyJs.AST_Toplevel({body: []});
        assetsToBundle.forEach(function (asset) {
            assetGraph.findRelations({from: asset, type: 'JavaScriptInclude'}, true).forEach(function (relation) {
                if (relation.parentNode === asset.parseTree) {
                    // This INCLUDE relation is a top level statement, update its .parentNode property to point at
                    // the top level statements array of the bundle:
                    relation.parentNode = constructorOptions.parseTree;
                }
            });
            Array.prototype.push.apply(constructorOptions.parseTree.body, asset.parseTree.body);
        });
    } else {
        // type === 'Css'
        constructorOptions.parseTree = new cssom.CSSStyleSheet();
        // Make sure that all @import rules go at the top of the bundle:
        var importRules = [];
        assetsToBundle.forEach(function (asset) {
            var cssRules = asset.parseTree.cssRules;
            asset.parseTree.parentStyleSheet = constructorOptions.parseTree;
            for (var i = 0 ; i < cssRules.length ; i += 1) {
                if (cssRules[i].type === cssom.CSSRule.IMPORT_RULE) {
                    importRules.push(cssRules[i]);
                    cssRules.splice(i, 1);
                    i -= 1;
                }
            }
            Array.prototype.push.apply(constructorOptions.parseTree.cssRules, cssRules);
        });
        if (importRules.length > 0) {
            Array.prototype.unshift.apply(constructorOptions.parseTree.cssRules, importRules);
        }
    }

    var bundleAsset = new AssetGraph[type](constructorOptions);

    bundleAsset.url = urlTools.resolveUrl(assetGraph.root, bundleAsset.id + bundleAsset.extension);
    bundleAsset._outgoingRelations = assetGraph.findRelations({from: assetsToBundle});
    bundleAsset._outgoingRelations.forEach(function (outgoingRelation) {
        outgoingRelation.remove();
        outgoingRelation.from = bundleAsset;
    });

    var seenReferringAssets = {},
        incomingRelations = assetGraph.findRelations({type: incomingType, to: assetsToBundle}),
        // Point at the bundled asset with a root-relative href if at least one of the relations
        // being bundled have a more specific hrefType than 'relative':
        bundleRelationHrefType = incomingRelations.some(function (incomingRelation) {
            return incomingRelation.hrefType !== 'relative';
        }) ? 'rootRelative' : 'relative';

    incomingRelations.forEach(function (incomingRelation) {
        if (!(incomingRelation.from.id in seenReferringAssets)) {
            var bundleRelation = new AssetGraph[incomingType]({
                hrefType: bundleRelationHrefType,
                to: bundleAsset
            });
            bundleRelation.attach(incomingRelation.from, 'before', incomingRelation);
            if (incomingType === 'HtmlStyle') {
                var media = incomingRelation.node.getAttribute('media');
                if (media && media !== 'all') {
                    bundleRelation.node.setAttribute('media', media);
                    bundleRelation.from.markDirty();
                }
            } else if (incomingType === 'HtmlScript') {
                for (var i = 0 ; i < incomingRelations.length ; i += 1) {
                    var otherIncomingRelation = incomingRelations[i];
                    if (otherIncomingRelation.assetGraph && otherIncomingRelation.node.hasAttribute('data-main')) {
                        var existingHtmlRequireJsMainRelations = assetGraph.findRelations({
                            from: otherIncomingRelation.from,
                            type: 'HtmlRequireJsMain'
                        }).filter(function (relation) {
                            return relation.node === otherIncomingRelation.node;
                        });
                        if (existingHtmlRequireJsMainRelations.length === 1) {
                            var existingHtmlRequireJsMainRelation = existingHtmlRequireJsMainRelations[0];
                            bundleRelation.node.setAttribute('data-main', otherIncomingRelation.node.getAttribute('data-main'));
                            existingHtmlRequireJsMainRelation.node = bundleRelation.node;
                            break;
                        } else {
                            throw new Error("transforms.bundleRelations: Unexpected number of existing HtmlRequireJsMain relations found");
                        }
                    }
                }
            }
            seenReferringAssets[incomingRelation.from.id] = true;
        }
        incomingRelation.detach();
    });

    assetGraph.addAsset(bundleAsset);

    assetsToBundle.forEach(function (asset) {
        if (assetGraph.findRelations({to: asset}).length === 0) {
            assetGraph.removeAsset(asset);
        }
    });
    return bundleAsset;
}

// Quick and dirty bundling strategy that gets you down to one <script> and one <link rel='stylesheet'>
// per document, but doesn't do any cross-page optimization.
bundleStrategyByName.oneBundlePerIncludingAsset = function (assetGraph, queryObj) {
    var assetsToBundleById = {},
        seenIncludingAssets = {},
        bundleAssets = [],
        relationsByIncludingAsset = {};

    assetGraph.findRelations(queryObj).forEach(function (relation) {
        assetsToBundleById[relation.to.id] = relation.to; // Means not in a bundle yet
        (relationsByIncludingAsset[relation.from.id] = relationsByIncludingAsset[relation.from.id] || []).push(relation);
    });

    Object.keys(relationsByIncludingAsset).forEach(function (includingAssetId) {
        var includingAsset = assetGraph.idIndex[includingAssetId],
            relationsToBundle = relationsByIncludingAsset[includingAssetId];

        _.unique(_.pluck(relationsToBundle, 'type')).forEach(function (relationType) {
            var currentBundle = [],
                bundleDiscriminator,
                relationsOfTypeToBundle = relationsToBundle.filter(function (relation) {
                    return relation.type === relationType;
                });

            function flushBundle() {
                if (currentBundle.length > 0) {
                    bundleAssets.push(makeBundle(assetGraph, currentBundle)); // FIXME
                    currentBundle = [];
                }
            }
            assetGraph.findRelations(assetGraph.constructor.query.or({type: relationType}, {type: 'HtmlConditionalComment'})).forEach(function (outgoingRelation) {
                if (outgoingRelation.type === 'HtmlConditionalComment') {
                    if (assetGraph.findRelations({from: outgoingRelation.to, type: relationType}, true).length > 0) {
                        flushBundle();
                    }
                } else if (relationsOfTypeToBundle.indexOf(outgoingRelation) !== -1) {
                    // Make sure that we don't bundle HtmlStyles with different media attributes together etc.:
                    var discriminator = getDiscriminatorForRelation(outgoingRelation);
                    if (bundleDiscriminator && discriminator !== bundleDiscriminator) {
                        flushBundle();
                    }
                    bundleDiscriminator = discriminator;
                    if (assetGraph.findRelations({to: outgoingRelation.to}).length > 1) {
                        currentBundle.push(outgoingRelation.to.clone(outgoingRelation));
                    } else {
                        currentBundle.push(outgoingRelation.to);
                    }
                } else {
                    flushBundle();
                }
            });
            flushBundle();
        });
    });
    return bundleAssets;
};

// Cross-page optimizing bundling strategy that never puts the same chunk in multiple bundles, but still tries
// to create as few bundles as possible. Also preserves inclusion order.
// FIXME: This bundling strategy is still quite buggy, please don't use it yet.
bundleStrategyByName.sharedBundles = function (assetGraph, queryObj) {
    var assetIndex = {},
        seenIncludingAssets = {},
        bundles = [],
        relationsByIncludingAsset = {};

    assetGraph.findRelations(queryObj).forEach(function (relation) {
        assetIndex[relation.to.id] = null; // Means not in a bundle yet
        seenIncludingAssets[relation.from.id] = relation.from;
        (relationsByIncludingAsset[relation.from.id] = relationsByIncludingAsset[relation.from.id] || []).push(relation);
    });

    function splitBundle(bundle, index) {
        var newBundle = bundle.splice(index);
        newBundle.forEach(function (asset) {
            assetIndex[asset.id] = newBundle;
        });
        if (newBundle.length > 0) {
            bundles.push(newBundle);
        }
        return newBundle;
    }

    _.values(seenIncludingAssets).forEach(function (includingAsset) {
        var relationsToBundle = relationsByIncludingAsset[includingAsset.id];

        _.unique(_.pluck(relationsToBundle, 'type')).forEach(function (relationType) {
            var outgoingRelations = assetGraph.findRelations({from: includingAsset, type: [relationType, 'HtmlConditionalComment']}, true), // includeUnresolved
                previousBundle,
                bundleDiscriminator,
                canAppendToPreviousBundle = false,
                previousBundleIndex;

            outgoingRelations.forEach(function (outgoingRelation) {
                if (outgoingRelation.type === 'HtmlConditionalComment') {
                    if (assetGraph.findRelations({from: outgoingRelation.to, type: relationType}).length > 0) {
                        canAppendToPreviousBundle = false;
                    }
                    return;
                }

                // Make sure that we don't bundle HtmlStyles with different media attributes together etc.:
                var discriminator = getDiscriminatorForRelation(outgoingRelation);
                if (bundleDiscriminator && discriminator !== bundleDiscriminator) {
                    canAppendToPreviousBundle = false;
                }
                bundleDiscriminator = discriminator;

                var existingBundle = assetIndex[outgoingRelation.to.id];
                if (existingBundle === null) {
                    // Not bundled yet, append to previousBundle if possible, else create a new one
                    if (canAppendToPreviousBundle) {
                        previousBundle.push(outgoingRelation.to);
                        previousBundleIndex = previousBundle.length - 1;
                    } else {
                        if (previousBundle && previousBundleIndex !== previousBundle.length - 1) {
                            splitBundle(previousBundle, previousBundleIndex + 1);
                        }
                        previousBundle = [outgoingRelation.to];
                        previousBundleIndex = 0;
                        bundles.push(previousBundle);
                        canAppendToPreviousBundle = true;
                    }
                    assetIndex[outgoingRelation.to.id] = previousBundle;
                } else if (existingBundle) {
                    // Already in another bundle
                    canAppendToPreviousBundle = false;
                    var indexInExistingBundle = existingBundle.indexOf(outgoingRelation.to);
                    if (previousBundle && existingBundle === previousBundle) {
                        if (indexInExistingBundle === previousBundleIndex + 1) {
                            previousBundleIndex = indexInExistingBundle;
                        } else {
                            splitBundle(previousBundle, indexInExistingBundle + 1);
                            existingBundle = assetIndex[outgoingRelation.to.id];
                            indexInExistingBundle = existingBundle.indexOf(outgoingRelation.to);
                            if (indexInExistingBundle !== 0) {
                                existingBundle = splitBundle(existingBundle, indexInExistingBundle);
                            }
                            previousBundle = existingBundle;
                            previousBundleIndex = 0;
                        }
                    } else {
                        if (previousBundle && previousBundleIndex !== (previousBundle.length - 1)) {
                            splitBundle(previousBundle, previousBundleIndex + 1);
                        }
                        if (indexInExistingBundle !== 0) {
                            existingBundle = splitBundle(existingBundle, indexInExistingBundle);
                        }
                        previousBundle = existingBundle;
                        previousBundleIndex = 0;
                    }
                } else {
                    // The relation doesn't point at an asset matched by queryObj
                    previousBundle = null;
                    canAppendToPreviousBundle = false;
                }
            });
            // No more outgoing relations for this asset, make sure that the asset that was bundled
            // last is at the last position in its bundle:
            if (previousBundle && previousBundleIndex !== previousBundle.length - 1) {
                splitBundle(previousBundle, previousBundleIndex + 1);
            }
        });
    });

    return bundles.map(function (bundle) {
        return makeBundle(assetGraph, bundle);
    });
};

module.exports = function (queryObj, bundleStrategyName) {
    if (!bundleStrategyName) {
        bundleStrategyName = 'oneBundlePerIncludingAsset';
    } else if (!(bundleStrategyName in bundleStrategyByName)) {
        throw new Error("transforms.bundleRelations: Unknown bundle strategy: " + bundleStrategyName);
    }

    return function bundleRelations(assetGraph) {
        var bundleAssets = bundleStrategyByName[bundleStrategyName](assetGraph, queryObj);
        assetGraph.recomputeBaseAssets();
        bundleAssets.forEach(function (bundleAsset) {
            assetGraph.findRelations({to: bundleAsset}).forEach(function (incomingRelation) {
                incomingRelation.refreshHref();
            });
            assetGraph.findRelations({from: bundleAsset}).forEach(function (outgoingRelation) {
                outgoingRelation.refreshHref();
            });
        });
    };
};
<|endoftext|>


<commit_before>/* configuration for jshint */
/* global d3, Calendar, LocalGeocoder, Progress */

(function() {
  'use strict';

  var file,
      DAY_HOURS = [11, 15],
      NIGHT_HOURS = [23, 4],
      hour_range = NIGHT_HOURS;

  var geocoder = LocalGeocoder(),
      calendar = Calendar(d3.select('#calendar'));

  // UID functions
  var fileUID = function(d) {
    return d.file.lastModified + '' + d.file.size + '' + d.file.name;
  };

  var fullUID = function(d) {
    return fileUID(d) + '' + d.hour_range;
  };

  // Processing functions
  function loadFile(env, cb) {
    var file = env.file,
        file_reader = new FileReader();

    file_reader.onprogress = function(e) {env.progress.set(e.loaded / e.total);};
    file_reader.onload = function(e) {cb(e.target.result);};
    file_reader.readAsText(file);
  }

  function parseJson(raw_data, env, cb) {
    cb(JSON.parse(raw_data));
  }

  function filterHours(data, env, cb) {
    var filtered_data = {},
        batch = Math.floor(data.locations.length / 100),
        l, t, d,
        hr;

    if (hour_range[0] < hour_range[1]) {
      hr = function(h) {return (h > hour_range[0] && h < hour_range[1]);};
    } else {
      hr = function(h) {return (h > hour_range[0] || h < hour_range[1]);};
    }

    // pull out dates that satisfy hour ranges
    function applyFilter(first, last) {
      last = Math.min(last, data.locations.length - 1);
      for (var i = first; i <= last; i++) {
        l = data.locations[i];
        t = new Date(+l.timestampMs);
        d = getDay(t);

        if (d) {
          d in filtered_data || (filtered_data[d] = []);
          filtered_data[d].push({
            lat: l.latitudeE7, lon: l.longitudeE7, ts: l.timestampMs,
          });
        }
      }

      if (first >= last) {
        cb(filtered_data);
      } else {
        env.progress.set(last / data.locations.length);
        setTimeout(applyFilter, 1, last, last + batch);
      }
    }

    applyFilter(0, batch);

    // Given a Date object, return yyyy-mm-dd for beginning of filter range
    // *iff* the time in estimated tz falls within filter range
    function getDay(t) {
      if (hr(t.getHours()) === false) {return null;}
      return t.getFullYear() + '-' + (t.getMonth() + 1) + '-' + t.getDate();
    }
  }

  function selectRepresentatives(filtered_data, env, cb) {
    var reps = [];
    // Currently using stub logic -- should eventually find
    // time-offset-weighted centroid of points rather than the first
    for (var d in filtered_data) {reps[d] = filtered_data[d][0];}
    cb(reps);
  }

  function reverseGeocode(representatives, env, cb) {
    // assign place name and info to each representative
    var georeps = [],
        r, v;
    for (r in representatives) {
      if (representatives.hasOwnProperty(r)) {
        v = representatives[r];
        v.location = geocoder.geocode(v);
        v.day = new Date(r);
        georeps.push(v);
      }
    }
    cb(georeps);
  }

  function render(georeps, env, cb) {
    d3.select('#container').style('display', 'block');
    calendar.render(georeps, env, cb);
  }

  // construct processing pipeline
  var loadFileC = progressify(loadFile, 'Processing File'),
      parseJsonC = pipeline(parseJson, 'Parsing JSON', fileUID, loadFileC),
      filterC = pipeline(filterHours, 'Applying Time Filter', fileUID, parseJsonC),
      repC = pipeline(selectRepresentatives, 'Selecting Locations', fullUID, filterC),
      geocodeC = pipeline(reverseGeocode, 'Reverse Geocoding', fullUID, repC),
      renderC = pipeline(render, 'Rendering', fullUID, geocodeC);

  // add caching to determine when required inputs have already been computed.
  function cachify(main_func, uid_func, prereq_func) {
    var cache = {};
    return function(env, cb) {
      var uid = uid_func(env);
      if (!cache.hasOwnProperty(uid)) {
        prereq_func(env, function() {
          cache[uid] = [].slice.call(arguments, 0);
          setTimeout(function() {
            main_func.apply(this, [].concat(cache[uid], env, cb));
          }, 50);
        });
      } else {
        setTimeout(function() {
          main_func.apply(this, [].concat(cache[uid], env, cb));
        }, 50);
      }
    };
  }

  // add progress bar during execution of main_func
  //
  // The main_func is assumed to take an env object as the penultimate
  // argument, and a "done" callback as the last argument.  The progress bar
  // is initialized on function invocations and automatically completed on
  // callback invocations.  Also, a reference to the progress bar object is
  // added to the env to allow calls to progress.set().
  function progressify(main_func, txt) {
    return function() {
      var args = [].slice.call(arguments, 0);
      if (args.length >= 2) {
        var progress = Progress(txt).start(),
            cb = args[args.length - 1];
        args[args.length - 2].progress = progress;
        args[args.length - 1] = function() {
          progress.done();
          cb.apply(this, arguments);
        };
        setTimeout(function() {
          main_func.apply(this, args);
        }, 100);
      }
    };
  }

  function pipeline(main_func, txt, uid_func, prereq_func) {
    return cachify(progressify(main_func, txt), uid_func, prereq_func);
  }

  function run(file, hour_range) {
    renderC({file: file, hour_range: hour_range}, function() {
      setTimeout(Progress().clearAll, 400);
    });
  }

  d3.select('#file-input').on('change', function() {
    run(file = this.files[0], hour_range);
  });

  d3.selectAll('#options input').on('change', function() {
    hour_range = this.value === 'night' ? NIGHT_HOURS : DAY_HOURS;
    console.log('you selected hour_range:', hour_range);
    if (file) {
      run(file, hour_range);
    }
  });

})();
<commit_msg>Added autoscroll<commit_after>/* configuration for jshint */
/* global d3, Calendar, LocalGeocoder, Progress */

(function() {
  'use strict';

  var file,
      DAY_HOURS = [11, 15],
      NIGHT_HOURS = [23, 4],
      hour_range = NIGHT_HOURS;

  var geocoder = LocalGeocoder(),
      calendar = Calendar(d3.select('#calendar'));

  // UID functions
  var fileUID = function(d) {
    return d.file.lastModified + '' + d.file.size + '' + d.file.name;
  };

  var fullUID = function(d) {
    return fileUID(d) + '' + d.hour_range;
  };

  // Processing functions
  function loadFile(env, cb) {
    var file = env.file,
        file_reader = new FileReader();

    file_reader.onprogress = function(e) {env.progress.set(e.loaded / e.total);};
    file_reader.onload = function(e) {cb(e.target.result);};
    file_reader.readAsText(file);
  }

  function parseJson(raw_data, env, cb) {
    cb(JSON.parse(raw_data));
  }

  function filterHours(data, env, cb) {
    var filtered_data = {},
        batch = Math.floor(data.locations.length / 100),
        l, t, d,
        hr;

    if (hour_range[0] < hour_range[1]) {
      hr = function(h) {return (h > hour_range[0] && h < hour_range[1]);};
    } else {
      hr = function(h) {return (h > hour_range[0] || h < hour_range[1]);};
    }

    // pull out dates that satisfy hour ranges
    function applyFilter(first, last) {
      last = Math.min(last, data.locations.length - 1);
      for (var i = first; i <= last; i++) {
        l = data.locations[i];
        t = new Date(+l.timestampMs);
        d = getDay(t);

        if (d) {
          d in filtered_data || (filtered_data[d] = []);
          filtered_data[d].push({
            lat: l.latitudeE7, lon: l.longitudeE7, ts: l.timestampMs,
          });
        }
      }

      if (first >= last) {
        cb(filtered_data);
      } else {
        env.progress.set(last / data.locations.length);
        setTimeout(applyFilter, 1, last, last + batch);
      }
    }

    applyFilter(0, batch);

    // Given a Date object, return yyyy-mm-dd for beginning of filter range
    // *iff* the time in estimated tz falls within filter range
    function getDay(t) {
      if (hr(t.getHours()) === false) {return null;}
      return t.getFullYear() + '-' + (t.getMonth() + 1) + '-' + t.getDate();
    }
  }

  function selectRepresentatives(filtered_data, env, cb) {
    var reps = [];
    // Currently using stub logic -- should eventually find
    // time-offset-weighted centroid of points rather than the first
    for (var d in filtered_data) {reps[d] = filtered_data[d][0];}
    cb(reps);
  }

  function reverseGeocode(representatives, env, cb) {
    // assign place name and info to each representative
    var georeps = [],
        r, v;
    for (r in representatives) {
      if (representatives.hasOwnProperty(r)) {
        v = representatives[r];
        v.location = geocoder.geocode(v);
        v.day = new Date(r);
        georeps.push(v);
      }
    }
    cb(georeps);
  }

  function render(georeps, env, cb) {
    d3.select('#container').style('display', 'block');
    calendar.render(georeps, env, cb);
  }

  // construct processing pipeline
  var loadFileC = progressify(loadFile, 'Processing File'),
      parseJsonC = pipeline(parseJson, 'Parsing JSON', fileUID, loadFileC),
      filterC = pipeline(filterHours, 'Applying Time Filter', fileUID, parseJsonC),
      repC = pipeline(selectRepresentatives, 'Selecting Locations', fullUID, filterC),
      geocodeC = pipeline(reverseGeocode, 'Reverse Geocoding', fullUID, repC),
      renderC = pipeline(render, 'Rendering', fullUID, geocodeC);

  // add caching to determine when required inputs have already been computed.
  function cachify(main_func, uid_func, prereq_func) {
    var cache = {};
    return function(env, cb) {
      var uid = uid_func(env);
      if (!cache.hasOwnProperty(uid)) {
        prereq_func(env, function() {
          cache[uid] = [].slice.call(arguments, 0);
          setTimeout(function() {
            main_func.apply(this, [].concat(cache[uid], env, cb));
          }, 50);
        });
      } else {
        setTimeout(function() {
          main_func.apply(this, [].concat(cache[uid], env, cb));
        }, 50);
      }
    };
  }

  // add progress bar during execution of main_func
  //
  // The main_func is assumed to take an env object as the penultimate
  // argument, and a "done" callback as the last argument.  The progress bar
  // is initialized on function invocations and automatically completed on
  // callback invocations.  Also, a reference to the progress bar object is
  // added to the env to allow calls to progress.set().
  function progressify(main_func, txt) {
    return function() {
      var args = [].slice.call(arguments, 0);
      if (args.length >= 2) {
        var progress = Progress(txt).start(),
            cb = args[args.length - 1];
        args[args.length - 2].progress = progress;
        args[args.length - 1] = function() {
          progress.done();
          cb.apply(this, arguments);
        };
        setTimeout(function() {
          main_func.apply(this, args);
        }, 100);
      }
    };
  }

  function pipeline(main_func, txt, uid_func, prereq_func) {
    return cachify(progressify(main_func, txt), uid_func, prereq_func);
  }

  function run(file, hour_range) {
    renderC({file: file, hour_range: hour_range}, function() {
      setTimeout(Progress().clearAll, 400);
      scrollToCalendar();
    });
  }

  function scrollToCalendar() {
    d3.transition().duration(1000)
        .tween('scroll', scrollTween(d3.select('#container').property('offsetTop')));
  }

  function scrollTween(offset) {
    return function() {
      var i = d3.interpolateNumber(window.pageYOffset || document.documentElement.scrollTop, offset);
      return function(t) { scrollTo(0, i(t)); };
    };
  }

  d3.select('#file-input').on('change', function() {
    run(file = this.files[0], hour_range);
  });

  d3.selectAll('#options input').on('change', function() {
    hour_range = this.value === 'night' ? NIGHT_HOURS : DAY_HOURS;
    console.log('you selected hour_range:', hour_range);
    if (file) {
      run(file, hour_range);
    }
  });

})();
<|endoftext|>


<commit_before>'use strict';
var domv = require('domv');
var FileReader = global.FileReader;
var hrtime = require('../../swissarmyknife/hrtime');
var bytes = require('bytes');

require('static-reference')('./style/EditableImage.less');

function EditableImage(node, maxSize, initialSrc)
{
        domv.Component.call(this, node, 'span');

        this.lastChangeByUser = null;
        this.fileValue = null; // note: reset if a src value is set
        this.maxSize = maxSize || 1024 * 1024; // 1 MiB

        if (this.isCreationConstructor(node))
        {
                this.cls('EditableImage');

                this.appendChild(this.errorNode = this.create('span', 'error'));
                this.image = null;
                this.srcValue = initialSrc;
                this.input = null; // lazy

        }
        else
        {
                this.assertHasClass('EditableImage');
                this.errorNode = this.assertSelector('> .error');
                this.image = this.selector('> img');
                this.input = this.selector('> input');

                if (this.input)
                {
                        this.input.on('change', this._onInputChange, false, this);
                }
        }

        this.on('click', this._onClick);
        this.on('keydown', this._onKeydown);

        // todo drag
}

module.exports = EditableImage;
require('inherits')(EditableImage, domv.Component);

Object.defineProperty(EditableImage.prototype, 'editing', {
        configurable: true,
        get: function()
        {
                return this.hasClass('editing');
        },
        set: function(value)
        {
                value = !!value;
                this.toggleClass('editing', value);
                this.attr('tabindex', value ? '0' : false);
                this.error = '';

                if (value)
                {
                        if (!this.input)
                        {
                                this.input = this.create('input', {
                                        type: 'file',
                                        accept: 'image/png,image/jpeg,image/svg+xml'
                                });
                                this.input.on('change', this._onInputChange, false, this);
                        }
                        this.appendChild(this.input);
                }
                else if (this.input)
                {
                        this.input.removeNode();
                }
        }
});

Object.defineProperty(EditableImage.prototype, 'srcValue', {
        configurable: true,
        get: function()
        {
                return (this.image && this.image.getAttr('img')) || null;
        },
        set: function(value)
        {
                this.fileValue = null;
                this.error = '';

                if (!value)
                {
                        if (this.image)
                        {
                                this.image.removeNode();
                                this.image = null;
                        }
                        return;
                }

                if (!this.image)
                {
                        this.appendChild(this.image = this.create('img'));
                }

                this.image.attr('src', value);
        }
});

Object.defineProperty(EditableImage.prototype, 'error', {
        configurable: true,
        get: function()
        {
                return this.errorNode.textContent;
        },
        set: function(value)
        {
                this.errorNode.textContent = value || '';
                this.errorNode.style.display = value ? 'block' : null;
        }
});

EditableImage.prototype._onClick = function(e)
{
        if (!this.editing)
        {
                return;
        }

        if (this.input.isNodeEqual(e.target))
        {
                return;
        }

        e.preventDefault();

        this.input.outerNode.click();
};

EditableImage.prototype._onKeydown = function(e)
{
        if (!this.editing)
        {
                return;
        }

        if (e.keyCode === 13 || // enter key
            e.keyCode === 32    // space
        )
        {
                e.preventDefault();

                this.input.outerNode.click();
        }
};

EditableImage.prototype.handleFileSelect = function(files)
{
        var imageFile = null;
        if (!files) { files = []; }
        this.error = '';

        for (var i = 0; i < files.length; ++i)
        {
                var file = files[i];

                if (! /^image\/(png|jpeg|svg\+xml)$/.test(file.type))
                {
                        continue;
                }

                imageFile = file;

                break;
        }

        if (!imageFile)
        {
                this.error = 'Please select a valid image for uploading (png, jpeg or svg)';
                return;
        }

        if (this.maxSize &&
            imageFile.size > this.maxSize)
        {
                this.error = 'The image file must be smaller than ' + bytes(this.maxSize).toUpperCase();
                return;
        }

        var eventData = {
                targetComponent: this,
                newFile: imageFile
        };

        if (this.emit('domv-editable-change', eventData))
        {
                this.lastChangeByUser = hrtime();

                this.fileValue = imageFile;

                var reader = new FileReader();

                reader.onerror = function()
                {
                        this.error = 'Your web browser was unable to read the file you selected';
                }.bind(this);

                reader.onloadend = function()
                {
                        this.image.attr('src', reader.result);
                }.bind(this);

                reader.readAsDataURL(imageFile);
        }
};

EditableImage.prototype._onInputChange = function(e)
{
        this.handleFileSelect(this.input.outerNode.files);
};<commit_msg>Avoid SVG, it is too hard to protect against malicious SVG files<commit_after>'use strict';
var domv = require('domv');
var FileReader = global.FileReader;
var hrtime = require('../../swissarmyknife/hrtime');
var bytes = require('bytes');

require('static-reference')('./style/EditableImage.less');

function EditableImage(node, maxSize, initialSrc)
{
        domv.Component.call(this, node, 'span');

        this.lastChangeByUser = null;
        this.fileValue = null; // note: reset if a src value is set
        this.maxSize = maxSize || 1024 * 1024; // 1 MiB

        if (this.isCreationConstructor(node))
        {
                this.cls('EditableImage');

                this.appendChild(this.errorNode = this.create('span', 'error'));
                this.image = null;
                this.srcValue = initialSrc;
                this.input = null; // lazy

        }
        else
        {
                this.assertHasClass('EditableImage');
                this.errorNode = this.assertSelector('> .error');
                this.image = this.selector('> img');
                this.input = this.selector('> input');

                if (this.input)
                {
                        this.input.on('change', this._onInputChange, false, this);
                }
        }

        this.on('click', this._onClick);
        this.on('keydown', this._onKeydown);

        // todo drag
}

module.exports = EditableImage;
require('inherits')(EditableImage, domv.Component);

Object.defineProperty(EditableImage.prototype, 'editing', {
        configurable: true,
        get: function()
        {
                return this.hasClass('editing');
        },
        set: function(value)
        {
                value = !!value;
                this.toggleClass('editing', value);
                this.attr('tabindex', value ? '0' : false);
                this.error = '';

                if (value)
                {
                        if (!this.input)
                        {
                                this.input = this.create('input', {
                                        type: 'file',
                                        accept: 'image/png,image/jpeg'
                                });
                                this.input.on('change', this._onInputChange, false, this);
                        }
                        this.appendChild(this.input);
                }
                else if (this.input)
                {
                        this.input.removeNode();
                }
        }
});

Object.defineProperty(EditableImage.prototype, 'srcValue', {
        configurable: true,
        get: function()
        {
                return (this.image && this.image.getAttr('img')) || null;
        },
        set: function(value)
        {
                this.fileValue = null;
                this.error = '';

                if (!value)
                {
                        if (this.image)
                        {
                                this.image.removeNode();
                                this.image = null;
                        }
                        return;
                }

                if (!this.image)
                {
                        this.appendChild(this.image = this.create('img'));
                }

                this.image.attr('src', value);
        }
});

Object.defineProperty(EditableImage.prototype, 'error', {
        configurable: true,
        get: function()
        {
                return this.errorNode.textContent;
        },
        set: function(value)
        {
                this.errorNode.textContent = value || '';
                this.errorNode.style.display = value ? 'block' : null;
        }
});

EditableImage.prototype._onClick = function(e)
{
        if (!this.editing)
        {
                return;
        }

        if (this.input.isNodeEqual(e.target))
        {
                return;
        }

        e.preventDefault();

        this.input.outerNode.click();
};

EditableImage.prototype._onKeydown = function(e)
{
        if (!this.editing)
        {
                return;
        }

        if (e.keyCode === 13 || // enter key
            e.keyCode === 32    // space
        )
        {
                e.preventDefault();

                this.input.outerNode.click();
        }
};

EditableImage.prototype.handleFileSelect = function(files)
{
        var imageFile = null;
        if (!files) { files = []; }
        this.error = '';

        for (var i = 0; i < files.length; ++i)
        {
                var file = files[i];

                if (! /^image\/(png|jpeg|svg\+xml)$/.test(file.type))
                {
                        continue;
                }

                imageFile = file;

                break;
        }

        if (!imageFile)
        {
                this.error = 'Please select a valid image for uploading (png, jpeg or svg)';
                return;
        }

        if (this.maxSize &&
            imageFile.size > this.maxSize)
        {
                this.error = 'The image file must be smaller than ' + bytes(this.maxSize).toUpperCase();
                return;
        }

        var eventData = {
                targetComponent: this,
                newFile: imageFile
        };

        if (this.emit('domv-editable-change', eventData))
        {
                this.lastChangeByUser = hrtime();

                this.fileValue = imageFile;

                var reader = new FileReader();

                reader.onerror = function()
                {
                        this.error = 'Your web browser was unable to read the file you selected';
                }.bind(this);

                reader.onloadend = function()
                {
                        this.image.attr('src', reader.result);
                }.bind(this);

                reader.readAsDataURL(imageFile);
        }
};

EditableImage.prototype._onInputChange = function(e)
{
        this.handleFileSelect(this.input.outerNode.files);
};<|endoftext|>


<commit_before>// Map centering variables
var lonRotate = -60;
var latRotate = 10;
var latBottom = 24;
var latCenter = 30;

// Map sizing variables
var mapRatio = 0.5;
var maxWidthRatio = 0.6;
var maxHeightRatio = 1.3;
var patternSide = 400;

var width = 0, height = 0, scale = 0;

var colors = {
// Colors from ColorBrewer2
    'red':          '#e41a1c',
    'blue':         '#377eb8',
    'green':        '#4daf4a',
    'purple':       '#984ea3',
    'orange':       '#ff7f00',
    'yellow':       '#ffff33',
    'brown':        '#a65628',
    'pink':         '#f781bf',
// Colors from variables.less
    'gray-darker':  '#272B30',
    'gray-dark':    '#3A3F44',
    'gray':         '#52575C',
    'gray-light':   '#7A8288',
    'gray-lighter': '#999',
};

// Display variables
var transitionDurationStroke = 300,
    transitionDurationWorld = 500,
    shadowHeight = 4,
    blurRadius = 3;

var lon = {
    'us': -100,
    'spain': -7,
    'us-edge': -65,
    'spain-edge': -20,
};

var pathStyle = {
    'stroke-width': 3,
    'hover': {
        'stroke-width': 5,
    }
};

var graticuleStyle = {
  'stroke': colors['gray'],
  'stroke-width': 0.5,
};

var landStyle = {
    'stroke': colors['gray'],
    'stroke-width': 1,
    'background-image': loadImage('images/mochaGrunge.png'),
};

// Data loading
var dataToLoad = {
    'at': {
        'url': 'at/',
        'color': colors['brown'],
        'stroke-width': pathStyle['stroke-width'],
        'center-lon': lon['us'],
    },
    'momdad': {
        'url': 'http://mannymarsha.wordpress.com/',
        'color': colors['purple'],
        'stroke-width': pathStyle['stroke-width'],
        'center-lon': lon['us'],
    },
    'transam': {
        'url': 'http://picasaweb.com/nathanbiketrip',
        'color': colors['green'],
        'stroke-width': pathStyle['stroke-width'],
        'center-lon': lon['us'],
    },
    'camino': {
        'url': 'http://blog.travelpod.com/travel-blog/aggiesontheway/2/tpod.html',
        'color': colors['red'],
        'stroke-width': pathStyle['stroke-width'],
        'center-lon': lon['spain'],
    },
};

// State variables
var isRotating = false;

// Image loading
function loadImage(url) {
    var image = new Image();
    image.src = url;
    return image;
}

var map = d3.select('#map');
var canvas = map.append("canvas")
var svg = map.append('svg');

var c = canvas.node().getContext("2d");

var graticule = d3.geo.graticule();

var projection = d3.geo.orthographic()
    .clipAngle(90);

var path = d3.geo.path()
    .projection(projection)
    .context(c);

var svgPath = d3.geo.path()
    .projection(projection);

//
// Load topjson data
//

// Add helper methods to String
if (typeof String.prototype.endsWith !== 'function') {
    String.prototype.endsWith = function(suffix) {
        return this.indexOf(suffix, this.length - suffix.length) !== -1;
    };
}
String.prototype.removeRight = function(suffix) {
    return this.substring(0, this.length - suffix.length);
};

var land;

var queue = queue()
    .defer(d3.json, '../data/world-110m.json');

for (var key in dataToLoad) {
    queue.defer(d3.json, '../data/' + key + '.json');
    queue.defer(d3.json, '../data/' + key + '_buffer.json');
}

queue.awaitAll(ready);

function ready(error, results) {
    // Load world data
    var landResult = results.shift();
    land = topojson.feature(landResult, landResult.objects.land);

    // Load all other path + buffer data
    results.forEach(function(result) {
        for (var key in result.objects) {
            if (dataToLoad[key]) {
                dataToLoad[key]['feature'] = topojson.feature(result, result.objects[key]);
            }
            else if (key.endsWith('_buffer')) {
                parent = key.removeRight('_buffer');
                if (dataToLoad[parent]) {
                    dataToLoad[parent]['buffer'] = topojson.feature(result, result.objects[key]);
                }
            }
        }
    });

    // Draw buffers on svg
    drawBuffer('momdad');
    drawBuffer('transam');
    drawBuffer('at');
    drawBuffer('camino');

    // Add resize event handler and run for the first time
    d3.select(window).on('resize', resize);
    resize();

    // Add mousemove event handler for auto rotation
    d3.select(window).on("mousemove", function() {
        if (!isRotating) {
            if ( lonRotate === lon['us'] &&
                 d3.event.clientX > projection([lon['us-edge'], latCenter])[0] ) {
                // Scroll to Spain
                rotate(lon['spain']);
            }
            else if ( lonRotate === lon['spain'] &&
                      d3.event.clientX < projection([lon['spain-edge'], latCenter])[0] ) {
                // Scroll to US
                rotate(lon['us']);
            }
            else if ( lonRotate !== lon['us'] && lonRotate !== lon['spain'] ) {
                if ( d3.event.clientX < (width / 2) ) {
                    // Scroll to US
                    rotate(lon['us']);
                }
                else {
                    // Scroll to Spain
                    rotate(lon['spain']);
                }
            }
        }
    });
}

function drawBuffer(pathName) {
    var buffer = svg.append('a')
                    .attr('class', 'buffer')
                    .attr('xlink:href', dataToLoad[pathName]['url'])
                    .append('path')
                        .attr('id', pathName)
                        .datum(dataToLoad[pathName]['buffer']);

    var link = d3.select('.copy .' + pathName);
    link.style('transition-duration', transitionDurationStroke + 'ms');

    function setTextShadow(width) {
        var textShadow = '0px 0px ' + width + 'px';
        if (width > 0) {
            textShadow += ' ' + dataToLoad[pathName]['color'];
        }
        link.style('text-shadow', textShadow);
    }

    function setPathStroke(width) {
        d3.transition()
            .duration(transitionDurationStroke)
            .tween("width", function() {
                w = d3.interpolate(dataToLoad[pathName]['stroke-width'], width);
                return function(t) {
                    dataToLoad[pathName]['stroke-width'] = w(t);
                    draw();
                };
            });
    }

    function doMouseover() {
        if (!isRotating) {
            resetPathStrokes();
            setPathStroke(pathStyle['hover']['stroke-width']);
            rotate(dataToLoad[pathName]['center-lon']);
        }
        setTextShadow(blurRadius);
    }

    function doMouseout() {
        if (!isRotating) {
            setPathStroke(pathStyle['stroke-width']);
        }
        setTextShadow(0);
    }

    // Add event handlers to svg buffers and to html links
    buffer.on('mouseover', doMouseover);
    link.on('mouseover', doMouseover);

    buffer.on('mouseout', doMouseout);
    link.on('mouseout', doMouseout);
}

function resize() {
    // Clear whatever is currently drawn
    c.clearRect(0, 0, width, height);

    // Calculate dimensions to ensure entire page is always visible
    var nonMapHeight = parseInt(d3.select('.container').style('height')) +
                       parseInt(d3.select('#footer').style('height')) +
                       5;

    width = parseInt(map.style('width'));

    height = Math.min(
        Math.round(width * mapRatio),
        window.innerHeight - nonMapHeight
    );

    scale = Math.min(
        width * maxWidthRatio,
        height * maxHeightRatio
    );

    draw();
    drawSvg();
}

function draw() {
    projection
        .rotate([-lonRotate, -latRotate])
        .center([0, latBottom - latRotate])
        .scale(scale)
        .translate([width / 2, height]);

    canvas
        .attr("width", width)
        .attr("height", height);

    d3.select(self.frameElement).style('height', height + 'px');

    // Clear whatever is currently drawn
    c.clearRect(0, 0, width, height);

    c.strokeStyle = graticuleStyle['stroke'],
        c.lineWidth = graticuleStyle['stroke-width'],
        c.beginPath(),
        path(graticule()),
        c.stroke();
    c.fillStyle = c.createPattern(landStyle['background-image'], 'repeat'),
        c.strokeStyle = landStyle['stroke'],
        c.lineWidth = landStyle['stroke-width'],
        c.beginPath(),
        path(land),
        c.fill(),
        c.stroke();

    for (var key in dataToLoad) {
        c.strokeStyle = dataToLoad[key]['color'],
            c.lineWidth = dataToLoad[key]['stroke-width'],
            c.beginPath(), path(dataToLoad[key]['feature']),
            c.stroke();
    }

    // Draw shadow on canvas bottom
    var shadow = c.createLinearGradient(0, height - shadowHeight, 0, height + shadowHeight);
    shadow.addColorStop(0, 'transparent');
    shadow.addColorStop(1, colors['gray-dark']);
    c.fillStyle = shadow,
        c.fillRect(0, height - shadowHeight, width, height);
}

function drawSvg() {
    svg
        .attr('width', width)
        .attr('height', height);

    // Redraw SVG buffers
    for (var key in dataToLoad) {
        svg.select('#' + key).attr('d', svgPath);
    }
}

function rotate(to) {
    var from = -projection.rotate()[0];
    if (from !== to) {
        isRotating = true;
        resetPathStrokes();
        d3.transition()
            .duration(transitionDurationWorld)
            .tween("rotate", function() {
                r = d3.interpolate(from, to);
                return function(t) {
                    lonRotate = r(t);
                    draw();
                };
            })
            .each('end', function() {
                isRotating = false;
                drawSvg();
            });
    }
}

function resetPathStrokes() {
    for (var key in dataToLoad) {
        dataToLoad[key]['stroke-width'] = pathStyle['stroke-width'];
    }
    draw();
}
<commit_msg>increase stroke width when rotating from text link<commit_after>// Map centering variables
var lonRotate = -60;
var latRotate = 10;
var latBottom = 24;
var latCenter = 30;

// Map sizing variables
var mapRatio = 0.5;
var maxWidthRatio = 0.6;
var maxHeightRatio = 1.3;
var patternSide = 400;

var width = 0, height = 0, scale = 0;

var colors = {
// Colors from ColorBrewer2
    'red':          '#e41a1c',
    'blue':         '#377eb8',
    'green':        '#4daf4a',
    'purple':       '#984ea3',
    'orange':       '#ff7f00',
    'yellow':       '#ffff33',
    'brown':        '#a65628',
    'pink':         '#f781bf',
// Colors from variables.less
    'gray-darker':  '#272B30',
    'gray-dark':    '#3A3F44',
    'gray':         '#52575C',
    'gray-light':   '#7A8288',
    'gray-lighter': '#999',
};

// Display variables
var transitionDurationStroke = 300,
    transitionDurationWorld = 500,
    shadowHeight = 4,
    blurRadius = 3;

var lon = {
    'us': -100,
    'spain': -7,
    'us-edge': -65,
    'spain-edge': -20,
};

var pathStyle = {
    'stroke-width': 3,
    'hover': {
        'stroke-width': 5,
    }
};

var graticuleStyle = {
  'stroke': colors['gray'],
  'stroke-width': 0.5,
};

var landStyle = {
    'stroke': colors['gray'],
    'stroke-width': 1,
    'background-image': loadImage('images/mochaGrunge.png'),
};

// Data loading
var dataToLoad = {
    'at': {
        'url': 'at/',
        'color': colors['brown'],
        'stroke-width': pathStyle['stroke-width'],
        'center-lon': lon['us'],
    },
    'momdad': {
        'url': 'http://mannymarsha.wordpress.com/',
        'color': colors['purple'],
        'stroke-width': pathStyle['stroke-width'],
        'center-lon': lon['us'],
    },
    'transam': {
        'url': 'http://picasaweb.com/nathanbiketrip',
        'color': colors['green'],
        'stroke-width': pathStyle['stroke-width'],
        'center-lon': lon['us'],
    },
    'camino': {
        'url': 'http://blog.travelpod.com/travel-blog/aggiesontheway/2/tpod.html',
        'color': colors['red'],
        'stroke-width': pathStyle['stroke-width'],
        'center-lon': lon['spain'],
    },
};

// State variables
var isRotating = false;

// Image loading
function loadImage(url) {
    var image = new Image();
    image.src = url;
    return image;
}

var map = d3.select('#map');
var canvas = map.append("canvas")
var svg = map.append('svg');

var c = canvas.node().getContext("2d");

var graticule = d3.geo.graticule();

var projection = d3.geo.orthographic()
    .clipAngle(90);

var path = d3.geo.path()
    .projection(projection)
    .context(c);

var svgPath = d3.geo.path()
    .projection(projection);

//
// Load topjson data
//

// Add helper methods to String
if (typeof String.prototype.endsWith !== 'function') {
    String.prototype.endsWith = function(suffix) {
        return this.indexOf(suffix, this.length - suffix.length) !== -1;
    };
}
String.prototype.removeRight = function(suffix) {
    return this.substring(0, this.length - suffix.length);
};

var land;

var queue = queue()
    .defer(d3.json, '../data/world-110m.json');

for (var key in dataToLoad) {
    queue.defer(d3.json, '../data/' + key + '.json');
    queue.defer(d3.json, '../data/' + key + '_buffer.json');
}

queue.awaitAll(ready);

function ready(error, results) {
    // Load world data
    var landResult = results.shift();
    land = topojson.feature(landResult, landResult.objects.land);

    // Load all other path + buffer data
    results.forEach(function(result) {
        for (var key in result.objects) {
            if (dataToLoad[key]) {
                dataToLoad[key]['feature'] = topojson.feature(result, result.objects[key]);
            }
            else if (key.endsWith('_buffer')) {
                parent = key.removeRight('_buffer');
                if (dataToLoad[parent]) {
                    dataToLoad[parent]['buffer'] = topojson.feature(result, result.objects[key]);
                }
            }
        }
    });

    // Draw buffers on svg
    drawBuffer('momdad');
    drawBuffer('transam');
    drawBuffer('at');
    drawBuffer('camino');

    // Add resize event handler and run for the first time
    d3.select(window).on('resize', resize);
    resize();

    // Add mousemove event handler for auto rotation
    svg.on("mousemove", function() {
        if (!isRotating) {
            if ( lonRotate === lon['us'] &&
                 d3.event.clientX > projection([lon['us-edge'], latCenter])[0] ) {
                // Scroll to Spain
                rotate(lon['spain']);
            }
            else if ( lonRotate === lon['spain'] &&
                      d3.event.clientX < projection([lon['spain-edge'], latCenter])[0] ) {
                // Scroll to US
                rotate(lon['us']);
            }
            else if ( lonRotate !== lon['us'] && lonRotate !== lon['spain'] ) {
                if ( d3.event.clientX < (width / 2) ) {
                    // Scroll to US
                    rotate(lon['us']);
                }
                else {
                    // Scroll to Spain
                    rotate(lon['spain']);
                }
            }
        }
    });
}

function drawBuffer(pathName) {
    var buffer = svg.append('a')
                    .attr('class', 'buffer')
                    .attr('xlink:href', dataToLoad[pathName]['url'])
                    .append('path')
                        .attr('id', pathName)
                        .datum(dataToLoad[pathName]['buffer']);

    var link = d3.select('.copy .' + pathName);
    link.style('transition-duration', transitionDurationStroke + 'ms');

    function setTextShadow(width) {
        var textShadow = '0px 0px ' + width + 'px';
        if (width > 0) {
            textShadow += ' ' + dataToLoad[pathName]['color'];
        }
        link.style('text-shadow', textShadow);
    }

    function setPathStroke(width) {
        d3.transition()
            .duration(transitionDurationStroke)
            .tween("width", function() {
                w = d3.interpolate(dataToLoad[pathName]['stroke-width'], width);
                return function(t) {
                    dataToLoad[pathName]['stroke-width'] = w(t);
                    draw();
                };
            });
    }

    function doMouseover() {
        if (!isRotating) {
            resetPathStrokes();
            setPathStroke(pathStyle['hover']['stroke-width']);
            rotate(dataToLoad[pathName]['center-lon'], pathName);
        }
        setTextShadow(blurRadius);
    }

    function doMouseout() {
        if (!isRotating) {
            setPathStroke(pathStyle['stroke-width']);
        }
        setTextShadow(0);
    }

    // Add event handlers to svg buffers and to html links
    buffer.on('mouseover', doMouseover);
    link.on('mouseover', doMouseover);

    buffer.on('mouseout', doMouseout);
    link.on('mouseout', doMouseout);
}

function resize() {
    // Clear whatever is currently drawn
    c.clearRect(0, 0, width, height);

    // Calculate dimensions to ensure entire page is always visible
    var nonMapHeight = parseInt(d3.select('.container').style('height')) +
                       parseInt(d3.select('#footer').style('height')) +
                       5;

    width = parseInt(map.style('width'));

    height = Math.min(
        Math.round(width * mapRatio),
        window.innerHeight - nonMapHeight
    );

    scale = Math.min(
        width * maxWidthRatio,
        height * maxHeightRatio
    );

    draw();
    drawSvg();
}

function draw() {
    projection
        .rotate([-lonRotate, -latRotate])
        .center([0, latBottom - latRotate])
        .scale(scale)
        .translate([width / 2, height]);

    canvas
        .attr("width", width)
        .attr("height", height);

    d3.select(self.frameElement).style('height', height + 'px');

    // Clear whatever is currently drawn
    c.clearRect(0, 0, width, height);

    c.strokeStyle = graticuleStyle['stroke'],
        c.lineWidth = graticuleStyle['stroke-width'],
        c.beginPath(),
        path(graticule()),
        c.stroke();
    c.fillStyle = c.createPattern(landStyle['background-image'], 'repeat'),
        c.strokeStyle = landStyle['stroke'],
        c.lineWidth = landStyle['stroke-width'],
        c.beginPath(),
        path(land),
        c.fill(),
        c.stroke();

    for (var key in dataToLoad) {
        c.strokeStyle = dataToLoad[key]['color'],
            c.lineWidth = dataToLoad[key]['stroke-width'],
            c.beginPath(), path(dataToLoad[key]['feature']),
            c.stroke();
    }

    // Draw shadow on canvas bottom
    var shadow = c.createLinearGradient(0, height - shadowHeight, 0, height + shadowHeight);
    shadow.addColorStop(0, 'transparent');
    shadow.addColorStop(1, colors['gray-dark']);
    c.fillStyle = shadow,
        c.fillRect(0, height - shadowHeight, width, height);
}

function drawSvg() {
    svg
        .attr('width', width)
        .attr('height', height);

    // Redraw SVG buffers
    for (var key in dataToLoad) {
        svg.select('#' + key).attr('d', svgPath);
    }
}

function rotate(to, pathName) {
    var from = -projection.rotate()[0];
    if (from !== to) {
        isRotating = true; // Disable all mouse events
        resetPathStrokes(); // Set all strokes to default width

        // If this rotate was caused by link mouseover, set stroke to hovered width
        if (pathName) {
            dataToLoad[pathName]['stroke-width'] = pathStyle['hover']['stroke-width'];
        }

        d3.transition()
            .duration(transitionDurationWorld)
            .tween("rotate", function() {
                r = d3.interpolate(from, to);
                return function(t) {
                    lonRotate = r(t);
                    draw();
                };
            })
            .each('end', function() {
                isRotating = false; // Enable events
                drawSvg(); // Move the invisible SVG buffers into place
            });
    }
}

function resetPathStrokes() {
    for (var key in dataToLoad) {
        dataToLoad[key]['stroke-width'] = pathStyle['stroke-width'];
    }
    draw();
}
<|endoftext|>


<commit_before>import React from 'react';
import {graphql, createRefetchContainer} from 'react-relay';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';

import IssueTimelineController from '../controllers/issue-timeline-controller';
import PrTimelineContainer from '../controllers/pr-timeline-controller';
import PrStatusesView from '../views/pr-statuses-view';
import Octicon from '../atom/octicon';
import IssueishBadge from '../views/issueish-badge';
import GithubDotcomMarkdown from '../views/github-dotcom-markdown';
import PeriodicRefresher from '../periodic-refresher';
import {EnableableOperationPropType} from '../prop-types';
import {autobind} from '../helpers';

const reactionTypeToEmoji = {
  THUMBS_UP: '👍',
  THUMBS_DOWN: '👎',
  LAUGH: '😆',
  HOORAY: '🎉',
  CONFUSED: '😕',
  HEART: '❤',
};

function createCheckoutState(name) {
  return function(cases) {
    return cases[name] || cases.default;
  };
}

export const checkoutStates = {
  HIDDEN: createCheckoutState('hidden'),
  DISABLED: createCheckoutState('disabled'),
  BUSY: createCheckoutState('busy'),
  CURRENT: createCheckoutState('current'),
};

export class BareIssueishDetailView extends React.Component {
  static propTypes = {
    relay: PropTypes.shape({
      refetch: PropTypes.func.isRequired,
    }),
    switchToIssueish: PropTypes.func.isRequired,
    checkoutOp: EnableableOperationPropType.isRequired,
    repository: PropTypes.shape({
      id: PropTypes.string.isRequired,
      name: PropTypes.string.isRequired,
      owner: PropTypes.shape({
        login: PropTypes.string,
      }),
    }),
    issueish: PropTypes.shape({
      __typename: PropTypes.string.isRequired,
      id: PropTypes.string.isRequired,
      title: PropTypes.string,
      countedCommits: PropTypes.shape({
        totalCount: PropTypes.number.isRequired,
      }).isRequired,
      isCrossRepository: PropTypes.bool,
      changedFiles: PropTypes.number.isRequired,
      url: PropTypes.string.isRequired,
      bodyHTML: PropTypes.string,
      number: PropTypes.number,
      state: PropTypes.oneOf([
        'OPEN', 'CLOSED', 'MERGED',
      ]).isRequired,
      author: PropTypes.shape({
        login: PropTypes.string.isRequired,
        avatarUrl: PropTypes.string.isRequired,
        url: PropTypes.string.isRequired,
      }).isRequired,
      reactionGroups: PropTypes.arrayOf(
        PropTypes.shape({
          content: PropTypes.string.isRequired,
          users: PropTypes.shape({
            totalCount: PropTypes.number.isRequired,
          }).isRequired,
        }),
      ).isRequired,
    }).isRequired,
  }

  state = {
    refreshing: false,
  }

  constructor(props) {
    super(props);
    autobind(this, 'handleRefreshClick', 'refresh');
  }

  componentDidMount() {
    this.refresher = new PeriodicRefresher(BareIssueishDetailView, {
      interval: () => 5 * 60 * 1000,
      getCurrentId: () => this.props.issueish.id,
      refresh: this.refresh,
      minimumIntervalPerId: 2 * 60 * 1000,
    });
    // auto-refresh disabled for now until pagination is handled
    // this.refresher.start();
  }

  componentWillUnmount() {
    this.refresher.destroy();
  }

  render() {
    const repo = this.props.repository;
    const issueish = this.props.issueish;
    const isPr = issueish.__typename === 'PullRequest';
    const childProps = {
      issue: issueish.__typename === 'Issue' ? issueish : null,
      pullRequest: issueish.__typename === 'PullRequest' ? issueish : null,
    };
    return (

                {issueish.title}

                <Octicon
                  icon="repo-sync"
                  className={cx('github-IssueishDetailView-headerRefreshButton', {refreshing: this.state.refreshing})}
                  onClick={this.handleRefreshClick}
                />

            {isPr &&

                {issueish.author.login} wants to merge{' '}
                {issueish.countedCommits.totalCount} commits and{' '}
                {issueish.changedFiles} changed files into{' '}
                {issueish.isCrossRepository ?
                  `${repo.owner.login}/${issueish.baseRefName}` : issueish.baseRefName} from{' '}
                {issueish.isCrossRepository ?
                  `${issueish.author.login}/${issueish.headRefName}` : issueish.headRefName}

}

                <IssueishBadge className="github-IssueishDetailView-headerBadge"
                  type={issueish.__typename}
                  state={issueish.state}
                />
                {repo.owner.login}/{repo.name}#{issueish.number}

                {isPr &&
                  <PrStatusesView pullRequest={issueish} displayType="check" />
                }

                {this.renderCheckoutButton()}

          {/* tabs start here! */}
          <Tabs>
            <TabList className="github-IssueishDetailView-tablist">
              <Tab>Conversation</Tab>
              <Tab>Commits</Tab>
              <Tab>Checks</Tab>
              <Tab>Files Changed</Tab>
            </TabList>

            {/* conversation */}
            <TabPanel>

                {issueish.reactionGroups.map(group => (
                  group.users.totalCount > 0
                    ?
                      {reactionTypeToEmoji[group.content]}   {group.users.totalCount}

                    : null
                ))}

            </TabPanel>

            {/* commits */}
            <TabPanel>
              {isPr ?
                <PrTimelineContainer
                  {...childProps}
                  switchToIssueish={this.props.switchToIssueish}
                /> :
                <IssueTimelineController
                  {...childProps}
                  switchToIssueish={this.props.switchToIssueish}
                />
              }
            </TabPanel>

            {/* checks */}
            <TabPanel>
              {isPr &&

                <PrStatusesView pullRequest={issueish} displayType="full" />

}
            </TabPanel>

            {/* files changed */}
            <TabPanel>
            </TabPanel>
          </Tabs>

          <GithubDotcomMarkdown
            html={issueish.bodyHTML || 'No description provided.'}
            switchToIssueish={this.props.switchToIssueish}
          />

            {repo.owner.login}/{repo.name}#{issueish.number}

    );
  }

  renderCheckoutButton() {
    const {checkoutOp} = this.props;
    let extraClass = null;
    let buttonText = 'Checkout';
    let buttonTitle = null;

    if (!checkoutOp.isEnabled()) {
      buttonTitle = checkoutOp.getMessage();
      const reason = checkoutOp.why();
      if (reason({hidden: true, default: false})) {
        return null;
      }

      buttonText = reason({
        current: 'Checked out',
        default: 'Checkout',
      });

      extraClass = 'github-IssueishDetailView-checkoutButton--' + reason({
        disabled: 'disabled',
        busy: 'busy',
        current: 'current',
      });
    }

    const classNames = cx('btn', 'btn-primary', 'github-IssueishDetailView-checkoutButton', extraClass);
    return (
      <button
        className={classNames}
        disabled={!checkoutOp.isEnabled()}
        title={buttonTitle}
        onClick={() => checkoutOp.run()}>
        {buttonText}
      </button>
    );
  }

  handleRefreshClick(e) {
    e.preventDefault();
    this.refresher.refreshNow(true);
  }

  refresh() {
    if (this.state.refreshing) {
      return;
    }

    this.setState({refreshing: true});
    this.props.relay.refetch({
      repoId: this.props.repository.id,
      issueishId: this.props.issueish.id,
      timelineCount: 100,
      timelineCursor: null,
    }, null, () => {
      this.setState({refreshing: false});
    }, {force: true});
  }
}

export default createRefetchContainer(BareIssueishDetailView, {
  repository: graphql`
    fragment issueishDetailView_repository on Repository {
      id
      name
      owner {
        login
      }
    }
  `,

  issueish: graphql`
    fragment issueishDetailView_issueish on IssueOrPullRequest
    @argumentDefinitions(
      timelineCount: {type: "Int!"},
      timelineCursor: {type: "String"}
    ) {
      __typename

      ... on Node {
        id
      }

      ... on Issue {
        state number title bodyHTML
        author {
          login avatarUrl
          ... on User { url }
          ... on Bot { url }
        }

        ...issueTimelineController_issue @arguments(timelineCount: $timelineCount, timelineCursor: $timelineCursor)
      }

      ... on PullRequest {
        isCrossRepository
        changedFiles
        countedCommits: commits {
          totalCount
        }
        ...prStatusesView_pullRequest
        state number title bodyHTML baseRefName headRefName
        author {
          login avatarUrl
          ... on User { url }
          ... on Bot { url }
        }

        ...prTimelineController_pullRequest @arguments(timelineCount: $timelineCount, timelineCursor: $timelineCursor)
      }

      ... on UniformResourceLocatable { url }

      ... on Reactable {
        reactionGroups {
          content users { totalCount }
        }
      }
    }
  `,
}, graphql`
  query issueishDetailViewRefetchQuery
  (
    $repoId: ID!, $issueishId: ID!, $timelineCount: Int!, $timelineCursor: String
  ) {
    repository:node(id: $repoId) {
      ...issueishDetailView_repository @arguments(timelineCount: $timelineCount, timelineCursor: $timelineCursor)
    }

    issueish:node(id: $issueishId) {
      ...issueishDetailView_issueish @arguments(timelineCount: $timelineCount, timelineCursor: $timelineCursor)
    }
  }
`);
<commit_msg>move timeline to conversation tab<commit_after>import React from 'react';
import {graphql, createRefetchContainer} from 'react-relay';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';

import IssueTimelineController from '../controllers/issue-timeline-controller';
import PrTimelineContainer from '../controllers/pr-timeline-controller';
import PrStatusesView from '../views/pr-statuses-view';
import Octicon from '../atom/octicon';
import IssueishBadge from '../views/issueish-badge';
import GithubDotcomMarkdown from '../views/github-dotcom-markdown';
import PeriodicRefresher from '../periodic-refresher';
import {EnableableOperationPropType} from '../prop-types';
import {autobind} from '../helpers';

const reactionTypeToEmoji = {
  THUMBS_UP: '👍',
  THUMBS_DOWN: '👎',
  LAUGH: '😆',
  HOORAY: '🎉',
  CONFUSED: '😕',
  HEART: '❤',
};

function createCheckoutState(name) {
  return function(cases) {
    return cases[name] || cases.default;
  };
}

export const checkoutStates = {
  HIDDEN: createCheckoutState('hidden'),
  DISABLED: createCheckoutState('disabled'),
  BUSY: createCheckoutState('busy'),
  CURRENT: createCheckoutState('current'),
};

export class BareIssueishDetailView extends React.Component {
  static propTypes = {
    relay: PropTypes.shape({
      refetch: PropTypes.func.isRequired,
    }),
    switchToIssueish: PropTypes.func.isRequired,
    checkoutOp: EnableableOperationPropType.isRequired,
    repository: PropTypes.shape({
      id: PropTypes.string.isRequired,
      name: PropTypes.string.isRequired,
      owner: PropTypes.shape({
        login: PropTypes.string,
      }),
    }),
    issueish: PropTypes.shape({
      __typename: PropTypes.string.isRequired,
      id: PropTypes.string.isRequired,
      title: PropTypes.string,
      countedCommits: PropTypes.shape({
        totalCount: PropTypes.number.isRequired,
      }).isRequired,
      isCrossRepository: PropTypes.bool,
      changedFiles: PropTypes.number.isRequired,
      url: PropTypes.string.isRequired,
      bodyHTML: PropTypes.string,
      number: PropTypes.number,
      state: PropTypes.oneOf([
        'OPEN', 'CLOSED', 'MERGED',
      ]).isRequired,
      author: PropTypes.shape({
        login: PropTypes.string.isRequired,
        avatarUrl: PropTypes.string.isRequired,
        url: PropTypes.string.isRequired,
      }).isRequired,
      reactionGroups: PropTypes.arrayOf(
        PropTypes.shape({
          content: PropTypes.string.isRequired,
          users: PropTypes.shape({
            totalCount: PropTypes.number.isRequired,
          }).isRequired,
        }),
      ).isRequired,
    }).isRequired,
  }

  state = {
    refreshing: false,
  }

  constructor(props) {
    super(props);
    autobind(this, 'handleRefreshClick', 'refresh');
  }

  componentDidMount() {
    this.refresher = new PeriodicRefresher(BareIssueishDetailView, {
      interval: () => 5 * 60 * 1000,
      getCurrentId: () => this.props.issueish.id,
      refresh: this.refresh,
      minimumIntervalPerId: 2 * 60 * 1000,
    });
    // auto-refresh disabled for now until pagination is handled
    // this.refresher.start();
  }

  componentWillUnmount() {
    this.refresher.destroy();
  }

  render() {
    const repo = this.props.repository;
    const issueish = this.props.issueish;
    const isPr = issueish.__typename === 'PullRequest';
    const childProps = {
      issue: issueish.__typename === 'Issue' ? issueish : null,
      pullRequest: issueish.__typename === 'PullRequest' ? issueish : null,
    };
    return (

                {issueish.title}

                <Octicon
                  icon="repo-sync"
                  className={cx('github-IssueishDetailView-headerRefreshButton', {refreshing: this.state.refreshing})}
                  onClick={this.handleRefreshClick}
                />

            {isPr &&

                {issueish.author.login} wants to merge{' '}
                {issueish.countedCommits.totalCount} commits and{' '}
                {issueish.changedFiles} changed files into{' '}
                {issueish.isCrossRepository ?
                  `${repo.owner.login}/${issueish.baseRefName}` : issueish.baseRefName} from{' '}
                {issueish.isCrossRepository ?
                  `${issueish.author.login}/${issueish.headRefName}` : issueish.headRefName}

}

                <IssueishBadge className="github-IssueishDetailView-headerBadge"
                  type={issueish.__typename}
                  state={issueish.state}
                />
                {repo.owner.login}/{repo.name}#{issueish.number}

                {isPr &&
                  <PrStatusesView pullRequest={issueish} displayType="check" />
                }

                {this.renderCheckoutButton()}

          {/* tabs start here! */}
          <Tabs>
            <TabList className="github-IssueishDetailView-tablist">
              <Tab>Conversation</Tab>
              <Tab>Commits</Tab>
              <Tab>Checks</Tab>
              <Tab>Files Changed</Tab>
            </TabList>

            {/* conversation */}
            <TabPanel>

                {issueish.reactionGroups.map(group => (
                  group.users.totalCount > 0
                    ?
                      {reactionTypeToEmoji[group.content]}   {group.users.totalCount}

                    : null
                ))}

              <GithubDotcomMarkdown
                html={issueish.bodyHTML || 'No description provided.'}
                switchToIssueish={this.props.switchToIssueish}
              />
              {isPr ?
                <PrTimelineContainer
                  {...childProps}
                  switchToIssueish={this.props.switchToIssueish}
                /> :
                <IssueTimelineController
                  {...childProps}
                  switchToIssueish={this.props.switchToIssueish}
                />
              }
            </TabPanel>

            {/* commits */}
            <TabPanel>
            </TabPanel>

            {/* checks */}
            <TabPanel>
              {isPr &&

                <PrStatusesView pullRequest={issueish} displayType="full" />

}
            </TabPanel>

            {/* files changed */}
            <TabPanel>
            </TabPanel>
          </Tabs>

            {repo.owner.login}/{repo.name}#{issueish.number}

    );
  }

  renderCheckoutButton() {
    const {checkoutOp} = this.props;
    let extraClass = null;
    let buttonText = 'Checkout';
    let buttonTitle = null;

    if (!checkoutOp.isEnabled()) {
      buttonTitle = checkoutOp.getMessage();
      const reason = checkoutOp.why();
      if (reason({hidden: true, default: false})) {
        return null;
      }

      buttonText = reason({
        current: 'Checked out',
        default: 'Checkout',
      });

      extraClass = 'github-IssueishDetailView-checkoutButton--' + reason({
        disabled: 'disabled',
        busy: 'busy',
        current: 'current',
      });
    }

    const classNames = cx('btn', 'btn-primary', 'github-IssueishDetailView-checkoutButton', extraClass);
    return (
      <button
        className={classNames}
        disabled={!checkoutOp.isEnabled()}
        title={buttonTitle}
        onClick={() => checkoutOp.run()}>
        {buttonText}
      </button>
    );
  }

  handleRefreshClick(e) {
    e.preventDefault();
    this.refresher.refreshNow(true);
  }

  refresh() {
    if (this.state.refreshing) {
      return;
    }

    this.setState({refreshing: true});
    this.props.relay.refetch({
      repoId: this.props.repository.id,
      issueishId: this.props.issueish.id,
      timelineCount: 100,
      timelineCursor: null,
    }, null, () => {
      this.setState({refreshing: false});
    }, {force: true});
  }
}

export default createRefetchContainer(BareIssueishDetailView, {
  repository: graphql`
    fragment issueishDetailView_repository on Repository {
      id
      name
      owner {
        login
      }
    }
  `,

  issueish: graphql`
    fragment issueishDetailView_issueish on IssueOrPullRequest
    @argumentDefinitions(
      timelineCount: {type: "Int!"},
      timelineCursor: {type: "String"}
    ) {
      __typename

      ... on Node {
        id
      }

      ... on Issue {
        state number title bodyHTML
        author {
          login avatarUrl
          ... on User { url }
          ... on Bot { url }
        }

        ...issueTimelineController_issue @arguments(timelineCount: $timelineCount, timelineCursor: $timelineCursor)
      }

      ... on PullRequest {
        isCrossRepository
        changedFiles
        countedCommits: commits {
          totalCount
        }
        ...prStatusesView_pullRequest
        state number title bodyHTML baseRefName headRefName
        author {
          login avatarUrl
          ... on User { url }
          ... on Bot { url }
        }

        ...prTimelineController_pullRequest @arguments(timelineCount: $timelineCount, timelineCursor: $timelineCursor)
      }

      ... on UniformResourceLocatable { url }

      ... on Reactable {
        reactionGroups {
          content users { totalCount }
        }
      }
    }
  `,
}, graphql`
  query issueishDetailViewRefetchQuery
  (
    $repoId: ID!, $issueishId: ID!, $timelineCount: Int!, $timelineCursor: String
  ) {
    repository:node(id: $repoId) {
      ...issueishDetailView_repository @arguments(timelineCount: $timelineCount, timelineCursor: $timelineCursor)
    }

    issueish:node(id: $issueishId) {
      ...issueishDetailView_issueish @arguments(timelineCount: $timelineCount, timelineCursor: $timelineCursor)
    }
  }
`);
<|endoftext|>