<commit_before>// ==UserScript==
// @name         SGTools Giveaways Helper (Custom by Barokai)
// @icon         https://cdn.steamgifts.com/img/favicon.ico
// @namespace    *://www.sgtools.info/
// @version      1.7.4
// @description  Makes your life easier!
// @author       Barokai | www.loigistal.at (Enhanced version of KnSYS which is based on a work from Mole & Archi. See below)
// @description  Enhanced create giveaway feature - added 3 buttons for 3 giveaway groups (BundleQuest, RPGTreasury, Unlucky-7) which will be chosen automatically on click.
// @homepage     https://github.com/Barokai/sgtools-giveaways-helper-custom/
// @license      https://github.com/Barokai/sgtools-giveaways-helper-custom/blob/master/LICENSE
// @updateURL    https://github.com/Barokai/sgtools-giveaways-helper-custom/raw/master/sgtools-giveaways-helper-custom.user.js
// @match        *://www.steamgifts.com/*
// @grant        GM_addStyle
// @run-at       document-end
// ==/UserScript==
/* Based on SGTools Giveaways Helper by KnSYS
 * which is
 * Based on Touhou Giveaways Helper
 * https://github.com/Aareksio/touhou-giveaways-helper
 * authors  Mole & Archi
 *
 * MIT License
 */

/* TODO Barokai:
 * set default duration per group
 * set default description per group (eg. unlucky-7: open for gifters or not, bundlequest: free/discount/premium,..)
 */

