<commit_before>// Adapted from http://www.lostdecadegames.com/how-to-make-a-simple-html5-canvas-game/

$(document).ready(function() {

    // Get rid of title screen
    $('button#playbutton').on('click', function() {
        $('div#game').fadeOut("slow", function() {

            // Show instruction bar
            $('div#instructions').fadeIn();

            // Create the canvas
            var canvas = document.createElement("canvas");
            var ctx = canvas.getContext("2d");
            canvas.width = 640;
            canvas.height = 480;
            document.body.appendChild(canvas);
            $(canvas).css('display:block;')

            /*
            * | #  | Objective                                                 |
            * | -- | --------------------------------------------------------- |
            * | 0  | Collide with the ATP to become glucose with 1 phosphate   |
            * | 1  | Collide with another ATP to have 2 phosphates             |
            * | 2  | Split into 2 molecules                                    |
            * | 3  | Control one of the G3Ps, find a phosphate                 |
            * | 4  | Control the same G3P, find an NADH, create NAD+           |
            * | 5  | Control the same G3P, find an ADP, lose a phosphate       |
            * | 6  | Control the same G3P, find another ADP, lose a phosphate  |
            * | 7  | step 3 with the other G3P                                 |
            * | 8  | step 4 with the other G3P                                 |
            * | 9  | step 5 with the other G3P                                 |
            * | 10 | step 6 with the other G3P                                 |
            */
            var level = 0

            console.log("Canvas created.");

            // Background image
            var bgReady = false;
            var bgImage = new Image();
            bgImage.onload = function() {
                bgReady = true;
            }
            bgImage.src = "images/background.png" // todo

            // Prepare molecules
            var glucoseReady = false;
            var glucoseImage = new Image();
            glucoseImage.onload = function() {
                glucoseReady = true;
            };
            glucoseImage.src = "images/glucose.png";

            var nadReady = false;
            var nadImage = new Image();
            nadImage.onload = function() {
                nadReady = true;
            };
            nadImage.src = "images/nad.png";

            var atpReady = false;
            var atpImage = new Image();
            atpImage.onload = function() {
                atpReady = true;
            };
            atpImage.src = "images/atp.png";

            var atp2Ready = false;
            var atp2Image = new Image();
            atp2Image.onload = function() {
                atp2Ready = true;
            };
            atp2Image.src = "images/atp.png";

            console.log("Images loaded.");

            var g1Ready = false;
            var g1Image = new Image();
            g1Image.onload = function () {
                g1Ready = true;
            };
            g1Image.src = "images/c3p1.png"

            var g2Ready = false;
            var g2Image = new Image();
            g2Image.onload = function () {
                g2Ready = true;
            };
            g2Image.src = "images/c3p2.png"

            // Game objects
            var glucose = {
                speed: 10, // movement in pixels/second
                x: 600,
                y: 400,
                length: 240, // in px; these are used to bound movement
                width: 40,
            };

            var nad = {
                speed: 2, // this needs to be used
                x: 0,
                y: 0,
                length: 30, // in px
                width: 20,
            };

            var atp = {
                speed: 5, // this needs to be used
                x: 0,
                y: 0,
                length: 60, // in px
                width: 15,
            }

            var g1 = {
                speed: 7,
                x: -500,
                y: -500,
                length: 125,
                width: 40,
            }

            var g2 = {
                speed: 7,
                x: -500,
                y: -500,
                length: 125,
                width: 40,
            }

            console.log("Classes successfully setup.");

            var points = 0;
            var characters = [glucose, atp, nad]
            // Handle keyboard controls
            var keysDown = {};

            addEventListener("keydown", function (e) {
                keysDown[e.keyCode] = true;
            }, false);

            addEventListener("keyup", function (e) {
                delete keysDown[e.keyCode];
            }, false);

            console.log("Event listeners setup.");

            // setup positions of molecules in beginning
            var setup = function() {
                glucose.x = (Math.random() * (canvas.width - 64)) + 32;
                glucose.y = (Math.random() * (canvas.height - 64)) + 32;

                nad.x = (Math.random() * (canvas.width - 64)) + 32;
                nad.y = (Math.random() * (canvas.height - 64)) + 32;

                atp.x = (Math.random() * (canvas.width - 64)) + 32;
                atp.y = (Math.random() * (canvas.height - 64)) + 32;

                // g1 and g2 are off screen initially
                g1.x = -500;
                g1.y = -500;

                g2.x = -500;
                g2.y = -500;
            }

            var mashCount = 0;
            var keyToPress = 37;
            // Update game objects
            var update = function(modifier) {
                // move glucose according to keyboard
                if (38 in keysDown) { // Player holding up
                    if (level < 3) {
                        glucose.y -= glucose.speed;
                    } else if (level >= 3 && level < 7) {
                        g1.y -= g1.speed;
                    } else if (level >= 7) {
                        g2.y -= g2.speed;
                    }
                }
                if (40 in keysDown) { // Player holding down
                    if (level < 3) {
                        glucose.y += glucose.speed;
                    } else if (level >= 3 && level < 7) {
                        g1.y += g1.speed;
                    } else if (level >= 7) {
                        g2.y += g2.speed;
                    }
                }
                if (37 in keysDown) { // Player holding left
                    if (level < 3) {
                        glucose.x -= glucose.speed;
                    } else if (level >= 3 && level < 7) {
                        g1.x -= g1.speed;
                    } else if (level >= 7) {
                        g2.x -= g2.speed;
                    }
                }
                if (39 in keysDown) { // Player holding right
                    if (level < 3) {
                        glucose.x += glucose.speed;
                    } else if (level >= 3 && level < 7) {
                        g1.x += g1.speed;
                    } else if (level >= 7) {
                        g2.x += g2.speed;
                    }
                }

                // Gain points (collision detection)
                if (
                    glucose.x <= (atp.x + atp.length) &&
                    atp.x <= (glucose.x + glucose.length) &&
                    glucose.y <= (atp.y + atp.width) &&
                    atp.y <= (glucose.y + glucose.width)
                ) {
                    if (level == 0) {
                        glucoseImage.src = "images/glucose_phosphate-2.png"
                        glucose.length = 245;

                        // Move ATP
                        atp.x = (Math.random() * (canvas.width - 64)) + 32;
                        atp.y = (Math.random() * (canvas.height - 64)) + 32;

                        level++;
                        console.log("Now at level " + level);
                        $('#instructions').html('
Great job! Now get to the other ATP.
');

                    } else if (level == 1) {
                        glucoseImage.src = "images/glucose_phosphate.png"
                        glucose.length = 250;
                        atpImage.src = "images/adp.png"
                        level++;
                        console.log("Now at level " + level);
                    }
                }
                if (level == 2) {
                    $('#instructions').html('
You did it! Now you need to change into 2 G3P molecules. To do this, alternate button mashing your left and right keys.
')

                    if (keyToPress == 37 && 37 in keysDown) {
                        mashCount++;
                        keyToPress = 39;
                    } else if (keyToPress == 39 && 39 in keysDown) {
                        mashCount++;
                        keyToPress = 37;
                    } if (mashCount < 40) {
                    } else {
                        level++;

                        // add g1 and g2
                        g1.x = glucose.x;
                        g1.y = glucose.y;
                        console.log("g1: (" + g1.x + ", " + g1.y + ")");

                        g2.x = glucose.x + glucose.length/2 + 5;
                        g2.y = glucose.y;
                        console.log("g2: (" + g2.x + ", " + g2.y + ")");

                        characters.push(g1, g2);
                        console.log(characters);

                        // remove glucose
                        glucose.x = -50;
                        glucose.y = -50;
                        console.log("glucose: (" + glucose.x + ", " + glucose.y + ")");
                        characters.splice(characters.indexOf(glucose), 1);
                        console.log(characters);
                    }
                }
                // Move NAD+ randomly
                // 2 steps in either direction
                probability = Math.random();
                if (probability >= 0.75) {
                    nad.x += nad.speed;
                } else if (probability <= 0.25){
                    nad.x -= nad.speed;
                }

                probability = Math.random();
                if (probability >= 0.75) {
                    nad.y += nad.speed;
                } else if (probability <= 0.25){
                    nad.y -= nad.speed;
                }

                // Move atp randomly, reduced jitters
                probability = Math.random();
                if (probability >= 0.75) {
                    atp.x += atp.speed;
                } else if (probability <= 0.25){
                    atp.x -= atp.speed;
                }

                probability = Math.random();
                if (probability >= 0.75) {
                    atp.y += atp.speed;
                } else if (probability <= 0.25){
                    atp.y -= atp.speed;
                }

                // don't go out of bounds
                for (i = 0; i < characters.length; i++) {
                    if (characters[i].y < 0) {
                        characters[i].y = 0;
                    } else if (characters[i].y + characters[i].width > canvas.height) {
                        characters[i].y = canvas.height - characters[i].width;
                    }
                    if (characters[i].x < 0) {
                        characters[i].x = 0;
                    } else if (characters[i].x + characters[i].length > canvas.width) {
                        characters[i].x = canvas.width - characters[i].length;
                    }
                }
                //console.log("Update function successful.");
            };

            // Draw everything
            var render = function() {
                if (bgReady) {
                    ctx.drawImage(bgImage, 0, 0);
                }
                if (glucoseReady) {
                    ctx.drawImage(glucoseImage, glucose.x, glucose.y);
                }
                if (atpReady) {
                    ctx.drawImage(atpImage, atp.x, atp.y);
                }
                if (nadReady) {
                    ctx.drawImage(nadImage, nad.x, nad.y);
                }
                if (g1Ready) {
                    ctx.drawImage(g1Image, g1.x, g1.y);
                }
                if (g2Ready) {
                    ctx.drawImage(g2Image, g2.x, g2.y);
                }
                /* We don't need to display points anymore
                // Score
                ctx.fillStyle = "rgb(0, 0, 0)";
                ctx.font = "24px Helvetica";
                ctx.textAlign = "left";
                ctx.textBaseline = "top";
                ctx.fillText("Points: " + points, 32, 32);
                //console.log("THINGS HAVE BEEN DOODLED.");
                */
            };

            // The main game loop
            var main = function(then) {
                var now = Date.now();
                var delta = now - then;

                update(delta / 1000);
                render();

                then = now;

                // Request to do this again ASAP
                requestAnimationFrame(main);
                //console.log("Main run.");
            };

            // Cross-browser support for requestAnimationFrame
            var w = window;
            requestAnimationFrame = w.requestAnimationFrame || w.webkitRequestAnimationFrame || w.msRequestAnimationFrame || w.mozRequestAnimationFrame;

            // Play the game
            console.log("Game begun.");
            var then = Date.now();
            main(then);
        });
    });
});
<commit_msg>Pi level<commit_after>// Adapted from http://www.lostdecadegames.com/how-to-make-a-simple-html5-canvas-game/

$(document).ready(function() {

    // Get rid of title screen
    $('button#playbutton').on('click', function() {
        $('div#game').fadeOut("slow", function() {

            // Show instruction bar
            $('div#instructions').fadeIn();

            // Create the canvas
            var canvas = document.createElement("canvas");
            var ctx = canvas.getContext("2d");
            canvas.width = 640;
            canvas.height = 480;
            document.body.appendChild(canvas);
            $(canvas).css('display:block;')

            /*
            * | #  | Objective                                                 |
            * | -- | --------------------------------------------------------- |
            * | 0  | Collide with the ATP to become glucose with 1 phosphate   |
            * | 1  | Collide with another ATP to have 2 phosphates             |
            * | 2  | Split into 2 molecules                                    |
            * | 3  | Control one of the G3Ps, find a phosphate                 |
            * | 4  | Control the same G3P, find an NADH, create NAD+           |
            * | 5  | Control the same G3P, find an ADP, lose a phosphate       |
            * | 6  | Control the same G3P, find another ADP, lose a phosphate  |
            * | 7  | step 3 with the other G3P                                 |
            * | 8  | step 4 with the other G3P                                 |
            * | 9  | step 5 with the other G3P                                 |
            * | 10 | step 6 with the other G3P                                 |
            */
            var level = 0

            console.log("Canvas created.");

            // Background image
            var bgReady = false;
            var bgImage = new Image();
            bgImage.onload = function() {
                bgReady = true;
            }
            bgImage.src = "images/background.png" // todo

            // Prepare molecules
            var glucoseReady = false;
            var glucoseImage = new Image();
            glucoseImage.onload = function() {
                glucoseReady = true;
            };
            glucoseImage.src = "images/glucose.png";

            var nadReady = false;
            var nadImage = new Image();
            nadImage.onload = function() {
                nadReady = true;
            };
            nadImage.src = "images/nad.png";

            var atpReady = false;
            var atpImage = new Image();
            atpImage.onload = function() {
                atpReady = true;
            };
            atpImage.src = "images/atp.png";

            var atp2Ready = false;
            var atp2Image = new Image();
            atp2Image.onload = function() {
                atp2Ready = true;
            };
            atp2Image.src = "images/atp.png";

            console.log("Images loaded.");

            var g1Ready = false;
            var g1Image = new Image();
            g1Image.onload = function () {
                g1Ready = true;
            };
            g1Image.src = "images/c3p1.png"

            var g2Ready = false;
            var g2Image = new Image();
            g2Image.onload = function () {
                g2Ready = true;
            };
            g2Image.src = "images/c3p2.png"

            var piReady = false;
            var piImage = new Image();
            piImage.onload = function () {
                piImage = true;
            };
            piImage.src = "images/phosphate.png"

            // Game objects
            var glucose = {
                speed: 10, // movement in pixels/second
                x: 600,
                y: 400,
                length: 240, // in px; these are used to bound movement
                width: 40,
            };

            var nad = {
                speed: 2, // this needs to be used
                x: 0,
                y: 0,
                length: 30, // in px
                width: 20,
            };

            var atp = {
                speed: 5, // this needs to be used
                x: 0,
                y: 0,
                length: 60, // in px
                width: 15,
            }

            // g3p #1
            var g1 = {
                speed: 7,
                x: -500,
                y: -500,
                length: 125,
                width: 40,
            }

            // g3p #2
            var g2 = {
                speed: 7,
                x: -500,
                y: -500,
                length: 125,
                width: 40,
            }

            // inorganic phosphate
            var pi = {
                speed: 3,
                x: -500,
                y: -500,
                length: 125,
                width: 40,
            }

            console.log("Classes successfully setup.");

            var points = 0;
            var characters = [glucose, atp, nad]
            // Handle keyboard controls
            var keysDown = {};

            addEventListener("keydown", function (e) {
                keysDown[e.keyCode] = true;
            }, false);

            addEventListener("keyup", function (e) {
                delete keysDown[e.keyCode];
            }, false);

            console.log("Event listeners setup.");

            // setup positions of molecules in beginning
            var setup = function() {
                glucose.x = (Math.random() * (canvas.width - 64)) + 32;
                glucose.y = (Math.random() * (canvas.height - 64)) + 32;

                nad.x = (Math.random() * (canvas.width - 64)) + 32;
                nad.y = (Math.random() * (canvas.height - 64)) + 32;

                atp.x = (Math.random() * (canvas.width - 64)) + 32;
                atp.y = (Math.random() * (canvas.height - 64)) + 32;

                // g1 and g2 are off screen initially
                g1.x = -500;
                g1.y = -500;

                g2.x = -500;
                g2.y = -500;
            }

            var mashCount = 0;
            var keyToPress = 37;
            // Update game objects
            var update = function(modifier) {
                // move glucose according to keyboard
                if (38 in keysDown) { // Player holding up
                    if (level < 3) {
                        glucose.y -= glucose.speed;
                    } else if (level >= 3 && level < 7) {
                        g1.y -= g1.speed;
                    } else if (level >= 7) {
                        g2.y -= g2.speed;
                    }
                }
                if (40 in keysDown) { // Player holding down
                    if (level < 3) {
                        glucose.y += glucose.speed;
                    } else if (level >= 3 && level < 7) {
                        g1.y += g1.speed;
                    } else if (level >= 7) {
                        g2.y += g2.speed;
                    }
                }
                if (37 in keysDown) { // Player holding left
                    if (level < 3) {
                        glucose.x -= glucose.speed;
                    } else if (level >= 3 && level < 7) {
                        g1.x -= g1.speed;
                    } else if (level >= 7) {
                        g2.x -= g2.speed;
                    }
                }
                if (39 in keysDown) { // Player holding right
                    if (level < 3) {
                        glucose.x += glucose.speed;
                    } else if (level >= 3 && level < 7) {
                        g1.x += g1.speed;
                    } else if (level >= 7) {
                        g2.x += g2.speed;
                    }
                }

                // Gain points (collision detection)
                if (
                    glucose.x <= (atp.x + atp.length) &&
                    atp.x <= (glucose.x + glucose.length) &&
                    glucose.y <= (atp.y + atp.width) &&
                    atp.y <= (glucose.y + glucose.width)
                ) {
                    if (level == 0) {
                        glucoseImage.src = "images/glucose_phosphate-2.png"
                        glucose.length = 245;

                        // Move ATP
                        atp.x = (Math.random() * (canvas.width - 64)) + 32;
                        atp.y = (Math.random() * (canvas.height - 64)) + 32;

                        level++;
                        console.log("Now at level " + level);
                        $('#instructions').html('
Great job! Now get to the other ATP.
');

                    } else if (level == 1) {
                        glucoseImage.src = "images/glucose_phosphate.png"
                        glucose.length = 250;
                        atpImage.src = "images/adp.png"
                        level++;
                        console.log("Now at level " + level);
                    }
                }
                if (level == 2) {
                    $('#instructions').html('
You did it! Now you need to change into 2 G3P molecules. To do this, alternate button mashing your left and right keys.
');

                    if (keyToPress == 37 && 37 in keysDown) {
                        mashCount++;
                        keyToPress = 39;
                    } else if (keyToPress == 39 && 39 in keysDown) {
                        mashCount++;
                        keyToPress = 37;
                    } if (mashCount < 40) {
                    } else {
                        level++;

                        // add g1 and g2
                        g1.x = glucose.x;
                        g1.y = glucose.y;
                        console.log("g1: (" + g1.x + ", " + g1.y + ")");

                        g2.x = canvas.width + 32;
                        g2.y = canvas.height + 32;
                        console.log("g2: (" + g2.x + ", " + g2.y + ")");

                        characters.push(g1, g2);
                        console.log(characters);

                        // remove glucose
                        glucose.x = -50;
                        glucose.y = -50;
                        console.log("glucose: (" + glucose.x + ", " + glucose.y + ")");
                        characters.splice(characters.indexOf(glucose), 1);
                        console.log(characters);
                    }
                } else if (level == 3) {
                    $('#instructions').html('
Great job! You now control one of the G3P molecules. First, find an inorganic phosphate.
');

                    // add phosphate (pi)
                    pi.x = (Math.random() * (canvas.width - 64)) + 32;
                    pi.y = (Math.random() * (canvas.height - 64)) + 32;

                    // detect g1/pi collision
                    if (
                        g1.x <= (pi.x + pi.length) &&
                        pi.x <= (g1.x + g1.length) &&
                        g1.y <= (pi.y + pi.width) &&
                        pi.y <= (g1.y + g1.width)
                    ) {
                        // hide pi
                        pi.x = -500;
                        pi.y = -500;

                        // update g1 image
                        g1Image.src = "images/c3p1_complete.png";
                    }
                }

                // Move NAD+ randomly
                // 2 steps in either direction
                probability = Math.random();
                if (probability >= 0.75) {
                    nad.x += nad.speed;
                } else if (probability <= 0.25){
                    nad.x -= nad.speed;
                }

                probability = Math.random();
                if (probability >= 0.75) {
                    nad.y += nad.speed;
                } else if (probability <= 0.25){
                    nad.y -= nad.speed;
                }

                // Move atp randomly, reduced jitters
                probability = Math.random();
                if (probability >= 0.75) {
                    atp.x += atp.speed;
                } else if (probability <= 0.25){
                    atp.x -= atp.speed;
                }

                probability = Math.random();
                if (probability >= 0.75) {
                    atp.y += atp.speed;
                } else if (probability <= 0.25){
                    atp.y -= atp.speed;
                }

                // don't go out of bounds
                for (i = 0; i < characters.length; i++) {
                    if (characters[i].y < 0) {
                        characters[i].y = 0;
                    } else if (characters[i].y + characters[i].width > canvas.height) {
                        characters[i].y = canvas.height - characters[i].width;
                    }
                    if (characters[i].x < 0) {
                        characters[i].x = 0;
                    } else if (characters[i].x + characters[i].length > canvas.width) {
                        characters[i].x = canvas.width - characters[i].length;
                    }
                }
                //console.log("Update function successful.");
            };

            // Draw everything
            var render = function() {
                if (bgReady) {
                    ctx.drawImage(bgImage, 0, 0);
                }
                if (glucoseReady) {
                    ctx.drawImage(glucoseImage, glucose.x, glucose.y);
                }
                if (atpReady) {
                    ctx.drawImage(atpImage, atp.x, atp.y);
                }
                if (nadReady) {
                    ctx.drawImage(nadImage, nad.x, nad.y);
                }
                if (g1Ready) {
                    ctx.drawImage(g1Image, g1.x, g1.y);
                }
                if (g2Ready) {
                    ctx.drawImage(g2Image, g2.x, g2.y);
                }
                if (piReady) {
                    ctx.drawImage(piImage, pi.x, pi.y);
                }
                /* We don't need to display points anymore
                // Score
                ctx.fillStyle = "rgb(0, 0, 0)";
                ctx.font = "24px Helvetica";
                ctx.textAlign = "left";
                ctx.textBaseline = "top";
                ctx.fillText("Points: " + points, 32, 32);
                //console.log("THINGS HAVE BEEN DOODLED.");
                */
            };

            // The main game loop
            var main = function(then) {
                var now = Date.now();
                var delta = now - then;

                update(delta / 1000);
                render();

                then = now;

                // Request to do this again ASAP
                requestAnimationFrame(main);
                //console.log("Main run.");
            };

            // Cross-browser support for requestAnimationFrame
            var w = window;
            requestAnimationFrame = w.requestAnimationFrame || w.webkitRequestAnimationFrame || w.msRequestAnimationFrame || w.mozRequestAnimationFrame;

            // Play the game
            console.log("Game begun.");
            var then = Date.now();
            main(then);
        });
    });
});
<|endoftext|>


<commit_before>'use strict'

const path = require('path')
const fs = require('fs-promise')

const gulp = require('gulp')
const execa = require('execa')
const lodash = require('lodash')
const request = require('request')
const revHash = require('rev-hash')
const archiver = require('archiver')
const uglifyJS = require('uglify-js')
const babelCore = require('babel-core')
const scriptTags = require('script-tags')
const prettyBytes = require('pretty-bytes')
const browserSync = require('browser-sync').create()
const htmlMinifier = require('html-minifier')
const minifyHtml = html => htmlMinifier.minify(html, {
  collapseWhitespace: true,
  conservativeCollapse: true,
  minifyCSS: true,
  minifyJS: true,
  removeComments: true,
  removeRedundantAttributes: true,
  sortAttributes: true,
  sortClassName: true
})

const debug = require('./src/compile/debug.js')
const paths = require('./src/compile/paths.js')
const config = require('./src/compile/config.js')
const articles = require('./src/compile/articles.js')
const nunjucksEnv = require('./src/compile/nunjucks/env.js')

if (!process.env.CI) { // Travis adds this env variable
  debug()
}

//
//
// Local state
//
//

const productionBuild = process.env.BUILD_ENV === 'production'
const nunjucks = nunjucksEnv(productionBuild)

console.log(`production build: ${productionBuild}`)
console.log(`NODE_ENV: ${process.env.NODE_ENV}`)

//
//
// Site compilation
//
//

gulp.task('site:prepare-dirs', done => {
  fs.remove(paths.dist)
    .then(() => {
      return fs.mkdir(paths.dist)
    })
    .then(() => {
      return Promise.all([
        fs.mkdir(paths.distDrafts),
        fs.mkdir(paths.distStatic)
      ])
    })
    .then(() => {
      return Promise.all([
        fs.mkdir(paths.distImages),
        fs.mkdir(paths.distStyles),
        fs.mkdir(paths.distScripts)
      ])
    })
    .then(() => done())
})

gulp.task('site:static-pages', done => {
  const pages = [
    { from: '404.njk', to: '404.html' },
    { from: 'robots.txt.njk', to: 'robots.txt' }
  ]

  for (const page of pages) {
    let html = nunjucks.render(page.from)
    if (productionBuild) {
      html = minifyHtml(html)
    }

    fs.writeFile(path.join(paths.dist, page.to), html)
      .then(done)
  }
})

gulp.task('site:styles', done => {
  if (!productionBuild) {
    fs.remove(paths.distStyles)
      .then(() => fs.symlink(paths.styles, paths.distStyles))
      .then(() => done())
    return
  }

  var postCss = require('postcss')
  var postCssImport = require('postcss-import')
  var postCssNext = require('postcss-cssnext')
  var cssnano = require('cssnano')

  const from = path.join(paths.styles, 'styles.css')

  postCss([
    postCssImport(),
    postCssNext({ browsers: config.supportedBrowsers }),
    cssnano({ autoprefixer: false })
  ])
    .process(fs.readFileSync(from, 'utf8'), {
      from: from,
      map: {
        inline: false,
        annotation: 'styles.map.css'
      }
    })
    .then(function (result) {
      const hashCss = revHash(Buffer.from(result.css))
      nunjucks.addGlobal('hashCss', hashCss)

      fs.writeFileSync(path.join(paths.distStyles, `styles-${hashCss}.css`), result.css)
      fs.writeFileSync(path.join(paths.distStyles, 'styles.map.css'), result.map)

      done()
    })
})

gulp.task('site:scripts', done => {
  if (!productionBuild) {
    fs.remove(paths.distScripts)
      .then(() => fs.symlink(paths.scripts, paths.distScripts))
      .then(() => done())
    return
  }

  const baseTemplate = fs.readFileSync(path.join(paths.templates, '/extends/base.njk'), 'utf8')

  let code = scriptTags(baseTemplate)
  code = lodash.filter(code, script => script.attrs.src)
  code = lodash.filter(code, script => 'data-dev' in script.attrs)
  code = lodash.map(code, script => script.attrs.src)
  code = lodash.map(code, src => path.join(paths.src, src))
  code = lodash.map(code, path => fs.readFileSync(path, 'utf8'))
  code = lodash.join(code, ';\n')
  code = babelCore.transform(code, {
    presets: [
      ['env', {
        targets: { browsers: config.supportedBrowsers },
        loose: true
      }]
    ]
  }).code
  code = uglifyJS.minify(code, { fromString: true }).code

  const hashJs = revHash(Buffer.from(code))
  nunjucks.addGlobal('hashJs', hashJs)

  fs.writeFileSync(path.join(paths.distScripts, `scripts-${hashJs}.js`), code)

  done()
})

gulp.task('site:images', done => {
  if (productionBuild) {
    fs.copy(paths.images, paths.distImages)
      .then(() => done())
  } else {
    fs.remove(paths.distImages)
      .then(() => fs.symlink(paths.images, paths.distImages))
      .then(() => done())
  }
})

gulp.task('site:node-modules', done => {
  if (!productionBuild) {
    fs.symlink(paths.nodeModules, paths.distNodeModules)
      .then(() => done())
  } else {
    done()
  }
})

gulp.task('site:articles', done => {
  // gather articles data
  let articlesData = articles.getArticles(paths.articles, paths.articlesDrafts, nunjucks)
  articlesData = lodash.sortBy(articlesData, 'metadata.dateLastUpdate')
  articlesData = articlesData.reverse()

  let articlesPublishedData = lodash.filter(articlesData, article => article.metadata.published === true)
  articlesPublishedData = lodash.filter(articlesPublishedData, article => article.metadata.dateLastUpdate <= new Date())

  // index page
  const indexArticles = lodash.slice(articlesPublishedData, 0, config.articles.perPage)
  let htmlIndex = nunjucks.render('index.njk', {articles: indexArticles})
  if (productionBuild) {
    htmlIndex = minifyHtml(htmlIndex)
  }

  fs.writeFileSync(path.join(paths.dist, 'index.html'), htmlIndex)

  // articles
  const articlePromises = []
  for (const article of articlesData) {
    // article directory
    const folder = path.join(
      article.metadata.published ? paths.dist : paths.distDrafts,
      article.metadata.url
    )

    const promise = fs.mkdir(folder)
      .then(() => {
        // article html
        let htmlArticle = nunjucks.render('article.njk', article)
        if (productionBuild) {
          htmlArticle = minifyHtml(htmlArticle)
        }

        const promiseHtml = fs.writeFile(path.join(folder, 'index.html'), htmlArticle)
        let promiseImages
        let promiseSnippets
        if (productionBuild) {
          const imagesPath = path.join(article.fs.path, paths.articleImages)
          if (fs.existsSync(imagesPath)) {
            promiseImages = fs.copy(imagesPath, path.join(folder, paths.articleImages))
          }

          const snippetsPath = path.join(article.fs.path, paths.articleSnippets)
          if (fs.existsSync(snippetsPath)) {
            promiseSnippets = fs.copy(snippetsPath, path.join(folder, paths.articleSnippets))
          }
        } else {
          promiseImages = fs.symlink(
            path.join(article.fs.path, paths.articleImages),
            path.join(folder, paths.articleImages)
          )
          promiseSnippets = fs.symlink(
            path.join(article.fs.path, paths.articleSnippets),
            path.join(folder, paths.articleSnippets)
          )
        }

        return Promise.all([
          promiseHtml,
          promiseImages,
          promiseSnippets
        ])
      })

    articlePromises.push(promise)
  }

  const rssPromise = Promise.resolve()
    .then(() => {
      const rssArticles = lodash.slice(articlesData, 0, config.articles.perRssFeed)
      const rssFeed = nunjucks.render('rss.njk', {articles: rssArticles})

      return fs.writeFile(path.join(paths.dist, 'rss.xml'), rssFeed)
    })

  const humansTxtPromise = Promise.resolve()
    .then(() => {
      const lastUpdate = articlesPublishedData[0].metadata.dateLastUpdate
      const humansTxt = nunjucks.render('humans.txt.njk', {lastUpdate: lastUpdate})

      return fs.writeFileSync(path.join(paths.dist, 'humans.txt'), humansTxt)
    })

  Promise.all([
    articlePromises,
    rssPromise,
    humansTxtPromise
  ]).then(() => done())
})

gulp.task('site:deploy', done => {
  const archive = archiver('zip')

  archive.on('error', err => {
    console.error(err)
    done()
  })

  const logFileSize = archive => {
    console.log('archive size:', prettyBytes(archive.pointer()))
  }

  if (productionBuild) {
    archive.pipe(
      request({
        method: 'POST',
        url: 'https://api.netlify.com/api/v1/sites/hurtak.netlify.com/deploys',
        headers: {
          'Content-Type': 'application/zip',
          Authorization: `Bearer ${process.env.NETLIFY_ACCESS_TOKEN}`
        }
      }, (error, _, body) => {
        logFileSize(archive)
        if (error) {
          console.error('upload failed:', error)
        } else {
          console.log('upload successful, server responded with:', body)
        }
        done()
      })
    )
  } else {
    const writeStream = fs.createWriteStream(path.join(paths.root, 'site.zip'))
    writeStream.on('close', () => {
      logFileSize(archive)
      done()
    })
    archive.pipe(writeStream)
  }

  archive.directory(paths.dist, '/')
  archive.finalize()
})

gulp.task('site:compile',
  productionBuild
    ? gulp.series(
        'site:prepare-dirs',
        gulp.parallel(
          gulp.series(
            gulp.parallel(
              'site:styles',
              'site:scripts'
            ),
            gulp.parallel(
              'site:static-pages',
              'site:articles'
            )
          ),
          'site:images'
        )
      )
    : gulp.series(
        'site:prepare-dirs',
        gulp.parallel(
          'site:static-pages',
          'site:styles',
          'site:scripts',
          'site:images',
          'site:node-modules',
          'site:articles'
        )
      )
)

//
//
// Browser sync
//
//

gulp.task('browser-sync:server', done => {
  browserSync.init({
    server: './dist',
    port: 8000,
    open: false,
    https: true,
    reloadOnRestart: true
  }, done)
})

gulp.task('browser-sync:reload-browser', done => {
  browserSync.reload()
  done()
})

//
//
// Tests
//
//

gulp.task('test:unit', done => {
  execa.shell('ava src/test/**/*.js')
    .then(() => done())
    .catch((res) => {
      console.log(res.stderr)
      done()
    })
})

gulp.task('test:lint', done => {
  execa.shell('standard --verbose "scripts/**/*.js" "src/**/*.js"')
    .then(() => done())
    .catch((res) => {
      console.log(res.stdout)
      done()
    })
})

gulp.task('test:coverage', done => {
  execa.shell('nyc --all --include="src" --exclude="src/test" ava src/test/**/*.js')
    .then(() => done())
    .catch((res) => {
      console.log(res.stderr)
      done()
    })
})

gulp.task('test:coverage-report', done => {
  execa.shell('nyc report --reporter=lcov')
    .then(() => done())
})

gulp.task('test:coveralls', done => {
  execa.shell('nyc report --reporter=text-lcov | coveralls')
    .then(() => done())
})

gulp.task('test:all', gulp.parallel(
  'test:lint',
  gulp.series('test:unit', 'test:coverage', 'test:coverage-report')
))

//
//
// Watches
//
//

gulp.task('watch:articles', () =>
  gulp.watch(['./articles/**/*', './src/**/*'],
    gulp.series(
      'site:compile',
      'browser-sync:reload-browser'
    )
))

gulp.task('watch:test', () =>
  gulp.watch(['./src/**/*.js'], gulp.series('test:all')
))

//
//
// Main tasks
//
//

gulp.task('dev',
  gulp.parallel(
    gulp.series('site:compile', 'browser-sync:server'),
    'test:all',
    'watch:articles',
    'watch:test'
  )
)

gulp.task('default', gulp.series('dev'))

//
//
// Continuous integration tasks
//
//

gulp.task('ci:test', gulp.series(
  'test:unit',
  'site:compile'
))

gulp.task('ci:deploy', gulp.series(
  'test:coverage',
  'site:compile',
  'site:deploy',
  'test:coveralls'
))
<commit_msg>ci: fix deploy task<commit_after>'use strict'

const path = require('path')
const fs = require('fs-promise')

const gulp = require('gulp')
const execa = require('execa')
const lodash = require('lodash')
const request = require('request')
const revHash = require('rev-hash')
const archiver = require('archiver')
const uglifyJS = require('uglify-js')
const babelCore = require('babel-core')
const scriptTags = require('script-tags')
const prettyBytes = require('pretty-bytes')
const browserSync = require('browser-sync').create()
const htmlMinifier = require('html-minifier')
const minifyHtml = html => htmlMinifier.minify(html, {
  collapseWhitespace: true,
  conservativeCollapse: true,
  minifyCSS: true,
  minifyJS: true,
  removeComments: true,
  removeRedundantAttributes: true,
  sortAttributes: true,
  sortClassName: true
})

const debug = require('./src/compile/debug.js')
const paths = require('./src/compile/paths.js')
const config = require('./src/compile/config.js')
const articles = require('./src/compile/articles.js')
const nunjucksEnv = require('./src/compile/nunjucks/env.js')

if (!process.env.CI) { // Travis adds this env variable
  debug()
}

//
//
// Local state
//
//

const productionBuild = process.env.BUILD_ENV === 'production'
const nunjucks = nunjucksEnv(productionBuild)

console.log(`production build: ${productionBuild}`)

//
//
// Site compilation
//
//

gulp.task('site:prepare-dirs', done => {
  fs.remove(paths.dist)
    .then(() => {
      return fs.mkdir(paths.dist)
    })
    .then(() => {
      return Promise.all([
        fs.mkdir(paths.distDrafts),
        fs.mkdir(paths.distStatic)
      ])
    })
    .then(() => {
      return Promise.all([
        fs.mkdir(paths.distImages),
        fs.mkdir(paths.distStyles),
        fs.mkdir(paths.distScripts)
      ])
    })
    .then(() => done())
})

gulp.task('site:static-pages', done => {
  const pages = [
    { from: '404.njk', to: '404.html' },
    { from: 'robots.txt.njk', to: 'robots.txt' }
  ]

  for (const page of pages) {
    let html = nunjucks.render(page.from)
    if (productionBuild) {
      html = minifyHtml(html)
    }

    fs.writeFile(path.join(paths.dist, page.to), html)
      .then(done)
  }
})

gulp.task('site:styles', done => {
  if (!productionBuild) {
    fs.remove(paths.distStyles)
      .then(() => fs.symlink(paths.styles, paths.distStyles))
      .then(() => done())
    return
  }

  var postCss = require('postcss')
  var postCssImport = require('postcss-import')
  var postCssNext = require('postcss-cssnext')
  var cssnano = require('cssnano')

  const from = path.join(paths.styles, 'styles.css')

  postCss([
    postCssImport(),
    postCssNext({ browsers: config.supportedBrowsers }),
    cssnano({ autoprefixer: false })
  ])
    .process(fs.readFileSync(from, 'utf8'), {
      from: from,
      map: {
        inline: false,
        annotation: 'styles.map.css'
      }
    })
    .then(function (result) {
      const hashCss = revHash(Buffer.from(result.css))
      nunjucks.addGlobal('hashCss', hashCss)

      fs.writeFileSync(path.join(paths.distStyles, `styles-${hashCss}.css`), result.css)
      fs.writeFileSync(path.join(paths.distStyles, 'styles.map.css'), result.map)

      done()
    })
})

gulp.task('site:scripts', done => {
  if (!productionBuild) {
    fs.remove(paths.distScripts)
      .then(() => fs.symlink(paths.scripts, paths.distScripts))
      .then(() => done())
    return
  }

  const baseTemplate = fs.readFileSync(path.join(paths.templates, '/extends/base.njk'), 'utf8')

  let code = scriptTags(baseTemplate)
  code = lodash.filter(code, script => script.attrs.src)
  code = lodash.filter(code, script => 'data-dev' in script.attrs)
  code = lodash.map(code, script => script.attrs.src)
  code = lodash.map(code, src => path.join(paths.src, src))
  code = lodash.map(code, path => fs.readFileSync(path, 'utf8'))
  code = lodash.join(code, ';\n')
  code = babelCore.transform(code, {
    presets: [
      ['env', {
        targets: { browsers: config.supportedBrowsers },
        loose: true
      }]
    ]
  }).code
  code = uglifyJS.minify(code, { fromString: true }).code

  const hashJs = revHash(Buffer.from(code))
  nunjucks.addGlobal('hashJs', hashJs)

  fs.writeFileSync(path.join(paths.distScripts, `scripts-${hashJs}.js`), code)

  done()
})

gulp.task('site:images', done => {
  if (productionBuild) {
    fs.copy(paths.images, paths.distImages)
      .then(() => done())
  } else {
    fs.remove(paths.distImages)
      .then(() => fs.symlink(paths.images, paths.distImages))
      .then(() => done())
  }
})

gulp.task('site:node-modules', done => {
  if (!productionBuild) {
    fs.symlink(paths.nodeModules, paths.distNodeModules)
      .then(() => done())
  } else {
    done()
  }
})

gulp.task('site:articles', done => {
  // gather articles data
  let articlesData = articles.getArticles(paths.articles, paths.articlesDrafts, nunjucks)
  articlesData = lodash.sortBy(articlesData, 'metadata.dateLastUpdate')
  articlesData = articlesData.reverse()

  let articlesPublishedData = lodash.filter(articlesData, article => article.metadata.published === true)
  articlesPublishedData = lodash.filter(articlesPublishedData, article => article.metadata.dateLastUpdate <= new Date())

  // index page
  const indexArticles = lodash.slice(articlesPublishedData, 0, config.articles.perPage)
  let htmlIndex = nunjucks.render('index.njk', {articles: indexArticles})
  if (productionBuild) {
    htmlIndex = minifyHtml(htmlIndex)
  }

  fs.writeFileSync(path.join(paths.dist, 'index.html'), htmlIndex)

  // articles
  const articlePromises = []
  for (const article of articlesData) {
    // article directory
    const folder = path.join(
      article.metadata.published ? paths.dist : paths.distDrafts,
      article.metadata.url
    )

    const promise = fs.mkdir(folder)
      .then(() => {
        // article html
        let htmlArticle = nunjucks.render('article.njk', article)
        if (productionBuild) {
          htmlArticle = minifyHtml(htmlArticle)
        }

        const promiseHtml = fs.writeFile(path.join(folder, 'index.html'), htmlArticle)
        let promiseImages
        let promiseSnippets
        if (productionBuild) {
          const imagesPath = path.join(article.fs.path, paths.articleImages)
          if (fs.existsSync(imagesPath)) {
            promiseImages = fs.copy(imagesPath, path.join(folder, paths.articleImages))
          }

          const snippetsPath = path.join(article.fs.path, paths.articleSnippets)
          if (fs.existsSync(snippetsPath)) {
            promiseSnippets = fs.copy(snippetsPath, path.join(folder, paths.articleSnippets))
          }
        } else {
          promiseImages = fs.symlink(
            path.join(article.fs.path, paths.articleImages),
            path.join(folder, paths.articleImages)
          )
          promiseSnippets = fs.symlink(
            path.join(article.fs.path, paths.articleSnippets),
            path.join(folder, paths.articleSnippets)
          )
        }

        return Promise.all([
          promiseHtml,
          promiseImages,
          promiseSnippets
        ])
      })

    articlePromises.push(promise)
  }

  const rssPromise = Promise.resolve()
    .then(() => {
      const rssArticles = lodash.slice(articlesData, 0, config.articles.perRssFeed)
      const rssFeed = nunjucks.render('rss.njk', {articles: rssArticles})

      return fs.writeFile(path.join(paths.dist, 'rss.xml'), rssFeed)
    })

  const humansTxtPromise = Promise.resolve()
    .then(() => {
      const lastUpdate = articlesPublishedData[0].metadata.dateLastUpdate
      const humansTxt = nunjucks.render('humans.txt.njk', {lastUpdate: lastUpdate})

      return fs.writeFileSync(path.join(paths.dist, 'humans.txt'), humansTxt)
    })

  Promise.all([
    articlePromises,
    rssPromise,
    humansTxtPromise
  ]).then(() => done())
})

gulp.task('site:deploy', done => {
  const archive = archiver('zip')

  archive.on('error', err => {
    console.error(err)
    done()
  })

  const logFileSize = archive => {
    console.log('archive size:', prettyBytes(archive.pointer()))
  }

  archive.directory(paths.dist, '/')
  archive.finalize()

  if (productionBuild) {
    archive.pipe(
      request({
        method: 'POST',
        url: 'https://api.netlify.com/api/v1/sites/hurtak.netlify.com/deploys',
        headers: {
          'Content-Type': 'application/zip',
          Authorization: `Bearer ${process.env.NETLIFY_ACCESS_TOKEN}`
        }
      }, (error, _, body) => {
        logFileSize(archive)
        if (error) {
          console.error('upload failed:', error)
        } else {
          console.log('upload successful, server responded with:', body)
        }
        done()
      })
    )
  } else {
    const writeStream = fs.createWriteStream(path.join(paths.root, 'site.zip'))
    writeStream.on('close', () => {
      logFileSize(archive)
      done()
    })
    archive.pipe(writeStream)
  }
})

gulp.task('site:compile',
  productionBuild
    ? gulp.series(
        'site:prepare-dirs',
        gulp.parallel(
          gulp.series(
            gulp.parallel(
              'site:styles',
              'site:scripts'
            ),
            gulp.parallel(
              'site:static-pages',
              'site:articles'
            )
          ),
          'site:images'
        )
      )
    : gulp.series(
        'site:prepare-dirs',
        gulp.parallel(
          'site:static-pages',
          'site:styles',
          'site:scripts',
          'site:images',
          'site:node-modules',
          'site:articles'
        )
      )
)

//
//
// Browser sync
//
//

gulp.task('browser-sync:server', done => {
  browserSync.init({
    server: './dist',
    port: 8000,
    open: false,
    https: true,
    reloadOnRestart: true
  }, done)
})

gulp.task('browser-sync:reload-browser', done => {
  browserSync.reload()
  done()
})

//
//
// Tests
//
//

gulp.task('test:unit', done => {
  execa.shell('ava src/test/**/*.js')
    .then(() => done())
    .catch((res) => {
      console.log(res.stderr)
      done()
    })
})

gulp.task('test:lint', done => {
  execa.shell('standard --verbose "scripts/**/*.js" "src/**/*.js"')
    .then(() => done())
    .catch((res) => {
      console.log(res.stdout)
      done()
    })
})

gulp.task('test:coverage', done => {
  execa.shell('nyc --all --include="src" --exclude="src/test" ava src/test/**/*.js')
    .then(() => done())
    .catch((res) => {
      console.log(res.stderr)
      done()
    })
})

gulp.task('test:coverage-report', done => {
  execa.shell('nyc report --reporter=lcov')
    .then(() => done())
})

gulp.task('test:coveralls', done => {
  execa.shell('nyc report --reporter=text-lcov | coveralls')
    .then(() => done())
})

gulp.task('test:all', gulp.parallel(
  'test:lint',
  gulp.series('test:unit', 'test:coverage', 'test:coverage-report')
))

//
//
// Watches
//
//

gulp.task('watch:articles', () =>
  gulp.watch(['./articles/**/*', './src/**/*'],
    gulp.series(
      'site:compile',
      'browser-sync:reload-browser'
    )
))

gulp.task('watch:test', () =>
  gulp.watch(['./src/**/*.js'], gulp.series('test:all')
))

//
//
// Main tasks
//
//

gulp.task('dev',
  gulp.parallel(
    gulp.series('site:compile', 'browser-sync:server'),
    'test:all',
    'watch:articles',
    'watch:test'
  )
)

gulp.task('default', gulp.series('dev'))

//
//
// Continuous integration tasks
//
//

gulp.task('ci:test', gulp.series(
  'test:unit',
  'site:compile'
))

gulp.task('ci:deploy', gulp.series(
  'test:coverage',
  'site:compile',
  'site:deploy',
  'test:coveralls'
))
<|endoftext|>


<commit_before>import React, {Component} from 'react'
import PropTypes from 'prop-types'
import _ from 'lodash'
import classnames from 'classnames'

import {MultiGrid, ColumnSizer} from 'react-virtualized'
import moment from 'moment'

import {timeSeriesToTableGraph} from 'src/utils/timeSeriesToDygraph'
import {
  NULL_ARRAY_INDEX,
  NULL_HOVER_TIME,
  TIME_FORMAT_DEFAULT,
  TIME_FIELD_DEFAULT,
  ASCENDING,
  DESCENDING,
  FIX_FIRST_COLUMN_DEFAULT,
  VERTICAL_TIME_AXIS_DEFAULT,
  calculateTimeColumnWidth,
} from 'src/shared/constants/tableGraph'
const DEFAULT_SORT = ASCENDING

import {generateThresholdsListHexs} from 'shared/constants/colorOperations'

const filterInvisibleColumns = (data, fieldNames) => {
  const visibility = {}
  const filteredData = data.map((row, i) => {
    return row.filter((col, j) => {
      if (i === 0) {
        const foundField = fieldNames.find(field => field.internalName === col)
        visibility[j] = foundField ? foundField.visible : true
      }
      return visibility[j]
    })
  })
  return filteredData[0].length ? filteredData : [[]]
}

const processData = (
  data,
  sortFieldName,
  direction,
  verticalTimeAxis,
  fieldNames
) => {
  const sortIndex = _.indexOf(data[0], sortFieldName)
  const sortedData = [
    data[0],
    ..._.orderBy(_.drop(data, 1), sortIndex, [direction]),
  ]
  const filteredData = filterInvisibleColumns(sortedData, fieldNames)
  const processedData = verticalTimeAxis ? filteredData : _.unzip(filteredData)

  return {processedData}
}

class TableGraph extends Component {
  constructor(props) {
    super(props)
    this.state = {
      data: [[]],
      timeColumnWidth: calculateTimeColumnWidth(props.tableOptions.timeFormat),
      processedData: [[]],
      hoveredColumnIndex: NULL_ARRAY_INDEX,
      hoveredRowIndex: NULL_ARRAY_INDEX,
      sortField: '',
      sortDirection: DEFAULT_SORT,
    }
  }

  componentWillReceiveProps(nextProps) {
    const {labels, data} = timeSeriesToTableGraph(nextProps.data)
    if (_.isEmpty(data[0])) {
      return
    }

    const {sortField, sortDirection} = this.state
    const {
      tableOptions: {
        sortBy: {internalName},
        fieldNames,
        verticalTimeAxis,
        timeFormat,
      },
      setDataLabels,
    } = nextProps

    if (timeFormat !== this.props.tableOptions.timeFormat) {
      this.setState({
        timeColumnWidth: calculateTimeColumnWidth(timeFormat),
      })
    }

    if (setDataLabels) {
      setDataLabels(labels)
    }

    let direction, sortFieldName
    if (
      _.isEmpty(sortField) ||
      _.get(this.props, ['tableOptions', 'sortBy', 'internalName'], '') !==
        _.get(nextProps, ['tableOptions', 'sortBy', 'internalName'], '')
    ) {
      direction = DEFAULT_SORT
      sortFieldName = internalName
    } else {
      direction = sortDirection
      sortFieldName = sortField
    }

    const {processedData} = processData(
      data,
      sortFieldName,
      direction,
      verticalTimeAxis,
      fieldNames
    )

    this.setState({
      data,
      processedData,
      sortField: sortFieldName,
      sortDirection: direction,
    })
  }

  calcHoverTimeIndex = (data, hoverTime, verticalTimeAxis) => {
    if (_.isEmpty(data) || hoverTime === NULL_HOVER_TIME) {
      return undefined
    }
    if (verticalTimeAxis) {
      return data.findIndex(
        row => row[0] && _.isNumber(row[0]) && row[0] >= hoverTime
      )
    }
    return data[0].findIndex(d => _.isNumber(d) && d >= hoverTime)
  }

  handleHover = (columnIndex, rowIndex) => () => {
    const {onSetHoverTime, tableOptions: {verticalTimeAxis}} = this.props
    const {data} = this.state
    if (rowIndex === 0 && verticalTimeAxis) {
      return
    }
    if (onSetHoverTime) {
      const hoverTime = verticalTimeAxis
        ? data[rowIndex][0]
        : data[columnIndex][0]
      onSetHoverTime(hoverTime.toString())
    }
    this.setState({
      hoveredColumnIndex: columnIndex,
      hoveredRowIndex: rowIndex,
    })
  }

  handleMouseOut = () => {
    if (this.props.onSetHoverTime) {
      this.props.onSetHoverTime(NULL_HOVER_TIME)
      this.setState({
        hoveredColumnIndex: NULL_ARRAY_INDEX,
        hoveredRowIndex: NULL_ARRAY_INDEX,
      })
    }
  }

  handleClickFieldName = fieldName => () => {
    const {tableOptions} = this.props
    const {data, sortField, sortDirection} = this.state
    const verticalTimeAxis = _.get(tableOptions, 'verticalTimeAxis', true)
    const fieldNames = _.get(tableOptions, 'fieldNames', [TIME_FIELD_DEFAULT])

    let direction
    if (fieldName === sortField) {
      direction = sortDirection === ASCENDING ? DESCENDING : ASCENDING
    } else {
      direction = DEFAULT_SORT
    }

    const {processedData} = processData(
      data,
      fieldName,
      direction,
      verticalTimeAxis,
      fieldNames
    )

    this.setState({
      processedData,
      sortField: fieldName,
      sortDirection: direction,
    })
  }

  calculateColumnWidth = columnSizerWidth => column => {
    const {index} = column
    const {tableOptions, tableOptions: {verticalTimeAxis}} = this.props
    const {timeColumnWidth} = this.state

    const columnNames = _.get(tableOptions, 'columnNames', [TIME_FIELD_DEFAULT])

    return verticalTimeAxis && columnNames[index].internalName === 'time'
      ? timeColumnWidth
      : columnSizerWidth
  }

  cellRenderer = ({columnIndex, rowIndex, key, parent, style}) => {
    const {hoveredColumnIndex, hoveredRowIndex, processedData} = this.state
    const {tableOptions, colors} = this.props

    const {
      timeFormat = TIME_FORMAT_DEFAULT,
      verticalTimeAxis = VERTICAL_TIME_AXIS_DEFAULT,
      fixFirstColumn = FIX_FIRST_COLUMN_DEFAULT,
      fieldNames = [TIME_FIELD_DEFAULT],
    } = tableOptions

    const cellData = processedData[rowIndex][columnIndex]

    const timeField = fieldNames.find(
      field => field.internalName === TIME_FIELD_DEFAULT.internalName
    )
    const visibleTime = _.get(timeField, 'visible', true)

    const isFixedRow = rowIndex === 0 && columnIndex > 0
    const isFixedColumn = fixFirstColumn && rowIndex > 0 && columnIndex === 0
    const isTimeData =
      visibleTime &&
      (verticalTimeAxis ? rowIndex > 0 && columnIndex === 0 : isFixedRow)
    const isFieldName = verticalTimeAxis ? rowIndex === 0 : columnIndex === 0
    const isFixedCorner = rowIndex === 0 && columnIndex === 0
    const dataIsNumerical = _.isNumber(cellData)
    const isHighlightedRow =
      rowIndex === parent.props.scrollToRow ||
      (rowIndex === hoveredRowIndex && hoveredRowIndex !== 0)
    const isHighlightedColumn =
      columnIndex === parent.props.scrollToColumn ||
      (columnIndex === hoveredColumnIndex && hoveredColumnIndex !== 0)

    let cellStyle = style

    if (!isFixedRow && !isFixedColumn && !isFixedCorner) {
      const {bgColor, textColor} = generateThresholdsListHexs({
        colors,
        lastValue: cellData,
        cellType: 'table',
      })

      cellStyle = {
        ...style,
        backgroundColor: bgColor,
        color: textColor,
      }
    }

    const cellClass = classnames('table-graph-cell', {
      'table-graph-cell__fixed-row': isFixedRow,
      'table-graph-cell__fixed-column': isFixedColumn,
      'table-graph-cell__fixed-corner': isFixedCorner,
      'table-graph-cell__highlight-row': isHighlightedRow,
      'table-graph-cell__highlight-column': isHighlightedColumn,
      'table-graph-cell__numerical': dataIsNumerical,
      'table-graph-cell__isFieldName': isFieldName,
    })

    const foundField =
      isFieldName && fieldNames.find(field => field.internalName === cellData)
    const fieldName =
      foundField && (foundField.displayName || foundField.internalName)

    const cellContents = isTimeData
      ? `${moment(cellData).format(timeFormat)}`
      : fieldName || `${cellData}`

    return (
      <div
        key={key}
        style={cellStyle}
        className={cellClass}
        onClick={isFieldName ? this.handleClickFieldName(cellData) : null}
        onMouseOver={this.handleHover(columnIndex, rowIndex)}
        title={cellContents}
      >
        {cellContents}
      </div>
    )
  }

  render() {
    const {
      hoveredColumnIndex,
      hoveredRowIndex,
      sortField,
      sortDirection,
      processedData,
      data,
    } = this.state
    const {hoverTime, tableOptions, colors} = this.props
    const {
      verticalTimeAxis = VERTICAL_TIME_AXIS_DEFAULT,
      fixFirstColumn = FIX_FIRST_COLUMN_DEFAULT,
    } = tableOptions

    const columnCount = _.get(processedData, ['0', 'length'], 0)
    const rowCount = columnCount === 0 ? 0 : processedData.length

    const COLUMN_MIN_WIDTH = 98
    const COLUMN_MAX_WIDTH = 1000
    const ROW_HEIGHT = 30

    const fixedColumnCount = fixFirstColumn && columnCount > 1 ? 1 : undefined

    const tableWidth = _.get(this, ['gridContainer', 'clientWidth'], 0)
    const tableHeight = _.get(this, ['gridContainer', 'clientHeight'], 0)

    const hoverTimeIndex =
      hoveredRowIndex === NULL_ARRAY_INDEX
        ? this.calcHoverTimeIndex(data, hoverTime, verticalTimeAxis)
        : hoveredRowIndex
    const hoveringThisTable = hoveredColumnIndex !== NULL_ARRAY_INDEX

    const scrollToColumn =
      !hoveringThisTable && !verticalTimeAxis ? hoverTimeIndex : undefined
    const scrollToRow =
      !hoveringThisTable && verticalTimeAxis ? hoverTimeIndex : undefined

    return (
      <div
        className="table-graph-container"
        ref={gridContainer => (this.gridContainer = gridContainer)}
        onMouseOut={this.handleMouseOut}
      >
        {rowCount > 0 &&
          <ColumnSizer
            columnCount={columnCount}
            columnMaxWidth={COLUMN_MAX_WIDTH}
            columnMinWidth={COLUMN_MIN_WIDTH}
            width={tableWidth}
          >
            {({getColumnWidth, registerChild}) =>
              <MultiGrid
                ref={registerChild}
                columnCount={columnCount}
                columnWidth={this.calculateColumnWidth(getColumnWidth())}
                rowCount={rowCount}
                rowHeight={ROW_HEIGHT}
                height={tableHeight}
                width={tableWidth}
                fixedColumnCount={fixedColumnCount}
                fixedRowCount={1}
                enableFixedColumnScroll={true}
                enableFixedRowScroll={true}
                scrollToRow={scrollToRow}
                scrollToColumn={scrollToColumn}
                sortField={sortField}
                sortDirection={sortDirection}
                cellRenderer={this.cellRenderer}
                hoveredColumnIndex={hoveredColumnIndex}
                hoveredRowIndex={hoveredRowIndex}
                hoverTime={hoverTime}
                colors={colors}
                tableOptions={tableOptions}
                classNameBottomRightGrid="table-graph--scroll-window"
              />}
          </ColumnSizer>}
      </div>
    )
  }
}

const {arrayOf, bool, number, shape, string, func} = PropTypes

TableGraph.propTypes = {
  cellHeight: number,
  data: arrayOf(shape()),
  tableOptions: shape({
    timeFormat: string.isRequired,
    verticalTimeAxis: bool.isRequired,
    sortBy: shape({
      internalName: string.isRequired,
      displayName: string.isRequired,
      visible: bool.isRequired,
    }).isRequired,
    wrapping: string.isRequired,
    fieldNames: arrayOf(
      shape({
        internalName: string.isRequired,
        displayName: string.isRequired,
        visible: bool.isRequired,
      })
    ).isRequired,
    fixFirstColumn: bool,
  }),
  hoverTime: string,
  onSetHoverTime: func,
  colors: arrayOf(
    shape({
      type: string.isRequired,
      hex: string.isRequired,
      id: string.isRequired,
      name: string.isRequired,
      value: string.isRequired,
    }).isRequired
  ),
  setDataLabels: func,
}

export default TableGraph
<commit_msg>Fix linter error<commit_after>import React, {Component} from 'react'
import PropTypes from 'prop-types'
import _ from 'lodash'
import classnames from 'classnames'

import {MultiGrid, ColumnSizer} from 'react-virtualized'
import moment from 'moment'

import {timeSeriesToTableGraph} from 'src/utils/timeSeriesToDygraph'
import {
  NULL_ARRAY_INDEX,
  NULL_HOVER_TIME,
  TIME_FORMAT_DEFAULT,
  TIME_FIELD_DEFAULT,
  ASCENDING,
  DESCENDING,
  FIX_FIRST_COLUMN_DEFAULT,
  VERTICAL_TIME_AXIS_DEFAULT,
  calculateTimeColumnWidth,
} from 'src/shared/constants/tableGraph'
const DEFAULT_SORT = ASCENDING

import {generateThresholdsListHexs} from 'shared/constants/colorOperations'

const filterInvisibleColumns = (data, fieldNames) => {
  const visibility = {}
  const filteredData = data.map((row, i) => {
    return row.filter((col, j) => {
      if (i === 0) {
        const foundField = fieldNames.find(field => field.internalName === col)
        visibility[j] = foundField ? foundField.visible : true
      }
      return visibility[j]
    })
  })
  return filteredData[0].length ? filteredData : [[]]
}

const processData = (
  data,
  sortFieldName,
  direction,
  verticalTimeAxis,
  fieldNames
) => {
  const sortIndex = _.indexOf(data[0], sortFieldName)
  const sortedData = [
    data[0],
    ..._.orderBy(_.drop(data, 1), sortIndex, [direction]),
  ]
  const filteredData = filterInvisibleColumns(sortedData, fieldNames)
  const processedData = verticalTimeAxis ? filteredData : _.unzip(filteredData)

  return {processedData}
}

class TableGraph extends Component {
  constructor(props) {
    super(props)
    this.state = {
      data: [[]],
      timeColumnWidth: calculateTimeColumnWidth(props.tableOptions.timeFormat),
      processedData: [[]],
      hoveredColumnIndex: NULL_ARRAY_INDEX,
      hoveredRowIndex: NULL_ARRAY_INDEX,
      sortField: '',
      sortDirection: DEFAULT_SORT,
    }
  }

  componentWillReceiveProps(nextProps) {
    const {labels, data} = timeSeriesToTableGraph(nextProps.data)
    if (_.isEmpty(data[0])) {
      return
    }

    const {sortField, sortDirection} = this.state
    const {
      tableOptions: {
        sortBy: {internalName},
        fieldNames,
        verticalTimeAxis,
        timeFormat,
      },
      setDataLabels,
    } = nextProps

    if (timeFormat !== this.props.tableOptions.timeFormat) {
      this.setState({
        timeColumnWidth: calculateTimeColumnWidth(timeFormat),
      })
    }

    if (setDataLabels) {
      setDataLabels(labels)
    }

    let direction, sortFieldName
    if (
      _.isEmpty(sortField) ||
      _.get(this.props, ['tableOptions', 'sortBy', 'internalName'], '') !==
        _.get(nextProps, ['tableOptions', 'sortBy', 'internalName'], '')
    ) {
      direction = DEFAULT_SORT
      sortFieldName = internalName
    } else {
      direction = sortDirection
      sortFieldName = sortField
    }

    const {processedData} = processData(
      data,
      sortFieldName,
      direction,
      verticalTimeAxis,
      fieldNames
    )

    this.setState({
      data,
      processedData,
      sortField: sortFieldName,
      sortDirection: direction,
    })
  }

  calcHoverTimeIndex = (data, hoverTime, verticalTimeAxis) => {
    if (_.isEmpty(data) || hoverTime === NULL_HOVER_TIME) {
      return undefined
    }
    if (verticalTimeAxis) {
      return data.findIndex(
        row => row[0] && _.isNumber(row[0]) && row[0] >= hoverTime
      )
    }
    return data[0].findIndex(d => _.isNumber(d) && d >= hoverTime)
  }

  handleHover = (columnIndex, rowIndex) => () => {
    const {onSetHoverTime, tableOptions: {verticalTimeAxis}} = this.props
    const {data} = this.state
    if (rowIndex === 0 && verticalTimeAxis) {
      return
    }
    if (onSetHoverTime) {
      const hoverTime = verticalTimeAxis
        ? data[rowIndex][0]
        : data[columnIndex][0]
      onSetHoverTime(hoverTime.toString())
    }
    this.setState({
      hoveredColumnIndex: columnIndex,
      hoveredRowIndex: rowIndex,
    })
  }

  handleMouseOut = () => {
    if (this.props.onSetHoverTime) {
      this.props.onSetHoverTime(NULL_HOVER_TIME)
      this.setState({
        hoveredColumnIndex: NULL_ARRAY_INDEX,
        hoveredRowIndex: NULL_ARRAY_INDEX,
      })
    }
  }

  handleClickFieldName = fieldName => () => {
    const {tableOptions} = this.props
    const {data, sortField, sortDirection} = this.state
    const verticalTimeAxis = _.get(tableOptions, 'verticalTimeAxis', true)
    const fieldNames = _.get(tableOptions, 'fieldNames', [TIME_FIELD_DEFAULT])

    let direction
    if (fieldName === sortField) {
      direction = sortDirection === ASCENDING ? DESCENDING : ASCENDING
    } else {
      direction = DEFAULT_SORT
    }

    const {processedData} = processData(
      data,
      fieldName,
      direction,
      verticalTimeAxis,
      fieldNames
    )

    this.setState({
      processedData,
      sortField: fieldName,
      sortDirection: direction,
    })
  }

  calculateColumnWidth = columnSizerWidth => column => {
    const {index} = column
    const {tableOptions, tableOptions: {verticalTimeAxis}} = this.props
    const {timeColumnWidth} = this.state

    const columnNames = _.get(tableOptions, 'columnNames', [TIME_FIELD_DEFAULT])

    return verticalTimeAxis && columnNames[index].internalName === 'time'
      ? timeColumnWidth
      : columnSizerWidth
  }

  cellRenderer = ({columnIndex, rowIndex, key, parent, style}) => {
    const {hoveredColumnIndex, hoveredRowIndex, processedData} = this.state
    const {tableOptions, colors} = this.props

    const {
      timeFormat = TIME_FORMAT_DEFAULT,
      verticalTimeAxis = VERTICAL_TIME_AXIS_DEFAULT,
      fixFirstColumn = FIX_FIRST_COLUMN_DEFAULT,
      fieldNames = [TIME_FIELD_DEFAULT],
    } = tableOptions

    const cellData = processedData[rowIndex][columnIndex]

    const timeField = fieldNames.find(
      field => field.internalName === TIME_FIELD_DEFAULT.internalName
    )
    const visibleTime = _.get(timeField, 'visible', true)

    const isFixedRow = rowIndex === 0 && columnIndex > 0
    const isFixedColumn = fixFirstColumn && rowIndex > 0 && columnIndex === 0
    const isTimeData =
      visibleTime &&
      (verticalTimeAxis ? rowIndex > 0 && columnIndex === 0 : isFixedRow)
    const isFieldName = verticalTimeAxis ? rowIndex === 0 : columnIndex === 0
    const isFixedCorner = rowIndex === 0 && columnIndex === 0
    const dataIsNumerical = _.isNumber(cellData)
    const isHighlightedRow =
      rowIndex === parent.props.scrollToRow ||
      (rowIndex === hoveredRowIndex && hoveredRowIndex !== 0)
    const isHighlightedColumn =
      columnIndex === parent.props.scrollToColumn ||
      (columnIndex === hoveredColumnIndex && hoveredColumnIndex !== 0)

    let cellStyle = style

    if (!isFixedRow && !isFixedColumn && !isFixedCorner) {
      const {bgColor, textColor} = generateThresholdsListHexs({
        colors,
        lastValue: cellData,
        cellType: 'table',
      })

      cellStyle = {
        ...style,
        backgroundColor: bgColor,
        color: textColor,
      }
    }

    const cellClass = classnames('table-graph-cell', {
      'table-graph-cell__fixed-row': isFixedRow,
      'table-graph-cell__fixed-column': isFixedColumn,
      'table-graph-cell__fixed-corner': isFixedCorner,
      'table-graph-cell__highlight-row': isHighlightedRow,
      'table-graph-cell__highlight-column': isHighlightedColumn,
      'table-graph-cell__numerical': dataIsNumerical,
      'table-graph-cell__isFieldName': isFieldName,
    })

    const foundField =
      isFieldName && fieldNames.find(field => field.internalName === cellData)
    const fieldName =
      foundField && (foundField.displayName || foundField.internalName)

    const cellContents = isTimeData
      ? `${moment(cellData).format(timeFormat)}`
      : fieldName || `${cellData}`

    return (
      <div
        key={key}
        style={cellStyle}
        className={cellClass}
        onClick={isFieldName ? this.handleClickFieldName(cellData) : null}
        onMouseOver={this.handleHover(columnIndex, rowIndex)}
        title={cellContents}
      >
        {cellContents}
      </div>
    )
  }

  render() {
    const {
      hoveredColumnIndex,
      hoveredRowIndex,
      sortField,
      sortDirection,
      processedData,
      data,
    } = this.state
    const {hoverTime, tableOptions, colors} = this.props
    const {
      verticalTimeAxis = VERTICAL_TIME_AXIS_DEFAULT,
      fixFirstColumn = FIX_FIRST_COLUMN_DEFAULT,
    } = tableOptions

    const columnCount = _.get(processedData, ['0', 'length'], 0)
    const rowCount = columnCount === 0 ? 0 : processedData.length

    const COLUMN_MIN_WIDTH = 98
    const COLUMN_MAX_WIDTH = 1000
    const ROW_HEIGHT = 30

    const fixedColumnCount = fixFirstColumn && columnCount > 1 ? 1 : undefined

    const tableWidth = _.get(this, ['gridContainer', 'clientWidth'], 0)
    const tableHeight = _.get(this, ['gridContainer', 'clientHeight'], 0)

    const hoverTimeIndex =
      hoveredRowIndex === NULL_ARRAY_INDEX
        ? this.calcHoverTimeIndex(data, hoverTime, verticalTimeAxis)
        : hoveredRowIndex
    const hoveringThisTable = hoveredColumnIndex !== NULL_ARRAY_INDEX

    const scrollToColumn =
      !hoveringThisTable && !verticalTimeAxis ? hoverTimeIndex : undefined
    const scrollToRow =
      !hoveringThisTable && verticalTimeAxis ? hoverTimeIndex : undefined

    return (
      <div
        className="table-graph-container"
        ref={gridContainer => (this.gridContainer = gridContainer)}
        onMouseOut={this.handleMouseOut}
      >
        {rowCount > 0 && (
          <ColumnSizer
            columnCount={columnCount}
            columnMaxWidth={COLUMN_MAX_WIDTH}
            columnMinWidth={COLUMN_MIN_WIDTH}
            width={tableWidth}
          >
            {({getColumnWidth, registerChild}) => (
              <MultiGrid
                ref={registerChild}
                columnCount={columnCount}
                columnWidth={this.calculateColumnWidth(getColumnWidth())}
                rowCount={rowCount}
                rowHeight={ROW_HEIGHT}
                height={tableHeight}
                width={tableWidth}
                fixedColumnCount={fixedColumnCount}
                fixedRowCount={1}
                enableFixedColumnScroll={true}
                enableFixedRowScroll={true}
                scrollToRow={scrollToRow}
                scrollToColumn={scrollToColumn}
                sortField={sortField}
                sortDirection={sortDirection}
                cellRenderer={this.cellRenderer}
                hoveredColumnIndex={hoveredColumnIndex}
                hoveredRowIndex={hoveredRowIndex}
                hoverTime={hoverTime}
                colors={colors}
                tableOptions={tableOptions}
                classNameBottomRightGrid="table-graph--scroll-window"
              />
            )}
          </ColumnSizer>
        )}
      </div>
    )
  }
}

const {arrayOf, bool, number, shape, string, func} = PropTypes

TableGraph.propTypes = {
  cellHeight: number,
  data: arrayOf(shape()),
  tableOptions: shape({
    timeFormat: string.isRequired,
    verticalTimeAxis: bool.isRequired,
    sortBy: shape({
      internalName: string.isRequired,
      displayName: string.isRequired,
      visible: bool.isRequired,
    }).isRequired,
    wrapping: string.isRequired,
    fieldNames: arrayOf(
      shape({
        internalName: string.isRequired,
        displayName: string.isRequired,
        visible: bool.isRequired,
      })
    ).isRequired,
    fixFirstColumn: bool,
  }),
  hoverTime: string,
  onSetHoverTime: func,
  colors: arrayOf(
    shape({
      type: string.isRequired,
      hex: string.isRequired,
      id: string.isRequired,
      name: string.isRequired,
      value: string.isRequired,
    }).isRequired
  ),
  setDataLabels: func,
}

export default TableGraph
<|endoftext|>


<commit_before>'use strict';

var $ = require('jquery'),
    StickyScrollBarComponent = require('./StickyScrollBarComponent');

require('datatables.net')(window, $);
require('datatables.net-buttons')(window, $);
require('datatables.net-responsive')(window, $);
require('datatables.net-select')(window, $);
require('../libs/jquery.countdown/dist/jquery.countdown.min');

function PulsarUIComponent(html, history) {
    this.history = history;
    this.$html = html;
    this.$window = $(window);
    this.stickyScrollBarComponent = new StickyScrollBarComponent(this.$window, this.$html);
}

PulsarUIComponent.prototype.init = function () {
    var component = this;

    // Stop disabled links from being interactive
    this.$html.on('click', 'a[disabled]', function(e) {
        e.preventDefault();
    });

    // Watch for push-state requests via data-html attribute
    this.$html.on('click', '[data-href]', function(e) {
        var href = $(this).data('href');
        component.history.pushState({state:1}, href, href);
    });

    this.initTables();
    this.initDataTables();
    this.initCountdown();
};

PulsarUIComponent.prototype.initTables = function () {

    // Wrap non datatable tables in responsive container so they can scroll
    // when required
    var $tables = this.$html.find('.table:not(.datatable), .table--datagrid:not(.datatable)');

    $tables.each(function(i, v) {
        var $table = $(v);

        if (!$table.parent().hasClass('table-container')) {
            $table.wrap('

');
        }
    });
};

PulsarUIComponent.prototype.initDataTables = function () {
    var component = this,
        datatables = component.$html.find('.datatable:not(.table--horizontal)'),
        datatablesHorizontal = component.$html.find('.datatable.table--horizontal');

    datatables.each(function () {
        var $this = $(this);

        var select = {
            className: 'dt-row-selected',
            style:     'multi',
            selector:  'td.table-selection'
        }

        var dom = '<"dataTables_top"Birf><"dataTables_actions"T>t<"dataTables_bottom"lip>';

        if (!$this.data('empty-table')) {
            $this.data('empty-table', 'There are currently no items to display');
        }

        if ($this.data('select') === false) {
            dom = '<"dataTables_top"irf><"dataTables_actions"T><"dt-disable-selection"t><"dataTables_bottom"p>';
            select = false;
        }

        let table = $this.DataTable({
            dom: dom,
            aaSorting: [],
            bAutoWidth: false,
            pageLength: 25,
            lengthChange: false,
            buttons: [],
            columnDefs: [
                { className: 'control', orderable: false, targets: 0 },
                { "searchable": false, "targets": [0] },
                { "orderable": false, "targets": [0, 1] }
            ],
            language: {
                "emptyTable": $this.data('empty-table'),
                "info": "Showing _START_ to _END_ of _TOTAL_ items",
                "infoEmpty": 'No items',
                "infoFiltered": " (filtered from _MAX_ items)",
                "zeroRecords": "No items matched your filter, please clear it and try again",
                "search": "Filter records"
            },
            responsive: {
                details: {
                    type: 'column'
                }
            },
            select: select,
            stateSave: false
        });

        $this.on('click', '.js-select-all', function(e) {
            var $checkbox = $(e.target),
                $allCheckboxes = $this.find('.js-select');

            if ($checkbox.hasClass('selected')) {
                table.rows().deselect();
                $checkbox.removeClass('selected');
                $allCheckboxes.removeClass('selected').prop('checked', false);
            } else {
                table.rows().select();
                $checkbox.addClass('selected');
                $allCheckboxes.addClass('selected').prop('checked', true);
            }
        });
    });

    datatablesHorizontal.each(function () {
        var $this = $(this);

        var select = {
            className: 'dt-row-selected',
            style:     'multi',
            selector:  '.js-select',
            info:       true
        }

        var dom = '<"dataTables_top"Birf><"dataTables_actions"T><"table-container"t><"dataTables_bottom"p>';

        if (!$this.data('empty-table')) {
            $this.data('empty-table', 'There are currently no items to display');
        }

        if ($this.data('select') === false) {
            dom = '<"dataTables_top"irf><"dataTables_actions"T><"dt-disable-selection"<"table-container"t>><"dataTables_bottom"lp>';
            select = false;
        }

        let table = $this.DataTable({
            dom: dom,
            aaSorting: [],
            bAutoWidth: false,
            stateSave: true,
            pageLength: 25,
            lengthChange: false,
            buttons: [],
            columnDefs: [
                { className: 'control', orderable: false, targets: 0 },
            ],
            language: {
                "emptyTable": $this.data('empty-table'),
                "info": "Showing _START_ to _END_ of _TOTAL_ items",
                "infoEmpty": 'No items',
                "infoFiltered": " (filtered from _MAX_ items)",
                "zeroRecords": "No items matched your filter, please clear it and try again",
                "search": "Quick filter"
            },
            select: select,
            stateSave: false
        });

        // Add sticky scroll bar
        component.stickyScrollBarComponent.init($this.parent());

        // Init bulk actions menu
        component.toggleBulkActions($this);

        // Rerun bulk actions on row select
        table.on('select deselect', function (e, dt, type, indexes) {
            if (type === 'row') {
                component.toggleBulkActions($this);
            }
        });

        $this.on('click', '.js-select-all', function(e) {
            var $checkbox = $(e.target),
                $allCheckboxes = $this.find('.js-select');

            if ($checkbox.hasClass('selected')) {
                table.rows({ search: 'applied' }).deselect();

                $checkbox.removeClass('selected');
                $allCheckboxes.removeClass('selected').prop('checked', false);
            } else {
                table.rows({ search: 'applied' }).select();
                $checkbox.addClass('selected');
                $allCheckboxes.addClass('selected').prop('checked', true);
            }
        });
    });

    // Refresh datatables when tabs are switched, this fixes some layout issues
    this.$html.find('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
        $($.fn.dataTable.tables(true)).DataTable().columns.adjust().responsive.recalc();
    });

    this.$html.find('.table--horizontal').each(function () {
        var $table = $(this).parent();

        $table.scroll(function () {
            component.styleTableOverflows($table);
        });

        $(window).on('load resize', function () {
            component.styleTableOverflows($table);
        });

        // Add sticky scroll bar
        component.stickyScrollBarComponent.init($table);
    });
};

PulsarUIComponent.prototype.styleTableOverflows = function ($container) {
    var $table = $container.find('.table'),
        tableFullWidth = $table[0].scrollWidth,
        tableVisibleWidth = $container.width();

    // Toggle right hand shadow, if overflowing to the right
    if (tableFullWidth === tableVisibleWidth) {
        $container
            .removeClass('table--overflow-right');
    }
    else {
        $container.addClass('table--overflow-right');
    }

    // Toggle left hand shadow, if overflowing to the left
    if (($table.offsetParent().offset().left - $table.offset().left) > 0) {
        $container.addClass('table--overflow-left');
    }
    else {
        $container.removeClass('table--overflow-left');
    }

    // Remove right hand shadow if table scrolled to right hand edge
    if (-Math.abs((tableFullWidth - tableVisibleWidth - $table.offsetParent().offset().left)) >= $table.offset().left) {
        $container.removeClass('table--overflow-right');
    }
};

PulsarUIComponent.prototype.initCountdown = function () {

    // Initial basic implementation of https://github.com/hilios/jQuery.countdown
    this.$html.find('.js-countdown').each(function () {

        var $this = $(this),
            format = '%ww %dd %Hh %Mm %S';

        if (typeof $this.data('format') !== 'undefined') {
            format = $this.data('format');
        }

        $this.countdown($this.data('final-date'), function(event) {
            $this.html(event.strftime(format));
        });
    });
};

PulsarUIComponent.prototype.toggleBulkActions = function(table) {
    var table = table.DataTable(),
        count = table.rows({ selected: true }).count(),
        $bulkActionsButton = this.$html.find('.bulk-actions'),
        $bulkActionsBadge = $bulkActionsButton.find('.js-bulk-actions-badge'),
        $bulkActions = this.$html.find('[data-bulk-action]');

    if (count === 0) {
        $bulkActionsBadge.hide();

        $bulkActions
            .addClass('disabled')
            .prop('aria-disabled', 'true')
            .prop('aria-label', 'no rows selected')
            .parent().attr({
                'data-toggle': 'tooltips',
                'data-placement': 'right',
                'data-container': 'body',
                'title': 'Select one or more items to perform this bulk action'
            }).tooltips();
    }
    else {
        $bulkActionsBadge
            .attr('aria-label', count + ' row' + ((count > 1) ? 's' : '') + ' selected')
            .text(count)
            .show();

        $bulkActions
            .removeClass('disabled')
            .prop('aria-disabled', 'false')
            .parent().tooltips('destroy');
    }
};

module.exports = PulsarUIComponent;
<commit_msg>Stop datatables displaying the ‘Showing X to Y of Z items` next to the pagination<commit_after>'use strict';

var $ = require('jquery'),
    StickyScrollBarComponent = require('./StickyScrollBarComponent');

require('datatables.net')(window, $);
require('datatables.net-buttons')(window, $);
require('datatables.net-responsive')(window, $);
require('datatables.net-select')(window, $);
require('../libs/jquery.countdown/dist/jquery.countdown.min');

function PulsarUIComponent(html, history) {
    this.history = history;
    this.$html = html;
    this.$window = $(window);
    this.stickyScrollBarComponent = new StickyScrollBarComponent(this.$window, this.$html);
}

PulsarUIComponent.prototype.init = function () {
    var component = this;

    // Stop disabled links from being interactive
    this.$html.on('click', 'a[disabled]', function(e) {
        e.preventDefault();
    });

    // Watch for push-state requests via data-html attribute
    this.$html.on('click', '[data-href]', function(e) {
        var href = $(this).data('href');
        component.history.pushState({state:1}, href, href);
    });

    this.initTables();
    this.initDataTables();
    this.initCountdown();
};

PulsarUIComponent.prototype.initTables = function () {

    // Wrap non datatable tables in responsive container so they can scroll
    // when required
    var $tables = this.$html.find('.table:not(.datatable), .table--datagrid:not(.datatable)');

    $tables.each(function(i, v) {
        var $table = $(v);

        if (!$table.parent().hasClass('table-container')) {
            $table.wrap('

');
        }
    });
};

PulsarUIComponent.prototype.initDataTables = function () {
    var component = this,
        datatables = component.$html.find('.datatable:not(.table--horizontal)'),
        datatablesHorizontal = component.$html.find('.datatable.table--horizontal');

    datatables.each(function () {
        var $this = $(this);

        var select = {
            className: 'dt-row-selected',
            style:     'multi',
            selector:  'td.table-selection'
        }

        var dom = '<"dataTables_top"Birf><"dataTables_actions"T>t<"dataTables_bottom"lp>';

        if (!$this.data('empty-table')) {
            $this.data('empty-table', 'There are currently no items to display');
        }

        if ($this.data('select') === false) {
            dom = '<"dataTables_top"irf><"dataTables_actions"T><"dt-disable-selection"t><"dataTables_bottom"p>';
            select = false;
        }

        let table = $this.DataTable({
            dom: dom,
            aaSorting: [],
            bAutoWidth: false,
            pageLength: 25,
            lengthChange: false,
            buttons: [],
            columnDefs: [
                { className: 'control', orderable: false, targets: 0 },
                { "searchable": false, "targets": [0] },
                { "orderable": false, "targets": [0, 1] }
            ],
            language: {
                "emptyTable": $this.data('empty-table'),
                "info": "Showing _START_ to _END_ of _TOTAL_ items",
                "infoEmpty": 'No items',
                "infoFiltered": " (filtered from _MAX_ items)",
                "zeroRecords": "No items matched your filter, please clear it and try again",
                "search": "Filter records"
            },
            responsive: {
                details: {
                    type: 'column'
                }
            },
            select: select,
            stateSave: false
        });

        $this.on('click', '.js-select-all', function(e) {
            var $checkbox = $(e.target),
                $allCheckboxes = $this.find('.js-select');

            if ($checkbox.hasClass('selected')) {
                table.rows().deselect();
                $checkbox.removeClass('selected');
                $allCheckboxes.removeClass('selected').prop('checked', false);
            } else {
                table.rows().select();
                $checkbox.addClass('selected');
                $allCheckboxes.addClass('selected').prop('checked', true);
            }
        });
    });

    datatablesHorizontal.each(function () {
        var $this = $(this);

        var select = {
            className: 'dt-row-selected',
            style:     'multi',
            selector:  '.js-select',
            info:       true
        }

        var dom = '<"dataTables_top"Birf><"dataTables_actions"T><"table-container"t><"dataTables_bottom"p>';

        if (!$this.data('empty-table')) {
            $this.data('empty-table', 'There are currently no items to display');
        }

        if ($this.data('select') === false) {
            dom = '<"dataTables_top"irf><"dataTables_actions"T><"dt-disable-selection"<"table-container"t>><"dataTables_bottom"lp>';
            select = false;
        }

        let table = $this.DataTable({
            dom: dom,
            aaSorting: [],
            bAutoWidth: false,
            stateSave: true,
            pageLength: 25,
            lengthChange: false,
            buttons: [],
            columnDefs: [
                { className: 'control', orderable: false, targets: 0 },
            ],
            language: {
                "emptyTable": $this.data('empty-table'),
                "info": "Showing _START_ to _END_ of _TOTAL_ items",
                "infoEmpty": 'No items',
                "infoFiltered": " (filtered from _MAX_ items)",
                "zeroRecords": "No items matched your filter, please clear it and try again",
                "search": "Quick filter"
            },
            select: select,
            stateSave: false
        });

        // Add sticky scroll bar
        component.stickyScrollBarComponent.init($this.parent());

        // Init bulk actions menu
        component.toggleBulkActions($this);

        // Rerun bulk actions on row select
        table.on('select deselect', function (e, dt, type, indexes) {
            if (type === 'row') {
                component.toggleBulkActions($this);
            }
        });

        $this.on('click', '.js-select-all', function(e) {
            var $checkbox = $(e.target),
                $allCheckboxes = $this.find('.js-select');

            if ($checkbox.hasClass('selected')) {
                table.rows({ search: 'applied' }).deselect();

                $checkbox.removeClass('selected');
                $allCheckboxes.removeClass('selected').prop('checked', false);
            } else {
                table.rows({ search: 'applied' }).select();
                $checkbox.addClass('selected');
                $allCheckboxes.addClass('selected').prop('checked', true);
            }
        });
    });

    // Refresh datatables when tabs are switched, this fixes some layout issues
    this.$html.find('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
        $($.fn.dataTable.tables(true)).DataTable().columns.adjust().responsive.recalc();
    });

    this.$html.find('.table--horizontal').each(function () {
        var $table = $(this).parent();

        $table.scroll(function () {
            component.styleTableOverflows($table);
        });

        $(window).on('load resize', function () {
            component.styleTableOverflows($table);
        });

        // Add sticky scroll bar
        component.stickyScrollBarComponent.init($table);
    });
};

PulsarUIComponent.prototype.styleTableOverflows = function ($container) {
    var $table = $container.find('.table'),
        tableFullWidth = $table[0].scrollWidth,
        tableVisibleWidth = $container.width();

    // Toggle right hand shadow, if overflowing to the right
    if (tableFullWidth === tableVisibleWidth) {
        $container
            .removeClass('table--overflow-right');
    }
    else {
        $container.addClass('table--overflow-right');
    }

    // Toggle left hand shadow, if overflowing to the left
    if (($table.offsetParent().offset().left - $table.offset().left) > 0) {
        $container.addClass('table--overflow-left');
    }
    else {
        $container.removeClass('table--overflow-left');
    }

    // Remove right hand shadow if table scrolled to right hand edge
    if (-Math.abs((tableFullWidth - tableVisibleWidth - $table.offsetParent().offset().left)) >= $table.offset().left) {
        $container.removeClass('table--overflow-right');
    }
};

PulsarUIComponent.prototype.initCountdown = function () {

    // Initial basic implementation of https://github.com/hilios/jQuery.countdown
    this.$html.find('.js-countdown').each(function () {

        var $this = $(this),
            format = '%ww %dd %Hh %Mm %S';

        if (typeof $this.data('format') !== 'undefined') {
            format = $this.data('format');
        }

        $this.countdown($this.data('final-date'), function(event) {
            $this.html(event.strftime(format));
        });
    });
};

PulsarUIComponent.prototype.toggleBulkActions = function(table) {
    var table = table.DataTable(),
        count = table.rows({ selected: true }).count(),
        $bulkActionsButton = this.$html.find('.bulk-actions'),
        $bulkActionsBadge = $bulkActionsButton.find('.js-bulk-actions-badge'),
        $bulkActions = this.$html.find('[data-bulk-action]');

    if (count === 0) {
        $bulkActionsBadge.hide();

        $bulkActions
            .addClass('disabled')
            .prop('aria-disabled', 'true')
            .prop('aria-label', 'no rows selected')
            .parent().attr({
                'data-toggle': 'tooltips',
                'data-placement': 'right',
                'data-container': 'body',
                'title': 'Select one or more items to perform this bulk action'
            }).tooltips();
    }
    else {
        $bulkActionsBadge
            .attr('aria-label', count + ' row' + ((count > 1) ? 's' : '') + ' selected')
            .text(count)
            .show();

        $bulkActions
            .removeClass('disabled')
            .prop('aria-disabled', 'false')
            .parent().tooltips('destroy');
    }
};

module.exports = PulsarUIComponent;
<|endoftext|>