<commit_before>'use strict';

const getRoute = require('./get');
const updateRoute = require('./update');
const createRoute = require('./create');
const stepGetRoute = require('./steps/get');
const artifactGetRoute = require('./artifacts/get');
const stepUpdateRoute = require('./steps/update');
const stepLogsRoute = require('./steps/logs');
const listSecretsRoute = require('./listSecrets');
const tokenRoute = require('./token');
const metricsRoute = require('./metrics');
const workflowParser = require('screwdriver-workflow-parser');

/**
 * Create the build. If config.start is false or not passed in then do not start the job
 * @method createBuild
 * @param  {Object}   config                Configuration object
 * @param  {Factory}  config.jobFactory     Job Factory
 * @param  {Factory}  config.buildFactory   Build Factory
 * @param  {Factory}  config.eventFactory   Event Factory
 * @param  {Number}   config.pipelineId     Pipeline Id
 * @param  {String}   config.jobName        Job name
 * @param  {String}   config.username       Username of build
 * @param  {String}   config.scmContext     SCM context
 * @param  {Build}    config.build          Build object
 * @param  {Boolean}  [config.start]        Whether to start the build or not
 * @return {Promise}
 */
function createBuild({ jobFactory, buildFactory, eventFactory, pipelineId, jobName, username,
    scmContext, build, start }) {
    return Promise.all([
        eventFactory.get(build.eventId),
        jobFactory.get({
            name: jobName,
            pipelineId
        })
    ]).then(([event, job]) => {
        if (job.state === 'ENABLED') {
            return buildFactory.create({
                jobId: job.id,
                sha: build.sha,
                parentBuildId: build.id,
                eventId: build.eventId,
                username,
                configPipelineSha: event.configPipelineSha,
                scmContext,
                start: start !== false
            });
        }

        return null;
    });
}

/**
 * Check if all the jobs in joinList are successful
 * @method isJoinDone
 * @param  {Array}      joinList       array of jobs(name,id) that are in join
 * @param  {Array}      finishedBuilds array of finished builds belong to this event
 * @return {Boolean}                   whether all the jobs in join are successful
 */
function isJoinDone(joinList, finishedBuilds) {
    const successBuilds = finishedBuilds.filter(b => b.status === 'SUCCESS').map(b => b.jobId);
    const successBuildsInJoin = joinList.filter(j => successBuilds.includes(j.id));

    return successBuildsInJoin.length === joinList.length;
}

/**
 * Check if there is no failures so far in the finishedBuilds
 * @method noFailureSoFar
 * @param  {Array}      joinList       array of jobs(name,id) that are in join
 * @param  {Array}      finishedBuilds array of finished builds belong to this event
 * @return {Boolean}                   whether there is no failure so far
 */
function noFailureSoFar(joinList, finishedBuilds) {
    const failedBuilds = finishedBuilds
        .filter(b => b.status === 'FAILURE' || b.status === 'ABORTED')
        .map(b => b.jobId);
    const failedBuildsInJoin = joinList.filter(j => failedBuilds.includes(j.id));

    return failedBuildsInJoin.length === 0;
}

/**
 * Return the successBuildsInJoinList
 * @method successBuildsInJoinList
 * @param  {Array}      joinList       array of jobs(name,id) that are in join
 * @param  {Array}      finishedBuilds array of finished builds belong to this event
 * @return {Array}                     success builds in join
 */
function successBuildsInJoinList(joinList, finishedBuilds) {
    const successBuilds = finishedBuilds
        .filter(b => b.status === 'SUCCESS')
        .map(b => ({ id: b.id, jobId: b.jobId }));

    const joinListJobIds = joinList.map(j => j.id);

    return successBuilds.filter(b => joinListJobIds.includes(b.jobId));
}

/**
 * Handle next build logic: create, update, start, or remove
 * @method handleNextBuild
 * @param  {Object}   config                    configuration object
 * @param  {Object}   config.buildConfig        config to create the build with
 * @param  {Array}    config.joinList           list of job that join on this current job
 * @param  {Array}    config.finishedBuilds     list of finished builds
 * @param  {String}   config.jobName            jobname for this build
 * @return {Promise}  the newly updated/created build
 */
function handleNextBuild({ buildConfig, joinList, finishedBuilds, jobId }) {
    return Promise.resolve().then(() => {
        const noFailedBuilds = noFailureSoFar(joinList, finishedBuilds);
        const nextBuild = finishedBuilds.filter(b => b.jobId === jobId)[0];

        // If anything failed so far, delete if nextBuild was created previously, or do nothing otherwise
        // [A B] -> C. A passed -> C created; B failed -> delete C
        // [A B] -> C. A failed -> C not created; B failed -> do nothing
        // [A B D] -> C. A passed -> C created; B failed -> delete C; D passed -> do nothing
        if (!noFailedBuilds) {
            return nextBuild ? nextBuild.remove() : null;
        }

        // If everything successful so far, create or update
        // [A B] -> C. A passed -> create C
        // [A B] -> C. A passed -> C created; B passed -> update C
        if (!nextBuild) {
            buildConfig.start = false;

            return createBuild(buildConfig);
        }

        // If build is already created, update the parentBuildId
        const successBuildsIds = successBuildsInJoinList(joinList, finishedBuilds)
            .map(b => b.id);

        nextBuild.parentBuildId = successBuildsIds;

        return nextBuild.update();
    }).then((b) => {
        const done = isJoinDone(joinList, finishedBuilds);

        if (!done) {
            return null;
        }

        b.status = 'QUEUED';

        return b.update()
            .then(newBuild => newBuild.start());
    });
}

/**
 * DFS the workflowGraph from the start point
 * @method dfs
 * @param  {Object} workflowGraph   workflowGraph
 * @param  {String} start           Start job name
 * @param  {Array} builds           An array of builds
 * @param  {Set} visited            A set to store visited build ids
 * @return {Set}                    A set of build ids that are visited
 */
function dfs(workflowGraph, start, builds, visited) {
    const jobId = workflowGraph.nodes.find(node => node.name === start).id;
    const nextJobs = workflowParser.getNextJobs(workflowGraph, { trigger: start });

    // If the start job has no build in parentEvent then just return
    if (!builds.find(build => build.jobId === jobId)) {
        return visited;
    }

    visited.add(builds.find(build => build.jobId === jobId).id);
    nextJobs.forEach(job => dfs(workflowGraph, job, builds, visited));

    return visited;
}

/**
 * Remove startFrom and all downstream builds from startFrom
 * @method removeDownstreamBuilds
 * @param  {Object} config
 * @param  {Array}  config.builds         An array of all builds from the parent event
 * @param  {String} config.startFrom      Job name to start the event from
 * @param  {Object} config.parentEvent    The parent event model
 * @return {Array}                        An array of upstream builds to be rerun
 */
function removeDownstreamBuilds(config) {
    const { builds, startFrom, parentEvent } = config;
    const visitedBuilds = dfs(parentEvent.workflowGraph, startFrom, builds, new Set());

    return builds.filter(build => !visitedBuilds.has(build.id));
}

/**
 * Build API Plugin
 * @method register
 * @param  {Hapi}     server                Hapi Server
 * @param  {Object}   options               Configuration
 * @param  {String}   options.logBaseUrl    Log service's base URL
 * @param  {Function} next                  Function to call when done
 */
exports.register = (server, options, next) => {
    /**
     * Create event for downstream pipeline that need to be rebuilt
     * @method triggerEvent
     * @param {Object}  config               Configuration object
     * @param {String}  config.pipelineId    Pipeline to be rebuilt
     * @param {String}  config.startFrom     Job to be rebuilt
     * @param {String}  config.causeMessage  Caused message, e.g. triggered by 1234(buildId)
     * @param {String}  config.parentBuildId ID of the build that triggers this event
     * @return {Promise}                     Resolves to the newly created event
     */
    server.expose('triggerEvent', (config) => {
        const { pipelineId, startFrom, causeMessage, parentBuildId } = config;
        const eventFactory = server.root.app.eventFactory;
        const pipelineFactory = server.root.app.pipelineFactory;
        const scm = eventFactory.scm;

        const payload = {
            pipelineId,
            startFrom,
            type: 'pipeline',
            causeMessage,
            parentBuildId
        };

        return pipelineFactory.get(pipelineId)
            .then(pipeline => pipeline.admin
                .then((realAdmin) => {
                    const scmUri = pipeline.scmUri;
                    const scmContext = pipeline.scmContext;

                    payload.scmContext = scmContext;
                    payload.username = realAdmin.username;

                    // get pipeline admin's token
                    return realAdmin.unsealToken()
                        .then((token) => {
                            const scmConfig = {
                                scmContext,
                                scmUri,
                                token
                            };

                            // Get commit sha
                            return scm.getCommitSha(scmConfig)
                                .then((sha) => {
                                    payload.sha = sha;

                                    return eventFactory.create(payload);
                                });
                        });
                }));
    });

    /**
     * Trigger the next jobs of the current job
     * @method triggerNextJobs
     * @param {Object}      config              Configuration object
     * @param {Pipeline}    config.pipeline     Current pipeline
     * @param {Job}         config.job          Current job
     * @param {Build}       config.build        Current build
     * @param {String}      config.username     Username
     * @param {String}      config.scmContext   scm context
     * @return {Promise}                        Resolves to the newly created build or null
     */
    server.expose('triggerNextJobs', (config) => {
        const { pipeline, job, build, username, scmContext } = config;
        const eventFactory = server.root.app.eventFactory;
        const jobFactory = server.root.app.jobFactory;
        const buildFactory = server.root.app.buildFactory;
        const currentJobName = job.name;
        const pipelineId = pipeline.id;

        return eventFactory.get({ id: build.eventId }).then((event) => {
            const workflowGraph = event.workflowGraph;
            const nextJobs = workflowParser.getNextJobs(workflowGraph, { trigger: currentJobName });

            // Create a join object like: {A:[B,C], D:[B,F]} where [B,C] join on A, [B,F] join on D, etc.
            const joinObj = nextJobs.reduce((obj, jobName) => {
                obj[jobName] = workflowParser.getSrcForJoin(workflowGraph, { jobName });

                return obj;
            }, {});

            return Promise.all(Object.keys(joinObj).map((nextJobName) => {
                const joinList = joinObj[nextJobName];
                const joinListNames = joinList.map(j => j.name);
                const buildConfig = {
                    jobFactory,
                    buildFactory,
                    eventFactory,
                    pipelineId,
                    jobName: nextJobName,
                    username,
                    scmContext,
                    build // this is the parentBuild for the next build
                };

                // Just start the build if falls in to these 2 scenarios
                // 1. No join
                // 2. ([~D,B,C]->A) currentJob=D, nextJob=A, joinList(A)=[B,C]
                //    joinList doesn't include C, so start A
                if (joinList.length === 0 || !joinListNames.includes(currentJobName)) {
                    return createBuild(buildConfig);
                }

                return Promise.resolve().then(() => {
                    if (!event.parentEventId) {
                        return event.getBuilds();
                    }

                    // If parent event id, merge parent build status data and
                    // rerun all builds in the path of the startFrom
                    return eventFactory.get({ id: event.parentEventId })
                        .then(parentEvent => parentEvent.getBuilds()
                            .then(parentBuilds => removeDownstreamBuilds({
                                builds: parentBuilds,
                                startFrom: event.startFrom,
                                parentEvent
                            }))
                        )
                        .then(upstreamBuilds => event.getBuilds()
                            .then(builds => builds.concat(upstreamBuilds)));
                }).then(finishedBuilds => handleNextBuild({
                    buildConfig,
                    joinList,
                    finishedBuilds,
                    jobId: workflowGraph.nodes.find(node => node.name === nextJobName).id
                }));
            }));
        });
    });

    server.route([
        getRoute(),
        updateRoute(),
        createRoute(),
        // Steps
        stepGetRoute(),
        stepUpdateRoute(),
        stepLogsRoute(options),
        // Secrets
        listSecretsRoute(),
        tokenRoute(),
        metricsRoute(),
        artifactGetRoute(options)
    ]);

    next();
};