(function() {
    'use strict';

    var SGTOOLS_SITE = 'https://www.sgtools.info/';
    var SGTOOLS_TIME = 2 * 24 * 60 * 60 * 1000; // 2 days recommended

    var DEFAULT_LEVEL = 0;
    var DEFAULT_DESCRIPTION = "**Good Luck!**";
    var GROUPS = {
        "none": {
            name : "",
            level: 1,
            description: DEFAULT_DESCRIPTION },
        "boxofkittens": {
            name : "Box of Kittens!",
            level: DEFAULT_LEVEL,
            description: DEFAULT_DESCRIPTION + "\n\nComment with a funny gif ;)"
        },
        "bundlequest":{
            name : "Bundle Quest",
            level: DEFAULT_LEVEL,
            description: DEFAULT_DESCRIPTION + "\n\n*This Giveaway is a normal/free/XX% discount giveaway.*" },
        "rpgtreasury": {
            name :  "RPG Treasury",
            level: DEFAULT_LEVEL,
            description: DEFAULT_DESCRIPTION },
        "unlucky7":  {
            name : "Unlucky-7",
            level: DEFAULT_LEVEL,
            description: DEFAULT_DESCRIPTION + "\n\nOpen for gifters, but please make sure that your [gift difference](http://i.imgur.com/69WNFSw.png) is above 7.0 or higher.\nYou can check yours [here](https://www.steamgifts.com/group/WWF2y/unlucky-7/users)."
        }
    };

    var current_path;
    if (/steamgifts\.com/.exec(window.location.href)) {
        current_path = window.location.pathname.split('/');
        removeFromArray(current_path, "");
    }

    if (current_path) {
        if (current_path.length !== 0 && current_path[0] === 'giveaways' && current_path[1] === 'new') {
            addStyles();
            giveawayNew();
        } else if (current_path.length !== 0 && current_path[0] === 'giveaway') {
            var id = current_path[1];
            console.log(id);
            getProtectedStatus(id);
        }
    }

    function getProtectedStatus(id) {
        var isInviteOnly = $('.featured__column--invite-only').length;
        if (isInviteOnly) {
            var url = SGTOOLS_SITE + 'api/isSGTProtected/' + id;
            $.getJSON(url + "?callback=?", function(result) {
                var isProtected = result["protected"];
                var toAppendValid = $(".sidebar__entry-insert");
                var toAppendError = $(".sidebar__error");
                if (isProtected) {
                    toAppendValid.html(' Enter SGTools Protected Giveaway');
                    toAppendError.html(' ' + toAppendError.html());
                    toAppendValid.css({
                        'border-color': "#93BBD3 #699DBC #427BA4 #70ACC8",
                        'background-image': 'linear-gradient(#A7D1EE 0%, #8AC4DF 50%, #6AA2C9 100%)',
                        'color': 'inherit'
                    });
                } else {
                    toAppendValid.html(' ' + toAppendValid.html());
                }
            });
        }
    }

    function giveawayNew() {
        let getGroupButtons = function(){
            var buttonMarkup = "";
            $.each(GROUPS, function(key, g) {
                // debugger;
                var groupname = g.name !== "" ? " for " + g.name : "";
                var id = "sgToolsBtn_"+key;
                buttonMarkup += '

 Fill default settings' + groupname + '

';
            });
            return buttonMarkup;
        };

        let bindOnClick = function(){
            $.each(GROUPS, function(key, group) {
                var id = "sgToolsBtn_"+key;
                $("#"+id).click(function() {
                    applySettings(group);
                });
            });
        };

        $(".form__rows").prepend(
            '
' +
            '
'+
            '
0.
'+
            '
SGTools Giveaway
'+
            '
' +
            getGroupButtons() +
            '
');
        bindOnClick();

        let applyGiveawayTypeKey = function() {
            var privateButton = $("div[data-checkbox-value='key']");
            if (!privateButton.hasClass('is-selected')) {
                privateButton.trigger("click");
            }
        };

        let applyDates = function() {
            let startingDate = new Date();
            let endingDate = new Date(startingDate.getTime() + SGTOOLS_TIME + (60 * 60 * 1000)); // Extra 1 hour
            $("input[name='start_time']").val(formatDate(startingDate));
            $("input[name='end_time']").val(formatDate(endingDate));
        };

        let applyRegionRestrictions = function() {
            $("div[data-checkbox-value='0']").trigger("click");
        };

        var applyPrivate = function(group) {
            if (group.name) {
                var privateButton = $("div[data-checkbox-value='invite_only']");
                if (!privateButton.hasClass('is-selected')) {
                    privateButton.trigger("click");
                }
            }
        };

        let scrollToGroup = function(group) {
            var container = $('.form__groups'),
                scrollTo = group;

            container.scrollTop(
                scrollTo.offset().top - container.offset().top + container.scrollTop()
            );
        };

        let applyGroup = function(group) {
            if (group.name){
                var privateButton = $("div[data-checkbox-value='groups']");
                if (!privateButton.hasClass('is-selected')) {
                    privateButton.trigger("click");
                }

                var group_form = $('div.form__group__details > div.form__group__name:contains(' + group.name + ')');
                group_form.click();
                scrollToGroup(group_form);
            }
        };

        let applyDescription = function(group) {
            let descarea = $("textarea[name='description']");
            descarea.val(group.description);
        };

        let applyLevel = function(group) {
            let levelValue = $("input[name='contributor_level']");
            levelValue.val(group.level);
            $(".ui-slider-range").css('width', group.level * 10 + "%");
            $(".ui-slider-handle").css('left', group.level * 10 + "%");
            $(".form__input-description--level").toggleClass("is-hidden");
            $(".form__input-description--no-level").toggleClass("is-hidden");
            $("span.form__level").text("level " + group.level);
        };

        let reset = function(){
            $(".form__checkbox.is-disabled").show();
            // TODO: unselect groups, key/gift selection, set level to 0 ?
        };

        let applySettings = function(group) {
            reset();
            applyGiveawayTypeKey();
            applyDates();
            applyRegionRestrictions();
            applyPrivate(group);
            applyDescription(group);
            applyLevel(group);
            applyGroup(group);
            // hides not used checkboxes
            $(".form__checkbox.is-disabled").hide();
            // focus input to enter game name
            $(".js__autocomplete-name").focus();
        };
    }

    /* Helpers */
    function removeFromArray(arr, item) {
        for (let i = arr.length; i--;) {
            if (arr[i] === item) {
                arr.splice(i, 1);
            }
        }
    }

    function formatDate(date) {
        // Fixed by Archi for all SG weird dates, do not touch

        // Fix hours
        let hours = date.getHours();
        let ampm = '';
        if (hours < 12) {
            ampm = 'am';
            if (hours === 0) {
                hours = 12;
            }
        } else {
            ampm = 'pm';
            if (hours !== 12) {
                hours = hours % 12;
            }
        }

        // Fix minutes
        let minutes = date.getMinutes();
        if (minutes < 10) {
            minutes = '0' + minutes;
        }

        // Return result
        return $.datepicker.formatDate('M d, yy', date) + " " + hours + ":" + minutes + " " + ampm;
    }
})();

/*jshint multistr: true */
function addStyles() {
    GM_addStyle("\
input[type=text], textarea{\
-webkit-transition: all 0.30s ease-in-out;\
-moz-transition: all 0.30s ease-in-out;\
-ms-transition: all 0.30s ease-in-out;\
-o-transition: all 0.30s ease-in-out;\
outline: none;\
padding: 3px 0px 3px 3px;\
margin: 5px 1px 3px 0px;\
border: 1px solid #DDDDDD;\
} \
input[type=text]:focus, textarea:focus {\
box-shadow: 0 0 5px rgba(81, 203, 238, 1);\
padding: 3px 0px 3px 3px;\
margin: 5px 1px 3px 0px;\
border: 1px solid rgba(81, 203, 238, 1) !important; \
}");
}
<commit_msg>smaller buttons with new icons, reset functionality<commit_after>// ==UserScript==
// @name         SGTools Giveaways Helper (Custom by Barokai)
// @icon         https://cdn.steamgifts.com/img/favicon.ico
// @namespace    *://www.sgtools.info/
// @version      1.7.5
// @description  Makes your life easier!
// @author       Barokai | www.loigistal.at (Enhanced version of KnSYS which is based on a work from Mole & Archi. See below)
// @description  Enhanced create giveaway feature - added 3 buttons for 3 giveaway groups (BundleQuest, RPGTreasury, Unlucky-7) which will be chosen automatically on click.
// @homepage     https://github.com/Barokai/sgtools-giveaways-helper-custom/
// @license      https://github.com/Barokai/sgtools-giveaways-helper-custom/blob/master/LICENSE
// @updateURL    https://github.com/Barokai/sgtools-giveaways-helper-custom/raw/master/sgtools-giveaways-helper-custom.user.js
// @match        *://www.steamgifts.com/*
// @grant        GM_addStyle
// @run-at       document-end
// ==/UserScript==
/* Based on SGTools Giveaways Helper by KnSYS
 * which is
 * Based on Touhou Giveaways Helper
 * https://github.com/Aareksio/touhou-giveaways-helper
 * authors  Mole & Archi
 *
 * MIT License
 */

/* TODO Barokai:
 * set default duration per group
 * set default description per group (eg. unlucky-7: open for gifters or not, bundlequest: free/discount/premium,..)
 */

(function() {
    'use strict';

    var SGTOOLS_SITE = 'https://www.sgtools.info/';
    var SGTOOLS_TIME = 2 * 24 * 60 * 60 * 1000; // 2 days recommended

    var DEFAULT_LEVEL = 0;
    var DEFAULT_DESCRIPTION = "**Good Luck!**";
    var GROUPS = {
        "none": {
            name: "",
            level: 1,
            description: DEFAULT_DESCRIPTION
        },
        "boxofkittens": {
            name: "Box of Kittens!",
            level: DEFAULT_LEVEL,
            description: DEFAULT_DESCRIPTION + "\n\nComment with a funny gif ;)"
        },
        "bundlequest": {
            name: "Bundle Quest",
            level: DEFAULT_LEVEL,
            description: DEFAULT_DESCRIPTION + "\n\n*This Giveaway is a normal/free/XX% discount giveaway.*"
        },
        "rpgtreasury": {
            name: "RPG Treasury",
            level: DEFAULT_LEVEL,
            description: DEFAULT_DESCRIPTION
        },
        "unlucky7": {
            name: "Unlucky-7",
            level: DEFAULT_LEVEL,
            description: DEFAULT_DESCRIPTION + "\n\nOpen for gifters, but please make sure that your [gift difference](http://i.imgur.com/69WNFSw.png) is above 7.0 or higher.\nYou can check yours [here](https://www.steamgifts.com/group/WWF2y/unlucky-7/users)."
        }
    };

    var current_path;
    if (/steamgifts\.com/.exec(window.location.href)) {
        current_path = window.location.pathname.split('/');
        removeFromArray(current_path, "");
    }

    if (current_path) {
        if (current_path.length !== 0 && current_path[0] === 'giveaways' && current_path[1] === 'new') {
            addStyles();
            giveawayNew();
        } else if (current_path.length !== 0 && current_path[0] === 'giveaway') {
            var id = current_path[1];
            console.log(id);
            getProtectedStatus(id);
        }
    }

    function getProtectedStatus(id) {
        var isInviteOnly = $('.featured__column--invite-only').length;
        if (isInviteOnly) {
            var url = SGTOOLS_SITE + 'api/isSGTProtected/' + id;
            $.getJSON(url + "?callback=?", function(result) {
                var isProtected = result["protected"];
                var toAppendValid = $(".sidebar__entry-insert");
                var toAppendError = $(".sidebar__error");
                if (isProtected) {
                    toAppendValid.html(' Enter SGTools Protected Giveaway');
                    toAppendError.html(' ' + toAppendError.html());
                    toAppendValid.css({
                        'border-color': "#93BBD3 #699DBC #427BA4 #70ACC8",
                        'background-image': 'linear-gradient(#A7D1EE 0%, #8AC4DF 50%, #6AA2C9 100%)',
                        'color': 'inherit'
                    });
                } else {
                    toAppendValid.html(' ' + toAppendValid.html());
                }
            });
        }
    }

    function giveawayNew() {
        let getGroupButtons = function() {
            var buttonMarkup = "";
            $.each(GROUPS, function(key, g) {
                //var groupname = g.name !== "" ? " for " + g.name : "";
                var groupname = g.name || "default";
                var icon = groupname !== "default" ? "group" : "gift";
                var id = "sgToolsBtn_" + key;
                //buttonMarkup += '

 Fill default settings' + groupname + '

';
                buttonMarkup += '
 ' + groupname + '
 ';
            });
            return buttonMarkup;
        };

        let bindOnClick = function() {
            $.each(GROUPS, function(key, group) {
                var id = "sgToolsBtn_" + key;
                $("#" + id).click(function() {
                    applySettings(group);
                    console.log("clicked " + (group.name || "default" ) + " button");
                });
            });
        };

        $(".form__rows").prepend(
            '
' +
            '
' +
            '
0.
' +
            '
SGTools Giveaway
' +
            '
' +
            getGroupButtons() +
            '
');
        bindOnClick();

        let applyGiveawayTypeKey = function() {
            var privateButton = $("div[data-checkbox-value='key']");
            if (!privateButton.hasClass('is-selected')) {
                privateButton.trigger("click");
            }
        };

        let applyDates = function() {
            let startingDate = new Date();
            let endingDate = new Date(startingDate.getTime() + SGTOOLS_TIME + (60 * 60 * 1000)); // Extra 1 hour
            $("input[name='start_time']").val(formatDate(startingDate));
            $("input[name='end_time']").val(formatDate(endingDate));
        };

        let applyRegionRestrictions = function() {
            $("div[data-checkbox-value='0']").trigger("click");
        };

        var applyPrivate = function(group) {
            if (group.name) {
                var privateButton = $("div[data-checkbox-value='invite_only']");
                if (!privateButton.hasClass('is-selected')) {
                    privateButton.trigger("click");
                }
            }
        };

        let scrollToGroup = function(group) {
            var container = $('.form__groups'),
                scrollTo = group;

            container.scrollTop(
                scrollTo.offset().top - container.offset().top + container.scrollTop()
            );
        };

        let applyGroup = function(group) {
            if (group.name) {
                var privateButton = $("div[data-checkbox-value='groups']");
                if (!privateButton.hasClass('is-selected')) {
                    privateButton.trigger("click");
                }

                var group_form = $('div.form__group__details > div.form__group__name:contains(' + group.name + ')');
                group_form.click();
                scrollToGroup(group_form);
            }
        };

        let applyDescription = function(group) {
            let descarea = $("textarea[name='description']");
            descarea.val(group.description);
        };

        let applyLevel = function(group) {
            let levelValue = $("input[name='contributor_level']");
            levelValue.val(group.level);
            $(".ui-slider-range").css('width', group.level * 10 + "%");
            $(".ui-slider-handle").css('left', group.level * 10 + "%");
            if (group.level > 0) {
                $(".form__input-description--level").removeClass("is-hidden");
                $(".form__input-description--no-level").addClass("is-hidden");
                $("span.form__level").text("level " + group.level);
            } else {
                $(".form__input-description--no-level").removeClass("is-hidden");
                $(".form__input-description--level").addClass("is-hidden");
            }

        };

        let reset = function() {
            $(".form__checkbox.is-disabled").show();
            $('div.form__group.form__group--steam.is-selected').click(); // deselect all previously selected groups
            $("div[data-checkbox-value='everyone']").click(); // set "who can enter" to everyone
            var group = {
                level: 0
            };
            applyLevel(group); // reset level to 0
            console.log("resetted checkboxes/groups/level")
        };

        let applySettings = function(group) {
            reset();
            applyGiveawayTypeKey();
            applyDates();
            applyRegionRestrictions();
            applyPrivate(group);
            applyDescription(group);
            applyLevel(group);
            applyGroup(group);
            // hides not used checkboxes
            $(".form__checkbox.is-disabled").hide();
            // focus input to enter game name
            $(".js__autocomplete-name").focus();
        };
    }

    /* Helpers */
    function removeFromArray(arr, item) {
        for (let i = arr.length; i--;) {
            if (arr[i] === item) {
                arr.splice(i, 1);
            }
        }
    }

    function formatDate(date) {
        // Fixed by Archi for all SG weird dates, do not touch

        // Fix hours
        let hours = date.getHours();
        let ampm = '';
        if (hours < 12) {
            ampm = 'am';
            if (hours === 0) {
                hours = 12;
            }
        } else {
            ampm = 'pm';
            if (hours !== 12) {
                hours = hours % 12;
            }
        }

        // Fix minutes
        let minutes = date.getMinutes();
        if (minutes < 10) {
            minutes = '0' + minutes;
        }

        // Return result
        return $.datepicker.formatDate('M d, yy', date) + " " + hours + ":" + minutes + " " + ampm;
    }
})();

/*jshint multistr: true */
function addStyles() {
    GM_addStyle("\
input[type=text], textarea{\
-webkit-transition: all 0.30s ease-in-out;\
-moz-transition: all 0.30s ease-in-out;\
-ms-transition: all 0.30s ease-in-out;\
-o-transition: all 0.30s ease-in-out;\
outline: none;\
padding: 3px 0px 3px 3px;\
margin: 5px 1px 3px 0px;\
border: 1px solid #DDDDDD;\
} \
input[type=text]:focus, textarea:focus {\
box-shadow: 0 0 5px rgba(81, 203, 238, 1);\
padding: 3px 0px 3px 3px;\
margin: 5px 1px 3px 0px;\
border: 1px solid rgba(81, 203, 238, 1) !important; \
}");
}
<|endoftext|>


<commit_before>'use strict'

var server = require('./server')
  , stream = require('stream')
  , fs = require('fs')
  , request = require('../index')
  , path = require('path')
  , util = require('util')
  , tape = require('tape')

var s = server.createServer()

s.on('/cat', function(req, res) {
  if (req.method === 'GET') {
    res.writeHead(200, {
      'content-type': 'text/plain-test',
      'content-length': 4
    })
    res.end('asdf')
  } else if (req.method === 'PUT') {
    var body = ''
    req.on('data', function(chunk) {
      body += chunk
    }).on('end', function() {
      res.writeHead(201)
      res.end()
      s.emit('catDone', req, res, body)
    })
  }
})

s.on('/doodle', function(req, res) {
  if (req.headers['x-oneline-proxy']) {
    res.setHeader('x-oneline-proxy', 'yup')
  }
  res.writeHead('200', { 'content-type': 'image/jpeg' })
  fs.createReadStream(path.join(__dirname, 'googledoodle.jpg')).pipe(res)
})

function ValidationStream(t, str) {
  this.str = str
  this.buf = ''
  this.on('data', function(data) {
    this.buf += data
  })
  this.on('end', function() {
    t.equal(this.str, this.buf)
  })
  this.writable = true
}

util.inherits(ValidationStream, stream.Stream)

ValidationStream.prototype.write = function(chunk) {
  this.emit('data', chunk)
}

ValidationStream.prototype.end = function(chunk) {
  if (chunk) {
    this.emit('data', chunk)
  }
  this.emit('end')
}

tape('setup', function(t) {
  s.listen(0, function() {
    t.end()
  })
})

tape('piping to a request object', function(t) {
  s.once('/push', server.createPostValidator('mydata'))

  var mydata = new stream.Stream()
  mydata.readable = true

  var r1 = request.put({
    url: s.url + '/push'
  }, function(err, res, body) {
    t.equal(err, null)
    t.equal(res.statusCode, 200)
    t.equal(body, 'mydata')
    t.end()
  })
  mydata.pipe(r1)

  mydata.emit('data', 'mydata')
  mydata.emit('end')
})

tape('piping to a request object with invalid uri', function(t) {
  var mybodydata = new stream.Stream()
  mybodydata.readable = true

  var r2 = request.put({
    url: '/bad-uri',
    json: true
  }, function(err, res, body) {
    t.ok(err instanceof Error)
    t.equal(err.message, 'Invalid URI "/bad-uri"')
    t.end()
  })
  mybodydata.pipe(r2)

  mybodydata.emit('data', JSON.stringify({ foo: 'bar' }))
  mybodydata.emit('end')
})

tape('piping to a request object with a json body', function(t) {
  var obj = {foo: 'bar'}
  var json = JSON.stringify(obj)
  s.once('/push-json', server.createPostValidator(json, 'application/json'))
  var mybodydata = new stream.Stream()
  mybodydata.readable = true

  var r2 = request.put({
    url: s.url + '/push-json',
    json: true
  }, function(err, res, body) {
    t.equal(err, null)
    t.equal(res.statusCode, 200)
    t.deepEqual(body, obj)
    t.end()
  })
  mybodydata.pipe(r2)

  mybodydata.emit('data', JSON.stringify({ foo: 'bar' }))
  mybodydata.emit('end')
})

tape('piping from a request object', function(t) {
  s.once('/pull', server.createGetResponse('mypulldata'))

  var mypulldata = new stream.Stream()
  mypulldata.writable = true

  request({
    url: s.url + '/pull'
  }).pipe(mypulldata)

  var d = ''

  mypulldata.write = function(chunk) {
    d += chunk
  }
  mypulldata.end = function() {
    t.equal(d, 'mypulldata')
    t.end()
  }
})

tape('pause when piping from a request object', function(t) {
  s.once('/chunks', function(req, res) {
    res.writeHead(200, {
      'content-type': 'text/plain'
    })
    res.write('Chunk 1')
    setTimeout(function() { res.end('Chunk 2') }, 10)
  })

  var chunkNum = 0
  var paused = false
  request({
    url: s.url + '/chunks'
  })
    .on('data', function(chunk) {
      var self = this

      t.notOk(paused, 'Only receive data when not paused')

      ++chunkNum
      if (chunkNum === 1) {
        t.equal(chunk.toString(), 'Chunk 1')
        self.pause()
        paused = true
        setTimeout(function() {
          paused = false
          self.resume()
        }, 100)
      } else {
        t.equal(chunk.toString(), 'Chunk 2')
      }
    })
    .on('end', t.end.bind(t))
})

tape('pause before piping from a request object', function(t) {
  s.once('/pause-before', function(req, res) {
    res.writeHead(200, {
      'content-type': 'text/plain'
    })
    res.end('Data')
  })

  var paused = true
  var r = request({
    url: s.url + '/pause-before'
  })
  r.pause()
  r.on('data', function(data) {
    t.notOk(paused, 'Only receive data when not paused')
    t.equal(data.toString(), 'Data')
  })
  r.on('end', t.end.bind(t))

  setTimeout(function() {
    paused = false
    r.resume()
  }, 100)
})

var fileContents = fs.readFileSync(__filename).toString()
function testPipeFromFile(testName, hasContentLength) {
  tape(testName, function(t) {
    s.once('/pushjs', function(req, res) {
      if (req.method === 'PUT') {
        t.equal(req.headers['content-type'], 'application/javascript')
        t.equal(
          req.headers['content-length'],
          (hasContentLength ? '' + fileContents.length : undefined))
        var body = ''
        req.on('data', function(data) {
          body += data
        })
        req.on('end', function() {
          t.equal(body, fileContents)
          t.end()
        })
        res.end()
      }
    })
    var r = request.put(s.url + '/pushjs')
    fs.createReadStream(__filename).pipe(r)
    if (hasContentLength) {
      r.setHeader('content-length', fileContents.length)
    }
  })
}

// TODO Piping from a file does not send content-length header
testPipeFromFile('piping from a file', false)
testPipeFromFile('piping from a file with content-length', true)

tape('piping to and from same URL', function(t) {
  s.once('catDone', function(req, res, body) {
    t.equal(req.headers['content-type'], 'text/plain-test')
    t.equal(req.headers['content-length'], '4')
    t.equal(body, 'asdf')
    t.end()
  })
  request.get(s.url + '/cat')
    .pipe(request.put(s.url + '/cat'))
})

tape('piping between urls', function(t) {
  s.once('/catresp', function(req, res) {
    request.get(s.url + '/cat').pipe(res)
  })

  request.get(s.url + '/catresp', function(err, res, body) {
    t.equal(err, null)
    t.equal(res.headers['content-type'], 'text/plain-test')
    t.equal(res.headers['content-length'], '4')
    t.end()
  })
})

tape('writing to file', function(t) {
  var doodleWrite = fs.createWriteStream(path.join(__dirname, 'test.jpg'))

  request.get(s.url + '/doodle').pipe(doodleWrite)

  doodleWrite.on('close', function() {
    t.deepEqual(
      fs.readFileSync(path.join(__dirname, 'googledoodle.jpg')),
      fs.readFileSync(path.join(__dirname, 'test.jpg')))
    fs.unlinkSync(path.join(__dirname, 'test.jpg'))
    t.end()
  })
})

tape('one-line proxy', function(t) {
  s.once('/onelineproxy', function(req, res) {
    var x = request(s.url + '/doodle')
    req.pipe(x)
    x.pipe(res)
  })

  request.get({
    uri: s.url + '/onelineproxy',
    headers: { 'x-oneline-proxy': 'nope' }
  }, function(err, res, body) {
    t.equal(err, null)
    t.equal(res.headers['x-oneline-proxy'], 'yup')
    t.end()
  })
})

tape('piping after response', function(t) {
  s.once('/afterresponse', function(req, res) {
    res.write('d')
    res.end()
  })

  var rAfterRes = request.post(s.url + '/afterresponse')

  rAfterRes.on('response', function() {
    var v = new ValidationStream(t, 'd')
    rAfterRes.pipe(v)
    v.on('end', function() {
      t.end()
    })
  })
})

tape('piping through a redirect', function(t) {
  s.once('/forward1', function(req, res) {
    res.writeHead(302, { location: '/forward2' })
    res.end()
  })
  s.once('/forward2', function(req, res) {
    res.writeHead('200', { 'content-type': 'image/png' })
    res.write('d')
    res.end()
  })

  var validateForward = new ValidationStream(t, 'd')

  request.get(s.url + '/forward1').pipe(validateForward)

  validateForward.on('end', function() {
    t.end()
  })
})

tape('pipe options', function(t) {
  s.once('/opts', server.createGetResponse('opts response'))

  var optsStream = new stream.Stream()
    , optsData = ''

  optsStream.writable = true
  optsStream.write = function(buf) {
    optsData += buf
    if (optsData === 'opts response') {
      setTimeout(function() {
        t.end()
      }, 10)
    }
  }
  optsStream.end = function() {
    t.fail('end called')
  }

  request({
    url: s.url + '/opts'
  }).pipe(optsStream, { end: false })
})

tape('request.pipefilter is called correctly', function(t) {
  s.once('/pipefilter', function(req, res) {
    res.end('d')
  })
  var validatePipeFilter = new ValidationStream(t, 'd')

  var r3 = request.get(s.url + '/pipefilter')
  r3.pipe(validatePipeFilter)
  r3.pipefilter = function(res, dest) {
    t.equal(res, r3.response)
    t.equal(dest, validatePipeFilter)
    t.end()
  }
})

tape('cleanup', function(t) {
  s.close(function() {
    t.end()
  })
})
<commit_msg>Don't end response early in tests/test-pipes<commit_after>'use strict'

var server = require('./server')
  , stream = require('stream')
  , fs = require('fs')
  , request = require('../index')
  , path = require('path')
  , util = require('util')
  , tape = require('tape')

var s = server.createServer()

s.on('/cat', function(req, res) {
  if (req.method === 'GET') {
    res.writeHead(200, {
      'content-type': 'text/plain-test',
      'content-length': 4
    })
    res.end('asdf')
  } else if (req.method === 'PUT') {
    var body = ''
    req.on('data', function(chunk) {
      body += chunk
    }).on('end', function() {
      res.writeHead(201)
      res.end()
      s.emit('catDone', req, res, body)
    })
  }
})

s.on('/doodle', function(req, res) {
  if (req.headers['x-oneline-proxy']) {
    res.setHeader('x-oneline-proxy', 'yup')
  }
  res.writeHead('200', { 'content-type': 'image/jpeg' })
  fs.createReadStream(path.join(__dirname, 'googledoodle.jpg')).pipe(res)
})

function ValidationStream(t, str) {
  this.str = str
  this.buf = ''
  this.on('data', function(data) {
    this.buf += data
  })
  this.on('end', function() {
    t.equal(this.str, this.buf)
  })
  this.writable = true
}

util.inherits(ValidationStream, stream.Stream)

ValidationStream.prototype.write = function(chunk) {
  this.emit('data', chunk)
}

ValidationStream.prototype.end = function(chunk) {
  if (chunk) {
    this.emit('data', chunk)
  }
  this.emit('end')
}

tape('setup', function(t) {
  s.listen(0, function() {
    t.end()
  })
})

tape('piping to a request object', function(t) {
  s.once('/push', server.createPostValidator('mydata'))

  var mydata = new stream.Stream()
  mydata.readable = true

  var r1 = request.put({
    url: s.url + '/push'
  }, function(err, res, body) {
    t.equal(err, null)
    t.equal(res.statusCode, 200)
    t.equal(body, 'mydata')
    t.end()
  })
  mydata.pipe(r1)

  mydata.emit('data', 'mydata')
  mydata.emit('end')
})

tape('piping to a request object with invalid uri', function(t) {
  var mybodydata = new stream.Stream()
  mybodydata.readable = true

  var r2 = request.put({
    url: '/bad-uri',
    json: true
  }, function(err, res, body) {
    t.ok(err instanceof Error)
    t.equal(err.message, 'Invalid URI "/bad-uri"')
    t.end()
  })
  mybodydata.pipe(r2)

  mybodydata.emit('data', JSON.stringify({ foo: 'bar' }))
  mybodydata.emit('end')
})

tape('piping to a request object with a json body', function(t) {
  var obj = {foo: 'bar'}
  var json = JSON.stringify(obj)
  s.once('/push-json', server.createPostValidator(json, 'application/json'))
  var mybodydata = new stream.Stream()
  mybodydata.readable = true

  var r2 = request.put({
    url: s.url + '/push-json',
    json: true
  }, function(err, res, body) {
    t.equal(err, null)
    t.equal(res.statusCode, 200)
    t.deepEqual(body, obj)
    t.end()
  })
  mybodydata.pipe(r2)

  mybodydata.emit('data', JSON.stringify({ foo: 'bar' }))
  mybodydata.emit('end')
})

tape('piping from a request object', function(t) {
  s.once('/pull', server.createGetResponse('mypulldata'))

  var mypulldata = new stream.Stream()
  mypulldata.writable = true

  request({
    url: s.url + '/pull'
  }).pipe(mypulldata)

  var d = ''

  mypulldata.write = function(chunk) {
    d += chunk
  }
  mypulldata.end = function() {
    t.equal(d, 'mypulldata')
    t.end()
  }
})

tape('pause when piping from a request object', function(t) {
  s.once('/chunks', function(req, res) {
    res.writeHead(200, {
      'content-type': 'text/plain'
    })
    res.write('Chunk 1')
    setTimeout(function() { res.end('Chunk 2') }, 10)
  })

  var chunkNum = 0
  var paused = false
  request({
    url: s.url + '/chunks'
  })
    .on('data', function(chunk) {
      var self = this

      t.notOk(paused, 'Only receive data when not paused')

      ++chunkNum
      if (chunkNum === 1) {
        t.equal(chunk.toString(), 'Chunk 1')
        self.pause()
        paused = true
        setTimeout(function() {
          paused = false
          self.resume()
        }, 100)
      } else {
        t.equal(chunk.toString(), 'Chunk 2')
      }
    })
    .on('end', t.end.bind(t))
})

tape('pause before piping from a request object', function(t) {
  s.once('/pause-before', function(req, res) {
    res.writeHead(200, {
      'content-type': 'text/plain'
    })
    res.end('Data')
  })

  var paused = true
  var r = request({
    url: s.url + '/pause-before'
  })
  r.pause()
  r.on('data', function(data) {
    t.notOk(paused, 'Only receive data when not paused')
    t.equal(data.toString(), 'Data')
  })
  r.on('end', t.end.bind(t))

  setTimeout(function() {
    paused = false
    r.resume()
  }, 100)
})

var fileContents = fs.readFileSync(__filename)
function testPipeFromFile(testName, hasContentLength) {
  tape(testName, function(t) {
    s.once('/pushjs', function(req, res) {
      if (req.method === 'PUT') {
        t.equal(req.headers['content-type'], 'application/javascript')
        t.equal(
          req.headers['content-length'],
          (hasContentLength ? '' + fileContents.length : undefined))
        var body = ''
        req.setEncoding('utf8')
        req.on('data', function(data) {
          body += data
        })
        req.on('end', function() {
          res.end()
          t.equal(body, fileContents.toString())
          t.end()
        })
      } else {
        res.end()
      }
    })
    var r = request.put(s.url + '/pushjs')
    fs.createReadStream(__filename).pipe(r)
    if (hasContentLength) {
      r.setHeader('content-length', fileContents.length)
    }
  })
}

// TODO Piping from a file does not send content-length header
testPipeFromFile('piping from a file', false)
testPipeFromFile('piping from a file with content-length', true)

tape('piping to and from same URL', function(t) {
  s.once('catDone', function(req, res, body) {
    t.equal(req.headers['content-type'], 'text/plain-test')
    t.equal(req.headers['content-length'], '4')
    t.equal(body, 'asdf')
    t.end()
  })
  request.get(s.url + '/cat')
    .pipe(request.put(s.url + '/cat'))
})

tape('piping between urls', function(t) {
  s.once('/catresp', function(req, res) {
    request.get(s.url + '/cat').pipe(res)
  })

  request.get(s.url + '/catresp', function(err, res, body) {
    t.equal(err, null)
    t.equal(res.headers['content-type'], 'text/plain-test')
    t.equal(res.headers['content-length'], '4')
    t.end()
  })
})

tape('writing to file', function(t) {
  var doodleWrite = fs.createWriteStream(path.join(__dirname, 'test.jpg'))

  request.get(s.url + '/doodle').pipe(doodleWrite)

  doodleWrite.on('close', function() {
    t.deepEqual(
      fs.readFileSync(path.join(__dirname, 'googledoodle.jpg')),
      fs.readFileSync(path.join(__dirname, 'test.jpg')))
    fs.unlinkSync(path.join(__dirname, 'test.jpg'))
    t.end()
  })
})

tape('one-line proxy', function(t) {
  s.once('/onelineproxy', function(req, res) {
    var x = request(s.url + '/doodle')
    req.pipe(x)
    x.pipe(res)
  })

  request.get({
    uri: s.url + '/onelineproxy',
    headers: { 'x-oneline-proxy': 'nope' }
  }, function(err, res, body) {
    t.equal(err, null)
    t.equal(res.headers['x-oneline-proxy'], 'yup')
    t.end()
  })
})

tape('piping after response', function(t) {
  s.once('/afterresponse', function(req, res) {
    res.write('d')
    res.end()
  })

  var rAfterRes = request.post(s.url + '/afterresponse')

  rAfterRes.on('response', function() {
    var v = new ValidationStream(t, 'd')
    rAfterRes.pipe(v)
    v.on('end', function() {
      t.end()
    })
  })
})

tape('piping through a redirect', function(t) {
  s.once('/forward1', function(req, res) {
    res.writeHead(302, { location: '/forward2' })
    res.end()
  })
  s.once('/forward2', function(req, res) {
    res.writeHead('200', { 'content-type': 'image/png' })
    res.write('d')
    res.end()
  })

  var validateForward = new ValidationStream(t, 'd')

  request.get(s.url + '/forward1').pipe(validateForward)

  validateForward.on('end', function() {
    t.end()
  })
})

tape('pipe options', function(t) {
  s.once('/opts', server.createGetResponse('opts response'))

  var optsStream = new stream.Stream()
    , optsData = ''

  optsStream.writable = true
  optsStream.write = function(buf) {
    optsData += buf
    if (optsData === 'opts response') {
      setTimeout(function() {
        t.end()
      }, 10)
    }
  }
  optsStream.end = function() {
    t.fail('end called')
  }

  request({
    url: s.url + '/opts'
  }).pipe(optsStream, { end: false })
})

tape('request.pipefilter is called correctly', function(t) {
  s.once('/pipefilter', function(req, res) {
    res.end('d')
  })
  var validatePipeFilter = new ValidationStream(t, 'd')

  var r3 = request.get(s.url + '/pipefilter')
  r3.pipe(validatePipeFilter)
  r3.pipefilter = function(res, dest) {
    t.equal(res, r3.response)
    t.equal(dest, validatePipeFilter)
    t.end()
  }
})

tape('cleanup', function(t) {
  s.close(function() {
    t.end()
  })
})
<|endoftext|>


<commit_before>/*
 * Copyright 2016-2017 Hewlett Packard Enterprise Development Company, L.P.
 * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
 */

define([
    'underscore',
    'jquery',
    'backbone',
    'moment',
    'find/app/vent',
    'find/app/model/find-base-collection',
    'find/app/page/search/filters/parametric/calibrate-buckets',
    'find/app/page/search/filters/parametric/numeric-range-rounder',
    'find/app/page/search/filters/parametric/numeric-widget',
    'find/app/model/bucketed-parametric-collection',
    'parametric-refinement/to-field-text-node',
    'find/app/util/date-picker',
    'js-whatever/js/model-any-changed-attribute-listener',
    'text!find/templates/app/page/search/filters/parametric/numeric-parametric-field-view.html',
    'text!find/templates/app/page/search/filters/parametric/numeric-parametric-field-view-numeric-input.html',
    'text!find/templates/app/page/search/filters/parametric/numeric-parametric-field-view-date-input.html',
    'text!find/templates/app/page/loading-spinner.html',
    'i18n!find/nls/bundle'
], function(_, $, Backbone, moment, vent, FindBaseCollection, calibrateBuckets, rounder, numericWidget,
            BucketedParametricCollection, toFieldTextNode, datePicker, addChangeListener,
            template, numericInputTemplate, dateInputTemplate, loadingTemplate, i18n) {
    'use strict';

    function sum(a, b) {
        return a + b;
    }

    // This view must be visible before it is rendered
    const NumericParametricFieldView = Backbone.View.extend({
        className: 'animated fadeIn',
        loadingSpinnerHtml: _.template(loadingTemplate)({i18n: i18n, large: true}),
        svgTemplate: _.template('<svg class="chart <%- selectionEnabled ? \'chart-selection-enabled\' : \'\' %>"></svg>'),
        template: _.template(template),

        events: {
            'click .numeric-parametric-no-min': function() {
                this.updateRestrictions([this.model.get('min'), null]);
            },
            'click .numeric-parametric-no-max': function() {
                this.updateRestrictions([null, this.model.get('max')]);
            },
            'click .numeric-parametric-reset': function() {
                this.clearRestrictions();
                this.model.set(this.model.getDefaultCurrentRange());
            },
            'change .numeric-parametric-min-input': function() {
                this.updateRestrictions([this.readMinInput(), null]);
            },
            'change .numeric-parametric-max-input': function() {
                this.updateRestrictions([null, this.readMaxInput()]);
            },
            'dp.change .results-filter-date[data-date-attribute="min-date"]': function(event) {
                this.updateRestrictions([event.date.unix(), null]);
            },
            'dp.change .results-filter-date[data-date-attribute="max-date"]': function(event) {
                this.updateRestrictions([null, event.date.unix()]);
            },
            'click .clickable-widget': function() {
                this.clickCallback();
            }
        },

        initialize: function(options) {
            this.delayedFetchBuckets = _.debounce(this.fetchBuckets, 500);
            this.collapseModel = options.collapseModel || null;

            this.inputTemplate = options.inputTemplate || NumericParametricFieldView.numericInputTemplate;
            this.queryModel = options.queryModel;
            this.selectedParametricValues = options.selectedParametricValues;
            this.pixelsPerBucket = options.pixelsPerBucket;
            this.selectionEnabled = options.selectionEnabled;
            this.zoomEnabled = options.zoomEnabled;
            this.buttonsEnabled = options.selectionEnabled && options.buttonsEnabled;
            this.coordinatesEnabled = _.isUndefined(options.coordinatesEnabled) || options.coordinatesEnabled;
            this.hideTitle = options.hideTitle;
            this.clickCallback = options.clickCallback;

            this.fieldName = this.model.id;
            this.displayName = this.model.get('displayName');
            this.type = this.model.get('type');

            const formatting = this.type === 'NumericDate'
                ? options.formatting
                : NumericParametricFieldView.defaultFormatting;
            this.formatValue = function(value) {
                return formatting.format(value, this.model.get('currentMin'), this.model.get('currentMax'));
            }.bind(this);
            this.parseValue = formatting.parse;
            this.renderCustomFormatting = formatting.render;
            this.parseBoundarySelection = formatting.parseBoundarySelection;

            this.widget = numericWidget({formattingFn: this.formatValue});

            // The view's this.model contains the current and
            // absolute ranges, the bucketModel contains the values
            this.bucketModel = new BucketedParametricCollection.Model({id: this.fieldName});

            // Bind the selection rectangle to the selected parametric range
            this.listenTo(this.selectedParametricValues, 'add remove change', function(model) {
                if(this.isTargetModel(model)) {
                    this.updateSelection();
                }
            });

            addChangeListener(this, this.model, ['currentMin', 'currentMax'], function() {
                // Immediately update the graph for the new range; we calibrate
                // the buckets to remove buckets outside of the range
                this.updateGraph();

                // Fetch new buckets when the range changes
                this.fetchBuckets();
            });

            addChangeListener(this, this.queryModel,
                ['queryText', 'fieldText', 'indexes', 'minDate', 'maxDate', 'minScore'],
                function() {
                    // Existing buckets are incorrect when the query changes
                    this.bucketModel.set({values: []});
                    this.fetchBuckets();
                });

            this.listenTo(vent, 'vent:resize', this.delayedFetchBucketsAndUpdate);

            this.listenTo(this.bucketModel, 'change:values request sync error', this.updateGraph);
        },

        render: function() {
            this.$el
                .html(this.template({
                    i18n: i18n,
                    fieldName: this.hideTitle
                        ? undefined
                        : this.model.get('displayName'),
                    clickable: !!this.clickCallback,
                    buttonsEnabled: this.buttonsEnabled,
                    inputsRowClass: this.selectionEnabled || this.coordinatesEnabled
                        ? ''
                        : 'hide',
                    inputColumnClass: this.selectionEnabled
                        ? (this.coordinatesEnabled ? 'col-xs-4' : 'col-xs-6')
                        : 'hide',
                    inputTemplate: this.inputTemplate,
                    loadingSpinnerHtml: this.loadingSpinnerHtml,
                    coordinatesColumnClass: this.coordinatesEnabled
                        ? (this.selectionEnabled ? 'col-xs-4' : 'col-xs-12')
                        : 'hide'
                }));

            if(this.selectionEnabled) {
                this.renderCustomFormatting(this.$el);
                this.$minInput = this.$('.numeric-parametric-min-input');
                this.$maxInput = this.$('.numeric-parametric-max-input');
            }

            this.updateGraph();

            // Width may have changed, re-fetch the buckets
            this.fetchBuckets();
        },

        // Draw the graph with the current data and ranges, or display
        // the loading spinner if we don't have any data yet
        updateGraph: function() {
            const noError = !this.bucketModel.error;
            const fetching = this.bucketModel.fetching;
            const hasValues = this.model.get('totalValues') !== 0;
            const modelBuckets = this.bucketModel.get('values');

            this.$('.numeric-parametric-error-text').toggleClass('hide', noError);
            this.$('.numeric-parametric-empty-text').toggleClass('hide', !noError || hasValues);

            const hideLoadingIndicator = !(noError && hasValues && fetching && modelBuckets.length === 0);
            this.$('.numeric-parametric-loading-indicator').toggleClass('hide', hideLoadingIndicator);

            const $chartRow = this.$('.numeric-parametric-chart-row');
            $chartRow.find('.chart').remove();
            const width = $chartRow.width();

            if(noError && hasValues && hideLoadingIndicator && width > 0) {
                const $chart = $(this.svgTemplate({selectionEnabled: this.selectionEnabled}));
                $chartRow.append($chart);

                const buckets = calibrateBuckets(
                    modelBuckets,
                    [this.model.get('currentMin'), this.model.get('currentMax')]
                );

                // Update the inputs as the user drags a selection on the graph.
                // Note that this means the value in the input does not depend
                // on just the selected parametric range model.
                const updateCallback = function(x1, x2) {
                    // rounding to one decimal place
                    this.updateMinInput(x1);
                    this.updateMaxInput(x2);
                }.bind(this);

                const selectionCallback = function(x1, x2) {
                    const newMin = this.parseBoundarySelection(x1);
                    const newMax = this.parseBoundarySelection(x2);
                    this.updateRestrictions([newMin, newMax]);
                }.bind(this);

                const mouseMoveCallback = function(x) {
                    this.$('.numeric-parametric-co-ordinates').text(this.formatValue(x));
                }.bind(this);

                const mouseLeaveCallback = function() {
                    this.$('.numeric-parametric-co-ordinates').text('');
                }.bind(this);

                const zoomCallback = function(min, max) {
                    this.model.set({
                        currentMin: min,
                        currentMax: max
                    });
                }.bind(this);

                this.graph = this.widget.drawGraph({
                    chart: $chart.get(0),
                    data: buckets,
                    updateCallback: updateCallback,
                    selectionCallback: selectionCallback,
                    deselectionCallback: this.clearRestrictions.bind(this),
                    mouseMoveCallback: mouseMoveCallback,
                    mouseLeaveCallback: mouseLeaveCallback,
                    zoomCallback: zoomCallback,
                    xRange: width,
                    yRange: $chart.height(),
                    tooltip: i18n['search.numericParametricFields.tooltip'],
                    dragEnabled: this.selectionEnabled,
                    zoomEnabled: this.zoomEnabled,
                    coordinatesEnabled: this.coordinatesEnabled
                });

                this.updateSelection();
            }

            this.listenTo(this.selectedParametricValues, 'remove', function(model) {
                if(model.get('field') == this.fieldName) {
                    this.updateSelection();
                }
            });
        },

        // Update the rendered selection rectangle and
        // inputs to match the selected parametric range model
        updateSelection: function() {
            if(this.graph) {
                const rangeModel = this.selectedParametricValues
                    .find(this.isTargetModel.bind(this));

                if(rangeModel) {
                    const range = rangeModel.get('range');

                    this.updateMinInput(range[0]);
                    this.updateMaxInput(range[1]);
                    this.graph.setSelection(range);
                } else {
                    this.updateMinInput(this.model.get('min'));
                    this.updateMaxInput(this.model.get('max'));
                    this.graph.clearSelection();
                }
            }
        },

        // Apply a new range selection; a null boundary will not be updated
        // Should be called with values that are already parsed
        updateRestrictions: function(newRange) {
            const existingModel = this.selectedParametricValues
                .find(this.isTargetModel.bind(this));
            const existingRange = existingModel
                ? existingModel.get('range')
                : [this.model.get('min'), this.model.get('max')];

            const newAttributes = {
                field: this.fieldName,
                displayName: this.displayName,
                type: this.type,
                range: _.map(newRange, function(value, index) {
                    // Explicitly check null since 0 is falsy
                    return value === null ? existingRange[index] : value;
                })
            };

            // Fixes error where user could manually input min > max or max < min
            if(newAttributes.range[0] > newAttributes.range[1]) {
                if(existingRange.reduce(sum) - newAttributes.range.reduce(sum) > 0) { // if max was decreased
                    newAttributes.range[0] = newAttributes.range[1]; //set min to equal max
                } else { // if min was increased
                    newAttributes.range[1] = newAttributes.range[0]; //set max to equal min
                }
            }

            if(existingModel) {
                existingModel.set(newAttributes);
            } else {
                this.selectedParametricValues.add(newAttributes);
            }
        },

        clearRestrictions: function() {
            this.selectedParametricValues.remove(
                this.selectedParametricValues
                    .filter(this.isTargetModel.bind(this))
            );
        },

        fetchBuckets: function() {
            if(!(this.collapseModel && this.collapseModel.get('collapsed'))) {
                const width = this.$('.numeric-parametric-chart-row').width();

                // If the SVG has no width or there are no values, there is no point fetching new data
                if(!(width === 0 || this.model.get('totalValues') === 0)) {
                    // Exclude any restrictions for this field from the field text
                    const otherSelectedValues = this.selectedParametricValues
                        .reject(this.isTargetModel.bind(this))
                        .map(function(model) {
                            return model.toJSON();
                        });

                    this.bucketModel.fetch({
                        data: {
                            queryText: this.queryModel.get('queryText'),
                            fieldText: toFieldTextNode(otherSelectedValues),
                            minDate: this.queryModel.getIsoDate('minDate'),
                            maxDate: this.queryModel.getIsoDate('maxDate'),
                            minScore: this.queryModel.get('minScore'),
                            databases: this.queryModel.get('indexes'),
                            targetNumberOfBuckets: Math.floor(width / this.pixelsPerBucket),
                            bucketMin: this.model.get('currentMin'),
                            bucketMax: this.model.get('currentMax')
                        }
                    });
                }
            }
        },

        delayedFetchBucketsAndUpdate: function() {
            this.updateGraph();
            this.delayedFetchBuckets();
        },

        readMinInput: function() {
            return this.parseValue(this.$minInput.val());
        },

        readMaxInput: function() {
            return this.parseValue(this.$maxInput.val());
        },

        updateMinInput: function(newValue) {
            if(this.selectionEnabled) {
                this.$minInput.val(this.formatValue(newValue));
            }
        },

        updateMaxInput: function(newValue) {
            if(this.selectionEnabled) {
                this.$maxInput.val(this.formatValue(newValue));
            }
        },

        isTargetModel: function(model) {
            return model.get('field') === this.fieldName &&
                model.get('range') &&
                model.get('type') === this.type;
        }
    }, {
        dateInputTemplate: _.template(dateInputTemplate),
        numericInputTemplate: _.template(numericInputTemplate),
        defaultFormatting: {
            format: rounder().round,
            parse: Number,
            parseBoundarySelection: _.identity,
            render: _.noop
        },
        dateFormatting: {
            format: function(unformattedString) {
                return moment(Math.round(unformattedString * 1000)).format(datePicker.DATE_WIDGET_FORMAT);
            },
            parse: function(formattedString) {
                return moment(formattedString, datePicker.DATE_WIDGET_FORMAT).unix();
            },
            parseBoundarySelection: function(input) {
                // Epoch seconds are not decimal
                return Math.round(input);
            },
            render: function($el) {
                datePicker.render($el.find('.results-filter-date[data-date-attribute="min-date"]'),
                    function() {
                        this.updateRestrictions([this.readMinInput(), null]);
                    }.bind(this));
                datePicker.render($el.find('.results-filter-date[data-date-attribute="max-date"]'),
                    function() {
                        this.updateRestrictions([null, this.readMaxInput()]);
                    }.bind(this));
            }
        }
    });

    return NumericParametricFieldView;
});
<commit_msg>[FIND-1151] Throttle bucketing calls on scroll-zoom<commit_after>/*
 * Copyright 2016-2017 Hewlett Packard Enterprise Development Company, L.P.
 * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
 */

define([
    'underscore',
    'jquery',
    'backbone',
    'moment',
    'find/app/vent',
    'find/app/model/find-base-collection',
    'find/app/page/search/filters/parametric/calibrate-buckets',
    'find/app/page/search/filters/parametric/numeric-range-rounder',
    'find/app/page/search/filters/parametric/numeric-widget',
    'find/app/model/bucketed-parametric-collection',
    'parametric-refinement/to-field-text-node',
    'find/app/util/date-picker',
    'js-whatever/js/model-any-changed-attribute-listener',
    'text!find/templates/app/page/search/filters/parametric/numeric-parametric-field-view.html',
    'text!find/templates/app/page/search/filters/parametric/numeric-parametric-field-view-numeric-input.html',
    'text!find/templates/app/page/search/filters/parametric/numeric-parametric-field-view-date-input.html',
    'text!find/templates/app/page/loading-spinner.html',
    'i18n!find/nls/bundle'
], function(_, $, Backbone, moment, vent, FindBaseCollection, calibrateBuckets, rounder, numericWidget,
            BucketedParametricCollection, toFieldTextNode, datePicker, addChangeListener,
            template, numericInputTemplate, dateInputTemplate, loadingTemplate, i18n) {
    'use strict';

    function sum(a, b) {
        return a + b;
    }

    // This view must be visible before it is rendered
    const NumericParametricFieldView = Backbone.View.extend({
        className: 'animated fadeIn',
        loadingSpinnerHtml: _.template(loadingTemplate)({i18n: i18n, large: true}),
        svgTemplate: _.template('<svg class="chart <%- selectionEnabled ? \'chart-selection-enabled\' : \'\' %>"></svg>'),
        template: _.template(template),

        events: {
            'click .numeric-parametric-no-min': function() {
                this.updateRestrictions([this.model.get('min'), null]);
            },
            'click .numeric-parametric-no-max': function() {
                this.updateRestrictions([null, this.model.get('max')]);
            },
            'click .numeric-parametric-reset': function() {
                this.clearRestrictions();
                this.model.set(this.model.getDefaultCurrentRange());
            },
            'change .numeric-parametric-min-input': function() {
                this.updateRestrictions([this.readMinInput(), null]);
            },
            'change .numeric-parametric-max-input': function() {
                this.updateRestrictions([null, this.readMaxInput()]);
            },
            'dp.change .results-filter-date[data-date-attribute="min-date"]': function(event) {
                this.updateRestrictions([event.date.unix(), null]);
            },
            'dp.change .results-filter-date[data-date-attribute="max-date"]': function(event) {
                this.updateRestrictions([null, event.date.unix()]);
            },
            'click .clickable-widget': function() {
                this.clickCallback();
            }
        },

        initialize: function(options) {
            this.delayedFetchBuckets = _.debounce(this.fetchBuckets, 500);
            this.collapseModel = options.collapseModel || null;

            this.inputTemplate = options.inputTemplate || NumericParametricFieldView.numericInputTemplate;
            this.queryModel = options.queryModel;
            this.selectedParametricValues = options.selectedParametricValues;
            this.pixelsPerBucket = options.pixelsPerBucket;
            this.selectionEnabled = options.selectionEnabled;
            this.zoomEnabled = options.zoomEnabled;
            this.buttonsEnabled = options.selectionEnabled && options.buttonsEnabled;
            this.coordinatesEnabled = _.isUndefined(options.coordinatesEnabled) || options.coordinatesEnabled;
            this.hideTitle = options.hideTitle;
            this.clickCallback = options.clickCallback;

            this.fieldName = this.model.id;
            this.displayName = this.model.get('displayName');
            this.type = this.model.get('type');

            const formatting = this.type === 'NumericDate'
                ? options.formatting
                : NumericParametricFieldView.defaultFormatting;
            this.formatValue = function(value) {
                return formatting.format(value, this.model.get('currentMin'), this.model.get('currentMax'));
            }.bind(this);
            this.parseValue = formatting.parse;
            this.renderCustomFormatting = formatting.render;
            this.parseBoundarySelection = formatting.parseBoundarySelection;

            this.widget = numericWidget({formattingFn: this.formatValue});

            // The view's this.model contains the current and
            // absolute ranges, the bucketModel contains the values
            this.bucketModel = new BucketedParametricCollection.Model({id: this.fieldName});

            // Bind the selection rectangle to the selected parametric range
            this.listenTo(this.selectedParametricValues, 'add remove change', function(model) {
                if(this.isTargetModel(model)) {
                    this.updateSelection();
                }
            });

            this.throttledFetchBuckets = _.throttle(this.fetchBuckets, 500);

            addChangeListener(this, this.model, ['currentMin', 'currentMax'], function() {
                // Immediately update the graph for the new range; we calibrate
                // the buckets to remove buckets outside of the range
                this.updateGraph();

                // Fetch new buckets when the range changes
                this.throttledFetchBuckets();
            });

            addChangeListener(this, this.queryModel,
                ['queryText', 'fieldText', 'indexes', 'minDate', 'maxDate', 'minScore'],
                function() {
                    // Existing buckets are incorrect when the query changes
                    this.bucketModel.set({values: []});
                    this.fetchBuckets();
                });

            this.listenTo(vent, 'vent:resize', this.delayedFetchBucketsAndUpdate);

            this.listenTo(this.bucketModel, 'change:values request sync error', this.updateGraph);
        },

        render: function() {
            this.$el
                .html(this.template({
                    i18n: i18n,
                    fieldName: this.hideTitle
                        ? undefined
                        : this.model.get('displayName'),
                    clickable: !!this.clickCallback,
                    buttonsEnabled: this.buttonsEnabled,
                    inputsRowClass: this.selectionEnabled || this.coordinatesEnabled
                        ? ''
                        : 'hide',
                    inputColumnClass: this.selectionEnabled
                        ? (this.coordinatesEnabled ? 'col-xs-4' : 'col-xs-6')
                        : 'hide',
                    inputTemplate: this.inputTemplate,
                    loadingSpinnerHtml: this.loadingSpinnerHtml,
                    coordinatesColumnClass: this.coordinatesEnabled
                        ? (this.selectionEnabled ? 'col-xs-4' : 'col-xs-12')
                        : 'hide'
                }));

            if(this.selectionEnabled) {
                this.renderCustomFormatting(this.$el);
                this.$minInput = this.$('.numeric-parametric-min-input');
                this.$maxInput = this.$('.numeric-parametric-max-input');
            }

            this.updateGraph();

            // Width may have changed, re-fetch the buckets
            this.fetchBuckets();
        },

        // Draw the graph with the current data and ranges, or display
        // the loading spinner if we don't have any data yet
        updateGraph: function() {
            const noError = !this.bucketModel.error;
            const fetching = this.bucketModel.fetching;
            const hasValues = this.model.get('totalValues') !== 0;
            const modelBuckets = this.bucketModel.get('values');

            this.$('.numeric-parametric-error-text').toggleClass('hide', noError);
            this.$('.numeric-parametric-empty-text').toggleClass('hide', !noError || hasValues);

            const hideLoadingIndicator = !(noError && hasValues && fetching && modelBuckets.length === 0);
            this.$('.numeric-parametric-loading-indicator').toggleClass('hide', hideLoadingIndicator);

            const $chartRow = this.$('.numeric-parametric-chart-row');
            $chartRow.find('.chart').remove();
            const width = $chartRow.width();

            if(noError && hasValues && hideLoadingIndicator && width > 0) {
                const $chart = $(this.svgTemplate({selectionEnabled: this.selectionEnabled}));
                $chartRow.append($chart);

                const buckets = calibrateBuckets(
                    modelBuckets,
                    [this.model.get('currentMin'), this.model.get('currentMax')]
                );

                // Update the inputs as the user drags a selection on the graph.
                // Note that this means the value in the input does not depend
                // on just the selected parametric range model.
                const updateCallback = function(x1, x2) {
                    // rounding to one decimal place
                    this.updateMinInput(x1);
                    this.updateMaxInput(x2);
                }.bind(this);

                const selectionCallback = function(x1, x2) {
                    const newMin = this.parseBoundarySelection(x1);
                    const newMax = this.parseBoundarySelection(x2);
                    this.updateRestrictions([newMin, newMax]);
                }.bind(this);

                const mouseMoveCallback = function(x) {
                    this.$('.numeric-parametric-co-ordinates').text(this.formatValue(x));
                }.bind(this);

                const mouseLeaveCallback = function() {
                    this.$('.numeric-parametric-co-ordinates').text('');
                }.bind(this);

                const zoomCallback = function(min, max) {
                    this.model.set({
                        currentMin: min,
                        currentMax: max
                    });
                }.bind(this);

                this.graph = this.widget.drawGraph({
                    chart: $chart.get(0),
                    data: buckets,
                    updateCallback: updateCallback,
                    selectionCallback: selectionCallback,
                    deselectionCallback: this.clearRestrictions.bind(this),
                    mouseMoveCallback: mouseMoveCallback,
                    mouseLeaveCallback: mouseLeaveCallback,
                    zoomCallback: zoomCallback,
                    xRange: width,
                    yRange: $chart.height(),
                    tooltip: i18n['search.numericParametricFields.tooltip'],
                    dragEnabled: this.selectionEnabled,
                    zoomEnabled: this.zoomEnabled,
                    coordinatesEnabled: this.coordinatesEnabled
                });

                this.updateSelection();
            }

            this.listenTo(this.selectedParametricValues, 'remove', function(model) {
                if(model.get('field') == this.fieldName) {
                    this.updateSelection();
                }
            });
        },

        // Update the rendered selection rectangle and
        // inputs to match the selected parametric range model
        updateSelection: function() {
            if(this.graph) {
                const rangeModel = this.selectedParametricValues
                    .find(this.isTargetModel.bind(this));

                if(rangeModel) {
                    const range = rangeModel.get('range');

                    this.updateMinInput(range[0]);
                    this.updateMaxInput(range[1]);
                    this.graph.setSelection(range);
                } else {
                    this.updateMinInput(this.model.get('min'));
                    this.updateMaxInput(this.model.get('max'));
                    this.graph.clearSelection();
                }
            }
        },

        // Apply a new range selection; a null boundary will not be updated
        // Should be called with values that are already parsed
        updateRestrictions: function(newRange) {
            const existingModel = this.selectedParametricValues
                .find(this.isTargetModel.bind(this));
            const existingRange = existingModel
                ? existingModel.get('range')
                : [this.model.get('min'), this.model.get('max')];

            const newAttributes = {
                field: this.fieldName,
                displayName: this.displayName,
                type: this.type,
                range: _.map(newRange, function(value, index) {
                    // Explicitly check null since 0 is falsy
                    return value === null
                        ? existingRange[index]
                        : value;
                })
            };

            // Fixes error where user could manually input min > max or max < min
            if(newAttributes.range[0] > newAttributes.range[1]) {
                if(existingRange.reduce(sum) - newAttributes.range.reduce(sum) > 0) { // if max was decreased
                    newAttributes.range[0] = newAttributes.range[1]; //set min to equal max
                } else { // if min was increased
                    newAttributes.range[1] = newAttributes.range[0]; //set max to equal min
                }
            }

            if(existingModel) {
                existingModel.set(newAttributes);
            } else {
                this.selectedParametricValues.add(newAttributes);
            }
        },

        clearRestrictions: function() {
            this.selectedParametricValues.remove(
                this.selectedParametricValues
                    .filter(this.isTargetModel.bind(this))
            );
        },

        fetchBuckets: function() {
            if(!(this.collapseModel && this.collapseModel.get('collapsed'))) {
                const width = this.$('.numeric-parametric-chart-row').width();

                // If the SVG has no width or there are no values, there is no point fetching new data
                if(!(width === 0 || this.model.get('totalValues') === 0)) {
                    // Exclude any restrictions for this field from the field text
                    const otherSelectedValues = this.selectedParametricValues
                        .reject(this.isTargetModel.bind(this))
                        .map(function(model) {
                            return model.toJSON();
                        });

                    this.bucketModel.fetch({
                        data: {
                            queryText: this.queryModel.get('queryText'),
                            fieldText: toFieldTextNode(otherSelectedValues),
                            minDate: this.queryModel.getIsoDate('minDate'),
                            maxDate: this.queryModel.getIsoDate('maxDate'),
                            minScore: this.queryModel.get('minScore'),
                            databases: this.queryModel.get('indexes'),
                            targetNumberOfBuckets: Math.floor(width / this.pixelsPerBucket),
                            bucketMin: this.model.get('currentMin'),
                            bucketMax: this.model.get('currentMax')
                        }
                    });
                }
            }
        },

        delayedFetchBucketsAndUpdate: function() {
            this.updateGraph();
            this.delayedFetchBuckets();
        },

        readMinInput: function() {
            return this.parseValue(this.$minInput.val());
        },

        readMaxInput: function() {
            return this.parseValue(this.$maxInput.val());
        },

        updateMinInput: function(newValue) {
            if(this.selectionEnabled) {
                this.$minInput.val(this.formatValue(newValue));
            }
        },

        updateMaxInput: function(newValue) {
            if(this.selectionEnabled) {
                this.$maxInput.val(this.formatValue(newValue));
            }
        },

        isTargetModel: function(model) {
            return model.get('field') === this.fieldName &&
                model.get('range') &&
                model.get('type') === this.type;
        }
    }, {
        dateInputTemplate: _.template(dateInputTemplate),
        numericInputTemplate: _.template(numericInputTemplate),
        defaultFormatting: {
            format: rounder().round,
            parse: Number,
            parseBoundarySelection: _.identity,
            render: _.noop
        },
        dateFormatting: {
            format: function(unformattedString) {
                return moment(Math.round(unformattedString * 1000)).format(datePicker.DATE_WIDGET_FORMAT);
            },
            parse: function(formattedString) {
                return moment(formattedString, datePicker.DATE_WIDGET_FORMAT).unix();
            },
            parseBoundarySelection: function(input) {
                // Epoch seconds are not decimal
                return Math.round(input);
            },
            render: function($el) {
                datePicker.render($el.find('.results-filter-date[data-date-attribute="min-date"]'),
                    function() {
                        this.updateRestrictions([this.readMinInput(), null]);
                    }.bind(this));
                datePicker.render($el.find('.results-filter-date[data-date-attribute="max-date"]'),
                    function() {
                        this.updateRestrictions([null, this.readMaxInput()]);
                    }.bind(this));
            }
        }
    });

    return NumericParametricFieldView;
});
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import { Popover } from 'zent';
import PropTypes from 'prop-types';
import isEqual from 'lodash/isEqual';
import { withRouter } from 'react-router-dom';
import isEmpty from 'lodash/isEmpty';

import ResultList from './ResultList';
import makeSearcher from './search';
import './style.pcss';
import { SKIP_SCROLL } from './constants';

const i18n = {
  'zh-CN': {
    placeholder: '搜索组件...',
  },

  'en-US': {
    placeholder: 'Search components...',
  },
};

class SearchBox extends Component {
  static propTypes = {
    locale: PropTypes.string.isRequired,
    navData: PropTypes.array.isRequired,
  };

  state = {
    keyword: '',
    activeIndex: SKIP_SCROLL,
    matches: [],
    resultVisible: false,
  };

  constructor(props) {
    super(props);
    this.buildLUT(props.navData);
  }

  componentWillReceiveProps(nextProps) {
    if (!isEqual(nextProps.navData, this.props.navData)) {
      this.buildLUT();
    }
  }

  render() {
    const { keyword, matches, activeIndex, resultVisible } = this.state;
    const { locale } = this.props;

    return (
      <Popover
        position={Popover.Position.BottomLeft}
        visible={resultVisible}
        cushion={5}
        onVisibleChange={this.onResultVisibleChange.bind(this)}
        display="inline-block"
        wrapperClassName="zandoc-react-search-box-popover-wrapper"
      >
        <Popover.Trigger.Click>
          <input
            className="zandoc-react-search-box-input"
            placeholder={i18n[locale].placeholder}
            value={keyword}
            onChange={this.onKeywordChange.bind(this)}
            onClick={this.onInputClick.bind(this)}
            onKeyDown={this.onKeydown.bind(this)}
          />
        </Popover.Trigger.Click>

        <Popover.Content>
          <ResultList
            matches={matches}
            activeIndex={activeIndex}
            locale={locale}
            redirectToResult={this.redirectToResult.bind(this)}
            clearActiveIndex={this.clearActiveIndex.bind(this)}
          />
        </Popover.Content>
      </Popover>
    );
  }

  // react-hot-loader rewrites this function into an infinite loop... lol
  // if we use arrow function here
  onKeywordChange(evt) {
    const keyword = evt.target.value;
    if (keyword !== this.state.keyword) {
      this.search(keyword);
    }
  }

  onKeydown(evt) {
    const { key } = evt;

    if (key === 'Enter') {
      return setTimeout(() => {
        const { matches } = this.state;
        if (!isEmpty(matches)) {
          let { activeIndex } = this.state;
          if (activeIndex < 0) {
            activeIndex = 0;
          }
          this.redirectToResult(matches[activeIndex]);
        }
      }, 0);
    }

    if (key !== 'ArrowDown' && key !== 'ArrowUp') {
      return;
    }

    const { resultVisible } = this.state;
    if (!resultVisible) {
      this.setState({
        activeIndex: 0,
        resultVisible: true,
      });
      return;
    }

    // Scroll list with arrow keys
    let { activeIndex } = this.state;
    if (activeIndex === SKIP_SCROLL) {
      activeIndex = -1;
    }

    if (key === 'ArrowDown') {
      activeIndex++;
    } else if (key === 'ArrowUp') {
      activeIndex--;
    } else {
      return;
    }
    const { matches } = this.state;
    const maxIndex = matches.length - 1;
    if (activeIndex < 0) {
      activeIndex = maxIndex;
    } else if (activeIndex > maxIndex) {
      activeIndex = 0;
    }

    this.setState({
      activeIndex,
      resultVisible: true,
    });
  }

  onResultVisibleChange(visible) {
    this.setState({
      resultVisible: visible,
    });
  }

  onInputClick() {
    this.search(this.state.keyword);
  }

  clearActiveIndex() {
    this.setState({
      activeIndex: SKIP_SCROLL,
    });
  }

  buildLUT(navData) {
    // Only include components
    const { groups } = navData[1];
    const data = groups.reduce(
      // eslint-disable-next-line
      (lut, grp) =>
        // eslint-disable-next-line
        grp.list.reduce((lut, item) => {
          lut.push(item);
          return lut;
        }, lut),
      []
    );

    data.sort((a, b) => {
      if (a.title > b.title) {
        return 1;
      } else if (a.title === b.title) {
        return 0;
      }

      return -1;
    });

    this.lut = makeSearcher(data);
  }

  search(keyword) {
    if (!this.lut) {
      return;
    }

    const matches = this.lut.search(keyword);

    this.setState({
      keyword,
      resultVisible: true,
      matches,
      activeIndex: 0,
    });
  }

  redirectToResult(item) {
    const { path } = item;
    const { history, locale } = this.props;
    const prefix = locale.split('-')[0];

    history.replace(`/${prefix}/${path}`);
    this.onResultVisibleChange(false);
  }
}

export default withRouter(SearchBox);
<commit_msg>fix: use zent searchInput (#741)<commit_after>import React, { Component } from 'react';
import { Popover, SearchInput } from 'zent';
import PropTypes from 'prop-types';
import isEqual from 'lodash/isEqual';
import { withRouter } from 'react-router-dom';
import isEmpty from 'lodash/isEmpty';

import ResultList from './ResultList';
import makeSearcher from './search';
import './style.pcss';
import { SKIP_SCROLL } from './constants';

const i18n = {
  'zh-CN': {
    placeholder: '搜索组件...',
  },

  'en-US': {
    placeholder: 'Search components...',
  },
};

class SearchBox extends Component {
  static propTypes = {
    locale: PropTypes.string.isRequired,
    navData: PropTypes.array.isRequired,
  };

  state = {
    keyword: '',
    activeIndex: SKIP_SCROLL,
    matches: [],
    resultVisible: false,
  };

  constructor(props) {
    super(props);
    this.buildLUT(props.navData);
  }

  componentWillReceiveProps(nextProps) {
    if (!isEqual(nextProps.navData, this.props.navData)) {
      this.buildLUT();
    }
  }

  render() {
    const { keyword, matches, activeIndex, resultVisible } = this.state;
    const { locale } = this.props;

    return (
      <Popover
        position={Popover.Position.BottomLeft}
        visible={resultVisible}
        cushion={5}
        onVisibleChange={this.onResultVisibleChange.bind(this)}
        display="inline-block"
        wrapperClassName="zandoc-react-search-box-popover-wrapper"
      >
        <Popover.Trigger.Click>
          <SearchInput
            className="zandoc-react-search-box-input"
            placeholder={i18n[locale].placeholder}
            value={keyword}
            onChange={this.onKeywordChange.bind(this)}
            onClick={this.onInputClick.bind(this)}
            onKeyDown={this.onKeydown.bind(this)}
          />
        </Popover.Trigger.Click>

        <Popover.Content>
          <ResultList
            matches={matches}
            activeIndex={activeIndex}
            locale={locale}
            redirectToResult={this.redirectToResult.bind(this)}
            clearActiveIndex={this.clearActiveIndex.bind(this)}
          />
        </Popover.Content>
      </Popover>
    );
  }

  // react-hot-loader rewrites this function into an infinite loop... lol
  // if we use arrow function here
  onKeywordChange(evt) {
    const keyword = evt.target.value;
    if (keyword !== this.state.keyword) {
      this.search(keyword);
    }
  }

  onKeydown(evt) {
    const { key } = evt;

    if (key === 'Enter') {
      return setTimeout(() => {
        const { matches } = this.state;
        if (!isEmpty(matches)) {
          let { activeIndex } = this.state;
          if (activeIndex < 0) {
            activeIndex = 0;
          }
          this.redirectToResult(matches[activeIndex]);
        }
      }, 0);
    }

    if (key !== 'ArrowDown' && key !== 'ArrowUp') {
      return;
    }

    const { resultVisible } = this.state;
    if (!resultVisible) {
      this.setState({
        activeIndex: 0,
        resultVisible: true,
      });
      return;
    }

    // Scroll list with arrow keys
    let { activeIndex } = this.state;
    if (activeIndex === SKIP_SCROLL) {
      activeIndex = -1;
    }

    if (key === 'ArrowDown') {
      activeIndex++;
    } else if (key === 'ArrowUp') {
      activeIndex--;
    } else {
      return;
    }
    const { matches } = this.state;
    const maxIndex = matches.length - 1;
    if (activeIndex < 0) {
      activeIndex = maxIndex;
    } else if (activeIndex > maxIndex) {
      activeIndex = 0;
    }

    this.setState({
      activeIndex,
      resultVisible: true,
    });
  }

  onResultVisibleChange(visible) {
    this.setState({
      resultVisible: visible,
    });
  }

  onInputClick() {
    this.search(this.state.keyword);
  }

  clearActiveIndex() {
    this.setState({
      activeIndex: SKIP_SCROLL,
    });
  }

  buildLUT(navData) {
    // Only include components
    const { groups } = navData[1];
    const data = groups.reduce(
      // eslint-disable-next-line
      (lut, grp) =>
        // eslint-disable-next-line
        grp.list.reduce((lut, item) => {
          lut.push(item);
          return lut;
        }, lut),
      []
    );

    data.sort((a, b) => {
      if (a.title > b.title) {
        return 1;
      } else if (a.title === b.title) {
        return 0;
      }

      return -1;
    });

    this.lut = makeSearcher(data);
  }

  search(keyword) {
    if (!this.lut) {
      return;
    }

    const matches = this.lut.search(keyword);

    this.setState({
      keyword,
      resultVisible: true,
      matches,
      activeIndex: 0,
    });
  }

  redirectToResult(item) {
    const { path } = item;
    const { history, locale } = this.props;
    const prefix = locale.split('-')[0];

    history.replace(`/${prefix}/${path}`);
    this.onResultVisibleChange(false);
  }
}

export default withRouter(SearchBox);
<|endoftext|>


<commit_before>'use strict'
const slapp = require('../../slackSetup.js').slapp
const getRadius = require('../../radius.js').getRadius
const tinyurl = require('tinyurl');
const routeFuncs = require('./routesIndex.js')

//db imports
const firebase    = require('../../firebaseSetup.js'),
      db = firebase.db,
      snaps = firebase.snaps,
      tags = firebase.tags,
      users = firebase.users

function snapsByGeo (lat,lng, msg, state){
    //firebase search by snap lat (start at bottom of circle, end at top)
    let radius = getRadius(lat,lng) //test: snap #1055

    let snapLat = snaps
      .orderByChild('lat')
      .startAt(radius[5].lat + "-") // "-"makes a string: required for query
      .endAt(radius[1].lat + "-")
      .once('value')
      .then(function(snapObj) {
        let count = 0
        snapObj.forEach(function(data) {
          let snap = data.val()
          // console.log(snap)
          //if returns lng within radius (east/west)
            //  console.log(snap.lng,'>',radius[0].lng,snap.lat)
            // console.log(snap.lng,'<',radius[3].lng)
          if (snap.lng >= radius[0].lng && snap.lng <= radius[3].lng) {
            console.log(snap.lng,'>',radius[0].lng)
            console.log(snap.lng,'<',radius[3].lng)
            count ++
            let thisCount = count
            let callback = function(picUrl){
              msg.say({
                  text: '',
                  attachments:[{
                    title: `${snap.description}`,
                    color: 'warning',
                    image_url: `${picUrl}`,
                    thumb_url: `${picUrl}`,
                    text: `${snap.address}`,
                    footer:`Jaywalk: ${thisCount}`
                  }]
              }) //end msg.say
              // .route('relaventAsk', (msg,state),60)
            }
            tinyurl.shorten(snap.picture, function(res) {
              // callback(res)
            })
          } //end if (lng checker)
        }) //end foreach
      }) //end .then(snap)
} // end snapsByGeo()

module.exports = {
  snapsByGeo: snapsByGeo
}<commit_msg>asdgf<commit_after>'use strict'
const slapp = require('../../slackSetup.js').slapp
const getRadius = require('../../radius.js').getRadius
const tinyurl = require('tinyurl');
const routeFuncs = require('./routesIndex.js')

//db imports
const firebase    = require('../../firebaseSetup.js'),
      db = firebase.db,
      snaps = firebase.snaps,
      tags = firebase.tags,
      users = firebase.users

function snapsByGeo (lat,lng, msg, state){
    //firebase search by snap lat (start at bottom of circle, end at top)
    let radius = getRadius(lat,lng) //test: snap #1055

    let snapLat = snaps
      .orderByChild('lat')
      .startAt(radius[5].lat + "-") // "-"makes a string: required for query
      .endAt(radius[1].lat + "-")
      .once('value')
      .then(function(snapObj) {
        let count = 0
        snapObj.forEach(function(data) {
          let snap = data.val()
          // console.log(snap)
          //if returns lng within radius (east/west)
            //  console.log(snap.lng,'>',radius[0].lng,snap.lat)
            // console.log(snap.lng,'<',radius[3].lng)
            console.log(count)
            count++
          if (snap.lng >= radius[0].lng && snap.lng <= radius[3].lng) {
            console.log(snap.lng,'>',radius[0].lng)
            console.log(snap.lng,'<',radius[3].lng)
            count ++
            let thisCount = count
            let callback = function(picUrl){
              msg.say({
                  text: '',
                  attachments:[{
                    title: `${snap.description}`,
                    color: 'warning',
                    image_url: `${picUrl}`,
                    thumb_url: `${picUrl}`,
                    text: `${snap.address}`,
                    footer:`Jaywalk: ${thisCount}`
                  }]
              }) //end msg.say
              // .route('relaventAsk', (msg,state),60)
            }
            tinyurl.shorten(snap.picture, function(res) {
              // callback(res)
            })
          } //end if (lng checker)
        }) //end foreach
      }) //end .then(snap)
} // end snapsByGeo()

module.exports = {
  snapsByGeo: snapsByGeo
}<|endoftext|>


<commit_before>import { mat4, mat3, vec3 } from 'gl-matrix';

class EnvironmentRenderer
{
    constructor(webgl)
    {
        const gl = webgl.context;

        this.indexBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([
            1, 2, 0,
            2, 3, 0,
            6, 2, 1,
            1, 5, 6,
            6, 5, 4,
            4, 7, 6,
            6, 3, 2,
            7, 3, 6,
            3, 7, 0,
            7, 4, 0,
            5, 1, 0,
            4, 5, 0
        ]), gl.STATIC_DRAW);

        this.vertexBuffer = gl.createBuffer()
        gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
            -1, -1, -1,
             1, -1, -1,
             1,  1, -1,
            -1,  1, -1,
            -1, -1,  1,
             1, -1,  1,
             1,  1,  1,
            -1,  1,  1
        ]), gl.STATIC_DRAW);
    }

    drawEnvironmentMap(webGl, viewProjectionMatrix, state, shaderCache, fragDefines)
    {
        if (state.environment == undefined || state.renderingParameters.renderEnvironmentMap == false)
        {
            return;
        }

        const gl = webGl.context;

        const vertShader = shaderCache.selectShader("cubemap.vert", []);
        const fragShader = shaderCache.selectShader("cubemap.frag", fragDefines);
        const shader = shaderCache.getShaderProgram(vertShader, fragShader);

        gl.useProgram(shader.program);
        webGl.setTexture(shader.getUniformLocation("u_specularEnvSampler"), state.environment, state.environment.specularEnvMap, 0);
        shader.updateUniform("u_MipCount", state.environment.mipCount);
        shader.updateUniform("u_envBlurNormalized", state.renderingParameters.blurEnvironmentMap ? 0.2 : 0.0);

        shader.updateUniform("u_ViewProjectionMatrix", viewProjectionMatrix);
        shader.updateUniform("u_Exposure", state.renderingParameters.exposure, false);

        let rotMatrix4 = mat4.create();
        mat4.rotateY(rotMatrix4, rotMatrix4,  state.renderingParameters.environmentRotation / 180.0 * Math.PI);
        let rotMatrix3 = mat3.create();
        mat3.fromMat4(rotMatrix3, rotMatrix4);
        shader.updateUniform("u_envRotation", rotMatrix3);

        gl.frontFace(gl.CCW);
        gl.enable(gl.CULL_FACE);
        gl.disable(gl.BLEND);
        gl.disable(gl.DEPTH_TEST);

        const positionAttributeLocation = shader.getAttributeLocation("a_position");
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
        gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
        gl.vertexAttribPointer(positionAttributeLocation, 3, gl.FLOAT, false, 0, 0);
        gl.enableVertexAttribArray(positionAttributeLocation);
        gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0);

        gl.enable(gl.DEPTH_TEST);
    }
}

export { EnvironmentRenderer }
<commit_msg>increase environment blur<commit_after>import { mat4, mat3, vec3 } from 'gl-matrix';

class EnvironmentRenderer
{
    constructor(webgl)
    {
        const gl = webgl.context;

        this.indexBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([
            1, 2, 0,
            2, 3, 0,
            6, 2, 1,
            1, 5, 6,
            6, 5, 4,
            4, 7, 6,
            6, 3, 2,
            7, 3, 6,
            3, 7, 0,
            7, 4, 0,
            5, 1, 0,
            4, 5, 0
        ]), gl.STATIC_DRAW);

        this.vertexBuffer = gl.createBuffer()
        gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
            -1, -1, -1,
             1, -1, -1,
             1,  1, -1,
            -1,  1, -1,
            -1, -1,  1,
             1, -1,  1,
             1,  1,  1,
            -1,  1,  1
        ]), gl.STATIC_DRAW);
    }

    drawEnvironmentMap(webGl, viewProjectionMatrix, state, shaderCache, fragDefines)
    {
        if (state.environment == undefined || state.renderingParameters.renderEnvironmentMap == false)
        {
            return;
        }

        const gl = webGl.context;

        const vertShader = shaderCache.selectShader("cubemap.vert", []);
        const fragShader = shaderCache.selectShader("cubemap.frag", fragDefines);
        const shader = shaderCache.getShaderProgram(vertShader, fragShader);

        gl.useProgram(shader.program);
        webGl.setTexture(shader.getUniformLocation("u_specularEnvSampler"), state.environment, state.environment.specularEnvMap, 0);
        shader.updateUniform("u_MipCount", state.environment.mipCount);
        shader.updateUniform("u_envBlurNormalized", state.renderingParameters.blurEnvironmentMap ? 0.6 : 0.0);

        shader.updateUniform("u_ViewProjectionMatrix", viewProjectionMatrix);
        shader.updateUniform("u_Exposure", state.renderingParameters.exposure, false);

        let rotMatrix4 = mat4.create();
        mat4.rotateY(rotMatrix4, rotMatrix4,  state.renderingParameters.environmentRotation / 180.0 * Math.PI);
        let rotMatrix3 = mat3.create();
        mat3.fromMat4(rotMatrix3, rotMatrix4);
        shader.updateUniform("u_envRotation", rotMatrix3);

        gl.frontFace(gl.CCW);
        gl.enable(gl.CULL_FACE);
        gl.disable(gl.BLEND);
        gl.disable(gl.DEPTH_TEST);

        const positionAttributeLocation = shader.getAttributeLocation("a_position");
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
        gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
        gl.vertexAttribPointer(positionAttributeLocation, 3, gl.FLOAT, false, 0, 0);
        gl.enableVertexAttribArray(positionAttributeLocation);
        gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0);

        gl.enable(gl.DEPTH_TEST);
    }
}