exports.register.attributes = {
    name: 'builds'
};
<commit_msg>fix(1010): Add chainPR value to getNextJobs function (#1539)<commit_after>'use strict';

const getRoute = require('./get');
const updateRoute = require('./update');
const createRoute = require('./create');
const stepGetRoute = require('./steps/get');
const artifactGetRoute = require('./artifacts/get');
const stepUpdateRoute = require('./steps/update');
const stepLogsRoute = require('./steps/logs');
const listSecretsRoute = require('./listSecrets');
const tokenRoute = require('./token');
const metricsRoute = require('./metrics');
const workflowParser = require('screwdriver-workflow-parser');

/**
 * Create the build. If config.start is false or not passed in then do not start the job
 * @method createBuild
 * @param  {Object}   config                Configuration object
 * @param  {Factory}  config.jobFactory     Job Factory
 * @param  {Factory}  config.buildFactory   Build Factory
 * @param  {Factory}  config.eventFactory   Event Factory
 * @param  {Number}   config.pipelineId     Pipeline Id
 * @param  {String}   config.jobName        Job name
 * @param  {String}   config.username       Username of build
 * @param  {String}   config.scmContext     SCM context
 * @param  {Build}    config.build          Build object
 * @param  {Boolean}  [config.start]        Whether to start the build or not
 * @return {Promise}
 */
function createBuild({ jobFactory, buildFactory, eventFactory, pipelineId, jobName, username,
    scmContext, build, start }) {
    return Promise.all([
        eventFactory.get(build.eventId),
        jobFactory.get({
            name: jobName,
            pipelineId
        })
    ]).then(([event, job]) => {
        if (job.state === 'ENABLED') {
            return buildFactory.create({
                jobId: job.id,
                sha: build.sha,
                parentBuildId: build.id,
                eventId: build.eventId,
                username,
                configPipelineSha: event.configPipelineSha,
                scmContext,
                start: start !== false
            });
        }

        return null;
    });
}

/**
 * Check if all the jobs in joinList are successful
 * @method isJoinDone
 * @param  {Array}      joinList       array of jobs(name,id) that are in join
 * @param  {Array}      finishedBuilds array of finished builds belong to this event
 * @return {Boolean}                   whether all the jobs in join are successful
 */
function isJoinDone(joinList, finishedBuilds) {
    const successBuilds = finishedBuilds.filter(b => b.status === 'SUCCESS').map(b => b.jobId);
    const successBuildsInJoin = joinList.filter(j => successBuilds.includes(j.id));

    return successBuildsInJoin.length === joinList.length;
}

/**
 * Check if there is no failures so far in the finishedBuilds
 * @method noFailureSoFar
 * @param  {Array}      joinList       array of jobs(name,id) that are in join
 * @param  {Array}      finishedBuilds array of finished builds belong to this event
 * @return {Boolean}                   whether there is no failure so far
 */
function noFailureSoFar(joinList, finishedBuilds) {
    const failedBuilds = finishedBuilds
        .filter(b => b.status === 'FAILURE' || b.status === 'ABORTED')
        .map(b => b.jobId);
    const failedBuildsInJoin = joinList.filter(j => failedBuilds.includes(j.id));

    return failedBuildsInJoin.length === 0;
}

/**
 * Return the successBuildsInJoinList
 * @method successBuildsInJoinList
 * @param  {Array}      joinList       array of jobs(name,id) that are in join
 * @param  {Array}      finishedBuilds array of finished builds belong to this event
 * @return {Array}                     success builds in join
 */
function successBuildsInJoinList(joinList, finishedBuilds) {
    const successBuilds = finishedBuilds
        .filter(b => b.status === 'SUCCESS')
        .map(b => ({ id: b.id, jobId: b.jobId }));

    const joinListJobIds = joinList.map(j => j.id);

    return successBuilds.filter(b => joinListJobIds.includes(b.jobId));
}

/**
 * Handle next build logic: create, update, start, or remove
 * @method handleNextBuild
 * @param  {Object}   config                    configuration object
 * @param  {Object}   config.buildConfig        config to create the build with
 * @param  {Array}    config.joinList           list of job that join on this current job
 * @param  {Array}    config.finishedBuilds     list of finished builds
 * @param  {String}   config.jobName            jobname for this build
 * @return {Promise}  the newly updated/created build
 */
function handleNextBuild({ buildConfig, joinList, finishedBuilds, jobId }) {
    return Promise.resolve().then(() => {
        const noFailedBuilds = noFailureSoFar(joinList, finishedBuilds);
        const nextBuild = finishedBuilds.filter(b => b.jobId === jobId)[0];

        // If anything failed so far, delete if nextBuild was created previously, or do nothing otherwise
        // [A B] -> C. A passed -> C created; B failed -> delete C
        // [A B] -> C. A failed -> C not created; B failed -> do nothing
        // [A B D] -> C. A passed -> C created; B failed -> delete C; D passed -> do nothing
        if (!noFailedBuilds) {
            return nextBuild ? nextBuild.remove() : null;
        }

        // If everything successful so far, create or update
        // [A B] -> C. A passed -> create C
        // [A B] -> C. A passed -> C created; B passed -> update C
        if (!nextBuild) {
            buildConfig.start = false;

            return createBuild(buildConfig);
        }

        // If build is already created, update the parentBuildId
        const successBuildsIds = successBuildsInJoinList(joinList, finishedBuilds)
            .map(b => b.id);

        nextBuild.parentBuildId = successBuildsIds;

        return nextBuild.update();
    }).then((b) => {
        const done = isJoinDone(joinList, finishedBuilds);

        if (!done) {
            return null;
        }

        b.status = 'QUEUED';

        return b.update()
            .then(newBuild => newBuild.start());
    });
}

/**
 * DFS the workflowGraph from the start point
 * @method dfs
 * @param  {Object} workflowGraph   workflowGraph
 * @param  {String} start           Start job name
 * @param  {Array} builds           An array of builds
 * @param  {Set} visited            A set to store visited build ids
 * @return {Set}                    A set of build ids that are visited
 */
function dfs(workflowGraph, start, builds, visited) {
    const jobId = workflowGraph.nodes.find(node => node.name === start).id;
    const nextJobs = workflowParser.getNextJobs(workflowGraph, { trigger: start });

    // If the start job has no build in parentEvent then just return
    if (!builds.find(build => build.jobId === jobId)) {
        return visited;
    }

    visited.add(builds.find(build => build.jobId === jobId).id);
    nextJobs.forEach(job => dfs(workflowGraph, job, builds, visited));

    return visited;
}

/**
 * Remove startFrom and all downstream builds from startFrom
 * @method removeDownstreamBuilds
 * @param  {Object} config
 * @param  {Array}  config.builds         An array of all builds from the parent event
 * @param  {String} config.startFrom      Job name to start the event from
 * @param  {Object} config.parentEvent    The parent event model
 * @return {Array}                        An array of upstream builds to be rerun
 */
function removeDownstreamBuilds(config) {
    const { builds, startFrom, parentEvent } = config;
    const visitedBuilds = dfs(parentEvent.workflowGraph, startFrom, builds, new Set());

    return builds.filter(build => !visitedBuilds.has(build.id));
}

/**
 * Build API Plugin
 * @method register
 * @param  {Hapi}     server                Hapi Server
 * @param  {Object}   options               Configuration
 * @param  {String}   options.logBaseUrl    Log service's base URL
 * @param  {Function} next                  Function to call when done
 */
exports.register = (server, options, next) => {
    /**
     * Create event for downstream pipeline that need to be rebuilt
     * @method triggerEvent
     * @param {Object}  config               Configuration object
     * @param {String}  config.pipelineId    Pipeline to be rebuilt
     * @param {String}  config.startFrom     Job to be rebuilt
     * @param {String}  config.causeMessage  Caused message, e.g. triggered by 1234(buildId)
     * @param {String}  config.parentBuildId ID of the build that triggers this event
     * @return {Promise}                     Resolves to the newly created event
     */
    server.expose('triggerEvent', (config) => {
        const { pipelineId, startFrom, causeMessage, parentBuildId } = config;
        const eventFactory = server.root.app.eventFactory;
        const pipelineFactory = server.root.app.pipelineFactory;
        const scm = eventFactory.scm;

        const payload = {
            pipelineId,
            startFrom,
            type: 'pipeline',
            causeMessage,
            parentBuildId
        };

        return pipelineFactory.get(pipelineId)
            .then(pipeline => pipeline.admin
                .then((realAdmin) => {
                    const scmUri = pipeline.scmUri;
                    const scmContext = pipeline.scmContext;

                    payload.scmContext = scmContext;
                    payload.username = realAdmin.username;

                    // get pipeline admin's token
                    return realAdmin.unsealToken()
                        .then((token) => {
                            const scmConfig = {
                                scmContext,
                                scmUri,
                                token
                            };

                            // Get commit sha
                            return scm.getCommitSha(scmConfig)
                                .then((sha) => {
                                    payload.sha = sha;

                                    return eventFactory.create(payload);
                                });
                        });
                }));
    });

    /**
     * Trigger the next jobs of the current job
     * @method triggerNextJobs
     * @param {Object}      config              Configuration object
     * @param {Pipeline}    config.pipeline     Current pipeline
     * @param {Job}         config.job          Current job
     * @param {Build}       config.build        Current build
     * @param {String}      config.username     Username
     * @param {String}      config.scmContext   scm context
     * @return {Promise}                        Resolves to the newly created build or null
     */
    server.expose('triggerNextJobs', (config) => {
        const { pipeline, job, build, username, scmContext } = config;
        const eventFactory = server.root.app.eventFactory;
        const jobFactory = server.root.app.jobFactory;
        const buildFactory = server.root.app.buildFactory;
        const currentJobName = job.name;
        const pipelineId = pipeline.id;

        return eventFactory.get({ id: build.eventId }).then((event) => {
            const workflowGraph = event.workflowGraph;
            const nextJobs = workflowParser.getNextJobs(workflowGraph,
                { trigger: currentJobName, prChain: pipeline.prChain });

            // Create a join object like: {A:[B,C], D:[B,F]} where [B,C] join on A, [B,F] join on D, etc.
            const joinObj = nextJobs.reduce((obj, jobName) => {
                obj[jobName] = workflowParser.getSrcForJoin(workflowGraph, { jobName });

                return obj;
            }, {});

            return Promise.all(Object.keys(joinObj).map((nextJobName) => {
                const joinList = joinObj[nextJobName];
                const joinListNames = joinList.map(j => j.name);
                const buildConfig = {
                    jobFactory,
                    buildFactory,
                    eventFactory,
                    pipelineId,
                    jobName: nextJobName,
                    username,
                    scmContext,
                    build // this is the parentBuild for the next build
                };

                // Just start the build if falls in to these 2 scenarios
                // 1. No join
                // 2. ([~D,B,C]->A) currentJob=D, nextJob=A, joinList(A)=[B,C]
                //    joinList doesn't include C, so start A
                if (joinList.length === 0 || !joinListNames.includes(currentJobName)) {
                    return createBuild(buildConfig);
                }

                return Promise.resolve().then(() => {
                    if (!event.parentEventId) {
                        return event.getBuilds();
                    }

                    // If parent event id, merge parent build status data and
                    // rerun all builds in the path of the startFrom
                    return eventFactory.get({ id: event.parentEventId })
                        .then(parentEvent => parentEvent.getBuilds()
                            .then(parentBuilds => removeDownstreamBuilds({
                                builds: parentBuilds,
                                startFrom: event.startFrom,
                                parentEvent
                            }))
                        )
                        .then(upstreamBuilds => event.getBuilds()
                            .then(builds => builds.concat(upstreamBuilds)));
                }).then(finishedBuilds => handleNextBuild({
                    buildConfig,
                    joinList,
                    finishedBuilds,
                    jobId: workflowGraph.nodes.find(node => node.name === nextJobName).id
                }));
            }));
        });
    });

    server.route([
        getRoute(),
        updateRoute(),
        createRoute(),
        // Steps
        stepGetRoute(),
        stepUpdateRoute(),
        stepLogsRoute(options),
        // Secrets
        listSecretsRoute(),
        tokenRoute(),
        metricsRoute(),
        artifactGetRoute(options)
    ]);

    next();
};

exports.register.attributes = {
    name: 'builds'
};
<|endoftext|>


<commit_before>// https://github.com/d3/d3-plugins/blob/master/box/box.js
(function () {

    // Inspired by http://informationandvisualization.de/blog/box-plot
    d3.box = function () {
        var width = 1,
            height = 1,
            duration = 0,
            delay = 0,
            domain = null,
            value = Number,
            whiskers = boxWhiskers,
            quartiles = boxQuartiles,
            tickFormat = null;

        // For each small multiple…
        function box (g) {
            g.each(function (d, i) {
                d = d.map(value).sort(d3.ascending);
                var g = d3.select(this),
                    n = d.length,
                    min = d[0],
                    max = d[n - 1];

                // Compute quartiles. Must return exactly 3 elements.
                var quartileData = d.quartiles = quartiles(d);

                // Compute whiskers. Must return exactly 2 elements, or null.
                var whiskerIndices = whiskers && whiskers.call(this, d, i),
                    whiskerData = whiskerIndices && whiskerIndices.map(function (i) { return d[i]; });

                // Compute outliers. If no whiskers are specified, all data are 'outliers'.
                // We compute the outliers as indices, so that we can join across transitions!
                var outlierIndices = whiskerIndices ?
                    d3.range(0, whiskerIndices[0]).concat(d3.range(whiskerIndices[1] + 1, n)) : d3.range(n);

                // Compute the new x-scale.
                var x1 = d3.scale.linear()
                    .domain(domain && domain.call(this, d, i) || [min, max])
                    .range([height, 0]);

                // Retrieve the old x-scale, if this is an update.
                var x0 = this.__chart__ || d3.scale.linear()
                    .domain([0, Infinity])
                    .range(x1.range());

                // Stash the new scale.
                this.__chart__ = x1;

                // Note: the box, median, and box tick elements are fixed in number,
                // so we only have to handle enter and update. In contrast, the outliers
                // and other elements are variable, so we need to exit them! Variable
                // elements also fade in and out.

                // Update center line: the vertical line spanning the whiskers.
                var center = g.selectAll('line.center')
                    .data(whiskerData ? [whiskerData] : []);

                center.enter().insert('line', 'rect')
                    .attr('class', 'center')
                    .attr('x1', width / 2)
                    .attr('y1', function (d) { return x0(d[0]); })
                    .attr('x2', width / 2)
                    .attr('y2', function (d) { return x0(d[1]); })
                    .style('opacity', 1e-6)
                    .transition()
                    .duration(duration)
                    .delay(delay)
                    .style('opacity', 1)
                    .attr('y1', function (d) { return x1(d[0]); })
                    .attr('y2', function (d) { return x1(d[1]); });

                center.transition()
                    .duration(duration)
                    .delay(delay)
                    .style('opacity', 1)
                    .attr('y1', function (d) { return x1(d[0]); })
                    .attr('y2', function (d) { return x1(d[1]); });

                center.exit().transition()
                    .duration(duration)
                    .delay(delay)
                    .style('opacity', 1e-6)
                    .attr('y1', function (d) { return x1(d[0]); })
                    .attr('y2', function (d) { return x1(d[1]); })
                    .remove();

                // Update innerquartile box.
                var box = g.selectAll('rect.box')
                    .data([quartileData]);

                box.enter().append('rect')
                    .attr('class', 'box')
                    .attr('x', 0)
                    .attr('y', function (d) { return x0(d[2]); })
                    .attr('width', width)
                    .attr('height', function (d) { return x0(d[0]) - x0(d[2]); })
                  .transition()
                    .duration(duration)
                    .delay(delay)
                    .attr('y', function (d) { return x1(d[2]); })
                    .attr('height', function (d) { return x1(d[0]) - x1(d[2]); });

                box.transition()
                    .duration(duration)
                    .delay(delay)
                    .attr('y', function (d) { return x1(d[2]); })
                    .attr('height', function (d) { return x1(d[0]) - x1(d[2]); });

                // Update median line.
                var medianLine = g.selectAll('line.median')
                    .data([quartileData[1]]);

                medianLine.enter().append('line')
                    .attr('class', 'median')
                    .attr('x1', 0)
                    .attr('y1', x0)
                    .attr('x2', width)
                    .attr('y2', x0)
                    .transition()
                    .duration(duration)
                    .delay(delay)
                    .attr('y1', x1)
                    .attr('y2', x1);

                medianLine.transition()
                    .duration(duration)
                    .delay(delay)
                    .attr('y1', x1)
                    .attr('y2', x1);

                // Update whiskers.
                var whisker = g.selectAll('line.whisker')
                    .data(whiskerData || []);

                whisker.enter().insert('line', 'circle, text')
                    .attr('class', 'whisker')
                    .attr('x1', 0)
                    .attr('y1', x0)
                    .attr('x2', width)
                    .attr('y2', x0)
                    .style('opacity', 1e-6)
                  .transition()
                    .duration(duration)
                    .delay(delay)
                    .attr('y1', x1)
                    .attr('y2', x1)
                    .style('opacity', 1);

                whisker.transition()
                    .duration(duration)
                    .delay(delay)
                    .attr('y1', x1)
                    .attr('y2', x1)
                    .style('opacity', 1);

                whisker.exit().transition()
                    .duration(duration)
                    .delay(delay)
                    .attr('y1', x1)
                    .attr('y2', x1)
                    .style('opacity', 1e-6)
                    .remove();

                // Update outliers.
                var outlier = g.selectAll('circle.outlier')
                    .data(outlierIndices, Number);

                outlier.enter().insert('circle', 'text')
                    .attr('class', 'outlier')
                    .attr('r', 5)
                    .attr('cx', width / 2)
                    .attr('cy', function (i) { return x0(d[i]); })
                    .style('opacity', 1e-6)
                    .transition()
                    .duration(duration)
                    .delay(delay)
                    .attr('cy', function (i) { return x1(d[i]); })
                    .style('opacity', 1);

                outlier.transition()
                    .duration(duration)
                    .delay(delay)
                    .attr('cy', function (i) { return x1(d[i]); })
                    .style('opacity', 1);

                outlier.exit().transition()
                    .duration(duration)
                    .delay(delay)
                    .attr('cy', function (i) { return x1(d[i]); })
                    .style('opacity', 1e-6)
                    .remove();

                // Compute the tick format.
                var format = tickFormat || x1.tickFormat(8);

                // Update box ticks.
                var boxTick = g.selectAll('text.box')
                    .data(quartileData);

                boxTick.enter().append('text')
                    .attr('class', 'box')
                    .attr('dy', '.3em')
                    .attr('dx', function (d, i) { return i & 1 ? 6 : -6; })
                    .attr('x', function (d, i) { return i & 1 ? width : 0; })
                    .attr('y', x0)
                    .attr('text-anchor', function (d, i) { return i & 1 ? 'start' : 'end'; })
                    .text(format)
                    .transition()
                    .duration(duration)
                    .delay(delay)
                    .attr('y', x1);

                boxTick.transition()
                    .duration(duration)
                    .delay(delay)
                    .text(format)
                    .attr('y', x1);

                // Update whisker ticks. These are handled separately from the box
                // ticks because they may or may not exist, and we want don't want
                // to join box ticks pre-transition with whisker ticks post-.
                var whiskerTick = g.selectAll('text.whisker')
                    .data(whiskerData || []);

                whiskerTick.enter().append('text')
                    .attr('class', 'whisker')
                    .attr('dy', '.3em')
                    .attr('dx', 6)
                    .attr('x', width)
                    .attr('y', x0)
                    .text(format)
                    .style('opacity', 1e-6)
                    .transition()
                    .duration(duration)
                    .delay(delay)
                    .attr('y', x1)
                    .style('opacity', 1);

                whiskerTick.transition()
                    .duration(duration)
                    .delay(delay)
                    .text(format)
                    .attr('y', x1)
                    .style('opacity', 1);

                whiskerTick.exit().transition()
                    .duration(duration)
                    .delay(delay)
                    .attr('y', x1)
                    .style('opacity', 1e-6)
                    .remove();
            });
            d3.timer.flush();
        }

        box.width = function (x) {
            if (!arguments.length) {
                return width;
            }
            width = x;
            return box;
        };

        box.height = function (x) {
            if (!arguments.length) {
                return height;
            }
            height = x;
            return box;
        };

        box.tickFormat = function (x) {
            if (!arguments.length) {
                return tickFormat;
            }
            tickFormat = x;
            return box;
        };

        box.duration = function (x) {
            if (!arguments.length) {
                return duration;
            }
            duration = x;
            return box;
        };

        box.domain = function (x) {
            if (!arguments.length) {
                return domain;
            }
            domain = x === null ? x : d3.functor(x);
            return box;
        };

        box.value = function (x) {
            if (!arguments.length) {
                return value;
            }
            value = x;
            return box;
        };

        box.whiskers = function (x) {
            if (!arguments.length) {
                return whiskers;
            }
            whiskers = x;
            return box;
        };

        box.quartiles = function (x) {
            if (!arguments.length) {
                return quartiles;
            }
            quartiles = x;
            return box;
        };

        return box;
    };

    function boxWhiskers (d) {
        return [0, d.length - 1];
    }

    function boxQuartiles (d) {
        return [
            d3.quantile(d, 0.25),
            d3.quantile(d, 0.5),
            d3.quantile(d, 0.75)
        ];
    }

})();
<commit_msg>When the domain of the chart changes, we need to keep the width attributes of the box-whiskers consistent across all data points.<commit_after>// https://github.com/d3/d3-plugins/blob/master/box/box.js
(function () {

    // Inspired by http://informationandvisualization.de/blog/box-plot
    d3.box = function () {
        var width = 1,
            height = 1,
            duration = 0,
            delay = 0,
            domain = null,
            value = Number,
            whiskers = boxWhiskers,
            quartiles = boxQuartiles,
            tickFormat = null;

        // For each small multiple…
        function box (g) {
            g.each(function (d, i) {
                d = d.map(value).sort(d3.ascending);
                var g = d3.select(this),
                    n = d.length,
                    min = d[0],
                    max = d[n - 1];

                // Compute quartiles. Must return exactly 3 elements.
                var quartileData = d.quartiles = quartiles(d);

                // Compute whiskers. Must return exactly 2 elements, or null.
                var whiskerIndices = whiskers && whiskers.call(this, d, i),
                    whiskerData = whiskerIndices && whiskerIndices.map(function (i) { return d[i]; });

                // Compute outliers. If no whiskers are specified, all data are 'outliers'.
                // We compute the outliers as indices, so that we can join across transitions!
                var outlierIndices = whiskerIndices ?
                    d3.range(0, whiskerIndices[0]).concat(d3.range(whiskerIndices[1] + 1, n)) : d3.range(n);

                // Compute the new x-scale.
                var x1 = d3.scale.linear()
                    .domain(domain && domain.call(this, d, i) || [min, max])
                    .range([height, 0]);

                // Retrieve the old x-scale, if this is an update.
                var x0 = this.__chart__ || d3.scale.linear()
                    .domain([0, Infinity])
                    .range(x1.range());

                // Stash the new scale.
                this.__chart__ = x1;

                // Note: the box, median, and box tick elements are fixed in number,
                // so we only have to handle enter and update. In contrast, the outliers
                // and other elements are variable, so we need to exit them! Variable
                // elements also fade in and out.

                // Update center line: the vertical line spanning the whiskers.
                var center = g.selectAll('line.center')
                    .data(whiskerData ? [whiskerData] : []);

                center.enter().insert('line', 'rect')
                    .attr('class', 'center')
                    .attr('x1', width / 2)
                    .attr('y1', function (d) { return x0(d[0]); })
                    .attr('x2', width / 2)
                    .attr('y2', function (d) { return x0(d[1]); })
                    .style('opacity', 1e-6)
                    .transition()
                    .duration(duration)
                    .delay(delay)
                    .style('opacity', 1)
                    .attr('y1', function (d) { return x1(d[0]); })
                    .attr('y2', function (d) { return x1(d[1]); });

                center.transition()
                    .duration(duration)
                    .delay(delay)
                    .style('opacity', 1)
                    .attr('x1', width / 2)
                    .attr('x2', width / 2)
                    .attr('y1', function (d) { return x1(d[0]); })
                    .attr('y2', function (d) { return x1(d[1]); });

                center.exit().transition()
                    .duration(duration)
                    .delay(delay)
                    .style('opacity', 1e-6)
                    .attr('y1', function (d) { return x1(d[0]); })
                    .attr('y2', function (d) { return x1(d[1]); })
                    .remove();

                // Update innerquartile box.
                var box = g.selectAll('rect.box')
                    .data([quartileData]);

                box.enter().append('rect')
                    .attr('class', 'box')
                    .attr('x', 0)
                    .attr('y', function (d) { return x0(d[2]); })
                    .attr('width', width)
                    .attr('height', function (d) { return x0(d[0]) - x0(d[2]); })
                  .transition()
                    .duration(duration)
                    .delay(delay)
                    .attr('y', function (d) { return x1(d[2]); })
                    .attr('height', function (d) { return x1(d[0]) - x1(d[2]); });

                box.transition()
                    .duration(duration)
                    .delay(delay)
                    .attr('width', width)
                    .attr('y', function (d) { return x1(d[2]); })
                    .attr('height', function (d) { return x1(d[0]) - x1(d[2]); });

                // Update median line.
                var medianLine = g.selectAll('line.median')
                    .data([quartileData[1]]);

                medianLine.enter().append('line')
                    .attr('class', 'median')
                    .attr('x1', 0)
                    .attr('y1', x0)
                    .attr('x2', width)
                    .attr('y2', x0)
                    .transition()
                    .duration(duration)
                    .delay(delay)
                    .attr('y1', x1)
                    .attr('y2', x1);

                medianLine.transition()
                    .duration(duration)
                    .delay(delay)
                    .attr('x1', 0)
                    .attr('x2', width)
                    .attr('y1', x1)
                    .attr('y2', x1);

                // Update whiskers.
                var whisker = g.selectAll('line.whisker')
                    .data(whiskerData || []);

                whisker.enter().insert('line', 'circle, text')
                    .attr('class', 'whisker')
                    .attr('x1', 0)
                    .attr('y1', x0)
                    .attr('x2', width)
                    .attr('y2', x0)
                    .style('opacity', 1e-6)
                  .transition()
                    .duration(duration)
                    .delay(delay)
                    .attr('y1', x1)
                    .attr('y2', x1)
                    .style('opacity', 1);

                whisker.transition()
                    .duration(duration)
                    .delay(delay)
                    .attr('x1', 0)
                    .attr('x2', width)
                    .attr('y1', x1)
                    .attr('y2', x1)
                    .style('opacity', 1);

                whisker.exit().transition()
                    .duration(duration)
                    .delay(delay)
                    .attr('y1', x1)
                    .attr('y2', x1)
                    .style('opacity', 1e-6)
                    .remove();

                // Update outliers.
                var outlier = g.selectAll('circle.outlier')
                    .data(outlierIndices, Number);

                outlier.enter().insert('circle', 'text')
                    .attr('class', 'outlier')
                    .attr('r', 5)
                    .attr('cx', width / 2)
                    .attr('cy', function (i) { return x0(d[i]); })
                    .style('opacity', 1e-6)
                    .transition()
                    .duration(duration)
                    .delay(delay)
                    .attr('cy', function (i) { return x1(d[i]); })
                    .style('opacity', 1);

                outlier.transition()
                    .duration(duration)
                    .delay(delay)
                    .attr('cx', width / 2)
                    .attr('cy', function (i) { return x1(d[i]); })
                    .style('opacity', 1);

                outlier.exit().transition()
                    .duration(duration)
                    .delay(delay)
                    .attr('cy', function (i) { return x1(d[i]); })
                    .style('opacity', 1e-6)
                    .remove();

                // Compute the tick format.
                var format = tickFormat || x1.tickFormat(8);

                // Update box ticks.
                var boxTick = g.selectAll('text.box')
                    .data(quartileData);

                boxTick.enter().append('text')
                    .attr('class', 'box')
                    .attr('dy', '.3em')
                    .attr('dx', function (d, i) { return i & 1 ? 6 : -6; })
                    .attr('x', function (d, i) { return i & 1 ? width : 0; })
                    .attr('y', x0)
                    .attr('text-anchor', function (d, i) { return i & 1 ? 'start' : 'end'; })
                    .text(format)
                    .transition()
                    .duration(duration)
                    .delay(delay)
                    .attr('y', x1);

                boxTick.transition()
                    .duration(duration)
                    .delay(delay)
                    .text(format)
                    .attr('x', function (d, i) { return i & 1 ? width : 0; })
                    .attr('y', x1);

                // Update whisker ticks. These are handled separately from the box
                // ticks because they may or may not exist, and we want don't want
                // to join box ticks pre-transition with whisker ticks post-.
                var whiskerTick = g.selectAll('text.whisker')
                    .data(whiskerData || []);

                whiskerTick.enter().append('text')
                    .attr('class', 'whisker')
                    .attr('dy', '.3em')
                    .attr('dx', 6)
                    .attr('x', width)
                    .attr('y', x0)
                    .text(format)
                    .style('opacity', 1e-6)
                    .transition()
                    .duration(duration)
                    .delay(delay)
                    .attr('y', x1)
                    .style('opacity', 1);

                whiskerTick.transition()
                    .duration(duration)
                    .delay(delay)
                    .text(format)
                    .attr('x', width)
                    .attr('y', x1)
                    .style('opacity', 1);

                whiskerTick.exit().transition()
                    .duration(duration)
                    .delay(delay)
                    .attr('y', x1)
                    .style('opacity', 1e-6)
                    .remove();
            });
            d3.timer.flush();
        }

        box.width = function (x) {
            if (!arguments.length) {
                return width;
            }
            width = x;
            return box;
        };

        box.height = function (x) {
            if (!arguments.length) {
                return height;
            }
            height = x;
            return box;
        };

        box.tickFormat = function (x) {
            if (!arguments.length) {
                return tickFormat;
            }
            tickFormat = x;
            return box;
        };

        box.duration = function (x) {
            if (!arguments.length) {
                return duration;
            }
            duration = x;
            return box;
        };

        box.domain = function (x) {
            if (!arguments.length) {
                return domain;
            }
            domain = x === null ? x : d3.functor(x);
            return box;
        };

        box.value = function (x) {
            if (!arguments.length) {
                return value;
            }
            value = x;
            return box;
        };

        box.whiskers = function (x) {
            if (!arguments.length) {
                return whiskers;
            }
            whiskers = x;
            return box;
        };

        box.quartiles = function (x) {
            if (!arguments.length) {
                return quartiles;
            }
            quartiles = x;
            return box;
        };

        return box;
    };

    function boxWhiskers (d) {
        return [0, d.length - 1];
    }

    function boxQuartiles (d) {
        return [
            d3.quantile(d, 0.25),
            d3.quantile(d, 0.5),
            d3.quantile(d, 0.75)
        ];
    }

})();
<|endoftext|>


<commit_before>
// START
$("#start").on('click', 'li', function(e) {
    var stopNumber = 0;
    var action =  $(e.target).hasClass('fa-pencil') ? "edit" : "";

    stopOnClick(this, stopNumber, action);
});

// FINISH
$("#finish").on('click', 'li', function(e) {
    var stopNumber = 999;
    var action =  $(e.target).hasClass('fa-pencil') ? "edit" : "";

    stopOnClick(this, stopNumber, action);
});

// STOPS
$("#stops").on('click', 'li', function(e) {
    var stopNumber = parseInt($(this).attr("stop-number"));
    var action = $(e.target).hasClass('fa-trash') ? "remove" : "";
    action = $(e.target).hasClass('fa-pencil') ? "edit" : action;
    action = $(e.target).hasClass('fa-copy') ? "duplicate" : action;

    if(!action){
    	for(var point in points){
			if (points[point] && points[point].orderNumber == stopNumber && points[point].marker) {
				var latlng = points[point].marker.getLatLng();
				map.setView(latlng, 20);
			}
		}
    }
    stopOnClick(this, stopNumber, action);
});

function stopOnClick(parent, stopNumber, action){
    if(action == "remove"){
        $(parent).remove();
        map.removeLayer("point" + stopNumber);
        map.removeLayer("pointText" + stopNumber);
        removeStop(stopNumber);
        return;
    }else if(action == "edit"){

    }else if(action == "duplicate"){
       duplicate(stopNumber);
    }
}

var editTimeout;
function createEditTimeout(){
	if(editTimeout != null) clearTimeout(editTimeout);
	editTimeout = setTimeout(function(){
		updateGameValues();
		savePlot(game);
		clearTimeout(editTimeout);
		editTimeout = null;
	}, 2000);
}

function updateGameValues(){
	game.name = $("#gameName").val();
	game.description = $("#gameDescription").val()
	game.time = $("#timeToggle").prop('checked') ? parseInt($("#gameTimeValue").val()) : 0;
}

function init(){
	$("#gameName").val(game.name);
	$("#gameDescription").val(game.description);

	if(game.time != 0){
		$("#timeToggle").prop('checked', true);
		$('#timeLimit').css("visibility", 'visible');
		$('#gameTimeValue').val(game.time);
	}

	$("#gameName").blur(onBlur);
	$("#gameDescription").on("input", onInput);
	$("#gameDescription").blur(onBlur);
	$("#gameName").on("input", onInput);
	$("#gameTimeValue").blur(onBlur);
	$("#gameTimeValue").on("input", onInput);
	$("#timeToggle").change(onBlur);

	$("#point0 a").attr("href", "screens-overview.php?id=" + start.id);
	$("#point999 a").attr("href", "screens-overview.php?id=" + finish.id);

	function onBlur(){
		if(editTimeout != null) clearTimeout(editTimeout);
		updateGameValues();
		savePlot(game);
	}

	function onInput(){
		if(editTimeout != null) clearTimeout(editTimeout);
		createEditTimeout();
	}

}

map.on('click', function(e) {
	if($(".leaflet-control-geocoder-form input").is(":focus")) return;
	var marker = addMarker(e.latlng);
	addStop(marker, "normal");
	map.addLayer(marker);
});

function loadStops(){
	points.forEach(function(p){
		if(p.type == "beacon") addBeaconMarker(p.beaconId, p);
		showStop(p);
		if(p.marker)p.marker.step = p;
	});

	sortPoints();
	updatePath();
}

function duplicate(stopNumber){
 	for(var point in points){
		if (points[point] && points[point].orderNumber == stopNumber) {
			poisCreated++;
			var copy = points[point].copy();
			copy.orderNumber = poisCreated;
			var lastMarker = copy.marker;
			var newPosition = addMetersToCoordinates(lastMarker._latlng, 200, 0);
			copy.marker = addMarker(newPosition, copy.type != "beacon");
			map.addLayer(copy.marker);
			map.panTo(copy.marker._latlng);
    		duplicatePOI(copy, game, function(id){
    			showStop(copy);
    			points[poisCreated] = copy;
    			updatePath();
    			sortPoints();
    		});
		}
	}
}

function addStop(marker, type){
	var lastPoi = poisCreated;

	poisCreated++;
	var step = new Step({marker: marker, orderNumber: poisCreated, type: type});
	if(marker)marker.step = step;
	points[poisCreated] = step;
	showStop(step);
	updatePath();
	sortPoints();

	savePOI(step, game, function(id){
		$("#stops").children().each(function() {
			var number = $(this).attr("stop-number");
			if(number == poisCreated){
				var url = game.type == "TreasureHunt" ? "screens-overview.php?id=" + id + "&noClue" : "screens-overview.php?id=" + id;
				$(this).find(".editPOI").attr("href", url);
			}
		});
	});
}

function showStop(stop){
	var len = Object.keys(points).length;
	var last = game.type == "TreasureHunt" && (stop.orderNumber == len);

	var url = stop.id ? "screens-overview.php?id=" + stop.id + (last ? "&noClue" : ""): "#"
	if(stop.type == "normal") {
		$('#stops').append(`
			<li class="stop-row poirow" id="point` + stop.orderNumber + `" stop-number="` + stop.orderNumber + `">

Stop ` + (stop.orderNumber) + `

   		`);
	}else if(stop.type == "beacon"){
		$('#stops').append(`
			<li class="stop-row poirow" id="point` + stop.orderNumber + `" stop-number="` + stop.orderNumber + `">

Stop ` + (stop.orderNumber) + `

							<select name="beacon-id" class="beacon-select-${stop.orderNumber}">
								<option hidden value="">Select Beacon</option>
								${beacons.map(b => `<option ${stop.beaconId==b.id?"selected":""} value="${b.id}">${b.id} - ${b.name}</option>`).join('\n')}
							</select>

   		`);

		$(".beacon-select-" + stop.orderNumber).on("change", function(e){
			var id = $(this).val();
			addBeaconMarker(id, stop, true);
			savePOI(stop, game);
		});
	}

}

function addBeaconMarker(id, step, focus){
	var beacon = null;
	for(var b in beacons){
		if(beacons[b].id == id){
			beacon = beacons[b];
			break;
		}
	}
	if(beacon == null) return;
	if(step.marker) map.removeLayer(step.marker);
	var coords = {lat: beacon.lat, lng: beacon.lng};
	var marker = addMarker(coords, false);
	map.addLayer(marker);
	step.marker = marker;
	step.beaconId = id;
	if(focus){
		map.setView(coords, 20);
	}
	sortPoints(null, true);
	updatePath();
}

function removeStop(stopNumber) {
	for(var point in points){
		if (points[point] && points[point].orderNumber == stopNumber) {
			if(points[point].marker)map.removeLayer(points[point].marker);
			removePOI(points[point]);
			delete points[point];
		}
	}

	poisCreated--;
	sortPoints(true);
}

function updateLabels() {
	$("#stops").children().each(function() {
		var number = $(this).attr("stop-number");
		for(var point in points){
			if (points[point] && points[point].orderNumber == number){
				if (points[point].title && points[point].title.length > 0){
					points[point].marker._tooltip.setContent(points[point].title);
					$(this).find("span.name").text(points[point].title);
				}else{
					var name = "Stop " + points[point].orderNumber;
					if(points[point].marker) points[point].marker._tooltip.setContent(name);
					$(this).find("span.name").text(name);
				}
				break;
			}
		}
	});
}

function sortPoints(save, skipSort){
	var len = Object.keys(points).length;
	if (len >= 1) {

		var newPointList = [];

		if(!skipSort){
			$("#stops").children().each(function (index) {
				var number = $(this).attr("stop-number");
				for (var stop in points) {
					if (points[stop] && points[stop].orderNumber == number) {
						$(this).attr("stop-number", index + 1);
						$(this).attr("id", "point" + (index + 1));
						points[stop].orderNumber = (index + 1);
						newPointList.push(points[stop]);
						if(save)savePOI(points[stop], game);
						points.splice(stop, 1);
						break;
					}
				}

			});
			points = newPointList;
			updatePath();
		}
	}

	if(game.type == "TreasureHunt"){
		$(".poiChest").addClass("hidden");
		var childrens = $("#stops").children();
		$(childrens[len - 1]).find(".poiChest").removeClass("hidden");

		childrens.each(function(index){
			var e = $(childrens[index]).find(".editPOI");
			e.attr("href", e.attr("href").replace("&noClue", ""));
		});

		var e = $(childrens[len - 1]).find(".editPOI");
		e.attr("href", e.attr("href") + "&noClue");

		points.forEach(function (p) {
			if(p.marker) p.marker.setIcon(p.type == "normal" ? generateMarker(1): generateMarker(1, true));
		});

		var p = points[points.length - 1];
		if(p && p.marker) p.marker.setIcon(finishTreasureMarkerIcon);
	}
}
<commit_msg>Fixed markers being added twice<commit_after>
var editTimeout;
function createEditTimeout(){
	if(editTimeout != null) clearTimeout(editTimeout);
	editTimeout = setTimeout(function(){
		updateGameValues();
		savePlot(game);
		clearTimeout(editTimeout);
		editTimeout = null;
	}, 2000);
}

function updateGameValues(){
	game.name = $("#gameName").val();
	game.description = $("#gameDescription").val()
	game.time = $("#timeToggle").prop('checked') ? parseInt($("#gameTimeValue").val()) : 0;
}

function init(){
	$("#gameName").val(game.name);
	$("#gameDescription").val(game.description);

	if(game.time != 0){
		$("#timeToggle").prop('checked', true);
		$('#timeLimit').css("visibility", 'visible');
		$('#gameTimeValue').val(game.time);
	}

	$("#gameName").blur(onBlur);
	$("#gameDescription").on("input", onInput);
	$("#gameDescription").blur(onBlur);
	$("#gameName").on("input", onInput);
	$("#gameTimeValue").blur(onBlur);
	$("#gameTimeValue").on("input", onInput);
	$("#timeToggle").change(onBlur);

	$("#point0 a").attr("href", "screens-overview.php?id=" + start.id);
	$("#point999 a").attr("href", "screens-overview.php?id=" + finish.id);

	function onBlur(){
		if(editTimeout != null) clearTimeout(editTimeout);
		updateGameValues();
		savePlot(game);
	}

	function onInput(){
		if(editTimeout != null) clearTimeout(editTimeout);
		createEditTimeout();
	}

}

function loadStops(){
	points.forEach(function(p){
		if(p.type == "beacon") addBeaconMarker(p.beaconId, p);
		showStop(p);
		if(p.marker)p.marker.step = p;
	});

	sortPoints();
	updatePath();
}

function duplicate(stopNumber){
 	for(var point in points){
		if (points[point] && points[point].orderNumber == stopNumber) {
			poisCreated++;
			var copy = points[point].copy();
			copy.orderNumber = poisCreated;
			var lastMarker = copy.marker;
			var newPosition = addMetersToCoordinates(lastMarker._latlng, 200, 0);
			copy.marker = addMarker(newPosition, copy.type != "beacon");
			map.addLayer(copy.marker);
			map.panTo(copy.marker._latlng);
    		duplicatePOI(copy, game, function(id){
    			showStop(copy);
    			points[poisCreated] = copy;
    			updatePath();
    			sortPoints();
    		});
		}
	}
}

function addStop(marker, type){
	var lastPoi = poisCreated;

	poisCreated++;
	var step = new Step({marker: marker, orderNumber: poisCreated, type: type});
	if(marker)marker.step = step;
	points[poisCreated] = step;
	showStop(step);
	updatePath();
	sortPoints();

	savePOI(step, game, function(id){
		$("#stops").children().each(function() {
			var number = $(this).attr("stop-number");
			if(number == poisCreated){
				var url = game.type == "TreasureHunt" ? "screens-overview.php?id=" + id + "&noClue" : "screens-overview.php?id=" + id;
				$(this).find(".editPOI").attr("href", url);
			}
		});
	});
}

function showStop(stop){
	var len = Object.keys(points).length;
	var last = game.type == "TreasureHunt" && (stop.orderNumber == len);

	var url = stop.id ? "screens-overview.php?id=" + stop.id + (last ? "&noClue" : ""): "#"
	if(stop.type == "normal") {
		$('#stops').append(`
			<li class="stop-row poirow" id="point` + stop.orderNumber + `" stop-number="` + stop.orderNumber + `">

Stop ` + (stop.orderNumber) + `

   		`);
	}else if(stop.type == "beacon"){
		$('#stops').append(`
			<li class="stop-row poirow" id="point` + stop.orderNumber + `" stop-number="` + stop.orderNumber + `">

Stop ` + (stop.orderNumber) + `

							<select name="beacon-id" class="beacon-select-${stop.orderNumber}">
								<option hidden value="">Select Beacon</option>
								${beacons.map(b => `<option ${stop.beaconId==b.id?"selected":""} value="${b.id}">${b.id} - ${b.name}</option>`).join('\n')}
							</select>

   		`);

		$(".beacon-select-" + stop.orderNumber).on("change", function(e){
			var id = $(this).val();
			addBeaconMarker(id, stop, true);
			savePOI(stop, game);
		});
	}

}

function addBeaconMarker(id, step, focus){
	var beacon = null;
	for(var b in beacons){
		if(beacons[b].id == id){
			beacon = beacons[b];
			break;
		}
	}
	if(beacon == null) return;
	if(step.marker) map.removeLayer(step.marker);
	var coords = {lat: beacon.lat, lng: beacon.lng};
	var marker = addMarker(coords, false);
	map.addLayer(marker);
	step.marker = marker;
	step.beaconId = id;
	if(focus){
		map.setView(coords, 20);
	}
	sortPoints(null, true);
	updatePath();
}

function removeStop(stopNumber) {
	for(var point in points){
		if (points[point] && points[point].orderNumber == stopNumber) {
			if(points[point].marker)map.removeLayer(points[point].marker);
			removePOI(points[point]);
			delete points[point];
		}
	}

	poisCreated--;
	sortPoints(true);
}

function updateLabels() {
	$("#stops").children().each(function() {
		var number = $(this).attr("stop-number");
		for(var point in points){
			if (points[point] && points[point].orderNumber == number){
				if (points[point].title && points[point].title.length > 0){
					points[point].marker._tooltip.setContent(points[point].title);
					$(this).find("span.name").text(points[point].title);
				}else{
					var name = "Stop " + points[point].orderNumber;
					if(points[point].marker) points[point].marker._tooltip.setContent(name);
					$(this).find("span.name").text(name);
				}
				break;
			}
		}
	});
}

function sortPoints(save, skipSort){
	var len = Object.keys(points).length;
	if (len >= 1) {

		var newPointList = [];

		if(!skipSort){
			$("#stops").children().each(function (index) {
				var number = $(this).attr("stop-number");
				for (var stop in points) {
					if (points[stop] && points[stop].orderNumber == number) {
						$(this).attr("stop-number", index + 1);
						$(this).attr("id", "point" + (index + 1));
						points[stop].orderNumber = (index + 1);
						newPointList.push(points[stop]);
						if(save)savePOI(points[stop], game);
						points.splice(stop, 1);
						break;
					}
				}

			});
			points = newPointList;
			updatePath();
		}
	}

	if(game.type == "TreasureHunt"){
		$(".poiChest").addClass("hidden");
		var childrens = $("#stops").children();
		$(childrens[len - 1]).find(".poiChest").removeClass("hidden");

		childrens.each(function(index){
			var e = $(childrens[index]).find(".editPOI");
			e.attr("href", e.attr("href").replace("&noClue", ""));
		});

		var e = $(childrens[len - 1]).find(".editPOI");
		e.attr("href", e.attr("href") + "&noClue");

		points.forEach(function (p) {
			if(p.marker) p.marker.setIcon(p.type == "normal" ? generateMarker(1): generateMarker(1, true));
		});

		var p = points[points.length - 1];
		if(p && p.marker) p.marker.setIcon(finishTreasureMarkerIcon);
	}
}
<|endoftext|>


<commit_before>// d3.tip
// Copyright (c) 2013 Justin Palmer
//
// Tooltips for d3.js SVG visualizations

// Public - contructs a new tooltip
//
// Returns a tip
d3.tip = function() {
  var direction = d3_tip_direction,
      offset    = d3_tip_offset,
      html      = d3_tip_html,
      node      = init_node(),
      svg       = null,
      point     = null;

  function tip(vis) {
    svg = get_svg_node(vis)
    point = svg.createSVGPoint()
    document.body.appendChild(node)
  }

  // Public - show the tooltip on the screen
  //
  // Returns a tip
  tip.show = function() {
    var content = html.apply(this, arguments),
        poffset = offset.apply(this, arguments),
        dir     = direction.apply(this, arguments),
        dirs    = direction_callbacks.keys(),
        nodel   = d3.select(node), i = 0,
        coords

    nodel.html(content).style('display', 'block')
    for(i; i < dirs.length; i++) nodel.classed(dirs[i], false)
    coords = direction_callbacks.get(dir).apply(this)
    nodel.classed(dir, true).style({
      top: (coords.top +  poffset[0]) + 'px',
      left: (coords.left + poffset[1]) + 'px'
    })

    return tip
  }

  // Public - hide the tooltip
  //
  // Returns a tip
  tip.hide = function() {
    node.style.display = 'none'
    node.innerHTML = ''

    return tip
  }

  // Public: Proxy attr calls to the d3 tip container.  Sets or gets attribute value.
  //
  // n - name of the attribute
  // v - value of the attribute
  //
  // Returns tip or attribute value
  tip.attr = function(n, v) {
    if (arguments.length < 2) {
      return d3.select(node).attr(n)
    } else {
      d3.select(node).attr(n, v)
    }

    return tip;
  }

  // Public: Proxy style calls to the d3 tip container.  Sets or gets a style value.
  //
  // n - name of the property
  // v - value of the property
  //
  // Returns tip or style property value
  tip.style = function(n, v) {
    if (arguments.length < 2) {
      return d3.select(node).style(n)
    } else {
      d3.select(node).style(n, v)
    }

    return tip;
  }

  // Public: Set or get the direction of the tooltip
  //
  // v - One of n(orth), s(outh), e(ast), or w(est)
  //
  // Returns tip or direction
  tip.direction = function(v) {
    if (!arguments.length) return direction;
    direction = v == null ? v : d3.functor(v);
    return tip;
  };

  // Public: Sets or gets the offset of the tip
  //
  // v - Array of [x, y] offset
  //
  // Returns offset or
  tip.offset = function(v) {
    if (!arguments.length) return offset;
    offset = v == null ? v : d3.functor(v);
    return tip;
  };

  // Public: sets or gets the html value of the tooltip
  //
  // v - String value of the tip
  //
  // Returns html value or tip
  tip.html = function(v) {
    if (!arguments.length) return html;
    html = v == null ? v : d3.functor(v)

    return tip
  };

  function d3_tip_direction() { return 'n' }
  function d3_tip_offset() { return [0, 0] }
  function d3_tip_html() { return ' ' }

  var direction_callbacks = d3.map({
    n:  direction_n,
    s:  direction_s,
    e:  direction_e,
    w:  direction_w,
    nw: direction_nw,
    ne: direction_ne,
    sw: direction_sw
  })

  function direction_n() {
    var bbox = get_screen_bbox()
    return {
      top:  bbox.n.y - node.offsetHeight,
      left: bbox.n.x - node.offsetWidth / 2
    }
  }

  function direction_s() {
    var bbox = get_screen_bbox()
    return {
      top:  bbox.s.y,
      left: bbox.s.x - node.offsetWidth / 2
    }
  }

  function direction_e() {
    var bbox = get_screen_bbox()
    return {
      top:  bbox.e.y - node.offsetHeight / 2,
      left: bbox.e.x
    }
  }

  function direction_w() {
    var bbox = get_screen_bbox()
    return {
      top:  bbox.w.y - node.offsetHeight / 2,
      left: bbox.w.x - node.offsetWidth
    }
  }

  function direction_nw() {
    var bbox = get_screen_bbox()
    return {
      top:  bbox.nw.y - node.offsetHeight,
      left: bbox.nw.x - node.offsetWidth
    }
  }

  function direction_ne() {
    var bbox = get_screen_bbox()
    return {
      top:  bbox.ne.y - node.offsetHeight,
      left: bbox.ne.x
    }
  }

  function direction_sw() {
    var bbox = get_screen_bbox()
    return {
      top:  bbox.sw.y,
      left: bbox.sw.x - node.offsetWidth
    }
  }

  function init_node() {
    var node = document.createElement('div')
    node.style.position = 'absolute'
    node.style.display = 'none'
    node.style.boxSizing = 'border-box'
    return node
  }

  function get_svg_node(el) {
    el = el.node()
    if(el.tagName.toLowerCase() == 'svg') {
      return el
    } else {
      while(el.parentNode) {
        el = el.parentNode
        if(el.tagName.toLowerCase() == 'svg')
          return el
      }
    }

    return null
  }

  // Private - gets the screen coordinates of a shape
  //
  // Given a shape on the screen, will return an SVGPoint for the directions
  // n(north), s(south), e(east), w(west), ne(northeast), se(southeast), nw(northwest),
  // sw(southwest).
  //
  //    +-+-+
  //    |   |
  //    +   +
  //    |   |
  //    +-+-+
  //
  // Returns an Object {n, s, e, w, nw, sw, ne, se}
  function get_screen_bbox() {
    var target     = d3.event.target,
        bbox       = {},
        matrix     = target.getScreenCTM(),
        tbbox      = target.getBBox(),
        width      = tbbox.width,
        height     = tbbox.height,
        x          = tbbox.x,
        y          = tbbox.y,
        scrollTop = document.body.scrollTop
        scrollLeft = document.body.scrollLeft

    if(document.documentElement && document.documentElement.scrollTop) {
      scrollTop = document.documentElement.scrollTop
      scrollLeft = document.documentElement.scrollLeft
    }

    point.x = x + scrollLeft
    point.y = y + scrollTop
    bbox.nw = point.matrixTransform(matrix)
    point.x += width
    bbox.ne = point.matrixTransform(matrix)
    point.y += height
    bbox.se = point.matrixTransform(matrix)
    point.x -= width
    bbox.sw = point.matrixTransform(matrix)
    point.y -= height / 2
    bbox.w  = point.matrixTransform(matrix)
    point.x += width
    bbox.e = point.matrixTransform(matrix)
    point.x -= width / 2
    point.y -= height / 2
    bbox.n = point.matrixTransform(matrix)
    point.y += height
    bbox.s = point.matrixTransform(matrix)

    return bbox
  }

  return tip;
}
<commit_msg>implement se direction support<commit_after>// d3.tip
// Copyright (c) 2013 Justin Palmer
//
// Tooltips for d3.js SVG visualizations

// Public - contructs a new tooltip
//
// Returns a tip
d3.tip = function() {
  var direction = d3_tip_direction,
      offset    = d3_tip_offset,
      html      = d3_tip_html,
      node      = init_node(),
      svg       = null,
      point     = null;

  function tip(vis) {
    svg = get_svg_node(vis)
    point = svg.createSVGPoint()
    document.body.appendChild(node)
  }

  // Public - show the tooltip on the screen
  //
  // Returns a tip
  tip.show = function() {
    var content = html.apply(this, arguments),
        poffset = offset.apply(this, arguments),
        dir     = direction.apply(this, arguments),
        dirs    = direction_callbacks.keys(),
        nodel   = d3.select(node), i = 0,
        coords

    nodel.html(content).style('display', 'block')
    for(i; i < dirs.length; i++) nodel.classed(dirs[i], false)
    coords = direction_callbacks.get(dir).apply(this)
    nodel.classed(dir, true).style({
      top: (coords.top +  poffset[0]) + 'px',
      left: (coords.left + poffset[1]) + 'px'
    })

    return tip
  }

  // Public - hide the tooltip
  //
  // Returns a tip
  tip.hide = function() {
    node.style.display = 'none'
    node.innerHTML = ''

    return tip
  }

  // Public: Proxy attr calls to the d3 tip container.  Sets or gets attribute value.
  //
  // n - name of the attribute
  // v - value of the attribute
  //
  // Returns tip or attribute value
  tip.attr = function(n, v) {
    if (arguments.length < 2) {
      return d3.select(node).attr(n)
    } else {
      d3.select(node).attr(n, v)
    }

    return tip;
  }

  // Public: Proxy style calls to the d3 tip container.  Sets or gets a style value.
  //
  // n - name of the property
  // v - value of the property
  //
  // Returns tip or style property value
  tip.style = function(n, v) {
    if (arguments.length < 2) {
      return d3.select(node).style(n)
    } else {
      d3.select(node).style(n, v)
    }

    return tip;
  }

  // Public: Set or get the direction of the tooltip
  //
  // v - One of n(orth), s(outh), e(ast), or w(est)
  //
  // Returns tip or direction
  tip.direction = function(v) {
    if (!arguments.length) return direction;
    direction = v == null ? v : d3.functor(v);
    return tip;
  };

  // Public: Sets or gets the offset of the tip
  //
  // v - Array of [x, y] offset
  //
  // Returns offset or
  tip.offset = function(v) {
    if (!arguments.length) return offset;
    offset = v == null ? v : d3.functor(v);
    return tip;
  };

  // Public: sets or gets the html value of the tooltip
  //
  // v - String value of the tip
  //
  // Returns html value or tip
  tip.html = function(v) {
    if (!arguments.length) return html;
    html = v == null ? v : d3.functor(v)

    return tip
  };

  function d3_tip_direction() { return 'n' }
  function d3_tip_offset() { return [0, 0] }
  function d3_tip_html() { return ' ' }

  var direction_callbacks = d3.map({
    n:  direction_n,
    s:  direction_s,
    e:  direction_e,
    w:  direction_w,
    nw: direction_nw,
    ne: direction_ne,
    sw: direction_sw,
    se: direction_se
  })

  function direction_n() {
    var bbox = get_screen_bbox()
    return {
      top:  bbox.n.y - node.offsetHeight,
      left: bbox.n.x - node.offsetWidth / 2
    }
  }

  function direction_s() {
    var bbox = get_screen_bbox()
    return {
      top:  bbox.s.y,
      left: bbox.s.x - node.offsetWidth / 2
    }
  }

  function direction_e() {
    var bbox = get_screen_bbox()
    return {
      top:  bbox.e.y - node.offsetHeight / 2,
      left: bbox.e.x
    }
  }

  function direction_w() {
    var bbox = get_screen_bbox()
    return {
      top:  bbox.w.y - node.offsetHeight / 2,
      left: bbox.w.x - node.offsetWidth
    }
  }

  function direction_nw() {
    var bbox = get_screen_bbox()
    return {
      top:  bbox.nw.y - node.offsetHeight,
      left: bbox.nw.x - node.offsetWidth
    }
  }

  function direction_ne() {
    var bbox = get_screen_bbox()
    return {
      top:  bbox.ne.y - node.offsetHeight,
      left: bbox.ne.x
    }
  }

  function direction_sw() {
    var bbox = get_screen_bbox()
    return {
      top:  bbox.sw.y,
      left: bbox.sw.x - node.offsetWidth
    }
  }

  function direction_se() {
    var bbox = get_screen_bbox()
    return {
      top:  bbox.se.y,
      left: bbox.e.x
    }
  }

  function init_node() {
    var node = document.createElement('div')
    node.style.position = 'absolute'
    node.style.display = 'none'
    node.style.boxSizing = 'border-box'
    return node
  }

  function get_svg_node(el) {
    el = el.node()
    if(el.tagName.toLowerCase() == 'svg') {
      return el
    } else {
      while(el.parentNode) {
        el = el.parentNode
        if(el.tagName.toLowerCase() == 'svg')
          return el
      }
    }

    return null
  }

  // Private - gets the screen coordinates of a shape
  //
  // Given a shape on the screen, will return an SVGPoint for the directions
  // n(north), s(south), e(east), w(west), ne(northeast), se(southeast), nw(northwest),
  // sw(southwest).
  //
  //    +-+-+
  //    |   |
  //    +   +
  //    |   |
  //    +-+-+
  //
  // Returns an Object {n, s, e, w, nw, sw, ne, se}
  function get_screen_bbox() {
    var target     = d3.event.target,
        bbox       = {},
        matrix     = target.getScreenCTM(),
        tbbox      = target.getBBox(),
        width      = tbbox.width,
        height     = tbbox.height,
        x          = tbbox.x,
        y          = tbbox.y,
        scrollTop = document.body.scrollTop
        scrollLeft = document.body.scrollLeft

    if(document.documentElement && document.documentElement.scrollTop) {
      scrollTop = document.documentElement.scrollTop
      scrollLeft = document.documentElement.scrollLeft
    }

    point.x = x + scrollLeft
    point.y = y + scrollTop
    bbox.nw = point.matrixTransform(matrix)
    point.x += width
    bbox.ne = point.matrixTransform(matrix)
    point.y += height
    bbox.se = point.matrixTransform(matrix)
    point.x -= width
    bbox.sw = point.matrixTransform(matrix)
    point.y -= height / 2
    bbox.w  = point.matrixTransform(matrix)
    point.x += width
    bbox.e = point.matrixTransform(matrix)
    point.x -= width / 2
    point.y -= height / 2
    bbox.n = point.matrixTransform(matrix)
    point.y += height
    bbox.s = point.matrixTransform(matrix)

    return bbox
  }

  return tip;
}
<|endoftext|>


<commit_before>/* eslint-disable */
import chai, { expect } from 'chai';
import DayPicker from '../lib/DayPicker.js';
import React from 'react';
import { shallow, mount, render } from 'enzyme';
import { jsdom } from 'jsdom';
import chaiEnzyme from 'chai-enzyme';

chai.use(chaiEnzyme());

global.document = jsdom('<!doctype html>');
global.window = document.defaultView;
global.navigator = global.window.navigator;

describe('DayPicker', () => {

    describe('渲染', () => {
        it('默认Props检查', () => {
            const dayPicker = <DayPicker />;
            const now = new Date();
            expect(dayPicker.props.initialMonth.getMonth()).to.equal(now.getMonth());
            expect(dayPicker.props.initialMonth.getYear()).to.equal(now.getYear());
            expect(dayPicker.props.numberOfMonths).to.equal(1);
            expect(dayPicker.props.canChangeMonth).to.equal(true);
            expect(dayPicker.props.fixedWeeks).to.equal(false);
            expect(dayPicker.props.modifiers).to.deep.equal({});
        })
        it('默认class检查', () => {
            const dayPicker = shallow(<DayPicker />);
            expect(dayPicker).to.have.className('DayPicker');
        });
        it('首次渲染把初始化月份设置为当前月份', () => {
            const wrapper = shallow(<DayPicker />);
            const instance = wrapper.instance();
            expect(instance.props.initialMonth.getFullYear()).to.equal(instance.state.currentMonth.getFullYear());
            expect(instance.props.initialMonth.getMonth()).to.equal(instance.state.currentMonth.getMonth());
        });
        it('渲染多个月份', () => {
            const wrapper = shallow(<DayPicker numberOfMonths={6} />);
            expect(wrapper.find('.DayPicker-Month')).to.have.length(6);
        });
        it('渲染完成后更改初始月份要求月份发生改变', () => {
            const wrapper = mount(<DayPicker />);
            wrapper.setProps({ initialMonth: new Date(2016, 1, 15) });
            const instance = wrapper.instance();
            expect(instance.state.currentMonth.getFullYear()).to.equal(2016);
            expect(instance.state.currentMonth.getMonth()).to.equal(1);
        });
        it('能够自定义class', () => {
            const wrapper = shallow(<DayPicker className="test" />);
            expect(wrapper).to.have.className('test');
        });
        it('需要能够有navBar渲染', () => {
            const wrapper = render(<DayPicker />);
            expect(wrapper.find('.DayPicker-NavBar')).to.exist;
        });
        it('所有的Day的容器都要能够渲染出来', () => {
            const wrapper = render(<DayPicker initialMonth={new Date(2016, 6)} />);
            expect(wrapper.find('.DayPicker-Day')).to.have.length(42);
        });
        it('应该能够渲染排除outside', () => {
            const wrapper = render(<DayPicker initialMonth={new Date(2016, 6)} />);
            expect(wrapper.find('.DayPicker-Day--outside')).to.have.length(11);
        });
        it('如果设置不能改变月份,navbar应该为空', () => {
            const wrapper = render(<DayPicker canChangeMonth={false} />);
            expect(wrapper.find('.DayPicker-NavBar').children('span')).to.have.length(0);
        });
        it('如果设置能改变月份,navbar应该不为空', () => {
            const wrapper = render(<DayPicker canChangeMonth={true} />);
            expect(wrapper.find('.DayPicker-NavBar').children('span')).to.have.length(2);
        });
        it('不应该渲染多余的天数', () => {
            const wrapper = mount(<DayPicker initialMonth={new Date(2016, 6)} />);
            expect(wrapper.find('.DayPicker-Day').at(0)).to.have.text('');
            expect(wrapper.find('.DayPicker-Day').at(1)).to.have.text('');
            expect(wrapper.find('.DayPicker-Day').at(2)).to.have.text('');
            expect(wrapper.find('.DayPicker-Day').at(3)).to.have.text('');
            expect(wrapper.find('.DayPicker-Day').at(4)).to.have.text('');
        });
        it('需要把额外的日期渲染出来', () => {
            const wrapper = mount(<DayPicker fixedWeeks initialMonth={new Date(2016, 6)} />);
            expect(wrapper.find('.DayPicker-Day').at(0)).to.have.text('26');
            expect(wrapper.find('.DayPicker-Day').at(1)).to.have.text('27');
            expect(wrapper.find('.DayPicker-Day').at(2)).to.have.text('28');
            expect(wrapper.find('.DayPicker-Day').at(3)).to.have.text('29');
            expect(wrapper.find('.DayPicker-Day').at(4)).to.have.text('30');
        });
    })

});
/* eslint-disable */

<commit_msg>react daypicker test<commit_after>/* eslint-disable */
import chai, { expect } from 'chai';
import DayPicker from '../lib/DayPicker.js';
import React from 'react';
import { shallow, mount, render } from 'enzyme';
import { jsdom } from 'jsdom';
import chaiEnzyme from 'chai-enzyme';

chai.use(chaiEnzyme());

global.document = jsdom('<!doctype html>');
global.window = document.defaultView;
global.navigator = global.window.navigator;

describe('DayPicker', () => {

    describe('渲染测试', () => {
        it('默认Props检查', () => {
            const dayPicker = <DayPicker />;
            const now = new Date();
            expect(dayPicker.props.initialMonth.getMonth()).to.equal(now.getMonth());
            expect(dayPicker.props.initialMonth.getYear()).to.equal(now.getYear());
            expect(dayPicker.props.numberOfMonths).to.equal(1);
            expect(dayPicker.props.canChangeMonth).to.equal(true);
            expect(dayPicker.props.fixedWeeks).to.equal(false);
            expect(dayPicker.props.modifiers).to.deep.equal({});
        })
        it('默认class检查', () => {
            const dayPicker = shallow(<DayPicker />);
            expect(dayPicker).to.have.className('DayPicker');
        });
        it('首次渲染把初始化月份设置为当前月份', () => {
            const wrapper = shallow(<DayPicker />);
            const instance = wrapper.instance();
            expect(instance.props.initialMonth.getFullYear()).to.equal(instance.state.currentMonth.getFullYear());
            expect(instance.props.initialMonth.getMonth()).to.equal(instance.state.currentMonth.getMonth());
        });
        it('渲染多个月份', () => {
            const wrapper = shallow(<DayPicker numberOfMonths={6} />);
            expect(wrapper.find('.DayPicker-Month')).to.have.length(6);
        });
        it('渲染完成后更改初始月份要求月份发生改变', () => {
            const wrapper = mount(<DayPicker />);
            wrapper.setProps({ initialMonth: new Date(2016, 1, 15) });
            const instance = wrapper.instance();
            expect(instance.state.currentMonth.getFullYear()).to.equal(2016);
            expect(instance.state.currentMonth.getMonth()).to.equal(1);
        });
        it('能够自定义class', () => {
            const wrapper = shallow(<DayPicker className="test" />);
            expect(wrapper).to.have.className('test');
        });
        it('需要能够有navBar渲染', () => {
            const wrapper = render(<DayPicker />);
            expect(wrapper.find('.DayPicker-NavBar')).to.exist;
        });
        it('所有的Day的容器都要能够渲染出来', () => {
            const wrapper = render(<DayPicker initialMonth={new Date(2016, 6)} />);
            expect(wrapper.find('.DayPicker-Day')).to.have.length(42);
        });
        it('应该能够渲染排除outside', () => {
            const wrapper = render(<DayPicker initialMonth={new Date(2016, 6)} />);
            expect(wrapper.find('.DayPicker-Day--outside')).to.have.length(11);
        });
        it('如果设置不能改变月份,navbar应该为空', () => {
            const wrapper = render(<DayPicker canChangeMonth={false} />);
            expect(wrapper.find('.DayPicker-NavBar').children('span')).to.have.length(0);
        });
        it('如果设置能改变月份,navbar应该不为空', () => {
            const wrapper = render(<DayPicker canChangeMonth={true} />);
            expect(wrapper.find('.DayPicker-NavBar').children('span')).to.have.length(2);
        });
        it('不应该渲染多余的天数', () => {
            const wrapper = mount(<DayPicker initialMonth={new Date(2016, 6)} />);
            expect(wrapper.find('.DayPicker-Day').at(0)).to.have.text('');
            expect(wrapper.find('.DayPicker-Day').at(1)).to.have.text('');
            expect(wrapper.find('.DayPicker-Day').at(2)).to.have.text('');
            expect(wrapper.find('.DayPicker-Day').at(3)).to.have.text('');
            expect(wrapper.find('.DayPicker-Day').at(4)).to.have.text('');
        });
        it('需要把额外的日期渲染出来', () => {
            const wrapper = mount(<DayPicker fixedWeeks initialMonth={new Date(2016, 6)} />);
            expect(wrapper.find('.DayPicker-Day').at(0)).to.have.text('26');
            expect(wrapper.find('.DayPicker-Day').at(1)).to.have.text('27');
            expect(wrapper.find('.DayPicker-Day').at(2)).to.have.text('28');
            expect(wrapper.find('.DayPicker-Day').at(3)).to.have.text('29');
            expect(wrapper.find('.DayPicker-Day').at(4)).to.have.text('30');
        });
    });

    describe('自带修饰器、自定义修饰器测试', () => {
        it('添加自带选中日期修饰器，当月所有日期都被选中', () => {
            const wrapper = mount(<DayPicker initialMonth={new Date(2016, 6)} selectedDays={() => true} />);
            expect(wrapper.find('.DayPicker-Day--selected')).to.have.length(42);
        });
        it('添加自带禁止操作日期修饰器，当月所有日期都不可操作', () => {
            const wrapper = mount(<DayPicker initialMonth={new Date(2016, 6)} disabledDays={() => true} />);
            expect(wrapper.find('.DayPicker-Day--disabled')).to.have.length(42);
        });
        it('添加一个修饰器foo能够在className体现', () => {
            const wrapper = mount(<DayPicker initialMonth={new Date(2016, 6)} modifiers={{ foo: () => true }} />);
            expect(wrapper.find('.DayPicker-Day--foo')).to.have.length(42);
        });
    });

    describe('月份调整：showNextMonth()', () => {
        it('currentMonth要变成下个月', () => {
          const instance = shallow(
            <DayPicker
              initialMonth={new Date(2016, 6)}
              numberOfMonths={2}
            />
          ).instance();
          instance.showNextMonth();
          expect(instance.state.currentMonth.getFullYear()).to.equal(2016);
          expect(instance.state.currentMonth.getMonth()).to.equal(7);
        });
    });
    describe('月份调整：showPreviousMonth()', () => {
        it('currentMonth要变成上个月', () => {
          const instance = shallow(
            <DayPicker
              initialMonth={new Date(2016, 6)}
              numberOfMonths={2}
            />
          ).instance();
          instance.showPreviousMonth();
          expect(instance.state.currentMonth.getFullYear()).to.equal(2016);
          expect(instance.state.currentMonth.getMonth()).to.equal(5);
        });
    });
    describe('任意月份调整:ShowMonth()', () => {
        it('2016年7月', () => {
          const instance = shallow(
            <DayPicker
              initialMonth={new Date(2015, 6)}
              numberOfMonths={2}
            />
          ).instance();
          instance.showMonth(new Date(2016, 6, 1));
          expect(instance.state.currentMonth.getFullYear()).to.equal(2016);
          expect(instance.state.currentMonth.getMonth()).to.equal(6);
          expect(instance.state.currentMonth.getDate()).to.equal(1);
        });
    });
    describe('事件处理eventHandlers', () => {

    });

});
/* eslint-disable */

<|endoftext|>