export { EnvironmentRenderer }
<|endoftext|>


<commit_before>import React from 'react';

function FinalistPopover({app, clickHandler}) {
  const classes = ['pn-finalists__popover'];
  if (!app.get('showPopover')) {
    return
;
  }

  const finalist = {
    age: setIfKeyPathExists(app, ['finalist', 'age']),
    images: setIfKeyPathExists(app, ['finalist', 'images']),
    weight: setIfKeyPathExists(app, ['finalist', 'weight']),
    inches: setIfKeyPathExists(app, ['finalist', 'inches']),
    bodyfat: setIfKeyPathExists(app, ['finalist', 'bodyfat']),
  };

  const images = ['front', 'side', 'back'].map(image => {
    return finalist.images[image] && (

        <img src={finalist.images[image]}
             key={image}
             onLoad={handleOnLoad}
             className={`pn-finalists__image pn-finalists__image--${image} pn-finalists__image--loading`} />

    );
  });

  return (

        {finalist.images &&

          {images}

}

              $250,000 Finalist

            {finalist.age &&

              Age:
              {finalist.age} years

}

            {finalist.weight &&

              Weight Lost:
              {roundToNearestTenth(finalist.weight.start - finalist.weight.end)} lbs
              (from {finalist.weight.start} lbs to {finalist.weight.end} lbs)

}

            {finalist.bodyfat &&

              % Body Fat Lost:
              {roundToNearestTenth(finalist.bodyfat.start - finalist.bodyfat.end)}%
              (from {finalist.bodyfat.start}% to {finalist.bodyfat.end}%)

}

            {finalist.inches &&

              Total Inches Lost:
              {roundToNearestTenth(finalist.inches.start - finalist.inches.end)} inches
              (from {finalist.inches.start} inches to {finalist.inches.end} inches)

}

        {String.fromCodePoint(0x00D7)}

  );
}

export default FinalistPopover;

function handleOnLoad(event) {
  event.target.classList.remove('pn-finalists__image--loading');
}

function setIfKeyPathExists(app, keyPath) {
  if (app.hasIn(keyPath)) {
    return app.getIn(keyPath);
  }

  return false;
}

function roundToNearestTenth(num) {
  return Math.round(num * 10) / 10;
}
<commit_msg>feat(bodyfat): hide empty bodyfat values<commit_after>import React from 'react';

function FinalistPopover({app, clickHandler}) {
  const classes = ['pn-finalists__popover'];
  if (!app.get('showPopover')) {
    return
;
  }

  const finalist = {
    age: setIfKeyPathExists(app, ['finalist', 'age']),
    images: setIfKeyPathExists(app, ['finalist', 'images']),
    weight: setIfKeyPathExists(app, ['finalist', 'weight']),
    inches: setIfKeyPathExists(app, ['finalist', 'inches']),
    bodyfat: setIfKeyPathExists(app, ['finalist', 'bodyfat']),
  };

  const images = ['front', 'side', 'back'].map(image => {
    return finalist.images[image] && (

        <img src={finalist.images[image]}
             key={image}
             onLoad={handleOnLoad}
             className={`pn-finalists__image pn-finalists__image--${image} pn-finalists__image--loading`} />

    );
  });

  return (

        {finalist.images &&

          {images}

}

              $250,000 Finalist

            {finalist.age &&

              Age:
              {finalist.age} years

}

            {finalist.weight &&

              Weight Lost:
              {roundToNearestTenth(finalist.weight.start - finalist.weight.end)} lbs
              (from {finalist.weight.start} lbs to {finalist.weight.end} lbs)

}

            {finalist.bodyfat > 0 &&

              % Body Fat Lost:
              {roundToNearestTenth(finalist.bodyfat.start - finalist.bodyfat.end)}%
              (from {finalist.bodyfat.start}% to {finalist.bodyfat.end}%)

}

            {finalist.inches &&

              Total Inches Lost:
              {roundToNearestTenth(finalist.inches.start - finalist.inches.end)} inches
              (from {finalist.inches.start} inches to {finalist.inches.end} inches)

}

        {String.fromCodePoint(0x00D7)}

  );
}

export default FinalistPopover;

function handleOnLoad(event) {
  event.target.classList.remove('pn-finalists__image--loading');
}

function setIfKeyPathExists(app, keyPath) {
  if (app.hasIn(keyPath)) {
    return app.getIn(keyPath);
  }

  return false;
}

function roundToNearestTenth(num) {
  return Math.round(num * 10) / 10;
}
<|endoftext|>